mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
Carry the per-run duplex bridge kill switch from the instance setting to every caller of startAdapterExecutionTargetPaperclipBridge. The host reads readSandboxDuplexBridgeInput() and stamps enableSandboxDuplexBridge on the sandbox target, on the same seam as effectiveCapabilities. The value stays on the host and never enters the sandbox environment. The stamp fails closed. An absent runtime, an absent read method, a read error, or any value other than true keeps the file bridge. The adapter-side spec parse reads the boolean and treats an absent or non-boolean field as no grant. The acpx engine and the six local adapter callers pass the parsed value into the bridge, so the two-gate selection is now reachable. Co-authored-by: Paperclip <noreply@paperclip.ing>
90 lines
3.5 KiB
TypeScript
90 lines
3.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { mockResolveEnvironmentDriverConfigForRuntime } = vi.hoisted(() => ({
|
|
mockResolveEnvironmentDriverConfigForRuntime: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../services/environment-config.js", () => ({
|
|
resolveEnvironmentDriverConfigForRuntime: mockResolveEnvironmentDriverConfigForRuntime,
|
|
}));
|
|
|
|
import { resolveEnvironmentExecutionTarget } from "../services/environment-execution-target.js";
|
|
import type { EnvironmentRuntimeService } from "../services/environment-runtime.js";
|
|
|
|
// Build the host sandbox target for one duplex kill-switch state. The fake
|
|
// runtime returns the given bridge input, or throws when `rejectRead` is set.
|
|
// When `omitMethod` is set the runtime has no `readSandboxDuplexBridgeInput`, so
|
|
// the test proves the stamp fails closed on a runtime that predates the read.
|
|
async function buildSandboxTarget(input: {
|
|
bridgeInput?: { enableDuplexBridge: boolean };
|
|
rejectRead?: boolean;
|
|
omitMethod?: boolean;
|
|
}) {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({
|
|
driver: "sandbox",
|
|
config: { provider: "daytona", timeoutMs: 30_000 },
|
|
});
|
|
|
|
const readSandboxDuplexBridgeInput = vi.fn(async () => {
|
|
if (input.rejectRead) {
|
|
throw new Error("settings read failed");
|
|
}
|
|
return input.bridgeInput ?? { enableDuplexBridge: false };
|
|
});
|
|
const runtime: Record<string, unknown> = {
|
|
supportsSync: () => false,
|
|
effectiveSandboxCapabilities: vi.fn(async () => null),
|
|
};
|
|
if (!input.omitMethod) {
|
|
runtime.readSandboxDuplexBridgeInput = readSandboxDuplexBridgeInput;
|
|
}
|
|
const environmentRuntime = runtime as unknown as EnvironmentRuntimeService;
|
|
|
|
const target = await resolveEnvironmentExecutionTarget({
|
|
db: {} as never,
|
|
companyId: "company-1",
|
|
adapterType: "codex_local",
|
|
environment: { id: "env-1", driver: "sandbox", config: { provider: "daytona" } },
|
|
leaseId: "lease-1",
|
|
leaseMetadata: { remoteCwd: "/work" },
|
|
lease: { id: "lease-1", leasePolicy: "reuse_by_environment" } as never,
|
|
environmentRuntime,
|
|
});
|
|
|
|
if (target?.kind !== "remote" || target.transport !== "sandbox") {
|
|
throw new Error("expected a sandbox target");
|
|
}
|
|
return { target, readSandboxDuplexBridgeInput };
|
|
}
|
|
|
|
describe("resolveEnvironmentExecutionTarget duplex kill switch", () => {
|
|
beforeEach(() => {
|
|
mockResolveEnvironmentDriverConfigForRuntime.mockReset();
|
|
});
|
|
|
|
it("stamps the kill switch true when the instance setting is on", async () => {
|
|
const { target, readSandboxDuplexBridgeInput } = await buildSandboxTarget({
|
|
bridgeInput: { enableDuplexBridge: true },
|
|
});
|
|
expect(readSandboxDuplexBridgeInput).toHaveBeenCalledTimes(1);
|
|
expect(target.enableSandboxDuplexBridge).toBe(true);
|
|
});
|
|
|
|
it("stamps no grant when the setting is off", async () => {
|
|
const { target } = await buildSandboxTarget({ bridgeInput: { enableDuplexBridge: false } });
|
|
expect(target.enableSandboxDuplexBridge).toBe(false);
|
|
});
|
|
|
|
it("stamps no grant when the settings read fails, and the error does not become a grant", async () => {
|
|
const { target, readSandboxDuplexBridgeInput } = await buildSandboxTarget({ rejectRead: true });
|
|
expect(readSandboxDuplexBridgeInput).toHaveBeenCalledTimes(1);
|
|
// The resolve did not throw, and the read error stamped no grant.
|
|
expect(target.enableSandboxDuplexBridge).toBe(false);
|
|
});
|
|
|
|
it("stamps no grant when the runtime has no duplex read method", async () => {
|
|
const { target } = await buildSandboxTarget({ omitMethod: true });
|
|
expect(target.enableSandboxDuplexBridge).toBe(false);
|
|
});
|
|
});
|