@mnemom/mnemom 0.17.32 → 0.17.34-next.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/dist/commands/agent-fuel.d.ts +9 -0
- package/dist/commands/agent-fuel.js +36 -2
- package/dist/commands/agent-run.d.ts +50 -0
- package/dist/commands/agent-run.js +221 -0
- package/dist/commands/agent-setup.d.ts +8 -3
- package/dist/commands/agent-setup.js +32 -9
- package/dist/commands/agent-share-fixture.d.ts +3 -1
- package/dist/commands/agent-share-fixture.js +11 -7
- package/dist/commands/agent-share-render.d.ts +23 -0
- package/dist/commands/agent-share-render.js +72 -0
- package/dist/commands/agent-share.d.ts +15 -2
- package/dist/commands/agent-share.js +30 -9
- package/dist/commands/agent.d.ts +34 -4
- package/dist/commands/agent.js +116 -20
- package/dist/commands/protection.js +1 -1
- package/dist/commands/scan.d.ts +97 -0
- package/dist/commands/scan.js +238 -0
- package/dist/index.js +55 -0
- package/dist/lib/api.d.ts +13 -0
- package/dist/lib/api.js +25 -1
- package/dist/lib/auth.d.ts +6 -0
- package/dist/lib/auth.js +161 -14
- package/dist/lib/experimental.d.ts +1 -1
- package/dist/lib/experimental.js +13 -0
- package/dist/lib/oauth.d.ts +7 -0
- package/dist/lib/oauth.js +13 -0
- package/dist/lib/runner/bundle.d.ts +129 -0
- package/dist/lib/runner/bundle.js +319 -0
- package/dist/lib/runner/contract-header.d.ts +46 -0
- package/dist/lib/runner/contract-header.js +85 -0
- package/dist/lib/runner/executor-client.d.ts +71 -0
- package/dist/lib/runner/executor-client.js +97 -0
- package/dist/lib/runner/job.d.ts +70 -0
- package/dist/lib/runner/job.js +463 -0
- package/dist/lib/runner/loop.d.ts +45 -0
- package/dist/lib/runner/loop.js +104 -0
- package/dist/lib/runner/oidc.d.ts +38 -0
- package/dist/lib/runner/oidc.js +89 -0
- package/dist/lib/runner/options.d.ts +41 -0
- package/dist/lib/runner/options.js +47 -0
- package/dist/lib/runner/outcome.d.ts +47 -0
- package/dist/lib/runner/outcome.js +89 -0
- package/dist/lib/runner/phases.d.ts +49 -0
- package/dist/lib/runner/phases.js +99 -0
- package/dist/lib/runner/proxy.d.ts +45 -0
- package/dist/lib/runner/proxy.js +153 -0
- package/dist/lib/runner/settings.d.ts +31 -0
- package/dist/lib/runner/settings.js +44 -0
- package/dist/lib/scan/caps.d.ts +35 -0
- package/dist/lib/scan/caps.js +67 -0
- package/dist/lib/scan/credential.d.ts +37 -0
- package/dist/lib/scan/credential.js +104 -0
- package/dist/lib/scan/failure.d.ts +10 -0
- package/dist/lib/scan/failure.js +43 -0
- package/dist/lib/scan/pr-source.d.ts +57 -0
- package/dist/lib/scan/pr-source.js +130 -0
- package/dist/lib/scan/record.d.ts +49 -0
- package/dist/lib/scan/record.js +85 -0
- package/dist/lib/scan/report-email.d.ts +92 -0
- package/dist/lib/scan/report-email.js +136 -0
- package/dist/lib/scan/report.d.ts +13 -0
- package/dist/lib/scan/report.js +30 -0
- package/dist/lib/scan/scan.d.ts +64 -0
- package/dist/lib/scan/scan.js +182 -0
- package/dist/lib/scan/share.d.ts +38 -0
- package/dist/lib/scan/share.js +47 -0
- package/dist/lib/scan/spend/ai-tool-detect.d.ts +6 -0
- package/dist/lib/scan/spend/ai-tool-detect.js +18 -0
- package/dist/lib/scan/spend/anthropic-usage.d.ts +57 -0
- package/dist/lib/scan/spend/anthropic-usage.js +244 -0
- package/dist/lib/scan/spend/cost-corpus.v1.d.ts +56 -0
- package/dist/lib/scan/spend/cost-corpus.v1.js +64 -0
- package/dist/lib/scan/spend/prices.d.ts +33 -0
- package/dist/lib/scan/spend/prices.js +57 -0
- package/dist/lib/scan/spend/spend-estimate.d.ts +30 -0
- package/dist/lib/scan/spend/spend-estimate.js +70 -0
- package/dist/lib/scan/spend/spend-tiers.d.ts +25 -0
- package/dist/lib/scan/spend/spend-tiers.js +42 -0
- package/dist/lib/scan/spend/token-rate.d.ts +30 -0
- package/dist/lib/scan/spend/token-rate.js +54 -0
- package/dist/lib/session-key.d.ts +41 -0
- package/dist/lib/session-key.js +124 -0
- package/dist/lib/statusline-feed.js +10 -11
- package/dist/lib/statusline.d.ts +6 -0
- package/dist/lib/statusline.js +19 -9
- package/dist/rc-key-401.mjs +75 -0
- package/dist/rc-proxy.mjs +290 -30
- package/package.json +2 -1
package/dist/rc-proxy.mjs
CHANGED
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
//
|
|
14
14
|
// WHAT IT DOES
|
|
15
15
|
// * Listens on 127.0.0.1 (ephemeral port) as an HTTP CONNECT proxy.
|
|
16
|
+
// * Serves only the caller that holds this session's proxy credential (MNE-9233). Loopback is not
|
|
17
|
+
// a boundary: every local process, any other user and every other seat's session can reach the
|
|
18
|
+
// port, and this proxy injects the owner's keys. The launcher mints the credential per session,
|
|
19
|
+
// hands it here on stdin and to Claude Code as the userinfo of HTTPS_PROXY
|
|
20
|
+
// (`http://mnemom:<credential>@127.0.0.1:<port>`), which Claude Code sends as
|
|
21
|
+
// `Proxy-Authorization: Basic` on CONNECT (measured on Claude Code 2.1.312) and on absolute-form
|
|
22
|
+
// requests (its bundled axios builds the header from the proxy URL's userinfo). Any other caller
|
|
23
|
+
// gets 407 before anything is tunnelled, terminated or injected. `Proxy-Authorization` is
|
|
24
|
+
// hop-by-hop: it is never forwarded upstream or to the gateway.
|
|
16
25
|
// * Terminates TLS for api.anthropic.com ONLY, with an ephemeral CA (397 days, key held in
|
|
17
26
|
// memory only) minted at startup into a private 0700 dir under ~/.mnemom/rc (removed on
|
|
18
27
|
// exit; see rc-state.mjs for why not the OS temp dir), and a 7-day leaf re-minted every 24 h
|
|
@@ -52,8 +61,23 @@
|
|
|
52
61
|
// (diagnostic only: it forwards continue-deltas the API rejects).
|
|
53
62
|
//
|
|
54
63
|
// SECRETS
|
|
55
|
-
// Arrive on stdin as
|
|
56
|
-
// empty), OpenAI egress key (may be empty)
|
|
64
|
+
// Arrive on stdin as four newline-terminated lines: Anthropic key, Mnemom key (may be
|
|
65
|
+
// empty), OpenAI egress key (may be empty), the per-session proxy credential (required).
|
|
66
|
+
// Never argv, never env, never disk, never logged.
|
|
67
|
+
//
|
|
68
|
+
// SESSION KEY RENEWAL
|
|
69
|
+
// The Mnemom key on line 2 is the launcher's minted session key (MNE-8245): 24h-TTL'd, and a
|
|
70
|
+
// Remote Control seat outlives it. When the launcher passes MNEMOM_AGENT_RC_CONTROL_FD, that fd is
|
|
71
|
+
// a pipe to it carrying newline-delimited lines both ways:
|
|
72
|
+
// launcher -> proxy `key <key id> <expires at> <key>` swap this key in (before the old expires)
|
|
73
|
+
// `key-failed <reason>` a re-mint this proxy asked for failed
|
|
74
|
+
// proxy -> launcher `remint <key id>` the gateway rejected this key
|
|
75
|
+
// The swap is a single assignment, so every request carries either the old key or the new one.
|
|
76
|
+
// On a gateway 401 that names the Mnemom key (`Invalid Mnemom API key`) for a buffered
|
|
77
|
+
// /v1/messages POST, the proxy holds the 401 back, retries once with a newer key when it already
|
|
78
|
+
// has one, else asks the launcher for one re-mint (single flight, bounded wait) and retries once
|
|
79
|
+
// with it; anything else hands the client the original 401. Never a second retry, and never a
|
|
80
|
+
// request whose body was streamed. Only key ids and expiry times are logged, never a key.
|
|
57
81
|
// The log (MNEMOM_AGENT_RC_PROXY_LOG, else stderr) carries method/path/status/latency and the
|
|
58
82
|
// gateway's `x-mnemom-agent` / `x-mnemom-goal-verdict` response headers only. Gateway error
|
|
59
83
|
// bodies (status ≥ 400) are logged truncated so a 4xx is diagnosable (compressed ones as
|
|
@@ -80,11 +104,18 @@
|
|
|
80
104
|
// is required: it anchors the governed identity.
|
|
81
105
|
// MNEMOM_AGENT_RC_PROXY_LOG append log lines to this file instead of stderr (optional)
|
|
82
106
|
// MNEMOM_AGENT_RC_PARENT_PID exit when this pid is gone, so no orphan holds the keys
|
|
107
|
+
// MNEMOM_AGENT_RC_CONTROL_FD the launcher's control pipe (optional; see SESSION KEY RENEWAL)
|
|
108
|
+
// MNEMOM_AGENT_RC_MNEMOM_KEY_ID the line-2 key's id (optional; logged, and named in `remint`)
|
|
109
|
+
// MNEMOM_AGENT_RC_MNEMOM_KEY_EXPIRES_AT the line-2 key's expiry (optional; logged)
|
|
83
110
|
// MNEMOM_AGENT_RC_PORT listen port (default 0 = ephemeral)
|
|
111
|
+
// MNEMOM_RC_REMINT_WAIT_MS how long a key-rejected request waits for the launcher's re-mint
|
|
112
|
+
// (default 20000; unset or empty means the default, "0" is
|
|
113
|
+
// honoured). The launcher never sets it, only tests do
|
|
84
114
|
// MNEMOM_AGENT_TURN_HEADER 0/off: no x-mnemom-turn header (default on in implicit mode;
|
|
85
115
|
// see turn-origin.mjs)
|
|
86
116
|
//
|
|
87
|
-
// stdout: exactly one line, `READY <port> <ca.pem path>`, once listening. Node ≥ 18,
|
|
117
|
+
// stdout: exactly one line, `READY <port> <ca.pem path>`, once listening (no secret on it). Node ≥ 18,
|
|
118
|
+
// no deps.
|
|
88
119
|
import http from "node:http";
|
|
89
120
|
import https from "node:https";
|
|
90
121
|
import net from "node:net";
|
|
@@ -96,6 +127,12 @@ import { execFileSync } from "node:child_process";
|
|
|
96
127
|
import { TURN_HEADER, TurnOrigins, turnHeaderEnabled, turnHeaderFor } from "./turn-origin.mjs";
|
|
97
128
|
import { OWNER_FILE, ensureRcStateRoot, keepPrivateDir, sweepRcDirs } from "./rc-state.mjs";
|
|
98
129
|
import { assertUsableCert, makeLeafRotator } from "./rc-certs.mjs";
|
|
130
|
+
import {
|
|
131
|
+
KEY_401_BODY_MAX,
|
|
132
|
+
holdBack401,
|
|
133
|
+
isMnemomKeyRejection,
|
|
134
|
+
remintWaitMs,
|
|
135
|
+
} from "./rc-key-401.mjs";
|
|
99
136
|
|
|
100
137
|
const MITM_HOST = "api.anthropic.com";
|
|
101
138
|
const env = process.env;
|
|
@@ -165,7 +202,7 @@ const log = (line, retry = true) => {
|
|
|
165
202
|
process.stderr.write(s);
|
|
166
203
|
};
|
|
167
204
|
|
|
168
|
-
// --- secrets:
|
|
205
|
+
// --- secrets: four lines on stdin -------------------------------------------------------
|
|
169
206
|
let secrets;
|
|
170
207
|
try {
|
|
171
208
|
const lines = fs.readFileSync(0, "utf8").split("\n");
|
|
@@ -173,15 +210,141 @@ try {
|
|
|
173
210
|
anthropic: (lines[0] || "").trim(),
|
|
174
211
|
mnemom: (lines[1] || "").trim(),
|
|
175
212
|
openai: (lines[2] || "").trim(),
|
|
213
|
+
proxy: (lines[3] || "").trim(),
|
|
176
214
|
};
|
|
177
215
|
} catch {
|
|
178
|
-
die("expected
|
|
216
|
+
die("expected four lines on stdin (anthropic, mnemom, openai, proxy credential)");
|
|
179
217
|
}
|
|
180
218
|
if (AGENT_ID && !secrets.mnemom)
|
|
181
219
|
die("a pinned agent (MNEMOM_AGENT_RC_AGENT_ID) needs the Mnemom key (stdin line 2)");
|
|
182
220
|
if (SUBSCRIPTION) {
|
|
183
221
|
if (!secrets.mnemom) die("subscription login needs the Mnemom key (stdin line 2)");
|
|
184
222
|
} else if (!secrets.anthropic) die("the Anthropic key (stdin line 1) is empty");
|
|
223
|
+
// Fail closed: with no credential there is nothing to check a caller against.
|
|
224
|
+
if (!secrets.proxy) die("the proxy credential (stdin line 4) is empty");
|
|
225
|
+
|
|
226
|
+
// --- Mnemom session key renewal (see SESSION KEY RENEWAL) ----------------------------------
|
|
227
|
+
// Key ids and expiry times are the only parts of a key line that may reach the log.
|
|
228
|
+
const KEY_ID_RE = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
229
|
+
const EXPIRES_RE = /^[0-9A-Za-z:.+-]{1,40}$/;
|
|
230
|
+
const KEY_RE = /^[\x21-\x7e]{1,4096}$/;
|
|
231
|
+
const keyMeta = {
|
|
232
|
+
id: KEY_ID_RE.test(env.MNEMOM_AGENT_RC_MNEMOM_KEY_ID || "")
|
|
233
|
+
? env.MNEMOM_AGENT_RC_MNEMOM_KEY_ID
|
|
234
|
+
: "-",
|
|
235
|
+
expires: EXPIRES_RE.test(env.MNEMOM_AGENT_RC_MNEMOM_KEY_EXPIRES_AT || "")
|
|
236
|
+
? env.MNEMOM_AGENT_RC_MNEMOM_KEY_EXPIRES_AT
|
|
237
|
+
: "-",
|
|
238
|
+
};
|
|
239
|
+
// How long a rejected request waits for the launcher's re-mint (rc-key-401.mjs).
|
|
240
|
+
const REMINT_WAIT_MS = remintWaitMs(env);
|
|
241
|
+
const CONTROL_LINE_MAX = 8192;
|
|
242
|
+
let control = null;
|
|
243
|
+
let remint = null; // { promise, resolve, timer } while a re-mint request is outstanding
|
|
244
|
+
|
|
245
|
+
function settleRemint(ok) {
|
|
246
|
+
if (!remint) return;
|
|
247
|
+
const r = remint;
|
|
248
|
+
remint = null;
|
|
249
|
+
clearTimeout(r.timer);
|
|
250
|
+
r.resolve(ok);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Resolves true once a new key has been swapped in, false when none is coming.
|
|
254
|
+
function requestRemint() {
|
|
255
|
+
if (remint) return remint.promise;
|
|
256
|
+
if (!control) return Promise.resolve(false);
|
|
257
|
+
let resolve;
|
|
258
|
+
const promise = new Promise((r) => (resolve = r));
|
|
259
|
+
const timer = setTimeout(() => {
|
|
260
|
+
log(`mnemom key re-mint: no answer from the launcher within ${REMINT_WAIT_MS}ms`);
|
|
261
|
+
settleRemint(false);
|
|
262
|
+
}, REMINT_WAIT_MS);
|
|
263
|
+
timer.unref();
|
|
264
|
+
remint = { promise, resolve, timer };
|
|
265
|
+
control.write(`remint ${keyMeta.id}\n`);
|
|
266
|
+
return promise;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function onControlLine(line) {
|
|
270
|
+
const parts = line.split(" ");
|
|
271
|
+
const cmd = parts[0];
|
|
272
|
+
if (
|
|
273
|
+
cmd === "key" &&
|
|
274
|
+
parts.length === 4 &&
|
|
275
|
+
KEY_ID_RE.test(parts[1]) &&
|
|
276
|
+
EXPIRES_RE.test(parts[2]) &&
|
|
277
|
+
KEY_RE.test(parts[3])
|
|
278
|
+
) {
|
|
279
|
+
const was = keyMeta.id;
|
|
280
|
+
secrets.mnemom = parts[3];
|
|
281
|
+
keyMeta.id = parts[1];
|
|
282
|
+
keyMeta.expires = parts[2];
|
|
283
|
+
log(`mnemom key renewed id=${keyMeta.id} expires=${keyMeta.expires} (was id=${was})`);
|
|
284
|
+
settleRemint(true);
|
|
285
|
+
} else if (cmd === "key-failed") {
|
|
286
|
+
const why = parts
|
|
287
|
+
.slice(1)
|
|
288
|
+
.join(" ")
|
|
289
|
+
.replace(/[^A-Za-z0-9 _.-]/g, "")
|
|
290
|
+
.slice(0, 80);
|
|
291
|
+
log(`mnemom key re-mint failed in the launcher (${why || "-"}); keeping id=${keyMeta.id}`);
|
|
292
|
+
settleRemint(false);
|
|
293
|
+
} else {
|
|
294
|
+
// Never echo the line: a malformed key line still carries a key.
|
|
295
|
+
log(`control: ignored a malformed line (${/^[a-z-]{1,16}$/.test(cmd) ? cmd : "?"})`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const CONTROL_FD = Number(env.MNEMOM_AGENT_RC_CONTROL_FD || 0);
|
|
300
|
+
if (secrets.mnemom && keyMeta.id !== "-")
|
|
301
|
+
log(`mnemom key id=${keyMeta.id} expires=${keyMeta.expires}`);
|
|
302
|
+
if (CONTROL_FD > 2) {
|
|
303
|
+
try {
|
|
304
|
+
const sock = new net.Socket({ fd: CONTROL_FD, readable: true, writable: true });
|
|
305
|
+
control = sock;
|
|
306
|
+
let buf = "";
|
|
307
|
+
sock.on("data", (c) => {
|
|
308
|
+
buf += c.toString("utf8");
|
|
309
|
+
let i;
|
|
310
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
311
|
+
const line = buf.slice(0, i);
|
|
312
|
+
buf = buf.slice(i + 1);
|
|
313
|
+
onControlLine(line);
|
|
314
|
+
}
|
|
315
|
+
if (buf.length > CONTROL_LINE_MAX) {
|
|
316
|
+
buf = "";
|
|
317
|
+
log("control: dropped an oversized line");
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
const gone = (why) => {
|
|
321
|
+
if (control !== sock) return;
|
|
322
|
+
control = null;
|
|
323
|
+
log(`control channel closed (${why}): no further session key renewal`);
|
|
324
|
+
settleRemint(false);
|
|
325
|
+
};
|
|
326
|
+
sock.on("error", (e) => gone(e.code || e.message));
|
|
327
|
+
sock.on("close", () => gone("close"));
|
|
328
|
+
} catch (e) {
|
|
329
|
+
log(`control channel unavailable (${e.code || e.message}): no session key renewal`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// --- caller authentication (MNE-9233; see WHAT IT DOES) -------------------------------------
|
|
334
|
+
// `Proxy-Authorization: Basic base64("mnemom:<credential>")`, compared in constant time. The
|
|
335
|
+
// header value is never logged.
|
|
336
|
+
const PROXY_USER = "mnemom";
|
|
337
|
+
const PROXY_AUTH_EXPECTED = Buffer.from(`${PROXY_USER}:${secrets.proxy}`, "utf8");
|
|
338
|
+
function callerAuthorized(header) {
|
|
339
|
+
const m = /^basic\s+([A-Za-z0-9+/]+={0,2})\s*$/i.exec(typeof header === "string" ? header : "");
|
|
340
|
+
if (!m) return false;
|
|
341
|
+
const got = Buffer.from(m[1], "base64");
|
|
342
|
+
return (
|
|
343
|
+
got.length === PROXY_AUTH_EXPECTED.length && crypto.timingSafeEqual(got, PROXY_AUTH_EXPECTED)
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const PROXY_REALM = 'Basic realm="mnemom-code-rc-proxy"';
|
|
347
|
+
const PROXY_407_NOTE = "407 proxy authentication required (no or wrong Proxy-Authorization)";
|
|
185
348
|
|
|
186
349
|
// --- ephemeral CA + rotating leaf for api.anthropic.com ---------------------------------
|
|
187
350
|
// Private 0700 dir under ~/.mnemom/rc (rc-state.mjs), removed on exit. Claude Code reads NODE_EXTRA_CA_CERTS
|
|
@@ -389,6 +552,7 @@ const upstreamAgent = new https.Agent({ keepAlive: true, maxSockets: 32 });
|
|
|
389
552
|
const HOP = new Set([
|
|
390
553
|
"connection",
|
|
391
554
|
"proxy-connection",
|
|
555
|
+
"proxy-authorization", // this session's proxy credential: never leaves the proxy
|
|
392
556
|
"keep-alive",
|
|
393
557
|
"upgrade",
|
|
394
558
|
"host",
|
|
@@ -490,9 +654,34 @@ function rejectThread(res, tag, thread) {
|
|
|
490
654
|
);
|
|
491
655
|
}
|
|
492
656
|
|
|
657
|
+
// 407 for an absolute-form request without this session's credential: nothing is parsed,
|
|
658
|
+
// forwarded or injected, and the connection is closed.
|
|
659
|
+
function refusePlain(req, res) {
|
|
660
|
+
req.resume(); // discard any body
|
|
661
|
+
let host = "-";
|
|
662
|
+
try {
|
|
663
|
+
host = new URL(req.url).host || "-";
|
|
664
|
+
} catch {
|
|
665
|
+
/* not absolute-form: logged as "-" */
|
|
666
|
+
}
|
|
667
|
+
res.writeHead(407, {
|
|
668
|
+
"proxy-authenticate": PROXY_REALM,
|
|
669
|
+
"content-type": "text/plain",
|
|
670
|
+
connection: "close",
|
|
671
|
+
});
|
|
672
|
+
res.end("mnemom-code-rc-proxy: proxy authentication required\n");
|
|
673
|
+
log(`${req.method} ${host} -> ${PROXY_407_NOTE}`);
|
|
674
|
+
}
|
|
675
|
+
|
|
493
676
|
// One handler serves both the MITM'd TLS stream (origin-form URLs, host = api.anthropic.com)
|
|
494
677
|
// and absolute-form plaintext requests sent straight to the proxy port.
|
|
495
678
|
function handle(req, res, viaPlain) {
|
|
679
|
+
// A request on the proxy port must carry the credential. One inside the MITM'd TLS stream
|
|
680
|
+
// already did, on the CONNECT that opened the tunnel.
|
|
681
|
+
if (viaPlain && !callerAuthorized(req.headers["proxy-authorization"])) {
|
|
682
|
+
refusePlain(req, res);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
496
685
|
let host = MITM_HOST,
|
|
497
686
|
port = 443,
|
|
498
687
|
urlPath = req.url,
|
|
@@ -534,7 +723,7 @@ function handle(req, res, viaPlain) {
|
|
|
534
723
|
delete headers["x-mnemom-agent-id"];
|
|
535
724
|
if (AGENT_ID) headers["x-mnemom-agent-id"] = AGENT_ID;
|
|
536
725
|
headers["x-mnemom-conversation-id"] = CONV;
|
|
537
|
-
|
|
726
|
+
// x-mnemom-api-key is set per attempt in forward(): the key can be swapped mid-session.
|
|
538
727
|
if (PROVIDER) headers["x-mnemom-provider"] = PROVIDER;
|
|
539
728
|
if (secrets.openai) headers["x-mnemom-egress-key"] = secrets.openai;
|
|
540
729
|
if (CONTRACT_B64) headers["x-mnemom-contract"] = CONTRACT_B64;
|
|
@@ -562,20 +751,96 @@ function handle(req, res, viaPlain) {
|
|
|
562
751
|
const tag = `${req.method} ${viaPlain ? host : ""}${urlPath}`;
|
|
563
752
|
const leg = toGateway ? "gateway" : "upstream";
|
|
564
753
|
|
|
754
|
+
// Gateway-leg response fields for the log line.
|
|
755
|
+
const identOf = (ur) => {
|
|
756
|
+
if (!toGateway) return "";
|
|
757
|
+
const goalRev = ur.headers["x-mnemom-goal-revision"];
|
|
758
|
+
const goalThread = ur.headers["x-mnemom-goal-thread"];
|
|
759
|
+
const steer = ur.headers["x-mnemom-goal-steer"];
|
|
760
|
+
return (
|
|
761
|
+
` agent=${ur.headers["x-mnemom-agent"] || "-"} verdict=${ur.headers["x-mnemom-goal-verdict"] || "-"}` +
|
|
762
|
+
(goalRev ? ` goal-rev=${goalRev}` : "") +
|
|
763
|
+
(goalThread ? ` goal-thread=${goalThread}` : "") +
|
|
764
|
+
(steer ? ` steer=${steer}` : "")
|
|
765
|
+
);
|
|
766
|
+
};
|
|
767
|
+
// Gateway error bodies are logged (truncated) so a 4xx is diagnosable — unless the body is
|
|
768
|
+
// compressed, in which case only its encoding + size is useful (`identity` is not a compression).
|
|
769
|
+
const errEncOf = (ur) => {
|
|
770
|
+
const enc = ur.headers["content-encoding"];
|
|
771
|
+
return enc && enc !== "identity" ? enc : "";
|
|
772
|
+
};
|
|
773
|
+
const bodyNoteOf = (errEnc, errBody, errBytes) =>
|
|
774
|
+
errEnc
|
|
775
|
+
? ` body=${errEnc} ${errBytes} bytes`
|
|
776
|
+
: errBody
|
|
777
|
+
? " body=" + errBody.slice(0, 1500).replace(/\s+/g, " ")
|
|
778
|
+
: "";
|
|
779
|
+
|
|
565
780
|
// Open the upstream request only once we know we are forwarding. `body` is the fully
|
|
566
781
|
// buffered (normalised) gateway body; undefined means stream the client request through.
|
|
567
|
-
|
|
782
|
+
// `retried` marks the one retry after a rejected Mnemom key (see SESSION KEY RENEWAL).
|
|
783
|
+
const forward = (body, retried = false) => {
|
|
784
|
+
const usedKey = toGateway ? secrets.mnemom : "";
|
|
785
|
+
const usedKeyId = keyMeta.id;
|
|
786
|
+
// Only this proxy states the Mnemom key, and only to the gateway: a client-supplied one is dropped
|
|
787
|
+
// on every leg, and no upstream other than the gateway door ever sees it.
|
|
788
|
+
delete headers["x-mnemom-api-key"];
|
|
789
|
+
if (usedKey) headers["x-mnemom-api-key"] = usedKey;
|
|
568
790
|
const opts = { ...target, method: req.method, headers };
|
|
569
791
|
if (!net.isIP(target.hostname)) opts.servername = target.hostname; // SNI is for names, not IPs
|
|
570
792
|
const up = (toGateway ? https : proto).request(opts, (ur) => {
|
|
793
|
+
// A 401 for a buffered request may be the key expiring under a long session: hold it back
|
|
794
|
+
// (nothing has reached the client yet) and decide once the whole error body is in.
|
|
795
|
+
if (toGateway && ur.statusCode === 401 && usedKey && body !== undefined && !retried) {
|
|
796
|
+
// Over KEY_401_BODY_MAX it is not the key rejection (and a decompression-bomb guard): it
|
|
797
|
+
// streams to the client as is (rc-key-401.mjs).
|
|
798
|
+
const onOverflow = (held) =>
|
|
799
|
+
log(
|
|
800
|
+
`${tag} -> ${leg} ${ur.statusCode} ${Date.now() - started}ms${identOf(ur)} body=${held} bytes (over ${KEY_401_BODY_MAX}: not a key rejection)`,
|
|
801
|
+
);
|
|
802
|
+
holdBack401(ur, res, {
|
|
803
|
+
onOverflow,
|
|
804
|
+
onBody: (raw) => {
|
|
805
|
+
const ms = Date.now() - started;
|
|
806
|
+
const errEnc = errEncOf(ur);
|
|
807
|
+
const answer = () => {
|
|
808
|
+
if (res.destroyed) return; // the client left while the re-mint was pending
|
|
809
|
+
res.writeHead(ur.statusCode, ur.headers);
|
|
810
|
+
res.end(raw);
|
|
811
|
+
const errBody = errEnc ? "" : raw.toString("utf8", 0, Math.min(raw.length, 1500));
|
|
812
|
+
log(
|
|
813
|
+
`${tag} -> ${leg} ${ur.statusCode} ${ms}ms${identOf(ur)}${bodyNoteOf(errEnc, errBody, raw.length)}`,
|
|
814
|
+
);
|
|
815
|
+
};
|
|
816
|
+
if (!isMnemomKeyRejection(raw, ur.headers["content-encoding"])) {
|
|
817
|
+
answer();
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const newer = secrets.mnemom !== usedKey;
|
|
821
|
+
log(
|
|
822
|
+
`${tag} -> ${leg} 401 ${ms}ms mnemom key id=${usedKeyId} rejected; ${
|
|
823
|
+
newer
|
|
824
|
+
? `retrying once with id=${keyMeta.id}`
|
|
825
|
+
: "asking the launcher for a fresh key"
|
|
826
|
+
}`,
|
|
827
|
+
);
|
|
828
|
+
(newer ? Promise.resolve(true) : requestRemint()).then((ok) => {
|
|
829
|
+
// A key line never carries an empty key (KEY_RE), so a changed key is a usable one.
|
|
830
|
+
if (ok && secrets.mnemom !== usedKey && !res.destroyed) {
|
|
831
|
+
forward(body, true);
|
|
832
|
+
} else {
|
|
833
|
+
answer();
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
},
|
|
837
|
+
});
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
571
840
|
res.writeHead(ur.statusCode, ur.headers);
|
|
572
|
-
// Gateway error bodies are logged (truncated) so a 4xx is diagnosable — unless the
|
|
573
|
-
// body is compressed, in which case only its encoding + size is useful
|
|
574
|
-
// (`identity` is not a compression).
|
|
575
841
|
let errBody = "",
|
|
576
842
|
errBytes = 0;
|
|
577
|
-
const
|
|
578
|
-
const errEnc = enc && enc !== "identity" ? enc : "";
|
|
843
|
+
const errEnc = errEncOf(ur);
|
|
579
844
|
if (toGateway && ur.statusCode >= 400) {
|
|
580
845
|
ur.on("data", (c) => {
|
|
581
846
|
errBytes += c.length;
|
|
@@ -583,25 +848,12 @@ function handle(req, res, viaPlain) {
|
|
|
583
848
|
});
|
|
584
849
|
}
|
|
585
850
|
ur.pipe(res);
|
|
586
|
-
const goalRev = ur.headers["x-mnemom-goal-revision"];
|
|
587
|
-
const goalThread = ur.headers["x-mnemom-goal-thread"];
|
|
588
|
-
const steer = ur.headers["x-mnemom-goal-steer"];
|
|
589
|
-
const ident = toGateway
|
|
590
|
-
? ` agent=${ur.headers["x-mnemom-agent"] || "-"} verdict=${ur.headers["x-mnemom-goal-verdict"] || "-"}` +
|
|
591
|
-
(goalRev ? ` goal-rev=${goalRev}` : "") +
|
|
592
|
-
(goalThread ? ` goal-thread=${goalThread}` : "") +
|
|
593
|
-
(steer ? ` steer=${steer}` : "")
|
|
594
|
-
: "";
|
|
595
851
|
ur.on("end", () => {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
? " body=" + errBody.slice(0, 1500).replace(/\s+/g, " ")
|
|
602
|
-
: "";
|
|
603
|
-
}
|
|
604
|
-
log(`${tag} -> ${leg} ${ur.statusCode} ${Date.now() - started}ms${ident}${bodyNote}`);
|
|
852
|
+
const bodyNote =
|
|
853
|
+
toGateway && ur.statusCode >= 400 ? bodyNoteOf(errEnc, errBody, errBytes) : "";
|
|
854
|
+
log(
|
|
855
|
+
`${tag} -> ${leg} ${ur.statusCode} ${Date.now() - started}ms${identOf(ur)}${bodyNote}${retried ? " (retry)" : ""}`,
|
|
856
|
+
);
|
|
605
857
|
});
|
|
606
858
|
});
|
|
607
859
|
up.on("error", (e) => {
|
|
@@ -681,6 +933,14 @@ proxy.on("connect", (req, sock, head) => {
|
|
|
681
933
|
const [host, portStr] = req.url.split(":");
|
|
682
934
|
const port = Number(portStr || 443);
|
|
683
935
|
sock.on("error", () => {});
|
|
936
|
+
if (!callerAuthorized(req.headers["proxy-authorization"])) {
|
|
937
|
+
// Before the MITM arm and before any upstream socket: an unauthenticated caller gets neither.
|
|
938
|
+
sock.end(
|
|
939
|
+
`HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: ${PROXY_REALM}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n`,
|
|
940
|
+
);
|
|
941
|
+
log(`CONNECT ${req.url} -> ${PROXY_407_NOTE}`);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
684
944
|
if (host === MITM_HOST && port === 443) {
|
|
685
945
|
// A timer can run late (laptop sleep): never hand out a leaf that is about to expire.
|
|
686
946
|
if (leaves.current().notAfter - Date.now() < LEAF_MIN_LEFT_MS) rotateLeaf("expiring");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mnemom/mnemom",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.34-next.1",
|
|
4
4
|
"description": "Transparent AI agent tracing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"test": "vitest"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@mnemom/aip-verifier": "https://github.com/mnemom/aip/releases/download/aip-verifier-v0.6.0/mnemom-aip-verifier-0.6.0.tgz",
|
|
20
21
|
"@mnemom/policy-engine": "^0.3.0",
|
|
21
22
|
"chalk": "^5.3.0",
|
|
22
23
|
"commander": "^12.0.0",
|