@mnemom/mnemom 0.17.34-next.2 → 0.17.34-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent-run.d.ts +16 -6
- package/dist/commands/agent-run.js +52 -27
- package/dist/commands/agent-share-test-issuer.d.ts +67 -0
- package/dist/commands/agent-share-test-issuer.js +139 -0
- package/dist/commands/agent-share.d.ts +7 -1
- package/dist/commands/agent-share.js +14 -4
- package/dist/commands/agent.d.ts +7 -5
- package/dist/commands/agent.js +84 -45
- package/dist/commands/durable-lock.d.ts +8 -1
- package/dist/commands/durable-lock.js +9 -4
- package/dist/commands/scan.d.ts +12 -1
- package/dist/commands/scan.js +22 -3
- package/dist/commands/wrap.js +1 -1
- package/dist/durable/adopt-live.js +5 -8
- package/dist/durable/adopt.d.ts +8 -0
- package/dist/durable/adopt.js +27 -6
- package/dist/durable/cwd-rule.d.ts +9 -1
- package/dist/durable/cwd-rule.js +18 -0
- package/dist/durable/launch-spec.d.ts +2 -0
- package/dist/durable/launch-spec.js +1 -1
- package/dist/durable/test-mode.mjs +25 -1
- package/dist/host/client.d.ts +3 -1
- package/dist/host/daemon.d.ts +7 -3
- package/dist/host/daemon.js +13 -3
- package/dist/host/owner.d.ts +3 -0
- package/dist/host/owner.js +2 -0
- package/dist/host/run.d.ts +2 -3
- package/dist/host/run.js +5 -20
- package/dist/host/s1-names.d.ts +1 -0
- package/dist/host/s1-names.js +1 -0
- package/dist/host/verbs.d.ts +2 -0
- package/dist/host/verbs.js +11 -0
- package/dist/index.js +13 -9
- package/dist/lib/agent.d.ts +5 -3
- package/dist/lib/agent.js +6 -3
- package/dist/lib/claude-bin.d.ts +6 -0
- package/dist/lib/claude-bin.js +26 -0
- package/dist/lib/commit-report.d.ts +5 -4
- package/dist/lib/commit-report.js +6 -9
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.js +28 -0
- package/dist/lib/durable/launch.d.ts +28 -11
- package/dist/lib/durable/launch.js +33 -15
- package/dist/lib/durable/live.d.ts +15 -19
- package/dist/lib/durable/live.js +41 -43
- package/dist/lib/launcher-config.d.ts +6 -0
- package/dist/lib/launcher-config.js +11 -0
- package/dist/lib/runner/exec-append.d.ts +15 -0
- package/dist/lib/runner/exec-append.js +2 -0
- package/dist/lib/runner/executor-client.d.ts +2 -1
- package/dist/lib/runner/executor-state.d.ts +10 -0
- package/dist/lib/runner/executor-state.js +24 -0
- package/dist/lib/runner/github-base.d.ts +24 -0
- package/dist/lib/runner/github-base.js +27 -0
- package/dist/lib/runner/job.d.ts +11 -4
- package/dist/lib/runner/job.js +59 -16
- package/dist/lib/runner/oidc.d.ts +3 -2
- package/dist/lib/runner/oidc.js +7 -4
- package/dist/lib/runner/options.d.ts +6 -0
- package/dist/lib/runner/options.js +19 -1
- package/dist/lib/runner/outcome.js +2 -3
- package/dist/lib/runner/phases.d.ts +7 -4
- package/dist/lib/runner/phases.js +8 -4
- package/dist/lib/runner/proxy.d.ts +5 -1
- package/dist/lib/runner/proxy.js +7 -1
- package/dist/lib/scan/cost-per-pr.d.ts +93 -0
- package/dist/lib/scan/cost-per-pr.js +101 -0
- package/dist/lib/scan/record.d.ts +27 -1
- package/dist/lib/scan/record.js +114 -16
- package/dist/lib/scan/report-email.d.ts +25 -5
- package/dist/lib/scan/report-email.js +23 -2
- package/dist/lib/scan/scan.d.ts +3 -0
- package/dist/lib/scan/scan.js +13 -1
- package/dist/lib/session-key.d.ts +4 -3
- package/dist/vendor/durable-contract/index.js +207 -169
- package/dist/vendor/shared/durable-contract/doors.d.ts +26 -1
- package/dist/vendor/shared/durable-contract/doors.js +37 -1
- package/dist/vendor/shared/durable-contract/events.d.ts +1 -1
- package/dist/vendor/shared/durable-contract/events.js +13 -3
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +2 -2
- package/dist/vendor/shared/durable-contract/schema.d.ts +5 -0
- package/dist/vendor/shared/durable-contract/schema.js +6 -0
- package/dist/vendor/shared/durable-contract/states.d.ts +2 -1
- package/dist/vendor/shared/durable-contract/states.js +2 -1
- package/package.json +1 -1
- package/dist/lib/durable/first-turn.d.ts +0 -50
- 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,36 @@ export interface RunDeps {
|
|
|
24
25
|
err(line: string): void;
|
|
25
26
|
/** Aborted by SIGTERM or SIGINT. */
|
|
26
27
|
stop: AbortSignal;
|
|
27
|
-
|
|
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
|
|
32
|
-
/**
|
|
33
|
-
|
|
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>;
|
|
45
|
+
/** Commits on the job branch that no origin ref has: the clone's base is origin's default branch. */
|
|
46
|
+
export declare function commitsAhead(git: Git, ws: Workspace): Promise<number>;
|
|
39
47
|
/** 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>;
|
|
48
|
+
export declare function openPr(git: Git, fetch: FetchFn, token: string | null, repo: string, ws: Workspace, title: string, apiBase?: string): Promise<string>;
|
|
41
49
|
/** 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>;
|
|
50
|
+
export declare function enableAutoMerge(fetch: FetchFn, token: string | null, prUrl: string, bases?: GithubBases): Promise<void>;
|
|
43
51
|
/** Run one phase's process in the job's working copy; stdout goes to our stderr, never stdout. */
|
|
44
52
|
export declare function runPhaseProcess(inv: PhaseInvocation, ws: Workspace, modelUrl: string, signal: AbortSignal, parentEnv?: NodeJS.ProcessEnv): Promise<number>;
|
|
45
53
|
export declare function liveJobDeps(api: RunnerApi, opts: {
|
|
46
54
|
fetch: FetchFn;
|
|
47
55
|
env: NodeJS.ProcessEnv;
|
|
56
|
+
executorId: string | null;
|
|
57
|
+
github: GithubBases;
|
|
48
58
|
}): JobDeps;
|
|
49
59
|
/** The process wiring: signals, stdout and stderr. Resolves the exit code. */
|
|
50
60
|
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 =
|
|
64
|
-
/**
|
|
65
|
-
|
|
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:
|
|
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(`${
|
|
123
|
+
const res = await fetch(`${apiBase}${path}`, {
|
|
107
124
|
method,
|
|
108
125
|
headers: {
|
|
109
126
|
accept: "application/vnd.github+json",
|
|
@@ -121,39 +138,41 @@ async function github(fetch, token, method, path, body) {
|
|
|
121
138
|
}
|
|
122
139
|
return { status: res.status, json };
|
|
123
140
|
}
|
|
141
|
+
/** Commits on the job branch that no origin ref has: the clone's base is origin's default branch. */
|
|
142
|
+
export async function commitsAhead(git, ws) {
|
|
143
|
+
const n = Number(await git(ws.dir, ["rev-list", "--count", "HEAD", "--not", "--remotes=origin"]));
|
|
144
|
+
if (!Number.isInteger(n) || n < 0)
|
|
145
|
+
throw new Error(`git rev-list answered ${n}`);
|
|
146
|
+
return n;
|
|
147
|
+
}
|
|
124
148
|
/** 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) {
|
|
149
|
+
export async function openPr(git, fetch, token, repo, ws, title, apiBase = GITHUB_BASES.api) {
|
|
126
150
|
await git(ws.dir, ["push", "--quiet", "origin", `HEAD:refs/heads/${ws.branch}`]);
|
|
127
|
-
const meta = await github(fetch, token, "GET", `/repos/${repo}
|
|
151
|
+
const meta = await github(fetch, token, "GET", `/repos/${repo}`, undefined, apiBase);
|
|
128
152
|
const base = meta.json.default_branch;
|
|
129
153
|
if (meta.status !== 200 || typeof base !== "string") {
|
|
130
154
|
throw new Error(`reading ${repo} answered ${meta.status}`);
|
|
131
155
|
}
|
|
132
|
-
const pr = await github(fetch, token, "POST", `/repos/${repo}/pulls`, {
|
|
133
|
-
title,
|
|
134
|
-
head: ws.branch,
|
|
135
|
-
base,
|
|
136
|
-
});
|
|
156
|
+
const pr = await github(fetch, token, "POST", `/repos/${repo}/pulls`, { title, head: ws.branch, base }, apiBase);
|
|
137
157
|
const url = pr.json.html_url;
|
|
138
158
|
if (pr.status !== 201 || typeof url !== "string") {
|
|
139
159
|
throw new Error(`opening the PR answered ${pr.status}`);
|
|
140
160
|
}
|
|
141
161
|
return url;
|
|
142
162
|
}
|
|
143
|
-
const PR_URL_RE = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)$/;
|
|
144
163
|
/** merge_on_green: GitHub's auto-merge on the PR (squash). */
|
|
145
|
-
export async function enableAutoMerge(fetch, token, prUrl) {
|
|
146
|
-
const m =
|
|
164
|
+
export async function enableAutoMerge(fetch, token, prUrl, bases = GITHUB_BASES) {
|
|
165
|
+
const m = prUrlPattern(bases.web).exec(prUrl);
|
|
147
166
|
if (!m)
|
|
148
167
|
throw new Error(`not a GitHub PR URL: ${prUrl}`);
|
|
149
|
-
const pr = await github(fetch, token, "GET", `/repos/${m[1]}/pulls/${m[2]}
|
|
168
|
+
const pr = await github(fetch, token, "GET", `/repos/${m[1]}/pulls/${m[2]}`, undefined, bases.api);
|
|
150
169
|
if (pr.status !== 200 || typeof pr.json.node_id !== "string") {
|
|
151
170
|
throw new Error(`reading the PR answered ${pr.status}`);
|
|
152
171
|
}
|
|
153
172
|
const r = await github(fetch, token, "POST", "/graphql", {
|
|
154
173
|
query: "mutation($id: ID!) { enablePullRequestAutoMerge(input: {pullRequestId: $id, mergeMethod: SQUASH}) { clientMutationId } }",
|
|
155
174
|
variables: { id: pr.json.node_id },
|
|
156
|
-
});
|
|
175
|
+
}, bases.api);
|
|
157
176
|
if (r.status !== 200 || r.json.errors !== undefined) {
|
|
158
177
|
throw new Error(`enabling auto-merge answered ${r.status}`);
|
|
159
178
|
}
|
|
@@ -162,7 +181,9 @@ export async function enableAutoMerge(fetch, token, prUrl) {
|
|
|
162
181
|
export function runPhaseProcess(inv, ws, modelUrl, signal, parentEnv = process.env) {
|
|
163
182
|
return new Promise((resolve, reject) => {
|
|
164
183
|
let started = false;
|
|
165
|
-
|
|
184
|
+
// A capabilities phase's `claude` is the pinned one: MNEMOM_AGENT_CLI when absolute, else PATH.
|
|
185
|
+
const command = inv.command === "claude" ? (resolveClaudeBin(parentEnv) ?? "claude") : inv.command;
|
|
186
|
+
const child = spawn(command, inv.args, {
|
|
166
187
|
cwd: ws.dir,
|
|
167
188
|
env: phaseEnv(parentEnv, ws.home, modelUrl),
|
|
168
189
|
stdio: ["pipe", "pipe", "inherit"],
|
|
@@ -181,10 +202,12 @@ export function runPhaseProcess(inv, ws, modelUrl, signal, parentEnv = process.e
|
|
|
181
202
|
});
|
|
182
203
|
}
|
|
183
204
|
export function liveJobDeps(api, opts) {
|
|
205
|
+
const gh = opts.github;
|
|
184
206
|
const token = resolveGithubToken(opts.env);
|
|
185
|
-
const git = liveGit(gitAuthEnv(opts.env, token));
|
|
207
|
+
const git = liveGit(gitAuthEnv(opts.env, token, gh.web));
|
|
186
208
|
return {
|
|
187
209
|
api,
|
|
210
|
+
executorId: opts.executorId,
|
|
188
211
|
nonce: randomUUID(),
|
|
189
212
|
newSpawnId: () => randomUUID(),
|
|
190
213
|
now: () => Date.now(),
|
|
@@ -192,10 +215,11 @@ export function liveJobDeps(api, opts) {
|
|
|
192
215
|
log: (line) => process.stderr.write(`mnemom agent run: ${line}\n`),
|
|
193
216
|
gatewayUrl: `${getGatewayUrl()}/anthropic`,
|
|
194
217
|
startProxy: (state) => startModelProxy(`${getGatewayUrl()}/anthropic`, state),
|
|
195
|
-
prepareWorkspace: (repo, sessionId) => prepareWorkspace(git,
|
|
218
|
+
prepareWorkspace: (repo, sessionId) => prepareWorkspace(git, `${gh.web}/${repo}.git`, sessionId),
|
|
196
219
|
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
|
-
|
|
220
|
+
openPr: (repo, ws, title) => openPr(git, opts.fetch, token, repo, ws, title, gh.api),
|
|
221
|
+
commitsAhead: (ws) => commitsAhead(git, ws),
|
|
222
|
+
enableAutoMerge: (prUrl) => enableAutoMerge(opts.fetch, token, prUrl, gh),
|
|
199
223
|
cleanup: cleanupWorkspace,
|
|
200
224
|
};
|
|
201
225
|
}
|
|
@@ -213,7 +237,8 @@ export function runAgentRun(flags) {
|
|
|
213
237
|
emit: (line) => process.stdout.write(`${line}\n`),
|
|
214
238
|
err: (line) => process.stderr.write(`${line}\n`),
|
|
215
239
|
stop: stop.signal,
|
|
216
|
-
|
|
240
|
+
readExecutorState: () => readFile(executorStatePath(homedir()), "utf8").catch(() => null),
|
|
241
|
+
jobDeps: (api, executorId, github) => liveJobDeps(api, { fetch, env: process.env, executorId, github }),
|
|
217
242
|
}).finally(() => {
|
|
218
243
|
process.off("SIGTERM", onSignal);
|
|
219
244
|
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)
|
|
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)
|
|
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 {
|
|
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:
|
|
279
|
+
websiteBase: getShareWebsiteUrl,
|
|
270
280
|
out: (line) => console.log(line),
|
|
271
281
|
err: (line) => console.error(line),
|
|
272
282
|
};
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -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)
|
|
393
|
-
*
|
|
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
|
|
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.
|