From 7d63699f9f5f25ff7ebb88c4fd37adbbe1b03669 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 21 Apr 2026 19:36:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20#145=20=E2=80=94=20wire=20`claw=20plugi?= =?UTF-8?q?ns`=20subcommand=20to=20CLI=20parser=20(prompt=20misdelivery=20?= =?UTF-8?q?fix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `claw plugins` (and `claw plugins list`, `claw plugins --help`, `claw plugins info `, etc.) fell through the top-level subcommand match and got routed into the prompt-execution path. Result: a purely local introspection command triggered an Anthropic API call and surfaced `missing Anthropic credentials` to the user. With valid credentials, it would actually send the literal string "plugins" as a user prompt to Claude, burning tokens for a local query. $ claw plugins error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY before calling the Anthropic API $ ANTHROPIC_API_KEY=dummy claw plugins โ ‹ ๐Ÿฆ€ Thinking... โœ˜ โŒ Request failed error: api returned 401 Unauthorized Meanwhile siblings (`agents`, `mcp`, `skills`) all worked correctly: $ claw agents No agents found. $ claw mcp MCP Working directory ... Configured servers 0 ## Root cause `CliAction::Plugins` exists, has a working dispatcher (`LiveCli::print_plugins`), and is produced inside the REPL via `SlashCommand::Plugins`. But the top-level CLI parser in `parse_subcommand()` had arms for `agents`, `mcp`, `skills`, `status`, `doctor`, `init`, `export`, `prompt`, etc., and **no arm for `plugins`**. The dispatch never ran from the CLI entry point. ## Changes ### rust/crates/rusty-claude-cli/src/main.rs Added a `"plugins"` arm to the top-level match in `parse_subcommand()` that produces `CliAction::Plugins { action, target, output_format }`, following the same positional convention as `mcp` (`action` = first positional, `target` = second). Rejects >2 positional args with a clear error. Added four regression assertions in the existing `parse_args` test: - `plugins` alone โ†’ `CliAction::Plugins { action: None, target: None }` - `plugins list` โ†’ action: Some("list"), target: None - `plugins enable ` โ†’ action: Some("enable"), target: Some(...) - `plugins --output-format json` โ†’ action: None, output_format: Json ### ROADMAP.md Added Pinpoint #145 documenting the gap, verification, root cause, fix shape, and acceptance. ## Live verification $ claw plugins # no credentials set Plugins example-bundled v0.1.0 disabled sample-hooks v0.1.0 disabled $ claw plugins --output-format json # no credentials set { "action": "list", "kind": "plugin", "message": "Plugins\n example-bundled ...\n sample-hooks ...", "reload_runtime": false, "target": null } Exit 0 in all modes. No network call. No "missing credentials" error. ## Tests - rusty-claude-cli bin: 177 tests pass (new plugin assertions included) - Full workspace green except pre-existing resume_latest flake (unrelated) Closes ROADMAP #145. --- ROADMAP.md | 58 ++++++++++++++++++++ rust/crates/rusty-claude-cli/src/main.rs | 70 ++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index a9f87c6..4f1e379 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -5520,3 +5520,61 @@ Exit 0. Full envelope with error surfaced. **Future phase (joins #143 Phase 2).** When typed-error taxonomy lands (ยง4.44), promote `config_load_error` from string to typed object across `doctor`, `status`, and `mcp` in one pass. **Source.** Jobdori dogfood 2026-04-21 18:59 KST on main HEAD `e2a43fc`. Joins **partial-success** cluster (#143, Principle #5) and **surface consistency** cluster. Session tally: ROADMAP #144. + +## Pinpoint #145. `claw plugins` subcommand not wired to CLI parser โ€” word gets treated as a prompt, hits Anthropic API + +**Gap.** `claw plugins` (and `claw plugins list`, `claw plugins --help`, `claw plugins info `, etc.) fall through the top-level subcommand match and get routed into the prompt-execution path. Result: a purely local introspection command triggers an Anthropic API call and surfaces `missing Anthropic credentials` to the user. With valid credentials, it would actually send the string `"plugins"` as a prompt to Claude, burning tokens for a local query. + +**Verified on main HEAD `faeaa1d` (2026-04-21 19:32 KST):** + +``` +$ claw plugins +error: missing Anthropic credentials; export ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY before calling the Anthropic API + +$ claw plugins --output-format json +{"error":"missing Anthropic credentials; ...","type":"error"} + +$ claw plugins --help +error: missing Anthropic credentials; ... + +$ claw plugins list +error: missing Anthropic credentials; ... + +$ ANTHROPIC_API_KEY=dummy claw plugins +โ ‹ ๐Ÿฆ€ Thinking... +โœ˜ โŒ Request failed +error: api returned 401 Unauthorized (authentication_error) +``` + +Compare `agents`, `mcp`, `skills` โ€” all recognized, all local, all exit 0: + +``` +$ claw agents +No agents found. +$ claw mcp +MCP + Working directory ... + Configured servers 0 +``` + +**Root cause.** In `rusty-claude-cli/src/main.rs`, the top-level `match rest[0].as_str()` parser has arms for `agents`, `mcp`, `skills`, `status`, `doctor`, `init`, `export`, `prompt`, etc., but **no arm for `plugins`**. The `CliAction::Plugins` variant exists, has a dispatcher (`print_plugins`), and is produced by `SlashCommand::Plugins` inside the REPL โ€” but the top-level CLI path was never wired. Result: `plugins` matches neither a known subcommand nor a slash path, so it falls through to the default "run as prompt" behavior. + +**Why this is a clawability gap.** +1. **Prompt misdelivery (explicit Clawhip category)**: the command string is sent to the LLM instead of dispatched locally. Real risk: without the credentials guard, `claw plugins` would send `"plugins"` as a user prompt to Claude, burning tokens. +2. **Surface asymmetry**: `plugins` is the only diagnostic-adjacent command that isn't wired. Documentation, slash command, and dispatcher all exist; parser wiring was missed. +3. **`--help` should never hit the network**. Anywhere. +4. **Misleading error**: user running `claw plugins` sees an Anthropic credential error. No hint that `plugins` wasn't a recognized subcommand. + +**Fix shape (~20 lines).** Add a `"plugins"` arm to the top-level parser in `main.rs` that produces `CliAction::Plugins { action, target, output_format }`, following the same positional convention as `mcp` (`action` = first positional, `target` = second). The existing `CliAction::Plugins` handler (`LiveCli::print_plugins`) already covers text and JSON. + +**Acceptance.** +- `claw plugins` exits 0 with plugins list (empty in a clean workspace, which is the honest state). +- `claw plugins --output-format json` emits `{"kind":"plugin","action":"list",...}` with exit 0. +- `claw plugins list` exits 0 and matches `claw plugins`. +- `claw plugins info ` resolves through the existing handler. +- No Anthropic network call occurs for any `plugins` invocation. +- Regression test: parse `["claw", "plugins"]`, assert `CliAction::Plugins { action: None, target: None, .. }`. + +**Blocker.** None. `CliAction::Plugins` already exists with a working dispatcher. + +**Source.** Jobdori dogfood 2026-04-21 19:30 KST on main HEAD `faeaa1d` in response to Clawhip nudge. Joins **prompt misdelivery** cluster. Session tally: ROADMAP #145. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 49905fd..90f9c58 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -661,6 +661,30 @@ fn parse_args(args: &[String]) -> Result { args: join_optional_args(&rest[1..]), output_format, }), + // #145: `plugins` was routed through the prompt fallback because no + // top-level parser arm produced CliAction::Plugins. That made `claw + // plugins` (and `claw plugins --help`, `claw plugins list`, ...) + // attempt an Anthropic network call, surfacing the misleading error + // `missing Anthropic credentials` even though the command is purely + // local introspection. Mirror `agents`/`mcp`/`skills`: action is the + // first positional arg, target is the second. + "plugins" => { + let tail = &rest[1..]; + let action = tail.first().cloned(); + let target = tail.get(1).cloned(); + if tail.len() > 2 { + return Err(format!( + "unexpected extra arguments after `claw plugins {}`: {}", + tail[..2].join(" "), + tail[2..].join(" ") + )); + } + Ok(CliAction::Plugins { + action, + target, + output_format, + }) + } "skills" => { let args = join_optional_args(&rest[1..]); match classify_skills_slash_command(args.as_deref()) { @@ -9549,6 +9573,52 @@ mod tests { output_format: CliOutputFormat::Text, } ); + // #145: `plugins` must parse as CliAction::Plugins (not fall through + // to the prompt path, which would hit the Anthropic API for a purely + // local introspection command). + assert_eq!( + parse_args(&["plugins".to_string()]).expect("plugins should parse"), + CliAction::Plugins { + action: None, + target: None, + output_format: CliOutputFormat::Text, + } + ); + assert_eq!( + parse_args(&["plugins".to_string(), "list".to_string()]) + .expect("plugins list should parse"), + CliAction::Plugins { + action: Some("list".to_string()), + target: None, + output_format: CliOutputFormat::Text, + } + ); + assert_eq!( + parse_args(&[ + "plugins".to_string(), + "enable".to_string(), + "example-bundled".to_string(), + ]) + .expect("plugins enable should parse"), + CliAction::Plugins { + action: Some("enable".to_string()), + target: Some("example-bundled".to_string()), + output_format: CliOutputFormat::Text, + } + ); + assert_eq!( + parse_args(&[ + "plugins".to_string(), + "--output-format".to_string(), + "json".to_string(), + ]) + .expect("plugins --output-format json should parse"), + CliAction::Plugins { + action: None, + target: None, + output_format: CliOutputFormat::Json, + } + ); } #[test]