@mnemom/mnemom 0.17.25 → 0.17.26
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 +22 -7
- package/dist/commands/agent-goal.js +7 -2
- package/dist/commands/agent-sessions.js +2 -1
- package/dist/commands/agent-setup.d.ts +104 -4
- package/dist/commands/agent-setup.js +414 -18
- 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 +89 -12
- 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 +62 -1
- package/dist/lib/agent.js +55 -0
- 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/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
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* on drift). Specifically:
|
|
14
14
|
* - alignment card: autonomy_mode = observe, integrity_mode = off,
|
|
15
15
|
* principal.relationship = delegated_authority, coding bounded/forbidden
|
|
16
|
-
* actions,
|
|
16
|
+
* actions, no default escalation triggers (the customer adds their own);
|
|
17
17
|
* - protection card: mode = off, thresholds 0.60/0.80/0.95 and all four
|
|
18
18
|
* screen_surfaces declared, ready for when it is turned on.
|
|
19
19
|
* - goal-alignment: nudge — sealed per-conversation via the goal contract
|
|
@@ -31,20 +31,21 @@
|
|
|
31
31
|
* (agents.ts), claimAgent + putAlignmentCard/putProtectionCard (lib/api.ts) — rather than
|
|
32
32
|
* reinventing it.
|
|
33
33
|
*/
|
|
34
|
-
import { claimAgent, getAgent, getAgentByName, getCardSources, getMe, getMuBalance, getMyPersonalOrg, MnemomApiError, mintProtectionCapability, putAlignmentCard, putProtectionCard, } from "../lib/api.js";
|
|
34
|
+
import { claimAgent, getAgent, getAgentByName, getCardSources, getMe, getMuBalance, getMyPersonalOrg, listMyAgents, MnemomApiError, mintProtectionCapability, putAlignmentCard, putProtectionCard, } from "../lib/api.js";
|
|
35
35
|
import { getAuthInfo, loginWithBrowser, loginWithDeviceFlow, resolveAuth } from "../lib/auth.js";
|
|
36
36
|
import { getActiveOrg } from "../lib/cli-config.js";
|
|
37
37
|
import { getApiUrl, getGatewayUrl, getWebsiteUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
38
38
|
import { fmt } from "../lib/format.js";
|
|
39
39
|
import { askYesNo, isInteractive } from "../lib/prompt.js";
|
|
40
|
-
import { resolveAnthropicDoor, resolveGatewayHost, sanitizeAgentName, singleCellError, } from "../lib/agent.js";
|
|
40
|
+
import { malformedAgentIdMessage, parseAgentRef, resolveAnthropicDoor, resolveGatewayHost, sanitizeAgentName, singleCellError, } from "../lib/agent.js";
|
|
41
|
+
import { readMnemomKey, resolveSubscriptionMnemomKey } from "../lib/subscription-login.js";
|
|
41
42
|
import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
|
|
42
|
-
import { IdentityRouteMissingError, resolveServedIdentity } from "../lib/agent-identity-route.js";
|
|
43
|
+
import { IdentityRouteMissingError, resolveServedIdentity, resolveServedIdentityDetail, } from "../lib/agent-identity-route.js";
|
|
43
44
|
import { readConfirmedAgent, readServedAgent, servedFingerprint, writeConfirmedAgent, writeServedAgent, } from "../lib/served-agent-cache.js";
|
|
44
45
|
import { deriveHashProof } from "./agents.js";
|
|
45
46
|
import { birthThroughGateway } from "./wrap.js";
|
|
46
|
-
import { resolveAnthropicKey } from "./agent.js";
|
|
47
|
-
import { assertCodeEnabled, CodeNotEnabledError, REQUEST_ACCESS_URL } from "./agent-invite.js";
|
|
47
|
+
import { defaultMnemomKeyDeps, resolveAnthropicKey } from "./agent.js";
|
|
48
|
+
import { assertCodeEnabled, CodeNotEnabledError, REQUEST_ACCESS_URL, sanitizeTerminalText, } from "./agent-invite.js";
|
|
48
49
|
const say = (line = "") => console.error(line);
|
|
49
50
|
/** The feature names `--governance <list>` accepts (plus `all` / `none`). */
|
|
50
51
|
export const GOVERNANCE_FEATURES = ["integrity", "safe-house"];
|
|
@@ -92,6 +93,105 @@ const modeOf = (on) => (on ? "observe" : "off");
|
|
|
92
93
|
export function describeModes(m) {
|
|
93
94
|
return `integrity ${m.integrity}, Safe House ${m.protection}`;
|
|
94
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* The governance mode ladder, weakest first. The same order mnemom-api composes
|
|
98
|
+
* on (compose.ts ALIGNMENT_MODE_ORDER and PROTECTION_MODE_ORDER): strictest wins.
|
|
99
|
+
*/
|
|
100
|
+
export const MODE_LADDER = ["off", "observe", "nudge", "enforce"];
|
|
101
|
+
/** A mode's rung on MODE_LADDER, or -1 when it is not on the ladder (absent, "unknown"). */
|
|
102
|
+
export function modeRank(mode) {
|
|
103
|
+
return mode === undefined ? -1 : MODE_LADDER.indexOf(mode);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* True when the composed mode sits on a higher rung than the agent's own card, so a
|
|
107
|
+
* stricter layer decides the mode that runs. An agent card that sets no mode, or a
|
|
108
|
+
* composed mode that is not on the ladder, is never an outrank.
|
|
109
|
+
*/
|
|
110
|
+
export function outranksAgentCard(own, effective) {
|
|
111
|
+
const mine = modeRank(own);
|
|
112
|
+
return mine >= 0 && modeRank(effective) > mine;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The upstream layers of a sources envelope whose own card sets `field` to `mode`,
|
|
116
|
+
* named for a person (`team "Lane Leads"`, `org "Acme"`, `platform`), in cascade
|
|
117
|
+
* order. Those are the layers that put the composed mode where it is. `platform`
|
|
118
|
+
* is asked for only where the platform card is a floor (protection mode); for
|
|
119
|
+
* integrity_mode it is only a default, so it never outranks an agent card that
|
|
120
|
+
* sets one. Empty when no layer row matches (an older api without the rows, or a
|
|
121
|
+
* composed card older than a template edit).
|
|
122
|
+
*/
|
|
123
|
+
export function layersSettingMode(sources, field, mode, opts) {
|
|
124
|
+
if (!sources)
|
|
125
|
+
return [];
|
|
126
|
+
// Org and team names are customer-written: drop control characters before they print.
|
|
127
|
+
const named = (kind, name, id) => name
|
|
128
|
+
? `${kind} "${sanitizeTerminalText(name)}"`
|
|
129
|
+
: id
|
|
130
|
+
? `${kind} ${sanitizeTerminalText(id)}`
|
|
131
|
+
: kind;
|
|
132
|
+
const out = [];
|
|
133
|
+
if (opts.platform && modeField(sources.platform?.card_json, field) === mode) {
|
|
134
|
+
out.push("platform");
|
|
135
|
+
}
|
|
136
|
+
const org = sources.org;
|
|
137
|
+
if (modeField(org?.card_json, field) === mode) {
|
|
138
|
+
out.push(named("org", org?.org_name, org?.org_id));
|
|
139
|
+
}
|
|
140
|
+
for (const team of sources.teams ?? []) {
|
|
141
|
+
if (modeField(team.card_json, field) === mode) {
|
|
142
|
+
out.push(named("team", team.team_name, team.team_id));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The composed Safe House (protection) mode for the launch summary, naming the
|
|
149
|
+
* layer that set it when the agent's own card says something else:
|
|
150
|
+
* `enforce (from team "Lane Leads"; agent card: observe)`. Same mode as the
|
|
151
|
+
* agent card prints the mode alone. No composed card reads `unknown`.
|
|
152
|
+
*/
|
|
153
|
+
export function describeComposedProtection(sources) {
|
|
154
|
+
const effective = modeField(sources?.composed?.card_json, "mode");
|
|
155
|
+
if (!effective)
|
|
156
|
+
return "unknown (no composed protection card yet)";
|
|
157
|
+
const own = modeField(sources?.agent?.card_json, "mode");
|
|
158
|
+
if (own === effective)
|
|
159
|
+
return sanitizeTerminalText(effective);
|
|
160
|
+
const layers = layersSettingMode(sources, "mode", effective, { platform: true });
|
|
161
|
+
const from = layers.length > 0
|
|
162
|
+
? `from ${layers.join(" and ")}`
|
|
163
|
+
: "from a stricter org, team or platform layer";
|
|
164
|
+
return sanitizeTerminalText(`${effective} (${from}; agent card: ${own ?? "none"})`);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* How long the launch waits for the protection card sources before printing the
|
|
168
|
+
* line without them. An object so tests can shorten it, like TWIN_LISTING.
|
|
169
|
+
*/
|
|
170
|
+
export const PROTECTION_READ = { timeoutMs: 5_000 };
|
|
171
|
+
/**
|
|
172
|
+
* The launch summary's protection phrase for `agentId`: one `getCardSources`
|
|
173
|
+
* read (`GET /v1/protection/agent/:id?include=sources`, the same read the doctor
|
|
174
|
+
* makes), which carries both the composed card and every layer's own card. Best
|
|
175
|
+
* effort: it never throws, so the caller can start it early and await it later,
|
|
176
|
+
* and a failed or slow read prints `unknown` rather than holding up the launch.
|
|
177
|
+
*/
|
|
178
|
+
export async function readComposedProtection(agentId) {
|
|
179
|
+
let timer;
|
|
180
|
+
const timeout = new Promise((_, reject) => {
|
|
181
|
+
timer = setTimeout(() => reject(new Error("timed out reading the protection card")), PROTECTION_READ.timeoutMs);
|
|
182
|
+
});
|
|
183
|
+
try {
|
|
184
|
+
const sources = await Promise.race([getCardSources("protection", agentId), timeout]);
|
|
185
|
+
return describeComposedProtection(sources);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
189
|
+
return `unknown (could not read the agent's protection card: ${sanitizeTerminalText(detail)})`;
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
95
195
|
/** The mode string a raw card carries in `field`, or undefined when absent/not a string. */
|
|
96
196
|
function modeField(card, field) {
|
|
97
197
|
const v = card?.[field];
|
|
@@ -220,6 +320,19 @@ export const SERVED_PROBE_TIMEOUT_MS = 10_000;
|
|
|
220
320
|
* three tries, about four seconds of backoff. Exported so tests can zero it.
|
|
221
321
|
*/
|
|
222
322
|
export const CONFIRM_RETRY_DELAYS_MS = [1000, 3000];
|
|
323
|
+
/** The gateway refused the Mnemom key an api-key launch sent alongside its Anthropic key. */
|
|
324
|
+
class MnemomKeyRejectedError extends Error {
|
|
325
|
+
constructor() {
|
|
326
|
+
super("the gateway rejected the Mnemom API key");
|
|
327
|
+
this.name = "MnemomKeyRejectedError";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/** The gateway's 401 for a Mnemom key it looked up and found invalid (gateway/src/index.ts). */
|
|
331
|
+
function isRejectedMnemomKey(err) {
|
|
332
|
+
return (err instanceof MnemomApiError &&
|
|
333
|
+
err.effectiveStatus === 401 &&
|
|
334
|
+
/invalid mnemom api key/i.test(err.message));
|
|
335
|
+
}
|
|
223
336
|
/**
|
|
224
337
|
* Mnemom couldn't be reached to answer (a transient failure that outlived its retries). Carries
|
|
225
338
|
* the served id when the gateway did answer before the api failed.
|
|
@@ -281,20 +394,36 @@ async function withRetry(call) {
|
|
|
281
394
|
* Interactive and non-interactive runs behave the same: there is no prompt to decline.
|
|
282
395
|
* MNE-7650: the name is client-controlled, so a same-named org agent proves nothing — only the
|
|
283
396
|
* served id counts.
|
|
397
|
+
*
|
|
398
|
+
* A PINNED launch (`opts.pin`, from `--agent mnm-<uuid>` / launcher.toml `agent_id`) takes a
|
|
399
|
+
* separate path (ensurePinnedAgent): the gateway must serve exactly the pin, and it never claims.
|
|
400
|
+
*
|
|
401
|
+
* An api-key launch that also carries a Mnemom key (`opts.mnemomKey` beside `opts.anthropicKey`)
|
|
402
|
+
* asks the probe with it too, so the answer is the one its requests will get. If the gateway
|
|
403
|
+
* rejects that key, the launch continues without it (result.mnemomKeyRejected) rather than fail.
|
|
284
404
|
*/
|
|
285
405
|
export async function ensureClaimedServedAgent(opts) {
|
|
286
406
|
const slug = opts.slug;
|
|
407
|
+
if (opts.pin)
|
|
408
|
+
return ensurePinnedAgent(opts, opts.pin);
|
|
287
409
|
const anchorKey = opts.anthropicKey || opts.mnemomKey;
|
|
288
410
|
if (!anchorKey) {
|
|
289
411
|
throw new UnclaimedAgentError(`mnemom agent: no credential anchors '${slug}' — an Anthropic key or a Mnemom API key is required.`);
|
|
290
412
|
}
|
|
291
413
|
const cacheDir = opts.cacheDir ?? MNEMOM_DIR;
|
|
292
|
-
const fp = servedFingerprint(anchorKey, slug, opts.gatewayHost.replace(/\/+$/, "")
|
|
414
|
+
const fp = servedFingerprint(anchorKey, slug, opts.gatewayHost.replace(/\/+$/, ""), {
|
|
415
|
+
withMnemomKey: Boolean(opts.anthropicKey && opts.mnemomKey),
|
|
416
|
+
});
|
|
293
417
|
let result;
|
|
294
418
|
try {
|
|
295
419
|
result = await confirmClaimed(opts, anchorKey, fp);
|
|
296
420
|
}
|
|
297
421
|
catch (err) {
|
|
422
|
+
if (err instanceof MnemomKeyRejectedError) {
|
|
423
|
+
say(fmt.warn("mnemom agent: the gateway rejected your stored Mnemom API key — launching without it (fix: `mnemom agent config` / re-run subscription setup, or unset MNEMOM_AGENT_MNEMOM_KEY)."));
|
|
424
|
+
const retried = await ensureClaimedServedAgent({ ...opts, mnemomKey: undefined });
|
|
425
|
+
return { ...retried, mnemomKeyRejected: true };
|
|
426
|
+
}
|
|
298
427
|
if (!(err instanceof MnemomUnreachableError))
|
|
299
428
|
throw err;
|
|
300
429
|
const confirmed = readConfirmedAgent(cacheDir, fp);
|
|
@@ -310,7 +439,9 @@ export async function ensureClaimedServedAgent(opts) {
|
|
|
310
439
|
async function confirmClaimed(opts, anchorKey, fp) {
|
|
311
440
|
const slug = opts.slug;
|
|
312
441
|
const agentId = await resolveServedAgent(opts, fp);
|
|
442
|
+
warnIfKeyReKeyed(opts, agentId);
|
|
313
443
|
if (await isClaimed(slug, agentId)) {
|
|
444
|
+
await warnIfNameAmbiguous(slug, agentId, opts.orgId);
|
|
314
445
|
if (opts.setup === true || (opts.governance && opts.setup !== false)) {
|
|
315
446
|
// An explicit `mnemom agent setup` / --setup repairs or completes the cards (e.g. a
|
|
316
447
|
// protection card that failed on a prior run before its one-time grant was minted).
|
|
@@ -335,6 +466,18 @@ async function confirmClaimed(opts, anchorKey, fp) {
|
|
|
335
466
|
throw new UnclaimedAgentError(`mnemom agent: '${slug}' (${agentId}) is not claimed, and this launch has no org to claim it into. ` +
|
|
336
467
|
"Choose one with `mnemom org use <org>`, then run `mnemom agent` again.");
|
|
337
468
|
}
|
|
469
|
+
// Anti-twin: the caller already has a claimed agent of this name in this org, yet the gateway
|
|
470
|
+
// served a NEW one (another credential on the same name — a rotated Anthropic key, a new
|
|
471
|
+
// machine). Claiming it would mint a same-named twin that splits the agent's history and cards
|
|
472
|
+
// (the 2026-10-07 agent-wassim split). Refuse and say how to pin the existing one instead.
|
|
473
|
+
//
|
|
474
|
+
// If the listing itself fails (api down, a timeout, an older api), the guard is skipped and
|
|
475
|
+
// the launch claims as before: refusing on a failed read would turn a Mnemom hiccup into a
|
|
476
|
+
// hard stop for every first launch, and the claim below is still the P0 rule's own gate.
|
|
477
|
+
const twins = await claimedSameNamedAgents(slug, agentId, opts.orgId).catch(() => null);
|
|
478
|
+
if (twins && twins.length > 0) {
|
|
479
|
+
throw new UnclaimedAgentError(twinRefusal(slug, agentId, twins, opts.orgLabel));
|
|
480
|
+
}
|
|
338
481
|
try {
|
|
339
482
|
const hashProof = deriveHashProof(anchorKey, slug);
|
|
340
483
|
await withRetry(() => claimAgent(agentId, { hashProof, orgId: opts.orgId }));
|
|
@@ -385,6 +528,8 @@ async function resolveServedAgent(opts, fp) {
|
|
|
385
528
|
return cached;
|
|
386
529
|
const served = await withRetry(() => birthThroughGateway(gatewayUrl, "anthropic", slug, anthropicKey, {
|
|
387
530
|
timeoutMs: opts.setup === true ? undefined : SERVED_PROBE_TIMEOUT_MS,
|
|
531
|
+
// Asked exactly as the launch's requests will be: with the Mnemom key when it sends one.
|
|
532
|
+
extraHeaders: opts.mnemomKey ? { "x-mnemom-api-key": opts.mnemomKey } : undefined,
|
|
388
533
|
}));
|
|
389
534
|
writeServedAgent(cacheDir, fp, served);
|
|
390
535
|
return served;
|
|
@@ -395,16 +540,247 @@ async function resolveServedAgent(opts, fp) {
|
|
|
395
540
|
return await withRetry(() => resolveServedIdentity(gatewayUrl, slug, mnemomKey, { timeoutMs: SERVED_PROBE_TIMEOUT_MS }));
|
|
396
541
|
}
|
|
397
542
|
catch (err) {
|
|
543
|
+
if (opts.anthropicKey && opts.mnemomKey && isRejectedMnemomKey(err)) {
|
|
544
|
+
throw new MnemomKeyRejectedError();
|
|
545
|
+
}
|
|
398
546
|
if (err instanceof IdentityRouteMissingError) {
|
|
399
547
|
throw new UnclaimedAgentError(`mnemom agent: ${err.message}, so it can't confirm '${slug}' is claimed. ` +
|
|
400
548
|
"Launch with an Anthropic API key instead (`--auth api-key`), or try again once the gateway is updated.");
|
|
401
549
|
}
|
|
550
|
+
if (err instanceof MnemomApiError && err.code === "agent_name_ambiguous") {
|
|
551
|
+
// The gateway won't pick between several of the caller's claimed agents of this name for a
|
|
552
|
+
// credential it has no agent for (and won't birth another). Name them when we can.
|
|
553
|
+
const ids = opts.orgId
|
|
554
|
+
? await claimedSameNamedAgents(slug, "", opts.orgId).catch(() => null)
|
|
555
|
+
: null;
|
|
556
|
+
const which = ids && ids.length > 0 ? ` (${ids.join(", ")})` : "";
|
|
557
|
+
throw new UnclaimedAgentError(`mnemom agent: several of your agents are named '${slug}'${which} and this launch's credential has none of its own, so the gateway won't choose (agent_name_ambiguous).\n` +
|
|
558
|
+
` Pin the one you mean: \`mnemom agent --agent mnm-<id>\`, or \`mnemom agent config set agent_id mnm-<id>\`.`);
|
|
559
|
+
}
|
|
402
560
|
if (isTransient(err))
|
|
403
561
|
throw new MnemomUnreachableError(err);
|
|
404
562
|
throw new UnclaimedAgentError(`mnemom agent: the gateway wouldn't say which agent it serves for '${slug}' (${describeApiError(err)}). ` +
|
|
405
563
|
"mnemom agent never runs on an agent it can't confirm is claimed.");
|
|
406
564
|
}
|
|
407
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* How long the anti-twin listing may take before it is treated as failed (best effort). An
|
|
568
|
+
* object so tests can shorten it, like CONFIRM_RETRY_DELAYS_MS.
|
|
569
|
+
*/
|
|
570
|
+
export const TWIN_LISTING = { timeoutMs: 8_000 };
|
|
571
|
+
/**
|
|
572
|
+
* The ids of the caller's OTHER live, claimed agents named `slug` in `orgId` — the same set the
|
|
573
|
+
* gateway's principal resolution picks from (claimed_by = the caller). Reads the caller's own
|
|
574
|
+
* agents (`GET /v1/agents?scope=all`), then each same-named one's full record for its claim and
|
|
575
|
+
* org; a tombstoned one (404) is skipped. Throws when the listing can't be completed in
|
|
576
|
+
* TWIN_LISTING.timeoutMs — every caller treats that as "unknown", never as "none".
|
|
577
|
+
*/
|
|
578
|
+
export async function claimedSameNamedAgents(slug, excludeId, orgId) {
|
|
579
|
+
const work = async () => {
|
|
580
|
+
const want = slug.toLowerCase();
|
|
581
|
+
const named = (await listMyAgents()).filter((a) => a.id !== excludeId && (a.name ?? "").toLowerCase() === want);
|
|
582
|
+
if (named.length === 0)
|
|
583
|
+
return [];
|
|
584
|
+
const me = (await getMe()).user_id;
|
|
585
|
+
const out = [];
|
|
586
|
+
for (const row of named) {
|
|
587
|
+
let agent;
|
|
588
|
+
try {
|
|
589
|
+
agent = await getAgent(row.id);
|
|
590
|
+
}
|
|
591
|
+
catch (err) {
|
|
592
|
+
if (err instanceof MnemomApiError && err.effectiveStatus === 404)
|
|
593
|
+
continue;
|
|
594
|
+
throw err;
|
|
595
|
+
}
|
|
596
|
+
if (agent.claimed_by && agent.claimed_by === me && agent.org_id === orgId)
|
|
597
|
+
out.push(row.id);
|
|
598
|
+
}
|
|
599
|
+
return out;
|
|
600
|
+
};
|
|
601
|
+
let timer;
|
|
602
|
+
const timeout = new Promise((_, reject) => {
|
|
603
|
+
timer = setTimeout(() => reject(new Error("timed out listing same-named agents")), TWIN_LISTING.timeoutMs);
|
|
604
|
+
});
|
|
605
|
+
try {
|
|
606
|
+
return await Promise.race([work(), timeout]);
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
clearTimeout(timer);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/** The refusal when an unclaimed served agent would become a same-named twin. */
|
|
613
|
+
export function twinRefusal(slug, servedId, twins, orgLabel) {
|
|
614
|
+
const ids = twins.join(", ");
|
|
615
|
+
const example = twins[0];
|
|
616
|
+
return (`mnemom agent: you already have ${twins.length === 1 ? "an agent" : `${twins.length} agents`} named '${slug}' in ${orgLabel} (${ids}), ` +
|
|
617
|
+
`but this launch's credential is served a new, unclaimed one (${servedId}). ` +
|
|
618
|
+
"Claiming it would create a same-named twin, so mnemom agent stopped without claiming.\n" +
|
|
619
|
+
` Pin the agent you mean: \`mnemom agent --agent ${example}\`, or set it once with \`mnemom agent config set agent_id ${example}\`.`);
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* A served agent that is already claimed while OTHER claimed agents of the caller share its name
|
|
623
|
+
* in this org: say once which one this launch runs as, and how to choose. Never refuses (that
|
|
624
|
+
* would break every launch that works today); a failed listing says nothing.
|
|
625
|
+
*/
|
|
626
|
+
async function warnIfNameAmbiguous(slug, servedId, orgId) {
|
|
627
|
+
if (!orgId)
|
|
628
|
+
return;
|
|
629
|
+
const others = await claimedSameNamedAgents(slug, servedId, orgId).catch(() => null);
|
|
630
|
+
if (!others || others.length === 0)
|
|
631
|
+
return;
|
|
632
|
+
say(fmt.dim(`mnemom agent: runs as ${servedId}; ${others.length + 1} of your agents are named '${slug}' (also ${others.join(", ")}) — pin one with --agent mnm-<id> or agent_id in launcher.toml.`));
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* An api-key launch that carries this machine's stored Mnemom key (the safe re-land of #2291)
|
|
636
|
+
* can be served a DIFFERENT agent than the same key + name without it — that's the entire point
|
|
637
|
+
* (the gateway's key-principal resolution; see CLAUDE.md "Changing resolveRequestIdentity
|
|
638
|
+
* re-keys scoped gateway state"). Every per-conversation key the gateway stores (fold state,
|
|
639
|
+
* compaction snapshots, goal contracts) is prefixed by the served agent id, so routing a launch
|
|
640
|
+
* to a different one silently strands that state behind the one this machine used to run as.
|
|
641
|
+
*
|
|
642
|
+
* This never refuses (the re-land is deliberate and the new agent may be exactly the right one
|
|
643
|
+
* — e.g. the key owner's own claimed agent), but it must not happen silently: say so, name both
|
|
644
|
+
* ids, and say how to keep running as the old one (pin it).
|
|
645
|
+
*/
|
|
646
|
+
function warnIfKeyReKeyed(opts, agentId) {
|
|
647
|
+
if (!opts.anthropicKey || !opts.mnemomKey)
|
|
648
|
+
return; // not an api-key launch carrying a key
|
|
649
|
+
const cacheDir = opts.cacheDir ?? MNEMOM_DIR;
|
|
650
|
+
const plainFp = servedFingerprint(opts.anthropicKey, opts.slug, opts.gatewayHost.replace(/\/+$/, ""));
|
|
651
|
+
const prior = readConfirmedAgent(cacheDir, plainFp);
|
|
652
|
+
if (!prior || prior === agentId)
|
|
653
|
+
return;
|
|
654
|
+
say(fmt.warn(`mnemom agent: carrying this machine's stored Mnemom key now serves '${opts.slug}' as ${agentId}, not ${prior} — the agent this machine last confirmed without that key. ` +
|
|
655
|
+
`${prior}'s goal history and compaction snapshots stay with ${prior}; they do not follow this launch. ` +
|
|
656
|
+
`To keep running as ${prior}, pin it: \`mnemom agent <scenario> --agent ${prior}\` (or \`agent_id\` in launcher.toml).`));
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Resolve a pin before launch: the agent must be visible to the caller (the org-member view) and
|
|
660
|
+
* claimed; its name becomes the launch's slug (`x-mnemom-agent`, which the gateway checks against
|
|
661
|
+
* the pin). Any failure refuses — a pinned launch never falls back to a name.
|
|
662
|
+
*/
|
|
663
|
+
export async function resolvePinnedAgent(id) {
|
|
664
|
+
let agent;
|
|
665
|
+
try {
|
|
666
|
+
agent = await withRetry(() => getAgent(id));
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
const status = err instanceof MnemomApiError ? err.effectiveStatus : undefined;
|
|
670
|
+
if (status === 404 || status === 403) {
|
|
671
|
+
throw new UnclaimedAgentError(`mnemom agent: agent ${id} was not found — it doesn't exist, was deleted, or isn't in an org you belong to. ` +
|
|
672
|
+
"See your agents with `mnemom agent list`.");
|
|
673
|
+
}
|
|
674
|
+
throw new UnclaimedAgentError(`mnemom agent: couldn't read the pinned agent ${id} (${describeApiError(err)}); try again. A pinned launch never falls back to another agent.`);
|
|
675
|
+
}
|
|
676
|
+
// The org-member view carries claimed_by; the public projection (someone else's public agent)
|
|
677
|
+
// does not — and a pin to it would be refused by the gateway anyway.
|
|
678
|
+
if (agent.claimed_by === undefined || (agent.caller && agent.caller !== "org_member")) {
|
|
679
|
+
throw new UnclaimedAgentError(`mnemom agent: agent ${id} isn't in an org you belong to, so this launch can't pin it. See your agents with \`mnemom agent list\`.`);
|
|
680
|
+
}
|
|
681
|
+
if (!agent.claimed_by) {
|
|
682
|
+
throw new UnclaimedAgentError(`mnemom agent: agent ${id} is not claimed, and mnemom agent never runs on an unclaimed agent. ` +
|
|
683
|
+
`Claim it first: \`mnemom agent claim ${id} --key <key>\`.`);
|
|
684
|
+
}
|
|
685
|
+
const slug = sanitizeAgentName(agent.name ?? "");
|
|
686
|
+
if (!slug) {
|
|
687
|
+
throw new UnclaimedAgentError(`mnemom agent: agent ${id} has no usable name to send as x-mnemom-agent; rename it, or launch by name.`);
|
|
688
|
+
}
|
|
689
|
+
return { id, slug };
|
|
690
|
+
}
|
|
691
|
+
/** The refusal for a pinned launch the gateway did not (or could not) honour. */
|
|
692
|
+
function pinRefusal(err, pin, slug) {
|
|
693
|
+
const head = `mnemom agent: the gateway would not run this launch as ${pin}`;
|
|
694
|
+
if (err instanceof IdentityRouteMissingError) {
|
|
695
|
+
return `${head}: ${err.message}, so it can't pin yet. Launch by name (drop --agent ${pin} / agent_id) or try again once the gateway is updated.`;
|
|
696
|
+
}
|
|
697
|
+
const code = err instanceof MnemomApiError ? err.code : undefined;
|
|
698
|
+
const status = err instanceof MnemomApiError ? err.effectiveStatus : undefined;
|
|
699
|
+
if (code === "agent_pin_forbidden") {
|
|
700
|
+
return (`${head} (agent_pin_forbidden): it is not your claimed agent, or not in the org of the Mnemom API key this launch sends. ` +
|
|
701
|
+
"See your agents with `mnemom agent list`; a key minted in another org can't pin it.");
|
|
702
|
+
}
|
|
703
|
+
if (code === "agent_pin_mismatch") {
|
|
704
|
+
return `${head} (agent_pin_mismatch): its name is not '${slug}'. Pass the id alone (--agent ${pin}) so the launcher sends the agent's own name.`;
|
|
705
|
+
}
|
|
706
|
+
if (code === "agent_name_ambiguous") {
|
|
707
|
+
return `${head} (agent_name_ambiguous): it reports several of your agents named '${slug}' without honouring the pin, so it can't pin yet. Try again once the gateway is updated.`;
|
|
708
|
+
}
|
|
709
|
+
if (code === "mnemom_api_key_required" || status === 401) {
|
|
710
|
+
return (`${head}: a pin needs a valid Mnemom API key (${code ?? "401"}). ` +
|
|
711
|
+
"Set MNEMOM_AGENT_MNEMOM_KEY, or delete mnemom_api_key from ~/.mnemom/launcher.json so the next pinned launch mints a fresh one.");
|
|
712
|
+
}
|
|
713
|
+
return `${head} (${describeApiError(err)}). A pinned launch never falls back to another agent.`;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* A pinned launch: ask the gateway's identity route which agent it serves for (Mnemom key, name,
|
|
717
|
+
* pin) on EVERY auth mode, and run only when it answers exactly the pin with `resolved_by:
|
|
718
|
+
* "pin"`. A gateway that predates pinning ignores the header and answers by name — that is a
|
|
719
|
+
* different agent, or the right one by coincidence for the wrong reason — so anything else
|
|
720
|
+
* refuses. Never claims, never births. The claim is re-checked here (P0) even though the
|
|
721
|
+
* gateway only honours a pin to the caller's own claimed agent. A transient failure may fall back
|
|
722
|
+
* to this machine's last confirmation of the SAME pin, exactly like the unpinned outage fallback.
|
|
723
|
+
*/
|
|
724
|
+
async function ensurePinnedAgent(opts, pin) {
|
|
725
|
+
const { slug } = opts;
|
|
726
|
+
const mnemomKey = opts.mnemomKey;
|
|
727
|
+
if (!mnemomKey) {
|
|
728
|
+
throw new UnclaimedAgentError(`mnemom agent: pinning ${pin} needs a Mnemom API key (the gateway honours a pin only with one). Set MNEMOM_AGENT_MNEMOM_KEY.`);
|
|
729
|
+
}
|
|
730
|
+
const gatewayUrl = opts.gatewayHost.replace(/\/+$/, "");
|
|
731
|
+
const cacheDir = opts.cacheDir ?? MNEMOM_DIR;
|
|
732
|
+
const fp = servedFingerprint(opts.anthropicKey || mnemomKey, slug, gatewayUrl, {
|
|
733
|
+
withMnemomKey: true,
|
|
734
|
+
pin,
|
|
735
|
+
});
|
|
736
|
+
const outageFallback = (reason) => {
|
|
737
|
+
if (readConfirmedAgent(cacheDir, fp) === pin) {
|
|
738
|
+
say(fmt.warn(`mnemom agent: couldn't reach Mnemom to confirm the pin (${describeApiError(reason)}) — launching on ${pin}, which this machine last confirmed.`));
|
|
739
|
+
return { slug, agentId: pin, claimedNow: false };
|
|
740
|
+
}
|
|
741
|
+
throw new UnclaimedAgentError(`mnemom agent: couldn't reach Mnemom to confirm the pinned agent ${pin} (${describeApiError(reason)}); try again.`);
|
|
742
|
+
};
|
|
743
|
+
let served;
|
|
744
|
+
try {
|
|
745
|
+
served = await withRetry(() => resolveServedIdentityDetail(gatewayUrl, slug, mnemomKey, {
|
|
746
|
+
timeoutMs: SERVED_PROBE_TIMEOUT_MS,
|
|
747
|
+
pin,
|
|
748
|
+
}));
|
|
749
|
+
}
|
|
750
|
+
catch (err) {
|
|
751
|
+
if (isTransient(err))
|
|
752
|
+
return outageFallback(err);
|
|
753
|
+
throw new UnclaimedAgentError(pinRefusal(err, pin, slug));
|
|
754
|
+
}
|
|
755
|
+
if (served.agentId !== pin || served.resolvedBy !== "pin") {
|
|
756
|
+
throw new UnclaimedAgentError(`mnemom agent: this gateway can't pin yet — asked for ${pin}, it answered ${served.agentId} (${served.resolvedBy ? `resolved by ${served.resolvedBy}` : "by name"}). ` +
|
|
757
|
+
`A pinned launch never runs as a different agent: launch by name (drop --agent ${pin} / agent_id), or try again once the gateway is updated.`);
|
|
758
|
+
}
|
|
759
|
+
let claimed;
|
|
760
|
+
try {
|
|
761
|
+
claimed = await isClaimed(slug, pin);
|
|
762
|
+
}
|
|
763
|
+
catch (err) {
|
|
764
|
+
if (err instanceof MnemomUnreachableError)
|
|
765
|
+
return outageFallback(err.reason);
|
|
766
|
+
throw err;
|
|
767
|
+
}
|
|
768
|
+
if (!claimed) {
|
|
769
|
+
throw new UnclaimedAgentError(`mnemom agent: the pinned agent ${pin} is not claimed, and mnemom agent never runs on an unclaimed agent. ` +
|
|
770
|
+
`Claim it first: \`mnemom agent claim ${pin} --key <key>\`.`);
|
|
771
|
+
}
|
|
772
|
+
if (opts.setup === true || (opts.governance && opts.setup !== false)) {
|
|
773
|
+
say(fmt.dim(`mnemom agent: pinned to ${pin} ('${slug}') — refreshing its cards.`));
|
|
774
|
+
const modes = await existingAgentModes(pin, opts.governance);
|
|
775
|
+
if (modes)
|
|
776
|
+
await publishDefaultCards(pin, modes);
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
say(fmt.dim(`mnemom agent: pinned to ${pin} ('${slug}'), claimed.`));
|
|
780
|
+
}
|
|
781
|
+
writeConfirmedAgent(cacheDir, fp, pin);
|
|
782
|
+
return { slug, agentId: pin, claimedNow: false };
|
|
783
|
+
}
|
|
408
784
|
/**
|
|
409
785
|
* Whether `agentId` is claimed. A 404/403 reads as unclaimed (an unclaimed sandbox agent can be
|
|
410
786
|
* invisible to a normal user), so the caller claims it. A transient failure is retried and then
|
|
@@ -521,8 +897,17 @@ async function publishDefaultCards(agentId, modes) {
|
|
|
521
897
|
say(fmt.dim(` The agent is claimed and running; finish it later with \`${publishCmd}\`.`));
|
|
522
898
|
}
|
|
523
899
|
}
|
|
524
|
-
/**
|
|
900
|
+
/**
|
|
901
|
+
* `mnemom agent setup [--agent <slug|mnm-id>] [--governance …]` — the explicit provisioning entry
|
|
902
|
+
* point. An id (`--agent mnm-<uuid>`, or launcher.toml `agent_id`) refreshes that pinned agent's
|
|
903
|
+
* cards and never claims or births; a malformed `mnm-…` refuses.
|
|
904
|
+
*/
|
|
525
905
|
export async function agentSetupCommand(opts = {}) {
|
|
906
|
+
const rawRef = opts.agent?.trim() || opts.agentId?.trim() || "";
|
|
907
|
+
const ref = rawRef ? parseAgentRef(rawRef) : null;
|
|
908
|
+
if (ref?.kind === "malformed-id") {
|
|
909
|
+
throw new Error(malformedAgentIdMessage(ref.raw, opts.agent?.trim() ? "--agent" : "agent_id").replace("mnemom agent:", "mnemom agent setup:"));
|
|
910
|
+
}
|
|
526
911
|
const gatewayHost = resolveGatewayHost(process.env);
|
|
527
912
|
const cellError = singleCellError({
|
|
528
913
|
door: resolveAnthropicDoor(process.env),
|
|
@@ -542,16 +927,31 @@ export async function agentSetupCommand(opts = {}) {
|
|
|
542
927
|
throw err;
|
|
543
928
|
}
|
|
544
929
|
const anthropicKey = await resolveAnthropicKey(process.env, { allowPrompt: true });
|
|
930
|
+
if (ref?.kind === "id") {
|
|
931
|
+
const pinned = await resolvePinnedAgent(ref.id);
|
|
932
|
+
await ensureClaimedServedAgent({
|
|
933
|
+
slug: pinned.slug,
|
|
934
|
+
anthropicKey,
|
|
935
|
+
mnemomKey: await resolveSubscriptionMnemomKey(defaultMnemomKeyDeps(), "pinning an agent"),
|
|
936
|
+
pin: pinned.id,
|
|
937
|
+
gatewayHost,
|
|
938
|
+
orgId: account.orgId,
|
|
939
|
+
orgLabel: account.orgLabel,
|
|
940
|
+
setup: true,
|
|
941
|
+
governance: opts.governance,
|
|
942
|
+
});
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
545
945
|
// Session exists now, so derive the real handle from /v1/auth/me. Strict: an
|
|
546
946
|
// explicit setup never silently provisions "agent-user" (MNE-7650).
|
|
547
|
-
const slug =
|
|
548
|
-
? sanitizeAgentName(opts.agent.trim())
|
|
549
|
-
: await resolveDefaultSlug({ strict: true });
|
|
947
|
+
const slug = ref?.kind === "name" ? sanitizeAgentName(ref.name) : await resolveDefaultSlug({ strict: true });
|
|
550
948
|
if (!slug)
|
|
551
949
|
throw new Error("mnemom agent setup: agent slug sanitised to empty — pass --agent <slug>.");
|
|
552
950
|
await ensureClaimedServedAgent({
|
|
553
951
|
slug,
|
|
554
952
|
anthropicKey,
|
|
953
|
+
// Asked the way a launch asks: with this machine's Mnemom key when it has one (never minted).
|
|
954
|
+
mnemomKey: readMnemomKey(defaultMnemomKeyDeps()),
|
|
555
955
|
gatewayHost,
|
|
556
956
|
orgId: account.orgId,
|
|
557
957
|
orgLabel: account.orgLabel,
|
|
@@ -780,13 +1180,9 @@ export function buildCodeAlignmentCard(agentId, modes = {
|
|
|
780
1180
|
"exfiltrate_credentials",
|
|
781
1181
|
"modify_ci_secrets",
|
|
782
1182
|
],
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
action: "escalate",
|
|
787
|
-
reason: "a human should confirm before irreversible changes",
|
|
788
|
-
},
|
|
789
|
-
],
|
|
1183
|
+
// No default "confirm irreversible operations" trigger (operator 2026-10-09):
|
|
1184
|
+
// which operations need a human is the customer's call, added on their own card.
|
|
1185
|
+
escalation_triggers: [],
|
|
790
1186
|
},
|
|
791
1187
|
audit: { retention_days: 30, queryable: false, trace_format: "otel" },
|
|
792
1188
|
};
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* # --help prints usage; unknown flags are refused
|
|
11
11
|
* mnemom agent slack delete [--channel C] <ts> # delete a message THIS session posted
|
|
12
12
|
* mnemom agent slack inbox [--peek] # unread messages addressed to THIS session
|
|
13
|
+
* mnemom agent slack alias # list handle → session aliases
|
|
14
|
+
* mnemom agent slack alias <handle> <session> # route @handle to <session> (idempotent)
|
|
15
|
+
* mnemom agent slack alias --remove <handle> # stop routing @handle
|
|
13
16
|
* mnemom agent slack listen # this session's channel server (spawned by --slack)
|
|
14
17
|
* mnemom agent slack router # the per-operator Socket Mode router (foreground)
|
|
15
18
|
*
|
|
@@ -33,4 +36,5 @@ export declare const POST_USAGE = "Usage: mnemom agent slack post [options] [--]
|
|
|
33
36
|
* never be posted as message text. Everything after `--` is text, verbatim.
|
|
34
37
|
*/
|
|
35
38
|
export declare function parsePostArgs(argv: string[]): PostArgs;
|
|
39
|
+
export declare const ALIAS_USAGE = "Usage: mnemom agent slack alias # list\n mnemom agent slack alias <handle> <session> # route @handle to <session>\n mnemom agent slack alias --remove <handle> # stop routing @handle\n\nA handle is letters, digits and dashes (a leading @ is fine). The session is the name\n`mnemom agent slack status` lists under \"listening\" (e.g. agent-hdc-henry-dorsett-case);\nit need not be live yet. Setting the same pair again is a no-op.\n\nAliases only route when `mnemom agent config set slack_aliases true` is set (off by\ndefault). Then a message for a session that is not listening is held: the sender is\ntold it is waiting, and if the session has not read it within an hour, that it was\nnot delivered.";
|
|
36
40
|
export {};
|