Skip to content

Commit bb76ec9

Browse files
committed
ROADMAP ultraworkers#123: --allowedTools tool-name normalization asymmetric; snake_case canonicals accept variants, PascalCase canonicals reject snake_case; whitespace+comma split undocumented; allowed_tools not surfaced in JSON
Dogfooded 2026-04-18 on main HEAD 2bf2a11 from /tmp/cdZZ. Asymmetric normalization: normalize_tool_name(value) = trim + lowercase + replace -→_ Canonical 'read_file' (snake_case): accepts: read_file, READ_FILE, Read-File, read-file, Read (alias), read (alias) rejects: ReadFile, readfile, READFILE → Because normalize('ReadFile')='readfile', and name_map has key 'read_file' not 'readfile'. Canonical 'WebFetch' (PascalCase): accepts: WebFetch, webfetch, WEBFETCH rejects: web_fetch, web-fetch, Web-Fetch → Because normalize('WebFetch')='webfetch' (no underscore). User input 'web_fetch' normalizes to 'web_fetch' (keeps underscore). Keys don't match. The normalize function ADDS underscores (hyphen→underscore) but DOESN'T REMOVE them. So PascalCase canonicals have underscore- free normalized keys; user input with explicit underscores keeps them, creating key mismatch. Result: 'bash,Bash,BASH,Read,read_file,Read-File,WebFetch' all accepted, but 'web_fetch,web-fetch' rejected. Additional silent-flag issues: - Splits on commas OR whitespace (undocumented — help says TOOL[,TOOL...]) - 'bash,Bash,BASH' silently accepts all 3 case variants, no dedup warning - Allowed tools NOT in status/doctor JSON — claw passing --allowedTools has no way to verify what runtime accepted Trace: tools/src/lib.rs:192-244 normalize_allowed_tools: canonical_names from mvp_tool_specs + plugin_tools + runtime name_map: (normalize_tool_name(canonical), canonical) for token in value.split(|c| c==',' || c.is_whitespace()): lookup normalize_tool_name(token) in name_map tools/src/lib.rs:370-372 normalize_tool_name: fn normalize_tool_name(value: &str) -> String { value.trim().replace('-', '_').to_ascii_lowercase() } Replaces - with _. Lowercases. Does NOT remove _. Asymmetry source: normalize('WebFetch')='webfetch', normalize('web_fetch')='web_fetch'. Different keys. --allowedTools NOT plumbed into Status JSON output (no 'allowed_tools' field). Fix shape (~50 lines): - Symmetric normalization: strip underscores from both canonical and input, OR don't normalize hyphens in input either. Pick one convention. - claw tools list / --allowedTools help subcommand that prints canonical names + accepted variants. - Surface allowed_tools in status/doctor JSON when flag set. - Document comma+whitespace split semantics in --help. - Warn on duplicate tokens (bash,Bash,BASH = 3 tokens, 1 unique). - Regression per normalization pair + status surface + duplicate. Joins Silent-flag/documented-but-unenforced (ultraworkers#96-ultraworkers#101, ultraworkers#104, ultraworkers#108, ultraworkers#111, ultraworkers#115, ultraworkers#116, ultraworkers#117, ultraworkers#118, ultraworkers#119, ultraworkers#121, ultraworkers#122) as 16th. Joins Permission-audit/tool-allow-list (ultraworkers#94, ultraworkers#97, ultraworkers#101, ultraworkers#106, ultraworkers#115, ultraworkers#120) as 7th. Joins Truth-audit — status/doctor JSON hides what allowed-tools set actually is. Joins Parallel-entry-point asymmetry (ultraworkers#91, ultraworkers#101, ultraworkers#104, ultraworkers#105, ultraworkers#108, ultraworkers#114, ultraworkers#117, ultraworkers#122) as 9th — --allowedTools vs .claw.json permissions.allow likely disagree on normalization. Natural bundles: ultraworkers#97 + ultraworkers#123 — --allowedTools trust-gap pair: empty silently blocks (ultraworkers#97) + asymmetric normalization + invisible runtime state (ultraworkers#123) Permission-audit 7-way (grown): ultraworkers#94 + ultraworkers#97 + ultraworkers#101 + ultraworkers#106 + ultraworkers#115 + ultraworkers#120 + ultraworkers#123 Flagship permission-audit sweep 8-way (grown): ultraworkers#50 + ultraworkers#87 + ultraworkers#91 + ultraworkers#94 + ultraworkers#97 + ultraworkers#101 + ultraworkers#115 + ultraworkers#123 Filed in response to Clawhip pinpoint nudge 1494993419536306176 in #clawcode-building-in-public.
1 parent 2bf2a11 commit bb76ec9

1 file changed

Lines changed: 116 additions & 0 deletions

File tree

ROADMAP.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4290,3 +4290,119 @@ ear], /color [scheme], /effort [low|medium|high], /fast, /summary, /tag [label],
42904290
**Blocker.** None. Parser refactor is localized.
42914291

42924292
**Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdYY` on main HEAD `d1608ae` in response to Clawhip pinpoint nudge at `1494978319920136232`. Joins **Silent-flag / documented-but-unenforced** (#96–#101, #104, #108, #111, #115, #116, #117, #118, #119, #121) as 15th — `--base-commit` silently accepts garbage values. Joins **Parser-level trust gaps** via quartet → quintet: #108 (typo → Prompt), #117 (`-p` greedy), #119 (slash-verb + arg → Prompt), **#122** (`--base-commit` greedy consumes subcommand/flag). All four are parser-level "too eager" bugs. Joins **Parallel-entry-point asymmetry** (#91, #101, #104, #105, #108, #114, #117) as 8th — stale-base check is implemented for Prompt path but absent from Status/Doctor surfaces. Joins **Truth-audit / diagnostic-integrity** — warning message "expected base commit (doctor)" lies by including user's mistake as truth. Cross-cluster with **Unplumbed-subsystem** (#78, #96, #100, #102, #103, #107, #109, #111, #113, #121) — stale-base signal exists in runtime but not in JSON. Natural bundle: **Parser-level trust gap quintet (grown)**: #108 + #117 + #119 + #122 — billable-token silent-burn via parser too-eager consumption. Also **#100 + #122**: stale-base unplumbed (Jobdori #100) + `--base-commit` flag accepts anything (Jobdori #122). Complete stale-base-diagnostic-integrity coverage. Session tally: ROADMAP #122.
4293+
4294+
123. **`--allowedTools` tool name normalization is asymmetric: `normalize_tool_name` converts `-` → `_` and lowercases, but canonical names aren't normalized the same way, so tools with snake_case canonical (`read_file`) accept underscore + hyphen + lowercase variants (`read_file`, `READ_FILE`, `Read-File`, `read-file`, plus aliases `read`/`Read`), while tools with PascalCase canonical (`WebFetch`) REJECT snake_case variants (`web_fetch`, `web-fetch` both fail). A user or claw defensively writing `--allowedTools WebFetch,web_fetch` gets half the tools accepted and half rejected. The acceptance list mixes conventions: `bash`, `read_file`, `write_file` are snake_case; `WebFetch`, `WebSearch`, `TodoWrite`, `Skill`, `Agent` are PascalCase. Help doesn't explain which convention to use when. Separately: `--allowedTools` splits on BOTH commas AND whitespace (`Bash Read` parses as two tools), duplicate/case-variant tokens like `bash,Bash,BASH` are silently accepted with no dedup warning, and the allowed-tool set is NOT surfaced in `status` / `doctor` JSON output — a claw invoking with `--allowedTools` has no post-hoc way to verify what the runtime actually accepted** — dogfooded 2026-04-18 on main HEAD `2bf2a11` from `/tmp/cdZZ`.
4295+
4296+
**Concrete repro.**
4297+
```
4298+
# Full tool-name matrix — same conceptual tool, different spellings:
4299+
4300+
# For canonical "bash":
4301+
$ claw --allowedTools Bash status --output-format json | head -1
4302+
{ ... accepted
4303+
$ claw --allowedTools bash status --output-format json | head -1
4304+
{ ... accepted (case-insensitive)
4305+
$ claw --allowedTools BASH status --output-format json | head -1
4306+
{ ... accepted
4307+
4308+
# For canonical "read_file" (snake_case):
4309+
$ claw --allowedTools read_file status --output-format json | head -1
4310+
{ ... accepted (exact)
4311+
$ claw --allowedTools READ_FILE status | head -1
4312+
{ ... accepted (case-insensitive)
4313+
$ claw --allowedTools Read-File status | head -1
4314+
{ ... accepted (hyphen → underscore normalization)
4315+
$ claw --allowedTools Read status | head -1
4316+
{ ... accepted (alias "read" → "read_file")
4317+
$ claw --allowedTools ReadFile status | head -1
4318+
{"error":"unsupported tool in --allowedTools: ReadFile"} # REJECTED
4319+
4320+
# For canonical "WebFetch" (PascalCase):
4321+
$ claw --allowedTools WebFetch status | head -1
4322+
{ ... accepted (exact)
4323+
$ claw --allowedTools webfetch status | head -1
4324+
{ ... accepted (case-insensitive)
4325+
$ claw --allowedTools WEBFETCH status | head -1
4326+
{ ... accepted
4327+
$ claw --allowedTools web_fetch status | head -1
4328+
{"error":"unsupported tool in --allowedTools: web_fetch"} # REJECTED
4329+
$ claw --allowedTools web-fetch status | head -1
4330+
{"error":"unsupported tool in --allowedTools: web-fetch"} # REJECTED
4331+
4332+
# Separators: comma OR whitespace both work:
4333+
$ claw --allowedTools 'Bash,Read' status | head -1 # comma
4334+
{ ...
4335+
$ claw --allowedTools 'Bash Read' status | head -1 # whitespace
4336+
{ ...
4337+
$ claw --allowedTools 'Bash Read' status | head -1 # multiple whitespace
4338+
{ ...
4339+
# Documentation says: `--allowedTools TOOL[,TOOL...]`. Whitespace split is not documented.
4340+
4341+
# Duplicate/case-variant tokens silently accepted:
4342+
$ claw --allowedTools 'bash,Bash,BASH' status | head -1
4343+
{ ... # no dedup warning
4344+
4345+
# Allowed-tools NOT in status JSON:
4346+
$ claw --allowedTools Bash --output-format json status | jq 'keys'
4347+
["kind","model","permission_mode","sandbox","usage","workspace"]
4348+
# No "allowed_tools" field. No way to verify what the runtime is honoring.
4349+
```
4350+
4351+
**Trace path.**
4352+
- `rust/crates/tools/src/lib.rs:192-244` — `normalize_allowed_tools`:
4353+
```rust
4354+
let builtin_specs = mvp_tool_specs();
4355+
let canonical_names = builtin_specs.iter().map(|spec| spec.name.to_string())
4356+
.chain(self.plugin_tools.iter().map(|tool| tool.definition().name.clone()))
4357+
.chain(self.runtime_tools.iter().map(|tool| tool.name.clone()))
4358+
.collect::<Vec<_>>();
4359+
let mut name_map = canonical_names.iter()
4360+
.map(|name| (normalize_tool_name(name), name.clone()))
4361+
.collect::<BTreeMap<_, _>>();
4362+
for (alias, canonical) in [
4363+
("read", "read_file"),
4364+
("write", "write_file"),
4365+
("edit", "edit_file"),
4366+
("glob", "glob_search"),
4367+
("grep", "grep_search"),
4368+
] {
4369+
name_map.insert(alias.to_string(), canonical.to_string());
4370+
}
4371+
// ... split + lookup ...
4372+
for token in value.split(|ch: char| ch == ',' || ch.is_whitespace())...
4373+
```
4374+
- `rust/crates/tools/src/lib.rs:370-372` — `normalize_tool_name`:
4375+
```rust
4376+
fn normalize_tool_name(value: &str) -> String {
4377+
value.trim().replace('-', "_").to_ascii_lowercase()
4378+
}
4379+
```
4380+
Lowercases + replaces `-` with `_`. But **does NOT remove underscores**, so input with underscores retains them.
4381+
- **The asymmetry**: For canonical name `WebFetch`, `normalize_tool_name("WebFetch")` = `"webfetch"` (no underscore). For user input `web_fetch`, `normalize_tool_name("web_fetch")` = `"web_fetch"` (underscore preserved). These don't match in `name_map`.
4382+
- For canonical `read_file`, `normalize_tool_name("read_file")` = `"read_file"`. User input `Read-File` → `"read_file"`. These match.
4383+
- So snake_case canonical names tolerate hyphen/underscore/case variants; PascalCase canonical names reject any form with underscores.
4384+
- `--allowedTools` value NOT plumbed into `CliAction::Status` or `ResumeCommandOutcome` for `/status` — no `allowed_tools` or `allowedTools` field in the JSON output.
4385+
4386+
**Why this is specifically a clawability gap.**
4387+
1. *Asymmetric normalization creates unpredictable acceptance.* A claw defensively normalizing to snake_case (a common Rust/Python convention) gets half its tools accepted. A claw using PascalCase gets the other half.
4388+
2. *Help doesn't document the convention.* `--help` shows just `--allowedTools TOOL[,TOOL...]` without explaining that internal tool names mix conventions, or that hyphen-to-underscore normalization exists for some but not all.
4389+
3. *Whitespace-as-separator is undocumented.* Help says `TOOL[,TOOL...]` — commas only. Implementation accepts whitespace. A claw piping through `tr ',' ' '` to strip commas gets the same effect silently.
4390+
4. *Duplicate-with-case-variants silently accepted.* `bash,Bash,BASH` all normalize to the same canonical but produce no warning. A claw programmatically generating tool lists can bloat its input with case variants without the runtime pushing back.
4391+
5. *Allowed-tools not surfaced in status/doctor JSON.* Pass `--allowedTools Bash` and `status` gives no indication that only Bash is allowed. A claw preflighting a run cannot verify the runtime's view of what's allowed.
4392+
6. *Joins #97 (--allowedTools empty-string silently blocks all).* Same flag, different axis of silent-acceptance-without-surface-feedback. #97 + #123 are both trust-gap failures for the same surface.
4393+
7. *Joins parallel-entry-point asymmetry.* `.claw.json permissions.allow` vs `--allowedTools` flag — do they accept the same normalization? Worth separate sweep. If yes, the inconsistency is user-invisible in both; if no, users have to remember two separate conventions.
4394+
8. *Joins silent-flag / documented-but-unenforced.* Convention isn't documented; whitespace-separator isn't documented; duplicate tolerance isn't documented.
4395+
4396+
**Fix shape — symmetric normalization + surface to JSON + document.**
4397+
1. *Symmetric normalization.* Either (a) strip underscores from both canonical and input: `normalize_tool_name` = `trim + lowercase + replace('-|_', "")`, making `web_fetch`, `web-fetch`, `webfetch`, `WebFetch` all equivalent; or (b) don't normalize hyphens-to-underscores in the input either, so only exact-case-insensitive match works. Pick one. ~5 lines.
4398+
2. *Document the canonical name list.* Add a `claw tools list` or `--allowedTools help` subcommand that prints the canonical names + accepted variants. ~20 lines.
4399+
3. *Surface allowed_tools in status/doctor JSON.* Add top-level `allowed_tools: [...]` field when `--allowedTools` is provided. ~10 lines.
4400+
4. *Document the comma+whitespace split semantics.* Update `--help` to say `TOOL[,TOOL...|TOOL TOOL...]` or pick one convention. ~3 lines.
4401+
5. *Warn on duplicate tokens.* If normalize-map deduplicates 3 → 1 silently, emit structured warning. ~8 lines.
4402+
6. *Regression tests.* (a) Symmetric normalization matrix: every (canonical, variant) pair accepts or rejects consistently. (b) Status JSON includes allowed_tools when flag set. (c) Duplicate-token warning.
4403+
4404+
**Acceptance.** `--allowedTools WebFetch` and `--allowedTools web_fetch` both accept/reject the same way. `claw status --output-format json` with `--allowedTools Bash` shows `allowed_tools: ["bash"]` in the JSON. `--help` documents the separator and normalization rules.
4405+
4406+
**Blocker.** None. Localized in `rust/crates/tools/src/lib.rs:370` + status/doctor JSON plumbing.
4407+
4408+
**Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdZZ` on main HEAD `2bf2a11` in response to Clawhip pinpoint nudge at `1494993419536306176`. Joins **Silent-flag / documented-but-unenforced** (#96–#101, #104, #108, #111, #115, #116, #117, #118, #119, #121, #122) as 16th member — `--allowedTools` has undocumented whitespace-separator behavior, undocumented normalization asymmetry, and silent duplicate-acceptance. Joins **Permission-audit / tool-allow-list** (#94, #97, #101, #106, #115, #120) as 7th — asymmetric normalization means claw allow-lists don't round-trip cleanly between canonical representations. Joins **Truth-audit / diagnostic-integrity** — status/doctor JSON hides what the allowed-tools set actually is. Joins **Parallel-entry-point asymmetry** (#91, #101, #104, #105, #108, #114, #117, #122) as 9th — `--allowedTools` vs `.claw.json permissions.allow` are two entry points that likely disagree on normalization (worth separate sweep). Natural bundle: **#97 + #123** — `--allowedTools` trust-gap pair: empty silently blocks (#97) + asymmetric normalization + invisible runtime state (#123). Also **Flagship permission-audit sweep 8-way (grown)**: #50 + #87 + #91 + #94 + #97 + #101 + #115 + **#123**. Also **Permission-audit 7-way (grown)**: #94 + #97 + #101 + #106 + #115 + #120 + **#123**. Session tally: ROADMAP #123.

0 commit comments

Comments
 (0)