@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.
- package/dist/commands/agent-setup.d.ts +9 -0
- package/dist/commands/agent-setup.js +56 -15
- package/dist/commands/agent.d.ts +5 -0
- package/dist/commands/agent.js +18 -5
- package/dist/commands/wrap.d.ts +3 -1
- package/dist/commands/wrap.js +7 -2
- package/dist/lib/served-agent-cache.d.ts +18 -0
- package/dist/lib/served-agent-cache.js +50 -0
- package/package.json +1 -1
|
@@ -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
|
/**
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -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;
|
package/dist/commands/agent.js
CHANGED
|
@@ -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
|
-
|
|
528
|
-
agentId =
|
|
535
|
+
if (ctx.agentId !== undefined) {
|
|
536
|
+
agentId = ctx.agentId;
|
|
529
537
|
}
|
|
530
|
-
|
|
531
|
-
|
|
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,
|
package/dist/commands/wrap.d.ts
CHANGED
|
@@ -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
|
|
27
|
+
export declare function birthThroughGateway(gatewayUrl: string, provider: Provider, agentName: string, providerKey: string, opts?: {
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}): Promise<string>;
|
|
28
30
|
export {};
|
package/dist/commands/wrap.js
CHANGED
|
@@ -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, {
|
|
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
|
+
}
|