create-metamynd-agent 0.11.2 → 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/index.mjs +100 -6
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -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
|
|
@@ -1739,6 +1763,7 @@ function gatewayServerFile(scope, port, apiBase, policyKey) {
|
|
|
1739
1763
|
// published policy bundle — it does not trust the agent's own guard.guardTool() check. A
|
|
1740
1764
|
// compromised or dishonest agent calling its own local function gets nothing here, because
|
|
1741
1765
|
// there is no local function: the tool only runs in this process.
|
|
1766
|
+
import { readFileSync } from 'node:fs';
|
|
1742
1767
|
import http from 'node:http';
|
|
1743
1768
|
import { createMcpGuard } from '${MCP_GUARD_PKG}';
|
|
1744
1769
|
import { createHttpGateway } from '${GATEWAY_PKG}';
|
|
@@ -1767,8 +1792,13 @@ async function bookFlight(args) {
|
|
|
1767
1792
|
// signed context, not the tool body.
|
|
1768
1793
|
const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}', valueFields: ['amount', 'merchant'], allowedFields: ['amount', 'currency', 'merchant'] }];
|
|
1769
1794
|
|
|
1770
|
-
//
|
|
1771
|
-
//
|
|
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)));
|
|
1772
1802
|
//
|
|
1773
1803
|
// requireAuthorization: true is what closes replay and cumulative spend, not just per-request
|
|
1774
1804
|
// policy — it requires the agent's authorizationId (from a REAL guard.authorize() call) to
|
|
@@ -1779,7 +1809,7 @@ const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}', valu
|
|
|
1779
1809
|
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1780
1810
|
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1781
1811
|
// 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:
|
|
1812
|
+
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
|
|
1783
1813
|
|
|
1784
1814
|
const gateway = createHttpGateway({
|
|
1785
1815
|
guard,
|
|
@@ -1855,7 +1885,7 @@ function gatewayEnvExample() {
|
|
|
1855
1885
|
}
|
|
1856
1886
|
|
|
1857
1887
|
function gatewayGitignore() {
|
|
1858
|
-
return `node_modules/\n.env\n`;
|
|
1888
|
+
return `node_modules/\n.env\nservice.metamynd.json\n`;
|
|
1859
1889
|
}
|
|
1860
1890
|
|
|
1861
1891
|
function gatewayReadme(slug, scope, port) {
|
|
@@ -1906,6 +1936,10 @@ add another protected route here rather than adding a local function back in \`i
|
|
|
1906
1936
|
- \`server.mjs\` — the gateway: one protected route (\`POST /book-flight\`, action \`${scope}\`),
|
|
1907
1937
|
\`@metamynd/agentsafe-mcp-guard\`'s \`verifyRequest()\` re-checking every request, and the real
|
|
1908
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\`.
|
|
1909
1943
|
- \`.env.example\` — where real tool credentials go (copy to \`.env\`, fill in, never commit).
|
|
1910
1944
|
|
|
1911
1945
|
## What this closes, precisely
|
|
@@ -1949,6 +1983,25 @@ own code, or a network attacker) might attempt:
|
|
|
1949
1983
|
the gateway will release that hold. Only the gateway that claimed the hold can do either — the agent
|
|
1950
1984
|
cannot capture it lower or void it, which is what stopped it recovering the budget of a purchase it
|
|
1951
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.
|
|
1952
2005
|
- **Amount unknown.** \`amount-unknown\` (\`@metamynd/agentsafe-mcp-guard\` ≥ 0.3.0) blocks a
|
|
1953
2006
|
platform tool by default when its raw bytes or a nested payload hide the amount from a naive
|
|
1954
2007
|
spend cap — AND this agent's OWN starter SOP (see \`agent.metamynd.json\` /
|
|
@@ -2012,7 +2065,7 @@ function assertScaffoldTarget(outDir, force) {
|
|
|
2012
2065
|
*/
|
|
2013
2066
|
// `demo` (from buildPolicyCases) selects the NON-financial project; without it this is the historical
|
|
2014
2067
|
// payment scaffold. `merchant` defaults to a payment demo's merchant only in that financial branch.
|
|
2015
|
-
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 }) {
|
|
2016
2069
|
const neutral = !!demo;
|
|
2017
2070
|
assertScaffoldTarget(outDir, force);
|
|
2018
2071
|
console.log(`\n ${c.b('Scaffolding')} ${c.dim(outDir)}`);
|
|
@@ -2060,6 +2113,17 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2060
2113
|
const policyKey = config.issuer?.policyKey ?? null;
|
|
2061
2114
|
const gwDir = join(outDir, 'gateway');
|
|
2062
2115
|
if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
|
|
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
|
+
}
|
|
2063
2127
|
writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase, policyKey) : gatewayServerFile(scope, gatewayPort, apiBase, policyKey), force);
|
|
2064
2128
|
writeFileSafe(gwDir, 'package.json', gatewayPackageJson(slug), force);
|
|
2065
2129
|
writeFileSafe(gwDir, '.env.example', neutral ? gatewayEnvExampleNeutral() : gatewayEnvExample(), force);
|
|
@@ -3673,6 +3737,15 @@ async function main() {
|
|
|
3673
3737
|
}
|
|
3674
3738
|
const body = {
|
|
3675
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',
|
|
3676
3749
|
...(financial ? { currency, maxAmount, perTxnMax } : {}),
|
|
3677
3750
|
merchants,
|
|
3678
3751
|
...(publicKey ? { publicKey } : {}),
|
|
@@ -3719,6 +3792,27 @@ async function main() {
|
|
|
3719
3792
|
console.log(c.dim(` POST ${base}/agent-identity/${config.identityId}/verify-key { "signature": "<hex>" } (owner token)`));
|
|
3720
3793
|
}
|
|
3721
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
|
+
|
|
3722
3816
|
// 4. Scaffold + next steps
|
|
3723
3817
|
// The demo comes from the rules actually provisioned: the caller's own, else the backend's default
|
|
3724
3818
|
// for a non-financial agent (the same amount-free review harnessDefaultSopNeutral describes).
|
|
@@ -3727,7 +3821,7 @@ async function main() {
|
|
|
3727
3821
|
console.log(` ${c.green('✓')} derived ${demo.cases.length} demo case(s) from your rules`);
|
|
3728
3822
|
for (const n of demo.notDemonstrated) console.log(` ${c.yellow('!')} not staged in the demo: ${n.rule} ${c.dim('— ' + n.why + '; still enforced')}`);
|
|
3729
3823
|
}
|
|
3730
|
-
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 });
|
|
3731
3825
|
}
|
|
3732
3826
|
|
|
3733
3827
|
// Exported so the smoke tests can exercise the generators directly. Importing this file must not
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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": {
|