create-metamynd-agent 0.12.6 → 0.13.1
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 +16 -1
- package/index.mjs +66 -22
- 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,11 @@ 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
|
-
|
|
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
|
+
// 0.17.0 signs the agent's context (envelopeSignature, MAGP 8.3.13) by default; the scaffolded agent gets it with no template
|
|
60
|
+
// change (signContext: false opts out). A daemon-custody agent needs agentsafe-signer >= 0.19.0 for its no-amount requests.
|
|
61
|
+
const GUARD_VERSION = '^0.17.0';
|
|
58
62
|
/** The harness entry point's config load, shared by both harness templates: a fresh clone has no
|
|
59
63
|
* agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
|
|
60
64
|
function harnessConfigLoad() {
|
|
@@ -109,7 +113,11 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
|
109
113
|
// change (the scaffolded gateways don't pay by x402), but the floor must cover the real current version.
|
|
110
114
|
// 0.15.0: payloadDigestOf/toWireJson exported and refundAuthorization (its own signed `refund` action). No template
|
|
111
115
|
// change (the scaffolded gateways don't refund), but the floor must cover the real current version.
|
|
112
|
-
|
|
116
|
+
// 0.16.0: verifies a v2 (signed-jurisdiction) request and judges jurisdiction rules on the signed value only. Required:
|
|
117
|
+
// the scaffolded agent now signs jurisdiction, which an older gateway would refuse SIGNATURE_INVALID.
|
|
118
|
+
// 0.17.0: verifies the agent's context signature (envelopeSignature) before judging its itinerary, and a
|
|
119
|
+
// requireContextSignature option. No template change (the scaffolded agent does not sign its context by default).
|
|
120
|
+
const MCP_GUARD_VERSION = '^0.17.0';
|
|
113
121
|
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
114
122
|
// 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
|
|
115
123
|
// never scaffold a range that could resolve below it.
|
|
@@ -124,7 +132,10 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
|
124
132
|
// 0.12.0: per-route `x402: true` declares an x402 payment on the claim. No template change.
|
|
125
133
|
// 0.13.0: a configured resolveCredential that refuses now FAILS CLOSED (502 CREDENTIAL_UNAVAILABLE) instead of forwarding
|
|
126
134
|
// without a credential. No template change (the scaffolded gateway does not use the Credential Vault).
|
|
127
|
-
|
|
135
|
+
// 0.14.0: depends on agentsafe-mcp-guard ^0.16.0 (verifies a signed-jurisdiction request). No template change.
|
|
136
|
+
// 0.15.0: depends on agentsafe-mcp-guard ^0.17.0 (context signature verified) and a requireContextSignature option.
|
|
137
|
+
// No template change.
|
|
138
|
+
const GATEWAY_VERSION = '^0.15.0';
|
|
128
139
|
const DEFAULT_API = 'https://metamynd.ai/api/v1';
|
|
129
140
|
const DEFAULT_GATEWAY_PORT = 4401; // distinct from --harness's dashboard (4400)
|
|
130
141
|
|
|
@@ -495,7 +506,16 @@ const ATOM_DEMO = {
|
|
|
495
506
|
fire: (cfg) => ({ riskLevel: cfg?.level ?? 'high' }),
|
|
496
507
|
says: (cfg) => 'the risk level is ' + (cfg?.level ?? 'high') + ' or above',
|
|
497
508
|
},
|
|
498
|
-
|
|
509
|
+
// 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.
|
|
510
|
+
'jurisdiction-not-allowed': {
|
|
511
|
+
field: 'jurisdiction',
|
|
512
|
+
fire: (cfg) => {
|
|
513
|
+
const allowed = new Set((Array.isArray(cfg?.allowed) ? cfg.allowed : []).map((c) => String(c).toUpperCase()));
|
|
514
|
+
const code = ['ZZ', 'XX', 'QZ', 'XZ'].find((c) => !allowed.has(c));
|
|
515
|
+
return code ? { jurisdiction: code } : null;
|
|
516
|
+
},
|
|
517
|
+
says: () => 'the jurisdiction is not on the allowed list',
|
|
518
|
+
},
|
|
499
519
|
'data-residency-violation': { field: 'dataResidency', fire: () => ({ dataResidency: 'zz-not-allowed' }), says: () => 'the data would be stored outside the allowed regions' },
|
|
500
520
|
'model-not-allowed': { field: 'model', fire: () => ({ model: 'unlisted-model' }), says: () => 'the model is not on the allowed list' },
|
|
501
521
|
'tool-not-allowed': { field: 'tool', fire: () => ({ tool: 'unlisted-tool' }), says: () => 'the tool is not on the allowed list' },
|
|
@@ -517,6 +537,8 @@ const ATOM_DEMO = {
|
|
|
517
537
|
},
|
|
518
538
|
};
|
|
519
539
|
|
|
540
|
+
const TWO_LETTERS = /^[A-Za-z]{2}$/;
|
|
541
|
+
|
|
520
542
|
// [predicate, request field, config key holding the allow-list, exact (case-sensitive) match?]
|
|
521
543
|
// data-source-not-approved compares exactly; the other allow-list atoms compare case-insensitively.
|
|
522
544
|
const ALLOW_LISTS = [
|
|
@@ -615,8 +637,23 @@ function buildPolicyCases(molecules, { maxCases = 8 } = {}) {
|
|
|
615
637
|
const lists = configsOf(pred).map((cfg) => (Array.isArray(cfg[key]) ? cfg[key].map(String) : []));
|
|
616
638
|
if (!lists.length || lists.some((l) => l.length === 0)) continue;
|
|
617
639
|
const norm = (v) => (exact ? v : v.toLowerCase().trim());
|
|
618
|
-
|
|
619
|
-
|
|
640
|
+
// The jurisdiction is SIGNED (MAGP 8.3.12), so only a real two-letter code can be sent at all.
|
|
641
|
+
const sendable = (v) => field !== 'jurisdiction' || TWO_LETTERS.test(v.trim());
|
|
642
|
+
const common = lists[0].find((v) => sendable(v) && lists.every((l) => l.some((x) => norm(x) === norm(v))));
|
|
643
|
+
if (common !== undefined) base[field] = field === 'jurisdiction' ? common.trim().toUpperCase() : common;
|
|
644
|
+
}
|
|
645
|
+
// A jurisdiction rule that can refuse makes the field REQUIRED: the gate (and the guard's local check) refuses a request
|
|
646
|
+
// that signs none with JURISDICTION_REQUIRED. With no code on every list, no request can pass — stage nothing, truthfully.
|
|
647
|
+
const jurisdictionEnforced = live.some((m) => m.decision !== 'observe' && m.atoms.some((a) => a.predicate === 'jurisdiction-not-allowed'));
|
|
648
|
+
if (jurisdictionEnforced && base.jurisdiction === undefined) {
|
|
649
|
+
return {
|
|
650
|
+
cases: [],
|
|
651
|
+
inputs,
|
|
652
|
+
notDemonstrated: molecules.map((m) => ({
|
|
653
|
+
rule: label(m),
|
|
654
|
+
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',
|
|
655
|
+
})),
|
|
656
|
+
};
|
|
620
657
|
}
|
|
621
658
|
const evidenceCfgs = configsOf('evidence-requirement');
|
|
622
659
|
if (evidenceCfgs.length) base.evidenceTypes = [...new Set(evidenceCfgs.flatMap((cfg) => (Array.isArray(cfg.required) ? cfg.required.map(String) : [])))];
|
|
@@ -1252,7 +1289,8 @@ ${withGateway ? `const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${
|
|
|
1252
1289
|
// --- this file to call directly — the tool, and any real credentials it needs, live only in
|
|
1253
1290
|
// --- ./gateway, which independently re-verifies this signed request itself.
|
|
1254
1291
|
async function performViaGateway(args, decision) {
|
|
1255
|
-
const
|
|
1292
|
+
const { jurisdiction, ...context } = args; // the jurisdiction is SIGNED (a top-level field), never context
|
|
1293
|
+
const signed = await guard.buildSignedRequest({ action: '${scope}', merchant: MERCHANT, jurisdiction, context });
|
|
1256
1294
|
signed.authorizationId = decision?.authorizationId; // none for a value-less action — see ./gateway/README.md
|
|
1257
1295
|
const res = await fetch(GATEWAY + '/perform', {
|
|
1258
1296
|
method: 'POST',
|
|
@@ -1278,11 +1316,12 @@ async function performAction(args) {
|
|
|
1278
1316
|
}
|
|
1279
1317
|
`}
|
|
1280
1318
|
// --- The GATED version. Register THIS with your agent instead of the raw handler. ---
|
|
1281
|
-
// The request's own fields (consent, piiPresent,
|
|
1319
|
+
// The request's own fields (consent, piiPresent, ...) travel as the gate's \`context\`. \`jurisdiction\` is the
|
|
1320
|
+
// exception: it is SIGNED (a top-level field, ISO 3166-1 alpha-2) — the gate ignores one in the unsigned context.
|
|
1282
1321
|
const gatedAction = guard.guardTool(
|
|
1283
1322
|
'${scope}', // = your mandate scope
|
|
1284
1323
|
${withGateway ? 'performViaGateway' : 'performAction'},
|
|
1285
|
-
(
|
|
1324
|
+
({ jurisdiction, ...context }) => ({ merchant: MERCHANT, jurisdiction, context }), // map tool args → gate inputs
|
|
1286
1325
|
);
|
|
1287
1326
|
|
|
1288
1327
|
// --- An action the agent was NEVER granted. Wrapping it is the demonstration: no rule anywhere
|
|
@@ -1512,8 +1551,9 @@ a forged bundle with a higher cap or no rules; \`server.mjs\` refuses an unsigne
|
|
|
1512
1551
|
|
|
1513
1552
|
**NOT closed — be precise about this:**
|
|
1514
1553
|
|
|
1515
|
-
- **Rule inputs are not signed
|
|
1516
|
-
|
|
1554
|
+
- **Rule inputs are not signed** — except \`jurisdiction\`, which this agent signs (MAGP 8.3.12; a
|
|
1555
|
+
registered payee's country overrides it at the gate). The other fields your rules read (\`consent\`,
|
|
1556
|
+
\`piiPresent\`, …) travel in the request's \`itinerary\`, which the signature does not cover, and
|
|
1517
1557
|
the body the tool executes is not bound to it (\`valueFields: []\`). An agent that CAN sign — a
|
|
1518
1558
|
dishonest one, or a compromised one — can omit a field or send a different one, and the rule that
|
|
1519
1559
|
reads it will not fire: an allow-list, consent or PII rule treats an absent field as "nothing to
|
|
@@ -1601,8 +1641,9 @@ these (the demo in \`index.mjs\` does):
|
|
|
1601
1641
|
${demo.inputs.map((i) => `- \`${i.field}\` — ${i.rules.join(', ')}`).join('\n') || '- (none — your rules read no request fields)'}
|
|
1602
1642
|
|
|
1603
1643
|
**A missing field is not a violation** for an allow-list, consent or PII rule: if your application
|
|
1604
|
-
forgets to send \`
|
|
1605
|
-
|
|
1644
|
+
forgets to send \`consent\`, that rule simply does not fire. Make sure the field is always present.
|
|
1645
|
+
(\`jurisdiction\` is different: it is signed, and a jurisdiction rule with none signed is refused
|
|
1646
|
+
\`JURISDICTION_REQUIRED\`.) These fields are asserted by the calling agent and are not covered by its signature —
|
|
1606
1647
|
an agent that can sign could omit or forge one, and the rule would not fire. Source them from a system you
|
|
1607
1648
|
control, not from model output.${demo.notDemonstrated.length ? `
|
|
1608
1649
|
|
|
@@ -1666,7 +1707,7 @@ function exampleReadme(slug, scope, withGateway, gatewayPort, daemonKey = false)
|
|
|
1666
1707
|
const configFileLine = daemonKey
|
|
1667
1708
|
? `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
|
|
1668
1709
|
**Holds no secret key.** Signing goes through your already-running agentsafe-signer daemon
|
|
1669
|
-
(\`daemonSocketPath\`) instead — see \`docs/integration/INSTALL-AGENTSAFE-SIGNER.md
|
|
1710
|
+
(\`daemonSocketPath\`) instead — see \`docs/integration/INSTALL-AGENTSAFE-SIGNER.md\`. Context signing (on by default) signs through the daemon too; use agentsafe-signer 0.19.0 or later (an older one refuses this agent's no-amount requests with CONTEXT_SIGNING_UNSUPPORTED).${withGateway ? ' Payload binding is on by default and signs through the daemon too, which needs agentsafe-signer 0.15.0 or later.' : ''}`
|
|
1670
1711
|
: `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
|
|
1671
1712
|
**Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.`;
|
|
1672
1713
|
const gatewaySection = withGateway
|
|
@@ -3100,7 +3141,8 @@ ${withGateway ? `console.log(dim(' gateway : http://localhost:${gatewayPort} (
|
|
|
3100
3141
|
// takes effect on the next decision, no restart. The gateway process (when scaffolded) reads the SAME file.
|
|
3101
3142
|
const getBundle = () => JSON.parse(readFileSync('./metamynd-rules.json', 'utf8'));
|
|
3102
3143
|
|
|
3103
|
-
// The request's own fields (consent, piiPresent,
|
|
3144
|
+
// The request's own fields (consent, piiPresent, ...) travel as the gate's \`context\`. \`jurisdiction\` is the exception:
|
|
3145
|
+
// it is SIGNED (a top-level field, ISO 3166-1 alpha-2) and rules judge only that one, never a context value.
|
|
3104
3146
|
const MERCHANT = ${merchant ? JSON.stringify(merchant) : 'undefined'};
|
|
3105
3147
|
${withGateway ? `const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://localhost:${gatewayPort}';
|
|
3106
3148
|
// Calls the gateway process instead of a local function — there is no raw performAction() in THIS
|
|
@@ -3109,7 +3151,8 @@ ${withGateway ? `const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://loca
|
|
|
3109
3151
|
async function callGateway(path, action, args) {
|
|
3110
3152
|
// The body is empty on purpose (this tool reads nothing from it), and it is signed as the payload so the gateway can
|
|
3111
3153
|
// require binding on every route: a body with anything in it is not the one signed (MAGP 8.3.9).
|
|
3112
|
-
const
|
|
3154
|
+
const { jurisdiction, ...context } = args;
|
|
3155
|
+
const signed = await guard.buildSignedRequest({ action, merchant: MERCHANT, jurisdiction, context, payload: {} });
|
|
3113
3156
|
const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: '{}' });
|
|
3114
3157
|
const body = await res.json().catch(() => null);
|
|
3115
3158
|
if (!res.ok) {
|
|
@@ -3129,7 +3172,7 @@ async function performAction(args) {
|
|
|
3129
3172
|
const gatedAction = guard.guardToolLocal(
|
|
3130
3173
|
'${scope}', // = your mandate scope
|
|
3131
3174
|
${withGateway ? `(args) => callGateway('/perform', '${scope}', args)` : 'performAction'},
|
|
3132
|
-
(
|
|
3175
|
+
({ jurisdiction, ...context }) => ({ merchant: MERCHANT, jurisdiction, context }), // map tool args → gate inputs
|
|
3133
3176
|
getBundle,
|
|
3134
3177
|
);
|
|
3135
3178
|
|
|
@@ -3309,8 +3352,9 @@ of these (the demo in \`index.mjs\` does):
|
|
|
3309
3352
|
${demo.inputs.map((i) => `- \`${i.field}\` — ${i.rules.join(', ')}`).join('\n') || '- (none — your rules read no request fields)'}
|
|
3310
3353
|
|
|
3311
3354
|
**A missing field is not a violation** for an allow-list, consent or PII rule: if your application
|
|
3312
|
-
forgets to send \`
|
|
3313
|
-
|
|
3355
|
+
forgets to send \`consent\`, that rule simply does not fire. Make sure the field is always present.
|
|
3356
|
+
(\`jurisdiction\` is different: it is signed, and a jurisdiction rule with none signed is refused
|
|
3357
|
+
\`JURISDICTION_REQUIRED\`.)${demo.notDemonstrated.length ? `
|
|
3314
3358
|
|
|
3315
3359
|
Rules the demo does not stage (they are still enforced):
|
|
3316
3360
|
|
|
@@ -3335,9 +3379,9 @@ runs, and so is any top-level body key you did not list in that route's \`allowe
|
|
|
3335
3379
|
(\`PAYLOAD_UNBINDABLE\`) — nothing signed covers it. When your real tool reads more body fields, list exactly
|
|
3336
3380
|
those keys; the gateway refuses the rest.${neutral ? `
|
|
3337
3381
|
|
|
3338
|
-
**Also NOT closed — rule inputs are not signed
|
|
3339
|
-
\`
|
|
3340
|
-
not cover. An agent that can sign could omit a field or send a different one, and the rule that reads
|
|
3382
|
+
**Also NOT closed — rule inputs are not signed** (except \`jurisdiction\`, a signed top-level field).
|
|
3383
|
+
The other request fields your rules read (\`consent\`, \`piiPresent\`, …) travel in the signed request's
|
|
3384
|
+
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
3385
|
it would not fire. The gateway enforces scope and identity firmly; an input-dependent rule is only as
|
|
3342
3386
|
strong as whatever supplies its input.` : ''}
|
|
3343
3387
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
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": {
|