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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
//! The in-process binding of the interception contract.
//!
//! ADR-0012 gives the interception seam one contract and two bindings: an
//! embedder implements the trait, or a workspace declares a subprocess in
//! `.basis/hooks.json`. This module is the first of those. Its sibling is the
//! whole rest of [`crate::hooks`] — [`wire`](super::wire) for what a subprocess
//! is told, [`HookSpec`](super::HookSpec) for how one is declared — and both
//! arrive at [`HookRunner`](super::HookRunner), which owns the ordering.
//!
//! It exists because a binding basis did not have was a power basis did not offer.
//! Redacting a credential out of a tool's input needs the host's own code —
//! the vault handle, the token it just minted, the regex it keeps in a config
//! struct — and until now the only way to get code onto this seam was to spawn
//! a process and hand it the tool call on stdin. That is the right answer for a
//! guard a repository ships and the wrong one for a guard the embedding program
//! *is*.
//!
//! ```no_run
//! use basis::{
//! HookOutcome, HookRequest, Interceptor, InterceptorError, Runtime, Workspace, async_trait,
//! };
//!
//! struct Redact;
//!
//! #[async_trait]
//! impl Interceptor for Redact {
//! fn name(&self) -> &str {
//! "redact"
//! }
//!
//! async fn intercept(&self, call: &HookRequest) -> Result<HookOutcome, InterceptorError> {
//! let Some(command) = call.input.get("command").and_then(|value| value.as_str()) else {
//! return Ok(HookOutcome::Allow);
//! };
//! if !command.contains("--token") {
//! return Ok(HookOutcome::Allow);
//! }
//!
//! Ok(HookOutcome::Modify {
//! input: serde_json::json!({"command": "deploy --token REDACTED"}),
//! reason: Some("stripped a credential".to_string()),
//! })
//! }
//! }
//!
//! # async fn example() -> Result<(), basis::RunError> {
//! // Host scope is runtime scope (ADR-0018): the guard registers on the
//! // runtime the workspaces share — or, as here, on the private one this
//! // workspace's open builds from the recipe.
//! let workspace = Workspace::builder("/repo")
//! .with_runtime_builder(Runtime::builder().with_interceptor(Redact))
//! .open()
//! .await?;
//! # let _ = workspace;
//! # Ok(())
//! # }
//! ```
use Arc;
use ;
/// Why an [`Interceptor`] could not decide.
///
/// Boxed rather than an enum of basis's own, because basis has nothing to say about
/// it: whatever went wrong happened inside the host's code, against the host's
/// dependencies. A box lets `?` carry any of them out, and the only thing basis
/// does with one is print it in the denial.
pub type InterceptorError = ;
/// Gets a say over each tool call, in the embedding program's own process.
///
/// The in-process binding of ADR-0012's interception contract, and the sibling
/// of the subprocess hooks in [`crate::hooks`]: same vocabulary
/// ([`HookOutcome`]), same request ([`HookRequest`]), same chain. What differs
/// is only who is speaking — the host's compiled code rather than a program
/// named in a file.
///
/// The other seam is [`Approver`](crate::approval::Approver), and the two are
/// deliberately not merged (ADR-0012, and mentra keeps them apart for the same
/// reason). An approver answers *may this happen* and its answer feeds the
/// permission machinery a person drives; an interceptor answers *may this
/// happen, in this form* and composes with every other interceptor and hook. A
/// host that wants to ask a person wants an approver; a host that wants to
/// rewrite an argument wants this.
///
/// Async because mentra's own hook trait is, and for the reason it gives: a
/// participant that reads a file, asks a service, or takes a lock would
/// otherwise block a runtime worker for its whole duration. The attribute to
/// spell that with is re-exported at the crate root —
/// [`async_trait`](crate::async_trait) — so writing an impl costs no manifest
/// line of the host's own.
///
/// # Fail closed
///
/// **An interceptor that cannot answer denies.** An `Err` denies, and so does a
/// panic — the call is put to the interceptor on its own task so that a panic
/// is caught rather than taking the turn with it. Either way the reason names
/// this interceptor and says what happened, and the failure is reported through
/// [`HookRunner::with_reporter`](super::HookRunner::with_reporter).
///
/// This is the same rule [`OnFailure`](super::OnFailure) states for hooks, and
/// the same asymmetry justifies it: failing open on a broken guard silently
/// removes a control someone believes is in place, while failing closed on a
/// broken observer is loud and gets fixed. There is no `OnFailure::Allow`
/// equivalent here, and none is needed — an interceptor that would rather be
/// ignored is one `Ok(HookOutcome::Allow)` away from saying so, in code it
/// already owns.
/// Forwards to the interceptor inside.
///
/// Lets a host hold an interceptor it chose at runtime — one of several, or one
/// a feature flag picked — and still hand it to anything taking
/// `impl Interceptor`. The same courtesy [`Approver`](crate::approval::Approver)
/// gets, and mentra's own hook trait.