@mnemom/mnemom 0.17.34-next.1 → 0.17.34-next.3

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-run.d.ts +14 -6
  6. package/dist/commands/agent-run.js +44 -27
  7. package/dist/commands/agent-session.d.ts +1 -2
  8. package/dist/commands/agent-session.js +1 -2
  9. package/dist/commands/agent-setup.d.ts +6 -0
  10. package/dist/commands/agent-setup.js +12 -1
  11. package/dist/commands/agent-share-test-issuer.d.ts +67 -0
  12. package/dist/commands/agent-share-test-issuer.js +139 -0
  13. package/dist/commands/agent-share.d.ts +7 -1
  14. package/dist/commands/agent-share.js +14 -4
  15. package/dist/commands/agent.d.ts +18 -5
  16. package/dist/commands/agent.js +186 -59
  17. package/dist/commands/durable-doctor.d.ts +50 -0
  18. package/dist/commands/durable-doctor.js +186 -0
  19. package/dist/commands/durable-lock.d.ts +261 -0
  20. package/dist/commands/durable-lock.js +600 -0
  21. package/dist/commands/durable-sessions.d.ts +87 -0
  22. package/dist/commands/durable-sessions.js +303 -0
  23. package/dist/commands/durable-verbs.d.ts +116 -0
  24. package/dist/commands/durable-verbs.js +393 -0
  25. package/dist/commands/scan.d.ts +12 -1
  26. package/dist/commands/scan.js +22 -3
  27. package/dist/commands/wrap.js +1 -1
  28. package/dist/durable/adopt-live.js +5 -8
  29. package/dist/durable/adopt.d.ts +8 -0
  30. package/dist/durable/adopt.js +27 -6
  31. package/dist/durable/cwd-rule.d.ts +9 -1
  32. package/dist/durable/cwd-rule.js +18 -0
  33. package/dist/durable/flock.d.ts +26 -0
  34. package/dist/durable/flock.js +80 -0
  35. package/dist/durable/launch-spec.d.ts +9 -1
  36. package/dist/durable/launch-spec.js +11 -4
  37. package/dist/durable/seat-proxy-entry.d.ts +11 -0
  38. package/dist/durable/seat-proxy-entry.js +25 -0
  39. package/dist/durable/seat-proxy.d.ts +107 -0
  40. package/dist/durable/seat-proxy.js +599 -0
  41. package/dist/durable/seat-socket.d.ts +46 -0
  42. package/dist/durable/seat-socket.js +179 -0
  43. package/dist/durable/seat-spec-validator.d.ts +28 -0
  44. package/dist/durable/seat-spec-validator.js +59 -0
  45. package/dist/durable/test-mode.mjs +105 -0
  46. package/dist/durable/turn-chain.d.ts +55 -0
  47. package/dist/durable/turn-chain.js +178 -0
  48. package/dist/host/auto-continue.d.ts +67 -0
  49. package/dist/host/auto-continue.js +118 -0
  50. package/dist/host/bootid.d.ts +13 -0
  51. package/dist/host/bootid.js +41 -0
  52. package/dist/host/client.d.ts +125 -0
  53. package/dist/host/client.js +171 -0
  54. package/dist/host/commands.d.ts +2 -0
  55. package/dist/host/commands.js +122 -0
  56. package/dist/host/control.d.ts +33 -0
  57. package/dist/host/control.js +144 -0
  58. package/dist/host/daemon.d.ts +221 -0
  59. package/dist/host/daemon.js +616 -0
  60. package/dist/host/gate.d.ts +46 -0
  61. package/dist/host/gate.js +82 -0
  62. package/dist/host/grant.d.ts +24 -0
  63. package/dist/host/grant.js +24 -0
  64. package/dist/host/instance.d.ts +35 -0
  65. package/dist/host/instance.js +62 -0
  66. package/dist/host/integrity.d.ts +35 -0
  67. package/dist/host/integrity.js +115 -0
  68. package/dist/host/launchd.d.ts +16 -0
  69. package/dist/host/launchd.js +45 -0
  70. package/dist/host/lock.d.ts +20 -0
  71. package/dist/host/lock.js +63 -0
  72. package/dist/host/owner.d.ts +58 -0
  73. package/dist/host/owner.js +123 -0
  74. package/dist/host/plist.d.ts +16 -0
  75. package/dist/host/plist.js +85 -0
  76. package/dist/host/procs.d.ts +27 -0
  77. package/dist/host/procs.js +105 -0
  78. package/dist/host/run.d.ts +65 -0
  79. package/dist/host/run.js +299 -0
  80. package/dist/host/runtime.d.ts +8 -0
  81. package/dist/host/runtime.js +22 -0
  82. package/dist/host/s1-names.d.ts +23 -0
  83. package/dist/host/s1-names.js +22 -0
  84. package/dist/host/seat-credential.d.ts +27 -0
  85. package/dist/host/seat-credential.js +41 -0
  86. package/dist/host/seat-launch-main.d.ts +1 -0
  87. package/dist/host/seat-launch-main.js +59 -0
  88. package/dist/host/seat-launch.d.ts +116 -0
  89. package/dist/host/seat-launch.js +302 -0
  90. package/dist/host/seat.d.ts +48 -0
  91. package/dist/host/seat.js +149 -0
  92. package/dist/host/state.d.ts +33 -0
  93. package/dist/host/state.js +117 -0
  94. package/dist/host/status.d.ts +101 -0
  95. package/dist/host/status.js +307 -0
  96. package/dist/host/tmux.d.ts +56 -0
  97. package/dist/host/tmux.js +182 -0
  98. package/dist/host/transcript.d.ts +30 -0
  99. package/dist/host/transcript.js +130 -0
  100. package/dist/host/verbs.d.ts +52 -0
  101. package/dist/host/verbs.js +282 -0
  102. package/dist/index.d.ts +8 -6
  103. package/dist/index.js +209 -43
  104. package/dist/lib/agent.d.ts +13 -29
  105. package/dist/lib/agent.js +17 -29
  106. package/dist/lib/claude-bin.d.ts +6 -0
  107. package/dist/lib/claude-bin.js +26 -0
  108. package/dist/lib/claude-plugin.d.ts +11 -2
  109. package/dist/lib/claude-plugin.js +10 -1
  110. package/dist/lib/commit-report.d.ts +5 -4
  111. package/dist/lib/commit-report.js +6 -9
  112. package/dist/lib/config.d.ts +8 -1
  113. package/dist/lib/config.js +28 -0
  114. package/dist/lib/durable/api.d.ts +136 -0
  115. package/dist/lib/durable/api.js +220 -0
  116. package/dist/lib/durable/codes.d.ts +8 -0
  117. package/dist/lib/durable/codes.js +2 -0
  118. package/dist/lib/durable/gate.d.ts +48 -0
  119. package/dist/lib/durable/gate.js +122 -0
  120. package/dist/lib/durable/handover.d.ts +101 -0
  121. package/dist/lib/durable/handover.js +175 -0
  122. package/dist/lib/durable/launch.d.ts +199 -0
  123. package/dist/lib/durable/launch.js +461 -0
  124. package/dist/lib/durable/launching.d.ts +15 -0
  125. package/dist/lib/durable/launching.js +70 -0
  126. package/dist/lib/durable/live.d.ts +190 -0
  127. package/dist/lib/durable/live.js +633 -0
  128. package/dist/lib/durable/refusal.d.ts +57 -0
  129. package/dist/lib/durable/refusal.js +78 -0
  130. package/dist/lib/durable/view.d.ts +83 -0
  131. package/dist/lib/durable/view.js +244 -0
  132. package/dist/lib/experimental.d.ts +0 -9
  133. package/dist/lib/experimental.js +18 -17
  134. package/dist/lib/launcher-config.d.ts +13 -0
  135. package/dist/lib/launcher-config.js +32 -0
  136. package/dist/lib/runner/exec-append.d.ts +15 -0
  137. package/dist/lib/runner/exec-append.js +2 -0
  138. package/dist/lib/runner/executor-client.d.ts +2 -1
  139. package/dist/lib/runner/executor-state.d.ts +10 -0
  140. package/dist/lib/runner/executor-state.js +24 -0
  141. package/dist/lib/runner/github-base.d.ts +24 -0
  142. package/dist/lib/runner/github-base.js +27 -0
  143. package/dist/lib/runner/job.d.ts +9 -4
  144. package/dist/lib/runner/job.js +43 -15
  145. package/dist/lib/runner/oidc.d.ts +3 -2
  146. package/dist/lib/runner/oidc.js +7 -4
  147. package/dist/lib/runner/options.d.ts +6 -0
  148. package/dist/lib/runner/options.js +19 -1
  149. package/dist/lib/runner/phases.d.ts +7 -4
  150. package/dist/lib/runner/phases.js +8 -4
  151. package/dist/lib/runner/proxy.d.ts +5 -1
  152. package/dist/lib/runner/proxy.js +7 -1
  153. package/dist/lib/scan/cost-per-pr.d.ts +93 -0
  154. package/dist/lib/scan/cost-per-pr.js +101 -0
  155. package/dist/lib/scan/record.d.ts +27 -1
  156. package/dist/lib/scan/record.js +114 -16
  157. package/dist/lib/scan/report-email.d.ts +26 -6
  158. package/dist/lib/scan/report-email.js +32 -3
  159. package/dist/lib/scan/scan.d.ts +3 -0
  160. package/dist/lib/scan/scan.js +13 -1
  161. package/dist/lib/subscription-login.d.ts +15 -0
  162. package/dist/lib/subscription-login.js +28 -1
  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 +3 -2
  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
@@ -15,7 +15,10 @@
15
15
  // (> WAIT_MS) → the request is forwarded without it (the gateway then behaves as before).
16
16
  //
17
17
  // CONFIG (env, none secret)
18
- // MNEMOM_AGENT_BU_DOOR gateway door URL (required, https only), e.g. https://gateway.mnemom.ai/anthropic
18
+ // MNEMOM_AGENT_BU_DOOR gateway door URL (required), e.g. https://gateway.mnemom.ai/anthropic;
19
+ // https; http only with MNEMOM_AGENT_BU_HTTP_DOOR=1 and on a
20
+ // loopback literal (127.0.0.1, [::1], localhost: the C17-R16 seam)
21
+ // MNEMOM_AGENT_BU_HTTP_DOOR "1" allows that http door; set by the launcher (see the gate below)
19
22
  // MNEMOM_AGENT_BU_CWD the session cwd (Claude Code's transcript lives under its slug)
20
23
  // MNEMOM_AGENT_BU_LOG append log lines here instead of stderr (optional)
21
24
  // MNEMOM_AGENT_BU_PARENT_PID exit when this pid is gone
@@ -43,7 +46,18 @@ try {
43
46
  } catch {
44
47
  die("MNEMOM_AGENT_BU_DOOR is required and must be a URL");
45
48
  }
46
- if (DOOR.protocol !== "https:") die("MNEMOM_AGENT_BU_DOOR must be https");
49
+ /** S1 LOOPBACK_HOSTS: the exact hostnames an http door may name (look-alikes such as 127.0.0.1.nip.io fail). */
50
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "[::1]", "localhost"]);
51
+ // The C17-R16 http-door seam (T3). The launcher (commands/agent.ts startBaseUrlProxy) sets
52
+ // MNEMOM_AGENT_BU_HTTP_DOOR=1 only when test-mode.mjs testSeamsHonoured / httpDoorAllowed hold, and
53
+ // strips an inherited value otherwise; this proxy reads no test-mode env itself. A proxy spawned
54
+ // directly trusts its parent's flag: the worst case is still http to a loopback literal only.
55
+ const httpDoorAllowed = env.MNEMOM_AGENT_BU_HTTP_DOOR === "1" && LOOPBACK_HOSTS.has(DOOR.hostname);
56
+ if (DOOR.protocol === "http:") {
57
+ if (!httpDoorAllowed)
58
+ die("MNEMOM_AGENT_BU_DOOR must be https (http only on a loopback literal with the door flag)");
59
+ } else if (DOOR.protocol !== "https:") die("MNEMOM_AGENT_BU_DOOR must be https");
60
+ const transport = DOOR.protocol === "http:" ? http : https;
47
61
  const LOGFILE = env.MNEMOM_AGENT_BU_LOG || "";
48
62
  const PARENT = Number(env.MNEMOM_AGENT_BU_PARENT_PID || 0);
49
63
  const PORT = Number(env.MNEMOM_AGENT_BU_PORT || 0);
@@ -101,7 +115,7 @@ function recordState(h) {
101
115
  }
102
116
  }
103
117
 
104
- const agent = new https.Agent({ keepAlive: true, maxSockets: 32 });
118
+ const agent = new transport.Agent({ keepAlive: true, maxSockets: 32 });
105
119
  const doorPath = DOOR.pathname.replace(/\/$/, "");
106
120
  const isMessages = (u) => u === "/v1/messages" || u.startsWith("/v1/messages?");
107
121
 
@@ -123,7 +137,7 @@ function handle(req, res) {
123
137
 
124
138
  const forward = (note) => {
125
139
  const target = new URL(DOOR.origin + doorPath + req.url);
126
- const up = https.request(target, { method: req.method, headers, agent }, (ur) => {
140
+ const up = transport.request(target, { method: req.method, headers, agent }, (ur) => {
127
141
  recordState(ur.headers);
128
142
  res.writeHead(ur.statusCode, ur.headers);
129
143
  ur.pipe(res);
@@ -56,7 +56,8 @@ function listConfig(enabled) {
56
56
  out("Set one with: mnemom agent config set <key> <value>");
57
57
  out("");
58
58
  out("Settable keys:");
59
- for (const spec of CONFIG_KEYS)
59
+ // The durable key is listed only while `durable` is on (hidden like its verbs; C17).
60
+ for (const spec of CONFIG_KEYS.filter((k) => k.key !== "durable" || enabled.includes("durable")))
60
61
  out(` ${spec.key.padEnd(22)} ${spec.describe}${configKeyMark(spec.key, undefined, enabled)}`);
61
62
  return;
62
63
  }
@@ -199,6 +200,9 @@ function deleteKey(cfg, key) {
199
200
  case "compaction_intercept":
200
201
  delete cfg.compactionIntercept;
201
202
  break;
203
+ case "durable":
204
+ delete cfg.durable;
205
+ break;
202
206
  case "context_profile":
203
207
  delete cfg.contextProfile;
204
208
  break;
@@ -17,6 +17,15 @@ export interface DoctorOptions {
17
17
  cli?: string;
18
18
  /** The agent slug a launch would use (--agent / launcher.toml); default agent-<handle>. */
19
19
  agent?: string;
20
+ /**
21
+ * The durable lock lines (commands/durable-doctor.ts), passed only while the experimental
22
+ * `durable` gate is on; absent, the doctor is exactly as before.
23
+ */
24
+ durable?: () => Promise<Array<{
25
+ mark: "ok" | "warn" | "info";
26
+ text: string;
27
+ hint?: string;
28
+ }>>;
20
29
  }
21
30
  /** Run the preflight and print the checklist. Sets process.exitCode on failure. */
22
31
  export declare function agentDoctorCommand(options?: DoctorOptions): Promise<void>;
@@ -125,6 +125,10 @@ export async function agentDoctorCommand(options = {}) {
125
125
  // ── CLI auto-update ──
126
126
  const upd = await probeUpdateStatus();
127
127
  add(upd.mark, upd.text, upd.hint);
128
+ // ── durable lock preconditions (gate on only): never a MISS ──
129
+ if (options.durable)
130
+ for (const l of await options.durable())
131
+ add(l.mark, l.text, l.hint);
128
132
  // ── render ──
129
133
  const door = resolveAnthropicDoor(env);
130
134
  out("mnemom agent doctor");
@@ -11,6 +11,7 @@
11
11
  * git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv).
12
12
  */
13
13
  import { type FetchFn, type RunnerApi } from "../lib/runner/executor-client.js";
14
+ import { type GithubBases } from "../lib/runner/github-base.js";
14
15
  import type { JobDeps, Workspace } from "../lib/runner/job.js";
15
16
  import { type PhaseInvocation } from "../lib/runner/phases.js";
16
17
  import { type RunFlags } from "../lib/runner/options.js";
@@ -24,27 +25,34 @@ export interface RunDeps {
24
25
  err(line: string): void;
25
26
  /** Aborted by SIGTERM or SIGINT. */
26
27
  stop: AbortSignal;
27
- jobDeps(api: RunnerApi): JobDeps;
28
+ /** The text of this host's executor-state.json, or null when there is none. */
29
+ readExecutorState?(): Promise<string | null>;
30
+ jobDeps(api: RunnerApi, executorId: string | null, github: GithubBases): JobDeps;
28
31
  }
29
32
  /** Resolves the process exit code; never throws for a usage, config or Mnemom error. */
30
33
  export declare function agentRunCommand(flags: RunFlags, deps: RunDeps): Promise<number>;
31
- export declare const GITHUB_API = "https://api.github.com";
32
- /** git's env with the token as an http extra header: it never reaches argv or the remote URL. */
33
- export declare function gitAuthEnv(parent: NodeJS.ProcessEnv, token: string | null): NodeJS.ProcessEnv;
34
+ export declare const GITHUB_API: string;
35
+ /**
36
+ * git's env with the token as an http extra header for the web base only: it never reaches argv
37
+ * or the remote URL.
38
+ */
39
+ export declare function gitAuthEnv(parent: NodeJS.ProcessEnv, token: string | null, web?: string): NodeJS.ProcessEnv;
34
40
  export type Git = (cwd: string | undefined, args: string[]) => Promise<string>;
35
41
  export declare function liveGit(env: NodeJS.ProcessEnv): Git;
36
42
  /** A fresh clone on a job branch, plus a fresh, empty HOME for the phases. */
37
43
  export declare function prepareWorkspace(git: Git, cloneUrl: string, sessionId: string, root?: string): Promise<Workspace>;
38
44
  export declare function cleanupWorkspace(ws: Workspace): Promise<void>;
39
45
  /** Push the job branch and open its PR against the repo's default branch; resolves its URL. */
40
- export declare function openPr(git: Git, fetch: FetchFn, token: string | null, repo: string, ws: Workspace, title: string): Promise<string>;
46
+ export declare function openPr(git: Git, fetch: FetchFn, token: string | null, repo: string, ws: Workspace, title: string, apiBase?: string): Promise<string>;
41
47
  /** merge_on_green: GitHub's auto-merge on the PR (squash). */
42
- export declare function enableAutoMerge(fetch: FetchFn, token: string | null, prUrl: string): Promise<void>;
48
+ export declare function enableAutoMerge(fetch: FetchFn, token: string | null, prUrl: string, bases?: GithubBases): Promise<void>;
43
49
  /** Run one phase's process in the job's working copy; stdout goes to our stderr, never stdout. */
44
50
  export declare function runPhaseProcess(inv: PhaseInvocation, ws: Workspace, modelUrl: string, signal: AbortSignal, parentEnv?: NodeJS.ProcessEnv): Promise<number>;
45
51
  export declare function liveJobDeps(api: RunnerApi, opts: {
46
52
  fetch: FetchFn;
47
53
  env: NodeJS.ProcessEnv;
54
+ executorId: string | null;
55
+ github: GithubBases;
48
56
  }): JobDeps;
49
57
  /** The process wiring: signals, stdout and stderr. Resolves the exit code. */
50
58
  export declare function runAgentRun(flags: RunFlags): Promise<number>;
@@ -12,11 +12,14 @@
12
12
  */
13
13
  import { spawn, execFile } from "node:child_process";
14
14
  import { randomUUID } from "node:crypto";
15
- import { mkdtemp, rm } from "node:fs/promises";
16
- import { tmpdir } from "node:os";
15
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
16
+ import { homedir, tmpdir } from "node:os";
17
17
  import { join } from "node:path";
18
18
  import { getApiUrl, getGatewayUrl } from "../lib/config.js";
19
19
  import { runnerApi } from "../lib/runner/executor-client.js";
20
+ import { resolveClaudeBin } from "../lib/claude-bin.js";
21
+ import { executorIdFromState } from "../lib/runner/executor-state.js";
22
+ import { GITHUB_BASES, prUrlPattern, resolveGithubBases, } from "../lib/runner/github-base.js";
20
23
  import { runOnce, runWatch } from "../lib/runner/loop.js";
21
24
  import { exchangeOidc } from "../lib/runner/oidc.js";
22
25
  import { exitCodeFor, outcomeLine, refusedOutcome, USAGE_EXIT_CODE, } from "../lib/runner/outcome.js";
@@ -24,6 +27,7 @@ import { phaseEnv } from "../lib/runner/phases.js";
24
27
  import { resolveRunPlan } from "../lib/runner/options.js";
25
28
  import { startModelProxy } from "../lib/runner/proxy.js";
26
29
  import { resolveGithubToken } from "./agent-fuel.js";
30
+ import { executorStatePath } from "../vendor/durable-contract/index.js";
27
31
  /** Resolves the process exit code; never throws for a usage, config or Mnemom error. */
28
32
  export async function agentRunCommand(flags, deps) {
29
33
  const resolved = resolveRunPlan(flags, deps.env);
@@ -34,7 +38,11 @@ export async function agentRunCommand(flags, deps) {
34
38
  const plan = resolved.plan;
35
39
  for (const w of plan.warnings)
36
40
  deps.err(w);
41
+ const gh = resolveGithubBases(deps.env);
42
+ if (gh.warning)
43
+ deps.err(gh.warning);
37
44
  let executorKey;
45
+ let executorId = plan.executorId;
38
46
  if (plan.credential.kind === "oidc") {
39
47
  const ex = await exchangeOidc({
40
48
  fetch: deps.fetch,
@@ -50,19 +58,28 @@ export async function agentRunCommand(flags, deps) {
50
58
  return exitCodeFor(o.outcome);
51
59
  }
52
60
  executorKey = ex.executorKey;
61
+ executorId ??= ex.executorId;
53
62
  }
54
63
  else {
55
64
  executorKey = plan.credential.key;
56
65
  }
66
+ // The env id wins, then the exchange answer's; else this host's executor-state.json, when it
67
+ // holds the key in use.
68
+ if (executorId === null && deps.readExecutorState) {
69
+ executorId = executorIdFromState(await deps.readExecutorState(), executorKey) ?? executorId;
70
+ }
57
71
  const api = runnerApi({ apiUrl: deps.apiUrl, executorKey, fetch: deps.fetch });
58
72
  const loop = { failMode: plan.failMode, stop: deps.stop, emit: deps.emit };
59
- const job = deps.jobDeps(api);
73
+ const job = deps.jobDeps(api, executorId, gh.bases);
60
74
  return plan.form === "once" ? runOnce(job, loop) : runWatch(job, loop);
61
75
  }
62
76
  // ── GitHub and git, for the live driver ──────────────────────────────────────
63
- export const GITHUB_API = "https://api.github.com";
64
- /** git's env with the token as an http extra header: it never reaches argv or the remote URL. */
65
- export function gitAuthEnv(parent, token) {
77
+ export const GITHUB_API = GITHUB_BASES.api;
78
+ /**
79
+ * git's env with the token as an http extra header for the web base only: it never reaches argv
80
+ * or the remote URL.
81
+ */
82
+ export function gitAuthEnv(parent, token, web = GITHUB_BASES.web) {
66
83
  const env = { ...parent, GIT_TERMINAL_PROMPT: "0" };
67
84
  if (!token)
68
85
  return env;
@@ -70,7 +87,7 @@ export function gitAuthEnv(parent, token) {
70
87
  return {
71
88
  ...env,
72
89
  GIT_CONFIG_COUNT: "1",
73
- GIT_CONFIG_KEY_0: "http.https://github.com/.extraheader",
90
+ GIT_CONFIG_KEY_0: `http.${web}/.extraheader`,
74
91
  GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${basic}`,
75
92
  };
76
93
  }
@@ -100,10 +117,10 @@ export async function prepareWorkspace(git, cloneUrl, sessionId, root = tmpdir()
100
117
  export async function cleanupWorkspace(ws) {
101
118
  await Promise.all([ws.dir, ws.home].map((d) => rm(d, { recursive: true, force: true })));
102
119
  }
103
- async function github(fetch, token, method, path, body) {
120
+ async function github(fetch, token, method, path, body, apiBase) {
104
121
  if (!token)
105
122
  throw new Error("no GitHub token (set GH_TOKEN or GITHUB_TOKEN)");
106
- const res = await fetch(`${GITHUB_API}${path}`, {
123
+ const res = await fetch(`${apiBase}${path}`, {
107
124
  method,
108
125
  headers: {
109
126
  accept: "application/vnd.github+json",
@@ -122,38 +139,33 @@ async function github(fetch, token, method, path, body) {
122
139
  return { status: res.status, json };
123
140
  }
124
141
  /** Push the job branch and open its PR against the repo's default branch; resolves its URL. */
125
- export async function openPr(git, fetch, token, repo, ws, title) {
142
+ export async function openPr(git, fetch, token, repo, ws, title, apiBase = GITHUB_BASES.api) {
126
143
  await git(ws.dir, ["push", "--quiet", "origin", `HEAD:refs/heads/${ws.branch}`]);
127
- const meta = await github(fetch, token, "GET", `/repos/${repo}`);
144
+ const meta = await github(fetch, token, "GET", `/repos/${repo}`, undefined, apiBase);
128
145
  const base = meta.json.default_branch;
129
146
  if (meta.status !== 200 || typeof base !== "string") {
130
147
  throw new Error(`reading ${repo} answered ${meta.status}`);
131
148
  }
132
- const pr = await github(fetch, token, "POST", `/repos/${repo}/pulls`, {
133
- title,
134
- head: ws.branch,
135
- base,
136
- });
149
+ const pr = await github(fetch, token, "POST", `/repos/${repo}/pulls`, { title, head: ws.branch, base }, apiBase);
137
150
  const url = pr.json.html_url;
138
151
  if (pr.status !== 201 || typeof url !== "string") {
139
152
  throw new Error(`opening the PR answered ${pr.status}`);
140
153
  }
141
154
  return url;
142
155
  }
143
- const PR_URL_RE = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)$/;
144
156
  /** merge_on_green: GitHub's auto-merge on the PR (squash). */
145
- export async function enableAutoMerge(fetch, token, prUrl) {
146
- const m = PR_URL_RE.exec(prUrl);
157
+ export async function enableAutoMerge(fetch, token, prUrl, bases = GITHUB_BASES) {
158
+ const m = prUrlPattern(bases.web).exec(prUrl);
147
159
  if (!m)
148
160
  throw new Error(`not a GitHub PR URL: ${prUrl}`);
149
- const pr = await github(fetch, token, "GET", `/repos/${m[1]}/pulls/${m[2]}`);
161
+ const pr = await github(fetch, token, "GET", `/repos/${m[1]}/pulls/${m[2]}`, undefined, bases.api);
150
162
  if (pr.status !== 200 || typeof pr.json.node_id !== "string") {
151
163
  throw new Error(`reading the PR answered ${pr.status}`);
152
164
  }
153
165
  const r = await github(fetch, token, "POST", "/graphql", {
154
166
  query: "mutation($id: ID!) { enablePullRequestAutoMerge(input: {pullRequestId: $id, mergeMethod: SQUASH}) { clientMutationId } }",
155
167
  variables: { id: pr.json.node_id },
156
- });
168
+ }, bases.api);
157
169
  if (r.status !== 200 || r.json.errors !== undefined) {
158
170
  throw new Error(`enabling auto-merge answered ${r.status}`);
159
171
  }
@@ -162,7 +174,9 @@ export async function enableAutoMerge(fetch, token, prUrl) {
162
174
  export function runPhaseProcess(inv, ws, modelUrl, signal, parentEnv = process.env) {
163
175
  return new Promise((resolve, reject) => {
164
176
  let started = false;
165
- const child = spawn(inv.command, inv.args, {
177
+ // A capabilities phase's `claude` is the pinned one: MNEMOM_AGENT_CLI when absolute, else PATH.
178
+ const command = inv.command === "claude" ? (resolveClaudeBin(parentEnv) ?? "claude") : inv.command;
179
+ const child = spawn(command, inv.args, {
166
180
  cwd: ws.dir,
167
181
  env: phaseEnv(parentEnv, ws.home, modelUrl),
168
182
  stdio: ["pipe", "pipe", "inherit"],
@@ -181,10 +195,12 @@ export function runPhaseProcess(inv, ws, modelUrl, signal, parentEnv = process.e
181
195
  });
182
196
  }
183
197
  export function liveJobDeps(api, opts) {
198
+ const gh = opts.github;
184
199
  const token = resolveGithubToken(opts.env);
185
- const git = liveGit(gitAuthEnv(opts.env, token));
200
+ const git = liveGit(gitAuthEnv(opts.env, token, gh.web));
186
201
  return {
187
202
  api,
203
+ executorId: opts.executorId,
188
204
  nonce: randomUUID(),
189
205
  newSpawnId: () => randomUUID(),
190
206
  now: () => Date.now(),
@@ -192,10 +208,10 @@ export function liveJobDeps(api, opts) {
192
208
  log: (line) => process.stderr.write(`mnemom agent run: ${line}\n`),
193
209
  gatewayUrl: `${getGatewayUrl()}/anthropic`,
194
210
  startProxy: (state) => startModelProxy(`${getGatewayUrl()}/anthropic`, state),
195
- prepareWorkspace: (repo, sessionId) => prepareWorkspace(git, `https://github.com/${repo}.git`, sessionId),
211
+ prepareWorkspace: (repo, sessionId) => prepareWorkspace(git, `${gh.web}/${repo}.git`, sessionId),
196
212
  runPhase: (inv, ws, modelUrl, signal) => runPhaseProcess(inv, ws, modelUrl, signal, opts.env),
197
- openPr: (repo, ws, title) => openPr(git, opts.fetch, token, repo, ws, title),
198
- enableAutoMerge: (prUrl) => enableAutoMerge(opts.fetch, token, prUrl),
213
+ openPr: (repo, ws, title) => openPr(git, opts.fetch, token, repo, ws, title, gh.api),
214
+ enableAutoMerge: (prUrl) => enableAutoMerge(opts.fetch, token, prUrl, gh),
199
215
  cleanup: cleanupWorkspace,
200
216
  };
201
217
  }
@@ -213,7 +229,8 @@ export function runAgentRun(flags) {
213
229
  emit: (line) => process.stdout.write(`${line}\n`),
214
230
  err: (line) => process.stderr.write(`${line}\n`),
215
231
  stop: stop.signal,
216
- jobDeps: (api) => liveJobDeps(api, { fetch, env: process.env }),
232
+ readExecutorState: () => readFile(executorStatePath(homedir()), "utf8").catch(() => null),
233
+ jobDeps: (api, executorId, github) => liveJobDeps(api, { fetch, env: process.env, executorId, github }),
217
234
  }).finally(() => {
218
235
  process.off("SIGTERM", onSignal);
219
236
  process.off("SIGINT", onSignal);
@@ -12,8 +12,7 @@
12
12
  * (`x-mnemom-contract`). Implicit alignment — the goal inferred from your own
13
13
  * messages (`x-mnemom-goal-mode: implicit`, no contract) — is the experimental
14
14
  * `implicit` feature: while it is on a bare launch runs implicit, while it is off
15
- * (the default) a bare launch runs with alignment off. On a prerelease (`next`)
16
- * build a bare launch runs implicit unless it is headless `-p` (T16). `--no-implicit` (or
15
+ * (the default) a bare launch runs with alignment off. `--no-implicit` (or
17
16
  * launcher.toml `goal_mode = "off"`) turns it off. The mode is resolved upstream
18
17
  * (`resolveImplicit`) and announced on the launch line.
19
18
  *
@@ -12,8 +12,7 @@
12
12
  * (`x-mnemom-contract`). Implicit alignment — the goal inferred from your own
13
13
  * messages (`x-mnemom-goal-mode: implicit`, no contract) — is the experimental
14
14
  * `implicit` feature: while it is on a bare launch runs implicit, while it is off
15
- * (the default) a bare launch runs with alignment off. On a prerelease (`next`)
16
- * build a bare launch runs implicit unless it is headless `-p` (T16). `--no-implicit` (or
15
+ * (the default) a bare launch runs with alignment off. `--no-implicit` (or
17
16
  * launcher.toml `goal_mode = "off"`) turns it off. The mode is resolved upstream
18
17
  * (`resolveImplicit`) and announced on the launch line.
19
18
  *
@@ -288,6 +288,12 @@ export declare function resolvePinnedAgent(id: string): Promise<{
288
288
  id: string;
289
289
  slug: string;
290
290
  }>;
291
+ /**
292
+ * The x-mnemom-agent value for a pinned agent: its stored name exactly, when that name is a
293
+ * header-safe token (every api-valid name is: AGENT_NAME_REGEX allows only letters, digits and
294
+ * hyphens); "" otherwise, which refuses the launch.
295
+ */
296
+ export declare function pinnedSlugOf(name: string | null | undefined): string;
291
297
  /**
292
298
  * One line for an api failure that keeps the parts an operator can act on: the
293
299
  * message, the error code, and the scope the endpoint said it needed
@@ -696,12 +696,23 @@ export async function resolvePinnedAgent(id) {
696
696
  throw new UnclaimedAgentError(`mnemom agent: agent ${id} is not claimed, and mnemom agent never runs on an unclaimed agent. ` +
697
697
  `Claim it first: \`mnemom agent claim ${id} --key <key>\`.`);
698
698
  }
699
- const slug = sanitizeAgentName(agent.name ?? "");
699
+ // The stored name goes out verbatim: the gateway compares the pin's name exactly
700
+ // (agent-identity.ts), and the api allows uppercase and hyphen runs, which sanitizeAgentName
701
+ // would rewrite (mnemom-test #1061). sanitizeAgentName is for new names only.
702
+ const slug = pinnedSlugOf(agent.name);
700
703
  if (!slug) {
701
704
  throw new UnclaimedAgentError(`mnemom agent: agent ${id} has no usable name to send as x-mnemom-agent; rename it, or launch by name.`);
702
705
  }
703
706
  return { id, slug };
704
707
  }
708
+ /**
709
+ * The x-mnemom-agent value for a pinned agent: its stored name exactly, when that name is a
710
+ * header-safe token (every api-valid name is: AGENT_NAME_REGEX allows only letters, digits and
711
+ * hyphens); "" otherwise, which refuses the launch.
712
+ */
713
+ export function pinnedSlugOf(name) {
714
+ return typeof name === "string" && /^[A-Za-z0-9-]+$/.test(name) ? name : "";
715
+ }
705
716
  /** The refusal for a pinned launch the gateway did not (or could not) honour. */
706
717
  function pinRefusal(err, pin, slug) {
707
718
  const head = `mnemom agent: the gateway would not run this launch as ${pin}`;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * TEST-CELL card issuer for `mnemom agent share`. Lets the share-card flow
3
+ * (AAP-ShareCard/v1) run end to end against the TEST cell before the production signing
4
+ * endpoint (MNE-9440) exists. It does NOT replace that endpoint and never touches a prod key.
5
+ *
6
+ * Gating is by the environment the CLI TARGETS, not by a flag: the issuer is selected only
7
+ * when BOTH the resolved API base is the test cell API and the resolved website base is the
8
+ * test website (see `isTestCellTarget`). Anything else (prod, www, local, a mixed target such
9
+ * as test website + prod API, an unparseable URL) is "not test" and gets no test signing.
10
+ * `agentShareCommand` re-checks this after issuance, so a test-signed card can never be
11
+ * linked from a non-test target even if a different issuer returned one.
12
+ *
13
+ * HOW A TEST-SIGNED CARD IS IDENTIFIED. The card is a normal, v1-valid AAP-ShareCard/v1:
14
+ * `signing_key_id` is the aip TEST key id `test-sharecard-key-1`. Every test key id starts with
15
+ * `test-` (TEST_KEY_ID_PREFIX); production ids start `aap-prod-`. `signing_key_id` sits inside
16
+ * the signed canonical JSON, so the marker cannot be stripped or forged without breaking the
17
+ * signature, and v1 verification is unchanged. Consumers label a card test-signed with
18
+ * `isTestSignedKeyId(card.signing_key_id)`.
19
+ *
20
+ * KEY. The seed is the public test seed (32 bytes of 0x2a) that mnemom/aip uses for its
21
+ * fixtures (aip packages/verifier/test/helpers/sharecard-fixtures.ts: TEST_SEED, TEST_KEY_ID).
22
+ * It is not a secret and has no relation to any production key. Its public half is
23
+ * TEST_SHARECARD_PUBLIC_KEY_HEX / TEST_SHARECARD_JWKS below, which a verifier must be given
24
+ * explicitly as the trusted key; the production JWKS never contains it.
25
+ */
26
+ import type * as AipVerifier from "@mnemom/aip-verifier";
27
+ import type { AgentShareDeps, CardIssuer } from "./agent-share.js";
28
+ /** aip test key id; ids of every test key start with this prefix. */
29
+ export declare const TEST_KEY_ID = "test-sharecard-key-1";
30
+ export declare const TEST_KEY_ID_PREFIX = "test-";
31
+ /** Public half of the test key, hex (same value the fixture verifier trusts). */
32
+ export declare const TEST_SHARECARD_PUBLIC_KEY_HEX = "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61";
33
+ /** The same public key as a JWKS (RFC 8037 OKP/Ed25519). Never serve this from a product JWKS. */
34
+ export declare const TEST_SHARECARD_JWKS: {
35
+ readonly keys: readonly [{
36
+ readonly kty: "OKP";
37
+ readonly crv: "Ed25519";
38
+ readonly kid: "test-sharecard-key-1";
39
+ readonly alg: "EdDSA";
40
+ readonly use: "sig";
41
+ readonly x: string;
42
+ }];
43
+ };
44
+ export declare const TEST_CELL_API_HOST = "api-az-test.mnemom.ai";
45
+ export declare const TEST_CELL_WEBSITE_HOST = "test.mnemom.ai";
46
+ export declare function isTestSignedKeyId(keyId: unknown): boolean;
47
+ /**
48
+ * True only when the CLI targets the test cell: API base AND website base both resolve to the
49
+ * test cell hosts (exact match, https). Fails closed on anything else, including a retired
50
+ * environment name that makes config resolution throw.
51
+ */
52
+ export declare function isTestCellTarget(api?: () => string, website?: () => string): boolean;
53
+ type Verifier = typeof AipVerifier;
54
+ export interface TestIssuerOptions {
55
+ now?: () => Date;
56
+ cardId?: () => string;
57
+ loadVerifier?: () => Promise<Pick<Verifier, "shareCardSigningInput">>;
58
+ }
59
+ /** Issues AAP-ShareCard/v1 cards signed with the aip TEST key. Construct only for a test target. */
60
+ export declare function createTestCardIssuer(opts?: TestIssuerOptions): CardIssuer;
61
+ /**
62
+ * Choose the dependency set for `mnemom agent share`. Pure function of the target: the test
63
+ * issuer (and fixture candidate lines, since no real statement source exists on test yet) are
64
+ * wired only for a test target. The fixture env flag on a non-test target is refused loudly.
65
+ */
66
+ export declare function selectAgentShareDeps(live: AgentShareDeps, env: Record<string, string | undefined>, isTest?: () => boolean): AgentShareDeps;
67
+ export {};
@@ -0,0 +1,139 @@
1
+ /**
2
+ * TEST-CELL card issuer for `mnemom agent share`. Lets the share-card flow
3
+ * (AAP-ShareCard/v1) run end to end against the TEST cell before the production signing
4
+ * endpoint (MNE-9440) exists. It does NOT replace that endpoint and never touches a prod key.
5
+ *
6
+ * Gating is by the environment the CLI TARGETS, not by a flag: the issuer is selected only
7
+ * when BOTH the resolved API base is the test cell API and the resolved website base is the
8
+ * test website (see `isTestCellTarget`). Anything else (prod, www, local, a mixed target such
9
+ * as test website + prod API, an unparseable URL) is "not test" and gets no test signing.
10
+ * `agentShareCommand` re-checks this after issuance, so a test-signed card can never be
11
+ * linked from a non-test target even if a different issuer returned one.
12
+ *
13
+ * HOW A TEST-SIGNED CARD IS IDENTIFIED. The card is a normal, v1-valid AAP-ShareCard/v1:
14
+ * `signing_key_id` is the aip TEST key id `test-sharecard-key-1`. Every test key id starts with
15
+ * `test-` (TEST_KEY_ID_PREFIX); production ids start `aap-prod-`. `signing_key_id` sits inside
16
+ * the signed canonical JSON, so the marker cannot be stripped or forged without breaking the
17
+ * signature, and v1 verification is unchanged. Consumers label a card test-signed with
18
+ * `isTestSignedKeyId(card.signing_key_id)`.
19
+ *
20
+ * KEY. The seed is the public test seed (32 bytes of 0x2a) that mnemom/aip uses for its
21
+ * fixtures (aip packages/verifier/test/helpers/sharecard-fixtures.ts: TEST_SEED, TEST_KEY_ID).
22
+ * It is not a secret and has no relation to any production key. Its public half is
23
+ * TEST_SHARECARD_PUBLIC_KEY_HEX / TEST_SHARECARD_JWKS below, which a verifier must be given
24
+ * explicitly as the trusted key; the production JWKS never contains it.
25
+ */
26
+ import { createPrivateKey, randomBytes, sign as edSign } from "node:crypto";
27
+ import { getApiUrl, getWebsiteUrl } from "../lib/config.js";
28
+ import { aipLineRenderer } from "./agent-share-render.js";
29
+ import { FIXTURE_CANDIDATES } from "./agent-share-fixture.js";
30
+ /** aip test key id; ids of every test key start with this prefix. */
31
+ export const TEST_KEY_ID = "test-sharecard-key-1";
32
+ export const TEST_KEY_ID_PREFIX = "test-";
33
+ /** Public aip fixture seed (0x2a x 32). TEST ONLY. */
34
+ const TEST_SEED = Buffer.alloc(32, 0x2a);
35
+ /** Public half of the test key, hex (same value the fixture verifier trusts). */
36
+ export const TEST_SHARECARD_PUBLIC_KEY_HEX = "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61";
37
+ /** The same public key as a JWKS (RFC 8037 OKP/Ed25519). Never serve this from a product JWKS. */
38
+ export const TEST_SHARECARD_JWKS = {
39
+ keys: [
40
+ {
41
+ kty: "OKP",
42
+ crv: "Ed25519",
43
+ kid: TEST_KEY_ID,
44
+ alg: "EdDSA",
45
+ use: "sig",
46
+ x: Buffer.from(TEST_SHARECARD_PUBLIC_KEY_HEX, "hex").toString("base64url"),
47
+ },
48
+ ],
49
+ };
50
+ export const TEST_CELL_API_HOST = "api-az-test.mnemom.ai";
51
+ export const TEST_CELL_WEBSITE_HOST = "test.mnemom.ai";
52
+ export function isTestSignedKeyId(keyId) {
53
+ return typeof keyId === "string" && keyId.startsWith(TEST_KEY_ID_PREFIX);
54
+ }
55
+ function hostOf(url) {
56
+ try {
57
+ const u = new URL(url);
58
+ return u.protocol === "https:" ? u.hostname.toLowerCase() : null;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * True only when the CLI targets the test cell: API base AND website base both resolve to the
66
+ * test cell hosts (exact match, https). Fails closed on anything else, including a retired
67
+ * environment name that makes config resolution throw.
68
+ */
69
+ export function isTestCellTarget(api = getApiUrl, website = getWebsiteUrl) {
70
+ try {
71
+ return hostOf(api()) === TEST_CELL_API_HOST && hostOf(website()) === TEST_CELL_WEBSITE_HOST;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ // PKCS#8 wrapper for a raw Ed25519 seed.
78
+ const PKCS8_ED25519_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
79
+ /** Issues AAP-ShareCard/v1 cards signed with the aip TEST key. Construct only for a test target. */
80
+ export function createTestCardIssuer(opts = {}) {
81
+ const now = opts.now ?? (() => new Date());
82
+ const cardId = opts.cardId ?? (() => `card-test-${randomBytes(6).toString("hex")}`);
83
+ const load = opts.loadVerifier ?? (() => import("@mnemom/aip-verifier"));
84
+ return {
85
+ async issue(lines) {
86
+ const { shareCardSigningInput } = await load();
87
+ const at = now();
88
+ const day = (d) => d.toISOString().slice(0, 10);
89
+ const hashes = [...new Set(lines.map((l) => l.statement_hash))];
90
+ const unsigned = {
91
+ typ: "AAP-ShareCard/v1",
92
+ card_id: cardId(),
93
+ period: { start: day(new Date(at.getTime() - 6 * 86_400_000)), end: day(at) },
94
+ issued_at: at.toISOString(),
95
+ signing_key_id: TEST_KEY_ID,
96
+ statements: hashes.map((hash) => ({ typ: "AAP-ConductRecord/v1", hash })),
97
+ lines: lines.map((l) => ({ ...l })),
98
+ };
99
+ const key = createPrivateKey({
100
+ key: Buffer.concat([PKCS8_ED25519_PREFIX, TEST_SEED]),
101
+ format: "der",
102
+ type: "pkcs8",
103
+ });
104
+ const signature = edSign(null, Buffer.from(shareCardSigningInput(unsigned), "utf8"), key);
105
+ return {
106
+ ...unsigned,
107
+ signature: signature.toString("base64url"),
108
+ };
109
+ },
110
+ };
111
+ }
112
+ /**
113
+ * Choose the dependency set for `mnemom agent share`. Pure function of the target: the test
114
+ * issuer (and fixture candidate lines, since no real statement source exists on test yet) are
115
+ * wired only for a test target. The fixture env flag on a non-test target is refused loudly.
116
+ */
117
+ export function selectAgentShareDeps(live, env, isTest = isTestCellTarget) {
118
+ if (isTest()) {
119
+ return {
120
+ ...live,
121
+ source: { listLines: async () => FIXTURE_CANDIDATES },
122
+ issuer: createTestCardIssuer(),
123
+ renderer: aipLineRenderer(TEST_SHARECARD_PUBLIC_KEY_HEX),
124
+ targetsTestCell: () => true,
125
+ };
126
+ }
127
+ if (env.MNEMOM_AGENT_SHARE_FIXTURE === "1") {
128
+ return {
129
+ ...live,
130
+ source: {
131
+ listLines: async () => {
132
+ throw new Error("fixture mode only runs against the test cell");
133
+ },
134
+ },
135
+ targetsTestCell: () => false,
136
+ };
137
+ }
138
+ return { ...live, targetsTestCell: () => false };
139
+ }
@@ -6,7 +6,11 @@
6
6
  * (W6.2 — the share offer never auto-posts).
7
7
  *
8
8
  * Gated by MNEMOM_AGENT_SHARE=1 (not experimental-registry; off by default, so default CLI
9
- * behaviour is unchanged). Everything outside the process is injected:
9
+ * behaviour is unchanged), EXCEPT when the CLI targets the test cell (MNEMOM_RING=test, or API
10
+ * and website both resolving to api-az-test.mnemom.ai / test.mnemom.ai): there it is on without
11
+ * the flag. The test target is detected once, by `isTestCellTarget` (agent-share-test-issuer.ts),
12
+ * and reaches the command as `deps.targetsTestCell`. Prod and every other ring stay gated.
13
+ * Everything outside the process is injected:
10
14
  * - `source` lists candidate lines from the user's signed statements,
11
15
  * - `issuer` returns the SIGNED AAP-ShareCard/v1 for the chosen lines (the CLI never holds
12
16
  * a signing key),
@@ -73,6 +77,8 @@ export interface AgentShareDeps {
73
77
  pick: (lines: SharedCardLine[]) => Promise<string[]>;
74
78
  isTTY: () => boolean;
75
79
  websiteBase: () => string;
80
+ /** True only when the CLI targets the test cell. Absent means "not test". */
81
+ targetsTestCell?: () => boolean;
76
82
  out: (line: string) => void;
77
83
  err: (line: string) => void;
78
84
  }