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.
Files changed (3) hide show
  1. package/README.md +5 -0
  2. package/index.mjs +68 -10
  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
@@ -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
- const GUARD_VERSION = '^0.17.0';
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
- const MCP_GUARD_VERSION = '^0.17.0';
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
- const GATEWAY_VERSION = '^0.16.0';
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${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
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${policyKey ? `, policyPublicKey: '${policyKey}'` : ''} });
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) — present on every provisioning
2262
- // response that talks to a real backend (full account, sandbox, delegated-claim). Baked into the
2263
- // generated gateway so it pins the bundle rather than trusting whatever the fetch returns; absent
2264
- // only for a config predating this field (an older scaffold's saved agent.metamynd.json), where
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.1",
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": {