@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,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildAuthorizer,
|
|
4
|
+
buildHold,
|
|
5
|
+
buildLeases,
|
|
6
|
+
log
|
|
7
|
+
} from "./chunk-SB27OD2T.js";
|
|
8
|
+
|
|
9
|
+
// src/shell-cli.ts
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
|
+
|
|
12
|
+
// src/shell-seam.ts
|
|
13
|
+
import {
|
|
14
|
+
DECISION_EFFECT,
|
|
15
|
+
digest,
|
|
16
|
+
isAllowed as holdAllowed,
|
|
17
|
+
leasePathFor,
|
|
18
|
+
leaseScopeFor,
|
|
19
|
+
proceeds,
|
|
20
|
+
resolveShellLine,
|
|
21
|
+
takesLease
|
|
22
|
+
} from "@memnox/core";
|
|
23
|
+
var SHELL_ACTION = "shell.execute";
|
|
24
|
+
var SHELL_EXIT_OK = 0;
|
|
25
|
+
var SHELL_EXIT_WITHHELD = 77;
|
|
26
|
+
var SEVERITY = {
|
|
27
|
+
[DECISION_EFFECT.ALLOW]: 0,
|
|
28
|
+
[DECISION_EFFECT.ASK]: 1,
|
|
29
|
+
[DECISION_EFFECT.DENY]: 2
|
|
30
|
+
};
|
|
31
|
+
function worse(a, b) {
|
|
32
|
+
return (SEVERITY[b.effect] ?? 0) > (SEVERITY[a.effect] ?? 0) ? b : a;
|
|
33
|
+
}
|
|
34
|
+
var ShellSeam = class {
|
|
35
|
+
constructor(deps) {
|
|
36
|
+
this.deps = deps;
|
|
37
|
+
}
|
|
38
|
+
deps;
|
|
39
|
+
async gate(command) {
|
|
40
|
+
if (command.length === 0) {
|
|
41
|
+
return { message: "no command to run", exitCode: SHELL_EXIT_WITHHELD };
|
|
42
|
+
}
|
|
43
|
+
const line = command.join(" ");
|
|
44
|
+
let verdict = await this.deps.authorizer.authorize(
|
|
45
|
+
this.requestFor(SHELL_ACTION, line)
|
|
46
|
+
);
|
|
47
|
+
for (const resolved of resolveShellLine(line, this.deps.env ?? {}).actions) {
|
|
48
|
+
if (resolved.action === SHELL_ACTION) continue;
|
|
49
|
+
const request = this.requestFor(resolved.action, resolved.target ?? line);
|
|
50
|
+
verdict = worse(verdict, await this.deps.authorizer.authorize(request));
|
|
51
|
+
}
|
|
52
|
+
if (verdict.effect === DECISION_EFFECT.ASK) {
|
|
53
|
+
const answered = await this.askAbout(line, verdict);
|
|
54
|
+
if (answered !== null) return answered;
|
|
55
|
+
} else if (verdict.effect !== DECISION_EFFECT.ALLOW) {
|
|
56
|
+
return { message: describe(verdict), exitCode: SHELL_EXIT_WITHHELD };
|
|
57
|
+
}
|
|
58
|
+
const held = await this.claim(line);
|
|
59
|
+
if (held !== null) return held;
|
|
60
|
+
return { run: command, exitCode: SHELL_EXIT_OK };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Puts an ASK to a person. Null when it was allowed and the line may proceed.
|
|
64
|
+
*
|
|
65
|
+
* Withheld rather than run when nobody answers: a walk-away must not become a yes,
|
|
66
|
+
* and a timeout is said differently from a refusal so the two read differently.
|
|
67
|
+
*/
|
|
68
|
+
async askAbout(line, verdict) {
|
|
69
|
+
const hold = this.deps.hold;
|
|
70
|
+
if (hold === void 0) {
|
|
71
|
+
return {
|
|
72
|
+
message: `${describe(verdict)}
|
|
73
|
+
Nobody could be asked, so it was withheld. Run the agent under "memnox run".`,
|
|
74
|
+
exitCode: SHELL_EXIT_WITHHELD
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const result = await hold.hold({
|
|
78
|
+
sessionId: this.deps.sessionId ?? "ses_local",
|
|
79
|
+
agent: "an agent",
|
|
80
|
+
operation: SHELL_ACTION,
|
|
81
|
+
fingerprint: digest(line),
|
|
82
|
+
reason: verdict.reason,
|
|
83
|
+
command: line
|
|
84
|
+
});
|
|
85
|
+
if (holdAllowed(result)) return null;
|
|
86
|
+
return { message: describe(verdict), exitCode: SHELL_EXIT_WITHHELD };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Takes the paths this line writes, or answers with who is already on them. Reads
|
|
90
|
+
* never reach the register: `takesLease` decides that here, so there is no path
|
|
91
|
+
* through this seam that can make a reader wait.
|
|
92
|
+
*/
|
|
93
|
+
async claim(line) {
|
|
94
|
+
const leases = this.deps.leases;
|
|
95
|
+
if (leases === void 0) return null;
|
|
96
|
+
for (const resolved of resolveShellLine(line, this.deps.env ?? {}).actions) {
|
|
97
|
+
if (!takesLease(String(resolved.class))) continue;
|
|
98
|
+
const path = leasePathFor(
|
|
99
|
+
resolved.target,
|
|
100
|
+
leases.repositoryRoot,
|
|
101
|
+
this.deps.workingDirectory ?? leases.repositoryRoot
|
|
102
|
+
);
|
|
103
|
+
if (path === null) continue;
|
|
104
|
+
const scope = leaseScopeFor(path, leases.isDirectory);
|
|
105
|
+
const verdict = await leases.gate.claim(
|
|
106
|
+
scope,
|
|
107
|
+
leases.holder,
|
|
108
|
+
`${resolved.action} ${resolved.target ?? ""}`.trim()
|
|
109
|
+
);
|
|
110
|
+
if (proceeds(verdict)) continue;
|
|
111
|
+
return {
|
|
112
|
+
message: verdict.message ?? `${scope} is held by another agent.`,
|
|
113
|
+
exitCode: SHELL_EXIT_WITHHELD
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
requestFor(action, target) {
|
|
119
|
+
return {
|
|
120
|
+
action,
|
|
121
|
+
target,
|
|
122
|
+
// LOCAL ONLY. The SDK strips this before anything reaches the runtime.
|
|
123
|
+
arguments: { command: target },
|
|
124
|
+
...this.deps.sessionId === void 0 ? {} : { sessionId: this.deps.sessionId },
|
|
125
|
+
...this.deps.workingDirectory === void 0 ? {} : { workingDirectory: this.deps.workingDirectory }
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
function describe(verdict) {
|
|
130
|
+
const parts = [verdict.reason];
|
|
131
|
+
if (verdict.alternative !== void 0) {
|
|
132
|
+
const target = verdict.alternative.resource === void 0 ? "" : ` ${verdict.alternative.resource}`;
|
|
133
|
+
parts.push(
|
|
134
|
+
`Instead: ${verdict.alternative.action}${target} \u2014 ${verdict.alternative.note}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (verdict.approvalId !== void 0) {
|
|
138
|
+
parts.push(`Ask a person: memnox approvals resolve ${verdict.approvalId} --by <you>`);
|
|
139
|
+
}
|
|
140
|
+
if (verdict.decisionId !== void 0)
|
|
141
|
+
parts.push(`Why: memnox why ${verdict.decisionId}`);
|
|
142
|
+
return parts.join(" ");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/shell-invocation.ts
|
|
146
|
+
import { basename } from "path";
|
|
147
|
+
var SHELL_MODE = {
|
|
148
|
+
/** `-c "<line>"`: the POSIX contract, and the only form an agent actually uses. */
|
|
149
|
+
COMMAND: "command",
|
|
150
|
+
/** `-- cmd args`: how a person or a test drives the wrapper directly. */
|
|
151
|
+
ARGV: "argv",
|
|
152
|
+
/** No command at all. Hand the terminal to the real shell rather than refuse it. */
|
|
153
|
+
INTERACTIVE: "interactive"
|
|
154
|
+
};
|
|
155
|
+
function commandFlag(argument) {
|
|
156
|
+
return /^-[a-z]*c$/.test(argument);
|
|
157
|
+
}
|
|
158
|
+
function shellInvocation(argv) {
|
|
159
|
+
const separator = argv.indexOf("--");
|
|
160
|
+
if (separator !== -1) {
|
|
161
|
+
return { mode: SHELL_MODE.ARGV, argv: [...argv.slice(separator + 1)], flags: [] };
|
|
162
|
+
}
|
|
163
|
+
const flags = [];
|
|
164
|
+
for (const [index, argument] of argv.entries()) {
|
|
165
|
+
if (commandFlag(argument)) {
|
|
166
|
+
const line = argv[index + 1];
|
|
167
|
+
if (line === void 0) break;
|
|
168
|
+
return { mode: SHELL_MODE.COMMAND, line, flags };
|
|
169
|
+
}
|
|
170
|
+
if (argument.startsWith("-")) {
|
|
171
|
+
flags.push(argument);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
return { mode: SHELL_MODE.ARGV, argv: [...argv.slice(index)], flags };
|
|
175
|
+
}
|
|
176
|
+
return { mode: SHELL_MODE.INTERACTIVE, flags };
|
|
177
|
+
}
|
|
178
|
+
var REAL_SHELL_VAR = "MEMNOX_REAL_SHELL";
|
|
179
|
+
var FALLBACK_SHELL = "/bin/sh";
|
|
180
|
+
function realShell(env, self) {
|
|
181
|
+
const named = env[REAL_SHELL_VAR];
|
|
182
|
+
if (named !== void 0 && named !== "" && basename(named) !== basename(self)) {
|
|
183
|
+
return named;
|
|
184
|
+
}
|
|
185
|
+
const inherited = env["SHELL"];
|
|
186
|
+
if (inherited !== void 0 && inherited !== "" && basename(inherited) !== basename(self)) {
|
|
187
|
+
return inherited;
|
|
188
|
+
}
|
|
189
|
+
return FALLBACK_SHELL;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/shell-cli.ts
|
|
193
|
+
var USAGE = `Usage: memnox-shell -c "<command line>"
|
|
194
|
+
memnox-shell -- <command...>
|
|
195
|
+
|
|
196
|
+
Gates what was asked against policy, then runs it unchanged through the real shell.
|
|
197
|
+
A refusal names an alternative where the rule gave one, and nothing is ever rewritten.`;
|
|
198
|
+
var SHELL_NAME = "memnox-shell";
|
|
199
|
+
function commandOf(invocation) {
|
|
200
|
+
if (invocation.mode === SHELL_MODE.COMMAND) return [invocation.line ?? ""];
|
|
201
|
+
return invocation.argv ?? [];
|
|
202
|
+
}
|
|
203
|
+
function run(executable, args) {
|
|
204
|
+
const child = spawn(executable, args, { stdio: "inherit" });
|
|
205
|
+
child.on("exit", (code, signal) => {
|
|
206
|
+
process.exitCode = code === null ? signal === null ? SHELL_EXIT_WITHHELD : 128 : code;
|
|
207
|
+
});
|
|
208
|
+
child.on("error", (err) => {
|
|
209
|
+
log(`could not run the command: ${String(err)}`);
|
|
210
|
+
process.exitCode = SHELL_EXIT_WITHHELD;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async function main() {
|
|
214
|
+
const invocation = shellInvocation(process.argv.slice(2));
|
|
215
|
+
const shell = realShell(process.env, SHELL_NAME);
|
|
216
|
+
if (invocation.mode === SHELL_MODE.INTERACTIVE) {
|
|
217
|
+
if (process.stdin.isTTY !== true) {
|
|
218
|
+
process.stderr.write(`${USAGE}
|
|
219
|
+
`);
|
|
220
|
+
process.exitCode = SHELL_EXIT_WITHHELD;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
run(shell, invocation.flags);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const command = commandOf(invocation);
|
|
227
|
+
if (command.length === 0 || command[0] === "") {
|
|
228
|
+
process.stderr.write(`${USAGE}
|
|
229
|
+
`);
|
|
230
|
+
process.exitCode = SHELL_EXIT_WITHHELD;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const leases = buildLeases();
|
|
234
|
+
const seam = new ShellSeam({
|
|
235
|
+
authorizer: await buildAuthorizer(),
|
|
236
|
+
workingDirectory: process.cwd(),
|
|
237
|
+
env: process.env,
|
|
238
|
+
hold: buildHold(),
|
|
239
|
+
...leases === void 0 ? {} : { leases }
|
|
240
|
+
});
|
|
241
|
+
const outcome = await seam.gate(command);
|
|
242
|
+
if (outcome.message !== void 0) log(outcome.message);
|
|
243
|
+
if (outcome.run === void 0) {
|
|
244
|
+
process.exitCode = outcome.exitCode;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (invocation.mode === SHELL_MODE.COMMAND) {
|
|
248
|
+
run(shell, [...invocation.flags, "-c", invocation.line ?? ""]);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const [executable, ...args] = outcome.run;
|
|
252
|
+
if (executable === void 0) return;
|
|
253
|
+
run(executable, args);
|
|
254
|
+
}
|
|
255
|
+
main().catch((err) => {
|
|
256
|
+
log(`shell seam failed, ruling on nothing: ${String(err)}`);
|
|
257
|
+
process.exitCode = SHELL_EXIT_WITHHELD;
|
|
258
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@memnox/interceptors",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "The local seams — a PreToolUse hook, a shell wrapper, a git credential helper, an egress proxy and a Docker socket gate — that route what an agent does through Memnox before it happens.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"hooks",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"seam",
|
|
9
|
+
"ai-agents",
|
|
10
|
+
"security"
|
|
11
|
+
],
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"author": "Memnox",
|
|
14
|
+
"homepage": "https://github.com/Memnox/memnox/tree/main/packages/interceptors#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/Memnox/memnox.git",
|
|
18
|
+
"directory": "packages/interceptors"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/Memnox/memnox/issues"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"provenance": true
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./interceptor-cli": "./dist/interceptor-cli.js",
|
|
36
|
+
"./shell-cli": "./dist/shell-cli.js",
|
|
37
|
+
"./git-credential-cli": "./dist/git-credential-cli.js",
|
|
38
|
+
"./egress-cli": "./dist/egress-cli.js"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
],
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@memnox/core": "0.1.1"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup"
|
|
53
|
+
}
|
|
54
|
+
}
|