create-metamynd-agent 0.7.1 → 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.
- package/README.md +38 -8
- package/index.mjs +63 -20
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -135,23 +135,53 @@ npm start
|
|
|
135
135
|
### Separate tool gateway (default)
|
|
136
136
|
|
|
137
137
|
This is the other half of **without MetaMynd, you can be bypassed**: WITH it — specifically, with
|
|
138
|
-
`gateway/`, the second process this scaffolds by default —
|
|
139
|
-
platform's own MCP counterparty can't
|
|
138
|
+
`gateway/`, the second process this scaffolds by default — calling the tool directly instead of
|
|
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 [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.
|
|
140
143
|
|
|
141
144
|
`guard.guardTool()` in `index.mjs` still runs — it's a fast, local, client-side pre-check that gives
|
|
142
145
|
good UX (fail fast on an obviously-blocked call, no round trip) — but it is **not** what stops a
|
|
143
146
|
bypass. It still calls its handler in the SAME process regardless of where the decision came from,
|
|
144
147
|
so anything able to call that handler directly gets the same result the gate would have given it.
|
|
145
148
|
|
|
146
|
-
What actually stops
|
|
149
|
+
What actually stops that bypass is that `bookFlight()` doesn't exist in the agent's process at all.
|
|
147
150
|
It exists only in `gateway/server.mjs` — a separate process, started separately, holding any real
|
|
148
151
|
tool credentials the agent process never sees — which independently re-verifies every request
|
|
149
|
-
against the agent's own published policy bundle before running it
|
|
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
|
|
150
157
|
counterparty check in [`@metamynd/agentsafe-mcp-guard`](https://www.npmjs.com/package/@metamynd/agentsafe-mcp-guard),
|
|
151
|
-
built with [`@metamynd/agentsafe-http-gateway`](https://www.npmjs.com/package/@metamynd/agentsafe-http-gateway)
|
|
152
|
-
It's a minimal slice of the
|
|
153
|
-
AgentSafe repo (mutual handshake, x402 payment binding, capability tokens) — this scaffold gives
|
|
154
|
-
|
|
158
|
+
built with [`@metamynd/agentsafe-http-gateway`](https://www.npmjs.com/package/@metamynd/agentsafe-http-gateway).
|
|
159
|
+
It's a minimal slice of the fuller pattern proven end to end in `demo/duffel-mcp-gateway` in the
|
|
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.
|
|
163
|
+
|
|
164
|
+
#### What this closes, precisely
|
|
165
|
+
|
|
166
|
+
Named precisely, not left implicit:
|
|
167
|
+
|
|
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.
|
|
155
185
|
|
|
156
186
|
Pass `--no-gateway` to opt out and get the old single-process scaffold instead — e.g. if you're
|
|
157
187
|
already running your own separate gateway and don't need this one. **You are back to being
|
package/index.mjs
CHANGED
|
@@ -26,9 +26,13 @@ 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
|
-
|
|
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
|
|
34
|
+
// never scaffold a range that could resolve below it.
|
|
35
|
+
const GATEWAY_VERSION = '^0.2.0';
|
|
32
36
|
const DEFAULT_API = 'https://metamynd.ai/api/v1';
|
|
33
37
|
const DEFAULT_GATEWAY_PORT = 4401; // distinct from --harness's dashboard (4400)
|
|
34
38
|
|
|
@@ -455,7 +459,11 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
|
|
|
455
459
|
// --- Calls the gateway process instead of a local function. There is no raw bookFlight() in
|
|
456
460
|
// --- this file to call directly — the tool, and any real credentials it needs, live only in
|
|
457
461
|
// --- ./gateway, which independently re-verifies this signed request itself.
|
|
458
|
-
|
|
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) {
|
|
459
467
|
const signed = guard.buildSignedRequest({
|
|
460
468
|
action: '${scope}',
|
|
461
469
|
amount: args.amount,
|
|
@@ -463,6 +471,7 @@ async function bookFlightViaGateway(args) {
|
|
|
463
471
|
merchant: args.merchant,
|
|
464
472
|
context: { tool: 'book-flight', riskLevel: args.riskLevel ?? 'low' },
|
|
465
473
|
});
|
|
474
|
+
signed.authorizationId = decision?.authorizationId;
|
|
466
475
|
const res = await fetch(GATEWAY + '/book-flight', {
|
|
467
476
|
method: 'POST',
|
|
468
477
|
headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
|
|
@@ -690,14 +699,17 @@ A MetaMynd/AgentSafe-governed agent, scaffolded with \`create-metamynd-agent\`.
|
|
|
690
699
|
${gatewaySection}${
|
|
691
700
|
withGateway
|
|
692
701
|
? `**With MetaMynd's gateway, you can't be bypassed** — that's what this section is about.
|
|
693
|
-
This scaffold's default shape (agent + separate gateway process, port ${gatewayPort} by
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
exists in \`./gateway\`, a process this one cannot reach into, which
|
|
698
|
-
every request against this agent's own policy bundle
|
|
699
|
-
|
|
700
|
-
README for
|
|
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.`
|
|
701
713
|
: `**Without MetaMynd, you can be bypassed** — this is that case. This scaffold has no
|
|
702
714
|
separate gateway process (either \`--sandbox\`, which never provisions real credentials, or
|
|
703
715
|
\`--no-gateway\` was passed): \`guard.guardTool()\` wraps a tool in the SAME process as the check
|
|
@@ -760,7 +772,11 @@ const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}' }];
|
|
|
760
772
|
|
|
761
773
|
// No serviceKey: this minimal gateway only calls verifyRequest() (re-check a signed request),
|
|
762
774
|
// not the mutual-handshake methods, which are the only thing that needs it.
|
|
763
|
-
|
|
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 });
|
|
764
780
|
|
|
765
781
|
const gateway = createHttpGateway({
|
|
766
782
|
guard,
|
|
@@ -838,7 +854,8 @@ function gatewayReadme(slug, scope, port) {
|
|
|
838
854
|
return `# ${slug}-gateway
|
|
839
855
|
|
|
840
856
|
**With MetaMynd, you can't be bypassed.** This process is why. It is the **real enforcement
|
|
841
|
-
boundary** for \`${slug}\`'s tool(s) — not \`../index.mjs\`.
|
|
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.
|
|
842
859
|
|
|
843
860
|
## Why this exists
|
|
844
861
|
|
|
@@ -854,7 +871,9 @@ This process closes that gap by being a **separate** one. The agent has no way t
|
|
|
854
871
|
and call \`bookFlight()\` directly, because \`bookFlight()\` doesn't exist in the agent's process —
|
|
855
872
|
it exists only here, and every request that reaches it has already been independently
|
|
856
873
|
re-verified against this agent's OWN published policy bundle, fetched over the network by THIS
|
|
857
|
-
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.
|
|
858
877
|
|
|
859
878
|
## Run
|
|
860
879
|
|
|
@@ -881,12 +900,36 @@ add another protected route here rather than adding a local function back in \`i
|
|
|
881
900
|
\`bookFlight()\`.
|
|
882
901
|
- \`.env.example\` — where real tool credentials go (copy to \`.env\`, fill in, never commit).
|
|
883
902
|
|
|
884
|
-
##
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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).
|
|
890
933
|
`;
|
|
891
934
|
}
|
|
892
935
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.7.
|
|
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
|
|
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"
|