@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
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildHold,
|
|
4
|
+
loadHookGate,
|
|
5
|
+
log,
|
|
6
|
+
readHookConfig
|
|
7
|
+
} from "./chunk-SB27OD2T.js";
|
|
8
|
+
|
|
9
|
+
// src/interceptor-cli.ts
|
|
10
|
+
import { spawnSync } from "child_process";
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { basename as basename2 } from "path";
|
|
14
|
+
|
|
15
|
+
// src/breaker-seam.ts
|
|
16
|
+
import {
|
|
17
|
+
breachIn,
|
|
18
|
+
DEFAULT_THRESHOLDS,
|
|
19
|
+
describePause,
|
|
20
|
+
outcomesFrom,
|
|
21
|
+
REPLAY_LIMIT,
|
|
22
|
+
SessionPauses,
|
|
23
|
+
SqliteEventStore
|
|
24
|
+
} from "@memnox/core";
|
|
25
|
+
async function pauseHolding(home, sessionId) {
|
|
26
|
+
if (sessionId === void 0 || sessionId === "") return null;
|
|
27
|
+
try {
|
|
28
|
+
return await new SessionPauses(home).inForce(sessionId);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function pauseMessage(pause) {
|
|
34
|
+
return [
|
|
35
|
+
`Paused by Memnox: ${describePause(pause)}`,
|
|
36
|
+
`Resume with "memnox resume ${pause.sessionId}".`
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
39
|
+
async function resumedAt(home, sessionId) {
|
|
40
|
+
try {
|
|
41
|
+
const pause = await new SessionPauses(home).read(sessionId);
|
|
42
|
+
return pause?.resumedAt ?? null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function observeSession(options) {
|
|
48
|
+
const { home, sessionId } = options;
|
|
49
|
+
if (sessionId === void 0 || sessionId === "") return null;
|
|
50
|
+
try {
|
|
51
|
+
const since = await resumedAt(home, sessionId);
|
|
52
|
+
const store = SqliteEventStore.forHome(home);
|
|
53
|
+
let events;
|
|
54
|
+
try {
|
|
55
|
+
events = await store.query({
|
|
56
|
+
sessionId,
|
|
57
|
+
limit: REPLAY_LIMIT,
|
|
58
|
+
...since === null ? {} : { since }
|
|
59
|
+
});
|
|
60
|
+
} finally {
|
|
61
|
+
store.close();
|
|
62
|
+
}
|
|
63
|
+
const breach = breachIn(
|
|
64
|
+
outcomesFrom(events),
|
|
65
|
+
options.thresholds ?? DEFAULT_THRESHOLDS
|
|
66
|
+
);
|
|
67
|
+
if (breach === null) return null;
|
|
68
|
+
const last = events[events.length - 1];
|
|
69
|
+
const pause = {
|
|
70
|
+
sessionId,
|
|
71
|
+
signal: breach.signal,
|
|
72
|
+
reason: breach.reason,
|
|
73
|
+
reached: breach.reached,
|
|
74
|
+
ceiling: breach.ceiling,
|
|
75
|
+
pausedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
76
|
+
...last === void 0 ? {} : { lastAction: last.operation }
|
|
77
|
+
};
|
|
78
|
+
await new SessionPauses(home).pause(pause);
|
|
79
|
+
return pause;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/interceptor-cli.ts
|
|
86
|
+
import {
|
|
87
|
+
DECISION_EFFECT as DECISION_EFFECT3,
|
|
88
|
+
isBrowserLauncher,
|
|
89
|
+
overlaysInForce,
|
|
90
|
+
provenanceOf,
|
|
91
|
+
SESSION_VAR,
|
|
92
|
+
splitCommandLine,
|
|
93
|
+
SqliteEventStore as SqliteEventStore2,
|
|
94
|
+
urlArgumentIn
|
|
95
|
+
} from "@memnox/core";
|
|
96
|
+
|
|
97
|
+
// src/interceptor.ts
|
|
98
|
+
import { basename, delimiter, join } from "path";
|
|
99
|
+
import {
|
|
100
|
+
DECISION_EFFECT,
|
|
101
|
+
describeHold,
|
|
102
|
+
digest,
|
|
103
|
+
evidenceFor,
|
|
104
|
+
HOLD_OUTCOME,
|
|
105
|
+
isAllowed as holdAllowed,
|
|
106
|
+
MEMNOX_HOME,
|
|
107
|
+
refusalShapeFor,
|
|
108
|
+
renderEvidence,
|
|
109
|
+
resolveAction
|
|
110
|
+
} from "@memnox/core";
|
|
111
|
+
var INTERCEPTOR_DIR = "bin";
|
|
112
|
+
function interceptorDirFor(home) {
|
|
113
|
+
return join(home, MEMNOX_HOME, INTERCEPTOR_DIR);
|
|
114
|
+
}
|
|
115
|
+
function realPath(path, home) {
|
|
116
|
+
const ours = interceptorDirFor(home);
|
|
117
|
+
return path.split(delimiter).filter((entry) => entry !== "" && entry !== ours).join(delimiter);
|
|
118
|
+
}
|
|
119
|
+
var INTERCEPT_BINARY = "memnox-intercept";
|
|
120
|
+
function invokedFor(argv) {
|
|
121
|
+
const binary = argv[2];
|
|
122
|
+
if (binary === void 0 || basename(binary) === INTERCEPT_BINARY) return null;
|
|
123
|
+
return { binary: basename(binary), args: [...argv.slice(3)] };
|
|
124
|
+
}
|
|
125
|
+
async function ruleOnCommand(binary, args, deps) {
|
|
126
|
+
const verdict = verdictFor(binary, args, deps.env ?? {});
|
|
127
|
+
const argsDigest = digest(args.join(" "));
|
|
128
|
+
const base = {
|
|
129
|
+
allowed: true,
|
|
130
|
+
binary,
|
|
131
|
+
args: [...args],
|
|
132
|
+
action: verdict.action,
|
|
133
|
+
class: verdict.class,
|
|
134
|
+
argsDigest,
|
|
135
|
+
...verdict.target === void 0 ? {} : { target: verdict.target }
|
|
136
|
+
};
|
|
137
|
+
const gate = deps.gate;
|
|
138
|
+
if (gate === void 0) return base;
|
|
139
|
+
const decision = gate.evaluate({
|
|
140
|
+
action: verdict.action,
|
|
141
|
+
...verdict.target === void 0 ? {} : { target: verdict.target }
|
|
142
|
+
});
|
|
143
|
+
if (decision.effect === DECISION_EFFECT.ALLOW) return base;
|
|
144
|
+
const shown = renderEvidence(
|
|
145
|
+
evidenceFor({
|
|
146
|
+
matched: decision.matchedPolicies,
|
|
147
|
+
moment: (/* @__PURE__ */ new Date()).toISOString(),
|
|
148
|
+
...deps.overlays === void 0 ? {} : { overlays: deps.overlays }
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
const matched = decision.matchedPolicies[0];
|
|
152
|
+
const decided = {
|
|
153
|
+
reason: decision.reason,
|
|
154
|
+
...matched === void 0 ? {} : { rule: { name: matched.name, layer: "project", file: "policy" } }
|
|
155
|
+
};
|
|
156
|
+
if (decision.effect === DECISION_EFFECT.ASK) {
|
|
157
|
+
const held = await askPerson(
|
|
158
|
+
verdict,
|
|
159
|
+
decision.reason,
|
|
160
|
+
shown,
|
|
161
|
+
[binary, ...args],
|
|
162
|
+
deps
|
|
163
|
+
);
|
|
164
|
+
if (held === null) return { ...base, ...decided };
|
|
165
|
+
if (typeof held !== "string") {
|
|
166
|
+
return {
|
|
167
|
+
...base,
|
|
168
|
+
...decided,
|
|
169
|
+
allowed: false,
|
|
170
|
+
edited: held.edited,
|
|
171
|
+
message: held.message
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return { ...base, ...decided, allowed: false, message: held };
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
...base,
|
|
178
|
+
...decided,
|
|
179
|
+
allowed: false,
|
|
180
|
+
message: [refusal(verdict, decision), ...shown].join("\n")
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async function askPerson(verdict, reason, evidence, command, deps) {
|
|
184
|
+
const hold = deps.hold;
|
|
185
|
+
const request = {
|
|
186
|
+
sessionId: deps.sessionId ?? "ses_local",
|
|
187
|
+
agent: deps.agent ?? "an agent",
|
|
188
|
+
operation: verdict.action,
|
|
189
|
+
fingerprint: digest(`${verdict.action}:${verdict.target ?? ""}`),
|
|
190
|
+
reason,
|
|
191
|
+
evidence,
|
|
192
|
+
command: command.join(" "),
|
|
193
|
+
...verdict.target === void 0 ? {} : { target: verdict.target }
|
|
194
|
+
};
|
|
195
|
+
if (hold === void 0) {
|
|
196
|
+
return `${reason}
|
|
197
|
+
Nobody could be asked, so it was denied. Run the agent under "memnox run".`;
|
|
198
|
+
}
|
|
199
|
+
const result = await hold.hold(request);
|
|
200
|
+
if (holdAllowed(result)) return null;
|
|
201
|
+
if (result.outcome === HOLD_OUTCOME.EDITED && result.edited !== void 0) {
|
|
202
|
+
return { message: describeHold(result, request), edited: result.edited };
|
|
203
|
+
}
|
|
204
|
+
return describeHold(result, request);
|
|
205
|
+
}
|
|
206
|
+
function refusal(verdict, decision) {
|
|
207
|
+
const alternative = decision.alternative;
|
|
208
|
+
const instead = alternative === void 0 ? "" : `
|
|
209
|
+
Instead: ${alternative.action}${alternative.resource === void 0 ? "" : ` ${alternative.resource}`} \u2014 ${alternative.note}`;
|
|
210
|
+
const { guidance } = refusalShapeFor(DECISION_EFFECT.DENY, decision.reason);
|
|
211
|
+
return `Denied by Memnox: ${decision.reason}
|
|
212
|
+
(${verdict.because})${instead}
|
|
213
|
+
${guidance}`;
|
|
214
|
+
}
|
|
215
|
+
function resolveReal(binary, path, exists) {
|
|
216
|
+
if (basename(binary) === INTERCEPT_BINARY) return null;
|
|
217
|
+
for (const entry of path.split(delimiter)) {
|
|
218
|
+
if (entry === "") continue;
|
|
219
|
+
const candidate = join(entry, binary);
|
|
220
|
+
if (basename(candidate) === INTERCEPT_BINARY) continue;
|
|
221
|
+
if (exists(candidate)) return candidate;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function verdictFor(binary, args, env = {}) {
|
|
226
|
+
const resolved = resolveAction(binary, args, env);
|
|
227
|
+
return {
|
|
228
|
+
action: resolved.action,
|
|
229
|
+
class: resolved.class,
|
|
230
|
+
because: resolved.because,
|
|
231
|
+
...resolved.target === void 0 ? {} : { target: resolved.target }
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/browser-seam.ts
|
|
236
|
+
import {
|
|
237
|
+
BrowserHosts,
|
|
238
|
+
DECISION_EFFECT as DECISION_EFFECT2,
|
|
239
|
+
describeHold as describeHold2,
|
|
240
|
+
digest as digest2,
|
|
241
|
+
isAllowed as holdAllowed2,
|
|
242
|
+
navigationHost
|
|
243
|
+
} from "@memnox/core";
|
|
244
|
+
var BROWSER_ACTION = "browser.navigate";
|
|
245
|
+
var BrowserSeam = class {
|
|
246
|
+
constructor(deps = {}) {
|
|
247
|
+
this.deps = deps;
|
|
248
|
+
this.hosts = deps.hosts ?? new BrowserHosts();
|
|
249
|
+
}
|
|
250
|
+
deps;
|
|
251
|
+
hosts;
|
|
252
|
+
async navigate(url) {
|
|
253
|
+
const host = navigationHost(url);
|
|
254
|
+
if (host === null) return { allowed: true, host: null };
|
|
255
|
+
const sessionId = this.deps.sessionId ?? "ses_local";
|
|
256
|
+
if (this.hosts.seen(sessionId, host)) {
|
|
257
|
+
return { allowed: true, host, remembered: true };
|
|
258
|
+
}
|
|
259
|
+
const gate = this.deps.gate;
|
|
260
|
+
const verdict = gate === void 0 ? { effect: DECISION_EFFECT2.ALLOW, reason: "no rules configured" } : gate.evaluate({ action: BROWSER_ACTION, target: host });
|
|
261
|
+
if (verdict.effect === DECISION_EFFECT2.ALLOW) {
|
|
262
|
+
this.hosts.remember(sessionId, host);
|
|
263
|
+
return { allowed: true, host };
|
|
264
|
+
}
|
|
265
|
+
if (verdict.effect === DECISION_EFFECT2.DENY) {
|
|
266
|
+
return { allowed: false, host, message: `Denied by Memnox: ${verdict.reason}` };
|
|
267
|
+
}
|
|
268
|
+
const request = {
|
|
269
|
+
sessionId,
|
|
270
|
+
agent: this.deps.agent ?? "an agent",
|
|
271
|
+
operation: BROWSER_ACTION,
|
|
272
|
+
target: host,
|
|
273
|
+
fingerprint: digest2(`${BROWSER_ACTION}:${host}`),
|
|
274
|
+
reason: verdict.reason
|
|
275
|
+
};
|
|
276
|
+
const hold = this.deps.hold;
|
|
277
|
+
if (hold === void 0) {
|
|
278
|
+
return {
|
|
279
|
+
allowed: false,
|
|
280
|
+
host,
|
|
281
|
+
message: `${verdict.reason}
|
|
282
|
+
Nobody could be asked, so it was denied. Run the agent under "memnox run".`
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const result = await hold.hold(request);
|
|
286
|
+
if (!holdAllowed2(result)) {
|
|
287
|
+
return { allowed: false, host, message: describeHold2(result, request) };
|
|
288
|
+
}
|
|
289
|
+
this.hosts.remember(sessionId, host);
|
|
290
|
+
return { allowed: true, host };
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// src/record.ts
|
|
295
|
+
import { randomUUID } from "crypto";
|
|
296
|
+
import {
|
|
297
|
+
ACTOR_TYPE,
|
|
298
|
+
ENFORCEMENT_MODE,
|
|
299
|
+
EVENT_SCHEMA_VERSION,
|
|
300
|
+
EVENT_SURFACE,
|
|
301
|
+
TOOL_CLASS
|
|
302
|
+
} from "@memnox/core";
|
|
303
|
+
var CLASSES = Object.values(TOOL_CLASS);
|
|
304
|
+
function classOf(value) {
|
|
305
|
+
return CLASSES.includes(value) ? value : TOOL_CLASS.UNKNOWN;
|
|
306
|
+
}
|
|
307
|
+
function eventFor(input) {
|
|
308
|
+
const { outcome } = input;
|
|
309
|
+
return {
|
|
310
|
+
id: `evt_${randomUUID().replace(/-/g, "").slice(0, 20)}`,
|
|
311
|
+
schemaVersion: EVENT_SCHEMA_VERSION,
|
|
312
|
+
at: input.at,
|
|
313
|
+
sessionId: input.sessionId ?? "ses_local",
|
|
314
|
+
agent: input.agent ?? "an agent",
|
|
315
|
+
actorType: ACTOR_TYPE.AGENT,
|
|
316
|
+
surface: outcome.binary === "git" ? EVENT_SURFACE.GIT : EVENT_SURFACE.SHELL,
|
|
317
|
+
operation: outcome.action,
|
|
318
|
+
class: classOf(outcome.class),
|
|
319
|
+
effect: input.effect,
|
|
320
|
+
mode: ENFORCEMENT_MODE.ENFORCE,
|
|
321
|
+
reason: input.reason,
|
|
322
|
+
// A digest, never the arguments: an argument list is where a secret would be.
|
|
323
|
+
argsDigest: outcome.argsDigest,
|
|
324
|
+
...outcome.target === void 0 ? {} : { target: outcome.target },
|
|
325
|
+
...input.rule === void 0 ? {} : { rule: input.rule },
|
|
326
|
+
...input.policyHash === void 0 ? {} : { policyHash: input.policyHash },
|
|
327
|
+
...input.bundleHash === void 0 ? {} : { bundleHash: input.bundleHash },
|
|
328
|
+
...input.conditionsInForce === void 0 ? {} : { conditionsInForce: input.conditionsInForce },
|
|
329
|
+
...input.exitCode === void 0 ? {} : { exitCode: input.exitCode },
|
|
330
|
+
...input.durationMs === void 0 ? {} : { durationMs: input.durationMs }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async function record(sink, input) {
|
|
334
|
+
if (sink === null) return;
|
|
335
|
+
try {
|
|
336
|
+
await sink.append(eventFor(input));
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/daemon-client.ts
|
|
342
|
+
import { connect } from "net";
|
|
343
|
+
import {
|
|
344
|
+
DAEMON_METHOD,
|
|
345
|
+
decodeResponse,
|
|
346
|
+
encode,
|
|
347
|
+
LineReader,
|
|
348
|
+
socketPathFor
|
|
349
|
+
} from "@memnox/core";
|
|
350
|
+
var DAEMON_TIMEOUT_MS = 250;
|
|
351
|
+
function reportToDaemon(home, options) {
|
|
352
|
+
return speak(home, options.timeoutMs, {
|
|
353
|
+
id: 1,
|
|
354
|
+
method: DAEMON_METHOD.RECORD,
|
|
355
|
+
action: options.action,
|
|
356
|
+
...options.target === void 0 ? {} : { target: options.target },
|
|
357
|
+
...options.sessionId === void 0 ? {} : { sessionId: options.sessionId },
|
|
358
|
+
...options.exitCode === void 0 ? {} : { exitCode: options.exitCode },
|
|
359
|
+
...options.outOfScope === void 0 ? {} : { outOfScope: options.outOfScope },
|
|
360
|
+
...options.costUsd === void 0 ? {} : { costUsd: options.costUsd }
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function speak(home, timeout, message) {
|
|
364
|
+
const path = socketPathFor(home);
|
|
365
|
+
const timeoutMs = timeout ?? DAEMON_TIMEOUT_MS;
|
|
366
|
+
return new Promise((resolve) => {
|
|
367
|
+
let settled = false;
|
|
368
|
+
const finish = (value) => {
|
|
369
|
+
if (settled) return;
|
|
370
|
+
settled = true;
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
socket.destroy();
|
|
373
|
+
resolve(value);
|
|
374
|
+
};
|
|
375
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
376
|
+
timer.unref?.();
|
|
377
|
+
const socket = connect(path, () => {
|
|
378
|
+
socket.write(encode(message));
|
|
379
|
+
});
|
|
380
|
+
const reader = new LineReader();
|
|
381
|
+
socket.setEncoding("utf8");
|
|
382
|
+
socket.on("data", (chunk) => {
|
|
383
|
+
for (const line of reader.push(chunk)) finish(decodeResponse(line));
|
|
384
|
+
});
|
|
385
|
+
socket.on("error", () => finish(null));
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/interceptor-cli.ts
|
|
390
|
+
async function main() {
|
|
391
|
+
const invocation = invokedFor(process.argv);
|
|
392
|
+
if (invocation === null) {
|
|
393
|
+
process.stderr.write(
|
|
394
|
+
"memnox-intercept is run through the wrappers in ~/.memnox/bin, not directly.\nUsage: memnox-intercept <binary> [args...]\n"
|
|
395
|
+
);
|
|
396
|
+
process.exit(2);
|
|
397
|
+
}
|
|
398
|
+
const { binary, args } = invocation;
|
|
399
|
+
const home = homedir();
|
|
400
|
+
const config = await readHookConfig(process.env, home);
|
|
401
|
+
const gate = await loadHookGate(config);
|
|
402
|
+
const overlays = await overlaysInForce(home);
|
|
403
|
+
const provenance = await provenanceOf(home, overlays, (/* @__PURE__ */ new Date()).toISOString());
|
|
404
|
+
const sink = openLedger(home);
|
|
405
|
+
const started = Date.now();
|
|
406
|
+
const outcome = await ruleOnCommand(binary, args, {
|
|
407
|
+
...gate === null ? {} : { gate },
|
|
408
|
+
overlays,
|
|
409
|
+
env: process.env,
|
|
410
|
+
/* Without this an `ask` rule denies instead of asking, which is the difference
|
|
411
|
+
between an agent that can be left running and one that cannot. */
|
|
412
|
+
hold: buildHold(),
|
|
413
|
+
...process.env[SESSION_VAR] === void 0 ? {} : { sessionId: process.env[SESSION_VAR] },
|
|
414
|
+
log
|
|
415
|
+
});
|
|
416
|
+
const held = await pauseHolding(home, process.env[SESSION_VAR]);
|
|
417
|
+
if (held !== null) {
|
|
418
|
+
process.stderr.write(`${pauseMessage(held)}
|
|
419
|
+
`);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
if (!outcome.allowed) {
|
|
423
|
+
process.stderr.write(`${outcome.message ?? "denied"}
|
|
424
|
+
`);
|
|
425
|
+
if (outcome.edited !== void 0) {
|
|
426
|
+
const replacement = splitCommandLine(outcome.edited);
|
|
427
|
+
const [next, ...rest] = replacement;
|
|
428
|
+
if (next !== void 0 && basename2(next) !== INTERCEPT_BINARY) {
|
|
429
|
+
process.stderr.write(`memnox: ruling on the replacement
|
|
430
|
+
`);
|
|
431
|
+
const again = await ruleOnCommand(basename2(next), rest, {
|
|
432
|
+
...gate === null ? {} : { gate },
|
|
433
|
+
overlays,
|
|
434
|
+
env: process.env,
|
|
435
|
+
hold: buildHold(),
|
|
436
|
+
log
|
|
437
|
+
});
|
|
438
|
+
if (again.allowed) {
|
|
439
|
+
await handAndRecord(
|
|
440
|
+
basename2(next),
|
|
441
|
+
rest,
|
|
442
|
+
home,
|
|
443
|
+
sink,
|
|
444
|
+
again,
|
|
445
|
+
started,
|
|
446
|
+
provenance
|
|
447
|
+
);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
process.stderr.write(`${again.message ?? "denied"}
|
|
451
|
+
`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
await record(sink, {
|
|
455
|
+
...provenance,
|
|
456
|
+
outcome,
|
|
457
|
+
effect: DECISION_EFFECT3.DENY,
|
|
458
|
+
reason: outcome.reason ?? "denied",
|
|
459
|
+
...outcome.rule === void 0 ? {} : { rule: outcome.rule },
|
|
460
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
461
|
+
...process.env[SESSION_VAR] === void 0 ? {} : { sessionId: process.env[SESSION_VAR] }
|
|
462
|
+
});
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
if (isBrowserLauncher(binary)) {
|
|
466
|
+
const url = urlArgumentIn(args);
|
|
467
|
+
if (url !== null) {
|
|
468
|
+
const seam = new BrowserSeam({
|
|
469
|
+
...gate === null ? {} : { gate },
|
|
470
|
+
...process.env[SESSION_VAR] === void 0 ? {} : { sessionId: process.env[SESSION_VAR] }
|
|
471
|
+
});
|
|
472
|
+
const visit = await seam.navigate(url);
|
|
473
|
+
if (!visit.allowed) {
|
|
474
|
+
process.stderr.write(`${visit.message ?? "denied"}
|
|
475
|
+
`);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
await handAndRecord(binary, args, home, sink, outcome, started, provenance);
|
|
481
|
+
}
|
|
482
|
+
function openLedger(home) {
|
|
483
|
+
try {
|
|
484
|
+
return SqliteEventStore2.forHome(home);
|
|
485
|
+
} catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function handAndRecord(binary, args, home, sink, outcome, started, provenance) {
|
|
490
|
+
const status = hand(binary, args, home);
|
|
491
|
+
await reportToDaemon(home, {
|
|
492
|
+
action: outcome.action,
|
|
493
|
+
exitCode: status,
|
|
494
|
+
...outcome.target === void 0 ? {} : { target: outcome.target },
|
|
495
|
+
...process.env[SESSION_VAR] === void 0 ? {} : { sessionId: process.env[SESSION_VAR] }
|
|
496
|
+
});
|
|
497
|
+
await record(sink, {
|
|
498
|
+
...provenance,
|
|
499
|
+
outcome,
|
|
500
|
+
effect: DECISION_EFFECT3.ALLOW,
|
|
501
|
+
reason: outcome.reason ?? "no rule matched",
|
|
502
|
+
...outcome.rule === void 0 ? {} : { rule: outcome.rule },
|
|
503
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
504
|
+
exitCode: status,
|
|
505
|
+
durationMs: Date.now() - started,
|
|
506
|
+
...process.env[SESSION_VAR] === void 0 ? {} : { sessionId: process.env[SESSION_VAR] }
|
|
507
|
+
});
|
|
508
|
+
const tripped = await observeSession({
|
|
509
|
+
home,
|
|
510
|
+
sessionId: process.env[SESSION_VAR]
|
|
511
|
+
});
|
|
512
|
+
if (tripped !== null) process.stderr.write(`${pauseMessage(tripped)}
|
|
513
|
+
`);
|
|
514
|
+
process.exit(status);
|
|
515
|
+
}
|
|
516
|
+
function hand(binary, args, home) {
|
|
517
|
+
const path = realPath(process.env["PATH"] ?? "", home);
|
|
518
|
+
const real = resolveReal(binary, path, existsSync);
|
|
519
|
+
if (real === null) {
|
|
520
|
+
process.stderr.write(`memnox: ${binary} is not on PATH behind the interceptor
|
|
521
|
+
`);
|
|
522
|
+
process.exit(127);
|
|
523
|
+
}
|
|
524
|
+
const result = spawnSync(real, [...args], {
|
|
525
|
+
stdio: "inherit",
|
|
526
|
+
env: { ...process.env, PATH: path }
|
|
527
|
+
});
|
|
528
|
+
return result.status ?? 1;
|
|
529
|
+
}
|
|
530
|
+
void main();
|