create-metamynd-agent 0.7.7 → 0.8.0

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 (2) hide show
  1. package/index.mjs +306 -44
  2. package/package.json +4 -1
package/index.mjs CHANGED
@@ -26,7 +26,10 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
26
26
  // 0.6.0 adds the amount-unknown atom (deny-by-default when a value-moving action's amount
27
27
  // can't be determined) — this was already missed once (this constant sat at ^0.5.0 through the
28
28
  // whole 0.6.0 release), silently scaffolding every new project without that protection.
29
- const GUARD_VERSION = '^0.6.0';
29
+ // 0.7.0 adds the opt-in `signContext` envelope signature (Tier 1 context-claim binding) —
30
+ // no scaffolded behavior changes (off by default), but the floor must still cover the real
31
+ // current version regardless, per this repo's standing internal-pin invariant.
32
+ const GUARD_VERSION = '^0.7.0';
30
33
  // The default hosted scaffold's SECOND process — the tool gateway (see scaffoldProject).
31
34
  const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
32
35
  // 0.2.0 adds requireAuthorization (closes replay + cumulative spend) — this scaffold sets that
@@ -87,7 +90,9 @@ ${c.b('Options')}
87
90
  --harness No login, no KYB, no network at all: a free local governance harness —
88
91
  your own rules, your own identity, decided entirely on this machine. See
89
92
  README#harness. Not for enterprise use (no anchored identity/evidence,
90
- no cross-party trust) — that is what the hosted platform adds.
93
+ no cross-party trust) — that is what the hosted platform adds. Add
94
+ --gateway for a second local process that closes the cooperative-only
95
+ gap too, still free and offline (see --gateway below).
91
96
  --sandbox No login, no KYB: scaffold against the shared sandbox agent (fastest start)
92
97
  --config <file> A JSON policy file (name/scope/limits + simple "rules") — see README#config-file.
93
98
  Flags below still override individual fields from the file. Works with
@@ -112,7 +117,12 @@ ${c.b('Options')}
112
117
  --no-gateway Hosted flow only: skip the separate tool-gateway process (see
113
118
  README#separate-tool-gateway-default) and scaffold the old
114
119
  single-process example instead. Not a separate enforcement boundary.
115
- --gateway-port <n> Hosted flow only: the gateway process's port (default 4401)
120
+ --gateway --harness only: ALSO scaffold a second local process (still zero
121
+ network, zero account) that independently re-verifies every request
122
+ against the same rules file, using the real @metamynd/agentsafe-mcp-guard.
123
+ Off by default. Does not close nonce replay/cumulative spend — see the
124
+ generated README#--gateway for exactly what it does and does not.
125
+ --gateway-port <n> The gateway process's port, hosted flow or --harness --gateway (default 4401)
116
126
  --port <n> --harness only: the local dashboard's port (default 4400)
117
127
  --yes, -y Non-interactive: use flags/env/defaults, never prompt
118
128
  -h, --help Show this help
@@ -254,6 +264,40 @@ function generateAgentKeypair() {
254
264
  };
255
265
  }
256
266
 
267
+ // did:key (base58btc multibase over an Ed25519-multicodec-prefixed raw public key) — mirrors
268
+ // backend/src/features/agent-identity/did.util.ts / magp-did.mjs's buildDidKey exactly, so a
269
+ // did:key this CLI mints is resolvable by any real MAGP verifier (agentsafe-guard,
270
+ // agentsafe-mcp-guard) with zero network calls: the public key is embedded in the DID string
271
+ // itself. Reimplemented inline (not imported) — this CLI stays zero-dependency, and it is
272
+ // ~15 lines of pure math, not something worth a package for.
273
+ const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
274
+ function base58(bytes) {
275
+ let zeros = 0;
276
+ while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
277
+ const digits = [];
278
+ for (let i = zeros; i < bytes.length; i++) {
279
+ let carry = bytes[i];
280
+ for (let j = 0; j < digits.length; j++) { carry += digits[j] << 8; digits[j] = carry % 58; carry = (carry / 58) | 0; }
281
+ while (carry > 0) { digits.push(carry % 58); carry = (carry / 58) | 0; }
282
+ }
283
+ let out = '';
284
+ for (let k = 0; k < zeros; k++) out += BASE58_ALPHABET[0];
285
+ for (let q = digits.length - 1; q >= 0; q--) out += BASE58_ALPHABET[digits[q]];
286
+ return out;
287
+ }
288
+ const ED25519_MULTICODEC = Uint8Array.of(0xed, 0x01);
289
+ const ED25519_SPKI_PREFIX_LEN = 12; // 302a300506032b6570032100 — fixed for every Ed25519 SPKI DER key
290
+ /** The RAW 32-byte Ed25519 public key from this CLI's own DER/SPKI hex export. */
291
+ function rawPublicKeyFromSpkiHex(publicKeyHex) {
292
+ return Buffer.from(publicKeyHex, 'hex').subarray(ED25519_SPKI_PREFIX_LEN);
293
+ }
294
+ function buildDidKey(publicKeyBytes) {
295
+ const prefixed = new Uint8Array(ED25519_MULTICODEC.length + publicKeyBytes.length);
296
+ prefixed.set(ED25519_MULTICODEC, 0);
297
+ prefixed.set(publicKeyBytes, ED25519_MULTICODEC.length);
298
+ return `did:key:z${base58(prefixed)}`;
299
+ }
300
+
257
301
  // Sign a BYOK challenge exactly as the gate verifies it: Ed25519 over the UTF-8 bytes of the raw
258
302
  // challenge nonce, hex-encoded. Mirrors agentsafe-guard's sign().
259
303
  function signChallengeHex(privateKeyHex, challenge) {
@@ -777,7 +821,12 @@ async function bookFlight(args) {
777
821
 
778
822
  // One protected route: only a request signed by this agent, for exactly this action, and
779
823
  // re-verified against this agent's own mandate/SOP, reaches bookFlight() below.
780
- const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}' }];
824
+ //
825
+ // valueFields is explicit on purpose, not left to the gateway's own default (which would be
826
+ // this exact list anyway): a route with a real amount/merchant should always say so itself,
827
+ // rather than relying on a library default to guess right. A route with NO value concept at
828
+ // all (a read, a status check) should set valueFields: [] instead — see the gateway's README.
829
+ const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}', valueFields: ['amount', 'merchant'] }];
781
830
 
782
831
  // No serviceKey: this minimal gateway only calls verifyRequest() (re-check a signed request),
783
832
  // not the mutual-handshake methods, which are the only thing that needs it.
@@ -917,13 +966,18 @@ own code, or a network attacker) might attempt:
917
966
  - **Direct call.** \`bookFlight()\` doesn't exist in the agent's process. There's nothing to call.
918
967
  - **Confused deputy (payload).** The gateway re-verifies the signed request against this agent's
919
968
  own policy AND binds it to the actual request body (payload binding,
920
- \`@metamynd/agentsafe-http-gateway\` ≥ 0.4.0) — signing a cheap request while executing an
921
- expensive one is refused before the tool ever runs. The default binder requires \`amount\`/
922
- \`merchant\` to actually be found in the body whenever the signature names a real value for
923
- them not just "did the body offer at least one correct-looking field." A first attempt at
924
- this (0.3.0) checked the weaker version and was re-tested and closed the same day: a correct
925
- decoy in one field let the OTHER field hide anywhere nested, renamed, an array, or an
926
- entirely empty/non-JSON body.
969
+ \`@metamynd/agentsafe-http-gateway\` ≥ 0.4.5) — signing a cheap request while executing an
970
+ expensive one is refused before the tool ever runs. This route's \`valueFields: ['amount',
971
+ 'merchant']\` (see \`server.mjs\`) is an explicit, server-controlled requirement, not a guess
972
+ inferred from anything the signed request itself declares that distinction is what closes
973
+ the full history below, not just the most recent case in it. Earlier attempts checked
974
+ progressively weaker versions of "is this real": 0.3.0 only refused a body offering NONE of
975
+ the governed fields (a correct decoy in one field let the other hide nested, renamed, an
976
+ array, or an entirely empty/non-JSON body); 0.4.0–0.4.2 required a field only when the
977
+ SIGNED request's own value for it looked "real," which a signer could defeat by signing
978
+ \`amount: 0\` — or, identically, by never signing an amount at all, since both verify against
979
+ the exact same canonical message. \`valueFields\` moves the requirement to something the
980
+ signer never controls at all.
927
981
  - **Replay.** \`requireAuthorization: true\` (set in \`server.mjs\`) requires the agent's
928
982
  \`authorizationId\` — from a REAL \`guard.authorize()\` call, which \`index.mjs\` already makes for
929
983
  any value-bearing action by default — to atomically claim single-use execution against the
@@ -1108,11 +1162,14 @@ function harnessMandate({ scope, currency, maxAmount, perTxnMax, merchants }) {
1108
1162
  };
1109
1163
  }
1110
1164
 
1111
- /** A clearly-local, clearly-not-anchored identifier `guard.agentDid` is just a signing
1112
- * subject in the local path (never resolved against Hedera), but the format should not
1113
- * read as a verified did:hedera when it is not one. */
1165
+ /** A genuine did:key — self-certifying (the verification key is embedded in the DID itself,
1166
+ * §4.1.2), clearly NOT a did:hedera (never resolved against Hedera, never anchored) but still
1167
+ * a REAL, resolvable DID: any MAGP verifier can check a signature against it completely
1168
+ * offline. This matters once --gateway is on (below): the harness's second local process
1169
+ * verifies the agent's requests via key-in-DID, exactly like a real did:hedera counterparty
1170
+ * would, just with no chain underneath it. */
1114
1171
  function harnessAgentDid(publicKeyHex) {
1115
- return `did:key:local-${crypto.createHash('sha256').update(publicKeyHex, 'hex').digest('hex').slice(0, 32)}`;
1172
+ return buildDidKey(rawPublicKeyFromSpkiHex(publicKeyHex));
1116
1173
  }
1117
1174
 
1118
1175
  function harnessRulesFile(mandate, sopDocument) {
@@ -1128,6 +1185,128 @@ function harnessRulesFile(mandate, sopDocument) {
1128
1185
  ) + '\n';
1129
1186
  }
1130
1187
 
1188
+ // ---------- --harness --gateway: a second local process, still zero network -----------------
1189
+ //
1190
+ // Everything above is ONE process: guardToolLocal() decides, and the SAME process holds the
1191
+ // tool. "Without MetaMynd, you can be bypassed" (the harness README says so directly) — call
1192
+ // bookFlight() instead of gatedBookFlight() and nothing stops you, because there is no
1193
+ // counterparty in the loop to disagree with you.
1194
+ //
1195
+ // --gateway adds one: a SEPARATE local process, using the real @metamynd/agentsafe-mcp-guard
1196
+ // (the same package a production Service uses), that independently re-verifies every signed
1197
+ // request against the SAME metamynd-rules.json — not by trusting the agent process, by
1198
+ // checking the Ed25519 signature itself via the agent's did:key (key-in-DID, §4.1.2, fully
1199
+ // offline). Still no account, still no network call, still free.
1200
+ //
1201
+ // What this DOES close: the agent process lying to itself. A compromised or dishonest agent
1202
+ // that skips its own guardToolLocal() call, or calls bookFlight() directly, gets nothing —
1203
+ // the tool only runs in the gateway process now.
1204
+ //
1205
+ // What this does NOT close (be precise, this is a local demo, not the hosted platform):
1206
+ // nonce replay and cumulative-spend across many calls. Those need a STATEFUL authority — the
1207
+ // hosted gate's `requireAuthorization` claims a real, single-use authorizationId against a
1208
+ // database. A local harness has no such database (that is the whole point of --harness), so
1209
+ // this gateway does per-request re-evaluation only, same as the hosted gateway's baseline
1210
+ // before `requireAuthorization` is added. The README says so.
1211
+
1212
+ function harnessGatewayServerFile(scope, gatewayPort, agentDid) {
1213
+ return `#!/usr/bin/env node
1214
+ // harness-gateway.mjs — a SEPARATE process from your agent. It holds the tool (bookFlight
1215
+ // below never runs anywhere else) and independently re-verifies every request against
1216
+ // ../metamynd-rules.json using the REAL @metamynd/agentsafe-mcp-guard — the same package a
1217
+ // production Service uses, just pointed at a local file instead of a hosted issuer. See
1218
+ // ../README.md#--gateway for exactly what this does and does not close.
1219
+ import { readFileSync } from 'node:fs';
1220
+ import http from 'node:http';
1221
+ import { createMcpGuard } from '${MCP_GUARD_PKG}';
1222
+
1223
+ const PORT = Number(process.env.PORT || ${gatewayPort});
1224
+ // The agent's did:key, fixed at scaffold time — a request claiming to be any OTHER agentDid
1225
+ // fails BUNDLE_SUBJECT_MISMATCH, not just an unmatched-signature error, because the bundle
1226
+ // this gateway serves is only ever this one agent's.
1227
+ const AGENT_DID = '${agentDid}';
1228
+
1229
+ // Reshapes the harness's own rules-file shape ({mandate, sops:[{standardKey,document}],
1230
+ // standards:[{standardKey,document}]}) into what agentsafe-mcp-guard's verifyRequest expects
1231
+ // ({mandates:[{action,document}], sops:[{id,document}], standards:[{key,document}]}) — a pure
1232
+ // format adapter, not a second source of truth: both this and index.mjs's dashboard read the
1233
+ // SAME ../metamynd-rules.json.
1234
+ function bundleFromRules(rules) {
1235
+ return {
1236
+ mandates: [{ action: '${scope}', document: rules.mandate }],
1237
+ sops: (rules.sops ?? []).map((s) => ({ id: s.standardKey, document: s.document })),
1238
+ standards: (rules.standards ?? []).map((s) => ({ key: s.standardKey, document: s.document })),
1239
+ };
1240
+ }
1241
+
1242
+ // No serviceKey: this gateway only calls verifyRequest() (re-check a signed request), not the
1243
+ // mutual-handshake methods, which are the only thing that needs one. No issuerApi either — the
1244
+ // whole point of --harness is no network; fetchBundle reads the SAME rules file the dashboard
1245
+ // and your agent process both read, so editing it takes effect on the next request everywhere.
1246
+ const guard = createMcpGuard({
1247
+ serviceDid: 'did:local:${scope}-gateway',
1248
+ fetchBundle: async (agentDid) => {
1249
+ if (agentDid !== AGENT_DID) return { subject: AGENT_DID, mandates: [], sops: [], standards: [] };
1250
+ const rules = JSON.parse(readFileSync('../metamynd-rules.json', 'utf8'));
1251
+ return { subject: AGENT_DID, ...bundleFromRules(rules) };
1252
+ },
1253
+ });
1254
+
1255
+ // One protected route per gated action in index.mjs. A path with no route below is refused —
1256
+ // there is nothing to fall through TO; this gateway IS the tool, not a proxy in front of one.
1257
+ const ROUTES = {
1258
+ '/book-flight': { action: '${scope}', run: async (args) => ({ pnr: 'PNR-DEMO', ...args }) },
1259
+ '/raise-limit': { action: 'permissions.update', run: async (args) => ({ updated: true, ...args }) },
1260
+ };
1261
+
1262
+ function readBody(req) {
1263
+ return new Promise((resolve, reject) => {
1264
+ const chunks = [];
1265
+ req.on('data', (c) => chunks.push(c));
1266
+ req.on('end', () => resolve(Buffer.concat(chunks)));
1267
+ req.on('error', reject);
1268
+ });
1269
+ }
1270
+
1271
+ const server = http.createServer(async (req, res) => {
1272
+ const route = ROUTES[req.url];
1273
+ const send = (status, body, decision) => {
1274
+ const headers = { 'content-type': 'application/json' };
1275
+ if (decision) headers['x-agentsafe-decision'] = decision;
1276
+ res.writeHead(status, headers);
1277
+ res.end(JSON.stringify(body));
1278
+ };
1279
+ if (req.method !== 'POST' || !route) return send(404, { decision: 'block', reasonCode: 'NO_SUCH_ROUTE' });
1280
+ try {
1281
+ const raw = await readBody(req);
1282
+ const { signed, args } = JSON.parse(raw.toString('utf8') || '{}');
1283
+ const verdict = await guard.verifyRequest({ ...signed, action: route.action });
1284
+ if (verdict.decision !== 'allow' && verdict.decision !== 'observe') {
1285
+ console.log('[harness-gateway] ' + verdict.decision.toUpperCase() + ' ' + req.url + ' — ' + verdict.reasonCode + ' (re-evaluated independently, did not trust the agent)');
1286
+ return send(403, verdict, verdict.decision);
1287
+ }
1288
+ console.log('[harness-gateway] ALLOW ' + req.url + ' — running the real tool here, not in the agent process');
1289
+ return send(200, await route.run(args ?? {}), verdict.decision);
1290
+ } catch (err) {
1291
+ return send(502, { decision: 'block', reasonCode: 'GATEWAY_ERROR', error: String(err?.message ?? err) });
1292
+ }
1293
+ });
1294
+
1295
+ server.listen(PORT, () => {
1296
+ console.log('[harness-gateway] listening on :' + PORT + ' — the only place your tools run.');
1297
+ console.log('[harness-gateway] re-verifying against ../metamynd-rules.json, independently of index.mjs.');
1298
+ });
1299
+ `;
1300
+ }
1301
+
1302
+ function harnessGatewayPackageJson(slug) {
1303
+ return JSON.stringify(
1304
+ { name: slug + '-harness-gateway', version: '0.1.0', private: true, type: 'module', scripts: { start: 'node harness-gateway.mjs' }, dependencies: { [MCP_GUARD_PKG]: MCP_GUARD_VERSION } },
1305
+ null,
1306
+ 2,
1307
+ ) + '\n';
1308
+ }
1309
+
1131
1310
  function harnessServerFile() {
1132
1311
  return `// harness-server.mjs — the free local governance dashboard. Zero dependencies.
1133
1312
  // Runs in-process with your agent: shows the rules in force, lets you add/edit/remove SOP
@@ -1498,13 +1677,15 @@ setInterval(refresh, 3000);
1498
1677
  `;
1499
1678
  }
1500
1679
 
1501
- function harnessIndexFile(scope, perTxnMax, port) {
1680
+ function harnessIndexFile(scope, perTxnMax, port, withGateway, gatewayPort) {
1502
1681
  const under = Math.max(1, Math.round(perTxnMax * 0.5));
1503
1682
  const over = Math.round(perTxnMax + 100);
1504
1683
  return `// index.mjs — your agent, governed entirely on this machine. No account, no network call
1505
1684
  // for a decision: guardToolLocal() decides allow/block/escalate against ./metamynd-rules.json
1506
1685
  // (edit it directly, or at the dashboard). An escalate is held here for YOU to approve —
1507
- // there is no hosted owner queue in this mode, so open the dashboard URL printed below.
1686
+ // there is no hosted owner queue in this mode, so open the dashboard URL printed below.${withGateway ? `
1687
+ // Your tools run in ./harness-gateway.mjs, a SEPARATE process — it independently re-verifies
1688
+ // every signed request for itself. See README.md#--gateway for what that closes.` : ''}
1508
1689
  import { readFileSync } from 'node:fs';
1509
1690
  import { createGuard } from '${GUARD_PKG}';
1510
1691
  import { startDashboard } from './harness-server.mjs';
@@ -1521,22 +1702,41 @@ const dashboard = startDashboard({
1521
1702
  rulesPath: './metamynd-rules.json',
1522
1703
  logPath: './metamynd-harness.log.jsonl',
1523
1704
  });
1524
- console.log('\\x1b[2m dashboard: ' + dashboard.url + ' (rules, approvals, decision log)\\x1b[0m\\n');
1705
+ console.log('\\x1b[2m dashboard: ' + dashboard.url + ' (rules, approvals, decision log)\\x1b[0m');
1706
+ ${withGateway ? `console.log('\\x1b[2m gateway : http://localhost:${gatewayPort} (a SEPARATE process — run \\'npm start\\' in ./harness-gateway first)\\x1b[0m\\n');` : `console.log('');`}
1525
1707
 
1526
1708
  // Reads the CURRENT rules file fresh every call — editing it (by hand, or at the dashboard)
1527
1709
  // takes effect on the next decision, no restart, matching the "no redeploy" experience the
1528
- // hosted platform gives you.
1710
+ // hosted platform gives you. The gateway process (below, when scaffolded) reads the SAME file.
1529
1711
  const getBundle = () => JSON.parse(readFileSync('./metamynd-rules.json', 'utf8'));
1530
-
1712
+ ${withGateway ? `
1713
+ const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://localhost:${gatewayPort}';
1714
+ // Calls the gateway process instead of a local function — there is no raw bookFlight() or
1715
+ // raiseOwnLimit() in THIS file to call directly. buildSignedRequest() is pure (no network, no
1716
+ // issuer): it builds and signs the same canonical message a real gate would verify, entirely
1717
+ // offline, using this agent's own did:key — the gateway verifies that signature for itself.
1718
+ async function callGateway(path, action, args) {
1719
+ const signed = guard.buildSignedRequest({ action, amount: args.amount, currency: 'USD', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' } });
1720
+ const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ signed, args }) });
1721
+ const body = await res.json().catch(() => null);
1722
+ if (!res.ok) {
1723
+ const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
1724
+ err.name = 'GovernanceBlocked';
1725
+ err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
1726
+ throw err;
1727
+ }
1728
+ return body;
1729
+ }
1730
+ ` : `
1531
1731
  // --- Your real tool. Replace the body with your actual implementation. ---
1532
1732
  async function bookFlight(args) {
1533
1733
  return { pnr: 'PNR-DEMO', ...args };
1534
1734
  }
1535
-
1735
+ `}
1536
1736
  // --- The GATED version. Register THIS with your agent instead of the raw handler. ---
1537
1737
  const gatedBookFlight = guard.guardToolLocal(
1538
1738
  '${scope}', // = your mandate scope
1539
- bookFlight,
1739
+ ${withGateway ? `(args) => callGateway('/book-flight', '${scope}', args)` : 'bookFlight'},
1540
1740
  (a) => ({ // map tool args → gate inputs
1541
1741
  amount: a.amount,
1542
1742
  merchant: a.merchant,
@@ -1546,14 +1746,14 @@ const gatedBookFlight = guard.guardToolLocal(
1546
1746
  );
1547
1747
 
1548
1748
  // --- A tool the agent was NEVER granted. Wrapping it is the demonstration: there is no
1549
- // --- rule anywhere forbidding this. The mandate simply never mentioned the action.
1749
+ // --- rule anywhere forbidding this. The mandate simply never mentioned the action.${withGateway ? '' : `
1550
1750
  async function raiseOwnLimit(args) {
1551
1751
  return { updated: true, ...args }; // never runs, and that is the point
1552
- }
1752
+ }`}
1553
1753
 
1554
1754
  const gatedRaiseOwnLimit = guard.guardToolLocal(
1555
1755
  'permissions.update', // an action NOT in the mandate
1556
- raiseOwnLimit,
1756
+ ${withGateway ? `(args) => callGateway('/raise-limit', 'permissions.update', args)` : 'raiseOwnLimit'},
1557
1757
  (a) => ({ amount: a.amount, merchant: a.merchant, context: { tool: 'permissions-update' } }),
1558
1758
  getBundle,
1559
1759
  );
@@ -1629,7 +1829,23 @@ console.log(dim(' - step 4 needed no rule to stop it. The agent could not wide
1629
1829
  console.log(dim(' authority, because it cannot name an action nobody delegated to it.'));
1630
1830
  console.log(dim(' - the blocked call never reached your tool at all.'));
1631
1831
  console.log(dim(' - every decision is in ./metamynd-harness.log.jsonl - yours, locally.'));
1832
+ console.log('');${withGateway ? `
1833
+ console.log(bold(' Checked twice, by two processes.') + ' bookFlight() lives in ./harness-gateway.mjs -');
1834
+ console.log(dim(' not here. It independently re-verified every attempt above against the SAME'));
1835
+ console.log(dim(' ./metamynd-rules.json, over a signed request, before running your tool.'));
1836
+ console.log('');
1837
+ console.log(dim(' What --gateway does NOT close: nonce replay and cumulative spend across many'));
1838
+ console.log(dim(' calls. Those need a STATEFUL authority (the hosted gate\\'s requireAuthorization'));
1839
+ console.log(dim(' claims a real, single-use id against a database) - a local harness has none.'));
1840
+ console.log(dim(' See README.md#--gateway for exactly what this does and does not prove.'));
1632
1841
  console.log('');
1842
+ console.log(' Edit ./metamynd-rules.json (or the dashboard) and run again - the outcome');
1843
+ console.log(dim(' changes, in BOTH processes, from the one file. That is the point.'));
1844
+ console.log('');
1845
+ console.log(dim(' Ready for more than one machine, a queue someone else can approve from,'));
1846
+ console.log(dim(' anchored evidence, KYC/KYB-backed identity, or nonce/cumulative-spend closure?'));
1847
+ console.log(dim(' That is the hosted platform - drop --harness and provision there; the same'));
1848
+ console.log(dim(' guardTool() call keeps working, sealed by a real gate instead of this file.'));` : `
1633
1849
  console.log(bold(' Without MetaMynd, you can be bypassed.') + ' bookFlight() above runs in THIS');
1634
1850
  console.log(dim(' process - call it directly instead of gatedBookFlight and nothing stops you.'));
1635
1851
  console.log(dim(' --harness proves your policy logic; it does not enforce it against that.'));
@@ -1637,10 +1853,11 @@ console.log('');
1637
1853
  console.log(' Edit ./metamynd-rules.json (or the dashboard) and run again - the outcome');
1638
1854
  console.log(dim(' changes. This file does not. That is the point.'));
1639
1855
  console.log('');
1640
- console.log(dim(' Ready for more than one machine, a queue someone else can approve from,'));
1641
- console.log(dim(' anchored evidence, or KYC/KYB-backed identity, AND a separate gateway process'));
1642
- console.log(dim(' that closes the bypass above? That is the hosted platform - drop --harness'));
1643
- console.log(dim(' and provision there; the same guardTool() call keeps working.'));
1856
+ console.log(dim(' Ready for a SEPARATE process that closes the bypass above, still free and'));
1857
+ console.log(dim(' local? Re-scaffold with --gateway. Ready for more than one machine, a queue'));
1858
+ console.log(dim(' someone else can approve from, anchored evidence, or KYC/KYB-backed identity?'));
1859
+ console.log(dim(' That is the hosted platform - drop --harness and provision there; the same'));
1860
+ console.log(dim(' guardTool() call keeps working.'));`}
1644
1861
  console.log('');
1645
1862
  dashboard.close();
1646
1863
  `;
@@ -1661,7 +1878,7 @@ function harnessPackageJson(slug) {
1661
1878
  ) + '\n';
1662
1879
  }
1663
1880
 
1664
- function harnessReadme(slug, scope, port) {
1881
+ function harnessReadme(slug, scope, port, withGateway, gatewayPort) {
1665
1882
  return `# ${slug}
1666
1883
 
1667
1884
  A free, local MetaMynd/AgentSafe governance harness — your own rules, your own identity,
@@ -1670,8 +1887,8 @@ decided entirely on this machine. No account, no network call for a decision.
1670
1887
  ## Run
1671
1888
 
1672
1889
  \`\`\`bash
1673
- npm install
1674
- npm start
1890
+ npm install${withGateway ? ' && (cd harness-gateway && npm install)' : ''}
1891
+ ${withGateway ? `(cd harness-gateway && npm start &) # the second process, in the background\n` : ''}npm start
1675
1892
  \`\`\`
1676
1893
 
1677
1894
  You should see an ALLOW, a BLOCK (over the per-transaction cap), an ESCALATE (high risk —
@@ -1679,18 +1896,45 @@ open the dashboard to approve it), and a BLOCK (an action outside the mandate en
1679
1896
 
1680
1897
  ## Files
1681
1898
 
1682
- - \`agent.metamynd.json\` — your local identity (a generated Ed25519 keypair; \`agentDid\` is a
1683
- local label, not an anchored/verifiable one). **Contains a secret key never commit it.**
1899
+ - \`agent.metamynd.json\` — your local identity: a generated Ed25519 keypair, and a REAL
1900
+ \`did:key\` (self-certifying the verification key is embedded in the DID itself, so a
1901
+ signature against it is checkable completely offline). Not anchored to Hedera; that's the
1902
+ hosted platform. **Contains a secret key — never commit it.**
1684
1903
  - \`metamynd-rules.json\` — your rules: the mandate (scope + spend limits) and SOP (extra checks).
1685
- Edit it directly, or at the dashboard. Reloaded on every decision — no restart.
1904
+ Edit it directly, or at the dashboard. Reloaded on every decision — no restart${withGateway ? ', in BOTH processes' : ''}.
1686
1905
  - \`metamynd-harness.log.jsonl\` — every decision this agent made, append-only.
1687
1906
  - \`harness-server.mjs\` — the local dashboard (port ${port}): rules, pending approvals, decision log.
1688
1907
  - \`index.mjs\` — wraps a tool with \`guard.guardToolLocal(...)\`; the tool only runs when the
1689
- LOCAL rules permit it.
1908
+ LOCAL rules permit it${withGateway ? ', AND the SEPARATE gateway process (below) independently agrees' : ''}.${withGateway ? `
1909
+ - \`harness-gateway/harness-gateway.mjs\` — a SECOND process. Your tools live HERE now, not in
1910
+ \`index.mjs\`. It re-verifies every signed request for itself against the SAME
1911
+ \`../metamynd-rules.json\`, using the real \`@metamynd/agentsafe-mcp-guard\` — the identical
1912
+ package a production Service uses, just pointed at a local file instead of a hosted issuer.` : ''}
1690
1913
 
1691
1914
  ## What this is not
1692
1915
 
1693
- **Without MetaMynd, you can be bypassed.** Everything below is why, precisely.
1916
+ ${withGateway ? `**\`--gateway\` closes one real gap, not every gap.** Precisely:
1917
+
1918
+ **Closed:** the agent process lying to itself. Call \`gatedBookFlight\`'s underlying handler
1919
+ directly (or skip \`index.mjs\` and hand a forged/altered request straight to
1920
+ \`harness-gateway.mjs\`) — either way, the gateway independently re-verifies the Ed25519
1921
+ signature and re-evaluates the SAME rules file for itself. There is no raw \`bookFlight()\` left
1922
+ in \`index.mjs\` to call for a shortcut, and a signature over an altered amount/merchant fails
1923
+ verification regardless of which process sent it.
1924
+
1925
+ **NOT closed:** nonce replay and cumulative spend across many calls. Those need a STATEFUL
1926
+ authority — the hosted gate's \`requireAuthorization\` atomically claims a real, single-use
1927
+ \`authorizationId\` against a database before a Service executes anything (see
1928
+ \`@metamynd/agentsafe-mcp-guard\`'s own README). A local harness has no database; that is the
1929
+ whole point of \`--harness\`. \`harness-gateway.mjs\` re-checks POLICY per request, which is
1930
+ real and worth having, but replaying the exact same signed request twice is NOT refused here
1931
+ the way it would be against the hosted gate.
1932
+
1933
+ Also not closed by \`--gateway\` alone: cross-party trust (nobody but you can verify this agent's
1934
+ identity or its decisions), evidence anyone but you can audit, a dashboard reachable when this
1935
+ machine is off, an owner queue someone else can approve from. That's the hosted platform
1936
+ (\`npx create-metamynd-agent\`, without \`--harness\`) — same \`guardTool()\` call, same rules
1937
+ shape, so upgrading later is a config change, not a rewrite.` : `**Without MetaMynd, you can be bypassed.** Everything below is why, precisely.
1694
1938
 
1695
1939
  No anchored/verifiable identity, no cross-party trust, no evidence anyone but you can audit,
1696
1940
  no dashboard reachable when this machine is off, no owner queue someone else can approve from.
@@ -1701,9 +1945,11 @@ It is also **not a separate enforcement boundary**. \`guardToolLocal()\` (in \`i
1701
1945
  cooperative library this process embeds — call the tool handler directly instead of the guarded
1702
1946
  one and nothing stops you, because there is no second party in the loop to disagree with you.
1703
1947
  That's structural, not a bug: use this harness to govern your own agent's own honest behavior,
1704
- not as a defense against an agent (or a person) actively trying to get around it. The hosted
1705
- platform's default scaffold doesn't have this gap, because a SEPARATE gateway process re-verifies
1706
- the agent's signed authority for itself instead of trusting that the agent's own guard ran.
1948
+ not as a defense against an agent (or a person) actively trying to get around it. Re-scaffold
1949
+ with \`--gateway\` for a SECOND local process that closes exactly this, still free and offline
1950
+ (see README#--gateway once scaffolded) or drop \`--harness\` entirely for the hosted platform's
1951
+ default scaffold, which has this gap closed AND closes nonce replay/cumulative spend, because a
1952
+ SEPARATE gateway process re-verifies the agent's signed authority against a real, stateful gate.`}
1707
1953
  `;
1708
1954
  }
1709
1955
 
@@ -1729,6 +1975,8 @@ async function runHarness(args) {
1729
1975
  const merchantsRaw = await pick('merchants', 'Allowed merchants (comma-sep, blank = any)', Array.isArray(fileConfig?.merchants) ? fileConfig.merchants.join(',') : '');
1730
1976
  const merchants = String(merchantsRaw).split(',').map((s) => s.trim()).filter(Boolean);
1731
1977
  const port = Number(args.port) || 4400;
1978
+ const withGateway = !!args.gateway;
1979
+ const gatewayPort = Number(args['gateway-port']) || DEFAULT_GATEWAY_PORT;
1732
1980
  const slug = slugify(name);
1733
1981
  const outDir = resolve(String(args.out || (interactive ? await ask(rl, 'Output directory', `./${slug}`) : `./${slug}`)));
1734
1982
  rl?.close();
@@ -1749,18 +1997,32 @@ async function runHarness(args) {
1749
1997
  writeFileSafe(outDir, 'agent.metamynd.json', JSON.stringify({ agentDid, agentKey: privateKeyHex, mode: 'harness' }, null, 2) + '\n', !!args.force);
1750
1998
  writeFileSafe(outDir, 'metamynd-rules.json', harnessRulesFile(mandate, sopDocument), !!args.force);
1751
1999
  writeFileSafe(outDir, 'harness-server.mjs', harnessServerFile(), !!args.force);
1752
- writeFileSafe(outDir, 'index.mjs', harnessIndexFile(scope, perTxnMax, port), !!args.force);
2000
+ writeFileSafe(outDir, 'index.mjs', harnessIndexFile(scope, perTxnMax, port, withGateway, gatewayPort), !!args.force);
1753
2001
  writeFileSafe(outDir, 'package.json', harnessPackageJson(slug), !!args.force);
1754
2002
  writeFileSafe(outDir, '.gitignore', gitignore(), !!args.force);
1755
- writeFileSafe(outDir, 'README.md', harnessReadme(slug, scope, port), !!args.force);
2003
+ writeFileSafe(outDir, 'README.md', harnessReadme(slug, scope, port, withGateway, gatewayPort), !!args.force);
2004
+
2005
+ if (withGateway) {
2006
+ const gwDir = join(outDir, 'harness-gateway');
2007
+ if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
2008
+ writeFileSafe(gwDir, 'harness-gateway.mjs', harnessGatewayServerFile(scope, gatewayPort, agentDid), !!args.force);
2009
+ writeFileSafe(gwDir, 'package.json', harnessGatewayPackageJson(slug), !!args.force);
2010
+ writeFileSafe(gwDir, '.gitignore', gatewayGitignore(), !!args.force);
2011
+ }
1756
2012
 
1757
2013
  const rel = outDir.replace(resolve('.'), '.').replace(/\\/g, '/');
1758
2014
  console.log(`\n${c.green(c.b(' ✓ Done.'))} Your local governance harness is ready.\n`);
1759
2015
  console.log(` ${c.dim('Free, local, no account. Not the hosted platform — see README#what-this-is-not.')}\n`);
1760
2016
  console.log(` Next:`);
1761
2017
  console.log(c.cyan(` cd ${rel}`));
1762
- console.log(c.cyan(` npm install`));
1763
- console.log(c.cyan(` npm start`) + c.dim(' → ALLOW · BLOCK (over cap) · ESCALATE (approve at the dashboard) · BLOCK (ungranted action)\n'));
2018
+ if (withGateway) {
2019
+ console.log(c.cyan(` npm install && (cd harness-gateway && npm install)`));
2020
+ console.log(c.cyan(` (cd harness-gateway && npm start &)`) + c.dim(' → the second process, in the background'));
2021
+ console.log(c.cyan(` npm start`) + c.dim(' → ALLOW · BLOCK (over cap) · ESCALATE (approve at the dashboard) · BLOCK (ungranted action)\n'));
2022
+ } else {
2023
+ console.log(c.cyan(` npm install`));
2024
+ console.log(c.cyan(` npm start`) + c.dim(' → ALLOW · BLOCK (over cap) · ESCALATE (approve at the dashboard) · BLOCK (ungranted action)\n'));
2025
+ }
1764
2026
  console.log(c.dim(` Edit ./metamynd-rules.json any time (by hand, or at http://127.0.0.1:${port}) — no redeploy.\n`));
1765
2027
  }
1766
2028
 
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "create-metamynd-agent",
3
- "version": "0.7.7",
3
+ "version": "0.8.0",
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": {
7
7
  "create-metamynd-agent": "index.mjs"
8
8
  },
9
+ "scripts": {
10
+ "test": "node harness-gateway.smoke.mjs"
11
+ },
9
12
  "files": [
10
13
  "index.mjs",
11
14
  "README.md"