@mnemom/mnemom 0.17.35 → 0.17.37
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 +4 -0
- package/dist/commands/agent-run.js +9 -1
- package/dist/commands/agent-share-render.js +1 -1
- package/dist/commands/scan.d.ts +13 -3
- package/dist/commands/scan.js +48 -10
- package/dist/host/commands.js +4 -1
- package/dist/host/owner.d.ts +2 -0
- package/dist/host/verbs.d.ts +16 -1
- package/dist/host/verbs.js +45 -3
- package/dist/index.js +3 -2
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.js +21 -0
- package/dist/lib/scan/pr-source.js +2 -0
- package/dist/lib/scan/repo-picker.d.ts +32 -0
- package/dist/lib/scan/repo-picker.js +80 -0
- package/dist/vendor/durable-contract/index.js +6 -3
- package/dist/vendor/shared/durable-contract/cli-surface.js +2 -2
- package/dist/vendor/shared/durable-contract/codes.js +4 -1
- package/node_modules/@mnemom/aip-verifier/LICENSE +190 -0
- package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.cts +478 -0
- package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.ts +478 -0
- package/node_modules/@mnemom/aip-verifier/dist/index.cjs +971 -0
- package/node_modules/@mnemom/aip-verifier/dist/index.cjs.map +1 -0
- package/node_modules/@mnemom/aip-verifier/dist/index.d.cts +330 -0
- package/node_modules/@mnemom/aip-verifier/dist/index.d.ts +330 -0
- package/node_modules/@mnemom/aip-verifier/dist/index.js +933 -0
- package/node_modules/@mnemom/aip-verifier/dist/index.js.map +1 -0
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs +879 -0
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs.map +1 -0
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.cts +96 -0
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.ts +96 -0
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.js +875 -0
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.js.map +1 -0
- package/node_modules/@mnemom/aip-verifier/package.json +86 -0
- package/node_modules/@noble/ed25519/LICENSE +21 -0
- package/node_modules/@noble/ed25519/README.md +371 -0
- package/node_modules/@noble/ed25519/index.d.ts +391 -0
- package/node_modules/@noble/ed25519/index.js +933 -0
- package/node_modules/@noble/ed25519/index.ts +1134 -0
- package/node_modules/@noble/ed25519/package.json +54 -0
- package/node_modules/@noble/hashes/LICENSE +21 -0
- package/node_modules/@noble/hashes/README.md +478 -0
- package/node_modules/@noble/hashes/_blake.d.ts +20 -0
- package/node_modules/@noble/hashes/_blake.js +59 -0
- package/node_modules/@noble/hashes/_md.d.ts +95 -0
- package/node_modules/@noble/hashes/_md.js +209 -0
- package/node_modules/@noble/hashes/_u64.d.ts +36 -0
- package/node_modules/@noble/hashes/_u64.js +77 -0
- package/node_modules/@noble/hashes/argon2.d.ts +148 -0
- package/node_modules/@noble/hashes/argon2.js +577 -0
- package/node_modules/@noble/hashes/blake1.d.ts +162 -0
- package/node_modules/@noble/hashes/blake1.js +630 -0
- package/node_modules/@noble/hashes/blake2.d.ts +203 -0
- package/node_modules/@noble/hashes/blake2.js +573 -0
- package/node_modules/@noble/hashes/blake3.d.ts +68 -0
- package/node_modules/@noble/hashes/blake3.js +310 -0
- package/node_modules/@noble/hashes/eskdf.d.ts +91 -0
- package/node_modules/@noble/hashes/eskdf.js +225 -0
- package/node_modules/@noble/hashes/hkdf.d.ts +70 -0
- package/node_modules/@noble/hashes/hkdf.js +152 -0
- package/node_modules/@noble/hashes/hmac.d.ts +50 -0
- package/node_modules/@noble/hashes/hmac.js +93 -0
- package/node_modules/@noble/hashes/index.d.ts +1 -0
- package/node_modules/@noble/hashes/index.js +34 -0
- package/node_modules/@noble/hashes/legacy.d.ts +107 -0
- package/node_modules/@noble/hashes/legacy.js +348 -0
- package/node_modules/@noble/hashes/package.json +91 -0
- package/node_modules/@noble/hashes/pbkdf2.d.ts +66 -0
- package/node_modules/@noble/hashes/pbkdf2.js +176 -0
- package/node_modules/@noble/hashes/scrypt.d.ts +98 -0
- package/node_modules/@noble/hashes/scrypt.js +304 -0
- package/node_modules/@noble/hashes/sha2.d.ts +176 -0
- package/node_modules/@noble/hashes/sha2.js +446 -0
- package/node_modules/@noble/hashes/sha3-addons.d.ts +533 -0
- package/node_modules/@noble/hashes/sha3-addons.js +973 -0
- package/node_modules/@noble/hashes/sha3.d.ts +208 -0
- package/node_modules/@noble/hashes/sha3.js +475 -0
- package/node_modules/@noble/hashes/src/_blake.ts +67 -0
- package/node_modules/@noble/hashes/src/_md.ts +227 -0
- package/node_modules/@noble/hashes/src/_u64.ts +111 -0
- package/node_modules/@noble/hashes/src/argon2.ts +693 -0
- package/node_modules/@noble/hashes/src/blake1.ts +677 -0
- package/node_modules/@noble/hashes/src/blake2.ts +648 -0
- package/node_modules/@noble/hashes/src/blake3.ts +344 -0
- package/node_modules/@noble/hashes/src/eskdf.ts +265 -0
- package/node_modules/@noble/hashes/src/hkdf.ts +161 -0
- package/node_modules/@noble/hashes/src/hmac.ts +130 -0
- package/node_modules/@noble/hashes/src/index.ts +33 -0
- package/node_modules/@noble/hashes/src/legacy.ts +361 -0
- package/node_modules/@noble/hashes/src/pbkdf2.ts +219 -0
- package/node_modules/@noble/hashes/src/scrypt.ts +375 -0
- package/node_modules/@noble/hashes/src/sha2.ts +486 -0
- package/node_modules/@noble/hashes/src/sha3-addons.ts +1157 -0
- package/node_modules/@noble/hashes/src/sha3.ts +525 -0
- package/node_modules/@noble/hashes/src/utils.ts +978 -0
- package/node_modules/@noble/hashes/src/webcrypto.ts +283 -0
- package/node_modules/@noble/hashes/utils.d.ts +560 -0
- package/node_modules/@noble/hashes/utils.js +687 -0
- package/node_modules/@noble/hashes/webcrypto.d.ts +121 -0
- package/node_modules/@noble/hashes/webcrypto.js +228 -0
- package/package.json +5 -2
|
@@ -24,6 +24,8 @@ export interface RunDeps {
|
|
|
24
24
|
env: NodeJS.ProcessEnv;
|
|
25
25
|
fetch: FetchFn;
|
|
26
26
|
apiUrl: string;
|
|
27
|
+
/** The gateway the model proxy forwards to. */
|
|
28
|
+
gatewayUrl: string;
|
|
27
29
|
/** stdout: one outcome line per job. */
|
|
28
30
|
emit(line: string): void;
|
|
29
31
|
/** stderr. */
|
|
@@ -36,6 +38,8 @@ export interface RunDeps {
|
|
|
36
38
|
}
|
|
37
39
|
/** The refusal of a runner with no ANTHROPIC_API_KEY (34.30.1); a config error, exit 2. */
|
|
38
40
|
export declare const RUNNER_MODEL_KEY_MISSING = "runner_model_key_missing";
|
|
41
|
+
/** The refusal of a non-production API paired with the production gateway; a config error, exit 2. */
|
|
42
|
+
export declare const RUNNER_GATEWAY_RING_MISMATCH = "runner_gateway_ring_mismatch";
|
|
39
43
|
/** Resolves the process exit code; never throws for a usage, config or Mnemom error. */
|
|
40
44
|
export declare function agentRunCommand(flags: RunFlags, deps: RunDeps): Promise<number>;
|
|
41
45
|
export declare const GITHUB_API: string;
|
|
@@ -20,7 +20,7 @@ import { randomUUID } from "node:crypto";
|
|
|
20
20
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
21
21
|
import { homedir, tmpdir } from "node:os";
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
-
import { getApiUrl, getGatewayUrl } from "../lib/config.js";
|
|
23
|
+
import { gatewayRingMismatch, getApiUrl, getGatewayUrl } from "../lib/config.js";
|
|
24
24
|
import { runnerApi } from "../lib/runner/executor-client.js";
|
|
25
25
|
import { resolveClaudeBin } from "../lib/claude-bin.js";
|
|
26
26
|
import { executorIdFromState } from "../lib/runner/executor-state.js";
|
|
@@ -35,6 +35,8 @@ import { resolveGithubToken } from "./agent-fuel.js";
|
|
|
35
35
|
import { executorStatePath } from "../vendor/durable-contract/index.js";
|
|
36
36
|
/** The refusal of a runner with no ANTHROPIC_API_KEY (34.30.1); a config error, exit 2. */
|
|
37
37
|
export const RUNNER_MODEL_KEY_MISSING = "runner_model_key_missing";
|
|
38
|
+
/** The refusal of a non-production API paired with the production gateway; a config error, exit 2. */
|
|
39
|
+
export const RUNNER_GATEWAY_RING_MISMATCH = "runner_gateway_ring_mismatch";
|
|
38
40
|
/** Resolves the process exit code; never throws for a usage, config or Mnemom error. */
|
|
39
41
|
export async function agentRunCommand(flags, deps) {
|
|
40
42
|
const resolved = resolveRunPlan(flags, deps.env);
|
|
@@ -45,6 +47,11 @@ export async function agentRunCommand(flags, deps) {
|
|
|
45
47
|
const plan = resolved.plan;
|
|
46
48
|
for (const w of plan.warnings)
|
|
47
49
|
deps.err(w);
|
|
50
|
+
if (gatewayRingMismatch(deps.apiUrl, deps.gatewayUrl)) {
|
|
51
|
+
deps.err(`mnemom agent run: refused (${RUNNER_GATEWAY_RING_MISMATCH}): the API is ${deps.apiUrl} but the gateway is the production one, ${deps.gatewayUrl}; nothing was claimed`);
|
|
52
|
+
deps.err(" fix: set MNEMOM_GATEWAY_URL to the same cell's gateway as MNEMOM_API_URL");
|
|
53
|
+
return USAGE_EXIT_CODE;
|
|
54
|
+
}
|
|
48
55
|
const modelKey = deps.env.ANTHROPIC_API_KEY?.trim();
|
|
49
56
|
if (!modelKey) {
|
|
50
57
|
deps.err(`mnemom agent run: refused (${RUNNER_MODEL_KEY_MISSING}): this runner has no ANTHROPIC_API_KEY for its phases' model calls; nothing was claimed`);
|
|
@@ -247,6 +254,7 @@ export function runAgentRun(flags) {
|
|
|
247
254
|
env: process.env,
|
|
248
255
|
fetch,
|
|
249
256
|
apiUrl: getApiUrl(),
|
|
257
|
+
gatewayUrl: getGatewayUrl(),
|
|
250
258
|
emit: (line) => process.stdout.write(`${line}\n`),
|
|
251
259
|
err: (line) => process.stderr.write(`${line}\n`),
|
|
252
260
|
stop: stop.signal,
|
|
@@ -20,7 +20,7 @@ async function loadVerifier(load) {
|
|
|
20
20
|
catch (error) {
|
|
21
21
|
const e = error;
|
|
22
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:
|
|
23
|
+
throw new Error("the aip verifier (@mnemom/aip-verifier) is not installed: it ships bundled inside @mnemom/mnemom, so reinstall with `npm install -g @mnemom/mnemom`", { cause: error });
|
|
24
24
|
}
|
|
25
25
|
throw new Error(`the aip verifier (@mnemom/aip-verifier) failed to load: ${e.message ?? error}`, { cause: error });
|
|
26
26
|
}
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { type AnthropicUsageDeps } from "../lib/scan/spend/anthropic-usage.js";
|
|
|
11
11
|
import { type RecordDeps, type RecordOutcome } from "../lib/scan/record.js";
|
|
12
12
|
import { type CostSpend } from "../lib/scan/cost-per-pr.js";
|
|
13
13
|
import { type ReportEmailDeps } from "../lib/scan/report-email.js";
|
|
14
|
+
import { type RepoListDeps } from "../lib/scan/repo-picker.js";
|
|
14
15
|
import { type ReportRecipients } from "../lib/scan/share.js";
|
|
15
16
|
export interface ScanCommandOptions {
|
|
16
17
|
repos: string[];
|
|
@@ -42,12 +43,20 @@ export interface ScanCommandOptions {
|
|
|
42
43
|
recordDeps?: RecordDeps;
|
|
43
44
|
/** Test seam: the report-email POST. */
|
|
44
45
|
emailDeps?: ReportEmailDeps;
|
|
46
|
+
/** Test seam: the org this login was authorized for (none of the stored tokens carry one today). */
|
|
47
|
+
authorizedOrg?: () => string | undefined;
|
|
48
|
+
/** Test seam: the `mnemom org use` default. */
|
|
49
|
+
activeOrg?: () => {
|
|
50
|
+
org_id: string;
|
|
51
|
+
} | undefined;
|
|
45
52
|
/** Test seam: the signed-in user's orgs, used to infer --org when it is omitted. */
|
|
46
53
|
listOrgs?: () => Promise<Array<{
|
|
47
54
|
org_id: string;
|
|
48
55
|
name?: string;
|
|
49
56
|
is_personal?: boolean;
|
|
50
57
|
}>>;
|
|
58
|
+
/** Test seam: the fetch behind the repo picker's list read. */
|
|
59
|
+
repoListDeps?: RepoListDeps;
|
|
51
60
|
/** Test seams. */
|
|
52
61
|
isTty?: boolean;
|
|
53
62
|
env?: NodeJS.ProcessEnv;
|
|
@@ -93,9 +102,10 @@ export declare function resolveMeasuredUsage(o: ScanCommandOptions, period: {
|
|
|
93
102
|
} | null>;
|
|
94
103
|
/**
|
|
95
104
|
* --org is optional only because it can be inferred: the report is emailed on every scan, so the scan
|
|
96
|
-
* needs an org.
|
|
97
|
-
*
|
|
98
|
-
* scan work, naming
|
|
105
|
+
* needs an org. Order: --org; the org the login was authorized for; the `mnemom org use` default; the
|
|
106
|
+
* signed-in user's single non-personal membership (GET /v1/orgs); a terminal picker over several; else it
|
|
107
|
+
* stops loudly BEFORE any scan work, naming each org beside its id. It never guesses between orgs and
|
|
108
|
+
* never skips the email.
|
|
99
109
|
*/
|
|
100
110
|
export declare function resolveScanOrg(o: ScanCommandOptions): Promise<string>;
|
|
101
111
|
export declare const INVALID_ORG_MSG = "--org must be an org id like org-1a2b3c4d.";
|
package/dist/commands/scan.js
CHANGED
|
@@ -12,10 +12,12 @@ import { isSpendTiersEnabled, NOT_ENABLED_MSG, parseMonthlySpendUsd, selectSpend
|
|
|
12
12
|
import { ADMIN_KEY_ENV, AnthropicUsageError, fetchMeasuredUsage, isAdminKeyShape, usageFailureText, } from "../lib/scan/spend/anthropic-usage.js";
|
|
13
13
|
import { getAuthInfo } from "../lib/auth.js";
|
|
14
14
|
import { listMyOrgs } from "../lib/api.js";
|
|
15
|
+
import { getActiveOrg } from "../lib/cli-config.js";
|
|
15
16
|
import { fetchRecordTotals, isOrgId, } from "../lib/scan/record.js";
|
|
16
17
|
import { modelledSpend } from "../lib/scan/cost-per-pr.js";
|
|
17
18
|
import { RECORD_NO_ORG_NOTE, renderRecordSection } from "../lib/scan/report.js";
|
|
18
19
|
import { buildEmailBody, buildEmailReport, DRY_RUN_MSG, UNAVAILABLE_MSG, failureMessage, ReportEmailError, sendReportEmail, } from "../lib/scan/report-email.js";
|
|
20
|
+
import { pickRepos, REPO_PICKER_NO_TERMINAL_HINT, } from "../lib/scan/repo-picker.js";
|
|
19
21
|
import { buildSharePayload, parseShareLevel, promptShareLevel, } from "../lib/scan/share.js";
|
|
20
22
|
/** Parses repeated/comma-separated `--repo owner/name`. Returns null on any invalid slug. */
|
|
21
23
|
export function parseRepoFlags(raw) {
|
|
@@ -126,9 +128,10 @@ export async function resolveMeasuredUsage(o, period) {
|
|
|
126
128
|
}
|
|
127
129
|
/**
|
|
128
130
|
* --org is optional only because it can be inferred: the report is emailed on every scan, so the scan
|
|
129
|
-
* needs an org.
|
|
130
|
-
*
|
|
131
|
-
* scan work, naming
|
|
131
|
+
* needs an org. Order: --org; the org the login was authorized for; the `mnemom org use` default; the
|
|
132
|
+
* signed-in user's single non-personal membership (GET /v1/orgs); a terminal picker over several; else it
|
|
133
|
+
* stops loudly BEFORE any scan work, naming each org beside its id. It never guesses between orgs and
|
|
134
|
+
* never skips the email.
|
|
132
135
|
*/
|
|
133
136
|
export async function resolveScanOrg(o) {
|
|
134
137
|
if (o.org !== undefined) {
|
|
@@ -136,6 +139,18 @@ export async function resolveScanOrg(o) {
|
|
|
136
139
|
throw new Error(INVALID_ORG_MSG);
|
|
137
140
|
return o.org;
|
|
138
141
|
}
|
|
142
|
+
// 2. The org this login was authorized for, when the token or session carries one.
|
|
143
|
+
const authorized = o.authorizedOrg?.();
|
|
144
|
+
if (authorized !== undefined) {
|
|
145
|
+
if (!isOrgId(authorized))
|
|
146
|
+
throw new Error(INVALID_ORG_MSG);
|
|
147
|
+
return authorized;
|
|
148
|
+
}
|
|
149
|
+
// 3. The default set with `mnemom org use <slug>` (a malformed stored id is ignored, not trusted).
|
|
150
|
+
const active = (o.activeOrg ?? getActiveOrg)()?.org_id;
|
|
151
|
+
if (active !== undefined && isOrgId(active))
|
|
152
|
+
return active;
|
|
153
|
+
// 4-6. Memberships: a single one, else a terminal picker, else fail closed.
|
|
139
154
|
let orgs;
|
|
140
155
|
try {
|
|
141
156
|
orgs = (await (o.listOrgs ?? listMyOrgs)()).filter((x) => !x.is_personal);
|
|
@@ -147,8 +162,20 @@ export async function resolveScanOrg(o) {
|
|
|
147
162
|
return orgs[0].org_id;
|
|
148
163
|
if (orgs.length === 0)
|
|
149
164
|
throw new Error("You belong to no organization, so the report has nowhere to be emailed from. Pass --org <org_id>.");
|
|
150
|
-
|
|
165
|
+
// Several orgs: ids alone mean nothing, so show each name beside its id. A terminal picks one; anything
|
|
166
|
+
// else fails closed (never a guess) with the same labels in the message.
|
|
167
|
+
const labels = orgs.map(orgLabel);
|
|
168
|
+
if (o.isTty ?? process.stdin.isTTY) {
|
|
169
|
+
const chosen = await askSelect("Which organization should the report be emailed for?", labels);
|
|
170
|
+
const hit = orgs.find((x, i) => labels[i] === chosen);
|
|
171
|
+
if (hit && isOrgId(hit.org_id))
|
|
172
|
+
return hit.org_id;
|
|
173
|
+
throw new Error("No organization selected. Pass --org <org_id>.");
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`You belong to ${orgs.length} organizations; pass --org <org_id> (${labels.join(", ")}).`);
|
|
151
176
|
}
|
|
177
|
+
/** "Name (org-id)", or just the id when the org has no usable name. */
|
|
178
|
+
const orgLabel = (x) => typeof x.name === "string" && x.name.trim() !== "" ? `${x.name.trim()} (${x.org_id})` : x.org_id;
|
|
152
179
|
export const INVALID_ORG_MSG = "--org must be an org id like org-1a2b3c4d.";
|
|
153
180
|
/**
|
|
154
181
|
* The Record read for the preview: null when the flag gate is off (no read, no output, as before).
|
|
@@ -168,23 +195,34 @@ export async function scanCommand(o) {
|
|
|
168
195
|
if (o.share !== undefined && parseShareLevel(o.share) === null) {
|
|
169
196
|
throw new Error("--share must be one of: summary, full, none");
|
|
170
197
|
}
|
|
171
|
-
|
|
172
|
-
|
|
198
|
+
const noRepoFlag = o.repos.length === 0;
|
|
199
|
+
// No --repo: on a terminal pick from the repos this GitHub login can read; otherwise the error stays, with a hint.
|
|
200
|
+
if (noRepoFlag && !(o.isTty ?? process.stdin.isTTY))
|
|
201
|
+
throw new Error(`--repo owner/name is required (repeatable). ${REPO_PICKER_NO_TERMINAL_HINT}`);
|
|
173
202
|
// The report is emailed on every scan, so resolve the org before any scan work; failure is loud.
|
|
174
203
|
const emailOrg = await resolveScanOrg(o);
|
|
175
204
|
if (o.ctoEmail !== undefined && !isEmail(o.ctoEmail))
|
|
176
205
|
throw new Error("--cto-email must be a valid email address");
|
|
206
|
+
const getCredential = async () => {
|
|
207
|
+
const c = o.credential ?? (await resolveCredential().catch(() => null));
|
|
208
|
+
if (!c)
|
|
209
|
+
throw new Error(failureText("credential"));
|
|
210
|
+
return c;
|
|
211
|
+
};
|
|
212
|
+
// Asked before the spend prompts so the repo comes first; the credential is reused for the scan.
|
|
213
|
+
const pickedCredential = noRepoFlag ? await getCredential() : null;
|
|
214
|
+
const repos = pickedCredential
|
|
215
|
+
? await pickRepos(pickedCredential, { isTty: true, deps: o.repoListDeps })
|
|
216
|
+
: o.repos;
|
|
177
217
|
const selfReported = await resolveSelfReportedSpend(o);
|
|
178
218
|
const now = o.now ?? Date.now;
|
|
179
219
|
const end = new Date(now()).toISOString().slice(0, 10);
|
|
180
220
|
const period = o.period ?? { start: end, end };
|
|
181
221
|
const measured = await resolveMeasuredUsage(o, period);
|
|
182
|
-
const credential =
|
|
183
|
-
if (!credential)
|
|
184
|
-
throw new Error(failureText("credential"));
|
|
222
|
+
const credential = pickedCredential ?? (await getCredential());
|
|
185
223
|
// Before the local scan: a refused credential fails closed without spending the GitHub reads.
|
|
186
224
|
const record = await resolveRecordOutcome(o);
|
|
187
|
-
const outcome = await runLocalScan({ credential, now, ...o.deps }, { repos
|
|
225
|
+
const outcome = await runLocalScan({ credential, now, ...o.deps }, { repos, period });
|
|
188
226
|
if (outcome.status === "withheld")
|
|
189
227
|
throw new Error(`Scan withheld: ${failureText(outcome.reason)}`);
|
|
190
228
|
const { result } = outcome;
|
package/dist/host/commands.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as os from "node:os";
|
|
|
8
8
|
import { CLI_VERSION } from "../version.js";
|
|
9
9
|
import { getApiUrl } from "../lib/config.js";
|
|
10
10
|
import { getAccessToken } from "../lib/auth.js";
|
|
11
|
+
import { getActiveOrg } from "../lib/cli-config.js";
|
|
11
12
|
import { resolveBootId, defaultBootIdDeps } from "./bootid.js";
|
|
12
13
|
import { realLaunchctl } from "./launchd.js";
|
|
13
14
|
import { ownerApi, parseExecutorList } from "./owner.js";
|
|
@@ -37,6 +38,7 @@ function verbIo(json) {
|
|
|
37
38
|
bootId: () => resolveBootId({ ...defaultBootIdDeps(), env }).bootId,
|
|
38
39
|
programArguments: () => [...cliExec(), "agent", "host", "run"],
|
|
39
40
|
claudeBin: () => resolveClaudeBin(env),
|
|
41
|
+
activeOrg: () => getActiveOrg()?.org_id ?? null,
|
|
40
42
|
};
|
|
41
43
|
}
|
|
42
44
|
async function guarded(json, fn) {
|
|
@@ -66,7 +68,8 @@ export function registerHostCommands(agent, hidden) {
|
|
|
66
68
|
host
|
|
67
69
|
.command("enroll")
|
|
68
70
|
.description("Enroll this machine as an executor (device step); `install` does this when needed")
|
|
69
|
-
.
|
|
71
|
+
.option("--org <org_id>", "Join this organization (you must be a member); defaults to the active org, else your personal org")
|
|
72
|
+
.action(async (o) => guarded(false, (io) => hostEnroll(io, { org: o.org })));
|
|
70
73
|
host
|
|
71
74
|
.command("renew")
|
|
72
75
|
.description("Renew this machine's executor grant (device step)")
|
package/dist/host/owner.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface OwnerApi {
|
|
|
11
11
|
runtime: string;
|
|
12
12
|
contract_version: string;
|
|
13
13
|
machine_label?: string;
|
|
14
|
+
/** Ruling 34.03.4: the org this host joins; the api checks membership. */
|
|
15
|
+
org_id?: string;
|
|
14
16
|
}): Promise<OwnerHttp>;
|
|
15
17
|
enrollPoll(pollSecret: string, approvalId: string | null): Promise<OwnerHttp>;
|
|
16
18
|
list(): Promise<OwnerHttp>;
|
package/dist/host/verbs.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface VerbIo {
|
|
|
20
20
|
readonly pollSleep?: (ms: number) => Promise<void>;
|
|
21
21
|
/** Takes the enrollment lock; acquireHostLock (macOS flock) when absent. */
|
|
22
22
|
readonly acquireLock?: (lockFile: string) => LockResult;
|
|
23
|
+
/** The CLI's active org id (`mnemom org use`), or null with none set; never read when absent. */
|
|
24
|
+
readonly activeOrg?: () => string | null;
|
|
23
25
|
}
|
|
24
26
|
/** Fresh-invocation gate for every host verb (C12-R25): exit 2 with the gated text, nothing else. */
|
|
25
27
|
export declare function freshGate(io: VerbIo): number | null;
|
|
@@ -40,13 +42,26 @@ export declare function executorLiveness(io: VerbIo, executorId: string): Promis
|
|
|
40
42
|
export declare function printRiskLine(io: VerbIo): void;
|
|
41
43
|
/** "<org name> (<org_id>)", or the bare id when the api sent no name. */
|
|
42
44
|
export declare function joinTarget(orgId: string, orgName: string | null): string;
|
|
45
|
+
/**
|
|
46
|
+
* The org a start names (ruling 34.03.4): `--org`, else the active org, else none, so the api
|
|
47
|
+
* keeps its fallback (the token's org, else the personal org).
|
|
48
|
+
*/
|
|
49
|
+
export declare function enrollOrg(io: VerbIo, opts: {
|
|
50
|
+
org?: string;
|
|
51
|
+
}): {
|
|
52
|
+
orgId: string;
|
|
53
|
+
from: "flag" | "active";
|
|
54
|
+
} | null;
|
|
43
55
|
/** The device step: start, show the code, poll, write the state file. Returns an exit code. */
|
|
44
56
|
export declare function enrollDevice(io: VerbIo, opts?: {
|
|
45
57
|
label?: string;
|
|
58
|
+
org?: string;
|
|
46
59
|
}): Promise<number>;
|
|
47
60
|
/** Write the plist and (re)load the job. */
|
|
48
61
|
export declare function writeAndLoad(io: VerbIo): Promise<number>;
|
|
49
62
|
export declare function hostInstall(io: VerbIo): Promise<number>;
|
|
50
|
-
export declare function hostEnroll(io: VerbIo
|
|
63
|
+
export declare function hostEnroll(io: VerbIo, opts?: {
|
|
64
|
+
org?: string;
|
|
65
|
+
}): Promise<number>;
|
|
51
66
|
export declare function hostRenew(io: VerbIo): Promise<number>;
|
|
52
67
|
export declare function hostUninstall(io: VerbIo): Promise<number>;
|
package/dist/host/verbs.js
CHANGED
|
@@ -23,6 +23,7 @@ import { renderPlist } from "./plist.js";
|
|
|
23
23
|
import { loadJob, unloadJob } from "./launchd.js";
|
|
24
24
|
import { classifyPoll, parseEnrollStart, parseExecutorList, EXECUTOR_CONTRACT, } from "./owner.js";
|
|
25
25
|
import { readRuntime } from "./runtime.js";
|
|
26
|
+
import { CODE } from "./s1-names.js";
|
|
26
27
|
import { RENEW_FIX, GRANT_EXPIRED_LINE } from "./grant.js";
|
|
27
28
|
export const INSTALL_FIX = "`mnemom agent host install`";
|
|
28
29
|
export const UNINSTALL_SESSION_LINE = (name) => `${name}: they stop restarting until ${INSTALL_FIX}`;
|
|
@@ -63,6 +64,22 @@ export function printRiskLine(io) {
|
|
|
63
64
|
export function joinTarget(orgId, orgName) {
|
|
64
65
|
return orgName ? `${orgName} (${orgId})` : orgId;
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The org a start names (ruling 34.03.4): `--org`, else the active org, else none, so the api
|
|
69
|
+
* keeps its fallback (the token's org, else the personal org).
|
|
70
|
+
*/
|
|
71
|
+
export function enrollOrg(io, opts) {
|
|
72
|
+
if (opts.org)
|
|
73
|
+
return { orgId: opts.org, from: "flag" };
|
|
74
|
+
const active = io.activeOrg?.() ?? null;
|
|
75
|
+
return active ? { orgId: active, from: "active" } : null;
|
|
76
|
+
}
|
|
77
|
+
/** The api's refusal code from an error body, or null. */
|
|
78
|
+
function errorCode(body) {
|
|
79
|
+
const e = body && typeof body === "object" ? body.error : undefined;
|
|
80
|
+
const code = e && typeof e === "object" ? e.code : undefined;
|
|
81
|
+
return typeof code === "string" ? code : null;
|
|
82
|
+
}
|
|
66
83
|
/** The device step: start, show the code, poll, write the state file. Returns an exit code. */
|
|
67
84
|
export async function enrollDevice(io, opts = {}) {
|
|
68
85
|
const { paths } = ctx(io);
|
|
@@ -71,16 +88,41 @@ export async function enrollDevice(io, opts = {}) {
|
|
|
71
88
|
io.err(`mnemom agent host: an enrollment is already in progress here (${lock.reason}): wait for it, then ${INSTALL_FIX}`);
|
|
72
89
|
return 1;
|
|
73
90
|
}
|
|
91
|
+
let org = enrollOrg(io, opts);
|
|
74
92
|
try {
|
|
75
|
-
const
|
|
93
|
+
const startWith = (o) => io.owner.enrollStart({
|
|
76
94
|
kind: "host",
|
|
77
95
|
runtime: "node",
|
|
78
96
|
contract_version: EXECUTOR_CONTRACT,
|
|
79
97
|
...(opts.label ? { machine_label: opts.label } : {}),
|
|
98
|
+
...(o ? { org_id: o.orgId } : {}),
|
|
80
99
|
});
|
|
100
|
+
let st = await startWith(org);
|
|
101
|
+
// An api from before 34.03.4 refuses org_id on a host start with its plain 400 invalid_scope
|
|
102
|
+
// (34.03.2 (1); mnemom-api enroll.ts "org_id is only accepted with scope org"; recorded beside
|
|
103
|
+
// exec_enroll in S1 codes.ts). An org taken from the active org then falls back to the start this
|
|
104
|
+
// CLI made before; an explicit --org cannot.
|
|
105
|
+
if (org && st.status === 400 && errorCode(st.body) === "invalid_scope") {
|
|
106
|
+
if (org.from === "flag") {
|
|
107
|
+
io.err(`mnemom agent host: this Mnemom server cannot enroll a machine into ${org.orgId} yet.`);
|
|
108
|
+
io.err("fix: `mnemom agent host enroll` without --org (joins your personal org)");
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
io.err(`mnemom agent host: this Mnemom server cannot enroll a machine into your active org ${org.orgId} yet; it joins your personal org.`);
|
|
112
|
+
org = null;
|
|
113
|
+
st = await startWith(null);
|
|
114
|
+
}
|
|
81
115
|
const start = st.status === 202 || st.status === 200 || st.status === 201
|
|
82
116
|
? parseEnrollStart(st.body)
|
|
83
117
|
: null;
|
|
118
|
+
if (!start && org && errorCode(st.body) === CODE.durable_approval_not_eligible) {
|
|
119
|
+
// 34.03.4: only a member of the org may enroll a machine into it.
|
|
120
|
+
io.err(`mnemom agent host: you are not a member of ${org.orgId}, so this machine cannot join it.`);
|
|
121
|
+
io.err(org.from === "flag"
|
|
122
|
+
? "fix: `mnemom org list` shows the organizations you belong to"
|
|
123
|
+
: "fix: `mnemom org use --clear`, then `mnemom agent host enroll` (joins your personal org)");
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
84
126
|
if (!start) {
|
|
85
127
|
const code = st.body && typeof st.body === "object"
|
|
86
128
|
? JSON.stringify(st.body).slice(0, 300)
|
|
@@ -211,8 +253,8 @@ export async function hostInstall(io) {
|
|
|
211
253
|
io.out(reload ? "Host daemon reloaded on this machine's enrollment." : "Host daemon installed.");
|
|
212
254
|
return 0;
|
|
213
255
|
}
|
|
214
|
-
export async function hostEnroll(io) {
|
|
215
|
-
return enrollDevice(io);
|
|
256
|
+
export async function hostEnroll(io, opts = {}) {
|
|
257
|
+
return enrollDevice(io, opts);
|
|
216
258
|
}
|
|
217
259
|
export async function hostRenew(io) {
|
|
218
260
|
const { paths } = ctx(io);
|
package/dist/index.js
CHANGED
|
@@ -2384,7 +2384,7 @@ program
|
|
|
2384
2384
|
program
|
|
2385
2385
|
.command("scan")
|
|
2386
2386
|
.description("Read-only local scan of GitHub PR metadata; the report is emailed to you every time (counts only, no repo names) and Mnemom keeps nothing at share level none")
|
|
2387
|
-
.option("--repo <owner/name...>", "Repo to scan (repeatable or comma-separated)")
|
|
2387
|
+
.option("--repo <owner/name...>", "Repo to scan (repeatable or comma-separated; omit on a terminal to pick from your repos)")
|
|
2388
2388
|
.option("--start <YYYY-MM-DD>", "Period start (default: today)")
|
|
2389
2389
|
.option("--end <YYYY-MM-DD>", "Period end (default: today)")
|
|
2390
2390
|
.option("--share <level>", "summary | full | none (default: full when asked; none when not interactive)")
|
|
@@ -2395,7 +2395,8 @@ program
|
|
|
2395
2395
|
.option("--org <org_id>", "Your org id; the report is emailed to you for this org (default: your only organization, else the scan stops and asks). Also selects the Record totals preview (needs MNEMOM_ORG_SCAN_MODE=true)")
|
|
2396
2396
|
.action(async (options) => {
|
|
2397
2397
|
try {
|
|
2398
|
-
|
|
2398
|
+
// Omitted --repo is allowed here: scanCommand picks from your repos on a terminal, else stops with a hint.
|
|
2399
|
+
const repos = options.repo === undefined ? [] : parseRepoFlags(options.repo);
|
|
2399
2400
|
if (!repos)
|
|
2400
2401
|
throw new Error("--repo owner/name is required (repeatable)");
|
|
2401
2402
|
const today = new Date().toISOString().slice(0, 10);
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -42,6 +42,13 @@ export declare function setRing(ring: CellRing | undefined): void;
|
|
|
42
42
|
export declare function getRing(): CellRing | undefined;
|
|
43
43
|
/** Override the active API base for this process (clears on undefined/empty). */
|
|
44
44
|
export declare function setApiUrlOverride(url: string | undefined): void;
|
|
45
|
+
/**
|
|
46
|
+
* True when the API is not production but the gateway is the production one. A lease key minted
|
|
47
|
+
* by a non-production cell's API means nothing to the production gateway, which answers every
|
|
48
|
+
* model call 401 and still receives the key (door2-path run 41783208169), so `agent run` refuses
|
|
49
|
+
* this pairing. A base that does not parse decides nothing. Pure.
|
|
50
|
+
*/
|
|
51
|
+
export declare function gatewayRingMismatch(apiUrl: string, gatewayUrl: string): boolean;
|
|
45
52
|
export declare function getApiUrl(): string;
|
|
46
53
|
export declare function getGatewayUrl(): string;
|
|
47
54
|
export declare function getWebsiteUrl(): string;
|
package/dist/lib/config.js
CHANGED
|
@@ -113,6 +113,27 @@ function ringUrls() {
|
|
|
113
113
|
const ring = getRing();
|
|
114
114
|
return ring ? RING_URLS[ring] : undefined;
|
|
115
115
|
}
|
|
116
|
+
function originOf(url) {
|
|
117
|
+
try {
|
|
118
|
+
return new URL(url).origin.toLowerCase();
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* True when the API is not production but the gateway is the production one. A lease key minted
|
|
126
|
+
* by a non-production cell's API means nothing to the production gateway, which answers every
|
|
127
|
+
* model call 401 and still receives the key (door2-path run 41783208169), so `agent run` refuses
|
|
128
|
+
* this pairing. A base that does not parse decides nothing. Pure.
|
|
129
|
+
*/
|
|
130
|
+
export function gatewayRingMismatch(apiUrl, gatewayUrl) {
|
|
131
|
+
const api = originOf(apiUrl);
|
|
132
|
+
const gateway = originOf(gatewayUrl);
|
|
133
|
+
if (api === null || gateway === null)
|
|
134
|
+
return false;
|
|
135
|
+
return api !== originOf(API_URLS.production) && gateway === originOf(GATEWAY_URLS.production);
|
|
136
|
+
}
|
|
116
137
|
export function getApiUrl() {
|
|
117
138
|
return (apiUrlOverride ?? envBase("MNEMOM_API_URL") ?? ringUrls()?.api ?? API_URLS[getEnvironment()]);
|
|
118
139
|
}
|
|
@@ -31,6 +31,8 @@ const ALLOWED_PATHS = [
|
|
|
31
31
|
new RegExp(`^/repos/${SLUG}/pulls\\?state=closed&sort=updated&direction=desc&per_page=\\d{1,3}&page=\\d{1,4}$`),
|
|
32
32
|
new RegExp(`^/repos/${SLUG}/pulls/\\d{1,9}$`),
|
|
33
33
|
new RegExp(`^/repos/${SLUG}/commits/[0-9a-f]{40}/check-runs\\?per_page=100$`),
|
|
34
|
+
// The repo picker's one list read (MNE-9643): the repos this login can read, newest push first.
|
|
35
|
+
/^\/user\/repos\?sort=pushed&direction=desc&per_page=\d{1,3}$/,
|
|
34
36
|
];
|
|
35
37
|
export function isAllowedScanPath(path) {
|
|
36
38
|
return ALLOWED_PATHS.some((re) => re.test(path));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo picker for `mnemom scan` when --repo is omitted on a terminal (MNE-9643). Read-only: one GET of
|
|
3
|
+
* the repos the signed-in GitHub account can read, through the same credential seam the scan uses.
|
|
4
|
+
* Only `full_name` is kept; descriptions, owners' logins beyond the slug, URLs and the token never leave
|
|
5
|
+
* this module. Failures throw fixed text (never a GitHub body, header or URL). Prompt style and fail-closed
|
|
6
|
+
* handling follow promptShareLevel in ./share.ts: no terminal, no answer, or a bad answer selects nothing.
|
|
7
|
+
*/
|
|
8
|
+
import { askMultiSelect } from "../prompt.js";
|
|
9
|
+
import { type GitHubCredential } from "./credential.js";
|
|
10
|
+
/** Most repos listed: the most recently pushed, so the list stays pickable. `--repo` names any other. */
|
|
11
|
+
export declare const REPO_PICKER_LIMIT = 30;
|
|
12
|
+
/** The only path the picker may read. */
|
|
13
|
+
export declare const REPO_LIST_PATH = "/user/repos?sort=pushed&direction=desc&per_page=30";
|
|
14
|
+
export declare const REPO_PICKER_NO_TERMINAL_HINT = "Pass --repo owner/name (repeatable), or run in a terminal to pick from your repos.";
|
|
15
|
+
export declare const REPO_LIST_FAILED_MSG = "Could not list your GitHub repos. Pass --repo owner/name, or check `gh auth status`.";
|
|
16
|
+
export declare const NO_REPOS_MSG = "No GitHub repos are readable by this account. Pass --repo owner/name.";
|
|
17
|
+
export declare const NO_REPO_SELECTED_MSG = "No repo selected. Pass --repo owner/name, or choose at least one number from the list.";
|
|
18
|
+
export interface RepoListDeps {
|
|
19
|
+
fetchImpl?: typeof fetch;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
/** GET-only read of the repos the credential can see. Returns valid slugs only, newest push first. */
|
|
23
|
+
export declare function listReadableRepos(credential: GitHubCredential, deps?: RepoListDeps): Promise<string[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Lists the readable repos and asks which to scan (comma-separated numbers, as the other multi-select
|
|
26
|
+
* prompts do). Throws fixed text when there is no terminal, nothing is listed, or nothing valid is chosen.
|
|
27
|
+
*/
|
|
28
|
+
export declare function pickRepos(credential: GitHubCredential, o: {
|
|
29
|
+
isTty: boolean;
|
|
30
|
+
deps?: RepoListDeps;
|
|
31
|
+
ask?: typeof askMultiSelect;
|
|
32
|
+
}): Promise<string[]>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo picker for `mnemom scan` when --repo is omitted on a terminal (MNE-9643). Read-only: one GET of
|
|
3
|
+
* the repos the signed-in GitHub account can read, through the same credential seam the scan uses.
|
|
4
|
+
* Only `full_name` is kept; descriptions, owners' logins beyond the slug, URLs and the token never leave
|
|
5
|
+
* this module. Failures throw fixed text (never a GitHub body, header or URL). Prompt style and fail-closed
|
|
6
|
+
* handling follow promptShareLevel in ./share.ts: no terminal, no answer, or a bad answer selects nothing.
|
|
7
|
+
*/
|
|
8
|
+
import { askMultiSelect } from "../prompt.js";
|
|
9
|
+
import { GITHUB_API, isAllowedScanPath } from "./pr-source.js";
|
|
10
|
+
import { assertReadOnlyCredential, isValidRepoSlug } from "./credential.js";
|
|
11
|
+
/** Most repos listed: the most recently pushed, so the list stays pickable. `--repo` names any other. */
|
|
12
|
+
export const REPO_PICKER_LIMIT = 30;
|
|
13
|
+
/** The only path the picker may read. */
|
|
14
|
+
export const REPO_LIST_PATH = `/user/repos?sort=pushed&direction=desc&per_page=${REPO_PICKER_LIMIT}`;
|
|
15
|
+
export const REPO_PICKER_NO_TERMINAL_HINT = "Pass --repo owner/name (repeatable), or run in a terminal to pick from your repos.";
|
|
16
|
+
export const REPO_LIST_FAILED_MSG = "Could not list your GitHub repos. Pass --repo owner/name, or check `gh auth status`.";
|
|
17
|
+
export const NO_REPOS_MSG = "No GitHub repos are readable by this account. Pass --repo owner/name.";
|
|
18
|
+
export const NO_REPO_SELECTED_MSG = "No repo selected. Pass --repo owner/name, or choose at least one number from the list.";
|
|
19
|
+
/** GET-only read of the repos the credential can see. Returns valid slugs only, newest push first. */
|
|
20
|
+
export async function listReadableRepos(credential, deps = {}) {
|
|
21
|
+
if (!isAllowedScanPath(REPO_LIST_PATH))
|
|
22
|
+
throw new Error(REPO_LIST_FAILED_MSG);
|
|
23
|
+
// Same order as runLocalScan: a credential GitHub granted more than read is refused before any read.
|
|
24
|
+
await assertReadOnlyCredential(credential).catch(() => {
|
|
25
|
+
throw new Error(REPO_LIST_FAILED_MSG);
|
|
26
|
+
});
|
|
27
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
28
|
+
let res;
|
|
29
|
+
try {
|
|
30
|
+
// User-level read: there is no single repo to scope to (the gh-login credential ignores the argument).
|
|
31
|
+
const bearer = await credential.tokenFor("");
|
|
32
|
+
res = await doFetch(`${GITHUB_API}${REPO_LIST_PATH}`, {
|
|
33
|
+
method: "GET",
|
|
34
|
+
headers: {
|
|
35
|
+
accept: "application/vnd.github+json",
|
|
36
|
+
"x-github-api-version": "2022-11-28",
|
|
37
|
+
"user-agent": "mnemom-cli-scan/1",
|
|
38
|
+
authorization: `Bearer ${bearer}`,
|
|
39
|
+
},
|
|
40
|
+
redirect: "error",
|
|
41
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? 10_000),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new Error(REPO_LIST_FAILED_MSG);
|
|
46
|
+
}
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
await res.body?.cancel().catch(() => { });
|
|
49
|
+
throw new Error(REPO_LIST_FAILED_MSG);
|
|
50
|
+
}
|
|
51
|
+
let json;
|
|
52
|
+
try {
|
|
53
|
+
json = await res.json();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error(REPO_LIST_FAILED_MSG);
|
|
57
|
+
}
|
|
58
|
+
if (!Array.isArray(json))
|
|
59
|
+
throw new Error(REPO_LIST_FAILED_MSG);
|
|
60
|
+
const slugs = json
|
|
61
|
+
.map((r) => (r && typeof r === "object" ? r.full_name : undefined))
|
|
62
|
+
.filter(isValidRepoSlug);
|
|
63
|
+
return [...new Set(slugs)].slice(0, REPO_PICKER_LIMIT);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Lists the readable repos and asks which to scan (comma-separated numbers, as the other multi-select
|
|
67
|
+
* prompts do). Throws fixed text when there is no terminal, nothing is listed, or nothing valid is chosen.
|
|
68
|
+
*/
|
|
69
|
+
export async function pickRepos(credential, o) {
|
|
70
|
+
if (!o.isTty)
|
|
71
|
+
throw new Error(`--repo owner/name is required (repeatable). ${REPO_PICKER_NO_TERMINAL_HINT}`);
|
|
72
|
+
const repos = await listReadableRepos(credential, o.deps);
|
|
73
|
+
if (repos.length === 0)
|
|
74
|
+
throw new Error(NO_REPOS_MSG);
|
|
75
|
+
const chosen = await (o.ask ?? askMultiSelect)("Which repos should be scanned (read-only)?", repos);
|
|
76
|
+
const picked = chosen.filter((c) => repos.includes(c));
|
|
77
|
+
if (picked.length === 0)
|
|
78
|
+
throw new Error(NO_REPO_SELECTED_MSG);
|
|
79
|
+
return picked;
|
|
80
|
+
}
|
|
@@ -1196,7 +1196,10 @@ var DURABLE_CODES = {
|
|
|
1196
1196
|
// ---- api executor enrollment start, POST ROUTES.exec_enroll (C04-R02, R03; wave-0 fix 3b) ----
|
|
1197
1197
|
// Names kept verbatim from C04-R03. A machine principal keeps 403 machine_principal_refused
|
|
1198
1198
|
// (C04-R02); a non-starter approver gets durable_approval_not_eligible (ruling 1.13). Device-poll
|
|
1199
|
-
// errors are not codes here: see ENROLL_POLL_ERRORS.
|
|
1199
|
+
// errors are not codes here: see ENROLL_POLL_ERRORS. Ruling 34.03.4: a host start naming an org_id
|
|
1200
|
+
// the caller is not a member of is also durable_approval_not_eligible (403, no approval row). A
|
|
1201
|
+
// malformed or refused org_id is the api's plain 400 invalid_scope, which is not a durable code;
|
|
1202
|
+
// an api from before 34.03.4 answers it to any org_id on a host start (34.03.2 (1)).
|
|
1200
1203
|
/** The caller lacks the entitlement durable.locked. */
|
|
1201
1204
|
not_entitled: { status: 403, surfaces: ["api"], fields: [] },
|
|
1202
1205
|
/** The declared kind is not in durable.executors (M1 allows 'host' only). */
|
|
@@ -1643,8 +1646,8 @@ var M1_CLI_SURFACE = Object.freeze([
|
|
|
1643
1646
|
entry(["agent", "done"], ["--json"]),
|
|
1644
1647
|
// BACKBONE C2 (`mnemom agent host` replaces `mnemom host`, amendment 2; design-v3 row 7).
|
|
1645
1648
|
entry(["agent", "host"]),
|
|
1646
|
-
// BACKBONE C2 enroll (device step).
|
|
1647
|
-
entry(["agent", "host", "enroll"]),
|
|
1649
|
+
// BACKBONE C2 enroll (device step); --org: ruling 34.03.4 (a host joins an org its user belongs to).
|
|
1650
|
+
entry(["agent", "host", "enroll"], ["--org"]),
|
|
1648
1651
|
// BACKBONE C2 install/status/uninstall; --json: C04 (`mnemom agent host install --json`),
|
|
1649
1652
|
// ruling 32.31.
|
|
1650
1653
|
entry(["agent", "host", "install"], ["--json"]),
|
|
@@ -36,8 +36,8 @@ export const M1_CLI_SURFACE = Object.freeze([
|
|
|
36
36
|
entry(["agent", "done"], ["--json"]),
|
|
37
37
|
// BACKBONE C2 (`mnemom agent host` replaces `mnemom host`, amendment 2; design-v3 row 7).
|
|
38
38
|
entry(["agent", "host"]),
|
|
39
|
-
// BACKBONE C2 enroll (device step).
|
|
40
|
-
entry(["agent", "host", "enroll"]),
|
|
39
|
+
// BACKBONE C2 enroll (device step); --org: ruling 34.03.4 (a host joins an org its user belongs to).
|
|
40
|
+
entry(["agent", "host", "enroll"], ["--org"]),
|
|
41
41
|
// BACKBONE C2 install/status/uninstall; --json: C04 (`mnemom agent host install --json`),
|
|
42
42
|
// ruling 32.31.
|
|
43
43
|
entry(["agent", "host", "install"], ["--json"]),
|
|
@@ -157,7 +157,10 @@ export const DURABLE_CODES = {
|
|
|
157
157
|
// ---- api executor enrollment start, POST ROUTES.exec_enroll (C04-R02, R03; wave-0 fix 3b) ----
|
|
158
158
|
// Names kept verbatim from C04-R03. A machine principal keeps 403 machine_principal_refused
|
|
159
159
|
// (C04-R02); a non-starter approver gets durable_approval_not_eligible (ruling 1.13). Device-poll
|
|
160
|
-
// errors are not codes here: see ENROLL_POLL_ERRORS.
|
|
160
|
+
// errors are not codes here: see ENROLL_POLL_ERRORS. Ruling 34.03.4: a host start naming an org_id
|
|
161
|
+
// the caller is not a member of is also durable_approval_not_eligible (403, no approval row). A
|
|
162
|
+
// malformed or refused org_id is the api's plain 400 invalid_scope, which is not a durable code;
|
|
163
|
+
// an api from before 34.03.4 answers it to any org_id on a host start (34.03.2 (1)).
|
|
161
164
|
/** The caller lacks the entitlement durable.locked. */
|
|
162
165
|
not_entitled: { status: 403, surfaces: ["api"], fields: [] },
|
|
163
166
|
/** The declared kind is not in durable.executors (M1 allows 'host' only). */
|