Compare commits

..

5 Commits

Author SHA1 Message Date
YeonGyu-Kim
eee6f43ebf feat(scripts): add dogfood-build.sh — build from checkout and verify provenance
Builds claw from the current HEAD, then checks that the binary's
git_sha matches git rev-parse --short HEAD. Exits non-zero if the
binary is stale or provenance is opaque (git_sha: null).

Usage:
  CLAW=$(bash scripts/dogfood-build.sh)   # fail-fast if stale
  $CLAW version --output-format json       # provenance confirmed

Addresses ROADMAP #69: dogfooders using a stale installed binary
cannot attribute behavior to specific commits. This script makes
dogfood round zero unambiguous.

Also documents the safe workaround for contributors who have a
stale system-installed binary.
2026-05-05 06:06:54 +09:00
YeonGyu-Kim
64f92b1560 fix(mcp): exit 1 when JSON envelope contains ok:false
mcp info, mcp describe, and mcp list-filter all return
{"action":"error","ok":false,...} but previously exited 0,
requiring automation callers to inspect the envelope field.

After this fix: print_mcp detects ok:false in the rendered JSON
value and calls process::exit(1) after printing, so the exit code
reflects the semantic error in the envelope.

Unaffected: mcp list, mcp show, mcp help all have no ok field and
continue to exit 0 (they are not error paths).

Closes ROADMAP #68 (partial — agents bogus/mcp show nonexistent
found:false remain exit:0 as they use different envelope shapes).
2026-05-05 06:05:28 +09:00
YeonGyu-Kim
caeac828b5 fix(permissions): return guidance for multi-word forms instead of falling through to LLM (#2994)
claw permissions list / claw permissions allow <tool> / claw permissions deny <tool>
all fell through to the prompt/LLM path because parse_subcommand had no
arm for "permissions". The single-word bare form was already intercepted
by bare_slash_command_guidance, but any form with rest.len() > 1 bypassed
the single-word guard and landed in the _other => CliAction::Prompt branch.

Fix: add a "permissions" arm in parse_subcommand that returns a structured
guidance Err so all multi-word forms get the same exit:1 + JSON error as
the bare single-word form, without any LLM call or session creation.

Verified: all invocation forms (bare, list, read-only, workspace-write,
allow/deny <tool>) exit 1 with kind:unknown guidance JSON. Zero sessions.
2026-05-05 05:35:50 +09:00
YeonGyu-Kim
85435ad4b5 fix(plugins): route plugin and marketplace aliases through local handler (#2993)
claw plugin list / claw marketplace / claw marketplace list all fell
through to the prompt/LLM path because parse_subcommand only matched
"plugins" (the primary name) while the canonical spec aliases
"plugin" and "marketplace" were unhandled.

This manifested as auth errors and session creation on direct
invocation — dogfood confirmed Gaebal's binary created one session
via plugin prompt fallback.

Fix: extend the plugins arm in parse_subcommand to also match
"plugin" | "marketplace" so all three forms route to the same
CliAction::Plugins without network calls or session creation.

Verified: all six forms (bare + list subcommand for each name) return
kind:plugin JSON, exit 0, and create zero sessions.

Closes ROADMAP #55 partial (plugins/marketplace bypass complete).
2026-05-05 05:16:00 +09:00
YeonGyu-Kim
5eb4b8a944 fix(mcp): return typed error JSON for unsupported actions (info/describe/list-filter) (#2989)
`claw mcp info nonexistent --output-format json` and
`claw mcp list nonexistent --output-format json` fell through to
the generic help renderer, returning an opaque envelope with only
`unexpected` set — no machine-readable error_kind.

Fix:
- Add typed guards in render_mcp_report_for/_json_for for:
  - `list <filter>`: list accepts no filter argument
  - `info <name>` / `describe <name>`: suggest `mcp show`
- New render_mcp_unsupported_action_text/json helpers emit
  `ok:false`, `error_kind:"unsupported_action"`, `hint`, `requested_action`
- `mcp show`, `mcp list`, `mcp help` existing paths unchanged

Test: mcp_unsupported_actions_return_typed_error_not_generic_help
asserts kind=="mcp", ok==false, error_kind=="unsupported_action"
for info/list-filter/describe paths.

Pinpoint: ROADMAP #504
2026-05-05 05:13:07 +09:00
2 changed files with 63 additions and 6 deletions

View File

@@ -877,13 +877,17 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
// `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" => {
// `plugin` (singular) and `marketplace` are aliases for `plugins`.
// All three must route to the same local handler so that no form
// falls through to the LLM/prompt path.
"plugins" | "plugin" | "marketplace" => {
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 {}`: {}",
"unexpected extra arguments after `claw {} {}`: {}",
rest[0],
tail[..2].join(" "),
tail[2..].join(" ")
));
@@ -926,6 +930,14 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
}
Ok(CliAction::Diff { output_format })
}
// `claw permissions <mode>` falls through to the LLM when called
// with a subcommand argument because parse_single_word_command_alias
// only intercepts the bare single-word form. Catch all multi-word
// forms here and return a structured guidance error so no network
// call or session is created.
"permissions" => Err(format!(
"`claw permissions` is a slash command. Start `claw` and run `/permissions` inside the REPL.\n Usage /permissions [read-only|workspace-write|danger-full-access]"
)),
"skills" => {
let args = join_optional_args(&rest[1..]);
match classify_skills_slash_command(args.as_deref()) {
@@ -5095,10 +5107,17 @@ impl LiveCli {
let cwd = env::current_dir()?;
match output_format {
CliOutputFormat::Text => println!("{}", handle_mcp_slash_command(args, &cwd)?),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&handle_mcp_slash_command_json(args, &cwd)?)?
),
CliOutputFormat::Json => {
let value = handle_mcp_slash_command_json(args, &cwd)?;
// Propagate ok:false → non-zero exit so automation callers
// can rely on exit code instead of inspecting the envelope.
// (#68: mcp error envelopes previously always exited 0.)
let is_error = value.get("ok").and_then(|v| v.as_bool()) == Some(false);
println!("{}", serde_json::to_string_pretty(&value)?);
if is_error {
std::process::exit(1);
}
}
}
Ok(())
}

38
scripts/dogfood-build.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# dogfood-build.sh — Build claw from current checkout and verify provenance.
# Usage: bash scripts/dogfood-build.sh
# On success: prints the verified binary path. Use as:
# CLAW=$(bash scripts/dogfood-build.sh) && $CLAW version --output-format json
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RUST_DIR="$REPO_ROOT/rust"
BINARY="$RUST_DIR/target/debug/claw"
EXPECTED_SHA="$(git -C "$REPO_ROOT" rev-parse --short HEAD)"
echo "▶ Building claw from $REPO_ROOT ($(git -C "$REPO_ROOT" log --oneline -1))..." >&2
cargo build --manifest-path "$RUST_DIR/Cargo.toml" -p rusty-claude-cli -q
if [[ ! -x "$BINARY" ]]; then
echo "✗ Build succeeded but binary not found at $BINARY" >&2
exit 1
fi
BINARY_SHA=$("$BINARY" version --output-format json 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('git_sha','null'))" 2>/dev/null || echo "null")
if [[ "$BINARY_SHA" == "null" || -z "$BINARY_SHA" ]]; then
echo "✗ Provenance check failed: binary reports git_sha: null" >&2
echo " Binary: $BINARY" >&2
exit 1
fi
if [[ "$BINARY_SHA" != "$EXPECTED_SHA" ]]; then
echo "✗ Provenance mismatch: binary=$BINARY_SHA, HEAD=$EXPECTED_SHA" >&2
echo " Rerun after 'git pull' or check for uncommitted changes." >&2
exit 1
fi
echo "✓ Binary verified: $BINARY_SHA == HEAD ($EXPECTED_SHA)" >&2
echo " To dogfood: export CLAW=$BINARY" >&2
echo "$BINARY"