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
//! **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 answer some invocations itself rather than
//! have the executor resolve a binary for them — 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).
//!
//! 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 a router that
/// answers the invocations it owns.
///
/// **Router obligations**, which lernie 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.** lernie 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 `lernie 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").