@mnemom/mnemom 0.15.1-next.0 → 0.15.1
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/try-me.js +160 -40
- package/package.json +1 -1
package/dist/commands/try-me.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* host-connector authorization in the MCP flow, not a forged credential.
|
|
25
25
|
*/
|
|
26
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";
|
|
27
|
+
import { putAlignmentCard, putProtectionCard, listOrgAgents, MnemomApiError } from "../lib/api.js";
|
|
28
28
|
import { resolveAuth, loginWithBrowser, loginWithDeviceFlow } from "../lib/auth.js";
|
|
29
29
|
import { openBrowser } from "../lib/oauth.js";
|
|
30
30
|
import { askSelect, askInput, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
@@ -114,12 +114,15 @@ export async function tryMeCommand(token, options = {}) {
|
|
|
114
114
|
result.claimed = true;
|
|
115
115
|
result.steps.push({ step: "claim", status: "ok", detail: agentId });
|
|
116
116
|
say(fmt.success("Claimed — you now own this agent."));
|
|
117
|
+
// The session context the card writes share: ensureSession + writeCardWithRetry
|
|
118
|
+
// both need it (the latter re-triggers ensureSession on a persistent auth 403).
|
|
119
|
+
const sessionCtx = { nonInteractive, autoOpen, agentId, say };
|
|
117
120
|
// ── Ensure a CLI session for the card writes (the human's own login) ────────
|
|
118
|
-
await ensureSession(
|
|
121
|
+
await ensureSession(sessionCtx);
|
|
119
122
|
// ── State: alignment (set directly with the post-claim session) ─────────────
|
|
120
123
|
say();
|
|
121
124
|
say(`${fmt.badge("alignment", "cyan")} Publishing your alignment card — your signed, public statement of intent…`);
|
|
122
|
-
await writeCardWithRetry("alignment",
|
|
125
|
+
await writeCardWithRetry("alignment", manifest.declare.alignment_card, options.pollTimeout ?? 600, sessionCtx);
|
|
123
126
|
result.alignment = "set";
|
|
124
127
|
result.steps.push({ step: "alignment", status: "ok" });
|
|
125
128
|
say(fmt.success("Alignment card set."));
|
|
@@ -133,7 +136,7 @@ export async function tryMeCommand(token, options = {}) {
|
|
|
133
136
|
say,
|
|
134
137
|
});
|
|
135
138
|
say(fmt.dim("Waiting for your approval, then setting the protection card…"));
|
|
136
|
-
await writeCardWithRetry("protection",
|
|
139
|
+
await writeCardWithRetry("protection", manifest.declare.protection_card, options.pollTimeout ?? 600, sessionCtx);
|
|
137
140
|
result.protection = "set";
|
|
138
141
|
result.steps.push({ step: "protection", status: "ok" });
|
|
139
142
|
say(fmt.success("Protection card set."));
|
|
@@ -223,28 +226,99 @@ async function pollUntilClaimed(apiBase, agentId, timeoutSeconds, say) {
|
|
|
223
226
|
say(fmt.dim(` …still waiting for the claim (${waited / 1000}s).`));
|
|
224
227
|
}
|
|
225
228
|
}
|
|
226
|
-
// ── CLI session for the card writes ──────────────────────────────────────────
|
|
227
229
|
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
230
|
+
* Validate that a JWT session can actually act for the just-claimed agent —
|
|
231
|
+
* NOT merely that some credential exists on disk.
|
|
232
|
+
*
|
|
233
|
+
* The card-write PUTs authorize on ORG MEMBERSHIP (ADR-062): the caller may set
|
|
234
|
+
* the cards only if they belong to the org the agent was claimed into. Listing
|
|
235
|
+
* the caller's org fleets is the cheapest authoritative probe of exactly that —
|
|
236
|
+
* it both proves the token is live (a stale/revoked token throws "Not
|
|
237
|
+
* authenticated" / 401) AND that this session governs the agent (it appears in
|
|
238
|
+
* the fleet). A locally-unexpired token signed in as a DIFFERENT account passes
|
|
239
|
+
* the old `type !== "none"` check yet fails here (the agent isn't in its fleet),
|
|
240
|
+
* which is the silent-403 hang this guards against.
|
|
241
|
+
*
|
|
242
|
+
* A brief read lag right after the claim is absorbed with a couple of bounded
|
|
243
|
+
* retries so the happy path doesn't bounce a valid owner into a needless
|
|
244
|
+
* re-login. A non-auth error (network blip / 5xx) is inconclusive → we return
|
|
245
|
+
* `true` and let the card-write path's grace+escalation be the authority, so a
|
|
246
|
+
* transient hiccup can't strand the run.
|
|
232
247
|
*/
|
|
233
|
-
async function
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
248
|
+
async function sessionCanActForAgent(agentId) {
|
|
249
|
+
const ATTEMPTS = 3;
|
|
250
|
+
for (let i = 0; i < ATTEMPTS; i++) {
|
|
251
|
+
try {
|
|
252
|
+
const fleet = await listOrgAgents();
|
|
253
|
+
if (fleet.some((a) => a.id === agentId))
|
|
254
|
+
return true;
|
|
255
|
+
// Authenticated, but the agent isn't (yet) in any of the caller's orgs.
|
|
256
|
+
// Retry a couple of times to ride out post-claim propagation before
|
|
257
|
+
// concluding the session is for the wrong account.
|
|
258
|
+
if (i < ATTEMPTS - 1)
|
|
259
|
+
await sleep(POLL_INTERVAL_MS);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
const status = err instanceof MnemomApiError ? err.effectiveStatus : undefined;
|
|
263
|
+
const authFailure = status === 401 || (err instanceof Error && /not authenticated/i.test(err.message));
|
|
264
|
+
// A definitive auth failure ⇒ the session is unusable ⇒ force re-login.
|
|
265
|
+
if (authFailure)
|
|
266
|
+
return false;
|
|
267
|
+
// Anything else is inconclusive — don't force a re-login on it.
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Ensure the CLI holds a session that can set THIS agent's cards before writing
|
|
275
|
+
* them. The card-write PUTs authorize on org membership and need the human's
|
|
276
|
+
* JWT/API-key — the headless twin of the MCP host-connector authorization.
|
|
277
|
+
*
|
|
278
|
+
* This does NOT trust a bare existence check: a stale/expired/wrong-account
|
|
279
|
+
* session passes `type !== "none"` but can't write the cards, which previously
|
|
280
|
+
* turned into a silent multi-minute 403 retry hang. So for a JWT we VALIDATE it
|
|
281
|
+
* can act for the agent's owner and, if not, fall through to a fresh login
|
|
282
|
+
* instead of returning early.
|
|
283
|
+
*
|
|
284
|
+
* `force` (set by the card-write escalation) skips the existence/validation
|
|
285
|
+
* check and re-authenticates unconditionally — the session on disk has already
|
|
286
|
+
* proven it can't write this agent's cards.
|
|
287
|
+
*/
|
|
288
|
+
async function ensureSession(ctx, opts = {}) {
|
|
289
|
+
if (!opts.force) {
|
|
290
|
+
const cred = await resolveAuth();
|
|
291
|
+
// An API key is a long-lived credential with no expiry/identity to validate
|
|
292
|
+
// (and the org-scoped probe rejects API keys) — trust it as before.
|
|
293
|
+
if (cred.type === "api-key")
|
|
294
|
+
return;
|
|
295
|
+
if (cred.type === "jwt") {
|
|
296
|
+
if (await sessionCanActForAgent(ctx.agentId))
|
|
297
|
+
return;
|
|
298
|
+
ctx.say();
|
|
299
|
+
ctx.say(fmt.warn("Your CLI session looks stale — it's expired or signed in as a different account, so " +
|
|
300
|
+
"it can't set this agent's cards. Re-authenticating as the agent's owner…"));
|
|
301
|
+
// fall through to the fresh-login path
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (!opts.force) {
|
|
305
|
+
ctx.say();
|
|
306
|
+
ctx.say(fmt.dim("To set your cards as you, the CLI needs your Mnemom session — this is the headless twin of " +
|
|
307
|
+
"the host-connector authorization (your own login, not a forged credential)."));
|
|
308
|
+
}
|
|
240
309
|
if (ctx.nonInteractive) {
|
|
241
|
-
throw new Error("
|
|
242
|
-
`
|
|
310
|
+
throw new Error("No usable Mnemom session for this agent. Run `mnemom login` (or set MNEMOM_TOKEN / " +
|
|
311
|
+
`MNEMOM_API_KEY) as the account that owns ${ctx.agentId}, then re-run with ` +
|
|
312
|
+
`--resume ${ctx.agentId} to resume after the claim.`);
|
|
243
313
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
314
|
+
// On a forced re-auth the user is mid-flow and we've already explained why; go
|
|
315
|
+
// straight to the login rather than prompting again.
|
|
316
|
+
if (!opts.force) {
|
|
317
|
+
const ok = await askYesNo("Sign in now? (one-click — you just signed in to claim)", true);
|
|
318
|
+
if (!ok) {
|
|
319
|
+
throw new Error("A Mnemom session is required to set the cards. Run `mnemom login`, then re-run with " +
|
|
320
|
+
`--resume ${ctx.agentId} to resume.`);
|
|
321
|
+
}
|
|
248
322
|
}
|
|
249
323
|
// No local browser (e.g. SSH) → device flow; otherwise the loopback OAuth flow.
|
|
250
324
|
if (ctx.autoOpen) {
|
|
@@ -255,18 +329,52 @@ async function ensureSession(ctx) {
|
|
|
255
329
|
}
|
|
256
330
|
ctx.say(fmt.success("Signed in."));
|
|
257
331
|
}
|
|
258
|
-
// ── card writes (retry on
|
|
332
|
+
// ── card writes (retry on grant propagation; escalate on stale auth) ─────────
|
|
333
|
+
/**
|
|
334
|
+
* Short window to absorb the legit org-membership propagation that can briefly
|
|
335
|
+
* 401/403 the alignment write right after a claim, before we treat a persistent
|
|
336
|
+
* unauthorized response as a stale-session auth problem worth re-authenticating.
|
|
337
|
+
*/
|
|
338
|
+
const AUTH_PROPAGATION_GRACE_MS = 20_000;
|
|
339
|
+
/**
|
|
340
|
+
* Classify a retriable (401/403) card-write failure.
|
|
341
|
+
*
|
|
342
|
+
* - `grant-pending`: a PROTECTION 403 — the one-time CISO grant hasn't landed in
|
|
343
|
+
* the browser yet (the manifest's `insufficient_scope`). This is expected and
|
|
344
|
+
* is polled for the full window; it must NOT trigger a re-login. (Alignment is
|
|
345
|
+
* written first and has no grant, so a stale/wrong-account session is caught
|
|
346
|
+
* there before protection runs — making a protection 403 reliably the grant.)
|
|
347
|
+
* - `auth`: a 401 anywhere, or an alignment 403 — the caller isn't authorized.
|
|
348
|
+
* After a short propagation grace this means the session is stale/mismatched.
|
|
349
|
+
*/
|
|
350
|
+
function classifyAuthError(kind, status) {
|
|
351
|
+
if (status === 403 && kind === "protection")
|
|
352
|
+
return "grant-pending";
|
|
353
|
+
return "auth";
|
|
354
|
+
}
|
|
259
355
|
/**
|
|
260
|
-
* Write a card,
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
356
|
+
* Write a card, tolerating the two legitimate transient unauthorized states and
|
|
357
|
+
* escalating a persistent one instead of hanging:
|
|
358
|
+
*
|
|
359
|
+
* - PROTECTION grant landing (403): poll the full window for the human to
|
|
360
|
+
* approve the one-time grant in the browser.
|
|
361
|
+
* - post-claim org-membership propagation (alignment 401/403): a SHORT grace.
|
|
362
|
+
*
|
|
363
|
+
* Beyond the grace a persistent 401/403 is a stale/wrong-account session, NOT a
|
|
364
|
+
* propagation delay — so we re-authenticate as the agent's owner (forced
|
|
365
|
+
* `ensureSession`) and retry the write ONCE. If it still 401/403s after that the
|
|
366
|
+
* account genuinely can't act for this agent → a clear error, never a 10-minute
|
|
367
|
+
* silent retry. Any non-401/403 status is a real error surfaced immediately.
|
|
265
368
|
*/
|
|
266
|
-
async function writeCardWithRetry(kind,
|
|
369
|
+
async function writeCardWithRetry(kind, card, timeoutSeconds, ctx) {
|
|
370
|
+
const { agentId, say } = ctx;
|
|
267
371
|
const body = JSON.stringify(card);
|
|
268
372
|
const put = kind === "alignment" ? putAlignmentCard : putProtectionCard;
|
|
269
373
|
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
374
|
+
// The auth-escalation grace is capped by the overall timeout so a 0-timeout
|
|
375
|
+
// (tests / explicit --poll-timeout 0) escalates immediately rather than never.
|
|
376
|
+
const authGraceDeadline = Date.now() + Math.min(AUTH_PROPAGATION_GRACE_MS, timeoutSeconds * 1000);
|
|
377
|
+
let reauthed = false;
|
|
270
378
|
let waited = 0;
|
|
271
379
|
for (;;) {
|
|
272
380
|
try {
|
|
@@ -275,23 +383,35 @@ async function writeCardWithRetry(kind, agentId, card, timeoutSeconds, say) {
|
|
|
275
383
|
}
|
|
276
384
|
catch (err) {
|
|
277
385
|
const status = err instanceof MnemomApiError ? err.effectiveStatus : undefined;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
386
|
+
if (status !== 401 && status !== 403)
|
|
387
|
+
throw err; // a real error — surface it
|
|
388
|
+
const kindOfFailure = classifyAuthError(kind, status);
|
|
389
|
+
// A stale/mismatched session: once the short propagation grace is spent,
|
|
390
|
+
// re-authenticate as the agent's owner and retry the write exactly once.
|
|
391
|
+
if (kindOfFailure === "auth" && Date.now() >= authGraceDeadline) {
|
|
392
|
+
if (!reauthed) {
|
|
393
|
+
reauthed = true;
|
|
394
|
+
await ensureSession(ctx, { force: true });
|
|
395
|
+
continue; // retry immediately with the fresh session
|
|
287
396
|
}
|
|
288
|
-
throw
|
|
397
|
+
throw new Error(`Couldn't set the ${kind} card — this account isn't authorized for ${agentId} ` +
|
|
398
|
+
`(last status ${status}) even after re-authenticating. Sign in as the account that ` +
|
|
399
|
+
`owns ${agentId}, then re-run with --resume ${agentId}.`, { cause: err });
|
|
400
|
+
}
|
|
401
|
+
// Overall timeout (covers the long human wait for the protection grant).
|
|
402
|
+
if (Date.now() >= deadline) {
|
|
403
|
+
throw new Error(`Timed out waiting to set the ${kind} card (last status ${status}). ` +
|
|
404
|
+
(kindOfFailure === "grant-pending"
|
|
405
|
+
? "Make sure you approved the one-time grant, then re-run with --resume " +
|
|
406
|
+
agentId +
|
|
407
|
+
"."
|
|
408
|
+
: "Re-run with --resume " + agentId + " to resume."), { cause: err });
|
|
289
409
|
}
|
|
290
410
|
await sleep(POLL_INTERVAL_MS);
|
|
291
411
|
waited += POLL_INTERVAL_MS;
|
|
292
412
|
if (waited % 15000 === 0) {
|
|
293
413
|
say(fmt.dim(` …waiting to set the ${kind} card (${waited / 1000}s)` +
|
|
294
|
-
(
|
|
414
|
+
(kindOfFailure === "grant-pending"
|
|
295
415
|
? " — approve the grant in your browser if you haven't."
|
|
296
416
|
: ".")));
|
|
297
417
|
}
|