mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
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 <noreply@paperclip.ing>
This commit is contained in:
1 parent
697b4d0526
commit
a8f109080e
5 files changed
+61
-6
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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" } });
|
||||
|
||||
@@ -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; });
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user