Batched findings for AI agent: tools, permissions, residency from the pre-v0.15 codebase audit (branch audit/pre-v015-review).
Each was produced by an end-to-end capability trace, then independently re-verified by a reviewer whose
brief was to refute it. Only findings that survived refutation appear here, at the verifier's corrected
severity — across the audit, 102 claims became 89 confirmed and 29 were downgraded.
4 findings — 4 medium, 0 low. Tick them off individually; split any one
out into its own issue if it needs real design work.
1. Byte-index string slicing panics on non-ASCII input — including on the editor and mae-agent startup path
bug · medium
Impact. mae and mae-agent abort at startup for any user whose project CLAUDE.md/README.md exceeds 8000 bytes and has a multi-byte character straddling byte 8000 — i.e. essentially any non-English or typographically-rich README. On the shell path, any command emitting >10KB of output containing a multi-byte char at byte 10000 (e.g. cargo build with —/→ in rustc diagnostics, ls over Unicode filenames) panics the AI task. Secondary #7 issue: PROJECT_CONTEXT_MAX_CHARS = 8000 (guidance.rs:17) is an undocumented magic constant even though the sibling budget for the same content IS an option (ai_guidance_inline_budget_chars, crates/core/src/options.rs:513-521).
Evidence
crates/ai/src/guidance.rs:46-49:
let truncated = if content.len() > PROJECT_CONTEXT_MAX_CHARS {
format!("{}...\n[truncated]", &content[..PROJECT_CONTEXT_MAX_CHARS])
content.len() is bytes; &content[..8000] panics if byte 8000 is not a char boundary. Scanning this repo's own markdown, three tracked files would trigger it if used as project context (docs/v0.15-two-machine-test-plan.md, byte 8000 = 0x86; docs/adr/063-guidance-delivery-uniformity.md, 0x80; docs/adr/067-admin-enforced-live-query-only-kb-access.md, 0x94 — all UTF-8 continuation bytes). Callers are on startup paths, outside any catch_unwind: crates/mae/src/main.rs:881 (MCP initialize instructions, during editor boot), crates/agent-cli/src/main.rs:214 (mae-agent startup, the ADR-049 default surface), crates/mae/src/bootstrap.rs:923 (system prompt).
Same defect at four more sites: crates/ai/src/session/run_loop.rs:77 &stdout[..10_000] and :85 &stderr[..5_000] (comment says "Truncate to 10k chars" but slices bytes), crates/ai/src/session/handle_prompt.rs:738 &result.output[..200] on fetched web page text, and crates/ai/src/executor/shell_exec.rs:135,143 (same two slices duplicated). The run_loop.rs/handle_prompt.rs sites run on the AI task thread, not inside catch_tool_panic (tool_dispatch.rs:617).
Verification
guidance.rs:46-49 does &content[..PROJECT_CONTEXT_MAX_CHARS] on a byte index. Callers: bootstrap.rs:923, agent-cli/src/main.rs:214, main.rs:879-885. No panic=abort, no catch_unwind. Sibling sites real: run_loop.rs:77/85, handle_prompt.rs:738, shell_exec.rs:135/143. PROJECT_CONTEXT_MAX_CHARS magic constant vs registered ai_guidance_inline_budget_chars option is a principle-#7 inconsistency.
Scope correction from verification: Two evidence claims are WRONG and must be dropped: (1) the cited docs/ files are never read - PROJECT_CONTEXT_FILES is [CLAUDE.md, README.md, README.org, .project] in cwd only; this repo's CLAUDE.md and README.md both have char boundaries at byte 8000, so mae does NOT abort at startup here. (2) 'any non-English README' overstates: needs a multibyte char straddling exactly byte 8000. Also run_loop/handle_prompt sites are on a spawned task - they kill the AI session, not the process. Only guidance.rs is process-fatal.
2. Failures reported as success: true — Scheme eval errors and invalid tool actions
bug · medium
Impact. The tool-result success flag is what the model (and crates/ai/src/session/progress.rs's stagnation scorer) uses to decide whether to retry or move on. A model that writes broken Scheme is told it worked and proceeds on a false premise; progress.rs counts the failed call as a success and under-reports stagnation. Worse on the flip side: because the drain happens after exec_result is computed, any leftover pending_scheme_eval entry (queued by a prior keybinding, hook, or run-build) overwrites the CURRENT tool's result — including a Permission denied or residency Deny result — and flips it to success: true.
Evidence
Both dispatch handlers force success after draining a Scheme eval. crates/mae/src/ai_event_handler.rs:203-206 (embedded) and :1034-1037 (MCP) are identical:
if let Some(output) = scheme_output {
result.output = output;
result.success = true;
}
drain_pending_scheme_evals (:1470-1487) returns Some(...) for errors too — eval_with_yield_handling (:1500-1503) returns the error as an ordinary string: Err(e) => return format!("; error: {}", e.message). So eval_scheme with a syntax error or unbound variable returns success: true with body "; error: …".
Separately, crates/ai/src/executor/tool_dispatch.rs:321-331 and :445-455 build error strings and then return them as success:
None => "Missing 'results' array for grade action".to_string(),
},
_ => "Invalid action: use 'plan' or 'grade'".to_string(),
};
return ExecuteResult::Immediate(ToolResult { ..., success: true, output });
Verification
Both mechanisms verified. crates/mae/src/ai_event_handler.rs:203-206 (embedded) and :1033-1037 (MCP) both do if let Some(output) = scheme_output { result.output = output; result.success = true; }. drain_pending_scheme_evals (:1470-1487) returns Some(...) whenever pending_scheme_eval was non-empty, regardless of outcome, and eval_with_yield_handling (:1500-1503) returns errors as an ordinary string: Err(e) => return format!("; error: {}", e.message). So eval_scheme on a syntax error or unbound variable yields success: true with a ; error: ... body. crates/ai/src/executor/tool_dispatch.rs:321-331 and :445-455 likewise build "Missing 'results' array for grade action" / "Invalid action: use 'plan' or 'grade'" and then return ExecuteResult::Immediate(ToolResult { ..., success: true, output }). crates/ai/src/session/progress.rs does consume the success flag for its stagnation scoring, so the downstream consequence is real.
The one part I could not confirm is the 'flip side': the claim that a leftover pending_scheme_eval from an unrelated keybinding/hook can overwrite the current tool's Deny result. Other producers do exist (scheme_ops.rs:26/50/62, dispatch/mod.rs:350, babel_ops.rs:126), and the drain is unconditional and after exec_result, so it is structurally possible — but it depends on event-loop interleaving I did not reproduce, so treat it as plausible rather than demonstrated.
Scope correction from verification: Narrower accurate claim: eval_scheme errors and the two model_exam argument-validation errors are reported with success: true (ai_event_handler.rs:203-206/:1033-1037, tool_dispatch.rs:321-331/:445-455). The stated escalation — a stale queued eval clobbering a Permission denied/residency Deny result — is structurally possible given the unconditional post-exec_result drain but was not demonstrated.
3. Two divergent shell_exec implementations with a copy-pasted security blocklist, a dead timeout_ms parameter, and sandbox coverage on only one
duplication · medium
Impact. A model that follows the advertised schema and passes timeout_ms: 5000 gets the 30-second default silently — a failure mode that presents as a hung agent, and the tool contract lies. Any future hardening of the blocklist must be landed twice or one caller silently keeps the weaker rules — precisely the third-parallel-implementation pattern principle #15 forbids. And because the embedded session bypasses sandbox_guard, self_test_suite's sandbox mode does not actually confine shell writes for the built-in agent. A related stale duplicate sits in crates/agent-cli/src/residency_check.rs:20-38, whose doc-comment points at crates/mae/src/ai_residency.rs::SINGLE_TARGET_KB_TOOLS — a constant deliberately deleted (see ai_residency.rs:14-26, which names those flat arrays as the root cause of #350/#351) — and whose lists omit every tool classified since (kb_history, kb_create, kb_promote, kb_graph, kb_health, kb_raw_query, …).
Evidence
crates/ai/src/executor/shell_exec.rs:82-84 states the duplication outright:
// Same blocklist as session's async version. Defense in depth, not a
// sandbox — substring-based and bypassable; see SECURITY.md.
let blocked_patterns = ["rm -rf /", "rm -fr /", "mkfs.", "dd if=", ":(){", ">(){ :"];
the original being crates/ai/src/session/run_loop.rs:35-38. The two copies already differ in formatting and each re-implements timeout/truncation independently (shell_exec.rs:94-98,110-126,132-148 vs run_loop.rs:51-56,58-65,73-90).
The declared schema does not match either implementation: crates/ai/src/tools/shell_tools.rs:13 advertises .prop("timeout_ms", "integer", "Timeout in milliseconds (default: 30000)"), while both impls read a different key — run_loop.rs:53 args.get("timeout_secs") and shell_exec.rs:95 args.get("timeout_secs"). No code path reads timeout_ms.
Sandbox confinement is applied only in the executor copy (shell_exec.rs:27-39 plus tool_dispatch.rs:819-825 sandbox_guard); the session copy has no sandbox check at all.
Verification
All three sub-claims verified. (1) The blocklist is duplicated and the code says so — crates/ai/src/executor/shell_exec.rs:82-84 "// Same blocklist as session's async version." with ["rm -rf /", "rm -fr /", "mkfs.", "dd if=", ":(){", ">(){ :"], mirroring crates/ai/src/session/run_loop.rs:35-38; the two also independently re-implement timeout (std blocking poll loop vs tokio::time::timeout) and truncation (10_000/5_000 in both, so they agree today but nothing holds them together). These can diverge observably: any hardening of one blocklist leaves the other caller running the weaker rules. (2) The schema/impl mismatch is real — crates/ai/src/tools/shell_tools.rs:13 advertises .prop("timeout_ms", "integer", "Timeout in milliseconds (default: 30000)") and a repo-wide grep for timeout_ms in crates/ai/src returns that single line; both implementations read timeout_secs (run_loop.rs:52, shell_exec.rs:95). A model passing timeout_ms: 5000 silently gets 30s. (3) Sandbox asymmetry is real — the executor copy filters via super::sandbox::filter_shell_command (shell_exec.rs:44-62) plus sandbox_guard's "shell_exec" arm (tool_dispatch.rs:819-825), while the embedded session's execute_shell (run_loop.rs:17+) is invoked directly from handle_prompt.rs:698-709 with no sandbox check at all. The crates/agent-cli/src/residency_check.rs:20-38 sub-claim is also literally correct: its doc-comment cites ai_residency.rs::SINGLE_TARGET_KB_TOOLS, a constant crates/mae/src/ai_residency.rs:14-26 explicitly names as the deleted root cause of #350/#351, and its lists omit kb_history/kb_create/kb_promote/kb_graph/kb_health/kb_raw_query. That last one is a doc-drift nit only — the same file documents itself as "Deliberately coarser", "best-effort, cheap early exit, not a substitute", and the authoritative server-side gate now fails closed on unclassified tools.
4. Nine AI tools are advertised over MCP but undispatchable — ask_user is in the default Core tool list
parity-gap · medium
Impact. A paired external agent (VS Code Copilot, Claude Code via the shim — the v0.15 headline use case) sees ask_user in its very first tools/list, calls it to ask the human a question, and gets Unknown tool: ask_user back. mod_tests.rs:1418 every_registered_tool_annotation_matches_its_permission_tier iterates the whole registry but only checks annotation/tier consistency — there is no test asserting every advertised tool is reachable through dispatch_tool, which is the #14 gap that let this ship. Related surface asymmetry: crates/core/src/editor/dispatch/ui.rs:218 hardcodes the human's profile palette as 4 profiles, omitting verifier, which AI_PROFILES (crates/ai/src/tools/mod.rs:26-32) offers the AI and bootstrap.rs:896 has a prompt for.
Evidence
ai_set_mode, ai_set_profile, ai_set_budget, delegate, ask_user, propose_changes, log_activity, read_transcript, web_fetch are handled ONLY inside crates/ai/src/session/handle_prompt.rs (:676,731,753,779,808,840,870,925,982). Grepping those nine names across crates/ai/src/executor/ and crates/ai/src/tool_impls/ returns zero non-test hits — no category dispatcher in dispatch_tool (tool_dispatch.rs:649-704) claims them, so an external MCP call falls through to Err(format!("Unknown tool: {}", call.name)) (tool_dispatch.rs:703). They are nevertheless in ai_specific_tools and therefore in all_tools and the tools/list payload (crates/mae/src/main.rs:806-844). ask_user is explicitly classified Core tier (crates/ai/src/tools/categories.rs:78), so it is in the DEFAULT tiered advertisement (mcp_tools_tiered_by_default = true, main.rs:801-814).
Verification
Verified name by name. Each of ai_set_mode, ai_set_profile, ai_set_budget, delegate, ask_user, propose_changes, log_activity, read_transcript, web_fetch is intercepted only inside crates/ai/src/session/handle_prompt.rs (:676, :731, :753, :779, :808, :840, :870, :925, :982) — grepping all nine across crates/ai/src/executor/ and crates/ai/src/tool_impls/ yields no non-test dispatch site. dispatch_tool (tool_dispatch.rs:649-704) tries eight category dispatchers, three perf names, the command_ prefix and editor.ai.scheme_tools, then falls through to Err(format!("Unknown tool: {}", call.name)) at :703. The MCP path reaches it: handle_mcp_request (ai_event_handler.rs:862+) builds a fake_call and routes to the same executor. ask_user is registered in ai_specific_tools (core_tools.rs:228-235, PermissionTier::ReadOnly) and listed in classify_tool_tier's Core arm (categories.rs:78), and main.rs:801-814 filters tools/list to Core when mcp_tools_tiered_by_default is true (default true) — so it is in an external client's very first tools/list and returns Unknown tool: ask_user when called. The interactive AskUser/ProposeChanges handling at ai_event_handler.rs:450/:498 is on the embedded AiEvent path only, not the MCP tool path. The AI_PROFILES sub-claim also holds: crates/ai/src/tools/mod.rs:26-32 lists five profiles including "verifier", while crates/core/src/editor/dispatch/ui.rs:218 hardcodes vec!["pair-programmer", "explorer", "planner", "reviewer"]. Not tracked in any open issue; #375's ADR-051 phase covers per-session policy, not advertise/dispatch consistency.
Batched findings for AI agent: tools, permissions, residency from the pre-v0.15 codebase audit (branch
audit/pre-v015-review).Each was produced by an end-to-end capability trace, then independently re-verified by a reviewer whose
brief was to refute it. Only findings that survived refutation appear here, at the verifier's corrected
severity — across the audit, 102 claims became 89 confirmed and 29 were downgraded.
4 findings — 4 medium, 0 low. Tick them off individually; split any one
out into its own issue if it needs real design work.
1. Byte-index string slicing panics on non-ASCII input — including on the editor and mae-agent startup path
bug· mediumImpact.
maeandmae-agentabort at startup for any user whose projectCLAUDE.md/README.mdexceeds 8000 bytes and has a multi-byte character straddling byte 8000 — i.e. essentially any non-English or typographically-rich README. On the shell path, any command emitting >10KB of output containing a multi-byte char at byte 10000 (e.g.cargo buildwith—/→in rustc diagnostics,lsover Unicode filenames) panics the AI task. Secondary #7 issue:PROJECT_CONTEXT_MAX_CHARS = 8000(guidance.rs:17) is an undocumented magic constant even though the sibling budget for the same content IS an option (ai_guidance_inline_budget_chars,crates/core/src/options.rs:513-521).Evidence
crates/ai/src/guidance.rs:46-49:content.len()is bytes;&content[..8000]panics if byte 8000 is not a char boundary. Scanning this repo's own markdown, three tracked files would trigger it if used as project context (docs/v0.15-two-machine-test-plan.md, byte 8000 = 0x86;docs/adr/063-guidance-delivery-uniformity.md, 0x80;docs/adr/067-admin-enforced-live-query-only-kb-access.md, 0x94 — all UTF-8 continuation bytes). Callers are on startup paths, outside anycatch_unwind:crates/mae/src/main.rs:881(MCPinitializeinstructions, during editor boot),crates/agent-cli/src/main.rs:214(mae-agent startup, the ADR-049 default surface),crates/mae/src/bootstrap.rs:923(system prompt).Same defect at four more sites:
crates/ai/src/session/run_loop.rs:77&stdout[..10_000]and:85&stderr[..5_000](comment says "Truncate to 10k chars" but slices bytes),crates/ai/src/session/handle_prompt.rs:738&result.output[..200]on fetched web page text, andcrates/ai/src/executor/shell_exec.rs:135,143(same two slices duplicated). Therun_loop.rs/handle_prompt.rssites run on the AI task thread, not insidecatch_tool_panic(tool_dispatch.rs:617).Verification
guidance.rs:46-49 does &content[..PROJECT_CONTEXT_MAX_CHARS] on a byte index. Callers: bootstrap.rs:923, agent-cli/src/main.rs:214, main.rs:879-885. No panic=abort, no catch_unwind. Sibling sites real: run_loop.rs:77/85, handle_prompt.rs:738, shell_exec.rs:135/143. PROJECT_CONTEXT_MAX_CHARS magic constant vs registered ai_guidance_inline_budget_chars option is a principle-#7 inconsistency.
2. Failures reported as
success: true— Scheme eval errors and invalid tool actionsbug· mediumImpact. The tool-result
successflag is what the model (andcrates/ai/src/session/progress.rs's stagnation scorer) uses to decide whether to retry or move on. A model that writes broken Scheme is told it worked and proceeds on a false premise;progress.rscounts the failed call as a success and under-reports stagnation. Worse on the flip side: because the drain happens afterexec_resultis computed, any leftoverpending_scheme_evalentry (queued by a prior keybinding, hook, orrun-build) overwrites the CURRENT tool's result — including aPermission deniedor residencyDenyresult — and flips it tosuccess: true.Evidence
Both dispatch handlers force success after draining a Scheme eval.
crates/mae/src/ai_event_handler.rs:203-206(embedded) and:1034-1037(MCP) are identical:drain_pending_scheme_evals(:1470-1487) returnsSome(...)for errors too —eval_with_yield_handling(:1500-1503) returns the error as an ordinary string:Err(e) => return format!("; error: {}", e.message). Soeval_schemewith a syntax error or unbound variable returnssuccess: truewith body"; error: …".Separately,
crates/ai/src/executor/tool_dispatch.rs:321-331and:445-455build error strings and then return them as success:Verification
Both mechanisms verified.
crates/mae/src/ai_event_handler.rs:203-206(embedded) and :1033-1037 (MCP) both doif let Some(output) = scheme_output { result.output = output; result.success = true; }.drain_pending_scheme_evals(:1470-1487) returnsSome(...)wheneverpending_scheme_evalwas non-empty, regardless of outcome, andeval_with_yield_handling(:1500-1503) returns errors as an ordinary string:Err(e) => return format!("; error: {}", e.message). Soeval_schemeon a syntax error or unbound variable yieldssuccess: truewith a; error: ...body.crates/ai/src/executor/tool_dispatch.rs:321-331and :445-455 likewise build"Missing 'results' array for grade action"/"Invalid action: use 'plan' or 'grade'"and thenreturn ExecuteResult::Immediate(ToolResult { ..., success: true, output }).crates/ai/src/session/progress.rsdoes consume the success flag for its stagnation scoring, so the downstream consequence is real.The one part I could not confirm is the 'flip side': the claim that a leftover
pending_scheme_evalfrom an unrelated keybinding/hook can overwrite the current tool's Deny result. Other producers do exist (scheme_ops.rs:26/50/62,dispatch/mod.rs:350,babel_ops.rs:126), and the drain is unconditional and afterexec_result, so it is structurally possible — but it depends on event-loop interleaving I did not reproduce, so treat it as plausible rather than demonstrated.3. Two divergent
shell_execimplementations with a copy-pasted security blocklist, a deadtimeout_msparameter, and sandbox coverage on only oneduplication· mediumImpact. A model that follows the advertised schema and passes
timeout_ms: 5000gets the 30-second default silently — a failure mode that presents as a hung agent, and the tool contract lies. Any future hardening of the blocklist must be landed twice or one caller silently keeps the weaker rules — precisely the third-parallel-implementation pattern principle #15 forbids. And because the embedded session bypassessandbox_guard,self_test_suite's sandbox mode does not actually confine shell writes for the built-in agent. A related stale duplicate sits incrates/agent-cli/src/residency_check.rs:20-38, whose doc-comment points atcrates/mae/src/ai_residency.rs::SINGLE_TARGET_KB_TOOLS— a constant deliberately deleted (seeai_residency.rs:14-26, which names those flat arrays as the root cause of #350/#351) — and whose lists omit every tool classified since (kb_history,kb_create,kb_promote,kb_graph,kb_health,kb_raw_query, …).Evidence
crates/ai/src/executor/shell_exec.rs:82-84states the duplication outright:the original being
crates/ai/src/session/run_loop.rs:35-38. The two copies already differ in formatting and each re-implements timeout/truncation independently (shell_exec.rs:94-98,110-126,132-148vsrun_loop.rs:51-56,58-65,73-90).The declared schema does not match either implementation:
crates/ai/src/tools/shell_tools.rs:13advertises.prop("timeout_ms", "integer", "Timeout in milliseconds (default: 30000)"), while both impls read a different key —run_loop.rs:53args.get("timeout_secs")andshell_exec.rs:95args.get("timeout_secs"). No code path readstimeout_ms.Sandbox confinement is applied only in the executor copy (
shell_exec.rs:27-39plustool_dispatch.rs:819-825 sandbox_guard); the session copy has no sandbox check at all.Verification
All three sub-claims verified. (1) The blocklist is duplicated and the code says so —
crates/ai/src/executor/shell_exec.rs:82-84"// Same blocklist as session's async version." with["rm -rf /", "rm -fr /", "mkfs.", "dd if=", ":(){", ">(){ :"], mirroringcrates/ai/src/session/run_loop.rs:35-38; the two also independently re-implement timeout (std blocking poll loop vstokio::time::timeout) and truncation (10_000/5_000 in both, so they agree today but nothing holds them together). These can diverge observably: any hardening of one blocklist leaves the other caller running the weaker rules. (2) The schema/impl mismatch is real —crates/ai/src/tools/shell_tools.rs:13advertises.prop("timeout_ms", "integer", "Timeout in milliseconds (default: 30000)")and a repo-wide grep fortimeout_msincrates/ai/srcreturns that single line; both implementations readtimeout_secs(run_loop.rs:52, shell_exec.rs:95). A model passingtimeout_ms: 5000silently gets 30s. (3) Sandbox asymmetry is real — the executor copy filters viasuper::sandbox::filter_shell_command(shell_exec.rs:44-62) plussandbox_guard's"shell_exec"arm (tool_dispatch.rs:819-825), while the embedded session'sexecute_shell(run_loop.rs:17+) is invoked directly from handle_prompt.rs:698-709 with no sandbox check at all. Thecrates/agent-cli/src/residency_check.rs:20-38sub-claim is also literally correct: its doc-comment citesai_residency.rs::SINGLE_TARGET_KB_TOOLS, a constantcrates/mae/src/ai_residency.rs:14-26explicitly names as the deleted root cause of #350/#351, and its lists omit kb_history/kb_create/kb_promote/kb_graph/kb_health/kb_raw_query. That last one is a doc-drift nit only — the same file documents itself as "Deliberately coarser", "best-effort, cheap early exit, not a substitute", and the authoritative server-side gate now fails closed on unclassified tools.4. Nine AI tools are advertised over MCP but undispatchable —
ask_useris in the default Core tool listparity-gap· mediumImpact. A paired external agent (VS Code Copilot, Claude Code via the shim — the v0.15 headline use case) sees
ask_userin its very firsttools/list, calls it to ask the human a question, and getsUnknown tool: ask_userback.mod_tests.rs:1418 every_registered_tool_annotation_matches_its_permission_tieriterates the whole registry but only checks annotation/tier consistency — there is no test asserting every advertised tool is reachable throughdispatch_tool, which is the #14 gap that let this ship. Related surface asymmetry:crates/core/src/editor/dispatch/ui.rs:218hardcodes the human's profile palette as 4 profiles, omittingverifier, whichAI_PROFILES(crates/ai/src/tools/mod.rs:26-32) offers the AI andbootstrap.rs:896has a prompt for.Evidence
ai_set_mode,ai_set_profile,ai_set_budget,delegate,ask_user,propose_changes,log_activity,read_transcript,web_fetchare handled ONLY insidecrates/ai/src/session/handle_prompt.rs(:676,731,753,779,808,840,870,925,982). Grepping those nine names acrosscrates/ai/src/executor/andcrates/ai/src/tool_impls/returns zero non-test hits — no category dispatcher indispatch_tool(tool_dispatch.rs:649-704) claims them, so an external MCP call falls through toErr(format!("Unknown tool: {}", call.name))(tool_dispatch.rs:703). They are nevertheless inai_specific_toolsand therefore inall_toolsand thetools/listpayload (crates/mae/src/main.rs:806-844).ask_useris explicitly classified Core tier (crates/ai/src/tools/categories.rs:78), so it is in the DEFAULT tiered advertisement (mcp_tools_tiered_by_default= true,main.rs:801-814).Verification
Verified name by name. Each of
ai_set_mode,ai_set_profile,ai_set_budget,delegate,ask_user,propose_changes,log_activity,read_transcript,web_fetchis intercepted only insidecrates/ai/src/session/handle_prompt.rs(:676, :731, :753, :779, :808, :840, :870, :925, :982) — grepping all nine acrosscrates/ai/src/executor/andcrates/ai/src/tool_impls/yields no non-test dispatch site.dispatch_tool(tool_dispatch.rs:649-704) tries eight category dispatchers, three perf names, thecommand_prefix andeditor.ai.scheme_tools, then falls through toErr(format!("Unknown tool: {}", call.name))at :703. The MCP path reaches it:handle_mcp_request(ai_event_handler.rs:862+) builds afake_calland routes to the same executor.ask_useris registered inai_specific_tools(core_tools.rs:228-235, PermissionTier::ReadOnly) and listed inclassify_tool_tier's Core arm (categories.rs:78), andmain.rs:801-814filterstools/listto Core whenmcp_tools_tiered_by_defaultis true (default true) — so it is in an external client's very firsttools/listand returnsUnknown tool: ask_userwhen called. The interactive AskUser/ProposeChanges handling at ai_event_handler.rs:450/:498 is on the embeddedAiEventpath only, not the MCP tool path. TheAI_PROFILESsub-claim also holds:crates/ai/src/tools/mod.rs:26-32lists five profiles including"verifier", whilecrates/core/src/editor/dispatch/ui.rs:218hardcodesvec!["pair-programmer", "explorer", "planner", "reviewer"]. Not tracked in any open issue; #375's ADR-051 phase covers per-session policy, not advertise/dispatch consistency.