From b6854e61c7ce7869d34f407968d16f3c3e5b9134 Mon Sep 17 00:00:00 2001 From: Nicky Leach Date: Tue, 25 Aug 2026 07:13:55 -0700 Subject: [PATCH] refactor(adapter-utils): rename EffectiveSandboxCapabilities to EffectiveExecutionCapabilities (#12119) ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The adapter utilities package defines shared types for agent execution targets > - The type name EffectiveSandboxCapabilities describes only one transport > - All execution target drivers return the same resolved capability snapshot > - This pull request gives the snapshot a general name and keeps the old type as a deprecated alias > - The benefit is clearer public vocabulary with source compatibility for current consumers ## Linked Issues or Issue Description **What existing behavior does this improve?** The exported capability snapshot type uses the name `EffectiveSandboxCapabilities`, although local, SSH, sandbox, and plugin drivers return it. **Subsystem affected** The change affects `packages/adapter-utils` and its server consumers. **Current behavior** The public type name points to the sandbox transport. The private parser also uses the sandbox-only name. **Proposed behavior** Use `EffectiveExecutionCapabilities` for the public type and `parseEffectiveExecutionCapabilities` for the private parser. Keep a deprecated alias for the old public type. **Reason and benefit** The new name matches the established execution-target vocabulary. The alias keeps existing type imports working during the migration. **Breaking changes** None. The runtime field, capability flags, parsed shape, and package versions do not change. **Additional context** GitHub search found no duplicate or related open issue or pull request. ## What Changed - Rename the exported interface to `EffectiveExecutionCapabilities`. - Keep `EffectiveSandboxCapabilities` as a deprecated type alias. - Rename the private parser and update its call site and references. - Add a type-level test for the deprecated alias. ## Verification - `npx tsc --noEmit -p packages/adapter-utils` - `npx vitest run packages/adapter-utils/src/execution-target-sandbox.test.ts` - `npx vitest run server/src/__tests__/environment-execution-target-capabilities.test.ts server/src/__tests__/environment-execution-target-duplex.test.ts` - The local checks passed with 133 adapter-utils tests and 31 server tests. - Reviewers can confirm that the runtime field and capability flags stay unchanged. ## Risks Low risk. The alias protects existing type imports. The change does not alter runtime behavior or serialized data. ## Model Used OpenAI Codex, GPT-5, tool use and code execution. The runtime does not expose the context window size. ## 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 --- .../src/execution-target-sandbox.test.ts | 23 ++++++++++++++++- .../adapter-utils/src/execution-target.ts | 25 ++++++++++++------- ...ment-execution-target-capabilities.test.ts | 10 ++++---- ...nvironment-execution-target-duplex.test.ts | 8 +++--- server/src/services/environment-runtime.ts | 10 ++++---- 5 files changed, 52 insertions(+), 24 deletions(-) diff --git a/packages/adapter-utils/src/execution-target-sandbox.test.ts b/packages/adapter-utils/src/execution-target-sandbox.test.ts index 5c3f86c03a..90fcb45b76 100644 --- a/packages/adapter-utils/src/execution-target-sandbox.test.ts +++ b/packages/adapter-utils/src/execution-target-sandbox.test.ts @@ -34,6 +34,7 @@ import { startAdapterExecutionTargetProcessSessionBridge, startAdapterExecutionTargetPaperclipBridge, type AdapterSandboxExecutionTarget, + type EffectiveExecutionCapabilities, type EffectiveSandboxCapabilities, } from "./execution-target.js"; import { @@ -3102,7 +3103,7 @@ describe("sandbox adapter execution targets", () => { // The full effective-capability snapshot with one flag set. The two strict // gates read `duplexCommandStream`; the other flags stay false. - function duplexCapabilities(duplexCommandStream: boolean): EffectiveSandboxCapabilities { + function duplexCapabilities(duplexCommandStream: boolean): EffectiveExecutionCapabilities { return { reusableLeases: false, nativeSyncIn: false, @@ -6900,3 +6901,23 @@ describe("duplex readiness gate replay-buffer reservation", () => { expect(counts.underflows).toBe(0); }); }); + +describe("EffectiveSandboxCapabilities deprecated alias", () => { + it("still type-checks as EffectiveExecutionCapabilities", () => { + // A type-level check, not a runtime one: this assignment fails to compile + // if the alias drifts from the renamed interface. Keep it here so a later + // removal of the alias is a deliberate act, not an accident. + const snapshot: EffectiveExecutionCapabilities = { + reusableLeases: false, + nativeSyncIn: false, + nativeSyncOut: false, + persistentProcessSessions: false, + independentControlCommands: false, + incrementalSessionOutput: false, + concurrentSyncOperations: false, + duplexCommandStream: false, + }; + const aliased: EffectiveSandboxCapabilities = snapshot; + expect(aliased).toEqual(snapshot); + }); +}); diff --git a/packages/adapter-utils/src/execution-target.ts b/packages/adapter-utils/src/execution-target.ts index a974f39453..d8f740a25d 100644 --- a/packages/adapter-utils/src/execution-target.ts +++ b/packages/adapter-utils/src/execution-target.ts @@ -137,13 +137,14 @@ export interface AdapterSshExecutionTarget extends AdapterExecutionTargetWorkspa } /** - * Read-only snapshot of the effective sandbox capabilities for one execution - * target. Each flag is the resolved result of the provider's declaration, the - * live worker's verified methods, and any narrowing from the config or lease. - * The host computes it once and attaches it to the target; a consumer reads it - * but never changes it, so every field is `readonly`. + * Read-only snapshot of the effective execution capabilities for one + * execution target — local, ssh, sandbox, or plugin. Each flag is the + * resolved result of the provider's declaration, the live worker's verified + * methods, and any narrowing from the config or lease. The host computes it + * once and attaches it to the target; a consumer reads it but never changes + * it, so every field is `readonly`. */ -export interface EffectiveSandboxCapabilities { +export interface EffectiveExecutionCapabilities { readonly reusableLeases: boolean; readonly nativeSyncIn: boolean; readonly nativeSyncOut: boolean; @@ -154,6 +155,12 @@ export interface EffectiveSandboxCapabilities { readonly duplexCommandStream: boolean; } +/** + * @deprecated Renamed to `EffectiveExecutionCapabilities`. This alias will + * be removed in a later major release. + */ +export interface EffectiveSandboxCapabilities extends EffectiveExecutionCapabilities {} + export interface AdapterSandboxExecutionTarget extends AdapterExecutionTargetWorkspaceMetadata { kind: "remote"; transport: "sandbox"; @@ -163,7 +170,7 @@ export interface AdapterSandboxExecutionTarget extends AdapterExecutionTargetWor * resolves it from the provider declaration ∩ the verified worker methods ∩ * narrowing, then attaches it here. Absent when no snapshot was resolved. */ - readonly effectiveCapabilities?: EffectiveSandboxCapabilities | null; + readonly effectiveCapabilities?: EffectiveExecutionCapabilities | null; /** * Per-run duplex bridge kill switch. The host stamps it on the same seam as * `effectiveCapabilities`. `true` selects the duplex transport only when the @@ -345,7 +352,7 @@ function readString(value: unknown): string | null { // missing or non-boolean field reads as `false`, so a round-tripped target // never grants a capability that the snapshot did not carry. Returns null when // there is no object to read. -function parseEffectiveSandboxCapabilities(value: unknown): EffectiveSandboxCapabilities | null { +function parseEffectiveExecutionCapabilities(value: unknown): EffectiveExecutionCapabilities | null { const parsed = parseObject(value); if (Object.keys(parsed).length === 0) return null; return { @@ -1311,7 +1318,7 @@ export function parseAdapterExecutionTarget(value: unknown): AdapterExecutionTar if (kind === "remote" && readStringMeta(parsed, "transport") === "sandbox") { const remoteCwd = readStringMeta(parsed, "remoteCwd"); if (!remoteCwd) return null; - const effectiveCapabilities = parseEffectiveSandboxCapabilities(parsed.effectiveCapabilities); + const effectiveCapabilities = parseEffectiveExecutionCapabilities(parsed.effectiveCapabilities); return { kind: "remote", transport: "sandbox", diff --git a/server/src/__tests__/environment-execution-target-capabilities.test.ts b/server/src/__tests__/environment-execution-target-capabilities.test.ts index 9a2785ad9d..b9cb1245cb 100644 --- a/server/src/__tests__/environment-execution-target-capabilities.test.ts +++ b/server/src/__tests__/environment-execution-target-capabilities.test.ts @@ -8,11 +8,11 @@ vi.mock("../services/environment-config.js", () => ({ resolveEnvironmentDriverConfigForRuntime: mockResolveEnvironmentDriverConfigForRuntime, })); -import type { EffectiveSandboxCapabilities } from "@paperclipai/adapter-utils/execution-target"; +import type { EffectiveExecutionCapabilities } from "@paperclipai/adapter-utils/execution-target"; import { resolveEnvironmentExecutionTarget } from "../services/environment-execution-target.js"; import type { EnvironmentRuntimeService } from "../services/environment-runtime.js"; -const SNAPSHOT: EffectiveSandboxCapabilities = { +const SNAPSHOT: EffectiveExecutionCapabilities = { reusableLeases: true, nativeSyncIn: true, nativeSyncOut: false, @@ -27,7 +27,7 @@ const SNAPSHOT: EffectiveSandboxCapabilities = { // A snapshot that grants every capability. A test overrides one flag to prove // that the removed capability alone changes the runtime decision. -const FULL_GRANT: EffectiveSandboxCapabilities = { +const FULL_GRANT: EffectiveExecutionCapabilities = { reusableLeases: true, nativeSyncIn: true, nativeSyncOut: true, @@ -42,7 +42,7 @@ const FULL_GRANT: EffectiveSandboxCapabilities = { // `supportsSync` result. The helper returns the sandbox target so a test reads // the runner and the streaming flag the snapshot gates. async function buildSandboxTarget(input: { - snapshot: EffectiveSandboxCapabilities | null; + snapshot: EffectiveExecutionCapabilities | null; supportsSync: boolean; config?: Record; // Reject the capability resolution to exercise the fail-closed error path. @@ -128,7 +128,7 @@ describe("resolveEnvironmentExecutionTarget effective capability snapshot", () = // The snapshot is read-only: it is frozen, so a write does not change it. expect(Object.isFrozen(target.effectiveCapabilities)).toBe(true); - const snapshot = target.effectiveCapabilities as EffectiveSandboxCapabilities; + const snapshot = target.effectiveCapabilities as EffectiveExecutionCapabilities; try { (snapshot as { reusableLeases: boolean }).reusableLeases = false; } catch { diff --git a/server/src/__tests__/environment-execution-target-duplex.test.ts b/server/src/__tests__/environment-execution-target-duplex.test.ts index 0343b7e254..a0d7b2f2d9 100644 --- a/server/src/__tests__/environment-execution-target-duplex.test.ts +++ b/server/src/__tests__/environment-execution-target-duplex.test.ts @@ -20,7 +20,7 @@ vi.mock("../services/plugin-environment-driver.js", async (importActual) => ({ resolvePluginSandboxProviderDriverById: mockResolvePluginSandboxProviderDriverById, })); -import type { EffectiveSandboxCapabilities } from "@paperclipai/adapter-utils/execution-target"; +import type { EffectiveExecutionCapabilities } from "@paperclipai/adapter-utils/execution-target"; import { adapterExecutionTargetDuplexAggregateByteLedger } from "@paperclipai/adapter-utils/execution-target"; import { DEFAULT_MAX_AGGREGATE_DUPLEX_ROUTE_BYTES, @@ -44,7 +44,7 @@ import type { // A snapshot that grants the opt-in duplex capability, plus the rest true so the // gate reads only the duplex flag. -const DUPLEX_GRANT: EffectiveSandboxCapabilities = { +const DUPLEX_GRANT: EffectiveExecutionCapabilities = { reusableLeases: true, nativeSyncIn: true, nativeSyncOut: true, @@ -54,7 +54,7 @@ const DUPLEX_GRANT: EffectiveSandboxCapabilities = { duplexCommandStream: true, }; -const DUPLEX_ABSENT: EffectiveSandboxCapabilities = { +const DUPLEX_ABSENT: EffectiveExecutionCapabilities = { ...DUPLEX_GRANT, duplexCommandStream: false, }; @@ -346,7 +346,7 @@ describe("sandbox execution target aggregate byte ledger stamp", () => { // environment runtime whose openDuplexChannel is a spy. The helper returns the // runner and the spy so a test reads the capability-gated member. async function buildSandboxRunner(input: { - snapshot: EffectiveSandboxCapabilities | null; + snapshot: EffectiveExecutionCapabilities | null; }) { mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({ driver: "sandbox", diff --git a/server/src/services/environment-runtime.ts b/server/src/services/environment-runtime.ts index a87f517ff5..ebb90b6e87 100644 --- a/server/src/services/environment-runtime.ts +++ b/server/src/services/environment-runtime.ts @@ -14,7 +14,7 @@ import type { SandboxProviderCapabilities, } from "@paperclipai/shared"; import { resolveDeclaredSandboxCapabilities } from "@paperclipai/shared"; -import type { EffectiveSandboxCapabilities } from "@paperclipai/adapter-utils/execution-target"; +import type { EffectiveExecutionCapabilities } from "@paperclipai/adapter-utils/execution-target"; import type { CommandManagedDuplexChannel, } from "@paperclipai/adapter-utils/command-managed-runtime"; @@ -259,7 +259,7 @@ export function classifyEnvironmentCapabilities(input: { declared?: Partial | null; narrowing?: Partial> | null; supportedCapabilities?: ReadonlySet | null; -}): EffectiveSandboxCapabilities { +}): EffectiveExecutionCapabilities { const verifiedMethods = new Set(input.verifiedMethods ?? []); const declared = input.declared ?? {}; const narrowing = input.narrowing ?? {}; @@ -606,7 +606,7 @@ export interface EnvironmentRuntimeDriver { * "this driver has no capability model" apart from "this driver is not * registered" reads {@link ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT} directly. */ - resolveCapabilities(input: EnvironmentDriverLeaseInput): Promise; + resolveCapabilities(input: EnvironmentDriverLeaseInput): Promise; /** * Retry the provider teardown for an orphan sandbox that an earlier acquire * provisioned but could not tear down. The pending-cleanup lease row carries @@ -2592,7 +2592,7 @@ function createSandboxEnvironmentDriver( */ async function resolveSandboxCapabilitiesForLease( input: EnvironmentDriverLeaseInput, - ): Promise { + ): Promise { const metadata = input.lease.metadata ?? {}; const providerKey = readString(metadata.provider) ?? @@ -3719,7 +3719,7 @@ export function environmentRuntimeService( */ async resolveCapabilities( input: EnvironmentDriverLeaseInput, - ): Promise { + ): Promise { const driver = getDriver(getLeaseDriverKey(input.lease, input.environment)); if (!driver) return null; return await driver.resolveCapabilities(input);