create-metamynd-agent 0.7.2 → 0.7.3

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 +31 -21
  2. package/index.mjs +62 -42
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -137,8 +137,9 @@ npm start
137
137
  This is the other half of **without MetaMynd, you can be bypassed**: WITH it — specifically, with
138
138
  `gateway/`, the second process this scaffolds by default — calling the tool directly instead of
139
139
  through the check no longer works, the same way the hosted platform's own MCP counterparty can't
140
- be talked around by a compromised agent. See [Not solved by the gateway](#not-solved-by-the-gateway)
141
- below for the two things this specifically does **not** cover.
140
+ be talked around by a compromised agent. See [What this closes, precisely](#what-this-closes-precisely)
141
+ below for exactly what that covers, including the one gap found while building it that isn't
142
+ closed yet.
142
143
 
143
144
  `guard.guardTool()` in `index.mjs` still runs — it's a fast, local, client-side pre-check that gives
144
145
  good UX (fail fast on an obviously-blocked call, no round trip) — but it is **not** what stops a
@@ -148,30 +149,39 @@ so anything able to call that handler directly gets the same result the gate wou
148
149
  What actually stops that bypass is that `bookFlight()` doesn't exist in the agent's process at all.
149
150
  It exists only in `gateway/server.mjs` — a separate process, started separately, holding any real
150
151
  tool credentials the agent process never sees — which independently re-verifies every request
151
- against the agent's own published policy bundle before running it, and **binds that request to the
152
- actual body being executed** (`@metamynd/agentsafe-http-gateway` ≥ 0.2.0) closing a confused-deputy
153
- gap found during testing, where a signed cheap request could be governed while a different, expensive
154
- body was the one that actually ran. Same shape as the mutual counterparty check in
155
- [`@metamynd/agentsafe-mcp-guard`](https://www.npmjs.com/package/@metamynd/agentsafe-mcp-guard), built
156
- with [`@metamynd/agentsafe-http-gateway`](https://www.npmjs.com/package/@metamynd/agentsafe-http-gateway).
152
+ against the agent's own published policy bundle before running it, **binds that request to the
153
+ actual body being executed** (`@metamynd/agentsafe-http-gateway` ≥ 0.2.0), and requires the
154
+ agent's `authorizationId` to atomically claim single-use execution against the real stateful gate
155
+ (`requireAuthorization`, `@metamynd/agentsafe-mcp-guard` 0.2.0) closing a confused-deputy gap
156
+ and a replay/cumulative-spend gap, both found during independent testing. Same shape as the mutual
157
+ counterparty check in [`@metamynd/agentsafe-mcp-guard`](https://www.npmjs.com/package/@metamynd/agentsafe-mcp-guard),
158
+ built with [`@metamynd/agentsafe-http-gateway`](https://www.npmjs.com/package/@metamynd/agentsafe-http-gateway).
157
159
  It's a minimal slice of the fuller pattern proven end to end in `demo/duffel-mcp-gateway` in the
158
- AgentSafe repo (mutual handshake, x402 payment binding, capability tokens) — this scaffold gives you
159
- just the part that closes the direct-call bypass, not the whole protocol.
160
+ AgentSafe repo (mutual handshake, x402 payment binding, capability tokens) — this scaffold gives
161
+ you the parts that close direct-call, confused-deputy, replay, and cumulative-spend bypasses, not
162
+ the whole protocol.
160
163
 
161
- #### Not solved by the gateway
164
+ #### What this closes, precisely
162
165
 
163
166
  Named precisely, not left implicit:
164
167
 
165
- - **Replay.** The gateway checks a signed request is fresh, not that it hasn't been used before —
166
- a captured valid request can be resent within the freshness window. Single-use nonce consumption
167
- is the stateful issuer gate's job (`POST /policy/mandate/authorize`); this scaffold never calls it.
168
- - **Cumulative spend.** Each call is checked against the per-transaction cap correctly, but the
169
- mandate's TOTAL budget isn't tracked at the gateway many separately-legal calls can still add
170
- up past it. The real total is only enforced where a hold is actually reserved: the issuer gate.
171
-
172
- Both require wiring the full authorize-before-execute flow (agent calls the stateful gate first,
173
- gateway checks a decision bound to that specific authorization) rather than per-request policy
174
- re-evaluation alone — see `gateway/README.md`'s own "Beyond this minimal slice" section.
168
+ - **Direct call.** `bookFlight()` doesn't exist in the agent's process.
169
+ - **Confused deputy (payload).** Signing a cheap request while executing an expensive one (a
170
+ different amount/currency/merchant in the body than what was signed) is refused before the tool
171
+ runs payload binding.
172
+ - **Replay.** A captured, resent request fails to atomically claim single-use execution the second
173
+ time `requireAuthorization`.
174
+ - **Cumulative spend.** The claimed authorization only exists because the real stateful gate
175
+ already checked it against the mandate's TOTAL budget when minted, not just this one request's
176
+ amount so many small legal-looking calls can't add up past the cap this way.
177
+
178
+ **One narrower gap, found while building this and disclosed rather than left implicit:** the claim
179
+ above verifies the claimed authorization's own `agentDid`/`amount`/`currency` match the request —
180
+ not `merchant`, because the backend's hold record doesn't currently store it. A same-amount,
181
+ same-currency authorization legitimately obtained for one merchant could in principle unlock a
182
+ booking with a different merchant. Closing this needs a small backend change (storing `merchant`
183
+ on the hold); it isn't done here. `gateway/README.md`'s own "What this closes, precisely" section
184
+ has the same disclosure.
175
185
 
176
186
  Pass `--no-gateway` to opt out and get the old single-process scaffold instead — e.g. if you're
177
187
  already running your own separate gateway and don't need this one. **You are back to being
package/index.mjs CHANGED
@@ -26,7 +26,9 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
26
26
  const GUARD_VERSION = '^0.5.0';
27
27
  // The default hosted scaffold's SECOND process — the tool gateway (see scaffoldProject).
28
28
  const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
29
- const MCP_GUARD_VERSION = '^0.1.0';
29
+ // 0.2.0 adds requireAuthorization (closes replay + cumulative spend) — this scaffold sets that
30
+ // option, so a range that could resolve below 0.2.0 would silently scaffold a no-op.
31
+ const MCP_GUARD_VERSION = '^0.2.0';
30
32
  const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
31
33
  // 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
32
34
  // never scaffold a range that could resolve below it.
@@ -457,7 +459,11 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
457
459
  // --- Calls the gateway process instead of a local function. There is no raw bookFlight() in
458
460
  // --- this file to call directly — the tool, and any real credentials it needs, live only in
459
461
  // --- ./gateway, which independently re-verifies this signed request itself.
460
- async function bookFlightViaGateway(args) {
462
+ // --- \`decision\` is guardTool()'s own verdict, already produced by the REAL remote gate for any
463
+ // --- value-bearing action (sealValueActions, on by default) — its authorizationId is what lets
464
+ // --- the gateway atomically claim single-use execution, closing replay + cumulative spend, not
465
+ // --- just re-checking policy. See ./gateway/README.md.
466
+ async function bookFlightViaGateway(args, decision) {
461
467
  const signed = guard.buildSignedRequest({
462
468
  action: '${scope}',
463
469
  amount: args.amount,
@@ -465,6 +471,7 @@ async function bookFlightViaGateway(args) {
465
471
  merchant: args.merchant,
466
472
  context: { tool: 'book-flight', riskLevel: args.riskLevel ?? 'low' },
467
473
  });
474
+ signed.authorizationId = decision?.authorizationId;
468
475
  const res = await fetch(GATEWAY + '/book-flight', {
469
476
  method: 'POST',
470
477
  headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
@@ -691,17 +698,18 @@ A MetaMynd/AgentSafe-governed agent, scaffolded with \`create-metamynd-agent\`.
691
698
 
692
699
  ${gatewaySection}${
693
700
  withGateway
694
- ? `**With MetaMynd's gateway, calling the tool directly and skipping the check no longer
695
- works** — that's what this section is about. This scaffold's default shape (agent + separate
696
- gateway process, port ${gatewayPort} by default) is the actual enforcement boundary for that
697
- specific bypass: \`guard.guardTool()\` in \`index.mjs\` is a client-side convenience, not a
698
- boundary it still runs its handler in-process regardless of where the decision came from. What
699
- actually stops it is that \`bookFlight()\` itself only exists in \`./gateway\`, a process this one
700
- cannot reach into, which independently re-verifies every request against this agent's own policy
701
- bundle AND binds it to the actual body being executed. It does NOT track replay or cumulative
702
- spend across calls — see \`./gateway/README.md\`'s "Beyond this minimal slice" section. Re-scaffold
703
- with \`--no-gateway\` for the old single-process shape it is NOT a separate enforcement boundary
704
- at all; see its own generated README for why.`
701
+ ? `**With MetaMynd's gateway, you can't be bypassed** that's what this section is about.
702
+ This scaffold's default shape (agent + separate gateway process, port ${gatewayPort} by default)
703
+ is the actual enforcement boundary: \`guard.guardTool()\` in \`index.mjs\` is a client-side
704
+ convenience, not a boundary it still runs its handler in-process regardless of where the
705
+ decision came from. What actually stops direct-call and confused-deputy bypasses is that
706
+ \`bookFlight()\` itself only exists in \`./gateway\`, a process this one cannot reach into, which
707
+ independently re-verifies every request against this agent's own policy bundle AND binds it to
708
+ the actual body being executed. Replay and cumulative spend are closed too, via
709
+ \`requireAuthorization\` — see \`./gateway/README.md\`'s "What this closes, precisely" section for
710
+ exactly what that covers, including the one narrower gap disclosed there. Re-scaffold with
711
+ \`--no-gateway\` for the old single-process shape it is NOT a separate enforcement boundary at
712
+ all; see its own generated README for why.`
705
713
  : `**Without MetaMynd, you can be bypassed** — this is that case. This scaffold has no
706
714
  separate gateway process (either \`--sandbox\`, which never provisions real credentials, or
707
715
  \`--no-gateway\` was passed): \`guard.guardTool()\` wraps a tool in the SAME process as the check
@@ -764,7 +772,11 @@ const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}' }];
764
772
 
765
773
  // No serviceKey: this minimal gateway only calls verifyRequest() (re-check a signed request),
766
774
  // not the mutual-handshake methods, which are the only thing that needs it.
767
- const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API });
775
+ //
776
+ // requireAuthorization: true is what closes replay and cumulative spend, not just per-request
777
+ // policy — it requires the agent's authorizationId (from a REAL guard.authorize() call) to
778
+ // atomically claim single-use execution against the issuer before this gateway runs the tool.
779
+ const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: true });
768
780
 
769
781
  const gateway = createHttpGateway({
770
782
  guard,
@@ -841,11 +853,9 @@ function gatewayGitignore() {
841
853
  function gatewayReadme(slug, scope, port) {
842
854
  return `# ${slug}-gateway
843
855
 
844
- **With MetaMynd, you can't call the tool directly and skip the check.** This process is why. It
845
- is the **real enforcement boundary** for \`${slug}\`'s tool(s) — not \`../index.mjs\` — for that
846
- specific bypass. See [Beyond this minimal slice](#beyond-this-minimal-slice) below for the two
847
- things it does NOT yet close on its own: replaying a captured request, and spend adding up
848
- across many separately-legal calls.
856
+ **With MetaMynd, you can't be bypassed.** This process is why. It is the **real enforcement
857
+ boundary** for \`${slug}\`'s tool(s) — not \`../index.mjs\`. See
858
+ [What this closes, precisely](#what-this-closes-precisely) below for exactly what that covers.
849
859
 
850
860
  ## Why this exists
851
861
 
@@ -861,7 +871,9 @@ This process closes that gap by being a **separate** one. The agent has no way t
861
871
  and call \`bookFlight()\` directly, because \`bookFlight()\` doesn't exist in the agent's process —
862
872
  it exists only here, and every request that reaches it has already been independently
863
873
  re-verified against this agent's OWN published policy bundle, fetched over the network by THIS
864
- process, not trusted from the agent's say-so.
874
+ process, not trusted from the agent's say-so — AND bound to the actual body being executed
875
+ (payload binding) AND to a real, single-use, stateful authorization (\`requireAuthorization\`) —
876
+ see below for what each of those means precisely.
865
877
 
866
878
  ## Run
867
879
 
@@ -888,28 +900,36 @@ add another protected route here rather than adding a local function back in \`i
888
900
  \`bookFlight()\`.
889
901
  - \`.env.example\` — where real tool credentials go (copy to \`.env\`, fill in, never commit).
890
902
 
891
- ## Beyond this minimal slice
892
-
893
- This gateway independently re-verifies a signed request AND binds it to the actual request body
894
- (§9.3/§9.6 of the MAGP spec, plus payload binding \`@metamynd/agentsafe-http-gateway\` ≥ 0.2.0).
895
- That closes the specific bypass this scaffold exists to close: an agent (or anything able to call
896
- its code) presenting one set of values while a different set actually executes.
897
-
898
- Two things it deliberately does NOT close, named precisely rather than left implicit:
899
-
900
- - **Replay.** \`verifyRequest()\` checks the signed request is fresh, not that it hasn't been used
901
- before a captured valid request can be resent within the freshness window. Single-use nonce
902
- consumption is the stateful issuer gate's job, not this gateway's; this scaffold never calls it.
903
- - **Cumulative spend.** Each call is checked against the per-transaction cap correctly, but the
904
- mandate's TOTAL budget isn't tracked here many separately-legal calls can still add up past
905
- it. The real total is only enforced where holds are actually reserved: \`POST
906
- /policy/mandate/authorize\` on the issuer.
907
-
908
- Closing both means wiring the full authorize-before-execute flow — the agent calling that
909
- stateful endpoint first, this gateway checking a decision bound to that specific authorization —
910
- not just re-evaluating policy per request. See \`@metamynd/agentsafe-mcp-guard\`'s own README
911
- (mutual DID handshake, x402 payment binding, commitment-bound capability tokens) and
912
- \`demo/duffel-mcp-gateway\` in the AgentSafe repo for what that full pattern looks like.
903
+ ## What this closes, precisely
904
+
905
+ Four independent checks, each closing a different bypass an agent (or anything able to call its
906
+ own code, or a network attacker) might attempt:
907
+
908
+ - **Direct call.** \`bookFlight()\` doesn't exist in the agent's process. There's nothing to call.
909
+ - **Confused deputy (payload).** The gateway re-verifies the signed request against this agent's
910
+ own policy AND binds it to the actual request body (payload binding,
911
+ \`@metamynd/agentsafe-http-gateway\` ≥ 0.2.0) — signing a cheap request while executing an
912
+ expensive one is refused before the tool ever runs.
913
+ - **Replay.** \`requireAuthorization: true\` (set in \`server.mjs\`) requires the agent's
914
+ \`authorizationId\` from a REAL \`guard.authorize()\` call, which \`index.mjs\` already makes for
915
+ any value-bearing action by default to atomically claim single-use execution against the
916
+ issuer. A captured, replayed request fails the claim the second time.
917
+ - **Cumulative spend.** The same \`authorizationId\` only exists because the real stateful gate
918
+ already checked it against the mandate's TOTAL budget when it was minted — not just this one
919
+ request's amount. Many small legal-looking calls can't add up past the mandate cap this way,
920
+ because each needed its own real authorization first.
921
+
922
+ **One narrower gap, found while building this and disclosed rather than left implicit:** the
923
+ claim above verifies the claimed authorization's own \`agentDid\`/\`amount\`/\`currency\` match the
924
+ request being executed but not \`merchant\`, because the backend's hold record doesn't currently
925
+ store it. A same-amount, same-currency authorization legitimately obtained for one merchant could
926
+ in principle be presented to unlock a booking with a different merchant. Closing this needs a
927
+ small backend change (storing \`merchant\` on the hold so it can be compared too); it isn't done
928
+ here. See \`@metamynd/agentsafe-mcp-guard\`'s own README (\`requireAuthorization\`) for the full
929
+ mechanism, and \`demo/duffel-mcp-gateway\` in the AgentSafe repo for the fuller pattern this is a
930
+ slice of (mutual DID handshake, x402 payment binding, commitment-bound capability tokens — which
931
+ WOULD close the merchant gap too, by binding the whole transaction to a cryptographic commitment
932
+ rather than comparing individual stored fields).
913
933
  `;
914
934
  }
915
935
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-metamynd-agent",
3
- "version": "0.7.2",
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 the direct-call bypass. --harness scaffolds a free, local, zero-network governance harness instead.",
3
+ "version": "0.7.3",
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"