A tool can be fully implemented, dispatch correctly, pass the whole suite, and still be dead under the daemon transport. I hit this twice in two hours after switching a box to subc, on a feature that had been green in CI for a month.
subc_plumbing_drift_test.rs already documents the class:
History: bash_wait_detach, bash_kill, bash_write, bash_notify and inspect_tier2_run each shipped in the plugins without a gate entry and were silently rejected (fail-closed) under the daemon transport until a log sweep caught the rejections in production.
Five tools, caught by a log sweep in production. That guard closes the hole for native plumbing sends. Agent tools have no equivalent, and there are more places to miss.
The six registrations
Standalone needs three: a main.rs dispatch arm, a translate_* arm, and the plugin tool. Subc needs three more, and one of the standalone three turns out to be gated by a fourth list:
| # |
Location |
Miss symptom |
| 1 |
is_subc_agent_core_tool (subc/manifest.rs:13) |
fail-closed rejection at subc/mod.rs:4031 |
| 2 |
command_lane (manifest.rs:~127) |
silent — _ => Lane::Mutating catches it, so a pure read serializes on the mutating lane |
| 3 |
build_manifest (manifest.rs:~213) |
tool absent from the advertised manifest |
| 4 |
BARE_TOOL_ORDER (subc-tool-schemas.ts) |
no schema emitted |
| 5 |
subc_tool_schemas.json |
placeholder {"type":"object"} schema + a warn line |
| 6 |
supports_tool (subc_translate.rs:831) |
silent, and it breaks standalone too |
Six is the one worth attention. run_tool_call.rs:166 only calls the translate arm when supports_tool returns true:
let (command, translated_args) = if crate::subc_translate::supports_tool(bare_name) {
... subc_translate_owned_with_context(...)
} else {
(bare_name.to_string(), map) // passthrough, untranslated
};
A tool absent there has its translate_* arm as unreachable dead code, in every transport. Mine was, from the day I wrote it. It looked fine because the command handler duplicates the mode validation — but translation is the only layer that calls resolve_path_from_project_root, and the error shapes diverged. Same input, before and after adding one name to that list:
question:"" before → {"code":"search_failed","message":"aft_gather_context: search failed: {\"code\":\"invalid_request\",\"message\":\"query must be non-empty\"}"}
question:"" after → {"code":"invalid_request","message":"aft_gather_context: provide either 'question' or 'symbol'+'filePath'"}
The inner search error was leaking out under a wrong code, because the handler validates with is_some() presence checks and an empty string passed its mode gate. It also accepted question plus a lone symbol and silently dropped the symbol.
Why the existing tests can't see any of this
Every guard iterates a hand-maintained list instead of deriving one:
build_manifest_serves_embedded_tool_schemas and build_manifest_classifies_execution_mode_by_observable_effect both iterate build_manifest()'s own output — a tool missing from the manifest is structurally invisible to them
supports_tool_covers_every_translated_arm iterates a hardcoded 21-name array. Its comment says it exists so that "if a translate arm is added but the allowlist isn't updated, that tool would silently bypass translation" — exactly my bug, and it passed, because the array omitted the tool
PLUGIN_NATIVE_SENDS is a const &[&str] with "Keep in sync when adding plugin bridge calls" — the guard against hand-maintained-list drift is a hand-maintained list, and it covers only native plumbing, not agent tools
- the v0.49 surface audit reads
subc_tool_schemas.json for legacy vocabulary only
registration-parity.test.ts does compare plugin surfaces, which is why my tool needed a HOSTONLY-V049-* inventory row — but it never looks at the subc manifest
The suggestion
subc_tool_schemas.json is already the cross-language bridge: generated from the real plugin tool factories by build-tool-schemas.ts, byte-pinned by a freshness test, and include_str!-embedded in the Rust manifest. It can be the source of truth instead of a sixth thing to remember.
One Rust test, reading the embedded map, asserting for every key:
is_subc_agent_core_tool(name)
- present in
build_manifest().provides[ToolProvider].tools
command_lane(name) was matched explicitly rather than falling through to _ => Lane::Mutating
supports_tool(name)
That turns six registrations into two — add to BARE_TOOL_ORDER, regenerate — and the plugin-side existence assertion in buildSubcToolSchemas() already fails loudly if a factory rename breaks the wiring. It would have caught both of my bugs at commit time, and all five tools in the drift guard's history.
The lane check needs command_lane to distinguish "matched Mutating" from "fell through to Mutating". A command_lane_explicit(&str) -> Option<Lane> with the current function as a wrapper is the smallest way there, though you may prefer a different shape.
Happy to implement it if you want it — say which shape you'd accept. Also happy to leave it; you know the invariants here better than I do, and this is a report either way.
Reproduction context: found on a box running aft as a subc module across two sessions, a config repo and this one. The consumer-side session can add detail if useful.
A tool can be fully implemented, dispatch correctly, pass the whole suite, and still be dead under the daemon transport. I hit this twice in two hours after switching a box to subc, on a feature that had been green in CI for a month.
subc_plumbing_drift_test.rsalready documents the class:Five tools, caught by a log sweep in production. That guard closes the hole for native plumbing sends. Agent tools have no equivalent, and there are more places to miss.
The six registrations
Standalone needs three: a
main.rsdispatch arm, atranslate_*arm, and the plugin tool. Subc needs three more, and one of the standalone three turns out to be gated by a fourth list:is_subc_agent_core_tool(subc/manifest.rs:13)subc/mod.rs:4031command_lane(manifest.rs:~127)_ => Lane::Mutatingcatches it, so a pure read serializes on the mutating lanebuild_manifest(manifest.rs:~213)BARE_TOOL_ORDER(subc-tool-schemas.ts)subc_tool_schemas.json{"type":"object"}schema + a warn linesupports_tool(subc_translate.rs:831)Six is the one worth attention.
run_tool_call.rs:166only calls the translate arm whensupports_toolreturns true:A tool absent there has its
translate_*arm as unreachable dead code, in every transport. Mine was, from the day I wrote it. It looked fine because the command handler duplicates the mode validation — but translation is the only layer that callsresolve_path_from_project_root, and the error shapes diverged. Same input, before and after adding one name to that list:The inner search error was leaking out under a wrong code, because the handler validates with
is_some()presence checks and an empty string passed its mode gate. It also acceptedquestionplus a lonesymboland silently dropped the symbol.Why the existing tests can't see any of this
Every guard iterates a hand-maintained list instead of deriving one:
build_manifest_serves_embedded_tool_schemasandbuild_manifest_classifies_execution_mode_by_observable_effectboth iteratebuild_manifest()'s own output — a tool missing from the manifest is structurally invisible to themsupports_tool_covers_every_translated_armiterates a hardcoded 21-name array. Its comment says it exists so that "if a translate arm is added but the allowlist isn't updated, that tool would silently bypass translation" — exactly my bug, and it passed, because the array omitted the toolPLUGIN_NATIVE_SENDSis aconst &[&str]with "Keep in sync when adding plugin bridge calls" — the guard against hand-maintained-list drift is a hand-maintained list, and it covers only native plumbing, not agent toolssubc_tool_schemas.jsonfor legacy vocabulary onlyregistration-parity.test.tsdoes compare plugin surfaces, which is why my tool needed aHOSTONLY-V049-*inventory row — but it never looks at the subc manifestThe suggestion
subc_tool_schemas.jsonis already the cross-language bridge: generated from the real plugin tool factories bybuild-tool-schemas.ts, byte-pinned by a freshness test, andinclude_str!-embedded in the Rust manifest. It can be the source of truth instead of a sixth thing to remember.One Rust test, reading the embedded map, asserting for every key:
is_subc_agent_core_tool(name)build_manifest().provides[ToolProvider].toolscommand_lane(name)was matched explicitly rather than falling through to_ => Lane::Mutatingsupports_tool(name)That turns six registrations into two — add to
BARE_TOOL_ORDER, regenerate — and the plugin-side existence assertion inbuildSubcToolSchemas()already fails loudly if a factory rename breaks the wiring. It would have caught both of my bugs at commit time, and all five tools in the drift guard's history.The lane check needs
command_laneto distinguish "matchedMutating" from "fell through toMutating". Acommand_lane_explicit(&str) -> Option<Lane>with the current function as a wrapper is the smallest way there, though you may prefer a different shape.Happy to implement it if you want it — say which shape you'd accept. Also happy to leave it; you know the invariants here better than I do, and this is a report either way.
Reproduction context: found on a box running aft as a subc module across two sessions, a config repo and this one. The consumer-side session can add detail if useful.