mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
fix(runner): preserve ACPX completion receipts and scrub failed Grok admissions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
78eb3f19f3
commit
52ba6b564a
8 files changed
+46
-6
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
describe("ACPX driver profile", () => {
|
||||
it.each([
|
||||
["codex", "available"],
|
||||
["grok", "available"],
|
||||
["claude", "available"],
|
||||
["pi", "unsupported"],
|
||||
] as const)(
|
||||
@@ -48,6 +49,7 @@ describe("ACPX driver profile", () => {
|
||||
it.each([
|
||||
["claude", "claude-sonnet-5"],
|
||||
["codex", "gpt-5.6-sol"],
|
||||
["grok", "grok-4.7"],
|
||||
] as const)("accepts the exact qualified %s model", (agent, model) => {
|
||||
expect(validateAcpxDriverConfig({ agent, model })).toEqual({
|
||||
ok: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ export const GROK_AUTH_REFRESH_FILE = "auth-refresh.json";
|
||||
export async function stageManagedGrokCredential(input: {
|
||||
agentHomeDirectory: string;
|
||||
environment?: NodeJS.ProcessEnv;
|
||||
retainRefresh?: () => boolean;
|
||||
}): Promise<AcpxProviderLifetimeLease> {
|
||||
const source = input.environment ?? {};
|
||||
const inline = source.PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET;
|
||||
@@ -51,7 +52,7 @@ export async function stageManagedGrokCredential(input: {
|
||||
// The runtime host calls close only after verified provider exit. Retain
|
||||
// a private refresh handoff for the controller's existing Grok merge
|
||||
// predicate, then scrub auth.json before releasing the process fence.
|
||||
if (inline) {
|
||||
if (inline && (input.retainRefresh?.() ?? true)) {
|
||||
const auth = await open(join(input.agentHomeDirectory, "auth.json"), constants.O_RDONLY | constants.O_NOFOLLOW).catch((error) => {
|
||||
if (error.code === "ENOENT") return null;
|
||||
throw error;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
|
||||
describe("qualified ACPX profiles", () => {
|
||||
it("binds each agent to one immutable package and model declaration", () => {
|
||||
for (const agent of ["pi", "claude", "codex"] as const) {
|
||||
for (const agent of ["pi", "claude", "codex", "grok"] as const) {
|
||||
const profile = QUALIFIED_ACPX_PROFILES[agent];
|
||||
expect(profile.agent).toBe(agent);
|
||||
expect(profile.commandDigest).toMatch(/^sha256:[a-f0-9]{64}$/);
|
||||
|
||||
@@ -828,6 +828,25 @@ describe("ACPX runtime host", () => {
|
||||
expect(openRuntime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["expired credential", "provider process died"])("cleans Grok credentials after failed initialization: %s", async (failure) => {
|
||||
const fixture = await hostFixture();
|
||||
let home = "";
|
||||
await expect(AcpxRuntimeHost.open({
|
||||
...fixture.options, agent: "grok", model: "grok-4.7", permissionMode: "approve-reads",
|
||||
environment: { PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET: JSON.stringify({ "https://accounts.x.ai::11111111-1111-4111-8111-111111111111": { key: "test-expired", refresh_token: "test-refresh" } }) },
|
||||
}, fixture.dependencies({ openRuntime: async (options) => {
|
||||
home = options.launchEnvironment.GROK_HOME!;
|
||||
expect(options.permissionMode).toBe("approve-reads");
|
||||
expect(options.launchEnvironment).not.toHaveProperty("PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET");
|
||||
expect(await readFile(join(home, "auth.json"), "utf8")).toContain("test-expired");
|
||||
throw new Error(failure);
|
||||
} }))).rejects.toThrow(failure);
|
||||
for (const filename of ["auth.json", "auth-refresh.json"]) {
|
||||
await expect(readFile(join(home, filename))).rejects.toMatchObject({ code: "ENOENT" });
|
||||
}
|
||||
expect(fixture.commandClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("cleans credentials and command leases when provider open fails", async () => {
|
||||
const fixture = await hostFixture();
|
||||
let authPath = "";
|
||||
|
||||
@@ -338,6 +338,7 @@ export class AcpxRuntimeHost {
|
||||
if (installation.commandDigest !== profile.commandDigest) {
|
||||
throw new Error("Verified ACPX installation does not match its profile");
|
||||
}
|
||||
let admissionSucceeded = false;
|
||||
let command: VerifiedAcpxCommandLease | null = null;
|
||||
let credential: AcpxProviderLifetimeLease | null = null;
|
||||
let toolBridge: RunnerToolBridge | null = null;
|
||||
@@ -408,7 +409,7 @@ export class AcpxRuntimeHost {
|
||||
} else if (options.agent === "grok") {
|
||||
credential = await acquireAbortableAdmissionResource({
|
||||
signal: options.signal,
|
||||
acquire: () => stageManagedGrokCredential({ agentHomeDirectory: sandbox.agentHomeDirectory, environment: options.environment }),
|
||||
acquire: () => stageManagedGrokCredential({ agentHomeDirectory: sandbox.agentHomeDirectory, environment: options.environment, retainRefresh: () => admissionSucceeded }),
|
||||
resource: "credential", releaseLate: (lease) => lease.close(),
|
||||
reportFailure: (failure) => dependencies.reportRetainedCleanupFailure(failure),
|
||||
});
|
||||
@@ -550,6 +551,7 @@ export class AcpxRuntimeHost {
|
||||
verifyExpectedAcpxIdentity(options.expectedIdentity, binding, identity);
|
||||
}
|
||||
options.signal?.throwIfAborted();
|
||||
admissionSucceeded = true;
|
||||
return new AcpxRuntimeHost({
|
||||
runtime,
|
||||
binding,
|
||||
|
||||
@@ -428,7 +428,7 @@ export async function prepareAcpxRuntimeSandbox(input: {
|
||||
XDG_CACHE_HOME: cacheDirectory,
|
||||
PAPERCLIP_ACPX_PROFILE: input.agent,
|
||||
PAPERCLIP_ACPX_ISOLATED_CONTEXT: "1",
|
||||
...(input.agent === "grok" ? { GROK_HOME: agentHomeDirectory, NO_BROWSER: "1" } : {}),
|
||||
...(input.agent === "grok" ? { GROK_HOME: agentHomeDirectory, NO_BROWSER: "1", GROK_DISABLE_AUTOUPDATER: "1" } : {}),
|
||||
...(input.agent === "pi"
|
||||
? {
|
||||
PI_CODING_AGENT_DIR: agentHomeDirectory,
|
||||
|
||||
@@ -3254,7 +3254,7 @@ export function createRunnerdCodexAppServerArgs(input: {
|
||||
);
|
||||
}
|
||||
|
||||
function unwrapToolResponse(response: Record<string, unknown>): {
|
||||
export function unwrapToolResponse(response: Record<string, unknown>, preserveEnvelope = false): {
|
||||
readonly __paperclipSemanticToolOutcome: true;
|
||||
readonly result: unknown;
|
||||
readonly isError: boolean;
|
||||
@@ -3265,7 +3265,7 @@ function unwrapToolResponse(response: Record<string, unknown>): {
|
||||
const value = record(items[0]).text;
|
||||
let result: unknown = response;
|
||||
try {
|
||||
if (typeof value === "string") result = JSON.parse(value);
|
||||
if (!preserveEnvelope && typeof value === "string") result = JSON.parse(value);
|
||||
} catch {
|
||||
result = response;
|
||||
}
|
||||
@@ -5461,6 +5461,8 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
this.options.provider === "acpx" &&
|
||||
(call.operationId === "paperclip_finish" || call.operationId === "paperclip_block"),
|
||||
);
|
||||
if (call.operationId === "call_api") {
|
||||
const result = record(outcome.result);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { unwrapToolResponse } from "./runnerd-codex-transport.js";
|
||||
|
||||
describe("runnerd semantic tool response", () => {
|
||||
it("preserves the validated ACPX terminal acknowledgement envelope", () => {
|
||||
const response = { success: true, contentItems: [{ type: "inputText", text: JSON.stringify({ ok: true, accepted: true }) }] };
|
||||
expect(unwrapToolResponse(response, true)).toEqual({ __paperclipSemanticToolOutcome: true, result: response, isError: false });
|
||||
expect(unwrapToolResponse(response)).toEqual({ __paperclipSemanticToolOutcome: true, result: { ok: true, accepted: true }, isError: false });
|
||||
});
|
||||
it("keeps denial feedback and does not promote rejected terminal reports to success", () => {
|
||||
const response = { success: false, contentItems: [{ type: "inputText", text: JSON.stringify({ ok: false, denial: { code: "stale_completion_contract" } }) }] };
|
||||
expect(unwrapToolResponse(response, true)).toMatchObject({ isError: true, result: { success: false } });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user