From a8f109080ec3baeae1fdfa6a277319bb71b9e0ff Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 23 Sep 2026 16:00:40 -0700 Subject: [PATCH] fix(grok): discard credential-bearing diagnostic logs Exclude Grok diagnostics from durable recovery checkpoints and remove them after provider containment while preserving session history. Co-Authored-By: Paperclip --- doc/grok-native-runner.md | 7 ++-- .../src/drivers/acpx/grok-credentials.test.ts | 16 ++++++++- .../src/drivers/acpx/grok-credentials.ts | 4 +++ .../native-session-executor.test.ts | 35 +++++++++++++++++++ .../native-runtime/native-session-executor.ts | 5 +-- 5 files changed, 61 insertions(+), 6 deletions(-) diff --git a/doc/grok-native-runner.md b/doc/grok-native-runner.md index b31d1686872..eebd0b0416f 100644 --- a/doc/grok-native-runner.md +++ b/doc/grok-native-runner.md @@ -36,9 +36,10 @@ existing Grok login when no company login has been selected. Only the selected credential is staged in the private runtime home. An ownership lease fences concurrent processes. After the provider exits, refreshed credentials are copied back through Grok's existing identity and refresh checks. Runtime -credentials and refresh handoffs are excluded from workspace backups and removed -on close. Host configuration, other provider credentials and unselected keys are -not forwarded to Grok. +credentials, refresh handoffs, and diagnostic logs are excluded from workspace +backups and removed on close. Session history remains available for resume. Host +configuration, other provider credentials and unselected keys are not forwarded +to Grok. ## Evaluation diff --git a/packages/paperclip-runner/src/drivers/acpx/grok-credentials.test.ts b/packages/paperclip-runner/src/drivers/acpx/grok-credentials.test.ts index 8c5609b66b0..e5267f360d8 100644 --- a/packages/paperclip-runner/src/drivers/acpx/grok-credentials.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/grok-credentials.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm, stat, symlink, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; @@ -29,6 +29,20 @@ describe("Grok isolated credentials", () => { expect(await readFile(join(directory, "auth-refresh.json"), "utf8")).toBe(refreshed); await lease.close(); }); + it.each([true, false])("removes diagnostic logs after shutdown without removing session history (admitted=%s)", async retainRefresh => { + const directory = await home(); + const lease = await stageManagedGrokCredential({ agentHomeDirectory: directory, + environment: { PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET: credential }, retainRefresh: () => retainRefresh }); + await mkdir(join(directory, "logs")); + await mkdir(join(directory, "sessions")); + await writeFile(join(directory, "logs", "unified.jsonl"), credential, { mode: 0o600 }); + await writeFile(join(directory, "sessions", "session.json"), "resume-state", { mode: 0o600 }); + await lease.close(); + await expect(stat(join(directory, "logs"))).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(join(directory, "sessions", "session.json"), "utf8")).toBe("resume-state"); + if (retainRefresh) expect(await readFile(join(directory, "auth-refresh.json"), "utf8")).toBe(credential); + else await expect(stat(join(directory, "auth-refresh.json"))).rejects.toMatchObject({ code: "ENOENT" }); + }); it("does not stage subscription credentials for an explicit API key", async () => { const directory = await home(); const lease = await stageManagedGrokCredential({ agentHomeDirectory: directory, environment: { XAI_API_KEY: "test-key" } }); diff --git a/packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts b/packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts index e99709d3646..88060a45bf9 100644 --- a/packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts +++ b/packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts @@ -79,6 +79,10 @@ export async function stageManagedGrokCredential(input: { } finally { await auth.close(); } } } + // Grok diagnostics can contain account/authentication fields. They are + // disposable, unlike session history, and must not outlive this owner. + // close is invoked only after the provider has been contained. + await rm(join(input.agentHomeDirectory, "logs"), { recursive: true, force: true }); await lease.close(); closed = true; })().finally(() => { closing = null; }); diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index d8cbfdc5002..74f66087fe2 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { access, + cp, lstat, mkdir, mkdtemp, @@ -1809,6 +1810,40 @@ describe("split durable provider checkpoint identity", () => { }); describe("remote provider checkpoint snapshots", () => { + it("excludes Grok credentials and diagnostic logs from real checkpoint copies while retaining sessions", async () => { + const root = await mkdtemp(join(tmpdir(), "grok-checkpoint-")); + try { + const source = join(root, "source"); + const target = join(root, "backup"); + const sessionDirectory = acpxRuntimeSessionDirectoryName("session"); + const relativeHome = join("acpx", sessionDirectory, "grok-home"); + const home = join(source, relativeHome); + await mkdir(join(home, "logs"), { recursive: true }); + await mkdir(join(home, "sessions")); + await writeFile(join(home, "logs", "unified.jsonl"), "AUTH_SENTINEL"); + await writeFile(join(home, "auth.json"), "AUTH_SENTINEL"); + await writeFile(join(home, "auth-refresh.json"), "AUTH_SENTINEL"); + await writeFile(join(home, "sessions", "session.json"), "resume-state"); + const profile = resolveNativeHarnessPersistenceProfile({ + provider: { kind: "acpx", agent: "grok" }, + session: { normalizedSessionId: "session", driverKind: "acpx_runtime" }, + } as unknown as NativeExecutionInputV1); + const execute = async ({ command, args }: { command: string; args: string[] }) => ({ + exitCode: 0, timedOut: false, stdout: execFileSync(command, args, { encoding: "utf8" }), stderr: "", + }); + const syncOut = async (operations: Array<{ files: Array<{ sourcePath: string; targetPath: string }> }>) => { + for (const operation of operations) for (const file of operation.files) + await cp(file.sourcePath, file.targetPath, { recursive: true }); + }; + await syncRemoteRunnerDirectoryOut({ runner: { execute, syncOut } as never, + sourcePath: source, targetPath: target, mode: 0o700, + excludeEntries: profile.directories.find(directory => directory.name === "acpx")!.excludeEntries }); + for (const entry of ["auth.json", "auth-refresh.json", "logs"]) + await expect(access(join(target, relativeHome, entry))).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(join(target, relativeHome, "sessions", "session.json"), "utf8")).toBe("resume-state"); + expect(await readFile(join(home, "logs", "unified.jsonl"), "utf8")).toBe("AUTH_SENTINEL"); + } finally { await rm(root, { recursive: true, force: true }); } + }); it("excludes Codex scratch and credential state without mutating the live provider home", async () => { const execute = vi .fn() diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 27caf2856f3..826bccd7aae 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -5301,10 +5301,11 @@ export function resolveNativeHarnessPersistenceProfile( // Codex creates process-local executable aliases in tmp/arg0; // they may point outside the runtime tree and are neither safe // nor necessary to restore. Credentials and launch-time config - // are also re-materialized in the replacement sandbox. + // are also re-materialized in the replacement sandbox. Grok + // diagnostics can contain auth fields and are not session state. excludeEntries: execution.provider.agent === "grok" - ? ["auth.json", "auth-refresh.json", "auth-refresh.json.tmp", "config.toml"].map((entry) => + ? ["auth.json", "auth-refresh.json", "auth-refresh.json.tmp", "config.toml", "logs"].map((entry) => `acpx/${acpxRuntimeSessionDirectoryName(nativeSessionKey(execution))}/grok-home/${entry}`) : execution.provider.agent === "codex" ? CODEX_HOME_NON_PERSISTENT_ENTRIES.map(