diff --git a/CHANGELOG.md b/CHANGELOG.md index bf192f6a9e..a8fece1901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1326,6 +1326,8 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Redacted the exact agent-mention GitHub credential from bounded CLI failure + diagnostics while retaining exit status and actionable non-secret stderr. - Prefer the job-scoped `github.token` when the central OpenCode dispatch publishes a commit status back to the same `.github` repository. The job's declared `statuses: write` permission now reaches the endpoint instead of an diff --git a/docs/doctoring/agent-mention-concurrency-isolation.md b/docs/doctoring/agent-mention-concurrency-isolation.md index 1beacd4f8a..3ec312e000 100644 --- a/docs/doctoring/agent-mention-concurrency-isolation.md +++ b/docs/doctoring/agent-mention-concurrency-isolation.md @@ -67,6 +67,16 @@ whose pull-list request completes first can expose its recent PR comments before a slower sibling. Dispatch remains sequential through the existing ledger and exact-head validation boundaries. +### Credential-safe transport diagnostics + +The router passes its GitHub credential only through `GH_TOKEN`, never through +the command arguments. GitHub CLI failures can still echo that credential in +standard error, so `GitHubClient.request` removes the client's exact token +before applying the existing 2,000-character diagnostic bound. Timeout errors +retain their fixed message and do not add command arguments. This follows the +OWASP logging guidance to remove access tokens while preserving actionable +operational evidence; it does not mask operational PII. + ## Preserved boundaries - No model provider, reviewer identity, repository allowlist, token name, credential scope, or branch-protection rule changes. @@ -75,6 +85,8 @@ ledger and exact-head validation boundaries. - Only trusted non-bot `OWNER`, `MEMBER`, or `COLLABORATOR` comments on open pull requests are eligible. - Pull request number, exact head and base SHAs, base branch, source comment, requested agent, and requesting actor remain bound to the invocation key. - Mention routing remains unable to approve, merge, update branches, publish, or release. +- GitHub CLI diagnostics retain exit status and sanitized error text but never + the active router credential. ## Operational acceptance @@ -87,6 +99,8 @@ After protected integration: 5. distinguish downstream provider or review failure from routing failure rather than treating every missing verdict as the same incident. 6. verify the slow-first/fast-later repository regression remains green so a delayed repository cannot starve a completed sibling's comment inventory. +7. force a GitHub CLI failure that echoes the synthetic client credential and + require `[REDACTED]` in the raised diagnostic with no credential value. A receipt proves routing and durable claim processing. It is not an approval and never substitutes for exact-head checks or branch protection. @@ -110,4 +124,6 @@ GitHub. (n.d.). *REST API endpoints for repositories: Create a repository dispat GitHub. (n.d.). *Store and share data with workflow artifacts*. GitHub Docs. Retrieved August 19, 2026, from https://docs.github.com/en/actions/tutorials/store-and-share-data +OWASP Foundation. (n.d.). *Logging cheat sheet*. OWASP Cheat Sheet Series. Retrieved August 22, 2026, from https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html + Python Software Foundation. (n.d.). *concurrent.futures — Launching parallel tasks*. Python documentation. Retrieved August 20, 2026, from https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.as_completed diff --git a/scripts/ci/agent_mention_router.py b/scripts/ci/agent_mention_router.py index 46332cfc2f..dbf1e62911 100755 --- a/scripts/ci/agent_mention_router.py +++ b/scripts/ci/agent_mention_router.py @@ -200,6 +200,7 @@ def request( diagnostic = " ".join(str(getattr(completed, "stderr", "") or "").split()) if not diagnostic: diagnostic = "no stderr output" + diagnostic = diagnostic.replace(self._token, "[REDACTED]") retryable = RATE_LIMIT_DIAGNOSTIC_RE.search(diagnostic) is not None if retryable and attempt < GITHUB_API_MAX_ATTEMPTS: time.sleep(attempt * 5) diff --git a/tests/test_agent_mention_router.py b/tests/test_agent_mention_router.py index dde1ef4669..38e4d73af9 100644 --- a/tests/test_agent_mention_router.py +++ b/tests/test_agent_mention_router.py @@ -687,7 +687,7 @@ def test_dispatch_noema_only_covers_non_opencode_path() -> None: def test_github_client_validates_token_and_decodes_json(monkeypatch) -> None: - """The token-bound client never places credentials in command arguments.""" + """The token-bound client keeps credentials out of commands and errors.""" module = load_module() with pytest.raises(ValueError, match="token"): @@ -712,6 +712,19 @@ def fake_run(command, **kwargs): lambda *args, **kwargs: SimpleNamespace(stdout=" "), ) assert client.request(["repos/x/y"]) is None + monkeypatch.setattr( + module.subprocess, + "run", + lambda *args, **kwargs: SimpleNamespace( + returncode=1, + stderr="authentication failed for secret-token", + stdout="", + ), + ) + with pytest.raises(RuntimeError) as exc_info: + client.request(["repos/x/y"]) + assert "secret-token" not in str(exc_info.value) + assert "[REDACTED]" in str(exc_info.value) def test_load_event_and_main_paths(tmp_path: Path, monkeypatch, capsys) -> None: