create-metamynd-agent 0.14.34 → 0.14.43

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.
Files changed (2) hide show
  1. package/index.mjs +159 -65
  2. package/package.json +2 -2
package/index.mjs CHANGED
@@ -87,8 +87,17 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
87
87
  // 0.30.0: a local permit or containment refusal is only given on a bundle at most lifecycleMaxAgeMs (5 s) old, so a
88
88
  // suspension or reinstatement reaches a running agent within seconds, not within the bundle's 10-minute maxStaleness.
89
89
  // 0.31.0: a guarded tool's decision carries governanceHeaders() — the signed request and its authorization, ready for a
90
- // gateway — so a tool no longer rebuilds and re-signs it. The scaffold's own gateway calls still build it themselves.
91
- const GUARD_VERSION = '^0.31.0';
90
+ // gateway — so a tool no longer rebuilds and re-signs it. Required: the scaffold's gateway calls (bookFlightViaGateway,
91
+ // performViaGateway, and npm run resume through them) send decision.governanceHeaders() (pre-beta rerun 6, FW6-2).
92
+ // 0.32.0: resume() signs the in-process resume claim as MAGP-RESUME-CLAIM-v2, binding the approved request and context at the
93
+ // issuer (pre-beta rerun 6 F-1-NF-R); an issuer that requires it refuses the v1 claim older guards sign.
94
+ // 0.33.0: the bundled policy-core judges owner-keyed operation rules (context-value-in / -not-in, NF-RISK-SELF) like the
95
+ // gate, so a local check escalates an owner-marked op (e.g. delete-all) whatever riskLevel the agent declares.
96
+ // 0.34.0: a tool whose gateway predates the approved-context binding (AUTHORIZATION_CONTEXT_REQUIRED) no longer voids the approval;
97
+ // resume() works again once the gateway is upgraded, and a voided approval is reported AUTHORIZATION_VOIDED (pre-beta rerun 6 FW6-1).
98
+ // 0.35.0: a payment in another currency than a currency-scoped cap is refused CURRENCY_NOT_ALLOWED, not SOP_SPEND_CAP (M2).
99
+ // 0.36.0: ships TypeScript declarations (agentsafe-guard.d.mts), so a TypeScript agent gets types with no @types package (L5).
100
+ const GUARD_VERSION = '^0.36.0';
92
101
  /** The harness entry point's config load, shared by both harness templates: a fresh clone has no
93
102
  * agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
94
103
  function harnessConfigLoad() {
@@ -172,7 +181,12 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
172
181
  // 0.27.0: a claim of a person-approved hold states the digest of the context it executes (MAGP §9a.5), and a grant naming
173
182
  // another (or none) is released and refused. Required: the issuer refuses AUTHORIZATION_CONTEXT_REQUIRED to a claimer that
174
183
  // states none, so an older gateway could no longer run an approved escalation.
175
- const MCP_GUARD_VERSION = '^0.27.0';
184
+ // 0.27.1: reportOutcome() reports an authorization a request only NAMED as presented, never as the one it ran under (FW6-3).
185
+ // 0.28.0: a gateway judges owner-keyed operation rules (context-value-in / -not-in) from its bundle like the gate
186
+ // (NF-RISK-SELF). Required: a gateway on an older guard would read an operation rule's atom as unknown and let it run.
187
+ // 0.29.0: a claim the issuer refuses is logged in one line and its block carries the issuer's detail (FW6-1).
188
+ // 0.30.0: same CURRENCY_NOT_ALLOWED code in a gateway's local judgement as at the gate (M2).
189
+ const MCP_GUARD_VERSION = '^0.30.0';
176
190
  /** A DID as it may appear inside a generated string literal (the gateway's allowedAgents pin): no quote, backslash or space. */
177
191
  const SAFE_DID = /^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/;
178
192
  const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
@@ -206,7 +220,13 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
206
220
  // 0.24.0: depends on agentsafe-mcp-guard ^0.25.0 (one guard in the tree with MCP_GUARD_VERSION above). No template change.
207
221
  // 0.25.0: depends on agentsafe-mcp-guard ^0.26.0 (one guard in the tree). No template change.
208
222
  // 0.26.0: depends on agentsafe-mcp-guard ^0.27.0 (the approved-context binding). Required with MCP_GUARD_VERSION above.
209
- const GATEWAY_VERSION = '^0.26.0';
223
+ // 0.26.1: a report names an authorization as claimed only when this gateway claimed it (FW6-3); depends on agentsafe-mcp-guard ^0.27.1.
224
+ // 0.27.0: depends on agentsafe-mcp-guard ^0.28.0 (operation rules). Required with MCP_GUARD_VERSION above.
225
+ // 0.28.0: a 403 for a refused claim carries the issuer's detail (FW6-1); depends on agentsafe-mcp-guard ^0.29.0.
226
+ // 0.29.0: a request signed for another action than the route's is refused GATEWAY_ACTION_MISMATCH, not SIGNATURE_INVALID (M2);
227
+ // depends on agentsafe-mcp-guard ^0.30.0.
228
+ // 0.30.0: `agentsafe-gateway service-id` / `accept-challenge` / `serve`, SERVICE_IDENTITY_FILE and AGENTSAFE_UPSTREAM_CREDENTIAL (M4).
229
+ const GATEWAY_VERSION = '^0.30.0';
210
230
 
211
231
  /** Appended to every scaffolded gateway server: give hold settlements still running a bounded moment on shutdown. */
212
232
  const DRAIN_ON_SHUTDOWN = `
@@ -941,7 +961,9 @@ function daemonRequest(socketPath, op, params, { connectTimeoutMs = 5000 } = {})
941
961
  sock.once('error', (err) => {
942
962
  cleanup();
943
963
  if (settled) return;
944
- if (err.code === 'ENOENT' && Date.now() < deadline) { setTimeout(attempt, 20); return; }
964
+ // ECONNREFUSED too: the one-shot admin socket is called right as `start --admin` builds it,
965
+ // and a Unix socket refuses between bind() and listen(). A refused connect sent nothing.
966
+ if ((err.code === 'ENOENT' || err.code === 'ECONNREFUSED') && Date.now() < deadline) { setTimeout(attempt, 20); return; }
945
967
  settled = true;
946
968
  clearTimeout(overallTimer);
947
969
  reject(Object.assign(new Error(`agentsafe-signer daemon unreachable at ${socketPath}: ${err.message}`), { code: 'DAEMON_UNREACHABLE' }));
@@ -981,6 +1003,10 @@ async function apiPost(base, path, body, token) {
981
1003
  } catch (e) {
982
1004
  fail(`Cannot reach ${base}${path} — is the API up? (${e.message})`);
983
1005
  }
1006
+ return readApiJson(res, path);
1007
+ }
1008
+
1009
+ async function readApiJson(res, path) {
984
1010
  const text = await res.text();
985
1011
  let json = null;
986
1012
  try { json = text ? JSON.parse(text) : null; } catch { /* non-JSON */ }
@@ -991,6 +1017,70 @@ async function apiPost(base, path, body, token) {
991
1017
  return json;
992
1018
  }
993
1019
 
1020
+ // ---------- owner session ----------
1021
+ // An owner access token lives 15 minutes (pre-beta rerun 6, OBS session TTL, #926); the refresh token from the same
1022
+ // sign-in renews it (POST /auth/refresh-token, which rotates both). Every owner-authenticated call this CLI makes goes
1023
+ // through ownerFetch: on a 401 it renews the session ONCE — the refresh token first, else a fresh sign-in with the
1024
+ // credentials already given to this run — and retries the call once. The password stays in this process's memory
1025
+ // for that purpose only; it is never written anywhere (pre-beta rerun 6, CLI login expiry).
1026
+ async function ownerLogin(base, email, password) {
1027
+ const login = await apiPost(base, '/auth/login', { username: email, password }, null);
1028
+ const token = login?.data?.accessToken;
1029
+ if (!token) fail('Login succeeded but no access token was returned.');
1030
+ return { base, email, password, token, refreshToken: login?.data?.refreshToken ?? null };
1031
+ }
1032
+
1033
+ /** Renew an expired owner session in place; true when a new access token was obtained. Never fatal. */
1034
+ async function renewOwnerSession(session) {
1035
+ const post = (path, body) => fetch(`${session.base}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
1036
+ if (session.refreshToken) {
1037
+ try {
1038
+ const res = await post('/auth/refresh-token', { refreshToken: session.refreshToken });
1039
+ const json = res.ok ? await res.json().catch(() => null) : null;
1040
+ // The refresh endpoint answers at the top level ({ accessToken, refreshToken }); accept a `data` envelope too.
1041
+ const token = json?.accessToken ?? json?.data?.accessToken;
1042
+ if (token) {
1043
+ session.token = token;
1044
+ session.refreshToken = json?.refreshToken ?? json?.data?.refreshToken ?? session.refreshToken;
1045
+ return true;
1046
+ }
1047
+ } catch { /* fall through to a fresh sign-in */ }
1048
+ }
1049
+ if (typeof session.password === 'string') {
1050
+ try {
1051
+ const res = await post('/auth/login', { username: session.email, password: session.password });
1052
+ const json = res.ok ? await res.json().catch(() => null) : null;
1053
+ if (json?.data?.accessToken) {
1054
+ session.token = json.data.accessToken;
1055
+ session.refreshToken = json.data.refreshToken ?? null;
1056
+ return true;
1057
+ }
1058
+ } catch { /* reported by the caller's retry */ }
1059
+ }
1060
+ return false;
1061
+ }
1062
+
1063
+ /** fetch() with the owner's bearer token; a 401 renews the session once and retries once. */
1064
+ async function ownerFetch(session, path, init = {}) {
1065
+ const send = () => fetch(`${session.base}${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...(init.headers ?? {}), Authorization: `Bearer ${session.token}` } });
1066
+ const res = await send();
1067
+ if (res.status !== 401) return res;
1068
+ if (!(await renewOwnerSession(session))) return res;
1069
+ console.log(c.dim(' → your owner session had expired; renewed it and retried'));
1070
+ return send();
1071
+ }
1072
+
1073
+ /** apiPost for an owner-authenticated call: fatal on failure, renewed once on an expired token. */
1074
+ async function ownerPost(session, path, body) {
1075
+ let res;
1076
+ try {
1077
+ res = await ownerFetch(session, path, { method: 'POST', body: JSON.stringify(body) });
1078
+ } catch (e) {
1079
+ fail(`Cannot reach ${session.base}${path} — is the API up? (${e.message})`);
1080
+ }
1081
+ return readApiJson(res, path);
1082
+ }
1083
+
994
1084
  /** GET a public, read-only endpoint; null on ANY failure (unreachable, non-2xx, non-JSON) — never fatal. */
995
1085
  async function softGet(base, path) {
996
1086
  try {
@@ -1059,24 +1149,22 @@ async function resolveHostedRulePack(base, key, { financial, perTxnMax, maxAmoun
1059
1149
  * dashboard registration shows its own dialog before setting the flag); see the printed message
1060
1150
  * either way.
1061
1151
  */
1062
- async function registerGatewayCounterparty(base, token, did, label, purpose = 'claim', privateKeyHex = null, agents = null) {
1152
+ async function registerGatewayCounterparty(session, did, label, purpose = 'claim', privateKeyHex = null, agents = null) {
1063
1153
  try {
1064
1154
  // The gateway accepts its own registration (MAGP §8.7.6): ask for a challenge naming this owner, sign it with the gateway's
1065
1155
  // key (generated here, so this process holds it), and register with that proof. A DID is never registered on its text.
1066
1156
  let proof;
1067
1157
  if (privateKeyHex) {
1068
- const ch = await fetch(`${base}/policy/counterparties/challenge`, {
1158
+ const ch = await ownerFetch(session, '/policy/counterparties/challenge', {
1069
1159
  method: 'POST',
1070
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
1071
1160
  body: JSON.stringify({ did, purpose }),
1072
1161
  });
1073
1162
  const chJson = await ch.json().catch(() => null);
1074
1163
  if (!ch.ok || !chJson?.data?.message) return { ok: false, message: chJson?.message ?? `challenge HTTP ${ch.status}` };
1075
1164
  proof = { challengeToken: chJson.data.challengeToken, signature: signChallengeHex(privateKeyHex, chJson.data.message) };
1076
1165
  }
1077
- const res = await fetch(`${base}/policy/counterparties`, {
1166
+ const res = await ownerFetch(session, '/policy/counterparties', {
1078
1167
  method: 'POST',
1079
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
1080
1168
  // `report` (a non-financial gateway, which never claims): it may only report to this owner, and it changes nothing about
1081
1169
  // who may claim their holds, so it needs no enforcement confirmation (MAGP §8.7.6, §16.4).
1082
1170
  // `agents` (report only): the agents this gateway serves — the issuer lets it claim an approved value-less hold of THOSE
@@ -1452,31 +1540,24 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
1452
1540
  // --- value-bearing action (sealValueActions, on by default) — its authorizationId is what lets
1453
1541
  // --- the gateway atomically claim single-use execution, closing replay + cumulative spend, not
1454
1542
  // --- just re-checking policy. See ./gateway/README.md.
1543
+ // --- decision.governanceHeaders() is that verdict as headers: the SAME request the gate checked (the
1544
+ // --- gatedBookFlight mapping below, payload included), freshly signed, carrying its authorizationId.
1545
+ // --- So this file never rebuilds or re-signs the request, and an approved escalation resumed with
1546
+ // --- gatedBookFlight.resume() reaches the gateway with the approval's authorization the same way.
1455
1547
  async function bookFlightViaGateway(args, decision) {
1456
- // The COMPLETE body the tool receives. It is signed as a payload (MAGP 8.3.9): the eight signed fields cover amount and
1457
- // merchant, not anything else a real tool takes (a payee, a passenger list). The digest covers ALL of it, and the gateway
1458
- // refuses to run the tool on a body that is not exactly this one.
1548
+ // The COMPLETE body the tool receives — the payload gatedBookFlight signs (MAGP 8.3.9). The gateway refuses to run the
1549
+ // tool on a body that is not exactly the one signed, so keep this in step with the mapping below if you add a field.
1459
1550
  const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
1460
- const signed = await guard.buildSignedRequest({
1461
- action: '${scope}',
1462
- amount: args.amount,
1463
- currency: args.currency ?? '${currency}',
1464
- merchant: args.merchant,
1465
- // Passed through as-is (never defaulted): a missing riskLevel must reach the gateway as missing.
1466
- context: { tool: 'book-flight', riskLevel: args.riskLevel },
1467
- payload,
1468
- });
1469
- signed.authorizationId = decision?.authorizationId;
1470
1551
  const res = await fetch(GATEWAY + '/book-flight', {
1471
1552
  method: 'POST',
1472
- headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
1553
+ headers: { 'content-type': 'application/json', ...(await decision.governanceHeaders()) },
1473
1554
  body: JSON.stringify(payload),
1474
1555
  });
1475
1556
  const body = await res.json().catch(() => null);
1476
1557
  if (!res.ok) {
1477
1558
  const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
1478
1559
  err.name = 'GovernanceBlocked';
1479
- err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
1560
+ err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
1480
1561
  throw err;
1481
1562
  }
1482
1563
  return body;
@@ -1496,7 +1577,9 @@ const gatedBookFlight = guard.guardTool(
1496
1577
  // the action for review — never 'low'.
1497
1578
  context: { tool: 'book-flight', riskLevel: a.riskLevel },
1498
1579
  // The authorization is bound to the SAME body bookFlightViaGateway() sends, so the gate records what this agent
1499
- // signed and the gateway can prove it is running exactly that. Keep the two in step if you add a field.
1580
+ // signed and the gateway can prove it is running exactly that. This mapping is also what governanceHeaders() signs
1581
+ // for the gateway (riskLevel passed through as-is: a missing one reaches the gateway as missing). Keep the two in
1582
+ // step if you add a field.
1500
1583
  payload: { amount: a.amount, merchant: a.merchant, currency: a.currency ?? '${currency}' },
1501
1584
  }),
1502
1585
  );
@@ -1690,20 +1773,21 @@ ${withGateway ? `const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${
1690
1773
  // --- Calls the gateway process instead of a local function. There is no raw performAction() in
1691
1774
  // --- this file to call directly — the tool, and any real credentials it needs, live only in
1692
1775
  // --- ./gateway, which independently re-verifies this signed request itself.
1776
+ // --- decision.governanceHeaders() is the request gatedAction checked (its jurisdiction SIGNED, a top-level
1777
+ // --- field, never context), freshly signed, with the authorization it was granted (none for a value-less
1778
+ // --- action — see ./gateway/README.md). So this file never rebuilds or re-signs it, and npm run resume
1779
+ // --- reaches the gateway through here the same way.
1693
1780
  async function performViaGateway(args, decision) {
1694
- const { jurisdiction, ...context } = args; // the jurisdiction is SIGNED (a top-level field), never context
1695
- const signed = await guard.buildSignedRequest({ action: '${scope}', merchant: MERCHANT, jurisdiction, context });
1696
- signed.authorizationId = decision?.authorizationId; // none for a value-less action — see ./gateway/README.md
1697
1781
  const res = await fetch(GATEWAY + '/perform', {
1698
1782
  method: 'POST',
1699
- headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
1783
+ headers: { 'content-type': 'application/json', ...(await decision.governanceHeaders()) },
1700
1784
  body: '{}', // this tool reads nothing from the body; the request's fields travel in the SIGNED context
1701
1785
  });
1702
1786
  const body = await res.json().catch(() => null);
1703
1787
  if (!res.ok) {
1704
1788
  const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
1705
1789
  err.name = 'GovernanceBlocked';
1706
- err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
1790
+ err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
1707
1791
  throw err;
1708
1792
  }
1709
1793
  return body;
@@ -2528,7 +2612,11 @@ re-register this gateway yourself — after removing it from the registry, say.
2528
2612
  Registration needs **proof of control** (MAGP §8.7.6): you ask for a challenge naming you, this
2529
2613
  gateway signs it with its own key from \`service.metamynd.json\`, and you register with that
2530
2614
  signature. A DID is never registered on its text alone, so nobody can register a gateway they do
2531
- not hold the key for. Run this from this directory (bash; \`TOKEN\` is your owner access token):
2615
+ not hold the key for. Run this from this directory (bash; \`TOKEN\` is your owner access token).
2616
+ An owner access token lasts **15 minutes**, so get a fresh one just before you run this:
2617
+ \`POST /auth/login\` with \`{ "username": "<your email>", "password": "<your password>" }\` returns it
2618
+ as \`data.accessToken\` (that sign-in also signs your account out of the dashboard — one session at a
2619
+ time). A \`401\` from the snippet means the token expired: sign in again and re-run it.
2532
2620
 
2533
2621
  \`\`\`bash
2534
2622
  MAGP_API=https://metamynd.ai/api/v1 TOKEN=<owner access token> node --input-type=module -e "
@@ -3462,7 +3550,7 @@ async function callGateway(path, action, args) {
3462
3550
  if (!res.ok) {
3463
3551
  const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
3464
3552
  err.name = 'GovernanceBlocked';
3465
- err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
3553
+ err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
3466
3554
  throw err;
3467
3555
  }
3468
3556
  return body;
@@ -3674,7 +3762,7 @@ async function callGateway(path, action, args) {
3674
3762
  if (!res.ok) {
3675
3763
  const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
3676
3764
  err.name = 'GovernanceBlocked';
3677
- err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
3765
+ err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
3678
3766
  throw err;
3679
3767
  }
3680
3768
  return body;
@@ -4070,11 +4158,9 @@ async function authFlow(args) {
4070
4158
  rl?.resume();
4071
4159
  }
4072
4160
  rl?.close();
4073
- const login = await apiPost(base, '/auth/login', { username: email, password }, null);
4074
- const token = login?.data?.accessToken;
4075
- if (!token) fail('Login succeeded but no access token was returned.');
4161
+ const session = await ownerLogin(base, email, password);
4076
4162
  console.log(c.dim(` ${SESSION_REPLACED_NOTE}`));
4077
- return { base, token, email };
4163
+ return { base, session, email };
4078
4164
  }
4079
4165
 
4080
4166
  // One session per account: a CLI login replaces the account's dashboard session (the dashboard then answers
@@ -4090,7 +4176,7 @@ const REQUEST_STATE_FILE = 'metamynd-request.json';
4090
4176
  async function runRequest(args) {
4091
4177
  const owner = (typeof args.owner === 'string' ? args.owner : undefined) ?? process.env.METAMYND_OWNER;
4092
4178
  if (!owner) fail('--owner <ownerEmail> is required for a delegated request.');
4093
- const { base, token } = await authFlow(args);
4179
+ const { base, session } = await authFlow(args);
4094
4180
 
4095
4181
  // The same decision --harness and the hosted flow make (see announceFinancial): a non-financial request asks the
4096
4182
  // OWNER to approve no spending authority at all, and says so explicitly - an omitted amount means "use the
@@ -4118,7 +4204,7 @@ async function runRequest(args) {
4118
4204
  }
4119
4205
 
4120
4206
  console.log(c.dim(` → requesting "${name}" for ${owner} …`));
4121
- const res = await apiPost(base, '/onboarding/requests', { ownerEmail: owner, name, scope, ...(financial ? { perTxnMax, ...(maxAmountReq !== undefined && Number.isFinite(maxAmountReq) ? { maxAmount: maxAmountReq } : {}), ...(currencyReq ? { currency: currencyReq } : {}) } : { financial: false }), ...(merchantsReq.length ? { merchants: merchantsReq } : {}), ...(publicKey ? { publicKey } : {}) }, token);
4207
+ const res = await ownerPost(session, '/onboarding/requests', { ownerEmail: owner, name, scope, ...(financial ? { perTxnMax, ...(maxAmountReq !== undefined && Number.isFinite(maxAmountReq) ? { maxAmount: maxAmountReq } : {}), ...(currencyReq ? { currency: currencyReq } : {}) } : { financial: false }), ...(merchantsReq.length ? { merchants: merchantsReq } : {}), ...(publicKey ? { publicKey } : {}) });
4122
4208
  const d = res.data;
4123
4209
  // A server that predates non-financial requests ignores the field and files the request WITH default spend
4124
4210
  // limits. The developer cannot withdraw it, so say exactly what now exists and who can deny it; do not save a
@@ -4268,7 +4354,9 @@ async function main() {
4268
4354
  return ask(rl, prompt, def);
4269
4355
  };
4270
4356
 
4271
- // 1. Connection + login
4357
+ // 1. Connection + credentials. The sign-in itself waits until every prompt is answered (step 3): an owner access
4358
+ // token lives 15 minutes, and an owner who took longer over the prompts used to reach provisioning with an expired
4359
+ // one (pre-beta rerun 6, CLI login expiry follow-up to #926).
4272
4360
  const apiRaw = await pick('api', 'METAMYND_API', 'API base URL', DEFAULT_API);
4273
4361
  const base = String(apiRaw).replace(/\/+$/, '');
4274
4362
  const email = await pick('email', 'METAMYND_EMAIL', 'Owner email', '');
@@ -4282,13 +4370,6 @@ async function main() {
4282
4370
  rl?.resume();
4283
4371
  }
4284
4372
 
4285
- console.log(c.dim(`\n → logging in to ${base} …`));
4286
- const login = await apiPost(base, '/auth/login', { username: email, password }, null);
4287
- const token = login?.data?.accessToken;
4288
- if (!token) { rl?.close(); fail('Login succeeded but no access token was returned.'); }
4289
- console.log(` ${c.green('✓')} authenticated as ${email}`);
4290
- console.log(c.dim(` ${SESSION_REPLACED_NOTE}`));
4291
-
4292
4373
  // 2. Agent details — a --config file's fields are the default at every prompt/flag below.
4293
4374
  const name = await pick('name', null, 'Agent name', fileConfig?.name ?? 'Support Bot');
4294
4375
  const scope = await pick('scope', null, 'Mandate scope (governed action)', fileConfig?.scope ?? (financial ? 'flight-purchase' : 'perform-action'));
@@ -4333,19 +4414,6 @@ async function main() {
4333
4414
  rl?.close();
4334
4415
  fail('--daemon-socket generates its own key — pass --byok alone, not --public-key.');
4335
4416
  }
4336
- let generatedKey = null;
4337
- let daemonPublicKeyHex = null;
4338
- if (args.byok && !publicKey && daemonSocket) {
4339
- console.log(c.dim(' → asking the agentsafe-signer daemon to generate a key …'));
4340
- const { publicKeyHex } = await daemonRequest(daemonAdminSocket, 'generate-key', { allowRekey: false });
4341
- daemonPublicKeyHex = publicKeyHex;
4342
- publicKey = publicKeyHex;
4343
- console.log(` ${c.green('✓')} generated an Ed25519 keypair via the signer daemon ${c.dim('(the private key never left it)')}`);
4344
- } else if (args.byok && !publicKey) {
4345
- generatedKey = generateAgentKeypair();
4346
- publicKey = generatedKey.publicKeyHex;
4347
- console.log(` ${c.green('✓')} generated an Ed25519 keypair locally ${c.dim('(private key stays on this machine)')}`);
4348
- }
4349
4417
 
4350
4418
  const slug = slugify(name);
4351
4419
  const outDir = resolve(String(args.out || (interactive ? await ask(rl, 'Output directory', `./${slug}`) : `./${slug}`)));
@@ -4368,6 +4436,30 @@ async function main() {
4368
4436
  if (pack.reviewedMoleculeIds) sopFields.reviewedMoleculeIds = pack.reviewedMoleculeIds;
4369
4437
  }
4370
4438
  }
4439
+
4440
+ // Sign in only now, with every answer in hand (see step 1). Should the token still expire before the last
4441
+ // owner call, ownerFetch renews it once and retries.
4442
+ console.log(c.dim(`\n → logging in to ${base} …`));
4443
+ const session = await ownerLogin(base, email, password);
4444
+ console.log(` ${c.green('✓')} authenticated as ${email}`);
4445
+ console.log(c.dim(` ${SESSION_REPLACED_NOTE}`));
4446
+
4447
+ // The key is generated after the sign-in succeeds, as it always was: a daemon refuses to re-key (allowRekey: false),
4448
+ // so a key minted for a run that then failed to sign in would block the next attempt.
4449
+ let generatedKey = null;
4450
+ let daemonPublicKeyHex = null;
4451
+ if (args.byok && !publicKey && daemonSocket) {
4452
+ console.log(c.dim(' → asking the agentsafe-signer daemon to generate a key …'));
4453
+ const { publicKeyHex } = await daemonRequest(daemonAdminSocket, 'generate-key', { allowRekey: false });
4454
+ daemonPublicKeyHex = publicKeyHex;
4455
+ publicKey = publicKeyHex;
4456
+ console.log(` ${c.green('✓')} generated an Ed25519 keypair via the signer daemon ${c.dim('(the private key never left it)')}`);
4457
+ } else if (args.byok && !publicKey) {
4458
+ generatedKey = generateAgentKeypair();
4459
+ publicKey = generatedKey.publicKeyHex;
4460
+ console.log(` ${c.green('✓')} generated an Ed25519 keypair locally ${c.dim('(private key stays on this machine)')}`);
4461
+ }
4462
+
4371
4463
  console.log(c.dim(`\n → provisioning "${name}" (identity + mandate + SOP + Standards) …`));
4372
4464
  const body = {
4373
4465
  name, scope,
@@ -4386,7 +4478,7 @@ async function main() {
4386
4478
  ...(publicKey ? { publicKey } : {}),
4387
4479
  ...sopFields,
4388
4480
  };
4389
- const provisioned = await apiPost(base, '/onboarding/agent', body, token);
4481
+ const provisioned = await ownerPost(session, '/onboarding/agent', body);
4390
4482
  const config = provisioned?.data;
4391
4483
  if (!config?.agentDid) fail('Provisioning did not return a config with an agentDid.');
4392
4484
  console.log(` ${c.green('✓')} agent DID ${c.b(config.agentDid)}`);
@@ -4407,7 +4499,7 @@ async function main() {
4407
4499
  if (config.challenge) {
4408
4500
  console.log(c.dim(' → proving key control via the daemon (verify-key) …'));
4409
4501
  const { signature } = await daemonRequest(daemonSocket, 'sign-key-control-challenge', { challenge: config.challenge });
4410
- await apiPost(base, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature }, token);
4502
+ await ownerPost(session, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature });
4411
4503
  config.keyVerified = true;
4412
4504
  delete config.challenge; // one-time; consumed
4413
4505
  console.log(` ${c.green('✓')} key verified — MetaMynd never saw your private key, and neither did this CLI`);
@@ -4419,7 +4511,7 @@ async function main() {
4419
4511
  if (config.challenge) {
4420
4512
  console.log(c.dim(' → proving key control (verify-key) …'));
4421
4513
  const signature = signChallengeHex(generatedKey.privateKeyHex, config.challenge);
4422
- await apiPost(base, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature }, token);
4514
+ await ownerPost(session, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature });
4423
4515
  config.keyVerified = true;
4424
4516
  delete config.challenge; // one-time; consumed
4425
4517
  console.log(` ${c.green('✓')} key verified — MetaMynd never saw your private key`);
@@ -4430,6 +4522,7 @@ async function main() {
4430
4522
  console.log(c.dim(` sign this challenge with your private key (Ed25519 over its UTF-8 bytes, hex):`));
4431
4523
  console.log(c.dim(` challenge: ${config.challenge ?? '(none returned)'}`));
4432
4524
  console.log(c.dim(` POST ${base}/agent-identity/${config.identityId}/verify-key { "signature": "<hex>" } (owner token)`));
4525
+ console.log(c.dim(` (an owner access token lasts 15 minutes — sign in again with POST ${base}/auth/login for a fresh one)`));
4433
4526
  }
4434
4527
 
4435
4528
  // 3c. The gateway's OWN identity — a real did:key, separate from the agent's own (an agent must
@@ -4442,7 +4535,7 @@ async function main() {
4442
4535
  const gatewayDid = buildDidKey(rawPublicKeyFromSpkiHex(gwKeypair.publicKeyHex));
4443
4536
  gatewayIdentity = { did: gatewayDid, keyHex: gwKeypair.privateKeyHex };
4444
4537
  console.log(c.dim(`\n → registering the gateway's identity as a trusted counterparty (${financial ? "so it can claim this agent's holds and report to you" : 'so its reports reach your Activity Log'}) …`));
4445
- const reg = await registerGatewayCounterparty(base, token, gatewayDid, `${scope}-gateway`, financial ? 'claim' : 'report', gwKeypair.privateKeyHex, financial ? null : [config.agentDid]);
4538
+ const reg = await registerGatewayCounterparty(session, gatewayDid, `${scope}-gateway`, financial ? 'claim' : 'report', gwKeypair.privateKeyHex, financial ? null : [config.agentDid]);
4446
4539
  if (reg.ok) {
4447
4540
  console.log(` ${c.green('✓')} gateway DID ${c.b(gatewayDid)} — ${reg.message}`);
4448
4541
  } else {
@@ -4477,6 +4570,7 @@ export {
4477
4570
  generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
4478
4571
  scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
4479
4572
  ensurePolicyKey,
4573
+ ownerLogin, ownerFetch, ownerPost,
4480
4574
  };
4481
4575
  if (!process.env.CREATE_METAMYND_AGENT_NO_MAIN) {
4482
4576
  // fail() can also be reached from a callback outside main()'s promise chain (readline, child process).
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "create-metamynd-agent",
3
- "version": "0.14.34",
3
+ "version": "0.14.43",
4
4
  "description": "Scaffold a MetaMynd/AgentSafe-governed AI agent in one command — logs in, provisions the agent (identity + mandate + SOP + Standards) in a single call, writes agent.metamynd.json plus a runnable agent + separate tool-gateway process that closes direct-call, confused-deputy, replay, and cumulative-spend bypasses. --harness scaffolds a free, local, zero-network governance harness instead.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-metamynd-agent": "index.mjs"
8
8
  },
9
9
  "scripts": {
10
- "test": "node generated-code-parses.smoke.mjs && node harness-gateway.smoke.mjs && node harness-gateway-bind.smoke.mjs && node policy-demo.smoke.mjs && node hosted-nonfinancial.smoke.mjs && node modes-nonfinancial.smoke.mjs && node byok-daemon.smoke.mjs"
10
+ "test": "node generated-code-parses.smoke.mjs && node harness-gateway.smoke.mjs && node harness-gateway-bind.smoke.mjs && node policy-demo.smoke.mjs && node hosted-nonfinancial.smoke.mjs && node modes-nonfinancial.smoke.mjs && node byok-daemon.smoke.mjs && node owner-session.smoke.mjs"
11
11
  },
12
12
  "files": [
13
13
  "index.mjs",