@mnemom/mnemom 0.17.25 → 0.17.27

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.
Files changed (45) hide show
  1. package/dist/commands/agent-config.js +9 -0
  2. package/dist/commands/agent-doctor.d.ts +3 -2
  3. package/dist/commands/agent-doctor.js +25 -9
  4. package/dist/commands/agent-goal.js +7 -2
  5. package/dist/commands/agent-sessions.js +2 -1
  6. package/dist/commands/agent-setup.d.ts +166 -9
  7. package/dist/commands/agent-setup.js +719 -54
  8. package/dist/commands/agent-slack.d.ts +4 -0
  9. package/dist/commands/agent-slack.js +67 -21
  10. package/dist/commands/agent.d.ts +29 -1
  11. package/dist/commands/agent.js +133 -17
  12. package/dist/commands/wrap.d.ts +2 -0
  13. package/dist/commands/wrap.js +13 -1
  14. package/dist/index.js +19 -5
  15. package/dist/lib/agent-identity-route.d.ts +21 -3
  16. package/dist/lib/agent-identity-route.js +42 -15
  17. package/dist/lib/agent-ledger.d.ts +5 -0
  18. package/dist/lib/agent.d.ts +75 -8
  19. package/dist/lib/agent.js +83 -14
  20. package/dist/lib/api.d.ts +33 -1
  21. package/dist/lib/api.js +10 -0
  22. package/dist/lib/experimental.js +14 -2
  23. package/dist/lib/goal-api.d.ts +3 -3
  24. package/dist/lib/goal-api.js +5 -2
  25. package/dist/lib/launcher-config.d.ts +18 -0
  26. package/dist/lib/launcher-config.js +47 -1
  27. package/dist/lib/model-tier.d.ts +75 -0
  28. package/dist/lib/model-tier.js +166 -0
  29. package/dist/lib/oauth.d.ts +7 -0
  30. package/dist/lib/oauth.js +92 -17
  31. package/dist/lib/served-agent-cache.d.ts +8 -1
  32. package/dist/lib/served-agent-cache.js +11 -2
  33. package/dist/lib/slack-channel.d.ts +1 -0
  34. package/dist/lib/slack-channel.js +30 -12
  35. package/dist/lib/slack-route.d.ts +72 -9
  36. package/dist/lib/slack-route.js +113 -24
  37. package/dist/lib/slack-router.d.ts +37 -2
  38. package/dist/lib/slack-router.js +98 -13
  39. package/dist/lib/slack-state.d.ts +105 -4
  40. package/dist/lib/slack-state.js +230 -11
  41. package/dist/lib/subscription-login.d.ts +12 -3
  42. package/dist/lib/subscription-login.js +19 -7
  43. package/dist/mcp/handoff-server.mjs +31 -3
  44. package/dist/rc-proxy.mjs +14 -0
  45. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ import { applyConfigToEnv, launcherEntries, loadLauncherConfig, withoutDisabledE
37
37
  import { loadExperimentalState } from "./lib/cli-config.js";
38
38
  import { assertEnabled, EXPERIMENTAL_CONTRACT, gateCheck, ignoredSettingLines, reportCliError, } from "./lib/experimental.js";
39
39
  import { experimentalDisableCommand, experimentalEnableCommand, experimentalListCommand, runExperimentalMigration, } from "./commands/experimental.js";
40
- import { parseCompactAt, parseContextProfile, resolveImplicit, } from "./lib/agent.js";
40
+ import { parseCompactAt, parseContextProfile, resolveImplicit, selectLaunchAgent, } from "./lib/agent.js";
41
41
  import { wrapCommand } from "./commands/wrap.js";
42
42
  import { onboardCommand } from "./commands/onboard.js";
43
43
  import { skillsListCommand, skillsDescribeCommand } from "./commands/skills.js";
@@ -154,9 +154,11 @@ const agentCmd = program
154
154
  .option("--budget <usd>", "Guardrail: spend ceiling in USD (positive number)")
155
155
  .option("--max-turns <n>", "Guardrail: turn ceiling (positive integer)")
156
156
  .option("--stall <n>", "Guardrail: consecutive no-write turns (positive integer)")
157
- .option("--mode <nudge|enforce>", "Sealed contract mode (MNE-8199; default nudge). Under enforce, a crossed turn/budget/time " +
157
+ .option("--mode <nudge|enforce>", "Goal-contract mode (MNE-8199, default nudge). Under enforce, a crossed turn/budget/time " +
158
158
  "ceiling REFUSES the next request on this conversation until the owner raises it via a " +
159
- "contract update — under nudge the ceiling only nudges, unchanged. Requires --goal.")
159
+ "contract update. Under nudge the ceiling only nudges, unchanged. Requires --goal. " +
160
+ "This is not the Safe House protection mode, which composes from the agent, team, org and " +
161
+ "platform cards (strictest wins). See `mnemom protection` for that mode.")
160
162
  .option("--lead-directed", "Seal the standard lead-directed requirement: own-branch push and draft/ready PR creation, " +
161
163
  "from a git worktree, on the lead's direction — not merging. Requires --goal; idempotent.")
162
164
  .addOption(new Option("--implicit", "EXPERIMENTAL (`mnemom experimental enable implicit`): implicit goal alignment — the gateway infers the goal from your messages and revises it as you steer (the default while the feature is on; not combinable with --goal/--requirement/--allow/--forbid/--goal-id/--mode/--lead-directed; the guardrail ceilings still apply)").hideHelp(hiddenUnless("implicit")))
@@ -313,7 +315,14 @@ const agentCmd = program
313
315
  // Sub-verb: `mnemom agent setup` provisions/claims the governed agent and
314
316
  // never launches. The agent slug follows the same precedence as a launch.
315
317
  if (scenario === "setup") {
316
- await agentSetupCommand({ agent: program.opts().agent ?? cfg.agent, governance });
318
+ await agentSetupCommand({
319
+ ...selectLaunchAgent({
320
+ flag: program.opts().agent,
321
+ configAgent: cfg.agent,
322
+ configAgentId: cfg.agentId,
323
+ }),
324
+ governance,
325
+ });
317
326
  return;
318
327
  }
319
328
  const numToStr = (n) => n === undefined ? undefined : String(n);
@@ -351,7 +360,12 @@ const agentCmd = program
351
360
  maxTurns: opts.maxTurns ?? (contractIntended ? numToStr(cfg.guardrails?.maxTurns) : undefined),
352
361
  stall: opts.stall ?? (contractIntended ? numToStr(cfg.guardrails?.stallTurns) : undefined),
353
362
  mode: opts.mode,
354
- agent: parentOpts.agent ?? cfg.agent,
363
+ // --agent wins; else launcher.toml agent_id (a pin) over agent (a name).
364
+ ...selectLaunchAgent({
365
+ flag: parentOpts.agent,
366
+ configAgent: cfg.agent,
367
+ configAgentId: cfg.agentId,
368
+ }),
355
369
  conversationId: opts.conversationId,
356
370
  cli: opts.cli,
357
371
  remoteControl: opts.remoteControl,
@@ -18,11 +18,29 @@ export declare class IdentityRouteMissingError extends Error {
18
18
  readonly gatewayUrl: string;
19
19
  constructor(gatewayUrl: string);
20
20
  }
21
+ /** The gateway's answer: the served id, and how it resolved it. */
22
+ export interface ServedIdentity {
23
+ agentId: string;
24
+ /**
25
+ * `pin` (the `x-mnemom-agent-id` pin was honoured), `principal` (the key owner's claimed agent
26
+ * of that name), `hash` (hash(key, name), birthed if absent), or null when an older gateway
27
+ * did not say. A pinned launch accepts only `pin`: an older gateway ignores the header and
28
+ * answers by name instead.
29
+ */
30
+ resolvedBy: string | null;
31
+ }
21
32
  /**
22
- * The mnm- id the gateway serves for (Mnemom key, name). Its response also carries claimed /
23
- * org_id / resolved_by / created; the launcher reads the claim state from the api instead, so
24
- * only the id is used here.
33
+ * The mnm- id the gateway serves for (Mnemom key, name[, pin]). Its response also carries
34
+ * claimed / org_id / created; the launcher reads the claim state from the api instead. A non-2xx
35
+ * answer throws a MnemomApiError carrying the gateway's error code (agent_pin_forbidden,
36
+ * agent_pin_mismatch, agent_name_ambiguous, mnemom_api_key_required, …) so the caller can say
37
+ * exactly what to fix.
25
38
  */
39
+ export declare function resolveServedIdentityDetail(gatewayUrl: string, agentName: string, mnemomKey: string, opts?: {
40
+ timeoutMs?: number;
41
+ pin?: string;
42
+ }): Promise<ServedIdentity>;
43
+ /** {@link resolveServedIdentityDetail}, the id only (an unpinned launch). */
26
44
  export declare function resolveServedIdentity(gatewayUrl: string, agentName: string, mnemomKey: string, opts?: {
27
45
  timeoutMs?: number;
28
46
  }): Promise<string>;
@@ -24,34 +24,61 @@ export class IdentityRouteMissingError extends Error {
24
24
  this.name = "IdentityRouteMissingError";
25
25
  }
26
26
  }
27
+ /** `{error:{code,message}}` (the gateway's envelope) → its code; anything else → undefined. */
28
+ function errorCode(text) {
29
+ try {
30
+ const body = JSON.parse(text);
31
+ const code = body?.error?.code;
32
+ return typeof code === "string" ? code : undefined;
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
27
38
  /**
28
- * The mnm- id the gateway serves for (Mnemom key, name). Its response also carries claimed /
29
- * org_id / resolved_by / created; the launcher reads the claim state from the api instead, so
30
- * only the id is used here.
39
+ * The mnm- id the gateway serves for (Mnemom key, name[, pin]). Its response also carries
40
+ * claimed / org_id / created; the launcher reads the claim state from the api instead. A non-2xx
41
+ * answer throws a MnemomApiError carrying the gateway's error code (agent_pin_forbidden,
42
+ * agent_pin_mismatch, agent_name_ambiguous, mnemom_api_key_required, …) so the caller can say
43
+ * exactly what to fix.
31
44
  */
32
- export async function resolveServedIdentity(gatewayUrl, agentName, mnemomKey, opts = {}) {
45
+ export async function resolveServedIdentityDetail(gatewayUrl, agentName, mnemomKey, opts = {}) {
46
+ const headers = {
47
+ Accept: "application/json",
48
+ "x-mnemom-api-key": mnemomKey,
49
+ "x-mnemom-agent": agentName,
50
+ };
51
+ // AGENT_ID_HEADER (lib/agent.ts), spelled out so this module imports nothing from the launcher.
52
+ if (opts.pin)
53
+ headers["x-mnemom-agent-id"] = opts.pin;
33
54
  const response = await fetch(`${gatewayUrl}${IDENTITY_ROUTE_PATH}`, {
34
55
  method: "POST",
35
- headers: {
36
- Accept: "application/json",
37
- "x-mnemom-api-key": mnemomKey,
38
- "x-mnemom-agent": agentName,
39
- },
56
+ headers,
40
57
  signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined,
41
58
  });
42
59
  if (response.status === 404)
43
60
  throw new IdentityRouteMissingError(gatewayUrl);
44
- if (response.status === 401) {
45
- throw new MnemomApiError(401, "the gateway rejected your Mnemom API key");
46
- }
47
61
  if (!response.ok) {
48
62
  const text = await response.text().catch(() => "");
49
- throw new MnemomApiError(response.status, `Gateway returned ${response.status}: ${text.slice(0, 300)}`);
63
+ const code = errorCode(text);
64
+ if (response.status === 401 && code !== "mnemom_api_key_required") {
65
+ throw new MnemomApiError(401, "the gateway rejected your Mnemom API key", { code });
66
+ }
67
+ throw new MnemomApiError(response.status, `Gateway returned ${response.status}: ${text.slice(0, 300)}`, { code });
50
68
  }
51
69
  const body = (await response.json().catch(() => null));
52
70
  const agentId = body?.agent_id;
53
- if (typeof agentId !== "string" || !/^mnm-/.test(agentId)) {
71
+ // Only `mnm-` + [A-Za-z0-9-]: the id is echoed into request headers (x-mnemom-agent-id), the
72
+ // sessions ledger and a printed resume hint, so nothing else from the gateway is trusted there.
73
+ if (typeof agentId !== "string" || !/^mnm-[A-Za-z0-9-]+$/.test(agentId)) {
54
74
  throw new Error(`Gateway returned no mnm- agent id from ${IDENTITY_ROUTE_PATH}.`);
55
75
  }
56
- return agentId;
76
+ return {
77
+ agentId,
78
+ resolvedBy: typeof body?.resolved_by === "string" ? body.resolved_by : null,
79
+ };
80
+ }
81
+ /** {@link resolveServedIdentityDetail}, the id only (an unpinned launch). */
82
+ export async function resolveServedIdentity(gatewayUrl, agentName, mnemomKey, opts = {}) {
83
+ return (await resolveServedIdentityDetail(gatewayUrl, agentName, mnemomKey, opts)).agentId;
57
84
  }
@@ -49,6 +49,11 @@ export interface SessionRecord {
49
49
  agent_slug: string;
50
50
  /** The agent record id when it resolved at launch, else null. */
51
51
  agent_id: string | null;
52
+ /**
53
+ * true when the launch PINNED `agent_id` (`--agent mnm-<uuid>` / launcher.toml `agent_id`), so
54
+ * a resume pins it again. Absent on an unpinned launch (and on every row written before pins).
55
+ */
56
+ agent_pinned?: true;
52
57
  org_id: string;
53
58
  org_label: string;
54
59
  gateway_host: string;
@@ -102,15 +102,19 @@ export declare function resolveGatewayHost(env?: NodeJS.ProcessEnv, cellGateway?
102
102
  /** Same scheme+host+port. An unparseable URL never matches. */
103
103
  export declare function sameOrigin(a: string, b: string): boolean;
104
104
  /**
105
- * A launch must use ONE cell: the Anthropic `door` its traffic goes to must be the
106
- * gateway of the cell the CLI signs in, resolves the agent and reads sessions on
107
- * (`cellGateway`, paired with `apiUrl`). A mixed launch records one cell's agent
108
- * and runs as another cell's, so `sessions`/`show` never find its goal state.
109
- * Returns the refusal message, or null. A loopback door is a local dev gateway and
110
- * is exempt.
105
+ * A launch must use ONE cell: the Anthropic `door` its traffic goes to, and the `gateway`
106
+ * host (`resolveGatewayHost`) the launcher sends the stored Mnemom key to (the identity
107
+ * route, setup's birth), must both be the gateway of the cell the CLI signs in, resolves
108
+ * the agent and reads sessions on (`cellGateway`, paired with `apiUrl`). A mixed launch
109
+ * records one cell's agent and runs as another cell's, so `sessions`/`show` never find its
110
+ * goal state. `MNEMOM_AGENT_ANTHROPIC_GATEWAY` sets the door apart from the gateway host,
111
+ * so checking the door alone leaves the key's destination unchecked. Returns the refusal
112
+ * message for the first offender, or null. A loopback host is a local dev gateway and is
113
+ * exempt.
111
114
  */
112
115
  export declare function singleCellError(opts: {
113
116
  door: string;
117
+ gateway: string;
114
118
  apiUrl: string;
115
119
  cellGateway: string;
116
120
  }): string | null;
@@ -131,9 +135,57 @@ export declare function isProdGateway(env?: NodeJS.ProcessEnv): boolean;
131
135
  * than `resolveGatewayHost` directly, because `MNEMOM_AGENT_ANTHROPIC_GATEWAY` can override the
132
136
  * door to a DIFFERENT host than `MNEMOM_AGENT_GATEWAY`/the default (e.g. a test cell) — reading
133
137
  * `resolveGatewayHost` here would silently send handoff calls to prod (or vice versa) while the
134
- * session's model traffic went elsewhere (MNE-7564 review).
138
+ * session's model traffic went elsewhere (MNE-7564 review). `singleCellError` now refuses a launch
139
+ * whose door and gateway host sit on different cells, but a loopback override is exempt from that
140
+ * check, so the door stays the source of truth here.
135
141
  */
136
142
  export declare function resolveHandoffGatewayHost(door: string): string;
143
+ /**
144
+ * The explicit agent pin header (`x-mnemom-agent-id: mnm-<uuid>`). The gateway honours it only
145
+ * with a verified Mnemom key, serves exactly that agent (the caller's own, claimed, live one) and
146
+ * never falls back to a name or births an agent; otherwise it refuses (400 agent_pin_mismatch,
147
+ * 403 agent_pin_forbidden, 401 mnemom_api_key_required).
148
+ */
149
+ export declare const AGENT_ID_HEADER = "x-mnemom-agent-id";
150
+ /** A full Mnemom agent id: `mnm-` + a lowercase UUID. */
151
+ export declare const AGENT_ID_RE: RegExp;
152
+ /** What an `--agent` / launcher.toml `agent` / `agent_id` value names. */
153
+ export type AgentRef = {
154
+ kind: "id";
155
+ id: string;
156
+ } | {
157
+ kind: "name";
158
+ name: string;
159
+ }
160
+ /** Starts with `mnm-` but is not a full id (a truncated/garbled paste). */
161
+ | {
162
+ kind: "malformed-id";
163
+ raw: string;
164
+ };
165
+ /**
166
+ * Classify an agent reference. A full `mnm-<uuid>` (any case — ids are lowercase on the wire, so
167
+ * it is lowercased) is an ID: the launch pins that exact agent. Anything else starting with
168
+ * `mnm-` is a MALFORMED id and refused by the caller: treating it as a name would make the
169
+ * gateway birth a brand-new agent named after the id (the 2026-10-08 restart-resume strays). Any
170
+ * other value is a NAME.
171
+ */
172
+ export declare function parseAgentRef(raw: string): AgentRef;
173
+ /**
174
+ * Which agent a launch asks for, from the three places it can be named. `--agent` (flag) wins
175
+ * outright — an id there pins, a name there launches by name even when launcher.toml pins;
176
+ * otherwise launcher.toml `agent_id` pins, and wins over launcher.toml `agent`. Returns the
177
+ * `--agent`-shaped value (`agent`) and the config pin (`agentId`) for AgentOptions.
178
+ */
179
+ export declare function selectLaunchAgent(sources: {
180
+ flag?: string;
181
+ configAgent?: string;
182
+ configAgentId?: string;
183
+ }): {
184
+ agent?: string;
185
+ agentId?: string;
186
+ };
187
+ /** The refusal for a `mnm-…` value that is not a full agent id. */
188
+ export declare function malformedAgentIdMessage(raw: string, source: string): string;
137
189
  /**
138
190
  * Sanitise a header value into a safe wire token: lowercase, alphanumerics and
139
191
  * single hyphens only, no leading/trailing hyphen. Returns "" when nothing
@@ -597,9 +649,14 @@ export declare function resolveRemoteControlShape(args: {
597
649
  export declare function buildCustomHeaders(opts: {
598
650
  /** Absent on a subscription-login launch: Claude Code then sends its own claude.ai login. */
599
651
  anthropicKey?: string;
600
- /** The governed-identity anchor on a subscription-login launch (lib/subscription-login.ts). */
652
+ /**
653
+ * The Mnemom key: the governed-identity anchor on a subscription-login launch, and the
654
+ * principal an api-key launch proves when one is stored (lib/subscription-login.ts).
655
+ */
601
656
  mnemomKey?: string;
602
657
  agent: string;
658
+ /** The pinned agent id (`--agent mnm-<uuid>` / launcher.toml `agent_id`), sent as AGENT_ID_HEADER. */
659
+ agentId?: string;
603
660
  conversationId: string;
604
661
  contractB64?: string;
605
662
  /** `buildGoalModeHeader` output for an `--implicit` launch. */
@@ -769,6 +826,14 @@ export declare const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
769
826
  * it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
770
827
  */
771
828
  export declare const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
829
+ /**
830
+ * The Remote Control counterpart for the Mnemom key: under RC the key rides only the proxy's
831
+ * stdin, so when the handoff tool is registered on a launch that has one, `spawnRcCli` sets this
832
+ * dedicated, non-ANTHROPIC var (same reasoning as HANDOFF_RC_KEY_ENV_NAME). The terminal shape
833
+ * needs none — the server reads the `x-mnemom-api-key:` line of its inherited
834
+ * `ANTHROPIC_CUSTOM_HEADERS`.
835
+ */
836
+ export declare const HANDOFF_RC_MNEMOM_KEY_ENV_NAME = "MNEMOM_HANDOFF_MNEMOM_KEY";
772
837
  /**
773
838
  * How the `handoff` MCP server (cli/src/mcp/handoff-server.mjs) recovers the Anthropic key
774
839
  * WITHOUT it ever being written into the `--mcp-config` file on disk: `envName` is the NAME of a
@@ -801,6 +866,8 @@ export interface HandoffMcpParams {
801
866
  gatewayHost: string;
802
867
  anthropicKeySource: HandoffAnthropicKeySource;
803
868
  agent: string;
869
+ /** The pinned agent id, when the launch pins one — the handoff call carries the same pin. */
870
+ agentId?: string;
804
871
  conversationId: string;
805
872
  /** `buildSessionConfigB64` output — forwarded so the route resolves the SAME per-session flag. */
806
873
  sessionConfigB64?: string;
package/dist/lib/agent.js CHANGED
@@ -55,27 +55,39 @@ export function sameOrigin(a, b) {
55
55
  }
56
56
  }
57
57
  /**
58
- * A launch must use ONE cell: the Anthropic `door` its traffic goes to must be the
59
- * gateway of the cell the CLI signs in, resolves the agent and reads sessions on
60
- * (`cellGateway`, paired with `apiUrl`). A mixed launch records one cell's agent
61
- * and runs as another cell's, so `sessions`/`show` never find its goal state.
62
- * Returns the refusal message, or null. A loopback door is a local dev gateway and
63
- * is exempt.
58
+ * A launch must use ONE cell: the Anthropic `door` its traffic goes to, and the `gateway`
59
+ * host (`resolveGatewayHost`) the launcher sends the stored Mnemom key to (the identity
60
+ * route, setup's birth), must both be the gateway of the cell the CLI signs in, resolves
61
+ * the agent and reads sessions on (`cellGateway`, paired with `apiUrl`). A mixed launch
62
+ * records one cell's agent and runs as another cell's, so `sessions`/`show` never find its
63
+ * goal state. `MNEMOM_AGENT_ANTHROPIC_GATEWAY` sets the door apart from the gateway host,
64
+ * so checking the door alone leaves the key's destination unchecked. Returns the refusal
65
+ * message for the first offender, or null. A loopback host is a local dev gateway and is
66
+ * exempt.
64
67
  */
65
68
  export function singleCellError(opts) {
66
- let doorUrl;
69
+ for (const target of [opts.door, opts.gateway]) {
70
+ const error = cellMismatch(target, opts.apiUrl, opts.cellGateway);
71
+ if (error)
72
+ return error;
73
+ }
74
+ return null;
75
+ }
76
+ /** {@link singleCellError} for one gateway URL. */
77
+ function cellMismatch(target, apiUrl, cellGateway) {
78
+ let targetUrl;
67
79
  try {
68
- doorUrl = new URL(opts.door);
80
+ targetUrl = new URL(target);
69
81
  }
70
82
  catch {
71
- return `mnemom agent: the gateway '${opts.door}' is not a valid URL.`;
83
+ return `mnemom agent: the gateway '${target}' is not a valid URL.`;
72
84
  }
73
- if (LOOPBACK_HOSTS.has(doorUrl.hostname))
85
+ if (LOOPBACK_HOSTS.has(targetUrl.hostname))
74
86
  return null;
75
- if (sameOrigin(opts.door, opts.cellGateway))
87
+ if (sameOrigin(target, cellGateway))
76
88
  return null;
77
- return (`mnemom agent: the gateway ${doorUrl.origin} is on a different cell from the one this CLI uses ` +
78
- `(API ${opts.apiUrl}, gateway ${opts.cellGateway}). A launch must use one cell. Point the whole ` +
89
+ return (`mnemom agent: the gateway ${targetUrl.origin} is on a different cell from the one this CLI uses ` +
90
+ `(API ${apiUrl}, gateway ${cellGateway}). A launch must use one cell. Point the whole ` +
79
91
  "CLI at that cell (MNEMOM_API_URL + MNEMOM_GATEWAY_URL, or MNEMOM_RING) and drop the gateway " +
80
92
  "override (--gateway, MNEMOM_AGENT_GATEWAY, MNEMOM_AGENT_ANTHROPIC_GATEWAY, or `gateway` in launcher.toml).");
81
93
  }
@@ -103,11 +115,57 @@ export function isProdGateway(env = process.env) {
103
115
  * than `resolveGatewayHost` directly, because `MNEMOM_AGENT_ANTHROPIC_GATEWAY` can override the
104
116
  * door to a DIFFERENT host than `MNEMOM_AGENT_GATEWAY`/the default (e.g. a test cell) — reading
105
117
  * `resolveGatewayHost` here would silently send handoff calls to prod (or vice versa) while the
106
- * session's model traffic went elsewhere (MNE-7564 review).
118
+ * session's model traffic went elsewhere (MNE-7564 review). `singleCellError` now refuses a launch
119
+ * whose door and gateway host sit on different cells, but a loopback override is exempt from that
120
+ * check, so the door stays the source of truth here.
107
121
  */
108
122
  export function resolveHandoffGatewayHost(door) {
109
123
  return new URL(door).origin;
110
124
  }
125
+ /**
126
+ * The explicit agent pin header (`x-mnemom-agent-id: mnm-<uuid>`). The gateway honours it only
127
+ * with a verified Mnemom key, serves exactly that agent (the caller's own, claimed, live one) and
128
+ * never falls back to a name or births an agent; otherwise it refuses (400 agent_pin_mismatch,
129
+ * 403 agent_pin_forbidden, 401 mnemom_api_key_required).
130
+ */
131
+ export const AGENT_ID_HEADER = "x-mnemom-agent-id";
132
+ /** A full Mnemom agent id: `mnm-` + a lowercase UUID. */
133
+ export const AGENT_ID_RE = /^mnm-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
134
+ /**
135
+ * Classify an agent reference. A full `mnm-<uuid>` (any case — ids are lowercase on the wire, so
136
+ * it is lowercased) is an ID: the launch pins that exact agent. Anything else starting with
137
+ * `mnm-` is a MALFORMED id and refused by the caller: treating it as a name would make the
138
+ * gateway birth a brand-new agent named after the id (the 2026-10-08 restart-resume strays). Any
139
+ * other value is a NAME.
140
+ */
141
+ export function parseAgentRef(raw) {
142
+ const v = raw.trim();
143
+ const lower = v.toLowerCase();
144
+ if (AGENT_ID_RE.test(lower))
145
+ return { kind: "id", id: lower };
146
+ if (lower.startsWith("mnm-"))
147
+ return { kind: "malformed-id", raw: v };
148
+ return { kind: "name", name: v };
149
+ }
150
+ /**
151
+ * Which agent a launch asks for, from the three places it can be named. `--agent` (flag) wins
152
+ * outright — an id there pins, a name there launches by name even when launcher.toml pins;
153
+ * otherwise launcher.toml `agent_id` pins, and wins over launcher.toml `agent`. Returns the
154
+ * `--agent`-shaped value (`agent`) and the config pin (`agentId`) for AgentOptions.
155
+ */
156
+ export function selectLaunchAgent(sources) {
157
+ if (sources.flag?.trim())
158
+ return { agent: sources.flag };
159
+ if (sources.configAgentId?.trim())
160
+ return { agentId: sources.configAgentId };
161
+ return sources.configAgent?.trim() ? { agent: sources.configAgent } : {};
162
+ }
163
+ /** The refusal for a `mnm-…` value that is not a full agent id. */
164
+ export function malformedAgentIdMessage(raw, source) {
165
+ return (`mnemom agent: ${source} '${raw}' looks like an agent id but isn't a full one (mnm-<uuid>). ` +
166
+ "Pass the complete id (see `mnemom agents`), or a plain agent name. " +
167
+ "It is not used as a name: that would create a new agent named after the id.");
168
+ }
111
169
  /**
112
170
  * Sanitise a header value into a safe wire token: lowercase, alphanumerics and
113
171
  * single hyphens only, no leading/trailing hyphen. Returns "" when nothing
@@ -966,6 +1024,7 @@ export function buildCustomHeaders(opts) {
966
1024
  ...(opts.anthropicKey ? [`x-api-key: ${opts.anthropicKey}`] : []),
967
1025
  ...(opts.mnemomKey ? [`x-mnemom-api-key: ${opts.mnemomKey}`] : []),
968
1026
  `x-mnemom-agent: ${opts.agent}`,
1027
+ ...(opts.agentId ? [`${AGENT_ID_HEADER}: ${opts.agentId}`] : []),
969
1028
  `x-mnemom-conversation-id: ${opts.conversationId}`,
970
1029
  ];
971
1030
  if (opts.contractB64)
@@ -1177,6 +1236,14 @@ export const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
1177
1236
  * it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
1178
1237
  */
1179
1238
  export const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
1239
+ /**
1240
+ * The Remote Control counterpart for the Mnemom key: under RC the key rides only the proxy's
1241
+ * stdin, so when the handoff tool is registered on a launch that has one, `spawnRcCli` sets this
1242
+ * dedicated, non-ANTHROPIC var (same reasoning as HANDOFF_RC_KEY_ENV_NAME). The terminal shape
1243
+ * needs none — the server reads the `x-mnemom-api-key:` line of its inherited
1244
+ * `ANTHROPIC_CUSTOM_HEADERS`.
1245
+ */
1246
+ export const HANDOFF_RC_MNEMOM_KEY_ENV_NAME = "MNEMOM_HANDOFF_MNEMOM_KEY";
1180
1247
  /** The `--mcp-config` document registering exactly the `handoff` tool. Pure — no I/O. */
1181
1248
  export function buildHandoffMcpConfig(p) {
1182
1249
  const env = {
@@ -1187,6 +1254,8 @@ export function buildHandoffMcpConfig(p) {
1187
1254
  };
1188
1255
  if (p.sessionConfigB64)
1189
1256
  env.MNEMOM_HANDOFF_SESSION_CONFIG = p.sessionConfigB64;
1257
+ if (p.agentId)
1258
+ env.MNEMOM_HANDOFF_AGENT_ID = p.agentId;
1190
1259
  return {
1191
1260
  mcpServers: {
1192
1261
  [HANDOFF_MCP_SERVER_NAME]: { command: p.nodeBin, args: [p.serverScript], env },
package/dist/lib/api.d.ts CHANGED
@@ -31,6 +31,13 @@ export interface Agent {
31
31
  claimed_at?: string | null;
32
32
  name?: string | null;
33
33
  claimed?: boolean;
34
+ /** Owner (org-member) view: the org the agent lives in. */
35
+ org_id?: string | null;
36
+ /**
37
+ * `org_member` on the full record, `authenticated` / `anonymous` on the public projection
38
+ * (handleGetAgent). Absent on an older api.
39
+ */
40
+ caller?: string;
34
41
  }
35
42
  /**
36
43
  * GET /v1/integrity/:id — components/schemas/IntegrityScore:
@@ -247,6 +254,13 @@ export interface AgentListItem {
247
254
  * routing here avoids the false "Not authenticated" error (MNE-194 F10).
248
255
  */
249
256
  export declare function listAgents(): Promise<AgentListItem[]>;
257
+ /**
258
+ * Every agent the caller OWNS, across all their orgs — `GET /v1/agents?scope=all`: rows whose
259
+ * `created_by` or `claimed_by` is the caller (MNE-191), newest first, up to 500. No org and no
260
+ * claim state on the rows (read those with {@link getAgent}); a tombstoned agent can still be
261
+ * listed and 404s there.
262
+ */
263
+ export declare function listMyAgents(): Promise<AgentListItem[]>;
250
264
  export interface OrgListItem {
251
265
  org_id: string;
252
266
  name: string;
@@ -645,7 +659,9 @@ export declare function putProtectionCard(agentId: string, body: string, content
645
659
  /**
646
660
  * The per-layer view of an agent's card (ADR-053 `?include=sources`): the raw
647
661
  * card the agent layer itself carries, next to the composed card every layer
648
- * produced. We type the two rows we read; the platform/org/teams rows ride along.
662
+ * produced. The platform, org and team rows are optional here because an older
663
+ * api, or a test double, can omit them. Shape per mnemom-api
664
+ * `buildAgentSourcesEnvelope` (src/composition/agent-handlers.ts).
649
665
  */
650
666
  export interface CardSources {
651
667
  agent: {
@@ -656,6 +672,22 @@ export interface CardSources {
656
672
  card_json: Record<string, unknown> | null;
657
673
  available: boolean;
658
674
  };
675
+ platform?: {
676
+ card_json: Record<string, unknown> | null;
677
+ available: boolean;
678
+ };
679
+ org?: {
680
+ card_json: Record<string, unknown> | null;
681
+ available: boolean;
682
+ org_id?: string | null;
683
+ org_name?: string | null;
684
+ };
685
+ teams?: Array<{
686
+ team_id: string;
687
+ team_name?: string | null;
688
+ card_json: Record<string, unknown> | null;
689
+ available: boolean;
690
+ }>;
659
691
  [key: string]: unknown;
660
692
  }
661
693
  /**
package/dist/lib/api.js CHANGED
@@ -211,6 +211,16 @@ export async function listAgents() {
211
211
  const data = await gFetch("/v1/agents?limit=100", {}, { fallback: "Failed to list agents" });
212
212
  return data.agents ?? [];
213
213
  }
214
+ /**
215
+ * Every agent the caller OWNS, across all their orgs — `GET /v1/agents?scope=all`: rows whose
216
+ * `created_by` or `claimed_by` is the caller (MNE-191), newest first, up to 500. No org and no
217
+ * claim state on the rows (read those with {@link getAgent}); a tombstoned agent can still be
218
+ * listed and 404s there.
219
+ */
220
+ export async function listMyAgents() {
221
+ const data = await gFetch("/v1/agents?scope=all&limit=500", {}, { fallback: "Failed to list your agents" });
222
+ return data.agents ?? [];
223
+ }
214
224
  /**
215
225
  * GET /v1/orgs — list every org the user is a member of, including the
216
226
  * personal-org-of-one (per ADR-044 Option A, GitHub model). Personal sorts
@@ -50,7 +50,13 @@ export const EXPERIMENTAL_FEATURES = [
50
50
  since: "0.17.3",
51
51
  gates: {
52
52
  flags: ["--slack"],
53
- configKeys: ["slack_channel", "slack_listen", "slack_allow"],
53
+ configKeys: [
54
+ "slack_channel",
55
+ "slack_listen",
56
+ "slack_allow",
57
+ "slack_aliases",
58
+ "slack_sticky_threads",
59
+ ],
54
60
  commands: ["mnemom agent slack"],
55
61
  env: [],
56
62
  },
@@ -156,6 +162,8 @@ export function experimentalIdForConfigKey(key, value) {
156
162
  case "slack_allow":
157
163
  return "slack";
158
164
  case "slack_listen":
165
+ case "slack_aliases":
166
+ case "slack_sticky_threads":
159
167
  return value === "true" ? "slack" : undefined;
160
168
  case "context_handoff":
161
169
  return value === "true" ? "context-handoff" : undefined;
@@ -229,7 +237,11 @@ export function migrateExperimental(deps) {
229
237
  }
230
238
  /** The feature a launcher.toml key belongs to at all (for marking `mnemom agent config list`). */
231
239
  export function configKeyFeature(key) {
232
- if (key === "slack_channel" || key === "slack_listen" || key === "slack_allow")
240
+ if (key === "slack_channel" ||
241
+ key === "slack_listen" ||
242
+ key === "slack_allow" ||
243
+ key === "slack_aliases" ||
244
+ key === "slack_sticky_threads")
233
245
  return "slack";
234
246
  if (key === "context_handoff")
235
247
  return "context-handoff";
@@ -7,8 +7,8 @@
7
7
  * POST https://<gateway_host>/mnemom/goal/intent/reset {conversation_id, thread, statement?, requirements?}
8
8
  *
9
9
  * Auth is the SAME credential the launch used: `x-api-key` (the Anthropic key from
10
- * `resolveAnthropicKey`), `x-mnemom-agent: <record.agent_slug>`, and `x-mnemom-api-key` when the
11
- * launch had a Mnemom key. The gateway hashes them exactly as a proxied request and only LOOKS
10
+ * `resolveAnthropicKey`), `x-mnemom-agent: <record.agent_slug>`, `x-mnemom-api-key` when the
11
+ * launch had a Mnemom key, and `x-mnemom-agent-id: <record.agent_id>` when the launch pinned it. The gateway hashes them exactly as a proxied request and only LOOKS
12
12
  * UP the agent. Key material is never printed: errors carry the status, the gateway's error code
13
13
  * and its message, nothing else.
14
14
  *
@@ -24,7 +24,7 @@ export type GoalOp = "show" | "pin" | "reset";
24
24
  export declare const GOAL_THREAD_RE: RegExp;
25
25
  export declare function isValidGoalThread(thread: string): boolean;
26
26
  /** The ledger fields the control call needs. */
27
- export type GoalControlRecord = Pick<SessionRecord, "gateway_host" | "agent_slug" | "conversation_id">;
27
+ export type GoalControlRecord = Pick<SessionRecord, "gateway_host" | "agent_slug" | "conversation_id"> & Partial<Pick<SessionRecord, "agent_id" | "agent_pinned">>;
28
28
  export interface GoalControlCreds {
29
29
  /** The Anthropic key the launch sent as `x-api-key`. */
30
30
  anthropicKey: string;
@@ -7,8 +7,8 @@
7
7
  * POST https://<gateway_host>/mnemom/goal/intent/reset {conversation_id, thread, statement?, requirements?}
8
8
  *
9
9
  * Auth is the SAME credential the launch used: `x-api-key` (the Anthropic key from
10
- * `resolveAnthropicKey`), `x-mnemom-agent: <record.agent_slug>`, and `x-mnemom-api-key` when the
11
- * launch had a Mnemom key. The gateway hashes them exactly as a proxied request and only LOOKS
10
+ * `resolveAnthropicKey`), `x-mnemom-agent: <record.agent_slug>`, `x-mnemom-api-key` when the
11
+ * launch had a Mnemom key, and `x-mnemom-agent-id: <record.agent_id>` when the launch pinned it. The gateway hashes them exactly as a proxied request and only LOOKS
12
12
  * UP the agent. Key material is never printed: errors carry the status, the gateway's error code
13
13
  * and its message, nothing else.
14
14
  *
@@ -58,6 +58,9 @@ export function goalControlHeaders(record, creds) {
58
58
  };
59
59
  if (creds.mnemomKey)
60
60
  headers["x-mnemom-api-key"] = creds.mnemomKey;
61
+ // A pinned launch's control calls carry the same pin, so they reach the agent it ran as.
62
+ if (record.agent_pinned && record.agent_id)
63
+ headers["x-mnemom-agent-id"] = record.agent_id;
61
64
  return headers;
62
65
  }
63
66
  /** `{error:{code,message}}` (the gateway's envelope) → its parts; anything else → nulls. */
@@ -27,6 +27,11 @@ export interface LauncherConfig {
27
27
  gateway?: string;
28
28
  model?: string;
29
29
  agent?: string;
30
+ /**
31
+ * The agent this machine's launches PIN (`x-mnemom-agent-id`): a full `mnm-<uuid>`. `--agent`
32
+ * on the command line wins over it; it wins over `agent`.
33
+ */
34
+ agentId?: string;
30
35
  effort?: string;
31
36
  cli?: string;
32
37
  launch?: LaunchShape;
@@ -43,6 +48,17 @@ export interface LauncherConfig {
43
48
  slackListen?: boolean;
44
49
  /** Slack member ids whose messages the router may deliver into a session. */
45
50
  slackAllow?: string[];
51
+ /**
52
+ * Router: honour `mnemom agent slack alias` handles (off when unset). Off means the
53
+ * router routes exactly as it did before aliases existed; registered aliases are kept
54
+ * but ignored.
55
+ */
56
+ slackAliases?: boolean;
57
+ /**
58
+ * Router: a thread a session was addressed in keeps routing replies to it (off when
59
+ * unset). Off means only threads a session posted in route replies, as before.
60
+ */
61
+ slackStickyThreads?: boolean;
46
62
  /**
47
63
  * Context-handoff default (EXPERIMENTAL, off when unset): `true` registers the `handoff`
48
64
  * MCP tool and sends `CONTEXT_HANDOFF_ENABLED: "true"` in the per-session config header for
@@ -86,6 +102,8 @@ export interface ConfigKeySpec {
86
102
  get(cfg: LauncherConfig): string | number | boolean | undefined;
87
103
  set(cfg: LauncherConfig, raw: string): void;
88
104
  }
105
+ /** launcher.toml `agent_id`: a full `mnm-<uuid>` (any case; stored lowercase). */
106
+ export declare function coerceAgentId(v: string): string;
89
107
  /** The full set of settable keys — drives get/set/unset/list and validation. */
90
108
  export declare const CONFIG_KEYS: ConfigKeySpec[];
91
109
  export declare function configKeySpec(key: string): ConfigKeySpec | undefined;