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
//! Declared subprocess tools — the workspace binding of the tool contract.
//!
//! ADR-0012: **one contract per seam, and transports are adapters.** The
//! contract is mentra's `ExecutableTool`, and it has three bindings — a host
//! registering a tool in Rust, an MCP server basis connects to
//! (`crate::mcp`, behind a cargo feature), and this one: a data file in the
//! workspace declares a name, a description, a JSON schema and a command, and
//! basis wraps that command as a tool the model can call. pi's "CLI tools
//! instead of MCP", typed and schema-checked.
//!
//! It is a *core* feature and not part of the `mcp` feature, deliberately.
//! Custom tools were never MCP's to own; MCP was one of the ways to reach them,
//! and a build with `default-features = false` still has this.
//!
//! # The use case it shipped against
//!
//! Held, not built, until there was one — the rule Phase D set itself
//! (PROPOSAL.md Bet 7, `docs/REDESIGN.md`). The one that arrived: a production
//! Rust host needed Jenkins operations available to the model as tools. With no
//! registration surface at all, they became shell scripts invoked through
//! [`spawn`](crate::tools::spawn)'s command mode — and because a command mode
//! takes *one string*, the SQL queries and free-text questions those scripts
//! act on ended up base64-encoded inside the command line, to survive shell
//! quoting on the way through.
//!
//! That is the shape of the problem this fixes, and it is worth naming
//! precisely: the model was writing a shell command, so every value it carried
//! had to be escaped by a model that cannot be relied on to escape anything,
//! and the workaround for that was an encoding the model had to perform
//! correctly instead. Here the model fills in a JSON schema, basis serializes
//! it, and the program reads an object from its stdin. There is no shell on the
//! path, so there is nothing to quote and nothing to encode around quoting.
//!
//! # The manifest
//!
//! `.basis/tools.json` in the workspace, and `tools.json` in the global config
//! directory — hooks' locations, for hooks' reasons: JSON because the wire
//! contract is already JSON, `.basis/` because that is where basis's other
//! workspace data lives.
//!
//! ```json
//! {
//! "schema": 1,
//! "tools": {
//! "jenkins_job": {
//! "description": "Trigger a Jenkins job and return its build number.",
//! "input_schema": {
//! "type": "object",
//! "properties": { "job": { "type": "string" } },
//! "required": ["job"]
//! },
//! "command": ["./.basis/tools/jenkins", "trigger"],
//! "env": { "JENKINS_TOKEN": "${JENKINS_TOKEN}" },
//! "side_effect": "external",
//! "timeout_ms": 60000
//! }
//! }
//! }
//! ```
//!
//! An object keyed by name, like `.mcp.json`'s `mcpServers` and unlike
//! `hooks.json`'s array — because the name *is* the tool here. Two hooks may
//! share a name and both still run; two tools may not, so the format is one
//! where saying it twice is not expressible.
//!
//! `command` is an argv array, never a shell string, so nothing in a tool's
//! input can be reinterpreted as shell syntax. A relative program path resolves
//! against the workspace root, a bare name is left to `PATH`, and `cwd` — when
//! given — resolves against the root too. `${VAR}` expands in `command`, `cwd`
//! and `env`, with `${VAR:-default}` as in a shell, which is how a credential
//! reaches the program without being written in a file people commit.
//!
//! # What the program's environment is made of
//!
//! Three layers, outermost first, each overriding the one before it for a name
//! they share:
//!
//! 1. **What the process inherits.** Nothing is cleared — `PATH`, `HOME` and
//! the rest are how a program is found and how most of them work at all.
//! (This is where a declared tool parts company with a `spawn` command,
//! which mentra's executor runs with the ambient environment cleared.)
//! 2. **The runtime's fixed command environment**, from
//! [`RuntimeBuilder::with_command_environment`](crate::RuntimeBuilder::with_command_environment).
//! A host saying where its service lives is saying it about *every* process
//! the runtime spawns, and a declared tool's program is one of those.
//! 3. **The manifest's `env`**, which is this tool's own statement and
//! therefore the last word: between two statements about one name, the more
//! specific one holds — the same direction in which a workspace's
//! `tools.json` already beats the global one.
//!
//! None of the three is printed anywhere. `env` values are redacted from every
//! `Debug`, and neither layer appears in the approver's preview: the command
//! and its arguments are how a spawn is understood, while the environment is
//! where the credential is.
//!
//! What is *not* in the format is as deliberate: no `enabled` flag (a tool
//! nobody wants is a tool nobody declares), and no way to say a tool is
//! read-only — see [`SideEffect`].
//!
//! # What the program is handed, and what it answers with
//!
//! One JSON object on stdin: the input the model produced, matching the schema
//! the manifest declared, and nothing wrapped around it. basis invents no
//! envelope, because an envelope would be a second schema nobody declared — the
//! file's own `schema` field versions the *manifest*, and the payload's shape
//! is the tool author's to state.
//!
//! Whatever the program prints on stdout is the result the model reads. Not
//! parsed, not interpreted: a program that wants to answer in JSON prints JSON
//! and the model reads JSON. The JSON contract is on the input side, where it
//! buys the thing that motivated the binding.
//!
//! A non-zero exit is a tool *error*, carrying the program's own stderr — the
//! same fail-loud voice as [`crate::hooks`], and for the same reason: what a
//! failure says is the only thing telling the model what to do next. So is a
//! program that cannot be started, one killed by a signal, and one that
//! outstays its deadline.
//!
//! # A declared tool is code from the workspace
//!
//! `.basis/tools.json` is workspace data, so cloning a repository and running
//! basis on it can register tools that repository chose, whose programs that
//! repository ships. That is the same exposure as [`crate::hooks`] and
//! [`crate::shell`], and it is bounded the same way — by whatever confines the
//! process (ADR-0004), not by a check in here.
//!
//! What *is* checked in here is the approval story, because that is basis's own
//! to get right. Every declared tool is consequential ([`SideEffect`] cannot
//! say otherwise), so every call reaches whatever
//! [`Approver`](crate::approval::Approver) the run installed, and what that
//! approver is shown is the command about to run rather than the name a
//! repository gave it (see [`DeclaredTool`]'s `authorization_preview`). A name
//! basis or mentra already answers to cannot be claimed at all, so no manifest
//! can quietly become `spawn`.
pub use ;
pub use DeclaredTool;
pub use DeclaredTools;