mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 20:05:57 +02:00
## 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>
127 lines
5.3 KiB
TypeScript
127 lines
5.3 KiB
TypeScript
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);
|
|
});
|
|
});
|