create-metamynd-agent 0.14.4 → 0.14.6

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 (3) hide show
  1. package/README.md +6 -3
  2. package/index.mjs +87 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -433,9 +433,12 @@ signed value only and ignores one in the context. Scaffolds pin `@metamynd/agent
433
433
 
434
434
  The starter rules include a high-risk review. A request that sends **no** `riskLevel` (or an unrecognised one)
435
435
  is no longer waved through: it is **escalated** (`CONTEXT_UNVERIFIABLE`) for a human, because an agent that omits
436
- its risk is indistinguishable from one hiding it. The scaffolded financial examples send `riskLevel` (`'low'`
437
- unless you say otherwise) — that default is a placeholder, not an assessment — and the neutral template passes
438
- your own `args` as the context, so include it there. A real integration should send an honest one — or, better, not depend on the agent at all: a mandate
436
+ its risk is indistinguishable from one hiding it. The scaffolded examples pass the tool call's own `riskLevel`
437
+ through **unchanged** — there is no default, so a call that does not say is sent for review rather than treated as
438
+ `'low'` (the demo steps each state their risk explicitly). The neutral template passes your own `args` as the
439
+ context, so include it there. Either way `riskLevel` is **the agent's own claim**: it travels in the request
440
+ context, and even when the agent's key signs that context, the signature proves who said it, not that it is true —
441
+ nothing checks the value unless your owner configures provenance for it. A real integration should send an honest one — or, better, not depend on the agent at all: a mandate
439
442
  permission can carry an owner-set `riskTier` that no claim can lower, and a gateway route can derive the risk
440
443
  itself with `route.trustedContext` (`@metamynd/agentsafe-http-gateway` 0.10.0). See MAGP §6.3.
441
444
 
package/index.mjs CHANGED
@@ -374,6 +374,16 @@ function loadConfigFile(path) {
374
374
  return json;
375
375
  }
376
376
 
377
+ // Written into every financial scaffold's README. The generated code passes the tool call's riskLevel through
378
+ // unchanged (no `?? 'low'`): a missing one must reach the gate as missing, where it escalates by design.
379
+ const RISK_LEVEL_NOTE = `## Risk level: the agent's own claim
380
+
381
+ \`riskLevel\` is **the agent's own claim**. It travels in the request's context: the agent's key may sign that context,
382
+ which proves who said it, not that it is true, and nothing checks the value unless your owner configures provenance for it (an owner-set \`riskTier\` on the mandate permission, or a
383
+ gateway route's \`trustedContext\`). \`index.mjs\` passes the tool call's own \`riskLevel\` through unchanged —
384
+ there is **no default**. A call that does not state one (or states one the gate does not recognise) is **sent
385
+ for review** (\`CONTEXT_UNVERIFIABLE\`), never treated as \`low\`. The demo steps each state their risk explicitly.`;
386
+
377
387
  // A rule needs `when.predicate` (the atom) and `then` (the decision the gate should return
378
388
  // when it fires) — everything else is optional sugar. See backend's atom-catalog.ts for the
379
389
  // full predicate list (amount-over, risk-at-or-above, jurisdiction-not-allowed, ...).
@@ -476,10 +486,27 @@ function defaultNeutralDemo() {
476
486
  return demo;
477
487
  }
478
488
 
489
+ /**
490
+ * Where an agent's rules are edited in the dashboard. SOPs belong to the legal entity, so the page is
491
+ * VeriFAI → Legal Entities (/dashboard/organization), SOPs tab; the AgentSafe sidebar group links the same
492
+ * tab as "SOPs". This said "Legal Entity → SOPs", a path no menu had (2026-10-02 eval, L-1).
493
+ */
494
+ const SOPS_PATH = 'Dashboard → VeriFAI → Legal Entities → SOPs tab (also linked as AgentSafe → SOPs)';
495
+ /** The same path for the generated demo's console output, which stays ASCII. */
496
+ const SOPS_PATH_ASCII = 'Dashboard -> VeriFAI -> Legal Entities -> SOPs';
497
+ /**
498
+ * A --sandbox scaffold runs as the SHARED sandbox agent, which no user has dashboard access to, so "change
499
+ * the rules in the dashboard" sent them somewhere they could not go (L-7). Say how to get an agent whose
500
+ * rules they CAN change instead.
501
+ */
502
+ const SANDBOX_RULES_FIXED = "This is the shared sandbox agent: its rules are fixed and nobody can edit them. To change the rules, provision your own agent — re-run without --sandbox (it signs you up and creates an agent whose SOPs you own, edited at " + SOPS_PATH + "), or use --harness for a free local agent whose rules you edit by hand.";
503
+
479
504
  /** A --config file's rules cannot reach an agent the caller does not provision themselves: say so instead of implying they apply. */
480
- function warnRulesNotApplied(fileConfig, where) {
505
+ function warnRulesNotApplied(fileConfig, where, { shared = false } = {}) {
481
506
  if (policyMolecules(fileConfig).length === 0) return;
482
- console.log(` ${c.yellow('!')} the rules in ${c.b('the config file')} are not applied to ${where} — its rules are the platform's defaults. Set your own in the dashboard (Legal Entity → SOPs), or provision your own agent (drop this flag) to author them here.`);
507
+ console.log(shared
508
+ ? ` ${c.yellow('!')} the rules in ${c.b('the config file')} are not applied to ${where} — its rules are the platform's defaults, and nobody can change them. Provision your own agent (drop this flag) to author them here.`
509
+ : ` ${c.yellow('!')} the rules in ${c.b('the config file')} are not applied to ${where} — its rules are the platform's defaults. Its owner sets them in the dashboard (${SOPS_PATH}), or provision your own agent (drop this flag) to author them here.`);
483
510
  }
484
511
 
485
512
  /**
@@ -1032,7 +1059,7 @@ function keepConfigAndFail(outDir, config, why) {
1032
1059
  * it — the same shape of gap --harness's README documents. See exampleIndex() below, which is
1033
1060
  * what the real (non-sandbox) flow scaffolds by default instead.
1034
1061
  */
1035
- function exampleIndexNoGateway(scope, perTxnMax, currency, merchant) {
1062
+ function exampleIndexNoGateway(scope, perTxnMax, currency, merchant, sandbox = false) {
1036
1063
  const under = Math.max(1, Math.round(perTxnMax * 0.5));
1037
1064
  const over = Math.round(perTxnMax + 100);
1038
1065
  return `// index.mjs — your agent, governed by MetaMynd/AgentSafe.
@@ -1061,7 +1088,10 @@ const gatedBookFlight = guard.guardTool(
1061
1088
  amount: a.amount,
1062
1089
  currency: '${currency}',
1063
1090
  merchant: a.merchant,
1064
- context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
1091
+ // riskLevel is the agent's OWN claim: signing it proves who said it, not that it is true, and nothing checks it unless
1092
+ // the owner configures provenance. Passed through as-is: when the agent does not say, it stays absent and the gate sends
1093
+ // the action for review — never 'low'.
1094
+ context: { tool: 'book-flight', riskLevel: a.riskLevel },
1065
1095
  }),
1066
1096
  );
1067
1097
 
@@ -1176,8 +1206,12 @@ console.log(bold(' Without MetaMynd, you can be bypassed.') + ' bookFlight() ru
1176
1206
  console.log(dim(' call it directly instead of gatedBookFlight and nothing above stops you.'));
1177
1207
  console.log(dim(' Re-scaffold without --sandbox/--no-gateway for the default shape, which does.'));
1178
1208
  console.log('');
1179
- console.log(' Change the cap in the dashboard (Legal Entity -> SOPs) and run again.');
1180
- console.log(dim(' The outcome changes. This file does not. That is the point.'));
1209
+ ${sandbox ? `console.log(' This is the SHARED sandbox agent: its rules are fixed and nobody can edit them.');
1210
+ console.log(dim(' To change the cap, provision your own agent: re-run create-metamynd-agent'));
1211
+ console.log(dim(' without --sandbox (it signs you up), then change the cap in your dashboard'));
1212
+ console.log(dim(' (${SOPS_PATH_ASCII}) and run again.'));
1213
+ console.log(dim(' The outcome changes. Your code does not. That is the point.'));` : `console.log(' Change the cap in the dashboard (${SOPS_PATH_ASCII}) and run again.');
1214
+ console.log(dim(' The outcome changes. This file does not. That is the point.'));`}
1181
1215
  console.log('');
1182
1216
  `;
1183
1217
  }
@@ -1222,7 +1256,8 @@ async function bookFlightViaGateway(args, decision) {
1222
1256
  amount: args.amount,
1223
1257
  currency: args.currency ?? '${currency}',
1224
1258
  merchant: args.merchant,
1225
- context: { tool: 'book-flight', riskLevel: args.riskLevel ?? 'low' },
1259
+ // Passed through as-is (never defaulted): a missing riskLevel must reach the gateway as missing.
1260
+ context: { tool: 'book-flight', riskLevel: args.riskLevel },
1226
1261
  payload,
1227
1262
  });
1228
1263
  signed.authorizationId = decision?.authorizationId;
@@ -1250,7 +1285,10 @@ const gatedBookFlight = guard.guardTool(
1250
1285
  amount: a.amount,
1251
1286
  currency: a.currency ?? '${currency}',
1252
1287
  merchant: a.merchant,
1253
- context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
1288
+ // riskLevel is the agent's OWN claim: signing it proves who said it, not that it is true, and nothing checks it unless
1289
+ // the owner configures provenance. Passed through as-is: when the agent does not say, it stays absent and the gate sends
1290
+ // the action for review — never 'low'.
1291
+ context: { tool: 'book-flight', riskLevel: a.riskLevel },
1254
1292
  // The authorization is bound to the SAME body bookFlightViaGateway() sends, so the gate records what this agent
1255
1293
  // signed and the gateway can prove it is running exactly that. Keep the two in step if you add a field.
1256
1294
  payload: { amount: a.amount, merchant: a.merchant, currency: a.currency ?? '${currency}' },
@@ -1373,7 +1411,7 @@ console.log('');
1373
1411
  console.log(bold(' With MetaMynd, you can\\'t be bypassed.') + ' ./gateway is why - it independently');
1374
1412
  console.log(dim(' re-verified step 1 before running it, and holds the tool this file never can.'));
1375
1413
  console.log('');
1376
- console.log(' Change the cap in the dashboard (Legal Entity -> SOPs) and run again.');
1414
+ console.log(' Change the cap in the dashboard (${SOPS_PATH_ASCII}) and run again.');
1377
1415
  console.log(dim(' The outcome changes. This file does not. That is the point.'));
1378
1416
  console.log('');
1379
1417
  `;
@@ -1406,7 +1444,7 @@ If a committed \`package-lock.json\` pins an older \`${GUARD_PKG}\` than this pr
1406
1444
  * with `gatewayPort` the tool lives in a separate process (./gateway, the real enforcement
1407
1445
  * boundary); without it (--no-gateway) the tool is a local function in this process.
1408
1446
  */
1409
- function exampleIndexNeutral({ scope, gatewayPort, demo, merchant }) {
1447
+ function exampleIndexNeutral({ scope, gatewayPort, demo, merchant, sandbox = false }) {
1410
1448
  const withGateway = gatewayPort != null;
1411
1449
  const steps = [
1412
1450
  ...demo.cases,
@@ -1553,8 +1591,12 @@ console.log(bold(' Without MetaMynd, you can be bypassed.') + ' performAction()
1553
1591
  console.log(dim(' call it directly instead of gatedAction and nothing above stops you.'));
1554
1592
  console.log(dim(' Re-scaffold without --no-gateway for the default shape, which closes that.'));`}
1555
1593
  console.log('');
1556
- console.log(' Change your rules in the dashboard (Legal Entity -> SOPs) and run again.');
1557
- console.log(dim(' The outcome changes. This file does not. That is the point.'));
1594
+ ${sandbox ? `console.log(' This is the SHARED sandbox agent: its rules are fixed and nobody can edit them.');
1595
+ console.log(dim(' To change them, provision your own agent: re-run create-metamynd-agent'));
1596
+ console.log(dim(' without --sandbox (it signs you up), then change its rules in your dashboard'));
1597
+ console.log(dim(' (${SOPS_PATH_ASCII}) and run again.'));
1598
+ console.log(dim(' The outcome changes. Your code does not. That is the point.'));` : `console.log(' Change your rules in the dashboard (${SOPS_PATH_ASCII}) and run again.');
1599
+ console.log(dim(' The outcome changes. This file does not. That is the point.'));`}
1558
1600
  console.log('');
1559
1601
  `;
1560
1602
  }
@@ -1733,7 +1775,15 @@ Replace \`performAction()\` in \`server.mjs\` with your implementation and put a
1733
1775
  }
1734
1776
 
1735
1777
  /** The README for a non-financial hosted scaffold. `withGateway` selects the two-process shape. */
1736
- function exampleReadmeNeutral(slug, scope, withGateway, gatewayPort, daemonKey, demo) {
1778
+ /** 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. */
1779
+ function changeRulesSection(sandbox) {
1780
+ return sandbox
1781
+ ? `${SANDBOX_RULES_FIXED} An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.`
1782
+ : `Edit the agent's SOPs in the dashboard (${SOPS_PATH}). The agent's behaviour changes live —
1783
+ no redeploy. An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.`;
1784
+ }
1785
+
1786
+ function exampleReadmeNeutral(slug, scope, withGateway, gatewayPort, daemonKey, demo, sandbox = false) {
1737
1787
  const configLine = daemonKey
1738
1788
  ? `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
1739
1789
  **Holds no secret key.** Signing goes through your already-running agentsafe-signer daemon instead.`
@@ -1812,8 +1862,7 @@ ${clonedFreshSection(daemonKey)}
1812
1862
 
1813
1863
  ## Change the rules
1814
1864
 
1815
- Edit the agent's SOPs in the dashboard (Legal Entity → SOPs). The agent's behaviour changes live —
1816
- no redeploy. An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.
1865
+ ${changeRulesSection(sandbox)}
1817
1866
 
1818
1867
  ## What this is not
1819
1868
 
@@ -1850,7 +1899,7 @@ function examplePackageJson(slug, neutral = false) {
1850
1899
  ) + '\n';
1851
1900
  }
1852
1901
 
1853
- function exampleReadme(slug, scope, withGateway, gatewayPort, daemonKey = false) {
1902
+ function exampleReadme(slug, scope, withGateway, gatewayPort, daemonKey = false, sandbox = false) {
1854
1903
  const configFileLine = daemonKey
1855
1904
  ? `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
1856
1905
  **Holds no secret key.** Signing goes through your already-running agentsafe-signer daemon
@@ -1877,6 +1926,8 @@ You should see an ALLOW (fulfilled by \`./gateway\`), a BLOCK (over the per-tran
1877
1926
  an ESCALATE (high risk). The BLOCK and ESCALATE never reach the gateway at all — this file's own
1878
1927
  \`guard.guardTool()\` refuses them first. Only the ALLOW crosses into the other process.
1879
1928
 
1929
+ ${RISK_LEVEL_NOTE}
1930
+
1880
1931
  ## Files
1881
1932
 
1882
1933
  ${configFileLine}
@@ -1898,6 +1949,8 @@ npm start
1898
1949
 
1899
1950
  You should see an ALLOW, a BLOCK (over the per-transaction cap), and an ESCALATE (high risk).
1900
1951
 
1952
+ ${RISK_LEVEL_NOTE}
1953
+
1901
1954
  ## Files
1902
1955
 
1903
1956
  ${configFileLine}
@@ -1940,8 +1993,7 @@ ${clonedFreshSection(daemonKey)}
1940
1993
 
1941
1994
  ## Change the rules
1942
1995
 
1943
- Edit the agent's SOPs in the dashboard (Legal Entity → SOPs). The agent's behaviour changes live —
1944
- no redeploy. An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.
1996
+ ${changeRulesSection(sandbox)}
1945
1997
 
1946
1998
  Full integration guide: \`docs/integration/INTEGRATE-WITH-METAMYND.md\`.
1947
1999
  `;
@@ -2282,10 +2334,10 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
2282
2334
  outDir,
2283
2335
  'index.mjs',
2284
2336
  neutral
2285
- ? exampleIndexNeutral({ scope, gatewayPort: withGateway ? gatewayPort : null, demo, merchant })
2337
+ ? exampleIndexNeutral({ scope, gatewayPort: withGateway ? gatewayPort : null, demo, merchant, sandbox: !!sandbox })
2286
2338
  : withGateway
2287
2339
  ? exampleIndex(scope, perTxnMax, gatewayPort, currency, paymentMerchant)
2288
- : exampleIndexNoGateway(scope, perTxnMax, currency, paymentMerchant),
2340
+ : exampleIndexNoGateway(scope, perTxnMax, currency, paymentMerchant, !!sandbox),
2289
2341
  force,
2290
2342
  );
2291
2343
  writeFileSafe(outDir, 'package.json', examplePackageJson(slug, neutral), force);
@@ -2294,8 +2346,8 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
2294
2346
  outDir,
2295
2347
  'README.md',
2296
2348
  neutral
2297
- ? exampleReadmeNeutral(slug, scope, withGateway, gatewayPort, config.keyProvider === 'daemon', demo)
2298
- : exampleReadme(slug, scope, withGateway, gatewayPort, config.keyProvider === 'daemon'),
2349
+ ? exampleReadmeNeutral(slug, scope, withGateway, gatewayPort, config.keyProvider === 'daemon', demo, !!sandbox)
2350
+ : exampleReadme(slug, scope, withGateway, gatewayPort, config.keyProvider === 'daemon', !!sandbox),
2299
2351
  force,
2300
2352
  );
2301
2353
  // `npm test` (agentsafe-guard verify --context) needs the request inputs of a compliant request:
@@ -2366,7 +2418,9 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
2366
2418
  console.log(c.dim(' · BLOCK (the agent asking to raise its OWN limit)\n'));
2367
2419
  }
2368
2420
  console.log(c.cyan(` npm test`) + c.dim(' → assert it CANNOT exceed its mandate. Put this in CI.\n'));
2369
- console.log(c.dim(` Change the rules any time in the dashboard (Legal Entity → SOPs) — no redeploy.\n`));
2421
+ console.log(c.dim(sandbox
2422
+ ? ` ${SANDBOX_RULES_FIXED}\n`
2423
+ : ` Change the rules any time in the dashboard (${SOPS_PATH}) — no redeploy.\n`));
2370
2424
  }
2371
2425
 
2372
2426
  /** --sandbox: no login, no KYB — fetch the shared sandbox agent config and scaffold. */
@@ -2381,7 +2435,7 @@ async function runSandbox(args) {
2381
2435
  const fileConfig = typeof args.config === 'string' ? loadConfigFile(args.config) : null;
2382
2436
  if (fileConfig) console.log(` ${c.green('✓')} loaded policy config ${c.dim(args.config)}`);
2383
2437
  const { financial } = announceFinancial(args, fileConfig);
2384
- warnRulesNotApplied(fileConfig, 'the shared sandbox agent');
2438
+ warnRulesNotApplied(fileConfig, 'the shared sandbox agent', { shared: true });
2385
2439
  if (!financial && typeof fileConfig?.scope === 'string' && fileConfig.scope !== 'perform-action') {
2386
2440
  console.log(` ${c.yellow('!')} the scope "${fileConfig.scope}" in the config file is not used: the shared sandbox agent's scope is "perform-action".`);
2387
2441
  }
@@ -3095,7 +3149,8 @@ async function callGateway(path, action, args) {
3095
3149
  // The COMPLETE body the tool receives, signed as a payload (MAGP 8.3.9): the eight signed fields cover amount and merchant
3096
3150
  // only, and this gateway refuses to run the tool on a body that is not exactly the one signed here.
3097
3151
  const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
3098
- const signed = await guard.buildSignedRequest({ action, amount: args.amount, currency: args.currency ?? '${currency}', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' }, payload });
3152
+ // riskLevel is passed through as-is (never defaulted): a missing one must reach the gateway as missing.
3153
+ const signed = await guard.buildSignedRequest({ action, amount: args.amount, currency: args.currency ?? '${currency}', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel }, payload });
3099
3154
  const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: JSON.stringify(payload) });
3100
3155
  const body = await res.json().catch(() => null);
3101
3156
  if (!res.ok) {
@@ -3120,7 +3175,10 @@ const gatedBookFlight = guard.guardToolLocal(
3120
3175
  amount: a.amount,
3121
3176
  currency: a.currency ?? '${currency}',
3122
3177
  merchant: a.merchant,
3123
- context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
3178
+ // riskLevel is the agent's OWN claim: signing it proves who said it, not that it is true, and nothing checks it unless
3179
+ // the owner configures provenance. Passed through as-is: when the agent does not say, it stays absent and the gate sends
3180
+ // the action for review — never 'low'.
3181
+ context: { tool: 'book-flight', riskLevel: a.riskLevel },
3124
3182
  }),
3125
3183
  getBundle,
3126
3184
  );
@@ -3474,7 +3532,9 @@ action outside the mandate entirely. Every step says what it expects and flags a
3474
3532
  : `None of your rules could be staged in the demo (see below), so it shows only a BLOCK for an
3475
3533
  action outside the mandate. Your rules are still enforced; see \`metamynd-rules.json\`.`)
3476
3534
  : `You should see an ALLOW, a BLOCK (over the per-transaction cap), an ESCALATE (high risk —
3477
- open the dashboard to approve it), and a BLOCK (an action outside the mandate entirely).`}
3535
+ open the dashboard to approve it), and a BLOCK (an action outside the mandate entirely).
3536
+
3537
+ ${RISK_LEVEL_NOTE}`}
3478
3538
 
3479
3539
  ## Files
3480
3540
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-metamynd-agent",
3
- "version": "0.14.4",
3
+ "version": "0.14.6",
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": {