@mnemom/mnemom 0.17.32 → 0.17.34-next.1
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 +34 -4
- package/dist/commands/agent.js +116 -20
- package/dist/commands/protection.js +1 -1
- package/dist/commands/scan.d.ts +97 -0
- package/dist/commands/scan.js +238 -0
- package/dist/index.js +55 -0
- package/dist/lib/api.d.ts +13 -0
- package/dist/lib/api.js +25 -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/scan/caps.d.ts +35 -0
- package/dist/lib/scan/caps.js +67 -0
- package/dist/lib/scan/credential.d.ts +37 -0
- package/dist/lib/scan/credential.js +104 -0
- package/dist/lib/scan/failure.d.ts +10 -0
- package/dist/lib/scan/failure.js +43 -0
- package/dist/lib/scan/pr-source.d.ts +57 -0
- package/dist/lib/scan/pr-source.js +130 -0
- package/dist/lib/scan/record.d.ts +49 -0
- package/dist/lib/scan/record.js +85 -0
- package/dist/lib/scan/report-email.d.ts +92 -0
- package/dist/lib/scan/report-email.js +136 -0
- package/dist/lib/scan/report.d.ts +13 -0
- package/dist/lib/scan/report.js +30 -0
- package/dist/lib/scan/scan.d.ts +64 -0
- package/dist/lib/scan/scan.js +182 -0
- package/dist/lib/scan/share.d.ts +38 -0
- package/dist/lib/scan/share.js +47 -0
- package/dist/lib/scan/spend/ai-tool-detect.d.ts +6 -0
- package/dist/lib/scan/spend/ai-tool-detect.js +18 -0
- package/dist/lib/scan/spend/anthropic-usage.d.ts +57 -0
- package/dist/lib/scan/spend/anthropic-usage.js +244 -0
- package/dist/lib/scan/spend/cost-corpus.v1.d.ts +56 -0
- package/dist/lib/scan/spend/cost-corpus.v1.js +64 -0
- package/dist/lib/scan/spend/prices.d.ts +33 -0
- package/dist/lib/scan/spend/prices.js +57 -0
- package/dist/lib/scan/spend/spend-estimate.d.ts +30 -0
- package/dist/lib/scan/spend/spend-estimate.js +70 -0
- package/dist/lib/scan/spend/spend-tiers.d.ts +25 -0
- package/dist/lib/scan/spend/spend-tiers.js +42 -0
- package/dist/lib/scan/spend/token-rate.d.ts +30 -0
- package/dist/lib/scan/spend/token-rate.js +54 -0
- package/dist/lib/session-key.d.ts +41 -0
- package/dist/lib/session-key.js +124 -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-key-401.mjs +75 -0
- package/dist/rc-proxy.mjs +290 -30
- package/package.json +2 -1
|
@@ -43,9 +43,15 @@ export interface RenderedLinePreview {
|
|
|
43
43
|
coverage?: string;
|
|
44
44
|
kind: string;
|
|
45
45
|
}
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* The aip `renderLines` contract, injected. Resolves to the lines the card may show; REJECTS
|
|
48
|
+
* (with the renderer's reasons) when the card is not verifiable or breaks claim-lint, in which
|
|
49
|
+
* case no link is printed. `period` asks for the period-card surface (reverts always shown).
|
|
50
|
+
*/
|
|
47
51
|
export interface LineRenderer {
|
|
48
|
-
renderLines(card: unknown
|
|
52
|
+
renderLines(card: unknown, opts?: {
|
|
53
|
+
period?: boolean;
|
|
54
|
+
}): RenderedLinePreview[] | Promise<RenderedLinePreview[]>;
|
|
49
55
|
}
|
|
50
56
|
export interface StatementSource {
|
|
51
57
|
listLines(): Promise<CandidateLine[]>;
|
|
@@ -56,6 +62,11 @@ export interface CardIssuer {
|
|
|
56
62
|
}
|
|
57
63
|
export interface AgentShareDeps {
|
|
58
64
|
source: StatementSource;
|
|
65
|
+
/**
|
|
66
|
+
* Optional claim-lint screen (aip `lintRendering`) run on each candidate before it is
|
|
67
|
+
* offered; returns a reason when the line must not be shared. Word lists live in aip only.
|
|
68
|
+
*/
|
|
69
|
+
screen?: (line: SharedCardLine) => Promise<string | null> | string | null;
|
|
59
70
|
issuer: CardIssuer;
|
|
60
71
|
renderer: LineRenderer;
|
|
61
72
|
/** Interactive picker: returns the chosen line ids. */
|
|
@@ -66,6 +77,8 @@ export interface AgentShareDeps {
|
|
|
66
77
|
err: (line: string) => void;
|
|
67
78
|
}
|
|
68
79
|
export interface AgentShareFlags {
|
|
80
|
+
/** Period card: every offered revert line is included, and a card without one is refused. */
|
|
81
|
+
period?: boolean;
|
|
69
82
|
lines?: string;
|
|
70
83
|
list?: boolean;
|
|
71
84
|
json?: boolean;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { createInterface } from "node:readline/promises";
|
|
18
18
|
import { getWebsiteUrl } from "../lib/config.js";
|
|
19
19
|
import { fmt } from "../lib/format.js";
|
|
20
|
+
import { aipLineRenderer, aipLineScreen } from "./agent-share-render.js";
|
|
20
21
|
export const AGENT_SHARE_ENV = "MNEMOM_AGENT_SHARE";
|
|
21
22
|
export function agentShareEnabled(env = process.env) {
|
|
22
23
|
return env[AGENT_SHARE_ENV] === "1";
|
|
@@ -132,10 +133,16 @@ export async function agentShareCommand(flags, deps, env = process.env) {
|
|
|
132
133
|
const refused = [];
|
|
133
134
|
for (const c of candidates) {
|
|
134
135
|
const r = refuseReason(c);
|
|
135
|
-
if (r)
|
|
136
|
+
if (r) {
|
|
136
137
|
refused.push(r);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const shared = toSharedLine(c);
|
|
141
|
+
const lint = deps.screen ? await deps.screen(shared) : null;
|
|
142
|
+
if (lint)
|
|
143
|
+
refused.push({ id: shared.id, reason: "estimate", detail: lint });
|
|
137
144
|
else
|
|
138
|
-
offered.push(
|
|
145
|
+
offered.push(shared);
|
|
139
146
|
}
|
|
140
147
|
if (flags.list) {
|
|
141
148
|
if (flags.json) {
|
|
@@ -175,6 +182,15 @@ export async function agentShareCommand(flags, deps, env = process.env) {
|
|
|
175
182
|
deps.err(fmt.error("No lines picked."));
|
|
176
183
|
return 1;
|
|
177
184
|
}
|
|
185
|
+
if (flags.period) {
|
|
186
|
+
for (const l of offered)
|
|
187
|
+
if (l.kind === "revert" && !ids.includes(l.id))
|
|
188
|
+
ids.push(l.id);
|
|
189
|
+
if (!offered.some((l) => l.kind === "revert")) {
|
|
190
|
+
deps.err(fmt.error("A period card always shows reverts, and none are available to share."));
|
|
191
|
+
return 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
178
194
|
const chosen = [];
|
|
179
195
|
for (const id of ids) {
|
|
180
196
|
const r = refused.find((x) => x.id === id);
|
|
@@ -202,13 +218,22 @@ export async function agentShareCommand(flags, deps, env = process.env) {
|
|
|
202
218
|
deps.err(fmt.error(`Refusing to share: ${problem}.`));
|
|
203
219
|
return 1;
|
|
204
220
|
}
|
|
221
|
+
// The renderer is the gate: nothing is linked unless aip renders the card (verified, lint clean).
|
|
222
|
+
let rendered;
|
|
223
|
+
try {
|
|
224
|
+
rendered = await deps.renderer.renderLines(card, { period: flags.period === true });
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
deps.err(fmt.error(`Refusing to share: ${error.message}.`));
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
205
230
|
const link = buildShareLink(card, deps.websiteBase());
|
|
206
231
|
if (flags.json) {
|
|
207
232
|
deps.out(JSON.stringify({ link, lines: chosen }, null, 2));
|
|
208
233
|
return 0;
|
|
209
234
|
}
|
|
210
235
|
deps.out("Your card:");
|
|
211
|
-
for (const r of
|
|
236
|
+
for (const r of rendered) {
|
|
212
237
|
deps.out(` ${r.text}${r.coverage ? ` (${r.coverage})` : ""}`);
|
|
213
238
|
}
|
|
214
239
|
deps.out("");
|
|
@@ -224,12 +249,8 @@ const notConnected = () => {
|
|
|
224
249
|
export const liveAgentShareDeps = {
|
|
225
250
|
source: { listLines: async () => notConnected() },
|
|
226
251
|
issuer: { issue: async () => notConnected() },
|
|
227
|
-
renderer:
|
|
228
|
-
|
|
229
|
-
text: `${l.label}: ${l.value}`,
|
|
230
|
-
kind: l.kind,
|
|
231
|
-
})),
|
|
232
|
-
},
|
|
252
|
+
renderer: aipLineRenderer(undefined),
|
|
253
|
+
screen: aipLineScreen,
|
|
233
254
|
pick: async (lines) => {
|
|
234
255
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
235
256
|
try {
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* line, or the proxy's stdin under RC). It is never logged, printed, or passed as
|
|
26
26
|
* an argv value.
|
|
27
27
|
*/
|
|
28
|
+
import { type ChildProcess } from "node:child_process";
|
|
29
|
+
import { type SessionKeyDeps, type SessionKeyMintArgs, type SessionKeyRenewal } from "../lib/session-key.js";
|
|
28
30
|
import { type MnemomKeyDeps } from "../lib/subscription-login.js";
|
|
29
31
|
import { type SessionRecord } from "../lib/agent-ledger.js";
|
|
30
32
|
import { type ExperimentalId } from "../lib/experimental.js";
|
|
@@ -417,21 +419,49 @@ export interface BaseUrlProxyOptions {
|
|
|
417
419
|
*/
|
|
418
420
|
export declare function startBaseUrlProxy(door: string, done: (baseUrl: string | null, recordStatePath?: string) => void, opts?: BaseUrlProxyOptions): void;
|
|
419
421
|
/**
|
|
420
|
-
* The
|
|
421
|
-
* Mnemom key rides line 2 on BOTH auth modes when the launch has one — the proxy
|
|
422
|
-
* x-mnemom-api-key either way; a subscription launch always has one, an api-key
|
|
423
|
-
* when this machine stores one or the launch pins.
|
|
422
|
+
* The four lines the Remote Control proxy reads on stdin (anthropic, mnemom, openai, proxy
|
|
423
|
+
* credential). The Mnemom key rides line 2 on BOTH auth modes when the launch has one — the proxy
|
|
424
|
+
* forwards it as x-mnemom-api-key either way; a subscription launch always has one, an api-key
|
|
425
|
+
* launch has one when this machine stores one or the launch pins. Line 4 is this session's proxy
|
|
426
|
+
* credential ({@link newRcProxyCredential}). Never logged.
|
|
424
427
|
*/
|
|
425
428
|
export declare function rcProxyStdin(ctx: {
|
|
426
429
|
anthropicKey?: string;
|
|
427
430
|
mnemomKey?: string;
|
|
431
|
+
proxyCredential: string;
|
|
428
432
|
}): string;
|
|
433
|
+
/**
|
|
434
|
+
* MNE-9233: a fresh per-session credential for the Remote Control proxy. The proxy injects the
|
|
435
|
+
* owner's keys and its loopback port is reachable by every local process, so it serves only a
|
|
436
|
+
* caller that presents this value as `Proxy-Authorization: Basic` (rc-proxy.mjs). 32 random bytes,
|
|
437
|
+
* hex: URL-safe in the HTTPS_PROXY userinfo without escaping.
|
|
438
|
+
*/
|
|
439
|
+
export declare function newRcProxyCredential(): string;
|
|
440
|
+
/**
|
|
441
|
+
* The HTTPS_PROXY value for the Remote Control child: the proxy's loopback port, with this
|
|
442
|
+
* session's credential as the userinfo. Claude Code sends it as `Proxy-Authorization` on CONNECT
|
|
443
|
+
* and on absolute-form requests. The user name must match rc-proxy.mjs's PROXY_USER.
|
|
444
|
+
*/
|
|
445
|
+
export declare function rcProxyUrl(port: string, proxyCredential: string): string;
|
|
429
446
|
/**
|
|
430
447
|
* The agent id a launch pins, or undefined for a name launch: `--agent` when it is a full
|
|
431
448
|
* `mnm-<uuid>` (any case), else launcher.toml `agent_id` (only when no `--agent` was given —
|
|
432
449
|
* selectLaunchAgent). A truncated/garbled `mnm-…` throws instead of becoming a name.
|
|
433
450
|
*/
|
|
434
451
|
export declare function resolveLaunchPin(options: Pick<AgentOptions, "agent" | "agentId">): string | undefined;
|
|
452
|
+
/** The longest line the proxy sends the launcher (`remint <key id>`) is far shorter than this. */
|
|
453
|
+
export declare const RC_CONTROL_LINE_MAX = 1024;
|
|
454
|
+
/**
|
|
455
|
+
* Renew the Remote Control proxy's session key over its control pipe (fd 3; rc-proxy.mjs SESSION
|
|
456
|
+
* KEY RENEWAL): a `key <id> <expires> <key>` line before each expiry, a `remint <id>` line from the
|
|
457
|
+
* proxy when the gateway rejects its key, answered with a new key or `key-failed`. Renewal events
|
|
458
|
+
* go to the proxy log (the terminal belongs to Claude Code by then), as key ids and expiry times.
|
|
459
|
+
*/
|
|
460
|
+
export declare function startRcSessionKeyRenewal(proxy: Pick<ChildProcess, "stdio">, proxyLog: string, initial: {
|
|
461
|
+
keyId: string;
|
|
462
|
+
expiresAt: string;
|
|
463
|
+
mintArgs: SessionKeyMintArgs;
|
|
464
|
+
}, makeDeps?: (say: (line: string) => void) => SessionKeyDeps): SessionKeyRenewal | undefined;
|
|
435
465
|
/**
|
|
436
466
|
* Locates the `handoff` MCP server (src/mcp/handoff-server.mjs in dev, dist/mcp/handoff-server.mjs
|
|
437
467
|
* once built — a raw, uncompiled `.mjs` file that `copy-assets.mjs` copies verbatim, exactly like
|
package/dist/commands/agent.js
CHANGED
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
* an argv value.
|
|
27
27
|
*/
|
|
28
28
|
import { spawn, spawnSync } from "node:child_process";
|
|
29
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
|
|
30
|
-
import { randomUUID } from "node:crypto";
|
|
29
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
|
|
30
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
31
31
|
import { tmpdir, homedir, hostname } from "node:os";
|
|
32
32
|
import { basename, join, dirname } from "node:path";
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
@@ -39,7 +39,7 @@ import { ALLOW_AUTOCOMPACT_OFF_ENV, dropAutocompactOffEnv, hasOwnSettingsFlag, m
|
|
|
39
39
|
import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
40
40
|
import { createApiKey } from "../lib/api.js";
|
|
41
41
|
import { recordOrgFor } from "../lib/record-org.js";
|
|
42
|
-
import { defaultSessionKeyDeps, mintSessionGatewayKey, revokeSessionGatewayKey, } from "../lib/session-key.js";
|
|
42
|
+
import { defaultSessionKeyDeps, mintSessionGatewayKey, revokeSessionGatewayKey, startSessionKeyRenewal, } from "../lib/session-key.js";
|
|
43
43
|
import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, sayRetiredConsoleNotice, readMnemomKey, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
|
|
44
44
|
import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, resolveRetentionDays, resumedContextProfile, toLedgerContract, } from "../lib/agent-ledger.js";
|
|
45
45
|
import { isLoggedIn } from "../lib/auth.js";
|
|
@@ -612,6 +612,8 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
612
612
|
// org is the key owner's personal org, where the agent does not exist, so the API answers 404
|
|
613
613
|
// and the launch would fail closed (door1-path run 40400479127). Same read as Record's.
|
|
614
614
|
let sessionKeyId;
|
|
615
|
+
// What a Remote Control launch needs to re-mint before the 24h expiry (startSessionKeyRenewal).
|
|
616
|
+
let sessionKeyRenewal;
|
|
615
617
|
// One read of the served agent's org serves the mint and Record; each says why in its own
|
|
616
618
|
// words when the read fails and the account org is used instead.
|
|
617
619
|
let servedOrg;
|
|
@@ -632,13 +634,15 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
632
634
|
say(fmt.warn(`mnemom agent: could not read ${servedAgentId}'s org (${o.fallback}); minting the session key under ${account.orgId}.`));
|
|
633
635
|
mintOrgId = o.orgId;
|
|
634
636
|
}
|
|
635
|
-
const
|
|
637
|
+
const mintArgs = {
|
|
636
638
|
agentId: servedAgentId,
|
|
637
639
|
orgId: mintOrgId,
|
|
638
640
|
sessionId: cc.id ?? undefined,
|
|
639
|
-
}
|
|
641
|
+
};
|
|
642
|
+
const minted = await mintSessionGatewayKey(defaultSessionKeyDeps(say), mintArgs);
|
|
640
643
|
mnemomKey = minted.key;
|
|
641
644
|
sessionKeyId = minted.keyId;
|
|
645
|
+
sessionKeyRenewal = { expiresAt: minted.expiresAt, mintArgs };
|
|
642
646
|
}
|
|
643
647
|
// ── 1M context window by default ── the model this launch runs on gets `[1m]` when it has no
|
|
644
648
|
// window suffix and supports 1M (resolveWindowModel); an explicit choice — the
|
|
@@ -828,6 +832,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
828
832
|
yolo,
|
|
829
833
|
ccSessionId,
|
|
830
834
|
sessionKeyId,
|
|
835
|
+
sessionKeyRenewal,
|
|
831
836
|
});
|
|
832
837
|
}
|
|
833
838
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -1620,13 +1625,31 @@ export function startBaseUrlProxy(door, done, opts = {}) {
|
|
|
1620
1625
|
// Remote Control launch (terminal RC + headless server) — behind the bundled proxy
|
|
1621
1626
|
// ───────────────────────────────────────────────────────────────────────────
|
|
1622
1627
|
/**
|
|
1623
|
-
* The
|
|
1624
|
-
* Mnemom key rides line 2 on BOTH auth modes when the launch has one — the proxy
|
|
1625
|
-
* x-mnemom-api-key either way; a subscription launch always has one, an api-key
|
|
1626
|
-
* when this machine stores one or the launch pins.
|
|
1628
|
+
* The four lines the Remote Control proxy reads on stdin (anthropic, mnemom, openai, proxy
|
|
1629
|
+
* credential). The Mnemom key rides line 2 on BOTH auth modes when the launch has one — the proxy
|
|
1630
|
+
* forwards it as x-mnemom-api-key either way; a subscription launch always has one, an api-key
|
|
1631
|
+
* launch has one when this machine stores one or the launch pins. Line 4 is this session's proxy
|
|
1632
|
+
* credential ({@link newRcProxyCredential}). Never logged.
|
|
1627
1633
|
*/
|
|
1628
1634
|
export function rcProxyStdin(ctx) {
|
|
1629
|
-
return `${ctx.anthropicKey ?? ""}\n${ctx.mnemomKey ?? ""}\n\n`;
|
|
1635
|
+
return `${ctx.anthropicKey ?? ""}\n${ctx.mnemomKey ?? ""}\n\n${ctx.proxyCredential}\n`;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* MNE-9233: a fresh per-session credential for the Remote Control proxy. The proxy injects the
|
|
1639
|
+
* owner's keys and its loopback port is reachable by every local process, so it serves only a
|
|
1640
|
+
* caller that presents this value as `Proxy-Authorization: Basic` (rc-proxy.mjs). 32 random bytes,
|
|
1641
|
+
* hex: URL-safe in the HTTPS_PROXY userinfo without escaping.
|
|
1642
|
+
*/
|
|
1643
|
+
export function newRcProxyCredential() {
|
|
1644
|
+
return randomBytes(32).toString("hex");
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* The HTTPS_PROXY value for the Remote Control child: the proxy's loopback port, with this
|
|
1648
|
+
* session's credential as the userinfo. Claude Code sends it as `Proxy-Authorization` on CONNECT
|
|
1649
|
+
* and on absolute-form requests. The user name must match rc-proxy.mjs's PROXY_USER.
|
|
1650
|
+
*/
|
|
1651
|
+
export function rcProxyUrl(port, proxyCredential) {
|
|
1652
|
+
return `http://mnemom:${proxyCredential}@127.0.0.1:${port}`;
|
|
1630
1653
|
}
|
|
1631
1654
|
/**
|
|
1632
1655
|
* The agent id a launch pins, or undefined for a name launch: `--agent` when it is a full
|
|
@@ -1652,6 +1675,8 @@ function launchRemoteControl(ctx) {
|
|
|
1652
1675
|
}
|
|
1653
1676
|
}
|
|
1654
1677
|
const proxyScript = resolveProxyScript();
|
|
1678
|
+
// MNE-9233: only the child launched below may use this session's proxy (stdin line 4 + HTTPS_PROXY).
|
|
1679
|
+
const proxyCredential = newRcProxyCredential();
|
|
1655
1680
|
const rcDir = mkdtempSync(join(rcStateRoot(), "mnemom-code-rc."));
|
|
1656
1681
|
writeFileSync(join(rcDir, "owner.pid"), `${process.pid}\n`, { mode: 0o600 }); // rc-state.mjs sweep
|
|
1657
1682
|
const proxyLog = process.env.MNEMOM_AGENT_RC_PROXY_LOG || join(rcDir, "proxy.log");
|
|
@@ -1668,13 +1693,28 @@ function launchRemoteControl(ctx) {
|
|
|
1668
1693
|
MNEMOM_AGENT_RC_PARENT_PID: String(process.pid),
|
|
1669
1694
|
MNEMOM_AGENT_RC_AUTH: ctx.anthropicKey ? "api-key" : "subscription",
|
|
1670
1695
|
};
|
|
1696
|
+
// The minted key expires after 24h and an RC seat outlives that: renew it over a control pipe
|
|
1697
|
+
// (fd 3) — the new key never touches argv, env or disk. Only its id and expiry ride in the env.
|
|
1698
|
+
const renewing = Boolean(ctx.sessionKeyId && ctx.sessionKeyRenewal && ctx.mnemomKey);
|
|
1699
|
+
if (renewing) {
|
|
1700
|
+
proxyEnv.MNEMOM_AGENT_RC_CONTROL_FD = "3";
|
|
1701
|
+
proxyEnv.MNEMOM_AGENT_RC_MNEMOM_KEY_ID = ctx.sessionKeyId;
|
|
1702
|
+
proxyEnv.MNEMOM_AGENT_RC_MNEMOM_KEY_EXPIRES_AT = ctx.sessionKeyRenewal?.expiresAt;
|
|
1703
|
+
}
|
|
1671
1704
|
// MNE-8245 defense-in-depth: the proxy gets the session's (minted) Mnemom key on stdin only
|
|
1672
1705
|
// and reads none of the owner's own Mnemom credential vars, so they never ride into its env.
|
|
1673
1706
|
stripOwnerCredentialEnv(proxyEnv);
|
|
1674
1707
|
const proxy = spawn("node", [proxyScript], {
|
|
1675
1708
|
env: proxyEnv,
|
|
1676
|
-
stdio: ["pipe", "pipe", "inherit"],
|
|
1709
|
+
stdio: ["pipe", "pipe", "inherit", renewing ? "pipe" : "ignore"],
|
|
1677
1710
|
});
|
|
1711
|
+
const renewal = renewing
|
|
1712
|
+
? startRcSessionKeyRenewal(proxy, proxyLog, {
|
|
1713
|
+
keyId: ctx.sessionKeyId,
|
|
1714
|
+
expiresAt: ctx.sessionKeyRenewal?.expiresAt,
|
|
1715
|
+
mintArgs: ctx.sessionKeyRenewal?.mintArgs,
|
|
1716
|
+
})
|
|
1717
|
+
: undefined;
|
|
1678
1718
|
let stopped = false;
|
|
1679
1719
|
const stopProxyAndTempDir = () => {
|
|
1680
1720
|
if (!proxy.killed) {
|
|
@@ -1701,9 +1741,12 @@ function launchRemoteControl(ctx) {
|
|
|
1701
1741
|
if (stopped)
|
|
1702
1742
|
return;
|
|
1703
1743
|
stopped = true;
|
|
1744
|
+
renewal?.stop();
|
|
1704
1745
|
stopProxyAndTempDir();
|
|
1705
|
-
|
|
1706
|
-
|
|
1746
|
+
// After a renewal the proxy holds a newer key than the one minted at launch: revoke that one.
|
|
1747
|
+
const keyId = renewal?.currentKeyId() ?? ctx.sessionKeyId;
|
|
1748
|
+
if (keyId) {
|
|
1749
|
+
await revokeSessionGatewayKey(defaultSessionKeyDeps(say), keyId);
|
|
1707
1750
|
}
|
|
1708
1751
|
};
|
|
1709
1752
|
// Last-resort SYNC fallback only — Node's "exit" handlers cannot await, so this branch never
|
|
@@ -1716,11 +1759,12 @@ function launchRemoteControl(ctx) {
|
|
|
1716
1759
|
if (stopped)
|
|
1717
1760
|
return;
|
|
1718
1761
|
stopped = true;
|
|
1762
|
+
renewal?.stop();
|
|
1719
1763
|
stopProxyAndTempDir();
|
|
1720
1764
|
});
|
|
1721
|
-
// Secrets on stdin —
|
|
1722
|
-
// Anthropic key, plus this machine's Mnemom key when it has one (the key principal;
|
|
1723
|
-
// with a pin); a subscription launch sends the Mnemom key only (the identity anchor — the
|
|
1765
|
+
// Secrets on stdin — four lines (anthropic, mnemom, openai, proxy credential). An api-key launch
|
|
1766
|
+
// sends the Anthropic key, plus this machine's Mnemom key when it has one (the key principal;
|
|
1767
|
+
// always with a pin); a subscription launch sends the Mnemom key only (the identity anchor — the
|
|
1724
1768
|
// user's OAuth bearer is what pays). openai is always empty on this Anthropic-only door.
|
|
1725
1769
|
// A proxy that exits before reading them turns this write into an EPIPE; that is
|
|
1726
1770
|
// not a failure of its own — the exited-before-READY check below refuses the
|
|
@@ -1731,7 +1775,7 @@ function launchRemoteControl(ctx) {
|
|
|
1731
1775
|
say(`mnemom agent: could not hand the secrets to the Remote Control proxy: ${err.message}`);
|
|
1732
1776
|
void onExit().finally(() => process.exit(1));
|
|
1733
1777
|
});
|
|
1734
|
-
proxy.stdin?.end(rcProxyStdin(ctx));
|
|
1778
|
+
proxy.stdin?.end(rcProxyStdin({ ...ctx, proxyCredential }));
|
|
1735
1779
|
let stdoutBuf = "";
|
|
1736
1780
|
const start = Date.now();
|
|
1737
1781
|
const tryStart = () => {
|
|
@@ -1748,7 +1792,7 @@ function launchRemoteControl(ctx) {
|
|
|
1748
1792
|
void onExit().finally(() => process.exit(1));
|
|
1749
1793
|
return;
|
|
1750
1794
|
}
|
|
1751
|
-
spawnRcCli({ ...ctx, port, ca, onExit });
|
|
1795
|
+
spawnRcCli({ ...ctx, port, ca, proxyCredential, onExit });
|
|
1752
1796
|
return;
|
|
1753
1797
|
}
|
|
1754
1798
|
if (proxy.exitCode !== null) {
|
|
@@ -1773,6 +1817,58 @@ function launchRemoteControl(ctx) {
|
|
|
1773
1817
|
});
|
|
1774
1818
|
tryStart();
|
|
1775
1819
|
}
|
|
1820
|
+
/** The longest line the proxy sends the launcher (`remint <key id>`) is far shorter than this. */
|
|
1821
|
+
export const RC_CONTROL_LINE_MAX = 1024;
|
|
1822
|
+
/**
|
|
1823
|
+
* Renew the Remote Control proxy's session key over its control pipe (fd 3; rc-proxy.mjs SESSION
|
|
1824
|
+
* KEY RENEWAL): a `key <id> <expires> <key>` line before each expiry, a `remint <id>` line from the
|
|
1825
|
+
* proxy when the gateway rejects its key, answered with a new key or `key-failed`. Renewal events
|
|
1826
|
+
* go to the proxy log (the terminal belongs to Claude Code by then), as key ids and expiry times.
|
|
1827
|
+
*/
|
|
1828
|
+
export function startRcSessionKeyRenewal(proxy, proxyLog, initial, makeDeps = defaultSessionKeyDeps) {
|
|
1829
|
+
const control = proxy.stdio?.[3];
|
|
1830
|
+
if (!control)
|
|
1831
|
+
return undefined;
|
|
1832
|
+
const logLine = (line) => {
|
|
1833
|
+
try {
|
|
1834
|
+
appendFileSync(proxyLog, `${new Date().toISOString()} launcher: ${line}\n`);
|
|
1835
|
+
}
|
|
1836
|
+
catch {
|
|
1837
|
+
/* best effort: the log dir may be gone at exit */
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
control.on("error", () => {
|
|
1841
|
+
/* the proxy exited: the next delivery fails and is logged */
|
|
1842
|
+
});
|
|
1843
|
+
const renewal = startSessionKeyRenewal({
|
|
1844
|
+
deps: makeDeps(logLine),
|
|
1845
|
+
mintArgs: initial.mintArgs,
|
|
1846
|
+
initial: { keyId: initial.keyId, expiresAt: initial.expiresAt },
|
|
1847
|
+
deliver: (minted) => {
|
|
1848
|
+
if (control.destroyed || !control.writable)
|
|
1849
|
+
throw new Error("the proxy's control pipe is closed");
|
|
1850
|
+
control.write(`key ${minted.keyId} ${minted.expiresAt} ${minted.key}\n`);
|
|
1851
|
+
},
|
|
1852
|
+
refuse: (reason) => {
|
|
1853
|
+
if (!control.destroyed && control.writable)
|
|
1854
|
+
control.write(`key-failed ${reason}\n`);
|
|
1855
|
+
},
|
|
1856
|
+
});
|
|
1857
|
+
let buf = "";
|
|
1858
|
+
control.on("data", (chunk) => {
|
|
1859
|
+
buf += chunk.toString("utf8");
|
|
1860
|
+
let i;
|
|
1861
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
1862
|
+
const [cmd, keyId] = buf.slice(0, i).split(" ");
|
|
1863
|
+
buf = buf.slice(i + 1);
|
|
1864
|
+
if (cmd === "remint")
|
|
1865
|
+
void renewal.renew("rejected", keyId);
|
|
1866
|
+
}
|
|
1867
|
+
if (buf.length > RC_CONTROL_LINE_MAX)
|
|
1868
|
+
buf = ""; // a runaway line, never a real request
|
|
1869
|
+
});
|
|
1870
|
+
return renewal;
|
|
1871
|
+
}
|
|
1776
1872
|
function spawnRcCli(ctx) {
|
|
1777
1873
|
// Session name for the claude.ai session list: "<agent>-<scenario>", unless
|
|
1778
1874
|
// the caller passed their own name after `--` (`--name x`, `--name=x`, `-n x`; lifted out here).
|
|
@@ -1805,8 +1901,8 @@ function spawnRcCli(ctx) {
|
|
|
1805
1901
|
// env. The RC gate forwards the Mnemom identity via the proxy's own auth plane, never from
|
|
1806
1902
|
// this child's env, so there is no legitimate use for these vars here either.
|
|
1807
1903
|
stripOwnerCredentialEnv(childEnv);
|
|
1808
|
-
childEnv.HTTPS_PROXY =
|
|
1809
|
-
childEnv.https_proxy =
|
|
1904
|
+
childEnv.HTTPS_PROXY = rcProxyUrl(ctx.port, ctx.proxyCredential);
|
|
1905
|
+
childEnv.https_proxy = childEnv.HTTPS_PROXY;
|
|
1810
1906
|
childEnv.NODE_EXTRA_CA_CERTS = ctx.ca;
|
|
1811
1907
|
childEnv[SESSION_NAME_ENV] = rcName;
|
|
1812
1908
|
// Autocompact guard (lib/autocompact-guard.ts). The interactive shape gets the --settings layer
|
|
@@ -15,7 +15,7 @@ const OP_SEVERITIES = ["low", "medium", "high", "critical"];
|
|
|
15
15
|
// Per ADR-037 Decision 4: deny public LLM endpoints + public DNS providers,
|
|
16
16
|
// and the any-host CIDRs, at write time.
|
|
17
17
|
// T8-4 (2026-05-19) extended the corpus per
|
|
18
|
-
// `safe-house-hardening/
|
|
18
|
+
// `safe-house-hardening/audits/deny-list-completeness.md`: added emerging
|
|
19
19
|
// LLM providers (Replicate, Together, Anyscale, Fireworks, xAI,
|
|
20
20
|
// Perplexity, DeepSeek) and three reserved IP ranges (link-local v4/v6
|
|
21
21
|
// + multicast v4).
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom scan`: a read-only local scan of GitHub PR metadata. Runs entirely on this machine; nothing is
|
|
3
|
+
* uploaded in this build. The share-level prompt gates what a later upload step may send, and the chosen
|
|
4
|
+
* payload is printed so the user sees exactly what would leave. Pure logic is exported so each reject
|
|
5
|
+
* arm is unit-testable without Commander (same pattern as commands/usage.ts).
|
|
6
|
+
*/
|
|
7
|
+
import { type GitHubCredential } from "../lib/scan/credential.js";
|
|
8
|
+
import { type ScanDeps } from "../lib/scan/scan.js";
|
|
9
|
+
import { type AnthropicUsageDeps } from "../lib/scan/spend/anthropic-usage.js";
|
|
10
|
+
import { type RecordDeps, type RecordOutcome } from "../lib/scan/record.js";
|
|
11
|
+
import { type ReportEmailDeps } from "../lib/scan/report-email.js";
|
|
12
|
+
import { type ReportRecipients } from "../lib/scan/share.js";
|
|
13
|
+
export interface ScanCommandOptions {
|
|
14
|
+
repos: string[];
|
|
15
|
+
period?: {
|
|
16
|
+
start: string;
|
|
17
|
+
end: string;
|
|
18
|
+
};
|
|
19
|
+
share?: string;
|
|
20
|
+
/** Report recipient. Required unless the runner is the CTO (asked when omitted). */
|
|
21
|
+
ctoEmail?: string;
|
|
22
|
+
/** Tier 2: the CTO's own monthly figure, as typed. Needs MNEMOM_ORG_SCAN_MODE=true. */
|
|
23
|
+
monthlySpend?: string;
|
|
24
|
+
/** Tier 2 without a figure on the command line: ask for one on the terminal. */
|
|
25
|
+
noAnthropicUsage?: boolean;
|
|
26
|
+
/** Tier 1: ask for the Anthropic admin key on the terminal (masked). Otherwise ANTHROPIC_ADMIN_KEY is used if set. */
|
|
27
|
+
anthropicAdminKey?: boolean;
|
|
28
|
+
/** Tier 1 result, when the caller already has it (test seam; set from the Anthropic reads otherwise). */
|
|
29
|
+
anthropicUsageUsd?: number;
|
|
30
|
+
anthropicOver200kShare?: number | null;
|
|
31
|
+
/** Test seam: the fetch behind the two Anthropic reads. */
|
|
32
|
+
anthropicDeps?: AnthropicUsageDeps;
|
|
33
|
+
/**
|
|
34
|
+
* The org the report is emailed for; inferred from the signed-in user's single non-personal org when
|
|
35
|
+
* omitted (see resolveScanOrg). The Record totals preview (MNEMOM_ORG_SCAN_MODE=true) reads ONLY an
|
|
36
|
+
* org passed explicitly here, never an inferred one: a guess could read the wrong org.
|
|
37
|
+
*/
|
|
38
|
+
org?: string;
|
|
39
|
+
/** Test seam: the Record read. */
|
|
40
|
+
recordDeps?: RecordDeps;
|
|
41
|
+
/** Test seam: the report-email POST. */
|
|
42
|
+
emailDeps?: ReportEmailDeps;
|
|
43
|
+
/** Test seam: the signed-in user's orgs, used to infer --org when it is omitted. */
|
|
44
|
+
listOrgs?: () => Promise<Array<{
|
|
45
|
+
org_id: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
is_personal?: boolean;
|
|
48
|
+
}>>;
|
|
49
|
+
/** Test seams. */
|
|
50
|
+
isTty?: boolean;
|
|
51
|
+
env?: NodeJS.ProcessEnv;
|
|
52
|
+
runnerEmail?: string;
|
|
53
|
+
credential?: GitHubCredential;
|
|
54
|
+
deps?: Partial<ScanDeps>;
|
|
55
|
+
now?: () => number;
|
|
56
|
+
}
|
|
57
|
+
/** Parses repeated/comma-separated `--repo owner/name`. Returns null on any invalid slug. */
|
|
58
|
+
export declare function parseRepoFlags(raw: string[] | undefined): string[] | null;
|
|
59
|
+
export declare const isEmail: (v: unknown) => v is string;
|
|
60
|
+
/** The runner's email (mnemom login, else asked) and the CTO's (flag, else "are you the CTO?"). */
|
|
61
|
+
export declare function resolveRecipients(o: {
|
|
62
|
+
runnerEmail?: string;
|
|
63
|
+
ctoEmail?: string;
|
|
64
|
+
}): Promise<ReportRecipients>;
|
|
65
|
+
/** The CTO's figure in USD, or null for none. Throws fixed text only; the input is never echoed. */
|
|
66
|
+
export declare function resolveSelfReportedSpend(o: ScanCommandOptions): Promise<number | null>;
|
|
67
|
+
export declare const NO_ADMIN_KEY_TERMINAL_MSG = "Set ANTHROPIC_ADMIN_KEY (there is no terminal to ask on).";
|
|
68
|
+
export declare const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tier.";
|
|
69
|
+
/**
|
|
70
|
+
* Tier 1: read measured usage with the CTO's Anthropic admin key, locally. The key comes from
|
|
71
|
+
* ANTHROPIC_ADMIN_KEY or a masked prompt (--anthropic-admin-key; never a value on argv), lives only in this
|
|
72
|
+
* call, and goes only to api.anthropic.com. Returns null for "no key" and for every failure, after printing a
|
|
73
|
+
* fixed sentence: the key, the thrown value and the response never reach the terminal. The caller then falls
|
|
74
|
+
* back to tier 2, then tier 3.
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveMeasuredUsage(o: ScanCommandOptions, period: {
|
|
77
|
+
start: string;
|
|
78
|
+
end: string;
|
|
79
|
+
}): Promise<{
|
|
80
|
+
usd: number;
|
|
81
|
+
over200kShare: number | null;
|
|
82
|
+
} | null>;
|
|
83
|
+
/**
|
|
84
|
+
* --org is optional only because it can be inferred: the report is emailed on every scan, so the scan
|
|
85
|
+
* needs an org. Inference uses the signed-in user's own memberships (GET /v1/orgs, personal org
|
|
86
|
+
* excluded): exactly one org is used; none, several, or a failed read stops the scan loudly BEFORE any
|
|
87
|
+
* scan work, naming the fix. It never guesses between orgs and never skips the email.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveScanOrg(o: ScanCommandOptions): Promise<string>;
|
|
90
|
+
export declare const INVALID_ORG_MSG = "--org must be an org id like org-1a2b3c4d.";
|
|
91
|
+
/**
|
|
92
|
+
* The Record read for the preview: null when the flag gate is off (no read, no output, as before).
|
|
93
|
+
* With the gate on and no --org it is a note, not a guess. 404 becomes a note; 401/403 throw
|
|
94
|
+
* RecordAuthError (fail closed); every other failure becomes a fixed note. Nothing is stored.
|
|
95
|
+
*/
|
|
96
|
+
export declare function resolveRecordOutcome(o: ScanCommandOptions): Promise<RecordOutcome | "no_org" | null>;
|
|
97
|
+
export declare function scanCommand(o: ScanCommandOptions): Promise<void>;
|