@mnemom/mnemom 0.17.4-next.5 → 0.17.4-next.6

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.
@@ -74,11 +74,20 @@ export interface EnsureAgentOptions {
74
74
  gatewayHost: string;
75
75
  /** Override interactivity (defaults to isInteractive()). */
76
76
  interactive?: boolean;
77
+ /** Where the served-agent cache lives (defaults to ~/.mnemom). */
78
+ cacheDir?: string;
77
79
  }
80
+ /** A bare launch never waits on the served-agent probe longer than this. */
81
+ export declare const SERVED_PROBE_TIMEOUT_MS = 10000;
78
82
  export interface EnsureAgentResult {
79
83
  slug: string;
80
84
  /** true only when this call actually birthed/claimed/published. */
81
85
  provisioned: boolean;
86
+ /**
87
+ * The mnm- id the gateway serves for (key, slug), when this call learned it (a verified name
88
+ * match, a mismatch, or a fresh birth). Absent when it never asked the gateway.
89
+ */
90
+ agentId?: string;
82
91
  }
83
92
  /**
84
93
  * Ensure `slug` is a usable governed agent in the customer's org. Returns
@@ -26,11 +26,12 @@
26
26
  import { claimAgent, getAgentByName, getMe, getMuBalance, getMyPersonalOrg, MnemomApiError, mintProtectionCapability, putAlignmentCard, putProtectionCard, } from "../lib/api.js";
27
27
  import { getAuthInfo, isLoggedIn, loginWithBrowser, loginWithDeviceFlow, resolveAuth, } from "../lib/auth.js";
28
28
  import { getActiveOrg } from "../lib/cli-config.js";
29
- import { getApiUrl, getGatewayUrl, getWebsiteUrl } from "../lib/config.js";
29
+ import { getApiUrl, getGatewayUrl, getWebsiteUrl, MNEMOM_DIR } from "../lib/config.js";
30
30
  import { fmt } from "../lib/format.js";
31
31
  import { askYesNo, isInteractive } from "../lib/prompt.js";
32
32
  import { resolveAnthropicDoor, resolveGatewayHost, sanitizeAgentName, singleCellError, } from "../lib/agent.js";
33
33
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
34
+ import { readServedAgent, servedFingerprint, writeServedAgent } from "../lib/served-agent-cache.js";
34
35
  import { deriveHashProof } from "./agents.js";
35
36
  import { birthThroughGateway } from "./wrap.js";
36
37
  import { resolveAnthropicKey } from "./agent.js";
@@ -104,6 +105,8 @@ export class AgentIdentityMismatchError extends Error {
104
105
  this.name = "AgentIdentityMismatchError";
105
106
  }
106
107
  }
108
+ /** A bare launch never waits on the served-agent probe longer than this. */
109
+ export const SERVED_PROBE_TIMEOUT_MS = 10_000;
107
110
  /**
108
111
  * Ensure `slug` is a usable governed agent in the customer's org. Returns
109
112
  * {provisioned:false} when it is already usable, when provisioning is not
@@ -115,33 +118,61 @@ export class AgentIdentityMismatchError extends Error {
115
118
  */
116
119
  export async function ensureGovernedAgent(opts) {
117
120
  const slug = opts.slug;
121
+ const cacheDir = opts.cacheDir ?? MNEMOM_DIR;
118
122
  const interactive = opts.interactive ?? isInteractive();
119
123
  // 1. Already usable? A named agent in the caller's fleet is claimed by them.
120
124
  const loggedIn = await isLoggedIn();
121
125
  if (loggedIn) {
122
126
  const existing = await getAgentByName(slug).catch(() => null);
123
127
  if (existing) {
128
+ // MNE-7650: the name is client-controlled and the API exposes no
129
+ // agent_hash, so a name match proves nothing about which agent the
130
+ // gateway will serve for (key, slug) — it looks agents up by
131
+ // hash(credential, name) only. Ask the gateway on EVERY launch, not just
132
+ // under --setup: the same 16-token "hello" the birth path uses answers
133
+ // with the served mnm- id on x-mnemom-agent. A bare launch that trusted
134
+ // the name recorded and reported an org agent while the gateway governed
135
+ // a different, unclaimed one (a second Anthropic key on a shared name).
136
+ //
137
+ // The answer is stable per (key, name, gateway), so a bare launch reuses a cached one and
138
+ // only probes (time-boxed) the first time; --setup always re-asks.
139
+ const gatewayUrl = opts.gatewayHost.replace(/\/+$/, "");
140
+ const fp = servedFingerprint(opts.anthropicKey, slug, gatewayUrl);
141
+ const cached = opts.setup === true ? null : readServedAgent(cacheDir, fp);
142
+ let served;
143
+ try {
144
+ served =
145
+ cached ??
146
+ (await birthThroughGateway(gatewayUrl, "anthropic", slug, opts.anthropicKey, {
147
+ timeoutMs: opts.setup === true ? undefined : SERVED_PROBE_TIMEOUT_MS,
148
+ }));
149
+ }
150
+ catch (err) {
151
+ // --setup must know which agent it governs; a bare launch carries on
152
+ // with the id unresolved rather than guessing it from the name.
153
+ if (opts.setup === true)
154
+ throw err;
155
+ say(fmt.dim(`mnemom agent: couldn't confirm which agent the gateway serves for '${slug}' (${err instanceof Error ? err.message : String(err)}) — launching anyway.`));
156
+ return { slug, provisioned: false };
157
+ }
158
+ if (!cached)
159
+ writeServedAgent(cacheDir, fp, served);
160
+ if (served !== existing.id) {
161
+ if (opts.setup === true)
162
+ throw new AgentIdentityMismatchError(slug, existing.id, served);
163
+ warnServedMismatch(slug, existing.id, served);
164
+ return { slug, provisioned: false, agentId: served };
165
+ }
124
166
  // On an EXPLICIT `mnemom agent setup`, repair/complete the cards on the
125
167
  // already-claimed agent (e.g. a protection card that failed on a prior run
126
168
  // before we minted its one-time grant). A bare launch leaves it alone.
127
169
  if (opts.setup === true) {
128
- // MNE-7650: the name is client-controlled and the API exposes no
129
- // agent_hash, so a name match proves nothing about which agent the
130
- // gateway will serve for (key, slug) — it looks agents up by
131
- // hash(credential, name) only. Ask the gateway: the same 16-token
132
- // "hello" the birth path uses answers with the served mnm- id on
133
- // x-mnemom-agent. Govern that agent or none.
134
- const gatewayUrl = opts.gatewayHost.replace(/\/+$/, "");
135
- const served = await birthThroughGateway(gatewayUrl, "anthropic", slug, opts.anthropicKey);
136
- if (served !== existing.id) {
137
- throw new AgentIdentityMismatchError(slug, existing.id, served);
138
- }
139
170
  say(fmt.dim(`mnemom agent: '${slug}' is already claimed (${existing.id}, the agent the gateway serves) — refreshing its cards.`));
140
171
  await publishDefaultCards(existing.id);
141
172
  persistAgentSlug(slug);
142
- return { slug, provisioned: true };
173
+ return { slug, provisioned: true, agentId: existing.id };
143
174
  }
144
- return { slug, provisioned: false };
175
+ return { slug, provisioned: false, agentId: existing.id };
145
176
  }
146
177
  }
147
178
  // 2. Are we allowed to provision here?
@@ -186,7 +217,17 @@ export async function ensureGovernedAgent(opts) {
186
217
  await publishDefaultCards(agentId);
187
218
  // 6. Remember the slug so future launches skip straight to usable.
188
219
  persistAgentSlug(slug);
189
- return { slug, provisioned: true };
220
+ return { slug, provisioned: true, agentId };
221
+ }
222
+ /**
223
+ * A bare launch whose name matches an org agent the gateway does NOT serve for this key. Not
224
+ * fatal (no --setup asked to govern it), but the user must know their governance lands on the
225
+ * served agent, and how to get a governed one of their own.
226
+ */
227
+ function warnServedMismatch(slug, namedId, servedId) {
228
+ say(fmt.warn(`mnemom agent: '${slug}' in your org is ${namedId}, but the gateway serves ${servedId} for this Anthropic key + name — this session is governed as ${servedId}, which is not claimed to your org.`));
229
+ say(fmt.dim(" The gateway identity is hash(Anthropic key, name), so another key on the same name is a different agent. " +
230
+ `Use a name of your own: \`mnemom agent <scenario> --agent <unique-name> --setup\` (or claim the served one: \`mnemom agent claim ${servedId} --key <this key>\`).`));
190
231
  }
191
232
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
192
233
  /**
@@ -141,6 +141,11 @@ export declare function recordSessionLaunch(ctx: {
141
141
  session: string;
142
142
  conversationId: string;
143
143
  agent: string;
144
+ /**
145
+ * The id the gateway serves, as setup learned it (null = asked, unknown). Undefined = setup
146
+ * never asked, so fall back to the org's name lookup.
147
+ */
148
+ agentId?: string | null;
144
149
  account: {
145
150
  orgId: string;
146
151
  orgLabel: string;
@@ -297,19 +297,24 @@ export async function agentCommand(scenarioArg, options = {}) {
297
297
  // provisions-with-confirm when interactive; warns and continues otherwise.
298
298
  // Skipped entirely by `--no-setup`. A provisioning failure never blocks the
299
299
  // launch — it degrades to the unclaimed (fail-open) identity with a warning.
300
+ // The agent id the gateway serves, when setup asked it (undefined = never asked, so the ledger
301
+ // falls back to the org's name lookup).
302
+ let servedAgentId;
300
303
  if (options.setup !== false && !anthropicKey) {
301
304
  // Subscription login: the gateway creates the agent in the Mnemom key's org on the first
302
305
  // turn, so there is nothing to birth or claim here.
303
306
  say(`mnemom agent: subscription login — '${agent}' is created in your org on the first turn (identity: your Mnemom API key).`);
304
307
  }
305
308
  else if (options.setup !== false && anthropicKey) {
309
+ servedAgentId = null; // asked below; stays null if setup can't tell
306
310
  try {
307
- await ensureGovernedAgent({
311
+ const ensured = await ensureGovernedAgent({
308
312
  slug: agent,
309
313
  anthropicKey,
310
314
  setup: options.setup,
311
315
  gatewayHost,
312
316
  });
317
+ servedAgentId = ensured.agentId ?? null;
313
318
  }
314
319
  catch (err) {
315
320
  // An identity mismatch is the one setup failure that must NOT degrade: the
@@ -368,6 +373,7 @@ export async function agentCommand(scenarioArg, options = {}) {
368
373
  session: scenario,
369
374
  conversationId,
370
375
  agent,
376
+ agentId: servedAgentId,
371
377
  account,
372
378
  gatewayHost,
373
379
  shape,
@@ -523,12 +529,19 @@ export async function resolveLaunchConversationId(input) {
523
529
  * ledger write failure warns to stderr and the launch proceeds regardless.
524
530
  */
525
531
  export async function recordSessionLaunch(ctx) {
532
+ // Never re-derive a known id from the name: the name can match an org agent the gateway does
533
+ // not serve for this key (MNE-7650), which is exactly what the ledger must not record.
526
534
  let agentId;
527
- try {
528
- agentId = (await getAgentByName(ctx.agent))?.id ?? null;
535
+ if (ctx.agentId !== undefined) {
536
+ agentId = ctx.agentId;
529
537
  }
530
- catch {
531
- agentId = null; // unresolvable right now — the ledger still records the launch
538
+ else {
539
+ try {
540
+ agentId = (await getAgentByName(ctx.agent))?.id ?? null;
541
+ }
542
+ catch {
543
+ agentId = null; // unresolvable right now — the ledger still records the launch
544
+ }
532
545
  }
533
546
  const result = appendSessionRecord({
534
547
  session: ctx.session,
@@ -24,5 +24,7 @@ export interface WrapOptions {
24
24
  providerKey?: string;
25
25
  }
26
26
  export declare function wrapCommand(options?: WrapOptions): Promise<void>;
27
- export declare function birthThroughGateway(gatewayUrl: string, provider: Provider, agentName: string, providerKey: string): Promise<string>;
27
+ export declare function birthThroughGateway(gatewayUrl: string, provider: Provider, agentName: string, providerKey: string, opts?: {
28
+ timeoutMs?: number;
29
+ }): Promise<string>;
28
30
  export {};
@@ -199,7 +199,7 @@ async function resolveProviderKey(options, prov, nonInteractive, say) {
199
199
  return key;
200
200
  }
201
201
  // ── Birth call ────────────────────────────────────────────────────────────────
202
- export async function birthThroughGateway(gatewayUrl, provider, agentName, providerKey) {
202
+ export async function birthThroughGateway(gatewayUrl, provider, agentName, providerKey, opts = {}) {
203
203
  const prov = PROVIDERS[provider];
204
204
  const authValue = provider === "openai" ? `Bearer ${providerKey}` : providerKey;
205
205
  const headers = {
@@ -235,7 +235,12 @@ export async function birthThroughGateway(gatewayUrl, provider, agentName, provi
235
235
  messages: [{ role: "user", content: "hello" }],
236
236
  });
237
237
  }
238
- const response = await fetch(url, { method: "POST", headers, body });
238
+ const response = await fetch(url, {
239
+ method: "POST",
240
+ headers,
241
+ body,
242
+ signal: opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined,
243
+ });
239
244
  const agentId = response.headers.get(AGENT_HEADER);
240
245
  if (!response.ok && !agentId) {
241
246
  const text = await response.text().catch(() => "");
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Which mnm- agent the gateway serves for (Anthropic key, agent name, gateway).
3
+ *
4
+ * The gateway identity is hash(key, name), so the answer for one (key, name, gateway) is stable:
5
+ * once a launch has asked, later launches reuse it instead of paying for another probe
6
+ * completion. Entries are keyed by a one-way fingerprint and never hold the key itself (nor the
7
+ * sha256(key|name) claim proof). `--setup` always re-asks and overwrites.
8
+ */
9
+ /**
10
+ * A slow KDF (scrypt), not a fast hash: the fingerprint is derived from a credential and lands on
11
+ * disk, so recovering the key from it must cost what guessing a password would. Name and
12
+ * gateway are the salt — the lookup has to be deterministic, and they make each entry unique.
13
+ */
14
+ export declare function servedFingerprint(anthropicKey: string, slug: string, gateway: string): string;
15
+ /** The cached served id, or null (missing, unreadable, or not an mnm- id). */
16
+ export declare function readServedAgent(dir: string, fingerprint: string): string | null;
17
+ /** Best effort: a failed write only means the next launch asks again. */
18
+ export declare function writeServedAgent(dir: string, fingerprint: string, agentId: string): void;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Which mnm- agent the gateway serves for (Anthropic key, agent name, gateway).
3
+ *
4
+ * The gateway identity is hash(key, name), so the answer for one (key, name, gateway) is stable:
5
+ * once a launch has asked, later launches reuse it instead of paying for another probe
6
+ * completion. Entries are keyed by a one-way fingerprint and never hold the key itself (nor the
7
+ * sha256(key|name) claim proof). `--setup` always re-asks and overwrites.
8
+ */
9
+ import { scryptSync } from "node:crypto";
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ const FILE = "served-agents.json";
13
+ /**
14
+ * A slow KDF (scrypt), not a fast hash: the fingerprint is derived from a credential and lands on
15
+ * disk, so recovering the key from it must cost what guessing a password would. Name and
16
+ * gateway are the salt — the lookup has to be deterministic, and they make each entry unique.
17
+ */
18
+ export function servedFingerprint(anthropicKey, slug, gateway) {
19
+ return scryptSync(anthropicKey, `mnemom-served-agent-v1\n${slug}\n${gateway}`, 32).toString("hex");
20
+ }
21
+ function readAll(dir) {
22
+ try {
23
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, FILE), "utf8"));
24
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
25
+ ? parsed
26
+ : {};
27
+ }
28
+ catch {
29
+ return {};
30
+ }
31
+ }
32
+ /** The cached served id, or null (missing, unreadable, or not an mnm- id). */
33
+ export function readServedAgent(dir, fingerprint) {
34
+ const id = readAll(dir)[fingerprint];
35
+ return typeof id === "string" && /^mnm-/.test(id) ? id : null;
36
+ }
37
+ /** Best effort: a failed write only means the next launch asks again. */
38
+ export function writeServedAgent(dir, fingerprint, agentId) {
39
+ try {
40
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
41
+ const tmp = path.join(dir, `${FILE}.${process.pid}.tmp`);
42
+ fs.writeFileSync(tmp, JSON.stringify({ ...readAll(dir), [fingerprint]: agentId }, null, 2), {
43
+ mode: 0o600,
44
+ });
45
+ fs.renameSync(tmp, path.join(dir, FILE));
46
+ }
47
+ catch {
48
+ /* best effort */
49
+ }
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.4-next.5",
3
+ "version": "0.17.4-next.6",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {