create-metamynd-agent 0.14.1 → 0.14.4
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 +5 -0
- package/index.mjs +68 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -175,6 +175,11 @@ the whole protocol.
|
|
|
175
175
|
Named precisely, not left implicit:
|
|
176
176
|
|
|
177
177
|
- **Direct call.** `bookFlight()` doesn't exist in the agent's process.
|
|
178
|
+
- **Forged or replayed policy (every gateway is pinned, 0.14.3).** The gateway's `policyPublicKey` is baked in from your
|
|
179
|
+
provisioning response (or, from an older API, `GET /magp/policy/pubkey`), and with `@metamynd/agentsafe-mcp-guard` 0.18.1 an
|
|
180
|
+
unsigned, tampered or stale bundle is refused for every action, amount 0 included — so a non-financial gateway cannot be granted
|
|
181
|
+
an action by whoever sits on its bundle fetch. If no key can be had, the CLI scaffolds no gateway at all (it keeps the
|
|
182
|
+
provisioned config and says so) rather than writing an unpinned one.
|
|
178
183
|
- **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
|
|
179
184
|
authorization it gets from the gate is bound to that same body, and the generated gateway sets `requirePayloadBinding: true`: it
|
|
180
185
|
digests the body it is about to run and the issuer refuses the claim unless that is the digest the agent signed. The eight
|
package/index.mjs
CHANGED
|
@@ -58,7 +58,10 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
|
|
|
58
58
|
// passes a request's jurisdiction that way, since the gate no longer reads one from the unsigned context.
|
|
59
59
|
// 0.17.0 signs the agent's context (envelopeSignature, MAGP 8.3.13) by default; the scaffolded agent gets it with no template
|
|
60
60
|
// change (signContext: false opts out). A daemon-custody agent needs agentsafe-signer >= 0.19.0 for its no-amount requests.
|
|
61
|
-
|
|
61
|
+
// 0.18.0 exports keepAliveFetch. No template change.
|
|
62
|
+
// 0.18.1 parks a local-first ESCALATE at the gate. Required: every scaffold's demo says "approve it in the dashboard and
|
|
63
|
+
// the action resumes", and below this the escalation never reached the owner's queue (decided in-process, no escalationId).
|
|
64
|
+
const GUARD_VERSION = '^0.18.1';
|
|
62
65
|
/** The harness entry point's config load, shared by both harness templates: a fresh clone has no
|
|
63
66
|
* agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
|
|
64
67
|
function harnessConfigLoad() {
|
|
@@ -117,7 +120,12 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
|
117
120
|
// the scaffolded agent now signs jurisdiction, which an older gateway would refuse SIGNATURE_INVALID.
|
|
118
121
|
// 0.17.0: verifies the agent's context signature (envelopeSignature) before judging its itinerary, and a
|
|
119
122
|
// requireContextSignature option. No template change (the scaffolded agent does not sign its context by default).
|
|
120
|
-
|
|
123
|
+
// 0.18.0: an opt-in bundleCache (push-invalidated by the issuer's events stream) and keepAliveFetch exported. No
|
|
124
|
+
// template change (the scaffolded gateways don't enable the cache).
|
|
125
|
+
// 0.18.1: the pinned policyPublicKey every scaffolded gateway sets now refuses an unsigned or stale bundle for an
|
|
126
|
+
// amount-0 action too. Required: the non-financial gateway's every request is amount 0, so below this a stripped
|
|
127
|
+
// bundle could grant it an action the agent never had.
|
|
128
|
+
const MCP_GUARD_VERSION = '^0.18.1';
|
|
121
129
|
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
122
130
|
// 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
|
|
123
131
|
// never scaffold a range that could resolve below it.
|
|
@@ -137,7 +145,8 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
|
137
145
|
// No template change.
|
|
138
146
|
// 0.16.0: the caller is answered first and the claimed hold is settled after (settleInBackground), retried on a
|
|
139
147
|
// transient failure. Template change: each scaffolded gateway drains settlements on SIGTERM/SIGINT (DRAIN_ON_SHUTDOWN).
|
|
140
|
-
|
|
148
|
+
// 0.17.0: depends on agentsafe-mcp-guard ^0.18.0 and exports keepAliveFetch. No template change.
|
|
149
|
+
const GATEWAY_VERSION = '^0.17.0';
|
|
141
150
|
|
|
142
151
|
/** Appended to every scaffolded gateway server: give hold settlements still running a bounded moment on shutdown. */
|
|
143
152
|
const DRAIN_ON_SHUTDOWN = `
|
|
@@ -968,6 +977,50 @@ async function registerGatewayCounterparty(base, token, did, label) {
|
|
|
968
977
|
}
|
|
969
978
|
}
|
|
970
979
|
|
|
980
|
+
/**
|
|
981
|
+
* Every scaffolded gateway pins the issuer's policy-signing key, so it refuses any bundle MetaMynd did not sign —
|
|
982
|
+
* an unpinned gateway trusts whatever its bundle fetch returns, and over plain http:// that is whatever an
|
|
983
|
+
* interceptor wrote (mcp-guard only refuses VALUE-bearing actions there, and a non-financial agent has none).
|
|
984
|
+
* The provisioning response carries the key (`issuer.policyKey`); when it does not (an older backend), it is read
|
|
985
|
+
* from GET /magp/policy/pubkey on the same API this CLI just provisioned through, then saved into the config.
|
|
986
|
+
* Returns the key, or null when neither source has one — the caller must then not scaffold a gateway.
|
|
987
|
+
*/
|
|
988
|
+
async function ensurePolicyKey(config, base) {
|
|
989
|
+
const isKey = (k) => typeof k === 'string' && /^[0-9a-f]{64}$/i.test(k);
|
|
990
|
+
if (isKey(config.issuer?.policyKey)) return config.issuer.policyKey;
|
|
991
|
+
let key = null;
|
|
992
|
+
try {
|
|
993
|
+
const res = await fetch(`${String(base).replace(/\/+$/, '')}/magp/policy/pubkey`);
|
|
994
|
+
key = (await res.json().catch(() => null))?.data?.publicKey ?? null;
|
|
995
|
+
} catch {
|
|
996
|
+
key = null;
|
|
997
|
+
}
|
|
998
|
+
if (!isKey(key)) return null;
|
|
999
|
+
config.issuer = { ...(config.issuer ?? {}), policyKey: key.toLowerCase() };
|
|
1000
|
+
return config.issuer.policyKey;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const NO_POLICY_KEY = (base) =>
|
|
1004
|
+
`Could not get MetaMynd's policy-signing key: the provisioning response carried none, and GET ${base}/magp/policy/pubkey ` +
|
|
1005
|
+
'did not return one. A gateway without it would trust whatever policy bundle its network fetch returns, so none was ' +
|
|
1006
|
+
'scaffolded. Retry when the API is reachable, or pass --no-gateway for the single-process example (not an enforcement boundary).';
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Stop after provisioning WITHOUT losing what it returned: a managed agent key is handed back once and cannot be
|
|
1010
|
+
* fetched again. Saves the config into the project directory (never over an existing file), then fails with `why`.
|
|
1011
|
+
*/
|
|
1012
|
+
function keepConfigAndFail(outDir, config, why) {
|
|
1013
|
+
let kept = null;
|
|
1014
|
+
try {
|
|
1015
|
+
mkdirSync(outDir, { recursive: true });
|
|
1016
|
+
kept = join(outDir, existsSync(join(outDir, 'agent.metamynd.json')) ? `agent.metamynd.${Date.now()}.json` : 'agent.metamynd.json');
|
|
1017
|
+
writeFileSync(kept, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
1018
|
+
} catch {
|
|
1019
|
+
kept = null;
|
|
1020
|
+
}
|
|
1021
|
+
fail(`${why}${kept ? `\n The agent was provisioned; its config (with its key) was saved to ${kept.replace(/\\/g, '/')}.` : ''}`);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
971
1024
|
// ---------- scaffolding ----------
|
|
972
1025
|
/**
|
|
973
1026
|
* The --no-gateway / --sandbox variant: the tool is a local function in the SAME process as
|
|
@@ -1557,7 +1610,7 @@ const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFie
|
|
|
1557
1610
|
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1558
1611
|
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1559
1612
|
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
1560
|
-
const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false
|
|
1613
|
+
const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false, policyPublicKey: '${policyKey}' });
|
|
1561
1614
|
|
|
1562
1615
|
const gateway = createHttpGateway({
|
|
1563
1616
|
guard,
|
|
@@ -1962,7 +2015,7 @@ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', impo
|
|
|
1962
2015
|
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1963
2016
|
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1964
2017
|
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
1965
|
-
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true
|
|
2018
|
+
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true, policyPublicKey: '${policyKey}' });
|
|
1966
2019
|
|
|
1967
2020
|
const gateway = createHttpGateway({
|
|
1968
2021
|
guard,
|
|
@@ -2258,12 +2311,14 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2258
2311
|
|
|
2259
2312
|
if (withGateway) {
|
|
2260
2313
|
const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
|
|
2261
|
-
// The key that signs every policy bundle (magp.policy-signer.ts)
|
|
2262
|
-
//
|
|
2263
|
-
//
|
|
2264
|
-
//
|
|
2265
|
-
// the generated gateway is unchanged from before rather than baking in nothing and crashing.
|
|
2314
|
+
// The key that signs every policy bundle (magp.policy-signer.ts), baked into the generated gateway so it pins the
|
|
2315
|
+
// bundle rather than trusting whatever the fetch returns. REQUIRED: an unpinned gateway takes an interceptor's
|
|
2316
|
+
// bundle as its rules (2026-10-02 — a non-financial gateway executed an action the agent was never granted). The
|
|
2317
|
+
// interactive flows fill it in with ensurePolicyKey() before calling this, so a missing key here is a caller bug.
|
|
2266
2318
|
const policyKey = config.issuer?.policyKey ?? null;
|
|
2319
|
+
if (!policyKey) {
|
|
2320
|
+
throw new Error("scaffoldProject: a gateway needs the issuer's policy-signing key (config.issuer.policyKey) to pin its bundle — call ensurePolicyKey() first, or pass withGateway: false");
|
|
2321
|
+
}
|
|
2267
2322
|
const gwDir = join(outDir, 'gateway');
|
|
2268
2323
|
if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
|
|
2269
2324
|
if (!neutral) {
|
|
@@ -3780,6 +3835,7 @@ async function runClaim(args) {
|
|
|
3780
3835
|
if (args.financial && issuedNonFinancial) {
|
|
3781
3836
|
keepConfigAndStop('--financial was passed, but the approved agent has NO spending authority (the request was non-financial).');
|
|
3782
3837
|
}
|
|
3838
|
+
if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndStop(NO_POLICY_KEY(base));
|
|
3783
3839
|
const slug = slugify(state.name || 'metamynd-agent');
|
|
3784
3840
|
const outDir = resolve(String(args.out || `./${slug}`));
|
|
3785
3841
|
if (issuedNonFinancial) {
|
|
@@ -4015,6 +4071,7 @@ async function main() {
|
|
|
4015
4071
|
console.log(` ${c.green('✓')} derived ${demo.cases.length} demo case(s) from your rules`);
|
|
4016
4072
|
for (const n of demo.notDemonstrated) console.log(` ${c.yellow('!')} not staged in the demo: ${n.rule} ${c.dim('— ' + n.why + '; still enforced')}`);
|
|
4017
4073
|
}
|
|
4074
|
+
if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndFail(outDir, config, NO_POLICY_KEY(base));
|
|
4018
4075
|
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 });
|
|
4019
4076
|
}
|
|
4020
4077
|
|
|
@@ -4024,6 +4081,7 @@ export {
|
|
|
4024
4081
|
buildPolicyCases, resolveFinancial, policyMolecules, ruleToMolecule,
|
|
4025
4082
|
generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
|
|
4026
4083
|
scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
|
|
4084
|
+
ensurePolicyKey,
|
|
4027
4085
|
};
|
|
4028
4086
|
if (!process.env.CREATE_METAMYND_AGENT_NO_MAIN) {
|
|
4029
4087
|
// fail() can also be reached from a callback outside main()'s promise chain (readline, child process).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.4",
|
|
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": {
|