@mnemom/mnemom 0.14.6 → 0.15.1-next.0

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.
@@ -0,0 +1,181 @@
1
+ /**
2
+ * `mnemom try-me <token>` — the deterministic Dojo-onboarding skill-runner
3
+ * (MNE-934, epic MNE-931).
4
+ *
5
+ * On a fresh machine with no MCP connector configured, the Dojo `/try-me`
6
+ * onboarding works badly because every agent improvises the manifest's flow
7
+ * differently. This module is the read-only client half of the fix: it resolves
8
+ * the shipped v5.0 briefing manifest and exposes the typed pieces + IO helpers
9
+ * the `try-me` command executes deterministically (resolve → birth → claim →
10
+ * alignment → protection → hand off).
11
+ *
12
+ * It is strictly ADDITIVE and consumes Alex's shipped endpoints READ-ONLY:
13
+ * - GET /v1/dojo/try-me/resolve?token=… (zero-auth; the token IS the credential)
14
+ * - POST <gateway.endpoint>/v1/messages (birth — the agent's first model call)
15
+ * - GET /v1/agents/:id (public claim poll)
16
+ * The card-write PUTs reuse the canonical client (lib/api.ts). NOTHING in the
17
+ * dojo backend or the website is touched.
18
+ *
19
+ * The manifest contract mirrored here is mnemom-api `src/dojo/try-me.ts`
20
+ * (MANIFEST_VERSION "5.0", MNE-879 B1). We type only the fields the runner
21
+ * consumes and tolerate everything else (`[key: string]: unknown`) so a manifest
22
+ * bump that adds fields never breaks the CLI.
23
+ */
24
+ /** A canonical card object (alignment/protection) — opaque to the runner; passed through verbatim. */
25
+ export type CanonicalCard = Record<string, unknown>;
26
+ /** The MODEL credential block — births the agent + carries gateway calls (BYOK). */
27
+ export interface ManifestGateway {
28
+ /** Provider gateway route, e.g. https://gateway.mnemom.ai/anthropic */
29
+ endpoint: string;
30
+ provider: string;
31
+ /** Concrete model id to send at birth (bare aliases 404 upstream). */
32
+ model: string;
33
+ /** Provider-native header the credential rides on (x-api-key / Authorization / x-goog-api-key). */
34
+ key_header: string;
35
+ /** Per-session birth token (mnbt_…) — token mode (preferred). */
36
+ birth_token?: string;
37
+ /** Legacy shared provider key — present only when birth_token is absent. */
38
+ provider_key?: string;
39
+ /** Header carrying the agent's birth NAME. */
40
+ agent_header: string;
41
+ [key: string]: unknown;
42
+ }
43
+ /** Human-handoff surfaces (claim/grant/dojo URLs + the name question). */
44
+ export interface ManifestHandoff {
45
+ name_question: string;
46
+ name_options: string[];
47
+ signup_url: string;
48
+ /** /claim template — {agent_id} + {hash_proof}, with &dojo_config (also binds server-side). */
49
+ claim_url_template: string;
50
+ /** First-party one-time protection-grant page — {agent_id} substituted. */
51
+ grant_url_template: string;
52
+ /** Recipe describing what hash_proof is (token mode: the raw mnbt_; legacy: SHA256). */
53
+ claim_hash_recipe: string;
54
+ dojo_url: string;
55
+ [key: string]: unknown;
56
+ }
57
+ /** The GOOD cards the agent declares. */
58
+ export interface ManifestDeclare {
59
+ alignment_card: CanonicalCard;
60
+ protection_card: CanonicalCard;
61
+ protection_mode: "nudge" | string;
62
+ [key: string]: unknown;
63
+ }
64
+ export interface BriefingStep {
65
+ n: number;
66
+ title: string;
67
+ body: string;
68
+ human_handoff?: boolean;
69
+ [key: string]: unknown;
70
+ }
71
+ export interface BriefingManifest {
72
+ directive?: string;
73
+ version: string;
74
+ token: string;
75
+ reassurance?: string;
76
+ mission?: {
77
+ scenario_id?: string;
78
+ name?: string;
79
+ role_system_prompt?: string;
80
+ failure_conditions?: Record<string, unknown>;
81
+ [key: string]: unknown;
82
+ };
83
+ model?: {
84
+ provider: string;
85
+ name: string;
86
+ };
87
+ gateway: ManifestGateway;
88
+ declare: ManifestDeclare;
89
+ bind?: {
90
+ endpoint?: string;
91
+ config_id?: string;
92
+ [key: string]: unknown;
93
+ };
94
+ handoff: ManifestHandoff;
95
+ steps?: BriefingStep[];
96
+ state_machine?: {
97
+ initial?: string;
98
+ states?: Array<Record<string, unknown>>;
99
+ };
100
+ instructions_markdown?: string;
101
+ [key: string]: unknown;
102
+ }
103
+ /** True iff `token` is shaped like a try-me token (tryme_… / a bare slug). */
104
+ export declare function looksLikeTryMeToken(token: string): boolean;
105
+ /**
106
+ * Validate a URL the manifest handed us before we fetch/open it. The manifest is
107
+ * server-built so its URLs are trusted, but we guard the protocol so a malformed
108
+ * manifest can never coerce a non-http(s) open (e.g. file://) on the host.
109
+ */
110
+ export declare function assertHttpUrl(url: string, what: string): string;
111
+ /**
112
+ * Resolve a try-me token to its briefing manifest. Zero-auth — the token is the
113
+ * credential. Idempotent within the server's reuse window (a re-resolve replays
114
+ * the SAME manifest + birth credential), so a retry is safe. Maps the documented
115
+ * status codes to actionable messages:
116
+ * 400 token missing/invalid · 404 never minted · 410 used/expired ·
117
+ * 503 demo not configured yet (retryable) · 5xx server error.
118
+ */
119
+ export declare function resolveBriefing(token: string, opts?: {
120
+ apiBase?: string;
121
+ }): Promise<BriefingManifest>;
122
+ /** Is this a token-mode manifest (per-session mnbt_ birth token) vs legacy provider key? */
123
+ export declare function isTokenMode(manifest: BriefingManifest): boolean;
124
+ /**
125
+ * The single credential the agent presents on `gateway.key_header` to be born:
126
+ * the per-session birth token (token mode) or the shared provider key (legacy).
127
+ */
128
+ export declare function birthCredential(manifest: BriefingManifest): string;
129
+ /**
130
+ * The claim `hash_proof`. In token mode it IS the birth token, presented RAW (the
131
+ * server double-hashes); we cannot derive the legacy SHA256("<key>|<name>") proof
132
+ * here because the runner never holds the provider key in token mode. For legacy
133
+ * manifests the caller must supply the provider key + name separately.
134
+ */
135
+ export declare function claimProof(manifest: BriefingManifest): string;
136
+ /** Build the canonical /claim URL: substitute agent_id + hash_proof; leave dojo_config as given. */
137
+ export declare function buildClaimUrl(manifest: BriefingManifest, agentId: string, proof: string): string;
138
+ /** Build the first-party protection-grant URL: substitute agent_id. */
139
+ export declare function buildGrantUrl(manifest: BriefingManifest, agentId: string): string;
140
+ /**
141
+ * The Dojo deep link with the agent pre-selected, so the human only has to press
142
+ * Begin Sim: `<dojo_url>?agent=<agent_id>` (mirrors the manifest's step 6).
143
+ */
144
+ export declare function buildDojoDeepLink(manifest: BriefingManifest, agentId: string): string;
145
+ /** The provider-native birth path under the gateway endpoint, per provider. */
146
+ export declare function birthPath(provider: string): string;
147
+ export interface BirthResult {
148
+ /** The minted agent id (mnm-…) captured from the x-mnemom-agent response header. */
149
+ agentId: string;
150
+ /** The gateway HTTP status (200 on a successful birth). */
151
+ status: number;
152
+ }
153
+ /**
154
+ * Be born: make the first model call through the Mnemom gateway with the birth
155
+ * credential, the human-chosen name, and the CONCRETE model id, then capture the
156
+ * minted `agent_id` from the `x-mnemom-agent` RESPONSE header.
157
+ *
158
+ * Only Anthropic births are supported here (the Dojo runs Haiku); other
159
+ * providers throw a clear error rather than send a malformed body. `fetchImpl`
160
+ * is injectable for tests.
161
+ */
162
+ export declare function birthAgent(manifest: BriefingManifest, name: string, opts?: {
163
+ fetchImpl?: typeof fetch;
164
+ }): Promise<BirthResult>;
165
+ /** The slice of the public agent projection the claim poll reads. */
166
+ export interface PublicAgentStatus {
167
+ id?: string;
168
+ name?: string | null;
169
+ claimed?: boolean;
170
+ [key: string]: unknown;
171
+ }
172
+ /**
173
+ * Read the PUBLIC (no-auth) agent projection used to detect the human's claim.
174
+ * Never throws — a transient 404/5xx (the agent not yet queryable, or a blip)
175
+ * resolves to null so the caller's poll loop simply keeps waiting.
176
+ */
177
+ export declare function fetchAgentClaimed(apiBase: string, agentId: string, opts?: {
178
+ fetchImpl?: typeof fetch;
179
+ }): Promise<PublicAgentStatus | null>;
180
+ /** Small awaitable sleep used by the runner's poll loops. */
181
+ export declare function sleep(ms: number): Promise<void>;
@@ -0,0 +1,245 @@
1
+ /**
2
+ * `mnemom try-me <token>` — the deterministic Dojo-onboarding skill-runner
3
+ * (MNE-934, epic MNE-931).
4
+ *
5
+ * On a fresh machine with no MCP connector configured, the Dojo `/try-me`
6
+ * onboarding works badly because every agent improvises the manifest's flow
7
+ * differently. This module is the read-only client half of the fix: it resolves
8
+ * the shipped v5.0 briefing manifest and exposes the typed pieces + IO helpers
9
+ * the `try-me` command executes deterministically (resolve → birth → claim →
10
+ * alignment → protection → hand off).
11
+ *
12
+ * It is strictly ADDITIVE and consumes Alex's shipped endpoints READ-ONLY:
13
+ * - GET /v1/dojo/try-me/resolve?token=… (zero-auth; the token IS the credential)
14
+ * - POST <gateway.endpoint>/v1/messages (birth — the agent's first model call)
15
+ * - GET /v1/agents/:id (public claim poll)
16
+ * The card-write PUTs reuse the canonical client (lib/api.ts). NOTHING in the
17
+ * dojo backend or the website is touched.
18
+ *
19
+ * The manifest contract mirrored here is mnemom-api `src/dojo/try-me.ts`
20
+ * (MANIFEST_VERSION "5.0", MNE-879 B1). We type only the fields the runner
21
+ * consumes and tolerate everything else (`[key: string]: unknown`) so a manifest
22
+ * bump that adds fields never breaks the CLI.
23
+ */
24
+ import { MnemomApiError, readApiError } from "./api.js";
25
+ import { getApiUrl } from "./config.js";
26
+ // ── resolve (zero-auth; the token IS the credential) ─────────────────────────
27
+ const TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
28
+ /** True iff `token` is shaped like a try-me token (tryme_… / a bare slug). */
29
+ export function looksLikeTryMeToken(token) {
30
+ return TOKEN_RE.test(token.trim());
31
+ }
32
+ /**
33
+ * Validate a URL the manifest handed us before we fetch/open it. The manifest is
34
+ * server-built so its URLs are trusted, but we guard the protocol so a malformed
35
+ * manifest can never coerce a non-http(s) open (e.g. file://) on the host.
36
+ */
37
+ export function assertHttpUrl(url, what) {
38
+ let parsed;
39
+ try {
40
+ parsed = new URL(url);
41
+ }
42
+ catch {
43
+ throw new Error(`${what} is not a valid URL: ${url}`);
44
+ }
45
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
46
+ throw new Error(`${what} must be http(s): ${url}`);
47
+ }
48
+ return parsed.href;
49
+ }
50
+ /** Shape gate: a resolved manifest must carry the fields the runner depends on. */
51
+ function isUsableManifest(m) {
52
+ if (!m || typeof m !== "object")
53
+ return false;
54
+ const x = m;
55
+ return (typeof x.token === "string" &&
56
+ !!x.gateway &&
57
+ typeof x.gateway.endpoint === "string" &&
58
+ typeof x.gateway.model === "string" &&
59
+ typeof x.gateway.key_header === "string" &&
60
+ !!x.declare &&
61
+ !!x.handoff &&
62
+ typeof x.handoff.claim_url_template === "string");
63
+ }
64
+ /**
65
+ * Resolve a try-me token to its briefing manifest. Zero-auth — the token is the
66
+ * credential. Idempotent within the server's reuse window (a re-resolve replays
67
+ * the SAME manifest + birth credential), so a retry is safe. Maps the documented
68
+ * status codes to actionable messages:
69
+ * 400 token missing/invalid · 404 never minted · 410 used/expired ·
70
+ * 503 demo not configured yet (retryable) · 5xx server error.
71
+ */
72
+ export async function resolveBriefing(token, opts = {}) {
73
+ const apiBase = (opts.apiBase ?? getApiUrl()).replace(/\/$/, "");
74
+ const url = `${apiBase}/v1/dojo/try-me/resolve?token=${encodeURIComponent(token)}`;
75
+ let response;
76
+ try {
77
+ response = await fetch(url, { headers: { Accept: "application/json" } });
78
+ }
79
+ catch (err) {
80
+ throw new Error(`Couldn't reach the Mnemom API at ${apiBase}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
81
+ }
82
+ if (!response.ok) {
83
+ // The resolve endpoint emits the nested {error:{code,message}} envelope, so
84
+ // readApiError surfaces the server's own message. Layer on actionable hints
85
+ // for the burn/expiry/setup cases.
86
+ const apiErr = await readApiError(response, "Failed to resolve the try-me briefing");
87
+ if (response.status === 410) {
88
+ throw new MnemomApiError(410, `${apiErr.message} A try-me token is single-use — mint a fresh one and try again.`, { code: apiErr.code });
89
+ }
90
+ if (response.status === 404) {
91
+ throw new MnemomApiError(404, `${apiErr.message} (no briefing exists for that token)`, {
92
+ code: apiErr.code,
93
+ });
94
+ }
95
+ if (response.status === 503) {
96
+ throw new MnemomApiError(503, `${apiErr.message} (the Dojo demo isn't fully configured yet — retryable)`, { code: apiErr.code });
97
+ }
98
+ throw apiErr;
99
+ }
100
+ const manifest = (await response.json().catch(() => null));
101
+ if (!isUsableManifest(manifest)) {
102
+ throw new Error("The resolved briefing is missing fields this CLI needs (gateway/declare/handoff). " +
103
+ "The manifest version may be newer than this CLI — run `npx @mnemom/mnemom@latest`.");
104
+ }
105
+ return manifest;
106
+ }
107
+ // ── credential + proof + URL builders (pure) ─────────────────────────────────
108
+ /** Is this a token-mode manifest (per-session mnbt_ birth token) vs legacy provider key? */
109
+ export function isTokenMode(manifest) {
110
+ return (typeof manifest.gateway.birth_token === "string" && manifest.gateway.birth_token.length > 0);
111
+ }
112
+ /**
113
+ * The single credential the agent presents on `gateway.key_header` to be born:
114
+ * the per-session birth token (token mode) or the shared provider key (legacy).
115
+ */
116
+ export function birthCredential(manifest) {
117
+ const cred = manifest.gateway.birth_token ?? manifest.gateway.provider_key;
118
+ if (!cred) {
119
+ throw new Error("Briefing carries no birth credential (neither birth_token nor provider_key).");
120
+ }
121
+ return cred;
122
+ }
123
+ /**
124
+ * The claim `hash_proof`. In token mode it IS the birth token, presented RAW (the
125
+ * server double-hashes); we cannot derive the legacy SHA256("<key>|<name>") proof
126
+ * here because the runner never holds the provider key in token mode. For legacy
127
+ * manifests the caller must supply the provider key + name separately.
128
+ */
129
+ export function claimProof(manifest) {
130
+ if (isTokenMode(manifest))
131
+ return manifest.gateway.birth_token;
132
+ throw new Error('Legacy provider-key manifests need SHA256("<provider_key>|<name>") as the claim proof; ' +
133
+ "token-mode (mnbt_) briefings are expected here.");
134
+ }
135
+ /** Build the canonical /claim URL: substitute agent_id + hash_proof; leave dojo_config as given. */
136
+ export function buildClaimUrl(manifest, agentId, proof) {
137
+ const url = manifest.handoff.claim_url_template
138
+ .replace("{agent_id}", encodeURIComponent(agentId))
139
+ .replace("{hash_proof}", encodeURIComponent(proof));
140
+ return assertHttpUrl(url, "claim_url_template");
141
+ }
142
+ /** Build the first-party protection-grant URL: substitute agent_id. */
143
+ export function buildGrantUrl(manifest, agentId) {
144
+ const url = manifest.handoff.grant_url_template.replace("{agent_id}", encodeURIComponent(agentId));
145
+ return assertHttpUrl(url, "grant_url_template");
146
+ }
147
+ /**
148
+ * The Dojo deep link with the agent pre-selected, so the human only has to press
149
+ * Begin Sim: `<dojo_url>?agent=<agent_id>` (mirrors the manifest's step 6).
150
+ */
151
+ export function buildDojoDeepLink(manifest, agentId) {
152
+ const base = assertHttpUrl(manifest.handoff.dojo_url, "dojo_url");
153
+ const u = new URL(base);
154
+ u.searchParams.set("agent", agentId);
155
+ return u.href;
156
+ }
157
+ /** The provider-native birth path under the gateway endpoint, per provider. */
158
+ export function birthPath(provider) {
159
+ switch (provider) {
160
+ case "anthropic":
161
+ return "/v1/messages";
162
+ case "openai":
163
+ return "/v1/chat/completions";
164
+ case "google":
165
+ // Gemini's generateContent path needs the model in it; surfaced to the
166
+ // caller, which only supports Anthropic births today (the Dojo default).
167
+ return "/v1beta/models";
168
+ default:
169
+ return "/v1/messages";
170
+ }
171
+ }
172
+ /**
173
+ * Be born: make the first model call through the Mnemom gateway with the birth
174
+ * credential, the human-chosen name, and the CONCRETE model id, then capture the
175
+ * minted `agent_id` from the `x-mnemom-agent` RESPONSE header.
176
+ *
177
+ * Only Anthropic births are supported here (the Dojo runs Haiku); other
178
+ * providers throw a clear error rather than send a malformed body. `fetchImpl`
179
+ * is injectable for tests.
180
+ */
181
+ export async function birthAgent(manifest, name, opts = {}) {
182
+ const doFetch = opts.fetchImpl ?? fetch;
183
+ const provider = manifest.gateway.provider;
184
+ if (provider !== "anthropic") {
185
+ throw new Error(`This CLI can birth Anthropic agents only (the Dojo default); manifest provider is "${provider}". ` +
186
+ "Birth it manually per the manifest's step 2, then re-run with --agent <id>.");
187
+ }
188
+ const endpoint = assertHttpUrl(manifest.gateway.endpoint, "gateway.endpoint");
189
+ const url = endpoint.replace(/\/$/, "") + birthPath(provider);
190
+ const cred = birthCredential(manifest);
191
+ const headers = {
192
+ "Content-Type": "application/json",
193
+ Accept: "application/json",
194
+ [manifest.gateway.key_header]: cred,
195
+ [manifest.gateway.agent_header]: name,
196
+ // Anthropic Messages API requires a version header; the gateway forwards it.
197
+ "anthropic-version": "2023-06-01",
198
+ };
199
+ const body = JSON.stringify({
200
+ model: manifest.gateway.model,
201
+ max_tokens: 32,
202
+ messages: [
203
+ {
204
+ role: "user",
205
+ content: "I am being born into the Mnemom Dojo. Reply with a one-word greeting.",
206
+ },
207
+ ],
208
+ });
209
+ const response = await doFetch(url, { method: "POST", headers, body });
210
+ // Capture the agent id BEFORE inspecting ok/body — the gateway stamps it on the
211
+ // response header even for a non-2xx model reply, and a born agent is the point.
212
+ const agentId = response.headers.get(manifest.gateway.agent_header.toLowerCase());
213
+ if (!response.ok && !agentId) {
214
+ const text = await response.text().catch(() => "");
215
+ throw new MnemomApiError(response.status, `Birth call failed (${response.status}) and no agent id was minted: ${text.slice(0, 300)}`);
216
+ }
217
+ if (!agentId || !/^mnm-/.test(agentId)) {
218
+ throw new Error(`Birth succeeded (${response.status}) but the gateway returned no mnm- agent id on the ` +
219
+ `${manifest.gateway.agent_header} response header. Cannot continue.`);
220
+ }
221
+ return { agentId, status: response.status };
222
+ }
223
+ /**
224
+ * Read the PUBLIC (no-auth) agent projection used to detect the human's claim.
225
+ * Never throws — a transient 404/5xx (the agent not yet queryable, or a blip)
226
+ * resolves to null so the caller's poll loop simply keeps waiting.
227
+ */
228
+ export async function fetchAgentClaimed(apiBase, agentId, opts = {}) {
229
+ const doFetch = opts.fetchImpl ?? fetch;
230
+ const base = apiBase.replace(/\/$/, "");
231
+ const url = `${base}/v1/agents/${encodeURIComponent(agentId)}`;
232
+ try {
233
+ const response = await doFetch(url, { headers: { Accept: "application/json" } });
234
+ if (!response.ok)
235
+ return null;
236
+ return (await response.json());
237
+ }
238
+ catch {
239
+ return null;
240
+ }
241
+ }
242
+ /** Small awaitable sleep used by the runner's poll loops. */
243
+ export function sleep(ms) {
244
+ return new Promise((resolve) => setTimeout(resolve, ms));
245
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.14.6",
3
+ "version": "0.15.1-next.0",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,10 +8,11 @@
8
8
  "smoltbot": "./dist/smoltbot-shim.js"
9
9
  },
10
10
  "scripts": {
11
- "build": "npm install --prefix ../shared/policy-engine && npm run build --prefix ../shared/policy-engine && tsc",
11
+ "build": "npm install && npm install --prefix ../shared/policy-engine && npm run build --prefix ../shared/policy-engine && tsc",
12
12
  "dev": "tsx src/index.ts",
13
13
  "gen:command-tree": "tsx scripts/gen-command-tree.mjs",
14
14
  "check:command-tree": "tsx scripts/gen-command-tree.mjs --check",
15
+ "version": "npm run gen:command-tree && git add command-tree.json",
15
16
  "test": "vitest"
16
17
  },
17
18
  "dependencies": {
@@ -1 +0,0 @@
1
- export declare function integrityCommand(agentName?: string): Promise<void>;