create-metamynd-agent 0.14.10 → 0.14.18
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 +169 -38
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -61,7 +61,11 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
|
|
|
61
61
|
// 0.18.0 exports keepAliveFetch. No template change.
|
|
62
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
63
|
// the action resumes", and below this the escalation never reached the owner's queue (decided in-process, no escalationId).
|
|
64
|
-
|
|
64
|
+
// 0.19.0: a handshake signs only a plain-token nonce (a malicious Service could otherwise obtain this agent's signature on an
|
|
65
|
+
// authorize request of its choosing, MAGP §8.2). Required for every scaffolded agent.
|
|
66
|
+
// 0.20.0: guard.capture() signs as the agent (MAGP-SETTLE-v1) — a hold nobody claimed is settled only by its agent, a
|
|
67
|
+
// counterparty the owner registered, or (an open testnet owner) anyone (MAGP §8.7.4).
|
|
68
|
+
const GUARD_VERSION = '^0.20.0';
|
|
65
69
|
/** The harness entry point's config load, shared by both harness templates: a fresh clone has no
|
|
66
70
|
* agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
|
|
67
71
|
function harnessConfigLoad() {
|
|
@@ -128,7 +132,20 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
|
128
132
|
// 0.19.0: a request the gateway's own policy escalates, carrying an authorization a PERSON approved, is executed (once,
|
|
129
133
|
// with every claim binding) instead of refused RISK_REVIEW (MAGP §8.7.18). Required: the scaffold tells the developer
|
|
130
134
|
// "approve it in the dashboard and the action resumes" — below this the gateway refused exactly that.
|
|
131
|
-
|
|
135
|
+
// 0.20.0: `allowedAgents` pins a Service to the agents it acts for (MAGP §16.3) — refused AGENT_NOT_ADMITTED otherwise.
|
|
136
|
+
// Required: every scaffolded gateway now sets it, and an older guard would ignore the option and serve ANY agent whose
|
|
137
|
+
// own owner granted it the same action, with this owner's credentials (XT-1, pre-beta evaluation 2026-10-03).
|
|
138
|
+
// 0.21.0: guard.reportOutcome(). Required: the scaffolded gateways report what they ran or refused (MAGP §16.4), and
|
|
139
|
+
// agentsafe-http-gateway's reportOutcomes needs it.
|
|
140
|
+
// 0.22.0: allowedAgents is required, gatewayOwnerPrincipal binds the gateway to the owner of its credentials (an admitted
|
|
141
|
+
// agent of any other owner: GATEWAY_OWNER_MISMATCH), per-route admission (CREDENTIAL_PROFILE_NOT_PERMITTED), the refusal is
|
|
142
|
+
// AGENT_NOT_ADMITTED, and a handshake signs only a plain-token nonce. Required: every scaffolded gateway sets the owner.
|
|
143
|
+
// 0.23.0: acceptCounterpartyChallenge() — a Service signs its acceptance of an owner's registration (MAGP §8.7.6). No
|
|
144
|
+
// template change; required with GATEWAY_VERSION below so the gateway never resolves a second guard of its own.
|
|
145
|
+
// 0.24.0: reportOutcome() signs a reportId and an occurrence count (aggregated refusals, a durable spool).
|
|
146
|
+
const MCP_GUARD_VERSION = '^0.24.0';
|
|
147
|
+
/** A DID as it may appear inside a generated string literal (the gateway's allowedAgents pin): no quote, backslash or space. */
|
|
148
|
+
const SAFE_DID = /^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/;
|
|
132
149
|
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
133
150
|
// 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
|
|
134
151
|
// never scaffold a range that could resolve below it.
|
|
@@ -151,7 +168,13 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
|
151
168
|
// 0.17.0: depends on agentsafe-mcp-guard ^0.18.0 and exports keepAliveFetch. No template change.
|
|
152
169
|
// 0.18.0: depends on agentsafe-mcp-guard ^0.19.0 (an approved escalation executes). Required with MCP_GUARD_VERSION
|
|
153
170
|
// above, so the gateway never resolves a second, older guard of its own.
|
|
154
|
-
|
|
171
|
+
// 0.19.0: depends on agentsafe-mcp-guard ^0.20.0 (allowedAgents) and reads AGENTSAFE_ALLOWED_AGENTS. Required with
|
|
172
|
+
// MCP_GUARD_VERSION above, so the gateway never resolves a second, older guard of its own.
|
|
173
|
+
// 0.20.0: `reportOutcomes`, which both hosted gateway templates set; depends on agentsafe-mcp-guard ^0.21.0.
|
|
174
|
+
// 0.21.0: per-route allowedAgents; depends on agentsafe-mcp-guard ^0.22.0.
|
|
175
|
+
// 0.22.0: depends on agentsafe-mcp-guard ^0.23.0.
|
|
176
|
+
// 0.23.0: refusals aggregated with their count, and `reportSpool` (both hosted templates set it); depends on agentsafe-mcp-guard ^0.24.0.
|
|
177
|
+
const GATEWAY_VERSION = '^0.23.0';
|
|
155
178
|
|
|
156
179
|
/** Appended to every scaffolded gateway server: give hold settlements still running a bounded moment on shutdown. */
|
|
157
180
|
const DRAIN_ON_SHUTDOWN = `
|
|
@@ -995,12 +1018,27 @@ async function resolveHostedRulePack(base, key, { financial, perTxnMax, maxAmoun
|
|
|
995
1018
|
* dashboard registration shows its own dialog before setting the flag); see the printed message
|
|
996
1019
|
* either way.
|
|
997
1020
|
*/
|
|
998
|
-
async function registerGatewayCounterparty(base, token, did, label) {
|
|
1021
|
+
async function registerGatewayCounterparty(base, token, did, label, purpose = 'claim', privateKeyHex = null) {
|
|
999
1022
|
try {
|
|
1023
|
+
// The gateway accepts its own registration (MAGP §8.7.6): ask for a challenge naming this owner, sign it with the gateway's
|
|
1024
|
+
// key (generated here, so this process holds it), and register with that proof. A DID is never registered on its text.
|
|
1025
|
+
let proof;
|
|
1026
|
+
if (privateKeyHex) {
|
|
1027
|
+
const ch = await fetch(`${base}/policy/counterparties/challenge`, {
|
|
1028
|
+
method: 'POST',
|
|
1029
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
1030
|
+
body: JSON.stringify({ did, purpose }),
|
|
1031
|
+
});
|
|
1032
|
+
const chJson = await ch.json().catch(() => null);
|
|
1033
|
+
if (!ch.ok || !chJson?.data?.message) return { ok: false, message: chJson?.message ?? `challenge HTTP ${ch.status}` };
|
|
1034
|
+
proof = { challengeToken: chJson.data.challengeToken, signature: signChallengeHex(privateKeyHex, chJson.data.message) };
|
|
1035
|
+
}
|
|
1000
1036
|
const res = await fetch(`${base}/policy/counterparties`, {
|
|
1001
1037
|
method: 'POST',
|
|
1002
1038
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
1003
|
-
|
|
1039
|
+
// `report` (a non-financial gateway, which never claims): it may only report to this owner, and it changes nothing about
|
|
1040
|
+
// who may claim their holds, so it needs no enforcement confirmation (MAGP §8.7.6, §16.4).
|
|
1041
|
+
body: JSON.stringify({ did, label, purpose, ...(proof ? { proof } : {}), ...(purpose === 'claim' ? { confirmEnforcementChange: true } : {}) }),
|
|
1004
1042
|
});
|
|
1005
1043
|
const json = await res.json().catch(() => null);
|
|
1006
1044
|
if (!res.ok || json?.success === false) return { ok: false, message: json?.message ?? `HTTP ${res.status}` };
|
|
@@ -1033,6 +1071,30 @@ async function ensurePolicyKey(config, base) {
|
|
|
1033
1071
|
return config.issuer.policyKey;
|
|
1034
1072
|
}
|
|
1035
1073
|
|
|
1074
|
+
/**
|
|
1075
|
+
* The principal DID that owns this agent (and so its gateway's credentials), read from the agent's own policy bundle
|
|
1076
|
+
* (`ownerPrincipal`, MAGP §16.3) on the API it was just provisioned through, and saved into the config. A gateway is bound
|
|
1077
|
+
* to it: an admitted agent of any other owner is refused. Returns it, or null when the bundle names none.
|
|
1078
|
+
*/
|
|
1079
|
+
async function ensureOwnerPrincipal(config, base) {
|
|
1080
|
+
if (typeof config.ownerPrincipal === 'string' && SAFE_DID.test(config.ownerPrincipal)) return config.ownerPrincipal;
|
|
1081
|
+
let owner = null;
|
|
1082
|
+
try {
|
|
1083
|
+
const res = await fetch(`${String(base).replace(/\/+$/, '')}/policy/bundle/${encodeURIComponent(config.agentDid)}`);
|
|
1084
|
+
owner = (await res.json().catch(() => null))?.data?.ownerPrincipal ?? null;
|
|
1085
|
+
} catch {
|
|
1086
|
+
owner = null;
|
|
1087
|
+
}
|
|
1088
|
+
if (typeof owner !== 'string' || !SAFE_DID.test(owner)) return null;
|
|
1089
|
+
config.ownerPrincipal = owner;
|
|
1090
|
+
return owner;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const NO_OWNER_PRINCIPAL = (base) =>
|
|
1094
|
+
`Could not read the principal that owns this agent from its policy bundle (GET ${base}/policy/bundle/<agent DID>, ownerPrincipal). ` +
|
|
1095
|
+
'A gateway is bound to the owner of its credentials, so none was scaffolded. Retry when the API is reachable (it needs a ' +
|
|
1096
|
+
'backend that names the owner), or pass --no-gateway for the single-process example (not an enforcement boundary).';
|
|
1097
|
+
|
|
1036
1098
|
const NO_POLICY_KEY = (base) =>
|
|
1037
1099
|
`Could not get MetaMynd's policy-signing key: the provisioning response carried none, and GET ${base}/magp/policy/pubkey ` +
|
|
1038
1100
|
'did not return one. A gateway without it would trust whatever policy bundle its network fetch returns, so none was ' +
|
|
@@ -1148,7 +1210,7 @@ const WHY = {
|
|
|
1148
1210
|
// gate, and neither of them depends on the amount.
|
|
1149
1211
|
NO_PERMISSION_FOR_ACTION: 'the mandate never granted this action - at any amount',
|
|
1150
1212
|
NO_MANDATE: 'there is no mandate for this action at all',
|
|
1151
|
-
MANDATE_REVOKED: 'the owner revoked the mandate for this action',
|
|
1213
|
+
MANDATE_REVOKED: 'the owner revoked the mandate for this action',
|
|
1152
1214
|
};
|
|
1153
1215
|
|
|
1154
1216
|
// ---------------------------------------------------------------- 1. CONTEXT
|
|
@@ -1168,7 +1230,9 @@ console.log('');
|
|
|
1168
1230
|
console.log(dim(' 1. this project holds an agent identity (a DID) and its signing key'));
|
|
1169
1231
|
console.log(dim(' 2. that agent has a mandate - a scope it may act in, and a spend cap'));
|
|
1170
1232
|
console.log(dim(' 3. guardTool() wraps your tool, so nothing calls the raw handler'));
|
|
1171
|
-
console.log(dim(' 4. each attempt is signed here
|
|
1233
|
+
console.log(dim(' 4. each attempt is signed here and checked against your signed rules: what'));
|
|
1234
|
+
console.log(dim(' they refuse stops HERE, with no network call; what could run is decided'));
|
|
1235
|
+
console.log(dim(' by the MetaMynd gate, which records it'));
|
|
1172
1236
|
console.log(dim(' 5. your tool runs ONLY if that decision is ALLOW'));
|
|
1173
1237
|
console.log('');
|
|
1174
1238
|
console.log(dim(' scope ${scope}'));
|
|
@@ -1178,7 +1242,7 @@ console.log(dim(' cap ${currency} ${perTxnMax} per transaction, set by your
|
|
|
1178
1242
|
async function attempt(n, intent, args, tool = gatedBookFlight) {
|
|
1179
1243
|
console.log('');
|
|
1180
1244
|
console.log(bold(' Step ' + n + ' of 4') + ' - ' + intent);
|
|
1181
|
-
console.log(dim(' signing the request
|
|
1245
|
+
console.log(dim(' signing the request here, checking it against your signed rules...'));
|
|
1182
1246
|
try {
|
|
1183
1247
|
const r = await tool(args);
|
|
1184
1248
|
console.log('\\x1b[32m ALLOWED\\x1b[0m your tool ran and returned ' + (r.pnr ?? 'ok'));
|
|
@@ -1193,7 +1257,7 @@ async function attempt(n, intent, args, tool = gatedBookFlight) {
|
|
|
1193
1257
|
} else {
|
|
1194
1258
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
|
|
1195
1259
|
console.log(dim(' ' + why));
|
|
1196
|
-
console.log(dim(' your tool never ran -
|
|
1260
|
+
console.log(dim(g.eventId ? ' decided by the MetaMynd gate - your tool never ran.' : ' decided right here from your signed rules (no network call), and reported for audit - your tool never ran.'));
|
|
1197
1261
|
}
|
|
1198
1262
|
}
|
|
1199
1263
|
}
|
|
@@ -1352,7 +1416,7 @@ const WHY = {
|
|
|
1352
1416
|
// gate, and neither of them depends on the amount.
|
|
1353
1417
|
NO_PERMISSION_FOR_ACTION: 'the mandate never granted this action - at any amount',
|
|
1354
1418
|
NO_MANDATE: 'there is no mandate for this action at all',
|
|
1355
|
-
MANDATE_REVOKED: 'the owner revoked the mandate for this action',
|
|
1419
|
+
MANDATE_REVOKED: 'the owner revoked the mandate for this action',
|
|
1356
1420
|
};
|
|
1357
1421
|
|
|
1358
1422
|
// ---------------------------------------------------------------- 1. CONTEXT
|
|
@@ -1385,11 +1449,12 @@ console.log(dim(' gateway ' + GATEWAY + ' (run it in a separate terminal - se
|
|
|
1385
1449
|
async function attempt(n, intent, args, tool = gatedBookFlight) {
|
|
1386
1450
|
console.log('');
|
|
1387
1451
|
console.log(bold(' Step ' + n + ' of 4') + ' - ' + intent);
|
|
1388
|
-
console.log(dim(' signing the request
|
|
1452
|
+
console.log(dim(' signing the request here, checking it against your signed rules...'));
|
|
1389
1453
|
try {
|
|
1390
1454
|
const r = await tool(args);
|
|
1391
1455
|
console.log('\\x1b[32m ALLOWED\\x1b[0m your tool ran (in ./gateway) and returned ' + (r.pnr ?? 'ok'));
|
|
1392
1456
|
console.log(dim(' ' + WHY.AUTHORIZED));
|
|
1457
|
+
return true;
|
|
1393
1458
|
} catch (e) {
|
|
1394
1459
|
const g = e.governance ?? {};
|
|
1395
1460
|
const why = WHY[g.reasonCode] ?? e.message;
|
|
@@ -1400,14 +1465,14 @@ async function attempt(n, intent, args, tool = gatedBookFlight) {
|
|
|
1400
1465
|
} else {
|
|
1401
1466
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
|
|
1402
1467
|
console.log(dim(' ' + why));
|
|
1403
|
-
console.log(dim(' your tool never ran -
|
|
1468
|
+
console.log(dim(g.eventId ? ' decided by the MetaMynd gate - your tool never ran.' : ' decided right here from your signed rules (no network call), and reported for audit - your tool never ran.'));
|
|
1404
1469
|
}
|
|
1405
1470
|
}
|
|
1406
1471
|
}
|
|
1407
1472
|
|
|
1408
1473
|
console.log('');
|
|
1409
1474
|
console.log(rule(66));
|
|
1410
|
-
await attempt(1, 'a ${currency} ${under} booking, low risk. Expected to pass.', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
|
|
1475
|
+
const step1Ran = await attempt(1, 'a ${currency} ${under} booking, low risk. Expected to pass.', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
|
|
1411
1476
|
await attempt(2, 'a ${currency} ${over} booking, deliberately over the cap.', { amount: ${over}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
|
|
1412
1477
|
await attempt(3, 'a ${currency} ${under} booking, but flagged high risk.', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'high' });
|
|
1413
1478
|
await attempt(
|
|
@@ -1425,7 +1490,13 @@ console.log(bold(' What this proved'));
|
|
|
1425
1490
|
console.log('');
|
|
1426
1491
|
console.log(dim(' - one code path, three outcomes. The rules decided, not this file'));
|
|
1427
1492
|
console.log(dim(' and not the model driving it.'));
|
|
1428
|
-
|
|
1493
|
+
// Only claim what happened (pre-beta rerun 2026-10-03, L-h: this said "step 1 ran in ./gateway" when step 1 was refused).
|
|
1494
|
+
if (step1Ran) {
|
|
1495
|
+
console.log(dim(' - step 1 ran in ./gateway, a process this file cannot reach into. There'));
|
|
1496
|
+
} else {
|
|
1497
|
+
console.log(dim(' - step 1 did not run this time (see above). When it does, it runs in'));
|
|
1498
|
+
console.log(dim(' ./gateway, a process this file cannot reach into. There'));
|
|
1499
|
+
}
|
|
1429
1500
|
console.log(dim(' is no rawBookFlight() here to call instead - that is what actually'));
|
|
1430
1501
|
console.log(dim(' stops a bypass, not the guardTool() call above it.'));
|
|
1431
1502
|
console.log(dim(' - step 4 needed no rule to stop it. The agent could not widen its own'));
|
|
@@ -1434,7 +1505,7 @@ console.log(dim(' - every blocked/escalated call never reached a real tool at
|
|
|
1434
1505
|
console.log(dim(' - if the gate were unreachable the guard fails CLOSED: it blocks.'));
|
|
1435
1506
|
console.log('');
|
|
1436
1507
|
console.log(bold(' With MetaMynd, you can\\'t be bypassed.') + ' ./gateway is why - it independently');
|
|
1437
|
-
console.log(dim(' re-verified step 1 before running it, and holds the tool this file never can.'));
|
|
1508
|
+
console.log(dim(step1Ran ? ' re-verified step 1 before running it, and holds the tool this file never can.' : ' re-verifies every call before running it, and holds the tool this file never can.'));
|
|
1438
1509
|
console.log('');
|
|
1439
1510
|
console.log(' Change the cap in the dashboard (${SOPS_PATH_ASCII}) and run again.');
|
|
1440
1511
|
console.log(dim(' The outcome changes. This file does not. That is the point.'));
|
|
@@ -1553,7 +1624,7 @@ async function attempt(n, total, step) {
|
|
|
1553
1624
|
const tool = step.action === 'permissions.update' ? gatedChangeOwnPermissions : gatedAction;
|
|
1554
1625
|
console.log('');
|
|
1555
1626
|
console.log(bold(' Step ' + n + ' of ' + total) + ' - ' + step.intent);
|
|
1556
|
-
console.log(dim(' signing the request
|
|
1627
|
+
console.log(dim(' signing the request here, checking it against your signed rules...'));
|
|
1557
1628
|
let got;
|
|
1558
1629
|
let g = {};
|
|
1559
1630
|
try {
|
|
@@ -1568,7 +1639,7 @@ async function attempt(n, total, step) {
|
|
|
1568
1639
|
${escalateNote(sandbox)}
|
|
1569
1640
|
} else {
|
|
1570
1641
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? e.message));
|
|
1571
|
-
console.log(dim(' your tool never ran -
|
|
1642
|
+
console.log(dim(g.eventId ? ' decided by the MetaMynd gate - your tool never ran.' : ' decided right here from your signed rules (no network call), and reported for audit - your tool never ran.'));
|
|
1572
1643
|
}
|
|
1573
1644
|
}
|
|
1574
1645
|
const asExpected = got === step.expect && (!step.reasonCode || g.reasonCode === step.reasonCode);
|
|
@@ -1633,7 +1704,7 @@ console.log('');
|
|
|
1633
1704
|
* allowed value-less request would be refused AUTHORIZATION_REQUIRED. The README states what that
|
|
1634
1705
|
* leaves open (replay) rather than implying the financial scaffold's guarantees.
|
|
1635
1706
|
*/
|
|
1636
|
-
function gatewayServerFileNeutral(scope, port, apiBase, policyKey) {
|
|
1707
|
+
function gatewayServerFileNeutral(scope, port, apiBase, policyKey, agentDid, ownerPrincipal) {
|
|
1637
1708
|
return `#!/usr/bin/env node
|
|
1638
1709
|
// gateway/server.mjs — the enforcement boundary for this agent's tool(s).
|
|
1639
1710
|
//
|
|
@@ -1643,6 +1714,7 @@ function gatewayServerFileNeutral(scope, port, apiBase, policyKey) {
|
|
|
1643
1714
|
// compromised or dishonest agent calling its own local function gets nothing here, because
|
|
1644
1715
|
// there is no local function: the tool only runs in this process.
|
|
1645
1716
|
import http from 'node:http';
|
|
1717
|
+
import { readFileSync } from 'node:fs';
|
|
1646
1718
|
import { createMcpGuard } from '${MCP_GUARD_PKG}';
|
|
1647
1719
|
import { createHttpGateway } from '${GATEWAY_PKG}';
|
|
1648
1720
|
|
|
@@ -1666,6 +1738,11 @@ async function performAction(args) {
|
|
|
1666
1738
|
// exactly those keys here — the gateway refuses every top-level key you do not name.
|
|
1667
1739
|
const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFields: [], allowedFields: [] }];
|
|
1668
1740
|
|
|
1741
|
+
// This gateway's OWN identity (a did:key, separate from the agent's), registered as your counterparty when
|
|
1742
|
+
// \`npx create-metamynd-agent\` provisioned it. It signs the reports below, so the issuer files them under YOUR account and
|
|
1743
|
+
// nobody else can write into it. Keep service.metamynd.json out of version control (it is gitignored).
|
|
1744
|
+
const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', import.meta.url)));
|
|
1745
|
+
|
|
1669
1746
|
// requireAuthorization is OFF on purpose. It makes the gateway claim a single-use, stateful
|
|
1670
1747
|
// authorization before running the tool — but the agent's guard only seals one for a value-bearing
|
|
1671
1748
|
// action (amount > 0), and this agent has no spending authority. With it ON, every ALLOWED request
|
|
@@ -1677,11 +1754,23 @@ const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFie
|
|
|
1677
1754
|
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1678
1755
|
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1679
1756
|
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
1680
|
-
|
|
1757
|
+
// allowedAgents pins this gateway to the ONE agent it was scaffolded for (MAGP §16.3). Every other check here judges the
|
|
1758
|
+
// caller by the caller's OWN mandate and SOP — written by the caller's owner. Without the pin, any agent on the platform
|
|
1759
|
+
// whose owner granted it this same action would run your tool with your credentials (refused AGENT_NOT_ADMITTED instead).
|
|
1760
|
+
// Add a DID here only for another agent you mean this gateway to act for. gatewayOwnerPrincipal binds it to YOU, the owner of
|
|
1761
|
+
// its credentials: an agent listed here that another principal owns is refused anyway (GATEWAY_OWNER_MISMATCH).
|
|
1762
|
+
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: false, policyPublicKey: '${policyKey}', allowedAgents: ['${agentDid}'], gatewayOwnerPrincipal: '${ownerPrincipal}' });
|
|
1763
|
+
// A guard below 0.20.0 ignores allowedAgents and serves every agent: refuse to start on one (a stale lockfile, say).
|
|
1764
|
+
if (!Array.isArray(guard.allowedAgents) || !guard.gatewayOwnerPrincipal) throw new Error('this gateway needs @metamynd/agentsafe-mcp-guard >= 0.22.0 to enforce allowedAgents and gatewayOwnerPrincipal - run npm install');
|
|
1681
1765
|
|
|
1682
1766
|
const gateway = createHttpGateway({
|
|
1683
1767
|
guard,
|
|
1684
1768
|
routes,
|
|
1769
|
+
// Every request this gateway runs or refuses is reported, signed with the identity above, to your Activity Log (MAGP
|
|
1770
|
+
// 16.4). It claims nothing, so without this the issuer would never see what it executed - or that another agent tried.
|
|
1771
|
+
reportOutcomes: true,
|
|
1772
|
+
// A report the issuer could not take (it was down, say) is kept here and re-sent, so nothing it saw is lost. Gitignored.
|
|
1773
|
+
reportSpool: './reports.spool.jsonl',
|
|
1685
1774
|
forward: async (req) => {
|
|
1686
1775
|
let args = {};
|
|
1687
1776
|
try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
|
|
@@ -1762,6 +1851,11 @@ the agent gets nothing, and there is no local function to call directly. The pol
|
|
|
1762
1851
|
pinned to MetaMynd's own signing key (\`policyPublicKey\`, baked in from your provisioning response) —
|
|
1763
1852
|
a party that can intercept the bundle fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway
|
|
1764
1853
|
a forged bundle with a higher cap or no rules; \`server.mjs\` refuses an unsigned or tampered one outright.
|
|
1854
|
+
It is also pinned to THIS agent, and bound to you as the owner of its credentials (\`allowedAgents\` and
|
|
1855
|
+
\`gatewayOwnerPrincipal\` in \`server.mjs\`, MAGP 16.3): any other agent - even one whose own
|
|
1856
|
+
owner granted it this same action - is refused \`AGENT_NOT_ADMITTED\` before its rules are read. Every request it runs or
|
|
1857
|
+
refuses is reported, signed with its own registered identity (\`service.metamynd.json\`), to your Activity Log (MAGP 16.4) -
|
|
1858
|
+
it claims nothing, so this is the only way what it ran reaches you.
|
|
1765
1859
|
|
|
1766
1860
|
**NOT closed — be precise about this:**
|
|
1767
1861
|
|
|
@@ -2037,7 +2131,7 @@ function gitignore() {
|
|
|
2037
2131
|
// in the AgentSafe repo for the full pattern (mutual handshake, x402 payment, capability
|
|
2038
2132
|
// binding) this is a minimal slice of.
|
|
2039
2133
|
|
|
2040
|
-
function gatewayServerFile(scope, port, apiBase, policyKey) {
|
|
2134
|
+
function gatewayServerFile(scope, port, apiBase, policyKey, agentDid, ownerPrincipal) {
|
|
2041
2135
|
return `#!/usr/bin/env node
|
|
2042
2136
|
// gateway/server.mjs — the REAL enforcement boundary for this agent's tool(s).
|
|
2043
2137
|
//
|
|
@@ -2092,7 +2186,16 @@ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', impo
|
|
|
2092
2186
|
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
2093
2187
|
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
2094
2188
|
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
2095
|
-
|
|
2189
|
+
// allowedAgents pins this gateway to the ONE agent it was scaffolded for (MAGP §16.3). Every other check here judges the
|
|
2190
|
+
// caller by the caller's OWN mandate and SOP — written by the caller's owner. Without the pin, any agent on the platform
|
|
2191
|
+
// whose owner granted it this same action would run your tool with your credentials (refused AGENT_NOT_ADMITTED instead).
|
|
2192
|
+
// Add a DID here only for another agent you mean this gateway to act for. gatewayOwnerPrincipal binds it to YOU, the owner of
|
|
2193
|
+
// its credentials: an agent listed here that another principal owns is refused anyway (GATEWAY_OWNER_MISMATCH).
|
|
2194
|
+
// The claim below does not stop that on its own: the hold belongs to the CALLER, and the caller's owner decides which
|
|
2195
|
+
// services may claim it — including registering this gateway's DID as one of theirs.
|
|
2196
|
+
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true, policyPublicKey: '${policyKey}', allowedAgents: ['${agentDid}'], gatewayOwnerPrincipal: '${ownerPrincipal}' });
|
|
2197
|
+
// A guard below 0.20.0 ignores allowedAgents and serves every agent: refuse to start on one (a stale lockfile, say).
|
|
2198
|
+
if (!Array.isArray(guard.allowedAgents) || !guard.gatewayOwnerPrincipal) throw new Error('this gateway needs @metamynd/agentsafe-mcp-guard >= 0.22.0 to enforce allowedAgents and gatewayOwnerPrincipal - run npm install');
|
|
2096
2199
|
|
|
2097
2200
|
const gateway = createHttpGateway({
|
|
2098
2201
|
guard,
|
|
@@ -2102,6 +2205,11 @@ const gateway = createHttpGateway({
|
|
|
2102
2205
|
// above lists which keys may appear; this makes the VALUES of every one of them (a payee, a passenger list) the agent's too.
|
|
2103
2206
|
// A request whose authorization bound no payload is refused (PAYLOAD_BINDING_REQUIRED) rather than run unbound.
|
|
2104
2207
|
requirePayloadBinding: true,
|
|
2208
|
+
// What this gateway refuses itself (another agent, a rule, a binding failure) is reported, signed, to your Activity Log
|
|
2209
|
+
// (MAGP 16.4). Claimed executions are already there, through the claim.
|
|
2210
|
+
reportOutcomes: true,
|
|
2211
|
+
// A report the issuer could not take (it was down, say) is kept here and re-sent, so nothing it saw is lost. Gitignored.
|
|
2212
|
+
reportSpool: './reports.spool.jsonl',
|
|
2105
2213
|
forward: async (req) => {
|
|
2106
2214
|
let args = {};
|
|
2107
2215
|
try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
|
|
@@ -2168,7 +2276,7 @@ function gatewayEnvExample() {
|
|
|
2168
2276
|
}
|
|
2169
2277
|
|
|
2170
2278
|
function gatewayGitignore() {
|
|
2171
|
-
return `node_modules/\n.env\nservice.metamynd.json\n`;
|
|
2279
|
+
return `node_modules/\n.env\nservice.metamynd.json\nreports.spool.jsonl\nreports.spool.jsonl.flushing\n`;
|
|
2172
2280
|
}
|
|
2173
2281
|
|
|
2174
2282
|
function gatewayReadme(slug, scope, port) {
|
|
@@ -2227,14 +2335,20 @@ add another protected route here rather than adding a local function back in \`i
|
|
|
2227
2335
|
|
|
2228
2336
|
## What this closes, precisely
|
|
2229
2337
|
|
|
2230
|
-
|
|
2231
|
-
own code, or a network attacker) might attempt:
|
|
2338
|
+
Five independent checks, each closing a different bypass an agent (or anything able to call its
|
|
2339
|
+
own code, or a network attacker) might attempt - and a record of what it turned away:
|
|
2232
2340
|
|
|
2233
2341
|
- **Direct call.** \`bookFlight()\` doesn't exist in the agent's process. There's nothing to call.
|
|
2234
2342
|
- **Forged policy bundle.** \`server.mjs\` bakes in \`policyPublicKey\` (from your provisioning
|
|
2235
2343
|
response) and refuses an unsigned or tampered bundle outright — a party that can intercept the
|
|
2236
2344
|
fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway a bundle with a higher cap or
|
|
2237
2345
|
no rules and have it trusted the same as the real one.
|
|
2346
|
+
- **Another agent.** Every other check judges the caller by the caller's OWN mandate, which its own owner
|
|
2347
|
+
writes. \`server.mjs\` pins \`allowedAgents\` to this agent and \`gatewayOwnerPrincipal\` to you (MAGP 16.3), so another agent - even one whose
|
|
2348
|
+
owner granted it this action and registered this gateway as its counterparty - is refused
|
|
2349
|
+
\`AGENT_NOT_ADMITTED\` before anything is claimed.
|
|
2350
|
+
- **Visibility.** What it refuses itself (another agent, a rule, a binding failure) is reported, signed with its own
|
|
2351
|
+
registered identity, to your Activity Log (MAGP 16.4); what it executes is already there, through the claim.
|
|
2238
2352
|
- **Confused deputy (payload).** The gateway re-verifies the signed request against this agent's
|
|
2239
2353
|
own policy AND binds it to the actual request body (payload binding,
|
|
2240
2354
|
\`@metamynd/agentsafe-http-gateway\` ≥ 0.4.5) — signing a cheap request while executing an
|
|
@@ -2396,20 +2510,30 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2396
2510
|
if (!policyKey) {
|
|
2397
2511
|
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");
|
|
2398
2512
|
}
|
|
2513
|
+
// The gateway acts for exactly this agent (allowedAgents, MAGP §16.3) — never scaffold one that would pin to nobody.
|
|
2514
|
+
if (!config.agentDid) {
|
|
2515
|
+
throw new Error('scaffoldProject: a gateway is pinned to the agent it serves (config.agentDid) — none was given; pass withGateway: false for an agent-only scaffold');
|
|
2516
|
+
}
|
|
2517
|
+
// It is written into generated JavaScript as a string literal: anything but a plain DID is refused, never quoted around.
|
|
2518
|
+
if (!SAFE_DID.test(config.agentDid)) throw new Error(`scaffoldProject: config.agentDid is not a DID: ${JSON.stringify(config.agentDid)}`);
|
|
2519
|
+
// ...and to the owner of its credentials (gatewayOwnerPrincipal, MAGP §16.3), read from the agent's bundle (ensureOwnerPrincipal).
|
|
2520
|
+
if (typeof config.ownerPrincipal !== 'string' || !SAFE_DID.test(config.ownerPrincipal)) {
|
|
2521
|
+
throw new Error('scaffoldProject: a gateway is bound to the principal that owns its credentials (config.ownerPrincipal) — call ensureOwnerPrincipal() first, or pass withGateway: false');
|
|
2522
|
+
}
|
|
2399
2523
|
const gwDir = join(outDir, 'gateway');
|
|
2400
2524
|
if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
|
|
2401
|
-
|
|
2525
|
+
{
|
|
2402
2526
|
// The gateway's OWN identity — the one that claims and settles this agent's holds (MAGP §8.7.6/
|
|
2403
2527
|
// §8.7.10), separate from the agent's own DID on purpose (an agent must not be able to release or
|
|
2404
|
-
// lower-settle a hold it authorized itself)
|
|
2405
|
-
// caller generated + registered one (main()'s hosted
|
|
2528
|
+
// lower-settle a hold it authorized itself), and the key its reports are signed with (MAGP 16.4) on either shape.
|
|
2529
|
+
// `gatewayIdentity` is real and registered when the caller generated + registered one (main()'s hosted path); a caller that didn't
|
|
2406
2530
|
// (an older code path, or a direct scaffoldProject() call in a test) falls back to the historical
|
|
2407
2531
|
// non-cryptographic placeholder — same shape as before this field existed, still parses and runs,
|
|
2408
2532
|
// just unable to claim a MAINNET hold or sign an authenticated claim on any network.
|
|
2409
2533
|
const identity = gatewayIdentity ?? { did: `did:local:${scope}-gateway`, keyHex: null };
|
|
2410
2534
|
writeFileSafe(gwDir, 'service.metamynd.json', JSON.stringify({ serviceDid: identity.did, serviceKey: identity.keyHex }, null, 2) + '\n', force, 0o600);
|
|
2411
2535
|
}
|
|
2412
|
-
writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase, policyKey) : gatewayServerFile(scope, gatewayPort, apiBase, policyKey), force);
|
|
2536
|
+
writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase, policyKey, config.agentDid, config.ownerPrincipal) : gatewayServerFile(scope, gatewayPort, apiBase, policyKey, config.agentDid, config.ownerPrincipal), force);
|
|
2413
2537
|
writeFileSafe(gwDir, 'package.json', gatewayPackageJson(slug), force);
|
|
2414
2538
|
writeFileSafe(gwDir, '.env.example', neutral ? gatewayEnvExampleNeutral() : gatewayEnvExample(), force);
|
|
2415
2539
|
writeFileSafe(gwDir, '.gitignore', gatewayGitignore(), force);
|
|
@@ -2638,6 +2762,8 @@ const PORT = Number(process.env.PORT || ${gatewayPort});
|
|
|
2638
2762
|
// fails BUNDLE_SUBJECT_MISMATCH, not just an unmatched-signature error, because the bundle
|
|
2639
2763
|
// this gateway serves is only ever this one agent's.
|
|
2640
2764
|
const AGENT_DID = '${agentDid}';
|
|
2765
|
+
// No principal exists offline: the harness bundle and this gateway name the same local owner (MAGP §16.3).
|
|
2766
|
+
const HARNESS_OWNER = 'did:local:harness-owner';
|
|
2641
2767
|
|
|
2642
2768
|
// Reshapes the harness's own rules-file shape ({mandate, sops:[{standardKey,document}],
|
|
2643
2769
|
// standards:[{standardKey,document}]}) into what agentsafe-mcp-guard's verifyRequest expects
|
|
@@ -2658,12 +2784,16 @@ function bundleFromRules(rules) {
|
|
|
2658
2784
|
// and your agent process both read, so editing it takes effect on the next request everywhere.
|
|
2659
2785
|
const guard = createMcpGuard({
|
|
2660
2786
|
serviceDid: 'did:local:${scope}-gateway',
|
|
2787
|
+
allowedAgents: [AGENT_DID], // MAGP §16.3: any other agent is refused AGENT_NOT_ADMITTED before its rules are read
|
|
2788
|
+
gatewayOwnerPrincipal: HARNESS_OWNER, // the local stand-in for the principal that owns this gateway's credentials
|
|
2661
2789
|
fetchBundle: async (agentDid) => {
|
|
2662
2790
|
if (agentDid !== AGENT_DID) return { subject: AGENT_DID, mandates: [], sops: [], standards: [] };
|
|
2663
2791
|
const rules = JSON.parse(readFileSync('../metamynd-rules.json', 'utf8'));
|
|
2664
|
-
return { subject: AGENT_DID, ...bundleFromRules(rules) };
|
|
2792
|
+
return { subject: AGENT_DID, ownerPrincipal: HARNESS_OWNER, ...bundleFromRules(rules) };
|
|
2665
2793
|
},
|
|
2666
2794
|
});
|
|
2795
|
+
// A guard below 0.20.0 ignores allowedAgents and serves every agent: refuse to start on one (a stale lockfile, say).
|
|
2796
|
+
if (!Array.isArray(guard.allowedAgents) || !guard.gatewayOwnerPrincipal) throw new Error('this gateway needs @metamynd/agentsafe-mcp-guard >= 0.22.0 to enforce allowedAgents and gatewayOwnerPrincipal - run npm install');
|
|
2667
2797
|
|
|
2668
2798
|
// One protected route per gated action in index.mjs. This gateway IS the tool, not a proxy in
|
|
2669
2799
|
// front of one, so a path with no route below is refused (denyByDefault) — nothing to fall through TO.
|
|
@@ -3232,7 +3362,7 @@ const WHY = {
|
|
|
3232
3362
|
MERCHANT_NOT_ALLOWED: 'the mandate lists which merchants this agent may pay',
|
|
3233
3363
|
NO_PERMISSION_FOR_ACTION: 'the mandate never granted this action - at any amount',
|
|
3234
3364
|
NO_MANDATE: 'there is no mandate for this action at all',
|
|
3235
|
-
MANDATE_REVOKED: 'the owner revoked the mandate for this action',
|
|
3365
|
+
MANDATE_REVOKED: 'the owner revoked the mandate for this action',
|
|
3236
3366
|
};
|
|
3237
3367
|
|
|
3238
3368
|
async function attempt(n, intent, action, args, tool = gatedBookFlight) {
|
|
@@ -3258,7 +3388,7 @@ async function attempt(n, intent, action, args, tool = gatedBookFlight) {
|
|
|
3258
3388
|
} else {
|
|
3259
3389
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
|
|
3260
3390
|
console.log(dim(' ' + why));
|
|
3261
|
-
console.log(dim(' your tool never ran -
|
|
3391
|
+
console.log(dim(g.eventId ? ' decided by the MetaMynd gate - your tool never ran.' : ' decided right here from your signed rules (no network call), and reported for audit - your tool never ran.'));
|
|
3262
3392
|
}
|
|
3263
3393
|
dashboard.logDecision(action, args, g);
|
|
3264
3394
|
}
|
|
@@ -3455,7 +3585,7 @@ async function attempt(n, total, step) {
|
|
|
3455
3585
|
}
|
|
3456
3586
|
} else {
|
|
3457
3587
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? e.message));
|
|
3458
|
-
console.log(dim(' your tool never ran -
|
|
3588
|
+
console.log(dim(g.eventId ? ' decided by the MetaMynd gate - your tool never ran.' : ' decided right here from your signed rules (no network call), and reported for audit - your tool never ran.'));
|
|
3459
3589
|
}
|
|
3460
3590
|
dashboard.logDecision(action, step.context, g);
|
|
3461
3591
|
}
|
|
@@ -3928,6 +4058,7 @@ async function runClaim(args) {
|
|
|
3928
4058
|
keepConfigAndStop('--financial was passed, but the approved agent has NO spending authority (the request was non-financial).');
|
|
3929
4059
|
}
|
|
3930
4060
|
if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndStop(NO_POLICY_KEY(base));
|
|
4061
|
+
if (!args['no-gateway'] && !(await ensureOwnerPrincipal(config, config.apiBase || base))) keepConfigAndStop(NO_OWNER_PRINCIPAL(base));
|
|
3931
4062
|
const slug = slugify(state.name || 'metamynd-agent');
|
|
3932
4063
|
const outDir = resolve(String(args.out || `./${slug}`));
|
|
3933
4064
|
if (issuedNonFinancial) {
|
|
@@ -4137,16 +4268,15 @@ async function main() {
|
|
|
4137
4268
|
|
|
4138
4269
|
// 3c. The gateway's OWN identity — a real did:key, separate from the agent's own (an agent must
|
|
4139
4270
|
// never be able to release or lower-settle a hold it authorized itself; only the SERVICE that
|
|
4140
|
-
// claimed it may)
|
|
4141
|
-
// non-financial gateway
|
|
4142
|
-
// it has no counterparty identity to register.
|
|
4271
|
+
// claimed it may) — and, on either shape, the key that signs its reports to your Activity Log (MAGP 16.4): a
|
|
4272
|
+
// non-financial gateway claims nothing, so without a registered identity nothing it ran would reach you.
|
|
4143
4273
|
let gatewayIdentity = null;
|
|
4144
|
-
if (
|
|
4274
|
+
if (!args['no-gateway']) {
|
|
4145
4275
|
const gwKeypair = generateAgentKeypair();
|
|
4146
4276
|
const gatewayDid = buildDidKey(rawPublicKeyFromSpkiHex(gwKeypair.publicKeyHex));
|
|
4147
4277
|
gatewayIdentity = { did: gatewayDid, keyHex: gwKeypair.privateKeyHex };
|
|
4148
|
-
console.log(c.dim(`\n → registering the gateway's identity as a trusted counterparty (so it can claim this agent's holds) …`));
|
|
4149
|
-
const reg = await registerGatewayCounterparty(base, token, gatewayDid, `${scope}-gateway
|
|
4278
|
+
console.log(c.dim(`\n → registering the gateway's identity as a trusted counterparty (${financial ? "so it can claim this agent's holds and report to you" : 'so its reports reach your Activity Log'}) …`));
|
|
4279
|
+
const reg = await registerGatewayCounterparty(base, token, gatewayDid, `${scope}-gateway`, financial ? 'claim' : 'report', gwKeypair.privateKeyHex);
|
|
4150
4280
|
if (reg.ok) {
|
|
4151
4281
|
console.log(` ${c.green('✓')} gateway DID ${c.b(gatewayDid)} — ${reg.message}`);
|
|
4152
4282
|
} else {
|
|
@@ -4165,6 +4295,7 @@ async function main() {
|
|
|
4165
4295
|
for (const n of demo.notDemonstrated) console.log(` ${c.yellow('!')} not staged in the demo: ${n.rule} ${c.dim('— ' + n.why + '; still enforced')}`);
|
|
4166
4296
|
}
|
|
4167
4297
|
if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndFail(outDir, config, NO_POLICY_KEY(base));
|
|
4298
|
+
if (!args['no-gateway'] && !(await ensureOwnerPrincipal(config, config.apiBase || base))) keepConfigAndFail(outDir, config, NO_OWNER_PRINCIPAL(base));
|
|
4168
4299
|
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 });
|
|
4169
4300
|
}
|
|
4170
4301
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.18",
|
|
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": {
|