fix(agent): harden narrationDirective against secret leakage - #211
Conversation
…210) Copilot flagged HIGH-severity credential-exposure risk on PR #210: the `full` narration directive asked the subprocess agent to echo commands and output verbatim, which can carry API keys (argv `--token=`), bearer tokens (`Authorization:` headers), env-var values from a `printenv` call, connection strings, private keys, etc. Every mode (off/brief/full — including the "bogus"→brief fallback) now carries a shared REDACTION clause that: - names the secret classes to substitute (API keys, bearer/OAuth/session tokens, passwords, private keys, Authorization/Cookie header values, connection strings, --token=/--api-key=/--password=/--secret= flag values, AWS_*/GITHUB_TOKEN/ANTHROPIC_API_KEY/*_KEY/*_TOKEN/*_SECRET env-var values, webhook signing secrets), - specifies the substitution (`<REDACTED>`, not omission), - forbids quoting `env`/`printenv`/`railway variables` output or `.env*` file contents in the narration channel, - covers `off` mode too because "only narrate on failure" is exactly where secrets tend to appear (401 responses, curl -v dumps). Subprocess agents (Codex CLI, Antigravity, opencode-selected models) do not inherit qmax-code's own secret-handling rules from CLAUDE.md, so the guard is spelled out in the injected system prompt. TestNarrationDirectiveCarriesRedactionClause asserts each mode names the key secret classes by string — a future edit that thins the directive without replacing the guard fails loudly.
QualityMax ReviewVerdict: COMMENT · Confidence: evidence-backed scan Files eligible: 2 · Files reviewed: 2 · Files with findings: 0 · Findings: 0 · Inline cards: 0 Priority findings
Review gates
Important files
Review lifecycleUse the inline cards to inspect evidence and suggested remediation. Re-run the QualityMax review after pushing a fix; unchanged cards are identified by their stable finding marker. Dismiss with a reason through the existing QualityMax/GitHub review feedback flow. 0 prior card(s) are stale/resolved on this head. Proof legend: VERIFIED independently judged patch · REPRODUCED verified finding · GROUNDED deterministic evidence · MODEL-ONLY model judgment. QualityMax project results are available in the configured project. Receipt · commit |
|
| Gate | Result |
|---|---|
| 🔍 AI diff review | ✅ Clean · gemini-3.1-flash-lite · completed · 2 eligible / 2 reviewed · gemini-3.1-flash-lite |
| 🔍 SAST | completed · 2 eligible / 2 reviewed · qwen3.7-plus |
| 🔍 Canonical PR review delivery | completed · 0 eligible / 0 reviewed · exact-head review #5327618075 and overview #5850218804 confirmed |
| 🧪 Repo Tests | ✅ 844/844 passed (go) |
Powered by QualityMax — AI-Powered Test Automation
Summary
Directive change
```go
// internal/agent/cc_agent.go — every mode now suffixes this shared clause
const redaction = " REDACTION (applies to any text you narrate — command, snippet, output, error): before printing, replace secrets with ``. Treat as secret: API keys, bearer/OAuth/session tokens, passwords, private keys, `Authorization:` and `Cookie:` header values, connection strings (postgres://user:pass@…, redis://…), `--token=`/`--api-key=`/`--password=`/`--secret=` flag values, `AWS_` / `GITHUB_TOKEN` / `ANTHROPIC_API_KEY` / other `_KEY` / `_TOKEN` / `_SECRET` env-var values, webhook signing secrets, and anything the surrounding text calls a key/token/secret. When in doubt, redact. Never quote raw `env` / `printenv` / `railway variables` output or `.env*` file contents in narration."
```
Why cover `off` too — that mode still says "only narrate on failure", and failure output is exactly where credentials tend to appear (401 responses, curl -v dumps, env listings from a broken test).
Guard test
```go
func TestNarrationDirectiveCarriesRedactionClause(t *testing.T) {
mustName := []string{
"REDACTION", "", "API keys", "bearer", "token",
"password", "private key", "Authorization:", "Cookie:",
"connection string", "--token=", "--api-key=", "env-var",
"webhook signing", "env", "printenv", ".env",
}
for _, mode := range []string{"off", "brief", "full", "bogus"} { ... }
}
```
Future edits that thin the directive without replacing the guard fail loudly.
Test plan
Release gate
Merging this unblocks `release: v1.37.0` — the release PR follows immediately after.