@mnemom/mnemom 0.17.2-next.2 → 0.17.2-next.4
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/README.md +33 -0
- package/dist/commands/agent-config.js +3 -0
- package/dist/commands/agent-goal.d.ts +79 -0
- package/dist/commands/agent-goal.js +276 -0
- package/dist/commands/agent-session.d.ts +10 -22
- package/dist/commands/agent-session.js +17 -54
- package/dist/commands/agent-sessions.d.ts +13 -3
- package/dist/commands/agent-sessions.js +46 -4
- package/dist/commands/agent-setup.d.ts +13 -0
- package/dist/commands/agent-setup.js +14 -0
- package/dist/commands/agent.d.ts +41 -2
- package/dist/commands/agent.js +205 -15
- package/dist/index.js +69 -2
- package/dist/lib/agent-ledger.d.ts +8 -0
- package/dist/lib/agent.d.ts +123 -6
- package/dist/lib/agent.js +106 -9
- package/dist/lib/goal-api.d.ts +133 -0
- package/dist/lib/goal-api.js +113 -0
- package/dist/lib/launcher-config.d.ts +6 -0
- package/dist/lib/launcher-config.js +15 -0
- package/dist/mcp/handoff-server.mjs +331 -0
- package/dist/rc-proxy.mjs +113 -37
- package/package.json +1 -1
package/dist/rc-proxy.mjs
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
//
|
|
14
14
|
// WHAT IT DOES
|
|
15
15
|
// * Listens on 127.0.0.1 (ephemeral port) as an HTTP CONNECT proxy.
|
|
16
|
-
// * Terminates TLS for api.anthropic.com ONLY, with an ephemeral CA
|
|
17
|
-
// startup into a private 0700 tmpdir (
|
|
18
|
-
//
|
|
16
|
+
// * Terminates TLS for api.anthropic.com ONLY, with an ephemeral CA (397 days, key held in
|
|
17
|
+
// memory only) minted at startup into a private 0700 tmpdir (removed on exit), and a 7-day
|
|
18
|
+
// leaf re-minted every 24 h and hot-swapped (see below). Every other host is tunnelled
|
|
19
|
+
// byte-for-byte (telemetry, Datadog, MCP servers…).
|
|
19
20
|
// * Inside api.anthropic.com, only `POST /v1/messages[?…]` is rewritten to the Mnemom
|
|
20
21
|
// gateway door: the real Anthropic key rides as `x-api-key` (API-key billing), plus
|
|
21
22
|
// `x-mnemom-agent`, `x-mnemom-conversation-id`, and optionally `x-mnemom-api-key`,
|
|
@@ -79,6 +80,7 @@ import net from "node:net";
|
|
|
79
80
|
import fs from "node:fs";
|
|
80
81
|
import os from "node:os";
|
|
81
82
|
import path from "node:path";
|
|
83
|
+
import crypto from "node:crypto";
|
|
82
84
|
import { execFileSync } from "node:child_process";
|
|
83
85
|
|
|
84
86
|
const MITM_HOST = "api.anthropic.com";
|
|
@@ -140,14 +142,27 @@ try {
|
|
|
140
142
|
}
|
|
141
143
|
if (!secrets.anthropic) die("the Anthropic key (stdin line 1) is empty");
|
|
142
144
|
|
|
143
|
-
// --- ephemeral CA + leaf for api.anthropic.com
|
|
144
|
-
// Private 0700 dir under the OS tmpdir
|
|
145
|
-
//
|
|
146
|
-
//
|
|
145
|
+
// --- ephemeral CA + rotating leaf for api.anthropic.com ---------------------------------
|
|
146
|
+
// Private 0700 dir under the OS tmpdir, removed on exit. Claude Code reads NODE_EXTRA_CA_CERTS
|
|
147
|
+
// once at process start, so the CA it trusts can never change mid-session: the CA is valid for
|
|
148
|
+
// CA_DAYS and its private key lives in memory only (read into a Buffer, then deleted from disk).
|
|
149
|
+
// The leaf is short-lived and re-minted from that key every LEAF_ROTATE_MS — and on demand when
|
|
150
|
+
// it has under LEAF_MIN_LEFT_MS left — then hot-swapped with setSecureContext, so a running
|
|
151
|
+
// session picks it up on its next TLS handshake. (A 2-day CA + leaf with the CA key deleted at
|
|
152
|
+
// startup broke every session older than 48 h with "SSL certificate has expired".) openssl only
|
|
153
|
+
// takes the CA key from a file, so each signing writes it 0600 into caDir and deletes it right
|
|
154
|
+
// after. Works with macOS's system LibreSSL (3.3+) and OpenSSL 1.1.1+ (`-addext`).
|
|
155
|
+
// MNEMOM_RC_LEAF_ROTATE_MS / MNEMOM_RC_LEAF_DAYS exist for tests only.
|
|
156
|
+
const CA_DAYS = 397;
|
|
157
|
+
const LEAF_DAYS = Number(env.MNEMOM_RC_LEAF_DAYS || 7);
|
|
158
|
+
const LEAF_ROTATE_MS = Number(env.MNEMOM_RC_LEAF_ROTATE_MS || 24 * 60 * 60 * 1000);
|
|
159
|
+
const LEAF_MIN_LEFT_MS = 24 * 60 * 60 * 1000;
|
|
147
160
|
const caDir = fs.mkdtempSync(path.join(os.tmpdir(), "mnemom-code-rc-ca-"));
|
|
148
161
|
fs.chmodSync(caDir, 0o700);
|
|
149
162
|
const p = (f) => path.join(caDir, f);
|
|
163
|
+
let rotateTimer = null;
|
|
150
164
|
const cleanup = () => {
|
|
165
|
+
clearInterval(rotateTimer); // a no-op while still null
|
|
151
166
|
try {
|
|
152
167
|
fs.rmSync(caDir, { recursive: true, force: true });
|
|
153
168
|
} catch {
|
|
@@ -156,8 +171,12 @@ const cleanup = () => {
|
|
|
156
171
|
};
|
|
157
172
|
process.on("exit", cleanup);
|
|
158
173
|
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(sig, () => process.exit(0));
|
|
174
|
+
const ossl = (...a) => execFileSync("openssl", a, { stdio: ["ignore", "ignore", "pipe"] });
|
|
175
|
+
// openssl's own stderr when it has any, else the error itself (spawn / fs failures).
|
|
176
|
+
const osslErr = (e) => (e.stderr && e.stderr.length ? e.stderr.toString().trim() : e.message);
|
|
177
|
+
|
|
178
|
+
let caKey; // Buffer — never on disk outside mintLeaf()'s signing call
|
|
159
179
|
try {
|
|
160
|
-
const ossl = (...a) => execFileSync("openssl", a, { stdio: ["ignore", "ignore", "pipe"] });
|
|
161
180
|
ossl(
|
|
162
181
|
"req",
|
|
163
182
|
"-x509",
|
|
@@ -171,7 +190,7 @@ try {
|
|
|
171
190
|
"-out",
|
|
172
191
|
p("ca.pem"),
|
|
173
192
|
"-days",
|
|
174
|
-
|
|
193
|
+
String(CA_DAYS),
|
|
175
194
|
"-subj",
|
|
176
195
|
"/CN=mnemom agent local CA (ephemeral)",
|
|
177
196
|
"-addext",
|
|
@@ -179,6 +198,19 @@ try {
|
|
|
179
198
|
"-addext",
|
|
180
199
|
"keyUsage=critical,keyCertSign,cRLSign",
|
|
181
200
|
);
|
|
201
|
+
caKey = fs.readFileSync(p("ca.key"));
|
|
202
|
+
fs.rmSync(p("ca.key"));
|
|
203
|
+
fs.writeFileSync(
|
|
204
|
+
p("leaf.ext"),
|
|
205
|
+
`subjectAltName=DNS:${MITM_HOST}\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n`,
|
|
206
|
+
);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
fs.rmSync(p("ca.key"), { force: true });
|
|
209
|
+
die(`could not mint the ephemeral CA with openssl: ${osslErr(e)}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Mint a fresh leaf key + cert signed by the in-memory CA key. Returns { key, cert, notAfter }.
|
|
213
|
+
function mintLeaf() {
|
|
182
214
|
ossl(
|
|
183
215
|
"req",
|
|
184
216
|
"-newkey",
|
|
@@ -193,32 +225,41 @@ try {
|
|
|
193
225
|
"-subj",
|
|
194
226
|
`/CN=${MITM_HOST}`,
|
|
195
227
|
);
|
|
196
|
-
|
|
197
|
-
p("
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
228
|
+
try {
|
|
229
|
+
fs.writeFileSync(p("ca.key"), caKey, { mode: 0o600, flag: "wx" });
|
|
230
|
+
ossl(
|
|
231
|
+
"x509",
|
|
232
|
+
"-req",
|
|
233
|
+
"-in",
|
|
234
|
+
p("leaf.csr"),
|
|
235
|
+
"-CA",
|
|
236
|
+
p("ca.pem"),
|
|
237
|
+
"-CAkey",
|
|
238
|
+
p("ca.key"),
|
|
239
|
+
"-CAcreateserial",
|
|
240
|
+
"-out",
|
|
241
|
+
p("leaf.pem"),
|
|
242
|
+
"-days",
|
|
243
|
+
String(LEAF_DAYS),
|
|
244
|
+
"-extfile",
|
|
245
|
+
p("leaf.ext"),
|
|
246
|
+
);
|
|
247
|
+
} finally {
|
|
248
|
+
fs.rmSync(p("ca.key"), { force: true });
|
|
249
|
+
}
|
|
250
|
+
const cert = fs.readFileSync(p("leaf.pem"));
|
|
251
|
+
return {
|
|
252
|
+
key: fs.readFileSync(p("leaf.key")),
|
|
253
|
+
cert,
|
|
254
|
+
notAfter: Date.parse(new crypto.X509Certificate(cert).validTo),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let leaf;
|
|
259
|
+
try {
|
|
260
|
+
leaf = mintLeaf();
|
|
218
261
|
} catch (e) {
|
|
219
|
-
die(
|
|
220
|
-
`could not mint the ephemeral CA with openssl: ${e.stderr ? e.stderr.toString() : e.message}`,
|
|
221
|
-
);
|
|
262
|
+
die(`could not mint the ephemeral CA with openssl: ${osslErr(e)}`);
|
|
222
263
|
}
|
|
223
264
|
|
|
224
265
|
// --- request handling ---------------------------------------------------------------------
|
|
@@ -299,6 +340,23 @@ const THREAD_400 = Buffer.from(
|
|
|
299
340
|
}),
|
|
300
341
|
);
|
|
301
342
|
let localSeq = 0;
|
|
343
|
+
|
|
344
|
+
// The Claude Code session ids seen on the /v1/messages leg (`x-claude-code-session-id`, not a
|
|
345
|
+
// secret): each NEW one is logged once as `cc-session=<id>`, so the server shape (many sessions in
|
|
346
|
+
// one proxy) gets a local conversation ↔ CC session record. Bounded; the id is clipped to a
|
|
347
|
+
// uuid-ish charset so a hostile header cannot forge log lines.
|
|
348
|
+
const CC_SESSION_SEEN_MAX = 256;
|
|
349
|
+
const ccSessionsSeen = new Set();
|
|
350
|
+
function noteCcSession(raw) {
|
|
351
|
+
const id = String(raw ?? "")
|
|
352
|
+
.replace(/[^A-Za-z0-9._:-]/g, "")
|
|
353
|
+
.slice(0, 64);
|
|
354
|
+
if (!id || ccSessionsSeen.has(id)) return;
|
|
355
|
+
if (ccSessionsSeen.size >= CC_SESSION_SEEN_MAX) ccSessionsSeen.clear();
|
|
356
|
+
ccSessionsSeen.add(id);
|
|
357
|
+
log(`cc-session=${id} conversation=${CONV}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
302
360
|
function rejectThread(res, tag, thread) {
|
|
303
361
|
res.writeHead(400, {
|
|
304
362
|
"content-type": "application/json",
|
|
@@ -344,6 +402,7 @@ function handle(req, res, viaPlain) {
|
|
|
344
402
|
path: GATEWAY.pathname.replace(/\/$/, "") + urlPath,
|
|
345
403
|
agent: gatewayAgent,
|
|
346
404
|
};
|
|
405
|
+
noteCcSession(headers["x-claude-code-session-id"]);
|
|
347
406
|
delete headers.authorization; // the OAuth bearer stays local
|
|
348
407
|
headers["x-api-key"] = secrets.anthropic;
|
|
349
408
|
headers["x-mnemom-agent"] = AGENT;
|
|
@@ -398,10 +457,12 @@ function handle(req, res, viaPlain) {
|
|
|
398
457
|
}
|
|
399
458
|
ur.pipe(res);
|
|
400
459
|
const goalRev = ur.headers["x-mnemom-goal-revision"];
|
|
460
|
+
const goalThread = ur.headers["x-mnemom-goal-thread"];
|
|
401
461
|
const steer = ur.headers["x-mnemom-goal-steer"];
|
|
402
462
|
const ident = toGateway
|
|
403
463
|
? ` agent=${ur.headers["x-mnemom-agent"] || "-"} verdict=${ur.headers["x-mnemom-goal-verdict"] || "-"}` +
|
|
404
464
|
(goalRev ? ` goal-rev=${goalRev}` : "") +
|
|
465
|
+
(goalThread ? ` goal-thread=${goalThread}` : "") +
|
|
405
466
|
(steer ? ` steer=${steer}` : "")
|
|
406
467
|
: "";
|
|
407
468
|
ur.on("end", () => {
|
|
@@ -459,18 +520,33 @@ function handle(req, res, viaPlain) {
|
|
|
459
520
|
}
|
|
460
521
|
}
|
|
461
522
|
|
|
462
|
-
const tlsServer = https.createServer(
|
|
463
|
-
|
|
464
|
-
(req, res) => handle(req, res, false),
|
|
523
|
+
const tlsServer = https.createServer({ key: leaf.key, cert: leaf.cert }, (req, res) =>
|
|
524
|
+
handle(req, res, false),
|
|
465
525
|
);
|
|
466
526
|
tlsServer.on("tlsClientError", (e) => log(`tls client error: ${e.code || e.message}`));
|
|
467
527
|
|
|
528
|
+
// Re-mint the leaf and hot-swap it; established TLS connections keep the old one, every new
|
|
529
|
+
// handshake gets the new one. A failed re-mint keeps serving the current leaf.
|
|
530
|
+
function rotateLeaf(why) {
|
|
531
|
+
try {
|
|
532
|
+
leaf = mintLeaf();
|
|
533
|
+
tlsServer.setSecureContext({ key: leaf.key, cert: leaf.cert });
|
|
534
|
+
log(`leaf rotated (${why}) notAfter=${new Date(leaf.notAfter).toISOString()}`);
|
|
535
|
+
} catch (e) {
|
|
536
|
+
log(`leaf rotation FAILED (${why}), keeping the current leaf: ${osslErr(e)}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
rotateTimer = setInterval(() => rotateLeaf("interval"), LEAF_ROTATE_MS);
|
|
540
|
+
rotateTimer.unref();
|
|
541
|
+
|
|
468
542
|
const proxy = http.createServer((req, res) => handle(req, res, true));
|
|
469
543
|
proxy.on("connect", (req, sock, head) => {
|
|
470
544
|
const [host, portStr] = req.url.split(":");
|
|
471
545
|
const port = Number(portStr || 443);
|
|
472
546
|
sock.on("error", () => {});
|
|
473
547
|
if (host === MITM_HOST && port === 443) {
|
|
548
|
+
// A timer can run late (laptop sleep): never hand out a leaf that is about to expire.
|
|
549
|
+
if (leaf.notAfter - Date.now() < LEAF_MIN_LEFT_MS) rotateLeaf("expiring");
|
|
474
550
|
sock.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
475
551
|
if (head && head.length) sock.unshift(head);
|
|
476
552
|
tlsServer.emit("connection", sock);
|