Files
PaperClipAI/server/src/services/plugin-environment-driver.ts
T
Nicky LeachandPaperclip e0b64529b3 feat(auth): normalize agent login in the sandbox onto one session table and a capability contract (#11730)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox agents need a safe login path for each supported adapter
> - Codex device login and Claude setup-token login used separate
session stores and route logic
> - Separate stores made session lookup, expiry, and login capability
checks harder to keep consistent
> - This pull request unifies both flows on one session table and one
capability contract
> - The benefit is one company-scoped login model with public session
identifiers and shared lifecycle rules

## Linked Issues or Issue Description

**Subsystem affected**

Cross-cutting (multiple of the above)

**Problem or motivation**

Codex and Claude sandbox login used separate session stores and
different route paths. This split increased the risk of inconsistent
company scoping, session lookup, and cleanup.

**Proposed solution**

Use `adapter_auth_sessions` for both login flows. Use public session
identifiers for API access. Select login behavior from projected adapter
capability data. Share the route spine, lease arguments, runner
lifecycle, and reaper rules.

**Alternatives considered**

Keep two session tables and add matching fixes to both routes. This
keeps duplicate logic and does not provide one capability contract, so
this pull request uses shared infrastructure.

**Roadmap alignment**

This change supports the shipped Cloud / Sandbox agents milestone in
`ROADMAP.md`.

## What Changed

- Unify Codex device login and Claude setup-token login on
`adapter_auth_sessions`.
- Return and look up sessions with company-scoped public session
identifiers.
- Enforce one active session for each company, owner, and adapter.
- Share the login route spine, sandbox lease arguments, runner
lifecycle, and missing-auth check.
- Add a standalone setup-token reaper with adapter-specific row
selection.
- Add optional login capability projection for adapters and drive route
and UI selection from that data.
- Rename the provider flag to `supportsLoginPty` and validate its
deprecated alias.
- Remove the old Claude setup-token session table and add the required
migrations.

## Verification

- Server typecheck passed with `tsc`.
- Database typecheck passed.
- UI typecheck passed with `tsc -b`.
- Codex login service and route suites passed.
- Setup-token session, route, and reaper suites passed.
- Adapter session schema, plugin validator, capability projection, UI
render, and Daytona suites passed.
- GitHub Actions must confirm the complete CI gate after pull request
creation.

## Risks

- The migrations remove short-lived in-flight login rows during
deployment. A login that spans the migration can continue until its
provider lease expires.
- The Codex credential store remains company-scoped. A cross-owner
credential race remains a documented, board-accepted risk.
- API clients that use internal session row identifiers no longer work.
The API accepts only public session identifiers.

## Model Used

Codex, GPT-5, exact runtime model ID not exposed in this handoff, large
context window, reasoning, and repository tool use. The implementing
engineer produced the code with AI assistance.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-19 11:51:31 -07:00

639 lines
23 KiB
TypeScript

import type { Db } from "@paperclipai/db";
import type {
EnvironmentProbeResult,
PluginEnvironmentConfig,
PluginEnvironmentDriverDeclaration,
} from "@paperclipai/shared";
import type {
PluginEnvironmentExecuteParams,
PluginEnvironmentExecuteResult,
PluginEnvironmentInteractiveSetupSession,
PluginEnvironmentStartInteractiveSetupParams,
PluginEnvironmentGetInteractiveSetupParams,
PluginEnvironmentCaptureTemplateParams,
PluginEnvironmentCaptureTemplateResult,
PluginEnvironmentCancelInteractiveSetupParams,
PluginEnvironmentCancelInteractiveSetupResult,
PluginEnvironmentDeleteTemplateParams,
PluginEnvironmentDeleteTemplateResult,
PluginEnvironmentLease,
PluginEnvironmentRealizeWorkspaceParams,
PluginEnvironmentRealizeWorkspaceResult,
} from "@paperclipai/plugin-sdk";
import { unprocessable } from "../errors.js";
import {
collectSecretRefPaths,
parseSecretRefBindingObject,
readConfigValueAtPath,
writeConfigValueAtPath,
} from "./json-schema-secret-refs.js";
import { pluginRegistryService } from "./plugin-registry.js";
import type { PluginWorkerManager } from "./plugin-worker-manager.js";
/**
* The worker methods a sandbox provider must advertise before the host reuses
* a provider lease across runs. The host resumes a lease through
* `environmentResumeLease`, ends it through `environmentReleaseLease`, and tears
* down a stale lease through `environmentDestroyLease`. The reuse path destroys
* the stale lease when a resume fails and then acquires a fresh lease, so a
* provider that omits any of the three methods can strand the stale lease and
* can never complete the reuse path.
*
* The runtime capability normalizer maps `reusableLeases` to these same methods,
* so the acquisition guard, the effective-capability snapshot, and the published
* provider-capabilities value all read one source and cannot drift.
*/
export const REUSABLE_LEASE_WORKER_METHODS = [
"environmentResumeLease",
"environmentReleaseLease",
"environmentDestroyLease",
] as const;
export interface ReadyPluginWorkerRecovery {
pluginKeys: readonly string[];
startWorker(plugin: { id: string; pluginKey: string }): Promise<boolean>;
timeoutMs?: number;
}
export interface ReadyPluginEnvironmentDriver {
pluginId: string;
pluginKey: string;
driverKey: string;
displayName: string;
description?: string;
configSchema: PluginEnvironmentDriverDeclaration["configSchema"];
supportsReusableLeases?: PluginEnvironmentDriverDeclaration["supportsReusableLeases"];
sandboxCapabilities?: PluginEnvironmentDriverDeclaration["sandboxCapabilities"];
/**
* The running worker for this exact plugin advertises ALL reusable-lease
* lifecycle methods (`REUSABLE_LEASE_WORKER_METHODS`: resume, release, and
* destroy). The published provider-capabilities value grants reusable leases
* only when the declaration allows them AND this flag is true, so presentation
* matches the acquisition guard, which also verifies the same methods live.
*/
reusableLeaseMethodsVerified: boolean;
supportsInteractiveSetup?: PluginEnvironmentDriverDeclaration["supportsInteractiveSetup"];
interactiveSetupConnectionTypes?: PluginEnvironmentDriverDeclaration["interactiveSetupConnectionTypes"];
supportsTemplateCapture?: PluginEnvironmentDriverDeclaration["supportsTemplateCapture"];
templateRefKind?: PluginEnvironmentDriverDeclaration["templateRefKind"];
templateConfigBinding?: PluginEnvironmentDriverDeclaration["templateConfigBinding"];
supportsTemplateDelete?: PluginEnvironmentDriverDeclaration["supportsTemplateDelete"];
supportsLoginPty?: PluginEnvironmentDriverDeclaration["supportsLoginPty"];
}
export function pluginDriverProviderKey(config: Pick<PluginEnvironmentConfig, "pluginKey" | "driverKey">): string {
return `${config.pluginKey}:${config.driverKey}`;
}
const DEFAULT_READY_PLUGIN_WORKER_RECOVERY_TIMEOUT_MS = 2_000;
async function resolveWithTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutValue: T): Promise<T> {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return await promise;
let timeout: ReturnType<typeof setTimeout> | null = null;
try {
return await Promise.race([
promise,
new Promise<T>((resolve) => {
timeout = setTimeout(() => resolve(timeoutValue), timeoutMs);
}),
]);
} finally {
if (timeout) clearTimeout(timeout);
}
}
export async function resolvePluginEnvironmentDriver(input: {
db: Db;
workerManager: PluginWorkerManager;
config: PluginEnvironmentConfig;
}) {
const pluginRegistry = pluginRegistryService(input.db);
const plugin = await pluginRegistry.getByKey(input.config.pluginKey);
if (!plugin || plugin.status !== "ready") {
throw new Error(`Plugin environment driver "${pluginDriverProviderKey(input.config)}" is not ready.`);
}
const driver = plugin.manifestJson.environmentDrivers?.find(
(candidate) => candidate.driverKey === input.config.driverKey,
);
if (!driver) {
throw new Error(`Plugin "${input.config.pluginKey}" does not declare environment driver "${input.config.driverKey}".`);
}
if (!input.workerManager.isRunning(plugin.id)) {
throw new Error(`Plugin environment driver "${pluginDriverProviderKey(input.config)}" has no running worker.`);
}
return { plugin, driver };
}
export async function resolvePluginEnvironmentDriverByKey(input: {
db: Db;
workerManager: PluginWorkerManager;
driverKey: string;
}) {
return await resolvePluginSandboxProviderDriverByKey({
db: input.db,
driverKey: input.driverKey,
workerManager: input.workerManager,
requireRunning: true,
});
}
export async function resolvePluginSandboxProviderDriverByKey(input: {
db: Db;
driverKey: string;
workerManager?: PluginWorkerManager;
requireRunning?: boolean;
}): Promise<{ plugin: Awaited<ReturnType<ReturnType<typeof pluginRegistryService>["list"]>>[number]; driver: PluginEnvironmentDriverDeclaration } | null> {
const pluginRegistry = pluginRegistryService(input.db);
const plugins = await pluginRegistry.list();
for (const plugin of plugins) {
const driver = plugin.manifestJson.environmentDrivers?.find(
(candidate) => candidate.driverKey === input.driverKey && candidate.kind === "sandbox_provider",
) as PluginEnvironmentDriverDeclaration | undefined;
if (!driver) continue;
if (input.requireRunning) {
if (plugin.status !== "ready") continue;
if (!input.workerManager?.isRunning(plugin.id)) continue;
}
return { plugin, driver };
}
return null;
}
/**
* Resolve the sandbox-provider driver declaration from one exact plugin id.
*
* A driver key is only unique inside a single manifest. Two installed plugins
* can declare the same driver key. A lease pins the plugin that acquired it
* through `metadata.pluginId`. Use this resolver, not the by-key resolver, when
* the caller must read the declaration from that exact plugin. The by-key
* resolver returns the first installed plugin with the key, which can be a
* different, even disabled, plugin.
*
* This resolver fails closed. It returns `null` when the plugin id is unknown,
* or when that plugin no longer declares a `sandbox_provider` driver with the
* given key.
*/
export async function resolvePluginSandboxProviderDriverById(input: {
db: Db;
pluginId: string;
driverKey: string;
}): Promise<{ plugin: Awaited<ReturnType<ReturnType<typeof pluginRegistryService>["getById"]>>; driver: PluginEnvironmentDriverDeclaration } | null> {
const pluginRegistry = pluginRegistryService(input.db);
const plugin = await pluginRegistry.getById(input.pluginId);
if (!plugin) return null;
const driver = plugin.manifestJson.environmentDrivers?.find(
(candidate) => candidate.driverKey === input.driverKey && candidate.kind === "sandbox_provider",
) as PluginEnvironmentDriverDeclaration | undefined;
if (!driver) return null;
return { plugin, driver };
}
export async function listReadyPluginEnvironmentDrivers(input: {
db: Db;
workerManager?: PluginWorkerManager;
recoverMissingWorker?: ReadyPluginWorkerRecovery;
}) {
if (!input.workerManager) return [];
const pluginRegistry = pluginRegistryService(input.db);
const plugins = await pluginRegistry.list();
const recoverablePluginKeys = new Set(input.recoverMissingWorker?.pluginKeys ?? []);
const readyPlugins = plugins.filter((plugin) => plugin.status === "ready");
const recoveryAttempts: Promise<boolean>[] = [];
for (const plugin of readyPlugins) {
const hasSandboxProviderDriver = plugin.manifestJson.environmentDrivers?.some(
(driver) => driver.kind === "sandbox_provider",
) ?? false;
const canRecover =
hasSandboxProviderDriver
&& !input.workerManager.isRunning(plugin.id)
&& recoverablePluginKeys.has(plugin.pluginKey)
&& !input.workerManager.getWorker(plugin.id);
if (!canRecover || !input.recoverMissingWorker) continue;
const timeoutMs =
input.recoverMissingWorker.timeoutMs ?? DEFAULT_READY_PLUGIN_WORKER_RECOVERY_TIMEOUT_MS;
recoveryAttempts.push(resolveWithTimeout(
input.recoverMissingWorker.startWorker({
id: plugin.id,
pluginKey: plugin.pluginKey,
}).catch(() => false),
timeoutMs,
false,
));
}
if (recoveryAttempts.length > 0) {
await Promise.all(recoveryAttempts);
}
const rows: ReadyPluginEnvironmentDriver[] = [];
for (const plugin of readyPlugins) {
if (!input.workerManager.isRunning(plugin.id)) {
continue;
}
// The plugin is running, so read the live worker's verified methods once per
// plugin. A provider advertises reusable leases only when its worker carries
// all reuse lifecycle methods; the declaration alone never grants them.
const workerMethods = new Set(input.workerManager.getWorker(plugin.id)?.supportedMethods ?? []);
const reusableLeaseMethodsVerified = REUSABLE_LEASE_WORKER_METHODS.every(
(method) => workerMethods.has(method),
);
rows.push(
...(plugin.manifestJson.environmentDrivers ?? [])
.filter((driver) => driver.kind === "sandbox_provider")
.map((driver) => ({
pluginId: plugin.id,
pluginKey: plugin.pluginKey,
driverKey: driver.driverKey,
displayName: driver.displayName,
description: driver.description,
configSchema: driver.configSchema,
supportsReusableLeases: driver.supportsReusableLeases,
sandboxCapabilities: driver.sandboxCapabilities,
reusableLeaseMethodsVerified,
supportsInteractiveSetup: driver.supportsInteractiveSetup,
interactiveSetupConnectionTypes: driver.interactiveSetupConnectionTypes,
supportsTemplateCapture: driver.supportsTemplateCapture,
templateRefKind: driver.templateRefKind,
templateConfigBinding: driver.templateConfigBinding,
supportsTemplateDelete: driver.supportsTemplateDelete,
supportsLoginPty: driver.supportsLoginPty,
})),
);
}
return rows;
}
export async function validatePluginSandboxProviderConfig(input: {
db: Db;
workerManager: PluginWorkerManager;
provider: string;
config: Record<string, unknown>;
}): Promise<{
normalizedConfig: Record<string, unknown>;
pluginId: string;
pluginKey: string;
driver: PluginEnvironmentDriverDeclaration;
}> {
const resolved = await resolvePluginSandboxProviderDriverByKey({
db: input.db,
driverKey: input.provider,
workerManager: input.workerManager,
requireRunning: true,
});
if (!resolved) {
throw unprocessable(`Sandbox provider "${input.provider}" is not installed or its plugin worker is not running.`);
}
// Secret pickers submit `{ type: "secret_ref", secretId, version }` binding
// objects for `format: "secret-ref"` fields. Plugins only understand string
// config values, so canonicalize bindings to the bare secret id (the
// persisted shape) before the plugin validates.
const configSchema =
resolved.driver.configSchema && typeof resolved.driver.configSchema === "object" && !Array.isArray(resolved.driver.configSchema)
? resolved.driver.configSchema as Record<string, unknown>
: null;
let config = input.config;
for (const path of collectSecretRefPaths(configSchema)) {
const binding = parseSecretRefBindingObject(readConfigValueAtPath(config, path));
if (!binding) continue;
if (binding.version !== "latest") {
throw unprocessable(
`Secret binding at ${path} pins version ${binding.version}; sandbox provider secret references always resolve the latest version.`,
);
}
config = writeConfigValueAtPath(config, path, binding.secretId);
}
const result = await input.workerManager.call(resolved.plugin.id, "environmentValidateConfig", {
driverKey: input.provider,
config,
});
if (!result.ok) {
throw unprocessable(
result.errors?.[0] ?? `Sandbox provider "${input.provider}" rejected its config.`,
{
errors: result.errors ?? [],
warnings: result.warnings ?? [],
},
);
}
return {
normalizedConfig: result.normalizedConfig ?? config,
pluginId: resolved.plugin.id,
pluginKey: resolved.plugin.pluginKey,
driver: resolved.driver,
};
}
export async function validatePluginEnvironmentDriverConfig(input: {
db: Db;
workerManager: PluginWorkerManager;
config: PluginEnvironmentConfig;
}): Promise<PluginEnvironmentConfig> {
const { plugin } = await resolvePluginEnvironmentDriver(input);
const result = await input.workerManager.call(plugin.id, "environmentValidateConfig", {
driverKey: input.config.driverKey,
config: input.config.driverConfig,
});
if (!result.ok) {
throw unprocessable(
result.errors?.[0] ?? `Plugin environment driver "${pluginDriverProviderKey(input.config)}" rejected its config.`,
{
errors: result.errors ?? [],
warnings: result.warnings ?? [],
},
);
}
return {
...input.config,
driverConfig: result.normalizedConfig ?? input.config.driverConfig,
};
}
export async function probePluginEnvironmentDriver(input: {
db: Db;
workerManager: PluginWorkerManager;
companyId: string;
environmentId: string;
config: PluginEnvironmentConfig;
}): Promise<EnvironmentProbeResult> {
const { plugin } = await resolvePluginEnvironmentDriver(input);
const result = await input.workerManager.call(plugin.id, "environmentProbe", {
driverKey: input.config.driverKey,
companyId: input.companyId,
environmentId: input.environmentId,
config: input.config.driverConfig,
}, 120_000);
return {
ok: result.ok,
driver: "plugin",
summary: result.summary ?? `Plugin environment driver "${pluginDriverProviderKey(input.config)}" probe ${result.ok ? "passed" : "failed"}.`,
details: {
pluginKey: input.config.pluginKey,
driverKey: input.config.driverKey,
diagnostics: result.diagnostics ?? [],
metadata: result.metadata ?? {},
},
};
}
export async function probePluginSandboxProviderDriver(input: {
db: Db;
workerManager: PluginWorkerManager;
companyId: string;
environmentId: string;
provider: string;
config: Record<string, unknown>;
}): Promise<EnvironmentProbeResult> {
const resolved = await resolvePluginEnvironmentDriverByKey({
db: input.db,
workerManager: input.workerManager,
driverKey: input.provider,
});
if (!resolved) {
return {
ok: false,
driver: "sandbox",
summary: `Sandbox provider "${input.provider}" is not installed or its plugin worker is not running.`,
details: {
provider: input.provider,
},
};
}
const { provider: _provider, ...driverConfig } = input.config;
const result = await input.workerManager.call(resolved.plugin.id, "environmentProbe", {
driverKey: input.provider,
companyId: input.companyId,
environmentId: input.environmentId,
config: driverConfig,
}, 120_000);
return {
ok: result.ok,
driver: "sandbox",
summary: result.summary ?? `Sandbox provider "${input.provider}" probe ${result.ok ? "passed" : "failed"}.`,
details: {
provider: input.provider,
pluginKey: resolved.plugin.pluginKey,
diagnostics: result.diagnostics ?? [],
metadata: result.metadata ?? {},
},
};
}
export async function resumePluginEnvironmentLease(input: {
db: Db;
workerManager: PluginWorkerManager;
companyId: string;
environmentId: string;
issueId?: string | null;
config: PluginEnvironmentConfig;
providerLeaseId: string;
leaseMetadata?: Record<string, unknown>;
}): Promise<PluginEnvironmentLease> {
const { plugin } = await resolvePluginEnvironmentDriver(input);
return await input.workerManager.call(plugin.id, "environmentResumeLease", {
driverKey: input.config.driverKey,
companyId: input.companyId,
environmentId: input.environmentId,
issueId: input.issueId ?? null,
config: input.config.driverConfig,
providerLeaseId: input.providerLeaseId,
leaseMetadata: input.leaseMetadata,
});
}
export async function destroyPluginEnvironmentLease(input: {
db: Db;
workerManager: PluginWorkerManager;
companyId: string;
environmentId: string;
issueId?: string | null;
config: PluginEnvironmentConfig;
providerLeaseId: string | null;
leaseMetadata?: Record<string, unknown>;
}): Promise<void> {
const { plugin } = await resolvePluginEnvironmentDriver(input);
await input.workerManager.call(plugin.id, "environmentDestroyLease", {
driverKey: input.config.driverKey,
companyId: input.companyId,
environmentId: input.environmentId,
issueId: input.issueId ?? null,
config: input.config.driverConfig,
providerLeaseId: input.providerLeaseId,
leaseMetadata: input.leaseMetadata,
});
}
export async function realizePluginEnvironmentWorkspace(input: {
db: Db;
workerManager: PluginWorkerManager;
pluginId?: string | null;
params: PluginEnvironmentRealizeWorkspaceParams;
config: PluginEnvironmentConfig;
}): Promise<PluginEnvironmentRealizeWorkspaceResult> {
const { plugin } = input.pluginId
? { plugin: { id: input.pluginId } }
: await resolvePluginEnvironmentDriver({
db: input.db,
workerManager: input.workerManager,
config: input.config,
});
return await input.workerManager.call(plugin.id, "environmentRealizeWorkspace", input.params);
}
export async function executePluginEnvironmentCommand(input: {
db: Db;
workerManager: PluginWorkerManager;
pluginId?: string | null;
params: PluginEnvironmentExecuteParams;
config: PluginEnvironmentConfig;
}): Promise<PluginEnvironmentExecuteResult> {
const { plugin } = input.pluginId
? { plugin: { id: input.pluginId } }
: await resolvePluginEnvironmentDriver({
db: input.db,
workerManager: input.workerManager,
config: input.config,
});
return await input.workerManager.call(
plugin.id,
"environmentExecute",
input.params,
resolvePluginExecuteRpcTimeoutMs({
requestedTimeoutMs: input.params.timeoutMs,
config: input.config.driverConfig,
}),
);
}
export async function startPluginEnvironmentInteractiveSetup(input: {
db: Db;
workerManager: PluginWorkerManager;
config: PluginEnvironmentConfig;
params: Omit<PluginEnvironmentStartInteractiveSetupParams, "driverKey" | "config">;
}): Promise<PluginEnvironmentInteractiveSetupSession> {
const { plugin } = await resolvePluginEnvironmentDriver({
db: input.db,
workerManager: input.workerManager,
config: input.config,
});
return await input.workerManager.call(plugin.id, "environmentStartInteractiveSetup", {
...input.params,
driverKey: input.config.driverKey,
config: input.config.driverConfig,
}, resolvePluginExecuteRpcTimeoutMs({
requestedTimeoutMs: undefined,
config: input.config.driverConfig,
}));
}
export async function getPluginEnvironmentInteractiveSetup(input: {
db: Db;
workerManager: PluginWorkerManager;
config: PluginEnvironmentConfig;
params: Omit<PluginEnvironmentGetInteractiveSetupParams, "driverKey" | "config">;
}): Promise<PluginEnvironmentInteractiveSetupSession> {
const { plugin } = await resolvePluginEnvironmentDriver({
db: input.db,
workerManager: input.workerManager,
config: input.config,
});
return await input.workerManager.call(plugin.id, "environmentGetInteractiveSetup", {
...input.params,
driverKey: input.config.driverKey,
config: input.config.driverConfig,
}, resolvePluginExecuteRpcTimeoutMs({
requestedTimeoutMs: undefined,
config: input.config.driverConfig,
}));
}
export async function capturePluginEnvironmentTemplate(input: {
db: Db;
workerManager: PluginWorkerManager;
config: PluginEnvironmentConfig;
params: Omit<PluginEnvironmentCaptureTemplateParams, "driverKey" | "config">;
}): Promise<PluginEnvironmentCaptureTemplateResult> {
const { plugin } = await resolvePluginEnvironmentDriver({
db: input.db,
workerManager: input.workerManager,
config: input.config,
});
return await input.workerManager.call(plugin.id, "environmentCaptureTemplate", {
...input.params,
driverKey: input.config.driverKey,
config: input.config.driverConfig,
}, resolvePluginExecuteRpcTimeoutMs({
requestedTimeoutMs: input.params.timeoutMs ?? undefined,
config: input.config.driverConfig,
}));
}
export async function cancelPluginEnvironmentInteractiveSetup(input: {
db: Db;
workerManager: PluginWorkerManager;
config: PluginEnvironmentConfig;
params: Omit<PluginEnvironmentCancelInteractiveSetupParams, "driverKey" | "config">;
}): Promise<PluginEnvironmentCancelInteractiveSetupResult> {
const { plugin } = await resolvePluginEnvironmentDriver({
db: input.db,
workerManager: input.workerManager,
config: input.config,
});
return await input.workerManager.call(plugin.id, "environmentCancelInteractiveSetup", {
...input.params,
driverKey: input.config.driverKey,
config: input.config.driverConfig,
}, resolvePluginExecuteRpcTimeoutMs({
requestedTimeoutMs: undefined,
config: input.config.driverConfig,
}));
}
export async function deletePluginEnvironmentTemplate(input: {
db: Db;
workerManager: PluginWorkerManager;
config: PluginEnvironmentConfig;
params: Omit<PluginEnvironmentDeleteTemplateParams, "driverKey" | "config">;
}): Promise<PluginEnvironmentDeleteTemplateResult> {
const { plugin } = await resolvePluginEnvironmentDriver({
db: input.db,
workerManager: input.workerManager,
config: input.config,
});
return await input.workerManager.call(plugin.id, "environmentDeleteTemplate", {
...input.params,
driverKey: input.config.driverKey,
config: input.config.driverConfig,
}, resolvePluginExecuteRpcTimeoutMs({
requestedTimeoutMs: undefined,
config: input.config.driverConfig,
}));
}
const RPC_OVERHEAD_BUFFER_MS = 30_000;
export function resolvePluginExecuteRpcTimeoutMs(input: {
requestedTimeoutMs?: number;
config: Record<string, unknown>;
}): number | undefined {
let baseMs: number | undefined;
if (Number.isFinite(input.requestedTimeoutMs) && (input.requestedTimeoutMs ?? 0) > 0) {
baseMs = Math.trunc(input.requestedTimeoutMs!);
} else {
const configTimeoutMs = typeof input.config.timeoutMs === "number" ? input.config.timeoutMs : null;
if (configTimeoutMs && Number.isFinite(configTimeoutMs) && configTimeoutMs > 0) {
baseMs = Math.trunc(configTimeoutMs);
}
}
return baseMs != null ? baseMs + RPC_OVERHEAD_BUFFER_MS : undefined;
}