create-metamynd-agent 0.10.16 → 0.11.2
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 +12 -3
- package/index.mjs +59 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@ default, a second `gateway/` process** built on
|
|
|
10
10
|
The agent's own `guardTool()` call is a fast, local, client-side check; the gateway is the real
|
|
11
11
|
enforcement boundary — see [Separate tool gateway](#separate-tool-gateway-default) below.
|
|
12
12
|
|
|
13
|
-
> **Prerequisite:** an agent is always owned by a
|
|
14
|
-
>
|
|
15
|
-
>
|
|
13
|
+
> **Prerequisite:** an agent is always owned by a person/org with a **MetaMynd account**. Verification
|
|
14
|
+
> is *reported*, not required, to get started: an unverified owner works on Testnet within the
|
|
15
|
+
> free-tier limits, and verifying once in the dashboard (KYC/KYB) is what unlocks mainnet.
|
|
16
16
|
|
|
17
17
|
## Free local harness (no account, no network, `--harness`)
|
|
18
18
|
|
|
@@ -170,6 +170,15 @@ the whole protocol.
|
|
|
170
170
|
Named precisely, not left implicit:
|
|
171
171
|
|
|
172
172
|
- **Direct call.** `bookFlight()` doesn't exist in the agent's process.
|
|
173
|
+
- **Payload binding is ON by default (0.11.0, MAGP §8.3.9).** The generated agent signs the COMPLETE body it sends as a payload, the
|
|
174
|
+
authorization it gets from the gate is bound to that same body, and the generated gateway sets `requirePayloadBinding: true`: it
|
|
175
|
+
digests the body it is about to run and the issuer refuses the claim unless that is the digest the agent signed. The eight
|
|
176
|
+
signed fields only ever covered amount and merchant; this covers the value of every field the route allows (a payee, a passenger
|
|
177
|
+
list you add). A request that bound no payload is refused (`PAYLOAD_BINDING_REQUIRED`) rather than run unbound. If you add a body
|
|
178
|
+
field, add it to `allowedFields` **and** to the `payload` the agent signs (both sit next to each other in the generated files).
|
|
179
|
+
A scaffold that signs through the signer daemon (`--byok --daemon-socket`) needs `@metamynd/agentsafe-signer` 0.15.0 or later. The
|
|
180
|
+
hosted non-financial scaffold reads nothing from its body, so there is nothing for it to bind, and a scaffold with no gateway
|
|
181
|
+
(`--no-gateway`, or `--sandbox`'s in-process demo) has no separate executor to hold to the digest, so it stays as it was.
|
|
173
182
|
- **Confused deputy (payload).** Signing a cheap request while executing an expensive one (a
|
|
174
183
|
different amount/merchant in the body than what was signed) is refused before the tool runs —
|
|
175
184
|
payload binding (`@metamynd/agentsafe-http-gateway` ≥ 0.4.0). The default binder requires
|
package/index.mjs
CHANGED
|
@@ -53,7 +53,7 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
|
|
|
53
53
|
// requires request inputs blocks a bare baseline request).
|
|
54
54
|
// 0.12.4 makes `verify` probe in the currency the mandate's caps name (it assumed USD), which a scaffolded
|
|
55
55
|
// non-USD agent's `npm test` needs.
|
|
56
|
-
const GUARD_VERSION = '^0.
|
|
56
|
+
const GUARD_VERSION = '^0.15.0';
|
|
57
57
|
/** The harness entry point's config load, shared by both harness templates: a fresh clone has no
|
|
58
58
|
* agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
|
|
59
59
|
function harnessConfigLoad() {
|
|
@@ -987,18 +987,23 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
|
|
|
987
987
|
// --- the gateway atomically claim single-use execution, closing replay + cumulative spend, not
|
|
988
988
|
// --- just re-checking policy. See ./gateway/README.md.
|
|
989
989
|
async function bookFlightViaGateway(args, decision) {
|
|
990
|
+
// The COMPLETE body the tool receives. It is signed as a payload (MAGP 8.3.9): the eight signed fields cover amount and
|
|
991
|
+
// merchant, not anything else a real tool takes (a payee, a passenger list). The digest covers ALL of it, and the gateway
|
|
992
|
+
// refuses to run the tool on a body that is not exactly this one.
|
|
993
|
+
const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
|
|
990
994
|
const signed = await guard.buildSignedRequest({
|
|
991
995
|
action: '${scope}',
|
|
992
996
|
amount: args.amount,
|
|
993
997
|
currency: args.currency ?? '${currency}',
|
|
994
998
|
merchant: args.merchant,
|
|
995
999
|
context: { tool: 'book-flight', riskLevel: args.riskLevel ?? 'low' },
|
|
1000
|
+
payload,
|
|
996
1001
|
});
|
|
997
1002
|
signed.authorizationId = decision?.authorizationId;
|
|
998
1003
|
const res = await fetch(GATEWAY + '/book-flight', {
|
|
999
1004
|
method: 'POST',
|
|
1000
1005
|
headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
|
|
1001
|
-
body: JSON.stringify(
|
|
1006
|
+
body: JSON.stringify(payload),
|
|
1002
1007
|
});
|
|
1003
1008
|
const body = await res.json().catch(() => null);
|
|
1004
1009
|
if (!res.ok) {
|
|
@@ -1020,6 +1025,9 @@ const gatedBookFlight = guard.guardTool(
|
|
|
1020
1025
|
currency: a.currency ?? '${currency}',
|
|
1021
1026
|
merchant: a.merchant,
|
|
1022
1027
|
context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
|
|
1028
|
+
// The authorization is bound to the SAME body bookFlightViaGateway() sends, so the gate records what this agent
|
|
1029
|
+
// signed and the gateway can prove it is running exactly that. Keep the two in step if you add a field.
|
|
1030
|
+
payload: { amount: a.amount, merchant: a.merchant, currency: a.currency ?? '${currency}' },
|
|
1023
1031
|
}),
|
|
1024
1032
|
);
|
|
1025
1033
|
|
|
@@ -1330,7 +1338,7 @@ console.log('');
|
|
|
1330
1338
|
* allowed value-less request would be refused AUTHORIZATION_REQUIRED. The README states what that
|
|
1331
1339
|
* leaves open (replay) rather than implying the financial scaffold's guarantees.
|
|
1332
1340
|
*/
|
|
1333
|
-
function gatewayServerFileNeutral(scope, port, apiBase) {
|
|
1341
|
+
function gatewayServerFileNeutral(scope, port, apiBase, policyKey) {
|
|
1334
1342
|
return `#!/usr/bin/env node
|
|
1335
1343
|
// gateway/server.mjs — the enforcement boundary for this agent's tool(s).
|
|
1336
1344
|
//
|
|
@@ -1369,7 +1377,12 @@ const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFie
|
|
|
1369
1377
|
// would be refused AUTHORIZATION_REQUIRED. What that leaves open (a signed request is reusable for
|
|
1370
1378
|
// the guard's 5-minute freshness window; the issuer's stateful rate/circuit-breaker floors; and the
|
|
1371
1379
|
// rule inputs, which are not covered by the signature) is spelled out in README.md.
|
|
1372
|
-
|
|
1380
|
+
// policyPublicKey pins the bundle to MetaMynd's OWN signing key (from your provisioning response,
|
|
1381
|
+
// baked in here — never fetched at runtime, so a compromised network can't swap it out alongside a
|
|
1382
|
+
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1383
|
+
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1384
|
+
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
1385
|
+
const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
|
|
1373
1386
|
|
|
1374
1387
|
const gateway = createHttpGateway({
|
|
1375
1388
|
guard,
|
|
@@ -1450,7 +1463,10 @@ This is narrower than it sounds, so read it before relying on it.
|
|
|
1450
1463
|
**Closed:** no tool call without a validly signed request from THIS agent for an action in its
|
|
1451
1464
|
mandate. The agent's Ed25519 signature is checked against its DID, the action must be granted, and a
|
|
1452
1465
|
suspended or quarantined agent is refused. Code that skips the agent's own guard and cannot sign as
|
|
1453
|
-
the agent gets nothing, and there is no local function to call directly.
|
|
1466
|
+
the agent gets nothing, and there is no local function to call directly. The policy bundle itself is
|
|
1467
|
+
pinned to MetaMynd's own signing key (\`policyPublicKey\`, baked in from your provisioning response) —
|
|
1468
|
+
a party that can intercept the bundle fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway
|
|
1469
|
+
a forged bundle with a higher cap or no rules; \`server.mjs\` refuses an unsigned or tampered one outright.
|
|
1454
1470
|
|
|
1455
1471
|
**NOT closed — be precise about this:**
|
|
1456
1472
|
|
|
@@ -1608,7 +1624,7 @@ function exampleReadme(slug, scope, withGateway, gatewayPort, daemonKey = false)
|
|
|
1608
1624
|
const configFileLine = daemonKey
|
|
1609
1625
|
? `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
|
|
1610
1626
|
**Holds no secret key.** Signing goes through your already-running agentsafe-signer daemon
|
|
1611
|
-
(\`daemonSocketPath\`) instead — see \`docs/integration/INSTALL-AGENTSAFE-SIGNER.md
|
|
1627
|
+
(\`daemonSocketPath\`) instead — see \`docs/integration/INSTALL-AGENTSAFE-SIGNER.md\`.${withGateway ? ' Payload binding is on by default and signs through the daemon too, which needs agentsafe-signer 0.15.0 or later.' : ''}`
|
|
1612
1628
|
: `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
|
|
1613
1629
|
**Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.`;
|
|
1614
1630
|
const gatewaySection = withGateway
|
|
@@ -1714,7 +1730,7 @@ function gitignore() {
|
|
|
1714
1730
|
// in the AgentSafe repo for the full pattern (mutual handshake, x402 payment, capability
|
|
1715
1731
|
// binding) this is a minimal slice of.
|
|
1716
1732
|
|
|
1717
|
-
function gatewayServerFile(scope, port, apiBase) {
|
|
1733
|
+
function gatewayServerFile(scope, port, apiBase, policyKey) {
|
|
1718
1734
|
return `#!/usr/bin/env node
|
|
1719
1735
|
// gateway/server.mjs — the REAL enforcement boundary for this agent's tool(s).
|
|
1720
1736
|
//
|
|
@@ -1757,11 +1773,22 @@ const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}', valu
|
|
|
1757
1773
|
// requireAuthorization: true is what closes replay and cumulative spend, not just per-request
|
|
1758
1774
|
// policy — it requires the agent's authorizationId (from a REAL guard.authorize() call) to
|
|
1759
1775
|
// atomically claim single-use execution against the issuer before this gateway runs the tool.
|
|
1760
|
-
|
|
1776
|
+
//
|
|
1777
|
+
// policyPublicKey pins the bundle to MetaMynd's OWN signing key (from your provisioning response,
|
|
1778
|
+
// baked in here — never fetched at runtime, so a compromised network can't swap it out alongside a
|
|
1779
|
+
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1780
|
+
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1781
|
+
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
1782
|
+
const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: true${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
|
|
1761
1783
|
|
|
1762
1784
|
const gateway = createHttpGateway({
|
|
1763
1785
|
guard,
|
|
1764
1786
|
routes,
|
|
1787
|
+
// Payload binding (MAGP 8.3.9): the agent signs a digest of the WHOLE body, this gateway digests the body it is about to
|
|
1788
|
+
// run, and the issuer refuses the claim unless the two are the digest the agent signed at authorize time. allowedFields
|
|
1789
|
+
// above lists which keys may appear; this makes the VALUES of every one of them (a payee, a passenger list) the agent's too.
|
|
1790
|
+
// A request whose authorization bound no payload is refused (PAYLOAD_BINDING_REQUIRED) rather than run unbound.
|
|
1791
|
+
requirePayloadBinding: true,
|
|
1765
1792
|
forward: async (req) => {
|
|
1766
1793
|
let args = {};
|
|
1767
1794
|
try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
|
|
@@ -1887,6 +1914,10 @@ Four independent checks, each closing a different bypass an agent (or anything a
|
|
|
1887
1914
|
own code, or a network attacker) might attempt:
|
|
1888
1915
|
|
|
1889
1916
|
- **Direct call.** \`bookFlight()\` doesn't exist in the agent's process. There's nothing to call.
|
|
1917
|
+
- **Forged policy bundle.** \`server.mjs\` bakes in \`policyPublicKey\` (from your provisioning
|
|
1918
|
+
response) and refuses an unsigned or tampered bundle outright — a party that can intercept the
|
|
1919
|
+
fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway a bundle with a higher cap or
|
|
1920
|
+
no rules and have it trusted the same as the real one.
|
|
1890
1921
|
- **Confused deputy (payload).** The gateway re-verifies the signed request against this agent's
|
|
1891
1922
|
own policy AND binds it to the actual request body (payload binding,
|
|
1892
1923
|
\`@metamynd/agentsafe-http-gateway\` ≥ 0.4.5) — signing a cheap request while executing an
|
|
@@ -2021,9 +2052,15 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2021
2052
|
|
|
2022
2053
|
if (withGateway) {
|
|
2023
2054
|
const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
|
|
2055
|
+
// The key that signs every policy bundle (magp.policy-signer.ts) — present on every provisioning
|
|
2056
|
+
// response that talks to a real backend (full account, sandbox, delegated-claim). Baked into the
|
|
2057
|
+
// generated gateway so it pins the bundle rather than trusting whatever the fetch returns; absent
|
|
2058
|
+
// only for a config predating this field (an older scaffold's saved agent.metamynd.json), where
|
|
2059
|
+
// the generated gateway is unchanged from before rather than baking in nothing and crashing.
|
|
2060
|
+
const policyKey = config.issuer?.policyKey ?? null;
|
|
2024
2061
|
const gwDir = join(outDir, 'gateway');
|
|
2025
2062
|
if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
|
|
2026
|
-
writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase) : gatewayServerFile(scope, gatewayPort, apiBase), force);
|
|
2063
|
+
writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase, policyKey) : gatewayServerFile(scope, gatewayPort, apiBase, policyKey), force);
|
|
2027
2064
|
writeFileSafe(gwDir, 'package.json', gatewayPackageJson(slug), force);
|
|
2028
2065
|
writeFileSafe(gwDir, '.env.example', neutral ? gatewayEnvExampleNeutral() : gatewayEnvExample(), force);
|
|
2029
2066
|
writeFileSafe(gwDir, '.gitignore', gatewayGitignore(), force);
|
|
@@ -2292,6 +2329,10 @@ const gateway = createHttpGateway({
|
|
|
2292
2329
|
guard,
|
|
2293
2330
|
routes: ROUTES,
|
|
2294
2331
|
denyByDefault: true,
|
|
2332
|
+
// Payload binding (MAGP 8.3.9): the agent signs a digest of the WHOLE body and this gateway refuses to run a tool on a
|
|
2333
|
+
// body that is not exactly that one — the values of every allowed field, not only amount and merchant. A request that
|
|
2334
|
+
// bound no payload is refused (PAYLOAD_BINDING_REQUIRED) instead of run unbound.
|
|
2335
|
+
requirePayloadBinding: true,
|
|
2295
2336
|
// Reached ONLY after the guard allowed the request AND the body was bound to what was signed.
|
|
2296
2337
|
forward: async (req) => {
|
|
2297
2338
|
const route = matchRoute(ROUTES, req.method, req.path);
|
|
@@ -2751,8 +2792,11 @@ const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://localhost:${gatewayPo
|
|
|
2751
2792
|
// issuer): it builds and signs the same canonical message a real gate would verify, entirely
|
|
2752
2793
|
// offline, using this agent's own did:key — the gateway verifies that signature for itself.
|
|
2753
2794
|
async function callGateway(path, action, args) {
|
|
2754
|
-
|
|
2755
|
-
|
|
2795
|
+
// The COMPLETE body the tool receives, signed as a payload (MAGP 8.3.9): the eight signed fields cover amount and merchant
|
|
2796
|
+
// only, and this gateway refuses to run the tool on a body that is not exactly the one signed here.
|
|
2797
|
+
const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
|
|
2798
|
+
const signed = await guard.buildSignedRequest({ action, amount: args.amount, currency: args.currency ?? '${currency}', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' }, payload });
|
|
2799
|
+
const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: JSON.stringify(payload) });
|
|
2756
2800
|
const body = await res.json().catch(() => null);
|
|
2757
2801
|
if (!res.ok) {
|
|
2758
2802
|
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
@@ -2953,7 +2997,9 @@ ${withGateway ? `const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://loca
|
|
|
2953
2997
|
// file to call directly. buildSignedRequest() is pure (no network, no issuer): it signs the request
|
|
2954
2998
|
// offline with this agent's own did:key, and the gateway verifies that signature for itself.
|
|
2955
2999
|
async function callGateway(path, action, args) {
|
|
2956
|
-
|
|
3000
|
+
// The body is empty on purpose (this tool reads nothing from it), and it is signed as the payload so the gateway can
|
|
3001
|
+
// require binding on every route: a body with anything in it is not the one signed (MAGP 8.3.9).
|
|
3002
|
+
const signed = await guard.buildSignedRequest({ action, merchant: MERCHANT, context: args, payload: {} });
|
|
2957
3003
|
const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: '{}' });
|
|
2958
3004
|
const body = await res.json().catch(() => null);
|
|
2959
3005
|
if (!res.ok) {
|
|
@@ -3689,6 +3735,6 @@ async function main() {
|
|
|
3689
3735
|
export {
|
|
3690
3736
|
buildPolicyCases, resolveFinancial, policyMolecules, ruleToMolecule,
|
|
3691
3737
|
generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
|
|
3692
|
-
scaffoldProject, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
|
|
3738
|
+
scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
|
|
3693
3739
|
};
|
|
3694
3740
|
if (!process.env.CREATE_METAMYND_AGENT_NO_MAIN) main().catch((e) => fail(e?.stack || e?.message || String(e)));
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.2",
|
|
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": {
|
|
7
7
|
"create-metamynd-agent": "index.mjs"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "node harness-gateway.smoke.mjs && node harness-gateway-bind.smoke.mjs && node policy-demo.smoke.mjs && node hosted-nonfinancial.smoke.mjs && node modes-nonfinancial.smoke.mjs && node byok-daemon.smoke.mjs"
|
|
10
|
+
"test": "node generated-code-parses.smoke.mjs && node harness-gateway.smoke.mjs && node harness-gateway-bind.smoke.mjs && node policy-demo.smoke.mjs && node hosted-nonfinancial.smoke.mjs && node modes-nonfinancial.smoke.mjs && node byok-daemon.smoke.mjs"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"index.mjs",
|