1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! **Host injection**: the binding's own tools, declared and answered
//! (ARCH §3.3 *Host-injected tools*, §3.4; `docs/DESIGN_TOOL_INJECTION.md`).
//!
//! The exec binding needs none of this. A *linked* binding may need two
//! things the pool cannot give it: to put tool definitions of its own in
//! front of the model, and to be the thing that executes — a
//! client-management tool, or a tool a remote client advertises across a
//! transport only the host speaks (yog's client/server split, its
//! `docs/REMOTE.md` §5).
//!
//! **An installed injection is the executor's whole backend** (bl-a00a).
//! [`ToolInjection::route`] is total: it answers *every* invocation the
//! agent makes while the host is installed, and the §3.3 three-hop binary
//! resolution stands behind it for no name at all. The binding chooses
//! one pipeline, once, by installing an injection or not; there is no
//! per-invocation choice and therefore no second pipeline with its own
//! adjudication story and its own capture shape (yog `docs/REMOTE.md` §5,
//! §12 *front door only*).
//!
//! Both halves ride **one** object the binding injects at
//! `cmd::Fx::tool_injection`, and one object is the point: a declaration
//! half without a permission half produces a tool the model is told about
//! and then refused ("declaring is not permitting", §3.3), and a
//! permission half without a declaration produces a tool nothing ever
//! calls. Held together they cannot disagree — [`ToolInjection::tools`]
//! is read by prompt assembly *and* by the grant gate, and
//! [`ToolInjection::route`] answers on the same object's behalf.
//!
//! What this seam deliberately is not:
//!
//! - **Not a multiplexer.** Each injected tool is individually named, so
//! the grant gate, the fork-time descriptor trim and the tool control
//! (§3.3) all keep seeing one name per capability. This is
//! `docs/DESIGN_MCP_BRIDGE.md` §6's ruling, unchanged and now also
//! binding on the host.
//! - **Not dynamic mid-drive.** The set is whatever the host states while
//! the drive runs; a host that changes it changes the prompt prefix and
//! pays the cache rebuild knowingly (ARCH §5.5).
//! - **Not an adjudication bypass.** A routed invocation is gated by the
//! grant and adjudicated by the configured tool control exactly as a
//! local one, *before* anything is routed (§3.3 *Tool control*).
use Value;
use Path;
use AtomicBool;
/// One tool definition spliced into a request by something other than the
/// calling role's `providers.yaml` `tools:` grant (ARCH §3.3) — the
/// compactor's procedure toolset (§2.7) and the host's injection are both
/// this shape, so the composer has one kind of injected thing to splice.
///
/// It carries exactly the three facts the `tools: [...]` entry needs; a
/// pool tool sources the same three from disk (`descriptions/tools/
/// <name>.json` plus the skill frontmatter), and an injected one has no
/// disk to source them from, which is the whole difference.
/// One invocation handed to a host router. The four wire facts a tool
/// subprocess gets on stdin and in its environment (§3.3 *Stdio
/// contract*), plus the cancel flag — nothing else, because a router
/// that needed more would be reaching for harness state the front door
/// does not carry.
/// What a router produced for one invocation — the same three facts a
/// tool subprocess produces (§3.3 *Stdio contract*), so everything
/// downstream is unchanged: the result envelope states the exit code,
/// `is_error` is `exit_code != 0`, the bounded projection caps both
/// streams, and `output.json` records them in full. A routed tool is
/// indistinguishable from a local one to the model, by construction
/// rather than by convention.
/// The binding's tool injection: extra definitions, and the router that
/// answers every invocation while it is installed.
///
/// **Router obligations**, which litany cannot enforce and therefore
/// states — [`route`](Self::route) runs *in the executor's own thread*,
/// so nothing in the harness can interrupt it:
///
/// - **It carries its own deadline.** litany imposes no wall-clock limit
/// on a tool (§3.3), and a subprocess's SIGTERM cascade has no
/// in-process analogue. Bound every wait, and render an expired one as
/// a non-zero [`RoutedCapture`].
/// - **A vanished endpoint is a result, not a hang and not a panic.**
/// Unreachable, disconnected, protocol garbage: all of them are
/// `exit_code != 0` with the reason on `stderr`, which is exactly what
/// an external tool that cannot reach its backend does.
/// - **It watches [`RoutedCall::stop`]** so a `litany stop` landing in a
/// routed invocation ends it as promptly as SIGTERM ends a subprocess.
///
/// The per-tool-call disk record (`input.json` / `output.json`, §3.3) is
/// **not** the router's to write: the executor lands it around every
/// answer, routed or spawned, so one convention holds for both and a
/// host cannot forget it (PRINCIPLES "Structure over discipline").