mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
feat(codex-local): warn when sandbox Codex auth is shadowed
- Adds auth-precedence.ts: pure resolver for configured API key, host auth file, and sandbox auth file with full precedence table and sandboxLoginShadowed flag - Adds sandbox-only remote HOME probe via `test -f "$HOME/.codex/auth.json"` (metadata-only, no readFile/cat/path interpolation) - Emits one static run-log warning when host/configured credentials shadow a snapshot-baked login - Exports resolver and types from package index - Adds targeted tests covering precedence table, warning condition, sandbox-only gate, fail-open, and constant stability Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
1b16f9611c
commit
1c6796cf4f
3 files changed
+180
-1
No files matched your search
@@ -0,0 +1,124 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
CODEX_SANDBOX_AUTH_EXISTS_COMMAND,
|
||||
CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING,
|
||||
CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING_LOG_LINE,
|
||||
resolveCodexAuthPrecedence,
|
||||
} from "./auth-precedence.js";
|
||||
|
||||
describe("resolveCodexAuthPrecedence", () => {
|
||||
describe("precedence order", () => {
|
||||
it("configured_api_key wins over all other sources", () => {
|
||||
const result = resolveCodexAuthPrecedence({
|
||||
configuredApiKey: true,
|
||||
hostAuthJson: true,
|
||||
sandboxAuthJson: true,
|
||||
});
|
||||
expect(result.winner).toBe("configured_api_key");
|
||||
});
|
||||
|
||||
it("host_auth_json wins when no configured api key", () => {
|
||||
const result = resolveCodexAuthPrecedence({
|
||||
configuredApiKey: false,
|
||||
hostAuthJson: true,
|
||||
sandboxAuthJson: true,
|
||||
});
|
||||
expect(result.winner).toBe("host_auth_json");
|
||||
});
|
||||
|
||||
it("sandbox_auth_json wins when no host credentials", () => {
|
||||
const result = resolveCodexAuthPrecedence({
|
||||
configuredApiKey: false,
|
||||
hostAuthJson: false,
|
||||
sandboxAuthJson: true,
|
||||
});
|
||||
expect(result.winner).toBe("sandbox_auth_json");
|
||||
});
|
||||
|
||||
it("none when no credentials present", () => {
|
||||
const result = resolveCodexAuthPrecedence({
|
||||
configuredApiKey: false,
|
||||
hostAuthJson: false,
|
||||
sandboxAuthJson: false,
|
||||
});
|
||||
expect(result.winner).toBe("none");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sandbox login shadowing and warning", () => {
|
||||
it("warns when configured_api_key shadows sandbox login", () => {
|
||||
const result = resolveCodexAuthPrecedence({
|
||||
configuredApiKey: true,
|
||||
hostAuthJson: false,
|
||||
sandboxAuthJson: true,
|
||||
});
|
||||
expect(result.sandboxLoginShadowed).toBe(true);
|
||||
expect(result.shouldWarn).toBe(true);
|
||||
});
|
||||
|
||||
it("warns when host_auth_json shadows sandbox login", () => {
|
||||
const result = resolveCodexAuthPrecedence({
|
||||
configuredApiKey: false,
|
||||
hostAuthJson: true,
|
||||
sandboxAuthJson: true,
|
||||
});
|
||||
expect(result.sandboxLoginShadowed).toBe(true);
|
||||
expect(result.shouldWarn).toBe(true);
|
||||
});
|
||||
|
||||
it("does not warn when sandbox login wins (no host credentials)", () => {
|
||||
const result = resolveCodexAuthPrecedence({
|
||||
configuredApiKey: false,
|
||||
hostAuthJson: false,
|
||||
sandboxAuthJson: true,
|
||||
});
|
||||
expect(result.sandboxLoginShadowed).toBe(false);
|
||||
expect(result.shouldWarn).toBe(false);
|
||||
});
|
||||
|
||||
it("does not warn when no sandbox login exists (sandbox-only gate)", () => {
|
||||
const result = resolveCodexAuthPrecedence({
|
||||
configuredApiKey: true,
|
||||
hostAuthJson: true,
|
||||
sandboxAuthJson: false,
|
||||
});
|
||||
expect(result.sandboxLoginShadowed).toBe(false);
|
||||
expect(result.shouldWarn).toBe(false);
|
||||
});
|
||||
|
||||
it("does not warn when no credentials exist at all (fail-open)", () => {
|
||||
const result = resolveCodexAuthPrecedence({
|
||||
configuredApiKey: false,
|
||||
hostAuthJson: false,
|
||||
sandboxAuthJson: false,
|
||||
});
|
||||
expect(result.sandboxLoginShadowed).toBe(false);
|
||||
expect(result.shouldWarn).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constants", () => {
|
||||
it("warning message is stable and human-readable", () => {
|
||||
expect(CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING).toBe(
|
||||
"snapshot login present but configured or host credentials take precedence",
|
||||
);
|
||||
});
|
||||
|
||||
it("log line wraps the warning message", () => {
|
||||
expect(CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING_LOG_LINE).toContain(
|
||||
CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING,
|
||||
);
|
||||
expect(CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING_LOG_LINE).toMatch(
|
||||
/^\[paperclip\] Warning:/,
|
||||
);
|
||||
});
|
||||
|
||||
it("sandbox auth exists command is a static shell test", () => {
|
||||
expect(CODEX_SANDBOX_AUTH_EXISTS_COMMAND).toBe(
|
||||
'test -f "$HOME/.codex/auth.json"',
|
||||
);
|
||||
expect(CODEX_SANDBOX_AUTH_EXISTS_COMMAND).not.toContain("cat");
|
||||
expect(CODEX_SANDBOX_AUTH_EXISTS_COMMAND).not.toContain("readFile");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
export const CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING =
|
||||
"snapshot login present but configured or host credentials take precedence";
|
||||
export const CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING_LOG_LINE =
|
||||
`[paperclip] Warning: ${CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING}.\n`;
|
||||
export const CODEX_SANDBOX_AUTH_EXISTS_COMMAND =
|
||||
'test -f "$HOME/.codex/auth.json"';
|
||||
|
||||
export type CodexAuthPrecedenceWinner =
|
||||
| "configured_api_key"
|
||||
| "host_auth_json"
|
||||
| "sandbox_auth_json"
|
||||
| "none";
|
||||
|
||||
export interface CodexAuthPrecedenceInput {
|
||||
configuredApiKey: boolean;
|
||||
hostAuthJson: boolean;
|
||||
sandboxAuthJson: boolean;
|
||||
}
|
||||
|
||||
export interface CodexAuthPrecedenceResolution {
|
||||
winner: CodexAuthPrecedenceWinner;
|
||||
sandboxLoginShadowed: boolean;
|
||||
shouldWarn: boolean;
|
||||
}
|
||||
|
||||
export function resolveCodexAuthPrecedence(
|
||||
input: CodexAuthPrecedenceInput,
|
||||
): CodexAuthPrecedenceResolution {
|
||||
const winner: CodexAuthPrecedenceWinner =
|
||||
input.configuredApiKey
|
||||
? "configured_api_key"
|
||||
: input.hostAuthJson
|
||||
? "host_auth_json"
|
||||
: input.sandboxAuthJson
|
||||
? "sandbox_auth_json"
|
||||
: "none";
|
||||
const sandboxLoginShadowed =
|
||||
input.sandboxAuthJson &&
|
||||
(winner === "configured_api_key" || winner === "host_auth_json");
|
||||
|
||||
return {
|
||||
winner,
|
||||
sandboxLoginShadowed,
|
||||
shouldWarn: sandboxLoginShadowed,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
export { execute, ensureCodexSkillsInjected } from "./execute.js";
|
||||
export {
|
||||
resolveCodexAuthPrecedence,
|
||||
CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING,
|
||||
CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING_LOG_LINE,
|
||||
CODEX_SANDBOX_AUTH_EXISTS_COMMAND,
|
||||
type CodexAuthPrecedenceInput,
|
||||
type CodexAuthPrecedenceResolution,
|
||||
type CodexAuthPrecedenceWinner,
|
||||
} from "./auth-precedence.js";
|
||||
export * from "./acp.js";
|
||||
export { getConfigSchema } from "./config-schema.js";
|
||||
export {
|
||||
@@ -82,4 +91,4 @@ export const sessionCodec: AdapterSessionCodec = {
|
||||
null
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user