@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.
- package/dist/commands/agent-config.js +9 -0
- package/dist/commands/agent-doctor.d.ts +3 -2
- package/dist/commands/agent-doctor.js +25 -9
- package/dist/commands/agent-goal.js +7 -2
- package/dist/commands/agent-sessions.js +2 -1
- package/dist/commands/agent-setup.d.ts +166 -9
- package/dist/commands/agent-setup.js +719 -54
- package/dist/commands/agent-slack.d.ts +4 -0
- package/dist/commands/agent-slack.js +67 -21
- package/dist/commands/agent.d.ts +29 -1
- package/dist/commands/agent.js +133 -17
- package/dist/commands/wrap.d.ts +2 -0
- package/dist/commands/wrap.js +13 -1
- package/dist/index.js +19 -5
- package/dist/lib/agent-identity-route.d.ts +21 -3
- package/dist/lib/agent-identity-route.js +42 -15
- package/dist/lib/agent-ledger.d.ts +5 -0
- package/dist/lib/agent.d.ts +75 -8
- package/dist/lib/agent.js +83 -14
- package/dist/lib/api.d.ts +33 -1
- package/dist/lib/api.js +10 -0
- package/dist/lib/experimental.js +14 -2
- package/dist/lib/goal-api.d.ts +3 -3
- package/dist/lib/goal-api.js +5 -2
- package/dist/lib/launcher-config.d.ts +18 -0
- package/dist/lib/launcher-config.js +47 -1
- package/dist/lib/model-tier.d.ts +75 -0
- package/dist/lib/model-tier.js +166 -0
- package/dist/lib/oauth.d.ts +7 -0
- package/dist/lib/oauth.js +92 -17
- package/dist/lib/served-agent-cache.d.ts +8 -1
- package/dist/lib/served-agent-cache.js +11 -2
- package/dist/lib/slack-channel.d.ts +1 -0
- package/dist/lib/slack-channel.js +30 -12
- package/dist/lib/slack-route.d.ts +72 -9
- package/dist/lib/slack-route.js +113 -24
- package/dist/lib/slack-router.d.ts +37 -2
- package/dist/lib/slack-router.js +98 -13
- package/dist/lib/slack-state.d.ts +105 -4
- package/dist/lib/slack-state.js +230 -11
- package/dist/lib/subscription-login.d.ts +12 -3
- package/dist/lib/subscription-login.js +19 -7
- package/dist/mcp/handoff-server.mjs +31 -3
- package/dist/rc-proxy.mjs +14 -0
- 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>", "
|
|
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
|
|
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({
|
|
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
|
|
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
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -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
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* and
|
|
109
|
-
*
|
|
110
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* and
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
targetUrl = new URL(target);
|
|
69
81
|
}
|
|
70
82
|
catch {
|
|
71
|
-
return `mnemom agent: the gateway '${
|
|
83
|
+
return `mnemom agent: the gateway '${target}' is not a valid URL.`;
|
|
72
84
|
}
|
|
73
|
-
if (LOOPBACK_HOSTS.has(
|
|
85
|
+
if (LOOPBACK_HOSTS.has(targetUrl.hostname))
|
|
74
86
|
return null;
|
|
75
|
-
if (sameOrigin(
|
|
87
|
+
if (sameOrigin(target, cellGateway))
|
|
76
88
|
return null;
|
|
77
|
-
return (`mnemom agent: the gateway ${
|
|
78
|
-
`(API ${
|
|
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.
|
|
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
|
package/dist/lib/experimental.js
CHANGED
|
@@ -50,7 +50,13 @@ export const EXPERIMENTAL_FEATURES = [
|
|
|
50
50
|
since: "0.17.3",
|
|
51
51
|
gates: {
|
|
52
52
|
flags: ["--slack"],
|
|
53
|
-
configKeys: [
|
|
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" ||
|
|
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";
|
package/dist/lib/goal-api.d.ts
CHANGED
|
@@ -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>`,
|
|
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;
|
package/dist/lib/goal-api.js
CHANGED
|
@@ -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>`,
|
|
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;
|