create-metamynd-agent 0.14.10 → 0.14.18

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 +169 -38
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -61,7 +61,11 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
61
61
  // 0.18.0 exports keepAliveFetch. No template change.
62
62
  // 0.18.1 parks a local-first ESCALATE at the gate. Required: every scaffold's demo says "approve it in the dashboard and
63
63
  // the action resumes", and below this the escalation never reached the owner's queue (decided in-process, no escalationId).
64
- const GUARD_VERSION = '^0.18.1';
64
+ // 0.19.0: a handshake signs only a plain-token nonce (a malicious Service could otherwise obtain this agent's signature on an
65
+ // authorize request of its choosing, MAGP §8.2). Required for every scaffolded agent.
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
+ // counterparty the owner registered, or (an open testnet owner) anyone (MAGP §8.7.4).
68
+ const GUARD_VERSION = '^0.20.0';
65
69
  /** The harness entry point's config load, shared by both harness templates: a fresh clone has no
66
70
  * agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
67
71
  function harnessConfigLoad() {
@@ -128,7 +132,20 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
128
132
  // 0.19.0: a request the gateway's own policy escalates, carrying an authorization a PERSON approved, is executed (once,
129
133
  // with every claim binding) instead of refused RISK_REVIEW (MAGP §8.7.18). Required: the scaffold tells the developer
130
134
  // "approve it in the dashboard and the action resumes" — below this the gateway refused exactly that.
131
- const MCP_GUARD_VERSION = '^0.19.0';
135
+ // 0.20.0: `allowedAgents` pins a Service to the agents it acts for (MAGP §16.3) — refused AGENT_NOT_ADMITTED otherwise.
136
+ // Required: every scaffolded gateway now sets it, and an older guard would ignore the option and serve ANY agent whose
137
+ // own owner granted it the same action, with this owner's credentials (XT-1, pre-beta evaluation 2026-10-03).
138
+ // 0.21.0: guard.reportOutcome(). Required: the scaffolded gateways report what they ran or refused (MAGP §16.4), and
139
+ // agentsafe-http-gateway's reportOutcomes needs it.
140
+ // 0.22.0: allowedAgents is required, gatewayOwnerPrincipal binds the gateway to the owner of its credentials (an admitted
141
+ // agent of any other owner: GATEWAY_OWNER_MISMATCH), per-route admission (CREDENTIAL_PROFILE_NOT_PERMITTED), the refusal is
142
+ // AGENT_NOT_ADMITTED, and a handshake signs only a plain-token nonce. Required: every scaffolded gateway sets the owner.
143
+ // 0.23.0: acceptCounterpartyChallenge() — a Service signs its acceptance of an owner's registration (MAGP §8.7.6). No
144
+ // template change; required with GATEWAY_VERSION below so the gateway never resolves a second guard of its own.
145
+ // 0.24.0: reportOutcome() signs a reportId and an occurrence count (aggregated refusals, a durable spool).
146
+ const MCP_GUARD_VERSION = '^0.24.0';
147
+ /** A DID as it may appear inside a generated string literal (the gateway's allowedAgents pin): no quote, backslash or space. */
148
+ const SAFE_DID = /^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/;
132
149
  const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
133
150
  // 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
134
151
  // never scaffold a range that could resolve below it.
@@ -151,7 +168,13 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
151
168
  // 0.17.0: depends on agentsafe-mcp-guard ^0.18.0 and exports keepAliveFetch. No template change.
152
169
  // 0.18.0: depends on agentsafe-mcp-guard ^0.19.0 (an approved escalation executes). Required with MCP_GUARD_VERSION
153
170
  // above, so the gateway never resolves a second, older guard of its own.
154
- const GATEWAY_VERSION = '^0.18.0';
171
+ // 0.19.0: depends on agentsafe-mcp-guard ^0.20.0 (allowedAgents) and reads AGENTSAFE_ALLOWED_AGENTS. Required with
172
+ // MCP_GUARD_VERSION above, so the gateway never resolves a second, older guard of its own.
173
+ // 0.20.0: `reportOutcomes`, which both hosted gateway templates set; depends on agentsafe-mcp-guard ^0.21.0.
174
+ // 0.21.0: per-route allowedAgents; depends on agentsafe-mcp-guard ^0.22.0.
175
+ // 0.22.0: depends on agentsafe-mcp-guard ^0.23.0.
176
+ // 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';
155
178
 
156
179
  /** Appended to every scaffolded gateway server: give hold settlements still running a bounded moment on shutdown. */
157
180
  const DRAIN_ON_SHUTDOWN = `
@@ -995,12 +1018,27 @@ async function resolveHostedRulePack(base, key, { financial, perTxnMax, maxAmoun
995
1018
  * dashboard registration shows its own dialog before setting the flag); see the printed message
996
1019
  * either way.
997
1020
  */
998
- async function registerGatewayCounterparty(base, token, did, label) {
1021
+ async function registerGatewayCounterparty(base, token, did, label, purpose = 'claim', privateKeyHex = null) {
999
1022
  try {
1023
+ // The gateway accepts its own registration (MAGP §8.7.6): ask for a challenge naming this owner, sign it with the gateway's
1024
+ // key (generated here, so this process holds it), and register with that proof. A DID is never registered on its text.
1025
+ let proof;
1026
+ if (privateKeyHex) {
1027
+ const ch = await fetch(`${base}/policy/counterparties/challenge`, {
1028
+ method: 'POST',
1029
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
1030
+ body: JSON.stringify({ did, purpose }),
1031
+ });
1032
+ const chJson = await ch.json().catch(() => null);
1033
+ if (!ch.ok || !chJson?.data?.message) return { ok: false, message: chJson?.message ?? `challenge HTTP ${ch.status}` };
1034
+ proof = { challengeToken: chJson.data.challengeToken, signature: signChallengeHex(privateKeyHex, chJson.data.message) };
1035
+ }
1000
1036
  const res = await fetch(`${base}/policy/counterparties`, {
1001
1037
  method: 'POST',
1002
1038
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
1003
- body: JSON.stringify({ did, label, confirmEnforcementChange: true }),
1039
+ // `report` (a non-financial gateway, which never claims): it may only report to this owner, and it changes nothing about
1040
+ // 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 } : {}) }),
1004
1042
  });
1005
1043
  const json = await res.json().catch(() => null);
1006
1044
  if (!res.ok || json?.success === false) return { ok: false, message: json?.message ?? `HTTP ${res.status}` };
@@ -1033,6 +1071,30 @@ async function ensurePolicyKey(config, base) {
1033
1071
  return config.issuer.policyKey;
1034
1072
  }
1035
1073
 
1074
+ /**
1075
+ * The principal DID that owns this agent (and so its gateway's credentials), read from the agent's own policy bundle
1076
+ * (`ownerPrincipal`, MAGP §16.3) on the API it was just provisioned through, and saved into the config. A gateway is bound
1077
+ * to it: an admitted agent of any other owner is refused. Returns it, or null when the bundle names none.
1078
+ */
1079
+ async function ensureOwnerPrincipal(config, base) {
1080
+ if (typeof config.ownerPrincipal === 'string' && SAFE_DID.test(config.ownerPrincipal)) return config.ownerPrincipal;
1081
+ let owner = null;
1082
+ try {
1083
+ const res = await fetch(`${String(base).replace(/\/+$/, '')}/policy/bundle/${encodeURIComponent(config.agentDid)}`);
1084
+ owner = (await res.json().catch(() => null))?.data?.ownerPrincipal ?? null;
1085
+ } catch {
1086
+ owner = null;
1087
+ }
1088
+ if (typeof owner !== 'string' || !SAFE_DID.test(owner)) return null;
1089
+ config.ownerPrincipal = owner;
1090
+ return owner;
1091
+ }
1092
+
1093
+ const NO_OWNER_PRINCIPAL = (base) =>
1094
+ `Could not read the principal that owns this agent from its policy bundle (GET ${base}/policy/bundle/<agent DID>, ownerPrincipal). ` +
1095
+ 'A gateway is bound to the owner of its credentials, so none was scaffolded. Retry when the API is reachable (it needs a ' +
1096
+ 'backend that names the owner), or pass --no-gateway for the single-process example (not an enforcement boundary).';
1097
+
1036
1098
  const NO_POLICY_KEY = (base) =>
1037
1099
  `Could not get MetaMynd's policy-signing key: the provisioning response carried none, and GET ${base}/magp/policy/pubkey ` +
1038
1100
  'did not return one. A gateway without it would trust whatever policy bundle its network fetch returns, so none was ' +
@@ -1148,7 +1210,7 @@ const WHY = {
1148
1210
  // gate, and neither of them depends on the amount.
1149
1211
  NO_PERMISSION_FOR_ACTION: 'the mandate never granted this action - at any amount',
1150
1212
  NO_MANDATE: 'there is no mandate for this action at all',
1151
- MANDATE_REVOKED: 'the owner revoked the mandate for this action',
1213
+ MANDATE_REVOKED: 'the owner revoked the mandate for this action',
1152
1214
  };
1153
1215
 
1154
1216
  // ---------------------------------------------------------------- 1. CONTEXT
@@ -1168,7 +1230,9 @@ console.log('');
1168
1230
  console.log(dim(' 1. this project holds an agent identity (a DID) and its signing key'));
1169
1231
  console.log(dim(' 2. that agent has a mandate - a scope it may act in, and a spend cap'));
1170
1232
  console.log(dim(' 3. guardTool() wraps your tool, so nothing calls the raw handler'));
1171
- console.log(dim(' 4. each attempt is signed here, then decided by MetaMynd remotely'));
1233
+ console.log(dim(' 4. each attempt is signed here and checked against your signed rules: what'));
1234
+ console.log(dim(' they refuse stops HERE, with no network call; what could run is decided'));
1235
+ console.log(dim(' by the MetaMynd gate, which records it'));
1172
1236
  console.log(dim(' 5. your tool runs ONLY if that decision is ALLOW'));
1173
1237
  console.log('');
1174
1238
  console.log(dim(' scope ${scope}'));
@@ -1178,7 +1242,7 @@ console.log(dim(' cap ${currency} ${perTxnMax} per transaction, set by your
1178
1242
  async function attempt(n, intent, args, tool = gatedBookFlight) {
1179
1243
  console.log('');
1180
1244
  console.log(bold(' Step ' + n + ' of 4') + ' - ' + intent);
1181
- console.log(dim(' signing the request locally, then asking the gate to decide...'));
1245
+ console.log(dim(' signing the request here, checking it against your signed rules...'));
1182
1246
  try {
1183
1247
  const r = await tool(args);
1184
1248
  console.log('\\x1b[32m ALLOWED\\x1b[0m your tool ran and returned ' + (r.pnr ?? 'ok'));
@@ -1193,7 +1257,7 @@ async function attempt(n, intent, args, tool = gatedBookFlight) {
1193
1257
  } else {
1194
1258
  console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
1195
1259
  console.log(dim(' ' + why));
1196
- console.log(dim(' your tool never ran - the gate refused before execution.'));
1260
+ 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.'));
1197
1261
  }
1198
1262
  }
1199
1263
  }
@@ -1352,7 +1416,7 @@ const WHY = {
1352
1416
  // gate, and neither of them depends on the amount.
1353
1417
  NO_PERMISSION_FOR_ACTION: 'the mandate never granted this action - at any amount',
1354
1418
  NO_MANDATE: 'there is no mandate for this action at all',
1355
- MANDATE_REVOKED: 'the owner revoked the mandate for this action',
1419
+ MANDATE_REVOKED: 'the owner revoked the mandate for this action',
1356
1420
  };
1357
1421
 
1358
1422
  // ---------------------------------------------------------------- 1. CONTEXT
@@ -1385,11 +1449,12 @@ console.log(dim(' gateway ' + GATEWAY + ' (run it in a separate terminal - se
1385
1449
  async function attempt(n, intent, args, tool = gatedBookFlight) {
1386
1450
  console.log('');
1387
1451
  console.log(bold(' Step ' + n + ' of 4') + ' - ' + intent);
1388
- console.log(dim(' signing the request locally, then asking the gate to decide...'));
1452
+ console.log(dim(' signing the request here, checking it against your signed rules...'));
1389
1453
  try {
1390
1454
  const r = await tool(args);
1391
1455
  console.log('\\x1b[32m ALLOWED\\x1b[0m your tool ran (in ./gateway) and returned ' + (r.pnr ?? 'ok'));
1392
1456
  console.log(dim(' ' + WHY.AUTHORIZED));
1457
+ return true;
1393
1458
  } catch (e) {
1394
1459
  const g = e.governance ?? {};
1395
1460
  const why = WHY[g.reasonCode] ?? e.message;
@@ -1400,14 +1465,14 @@ async function attempt(n, intent, args, tool = gatedBookFlight) {
1400
1465
  } else {
1401
1466
  console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
1402
1467
  console.log(dim(' ' + why));
1403
- console.log(dim(' your tool never ran - refused before execution.'));
1468
+ 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.'));
1404
1469
  }
1405
1470
  }
1406
1471
  }
1407
1472
 
1408
1473
  console.log('');
1409
1474
  console.log(rule(66));
1410
- await attempt(1, 'a ${currency} ${under} booking, low risk. Expected to pass.', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
1475
+ const step1Ran = await attempt(1, 'a ${currency} ${under} booking, low risk. Expected to pass.', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
1411
1476
  await attempt(2, 'a ${currency} ${over} booking, deliberately over the cap.', { amount: ${over}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
1412
1477
  await attempt(3, 'a ${currency} ${under} booking, but flagged high risk.', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'high' });
1413
1478
  await attempt(
@@ -1425,7 +1490,13 @@ console.log(bold(' What this proved'));
1425
1490
  console.log('');
1426
1491
  console.log(dim(' - one code path, three outcomes. The rules decided, not this file'));
1427
1492
  console.log(dim(' and not the model driving it.'));
1428
- console.log(dim(' - step 1 ran in ./gateway, a process this file cannot reach into. There'));
1493
+ // Only claim what happened (pre-beta rerun 2026-10-03, L-h: this said "step 1 ran in ./gateway" when step 1 was refused).
1494
+ if (step1Ran) {
1495
+ console.log(dim(' - step 1 ran in ./gateway, a process this file cannot reach into. There'));
1496
+ } else {
1497
+ console.log(dim(' - step 1 did not run this time (see above). When it does, it runs in'));
1498
+ console.log(dim(' ./gateway, a process this file cannot reach into. There'));
1499
+ }
1429
1500
  console.log(dim(' is no rawBookFlight() here to call instead - that is what actually'));
1430
1501
  console.log(dim(' stops a bypass, not the guardTool() call above it.'));
1431
1502
  console.log(dim(' - step 4 needed no rule to stop it. The agent could not widen its own'));
@@ -1434,7 +1505,7 @@ console.log(dim(' - every blocked/escalated call never reached a real tool at
1434
1505
  console.log(dim(' - if the gate were unreachable the guard fails CLOSED: it blocks.'));
1435
1506
  console.log('');
1436
1507
  console.log(bold(' With MetaMynd, you can\\'t be bypassed.') + ' ./gateway is why - it independently');
1437
- console.log(dim(' re-verified step 1 before running it, and holds the tool this file never can.'));
1508
+ console.log(dim(step1Ran ? ' re-verified step 1 before running it, and holds the tool this file never can.' : ' re-verifies every call before running it, and holds the tool this file never can.'));
1438
1509
  console.log('');
1439
1510
  console.log(' Change the cap in the dashboard (${SOPS_PATH_ASCII}) and run again.');
1440
1511
  console.log(dim(' The outcome changes. This file does not. That is the point.'));
@@ -1553,7 +1624,7 @@ async function attempt(n, total, step) {
1553
1624
  const tool = step.action === 'permissions.update' ? gatedChangeOwnPermissions : gatedAction;
1554
1625
  console.log('');
1555
1626
  console.log(bold(' Step ' + n + ' of ' + total) + ' - ' + step.intent);
1556
- console.log(dim(' signing the request locally, then asking the gate to decide...'));
1627
+ console.log(dim(' signing the request here, checking it against your signed rules...'));
1557
1628
  let got;
1558
1629
  let g = {};
1559
1630
  try {
@@ -1568,7 +1639,7 @@ async function attempt(n, total, step) {
1568
1639
  ${escalateNote(sandbox)}
1569
1640
  } else {
1570
1641
  console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? e.message));
1571
- console.log(dim(' your tool never ran - refused before execution.'));
1642
+ 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.'));
1572
1643
  }
1573
1644
  }
1574
1645
  const asExpected = got === step.expect && (!step.reasonCode || g.reasonCode === step.reasonCode);
@@ -1633,7 +1704,7 @@ console.log('');
1633
1704
  * allowed value-less request would be refused AUTHORIZATION_REQUIRED. The README states what that
1634
1705
  * leaves open (replay) rather than implying the financial scaffold's guarantees.
1635
1706
  */
1636
- function gatewayServerFileNeutral(scope, port, apiBase, policyKey) {
1707
+ function gatewayServerFileNeutral(scope, port, apiBase, policyKey, agentDid, ownerPrincipal) {
1637
1708
  return `#!/usr/bin/env node
1638
1709
  // gateway/server.mjs — the enforcement boundary for this agent's tool(s).
1639
1710
  //
@@ -1643,6 +1714,7 @@ function gatewayServerFileNeutral(scope, port, apiBase, policyKey) {
1643
1714
  // compromised or dishonest agent calling its own local function gets nothing here, because
1644
1715
  // there is no local function: the tool only runs in this process.
1645
1716
  import http from 'node:http';
1717
+ import { readFileSync } from 'node:fs';
1646
1718
  import { createMcpGuard } from '${MCP_GUARD_PKG}';
1647
1719
  import { createHttpGateway } from '${GATEWAY_PKG}';
1648
1720
 
@@ -1666,6 +1738,11 @@ async function performAction(args) {
1666
1738
  // exactly those keys here — the gateway refuses every top-level key you do not name.
1667
1739
  const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFields: [], allowedFields: [] }];
1668
1740
 
1741
+ // This gateway's OWN identity (a did:key, separate from the agent's), registered as your counterparty when
1742
+ // \`npx create-metamynd-agent\` provisioned it. It signs the reports below, so the issuer files them under YOUR account and
1743
+ // nobody else can write into it. Keep service.metamynd.json out of version control (it is gitignored).
1744
+ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', import.meta.url)));
1745
+
1669
1746
  // requireAuthorization is OFF on purpose. It makes the gateway claim a single-use, stateful
1670
1747
  // authorization before running the tool — but the agent's guard only seals one for a value-bearing
1671
1748
  // action (amount > 0), and this agent has no spending authority. With it ON, every ALLOWED request
@@ -1677,11 +1754,23 @@ const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFie
1677
1754
  // forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
1678
1755
  // fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
1679
1756
  // a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
1680
- const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false, policyPublicKey: '${policyKey}' });
1757
+ // allowedAgents pins this gateway to the ONE agent it was scaffolded for (MAGP §16.3). Every other check here judges the
1758
+ // caller by the caller's OWN mandate and SOP — written by the caller's owner. Without the pin, any agent on the platform
1759
+ // whose owner granted it this same action would run your tool with your credentials (refused AGENT_NOT_ADMITTED instead).
1760
+ // Add a DID here only for another agent you mean this gateway to act for. gatewayOwnerPrincipal binds it to YOU, the owner of
1761
+ // 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).
1764
+ 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');
1681
1765
 
1682
1766
  const gateway = createHttpGateway({
1683
1767
  guard,
1684
1768
  routes,
1769
+ // 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.
1771
+ reportOutcomes: true,
1772
+ // 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
+ reportSpool: './reports.spool.jsonl',
1685
1774
  forward: async (req) => {
1686
1775
  let args = {};
1687
1776
  try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
@@ -1762,6 +1851,11 @@ the agent gets nothing, and there is no local function to call directly. The pol
1762
1851
  pinned to MetaMynd's own signing key (\`policyPublicKey\`, baked in from your provisioning response) —
1763
1852
  a party that can intercept the bundle fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway
1764
1853
  a forged bundle with a higher cap or no rules; \`server.mjs\` refuses an unsigned or tampered one outright.
1854
+ It is also pinned to THIS agent, and bound to you as the owner of its credentials (\`allowedAgents\` and
1855
+ \`gatewayOwnerPrincipal\` in \`server.mjs\`, MAGP 16.3): any other agent - even one whose own
1856
+ owner granted it this same action - is refused \`AGENT_NOT_ADMITTED\` before its rules are read. Every request it runs or
1857
+ refuses is reported, signed with its own registered identity (\`service.metamynd.json\`), to your Activity Log (MAGP 16.4) -
1858
+ it claims nothing, so this is the only way what it ran reaches you.
1765
1859
 
1766
1860
  **NOT closed — be precise about this:**
1767
1861
 
@@ -2037,7 +2131,7 @@ function gitignore() {
2037
2131
  // in the AgentSafe repo for the full pattern (mutual handshake, x402 payment, capability
2038
2132
  // binding) this is a minimal slice of.
2039
2133
 
2040
- function gatewayServerFile(scope, port, apiBase, policyKey) {
2134
+ function gatewayServerFile(scope, port, apiBase, policyKey, agentDid, ownerPrincipal) {
2041
2135
  return `#!/usr/bin/env node
2042
2136
  // gateway/server.mjs — the REAL enforcement boundary for this agent's tool(s).
2043
2137
  //
@@ -2092,7 +2186,16 @@ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', impo
2092
2186
  // forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
2093
2187
  // fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
2094
2188
  // a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
2095
- const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true, policyPublicKey: '${policyKey}' });
2189
+ // allowedAgents pins this gateway to the ONE agent it was scaffolded for (MAGP §16.3). Every other check here judges the
2190
+ // caller by the caller's OWN mandate and SOP — written by the caller's owner. Without the pin, any agent on the platform
2191
+ // whose owner granted it this same action would run your tool with your credentials (refused AGENT_NOT_ADMITTED instead).
2192
+ // Add a DID here only for another agent you mean this gateway to act for. gatewayOwnerPrincipal binds it to YOU, the owner of
2193
+ // its credentials: an agent listed here that another principal owns is refused anyway (GATEWAY_OWNER_MISMATCH).
2194
+ // The claim below does not stop that on its own: the hold belongs to the CALLER, and the caller's owner decides which
2195
+ // services may claim it — including registering this gateway's DID as one of theirs.
2196
+ 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).
2198
+ 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');
2096
2199
 
2097
2200
  const gateway = createHttpGateway({
2098
2201
  guard,
@@ -2102,6 +2205,11 @@ const gateway = createHttpGateway({
2102
2205
  // above lists which keys may appear; this makes the VALUES of every one of them (a payee, a passenger list) the agent's too.
2103
2206
  // A request whose authorization bound no payload is refused (PAYLOAD_BINDING_REQUIRED) rather than run unbound.
2104
2207
  requirePayloadBinding: true,
2208
+ // What this gateway refuses itself (another agent, a rule, a binding failure) is reported, signed, to your Activity Log
2209
+ // (MAGP 16.4). Claimed executions are already there, through the claim.
2210
+ reportOutcomes: true,
2211
+ // A report the issuer could not take (it was down, say) is kept here and re-sent, so nothing it saw is lost. Gitignored.
2212
+ reportSpool: './reports.spool.jsonl',
2105
2213
  forward: async (req) => {
2106
2214
  let args = {};
2107
2215
  try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
@@ -2168,7 +2276,7 @@ function gatewayEnvExample() {
2168
2276
  }
2169
2277
 
2170
2278
  function gatewayGitignore() {
2171
- return `node_modules/\n.env\nservice.metamynd.json\n`;
2279
+ return `node_modules/\n.env\nservice.metamynd.json\nreports.spool.jsonl\nreports.spool.jsonl.flushing\n`;
2172
2280
  }
2173
2281
 
2174
2282
  function gatewayReadme(slug, scope, port) {
@@ -2227,14 +2335,20 @@ add another protected route here rather than adding a local function back in \`i
2227
2335
 
2228
2336
  ## What this closes, precisely
2229
2337
 
2230
- Four independent checks, each closing a different bypass an agent (or anything able to call its
2231
- own code, or a network attacker) might attempt:
2338
+ Five independent checks, each closing a different bypass an agent (or anything able to call its
2339
+ own code, or a network attacker) might attempt - and a record of what it turned away:
2232
2340
 
2233
2341
  - **Direct call.** \`bookFlight()\` doesn't exist in the agent's process. There's nothing to call.
2234
2342
  - **Forged policy bundle.** \`server.mjs\` bakes in \`policyPublicKey\` (from your provisioning
2235
2343
  response) and refuses an unsigned or tampered bundle outright — a party that can intercept the
2236
2344
  fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway a bundle with a higher cap or
2237
2345
  no rules and have it trusted the same as the real one.
2346
+ - **Another agent.** Every other check judges the caller by the caller's OWN mandate, which its own owner
2347
+ writes. \`server.mjs\` pins \`allowedAgents\` to this agent and \`gatewayOwnerPrincipal\` to you (MAGP 16.3), so another agent - even one whose
2348
+ owner granted it this action and registered this gateway as its counterparty - is refused
2349
+ \`AGENT_NOT_ADMITTED\` before anything is claimed.
2350
+ - **Visibility.** What it refuses itself (another agent, a rule, a binding failure) is reported, signed with its own
2351
+ registered identity, to your Activity Log (MAGP 16.4); what it executes is already there, through the claim.
2238
2352
  - **Confused deputy (payload).** The gateway re-verifies the signed request against this agent's
2239
2353
  own policy AND binds it to the actual request body (payload binding,
2240
2354
  \`@metamynd/agentsafe-http-gateway\` ≥ 0.4.5) — signing a cheap request while executing an
@@ -2396,20 +2510,30 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
2396
2510
  if (!policyKey) {
2397
2511
  throw new Error("scaffoldProject: a gateway needs the issuer's policy-signing key (config.issuer.policyKey) to pin its bundle — call ensurePolicyKey() first, or pass withGateway: false");
2398
2512
  }
2513
+ // The gateway acts for exactly this agent (allowedAgents, MAGP §16.3) — never scaffold one that would pin to nobody.
2514
+ if (!config.agentDid) {
2515
+ throw new Error('scaffoldProject: a gateway is pinned to the agent it serves (config.agentDid) — none was given; pass withGateway: false for an agent-only scaffold');
2516
+ }
2517
+ // It is written into generated JavaScript as a string literal: anything but a plain DID is refused, never quoted around.
2518
+ if (!SAFE_DID.test(config.agentDid)) throw new Error(`scaffoldProject: config.agentDid is not a DID: ${JSON.stringify(config.agentDid)}`);
2519
+ // ...and to the owner of its credentials (gatewayOwnerPrincipal, MAGP §16.3), read from the agent's bundle (ensureOwnerPrincipal).
2520
+ if (typeof config.ownerPrincipal !== 'string' || !SAFE_DID.test(config.ownerPrincipal)) {
2521
+ throw new Error('scaffoldProject: a gateway is bound to the principal that owns its credentials (config.ownerPrincipal) — call ensureOwnerPrincipal() first, or pass withGateway: false');
2522
+ }
2399
2523
  const gwDir = join(outDir, 'gateway');
2400
2524
  if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
2401
- if (!neutral) {
2525
+ {
2402
2526
  // The gateway's OWN identity — the one that claims and settles this agent's holds (MAGP §8.7.6/
2403
2527
  // §8.7.10), separate from the agent's own DID on purpose (an agent must not be able to release or
2404
- // lower-settle a hold it authorized itself). `gatewayIdentity` is real and registered when the
2405
- // caller generated + registered one (main()'s hosted-financial path); a caller that didn't
2528
+ // lower-settle a hold it authorized itself), and the key its reports are signed with (MAGP 16.4) on either shape.
2529
+ // `gatewayIdentity` is real and registered when the caller generated + registered one (main()'s hosted path); a caller that didn't
2406
2530
  // (an older code path, or a direct scaffoldProject() call in a test) falls back to the historical
2407
2531
  // non-cryptographic placeholder — same shape as before this field existed, still parses and runs,
2408
2532
  // just unable to claim a MAINNET hold or sign an authenticated claim on any network.
2409
2533
  const identity = gatewayIdentity ?? { did: `did:local:${scope}-gateway`, keyHex: null };
2410
2534
  writeFileSafe(gwDir, 'service.metamynd.json', JSON.stringify({ serviceDid: identity.did, serviceKey: identity.keyHex }, null, 2) + '\n', force, 0o600);
2411
2535
  }
2412
- writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase, policyKey) : gatewayServerFile(scope, gatewayPort, apiBase, policyKey), force);
2536
+ writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase, policyKey, config.agentDid, config.ownerPrincipal) : gatewayServerFile(scope, gatewayPort, apiBase, policyKey, config.agentDid, config.ownerPrincipal), force);
2413
2537
  writeFileSafe(gwDir, 'package.json', gatewayPackageJson(slug), force);
2414
2538
  writeFileSafe(gwDir, '.env.example', neutral ? gatewayEnvExampleNeutral() : gatewayEnvExample(), force);
2415
2539
  writeFileSafe(gwDir, '.gitignore', gatewayGitignore(), force);
@@ -2638,6 +2762,8 @@ const PORT = Number(process.env.PORT || ${gatewayPort});
2638
2762
  // fails BUNDLE_SUBJECT_MISMATCH, not just an unmatched-signature error, because the bundle
2639
2763
  // this gateway serves is only ever this one agent's.
2640
2764
  const AGENT_DID = '${agentDid}';
2765
+ // No principal exists offline: the harness bundle and this gateway name the same local owner (MAGP §16.3).
2766
+ const HARNESS_OWNER = 'did:local:harness-owner';
2641
2767
 
2642
2768
  // Reshapes the harness's own rules-file shape ({mandate, sops:[{standardKey,document}],
2643
2769
  // standards:[{standardKey,document}]}) into what agentsafe-mcp-guard's verifyRequest expects
@@ -2658,12 +2784,16 @@ function bundleFromRules(rules) {
2658
2784
  // and your agent process both read, so editing it takes effect on the next request everywhere.
2659
2785
  const guard = createMcpGuard({
2660
2786
  serviceDid: 'did:local:${scope}-gateway',
2787
+ allowedAgents: [AGENT_DID], // MAGP §16.3: any other agent is refused AGENT_NOT_ADMITTED before its rules are read
2788
+ gatewayOwnerPrincipal: HARNESS_OWNER, // the local stand-in for the principal that owns this gateway's credentials
2661
2789
  fetchBundle: async (agentDid) => {
2662
2790
  if (agentDid !== AGENT_DID) return { subject: AGENT_DID, mandates: [], sops: [], standards: [] };
2663
2791
  const rules = JSON.parse(readFileSync('../metamynd-rules.json', 'utf8'));
2664
- return { subject: AGENT_DID, ...bundleFromRules(rules) };
2792
+ return { subject: AGENT_DID, ownerPrincipal: HARNESS_OWNER, ...bundleFromRules(rules) };
2665
2793
  },
2666
2794
  });
2795
+ // A guard below 0.20.0 ignores allowedAgents and serves every agent: refuse to start on one (a stale lockfile, say).
2796
+ 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');
2667
2797
 
2668
2798
  // One protected route per gated action in index.mjs. This gateway IS the tool, not a proxy in
2669
2799
  // front of one, so a path with no route below is refused (denyByDefault) — nothing to fall through TO.
@@ -3232,7 +3362,7 @@ const WHY = {
3232
3362
  MERCHANT_NOT_ALLOWED: 'the mandate lists which merchants this agent may pay',
3233
3363
  NO_PERMISSION_FOR_ACTION: 'the mandate never granted this action - at any amount',
3234
3364
  NO_MANDATE: 'there is no mandate for this action at all',
3235
- MANDATE_REVOKED: 'the owner revoked the mandate for this action',
3365
+ MANDATE_REVOKED: 'the owner revoked the mandate for this action',
3236
3366
  };
3237
3367
 
3238
3368
  async function attempt(n, intent, action, args, tool = gatedBookFlight) {
@@ -3258,7 +3388,7 @@ async function attempt(n, intent, action, args, tool = gatedBookFlight) {
3258
3388
  } else {
3259
3389
  console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
3260
3390
  console.log(dim(' ' + why));
3261
- console.log(dim(' your tool never ran - the gate refused before execution.'));
3391
+ 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.'));
3262
3392
  }
3263
3393
  dashboard.logDecision(action, args, g);
3264
3394
  }
@@ -3455,7 +3585,7 @@ async function attempt(n, total, step) {
3455
3585
  }
3456
3586
  } else {
3457
3587
  console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? e.message));
3458
- console.log(dim(' your tool never ran - the gate refused before execution.'));
3588
+ 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.'));
3459
3589
  }
3460
3590
  dashboard.logDecision(action, step.context, g);
3461
3591
  }
@@ -3928,6 +4058,7 @@ async function runClaim(args) {
3928
4058
  keepConfigAndStop('--financial was passed, but the approved agent has NO spending authority (the request was non-financial).');
3929
4059
  }
3930
4060
  if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndStop(NO_POLICY_KEY(base));
4061
+ if (!args['no-gateway'] && !(await ensureOwnerPrincipal(config, config.apiBase || base))) keepConfigAndStop(NO_OWNER_PRINCIPAL(base));
3931
4062
  const slug = slugify(state.name || 'metamynd-agent');
3932
4063
  const outDir = resolve(String(args.out || `./${slug}`));
3933
4064
  if (issuedNonFinancial) {
@@ -4137,16 +4268,15 @@ async function main() {
4137
4268
 
4138
4269
  // 3c. The gateway's OWN identity — a real did:key, separate from the agent's own (an agent must
4139
4270
  // never be able to release or lower-settle a hold it authorized itself; only the SERVICE that
4140
- // claimed it may). Only the financial, with-gateway shape ever claims a hold at all; the
4141
- // non-financial gateway never calls claimAuthorization() (requireAuthorization is off there), so
4142
- // it has no counterparty identity to register.
4271
+ // claimed it may) — and, on either shape, the key that signs its reports to your Activity Log (MAGP 16.4): a
4272
+ // non-financial gateway claims nothing, so without a registered identity nothing it ran would reach you.
4143
4273
  let gatewayIdentity = null;
4144
- if (financial && !args['no-gateway']) {
4274
+ if (!args['no-gateway']) {
4145
4275
  const gwKeypair = generateAgentKeypair();
4146
4276
  const gatewayDid = buildDidKey(rawPublicKeyFromSpkiHex(gwKeypair.publicKeyHex));
4147
4277
  gatewayIdentity = { did: gatewayDid, keyHex: gwKeypair.privateKeyHex };
4148
- console.log(c.dim(`\n → registering the gateway's identity as a trusted counterparty (so it can claim this agent's holds) …`));
4149
- const reg = await registerGatewayCounterparty(base, token, gatewayDid, `${scope}-gateway`);
4278
+ 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);
4150
4280
  if (reg.ok) {
4151
4281
  console.log(` ${c.green('✓')} gateway DID ${c.b(gatewayDid)} — ${reg.message}`);
4152
4282
  } else {
@@ -4165,6 +4295,7 @@ async function main() {
4165
4295
  for (const n of demo.notDemonstrated) console.log(` ${c.yellow('!')} not staged in the demo: ${n.rule} ${c.dim('— ' + n.why + '; still enforced')}`);
4166
4296
  }
4167
4297
  if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndFail(outDir, config, NO_POLICY_KEY(base));
4298
+ if (!args['no-gateway'] && !(await ensureOwnerPrincipal(config, config.apiBase || base))) keepConfigAndFail(outDir, config, NO_OWNER_PRINCIPAL(base));
4168
4299
  scaffoldProject({ demo, outDir, config, slug, scope, perTxnMax, currency, merchant: financial ? merchants[0] || 'demo-merchant' : merchants[0], sandbox: false, withGateway: !args['no-gateway'], gatewayPort: Number(args['gateway-port']) || DEFAULT_GATEWAY_PORT, force: !!args.force, gatewayIdentity });
4169
4300
  }
4170
4301
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-metamynd-agent",
3
- "version": "0.14.10",
3
+ "version": "0.14.18",
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": {