create-metamynd-agent 0.13.1 → 0.14.1

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 +9 -2
  2. package/index.mjs +115 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -315,8 +315,15 @@ with the default hosted flow (login + provision):
315
315
  - **No spend constraint anywhere.** The mandate carries none and the default SOP has no amount rule
316
316
  (an `amount-unknown` block would refuse every action that carries no amount). Hosted: the
317
317
  provisioning call sends no `currency` / `maxAmount` / `perTxnMax` at all — the backend treats their
318
- absence as a non-financial mandate. A `rulePack` is built from spend limits, so it is ignored (and
319
- the CLI says so); list your rules under `rules` instead.
318
+ absence as a non-financial mandate.
319
+ - **Rule packs (hosted, since 0.14.0).** The CLI checks a `rulePack` against the platform's catalog
320
+ before provisioning. A pack that needs no spend limits (`comms`, `compliance`, the oversight packs, …)
321
+ is applied to a non-financial agent as-is, and its rules are printed and sent back as
322
+ `reviewedMoleculeIds`, so the platform provisions exactly those rules or nothing. A pack built from
323
+ spend limits (`spend-basic`, …) cannot govern a non-financial agent, and an unknown pack name is
324
+ refused: either stops the CLI before anything is provisioned, naming what would work. Against a
325
+ platform version that predates per-pack spend information, a non-financial agent's pack is still
326
+ dropped (and the CLI says so). `--harness` does not apply packs; list your rules under `rules`.
320
327
  - **The demo is derived from your rules.** `npm start` runs one request that satisfies every rule,
321
328
  then one per rule that should trip it, then an action nobody delegated. Each step states what it
322
329
  expects and flags any surprise, so changing your rules visibly changes the outcome. Rules the demo
package/index.mjs CHANGED
@@ -135,7 +135,21 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
135
135
  // 0.14.0: depends on agentsafe-mcp-guard ^0.16.0 (verifies a signed-jurisdiction request). No template change.
136
136
  // 0.15.0: depends on agentsafe-mcp-guard ^0.17.0 (context signature verified) and a requireContextSignature option.
137
137
  // No template change.
138
- const GATEWAY_VERSION = '^0.15.0';
138
+ // 0.16.0: the caller is answered first and the claimed hold is settled after (settleInBackground), retried on a
139
+ // transient failure. Template change: each scaffolded gateway drains settlements on SIGTERM/SIGINT (DRAIN_ON_SHUTDOWN).
140
+ const GATEWAY_VERSION = '^0.16.0';
141
+
142
+ /** Appended to every scaffolded gateway server: give hold settlements still running a bounded moment on shutdown. */
143
+ const DRAIN_ON_SHUTDOWN = `
144
+ // The gateway answers first and settles the hold it claimed after (agentsafe-http-gateway 0.16.0). On shutdown, stop
145
+ // taking requests and let the settlements still running land (up to 5 s); one that does not stays committed to the cap.
146
+ for (const signal of ['SIGTERM', 'SIGINT']) {
147
+ process.once(signal, () => {
148
+ server.close();
149
+ gateway.drainSettlements(5000).then(() => process.exit(0));
150
+ });
151
+ }
152
+ `;
139
153
  const DEFAULT_API = 'https://metamynd.ai/api/v1';
140
154
  const DEFAULT_GATEWAY_PORT = 4401; // distinct from --harness's dashboard (4400)
141
155
 
@@ -293,8 +307,29 @@ function askHidden(query) {
293
307
  });
294
308
  }
295
309
 
310
+ /** Thrown by fail() to end the run; carries no message of its own (fail() already printed it). */
311
+ class CliExit extends Error {
312
+ constructor(code) {
313
+ super('create-metamynd-agent exit');
314
+ this.exitCode = code;
315
+ }
316
+ }
317
+
296
318
  function fail(msg) {
297
319
  console.error(`\n${c.red('✖')} ${msg}\n`);
320
+ // Not process.exit(): after a fetch(), its keep-alive socket may still be closing, and exiting then
321
+ // aborts Node 24 on Windows (libuv "!(handle->flags & UV_HANDLE_CLOSING)", exit 0xC0000409) instead of
322
+ // exiting 1. Unwind instead and let the event loop drain; the unref'd timer only fires if something
323
+ // else would keep the process alive.
324
+ process.exitCode = 1;
325
+ setTimeout(() => process.exit(1), 5000).unref();
326
+ throw new CliExit(1);
327
+ }
328
+
329
+ /** The run's last-resort handler: a CliExit already said why; anything else is a real crash. */
330
+ function onCliError(e) {
331
+ if (e instanceof CliExit) return;
332
+ console.error(`\n${c.red('✖')} ${e?.stack || e?.message || String(e)}\n`);
298
333
  process.exit(1);
299
334
  }
300
335
 
@@ -850,6 +885,65 @@ async function apiPost(base, path, body, token) {
850
885
  return json;
851
886
  }
852
887
 
888
+ /** GET a public, read-only endpoint; null on ANY failure (unreachable, non-2xx, non-JSON) — never fatal. */
889
+ async function softGet(base, path) {
890
+ try {
891
+ const res = await fetch(`${base}${path}`);
892
+ if (!res.ok) return { status: res.status, json: await res.json().catch(() => null) };
893
+ return { status: res.status, json: await res.json() };
894
+ } catch {
895
+ return null;
896
+ }
897
+ }
898
+
899
+ /**
900
+ * Decide what to do with a config file's `rulePack` BEFORE provisioning, from the platform's own catalog
901
+ * (GET /onboarding/rule-packs) and preview (GET /onboarding/rule-packs/:key/preview):
902
+ * - an unknown pack fails here, naming the packs the platform offers — the platform refuses it too
903
+ * (400 UNKNOWN_RULE_PACK; an older platform silently applied its default rules instead);
904
+ * - a pack built from spend limits cannot govern a non-financial agent: fail and say what would work;
905
+ * - any other pack is applied as-is, and its rules are sent back as `reviewedMoleculeIds`, so the
906
+ * platform provisions exactly the rules shown here or nothing at all.
907
+ * A platform whose catalog does not say which packs need spend limits (it predates FA-001) only applies a
908
+ * pack when spend limits are supplied, so for a non-financial agent the pack is dropped and that is said.
909
+ * Returns { drop: true } or { molecules, reviewedMoleculeIds? }.
910
+ */
911
+ async function resolveHostedRulePack(base, key, { financial, perTxnMax, maxAmount, currency }) {
912
+ const catalogRes = await softGet(base, '/onboarding/rule-packs');
913
+ const catalog = Array.isArray(catalogRes?.json?.data) ? catalogRes.json.data : null;
914
+ const legacyDrop = () => {
915
+ console.log(` ${c.yellow('!')} the rule pack "${key}" cannot be applied to a non-financial agent on this platform version (it applies packs only when spend limits are supplied) — the default (an amount-free high-risk review) applies. List your rules under "rules" to set your own.`);
916
+ return { drop: true };
917
+ };
918
+ if (!catalog) {
919
+ // Catalog unavailable: send the pack for a financial agent (the platform validates it), keep the old drop otherwise.
920
+ return financial ? { molecules: null } : legacyDrop();
921
+ }
922
+ const entry = catalog.find((p) => p?.key === key);
923
+ if (!entry) {
924
+ fail(`Unknown rule pack "${key}". This platform offers: ${catalog.map((p) => p.key).join(', ')}. Fix "rulePack" in the config file, or list your rules under "rules".`);
925
+ }
926
+ const knowsSpend = catalog.some((p) => typeof p?.requiresSpend === 'boolean');
927
+ if (!financial) {
928
+ if (!knowsSpend) return legacyDrop();
929
+ if (entry.requiresSpend) {
930
+ const usable = catalog.filter((p) => p.requiresSpend === false).map((p) => p.key);
931
+ fail(`The rule pack "${key}" is built from spend limits, and this agent has none (non-financial). Choose a pack that needs no spend limits (${usable.join(', ')}), pass --financial with spend limits, or list your rules under "rules".`);
932
+ }
933
+ }
934
+ const query = financial
935
+ ? `perTxnMax=${encodeURIComponent(perTxnMax)}&maxAmount=${encodeURIComponent(maxAmount)}&currency=${encodeURIComponent(currency)}`
936
+ : 'financial=false';
937
+ const preview = await softGet(base, `/onboarding/rule-packs/${encodeURIComponent(key)}/preview?${query}`);
938
+ const molecules = Array.isArray(preview?.json?.data?.molecules) ? preview.json.data.molecules : null;
939
+ if (!molecules) {
940
+ if (!financial) fail(`Could not read the rules of the rule pack "${key}" from ${base} (HTTP ${preview?.status ?? 'unreachable'}${preview?.json?.message ? ': ' + preview.json.message : ''}) — nothing was provisioned.`);
941
+ return { molecules: null };
942
+ }
943
+ console.log(` ${c.green('✓')} rule pack "${entry.label ?? key}": ${molecules.map((m) => m.name ?? m.id).join(', ')}`);
944
+ return { molecules, ...(knowsSpend ? { reviewedMoleculeIds: molecules.map((m) => m.id) } : {}) };
945
+ }
946
+
853
947
  /**
854
948
  * POST /policy/counterparties for the gateway's own DID — best-effort, unlike apiPost above: a
855
949
  * hosted financial gateway works fine on testnet with no registration at all (open by default), so
@@ -1508,7 +1602,7 @@ server.listen(PORT, () => {
1508
1602
  console.log('[gateway] listening on :' + PORT + ' -> the only place performAction() runs.');
1509
1603
  console.log('[gateway] every request is independently re-verified against this agent\\'s own policy.');
1510
1604
  });
1511
- `;
1605
+ ${DRAIN_ON_SHUTDOWN}`;
1512
1606
  }
1513
1607
 
1514
1608
  function gatewayEnvExampleNeutral() {
@@ -1918,7 +2012,7 @@ server.listen(PORT, () => {
1918
2012
  console.log('[gateway] listening on :' + PORT + ' -> the only place bookFlight() runs.');
1919
2013
  console.log('[gateway] every request is independently re-verified against this agent\\'s own policy.');
1920
2014
  });
1921
- `;
2015
+ ${DRAIN_ON_SHUTDOWN}`;
1922
2016
  }
1923
2017
 
1924
2018
  function gatewayPackageJson(slug) {
@@ -2523,7 +2617,7 @@ server.listen(PORT, () => {
2523
2617
  console.log('[harness-gateway] listening on :' + PORT + ' — the only place your tools run.');
2524
2618
  console.log('[harness-gateway] re-verifying against ../metamynd-rules.json, independently of index.mjs.');
2525
2619
  });
2526
- `;
2620
+ ${DRAIN_ON_SHUTDOWN}`;
2527
2621
  }
2528
2622
 
2529
2623
  function harnessGatewayPackageJson(slug) {
@@ -3823,13 +3917,18 @@ async function main() {
3823
3917
  // (a per-transaction cap + high-risk review), same as before --config existed.
3824
3918
  const sopFields = configFileSopFields(fileConfig);
3825
3919
  if (sopFields.sop) console.log(` ${c.green('✓')} compiled ${sopFields.sop.documentJson.molecules.length} rule(s) from the config file`);
3826
- console.log(c.dim(`\n → provisioning "${name}" (identity + mandate + SOP + Standards) …`));
3827
3920
  // A non-financial agent sends NO spend fields at all (currency/maxAmount/perTxnMax omitted together
3828
- // is how the backend recognises one). A rule pack is built from spend limits, so it cannot apply.
3829
- if (!financial && sopFields.rulePack) {
3830
- console.log(` ${c.yellow('!')} the rule pack "${sopFields.rulePack}" cannot be applied to a non-financial hosted agent (the platform applies a pack only when spend limits are supplied) — the default (an amount-free high-risk review) applies. List your rules under "rules" to set your own.`);
3831
- delete sopFields.rulePack;
3921
+ // is how the backend recognises one). Whether a rule pack can govern it is the platform's answer.
3922
+ let packMolecules = null;
3923
+ if (sopFields.rulePack) {
3924
+ const pack = await resolveHostedRulePack(base, sopFields.rulePack, { financial, perTxnMax, maxAmount, currency });
3925
+ if (pack.drop) delete sopFields.rulePack;
3926
+ else {
3927
+ packMolecules = pack.molecules;
3928
+ if (pack.reviewedMoleculeIds) sopFields.reviewedMoleculeIds = pack.reviewedMoleculeIds;
3929
+ }
3832
3930
  }
3931
+ console.log(c.dim(`\n → provisioning "${name}" (identity + mandate + SOP + Standards) …`));
3833
3932
  const body = {
3834
3933
  name, scope,
3835
3934
  // Explicit, not left to the backend's own eligibility-based default (mainnet for a KYB-verified
@@ -3911,7 +4010,7 @@ async function main() {
3911
4010
  // 4. Scaffold + next steps
3912
4011
  // The demo comes from the rules actually provisioned: the caller's own, else the backend's default
3913
4012
  // for a non-financial agent (the same amount-free review harnessDefaultSopNeutral describes).
3914
- const demo = financial ? null : buildPolicyCases(sopFields.sop?.documentJson?.molecules ?? harnessDefaultSopNeutral().molecules);
4013
+ const demo = financial ? null : buildPolicyCases(sopFields.sop?.documentJson?.molecules ?? packMolecules ?? harnessDefaultSopNeutral().molecules);
3915
4014
  if (demo) {
3916
4015
  console.log(` ${c.green('✓')} derived ${demo.cases.length} demo case(s) from your rules`);
3917
4016
  for (const n of demo.notDemonstrated) console.log(` ${c.yellow('!')} not staged in the demo: ${n.rule} ${c.dim('— ' + n.why + '; still enforced')}`);
@@ -3926,4 +4025,9 @@ export {
3926
4025
  generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
3927
4026
  scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
3928
4027
  };
3929
- if (!process.env.CREATE_METAMYND_AGENT_NO_MAIN) main().catch((e) => fail(e?.stack || e?.message || String(e)));
4028
+ if (!process.env.CREATE_METAMYND_AGENT_NO_MAIN) {
4029
+ // fail() can also be reached from a callback outside main()'s promise chain (readline, child process).
4030
+ process.on('uncaughtException', onCliError);
4031
+ process.on('unhandledRejection', onCliError);
4032
+ main().catch(onCliError);
4033
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-metamynd-agent",
3
- "version": "0.13.1",
3
+ "version": "0.14.1",
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": {