create-metamynd-agent 0.7.8 → 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.
- package/index.mjs +288 -36
- 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
|
-
|
|
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
|
|
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) {
|
|
@@ -1118,11 +1162,14 @@ function harnessMandate({ scope, currency, maxAmount, perTxnMax, merchants }) {
|
|
|
1118
1162
|
};
|
|
1119
1163
|
}
|
|
1120
1164
|
|
|
1121
|
-
/** A
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
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. */
|
|
1124
1171
|
function harnessAgentDid(publicKeyHex) {
|
|
1125
|
-
return
|
|
1172
|
+
return buildDidKey(rawPublicKeyFromSpkiHex(publicKeyHex));
|
|
1126
1173
|
}
|
|
1127
1174
|
|
|
1128
1175
|
function harnessRulesFile(mandate, sopDocument) {
|
|
@@ -1138,6 +1185,128 @@ function harnessRulesFile(mandate, sopDocument) {
|
|
|
1138
1185
|
) + '\n';
|
|
1139
1186
|
}
|
|
1140
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
|
+
|
|
1141
1310
|
function harnessServerFile() {
|
|
1142
1311
|
return `// harness-server.mjs — the free local governance dashboard. Zero dependencies.
|
|
1143
1312
|
// Runs in-process with your agent: shows the rules in force, lets you add/edit/remove SOP
|
|
@@ -1508,13 +1677,15 @@ setInterval(refresh, 3000);
|
|
|
1508
1677
|
`;
|
|
1509
1678
|
}
|
|
1510
1679
|
|
|
1511
|
-
function harnessIndexFile(scope, perTxnMax, port) {
|
|
1680
|
+
function harnessIndexFile(scope, perTxnMax, port, withGateway, gatewayPort) {
|
|
1512
1681
|
const under = Math.max(1, Math.round(perTxnMax * 0.5));
|
|
1513
1682
|
const over = Math.round(perTxnMax + 100);
|
|
1514
1683
|
return `// index.mjs — your agent, governed entirely on this machine. No account, no network call
|
|
1515
1684
|
// for a decision: guardToolLocal() decides allow/block/escalate against ./metamynd-rules.json
|
|
1516
1685
|
// (edit it directly, or at the dashboard). An escalate is held here for YOU to approve —
|
|
1517
|
-
// 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.` : ''}
|
|
1518
1689
|
import { readFileSync } from 'node:fs';
|
|
1519
1690
|
import { createGuard } from '${GUARD_PKG}';
|
|
1520
1691
|
import { startDashboard } from './harness-server.mjs';
|
|
@@ -1531,22 +1702,41 @@ const dashboard = startDashboard({
|
|
|
1531
1702
|
rulesPath: './metamynd-rules.json',
|
|
1532
1703
|
logPath: './metamynd-harness.log.jsonl',
|
|
1533
1704
|
});
|
|
1534
|
-
console.log('\\x1b[2m dashboard: ' + dashboard.url + ' (rules, approvals, decision log)\\x1b[0m
|
|
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('');`}
|
|
1535
1707
|
|
|
1536
1708
|
// Reads the CURRENT rules file fresh every call — editing it (by hand, or at the dashboard)
|
|
1537
1709
|
// takes effect on the next decision, no restart, matching the "no redeploy" experience the
|
|
1538
|
-
// hosted platform gives you.
|
|
1710
|
+
// hosted platform gives you. The gateway process (below, when scaffolded) reads the SAME file.
|
|
1539
1711
|
const getBundle = () => JSON.parse(readFileSync('./metamynd-rules.json', 'utf8'));
|
|
1540
|
-
|
|
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
|
+
` : `
|
|
1541
1731
|
// --- Your real tool. Replace the body with your actual implementation. ---
|
|
1542
1732
|
async function bookFlight(args) {
|
|
1543
1733
|
return { pnr: 'PNR-DEMO', ...args };
|
|
1544
1734
|
}
|
|
1545
|
-
|
|
1735
|
+
`}
|
|
1546
1736
|
// --- The GATED version. Register THIS with your agent instead of the raw handler. ---
|
|
1547
1737
|
const gatedBookFlight = guard.guardToolLocal(
|
|
1548
1738
|
'${scope}', // = your mandate scope
|
|
1549
|
-
bookFlight,
|
|
1739
|
+
${withGateway ? `(args) => callGateway('/book-flight', '${scope}', args)` : 'bookFlight'},
|
|
1550
1740
|
(a) => ({ // map tool args → gate inputs
|
|
1551
1741
|
amount: a.amount,
|
|
1552
1742
|
merchant: a.merchant,
|
|
@@ -1556,14 +1746,14 @@ const gatedBookFlight = guard.guardToolLocal(
|
|
|
1556
1746
|
);
|
|
1557
1747
|
|
|
1558
1748
|
// --- A tool the agent was NEVER granted. Wrapping it is the demonstration: there is no
|
|
1559
|
-
// --- rule anywhere forbidding this. The mandate simply never mentioned the action
|
|
1749
|
+
// --- rule anywhere forbidding this. The mandate simply never mentioned the action.${withGateway ? '' : `
|
|
1560
1750
|
async function raiseOwnLimit(args) {
|
|
1561
1751
|
return { updated: true, ...args }; // never runs, and that is the point
|
|
1562
|
-
}
|
|
1752
|
+
}`}
|
|
1563
1753
|
|
|
1564
1754
|
const gatedRaiseOwnLimit = guard.guardToolLocal(
|
|
1565
1755
|
'permissions.update', // an action NOT in the mandate
|
|
1566
|
-
raiseOwnLimit,
|
|
1756
|
+
${withGateway ? `(args) => callGateway('/raise-limit', 'permissions.update', args)` : 'raiseOwnLimit'},
|
|
1567
1757
|
(a) => ({ amount: a.amount, merchant: a.merchant, context: { tool: 'permissions-update' } }),
|
|
1568
1758
|
getBundle,
|
|
1569
1759
|
);
|
|
@@ -1639,7 +1829,23 @@ console.log(dim(' - step 4 needed no rule to stop it. The agent could not wide
|
|
|
1639
1829
|
console.log(dim(' authority, because it cannot name an action nobody delegated to it.'));
|
|
1640
1830
|
console.log(dim(' - the blocked call never reached your tool at all.'));
|
|
1641
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.'));
|
|
1642
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.'));` : `
|
|
1643
1849
|
console.log(bold(' Without MetaMynd, you can be bypassed.') + ' bookFlight() above runs in THIS');
|
|
1644
1850
|
console.log(dim(' process - call it directly instead of gatedBookFlight and nothing stops you.'));
|
|
1645
1851
|
console.log(dim(' --harness proves your policy logic; it does not enforce it against that.'));
|
|
@@ -1647,10 +1853,11 @@ console.log('');
|
|
|
1647
1853
|
console.log(' Edit ./metamynd-rules.json (or the dashboard) and run again - the outcome');
|
|
1648
1854
|
console.log(dim(' changes. This file does not. That is the point.'));
|
|
1649
1855
|
console.log('');
|
|
1650
|
-
console.log(dim(' Ready for
|
|
1651
|
-
console.log(dim('
|
|
1652
|
-
console.log(dim('
|
|
1653
|
-
console.log(dim(' and provision there; the same
|
|
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.'));`}
|
|
1654
1861
|
console.log('');
|
|
1655
1862
|
dashboard.close();
|
|
1656
1863
|
`;
|
|
@@ -1671,7 +1878,7 @@ function harnessPackageJson(slug) {
|
|
|
1671
1878
|
) + '\n';
|
|
1672
1879
|
}
|
|
1673
1880
|
|
|
1674
|
-
function harnessReadme(slug, scope, port) {
|
|
1881
|
+
function harnessReadme(slug, scope, port, withGateway, gatewayPort) {
|
|
1675
1882
|
return `# ${slug}
|
|
1676
1883
|
|
|
1677
1884
|
A free, local MetaMynd/AgentSafe governance harness — your own rules, your own identity,
|
|
@@ -1680,8 +1887,8 @@ decided entirely on this machine. No account, no network call for a decision.
|
|
|
1680
1887
|
## Run
|
|
1681
1888
|
|
|
1682
1889
|
\`\`\`bash
|
|
1683
|
-
npm install
|
|
1684
|
-
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
|
|
1685
1892
|
\`\`\`
|
|
1686
1893
|
|
|
1687
1894
|
You should see an ALLOW, a BLOCK (over the per-transaction cap), an ESCALATE (high risk —
|
|
@@ -1689,18 +1896,45 @@ open the dashboard to approve it), and a BLOCK (an action outside the mandate en
|
|
|
1689
1896
|
|
|
1690
1897
|
## Files
|
|
1691
1898
|
|
|
1692
|
-
- \`agent.metamynd.json\` — your local identity
|
|
1693
|
-
|
|
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.**
|
|
1694
1903
|
- \`metamynd-rules.json\` — your rules: the mandate (scope + spend limits) and SOP (extra checks).
|
|
1695
|
-
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' : ''}.
|
|
1696
1905
|
- \`metamynd-harness.log.jsonl\` — every decision this agent made, append-only.
|
|
1697
1906
|
- \`harness-server.mjs\` — the local dashboard (port ${port}): rules, pending approvals, decision log.
|
|
1698
1907
|
- \`index.mjs\` — wraps a tool with \`guard.guardToolLocal(...)\`; the tool only runs when the
|
|
1699
|
-
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.` : ''}
|
|
1700
1913
|
|
|
1701
1914
|
## What this is not
|
|
1702
1915
|
|
|
1703
|
-
|
|
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.
|
|
1704
1938
|
|
|
1705
1939
|
No anchored/verifiable identity, no cross-party trust, no evidence anyone but you can audit,
|
|
1706
1940
|
no dashboard reachable when this machine is off, no owner queue someone else can approve from.
|
|
@@ -1711,9 +1945,11 @@ It is also **not a separate enforcement boundary**. \`guardToolLocal()\` (in \`i
|
|
|
1711
1945
|
cooperative library this process embeds — call the tool handler directly instead of the guarded
|
|
1712
1946
|
one and nothing stops you, because there is no second party in the loop to disagree with you.
|
|
1713
1947
|
That's structural, not a bug: use this harness to govern your own agent's own honest behavior,
|
|
1714
|
-
not as a defense against an agent (or a person) actively trying to get around it.
|
|
1715
|
-
|
|
1716
|
-
|
|
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.`}
|
|
1717
1953
|
`;
|
|
1718
1954
|
}
|
|
1719
1955
|
|
|
@@ -1739,6 +1975,8 @@ async function runHarness(args) {
|
|
|
1739
1975
|
const merchantsRaw = await pick('merchants', 'Allowed merchants (comma-sep, blank = any)', Array.isArray(fileConfig?.merchants) ? fileConfig.merchants.join(',') : '');
|
|
1740
1976
|
const merchants = String(merchantsRaw).split(',').map((s) => s.trim()).filter(Boolean);
|
|
1741
1977
|
const port = Number(args.port) || 4400;
|
|
1978
|
+
const withGateway = !!args.gateway;
|
|
1979
|
+
const gatewayPort = Number(args['gateway-port']) || DEFAULT_GATEWAY_PORT;
|
|
1742
1980
|
const slug = slugify(name);
|
|
1743
1981
|
const outDir = resolve(String(args.out || (interactive ? await ask(rl, 'Output directory', `./${slug}`) : `./${slug}`)));
|
|
1744
1982
|
rl?.close();
|
|
@@ -1759,18 +1997,32 @@ async function runHarness(args) {
|
|
|
1759
1997
|
writeFileSafe(outDir, 'agent.metamynd.json', JSON.stringify({ agentDid, agentKey: privateKeyHex, mode: 'harness' }, null, 2) + '\n', !!args.force);
|
|
1760
1998
|
writeFileSafe(outDir, 'metamynd-rules.json', harnessRulesFile(mandate, sopDocument), !!args.force);
|
|
1761
1999
|
writeFileSafe(outDir, 'harness-server.mjs', harnessServerFile(), !!args.force);
|
|
1762
|
-
writeFileSafe(outDir, 'index.mjs', harnessIndexFile(scope, perTxnMax, port), !!args.force);
|
|
2000
|
+
writeFileSafe(outDir, 'index.mjs', harnessIndexFile(scope, perTxnMax, port, withGateway, gatewayPort), !!args.force);
|
|
1763
2001
|
writeFileSafe(outDir, 'package.json', harnessPackageJson(slug), !!args.force);
|
|
1764
2002
|
writeFileSafe(outDir, '.gitignore', gitignore(), !!args.force);
|
|
1765
|
-
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
|
+
}
|
|
1766
2012
|
|
|
1767
2013
|
const rel = outDir.replace(resolve('.'), '.').replace(/\\/g, '/');
|
|
1768
2014
|
console.log(`\n${c.green(c.b(' ✓ Done.'))} Your local governance harness is ready.\n`);
|
|
1769
2015
|
console.log(` ${c.dim('Free, local, no account. Not the hosted platform — see README#what-this-is-not.')}\n`);
|
|
1770
2016
|
console.log(` Next:`);
|
|
1771
2017
|
console.log(c.cyan(` cd ${rel}`));
|
|
1772
|
-
|
|
1773
|
-
|
|
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
|
+
}
|
|
1774
2026
|
console.log(c.dim(` Edit ./metamynd-rules.json any time (by hand, or at http://127.0.0.1:${port}) — no redeploy.\n`));
|
|
1775
2027
|
}
|
|
1776
2028
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.
|
|
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"
|