@mnemom/mnemom 0.17.0-next.5 → 0.17.0-next.7

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/rc-proxy.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // mnemom-code-rc-proxy — the local proxy behind ``mnemom code … --remote-control``.
2
+ // mnemom-code-rc-proxy — the local proxy behind ``mnemom agent … --remote-control``.
3
3
  //
4
4
  // WHY THIS EXISTS
5
5
  // Claude Code's `remote-control` subcommand (drive a local session from claude.ai / the
6
6
  // mobile app) is gated: it refuses to start unless the CLI believes it talks to
7
7
  // api.anthropic.com with a claude.ai (OAuth) login — no ANTHROPIC_BASE_URL, no
8
8
  // ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN, no ANTHROPIC_UNIX_SOCKET. Every knob the
9
- // normal `mnemom code` launch uses to reach the Mnemom gateway trips that gate.
9
+ // normal `mnemom agent` launch uses to reach the Mnemom gateway trips that gate.
10
10
  // HTTPS_PROXY + NODE_EXTRA_CA_CERTS are honoured and NOT gated (measured 2026-09-13,
11
11
  // Claude Code 2.1.269). So `--remote-control` runs Claude Code untouched — OAuth login,
12
12
  // first-party base URL — and puts this proxy in front of it.
@@ -28,7 +28,7 @@
28
28
  // * Serves BOTH request forms: CONNECT tunnels and absolute-form plaintext requests to the
29
29
  // proxy port. Remote Control registration and the environment work-poll loop use the
30
30
  // latter for https URLs; a CONNECT-only proxy answers 405 and registration fails.
31
- // * Body handling on the gateway leg (see the mnemom code docs (Remote Control)):
31
+ // * Body handling on the gateway leg (see the mnemom agent docs (Remote Control)):
32
32
  // - deletes every `cache_control.ttl` — Claude Code on claude.ai auth marks blocks
33
33
  // `ttl:"1h"`; the gateway prepends its own 5m-cached system block and Anthropic
34
34
  // rejects a 1h block that follows a 5m one (400). Forwarded bodies only.
@@ -46,105 +46,188 @@
46
46
  // one more local 400: typically one or two `-> local 400` lines early in the
47
47
  // session, each an instant local round trip, and zero gateway 400s. A `thread: null`
48
48
  // is not a thread — the key is deleted and the body forwarded.
49
- // MNEMOM_CODE_RC_THREAD_MODE=strip restores the legacy delete-and-forward behaviour
49
+ // MNEMOM_AGENT_RC_THREAD_MODE=strip restores the legacy delete-and-forward behaviour
50
50
  // (diagnostic only: it forwards continue-deltas the API rejects).
51
51
  //
52
52
  // SECRETS
53
53
  // Arrive on stdin as three newline-terminated lines: Anthropic key, Mnemom key (may be
54
54
  // empty), OpenAI egress key (may be empty). Never argv, never env, never disk, never logged.
55
- // The log (MNEMOM_CODE_RC_PROXY_LOG, else stderr) carries method/path/status/latency and the
55
+ // The log (MNEMOM_AGENT_RC_PROXY_LOG, else stderr) carries method/path/status/latency and the
56
56
  // gateway's `x-mnemom-agent` / `x-mnemom-goal-verdict` response headers only. Gateway error
57
57
  // bodies (status ≥ 400) are logged truncated so a 4xx is diagnosable (compressed ones as
58
58
  // `body=<encoding> <N> bytes`). Locally answered thread 400s log the thread type only.
59
59
  //
60
60
  // CONFIG (env, none secret)
61
- // MNEMOM_CODE_RC_DOOR gateway door URL (required), e.g. https://gateway.mnemom.ai/anthropic
62
- // MNEMOM_CODE_RC_AGENT x-mnemom-agent value (required)
63
- // MNEMOM_CODE_RC_CONVERSATION_ID x-mnemom-conversation-id value (required)
64
- // MNEMOM_CODE_RC_CONTRACT_B64 base64 contract for x-mnemom-contract (optional)
65
- // MNEMOM_CODE_RC_PROVIDER x-mnemom-provider directive JSON (optional; router door)
66
- // MNEMOM_CODE_RC_THREAD_MODE `reject` (default: answer thread-bearing requests locally,
61
+ // MNEMOM_AGENT_RC_DOOR gateway door URL (required), e.g. https://gateway.mnemom.ai/anthropic
62
+ // MNEMOM_AGENT_RC_AGENT x-mnemom-agent value (required)
63
+ // MNEMOM_AGENT_RC_CONVERSATION_ID x-mnemom-conversation-id value (required)
64
+ // MNEMOM_AGENT_RC_CONTRACT_B64 base64 contract for x-mnemom-contract (optional)
65
+ // MNEMOM_AGENT_RC_PROVIDER x-mnemom-provider directive JSON (optional; router door)
66
+ // MNEMOM_AGENT_RC_THREAD_MODE `reject` (default: answer thread-bearing requests locally,
67
67
  // see above) | `strip` (legacy: delete `thread`, forward)
68
- // MNEMOM_CODE_RC_PROXY_LOG append log lines to this file instead of stderr (optional)
69
- // MNEMOM_CODE_RC_PARENT_PID exit when this pid is gone, so no orphan holds the keys
70
- // MNEMOM_CODE_RC_PORT listen port (default 0 = ephemeral)
68
+ // MNEMOM_AGENT_RC_PROXY_LOG append log lines to this file instead of stderr (optional)
69
+ // MNEMOM_AGENT_RC_PARENT_PID exit when this pid is gone, so no orphan holds the keys
70
+ // MNEMOM_AGENT_RC_PORT listen port (default 0 = ephemeral)
71
71
  //
72
72
  // stdout: exactly one line, `READY <port> <ca.pem path>`, once listening. Node ≥ 18, no deps.
73
- import http from 'node:http';
74
- import https from 'node:https';
75
- import net from 'node:net';
76
- import fs from 'node:fs';
77
- import os from 'node:os';
78
- import path from 'node:path';
79
- import { execFileSync } from 'node:child_process';
73
+ import http from "node:http";
74
+ import https from "node:https";
75
+ import net from "node:net";
76
+ import fs from "node:fs";
77
+ import os from "node:os";
78
+ import path from "node:path";
79
+ import { execFileSync } from "node:child_process";
80
80
 
81
- const MITM_HOST = 'api.anthropic.com';
81
+ const MITM_HOST = "api.anthropic.com";
82
82
  const env = process.env;
83
83
 
84
- function die(msg) { process.stderr.write(`mnemom-code-rc-proxy: ${msg}\n`); process.exit(2); }
84
+ function die(msg) {
85
+ process.stderr.write(`mnemom-code-rc-proxy: ${msg}\n`);
86
+ process.exit(2);
87
+ }
85
88
 
86
- if (!env.MNEMOM_CODE_RC_DOOR) die('MNEMOM_CODE_RC_DOOR is required');
89
+ if (!env.MNEMOM_AGENT_RC_DOOR) die("MNEMOM_AGENT_RC_DOOR is required");
87
90
  let GATEWAY;
88
- try { GATEWAY = new URL(env.MNEMOM_CODE_RC_DOOR); } catch { die(`MNEMOM_CODE_RC_DOOR is not a URL: ${env.MNEMOM_CODE_RC_DOOR}`); }
89
- if (GATEWAY.protocol !== 'https:') die('MNEMOM_CODE_RC_DOOR must be https');
90
- const AGENT = env.MNEMOM_CODE_RC_AGENT || die('MNEMOM_CODE_RC_AGENT is required');
91
- const CONV = env.MNEMOM_CODE_RC_CONVERSATION_ID || die('MNEMOM_CODE_RC_CONVERSATION_ID is required');
92
- const CONTRACT_B64 = env.MNEMOM_CODE_RC_CONTRACT_B64 || '';
93
- const PROVIDER = env.MNEMOM_CODE_RC_PROVIDER || '';
94
- const THREAD_MODE = env.MNEMOM_CODE_RC_THREAD_MODE || 'reject';
95
- if (THREAD_MODE !== 'reject' && THREAD_MODE !== 'strip') die(`MNEMOM_CODE_RC_THREAD_MODE must be 'reject' or 'strip', got '${THREAD_MODE}'`);
96
- const LOGFILE = env.MNEMOM_CODE_RC_PROXY_LOG || '';
97
- const PARENT = Number(env.MNEMOM_CODE_RC_PARENT_PID || 0);
98
- const PORT = Number(env.MNEMOM_CODE_RC_PORT || 0);
91
+ try {
92
+ GATEWAY = new URL(env.MNEMOM_AGENT_RC_DOOR);
93
+ } catch {
94
+ die(`MNEMOM_AGENT_RC_DOOR is not a URL: ${env.MNEMOM_AGENT_RC_DOOR}`);
95
+ }
96
+ if (GATEWAY.protocol !== "https:") die("MNEMOM_AGENT_RC_DOOR must be https");
97
+ const AGENT = env.MNEMOM_AGENT_RC_AGENT || die("MNEMOM_AGENT_RC_AGENT is required");
98
+ const CONV =
99
+ env.MNEMOM_AGENT_RC_CONVERSATION_ID || die("MNEMOM_AGENT_RC_CONVERSATION_ID is required");
100
+ const CONTRACT_B64 = env.MNEMOM_AGENT_RC_CONTRACT_B64 || "";
101
+ const PROVIDER = env.MNEMOM_AGENT_RC_PROVIDER || "";
102
+ const THREAD_MODE = env.MNEMOM_AGENT_RC_THREAD_MODE || "reject";
103
+ if (THREAD_MODE !== "reject" && THREAD_MODE !== "strip")
104
+ die(`MNEMOM_AGENT_RC_THREAD_MODE must be 'reject' or 'strip', got '${THREAD_MODE}'`);
105
+ const LOGFILE = env.MNEMOM_AGENT_RC_PROXY_LOG || "";
106
+ const PARENT = Number(env.MNEMOM_AGENT_RC_PARENT_PID || 0);
107
+ const PORT = Number(env.MNEMOM_AGENT_RC_PORT || 0);
99
108
 
100
109
  const log = (line) => {
101
110
  const s = `${new Date().toISOString()} ${line}\n`;
102
- if (LOGFILE) { try { fs.appendFileSync(LOGFILE, s); return; } catch { /* fall through */ } }
111
+ if (LOGFILE) {
112
+ try {
113
+ fs.appendFileSync(LOGFILE, s);
114
+ return;
115
+ } catch {
116
+ /* fall through */
117
+ }
118
+ }
103
119
  process.stderr.write(s);
104
120
  };
105
121
 
106
122
  // --- secrets: three lines on stdin ------------------------------------------------------
107
123
  let secrets;
108
124
  try {
109
- const lines = fs.readFileSync(0, 'utf8').split('\n');
110
- secrets = { anthropic: (lines[0] || '').trim(), mnemom: (lines[1] || '').trim(), openai: (lines[2] || '').trim() };
125
+ const lines = fs.readFileSync(0, "utf8").split("\n");
126
+ secrets = {
127
+ anthropic: (lines[0] || "").trim(),
128
+ mnemom: (lines[1] || "").trim(),
129
+ openai: (lines[2] || "").trim(),
130
+ };
111
131
  } catch {
112
- die('expected three lines of secrets on stdin (anthropic, mnemom, openai)');
132
+ die("expected three lines of secrets on stdin (anthropic, mnemom, openai)");
113
133
  }
114
- if (!secrets.anthropic) die('the Anthropic key (stdin line 1) is empty');
134
+ if (!secrets.anthropic) die("the Anthropic key (stdin line 1) is empty");
115
135
 
116
136
  // --- ephemeral CA + leaf for api.anthropic.com ------------------------------------------
117
137
  // Private 0700 dir under the OS tmpdir; the CA key is deleted as soon as the leaf is signed
118
138
  // (only the leaf key is needed to serve sessions) and the whole dir goes on exit. Works with
119
139
  // macOS's system LibreSSL (3.3+) and OpenSSL 1.1.1+ (`-addext`).
120
- const caDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mnemom-code-rc-ca-'));
140
+ const caDir = fs.mkdtempSync(path.join(os.tmpdir(), "mnemom-code-rc-ca-"));
121
141
  fs.chmodSync(caDir, 0o700);
122
142
  const p = (f) => path.join(caDir, f);
123
- const cleanup = () => { try { fs.rmSync(caDir, { recursive: true, force: true }); } catch { /* best effort */ } };
124
- process.on('exit', cleanup);
125
- for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(sig, () => process.exit(0));
143
+ const cleanup = () => {
144
+ try {
145
+ fs.rmSync(caDir, { recursive: true, force: true });
146
+ } catch {
147
+ /* best effort */
148
+ }
149
+ };
150
+ process.on("exit", cleanup);
151
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(sig, () => process.exit(0));
126
152
  try {
127
- const ossl = (...a) => execFileSync('openssl', a, { stdio: ['ignore', 'ignore', 'pipe'] });
128
- ossl('req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:prime256v1', '-nodes',
129
- '-keyout', p('ca.key'), '-out', p('ca.pem'), '-days', '2', '-subj', '/CN=mnemom code local CA (ephemeral)',
130
- '-addext', 'basicConstraints=critical,CA:TRUE', '-addext', 'keyUsage=critical,keyCertSign,cRLSign');
131
- ossl('req', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:prime256v1', '-nodes',
132
- '-keyout', p('leaf.key'), '-out', p('leaf.csr'), '-subj', `/CN=${MITM_HOST}`);
133
- fs.writeFileSync(p('leaf.ext'),
134
- `subjectAltName=DNS:${MITM_HOST}\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n`);
135
- ossl('x509', '-req', '-in', p('leaf.csr'), '-CA', p('ca.pem'), '-CAkey', p('ca.key'), '-CAcreateserial',
136
- '-out', p('leaf.pem'), '-days', '2', '-extfile', p('leaf.ext'));
137
- fs.rmSync(p('ca.key'));
153
+ const ossl = (...a) => execFileSync("openssl", a, { stdio: ["ignore", "ignore", "pipe"] });
154
+ ossl(
155
+ "req",
156
+ "-x509",
157
+ "-newkey",
158
+ "ec",
159
+ "-pkeyopt",
160
+ "ec_paramgen_curve:prime256v1",
161
+ "-nodes",
162
+ "-keyout",
163
+ p("ca.key"),
164
+ "-out",
165
+ p("ca.pem"),
166
+ "-days",
167
+ "2",
168
+ "-subj",
169
+ "/CN=mnemom agent local CA (ephemeral)",
170
+ "-addext",
171
+ "basicConstraints=critical,CA:TRUE",
172
+ "-addext",
173
+ "keyUsage=critical,keyCertSign,cRLSign",
174
+ );
175
+ ossl(
176
+ "req",
177
+ "-newkey",
178
+ "ec",
179
+ "-pkeyopt",
180
+ "ec_paramgen_curve:prime256v1",
181
+ "-nodes",
182
+ "-keyout",
183
+ p("leaf.key"),
184
+ "-out",
185
+ p("leaf.csr"),
186
+ "-subj",
187
+ `/CN=${MITM_HOST}`,
188
+ );
189
+ fs.writeFileSync(
190
+ p("leaf.ext"),
191
+ `subjectAltName=DNS:${MITM_HOST}\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth\n`,
192
+ );
193
+ ossl(
194
+ "x509",
195
+ "-req",
196
+ "-in",
197
+ p("leaf.csr"),
198
+ "-CA",
199
+ p("ca.pem"),
200
+ "-CAkey",
201
+ p("ca.key"),
202
+ "-CAcreateserial",
203
+ "-out",
204
+ p("leaf.pem"),
205
+ "-days",
206
+ "2",
207
+ "-extfile",
208
+ p("leaf.ext"),
209
+ );
210
+ fs.rmSync(p("ca.key"));
138
211
  } catch (e) {
139
- die(`could not mint the ephemeral CA with openssl: ${e.stderr ? e.stderr.toString() : e.message}`);
212
+ die(
213
+ `could not mint the ephemeral CA with openssl: ${e.stderr ? e.stderr.toString() : e.message}`,
214
+ );
140
215
  }
141
216
 
142
217
  // --- request handling ---------------------------------------------------------------------
143
218
  const gatewayAgent = new https.Agent({ keepAlive: false }); // a reused socket to the gateway gave ECONNRESET on retry
144
219
  const upstreamAgent = new https.Agent({ keepAlive: true, maxSockets: 32 });
145
- const HOP = new Set(['connection', 'proxy-connection', 'keep-alive', 'upgrade', 'host', 'te', 'trailer']);
220
+ const HOP = new Set([
221
+ "connection",
222
+ "proxy-connection",
223
+ "keep-alive",
224
+ "upgrade",
225
+ "host",
226
+ "te",
227
+ "trailer",
228
+ ]);
146
229
 
147
- const isMessagesPath = (u) => u === '/v1/messages' || u.startsWith('/v1/messages?');
230
+ const isMessagesPath = (u) => u === "/v1/messages" || u.startsWith("/v1/messages?");
148
231
 
149
232
  // Inspect + normalise a /v1/messages body for the gateway leg. Returns
150
233
  // { body: <possibly rewritten Buffer>, thread: null | 'create' | 'continue' | 'other' }
@@ -157,24 +240,34 @@ const isMessagesPath = (u) => u === '/v1/messages' || u.startsWith('/v1/messages
157
240
  // unchanged apart from `cache_control.ttl`, because the caller will not forward it.
158
241
  function normalizeBody(body) {
159
242
  try {
160
- const j = JSON.parse(body.toString('utf8'));
243
+ const j = JSON.parse(body.toString("utf8"));
161
244
  let n = 0;
162
245
  const walk = (o) => {
163
- if (Array.isArray(o)) { for (const x of o) walk(x); return; }
164
- if (o && typeof o === 'object') {
165
- if (o.cache_control && typeof o.cache_control === 'object' && 'ttl' in o.cache_control) { delete o.cache_control.ttl; n++; }
246
+ if (Array.isArray(o)) {
247
+ for (const x of o) walk(x);
248
+ return;
249
+ }
250
+ if (o && typeof o === "object") {
251
+ if (o.cache_control && typeof o.cache_control === "object" && "ttl" in o.cache_control) {
252
+ delete o.cache_control.ttl;
253
+ n++;
254
+ }
166
255
  for (const v of Object.values(o)) walk(v);
167
256
  }
168
257
  };
169
258
  walk(j);
170
259
  let thread = null;
171
- if (j && typeof j === 'object' && !Array.isArray(j) && 'thread' in j) {
260
+ if (j && typeof j === "object" && !Array.isArray(j) && "thread" in j) {
172
261
  if (j.thread === null || j.thread === undefined) {
173
- delete j.thread; n++; // not a thread: forward the rest
262
+ delete j.thread;
263
+ n++; // not a thread: forward the rest
174
264
  } else {
175
- const t = typeof j.thread === 'object' ? j.thread.type : undefined;
176
- thread = t === 'create' || t === 'continue' ? t : 'other';
177
- if (THREAD_MODE === 'strip') { delete j.thread; n++; }
265
+ const t = typeof j.thread === "object" ? j.thread.type : undefined;
266
+ thread = t === "create" || t === "continue" ? t : "other";
267
+ if (THREAD_MODE === "strip") {
268
+ delete j.thread;
269
+ n++;
270
+ }
178
271
  }
179
272
  }
180
273
  return { body: n ? Buffer.from(JSON.stringify(j)) : body, thread };
@@ -187,36 +280,50 @@ function normalizeBody(body) {
187
280
  // classifier must land on "unsupported_request": `details.error_code` is the primary
188
281
  // signal, and the message names the beta, the `thread` param and `previous_message_id`
189
282
  // so the fallback regexes agree even if the details path were ever ignored.
190
- const THREAD_400 = Buffer.from(JSON.stringify({
191
- type: 'error',
192
- error: {
193
- type: 'invalid_request_error',
194
- message: 'message threads (beta message-threads-2026-08-12: top-level thread param / previous_message_id) are not supported on the mnemom gateway leg; resend the full transcript without a thread',
195
- details: { error_code: 'thread_unsupported_request' },
196
- },
197
- }));
283
+ const THREAD_400 = Buffer.from(
284
+ JSON.stringify({
285
+ type: "error",
286
+ error: {
287
+ type: "invalid_request_error",
288
+ message:
289
+ "message threads (beta message-threads-2026-08-12: top-level thread param / previous_message_id) are not supported on the mnemom gateway leg; resend the full transcript without a thread",
290
+ details: { error_code: "thread_unsupported_request" },
291
+ },
292
+ }),
293
+ );
198
294
  let localSeq = 0;
199
295
  function rejectThread(res, tag, thread) {
200
296
  res.writeHead(400, {
201
- 'content-type': 'application/json',
202
- 'content-length': String(THREAD_400.length),
203
- 'request-id': `mnemom-code-rc-proxy-${++localSeq}`,
297
+ "content-type": "application/json",
298
+ "content-length": String(THREAD_400.length),
299
+ "request-id": `mnemom-code-rc-proxy-${++localSeq}`,
204
300
  });
205
301
  res.end(THREAD_400);
206
- log(`${tag} -> local 400 thread=${thread} (thread_unsupported_request; Claude Code resends this turn stateless and stops threading for this agent+model for the rest of the session — a subagent or model switch takes one more local 400)`);
302
+ log(
303
+ `${tag} -> local 400 thread=${thread} (thread_unsupported_request; Claude Code resends this turn stateless and stops threading for this agent+model for the rest of the session — a subagent or model switch takes one more local 400)`,
304
+ );
207
305
  }
208
306
 
209
307
  // One handler serves both the MITM'd TLS stream (origin-form URLs, host = api.anthropic.com)
210
308
  // and absolute-form plaintext requests sent straight to the proxy port.
211
309
  function handle(req, res, viaPlain) {
212
- let host = MITM_HOST, port = 443, urlPath = req.url, proto = https;
310
+ let host = MITM_HOST,
311
+ port = 443,
312
+ urlPath = req.url,
313
+ proto = https;
213
314
  if (viaPlain) {
214
315
  let u;
215
- try { u = new URL(req.url); } catch { res.writeHead(400); res.end('mnemom-code-rc-proxy: absolute-form URL required\n'); return; }
316
+ try {
317
+ u = new URL(req.url);
318
+ } catch {
319
+ res.writeHead(400);
320
+ res.end("mnemom-code-rc-proxy: absolute-form URL required\n");
321
+ return;
322
+ }
216
323
  host = u.hostname;
217
- port = Number(u.port || (u.protocol === 'http:' ? 80 : 443));
324
+ port = Number(u.port || (u.protocol === "http:" ? 80 : 443));
218
325
  urlPath = u.pathname + u.search;
219
- proto = u.protocol === 'http:' ? http : https;
326
+ proto = u.protocol === "http:" ? http : https;
220
327
  }
221
328
  const headers = {};
222
329
  for (const [k, v] of Object.entries(req.headers)) if (!HOP.has(k)) headers[k] = v;
@@ -224,27 +331,41 @@ function handle(req, res, viaPlain) {
224
331
  const toGateway = host === MITM_HOST && isMessagesPath(urlPath);
225
332
  let target;
226
333
  if (toGateway) {
227
- target = { hostname: GATEWAY.hostname, port: GATEWAY.port || 443,
228
- path: GATEWAY.pathname.replace(/\/$/, '') + urlPath, agent: gatewayAgent };
229
- delete headers.authorization; // the OAuth bearer stays local
230
- headers['x-api-key'] = secrets.anthropic;
231
- headers['x-mnemom-agent'] = AGENT;
232
- headers['x-mnemom-conversation-id'] = CONV;
233
- if (secrets.mnemom) headers['x-mnemom-api-key'] = secrets.mnemom;
234
- if (PROVIDER) headers['x-mnemom-provider'] = PROVIDER;
235
- if (secrets.openai) headers['x-mnemom-egress-key'] = secrets.openai;
236
- if (CONTRACT_B64) headers['x-mnemom-contract'] = CONTRACT_B64;
237
- if (headers['anthropic-beta']) { // drop the OAuth-only beta tokens
238
- const b = String(headers['anthropic-beta']).split(',').map((s) => s.trim()).filter((s) => s && !s.startsWith('oauth-'));
239
- if (b.length) headers['anthropic-beta'] = b.join(','); else delete headers['anthropic-beta'];
334
+ target = {
335
+ hostname: GATEWAY.hostname,
336
+ port: GATEWAY.port || 443,
337
+ path: GATEWAY.pathname.replace(/\/$/, "") + urlPath,
338
+ agent: gatewayAgent,
339
+ };
340
+ delete headers.authorization; // the OAuth bearer stays local
341
+ headers["x-api-key"] = secrets.anthropic;
342
+ headers["x-mnemom-agent"] = AGENT;
343
+ headers["x-mnemom-conversation-id"] = CONV;
344
+ if (secrets.mnemom) headers["x-mnemom-api-key"] = secrets.mnemom;
345
+ if (PROVIDER) headers["x-mnemom-provider"] = PROVIDER;
346
+ if (secrets.openai) headers["x-mnemom-egress-key"] = secrets.openai;
347
+ if (CONTRACT_B64) headers["x-mnemom-contract"] = CONTRACT_B64;
348
+ if (headers["anthropic-beta"]) {
349
+ // drop the OAuth-only beta tokens
350
+ const b = String(headers["anthropic-beta"])
351
+ .split(",")
352
+ .map((s) => s.trim())
353
+ .filter((s) => s && !s.startsWith("oauth-"));
354
+ if (b.length) headers["anthropic-beta"] = b.join(",");
355
+ else delete headers["anthropic-beta"];
240
356
  }
241
357
  } else {
242
- target = { hostname: host, port, path: urlPath, agent: proto === https ? upstreamAgent : undefined };
358
+ target = {
359
+ hostname: host,
360
+ port,
361
+ path: urlPath,
362
+ agent: proto === https ? upstreamAgent : undefined,
363
+ };
243
364
  }
244
365
 
245
366
  const started = Date.now();
246
- const tag = `${req.method} ${viaPlain ? host : ''}${urlPath}`;
247
- const leg = toGateway ? 'gateway' : 'upstream';
367
+ const tag = `${req.method} ${viaPlain ? host : ""}${urlPath}`;
368
+ const leg = toGateway ? "gateway" : "upstream";
248
369
 
249
370
  // Open the upstream request only once we know we are forwarding. `body` is the fully
250
371
  // buffered (normalised) gateway body; undefined means stream the client request through.
@@ -256,45 +377,68 @@ function handle(req, res, viaPlain) {
256
377
  // Gateway error bodies are logged (truncated) so a 4xx is diagnosable — unless the
257
378
  // body is compressed, in which case only its encoding + size is useful
258
379
  // (`identity` is not a compression).
259
- let errBody = '', errBytes = 0;
260
- const enc = ur.headers['content-encoding'];
261
- const errEnc = enc && enc !== 'identity' ? enc : '';
380
+ let errBody = "",
381
+ errBytes = 0;
382
+ const enc = ur.headers["content-encoding"];
383
+ const errEnc = enc && enc !== "identity" ? enc : "";
262
384
  if (toGateway && ur.statusCode >= 400) {
263
- ur.on('data', (c) => { errBytes += c.length; if (!errEnc && errBody.length < 1500) errBody += c.toString('utf8'); });
385
+ ur.on("data", (c) => {
386
+ errBytes += c.length;
387
+ if (!errEnc && errBody.length < 1500) errBody += c.toString("utf8");
388
+ });
264
389
  }
265
390
  ur.pipe(res);
266
- const ident = toGateway ? ` agent=${ur.headers['x-mnemom-agent'] || '-'} verdict=${ur.headers['x-mnemom-goal-verdict'] || '-'}` : '';
267
- ur.on('end', () => {
268
- let bodyNote = '';
391
+ const ident = toGateway
392
+ ? ` agent=${ur.headers["x-mnemom-agent"] || "-"} verdict=${ur.headers["x-mnemom-goal-verdict"] || "-"}`
393
+ : "";
394
+ ur.on("end", () => {
395
+ let bodyNote = "";
269
396
  if (toGateway && ur.statusCode >= 400) {
270
- bodyNote = errEnc ? ` body=${errEnc} ${errBytes} bytes` : (errBody ? ' body=' + errBody.slice(0, 1500).replace(/\s+/g, ' ') : '');
397
+ bodyNote = errEnc
398
+ ? ` body=${errEnc} ${errBytes} bytes`
399
+ : errBody
400
+ ? " body=" + errBody.slice(0, 1500).replace(/\s+/g, " ")
401
+ : "";
271
402
  }
272
403
  log(`${tag} -> ${leg} ${ur.statusCode} ${Date.now() - started}ms${ident}${bodyNote}`);
273
404
  });
274
405
  });
275
- up.on('error', (e) => {
406
+ up.on("error", (e) => {
276
407
  log(`${tag} -> ${leg} ERROR ${e.code || e.message}`);
277
- if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });
278
- res.end(JSON.stringify({ type: 'error', error: { type: 'proxy_error', message: `mnemom-code-rc-proxy upstream: ${e.code || e.message}` } }));
408
+ if (!res.headersSent) res.writeHead(502, { "content-type": "application/json" });
409
+ res.end(
410
+ JSON.stringify({
411
+ type: "error",
412
+ error: {
413
+ type: "proxy_error",
414
+ message: `mnemom-code-rc-proxy upstream: ${e.code || e.message}`,
415
+ },
416
+ }),
417
+ );
418
+ });
419
+ res.on("close", () => {
420
+ if (!up.destroyed) up.destroy();
279
421
  });
280
- res.on('close', () => { if (!up.destroyed) up.destroy(); });
281
422
  if (body !== undefined) {
282
- up.removeHeader('transfer-encoding');
283
- up.setHeader('content-length', String(body.length));
423
+ up.removeHeader("transfer-encoding");
424
+ up.setHeader("content-length", String(body.length));
284
425
  up.end(body);
285
426
  } else {
286
427
  req.pipe(up);
287
428
  }
288
429
  };
289
430
 
290
- if (toGateway && req.method === 'POST') {
431
+ if (toGateway && req.method === "POST") {
291
432
  // Buffer the body first: a thread-bearing request is answered here and nothing may be
292
433
  // sent to (or left half-open toward) the gateway for it.
293
434
  const chunks = [];
294
- req.on('data', (c) => chunks.push(c));
295
- req.on('end', () => {
435
+ req.on("data", (c) => chunks.push(c));
436
+ req.on("end", () => {
296
437
  const { body, thread } = normalizeBody(Buffer.concat(chunks));
297
- if (thread !== null && THREAD_MODE === 'reject') { rejectThread(res, tag, thread); return; }
438
+ if (thread !== null && THREAD_MODE === "reject") {
439
+ rejectThread(res, tag, thread);
440
+ return;
441
+ }
298
442
  forward(body);
299
443
  });
300
444
  } else {
@@ -303,39 +447,58 @@ function handle(req, res, viaPlain) {
303
447
  }
304
448
 
305
449
  const tlsServer = https.createServer(
306
- { key: fs.readFileSync(p('leaf.key')), cert: fs.readFileSync(p('leaf.pem')) },
450
+ { key: fs.readFileSync(p("leaf.key")), cert: fs.readFileSync(p("leaf.pem")) },
307
451
  (req, res) => handle(req, res, false),
308
452
  );
309
- tlsServer.on('tlsClientError', (e) => log(`tls client error: ${e.code || e.message}`));
453
+ tlsServer.on("tlsClientError", (e) => log(`tls client error: ${e.code || e.message}`));
310
454
 
311
455
  const proxy = http.createServer((req, res) => handle(req, res, true));
312
- proxy.on('connect', (req, sock, head) => {
313
- const [host, portStr] = req.url.split(':');
456
+ proxy.on("connect", (req, sock, head) => {
457
+ const [host, portStr] = req.url.split(":");
314
458
  const port = Number(portStr || 443);
315
- sock.on('error', () => {});
459
+ sock.on("error", () => {});
316
460
  if (host === MITM_HOST && port === 443) {
317
- sock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
461
+ sock.write("HTTP/1.1 200 Connection Established\r\n\r\n");
318
462
  if (head && head.length) sock.unshift(head);
319
- tlsServer.emit('connection', sock);
463
+ tlsServer.emit("connection", sock);
320
464
  return;
321
465
  }
322
466
  const up = net.connect(port, host, () => {
323
- sock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
467
+ sock.write("HTTP/1.1 200 Connection Established\r\n\r\n");
324
468
  if (head && head.length) up.write(head);
325
- up.pipe(sock); sock.pipe(up);
469
+ up.pipe(sock);
470
+ sock.pipe(up);
326
471
  });
327
- up.on('error', (e) => { log(`CONNECT ${req.url} tunnel ERROR ${e.code || e.message}`); sock.destroy(); });
328
- sock.on('close', () => up.destroy());
329
- up.on('close', () => sock.destroy());
472
+ up.on("error", (e) => {
473
+ log(`CONNECT ${req.url} tunnel ERROR ${e.code || e.message}`);
474
+ sock.destroy();
475
+ });
476
+ sock.on("close", () => up.destroy());
477
+ up.on("close", () => sock.destroy());
330
478
  log(`CONNECT ${req.url} tunnel`);
331
479
  });
332
- proxy.on('clientError', (e, sock) => { try { sock.destroy(); } catch { /* already gone */ } });
480
+ proxy.on("clientError", (e, sock) => {
481
+ try {
482
+ sock.destroy();
483
+ } catch {
484
+ /* already gone */
485
+ }
486
+ });
333
487
 
334
- proxy.listen(PORT, '127.0.0.1', () => {
488
+ proxy.listen(PORT, "127.0.0.1", () => {
335
489
  const { port } = proxy.address();
336
- log(`listening 127.0.0.1:${port} agent=${AGENT} conv=${CONV} door=${GATEWAY.href} contract=${CONTRACT_B64 ? 'yes' : 'no'} provider=${PROVIDER || '-'} thread=${THREAD_MODE}`);
337
- process.stdout.write(`READY ${port} ${p('ca.pem')}\n`);
490
+ log(
491
+ `listening 127.0.0.1:${port} agent=${AGENT} conv=${CONV} door=${GATEWAY.href} contract=${CONTRACT_B64 ? "yes" : "no"} provider=${PROVIDER || "-"} thread=${THREAD_MODE}`,
492
+ );
493
+ process.stdout.write(`READY ${port} ${p("ca.pem")}\n`);
338
494
  });
339
495
 
340
496
  // Die with the launcher so no orphan keeps the secrets in memory.
341
- if (PARENT) setInterval(() => { try { process.kill(PARENT, 0); } catch { process.exit(0); } }, 2000).unref();
497
+ if (PARENT)
498
+ setInterval(() => {
499
+ try {
500
+ process.kill(PARENT, 0);
501
+ } catch {
502
+ process.exit(0);
503
+ }
504
+ }, 2000).unref();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.0-next.5",
3
+ "version": "0.17.0-next.7",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,17 +0,0 @@
1
- /**
2
- * `mnemom code config …` — read and write the persistent per-customer settings
3
- * for `mnemom code` (see lib/code-config.ts). A sub-verb of `mnemom code`, so a
4
- * customer configures the launcher once and never retypes their flags:
5
- *
6
- * mnemom code config list # show the current settings
7
- * mnemom code config get <key> # print one value
8
- * mnemom code config set <key> <value> # validate + persist one value
9
- * mnemom code config unset <key> # remove one value
10
- * mnemom code config edit # open ~/.mnemom/code.toml in $EDITOR
11
- * mnemom code config path # print the settings file path
12
- *
13
- * Settable keys are the single source of truth in lib/code-config.ts CONFIG_KEYS.
14
- * The secret Anthropic key is NOT a config key — it lives in ~/.mnemom/code.json.
15
- */
16
- /** Entry point for `mnemom code config <verb> [args...]`. Throws on misuse. */
17
- export declare function codeConfigCommand(argv: string[]): Promise<void>;