refactor(environment): classify environment capabilities from static driver definitions (#12045)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Environment runtime drivers provide workspace, lease, and custom
image behavior
> - Runtime code used driver identity checks and several
capability-specific members
> - These checks spread capability rules across the runtime and made new
drivers harder to verify
> - This pull request adds one general capability classifier and one
static driver support table
> - The benefit is one fail-closed capability model that keeps current
behavior and supports future drivers

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Environment runtime capability checks for workspace realization, custom
images, lease capabilities, and duplex authorization.

**Subsystem affected**

Cross-cutting (multiple of the above)

**Current behavior**

The runtime selects several capability paths from driver identity and
separate capability members. Custom image gates also trust provider
declarations without checking every matching live worker method.

**Proposed behavior**

The runtime uses one general capability classifier and one static
support table. Custom image gates require both the provider declaration
and every matching live worker method. The public capability names
remain unchanged.

**Reason and benefit**

The change keeps capability rules in one place. It removes identity
conditions from runtime consumers and makes unsupported drivers fail
closed.

**Breaking changes**

None. The public names sandboxCapabilities, sandboxProviders, and
EffectiveSandboxCapabilities remain available.

## What Changed

- Add classifyEnvironmentCapabilities and static support definitions for
all four driver families.
- Add resolveCapabilities to every environment runtime driver.
- Move driver traits into environment-driver-traits.ts and migrate
runtime consumers.
- Require provider declarations and matching live worker methods for all
custom image gates.
- Migrate duplex authorization to the general resolver and remove the
dead sandbox-only member.
- Delete the unused resolveEffectiveSandboxCapabilities wrapper and
update its test.

## Verification

- pnpm --filter @paperclipai/server typecheck
- pnpm exec vitest run
server/src/__tests__/environment-capability-contract.test.ts
server/src/__tests__/environment-runtime.test.ts — 92 tests pass
- pnpm exec vitest run
server/src/__tests__/environment-driver-traits.test.ts
server/src/__tests__/general-capability-classifier.test.ts — 12 tests
pass
- pnpm exec vitest run
server/src/__tests__/environment-custom-images-service.test.ts
server/src/__tests__/environment-execution-target-capabilities.test.ts
server/src/__tests__/environment-execution-target-duplex.test.ts
server/src/__tests__/environment-execution-target-duplex-kill-switch.test.ts
— 66 tests pass

## Risks

The main risk is a capability gate that denies a valid driver or permits
an invalid driver. The static support matrix, live worker method checks,
and regression tests reduce this risk. No database, public API, or
published type name changes.

## Model Used

OpenAI Codex, GPT-5, with tool use and code execution. The deployment
does not provide a separate context-window value.

## 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 (for example, docs/... or
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>
This commit is contained in:
Nicky LeachandPaperclip authored and GitHub committed 2026-08-23 21:24:59 -07:00
1 parent fc9e9b704f
commit a14e51d592
20 files changed
+1126 -212

No files matched your search

@@ -1,9 +1,10 @@
import { describe, expect, it } from "vitest";
import {
ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT,
SANDBOX_CAPABILITY_KEYS,
buildSandboxCapabilityNarrowing,
builtinSandboxProviderVerifiedMethods,
resolveEffectiveSandboxCapabilities,
classifyEnvironmentCapabilities,
} from "../services/environment-runtime.js";
// The worker verbs a fully-capable plug-in provider advertises.
@@ -17,12 +18,12 @@ const ALL_PLUGIN_METHODS = [
"environmentSyncOut",
];
describe("sandbox capability contract normalizer", () => {
describe("environment capability contract normalizer", () => {
it("test_absent_declaration_defers_to_worker_supported_methods_discovery", () => {
// No declaration at all. The effective set must fall back to what the worker
// verified, so a third-party provider that implements the sync hooks keeps
// native sync without declaring it.
const effective = resolveEffectiveSandboxCapabilities({
const effective = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentSyncIn", "environmentSyncOut"],
declared: null,
});
@@ -41,7 +42,7 @@ describe("sandbox capability contract normalizer", () => {
independentControlCommands: false,
nativeSyncIn: true,
};
const effective = resolveEffectiveSandboxCapabilities({ verifiedMethods, declared });
const effective = classifyEnvironmentCapabilities({ verifiedMethods, declared });
// Verified + declared true.
expect(effective.persistentProcessSessions).toBe(true);
@@ -53,7 +54,7 @@ describe("sandbox capability contract normalizer", () => {
// Every effective capability must be a subset of the verified set and the
// declaration: an effective `true` never appears where the worker did not
// verify or the declaration set `false`.
const verifiedOnly = resolveEffectiveSandboxCapabilities({ verifiedMethods });
const verifiedOnly = classifyEnvironmentCapabilities({ verifiedMethods });
for (const key of SANDBOX_CAPABILITY_KEYS) {
if (effective[key]) {
expect(verifiedOnly[key]).toBe(true);
@@ -67,7 +68,7 @@ describe("sandbox capability contract normalizer", () => {
leasePolicy: "ephemeral",
leaseMetadata: { backend: "job" },
});
const effective = resolveEffectiveSandboxCapabilities({
const effective = classifyEnvironmentCapabilities({
verifiedMethods: ALL_PLUGIN_METHODS,
declared: { nativeSyncIn: true, nativeSyncOut: true },
narrowing,
@@ -101,7 +102,7 @@ describe("sandbox capability contract normalizer", () => {
// A normal lease adds no persistent-session narrowing.
expect(narrowing.persistentProcessSessions).toBeUndefined();
const effective = resolveEffectiveSandboxCapabilities({
const effective = classifyEnvironmentCapabilities({
verifiedMethods,
declared,
narrowing,
@@ -124,7 +125,7 @@ describe("sandbox capability contract normalizer", () => {
});
expect(narrowing.persistentProcessSessions).toBe(false);
const effective = resolveEffectiveSandboxCapabilities({
const effective = classifyEnvironmentCapabilities({
verifiedMethods,
declared,
narrowing,
@@ -150,13 +151,13 @@ describe("sandbox capability contract normalizer", () => {
supportsReusableLeases: true,
execute: () => undefined,
});
const builtinEffective = resolveEffectiveSandboxCapabilities({
const builtinEffective = classifyEnvironmentCapabilities({
verifiedMethods: builtinMethods,
declared,
});
// A plug-in provider that advertises the equivalent verbs.
const pluginEffective = resolveEffectiveSandboxCapabilities({
const pluginEffective = classifyEnvironmentCapabilities({
verifiedMethods: [
"environmentResumeLease",
"environmentReleaseLease",
@@ -175,7 +176,7 @@ describe("sandbox capability contract normalizer", () => {
expect(builtinEffective.nativeSyncIn).toBe(false);
// A built-in provider without an execute method verifies no exec capability.
const noExec = resolveEffectiveSandboxCapabilities({
const noExec = classifyEnvironmentCapabilities({
verifiedMethods: builtinSandboxProviderVerifiedMethods({ supportsReusableLeases: false }),
declared: { persistentProcessSessions: true },
});
@@ -186,7 +187,7 @@ describe("sandbox capability contract normalizer", () => {
// One case per capability: the declaration sets the flag `true`, the worker
// lacks a prerequisite verb, and the effective value stays `false`.
for (const key of SANDBOX_CAPABILITY_KEYS) {
const effective = resolveEffectiveSandboxCapabilities({
const effective = classifyEnvironmentCapabilities({
verifiedMethods: [],
declared: { [key]: true },
});
@@ -195,7 +196,7 @@ describe("sandbox capability contract normalizer", () => {
// A single missing prerequisite verb is enough: reusable leases needs
// resume, release, and destroy, so resume alone does not grant it.
const resumeOnly = resolveEffectiveSandboxCapabilities({
const resumeOnly = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentResumeLease"],
declared: { reusableLeases: true },
});
@@ -206,14 +207,14 @@ describe("sandbox capability contract normalizer", () => {
// A provider that verifies resume and release but not destroy is not
// eligible for reusable leases. The reuse path destroys a stale lease when a
// resume fails, so a provider without destroy support would strand the lease.
const resumeAndReleaseOnly = resolveEffectiveSandboxCapabilities({
const resumeAndReleaseOnly = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentResumeLease", "environmentReleaseLease"],
declared: { reusableLeases: true },
});
expect(resumeAndReleaseOnly.reusableLeases).toBe(false);
// Adding the destroy verb makes the same provider eligible.
const allReuseVerbs = resolveEffectiveSandboxCapabilities({
const allReuseVerbs = classifyEnvironmentCapabilities({
verifiedMethods: [
"environmentResumeLease",
"environmentReleaseLease",
@@ -231,7 +232,7 @@ describe("sandbox capability contract normalizer", () => {
// true, but `incrementalSessionOutput` must stay false because the provider
// did not declare the opt-in behavior. The session-output streaming gate
// reads `incrementalSessionOutput`, so this provider keeps the poll path.
const effective = resolveEffectiveSandboxCapabilities({
const effective = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentExecute"],
declared: {
persistentProcessSessions: true,
@@ -249,7 +250,7 @@ describe("sandbox capability contract normalizer", () => {
// An absent declaration denies the opt-in capability even when the worker
// verifies the prerequisite verb. This differs from a worker-property
// capability, which defers to the verified baseline.
const undeclared = resolveEffectiveSandboxCapabilities({
const undeclared = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentExecute"],
declared: null,
});
@@ -257,14 +258,14 @@ describe("sandbox capability contract normalizer", () => {
// A provider that declares the capability and verifies the prerequisite gets
// the streaming path.
const declared = resolveEffectiveSandboxCapabilities({
const declared = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentExecute"],
declared: { incrementalSessionOutput: true },
});
expect(declared.incrementalSessionOutput).toBe(true);
// A declaration never grants the capability without the verified verb.
const declaredButUnverified = resolveEffectiveSandboxCapabilities({
const declaredButUnverified = classifyEnvironmentCapabilities({
verifiedMethods: [],
declared: { incrementalSessionOutput: true },
});
@@ -282,7 +283,7 @@ describe("sandbox capability contract normalizer", () => {
});
expect(narrowing.incrementalSessionOutput).toBe(false);
const effective = resolveEffectiveSandboxCapabilities({
const effective = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentExecute"],
declared: { incrementalSessionOutput: true },
narrowing,
@@ -294,14 +295,14 @@ describe("sandbox capability contract normalizer", () => {
// Parallel bidirectional file sync is opt-in and direction-neutral. It needs
// both sync verbs, so a provider that verifies only one direction cannot get
// the capability. An absent declaration denies it even with both verbs.
const undeclared = resolveEffectiveSandboxCapabilities({
const undeclared = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentSyncIn", "environmentSyncOut"],
declared: null,
});
expect(undeclared.concurrentSyncOperations).toBe(false);
// A positive declaration with both verified verbs resolves true.
const bothVerbs = resolveEffectiveSandboxCapabilities({
const bothVerbs = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentSyncIn", "environmentSyncOut"],
declared: { concurrentSyncOperations: true },
});
@@ -309,7 +310,7 @@ describe("sandbox capability contract normalizer", () => {
// Only the inbound verb: the outbound prerequisite is missing, so it resolves
// false.
const inOnly = resolveEffectiveSandboxCapabilities({
const inOnly = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentSyncIn"],
declared: { concurrentSyncOperations: true },
});
@@ -317,7 +318,7 @@ describe("sandbox capability contract normalizer", () => {
// Only the outbound verb: the inbound prerequisite is missing, so it resolves
// false.
const outOnly = resolveEffectiveSandboxCapabilities({
const outOnly = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentSyncOut"],
declared: { concurrentSyncOperations: true },
});
@@ -329,7 +330,7 @@ describe("sandbox capability contract normalizer", () => {
// the capability even when the worker verifies the duplex open verb. This
// matches the incremental-session-output pattern: an opt-in behavioral
// guarantee needs a positive declaration, not just a verified verb.
const undeclared = resolveEffectiveSandboxCapabilities({
const undeclared = classifyEnvironmentCapabilities({
verifiedMethods: ["duplexChannelOpen"],
declared: null,
});
@@ -340,7 +341,7 @@ describe("sandbox capability contract normalizer", () => {
// A declaration never grants the capability without the verified duplex open
// verb. A provider that declares the capability but whose worker does not
// report the duplex open method resolves false.
const declaredButUnverified = resolveEffectiveSandboxCapabilities({
const declaredButUnverified = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentExecute"],
declared: { duplexCommandStream: true },
});
@@ -350,7 +351,7 @@ describe("sandbox capability contract normalizer", () => {
it("test_duplex_command_stream_declared_and_verified_resolves_true_but_narrowing_removes_it", () => {
// A provider that declares the capability and whose worker verifies the
// duplex open verb gets the capability.
const granted = resolveEffectiveSandboxCapabilities({
const granted = classifyEnvironmentCapabilities({
verifiedMethods: ["duplexChannelOpen"],
declared: { duplexCommandStream: true },
});
@@ -358,7 +359,7 @@ describe("sandbox capability contract normalizer", () => {
// Per-target narrowing still removes a verified and declared capability, so a
// lease that cannot use the duplex channel keeps the file bridge.
const narrowed = resolveEffectiveSandboxCapabilities({
const narrowed = classifyEnvironmentCapabilities({
verifiedMethods: ["duplexChannelOpen"],
declared: { duplexCommandStream: true },
narrowing: { duplexCommandStream: false },
@@ -378,10 +379,103 @@ describe("sandbox capability contract normalizer", () => {
};
for (const verifiedMethods of [null, undefined, [] as string[]]) {
const effective = resolveEffectiveSandboxCapabilities({ verifiedMethods, declared: declaredAll });
const effective = classifyEnvironmentCapabilities({ verifiedMethods, declared: declaredAll });
for (const key of SANDBOX_CAPABILITY_KEYS) {
expect(effective[key]).toBe(false);
}
}
});
});
describe("general runtime capability resolver — four-driver matrix", () => {
// A declaration that would grant every capability, paired with a worker
// method list that verifies every prerequisite. Used to probe each driver's
// static support ceiling: whatever the driver family cannot support must
// stay `false` even under the most permissive declaration and worker.
const DECLARE_ALL = {
reusableLeases: true,
nativeSyncIn: true,
nativeSyncOut: true,
persistentProcessSessions: true,
independentControlCommands: true,
incrementalSessionOutput: true,
concurrentSyncOperations: true,
duplexCommandStream: true,
};
const VERIFY_ALL = [...ALL_PLUGIN_METHODS, "duplexChannelOpen"];
it("test_local_and_ssh_drivers_support_no_capability_regardless_of_declaration_or_worker", () => {
// The `local` and `ssh` static support definitions name none of the eight
// capabilities, so the classifier resolves every field `false` even with a
// full declaration and a fully verified worker.
for (const driver of ["local", "ssh"] as const) {
const effective = classifyEnvironmentCapabilities({
verifiedMethods: VERIFY_ALL,
declared: DECLARE_ALL,
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT[driver].supportedCapabilities,
});
for (const key of SANDBOX_CAPABILITY_KEYS) {
expect(effective[key]).toBe(false);
}
}
});
it("test_sandbox_and_plugin_drivers_support_the_whole_capability_set", () => {
// The `sandbox` and `plugin` static support definitions name every
// capability, so the classifier defers fully to the declaration, the
// verified worker methods, and the narrowing — the static gate adds no
// extra restriction for either driver.
for (const driver of ["sandbox", "plugin"] as const) {
const effective = classifyEnvironmentCapabilities({
verifiedMethods: VERIFY_ALL,
declared: DECLARE_ALL,
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT[driver].supportedCapabilities,
});
for (const key of SANDBOX_CAPABILITY_KEYS) {
expect(effective[key]).toBe(true);
}
// The static gate changes nothing versus the ungated normalizer for
// these two drivers, so the two results match field for field.
expect(effective).toEqual(
classifyEnvironmentCapabilities({ verifiedMethods: VERIFY_ALL, declared: DECLARE_ALL }),
);
}
});
it("test_sandbox_and_plugin_drivers_fail_closed_on_a_missing_worker_method_list", () => {
// A missing, undefined, or empty worker method list verifies no
// prerequisite, so every capability resolves `false` for a driver that
// supports the whole set, even under a full declaration. This is the
// fail-closed contract Phase 2 must keep for the live plugin worker path.
for (const driver of ["sandbox", "plugin"] as const) {
for (const verifiedMethods of [null, undefined, [] as string[]]) {
const effective = classifyEnvironmentCapabilities({
verifiedMethods,
declared: DECLARE_ALL,
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT[driver].supportedCapabilities,
});
for (const key of SANDBOX_CAPABILITY_KEYS) {
expect(effective[key]).toBe(false);
}
}
}
});
it("test_narrowing_still_removes_a_capability_the_static_support_and_declaration_both_grant", () => {
// Per-target narrowing stays a separate, later gate: it removes a
// capability that the static support, the verified worker, and the
// declaration all grant. This holds for every driver whose static support
// names the capability.
for (const driver of ["sandbox", "plugin"] as const) {
const effective = classifyEnvironmentCapabilities({
verifiedMethods: VERIFY_ALL,
declared: DECLARE_ALL,
narrowing: { duplexCommandStream: false },
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT[driver].supportedCapabilities,
});
expect(effective.duplexCommandStream).toBe(false);
// A capability the narrowing does not name is unaffected.
expect(effective.persistentProcessSessions).toBe(true);
}
});
});
@@ -66,7 +66,16 @@ function pluginManifest() {
} as const;
}
function createWorkerManager() {
const ALL_CUSTOM_IMAGE_WORKER_METHODS = [
"environmentStartInteractiveSetup",
"environmentGetInteractiveSetup",
"environmentCancelInteractiveSetup",
"environmentCaptureTemplate",
"environmentDeleteTemplate",
] as const;
function createWorkerManager(options?: { workerMethods?: readonly string[] }) {
const supportedMethods = options?.workerMethods ?? ALL_CUSTOM_IMAGE_WORKER_METHODS;
const call = vi.fn(async (_pluginId: string, method: string, params: Record<string, unknown>) => {
if (method === "environmentStartInteractiveSetup") {
return {
@@ -134,6 +143,7 @@ function createWorkerManager() {
return {
call,
isRunning: vi.fn(() => true),
getWorker: vi.fn(() => ({ supportedMethods: [...supportedMethods] })),
} as unknown as PluginWorkerManager & { call: typeof call };
}
@@ -391,6 +401,99 @@ describeEmbeddedPostgres("environmentCustomImageService", () => {
expect(timedOut?.status).toBe("timed_out");
});
it("rejects interactive setup when the provider declares it but the worker omits a setup method", async () => {
const { environmentId } = await seed();
// The manifest declares supportsInteractiveSetup, but the worker reports
// none of the three setup methods. The gate must name the first missing one.
const workerManager = createWorkerManager({
workerMethods: ["environmentCaptureTemplate", "environmentDeleteTemplate"],
});
const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager });
await expect(service.startSetupSession({
environmentId,
actor: { userId: "user-1" },
})).rejects.toThrow('does not report the "environmentStartInteractiveSetup" method');
expect(workerManager.call).not.toHaveBeenCalled();
});
it("rejects template capture when the provider declares it but the worker omits the capture method", async () => {
const { environmentId } = await seed();
// The worker reports every setup method, so the session starts and
// finishes the interactive part. It omits environmentCaptureTemplate, so
// finishing the session must fail at the capture gate.
const workerManager = createWorkerManager({
workerMethods: [
"environmentStartInteractiveSetup",
"environmentGetInteractiveSetup",
"environmentCancelInteractiveSetup",
"environmentDeleteTemplate",
],
});
const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager });
const started = await service.startSetupSession({
environmentId,
actor: { userId: "user-1" },
});
await expect(service.finishSetupSession({ sessionId: started.session.id }))
.rejects.toThrow('does not report the "environmentCaptureTemplate" method');
});
it("rejects template deletion when the provider declares it but the worker omits the delete method", async () => {
const { environmentId } = await seed();
// The worker reports every setup and capture method but omits
// environmentDeleteTemplate, so a delete-on-disable request must fail at
// the delete gate after the template is already captured.
const workerManager = createWorkerManager({
workerMethods: [
"environmentStartInteractiveSetup",
"environmentGetInteractiveSetup",
"environmentCancelInteractiveSetup",
"environmentCaptureTemplate",
],
});
const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager });
const started = await service.startSetupSession({
environmentId,
actor: { userId: "user-1" },
});
await service.finishSetupSession({ sessionId: started.session.id });
await expect(service.disableTemplate({
environmentId,
deleteProviderTemplate: true,
})).rejects.toThrow('does not report the "environmentDeleteTemplate" method');
});
it("passes every gate when the provider declares each flag and the worker reports every matching method", async () => {
const { environmentId } = await seed();
// The default worker manager reports all five methods, matching every
// flag the manifest declares. Every gate must pass.
const workerManager = createWorkerManager();
const service = environmentCustomImageService(db, { pluginWorkerManager: workerManager });
const started = await service.startSetupSession({
environmentId,
actor: { userId: "user-1" },
});
const promoted = await service.finishSetupSession({ sessionId: started.session.id });
expect(promoted.template.status).toBe("active");
const disabled = await service.disableTemplate({
environmentId,
deleteProviderTemplate: true,
});
expect(disabled.status).toBe("revoked");
expect(workerManager.call).toHaveBeenCalledWith(
expect.any(String),
"environmentDeleteTemplate",
expect.any(Object),
undefined,
);
});
it("rejects templates from another environment", async () => {
const { environmentId } = await seed();
const otherEnvironmentId = randomUUID();
@@ -0,0 +1,67 @@
import { describe, expect, it } from "vitest";
import {
ENVIRONMENT_DRIVER_TRAITS,
getEnvironmentDriverTraits,
type EnvironmentDriverTraits,
} from "../services/environment-driver-traits.js";
// The exact trait table this phase moves each of the four consumers onto. Each
// value is the value the current per-consumer driver-name condition produces
// today, so this table proves the refactor changes no decision.
const EXPECTED_TRAITS: Record<string, Omit<EnvironmentDriverTraits, "driver">> = {
local: {
realizesWorkspace: true,
runsWorkspaceOffHost: false,
confinesStagedProjects: false,
hasLeaseCapabilityModel: false,
},
ssh: {
realizesWorkspace: true,
runsWorkspaceOffHost: true,
confinesStagedProjects: false,
hasLeaseCapabilityModel: false,
},
sandbox: {
realizesWorkspace: true,
runsWorkspaceOffHost: true,
confinesStagedProjects: true,
hasLeaseCapabilityModel: true,
},
plugin: {
realizesWorkspace: false,
runsWorkspaceOffHost: true,
confinesStagedProjects: false,
hasLeaseCapabilityModel: false,
},
};
describe("environment driver traits", () => {
for (const [driver, expected] of Object.entries(EXPECTED_TRAITS)) {
it(`carries the exact trait row for "${driver}"`, () => {
const traits = ENVIRONMENT_DRIVER_TRAITS[driver as keyof typeof ENVIRONMENT_DRIVER_TRAITS];
expect(traits.driver).toBe(driver);
expect(traits.realizesWorkspace).toBe(expected.realizesWorkspace);
expect(traits.runsWorkspaceOffHost).toBe(expected.runsWorkspaceOffHost);
expect(traits.confinesStagedProjects).toBe(expected.confinesStagedProjects);
expect(traits.hasLeaseCapabilityModel).toBe(expected.hasLeaseCapabilityModel);
});
}
it("only the sandbox driver has a lease capability model today", () => {
const withModel = Object.values(ENVIRONMENT_DRIVER_TRAITS).filter(
(traits) => traits.hasLeaseCapabilityModel,
);
expect(withModel.map((traits) => traits.driver)).toEqual(["sandbox"]);
});
it("getEnvironmentDriverTraits resolves a registered driver", () => {
expect(getEnvironmentDriverTraits("sandbox")?.hasLeaseCapabilityModel).toBe(true);
expect(getEnvironmentDriverTraits("plugin")?.realizesWorkspace).toBe(false);
});
it("getEnvironmentDriverTraits returns null for an unknown or absent driver", () => {
expect(getEnvironmentDriverTraits("not-a-real-driver")).toBeNull();
expect(getEnvironmentDriverTraits(null)).toBeNull();
expect(getEnvironmentDriverTraits(undefined)).toBeNull();
});
});
@@ -66,7 +66,7 @@ async function buildSandboxTarget(input: {
supportsSync: () => input.supportsSync,
syncIn: vi.fn(),
syncOut: vi.fn(),
effectiveSandboxCapabilities: vi.fn(async () => {
resolveCapabilities: vi.fn(async () => {
if (input.rejectResolution) {
throw new Error("capability resolution failed");
}
@@ -102,10 +102,10 @@ describe("resolveEnvironmentExecutionTarget effective capability snapshot", () =
config: { provider: "daytona", reuseLease: true, timeoutMs: 30_000 },
});
const effectiveSandboxCapabilities = vi.fn(async () => Object.freeze({ ...SNAPSHOT }));
const resolveCapabilities = vi.fn(async () => Object.freeze({ ...SNAPSHOT }));
const environmentRuntime = {
supportsSync: () => false,
effectiveSandboxCapabilities,
resolveCapabilities,
} as unknown as EnvironmentRuntimeService;
const target = await resolveEnvironmentExecutionTarget({
@@ -123,7 +123,7 @@ describe("resolveEnvironmentExecutionTarget effective capability snapshot", () =
if (target?.kind !== "remote" || target.transport !== "sandbox") {
throw new Error("expected a sandbox target");
}
expect(effectiveSandboxCapabilities).toHaveBeenCalledTimes(1);
expect(resolveCapabilities).toHaveBeenCalledTimes(1);
expect(target.effectiveCapabilities).toEqual(SNAPSHOT);
// The snapshot is read-only: it is frozen, so a write does not change it.
@@ -160,6 +160,43 @@ describe("resolveEnvironmentExecutionTarget effective capability snapshot", () =
}
expect(target.effectiveCapabilities).toBeUndefined();
});
// The static `hasLeaseCapabilityModel` trait is `true` only for the
// `sandbox` driver in this phase (see `environment-driver-traits.ts`). An
// `ssh` lease must never reach the general resolver: the new
// `resolveCapabilities` method never returns `null` for a registered
// driver, so an ungated call would turn "no snapshot" into "every
// capability denied" for a driver this file does not otherwise gate on.
it("never calls the general capability resolver for an ssh lease", async () => {
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
driver: "ssh",
config: {
host: "example.test",
port: 22,
username: "agent",
remoteWorkspacePath: "/work",
},
});
const resolveCapabilities = vi.fn(async () => ({ ...SNAPSHOT }));
const environmentRuntime = {
resolveCapabilities,
} as unknown as EnvironmentRuntimeService;
const target = await resolveEnvironmentExecutionTarget({
db: {} as never,
companyId: "company-1",
adapterType: "codex_local",
environment: { id: "env-1", driver: "ssh", config: {} },
leaseId: "lease-1",
leaseMetadata: {},
lease: { id: "lease-1", leasePolicy: "reuse_by_environment" } as never,
environmentRuntime,
});
expect(target?.kind).toBe("remote");
expect(resolveCapabilities).not.toHaveBeenCalled();
});
});
describe("effective snapshot gates the sync decision", () => {
@@ -33,7 +33,7 @@ async function buildSandboxTarget(input: {
});
const runtime: Record<string, unknown> = {
supportsSync: () => false,
effectiveSandboxCapabilities: vi.fn(async () => null),
resolveCapabilities: vi.fn(async () => null),
};
if (!input.omitMethod) {
runtime.readSandboxDuplexBridgeInput = readSandboxDuplexBridgeInput;
@@ -246,7 +246,7 @@ describe("EnvironmentRuntimeService.openDuplexChannel capability gate", () => {
driver: "sandbox",
acquireRunLease: vi.fn(),
releaseRunLease: vi.fn(),
effectiveSandboxCapabilities: vi.fn(async () => ({ ...DUPLEX_ABSENT })),
resolveCapabilities: vi.fn(async () => ({ ...DUPLEX_ABSENT })),
openDuplexChannel,
} as unknown as EnvironmentRuntimeDriver;
const service = environmentRuntimeService({} as never, { drivers: [narrowedDriver] });
@@ -373,7 +373,7 @@ async function buildSandboxRunner(input: {
syncIn: vi.fn(),
syncOut: vi.fn(),
openDuplexChannel,
effectiveSandboxCapabilities: vi.fn(async () =>
resolveCapabilities: vi.fn(async () =>
input.snapshot ? Object.freeze({ ...input.snapshot }) : null,
),
} as unknown as EnvironmentRuntimeService;
@@ -5929,7 +5929,7 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
expect(reusableLease.metadata?.pluginId).toBe(pluginId);
const effective = await runtimeWithPlugin.effectiveSandboxCapabilities({
const effective = await runtimeWithPlugin.resolveCapabilities({
environment,
lease: reusableLease,
});
@@ -5966,7 +5966,7 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
} as unknown as PluginWorkerManager;
const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager });
const effective = await runtimeWithPlugin.effectiveSandboxCapabilities({
const effective = await runtimeWithPlugin.resolveCapabilities({
environment,
lease: reusableLease,
});
@@ -6025,7 +6025,7 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
} as unknown as PluginWorkerManager;
const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager });
const effective = await runtimeWithPlugin.effectiveSandboxCapabilities({
const effective = await runtimeWithPlugin.resolveCapabilities({
environment,
lease: reusableLease,
});
@@ -6079,7 +6079,7 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
} as unknown as PluginWorkerManager;
const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager });
const effective = await runtimeWithPlugin.effectiveSandboxCapabilities({
const effective = await runtimeWithPlugin.resolveCapabilities({
environment,
lease: reusableLease,
});
@@ -0,0 +1,126 @@
import { describe, expect, it } from "vitest";
import {
ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT,
SANDBOX_CAPABILITY_KEYS,
classifyEnvironmentCapabilities,
} from "../services/environment-runtime.js";
// The worker verbs a fully-capable provider advertises. A built-in driver maps
// its own methods onto these verb names, so the general classifier reads one
// verb vocabulary for every driver.
const ALL_PROVIDER_METHODS = [
"environmentResumeLease",
"environmentReleaseLease",
"environmentDestroyLease",
"environmentExecute",
"environmentSyncIn",
"environmentSyncOut",
"duplexChannelOpen",
];
describe("general capability classifier", () => {
it("returns the eight Boolean fields for a full sandbox declaration input", () => {
// A sandbox provider that verifies every prerequisite verb and declares every
// capability resolves the whole eight-field set to true. The classifier reads
// the sandbox driver's static support definition, so it names no driver.
const effective = classifyEnvironmentCapabilities({
verifiedMethods: ALL_PROVIDER_METHODS,
declared: {
reusableLeases: true,
nativeSyncIn: true,
nativeSyncOut: true,
persistentProcessSessions: true,
independentControlCommands: true,
incrementalSessionOutput: true,
concurrentSyncOperations: true,
duplexCommandStream: true,
},
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.sandbox.supportedCapabilities,
});
// The result carries exactly the eight capability fields, each a Boolean.
expect(Object.keys(effective).sort()).toEqual([...SANDBOX_CAPABILITY_KEYS].sort());
for (const key of SANDBOX_CAPABILITY_KEYS) {
expect(typeof effective[key]).toBe("boolean");
expect(effective[key]).toBe(true);
}
});
it("default rule one: an absent declaration defers to verification for a worker-property field", () => {
// A worker-property capability has no opt-in declaration. An absent
// declaration defers to the verified baseline, so a verified verb grants the
// capability and an unverified verb denies it.
const effective = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentSyncIn", "environmentSyncOut"],
declared: null,
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.sandbox.supportedCapabilities,
});
// Verified sync verbs grant native sync without any declaration.
expect(effective.nativeSyncIn).toBe(true);
expect(effective.nativeSyncOut).toBe(true);
// The worker did not verify the execute or reuse verbs, so the baseline denies
// the matching worker-property capabilities.
expect(effective.persistentProcessSessions).toBe(false);
expect(effective.independentControlCommands).toBe(false);
expect(effective.reusableLeases).toBe(false);
});
it("default rule two: the three opt-in fields deny by default without a declaration", () => {
// The opt-in fields are behavioral guarantees. An absent declaration denies
// them even when the worker verifies the prerequisite verb.
const effective = classifyEnvironmentCapabilities({
verifiedMethods: ["environmentExecute", "environmentSyncIn", "environmentSyncOut", "duplexChannelOpen"],
declared: null,
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.sandbox.supportedCapabilities,
});
expect(effective.incrementalSessionOutput).toBe(false);
expect(effective.concurrentSyncOperations).toBe(false);
expect(effective.duplexCommandStream).toBe(false);
});
it("a built-in driver static definition denies every capability it does not support", () => {
// The local and SSH drivers run no provider capability model, so their static
// support definition names no capability. Every capability resolves false even
// with every verb verified and every capability declared.
for (const driver of ["local", "ssh"] as const) {
const effective = classifyEnvironmentCapabilities({
verifiedMethods: ALL_PROVIDER_METHODS,
declared: {
reusableLeases: true,
nativeSyncIn: true,
nativeSyncOut: true,
persistentProcessSessions: true,
independentControlCommands: true,
incrementalSessionOutput: true,
concurrentSyncOperations: true,
duplexCommandStream: true,
},
supportedCapabilities: ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT[driver].supportedCapabilities,
});
for (const key of SANDBOX_CAPABILITY_KEYS) {
expect(effective[key]).toBe(false);
}
}
});
it("defines static capability support for the four drivers", () => {
expect(Object.keys(ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT).sort()).toEqual([
"local",
"plugin",
"sandbox",
"ssh",
]);
// The two remote provider drivers support the whole capability set; the two
// host drivers support none.
expect(ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.sandbox.supportedCapabilities.size).toBe(
SANDBOX_CAPABILITY_KEYS.length,
);
expect(ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.plugin.supportedCapabilities.size).toBe(
SANDBOX_CAPABILITY_KEYS.length,
);
expect(ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.local.supportedCapabilities.size).toBe(0);
expect(ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT.ssh.supportedCapabilities.size).toBe(0);
});
});
+155 -1
View File
@@ -63,7 +63,11 @@ import {
buildWorkspaceRealizationRequest,
readWorkspaceRealizationRequest,
} from "../services/workspace-realization.ts";
import { deriveViteHmrPort, type Environment, type EnvironmentLease } from "@paperclipai/shared";
import {
deriveViteHmrPort,
type Environment,
type EnvironmentLease,
} from "@paperclipai/shared";
import { resolvePaperclipConfigPath } from "../paths.ts";
import type { WorkspaceOperation } from "@paperclipai/shared";
import type { WorkspaceOperationRecorder } from "../services/workspace-operations.ts";
@@ -9079,6 +9083,156 @@ describe("workspace realization request additionalSources", () => {
expect(record.local.path).toBe("/anchor");
});
// Characterization test: this test must pass on the code before and after a
// pure refactor of the driver-trait lookup. It pins today's behavior so the
// refactor changes no field the record writes.
it("selects the provider and summary from the driver's traits", () => {
const now = new Date(0);
const workspace = buildRealizedWorkspace();
const request = buildWorkspaceRealizationRequest({
adapterType: "codex",
companyId: "company-1",
environmentId: "environment-1",
executionWorkspaceId: "execution-workspace-1",
issueId: "issue-1",
heartbeatRunId: "run-1",
requestedMode: "shared_workspace",
workspace,
workspaceConfig: null,
});
const lease: EnvironmentLease = {
id: "lease-1",
companyId: "company-1",
environmentId: "environment-1",
executionWorkspaceId: "execution-workspace-1",
issueId: "issue-1",
heartbeatRunId: "run-1",
status: "active",
leasePolicy: "ephemeral",
provider: null,
providerLeaseId: null,
acquiredAt: now,
lastUsedAt: now,
expiresAt: null,
releasedAt: null,
failureReason: null,
cleanupStatus: null,
metadata: null,
createdAt: now,
updatedAt: now,
};
function buildEnvironment(driver: string): Environment {
return {
id: "environment-1",
name: "env",
description: null,
driver: driver as Environment["driver"],
status: "active",
config: {},
envVars: {},
metadata: null,
createdAt: now,
updatedAt: now,
};
}
const cases: Array<{
driver: string;
provider: string | null;
summary: string;
}> = [
{
driver: "local",
provider: "local",
summary: "Local workspace realized at /anchor.",
},
{
driver: "ssh",
provider: "ssh",
summary: "SSH workspace realized at user@host:22:/anchor.",
},
{
driver: "sandbox",
provider: null,
summary: "Sandbox workspace realized at /.",
},
{
driver: "plugin",
provider: null,
summary: "Plugin workspace realized at /anchor.",
},
// An unknown driver string falls back to the "local" trait row: provider "local".
{
driver: "unknown-driver",
provider: "local",
summary: "Local workspace realized at /anchor.",
},
];
for (const testCase of cases) {
const record = buildWorkspaceRealizationRecord({
environment: buildEnvironment(testCase.driver),
lease,
request,
});
expect(record.provider).toBe(testCase.provider);
expect(record.summary).toBe(testCase.summary);
}
});
it("reads the in_place mode from the lease metadata", () => {
const now = new Date(0);
const workspace = buildRealizedWorkspace();
const request = buildWorkspaceRealizationRequest({
adapterType: "codex",
companyId: "company-1",
environmentId: "environment-1",
executionWorkspaceId: "execution-workspace-1",
issueId: "issue-1",
heartbeatRunId: "run-1",
requestedMode: "shared_workspace",
workspace,
workspaceConfig: null,
});
const lease: EnvironmentLease = {
id: "lease-1",
companyId: "company-1",
environmentId: "environment-1",
executionWorkspaceId: "execution-workspace-1",
issueId: "issue-1",
heartbeatRunId: "run-1",
status: "active",
leasePolicy: "ephemeral",
provider: null,
providerLeaseId: null,
acquiredAt: now,
lastUsedAt: now,
expiresAt: null,
releasedAt: null,
failureReason: null,
cleanupStatus: null,
metadata: { workspaceRealization: { mode: "in_place" } },
createdAt: now,
updatedAt: now,
};
const environment: Environment = {
id: "environment-1",
name: "env",
description: null,
driver: "sandbox",
status: "active",
config: {},
envVars: {},
metadata: null,
createdAt: now,
updatedAt: now,
};
const record = buildWorkspaceRealizationRecord({ environment, lease, request });
expect(record.mode).toBe("in_place");
});
it("reads a legacy request without additionalSources as an empty array", () => {
const legacyRequest = {
version: 1,