Skip to content

Feat/risk tiering#633

Open
sang-neo03 wants to merge 6 commits intomainfrom
feat/risk-tiering
Open

Feat/risk tiering#633
sang-neo03 wants to merge 6 commits intomainfrom
feat/risk-tiering

Conversation

@sang-neo03
Copy link
Copy Markdown
Collaborator

@sang-neo03 sang-neo03 commented Apr 23, 2026

Summary

Give AI agents a stable protocol signal for high-risk write operations. Introduces exit code 10 = confirmation_required with a structured error envelope (risk.level,
risk.action, fix_command), unifying the confirmation path across shortcut and service commands.

Changes

  • Add ExitConfirmationRequired = 10 and extend ErrDetail with FixCommand + Risk fields (internal/output/{exitcode,envelope}.go)
  • New cmdutil.RequireConfirmation(action) + SetRisk/GetRisk helpers, shared by shortcut runner and service command path (internal/cmdutil/{confirm,risk}.go)
  • Shortcut path: replace ad-hoc gate with shared RequireConfirmation, wire SetRisk on mount (shortcuts/common/{runner.go,common.go})
  • Service path: read method["risk"] from meta, auto-register --yes for high-risk-write, insert gate after --dry-run check (cmd/service/service.go)
  • Render Risk: <level> line in --help output for any command with risk metadata (cmd/root.go)
  • Unit tests: risk_test.go, confirm_test.go, root_risk_help_test.go, service_risk_test.go

Test Plan

  • Unit tests pass (go test ./... — all framework packages green; e2e failures pre-existing on main)
  • Manual local verification confirms the lark-cli drive +delete and lark-cli drive files copy commands return exit 10 + confirmation_required envelope when --yes is
    omitted
  • --help shows Risk: <level> for shortcut and service commands
  • --dry-run bypasses gate for both paths

Summary by CodeRabbit

  • New Features

    • High-risk write operations now require explicit confirmation via --yes and surface a "Risk: " line in help.
    • Error responses include structured risk details and a dedicated exit code when confirmation is required.
  • Documentation

    • Guidance added for approval flows, previews (--dry-run) and safe retry behavior for high-risk commands.
  • Tests

    • New tests for risk annotations, help rendering, confirmation gating, and error serialization.

@github-actions github-actions Bot added the size/L Large or sensitive change across domains or core paths label Apr 23, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aaa2b214-9871-4d21-8464-e86509674d89

📥 Commits

Reviewing files that changed from the base of the PR and between 6af1a2a and 709cabe.

📒 Files selected for processing (4)
  • cmd/service/service.go
  • internal/output/envelope.go
  • internal/output/exitcode.go
  • shortcuts/common/runner.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/output/exitcode.go
  • internal/output/envelope.go
  • shortcuts/common/runner.go
  • cmd/service/service.go

📝 Walkthrough

Walkthrough

Adds command-level risk annotations and a confirmation flow: commands can be annotated with a risk level, help shows a Risk: line, high-risk-write methods require explicit --yes (exiting with code 10 and a structured confirmation_required error) and supporting utilities, tests, and docs were added.

Changes

Cohort / File(s) Summary
Help output & tests
cmd/root.go, cmd/root_risk_help_test.go
Help wrapper now prints Risk: <level> when a command has a risk annotation; tests assert Risk presence/ordering with Tips.
Service commands & tests
cmd/service/service.go, cmd/service/service_risk_test.go
Service method factory reads method risk, registers --yes for high-risk-write, sets command risk via cmdutil.SetRisk, and enforces confirmation at runtime; tests verify flag, GetRisk, gating, and dry-run behavior.
Shortcut integration & removal
shortcuts/common/runner.go, shortcuts/common/common.go
Shortcut mounting sets command risk via cmdutil.SetRisk; confirmation logic moved to cmdutil.RequireConfirmation and the old helper was removed.
cmdutil: risk & confirmation utilities + tests
internal/cmdutil/risk.go, internal/cmdutil/risk_test.go, internal/cmdutil/confirm.go, internal/cmdutil/confirm_test.go
Added SetRisk/GetRisk to store risk in Cobra annotations and RequireConfirmation(action) which returns a structured ExitError with risk/action; tests cover annotation edge cases and error shape/serialization.
Output envelope & exit codes
internal/output/envelope.go, internal/output/exitcode.go
Added RiskDetail to ErrDetail and new exit code ExitConfirmationRequired = 10 for confirmation-required failures.
Docs: agent approval protocol
skills/lark-shared/SKILL.md
Documents detection/handling of high-risk-write exit code 10 and JSON envelope, retry guidance for agents, and restrictions on automatic retries.

Sequence Diagram

sequenceDiagram
    participant User as Agent/User
    participant CLI as CLI Command
    participant Service as Service Handler
    participant Confirm as cmdutil.RequireConfirmation
    participant Output as Error Output

    User->>CLI: Run high-risk-write command (no --yes)
    CLI->>Service: Invoke method handler
    Service->>Service: Check risk == "high-risk-write"?
    alt --yes present
        Service->>Service: Proceed with API call
        Service->>User: Return result
    else --yes missing
        Service->>Confirm: Call RequireConfirmation(action)
        Confirm->>Output: Build ExitError (confirmation_required) with risk/action
        Output->>User: Exit code 10 + JSON error envelope (stderr)
        User->>User: Inspect envelope, decide to confirm
        User->>CLI: Re-run command with --yes
        CLI->>Service: Handler proceeds and executes
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Suggested reviewers

  • liangshuo-1
  • hugang-lark

Poem

🐰 I sniff the path, a red flag on my nose,
"Risk" I whisper where the danger grows.
If you say "Yes", I'll bravely bound ahead,
Otherwise I pause — a cautious hop instead. 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feat/risk tiering' is vague and generic, using conventional prefixes but failing to clearly convey the specific change or feature being introduced. Consider a more descriptive title like 'Add confirmation protocol for high-risk write operations' or 'Introduce exit code 10 for risk-gated operations' to better communicate the main change.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description comprehensively covers all required template sections: Summary explains the motivation, Changes lists all main modifications across multiple files, Test Plan documents verification steps with checkmarks, and Related Issues is acknowledged as 'None'.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/risk-tiering

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 and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 95.45455% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.15%. Comparing base (ce80b3b) to head (709cabe).

Files with missing lines Patch % Lines
shortcuts/common/runner.go 33.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #633      +/-   ##
==========================================
- Coverage   62.89%   61.15%   -1.74%     
==========================================
  Files         428      430       +2     
  Lines       37280    45221    +7941     
==========================================
+ Hits        23448    27656    +4208     
- Misses      11721    15454    +3733     
  Partials     2111     2111              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@709cabe23ae8b3201c5af6de2f346f74aadd8018

🧩 Skill update

npx skills add larksuite/cli#feat/risk-tiering -y -g

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (2)
shortcuts/common/runner.go (1)

638-655: ⚠️ Potential issue | 🟠 Major

Gate unconfirmed high-risk shortcuts before resolving inputs.

resolveInputFlags can consume stdin and read @file contents before the confirmation error is returned. For high-risk writes without --yes, fail with exit 10 before local I/O; keep --dry-run bypassing the gate.

🛡️ Proposed fix
 	if err := validateEnumFlags(rctx, s.Flags); err != nil {
 		return err
 	}
+	if s.Risk == "high-risk-write" && !rctx.Bool("dry-run") && !rctx.Bool("yes") {
+		return cmdutil.RequireConfirmation(s.Service + " " + s.Command)
+	}
 	if err := resolveInputFlags(rctx, s.Flags); err != nil {
 		return err
 	}
 	if err := output.ValidateJqFlags(rctx.JqExpr, "", rctx.Format); err != nil {
@@
 	if rctx.Bool("dry-run") {
 		return handleShortcutDryRun(f, rctx, s)
 	}
 
-	if s.Risk == "high-risk-write" && !rctx.Bool("yes") {
-		return cmdutil.RequireConfirmation(s.Service + " " + s.Command)
-	}
-
 	if err := s.Execute(rctx.ctx, rctx); err != nil {

Please add regression coverage for a high-risk shortcut using stdin or @file input without --yes. As per coding guidelines, "Every behavior change must have an accompanying test".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/common/runner.go` around lines 638 - 655, Move the high-risk
confirmation gate to run before any local I/O by checking s.Risk ==
"high-risk-write" and !rctx.Bool("yes") (but still allow rctx.Bool("dry-run") to
bypass the gate) before calling resolveInputFlags; specifically, perform the
s.Risk + !rctx.Bool("yes") check (and call/return
cmdutil.RequireConfirmation(s.Service + " " + s.Command") to produce the exit-10
behavior) immediately after the existing dry-run check and before
resolveInputFlags/rctx JQ validation, so resolveInputFlags (which may consume
stdin or `@file`) is not invoked for unconfirmed high-risk shortcuts; also add a
regression test that invokes the shortcut with stdin or `@file` input without
--yes and verifies it fails early with the expected exit code.
cmd/service/service.go (1)

245-261: ⚠️ Potential issue | 🟠 Major

Move the confirmation gate before request construction.

buildServiceRequest(opts) can consume stdin and read upload files before the confirmation_required error is returned. For an unconfirmed high-risk write, return exit 10 before local I/O while still allowing --dry-run to render the preview.

🛡️ Proposed fix
-	request, fileMeta, err := buildServiceRequest(opts)
-	if err != nil {
-		return err
-	}
-
-	if opts.DryRun {
-		if fileMeta != nil {
-			return cmdutil.PrintDryRunWithFile(f.IOStreams.Out, request, config, opts.Format, fileMeta.FieldName, fileMeta.FilePath, fileMeta.FormFields)
-		}
-		return serviceDryRun(f, request, config, opts.Format)
-	}
-
-	if registry.GetStrFromMap(opts.Method, "risk") == "high-risk-write" {
+	highRisk := registry.GetStrFromMap(opts.Method, "risk") == "high-risk-write"
+	if highRisk && !opts.DryRun {
 		if yes, _ := opts.Cmd.Flags().GetBool("yes"); !yes {
 			return cmdutil.RequireConfirmation(opts.SchemaPath)
 		}
 	}
 
+	request, fileMeta, err := buildServiceRequest(opts)
+	if err != nil {
+		return err
+	}
+
+	if opts.DryRun {
+		if fileMeta != nil {
+			return cmdutil.PrintDryRunWithFile(f.IOStreams.Out, request, config, opts.Format, fileMeta.FieldName, fileMeta.FilePath, fileMeta.FormFields)
+		}
+		return serviceDryRun(f, request, config, opts.Format)
+	}
+

Please add a regression test for a high-risk service command with --params -, --data -, or --file confirming it returns confirmation_required before consuming input. As per coding guidelines, "Every behavior change must have an accompanying test".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/service/service.go` around lines 245 - 261, Move the high-risk
confirmation check to run before calling buildServiceRequest(opts): check
registry.GetStrFromMap(opts.Method, "risk") == "high-risk-write" and, unless
opts.DryRun is true, require confirmation via
cmdutil.RequireConfirmation(opts.SchemaPath) (using the existing
opts.Cmd.Flags().GetBool("yes") logic) so we return the confirmation_required
exit before any local I/O or stdin consumption; preserve existing DryRun
behavior so --dry-run still constructs and prints the request (using
cmdutil.PrintDryRunWithFile/serviceDryRun) without prompting. Also add a
regression test that invokes the high-risk service command with --params - /
--data - / --file and asserts it returns confirmation_required before consuming
input.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/service/service_risk_test.go`:
- Around line 87-96: The test currently asserts on the error string from
cmd.Execute() instead of the structured error; update the test to type-assert
the returned error to ExitConfirmationRequired (or unwrap and compare via
errors.Is against ExitConfirmationRequired) and then assert the embedded risk
details match the expected action "drive.files.delete" (e.g., inspect the
returned confirmation error's Risk or Detail fields) rather than relying on
substring checks so the service gate protocol remains stable; locate the
assertions around cmd.Execute() in service_risk_test.go and replace the string
checks with these structured assertions.

In `@internal/cmdutil/confirm_test.go`:
- Around line 16-51: The test TestRequireConfirmation_EnvelopeShape does not
assert the presence or value of the FixCommand on the confirmation envelope, so
regressions that omit FixCommand will go unnoticed; update the test (in
internal/cmdutil/confirm_test.go, within TestRequireConfirmation_EnvelopeShape)
to check that exitErr.Detail.FixCommand (or the corresponding field on d) is
non-nil/non-empty and equals the expected remediation command (e.g., the exact
string the user should run to fix the issue such as "your-expected-fix-command"
or "add --yes to confirm" style), failing the test if the FixCommand is missing
or incorrect so that RequireConfirmation must continue to populate that field.
- Around line 111-120: The test assumes buildFixCommand simply joins os.Args
with spaces; update both the implementation (buildFixCommand) to produce a
shell-safe serialization (escape or quote tokens containing spaces, quotes or
JSON characters so the resulting fix_command is replayable by shells/agents) and
the test (TestBuildFixCommand_AppendsYes) to cover complex tokens (e.g., a JSON
param or a path with spaces) and assert the returned string contains properly
quoted/escaped tokens plus the appended "--yes"; locate buildFixCommand and any
code that emits fix_command to implement quoting/escaping (use a standard
shell-quoting strategy) and change the test’s os.Args and expected string to
match the new safe serialization.

In `@internal/cmdutil/confirm.go`:
- Around line 49-52: The current construction of fix_command uses
strings.Join(os.Args, " ") which can produce unsafe/unambiguous shell input when
args contain spaces or shell metacharacters; modify the code that builds parts
(the parts slice and the strings.Join call in confirm.go) to shell-escape or
quote each argv element before joining (also quote the appended "--yes");
implement per-argument quoting (e.g., map each element through a shell-escape
helper or use strconv.Quote/robust single-quote-escape logic) and then join the
quoted elements with spaces so the resulting fix_command is safe for agent
execution.

---

Outside diff comments:
In `@cmd/service/service.go`:
- Around line 245-261: Move the high-risk confirmation check to run before
calling buildServiceRequest(opts): check registry.GetStrFromMap(opts.Method,
"risk") == "high-risk-write" and, unless opts.DryRun is true, require
confirmation via cmdutil.RequireConfirmation(opts.SchemaPath) (using the
existing opts.Cmd.Flags().GetBool("yes") logic) so we return the
confirmation_required exit before any local I/O or stdin consumption; preserve
existing DryRun behavior so --dry-run still constructs and prints the request
(using cmdutil.PrintDryRunWithFile/serviceDryRun) without prompting. Also add a
regression test that invokes the high-risk service command with --params - /
--data - / --file and asserts it returns confirmation_required before consuming
input.

In `@shortcuts/common/runner.go`:
- Around line 638-655: Move the high-risk confirmation gate to run before any
local I/O by checking s.Risk == "high-risk-write" and !rctx.Bool("yes") (but
still allow rctx.Bool("dry-run") to bypass the gate) before calling
resolveInputFlags; specifically, perform the s.Risk + !rctx.Bool("yes") check
(and call/return cmdutil.RequireConfirmation(s.Service + " " + s.Command") to
produce the exit-10 behavior) immediately after the existing dry-run check and
before resolveInputFlags/rctx JQ validation, so resolveInputFlags (which may
consume stdin or `@file`) is not invoked for unconfirmed high-risk shortcuts; also
add a regression test that invokes the shortcut with stdin or `@file` input
without --yes and verifies it fails early with the expected exit code.
🪄 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: 69f1a5f6-9945-4d41-8ed4-c3b23b4ef49c

📥 Commits

Reviewing files that changed from the base of the PR and between c7ced37 and f54083c.

📒 Files selected for processing (13)
  • cmd/root.go
  • cmd/root_risk_help_test.go
  • cmd/service/service.go
  • cmd/service/service_risk_test.go
  • internal/cmdutil/confirm.go
  • internal/cmdutil/confirm_test.go
  • internal/cmdutil/risk.go
  • internal/cmdutil/risk_test.go
  • internal/output/envelope.go
  • internal/output/exitcode.go
  • shortcuts/common/common.go
  • shortcuts/common/runner.go
  • skills/lark-shared/SKILL.md
💤 Files with no reviewable changes (1)
  • shortcuts/common/common.go

Comment on lines +87 to +96
err := cmd.Execute()
if err == nil {
t.Fatal("expected confirmation error, got nil")
}
if !strings.Contains(err.Error(), "requires confirmation") {
t.Errorf("expected 'requires confirmation' in error, got: %v", err)
}
if !strings.Contains(err.Error(), "drive.files.delete") {
t.Errorf("expected schema path in error action, got: %v", err)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Assert the structured confirmation error, not only text.

This integration test would still pass if the service path returned a plain error containing the same words. Pin ExitConfirmationRequired and the risk detail so the service gate keeps the stable agent-facing protocol.

Proposed assertion
 import (
+	"errors"
 	"strings"
 	"testing"
 
 	"github.com/larksuite/cli/internal/cmdutil"
+	"github.com/larksuite/cli/internal/output"
 )
@@
 	if err == nil {
 		t.Fatal("expected confirmation error, got nil")
 	}
+	var exitErr *output.ExitError
+	if !errors.As(err, &exitErr) {
+		t.Fatalf("expected *output.ExitError, got %T: %v", err, err)
+	}
+	if exitErr.Code != output.ExitConfirmationRequired {
+		t.Errorf("exit code = %d, want %d", exitErr.Code, output.ExitConfirmationRequired)
+	}
+	if exitErr.Detail == nil || exitErr.Detail.Risk == nil {
+		t.Fatalf("expected structured risk detail, got %#v", exitErr.Detail)
+	}
+	if exitErr.Detail.Risk.Level != "high-risk-write" || exitErr.Detail.Risk.Action != "drive.files.delete" {
+		t.Errorf("risk detail = %#v", exitErr.Detail.Risk)
+	}
 	if !strings.Contains(err.Error(), "requires confirmation") {
 		t.Errorf("expected 'requires confirmation' in error, got: %v", err)
 	}

As per coding guidelines, “Make error messages structured, actionable, and specific for AI agent parsing”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/service/service_risk_test.go` around lines 87 - 96, The test currently
asserts on the error string from cmd.Execute() instead of the structured error;
update the test to type-assert the returned error to ExitConfirmationRequired
(or unwrap and compare via errors.Is against ExitConfirmationRequired) and then
assert the embedded risk details match the expected action "drive.files.delete"
(e.g., inspect the returned confirmation error's Risk or Detail fields) rather
than relying on substring checks so the service gate protocol remains stable;
locate the assertions around cmd.Execute() in service_risk_test.go and replace
the string checks with these structured assertions.

Comment on lines +16 to +51
func TestRequireConfirmation_EnvelopeShape(t *testing.T) {
err := RequireConfirmation("drive +delete")
if err == nil {
t.Fatal("expected non-nil error")
}

var exitErr *output.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("expected *output.ExitError, got %T", err)
}
if exitErr.Code != output.ExitConfirmationRequired {
t.Errorf("Code = %d, want %d", exitErr.Code, output.ExitConfirmationRequired)
}
if exitErr.Detail == nil {
t.Fatal("Detail is nil")
}
d := exitErr.Detail
if d.Type != "confirmation_required" {
t.Errorf("Type = %q, want confirmation_required", d.Type)
}
if !strings.Contains(d.Message, "drive +delete") || !strings.Contains(d.Message, "requires confirmation") {
t.Errorf("Message = %q, want it to mention action and 'requires confirmation'", d.Message)
}
if d.Hint != "add --yes to confirm" {
t.Errorf("Hint = %q, want 'add --yes to confirm'", d.Hint)
}
if d.Risk == nil {
t.Fatal("Risk is nil")
}
if d.Risk.Level != "high-risk-write" {
t.Errorf("Risk.Level = %q, want high-risk-write", d.Risk.Level)
}
if d.Risk.Action != "drive +delete" {
t.Errorf("Risk.Action = %q, want drive +delete", d.Risk.Action)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Assert fix_command on the confirmation envelope.

This test pins risk and the message, but not the required remediation command. If RequireConfirmation stops assigning FixCommand, these tests still pass even though the protocol regresses.

Proposed test tightening
 func TestRequireConfirmation_EnvelopeShape(t *testing.T) {
+	orig := os.Args
+	t.Cleanup(func() { os.Args = orig })
+	os.Args = []string{"lark-cli", "drive", "+delete"}
+
 	err := RequireConfirmation("drive +delete")
 	if err == nil {
 		t.Fatal("expected non-nil error")
 	}
@@
 	if d.Hint != "add --yes to confirm" {
 		t.Errorf("Hint = %q, want 'add --yes to confirm'", d.Hint)
 	}
+	if d.FixCommand != "lark-cli drive +delete --yes" {
+		t.Errorf("FixCommand = %q, want retry command with --yes", d.FixCommand)
+	}
 	if d.Risk == nil {
 		t.Fatal("Risk is nil")
 	}

As per coding guidelines, “Every behavior change must have an accompanying test” and “Make error messages structured, actionable, and specific for AI agent parsing”.

📝 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 TestRequireConfirmation_EnvelopeShape(t *testing.T) {
err := RequireConfirmation("drive +delete")
if err == nil {
t.Fatal("expected non-nil error")
}
var exitErr *output.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("expected *output.ExitError, got %T", err)
}
if exitErr.Code != output.ExitConfirmationRequired {
t.Errorf("Code = %d, want %d", exitErr.Code, output.ExitConfirmationRequired)
}
if exitErr.Detail == nil {
t.Fatal("Detail is nil")
}
d := exitErr.Detail
if d.Type != "confirmation_required" {
t.Errorf("Type = %q, want confirmation_required", d.Type)
}
if !strings.Contains(d.Message, "drive +delete") || !strings.Contains(d.Message, "requires confirmation") {
t.Errorf("Message = %q, want it to mention action and 'requires confirmation'", d.Message)
}
if d.Hint != "add --yes to confirm" {
t.Errorf("Hint = %q, want 'add --yes to confirm'", d.Hint)
}
if d.Risk == nil {
t.Fatal("Risk is nil")
}
if d.Risk.Level != "high-risk-write" {
t.Errorf("Risk.Level = %q, want high-risk-write", d.Risk.Level)
}
if d.Risk.Action != "drive +delete" {
t.Errorf("Risk.Action = %q, want drive +delete", d.Risk.Action)
}
}
func TestRequireConfirmation_EnvelopeShape(t *testing.T) {
orig := os.Args
t.Cleanup(func() { os.Args = orig })
os.Args = []string{"lark-cli", "drive", "+delete"}
err := RequireConfirmation("drive +delete")
if err == nil {
t.Fatal("expected non-nil error")
}
var exitErr *output.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("expected *output.ExitError, got %T", err)
}
if exitErr.Code != output.ExitConfirmationRequired {
t.Errorf("Code = %d, want %d", exitErr.Code, output.ExitConfirmationRequired)
}
if exitErr.Detail == nil {
t.Fatal("Detail is nil")
}
d := exitErr.Detail
if d.Type != "confirmation_required" {
t.Errorf("Type = %q, want confirmation_required", d.Type)
}
if !strings.Contains(d.Message, "drive +delete") || !strings.Contains(d.Message, "requires confirmation") {
t.Errorf("Message = %q, want it to mention action and 'requires confirmation'", d.Message)
}
if d.Hint != "add --yes to confirm" {
t.Errorf("Hint = %q, want 'add --yes to confirm'", d.Hint)
}
if d.FixCommand != "lark-cli drive +delete --yes" {
t.Errorf("FixCommand = %q, want retry command with --yes", d.FixCommand)
}
if d.Risk == nil {
t.Fatal("Risk is nil")
}
if d.Risk.Level != "high-risk-write" {
t.Errorf("Risk.Level = %q, want high-risk-write", d.Risk.Level)
}
if d.Risk.Action != "drive +delete" {
t.Errorf("Risk.Action = %q, want drive +delete", d.Risk.Action)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmdutil/confirm_test.go` around lines 16 - 51, The test
TestRequireConfirmation_EnvelopeShape does not assert the presence or value of
the FixCommand on the confirmation envelope, so regressions that omit FixCommand
will go unnoticed; update the test (in internal/cmdutil/confirm_test.go, within
TestRequireConfirmation_EnvelopeShape) to check that exitErr.Detail.FixCommand
(or the corresponding field on d) is non-nil/non-empty and equals the expected
remediation command (e.g., the exact string the user should run to fix the issue
such as "your-expected-fix-command" or "add --yes to confirm" style), failing
the test if the FixCommand is missing or incorrect so that RequireConfirmation
must continue to populate that field.

Comment thread internal/cmdutil/confirm_test.go Outdated
Comment on lines +111 to +120
func TestBuildFixCommand_AppendsYes(t *testing.T) {
orig := os.Args
t.Cleanup(func() { os.Args = orig })
os.Args = []string{"lark-cli", "drive", "+delete", "--file-token", "abc"}

got := buildFixCommand()
want := "lark-cli drive +delete --file-token abc --yes"
if got != want {
t.Errorf("buildFixCommand() = %q, want %q", got, want)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Cover shell-safe fix_command serialization.

The current expectation only covers trivial tokens and effectively locks in raw space-joining. A retry command containing JSON params or paths with spaces becomes ambiguous/non-replayable for agents.

Proposed regression test
 func TestBuildFixCommand_AppendsYes(t *testing.T) {
 	orig := os.Args
 	t.Cleanup(func() { os.Args = orig })
 	os.Args = []string{"lark-cli", "drive", "+delete", "--file-token", "abc"}
@@
 	if got != want {
 		t.Errorf("buildFixCommand() = %q, want %q", got, want)
 	}
 }
+
+func TestBuildFixCommand_QuotesArgsWithSpaces(t *testing.T) {
+	orig := os.Args
+	t.Cleanup(func() { os.Args = orig })
+	os.Args = []string{
+		"lark-cli",
+		"drive",
+		"+delete",
+		"--params",
+		`{"file_token":"tok abc"}`,
+	}
+
+	got := buildFixCommand()
+	want := `lark-cli drive +delete --params '{"file_token":"tok abc"}' --yes`
+	if got != want {
+		t.Errorf("buildFixCommand() = %q, want %q", got, want)
+	}
+}

As per coding guidelines, “Design CLI flags, help text, and error messages with AI agent consumption in mind; every error message will be parsed by AI agents”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmdutil/confirm_test.go` around lines 111 - 120, The test assumes
buildFixCommand simply joins os.Args with spaces; update both the implementation
(buildFixCommand) to produce a shell-safe serialization (escape or quote tokens
containing spaces, quotes or JSON characters so the resulting fix_command is
replayable by shells/agents) and the test (TestBuildFixCommand_AppendsYes) to
cover complex tokens (e.g., a JSON param or a path with spaces) and assert the
returned string contains properly quoted/escaped tokens plus the appended
"--yes"; locate buildFixCommand and any code that emits fix_command to implement
quoting/escaping (use a standard shell-quoting strategy) and change the test’s
os.Args and expected string to match the new safe serialization.

Comment thread internal/cmdutil/confirm.go Outdated
Comment on lines +49 to +52
parts := make([]string, 0, len(os.Args)+1)
parts = append(parts, os.Args...)
parts = append(parts, "--yes")
return strings.Join(parts, " ")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Shell-escape argv before emitting fix_command.

strings.Join(os.Args, " ") makes fix_command ambiguous and potentially unsafe for agent execution when flag values contain spaces, quotes, ;, &&, $(), or newlines. Since agents are instructed to retry this command, quote each argv element before joining.

🛡️ Proposed fix
 	parts := make([]string, 0, len(os.Args)+1)
-	parts = append(parts, os.Args...)
+	for _, arg := range os.Args {
+		parts = append(parts, shellQuoteArg(arg))
+	}
 	parts = append(parts, "--yes")
 	return strings.Join(parts, " ")
 }
+
+func shellQuoteArg(arg string) string {
+	if arg != "" && strings.IndexFunc(arg, func(r rune) bool {
+		return !((r >= 'a' && r <= 'z') ||
+			(r >= 'A' && r <= 'Z') ||
+			(r >= '0' && r <= '9') ||
+			strings.ContainsRune("@%_+=:,./-", r))
+	}) == -1 {
+		return arg
+	}
+	return "'" + strings.ReplaceAll(arg, "'", "'\"'\"'") + "'"
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmdutil/confirm.go` around lines 49 - 52, The current construction
of fix_command uses strings.Join(os.Args, " ") which can produce
unsafe/unambiguous shell input when args contain spaces or shell metacharacters;
modify the code that builds parts (the parts slice and the strings.Join call in
confirm.go) to shell-escape or quote each argv element before joining (also
quote the appended "--yes"); implement per-argument quoting (e.g., map each
element through a shell-escape helper or use strconv.Quote/robust
single-quote-escape logic) and then join the quoted elements with spaces so the
resulting fix_command is safe for agent execution.

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

Labels

size/L Large or sensitive change across domains or core paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant