Read native plugin task output files on the execution host (#17285)

This commit is contained in:
Nicky Leach authored and GitHub committed 2026-10-22 23:38:09 -07:00
1 parent d69631b01b
commit 412914cfa7
15 files changed
+202 -24

No files matched your search

@@ -43,10 +43,10 @@ export async function executeEnvironmentTask(db: Db, workers: PluginWorkerManage
const activeProbe = lease.heartbeatRunId === null && lease.issueId === null && probe
&& typeof probe.runId === "string" && typeof probe.sessionId === "string"
&& typeof probe.expiresAtMs === "number" && probe.expiresAtMs > Date.now();
if ((operation.kind === "describe" || operation.kind === "prepare" || operation.kind === "submit" || operation.kind === "connection" || operation.kind === "read_state" || operation.kind === "archive_state" || operation.kind === "check_project_auth" || operation.kind === "copilot_metadata") &&
if ((operation.kind === "read_file" || operation.kind === "describe" || operation.kind === "prepare" || operation.kind === "submit" || operation.kind === "connection" || operation.kind === "read_state" || operation.kind === "archive_state" || operation.kind === "check_project_auth" || operation.kind === "copilot_metadata") &&
(metadata.runnerTaskRetiring === true || !environment || (!activeProbe && run?.status !== "running") || lease.status !== "active" || (lease.expiresAt && lease.expiresAt.getTime() <= Date.now()))) throw new Error("Plugin-provided Runner execution lease is not active");
if (operation.kind === "copilot_metadata" && !activeProbe) throw new Error("Copilot metadata requires a private setup lease");
const checkpointIdentity = operation.kind === "read_state" ? operation.identity
const checkpointIdentity = operation.kind === "read_state" || operation.kind === "read_file" ? operation.identity
: operation.kind === "archive_state" ? operation.priorIdentity : null;
const runner = checkpointIdentity ? { runnerId: checkpointIdentity.runnerInstanceId, leaseId: checkpointIdentity.environmentLeaseId,
runId: checkpointIdentity.runId, sessionId: checkpointIdentity.normalizedSessionId } : operation.kind === "submit" ? operation.runner : operation.kind === "prepare" ? {
@@ -55,7 +55,7 @@ export async function executeEnvironmentTask(db: Db, workers: PluginWorkerManage
runId: operation.requirements.identity.runId, sessionId: operation.requirements.identity.normalizedSessionId,
} : null;
const launches = operation.kind === "submit" || operation.kind === "prepare";
const scopesProjects = launches || operation.kind === "check_project_auth";
const scopesProjects = launches || operation.kind === "check_project_auth" || operation.kind === "read_file";
const resumeFrom = launches || checkpointIdentity ? (operation as { resumeFrom?: string }).resumeFrom : undefined;
const savedIdentity = metadata.runnerTaskIdentity as Record<string, unknown> | undefined;
const ownCheckpoint = checkpointIdentity && !resumeFrom && savedIdentity
@@ -101,7 +101,7 @@ export async function executeEnvironmentTask(db: Db, workers: PluginWorkerManage
eq(executionWorkspaces.id, workspaceBinding.executionWorkspaceId), eq(executionWorkspaces.companyId, input.companyId)));
const saved = readWorkspaceProviderBinding(workspace?.metadata);
if (!workspace || workspace.id !== lease.executionWorkspaceId || workspace.providerType !== "plugin" || workspace.providerRef !== workspaceBinding.workspaceRef ||
(launches && !["active", "idle", "in_review"].includes(workspace.status)) ||
(scopesProjects && !["active", "idle", "in_review"].includes(workspace.status)) ||
workspaceBinding.pluginId !== plugin.id || !saved || JSON.stringify(saved) !== JSON.stringify(workspaceBinding)) {
throw new Error("Managed workspace execution binding changed");
}
@@ -127,6 +127,12 @@ export async function executeEnvironmentTask(db: Db, workers: PluginWorkerManage
const driverConfig = config.pluginKey === plugin.pluginKey && config.driverKey === metadata.driverKey
? (config.driverConfig as Record<string, unknown> | undefined) ?? {} : {};
let requiredProjectAuthProof: Record<string, unknown> | undefined;
if (operation.kind === "read_file") {
const proof = metadata.runnerTaskProjectAuthValidation as { projectIds?: string[] } | undefined;
if (metadata.runnerTaskExecutionStopped === true || !Array.isArray(proof?.projectIds) ||
JSON.stringify([...proof.projectIds].sort()) !== JSON.stringify([...operation.projectIds].sort()))
throw new Error("Plugin Runner file scope unavailable");
}
if (operation.kind === "submit") {
const proof = metadata.runnerTaskProjectAuthValidation as { configurationId?: unknown; provider?: unknown; projectIds?: string[] } | undefined;
const provider = operation.runner.harness === "codex" ? "openai" : operation.runner.harness === "claude" ? "anthropic" : operation.runner.harness;
@@ -174,9 +180,17 @@ export async function executeEnvironmentTask(db: Db, workers: PluginWorkerManage
taskId, runId: run?.id ?? (activeProbe ? String(probe.runId) : null), agentId: run?.agentId ?? null, projectIds,
lease: { providerLeaseId: lease.providerLeaseId, metadata: lease.metadata ?? undefined },
...(workspaceBinding ? { workspace: workspaceBinding } : {}),
...(freshWorkspace && (launches || operation.kind === "check_project_auth") ? { fresh: freshWorkspace } : {}), operation,
...(freshWorkspace && scopesProjects ? { fresh: freshWorkspace } : {}), operation,
}, operation.kind === "copilot_metadata" ? Math.max(1, Math.min(40_000, Number(probe?.expiresAtMs) - Date.now())) : 15_000);
const parsed = parseEnvironmentTaskResult(operation, taskId, result);
if (operation.kind === "read_file") {
const [current] = await db.select({ lease: environmentLeases, run: heartbeatRuns }).from(environmentLeases)
.leftJoin(heartbeatRuns, and(eq(heartbeatRuns.id, environmentLeases.heartbeatRunId), eq(heartbeatRuns.companyId, environmentLeases.companyId)))
.where(and(eq(environmentLeases.id, lease.id), eq(environmentLeases.companyId, input.companyId))).limit(1);
if (!current || current.lease.status !== "active" || current.run?.status !== "running" ||
current.lease.metadata?.runnerTaskRetiring === true || current.lease.metadata?.runnerTaskExecutionStopped === true ||
current.lease.expiresAt && current.lease.expiresAt.getTime() <= Date.now()) throw new Error("Plugin Runner file lease expired during transfer");
}
if (freshWorkspace && parsed.kind === "description" && parsed.freshWorkspaceVersion !== 1)
throw new Error("Plugin-provided fresh workspace preparation unavailable");
if (operation.kind === "check_project_auth" && parsed.kind === "project_auth") {
@@ -122,6 +122,7 @@ describe("native runner file handoff", () => {
overrides: Partial<{
companyId: string;
executionTargetKind: "local" | "remote";
readRemoteTaskFile: (input: { contentRef: string; byteSize: number; sha256: string }) => Promise<Buffer>;
readRemoteWorkspaceFile: (input: { contentRef: string; byteSize: number; sha256: string }) => Promise<Buffer>;
}> = {},
) {
@@ -133,6 +134,7 @@ describe("native runner file handoff", () => {
workspaceRoot,
executionTargetKind: overrides.executionTargetKind ?? "local",
readRemoteWorkspaceFile: overrides.readRemoteWorkspaceFile,
readRemoteTaskFile: overrides.readRemoteTaskFile,
storage: createStorageService(
createLocalDiskStorageProvider(storageRoot),
),
@@ -600,6 +602,15 @@ describe("native runner file handoff", () => {
.rejects.toThrow(/size|hash/);
});
it("registers an absolute file only through a task-location-authorizing reader", async () => {
const body=Buffer.from("Home output\n");const reader=vi.fn(async()=>body);
const call=callFor("/home/agent/proof.txt",body,"home-output");
await expect(authority({executionTargetKind:"remote",readRemoteWorkspaceFile:reader}).execute(call)).rejects.toThrow("path_denied");
expect(reader).not.toHaveBeenCalled();
await expect(authority({executionTargetKind:"remote",readRemoteTaskFile:reader}).execute(call)).resolves.toBeDefined();
expect(reader).toHaveBeenCalledWith(expect.objectContaining({contentRef:"/home/agent/proof.txt"}));
});
it("stages only exact wake-bound inbound bytes without exposing an API credential", async () => {
const storage = createStorageService(
createLocalDiskStorageProvider(storageRoot),
@@ -47,6 +47,8 @@ export interface NativeRunnerFileHandoffBinding {
readonly executionTargetKind: "local" | "remote";
/** Server-bound reader. Never supplied by the model or a request body. */
readonly readRemoteWorkspaceFile?: RemoteWorkspaceFileReader;
/** Provider-bound reader which authorizes absolute paths within task-granted locations. */
readonly readRemoteTaskFile?: RemoteWorkspaceFileReader;
}
export interface NativeRunnerFileHandoffInput {
@@ -326,14 +328,15 @@ async function readVerifiedWorkspaceFile(
}
if (binding.executionTargetKind === "remote") {
if (!binding.readRemoteWorkspaceFile) throw new Error("paperclip_runner_file_handoff_remote_unsupported");
const readFile = binding.readRemoteTaskFile ?? binding.readRemoteWorkspaceFile;
if (!readFile) throw new Error("paperclip_runner_file_handoff_remote_unsupported");
const contentRef = requiredText(input.contentRef, "content_ref", 2_000);
if (path.posix.isAbsolute(contentRef) || /^[a-z][a-z0-9+.-]*:/iu.test(contentRef)
if ((!binding.readRemoteTaskFile && path.posix.isAbsolute(contentRef)) || /^[a-z][a-z0-9+.-]*:/iu.test(contentRef)
|| contentRef.includes("\\") || path.posix.normalize(contentRef) === ".."
|| path.posix.normalize(contentRef).startsWith("../")) {
throw new Error("paperclip_runner_file_handoff_path_denied");
}
const body = await binding.readRemoteWorkspaceFile({ contentRef, byteSize: input.byteSize, sha256: expectedSha256 });
const body = await readFile({ contentRef, byteSize: input.byteSize, sha256: expectedSha256 });
if (body.length !== input.byteSize) throw new Error("paperclip_runner_file_handoff_size_denied");
if (createHash("sha256").update(body).digest("hex") !== expectedSha256) throw new Error("paperclip_runner_file_handoff_hash_mismatch");
return { body, contentType, filename, sha256: expectedSha256, title };
@@ -11074,10 +11074,12 @@ describe("runnerd provider runtime wiring", () => {
const launcher = vi.fn();
const registration = vi.fn();
const driver = { prepareLaunch: vi.fn(), runnerProcessLauncher: launcher, controlPlaneRegistration: registration };
const owner = vi.spyOn(pluginTaskRuntime, "createPluginRunnerTaskOwner").mockReturnValue({ launcher, registration, driver });
state.toolAuthorityDefinitions.mockImplementation(async binding => binding.readRemoteTaskFile ? [{ name: "register_deliverable" }] as never : []);
const readWorkspaceFile = vi.fn(async () => Buffer.from("proof"));
const owner = vi.spyOn(pluginTaskRuntime, "createPluginRunnerTaskOwner").mockReturnValue({ launcher, registration, driver, readWorkspaceFile });
const task = vi.fn(async (operation: unknown) => {
environmentTaskOperationSchema.parse(operation);
return { kind: "description", launchPreparationVersion: 1, projectAuthCheckVersion: 1, taskId: "task", runner: { version: "fixture", digest: `sha256:${"a".repeat(64)}` } };
return { kind: "description", launchPreparationVersion: 1, projectAuthCheckVersion: 1, ...(kind === "codex" ? { workspaceFileReadVersion: 1 } : {}), taskId: "task", runner: { version: "fixture", digest: `sha256:${"a".repeat(64)}` } };
});
const target = { kind: "remote", transport: "sandbox", providerKey: "plugin", leaseId: "plugin-task-lease",
remoteCwd: "/unused", runnerTask: task, runnerTaskProjectIds: ["10000000-0000-4000-8000-000000000001"] } as const;
@@ -11098,6 +11100,9 @@ describe("runnerd provider runtime wiring", () => {
expect(state.createBackend.mock.calls.at(-1)![1]).toMatchObject({ workingDirectoryAuthority: "runner_prepared_launch", environment: {} });
state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!();
const options = state.createTransport.mock.calls.at(-1)![0];
const expectedTools = kind === "codex" ? [{ name: "register_deliverable" }] : [];
expect(state.createBackend.mock.calls.at(-1)![1]).toMatchObject({ dynamicTools: expectedTools });
expect(options).toMatchObject({ resumeDynamicTools: expectedTools });
expect(options).toMatchObject({ ...driver, runnerArtifact: { version: "fixture" },
prpIdentity: { environmentLeaseId: "plugin-task-lease" } });
if (kind === "acpx") expect(options).toMatchObject({ acpxAgent: "claude", claudeEffort: "high" });
@@ -11105,6 +11110,18 @@ describe("runnerd provider runtime wiring", () => {
expect(options).not.toHaveProperty(key);
}
expect(task.mock.calls).toEqual([[{ kind: "describe" }]]);
const binding = state.toolAuthorityDefinitions.mock.calls.at(-1)![0] as unknown as {
readRemoteApiFile?: (path: string, maxBytes: number) => Promise<Buffer>;
readRemoteTaskFile?: (file: {contentRef: string;byteSize: number;sha256: string}) => Promise<Buffer>;
};
if (kind === "codex") {
await expect(binding.readRemoteApiFile!("proof.txt",1024)).resolves.toEqual(Buffer.from("proof"));
await expect(binding.readRemoteTaskFile!({contentRef:"proof.txt",byteSize:5,sha256:"a".repeat(64)})).resolves.toEqual(Buffer.from("proof"));
expect(readWorkspaceFile).toHaveBeenCalledWith("proof.txt",5);
} else {
expect(binding.readRemoteApiFile).toBeUndefined();
expect(binding.readRemoteTaskFile).toBeUndefined();
}
expect(owner.mock.calls.at(-1)![0]).not.toHaveProperty("assets");
const identity: Record<string, unknown> = options.prpIdentity!;
await owner.mock.calls.at(-1)![0].onSessionRetained!({ schema: "paperclip.runner.durable.state.v1", lifecycle: "suspended", ...identity });
@@ -11813,6 +11813,11 @@ async function createRunnerdBackendWithinSessionClaim(
.where(and(eq(environmentLeases.id, remoteTarget.leaseId!), eq(environmentLeases.companyId, input.execution.binding.companyId)));
}, onDispatched: input.onTaskDispatched,
});
if (description.workspaceFileReadVersion === 1) {
authority.binding.readRemoteApiFile = owner.readWorkspaceFile;
authority.binding.readRemoteTaskFile = file => owner.readWorkspaceFile(file.contentRef, file.byteSize);
dynamicTools = await authorityEpoch.definitions();
}
const executeTool = (call: Parameters<SessionToolAuthorityEpoch["execute"]>[0]) => {
const current = sessionToolAuthorityEpochs.get(sessionScopeId);
if (!current) throw new Error("native_session_tool_authority_unavailable");
@@ -179,6 +179,8 @@ type Binding = {
assertBridgeAuthority?: (database?: Db) => Promise<void>;
executionTargetKind?: "local" | "remote";
readRemoteWorkspaceFile?: RemoteWorkspaceFileReader;
readRemoteTaskFile?: RemoteWorkspaceFileReader;
readRemoteApiFile?: (path: string, maxBytes: number) => Promise<Buffer>;
currentWakeComments?: CurrentWakeCommentsBinding;
chatAttachmentReadScope?: NativeChatAttachmentReadScope;
syncIssueExternalObjects?: (issueId: string) => Promise<void>;
@@ -278,7 +280,7 @@ export class PaperclipRunnerToolAuthority {
descriptor.allowedModes.includes(workMode) &&
(descriptor.operationId !== "register_deliverable" ||
(Boolean(this.binding.workspaceRoot) &&
((this.binding.executionTargetKind ?? "local") === "local" || Boolean(this.binding.readRemoteWorkspaceFile)))),
((this.binding.executionTargetKind ?? "local") === "local" || Boolean(this.binding.readRemoteWorkspaceFile || this.binding.readRemoteTaskFile)))),
).map((descriptor) => ({
name: descriptor.operationId,
description:
@@ -303,8 +305,9 @@ export class PaperclipRunnerToolAuthority {
},
contentRef: {
...(descriptor.inputSchema.properties?.contentRef ?? {}),
description:
"Workspace-relative source path. Absolute paths, URLs, traversal, and symlinks are rejected.",
description: this.binding.readRemoteTaskFile
? "Path to the file on the execution host: workspace-relative or absolute within a location granted to this task, including its agent home."
: "Workspace-relative source path. Absolute paths, URLs, traversal, and symlinks are rejected.",
},
},
}
@@ -1165,6 +1168,14 @@ export class PaperclipRunnerToolAuthority {
await assertArtifactAccess();
return { bytes: Buffer.concat(chunks), filename: asset.originalFilename ?? "file", contentType: asset.contentType };
}
if (this.binding.executionTargetKind === "remote") {
if (!this.binding.readRemoteApiFile) throw badRequest("Remote workspace file uploads are unavailable for this execution provider");
await this.#boundContext();
const bytes = await this.binding.readRemoteApiFile(file.path!, RUNNER_API_MAX_BYTES);
await this.#boundContext();
if (bytes.length > RUNNER_API_MAX_BYTES) throw badRequest("Workspace file exceeds API transfer limit");
return { bytes, filename: basename(file.path!), contentType: "application/octet-stream" };
}
const resolved = await workspaceFileResourceService(this.db).prepareDownload(this.binding.issueId, { path: file.path!, workspace: "auto" });
const handle = await openRunnerApiWorkspaceFile(resolved.realPath);
try {
@@ -1940,6 +1951,7 @@ export class PaperclipRunnerToolAuthority {
workspaceRoot,
executionTargetKind: this.binding.executionTargetKind ?? "local",
readRemoteWorkspaceFile: this.binding.readRemoteWorkspaceFile,
readRemoteTaskFile: this.binding.readRemoteTaskFile,
},
deliverable: {
filename: typeof input.filename === "string" ? input.filename : "",
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { describe, expect, it, vi } from "vitest";
import type { RunnerProviderLaunchRequirements } from "@paperclipai/paperclip-runner";
import type { RunnerProcessLaunchSpec } from "../../vendor/paperclip-runner/index.js";
@@ -274,3 +275,31 @@ describe("plugin native Runner ownership", () => {
} finally { process.child.kill(); await process.completion.catch(() => {}); }
});
});
describe("plugin task workspace file transfer", () => {
it("reads only a submitted task, validates bytes, and rejects late responses after release", async () => {
const {task}=provider(); const perform=task.getMockImplementation()!;
const bytes=Buffer.from("VM proof\n");
let file={kind:"file",taskId:"task",base64:bytes.toString("base64"),byteSize:bytes.length,sha256:createHash("sha256").update(bytes).digest("hex")};
let afterRead=async()=>{};
task.mockImplementation(async op=>{if(op.kind==="read_file"){await afterRead();return file;}return perform(op);});
const owner=createPluginRunnerTaskOwner({projectAuthCheckVersion:1,task,projectIds:[],harness:"codex",pollMs:1});
await expect(owner.readWorkspaceFile("proof.txt",1024)).rejects.toThrow("unavailable");
await prepare(owner);
ingress.connect.mockReturnValue({ready:Promise.resolve(),close:async()=>{}});
const registration=await owner.registration({} as never,"run");
const handle=owner.launcher(launchSpec());registration.activate();await registration.ready();
try {
await expect(owner.readWorkspaceFile("proof.txt",1024)).resolves.toEqual(bytes);
await expect(owner.readWorkspaceFile("/home/agent/proof.txt",1024)).resolves.toEqual(bytes);
expect(task.mock.calls.find(([op])=>op.kind==="read_file")?.[0]).toMatchObject({identity:requirements.identity,path:"proof.txt",projectIds:[]});
for(const path of ["../escape","//etc/passwd","file:/secret","a/../b","a\\b"]) await expect(owner.readWorkspaceFile(path,1024)).rejects.toThrow();
await expect(owner.readWorkspaceFile("proof.txt",1)).rejects.toThrow("unavailable");
file={...file,sha256:"f".repeat(64)};
await expect(owner.readWorkspaceFile("proof.txt",1024)).rejects.toThrow("integrity");
afterRead=()=>registration.release();
await expect(owner.readWorkspaceFile("proof.txt",1024)).rejects.toThrow("unavailable");
} finally {await registration.release();await registration.checkpoint("unsettled");await handle.completion;}
});
});
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { setTimeout as delay } from "node:timers/promises";
import { environmentTaskOperationSchema, environmentTaskResultSchema, type PluginEnvironmentTaskOperation, type PluginEnvironmentTaskResult, type PluginEnvironmentTaskCredential } from "@paperclipai/plugin-sdk";
import type { RunnerIngressEndpoint } from "@paperclipai/adapter-utils/runner-connectivity";
@@ -44,7 +45,7 @@ export function createPluginRunnerTaskOwner(input: {
void running.catch(() => undefined);
const call = async (operation: PluginEnvironmentTaskOperation) => {
const result = environmentTaskResultSchema.parse(await input.task(operation));
if (result.kind !== (operation.kind === "describe" ? "description" : operation.kind === "prepare" ? "prepared" : operation.kind === "check_project_auth" ? "project_auth" : ["read_state", "archive_state"].includes(operation.kind) ? "state" : ["status", "connection", "credentials"].includes(operation.kind) ? operation.kind : "accepted")) {
if (result.kind !== (operation.kind === "read_file" ? "file" : operation.kind === "describe" ? "description" : operation.kind === "prepare" ? "prepared" : operation.kind === "check_project_auth" ? "project_auth" : ["read_state", "archive_state"].includes(operation.kind) ? "state" : ["status", "connection", "credentials"].includes(operation.kind) ? operation.kind : "accepted")) {
throw new Error("Plugin Runner returned an invalid operation result");
}
return result;
@@ -247,5 +248,15 @@ export function createPluginRunnerTaskOwner(input: {
},
controlPlaneRegistration: (authority, identity, signal) => registration(authority, identity?.runId ?? "", signal),
};
return { launcher, registration, driver };
const readWorkspaceFile = async (path: string, maxBytes: number): Promise<Buffer> => {
if (!submitted || !preparedIdentity || released || stopRequested) throw new Error("Plugin Runner file access unavailable");
const result = await call(environmentTaskOperationSchema.parse({ kind: "read_file", identity: preparedIdentity,
projectIds: [...input.projectIds], path, maxBytes }));
if (released || stopRequested || result.kind !== "file" || result.byteSize > maxBytes) throw new Error("Plugin Runner file access unavailable");
const bytes = Buffer.from(result.base64, "base64");
if (bytes.length !== result.byteSize || bytes.toString("base64") !== result.base64 ||
createHash("sha256").update(bytes).digest("hex") !== result.sha256) throw new Error("Plugin Runner file integrity check failed");
return bytes;
};
return { launcher, registration, driver, readWorkspaceFile };
}
@@ -1,5 +1,5 @@
import { existsSync } from "node:fs";
import { randomUUID } from "node:crypto";
import { createHash, randomUUID } from "node:crypto";
import { chmod, writeFile, symlink, mkdir, open } from "node:fs/promises";
import { join, dirname } from "node:path";
import { eq } from "drizzle-orm";
@@ -933,6 +933,22 @@ else if(m.id!==undefined) send({id:m.id,result:{}});
expect((await fixture.snapshot()).assets).toEqual(expect.arrayContaining([expect.objectContaining({ id: download.artifact.artifactId, companyId: fixture.companyId, createdByAgentId: fixture.agentId })]));
});
it.each(["sample.txt", "/home/agent/proof.txt"])("uploads remote task bytes at %s and fails closed without the reader", async (sourcePath) => {
const fixture=await server.fixture();
const bytes=Buffer.from("proof exists only on the execution host\n");
const read=vi.fn(async(path:string,maxBytes:number)=>{expect(path).toBe(sourcePath);expect(maxBytes).toBeGreaterThan(bytes.length);return bytes;});
const binding={...fixture.authority.binding,executionTargetKind:"remote" as const,readRemoteApiFile:read};
const authority=new PaperclipRunnerToolAuthority(server.db,binding);
const call={tool:"call_api" as const,callId:randomUUID(),arguments:{operationId:"POST /api/companies/{companyId}/issues/{issueId}/attachments",pathParams:{issueId:fixture.issueId},files:[{path:sourcePath}]}};
await expect(authority.execute(call)).resolves.toMatchObject({status:201});
expect(read).toHaveBeenCalledOnce();
expect((await fixture.snapshot()).assets).toEqual(expect.arrayContaining([expect.objectContaining({sha256:createHash("sha256").update(bytes).digest("hex"),byteSize:bytes.length})]));
const unsupported=new PaperclipRunnerToolAuthority(server.db,{...binding,readRemoteApiFile:undefined});
await expect(unsupported.execute({...call,callId:randomUUID()})).rejects.toThrow("Remote workspace file uploads are unavailable");
const changing=new PaperclipRunnerToolAuthority(server.db,{...binding,readRemoteApiFile:async()=>{await server.db.update(heartbeatRuns).set({status:"cancelled"}).where(eq(heartbeatRuns.id,fixture.runId));return bytes;}});
await expect(changing.execute({...call,callId:randomUUID()})).rejects.toThrow();
});
it("does not accept caller-supplied identity in an API comment", async () => {
const fixture = await server.fixture();
const result = await fixture.authority.execute({ tool: "call_api", callId: "comment", arguments: { operationId: "POST /api/issues/{id}/comments", pathParams: { id: fixture.issueId }, body: { body: "Identity proof", authorAgentId: randomUUID(), authorUserId: "spoofed", runId: randomUUID() } } });