From e327370d963ae2d2a34988bdf5b10d2cee6a79e4 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 17 Jul 2026 04:22:33 -0700 Subject: [PATCH 1/2] fix(miner): forward HOME and the real coding-agent credential to CLI subprocesses The claude-cli/codex-cli driver's parentEnv is deliberately, strictly allowlisted and never carries HOME or any credential (a coding-agent subprocess getting the operator's full, unvetted 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 -- 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 invoked command's own real credential keys (CLAUDE_CODE_OAUTH_TOKEN/ANTHROPIC_API_KEY for claude, OPENAI_API_KEY/CODEX_ACCESS_TOKEN for codex) from the full env this factory already has, command-scoped so an operator with both providers' keys set never leaks the unrelated one -- never the raw env object itself, preserving the strict-allowlist boundary for everything else. Confirmed via a live, authenticated end-to-end reproduction (real Read/Bash tool use, a real committed fix). Closes #6875 --- .../src/miner/driver-factory.ts | 26 +++++++++++++++ test/unit/coding-agent-miner.test.ts | 33 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/packages/loopover-engine/src/miner/driver-factory.ts b/packages/loopover-engine/src/miner/driver-factory.ts index 53a39f0599..3815ed99c3 100644 --- a/packages/loopover-engine/src/miner/driver-factory.ts +++ b/packages/loopover-engine/src/miner/driver-factory.ts @@ -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): Record { + const credentialKeys = command === "claude" ? CLAUDE_CREDENTIAL_ENV_KEYS : CODEX_CREDENTIAL_ENV_KEYS; + const resolved: Record = {}; + 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): number | undefined { const raw = Number(firstConfiguredEnvValue(env.MINER_CODING_AGENT_TIMEOUT_MS)); @@ -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 } : {}), diff --git a/test/unit/coding-agent-miner.test.ts b/test/unit/coding-agent-miner.test.ts index fae02caece..131f5c528e 100644 --- a/test/unit/coding-agent-miner.test.ts +++ b/test/unit/coding-agent-miner.test.ts @@ -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); From a1d9504a7d70fca4ffd1d0c69b485c97235f1137 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 17 Jul 2026 04:24:06 -0700 Subject: [PATCH 2/2] docs(ams): correct the stale tokens_used-always-NULL observability claim Predates #5653, which wired real per-iteration token accumulation into finalMeterTotals.tokens -- the pipeline already correctly persists real tokens_used today; only a genuinely non-running or non-reporting driver produces an honest 0. --- apps/loopover-ui/content/docs/ams-observability.mdx | 2 +- packages/loopover-miner/docs/observability.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/loopover-ui/content/docs/ams-observability.mdx b/apps/loopover-ui/content/docs/ams-observability.mdx index cf63cbd2fb..555ba2d961 100644 --- a/apps/loopover-ui/content/docs/ams-observability.mdx +++ b/apps/loopover-ui/content/docs/ams-observability.mdx @@ -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", diff --git a/packages/loopover-miner/docs/observability.md b/packages/loopover-miner/docs/observability.md index e6427e6795..83a42c9c67 100644 --- a/packages/loopover-miner/docs/observability.md +++ b/packages/loopover-miner/docs/observability.md @@ -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