create-metamynd-agent 0.11.2 → 0.12.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 +2 -0
- package/index.mjs +159 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ enforcement boundary — see [Separate tool gateway](#separate-tool-gateway-defa
|
|
|
13
13
|
> **Prerequisite:** an agent is always owned by a person/org with a **MetaMynd account**. Verification
|
|
14
14
|
> is *reported*, not required, to get started: an unverified owner works on Testnet within the
|
|
15
15
|
> free-tier limits, and verifying once in the dashboard (KYC/KYB) is what unlocks mainnet.
|
|
16
|
+
> This CLI always provisions **Testnet** agents; a mainnet agent is launched from the dashboard's
|
|
17
|
+
> **Launchpad** (`/dashboard/launchpad`) once you are verified.
|
|
16
18
|
|
|
17
19
|
## Free local harness (no account, no network, `--harness`)
|
|
18
20
|
|
package/index.mjs
CHANGED
|
@@ -102,7 +102,7 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
|
102
102
|
// 0.6.0 brings buildAuthMessage's `resource` field and buildLocalDecisionMessage into this
|
|
103
103
|
// package's own bundled policy-core.mjs (alongside the guard's own 0.10.0) — no scaffolded
|
|
104
104
|
// template code changes, but the floor must still cover the real current version.
|
|
105
|
-
const MCP_GUARD_VERSION = '^0.
|
|
105
|
+
const MCP_GUARD_VERSION = '^0.12.0';
|
|
106
106
|
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
107
107
|
// 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
|
|
108
108
|
// never scaffold a range that could resolve below it.
|
|
@@ -205,7 +205,7 @@ ${c.b('Options')}
|
|
|
205
205
|
--gateway --harness only: ALSO scaffold a second local process (still zero
|
|
206
206
|
network, zero account) that independently re-verifies every request
|
|
207
207
|
against the same rules file, using the real @metamynd/agentsafe-mcp-guard.
|
|
208
|
-
Off by default.
|
|
208
|
+
Off by default. Refuses a replayed request; does not track cumulative spend — see the
|
|
209
209
|
generated README#--gateway for exactly what it does and does not.
|
|
210
210
|
--gateway-port <n> The gateway process's port, hosted flow or --harness --gateway (default 4401)
|
|
211
211
|
--port <n> --harness only: the local dashboard's port (default 4400)
|
|
@@ -216,6 +216,12 @@ ${c.b('Options')}
|
|
|
216
216
|
${c.b('Environment')}
|
|
217
217
|
METAMYND_API, METAMYND_EMAIL, METAMYND_PASSWORD — fallbacks for the flags above
|
|
218
218
|
|
|
219
|
+
${c.b('Network')}
|
|
220
|
+
Every hosted agent this CLI provisions is a ${c.b('Testnet')} agent, whatever your verification status —
|
|
221
|
+
there is deliberately no mainnet flag. To launch a ${c.b('Mainnet')} agent, use the Launchpad in the
|
|
222
|
+
dashboard (https://metamynd.ai/dashboard/launchpad): its Mainnet option is available once your
|
|
223
|
+
owner verification (KYC/KYB) is complete.
|
|
224
|
+
|
|
219
225
|
${c.b('What it does')}
|
|
220
226
|
1. Logs in as a KYB-verified owner → owner access token
|
|
221
227
|
2. POST /onboarding/agent (one call) → identity + mandate + SOP + Standards
|
|
@@ -795,6 +801,30 @@ async function apiPost(base, path, body, token) {
|
|
|
795
801
|
return json;
|
|
796
802
|
}
|
|
797
803
|
|
|
804
|
+
/**
|
|
805
|
+
* POST /policy/counterparties for the gateway's own DID — best-effort, unlike apiPost above: a
|
|
806
|
+
* hosted financial gateway works fine on testnet with no registration at all (open by default), so
|
|
807
|
+
* a failure here (network blip, an older backend without the endpoint) should not abort scaffolding
|
|
808
|
+
* the way a failed agent provisioning does. Always sends confirmEnforcementChange: true — this CLI
|
|
809
|
+
* IS the confirmation (the caller is reading this exact terminal output live, the same way a
|
|
810
|
+
* dashboard registration shows its own dialog before setting the flag); see the printed message
|
|
811
|
+
* either way.
|
|
812
|
+
*/
|
|
813
|
+
async function registerGatewayCounterparty(base, token, did, label) {
|
|
814
|
+
try {
|
|
815
|
+
const res = await fetch(`${base}/policy/counterparties`, {
|
|
816
|
+
method: 'POST',
|
|
817
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
818
|
+
body: JSON.stringify({ did, label, confirmEnforcementChange: true }),
|
|
819
|
+
});
|
|
820
|
+
const json = await res.json().catch(() => null);
|
|
821
|
+
if (!res.ok || json?.success === false) return { ok: false, message: json?.message ?? `HTTP ${res.status}` };
|
|
822
|
+
return { ok: true, message: json?.message ?? 'registered' };
|
|
823
|
+
} catch (e) {
|
|
824
|
+
return { ok: false, message: String(e?.message ?? e) };
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
798
828
|
// ---------- scaffolding ----------
|
|
799
829
|
/**
|
|
800
830
|
* The --no-gateway / --sandbox variant: the tool is a local function in the SAME process as
|
|
@@ -1739,6 +1769,7 @@ function gatewayServerFile(scope, port, apiBase, policyKey) {
|
|
|
1739
1769
|
// published policy bundle — it does not trust the agent's own guard.guardTool() check. A
|
|
1740
1770
|
// compromised or dishonest agent calling its own local function gets nothing here, because
|
|
1741
1771
|
// there is no local function: the tool only runs in this process.
|
|
1772
|
+
import { readFileSync } from 'node:fs';
|
|
1742
1773
|
import http from 'node:http';
|
|
1743
1774
|
import { createMcpGuard } from '${MCP_GUARD_PKG}';
|
|
1744
1775
|
import { createHttpGateway } from '${GATEWAY_PKG}';
|
|
@@ -1767,8 +1798,13 @@ async function bookFlight(args) {
|
|
|
1767
1798
|
// signed context, not the tool body.
|
|
1768
1799
|
const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}', valueFields: ['amount', 'merchant'], allowedFields: ['amount', 'currency', 'merchant'] }];
|
|
1769
1800
|
|
|
1770
|
-
//
|
|
1771
|
-
//
|
|
1801
|
+
// This gateway's OWN identity — separate from the agent's on purpose (an agent must not be able to
|
|
1802
|
+
// release or lower-settle a hold it authorized itself; only the SERVICE that claimed it may). Real
|
|
1803
|
+
// and registered as a trusted counterparty when \`npx create-metamynd-agent\` provisioned it (see
|
|
1804
|
+
// README.md); a real serviceKey is what lets serviceAuthHeaders() sign an AUTHENTICATED claim
|
|
1805
|
+
// (MAGP-SERVICE-v1) instead of relying on the bearer claimToken alone — required for a MAINNET
|
|
1806
|
+
// hold (MAGP §8.7.10), and strictly better than an anonymous claim on testnet too.
|
|
1807
|
+
const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', import.meta.url)));
|
|
1772
1808
|
//
|
|
1773
1809
|
// requireAuthorization: true is what closes replay and cumulative spend, not just per-request
|
|
1774
1810
|
// policy — it requires the agent's authorizationId (from a REAL guard.authorize() call) to
|
|
@@ -1779,7 +1815,7 @@ const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}', valu
|
|
|
1779
1815
|
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1780
1816
|
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1781
1817
|
// 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:
|
|
1818
|
+
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
|
|
1783
1819
|
|
|
1784
1820
|
const gateway = createHttpGateway({
|
|
1785
1821
|
guard,
|
|
@@ -1855,7 +1891,7 @@ function gatewayEnvExample() {
|
|
|
1855
1891
|
}
|
|
1856
1892
|
|
|
1857
1893
|
function gatewayGitignore() {
|
|
1858
|
-
return `node_modules/\n.env\n`;
|
|
1894
|
+
return `node_modules/\n.env\nservice.metamynd.json\n`;
|
|
1859
1895
|
}
|
|
1860
1896
|
|
|
1861
1897
|
function gatewayReadme(slug, scope, port) {
|
|
@@ -1906,6 +1942,10 @@ add another protected route here rather than adding a local function back in \`i
|
|
|
1906
1942
|
- \`server.mjs\` — the gateway: one protected route (\`POST /book-flight\`, action \`${scope}\`),
|
|
1907
1943
|
\`@metamynd/agentsafe-mcp-guard\`'s \`verifyRequest()\` re-checking every request, and the real
|
|
1908
1944
|
\`bookFlight()\`.
|
|
1945
|
+
- \`service.metamynd.json\` — this gateway's OWN identity (\`serviceDid\`/\`serviceKey\`), gitignored
|
|
1946
|
+
like \`agent.metamynd.json\` one directory up. \`npx create-metamynd-agent\` generated it and
|
|
1947
|
+
registered its DID as a trusted counterparty (see "Who may claim this agent's holds" below); a
|
|
1948
|
+
clone needs its own, the same way it needs its own \`agent.metamynd.json\`.
|
|
1909
1949
|
- \`.env.example\` — where real tool credentials go (copy to \`.env\`, fill in, never commit).
|
|
1910
1950
|
|
|
1911
1951
|
## What this closes, precisely
|
|
@@ -1949,6 +1989,25 @@ own code, or a network attacker) might attempt:
|
|
|
1949
1989
|
the gateway will release that hold. Only the gateway that claimed the hold can do either — the agent
|
|
1950
1990
|
cannot capture it lower or void it, which is what stopped it recovering the budget of a purchase it
|
|
1951
1991
|
had just had executed.
|
|
1992
|
+
|
|
1993
|
+
### Who may claim this agent's holds
|
|
1994
|
+
|
|
1995
|
+
This gateway claims with its own real identity (\`service.metamynd.json\`, above) — not the agent's,
|
|
1996
|
+
on purpose: an agent must never be able to release or lower-settle a hold it authorized itself. On
|
|
1997
|
+
provisioning, \`npx create-metamynd-agent\` registered that DID as a trusted counterparty for you
|
|
1998
|
+
(\`POST /policy/counterparties\`). A **MAINNET** hold is *always* registered-only, regardless of your
|
|
1999
|
+
registry (MAGP §8.7.10) — a testnet hold stays open unless you have registered anything at all.
|
|
2000
|
+
Manage the registry at \`/dashboard/counterparties\`, or:
|
|
2001
|
+
|
|
2002
|
+
\`\`\`bash
|
|
2003
|
+
curl -X POST $MAGP_API/policy/counterparties \\
|
|
2004
|
+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \\
|
|
2005
|
+
-d '{ "did": "'"$(node -e "console.log(JSON.parse(require('fs').readFileSync('./service.metamynd.json')).serviceDid)")"'", "confirmEnforcementChange": true }'
|
|
2006
|
+
\`\`\`
|
|
2007
|
+
|
|
2008
|
+
\`confirmEnforcementChange: true\` is required only for your account's FIRST-EVER registration —
|
|
2009
|
+
it switches EVERY one of your holds (not just this agent's) from open to registered-only, so the
|
|
2010
|
+
API refuses a first registration without it.
|
|
1952
2011
|
- **Amount unknown.** \`amount-unknown\` (\`@metamynd/agentsafe-mcp-guard\` ≥ 0.3.0) blocks a
|
|
1953
2012
|
platform tool by default when its raw bytes or a nested payload hide the amount from a naive
|
|
1954
2013
|
spend cap — AND this agent's OWN starter SOP (see \`agent.metamynd.json\` /
|
|
@@ -2012,7 +2071,7 @@ function assertScaffoldTarget(outDir, force) {
|
|
|
2012
2071
|
*/
|
|
2013
2072
|
// `demo` (from buildPolicyCases) selects the NON-financial project; without it this is the historical
|
|
2014
2073
|
// 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 }) {
|
|
2074
|
+
function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'USD', merchant, sandbox, withGateway, gatewayPort = DEFAULT_GATEWAY_PORT, force = false, demo = null, gatewayIdentity = null }) {
|
|
2016
2075
|
const neutral = !!demo;
|
|
2017
2076
|
assertScaffoldTarget(outDir, force);
|
|
2018
2077
|
console.log(`\n ${c.b('Scaffolding')} ${c.dim(outDir)}`);
|
|
@@ -2060,6 +2119,17 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2060
2119
|
const policyKey = config.issuer?.policyKey ?? null;
|
|
2061
2120
|
const gwDir = join(outDir, 'gateway');
|
|
2062
2121
|
if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
|
|
2122
|
+
if (!neutral) {
|
|
2123
|
+
// The gateway's OWN identity — the one that claims and settles this agent's holds (MAGP §8.7.6/
|
|
2124
|
+
// §8.7.10), separate from the agent's own DID on purpose (an agent must not be able to release or
|
|
2125
|
+
// lower-settle a hold it authorized itself). `gatewayIdentity` is real and registered when the
|
|
2126
|
+
// caller generated + registered one (main()'s hosted-financial path); a caller that didn't
|
|
2127
|
+
// (an older code path, or a direct scaffoldProject() call in a test) falls back to the historical
|
|
2128
|
+
// non-cryptographic placeholder — same shape as before this field existed, still parses and runs,
|
|
2129
|
+
// just unable to claim a MAINNET hold or sign an authenticated claim on any network.
|
|
2130
|
+
const identity = gatewayIdentity ?? { did: `did:local:${scope}-gateway`, keyHex: null };
|
|
2131
|
+
writeFileSafe(gwDir, 'service.metamynd.json', JSON.stringify({ serviceDid: identity.did, serviceKey: identity.keyHex }, null, 2) + '\n', force, 0o600);
|
|
2132
|
+
}
|
|
2063
2133
|
writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase, policyKey) : gatewayServerFile(scope, gatewayPort, apiBase, policyKey), force);
|
|
2064
2134
|
writeFileSafe(gwDir, 'package.json', gatewayPackageJson(slug), force);
|
|
2065
2135
|
writeFileSafe(gwDir, '.env.example', neutral ? gatewayEnvExampleNeutral() : gatewayEnvExample(), force);
|
|
@@ -2255,12 +2325,14 @@ function harnessRulesFile(mandate, sopDocument) {
|
|
|
2255
2325
|
// that skips its own guardToolLocal() call, or calls bookFlight() directly, gets nothing —
|
|
2256
2326
|
// the tool only runs in the gateway process now.
|
|
2257
2327
|
//
|
|
2328
|
+
// Replay of the SAME signed request is refused too (REPLAY_DETECTED): the gateway remembers each nonce for as long as
|
|
2329
|
+
// the guard would accept it, in memory (a restart forgets them). An independent tester's rerun (2026-09-24) found the
|
|
2330
|
+
// same bytes accepted twice before this.
|
|
2331
|
+
//
|
|
2258
2332
|
// What this does NOT close (be precise, this is a local demo, not the hosted platform):
|
|
2259
|
-
//
|
|
2260
|
-
//
|
|
2261
|
-
//
|
|
2262
|
-
// this gateway does per-request re-evaluation only, same as the hosted gateway's baseline
|
|
2263
|
-
// before `requireAuthorization` is added. The README says so.
|
|
2333
|
+
// cumulative spend across many DIFFERENT calls. That needs a STATEFUL authority — the hosted gate's
|
|
2334
|
+
// `requireAuthorization` claims a real, single-use authorizationId against a database and holds the budget there. A
|
|
2335
|
+
// local harness has no such database (that is the whole point of --harness). The README says so.
|
|
2264
2336
|
|
|
2265
2337
|
function harnessGatewayServerFile(scope, gatewayPort, agentDid, neutral = false) {
|
|
2266
2338
|
return `#!/usr/bin/env node
|
|
@@ -2351,9 +2423,35 @@ function readBody(req) {
|
|
|
2351
2423
|
});
|
|
2352
2424
|
}
|
|
2353
2425
|
|
|
2426
|
+
// Replay: every signed request carries a fresh nonce, and the guard refuses one more than 5 minutes old. So a nonce
|
|
2427
|
+
// only has to be remembered for that long to refuse the SAME signed request twice. Checked and recorded in one
|
|
2428
|
+
// synchronous step, before anything awaits, so two simultaneous copies cannot both get through. In memory: restarting
|
|
2429
|
+
// this process forgets it (a replay inside the 5-minute window after a restart is not caught). Cumulative spend across
|
|
2430
|
+
// many DIFFERENT requests is still not tracked here — that needs the hosted gate.
|
|
2431
|
+
const NONCE_TTL_MS = 6 * 60 * 1000;
|
|
2432
|
+
const seenNonces = new Map(); // agentDid|nonce -> expiry
|
|
2433
|
+
function firstUseOfNonce(headers) {
|
|
2434
|
+
let signed;
|
|
2435
|
+
try { signed = JSON.parse(headers['x-magp-request'] ?? 'null'); } catch { return true; } // malformed: the guard refuses it
|
|
2436
|
+
if (!signed || typeof signed.nonce !== 'string') return true; // no nonce: the guard refuses it
|
|
2437
|
+
const now = Date.now();
|
|
2438
|
+
if (seenNonces.size > 10000) for (const [k, exp] of seenNonces) if (exp <= now) seenNonces.delete(k);
|
|
2439
|
+
const key = String(signed.agentDid) + '|' + signed.nonce;
|
|
2440
|
+
const exp = seenNonces.get(key);
|
|
2441
|
+
if (exp !== undefined && exp > now) return false;
|
|
2442
|
+
seenNonces.set(key, now + NONCE_TTL_MS);
|
|
2443
|
+
return true;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2354
2446
|
const server = http.createServer(async (req, res) => {
|
|
2355
2447
|
try {
|
|
2356
2448
|
const rawBody = await readBody(req);
|
|
2449
|
+
if (!firstUseOfNonce(req.headers)) {
|
|
2450
|
+
console.log('[harness-gateway] BLOCK ' + req.url + ' — REPLAY_DETECTED (this exact signed request already came through)');
|
|
2451
|
+
res.writeHead(403, { 'content-type': 'application/json', 'x-agentsafe-decision': 'block' });
|
|
2452
|
+
res.end(JSON.stringify({ decision: 'block', reasonCode: 'REPLAY_DETECTED' }));
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2357
2455
|
const result = await gateway({ method: req.method, path: req.url, headers: req.headers, rawBody });
|
|
2358
2456
|
const headers = { 'content-type': 'application/json' };
|
|
2359
2457
|
const decision = result.governance?.decision;
|
|
@@ -2914,9 +3012,9 @@ console.log(bold(' Checked twice, by two processes.') + ' bookFlight() lives in
|
|
|
2914
3012
|
console.log(dim(' not here. It independently re-verified every attempt above against the SAME'));
|
|
2915
3013
|
console.log(dim(' ./metamynd-rules.json, over a signed request, before running your tool.'));
|
|
2916
3014
|
console.log('');
|
|
2917
|
-
console.log(dim(' What --gateway does NOT close:
|
|
2918
|
-
console.log(dim(' calls.
|
|
2919
|
-
console.log(dim('
|
|
3015
|
+
console.log(dim(' It also refuses a replay of the same signed request. What --gateway does NOT close:'));
|
|
3016
|
+
console.log(dim(' cumulative spend across many different calls. That needs a STATEFUL authority (the'));
|
|
3017
|
+
console.log(dim(' hosted gate holds the budget in a database) - a local harness has none.'));
|
|
2920
3018
|
console.log(dim(' See README.md#--gateway for exactly what this does and does not prove.'));
|
|
2921
3019
|
console.log('');
|
|
2922
3020
|
console.log(' Edit ./metamynd-rules.json (or the dashboard) and run again - the outcome');
|
|
@@ -3111,9 +3209,8 @@ console.log(dim(' ./metamynd-rules.json, over a signed request, before running
|
|
|
3111
3209
|
console.log(dim(' It enforces scope and identity firmly; the rule inputs above are NOT signed, so an'));
|
|
3112
3210
|
console.log(dim(' agent that can sign could omit or forge one and its rule would not fire.'));
|
|
3113
3211
|
console.log('');
|
|
3114
|
-
console.log(dim('
|
|
3115
|
-
console.log(dim('
|
|
3116
|
-
console.log(dim(' harness has none. See README.md#--gateway for exactly what this does and does not prove.'));
|
|
3212
|
+
console.log(dim(' It also refuses a replay of the same signed request (remembered in memory, so a restart'));
|
|
3213
|
+
console.log(dim(' forgets). See README.md#--gateway for exactly what this does and does not prove.'));
|
|
3117
3214
|
console.log('');
|
|
3118
3215
|
console.log(' Edit ./metamynd-rules.json (or the dashboard) and run again - the outcome');
|
|
3119
3216
|
console.log(dim(' changes, in BOTH processes, from the one file. That is the point.'));` : `
|
|
@@ -3232,13 +3329,19 @@ not cover. An agent that can sign could omit a field or send a different one, an
|
|
|
3232
3329
|
it would not fire. The gateway enforces scope and identity firmly; an input-dependent rule is only as
|
|
3233
3330
|
strong as whatever supplies its input.` : ''}
|
|
3234
3331
|
|
|
3235
|
-
**
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3332
|
+
**Also closed — replaying the same signed request.** \`harness-gateway.mjs\` remembers every nonce for as long as
|
|
3333
|
+
the guard would accept the request (about 5 minutes) and refuses the second copy (\`REPLAY_DETECTED\`). The memory is
|
|
3334
|
+
in-process: restarting the gateway forgets it.
|
|
3335
|
+
|
|
3336
|
+
${demo ? `**NOT closed:** anything that depends on history across many DIFFERENT calls (rate limits, circuit breakers,
|
|
3337
|
+
patterns). That needs a STATEFUL authority — the hosted gate's \`requireAuthorization\` atomically claims a real,
|
|
3338
|
+
single-use \`authorizationId\` against a database (see \`@metamynd/agentsafe-mcp-guard\`'s own README). A local
|
|
3339
|
+
harness has no database; that is the whole point of \`--harness\`. \`harness-gateway.mjs\` re-checks POLICY per
|
|
3340
|
+
request, which is real and worth having, but it does not remember what earlier calls did.` : `**NOT closed:** cumulative spend across many DIFFERENT calls. That needs a STATEFUL authority — the hosted gate's
|
|
3341
|
+
\`requireAuthorization\` atomically claims a real, single-use \`authorizationId\` against a database and holds the
|
|
3342
|
+
budget there (see \`@metamynd/agentsafe-mcp-guard\`'s own README). A local harness has no database; that is the
|
|
3343
|
+
whole point of \`--harness\`. \`harness-gateway.mjs\` re-checks POLICY per request, so each call is held to the
|
|
3344
|
+
per-transaction cap, but many calls under the cap are not added up here the way the hosted gate adds them up.`}
|
|
3242
3345
|
|
|
3243
3346
|
Also not closed by \`--gateway\` alone: cross-party trust (nobody but you can verify this agent's
|
|
3244
3347
|
identity or its decisions), evidence anyone but you can audit, a dashboard reachable when this
|
|
@@ -3673,6 +3776,15 @@ async function main() {
|
|
|
3673
3776
|
}
|
|
3674
3777
|
const body = {
|
|
3675
3778
|
name, scope,
|
|
3779
|
+
// Explicit, not left to the backend's own eligibility-based default (mainnet for a KYB-verified
|
|
3780
|
+
// owner, testnet otherwise) — an owner who happens to be verified would otherwise silently get a
|
|
3781
|
+
// REAL mainnet agent from a plain `npx create-metamynd-agent` with no signal that anything
|
|
3782
|
+
// changed. The generated financial gateway CAN now satisfy mainnet's registry enforcement (a
|
|
3783
|
+
// real, registered did:key — see the gateway identity step below), but this CLI has no flag to
|
|
3784
|
+
// opt into mainnet yet; that is a separate decision (exposing it safely — KYB status, spend
|
|
3785
|
+
// caps — is more than a network toggle) from giving the gateway a real identity, which is what
|
|
3786
|
+
// this fixes. docs/design/release-blockers-open-items.md, "Registry default".
|
|
3787
|
+
network: 'testnet',
|
|
3676
3788
|
...(financial ? { currency, maxAmount, perTxnMax } : {}),
|
|
3677
3789
|
merchants,
|
|
3678
3790
|
...(publicKey ? { publicKey } : {}),
|
|
@@ -3719,6 +3831,27 @@ async function main() {
|
|
|
3719
3831
|
console.log(c.dim(` POST ${base}/agent-identity/${config.identityId}/verify-key { "signature": "<hex>" } (owner token)`));
|
|
3720
3832
|
}
|
|
3721
3833
|
|
|
3834
|
+
// 3c. The gateway's OWN identity — a real did:key, separate from the agent's own (an agent must
|
|
3835
|
+
// never be able to release or lower-settle a hold it authorized itself; only the SERVICE that
|
|
3836
|
+
// claimed it may). Only the financial, with-gateway shape ever claims a hold at all; the
|
|
3837
|
+
// non-financial gateway never calls claimAuthorization() (requireAuthorization is off there), so
|
|
3838
|
+
// it has no counterparty identity to register.
|
|
3839
|
+
let gatewayIdentity = null;
|
|
3840
|
+
if (financial && !args['no-gateway']) {
|
|
3841
|
+
const gwKeypair = generateAgentKeypair();
|
|
3842
|
+
const gatewayDid = buildDidKey(rawPublicKeyFromSpkiHex(gwKeypair.publicKeyHex));
|
|
3843
|
+
gatewayIdentity = { did: gatewayDid, keyHex: gwKeypair.privateKeyHex };
|
|
3844
|
+
console.log(c.dim(`\n → registering the gateway's identity as a trusted counterparty (so it can claim this agent's holds) …`));
|
|
3845
|
+
const reg = await registerGatewayCounterparty(base, token, gatewayDid, `${scope}-gateway`);
|
|
3846
|
+
if (reg.ok) {
|
|
3847
|
+
console.log(` ${c.green('✓')} gateway DID ${c.b(gatewayDid)} — ${reg.message}`);
|
|
3848
|
+
} else {
|
|
3849
|
+
console.log(` ${c.yellow('!')} could not register the gateway as a trusted counterparty: ${reg.message}`);
|
|
3850
|
+
console.log(c.dim(` it still works on testnet (open by default with no registry entries); a MAINNET hold cannot be`));
|
|
3851
|
+
console.log(c.dim(` claimed until it is registered — POST ${base}/policy/counterparties { "did": "${gatewayDid}", "confirmEnforcementChange": true }`));
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3722
3855
|
// 4. Scaffold + next steps
|
|
3723
3856
|
// The demo comes from the rules actually provisioned: the caller's own, else the backend's default
|
|
3724
3857
|
// for a non-financial agent (the same amount-free review harnessDefaultSopNeutral describes).
|
|
@@ -3727,7 +3860,7 @@ async function main() {
|
|
|
3727
3860
|
console.log(` ${c.green('✓')} derived ${demo.cases.length} demo case(s) from your rules`);
|
|
3728
3861
|
for (const n of demo.notDemonstrated) console.log(` ${c.yellow('!')} not staged in the demo: ${n.rule} ${c.dim('— ' + n.why + '; still enforced')}`);
|
|
3729
3862
|
}
|
|
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 });
|
|
3863
|
+
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
3864
|
}
|
|
3732
3865
|
|
|
3733
3866
|
// 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.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": {
|