Merge commit '46b759888' into codex/runner-copilot-acp

* commit '46b759888':
  Bind ACP persistent agent files to registered runtime context
This commit is contained in:
Dotta committed 2026-09-29 09:45:41 -05:00
commit a8a503ee13
7 files changed
+181 -5

No files matched your search

@@ -21,6 +21,18 @@ recommends Cursor and Copilot, followed by Pi, using the existing qualified ACPX
path. Codex app-server is the richness benchmark. The legacy Cursor and Pi
adapters are outside this change.
## Registered persistent agent files
Candidate ACP processes receive `AGENT_HOME` only from the authenticated
runtime context's server-registered `agent_files` working copy. Ambient
environment values cannot grant a directory. Admission rejects symlink roots,
filesystem roots and overlap with provider runtime state; directory identity is
rechecked at launch and before each turn. Each provider reopen receives the
current run's registered copy. The native executor requires provider shutdown
before collecting and synchronizing the copy. Pi additionally receives a
runner-owned native-tool root binding; its task read-only policy still applies.
These checks do not by themselves qualify provider-specific native file tools.
## Branches and evidence ownership
| Branch | Deliverable |
@@ -14,7 +14,7 @@ import {
export interface CodexAcpxNativeSessionBackendOptions extends Omit<
CodexAcpxDriverOptions,
"model" | "permissionMode" | "systemInstructions" | "providerPolicy"
"model" | "permissionMode" | "systemInstructions" | "providerPolicy" | "runtimeContext"
> {}
export type AcpxNativeSessionBackendOptions =
@@ -73,6 +73,7 @@ export function createAcpxNativeSessionBackend(
model: input.provider.model,
permissionMode: input.provider.permissionMode ?? "approve-reads",
systemInstructions,
runtimeContext: "runtimeContext" in input ? input.runtimeContext : null,
providerPolicy: { readOnly: "executionMode" in input && input.executionMode === "plan" },
}),
);
@@ -0,0 +1,56 @@
import { mkdtempSync, mkdirSync, renameSync, rmSync, symlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { NativeRuntimeContextSnapshot } from "../../contracts/runtime-context.js";
import { bindAcpxAgentFiles } from "./agent-files-binding.js";
const roots: string[] = [];
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
function fixture() {
const parent = mkdtempSync(join(tmpdir(), "acpx-agent-files-"));
roots.push(parent);
const root = join(parent, "registered-run-copy");
const protectedRoot = join(parent, "provider-state");
mkdirSync(root); mkdirSync(protectedRoot);
const context = { instructions: { workingCopy: { kind: "agent_files", rootPath: root, entryPath: "AGENTS.md" } } } as NativeRuntimeContextSnapshot;
return { parent, root, protectedRoot, context };
}
describe("registered ACP agent files", () => {
it("does not grant absent or legacy instruction copies", () => {
expect(bindAcpxAgentFiles(null, [])).toBeNull();
const { context } = fixture();
delete context.instructions.workingCopy!.kind;
expect(bindAcpxAgentFiles(context, [])).toBeNull();
});
it("binds only the registered copy and detects replaced roots", () => {
const { root, context, protectedRoot } = fixture();
const binding = bindAcpxAgentFiles(context, [protectedRoot])!;
expect(binding.root.endsWith("/registered-run-copy")).toBe(true);
expect(() => binding.assertHeld()).not.toThrow();
renameSync(root, `${root}-old`); mkdirSync(root);
expect(() => binding.assertHeld()).toThrow("changed");
});
it("rejects symlink roots and symlink replacement", () => {
const { root, context, protectedRoot } = fixture();
const binding = bindAcpxAgentFiles(context, [])!;
renameSync(root, `${root}-old`); symlinkSync(protectedRoot, root);
expect(() => binding.assertHeld()).toThrow("changed");
expect(() => bindAcpxAgentFiles(context, [])).toThrow("real directory");
});
it("rejects both directions of protected-root overlap", () => {
const { parent, root, context } = fixture();
const child = join(root, "nested-runtime"); mkdirSync(child);
for (const protectedRoot of [parent, root, child]) {
expect(() => bindAcpxAgentFiles(context, [protectedRoot])).toThrow("overlaps");
}
});
it("rejects relative, non-normalized and filesystem-root grants", () => {
const { context } = fixture();
for (const rootPath of ["relative", "/tmp/../tmp", "/", "/tmp\0invalid"]) {
context.instructions.workingCopy!.rootPath = rootPath;
expect(() => bindAcpxAgentFiles(context, [])).toThrow();
}
});
});
@@ -0,0 +1,48 @@
import { lstatSync, realpathSync } from "node:fs";
import { isAbsolute, relative, resolve } from "node:path";
import type { NativeRuntimeContextSnapshot } from "../../contracts/runtime-context.js";
export interface AcpxAgentFilesBinding {
readonly root: string;
assertHeld(): void;
}
/** The authenticated runtime context, never an ambient environment value, grants this run copy. */
export function bindAcpxAgentFiles(
context: NativeRuntimeContextSnapshot | null | undefined,
protectedRoots: readonly string[],
): AcpxAgentFilesBinding | null {
const copy = context?.instructions.workingCopy;
if (copy?.kind !== "agent_files") return null;
const declared = copy.rootPath;
if (!isAbsolute(declared) || declared.includes("\0") || resolve(declared) !== declared) {
throw new Error("ACP agent directory must be an absolute normalized registered path");
}
const metadata = lstatSync(declared);
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
throw new Error("ACP agent directory must be a real directory");
}
const root = realpathSync(declared);
if (resolve(root, "..") === root) throw new Error("ACP agent directory cannot be a filesystem root");
for (const protectedRoot of protectedRoots) {
// These roots belong to the verified runtime and sandbox, not user input.
const canonical = realpathSync(protectedRoot);
if (contains(root, canonical) || contains(canonical, root)) {
throw new Error("ACP agent directory overlaps protected runtime state");
}
}
const assertHeld = () => {
const current = lstatSync(declared);
if (!current.isDirectory() || current.isSymbolicLink()
|| current.dev !== metadata.dev || current.ino !== metadata.ino
|| realpathSync(declared) !== root) {
throw new Error("ACP registered agent directory changed during provider lifetime");
}
};
return Object.freeze({ root, assertHeld });
}
function contains(root: string, path: string): boolean {
const rel = relative(root, path);
return rel === "" || (!rel.startsWith("../") && rel !== ".." && !isAbsolute(rel));
}
@@ -141,6 +141,7 @@ export interface CodexAcpxDriverOptions {
model: string;
permissionMode?: NativeAcpxPermissionMode;
providerPolicy?: { readOnly: boolean };
runtimeContext?: OpenAcpxRuntimeHostOptions["runtimeContext"];
systemInstructions?: string;
environment?: NodeJS.ProcessEnv;
managedCodexCredentialSourcePath?: string;
@@ -453,6 +454,7 @@ export class CodexAcpxDriver implements HarnessDriver {
model: this.#options.model,
permissionMode: this.#options.permissionMode ?? "approve-all",
providerPolicy: this.#options.providerPolicy,
runtimeContext: this.#options.runtimeContext,
systemInstructions: this.#options.systemInstructions,
environment: this.#options.environment,
managedCodexCredentialSourcePath:
@@ -1,4 +1,4 @@
import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -832,6 +832,43 @@ describe("ACPX runtime host", () => {
expect(openRuntime).toHaveBeenCalledOnce();
});
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-"));
temporaryDirectories.push(copies);
const model = agent === "pi" ? "openrouter/deepseek/deepseek-v4-flash-0731" : "explicit-test-model";
const options = { ...fixture.options, agent, model, permissionMode: "approve-all" as const,
providerPolicy: { readOnly: false }, environment: { AGENT_HOME: "/ambient/other-agent", PAPERCLIP_PI_AGENT_HOME: "/ambient/other-agent",
...(agent === "pi" ? { OPENROUTER_API_KEY: "test" } : agent === "cursor" ? { CURSOR_API_KEY: "test" } : { COPILOT_GITHUB_TOKEN: "test" }),
},
};
const opened: AcpxRuntimePortOpenOptions[] = [];
const dependencies = fixture.dependencies({ openRuntime: async launch => {
opened.push(launch);
return runtimePort({ getStatus: async () => ({ models: { currentModelId: model } }) });
} });
// Missing trusted context must not turn ambient values into authority.
const withoutCopy = await AcpxRuntimeHost.open(options, dependencies);
expect(opened.at(-1)!.launchEnvironment.AGENT_HOME).toBeUndefined();
expect(opened.at(-1)!.launchEnvironment.PAPERCLIP_PI_AGENT_HOME).toBeUndefined();
await withoutCopy.close({ reason: "no registered copy" });
for (const run of ["first", "second"]) {
const rootPath = join(copies, run); await mkdir(rootPath);
const runtimeContext = { instructions: { workingCopy: { kind: "agent_files", rootPath, entryPath: "AGENTS.md" } }, skills: [], mcp: { bindingId: null } } as unknown as NativeRuntimeContextSnapshot;
const host = await AcpxRuntimeHost.open({ ...options, runtimeContext }, dependencies);
const launch = opened.at(-1)!;
expect(launch.launchEnvironment.AGENT_HOME?.endsWith(`/${run}`)).toBe(true);
expect(launch.launchEnvironment.PAPERCLIP_PI_AGENT_HOME).toBe(agent === "pi" ? launch.launchEnvironment.AGENT_HOME : undefined);
expect(launch.launchEnvironment.HOME).not.toBe(launch.launchEnvironment.AGENT_HOME);
if (run === "second") {
await rename(rootPath, `${rootPath}-replaced`); await mkdir(rootPath);
expect(() => launch.assertWorkspaceHeld?.()).toThrow("changed");
expect(() => host.startTurn({ text: "test", requestId: "stale-directory" })).toThrow("changed");
}
await host.close({ reason: "registered copy test complete" });
}
});
it("selects and verifies Claude's qualified reported model", async () => {
const fixture = await hostFixture();
let selected = false;
@@ -1,4 +1,5 @@
import { dirname, join } from "node:path";
import { bindAcpxAgentFiles } from "./agent-files-binding.js";
import { assertAcpxProfileEnvironment, assertAcpxProfileWorkspace, classifyAcpxProfileError, verifyAcpxProfileInstallation } from "./profile-installation.js";
import { createAcpxRuntimeSkillLease } from "./runtime-skill-lease.js";
import { stageManagedGrokCredential } from "./grok-credentials.js";
@@ -278,6 +279,7 @@ export class AcpxRuntimeHost {
readonly #command: VerifiedAcpxCommandLease;
readonly #toolBridge: RunnerToolBridge | null;
readonly #claudeSkillNames: readonly string[];
readonly #assertAgentFilesHeld: (() => void) | undefined;
#activeTurn: AcpxRuntimeTurn | null = null;
#closingStarted = false;
#closePromise: Promise<void> | null = null;
@@ -292,6 +294,7 @@ export class AcpxRuntimeHost {
command: VerifiedAcpxCommandLease;
toolBridge: RunnerToolBridge | null;
claudeSkillNames: readonly string[];
assertAgentFilesHeld?: () => void;
}) {
this.#runtime = input.runtime;
this.#binding = input.binding;
@@ -301,6 +304,7 @@ export class AcpxRuntimeHost {
this.#command = input.command;
this.#toolBridge = input.toolBridge;
this.#claudeSkillNames = [...input.claudeSkillNames];
this.#assertAgentFilesHeld = input.assertAgentFilesHeld;
}
static async open(
@@ -458,6 +462,19 @@ export class AcpxRuntimeHost {
}
assertAcpxProfileEnvironment(options.agent, sandbox.launchEnvironment);
let launchEnvironment = sandbox.launchEnvironment;
// This copy is registered by the authenticated controller for its company,
// agent and run. Environment/config values cannot widen the grant. Each
// resumed process receives the newly registered copy; collection already
// requires verified provider shutdown in the native executor.
const agentFiles = ["cursor", "copilot", "pi"].includes(options.agent)
? bindAcpxAgentFiles(options.runtimeContext, [sandbox.root,
...(installation.agentServerPackageJsonPath === null ? [] : [dirname(installation.agentServerPackageJsonPath)]),
]) : null;
if (agentFiles) {
launchEnvironment = Object.freeze({ ...launchEnvironment, AGENT_HOME: agentFiles.root,
...(options.agent === "pi" ? { PAPERCLIP_PI_AGENT_HOME: agentFiles.root } : {}),
});
}
if (options.agent === "pi") {
const skills = await acquireAbortableAdmissionResource({
signal: options.signal,
@@ -476,7 +493,7 @@ export class AcpxRuntimeHost {
await skills.close();
},
};
launchEnvironment = Object.freeze({ ...sandbox.launchEnvironment,
launchEnvironment = Object.freeze({ ...launchEnvironment,
PAPERCLIP_PI_READ_ROOTS: JSON.stringify([...(options.providerPolicy?.readRoots ?? []), ...skills.readRoots]),
});
}
@@ -523,6 +540,7 @@ export class AcpxRuntimeHost {
signal: options.signal,
acquire: async () => {
options.assertWorkspaceHeld?.();
agentFiles?.assertHeld();
await assertAcpxProfileWorkspace(options.agent, binding.workspacePath);
return dependencies.openRuntime({
...(options.clientCapabilities === undefined ? {} : {
@@ -543,9 +561,9 @@ export class AcpxRuntimeHost {
activateCredentialFenceOwner:
admittedLifetime.activateLifetimeOwner.bind(admittedLifetime),
systemInstructions: boundedInstructions(options.systemInstructions),
...(options.assertWorkspaceHeld === undefined
...(options.assertWorkspaceHeld === undefined && !agentFiles
? {}
: { assertWorkspaceHeld: options.assertWorkspaceHeld }),
: { assertWorkspaceHeld: () => { options.assertWorkspaceHeld?.(); agentFiles?.assertHeld(); } }),
...(options.signal === undefined ? {} : { signal: options.signal }),
mcpServers: [
...(toolBridge ? [{ name: "paperclip", url: toolBridge.url,
@@ -618,6 +636,7 @@ export class AcpxRuntimeHost {
credential,
command,
toolBridge,
assertAgentFilesHeld: agentFiles?.assertHeld,
claudeSkillNames: options.agent === "claude"
? (options.runtimeContext?.skills.map((skill) => skill.runtimeName) ?? [])
: [],
@@ -741,6 +760,7 @@ export class AcpxRuntimeHost {
}
startTurn(input: AcpxRuntimeTurnInput): AcpxRuntimeTurn {
this.#assertAgentFilesHeld?.();
if (this.#closed || this.#closingStarted) {
throw new Error("ACPX runtime host is closing");
}