mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-08 21:03:51 +02:00
test(runner): scope remote evidence around verified runtime state
Admit the actual staged runtime layout, retain sentinel and sibling mutation coverage, distinguish observed PRP identity from the sandbox lease, and bound setup readiness inside the caller deadline. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
63eae696e7
commit
133793b419
2 files changed
+94
-28
No files matched your search
@@ -16,7 +16,7 @@ function snapshot(): RemoteNativeSnapshot {
|
||||
return { binding, observedAtMs: 1000, observedMonotonicNs: "10000", receivedAtMs: 1000, complete: true, workspace: { "existing.txt": hash("original") },
|
||||
targets: { "result.txt": { absent: true, sha256: null, parent: { dev: "1", ino: "2" }, mutationCount: 0, complete: true } },
|
||||
watcher: { complete: true, targetMutationCount: 0, workspaceMutationCount: 0 },
|
||||
processes: { captured: true, root, journal: [root], live: [21] }, setup: { path: "action.txt", sha256: null, published: false }, attached: null };
|
||||
processes: { captured: true, root, journal: [root], live: [21] }, scope: { kind: "user_workspace", excludedRuntime: { relativePath: ".paperclip-runtime/paperclip-runner", absolutePath: "/workspace/.paperclip-runtime/paperclip-runner", dev: "1", ino: "4", runnerExecutableSha256: hash("runnerd") }, observedPrpEnvironmentLeaseId: "workspace-id", prpEnvironmentLeaseIdVerified: false }, setup: { path: "action.txt", sha256: null, published: false }, attached: null };
|
||||
}
|
||||
function harness() {
|
||||
let lease: Record<string, unknown> = { id: "lease", companyId: "company", environmentId: "environment", heartbeatRunId: "run", provider: "daytona", providerLeaseId: "sandbox", status: "active", releasedAt: null,
|
||||
@@ -42,7 +42,7 @@ function harness() {
|
||||
});
|
||||
const get = vi.fn(async () => ({ id: "sandbox", labels, process: { executeCommand } }));
|
||||
const apiGet = vi.fn(async (path: string) => path.includes("/environments/") ? [structuredClone(lease)] : structuredClone(lease));
|
||||
const options: RemoteNativeFixtureOptions = { api: { get: apiGet as RemoteNativeFixtureOptions["api"]["get"] }, daytona: { get }, sdkVersion: "0.203.0", authority, nodeSha256: hash("node"), runnerdSha256: hash("runnerd"), targets: ["result.txt"], actionFile: "action.txt" };
|
||||
const options: RemoteNativeFixtureOptions = { api: { get: apiGet as RemoteNativeFixtureOptions["api"]["get"] }, daytona: { get }, sdkVersion: "0.203.0", authority, nodeSha256: hash("node"), runnerdSha256: hash("runnerd"), targets: ["result.txt"], actionFile: "action.txt", deadlineAt: Date.now() + 60_000 };
|
||||
return { options, current, labels, calls, executeCommand, apiGet, get, resolveTerminal, rejectTerminal,
|
||||
setLease(value: Record<string, unknown>) { lease = value; }, lease: () => lease, override(fn: typeof override) { override = fn; } };
|
||||
}
|
||||
@@ -52,6 +52,11 @@ describe("remote native lease admission", () => {
|
||||
const h = harness(); h.setLease({ ...h.lease(), [key]: "foreign" });
|
||||
await expect(bindRemoteNativeFixture(h.options)).rejects.toThrow("lease_scope"); expect(h.executeCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
it("rejects protected runtime targets and insufficient setup budget before SDK access", async () => {
|
||||
for (const patch of [{ targets: [".paperclip-runtime/paperclip-runner/bin/forbidden"] }, { actionFile: ".paperclip-runtime/paperclip-runner/task.txt" }, { deadlineAt: Date.now() + 1000 }]) {
|
||||
const h = harness(); await expect(bindRemoteNativeFixture({ ...h.options, ...patch })).rejects.toThrow(); expect(h.get).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
it("requires exact SDK, immutable image and executable hashes", async () => {
|
||||
for (const input of [{ sdkVersion: "0.204.0" }, { nodeSha256: "unknown" }, { authority: { ...authority, image: "image:latest" } }]) {
|
||||
const h = harness(); await expect(bindRemoteNativeFixture({ ...h.options, ...input } as RemoteNativeFixtureOptions)).rejects.toThrow(); expect(h.get).not.toHaveBeenCalled();
|
||||
@@ -128,6 +133,10 @@ describe("remote native lease admission", () => {
|
||||
const h = harness(); await bindRemoteNativeFixture(h.options);
|
||||
const install = h.calls[0]!; const source = install.request.source as string;
|
||||
expect(() => new Script(source)).not.toThrow(); expect(source).not.toContain("__name(");
|
||||
const rpcQuoted = install.command.match(/ -e (.+) '[A-Za-z0-9+/=]+'$/su)![1]!;
|
||||
const rpc = rpcQuoted.slice(1, -1).replaceAll("'\\''", "'");
|
||||
expect(() => new Script(rpc)).not.toThrow();
|
||||
expect(rpc).toContain("Date.now()+20000"); expect(install.timeout).toBe(25);
|
||||
expect(source).toContain("/proc/"); expect(source).toContain("workspaceWatch"); expect(source).toContain("finalReceipt.files");
|
||||
expect(install.command).toMatch(/^\/usr\/bin\/env -i PATH=\/usr\/bin:\/bin /u);
|
||||
expect(install.request.config.runnerdSha256).toBe(hash("runnerd"));
|
||||
@@ -169,12 +178,13 @@ describe("actual generated observer state machine", () => {
|
||||
const h = harness(); await bindRemoteNativeFixture(h.options);
|
||||
const { source, config } = h.calls[0]!.request;
|
||||
const intervals: Array<() => void> = [], timers: Array<{ fn: () => void; ms: number }> = [];
|
||||
const proc = new Map<number, { ppid: number; group: number; ticks: string; argv: string[] }>([[21, { ppid: 1, group: 21, ticks: "100", argv: ["/opt/bin/paperclip-runnerd", "--run-id", "run", "--environment-lease-id", "lease", "--lifecycle-mode", "per_turn"] }]]);
|
||||
const proc = new Map<number, { ppid: number; group: number; ticks: string; argv: string[] }>([[21, { ppid: 1, group: 21, ticks: "100", argv: ["/workspace/.paperclip-runtime/paperclip-runner/bin/paperclip-runnerd", "--run-id", "run", "--environment-lease-id", "workspace-id", "--lifecycle-mode", "per_turn", "--state-dir", "/workspace/.paperclip-runtime/paperclip-runner/sessions/" + "a".repeat(64) + "/runner"] }]]);
|
||||
const files = new Map<string, Buffer>([[`${config.root}/observer.cjs`, Buffer.from(source)], [config.sentinel.path, Buffer.from(JSON.stringify({ version: 1, provider: "daytona", token: config.sentinel.token, companyId: "company", environmentId: "environment" }))]]);
|
||||
const watches: Array<{ path: string; callback: (_kind: string, name: string | null) => void; closed: boolean }> = [];
|
||||
const handlers: Array<(socket: any) => void> = [], children: any[] = [];
|
||||
const missing = () => { throw Object.assign(new Error("missing"), { code: "ENOENT" }); };
|
||||
const fds = new Map<number, string>(); let nextFd = 50;
|
||||
const fds = new Map<number, string>(); let nextFd = 50, runtimeInode = 4n;
|
||||
const symbolicLinks = new Set<string>();
|
||||
const fs = {
|
||||
constants: { O_RDONLY: 0, O_NOFOLLOW: 131072 },
|
||||
openSync(path: string, flags: number) { expect(flags).toBe(131072); if (!files.has(path)) return missing(); const fd = nextFd++; fds.set(fd, path); return fd; },
|
||||
@@ -193,19 +203,19 @@ describe("actual generated observer state machine", () => {
|
||||
if (!value) return missing(); return encoding ? value.toString() : value;
|
||||
},
|
||||
lstatSync(path: string) {
|
||||
const directory = path === config.root || path === "/workspace";
|
||||
if (!directory && !files.has(path)) return missing();
|
||||
return { dev: 1n, ino: path === config.root ? 2n : 3n, mtimeNs: 4n, ctimeNs: 5n, isDirectory: () => directory, isFile: () => !directory, isSymbolicLink: () => false, size: files.get(path)?.length ?? 0 };
|
||||
const directory = path === config.root || path === "/workspace" || path === "/workspace/.paperclip-runtime" || path === "/workspace/.paperclip-runtime/paperclip-runner";
|
||||
if (!directory && !files.has(path) && !symbolicLinks.has(path)) return missing();
|
||||
return { dev: 1n, ino: path === config.root ? 2n : path === "/workspace/.paperclip-runtime/paperclip-runner" ? runtimeInode : 3n, mtimeNs: 4n, ctimeNs: 5n, isDirectory: () => directory, isFile: () => !directory, isSymbolicLink: () => symbolicLinks.has(path), size: files.get(path)?.length ?? 0 };
|
||||
},
|
||||
realpathSync: (path: string) => path,
|
||||
readdirSync(path: string) { if (path === "/proc") return [...proc.keys()].map(String); if (path === "/workspace") return [...files.keys()].filter(p => p.startsWith("/workspace/") && !p.slice(11).includes("/")).map(p => p.slice(11)); return []; },
|
||||
readdirSync(path: string) { if (path === "/proc") return [...proc.keys()].map(String); if (path === "/workspace") return [".paperclip-runtime", ...[...files.keys(), ...symbolicLinks].filter(p => p.startsWith("/workspace/") && !p.slice(11).includes("/")).map(p => p.slice(11))]; if (path === "/workspace/.paperclip-runtime") return ["reusable-sandbox-lease.json", "paperclip-runner", ...[...files.keys()].filter(p => p.startsWith(path + "/") && !p.slice(path.length + 1).includes("/") && !p.endsWith("reusable-sandbox-lease.json")).map(p => p.slice(path.length + 1))]; if (path.startsWith("/workspace/.paperclip-runtime/paperclip-runner")) throw new Error("excluded runtime must not be traversed"); return []; },
|
||||
watch(path: string, options: unknown, callback?: (_kind: string, name: string | null) => void) {
|
||||
const entry = { path, callback: (callback ?? options) as (_kind: string, name: string | null) => void, closed: false }; watches.push(entry);
|
||||
return Object.assign(new EventEmitter(), { close: () => { entry.closed = true; } });
|
||||
},
|
||||
writeFileSync(path: string, content: string, opts: { flag: string }) {
|
||||
if (opts.flag === "wx" && files.has(path)) throw new Error("EEXIST"); files.set(path, Buffer.from(content));
|
||||
for (const w of watches) if (!w.closed && path.startsWith(w.path + "/")) w.callback("rename", path.slice(w.path.length + 1));
|
||||
for (const w of watches) if (!w.closed && path.slice(0, path.lastIndexOf("/")) === w.path) w.callback("rename", path.slice(w.path.length + 1));
|
||||
},
|
||||
rmSync: vi.fn(),
|
||||
};
|
||||
@@ -223,7 +233,7 @@ describe("actual generated observer state machine", () => {
|
||||
const replies: any[] = [], socket = Object.assign(new EventEmitter(), { end: (value: string) => replies.push(JSON.parse(value)), destroy: vi.fn() });
|
||||
handlers[0]!(socket); socket.emit("data", Buffer.from(JSON.stringify({ op, nonce: config.nonce, ...args }) + "\n")); return replies;
|
||||
}
|
||||
return { request, proc, files, watches, fs, intervals, timers, config, handlers, children };
|
||||
return { request, proc, files, watches, fs, intervals, timers, config, handlers, children, symbolicLinks, replaceRuntimeRoot() { runtimeInode = 999n; } };
|
||||
}
|
||||
it("acknowledges receipt-channel readiness only after the long waiter connects", async () => {
|
||||
const o = await observerHarness(); const arm = o.request("arm"); expect(arm).toHaveLength(0);
|
||||
@@ -269,6 +279,27 @@ describe("actual generated observer state machine", () => {
|
||||
expect(o.children).toHaveLength(0); expect(socket.destroy).toHaveBeenCalled();
|
||||
expect(o.request("snapshot")[0].result.attached.failure).toBe("client_rejected");
|
||||
});
|
||||
it("scopes actual runtime symlinks/state churn out while retaining sentinel and sibling coverage", async () => {
|
||||
const o = await observerHarness();
|
||||
o.symbolicLinks.add("/workspace/.paperclip-runtime/paperclip-runner/provider-pack");
|
||||
o.files.set("/workspace/.paperclip-runtime/paperclip-runner/bin/paperclip-runnerd", Buffer.alloc(100000));
|
||||
o.fs.writeFileSync("/workspace/.paperclip-runtime/paperclip-runner/sessions/state.json", "runtime churn", { flag: "wx" });
|
||||
const before = o.request("snapshot")[0].result;
|
||||
expect(before.complete).toBe(true); expect(before.watcher.workspaceMutationCount).toBe(0);
|
||||
expect(Object.keys(before.workspace)).toContain(".paperclip-runtime/reusable-sandbox-lease.json");
|
||||
expect(Object.keys(before.workspace).some(p => p.startsWith(".paperclip-runtime/paperclip-runner"))).toBe(false);
|
||||
expect(before.scope.excludedRuntime.ino).toBe("4"); expect(before.scope.observedPrpEnvironmentLeaseId).toBe("workspace-id");
|
||||
expect(before.scope.prpEnvironmentLeaseIdVerified).toBe(false);
|
||||
o.fs.writeFileSync("/workspace/.paperclip-runtime/user-file", "not runtime internal", { flag: "wx" });
|
||||
const after = o.request("snapshot")[0].result;
|
||||
expect(after.watcher.workspaceMutationCount).toBe(1); expect(after.workspace[".paperclip-runtime/user-file"]).toBe(hash("not runtime internal"));
|
||||
});
|
||||
it("rejects runtime root replacement, foreign workspace symlinks and sentinel tampering", async () => {
|
||||
const replaced = await observerHarness(); replaced.replaceRuntimeRoot(); expect(replaced.request("snapshot")[0].ok).toBe(false);
|
||||
const linked = await observerHarness(); linked.symbolicLinks.add("/workspace/user-link"); expect(linked.request("snapshot")[0].ok).toBe(false);
|
||||
const sentinel = await observerHarness(); sentinel.files.set(sentinel.config.sentinel.path, Buffer.from(JSON.stringify({ token: "foreign" })));
|
||||
expect(sentinel.request("snapshot")[0].ok).toBe(false);
|
||||
});
|
||||
it("marks PID reuse incomplete rather than mistaking a new process for retired authority", async () => {
|
||||
const o = await observerHarness(); o.request("snapshot"); const wait = o.request("wait");
|
||||
o.proc.set(21, { ppid: 1, group: 99, ticks: "999", argv: ["/unrelated"] }); o.intervals[0]!(); o.timers.find(t => t.ms === 100)!.fn();
|
||||
@@ -277,16 +308,16 @@ describe("actual generated observer state machine", () => {
|
||||
it("counts transient workspace create/delete and rejects changed setup-file bytes", async () => {
|
||||
const o = await observerHarness(); o.request("snapshot");
|
||||
o.fs.writeFileSync("/workspace/transient.txt", "not allowed", { flag: "wx" }); o.files.delete("/workspace/transient.txt");
|
||||
for (const w of o.watches) w.callback("rename", "transient.txt");
|
||||
for (const w of o.watches) if (w.path === "/workspace") w.callback("rename", "transient.txt");
|
||||
const snapshot = o.request("snapshot")[0].result;
|
||||
expect(snapshot.workspace["transient.txt"]).toBeUndefined(); expect(snapshot.watcher.workspaceMutationCount).toBe(2);
|
||||
o.request("publish", { path: "action.txt", text: "approved" }); o.files.set("/workspace/action.txt", Buffer.from("replacement"));
|
||||
expect(o.request("snapshot")[0].ok).toBe(false);
|
||||
});
|
||||
it("rejects ambiguous run roots and mismatched lease flags", async () => {
|
||||
it("rejects ambiguous run roots and malformed observed PRP identifiers", async () => {
|
||||
const o = await observerHarness(); const p = o.proc.get(21)!;
|
||||
o.proc.set(22, { ...p, group: 22, ticks: "200" }); expect(o.request("snapshot")[0].result.complete).toBe(false);
|
||||
const other = await observerHarness(); other.proc.get(21)!.argv[4] = "foreign-lease";
|
||||
const other = await observerHarness(); other.proc.get(21)!.argv[4] = "bad value with spaces";
|
||||
expect(other.request("snapshot")[0].ok).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,11 @@ import { posix } from "node:path";
|
||||
export const REMOTE_FIXTURE_DAYTONA_SDK_VERSION = "0.203.0";
|
||||
const NODE = "/opt/paperclip-runner/provider-pack/node_modules/node/bin/node";
|
||||
const MAX_OUTPUT = 256 * 1024;
|
||||
// createRunnerdBackend stages its verified executable, pack symlink, mutable
|
||||
// sessions, homes and injected context beneath this exact path. Qualification
|
||||
// covers user workspace files, not these controller/provider runtime internals.
|
||||
// The sentinel and all other .paperclip-runtime entries remain in scope.
|
||||
const RUNTIME_RELATIVE = ".paperclip-runtime/paperclip-runner";
|
||||
const digest = (value: string) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
||||
const record = (value: unknown): Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
const fail = (condition: unknown, code: string): void => { if (!condition) throw new Error(`remote_native_fixture:${code}`); };
|
||||
@@ -27,6 +32,12 @@ export interface RemoteNativeSnapshot {
|
||||
targets: Record<string, { absent: boolean; sha256: string | null; parent: { dev: string; ino: string }; mutationCount: number; complete: boolean }>;
|
||||
watcher: { complete: boolean; targetMutationCount: number; workspaceMutationCount: number };
|
||||
processes: { captured: boolean; root: RemoteProcessIdentity | null; journal: RemoteProcessIdentity[]; live: number[] };
|
||||
scope: {
|
||||
kind: "user_workspace";
|
||||
excludedRuntime: { relativePath: string; absolutePath: string; dev: string; ino: string; runnerExecutableSha256: string };
|
||||
observedPrpEnvironmentLeaseId: string;
|
||||
prpEnvironmentLeaseIdVerified: false;
|
||||
};
|
||||
setup: { path: string; sha256: string | null; published: boolean };
|
||||
attached: { connections: number; failure: string | null; commandExit: { code: number; observedAtMs: number; observedMonotonicNs: string } | null; markerWrittenAtMs: number | null; markerWrittenMonotonicNs: string | null; clientExitedAtMs: number | null; clientExitedMonotonicNs: string | null } | null;
|
||||
}
|
||||
@@ -87,6 +98,7 @@ const config=JSON.parse(Buffer.from(process.argv[2],'base64').toString());
|
||||
const parseStat=PARSE_STAT;const runRoot=RUN_ROOT;const watchTarget=WATCH_TARGET;
|
||||
const hash=x=>'sha256:'+crypto.createHash('sha256').update(x).digest('hex');
|
||||
const cwdStat=fs.lstatSync(config.binding.remoteCwd,{bigint:true}),rootStat=fs.lstatSync(config.root,{bigint:true}),scriptStat=fs.lstatSync(__filename,{bigint:true}),scriptHash=hash(fs.readFileSync(__filename));
|
||||
const runtimeRoot=path.join(config.binding.remoteCwd,config.runtimeRelative),runtimeStat=fs.lstatSync(runtimeRoot,{bigint:true});if(!runtimeStat.isDirectory()||runtimeStat.isSymbolicLink()||fs.realpathSync(runtimeRoot)!==runtimeRoot)throw Error('runtime_root_identity');let observedPrpEnvironmentLeaseId=null;
|
||||
const boot=fs.readFileSync('/proc/sys/kernel/random/boot_id','utf8').trim();
|
||||
const targets=new Map();let complete=true,sealed=false,root=null,attached=null,child=null,client=null,childTimer=null;
|
||||
const journal=new Map(),sockets=new Set(),waiters=new Set(),armWaiters=new Set();let observedRootCount=0,publishedHash=null,finalReceipt=null,retiring=false;
|
||||
@@ -95,7 +107,7 @@ function table(){const ids=fs.readdirSync('/proc').filter(x=>/^\d+$/.test(x)&&Nu
|
||||
function sample(){
|
||||
const all=table();const candidates=[];
|
||||
for(const p of all){if(p.state==='Z')continue;let argv;try{argv=fs.readFileSync('/proc/'+p.pid+'/cmdline').toString().split('\0').filter(Boolean)}catch(e){if(e.code==='ENOENT'||e.code==='ESRCH')continue;throw e}
|
||||
if(runRoot(argv,config.binding.runId,p)){const at=argv.indexOf('--environment-lease-id');if(at<1||argv.lastIndexOf('--environment-lease-id')!==at||argv[at+1]!==config.binding.leaseId)throw Error('process_lease_identity');candidates.push(p);}}
|
||||
if(runRoot(argv,config.binding.runId,p)){const at=argv.indexOf('--environment-lease-id'),stateAt=argv.indexOf('--state-dir');if(at<1||argv.lastIndexOf('--environment-lease-id')!==at||!/^[-a-zA-Z0-9._:]{1,256}$/.test(argv[at+1]??''))throw Error('process_prp_identity_shape');if(argv[0]!==path.join(runtimeRoot,'bin','paperclip-runnerd')||stateAt<1||argv.lastIndexOf('--state-dir')!==stateAt||!argv[stateAt+1]?.startsWith(runtimeRoot+'/sessions/')||!/^([a-f0-9]{64})\/runner$/.test(argv[stateAt+1].slice((runtimeRoot+'/sessions/').length)))throw Error('process_runtime_binding');if(observedPrpEnvironmentLeaseId!==null&&observedPrpEnvironmentLeaseId!==argv[at+1])throw Error('process_prp_identity_changed');observedPrpEnvironmentLeaseId=argv[at+1];candidates.push(p);}}
|
||||
if(candidates.length>1)complete=false;
|
||||
if(!root&&candidates.length===1){if(hash(fs.readFileSync('/proc/'+candidates[0].pid+'/exe'))!==config.runnerdSha256)throw Error('runner_binary_identity');root=candidates[0];journal.set(root.pid,root);observedRootCount++;}
|
||||
if(root&&candidates.some(p=>p.pid!==root.pid||p.startTicks!==root.startTicks))complete=false;
|
||||
@@ -105,16 +117,20 @@ function sample(){
|
||||
if(client&&!all.some(p=>p.pid===client.pid&&p.startTicks===client.startTicks&&p.state!=='Z')&&attached&&!attached.clientExitedAtMs){attached.clientExitedAtMs=Date.now();attached.clientExitedMonotonicNs=process.hrtime.bigint().toString();}
|
||||
return {captured:root!==null,root,journal:[...journal.values()],live};
|
||||
}
|
||||
function guard(){const s=fs.lstatSync(config.root,{bigint:true});if(s.dev!==rootStat.dev||s.ino!==rootStat.ino||!s.isDirectory()||s.isSymbolicLink()||hash(fs.readFileSync(__filename))!==scriptHash||fs.lstatSync(__filename,{bigint:true}).ino!==scriptStat.ino)throw Error('observer_identity_changed');
|
||||
function guard(){const rs=fs.lstatSync(runtimeRoot,{bigint:true});if(!rs.isDirectory()||rs.isSymbolicLink()||rs.dev!==runtimeStat.dev||rs.ino!==runtimeStat.ino||fs.realpathSync(runtimeRoot)!==runtimeRoot)throw Error('runtime_root_replaced');const s=fs.lstatSync(config.root,{bigint:true});if(s.dev!==rootStat.dev||s.ino!==rootStat.ino||!s.isDirectory()||s.isSymbolicLink()||hash(fs.readFileSync(__filename))!==scriptHash||fs.lstatSync(__filename,{bigint:true}).ino!==scriptStat.ino)throw Error('observer_identity_changed');
|
||||
const st=fs.lstatSync(config.sentinel.path);if(!st.isFile()||st.isSymbolicLink()||st.size>16384||fs.realpathSync(config.sentinel.path)!==config.sentinel.path)throw Error('sentinel_type');const sentinel=JSON.parse(fs.readFileSync(config.sentinel.path,'utf8'));if(sentinel.version!==1||sentinel.provider!=='daytona'||sentinel.token!==config.sentinel.token||sentinel.companyId!==config.binding.companyId||sentinel.environmentId!==config.binding.environmentId)throw Error('sentinel_changed');
|
||||
const c=fs.lstatSync(config.binding.remoteCwd,{bigint:true});if(c.dev!==cwdStat.dev||c.ino!==cwdStat.ino||!c.isDirectory()||c.isSymbolicLink()||fs.realpathSync(config.binding.remoteCwd)!==config.binding.remoteCwd)throw Error('workspace_replaced');}
|
||||
function readSafe(p){const fd=fs.openSync(p,fs.constants.O_RDONLY|fs.constants.O_NOFOLLOW);try{const before=fs.fstatSync(fd,{bigint:true});if(!before.isFile()||before.size>65536n)throw Error('file_bound_or_type');const bytes=fs.readFileSync(fd),after=fs.fstatSync(fd,{bigint:true}),named=fs.lstatSync(p,{bigint:true});if(before.dev!==named.dev||before.ino!==named.ino||named.isSymbolicLink()||before.size!==after.size||before.mtimeNs!==after.mtimeNs||BigInt(bytes.length)!==before.size)throw Error('file_changed');return bytes}finally{fs.closeSync(fd)}}
|
||||
function file(p){try{const s=fs.lstatSync(p);if(s.isSymbolicLink()||!s.isFile()||s.size>65536)throw Error('file_bound_or_type');return {absent:false,sha256:hash(readSafe(p))}}catch(e){if(e.code==='ENOENT')return {absent:true,sha256:null};throw e}}
|
||||
function workspace(){const result={};let count=0,bytes=0;function visit(dir,prefix){for(const name of fs.readdirSync(dir).sort()){if(++count>512)throw Error('workspace_entry_bound');const full=path.join(dir,name),rel=prefix+name,s=fs.lstatSync(full);if(rel===config.actionFile){if(!publishedHash||file(full).sha256!==publishedHash)throw Error('setup_file_changed');continue;}if(s.isSymbolicLink())throw Error('workspace_symlink');if(s.isDirectory()){result[rel]='directory';visit(full,rel+'/')}else if(s.isFile()){bytes+=s.size;if(s.size>65536||bytes>4194304)throw Error('workspace_byte_bound');result[rel]=hash(readSafe(full))}else throw Error('workspace_special_file')}}visit(config.binding.remoteCwd,'');return result}
|
||||
function snapshot(){guard();const processes=sample(),out={};let watchComplete=complete,total=0;for(const [name,t] of targets){const status=t.watch.snapshot();out[name]={...file(t.path),...status};watchComplete&&=status.complete;total+=status.mutationCount}return {binding:config.binding,observedAtMs:Date.now(),observedMonotonicNs:process.hrtime.bigint().toString(),complete:complete&&watchComplete,workspace:workspace(),targets:out,watcher:{complete:watchComplete,targetMutationCount:total,workspaceMutationCount},processes,setup:{path:config.actionFile,sha256:publishedHash,published:publishedHash!==null},attached}}
|
||||
function workspace(){const result={};let count=0,bytes=0;function visit(dir,prefix){for(const name of fs.readdirSync(dir).sort()){if(++count>512)throw Error('workspace_entry_bound');const full=path.join(dir,name),rel=prefix+name,s=fs.lstatSync(full);if(rel===config.runtimeRelative)continue;if(rel===config.actionFile){if(!publishedHash||file(full).sha256!==publishedHash)throw Error('setup_file_changed');continue;}if(s.isSymbolicLink())throw Error('workspace_symlink');if(s.isDirectory()){result[rel]='directory';visit(full,rel+'/')}else if(s.isFile()){bytes+=s.size;if(s.size>65536||bytes>4194304)throw Error('workspace_byte_bound');result[rel]=hash(readSafe(full))}else throw Error('workspace_special_file')}}visit(config.binding.remoteCwd,'');return result}
|
||||
function snapshot(){guard();verifyWorkspaceWatchRoots();const processes=sample(),out={};let watchComplete=complete,total=0;for(const [name,t] of targets){const status=t.watch.snapshot();out[name]={...file(t.path),...status};watchComplete&&=status.complete;total+=status.mutationCount}return {binding:config.binding,observedAtMs:Date.now(),observedMonotonicNs:process.hrtime.bigint().toString(),complete:complete&&watchComplete,workspace:workspace(),targets:out,watcher:{complete:watchComplete,targetMutationCount:total,workspaceMutationCount},processes,scope:{kind:'user_workspace',excludedRuntime:{relativePath:config.runtimeRelative,absolutePath:runtimeRoot,dev:String(runtimeStat.dev),ino:String(runtimeStat.ino),runnerExecutableSha256:config.runnerdSha256},observedPrpEnvironmentLeaseId,prpEnvironmentLeaseIdVerified:false},setup:{path:config.actionFile,sha256:publishedHash,published:publishedHash!==null},attached}}
|
||||
for(const name of config.targets){const p=path.join(config.binding.remoteCwd,name);if(fs.realpathSync(path.dirname(p))!==path.dirname(p))throw Error('target_parent_symlink');targets.set(name,{path:p,watch:watchTarget(path.dirname(p),path.basename(p),{watch:fs.watch,lstatSync:fs.lstatSync})})}
|
||||
if(config.crossRoot){const p=path.join(config.root,'cross-root-target');fs.writeFileSync(p,config.crossRoot.initialText,{flag:'wx',mode:0o600});targets.set('@cross-root',{path:p,watch:watchTarget(config.root,'cross-root-target',{watch:fs.watch,lstatSync:fs.lstatSync})})}
|
||||
let workspaceMutationCount=0;const workspaceWatch=fs.watch(config.binding.remoteCwd,{recursive:true},(_kind,name)=>{if(name!==null&&String(name)===config.actionFile){try{if(!publishedHash||file(path.join(config.binding.remoteCwd,config.actionFile)).sha256!==publishedHash)complete=false}catch{complete=false}return}workspaceMutationCount++;if(name===null||workspaceMutationCount>4096)complete=false;});workspaceWatch.on('error',()=>{complete=false});
|
||||
let workspaceMutationCount=0;const directoryWatches=[];
|
||||
function watchDirectory(directory,prefix=''){if(directoryWatches.length>=512)throw Error('watch_directory_bound');const before=fs.lstatSync(directory,{bigint:true});if(!before.isDirectory()||before.isSymbolicLink())throw Error('watch_directory_identity');const handle=fs.watch(directory,(_kind,name)=>{if(name===null){complete=false;return}const relative=prefix+String(name);if(relative===config.runtimeRelative)return;if(relative===config.actionFile){try{if(!publishedHash||file(path.join(config.binding.remoteCwd,config.actionFile)).sha256!==publishedHash)complete=false}catch{complete=false}return}workspaceMutationCount++;if(workspaceMutationCount>4096)complete=false;try{if(fs.lstatSync(path.join(directory,String(name))).isDirectory())complete=false}catch(e){if(e.code!=='ENOENT')complete=false}});handle.on('error',()=>{complete=false});directoryWatches.push({directory,before,handle});for(const name of fs.readdirSync(directory)){const rel=prefix+name;if(rel===config.runtimeRelative)continue;const full=path.join(directory,name),s=fs.lstatSync(full);if(s.isSymbolicLink())throw Error('workspace_symlink');if(s.isDirectory())watchDirectory(full,rel+'/')}}
|
||||
watchDirectory(config.binding.remoteCwd);
|
||||
const workspaceWatch={close(){for(const item of directoryWatches)item.handle.close()}};
|
||||
function verifyWorkspaceWatchRoots(){for(const item of directoryWatches){const after=fs.lstatSync(item.directory,{bigint:true});if(after.dev!==item.before.dev||after.ino!==item.before.ino||!after.isDirectory()||after.isSymbolicLink())throw Error('workspace_watch_root_replaced')}}
|
||||
function publicSnapshot(){const result=snapshot();if(result.attached)result.attached={connections:attached.connections,failure:attached.failure,commandExit:attached.commandExit,markerWrittenAtMs:attached.markerWrittenAtMs,markerWrittenMonotonicNs:attached.markerWrittenMonotonicNs,clientExitedAtMs:attached.clientExitedAtMs,clientExitedMonotonicNs:attached.clientExitedMonotonicNs};return result}
|
||||
function seal(){if(sealed)return;sealed=true;workspaceWatch.close();for(const t of targets.values())t.watch.close();clearInterval(observer);finalReceipt=publicSnapshot();finalReceipt.files={};for(const [name,t] of targets){if(!file(t.path).absent)finalReceipt.files[name]=readSafe(t.path).toString('base64');}if(Buffer.byteLength(JSON.stringify(finalReceipt))>250000){finalReceipt.complete=false;finalReceipt.files={};}if(child&&child.exitCode===null&&child.signalCode===null)finalReceipt.complete=false;for(const socket of waiters)socket.end(JSON.stringify({ok:true,result:finalReceipt})+'\n');waiters.clear();setTimeout(()=>shutdown(null),250);}
|
||||
const observer=setInterval(()=>{try{const p=sample();if(p.captured&&p.live.length===0&&!retiring){retiring=true;setTimeout(()=>{try{const end=sample();if(end.live.length===0)seal();else retiring=false}catch{complete=false}},100)}}catch{complete=false}},25);
|
||||
@@ -148,10 +164,16 @@ function observerSource() {
|
||||
.replace("WATCH_TARGET", () => createRemoteTargetWatch.toString()).replace("ATTACHED_CLIENT", () => JSON.stringify(ATTACHED_CLIENT));
|
||||
}
|
||||
const RPC = String.raw`const fs=require('node:fs'),net=require('node:net'),cp=require('node:child_process'),crypto=require('node:crypto');const r=JSON.parse(Buffer.from(process.argv[1],'base64').toString());const hash=x=>'sha256:'+crypto.createHash('sha256').update(x).digest('hex');
|
||||
(async()=>{if(hash(fs.readFileSync(process.execPath))!==r.nodeSha256)throw Error('node_identity');if(r.op==='install'){const c=r.config;const st=fs.lstatSync(c.sentinel.path);if(!st.isFile()||st.isSymbolicLink()||st.size>16384||fs.realpathSync(c.sentinel.path)!==c.sentinel.path)throw Error('sentinel_type');const s=JSON.parse(fs.readFileSync(c.sentinel.path,'utf8'));if(s.version!==1||s.provider!=='daytona'||s.token!==c.sentinel.token||s.companyId!==c.binding.companyId||s.environmentId!==c.binding.environmentId)throw Error('sentinel');if(fs.realpathSync(c.binding.remoteCwd)!==c.binding.remoteCwd)throw Error('cwd');fs.mkdirSync(c.root,{mode:0o700});fs.writeFileSync(c.root+'/observer.cjs',r.source,{flag:'wx',mode:0o400});const child=cp.spawn(process.execPath,[c.root+'/observer.cjs',Buffer.from(JSON.stringify(c)).toString('base64')],{detached:true,stdio:'ignore',env:{PATH:'/usr/bin:/bin'}});child.unref();r.root=c.root;r.nonce=c.nonce;r.op='snapshot';}
|
||||
const parseStat=PARSE_STAT,runRoot=RUN_ROOT;
|
||||
async function waitRuntime(c){const root=c.binding.remoteCwd+'/'+c.runtimeRelative,boot=fs.readFileSync('/proc/sys/kernel/random/boot_id','utf8').trim(),until=Date.now()+20000;while(Date.now()<until){try{const s=fs.lstatSync(root);if(!s.isDirectory()||s.isSymbolicLink()||fs.realpathSync(root)!==root)throw Error('runtime_root_identity');const matches=[];const entries=fs.readdirSync('/proc');if(entries.length>8192)throw Error('proc_bound');for(const name of entries){if(!/^\d+$/.test(name)||Number(name)<2)continue;try{const p=parseStat(Number(name),fs.readFileSync('/proc/'+name+'/stat','utf8'),boot),argv=fs.readFileSync('/proc/'+name+'/cmdline').toString().split('\0').filter(Boolean);if(runRoot(argv,c.binding.runId,p)){if(argv[0]!==root+'/bin/paperclip-runnerd'||hash(fs.readFileSync('/proc/'+name+'/exe'))!==c.runnerdSha256)throw Error('runtime_binary_identity');matches.push(p)}}catch(e){if(e.code!=='ENOENT'&&e.code!=='ESRCH')throw e}}if(matches.length>1)throw Error('ambiguous_run_root');if(matches.length===1)return;}catch(e){if(e.code!=='ENOENT')throw e}await new Promise(resolve=>setTimeout(resolve,50))}throw Error('runtime_not_ready')}
|
||||
(async()=>{if(hash(fs.readFileSync(process.execPath))!==r.nodeSha256)throw Error('node_identity');if(r.op==='install'){const c=r.config;await waitRuntime(c);const st=fs.lstatSync(c.sentinel.path);if(!st.isFile()||st.isSymbolicLink()||st.size>16384||fs.realpathSync(c.sentinel.path)!==c.sentinel.path)throw Error('sentinel_type');const s=JSON.parse(fs.readFileSync(c.sentinel.path,'utf8'));if(s.version!==1||s.provider!=='daytona'||s.token!==c.sentinel.token||s.companyId!==c.binding.companyId||s.environmentId!==c.binding.environmentId)throw Error('sentinel');if(fs.realpathSync(c.binding.remoteCwd)!==c.binding.remoteCwd)throw Error('cwd');fs.mkdirSync(c.root,{mode:0o700});fs.writeFileSync(c.root+'/observer.cjs',r.source,{flag:'wx',mode:0o400});const child=cp.spawn(process.execPath,[c.root+'/observer.cjs',Buffer.from(JSON.stringify(c)).toString('base64')],{detached:true,stdio:'ignore',env:{PATH:'/usr/bin:/bin'}});child.unref();r.root=c.root;r.nonce=c.nonce;r.op='snapshot';}
|
||||
for(let i=0;!fs.existsSync(r.root+'/control.sock')&&i<200;i++)await new Promise(resolve=>setTimeout(resolve,10));const socket=net.connect(r.root+'/control.sock');let output='';socket.setTimeout(r.op==='wait'?290000:5000);socket.on('timeout',()=>{socket.destroy();process.exitCode=2});socket.on('error',()=>{process.exitCode=2});socket.on('connect',()=>socket.write(JSON.stringify(r)+'\n'));socket.on('data',b=>{output+=b;if(Buffer.byteLength(output)>262144){socket.destroy();process.exitCode=2}});socket.on('end',()=>{if(!process.exitCode)process.stdout.write(output)});
|
||||
})().catch(()=>{process.exitCode=2});`;
|
||||
|
||||
function rpcSource() {
|
||||
return RPC.replace("PARSE_STAT", () => parseRemoteProcStat.toString()).replace("RUN_ROOT", () => isRemoteRunRoot.toString());
|
||||
}
|
||||
|
||||
export interface RemoteNativeFixture {
|
||||
readonly binding: RemoteNativeBinding;
|
||||
readonly remoteCwd: string;
|
||||
@@ -177,10 +199,11 @@ export interface RemoteNativeFixtureOptions {
|
||||
runnerdSha256: string;
|
||||
targets: string[];
|
||||
actionFile: string;
|
||||
deadlineAt: number;
|
||||
crossRoot?: { initialText: string };
|
||||
}
|
||||
const sha = (value: unknown): value is string => typeof value === "string" && /^sha256:[a-f0-9]{64}$/u.test(value);
|
||||
function readSnapshot(value: unknown, binding: RemoteNativeBinding, names: string[], actionFile: string): RemoteNativeSnapshot {
|
||||
function readSnapshot(value: unknown, binding: RemoteNativeBinding, names: string[], actionFile: string, runnerdSha256: string): RemoteNativeSnapshot {
|
||||
const row = record(value), watcher = record(row.watcher), processes = record(row.processes), setup = record(row.setup);
|
||||
fail(JSON.stringify(row.binding) === JSON.stringify(binding), "receipt_binding");
|
||||
fail(Number.isSafeInteger(row.observedAtMs) && (row.observedAtMs as number) > 0 && typeof row.observedMonotonicNs === "string" && /^\d+$/u.test(row.observedMonotonicNs) && typeof row.complete === "boolean", "receipt_shape");
|
||||
@@ -203,6 +226,11 @@ function readSnapshot(value: unknown, binding: RemoteNativeBinding, names: strin
|
||||
&& Array.isArray(processes.journal) && processes.journal.length <= 512 && processes.journal.every(validProcess)
|
||||
&& Array.isArray(processes.live) && processes.live.length <= 512
|
||||
&& processes.live.every(pid => (processes.journal as unknown[]).some((p: unknown) => record(p).pid === pid)), "process_shape");
|
||||
const scope = record(row.scope), excluded = record(scope.excludedRuntime);
|
||||
fail(scope.kind === "user_workspace" && excluded.relativePath === RUNTIME_RELATIVE && excluded.absolutePath === `${binding.remoteCwd}/${RUNTIME_RELATIVE}`
|
||||
&& /^\d+$/u.test(String(excluded.dev)) && /^\d+$/u.test(String(excluded.ino)) && excluded.runnerExecutableSha256 === runnerdSha256
|
||||
&& typeof scope.observedPrpEnvironmentLeaseId === "string" && /^[-a-zA-Z0-9._:]{1,256}$/u.test(scope.observedPrpEnvironmentLeaseId)
|
||||
&& scope.prpEnvironmentLeaseIdVerified === false, "evidence_scope");
|
||||
fail(setup.path === actionFile && typeof setup.published === "boolean" && (setup.published ? sha(setup.sha256) : setup.sha256 === null), "setup_shape");
|
||||
if (row.attached !== null) {
|
||||
const a = record(row.attached), exit = record(a.commandExit);
|
||||
@@ -217,15 +245,21 @@ function readSnapshot(value: unknown, binding: RemoteNativeBinding, names: strin
|
||||
|
||||
/** Must be called while the initial native input-file bootstrap holds the run.
|
||||
* No effect-producing task is published until baseline proves watcher + PID
|
||||
* admission. Same-UID observer opacity is not an OS adversarial sandbox. */
|
||||
* admission. The exact controller runtime subtree is excluded only after its
|
||||
* directory inode/realpath, pinned executable and run/state-dir binding agree.
|
||||
* The PRP environment ID can be a durable workspace identity, not the sandbox
|
||||
* database lease ID; it is retained as observed, explicitly unverified metadata.
|
||||
* Same-UID observer opacity is not an OS adversarial sandbox. */
|
||||
export async function bindRemoteNativeFixture(options: RemoteNativeFixtureOptions): Promise<RemoteNativeFixture> {
|
||||
const { authority, api, daytona } = options;
|
||||
fail(options.sdkVersion === REMOTE_FIXTURE_DAYTONA_SDK_VERSION, "sdk_pin");
|
||||
fail(Object.entries(authority).every(([key, value]) => key === "image" ? typeof value === "string" && /^[^\s]+@sha256:[a-f0-9]{64}$/u.test(value) : typeof value === "string" && id(value)), "authority_shape");
|
||||
fail(sha(options.nodeSha256) && sha(options.runnerdSha256), "binary_pins");
|
||||
fail(Number.isFinite(options.deadlineAt) && options.deadlineAt - Date.now() >= 27_000, "insufficient_setup_budget");
|
||||
fail(options.targets.length <= 8 && new Set(options.targets).size === options.targets.length, "target_bound");
|
||||
const targets = options.targets.map(relative), actionFile = relative(options.actionFile);
|
||||
fail(!targets.includes(actionFile), "setup_target_overlap");
|
||||
fail([...targets, actionFile].every(path => path !== RUNTIME_RELATIVE && !path.startsWith(`${RUNTIME_RELATIVE}/`)), "runtime_target_forbidden");
|
||||
fail(!options.crossRoot || Buffer.byteLength(options.crossRoot.initialText) <= 4096, "cross_root_bound");
|
||||
const root = `/tmp/pc-native-${randomBytes(18).toString("hex")}`, nonce = randomBytes(32).toString("hex");
|
||||
let binding: RemoteNativeBinding | undefined, sentinel: { path: string; token: string } | undefined, identityKey: string | undefined;
|
||||
@@ -256,14 +290,15 @@ export async function bindRemoteNativeFixture(options: RemoteNativeFixtureOption
|
||||
}
|
||||
async function rpc(request: Record<string, unknown>, admitted?: Awaited<ReturnType<typeof admittedSandbox>>) {
|
||||
const sandbox = admitted ?? await admittedSandbox();
|
||||
if (request.op === "install") fail(options.deadlineAt - Date.now() >= 27_000, "insufficient_setup_budget");
|
||||
const payload = Buffer.from(JSON.stringify({ ...request, root, nonce, nodeSha256: options.nodeSha256 })).toString("base64");
|
||||
const command = `/usr/bin/env -i PATH=/usr/bin:/bin ${quote(NODE)} -e ${quote(RPC)} ${quote(payload)}`;
|
||||
const command = `/usr/bin/env -i PATH=/usr/bin:/bin ${quote(NODE)} -e ${quote(rpcSource())} ${quote(payload)}`;
|
||||
let deadline: ReturnType<typeof setTimeout> | undefined;
|
||||
let response: { exitCode: number; result: string };
|
||||
try {
|
||||
response = await Promise.race([
|
||||
sandbox.process.executeCommand(command, binding!.remoteCwd, {}, request.op === "wait" ? 295 : 10),
|
||||
new Promise<never>((_resolve, reject) => { deadline = setTimeout(() => reject(new Error("deadline")), request.op === "wait" ? 300_000 : 12_000); deadline.unref(); }),
|
||||
sandbox.process.executeCommand(command, binding!.remoteCwd, {}, request.op === "wait" ? 295 : request.op === "install" ? 25 : 10),
|
||||
new Promise<never>((_resolve, reject) => { deadline = setTimeout(() => reject(new Error("deadline")), request.op === "wait" ? 300_000 : request.op === "install" ? 27_000 : 12_000); deadline.unref(); }),
|
||||
]);
|
||||
} catch { throw new Error("remote_native_fixture:remote_command_failed_or_deadline"); }
|
||||
finally { if (deadline) clearTimeout(deadline); }
|
||||
@@ -275,10 +310,10 @@ export async function bindRemoteNativeFixture(options: RemoteNativeFixtureOption
|
||||
}
|
||||
const sandbox = await admittedSandbox();
|
||||
const names = [...targets, ...(options.crossRoot ? ["@cross-root"] : [])];
|
||||
const config = { root, nonce, binding, sentinel, targets, actionFile, crossRoot: options.crossRoot, runnerdSha256: options.runnerdSha256 };
|
||||
const config = { root, nonce, binding, sentinel, targets, actionFile, crossRoot: options.crossRoot, runtimeRelative: RUNTIME_RELATIVE, runnerdSha256: options.runnerdSha256 };
|
||||
let baseline: RemoteNativeSnapshot;
|
||||
try {
|
||||
baseline = readSnapshot(await rpc({ op: "install", config, source: observerSource() }, sandbox), binding!, names, actionFile);
|
||||
baseline = readSnapshot(await rpc({ op: "install", config, source: observerSource() }, sandbox), binding!, names, actionFile, options.runnerdSha256);
|
||||
fail(baseline.complete && baseline.processes.captured && baseline.processes.live.length > 0 && baseline.watcher.complete && !baseline.setup.published, "bootstrap_not_held");
|
||||
} catch (error) {
|
||||
// Only the nonce/inode-bound observer can acknowledge this cleanup. If
|
||||
@@ -306,7 +341,7 @@ export async function bindRemoteNativeFixture(options: RemoteNativeFixtureOption
|
||||
async snapshot(label) {
|
||||
fail(typeof label === "string" && /^[a-zA-Z0-9_-]{1,80}$/u.test(label), "snapshot_label");
|
||||
fail(!closed && !finished, "fixture_closed");
|
||||
return readSnapshot(await rpc({ op: "snapshot" }), binding!, names, actionFile);
|
||||
return readSnapshot(await rpc({ op: "snapshot" }), binding!, names, actionFile, options.runnerdSha256);
|
||||
},
|
||||
async readFile(path) {
|
||||
fail(!closed && names.includes(path), "unregistered_read");
|
||||
@@ -337,7 +372,7 @@ export async function bindRemoteNativeFixture(options: RemoteNativeFixtureOption
|
||||
if (finished) return finished;
|
||||
const receipt = await terminal;
|
||||
if ("error" in receipt) throw receipt.error;
|
||||
const result = readSnapshot(receipt.value, binding!, names, actionFile);
|
||||
const result = readSnapshot(receipt.value, binding!, names, actionFile, options.runnerdSha256);
|
||||
fail(published && result.setup.published && result.complete && result.watcher.complete && result.processes.captured && result.processes.live.length === 0, "terminal_evidence_incomplete");
|
||||
const files = record(record(receipt.value).files);
|
||||
for (const name of names) {
|
||||
|
||||
Reference in new issue
Block a user