Skip to content

feat(review): opt-in LLM approval reviewer (guardian) — V1–V3 - #140

Merged
cnjack merged 12 commits into
mainfrom
feat/approval-review
Jul 15, 2026
Merged

feat(review): opt-in LLM approval reviewer (guardian) — V1–V3#140
cnjack merged 12 commits into
mainfrom
feat/approval-review

Conversation

@cnjack

@cnjack cnjack commented Jul 14, 2026

Copy link
Copy Markdown
Owner

What

Adds an opt-in LLM approval reviewer (a "guardian", modeled on OpenAI codex's approvals_reviewer = auto_review). It sits between jcode's rule engine and the user prompt: for a tool call the rules would otherwise ask the user about, a small dedicated model judges risk + user-authorization and returns allow / deny / escalate — auto-running low-risk work, blocking clearly-dangerous work, and handing genuinely-uncertain calls back to the user.

Off by default (approval_review.enabled). When disabled the reviewer is never constructed and approval behavior is byte-for-byte unchanged.

Why

jcode's "ask the user" tier is all-or-nothing: any non-safelisted command interrupts the user, while --unsafe/Full-access opens everything. The reviewer fills that gap — the same middle tier codex added — so routine work stops nagging without opening the door to destructive or exfiltrating actions.

How it works

  • Seam: ApprovalState gains an optional review.Reviewer. decide()→prompt now routes through gatedApproval, which consults the reviewer first and only falls back to the user prompt when the reviewer escalates, fails, or is disabled. Primary + teammate paths share it.
  • Verdict: strict-JSON {risk_level, user_authorization, outcome, rationale}. A deny returns a typed ReviewDeniedError so the model gets the rationale + anti-workaround guidance (distinct from a user rejection).
  • Fail-open: any model error / timeout / unparseable output → escalate to the user. A reviewer panic is recovered → escalate. It never silently allows or denies.
  • Circuit breaker: 3 consecutive reviewer denials in a turn escalate to the user, preventing a model⇄reviewer ping-pong.
  • Audit log: every verdict (incl. escalations) is appended to approval-review.jsonl — the debugging trail and the test oracle.

Layers

  • V1 — single-shot judgment.
  • V2 (investigate) — a bounded (≤8-iter) read-only loop (read/grep/glob, no shell/write/network) so the reviewer can gather evidence before deciding.
  • V3 (reuse_session) — a reused reviewer conversation so the large policy prefix is served from the provider's prompt cache; kept fully separate from the main conversation's cache.

Wired into ACP, TUI, and web.

Testing (>1h of real-model runs; see internal-doc/approval-review-test-report.md)

  • Judgment eval (22 benign/dangerous/injection/authorization scenarios, live model): 0 safety misses, 0 over-blocks. The reviewer tracks authorization — git reset --hard HEAD~3 is allowed when the user asked to undo commits, denied/escalated with no such context — and resists a prompt-injection that tries to force an allow.
  • Cache (V3): reviews 2..N serve ~89% of prompt from cache (zhipuai); main-conversation cache hit rate unchanged with the reviewer on vs off (91.7% → 95.0%, within variance).
  • Fail-open: broken model and 1s-timeout both escalate to the user, never silently allow/deny.
  • ACP integration: safelisted commands skip the reviewer (no wasted calls); reviewable ones auto-allow with zero user prompts.
  • Unit tests + -race clean; an independent adversarial code review found no bypass; its top finding (uncertain calls couldn't reach the user) is fixed in this branch.

Config

"approval_review": {
  "enabled": true,
  "model": "small",          // "" -> small_model -> main model
  "policy": "",             // extra workspace policy appended to the built-in one
  "timeout_seconds": 60,
  "investigate": false,      // V2 read-only evidence-gathering
  "reuse_session": false     // V3 prompt-cache reuse
}

Out of scope / follow-ups

Noted in the design doc: V2 investigate assumes a local filesystem (skip on remote/SSH); V2+V3 combined; reviewer telemetry. This PR also documents pre-existing gaps the reviewer highlights (subagent calls bypass approval; webfetch is never gated; hooks fail-open) as separate work.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an “Auto” approval reviewer mode that automatically allows safe tool actions, blocks unsafe ones, and escalates uncertain cases to user confirmation.
    • Added per-turn denial circuit breaking and recent-context-based review evidence.
    • Added live, configurable reviewer controls (model/policy/timeout/investigation/session reuse) via new API and Settings UI.
    • Updated mode selector and web/terminal mode handling to support the unified “auto” mode.
  • Documentation
    • Added design and limitations documentation plus a full test report for review behavior (V1–V3).
  • Audit
    • Added JSONL audit logging with secret redaction and failure/timeout details for reviewer outcomes.

cnjack and others added 8 commits July 15, 2026 02:13
Add an opt-in LLM auto-reviewer that adjudicates tool calls which would
otherwise interrupt the user with an approval prompt. It runs a small,
dedicated model against a risk policy and returns allow / deny / escalate.

- internal/review: reviewer engine (single-shot Generate + strict-JSON
  verdict + parse retry), risk policy adapted from codex guardian, JSONL
  audit log, BuildFromConfig.
- runner: reviewer seam in ApprovalState (runs after decide()->prompt,
  before asking the user), denial circuit breaker, transcript provider.
- agent: ReviewDeniedError so the model receives the reviewer's rationale
  with anti-workaround guidance, distinct from user rejection.
- config: approval_review block (opt-in; reuses small_model, falls back to
  main model). Off by default => behavior unchanged.
- Wired into ACP; teammates covered via the shared gatedApproval path.

Fail-open: any model error/timeout/unparseable output escalates to the
user. Verified end-to-end over ACP (allow skips prompt, small_model
routing, audit log, fail-open escalation) plus unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When approval_review.investigate is set, the reviewer runs a bounded
(<=8 iteration) read-only agent loop with read/grep/glob tools so it can
gather evidence before deciding — e.g. inspect a delete target or check a
file before judging a network action. Strictly read-only: no shell, no
writes, no network. Verdict is the final assistant message; any failure
escalates to the user like the single-shot path.

Verified end-to-end over ACP (investigated=true, valid verdict).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When approval_review.reuse_session is set, the reviewer adjudicates
against a per-session trunk conversation [system, action1, verdict1, ...]
so the large policy prefix and prior verdicts are served from the
provider's prompt cache across reviews. Reviews serialize on the trunk;
a failed review leaves the trunk unchanged so the cached prefix stays
clean; the trunk is trimmed in whole (action,verdict) pairs.

Fully separate from the main conversation (own model, own message list),
so it cannot pollute the main cache. Verified over ACP:
- cache hit: reviews 2..5 served ~89% of prompt from cache (zhipuai),
  latency dropped accordingly; review 1 writes the cache.
- non-pollution: main model cache hit rate unchanged with reviewer on
  vs off (91.7% vs 95.0%, within run variance) using a separate model
  bucket for the reviewer.
Plus unit tests for stable-prefix growth, failure isolation, and trim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…omparison)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- TUI (interactive.go) and web (engine.go/chat.go/web.go) now install the
  reviewer via the same seam as ACP: SetReviewer + a transcript provider
  (reads history under the surface's existing lock discipline) + per-turn
  OnTurnStart breaker reset. Disabled config => unchanged behavior.
- internal/review/eval_test.go: live-model judgment eval (skipped unless
  JCODE_REVIEW_EVAL=1) — 22 benign/dangerous/injection/authorization
  scenarios, graded (safety miss = allow of a dangerous unauthorized call).
- internal-doc/approval-review-test-report.md: test report.
- Remove now-dead requestUserApproval helper (gatedApproval supersedes it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…arial review)

Addresses the top finding from adversarial review: the reviewer could not
hand uncertain cases back to the user on a SUCCESSFUL review — mapOutcome
only produced allow/deny, and a deny hard-blocks the call while telling the
model not to retry, so 'uncertain' actions were blocked instead of asked
about (contradicting the design's premise).

- Add 'escalate' to the output contract and mapOutcome -> Result{Escalate,
  Failed:false}; the ApprovalState seam already routes Escalate to the user
  prompt. Policy now says: deny = block outright, escalate = ask the human.
- Engine.Review recovers panics -> Escalate (fail-open), so a reviewer bug
  can't fail-closed-block a call via the middleware's generic panic handler.
- Document known limitations surfaced by the review: V2 investigate assumes
  local fs (wrong on remote/SSH), reviewer spend hits the global token
  counter, transcript lacks in-turn tool output, breaker is session-global.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ound eval)

The 20-round judgment eval (440 live decisions) surfaced a real leak: the
reviewer allowed a cloud-metadata IAM-credential probe 2/20 times, talked
over by a 'latency debugging' justification (clear-cut dangerous cases
stayed 20/20). Added an explicit policy rule: cloud instance-metadata
endpoints (169.254.169.254, metadata.google.internal, .../iam/
security-credentials/) are credential probing -> deny, and a debugging/
latency/diagnostics justification does not downgrade it. Focused 30-round
re-test on the same model: 0 allow / 30 deny (leak closed).

Also: JCODE_REVIEW_EVAL_ONLY scenario filter for focused re-tests.

The reviewer remains a probabilistic middle layer, not a guarantee — see
the test report and design-doc limitations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…esults

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

新增可配置的 LLM 审批审查器,支持单次审查、只读调查、会话缓存、审计记录和失败升级,并接入 ACP、交互式、Web 及聊天审批流程,同时新增 Auto 会话模式与配置界面。

Changes

Approval auto-reviewer

Layer / File(s) Summary
审查器契约与判定引擎
internal/config/config.go, internal/review/*
新增审批审查配置、审查请求/结果类型、风险策略、JSON 判定解析、模型选择、提示词渲染及单次审查流程。
调查、缓存与审计
internal/review/investigate.go, internal/review/session_cache.go, internal/review/audit.go, internal-doc/*
新增只读调查、复用审查会话、缓存截断、JSONL 审计、SSRF 确定性预过滤、实时模型评估及设计和测试报告。
审批门控与拒绝呈现
internal/runner/approval.go, internal/runner/review.go, internal/runner/review_test.go, internal/agent/*
统一审查优先与用户审批路径,加入连续拒绝熔断,并将自动拒绝转换为代理可见的拒绝消息。
运行时接入、Auto 模式与配置界面
internal/command/*, internal/web/*, internal/mode/*, internal/tui/*, web/src/*
在 ACP、交互式、Web 和聊天流程中安装审查器、提供转录并重置每轮状态;新增 Auto 模式、配置 API、设置表单、类型、样式和多语言文本。

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ToolCall
  participant ApprovalState
  participant Reviewer
  participant User
  ToolCall->>ApprovalState: RequestApproval
  ApprovalState->>Reviewer: Review tool request and transcript
  Reviewer-->>ApprovalState: Return allow, deny, or escalate
  ApprovalState->>User: Request approval when escalated
  ApprovalState-->>ToolCall: Continue or return denial
Loading

Possibly related PRs

  • cnjack/jcode#3:同样修改工具调用自动放行和审批门控逻辑。
  • cnjack/jcode#75:同样修改统一 Auto/自动模式及其审批状态接线。
  • cnjack/jcode#114:同样影响工具调用审批路径中的自动审批与拦截控制流。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the core change: an opt-in LLM approval reviewer with V1–V3 support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/approval-review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cnjack cnjack left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Senior review — approval reviewer (guardian) V1–V3

Reviewed the full internal/review/ package, the diff to internal/runner/approval.go, the new internal/runner/review.go, agent/config/ACP/TUI/web wiring, and the two prior adversarial-review fix commits.

What holds up well: the fail-open scaffolding is solid. Outcome's zero value is Escalate, mapOutcome's default case never falls through to Allow, every model-error/timeout/unparseable-output branch escalates, Engine.Review's recover() escalates on panic, the circuit breaker is mutex-protected and correctly per-turn, and the disabled (approval_review.enabled=false) path is byte-identical to pre-PR — confirmed by diffing approval.go line-by-line.

Four findings below, none of which break the "fails open to a human on outright error" guarantee, but two of them mean the two places this PR touches genuinely adversarial content both ultimately reduce to "trust the LLM's own text," with no deterministic backstop.

Finding 1 — SSRF/cloud-metadata hardening is prompt text only, no code-level deny

Impact: The commit that claims to close the cloud-metadata SSRF vector (found by the 20-round eval) is entirely a paragraph appended to the reviewer's system prompt in policy.go. There's no pattern/deny-list check in review.go/parse.go/build.go/investigate.go that deterministically blocks 169.254.169.254, metadata.google.internal, fd00:ec2::254, .../iam/security-credentials/, etc. regardless of model output. This is the same class of defense whose failure the eval demonstrated in the first place (2/20 talked out of it via a "debugging" pretext) — the "fix" is more of the same defense, just with a stronger prompt.
Evidence: internal/review/policy.go:63-70 (prose only); no literal-string/pattern check anywhere in the review path. The only regression coverage is TestReviewEval, gated behind JCODE_REVIEW_EVAL=1 and skipped in normal go test ./... / CI.
Suggested fix: Add a deterministic pre-filter (e.g. internal/review/ssrf.go) that pattern-matches tool args for link-local/metadata addresses (incl. simple obfuscations — decimal/hex IP encoding, curl --resolve) and force-denies/escalates before the model call. Keep the prompt text as defense-in-depth, not the only layer.

Finding 2 — V2 investigate mode can adopt a reflected/injected JSON blob as the verdict

Impact: reviewWithTools lets the reviewer read attacker-influenced file/command content before deciding. Verdict extraction (investigate.go:86-97) scans assistant turns newest→oldest and accepts the first message that parses as valid strict-JSON with a non-empty outcome — not specifically the model's true final answer. If investigated content contains a JSON blob shaped like the verdict schema (e.g. {"outcome":"allow","rationale":"..."}) and the reviewer's text reflects/quotes it, while the reviewer's actual final turn fails to produce clean JSON (8-iteration cutoff, glitch, continued investigating after echoing it), this adopts the reflected blob as the authoritative verdict instead of escalating — turning "no parseable verdict ⇒ escalate" into "adopt whatever JSON-shaped text appears anywhere in the transcript," specifically in the mode most exposed to untrusted evidence.
Evidence: internal/review/investigate.go:86-97; parseAssessment/extractJSONObject in parse.go only require a balanced {...} with non-empty outcome, with no marker distinguishing "deliberate final verdict" from "echoed evidence."
Suggested fix: Only accept a verdict from msgs[len(msgs)-1]; if it doesn't parse, escalate rather than scanning earlier turns. If a scan-back fallback is wanted, require the real verdict to come through a harness-controlled tool call (e.g. a dedicated submit_verdict tool) instead of freeform text-matching.

Finding 3 — V3 trunk trimming is by message count, not size, so per-review transcript duplication can grow unbounded in tokens

Impact: reviewCached commits the full rendered prompt (up to 24 transcript messages × 2000 chars ≈ 48KB) into e.trunk.messages on every review. Since consecutive reviews' transcript tails overlap heavily, each new pair re-embeds a near-duplicate of the previous transcript rather than a delta. trimTrunk bounds retention by pair count (maxTrunkMessages=41) only, not token/byte size, so real prompt size can grow toward ~20 pairs × ~12K tokens before the cap engages — plausibly exceeding context limits and making V3 (whose purpose is cutting cost via cache reuse) larger and more expensive than V1 in long sessions, or failing on context-length errors. Not a security issue (fails safe on error), but undermines the feature it implements.
Evidence: internal/review/session_cache.go:86-91 (commit), :104-121 (trimTrunk, count-only).
Suggested fix: Don't re-embed the full transcript on every cached review (pass only the delta once prior turns are in the trunk), or bound trimTrunk by an approximate token/byte budget in addition to pair count.

Finding 4 — Reviewer wiring lets an LLM verdict satisfy the "background commands always need a human" invariant

Impact: ApprovalState.decide() special-cases background:true execute calls to always return decisionPrompt, with a comment stating this exists because auto-approving background commands "would let any command (including destructive ones) bypass the gate." Post-PR, decisionPrompt now flows through gatedApproval → tryReview first, so with approval_review.enabled=true a background command can be silently allowed by the reviewer with no human ever seeing it — the exact bypass the comment was written to prevent, reachable through a different door. May be intentional given the PR's overall goal, but (a) policy.go never flags "background" as a risk signal even though it's present in the JSON args, and (b) delayed-visibility of background output is exactly the property that motivated the human-only rule, so the stale comment now overstates the guarantee.
Evidence: internal/runner/approval.go:277-282 (if input.Background { return decisionPrompt }); internal/runner/review.go:117-122 (gatedApproval routes every decisionPrompt through the reviewer first).
Suggested fix: Either explicitly bypass the reviewer for background commands (go straight to the human, matching the original invariant), or update the comment and add an explicit "background execution" risk category to the policy so the reviewer weights unreactable/delayed-visibility side effects appropriately.

Overall Risk: Medium

Top Findings (ranked)

  1. Finding 1 — SSRF/cloud-metadata hardening is prompt-only with no deterministic backstop, and its only test coverage is excluded from normal CI.
  2. Finding 2 — V2 investigate's verdict extraction can adopt a reflected/injected JSON blob from investigated evidence instead of escalating.
  3. Finding 3 — V3 trunk trimming by message count lets prompt size grow unbounded, risking context-length failures and undermining the cache-reuse goal.
  4. Finding 4 — Background commands can now be silently reviewer-approved, weakening a previously human-only invariant, with a stale supporting comment.

Generated by Claude Code

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/review/audit.go (1)

61-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Audit write failures are completely silent — no diagnostics anywhere.

Both the json.Marshal error path and the os.OpenFile error path swallow the error with no logging. A misconfigured AuditPath, permission issue, or full disk silently disables the audit trail forever with zero visibility, undermining the audit log's value as a debugging/test oracle described in the file's own doc comment.

As per coding guidelines, **/*.go: "Send all diagnostics through config.Logger()", failures here should be logged (rate-limited/once, to avoid log spam) rather than dropped entirely.

♻️ Suggested fix
 	line, err := json.Marshal(rec)
 	if err != nil {
+		config.Logger().Printf("[review] audit marshal failed: %v", err)
 		return
 	}
 	a.mu.Lock()
 	defer a.mu.Unlock()
 	f, err := os.OpenFile(a.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
 	if err != nil {
+		config.Logger().Printf("[review] audit open %q failed: %v", a.path, err)
 		return
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/review/audit.go` around lines 61 - 75, Update the audit write flow
around json.Marshal and os.OpenFile to report failures through config.Logger(),
including the relevant error details and audit path context. Ensure these
diagnostics are rate-limited or emitted only once to avoid log spam, while
preserving the existing early-return behavior after each failure.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/command/acp.go`:
- Around line 89-118: Extract the shared last-24-message filtering and
role-mapping logic from acpSession.recentTranscript into a review helper such as
review.MsgsFromHistory, preserving exclusion of empty/system messages and
mapping assistant/tool roles. In internal/command/acp.go lines 89-118, keep
sess.mu locked around the helper call; in internal/command/interactive.go lines
409-435, replace interactiveState.recentTranscript’s duplicate implementation
with the same helper call.

In `@internal/review/audit.go`:
- Around line 14-41: Redact sensitive values from tool arguments before
persisting them in auditRecord.Args, rather than relying only on auditArgsCap.
Add or reuse a sanitizer for common secret patterns such as Bearer/Authorization
headers, password flags, and known key prefixes, then apply it before truncation
and JSONL writing while preserving non-sensitive argument content.

In `@internal/review/investigate.go`:
- Around line 138-156: Close mo.MessageStream immediately when entering the
mo.IsStreaming branch by deferring its Close method before the Recv loop.
Preserve the existing draining, EOF handling, and error-return behavior.

---

Nitpick comments:
In `@internal/review/audit.go`:
- Around line 61-75: Update the audit write flow around json.Marshal and
os.OpenFile to report failures through config.Logger(), including the relevant
error details and audit path context. Ensure these diagnostics are rate-limited
or emitted only once to avoid log spam, while preserving the existing
early-return behavior after each failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e1396d3-d132-49a4-a113-5eb26594da36

📥 Commits

Reviewing files that changed from the base of the PR and between ca81c56 and 172c6bf.

📒 Files selected for processing (23)
  • internal-doc/approval-review-design.md
  • internal-doc/approval-review-test-report.md
  • internal/agent/agent.go
  • internal/agent/middleware.go
  • internal/command/acp.go
  • internal/command/interactive.go
  • internal/command/web.go
  • internal/config/config.go
  • internal/review/audit.go
  • internal/review/build.go
  • internal/review/eval_test.go
  • internal/review/investigate.go
  • internal/review/parse.go
  • internal/review/policy.go
  • internal/review/review.go
  • internal/review/review_test.go
  • internal/review/session_cache.go
  • internal/review/session_cache_test.go
  • internal/runner/approval.go
  • internal/runner/review.go
  • internal/runner/review_test.go
  • internal/web/chat.go
  • internal/web/engine.go

Comment thread internal/command/acp.go
Comment thread internal/review/audit.go
Comment on lines +138 to +156
if mo.IsStreaming {
for {
chunk, err := mo.MessageStream.Recv()
if err == io.EOF {
break
}
if err != nil {
if ctx.Err() != nil {
return messages, calls, ctx.Err()
}
return messages, calls, err
}
if chunk != nil {
cur.WriteString(chunk.Content)
}
}
} else if mo.Message != nil {
cur.WriteString(mo.Message.Content)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does cloudwego/eino v0.9.9's adk package require callers to Close() the MessageOutput.MessageStream returned by ChatModelAgent.Run events, or is it closed internally by the framework?

💡 Result:

In CloudWeGo Eino v0.9.9, when using the adk package, callers are responsible for managing the lifecycle of the MessageStream (typically a schema.StreamReader) returned within ChatModelAgent.Run events [1][2]. While the Eino framework manages the AgentEvent iterator, it does not automatically close the underlying MessageStream for the caller [1][2]. Because schema.StreamReader can only be consumed once [1][2] and consumes underlying system resources, it is standard practice—and explicitly recommended in Eino documentation—for the caller to call Close() on the stream after consumption is complete, typically using a defer statement [1][2]. If you are iterating through AgentEvent objects and processing the MessageStream manually, you should ensure that reader.Close() is called after you have finished receiving all chunks (i.e., after encountering io.EOF or an error) [1][2]. Note that if you use high-level convenience methods (such as those that automatically concatenate the stream into a single message), the framework may handle the consumption, but in manual streaming scenarios, the responsibility lies with the caller [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file, then inspect the relevant region with line numbers.
ast-grep outline internal/review/investigate.go --view expanded || true
sed -n '1,220p' internal/review/investigate.go | cat -n

Repository: cnjack/jcode

Length of output: 7140


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any explicit Close calls on MessageStream or StreamReader in the review path.
rg -n "MessageStream\.Close|StreamReader.*Close|defer .*Close\(\)" internal/review internal -g '!**/*vendor*'

Repository: cnjack/jcode

Length of output: 5082


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for the Eino stream reader contract in repository docs or vendored references.
rg -n "notify the sender|defer sr.Close|StreamReader" . -g '!**/*.sum' -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**'

Repository: cnjack/jcode

Length of output: 854


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify whether the stream type is used in a pattern that expects Close to be called by consumers.
python3 - <<'PY'
from pathlib import Path
p = Path("internal/review/investigate.go")
text = p.read_text()
for needle in ["MessageStream.Recv()", "MessageStream.Close()", "context.WithTimeout", "iterator.Next()"]:
    print(f"{needle}: {text.count(needle)}")
PY

Repository: cnjack/jcode

Length of output: 241


Close the assistant message stream after draining it.
mo.MessageStream.Recv() is read to io.EOF, but the stream is never closed. Add defer mo.MessageStream.Close() when mo.IsStreaming is true so the sender is notified and per-call resources are released.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/review/investigate.go` around lines 138 - 156, Close
mo.MessageStream immediately when entering the mo.IsStreaming branch by
deferring its Close method before the Recv loop. Preserve the existing draining,
EOF handling, and error-return behavior.

cnjack and others added 3 commits July 15, 2026 08:54
…ardening, V3 incremental transcript

CI: gofmt/errcheck/revive (tryReview now returns (approved, handled, err)).

Review findings:
1. SSRF/metadata hardening was prompt-only — the same defense class the eval
   proved can be talked out of. Add internal/review/ssrf.go: a deterministic
   pre-filter that runs BEFORE the model. Metadata address (incl. decimal/hex/
   octal obfuscations) + credential path -> deny; bare address mention ->
   escalate (a deny cannot be overridden by the user, an escalate can). Covered
   by normal-CI unit tests, including a wiring test proving it decides without
   a model. Policy prose stays as defense-in-depth.
2. V2 investigate could adopt a reflected/injected JSON blob as its verdict
   (newest->oldest scan). Now only the reviewer's FINAL message counts;
   anything else escalates. Injection regression test added.
3. V3 trunk re-embedded the full transcript every review and trimmed by count
   only, so it could grow past the context window — making V3 costlier than V1.
   Now: transcript is re-sent only when it actually changes (fingerprinted; the
   frontends only extend history between turns), and trimTrunk enforces a byte
   budget as well as a count. A trim forces the next review to re-send evidence.
4. background=true could be silently reviewer-approved, weakening a previously
   human-only rule. Corrected the stale comment (the invariant is "the flag
   cannot buy a free pass", not "a human sees every background command") and
   made background_execution an explicit risk signal in the prompt + policy.

CodeRabbit:
- Extract review.MsgsFromHistory; ACP/TUI/web now share one transcript helper.
- Redact credential-shaped values (auth headers, --password/--token, secret
  env assignments, gh_/xox/AKIA/sk- prefixes, private keys) before writing the
  append-only audit log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…101 tok/review)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cnjack

cnjack commented Jul 15, 2026

Copy link
Copy Markdown
Owner Author

Thanks — the review caught a genuine methodological hole, not just nits. All six findings are addressed in f4210d3, plus the CI lint failure. Summary and reasoning below.

CI (was failing): golangci-lint only — gofmt ×3, errcheck ×1 (f.Close), revive error-return ×1 (tryReview returned (bool, error, bool); error is now last). golangci-lint run is clean locally.


Finding 1 — SSRF hardening was prompt-only ✅ fixed

This was the most important one and I think the framing is exactly right: a prompt is not a control. Being talked out of a rule by a "debugging" pretext is precisely how prompt-class defenses fail, so patching it with more prompt is answering a demonstrated failure with more of the same. Worse, the only coverage was behind JCODE_REVIEW_EVAL=1 and never ran in CI.

Added internal/review/ssrf.go, a deterministic pre-filter that runs before the model is consulted (so it also costs nothing):

  • metadata address — including decimal 2852039166, hex 0xa9fea9fe, dotted-hex/octal, IPv6 fd00:ec2::254, GCP/Azure/Alibaba/Oracle — plus a credential/identity path → deny, regardless of model output or stated justification.
  • a bare metadata address with no credential path (e.g. rg 169.254.169.254 access.log) → escalate, deliberately not deny: a denial can't be overridden by the user, an escalation can. Over-blocking a legitimate log grep isn't worth it.

Coverage runs in normal CI: 12 deny cases (incl. the exact "latency debugging" framing that leaked 2/20), the bare-mention escalate, no-false-positive cases, and a wiring test that gives the Engine no usable model — a Deny can then only come from the deterministic path, proving the filter precedes model resolution rather than merely existing. The policy prose stays as defense-in-depth.

Finding 2 — investigate could adopt a reflected verdict ✅ fixed

Agreed, and it was worst exactly where it mattered (the mode that reads untrusted content). Verdict extraction now accepts only the reviewer's final message; anything else escalates. Extracted verdictFromFinalMessage so it's directly testable, with a regression test where an echoed {"outcome":"allow"} from investigated file content is followed by a non-JSON final turn — it must escalate, not adopt the blob.

Finding 3 — V3 trunk grew unbounded in tokens ✅ fixed

Correct, and it undercut the feature's whole purpose. Two fixes:

  • Incremental evidence: the transcript is re-sent only when it actually changes. The frontends only extend history between turns, so within a turn it's identical across reviews — N reviews in a turn now embed it once, not N times. Fingerprinted (transcriptKey); a trim resets the fingerprint so the next review re-sends evidence rather than silently reviewing without it.
  • trimTrunk now enforces a byte budget (~60KB) alongside the count, and reports whether it trimmed.

Finding 4 — background commands ✅ fixed (comment was the bug)

You're right that the comment overstated the guarantee. The invariant that rule actually protects is "the agent can't buy a free pass by setting a flag" — not "a human must see every background command". A reviewer that sees the command on its merits doesn't reinstate the bypass; the safe-command shortcut is what the flag must never reach. Corrected the comment, and made the delayed-visibility property a first-class signal instead of leaving it buried in the args blob: background_execution: true is now surfaced explicitly in the action prompt, with a matching policy category ("apply one step more scrutiny; don't allow side effects you wouldn't allow unattended").

CodeRabbit ✅ both fixed

  • Duplicated transcript logic: extracted review.MsgsFromHistory; ACP/TUI/web now share one helper (it was 3 copies).
  • Audit log retained raw args: now redacts credential-shaped values before writing (auth headers, --password/--token flags, secret-named env assignments, ghp_/xox/AKIA/sk- prefixes, PRIVATE KEY blocks); rationale too, since it's model-authored and can quote the command. Redaction runs before truncation so a sliced secret can't escape the patterns. 10 tests including "doesn't mangle ordinary commands". Documented as best-effort — it can't catch an opaque secret with no surrounding syntax.

On the standing caveat: this doesn't change the honest framing in the design doc and test report — the reviewer is a probabilistic middle layer, not a guarantee. That's exactly why F1's deterministic backstop matters, why every failure path fails open to a human, and why OS-level sandboxing remains a separate follow-up rather than something this PR claims to replace.

Add Auto to the unified selector (Ask for approval → Plan → Auto → Full
access): the full tool set, with the LLM reviewer adjudicating every call
that would otherwise prompt — low-risk allowed, high-risk denied, uncertain
escalated back to the user.

The reviewer loses its approval_review.enabled switch; Auto mode is now the
switch. ApprovalState builds the reviewer lazily on entering Auto and drops
it on leaving, so BuildFromConfig no longer returns nil and the three
frontends stop each deciding whether to wire it. Auto keeps the approval
axis on Manual — the reviewer is an extra gate, not a bypass, and can still
prompt when it escalates.

Expose the tuning knobs via /api/approval-review-config + a settings section:
the model picker reuses the small_model role picker's grouped enabled-model
list, and timeout/audit-path keep "empty = built-in default" while showing
the server's resolved defaults as placeholders. '' and the 'small' alias
resolve identically in resolveModelRef, so they share one option rather than
offering two spellings of the same thing.

Fixes found reviewing the above (each covered by a regression test that was
confirmed to fail against the pre-fix code):

1. TUI "Approve all" moved only the approval axis, so the mode pill kept
   naming the old mode while the backend had already gone to Full access,
   and the next Shift+Tab cycled from that stale value. It now promotes the
   unified mode too — except during Plan, where the backend keeps the
   read-only tool set and the tool axis still names the mode.
2. An approved plan moving to execution recorded and displayed Plan while
   already holding the full tool set, so the session claimed read-only while
   writable. Leaving the plan tool axis now normalizes to Approval, matching
   how resume treats a saved Plan.
3. The settings form POSTed an empty config over the stored one when its
   initial GET failed (the error was swallowed, cfg stayed {}). A failed load
   now renders an error + retry with no form to submit.
4. The web settings handler swapped Config.ApprovalReview in place while a
   task goroutine read it to build its reviewer, holding no lock in common —
   a real data race, confirmed under -race. Both sides now go through
   synchronized accessors returning a snapshot. The lock is package-level
   because Config is copied by value elsewhere and an embedded mutex would
   trip go vet's copylocks.

Generated with Jack AI bot

@coderabbitai coderabbitai 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.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/tui/tui.go (1)

367-372: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix mode mismatch when loading configuration.

If cfg.AutoApprove is true, only m.approvalMode is set to ModeAuto, but m.sessionMode is left at mode.Approval. This will cause the TUI pill to incorrectly display "Ask for approval" while the agent operates with automatic approvals.

Additionally, this block should also parse cfg.DefaultMode so that the unified session mode correctly starts in the mode saved within the configuration. You can use the new m.applySelectorMode helper to ensure all axes remain correctly synchronized.

🐛 Proposed fix
 	if cfg, err := config.LoadConfig(); err == nil {
 		m.activeProvider, m.activeModel = cfg.GetProviderModel()
-		if cfg.AutoApprove { //nolint:staticcheck // intentional fallback to the deprecated field when DefaultMode is unset
-			m.approvalMode = ModeAuto
+		if cfg.DefaultMode != "" {
+			m.applySelectorMode(mode.Parse(cfg.DefaultMode))
+		} else if cfg.AutoApprove { //nolint:staticcheck // intentional fallback to the deprecated field when DefaultMode is unset
+			m.applySelectorMode(mode.FullAccess)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/tui.go` around lines 367 - 372, Update the configuration-loading
block around cfg.GetProviderModel so it initializes the unified session mode
from cfg.DefaultMode via m.applySelectorMode, while preserving the deprecated
cfg.AutoApprove fallback by applying ModeAuto through the same helper. Ensure
approvalMode and sessionMode remain synchronized and the saved configuration
mode determines the initial TUI state.
internal/review/session_cache.go (1)

77-111: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix retry logic to handle network errors correctly and satisfy alternating role contracts.

The current retry loop has a three-part logic flaw when correcting invalid JSON:

  1. Network failures mutate the prompt: By checking attempt > 0, the code assumes the last attempt generated bad JSON. If the loop restarts due to a network error (err != nil), it incorrectly appends the JSON nudge without any prior generation.
  2. Consecutive user messages: Appending the nudge as a UserMessage directly after the action UserMessage creates consecutive user roles. Strict APIs (like Anthropic Claude) will instantly reject this with a 400 Bad Request error.
  3. Missing context: The nudge says "Your previous reply was not valid JSON", but the assistant's previous reply is omitted from the request slice, making the instruction confusing.

Storing the invalid output and tracking it directly solves all three issues.

(Note: Any unit tests asserting the exact length of reqMsgs on the retry attempt may need a minor +1 bump for the assistant message).

🐛 Proposed fix for the retry loop
-	var lastErr error
+	var lastErr error
+	var lastInvalidContent string
 	for attempt := 0; attempt < parseAttempts; attempt++ {
-		reqMsgs := make([]*schema.Message, 0, len(base)+2)
+		reqMsgs := make([]*schema.Message, 0, len(base)+4)
 		reqMsgs = append(reqMsgs, base...)
 		reqMsgs = append(reqMsgs, userMsg)
-		if attempt > 0 {
+		if lastInvalidContent != "" {
 			// The nudge lives only in the throwaway request, never in the trunk,
 			// so the committed prefix stays a clean action/verdict transcript.
+			reqMsgs = append(reqMsgs, &schema.Message{Role: schema.Assistant, Content: lastInvalidContent})
 			reqMsgs = append(reqMsgs, schema.UserMessage("Your previous reply was not valid JSON. Reply with ONLY the JSON value."))
 		}
 		meta.calls++
 		out, err := cm.Generate(ctx, reqMsgs)
 		if err != nil {
 			lastErr = err
 			if ctx.Err() != nil {
 				break
 			}
 			continue
 		}
 		if out == nil {
 			lastErr = fmt.Errorf("nil model output")
 			continue
 		}
 		a, ok := parseAssessment(out.Content)
 		if !ok {
 			lastErr = fmt.Errorf("unparseable output")
+			lastInvalidContent = out.Content
 			continue
 		}
 		meta.userAuth = a.UserAuthorization
 		res, ok := mapOutcome(a)
 		if !ok {
 			lastErr = fmt.Errorf("missing/invalid outcome")
+			lastInvalidContent = out.Content
 			continue
 		}
 		// Commit a clean (action, verdict) pair to the trunk.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/review/session_cache.go` around lines 77 - 111, Update the retry
flow around reqMsgs and parseAssessment so retries are triggered only after an
actual invalid model response, not merely because attempt > 0. Retain the
invalid output and append it as an assistant message before the user JSON-only
nudge, preserving alternating roles and including the missing context. Network
errors should retry with the original request messages unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/review/audit.go`:
- Around line 60-76: Update the secretPatterns regexes in
internal/review/audit.go that currently use \S+ to recognize quoted secret
values, including values with spaces, while retaining unquoted matching as a
fallback. Apply this consistently to flag, header, and environment-assignment
patterns so inputs such as --password="my secret token" and TOKEN="..." are
fully redacted.

In `@internal/review/ssrf.go`:
- Around line 23-36: Update the metadata-host matching logic in
internal/review/ssrf.go to parse IP literals and compare their canonical
normalized forms, ensuring expanded and compressed IPv6 representations such as
fd00:ec2:0:0:0:0:0:254 are treated identically. Preserve hostname matching and
existing numeric IPv4 encodings, and add tests covering both IPv6 forms.

In `@internal/runner/approval.go`:
- Around line 126-134: Update requestUserApprovalWithWorker so the “Approve All”
path clears the reviewer when promoting the session to mode.FullAccess. Reuse
the existing clearReviewerLocked lifecycle helper while holding the session
mutex, ensuring the reviewer and its transcript are released without changing
other approval behavior.

In `@internal/web/approval_review.go`:
- Around line 21-27: Update handleGetApprovalReviewConfig to check whether the
copied cfg is nil before calling ApprovalReviewSettings(); return the
established unavailable-configuration HTTP response when it is nil, while
preserving the existing flow for valid configurations.

In `@web/src/components/ChatInput.tsx`:
- Around line 96-99: Update MODE_DEFS in ChatInput.tsx to store translation keys
for Auto mode’s label and description instead of hardcoded English copy, then
resolve those keys through the existing t localization function. Reuse the
translation key used for the localized Auto label in AutomationsView and the
corresponding Auto description key, while preserving the mode’s existing value,
risk, and icon.

In `@web/src/components/SettingsDialog.tsx`:
- Line 1690: Give both switch controls in the SettingsDialog investigation and
session-reuse settings explicit accessible names. Add distinct accessible
labeling, such as appropriate aria-label values, to the Switch elements so
assistive technology identifies each setting without changing their existing
toggle behavior.
- Around line 1571-1574: Update the model filtering in the modelOptions
construction to retain models unless their enabled property is explicitly false,
matching ChatInput’s behavior. Preserve the provider-level filtering so
providers with no retained models are still excluded.

In `@web/src/i18n/locales/ko.ts`:
- Around line 361-377: Translate the newly added Auto-mode reviewer locale
values in the Korean locale object, including loading/error messages, title,
description, model, policy, timeout, audit path, investigation, session reuse,
and save-status labels. Preserve all existing translation keys and interpolation
placeholders such as {reason}, while replacing the English user-facing text with
natural Korean.

---

Outside diff comments:
In `@internal/review/session_cache.go`:
- Around line 77-111: Update the retry flow around reqMsgs and parseAssessment
so retries are triggered only after an actual invalid model response, not merely
because attempt > 0. Retain the invalid output and append it as an assistant
message before the user JSON-only nudge, preserving alternating roles and
including the missing context. Network errors should retry with the original
request messages unchanged.

In `@internal/tui/tui.go`:
- Around line 367-372: Update the configuration-loading block around
cfg.GetProviderModel so it initializes the unified session mode from
cfg.DefaultMode via m.applySelectorMode, while preserving the deprecated
cfg.AutoApprove fallback by applying ModeAuto through the same helper. Ensure
approvalMode and sessionMode remain synchronized and the saved configuration
mode determines the initial TUI state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 72dd2b3c-3dcb-49a8-9fc8-2846fac73772

📥 Commits

Reviewing files that changed from the base of the PR and between 172c6bf and ff84517.

📒 Files selected for processing (49)
  • internal-doc/approval-review-design.md
  • internal-doc/approval-review-test-report.md
  • internal/command/acp.go
  • internal/command/interactive.go
  • internal/command/mode_helpers_test.go
  • internal/command/web.go
  • internal/config/approval_review_test.go
  • internal/config/config.go
  • internal/mode/mode.go
  • internal/mode/mode_test.go
  • internal/review/audit.go
  • internal/review/audit_test.go
  • internal/review/build.go
  • internal/review/history.go
  • internal/review/investigate.go
  • internal/review/investigate_test.go
  • internal/review/policy.go
  • internal/review/review.go
  • internal/review/review_test.go
  • internal/review/session_cache.go
  • internal/review/session_cache_test.go
  • internal/review/ssrf.go
  • internal/review/ssrf_test.go
  • internal/runner/approval.go
  • internal/runner/approval_test.go
  • internal/runner/review.go
  • internal/runner/review_test.go
  • internal/session/mode_roundtrip_test.go
  • internal/tui/input_views.go
  • internal/tui/mode_pill_test.go
  • internal/tui/styles.go
  • internal/tui/tui.go
  • internal/tui/update.go
  • internal/web/approval_review.go
  • internal/web/approval_review_test.go
  • internal/web/engine.go
  • internal/web/mode_test.go
  • internal/web/models.go
  • internal/web/server.go
  • web/src/components/AutomationsView.tsx
  • web/src/components/ChatInput.tsx
  • web/src/components/SettingsDialog.tsx
  • web/src/i18n/locales/en.ts
  • web/src/i18n/locales/ja.ts
  • web/src/i18n/locales/ko.ts
  • web/src/i18n/locales/zh-Hans.ts
  • web/src/i18n/locales/zh-Hant.ts
  • web/src/lib/api.ts
  • web/src/lib/types.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/review/policy.go
  • internal/runner/review_test.go
  • internal/runner/review.go
  • internal/review/investigate.go
  • internal/review/review.go

Comment thread internal/review/audit.go
Comment on lines +60 to +76
var secretPatterns = []*regexp.Regexp{
// Authorization: Bearer <token> / Basic <blob>, in a header or flag.
regexp.MustCompile(`(?i)(authorization\s*:\s*(?:bearer|basic|token)\s+)\S+`),
// -H 'X-...-Token: v' / api-key: v / x-api-key: v
regexp.MustCompile(`(?i)((?:x-)?(?:api[-_]?key|auth[-_]?token|access[-_]?token|secret)\s*[:=]\s*)\S+`),
// --password=v, --token v, -p v (long-form flags only; -p alone is too noisy)
regexp.MustCompile(`(?i)(--(?:password|passwd|token|secret|api[-_]?key)(?:[=\s]+))\S+`),
// KEY=value env assignments for secret-ish names.
regexp.MustCompile(`(?i)\b([A-Z0-9_]*(?:PASSWORD|SECRET|TOKEN|APIKEY|API_KEY|ACCESS_KEY)[A-Z0-9_]*\s*=\s*)\S+`),
// Well-known key prefixes (GitHub, Slack, AWS, OpenAI-style, private keys).
regexp.MustCompile(`\b(gh[pousr]_)[A-Za-z0-9]{16,}`),
regexp.MustCompile(`\b(xox[baprs]-)[A-Za-z0-9-]{10,}`),
regexp.MustCompile(`\b(AKIA)[A-Z0-9]{12,}`),
regexp.MustCompile(`\b(sk-)[A-Za-z0-9_-]{16,}`),
regexp.MustCompile(`(-----BEGIN [A-Z ]*PRIVATE KEY-----)[\s\S]*?(-----END [A-Z ]*PRIVATE KEY-----)`),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Update secret redaction regex to handle quoted strings.

The \S+ pattern stops at the first whitespace. If a secret is provided within quotes (e.g., --password="my secret token" or export TOKEN="ghp_123..."), only the first token (like "my) will be redacted, leaking the rest of the sensitive string into the plaintext audit log.

Updating the capture group to match quoted strings or fallback to \S+ resolves this and makes the best-effort redaction significantly more robust against common shell usage patterns.

🔒️ Proposed regex fix
 var secretPatterns = []*regexp.Regexp{
 	// Authorization: Bearer <token> / Basic <blob>, in a header or flag.
-	regexp.MustCompile(`(?i)(authorization\s*:\s*(?:bearer|basic|token)\s+)\S+`),
+	regexp.MustCompile(`(?i)(authorization\s*:\s*(?:bearer|basic|token)\s+)(?:"[^"]*"|'[^']*'|\S+)`),
 	// -H 'X-...-Token: v' / api-key: v / x-api-key: v
-	regexp.MustCompile(`(?i)((?:x-)?(?:api[-_]?key|auth[-_]?token|access[-_]?token|secret)\s*[:=]\s*)\S+`),
+	regexp.MustCompile(`(?i)((?:x-)?(?:api[-_]?key|auth[-_]?token|access[-_]?token|secret)\s*[:=]\s*)(?:"[^"]*"|'[^']*'|\S+)`),
 	// --password=v, --token v, -p v (long-form flags only; -p alone is too noisy)
-	regexp.MustCompile(`(?i)(--(?:password|passwd|token|secret|api[-_]?key)(?:[=\s]+))\S+`),
+	regexp.MustCompile(`(?i)(--(?:password|passwd|token|secret|api[-_]?key)(?:[=\s]+))(?:"[^"]*"|'[^']*'|\S+)`),
 	// KEY=value env assignments for secret-ish names.
-	regexp.MustCompile(`(?i)\b([A-Z0-9_]*(?:PASSWORD|SECRET|TOKEN|APIKEY|API_KEY|ACCESS_KEY)[A-Z0-9_]*\s*=\s*)\S+`),
+	regexp.MustCompile(`(?i)\b([A-Z0-9_]*(?:PASSWORD|SECRET|TOKEN|APIKEY|API_KEY|ACCESS_KEY)[A-Z0-9_]*\s*=\s*)(?:"[^"]*"|'[^']*'|\S+)`),
 	// Well-known key prefixes (GitHub, Slack, AWS, OpenAI-style, private keys).
 	regexp.MustCompile(`\b(gh[pousr]_)[A-Za-z0-9]{16,}`),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var secretPatterns = []*regexp.Regexp{
// Authorization: Bearer <token> / Basic <blob>, in a header or flag.
regexp.MustCompile(`(?i)(authorization\s*:\s*(?:bearer|basic|token)\s+)\S+`),
// -H 'X-...-Token: v' / api-key: v / x-api-key: v
regexp.MustCompile(`(?i)((?:x-)?(?:api[-_]?key|auth[-_]?token|access[-_]?token|secret)\s*[:=]\s*)\S+`),
// --password=v, --token v, -p v (long-form flags only; -p alone is too noisy)
regexp.MustCompile(`(?i)(--(?:password|passwd|token|secret|api[-_]?key)(?:[=\s]+))\S+`),
// KEY=value env assignments for secret-ish names.
regexp.MustCompile(`(?i)\b([A-Z0-9_]*(?:PASSWORD|SECRET|TOKEN|APIKEY|API_KEY|ACCESS_KEY)[A-Z0-9_]*\s*=\s*)\S+`),
// Well-known key prefixes (GitHub, Slack, AWS, OpenAI-style, private keys).
regexp.MustCompile(`\b(gh[pousr]_)[A-Za-z0-9]{16,}`),
regexp.MustCompile(`\b(xox[baprs]-)[A-Za-z0-9-]{10,}`),
regexp.MustCompile(`\b(AKIA)[A-Z0-9]{12,}`),
regexp.MustCompile(`\b(sk-)[A-Za-z0-9_-]{16,}`),
regexp.MustCompile(`(-----BEGIN [A-Z ]*PRIVATE KEY-----)[\s\S]*?(-----END [A-Z ]*PRIVATE KEY-----)`),
}
var secretPatterns = []*regexp.Regexp{
// Authorization: Bearer <token> / Basic <blob>, in a header or flag.
regexp.MustCompile(`(?i)(authorization\s*:\s*(?:bearer|basic|token)\s+)(?:"[^"]*"|'[^']*'|\S+)`),
// -H 'X-...-Token: v' / api-key: v / x-api-key: v
regexp.MustCompile(`(?i)((?:x-)?(?:api[-_]?key|auth[-_]?token|access[-_]?token|secret)\s*[:=]\s*)(?:"[^"]*"|'[^']*'|\S+)`),
// --password=v, --token v, -p v (long-form flags only; -p alone is too noisy)
regexp.MustCompile(`(?i)(--(?:password|passwd|token|secret|api[-_]?key)(?:[=\s]+))(?:"[^"]*"|'[^']*'|\S+)`),
// KEY=value env assignments for secret-ish names.
regexp.MustCompile(`(?i)\b([A-Z0-9_]*(?:PASSWORD|SECRET|TOKEN|APIKEY|API_KEY|ACCESS_KEY)[A-Z0-9_]*\s*=\s*)(?:"[^"]*"|'[^']*'|\S+)`),
// Well-known key prefixes (GitHub, Slack, AWS, OpenAI-style, private keys).
regexp.MustCompile(`\b(gh[pousr]_)[A-Za-z0-9]{16,}`),
regexp.MustCompile(`\b(xox[baprs]-)[A-Za-z0-9-]{10,}`),
regexp.MustCompile(`\b(AKIA)[A-Z0-9]{12,}`),
regexp.MustCompile(`\b(sk-)[A-Za-z0-9_-]{16,}`),
regexp.MustCompile(`(-----BEGIN [A-Z ]*PRIVATE KEY-----)[\s\S]*?(-----END [A-Z ]*PRIVATE KEY-----)`),
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/review/audit.go` around lines 60 - 76, Update the secretPatterns
regexes in internal/review/audit.go that currently use \S+ to recognize quoted
secret values, including values with spaces, while retaining unquoted matching
as a fallback. Apply this consistently to flag, header, and
environment-assignment patterns so inputs such as --password="my secret token"
and TOKEN="..." are fully redacted.

Comment thread internal/review/ssrf.go
Comment on lines +23 to +36
var metadataHosts = []string{
"169.254.", // IPv4 link-local: IMDS (…169.254), ECS task (…170.2)
"fd00:ec2::254", // AWS IMDS over IPv6
"metadata.google.internal",
"metadata.goog",
"100.100.100.200", // Alibaba Cloud
"192.0.0.192", // Oracle Cloud
// Numeric encodings of 169.254.169.254 — curl/wget accept all of these.
"2852039166", // decimal
"0xa9fea9fe", // hex
"0xa9.0xfe.0xa9.0xfe", // dotted hex
"0251.0376.0251.0376", // dotted octal
"0251.0376.43518", // mixed octal/decimal
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and inspect its structure first.
git ls-files internal/review/ssrf.go internal/review | cat

echo
echo "== ast-grep outline =="
ast-grep outline internal/review/ssrf.go --view expanded || true

echo
echo "== relevant file excerpt =="
nl -ba internal/review/ssrf.go | sed -n '1,220p'

echo
echo "== tests / usages mentioning metadataHosts, ParseIP, or URL host checks =="
rg -n "metadataHosts|ParseIP|metadata\.google\.internal|fd00:ec2|169\.254\.|100\.100\.100\.200|192\.0\.0\.192" internal -S

Repository: cnjack/jcode

Length of output: 896


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== internal/review/ssrf.go =="
sed -n '1,220p' internal/review/ssrf.go

echo
echo "== internal/review/ssrf_test.go =="
sed -n '1,260p' internal/review/ssrf_test.go

echo
echo "== host/IP parsing usages in review package =="
rg -n "ParseIP|ParseAddr|Hostname\(|URL\.Host|Hostname\(\)|containsAny\(" internal/review -S

Repository: cnjack/jcode

Length of output: 8822


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Request definition and request construction =="
rg -n "type Request|ToolArgs|execReq\\(|u\\(" internal/review internal -S

echo
echo "== targeted excerpts =="
sed -n '1,220p' internal/review/review.go
echo
sed -n '1,220p' internal/review/ssrf_test.go

Repository: cnjack/jcode

Length of output: 26909


Normalize IP literals before matching metadata hosts. The expanded form fd00:ec2:0:0:0:0:0:254 bypasses the current substring check, so curl http://[fd00:ec2:0:0:0:0:0:254]/latest/meta-data/... skips the prefilter. Parse and canonicalize IPs before comparison, and add tests for expanded/compressed IPv6 forms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/review/ssrf.go` around lines 23 - 36, Update the metadata-host
matching logic in internal/review/ssrf.go to parse IP literals and compare their
canonical normalized forms, ensuring expanded and compressed IPv6
representations such as fd00:ec2:0:0:0:0:0:254 are treated identically. Preserve
hostname matching and existing numeric IPv4 encodings, and add tests covering
both IPv6 forms.

Comment on lines +126 to +134
defer s.mu.Unlock()
s.sessionMode = m
s.mode = approvalModeFor(m)
s.mu.Unlock()
if m == mode.Auto {
s.ensureReviewerLocked()
} else {
s.clearReviewerLocked()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear the reviewer when promoting to Full Access in requestUserApprovalWithWorker.

This changed segment correctly manages the reviewer lifecycle by clearing it when leaving Auto mode. However, in the unchanged requestUserApprovalWithWorker function (lines 483-488), when the user selects "Approve All", the session is promoted to mode.FullAccess by mutating the state inline without calling s.clearReviewerLocked(). This causes the reviewer (and its potentially large V3 transcript trunk) to leak in memory for the lifetime of the session.

Please update the unchanged requestUserApprovalWithWorker path to also clear the reviewer.

🐛 Proposed fix for the unchanged `requestUserApprovalWithWorker` function
 	if resp.Approved && resp.Mode == handler.ModeAuto {
 		s.mu.Lock()
 		s.sessionMode = mode.FullAccess
 		s.mode = handler.ModeAuto
+		s.clearReviewerLocked()
 		s.mu.Unlock()
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/runner/approval.go` around lines 126 - 134, Update
requestUserApprovalWithWorker so the “Approve All” path clears the reviewer when
promoting the session to mode.FullAccess. Reuse the existing clearReviewerLocked
lifecycle helper while holding the session mutex, ensuring the reviewer and its
transcript are released without changing other approval behavior.

Comment on lines +21 to +27
func (s *Server) handleGetApprovalReviewConfig(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
cfg := s.cfg
s.mu.Unlock()

arc := cfg.ApprovalReviewSettings()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Prevent nil-pointer dereference when config is unavailable.

s.cfg is accessed without a nil check in handleGetApprovalReviewConfig, whereas the setter properly validates it. If the configuration is unavailable (s.cfg == nil), calling cfg.ApprovalReviewSettings() will panic and crash the process.

🛡️ Proposed fix
 func (s *Server) handleGetApprovalReviewConfig(w http.ResponseWriter, r *http.Request) {
 	s.mu.Lock()
 	cfg := s.cfg
 	s.mu.Unlock()
 
+	if cfg == nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "config unavailable"})
+		return
+	}
+
 	arc := cfg.ApprovalReviewSettings()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *Server) handleGetApprovalReviewConfig(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
cfg := s.cfg
s.mu.Unlock()
arc := cfg.ApprovalReviewSettings()
func (s *Server) handleGetApprovalReviewConfig(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
cfg := s.cfg
s.mu.Unlock()
if cfg == nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "config unavailable"})
return
}
arc := cfg.ApprovalReviewSettings()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/web/approval_review.go` around lines 21 - 27, Update
handleGetApprovalReviewConfig to check whether the copied cfg is nil before
calling ApprovalReviewSettings(); return the established
unavailable-configuration HTTP response when it is nil, while preserving the
existing flow for valid configurations.

Comment on lines 96 to +99
const MODE_DEFS: ModeDef[] = [
{ value: 'approval', label: 'Ask for approval', sub: 'Agent asks before running tools', risk: 'neutral', Icon: HandRaisedIcon },
{ value: 'plan', label: 'Plan', sub: 'Agent plans, then waits for your go-ahead', risk: 'plan', Icon: ClipboardDocumentListIcon },
{ value: 'auto', label: 'Auto', sub: 'AI reviewer allows safe tools; uncertain ones ask', risk: 'info', Icon: SparklesIcon },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Localize the new Auto mode copy.

The label and description are hardcoded, so Auto remains English in non-English sessions. Store translation keys in MODE_DEFS and resolve them with t, matching the localized Auto label used by AutomationsView.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ChatInput.tsx` around lines 96 - 99, Update MODE_DEFS in
ChatInput.tsx to store translation keys for Auto mode’s label and description
instead of hardcoded English copy, then resolve those keys through the existing
t localization function. Reuse the translation key used for the localized Auto
label in AutomationsView and the corresponding Auto description key, while
preserving the mode’s existing value, risk, and icon.

Comment on lines +1571 to +1574
// Enabled models grouped by provider, matching the small_model role picker.
const modelOptions = pickerProviders
.map((p) => ({ ...p, models: p.models.filter((m) => m.enabled) }))
.filter((p) => p.models.length > 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the settings dialog and picker implementation.
git ls-files 'web/src/components/SettingsDialog.tsx' 'web/src/components/*' 'web/src/**/*picker*' 'web/src/**/*Picker*' | sed 's#^`#FILE` #'

echo
echo "== SettingsDialog.tsx around the referenced lines =="
sed -n '1548,1590p' web/src/components/SettingsDialog.tsx

echo
echo "== Search for enabled flag handling in picker-related code =="
rg -n --context 2 'enabled !== false|\.filter\(\(m\) => m\.enabled\)|enabled === false|small_model role picker|pickerProviders|modelOptions' web/src

echo
echo "== Search for model selection UI controls in SettingsDialog.tsx =="
rg -n --context 2 'switch|checkbox|aria-label|label|accessible|enabled' web/src/components/SettingsDialog.tsx

Repository: cnjack/jcode

Length of output: 27078


Preserve models when enabled is omitted. ChatInput treats only enabled === false as disabled, so this filter drops valid models with an unset flag and can leave the reviewer selector empty.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/SettingsDialog.tsx` around lines 1571 - 1574, Update the
model filtering in the modelOptions construction to retain models unless their
enabled property is explicitly false, matching ChatInput’s behavior. Preserve
the provider-level filtering so providers with no retained models are still
excluded.

<div className="text-[12px] font-medium text-[var(--color-foreground)]">{t('settings.general.approvalReviewInvestigate')}</div>
<div className="text-[11px] text-[var(--color-muted-foreground)]">{t('settings.general.approvalReviewInvestigateDesc')}</div>
</div>
<Switch on={!!cfg.investigate} onClick={() => update({ investigate: !cfg.investigate })} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Give both switches accessible names.

These switch buttons contain no text and receive no title or ARIA label, so assistive technology cannot distinguish Investigation from Session reuse.

Proposed fix
-<Switch on={!!cfg.investigate} onClick={() => update({ investigate: !cfg.investigate })} />
+<Switch
+  on={!!cfg.investigate}
+  onClick={() => update({ investigate: !cfg.investigate })}
+  title={t('settings.general.approvalReviewInvestigate')}
+/>

-<Switch on={!!cfg.reuse_session} onClick={() => update({ reuse_session: !cfg.reuse_session })} />
+<Switch
+  on={!!cfg.reuse_session}
+  onClick={() => update({ reuse_session: !cfg.reuse_session })}
+  title={t('settings.general.approvalReviewReuseSession')}
+/>

Also applies to: 1697-1697

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/SettingsDialog.tsx` at line 1690, Give both switch
controls in the SettingsDialog investigation and session-reuse settings explicit
accessible names. Add distinct accessible labeling, such as appropriate
aria-label values, to the Switch elements so assistive technology identifies
each setting without changing their existing toggle behavior.

Comment on lines +361 to +377
approvalReviewLoading: 'Loading approval review settings…',
approvalReviewLoadFailed: 'Failed to load settings: {reason}',
approvalReviewTitle: 'Auto-mode approval reviewer',
approvalReviewDesc: 'Tuning knobs for the LLM reviewer that decides whether to approve risky tool calls in Auto session mode.',
approvalReviewModel: 'Reviewer model',
approvalReviewModelUnset: 'Follow small_model → main model',
approvalReviewModelUnavailable: 'unavailable',
approvalReviewPolicy: 'Extra policy',
approvalReviewPolicyPlaceholder: 'Append workspace-specific rules to the built-in risk policy…',
approvalReviewTimeout: 'Timeout (seconds)',
approvalReviewAuditPath: 'Audit log path',
approvalReviewInvestigate: 'Investigate before verdict',
approvalReviewInvestigateDesc: 'Let the reviewer run read-only tools to gather evidence.',
approvalReviewReuseSession: 'Reuse reviewer session',
approvalReviewReuseSessionDesc: 'Keep a cached conversation to reduce prompt-cache misses.',
approvalReviewSaveFailed: 'Failed to save: {reason}',
approvalReviewSaved: 'Saved',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Translate the Auto-mode settings into Korean.

The newly added configuration fields for the Auto-mode reviewer were left in English within the Korean locale file. Translating them ensures a consistent localized user experience.

🌐 Proposed translations
-      approvalReviewLoading: 'Loading approval review settings…',
-      approvalReviewLoadFailed: 'Failed to load settings: {reason}',
-      approvalReviewTitle: 'Auto-mode approval reviewer',
-      approvalReviewDesc: 'Tuning knobs for the LLM reviewer that decides whether to approve risky tool calls in Auto session mode.',
-      approvalReviewModel: 'Reviewer model',
-      approvalReviewModelUnset: 'Follow small_model → main model',
-      approvalReviewModelUnavailable: 'unavailable',
-      approvalReviewPolicy: 'Extra policy',
-      approvalReviewPolicyPlaceholder: 'Append workspace-specific rules to the built-in risk policy…',
-      approvalReviewTimeout: 'Timeout (seconds)',
-      approvalReviewAuditPath: 'Audit log path',
-      approvalReviewInvestigate: 'Investigate before verdict',
-      approvalReviewInvestigateDesc: 'Let the reviewer run read-only tools to gather evidence.',
-      approvalReviewReuseSession: 'Reuse reviewer session',
-      approvalReviewReuseSessionDesc: 'Keep a cached conversation to reduce prompt-cache misses.',
-      approvalReviewSaveFailed: 'Failed to save: {reason}',
-      approvalReviewSaved: 'Saved',
+      approvalReviewLoading: '승인 리뷰 설정을 불러오는 중…',
+      approvalReviewLoadFailed: '설정 불러오기 실패: {reason}',
+      approvalReviewTitle: '자동 모드 승인 리뷰어',
+      approvalReviewDesc: '자동 세션 모드에서 위험한 도구 호출의 승인 여부를 결정하는 LLM 리뷰어의 설정을 조정합니다.',
+      approvalReviewModel: '리뷰어 모델',
+      approvalReviewModelUnset: 'small_model → 메인 모델을 따름',
+      approvalReviewModelUnavailable: '사용 불가',
+      approvalReviewPolicy: '추가 정책',
+      approvalReviewPolicyPlaceholder: '기본 위험 정책에 워크스페이스 관련 규칙 추가…',
+      approvalReviewTimeout: '시간 초과 (초)',
+      approvalReviewAuditPath: '감사 로그 경로',
+      approvalReviewInvestigate: '판결 전 조사',
+      approvalReviewInvestigateDesc: '리뷰어가 증거를 수집하기 위해 읽기 전용 도구를 실행할 수 있도록 허용합니다.',
+      approvalReviewReuseSession: '리뷰어 세션 재사용',
+      approvalReviewReuseSessionDesc: '프롬프트 캐시 미스를 줄이기 위해 캐시된 대화를 유지합니다.',
+      approvalReviewSaveFailed: '저장 실패: {reason}',
+      approvalReviewSaved: '저장됨',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
approvalReviewLoading: 'Loading approval review settings…',
approvalReviewLoadFailed: 'Failed to load settings: {reason}',
approvalReviewTitle: 'Auto-mode approval reviewer',
approvalReviewDesc: 'Tuning knobs for the LLM reviewer that decides whether to approve risky tool calls in Auto session mode.',
approvalReviewModel: 'Reviewer model',
approvalReviewModelUnset: 'Follow small_model → main model',
approvalReviewModelUnavailable: 'unavailable',
approvalReviewPolicy: 'Extra policy',
approvalReviewPolicyPlaceholder: 'Append workspace-specific rules to the built-in risk policy…',
approvalReviewTimeout: 'Timeout (seconds)',
approvalReviewAuditPath: 'Audit log path',
approvalReviewInvestigate: 'Investigate before verdict',
approvalReviewInvestigateDesc: 'Let the reviewer run read-only tools to gather evidence.',
approvalReviewReuseSession: 'Reuse reviewer session',
approvalReviewReuseSessionDesc: 'Keep a cached conversation to reduce prompt-cache misses.',
approvalReviewSaveFailed: 'Failed to save: {reason}',
approvalReviewSaved: 'Saved',
approvalReviewLoading: '승인 리뷰 설정을 불러오는 중…',
approvalReviewLoadFailed: '설정 불러오기 실패: {reason}',
approvalReviewTitle: '자동 모드 승인 리뷰어',
approvalReviewDesc: '자동 세션 모드에서 위험한 도구 호출의 승인 여부를 결정하는 LLM 리뷰어의 설정을 조정합니다.',
approvalReviewModel: '리뷰어 모델',
approvalReviewModelUnset: 'small_model → 메인 모델을 따름',
approvalReviewModelUnavailable: '사용 불가',
approvalReviewPolicy: '추가 정책',
approvalReviewPolicyPlaceholder: '기본 위험 정책에 워크스페이스 관련 규칙 추가…',
approvalReviewTimeout: '시간 초과 (초)',
approvalReviewAuditPath: '감사 로그 경로',
approvalReviewInvestigate: '판결 전 조사',
approvalReviewInvestigateDesc: '리뷰어가 증거를 수집하기 위해 읽기 전용 도구를 실행할 수 있도록 허용합니다.',
approvalReviewReuseSession: '리뷰어 세션 재사용',
approvalReviewReuseSessionDesc: '프롬프트 캐시 미스를 줄이기 위해 캐시된 대화를 유지합니다.',
approvalReviewSaveFailed: '저장 실패: {reason}',
approvalReviewSaved: '저장됨',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/i18n/locales/ko.ts` around lines 361 - 377, Translate the newly added
Auto-mode reviewer locale values in the Korean locale object, including
loading/error messages, title, description, model, policy, timeout, audit path,
investigation, session reuse, and save-status labels. Preserve all existing
translation keys and interpolation placeholders such as {reason}, while
replacing the English user-facing text with natural Korean.

@cnjack
cnjack merged commit 8493f19 into main Jul 15, 2026
3 checks passed
@cnjack
cnjack deleted the feat/approval-review branch July 15, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant