fix(coding-agent): resolve SDK bus tokens from trusted env - #3288
Conversation
Yeachan-Heo
left a comment
There was a problem hiding this comment.
Verdict: APPROVE — exact head c9c8e34fe0c835e0046feb0232e6e57a820669cd
Two fixes: notificationsEnabled() now reads GJC_NOTIFICATIONS/GJC_NOTIFICATIONS_TOKEN via $credentialEnv (its siblings in config.ts/session-control.ts already used an injected env record — this was the outlier direct read), and the Telegram reference client's GJC_TG_BOT_TOKEN/GJC_TG_CHAT_ID fallbacks do the same.
Checks performed:
- Notification reply/privacy routing: this is the primary risk surface for this PR. Before the fix, a repo-planted
GJC_NOTIFICATIONS_TOKENcould open the session control/answer channel, and a repo-plantedGJC_TG_BOT_TOKENcould route session notifications and the operator's replies to a bot the repository chose — verified in code and covered by dedicated test cases (ignores a Telegram bot token planted by the project .env,does not let the project .env enable the notifications channelvia both the token and the=1flag paths). This closes a real reply-privacy exfiltration path, not just a credential leak. - Trust-source precedence: consistent with
$credentialEnvboundary used across the batch. - Overlap/order: shares the
packages/coding-agent/CHANGELOG.mdanchor with #3286/#3287 — same trivial note, no source-file overlap. - Tests: hermetic (
HOME/GJC_CODING_AGENT_DIRboth neutralized); covers default-disabled, project-cannot-enable (both paths), project-cannot-plant-bot-token, inherited-honored, and project-cannot-override-inherited. - CI: all 22 checks pass on the exact head (two shard/check jobs were still pending at first poll; rechecked and confirmed all-pass before this verdict).
- Novelty: no other open PR touches
sdk/bus/index.tsorsdk/bus/telegram-cli.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) 🦞]
afe7099 to
5ba3b09
Compare
Yeachan-Heo
left a comment
There was a problem hiding this comment.
Verdict: APPROVE — exact head 5ba3b0991b68fcfda2257dd7a928a91b76839e00
Rebase-only refresh of the prior exact-head review at c9c8e34fe0c835e0046feb0232e6e57a820669cd (APPROVE).
Checks performed:
- Content identity confirmed, not assumed:
diff <(git show c9c8e34f --format= -- packages/coding-agent/src/sdk/bus/index.ts packages/coding-agent/src/sdk/bus/telegram-cli.ts packages/coding-agent/test/fixtures/sdk-bus-token-probe.ts packages/coding-agent/test/sdk-bus-token-trust.test.ts) <(git show 5ba3b099 --format= -- packages/coding-agent/src/sdk/bus/index.ts packages/coding-agent/src/sdk/bus/telegram-cli.ts packages/coding-agent/test/fixtures/sdk-bus-token-probe.ts packages/coding-agent/test/sdk-bus-token-trust.test.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: 17 success / 0 failure / 0 pending / 5 skipped (total 22). 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 5ba3b0991b68fcfda2257dd7a928a91b76839e00
Verdict: MERGE_READY — held, do not merge (see gating).
Trace
Two independent fixes:
sdk/bus/index.ts:notificationsEnabled()—GJC_NOTIFICATIONS/GJC_NOTIFICATIONS_TOKENmove from rawprocess.envto$credentialEnv. The outlier claim is verified against the tree, not taken on trust:sdk/bus/config.ts:431-498andsdk/bus/session-control.ts:424-428both read an injected env record (input.env/this.#env) rather than the merged global view. This function was the only direct global read of that switch. Enabling it opens the session control/answer channel, so repository-reachable enablement was the real exposure.sdk/bus/telegram-cli.ts:main()—GJC_TG_BOT_TOKEN/GJC_TG_CHAT_IDfallbacks move to$credentialEnv. Explicit--bot-token/--chat-idargs keep precedence. This client is normally invoked from inside a repository, so the planted-token path was directly reachable; a planted bot token would have routed the session's notifications and the operator's replies to an attacker's bot.
Asymmetry at index.ts:4860 — checked, and correct as-is
process.env.GJC_NOTIFICATIONS === "0" is still read from the merged view. I probed this as a possible missed conversion and it is not one: that read is a hard opt-out. Leaving it on the merged view means a repository can only ever disable notifications, never enable them. Converting it would strictly reduce the fail-safe. The direction of trust is right — narrow the enabling read, leave the disabling read broad.
Tests / CI
sdk-bus-token-trust.test.ts uses the hermetic subprocess pattern with a controlled cwd. Exact-head CI green: check:@gajae-code/coding-agent, test:packages/coding-agent/test/tools/index.test.ts, native-build, cli-smoke, Telegram daemon generation guard, gjc-state-gates.
Merge gating
dev CI is currently red. Durable non-resident hold; sequencing only, no technical defect.
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
5ba3b09 to
beb64be
Compare
Two reads went through the merged view that includes the caller's cwd/.env: sdk/bus/index.ts:1147 GJC_NOTIFICATIONS / GJC_NOTIFICATIONS_TOKEN sdk/bus/telegram-cli.ts:154 GJC_TG_BOT_TOKEN (and :171 GJC_TG_CHAT_ID) notificationsEnabled() decides whether the session control/answer channel opens at all, so a repository could turn it on. Its siblings in config.ts and session-control.ts already read an injected env record rather than the merged view, so this direct read was the outlier rather than the pattern. The Telegram reference client selects the bot that receives the session's notifications and the operator's replies, and it is normally run from inside a repository, so a planted token would have routed both to a bot the repository chose. Explicit --bot-token / --chat-id arguments still take precedence. Both now resolve through $credentialEnv. Regression: a child process with a controlled cwd and neutral HOME/agent dir proves the project .env can enable the channel through neither the token nor the flag, cannot plant a bot token, and cannot override inherited settings. Reverting notificationsEnabled fails exactly the two enable cases.
beb64be to
6a1aa10
Compare
Same trust-boundary class as #3276 / #3286, applied to the SDK notification bus.
Problem
sdk/bus/index.ts:1147GJC_NOTIFICATIONS,GJC_NOTIFICATIONS_TOKENnotificationsEnabled()decides whether the session control/answer channel openssdk/bus/telegram-cli.ts:154,:171GJC_TG_BOT_TOKEN,GJC_TG_CHAT_IDBun.env === process.env, and the env module folds the caller'scwd/.envinto it.The bus read was an internal outlier. Its siblings already avoid the merged view:
The Telegram client matters because it is normally run from inside a repository, so a planted bot token routes the session's notifications and the operator's replies to a bot the repository chose.
Measured with a project
.envas the only input (HOME and agent dir neutral):Fix
Both resolve through
$credentialEnv. Explicit--bot-token/--chat-idarguments still take precedence, and shell /~/.env/ GJC-owned.envconfiguration is unchanged.Tests
sdk-bus-token-trust.test.tsspawns a probe with a controlled cwd and neutralHOME/GJC_CODING_AGENT_DIR: disabled with no token by default, the project.envcan enable the channel through neitherGJC_NOTIFICATIONS_TOKENnorGJC_NOTIFICATIONS=1, a planted bot token is ignored, inherited settings still apply, and a planted.envcannot override them.Proof-first: reverting
notificationsEnabledfails exactly the two enable cases (4 pass / 2 fail).Verification
bun run checkinpackages/coding-agent(whole-package biome + tsc): exit 0. New suite: 6 pass / 0 fail. Notification-named suites: 574 pass / 0 fail.