create-metamynd-agent 0.12.6 → 0.13.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 +16 -1
  2. package/index.mjs +59 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -325,7 +325,8 @@ with the default hosted flow (login + provision):
325
325
  never faked, still enforced.
326
326
  - **The generated README lists the request fields your rules read** (`consent`, `piiPresent`,
327
327
  `jurisdiction`, …). An allow-list, consent or PII rule does not fire when its field is absent, so
328
- your application must supply it. These fields are asserted by the calling agent.
328
+ your application must supply it. These fields are asserted by the calling agent — except
329
+ `jurisdiction`, which is signed (see [Jurisdiction](#jurisdiction-signed-since-0130)).
329
330
 
330
331
  **Hosted specifics.** The default shape is still agent + `gateway/`. The gateway runs with
331
332
  `requireAuthorization: false` and a route with no value fields: the guard only seals a single-use
@@ -402,6 +403,20 @@ polls until the owner approves, then scaffolds the project (the same default two
402
403
  the full flow above — `--no-gateway`/`--gateway-port` work here too). With `--byok` the keypair is
403
404
  generated locally and control is proven on claim — MetaMynd never sees the private key.
404
405
 
406
+ ## Jurisdiction (signed, since 0.13.0)
407
+
408
+ The scaffolded agents pass a request's `jurisdiction` (ISO 3166-1 alpha-2) as the guard's **signed** top-level field
409
+ (MAGP §8.3.12), not as context: the generated `mapArgs` is `({ jurisdiction, ...context }) => ({ jurisdiction, context })`,
410
+ and the gateway call signs it the same way. The gate (and the harness's local check) judges jurisdiction rules on the
411
+ signed value only and ignores one in the context. Scaffolds pin `@metamynd/agentsafe-guard` ^0.16.0,
412
+ `@metamynd/agentsafe-mcp-guard` ^0.16.0 and `@metamynd/agentsafe-http-gateway` ^0.14.0, which sign and verify it.
413
+
414
+ - A value that is not two ASCII letters is refused before anything is sent (`MALFORMED_REQUEST`).
415
+ - **A registered payee's country wins**: a signed value that differs is refused `JURISDICTION_MISMATCH`.
416
+ - A jurisdiction rule with none signed is refused `JURISDICTION_REQUIRED`; a value off the mandate's list is
417
+ `JURISDICTION_NOT_ALLOWED`. The demo stages a jurisdiction rule with a user-assigned code (`ZZ`) off the list, and
418
+ stages nothing when no two-letter code is on every jurisdiction allow-list (no passing request can exist).
419
+
405
420
  ## Risk: say it, or have your owner set it
406
421
 
407
422
  The starter rules include a high-risk review. A request that sends **no** `riskLevel` (or an unrecognised one)
package/index.mjs CHANGED
@@ -54,7 +54,9 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
54
54
  // requires request inputs blocks a bare baseline request).
55
55
  // 0.12.4 makes `verify` probe in the currency the mandate's caps name (it assumed USD), which a scaffolded
56
56
  // non-USD agent's `npm test` needs.
57
- const GUARD_VERSION = '^0.15.0';
57
+ // 0.16.0 signs an optional top-level `jurisdiction` (MAGP 8.3.12, the v2 message) — the scaffolded non-financial agent
58
+ // passes a request's jurisdiction that way, since the gate no longer reads one from the unsigned context.
59
+ const GUARD_VERSION = '^0.16.0';
58
60
  /** The harness entry point's config load, shared by both harness templates: a fresh clone has no
59
61
  * agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
60
62
  function harnessConfigLoad() {
@@ -109,7 +111,9 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
109
111
  // change (the scaffolded gateways don't pay by x402), but the floor must cover the real current version.
110
112
  // 0.15.0: payloadDigestOf/toWireJson exported and refundAuthorization (its own signed `refund` action). No template
111
113
  // change (the scaffolded gateways don't refund), but the floor must cover the real current version.
112
- const MCP_GUARD_VERSION = '^0.15.0';
114
+ // 0.16.0: verifies a v2 (signed-jurisdiction) request and judges jurisdiction rules on the signed value only. Required:
115
+ // the scaffolded agent now signs jurisdiction, which an older gateway would refuse SIGNATURE_INVALID.
116
+ const MCP_GUARD_VERSION = '^0.16.0';
113
117
  const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
114
118
  // 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
115
119
  // never scaffold a range that could resolve below it.
@@ -124,7 +128,8 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
124
128
  // 0.12.0: per-route `x402: true` declares an x402 payment on the claim. No template change.
125
129
  // 0.13.0: a configured resolveCredential that refuses now FAILS CLOSED (502 CREDENTIAL_UNAVAILABLE) instead of forwarding
126
130
  // without a credential. No template change (the scaffolded gateway does not use the Credential Vault).
127
- const GATEWAY_VERSION = '^0.13.0';
131
+ // 0.14.0: depends on agentsafe-mcp-guard ^0.16.0 (verifies a signed-jurisdiction request). No template change.
132
+ const GATEWAY_VERSION = '^0.14.0';
128
133
  const DEFAULT_API = 'https://metamynd.ai/api/v1';
129
134
  const DEFAULT_GATEWAY_PORT = 4401; // distinct from --harness's dashboard (4400)
130
135
 
@@ -495,7 +500,16 @@ const ATOM_DEMO = {
495
500
  fire: (cfg) => ({ riskLevel: cfg?.level ?? 'high' }),
496
501
  says: (cfg) => 'the risk level is ' + (cfg?.level ?? 'high') + ' or above',
497
502
  },
498
- 'jurisdiction-not-allowed': { field: 'jurisdiction', fire: () => ({ jurisdiction: 'ZZ-NOT-ALLOWED' }), says: () => 'the jurisdiction is not on the allowed list' },
503
+ // The jurisdiction is SIGNED, so it must be a real two-letter code (MAGP 8.3.12): a user-assigned ISO code the list omits.
504
+ 'jurisdiction-not-allowed': {
505
+ field: 'jurisdiction',
506
+ fire: (cfg) => {
507
+ const allowed = new Set((Array.isArray(cfg?.allowed) ? cfg.allowed : []).map((c) => String(c).toUpperCase()));
508
+ const code = ['ZZ', 'XX', 'QZ', 'XZ'].find((c) => !allowed.has(c));
509
+ return code ? { jurisdiction: code } : null;
510
+ },
511
+ says: () => 'the jurisdiction is not on the allowed list',
512
+ },
499
513
  'data-residency-violation': { field: 'dataResidency', fire: () => ({ dataResidency: 'zz-not-allowed' }), says: () => 'the data would be stored outside the allowed regions' },
500
514
  'model-not-allowed': { field: 'model', fire: () => ({ model: 'unlisted-model' }), says: () => 'the model is not on the allowed list' },
501
515
  'tool-not-allowed': { field: 'tool', fire: () => ({ tool: 'unlisted-tool' }), says: () => 'the tool is not on the allowed list' },
@@ -517,6 +531,8 @@ const ATOM_DEMO = {
517
531
  },
518
532
  };
519
533
 
534
+ const TWO_LETTERS = /^[A-Za-z]{2}$/;
535
+
520
536
  // [predicate, request field, config key holding the allow-list, exact (case-sensitive) match?]
521
537
  // data-source-not-approved compares exactly; the other allow-list atoms compare case-insensitively.
522
538
  const ALLOW_LISTS = [
@@ -615,8 +631,23 @@ function buildPolicyCases(molecules, { maxCases = 8 } = {}) {
615
631
  const lists = configsOf(pred).map((cfg) => (Array.isArray(cfg[key]) ? cfg[key].map(String) : []));
616
632
  if (!lists.length || lists.some((l) => l.length === 0)) continue;
617
633
  const norm = (v) => (exact ? v : v.toLowerCase().trim());
618
- const common = lists[0].find((v) => lists.every((l) => l.some((x) => norm(x) === norm(v))));
619
- if (common !== undefined) base[field] = common;
634
+ // The jurisdiction is SIGNED (MAGP 8.3.12), so only a real two-letter code can be sent at all.
635
+ const sendable = (v) => field !== 'jurisdiction' || TWO_LETTERS.test(v.trim());
636
+ const common = lists[0].find((v) => sendable(v) && lists.every((l) => l.some((x) => norm(x) === norm(v))));
637
+ if (common !== undefined) base[field] = field === 'jurisdiction' ? common.trim().toUpperCase() : common;
638
+ }
639
+ // A jurisdiction rule that can refuse makes the field REQUIRED: the gate (and the guard's local check) refuses a request
640
+ // that signs none with JURISDICTION_REQUIRED. With no code on every list, no request can pass — stage nothing, truthfully.
641
+ const jurisdictionEnforced = live.some((m) => m.decision !== 'observe' && m.atoms.some((a) => a.predicate === 'jurisdiction-not-allowed'));
642
+ if (jurisdictionEnforced && base.jurisdiction === undefined) {
643
+ return {
644
+ cases: [],
645
+ inputs,
646
+ notDemonstrated: molecules.map((m) => ({
647
+ rule: label(m),
648
+ why: 'no two-letter jurisdiction is on every jurisdiction allow-list, and a request that signs none is refused JURISDICTION_REQUIRED, so the demo cannot stage a passing request',
649
+ })),
650
+ };
620
651
  }
621
652
  const evidenceCfgs = configsOf('evidence-requirement');
622
653
  if (evidenceCfgs.length) base.evidenceTypes = [...new Set(evidenceCfgs.flatMap((cfg) => (Array.isArray(cfg.required) ? cfg.required.map(String) : [])))];
@@ -1252,7 +1283,8 @@ ${withGateway ? `const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${
1252
1283
  // --- this file to call directly — the tool, and any real credentials it needs, live only in
1253
1284
  // --- ./gateway, which independently re-verifies this signed request itself.
1254
1285
  async function performViaGateway(args, decision) {
1255
- const signed = await guard.buildSignedRequest({ action: '${scope}', merchant: MERCHANT, context: args });
1286
+ const { jurisdiction, ...context } = args; // the jurisdiction is SIGNED (a top-level field), never context
1287
+ const signed = await guard.buildSignedRequest({ action: '${scope}', merchant: MERCHANT, jurisdiction, context });
1256
1288
  signed.authorizationId = decision?.authorizationId; // none for a value-less action — see ./gateway/README.md
1257
1289
  const res = await fetch(GATEWAY + '/perform', {
1258
1290
  method: 'POST',
@@ -1278,11 +1310,12 @@ async function performAction(args) {
1278
1310
  }
1279
1311
  `}
1280
1312
  // --- The GATED version. Register THIS with your agent instead of the raw handler. ---
1281
- // The request's own fields (consent, piiPresent, jurisdiction, ...) travel as the gate's \`context\`.
1313
+ // The request's own fields (consent, piiPresent, ...) travel as the gate's \`context\`. \`jurisdiction\` is the
1314
+ // exception: it is SIGNED (a top-level field, ISO 3166-1 alpha-2) — the gate ignores one in the unsigned context.
1282
1315
  const gatedAction = guard.guardTool(
1283
1316
  '${scope}', // = your mandate scope
1284
1317
  ${withGateway ? 'performViaGateway' : 'performAction'},
1285
- (a) => ({ merchant: MERCHANT, context: a }), // map tool args → gate inputs
1318
+ ({ jurisdiction, ...context }) => ({ merchant: MERCHANT, jurisdiction, context }), // map tool args → gate inputs
1286
1319
  );
1287
1320
 
1288
1321
  // --- An action the agent was NEVER granted. Wrapping it is the demonstration: no rule anywhere
@@ -1512,8 +1545,9 @@ a forged bundle with a higher cap or no rules; \`server.mjs\` refuses an unsigne
1512
1545
 
1513
1546
  **NOT closed — be precise about this:**
1514
1547
 
1515
- - **Rule inputs are not signed.** The request fields your rules read (\`consent\`, \`piiPresent\`,
1516
- \`jurisdiction\`, …) travel in the request's \`itinerary\`, which the signature does not cover, and
1548
+ - **Rule inputs are not signed** — except \`jurisdiction\`, which this agent signs (MAGP 8.3.12; a
1549
+ registered payee's country overrides it at the gate). The other fields your rules read (\`consent\`,
1550
+ \`piiPresent\`, …) travel in the request's \`itinerary\`, which the signature does not cover, and
1517
1551
  the body the tool executes is not bound to it (\`valueFields: []\`). An agent that CAN sign — a
1518
1552
  dishonest one, or a compromised one — can omit a field or send a different one, and the rule that
1519
1553
  reads it will not fire: an allow-list, consent or PII rule treats an absent field as "nothing to
@@ -1601,8 +1635,9 @@ these (the demo in \`index.mjs\` does):
1601
1635
  ${demo.inputs.map((i) => `- \`${i.field}\` — ${i.rules.join(', ')}`).join('\n') || '- (none — your rules read no request fields)'}
1602
1636
 
1603
1637
  **A missing field is not a violation** for an allow-list, consent or PII rule: if your application
1604
- forgets to send \`jurisdiction\` or \`consent\`, that rule simply does not fire. Make sure the field is
1605
- always present. These fields are asserted by the calling agent and are not covered by its signature —
1638
+ forgets to send \`consent\`, that rule simply does not fire. Make sure the field is always present.
1639
+ (\`jurisdiction\` is different: it is signed, and a jurisdiction rule with none signed is refused
1640
+ \`JURISDICTION_REQUIRED\`.) These fields are asserted by the calling agent and are not covered by its signature —
1606
1641
  an agent that can sign could omit or forge one, and the rule would not fire. Source them from a system you
1607
1642
  control, not from model output.${demo.notDemonstrated.length ? `
1608
1643
 
@@ -3100,7 +3135,8 @@ ${withGateway ? `console.log(dim(' gateway : http://localhost:${gatewayPort} (
3100
3135
  // takes effect on the next decision, no restart. The gateway process (when scaffolded) reads the SAME file.
3101
3136
  const getBundle = () => JSON.parse(readFileSync('./metamynd-rules.json', 'utf8'));
3102
3137
 
3103
- // The request's own fields (consent, piiPresent, jurisdiction, ...) travel as the gate's \`context\`.
3138
+ // The request's own fields (consent, piiPresent, ...) travel as the gate's \`context\`. \`jurisdiction\` is the exception:
3139
+ // it is SIGNED (a top-level field, ISO 3166-1 alpha-2) and rules judge only that one, never a context value.
3104
3140
  const MERCHANT = ${merchant ? JSON.stringify(merchant) : 'undefined'};
3105
3141
  ${withGateway ? `const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://localhost:${gatewayPort}';
3106
3142
  // Calls the gateway process instead of a local function — there is no raw performAction() in THIS
@@ -3109,7 +3145,8 @@ ${withGateway ? `const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://loca
3109
3145
  async function callGateway(path, action, args) {
3110
3146
  // The body is empty on purpose (this tool reads nothing from it), and it is signed as the payload so the gateway can
3111
3147
  // require binding on every route: a body with anything in it is not the one signed (MAGP 8.3.9).
3112
- const signed = await guard.buildSignedRequest({ action, merchant: MERCHANT, context: args, payload: {} });
3148
+ const { jurisdiction, ...context } = args;
3149
+ const signed = await guard.buildSignedRequest({ action, merchant: MERCHANT, jurisdiction, context, payload: {} });
3113
3150
  const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: '{}' });
3114
3151
  const body = await res.json().catch(() => null);
3115
3152
  if (!res.ok) {
@@ -3129,7 +3166,7 @@ async function performAction(args) {
3129
3166
  const gatedAction = guard.guardToolLocal(
3130
3167
  '${scope}', // = your mandate scope
3131
3168
  ${withGateway ? `(args) => callGateway('/perform', '${scope}', args)` : 'performAction'},
3132
- (a) => ({ merchant: MERCHANT, context: a }), // map tool args → gate inputs
3169
+ ({ jurisdiction, ...context }) => ({ merchant: MERCHANT, jurisdiction, context }), // map tool args → gate inputs
3133
3170
  getBundle,
3134
3171
  );
3135
3172
 
@@ -3309,8 +3346,9 @@ of these (the demo in \`index.mjs\` does):
3309
3346
  ${demo.inputs.map((i) => `- \`${i.field}\` — ${i.rules.join(', ')}`).join('\n') || '- (none — your rules read no request fields)'}
3310
3347
 
3311
3348
  **A missing field is not a violation** for an allow-list, consent or PII rule: if your application
3312
- forgets to send \`jurisdiction\` or \`consent\`, that rule simply does not fire. Make sure the
3313
- field is always present.${demo.notDemonstrated.length ? `
3349
+ forgets to send \`consent\`, that rule simply does not fire. Make sure the field is always present.
3350
+ (\`jurisdiction\` is different: it is signed, and a jurisdiction rule with none signed is refused
3351
+ \`JURISDICTION_REQUIRED\`.)${demo.notDemonstrated.length ? `
3314
3352
 
3315
3353
  Rules the demo does not stage (they are still enforced):
3316
3354
 
@@ -3335,9 +3373,9 @@ runs, and so is any top-level body key you did not list in that route's \`allowe
3335
3373
  (\`PAYLOAD_UNBINDABLE\`) — nothing signed covers it. When your real tool reads more body fields, list exactly
3336
3374
  those keys; the gateway refuses the rest.${neutral ? `
3337
3375
 
3338
- **Also NOT closed — rule inputs are not signed.** The request fields your rules read (\`consent\`,
3339
- \`piiPresent\`, \`jurisdiction\`, …) travel in the signed request's context, which the signature does
3340
- not cover. An agent that can sign could omit a field or send a different one, and the rule that reads
3376
+ **Also NOT closed — rule inputs are not signed** (except \`jurisdiction\`, a signed top-level field).
3377
+ The other request fields your rules read (\`consent\`, \`piiPresent\`, …) travel in the signed request's
3378
+ context, which the signature does not cover. An agent that can sign could omit a field or send a different one, and the rule that reads
3341
3379
  it would not fire. The gateway enforces scope and identity firmly; an input-dependent rule is only as
3342
3380
  strong as whatever supplies its input.` : ''}
3343
3381
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-metamynd-agent",
3
- "version": "0.12.6",
3
+ "version": "0.13.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": {