create-metamynd-agent 0.7.6 → 0.7.8

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 +17 -12
  2. package/index.mjs +40 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -150,7 +150,7 @@ What actually stops that bypass is that `bookFlight()` doesn't exist in the agen
150
150
  It exists only in `gateway/server.mjs` — a separate process, started separately, holding any real
151
151
  tool credentials the agent process never sees — which independently re-verifies every request
152
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.3.0), and requires the
153
+ actual body being executed** (`@metamynd/agentsafe-http-gateway` ≥ 0.4.0), and requires the
154
154
  agent's `authorizationId` to atomically claim single-use execution against the real stateful gate
155
155
  (`requireAuthorization`, `@metamynd/agentsafe-mcp-guard` ≥ 0.3.0) — closing a confused-deputy gap
156
156
  and a replay/cumulative-spend gap, both found during independent testing. Same shape as the mutual
@@ -167,22 +167,27 @@ Named precisely, not left implicit:
167
167
 
168
168
  - **Direct call.** `bookFlight()` doesn't exist in the agent's process.
169
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 (`@metamynd/agentsafe-http-gateway` ≥ 0.3.0). The default binder fails
172
- the request CLOSED, not just when it finds a mismatched flat field, but also when it can't find
173
- the governed fields at all nested JSON, an array, a renamed or differently-cased key. That
174
- gap was found and closed the same way: a signed $250/skyward-air request had previously been
175
- able to execute $5000/evil-corp via `{ booking: { amount, merchant } }`, because the flat
176
- matcher found nothing to compare and treated "nothing found" as "nothing to check."
170
+ different amount/merchant in the body than what was signed) is refused before the tool runs —
171
+ payload binding (`@metamynd/agentsafe-http-gateway` ≥ 0.4.0). The default binder requires
172
+ `amount`/`merchant` to actually be found in the body whenever the signed request names a real
173
+ value for them not just "did the body offer at least one correct-looking field." A first
174
+ attempt at this (0.3.0) checked the weaker version and was re-tested and closed the same day: a
175
+ correct decoy in one field (e.g. a matching `merchant`) let the OTHER field hide anywhere —
176
+ nested, renamed, an array, or an entirely empty/non-JSON body.
177
177
  - **Replay.** A captured, resent request fails to atomically claim single-use execution the second
178
178
  time — `requireAuthorization`.
179
179
  - **Cumulative spend.** The claimed authorization only exists because the real stateful gate
180
180
  already checked it against the mandate's TOTAL budget when minted, not just this one request's
181
181
  amount — so many small legal-looking calls can't add up past the cap this way.
182
- - **Amount unknown.** A signed-transaction tool (raw bytes) or a nested x402 payload carries its
183
- amount somewhere a naive spend cap never looks — `amount-unknown` (`@metamynd/agentsafe-guard`
184
- ≥ 0.6.0, `@metamynd/agentsafe-mcp-guard` ≥ 0.3.0) blocks by default when the gate can't
185
- determine the value, instead of letting it slip past the cap untested.
182
+ - **Amount unknown.** Two separate places this matters, both actually authored, not just
183
+ available: the platform's own custodial-signing tools (`@metamynd/agentsafe-guard` ≥ 0.6.0,
184
+ `@metamynd/agentsafe-mcp-guard` ≥ 0.3.0) block by default when a signed-transaction tool's raw
185
+ bytes or a nested x402 payload hide the amount from a naive spend cap — AND this agent's own
186
+ starter SOP puts the same `amount-unknown` check ahead of its per-transaction cap (both the
187
+ hosted default and `--harness`'s local one). The atom existing was not the gap: for a while
188
+ this SOP still only ever authored `amount-over`, which silently does not fire on a missing or
189
+ string amount (`typeof c.amount === 'number'` is false either way) — a real, live-confirmed way
190
+ to slip a booking's cap untested. Fixed at the template, not just the atom registry.
186
191
 
187
192
  The claim above also checks `agentDid`/`amount`/`currency`/`merchant` together against the
188
193
  request being executed (`@metamynd/agentsafe-mcp-guard` ≥ 0.2.1) — a same-amount, same-currency
package/index.mjs CHANGED
@@ -36,11 +36,12 @@ const MCP_GUARD_VERSION = '^0.3.0';
36
36
  const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
37
37
  // 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
38
38
  // never scaffold a range that could resolve below it.
39
- // 0.3.0 fixes the follow-on gap: the DEFAULT binder only sees flat, top-level, exactly-named
40
- // fields, and silently skipped binding (not blocked) for anything else nested JSON, an array,
41
- // a renamed or differently-cased key. Verified live: a signed $250/skyward-air request executed
42
- // $5000/evil-corp via `{ booking: { amount, merchant } }`. 0.3.0 fails that CLOSED instead.
43
- const GATEWAY_VERSION = '^0.3.0';
39
+ // 0.3.0 was a first, INCOMPLETE attempt at the follow-on gap (checked only "did the body offer
40
+ // NONE of the three fields" a correct decoy in one field let the other hide anywhere). 0.4.0
41
+ // is the actual fix: requires amount/merchant specifically, whenever the signature names a real
42
+ // value for them. Re-tested live and closed same day; ^0.3.0 here would still resolve to the
43
+ // broken version.
44
+ const GATEWAY_VERSION = '^0.4.0';
44
45
  const DEFAULT_API = 'https://metamynd.ai/api/v1';
45
46
  const DEFAULT_GATEWAY_PORT = 4401; // distinct from --harness's dashboard (4400)
46
47
 
@@ -776,7 +777,12 @@ async function bookFlight(args) {
776
777
 
777
778
  // One protected route: only a request signed by this agent, for exactly this action, and
778
779
  // re-verified against this agent's own mandate/SOP, reaches bookFlight() below.
779
- const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}' }];
780
+ //
781
+ // valueFields is explicit on purpose, not left to the gateway's own default (which would be
782
+ // this exact list anyway): a route with a real amount/merchant should always say so itself,
783
+ // rather than relying on a library default to guess right. A route with NO value concept at
784
+ // all (a read, a status check) should set valueFields: [] instead — see the gateway's README.
785
+ const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}', valueFields: ['amount', 'merchant'] }];
780
786
 
781
787
  // No serviceKey: this minimal gateway only calls verifyRequest() (re-check a signed request),
782
788
  // not the mutual-handshake methods, which are the only thing that needs it.
@@ -916,13 +922,18 @@ own code, or a network attacker) might attempt:
916
922
  - **Direct call.** \`bookFlight()\` doesn't exist in the agent's process. There's nothing to call.
917
923
  - **Confused deputy (payload).** The gateway re-verifies the signed request against this agent's
918
924
  own policy AND binds it to the actual request body (payload binding,
919
- \`@metamynd/agentsafe-http-gateway\` ≥ 0.3.0) — signing a cheap request while executing an
920
- expensive one is refused before the tool ever runs. The default binder fails CLOSED not just
921
- on a mismatched flat field but whenever it can't find the governed fields at all — nested
922
- JSON, an array, a renamed or differently-cased key. A signed \$250 request had previously been
923
- able to execute \$5000 via \`{ booking: { amount, merchant } }\`, because the flat matcher found
924
- nothing to compare and treated "nothing found" as "nothing to check" fixed the same way this
925
- list gets fixed: found, closed, named here.
925
+ \`@metamynd/agentsafe-http-gateway\` ≥ 0.4.5) — signing a cheap request while executing an
926
+ expensive one is refused before the tool ever runs. This route's \`valueFields: ['amount',
927
+ 'merchant']\` (see \`server.mjs\`) is an explicit, server-controlled requirement, not a guess
928
+ inferred from anything the signed request itself declares that distinction is what closes
929
+ the full history below, not just the most recent case in it. Earlier attempts checked
930
+ progressively weaker versions of "is this real": 0.3.0 only refused a body offering NONE of
931
+ the governed fields (a correct decoy in one field let the other hide nested, renamed, an
932
+ array, or an entirely empty/non-JSON body); 0.4.0–0.4.2 required a field only when the
933
+ SIGNED request's own value for it looked "real," which a signer could defeat by signing
934
+ \`amount: 0\` — or, identically, by never signing an amount at all, since both verify against
935
+ the exact same canonical message. \`valueFields\` moves the requirement to something the
936
+ signer never controls at all.
926
937
  - **Replay.** \`requireAuthorization: true\` (set in \`server.mjs\`) requires the agent's
927
938
  \`authorizationId\` — from a REAL \`guard.authorize()\` call, which \`index.mjs\` already makes for
928
939
  any value-bearing action by default — to atomically claim single-use execution against the
@@ -931,10 +942,13 @@ own code, or a network attacker) might attempt:
931
942
  already checked it against the mandate's TOTAL budget when it was minted — not just this one
932
943
  request's amount. Many small legal-looking calls can't add up past the mandate cap this way,
933
944
  because each needed its own real authorization first.
934
- - **Amount unknown.** A signed-transaction tool or a nested payload can carry its amount
935
- somewhere a naive spend cap never looks \`amount-unknown\`
936
- (\`@metamynd/agentsafe-mcp-guard\` 0.3.0) blocks by default when the gate can't determine the
937
- value, instead of letting it slip past the cap untested.
945
+ - **Amount unknown.** \`amount-unknown\` (\`@metamynd/agentsafe-mcp-guard\` 0.3.0) blocks a
946
+ platform tool by default when its raw bytes or a nested payload hide the amount from a naive
947
+ spend cap AND this agent's OWN starter SOP (see \`agent.metamynd.json\` /
948
+ \`harness-rules.json\`) puts the same check ahead of its per-transaction cap. That second part
949
+ didn't used to be true: the SOP only ever authored \`amount-over\`, which silently does not fire
950
+ on a missing or string amount, so either one slipped the cap untested — the atom existing
951
+ wasn't the gap, this template never authoring it was.
938
952
 
939
953
  The claim above also checks the claimed authorization's own \`agentDid\`/\`amount\`/\`currency\`/
940
954
  \`merchant\` against the request actually being executed (\`@metamynd/agentsafe-mcp-guard\` ≥ 0.2.1)
@@ -1064,10 +1078,17 @@ async function runSandbox(args) {
1064
1078
 
1065
1079
  /** Mirrors defaultSopDocument() in backend/src/features/onboarding/onboarding.provision.ts —
1066
1080
  * same starter rules the hosted platform issues, so a harness project behaves identically
1067
- * to a freshly-provisioned one before anyone edits either. */
1081
+ * to a freshly-provisioned one before anyone edits either.
1082
+ *
1083
+ * `amount-unknown` first matters MORE here than on the hosted path: evaluateLocally() runs
1084
+ * entirely client-side with no schema boundary in front of it, so nothing stops a caller from
1085
+ * passing amount: "5000" (a string) or omitting amount entirely — `amount-over` silently does
1086
+ * not fire on either (`typeof c.amount === 'number'` is false), so the cap passes untested,
1087
+ * not safe. Ordering amount-unknown first blocks that instead of letting it through. */
1068
1088
  function harnessDefaultSop(perTxnMax) {
1069
1089
  return {
1070
1090
  molecules: [
1091
+ { id: 'amount-known', name: 'Amount must be determinable', combinator: 'any', atoms: [{ id: 'a0', predicate: 'amount-unknown' }], decision: 'block', reasonCode: 'AMOUNT_NOT_DETERMINABLE' },
1071
1092
  { id: 'cap', name: 'Per-transaction cap', combinator: 'any', atoms: [{ id: 'a1', predicate: 'amount-over', config: { limit: perTxnMax } }], decision: 'block', reasonCode: 'SOP_SPEND_CAP' },
1072
1093
  { id: 'review', name: 'High-risk review', combinator: 'any', atoms: [{ id: 'a2', predicate: 'risk-at-or-above', config: { level: 'high' } }], decision: 'escalate', reasonCode: 'RISK_REVIEW' },
1073
1094
  ],
@@ -1139,6 +1160,7 @@ function renderConstraint(c) {
1139
1160
  function renderAtom(a) {
1140
1161
  const c = a.config || {};
1141
1162
  switch (a.predicate) {
1163
+ case 'amount-unknown': return \`transaction amount must be a real, determinable number\`;
1142
1164
  case 'amount-over': return \`transaction amount must not exceed \${c.limit}\`;
1143
1165
  case 'cumulative-over': return \`cumulative spend must not exceed \${c.limit}\`;
1144
1166
  case 'jurisdiction-not-allowed': return \`jurisdiction must be one of [\${(c.allowed || []).join(', ')}]\`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-metamynd-agent",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
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": {