@mnemom/mnemom 0.17.32-next.4 → 0.17.33
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-fuel.d.ts +9 -0
- package/dist/commands/agent-fuel.js +36 -2
- package/dist/commands/agent-run.d.ts +50 -0
- package/dist/commands/agent-run.js +221 -0
- package/dist/commands/agent-setup.d.ts +8 -3
- package/dist/commands/agent-setup.js +32 -9
- package/dist/commands/agent-share-fixture.d.ts +3 -1
- package/dist/commands/agent-share-fixture.js +11 -7
- package/dist/commands/agent-share-render.d.ts +23 -0
- package/dist/commands/agent-share-render.js +72 -0
- package/dist/commands/agent-share.d.ts +15 -2
- package/dist/commands/agent-share.js +30 -9
- package/dist/commands/agent.d.ts +19 -4
- package/dist/commands/agent.js +33 -13
- package/dist/index.js +20 -0
- package/dist/lib/api.js +17 -1
- package/dist/lib/auth.d.ts +6 -0
- package/dist/lib/auth.js +161 -14
- package/dist/lib/experimental.d.ts +1 -1
- package/dist/lib/experimental.js +13 -0
- package/dist/lib/oauth.d.ts +7 -0
- package/dist/lib/oauth.js +13 -0
- package/dist/lib/runner/bundle.d.ts +129 -0
- package/dist/lib/runner/bundle.js +319 -0
- package/dist/lib/runner/contract-header.d.ts +46 -0
- package/dist/lib/runner/contract-header.js +85 -0
- package/dist/lib/runner/executor-client.d.ts +71 -0
- package/dist/lib/runner/executor-client.js +97 -0
- package/dist/lib/runner/job.d.ts +70 -0
- package/dist/lib/runner/job.js +463 -0
- package/dist/lib/runner/loop.d.ts +45 -0
- package/dist/lib/runner/loop.js +104 -0
- package/dist/lib/runner/oidc.d.ts +38 -0
- package/dist/lib/runner/oidc.js +89 -0
- package/dist/lib/runner/options.d.ts +41 -0
- package/dist/lib/runner/options.js +47 -0
- package/dist/lib/runner/outcome.d.ts +47 -0
- package/dist/lib/runner/outcome.js +89 -0
- package/dist/lib/runner/phases.d.ts +49 -0
- package/dist/lib/runner/phases.js +99 -0
- package/dist/lib/runner/proxy.d.ts +45 -0
- package/dist/lib/runner/proxy.js +153 -0
- package/dist/lib/runner/settings.d.ts +31 -0
- package/dist/lib/runner/settings.js +44 -0
- package/dist/lib/statusline-feed.js +10 -11
- package/dist/lib/statusline.d.ts +6 -0
- package/dist/lib/statusline.js +19 -9
- package/dist/rc-proxy.mjs +69 -5
- package/package.json +2 -1
|
@@ -44,7 +44,16 @@ export interface FuelDeps {
|
|
|
44
44
|
readCommitted(root: string, path: string): string;
|
|
45
45
|
out(line: string): void;
|
|
46
46
|
err(line: string): void;
|
|
47
|
+
/** Wait this many seconds (a 429's Retry-After); injected so tests never sleep. */
|
|
48
|
+
sleep?(seconds: number): Promise<void>;
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* How many times a 429 (the org's seal budget, Doors W5.9) is waited out before giving up. Each
|
|
52
|
+
* resend carries the same key, so a refused batch is sealed exactly once when it gets through.
|
|
53
|
+
*/
|
|
54
|
+
export declare const RATE_LIMIT_RETRIES = 5;
|
|
55
|
+
/** Seconds to wait before resending, for a 429; null for anything else. */
|
|
56
|
+
export declare function rateLimitWait(error: unknown): number | null;
|
|
48
57
|
/** A contract the lead drafted, checked and hashed locally before anything is sent. */
|
|
49
58
|
interface DraftedContract {
|
|
50
59
|
contract: Contract;
|
|
@@ -16,6 +16,21 @@ import { dirname, join } from "node:path";
|
|
|
16
16
|
import { createInterface } from "node:readline/promises";
|
|
17
17
|
import { listMyOrgs, MnemomApiError, newIdempotencyKey, sealContractBatch, } from "../lib/api.js";
|
|
18
18
|
import { checkContract, contractFileText, contractRepoPath, contractSha256, } from "../lib/contract-hash.js";
|
|
19
|
+
/**
|
|
20
|
+
* How many times a 429 (the org's seal budget, Doors W5.9) is waited out before giving up. Each
|
|
21
|
+
* resend carries the same key, so a refused batch is sealed exactly once when it gets through.
|
|
22
|
+
*/
|
|
23
|
+
export const RATE_LIMIT_RETRIES = 5;
|
|
24
|
+
/** The longest single wait, whatever Retry-After says; the api's window is 60 s. */
|
|
25
|
+
const RATE_LIMIT_WAIT_MAX_S = 60;
|
|
26
|
+
/** Seconds to wait before resending, for a 429; null for anything else. */
|
|
27
|
+
export function rateLimitWait(error) {
|
|
28
|
+
if (!(error instanceof MnemomApiError) || error.effectiveStatus !== 429)
|
|
29
|
+
return null;
|
|
30
|
+
const after = error.details?.retry_after_s;
|
|
31
|
+
const seconds = typeof after === "number" && Number.isFinite(after) && after > 0 ? after : 1;
|
|
32
|
+
return Math.min(Math.ceil(seconds), RATE_LIMIT_WAIT_MAX_S);
|
|
33
|
+
}
|
|
19
34
|
/** `owner/name` as GitHub issues it, the same rule the api applies: never `.` or `..` as a name. */
|
|
20
35
|
const REPO_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/(?!\.\.?$)[A-Za-z0-9_.-]{1,100}$/;
|
|
21
36
|
/** The batch size the api enforces. */
|
|
@@ -142,16 +157,32 @@ export async function agentFuelCommand(file, opts, deps) {
|
|
|
142
157
|
source: "cli",
|
|
143
158
|
contracts: drafted.map((d) => d.jobKey ? { contract: d.contract, job_key: d.jobKey } : { contract: d.contract }),
|
|
144
159
|
};
|
|
160
|
+
// A 429 is the org's seal budget (W5.9): wait out Retry-After and resend the same request, so a
|
|
161
|
+
// burst is delayed, never dropped. The api refused it before writing anything.
|
|
162
|
+
const send = async () => {
|
|
163
|
+
for (let attempt = 0;; attempt++) {
|
|
164
|
+
try {
|
|
165
|
+
return await deps.seal(orgId, body, key, githubToken);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const wait = rateLimitWait(error);
|
|
169
|
+
if (wait === null || attempt >= RATE_LIMIT_RETRIES)
|
|
170
|
+
throw error;
|
|
171
|
+
deps.err(`The organization's seal budget is spent; resending in ${wait} s.`);
|
|
172
|
+
await (deps.sleep ?? sleepSeconds)(wait);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
145
176
|
let result;
|
|
146
177
|
try {
|
|
147
|
-
result = await
|
|
178
|
+
result = await send();
|
|
148
179
|
}
|
|
149
180
|
catch (error) {
|
|
150
181
|
// A 5xx or a network fault may have reached the api, so retry once with the same key. A 4xx
|
|
151
182
|
// is the api's answer and is not retried.
|
|
152
183
|
if (!(error instanceof MnemomApiError) || error.effectiveStatus >= 500) {
|
|
153
184
|
try {
|
|
154
|
-
result = await
|
|
185
|
+
result = await send();
|
|
155
186
|
}
|
|
156
187
|
catch (retryError) {
|
|
157
188
|
// The api may have sealed before failing; the same key replays that batch, never a second one.
|
|
@@ -238,6 +269,9 @@ export function resolveGithubToken(env = process.env, gh = ghAuthToken) {
|
|
|
238
269
|
export function readFuelInput(path, read = readFileSync) {
|
|
239
270
|
return read(path === "-" ? 0 : path, "utf8");
|
|
240
271
|
}
|
|
272
|
+
function sleepSeconds(seconds) {
|
|
273
|
+
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
|
274
|
+
}
|
|
241
275
|
export const liveFuelDeps = {
|
|
242
276
|
readInput: (path) => readFuelInput(path),
|
|
243
277
|
listOrgs: () => listMyOrgs(),
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent run --watch|--once` (ruling 34.12; Doors W-L13, W3.11, W4.4, W4.12): the thin
|
|
3
|
+
* runner's driver.
|
|
4
|
+
*
|
|
5
|
+
* Everything that decides an outcome lives in lib/runner (and holds no prompts); this file only
|
|
6
|
+
* wires it to the world: the credential (env key, or the --oidc exchange), the executor client,
|
|
7
|
+
* the job's working copy, the phases' child processes, the PR, and SIGTERM/SIGINT. Usage and config
|
|
8
|
+
* errors exit 2 with no outcome line; every outcome line goes to stdout, every log to stderr.
|
|
9
|
+
*
|
|
10
|
+
* The GitHub token (GH_TOKEN, GITHUB_TOKEN, else `gh auth token`) is the runner's own: it reaches
|
|
11
|
+
* git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv).
|
|
12
|
+
*/
|
|
13
|
+
import { type FetchFn, type RunnerApi } from "../lib/runner/executor-client.js";
|
|
14
|
+
import type { JobDeps, Workspace } from "../lib/runner/job.js";
|
|
15
|
+
import { type PhaseInvocation } from "../lib/runner/phases.js";
|
|
16
|
+
import { type RunFlags } from "../lib/runner/options.js";
|
|
17
|
+
export interface RunDeps {
|
|
18
|
+
env: NodeJS.ProcessEnv;
|
|
19
|
+
fetch: FetchFn;
|
|
20
|
+
apiUrl: string;
|
|
21
|
+
/** stdout: one outcome line per job. */
|
|
22
|
+
emit(line: string): void;
|
|
23
|
+
/** stderr. */
|
|
24
|
+
err(line: string): void;
|
|
25
|
+
/** Aborted by SIGTERM or SIGINT. */
|
|
26
|
+
stop: AbortSignal;
|
|
27
|
+
jobDeps(api: RunnerApi): JobDeps;
|
|
28
|
+
}
|
|
29
|
+
/** Resolves the process exit code; never throws for a usage, config or Mnemom error. */
|
|
30
|
+
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 type Git = (cwd: string | undefined, args: string[]) => Promise<string>;
|
|
35
|
+
export declare function liveGit(env: NodeJS.ProcessEnv): Git;
|
|
36
|
+
/** A fresh clone on a job branch, plus a fresh, empty HOME for the phases. */
|
|
37
|
+
export declare function prepareWorkspace(git: Git, cloneUrl: string, sessionId: string, root?: string): Promise<Workspace>;
|
|
38
|
+
export declare function cleanupWorkspace(ws: Workspace): Promise<void>;
|
|
39
|
+
/** 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>;
|
|
41
|
+
/** 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>;
|
|
43
|
+
/** Run one phase's process in the job's working copy; stdout goes to our stderr, never stdout. */
|
|
44
|
+
export declare function runPhaseProcess(inv: PhaseInvocation, ws: Workspace, modelUrl: string, signal: AbortSignal, parentEnv?: NodeJS.ProcessEnv): Promise<number>;
|
|
45
|
+
export declare function liveJobDeps(api: RunnerApi, opts: {
|
|
46
|
+
fetch: FetchFn;
|
|
47
|
+
env: NodeJS.ProcessEnv;
|
|
48
|
+
}): JobDeps;
|
|
49
|
+
/** The process wiring: signals, stdout and stderr. Resolves the exit code. */
|
|
50
|
+
export declare function runAgentRun(flags: RunFlags): Promise<number>;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent run --watch|--once` (ruling 34.12; Doors W-L13, W3.11, W4.4, W4.12): the thin
|
|
3
|
+
* runner's driver.
|
|
4
|
+
*
|
|
5
|
+
* Everything that decides an outcome lives in lib/runner (and holds no prompts); this file only
|
|
6
|
+
* wires it to the world: the credential (env key, or the --oidc exchange), the executor client,
|
|
7
|
+
* the job's working copy, the phases' child processes, the PR, and SIGTERM/SIGINT. Usage and config
|
|
8
|
+
* errors exit 2 with no outcome line; every outcome line goes to stdout, every log to stderr.
|
|
9
|
+
*
|
|
10
|
+
* The GitHub token (GH_TOKEN, GITHUB_TOKEN, else `gh auth token`) is the runner's own: it reaches
|
|
11
|
+
* git only through GIT_CONFIG_* env (never argv) and the phases never see it (phaseEnv).
|
|
12
|
+
*/
|
|
13
|
+
import { spawn, execFile } from "node:child_process";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { getApiUrl, getGatewayUrl } from "../lib/config.js";
|
|
19
|
+
import { runnerApi } from "../lib/runner/executor-client.js";
|
|
20
|
+
import { runOnce, runWatch } from "../lib/runner/loop.js";
|
|
21
|
+
import { exchangeOidc } from "../lib/runner/oidc.js";
|
|
22
|
+
import { exitCodeFor, outcomeLine, refusedOutcome, USAGE_EXIT_CODE, } from "../lib/runner/outcome.js";
|
|
23
|
+
import { phaseEnv } from "../lib/runner/phases.js";
|
|
24
|
+
import { resolveRunPlan } from "../lib/runner/options.js";
|
|
25
|
+
import { startModelProxy } from "../lib/runner/proxy.js";
|
|
26
|
+
import { resolveGithubToken } from "./agent-fuel.js";
|
|
27
|
+
/** Resolves the process exit code; never throws for a usage, config or Mnemom error. */
|
|
28
|
+
export async function agentRunCommand(flags, deps) {
|
|
29
|
+
const resolved = resolveRunPlan(flags, deps.env);
|
|
30
|
+
if (!resolved.ok) {
|
|
31
|
+
deps.err(resolved.usage);
|
|
32
|
+
return USAGE_EXIT_CODE;
|
|
33
|
+
}
|
|
34
|
+
const plan = resolved.plan;
|
|
35
|
+
for (const w of plan.warnings)
|
|
36
|
+
deps.err(w);
|
|
37
|
+
let executorKey;
|
|
38
|
+
if (plan.credential.kind === "oidc") {
|
|
39
|
+
const ex = await exchangeOidc({
|
|
40
|
+
fetch: deps.fetch,
|
|
41
|
+
apiUrl: deps.apiUrl,
|
|
42
|
+
requestUrl: plan.credential.requestUrl,
|
|
43
|
+
requestToken: plan.credential.requestToken,
|
|
44
|
+
});
|
|
45
|
+
if (!ex.ok) {
|
|
46
|
+
// No job is held yet: fail closed whatever --fail-mode says (34.12 (6)).
|
|
47
|
+
deps.err(`mnemom agent run: refused (${ex.reason}): ${ex.detail}`);
|
|
48
|
+
const o = refusedOutcome({}, ex.reason);
|
|
49
|
+
deps.emit(outcomeLine(o));
|
|
50
|
+
return exitCodeFor(o.outcome);
|
|
51
|
+
}
|
|
52
|
+
executorKey = ex.executorKey;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
executorKey = plan.credential.key;
|
|
56
|
+
}
|
|
57
|
+
const api = runnerApi({ apiUrl: deps.apiUrl, executorKey, fetch: deps.fetch });
|
|
58
|
+
const loop = { failMode: plan.failMode, stop: deps.stop, emit: deps.emit };
|
|
59
|
+
const job = deps.jobDeps(api);
|
|
60
|
+
return plan.form === "once" ? runOnce(job, loop) : runWatch(job, loop);
|
|
61
|
+
}
|
|
62
|
+
// ── 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) {
|
|
66
|
+
const env = { ...parent, GIT_TERMINAL_PROMPT: "0" };
|
|
67
|
+
if (!token)
|
|
68
|
+
return env;
|
|
69
|
+
const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
|
|
70
|
+
return {
|
|
71
|
+
...env,
|
|
72
|
+
GIT_CONFIG_COUNT: "1",
|
|
73
|
+
GIT_CONFIG_KEY_0: "http.https://github.com/.extraheader",
|
|
74
|
+
GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${basic}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function liveGit(env) {
|
|
78
|
+
return (cwd, args) => new Promise((resolve, reject) => execFile("git", args, { cwd, env, encoding: "utf8" }, (e, out, stderr) => e
|
|
79
|
+
? reject(new Error(`git ${args[0]}: ${String(stderr).trim() || e.message}`))
|
|
80
|
+
: resolve(out.trim())));
|
|
81
|
+
}
|
|
82
|
+
/** A fresh clone on a job branch, plus a fresh, empty HOME for the phases. */
|
|
83
|
+
export async function prepareWorkspace(git, cloneUrl, sessionId, root = tmpdir()) {
|
|
84
|
+
const dir = await mkdtemp(join(root, "mnemom-job-"));
|
|
85
|
+
const home = await mkdtemp(join(root, "mnemom-home-"));
|
|
86
|
+
const branch = `mnemom/job-${sessionId.slice(0, 8)}`;
|
|
87
|
+
try {
|
|
88
|
+
await git(undefined, ["clone", "--quiet", "--depth", "50", "--", cloneUrl, dir]);
|
|
89
|
+
await git(dir, ["checkout", "--quiet", "-b", branch]);
|
|
90
|
+
// A phase that commits needs an identity; HOME is empty, so it is set on the repo.
|
|
91
|
+
await git(dir, ["config", "user.name", "mnemom runner"]);
|
|
92
|
+
await git(dir, ["config", "user.email", "runner@mnemom.ai"]);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
await cleanupWorkspace({ dir, home, branch });
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
return { dir, home, branch };
|
|
99
|
+
}
|
|
100
|
+
export async function cleanupWorkspace(ws) {
|
|
101
|
+
await Promise.all([ws.dir, ws.home].map((d) => rm(d, { recursive: true, force: true })));
|
|
102
|
+
}
|
|
103
|
+
async function github(fetch, token, method, path, body) {
|
|
104
|
+
if (!token)
|
|
105
|
+
throw new Error("no GitHub token (set GH_TOKEN or GITHUB_TOKEN)");
|
|
106
|
+
const res = await fetch(`${GITHUB_API}${path}`, {
|
|
107
|
+
method,
|
|
108
|
+
headers: {
|
|
109
|
+
accept: "application/vnd.github+json",
|
|
110
|
+
authorization: `Bearer ${token}`,
|
|
111
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
112
|
+
},
|
|
113
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
114
|
+
});
|
|
115
|
+
let json = {};
|
|
116
|
+
try {
|
|
117
|
+
json = JSON.parse(await res.text());
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// an empty or non-JSON answer: the status decides
|
|
121
|
+
}
|
|
122
|
+
return { status: res.status, json };
|
|
123
|
+
}
|
|
124
|
+
/** 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) {
|
|
126
|
+
await git(ws.dir, ["push", "--quiet", "origin", `HEAD:refs/heads/${ws.branch}`]);
|
|
127
|
+
const meta = await github(fetch, token, "GET", `/repos/${repo}`);
|
|
128
|
+
const base = meta.json.default_branch;
|
|
129
|
+
if (meta.status !== 200 || typeof base !== "string") {
|
|
130
|
+
throw new Error(`reading ${repo} answered ${meta.status}`);
|
|
131
|
+
}
|
|
132
|
+
const pr = await github(fetch, token, "POST", `/repos/${repo}/pulls`, {
|
|
133
|
+
title,
|
|
134
|
+
head: ws.branch,
|
|
135
|
+
base,
|
|
136
|
+
});
|
|
137
|
+
const url = pr.json.html_url;
|
|
138
|
+
if (pr.status !== 201 || typeof url !== "string") {
|
|
139
|
+
throw new Error(`opening the PR answered ${pr.status}`);
|
|
140
|
+
}
|
|
141
|
+
return url;
|
|
142
|
+
}
|
|
143
|
+
const PR_URL_RE = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)$/;
|
|
144
|
+
/** 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);
|
|
147
|
+
if (!m)
|
|
148
|
+
throw new Error(`not a GitHub PR URL: ${prUrl}`);
|
|
149
|
+
const pr = await github(fetch, token, "GET", `/repos/${m[1]}/pulls/${m[2]}`);
|
|
150
|
+
if (pr.status !== 200 || typeof pr.json.node_id !== "string") {
|
|
151
|
+
throw new Error(`reading the PR answered ${pr.status}`);
|
|
152
|
+
}
|
|
153
|
+
const r = await github(fetch, token, "POST", "/graphql", {
|
|
154
|
+
query: "mutation($id: ID!) { enablePullRequestAutoMerge(input: {pullRequestId: $id, mergeMethod: SQUASH}) { clientMutationId } }",
|
|
155
|
+
variables: { id: pr.json.node_id },
|
|
156
|
+
});
|
|
157
|
+
if (r.status !== 200 || r.json.errors !== undefined) {
|
|
158
|
+
throw new Error(`enabling auto-merge answered ${r.status}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Run one phase's process in the job's working copy; stdout goes to our stderr, never stdout. */
|
|
162
|
+
export function runPhaseProcess(inv, ws, modelUrl, signal, parentEnv = process.env) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
let started = false;
|
|
165
|
+
const child = spawn(inv.command, inv.args, {
|
|
166
|
+
cwd: ws.dir,
|
|
167
|
+
env: phaseEnv(parentEnv, ws.home, modelUrl),
|
|
168
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
169
|
+
signal,
|
|
170
|
+
});
|
|
171
|
+
child.on("spawn", () => {
|
|
172
|
+
started = true;
|
|
173
|
+
child.stdin?.end(inv.stdin ?? "");
|
|
174
|
+
});
|
|
175
|
+
child.stdout?.pipe(process.stderr, { end: false });
|
|
176
|
+
child.on("error", (e) => {
|
|
177
|
+
if (!started)
|
|
178
|
+
reject(e);
|
|
179
|
+
});
|
|
180
|
+
child.on("close", (code) => resolve(code ?? -1));
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
export function liveJobDeps(api, opts) {
|
|
184
|
+
const token = resolveGithubToken(opts.env);
|
|
185
|
+
const git = liveGit(gitAuthEnv(opts.env, token));
|
|
186
|
+
return {
|
|
187
|
+
api,
|
|
188
|
+
nonce: randomUUID(),
|
|
189
|
+
newSpawnId: () => randomUUID(),
|
|
190
|
+
now: () => Date.now(),
|
|
191
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
192
|
+
log: (line) => process.stderr.write(`mnemom agent run: ${line}\n`),
|
|
193
|
+
gatewayUrl: `${getGatewayUrl()}/anthropic`,
|
|
194
|
+
startProxy: (state) => startModelProxy(`${getGatewayUrl()}/anthropic`, state),
|
|
195
|
+
prepareWorkspace: (repo, sessionId) => prepareWorkspace(git, `https://github.com/${repo}.git`, sessionId),
|
|
196
|
+
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),
|
|
199
|
+
cleanup: cleanupWorkspace,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/** The process wiring: signals, stdout and stderr. Resolves the exit code. */
|
|
203
|
+
export function runAgentRun(flags) {
|
|
204
|
+
const stop = new AbortController();
|
|
205
|
+
const onSignal = () => stop.abort();
|
|
206
|
+
process.once("SIGTERM", onSignal);
|
|
207
|
+
process.once("SIGINT", onSignal);
|
|
208
|
+
const fetch = globalThis.fetch;
|
|
209
|
+
return agentRunCommand(flags, {
|
|
210
|
+
env: process.env,
|
|
211
|
+
fetch,
|
|
212
|
+
apiUrl: getApiUrl(),
|
|
213
|
+
emit: (line) => process.stdout.write(`${line}\n`),
|
|
214
|
+
err: (line) => process.stderr.write(`${line}\n`),
|
|
215
|
+
stop: stop.signal,
|
|
216
|
+
jobDeps: (api) => liveJobDeps(api, { fetch, env: process.env }),
|
|
217
|
+
}).finally(() => {
|
|
218
|
+
process.off("SIGTERM", onSignal);
|
|
219
|
+
process.off("SIGINT", onSignal);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -316,14 +316,19 @@ interface PublishOpts {
|
|
|
316
316
|
*/
|
|
317
317
|
export declare function keptPrincipal(own: unknown, opts: Pick<PublishOpts, "orgId" | "ownOrg">): Record<string, unknown>;
|
|
318
318
|
/**
|
|
319
|
-
* `mnemom agent setup [--agent <slug|mnm-id>] [--governance …]` — the explicit
|
|
320
|
-
* point. An id (`--agent mnm-<uuid>`, or launcher.toml `agent_id`) refreshes
|
|
321
|
-
* cards and never claims or births; a malformed `mnm-…` refuses.
|
|
319
|
+
* `mnemom agent setup [--agent <slug|mnm-id>] [--governance …] [--auth …]` — the explicit
|
|
320
|
+
* provisioning entry point. An id (`--agent mnm-<uuid>`, or launcher.toml `agent_id`) refreshes
|
|
321
|
+
* that pinned agent's cards and never claims or births; a malformed `mnm-…` refuses.
|
|
322
|
+
*
|
|
323
|
+
* The auth mode resolves as a launch's does (`--auth`, else MNEMOM_AGENT_AUTH, which launcher.toml
|
|
324
|
+
* `auth` folds into, else api-key). A subscription setup asks for no Anthropic key: the identity
|
|
325
|
+
* anchors on the Mnemom API key, exactly as the subscription launch it provisions for (MNE-9282).
|
|
322
326
|
*/
|
|
323
327
|
export declare function agentSetupCommand(opts?: {
|
|
324
328
|
agent?: string;
|
|
325
329
|
agentId?: string;
|
|
326
330
|
governance?: GovernanceOptIn;
|
|
331
|
+
auth?: string;
|
|
327
332
|
}): Promise<void>;
|
|
328
333
|
/** Thrown when the agent's org has no spendable MU balance. The user-facing
|
|
329
334
|
* top-up guidance is already printed when this is thrown — callers exit quietly. */
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
* reinventing it.
|
|
37
37
|
*/
|
|
38
38
|
import { claimAgent, getAgent, getAgentByName, getCardSources, getMe, getMuBalance, getMyPersonalOrg, listMyAgents, MnemomApiError, mintProtectionCapability, putAlignmentCard, putProtectionCard, } from "../lib/api.js";
|
|
39
|
-
import { getAuthInfo, loginWithBrowser, loginWithDeviceFlow, resolveAuth } from "../lib/auth.js";
|
|
39
|
+
import { getAuthInfo, hasExpiredStoredSession, loginWithBrowser, loginWithDeviceFlow, resolveAuth, } from "../lib/auth.js";
|
|
40
40
|
import { getActiveOrg } from "../lib/cli-config.js";
|
|
41
41
|
import { getApiUrl, getGatewayUrl, getWebsiteUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
42
42
|
import { fmt } from "../lib/format.js";
|
|
43
43
|
import { askYesNo, isInteractive } from "../lib/prompt.js";
|
|
44
44
|
import { malformedAgentIdMessage, parseAgentRef, resolveAnthropicDoor, resolveGatewayHost, sanitizeAgentName, singleCellError, } from "../lib/agent.js";
|
|
45
|
-
import { readMnemomKey, resolveSubscriptionMnemomKey } from "../lib/subscription-login.js";
|
|
45
|
+
import { readMnemomKey, resolveAgentAuth, resolveSubscriptionMnemomKey, sayRetiredConsoleNotice, } from "../lib/subscription-login.js";
|
|
46
46
|
import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
|
|
47
47
|
import { IdentityRouteMissingError, resolveServedIdentity, resolveServedIdentityDetail, } from "../lib/agent-identity-route.js";
|
|
48
48
|
import { readConfirmedAgent, readServedAgent, servedFingerprint, writeConfirmedAgent, writeServedAgent, } from "../lib/served-agent-cache.js";
|
|
@@ -964,9 +964,13 @@ async function publishDefaultCards(agentId, modes, opts = {}) {
|
|
|
964
964
|
}
|
|
965
965
|
}
|
|
966
966
|
/**
|
|
967
|
-
* `mnemom agent setup [--agent <slug|mnm-id>] [--governance …]` — the explicit
|
|
968
|
-
* point. An id (`--agent mnm-<uuid>`, or launcher.toml `agent_id`) refreshes
|
|
969
|
-
* cards and never claims or births; a malformed `mnm-…` refuses.
|
|
967
|
+
* `mnemom agent setup [--agent <slug|mnm-id>] [--governance …] [--auth …]` — the explicit
|
|
968
|
+
* provisioning entry point. An id (`--agent mnm-<uuid>`, or launcher.toml `agent_id`) refreshes
|
|
969
|
+
* that pinned agent's cards and never claims or births; a malformed `mnm-…` refuses.
|
|
970
|
+
*
|
|
971
|
+
* The auth mode resolves as a launch's does (`--auth`, else MNEMOM_AGENT_AUTH, which launcher.toml
|
|
972
|
+
* `auth` folds into, else api-key). A subscription setup asks for no Anthropic key: the identity
|
|
973
|
+
* anchors on the Mnemom API key, exactly as the subscription launch it provisions for (MNE-9282).
|
|
970
974
|
*/
|
|
971
975
|
export async function agentSetupCommand(opts = {}) {
|
|
972
976
|
const rawRef = opts.agent?.trim() || opts.agentId?.trim() || "";
|
|
@@ -974,6 +978,9 @@ export async function agentSetupCommand(opts = {}) {
|
|
|
974
978
|
if (ref?.kind === "malformed-id") {
|
|
975
979
|
throw new Error(malformedAgentIdMessage(ref.raw, opts.agent?.trim() ? "--agent" : "agent_id").replace("mnemom agent:", "mnemom agent setup:"));
|
|
976
980
|
}
|
|
981
|
+
// Validated up front, as a launch does, so a bad --auth fails before any sign-in.
|
|
982
|
+
const auth = resolveAgentAuth(opts.auth, process.env);
|
|
983
|
+
sayRetiredConsoleNotice(opts.auth, process.env, say);
|
|
977
984
|
const gatewayHost = resolveGatewayHost(process.env);
|
|
978
985
|
const cellError = singleCellError({
|
|
979
986
|
door: resolveAnthropicDoor(process.env),
|
|
@@ -993,13 +1000,23 @@ export async function agentSetupCommand(opts = {}) {
|
|
|
993
1000
|
process.exit(1); // guidance already printed
|
|
994
1001
|
throw err;
|
|
995
1002
|
}
|
|
996
|
-
|
|
1003
|
+
// The credentials resolve as the launch's do (agentCommand): a subscription launch has no
|
|
1004
|
+
// Anthropic key and anchors on a Mnemom key (minted once if there is none); an api-key one
|
|
1005
|
+
// sends this machine's existing Mnemom key beside its Anthropic key, minting only for a pin.
|
|
1006
|
+
const subscription = auth === "subscription";
|
|
1007
|
+
const anthropicKey = subscription
|
|
1008
|
+
? undefined
|
|
1009
|
+
: await resolveAnthropicKey(process.env, { allowPrompt: true });
|
|
1010
|
+
const subscriptionKey = subscription
|
|
1011
|
+
? await resolveSubscriptionMnemomKey(defaultMnemomKeyDeps())
|
|
1012
|
+
: undefined;
|
|
997
1013
|
if (ref?.kind === "id") {
|
|
998
1014
|
const pinned = await resolvePinnedAgent(ref.id);
|
|
999
1015
|
await ensureClaimedServedAgent({
|
|
1000
1016
|
slug: pinned.slug,
|
|
1001
1017
|
anthropicKey,
|
|
1002
|
-
mnemomKey:
|
|
1018
|
+
mnemomKey: subscriptionKey ??
|
|
1019
|
+
(await resolveSubscriptionMnemomKey(defaultMnemomKeyDeps(), "pinning an agent")),
|
|
1003
1020
|
pin: pinned.id,
|
|
1004
1021
|
gatewayHost,
|
|
1005
1022
|
orgId: account.orgId,
|
|
@@ -1017,8 +1034,9 @@ export async function agentSetupCommand(opts = {}) {
|
|
|
1017
1034
|
await ensureClaimedServedAgent({
|
|
1018
1035
|
slug,
|
|
1019
1036
|
anthropicKey,
|
|
1020
|
-
// Asked the way a launch asks: with this machine's Mnemom key when it has
|
|
1021
|
-
|
|
1037
|
+
// Asked the way a launch asks: an api-key launch with this machine's Mnemom key when it has
|
|
1038
|
+
// one (never minted).
|
|
1039
|
+
mnemomKey: subscriptionKey ?? readMnemomKey(defaultMnemomKeyDeps()),
|
|
1022
1040
|
gatewayHost,
|
|
1023
1041
|
orgId: account.orgId,
|
|
1024
1042
|
orgLabel: account.orgLabel,
|
|
@@ -1182,6 +1200,11 @@ export async function ensureSessionAndBalance(opts) {
|
|
|
1182
1200
|
const interactive = opts.interactive ?? isInteractive();
|
|
1183
1201
|
const haveSession = await ensureMnemomSession({ interactive });
|
|
1184
1202
|
if (!haveSession) {
|
|
1203
|
+
// A stored session whose refresh was rejected needs a fresh login, not an
|
|
1204
|
+
// account: the generic message below sent signed-in users to the waitlist.
|
|
1205
|
+
if (hasExpiredStoredSession()) {
|
|
1206
|
+
throw new Error("Your Mnemom session expired. Run `mnemom login` to sign in again (or set MNEMOM_API_KEY).");
|
|
1207
|
+
}
|
|
1185
1208
|
throw new Error("mnemom agent needs a Mnemom account — it runs your coding session governed and on your org's µ balance.\n" +
|
|
1186
1209
|
" Sign in: mnemom login (or set MNEMOM_API_KEY)\n" +
|
|
1187
1210
|
" Invited? mnemom agent --invite <token> (signs you in and unlocks Mnemom Agent)\n" +
|
|
@@ -17,4 +17,6 @@ export declare const FIXTURE_CARD_VALID: ShareCardLike;
|
|
|
17
17
|
export declare const FIXTURE_CARD_PERIOD_REVERT: ShareCardLike;
|
|
18
18
|
/** Real candidate lines plus two that must be refused. */
|
|
19
19
|
export declare const FIXTURE_CANDIDATES: CandidateLine[];
|
|
20
|
-
|
|
20
|
+
/** Public half of the aip TEST key (seed 0x2a) that signed the fixture cards. Test only. */
|
|
21
|
+
export declare const FIXTURE_TEST_PUBLIC_KEY = "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61";
|
|
22
|
+
export declare const fixtureAgentShareParts: Pick<AgentShareDeps, "source" | "issuer" | "renderer">;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* the refusal path can be shown. This is not a real issuer: a production source and signer
|
|
11
11
|
* are a separate, human-gated change.
|
|
12
12
|
*/
|
|
13
|
+
import { aipLineRenderer } from "./agent-share-render.js";
|
|
13
14
|
export const AGENT_SHARE_FIXTURE_ENV = "MNEMOM_AGENT_SHARE_FIXTURE";
|
|
14
15
|
export function agentShareFixtureEnabled(env = process.env) {
|
|
15
16
|
return env.MNEMOM_AGENT_SHARE === "1" && env[AGENT_SHARE_FIXTURE_ENV] === "1";
|
|
@@ -25,11 +26,11 @@ export const FIXTURE_CARD_VALID = {
|
|
|
25
26
|
signing_key_id: "test-sharecard-key-1",
|
|
26
27
|
statements: [
|
|
27
28
|
{
|
|
28
|
-
typ: "AAP-
|
|
29
|
+
typ: "AAP-ConductRecord/v1",
|
|
29
30
|
hash: "1111111111111111111111111111111111111111111111111111111111111111",
|
|
30
31
|
},
|
|
31
32
|
{
|
|
32
|
-
typ: "AAP-
|
|
33
|
+
typ: "AAP-ConductRecord/v1",
|
|
33
34
|
hash: "2222222222222222222222222222222222222222222222222222222222222222",
|
|
34
35
|
},
|
|
35
36
|
],
|
|
@@ -49,7 +50,7 @@ export const FIXTURE_CARD_VALID = {
|
|
|
49
50
|
statement_hash: "2222222222222222222222222222222222222222222222222222222222222222",
|
|
50
51
|
},
|
|
51
52
|
],
|
|
52
|
-
signature: "
|
|
53
|
+
signature: "PCa6dhIMUJrJoA9A_v50FTXWxmRSADYs1F_iZWWJrJiujUbsxPkadHQhS8cQvug0ChRQpkOrtNkQJ48opcK-Cg",
|
|
53
54
|
};
|
|
54
55
|
export const FIXTURE_CARD_PERIOD_REVERT = {
|
|
55
56
|
typ: "AAP-ShareCard/v1",
|
|
@@ -62,15 +63,15 @@ export const FIXTURE_CARD_PERIOD_REVERT = {
|
|
|
62
63
|
signing_key_id: "test-sharecard-key-1",
|
|
63
64
|
statements: [
|
|
64
65
|
{
|
|
65
|
-
typ: "AAP-
|
|
66
|
+
typ: "AAP-ConductRecord/v1",
|
|
66
67
|
hash: "1111111111111111111111111111111111111111111111111111111111111111",
|
|
67
68
|
},
|
|
68
69
|
{
|
|
69
|
-
typ: "AAP-
|
|
70
|
+
typ: "AAP-ConductRecord/v1",
|
|
70
71
|
hash: "2222222222222222222222222222222222222222222222222222222222222222",
|
|
71
72
|
},
|
|
72
73
|
{
|
|
73
|
-
typ: "AAP-
|
|
74
|
+
typ: "AAP-ConductRecord/v1",
|
|
74
75
|
hash: "3333333333333333333333333333333333333333333333333333333333333333",
|
|
75
76
|
},
|
|
76
77
|
],
|
|
@@ -97,7 +98,7 @@ export const FIXTURE_CARD_PERIOD_REVERT = {
|
|
|
97
98
|
statement_hash: "3333333333333333333333333333333333333333333333333333333333333333",
|
|
98
99
|
},
|
|
99
100
|
],
|
|
100
|
-
signature: "
|
|
101
|
+
signature: "0aUrWIfIYKlp2H40Ovh4AYE6MgWokOrjfLjd7Wm9-tT0UHTEvPvo4a5lAy-WQ5iRBUI9JJncNJS9zpVPY2X6DA",
|
|
101
102
|
};
|
|
102
103
|
const FIXTURE_CARDS = [FIXTURE_CARD_VALID, FIXTURE_CARD_PERIOD_REVERT];
|
|
103
104
|
const HASH_E = "e".repeat(64);
|
|
@@ -120,7 +121,10 @@ const same = (a, b) => a.length === b.length &&
|
|
|
120
121
|
const m = b.find((x) => x.id === l.id);
|
|
121
122
|
return m !== undefined && JSON.stringify(m) === JSON.stringify(l);
|
|
122
123
|
});
|
|
124
|
+
/** Public half of the aip TEST key (seed 0x2a) that signed the fixture cards. Test only. */
|
|
125
|
+
export const FIXTURE_TEST_PUBLIC_KEY = "197f6b23e16c8532c6abc838facd5ea789be0c76b2920334039bfa8b3d368d61";
|
|
123
126
|
export const fixtureAgentShareParts = {
|
|
127
|
+
renderer: aipLineRenderer(FIXTURE_TEST_PUBLIC_KEY),
|
|
124
128
|
source: { listLines: async () => FIXTURE_CANDIDATES },
|
|
125
129
|
issuer: {
|
|
126
130
|
issue: async (lines) => {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge from `mnemom agent share` to the aip verifier (`@mnemom/aip-verifier`, aip#192/#178):
|
|
3
|
+
* the renderer (`renderLines`) and claim-lint (`lintRendering`) are the ONLY source of what a
|
|
4
|
+
* card may say. No word lists live here. The package is a devDependency pinned to the aip
|
|
5
|
+
* release tarball and is loaded lazily, so the default CLI path never touches it; if it is
|
|
6
|
+
* absent at runtime the share command fails closed with a clear message.
|
|
7
|
+
*/
|
|
8
|
+
import type { LineRenderer, SharedCardLine } from "./agent-share.js";
|
|
9
|
+
import type * as AipMain from "@mnemom/aip-verifier";
|
|
10
|
+
import type * as AipRender from "@mnemom/aip-verifier/render-lines";
|
|
11
|
+
export interface VerifierModules {
|
|
12
|
+
lintRendering: typeof AipMain.lintRendering;
|
|
13
|
+
renderLines: typeof AipRender.renderLines;
|
|
14
|
+
}
|
|
15
|
+
/** Loader seam: tests swap it to simulate a missing or broken package. */
|
|
16
|
+
export type VerifierLoader = () => Promise<VerifierModules>;
|
|
17
|
+
/**
|
|
18
|
+
* `publicKey` is the trusted Ed25519 key (hex) that must have signed the card. Without one the
|
|
19
|
+
* renderer fails closed (missing_key), so an unsigned or foreign-signed card is never linked.
|
|
20
|
+
*/
|
|
21
|
+
export declare function aipLineRenderer(publicKey: string | undefined, load?: VerifierLoader): LineRenderer;
|
|
22
|
+
/** claim-lint one candidate line as the share card would show it; a reason when it fails. */
|
|
23
|
+
export declare function aipLineScreen(line: SharedCardLine, load?: VerifierLoader): Promise<string | null>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge from `mnemom agent share` to the aip verifier (`@mnemom/aip-verifier`, aip#192/#178):
|
|
3
|
+
* the renderer (`renderLines`) and claim-lint (`lintRendering`) are the ONLY source of what a
|
|
4
|
+
* card may say. No word lists live here. The package is a devDependency pinned to the aip
|
|
5
|
+
* release tarball and is loaded lazily, so the default CLI path never touches it; if it is
|
|
6
|
+
* absent at runtime the share command fails closed with a clear message.
|
|
7
|
+
*/
|
|
8
|
+
const defaultLoader = async () => {
|
|
9
|
+
const [main, render] = await Promise.all([
|
|
10
|
+
import("@mnemom/aip-verifier"),
|
|
11
|
+
import("@mnemom/aip-verifier/render-lines"),
|
|
12
|
+
]);
|
|
13
|
+
return { lintRendering: main.lintRendering, renderLines: render.renderLines };
|
|
14
|
+
};
|
|
15
|
+
/** Keep the real cause: "not installed" only for a genuinely missing module. */
|
|
16
|
+
async function loadVerifier(load) {
|
|
17
|
+
try {
|
|
18
|
+
return await load();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const e = error;
|
|
22
|
+
if (e.code === "ERR_MODULE_NOT_FOUND" || e.code === "MODULE_NOT_FOUND") {
|
|
23
|
+
throw new Error("the aip verifier (@mnemom/aip-verifier) is not installed: run `npm install https://github.com/mnemom/aip/releases/download/aip-verifier-v0.6.0/mnemom-aip-verifier-0.6.0.tgz`", { cause: error });
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`the aip verifier (@mnemom/aip-verifier) failed to load: ${e.message ?? error}`, { cause: error });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `publicKey` is the trusted Ed25519 key (hex) that must have signed the card. Without one the
|
|
30
|
+
* renderer fails closed (missing_key), so an unsigned or foreign-signed card is never linked.
|
|
31
|
+
*/
|
|
32
|
+
export function aipLineRenderer(publicKey, load = defaultLoader) {
|
|
33
|
+
return {
|
|
34
|
+
async renderLines(card, opts) {
|
|
35
|
+
const { renderLines } = await loadVerifier(load);
|
|
36
|
+
const r = await renderLines(card, {
|
|
37
|
+
enabled: true,
|
|
38
|
+
surface: opts?.period ? "period_card" : "share_card",
|
|
39
|
+
...(publicKey ? { publicKey } : {}),
|
|
40
|
+
});
|
|
41
|
+
if (!r.ok)
|
|
42
|
+
throw new Error(`the card did not render (${r.reasons.join(", ")})`);
|
|
43
|
+
return r.lines.map((l) => ({
|
|
44
|
+
text: l.text,
|
|
45
|
+
kind: l.class,
|
|
46
|
+
...(l.coverage ? { coverage: `${l.coverage.judged} of ${l.coverage.total}` } : {}),
|
|
47
|
+
}));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** claim-lint one candidate line as the share card would show it; a reason when it fails. */
|
|
52
|
+
export async function aipLineScreen(line, load = defaultLoader) {
|
|
53
|
+
let v;
|
|
54
|
+
try {
|
|
55
|
+
v = await loadVerifier(load);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return error.message;
|
|
59
|
+
}
|
|
60
|
+
const text = `${line.label}: ${line.value}`;
|
|
61
|
+
let res;
|
|
62
|
+
try {
|
|
63
|
+
res = v.lintRendering({
|
|
64
|
+
surface: "share_card",
|
|
65
|
+
lines: [{ class: line.kind === "revert" ? "outcome" : "measured", text }],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return `claim-lint could not run (${error.message})`;
|
|
70
|
+
}
|
|
71
|
+
return res.ok ? null : `claim-lint rejects it (${res.violations.map((x) => x.rule).join(", ")})`;
|
|
72
|
+
}
|