@mnemom/mnemom 0.17.33 → 0.17.34-next.2

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.
Files changed (236) hide show
  1. package/dist/base-url-proxy.mjs +18 -4
  2. package/dist/commands/agent-config.js +5 -1
  3. package/dist/commands/agent-doctor.d.ts +9 -0
  4. package/dist/commands/agent-doctor.js +4 -0
  5. package/dist/commands/agent-session.d.ts +1 -2
  6. package/dist/commands/agent-session.js +1 -2
  7. package/dist/commands/agent-setup.d.ts +6 -0
  8. package/dist/commands/agent-setup.js +12 -1
  9. package/dist/commands/agent-share-render.d.ts +1 -1
  10. package/dist/commands/agent-share-render.js +1 -1
  11. package/dist/commands/agent.d.ts +26 -0
  12. package/dist/commands/agent.js +228 -53
  13. package/dist/commands/durable-doctor.d.ts +50 -0
  14. package/dist/commands/durable-doctor.js +186 -0
  15. package/dist/commands/durable-lock.d.ts +254 -0
  16. package/dist/commands/durable-lock.js +595 -0
  17. package/dist/commands/durable-sessions.d.ts +87 -0
  18. package/dist/commands/durable-sessions.js +303 -0
  19. package/dist/commands/durable-verbs.d.ts +116 -0
  20. package/dist/commands/durable-verbs.js +393 -0
  21. package/dist/commands/protection.js +1 -1
  22. package/dist/commands/scan.d.ts +97 -0
  23. package/dist/commands/scan.js +238 -0
  24. package/dist/durable/flock.d.ts +26 -0
  25. package/dist/durable/flock.js +80 -0
  26. package/dist/durable/launch-spec.d.ts +7 -1
  27. package/dist/durable/launch-spec.js +10 -3
  28. package/dist/durable/seat-proxy-entry.d.ts +11 -0
  29. package/dist/durable/seat-proxy-entry.js +25 -0
  30. package/dist/durable/seat-proxy.d.ts +107 -0
  31. package/dist/durable/seat-proxy.js +599 -0
  32. package/dist/durable/seat-socket.d.ts +46 -0
  33. package/dist/durable/seat-socket.js +179 -0
  34. package/dist/durable/seat-spec-validator.d.ts +28 -0
  35. package/dist/durable/seat-spec-validator.js +59 -0
  36. package/dist/durable/test-mode.mjs +81 -0
  37. package/dist/durable/turn-chain.d.ts +55 -0
  38. package/dist/durable/turn-chain.js +178 -0
  39. package/dist/host/auto-continue.d.ts +67 -0
  40. package/dist/host/auto-continue.js +118 -0
  41. package/dist/host/bootid.d.ts +13 -0
  42. package/dist/host/bootid.js +41 -0
  43. package/dist/host/client.d.ts +123 -0
  44. package/dist/host/client.js +171 -0
  45. package/dist/host/commands.d.ts +2 -0
  46. package/dist/host/commands.js +122 -0
  47. package/dist/host/control.d.ts +33 -0
  48. package/dist/host/control.js +144 -0
  49. package/dist/host/daemon.d.ts +217 -0
  50. package/dist/host/daemon.js +606 -0
  51. package/dist/host/gate.d.ts +46 -0
  52. package/dist/host/gate.js +82 -0
  53. package/dist/host/grant.d.ts +24 -0
  54. package/dist/host/grant.js +24 -0
  55. package/dist/host/instance.d.ts +35 -0
  56. package/dist/host/instance.js +62 -0
  57. package/dist/host/integrity.d.ts +35 -0
  58. package/dist/host/integrity.js +115 -0
  59. package/dist/host/launchd.d.ts +16 -0
  60. package/dist/host/launchd.js +45 -0
  61. package/dist/host/lock.d.ts +20 -0
  62. package/dist/host/lock.js +63 -0
  63. package/dist/host/owner.d.ts +55 -0
  64. package/dist/host/owner.js +121 -0
  65. package/dist/host/plist.d.ts +16 -0
  66. package/dist/host/plist.js +85 -0
  67. package/dist/host/procs.d.ts +27 -0
  68. package/dist/host/procs.js +105 -0
  69. package/dist/host/run.d.ts +66 -0
  70. package/dist/host/run.js +314 -0
  71. package/dist/host/runtime.d.ts +8 -0
  72. package/dist/host/runtime.js +22 -0
  73. package/dist/host/s1-names.d.ts +22 -0
  74. package/dist/host/s1-names.js +21 -0
  75. package/dist/host/seat-credential.d.ts +27 -0
  76. package/dist/host/seat-credential.js +41 -0
  77. package/dist/host/seat-launch-main.d.ts +1 -0
  78. package/dist/host/seat-launch-main.js +59 -0
  79. package/dist/host/seat-launch.d.ts +116 -0
  80. package/dist/host/seat-launch.js +302 -0
  81. package/dist/host/seat.d.ts +48 -0
  82. package/dist/host/seat.js +149 -0
  83. package/dist/host/state.d.ts +33 -0
  84. package/dist/host/state.js +117 -0
  85. package/dist/host/status.d.ts +101 -0
  86. package/dist/host/status.js +307 -0
  87. package/dist/host/tmux.d.ts +56 -0
  88. package/dist/host/tmux.js +182 -0
  89. package/dist/host/transcript.d.ts +30 -0
  90. package/dist/host/transcript.js +130 -0
  91. package/dist/host/verbs.d.ts +50 -0
  92. package/dist/host/verbs.js +271 -0
  93. package/dist/index.d.ts +8 -6
  94. package/dist/index.js +232 -35
  95. package/dist/lib/agent.d.ts +8 -26
  96. package/dist/lib/agent.js +11 -26
  97. package/dist/lib/api.d.ts +13 -0
  98. package/dist/lib/api.js +8 -0
  99. package/dist/lib/claude-plugin.d.ts +11 -2
  100. package/dist/lib/claude-plugin.js +10 -1
  101. package/dist/lib/durable/api.d.ts +136 -0
  102. package/dist/lib/durable/api.js +220 -0
  103. package/dist/lib/durable/codes.d.ts +8 -0
  104. package/dist/lib/durable/codes.js +2 -0
  105. package/dist/lib/durable/first-turn.d.ts +50 -0
  106. package/dist/lib/durable/first-turn.js +102 -0
  107. package/dist/lib/durable/gate.d.ts +48 -0
  108. package/dist/lib/durable/gate.js +122 -0
  109. package/dist/lib/durable/handover.d.ts +101 -0
  110. package/dist/lib/durable/handover.js +175 -0
  111. package/dist/lib/durable/launch.d.ts +182 -0
  112. package/dist/lib/durable/launch.js +443 -0
  113. package/dist/lib/durable/launching.d.ts +15 -0
  114. package/dist/lib/durable/launching.js +70 -0
  115. package/dist/lib/durable/live.d.ts +194 -0
  116. package/dist/lib/durable/live.js +635 -0
  117. package/dist/lib/durable/refusal.d.ts +57 -0
  118. package/dist/lib/durable/refusal.js +78 -0
  119. package/dist/lib/durable/view.d.ts +83 -0
  120. package/dist/lib/durable/view.js +244 -0
  121. package/dist/lib/experimental.d.ts +0 -9
  122. package/dist/lib/experimental.js +18 -17
  123. package/dist/lib/launcher-config.d.ts +7 -0
  124. package/dist/lib/launcher-config.js +21 -0
  125. package/dist/lib/scan/caps.d.ts +35 -0
  126. package/dist/lib/scan/caps.js +67 -0
  127. package/dist/lib/scan/credential.d.ts +37 -0
  128. package/dist/lib/scan/credential.js +104 -0
  129. package/dist/lib/scan/failure.d.ts +10 -0
  130. package/dist/lib/scan/failure.js +43 -0
  131. package/dist/lib/scan/pr-source.d.ts +57 -0
  132. package/dist/lib/scan/pr-source.js +130 -0
  133. package/dist/lib/scan/record.d.ts +49 -0
  134. package/dist/lib/scan/record.js +85 -0
  135. package/dist/lib/scan/report-email.d.ts +92 -0
  136. package/dist/lib/scan/report-email.js +144 -0
  137. package/dist/lib/scan/report.d.ts +13 -0
  138. package/dist/lib/scan/report.js +30 -0
  139. package/dist/lib/scan/scan.d.ts +64 -0
  140. package/dist/lib/scan/scan.js +182 -0
  141. package/dist/lib/scan/share.d.ts +38 -0
  142. package/dist/lib/scan/share.js +47 -0
  143. package/dist/lib/scan/spend/ai-tool-detect.d.ts +6 -0
  144. package/dist/lib/scan/spend/ai-tool-detect.js +18 -0
  145. package/dist/lib/scan/spend/anthropic-usage.d.ts +57 -0
  146. package/dist/lib/scan/spend/anthropic-usage.js +244 -0
  147. package/dist/lib/scan/spend/cost-corpus.v1.d.ts +56 -0
  148. package/dist/lib/scan/spend/cost-corpus.v1.js +64 -0
  149. package/dist/lib/scan/spend/prices.d.ts +33 -0
  150. package/dist/lib/scan/spend/prices.js +57 -0
  151. package/dist/lib/scan/spend/spend-estimate.d.ts +30 -0
  152. package/dist/lib/scan/spend/spend-estimate.js +70 -0
  153. package/dist/lib/scan/spend/spend-tiers.d.ts +25 -0
  154. package/dist/lib/scan/spend/spend-tiers.js +42 -0
  155. package/dist/lib/scan/spend/token-rate.d.ts +30 -0
  156. package/dist/lib/scan/spend/token-rate.js +54 -0
  157. package/dist/lib/session-key.d.ts +41 -0
  158. package/dist/lib/session-key.js +124 -0
  159. package/dist/lib/subscription-login.d.ts +15 -0
  160. package/dist/lib/subscription-login.js +28 -1
  161. package/dist/rc-key-401.mjs +75 -0
  162. package/dist/rc-proxy.mjs +221 -25
  163. package/dist/vendor/durable-contract/index.js +240 -18
  164. package/dist/vendor/shared/durable-contract/admission.d.ts +240 -0
  165. package/dist/vendor/shared/durable-contract/admission.js +533 -0
  166. package/dist/vendor/shared/durable-contract/arm.d.ts +51 -0
  167. package/dist/vendor/shared/durable-contract/arm.js +133 -0
  168. package/dist/vendor/shared/durable-contract/canonical.d.ts +29 -0
  169. package/dist/vendor/shared/durable-contract/canonical.js +98 -0
  170. package/dist/vendor/shared/durable-contract/cell-id.d.ts +22 -0
  171. package/dist/vendor/shared/durable-contract/cell-id.js +38 -0
  172. package/dist/vendor/shared/durable-contract/cli-output.d.ts +342 -0
  173. package/dist/vendor/shared/durable-contract/cli-output.js +201 -0
  174. package/dist/vendor/shared/durable-contract/cli-surface.d.ts +119 -0
  175. package/dist/vendor/shared/durable-contract/cli-surface.js +270 -0
  176. package/dist/vendor/shared/durable-contract/codes.d.ts +537 -0
  177. package/dist/vendor/shared/durable-contract/codes.js +475 -0
  178. package/dist/vendor/shared/durable-contract/constants.d.ts +153 -0
  179. package/dist/vendor/shared/durable-contract/constants.js +179 -0
  180. package/dist/vendor/shared/durable-contract/doors.d.ts +119 -0
  181. package/dist/vendor/shared/durable-contract/doors.js +249 -0
  182. package/dist/vendor/shared/durable-contract/envelope.d.ts +103 -0
  183. package/dist/vendor/shared/durable-contract/envelope.js +317 -0
  184. package/dist/vendor/shared/durable-contract/events.d.ts +147 -0
  185. package/dist/vendor/shared/durable-contract/events.js +630 -0
  186. package/dist/vendor/shared/durable-contract/executor-state.d.ts +41 -0
  187. package/dist/vendor/shared/durable-contract/executor-state.js +32 -0
  188. package/dist/vendor/shared/durable-contract/fixlines.d.ts +318 -0
  189. package/dist/vendor/shared/durable-contract/fixlines.js +483 -0
  190. package/dist/vendor/shared/durable-contract/freeze-record.d.ts +787 -0
  191. package/dist/vendor/shared/durable-contract/freeze-record.js +361 -0
  192. package/dist/vendor/shared/durable-contract/index.d.ts +36 -0
  193. package/dist/vendor/shared/durable-contract/index.js +37 -0
  194. package/dist/vendor/shared/durable-contract/journal-approvals.d.ts +29 -0
  195. package/dist/vendor/shared/durable-contract/journal-approvals.js +60 -0
  196. package/dist/vendor/shared/durable-contract/keys.d.ts +24 -0
  197. package/dist/vendor/shared/durable-contract/keys.js +66 -0
  198. package/dist/vendor/shared/durable-contract/lock.d.ts +128 -0
  199. package/dist/vendor/shared/durable-contract/lock.js +151 -0
  200. package/dist/vendor/shared/durable-contract/names.d.ts +25 -0
  201. package/dist/vendor/shared/durable-contract/names.js +78 -0
  202. package/dist/vendor/shared/durable-contract/owner-verbs.d.ts +175 -0
  203. package/dist/vendor/shared/durable-contract/owner-verbs.js +403 -0
  204. package/dist/vendor/shared/durable-contract/paths.d.ts +23 -0
  205. package/dist/vendor/shared/durable-contract/paths.js +35 -0
  206. package/dist/vendor/shared/durable-contract/pure-fn.d.ts +48 -0
  207. package/dist/vendor/shared/durable-contract/pure-fn.js +89 -0
  208. package/dist/vendor/shared/durable-contract/routes.d.ts +205 -0
  209. package/dist/vendor/shared/durable-contract/routes.js +315 -0
  210. package/dist/vendor/shared/durable-contract/schema.d.ts +126 -0
  211. package/dist/vendor/shared/durable-contract/schema.js +441 -0
  212. package/dist/vendor/shared/durable-contract/seal.d.ts +54 -0
  213. package/dist/vendor/shared/durable-contract/seal.js +99 -0
  214. package/dist/vendor/shared/durable-contract/secret-scope.d.ts +28 -0
  215. package/dist/vendor/shared/durable-contract/secret-scope.js +76 -0
  216. package/dist/vendor/shared/durable-contract/session-id.d.ts +21 -0
  217. package/dist/vendor/shared/durable-contract/session-id.js +58 -0
  218. package/dist/vendor/shared/durable-contract/spec.d.ts +162 -0
  219. package/dist/vendor/shared/durable-contract/spec.js +286 -0
  220. package/dist/vendor/shared/durable-contract/states.d.ts +96 -0
  221. package/dist/vendor/shared/durable-contract/states.js +218 -0
  222. package/dist/vendor/shared/durable-contract/telemetry.d.ts +94 -0
  223. package/dist/vendor/shared/durable-contract/telemetry.js +180 -0
  224. package/dist/vendor/shared/durable-contract/test-mode.d.ts +62 -0
  225. package/dist/vendor/shared/durable-contract/test-mode.js +140 -0
  226. package/dist/vendor/shared/durable-contract/turn.d.ts +57 -0
  227. package/dist/vendor/shared/durable-contract/turn.js +81 -0
  228. package/dist/vendor/shared/durable-contract/vectors.d.ts +191 -0
  229. package/dist/vendor/shared/durable-contract/vectors.js +208 -0
  230. package/package.json +4 -3
  231. package/plugins/integrity-manifest.json +35 -0
  232. package/plugins/mnemom/hooks/hooks.json +15 -1
  233. package/plugins/mnemom/hooks/register.tsx +60 -2
  234. package/plugins/mnemom/hooks/seat-hello.mjs +116 -0
  235. package/dist/commands/agent-implicit-notice.d.ts +0 -31
  236. package/dist/commands/agent-implicit-notice.js +0 -73
package/dist/rc-proxy.mjs CHANGED
@@ -64,6 +64,20 @@
64
64
  // Arrive on stdin as four newline-terminated lines: Anthropic key, Mnemom key (may be
65
65
  // empty), OpenAI egress key (may be empty), the per-session proxy credential (required).
66
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.
67
81
  // The log (MNEMOM_AGENT_RC_PROXY_LOG, else stderr) carries method/path/status/latency and the
68
82
  // gateway's `x-mnemom-agent` / `x-mnemom-goal-verdict` response headers only. Gateway error
69
83
  // bodies (status ≥ 400) are logged truncated so a 4xx is diagnosable (compressed ones as
@@ -90,7 +104,13 @@
90
104
  // is required: it anchors the governed identity.
91
105
  // MNEMOM_AGENT_RC_PROXY_LOG append log lines to this file instead of stderr (optional)
92
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)
93
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
94
114
  // MNEMOM_AGENT_TURN_HEADER 0/off: no x-mnemom-turn header (default on in implicit mode;
95
115
  // see turn-origin.mjs)
96
116
  //
@@ -107,6 +127,12 @@ import { execFileSync } from "node:child_process";
107
127
  import { TURN_HEADER, TurnOrigins, turnHeaderEnabled, turnHeaderFor } from "./turn-origin.mjs";
108
128
  import { OWNER_FILE, ensureRcStateRoot, keepPrivateDir, sweepRcDirs } from "./rc-state.mjs";
109
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";
110
136
 
111
137
  const MITM_HOST = "api.anthropic.com";
112
138
  const env = process.env;
@@ -197,6 +223,113 @@ if (SUBSCRIPTION) {
197
223
  // Fail closed: with no credential there is nothing to check a caller against.
198
224
  if (!secrets.proxy) die("the proxy credential (stdin line 4) is empty");
199
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
+
200
333
  // --- caller authentication (MNE-9233; see WHAT IT DOES) -------------------------------------
201
334
  // `Proxy-Authorization: Basic base64("mnemom:<credential>")`, compared in constant time. The
202
335
  // header value is never logged.
@@ -590,7 +723,7 @@ function handle(req, res, viaPlain) {
590
723
  delete headers["x-mnemom-agent-id"];
591
724
  if (AGENT_ID) headers["x-mnemom-agent-id"] = AGENT_ID;
592
725
  headers["x-mnemom-conversation-id"] = CONV;
593
- if (secrets.mnemom) headers["x-mnemom-api-key"] = secrets.mnemom;
726
+ // x-mnemom-api-key is set per attempt in forward(): the key can be swapped mid-session.
594
727
  if (PROVIDER) headers["x-mnemom-provider"] = PROVIDER;
595
728
  if (secrets.openai) headers["x-mnemom-egress-key"] = secrets.openai;
596
729
  if (CONTRACT_B64) headers["x-mnemom-contract"] = CONTRACT_B64;
@@ -618,20 +751,96 @@ function handle(req, res, viaPlain) {
618
751
  const tag = `${req.method} ${viaPlain ? host : ""}${urlPath}`;
619
752
  const leg = toGateway ? "gateway" : "upstream";
620
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
+
621
780
  // Open the upstream request only once we know we are forwarding. `body` is the fully
622
781
  // buffered (normalised) gateway body; undefined means stream the client request through.
623
- const forward = (body) => {
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;
624
790
  const opts = { ...target, method: req.method, headers };
625
791
  if (!net.isIP(target.hostname)) opts.servername = target.hostname; // SNI is for names, not IPs
626
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
+ }
627
840
  res.writeHead(ur.statusCode, ur.headers);
628
- // Gateway error bodies are logged (truncated) so a 4xx is diagnosable — unless the
629
- // body is compressed, in which case only its encoding + size is useful
630
- // (`identity` is not a compression).
631
841
  let errBody = "",
632
842
  errBytes = 0;
633
- const enc = ur.headers["content-encoding"];
634
- const errEnc = enc && enc !== "identity" ? enc : "";
843
+ const errEnc = errEncOf(ur);
635
844
  if (toGateway && ur.statusCode >= 400) {
636
845
  ur.on("data", (c) => {
637
846
  errBytes += c.length;
@@ -639,25 +848,12 @@ function handle(req, res, viaPlain) {
639
848
  });
640
849
  }
641
850
  ur.pipe(res);
642
- const goalRev = ur.headers["x-mnemom-goal-revision"];
643
- const goalThread = ur.headers["x-mnemom-goal-thread"];
644
- const steer = ur.headers["x-mnemom-goal-steer"];
645
- const ident = toGateway
646
- ? ` agent=${ur.headers["x-mnemom-agent"] || "-"} verdict=${ur.headers["x-mnemom-goal-verdict"] || "-"}` +
647
- (goalRev ? ` goal-rev=${goalRev}` : "") +
648
- (goalThread ? ` goal-thread=${goalThread}` : "") +
649
- (steer ? ` steer=${steer}` : "")
650
- : "";
651
851
  ur.on("end", () => {
652
- let bodyNote = "";
653
- if (toGateway && ur.statusCode >= 400) {
654
- bodyNote = errEnc
655
- ? ` body=${errEnc} ${errBytes} bytes`
656
- : errBody
657
- ? " body=" + errBody.slice(0, 1500).replace(/\s+/g, " ")
658
- : "";
659
- }
660
- 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
+ );
661
857
  });
662
858
  });
663
859
  up.on("error", (e) => {