create-metamynd-agent 0.14.18 → 0.14.24

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 +168 -37
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -65,7 +65,14 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
65
65
  // authorize request of its choosing, MAGP §8.2). Required for every scaffolded agent.
66
66
  // 0.20.0: guard.capture() signs as the agent (MAGP-SETTLE-v1) — a hold nobody claimed is settled only by its agent, a
67
67
  // counterparty the owner registered, or (an open testnet owner) anyone (MAGP §8.7.4).
68
- const GUARD_VERSION = '^0.20.0';
68
+ // 0.21.0: guardTool() settles the hold an allowed call was granted (captured when the tool runs, released when it cannot
69
+ // have), guard.void() releases one signed, and a gated tool's .resume(escalationId, args) runs an approved escalation once.
70
+ // Required: the scaffold's npm run resume uses .resume().
71
+ // 0.22.0: a concurrent resume of one escalation in a process is refused AUTHORIZATION_IN_USE, and a hold minted for an
72
+ // authorize whose answer never arrived is found by nonce and released. Required: npm run resume relies on the lock.
73
+ // 0.23.0: the guard derives the same risk floor as the gate (owner tier + a payment at or above the owner's share of the
74
+ // per-transaction cap is HIGH), so a local check and the gate agree on a large payment's review. No template change.
75
+ const GUARD_VERSION = '^0.23.0';
69
76
  /** The harness entry point's config load, shared by both harness templates: a fresh clone has no
70
77
  * agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
71
78
  function harnessConfigLoad() {
@@ -143,7 +150,10 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
143
150
  // 0.23.0: acceptCounterpartyChallenge() — a Service signs its acceptance of an owner's registration (MAGP §8.7.6). No
144
151
  // template change; required with GATEWAY_VERSION below so the gateway never resolves a second guard of its own.
145
152
  // 0.24.0: reportOutcome() signs a reportId and an occurrence count (aggregated refusals, a durable spool).
146
- const MCP_GUARD_VERSION = '^0.24.0';
153
+ // 0.25.0: honourApprovals — a Service without requireAuthorization (the value-less gateway) still runs an owner-approved
154
+ // escalation, exactly once (MAGP §8.7.18). Required: the non-financial gateway sets it and asserts guard.honoursApprovals.
155
+ // 0.26.0: a gateway judges the same derived risk floor as the gate (owner tier + amount share of the cap).
156
+ const MCP_GUARD_VERSION = '^0.26.0';
147
157
  /** A DID as it may appear inside a generated string literal (the gateway's allowedAgents pin): no quote, backslash or space. */
148
158
  const SAFE_DID = /^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/;
149
159
  const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
@@ -174,7 +184,9 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
174
184
  // 0.21.0: per-route allowedAgents; depends on agentsafe-mcp-guard ^0.22.0.
175
185
  // 0.22.0: depends on agentsafe-mcp-guard ^0.23.0.
176
186
  // 0.23.0: refusals aggregated with their count, and `reportSpool` (both hosted templates set it); depends on agentsafe-mcp-guard ^0.24.0.
177
- const GATEWAY_VERSION = '^0.23.0';
187
+ // 0.24.0: depends on agentsafe-mcp-guard ^0.25.0 (one guard in the tree with MCP_GUARD_VERSION above). No template change.
188
+ // 0.25.0: depends on agentsafe-mcp-guard ^0.26.0 (one guard in the tree). No template change.
189
+ const GATEWAY_VERSION = '^0.25.0';
178
190
 
179
191
  /** Appended to every scaffolded gateway server: give hold settlements still running a bounded moment on shutdown. */
180
192
  const DRAIN_ON_SHUTDOWN = `
@@ -261,6 +273,11 @@ ${c.b('Options')}
261
273
  --max-amount <n> Total mandate budget (default 10000)
262
274
  --currency <cur> Currency (default USD)
263
275
  --merchants <a,b> Allowed merchants, comma-separated (optional)
276
+ --risk-tier <level> The owner's risk floor for the action: low | medium | high | critical. The agent's own
277
+ riskLevel can raise it, never lower it (high = every call is reviewed by a person).
278
+ --amount-review <f> A single payment at or above this share of the per-transaction cap is high risk (reviewed),
279
+ whatever the agent claims. A fraction in (0, 1] (default 0.7), or "off". Without --merchants,
280
+ the first payment to each merchant is reviewed too.
264
281
  --byok Bring-your-own-key: generate the keypair locally, provision + prove control
265
282
  (MetaMynd never sees the private key). Overridden by --public-key.
266
283
  --public-key <hex> BYOK with a key you already hold (SPKI/raw hex); you prove control yourself
@@ -1018,7 +1035,7 @@ async function resolveHostedRulePack(base, key, { financial, perTxnMax, maxAmoun
1018
1035
  * dashboard registration shows its own dialog before setting the flag); see the printed message
1019
1036
  * either way.
1020
1037
  */
1021
- async function registerGatewayCounterparty(base, token, did, label, purpose = 'claim', privateKeyHex = null) {
1038
+ async function registerGatewayCounterparty(base, token, did, label, purpose = 'claim', privateKeyHex = null, agents = null) {
1022
1039
  try {
1023
1040
  // The gateway accepts its own registration (MAGP §8.7.6): ask for a challenge naming this owner, sign it with the gateway's
1024
1041
  // key (generated here, so this process holds it), and register with that proof. A DID is never registered on its text.
@@ -1038,7 +1055,9 @@ async function registerGatewayCounterparty(base, token, did, label, purpose = 'c
1038
1055
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
1039
1056
  // `report` (a non-financial gateway, which never claims): it may only report to this owner, and it changes nothing about
1040
1057
  // who may claim their holds, so it needs no enforcement confirmation (MAGP §8.7.6, §16.4).
1041
- body: JSON.stringify({ did, label, purpose, ...(proof ? { proof } : {}), ...(purpose === 'claim' ? { confirmEnforcementChange: true } : {}) }),
1058
+ // `agents` (report only): the agents this gateway serves — the issuer lets it claim an approved value-less hold of THOSE
1059
+ // agents only, never another agent of the same owner (MAGP §8.7.6).
1060
+ body: JSON.stringify({ did, label, purpose, ...(proof ? { proof } : {}), ...(agents ? { agents } : {}), ...(purpose === 'claim' ? { confirmEnforcementChange: true } : {}) }),
1042
1061
  });
1043
1062
  const json = await res.json().catch(() => null);
1044
1063
  if (!res.ok || json?.success === false) return { ok: false, message: json?.message ?? `HTTP ${res.status}` };
@@ -1134,12 +1153,74 @@ function keepConfigAndFail(outDir, config, why) {
1134
1153
  * guard.escalationStatus(id) for the authorization and presents it to run the action (MAGP §8.7.18). Generated code: it
1135
1154
  * reads `g`, the refusal's governance verdict, and `dim`, both in scope at every call site.
1136
1155
  */
1137
- function escalateNote(sandbox) {
1138
- return sandbox
1139
- ? `console.log(dim(' nobody can approve it here: the shared sandbox has no owner you can sign in as.'));
1140
- console.log(dim(' Provision your own agent to approve one in your dashboard.'));`
1141
- : `console.log(dim(' not a failure: it waits in your dashboard, under Escalations' + (g.escalationId ? ' (' + g.escalationId + ')' : '') + '.'));
1156
+ function escalateNote(sandbox, argsExpr = null) {
1157
+ if (sandbox) {
1158
+ return `console.log(dim(' nobody can approve it here: the shared sandbox has no owner you can sign in as.'));
1159
+ console.log(dim(' Provision your own agent to approve one in your dashboard.'));`;
1160
+ }
1161
+ if (!argsExpr) {
1162
+ return `console.log(dim(' not a failure: it waits in your dashboard, under Escalations' + (g.escalationId ? ' (' + g.escalationId + ')' : '') + '.'));
1142
1163
  console.log(dim(' Once you approve it, guard.escalationStatus(id) gives the agent the authorization to run it.'));`;
1164
+ }
1165
+ // Resumable (0.14.21; pre-beta rerun 3, E-1): the call is saved so `npm run resume -- <id>` can run it, once approved.
1166
+ return `console.log(dim(' not a failure: it waits in your dashboard, under Escalations' + (g.escalationId ? ' (' + g.escalationId + ')' : '') + '.'));
1167
+ if (g.escalationId) {
1168
+ saveEscalation(g.escalationId, ${argsExpr});
1169
+ console.log(dim(' Approve it there, then run it - exactly once - with: npm run resume -- ' + g.escalationId));
1170
+ }`;
1171
+ }
1172
+
1173
+ /**
1174
+ * The resume path a resumable scaffold carries (0.14.21; pre-beta rerun 3, E-1 — resuming used to mean writing ~15 lines
1175
+ * by hand). `npm start` saves an escalated call's arguments to ./escalations/<id>.json; `npm run resume -- <id>` hands them
1176
+ * to the gated tool's `.resume()` (agentsafe-guard >= 0.21.0), which waits for the owner's decision and then runs the tool
1177
+ * exactly once under the authorization the approval minted. Generated code; `tool` is the gated tool's variable name.
1178
+ */
1179
+ function resumeBlock(tool) {
1180
+ return `// --- Escalations: \`npm run resume -- <escalationId>\` runs a call your owner APPROVED, exactly once. ---
1181
+ // npm start saves an escalated call's arguments to ./escalations/<id>.json. Approve it in the dashboard (AgentSafe ->
1182
+ // Escalations), then resume it: ${tool}.resume() waits for the decision and runs the tool once, under the authorization
1183
+ // the approval minted. Your own agent does the same with the escalationId from the refusal (e.governance.escalationId).
1184
+ import { mkdirSync as __mkdir, writeFileSync as __write, readFileSync as __read, rmSync as __rm, renameSync as __rename } from 'node:fs';
1185
+ const ESCALATIONS = new URL('./escalations/', import.meta.url);
1186
+ const escalationFile = (id) => {
1187
+ if (!/^[A-Za-z0-9_-]{1,80}$/.test(String(id ?? ''))) throw new Error('not an escalation id: ' + id);
1188
+ return new URL(id + '.json', ESCALATIONS);
1189
+ };
1190
+ function saveEscalation(id, args) {
1191
+ __mkdir(ESCALATIONS, { recursive: true });
1192
+ __write(escalationFile(id), JSON.stringify(args, null, 2));
1193
+ }
1194
+ if (process.argv[2] === '--resume') {
1195
+ const id = process.argv[3];
1196
+ // Take the saved call ATOMICALLY (a rename): a second npm run resume of the same escalation started meanwhile finds
1197
+ // nothing to take, so two processes cannot both run it. Put back if it is still undecided; removed once it is final.
1198
+ let saved, taken;
1199
+ try {
1200
+ const file = escalationFile(id);
1201
+ taken = new URL(file.href + '.resuming');
1202
+ __rename(file, taken);
1203
+ saved = JSON.parse(__read(taken, 'utf8'));
1204
+ } catch {
1205
+ console.error(' No saved escalation "' + (id ?? '') + '" to resume (or another npm run resume is running it). Run npm start first, then: npm run resume -- <escalationId>');
1206
+ process.exit(1);
1207
+ }
1208
+ console.log(' waiting for your decision on ' + id + ' (approve it in the dashboard: AgentSafe -> Escalations) ...');
1209
+ try {
1210
+ const result = await ${tool}.resume(id, saved);
1211
+ __rm(taken, { force: true }); // used: a second resume has nothing to run
1212
+ console.log('\\x1b[32m RAN\\x1b[0m approved, and run exactly once: ' + JSON.stringify(result));
1213
+ } catch (e) {
1214
+ const g = e.governance ?? {};
1215
+ // A final answer (rejected, already used, expired) retires the saved call; still pending or unreachable keeps it.
1216
+ if (g.decision === 'block' && g.reasonCode !== 'GATE_UNREACHABLE') __rm(taken, { force: true });
1217
+ else __rename(taken, escalationFile(id)); // still undecided, or the gate was unreachable: keep it for the next try
1218
+ console.log('\\x1b[31m NOT RUN\\x1b[0m ' + (g.reasonCode ?? e.message) + (g.status ? ' (' + g.status + ')' : ''));
1219
+ process.exitCode = 1;
1220
+ }
1221
+ process.exit();
1222
+ }
1223
+ `;
1143
1224
  }
1144
1225
 
1145
1226
  function exampleIndexNoGateway(scope, perTxnMax, currency, merchant, sandbox = false) {
@@ -1195,6 +1276,7 @@ const gatedRaiseOwnLimit = guard.guardTool(
1195
1276
  }),
1196
1277
  );
1197
1278
 
1279
+ ${sandbox ? '' : resumeBlock('gatedBookFlight')}
1198
1280
  const dim = (t) => '\\x1b[2m' + t + '\\x1b[0m';
1199
1281
  const bold = (t) => '\\x1b[1m' + t + '\\x1b[0m';
1200
1282
  const rule = (n) => ' ' + '-'.repeat(n);
@@ -1253,7 +1335,7 @@ async function attempt(n, intent, args, tool = gatedBookFlight) {
1253
1335
  if (g.decision === 'escalate') {
1254
1336
  console.log('\\x1b[33m ESCALATED\\x1b[0m held for a human - ' + g.reasonCode);
1255
1337
  console.log(dim(' ' + why));
1256
- ${escalateNote(sandbox)}
1338
+ ${escalateNote(sandbox, sandbox ? null : 'args')}
1257
1339
  } else {
1258
1340
  console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
1259
1341
  console.log(dim(' ' + why));
@@ -1400,6 +1482,7 @@ const gatedRaiseOwnLimit = guard.guardTool(
1400
1482
  }),
1401
1483
  );
1402
1484
 
1485
+ ${resumeBlock('gatedBookFlight')}
1403
1486
  const dim = (t) => '\\x1b[2m' + t + '\\x1b[0m';
1404
1487
  const bold = (t) => '\\x1b[1m' + t + '\\x1b[0m';
1405
1488
  const rule = (n) => ' ' + '-'.repeat(n);
@@ -1461,7 +1544,7 @@ async function attempt(n, intent, args, tool = gatedBookFlight) {
1461
1544
  if (g.decision === 'escalate') {
1462
1545
  console.log('\\x1b[33m ESCALATED\\x1b[0m held for a human - ' + g.reasonCode);
1463
1546
  console.log(dim(' ' + why));
1464
- ${escalateNote(false)}
1547
+ ${escalateNote(false, 'args')}
1465
1548
  } else {
1466
1549
  console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
1467
1550
  console.log(dim(' ' + why));
@@ -1615,7 +1698,7 @@ const gatedChangeOwnPermissions = guard.guardTool(
1615
1698
  changeOwnPermissions,
1616
1699
  (a) => ({ context: a }),
1617
1700
  );
1618
-
1701
+ ${!sandbox ? '\n' + resumeBlock('gatedAction') : ''}
1619
1702
  const STEPS = ${JSON.stringify(steps, null, 2)};
1620
1703
  const INPUTS = ${JSON.stringify(demo.inputs)};
1621
1704
  const NOT_STAGED = ${JSON.stringify(demo.notDemonstrated)};
@@ -1636,7 +1719,7 @@ async function attempt(n, total, step) {
1636
1719
  got = g.decision ?? 'error';
1637
1720
  if (g.decision === 'escalate') {
1638
1721
  console.log('\\x1b[33m ESCALATED\\x1b[0m held for a human - ' + g.reasonCode);
1639
- ${escalateNote(sandbox)}
1722
+ ${escalateNote(sandbox, !sandbox ? 'step.context' : null)}
1640
1723
  } else {
1641
1724
  console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? e.message));
1642
1725
  console.log(dim(g.eventId ? ' decided by the MetaMynd gate - your tool never ran.' : ' decided right here from your signed rules (no network call), and reported for audit - your tool never ran.'));
@@ -1759,15 +1842,20 @@ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', impo
1759
1842
  // whose owner granted it this same action would run your tool with your credentials (refused AGENT_NOT_ADMITTED instead).
1760
1843
  // Add a DID here only for another agent you mean this gateway to act for. gatewayOwnerPrincipal binds it to YOU, the owner of
1761
1844
  // its credentials: an agent listed here that another principal owns is refused anyway (GATEWAY_OWNER_MISMATCH).
1762
- const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: false, policyPublicKey: '${policyKey}', allowedAgents: ['${agentDid}'], gatewayOwnerPrincipal: '${ownerPrincipal}' });
1763
- // A guard below 0.20.0 ignores allowedAgents and serves every agent: refuse to start on one (a stale lockfile, say).
1845
+ // honourApprovals: with requireAuthorization off this gateway claims nothing — except an escalated call that presents the
1846
+ // authorization your APPROVAL minted (Dashboard -> Escalations). That one it claims as an approval: the issuer grants it
1847
+ // once, only for a hold a person approved, for this agent and these values, so the approved call runs here exactly once.
1848
+ const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: false, policyPublicKey: '${policyKey}', allowedAgents: ['${agentDid}'], gatewayOwnerPrincipal: '${ownerPrincipal}', honourApprovals: true });
1849
+ // A guard below 0.22.0 cannot enforce both pins (below 0.20.0 it ignores allowedAgents and serves every agent; 0.22.0 added gatewayOwnerPrincipal): refuse to start on one (a stale lockfile, say).
1764
1850
  if (!Array.isArray(guard.allowedAgents) || !guard.gatewayOwnerPrincipal) throw new Error('this gateway needs @metamynd/agentsafe-mcp-guard >= 0.22.0 to enforce allowedAgents and gatewayOwnerPrincipal - run npm install');
1851
+ // A guard below 0.25.0 ignores honourApprovals, and an approved escalation would stay escalated here: refuse to start on one.
1852
+ if (guard.honoursApprovals !== true) throw new Error('this gateway needs @metamynd/agentsafe-mcp-guard >= 0.25.0 to run approved escalations - run npm install');
1765
1853
 
1766
1854
  const gateway = createHttpGateway({
1767
1855
  guard,
1768
1856
  routes,
1769
1857
  // Every request this gateway runs or refuses is reported, signed with the identity above, to your Activity Log (MAGP
1770
- // 16.4). It claims nothing, so without this the issuer would never see what it executed - or that another agent tried.
1858
+ // 16.4). It claims only an approved escalation, so without this the issuer would never see what it executed - or that another agent tried.
1771
1859
  reportOutcomes: true,
1772
1860
  // A report the issuer could not take (it was down, say) is kept here and re-sent, so nothing it saw is lost. Gitignored.
1773
1861
  reportSpool: './reports.spool.jsonl',
@@ -1895,10 +1983,18 @@ Replace \`performAction()\` in \`server.mjs\` with your implementation and put a
1895
1983
 
1896
1984
  /** The README for a non-financial hosted scaffold. `withGateway` selects the two-process shape. */
1897
1985
  /** The generated README's "Change the rules" body: the dashboard path, or — for the shared sandbox agent — how to get an agent whose rules you can change. */
1898
- function changeRulesSection(sandbox) {
1986
+ function changeRulesSection(sandbox, resumable = false) {
1899
1987
  return sandbox
1900
1988
  ? `${SANDBOX_RULES_FIXED} An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.`
1901
- : `Edit the agent's SOPs in the dashboard (${SOPS_PATH}). The agent's behaviour changes live —
1989
+ : resumable
1990
+ ? `Edit the agent's SOPs in the dashboard (${SOPS_PATH}). The agent's behaviour changes live —
1991
+ no redeploy.
1992
+
1993
+ An \`escalate\` verdict is held for an owner to approve. \`npm start\` saves an escalated call and prints its id; approve it
1994
+ in the dashboard (AgentSafe → Escalations), then \`npm run resume -- <escalationId>\` runs it exactly once under the
1995
+ authorization the approval minted. In your own agent that is one call: \`await gatedTool.resume(escalationId, sameArgs)\`
1996
+ (\`@metamynd/agentsafe-guard\` ≥ 0.21.0) — pass the SAME arguments; the approval is bound to that request.`
1997
+ : `Edit the agent's SOPs in the dashboard (${SOPS_PATH}). The agent's behaviour changes live —
1902
1998
  no redeploy. An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.`;
1903
1999
  }
1904
2000
 
@@ -1981,7 +2077,7 @@ ${clonedFreshSection(daemonKey)}
1981
2077
 
1982
2078
  ## Change the rules
1983
2079
 
1984
- ${changeRulesSection(sandbox)}
2080
+ ${changeRulesSection(sandbox, !sandbox)}
1985
2081
 
1986
2082
  ## What this is not
1987
2083
 
@@ -2000,7 +2096,7 @@ Full integration guide: \`docs/integration/INTEGRATE-WITH-METAMYND.md\`.
2000
2096
  `;
2001
2097
  }
2002
2098
 
2003
- function examplePackageJson(slug, neutral = false) {
2099
+ function examplePackageJson(slug, neutral = false, resumable = false) {
2004
2100
  return JSON.stringify(
2005
2101
  {
2006
2102
  name: slug,
@@ -2010,7 +2106,7 @@ function examplePackageJson(slug, neutral = false) {
2010
2106
  // `verify` is scaffolded in because governance that lives only in a dashboard is a
2011
2107
  // thing someone has to remember to look at. As a build step it is a control: a change
2012
2108
  // that widens this agent's authority fails `npm test`.
2013
- scripts: { start: 'node index.mjs', test: neutral ? 'agentsafe-guard verify --context ./verify-context.json' : 'agentsafe-guard verify' },
2109
+ scripts: { start: 'node index.mjs', ...(resumable ? { resume: 'node index.mjs --resume' } : {}), test: neutral ? 'agentsafe-guard verify --context ./verify-context.json' : 'agentsafe-guard verify' },
2014
2110
  dependencies: { [GUARD_PKG]: GUARD_VERSION },
2015
2111
  },
2016
2112
  null,
@@ -2112,14 +2208,14 @@ ${clonedFreshSection(daemonKey)}
2112
2208
 
2113
2209
  ## Change the rules
2114
2210
 
2115
- ${changeRulesSection(sandbox)}
2211
+ ${changeRulesSection(sandbox, !sandbox)}
2116
2212
 
2117
2213
  Full integration guide: \`docs/integration/INTEGRATE-WITH-METAMYND.md\`.
2118
2214
  `;
2119
2215
  }
2120
2216
 
2121
2217
  function gitignore() {
2122
- return `node_modules/\nagent.metamynd.json\n.env\n`;
2218
+ return `node_modules/\nagent.metamynd.json\n.env\nescalations/\n`;
2123
2219
  }
2124
2220
 
2125
2221
  // ---------- the default hosted scaffold's second process: a separate tool gateway ----------
@@ -2194,7 +2290,7 @@ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', impo
2194
2290
  // The claim below does not stop that on its own: the hold belongs to the CALLER, and the caller's owner decides which
2195
2291
  // services may claim it — including registering this gateway's DID as one of theirs.
2196
2292
  const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true, policyPublicKey: '${policyKey}', allowedAgents: ['${agentDid}'], gatewayOwnerPrincipal: '${ownerPrincipal}' });
2197
- // A guard below 0.20.0 ignores allowedAgents and serves every agent: refuse to start on one (a stale lockfile, say).
2293
+ // A guard below 0.22.0 cannot enforce both pins (below 0.20.0 it ignores allowedAgents and serves every agent; 0.22.0 added gatewayOwnerPrincipal): refuse to start on one (a stale lockfile, say).
2198
2294
  if (!Array.isArray(guard.allowedAgents) || !guard.gatewayOwnerPrincipal) throw new Error('this gateway needs @metamynd/agentsafe-mcp-guard >= 0.22.0 to enforce allowedAgents and gatewayOwnerPrincipal - run npm install');
2199
2295
 
2200
2296
  const gateway = createHttpGateway({
@@ -2388,17 +2484,33 @@ on purpose: an agent must never be able to release or lower-settle a hold it aut
2388
2484
  provisioning, \`npx create-metamynd-agent\` registered that DID as a trusted counterparty for you
2389
2485
  (\`POST /policy/counterparties\`). A **MAINNET** hold is *always* registered-only, regardless of your
2390
2486
  registry (MAGP §8.7.10) — a testnet hold stays open unless you have registered anything at all.
2391
- Manage the registry at \`/dashboard/counterparties\`, or:
2487
+ Manage the registry at \`/dashboard/counterparties\` (it walks you through the proof below), or
2488
+ re-register this gateway yourself — after removing it from the registry, say.
2489
+
2490
+ Registration needs **proof of control** (MAGP §8.7.6): you ask for a challenge naming you, this
2491
+ gateway signs it with its own key from \`service.metamynd.json\`, and you register with that
2492
+ signature. A DID is never registered on its text alone, so nobody can register a gateway they do
2493
+ not hold the key for. Run this from this directory (bash; \`TOKEN\` is your owner access token):
2392
2494
 
2393
2495
  \`\`\`bash
2394
- curl -X POST $MAGP_API/policy/counterparties \\
2395
- -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \\
2396
- -d '{ "did": "'"$(node -e "console.log(JSON.parse(require('fs').readFileSync('./service.metamynd.json')).serviceDid)")"'", "confirmEnforcementChange": true }'
2496
+ MAGP_API=https://metamynd.ai/api/v1 TOKEN=<owner access token> node --input-type=module -e "
2497
+ import { readFileSync } from 'node:fs'; import crypto from 'node:crypto';
2498
+ const { serviceDid, serviceKey } = JSON.parse(readFileSync('./service.metamynd.json', 'utf8'));
2499
+ const api = process.env.MAGP_API, headers = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + process.env.TOKEN };
2500
+ const post = (path, body) => fetch(api + path, { method: 'POST', headers, body: JSON.stringify(body) });
2501
+ const ch = (await (await post('/policy/counterparties/challenge', { did: serviceDid, purpose: 'claim' })).json()).data;
2502
+ const key = crypto.createPrivateKey({ key: Buffer.from(serviceKey, 'hex'), format: 'der', type: 'pkcs8' });
2503
+ const signature = crypto.sign(null, Buffer.from(ch.message, 'utf8'), key).toString('hex');
2504
+ const r = await post('/policy/counterparties', { did: serviceDid, purpose: 'claim', proof: { challengeToken: ch.challengeToken, signature }, confirmEnforcementChange: true });
2505
+ console.log(r.status, await r.text());
2506
+ "
2397
2507
  \`\`\`
2398
2508
 
2399
- \`confirmEnforcementChange: true\` is required only for your account's FIRST-EVER registration —
2400
- it switches EVERY one of your holds (not just this agent's) from open to registered-only, so the
2401
- API refuses a first registration without it.
2509
+ The gateway's own \`guard.acceptCounterpartyChallenge(message)\` (\`@metamynd/agentsafe-mcp-guard\`
2510
+ ≥ 0.23.0) signs the same thing, if you would rather expose it from \`server.mjs\`.
2511
+ \`confirmEnforcementChange: true\` matters only for your account's FIRST-EVER registration — it
2512
+ switches EVERY one of your holds (not just this agent's) from open to registered-only, so the API
2513
+ refuses a first registration without it.
2402
2514
  - **Amount unknown.** \`amount-unknown\` (\`@metamynd/agentsafe-mcp-guard\` ≥ 0.3.0) blocks a
2403
2515
  platform tool by default when its raw bytes or a nested payload hide the amount from a naive
2404
2516
  spend cap — AND this agent's OWN starter SOP (see \`agent.metamynd.json\` /
@@ -2479,7 +2591,8 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
2479
2591
  : exampleIndexNoGateway(scope, perTxnMax, currency, paymentMerchant, !!sandbox),
2480
2592
  force,
2481
2593
  );
2482
- writeFileSafe(outDir, 'package.json', examplePackageJson(slug, neutral), force);
2594
+ // Resumable = the index carries the resume path (resumeBlock): every agent but the shared sandbox, where nobody can approve.
2595
+ writeFileSafe(outDir, 'package.json', examplePackageJson(slug, neutral, !sandbox), force);
2483
2596
  writeFileSafe(outDir, '.gitignore', gitignore(), force);
2484
2597
  writeFileSafe(
2485
2598
  outDir,
@@ -2792,7 +2905,7 @@ const guard = createMcpGuard({
2792
2905
  return { subject: AGENT_DID, ownerPrincipal: HARNESS_OWNER, ...bundleFromRules(rules) };
2793
2906
  },
2794
2907
  });
2795
- // A guard below 0.20.0 ignores allowedAgents and serves every agent: refuse to start on one (a stale lockfile, say).
2908
+ // A guard below 0.22.0 cannot enforce both pins (below 0.20.0 it ignores allowedAgents and serves every agent; 0.22.0 added gatewayOwnerPrincipal): refuse to start on one (a stale lockfile, say).
2796
2909
  if (!Array.isArray(guard.allowedAgents) || !guard.gatewayOwnerPrincipal) throw new Error('this gateway needs @metamynd/agentsafe-mcp-guard >= 0.22.0 to enforce allowedAgents and gatewayOwnerPrincipal - run npm install');
2797
2910
 
2798
2911
  // One protected route per gated action in index.mjs. This gateway IS the tool, not a proxy in
@@ -4152,6 +4265,13 @@ async function main() {
4152
4265
  Array.isArray(fileConfig?.merchants) ? fileConfig.merchants.join(',') : '',
4153
4266
  );
4154
4267
  const merchants = String(merchantsRaw).split(',').map((s) => s.trim()).filter(Boolean);
4268
+ // Risk the agent cannot lower (MAGP 6.4.3): the owner's floor, and the amount share of the cap that is reviewed.
4269
+ const riskTier = typeof args['risk-tier'] === 'string' ? args['risk-tier'].trim().toLowerCase() : undefined;
4270
+ if (riskTier !== undefined && !['low', 'medium', 'high', 'critical'].includes(riskTier)) fail('--risk-tier must be low, medium, high or critical');
4271
+ const amountReviewRaw = typeof args['amount-review'] === 'string' ? args['amount-review'].trim().toLowerCase() : undefined;
4272
+ const amountShare = amountReviewRaw === undefined ? undefined : amountReviewRaw === 'off' ? false : Number(amountReviewRaw);
4273
+ if (typeof amountShare === 'number' && !(amountShare > 0 && amountShare <= 1)) fail('--amount-review must be a fraction in (0, 1], or "off"');
4274
+ const riskFields = { ...(riskTier ? { riskTier } : {}), ...(amountShare !== undefined && financial ? { riskSignals: { amountShare } } : {}) };
4155
4275
 
4156
4276
  // BYOK: --byok generates a keypair on THIS machine (MetaMynd never sees the private key) —
4157
4277
  // either locally in this process (default) or, opt-in, via an already-running agentsafe-signer
@@ -4222,6 +4342,7 @@ async function main() {
4222
4342
  network: 'testnet',
4223
4343
  ...(financial ? { currency, maxAmount, perTxnMax } : {}),
4224
4344
  merchants,
4345
+ ...riskFields,
4225
4346
  ...(publicKey ? { publicKey } : {}),
4226
4347
  ...sopFields,
4227
4348
  };
@@ -4230,6 +4351,11 @@ async function main() {
4230
4351
  if (!config?.agentDid) fail('Provisioning did not return a config with an agentDid.');
4231
4352
  console.log(` ${c.green('✓')} agent DID ${c.b(config.agentDid)}`);
4232
4353
  if (config.standards?.length) console.log(` ${c.green('✓')} enforced Standards: ${config.standards.join(', ')}`);
4354
+ if (financial && merchants.length === 0) {
4355
+ console.log(c.dim(' no --merchants list: the FIRST payment to each merchant is held for your review (a risk the agent cannot'));
4356
+ console.log(c.dim(' lower). Pass --merchants a,b to pre-approve them, or register them as payees in the dashboard.'));
4357
+ }
4358
+ if (riskTier) console.log(` ${c.green('✓')} risk floor: ${riskTier} (the agent's own claim can raise it, never lower it)`);
4233
4359
 
4234
4360
  // 3b. BYOK: prove control of the key (verify-key), else the gate blocks with AGENT_KEY_UNVERIFIED.
4235
4361
  if (daemonPublicKeyHex) {
@@ -4276,13 +4402,18 @@ async function main() {
4276
4402
  const gatewayDid = buildDidKey(rawPublicKeyFromSpkiHex(gwKeypair.publicKeyHex));
4277
4403
  gatewayIdentity = { did: gatewayDid, keyHex: gwKeypair.privateKeyHex };
4278
4404
  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'}) …`));
4279
- const reg = await registerGatewayCounterparty(base, token, gatewayDid, `${scope}-gateway`, financial ? 'claim' : 'report', gwKeypair.privateKeyHex);
4405
+ const reg = await registerGatewayCounterparty(base, token, gatewayDid, `${scope}-gateway`, financial ? 'claim' : 'report', gwKeypair.privateKeyHex, financial ? null : [config.agentDid]);
4280
4406
  if (reg.ok) {
4281
4407
  console.log(` ${c.green('✓')} gateway DID ${c.b(gatewayDid)} — ${reg.message}`);
4282
4408
  } else {
4283
4409
  console.log(` ${c.yellow('!')} could not register the gateway as a trusted counterparty: ${reg.message}`);
4284
- console.log(c.dim(` it still works on testnet (open by default with no registry entries); a MAINNET hold cannot be`));
4285
- console.log(c.dim(` claimed until it is registered — POST ${base}/policy/counterparties { "did": "${gatewayDid}", "confirmEnforcementChange": true }`));
4410
+ console.log(c.dim(financial
4411
+ ? ` it still works on testnet (open by default with no registry entries); a MAINNET hold cannot be`
4412
+ : ` it still enforces; only its refusal reports cannot reach your Activity Log until it is`));
4413
+ console.log(c.dim(financial
4414
+ ? ` claimed until it is registered — gateway/README.md ("Who may claim this agent's holds") shows how, with the`
4415
+ : ` registered — at /dashboard/counterparties, with the`));
4416
+ console.log(c.dim(` proof-of-control step registration requires, for ${gatewayDid}`));
4286
4417
  }
4287
4418
  }
4288
4419
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-metamynd-agent",
3
- "version": "0.14.18",
3
+ "version": "0.14.24",
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": {