create-metamynd-agent 0.10.16 → 0.12.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.
- package/README.md +12 -3
- package/index.mjs +158 -18
- 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() {
|
|
@@ -795,6 +795,30 @@ async function apiPost(base, path, body, token) {
|
|
|
795
795
|
return json;
|
|
796
796
|
}
|
|
797
797
|
|
|
798
|
+
/**
|
|
799
|
+
* POST /policy/counterparties for the gateway's own DID — best-effort, unlike apiPost above: a
|
|
800
|
+
* hosted financial gateway works fine on testnet with no registration at all (open by default), so
|
|
801
|
+
* a failure here (network blip, an older backend without the endpoint) should not abort scaffolding
|
|
802
|
+
* the way a failed agent provisioning does. Always sends confirmEnforcementChange: true — this CLI
|
|
803
|
+
* IS the confirmation (the caller is reading this exact terminal output live, the same way a
|
|
804
|
+
* dashboard registration shows its own dialog before setting the flag); see the printed message
|
|
805
|
+
* either way.
|
|
806
|
+
*/
|
|
807
|
+
async function registerGatewayCounterparty(base, token, did, label) {
|
|
808
|
+
try {
|
|
809
|
+
const res = await fetch(`${base}/policy/counterparties`, {
|
|
810
|
+
method: 'POST',
|
|
811
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
812
|
+
body: JSON.stringify({ did, label, confirmEnforcementChange: true }),
|
|
813
|
+
});
|
|
814
|
+
const json = await res.json().catch(() => null);
|
|
815
|
+
if (!res.ok || json?.success === false) return { ok: false, message: json?.message ?? `HTTP ${res.status}` };
|
|
816
|
+
return { ok: true, message: json?.message ?? 'registered' };
|
|
817
|
+
} catch (e) {
|
|
818
|
+
return { ok: false, message: String(e?.message ?? e) };
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
798
822
|
// ---------- scaffolding ----------
|
|
799
823
|
/**
|
|
800
824
|
* The --no-gateway / --sandbox variant: the tool is a local function in the SAME process as
|
|
@@ -987,18 +1011,23 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
|
|
|
987
1011
|
// --- the gateway atomically claim single-use execution, closing replay + cumulative spend, not
|
|
988
1012
|
// --- just re-checking policy. See ./gateway/README.md.
|
|
989
1013
|
async function bookFlightViaGateway(args, decision) {
|
|
1014
|
+
// The COMPLETE body the tool receives. It is signed as a payload (MAGP 8.3.9): the eight signed fields cover amount and
|
|
1015
|
+
// merchant, not anything else a real tool takes (a payee, a passenger list). The digest covers ALL of it, and the gateway
|
|
1016
|
+
// refuses to run the tool on a body that is not exactly this one.
|
|
1017
|
+
const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
|
|
990
1018
|
const signed = await guard.buildSignedRequest({
|
|
991
1019
|
action: '${scope}',
|
|
992
1020
|
amount: args.amount,
|
|
993
1021
|
currency: args.currency ?? '${currency}',
|
|
994
1022
|
merchant: args.merchant,
|
|
995
1023
|
context: { tool: 'book-flight', riskLevel: args.riskLevel ?? 'low' },
|
|
1024
|
+
payload,
|
|
996
1025
|
});
|
|
997
1026
|
signed.authorizationId = decision?.authorizationId;
|
|
998
1027
|
const res = await fetch(GATEWAY + '/book-flight', {
|
|
999
1028
|
method: 'POST',
|
|
1000
1029
|
headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
|
|
1001
|
-
body: JSON.stringify(
|
|
1030
|
+
body: JSON.stringify(payload),
|
|
1002
1031
|
});
|
|
1003
1032
|
const body = await res.json().catch(() => null);
|
|
1004
1033
|
if (!res.ok) {
|
|
@@ -1020,6 +1049,9 @@ const gatedBookFlight = guard.guardTool(
|
|
|
1020
1049
|
currency: a.currency ?? '${currency}',
|
|
1021
1050
|
merchant: a.merchant,
|
|
1022
1051
|
context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
|
|
1052
|
+
// The authorization is bound to the SAME body bookFlightViaGateway() sends, so the gate records what this agent
|
|
1053
|
+
// signed and the gateway can prove it is running exactly that. Keep the two in step if you add a field.
|
|
1054
|
+
payload: { amount: a.amount, merchant: a.merchant, currency: a.currency ?? '${currency}' },
|
|
1023
1055
|
}),
|
|
1024
1056
|
);
|
|
1025
1057
|
|
|
@@ -1330,7 +1362,7 @@ console.log('');
|
|
|
1330
1362
|
* allowed value-less request would be refused AUTHORIZATION_REQUIRED. The README states what that
|
|
1331
1363
|
* leaves open (replay) rather than implying the financial scaffold's guarantees.
|
|
1332
1364
|
*/
|
|
1333
|
-
function gatewayServerFileNeutral(scope, port, apiBase) {
|
|
1365
|
+
function gatewayServerFileNeutral(scope, port, apiBase, policyKey) {
|
|
1334
1366
|
return `#!/usr/bin/env node
|
|
1335
1367
|
// gateway/server.mjs — the enforcement boundary for this agent's tool(s).
|
|
1336
1368
|
//
|
|
@@ -1369,7 +1401,12 @@ const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFie
|
|
|
1369
1401
|
// would be refused AUTHORIZATION_REQUIRED. What that leaves open (a signed request is reusable for
|
|
1370
1402
|
// the guard's 5-minute freshness window; the issuer's stateful rate/circuit-breaker floors; and the
|
|
1371
1403
|
// rule inputs, which are not covered by the signature) is spelled out in README.md.
|
|
1372
|
-
|
|
1404
|
+
// policyPublicKey pins the bundle to MetaMynd's OWN signing key (from your provisioning response,
|
|
1405
|
+
// baked in here — never fetched at runtime, so a compromised network can't swap it out alongside a
|
|
1406
|
+
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1407
|
+
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1408
|
+
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
1409
|
+
const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
|
|
1373
1410
|
|
|
1374
1411
|
const gateway = createHttpGateway({
|
|
1375
1412
|
guard,
|
|
@@ -1450,7 +1487,10 @@ This is narrower than it sounds, so read it before relying on it.
|
|
|
1450
1487
|
**Closed:** no tool call without a validly signed request from THIS agent for an action in its
|
|
1451
1488
|
mandate. The agent's Ed25519 signature is checked against its DID, the action must be granted, and a
|
|
1452
1489
|
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.
|
|
1490
|
+
the agent gets nothing, and there is no local function to call directly. The policy bundle itself is
|
|
1491
|
+
pinned to MetaMynd's own signing key (\`policyPublicKey\`, baked in from your provisioning response) —
|
|
1492
|
+
a party that can intercept the bundle fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway
|
|
1493
|
+
a forged bundle with a higher cap or no rules; \`server.mjs\` refuses an unsigned or tampered one outright.
|
|
1454
1494
|
|
|
1455
1495
|
**NOT closed — be precise about this:**
|
|
1456
1496
|
|
|
@@ -1608,7 +1648,7 @@ function exampleReadme(slug, scope, withGateway, gatewayPort, daemonKey = false)
|
|
|
1608
1648
|
const configFileLine = daemonKey
|
|
1609
1649
|
? `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
|
|
1610
1650
|
**Holds no secret key.** Signing goes through your already-running agentsafe-signer daemon
|
|
1611
|
-
(\`daemonSocketPath\`) instead — see \`docs/integration/INSTALL-AGENTSAFE-SIGNER.md
|
|
1651
|
+
(\`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
1652
|
: `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
|
|
1613
1653
|
**Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.`;
|
|
1614
1654
|
const gatewaySection = withGateway
|
|
@@ -1714,7 +1754,7 @@ function gitignore() {
|
|
|
1714
1754
|
// in the AgentSafe repo for the full pattern (mutual handshake, x402 payment, capability
|
|
1715
1755
|
// binding) this is a minimal slice of.
|
|
1716
1756
|
|
|
1717
|
-
function gatewayServerFile(scope, port, apiBase) {
|
|
1757
|
+
function gatewayServerFile(scope, port, apiBase, policyKey) {
|
|
1718
1758
|
return `#!/usr/bin/env node
|
|
1719
1759
|
// gateway/server.mjs — the REAL enforcement boundary for this agent's tool(s).
|
|
1720
1760
|
//
|
|
@@ -1723,6 +1763,7 @@ function gatewayServerFile(scope, port, apiBase) {
|
|
|
1723
1763
|
// published policy bundle — it does not trust the agent's own guard.guardTool() check. A
|
|
1724
1764
|
// compromised or dishonest agent calling its own local function gets nothing here, because
|
|
1725
1765
|
// there is no local function: the tool only runs in this process.
|
|
1766
|
+
import { readFileSync } from 'node:fs';
|
|
1726
1767
|
import http from 'node:http';
|
|
1727
1768
|
import { createMcpGuard } from '${MCP_GUARD_PKG}';
|
|
1728
1769
|
import { createHttpGateway } from '${GATEWAY_PKG}';
|
|
@@ -1751,17 +1792,33 @@ async function bookFlight(args) {
|
|
|
1751
1792
|
// signed context, not the tool body.
|
|
1752
1793
|
const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}', valueFields: ['amount', 'merchant'], allowedFields: ['amount', 'currency', 'merchant'] }];
|
|
1753
1794
|
|
|
1754
|
-
//
|
|
1755
|
-
//
|
|
1795
|
+
// This gateway's OWN identity — separate from the agent's on purpose (an agent must not be able to
|
|
1796
|
+
// release or lower-settle a hold it authorized itself; only the SERVICE that claimed it may). Real
|
|
1797
|
+
// and registered as a trusted counterparty when \`npx create-metamynd-agent\` provisioned it (see
|
|
1798
|
+
// README.md); a real serviceKey is what lets serviceAuthHeaders() sign an AUTHENTICATED claim
|
|
1799
|
+
// (MAGP-SERVICE-v1) instead of relying on the bearer claimToken alone — required for a MAINNET
|
|
1800
|
+
// hold (MAGP §8.7.10), and strictly better than an anonymous claim on testnet too.
|
|
1801
|
+
const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', import.meta.url)));
|
|
1756
1802
|
//
|
|
1757
1803
|
// requireAuthorization: true is what closes replay and cumulative spend, not just per-request
|
|
1758
1804
|
// policy — it requires the agent's authorizationId (from a REAL guard.authorize() call) to
|
|
1759
1805
|
// atomically claim single-use execution against the issuer before this gateway runs the tool.
|
|
1760
|
-
|
|
1806
|
+
//
|
|
1807
|
+
// policyPublicKey pins the bundle to MetaMynd's OWN signing key (from your provisioning response,
|
|
1808
|
+
// baked in here — never fetched at runtime, so a compromised network can't swap it out alongside a
|
|
1809
|
+
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1810
|
+
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1811
|
+
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
1812
|
+
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
|
|
1761
1813
|
|
|
1762
1814
|
const gateway = createHttpGateway({
|
|
1763
1815
|
guard,
|
|
1764
1816
|
routes,
|
|
1817
|
+
// Payload binding (MAGP 8.3.9): the agent signs a digest of the WHOLE body, this gateway digests the body it is about to
|
|
1818
|
+
// run, and the issuer refuses the claim unless the two are the digest the agent signed at authorize time. allowedFields
|
|
1819
|
+
// above lists which keys may appear; this makes the VALUES of every one of them (a payee, a passenger list) the agent's too.
|
|
1820
|
+
// A request whose authorization bound no payload is refused (PAYLOAD_BINDING_REQUIRED) rather than run unbound.
|
|
1821
|
+
requirePayloadBinding: true,
|
|
1765
1822
|
forward: async (req) => {
|
|
1766
1823
|
let args = {};
|
|
1767
1824
|
try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
|
|
@@ -1828,7 +1885,7 @@ function gatewayEnvExample() {
|
|
|
1828
1885
|
}
|
|
1829
1886
|
|
|
1830
1887
|
function gatewayGitignore() {
|
|
1831
|
-
return `node_modules/\n.env\n`;
|
|
1888
|
+
return `node_modules/\n.env\nservice.metamynd.json\n`;
|
|
1832
1889
|
}
|
|
1833
1890
|
|
|
1834
1891
|
function gatewayReadme(slug, scope, port) {
|
|
@@ -1879,6 +1936,10 @@ add another protected route here rather than adding a local function back in \`i
|
|
|
1879
1936
|
- \`server.mjs\` — the gateway: one protected route (\`POST /book-flight\`, action \`${scope}\`),
|
|
1880
1937
|
\`@metamynd/agentsafe-mcp-guard\`'s \`verifyRequest()\` re-checking every request, and the real
|
|
1881
1938
|
\`bookFlight()\`.
|
|
1939
|
+
- \`service.metamynd.json\` — this gateway's OWN identity (\`serviceDid\`/\`serviceKey\`), gitignored
|
|
1940
|
+
like \`agent.metamynd.json\` one directory up. \`npx create-metamynd-agent\` generated it and
|
|
1941
|
+
registered its DID as a trusted counterparty (see "Who may claim this agent's holds" below); a
|
|
1942
|
+
clone needs its own, the same way it needs its own \`agent.metamynd.json\`.
|
|
1882
1943
|
- \`.env.example\` — where real tool credentials go (copy to \`.env\`, fill in, never commit).
|
|
1883
1944
|
|
|
1884
1945
|
## What this closes, precisely
|
|
@@ -1887,6 +1948,10 @@ Four independent checks, each closing a different bypass an agent (or anything a
|
|
|
1887
1948
|
own code, or a network attacker) might attempt:
|
|
1888
1949
|
|
|
1889
1950
|
- **Direct call.** \`bookFlight()\` doesn't exist in the agent's process. There's nothing to call.
|
|
1951
|
+
- **Forged policy bundle.** \`server.mjs\` bakes in \`policyPublicKey\` (from your provisioning
|
|
1952
|
+
response) and refuses an unsigned or tampered bundle outright — a party that can intercept the
|
|
1953
|
+
fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway a bundle with a higher cap or
|
|
1954
|
+
no rules and have it trusted the same as the real one.
|
|
1890
1955
|
- **Confused deputy (payload).** The gateway re-verifies the signed request against this agent's
|
|
1891
1956
|
own policy AND binds it to the actual request body (payload binding,
|
|
1892
1957
|
\`@metamynd/agentsafe-http-gateway\` ≥ 0.4.5) — signing a cheap request while executing an
|
|
@@ -1918,6 +1983,25 @@ own code, or a network attacker) might attempt:
|
|
|
1918
1983
|
the gateway will release that hold. Only the gateway that claimed the hold can do either — the agent
|
|
1919
1984
|
cannot capture it lower or void it, which is what stopped it recovering the budget of a purchase it
|
|
1920
1985
|
had just had executed.
|
|
1986
|
+
|
|
1987
|
+
### Who may claim this agent's holds
|
|
1988
|
+
|
|
1989
|
+
This gateway claims with its own real identity (\`service.metamynd.json\`, above) — not the agent's,
|
|
1990
|
+
on purpose: an agent must never be able to release or lower-settle a hold it authorized itself. On
|
|
1991
|
+
provisioning, \`npx create-metamynd-agent\` registered that DID as a trusted counterparty for you
|
|
1992
|
+
(\`POST /policy/counterparties\`). A **MAINNET** hold is *always* registered-only, regardless of your
|
|
1993
|
+
registry (MAGP §8.7.10) — a testnet hold stays open unless you have registered anything at all.
|
|
1994
|
+
Manage the registry at \`/dashboard/counterparties\`, or:
|
|
1995
|
+
|
|
1996
|
+
\`\`\`bash
|
|
1997
|
+
curl -X POST $MAGP_API/policy/counterparties \\
|
|
1998
|
+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \\
|
|
1999
|
+
-d '{ "did": "'"$(node -e "console.log(JSON.parse(require('fs').readFileSync('./service.metamynd.json')).serviceDid)")"'", "confirmEnforcementChange": true }'
|
|
2000
|
+
\`\`\`
|
|
2001
|
+
|
|
2002
|
+
\`confirmEnforcementChange: true\` is required only for your account's FIRST-EVER registration —
|
|
2003
|
+
it switches EVERY one of your holds (not just this agent's) from open to registered-only, so the
|
|
2004
|
+
API refuses a first registration without it.
|
|
1921
2005
|
- **Amount unknown.** \`amount-unknown\` (\`@metamynd/agentsafe-mcp-guard\` ≥ 0.3.0) blocks a
|
|
1922
2006
|
platform tool by default when its raw bytes or a nested payload hide the amount from a naive
|
|
1923
2007
|
spend cap — AND this agent's OWN starter SOP (see \`agent.metamynd.json\` /
|
|
@@ -1981,7 +2065,7 @@ function assertScaffoldTarget(outDir, force) {
|
|
|
1981
2065
|
*/
|
|
1982
2066
|
// `demo` (from buildPolicyCases) selects the NON-financial project; without it this is the historical
|
|
1983
2067
|
// payment scaffold. `merchant` defaults to a payment demo's merchant only in that financial branch.
|
|
1984
|
-
function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'USD', merchant, sandbox, withGateway, gatewayPort = DEFAULT_GATEWAY_PORT, force = false, demo = null }) {
|
|
2068
|
+
function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'USD', merchant, sandbox, withGateway, gatewayPort = DEFAULT_GATEWAY_PORT, force = false, demo = null, gatewayIdentity = null }) {
|
|
1985
2069
|
const neutral = !!demo;
|
|
1986
2070
|
assertScaffoldTarget(outDir, force);
|
|
1987
2071
|
console.log(`\n ${c.b('Scaffolding')} ${c.dim(outDir)}`);
|
|
@@ -2021,9 +2105,26 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2021
2105
|
|
|
2022
2106
|
if (withGateway) {
|
|
2023
2107
|
const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
|
|
2108
|
+
// The key that signs every policy bundle (magp.policy-signer.ts) — present on every provisioning
|
|
2109
|
+
// response that talks to a real backend (full account, sandbox, delegated-claim). Baked into the
|
|
2110
|
+
// generated gateway so it pins the bundle rather than trusting whatever the fetch returns; absent
|
|
2111
|
+
// only for a config predating this field (an older scaffold's saved agent.metamynd.json), where
|
|
2112
|
+
// the generated gateway is unchanged from before rather than baking in nothing and crashing.
|
|
2113
|
+
const policyKey = config.issuer?.policyKey ?? null;
|
|
2024
2114
|
const gwDir = join(outDir, 'gateway');
|
|
2025
2115
|
if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
|
|
2026
|
-
|
|
2116
|
+
if (!neutral) {
|
|
2117
|
+
// The gateway's OWN identity — the one that claims and settles this agent's holds (MAGP §8.7.6/
|
|
2118
|
+
// §8.7.10), separate from the agent's own DID on purpose (an agent must not be able to release or
|
|
2119
|
+
// lower-settle a hold it authorized itself). `gatewayIdentity` is real and registered when the
|
|
2120
|
+
// caller generated + registered one (main()'s hosted-financial path); a caller that didn't
|
|
2121
|
+
// (an older code path, or a direct scaffoldProject() call in a test) falls back to the historical
|
|
2122
|
+
// non-cryptographic placeholder — same shape as before this field existed, still parses and runs,
|
|
2123
|
+
// just unable to claim a MAINNET hold or sign an authenticated claim on any network.
|
|
2124
|
+
const identity = gatewayIdentity ?? { did: `did:local:${scope}-gateway`, keyHex: null };
|
|
2125
|
+
writeFileSafe(gwDir, 'service.metamynd.json', JSON.stringify({ serviceDid: identity.did, serviceKey: identity.keyHex }, null, 2) + '\n', force, 0o600);
|
|
2126
|
+
}
|
|
2127
|
+
writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase, policyKey) : gatewayServerFile(scope, gatewayPort, apiBase, policyKey), force);
|
|
2027
2128
|
writeFileSafe(gwDir, 'package.json', gatewayPackageJson(slug), force);
|
|
2028
2129
|
writeFileSafe(gwDir, '.env.example', neutral ? gatewayEnvExampleNeutral() : gatewayEnvExample(), force);
|
|
2029
2130
|
writeFileSafe(gwDir, '.gitignore', gatewayGitignore(), force);
|
|
@@ -2292,6 +2393,10 @@ const gateway = createHttpGateway({
|
|
|
2292
2393
|
guard,
|
|
2293
2394
|
routes: ROUTES,
|
|
2294
2395
|
denyByDefault: true,
|
|
2396
|
+
// 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
|
|
2397
|
+
// body that is not exactly that one — the values of every allowed field, not only amount and merchant. A request that
|
|
2398
|
+
// bound no payload is refused (PAYLOAD_BINDING_REQUIRED) instead of run unbound.
|
|
2399
|
+
requirePayloadBinding: true,
|
|
2295
2400
|
// Reached ONLY after the guard allowed the request AND the body was bound to what was signed.
|
|
2296
2401
|
forward: async (req) => {
|
|
2297
2402
|
const route = matchRoute(ROUTES, req.method, req.path);
|
|
@@ -2751,8 +2856,11 @@ const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://localhost:${gatewayPo
|
|
|
2751
2856
|
// issuer): it builds and signs the same canonical message a real gate would verify, entirely
|
|
2752
2857
|
// offline, using this agent's own did:key — the gateway verifies that signature for itself.
|
|
2753
2858
|
async function callGateway(path, action, args) {
|
|
2754
|
-
|
|
2755
|
-
|
|
2859
|
+
// The COMPLETE body the tool receives, signed as a payload (MAGP 8.3.9): the eight signed fields cover amount and merchant
|
|
2860
|
+
// only, and this gateway refuses to run the tool on a body that is not exactly the one signed here.
|
|
2861
|
+
const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
|
|
2862
|
+
const signed = await guard.buildSignedRequest({ action, amount: args.amount, currency: args.currency ?? '${currency}', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' }, payload });
|
|
2863
|
+
const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: JSON.stringify(payload) });
|
|
2756
2864
|
const body = await res.json().catch(() => null);
|
|
2757
2865
|
if (!res.ok) {
|
|
2758
2866
|
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
@@ -2953,7 +3061,9 @@ ${withGateway ? `const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://loca
|
|
|
2953
3061
|
// file to call directly. buildSignedRequest() is pure (no network, no issuer): it signs the request
|
|
2954
3062
|
// offline with this agent's own did:key, and the gateway verifies that signature for itself.
|
|
2955
3063
|
async function callGateway(path, action, args) {
|
|
2956
|
-
|
|
3064
|
+
// The body is empty on purpose (this tool reads nothing from it), and it is signed as the payload so the gateway can
|
|
3065
|
+
// require binding on every route: a body with anything in it is not the one signed (MAGP 8.3.9).
|
|
3066
|
+
const signed = await guard.buildSignedRequest({ action, merchant: MERCHANT, context: args, payload: {} });
|
|
2957
3067
|
const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: '{}' });
|
|
2958
3068
|
const body = await res.json().catch(() => null);
|
|
2959
3069
|
if (!res.ok) {
|
|
@@ -3627,6 +3737,15 @@ async function main() {
|
|
|
3627
3737
|
}
|
|
3628
3738
|
const body = {
|
|
3629
3739
|
name, scope,
|
|
3740
|
+
// Explicit, not left to the backend's own eligibility-based default (mainnet for a KYB-verified
|
|
3741
|
+
// owner, testnet otherwise) — an owner who happens to be verified would otherwise silently get a
|
|
3742
|
+
// REAL mainnet agent from a plain `npx create-metamynd-agent` with no signal that anything
|
|
3743
|
+
// changed. The generated financial gateway CAN now satisfy mainnet's registry enforcement (a
|
|
3744
|
+
// real, registered did:key — see the gateway identity step below), but this CLI has no flag to
|
|
3745
|
+
// opt into mainnet yet; that is a separate decision (exposing it safely — KYB status, spend
|
|
3746
|
+
// caps — is more than a network toggle) from giving the gateway a real identity, which is what
|
|
3747
|
+
// this fixes. docs/design/release-blockers-open-items.md, "Registry default".
|
|
3748
|
+
network: 'testnet',
|
|
3630
3749
|
...(financial ? { currency, maxAmount, perTxnMax } : {}),
|
|
3631
3750
|
merchants,
|
|
3632
3751
|
...(publicKey ? { publicKey } : {}),
|
|
@@ -3673,6 +3792,27 @@ async function main() {
|
|
|
3673
3792
|
console.log(c.dim(` POST ${base}/agent-identity/${config.identityId}/verify-key { "signature": "<hex>" } (owner token)`));
|
|
3674
3793
|
}
|
|
3675
3794
|
|
|
3795
|
+
// 3c. The gateway's OWN identity — a real did:key, separate from the agent's own (an agent must
|
|
3796
|
+
// never be able to release or lower-settle a hold it authorized itself; only the SERVICE that
|
|
3797
|
+
// claimed it may). Only the financial, with-gateway shape ever claims a hold at all; the
|
|
3798
|
+
// non-financial gateway never calls claimAuthorization() (requireAuthorization is off there), so
|
|
3799
|
+
// it has no counterparty identity to register.
|
|
3800
|
+
let gatewayIdentity = null;
|
|
3801
|
+
if (financial && !args['no-gateway']) {
|
|
3802
|
+
const gwKeypair = generateAgentKeypair();
|
|
3803
|
+
const gatewayDid = buildDidKey(rawPublicKeyFromSpkiHex(gwKeypair.publicKeyHex));
|
|
3804
|
+
gatewayIdentity = { did: gatewayDid, keyHex: gwKeypair.privateKeyHex };
|
|
3805
|
+
console.log(c.dim(`\n → registering the gateway's identity as a trusted counterparty (so it can claim this agent's holds) …`));
|
|
3806
|
+
const reg = await registerGatewayCounterparty(base, token, gatewayDid, `${scope}-gateway`);
|
|
3807
|
+
if (reg.ok) {
|
|
3808
|
+
console.log(` ${c.green('✓')} gateway DID ${c.b(gatewayDid)} — ${reg.message}`);
|
|
3809
|
+
} else {
|
|
3810
|
+
console.log(` ${c.yellow('!')} could not register the gateway as a trusted counterparty: ${reg.message}`);
|
|
3811
|
+
console.log(c.dim(` it still works on testnet (open by default with no registry entries); a MAINNET hold cannot be`));
|
|
3812
|
+
console.log(c.dim(` claimed until it is registered — POST ${base}/policy/counterparties { "did": "${gatewayDid}", "confirmEnforcementChange": true }`));
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3676
3816
|
// 4. Scaffold + next steps
|
|
3677
3817
|
// The demo comes from the rules actually provisioned: the caller's own, else the backend's default
|
|
3678
3818
|
// for a non-financial agent (the same amount-free review harnessDefaultSopNeutral describes).
|
|
@@ -3681,7 +3821,7 @@ async function main() {
|
|
|
3681
3821
|
console.log(` ${c.green('✓')} derived ${demo.cases.length} demo case(s) from your rules`);
|
|
3682
3822
|
for (const n of demo.notDemonstrated) console.log(` ${c.yellow('!')} not staged in the demo: ${n.rule} ${c.dim('— ' + n.why + '; still enforced')}`);
|
|
3683
3823
|
}
|
|
3684
|
-
scaffoldProject({ demo, outDir, config, slug, scope, perTxnMax, currency, merchant: financial ? merchants[0] || 'demo-merchant' : merchants[0], sandbox: false, withGateway: !args['no-gateway'], gatewayPort: Number(args['gateway-port']) || DEFAULT_GATEWAY_PORT, force: !!args.force });
|
|
3824
|
+
scaffoldProject({ demo, outDir, config, slug, scope, perTxnMax, currency, merchant: financial ? merchants[0] || 'demo-merchant' : merchants[0], sandbox: false, withGateway: !args['no-gateway'], gatewayPort: Number(args['gateway-port']) || DEFAULT_GATEWAY_PORT, force: !!args.force, gatewayIdentity });
|
|
3685
3825
|
}
|
|
3686
3826
|
|
|
3687
3827
|
// Exported so the smoke tests can exercise the generators directly. Importing this file must not
|
|
@@ -3689,6 +3829,6 @@ async function main() {
|
|
|
3689
3829
|
export {
|
|
3690
3830
|
buildPolicyCases, resolveFinancial, policyMolecules, ruleToMolecule,
|
|
3691
3831
|
generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
|
|
3692
|
-
scaffoldProject, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
|
|
3832
|
+
scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
|
|
3693
3833
|
};
|
|
3694
3834
|
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.12.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": {
|
|
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",
|