Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/loopover-ui/content/docs/ams-observability.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ deployment guide](/docs/ams-deployment)):
{
title: "attempt-log.sqlite3",
description:
"The driver-level attempt event trace (event type, action class, mode, reason, timestamps), table attempt_log_events. One attempt_outcome_summary row per completed attempt also carries the real configured provider and the real accumulated cost_usd -- tokens_used is always NULL today, an honest gap rather than a fabricated 0: no coding-agent driver reports real token usage yet.",
"The driver-level attempt event trace (event type, action class, mode, reason, timestamps), table attempt_log_events. One attempt_outcome_summary row per completed attempt also carries the real configured provider, the real accumulated cost_usd, and the real accumulated tokens_used -- 0, never fabricated, for an attempt whose driver never actually ran or whose provider reports no token signal for a given iteration.",
},
{
title: "prediction-ledger.sqlite3",
Expand Down
26 changes: 26 additions & 0 deletions packages/loopover-engine/src/miner/driver-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,31 @@ function firstConfiguredEnvValue(value: string | undefined): string | undefined
return trimmed ? trimmed : undefined;
}

// The credential env var names each CLI actually reads. Command-scoped (not a blanket forward of every
// possible provider's credential regardless of which CLI is running) so an operator with both
// ANTHROPIC_API_KEY and OPENAI_API_KEY set only ever leaks the one the invoked CLI actually needs.
const CLAUDE_CREDENTIAL_ENV_KEYS = ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"] as const;
const CODEX_CREDENTIAL_ENV_KEYS = ["OPENAI_API_KEY", "CODEX_ACCESS_TOKEN"] as const;

/** #6875: `createCliSubprocessCodingAgentDriver`'s `parentEnv` is deliberately, strictly allowlisted and never
* carries `HOME` or any credential (a coding-agent subprocess getting the operator's full, un-vetted
* environment is the exact thing that allowlist exists to prevent) -- its separate `env` field ("extra env
* overlaid on the allowlisted parent") is the intended channel for exactly this kind of deliberately, narrowly
* forwarded value, but nothing ever populated it, so a spawned `claude`/`codex` subprocess had no `HOME` to
* locate a persisted credential file with, AND no credential env var either -- unable to authenticate at all,
* independent of and more fundamental than #6840's separate `--permission-mode` gap. Resolves only `HOME` plus
* the command's own real credential keys from the full env this factory already has -- never the raw env
* object itself, preserving the strict-allowlist boundary for everything else. */
function resolveCliCredentialEnv(command: "claude" | "codex", env: Record<string, string | undefined>): Record<string, string | undefined> {
const credentialKeys = command === "claude" ? CLAUDE_CREDENTIAL_ENV_KEYS : CODEX_CREDENTIAL_ENV_KEYS;
const resolved: Record<string, string | undefined> = {};
if (env.HOME !== undefined) resolved.HOME = env.HOME;
for (const key of credentialKeys) {
if (env[key] !== undefined) resolved[key] = env[key];
}
return resolved;
}

/** Positive-integer env parse for the CLI wall-clock ceiling; anything else defers to the driver default. */
function configuredTimeoutMs(env: Record<string, string | undefined>): number | undefined {
const raw = Number(firstConfiguredEnvValue(env.MINER_CODING_AGENT_TIMEOUT_MS));
Expand Down Expand Up @@ -168,6 +193,7 @@ function createCliProvider(
command,
spawn: options.spawn,
parentEnv: env,
env: resolveCliCredentialEnv(command, env),
...(timeoutMs !== undefined ? { timeoutMs } : {}),
...(buildArgs !== undefined ? { buildArgs } : {}),
...(options.knownSecrets !== undefined ? { knownSecrets: options.knownSecrets } : {}),
Expand Down
5 changes: 3 additions & 2 deletions packages/loopover-miner/docs/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ The miner writes append-only SQLite ledgers under `LOOPOVER_MINER_CONFIG_DIR` (d

- **`attempt-log.sqlite3`** — the driver-level attempt event trace (event type, action class, mode, reason,
timestamps), table `attempt_log_events`. One `attempt_outcome_summary` row per completed attempt also carries
the real configured `provider` and the real accumulated `cost_usd` (#5185) — `tokens_used` is always `NULL`
today, an honest gap rather than a fabricated `0`: no coding-agent driver reports real token usage yet (#5395).
the real configured `provider`, the real accumulated `cost_usd` (#5185), and the real accumulated
`tokens_used` (#5653) — `0`, never fabricated, for an attempt whose driver never actually ran (e.g. blocked
before invoking the CLI at all) or whose provider genuinely reports no token signal for a given iteration.
- **`prediction-ledger.sqlite3`** — recorded predicted-gate verdicts for later scoring.

Those live files can contain free-form payloads, repo/target identifiers, readiness scores, and blocker/warning
Expand Down
33 changes: 33 additions & 0 deletions test/unit/coding-agent-miner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,39 @@ describe("createCodingAgentDriver provider resolution (#4289)", () => {
expect([...calls[0]!.args].slice(0, 3)).toEqual(["exec", "--model", "gpt-5.1-codex"]);
});

it("#6875: forwards HOME and the command's own credential to the spawned CLI, never the other provider's credential", async () => {
const { spawn: claudeSpawn, calls: claudeCalls } = recordingSpawn();
await createCodingAgentDriver({
providerName: "claude-cli",
spawn: claudeSpawn,
env: { HOME: "/home/miner", CLAUDE_CODE_OAUTH_TOKEN: "claude-token-value", OPENAI_API_KEY: "codex-key-value" },
}).run(cliTask);
expect(claudeCalls[0]!.opts.env.HOME).toBe("/home/miner");
expect(claudeCalls[0]!.opts.env.CLAUDE_CODE_OAUTH_TOKEN).toBe("claude-token-value");
expect(claudeCalls[0]!.opts.env.OPENAI_API_KEY).toBeUndefined();

const { spawn: codexSpawn, calls: codexCalls } = recordingSpawn();
await createCodingAgentDriver({
providerName: "codex-cli",
spawn: codexSpawn,
env: { HOME: "/home/miner", OPENAI_API_KEY: "codex-key-value", CLAUDE_CODE_OAUTH_TOKEN: "claude-token-value" },
}).run(cliTask);
expect(codexCalls[0]!.opts.env.HOME).toBe("/home/miner");
expect(codexCalls[0]!.opts.env.OPENAI_API_KEY).toBe("codex-key-value");
expect(codexCalls[0]!.opts.env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
});

it("#6875: forwards ANTHROPIC_API_KEY/CODEX_ACCESS_TOKEN too, and omits HOME entirely when it isn't configured", async () => {
const { spawn: claudeSpawn, calls: claudeCalls } = recordingSpawn();
await createCodingAgentDriver({ providerName: "claude-cli", spawn: claudeSpawn, env: { ANTHROPIC_API_KEY: "sk-ant-value" } }).run(cliTask);
expect(claudeCalls[0]!.opts.env.ANTHROPIC_API_KEY).toBe("sk-ant-value");
expect(claudeCalls[0]!.opts.env.HOME).toBeUndefined();

const { spawn: codexSpawn, calls: codexCalls } = recordingSpawn();
await createCodingAgentDriver({ providerName: "codex-cli", spawn: codexSpawn, env: { CODEX_ACCESS_TOKEN: "codex-access-value" } }).run(cliTask);
expect(codexCalls[0]!.opts.env.CODEX_ACCESS_TOKEN).toBe("codex-access-value");
});

it("CONSUMES the declared timeout env key when it is a positive integer, else defers to the driver default", async () => {
const { spawn, calls } = recordingSpawn();
await createCodingAgentDriver({ providerName: "claude-cli", spawn, env: { MINER_CODING_AGENT_TIMEOUT_MS: "90000" } }).run(cliTask);
Expand Down
Loading