create-metamynd-agent 0.10.16 → 0.12.0

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 +12 -3
  2. package/index.mjs +158 -18
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -10,9 +10,9 @@ default, a second `gateway/` process** built on
10
10
  The agent's own `guardTool()` call is a fast, local, client-side check; the gateway is the real
11
11
  enforcement boundary — see [Separate tool gateway](#separate-tool-gateway-default) below.
12
12
 
13
- > **Prerequisite:** an agent is always owned by a **KYB-verified owner** — a person/org with a
14
- > MetaMynd account. If that's you and you're verified, you're ready. Verify once in the dashboard if
15
- > not; it's the only gate.
13
+ > **Prerequisite:** an agent is always owned by a person/org with a **MetaMynd account**. Verification
14
+ > is *reported*, not required, to get started: an unverified owner works on Testnet within the
15
+ > free-tier limits, and verifying once in the dashboard (KYC/KYB) is what unlocks mainnet.
16
16
 
17
17
  ## Free local harness (no account, no network, `--harness`)
18
18
 
@@ -170,6 +170,15 @@ the whole protocol.
170
170
  Named precisely, not left implicit:
171
171
 
172
172
  - **Direct call.** `bookFlight()` doesn't exist in the agent's process.
173
+ - **Payload binding is ON by default (0.11.0, MAGP §8.3.9).** The generated agent signs the COMPLETE body it sends as a payload, the
174
+ authorization it gets from the gate is bound to that same body, and the generated gateway sets `requirePayloadBinding: true`: it
175
+ digests the body it is about to run and the issuer refuses the claim unless that is the digest the agent signed. The eight
176
+ signed fields only ever covered amount and merchant; this covers the value of every field the route allows (a payee, a passenger
177
+ list you add). A request that bound no payload is refused (`PAYLOAD_BINDING_REQUIRED`) rather than run unbound. If you add a body
178
+ field, add it to `allowedFields` **and** to the `payload` the agent signs (both sit next to each other in the generated files).
179
+ A scaffold that signs through the signer daemon (`--byok --daemon-socket`) needs `@metamynd/agentsafe-signer` 0.15.0 or later. The
180
+ hosted non-financial scaffold reads nothing from its body, so there is nothing for it to bind, and a scaffold with no gateway
181
+ (`--no-gateway`, or `--sandbox`'s in-process demo) has no separate executor to hold to the digest, so it stays as it was.
173
182
  - **Confused deputy (payload).** Signing a cheap request while executing an expensive one (a
174
183
  different amount/merchant in the body than what was signed) is refused before the tool runs —
175
184
  payload binding (`@metamynd/agentsafe-http-gateway` ≥ 0.4.0). The default binder requires
package/index.mjs CHANGED
@@ -53,7 +53,7 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
53
53
  // requires request inputs blocks a bare baseline request).
54
54
  // 0.12.4 makes `verify` probe in the currency the mandate's caps name (it assumed USD), which a scaffolded
55
55
  // non-USD agent's `npm test` needs.
56
- const GUARD_VERSION = '^0.14.0';
56
+ const GUARD_VERSION = '^0.15.0';
57
57
  /** The harness entry point's config load, shared by both harness templates: a fresh clone has no
58
58
  * agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
59
59
  function harnessConfigLoad() {
@@ -795,6 +795,30 @@ async function apiPost(base, path, body, token) {
795
795
  return json;
796
796
  }
797
797
 
798
+ /**
799
+ * POST /policy/counterparties for the gateway's own DID — best-effort, unlike apiPost above: a
800
+ * hosted financial gateway works fine on testnet with no registration at all (open by default), so
801
+ * a failure here (network blip, an older backend without the endpoint) should not abort scaffolding
802
+ * the way a failed agent provisioning does. Always sends confirmEnforcementChange: true — this CLI
803
+ * IS the confirmation (the caller is reading this exact terminal output live, the same way a
804
+ * dashboard registration shows its own dialog before setting the flag); see the printed message
805
+ * either way.
806
+ */
807
+ async function registerGatewayCounterparty(base, token, did, label) {
808
+ try {
809
+ const res = await fetch(`${base}/policy/counterparties`, {
810
+ method: 'POST',
811
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
812
+ body: JSON.stringify({ did, label, confirmEnforcementChange: true }),
813
+ });
814
+ const json = await res.json().catch(() => null);
815
+ if (!res.ok || json?.success === false) return { ok: false, message: json?.message ?? `HTTP ${res.status}` };
816
+ return { ok: true, message: json?.message ?? 'registered' };
817
+ } catch (e) {
818
+ return { ok: false, message: String(e?.message ?? e) };
819
+ }
820
+ }
821
+
798
822
  // ---------- scaffolding ----------
799
823
  /**
800
824
  * The --no-gateway / --sandbox variant: the tool is a local function in the SAME process as
@@ -987,18 +1011,23 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
987
1011
  // --- the gateway atomically claim single-use execution, closing replay + cumulative spend, not
988
1012
  // --- just re-checking policy. See ./gateway/README.md.
989
1013
  async function bookFlightViaGateway(args, decision) {
1014
+ // The COMPLETE body the tool receives. It is signed as a payload (MAGP 8.3.9): the eight signed fields cover amount and
1015
+ // merchant, not anything else a real tool takes (a payee, a passenger list). The digest covers ALL of it, and the gateway
1016
+ // refuses to run the tool on a body that is not exactly this one.
1017
+ const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
990
1018
  const signed = await guard.buildSignedRequest({
991
1019
  action: '${scope}',
992
1020
  amount: args.amount,
993
1021
  currency: args.currency ?? '${currency}',
994
1022
  merchant: args.merchant,
995
1023
  context: { tool: 'book-flight', riskLevel: args.riskLevel ?? 'low' },
1024
+ payload,
996
1025
  });
997
1026
  signed.authorizationId = decision?.authorizationId;
998
1027
  const res = await fetch(GATEWAY + '/book-flight', {
999
1028
  method: 'POST',
1000
1029
  headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
1001
- body: JSON.stringify({ amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' }),
1030
+ body: JSON.stringify(payload),
1002
1031
  });
1003
1032
  const body = await res.json().catch(() => null);
1004
1033
  if (!res.ok) {
@@ -1020,6 +1049,9 @@ const gatedBookFlight = guard.guardTool(
1020
1049
  currency: a.currency ?? '${currency}',
1021
1050
  merchant: a.merchant,
1022
1051
  context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
1052
+ // The authorization is bound to the SAME body bookFlightViaGateway() sends, so the gate records what this agent
1053
+ // signed and the gateway can prove it is running exactly that. Keep the two in step if you add a field.
1054
+ payload: { amount: a.amount, merchant: a.merchant, currency: a.currency ?? '${currency}' },
1023
1055
  }),
1024
1056
  );
1025
1057
 
@@ -1330,7 +1362,7 @@ console.log('');
1330
1362
  * allowed value-less request would be refused AUTHORIZATION_REQUIRED. The README states what that
1331
1363
  * leaves open (replay) rather than implying the financial scaffold's guarantees.
1332
1364
  */
1333
- function gatewayServerFileNeutral(scope, port, apiBase) {
1365
+ function gatewayServerFileNeutral(scope, port, apiBase, policyKey) {
1334
1366
  return `#!/usr/bin/env node
1335
1367
  // gateway/server.mjs — the enforcement boundary for this agent's tool(s).
1336
1368
  //
@@ -1369,7 +1401,12 @@ const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFie
1369
1401
  // would be refused AUTHORIZATION_REQUIRED. What that leaves open (a signed request is reusable for
1370
1402
  // the guard's 5-minute freshness window; the issuer's stateful rate/circuit-breaker floors; and the
1371
1403
  // rule inputs, which are not covered by the signature) is spelled out in README.md.
1372
- const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false });
1404
+ // policyPublicKey pins the bundle to MetaMynd's OWN signing key (from your provisioning response,
1405
+ // baked in here — never fetched at runtime, so a compromised network can't swap it out alongside a
1406
+ // forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
1407
+ // fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
1408
+ // a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
1409
+ const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
1373
1410
 
1374
1411
  const gateway = createHttpGateway({
1375
1412
  guard,
@@ -1450,7 +1487,10 @@ This is narrower than it sounds, so read it before relying on it.
1450
1487
  **Closed:** no tool call without a validly signed request from THIS agent for an action in its
1451
1488
  mandate. The agent's Ed25519 signature is checked against its DID, the action must be granted, and a
1452
1489
  suspended or quarantined agent is refused. Code that skips the agent's own guard and cannot sign as
1453
- the agent gets nothing, and there is no local function to call directly.
1490
+ the agent gets nothing, and there is no local function to call directly. The policy bundle itself is
1491
+ pinned to MetaMynd's own signing key (\`policyPublicKey\`, baked in from your provisioning response) —
1492
+ a party that can intercept the bundle fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway
1493
+ a forged bundle with a higher cap or no rules; \`server.mjs\` refuses an unsigned or tampered one outright.
1454
1494
 
1455
1495
  **NOT closed — be precise about this:**
1456
1496
 
@@ -1608,7 +1648,7 @@ function exampleReadme(slug, scope, withGateway, gatewayPort, daemonKey = false)
1608
1648
  const configFileLine = daemonKey
1609
1649
  ? `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
1610
1650
  **Holds no secret key.** Signing goes through your already-running agentsafe-signer daemon
1611
- (\`daemonSocketPath\`) instead — see \`docs/integration/INSTALL-AGENTSAFE-SIGNER.md\`.`
1651
+ (\`daemonSocketPath\`) instead — see \`docs/integration/INSTALL-AGENTSAFE-SIGNER.md\`.${withGateway ? ' Payload binding is on by default and signs through the daemon too, which needs agentsafe-signer 0.15.0 or later.' : ''}`
1612
1652
  : `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
1613
1653
  **Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.`;
1614
1654
  const gatewaySection = withGateway
@@ -1714,7 +1754,7 @@ function gitignore() {
1714
1754
  // in the AgentSafe repo for the full pattern (mutual handshake, x402 payment, capability
1715
1755
  // binding) this is a minimal slice of.
1716
1756
 
1717
- function gatewayServerFile(scope, port, apiBase) {
1757
+ function gatewayServerFile(scope, port, apiBase, policyKey) {
1718
1758
  return `#!/usr/bin/env node
1719
1759
  // gateway/server.mjs — the REAL enforcement boundary for this agent's tool(s).
1720
1760
  //
@@ -1723,6 +1763,7 @@ function gatewayServerFile(scope, port, apiBase) {
1723
1763
  // published policy bundle — it does not trust the agent's own guard.guardTool() check. A
1724
1764
  // compromised or dishonest agent calling its own local function gets nothing here, because
1725
1765
  // there is no local function: the tool only runs in this process.
1766
+ import { readFileSync } from 'node:fs';
1726
1767
  import http from 'node:http';
1727
1768
  import { createMcpGuard } from '${MCP_GUARD_PKG}';
1728
1769
  import { createHttpGateway } from '${GATEWAY_PKG}';
@@ -1751,17 +1792,33 @@ async function bookFlight(args) {
1751
1792
  // signed context, not the tool body.
1752
1793
  const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}', valueFields: ['amount', 'merchant'], allowedFields: ['amount', 'currency', 'merchant'] }];
1753
1794
 
1754
- // No serviceKey: this minimal gateway only calls verifyRequest() (re-check a signed request),
1755
- // not the mutual-handshake methods, which are the only thing that needs it.
1795
+ // This gateway's OWN identity — separate from the agent's on purpose (an agent must not be able to
1796
+ // release or lower-settle a hold it authorized itself; only the SERVICE that claimed it may). Real
1797
+ // and registered as a trusted counterparty when \`npx create-metamynd-agent\` provisioned it (see
1798
+ // README.md); a real serviceKey is what lets serviceAuthHeaders() sign an AUTHENTICATED claim
1799
+ // (MAGP-SERVICE-v1) instead of relying on the bearer claimToken alone — required for a MAINNET
1800
+ // hold (MAGP §8.7.10), and strictly better than an anonymous claim on testnet too.
1801
+ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', import.meta.url)));
1756
1802
  //
1757
1803
  // requireAuthorization: true is what closes replay and cumulative spend, not just per-request
1758
1804
  // policy — it requires the agent's authorizationId (from a REAL guard.authorize() call) to
1759
1805
  // atomically claim single-use execution against the issuer before this gateway runs the tool.
1760
- const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: true });
1806
+ //
1807
+ // policyPublicKey pins the bundle to MetaMynd's OWN signing key (from your provisioning response,
1808
+ // baked in here — never fetched at runtime, so a compromised network can't swap it out alongside a
1809
+ // forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
1810
+ // fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
1811
+ // a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
1812
+ const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
1761
1813
 
1762
1814
  const gateway = createHttpGateway({
1763
1815
  guard,
1764
1816
  routes,
1817
+ // Payload binding (MAGP 8.3.9): the agent signs a digest of the WHOLE body, this gateway digests the body it is about to
1818
+ // run, and the issuer refuses the claim unless the two are the digest the agent signed at authorize time. allowedFields
1819
+ // above lists which keys may appear; this makes the VALUES of every one of them (a payee, a passenger list) the agent's too.
1820
+ // A request whose authorization bound no payload is refused (PAYLOAD_BINDING_REQUIRED) rather than run unbound.
1821
+ requirePayloadBinding: true,
1765
1822
  forward: async (req) => {
1766
1823
  let args = {};
1767
1824
  try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
@@ -1828,7 +1885,7 @@ function gatewayEnvExample() {
1828
1885
  }
1829
1886
 
1830
1887
  function gatewayGitignore() {
1831
- return `node_modules/\n.env\n`;
1888
+ return `node_modules/\n.env\nservice.metamynd.json\n`;
1832
1889
  }
1833
1890
 
1834
1891
  function gatewayReadme(slug, scope, port) {
@@ -1879,6 +1936,10 @@ add another protected route here rather than adding a local function back in \`i
1879
1936
  - \`server.mjs\` — the gateway: one protected route (\`POST /book-flight\`, action \`${scope}\`),
1880
1937
  \`@metamynd/agentsafe-mcp-guard\`'s \`verifyRequest()\` re-checking every request, and the real
1881
1938
  \`bookFlight()\`.
1939
+ - \`service.metamynd.json\` — this gateway's OWN identity (\`serviceDid\`/\`serviceKey\`), gitignored
1940
+ like \`agent.metamynd.json\` one directory up. \`npx create-metamynd-agent\` generated it and
1941
+ registered its DID as a trusted counterparty (see "Who may claim this agent's holds" below); a
1942
+ clone needs its own, the same way it needs its own \`agent.metamynd.json\`.
1882
1943
  - \`.env.example\` — where real tool credentials go (copy to \`.env\`, fill in, never commit).
1883
1944
 
1884
1945
  ## What this closes, precisely
@@ -1887,6 +1948,10 @@ Four independent checks, each closing a different bypass an agent (or anything a
1887
1948
  own code, or a network attacker) might attempt:
1888
1949
 
1889
1950
  - **Direct call.** \`bookFlight()\` doesn't exist in the agent's process. There's nothing to call.
1951
+ - **Forged policy bundle.** \`server.mjs\` bakes in \`policyPublicKey\` (from your provisioning
1952
+ response) and refuses an unsigned or tampered bundle outright — a party that can intercept the
1953
+ fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway a bundle with a higher cap or
1954
+ no rules and have it trusted the same as the real one.
1890
1955
  - **Confused deputy (payload).** The gateway re-verifies the signed request against this agent's
1891
1956
  own policy AND binds it to the actual request body (payload binding,
1892
1957
  \`@metamynd/agentsafe-http-gateway\` ≥ 0.4.5) — signing a cheap request while executing an
@@ -1918,6 +1983,25 @@ own code, or a network attacker) might attempt:
1918
1983
  the gateway will release that hold. Only the gateway that claimed the hold can do either — the agent
1919
1984
  cannot capture it lower or void it, which is what stopped it recovering the budget of a purchase it
1920
1985
  had just had executed.
1986
+
1987
+ ### Who may claim this agent's holds
1988
+
1989
+ This gateway claims with its own real identity (\`service.metamynd.json\`, above) — not the agent's,
1990
+ on purpose: an agent must never be able to release or lower-settle a hold it authorized itself. On
1991
+ provisioning, \`npx create-metamynd-agent\` registered that DID as a trusted counterparty for you
1992
+ (\`POST /policy/counterparties\`). A **MAINNET** hold is *always* registered-only, regardless of your
1993
+ registry (MAGP §8.7.10) — a testnet hold stays open unless you have registered anything at all.
1994
+ Manage the registry at \`/dashboard/counterparties\`, or:
1995
+
1996
+ \`\`\`bash
1997
+ curl -X POST $MAGP_API/policy/counterparties \\
1998
+ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \\
1999
+ -d '{ "did": "'"$(node -e "console.log(JSON.parse(require('fs').readFileSync('./service.metamynd.json')).serviceDid)")"'", "confirmEnforcementChange": true }'
2000
+ \`\`\`
2001
+
2002
+ \`confirmEnforcementChange: true\` is required only for your account's FIRST-EVER registration —
2003
+ it switches EVERY one of your holds (not just this agent's) from open to registered-only, so the
2004
+ API refuses a first registration without it.
1921
2005
  - **Amount unknown.** \`amount-unknown\` (\`@metamynd/agentsafe-mcp-guard\` ≥ 0.3.0) blocks a
1922
2006
  platform tool by default when its raw bytes or a nested payload hide the amount from a naive
1923
2007
  spend cap — AND this agent's OWN starter SOP (see \`agent.metamynd.json\` /
@@ -1981,7 +2065,7 @@ function assertScaffoldTarget(outDir, force) {
1981
2065
  */
1982
2066
  // `demo` (from buildPolicyCases) selects the NON-financial project; without it this is the historical
1983
2067
  // payment scaffold. `merchant` defaults to a payment demo's merchant only in that financial branch.
1984
- function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'USD', merchant, sandbox, withGateway, gatewayPort = DEFAULT_GATEWAY_PORT, force = false, demo = null }) {
2068
+ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'USD', merchant, sandbox, withGateway, gatewayPort = DEFAULT_GATEWAY_PORT, force = false, demo = null, gatewayIdentity = null }) {
1985
2069
  const neutral = !!demo;
1986
2070
  assertScaffoldTarget(outDir, force);
1987
2071
  console.log(`\n ${c.b('Scaffolding')} ${c.dim(outDir)}`);
@@ -2021,9 +2105,26 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
2021
2105
 
2022
2106
  if (withGateway) {
2023
2107
  const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
2108
+ // The key that signs every policy bundle (magp.policy-signer.ts) — present on every provisioning
2109
+ // response that talks to a real backend (full account, sandbox, delegated-claim). Baked into the
2110
+ // generated gateway so it pins the bundle rather than trusting whatever the fetch returns; absent
2111
+ // only for a config predating this field (an older scaffold's saved agent.metamynd.json), where
2112
+ // the generated gateway is unchanged from before rather than baking in nothing and crashing.
2113
+ const policyKey = config.issuer?.policyKey ?? null;
2024
2114
  const gwDir = join(outDir, 'gateway');
2025
2115
  if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
2026
- writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase) : gatewayServerFile(scope, gatewayPort, apiBase), force);
2116
+ if (!neutral) {
2117
+ // The gateway's OWN identity — the one that claims and settles this agent's holds (MAGP §8.7.6/
2118
+ // §8.7.10), separate from the agent's own DID on purpose (an agent must not be able to release or
2119
+ // lower-settle a hold it authorized itself). `gatewayIdentity` is real and registered when the
2120
+ // caller generated + registered one (main()'s hosted-financial path); a caller that didn't
2121
+ // (an older code path, or a direct scaffoldProject() call in a test) falls back to the historical
2122
+ // non-cryptographic placeholder — same shape as before this field existed, still parses and runs,
2123
+ // just unable to claim a MAINNET hold or sign an authenticated claim on any network.
2124
+ const identity = gatewayIdentity ?? { did: `did:local:${scope}-gateway`, keyHex: null };
2125
+ writeFileSafe(gwDir, 'service.metamynd.json', JSON.stringify({ serviceDid: identity.did, serviceKey: identity.keyHex }, null, 2) + '\n', force, 0o600);
2126
+ }
2127
+ writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase, policyKey) : gatewayServerFile(scope, gatewayPort, apiBase, policyKey), force);
2027
2128
  writeFileSafe(gwDir, 'package.json', gatewayPackageJson(slug), force);
2028
2129
  writeFileSafe(gwDir, '.env.example', neutral ? gatewayEnvExampleNeutral() : gatewayEnvExample(), force);
2029
2130
  writeFileSafe(gwDir, '.gitignore', gatewayGitignore(), force);
@@ -2292,6 +2393,10 @@ const gateway = createHttpGateway({
2292
2393
  guard,
2293
2394
  routes: ROUTES,
2294
2395
  denyByDefault: true,
2396
+ // Payload binding (MAGP 8.3.9): the agent signs a digest of the WHOLE body and this gateway refuses to run a tool on a
2397
+ // body that is not exactly that one — the values of every allowed field, not only amount and merchant. A request that
2398
+ // bound no payload is refused (PAYLOAD_BINDING_REQUIRED) instead of run unbound.
2399
+ requirePayloadBinding: true,
2295
2400
  // Reached ONLY after the guard allowed the request AND the body was bound to what was signed.
2296
2401
  forward: async (req) => {
2297
2402
  const route = matchRoute(ROUTES, req.method, req.path);
@@ -2751,8 +2856,11 @@ const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://localhost:${gatewayPo
2751
2856
  // issuer): it builds and signs the same canonical message a real gate would verify, entirely
2752
2857
  // offline, using this agent's own did:key — the gateway verifies that signature for itself.
2753
2858
  async function callGateway(path, action, args) {
2754
- const signed = await guard.buildSignedRequest({ action, amount: args.amount, currency: args.currency ?? '${currency}', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' } });
2755
- const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: JSON.stringify({ amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' }) });
2859
+ // The COMPLETE body the tool receives, signed as a payload (MAGP 8.3.9): the eight signed fields cover amount and merchant
2860
+ // only, and this gateway refuses to run the tool on a body that is not exactly the one signed here.
2861
+ const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
2862
+ const signed = await guard.buildSignedRequest({ action, amount: args.amount, currency: args.currency ?? '${currency}', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' }, payload });
2863
+ const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: JSON.stringify(payload) });
2756
2864
  const body = await res.json().catch(() => null);
2757
2865
  if (!res.ok) {
2758
2866
  const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
@@ -2953,7 +3061,9 @@ ${withGateway ? `const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://loca
2953
3061
  // file to call directly. buildSignedRequest() is pure (no network, no issuer): it signs the request
2954
3062
  // offline with this agent's own did:key, and the gateway verifies that signature for itself.
2955
3063
  async function callGateway(path, action, args) {
2956
- const signed = await guard.buildSignedRequest({ action, merchant: MERCHANT, context: args });
3064
+ // The body is empty on purpose (this tool reads nothing from it), and it is signed as the payload so the gateway can
3065
+ // require binding on every route: a body with anything in it is not the one signed (MAGP 8.3.9).
3066
+ const signed = await guard.buildSignedRequest({ action, merchant: MERCHANT, context: args, payload: {} });
2957
3067
  const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: '{}' });
2958
3068
  const body = await res.json().catch(() => null);
2959
3069
  if (!res.ok) {
@@ -3627,6 +3737,15 @@ async function main() {
3627
3737
  }
3628
3738
  const body = {
3629
3739
  name, scope,
3740
+ // Explicit, not left to the backend's own eligibility-based default (mainnet for a KYB-verified
3741
+ // owner, testnet otherwise) — an owner who happens to be verified would otherwise silently get a
3742
+ // REAL mainnet agent from a plain `npx create-metamynd-agent` with no signal that anything
3743
+ // changed. The generated financial gateway CAN now satisfy mainnet's registry enforcement (a
3744
+ // real, registered did:key — see the gateway identity step below), but this CLI has no flag to
3745
+ // opt into mainnet yet; that is a separate decision (exposing it safely — KYB status, spend
3746
+ // caps — is more than a network toggle) from giving the gateway a real identity, which is what
3747
+ // this fixes. docs/design/release-blockers-open-items.md, "Registry default".
3748
+ network: 'testnet',
3630
3749
  ...(financial ? { currency, maxAmount, perTxnMax } : {}),
3631
3750
  merchants,
3632
3751
  ...(publicKey ? { publicKey } : {}),
@@ -3673,6 +3792,27 @@ async function main() {
3673
3792
  console.log(c.dim(` POST ${base}/agent-identity/${config.identityId}/verify-key { "signature": "<hex>" } (owner token)`));
3674
3793
  }
3675
3794
 
3795
+ // 3c. The gateway's OWN identity — a real did:key, separate from the agent's own (an agent must
3796
+ // never be able to release or lower-settle a hold it authorized itself; only the SERVICE that
3797
+ // claimed it may). Only the financial, with-gateway shape ever claims a hold at all; the
3798
+ // non-financial gateway never calls claimAuthorization() (requireAuthorization is off there), so
3799
+ // it has no counterparty identity to register.
3800
+ let gatewayIdentity = null;
3801
+ if (financial && !args['no-gateway']) {
3802
+ const gwKeypair = generateAgentKeypair();
3803
+ const gatewayDid = buildDidKey(rawPublicKeyFromSpkiHex(gwKeypair.publicKeyHex));
3804
+ gatewayIdentity = { did: gatewayDid, keyHex: gwKeypair.privateKeyHex };
3805
+ console.log(c.dim(`\n → registering the gateway's identity as a trusted counterparty (so it can claim this agent's holds) …`));
3806
+ const reg = await registerGatewayCounterparty(base, token, gatewayDid, `${scope}-gateway`);
3807
+ if (reg.ok) {
3808
+ console.log(` ${c.green('✓')} gateway DID ${c.b(gatewayDid)} — ${reg.message}`);
3809
+ } else {
3810
+ console.log(` ${c.yellow('!')} could not register the gateway as a trusted counterparty: ${reg.message}`);
3811
+ console.log(c.dim(` it still works on testnet (open by default with no registry entries); a MAINNET hold cannot be`));
3812
+ console.log(c.dim(` claimed until it is registered — POST ${base}/policy/counterparties { "did": "${gatewayDid}", "confirmEnforcementChange": true }`));
3813
+ }
3814
+ }
3815
+
3676
3816
  // 4. Scaffold + next steps
3677
3817
  // The demo comes from the rules actually provisioned: the caller's own, else the backend's default
3678
3818
  // for a non-financial agent (the same amount-free review harnessDefaultSopNeutral describes).
@@ -3681,7 +3821,7 @@ async function main() {
3681
3821
  console.log(` ${c.green('✓')} derived ${demo.cases.length} demo case(s) from your rules`);
3682
3822
  for (const n of demo.notDemonstrated) console.log(` ${c.yellow('!')} not staged in the demo: ${n.rule} ${c.dim('— ' + n.why + '; still enforced')}`);
3683
3823
  }
3684
- 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 });
3824
+ 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 });
3685
3825
  }
3686
3826
 
3687
3827
  // Exported so the smoke tests can exercise the generators directly. Importing this file must not
@@ -3689,6 +3829,6 @@ async function main() {
3689
3829
  export {
3690
3830
  buildPolicyCases, resolveFinancial, policyMolecules, ruleToMolecule,
3691
3831
  generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
3692
- scaffoldProject, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
3832
+ scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
3693
3833
  };
3694
3834
  if (!process.env.CREATE_METAMYND_AGENT_NO_MAIN) main().catch((e) => fail(e?.stack || e?.message || String(e)));
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "create-metamynd-agent",
3
- "version": "0.10.16",
3
+ "version": "0.12.0",
4
4
  "description": "Scaffold a MetaMynd/AgentSafe-governed AI agent in one command — logs in, provisions the agent (identity + mandate + SOP + Standards) in a single call, writes agent.metamynd.json plus a runnable agent + separate tool-gateway process that closes direct-call, confused-deputy, replay, and cumulative-spend bypasses. --harness scaffolds a free, local, zero-network governance harness instead.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-metamynd-agent": "index.mjs"
8
8
  },
9
9
  "scripts": {
10
- "test": "node harness-gateway.smoke.mjs && node harness-gateway-bind.smoke.mjs && node policy-demo.smoke.mjs && node hosted-nonfinancial.smoke.mjs && node modes-nonfinancial.smoke.mjs && node byok-daemon.smoke.mjs"
10
+ "test": "node generated-code-parses.smoke.mjs && node harness-gateway.smoke.mjs && node harness-gateway-bind.smoke.mjs && node policy-demo.smoke.mjs && node hosted-nonfinancial.smoke.mjs && node modes-nonfinancial.smoke.mjs && node byok-daemon.smoke.mjs"
11
11
  },
12
12
  "files": [
13
13
  "index.mjs",