@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/index.js
CHANGED
|
@@ -41,6 +41,7 @@ import { isPrerelease } from "./lib/update/semver.js";
|
|
|
41
41
|
import { applyConfigToEnv, launcherEntries, loadLauncherConfig, withoutDisabledExperimental, } from "./lib/launcher-config.js";
|
|
42
42
|
import { loadExperimentalState } from "./lib/cli-config.js";
|
|
43
43
|
import { assertEnabled, availableFeatures, EXPERIMENTAL_CONTRACT, gateCheck, ignoredSettingLines, reportCliError, } from "./lib/experimental.js";
|
|
44
|
+
import { durableVerbOffLines } from "./durable/gate.js";
|
|
44
45
|
import { experimentalDisableCommand, experimentalEnableCommand, experimentalListCommand, runExperimentalMigration, } from "./commands/experimental.js";
|
|
45
46
|
import { parseCompactAt, parseContextProfile, passthroughIsPrint, resolveImplicitSource, selectLaunchAgent, splitSessionArg, } from "./lib/agent.js";
|
|
46
47
|
import { wrapCommand } from "./commands/wrap.js";
|
|
@@ -928,7 +929,7 @@ agentCmd
|
|
|
928
929
|
: liveAgentShareDeps;
|
|
929
930
|
process.exitCode = await agentShareCommand(opts, deps);
|
|
930
931
|
});
|
|
931
|
-
agentCmd
|
|
932
|
+
const agentSessionsCmd = agentCmd
|
|
932
933
|
.command("sessions")
|
|
933
934
|
.description("List your Mnemom Agent sessions from the local ledger (~/.mnemom/sessions.jsonl), newest first — the last 20 unless --all")
|
|
934
935
|
.option("--all", "Every session, not just the newest 20")
|
|
@@ -942,6 +943,56 @@ agentCmd
|
|
|
942
943
|
process.exit(1);
|
|
943
944
|
}
|
|
944
945
|
});
|
|
946
|
+
/** preAction (and --help) guard for a durable verb: while durable is off it does not exist (C14-R22). */
|
|
947
|
+
function requireDurableVerb(word, what) {
|
|
948
|
+
const lines = durableVerbOffLines(word, what, loadExperimentalState().enabled);
|
|
949
|
+
if (lines) {
|
|
950
|
+
for (const l of lines)
|
|
951
|
+
console.error(l);
|
|
952
|
+
process.exit(2);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// `mnemom agent sessions adopt` (durable M1, C4): hand the running tracked sessions on this
|
|
956
|
+
// machine to the host daemon, one table and one [Y/n] per agent group. Gated on `durable`.
|
|
957
|
+
const ADOPT_WHAT = "`mnemom agent sessions adopt`";
|
|
958
|
+
const agentSessionsAdoptCmd = agentSessionsCmd
|
|
959
|
+
.command("adopt", { hidden: hiddenUnless("durable") })
|
|
960
|
+
.description("EXPERIMENTAL (`mnemom experimental enable durable`): adopt the running sessions on this machine as durable sessions the host daemon runs, one table per agent")
|
|
961
|
+
.option("--dry-run", "Print the plan (tables, specs, dropped flags) and change nothing")
|
|
962
|
+
.option("--json", "Output the plan as JSON")
|
|
963
|
+
.option("--own-agent", "Move each adopted group onto its own agent")
|
|
964
|
+
.hook("preAction", () => requireDurableVerb("adopt", ADOPT_WHAT))
|
|
965
|
+
.action(async (opts, cmd) => {
|
|
966
|
+
try {
|
|
967
|
+
const { adoptCommand } = await import("./durable/adopt.js");
|
|
968
|
+
const { liveAdoptDeps } = await import("./durable/adopt-live.js");
|
|
969
|
+
// `agent` declares --dry-run and `sessions` declares --json, and commander hands a flag to
|
|
970
|
+
// the first command on the line that declares it: read both with the ancestors' options,
|
|
971
|
+
// or `adopt --dry-run` would adopt for real.
|
|
972
|
+
const all = cmd.optsWithGlobals();
|
|
973
|
+
process.exitCode = await adoptCommand(liveAdoptDeps(), {
|
|
974
|
+
dryRun: all.dryRun === true,
|
|
975
|
+
json: all.json === true,
|
|
976
|
+
ownAgent: opts.ownAgent,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
catch (error) {
|
|
980
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
981
|
+
process.exit(1);
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
/**
|
|
985
|
+
* The durable-gated commands (BACKBONE Flags/CLI: off lists no durable verb). The committed
|
|
986
|
+
* command-tree.json leaves them out whatever the generating machine's experimental state
|
|
987
|
+
* (scripts/gen-command-tree.mjs); a piece that registers another durable verb adds it here.
|
|
988
|
+
*/
|
|
989
|
+
export const DURABLE_GATED_COMMANDS = new Set([agentSessionsAdoptCmd]);
|
|
990
|
+
// commander answers --help before preAction: gate the help too, so the verb does not exist while off.
|
|
991
|
+
const adoptOutputHelp = agentSessionsAdoptCmd.outputHelp.bind(agentSessionsAdoptCmd);
|
|
992
|
+
agentSessionsAdoptCmd.outputHelp = ((...args) => {
|
|
993
|
+
requireDurableVerb("adopt", ADOPT_WHAT);
|
|
994
|
+
adoptOutputHelp(...args);
|
|
995
|
+
});
|
|
945
996
|
agentCmd
|
|
946
997
|
.command("show [ref]")
|
|
947
998
|
.description("Show one session — the local launch record merged with the gateway's live goal state (sealed contract, latest verdict, guardrails). " +
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -690,6 +690,13 @@ export declare function buildCustomHeaders(opts: {
|
|
|
690
690
|
/** `buildSessionConfigB64` output — the per-session env overlay (e.g. fold off). */
|
|
691
691
|
sessionConfigB64?: string;
|
|
692
692
|
}): string;
|
|
693
|
+
/**
|
|
694
|
+
* Strip the owner's own Mnemom credential env vars from a child env, IN
|
|
695
|
+
* PLACE — defense in depth against them riding through just because they
|
|
696
|
+
* happened to sit in the CLI's own process env (an exported shell var, or a
|
|
697
|
+
* leftover override) when `{ ...process.env }` was spread into it.
|
|
698
|
+
*/
|
|
699
|
+
export declare function stripOwnerCredentialEnv(env: NodeJS.ProcessEnv): void;
|
|
693
700
|
/**
|
|
694
701
|
* Whether context folding is ON for this launch. Folding is a gateway feature
|
|
695
702
|
* that summarises older turns to fit the window; it is ON by default. Precedence:
|
package/dist/lib/agent.js
CHANGED
|
@@ -1096,6 +1096,32 @@ export function buildCustomHeaders(opts) {
|
|
|
1096
1096
|
lines.push(`${SESSION_CONFIG_HEADER}: ${opts.sessionConfigB64}`);
|
|
1097
1097
|
return lines.join("\n");
|
|
1098
1098
|
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Env vars that carry the OWNER's own Mnemom credential, in the CLI process's
|
|
1101
|
+
* own env — never the launched session's (MNE-8245). `MNEMOM_API_KEY` /
|
|
1102
|
+
* `MNEMOM_TOKEN` are `lib/auth.ts`'s ambient owner-credential vars (the ones
|
|
1103
|
+
* `resolveAuth` reads to authenticate the CLI's OWN calls, e.g. the session-key
|
|
1104
|
+
* mint); `MNEMOM_AGENT_MNEMOM_KEY` is `lib/subscription-login.ts`'s override for
|
|
1105
|
+
* that same owner credential on a subscription launch. None of the three has
|
|
1106
|
+
* any legitimate reason to reach a launched session's own env: the session's
|
|
1107
|
+
* only Mnemom credential is the minted, session-scoped key already spliced
|
|
1108
|
+
* into its headers/stdin by the caller.
|
|
1109
|
+
*/
|
|
1110
|
+
const OWNER_CREDENTIAL_ENV_VARS = [
|
|
1111
|
+
"MNEMOM_API_KEY",
|
|
1112
|
+
"MNEMOM_TOKEN",
|
|
1113
|
+
"MNEMOM_AGENT_MNEMOM_KEY",
|
|
1114
|
+
];
|
|
1115
|
+
/**
|
|
1116
|
+
* Strip the owner's own Mnemom credential env vars from a child env, IN
|
|
1117
|
+
* PLACE — defense in depth against them riding through just because they
|
|
1118
|
+
* happened to sit in the CLI's own process env (an exported shell var, or a
|
|
1119
|
+
* leftover override) when `{ ...process.env }` was spread into it.
|
|
1120
|
+
*/
|
|
1121
|
+
export function stripOwnerCredentialEnv(env) {
|
|
1122
|
+
for (const k of OWNER_CREDENTIAL_ENV_VARS)
|
|
1123
|
+
Reflect.deleteProperty(env, k);
|
|
1124
|
+
}
|
|
1099
1125
|
/**
|
|
1100
1126
|
* Whether context folding is ON for this launch. Folding is a gateway feature
|
|
1101
1127
|
* that summarises older turns to fit the window; it is ON by default. Precedence:
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -1181,6 +1181,38 @@ export declare function createApiKey(name: string, scopes: ApiKeyScope[]): Promi
|
|
|
1181
1181
|
* moment this returns.
|
|
1182
1182
|
*/
|
|
1183
1183
|
export declare function rotateApiKey(keyId: string): Promise<ApiKeyCreated>;
|
|
1184
|
+
/**
|
|
1185
|
+
* Response shape of `POST /v1/agents/{agent_id}/session-keys` (MNE-8244,
|
|
1186
|
+
* mnemom-api `src/code/session-keys.ts`). `key` is the cleartext secret,
|
|
1187
|
+
* returned once.
|
|
1188
|
+
*/
|
|
1189
|
+
export interface SessionKeyMinted {
|
|
1190
|
+
key_id: string;
|
|
1191
|
+
key: string;
|
|
1192
|
+
key_prefix: string;
|
|
1193
|
+
scopes: string[];
|
|
1194
|
+
kind: string;
|
|
1195
|
+
agent_id: string;
|
|
1196
|
+
session_id: string | null;
|
|
1197
|
+
expires_at: string;
|
|
1198
|
+
created_at: string;
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* POST /v1/agents/{agent_id}/session-keys (MNE-8244) — mint a short-lived
|
|
1202
|
+
* (24h), `['gateway']`-only API key bound to `agentId` (and `sessionId`
|
|
1203
|
+
* when known), using the CALLER'S OWN ambient credential (`postApi`'s
|
|
1204
|
+
* `authHeaders()` — never anything read from a launched session). This is
|
|
1205
|
+
* owner-gated server-side: a 403 `session_credential_forbidden` is what
|
|
1206
|
+
* the API returns if a session key is ever presented here instead.
|
|
1207
|
+
*
|
|
1208
|
+
* `mnemom agent` calls this so the LAUNCHED session never holds the
|
|
1209
|
+
* owner's own key (MNE-8245) — see `lib/session-key.ts` for the
|
|
1210
|
+
* fail-closed wiring around this call.
|
|
1211
|
+
*/
|
|
1212
|
+
export declare function mintAgentSessionKey(agentId: string, opts?: {
|
|
1213
|
+
orgId?: string;
|
|
1214
|
+
sessionId?: string;
|
|
1215
|
+
}): Promise<SessionKeyMinted>;
|
|
1184
1216
|
/**
|
|
1185
1217
|
* DELETE /v1/api-keys/{key_id} — soft-revoke. The key row stays for
|
|
1186
1218
|
* audit; `is_active` flips to false and `revoked_at` is timestamped.
|
package/dist/lib/api.js
CHANGED
|
@@ -1477,6 +1477,22 @@ export async function rotateApiKey(keyId) {
|
|
|
1477
1477
|
}
|
|
1478
1478
|
return (await response.json());
|
|
1479
1479
|
}
|
|
1480
|
+
/**
|
|
1481
|
+
* POST /v1/agents/{agent_id}/session-keys (MNE-8244) — mint a short-lived
|
|
1482
|
+
* (24h), `['gateway']`-only API key bound to `agentId` (and `sessionId`
|
|
1483
|
+
* when known), using the CALLER'S OWN ambient credential (`postApi`'s
|
|
1484
|
+
* `authHeaders()` — never anything read from a launched session). This is
|
|
1485
|
+
* owner-gated server-side: a 403 `session_credential_forbidden` is what
|
|
1486
|
+
* the API returns if a session key is ever presented here instead.
|
|
1487
|
+
*
|
|
1488
|
+
* `mnemom agent` calls this so the LAUNCHED session never holds the
|
|
1489
|
+
* owner's own key (MNE-8245) — see `lib/session-key.ts` for the
|
|
1490
|
+
* fail-closed wiring around this call.
|
|
1491
|
+
*/
|
|
1492
|
+
export async function mintAgentSessionKey(agentId, opts = {}) {
|
|
1493
|
+
const endpoint = `/v1/agents/${encodeURIComponent(agentId)}/session-keys${opts.orgId ? `?org_id=${encodeURIComponent(opts.orgId)}` : ""}`;
|
|
1494
|
+
return postApi(endpoint, opts.sessionId ? { session_id: opts.sessionId } : {});
|
|
1495
|
+
}
|
|
1480
1496
|
/**
|
|
1481
1497
|
* DELETE /v1/api-keys/{key_id} — soft-revoke. The key row stays for
|
|
1482
1498
|
* audit; `is_active` flips to false and `revoked_at` is timestamped.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Everything here is pure or dependency-injected; index.ts and the command
|
|
13
13
|
* modules are the thin drivers.
|
|
14
14
|
*/
|
|
15
|
-
export type ExperimentalId = "context-handoff" | "slack" | "implicit" | "measured-savings" | "record" | "fuel";
|
|
15
|
+
export type ExperimentalId = "context-handoff" | "slack" | "implicit" | "measured-savings" | "record" | "fuel" | "durable";
|
|
16
16
|
/**
|
|
17
17
|
* Features that used to be experimental and are now on by default for everyone. Their ids are
|
|
18
18
|
* no longer in the registry, but `mnemom experimental enable|disable <id>` still accepts them
|
package/dist/lib/experimental.js
CHANGED
|
@@ -107,6 +107,19 @@ export const EXPERIMENTAL_FEATURES = [
|
|
|
107
107
|
env: [],
|
|
108
108
|
},
|
|
109
109
|
},
|
|
110
|
+
{
|
|
111
|
+
// Durable governed sessions (M1). Off: every durable verb word exits 2 with the gate pointer
|
|
112
|
+
// and launches are unchanged; the host daemon reads config.json only, never MNEMOM_EXPERIMENTAL.
|
|
113
|
+
id: "durable",
|
|
114
|
+
summary: "Durable sessions: lock a session so this machine keeps it running, and adopt running ones",
|
|
115
|
+
since: "0.17.27",
|
|
116
|
+
gates: {
|
|
117
|
+
flags: [],
|
|
118
|
+
configKeys: ["durable"],
|
|
119
|
+
commands: ["mnemom agent sessions adopt"],
|
|
120
|
+
env: [],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
110
123
|
];
|
|
111
124
|
export const EXPERIMENTAL_IDS = EXPERIMENTAL_FEATURES.map((f) => f.id);
|
|
112
125
|
export const EXPERIMENTAL_ENV = "MNEMOM_EXPERIMENTAL";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The org Record keys a launch's lineage under (Doors W1.2, W1.4, W1.8).
|
|
3
|
+
*
|
|
4
|
+
* The gateway writes a request's record deltas under the org it bills, which is the served
|
|
5
|
+
* agent's org. The launcher's account org can differ: with an org API key, the account
|
|
6
|
+
* preflight resolves the key owner's personal org (`/v1/auth/me/personal-org`, a `pers-*`
|
|
7
|
+
* id), so a totals read or commit report keyed on it lands on an org that holds no row for
|
|
8
|
+
* the lineage, and Record answers 404. Every Record read and write of a launch (conduct
|
|
9
|
+
* receipt, status-line feed, commit reporting) therefore uses the served agent's org.
|
|
10
|
+
*
|
|
11
|
+
* FAIL-OPEN, BOUNDED. One agent read, raced against RECORD_ORG_TIMEOUT_MS; a failed, slow or
|
|
12
|
+
* off-shape answer falls back to the account org (the behaviour before this read existed), and
|
|
13
|
+
* `onFallback` says why: Record answers a wrong-org read or commit report with 404, which its
|
|
14
|
+
* callers treat as "dark", so a silent fallback would look like Record being off. Never throws.
|
|
15
|
+
*/
|
|
16
|
+
/** The most the read adds to launch. */
|
|
17
|
+
export declare const RECORD_ORG_TIMEOUT_MS = 1500;
|
|
18
|
+
export interface RecordOrgDeps {
|
|
19
|
+
/** The served agent's record; only `org_id` is read. */
|
|
20
|
+
readAgent?: (agentId: string) => Promise<{
|
|
21
|
+
org_id?: string | null;
|
|
22
|
+
}>;
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
/** Told why the account org was used instead (one line, no secret). */
|
|
25
|
+
onFallback?: (reason: string) => void;
|
|
26
|
+
}
|
|
27
|
+
export declare function recordOrgFor(agentId: string, accountOrgId: string, deps?: RecordOrgDeps): Promise<string>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The org Record keys a launch's lineage under (Doors W1.2, W1.4, W1.8).
|
|
3
|
+
*
|
|
4
|
+
* The gateway writes a request's record deltas under the org it bills, which is the served
|
|
5
|
+
* agent's org. The launcher's account org can differ: with an org API key, the account
|
|
6
|
+
* preflight resolves the key owner's personal org (`/v1/auth/me/personal-org`, a `pers-*`
|
|
7
|
+
* id), so a totals read or commit report keyed on it lands on an org that holds no row for
|
|
8
|
+
* the lineage, and Record answers 404. Every Record read and write of a launch (conduct
|
|
9
|
+
* receipt, status-line feed, commit reporting) therefore uses the served agent's org.
|
|
10
|
+
*
|
|
11
|
+
* FAIL-OPEN, BOUNDED. One agent read, raced against RECORD_ORG_TIMEOUT_MS; a failed, slow or
|
|
12
|
+
* off-shape answer falls back to the account org (the behaviour before this read existed), and
|
|
13
|
+
* `onFallback` says why: Record answers a wrong-org read or commit report with 404, which its
|
|
14
|
+
* callers treat as "dark", so a silent fallback would look like Record being off. Never throws.
|
|
15
|
+
*/
|
|
16
|
+
import { getAgent } from "./api.js";
|
|
17
|
+
/** The most the read adds to launch. */
|
|
18
|
+
export const RECORD_ORG_TIMEOUT_MS = 1500;
|
|
19
|
+
const ORG_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
20
|
+
export async function recordOrgFor(agentId, accountOrgId, deps = {}) {
|
|
21
|
+
const readAgent = deps.readAgent ?? getAgent;
|
|
22
|
+
let timer;
|
|
23
|
+
const timeout = new Promise((resolve) => {
|
|
24
|
+
timer = setTimeout(() => resolve(null), deps.timeoutMs ?? RECORD_ORG_TIMEOUT_MS);
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
let failed = false;
|
|
28
|
+
const agent = await Promise.race([
|
|
29
|
+
readAgent(agentId).catch(() => {
|
|
30
|
+
failed = true;
|
|
31
|
+
return null;
|
|
32
|
+
}),
|
|
33
|
+
timeout,
|
|
34
|
+
]);
|
|
35
|
+
const org = agent?.org_id;
|
|
36
|
+
if (typeof org === "string" && ORG_ID_RE.test(org))
|
|
37
|
+
return org;
|
|
38
|
+
deps.onFallback?.(failed ? "agent read failed" : agent ? "agent has no org" : "agent read timed out");
|
|
39
|
+
return accountOrgId;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MNE-8245 — the LAUNCHED `mnemom agent` session must never hold the owner's
|
|
3
|
+
* own Mnemom API key: code running inside that session (a shell tool call,
|
|
4
|
+
* a malicious prompt injection, …) could read it straight out of its own
|
|
5
|
+
* environment and call owner-only mnemom-api routes as the owner, including
|
|
6
|
+
* the owner-only `update_contract` endpoint (MNE-8233) — defeating sealed
|
|
7
|
+
* mode, where a sealed contract changes only through the owner's explicit,
|
|
8
|
+
* out-of-session update.
|
|
9
|
+
*
|
|
10
|
+
* Per the 2026-10-06 ruling: the session gets a minted, session-scoped,
|
|
11
|
+
* `['gateway']`-only key instead (MNE-8244's `POST
|
|
12
|
+
* /v1/agents/{agent_id}/session-keys`), and the launch FAILS CLOSED if
|
|
13
|
+
* minting doesn't succeed — never a fallback to the owner's own credential.
|
|
14
|
+
*
|
|
15
|
+
* Pure apart from the injected deps, so every arm (mint success, every
|
|
16
|
+
* failure mode, revoke success/failure) is unit-tested without a real API.
|
|
17
|
+
*/
|
|
18
|
+
import { type SessionKeyMinted } from "./api.js";
|
|
19
|
+
export interface SessionKeyDeps {
|
|
20
|
+
mint: (agentId: string, opts: {
|
|
21
|
+
orgId?: string;
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
}) => Promise<SessionKeyMinted>;
|
|
24
|
+
revoke: (keyId: string) => Promise<void>;
|
|
25
|
+
say: (line: string) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Override the mint/revoke timeout, tests only — real callers never set these, so the real
|
|
28
|
+
* 10s/3s bounds below always apply in production. Exists so a test can exercise "never
|
|
29
|
+
* settles" against a short, fast timeout rather than waiting out the real one.
|
|
30
|
+
*/
|
|
31
|
+
mintTimeoutMs?: number;
|
|
32
|
+
revokeTimeoutMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/** The real deps: `lib/api.ts`'s `mintAgentSessionKey` / `revokeApiKey`. */
|
|
35
|
+
export declare function defaultSessionKeyDeps(say: (line: string) => void): SessionKeyDeps;
|
|
36
|
+
export interface MintedSessionGatewayKey {
|
|
37
|
+
/** The cleartext secret — place this ONLY in the launched session's env. */
|
|
38
|
+
key: string;
|
|
39
|
+
/** Needed to revoke the key later; never placed in the session's env. */
|
|
40
|
+
keyId: string;
|
|
41
|
+
expiresAt: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Mint the ONE gateway-only, session-scoped key a launched session is
|
|
45
|
+
* allowed to carry.
|
|
46
|
+
*
|
|
47
|
+
* FAILS CLOSED: throws on ANY problem — a mint call that rejects (network
|
|
48
|
+
* error, non-2xx HTTP status, a malformed/empty body) — or a response that
|
|
49
|
+
* parses fine but is not the credential this is for: no key, no key_id (it
|
|
50
|
+
* could never be revoked), scopes that are not EXACTLY `['gateway']`, or a
|
|
51
|
+
* `kind` other than `'session'` when the response reports one. Every arm
|
|
52
|
+
* throws a `mnemom agent: …`-prefixed `Error` — the caller MUST NOT catch
|
|
53
|
+
* it and fall back to any other credential; it is meant to propagate all
|
|
54
|
+
* the way to the CLI's top-level error handler, which exits non-zero
|
|
55
|
+
* before anything is spawned.
|
|
56
|
+
*/
|
|
57
|
+
export declare function mintSessionGatewayKey(deps: SessionKeyDeps, args: {
|
|
58
|
+
agentId: string;
|
|
59
|
+
orgId?: string;
|
|
60
|
+
sessionId?: string;
|
|
61
|
+
}): Promise<MintedSessionGatewayKey>;
|
|
62
|
+
/**
|
|
63
|
+
* Best-effort revoke on session exit (clean or not) — a failure here is a
|
|
64
|
+
* WARNING only, never thrown: the key is 24h-TTL'd regardless, so an
|
|
65
|
+
* un-revoked key is not a fresh security hole, just a slightly longer-lived
|
|
66
|
+
* one. Never called with the owner's own key id.
|
|
67
|
+
*/
|
|
68
|
+
export declare function revokeSessionGatewayKey(deps: SessionKeyDeps, keyId: string): Promise<void>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MNE-8245 — the LAUNCHED `mnemom agent` session must never hold the owner's
|
|
3
|
+
* own Mnemom API key: code running inside that session (a shell tool call,
|
|
4
|
+
* a malicious prompt injection, …) could read it straight out of its own
|
|
5
|
+
* environment and call owner-only mnemom-api routes as the owner, including
|
|
6
|
+
* the owner-only `update_contract` endpoint (MNE-8233) — defeating sealed
|
|
7
|
+
* mode, where a sealed contract changes only through the owner's explicit,
|
|
8
|
+
* out-of-session update.
|
|
9
|
+
*
|
|
10
|
+
* Per the 2026-10-06 ruling: the session gets a minted, session-scoped,
|
|
11
|
+
* `['gateway']`-only key instead (MNE-8244's `POST
|
|
12
|
+
* /v1/agents/{agent_id}/session-keys`), and the launch FAILS CLOSED if
|
|
13
|
+
* minting doesn't succeed — never a fallback to the owner's own credential.
|
|
14
|
+
*
|
|
15
|
+
* Pure apart from the injected deps, so every arm (mint success, every
|
|
16
|
+
* failure mode, revoke success/failure) is unit-tested without a real API.
|
|
17
|
+
*/
|
|
18
|
+
import { mintAgentSessionKey, revokeApiKey } from "./api.js";
|
|
19
|
+
/** The real deps: `lib/api.ts`'s `mintAgentSessionKey` / `revokeApiKey`. */
|
|
20
|
+
export function defaultSessionKeyDeps(say) {
|
|
21
|
+
return { mint: mintAgentSessionKey, revoke: revokeApiKey, say };
|
|
22
|
+
}
|
|
23
|
+
/** How long a launch waits on the mint call before giving up (still fails closed — see below). */
|
|
24
|
+
const MINT_TIMEOUT_MS = 10_000;
|
|
25
|
+
/** How long session exit waits on the best-effort revoke before moving on regardless. */
|
|
26
|
+
const REVOKE_TIMEOUT_MS = 3_000;
|
|
27
|
+
/**
|
|
28
|
+
* Bounds `promise` to `ms`: resolves/rejects exactly as `promise` would, UNLESS `AbortSignal
|
|
29
|
+
* .timeout(ms)` fires first, in which case this rejects with a fresh timeout Error instead. Does
|
|
30
|
+
* NOT cancel `promise` itself — `deps.mint`/`deps.revoke` take no signal of their own — this only
|
|
31
|
+
* bounds how long the CALLER waits, which is exactly what both call sites below need: the launch
|
|
32
|
+
* (mint) and the exit handler (revoke) must never hang on a stuck network call.
|
|
33
|
+
*/
|
|
34
|
+
function raceTimeout(promise, ms, message) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const signal = AbortSignal.timeout(ms);
|
|
37
|
+
const onAbort = () => reject(new Error(message));
|
|
38
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
39
|
+
promise.then((value) => {
|
|
40
|
+
signal.removeEventListener("abort", onAbort);
|
|
41
|
+
resolve(value);
|
|
42
|
+
}, (err) => {
|
|
43
|
+
signal.removeEventListener("abort", onAbort);
|
|
44
|
+
reject(err);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Mint the ONE gateway-only, session-scoped key a launched session is
|
|
50
|
+
* allowed to carry.
|
|
51
|
+
*
|
|
52
|
+
* FAILS CLOSED: throws on ANY problem — a mint call that rejects (network
|
|
53
|
+
* error, non-2xx HTTP status, a malformed/empty body) — or a response that
|
|
54
|
+
* parses fine but is not the credential this is for: no key, no key_id (it
|
|
55
|
+
* could never be revoked), scopes that are not EXACTLY `['gateway']`, or a
|
|
56
|
+
* `kind` other than `'session'` when the response reports one. Every arm
|
|
57
|
+
* throws a `mnemom agent: …`-prefixed `Error` — the caller MUST NOT catch
|
|
58
|
+
* it and fall back to any other credential; it is meant to propagate all
|
|
59
|
+
* the way to the CLI's top-level error handler, which exits non-zero
|
|
60
|
+
* before anything is spawned.
|
|
61
|
+
*/
|
|
62
|
+
export async function mintSessionGatewayKey(deps, args) {
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
const timeoutMs = deps.mintTimeoutMs ?? MINT_TIMEOUT_MS;
|
|
66
|
+
res = await raceTimeout(deps.mint(args.agentId, { orgId: args.orgId, sessionId: args.sessionId }), timeoutMs, `mint timed out after ${timeoutMs}ms`);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
70
|
+
throw new Error(`mnemom agent: could not mint a session-scoped gateway key for this launch (${msg}) — refusing to launch with the owner's own credential instead.`, { cause: err });
|
|
71
|
+
}
|
|
72
|
+
if (!res || typeof res.key !== "string" || res.key.length === 0) {
|
|
73
|
+
throw new Error("mnemom agent: the session-key mint API returned no key — refusing to launch.");
|
|
74
|
+
}
|
|
75
|
+
if (!res.key_id) {
|
|
76
|
+
throw new Error("mnemom agent: the session-key mint API returned no key_id (it could never be revoked) — refusing to launch.");
|
|
77
|
+
}
|
|
78
|
+
const scopes = res.scopes ?? [];
|
|
79
|
+
if (!(scopes.length === 1 && scopes[0] === "gateway")) {
|
|
80
|
+
throw new Error(`mnemom agent: the minted key's scopes were ${JSON.stringify(scopes)}, not exactly ['gateway'] — refusing to launch with it.`);
|
|
81
|
+
}
|
|
82
|
+
if (res.kind !== undefined && res.kind !== null && res.kind !== "session") {
|
|
83
|
+
throw new Error(`mnemom agent: the minted key's kind was '${String(res.kind)}', not 'session' — refusing to launch with it.`);
|
|
84
|
+
}
|
|
85
|
+
return { key: res.key, keyId: res.key_id, expiresAt: res.expires_at };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Best-effort revoke on session exit (clean or not) — a failure here is a
|
|
89
|
+
* WARNING only, never thrown: the key is 24h-TTL'd regardless, so an
|
|
90
|
+
* un-revoked key is not a fresh security hole, just a slightly longer-lived
|
|
91
|
+
* one. Never called with the owner's own key id.
|
|
92
|
+
*/
|
|
93
|
+
export async function revokeSessionGatewayKey(deps, keyId) {
|
|
94
|
+
try {
|
|
95
|
+
const timeoutMs = deps.revokeTimeoutMs ?? REVOKE_TIMEOUT_MS;
|
|
96
|
+
await raceTimeout(deps.revoke(keyId), timeoutMs, `revoke timed out after ${timeoutMs}ms`);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
deps.say(`mnemom agent: could not revoke the session key (non-fatal — it expires on its own within 24h): ${err instanceof Error ? err.message : String(err)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
package/dist/lib/statusline.d.ts
CHANGED
|
@@ -59,12 +59,25 @@ export declare function render(stdin: string, deps?: RenderDeps): string;
|
|
|
59
59
|
export interface UserStatusLine {
|
|
60
60
|
command: string;
|
|
61
61
|
padding?: number;
|
|
62
|
+
/** Seconds between timed re-runs (Claude Code's statusLine.refreshInterval). */
|
|
63
|
+
refreshInterval?: number;
|
|
62
64
|
}
|
|
63
65
|
/**
|
|
64
66
|
* The statusLine Claude Code would use without us: the first settings file (highest
|
|
65
67
|
* precedence first) that sets one. Only a `command` statusLine can be wrapped.
|
|
66
68
|
*/
|
|
67
69
|
export declare function userStatusLine(cwd: string, configDir: string): UserStatusLine | null;
|
|
70
|
+
/**
|
|
71
|
+
* Seconds between timed redraws on a fed launch, the feed's own poll (FEED_INTERVAL_MS).
|
|
72
|
+
* Claude Code redraws the status line only on conversation events unless refreshInterval is
|
|
73
|
+
* set, so totals that land after the last turn would never reach the screen.
|
|
74
|
+
*/
|
|
75
|
+
export declare const FED_REFRESH_S = 10;
|
|
76
|
+
/**
|
|
77
|
+
* The statusLine layer to launch with: none when the segment is off; on a fed launch a timed
|
|
78
|
+
* redraw, the user's own shorter interval kept.
|
|
79
|
+
*/
|
|
80
|
+
export declare function launchStatusLine(settings: Record<string, unknown> | null, fed: boolean): Record<string, unknown> | null;
|
|
68
81
|
/**
|
|
69
82
|
* Launch args + env that add the segment, or null when it should stay off: the opt-out
|
|
70
83
|
* env, or the user passing their own --settings (two would fight over precedence).
|
package/dist/lib/statusline.js
CHANGED
|
@@ -122,12 +122,34 @@ export function userStatusLine(cwd, configDir) {
|
|
|
122
122
|
continue;
|
|
123
123
|
if (sl.type !== "command" || typeof sl.command !== "string" || !sl.command)
|
|
124
124
|
return null;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
const user = { command: sl.command };
|
|
126
|
+
if (typeof sl.padding === "number")
|
|
127
|
+
user.padding = sl.padding;
|
|
128
|
+
if (typeof sl.refreshInterval === "number" && sl.refreshInterval >= 1)
|
|
129
|
+
user.refreshInterval = sl.refreshInterval;
|
|
130
|
+
return user;
|
|
128
131
|
}
|
|
129
132
|
return null;
|
|
130
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Seconds between timed redraws on a fed launch, the feed's own poll (FEED_INTERVAL_MS).
|
|
136
|
+
* Claude Code redraws the status line only on conversation events unless refreshInterval is
|
|
137
|
+
* set, so totals that land after the last turn would never reach the screen.
|
|
138
|
+
*/
|
|
139
|
+
export const FED_REFRESH_S = 10;
|
|
140
|
+
/**
|
|
141
|
+
* The statusLine layer to launch with: none when the segment is off; on a fed launch a timed
|
|
142
|
+
* redraw, the user's own shorter interval kept.
|
|
143
|
+
*/
|
|
144
|
+
export function launchStatusLine(settings, fed) {
|
|
145
|
+
if (!settings || !fed)
|
|
146
|
+
return settings;
|
|
147
|
+
const sl = settings.statusLine;
|
|
148
|
+
if (!sl || typeof sl !== "object")
|
|
149
|
+
return settings;
|
|
150
|
+
const own = typeof sl.refreshInterval === "number" ? sl.refreshInterval : Infinity;
|
|
151
|
+
return { ...settings, statusLine: { ...sl, refreshInterval: Math.min(own, FED_REFRESH_S) } };
|
|
152
|
+
}
|
|
131
153
|
const sq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
132
154
|
/**
|
|
133
155
|
* Launch args + env that add the segment, or null when it should stay off: the opt-out
|
|
@@ -146,6 +168,8 @@ export function statuslineLaunch(o) {
|
|
|
146
168
|
};
|
|
147
169
|
if (user?.padding !== undefined)
|
|
148
170
|
statusLine.padding = user.padding;
|
|
171
|
+
if (user?.refreshInterval !== undefined)
|
|
172
|
+
statusLine.refreshInterval = user.refreshInterval;
|
|
149
173
|
const env = user ? { [INNER_ENV]: user.command } : {};
|
|
150
174
|
// `settings` is the same layer as `args`, for a caller that merges it with its own (Claude Code
|
|
151
175
|
// keeps only the last --settings it is given).
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Pure apart from the injected deps, so every arm is unit-tested.
|
|
18
18
|
*/
|
|
19
|
-
import type
|
|
19
|
+
import { type LaunchShape } from "./agent.js";
|
|
20
20
|
export type AgentAuth = "api-key" | "subscription";
|
|
21
21
|
export declare const AGENT_AUTHS: readonly AgentAuth[];
|
|
22
22
|
/**
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Pure apart from the injected deps, so every arm is unit-tested.
|
|
18
18
|
*/
|
|
19
|
+
import { stripOwnerCredentialEnv } from "./agent.js";
|
|
19
20
|
export const AGENT_AUTHS = ["api-key", "subscription"];
|
|
20
21
|
/**
|
|
21
22
|
* The retired `console` value (it read the key Claude Code's Anthropic Console /login
|
|
@@ -140,7 +141,9 @@ export function subscriptionChildEnv(base, door, headers) {
|
|
|
140
141
|
delete env.ANTHROPIC_API_KEY;
|
|
141
142
|
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
142
143
|
// The key already rides in the headers; the child process has no other use for it.
|
|
143
|
-
|
|
144
|
+
// MNE-8245 defense-in-depth: also strips MNEMOM_API_KEY/MNEMOM_TOKEN, in case the
|
|
145
|
+
// owner's own ambient credential sits in the CLI's process env for an unrelated reason.
|
|
146
|
+
stripOwnerCredentialEnv(env);
|
|
144
147
|
env.ANTHROPIC_BASE_URL = door;
|
|
145
148
|
env.ANTHROPIC_CUSTOM_HEADERS = headers;
|
|
146
149
|
return env;
|