mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
Route native call_api through existing agent REST permissions. Preserve admitted credentials across restarts, protect sensitive replies and private snapshots, and compare saved completion with provider history through the same redaction projection. Add permission, privacy and recovery regressions and update the access map and run-log guide. Co-Authored-By: Paperclip <noreply@paperclip.ing>
43 lines
3.0 KiB
TypeScript
43 lines
3.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { HarnessDriverBackend } from "../../../packages/paperclip-runner/src/backends/harness-driver-backend.js";
|
|
import { FakeCodexTransport, makeDriver, result, WORKSPACE } from "../../../packages/paperclip-runner/src/drivers/codex/codex-app-server-driver.test-support.js";
|
|
import { redactRunnerApiPayloads } from "../services/native-runtime/runner-api-log-redaction.js";
|
|
|
|
describe("private API output in provider recovery", () => {
|
|
it.each([false, true])("compares active completion through the saved projection (changed=%s)", async changed => {
|
|
const first = new FakeCodexTransport();
|
|
const second = new FakeCodexTransport();
|
|
const backend = new HarnessDriverBackend(makeDriver([first, second]));
|
|
const identity = { runId: "api-recovery-run", sessionId: "api-recovery-session", companyId: "company", issueId: "issue", agentId: "agent" };
|
|
const original = await backend.openSession({ identity, workingDirectory: WORKSPACE });
|
|
await original.startTurn({ message: { role: "user", text: "Finish the task." } });
|
|
const receipt = { operationId: "call_api", apiOperationId: "POST /api/companies/{companyId}/exports", callId: "export", ok: true, status: 200, data: { credential: "private-recovery-value" } };
|
|
const completion = { ...result, summary: JSON.stringify(receipt) };
|
|
expect(await first.invoke({ id: "finish", method: "item/tool/call", params: {
|
|
threadId: "thread-1", turnId: "turn-1", callId: "finish", tool: "paperclip_finish", arguments: completion,
|
|
} })).toMatchObject({ success: true });
|
|
const raw = await original.snapshot();
|
|
expect(raw.activeTurnId).toBe("turn-1");
|
|
expect(raw.semanticResult).toEqual(completion);
|
|
const checkpoint = redactRunnerApiPayloads(raw);
|
|
expect(JSON.stringify(checkpoint)).not.toContain("private-recovery-value");
|
|
expect(JSON.stringify(checkpoint)).toContain("apiPayloadSha256");
|
|
await original.close({ reason: "simulate restart before the terminal checkpoint" });
|
|
const providerResult = changed ? { ...completion, summary: JSON.stringify({ ...receipt, data: { credential: "changed-value" } }) } : completion;
|
|
second.readResponse = { thread: { id: "thread-1", sessionId: "provider-session-1", cwd: WORKSPACE,
|
|
turns: [{ id: "turn-1", status: "completed", items: [{ id: "finish", type: "agentMessage", text: JSON.stringify(providerResult) }] }],
|
|
} };
|
|
const recovered = await backend.recoverSession(checkpoint, {
|
|
signal: new AbortController().signal, normalizeReplayPayload: redactRunnerApiPayloads,
|
|
});
|
|
if (changed) {
|
|
expect(recovered).toMatchObject({ recovered: false, reason: expect.stringContaining("conflicting") });
|
|
} else {
|
|
expect(recovered.recovered).toBe(true);
|
|
expect((await recovered.session!.snapshot()).semanticResult).toEqual(checkpoint.semanticResult);
|
|
await recovered.session!.close({ reason: "verified recovery" });
|
|
}
|
|
expect(second.calls.some(call => call.method === "turn/start")).toBe(false);
|
|
});
|
|
});
|