mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 16:35:48 +01: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>
482 lines
20 KiB
TypeScript
482 lines
20 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
ENVIRONMENT_DRIVER_CAPABILITY_SUPPORT,
|
|
SANDBOX_CAPABILITY_KEYS,
|
|
buildSandboxCapabilityNarrowing,
|
|
builtinSandboxProviderVerifiedMethods,
|
|
classifyEnvironmentCapabilities,
|
|
} from "../services/environment-runtime.js";
|
|
|
|
// The worker verbs a fully-capable plug-in provider advertises.
|
|
const ALL_PLUGIN_METHODS = [
|
|
"environmentAcquireLease",
|
|
"environmentResumeLease",
|
|
"environmentReleaseLease",
|
|
"environmentDestroyLease",
|
|
"environmentExecute",
|
|
"environmentSyncIn",
|
|
"environmentSyncOut",
|
|
];
|
|
|
|
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 = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentSyncIn", "environmentSyncOut"],
|
|
declared: null,
|
|
});
|
|
|
|
expect(effective.nativeSyncIn).toBe(true);
|
|
expect(effective.nativeSyncOut).toBe(true);
|
|
// The worker did not verify these verbs, so the baseline is false.
|
|
expect(effective.persistentProcessSessions).toBe(false);
|
|
expect(effective.reusableLeases).toBe(false);
|
|
});
|
|
|
|
it("test_effective_capabilities_are_subset_of_verified_and_declared", () => {
|
|
const verifiedMethods = ["environmentExecute"];
|
|
const declared = {
|
|
persistentProcessSessions: true,
|
|
independentControlCommands: false,
|
|
nativeSyncIn: true,
|
|
};
|
|
const effective = classifyEnvironmentCapabilities({ verifiedMethods, declared });
|
|
|
|
// Verified + declared true.
|
|
expect(effective.persistentProcessSessions).toBe(true);
|
|
// Declared false, so removed even though verified.
|
|
expect(effective.independentControlCommands).toBe(false);
|
|
// Declared true but not verified, so removed.
|
|
expect(effective.nativeSyncIn).toBe(false);
|
|
|
|
// 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 = classifyEnvironmentCapabilities({ verifiedMethods });
|
|
for (const key of SANDBOX_CAPABILITY_KEYS) {
|
|
if (effective[key]) {
|
|
expect(verifiedOnly[key]).toBe(true);
|
|
expect((declared as Record<string, boolean | undefined>)[key]).not.toBe(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("test_kubernetes_job_lease_disables_native_sync", () => {
|
|
const narrowing = buildSandboxCapabilityNarrowing({
|
|
leasePolicy: "ephemeral",
|
|
leaseMetadata: { backend: "job" },
|
|
});
|
|
const effective = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ALL_PLUGIN_METHODS,
|
|
declared: { nativeSyncIn: true, nativeSyncOut: true },
|
|
narrowing,
|
|
});
|
|
|
|
expect(effective.nativeSyncIn).toBe(false);
|
|
expect(effective.nativeSyncOut).toBe(false);
|
|
// A non-sync capability is unaffected by the job-lease narrowing.
|
|
expect(effective.persistentProcessSessions).toBe(true);
|
|
|
|
// The `nativeFileSyncUnsupported` lease flag narrows the same way.
|
|
const flaggedNarrowing = buildSandboxCapabilityNarrowing({
|
|
leasePolicy: "ephemeral",
|
|
leaseMetadata: { nativeFileSyncUnsupported: true },
|
|
});
|
|
expect(flaggedNarrowing.nativeSyncIn).toBe(false);
|
|
expect(flaggedNarrowing.nativeSyncOut).toBe(false);
|
|
});
|
|
|
|
it("test_persistent_process_sessions_follow_the_verified_and_declared_capability", () => {
|
|
// Session-output streaming now follows the capability snapshot alone, not a
|
|
// config flag. A provider that declares and verifies persistent process
|
|
// sessions keeps the capability when no narrowing removes it.
|
|
const verifiedMethods = ["environmentExecute"];
|
|
const declared = { persistentProcessSessions: true };
|
|
|
|
const narrowing = buildSandboxCapabilityNarrowing({
|
|
leasePolicy: "ephemeral",
|
|
leaseMetadata: {},
|
|
});
|
|
// A normal lease adds no persistent-session narrowing.
|
|
expect(narrowing.persistentProcessSessions).toBeUndefined();
|
|
|
|
const effective = classifyEnvironmentCapabilities({
|
|
verifiedMethods,
|
|
declared,
|
|
narrowing,
|
|
});
|
|
expect(effective.persistentProcessSessions).toBe(true);
|
|
});
|
|
|
|
it("test_config_resolution_failure_fails_closed_on_persistent_process_sessions", () => {
|
|
const verifiedMethods = ["environmentExecute"];
|
|
const declared = { persistentProcessSessions: true };
|
|
|
|
// Config resolution failed, so the provider is untrusted. The narrowing must
|
|
// deny persistent process sessions instead of allowing them through. Without
|
|
// the fail-closed guard this narrowing key stays undefined and
|
|
// `persistentProcessSessions` resolves to true.
|
|
const narrowing = buildSandboxCapabilityNarrowing({
|
|
leasePolicy: "ephemeral",
|
|
leaseMetadata: {},
|
|
configResolutionFailed: true,
|
|
});
|
|
expect(narrowing.persistentProcessSessions).toBe(false);
|
|
|
|
const effective = classifyEnvironmentCapabilities({
|
|
verifiedMethods,
|
|
declared,
|
|
narrowing,
|
|
});
|
|
expect(effective.persistentProcessSessions).toBe(false);
|
|
|
|
// Native sync and reusable lease enforcement stay unchanged on failure.
|
|
const syncNarrowing = buildSandboxCapabilityNarrowing({
|
|
leasePolicy: "reuse_by_environment",
|
|
leaseMetadata: { backend: "job" },
|
|
configResolutionFailed: true,
|
|
});
|
|
expect(syncNarrowing.reusableLeases).toBe(true);
|
|
expect(syncNarrowing.nativeSyncIn).toBe(false);
|
|
expect(syncNarrowing.nativeSyncOut).toBe(false);
|
|
});
|
|
|
|
it("test_builtin_provider_branch_uses_same_normalizer_as_plugin_branch", () => {
|
|
const declared = { reusableLeases: true, persistentProcessSessions: true };
|
|
|
|
// A built-in provider maps its own methods to the same verb names.
|
|
const builtinMethods = builtinSandboxProviderVerifiedMethods({
|
|
supportsReusableLeases: true,
|
|
execute: () => undefined,
|
|
});
|
|
const builtinEffective = classifyEnvironmentCapabilities({
|
|
verifiedMethods: builtinMethods,
|
|
declared,
|
|
});
|
|
|
|
// A plug-in provider that advertises the equivalent verbs.
|
|
const pluginEffective = classifyEnvironmentCapabilities({
|
|
verifiedMethods: [
|
|
"environmentResumeLease",
|
|
"environmentReleaseLease",
|
|
"environmentDestroyLease",
|
|
"environmentExecute",
|
|
],
|
|
declared,
|
|
});
|
|
|
|
// The one normalizer drives both branches, so equivalent verb sets resolve
|
|
// to the identical effective capabilities.
|
|
expect(builtinEffective).toEqual(pluginEffective);
|
|
expect(builtinEffective.reusableLeases).toBe(true);
|
|
expect(builtinEffective.persistentProcessSessions).toBe(true);
|
|
// A built-in provider has no native sync hooks, so it never verifies sync.
|
|
expect(builtinEffective.nativeSyncIn).toBe(false);
|
|
|
|
// A built-in provider without an execute method verifies no exec capability.
|
|
const noExec = classifyEnvironmentCapabilities({
|
|
verifiedMethods: builtinSandboxProviderVerifiedMethods({ supportsReusableLeases: false }),
|
|
declared: { persistentProcessSessions: true },
|
|
});
|
|
expect(noExec.persistentProcessSessions).toBe(false);
|
|
});
|
|
|
|
it("test_present_declaration_never_grants_beyond_verified_supported_methods", () => {
|
|
// 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 = classifyEnvironmentCapabilities({
|
|
verifiedMethods: [],
|
|
declared: { [key]: true },
|
|
});
|
|
expect(effective[key]).toBe(false);
|
|
}
|
|
|
|
// A single missing prerequisite verb is enough: reusable leases needs
|
|
// resume, release, and destroy, so resume alone does not grant it.
|
|
const resumeOnly = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentResumeLease"],
|
|
declared: { reusableLeases: true },
|
|
});
|
|
expect(resumeOnly.reusableLeases).toBe(false);
|
|
});
|
|
|
|
it("test_reusable_provider_without_destroy_support_resolves_false", () => {
|
|
// 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 = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentResumeLease", "environmentReleaseLease"],
|
|
declared: { reusableLeases: true },
|
|
});
|
|
expect(resumeAndReleaseOnly.reusableLeases).toBe(false);
|
|
|
|
// Adding the destroy verb makes the same provider eligible.
|
|
const allReuseVerbs = classifyEnvironmentCapabilities({
|
|
verifiedMethods: [
|
|
"environmentResumeLease",
|
|
"environmentReleaseLease",
|
|
"environmentDestroyLease",
|
|
],
|
|
declared: { reusableLeases: true },
|
|
});
|
|
expect(allReuseVerbs.reusableLeases).toBe(true);
|
|
});
|
|
|
|
it("test_generic_one_shot_provider_does_not_get_session_output_streaming", () => {
|
|
// The regression: a generic one-shot provider (for example Modal) verifies
|
|
// `environmentExecute` and declares the two broad session capabilities, yet
|
|
// it never emits incremental session output. Both broad capabilities resolve
|
|
// 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 = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentExecute"],
|
|
declared: {
|
|
persistentProcessSessions: true,
|
|
independentControlCommands: true,
|
|
},
|
|
});
|
|
|
|
expect(effective.persistentProcessSessions).toBe(true);
|
|
expect(effective.independentControlCommands).toBe(true);
|
|
// Opt-in denied: the provider did not declare incremental session output.
|
|
expect(effective.incrementalSessionOutput).toBe(false);
|
|
});
|
|
|
|
it("test_incremental_session_output_is_opt_in_and_needs_a_declaration", () => {
|
|
// 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 = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentExecute"],
|
|
declared: null,
|
|
});
|
|
expect(undeclared.incrementalSessionOutput).toBe(false);
|
|
|
|
// A provider that declares the capability and verifies the prerequisite gets
|
|
// the streaming path.
|
|
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 = classifyEnvironmentCapabilities({
|
|
verifiedMethods: [],
|
|
declared: { incrementalSessionOutput: true },
|
|
});
|
|
expect(declaredButUnverified.incrementalSessionOutput).toBe(false);
|
|
});
|
|
|
|
it("test_config_resolution_failure_fails_closed_on_incremental_session_output", () => {
|
|
// Config resolution failed, so the provider is untrusted. The narrowing must
|
|
// deny incremental session output even with a positive declaration, so the
|
|
// session-output streaming gate fails closed to the poll path.
|
|
const narrowing = buildSandboxCapabilityNarrowing({
|
|
leasePolicy: "ephemeral",
|
|
leaseMetadata: {},
|
|
configResolutionFailed: true,
|
|
});
|
|
expect(narrowing.incrementalSessionOutput).toBe(false);
|
|
|
|
const effective = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentExecute"],
|
|
declared: { incrementalSessionOutput: true },
|
|
narrowing,
|
|
});
|
|
expect(effective.incrementalSessionOutput).toBe(false);
|
|
});
|
|
|
|
it("test_concurrent_sync_operations_is_opt_in_and_needs_both_sync_verbs", () => {
|
|
// 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 = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentSyncIn", "environmentSyncOut"],
|
|
declared: null,
|
|
});
|
|
expect(undeclared.concurrentSyncOperations).toBe(false);
|
|
|
|
// A positive declaration with both verified verbs resolves true.
|
|
const bothVerbs = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentSyncIn", "environmentSyncOut"],
|
|
declared: { concurrentSyncOperations: true },
|
|
});
|
|
expect(bothVerbs.concurrentSyncOperations).toBe(true);
|
|
|
|
// Only the inbound verb: the outbound prerequisite is missing, so it resolves
|
|
// false.
|
|
const inOnly = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentSyncIn"],
|
|
declared: { concurrentSyncOperations: true },
|
|
});
|
|
expect(inOnly.concurrentSyncOperations).toBe(false);
|
|
|
|
// Only the outbound verb: the inbound prerequisite is missing, so it resolves
|
|
// false.
|
|
const outOnly = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentSyncOut"],
|
|
declared: { concurrentSyncOperations: true },
|
|
});
|
|
expect(outOnly.concurrentSyncOperations).toBe(false);
|
|
});
|
|
|
|
it("test_duplex_command_stream_absent_declaration_resolves_false", () => {
|
|
// The duplex channel is opt-in and fail-closed. An absent declaration denies
|
|
// 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 = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["duplexChannelOpen"],
|
|
declared: null,
|
|
});
|
|
expect(undeclared.duplexCommandStream).toBe(false);
|
|
});
|
|
|
|
it("test_duplex_command_stream_needs_verified_worker_method", () => {
|
|
// 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 = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["environmentExecute"],
|
|
declared: { duplexCommandStream: true },
|
|
});
|
|
expect(declaredButUnverified.duplexCommandStream).toBe(false);
|
|
});
|
|
|
|
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 = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["duplexChannelOpen"],
|
|
declared: { duplexCommandStream: true },
|
|
});
|
|
expect(granted.duplexCommandStream).toBe(true);
|
|
|
|
// 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 = classifyEnvironmentCapabilities({
|
|
verifiedMethods: ["duplexChannelOpen"],
|
|
declared: { duplexCommandStream: true },
|
|
narrowing: { duplexCommandStream: false },
|
|
});
|
|
expect(narrowed.duplexCommandStream).toBe(false);
|
|
});
|
|
|
|
it("test_unknown_or_unavailable_verification_resolves_false", () => {
|
|
const declaredAll = {
|
|
reusableLeases: true,
|
|
nativeSyncIn: true,
|
|
nativeSyncOut: true,
|
|
persistentProcessSessions: true,
|
|
independentControlCommands: true,
|
|
incrementalSessionOutput: true,
|
|
concurrentSyncOperations: true,
|
|
};
|
|
|
|
for (const verifiedMethods of [null, undefined, [] as string[]]) {
|
|
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);
|
|
}
|
|
});
|
|
});
|