Merge shared runtime prerequisites into Boat integration

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-11 07:33:39 -05:00
commit 8ecad0f359
16 files changed
+532 -12

No files matched your search

@@ -6,4 +6,3 @@ export function adapterExecutionTargetIsCommandBacked(
): target is AdapterCommandBackedExecutionTarget {
return target?.kind === "remote" && (target.transport === "sandbox" || target.transport === "computer");
}
@@ -5,6 +5,9 @@ import * as ssh from "./ssh.js";
import * as serverUtils from "./server-utils.js";
import {
cleanupGitHubOperationLaunchers,
adapterExecutionTargetRuntimeRoot,
prepareAdapterExecutionTargetRuntime,
type AdapterComputerExecutionTarget,
prepareGitHubOperationLaunchers,
adapterExecutionTargetUsesManagedHome,
ensureAdapterExecutionTargetRuntimeCommandInstalled,
@@ -437,3 +440,36 @@ describe("GitHub launcher lifecycle", () => {
expect(runner.execute).toHaveBeenCalledTimes(1);
});
});
describe("computer legacy runtime ownership", () => {
it.each(["claude", "codex"])("isolates %s assets and callback roots for owners sharing a checkout", async adapterKey => {
const execute = vi.fn(async () => ({ exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "", pid: 123, startedAt: "2026-10-10T00:00:00Z" }));
const target = (ownerId: string, generation = 1) => ({
kind: "remote", transport: "computer", providerKey: "boat", environmentId: "environment",
leaseId: ownerId, remoteCwd: "/shared/project", listenerPort: 43127,
runner: { execute }, processRunner: { execute },
resourceAuthority: { kind: "computer-owner", computerId: "computer", ownerId, generation },
fileAuthority: { kind: "remote-persistent", placementId: "shared", root: "/shared/project", agentHome: `/homes/${ownerId}` },
launch: vi.fn(), inspectProcess: vi.fn(), retainWarm: vi.fn(), retire: vi.fn(),
computerTool: { command: "cua-driver", args: ["mcp"] },
} satisfies AdapterComputerExecutionTarget);
const prepare = (owner: AdapterComputerExecutionTarget) => prepareAdapterExecutionTargetRuntime({
target: owner, runId: owner.leaseId!, adapterKey, workspaceLocalDir: "/unused-local-workspace", syncWorkspace: false,
});
const first = await prepare(target("agent-one"));
const second = await prepare(target("agent-two"));
expect(first.workspaceRemoteDir).toBe("/shared/project");
expect(second.workspaceRemoteDir).toBe(first.workspaceRemoteDir);
expect(first.runtimeRootDir).toBe(adapterExecutionTargetRuntimeRoot(target("agent-one"), adapterKey));
expect(second.runtimeRootDir).not.toBe(first.runtimeRootDir);
expect(adapterExecutionTargetRuntimeRoot(target("agent-one", 2), adapterKey)).not.toBe(first.runtimeRootDir);
expect((await prepare(target("agent-one"))).runtimeRootDir).toBe(first.runtimeRootDir);
expect(first.runtimeRootDir).toMatch(new RegExp(`^/shared/project/\\.paperclip-runtime/${adapterKey}/owners/[a-f0-9]{64}$`));
});
it("preserves the existing sandbox runtime root", () => {
const target = { kind: "remote", transport: "sandbox", remoteCwd: "/workspace" } as const;
expect(adapterExecutionTargetRuntimeRoot(target, "claude")).toBe("/workspace/.paperclip-runtime/claude");
});
});
@@ -210,3 +210,29 @@ describe("paperclip runner transport routing", () => {
).toThrow();
});
});
it("uses each computer owner's allocated port without changing the sandbox default", async () => {
const { adapterExecutionTargetIsCommandBacked } = await import("./execution-target.js");
const endpoint = ingress();
const getRunnerIngressEndpoint = vi.fn(async () => endpoint);
const base = {
kind: "remote" as const, transport: "computer" as const, providerKey: "boat", remoteCwd: "/home/user/project",
leaseId: "attempt-lease", listenerPort: 45101, effectiveCapabilities: capabilities, getRunnerIngressEndpoint,
processRunner: { execute: async () => ({ exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "", pid: null, startedAt: new Date().toISOString() }) },
resourceAuthority: { kind: "computer-owner" as const, computerId: "machine", ownerId: "owner-a", generation: 2 },
fileAuthority: { kind: "remote-persistent" as const, placementId: "placement", root: "/home/user/project", agentHome: "/home/user/agent-a" },
launch: async () => ({}), inspectProcess: async () => ({ running: true, claim: {} }),
retainWarm: async () => {}, retire: async () => true, computerTool: { command: "cua-driver", args: ["mcp"] },
};
expect(adapterExecutionTargetIsCommandBacked(base)).toBe(true);
for (const port of [45101, 45102]) {
const resolved = await resolvePaperclipRunnerTransport({ target: { ...base, listenerPort: port }, runId: "run-1",
localConnectUrl: "ws://localhost/unused", runnerIngressAuthorized: true });
expect(resolved).toMatchObject({ mode: "provider_ingress", listenPort: port });
expect(getRunnerIngressEndpoint).toHaveBeenLastCalledWith({ leaseId: "attempt-lease", port, path: "/api/runner/v1/connect/run-1" });
}
const { listenerPort, processRunner, resourceAuthority, fileAuthority, launch, inspectProcess, retainWarm, retire, computerTool, ...common } = base;
const resolved = await resolvePaperclipRunnerTransport({ target: { ...common, transport: "sandbox" }, runId: "sandbox-run",
localConnectUrl: "ws://localhost/unused", runnerIngressAuthorized: true });
expect(resolved).toMatchObject({ mode: "provider_ingress", listenPort: 43127 });
});
@@ -6,6 +6,7 @@ import path from "node:path";
import { afterAll, afterEach, describe, expect, it } from "vitest";
import {
buildSshSpawnTarget,
createSshCommandManagedRuntimeRunner,
buildSshEnvLabFixtureConfig,
getSshEnvLabSupport,
prepareWorkspaceForSshExecution,
@@ -181,6 +182,28 @@ describe("ssh env-lab fixture", () => {
// this drains it too instead of stranding a listener until the process exits.
afterAll(drainFixtureTeardowns);
it.each([0, 7])("handles a closed SSH input pipe without losing exit diagnostics (exit %i)", async (exitCode) => {
const rootDir = await createFixtureRootDir();
const previousPath = process.env.PATH;
await writeFile(path.join(rootDir, "ssh"), `#!/bin/sh
exec 0<&-
printf 'remote rejected upload' >&2
sleep 0.05
exit ${exitCode}
`, { mode: 0o700 });
process.env.PATH = `${rootDir}${path.delimiter}${previousPath ?? ""}`;
try {
const runner = createSshCommandManagedRuntimeRunner({ spec: UNREACHABLE_SSH_SPEC });
const result = await runner.execute({ command: "cat", stdin: "x".repeat(8 * 1024 * 1024), timeoutMs: 5000 });
expect(result.exitCode).toBe(exitCode === 0 ? null : exitCode);
expect(result.stderr).toBe("remote rejected upload");
expect(result.timedOut).toBe(false);
} finally {
if (previousPath === undefined) delete process.env.PATH;
else process.env.PATH = previousPath;
}
});
it("starts an isolated sshd fixture and executes commands through it", async () => {
const rootDir = await createFixtureRootDir();
const statePath = path.join(rootDir, "state.json");
@@ -231,6 +231,31 @@ describe("claude remote execution", () => {
}));
});
it("keeps an authoritative remote workspace in place while delivering managed Claude assets", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-claude-in-place-"));
cleanupDirs.push(rootDir);
await execute({
runId: "run-in-place",
agent: { id: "agent-1", companyId: "company-1", name: "Claude", adapterType: "claude_local", adapterConfig: {} },
runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null },
config: { engine: "cli", command: "claude", cwd: "/stale-host-cwd" },
context: { paperclipWorkspace: { cwd: "/remote-only-agent-home", source: "agent_home" } },
executionTarget: {
kind: "remote", transport: "ssh", remoteCwd: "/old-copied-workspace",
workspaceRealization: { mode: "in_place", authoritativeRoot: "/persistent-project", pathAliases: [], outboundRestorePaths: [] },
spec: { host: "127.0.0.1", port: 2222, username: "fixture", remoteWorkspacePath: "/persistent-project", remoteCwd: "/persistent-project",
privateKey: "PRIVATE KEY", knownHosts: "[127.0.0.1]:2222 ssh-ed25519 AAAA", strictHostKeyChecking: true },
},
onLog: async () => {},
});
expect(prepareWorkspaceForSshExecution).not.toHaveBeenCalled();
expect(restoreWorkspaceFromSshExecution).not.toHaveBeenCalled();
expect(syncDirectoryToSsh).toHaveBeenCalledTimes(2);
expect(syncDirectoryToSsh).toHaveBeenCalledWith(expect.objectContaining({ remoteDir: "/persistent-project/.paperclip-runtime/claude/skills" }));
const call = runChildProcess.mock.calls[0] as unknown as [string, string, string[], { remoteExecution: { remoteCwd: string } }];
expect(call[3].remoteExecution.remoteCwd).toBe("/persistent-project");
});
it("does not resume saved Claude sessions for remote SSH execution without a matching remote identity", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-claude-remote-resume-"));
cleanupDirs.push(rootDir);
@@ -37,6 +37,13 @@ function savedContext(revision: string, text: string) {
}
describe("persisted native execution prompts", () => {
it("preserves an authenticated persistent home independently of instruction copies", () => {
const context = { ...savedContext("saved", "Instructions"), persistentAgentHome: { rootPath: "/computer/agent" } };
expect(parseNativeRuntimeContext(context).persistentAgentHome).toEqual({ rootPath: "/computer/agent" });
expect(parseNativeRuntimeContext(context).instructions.workingCopy).toBeUndefined();
expect(() => parseNativeRuntimeContext({ ...context, persistentAgentHome: { rootPath: "" } })).toThrow();
expect(() => parseNativeRuntimeContext({ ...context, persistentAgentHome: { rootPath: "/computer/agent", untrusted: true } })).toThrow();
});
it.each([
["snapshot-before-upgrade", "Saved instructions from before the upgrade."],
["future-prompt-fixture", " Saved instructions absent from this release.\n"],
@@ -18,6 +18,19 @@ function fixture() {
}
describe("registered ACP agent files", () => {
it("binds a persistent computer home without inventing an instruction copy", () => {
const { root, parent, context } = fixture();
delete context.instructions.workingCopy;
context.persistentAgentHome = { rootPath: root };
const nestedRuntime = join(root, ".paperclip-runtime"); mkdirSync(nestedRuntime);
const binding = bindAcpxAgentFiles(context, [nestedRuntime])!;
expect(binding.root.endsWith("registered-run-copy")).toBe(true);
for (const protectedRoot of [parent, root]) {
expect(() => bindAcpxAgentFiles(context, [protectedRoot])).toThrow("overlaps");
}
renameSync(root, `${root}-old`); mkdirSync(root);
expect(() => binding.assertHeld()).toThrow("changed");
});
it("does not grant absent or legacy instruction copies", () => {
expect(bindAcpxAgentFiles(null, [])).toBeNull();
const { context } = fixture();
@@ -949,6 +949,34 @@ describe("ACPX runtime host", () => {
expect(fixture.commandClose).toHaveBeenCalledOnce();
});
it("gives Claude shells only the registered persistent home while keeping provider HOME isolated", async () => {
const fixture = await hostFixture();
const home = await realpath(await mkdtemp(join(tmpdir(), "paperclip-computer-agent-home-")));
temporaryDirectories.push(home);
const context: NativeRuntimeContextSnapshot = {
prompt: { revision: PAPERCLIP_EXECUTION_PROMPT_REVISION, text: PAPERCLIP_EXECUTION_PROMPT, digest: nativeRuntimePromptDigest() },
instructions: { entryPath: "AGENTS.md", bundle: { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest: "0".repeat(64), manifestDigest: "0".repeat(64), rootPath: home, fileCount: 0, totalBytes: 0 } },
persistentAgentHome: { rootPath: home }, skills: [],
mcp: { assignmentSetId: "none", digest: "0".repeat(64), bindingId: null }, aggregateDigest: "",
};
context.aggregateDigest = canonicalNativeRuntimeContextDigest(context);
let observed: { home: string; agentHome: string } | undefined;
const host = await AcpxRuntimeHost.open({ ...fixture.options, agent: "claude", model: "claude-sonnet-5",
environment: { AGENT_HOME: "/ambient/untrusted" }, runtimeContext: context,
}, fixture.dependencies({ openRuntime: async launch => {
observed = JSON.parse(execFileSync(process.execPath, ["-e", 'console.log(JSON.stringify({home:process.env.HOME,agentHome:process.env.AGENT_HOME}))'], {
env: launch.launchEnvironment, encoding: "utf8", timeout: 5_000,
}));
return runtimePort({ getStatus: async () => ({ models: { currentModelId: "claude-sonnet-5" } }) });
} }));
try {
expect(observed?.agentHome).toBe(home);
expect(observed?.home).not.toBe(home);
expect(observed?.home).toContain("home");
expect(context.instructions.workingCopy).toBeUndefined();
} finally { await host.close({ reason: "persistent home verified" }); }
});
it.each(["cursor", "copilot", "pi"] as const)("binds %s agent files from each registered run copy, never ambient roots", async (agent) => {
const fixture = await hostFixture();
const copies = await mkdtemp(join(tmpdir(), "paperclip-agent-copies-"));
@@ -1,3 +1,4 @@
import { createRunnerdCodexAppServerArgs } from "../live/runnerd-codex-transport.js";
import {
chmod,
lstat,
@@ -27,6 +28,7 @@ import { nativeMcpLaunchBinding } from "./native-mcp.js";
import {
materializeNativeRuntimeSkills,
prepareIsolatedCodexHome,
releaseMaterializedNativeRuntimeSkills,
} from "./runtime-context-materializer.js";
const roots: string[] = [];
@@ -93,6 +95,19 @@ function context(
}
describe("runtime context materialization", () => {
it("installs only the trusted environment computer binding and removes it on refresh without that capability", async () => {
const root = await mkdtemp(join(tmpdir(), "paperclip-computer-context-"));
roots.push(root);
const codexHome = join(root, "codex-home");
await prepareIsolatedCodexHome({ context: null, codexHome,
computerTool: { command: "/opt/ascii/cua-driver/cua-driver", args: ["mcp", "--socket", "/run/ascii-cua/driver.sock"] } });
const config = await readFile(join(codexHome, "config.toml"), "utf8");
expect(config).toContain("[mcp_servers.paperclip_computer]");
expect(config).toContain('command = "/opt/ascii/cua-driver/cua-driver"');
await releaseMaterializedNativeRuntimeSkills(join(codexHome, "skills"));
await prepareIsolatedCodexHome({ context: null, codexHome });
expect(await readFile(join(codexHome, "config.toml"), "utf8")).not.toContain("paperclip_computer");
});
it("restores the legacy working-copy contract without changing its digest or adding new fields", () => {
const old = context("/skills", "/instructions");
old.instructions.workingCopy = { rootPath: "/old-run/copy", entryPath: "AGENTS.md" };
@@ -503,3 +518,12 @@ command = "untrusted-command"
await expect(stat(join(symlinkCodexHome, "auth.json"))).rejects.toThrow();
});
});
it("exposes a trusted persistent personal directory without instruction files", () => {
const options = { environment: { AGENT_HOME: "/untrusted", HOME: "/ambient" }, codexHome: "/private-provider" };
const supplied = createRunnerdCodexAppServerArgs({ ...options, persistentAgentHome: "/personal/agent" }).join("\n");
expect(supplied).toContain('AGENT_HOME="/personal/agent"');
expect(supplied).toContain('"/private-provider"="none"');
expect(supplied).not.toContain('AGENT_HOME="/untrusted"');
expect(createRunnerdCodexAppServerArgs(options).join("\n")).not.toContain("AGENT_HOME");
});
@@ -643,6 +643,46 @@ it("retargets only composed instruction framing and retains legacy custom instru
});
it.each([false, true])("preserves ACPX asset framing across cold, warm, and post-idle attachment (guidance=%s)", (guidance) => {
const source = assignedRuntimeContext("/controller/skills", "/controller/bundle");
const cold = assignedRuntimeContext("/boat/skills", "/boat/context/cold");
const resumed = assignedRuntimeContext("/boat/skills", "/boat/context/resumed");
const custom = "Managed instructions with literal /controller/bundle example.";
const original = composeNativeSystemInstructions(source, custom);
const coldText = runnerdRecoveryInternals.retargetComposedInstructions(
runnerdRecoveryInternals.withComputerProcessInstructions(original, guidance, source),
source,
cold,
);
const suffix = (context: NativeRuntimeContextSnapshot) =>
composeNativeSystemInstructions(context, "").slice(context.prompt.text.length);
expect(coldText.endsWith(suffix(cold))).toBe(true);
const state = { runAttachTemplate: { provider: {
kind: "acpx", agent: "claude", runId: "cold-run", runtimeContext: cold, instructions: coldText,
} } };
const desired = {
runnerInstanceId: "runner", environmentLeaseId: "workspace", runId: "warm-run",
normalizedSessionId: "same-session", turnId: "turn", itemId: "item",
};
const warm = runnerdRecoveryInternals.rotatedRunAttachPayload(state, desired, null, undefined);
expect((warm.provider as { instructions: string }).instructions).toBe(coldText);
const recover = (text: string) => runnerdRecoveryInternals.rotatedRunAttachPayload(
{ runAttachTemplate: warm }, { ...desired, runId: "post-idle-run" }, null, undefined, resumed,
{ text, context: source },
).provider as { instructions: string; runtimeContext: NativeRuntimeContextSnapshot };
const recovered = recover(composeNativeSystemInstructions(source, custom));
expect(recovered.runtimeContext).toEqual(resumed);
expect(recovered.instructions.endsWith(suffix(resumed))).toBe(true);
// This is the immutable-prefix equality required by Claude's guest guard.
expect(recovered.instructions.slice(0, -suffix(resumed).length))
.toBe(coldText.slice(0, -suffix(cold).length));
expect(recovered.instructions).toContain(custom);
expect(recovered.instructions).not.toContain("Read-only instruction sibling root: /controller/");
// Never silently substitute old managed instructions for current grants.
expect(recover(composeNativeSystemInstructions(source, "Changed managed instructions")).instructions)
.toContain("Changed managed instructions");
});
it("replays the durable run attachment outcome and latest provider identity", () => {
expect(
runnerdRecoveryInternals.recoveredRunAttachment({
@@ -2158,6 +2198,32 @@ it.each(["installed", "explicit"] as const)("records the selected %s Codex comma
}
});
it.each([false, true])("binds detached-process guidance only for a trusted persistent computer (%s)", async (computer) => {
const root = await mkdtemp(join(tmpdir(), "paperclip-computer-process-guidance-"));
const runnerBinary = join(root, "runnerd");
await writeFile(runnerBinary, "unexecuted fixture artifact", { mode: 0o700 });
let instructions = "";
const { transport } = createCapabilityRunnerdCodexTransport({
provider: "codex", runnerBinary, stateDirectory: root, sourceCodexHome: "",
codexCommand: "/qualified/codex", persistentAgentHome: computer ? "/agent/home" : undefined,
controlPlaneRegistration: async (authority) => {
instructions = String((authority.store.state.runAttachTemplate!.provider as { instructions: string }).instructions);
throw new Error("fixture_stop_before_runner_launch");
},
runnerProcessLauncher: () => { throw new Error("must not launch"); },
});
try {
await expect(transport.request("thread/start", { cwd: root, model: "gpt-6.1-sol", dynamicTools: [],
developerInstructions: "Preserve the task instructions." })).rejects.toThrow("fixture_stop_before_runner_launch");
expect(instructions).toContain("Preserve the task instructions.");
expect(instructions.includes("nohup setsid")).toBe(computer);
if (computer) {
expect(instructions).toContain("stop when its warm timeout expires");
expect(instructions).toContain("Do not create services or change runner ownership");
}
} finally { await transport.close(); await rm(root, { recursive: true, force: true }); }
});
it("rejects remote Codex without a guest executable before resolving controller dependencies", async () => {
const root = await mkdtemp(join(tmpdir(), "paperclip-runner-remote-codex-command-"));
const runnerBinary = join(root, "runnerd");
@@ -326,7 +326,7 @@ describe("executeNativeSession recovery", () => {
const completeRun = vi.fn(async () => {});
try {
const execution = executeNativeSession({
input, turnTimeoutMs,
input, turnTimeoutMs, bootstrapTimeoutMs: 5,
backend: {
async descriptor() { return { kind: "mock", name: "long-lived", version: "1", capabilities }; },
async openSession() { return session; },
@@ -2256,15 +2256,18 @@ describe("executeNativeSession recovery", () => {
runnerInstanceId: "runner-recovery",
controlPlaneInstanceId: "control-recovery",
timeoutMs: 5,
bootstrapTimeoutMs: 10,
});
const execution = execute();
const rejection = expect(execution).rejects.toThrow(
"native session bootstrap timed out after 5ms",
"native session bootstrap timed out after 10ms",
);
await bootstrapStarted;
expect(bootstrapSignal?.aborted).toBe(false);
await vi.advanceTimersByTimeAsync(5);
expect(bootstrapSignal?.aborted).toBe(false);
await vi.advanceTimersByTimeAsync(5);
await rejection;
expect(bootstrapSignal?.aborted).toBe(true);
expect(openRun).not.toHaveBeenCalled();
@@ -2659,6 +2662,7 @@ describe("executeNativeSession recovery", () => {
runnerInstanceId: "runner-recovery",
controlPlaneInstanceId: "control-recovery",
timeoutMs: 10,
bootstrapTimeoutMs: 100,
keepSessionOpen: true,
onSession: (current) => retainedSessions.push(current),
});
@@ -5513,6 +5517,7 @@ describe("executeNativeSession recovery", () => {
controlPlaneInstanceId: "control-recovery",
persistedSession: structuredClone(checkpoint),
timeoutMs: 5,
bootstrapTimeoutMs: 100,
onSession,
});
const rejection = expect(execution).rejects.toThrow(
@@ -5648,15 +5653,18 @@ describe("executeNativeSession recovery", () => {
controlPlaneInstanceId: "control-recovery",
persistedSession: structuredClone(checkpoint),
timeoutMs: 5,
bootstrapTimeoutMs: 10,
onSession,
});
const rejection = expect(execution).rejects.toThrow(
"native session replacement bootstrap timed out after 5ms",
"native session replacement bootstrap timed out after 10ms",
);
await replacementStarted;
expect(replacementSignal?.aborted).toBe(false);
await vi.advanceTimersByTimeAsync(5);
expect(replacementSignal?.aborted).toBe(false);
await vi.advanceTimersByTimeAsync(5);
await rejection;
expect(replacementSignal?.aborted).toBe(true);
expect(openRun).not.toHaveBeenCalled();
@@ -983,6 +983,110 @@ describe("claude execute", () => {
}
}, 10_000);
it("uses registered persistent instruction paths in computer CLI prompts", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-claude-execute-sandbox-"));
const localWorkspace = path.join(root, "workspace");
const remoteWorkspace = path.join(root, "sandbox-$HOME");
const binDir = path.join(root, "bin");
const commandPath = path.join(binDir, "claude");
const capturePath1 = path.join(remoteWorkspace, "capture-1.json");
const claudeRoot = path.join(root, ".claude");
const localInstructions = path.join(localWorkspace, "AGENTS.md");
const remoteInstructions = path.join(remoteWorkspace, "nested", "AGENTS.md");
const runner = createLocalSandboxRunner();
const previousHome = process.env.HOME;
const previousPath = process.env.PATH;
await fs.mkdir(localWorkspace, { recursive: true });
await fs.mkdir(remoteWorkspace, { recursive: true });
await fs.mkdir(binDir, { recursive: true });
await fs.mkdir(claudeRoot, { recursive: true });
await fs.writeFile(path.join(claudeRoot, "settings.json"), JSON.stringify({ theme: "test" }), "utf8");
await writeFakeClaudeCommand(commandPath);
await fs.writeFile(localInstructions, "Persistent guidance contents.");
process.env.HOME = root;
process.env.PATH = `${binDir}${path.delimiter}${process.env.PATH ?? ""}`;
try {
const result = await execute({
runId: "run-sandbox-auth",
agent: {
id: "agent-1",
companyId: "company-1",
name: "Claude Coder",
adapterType: "claude_local",
adapterConfig: { engine: "cli" },
},
runtime: {
sessionId: null,
sessionParams: null,
sessionDisplayId: null,
taskKey: null,
},
config: {
engine: "cli",
managedAiConnection: true,
command: commandPath,
cwd: localWorkspace,
instructionsFilePath: localInstructions,
env: {
CLAUDE_CONFIG_DIR: claudeRoot,
PAPERCLIP_TEST_CAPTURE_PATH: capturePath1,
},
promptTemplate: "Follow the paperclip heartbeat.",
},
context: { paperclipWorkspace: { instructionsFilePath: remoteInstructions, agentHome: remoteWorkspace } },
executionTarget: {
kind: "remote",
transport: "computer",
providerKey: "e2b",
environmentId: "env-1",
leaseId: "lease-1",
remoteCwd: remoteWorkspace,
timeoutMs: 30_000,
runner,
processRunner: runner,
listenerPort: 12345,
resourceAuthority: { kind: "computer-owner", computerId: "computer-1", ownerId: "owner-1", generation: 1 },
fileAuthority: { kind: "remote-persistent", placementId: "placement-1", root: remoteWorkspace, agentHome: remoteWorkspace },
workspaceRealization: { mode: "in_place", authoritativeRoot: remoteWorkspace },
launch: async () => ({}),
inspectProcess: async () => ({ running: false, claim: null }),
retainWarm: async () => {},
retire: async () => true,
computerTool: { command: "unused", args: [] },
},
authToken: "run-jwt-token",
onLog: async () => {},
});
expect(result.exitCode).toBe(0);
const capture = JSON.parse(await fs.readFile(capturePath1, "utf8")) as CapturePayload;
expect(capture.argv).toContain("--dangerously-skip-permissions");
expect(capture.argv).not.toContain("--allowedTools");
expect(capture.claudeConfigDir).toContain(path.join(remoteWorkspace, ".paperclip-runtime", "claude", "owners"));
expect(capture.argv.slice(capture.argv.indexOf("--setting-sources"), capture.argv.indexOf("--setting-sources") + 2)).toEqual(["--setting-sources", "user"]);
const userSkills = path.join(capture.claudeConfigDir!, "skills");
expect(await fs.realpath(userSkills)).toBe(await fs.realpath(path.join(capture.addDir!, ".claude", "skills")));
expect(await fs.readFile(path.join(userSkills, "paperclip", "SKILL.md"), "utf8")).toContain("name: paperclip");
expect(capture.instructionsContents).toContain("Persistent guidance contents.");
expect(capture.claudeConfigEntries).toContain("settings.json");
expect(capture.prompt).toContain(remoteInstructions);
expect(capture.prompt).toContain(`${path.dirname(remoteInstructions)}/`);
expect(capture.prompt).not.toContain(localInstructions);
expect(capture.paperclipApiUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
expect(capture.paperclipApiKey).not.toBe("run-jwt-token");
expect(capture.paperclipApiBridgeMode).toBe("queue_v1");
} finally {
if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome;
if (previousPath === undefined) delete process.env.PATH;
else process.env.PATH = previousPath;
await fs.rm(root, { recursive: true, force: true });
}
}, 10_000);
it("omits --effort for sandbox-managed runs when the installed Claude CLI does not advertise it", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-claude-execute-sandbox-effort-"));
const { workspace, commandPath, capturePath, restore } = await setupExecuteEnv(root, {
@@ -538,6 +538,101 @@ describe("codex execute", () => {
}
});
it("uses registered persistent instruction paths in computer CLI prompts", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-sandbox-"));
const localWorkspace = path.join(root, "workspace");
const remoteWorkspace = path.join(root, "sandbox");
const binDir = path.join(root, "bin");
const commandPath = path.join(binDir, "codex");
const capturePath = path.join(remoteWorkspace, "capture.json");
const localInstructions = path.join(localWorkspace, "AGENTS.md");
const remoteInstructions = path.join(remoteWorkspace, "nested", "AGENTS.md");
const runner = createLocalSandboxRunner();
const previousHome = process.env.HOME;
const previousPath = process.env.PATH;
await fs.mkdir(localWorkspace, { recursive: true });
await fs.mkdir(remoteWorkspace, { recursive: true });
await fs.mkdir(binDir, { recursive: true });
await writeFakeCodexCommand(commandPath);
await fs.writeFile(localInstructions, "Persistent guidance contents.");
process.env.HOME = root;
process.env.PATH = `${binDir}${path.delimiter}${process.env.PATH ?? ""}`;
await seedSharedCodexAuth(root);
try {
const result = await execute({
runId: "run-sandbox-auth",
agent: {
id: "agent-1",
companyId: "company-1",
name: "Codex Coder",
adapterType: "codex_local",
adapterConfig: { engine: "cli" },
},
runtime: {
sessionId: null,
sessionParams: null,
sessionDisplayId: null,
taskKey: null,
},
config: {
engine: "cli",
command: commandPath,
cwd: localWorkspace,
instructionsFilePath: localInstructions,
env: {
PAPERCLIP_TEST_CAPTURE_PATH: capturePath,
},
promptTemplate: "Follow the paperclip heartbeat.",
},
context: { paperclipWorkspace: { instructionsFilePath: remoteInstructions, agentHome: remoteWorkspace } },
executionTarget: {
kind: "remote",
transport: "computer",
providerKey: "e2b",
environmentId: "env-1",
leaseId: "lease-1",
remoteCwd: remoteWorkspace,
timeoutMs: 30_000,
runner,
processRunner: runner,
listenerPort: 12345,
resourceAuthority: { kind: "computer-owner", computerId: "computer-1", ownerId: "owner-1", generation: 1 },
fileAuthority: { kind: "remote-persistent", placementId: "placement-1", root: remoteWorkspace, agentHome: remoteWorkspace },
workspaceRealization: { mode: "in_place", authoritativeRoot: remoteWorkspace },
launch: async () => ({}),
inspectProcess: async () => ({ running: false, claim: null }),
retainWarm: async () => {},
retire: async () => true,
computerTool: { command: "unused", args: [] },
},
authToken: "run-jwt-token",
onLog: async () => {},
});
expect(result.exitCode).toBe(0);
expect(result.errorMessage).toBeNull();
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
expect(capture.codexHome).toContain(path.join(remoteWorkspace, ".paperclip-runtime", "codex", "owners"));
expect(capture.prompt).toContain("Persistent guidance contents.");
expect(capture.prompt).toContain(remoteInstructions);
expect(capture.prompt).toContain(`${path.dirname(remoteInstructions)}/`);
expect(capture.prompt).not.toContain(localInstructions);
expect(capture.paperclipApiUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
expect(capture.paperclipApiKey).not.toBe("run-jwt-token");
expect(capture.paperclipApiBridgeMode).toBe("queue_v1");
} finally {
if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome;
if (previousPath === undefined) delete process.env.PATH;
else process.env.PATH = previousPath;
await fs.rm(root, { recursive: true, force: true });
}
});
it.each([false, true])("delivers oversized wake context through stdin (sandbox=%s)", async (sandbox) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-wake-"));
const workspace = path.join(root, "workspace");
@@ -8,10 +8,11 @@ import type { CommandManagedRuntimeRunner } from "@paperclipai/adapter-utils/com
import { describe, expect, it, vi } from "vitest";
import { prepareHeartbeatGitHubLaunchers } from "./heartbeat-github-launchers.js";
const computerTarget = { kind: "remote", transport: "computer", remoteCwd: "/workspace" } as Parameters<typeof prepareHeartbeatGitHubLaunchers>[0]["target"];
const target = { kind: "remote" as const, transport: "sandbox" as const, providerKey: "daytona", remoteCwd: "/workspace" };
describe("heartbeat GitHub launcher lifetime", () => {
it.each([target, null])("defers native managed authorization/staging to the session owner (%j)", async (target) => {
it.each([target, computerTarget, null])("defers native managed authorization/staging to the session owner (%j)", async (target) => {
const prepare = vi.fn();
const mint = vi.fn();
const result = await prepareHeartbeatGitHubLaunchers({
@@ -23,7 +24,7 @@ describe("heartbeat GitHub launcher lifetime", () => {
expect(result.cleanupLocation).toBeNull();
expect(result.env).toMatchObject({ GH_TOKEN: "", PAPERCLIP_GITHUB_BROKER_TOKEN: "" });
});
it("keeps anonymous native sandbox launchers stable without issuing a run capability", async () => {
it.each([target, computerTarget])("keeps anonymous native launchers stable without issuing a run capability (%j)", async (target) => {
const createBrokerToken = vi.fn(() => "run-secret");
const prepareLaunchers = vi.fn(async (input) => input.env);
const inputs = { native: true, githubConfigured: false, agentId: "agent-a", target,
@@ -50,7 +51,7 @@ describe("heartbeat GitHub launcher lifetime", () => {
expect(cleanupLaunchers).toHaveBeenCalledExactlyOnceWith({ runId: "failed-run", target });
});
it("does not remove shared anonymous wrappers when staging a later run fails", async () => {
it.each([target, computerTarget])("does not remove shared anonymous wrappers when staging a later run fails (%j)", async (target) => {
const stagingError = new Error("remote launcher staging failed");
const cleanupLaunchers = vi.fn(async () => undefined);
await expect(prepareHeartbeatGitHubLaunchers({
@@ -64,6 +65,7 @@ describe("heartbeat GitHub launcher lifetime", () => {
it.each([
{ native: false, githubConfigured: true, target },
{ native: false, githubConfigured: false, target },
{ native: false, githubConfigured: false, target: computerTarget },
{ native: true, githubConfigured: false, target: null },
])("preserves run-scoped managed capabilities outside anonymous native sandboxes: %j", async (mode) => {
const createBrokerToken = vi.fn(() => "current-run-secret");
@@ -2,6 +2,7 @@ import { afterEach, expect, it, vi } from "vitest";
import { spawn, execFile } from "node:child_process";
import { mkdtemp, mkdir, readFile, rm, writeFile, access } from "node:fs/promises";
import { promisify } from "node:util";
import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target";
import type { CommandManagedRuntimeRunner } from "@paperclipai/adapter-utils/command-managed-runtime";
import { tmpdir } from "node:os";
import path from "node:path";
@@ -13,7 +14,7 @@ const cleanups: (() => Promise<unknown>)[] = [];
afterEach(async () => { for (const cleanup of cleanups.splice(0).reverse()) await cleanup(); });
async function broker(resolveCredentials = vi.fn(async (binding: ReturnType<typeof run>) => ({
status: "available", env: { GH_TOKEN: `fixture-${binding.runId}` },
})), remote = false, bridgeUnavailable = false, onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>) {
})), remote: boolean | "computer" = false, bridgeUnavailable = false, onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>) {
const root = await mkdtemp(path.join(tmpdir(), "native-github-reuse-"));
cleanups.push(() => rm(root, { recursive: true, force: true }));
const bin = path.join(root, "real-bin");
@@ -29,20 +30,42 @@ async function broker(resolveCredentials = vi.fn(async (binding: ReturnType<type
const result = await child;
return { ...result, exitCode: 0, signal: null, timedOut: false, pid: null, startedAt };
};
const target = remote ? { kind: "remote" as const, transport: "sandbox" as const, providerKey: "test",
remoteCwd: root, runner: { execute }, streamRunLogs: false } : null;
let generation = 1;
let processLive = true;
const processRunner = { execute: vi.fn(async (input: Parameters<typeof execute>[0]) => {
if (!processLive) throw new Error("computer_process_claim_stale");
return execute(input);
}) };
const target: AdapterExecutionTarget | null = remote === "computer" ? {
kind: "remote", transport: "computer", remoteCwd: root,
runner: { execute: async (input: Parameters<typeof execute>[0]) => {
if (generation !== 1) throw new Error("computer_attempt_superseded");
return execute(input);
} }, processRunner,
listenerPort: 43127,
resourceAuthority: { kind: "computer-owner", computerId: "computer-a", ownerId: "owner-a", generation: 1 },
fileAuthority: { kind: "remote-persistent", placementId: "placement-a", root, agentHome: root },
launch: async () => { throw new Error("Fixture runner is already launched"); },
inspectProcess: async () => ({ running: processLive, claim: { nonce: "fixture-process" } }),
retainWarm: async () => undefined,
retire: async () => { processLive = false; return true; },
computerTool: { command: "fixture-computer-tool", args: [] },
} : remote ? {
kind: "remote" as const, transport: "sandbox" as const, providerKey: "test",
remoteCwd: root, runner: { execute }, streamRunLogs: false,
} : null;
const result = await createNativeGitHubAccess({ scope, target, cwd: root,
env: { PATH: `${bin}:${path.dirname(process.execPath)}:/usr/bin:/bin`, PAPERCLIP_API_KEY: "old-run-api-key" }, resolveCredentials, onLog },
bridgeUnavailable ? async () => { throw new Error("fixture bridge unavailable"); } : undefined);
cleanups.push(result.stop);
return { ...result, root, resolveCredentials };
return { ...result, root, resolveCredentials, processRunner, advanceGeneration: () => { generation++; }, retireProcess: () => { processLive = false; } };
}
function request(broker: NativeGitHubAccess, extra: RequestInit = {}, endpoint = "/runtime-tools/github/credentials") {
return fetch(broker.env.PAPERCLIP_GITHUB_BROKER_URL + endpoint, {
method: "POST", body: "{}", headers: { authorization: `Bearer ${broker.env.PAPERCLIP_GITHUB_BRIDGE_TOKEN}`, "content-type": "application/json" }, ...extra,
});
}
it.each([false, true])("keeps one live parent and its original launcher environment across two authorized runs (callback bridge: %s)", async (remote) => {
it.each([false, true, "computer"] as const)("keeps one live parent and its original launcher environment across two authorized runs (callback bridge: %s)", async (remote) => {
const b = await broker(undefined, remote);
const parent = spawn(process.execPath, ["-e", `
const {execFile}=require('node:child_process');
@@ -61,6 +84,7 @@ it.each([false, true])("keeps one live parent and its original launcher environm
expect(a.value).toBe("fixture-run-a");
releaseA();
expect((await operation()).value).toBe("anonymous");
b.advanceGeneration();
const releaseB = b.activate(run("run-b"));
releaseA(); // A's delayed cleanup cannot revoke B.
const second = await operation();
@@ -70,6 +94,11 @@ it.each([false, true])("keeps one live parent and its original launcher environm
expect(await readFile(path.join(b.env.PAPERCLIP_GITHUB_LAUNCHER_DIR, "gh"), "utf8")).not.toContain(b.env.PAPERCLIP_GITHUB_BROKER_TOKEN);
releaseB(); await b.stop();
await expect(access(b.env.PAPERCLIP_GITHUB_LAUNCHER_DIR)).rejects.toThrow();
if (remote === "computer") {
expect(b.processRunner.execute).toHaveBeenCalled();
b.retireProcess();
await expect(b.processRunner.execute({ command: "true" })).rejects.toThrow("computer_process_claim_stale");
}
}, 45_000);
it("rejects other scopes, concurrent bindings, browser requests and forged authority", async () => {
const b = await broker();
@@ -8028,6 +8028,41 @@ describe("native warm session supervision", () => {
expect(secondCopy.collectStopped).toHaveBeenCalledOnce();
});
it.each(["same", "generation", "owner", "computer"])("preserves command-transport directory identity across %s changes", async change => {
const name = `managed-computer-identity-${change}`;
const current = { ...execution, binding: { ...execution.binding, runId: `${name}-one`, executionWorkspaceId: name },
session: { ...execution.session, normalizedSessionId: name, lifecyclePolicy: { mode: "warm", idleTimeoutMs: 60_000 } } } as NativeExecutionInputV1;
const close = vi.fn(async () => undefined);
const collectStopped = vi.fn(async () => undefined);
const target = { kind: "remote", transport: "computer", environmentId: name, remoteCwd: `/tmp/${name}`,
resourceAuthority: { kind: "computer-owner", computerId: name, ownerId: name, generation: 1 } };
state.execute.mockReset().mockImplementationOnce(async options => { await options.onSession?.({ close }); return result; });
await executePaperclipNativeSession({ db: leaseDb(current), execution: current, runnerInstanceId: name,
runnerExecutionTarget: target as never,
instructionWorkingCopy: { runId: current.binding.runId, root: `/tmp/${name}/home`, collectStopped,
checkpointWarm: async () => true, hasChanges: async () => false } });
const canReuse = vi.fn(async () => true);
const nextAuthority = { ...target.resourceAuthority,
...(change === "generation" ? { generation: 2 } : {}),
...(change === "owner" ? { ownerId: "different-owner" } : {}),
...(change === "computer" ? { computerId: "different-computer" } : {}),
};
const reservation = await reserveWarmNativeInstructionDirectory({ companyId: current.binding.companyId,
agentId: current.binding.agentId, previousRunId: current.binding.runId, runId: `${name}-two`, canReuse,
target: { ...target, resourceAuthority: nextAuthority } as never });
if (change === "same" || change === "generation") {
expect(reservation?.reuseRunId).toBe(current.binding.runId);
expect(canReuse).toHaveBeenCalledOnce();
expect(close).not.toHaveBeenCalled();
await reservation!.release();
} else {
expect(reservation).toBeNull();
expect(canReuse).not.toHaveBeenCalled();
expect(close).toHaveBeenCalledOnce();
expect(collectStopped).toHaveBeenCalledOnce();
}
});
it("stops before collecting when a checkpoint cannot stabilize", async () => {
const f = await start("managed-fallback", false);
expect(f.close).toHaveBeenCalledOnce();