@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.
- package/dist/commands/activity.d.ts +2 -0
- package/dist/commands/{integrity.js → activity.js} +4 -1
- package/dist/commands/agents.js +18 -1
- package/dist/commands/api-key.js +20 -4
- package/dist/commands/auth.js +39 -32
- package/dist/commands/card.js +17 -2
- package/dist/commands/org.js +15 -2
- package/dist/commands/protection.d.ts +2 -1
- package/dist/commands/protection.js +163 -3
- package/dist/commands/status.js +4 -1
- package/dist/commands/try-me.d.ts +52 -0
- package/dist/commands/try-me.js +374 -0
- package/dist/index.js +63 -3
- package/dist/lib/api.js +5 -6
- package/dist/lib/auth.d.ts +33 -10
- package/dist/lib/auth.js +71 -201
- package/dist/lib/oauth.d.ts +132 -0
- package/dist/lib/oauth.js +490 -0
- package/dist/lib/prompt.js +19 -7
- package/dist/lib/try-me.d.ts +181 -0
- package/dist/lib/try-me.js +245 -0
- package/package.json +3 -2
- package/dist/commands/integrity.d.ts +0 -1
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom try-me <token>` — the universal, deterministic Dojo-onboarding runner
|
|
3
|
+
* (MNE-934, epic MNE-931).
|
|
4
|
+
*
|
|
5
|
+
* Executes the shipped v5.0 briefing manifest's flow so an arbitrary agent/human
|
|
6
|
+
* on a fresh machine doesn't have to improvise it: resolve → name → be born →
|
|
7
|
+
* hand off (human claims in the browser) → set alignment → open a one-time grant
|
|
8
|
+
* (human approves in the browser) → set protection → hand off to the Dojo.
|
|
9
|
+
*
|
|
10
|
+
* DEMO-SAFE BY CONSTRUCTION:
|
|
11
|
+
* - Additive: a NEW verb; it modifies no existing command and touches no
|
|
12
|
+
* backend/website. It consumes Alex's shipped endpoints READ-ONLY (resolve,
|
|
13
|
+
* gateway birth, public claim poll) + the canonical card-write client.
|
|
14
|
+
* - The two privileged human acts stay in the human's browser: the claim
|
|
15
|
+
* sign-in (step 4) and the one-time protection grant (step 6). The command
|
|
16
|
+
* only orchestrates + polls — it never forges the human's session.
|
|
17
|
+
*
|
|
18
|
+
* AUTH MODEL (see the runner report / MNE-934): the canonical card-write PUTs
|
|
19
|
+
* (`/v1/{alignment,protection}/agent/:id`) authorize on the human's ORG
|
|
20
|
+
* MEMBERSHIP (ADR-062) and accept the human's `mnemom login` JWT or
|
|
21
|
+
* `MNEMOM_API_KEY` — NOT the birth token. So before the card writes the runner
|
|
22
|
+
* ensures a CLI session (offering `mnemom login`, one-click after the claim
|
|
23
|
+
* sign-in). That session IS the human's own — it is the headless twin of the
|
|
24
|
+
* host-connector authorization in the MCP flow, not a forged credential.
|
|
25
|
+
*/
|
|
26
|
+
import { resolveBriefing, birthAgent, fetchAgentClaimed, buildClaimUrl, buildGrantUrl, buildDojoDeepLink, claimProof, isTokenMode, looksLikeTryMeToken, sleep, } from "../lib/try-me.js";
|
|
27
|
+
import { putAlignmentCard, putProtectionCard, MnemomApiError } from "../lib/api.js";
|
|
28
|
+
import { resolveAuth, loginWithBrowser, loginWithDeviceFlow } from "../lib/auth.js";
|
|
29
|
+
import { openBrowser } from "../lib/oauth.js";
|
|
30
|
+
import { askSelect, askInput, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
31
|
+
import { getApiUrl } from "../lib/config.js";
|
|
32
|
+
import { fmt } from "../lib/format.js";
|
|
33
|
+
const POLL_INTERVAL_MS = 3000;
|
|
34
|
+
/**
|
|
35
|
+
* Entry point for `mnemom try-me <token>`. Throws on fatal misconfiguration
|
|
36
|
+
* (caught by the index.ts wrapper, which prints + exits non-zero); soft failures
|
|
37
|
+
* are surfaced as human guidance.
|
|
38
|
+
*/
|
|
39
|
+
export async function tryMeCommand(token, options = {}) {
|
|
40
|
+
const json = !!options.json;
|
|
41
|
+
// JSON output is only coherent non-interactively — no prompts can be shown.
|
|
42
|
+
const nonInteractive = !!options.yes || json || !isInteractive();
|
|
43
|
+
// The name pick is a deliberate human-handoff checkpoint (the manifest marks
|
|
44
|
+
// it human_handoff:true). It must fire on EVERY real run — agent-driven,
|
|
45
|
+
// piped, and inherited-stdin runs all report isTTY=false yet can still answer
|
|
46
|
+
// a prompt — so it is auto-skipped ONLY on an explicit non-interactive
|
|
47
|
+
// request: --yes, --json, or --name (the last handled inside pickName). It is
|
|
48
|
+
// intentionally NOT gated on isInteractive(), unlike the open/login prompts.
|
|
49
|
+
const skipNamePrompt = !!options.yes || json;
|
|
50
|
+
const autoOpen = options.open !== false; // --no-open → false
|
|
51
|
+
const result = { token, version: "", dry_run: !!options.dryRun, steps: [] };
|
|
52
|
+
// A human-facing log that is silenced in --json mode (the JSON is the output).
|
|
53
|
+
const say = (line = "") => {
|
|
54
|
+
if (!json)
|
|
55
|
+
console.log(line);
|
|
56
|
+
};
|
|
57
|
+
if (!looksLikeTryMeToken(token)) {
|
|
58
|
+
throw new Error(`'${token}' doesn't look like a try-me token (expected e.g. tryme_…). ` +
|
|
59
|
+
"Copy the token from your Dojo /try-me invite.");
|
|
60
|
+
}
|
|
61
|
+
// ── State: resolve ────────────────────────────────────────────────────────
|
|
62
|
+
say(fmt.header("Mnemom Dojo — try-me"));
|
|
63
|
+
say();
|
|
64
|
+
say(fmt.dim(`Resolving briefing for token ${token}…`));
|
|
65
|
+
const manifest = await resolveBriefing(token, { apiBase: options.api });
|
|
66
|
+
result.version = manifest.version;
|
|
67
|
+
result.steps.push({ step: "resolve", status: "ok", detail: `manifest v${manifest.version}` });
|
|
68
|
+
if (options.dryRun) {
|
|
69
|
+
if (json) {
|
|
70
|
+
console.log(JSON.stringify(buildDryRunJson(manifest), null, 2));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.log(renderPlan(manifest));
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const apiBase = (options.api ?? getApiUrl()).replace(/\/$/, "");
|
|
78
|
+
// ── State: name (human checkpoint) ──────────────────────────────────────────
|
|
79
|
+
const name = await pickName(manifest, options, skipNamePrompt, say);
|
|
80
|
+
result.steps.push({ step: "name", status: "ok", detail: name });
|
|
81
|
+
// ── State: birth ────────────────────────────────────────────────────────────
|
|
82
|
+
let agentId;
|
|
83
|
+
if (options.agent) {
|
|
84
|
+
agentId = options.agent;
|
|
85
|
+
result.steps.push({ step: "birth", status: "skipped", detail: `using --resume ${agentId}` });
|
|
86
|
+
say(fmt.dim(`Skipping birth — resuming with agent ${agentId}.`));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
say();
|
|
90
|
+
say(`${fmt.badge("birth", "cyan")} Registering your identity with Mnemom (your first model call)…`);
|
|
91
|
+
const born = await birthAgent(manifest, name);
|
|
92
|
+
agentId = born.agentId;
|
|
93
|
+
result.agent_id = agentId;
|
|
94
|
+
result.steps.push({ step: "birth", status: "ok", detail: agentId });
|
|
95
|
+
say(fmt.success(`Born as ${fmt.badge(name, "magenta")} → ${agentId}`));
|
|
96
|
+
}
|
|
97
|
+
result.agent_id = agentId;
|
|
98
|
+
// ── State: claim (human handoff, then poll) ─────────────────────────────────
|
|
99
|
+
const proof = isTokenMode(manifest) ? claimProof(manifest) : missingLegacyProof(); // token mode is the shipped reality; legacy is unsupported here
|
|
100
|
+
const claimUrl = buildClaimUrl(manifest, agentId, proof);
|
|
101
|
+
say();
|
|
102
|
+
say(`${fmt.badge("claim", "cyan")} Your turn — sign in and claim ${name} into your org.`);
|
|
103
|
+
await offerToOpen(claimUrl, "the claim page", { nonInteractive, autoOpen, say });
|
|
104
|
+
say(fmt.dim(`(Brand new? You can create your account at ${manifest.handoff.signup_url}.)`));
|
|
105
|
+
say(fmt.dim("Waiting for you to finish signing in / creating your account…"));
|
|
106
|
+
const claimed = await pollUntilClaimed(apiBase, agentId, options.pollTimeout ?? 600, say);
|
|
107
|
+
if (!claimed) {
|
|
108
|
+
result.steps.push({ step: "claim", status: "pending", detail: "not claimed before timeout" });
|
|
109
|
+
if (json)
|
|
110
|
+
console.log(JSON.stringify(result, null, 2));
|
|
111
|
+
throw new Error(`${agentId} wasn't claimed in time. Finish the claim at the link above, then re-run with ` +
|
|
112
|
+
`--resume ${agentId} to resume (your birth credential is reusable within the manifest window).`);
|
|
113
|
+
}
|
|
114
|
+
result.claimed = true;
|
|
115
|
+
result.steps.push({ step: "claim", status: "ok", detail: agentId });
|
|
116
|
+
say(fmt.success("Claimed — you now own this agent."));
|
|
117
|
+
// ── Ensure a CLI session for the card writes (the human's own login) ────────
|
|
118
|
+
await ensureSession({ nonInteractive, autoOpen, agentId, say });
|
|
119
|
+
// ── State: alignment (set directly with the post-claim session) ─────────────
|
|
120
|
+
say();
|
|
121
|
+
say(`${fmt.badge("alignment", "cyan")} Publishing your alignment card — your signed, public statement of intent…`);
|
|
122
|
+
await writeCardWithRetry("alignment", agentId, manifest.declare.alignment_card, options.pollTimeout ?? 600, say);
|
|
123
|
+
result.alignment = "set";
|
|
124
|
+
result.steps.push({ step: "alignment", status: "ok" });
|
|
125
|
+
say(fmt.success("Alignment card set."));
|
|
126
|
+
// ── State: protection (one-time grant in the browser, then write) ───────────
|
|
127
|
+
say();
|
|
128
|
+
say(`${fmt.badge("protection", "cyan")} Your protection card needs a one-time grant (the CISO moment).`);
|
|
129
|
+
const grantUrl = buildGrantUrl(manifest, agentId);
|
|
130
|
+
await offerToOpen(grantUrl, "the one-time protection-grant page", {
|
|
131
|
+
nonInteractive,
|
|
132
|
+
autoOpen,
|
|
133
|
+
say,
|
|
134
|
+
});
|
|
135
|
+
say(fmt.dim("Waiting for your approval, then setting the protection card…"));
|
|
136
|
+
await writeCardWithRetry("protection", agentId, manifest.declare.protection_card, options.pollTimeout ?? 600, say);
|
|
137
|
+
result.protection = "set";
|
|
138
|
+
result.steps.push({ step: "protection", status: "ok" });
|
|
139
|
+
say(fmt.success("Protection card set."));
|
|
140
|
+
// ── State: hand off to the Dojo ─────────────────────────────────────────────
|
|
141
|
+
const dojoUrl = buildDojoDeepLink(manifest, agentId);
|
|
142
|
+
result.dojo_url = dojoUrl;
|
|
143
|
+
say();
|
|
144
|
+
say(`${fmt.badge("dojo", "cyan")} You're verified and armed — opening the Dojo so you can press Begin Sim.`);
|
|
145
|
+
await offerToOpen(dojoUrl, "the Dojo (you're pre-selected)", { nonInteractive, autoOpen, say });
|
|
146
|
+
result.steps.push({ step: "handoff", status: "ok", detail: dojoUrl });
|
|
147
|
+
say();
|
|
148
|
+
say(renderRecount(manifest, name, agentId));
|
|
149
|
+
if (json)
|
|
150
|
+
console.log(JSON.stringify(result, null, 2));
|
|
151
|
+
}
|
|
152
|
+
// ── name (human checkpoint) ──────────────────────────────────────────────────
|
|
153
|
+
async function pickName(manifest, options, skipPrompt, say) {
|
|
154
|
+
if (options.name && options.name.trim())
|
|
155
|
+
return options.name.trim();
|
|
156
|
+
const opts = manifest.handoff.name_options ?? [];
|
|
157
|
+
const fallback = opts[0] ?? "mnemom-dojo-agent";
|
|
158
|
+
if (skipPrompt) {
|
|
159
|
+
say(fmt.dim(`Non-interactive: naming the agent "${fallback}" (override with --name).`));
|
|
160
|
+
return fallback;
|
|
161
|
+
}
|
|
162
|
+
say();
|
|
163
|
+
const TYPE_MY_OWN = "Type my own";
|
|
164
|
+
const choice = await askSelect(manifest.handoff.name_question, [...opts, TYPE_MY_OWN]);
|
|
165
|
+
if (choice && choice !== TYPE_MY_OWN)
|
|
166
|
+
return choice;
|
|
167
|
+
// "Type my own" — or no valid selection — falls through to free-form entry.
|
|
168
|
+
for (;;) {
|
|
169
|
+
const typed = (await askInput("Enter a name for your agent:")).trim();
|
|
170
|
+
if (typed)
|
|
171
|
+
return typed;
|
|
172
|
+
// A non-responsive stream (EOF on a pipe) returns "" on every read; don't
|
|
173
|
+
// spin forever — fall back to the default so an unattended run completes.
|
|
174
|
+
if (!process.stdin.isTTY) {
|
|
175
|
+
say(fmt.warn(`No name entered on a non-interactive stream — using "${fallback}".`));
|
|
176
|
+
return fallback;
|
|
177
|
+
}
|
|
178
|
+
say(fmt.warn("A name is required (it's permanent) — please enter one."));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Token mode is the shipped reality; legacy provider-key claims aren't supported by this runner. */
|
|
182
|
+
function missingLegacyProof() {
|
|
183
|
+
throw new Error("This briefing is a legacy provider-key flow; `mnemom try-me` supports token-mode (mnbt_) " +
|
|
184
|
+
"briefings. Follow the manifest's steps manually, or ask for a token-mode invite.");
|
|
185
|
+
}
|
|
186
|
+
// ── offer-to-open (enter-default = open it for me) ──────────────────────────
|
|
187
|
+
async function offerToOpen(url, what, ctx) {
|
|
188
|
+
ctx.say(fmt.label(` Link:`, url));
|
|
189
|
+
if (!ctx.autoOpen) {
|
|
190
|
+
ctx.say(fmt.dim(` Open ${what} in your browser, then come back here.`));
|
|
191
|
+
return "printed";
|
|
192
|
+
}
|
|
193
|
+
if (ctx.nonInteractive) {
|
|
194
|
+
openBrowser(url);
|
|
195
|
+
ctx.say(fmt.dim(` Opened ${what} in your browser.`));
|
|
196
|
+
return "opened";
|
|
197
|
+
}
|
|
198
|
+
// Enter-default is the FIRST choice — "Open it for me".
|
|
199
|
+
const choice = await askSelect(`Open ${what}?`, ["Open it for me", "I'll open it myself"]);
|
|
200
|
+
if (choice === "I'll open it myself") {
|
|
201
|
+
ctx.say(fmt.dim(` Okay — open the link above when you're ready.`));
|
|
202
|
+
return "printed";
|
|
203
|
+
}
|
|
204
|
+
// Default (enter / first choice / unrecognized) → open it.
|
|
205
|
+
openBrowser(url);
|
|
206
|
+
ctx.say(fmt.dim(` Opened ${what} in your browser.`));
|
|
207
|
+
return "opened";
|
|
208
|
+
}
|
|
209
|
+
// ── claim poll ───────────────────────────────────────────────────────────────
|
|
210
|
+
async function pollUntilClaimed(apiBase, agentId, timeoutSeconds, say) {
|
|
211
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
212
|
+
let waited = 0;
|
|
213
|
+
for (;;) {
|
|
214
|
+
const status = await fetchAgentClaimed(apiBase, agentId);
|
|
215
|
+
if (status?.claimed)
|
|
216
|
+
return true;
|
|
217
|
+
if (Date.now() >= deadline)
|
|
218
|
+
return false;
|
|
219
|
+
await sleep(POLL_INTERVAL_MS);
|
|
220
|
+
waited += POLL_INTERVAL_MS;
|
|
221
|
+
// A gentle heartbeat every ~30s so a multi-minute signup doesn't look hung.
|
|
222
|
+
if (waited % 30000 === 0)
|
|
223
|
+
say(fmt.dim(` …still waiting for the claim (${waited / 1000}s).`));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ── CLI session for the card writes ──────────────────────────────────────────
|
|
227
|
+
/**
|
|
228
|
+
* Ensure the CLI holds the human's session before the card writes. The card-write
|
|
229
|
+
* PUTs authorize on org membership and need the human's JWT/API-key — the
|
|
230
|
+
* headless twin of the MCP host-connector authorization. If unauthenticated, offer
|
|
231
|
+
* a one-click `mnemom login` (one-click since the human just signed in to claim).
|
|
232
|
+
*/
|
|
233
|
+
async function ensureSession(ctx) {
|
|
234
|
+
const cred = await resolveAuth();
|
|
235
|
+
if (cred.type !== "none")
|
|
236
|
+
return;
|
|
237
|
+
ctx.say();
|
|
238
|
+
ctx.say(fmt.dim("To set your cards as you, the CLI needs your Mnemom session — this is the headless twin of " +
|
|
239
|
+
"the host-connector authorization (your own login, not a forged credential)."));
|
|
240
|
+
if (ctx.nonInteractive) {
|
|
241
|
+
throw new Error("Not authenticated. Run `mnemom login` (or set MNEMOM_TOKEN / MNEMOM_API_KEY) first, then " +
|
|
242
|
+
`re-run with --resume ${ctx.agentId} to resume after the claim.`);
|
|
243
|
+
}
|
|
244
|
+
const ok = await askYesNo("Sign in now? (one-click — you just signed in to claim)", true);
|
|
245
|
+
if (!ok) {
|
|
246
|
+
throw new Error("A Mnemom session is required to set the cards. Run `mnemom login`, then re-run with " +
|
|
247
|
+
`--resume ${ctx.agentId} to resume.`);
|
|
248
|
+
}
|
|
249
|
+
// No local browser (e.g. SSH) → device flow; otherwise the loopback OAuth flow.
|
|
250
|
+
if (ctx.autoOpen) {
|
|
251
|
+
await loginWithBrowser();
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
await loginWithDeviceFlow();
|
|
255
|
+
}
|
|
256
|
+
ctx.say(fmt.success("Signed in."));
|
|
257
|
+
}
|
|
258
|
+
// ── card writes (retry on first-auth / grant propagation) ───────────────────
|
|
259
|
+
/**
|
|
260
|
+
* Write a card, retrying while the server reports the caller isn't yet authorized
|
|
261
|
+
* (401 / 403). For alignment this absorbs the brief org-membership propagation
|
|
262
|
+
* right after the claim; for protection it ALSO absorbs the one-time grant landing
|
|
263
|
+
* (the manifest's 403 insufficient_scope → keep polling; first 200 → done). Any
|
|
264
|
+
* other status is a real error and is surfaced immediately.
|
|
265
|
+
*/
|
|
266
|
+
async function writeCardWithRetry(kind, agentId, card, timeoutSeconds, say) {
|
|
267
|
+
const body = JSON.stringify(card);
|
|
268
|
+
const put = kind === "alignment" ? putAlignmentCard : putProtectionCard;
|
|
269
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
270
|
+
let waited = 0;
|
|
271
|
+
for (;;) {
|
|
272
|
+
try {
|
|
273
|
+
await put(agentId, body, "application/json");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
const status = err instanceof MnemomApiError ? err.effectiveStatus : undefined;
|
|
278
|
+
const retriable = status === 401 || status === 403;
|
|
279
|
+
if (!retriable || Date.now() >= deadline) {
|
|
280
|
+
if (retriable) {
|
|
281
|
+
throw new Error(`Timed out waiting to set the ${kind} card (last status ${status}). ` +
|
|
282
|
+
(kind === "protection"
|
|
283
|
+
? "Make sure you approved the one-time grant, then re-run with --resume " +
|
|
284
|
+
agentId +
|
|
285
|
+
"."
|
|
286
|
+
: "Re-run with --resume " + agentId + " to resume."), { cause: err });
|
|
287
|
+
}
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
290
|
+
await sleep(POLL_INTERVAL_MS);
|
|
291
|
+
waited += POLL_INTERVAL_MS;
|
|
292
|
+
if (waited % 15000 === 0) {
|
|
293
|
+
say(fmt.dim(` …waiting to set the ${kind} card (${waited / 1000}s)` +
|
|
294
|
+
(kind === "protection"
|
|
295
|
+
? " — approve the grant in your browser if you haven't."
|
|
296
|
+
: ".")));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// ── dry-run rendering ────────────────────────────────────────────────────────
|
|
302
|
+
/** Render the resolved plan as human-readable text (no side effects). */
|
|
303
|
+
export function renderPlan(manifest) {
|
|
304
|
+
const L = [];
|
|
305
|
+
const mission = manifest.mission ?? {};
|
|
306
|
+
const credKind = isTokenMode(manifest) ? "birth_token (mnbt_…)" : "provider_key (legacy)";
|
|
307
|
+
L.push(fmt.header("Mnemom Dojo — try-me (dry run)"));
|
|
308
|
+
L.push("");
|
|
309
|
+
L.push(fmt.label(" Manifest:", `v${manifest.version} · token ${manifest.token}`));
|
|
310
|
+
if (mission.name)
|
|
311
|
+
L.push(fmt.label(" Mission: ", `${mission.name} (${mission.scenario_id ?? "?"})`));
|
|
312
|
+
L.push(fmt.label(" Model: ", `${manifest.gateway.provider} / ${manifest.gateway.model} via ${manifest.gateway.endpoint}`));
|
|
313
|
+
L.push(fmt.label(" Credential:", credKind));
|
|
314
|
+
L.push("");
|
|
315
|
+
L.push(fmt.dim(" The runner will, with NO side effects in this dry run:"));
|
|
316
|
+
L.push(` 1. ${fmt.badge("name", "cyan")} ask: "${manifest.handoff.name_question}"`);
|
|
317
|
+
L.push(fmt.dim(` options: ${(manifest.handoff.name_options ?? []).join(", ")}, or type your own`));
|
|
318
|
+
L.push(` 2. ${fmt.badge("birth", "cyan")} POST ${manifest.gateway.endpoint}/v1/messages`);
|
|
319
|
+
L.push(fmt.dim(` headers: ${manifest.gateway.key_header}: <${isTokenMode(manifest) ? "birth_token" : "provider_key"}>, ` +
|
|
320
|
+
`${manifest.gateway.agent_header}: <name> · model ${manifest.gateway.model}`));
|
|
321
|
+
L.push(` 3. ${fmt.badge("claim", "cyan")} open ${manifest.handoff.claim_url_template}`);
|
|
322
|
+
L.push(fmt.dim(" then poll GET /v1/agents/<agent_id> until claimed:true"));
|
|
323
|
+
L.push(` 4. ${fmt.badge("alignment", "cyan")} PUT /v1/alignment/agent/<agent_id> (your session)`);
|
|
324
|
+
L.push(` 5. ${fmt.badge("protection", "cyan")} open ${manifest.handoff.grant_url_template}`);
|
|
325
|
+
L.push(fmt.dim(" then PUT /v1/protection/agent/<agent_id> (poll until the grant lands)"));
|
|
326
|
+
L.push(` 6. ${fmt.badge("dojo", "cyan")} open ${manifest.handoff.dojo_url}?agent=<agent_id> → Begin Sim`);
|
|
327
|
+
L.push("");
|
|
328
|
+
L.push(fmt.dim(" Cards that would be declared:"));
|
|
329
|
+
L.push(fmt.dim(` alignment: ${compactCard(manifest.declare.alignment_card)}`));
|
|
330
|
+
L.push(fmt.dim(` protection: ${compactCard(manifest.declare.protection_card)} (mode ${manifest.declare.protection_mode})`));
|
|
331
|
+
L.push("");
|
|
332
|
+
L.push(fmt.warn("Dry run — nothing was resolved-and-acted-on beyond reading the briefing."));
|
|
333
|
+
return L.join("\n");
|
|
334
|
+
}
|
|
335
|
+
/** One-line preview of a card's top-level keys (avoids dumping the whole object). */
|
|
336
|
+
function compactCard(card) {
|
|
337
|
+
const keys = Object.keys(card ?? {});
|
|
338
|
+
return keys.length ? `{ ${keys.join(", ")} }` : "{}";
|
|
339
|
+
}
|
|
340
|
+
/** Machine-readable dry-run plan. */
|
|
341
|
+
function buildDryRunJson(manifest) {
|
|
342
|
+
return {
|
|
343
|
+
dry_run: true,
|
|
344
|
+
token: manifest.token,
|
|
345
|
+
version: manifest.version,
|
|
346
|
+
mission: manifest.mission ?? null,
|
|
347
|
+
model: { provider: manifest.gateway.provider, name: manifest.gateway.model },
|
|
348
|
+
gateway: {
|
|
349
|
+
endpoint: manifest.gateway.endpoint,
|
|
350
|
+
key_header: manifest.gateway.key_header,
|
|
351
|
+
credential: isTokenMode(manifest) ? "birth_token" : "provider_key",
|
|
352
|
+
},
|
|
353
|
+
handoff: {
|
|
354
|
+
name_question: manifest.handoff.name_question,
|
|
355
|
+
name_options: manifest.handoff.name_options,
|
|
356
|
+
claim_url_template: manifest.handoff.claim_url_template,
|
|
357
|
+
grant_url_template: manifest.handoff.grant_url_template,
|
|
358
|
+
dojo_url: manifest.handoff.dojo_url,
|
|
359
|
+
},
|
|
360
|
+
declare: {
|
|
361
|
+
alignment_card_keys: Object.keys(manifest.declare.alignment_card ?? {}),
|
|
362
|
+
protection_card_keys: Object.keys(manifest.declare.protection_card ?? {}),
|
|
363
|
+
protection_mode: manifest.declare.protection_mode,
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/** The closing recount the human can paste back, personalized from the run. */
|
|
368
|
+
function renderRecount(manifest, name, agentId) {
|
|
369
|
+
const closing = manifest.closing;
|
|
370
|
+
const tmpl = closing?.recount_template ??
|
|
371
|
+
"I ran the Mnemom try-me as {agent_name}: born, claimed, alignment + protection set. Ready for Begin Sim.";
|
|
372
|
+
const filled = tmpl.replace("{agent_name}", name).replace("{org}", "your org");
|
|
373
|
+
return fmt.dim(`Done — ${agentId} is ready. Press Begin Sim in the Dojo.\n\n${filled}`);
|
|
374
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { program } from "commander";
|
|
|
3
3
|
import { isEntrypoint } from "./lib/entrypoint.js";
|
|
4
4
|
import { CLI_VERSION } from "./version.js";
|
|
5
5
|
import { statusCommand } from "./commands/status.js";
|
|
6
|
-
import { integrityCommand } from "./commands/
|
|
6
|
+
import { activityCommand, integrityCommand } from "./commands/activity.js";
|
|
7
7
|
import { logsCommand } from "./commands/logs.js";
|
|
8
8
|
import { licenseActivateCommand, licenseStatusCommand, licenseDeactivateCommand, } from "./commands/license.js";
|
|
9
9
|
import { cardShowCommand, cardPublishCommand, cardValidateCommand, cardEditCommand, cardEvaluateCommand, } from "./commands/card.js";
|
|
@@ -19,11 +19,57 @@ import { validateSafeHouseCommand } from "./commands/validate.js";
|
|
|
19
19
|
import { apiKeyListCommand, apiKeyCreateCommand, apiKeyRotateCommand, apiKeyRevokeCommand, } from "./commands/api-key.js";
|
|
20
20
|
import { webhooksListCommand, webhooksGetCommand, webhooksCreateCommand, webhooksUpdateCommand, webhooksDeleteCommand, webhooksRotateSecretCommand, webhooksTriggerCommand, webhooksListDeliveriesCommand, webhooksRedeliverCommand, webhooksReplayCommand, } from "./commands/webhooks.js";
|
|
21
21
|
import { listenCommand } from "./commands/listen.js";
|
|
22
|
+
import { tryMeCommand } from "./commands/try-me.js";
|
|
22
23
|
program
|
|
23
24
|
.name("mnemom")
|
|
24
25
|
.description("Transparent AI agent tracing")
|
|
25
26
|
.version(CLI_VERSION)
|
|
26
27
|
.option("--agent <name>", "Select agent by name (or set MNEMOM_AGENT)");
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// try-me — deterministic Dojo-onboarding skill-runner (MNE-934, epic MNE-931)
|
|
30
|
+
//
|
|
31
|
+
// `mnemom try-me <token>` executes the shipped v5.0 Dojo briefing manifest's
|
|
32
|
+
// flow so a human/agent on a fresh machine (no MCP connector) doesn't improvise
|
|
33
|
+
// it: resolve → name → be born → hand off (human claims in-browser) → set
|
|
34
|
+
// alignment → one-time protection grant (human approves in-browser) → set
|
|
35
|
+
// protection → hand off to the Dojo. Additive + read-only on the dojo backend;
|
|
36
|
+
// the two privileged acts stay in the human's browser (the runner orchestrates
|
|
37
|
+
// + polls). See commands/try-me.ts for the auth model.
|
|
38
|
+
// ============================================================================
|
|
39
|
+
program
|
|
40
|
+
.command("try-me <token>")
|
|
41
|
+
.description("Run the Mnemom Dojo onboarding for a /try-me token (born → claim → declare → spar)")
|
|
42
|
+
.option("--dry-run", "Resolve the briefing and print the plan — no birth, claim, card writes, or sim")
|
|
43
|
+
.option("--json", "Emit machine-readable step outcomes (implies non-interactive)")
|
|
44
|
+
.option("-y, --yes", "Non-interactive: auto-pick defaults and auto-open URLs")
|
|
45
|
+
.option("--name <name>", "Pre-choose the agent's name (skips the name prompt)")
|
|
46
|
+
// NB: a distinct `--resume` (not `--agent`) — the program-level `--agent`
|
|
47
|
+
// option shadows any subcommand `--agent` under commander@12 (see the
|
|
48
|
+
// `advisories` command's MNE-238 note), so a subcommand `--agent` would bind
|
|
49
|
+
// to the parent program and arrive undefined here.
|
|
50
|
+
.option("--resume <agent_id>", "Resume from an already-born agent id (skip birth)")
|
|
51
|
+
.option("--api <url>", "Override the API base used to resolve the token (default: env)")
|
|
52
|
+
.option("--no-open", "Never auto-open URLs in a browser — just print them")
|
|
53
|
+
.option("--poll-timeout <seconds>", "How long to poll the claim/grant before giving up", "600")
|
|
54
|
+
.action(async (token, opts) => {
|
|
55
|
+
try {
|
|
56
|
+
const pollTimeout = opts.pollTimeout ? parseInt(opts.pollTimeout, 10) : 600;
|
|
57
|
+
await tryMeCommand(token, {
|
|
58
|
+
dryRun: opts.dryRun,
|
|
59
|
+
json: opts.json,
|
|
60
|
+
yes: opts.yes,
|
|
61
|
+
name: opts.name,
|
|
62
|
+
agent: opts.resume,
|
|
63
|
+
api: opts.api,
|
|
64
|
+
open: opts.open,
|
|
65
|
+
pollTimeout: isNaN(pollTimeout) ? 600 : pollTimeout,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
27
73
|
program
|
|
28
74
|
.command("status")
|
|
29
75
|
.description("Show agent status and connection info")
|
|
@@ -37,11 +83,25 @@ program
|
|
|
37
83
|
process.exit(1);
|
|
38
84
|
}
|
|
39
85
|
});
|
|
86
|
+
program
|
|
87
|
+
.command("activity")
|
|
88
|
+
.description("Show AAP behavioral activity and activity score (agent-side runtime audit log; distinct from AIP checkpoints)")
|
|
89
|
+
.action(async () => {
|
|
90
|
+
try {
|
|
91
|
+
const opts = program.opts();
|
|
92
|
+
await activityCommand(opts.agent);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
40
99
|
program
|
|
41
100
|
.command("integrity")
|
|
42
|
-
.description("
|
|
101
|
+
.description("(deprecated, use 'mnemom activity') AAP behavioral activity — note: 'integrity' is reserved for AIP checkpoint surfacing in a future release")
|
|
43
102
|
.action(async () => {
|
|
44
103
|
try {
|
|
104
|
+
console.error("Deprecated: use 'mnemom activity' instead. 'mnemom integrity' will surface AIP checkpoints in a future release.");
|
|
45
105
|
const opts = program.opts();
|
|
46
106
|
await integrityCommand(opts.agent);
|
|
47
107
|
}
|
|
@@ -962,7 +1022,7 @@ postureCmd
|
|
|
962
1022
|
program
|
|
963
1023
|
.command("login")
|
|
964
1024
|
.description("Authenticate with your Mnemom account")
|
|
965
|
-
.option("--no-browser", "Use
|
|
1025
|
+
.option("--no-browser", "Use the device code flow instead of opening a browser")
|
|
966
1026
|
.action(async (options) => {
|
|
967
1027
|
try {
|
|
968
1028
|
await loginCommand({ noBrowser: options.browser === false });
|
package/dist/lib/api.js
CHANGED
|
@@ -179,14 +179,13 @@ async function authHeaders() {
|
|
|
179
179
|
* Issue an authenticated fetch, and on a 401 response transparently force a
|
|
180
180
|
* token refresh and retry once.
|
|
181
181
|
*
|
|
182
|
-
* We hit this
|
|
183
|
-
*
|
|
184
|
-
*
|
|
182
|
+
* We hit this when the locally-cached access token is stale despite the
|
|
183
|
+
* stored expiresAt claiming it's valid — server-side revocation, clock skew,
|
|
184
|
+
* or the token being invalidated out from under us. Without this retry,
|
|
185
185
|
* `whoami` cheerfully reports a "valid" token while every authenticated
|
|
186
186
|
* call gets 401 — and the user has no path forward besides
|
|
187
|
-
* `mnemom logout && mnemom login`. The retry
|
|
188
|
-
*
|
|
189
|
-
* computeExpiresAt fix landed.
|
|
187
|
+
* `mnemom logout && mnemom login`. The retry runs the OAuth refresh_token
|
|
188
|
+
* grant and heals the stale auth file transparently.
|
|
190
189
|
*
|
|
191
190
|
* `buildInit` is invoked fresh for each attempt so the retry picks up the
|
|
192
191
|
* new Authorization header from the refreshed token. We do NOT mint a new
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -3,13 +3,32 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Stores auth tokens in ~/.mnemom/auth.json (UC-9: no more config.json).
|
|
5
5
|
* License JWTs are stored alongside auth tokens.
|
|
6
|
+
*
|
|
7
|
+
* As of MNE-806, `mnemom login` authenticates against the Mnemom OAuth
|
|
8
|
+
* Authorization Server (see lib/oauth.ts) and persists SCOPED OAuth tokens
|
|
9
|
+
* (access + optional refresh, scope, token_type, and the registered client_id
|
|
10
|
+
* needed to refresh) rather than a full Supabase session. The stored shape
|
|
11
|
+
* stays backward-compatible: `email`/`userId` are optional (opaque OAuth access
|
|
12
|
+
* tokens don't carry identity) and pre-existing Supabase sessions on disk
|
|
13
|
+
* continue to resolve until they expire.
|
|
6
14
|
*/
|
|
7
15
|
export interface AuthTokens {
|
|
8
16
|
accessToken: string;
|
|
9
|
-
|
|
17
|
+
/** Absent when the AS issues no refresh token for this grant. */
|
|
18
|
+
refreshToken?: string;
|
|
10
19
|
expiresAt: number;
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
/** OAuth scope granted to this token (e.g. "mcp:read mcp:write"). */
|
|
21
|
+
scope?: string;
|
|
22
|
+
/** OAuth token_type (typically "Bearer"). */
|
|
23
|
+
tokenType?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The OAuth client_id this token was issued to. Required to refresh (the AS
|
|
26
|
+
* uses public clients, so the client_id is presented at the token endpoint).
|
|
27
|
+
*/
|
|
28
|
+
clientId?: string;
|
|
29
|
+
/** Present only for legacy Supabase sessions; opaque OAuth tokens omit these. */
|
|
30
|
+
userId?: string;
|
|
31
|
+
email?: string;
|
|
13
32
|
}
|
|
14
33
|
export interface AuthStore {
|
|
15
34
|
auth?: AuthTokens;
|
|
@@ -30,12 +49,6 @@ export type AuthCredential = {
|
|
|
30
49
|
} | {
|
|
31
50
|
type: "none";
|
|
32
51
|
};
|
|
33
|
-
/**
|
|
34
|
-
* Compute the effective expiresAt for a freshly issued access token.
|
|
35
|
-
* Prefers the JWT's own `exp` claim; falls back to `now + expires_in` if the
|
|
36
|
-
* token can't be parsed (e.g. an opaque token).
|
|
37
|
-
*/
|
|
38
|
-
export declare function computeExpiresAt(accessToken: string, expiresInSeconds: number): number;
|
|
39
52
|
/**
|
|
40
53
|
* Get a valid access token, or null if not authenticated.
|
|
41
54
|
*
|
|
@@ -81,5 +94,15 @@ export declare function requireAuth(): Promise<AuthCredential & {
|
|
|
81
94
|
* Check if the user is logged in (has any credential).
|
|
82
95
|
*/
|
|
83
96
|
export declare function isLoggedIn(): Promise<boolean>;
|
|
97
|
+
/**
|
|
98
|
+
* Interactive browser login: OAuth 2.1 authorization-code + PKCE with a
|
|
99
|
+
* loopback redirect (the `wrangler login` pattern). Persists the resulting
|
|
100
|
+
* SCOPED tokens (plus the client_id needed to refresh) to ~/.mnemom/auth.json.
|
|
101
|
+
*/
|
|
84
102
|
export declare function loginWithBrowser(): Promise<AuthTokens>;
|
|
85
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Headless login: RFC 8628 device authorization grant. Shows a user_code +
|
|
105
|
+
* verification_uri for the user to approve in any browser (possibly on another
|
|
106
|
+
* device), polls until approval, and persists the scoped tokens.
|
|
107
|
+
*/
|
|
108
|
+
export declare function loginWithDeviceFlow(): Promise<AuthTokens>;
|