create-metamynd-agent 0.14.18 → 0.14.24
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 +168 -37
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -65,7 +65,14 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
|
|
|
65
65
|
// authorize request of its choosing, MAGP §8.2). Required for every scaffolded agent.
|
|
66
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
67
|
// counterparty the owner registered, or (an open testnet owner) anyone (MAGP §8.7.4).
|
|
68
|
-
|
|
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
|
+
// 0.22.0: a concurrent resume of one escalation in a process is refused AUTHORIZATION_IN_USE, and a hold minted for an
|
|
72
|
+
// authorize whose answer never arrived is found by nonce and released. Required: npm run resume relies on the lock.
|
|
73
|
+
// 0.23.0: the guard derives the same risk floor as the gate (owner tier + a payment at or above the owner's share of the
|
|
74
|
+
// per-transaction cap is HIGH), so a local check and the gate agree on a large payment's review. No template change.
|
|
75
|
+
const GUARD_VERSION = '^0.23.0';
|
|
69
76
|
/** The harness entry point's config load, shared by both harness templates: a fresh clone has no
|
|
70
77
|
* agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
|
|
71
78
|
function harnessConfigLoad() {
|
|
@@ -143,7 +150,10 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
|
143
150
|
// 0.23.0: acceptCounterpartyChallenge() — a Service signs its acceptance of an owner's registration (MAGP §8.7.6). No
|
|
144
151
|
// template change; required with GATEWAY_VERSION below so the gateway never resolves a second guard of its own.
|
|
145
152
|
// 0.24.0: reportOutcome() signs a reportId and an occurrence count (aggregated refusals, a durable spool).
|
|
146
|
-
|
|
153
|
+
// 0.25.0: honourApprovals — a Service without requireAuthorization (the value-less gateway) still runs an owner-approved
|
|
154
|
+
// escalation, exactly once (MAGP §8.7.18). Required: the non-financial gateway sets it and asserts guard.honoursApprovals.
|
|
155
|
+
// 0.26.0: a gateway judges the same derived risk floor as the gate (owner tier + amount share of the cap).
|
|
156
|
+
const MCP_GUARD_VERSION = '^0.26.0';
|
|
147
157
|
/** A DID as it may appear inside a generated string literal (the gateway's allowedAgents pin): no quote, backslash or space. */
|
|
148
158
|
const SAFE_DID = /^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/;
|
|
149
159
|
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
@@ -174,7 +184,9 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
|
174
184
|
// 0.21.0: per-route allowedAgents; depends on agentsafe-mcp-guard ^0.22.0.
|
|
175
185
|
// 0.22.0: depends on agentsafe-mcp-guard ^0.23.0.
|
|
176
186
|
// 0.23.0: refusals aggregated with their count, and `reportSpool` (both hosted templates set it); depends on agentsafe-mcp-guard ^0.24.0.
|
|
177
|
-
|
|
187
|
+
// 0.24.0: depends on agentsafe-mcp-guard ^0.25.0 (one guard in the tree with MCP_GUARD_VERSION above). No template change.
|
|
188
|
+
// 0.25.0: depends on agentsafe-mcp-guard ^0.26.0 (one guard in the tree). No template change.
|
|
189
|
+
const GATEWAY_VERSION = '^0.25.0';
|
|
178
190
|
|
|
179
191
|
/** Appended to every scaffolded gateway server: give hold settlements still running a bounded moment on shutdown. */
|
|
180
192
|
const DRAIN_ON_SHUTDOWN = `
|
|
@@ -261,6 +273,11 @@ ${c.b('Options')}
|
|
|
261
273
|
--max-amount <n> Total mandate budget (default 10000)
|
|
262
274
|
--currency <cur> Currency (default USD)
|
|
263
275
|
--merchants <a,b> Allowed merchants, comma-separated (optional)
|
|
276
|
+
--risk-tier <level> The owner's risk floor for the action: low | medium | high | critical. The agent's own
|
|
277
|
+
riskLevel can raise it, never lower it (high = every call is reviewed by a person).
|
|
278
|
+
--amount-review <f> A single payment at or above this share of the per-transaction cap is high risk (reviewed),
|
|
279
|
+
whatever the agent claims. A fraction in (0, 1] (default 0.7), or "off". Without --merchants,
|
|
280
|
+
the first payment to each merchant is reviewed too.
|
|
264
281
|
--byok Bring-your-own-key: generate the keypair locally, provision + prove control
|
|
265
282
|
(MetaMynd never sees the private key). Overridden by --public-key.
|
|
266
283
|
--public-key <hex> BYOK with a key you already hold (SPKI/raw hex); you prove control yourself
|
|
@@ -1018,7 +1035,7 @@ async function resolveHostedRulePack(base, key, { financial, perTxnMax, maxAmoun
|
|
|
1018
1035
|
* dashboard registration shows its own dialog before setting the flag); see the printed message
|
|
1019
1036
|
* either way.
|
|
1020
1037
|
*/
|
|
1021
|
-
async function registerGatewayCounterparty(base, token, did, label, purpose = 'claim', privateKeyHex = null) {
|
|
1038
|
+
async function registerGatewayCounterparty(base, token, did, label, purpose = 'claim', privateKeyHex = null, agents = null) {
|
|
1022
1039
|
try {
|
|
1023
1040
|
// The gateway accepts its own registration (MAGP §8.7.6): ask for a challenge naming this owner, sign it with the gateway's
|
|
1024
1041
|
// key (generated here, so this process holds it), and register with that proof. A DID is never registered on its text.
|
|
@@ -1038,7 +1055,9 @@ async function registerGatewayCounterparty(base, token, did, label, purpose = 'c
|
|
|
1038
1055
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
1039
1056
|
// `report` (a non-financial gateway, which never claims): it may only report to this owner, and it changes nothing about
|
|
1040
1057
|
// who may claim their holds, so it needs no enforcement confirmation (MAGP §8.7.6, §16.4).
|
|
1041
|
-
|
|
1058
|
+
// `agents` (report only): the agents this gateway serves — the issuer lets it claim an approved value-less hold of THOSE
|
|
1059
|
+
// agents only, never another agent of the same owner (MAGP §8.7.6).
|
|
1060
|
+
body: JSON.stringify({ did, label, purpose, ...(proof ? { proof } : {}), ...(agents ? { agents } : {}), ...(purpose === 'claim' ? { confirmEnforcementChange: true } : {}) }),
|
|
1042
1061
|
});
|
|
1043
1062
|
const json = await res.json().catch(() => null);
|
|
1044
1063
|
if (!res.ok || json?.success === false) return { ok: false, message: json?.message ?? `HTTP ${res.status}` };
|
|
@@ -1134,12 +1153,74 @@ function keepConfigAndFail(outDir, config, why) {
|
|
|
1134
1153
|
* guard.escalationStatus(id) for the authorization and presents it to run the action (MAGP §8.7.18). Generated code: it
|
|
1135
1154
|
* reads `g`, the refusal's governance verdict, and `dim`, both in scope at every call site.
|
|
1136
1155
|
*/
|
|
1137
|
-
function escalateNote(sandbox) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
console.log(dim(' Provision your own agent to approve one in your dashboard.'))
|
|
1141
|
-
|
|
1156
|
+
function escalateNote(sandbox, argsExpr = null) {
|
|
1157
|
+
if (sandbox) {
|
|
1158
|
+
return `console.log(dim(' nobody can approve it here: the shared sandbox has no owner you can sign in as.'));
|
|
1159
|
+
console.log(dim(' Provision your own agent to approve one in your dashboard.'));`;
|
|
1160
|
+
}
|
|
1161
|
+
if (!argsExpr) {
|
|
1162
|
+
return `console.log(dim(' not a failure: it waits in your dashboard, under Escalations' + (g.escalationId ? ' (' + g.escalationId + ')' : '') + '.'));
|
|
1142
1163
|
console.log(dim(' Once you approve it, guard.escalationStatus(id) gives the agent the authorization to run it.'));`;
|
|
1164
|
+
}
|
|
1165
|
+
// Resumable (0.14.21; pre-beta rerun 3, E-1): the call is saved so `npm run resume -- <id>` can run it, once approved.
|
|
1166
|
+
return `console.log(dim(' not a failure: it waits in your dashboard, under Escalations' + (g.escalationId ? ' (' + g.escalationId + ')' : '') + '.'));
|
|
1167
|
+
if (g.escalationId) {
|
|
1168
|
+
saveEscalation(g.escalationId, ${argsExpr});
|
|
1169
|
+
console.log(dim(' Approve it there, then run it - exactly once - with: npm run resume -- ' + g.escalationId));
|
|
1170
|
+
}`;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* The resume path a resumable scaffold carries (0.14.21; pre-beta rerun 3, E-1 — resuming used to mean writing ~15 lines
|
|
1175
|
+
* by hand). `npm start` saves an escalated call's arguments to ./escalations/<id>.json; `npm run resume -- <id>` hands them
|
|
1176
|
+
* to the gated tool's `.resume()` (agentsafe-guard >= 0.21.0), which waits for the owner's decision and then runs the tool
|
|
1177
|
+
* exactly once under the authorization the approval minted. Generated code; `tool` is the gated tool's variable name.
|
|
1178
|
+
*/
|
|
1179
|
+
function resumeBlock(tool) {
|
|
1180
|
+
return `// --- Escalations: \`npm run resume -- <escalationId>\` runs a call your owner APPROVED, exactly once. ---
|
|
1181
|
+
// npm start saves an escalated call's arguments to ./escalations/<id>.json. Approve it in the dashboard (AgentSafe ->
|
|
1182
|
+
// Escalations), then resume it: ${tool}.resume() waits for the decision and runs the tool once, under the authorization
|
|
1183
|
+
// the approval minted. Your own agent does the same with the escalationId from the refusal (e.governance.escalationId).
|
|
1184
|
+
import { mkdirSync as __mkdir, writeFileSync as __write, readFileSync as __read, rmSync as __rm, renameSync as __rename } from 'node:fs';
|
|
1185
|
+
const ESCALATIONS = new URL('./escalations/', import.meta.url);
|
|
1186
|
+
const escalationFile = (id) => {
|
|
1187
|
+
if (!/^[A-Za-z0-9_-]{1,80}$/.test(String(id ?? ''))) throw new Error('not an escalation id: ' + id);
|
|
1188
|
+
return new URL(id + '.json', ESCALATIONS);
|
|
1189
|
+
};
|
|
1190
|
+
function saveEscalation(id, args) {
|
|
1191
|
+
__mkdir(ESCALATIONS, { recursive: true });
|
|
1192
|
+
__write(escalationFile(id), JSON.stringify(args, null, 2));
|
|
1193
|
+
}
|
|
1194
|
+
if (process.argv[2] === '--resume') {
|
|
1195
|
+
const id = process.argv[3];
|
|
1196
|
+
// Take the saved call ATOMICALLY (a rename): a second npm run resume of the same escalation started meanwhile finds
|
|
1197
|
+
// nothing to take, so two processes cannot both run it. Put back if it is still undecided; removed once it is final.
|
|
1198
|
+
let saved, taken;
|
|
1199
|
+
try {
|
|
1200
|
+
const file = escalationFile(id);
|
|
1201
|
+
taken = new URL(file.href + '.resuming');
|
|
1202
|
+
__rename(file, taken);
|
|
1203
|
+
saved = JSON.parse(__read(taken, 'utf8'));
|
|
1204
|
+
} catch {
|
|
1205
|
+
console.error(' No saved escalation "' + (id ?? '') + '" to resume (or another npm run resume is running it). Run npm start first, then: npm run resume -- <escalationId>');
|
|
1206
|
+
process.exit(1);
|
|
1207
|
+
}
|
|
1208
|
+
console.log(' waiting for your decision on ' + id + ' (approve it in the dashboard: AgentSafe -> Escalations) ...');
|
|
1209
|
+
try {
|
|
1210
|
+
const result = await ${tool}.resume(id, saved);
|
|
1211
|
+
__rm(taken, { force: true }); // used: a second resume has nothing to run
|
|
1212
|
+
console.log('\\x1b[32m RAN\\x1b[0m approved, and run exactly once: ' + JSON.stringify(result));
|
|
1213
|
+
} catch (e) {
|
|
1214
|
+
const g = e.governance ?? {};
|
|
1215
|
+
// A final answer (rejected, already used, expired) retires the saved call; still pending or unreachable keeps it.
|
|
1216
|
+
if (g.decision === 'block' && g.reasonCode !== 'GATE_UNREACHABLE') __rm(taken, { force: true });
|
|
1217
|
+
else __rename(taken, escalationFile(id)); // still undecided, or the gate was unreachable: keep it for the next try
|
|
1218
|
+
console.log('\\x1b[31m NOT RUN\\x1b[0m ' + (g.reasonCode ?? e.message) + (g.status ? ' (' + g.status + ')' : ''));
|
|
1219
|
+
process.exitCode = 1;
|
|
1220
|
+
}
|
|
1221
|
+
process.exit();
|
|
1222
|
+
}
|
|
1223
|
+
`;
|
|
1143
1224
|
}
|
|
1144
1225
|
|
|
1145
1226
|
function exampleIndexNoGateway(scope, perTxnMax, currency, merchant, sandbox = false) {
|
|
@@ -1195,6 +1276,7 @@ const gatedRaiseOwnLimit = guard.guardTool(
|
|
|
1195
1276
|
}),
|
|
1196
1277
|
);
|
|
1197
1278
|
|
|
1279
|
+
${sandbox ? '' : resumeBlock('gatedBookFlight')}
|
|
1198
1280
|
const dim = (t) => '\\x1b[2m' + t + '\\x1b[0m';
|
|
1199
1281
|
const bold = (t) => '\\x1b[1m' + t + '\\x1b[0m';
|
|
1200
1282
|
const rule = (n) => ' ' + '-'.repeat(n);
|
|
@@ -1253,7 +1335,7 @@ async function attempt(n, intent, args, tool = gatedBookFlight) {
|
|
|
1253
1335
|
if (g.decision === 'escalate') {
|
|
1254
1336
|
console.log('\\x1b[33m ESCALATED\\x1b[0m held for a human - ' + g.reasonCode);
|
|
1255
1337
|
console.log(dim(' ' + why));
|
|
1256
|
-
${escalateNote(sandbox)}
|
|
1338
|
+
${escalateNote(sandbox, sandbox ? null : 'args')}
|
|
1257
1339
|
} else {
|
|
1258
1340
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
|
|
1259
1341
|
console.log(dim(' ' + why));
|
|
@@ -1400,6 +1482,7 @@ const gatedRaiseOwnLimit = guard.guardTool(
|
|
|
1400
1482
|
}),
|
|
1401
1483
|
);
|
|
1402
1484
|
|
|
1485
|
+
${resumeBlock('gatedBookFlight')}
|
|
1403
1486
|
const dim = (t) => '\\x1b[2m' + t + '\\x1b[0m';
|
|
1404
1487
|
const bold = (t) => '\\x1b[1m' + t + '\\x1b[0m';
|
|
1405
1488
|
const rule = (n) => ' ' + '-'.repeat(n);
|
|
@@ -1461,7 +1544,7 @@ async function attempt(n, intent, args, tool = gatedBookFlight) {
|
|
|
1461
1544
|
if (g.decision === 'escalate') {
|
|
1462
1545
|
console.log('\\x1b[33m ESCALATED\\x1b[0m held for a human - ' + g.reasonCode);
|
|
1463
1546
|
console.log(dim(' ' + why));
|
|
1464
|
-
${escalateNote(false)}
|
|
1547
|
+
${escalateNote(false, 'args')}
|
|
1465
1548
|
} else {
|
|
1466
1549
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
|
|
1467
1550
|
console.log(dim(' ' + why));
|
|
@@ -1615,7 +1698,7 @@ const gatedChangeOwnPermissions = guard.guardTool(
|
|
|
1615
1698
|
changeOwnPermissions,
|
|
1616
1699
|
(a) => ({ context: a }),
|
|
1617
1700
|
);
|
|
1618
|
-
|
|
1701
|
+
${!sandbox ? '\n' + resumeBlock('gatedAction') : ''}
|
|
1619
1702
|
const STEPS = ${JSON.stringify(steps, null, 2)};
|
|
1620
1703
|
const INPUTS = ${JSON.stringify(demo.inputs)};
|
|
1621
1704
|
const NOT_STAGED = ${JSON.stringify(demo.notDemonstrated)};
|
|
@@ -1636,7 +1719,7 @@ async function attempt(n, total, step) {
|
|
|
1636
1719
|
got = g.decision ?? 'error';
|
|
1637
1720
|
if (g.decision === 'escalate') {
|
|
1638
1721
|
console.log('\\x1b[33m ESCALATED\\x1b[0m held for a human - ' + g.reasonCode);
|
|
1639
|
-
${escalateNote(sandbox)}
|
|
1722
|
+
${escalateNote(sandbox, !sandbox ? 'step.context' : null)}
|
|
1640
1723
|
} else {
|
|
1641
1724
|
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? e.message));
|
|
1642
1725
|
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.'));
|
|
@@ -1759,15 +1842,20 @@ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', impo
|
|
|
1759
1842
|
// whose owner granted it this same action would run your tool with your credentials (refused AGENT_NOT_ADMITTED instead).
|
|
1760
1843
|
// Add a DID here only for another agent you mean this gateway to act for. gatewayOwnerPrincipal binds it to YOU, the owner of
|
|
1761
1844
|
// its credentials: an agent listed here that another principal owns is refused anyway (GATEWAY_OWNER_MISMATCH).
|
|
1762
|
-
|
|
1763
|
-
//
|
|
1845
|
+
// honourApprovals: with requireAuthorization off this gateway claims nothing — except an escalated call that presents the
|
|
1846
|
+
// authorization your APPROVAL minted (Dashboard -> Escalations). That one it claims as an approval: the issuer grants it
|
|
1847
|
+
// once, only for a hold a person approved, for this agent and these values, so the approved call runs here exactly once.
|
|
1848
|
+
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: false, policyPublicKey: '${policyKey}', allowedAgents: ['${agentDid}'], gatewayOwnerPrincipal: '${ownerPrincipal}', honourApprovals: true });
|
|
1849
|
+
// 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).
|
|
1764
1850
|
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');
|
|
1851
|
+
// A guard below 0.25.0 ignores honourApprovals, and an approved escalation would stay escalated here: refuse to start on one.
|
|
1852
|
+
if (guard.honoursApprovals !== true) throw new Error('this gateway needs @metamynd/agentsafe-mcp-guard >= 0.25.0 to run approved escalations - run npm install');
|
|
1765
1853
|
|
|
1766
1854
|
const gateway = createHttpGateway({
|
|
1767
1855
|
guard,
|
|
1768
1856
|
routes,
|
|
1769
1857
|
// Every request this gateway runs or refuses is reported, signed with the identity above, to your Activity Log (MAGP
|
|
1770
|
-
// 16.4). It claims
|
|
1858
|
+
// 16.4). It claims only an approved escalation, so without this the issuer would never see what it executed - or that another agent tried.
|
|
1771
1859
|
reportOutcomes: true,
|
|
1772
1860
|
// 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
1861
|
reportSpool: './reports.spool.jsonl',
|
|
@@ -1895,10 +1983,18 @@ Replace \`performAction()\` in \`server.mjs\` with your implementation and put a
|
|
|
1895
1983
|
|
|
1896
1984
|
/** The README for a non-financial hosted scaffold. `withGateway` selects the two-process shape. */
|
|
1897
1985
|
/** 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. */
|
|
1898
|
-
function changeRulesSection(sandbox) {
|
|
1986
|
+
function changeRulesSection(sandbox, resumable = false) {
|
|
1899
1987
|
return sandbox
|
|
1900
1988
|
? `${SANDBOX_RULES_FIXED} An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.`
|
|
1901
|
-
:
|
|
1989
|
+
: resumable
|
|
1990
|
+
? `Edit the agent's SOPs in the dashboard (${SOPS_PATH}). The agent's behaviour changes live —
|
|
1991
|
+
no redeploy.
|
|
1992
|
+
|
|
1993
|
+
An \`escalate\` verdict is held for an owner to approve. \`npm start\` saves an escalated call and prints its id; approve it
|
|
1994
|
+
in the dashboard (AgentSafe → Escalations), then \`npm run resume -- <escalationId>\` runs it exactly once under the
|
|
1995
|
+
authorization the approval minted. In your own agent that is one call: \`await gatedTool.resume(escalationId, sameArgs)\`
|
|
1996
|
+
(\`@metamynd/agentsafe-guard\` ≥ 0.21.0) — pass the SAME arguments; the approval is bound to that request.`
|
|
1997
|
+
: `Edit the agent's SOPs in the dashboard (${SOPS_PATH}). The agent's behaviour changes live —
|
|
1902
1998
|
no redeploy. An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.`;
|
|
1903
1999
|
}
|
|
1904
2000
|
|
|
@@ -1981,7 +2077,7 @@ ${clonedFreshSection(daemonKey)}
|
|
|
1981
2077
|
|
|
1982
2078
|
## Change the rules
|
|
1983
2079
|
|
|
1984
|
-
${changeRulesSection(sandbox)}
|
|
2080
|
+
${changeRulesSection(sandbox, !sandbox)}
|
|
1985
2081
|
|
|
1986
2082
|
## What this is not
|
|
1987
2083
|
|
|
@@ -2000,7 +2096,7 @@ Full integration guide: \`docs/integration/INTEGRATE-WITH-METAMYND.md\`.
|
|
|
2000
2096
|
`;
|
|
2001
2097
|
}
|
|
2002
2098
|
|
|
2003
|
-
function examplePackageJson(slug, neutral = false) {
|
|
2099
|
+
function examplePackageJson(slug, neutral = false, resumable = false) {
|
|
2004
2100
|
return JSON.stringify(
|
|
2005
2101
|
{
|
|
2006
2102
|
name: slug,
|
|
@@ -2010,7 +2106,7 @@ function examplePackageJson(slug, neutral = false) {
|
|
|
2010
2106
|
// `verify` is scaffolded in because governance that lives only in a dashboard is a
|
|
2011
2107
|
// thing someone has to remember to look at. As a build step it is a control: a change
|
|
2012
2108
|
// that widens this agent's authority fails `npm test`.
|
|
2013
|
-
scripts: { start: 'node index.mjs', test: neutral ? 'agentsafe-guard verify --context ./verify-context.json' : 'agentsafe-guard verify' },
|
|
2109
|
+
scripts: { start: 'node index.mjs', ...(resumable ? { resume: 'node index.mjs --resume' } : {}), test: neutral ? 'agentsafe-guard verify --context ./verify-context.json' : 'agentsafe-guard verify' },
|
|
2014
2110
|
dependencies: { [GUARD_PKG]: GUARD_VERSION },
|
|
2015
2111
|
},
|
|
2016
2112
|
null,
|
|
@@ -2112,14 +2208,14 @@ ${clonedFreshSection(daemonKey)}
|
|
|
2112
2208
|
|
|
2113
2209
|
## Change the rules
|
|
2114
2210
|
|
|
2115
|
-
${changeRulesSection(sandbox)}
|
|
2211
|
+
${changeRulesSection(sandbox, !sandbox)}
|
|
2116
2212
|
|
|
2117
2213
|
Full integration guide: \`docs/integration/INTEGRATE-WITH-METAMYND.md\`.
|
|
2118
2214
|
`;
|
|
2119
2215
|
}
|
|
2120
2216
|
|
|
2121
2217
|
function gitignore() {
|
|
2122
|
-
return `node_modules/\nagent.metamynd.json\n.env\n`;
|
|
2218
|
+
return `node_modules/\nagent.metamynd.json\n.env\nescalations/\n`;
|
|
2123
2219
|
}
|
|
2124
2220
|
|
|
2125
2221
|
// ---------- the default hosted scaffold's second process: a separate tool gateway ----------
|
|
@@ -2194,7 +2290,7 @@ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', impo
|
|
|
2194
2290
|
// The claim below does not stop that on its own: the hold belongs to the CALLER, and the caller's owner decides which
|
|
2195
2291
|
// services may claim it — including registering this gateway's DID as one of theirs.
|
|
2196
2292
|
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).
|
|
2293
|
+
// 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).
|
|
2198
2294
|
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');
|
|
2199
2295
|
|
|
2200
2296
|
const gateway = createHttpGateway({
|
|
@@ -2388,17 +2484,33 @@ on purpose: an agent must never be able to release or lower-settle a hold it aut
|
|
|
2388
2484
|
provisioning, \`npx create-metamynd-agent\` registered that DID as a trusted counterparty for you
|
|
2389
2485
|
(\`POST /policy/counterparties\`). A **MAINNET** hold is *always* registered-only, regardless of your
|
|
2390
2486
|
registry (MAGP §8.7.10) — a testnet hold stays open unless you have registered anything at all.
|
|
2391
|
-
Manage the registry at \`/dashboard/counterparties
|
|
2487
|
+
Manage the registry at \`/dashboard/counterparties\` (it walks you through the proof below), or
|
|
2488
|
+
re-register this gateway yourself — after removing it from the registry, say.
|
|
2489
|
+
|
|
2490
|
+
Registration needs **proof of control** (MAGP §8.7.6): you ask for a challenge naming you, this
|
|
2491
|
+
gateway signs it with its own key from \`service.metamynd.json\`, and you register with that
|
|
2492
|
+
signature. A DID is never registered on its text alone, so nobody can register a gateway they do
|
|
2493
|
+
not hold the key for. Run this from this directory (bash; \`TOKEN\` is your owner access token):
|
|
2392
2494
|
|
|
2393
2495
|
\`\`\`bash
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2496
|
+
MAGP_API=https://metamynd.ai/api/v1 TOKEN=<owner access token> node --input-type=module -e "
|
|
2497
|
+
import { readFileSync } from 'node:fs'; import crypto from 'node:crypto';
|
|
2498
|
+
const { serviceDid, serviceKey } = JSON.parse(readFileSync('./service.metamynd.json', 'utf8'));
|
|
2499
|
+
const api = process.env.MAGP_API, headers = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + process.env.TOKEN };
|
|
2500
|
+
const post = (path, body) => fetch(api + path, { method: 'POST', headers, body: JSON.stringify(body) });
|
|
2501
|
+
const ch = (await (await post('/policy/counterparties/challenge', { did: serviceDid, purpose: 'claim' })).json()).data;
|
|
2502
|
+
const key = crypto.createPrivateKey({ key: Buffer.from(serviceKey, 'hex'), format: 'der', type: 'pkcs8' });
|
|
2503
|
+
const signature = crypto.sign(null, Buffer.from(ch.message, 'utf8'), key).toString('hex');
|
|
2504
|
+
const r = await post('/policy/counterparties', { did: serviceDid, purpose: 'claim', proof: { challengeToken: ch.challengeToken, signature }, confirmEnforcementChange: true });
|
|
2505
|
+
console.log(r.status, await r.text());
|
|
2506
|
+
"
|
|
2397
2507
|
\`\`\`
|
|
2398
2508
|
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2509
|
+
The gateway's own \`guard.acceptCounterpartyChallenge(message)\` (\`@metamynd/agentsafe-mcp-guard\`
|
|
2510
|
+
≥ 0.23.0) signs the same thing, if you would rather expose it from \`server.mjs\`.
|
|
2511
|
+
\`confirmEnforcementChange: true\` matters only for your account's FIRST-EVER registration — it
|
|
2512
|
+
switches EVERY one of your holds (not just this agent's) from open to registered-only, so the API
|
|
2513
|
+
refuses a first registration without it.
|
|
2402
2514
|
- **Amount unknown.** \`amount-unknown\` (\`@metamynd/agentsafe-mcp-guard\` ≥ 0.3.0) blocks a
|
|
2403
2515
|
platform tool by default when its raw bytes or a nested payload hide the amount from a naive
|
|
2404
2516
|
spend cap — AND this agent's OWN starter SOP (see \`agent.metamynd.json\` /
|
|
@@ -2479,7 +2591,8 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2479
2591
|
: exampleIndexNoGateway(scope, perTxnMax, currency, paymentMerchant, !!sandbox),
|
|
2480
2592
|
force,
|
|
2481
2593
|
);
|
|
2482
|
-
|
|
2594
|
+
// Resumable = the index carries the resume path (resumeBlock): every agent but the shared sandbox, where nobody can approve.
|
|
2595
|
+
writeFileSafe(outDir, 'package.json', examplePackageJson(slug, neutral, !sandbox), force);
|
|
2483
2596
|
writeFileSafe(outDir, '.gitignore', gitignore(), force);
|
|
2484
2597
|
writeFileSafe(
|
|
2485
2598
|
outDir,
|
|
@@ -2792,7 +2905,7 @@ const guard = createMcpGuard({
|
|
|
2792
2905
|
return { subject: AGENT_DID, ownerPrincipal: HARNESS_OWNER, ...bundleFromRules(rules) };
|
|
2793
2906
|
},
|
|
2794
2907
|
});
|
|
2795
|
-
// A guard below 0.20.0 ignores allowedAgents and serves every agent: refuse to start on one (a stale lockfile, say).
|
|
2908
|
+
// 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).
|
|
2796
2909
|
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');
|
|
2797
2910
|
|
|
2798
2911
|
// One protected route per gated action in index.mjs. This gateway IS the tool, not a proxy in
|
|
@@ -4152,6 +4265,13 @@ async function main() {
|
|
|
4152
4265
|
Array.isArray(fileConfig?.merchants) ? fileConfig.merchants.join(',') : '',
|
|
4153
4266
|
);
|
|
4154
4267
|
const merchants = String(merchantsRaw).split(',').map((s) => s.trim()).filter(Boolean);
|
|
4268
|
+
// Risk the agent cannot lower (MAGP 6.4.3): the owner's floor, and the amount share of the cap that is reviewed.
|
|
4269
|
+
const riskTier = typeof args['risk-tier'] === 'string' ? args['risk-tier'].trim().toLowerCase() : undefined;
|
|
4270
|
+
if (riskTier !== undefined && !['low', 'medium', 'high', 'critical'].includes(riskTier)) fail('--risk-tier must be low, medium, high or critical');
|
|
4271
|
+
const amountReviewRaw = typeof args['amount-review'] === 'string' ? args['amount-review'].trim().toLowerCase() : undefined;
|
|
4272
|
+
const amountShare = amountReviewRaw === undefined ? undefined : amountReviewRaw === 'off' ? false : Number(amountReviewRaw);
|
|
4273
|
+
if (typeof amountShare === 'number' && !(amountShare > 0 && amountShare <= 1)) fail('--amount-review must be a fraction in (0, 1], or "off"');
|
|
4274
|
+
const riskFields = { ...(riskTier ? { riskTier } : {}), ...(amountShare !== undefined && financial ? { riskSignals: { amountShare } } : {}) };
|
|
4155
4275
|
|
|
4156
4276
|
// BYOK: --byok generates a keypair on THIS machine (MetaMynd never sees the private key) —
|
|
4157
4277
|
// either locally in this process (default) or, opt-in, via an already-running agentsafe-signer
|
|
@@ -4222,6 +4342,7 @@ async function main() {
|
|
|
4222
4342
|
network: 'testnet',
|
|
4223
4343
|
...(financial ? { currency, maxAmount, perTxnMax } : {}),
|
|
4224
4344
|
merchants,
|
|
4345
|
+
...riskFields,
|
|
4225
4346
|
...(publicKey ? { publicKey } : {}),
|
|
4226
4347
|
...sopFields,
|
|
4227
4348
|
};
|
|
@@ -4230,6 +4351,11 @@ async function main() {
|
|
|
4230
4351
|
if (!config?.agentDid) fail('Provisioning did not return a config with an agentDid.');
|
|
4231
4352
|
console.log(` ${c.green('✓')} agent DID ${c.b(config.agentDid)}`);
|
|
4232
4353
|
if (config.standards?.length) console.log(` ${c.green('✓')} enforced Standards: ${config.standards.join(', ')}`);
|
|
4354
|
+
if (financial && merchants.length === 0) {
|
|
4355
|
+
console.log(c.dim(' no --merchants list: the FIRST payment to each merchant is held for your review (a risk the agent cannot'));
|
|
4356
|
+
console.log(c.dim(' lower). Pass --merchants a,b to pre-approve them, or register them as payees in the dashboard.'));
|
|
4357
|
+
}
|
|
4358
|
+
if (riskTier) console.log(` ${c.green('✓')} risk floor: ${riskTier} (the agent's own claim can raise it, never lower it)`);
|
|
4233
4359
|
|
|
4234
4360
|
// 3b. BYOK: prove control of the key (verify-key), else the gate blocks with AGENT_KEY_UNVERIFIED.
|
|
4235
4361
|
if (daemonPublicKeyHex) {
|
|
@@ -4276,13 +4402,18 @@ async function main() {
|
|
|
4276
4402
|
const gatewayDid = buildDidKey(rawPublicKeyFromSpkiHex(gwKeypair.publicKeyHex));
|
|
4277
4403
|
gatewayIdentity = { did: gatewayDid, keyHex: gwKeypair.privateKeyHex };
|
|
4278
4404
|
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);
|
|
4405
|
+
const reg = await registerGatewayCounterparty(base, token, gatewayDid, `${scope}-gateway`, financial ? 'claim' : 'report', gwKeypair.privateKeyHex, financial ? null : [config.agentDid]);
|
|
4280
4406
|
if (reg.ok) {
|
|
4281
4407
|
console.log(` ${c.green('✓')} gateway DID ${c.b(gatewayDid)} — ${reg.message}`);
|
|
4282
4408
|
} else {
|
|
4283
4409
|
console.log(` ${c.yellow('!')} could not register the gateway as a trusted counterparty: ${reg.message}`);
|
|
4284
|
-
console.log(c.dim(
|
|
4285
|
-
|
|
4410
|
+
console.log(c.dim(financial
|
|
4411
|
+
? ` it still works on testnet (open by default with no registry entries); a MAINNET hold cannot be`
|
|
4412
|
+
: ` it still enforces; only its refusal reports cannot reach your Activity Log until it is`));
|
|
4413
|
+
console.log(c.dim(financial
|
|
4414
|
+
? ` claimed until it is registered — gateway/README.md ("Who may claim this agent's holds") shows how, with the`
|
|
4415
|
+
: ` registered — at /dashboard/counterparties, with the`));
|
|
4416
|
+
console.log(c.dim(` proof-of-control step registration requires, for ${gatewayDid}`));
|
|
4286
4417
|
}
|
|
4287
4418
|
}
|
|
4288
4419
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.24",
|
|
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": {
|