@mnemom/mnemom 0.17.32-next.2 → 0.17.32-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent.js +174 -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/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.js
CHANGED
|
@@ -31,7 +31,7 @@ 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";
|
|
@@ -39,6 +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
43
|
import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, sayRetiredConsoleNotice, readMnemomKey, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
|
|
43
44
|
import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, resolveRetentionDays, resumedContextProfile, toLedgerContract, } from "../lib/agent-ledger.js";
|
|
44
45
|
import { isLoggedIn } from "../lib/auth.js";
|
|
@@ -51,7 +52,7 @@ import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
|
51
52
|
import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, } from "../lib/login-detect.js";
|
|
52
53
|
import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
|
|
53
54
|
import { defaultAgentSlug, ensureClaimedServedAgent, ensureSessionAndBalance, isQuietPreambleExit, readComposedProtection, resolveDefaultSlug, resolvePinnedAgent, resolvePreflightedAccount, } from "./agent-setup.js";
|
|
54
|
-
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";
|
|
55
56
|
import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
|
|
56
57
|
import { slackChannelArgs } from "../lib/slack-launch.js";
|
|
57
58
|
import { appendPluginDirsEnv, applyPluginEnv, pluginEnabled, pluginPlanLine, resolvePluginDir, } from "../lib/claude-plugin.js";
|
|
@@ -589,6 +590,56 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
589
590
|
// hosts many sessions in one process, so it records none and injects nothing.
|
|
590
591
|
const cc = shape === "server" ? { id: null, inject: false } : pickCcSessionId(options.passthrough ?? []);
|
|
591
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
|
+
//
|
|
611
|
+
// The mint names the served agent's org, not the account org: with an org API key the account
|
|
612
|
+
// org is the key owner's personal org, where the agent does not exist, so the API answers 404
|
|
613
|
+
// and the launch would fail closed (door1-path run 40400479127). Same read as Record's.
|
|
614
|
+
let sessionKeyId;
|
|
615
|
+
// One read of the served agent's org serves the mint and Record; each says why in its own
|
|
616
|
+
// words when the read fails and the account org is used instead.
|
|
617
|
+
let servedOrg;
|
|
618
|
+
const readServedOrg = (accountOrgId) =>
|
|
619
|
+
// Every caller passes account.orgId, so the first call's fallback holds for the rest.
|
|
620
|
+
(servedOrg ??= (async () => {
|
|
621
|
+
let fallback;
|
|
622
|
+
const orgId = await recordOrgFor(servedAgentId, accountOrgId, {
|
|
623
|
+
onFallback: (reason) => (fallback = reason),
|
|
624
|
+
});
|
|
625
|
+
return { orgId, fallback };
|
|
626
|
+
})());
|
|
627
|
+
if (mnemomKey) {
|
|
628
|
+
let mintOrgId = account.orgId;
|
|
629
|
+
if (account.orgId) {
|
|
630
|
+
const o = await readServedOrg(account.orgId);
|
|
631
|
+
if (o.fallback)
|
|
632
|
+
say(fmt.warn(`mnemom agent: could not read ${servedAgentId}'s org (${o.fallback}); minting the session key under ${account.orgId}.`));
|
|
633
|
+
mintOrgId = o.orgId;
|
|
634
|
+
}
|
|
635
|
+
const minted = await mintSessionGatewayKey(defaultSessionKeyDeps(say), {
|
|
636
|
+
agentId: servedAgentId,
|
|
637
|
+
orgId: mintOrgId,
|
|
638
|
+
sessionId: cc.id ?? undefined,
|
|
639
|
+
});
|
|
640
|
+
mnemomKey = minted.key;
|
|
641
|
+
sessionKeyId = minted.keyId;
|
|
642
|
+
}
|
|
592
643
|
// ── 1M context window by default ── the model this launch runs on gets `[1m]` when it has no
|
|
593
644
|
// window suffix and supports 1M (resolveWindowModel); an explicit choice — the
|
|
594
645
|
// CLAUDE_CODE_DISABLE_1M_CONTEXT opt-out, a passthrough `-- --model`, a suffixed id — is kept.
|
|
@@ -713,8 +764,10 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
713
764
|
commitReport: cli.name === "claude" && commitReportEnabled(process.env[COMMIT_REPORT_ENV], CLI_VERSION),
|
|
714
765
|
fedStatusline: cli.name === "claude" && fedStatuslineWanted(CLI_VERSION, Boolean(goalMode), process.env),
|
|
715
766
|
}) && account.orgId
|
|
716
|
-
? await
|
|
717
|
-
|
|
767
|
+
? await readServedOrg(account.orgId).then((o) => {
|
|
768
|
+
if (o.fallback)
|
|
769
|
+
say(fmt.warn(`mnemom agent: record: could not read ${servedAgentId}'s org (${o.fallback}); using ${account.orgId}, which may hold none of this session's record.`));
|
|
770
|
+
return o.orgId;
|
|
718
771
|
})
|
|
719
772
|
: account.orgId;
|
|
720
773
|
launchTerminal({
|
|
@@ -734,6 +787,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
734
787
|
yolo,
|
|
735
788
|
contextHint,
|
|
736
789
|
ccSessionId,
|
|
790
|
+
sessionKeyId,
|
|
737
791
|
handoffMcpConfigPath,
|
|
738
792
|
resumeHint: (id) => resumeHintLine({
|
|
739
793
|
scenario,
|
|
@@ -773,6 +827,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
773
827
|
options: launchOptions,
|
|
774
828
|
yolo,
|
|
775
829
|
ccSessionId,
|
|
830
|
+
sessionKeyId,
|
|
776
831
|
});
|
|
777
832
|
}
|
|
778
833
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -1271,6 +1326,12 @@ function launchTerminal(ctx) {
|
|
|
1271
1326
|
else {
|
|
1272
1327
|
childEnv = subscriptionChildEnv(process.env, ctx.door, headers);
|
|
1273
1328
|
}
|
|
1329
|
+
// MNE-8245 defense-in-depth: the owner's own Mnemom credential must never ride
|
|
1330
|
+
// through to the session just because it happened to sit in the CLI's OWN
|
|
1331
|
+
// process env (a shell MNEMOM_API_KEY / MNEMOM_TOKEN export, or a leftover
|
|
1332
|
+
// MNEMOM_AGENT_MNEMOM_KEY) — the session's only Mnemom credential is the
|
|
1333
|
+
// minted key already spliced into `headers` above (ctx.mnemomKey).
|
|
1334
|
+
stripOwnerCredentialEnv(childEnv);
|
|
1274
1335
|
// Both auth arms point ANTHROPIC_BASE_URL at the gateway door, which makes Claude Code turn
|
|
1275
1336
|
// tool search off and inline every tool schema on every request — unless told otherwise.
|
|
1276
1337
|
applyToolSearchDefault(childEnv);
|
|
@@ -1312,7 +1373,8 @@ function launchTerminal(ctx) {
|
|
|
1312
1373
|
const ownSettings = hasOwnSettingsFlag(passthrough);
|
|
1313
1374
|
const guard = applyAutocompactGuard(childEnv, ctx.options, !ownSettings);
|
|
1314
1375
|
const extra = launchSettingsExtra({
|
|
1315
|
-
|
|
1376
|
+
// Fed: a timed redraw, so totals that land after the last turn still reach the screen.
|
|
1377
|
+
statusline: launchStatusLine(sl?.settings ?? null, feed !== null),
|
|
1316
1378
|
commitReport: Boolean(ctx.commitReport),
|
|
1317
1379
|
ownSettings,
|
|
1318
1380
|
conversationId: ctx.conversationId,
|
|
@@ -1343,24 +1405,30 @@ function launchTerminal(ctx) {
|
|
|
1343
1405
|
process.exit(1);
|
|
1344
1406
|
});
|
|
1345
1407
|
child.on("exit", (code, signal) => {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1408
|
+
void (async () => {
|
|
1409
|
+
// MNE-8245: best-effort revoke of the minted session-scoped key — a failure
|
|
1410
|
+
// here is non-fatal (the key is 24h-TTL'd regardless); never blocks exit.
|
|
1411
|
+
if (ctx.sessionKeyId) {
|
|
1412
|
+
await revokeSessionGatewayKey(defaultSessionKeyDeps(say), ctx.sessionKeyId);
|
|
1413
|
+
}
|
|
1414
|
+
if (signal) {
|
|
1415
|
+
process.kill(process.pid, signal);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
// The conduct receipt and the last commit sweep run side by side, each under its own
|
|
1419
|
+
// hard deadline inside W1.3's exit budget; neither changes the exit code.
|
|
1420
|
+
const swept = commits?.finish().catch(() => undefined);
|
|
1421
|
+
if (ctx.receipt)
|
|
1422
|
+
await sayConductReceipt({ ...ctx.receipt, statePath: recordStatePath });
|
|
1423
|
+
await swept;
|
|
1424
|
+
// Claude Code's own last line is "Resume this session with: claude --resume <id>",
|
|
1425
|
+
// which reopens it OUTSIDE Mnemom (ungoverned, unbilled). Ours goes after it, last.
|
|
1426
|
+
if (code === 0 && ctx.resumeHint && wantsResumeHint(ctx.cli, args)) {
|
|
1427
|
+
const id = ctx.knownCcSessionId ?? newestCcSessionIn(process.cwd(), process.env);
|
|
1428
|
+
say(ctx.resumeHint(id));
|
|
1429
|
+
}
|
|
1430
|
+
process.exit(code ?? 0);
|
|
1431
|
+
})();
|
|
1364
1432
|
});
|
|
1365
1433
|
};
|
|
1366
1434
|
// Implicit goal mode: put the local base-URL proxy in front of the door so each main request
|
|
@@ -1503,16 +1571,19 @@ export function startBaseUrlProxy(door, done, opts = {}) {
|
|
|
1503
1571
|
// The receipt's record state (lineage key, record URL: non-secret) lives beside the log and is
|
|
1504
1572
|
// removed with it on exit, after the receipt has read it.
|
|
1505
1573
|
const recordStatePath = opts.recordState ? join(logDir, "record.json") : undefined;
|
|
1574
|
+
const proxyEnv = {
|
|
1575
|
+
...process.env,
|
|
1576
|
+
MNEMOM_AGENT_BU_DOOR: door,
|
|
1577
|
+
MNEMOM_AGENT_BU_CWD: process.cwd(),
|
|
1578
|
+
MNEMOM_AGENT_BU_LOG: process.env.MNEMOM_AGENT_TURN_PROXY_LOG || join(logDir, "proxy.log"),
|
|
1579
|
+
MNEMOM_AGENT_BU_PARENT_PID: String(process.pid),
|
|
1580
|
+
...(opts.turnHeader === false ? { MNEMOM_AGENT_TURN_HEADER: "0" } : {}),
|
|
1581
|
+
...(recordStatePath ? { MNEMOM_AGENT_BU_RECORD_STATE: recordStatePath } : {}),
|
|
1582
|
+
};
|
|
1583
|
+
// MNE-8245 defense-in-depth: the proxy reads none of the owner's own Mnemom credential vars.
|
|
1584
|
+
stripOwnerCredentialEnv(proxyEnv);
|
|
1506
1585
|
const proxy = spawn(opts.node ?? process.execPath, [script], {
|
|
1507
|
-
env:
|
|
1508
|
-
...process.env,
|
|
1509
|
-
MNEMOM_AGENT_BU_DOOR: door,
|
|
1510
|
-
MNEMOM_AGENT_BU_CWD: process.cwd(),
|
|
1511
|
-
MNEMOM_AGENT_BU_LOG: process.env.MNEMOM_AGENT_TURN_PROXY_LOG || join(logDir, "proxy.log"),
|
|
1512
|
-
MNEMOM_AGENT_BU_PARENT_PID: String(process.pid),
|
|
1513
|
-
...(opts.turnHeader === false ? { MNEMOM_AGENT_TURN_HEADER: "0" } : {}),
|
|
1514
|
-
...(recordStatePath ? { MNEMOM_AGENT_BU_RECORD_STATE: recordStatePath } : {}),
|
|
1515
|
-
},
|
|
1586
|
+
env: proxyEnv,
|
|
1516
1587
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1517
1588
|
});
|
|
1518
1589
|
process.on("exit", () => {
|
|
@@ -1584,27 +1655,28 @@ function launchRemoteControl(ctx) {
|
|
|
1584
1655
|
const rcDir = mkdtempSync(join(rcStateRoot(), "mnemom-code-rc."));
|
|
1585
1656
|
writeFileSync(join(rcDir, "owner.pid"), `${process.pid}\n`, { mode: 0o600 }); // rc-state.mjs sweep
|
|
1586
1657
|
const proxyLog = process.env.MNEMOM_AGENT_RC_PROXY_LOG || join(rcDir, "proxy.log");
|
|
1658
|
+
const proxyEnv = {
|
|
1659
|
+
...process.env,
|
|
1660
|
+
MNEMOM_AGENT_RC_DOOR: ctx.door,
|
|
1661
|
+
MNEMOM_AGENT_RC_AGENT: ctx.agent,
|
|
1662
|
+
MNEMOM_AGENT_RC_AGENT_ID: ctx.agentId ?? "",
|
|
1663
|
+
MNEMOM_AGENT_RC_CONVERSATION_ID: ctx.conversationId,
|
|
1664
|
+
MNEMOM_AGENT_RC_CONTRACT_B64: ctx.envelope?.base64 ?? "",
|
|
1665
|
+
MNEMOM_AGENT_RC_GOAL_MODE: ctx.goalMode ?? "",
|
|
1666
|
+
MNEMOM_AGENT_RC_SESSION_CONFIG: ctx.sessionConfigB64 ?? "",
|
|
1667
|
+
MNEMOM_AGENT_RC_PROXY_LOG: proxyLog,
|
|
1668
|
+
MNEMOM_AGENT_RC_PARENT_PID: String(process.pid),
|
|
1669
|
+
MNEMOM_AGENT_RC_AUTH: ctx.anthropicKey ? "api-key" : "subscription",
|
|
1670
|
+
};
|
|
1671
|
+
// MNE-8245 defense-in-depth: the proxy gets the session's (minted) Mnemom key on stdin only
|
|
1672
|
+
// and reads none of the owner's own Mnemom credential vars, so they never ride into its env.
|
|
1673
|
+
stripOwnerCredentialEnv(proxyEnv);
|
|
1587
1674
|
const proxy = spawn("node", [proxyScript], {
|
|
1588
|
-
env:
|
|
1589
|
-
...process.env,
|
|
1590
|
-
MNEMOM_AGENT_RC_DOOR: ctx.door,
|
|
1591
|
-
MNEMOM_AGENT_RC_AGENT: ctx.agent,
|
|
1592
|
-
MNEMOM_AGENT_RC_AGENT_ID: ctx.agentId ?? "",
|
|
1593
|
-
MNEMOM_AGENT_RC_CONVERSATION_ID: ctx.conversationId,
|
|
1594
|
-
MNEMOM_AGENT_RC_CONTRACT_B64: ctx.envelope?.base64 ?? "",
|
|
1595
|
-
MNEMOM_AGENT_RC_GOAL_MODE: ctx.goalMode ?? "",
|
|
1596
|
-
MNEMOM_AGENT_RC_SESSION_CONFIG: ctx.sessionConfigB64 ?? "",
|
|
1597
|
-
MNEMOM_AGENT_RC_PROXY_LOG: proxyLog,
|
|
1598
|
-
MNEMOM_AGENT_RC_PARENT_PID: String(process.pid),
|
|
1599
|
-
MNEMOM_AGENT_RC_AUTH: ctx.anthropicKey ? "api-key" : "subscription",
|
|
1600
|
-
},
|
|
1675
|
+
env: proxyEnv,
|
|
1601
1676
|
stdio: ["pipe", "pipe", "inherit"],
|
|
1602
1677
|
});
|
|
1603
1678
|
let stopped = false;
|
|
1604
|
-
const
|
|
1605
|
-
if (stopped)
|
|
1606
|
-
return;
|
|
1607
|
-
stopped = true;
|
|
1679
|
+
const stopProxyAndTempDir = () => {
|
|
1608
1680
|
if (!proxy.killed) {
|
|
1609
1681
|
try {
|
|
1610
1682
|
proxy.kill("SIGTERM");
|
|
@@ -1620,7 +1692,32 @@ function launchRemoteControl(ctx) {
|
|
|
1620
1692
|
/* best effort */
|
|
1621
1693
|
}
|
|
1622
1694
|
};
|
|
1623
|
-
|
|
1695
|
+
// MNE-8245 review-fix: this MUST be awaited by every call site below, and exit/re-signal must
|
|
1696
|
+
// happen only after it settles — a fire-and-forget `void revokeSessionGatewayKey(...)`
|
|
1697
|
+
// immediately followed by `process.exit()`/`process.kill()` never lets the revoke's fetch even
|
|
1698
|
+
// start before the process dies. revokeSessionGatewayKey bounds itself internally (~3s via
|
|
1699
|
+
// AbortSignal.timeout in lib/session-key.ts), so awaiting it here cannot hang the exit path.
|
|
1700
|
+
const onExit = async () => {
|
|
1701
|
+
if (stopped)
|
|
1702
|
+
return;
|
|
1703
|
+
stopped = true;
|
|
1704
|
+
stopProxyAndTempDir();
|
|
1705
|
+
if (ctx.sessionKeyId) {
|
|
1706
|
+
await revokeSessionGatewayKey(defaultSessionKeyDeps(say), ctx.sessionKeyId);
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
// Last-resort SYNC fallback only — Node's "exit" handlers cannot await, so this branch never
|
|
1710
|
+
// revokes the key (the 24h TTL covers it). It exists solely to guarantee the proxy child and
|
|
1711
|
+
// temp dir are cleaned up on an exit path that didn't go through (and await) onExit() above,
|
|
1712
|
+
// e.g. an unexpected top-level throw. Every normal exit path in this function calls onExit()
|
|
1713
|
+
// directly and awaits it before exiting, so this is a no-op on the common paths (`stopped` is
|
|
1714
|
+
// already true by the time it fires).
|
|
1715
|
+
process.on("exit", () => {
|
|
1716
|
+
if (stopped)
|
|
1717
|
+
return;
|
|
1718
|
+
stopped = true;
|
|
1719
|
+
stopProxyAndTempDir();
|
|
1720
|
+
});
|
|
1624
1721
|
// Secrets on stdin — three lines (anthropic, mnemom, openai). An api-key launch sends the
|
|
1625
1722
|
// Anthropic key, plus this machine's Mnemom key when it has one (the key principal; always
|
|
1626
1723
|
// with a pin); a subscription launch sends the Mnemom key only (the identity anchor — the
|
|
@@ -1632,21 +1729,24 @@ function launchRemoteControl(ctx) {
|
|
|
1632
1729
|
if (err.code === "EPIPE")
|
|
1633
1730
|
return;
|
|
1634
1731
|
say(`mnemom agent: could not hand the secrets to the Remote Control proxy: ${err.message}`);
|
|
1635
|
-
onExit();
|
|
1636
|
-
process.exit(1);
|
|
1732
|
+
void onExit().finally(() => process.exit(1));
|
|
1637
1733
|
});
|
|
1638
1734
|
proxy.stdin?.end(rcProxyStdin(ctx));
|
|
1639
1735
|
let stdoutBuf = "";
|
|
1640
1736
|
const start = Date.now();
|
|
1641
1737
|
const tryStart = () => {
|
|
1738
|
+
// Cleanup already ran (a refusal, or the exit fallback reaped the proxy): stop polling, so a
|
|
1739
|
+
// proxy killed by a signal (exitCode stays null) never reaches the 15s refusal afterwards.
|
|
1740
|
+
if (stopped)
|
|
1741
|
+
return;
|
|
1642
1742
|
const m = stdoutBuf.match(/^READY (\d+) (.+)$/m);
|
|
1643
1743
|
if (m) {
|
|
1644
1744
|
const port = m[1];
|
|
1645
1745
|
const ca = m[2].trim();
|
|
1646
1746
|
if (!existsSync(ca)) {
|
|
1647
1747
|
say("mnemom agent: malformed READY line from the Remote Control proxy.");
|
|
1648
|
-
onExit();
|
|
1649
|
-
|
|
1748
|
+
void onExit().finally(() => process.exit(1));
|
|
1749
|
+
return;
|
|
1650
1750
|
}
|
|
1651
1751
|
spawnRcCli({ ...ctx, port, ca, onExit });
|
|
1652
1752
|
return;
|
|
@@ -1654,13 +1754,13 @@ function launchRemoteControl(ctx) {
|
|
|
1654
1754
|
if (proxy.exitCode !== null) {
|
|
1655
1755
|
say("mnemom agent: the Remote Control proxy exited before it was ready — refusing to launch un-routed.");
|
|
1656
1756
|
redactedProxyLog(proxyLog);
|
|
1657
|
-
onExit();
|
|
1658
|
-
|
|
1757
|
+
void onExit().finally(() => process.exit(1));
|
|
1758
|
+
return;
|
|
1659
1759
|
}
|
|
1660
1760
|
if (Date.now() - start > 15_000) {
|
|
1661
1761
|
say("mnemom agent: the Remote Control proxy did not report READY within 15s.");
|
|
1662
|
-
onExit();
|
|
1663
|
-
|
|
1762
|
+
void onExit().finally(() => process.exit(1));
|
|
1763
|
+
return;
|
|
1664
1764
|
}
|
|
1665
1765
|
setTimeout(tryStart, 200);
|
|
1666
1766
|
};
|
|
@@ -1669,8 +1769,7 @@ function launchRemoteControl(ctx) {
|
|
|
1669
1769
|
});
|
|
1670
1770
|
proxy.on("error", (err) => {
|
|
1671
1771
|
say(`mnemom agent: failed to launch the Remote Control proxy: ${err.message}`);
|
|
1672
|
-
onExit();
|
|
1673
|
-
process.exit(1);
|
|
1772
|
+
void onExit().finally(() => process.exit(1));
|
|
1674
1773
|
});
|
|
1675
1774
|
tryStart();
|
|
1676
1775
|
}
|
|
@@ -1701,6 +1800,11 @@ function spawnRcCli(ctx) {
|
|
|
1701
1800
|
if (!strip.has(k))
|
|
1702
1801
|
childEnv[k] = v;
|
|
1703
1802
|
}
|
|
1803
|
+
// MNE-8245 defense-in-depth: see the identical call in launchTerminal — the owner's own
|
|
1804
|
+
// Mnemom credential must never ride through just because it sits in the CLI's own process
|
|
1805
|
+
// env. The RC gate forwards the Mnemom identity via the proxy's own auth plane, never from
|
|
1806
|
+
// this child's env, so there is no legitimate use for these vars here either.
|
|
1807
|
+
stripOwnerCredentialEnv(childEnv);
|
|
1704
1808
|
childEnv.HTTPS_PROXY = `http://127.0.0.1:${ctx.port}`;
|
|
1705
1809
|
childEnv.https_proxy = `http://127.0.0.1:${ctx.port}`;
|
|
1706
1810
|
childEnv.NODE_EXTRA_CA_CERTS = ctx.ca;
|
|
@@ -1782,15 +1886,18 @@ function spawnRcCli(ctx) {
|
|
|
1782
1886
|
const child = spawn(ctx.cli.bin, args, { stdio: "inherit", env: childEnv });
|
|
1783
1887
|
child.on("error", (err) => {
|
|
1784
1888
|
say(`mnemom agent: failed to launch ${ctx.cli.name} under Remote Control: ${err.message}`);
|
|
1785
|
-
ctx.onExit();
|
|
1786
|
-
process.exit(1);
|
|
1889
|
+
void ctx.onExit().finally(() => process.exit(1));
|
|
1787
1890
|
});
|
|
1788
1891
|
child.on("exit", (code, signal) => {
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1892
|
+
// MNE-8245 review-fix: await the (internally bounded) revoke before exiting/re-signaling —
|
|
1893
|
+
// the previous fire-and-forget `ctx.onExit()` immediately followed by exit/kill never let the
|
|
1894
|
+
// revoke's fetch start before the process died, so it silently never ran on this path.
|
|
1895
|
+
void ctx.onExit().finally(() => {
|
|
1896
|
+
if (signal)
|
|
1897
|
+
process.kill(process.pid, signal);
|
|
1898
|
+
else
|
|
1899
|
+
process.exit(code ?? 0);
|
|
1900
|
+
});
|
|
1794
1901
|
});
|
|
1795
1902
|
}
|
|
1796
1903
|
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;
|