@mnemom/mnemom 0.17.34-next.2 → 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 (75) hide show
  1. package/dist/commands/agent-run.d.ts +14 -6
  2. package/dist/commands/agent-run.js +44 -27
  3. package/dist/commands/agent-share-test-issuer.d.ts +67 -0
  4. package/dist/commands/agent-share-test-issuer.js +139 -0
  5. package/dist/commands/agent-share.d.ts +7 -1
  6. package/dist/commands/agent-share.js +14 -4
  7. package/dist/commands/agent.d.ts +7 -5
  8. package/dist/commands/agent.js +64 -36
  9. package/dist/commands/durable-lock.d.ts +8 -1
  10. package/dist/commands/durable-lock.js +9 -4
  11. package/dist/commands/scan.d.ts +12 -1
  12. package/dist/commands/scan.js +22 -3
  13. package/dist/commands/wrap.js +1 -1
  14. package/dist/durable/adopt-live.js +5 -8
  15. package/dist/durable/adopt.d.ts +8 -0
  16. package/dist/durable/adopt.js +27 -6
  17. package/dist/durable/cwd-rule.d.ts +9 -1
  18. package/dist/durable/cwd-rule.js +18 -0
  19. package/dist/durable/launch-spec.d.ts +2 -0
  20. package/dist/durable/launch-spec.js +1 -1
  21. package/dist/durable/test-mode.mjs +25 -1
  22. package/dist/host/client.d.ts +3 -1
  23. package/dist/host/daemon.d.ts +7 -3
  24. package/dist/host/daemon.js +13 -3
  25. package/dist/host/owner.d.ts +3 -0
  26. package/dist/host/owner.js +2 -0
  27. package/dist/host/run.d.ts +2 -3
  28. package/dist/host/run.js +5 -20
  29. package/dist/host/s1-names.d.ts +1 -0
  30. package/dist/host/s1-names.js +1 -0
  31. package/dist/host/verbs.d.ts +2 -0
  32. package/dist/host/verbs.js +11 -0
  33. package/dist/index.js +13 -9
  34. package/dist/lib/agent.d.ts +5 -3
  35. package/dist/lib/agent.js +6 -3
  36. package/dist/lib/claude-bin.d.ts +6 -0
  37. package/dist/lib/claude-bin.js +26 -0
  38. package/dist/lib/commit-report.d.ts +5 -4
  39. package/dist/lib/commit-report.js +6 -9
  40. package/dist/lib/config.d.ts +8 -1
  41. package/dist/lib/config.js +28 -0
  42. package/dist/lib/durable/launch.d.ts +28 -11
  43. package/dist/lib/durable/launch.js +33 -15
  44. package/dist/lib/durable/live.d.ts +15 -19
  45. package/dist/lib/durable/live.js +41 -43
  46. package/dist/lib/launcher-config.d.ts +6 -0
  47. package/dist/lib/launcher-config.js +11 -0
  48. package/dist/lib/runner/exec-append.d.ts +15 -0
  49. package/dist/lib/runner/exec-append.js +2 -0
  50. package/dist/lib/runner/executor-client.d.ts +2 -1
  51. package/dist/lib/runner/executor-state.d.ts +10 -0
  52. package/dist/lib/runner/executor-state.js +24 -0
  53. package/dist/lib/runner/github-base.d.ts +24 -0
  54. package/dist/lib/runner/github-base.js +27 -0
  55. package/dist/lib/runner/job.d.ts +9 -4
  56. package/dist/lib/runner/job.js +43 -15
  57. package/dist/lib/runner/oidc.d.ts +3 -2
  58. package/dist/lib/runner/oidc.js +7 -4
  59. package/dist/lib/runner/options.d.ts +6 -0
  60. package/dist/lib/runner/options.js +19 -1
  61. package/dist/lib/runner/phases.d.ts +7 -4
  62. package/dist/lib/runner/phases.js +8 -4
  63. package/dist/lib/runner/proxy.d.ts +5 -1
  64. package/dist/lib/runner/proxy.js +7 -1
  65. package/dist/lib/scan/cost-per-pr.d.ts +93 -0
  66. package/dist/lib/scan/cost-per-pr.js +101 -0
  67. package/dist/lib/scan/record.d.ts +27 -1
  68. package/dist/lib/scan/record.js +114 -16
  69. package/dist/lib/scan/report-email.d.ts +25 -5
  70. package/dist/lib/scan/report-email.js +23 -2
  71. package/dist/lib/scan/scan.d.ts +3 -0
  72. package/dist/lib/scan/scan.js +13 -1
  73. package/package.json +1 -1
  74. package/dist/lib/durable/first-turn.d.ts +0 -50
  75. package/dist/lib/durable/first-turn.js +0 -102
@@ -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);
@@ -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
  }
@@ -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),
@@ -15,9 +19,10 @@
15
19
  * identifying fields, and re-checks the returned card for the same.
16
20
  */
17
21
  import { createInterface } from "node:readline/promises";
18
- import { getWebsiteUrl } from "../lib/config.js";
22
+ import { getShareWebsiteUrl } from "../lib/config.js";
19
23
  import { fmt } from "../lib/format.js";
20
24
  import { aipLineRenderer, aipLineScreen } from "./agent-share-render.js";
25
+ import { isTestSignedKeyId } from "./agent-share-test-issuer.js";
21
26
  export const AGENT_SHARE_ENV = "MNEMOM_AGENT_SHARE";
22
27
  export function agentShareEnabled(env = process.env) {
23
28
  return env[AGENT_SHARE_ENV] === "1";
@@ -117,7 +122,7 @@ function checkIssuedCard(card, chosen) {
117
122
  return null;
118
123
  }
119
124
  export async function agentShareCommand(flags, deps, env = process.env) {
120
- if (!agentShareEnabled(env)) {
125
+ if (!agentShareEnabled(env) && deps.targetsTestCell?.() !== true) {
121
126
  deps.err(fmt.error("unknown command 'share' for 'mnemom agent'"));
122
127
  return 1;
123
128
  }
@@ -218,6 +223,11 @@ export async function agentShareCommand(flags, deps, env = process.env) {
218
223
  deps.err(fmt.error(`Refusing to share: ${problem}.`));
219
224
  return 1;
220
225
  }
226
+ // A test-signed card (aip test key id) may only leave a test-cell run; never a prod/www one.
227
+ if (isTestSignedKeyId(card.signing_key_id) && deps.targetsTestCell?.() !== true) {
228
+ deps.err(fmt.error("Refusing to share: the card is test-signed but this is not the test cell."));
229
+ return 1;
230
+ }
221
231
  // The renderer is the gate: nothing is linked unless aip renders the card (verified, lint clean).
222
232
  let rendered;
223
233
  try {
@@ -266,7 +276,7 @@ export const liveAgentShareDeps = {
266
276
  }
267
277
  },
268
278
  isTTY: () => Boolean(process.stdin.isTTY),
269
- websiteBase: getWebsiteUrl,
279
+ websiteBase: getShareWebsiteUrl,
270
280
  out: (line) => console.log(line),
271
281
  err: (line) => console.error(line),
272
282
  };
@@ -307,6 +307,8 @@ export declare function promptForCliPath(cliOpt: string | undefined, env: NodeJS
307
307
  * Does the resolved coding-agent CLI advertise the `--remote-control` startup
308
308
  * flag (Claude Code >= 2.1.269)? Determined by scanning its `--help`. Best-effort:
309
309
  * a spawn failure or timeout reads as "no". Only the claude adapter wires RC.
310
+ * One launch asks this more than once (the shape choice, then the lock's RC naming), so the answer
311
+ * for an unchanged binary is remembered and `--help` runs once per process (BACKBONE l.150).
310
312
  */
311
313
  export declare function cliSupportsRemoteControl(bin: string): boolean;
312
314
  /** The result of checking whether Remote Control can run here. */
@@ -343,6 +345,7 @@ export declare function proxyWanted(o: {
343
345
  turnHeader: boolean;
344
346
  receipt: boolean;
345
347
  commitReport: boolean;
348
+ fedStatusline?: boolean;
346
349
  }): boolean;
347
350
  /**
348
351
  * Whether the proxy captures the gateway's record state: the conduct receipt reads it at exit,
@@ -369,8 +372,6 @@ export declare function prepareStatuslineFeed(o: {
369
372
  statusline: boolean;
370
373
  orgId?: string;
371
374
  version: string;
372
- goalMode: boolean;
373
- env: NodeJS.ProcessEnv;
374
375
  childEnv: NodeJS.ProcessEnv;
375
376
  files?: () => {
376
377
  feedFile: string;
@@ -389,10 +390,11 @@ export declare function recordOrgWanted(o: {
389
390
  }): boolean;
390
391
  /**
391
392
  * Whether this launch feeds the status line measured model cost and μ (W1.4): prerelease
392
- * builds only (stable stays "mnemom", decision 14), and only when the turn proxy runs, since
393
- * the proxy is what learns the gateway's lineage key for the session.
393
+ * builds only (stable stays "mnemom", decision 14). Not tied to goal mode: the base-URL proxy
394
+ * runs for a fed launch (proxyWanted) and records the lineage key whatever the goal mode, and
395
+ * since A-W-016 a -next.N launch has no goal by default.
394
396
  */
395
- export declare function fedStatuslineWanted(version: string, goalMode: boolean, env: NodeJS.ProcessEnv): boolean;
397
+ export declare function fedStatuslineWanted(version: string): boolean;
396
398
  /**
397
399
  * Add 127.0.0.1 and localhost to BOTH NO_PROXY and no_proxy (proxy clients disagree on which one
398
400
  * wins, and some read only the lowercase one), keeping whatever each already listed.