fix(coding-agent): resolve the Exa API key from trusted env - #3287
Conversation
There was a problem hiding this comment.
Verdict: APPROVE — exact head adf8b3f9e7d5ea137bed163acc3c86b6c6673d07
findApiKey() moves from $env.EXA_API_KEY (merged view including caller's cwd/.env) to $credentialEnv("EXA_API_KEY"). The key authenticates every Exa MCP call and travels in the request URL, so this closes an account-redirection/query-visibility exposure.
Checks performed:
- Trust-source precedence: consistent with the
$credentialEnvboundary validated in #3285/#3286. - Type correctness bonus:
findApiKey()now returnsnull(notundefined) when nothing supplies a key, matching its declaredstring | nullreturn type — a genuine, in-scope bug fix riding along with the trust fix, not scope creep. - Overlap/order: shares the same
packages/coding-agent/CHANGELOG.mdanchor as #3286/#3288 — same trivial additive-conflict note as above, no functional overlap in source files. - Tests: hermetic (
HOMEandGJC_CODING_AGENT_DIRboth pointed at temp dirs); exercises default/no-key, project-planted rejection, inherited-honored, and project-cannot-override-inherited. - CI: all 20 checks pass on the exact head.
- Novelty: no other open PR touches
exa/mcp-client.ts; not a duplicate. - No automated reviewer comments present.
No merge action taken — post-#3281 dev CI 30253189244 is nonterminal and this batch is read-only review only.
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
82bda93 to
d152d86
Compare
Yeachan-Heo
left a comment
There was a problem hiding this comment.
Verdict: APPROVE — exact head d152d86889fee89dec0b6e5162aa2feb7603fb65
Rebase-only refresh of the prior exact-head review at adf8b3f9e7d5ea137bed163acc3c86b6c6673d07 (APPROVE).
Checks performed:
- Content identity confirmed, not assumed:
diff <(git show adf8b3f9 --format= -- packages/coding-agent/src/exa/mcp-client.ts packages/coding-agent/test/exa-api-key-trust.test.ts packages/coding-agent/test/fixtures/exa-api-key-probe.ts) <(git show d152d868 --format= -- packages/coding-agent/src/exa/mcp-client.ts packages/coding-agent/test/exa-api-key-trust.test.ts packages/coding-agent/test/fixtures/exa-api-key-probe.ts)is empty — the PR's own commit is byte-for-byte identical to the version the prior review validated. CHANGELOG.md is intentionally excluded from this file set: its diff context shifted because independently-mergeddevcommits inserted new entries above the same insertion point, which is expected rebase churn, not a content change to this PR's own line. - Rebase mechanics verified:
git merge-baseconfirms this PR carries the same single commit both before and after the rebase; every additional commit between the two heads is an independently-mergeddevcommit (own-PR-numbered), not new work folded into this PR. - Exact-head terminal CI: 15 success / 0 failure / 0 pending / 5 skipped (total 20). No pending/queued checks remain.
Prior technical analysis still applies verbatim to this head.
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
Yeachan-Heo
left a comment
There was a problem hiding this comment.
Adversarial batch re-review — exact head d152d86889fee89dec0b6e5162aa2feb7603fb65
Verdict: MERGE_READY — held, do not merge (see gating).
Trace
findApiKey() in packages/coding-agent/src/exa/mcp-client.ts moves EXA_API_KEY from $env (merged view, includes caller's cwd/.env) to $credentialEnv("EXA_API_KEY") ?? null.
Severity reasoning independently checked: the key authenticates every Exa MCP call and travels in the request URL, so a repository-supplied key redirects the agent's searches through an attacker-controlled account — making the queries themselves visible to that account. That is a real exfiltration channel, not only a billing swap.
Type correctness — a genuine secondary fix
The declared return type is string | null, but the previous body returned $env.EXA_API_KEY, i.e. string | undefined, silently widening at runtime. The explicit ?? null now matches the signature. Callers such as websets.ts and mcp-client.ts:327 branch on falsiness, so behavior is unchanged for both null and undefined — this is a correctness tightening with no behavioral risk.
Residual reads audited
Remaining EXA_API_KEY occurrences on this head are non-credential-resolution sites and correctly left alone:
packages/ai/src/stream.ts:114— provider→env-name label map, not a read.runtime-mcp/config.ts— parses a key out of user-supplied MCP config/URL, a different trust domain.eval/py/runtime.ts,plugins/doctor.ts,cli/fast-help.ts,settings-schema.ts— passthrough allowlist, doctor display, help text.
No unconverted authenticating read of EXA_API_KEY remains.
Tests / CI
exa-api-key-trust.test.ts follows the batch's hermetic subprocess pattern (controlled cwd, neutralized HOME/agent dir). Exact-head CI green: test:packages/coding-agent/test/exa-api-key-trust.test.ts, check:@gajae-code/coding-agent, native-build, cli-smoke.
Merge gating
dev CI is currently red. Durable non-resident hold; sequencing only, no technical defect.
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
d152d86 to
75c63af
Compare
findApiKey() read $env.EXA_API_KEY, the merged view that includes the caller's cwd/.env. The key authenticates every Exa MCP call and travels in the request URL (?exaApiKey=...), so repository content could decide which account the agent's searches run through, and therefore who can see those queries. The Exa endpoints themselves are hard-coded, so this is credential supply rather than redirection — stated plainly rather than overclaimed. Resolve through $credentialEnv. The function also now returns null instead of undefined when nothing supplies a key, which matches its declared string | null return type; callers already branch on truthiness and the exa suites stay green. Regression: a child process with a controlled cwd and neutral HOME/agent dir proves a planted .env cannot supply the key, an inherited key still applies, and a planted .env cannot override it. Reverting the resolver fails the planted case.
75c63af to
d948f01
Compare
Same trust-boundary class as #3272 / #3283, applied to the Exa MCP client.
Problem
Bun.env === process.env, and the env module folds the caller'scwd/.envinto it, so a repository could supply this key.Honest severity
The Exa endpoints are hard-coded (
https://mcp.exa.ai/mcp,https://websetsmcp.exa.ai/mcp), so this is not a redirect and the user's own key cannot be exfiltrated. What it does allow is credential supply: a repository can hand the agent an account it controls, and the key travels in the request URL, so every search the agent runs through Exa lands in that account's history. That is worth closing, and it is the rule the codebase already states for provider credentials.Measured with a project
.envas the only input (HOME and agent dir neutral):Fix
Resolve through
$credentialEnv. The launching shell,~/.env, the GJC config.envand the agent.envkeep working.One incidental correctness fix: the function declared
string | nullbut returned$env.EXA_API_KEY, i.e.undefined, when unset. It now returnsnull, matching the declared type. Callers already branch on truthiness and the exa suites stay green (746 pass).Tests
exa-api-key-trust.test.tsspawns a probe with a controlled cwd and neutralHOME/GJC_CODING_AGENT_DIR: no key by default, a planted.envcannot supply one, an inherited key still applies, and a planted.envcannot override it.Proof-first: reverting the resolver fails the planted case.
Verification
bun run checkinpackages/coding-agent(whole-package biome + tsc): exit 0. New suite: 4 pass / 0 fail. Exa-named suites: 746 pass / 0 fail.