@mnemom/mnemom 0.17.32-next.1 → 0.17.32-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent.d.ts +9 -0
- package/dist/commands/agent.js +167 -67
- package/dist/durable/adopt-api.d.ts +37 -0
- package/dist/durable/adopt-api.js +106 -0
- package/dist/durable/adopt-live.d.ts +17 -0
- package/dist/durable/adopt-live.js +152 -0
- package/dist/durable/adopt.d.ts +305 -0
- package/dist/durable/adopt.js +679 -0
- package/dist/durable/cwd-rule.d.ts +65 -0
- package/dist/durable/cwd-rule.js +111 -0
- package/dist/durable/discover.d.ts +68 -0
- package/dist/durable/discover.js +123 -0
- package/dist/durable/gate.d.ts +8 -0
- package/dist/durable/gate.js +13 -0
- package/dist/durable/launch-spec.d.ts +137 -0
- package/dist/durable/launch-spec.js +277 -0
- package/dist/durable/proc.d.ts +86 -0
- package/dist/durable/proc.js +279 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +52 -1
- package/dist/lib/agent.d.ts +7 -0
- package/dist/lib/agent.js +26 -0
- package/dist/lib/api.d.ts +32 -0
- package/dist/lib/api.js +16 -0
- package/dist/lib/experimental.d.ts +1 -1
- package/dist/lib/experimental.js +13 -0
- package/dist/lib/record-org.d.ts +27 -0
- package/dist/lib/record-org.js +44 -0
- package/dist/lib/session-key.d.ts +68 -0
- package/dist/lib/session-key.js +101 -0
- package/dist/lib/statusline.d.ts +13 -0
- package/dist/lib/statusline.js +27 -3
- package/dist/lib/subscription-login.d.ts +1 -1
- package/dist/lib/subscription-login.js +4 -1
- package/dist/vendor/durable-contract/index.js +5663 -0
- package/package.json +5 -4
package/dist/commands/agent.d.ts
CHANGED
|
@@ -367,6 +367,15 @@ export declare function prepareStatuslineFeed(o: {
|
|
|
367
367
|
}): {
|
|
368
368
|
feedFile: string;
|
|
369
369
|
} | null;
|
|
370
|
+
/**
|
|
371
|
+
* Whether the launch reads or writes Record, and so needs the served agent's org: the conduct
|
|
372
|
+
* receipt, commit reporting and the fed status line each key on it (lib/record-org).
|
|
373
|
+
*/
|
|
374
|
+
export declare function recordOrgWanted(o: {
|
|
375
|
+
receipt: boolean;
|
|
376
|
+
commitReport: boolean;
|
|
377
|
+
fedStatusline: boolean;
|
|
378
|
+
}): boolean;
|
|
370
379
|
/**
|
|
371
380
|
* Whether this launch feeds the status line measured model cost and μ (W1.4): prerelease
|
|
372
381
|
* builds only (stable stays "mnemom", decision 14), and only when the turn proxy runs, since
|
package/dist/commands/agent.js
CHANGED
|
@@ -31,13 +31,15 @@ import { 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";
|
|
34
|
-
import { FEED_ENV, statuslineLaunch } from "../lib/statusline.js";
|
|
34
|
+
import { FEED_ENV, launchStatusLine, statuslineLaunch } from "../lib/statusline.js";
|
|
35
35
|
import { startFeed } from "../lib/statusline-feed.js";
|
|
36
36
|
import { isPrerelease } from "../lib/update/semver.js";
|
|
37
37
|
import { attributionSettings, COMMIT_REPORT_ENV, commitReportEnabled, realGit, startCommitReporter, } from "../lib/commit-report.js";
|
|
38
38
|
import { ALLOW_AUTOCOMPACT_OFF_ENV, dropAutocompactOffEnv, hasOwnSettingsFlag, mergedSettingsArgs, planAutocompactGuard, } from "../lib/autocompact-guard.js";
|
|
39
39
|
import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
40
40
|
import { createApiKey } from "../lib/api.js";
|
|
41
|
+
import { recordOrgFor } from "../lib/record-org.js";
|
|
42
|
+
import { defaultSessionKeyDeps, mintSessionGatewayKey, revokeSessionGatewayKey, } from "../lib/session-key.js";
|
|
41
43
|
import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, sayRetiredConsoleNotice, readMnemomKey, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
|
|
42
44
|
import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, resolveRetentionDays, resumedContextProfile, toLedgerContract, } from "../lib/agent-ledger.js";
|
|
43
45
|
import { isLoggedIn } from "../lib/auth.js";
|
|
@@ -50,7 +52,7 @@ import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
|
50
52
|
import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, } from "../lib/login-detect.js";
|
|
51
53
|
import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
|
|
52
54
|
import { defaultAgentSlug, ensureClaimedServedAgent, ensureSessionAndBalance, isQuietPreambleExit, readComposedProtection, resolveDefaultSlug, resolvePinnedAgent, resolvePreflightedAccount, } from "./agent-setup.js";
|
|
53
|
-
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, assertOffProfileCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, resolveUsageSavings, HANDOFF_RC_KEY_ENV_NAME, HANDOFF_RC_MNEMOM_KEY_ENV_NAME, malformedAgentIdMessage, parseAgentRef, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, passthroughModel, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, liftPassthroughName, passthroughName, sanitizeAgentName, resumeCommand, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
|
|
55
|
+
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, assertOffProfileCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, resolveUsageSavings, HANDOFF_RC_KEY_ENV_NAME, HANDOFF_RC_MNEMOM_KEY_ENV_NAME, malformedAgentIdMessage, parseAgentRef, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, passthroughModel, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, liftPassthroughName, passthroughName, sanitizeAgentName, resumeCommand, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, stripOwnerCredentialEnv, } from "../lib/agent.js";
|
|
54
56
|
import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
|
|
55
57
|
import { slackChannelArgs } from "../lib/slack-launch.js";
|
|
56
58
|
import { appendPluginDirsEnv, applyPluginEnv, pluginEnabled, pluginPlanLine, resolvePluginDir, } from "../lib/claude-plugin.js";
|
|
@@ -588,6 +590,33 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
588
590
|
// hosts many sessions in one process, so it records none and injects nothing.
|
|
589
591
|
const cc = shape === "server" ? { id: null, inject: false } : pickCcSessionId(options.passthrough ?? []);
|
|
590
592
|
const ccSessionId = cc.inject && cc.id ? cc.id : undefined;
|
|
593
|
+
// ── MNE-8245: the LAUNCHED session must never hold the owner's own Mnemom
|
|
594
|
+
// key ── whenever `mnemomKey` above is set it is the owner's own credential:
|
|
595
|
+
// a subscription launch's identity anchor, or (since #2291's safe core) the
|
|
596
|
+
// stored/pinning key an api-key launch also sends. It is used ONLY for the
|
|
597
|
+
// claim call already made, and for nothing after this point: replace it here with a session-scoped, agent-bound,
|
|
598
|
+
// 24h `['gateway']`-only key minted fresh for THIS launch (MNE-8244's
|
|
599
|
+
// session-keys route), via the CLI PROCESS's own ambient auth — never
|
|
600
|
+
// anything read back out of the launched session. Minting is itself
|
|
601
|
+
// owner-gated server-side (a session key can never mint a broader one),
|
|
602
|
+
// so this closes the loop even if a future bug reintroduced a leak.
|
|
603
|
+
//
|
|
604
|
+
// FAILS CLOSED: `mintSessionGatewayKey` throws on ANY problem (network,
|
|
605
|
+
// non-2xx, malformed body, or scopes/kind not exactly a session-only
|
|
606
|
+
// `['gateway']` key); that throw is NOT caught here, so it propagates to
|
|
607
|
+
// index.ts's top-level handler, which prints the message and exits
|
|
608
|
+
// non-zero before anything is spawned. There is no fallback path back to
|
|
609
|
+
// `mnemomKey`'s original (owner) value.
|
|
610
|
+
let sessionKeyId;
|
|
611
|
+
if (mnemomKey) {
|
|
612
|
+
const minted = await mintSessionGatewayKey(defaultSessionKeyDeps(say), {
|
|
613
|
+
agentId: servedAgentId,
|
|
614
|
+
orgId: account.orgId,
|
|
615
|
+
sessionId: cc.id ?? undefined,
|
|
616
|
+
});
|
|
617
|
+
mnemomKey = minted.key;
|
|
618
|
+
sessionKeyId = minted.keyId;
|
|
619
|
+
}
|
|
591
620
|
// ── 1M context window by default ── the model this launch runs on gets `[1m]` when it has no
|
|
592
621
|
// window suffix and supports 1M (resolveWindowModel); an explicit choice — the
|
|
593
622
|
// CLAUDE_CODE_DISABLE_1M_CONTEXT opt-out, a passthrough `-- --model`, a suffixed id — is kept.
|
|
@@ -706,6 +735,16 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
706
735
|
}
|
|
707
736
|
}
|
|
708
737
|
if (shape === "terminal") {
|
|
738
|
+
// Record keys the lineage under the served agent's org, not the account org (lib/record-org).
|
|
739
|
+
const recordOrgId = recordOrgWanted({
|
|
740
|
+
receipt: experimental.includes("record"),
|
|
741
|
+
commitReport: cli.name === "claude" && commitReportEnabled(process.env[COMMIT_REPORT_ENV], CLI_VERSION),
|
|
742
|
+
fedStatusline: cli.name === "claude" && fedStatuslineWanted(CLI_VERSION, Boolean(goalMode), process.env),
|
|
743
|
+
}) && account.orgId
|
|
744
|
+
? await recordOrgFor(servedAgentId, account.orgId, {
|
|
745
|
+
onFallback: (reason) => say(fmt.warn(`mnemom agent: record: could not read ${servedAgentId}'s org (${reason}); using ${account.orgId}, which may hold none of this session's record.`)),
|
|
746
|
+
})
|
|
747
|
+
: account.orgId;
|
|
709
748
|
launchTerminal({
|
|
710
749
|
cli,
|
|
711
750
|
door,
|
|
@@ -723,6 +762,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
723
762
|
yolo,
|
|
724
763
|
contextHint,
|
|
725
764
|
ccSessionId,
|
|
765
|
+
sessionKeyId,
|
|
726
766
|
handoffMcpConfigPath,
|
|
727
767
|
resumeHint: (id) => resumeHintLine({
|
|
728
768
|
scenario,
|
|
@@ -735,9 +775,9 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
735
775
|
knownCcSessionId: cc.id ?? continuedCcSessionId,
|
|
736
776
|
plugin,
|
|
737
777
|
receipt: experimental.includes("record")
|
|
738
|
-
? { orgId:
|
|
778
|
+
? { orgId: recordOrgId, agentId: servedAgentId }
|
|
739
779
|
: undefined,
|
|
740
|
-
orgId:
|
|
780
|
+
orgId: recordOrgId,
|
|
741
781
|
commitReport: cli.name === "claude" &&
|
|
742
782
|
Boolean(account.orgId) &&
|
|
743
783
|
commitReportEnabled(process.env[COMMIT_REPORT_ENV], CLI_VERSION),
|
|
@@ -762,6 +802,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
762
802
|
options: launchOptions,
|
|
763
803
|
yolo,
|
|
764
804
|
ccSessionId,
|
|
805
|
+
sessionKeyId,
|
|
765
806
|
});
|
|
766
807
|
}
|
|
767
808
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -1260,6 +1301,12 @@ function launchTerminal(ctx) {
|
|
|
1260
1301
|
else {
|
|
1261
1302
|
childEnv = subscriptionChildEnv(process.env, ctx.door, headers);
|
|
1262
1303
|
}
|
|
1304
|
+
// MNE-8245 defense-in-depth: the owner's own Mnemom credential must never ride
|
|
1305
|
+
// through to the session just because it happened to sit in the CLI's OWN
|
|
1306
|
+
// process env (a shell MNEMOM_API_KEY / MNEMOM_TOKEN export, or a leftover
|
|
1307
|
+
// MNEMOM_AGENT_MNEMOM_KEY) — the session's only Mnemom credential is the
|
|
1308
|
+
// minted key already spliced into `headers` above (ctx.mnemomKey).
|
|
1309
|
+
stripOwnerCredentialEnv(childEnv);
|
|
1263
1310
|
// Both auth arms point ANTHROPIC_BASE_URL at the gateway door, which makes Claude Code turn
|
|
1264
1311
|
// tool search off and inline every tool schema on every request — unless told otherwise.
|
|
1265
1312
|
applyToolSearchDefault(childEnv);
|
|
@@ -1301,7 +1348,8 @@ function launchTerminal(ctx) {
|
|
|
1301
1348
|
const ownSettings = hasOwnSettingsFlag(passthrough);
|
|
1302
1349
|
const guard = applyAutocompactGuard(childEnv, ctx.options, !ownSettings);
|
|
1303
1350
|
const extra = launchSettingsExtra({
|
|
1304
|
-
|
|
1351
|
+
// Fed: a timed redraw, so totals that land after the last turn still reach the screen.
|
|
1352
|
+
statusline: launchStatusLine(sl?.settings ?? null, feed !== null),
|
|
1305
1353
|
commitReport: Boolean(ctx.commitReport),
|
|
1306
1354
|
ownSettings,
|
|
1307
1355
|
conversationId: ctx.conversationId,
|
|
@@ -1332,24 +1380,30 @@ function launchTerminal(ctx) {
|
|
|
1332
1380
|
process.exit(1);
|
|
1333
1381
|
});
|
|
1334
1382
|
child.on("exit", (code, signal) => {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1383
|
+
void (async () => {
|
|
1384
|
+
// MNE-8245: best-effort revoke of the minted session-scoped key — a failure
|
|
1385
|
+
// here is non-fatal (the key is 24h-TTL'd regardless); never blocks exit.
|
|
1386
|
+
if (ctx.sessionKeyId) {
|
|
1387
|
+
await revokeSessionGatewayKey(defaultSessionKeyDeps(say), ctx.sessionKeyId);
|
|
1388
|
+
}
|
|
1389
|
+
if (signal) {
|
|
1390
|
+
process.kill(process.pid, signal);
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
// The conduct receipt and the last commit sweep run side by side, each under its own
|
|
1394
|
+
// hard deadline inside W1.3's exit budget; neither changes the exit code.
|
|
1395
|
+
const swept = commits?.finish().catch(() => undefined);
|
|
1396
|
+
if (ctx.receipt)
|
|
1397
|
+
await sayConductReceipt({ ...ctx.receipt, statePath: recordStatePath });
|
|
1398
|
+
await swept;
|
|
1399
|
+
// Claude Code's own last line is "Resume this session with: claude --resume <id>",
|
|
1400
|
+
// which reopens it OUTSIDE Mnemom (ungoverned, unbilled). Ours goes after it, last.
|
|
1401
|
+
if (code === 0 && ctx.resumeHint && wantsResumeHint(ctx.cli, args)) {
|
|
1402
|
+
const id = ctx.knownCcSessionId ?? newestCcSessionIn(process.cwd(), process.env);
|
|
1403
|
+
say(ctx.resumeHint(id));
|
|
1404
|
+
}
|
|
1405
|
+
process.exit(code ?? 0);
|
|
1406
|
+
})();
|
|
1353
1407
|
});
|
|
1354
1408
|
};
|
|
1355
1409
|
// Implicit goal mode: put the local base-URL proxy in front of the door so each main request
|
|
@@ -1428,6 +1482,13 @@ export function prepareStatuslineFeed(o) {
|
|
|
1428
1482
|
o.childEnv[FEED_ENV] = feed.feedFile;
|
|
1429
1483
|
return feed;
|
|
1430
1484
|
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Whether the launch reads or writes Record, and so needs the served agent's org: the conduct
|
|
1487
|
+
* receipt, commit reporting and the fed status line each key on it (lib/record-org).
|
|
1488
|
+
*/
|
|
1489
|
+
export function recordOrgWanted(o) {
|
|
1490
|
+
return o.receipt || o.commitReport || o.fedStatusline;
|
|
1491
|
+
}
|
|
1431
1492
|
/**
|
|
1432
1493
|
* Whether this launch feeds the status line measured model cost and μ (W1.4): prerelease
|
|
1433
1494
|
* builds only (stable stays "mnemom", decision 14), and only when the turn proxy runs, since
|
|
@@ -1485,16 +1546,19 @@ export function startBaseUrlProxy(door, done, opts = {}) {
|
|
|
1485
1546
|
// The receipt's record state (lineage key, record URL: non-secret) lives beside the log and is
|
|
1486
1547
|
// removed with it on exit, after the receipt has read it.
|
|
1487
1548
|
const recordStatePath = opts.recordState ? join(logDir, "record.json") : undefined;
|
|
1549
|
+
const proxyEnv = {
|
|
1550
|
+
...process.env,
|
|
1551
|
+
MNEMOM_AGENT_BU_DOOR: door,
|
|
1552
|
+
MNEMOM_AGENT_BU_CWD: process.cwd(),
|
|
1553
|
+
MNEMOM_AGENT_BU_LOG: process.env.MNEMOM_AGENT_TURN_PROXY_LOG || join(logDir, "proxy.log"),
|
|
1554
|
+
MNEMOM_AGENT_BU_PARENT_PID: String(process.pid),
|
|
1555
|
+
...(opts.turnHeader === false ? { MNEMOM_AGENT_TURN_HEADER: "0" } : {}),
|
|
1556
|
+
...(recordStatePath ? { MNEMOM_AGENT_BU_RECORD_STATE: recordStatePath } : {}),
|
|
1557
|
+
};
|
|
1558
|
+
// MNE-8245 defense-in-depth: the proxy reads none of the owner's own Mnemom credential vars.
|
|
1559
|
+
stripOwnerCredentialEnv(proxyEnv);
|
|
1488
1560
|
const proxy = spawn(opts.node ?? process.execPath, [script], {
|
|
1489
|
-
env:
|
|
1490
|
-
...process.env,
|
|
1491
|
-
MNEMOM_AGENT_BU_DOOR: door,
|
|
1492
|
-
MNEMOM_AGENT_BU_CWD: process.cwd(),
|
|
1493
|
-
MNEMOM_AGENT_BU_LOG: process.env.MNEMOM_AGENT_TURN_PROXY_LOG || join(logDir, "proxy.log"),
|
|
1494
|
-
MNEMOM_AGENT_BU_PARENT_PID: String(process.pid),
|
|
1495
|
-
...(opts.turnHeader === false ? { MNEMOM_AGENT_TURN_HEADER: "0" } : {}),
|
|
1496
|
-
...(recordStatePath ? { MNEMOM_AGENT_BU_RECORD_STATE: recordStatePath } : {}),
|
|
1497
|
-
},
|
|
1561
|
+
env: proxyEnv,
|
|
1498
1562
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1499
1563
|
});
|
|
1500
1564
|
process.on("exit", () => {
|
|
@@ -1566,27 +1630,28 @@ function launchRemoteControl(ctx) {
|
|
|
1566
1630
|
const rcDir = mkdtempSync(join(rcStateRoot(), "mnemom-code-rc."));
|
|
1567
1631
|
writeFileSync(join(rcDir, "owner.pid"), `${process.pid}\n`, { mode: 0o600 }); // rc-state.mjs sweep
|
|
1568
1632
|
const proxyLog = process.env.MNEMOM_AGENT_RC_PROXY_LOG || join(rcDir, "proxy.log");
|
|
1633
|
+
const proxyEnv = {
|
|
1634
|
+
...process.env,
|
|
1635
|
+
MNEMOM_AGENT_RC_DOOR: ctx.door,
|
|
1636
|
+
MNEMOM_AGENT_RC_AGENT: ctx.agent,
|
|
1637
|
+
MNEMOM_AGENT_RC_AGENT_ID: ctx.agentId ?? "",
|
|
1638
|
+
MNEMOM_AGENT_RC_CONVERSATION_ID: ctx.conversationId,
|
|
1639
|
+
MNEMOM_AGENT_RC_CONTRACT_B64: ctx.envelope?.base64 ?? "",
|
|
1640
|
+
MNEMOM_AGENT_RC_GOAL_MODE: ctx.goalMode ?? "",
|
|
1641
|
+
MNEMOM_AGENT_RC_SESSION_CONFIG: ctx.sessionConfigB64 ?? "",
|
|
1642
|
+
MNEMOM_AGENT_RC_PROXY_LOG: proxyLog,
|
|
1643
|
+
MNEMOM_AGENT_RC_PARENT_PID: String(process.pid),
|
|
1644
|
+
MNEMOM_AGENT_RC_AUTH: ctx.anthropicKey ? "api-key" : "subscription",
|
|
1645
|
+
};
|
|
1646
|
+
// MNE-8245 defense-in-depth: the proxy gets the session's (minted) Mnemom key on stdin only
|
|
1647
|
+
// and reads none of the owner's own Mnemom credential vars, so they never ride into its env.
|
|
1648
|
+
stripOwnerCredentialEnv(proxyEnv);
|
|
1569
1649
|
const proxy = spawn("node", [proxyScript], {
|
|
1570
|
-
env:
|
|
1571
|
-
...process.env,
|
|
1572
|
-
MNEMOM_AGENT_RC_DOOR: ctx.door,
|
|
1573
|
-
MNEMOM_AGENT_RC_AGENT: ctx.agent,
|
|
1574
|
-
MNEMOM_AGENT_RC_AGENT_ID: ctx.agentId ?? "",
|
|
1575
|
-
MNEMOM_AGENT_RC_CONVERSATION_ID: ctx.conversationId,
|
|
1576
|
-
MNEMOM_AGENT_RC_CONTRACT_B64: ctx.envelope?.base64 ?? "",
|
|
1577
|
-
MNEMOM_AGENT_RC_GOAL_MODE: ctx.goalMode ?? "",
|
|
1578
|
-
MNEMOM_AGENT_RC_SESSION_CONFIG: ctx.sessionConfigB64 ?? "",
|
|
1579
|
-
MNEMOM_AGENT_RC_PROXY_LOG: proxyLog,
|
|
1580
|
-
MNEMOM_AGENT_RC_PARENT_PID: String(process.pid),
|
|
1581
|
-
MNEMOM_AGENT_RC_AUTH: ctx.anthropicKey ? "api-key" : "subscription",
|
|
1582
|
-
},
|
|
1650
|
+
env: proxyEnv,
|
|
1583
1651
|
stdio: ["pipe", "pipe", "inherit"],
|
|
1584
1652
|
});
|
|
1585
1653
|
let stopped = false;
|
|
1586
|
-
const
|
|
1587
|
-
if (stopped)
|
|
1588
|
-
return;
|
|
1589
|
-
stopped = true;
|
|
1654
|
+
const stopProxyAndTempDir = () => {
|
|
1590
1655
|
if (!proxy.killed) {
|
|
1591
1656
|
try {
|
|
1592
1657
|
proxy.kill("SIGTERM");
|
|
@@ -1602,7 +1667,32 @@ function launchRemoteControl(ctx) {
|
|
|
1602
1667
|
/* best effort */
|
|
1603
1668
|
}
|
|
1604
1669
|
};
|
|
1605
|
-
|
|
1670
|
+
// MNE-8245 review-fix: this MUST be awaited by every call site below, and exit/re-signal must
|
|
1671
|
+
// happen only after it settles — a fire-and-forget `void revokeSessionGatewayKey(...)`
|
|
1672
|
+
// immediately followed by `process.exit()`/`process.kill()` never lets the revoke's fetch even
|
|
1673
|
+
// start before the process dies. revokeSessionGatewayKey bounds itself internally (~3s via
|
|
1674
|
+
// AbortSignal.timeout in lib/session-key.ts), so awaiting it here cannot hang the exit path.
|
|
1675
|
+
const onExit = async () => {
|
|
1676
|
+
if (stopped)
|
|
1677
|
+
return;
|
|
1678
|
+
stopped = true;
|
|
1679
|
+
stopProxyAndTempDir();
|
|
1680
|
+
if (ctx.sessionKeyId) {
|
|
1681
|
+
await revokeSessionGatewayKey(defaultSessionKeyDeps(say), ctx.sessionKeyId);
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
// Last-resort SYNC fallback only — Node's "exit" handlers cannot await, so this branch never
|
|
1685
|
+
// revokes the key (the 24h TTL covers it). It exists solely to guarantee the proxy child and
|
|
1686
|
+
// temp dir are cleaned up on an exit path that didn't go through (and await) onExit() above,
|
|
1687
|
+
// e.g. an unexpected top-level throw. Every normal exit path in this function calls onExit()
|
|
1688
|
+
// directly and awaits it before exiting, so this is a no-op on the common paths (`stopped` is
|
|
1689
|
+
// already true by the time it fires).
|
|
1690
|
+
process.on("exit", () => {
|
|
1691
|
+
if (stopped)
|
|
1692
|
+
return;
|
|
1693
|
+
stopped = true;
|
|
1694
|
+
stopProxyAndTempDir();
|
|
1695
|
+
});
|
|
1606
1696
|
// Secrets on stdin — three lines (anthropic, mnemom, openai). An api-key launch sends the
|
|
1607
1697
|
// Anthropic key, plus this machine's Mnemom key when it has one (the key principal; always
|
|
1608
1698
|
// with a pin); a subscription launch sends the Mnemom key only (the identity anchor — the
|
|
@@ -1614,21 +1704,24 @@ function launchRemoteControl(ctx) {
|
|
|
1614
1704
|
if (err.code === "EPIPE")
|
|
1615
1705
|
return;
|
|
1616
1706
|
say(`mnemom agent: could not hand the secrets to the Remote Control proxy: ${err.message}`);
|
|
1617
|
-
onExit();
|
|
1618
|
-
process.exit(1);
|
|
1707
|
+
void onExit().finally(() => process.exit(1));
|
|
1619
1708
|
});
|
|
1620
1709
|
proxy.stdin?.end(rcProxyStdin(ctx));
|
|
1621
1710
|
let stdoutBuf = "";
|
|
1622
1711
|
const start = Date.now();
|
|
1623
1712
|
const tryStart = () => {
|
|
1713
|
+
// Cleanup already ran (a refusal, or the exit fallback reaped the proxy): stop polling, so a
|
|
1714
|
+
// proxy killed by a signal (exitCode stays null) never reaches the 15s refusal afterwards.
|
|
1715
|
+
if (stopped)
|
|
1716
|
+
return;
|
|
1624
1717
|
const m = stdoutBuf.match(/^READY (\d+) (.+)$/m);
|
|
1625
1718
|
if (m) {
|
|
1626
1719
|
const port = m[1];
|
|
1627
1720
|
const ca = m[2].trim();
|
|
1628
1721
|
if (!existsSync(ca)) {
|
|
1629
1722
|
say("mnemom agent: malformed READY line from the Remote Control proxy.");
|
|
1630
|
-
onExit();
|
|
1631
|
-
|
|
1723
|
+
void onExit().finally(() => process.exit(1));
|
|
1724
|
+
return;
|
|
1632
1725
|
}
|
|
1633
1726
|
spawnRcCli({ ...ctx, port, ca, onExit });
|
|
1634
1727
|
return;
|
|
@@ -1636,13 +1729,13 @@ function launchRemoteControl(ctx) {
|
|
|
1636
1729
|
if (proxy.exitCode !== null) {
|
|
1637
1730
|
say("mnemom agent: the Remote Control proxy exited before it was ready — refusing to launch un-routed.");
|
|
1638
1731
|
redactedProxyLog(proxyLog);
|
|
1639
|
-
onExit();
|
|
1640
|
-
|
|
1732
|
+
void onExit().finally(() => process.exit(1));
|
|
1733
|
+
return;
|
|
1641
1734
|
}
|
|
1642
1735
|
if (Date.now() - start > 15_000) {
|
|
1643
1736
|
say("mnemom agent: the Remote Control proxy did not report READY within 15s.");
|
|
1644
|
-
onExit();
|
|
1645
|
-
|
|
1737
|
+
void onExit().finally(() => process.exit(1));
|
|
1738
|
+
return;
|
|
1646
1739
|
}
|
|
1647
1740
|
setTimeout(tryStart, 200);
|
|
1648
1741
|
};
|
|
@@ -1651,8 +1744,7 @@ function launchRemoteControl(ctx) {
|
|
|
1651
1744
|
});
|
|
1652
1745
|
proxy.on("error", (err) => {
|
|
1653
1746
|
say(`mnemom agent: failed to launch the Remote Control proxy: ${err.message}`);
|
|
1654
|
-
onExit();
|
|
1655
|
-
process.exit(1);
|
|
1747
|
+
void onExit().finally(() => process.exit(1));
|
|
1656
1748
|
});
|
|
1657
1749
|
tryStart();
|
|
1658
1750
|
}
|
|
@@ -1683,6 +1775,11 @@ function spawnRcCli(ctx) {
|
|
|
1683
1775
|
if (!strip.has(k))
|
|
1684
1776
|
childEnv[k] = v;
|
|
1685
1777
|
}
|
|
1778
|
+
// MNE-8245 defense-in-depth: see the identical call in launchTerminal — the owner's own
|
|
1779
|
+
// Mnemom credential must never ride through just because it sits in the CLI's own process
|
|
1780
|
+
// env. The RC gate forwards the Mnemom identity via the proxy's own auth plane, never from
|
|
1781
|
+
// this child's env, so there is no legitimate use for these vars here either.
|
|
1782
|
+
stripOwnerCredentialEnv(childEnv);
|
|
1686
1783
|
childEnv.HTTPS_PROXY = `http://127.0.0.1:${ctx.port}`;
|
|
1687
1784
|
childEnv.https_proxy = `http://127.0.0.1:${ctx.port}`;
|
|
1688
1785
|
childEnv.NODE_EXTRA_CA_CERTS = ctx.ca;
|
|
@@ -1764,15 +1861,18 @@ function spawnRcCli(ctx) {
|
|
|
1764
1861
|
const child = spawn(ctx.cli.bin, args, { stdio: "inherit", env: childEnv });
|
|
1765
1862
|
child.on("error", (err) => {
|
|
1766
1863
|
say(`mnemom agent: failed to launch ${ctx.cli.name} under Remote Control: ${err.message}`);
|
|
1767
|
-
ctx.onExit();
|
|
1768
|
-
process.exit(1);
|
|
1864
|
+
void ctx.onExit().finally(() => process.exit(1));
|
|
1769
1865
|
});
|
|
1770
1866
|
child.on("exit", (code, signal) => {
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1867
|
+
// MNE-8245 review-fix: await the (internally bounded) revoke before exiting/re-signaling —
|
|
1868
|
+
// the previous fire-and-forget `ctx.onExit()` immediately followed by exit/kill never let the
|
|
1869
|
+
// revoke's fetch start before the process died, so it silently never ran on this path.
|
|
1870
|
+
void ctx.onExit().finally(() => {
|
|
1871
|
+
if (signal)
|
|
1872
|
+
process.kill(process.pid, signal);
|
|
1873
|
+
else
|
|
1874
|
+
process.exit(code ?? 0);
|
|
1875
|
+
});
|
|
1776
1876
|
});
|
|
1777
1877
|
}
|
|
1778
1878
|
function warnServerDrops(options, yolo) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The A2 calls `sessions adopt` makes (BACKBONE A2: /v1/agent/sessions/{cid} get and lock).
|
|
3
|
+
*
|
|
4
|
+
* - getSession: GET /v1/agent/sessions/{cid}?agent_id&org_id (the existing owner read, which A2
|
|
5
|
+
* extends with durable_mode, state, effective_state and cc_session_id).
|
|
6
|
+
* - lock: POST S1 ROUTES.own_lock with the closed S1 LockBody, under the caller's
|
|
7
|
+
* Idempotency-Key (one per handover, reused on every retry) and LOCK_HTTP_TIMEOUT_MS.
|
|
8
|
+
* - lockCheck (PRECHECK): C14-R25 step 1 runs the R17 sharing and R18 lock-precondition checks
|
|
9
|
+
* before any SIGSTOP, and the C14 cases assert no request to a path ending in /lock for a
|
|
10
|
+
* refused group. S1 freezes no non-mutating check route, so adopt asks
|
|
11
|
+
* POST /v1/agent/sessions/{cid}/lock-preconditions with the same LockBody (an A2 assumption,
|
|
12
|
+
* written up in the w1 gaps). A 404/405 with no durable code means the route is not served;
|
|
13
|
+
* adopt then fails closed: the group is refused before any signal.
|
|
14
|
+
*
|
|
15
|
+
* Every answer is an ApiAnswer; nothing here throws on HTTP status.
|
|
16
|
+
*/
|
|
17
|
+
import type { ApiAnswer, AdoptApi } from "./adopt.js";
|
|
18
|
+
export declare const PRECHECK_SUFFIX = "/lock-preconditions";
|
|
19
|
+
export declare const ADOPT_HTTP_TIMEOUT_MS = 30000;
|
|
20
|
+
/**
|
|
21
|
+
* The lock request's own timeout: below the api's 30 s window after which a pending
|
|
22
|
+
* Idempotency-Key reservation is taken over by the next call with that key. A same-key retry sent
|
|
23
|
+
* after a lock timeout therefore reaches the api while the first call's reservation still holds,
|
|
24
|
+
* and is answered 409 idempotency_key_in_flight (still pending) or with the replayed 2xx.
|
|
25
|
+
*/
|
|
26
|
+
export declare const LOCK_HTTP_TIMEOUT_MS = 20000;
|
|
27
|
+
export interface HttpDeps {
|
|
28
|
+
base: string;
|
|
29
|
+
/** Auth headers for the owner (JWT or the owner's API key). */
|
|
30
|
+
auth(): Promise<Record<string, string>>;
|
|
31
|
+
fetch(url: string, init: RequestInit): Promise<Response>;
|
|
32
|
+
idempotencyKey(): string;
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
}
|
|
35
|
+
/** A Response as an ApiAnswer: 2xx ok with its JSON body; else the error envelope's code and details. */
|
|
36
|
+
export declare function answerOf(res: Response): Promise<ApiAnswer>;
|
|
37
|
+
export declare function httpAdoptApi(deps: HttpDeps): AdoptApi;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The A2 calls `sessions adopt` makes (BACKBONE A2: /v1/agent/sessions/{cid} get and lock).
|
|
3
|
+
*
|
|
4
|
+
* - getSession: GET /v1/agent/sessions/{cid}?agent_id&org_id (the existing owner read, which A2
|
|
5
|
+
* extends with durable_mode, state, effective_state and cc_session_id).
|
|
6
|
+
* - lock: POST S1 ROUTES.own_lock with the closed S1 LockBody, under the caller's
|
|
7
|
+
* Idempotency-Key (one per handover, reused on every retry) and LOCK_HTTP_TIMEOUT_MS.
|
|
8
|
+
* - lockCheck (PRECHECK): C14-R25 step 1 runs the R17 sharing and R18 lock-precondition checks
|
|
9
|
+
* before any SIGSTOP, and the C14 cases assert no request to a path ending in /lock for a
|
|
10
|
+
* refused group. S1 freezes no non-mutating check route, so adopt asks
|
|
11
|
+
* POST /v1/agent/sessions/{cid}/lock-preconditions with the same LockBody (an A2 assumption,
|
|
12
|
+
* written up in the w1 gaps). A 404/405 with no durable code means the route is not served;
|
|
13
|
+
* adopt then fails closed: the group is refused before any signal.
|
|
14
|
+
*
|
|
15
|
+
* Every answer is an ApiAnswer; nothing here throws on HTTP status.
|
|
16
|
+
*/
|
|
17
|
+
import { routePath } from "../vendor/durable-contract/index.js";
|
|
18
|
+
export const PRECHECK_SUFFIX = "/lock-preconditions";
|
|
19
|
+
export const ADOPT_HTTP_TIMEOUT_MS = 30_000;
|
|
20
|
+
/**
|
|
21
|
+
* The lock request's own timeout: below the api's 30 s window after which a pending
|
|
22
|
+
* Idempotency-Key reservation is taken over by the next call with that key. A same-key retry sent
|
|
23
|
+
* after a lock timeout therefore reaches the api while the first call's reservation still holds,
|
|
24
|
+
* and is answered 409 idempotency_key_in_flight (still pending) or with the replayed 2xx.
|
|
25
|
+
*/
|
|
26
|
+
export const LOCK_HTTP_TIMEOUT_MS = 20_000;
|
|
27
|
+
/** A Response as an ApiAnswer: 2xx ok with its JSON body; else the error envelope's code and details. */
|
|
28
|
+
export async function answerOf(res) {
|
|
29
|
+
let body;
|
|
30
|
+
const text = await res.text();
|
|
31
|
+
try {
|
|
32
|
+
body = text === "" ? null : JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
body = text;
|
|
36
|
+
}
|
|
37
|
+
if (res.ok)
|
|
38
|
+
return { kind: "ok", status: res.status, body };
|
|
39
|
+
const b = (body && typeof body === "object" ? body : {});
|
|
40
|
+
const err = (b.error && typeof b.error === "object" ? b.error : b);
|
|
41
|
+
return {
|
|
42
|
+
kind: "error",
|
|
43
|
+
status: res.status,
|
|
44
|
+
code: typeof err.code === "string" ? err.code : null,
|
|
45
|
+
details: err.details ?? null,
|
|
46
|
+
message: typeof err.message === "string"
|
|
47
|
+
? err.message
|
|
48
|
+
: typeof b.error === "string"
|
|
49
|
+
? b.error
|
|
50
|
+
: `HTTP ${res.status}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function call(deps, path, init, timeoutMs = ADOPT_HTTP_TIMEOUT_MS) {
|
|
54
|
+
const ctl = new AbortController();
|
|
55
|
+
const timer = setTimeout(() => ctl.abort(), deps.timeoutMs ?? timeoutMs);
|
|
56
|
+
try {
|
|
57
|
+
const res = await deps.fetch(`${deps.base}${path}`, { ...init, signal: ctl.signal });
|
|
58
|
+
return await answerOf(res);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
if (ctl.signal.aborted)
|
|
62
|
+
return { kind: "timeout" };
|
|
63
|
+
return { kind: "network", message: e instanceof Error ? e.message : String(e) };
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function httpAdoptApi(deps) {
|
|
70
|
+
const post = async (path, body, key = deps.idempotencyKey(), timeoutMs) => call(deps, path, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: {
|
|
73
|
+
...(await deps.auth()),
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
"Idempotency-Key": key,
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify(body),
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
return {
|
|
80
|
+
async getSession(cid, facts) {
|
|
81
|
+
const q = new URLSearchParams();
|
|
82
|
+
if (facts.agentId)
|
|
83
|
+
q.set("agent_id", facts.agentId);
|
|
84
|
+
if (facts.orgId)
|
|
85
|
+
q.set("org_id", facts.orgId);
|
|
86
|
+
const qs = q.toString();
|
|
87
|
+
return call(deps, `/v1/agent/sessions/${encodeURIComponent(cid)}${qs ? `?${qs}` : ""}`, {
|
|
88
|
+
method: "GET",
|
|
89
|
+
headers: await deps.auth(),
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
lockCheck: (cid, body) => post(`/v1/agent/sessions/${encodeURIComponent(cid)}${PRECHECK_SUFFIX}`, body),
|
|
93
|
+
// One key per handover: the caller passes the same key to every retry of one lock.
|
|
94
|
+
async lock(cid, body, idempotencyKey) {
|
|
95
|
+
let path;
|
|
96
|
+
try {
|
|
97
|
+
path = routePath("own_lock", cid);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
// Never throw after the caller's SIGSTOP: an unroutable id is a refused commit.
|
|
101
|
+
return { kind: "error", status: 400, code: null, details: null, message: String(e) };
|
|
102
|
+
}
|
|
103
|
+
return post(path, body, idempotencyKey ?? deps.idempotencyKey(), LOCK_HTTP_TIMEOUT_MS);
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thin live driver for `sessions adopt`: real processes, files, HTTP and the terminal. The
|
|
3
|
+
* decisions live in adopt.ts (pure over AdoptDeps); this file only wires them to the machine.
|
|
4
|
+
*/
|
|
5
|
+
import { type ExecutorState, type LauncherKey } from "../vendor/durable-contract/index.js";
|
|
6
|
+
import { type SessionRecord } from "../lib/agent-ledger.js";
|
|
7
|
+
import type { AdoptDeps, LedgerFacts } from "./adopt.js";
|
|
8
|
+
/** The ledger facts for a conversation: its newest record. */
|
|
9
|
+
export declare function ledgerFactsFrom(records: readonly SessionRecord[], cid: string): LedgerFacts | null;
|
|
10
|
+
/** The launcher key `durable` (MNEMOM_AGENT_DURABLE, then launcher.toml), default off. */
|
|
11
|
+
export declare function launcherKeyFrom(env: Readonly<Record<string, string | undefined>>, tomlText: string | null): LauncherKey;
|
|
12
|
+
/** The executor state file, or null when absent or not the frozen S1 shape. */
|
|
13
|
+
export declare function executorStateFrom(text: string | null): ExecutorState | null;
|
|
14
|
+
export declare function authHeaders(): Promise<Record<string, string>>;
|
|
15
|
+
/** A [Y/n] answer: anything not starting with n is yes. */
|
|
16
|
+
export declare function answerIsYes(a: string): boolean;
|
|
17
|
+
export declare function liveAdoptDeps(): AdoptDeps;
|