create-metamynd-agent 0.14.10 → 0.14.22
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 +292 -64
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -61,7 +61,14 @@ 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
|
+
// 0.21.0: guardTool() settles the hold an allowed call was granted (captured when the tool runs, released when it cannot
|
|
69
|
+
// have), guard.void() releases one signed, and a gated tool's .resume(escalationId, args) runs an approved escalation once.
|
|
70
|
+
// Required: the scaffold's npm run resume uses .resume().
|
|
71
|
+
const GUARD_VERSION = '^0.21.0';
|
|
65
72
|
/** The harness entry point's config load, shared by both harness templates: a fresh clone has no
|
|
66
73
|
* agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
|
|
67
74
|
function harnessConfigLoad() {
|
|
@@ -128,7 +135,22 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
|
128
135
|
// 0.19.0: a request the gateway's own policy escalates, carrying an authorization a PERSON approved, is executed (once,
|
|
129
136
|
// with every claim binding) instead of refused RISK_REVIEW (MAGP §8.7.18). Required: the scaffold tells the developer
|
|
130
137
|
// "approve it in the dashboard and the action resumes" — below this the gateway refused exactly that.
|
|
131
|
-
|
|
138
|
+
// 0.20.0: `allowedAgents` pins a Service to the agents it acts for (MAGP §16.3) — refused AGENT_NOT_ADMITTED otherwise.
|
|
139
|
+
// Required: every scaffolded gateway now sets it, and an older guard would ignore the option and serve ANY agent whose
|
|
140
|
+
// own owner granted it the same action, with this owner's credentials (XT-1, pre-beta evaluation 2026-10-03).
|
|
141
|
+
// 0.21.0: guard.reportOutcome(). Required: the scaffolded gateways report what they ran or refused (MAGP §16.4), and
|
|
142
|
+
// agentsafe-http-gateway's reportOutcomes needs it.
|
|
143
|
+
// 0.22.0: allowedAgents is required, gatewayOwnerPrincipal binds the gateway to the owner of its credentials (an admitted
|
|
144
|
+
// agent of any other owner: GATEWAY_OWNER_MISMATCH), per-route admission (CREDENTIAL_PROFILE_NOT_PERMITTED), the refusal is
|
|
145
|
+
// AGENT_NOT_ADMITTED, and a handshake signs only a plain-token nonce. Required: every scaffolded gateway sets the owner.
|
|
146
|
+
// 0.23.0: acceptCounterpartyChallenge() — a Service signs its acceptance of an owner's registration (MAGP §8.7.6). No
|
|
147
|
+
// template change; required with GATEWAY_VERSION below so the gateway never resolves a second guard of its own.
|
|
148
|
+
// 0.24.0: reportOutcome() signs a reportId and an occurrence count (aggregated refusals, a durable spool).
|
|
149
|
+
// 0.25.0: honourApprovals — a Service without requireAuthorization (the value-less gateway) still runs an owner-approved
|
|
150
|
+
// escalation, exactly once (MAGP §8.7.18). Required: the non-financial gateway sets it and asserts guard.honoursApprovals.
|
|
151
|
+
const MCP_GUARD_VERSION = '^0.25.0';
|
|
152
|
+
/** A DID as it may appear inside a generated string literal (the gateway's allowedAgents pin): no quote, backslash or space. */
|
|
153
|
+
const SAFE_DID = /^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/;
|
|
132
154
|
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
133
155
|
// 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
|
|
134
156
|
// never scaffold a range that could resolve below it.
|
|
@@ -151,7 +173,14 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
|
151
173
|
// 0.17.0: depends on agentsafe-mcp-guard ^0.18.0 and exports keepAliveFetch. No template change.
|
|
152
174
|
// 0.18.0: depends on agentsafe-mcp-guard ^0.19.0 (an approved escalation executes). Required with MCP_GUARD_VERSION
|
|
153
175
|
// above, so the gateway never resolves a second, older guard of its own.
|
|
154
|
-
|
|
176
|
+
// 0.19.0: depends on agentsafe-mcp-guard ^0.20.0 (allowedAgents) and reads AGENTSAFE_ALLOWED_AGENTS. Required with
|
|
177
|
+
// MCP_GUARD_VERSION above, so the gateway never resolves a second, older guard of its own.
|
|
178
|
+
// 0.20.0: `reportOutcomes`, which both hosted gateway templates set; depends on agentsafe-mcp-guard ^0.21.0.
|
|
179
|
+
// 0.21.0: per-route allowedAgents; depends on agentsafe-mcp-guard ^0.22.0.
|
|
180
|
+
// 0.22.0: depends on agentsafe-mcp-guard ^0.23.0.
|
|
181
|
+
// 0.23.0: refusals aggregated with their count, and `reportSpool` (both hosted templates set it); depends on agentsafe-mcp-guard ^0.24.0.
|
|
182
|
+
// 0.24.0: depends on agentsafe-mcp-guard ^0.25.0 (one guard in the tree with MCP_GUARD_VERSION above). No template change.
|
|
183
|
+
const GATEWAY_VERSION = '^0.24.0';
|
|
155
184
|
|
|
156
185
|
/** Appended to every scaffolded gateway server: give hold settlements still running a bounded moment on shutdown. */
|
|
157
186
|
const DRAIN_ON_SHUTDOWN = `
|
|
@@ -995,12 +1024,27 @@ async function resolveHostedRulePack(base, key, { financial, perTxnMax, maxAmoun
|
|
|
995
1024
|
* dashboard registration shows its own dialog before setting the flag); see the printed message
|
|
996
1025
|
* either way.
|
|
997
1026
|
*/
|
|
998
|
-
async function registerGatewayCounterparty(base, token, did, label) {
|
|
1027
|
+
async function registerGatewayCounterparty(base, token, did, label, purpose = 'claim', privateKeyHex = null) {
|
|
999
1028
|
try {
|
|
1029
|
+
// The gateway accepts its own registration (MAGP §8.7.6): ask for a challenge naming this owner, sign it with the gateway's
|
|
1030
|
+
// key (generated here, so this process holds it), and register with that proof. A DID is never registered on its text.
|
|
1031
|
+
let proof;
|
|
1032
|
+
if (privateKeyHex) {
|
|
1033
|
+
const ch = await fetch(`${base}/policy/counterparties/challenge`, {
|
|
1034
|
+
method: 'POST',
|
|
1035
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
1036
|
+
body: JSON.stringify({ did, purpose }),
|
|
1037
|
+
});
|
|
1038
|
+
const chJson = await ch.json().catch(() => null);
|
|
1039
|
+
if (!ch.ok || !chJson?.data?.message) return { ok: false, message: chJson?.message ?? `challenge HTTP ${ch.status}` };
|
|
1040
|
+
proof = { challengeToken: chJson.data.challengeToken, signature: signChallengeHex(privateKeyHex, chJson.data.message) };
|
|
1041
|
+
}
|
|
1000
1042
|
const res = await fetch(`${base}/policy/counterparties`, {
|
|
1001
1043
|
method: 'POST',
|
|
1002
1044
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
1003
|
-
|
|
1045
|
+
// `report` (a non-financial gateway, which never claims): it may only report to this owner, and it changes nothing about
|
|
1046
|
+
// who may claim their holds, so it needs no enforcement confirmation (MAGP §8.7.6, §16.4).
|
|
1047
|
+
body: JSON.stringify({ did, label, purpose, ...(proof ? { proof } : {}), ...(purpose === 'claim' ? { confirmEnforcementChange: true } : {}) }),
|
|
1004
1048
|
});
|
|
1005
1049
|
const json = await res.json().catch(() => null);
|
|
1006
1050
|
if (!res.ok || json?.success === false) return { ok: false, message: json?.message ?? `HTTP ${res.status}` };
|
|
@@ -1033,6 +1077,30 @@ async function ensurePolicyKey(config, base) {
|
|
|
1033
1077
|
return config.issuer.policyKey;
|
|
1034
1078
|
}
|
|
1035
1079
|
|
|
1080
|
+
/**
|
|
1081
|
+
* The principal DID that owns this agent (and so its gateway's credentials), read from the agent's own policy bundle
|
|
1082
|
+
* (`ownerPrincipal`, MAGP §16.3) on the API it was just provisioned through, and saved into the config. A gateway is bound
|
|
1083
|
+
* to it: an admitted agent of any other owner is refused. Returns it, or null when the bundle names none.
|
|
1084
|
+
*/
|
|
1085
|
+
async function ensureOwnerPrincipal(config, base) {
|
|
1086
|
+
if (typeof config.ownerPrincipal === 'string' && SAFE_DID.test(config.ownerPrincipal)) return config.ownerPrincipal;
|
|
1087
|
+
let owner = null;
|
|
1088
|
+
try {
|
|
1089
|
+
const res = await fetch(`${String(base).replace(/\/+$/, '')}/policy/bundle/${encodeURIComponent(config.agentDid)}`);
|
|
1090
|
+
owner = (await res.json().catch(() => null))?.data?.ownerPrincipal ?? null;
|
|
1091
|
+
} catch {
|
|
1092
|
+
owner = null;
|
|
1093
|
+
}
|
|
1094
|
+
if (typeof owner !== 'string' || !SAFE_DID.test(owner)) return null;
|
|
1095
|
+
config.ownerPrincipal = owner;
|
|
1096
|
+
return owner;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const NO_OWNER_PRINCIPAL = (base) =>
|
|
1100
|
+
`Could not read the principal that owns this agent from its policy bundle (GET ${base}/policy/bundle/<agent DID>, ownerPrincipal). ` +
|
|
1101
|
+
'A gateway is bound to the owner of its credentials, so none was scaffolded. Retry when the API is reachable (it needs a ' +
|
|
1102
|
+
'backend that names the owner), or pass --no-gateway for the single-process example (not an enforcement boundary).';
|
|
1103
|
+
|
|
1036
1104
|
const NO_POLICY_KEY = (base) =>
|
|
1037
1105
|
`Could not get MetaMynd's policy-signing key: the provisioning response carried none, and GET ${base}/magp/policy/pubkey ` +
|
|
1038
1106
|
'did not return one. A gateway without it would trust whatever policy bundle its network fetch returns, so none was ' +
|
|
@@ -1072,12 +1140,66 @@ function keepConfigAndFail(outDir, config, why) {
|
|
|
1072
1140
|
* guard.escalationStatus(id) for the authorization and presents it to run the action (MAGP §8.7.18). Generated code: it
|
|
1073
1141
|
* reads `g`, the refusal's governance verdict, and `dim`, both in scope at every call site.
|
|
1074
1142
|
*/
|
|
1075
|
-
function escalateNote(sandbox) {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
console.log(dim(' Provision your own agent to approve one in your dashboard.'))
|
|
1079
|
-
|
|
1143
|
+
function escalateNote(sandbox, argsExpr = null) {
|
|
1144
|
+
if (sandbox) {
|
|
1145
|
+
return `console.log(dim(' nobody can approve it here: the shared sandbox has no owner you can sign in as.'));
|
|
1146
|
+
console.log(dim(' Provision your own agent to approve one in your dashboard.'));`;
|
|
1147
|
+
}
|
|
1148
|
+
if (!argsExpr) {
|
|
1149
|
+
return `console.log(dim(' not a failure: it waits in your dashboard, under Escalations' + (g.escalationId ? ' (' + g.escalationId + ')' : '') + '.'));
|
|
1080
1150
|
console.log(dim(' Once you approve it, guard.escalationStatus(id) gives the agent the authorization to run it.'));`;
|
|
1151
|
+
}
|
|
1152
|
+
// Resumable (0.14.21; pre-beta rerun 3, E-1): the call is saved so `npm run resume -- <id>` can run it, once approved.
|
|
1153
|
+
return `console.log(dim(' not a failure: it waits in your dashboard, under Escalations' + (g.escalationId ? ' (' + g.escalationId + ')' : '') + '.'));
|
|
1154
|
+
if (g.escalationId) {
|
|
1155
|
+
saveEscalation(g.escalationId, ${argsExpr});
|
|
1156
|
+
console.log(dim(' Approve it there, then run it - exactly once - with: npm run resume -- ' + g.escalationId));
|
|
1157
|
+
}`;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* The resume path a resumable scaffold carries (0.14.21; pre-beta rerun 3, E-1 — resuming used to mean writing ~15 lines
|
|
1162
|
+
* by hand). `npm start` saves an escalated call's arguments to ./escalations/<id>.json; `npm run resume -- <id>` hands them
|
|
1163
|
+
* to the gated tool's `.resume()` (agentsafe-guard >= 0.21.0), which waits for the owner's decision and then runs the tool
|
|
1164
|
+
* exactly once under the authorization the approval minted. Generated code; `tool` is the gated tool's variable name.
|
|
1165
|
+
*/
|
|
1166
|
+
function resumeBlock(tool) {
|
|
1167
|
+
return `// --- Escalations: \`npm run resume -- <escalationId>\` runs a call your owner APPROVED, exactly once. ---
|
|
1168
|
+
// npm start saves an escalated call's arguments to ./escalations/<id>.json. Approve it in the dashboard (AgentSafe ->
|
|
1169
|
+
// Escalations), then resume it: ${tool}.resume() waits for the decision and runs the tool once, under the authorization
|
|
1170
|
+
// the approval minted. Your own agent does the same with the escalationId from the refusal (e.governance.escalationId).
|
|
1171
|
+
import { mkdirSync as __mkdir, writeFileSync as __write, readFileSync as __read, rmSync as __rm } from 'node:fs';
|
|
1172
|
+
const ESCALATIONS = new URL('./escalations/', import.meta.url);
|
|
1173
|
+
const escalationFile = (id) => {
|
|
1174
|
+
if (!/^[A-Za-z0-9_-]{1,80}$/.test(String(id ?? ''))) throw new Error('not an escalation id: ' + id);
|
|
1175
|
+
return new URL(id + '.json', ESCALATIONS);
|
|
1176
|
+
};
|
|
1177
|
+
function saveEscalation(id, args) {
|
|
1178
|
+
__mkdir(ESCALATIONS, { recursive: true });
|
|
1179
|
+
__write(escalationFile(id), JSON.stringify(args, null, 2));
|
|
1180
|
+
}
|
|
1181
|
+
if (process.argv[2] === '--resume') {
|
|
1182
|
+
const id = process.argv[3];
|
|
1183
|
+
let saved;
|
|
1184
|
+
try { saved = JSON.parse(__read(escalationFile(id), 'utf8')); } catch {
|
|
1185
|
+
console.error(' No saved escalation "' + (id ?? '') + '". Run npm start first, then: npm run resume -- <escalationId>');
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
1188
|
+
console.log(' waiting for your decision on ' + id + ' (approve it in the dashboard: AgentSafe -> Escalations) ...');
|
|
1189
|
+
try {
|
|
1190
|
+
const result = await ${tool}.resume(id, saved);
|
|
1191
|
+
__rm(escalationFile(id), { force: true }); // used: a second resume has nothing to run
|
|
1192
|
+
console.log('\\x1b[32m RAN\\x1b[0m approved, and run exactly once: ' + JSON.stringify(result));
|
|
1193
|
+
} catch (e) {
|
|
1194
|
+
const g = e.governance ?? {};
|
|
1195
|
+
// A final answer (rejected, already used, expired) retires the saved call; still pending or unreachable keeps it.
|
|
1196
|
+
if (g.decision === 'block' && g.reasonCode !== 'GATE_UNREACHABLE') __rm(escalationFile(id), { force: true });
|
|
1197
|
+
console.log('\\x1b[31m NOT RUN\\x1b[0m ' + (g.reasonCode ?? e.message) + (g.status ? ' (' + g.status + ')' : ''));
|
|
1198
|
+
process.exitCode = 1;
|
|
1199
|
+
}
|
|
1200
|
+
process.exit();
|
|
1201
|
+
}
|
|
1202
|
+
`;
|
|
1081
1203
|
}
|
|
1082
1204
|
|
|
1083
1205
|
function exampleIndexNoGateway(scope, perTxnMax, currency, merchant, sandbox = false) {
|
|
@@ -1133,6 +1255,7 @@ const gatedRaiseOwnLimit = guard.guardTool(
|
|
|
1133
1255
|
}),
|
|
1134
1256
|
);
|
|
1135
1257
|
|
|
1258
|
+
${sandbox ? '' : resumeBlock('gatedBookFlight')}
|
|
1136
1259
|
const dim = (t) => '\\x1b[2m' + t + '\\x1b[0m';
|
|
1137
1260
|
const bold = (t) => '\\x1b[1m' + t + '\\x1b[0m';
|
|
1138
1261
|
const rule = (n) => ' ' + '-'.repeat(n);
|
|
@@ -1148,7 +1271,7 @@ const WHY = {
|
|
|
1148
1271
|
// gate, and neither of them depends on the amount.
|
|
1149
1272
|
NO_PERMISSION_FOR_ACTION: 'the mandate never granted this action - at any amount',
|
|
1150
1273
|
NO_MANDATE: 'there is no mandate for this action at all',
|
|
1151
|
-
MANDATE_REVOKED: 'the owner revoked the mandate for this action',
|
|
1274
|
+
MANDATE_REVOKED: 'the owner revoked the mandate for this action',
|
|
1152
1275
|
};
|
|
1153
1276
|
|
|
1154
1277
|
// ---------------------------------------------------------------- 1. CONTEXT
|
|
@@ -1168,7 +1291,9 @@ console.log('');
|
|
|
1168
1291
|
console.log(dim(' 1. this project holds an agent identity (a DID) and its signing key'));
|
|
1169
1292
|
console.log(dim(' 2. that agent has a mandate - a scope it may act in, and a spend cap'));
|
|
1170
1293
|
console.log(dim(' 3. guardTool() wraps your tool, so nothing calls the raw handler'));
|
|
1171
|
-
console.log(dim(' 4. each attempt is signed here
|
|
1294
|
+
console.log(dim(' 4. each attempt is signed here and checked against your signed rules: what'));
|
|
1295
|
+
console.log(dim(' they refuse stops HERE, with no network call; what could run is decided'));
|
|
1296
|
+
console.log(dim(' by the MetaMynd gate, which records it'));
|
|
1172
1297
|
console.log(dim(' 5. your tool runs ONLY if that decision is ALLOW'));
|
|
1173
1298
|
console.log('');
|
|
1174
1299
|
console.log(dim(' scope ${scope}'));
|
|
@@ -1178,7 +1303,7 @@ console.log(dim(' cap ${currency} ${perTxnMax} per transaction, set by your
|
|
|
1178
1303
|
async function attempt(n, intent, args, tool = gatedBookFlight) {
|
|
1179
1304
|
console.log('');
|
|
1180
1305
|
console.log(bold(' Step ' + n + ' of 4') + ' - ' + intent);
|
|
1181
|
-
console.log(dim(' signing the request
|
|
1306
|
+
console.log(dim(' signing the request here, checking it against your signed rules...'));
|
|
1182
1307
|
try {
|
|
1183
1308
|
const r = await tool(args);
|
|
1184
1309
|
console.log('\\x1b[32m ALLOWED\\x1b[0m your tool ran and returned ' + (r.pnr ?? 'ok'));
|
|
@@ -1189,11 +1314,11 @@ async function attempt(n, intent, args, tool = gatedBookFlight) {
|
|
|
1189
1314
|
if (g.decision === 'escalate') {
|
|
1190
1315
|
console.log('\\x1b[33m ESCALATED\\x1b[0m held for a human - ' + g.reasonCode);
|
|
1191
1316
|
console.log(dim(' ' + why));
|
|
1192
|
-
${escalateNote(sandbox)}
|
|
1317
|
+
${escalateNote(sandbox, sandbox ? null : 'args')}
|
|
1193
1318
|
} else {
|
|
1194
1319
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
|
|
1195
1320
|
console.log(dim(' ' + why));
|
|
1196
|
-
console.log(dim(' your tool never ran -
|
|
1321
|
+
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
1322
|
}
|
|
1198
1323
|
}
|
|
1199
1324
|
}
|
|
@@ -1336,6 +1461,7 @@ const gatedRaiseOwnLimit = guard.guardTool(
|
|
|
1336
1461
|
}),
|
|
1337
1462
|
);
|
|
1338
1463
|
|
|
1464
|
+
${resumeBlock('gatedBookFlight')}
|
|
1339
1465
|
const dim = (t) => '\\x1b[2m' + t + '\\x1b[0m';
|
|
1340
1466
|
const bold = (t) => '\\x1b[1m' + t + '\\x1b[0m';
|
|
1341
1467
|
const rule = (n) => ' ' + '-'.repeat(n);
|
|
@@ -1352,7 +1478,7 @@ const WHY = {
|
|
|
1352
1478
|
// gate, and neither of them depends on the amount.
|
|
1353
1479
|
NO_PERMISSION_FOR_ACTION: 'the mandate never granted this action - at any amount',
|
|
1354
1480
|
NO_MANDATE: 'there is no mandate for this action at all',
|
|
1355
|
-
MANDATE_REVOKED: 'the owner revoked the mandate for this action',
|
|
1481
|
+
MANDATE_REVOKED: 'the owner revoked the mandate for this action',
|
|
1356
1482
|
};
|
|
1357
1483
|
|
|
1358
1484
|
// ---------------------------------------------------------------- 1. CONTEXT
|
|
@@ -1385,29 +1511,30 @@ console.log(dim(' gateway ' + GATEWAY + ' (run it in a separate terminal - se
|
|
|
1385
1511
|
async function attempt(n, intent, args, tool = gatedBookFlight) {
|
|
1386
1512
|
console.log('');
|
|
1387
1513
|
console.log(bold(' Step ' + n + ' of 4') + ' - ' + intent);
|
|
1388
|
-
console.log(dim(' signing the request
|
|
1514
|
+
console.log(dim(' signing the request here, checking it against your signed rules...'));
|
|
1389
1515
|
try {
|
|
1390
1516
|
const r = await tool(args);
|
|
1391
1517
|
console.log('\\x1b[32m ALLOWED\\x1b[0m your tool ran (in ./gateway) and returned ' + (r.pnr ?? 'ok'));
|
|
1392
1518
|
console.log(dim(' ' + WHY.AUTHORIZED));
|
|
1519
|
+
return true;
|
|
1393
1520
|
} catch (e) {
|
|
1394
1521
|
const g = e.governance ?? {};
|
|
1395
1522
|
const why = WHY[g.reasonCode] ?? e.message;
|
|
1396
1523
|
if (g.decision === 'escalate') {
|
|
1397
1524
|
console.log('\\x1b[33m ESCALATED\\x1b[0m held for a human - ' + g.reasonCode);
|
|
1398
1525
|
console.log(dim(' ' + why));
|
|
1399
|
-
${escalateNote(false)}
|
|
1526
|
+
${escalateNote(false, 'args')}
|
|
1400
1527
|
} else {
|
|
1401
1528
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
|
|
1402
1529
|
console.log(dim(' ' + why));
|
|
1403
|
-
console.log(dim(' your tool never ran -
|
|
1530
|
+
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
1531
|
}
|
|
1405
1532
|
}
|
|
1406
1533
|
}
|
|
1407
1534
|
|
|
1408
1535
|
console.log('');
|
|
1409
1536
|
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' });
|
|
1537
|
+
const step1Ran = await attempt(1, 'a ${currency} ${under} booking, low risk. Expected to pass.', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
|
|
1411
1538
|
await attempt(2, 'a ${currency} ${over} booking, deliberately over the cap.', { amount: ${over}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
|
|
1412
1539
|
await attempt(3, 'a ${currency} ${under} booking, but flagged high risk.', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'high' });
|
|
1413
1540
|
await attempt(
|
|
@@ -1425,7 +1552,13 @@ console.log(bold(' What this proved'));
|
|
|
1425
1552
|
console.log('');
|
|
1426
1553
|
console.log(dim(' - one code path, three outcomes. The rules decided, not this file'));
|
|
1427
1554
|
console.log(dim(' and not the model driving it.'));
|
|
1428
|
-
|
|
1555
|
+
// Only claim what happened (pre-beta rerun 2026-10-03, L-h: this said "step 1 ran in ./gateway" when step 1 was refused).
|
|
1556
|
+
if (step1Ran) {
|
|
1557
|
+
console.log(dim(' - step 1 ran in ./gateway, a process this file cannot reach into. There'));
|
|
1558
|
+
} else {
|
|
1559
|
+
console.log(dim(' - step 1 did not run this time (see above). When it does, it runs in'));
|
|
1560
|
+
console.log(dim(' ./gateway, a process this file cannot reach into. There'));
|
|
1561
|
+
}
|
|
1429
1562
|
console.log(dim(' is no rawBookFlight() here to call instead - that is what actually'));
|
|
1430
1563
|
console.log(dim(' stops a bypass, not the guardTool() call above it.'));
|
|
1431
1564
|
console.log(dim(' - step 4 needed no rule to stop it. The agent could not widen its own'));
|
|
@@ -1434,7 +1567,7 @@ console.log(dim(' - every blocked/escalated call never reached a real tool at
|
|
|
1434
1567
|
console.log(dim(' - if the gate were unreachable the guard fails CLOSED: it blocks.'));
|
|
1435
1568
|
console.log('');
|
|
1436
1569
|
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.'));
|
|
1570
|
+
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
1571
|
console.log('');
|
|
1439
1572
|
console.log(' Change the cap in the dashboard (${SOPS_PATH_ASCII}) and run again.');
|
|
1440
1573
|
console.log(dim(' The outcome changes. This file does not. That is the point.'));
|
|
@@ -1544,7 +1677,7 @@ const gatedChangeOwnPermissions = guard.guardTool(
|
|
|
1544
1677
|
changeOwnPermissions,
|
|
1545
1678
|
(a) => ({ context: a }),
|
|
1546
1679
|
);
|
|
1547
|
-
|
|
1680
|
+
${!sandbox ? '\n' + resumeBlock('gatedAction') : ''}
|
|
1548
1681
|
const STEPS = ${JSON.stringify(steps, null, 2)};
|
|
1549
1682
|
const INPUTS = ${JSON.stringify(demo.inputs)};
|
|
1550
1683
|
const NOT_STAGED = ${JSON.stringify(demo.notDemonstrated)};
|
|
@@ -1553,7 +1686,7 @@ async function attempt(n, total, step) {
|
|
|
1553
1686
|
const tool = step.action === 'permissions.update' ? gatedChangeOwnPermissions : gatedAction;
|
|
1554
1687
|
console.log('');
|
|
1555
1688
|
console.log(bold(' Step ' + n + ' of ' + total) + ' - ' + step.intent);
|
|
1556
|
-
console.log(dim(' signing the request
|
|
1689
|
+
console.log(dim(' signing the request here, checking it against your signed rules...'));
|
|
1557
1690
|
let got;
|
|
1558
1691
|
let g = {};
|
|
1559
1692
|
try {
|
|
@@ -1565,10 +1698,10 @@ async function attempt(n, total, step) {
|
|
|
1565
1698
|
got = g.decision ?? 'error';
|
|
1566
1699
|
if (g.decision === 'escalate') {
|
|
1567
1700
|
console.log('\\x1b[33m ESCALATED\\x1b[0m held for a human - ' + g.reasonCode);
|
|
1568
|
-
${escalateNote(sandbox)}
|
|
1701
|
+
${escalateNote(sandbox, !sandbox ? 'step.context' : null)}
|
|
1569
1702
|
} else {
|
|
1570
1703
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? e.message));
|
|
1571
|
-
console.log(dim(' your tool never ran -
|
|
1704
|
+
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
1705
|
}
|
|
1573
1706
|
}
|
|
1574
1707
|
const asExpected = got === step.expect && (!step.reasonCode || g.reasonCode === step.reasonCode);
|
|
@@ -1633,7 +1766,7 @@ console.log('');
|
|
|
1633
1766
|
* allowed value-less request would be refused AUTHORIZATION_REQUIRED. The README states what that
|
|
1634
1767
|
* leaves open (replay) rather than implying the financial scaffold's guarantees.
|
|
1635
1768
|
*/
|
|
1636
|
-
function gatewayServerFileNeutral(scope, port, apiBase, policyKey) {
|
|
1769
|
+
function gatewayServerFileNeutral(scope, port, apiBase, policyKey, agentDid, ownerPrincipal) {
|
|
1637
1770
|
return `#!/usr/bin/env node
|
|
1638
1771
|
// gateway/server.mjs — the enforcement boundary for this agent's tool(s).
|
|
1639
1772
|
//
|
|
@@ -1643,6 +1776,7 @@ function gatewayServerFileNeutral(scope, port, apiBase, policyKey) {
|
|
|
1643
1776
|
// compromised or dishonest agent calling its own local function gets nothing here, because
|
|
1644
1777
|
// there is no local function: the tool only runs in this process.
|
|
1645
1778
|
import http from 'node:http';
|
|
1779
|
+
import { readFileSync } from 'node:fs';
|
|
1646
1780
|
import { createMcpGuard } from '${MCP_GUARD_PKG}';
|
|
1647
1781
|
import { createHttpGateway } from '${GATEWAY_PKG}';
|
|
1648
1782
|
|
|
@@ -1666,6 +1800,11 @@ async function performAction(args) {
|
|
|
1666
1800
|
// exactly those keys here — the gateway refuses every top-level key you do not name.
|
|
1667
1801
|
const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFields: [], allowedFields: [] }];
|
|
1668
1802
|
|
|
1803
|
+
// This gateway's OWN identity (a did:key, separate from the agent's), registered as your counterparty when
|
|
1804
|
+
// \`npx create-metamynd-agent\` provisioned it. It signs the reports below, so the issuer files them under YOUR account and
|
|
1805
|
+
// nobody else can write into it. Keep service.metamynd.json out of version control (it is gitignored).
|
|
1806
|
+
const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', import.meta.url)));
|
|
1807
|
+
|
|
1669
1808
|
// requireAuthorization is OFF on purpose. It makes the gateway claim a single-use, stateful
|
|
1670
1809
|
// authorization before running the tool — but the agent's guard only seals one for a value-bearing
|
|
1671
1810
|
// action (amount > 0), and this agent has no spending authority. With it ON, every ALLOWED request
|
|
@@ -1677,11 +1816,28 @@ const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFie
|
|
|
1677
1816
|
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1678
1817
|
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1679
1818
|
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
1680
|
-
|
|
1819
|
+
// allowedAgents pins this gateway to the ONE agent it was scaffolded for (MAGP §16.3). Every other check here judges the
|
|
1820
|
+
// caller by the caller's OWN mandate and SOP — written by the caller's owner. Without the pin, any agent on the platform
|
|
1821
|
+
// whose owner granted it this same action would run your tool with your credentials (refused AGENT_NOT_ADMITTED instead).
|
|
1822
|
+
// Add a DID here only for another agent you mean this gateway to act for. gatewayOwnerPrincipal binds it to YOU, the owner of
|
|
1823
|
+
// its credentials: an agent listed here that another principal owns is refused anyway (GATEWAY_OWNER_MISMATCH).
|
|
1824
|
+
// honourApprovals: with requireAuthorization off this gateway claims nothing — except an escalated call that presents the
|
|
1825
|
+
// authorization your APPROVAL minted (Dashboard -> Escalations). That one it claims as an approval: the issuer grants it
|
|
1826
|
+
// once, only for a hold a person approved, for this agent and these values, so the approved call runs here exactly once.
|
|
1827
|
+
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: false, policyPublicKey: '${policyKey}', allowedAgents: ['${agentDid}'], gatewayOwnerPrincipal: '${ownerPrincipal}', honourApprovals: true });
|
|
1828
|
+
// A guard below 0.22.0 cannot enforce both pins (below 0.20.0 it ignores allowedAgents and serves every agent; 0.22.0 added gatewayOwnerPrincipal): refuse to start on one (a stale lockfile, say).
|
|
1829
|
+
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');
|
|
1830
|
+
// A guard below 0.25.0 ignores honourApprovals, and an approved escalation would stay escalated here: refuse to start on one.
|
|
1831
|
+
if (guard.honoursApprovals !== true) throw new Error('this gateway needs @metamynd/agentsafe-mcp-guard >= 0.25.0 to run approved escalations - run npm install');
|
|
1681
1832
|
|
|
1682
1833
|
const gateway = createHttpGateway({
|
|
1683
1834
|
guard,
|
|
1684
1835
|
routes,
|
|
1836
|
+
// Every request this gateway runs or refuses is reported, signed with the identity above, to your Activity Log (MAGP
|
|
1837
|
+
// 16.4). It claims only an approved escalation, so without this the issuer would never see what it executed - or that another agent tried.
|
|
1838
|
+
reportOutcomes: true,
|
|
1839
|
+
// A report the issuer could not take (it was down, say) is kept here and re-sent, so nothing it saw is lost. Gitignored.
|
|
1840
|
+
reportSpool: './reports.spool.jsonl',
|
|
1685
1841
|
forward: async (req) => {
|
|
1686
1842
|
let args = {};
|
|
1687
1843
|
try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
|
|
@@ -1762,6 +1918,11 @@ the agent gets nothing, and there is no local function to call directly. The pol
|
|
|
1762
1918
|
pinned to MetaMynd's own signing key (\`policyPublicKey\`, baked in from your provisioning response) —
|
|
1763
1919
|
a party that can intercept the bundle fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway
|
|
1764
1920
|
a forged bundle with a higher cap or no rules; \`server.mjs\` refuses an unsigned or tampered one outright.
|
|
1921
|
+
It is also pinned to THIS agent, and bound to you as the owner of its credentials (\`allowedAgents\` and
|
|
1922
|
+
\`gatewayOwnerPrincipal\` in \`server.mjs\`, MAGP 16.3): any other agent - even one whose own
|
|
1923
|
+
owner granted it this same action - is refused \`AGENT_NOT_ADMITTED\` before its rules are read. Every request it runs or
|
|
1924
|
+
refuses is reported, signed with its own registered identity (\`service.metamynd.json\`), to your Activity Log (MAGP 16.4) -
|
|
1925
|
+
it claims nothing, so this is the only way what it ran reaches you.
|
|
1765
1926
|
|
|
1766
1927
|
**NOT closed — be precise about this:**
|
|
1767
1928
|
|
|
@@ -1801,10 +1962,18 @@ Replace \`performAction()\` in \`server.mjs\` with your implementation and put a
|
|
|
1801
1962
|
|
|
1802
1963
|
/** The README for a non-financial hosted scaffold. `withGateway` selects the two-process shape. */
|
|
1803
1964
|
/** The generated README's "Change the rules" body: the dashboard path, or — for the shared sandbox agent — how to get an agent whose rules you can change. */
|
|
1804
|
-
function changeRulesSection(sandbox) {
|
|
1965
|
+
function changeRulesSection(sandbox, resumable = false) {
|
|
1805
1966
|
return sandbox
|
|
1806
1967
|
? `${SANDBOX_RULES_FIXED} An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.`
|
|
1807
|
-
:
|
|
1968
|
+
: resumable
|
|
1969
|
+
? `Edit the agent's SOPs in the dashboard (${SOPS_PATH}). The agent's behaviour changes live —
|
|
1970
|
+
no redeploy.
|
|
1971
|
+
|
|
1972
|
+
An \`escalate\` verdict is held for an owner to approve. \`npm start\` saves an escalated call and prints its id; approve it
|
|
1973
|
+
in the dashboard (AgentSafe → Escalations), then \`npm run resume -- <escalationId>\` runs it exactly once under the
|
|
1974
|
+
authorization the approval minted. In your own agent that is one call: \`await gatedTool.resume(escalationId, sameArgs)\`
|
|
1975
|
+
(\`@metamynd/agentsafe-guard\` ≥ 0.21.0) — pass the SAME arguments; the approval is bound to that request.`
|
|
1976
|
+
: `Edit the agent's SOPs in the dashboard (${SOPS_PATH}). The agent's behaviour changes live —
|
|
1808
1977
|
no redeploy. An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.`;
|
|
1809
1978
|
}
|
|
1810
1979
|
|
|
@@ -1887,7 +2056,7 @@ ${clonedFreshSection(daemonKey)}
|
|
|
1887
2056
|
|
|
1888
2057
|
## Change the rules
|
|
1889
2058
|
|
|
1890
|
-
${changeRulesSection(sandbox)}
|
|
2059
|
+
${changeRulesSection(sandbox, !sandbox)}
|
|
1891
2060
|
|
|
1892
2061
|
## What this is not
|
|
1893
2062
|
|
|
@@ -1906,7 +2075,7 @@ Full integration guide: \`docs/integration/INTEGRATE-WITH-METAMYND.md\`.
|
|
|
1906
2075
|
`;
|
|
1907
2076
|
}
|
|
1908
2077
|
|
|
1909
|
-
function examplePackageJson(slug, neutral = false) {
|
|
2078
|
+
function examplePackageJson(slug, neutral = false, resumable = false) {
|
|
1910
2079
|
return JSON.stringify(
|
|
1911
2080
|
{
|
|
1912
2081
|
name: slug,
|
|
@@ -1916,7 +2085,7 @@ function examplePackageJson(slug, neutral = false) {
|
|
|
1916
2085
|
// `verify` is scaffolded in because governance that lives only in a dashboard is a
|
|
1917
2086
|
// thing someone has to remember to look at. As a build step it is a control: a change
|
|
1918
2087
|
// that widens this agent's authority fails `npm test`.
|
|
1919
|
-
scripts: { start: 'node index.mjs', test: neutral ? 'agentsafe-guard verify --context ./verify-context.json' : 'agentsafe-guard verify' },
|
|
2088
|
+
scripts: { start: 'node index.mjs', ...(resumable ? { resume: 'node index.mjs --resume' } : {}), test: neutral ? 'agentsafe-guard verify --context ./verify-context.json' : 'agentsafe-guard verify' },
|
|
1920
2089
|
dependencies: { [GUARD_PKG]: GUARD_VERSION },
|
|
1921
2090
|
},
|
|
1922
2091
|
null,
|
|
@@ -2018,14 +2187,14 @@ ${clonedFreshSection(daemonKey)}
|
|
|
2018
2187
|
|
|
2019
2188
|
## Change the rules
|
|
2020
2189
|
|
|
2021
|
-
${changeRulesSection(sandbox)}
|
|
2190
|
+
${changeRulesSection(sandbox, !sandbox)}
|
|
2022
2191
|
|
|
2023
2192
|
Full integration guide: \`docs/integration/INTEGRATE-WITH-METAMYND.md\`.
|
|
2024
2193
|
`;
|
|
2025
2194
|
}
|
|
2026
2195
|
|
|
2027
2196
|
function gitignore() {
|
|
2028
|
-
return `node_modules/\nagent.metamynd.json\n.env\n`;
|
|
2197
|
+
return `node_modules/\nagent.metamynd.json\n.env\nescalations/\n`;
|
|
2029
2198
|
}
|
|
2030
2199
|
|
|
2031
2200
|
// ---------- the default hosted scaffold's second process: a separate tool gateway ----------
|
|
@@ -2037,7 +2206,7 @@ function gitignore() {
|
|
|
2037
2206
|
// in the AgentSafe repo for the full pattern (mutual handshake, x402 payment, capability
|
|
2038
2207
|
// binding) this is a minimal slice of.
|
|
2039
2208
|
|
|
2040
|
-
function gatewayServerFile(scope, port, apiBase, policyKey) {
|
|
2209
|
+
function gatewayServerFile(scope, port, apiBase, policyKey, agentDid, ownerPrincipal) {
|
|
2041
2210
|
return `#!/usr/bin/env node
|
|
2042
2211
|
// gateway/server.mjs — the REAL enforcement boundary for this agent's tool(s).
|
|
2043
2212
|
//
|
|
@@ -2092,7 +2261,16 @@ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', impo
|
|
|
2092
2261
|
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
2093
2262
|
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
2094
2263
|
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
2095
|
-
|
|
2264
|
+
// allowedAgents pins this gateway to the ONE agent it was scaffolded for (MAGP §16.3). Every other check here judges the
|
|
2265
|
+
// caller by the caller's OWN mandate and SOP — written by the caller's owner. Without the pin, any agent on the platform
|
|
2266
|
+
// whose owner granted it this same action would run your tool with your credentials (refused AGENT_NOT_ADMITTED instead).
|
|
2267
|
+
// Add a DID here only for another agent you mean this gateway to act for. gatewayOwnerPrincipal binds it to YOU, the owner of
|
|
2268
|
+
// its credentials: an agent listed here that another principal owns is refused anyway (GATEWAY_OWNER_MISMATCH).
|
|
2269
|
+
// The claim below does not stop that on its own: the hold belongs to the CALLER, and the caller's owner decides which
|
|
2270
|
+
// services may claim it — including registering this gateway's DID as one of theirs.
|
|
2271
|
+
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true, policyPublicKey: '${policyKey}', allowedAgents: ['${agentDid}'], gatewayOwnerPrincipal: '${ownerPrincipal}' });
|
|
2272
|
+
// A guard below 0.22.0 cannot enforce both pins (below 0.20.0 it ignores allowedAgents and serves every agent; 0.22.0 added gatewayOwnerPrincipal): refuse to start on one (a stale lockfile, say).
|
|
2273
|
+
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
2274
|
|
|
2097
2275
|
const gateway = createHttpGateway({
|
|
2098
2276
|
guard,
|
|
@@ -2102,6 +2280,11 @@ const gateway = createHttpGateway({
|
|
|
2102
2280
|
// 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
2281
|
// A request whose authorization bound no payload is refused (PAYLOAD_BINDING_REQUIRED) rather than run unbound.
|
|
2104
2282
|
requirePayloadBinding: true,
|
|
2283
|
+
// What this gateway refuses itself (another agent, a rule, a binding failure) is reported, signed, to your Activity Log
|
|
2284
|
+
// (MAGP 16.4). Claimed executions are already there, through the claim.
|
|
2285
|
+
reportOutcomes: true,
|
|
2286
|
+
// A report the issuer could not take (it was down, say) is kept here and re-sent, so nothing it saw is lost. Gitignored.
|
|
2287
|
+
reportSpool: './reports.spool.jsonl',
|
|
2105
2288
|
forward: async (req) => {
|
|
2106
2289
|
let args = {};
|
|
2107
2290
|
try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
|
|
@@ -2168,7 +2351,7 @@ function gatewayEnvExample() {
|
|
|
2168
2351
|
}
|
|
2169
2352
|
|
|
2170
2353
|
function gatewayGitignore() {
|
|
2171
|
-
return `node_modules/\n.env\nservice.metamynd.json\n`;
|
|
2354
|
+
return `node_modules/\n.env\nservice.metamynd.json\nreports.spool.jsonl\nreports.spool.jsonl.flushing\n`;
|
|
2172
2355
|
}
|
|
2173
2356
|
|
|
2174
2357
|
function gatewayReadme(slug, scope, port) {
|
|
@@ -2227,14 +2410,20 @@ add another protected route here rather than adding a local function back in \`i
|
|
|
2227
2410
|
|
|
2228
2411
|
## What this closes, precisely
|
|
2229
2412
|
|
|
2230
|
-
|
|
2231
|
-
own code, or a network attacker) might attempt:
|
|
2413
|
+
Five independent checks, each closing a different bypass an agent (or anything able to call its
|
|
2414
|
+
own code, or a network attacker) might attempt - and a record of what it turned away:
|
|
2232
2415
|
|
|
2233
2416
|
- **Direct call.** \`bookFlight()\` doesn't exist in the agent's process. There's nothing to call.
|
|
2234
2417
|
- **Forged policy bundle.** \`server.mjs\` bakes in \`policyPublicKey\` (from your provisioning
|
|
2235
2418
|
response) and refuses an unsigned or tampered bundle outright — a party that can intercept the
|
|
2236
2419
|
fetch (a MITM, a compromised DNS/proxy) cannot hand this gateway a bundle with a higher cap or
|
|
2237
2420
|
no rules and have it trusted the same as the real one.
|
|
2421
|
+
- **Another agent.** Every other check judges the caller by the caller's OWN mandate, which its own owner
|
|
2422
|
+
writes. \`server.mjs\` pins \`allowedAgents\` to this agent and \`gatewayOwnerPrincipal\` to you (MAGP 16.3), so another agent - even one whose
|
|
2423
|
+
owner granted it this action and registered this gateway as its counterparty - is refused
|
|
2424
|
+
\`AGENT_NOT_ADMITTED\` before anything is claimed.
|
|
2425
|
+
- **Visibility.** What it refuses itself (another agent, a rule, a binding failure) is reported, signed with its own
|
|
2426
|
+
registered identity, to your Activity Log (MAGP 16.4); what it executes is already there, through the claim.
|
|
2238
2427
|
- **Confused deputy (payload).** The gateway re-verifies the signed request against this agent's
|
|
2239
2428
|
own policy AND binds it to the actual request body (payload binding,
|
|
2240
2429
|
\`@metamynd/agentsafe-http-gateway\` ≥ 0.4.5) — signing a cheap request while executing an
|
|
@@ -2274,17 +2463,33 @@ on purpose: an agent must never be able to release or lower-settle a hold it aut
|
|
|
2274
2463
|
provisioning, \`npx create-metamynd-agent\` registered that DID as a trusted counterparty for you
|
|
2275
2464
|
(\`POST /policy/counterparties\`). A **MAINNET** hold is *always* registered-only, regardless of your
|
|
2276
2465
|
registry (MAGP §8.7.10) — a testnet hold stays open unless you have registered anything at all.
|
|
2277
|
-
Manage the registry at \`/dashboard/counterparties
|
|
2466
|
+
Manage the registry at \`/dashboard/counterparties\` (it walks you through the proof below), or
|
|
2467
|
+
re-register this gateway yourself — after removing it from the registry, say.
|
|
2468
|
+
|
|
2469
|
+
Registration needs **proof of control** (MAGP §8.7.6): you ask for a challenge naming you, this
|
|
2470
|
+
gateway signs it with its own key from \`service.metamynd.json\`, and you register with that
|
|
2471
|
+
signature. A DID is never registered on its text alone, so nobody can register a gateway they do
|
|
2472
|
+
not hold the key for. Run this from this directory (bash; \`TOKEN\` is your owner access token):
|
|
2278
2473
|
|
|
2279
2474
|
\`\`\`bash
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2475
|
+
MAGP_API=https://metamynd.ai/api/v1 TOKEN=<owner access token> node --input-type=module -e "
|
|
2476
|
+
import { readFileSync } from 'node:fs'; import crypto from 'node:crypto';
|
|
2477
|
+
const { serviceDid, serviceKey } = JSON.parse(readFileSync('./service.metamynd.json', 'utf8'));
|
|
2478
|
+
const api = process.env.MAGP_API, headers = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + process.env.TOKEN };
|
|
2479
|
+
const post = (path, body) => fetch(api + path, { method: 'POST', headers, body: JSON.stringify(body) });
|
|
2480
|
+
const ch = (await (await post('/policy/counterparties/challenge', { did: serviceDid, purpose: 'claim' })).json()).data;
|
|
2481
|
+
const key = crypto.createPrivateKey({ key: Buffer.from(serviceKey, 'hex'), format: 'der', type: 'pkcs8' });
|
|
2482
|
+
const signature = crypto.sign(null, Buffer.from(ch.message, 'utf8'), key).toString('hex');
|
|
2483
|
+
const r = await post('/policy/counterparties', { did: serviceDid, purpose: 'claim', proof: { challengeToken: ch.challengeToken, signature }, confirmEnforcementChange: true });
|
|
2484
|
+
console.log(r.status, await r.text());
|
|
2485
|
+
"
|
|
2283
2486
|
\`\`\`
|
|
2284
2487
|
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2488
|
+
The gateway's own \`guard.acceptCounterpartyChallenge(message)\` (\`@metamynd/agentsafe-mcp-guard\`
|
|
2489
|
+
≥ 0.23.0) signs the same thing, if you would rather expose it from \`server.mjs\`.
|
|
2490
|
+
\`confirmEnforcementChange: true\` matters only for your account's FIRST-EVER registration — it
|
|
2491
|
+
switches EVERY one of your holds (not just this agent's) from open to registered-only, so the API
|
|
2492
|
+
refuses a first registration without it.
|
|
2288
2493
|
- **Amount unknown.** \`amount-unknown\` (\`@metamynd/agentsafe-mcp-guard\` ≥ 0.3.0) blocks a
|
|
2289
2494
|
platform tool by default when its raw bytes or a nested payload hide the amount from a naive
|
|
2290
2495
|
spend cap — AND this agent's OWN starter SOP (see \`agent.metamynd.json\` /
|
|
@@ -2365,7 +2570,8 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2365
2570
|
: exampleIndexNoGateway(scope, perTxnMax, currency, paymentMerchant, !!sandbox),
|
|
2366
2571
|
force,
|
|
2367
2572
|
);
|
|
2368
|
-
|
|
2573
|
+
// Resumable = the index carries the resume path (resumeBlock): every agent but the shared sandbox, where nobody can approve.
|
|
2574
|
+
writeFileSafe(outDir, 'package.json', examplePackageJson(slug, neutral, !sandbox), force);
|
|
2369
2575
|
writeFileSafe(outDir, '.gitignore', gitignore(), force);
|
|
2370
2576
|
writeFileSafe(
|
|
2371
2577
|
outDir,
|
|
@@ -2396,20 +2602,30 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2396
2602
|
if (!policyKey) {
|
|
2397
2603
|
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
2604
|
}
|
|
2605
|
+
// The gateway acts for exactly this agent (allowedAgents, MAGP §16.3) — never scaffold one that would pin to nobody.
|
|
2606
|
+
if (!config.agentDid) {
|
|
2607
|
+
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');
|
|
2608
|
+
}
|
|
2609
|
+
// It is written into generated JavaScript as a string literal: anything but a plain DID is refused, never quoted around.
|
|
2610
|
+
if (!SAFE_DID.test(config.agentDid)) throw new Error(`scaffoldProject: config.agentDid is not a DID: ${JSON.stringify(config.agentDid)}`);
|
|
2611
|
+
// ...and to the owner of its credentials (gatewayOwnerPrincipal, MAGP §16.3), read from the agent's bundle (ensureOwnerPrincipal).
|
|
2612
|
+
if (typeof config.ownerPrincipal !== 'string' || !SAFE_DID.test(config.ownerPrincipal)) {
|
|
2613
|
+
throw new Error('scaffoldProject: a gateway is bound to the principal that owns its credentials (config.ownerPrincipal) — call ensureOwnerPrincipal() first, or pass withGateway: false');
|
|
2614
|
+
}
|
|
2399
2615
|
const gwDir = join(outDir, 'gateway');
|
|
2400
2616
|
if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
|
|
2401
|
-
|
|
2617
|
+
{
|
|
2402
2618
|
// The gateway's OWN identity — the one that claims and settles this agent's holds (MAGP §8.7.6/
|
|
2403
2619
|
// §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
|
|
2620
|
+
// lower-settle a hold it authorized itself), and the key its reports are signed with (MAGP 16.4) on either shape.
|
|
2621
|
+
// `gatewayIdentity` is real and registered when the caller generated + registered one (main()'s hosted path); a caller that didn't
|
|
2406
2622
|
// (an older code path, or a direct scaffoldProject() call in a test) falls back to the historical
|
|
2407
2623
|
// non-cryptographic placeholder — same shape as before this field existed, still parses and runs,
|
|
2408
2624
|
// just unable to claim a MAINNET hold or sign an authenticated claim on any network.
|
|
2409
2625
|
const identity = gatewayIdentity ?? { did: `did:local:${scope}-gateway`, keyHex: null };
|
|
2410
2626
|
writeFileSafe(gwDir, 'service.metamynd.json', JSON.stringify({ serviceDid: identity.did, serviceKey: identity.keyHex }, null, 2) + '\n', force, 0o600);
|
|
2411
2627
|
}
|
|
2412
|
-
writeFileSafe(gwDir, 'server.mjs', neutral ? gatewayServerFileNeutral(scope, gatewayPort, apiBase, policyKey) : gatewayServerFile(scope, gatewayPort, apiBase, policyKey), force);
|
|
2628
|
+
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
2629
|
writeFileSafe(gwDir, 'package.json', gatewayPackageJson(slug), force);
|
|
2414
2630
|
writeFileSafe(gwDir, '.env.example', neutral ? gatewayEnvExampleNeutral() : gatewayEnvExample(), force);
|
|
2415
2631
|
writeFileSafe(gwDir, '.gitignore', gatewayGitignore(), force);
|
|
@@ -2638,6 +2854,8 @@ const PORT = Number(process.env.PORT || ${gatewayPort});
|
|
|
2638
2854
|
// fails BUNDLE_SUBJECT_MISMATCH, not just an unmatched-signature error, because the bundle
|
|
2639
2855
|
// this gateway serves is only ever this one agent's.
|
|
2640
2856
|
const AGENT_DID = '${agentDid}';
|
|
2857
|
+
// No principal exists offline: the harness bundle and this gateway name the same local owner (MAGP §16.3).
|
|
2858
|
+
const HARNESS_OWNER = 'did:local:harness-owner';
|
|
2641
2859
|
|
|
2642
2860
|
// Reshapes the harness's own rules-file shape ({mandate, sops:[{standardKey,document}],
|
|
2643
2861
|
// standards:[{standardKey,document}]}) into what agentsafe-mcp-guard's verifyRequest expects
|
|
@@ -2658,12 +2876,16 @@ function bundleFromRules(rules) {
|
|
|
2658
2876
|
// and your agent process both read, so editing it takes effect on the next request everywhere.
|
|
2659
2877
|
const guard = createMcpGuard({
|
|
2660
2878
|
serviceDid: 'did:local:${scope}-gateway',
|
|
2879
|
+
allowedAgents: [AGENT_DID], // MAGP §16.3: any other agent is refused AGENT_NOT_ADMITTED before its rules are read
|
|
2880
|
+
gatewayOwnerPrincipal: HARNESS_OWNER, // the local stand-in for the principal that owns this gateway's credentials
|
|
2661
2881
|
fetchBundle: async (agentDid) => {
|
|
2662
2882
|
if (agentDid !== AGENT_DID) return { subject: AGENT_DID, mandates: [], sops: [], standards: [] };
|
|
2663
2883
|
const rules = JSON.parse(readFileSync('../metamynd-rules.json', 'utf8'));
|
|
2664
|
-
return { subject: AGENT_DID, ...bundleFromRules(rules) };
|
|
2884
|
+
return { subject: AGENT_DID, ownerPrincipal: HARNESS_OWNER, ...bundleFromRules(rules) };
|
|
2665
2885
|
},
|
|
2666
2886
|
});
|
|
2887
|
+
// A guard below 0.22.0 cannot enforce both pins (below 0.20.0 it ignores allowedAgents and serves every agent; 0.22.0 added gatewayOwnerPrincipal): refuse to start on one (a stale lockfile, say).
|
|
2888
|
+
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
2889
|
|
|
2668
2890
|
// One protected route per gated action in index.mjs. This gateway IS the tool, not a proxy in
|
|
2669
2891
|
// front of one, so a path with no route below is refused (denyByDefault) — nothing to fall through TO.
|
|
@@ -3232,7 +3454,7 @@ const WHY = {
|
|
|
3232
3454
|
MERCHANT_NOT_ALLOWED: 'the mandate lists which merchants this agent may pay',
|
|
3233
3455
|
NO_PERMISSION_FOR_ACTION: 'the mandate never granted this action - at any amount',
|
|
3234
3456
|
NO_MANDATE: 'there is no mandate for this action at all',
|
|
3235
|
-
MANDATE_REVOKED: 'the owner revoked the mandate for this action',
|
|
3457
|
+
MANDATE_REVOKED: 'the owner revoked the mandate for this action',
|
|
3236
3458
|
};
|
|
3237
3459
|
|
|
3238
3460
|
async function attempt(n, intent, action, args, tool = gatedBookFlight) {
|
|
@@ -3258,7 +3480,7 @@ async function attempt(n, intent, action, args, tool = gatedBookFlight) {
|
|
|
3258
3480
|
} else {
|
|
3259
3481
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
|
|
3260
3482
|
console.log(dim(' ' + why));
|
|
3261
|
-
console.log(dim(' your tool never ran -
|
|
3483
|
+
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
3484
|
}
|
|
3263
3485
|
dashboard.logDecision(action, args, g);
|
|
3264
3486
|
}
|
|
@@ -3455,7 +3677,7 @@ async function attempt(n, total, step) {
|
|
|
3455
3677
|
}
|
|
3456
3678
|
} else {
|
|
3457
3679
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? e.message));
|
|
3458
|
-
console.log(dim(' your tool never ran -
|
|
3680
|
+
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
3681
|
}
|
|
3460
3682
|
dashboard.logDecision(action, step.context, g);
|
|
3461
3683
|
}
|
|
@@ -3928,6 +4150,7 @@ async function runClaim(args) {
|
|
|
3928
4150
|
keepConfigAndStop('--financial was passed, but the approved agent has NO spending authority (the request was non-financial).');
|
|
3929
4151
|
}
|
|
3930
4152
|
if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndStop(NO_POLICY_KEY(base));
|
|
4153
|
+
if (!args['no-gateway'] && !(await ensureOwnerPrincipal(config, config.apiBase || base))) keepConfigAndStop(NO_OWNER_PRINCIPAL(base));
|
|
3931
4154
|
const slug = slugify(state.name || 'metamynd-agent');
|
|
3932
4155
|
const outDir = resolve(String(args.out || `./${slug}`));
|
|
3933
4156
|
if (issuedNonFinancial) {
|
|
@@ -4137,22 +4360,26 @@ async function main() {
|
|
|
4137
4360
|
|
|
4138
4361
|
// 3c. The gateway's OWN identity — a real did:key, separate from the agent's own (an agent must
|
|
4139
4362
|
// 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.
|
|
4363
|
+
// claimed it may) — and, on either shape, the key that signs its reports to your Activity Log (MAGP 16.4): a
|
|
4364
|
+
// non-financial gateway claims nothing, so without a registered identity nothing it ran would reach you.
|
|
4143
4365
|
let gatewayIdentity = null;
|
|
4144
|
-
if (
|
|
4366
|
+
if (!args['no-gateway']) {
|
|
4145
4367
|
const gwKeypair = generateAgentKeypair();
|
|
4146
4368
|
const gatewayDid = buildDidKey(rawPublicKeyFromSpkiHex(gwKeypair.publicKeyHex));
|
|
4147
4369
|
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
|
|
4370
|
+
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'}) …`));
|
|
4371
|
+
const reg = await registerGatewayCounterparty(base, token, gatewayDid, `${scope}-gateway`, financial ? 'claim' : 'report', gwKeypair.privateKeyHex);
|
|
4150
4372
|
if (reg.ok) {
|
|
4151
4373
|
console.log(` ${c.green('✓')} gateway DID ${c.b(gatewayDid)} — ${reg.message}`);
|
|
4152
4374
|
} else {
|
|
4153
4375
|
console.log(` ${c.yellow('!')} could not register the gateway as a trusted counterparty: ${reg.message}`);
|
|
4154
|
-
console.log(c.dim(
|
|
4155
|
-
|
|
4376
|
+
console.log(c.dim(financial
|
|
4377
|
+
? ` it still works on testnet (open by default with no registry entries); a MAINNET hold cannot be`
|
|
4378
|
+
: ` it still enforces; only its refusal reports cannot reach your Activity Log until it is`));
|
|
4379
|
+
console.log(c.dim(financial
|
|
4380
|
+
? ` claimed until it is registered — gateway/README.md ("Who may claim this agent's holds") shows how, with the`
|
|
4381
|
+
: ` registered — at /dashboard/counterparties, with the`));
|
|
4382
|
+
console.log(c.dim(` proof-of-control step registration requires, for ${gatewayDid}`));
|
|
4156
4383
|
}
|
|
4157
4384
|
}
|
|
4158
4385
|
|
|
@@ -4165,6 +4392,7 @@ async function main() {
|
|
|
4165
4392
|
for (const n of demo.notDemonstrated) console.log(` ${c.yellow('!')} not staged in the demo: ${n.rule} ${c.dim('— ' + n.why + '; still enforced')}`);
|
|
4166
4393
|
}
|
|
4167
4394
|
if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndFail(outDir, config, NO_POLICY_KEY(base));
|
|
4395
|
+
if (!args['no-gateway'] && !(await ensureOwnerPrincipal(config, config.apiBase || base))) keepConfigAndFail(outDir, config, NO_OWNER_PRINCIPAL(base));
|
|
4168
4396
|
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
4397
|
}
|
|
4170
4398
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.22",
|
|
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": {
|