@mnemom/mnemom 0.17.0-next.6 → 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/commands/agent-config.d.ts +17 -0
- package/dist/commands/agent-config.js +173 -0
- package/dist/commands/{code-doctor.d.ts → agent-doctor.d.ts} +2 -2
- package/dist/commands/{code-doctor.js → agent-doctor.js} +27 -22
- package/dist/commands/{code-invite.d.ts → agent-invite.d.ts} +18 -18
- package/dist/commands/{code-invite.js → agent-invite.js} +30 -30
- package/dist/commands/{code-session.d.ts → agent-session.d.ts} +2 -2
- package/dist/commands/{code-session.js → agent-session.js} +8 -8
- package/dist/commands/{code-setup.d.ts → agent-setup.d.ts} +13 -13
- package/dist/commands/{code-setup.js → agent-setup.js} +46 -42
- package/dist/commands/{code.d.ts → agent.d.ts} +14 -14
- package/dist/commands/{code.js → agent.js} +76 -76
- package/dist/commands/agents.d.ts +2 -2
- package/dist/commands/agents.js +5 -5
- package/dist/commands/onboard.js +1 -1
- package/dist/commands/wrap.js +1 -1
- package/dist/index.js +42 -47
- package/dist/lib/{code.d.ts → agent.d.ts} +9 -9
- package/dist/lib/{code.js → agent.js} +14 -14
- package/dist/lib/api.d.ts +4 -4
- package/dist/lib/api.js +9 -9
- package/dist/lib/{code-config.d.ts → launcher-config.d.ts} +23 -23
- package/dist/lib/{code-config.js → launcher-config.js} +31 -31
- package/dist/lib/version-gate.js +1 -1
- package/dist/rc-proxy.mjs +297 -134
- package/package.json +1 -1
- package/dist/commands/code-config.d.ts +0 -17
- package/dist/commands/code-config.js +0 -147
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
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
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
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
|
|
74
|
-
import https from
|
|
75
|
-
import net from
|
|
76
|
-
import fs from
|
|
77
|
-
import os from
|
|
78
|
-
import path from
|
|
79
|
-
import { execFileSync } from
|
|
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 =
|
|
81
|
+
const MITM_HOST = "api.anthropic.com";
|
|
82
82
|
const env = process.env;
|
|
83
83
|
|
|
84
|
-
function die(msg) {
|
|
84
|
+
function die(msg) {
|
|
85
|
+
process.stderr.write(`mnemom-code-rc-proxy: ${msg}\n`);
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
85
88
|
|
|
86
|
-
if (!env.
|
|
89
|
+
if (!env.MNEMOM_AGENT_RC_DOOR) die("MNEMOM_AGENT_RC_DOOR is required");
|
|
87
90
|
let GATEWAY;
|
|
88
|
-
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
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) {
|
|
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,
|
|
110
|
-
secrets = {
|
|
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(
|
|
132
|
+
die("expected three lines of secrets on stdin (anthropic, mnemom, openai)");
|
|
113
133
|
}
|
|
114
|
-
if (!secrets.anthropic) die(
|
|
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(),
|
|
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 = () => {
|
|
124
|
-
|
|
125
|
-
|
|
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(
|
|
128
|
-
ossl(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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(
|
|
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([
|
|
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 ===
|
|
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(
|
|
243
|
+
const j = JSON.parse(body.toString("utf8"));
|
|
161
244
|
let n = 0;
|
|
162
245
|
const walk = (o) => {
|
|
163
|
-
if (Array.isArray(o)) {
|
|
164
|
-
|
|
165
|
-
|
|
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 ===
|
|
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;
|
|
262
|
+
delete j.thread;
|
|
263
|
+
n++; // not a thread: forward the rest
|
|
174
264
|
} else {
|
|
175
|
-
const t = typeof j.thread ===
|
|
176
|
-
thread = t ===
|
|
177
|
-
if (THREAD_MODE ===
|
|
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(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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(
|
|
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,
|
|
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 {
|
|
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 ===
|
|
324
|
+
port = Number(u.port || (u.protocol === "http:" ? 80 : 443));
|
|
218
325
|
urlPath = u.pathname + u.search;
|
|
219
|
-
proto = u.protocol ===
|
|
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 = {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (headers[
|
|
238
|
-
|
|
239
|
-
|
|
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 = {
|
|
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 :
|
|
247
|
-
const leg = toGateway ?
|
|
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 =
|
|
260
|
-
|
|
261
|
-
const
|
|
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(
|
|
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
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
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(
|
|
406
|
+
up.on("error", (e) => {
|
|
276
407
|
log(`${tag} -> ${leg} ERROR ${e.code || e.message}`);
|
|
277
|
-
if (!res.headersSent) res.writeHead(502, {
|
|
278
|
-
res.end(
|
|
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(
|
|
283
|
-
up.setHeader(
|
|
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 ===
|
|
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(
|
|
295
|
-
req.on(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
459
|
+
sock.on("error", () => {});
|
|
316
460
|
if (host === MITM_HOST && port === 443) {
|
|
317
|
-
sock.write(
|
|
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(
|
|
463
|
+
tlsServer.emit("connection", sock);
|
|
320
464
|
return;
|
|
321
465
|
}
|
|
322
466
|
const up = net.connect(port, host, () => {
|
|
323
|
-
sock.write(
|
|
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);
|
|
469
|
+
up.pipe(sock);
|
|
470
|
+
sock.pipe(up);
|
|
326
471
|
});
|
|
327
|
-
up.on(
|
|
328
|
-
|
|
329
|
-
|
|
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(
|
|
480
|
+
proxy.on("clientError", (e, sock) => {
|
|
481
|
+
try {
|
|
482
|
+
sock.destroy();
|
|
483
|
+
} catch {
|
|
484
|
+
/* already gone */
|
|
485
|
+
}
|
|
486
|
+
});
|
|
333
487
|
|
|
334
|
-
proxy.listen(PORT,
|
|
488
|
+
proxy.listen(PORT, "127.0.0.1", () => {
|
|
335
489
|
const { port } = proxy.address();
|
|
336
|
-
log(
|
|
337
|
-
|
|
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)
|
|
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,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>;
|