create-metamynd-agent 0.14.2 → 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.
Files changed (3) hide show
  1. package/README.md +5 -0
  2. package/index.mjs +63 -9
  3. 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
@@ -59,7 +59,9 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
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
- const GUARD_VERSION = '^0.18.0';
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';
63
65
  /** The harness entry point's config load, shared by both harness templates: a fresh clone has no
64
66
  * agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
65
67
  function harnessConfigLoad() {
@@ -120,7 +122,10 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
120
122
  // requireContextSignature option. No template change (the scaffolded agent does not sign its context by default).
121
123
  // 0.18.0: an opt-in bundleCache (push-invalidated by the issuer's events stream) and keepAliveFetch exported. No
122
124
  // template change (the scaffolded gateways don't enable the cache).
123
- const MCP_GUARD_VERSION = '^0.18.0';
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';
124
129
  const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
125
130
  // 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
126
131
  // never scaffold a range that could resolve below it.
@@ -972,6 +977,50 @@ async function registerGatewayCounterparty(base, token, did, label) {
972
977
  }
973
978
  }
974
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
+
975
1024
  // ---------- scaffolding ----------
976
1025
  /**
977
1026
  * The --no-gateway / --sandbox variant: the tool is a local function in the SAME process as
@@ -1561,7 +1610,7 @@ const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFie
1561
1610
  // forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
1562
1611
  // fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
1563
1612
  // a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
1564
- const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
1613
+ const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false, policyPublicKey: '${policyKey}' });
1565
1614
 
1566
1615
  const gateway = createHttpGateway({
1567
1616
  guard,
@@ -1966,7 +2015,7 @@ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', impo
1966
2015
  // forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
1967
2016
  // fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
1968
2017
  // a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
1969
- const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
2018
+ const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true, policyPublicKey: '${policyKey}' });
1970
2019
 
1971
2020
  const gateway = createHttpGateway({
1972
2021
  guard,
@@ -2262,12 +2311,14 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
2262
2311
 
2263
2312
  if (withGateway) {
2264
2313
  const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
2265
- // The key that signs every policy bundle (magp.policy-signer.ts) — present on every provisioning
2266
- // response that talks to a real backend (full account, sandbox, delegated-claim). Baked into the
2267
- // generated gateway so it pins the bundle rather than trusting whatever the fetch returns; absent
2268
- // only for a config predating this field (an older scaffold's saved agent.metamynd.json), where
2269
- // 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.
2270
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
+ }
2271
2322
  const gwDir = join(outDir, 'gateway');
2272
2323
  if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
2273
2324
  if (!neutral) {
@@ -3784,6 +3835,7 @@ async function runClaim(args) {
3784
3835
  if (args.financial && issuedNonFinancial) {
3785
3836
  keepConfigAndStop('--financial was passed, but the approved agent has NO spending authority (the request was non-financial).');
3786
3837
  }
3838
+ if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndStop(NO_POLICY_KEY(base));
3787
3839
  const slug = slugify(state.name || 'metamynd-agent');
3788
3840
  const outDir = resolve(String(args.out || `./${slug}`));
3789
3841
  if (issuedNonFinancial) {
@@ -4019,6 +4071,7 @@ async function main() {
4019
4071
  console.log(` ${c.green('✓')} derived ${demo.cases.length} demo case(s) from your rules`);
4020
4072
  for (const n of demo.notDemonstrated) console.log(` ${c.yellow('!')} not staged in the demo: ${n.rule} ${c.dim('— ' + n.why + '; still enforced')}`);
4021
4073
  }
4074
+ if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndFail(outDir, config, NO_POLICY_KEY(base));
4022
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 });
4023
4076
  }
4024
4077
 
@@ -4028,6 +4081,7 @@ export {
4028
4081
  buildPolicyCases, resolveFinancial, policyMolecules, ruleToMolecule,
4029
4082
  generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
4030
4083
  scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
4084
+ ensurePolicyKey,
4031
4085
  };
4032
4086
  if (!process.env.CREATE_METAMYND_AGENT_NO_MAIN) {
4033
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.2",
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": {