Skip to content

fix(agent): surface opencode model/turn failures and retry internal crashes - #181

Open
Desperado wants to merge 3 commits into
mainfrom
fix/opencode-failure-diagnostics
Open

fix(agent): surface opencode model/turn failures and retry internal crashes#181
Desperado wants to merge 3 commits into
mainfrom
fix/opencode-failure-diagnostics

Conversation

@Desperado

Copy link
Copy Markdown
Contributor

Problem

Three user-visible failures from one qmax-code session (opencode 1.17.18, Z.AI coding plan):

  1. Models silently missing from /orch. glm-5.3 was absent from the picker on the first /orch invocation, appeared minutes later. User: "check why we can't add glm-5.3".
  2. ✗ opencode exited with error: exit status 1 with no cause. Every prompt failed with zero diagnostics.
  3. G.includes is not a function crash. Typing a short reply ("3") killed the turn mid-stream; process died with nothing in opencode's log.

Root causes (all verified against live logs/repro)

  1. OpenCodeModels shells out to opencode models <provider> — a live network query (~7s warm, slower cold) — with a 15s timeout and silent nil on error (internal/agent/opencode_config.go). A transient failure made the provider's rows vanish with no warning.
  2. Reproduced with opencode run --model zai-coding-plan/glm-5v-turbo:
    {"type":"error",...,"error":{"name":"UnknownError","data":{"message":"Unexpected server error. Check server logs for details."}}}
    EXIT=1
    
    The real reason (当前订阅套餐暂未开放GLM-5V-Turbo权限 — model not in the subscription plan) exists only in opencode's log; the stream event is status-code-less, and handleOCError deliberately suppresses status-code-less events as benign 1.0.x noise on successful turns → on a failed turn nothing surfaces → generic exit error.
  3. opencode-internal JS TypeError while streaming the final response (upstream opentui: fatal: V.includes is not a function. (In 'V.includes(w)', 'V.includes' is undefined) anomalyco/opencode#28117, closed not-planned). Session log shows the step-1 stream starting and the process dying with no error logged. Not fixable from qmax-code — only survivable.

Fix

  • opencode_config.go — model query: 30s timeout, one retry, returns the error; timeout is reported as a timeout.
  • repl.go/orch prints per-provider failures: Model list for Z.AI unavailable (…) — re-run /orch to retry.
  • opencode_agent.go — failed turns explain themselves:
    • provider refusal in the stream → surface the event message + pointer to opencode's log; no retry (deterministic).
    • crash with no stream error → retry the turn once (retry re-evaluates session state, so no duplicate system-prompt injection); if both attempts die, the error carries the stderr tail.
    • error events are recorded even when suppressed from display — they are the only in-band clue for empty-result failures.

Tests

  • TestOpenCodeModelsRetriesTransientFailure — transient list failure recovers via retry
  • TestOpenCodeModelsReturnsErrorWhenAllAttemptsFail — silent-nil regression
  • TestOpenCodeParseRecordsErrorForDiagnostics — real captured refusal event is recorded
  • TestOpenCodeRunRetriesAfterInternalCrash — replays the exact G.includes crash, retry recovers, exactly 2 invocations
  • TestOpenCodeRunSurfacesProviderRefusalWithoutRetry — refusal surfaced, exactly 1 invocation
  • TestOpenCodeRunCrashTwiceIncludesStderrTail — both attempts die → stderr tail in error

Test plan

  • go test ./... — 18 packages, 0 failures
  • gofmt clean on touched files, go vet clean
  • Manual: re-run the failing session shape (pick an unentitled model → prompt; then pick glm-5.3 → short numeric reply)

…rashes

- opencode models query: 30s timeout, one retry, error returned — /orch now
  prints why a provider's rows are missing instead of silently dropping them
- failed turns: the provider-refusal event message is surfaced with a pointer
  to opencode's log; deterministic refusals are not retried
- internal opencode crashes (empty-result exit, no stream error): retry the
  turn once; if both attempts die the error carries the stderr tail
@sigilix

sigilix Bot commented Sep 2, 2026

Copy link
Copy Markdown

Sigilix Overview

Effort: 4/5 (large)

Quality gates

  • ✅ PR title follows convention
  • ✅ PR description is complete
  • ℹ️ PR is linked to an issue — No Closes #N / Closes SIG-N keyword found in PR body or commit messages.

Summary — latest push

Surfaces previously silent opencode model-query timeouts and provider-refusal errors, and adds a single retry for internal opencode crashes (e.g. JS TypeErrors) that kill the process mid-turn. The change also defensively redacts credential-shaped strings from stderr tails before they reach the TUI or logs, addressing a security concern raised during review.

Important files

File Score Notes Next step
internal/agent/opencode_agent.go 5/5 Adds retry logic for internal crashes, surfaces provider refusals, captures stderr tails, and redacts secrets before they hit the TUI/logs. Verify the regex replacement in stderrSecretPatterns correctly preserves the named capture group (e.g. ${1}) instead of literal $1 to ensure redaction doesn't mangle the output.
internal/agent/opencode_run_test.go 4/5 End-to-end tests for crash retries, provider refusals, and stderr redaction using shell stubs to simulate opencode behavior. Add a test case verifying that a partial result returned alongside a non-zero exit code is preserved and returned without triggering a retry.
internal/agent/opencode_config.go 4/5 Changes OpenCodeModels to return an error instead of nil on failure, adds a single retry, and increases the timeout to 30s. Ensure callers of OpenCodeModels correctly handle the new error return and surface it to the user instead of silently failing.
internal/agent/opencode_config_test.go 3/5 Tests for model list retry, total failure, and timeout paths using shell stubs and a mutable package-level timeout variable. Add a test ensuring the context cancellation error is correctly wrapped when openCodeModelsTimeout is hit during an active query.
internal/repl/repl.go 3/5 Updates /orch to display per-provider warnings when model queries fail, preventing silent model disappearance from the picker. Ensure the warning format string is consistent with the terminal's error styling and doesn't leak internal error details to the user.

Confidence: 3/5

The stderr redaction regex uses a literal $1 instead of the required ${1} for named capture groups, which will mangle the replacement string and potentially leak partial credentials.

  • The regex replacement in stderrSecretPatterns uses $1 instead of ${1} for the named capture group, which will not interpolate correctly and will leave the literal string $1=<redacted> in the output instead of the key name.
  • Verify that all callers of OpenCodeModels correctly handle the new (slice, error) return signature and surface the error to the user, as the function previously returned nil silently.
  • Confirm the stderrTailBuffer correctly captures the tail of stderr when cmd.Stderr is set to io.MultiWriter(term.Stderr(), tail) and doesn't race with the process exit.
  • Check that runAttempt correctly resets lastOCErrorSeen and lastOCErrorMsg between retries so a stale error from a first attempt doesn't incorrectly classify a second crash.

Suggested labels: bug security

Phase: Specialists
Commit: 6ad977d
Specialists: 0/4 completed
Dispatched: logic, security, performance, tests
Graph context: none; 0/2 files; 0 nodes

@sigilix sigilix Bot added the bug Something isn't working label Sep 2, 2026

@qualitymaxapp qualitymaxapp Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QualityMax Review — canonical overview updated; inline findings are attached to this review.

@qualitymaxapp

qualitymaxapp Bot commented Sep 2, 2026

Copy link
Copy Markdown

QualityMax Review

Verdict: COMMENT · Confidence: evidence-backed scan

Files eligible: 6 · Files reviewed: 6 · Files with findings: 0 · Findings: 0 · Inline cards: 0

Priority findings

priority location finding
No blocking findings

Review gates

gate status
AI diff review completed · eligible 6, reviewed 6 · LLM · served gemini-3.1-flash-lite
SAST completed · eligible 6, reviewed 6 · hybrid · served qwen3.7-plus
Overall review evidence clean
Inline evidence not needed

Important files

file risk note next step
No findings

Change diagram — Flow

flowchart TD
    Start[Run] --> Attempt1[runAttempt]
    Attempt1 --> Success1{Success?}
    Success1 -- Yes --> Return[Return Result]
    Success1 -- No --> ProviderErr{Provider Error?}
    ProviderErr -- Yes --> Fail[Return Error with Details]
    ProviderErr -- No --> Retry[Retry runAttempt]
    Retry --> Success2{Success?}
    Success2 -- Yes --> Return
    Success2 -- No --> FailTail[Return Error with Stderr Tail]
Loading

Review lifecycle

Use 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. @qmax Q&A is tracked separately.

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 6ad977dea811c55f638e25426e5143158fbbcafa · run 2026-09-02T13:51:00+00:00 · model served qwen3.7-plus, gemini-3.1-flash-lite · model requested qwen3.7-plus, gemini-3.1-flash-lite · model review recorded — 1261 model output tokens · model source repository ai_review_preferences.preferred_model · re-review 2 · proof counts {}

@@ -141,7 +153,17 @@ func validOpenCodeSessionID(id string) bool {
return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 LOGICGROUNDED Race condition between parseStream completion and error classification in Run

The Run method spawns parseStream as a goroutine and later reads a.lastRunError() to decide whether the failure was a provider refusal (deterministic, no retry) or an internal crash (retry once). The goroutine is not waited on; runAttempt returns after cmd.Wait(), but the stream reader may still be processing buffered data. This creates a race: lastOCErrorSeen/lastOCErrorMsg may still be unset when Run checks them, causing a provider refusal to be misclassified as a crash, leading to an unwanted retry and a misleading error message.

Detailed reasoning

The fix is to have runAttempt wait for the parseStream goroutine to finish before returning, e.g., via a channel, so that the error-event state is guaranteed to be final when the caller inspects it.

More Info
  • Threat model: When the opencode subprocess exits with an error event near the end of the stream, the main goroutine may reach the lastRunError check before the parseStream goroutine has processed the final bytes, leading to a false negative on the provider refusal case.
  • Specific code citations: go a.parseStream(stdout, term) in runAttempt (line ~260); a.lastRunError() check in Run (line ~187); a.lastOCErrorSeen, a.lastOCErrorMsg set inside parseStream.
  • Existing protections: The fields are protected by a mutex, but that only prevents concurrent access, not the ordering guarantee. The mutex does not synchronize with the goroutine's completion.
  • Proposed mitigation: In runAttempt, use a channel to signal the goroutine's completion, and wait for it before returning. This ensures that lastOCErrorSeen and lastOCErrorMsg are final when the caller reads them.
  • Alternative mitigations considered: A channel-based signal from parseStream is the simplest; a WaitGroup is also appropriate. Relying on a short sleep would be fragile.
  • Severity calibration: A provider refusal could be retried, violating the no-retry policy and possibly causing a confusing error message. The race is window-dependent but plausible in production. Score 4 reflects a likely bug under normal scheduling.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/opencode_agent.go
Line: 153

Comment:
**Race condition between parseStream completion and error classification in Run**

The `Run` method spawns `parseStream` as a goroutine and later reads `a.lastRunError()` to decide whether the failure was a provider refusal (deterministic, no retry) or an internal crash (retry once). The goroutine is not waited on; `runAttempt` returns after `cmd.Wait()`, but the stream reader may still be processing buffered data. This creates a race: `lastOCErrorSeen`/`lastOCErrorMsg` may still be unset when `Run` checks them, causing a provider refusal to be misclassified as a crash, leading to an unwanted retry and a misleading error message.

The fix is to have `runAttempt` wait for the `parseStream` goroutine to finish before returning, e.g., via a channel, so that the error-event state is guaranteed to be final when the caller inspects it.

Threat model:
When the opencode subprocess exits with an error event near the end of the stream, the main goroutine may reach the `lastRunError` check before the parseStream goroutine has processed the final bytes, leading to a false negative on the provider refusal case.

Specific code citations:
`go a.parseStream(stdout, term)` in runAttempt (line ~260); `a.lastRunError()` check in Run (line ~187); `a.lastOCErrorSeen`, `a.lastOCErrorMsg` set inside parseStream.

Existing protections:
The fields are protected by a mutex, but that only prevents concurrent access, not the ordering guarantee. The mutex does not synchronize with the goroutine's completion.

Proposed mitigation:
In `runAttempt`, use a channel to signal the goroutine's completion, and wait for it before returning. This ensures that `lastOCErrorSeen` and `lastOCErrorMsg` are final when the caller reads them.

Alternative mitigations considered:
A channel-based signal from parseStream is the simplest; a WaitGroup is also appropriate. Relying on a short sleep would be fragile.

Severity calibration:
A provider refusal could be retried, violating the no-retry policy and possibly causing a confusing error message. The race is window-dependent but plausible in production. Score 4 reflects a likely bug under normal scheduling.

How can I resolve this? If you propose a fix, please make it concise.


// stderrTailBuffer keeps the last bytes written to it. One instance per run
// attempt; not safe for concurrent use.
type stderrTailBuffer struct{ buf []byte }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 SECURITYGROUNDED Subprocess stderr tail buffer retains sensitive error output in memory

The new stderrTailBuffer retains up to 8KB of subprocess stderr output in memory, which could include provider error messages containing API keys, tokens, or other secrets. While this improves diagnostics, it also increases the attack surface for memory inspection attacks.

More Info
  • Threat model: An attacker with memory access (local privilege escalation, core dump) could extract retained stderr containing provider secrets or sensitive error messages.
  • Specific code citations: stderrTailBuffer struct with buf []byte field; Write method appends up to stderrTailLimit (8KB); stderrTailSnapshot returns trimmed tail in error messages.
  • Existing protections: No explicit sanitization or redaction of secrets from stderr before retention.
  • Proposed mitigation: Consider truncating or redacting stderr output before storing, or clearing the buffer after use. Alternatively, store only a hash or fingerprint for crash detection.
  • Alternative mitigations considered: 1) Do not retain stderr at all, rely on external logs. 2) Retain only non-sensitive metadata (exit code, timestamp). 3) Encrypt the buffer in memory (complex).
  • Severity calibration: Score 3 (hardening gap) because exploitation requires memory access, but the retained data could include credentials. The blast radius is limited to the process memory.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/opencode_agent.go
Line: 330

Comment:
**Subprocess stderr tail buffer retains sensitive error output in memory**

The new `stderrTailBuffer` retains up to 8KB of subprocess stderr output in memory, which could include provider error messages containing API keys, tokens, or other secrets. While this improves diagnostics, it also increases the attack surface for memory inspection attacks.

Threat model:
An attacker with memory access (local privilege escalation, core dump) could extract retained stderr containing provider secrets or sensitive error messages.

Specific code citations:
`stderrTailBuffer` struct with `buf []byte` field; `Write` method appends up to `stderrTailLimit` (8KB); `stderrTailSnapshot` returns trimmed tail in error messages.

Existing protections:
No explicit sanitization or redaction of secrets from stderr before retention.

Proposed mitigation:
Consider truncating or redacting stderr output before storing, or clearing the buffer after use. Alternatively, store only a hash or fingerprint for crash detection.

Alternative mitigations considered:
1) Do not retain stderr at all, rely on external logs. 2) Retain only non-sensitive metadata (exit code, timestamp). 3) Encrypt the buffer in memory (complex).

Severity calibration:
Score 3 (hardening gap) because exploitation requires memory access, but the retained data could include credentials. The blast radius is limited to the process memory.

How can I resolve this? If you propose a fix, please make it concise.

// text (entitlement message, provider status) lands here.
func openCodeLogPath() string {
if base := os.Getenv("XDG_DATA_HOME"); base != "" {
return filepath.Join(base, "opencode", "log", "opencode.log")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 SECURITYGROUNDED Log file path exposure in error messages could aid path traversal

The openCodeLogPath function returns opencode's log file path, which is included in error messages. While not a secret, revealing the exact path could assist an attacker in constructing path traversal attacks if the log file is accessible.

Example:

Error message: 'opencode turn failed: Unexpected server error (provider details: /home/user/.local/share/opencode/log/opencode.log)'

Suggested fix:

return 'opencode turn failed: Unexpected server error (see opencode log for details)'
More Info
  • Threat model: An attacker who can read error messages (e.g., via logs) learns the exact path to opencode's log file, which could be targeted for reading or writing if file permissions are weak.
  • Specific code citations: openCodeLogPath returns ~/.local/share/opencode/log/opencode.log or $XDG_DATA_HOME/opencode/log/opencode.log; included in error strings via opencode.log pointer.
  • Existing protections: The log file is presumably user-owned and not world-readable, but path disclosure still reduces attacker effort.
  • Proposed mitigation: Omit the full path from user-facing errors; use a generic message like 'see opencode log for details'.
  • Alternative mitigations considered: Keep the path for debugging but only log it at debug level, not in user-facing errors.
  • Severity calibration: Score 2 (small hardening improvement) because the path is predictable anyway (~/.local/share/opencode/log/opencode.log), but explicit disclosure slightly increases risk.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/opencode_agent.go
Line: 350

Comment:
**Log file path exposure in error messages could aid path traversal**

The `openCodeLogPath` function returns opencode's log file path, which is included in error messages. While not a secret, revealing the exact path could assist an attacker in constructing path traversal attacks if the log file is accessible.

Example:
Error message: 'opencode turn failed: Unexpected server error (provider details: /home/user/.local/share/opencode/log/opencode.log)'

Threat model:
An attacker who can read error messages (e.g., via logs) learns the exact path to opencode's log file, which could be targeted for reading or writing if file permissions are weak.

Specific code citations:
`openCodeLogPath` returns `~/.local/share/opencode/log/opencode.log` or `$XDG_DATA_HOME/opencode/log/opencode.log`; included in error strings via `opencode.log` pointer.

Existing protections:
The log file is presumably user-owned and not world-readable, but path disclosure still reduces attacker effort.

Proposed mitigation:
Omit the full path from user-facing errors; use a generic message like 'see opencode log for details'.

Alternative mitigations considered:
Keep the path for debugging but only log it at debug level, not in user-facing errors.

Severity calibration:
Score 2 (small hardening improvement) because the path is predictable anyway (~/.local/share/opencode/log/opencode.log), but explicit disclosure slightly increases risk.

Suggested fix shape:
return 'opencode turn failed: Unexpected server error (see opencode log for details)'

How can I resolve this? If you propose a fix, please make it concise.

@qualitymaxapp

qualitymaxapp Bot commented Sep 2, 2026

Copy link
Copy Markdown

⚠️ QualityMax Pipeline

Gate Result
🔍 AI diff review ✅ Clean · gemini-3.1-flash-lite · completed · 6 eligible / 6 reviewed · gemini-3.1-flash-lite
🔍 SAST completed · 6 eligible / 6 reviewed · qwen3.7-plus
🔍 Canonical PR review delivery completed · 0 eligible / 0 reviewed · exact-head review #5090516911 and overview #5509156304 confirmed
🧪 Repo Tests ✅ 722/722 passed (go)

Powered by QualityMax — AI-Powered Test Automation

@Desperado

Copy link
Copy Markdown
Contributor Author

@Qmax please review it

@qualitymaxapp

qualitymaxapp Bot commented Sep 2, 2026

Copy link
Copy Markdown

👀 Review request accepted; a fresh exact-head Gate Alpha review has started. View it in PR checks. GitHub App: @qualitymaxapp (@qmax remains supported shorthand). Head: 490f922c15e2.

Review follow-ups on #181:
- stderrTailSnapshot now redacts credential-shaped output (api keys, bearer
  tokens, JWTs, sk-/gsk-/ghp- style prefixes) before the tail lands in a
  returned error — defense in depth against a crash dumping auth state
- openCodeModelsTimeout is an injectable variable and the timeout path has a
  fast test (sleeping stub + 100ms deadline) asserting the 'timed out' report
@Desperado

Copy link
Copy Markdown
Contributor Author

Review triage (Sigilix findings + QualityMax clean)

QualityMax gates: clean, 0 findings — no action.

Sigilix (model-only, 3 findings) triaged:

Fixed in d7f93a4:

  • stderr tail may expose secrets — agreed as defense-in-depth. stderrTailSnapshot now redacts credential-shaped output (api keys, bearer tokens, JWTs, sk-/gsk-/ghp- prefixes) before the tail reaches a returned error; TestRedactStderrTail pins it while asserting diagnostic text survives.
  • timeout path untested (config_test note) — openCodeModelsTimeout is now an injectable variable; TestOpenCodeModelsReportsTimeout drives a sleeping stub with a 100ms deadline and asserts the timed out report.

Dismissed:

  • Retry may duplicate the system prompt when sessionID is captured mid-stream — @sigilix dismiss already-coveredrunAttempt re-evaluates a.sessionID per attempt precisely so a retry sends the bare message via --session; the pre-crash attempt's message is what's in the persisted session, not a second system prompt (documented at the top of runAttempt).
  • 30-min context covering both attempts — @sigilix dismiss not-a-bug — intentional: one shared budget keeps the worst case at 30 min instead of doubling to 60; a slow first attempt leaving little for the retry is the correct trade for a CLI turn.
  • Retry jitter for thundering herd — @sigilix dismiss too-minor — single-user CLI, at most one immediate retry; there is no herd to throttle.

Full suite re-run on d7f93a4: 18 packages, 0 failures; gofmt/vet clean on touched files.

@qualitymaxapp qualitymaxapp Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QualityMax Review — canonical overview updated; inline findings are attached to this review.

Comment thread internal/agent/opencode_agent.go Outdated
Comment on lines +333 to +339
var stderrSecretPatterns = []struct {
re *regexp.Regexp
repl string
}{
{regexp.MustCompile(`(?i)\b(api[_-]?key|token|secret|password|authorization)\b\s*[=:]\s*(bearer\s+)?\S+`), "${1}=<redacted>"},
{regexp.MustCompile(`(?i)\bbearer\s+\S+`), "bearer <redacted>"},
{regexp.MustCompile(`\b(sk|gsk|rk|ghp|gho|ghu|ghs|xox[bpars]|AIza)[A-Za-z0-9_\-]{16,}\b`), "<redacted>"},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 SECURITYGROUNDED Secret redaction regex patterns may match non-secret values, causing false positives and potential data loss.

The stderrSecretPatterns regexes are overly broad and could match non-secret strings like random base64-encoded data, commit hashes, or UUIDs, leading to unnecessary redaction in error messages. This could obscure diagnostic information. The patterns also may miss some credential formats (e.g., sk_live_ for Stripe, AKIA for AWS).

Example:

Stderr contains: `Commit: abcdef1234567890abcdef1234567890abcdef12` (a git SHA). The regex matches `abcdef1234567890abcdef1234567890abcdef12` (40 chars, starts with hex) and redacts it, losing the commit hash.

Suggested fix:

Add a pattern to exclude git commit SHAs (40 hex chars). Tighten the generic token pattern to require a known prefix followed by a more specific pattern (e.g., `sk_live_` for Stripe).

Why this wasn't caught: The existing TestRedactStderrTail does not test false positives for non-secret values like git SHAs or UUIDs.

More Info
  • Threat model: If the redaction regex incorrectly redacts non-secret stderr output, debugging becomes harder. If it misses a credential format, a secret could leak into logs/TUI.
  • Specific code citations: Lines 333-342 define stderrSecretPatterns. The regex \b(sk|gsk|rk|ghp|gho|ghu|ghs|xox[bpars]|AIza)[A-Za-z0-9_\-]{16,}\b matches many 16+ character alphanumeric strings starting with those prefixes, which could include non-secrets.
  • Existing protections: The redaction runs only on stderr tail included in error messages, not on all logs. The tail is limited to 500 chars.
  • Proposed mitigation: Tighten regex patterns to be more specific to known credential formats, and consider using a allowlist of known safe patterns (like commit SHAs) to avoid false positives. Add tests for edge cases.
  • Alternative mitigations considered: Remove redaction entirely and rely on opencode not to leak secrets; this is risky. Use a library for secret detection; adds dependency.
  • Severity calibration: Score 3 because it's a hardening gap that could affect debuggability and might not catch all real secrets, but the blast radius is limited to error messages from a crashing subprocess.
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/opencode_agent.go
Line: 333-339

Comment:
**Secret redaction regex patterns may match non-secret values, causing false positives and potential data loss.**

The `stderrSecretPatterns` regexes are overly broad and could match non-secret strings like random base64-encoded data, commit hashes, or UUIDs, leading to unnecessary redaction in error messages. This could obscure diagnostic information. The patterns also may miss some credential formats (e.g., `sk_live_` for Stripe, `AKIA` for AWS).

Example:
Stderr contains: `Commit: abcdef1234567890abcdef1234567890abcdef12` (a git SHA). The regex matches `abcdef1234567890abcdef1234567890abcdef12` (40 chars, starts with hex) and redacts it, losing the commit hash.

Threat model:
If the redaction regex incorrectly redacts non-secret stderr output, debugging becomes harder. If it misses a credential format, a secret could leak into logs/TUI.

Specific code citations:
Lines 333-342 define `stderrSecretPatterns`. The regex `\b(sk|gsk|rk|ghp|gho|ghu|ghs|xox[bpars]|AIza)[A-Za-z0-9_\-]{16,}\b` matches many 16+ character alphanumeric strings starting with those prefixes, which could include non-secrets.

Existing protections:
The redaction runs only on stderr tail included in error messages, not on all logs. The tail is limited to 500 chars.

Proposed mitigation:
Tighten regex patterns to be more specific to known credential formats, and consider using a allowlist of known safe patterns (like commit SHAs) to avoid false positives. Add tests for edge cases.

Alternative mitigations considered:
Remove redaction entirely and rely on opencode not to leak secrets; this is risky. Use a library for secret detection; adds dependency.

Severity calibration:
Score 3 because it's a hardening gap that could affect debuggability and might not catch all real secrets, but the blast radius is limited to error messages from a crashing subprocess.

Suggested fix shape:
Add a pattern to exclude git commit SHAs (40 hex chars). Tighten the generic token pattern to require a known prefix followed by a more specific pattern (e.g., `sk_live_` for Stripe).

Why this wasn't caught:
The existing TestRedactStderrTail does not test false positives for non-secret values like git SHAs or UUIDs.

How can I resolve this? If you propose a fix, please make it concise.

cmd := exec.CommandContext(ctx, a.openCodeBin, args...)
cmd.Stdin = strings.NewReader("")
cmd.Stderr = term.Stderr()
tail := &stderrTailBuffer{}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 SECURITYGROUNDED Stderr tail redaction may leak secrets if opencode prints them in unexpected formats.

The redactStderrTail function applies a fixed set of regex patterns; if opencode outputs a credential in a format not covered (e.g., X-API-Key: secret, token=...), it will not be redacted and could appear in the error message shown to the user or logged.

More Info
  • Threat model: A crashing opencode subprocess might dump environment variables or debug info containing API keys in an unrecognized format, leading to secret exposure in the TUI or logs.
  • Specific code citations: Lines 333-342 define the patterns; line 271 calls redactStderrTail. The patterns may not cover all possible secret formats.
  • Existing protections: The tail is limited to 500 chars and only appears in error messages from a failed turn. opencode itself may not print secrets.
  • Proposed mitigation: Add more comprehensive patterns, especially for common env var formats (KEY=value). Consider redacting any line containing key, token, secret, password followed by an equals sign and a high-entropy string.
  • Alternative mitigations considered: Discard stderr entirely for crash cases; but then diagnostic info is lost.
  • Severity calibration: Score 2 because it's a low-probability hardening gap; opencode is unlikely to print secrets, but if it does, the impact is moderate (secret in error message).
Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/agent/opencode_agent.go
Line: 276

Comment:
**Stderr tail redaction may leak secrets if opencode prints them in unexpected formats.**

The `redactStderrTail` function applies a fixed set of regex patterns; if opencode outputs a credential in a format not covered (e.g., `X-API-Key: secret`, `token=...`), it will not be redacted and could appear in the error message shown to the user or logged.

Threat model:
A crashing opencode subprocess might dump environment variables or debug info containing API keys in an unrecognized format, leading to secret exposure in the TUI or logs.

Specific code citations:
Lines 333-342 define the patterns; line 271 calls `redactStderrTail`. The patterns may not cover all possible secret formats.

Existing protections:
The tail is limited to 500 chars and only appears in error messages from a failed turn. opencode itself may not print secrets.

Proposed mitigation:
Add more comprehensive patterns, especially for common env var formats (`KEY=value`). Consider redacting any line containing `key`, `token`, `secret`, `password` followed by an equals sign and a high-entropy string.

Alternative mitigations considered:
Discard stderr entirely for crash cases; but then diagnostic info is lost.

Severity calibration:
Score 2 because it's a low-probability hardening gap; opencode is unlikely to print secrets, but if it does, the impact is moderate (secret in error message).

How can I resolve this? If you propose a fix, please make it concise.

Review follow-up round 2 on #181:
- add AKIA/ASIA prefixes to the credential patterns (the one real gap)
- TestRedactStderrTailEdgeCases pins both directions: hex git SHAs survive
  redaction (a SHA cannot start with a credential prefix), while X-API-Key:,
  token=…, and AWS access-key ids are redacted
@Desperado

Copy link
Copy Markdown
Contributor Author

Sigilix round-2 triage (6ad977d)

P2 — redaction false-positives on git SHAs@sigilix dismiss bad-anchor. The cited example cannot match: the pattern requires the string to start with one of sk|gsk|rk|ghp|gho|ghu|ghs|xox*|AIza|AKIA|ASIA at a word boundary, and a hex git SHA (0-9a-f) never begins with any of those prefixes. Pinned by TestRedactStderrTailEdgeCases, which asserts the 40-char SHA survives verbatim. The one legitimate gap in the finding — missing AWS formats — is fixed in 6ad977d (AKIA/ASIA added, pinned by the same test).

P3 — secrets in unexpected formats (X-API-Key:, token=…)@sigilix dismiss already-covered. Both cited shapes are redacted by the first pattern today ((?i)\b(api[_-]?key|token|secret|password|authorization)\b\s*[=:]\s*(bearer\s+)?\S+); TestRedactStderrTailEdgeCases asserts both, plus an AWS access-key id. "May not cover all formats" is unfalsifiable for any regex set — coverage is pinned to the formats that exist in this threat model (opencode subprocess stderr), and the tail is bounded to 500 chars in error messages only.

Suite on 6ad977d: 18 packages, 0 failures.

@qualitymaxapp qualitymaxapp Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QualityMax Review — canonical overview updated; inline findings are attached to this review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working qualitymax:reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant