create-metamynd-agent 0.10.16 → 0.11.2

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 +59 -13
  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() {
@@ -987,18 +987,23 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
987
987
  // --- the gateway atomically claim single-use execution, closing replay + cumulative spend, not
988
988
  // --- just re-checking policy. See ./gateway/README.md.
989
989
  async function bookFlightViaGateway(args, decision) {
990
+ // The COMPLETE body the tool receives. It is signed as a payload (MAGP 8.3.9): the eight signed fields cover amount and
991
+ // merchant, not anything else a real tool takes (a payee, a passenger list). The digest covers ALL of it, and the gateway
992
+ // refuses to run the tool on a body that is not exactly this one.
993
+ const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
990
994
  const signed = await guard.buildSignedRequest({
991
995
  action: '${scope}',
992
996
  amount: args.amount,
993
997
  currency: args.currency ?? '${currency}',
994
998
  merchant: args.merchant,
995
999
  context: { tool: 'book-flight', riskLevel: args.riskLevel ?? 'low' },
1000
+ payload,
996
1001
  });
997
1002
  signed.authorizationId = decision?.authorizationId;
998
1003
  const res = await fetch(GATEWAY + '/book-flight', {
999
1004
  method: 'POST',
1000
1005
  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}' }),
1006
+ body: JSON.stringify(payload),
1002
1007
  });
1003
1008
  const body = await res.json().catch(() => null);
1004
1009
  if (!res.ok) {
@@ -1020,6 +1025,9 @@ const gatedBookFlight = guard.guardTool(
1020
1025
  currency: a.currency ?? '${currency}',
1021
1026
  merchant: a.merchant,
1022
1027
  context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
1028
+ // The authorization is bound to the SAME body bookFlightViaGateway() sends, so the gate records what this agent
1029
+ // signed and the gateway can prove it is running exactly that. Keep the two in step if you add a field.
1030
+ payload: { amount: a.amount, merchant: a.merchant, currency: a.currency ?? '${currency}' },
1023
1031
  }),
1024
1032
  );
1025
1033
 
@@ -1330,7 +1338,7 @@ console.log('');
1330
1338
  * allowed value-less request would be refused AUTHORIZATION_REQUIRED. The README states what that
1331
1339
  * leaves open (replay) rather than implying the financial scaffold's guarantees.
1332
1340
  */
1333
- function gatewayServerFileNeutral(scope, port, apiBase) {
1341
+ function gatewayServerFileNeutral(scope, port, apiBase, policyKey) {
1334
1342
  return `#!/usr/bin/env node
1335
1343
  // gateway/server.mjs — the enforcement boundary for this agent's tool(s).
1336
1344
  //
@@ -1369,7 +1377,12 @@ const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFie
1369
1377
  // would be refused AUTHORIZATION_REQUIRED. What that leaves open (a signed request is reusable for
1370
1378
  // the guard's 5-minute freshness window; the issuer's stateful rate/circuit-breaker floors; and the
1371
1379
  // 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 });
1380
+ // policyPublicKey pins the bundle to MetaMynd's OWN signing key (from your provisioning response,
1381
+ // baked in here — never fetched at runtime, so a compromised network can't swap it out alongside a
1382
+ // forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
1383
+ // fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
1384
+ // a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
1385
+ const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
1373
1386
 
1374
1387
  const gateway = createHttpGateway({
1375
1388
  guard,
@@ -1450,7 +1463,10 @@ This is narrower than it sounds, so read it before relying on it.
1450
1463
  **Closed:** no tool call without a validly signed request from THIS agent for an action in its
1451
1464
  mandate. The agent's Ed25519 signature is checked against its DID, the action must be granted, and a
1452
1465
  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.
1466
+ the agent gets nothing, and there is no local function to call directly. The policy bundle itself is
1467
+ pinned to MetaMynd's own signing key (\`policyPublicKey\`, baked in from your provisioning response) —
1468
+ a party that can intercept the bundle fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway
1469
+ a forged bundle with a higher cap or no rules; \`server.mjs\` refuses an unsigned or tampered one outright.
1454
1470
 
1455
1471
  **NOT closed — be precise about this:**
1456
1472
 
@@ -1608,7 +1624,7 @@ function exampleReadme(slug, scope, withGateway, gatewayPort, daemonKey = false)
1608
1624
  const configFileLine = daemonKey
1609
1625
  ? `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
1610
1626
  **Holds no secret key.** Signing goes through your already-running agentsafe-signer daemon
1611
- (\`daemonSocketPath\`) instead — see \`docs/integration/INSTALL-AGENTSAFE-SIGNER.md\`.`
1627
+ (\`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
1628
  : `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
1613
1629
  **Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.`;
1614
1630
  const gatewaySection = withGateway
@@ -1714,7 +1730,7 @@ function gitignore() {
1714
1730
  // in the AgentSafe repo for the full pattern (mutual handshake, x402 payment, capability
1715
1731
  // binding) this is a minimal slice of.
1716
1732
 
1717
- function gatewayServerFile(scope, port, apiBase) {
1733
+ function gatewayServerFile(scope, port, apiBase, policyKey) {
1718
1734
  return `#!/usr/bin/env node
1719
1735
  // gateway/server.mjs — the REAL enforcement boundary for this agent's tool(s).
1720
1736
  //
@@ -1757,11 +1773,22 @@ const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}', valu
1757
1773
  // requireAuthorization: true is what closes replay and cumulative spend, not just per-request
1758
1774
  // policy — it requires the agent's authorizationId (from a REAL guard.authorize() call) to
1759
1775
  // 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 });
1776
+ //
1777
+ // policyPublicKey pins the bundle to MetaMynd's OWN signing key (from your provisioning response,
1778
+ // baked in here — never fetched at runtime, so a compromised network can't swap it out alongside a
1779
+ // forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
1780
+ // fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
1781
+ // a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
1782
+ const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: true${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
1761
1783
 
1762
1784
  const gateway = createHttpGateway({
1763
1785
  guard,
1764
1786
  routes,
1787
+ // Payload binding (MAGP 8.3.9): the agent signs a digest of the WHOLE body, this gateway digests the body it is about to
1788
+ // run, and the issuer refuses the claim unless the two are the digest the agent signed at authorize time. allowedFields
1789
+ // above lists which keys may appear; this makes the VALUES of every one of them (a payee, a passenger list) the agent's too.
1790
+ // A request whose authorization bound no payload is refused (PAYLOAD_BINDING_REQUIRED) rather than run unbound.
1791
+ requirePayloadBinding: true,
1765
1792
  forward: async (req) => {
1766
1793
  let args = {};
1767
1794
  try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
@@ -1887,6 +1914,10 @@ Four independent checks, each closing a different bypass an agent (or anything a
1887
1914
  own code, or a network attacker) might attempt:
1888
1915
 
1889
1916
  - **Direct call.** \`bookFlight()\` doesn't exist in the agent's process. There's nothing to call.
1917
+ - **Forged policy bundle.** \`server.mjs\` bakes in \`policyPublicKey\` (from your provisioning
1918
+ response) and refuses an unsigned or tampered bundle outright — a party that can intercept the
1919
+ fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway a bundle with a higher cap or
1920
+ no rules and have it trusted the same as the real one.
1890
1921
  - **Confused deputy (payload).** The gateway re-verifies the signed request against this agent's
1891
1922
  own policy AND binds it to the actual request body (payload binding,
1892
1923
  \`@metamynd/agentsafe-http-gateway\` ≥ 0.4.5) — signing a cheap request while executing an
@@ -2021,9 +2052,15 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
2021
2052
 
2022
2053
  if (withGateway) {
2023
2054
  const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
2055
+ // The key that signs every policy bundle (magp.policy-signer.ts) — present on every provisioning
2056
+ // response that talks to a real backend (full account, sandbox, delegated-claim). Baked into the
2057
+ // generated gateway so it pins the bundle rather than trusting whatever the fetch returns; absent
2058
+ // only for a config predating this field (an older scaffold's saved agent.metamynd.json), where
2059
+ // the generated gateway is unchanged from before rather than baking in nothing and crashing.
2060
+ const policyKey = config.issuer?.policyKey ?? null;
2024
2061
  const gwDir = join(outDir, 'gateway');
2025
2062
  if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
2026
- writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase) : gatewayServerFile(scope, gatewayPort, apiBase), force);
2063
+ writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase, policyKey) : gatewayServerFile(scope, gatewayPort, apiBase, policyKey), force);
2027
2064
  writeFileSafe(gwDir, 'package.json', gatewayPackageJson(slug), force);
2028
2065
  writeFileSafe(gwDir, '.env.example', neutral ? gatewayEnvExampleNeutral() : gatewayEnvExample(), force);
2029
2066
  writeFileSafe(gwDir, '.gitignore', gatewayGitignore(), force);
@@ -2292,6 +2329,10 @@ const gateway = createHttpGateway({
2292
2329
  guard,
2293
2330
  routes: ROUTES,
2294
2331
  denyByDefault: true,
2332
+ // 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
2333
+ // body that is not exactly that one — the values of every allowed field, not only amount and merchant. A request that
2334
+ // bound no payload is refused (PAYLOAD_BINDING_REQUIRED) instead of run unbound.
2335
+ requirePayloadBinding: true,
2295
2336
  // Reached ONLY after the guard allowed the request AND the body was bound to what was signed.
2296
2337
  forward: async (req) => {
2297
2338
  const route = matchRoute(ROUTES, req.method, req.path);
@@ -2751,8 +2792,11 @@ const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://localhost:${gatewayPo
2751
2792
  // issuer): it builds and signs the same canonical message a real gate would verify, entirely
2752
2793
  // offline, using this agent's own did:key — the gateway verifies that signature for itself.
2753
2794
  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}' }) });
2795
+ // The COMPLETE body the tool receives, signed as a payload (MAGP 8.3.9): the eight signed fields cover amount and merchant
2796
+ // only, and this gateway refuses to run the tool on a body that is not exactly the one signed here.
2797
+ const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
2798
+ const signed = await guard.buildSignedRequest({ action, amount: args.amount, currency: args.currency ?? '${currency}', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' }, payload });
2799
+ const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: JSON.stringify(payload) });
2756
2800
  const body = await res.json().catch(() => null);
2757
2801
  if (!res.ok) {
2758
2802
  const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
@@ -2953,7 +2997,9 @@ ${withGateway ? `const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://loca
2953
2997
  // file to call directly. buildSignedRequest() is pure (no network, no issuer): it signs the request
2954
2998
  // offline with this agent's own did:key, and the gateway verifies that signature for itself.
2955
2999
  async function callGateway(path, action, args) {
2956
- const signed = await guard.buildSignedRequest({ action, merchant: MERCHANT, context: args });
3000
+ // The body is empty on purpose (this tool reads nothing from it), and it is signed as the payload so the gateway can
3001
+ // require binding on every route: a body with anything in it is not the one signed (MAGP 8.3.9).
3002
+ const signed = await guard.buildSignedRequest({ action, merchant: MERCHANT, context: args, payload: {} });
2957
3003
  const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: '{}' });
2958
3004
  const body = await res.json().catch(() => null);
2959
3005
  if (!res.ok) {
@@ -3689,6 +3735,6 @@ async function main() {
3689
3735
  export {
3690
3736
  buildPolicyCases, resolveFinancial, policyMolecules, ruleToMolecule,
3691
3737
  generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
3692
- scaffoldProject, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
3738
+ scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
3693
3739
  };
3694
3740
  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.11.2",
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",