@memnox/interceptors 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +54 -0
- package/dist/chunk-SB27OD2T.js +206 -0
- package/dist/egress-cli.js +275 -0
- package/dist/git-credential-cli.js +74 -0
- package/dist/index.d.ts +521 -0
- package/dist/index.js +1123 -0
- package/dist/interceptor-cli.js +530 -0
- package/dist/shell-cli.js +258 -0
- package/package.json +54 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { LocalGate, ActionRequest, DecisionEffect, Alternative, HoldService, LeaseGate, LeaseHolder, Overlay, BinaryVerdict, DaemonResponse, BrowserHosts, MemnoxEvent, EventSink, BreakerThresholds, SessionPause } from '@memnox/core';
|
|
2
|
+
|
|
3
|
+
/** Policy files evaluated in-process — this is what sees the tool's arguments. */
|
|
4
|
+
declare const ENV_POLICIES = "MEMNOX_POLICIES";
|
|
5
|
+
/** Name the local rules match on `agents:`; defaults to the agent kind. */
|
|
6
|
+
declare const ENV_AGENT_NAME = "MEMNOX_AGENT_NAME";
|
|
7
|
+
/** The job it was enrolled under, matched by a rule's `roles:`. A workforce is
|
|
8
|
+
* several agents with different authority, and this is what tells them apart. */
|
|
9
|
+
declare const ENV_AGENT_ROLE = "MEMNOX_AGENT_ROLE";
|
|
10
|
+
declare const POLICY_PATH_SEPARATOR = ",";
|
|
11
|
+
/** The agent kind this seam is installed into, used as the default policy identity. */
|
|
12
|
+
declare const DEFAULT_AGENT_NAME = "claude-code";
|
|
13
|
+
/** Actions that carry a payload somewhere this machine does not control. */
|
|
14
|
+
declare const EGRESS_ACTIONS: readonly string[];
|
|
15
|
+
/** Loopback only: a proxy reachable from the network is a hole, not a seam. */
|
|
16
|
+
declare const EGRESS_DEFAULT_PORT = 8888;
|
|
17
|
+
/** A body larger than this is not read, and is never treated as though it had been. */
|
|
18
|
+
declare const EGRESS_MAX_BODY_BYTES = 1000000;
|
|
19
|
+
|
|
20
|
+
interface HookVerdict {
|
|
21
|
+
effect: DecisionEffect;
|
|
22
|
+
reason: string;
|
|
23
|
+
/** What the agent may use instead, carried into the denial the model reads. */
|
|
24
|
+
alternative?: Alternative;
|
|
25
|
+
/** The rule that matched, so a reported verdict cites rather than asserts. */
|
|
26
|
+
rule?: string;
|
|
27
|
+
/** Present when a person has to answer; printed so the terminal can resolve it. */
|
|
28
|
+
approvalId?: string;
|
|
29
|
+
/** The verdict this came from, so a hooked call joins its decision in the ledger. */
|
|
30
|
+
decisionId?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Set when nobody could be asked, rather than when somebody said no. A seam that
|
|
33
|
+
* can only ever subtract needs to tell those apart before it decides what to do.
|
|
34
|
+
*/
|
|
35
|
+
unreachable?: true;
|
|
36
|
+
}
|
|
37
|
+
interface HookAuthorizerDeps {
|
|
38
|
+
/** Evaluated in-process against this machine's policy files; sees the arguments. */
|
|
39
|
+
gate?: LocalGate;
|
|
40
|
+
/** The runtime, which alone can resolve an alternative and raise an approval. */
|
|
41
|
+
/** Allow the tool when the runtime is unreachable. Default false — fail closed. */
|
|
42
|
+
failOpen?: boolean;
|
|
43
|
+
/** Which seam is reporting, so coverage and drift can tell them apart. */
|
|
44
|
+
seam?: string;
|
|
45
|
+
log: (message: string) => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Local first, runtime second, strictest wins — the same order the MCP seam uses. A
|
|
49
|
+
* local refusal never becomes a network request, so the arguments that produced it
|
|
50
|
+
* stay on this machine.
|
|
51
|
+
*/
|
|
52
|
+
declare class HookAuthorizer {
|
|
53
|
+
private readonly deps;
|
|
54
|
+
constructor(deps: HookAuthorizerDeps);
|
|
55
|
+
authorize(request: ActionRequest): Promise<HookVerdict>;
|
|
56
|
+
private egress;
|
|
57
|
+
/** Null when no policy files were configured, which leaves the runtime as the gate. */
|
|
58
|
+
private locally;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface HookConfig {
|
|
62
|
+
policyFiles: string[];
|
|
63
|
+
agentName?: string;
|
|
64
|
+
/** The role a rule's `roles:` matches, when this agent was enrolled under one. */
|
|
65
|
+
agentRole?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The environment first, then the registry on disk. An agent launched from a desktop
|
|
69
|
+
* icon inherits no shell, so reading only the environment would install cleanly and
|
|
70
|
+
* then govern nothing — which is worse than not installing.
|
|
71
|
+
*/
|
|
72
|
+
declare function readHookConfig(env: NodeJS.ProcessEnv, homeDir: string): Promise<HookConfig>;
|
|
73
|
+
|
|
74
|
+
/** Null leaves the runtime as the only gate, which is what an unconfigured install has. */
|
|
75
|
+
declare function loadHookGate(config: HookConfig, home?: string, now?: () => string): Promise<LocalGate | null>;
|
|
76
|
+
|
|
77
|
+
declare const SHELL_ACTION = "shell.execute";
|
|
78
|
+
interface ShellOutcome {
|
|
79
|
+
/** The command to run, present only when it may proceed. */
|
|
80
|
+
run?: readonly string[];
|
|
81
|
+
/** Printed on stderr. A refusal that explains nothing gets the wrapper removed. */
|
|
82
|
+
message?: string;
|
|
83
|
+
exitCode: number;
|
|
84
|
+
}
|
|
85
|
+
declare const SHELL_EXIT_OK = 0;
|
|
86
|
+
declare const SHELL_EXIT_WITHHELD = 77;
|
|
87
|
+
interface ShellSeamDeps {
|
|
88
|
+
authorizer: HookAuthorizer;
|
|
89
|
+
sessionId?: string;
|
|
90
|
+
workingDirectory?: string;
|
|
91
|
+
env?: NodeJS.ProcessEnv;
|
|
92
|
+
/**
|
|
93
|
+
* Somebody to ask. Absent means an ASK is withheld and says so, which is right for
|
|
94
|
+
* a test and wrong for a shell an agent is typing into: without one, every `ask`
|
|
95
|
+
* rule the line hits is a refusal nobody was offered the chance to answer.
|
|
96
|
+
*/
|
|
97
|
+
hold?: HoldService;
|
|
98
|
+
/**
|
|
99
|
+
* Two agents on one repository. Absent on a machine running one agent, which is the
|
|
100
|
+
* ordinary case and must stay free of every cost this adds.
|
|
101
|
+
*/
|
|
102
|
+
leases?: {
|
|
103
|
+
gate: LeaseGate;
|
|
104
|
+
holder: LeaseHolder;
|
|
105
|
+
repositoryRoot: string;
|
|
106
|
+
isDirectory: (path: string) => boolean;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Gates a command and then gets out of the way. It never rewrites what was asked for:
|
|
111
|
+
* a modified command is a bug the person cannot see and the reader cannot audit.
|
|
112
|
+
*
|
|
113
|
+
* Every command in the line is resolved through the one resolver in core, so a rule
|
|
114
|
+
* named `gh.pr-merge` — which is what `protect --for gh` writes and what `explain`
|
|
115
|
+
* promises — fires here too. Ruling on the raw line alone would have made every screen
|
|
116
|
+
* that names a CLI verb describe a gate that never closes.
|
|
117
|
+
*/
|
|
118
|
+
declare class ShellSeam {
|
|
119
|
+
private readonly deps;
|
|
120
|
+
constructor(deps: ShellSeamDeps);
|
|
121
|
+
gate(command: readonly string[]): Promise<ShellOutcome>;
|
|
122
|
+
/**
|
|
123
|
+
* Puts an ASK to a person. Null when it was allowed and the line may proceed.
|
|
124
|
+
*
|
|
125
|
+
* Withheld rather than run when nobody answers: a walk-away must not become a yes,
|
|
126
|
+
* and a timeout is said differently from a refusal so the two read differently.
|
|
127
|
+
*/
|
|
128
|
+
private askAbout;
|
|
129
|
+
/**
|
|
130
|
+
* Takes the paths this line writes, or answers with who is already on them. Reads
|
|
131
|
+
* never reach the register: `takesLease` decides that here, so there is no path
|
|
132
|
+
* through this seam that can make a reader wait.
|
|
133
|
+
*/
|
|
134
|
+
private claim;
|
|
135
|
+
private requestFor;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* What a shell was asked to do. `memnox run` sets this binary as `SHELL`, and an agent's
|
|
140
|
+
* Bash tool then calls it the way it calls any shell — `$SHELL -c "<line>"`. Reading
|
|
141
|
+
* argv as a command to spawn made that `spawn -c` and every command failed with ENOENT.
|
|
142
|
+
*/
|
|
143
|
+
declare const SHELL_MODE: {
|
|
144
|
+
/** `-c "<line>"`: the POSIX contract, and the only form an agent actually uses. */
|
|
145
|
+
readonly COMMAND: "command";
|
|
146
|
+
/** `-- cmd args`: how a person or a test drives the wrapper directly. */
|
|
147
|
+
readonly ARGV: "argv";
|
|
148
|
+
/** No command at all. Hand the terminal to the real shell rather than refuse it. */
|
|
149
|
+
readonly INTERACTIVE: "interactive";
|
|
150
|
+
};
|
|
151
|
+
type ShellMode = (typeof SHELL_MODE)[keyof typeof SHELL_MODE];
|
|
152
|
+
interface ShellInvocation {
|
|
153
|
+
mode: ShellMode;
|
|
154
|
+
/** The line to gate and to hand on unchanged, for the `-c` form. */
|
|
155
|
+
line?: string;
|
|
156
|
+
/** The command to gate and to spawn, for the `--` form. */
|
|
157
|
+
argv?: string[];
|
|
158
|
+
/** Flags seen before `-c`, so the real shell is invoked as it was asked to be. */
|
|
159
|
+
flags: string[];
|
|
160
|
+
}
|
|
161
|
+
declare function shellInvocation(argv: readonly string[]): ShellInvocation;
|
|
162
|
+
declare const REAL_SHELL_VAR = "MEMNOX_REAL_SHELL";
|
|
163
|
+
/** The last shell that is not this one; `/bin/sh` exists on every machine this runs on. */
|
|
164
|
+
declare const FALLBACK_SHELL = "/bin/sh";
|
|
165
|
+
/**
|
|
166
|
+
* Never this binary. `memnox run` overwrites `SHELL`, so reading `SHELL` back here
|
|
167
|
+
* would make the wrapper exec itself for ever — the same shape as the interceptor
|
|
168
|
+
* fork bomb, and just as invisible until a terminal stops answering.
|
|
169
|
+
*/
|
|
170
|
+
declare function realShell(env: NodeJS.ProcessEnv, self: string): string;
|
|
171
|
+
|
|
172
|
+
declare const GIT_CREDENTIAL_ACTION = "git.credential";
|
|
173
|
+
interface GitCredentialOutcome {
|
|
174
|
+
/**
|
|
175
|
+
* What to write on stdout. Empty means "no opinion", and git asks the next helper;
|
|
176
|
+
* `quit=1` stops it asking anyone. A credential is never among the things it can say.
|
|
177
|
+
*/
|
|
178
|
+
stdout: string;
|
|
179
|
+
message?: string;
|
|
180
|
+
}
|
|
181
|
+
interface GitCredentialSeamDeps {
|
|
182
|
+
authorizer: HookAuthorizer;
|
|
183
|
+
sessionId?: string;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Reads git's own key=value block, rules on the remote it names, and either stays
|
|
187
|
+
* silent or declines. It holds no secrets and can hand none out, which is the only
|
|
188
|
+
* shape of credential helper worth trusting inside a governance tool.
|
|
189
|
+
*/
|
|
190
|
+
declare class GitCredentialSeam {
|
|
191
|
+
private readonly deps;
|
|
192
|
+
constructor(deps: GitCredentialSeamDeps);
|
|
193
|
+
gate(input: string): Promise<GitCredentialOutcome>;
|
|
194
|
+
}
|
|
195
|
+
/** git writes one `key=value` per line, terminated by a blank line. */
|
|
196
|
+
declare function parseGitInput(input: string): Record<string, string>;
|
|
197
|
+
|
|
198
|
+
declare const EGRESS_REQUEST_ACTION = "http.request";
|
|
199
|
+
/** A tunnel is a different question from a request: only the destination is knowable. */
|
|
200
|
+
declare const EGRESS_CONNECT_ACTION = "http.connect";
|
|
201
|
+
/**
|
|
202
|
+
* Declared, and the first one is the whole reason this seam is honest about itself.
|
|
203
|
+
* A governed agent with an unwatched side channel is worse than an ungoverned one.
|
|
204
|
+
*/
|
|
205
|
+
declare const EGRESS_BLIND_SPOTS: readonly string[];
|
|
206
|
+
interface EgressOutcome {
|
|
207
|
+
allowed: boolean;
|
|
208
|
+
/** Returned to the client on a refusal, and logged either way. */
|
|
209
|
+
message?: string;
|
|
210
|
+
}
|
|
211
|
+
interface HttpAttempt {
|
|
212
|
+
method: string;
|
|
213
|
+
/** Absolute-form, as a forward proxy receives it. */
|
|
214
|
+
url: string;
|
|
215
|
+
headers?: Readonly<Record<string, string>>;
|
|
216
|
+
/** Read only for plain HTTP; a tunnelled body never reaches this seam. */
|
|
217
|
+
body?: string;
|
|
218
|
+
}
|
|
219
|
+
interface EgressSeamDeps {
|
|
220
|
+
authorizer: HookAuthorizer;
|
|
221
|
+
/**
|
|
222
|
+
* Somebody to ask. Absent means an ask does not go and says so, which is right for
|
|
223
|
+
* a test and wrong for a proxy an agent is reaching the network through.
|
|
224
|
+
*/
|
|
225
|
+
hold?: HoldService;
|
|
226
|
+
sessionId?: string;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Destination and payload, both, where both are visible. An allowed host carrying a
|
|
230
|
+
* credential is still a refusal, and nothing is ever rewritten on the way through —
|
|
231
|
+
* modifying a payload and letting it pass is a bug the agent cannot see.
|
|
232
|
+
*/
|
|
233
|
+
declare class EgressSeam {
|
|
234
|
+
private readonly deps;
|
|
235
|
+
constructor(deps: EgressSeamDeps);
|
|
236
|
+
gateRequest(attempt: HttpAttempt): Promise<EgressOutcome>;
|
|
237
|
+
/**
|
|
238
|
+
* All that is knowable about a tunnel is where it goes. Ruling on the destination and
|
|
239
|
+
* saying plainly that the body is unseen beats pretending to inspect it.
|
|
240
|
+
*/
|
|
241
|
+
gateConnect(authority: string): Promise<EgressOutcome>;
|
|
242
|
+
private rule;
|
|
243
|
+
/**
|
|
244
|
+
* Puts an ask to a person. Null when it was allowed and the request may go.
|
|
245
|
+
*
|
|
246
|
+
* Without this an `ask` rule refused the request outright and told the reader "you
|
|
247
|
+
* chose to be asked about this" while nobody had been asked — the rule's own words
|
|
248
|
+
* arguing with what had just happened to them.
|
|
249
|
+
*/
|
|
250
|
+
private ask;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The directory that goes on the front of PATH. Every entry in it is this one binary. */
|
|
254
|
+
declare const INTERCEPTOR_DIR = "bin";
|
|
255
|
+
declare function interceptorDirFor(home: string): string;
|
|
256
|
+
/**
|
|
257
|
+
* PATH with our directory removed, so the interceptor can find the binary it stands in front
|
|
258
|
+
* of. Without this the interceptor would exec itself, which is a fork bomb rather than a gate.
|
|
259
|
+
*/
|
|
260
|
+
declare function realPath(path: string, home: string): string;
|
|
261
|
+
interface InterceptOutcome {
|
|
262
|
+
allowed: boolean;
|
|
263
|
+
/** What the interceptor should exec, once it is allowed to. */
|
|
264
|
+
binary: string;
|
|
265
|
+
args: string[];
|
|
266
|
+
action: string;
|
|
267
|
+
class: string;
|
|
268
|
+
target?: string;
|
|
269
|
+
/** Printed to stderr when the answer is no. Always names a way forward. */
|
|
270
|
+
message?: string;
|
|
271
|
+
/** Recorded against the decision. A digest, never the arguments. */
|
|
272
|
+
argsDigest: string;
|
|
273
|
+
/** What a person typed instead. The caller rules on it again; it is never trusted. */
|
|
274
|
+
edited?: string;
|
|
275
|
+
/** The rule's own reason, apart from the message a person reads. For the ledger. */
|
|
276
|
+
reason?: string;
|
|
277
|
+
/** The rule that decided, so `why` can name it rather than only quote it. */
|
|
278
|
+
rule?: {
|
|
279
|
+
name: string;
|
|
280
|
+
layer: string;
|
|
281
|
+
file: string;
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
interface InterceptDeps {
|
|
285
|
+
gate?: LocalGate;
|
|
286
|
+
/** Read for a database host only; nothing else here looks at the environment. */
|
|
287
|
+
env?: NodeJS.ProcessEnv;
|
|
288
|
+
hold?: HoldService;
|
|
289
|
+
/** What is in force, so the refusal can name the freeze rather than only the rule. */
|
|
290
|
+
overlays?: readonly Overlay[];
|
|
291
|
+
sessionId?: string;
|
|
292
|
+
agent?: string;
|
|
293
|
+
log: (message: string) => void;
|
|
294
|
+
}
|
|
295
|
+
/** Our own name. Nothing resolved to this may ever be exec'd — that is the fork bomb. */
|
|
296
|
+
declare const INTERCEPT_BINARY = "memnox-intercept";
|
|
297
|
+
/**
|
|
298
|
+
* The wrapper passes the binary it stands for as the first argument, so one executable
|
|
299
|
+
* serves every entry in the directory. Reading it from argv[1] instead would read our
|
|
300
|
+
* own path, classify us, and exec us again — which is a fork bomb, not a gate.
|
|
301
|
+
*/
|
|
302
|
+
declare function invokedFor(argv: readonly string[]): {
|
|
303
|
+
binary: string;
|
|
304
|
+
args: string[];
|
|
305
|
+
} | null;
|
|
306
|
+
/**
|
|
307
|
+
* Evaluate, then exec. Nothing here reads the file the command names or the output it
|
|
308
|
+
* produces: the interceptor sees argv and a verdict, and the real binary does the work.
|
|
309
|
+
*/
|
|
310
|
+
declare function ruleOnCommand(binary: string, args: readonly string[], deps: InterceptDeps): Promise<InterceptOutcome>;
|
|
311
|
+
/** Where the real binary lives, found along PATH with our own directory removed. */
|
|
312
|
+
declare function resolveReal(binary: string, path: string, exists: (p: string) => boolean): string | null;
|
|
313
|
+
/** The one resolver in core, so every surface agrees on what a command line is. */
|
|
314
|
+
declare function verdictFor(binary: string, args: readonly string[], env?: NodeJS.ProcessEnv): BinaryVerdict;
|
|
315
|
+
|
|
316
|
+
interface InterceptorInstallReport {
|
|
317
|
+
directory: string;
|
|
318
|
+
installed: string[];
|
|
319
|
+
/** Known to the rules, absent from this machine. Named so the list is never a mystery. */
|
|
320
|
+
absent: string[];
|
|
321
|
+
/** What to add to PATH, printed rather than written into somebody's shell profile. */
|
|
322
|
+
pathLine: string;
|
|
323
|
+
}
|
|
324
|
+
interface InstallSeams {
|
|
325
|
+
path?: string;
|
|
326
|
+
exists?: (candidate: string) => boolean;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Only binaries this machine actually has. A shim for an absent `aws` would answer
|
|
330
|
+
* `command -v aws` and make every script that checks for it take the wrong branch —
|
|
331
|
+
* a governance tool that breaks a build is a governance tool somebody removes.
|
|
332
|
+
*/
|
|
333
|
+
declare function installInterceptors(home: string, interceptBinary: string, seams?: InstallSeams): Promise<InterceptorInstallReport>;
|
|
334
|
+
/** Removes every interceptor and the directory, so a machine goes back exactly as it was. */
|
|
335
|
+
declare function removeInterceptors(home: string): Promise<string[]>;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Defence in depth. An interceptor is bypassed by anything that calls the real binary
|
|
339
|
+
* directly; a hook runs inside git itself, so a push that dodged PATH still meets a
|
|
340
|
+
* rule. Slower and narrower than the interceptor, which is why it is the second line.
|
|
341
|
+
*/
|
|
342
|
+
declare const HOOKS: readonly ["pre-push", "pre-commit"];
|
|
343
|
+
type GitHook = (typeof HOOKS)[number];
|
|
344
|
+
declare const HOOK_MARKER = "# installed by memnox";
|
|
345
|
+
interface HookInstallReport {
|
|
346
|
+
installed: GitHook[];
|
|
347
|
+
/** Hooks somebody else wrote. Never overwritten; named so a person decides. */
|
|
348
|
+
skipped: {
|
|
349
|
+
hook: GitHook;
|
|
350
|
+
because: string;
|
|
351
|
+
}[];
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Written with the path Memnox was actually run from, so the hook keeps working in a
|
|
355
|
+
* shell that has never heard of it — which is every shell, after `npx memnox`.
|
|
356
|
+
*/
|
|
357
|
+
declare function installGitHooks(repoDir: string, binary?: string): Promise<HookInstallReport>;
|
|
358
|
+
/** Removes only hooks carrying our marker, so somebody else's survives. */
|
|
359
|
+
declare function removeGitHooks(repoDir: string): Promise<GitHook[]>;
|
|
360
|
+
|
|
361
|
+
/** Milliseconds. An interceptor runs on every command; a slow daemon must not be felt. */
|
|
362
|
+
declare const DAEMON_TIMEOUT_MS = 250;
|
|
363
|
+
interface AskOptions {
|
|
364
|
+
action: string;
|
|
365
|
+
target?: string;
|
|
366
|
+
sessionId?: string;
|
|
367
|
+
timeoutMs?: number;
|
|
368
|
+
}
|
|
369
|
+
interface ReportOptions {
|
|
370
|
+
action: string;
|
|
371
|
+
target?: string;
|
|
372
|
+
sessionId?: string;
|
|
373
|
+
/** How the command ended. The breaker's error and progress signals need this. */
|
|
374
|
+
exitCode?: number;
|
|
375
|
+
outOfScope?: boolean;
|
|
376
|
+
/** Reported, never estimated. Absent on every surface that cannot know it. */
|
|
377
|
+
costUsd?: number;
|
|
378
|
+
timeoutMs?: number;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Null on any failure at all — not running, too slow, garbled. The caller then
|
|
382
|
+
* evaluates in process, which is the same rules a little slower. A daemon that could
|
|
383
|
+
* fail open would be a gate that stops working when it is under load.
|
|
384
|
+
*/
|
|
385
|
+
declare function askDaemon(home: string, options: AskOptions): Promise<DaemonResponse | null>;
|
|
386
|
+
/**
|
|
387
|
+
* What happened, after it happened.
|
|
388
|
+
*
|
|
389
|
+
* The breaker watches outcomes, so without this every one of its signals counts
|
|
390
|
+
* nothing: a request on its own cannot say whether the same command has now failed
|
|
391
|
+
* eleven times. Best effort, like everything else here — a daemon that is not running
|
|
392
|
+
* means the counters are not kept, not that the command is held up.
|
|
393
|
+
*/
|
|
394
|
+
declare function reportToDaemon(home: string, options: ReportOptions): Promise<DaemonResponse | null>;
|
|
395
|
+
/** Whether this session is held. One connect, asked before anything runs. */
|
|
396
|
+
declare function askStatus(home: string, sessionId: string, timeoutMs?: number): Promise<DaemonResponse | null>;
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* A browser driver reaches sites the person is signed into, so the thing worth ruling
|
|
400
|
+
* on is the host — not the script, and not each navigation. One ask per host per
|
|
401
|
+
* session: asking on every page would train somebody to hold the key down, which is
|
|
402
|
+
* worse than not asking at all.
|
|
403
|
+
*/
|
|
404
|
+
declare const BROWSER_ACTION = "browser.navigate";
|
|
405
|
+
interface BrowserGateDeps {
|
|
406
|
+
gate?: LocalGate;
|
|
407
|
+
hold?: HoldService;
|
|
408
|
+
hosts?: BrowserHosts;
|
|
409
|
+
sessionId?: string;
|
|
410
|
+
agent?: string;
|
|
411
|
+
}
|
|
412
|
+
interface BrowserOutcome {
|
|
413
|
+
allowed: boolean;
|
|
414
|
+
/** Null for a local target, which is nothing to rule on. */
|
|
415
|
+
host: string | null;
|
|
416
|
+
/** Set when this session had already answered for this host. */
|
|
417
|
+
remembered?: true;
|
|
418
|
+
message?: string;
|
|
419
|
+
}
|
|
420
|
+
declare class BrowserSeam {
|
|
421
|
+
private readonly deps;
|
|
422
|
+
private readonly hosts;
|
|
423
|
+
constructor(deps?: BrowserGateDeps);
|
|
424
|
+
navigate(url: string): Promise<BrowserOutcome>;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The row behind `why`, `timeline`, `trace`, `collisions` and every export. Without it
|
|
429
|
+
* the ledger has readers and no writer, and every one of those commands answers "nothing
|
|
430
|
+
* recorded yet" about a machine that has been governing commands all day.
|
|
431
|
+
*/
|
|
432
|
+
interface RecordInput {
|
|
433
|
+
outcome: InterceptOutcome;
|
|
434
|
+
effect: DecisionEffect;
|
|
435
|
+
reason: string;
|
|
436
|
+
at: string;
|
|
437
|
+
sessionId?: string;
|
|
438
|
+
agent?: string;
|
|
439
|
+
rule?: {
|
|
440
|
+
name: string;
|
|
441
|
+
layer: string;
|
|
442
|
+
file: string;
|
|
443
|
+
line?: number;
|
|
444
|
+
};
|
|
445
|
+
policyHash?: string;
|
|
446
|
+
bundleHash?: string;
|
|
447
|
+
conditionsInForce?: readonly string[];
|
|
448
|
+
exitCode?: number;
|
|
449
|
+
durationMs?: number;
|
|
450
|
+
}
|
|
451
|
+
declare function eventFor(input: RecordInput): MemnoxEvent;
|
|
452
|
+
/**
|
|
453
|
+
* Best effort, and silent about failing. A ledger that cannot be written is a lost row;
|
|
454
|
+
* a ledger that stops the command is a tool somebody uninstalls. The gate has already
|
|
455
|
+
* decided by the time this runs, so nothing here can change the answer.
|
|
456
|
+
*/
|
|
457
|
+
declare function record(sink: EventSink | null, input: RecordInput): Promise<void>;
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* The circuit breaker, in the seam rather than in a daemon.
|
|
461
|
+
*
|
|
462
|
+
* It lived only in the daemon, and no seam has ever spoken to the daemon on the hot
|
|
463
|
+
* path — so on an ordinary machine the breaker observed nothing and the loop it exists
|
|
464
|
+
* to stop ran until a person noticed the bill. Every seam already writes its outcome to
|
|
465
|
+
* the ledger, so this replays the session from there. No second process to install, no
|
|
466
|
+
* counter to keep, and it works in the default path rather than the optional one.
|
|
467
|
+
*
|
|
468
|
+
* Both halves are best effort. A ledger that will not open loses the breaker, never the
|
|
469
|
+
* command: a tool that stops somebody working because it could not read its own history
|
|
470
|
+
* is one they uninstall, and an uninstalled breaker stops nothing at all.
|
|
471
|
+
*/
|
|
472
|
+
interface BreakerSeamOptions {
|
|
473
|
+
home: string;
|
|
474
|
+
sessionId: string | undefined;
|
|
475
|
+
thresholds?: BreakerThresholds;
|
|
476
|
+
}
|
|
477
|
+
/** The pause holding this session, or null. Read before anything is allowed to run. */
|
|
478
|
+
declare function pauseHolding(home: string, sessionId: string | undefined): Promise<SessionPause | null>;
|
|
479
|
+
/** What a held session prints instead of running. Named so `resume` is discoverable. */
|
|
480
|
+
declare function pauseMessage(pause: SessionPause): string;
|
|
481
|
+
/**
|
|
482
|
+
* Replay what this session has done and hold it if the breaker trips.
|
|
483
|
+
*
|
|
484
|
+
* Called after the action, because the trip conditions need the outcome: "ran the same
|
|
485
|
+
* command eleven times" is only knowable once the eleventh has finished.
|
|
486
|
+
*/
|
|
487
|
+
declare function observeSession(options: BreakerSeamOptions): Promise<SessionPause | null>;
|
|
488
|
+
|
|
489
|
+
/** stderr is the safe side channel — stdout belongs to whatever protocol is speaking. */
|
|
490
|
+
declare const log: (message: string) => void;
|
|
491
|
+
declare function buildAuthorizer(): Promise<HookAuthorizer>;
|
|
492
|
+
declare function readStdin(): Promise<string>;
|
|
493
|
+
interface SeamLeases {
|
|
494
|
+
gate: LeaseGate;
|
|
495
|
+
holder: LeaseHolder;
|
|
496
|
+
repositoryRoot: string;
|
|
497
|
+
isDirectory: (path: string) => boolean;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* The register, when there is a repository to have one about.
|
|
501
|
+
*
|
|
502
|
+
* Undefined outside a checkout, and undefined is the right answer rather than a
|
|
503
|
+
* degraded one: a lease is repository-relative, and two machines cannot agree about a
|
|
504
|
+
* path that has no root. A single-agent laptop pays nothing for this either way — the
|
|
505
|
+
* register is only ever consulted for a write, and an empty one never refuses.
|
|
506
|
+
*/
|
|
507
|
+
declare function buildLeases(cwd?: string): SeamLeases | undefined;
|
|
508
|
+
/**
|
|
509
|
+
* Somebody to ask.
|
|
510
|
+
*
|
|
511
|
+
* Every seam took an optional hold service and nothing ever built one, so an `ask`
|
|
512
|
+
* rule reached "nobody could be asked, so it was denied" — which made `ask` a synonym
|
|
513
|
+
* for `deny` and left no way to run an agent unattended at all.
|
|
514
|
+
*
|
|
515
|
+
* The question is written to `~/.memnox/pending` first and answered from wherever an
|
|
516
|
+
* answer turns up: this terminal when there is one, `memnox approve` in another, or
|
|
517
|
+
* the control plane reading the same directory.
|
|
518
|
+
*/
|
|
519
|
+
declare function buildHold(timeoutMs?: number): HoldService;
|
|
520
|
+
|
|
521
|
+
export { type AskOptions, BROWSER_ACTION, type BreakerSeamOptions, type BrowserGateDeps, type BrowserOutcome, BrowserSeam, DAEMON_TIMEOUT_MS, DEFAULT_AGENT_NAME, EGRESS_ACTIONS, EGRESS_BLIND_SPOTS, EGRESS_CONNECT_ACTION, EGRESS_DEFAULT_PORT, EGRESS_MAX_BODY_BYTES, EGRESS_REQUEST_ACTION, ENV_AGENT_NAME, ENV_AGENT_ROLE, ENV_POLICIES, type EgressOutcome, EgressSeam, type EgressSeamDeps, FALLBACK_SHELL, GIT_CREDENTIAL_ACTION, type GitCredentialOutcome, GitCredentialSeam, type GitCredentialSeamDeps, type GitHook, HOOKS, HOOK_MARKER, HookAuthorizer, type HookAuthorizerDeps, type HookConfig, type HookInstallReport, type HookVerdict, type HttpAttempt, INTERCEPTOR_DIR, INTERCEPT_BINARY, type InstallSeams, type InterceptDeps, type InterceptOutcome, type InterceptorInstallReport, POLICY_PATH_SEPARATOR, REAL_SHELL_VAR, type RecordInput, type ReportOptions, SHELL_ACTION, SHELL_EXIT_OK, SHELL_EXIT_WITHHELD, SHELL_MODE, type ShellInvocation, type ShellMode, type ShellOutcome, ShellSeam, type ShellSeamDeps, askDaemon, askStatus, buildAuthorizer, buildHold, buildLeases, eventFor, installGitHooks, installInterceptors, interceptorDirFor, invokedFor, loadHookGate, log, observeSession, parseGitInput, pauseHolding, pauseMessage, readHookConfig, readStdin, realPath, realShell, record, removeGitHooks, removeInterceptors, reportToDaemon, resolveReal, ruleOnCommand, shellInvocation, verdictFor };
|