create-metamynd-agent 0.14.34 → 0.14.40

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 +153 -65
  2. package/package.json +2 -2
package/index.mjs CHANGED
@@ -87,8 +87,15 @@ 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
+ const GUARD_VERSION = '^0.34.0';
92
99
  /** The harness entry point's config load, shared by both harness templates: a fresh clone has no
93
100
  * agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
94
101
  function harnessConfigLoad() {
@@ -172,7 +179,11 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
172
179
  // 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
180
  // another (or none) is released and refused. Required: the issuer refuses AUTHORIZATION_CONTEXT_REQUIRED to a claimer that
174
181
  // states none, so an older gateway could no longer run an approved escalation.
175
- const MCP_GUARD_VERSION = '^0.27.0';
182
+ // 0.27.1: reportOutcome() reports an authorization a request only NAMED as presented, never as the one it ran under (FW6-3).
183
+ // 0.28.0: a gateway judges owner-keyed operation rules (context-value-in / -not-in) from its bundle like the gate
184
+ // (NF-RISK-SELF). Required: a gateway on an older guard would read an operation rule's atom as unknown and let it run.
185
+ // 0.29.0: a claim the issuer refuses is logged in one line and its block carries the issuer's detail (FW6-1).
186
+ const MCP_GUARD_VERSION = '^0.29.0';
176
187
  /** A DID as it may appear inside a generated string literal (the gateway's allowedAgents pin): no quote, backslash or space. */
177
188
  const SAFE_DID = /^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/;
178
189
  const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
@@ -206,7 +217,10 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
206
217
  // 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
218
  // 0.25.0: depends on agentsafe-mcp-guard ^0.26.0 (one guard in the tree). No template change.
208
219
  // 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';
220
+ // 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.
221
+ // 0.27.0: depends on agentsafe-mcp-guard ^0.28.0 (operation rules). Required with MCP_GUARD_VERSION above.
222
+ // 0.28.0: a 403 for a refused claim carries the issuer's detail (FW6-1); depends on agentsafe-mcp-guard ^0.29.0.
223
+ const GATEWAY_VERSION = '^0.28.0';
210
224
 
211
225
  /** Appended to every scaffolded gateway server: give hold settlements still running a bounded moment on shutdown. */
212
226
  const DRAIN_ON_SHUTDOWN = `
@@ -941,7 +955,9 @@ function daemonRequest(socketPath, op, params, { connectTimeoutMs = 5000 } = {})
941
955
  sock.once('error', (err) => {
942
956
  cleanup();
943
957
  if (settled) return;
944
- if (err.code === 'ENOENT' && Date.now() < deadline) { setTimeout(attempt, 20); return; }
958
+ // ECONNREFUSED too: the one-shot admin socket is called right as `start --admin` builds it,
959
+ // and a Unix socket refuses between bind() and listen(). A refused connect sent nothing.
960
+ if ((err.code === 'ENOENT' || err.code === 'ECONNREFUSED') && Date.now() < deadline) { setTimeout(attempt, 20); return; }
945
961
  settled = true;
946
962
  clearTimeout(overallTimer);
947
963
  reject(Object.assign(new Error(`agentsafe-signer daemon unreachable at ${socketPath}: ${err.message}`), { code: 'DAEMON_UNREACHABLE' }));
@@ -981,6 +997,10 @@ async function apiPost(base, path, body, token) {
981
997
  } catch (e) {
982
998
  fail(`Cannot reach ${base}${path} — is the API up? (${e.message})`);
983
999
  }
1000
+ return readApiJson(res, path);
1001
+ }
1002
+
1003
+ async function readApiJson(res, path) {
984
1004
  const text = await res.text();
985
1005
  let json = null;
986
1006
  try { json = text ? JSON.parse(text) : null; } catch { /* non-JSON */ }
@@ -991,6 +1011,70 @@ async function apiPost(base, path, body, token) {
991
1011
  return json;
992
1012
  }
993
1013
 
1014
+ // ---------- owner session ----------
1015
+ // An owner access token lives 15 minutes (pre-beta rerun 6, OBS session TTL, #926); the refresh token from the same
1016
+ // sign-in renews it (POST /auth/refresh-token, which rotates both). Every owner-authenticated call this CLI makes goes
1017
+ // through ownerFetch: on a 401 it renews the session ONCE — the refresh token first, else a fresh sign-in with the
1018
+ // credentials already given to this run — and retries the call once. The password stays in this process's memory
1019
+ // for that purpose only; it is never written anywhere (pre-beta rerun 6, CLI login expiry).
1020
+ async function ownerLogin(base, email, password) {
1021
+ const login = await apiPost(base, '/auth/login', { username: email, password }, null);
1022
+ const token = login?.data?.accessToken;
1023
+ if (!token) fail('Login succeeded but no access token was returned.');
1024
+ return { base, email, password, token, refreshToken: login?.data?.refreshToken ?? null };
1025
+ }
1026
+
1027
+ /** Renew an expired owner session in place; true when a new access token was obtained. Never fatal. */
1028
+ async function renewOwnerSession(session) {
1029
+ const post = (path, body) => fetch(`${session.base}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
1030
+ if (session.refreshToken) {
1031
+ try {
1032
+ const res = await post('/auth/refresh-token', { refreshToken: session.refreshToken });
1033
+ const json = res.ok ? await res.json().catch(() => null) : null;
1034
+ // The refresh endpoint answers at the top level ({ accessToken, refreshToken }); accept a `data` envelope too.
1035
+ const token = json?.accessToken ?? json?.data?.accessToken;
1036
+ if (token) {
1037
+ session.token = token;
1038
+ session.refreshToken = json?.refreshToken ?? json?.data?.refreshToken ?? session.refreshToken;
1039
+ return true;
1040
+ }
1041
+ } catch { /* fall through to a fresh sign-in */ }
1042
+ }
1043
+ if (typeof session.password === 'string') {
1044
+ try {
1045
+ const res = await post('/auth/login', { username: session.email, password: session.password });
1046
+ const json = res.ok ? await res.json().catch(() => null) : null;
1047
+ if (json?.data?.accessToken) {
1048
+ session.token = json.data.accessToken;
1049
+ session.refreshToken = json.data.refreshToken ?? null;
1050
+ return true;
1051
+ }
1052
+ } catch { /* reported by the caller's retry */ }
1053
+ }
1054
+ return false;
1055
+ }
1056
+
1057
+ /** fetch() with the owner's bearer token; a 401 renews the session once and retries once. */
1058
+ async function ownerFetch(session, path, init = {}) {
1059
+ const send = () => fetch(`${session.base}${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...(init.headers ?? {}), Authorization: `Bearer ${session.token}` } });
1060
+ const res = await send();
1061
+ if (res.status !== 401) return res;
1062
+ if (!(await renewOwnerSession(session))) return res;
1063
+ console.log(c.dim(' → your owner session had expired; renewed it and retried'));
1064
+ return send();
1065
+ }
1066
+
1067
+ /** apiPost for an owner-authenticated call: fatal on failure, renewed once on an expired token. */
1068
+ async function ownerPost(session, path, body) {
1069
+ let res;
1070
+ try {
1071
+ res = await ownerFetch(session, path, { method: 'POST', body: JSON.stringify(body) });
1072
+ } catch (e) {
1073
+ fail(`Cannot reach ${session.base}${path} — is the API up? (${e.message})`);
1074
+ }
1075
+ return readApiJson(res, path);
1076
+ }
1077
+
994
1078
  /** GET a public, read-only endpoint; null on ANY failure (unreachable, non-2xx, non-JSON) — never fatal. */
995
1079
  async function softGet(base, path) {
996
1080
  try {
@@ -1059,24 +1143,22 @@ async function resolveHostedRulePack(base, key, { financial, perTxnMax, maxAmoun
1059
1143
  * dashboard registration shows its own dialog before setting the flag); see the printed message
1060
1144
  * either way.
1061
1145
  */
1062
- async function registerGatewayCounterparty(base, token, did, label, purpose = 'claim', privateKeyHex = null, agents = null) {
1146
+ async function registerGatewayCounterparty(session, did, label, purpose = 'claim', privateKeyHex = null, agents = null) {
1063
1147
  try {
1064
1148
  // The gateway accepts its own registration (MAGP §8.7.6): ask for a challenge naming this owner, sign it with the gateway's
1065
1149
  // key (generated here, so this process holds it), and register with that proof. A DID is never registered on its text.
1066
1150
  let proof;
1067
1151
  if (privateKeyHex) {
1068
- const ch = await fetch(`${base}/policy/counterparties/challenge`, {
1152
+ const ch = await ownerFetch(session, '/policy/counterparties/challenge', {
1069
1153
  method: 'POST',
1070
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
1071
1154
  body: JSON.stringify({ did, purpose }),
1072
1155
  });
1073
1156
  const chJson = await ch.json().catch(() => null);
1074
1157
  if (!ch.ok || !chJson?.data?.message) return { ok: false, message: chJson?.message ?? `challenge HTTP ${ch.status}` };
1075
1158
  proof = { challengeToken: chJson.data.challengeToken, signature: signChallengeHex(privateKeyHex, chJson.data.message) };
1076
1159
  }
1077
- const res = await fetch(`${base}/policy/counterparties`, {
1160
+ const res = await ownerFetch(session, '/policy/counterparties', {
1078
1161
  method: 'POST',
1079
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
1080
1162
  // `report` (a non-financial gateway, which never claims): it may only report to this owner, and it changes nothing about
1081
1163
  // who may claim their holds, so it needs no enforcement confirmation (MAGP §8.7.6, §16.4).
1082
1164
  // `agents` (report only): the agents this gateway serves — the issuer lets it claim an approved value-less hold of THOSE
@@ -1452,31 +1534,24 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
1452
1534
  // --- value-bearing action (sealValueActions, on by default) — its authorizationId is what lets
1453
1535
  // --- the gateway atomically claim single-use execution, closing replay + cumulative spend, not
1454
1536
  // --- just re-checking policy. See ./gateway/README.md.
1537
+ // --- decision.governanceHeaders() is that verdict as headers: the SAME request the gate checked (the
1538
+ // --- gatedBookFlight mapping below, payload included), freshly signed, carrying its authorizationId.
1539
+ // --- So this file never rebuilds or re-signs the request, and an approved escalation resumed with
1540
+ // --- gatedBookFlight.resume() reaches the gateway with the approval's authorization the same way.
1455
1541
  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.
1542
+ // The COMPLETE body the tool receives — the payload gatedBookFlight signs (MAGP 8.3.9). The gateway refuses to run the
1543
+ // 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
1544
  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
1545
  const res = await fetch(GATEWAY + '/book-flight', {
1471
1546
  method: 'POST',
1472
- headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
1547
+ headers: { 'content-type': 'application/json', ...(await decision.governanceHeaders()) },
1473
1548
  body: JSON.stringify(payload),
1474
1549
  });
1475
1550
  const body = await res.json().catch(() => null);
1476
1551
  if (!res.ok) {
1477
1552
  const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
1478
1553
  err.name = 'GovernanceBlocked';
1479
- err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
1554
+ err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
1480
1555
  throw err;
1481
1556
  }
1482
1557
  return body;
@@ -1496,7 +1571,9 @@ const gatedBookFlight = guard.guardTool(
1496
1571
  // the action for review — never 'low'.
1497
1572
  context: { tool: 'book-flight', riskLevel: a.riskLevel },
1498
1573
  // 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.
1574
+ // signed and the gateway can prove it is running exactly that. This mapping is also what governanceHeaders() signs
1575
+ // for the gateway (riskLevel passed through as-is: a missing one reaches the gateway as missing). Keep the two in
1576
+ // step if you add a field.
1500
1577
  payload: { amount: a.amount, merchant: a.merchant, currency: a.currency ?? '${currency}' },
1501
1578
  }),
1502
1579
  );
@@ -1690,20 +1767,21 @@ ${withGateway ? `const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${
1690
1767
  // --- Calls the gateway process instead of a local function. There is no raw performAction() in
1691
1768
  // --- this file to call directly — the tool, and any real credentials it needs, live only in
1692
1769
  // --- ./gateway, which independently re-verifies this signed request itself.
1770
+ // --- decision.governanceHeaders() is the request gatedAction checked (its jurisdiction SIGNED, a top-level
1771
+ // --- field, never context), freshly signed, with the authorization it was granted (none for a value-less
1772
+ // --- action — see ./gateway/README.md). So this file never rebuilds or re-signs it, and npm run resume
1773
+ // --- reaches the gateway through here the same way.
1693
1774
  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
1775
  const res = await fetch(GATEWAY + '/perform', {
1698
1776
  method: 'POST',
1699
- headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
1777
+ headers: { 'content-type': 'application/json', ...(await decision.governanceHeaders()) },
1700
1778
  body: '{}', // this tool reads nothing from the body; the request's fields travel in the SIGNED context
1701
1779
  });
1702
1780
  const body = await res.json().catch(() => null);
1703
1781
  if (!res.ok) {
1704
1782
  const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
1705
1783
  err.name = 'GovernanceBlocked';
1706
- err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
1784
+ err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
1707
1785
  throw err;
1708
1786
  }
1709
1787
  return body;
@@ -2528,7 +2606,11 @@ re-register this gateway yourself — after removing it from the registry, say.
2528
2606
  Registration needs **proof of control** (MAGP §8.7.6): you ask for a challenge naming you, this
2529
2607
  gateway signs it with its own key from \`service.metamynd.json\`, and you register with that
2530
2608
  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):
2609
+ not hold the key for. Run this from this directory (bash; \`TOKEN\` is your owner access token).
2610
+ An owner access token lasts **15 minutes**, so get a fresh one just before you run this:
2611
+ \`POST /auth/login\` with \`{ "username": "<your email>", "password": "<your password>" }\` returns it
2612
+ as \`data.accessToken\` (that sign-in also signs your account out of the dashboard — one session at a
2613
+ time). A \`401\` from the snippet means the token expired: sign in again and re-run it.
2532
2614
 
2533
2615
  \`\`\`bash
2534
2616
  MAGP_API=https://metamynd.ai/api/v1 TOKEN=<owner access token> node --input-type=module -e "
@@ -3462,7 +3544,7 @@ async function callGateway(path, action, args) {
3462
3544
  if (!res.ok) {
3463
3545
  const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
3464
3546
  err.name = 'GovernanceBlocked';
3465
- err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
3547
+ err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
3466
3548
  throw err;
3467
3549
  }
3468
3550
  return body;
@@ -3674,7 +3756,7 @@ async function callGateway(path, action, args) {
3674
3756
  if (!res.ok) {
3675
3757
  const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
3676
3758
  err.name = 'GovernanceBlocked';
3677
- err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
3759
+ err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
3678
3760
  throw err;
3679
3761
  }
3680
3762
  return body;
@@ -4070,11 +4152,9 @@ async function authFlow(args) {
4070
4152
  rl?.resume();
4071
4153
  }
4072
4154
  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.');
4155
+ const session = await ownerLogin(base, email, password);
4076
4156
  console.log(c.dim(` ${SESSION_REPLACED_NOTE}`));
4077
- return { base, token, email };
4157
+ return { base, session, email };
4078
4158
  }
4079
4159
 
4080
4160
  // One session per account: a CLI login replaces the account's dashboard session (the dashboard then answers
@@ -4090,7 +4170,7 @@ const REQUEST_STATE_FILE = 'metamynd-request.json';
4090
4170
  async function runRequest(args) {
4091
4171
  const owner = (typeof args.owner === 'string' ? args.owner : undefined) ?? process.env.METAMYND_OWNER;
4092
4172
  if (!owner) fail('--owner <ownerEmail> is required for a delegated request.');
4093
- const { base, token } = await authFlow(args);
4173
+ const { base, session } = await authFlow(args);
4094
4174
 
4095
4175
  // The same decision --harness and the hosted flow make (see announceFinancial): a non-financial request asks the
4096
4176
  // OWNER to approve no spending authority at all, and says so explicitly - an omitted amount means "use the
@@ -4118,7 +4198,7 @@ async function runRequest(args) {
4118
4198
  }
4119
4199
 
4120
4200
  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);
4201
+ 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
4202
  const d = res.data;
4123
4203
  // A server that predates non-financial requests ignores the field and files the request WITH default spend
4124
4204
  // limits. The developer cannot withdraw it, so say exactly what now exists and who can deny it; do not save a
@@ -4268,7 +4348,9 @@ async function main() {
4268
4348
  return ask(rl, prompt, def);
4269
4349
  };
4270
4350
 
4271
- // 1. Connection + login
4351
+ // 1. Connection + credentials. The sign-in itself waits until every prompt is answered (step 3): an owner access
4352
+ // token lives 15 minutes, and an owner who took longer over the prompts used to reach provisioning with an expired
4353
+ // one (pre-beta rerun 6, CLI login expiry follow-up to #926).
4272
4354
  const apiRaw = await pick('api', 'METAMYND_API', 'API base URL', DEFAULT_API);
4273
4355
  const base = String(apiRaw).replace(/\/+$/, '');
4274
4356
  const email = await pick('email', 'METAMYND_EMAIL', 'Owner email', '');
@@ -4282,13 +4364,6 @@ async function main() {
4282
4364
  rl?.resume();
4283
4365
  }
4284
4366
 
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
4367
  // 2. Agent details — a --config file's fields are the default at every prompt/flag below.
4293
4368
  const name = await pick('name', null, 'Agent name', fileConfig?.name ?? 'Support Bot');
4294
4369
  const scope = await pick('scope', null, 'Mandate scope (governed action)', fileConfig?.scope ?? (financial ? 'flight-purchase' : 'perform-action'));
@@ -4333,19 +4408,6 @@ async function main() {
4333
4408
  rl?.close();
4334
4409
  fail('--daemon-socket generates its own key — pass --byok alone, not --public-key.');
4335
4410
  }
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
4411
 
4350
4412
  const slug = slugify(name);
4351
4413
  const outDir = resolve(String(args.out || (interactive ? await ask(rl, 'Output directory', `./${slug}`) : `./${slug}`)));
@@ -4368,6 +4430,30 @@ async function main() {
4368
4430
  if (pack.reviewedMoleculeIds) sopFields.reviewedMoleculeIds = pack.reviewedMoleculeIds;
4369
4431
  }
4370
4432
  }
4433
+
4434
+ // Sign in only now, with every answer in hand (see step 1). Should the token still expire before the last
4435
+ // owner call, ownerFetch renews it once and retries.
4436
+ console.log(c.dim(`\n → logging in to ${base} …`));
4437
+ const session = await ownerLogin(base, email, password);
4438
+ console.log(` ${c.green('✓')} authenticated as ${email}`);
4439
+ console.log(c.dim(` ${SESSION_REPLACED_NOTE}`));
4440
+
4441
+ // The key is generated after the sign-in succeeds, as it always was: a daemon refuses to re-key (allowRekey: false),
4442
+ // so a key minted for a run that then failed to sign in would block the next attempt.
4443
+ let generatedKey = null;
4444
+ let daemonPublicKeyHex = null;
4445
+ if (args.byok && !publicKey && daemonSocket) {
4446
+ console.log(c.dim(' → asking the agentsafe-signer daemon to generate a key …'));
4447
+ const { publicKeyHex } = await daemonRequest(daemonAdminSocket, 'generate-key', { allowRekey: false });
4448
+ daemonPublicKeyHex = publicKeyHex;
4449
+ publicKey = publicKeyHex;
4450
+ console.log(` ${c.green('✓')} generated an Ed25519 keypair via the signer daemon ${c.dim('(the private key never left it)')}`);
4451
+ } else if (args.byok && !publicKey) {
4452
+ generatedKey = generateAgentKeypair();
4453
+ publicKey = generatedKey.publicKeyHex;
4454
+ console.log(` ${c.green('✓')} generated an Ed25519 keypair locally ${c.dim('(private key stays on this machine)')}`);
4455
+ }
4456
+
4371
4457
  console.log(c.dim(`\n → provisioning "${name}" (identity + mandate + SOP + Standards) …`));
4372
4458
  const body = {
4373
4459
  name, scope,
@@ -4386,7 +4472,7 @@ async function main() {
4386
4472
  ...(publicKey ? { publicKey } : {}),
4387
4473
  ...sopFields,
4388
4474
  };
4389
- const provisioned = await apiPost(base, '/onboarding/agent', body, token);
4475
+ const provisioned = await ownerPost(session, '/onboarding/agent', body);
4390
4476
  const config = provisioned?.data;
4391
4477
  if (!config?.agentDid) fail('Provisioning did not return a config with an agentDid.');
4392
4478
  console.log(` ${c.green('✓')} agent DID ${c.b(config.agentDid)}`);
@@ -4407,7 +4493,7 @@ async function main() {
4407
4493
  if (config.challenge) {
4408
4494
  console.log(c.dim(' → proving key control via the daemon (verify-key) …'));
4409
4495
  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);
4496
+ await ownerPost(session, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature });
4411
4497
  config.keyVerified = true;
4412
4498
  delete config.challenge; // one-time; consumed
4413
4499
  console.log(` ${c.green('✓')} key verified — MetaMynd never saw your private key, and neither did this CLI`);
@@ -4419,7 +4505,7 @@ async function main() {
4419
4505
  if (config.challenge) {
4420
4506
  console.log(c.dim(' → proving key control (verify-key) …'));
4421
4507
  const signature = signChallengeHex(generatedKey.privateKeyHex, config.challenge);
4422
- await apiPost(base, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature }, token);
4508
+ await ownerPost(session, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature });
4423
4509
  config.keyVerified = true;
4424
4510
  delete config.challenge; // one-time; consumed
4425
4511
  console.log(` ${c.green('✓')} key verified — MetaMynd never saw your private key`);
@@ -4430,6 +4516,7 @@ async function main() {
4430
4516
  console.log(c.dim(` sign this challenge with your private key (Ed25519 over its UTF-8 bytes, hex):`));
4431
4517
  console.log(c.dim(` challenge: ${config.challenge ?? '(none returned)'}`));
4432
4518
  console.log(c.dim(` POST ${base}/agent-identity/${config.identityId}/verify-key { "signature": "<hex>" } (owner token)`));
4519
+ console.log(c.dim(` (an owner access token lasts 15 minutes — sign in again with POST ${base}/auth/login for a fresh one)`));
4433
4520
  }
4434
4521
 
4435
4522
  // 3c. The gateway's OWN identity — a real did:key, separate from the agent's own (an agent must
@@ -4442,7 +4529,7 @@ async function main() {
4442
4529
  const gatewayDid = buildDidKey(rawPublicKeyFromSpkiHex(gwKeypair.publicKeyHex));
4443
4530
  gatewayIdentity = { did: gatewayDid, keyHex: gwKeypair.privateKeyHex };
4444
4531
  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]);
4532
+ const reg = await registerGatewayCounterparty(session, gatewayDid, `${scope}-gateway`, financial ? 'claim' : 'report', gwKeypair.privateKeyHex, financial ? null : [config.agentDid]);
4446
4533
  if (reg.ok) {
4447
4534
  console.log(` ${c.green('✓')} gateway DID ${c.b(gatewayDid)} — ${reg.message}`);
4448
4535
  } else {
@@ -4477,6 +4564,7 @@ export {
4477
4564
  generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
4478
4565
  scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
4479
4566
  ensurePolicyKey,
4567
+ ownerLogin, ownerFetch, ownerPost,
4480
4568
  };
4481
4569
  if (!process.env.CREATE_METAMYND_AGENT_NO_MAIN) {
4482
4570
  // 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.40",
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",