create-metamynd-agent 0.14.2 → 0.14.6
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/README.md +11 -3
- package/index.mjs +150 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -175,6 +175,11 @@ the whole protocol.
|
|
|
175
175
|
Named precisely, not left implicit:
|
|
176
176
|
|
|
177
177
|
- **Direct call.** `bookFlight()` doesn't exist in the agent's process.
|
|
178
|
+
- **Forged or replayed policy (every gateway is pinned, 0.14.3).** The gateway's `policyPublicKey` is baked in from your
|
|
179
|
+
provisioning response (or, from an older API, `GET /magp/policy/pubkey`), and with `@metamynd/agentsafe-mcp-guard` 0.18.1 an
|
|
180
|
+
unsigned, tampered or stale bundle is refused for every action, amount 0 included — so a non-financial gateway cannot be granted
|
|
181
|
+
an action by whoever sits on its bundle fetch. If no key can be had, the CLI scaffolds no gateway at all (it keeps the
|
|
182
|
+
provisioned config and says so) rather than writing an unpinned one.
|
|
178
183
|
- **Payload binding is ON by default (0.11.0, MAGP §8.3.9).** The generated agent signs the COMPLETE body it sends as a payload, the
|
|
179
184
|
authorization it gets from the gate is bound to that same body, and the generated gateway sets `requirePayloadBinding: true`: it
|
|
180
185
|
digests the body it is about to run and the issuer refuses the claim unless that is the digest the agent signed. The eight
|
|
@@ -428,9 +433,12 @@ signed value only and ignores one in the context. Scaffolds pin `@metamynd/agent
|
|
|
428
433
|
|
|
429
434
|
The starter rules include a high-risk review. A request that sends **no** `riskLevel` (or an unrecognised one)
|
|
430
435
|
is no longer waved through: it is **escalated** (`CONTEXT_UNVERIFIABLE`) for a human, because an agent that omits
|
|
431
|
-
its risk is indistinguishable from one hiding it. The scaffolded
|
|
432
|
-
|
|
433
|
-
|
|
436
|
+
its risk is indistinguishable from one hiding it. The scaffolded examples pass the tool call's own `riskLevel`
|
|
437
|
+
through **unchanged** — there is no default, so a call that does not say is sent for review rather than treated as
|
|
438
|
+
`'low'` (the demo steps each state their risk explicitly). The neutral template passes your own `args` as the
|
|
439
|
+
context, so include it there. Either way `riskLevel` is **the agent's own claim**: it travels in the request
|
|
440
|
+
context, and even when the agent's key signs that context, the signature proves who said it, not that it is true —
|
|
441
|
+
nothing checks the value unless your owner configures provenance for it. A real integration should send an honest one — or, better, not depend on the agent at all: a mandate
|
|
434
442
|
permission can carry an owner-set `riskTier` that no claim can lower, and a gateway route can derive the risk
|
|
435
443
|
itself with `route.trustedContext` (`@metamynd/agentsafe-http-gateway` 0.10.0). See MAGP §6.3.
|
|
436
444
|
|
package/index.mjs
CHANGED
|
@@ -59,7 +59,9 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
|
|
|
59
59
|
// 0.17.0 signs the agent's context (envelopeSignature, MAGP 8.3.13) by default; the scaffolded agent gets it with no template
|
|
60
60
|
// change (signContext: false opts out). A daemon-custody agent needs agentsafe-signer >= 0.19.0 for its no-amount requests.
|
|
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
|
+
// the action resumes", and below this the escalation never reached the owner's queue (decided in-process, no escalationId).
|
|
64
|
+
const GUARD_VERSION = '^0.18.1';
|
|
63
65
|
/** The harness entry point's config load, shared by both harness templates: a fresh clone has no
|
|
64
66
|
* agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
|
|
65
67
|
function harnessConfigLoad() {
|
|
@@ -120,7 +122,10 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
|
120
122
|
// requireContextSignature option. No template change (the scaffolded agent does not sign its context by default).
|
|
121
123
|
// 0.18.0: an opt-in bundleCache (push-invalidated by the issuer's events stream) and keepAliveFetch exported. No
|
|
122
124
|
// template change (the scaffolded gateways don't enable the cache).
|
|
123
|
-
|
|
125
|
+
// 0.18.1: the pinned policyPublicKey every scaffolded gateway sets now refuses an unsigned or stale bundle for an
|
|
126
|
+
// amount-0 action too. Required: the non-financial gateway's every request is amount 0, so below this a stripped
|
|
127
|
+
// bundle could grant it an action the agent never had.
|
|
128
|
+
const MCP_GUARD_VERSION = '^0.18.1';
|
|
124
129
|
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
125
130
|
// 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
|
|
126
131
|
// never scaffold a range that could resolve below it.
|
|
@@ -369,6 +374,16 @@ function loadConfigFile(path) {
|
|
|
369
374
|
return json;
|
|
370
375
|
}
|
|
371
376
|
|
|
377
|
+
// Written into every financial scaffold's README. The generated code passes the tool call's riskLevel through
|
|
378
|
+
// unchanged (no `?? 'low'`): a missing one must reach the gate as missing, where it escalates by design.
|
|
379
|
+
const RISK_LEVEL_NOTE = `## Risk level: the agent's own claim
|
|
380
|
+
|
|
381
|
+
\`riskLevel\` is **the agent's own claim**. It travels in the request's context: the agent's key may sign that context,
|
|
382
|
+
which proves who said it, not that it is true, and nothing checks the value unless your owner configures provenance for it (an owner-set \`riskTier\` on the mandate permission, or a
|
|
383
|
+
gateway route's \`trustedContext\`). \`index.mjs\` passes the tool call's own \`riskLevel\` through unchanged —
|
|
384
|
+
there is **no default**. A call that does not state one (or states one the gate does not recognise) is **sent
|
|
385
|
+
for review** (\`CONTEXT_UNVERIFIABLE\`), never treated as \`low\`. The demo steps each state their risk explicitly.`;
|
|
386
|
+
|
|
372
387
|
// A rule needs `when.predicate` (the atom) and `then` (the decision the gate should return
|
|
373
388
|
// when it fires) — everything else is optional sugar. See backend's atom-catalog.ts for the
|
|
374
389
|
// full predicate list (amount-over, risk-at-or-above, jurisdiction-not-allowed, ...).
|
|
@@ -471,10 +486,27 @@ function defaultNeutralDemo() {
|
|
|
471
486
|
return demo;
|
|
472
487
|
}
|
|
473
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Where an agent's rules are edited in the dashboard. SOPs belong to the legal entity, so the page is
|
|
491
|
+
* VeriFAI → Legal Entities (/dashboard/organization), SOPs tab; the AgentSafe sidebar group links the same
|
|
492
|
+
* tab as "SOPs". This said "Legal Entity → SOPs", a path no menu had (2026-10-02 eval, L-1).
|
|
493
|
+
*/
|
|
494
|
+
const SOPS_PATH = 'Dashboard → VeriFAI → Legal Entities → SOPs tab (also linked as AgentSafe → SOPs)';
|
|
495
|
+
/** The same path for the generated demo's console output, which stays ASCII. */
|
|
496
|
+
const SOPS_PATH_ASCII = 'Dashboard -> VeriFAI -> Legal Entities -> SOPs';
|
|
497
|
+
/**
|
|
498
|
+
* A --sandbox scaffold runs as the SHARED sandbox agent, which no user has dashboard access to, so "change
|
|
499
|
+
* the rules in the dashboard" sent them somewhere they could not go (L-7). Say how to get an agent whose
|
|
500
|
+
* rules they CAN change instead.
|
|
501
|
+
*/
|
|
502
|
+
const SANDBOX_RULES_FIXED = "This is the shared sandbox agent: its rules are fixed and nobody can edit them. To change the rules, provision your own agent — re-run without --sandbox (it signs you up and creates an agent whose SOPs you own, edited at " + SOPS_PATH + "), or use --harness for a free local agent whose rules you edit by hand.";
|
|
503
|
+
|
|
474
504
|
/** A --config file's rules cannot reach an agent the caller does not provision themselves: say so instead of implying they apply. */
|
|
475
|
-
function warnRulesNotApplied(fileConfig, where) {
|
|
505
|
+
function warnRulesNotApplied(fileConfig, where, { shared = false } = {}) {
|
|
476
506
|
if (policyMolecules(fileConfig).length === 0) return;
|
|
477
|
-
console.log(
|
|
507
|
+
console.log(shared
|
|
508
|
+
? ` ${c.yellow('!')} the rules in ${c.b('the config file')} are not applied to ${where} — its rules are the platform's defaults, and nobody can change them. Provision your own agent (drop this flag) to author them here.`
|
|
509
|
+
: ` ${c.yellow('!')} the rules in ${c.b('the config file')} are not applied to ${where} — its rules are the platform's defaults. Its owner sets them in the dashboard (${SOPS_PATH}), or provision your own agent (drop this flag) to author them here.`);
|
|
478
510
|
}
|
|
479
511
|
|
|
480
512
|
/**
|
|
@@ -972,6 +1004,50 @@ async function registerGatewayCounterparty(base, token, did, label) {
|
|
|
972
1004
|
}
|
|
973
1005
|
}
|
|
974
1006
|
|
|
1007
|
+
/**
|
|
1008
|
+
* Every scaffolded gateway pins the issuer's policy-signing key, so it refuses any bundle MetaMynd did not sign —
|
|
1009
|
+
* an unpinned gateway trusts whatever its bundle fetch returns, and over plain http:// that is whatever an
|
|
1010
|
+
* interceptor wrote (mcp-guard only refuses VALUE-bearing actions there, and a non-financial agent has none).
|
|
1011
|
+
* The provisioning response carries the key (`issuer.policyKey`); when it does not (an older backend), it is read
|
|
1012
|
+
* from GET /magp/policy/pubkey on the same API this CLI just provisioned through, then saved into the config.
|
|
1013
|
+
* Returns the key, or null when neither source has one — the caller must then not scaffold a gateway.
|
|
1014
|
+
*/
|
|
1015
|
+
async function ensurePolicyKey(config, base) {
|
|
1016
|
+
const isKey = (k) => typeof k === 'string' && /^[0-9a-f]{64}$/i.test(k);
|
|
1017
|
+
if (isKey(config.issuer?.policyKey)) return config.issuer.policyKey;
|
|
1018
|
+
let key = null;
|
|
1019
|
+
try {
|
|
1020
|
+
const res = await fetch(`${String(base).replace(/\/+$/, '')}/magp/policy/pubkey`);
|
|
1021
|
+
key = (await res.json().catch(() => null))?.data?.publicKey ?? null;
|
|
1022
|
+
} catch {
|
|
1023
|
+
key = null;
|
|
1024
|
+
}
|
|
1025
|
+
if (!isKey(key)) return null;
|
|
1026
|
+
config.issuer = { ...(config.issuer ?? {}), policyKey: key.toLowerCase() };
|
|
1027
|
+
return config.issuer.policyKey;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const NO_POLICY_KEY = (base) =>
|
|
1031
|
+
`Could not get MetaMynd's policy-signing key: the provisioning response carried none, and GET ${base}/magp/policy/pubkey ` +
|
|
1032
|
+
'did not return one. A gateway without it would trust whatever policy bundle its network fetch returns, so none was ' +
|
|
1033
|
+
'scaffolded. Retry when the API is reachable, or pass --no-gateway for the single-process example (not an enforcement boundary).';
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Stop after provisioning WITHOUT losing what it returned: a managed agent key is handed back once and cannot be
|
|
1037
|
+
* fetched again. Saves the config into the project directory (never over an existing file), then fails with `why`.
|
|
1038
|
+
*/
|
|
1039
|
+
function keepConfigAndFail(outDir, config, why) {
|
|
1040
|
+
let kept = null;
|
|
1041
|
+
try {
|
|
1042
|
+
mkdirSync(outDir, { recursive: true });
|
|
1043
|
+
kept = join(outDir, existsSync(join(outDir, 'agent.metamynd.json')) ? `agent.metamynd.${Date.now()}.json` : 'agent.metamynd.json');
|
|
1044
|
+
writeFileSync(kept, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
1045
|
+
} catch {
|
|
1046
|
+
kept = null;
|
|
1047
|
+
}
|
|
1048
|
+
fail(`${why}${kept ? `\n The agent was provisioned; its config (with its key) was saved to ${kept.replace(/\\/g, '/')}.` : ''}`);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
975
1051
|
// ---------- scaffolding ----------
|
|
976
1052
|
/**
|
|
977
1053
|
* The --no-gateway / --sandbox variant: the tool is a local function in the SAME process as
|
|
@@ -983,7 +1059,7 @@ async function registerGatewayCounterparty(base, token, did, label) {
|
|
|
983
1059
|
* it — the same shape of gap --harness's README documents. See exampleIndex() below, which is
|
|
984
1060
|
* what the real (non-sandbox) flow scaffolds by default instead.
|
|
985
1061
|
*/
|
|
986
|
-
function exampleIndexNoGateway(scope, perTxnMax, currency, merchant) {
|
|
1062
|
+
function exampleIndexNoGateway(scope, perTxnMax, currency, merchant, sandbox = false) {
|
|
987
1063
|
const under = Math.max(1, Math.round(perTxnMax * 0.5));
|
|
988
1064
|
const over = Math.round(perTxnMax + 100);
|
|
989
1065
|
return `// index.mjs — your agent, governed by MetaMynd/AgentSafe.
|
|
@@ -1012,7 +1088,10 @@ const gatedBookFlight = guard.guardTool(
|
|
|
1012
1088
|
amount: a.amount,
|
|
1013
1089
|
currency: '${currency}',
|
|
1014
1090
|
merchant: a.merchant,
|
|
1015
|
-
|
|
1091
|
+
// riskLevel is the agent's OWN claim: signing it proves who said it, not that it is true, and nothing checks it unless
|
|
1092
|
+
// the owner configures provenance. Passed through as-is: when the agent does not say, it stays absent and the gate sends
|
|
1093
|
+
// the action for review — never 'low'.
|
|
1094
|
+
context: { tool: 'book-flight', riskLevel: a.riskLevel },
|
|
1016
1095
|
}),
|
|
1017
1096
|
);
|
|
1018
1097
|
|
|
@@ -1127,8 +1206,12 @@ console.log(bold(' Without MetaMynd, you can be bypassed.') + ' bookFlight() ru
|
|
|
1127
1206
|
console.log(dim(' call it directly instead of gatedBookFlight and nothing above stops you.'));
|
|
1128
1207
|
console.log(dim(' Re-scaffold without --sandbox/--no-gateway for the default shape, which does.'));
|
|
1129
1208
|
console.log('');
|
|
1130
|
-
console.log('
|
|
1131
|
-
console.log(dim('
|
|
1209
|
+
${sandbox ? `console.log(' This is the SHARED sandbox agent: its rules are fixed and nobody can edit them.');
|
|
1210
|
+
console.log(dim(' To change the cap, provision your own agent: re-run create-metamynd-agent'));
|
|
1211
|
+
console.log(dim(' without --sandbox (it signs you up), then change the cap in your dashboard'));
|
|
1212
|
+
console.log(dim(' (${SOPS_PATH_ASCII}) and run again.'));
|
|
1213
|
+
console.log(dim(' The outcome changes. Your code does not. That is the point.'));` : `console.log(' Change the cap in the dashboard (${SOPS_PATH_ASCII}) and run again.');
|
|
1214
|
+
console.log(dim(' The outcome changes. This file does not. That is the point.'));`}
|
|
1132
1215
|
console.log('');
|
|
1133
1216
|
`;
|
|
1134
1217
|
}
|
|
@@ -1173,7 +1256,8 @@ async function bookFlightViaGateway(args, decision) {
|
|
|
1173
1256
|
amount: args.amount,
|
|
1174
1257
|
currency: args.currency ?? '${currency}',
|
|
1175
1258
|
merchant: args.merchant,
|
|
1176
|
-
|
|
1259
|
+
// Passed through as-is (never defaulted): a missing riskLevel must reach the gateway as missing.
|
|
1260
|
+
context: { tool: 'book-flight', riskLevel: args.riskLevel },
|
|
1177
1261
|
payload,
|
|
1178
1262
|
});
|
|
1179
1263
|
signed.authorizationId = decision?.authorizationId;
|
|
@@ -1201,7 +1285,10 @@ const gatedBookFlight = guard.guardTool(
|
|
|
1201
1285
|
amount: a.amount,
|
|
1202
1286
|
currency: a.currency ?? '${currency}',
|
|
1203
1287
|
merchant: a.merchant,
|
|
1204
|
-
|
|
1288
|
+
// riskLevel is the agent's OWN claim: signing it proves who said it, not that it is true, and nothing checks it unless
|
|
1289
|
+
// the owner configures provenance. Passed through as-is: when the agent does not say, it stays absent and the gate sends
|
|
1290
|
+
// the action for review — never 'low'.
|
|
1291
|
+
context: { tool: 'book-flight', riskLevel: a.riskLevel },
|
|
1205
1292
|
// The authorization is bound to the SAME body bookFlightViaGateway() sends, so the gate records what this agent
|
|
1206
1293
|
// signed and the gateway can prove it is running exactly that. Keep the two in step if you add a field.
|
|
1207
1294
|
payload: { amount: a.amount, merchant: a.merchant, currency: a.currency ?? '${currency}' },
|
|
@@ -1324,7 +1411,7 @@ console.log('');
|
|
|
1324
1411
|
console.log(bold(' With MetaMynd, you can\\'t be bypassed.') + ' ./gateway is why - it independently');
|
|
1325
1412
|
console.log(dim(' re-verified step 1 before running it, and holds the tool this file never can.'));
|
|
1326
1413
|
console.log('');
|
|
1327
|
-
console.log(' Change the cap in the dashboard (
|
|
1414
|
+
console.log(' Change the cap in the dashboard (${SOPS_PATH_ASCII}) and run again.');
|
|
1328
1415
|
console.log(dim(' The outcome changes. This file does not. That is the point.'));
|
|
1329
1416
|
console.log('');
|
|
1330
1417
|
`;
|
|
@@ -1357,7 +1444,7 @@ If a committed \`package-lock.json\` pins an older \`${GUARD_PKG}\` than this pr
|
|
|
1357
1444
|
* with `gatewayPort` the tool lives in a separate process (./gateway, the real enforcement
|
|
1358
1445
|
* boundary); without it (--no-gateway) the tool is a local function in this process.
|
|
1359
1446
|
*/
|
|
1360
|
-
function exampleIndexNeutral({ scope, gatewayPort, demo, merchant }) {
|
|
1447
|
+
function exampleIndexNeutral({ scope, gatewayPort, demo, merchant, sandbox = false }) {
|
|
1361
1448
|
const withGateway = gatewayPort != null;
|
|
1362
1449
|
const steps = [
|
|
1363
1450
|
...demo.cases,
|
|
@@ -1504,8 +1591,12 @@ console.log(bold(' Without MetaMynd, you can be bypassed.') + ' performAction()
|
|
|
1504
1591
|
console.log(dim(' call it directly instead of gatedAction and nothing above stops you.'));
|
|
1505
1592
|
console.log(dim(' Re-scaffold without --no-gateway for the default shape, which closes that.'));`}
|
|
1506
1593
|
console.log('');
|
|
1507
|
-
console.log('
|
|
1508
|
-
console.log(dim('
|
|
1594
|
+
${sandbox ? `console.log(' This is the SHARED sandbox agent: its rules are fixed and nobody can edit them.');
|
|
1595
|
+
console.log(dim(' To change them, provision your own agent: re-run create-metamynd-agent'));
|
|
1596
|
+
console.log(dim(' without --sandbox (it signs you up), then change its rules in your dashboard'));
|
|
1597
|
+
console.log(dim(' (${SOPS_PATH_ASCII}) and run again.'));
|
|
1598
|
+
console.log(dim(' The outcome changes. Your code does not. That is the point.'));` : `console.log(' Change your rules in the dashboard (${SOPS_PATH_ASCII}) and run again.');
|
|
1599
|
+
console.log(dim(' The outcome changes. This file does not. That is the point.'));`}
|
|
1509
1600
|
console.log('');
|
|
1510
1601
|
`;
|
|
1511
1602
|
}
|
|
@@ -1561,7 +1652,7 @@ const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFie
|
|
|
1561
1652
|
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1562
1653
|
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1563
1654
|
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
1564
|
-
const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false
|
|
1655
|
+
const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false, policyPublicKey: '${policyKey}' });
|
|
1565
1656
|
|
|
1566
1657
|
const gateway = createHttpGateway({
|
|
1567
1658
|
guard,
|
|
@@ -1684,7 +1775,15 @@ Replace \`performAction()\` in \`server.mjs\` with your implementation and put a
|
|
|
1684
1775
|
}
|
|
1685
1776
|
|
|
1686
1777
|
/** The README for a non-financial hosted scaffold. `withGateway` selects the two-process shape. */
|
|
1687
|
-
|
|
1778
|
+
/** 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. */
|
|
1779
|
+
function changeRulesSection(sandbox) {
|
|
1780
|
+
return sandbox
|
|
1781
|
+
? `${SANDBOX_RULES_FIXED} An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.`
|
|
1782
|
+
: `Edit the agent's SOPs in the dashboard (${SOPS_PATH}). The agent's behaviour changes live —
|
|
1783
|
+
no redeploy. An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.`;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function exampleReadmeNeutral(slug, scope, withGateway, gatewayPort, daemonKey, demo, sandbox = false) {
|
|
1688
1787
|
const configLine = daemonKey
|
|
1689
1788
|
? `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
|
|
1690
1789
|
**Holds no secret key.** Signing goes through your already-running agentsafe-signer daemon instead.`
|
|
@@ -1763,8 +1862,7 @@ ${clonedFreshSection(daemonKey)}
|
|
|
1763
1862
|
|
|
1764
1863
|
## Change the rules
|
|
1765
1864
|
|
|
1766
|
-
|
|
1767
|
-
no redeploy. An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.
|
|
1865
|
+
${changeRulesSection(sandbox)}
|
|
1768
1866
|
|
|
1769
1867
|
## What this is not
|
|
1770
1868
|
|
|
@@ -1801,7 +1899,7 @@ function examplePackageJson(slug, neutral = false) {
|
|
|
1801
1899
|
) + '\n';
|
|
1802
1900
|
}
|
|
1803
1901
|
|
|
1804
|
-
function exampleReadme(slug, scope, withGateway, gatewayPort, daemonKey = false) {
|
|
1902
|
+
function exampleReadme(slug, scope, withGateway, gatewayPort, daemonKey = false, sandbox = false) {
|
|
1805
1903
|
const configFileLine = daemonKey
|
|
1806
1904
|
? `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
|
|
1807
1905
|
**Holds no secret key.** Signing goes through your already-running agentsafe-signer daemon
|
|
@@ -1828,6 +1926,8 @@ You should see an ALLOW (fulfilled by \`./gateway\`), a BLOCK (over the per-tran
|
|
|
1828
1926
|
an ESCALATE (high risk). The BLOCK and ESCALATE never reach the gateway at all — this file's own
|
|
1829
1927
|
\`guard.guardTool()\` refuses them first. Only the ALLOW crosses into the other process.
|
|
1830
1928
|
|
|
1929
|
+
${RISK_LEVEL_NOTE}
|
|
1930
|
+
|
|
1831
1931
|
## Files
|
|
1832
1932
|
|
|
1833
1933
|
${configFileLine}
|
|
@@ -1849,6 +1949,8 @@ npm start
|
|
|
1849
1949
|
|
|
1850
1950
|
You should see an ALLOW, a BLOCK (over the per-transaction cap), and an ESCALATE (high risk).
|
|
1851
1951
|
|
|
1952
|
+
${RISK_LEVEL_NOTE}
|
|
1953
|
+
|
|
1852
1954
|
## Files
|
|
1853
1955
|
|
|
1854
1956
|
${configFileLine}
|
|
@@ -1891,8 +1993,7 @@ ${clonedFreshSection(daemonKey)}
|
|
|
1891
1993
|
|
|
1892
1994
|
## Change the rules
|
|
1893
1995
|
|
|
1894
|
-
|
|
1895
|
-
no redeploy. An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.
|
|
1996
|
+
${changeRulesSection(sandbox)}
|
|
1896
1997
|
|
|
1897
1998
|
Full integration guide: \`docs/integration/INTEGRATE-WITH-METAMYND.md\`.
|
|
1898
1999
|
`;
|
|
@@ -1966,7 +2067,7 @@ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', impo
|
|
|
1966
2067
|
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1967
2068
|
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1968
2069
|
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
1969
|
-
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true
|
|
2070
|
+
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true, policyPublicKey: '${policyKey}' });
|
|
1970
2071
|
|
|
1971
2072
|
const gateway = createHttpGateway({
|
|
1972
2073
|
guard,
|
|
@@ -2233,10 +2334,10 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2233
2334
|
outDir,
|
|
2234
2335
|
'index.mjs',
|
|
2235
2336
|
neutral
|
|
2236
|
-
? exampleIndexNeutral({ scope, gatewayPort: withGateway ? gatewayPort : null, demo, merchant })
|
|
2337
|
+
? exampleIndexNeutral({ scope, gatewayPort: withGateway ? gatewayPort : null, demo, merchant, sandbox: !!sandbox })
|
|
2237
2338
|
: withGateway
|
|
2238
2339
|
? exampleIndex(scope, perTxnMax, gatewayPort, currency, paymentMerchant)
|
|
2239
|
-
: exampleIndexNoGateway(scope, perTxnMax, currency, paymentMerchant),
|
|
2340
|
+
: exampleIndexNoGateway(scope, perTxnMax, currency, paymentMerchant, !!sandbox),
|
|
2240
2341
|
force,
|
|
2241
2342
|
);
|
|
2242
2343
|
writeFileSafe(outDir, 'package.json', examplePackageJson(slug, neutral), force);
|
|
@@ -2245,8 +2346,8 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2245
2346
|
outDir,
|
|
2246
2347
|
'README.md',
|
|
2247
2348
|
neutral
|
|
2248
|
-
? exampleReadmeNeutral(slug, scope, withGateway, gatewayPort, config.keyProvider === 'daemon', demo)
|
|
2249
|
-
: exampleReadme(slug, scope, withGateway, gatewayPort, config.keyProvider === 'daemon'),
|
|
2349
|
+
? exampleReadmeNeutral(slug, scope, withGateway, gatewayPort, config.keyProvider === 'daemon', demo, !!sandbox)
|
|
2350
|
+
: exampleReadme(slug, scope, withGateway, gatewayPort, config.keyProvider === 'daemon', !!sandbox),
|
|
2250
2351
|
force,
|
|
2251
2352
|
);
|
|
2252
2353
|
// `npm test` (agentsafe-guard verify --context) needs the request inputs of a compliant request:
|
|
@@ -2262,12 +2363,14 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2262
2363
|
|
|
2263
2364
|
if (withGateway) {
|
|
2264
2365
|
const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
|
|
2265
|
-
// The key that signs every policy bundle (magp.policy-signer.ts)
|
|
2266
|
-
//
|
|
2267
|
-
//
|
|
2268
|
-
//
|
|
2269
|
-
// the generated gateway is unchanged from before rather than baking in nothing and crashing.
|
|
2366
|
+
// The key that signs every policy bundle (magp.policy-signer.ts), baked into the generated gateway so it pins the
|
|
2367
|
+
// bundle rather than trusting whatever the fetch returns. REQUIRED: an unpinned gateway takes an interceptor's
|
|
2368
|
+
// bundle as its rules (2026-10-02 — a non-financial gateway executed an action the agent was never granted). The
|
|
2369
|
+
// interactive flows fill it in with ensurePolicyKey() before calling this, so a missing key here is a caller bug.
|
|
2270
2370
|
const policyKey = config.issuer?.policyKey ?? null;
|
|
2371
|
+
if (!policyKey) {
|
|
2372
|
+
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");
|
|
2373
|
+
}
|
|
2271
2374
|
const gwDir = join(outDir, 'gateway');
|
|
2272
2375
|
if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
|
|
2273
2376
|
if (!neutral) {
|
|
@@ -2315,7 +2418,9 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2315
2418
|
console.log(c.dim(' · BLOCK (the agent asking to raise its OWN limit)\n'));
|
|
2316
2419
|
}
|
|
2317
2420
|
console.log(c.cyan(` npm test`) + c.dim(' → assert it CANNOT exceed its mandate. Put this in CI.\n'));
|
|
2318
|
-
console.log(c.dim(
|
|
2421
|
+
console.log(c.dim(sandbox
|
|
2422
|
+
? ` ${SANDBOX_RULES_FIXED}\n`
|
|
2423
|
+
: ` Change the rules any time in the dashboard (${SOPS_PATH}) — no redeploy.\n`));
|
|
2319
2424
|
}
|
|
2320
2425
|
|
|
2321
2426
|
/** --sandbox: no login, no KYB — fetch the shared sandbox agent config and scaffold. */
|
|
@@ -2330,7 +2435,7 @@ async function runSandbox(args) {
|
|
|
2330
2435
|
const fileConfig = typeof args.config === 'string' ? loadConfigFile(args.config) : null;
|
|
2331
2436
|
if (fileConfig) console.log(` ${c.green('✓')} loaded policy config ${c.dim(args.config)}`);
|
|
2332
2437
|
const { financial } = announceFinancial(args, fileConfig);
|
|
2333
|
-
warnRulesNotApplied(fileConfig, 'the shared sandbox agent');
|
|
2438
|
+
warnRulesNotApplied(fileConfig, 'the shared sandbox agent', { shared: true });
|
|
2334
2439
|
if (!financial && typeof fileConfig?.scope === 'string' && fileConfig.scope !== 'perform-action') {
|
|
2335
2440
|
console.log(` ${c.yellow('!')} the scope "${fileConfig.scope}" in the config file is not used: the shared sandbox agent's scope is "perform-action".`);
|
|
2336
2441
|
}
|
|
@@ -3044,7 +3149,8 @@ async function callGateway(path, action, args) {
|
|
|
3044
3149
|
// The COMPLETE body the tool receives, signed as a payload (MAGP 8.3.9): the eight signed fields cover amount and merchant
|
|
3045
3150
|
// only, and this gateway refuses to run the tool on a body that is not exactly the one signed here.
|
|
3046
3151
|
const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
|
|
3047
|
-
|
|
3152
|
+
// riskLevel is passed through as-is (never defaulted): a missing one must reach the gateway as missing.
|
|
3153
|
+
const signed = await guard.buildSignedRequest({ action, amount: args.amount, currency: args.currency ?? '${currency}', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel }, payload });
|
|
3048
3154
|
const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: JSON.stringify(payload) });
|
|
3049
3155
|
const body = await res.json().catch(() => null);
|
|
3050
3156
|
if (!res.ok) {
|
|
@@ -3069,7 +3175,10 @@ const gatedBookFlight = guard.guardToolLocal(
|
|
|
3069
3175
|
amount: a.amount,
|
|
3070
3176
|
currency: a.currency ?? '${currency}',
|
|
3071
3177
|
merchant: a.merchant,
|
|
3072
|
-
|
|
3178
|
+
// riskLevel is the agent's OWN claim: signing it proves who said it, not that it is true, and nothing checks it unless
|
|
3179
|
+
// the owner configures provenance. Passed through as-is: when the agent does not say, it stays absent and the gate sends
|
|
3180
|
+
// the action for review — never 'low'.
|
|
3181
|
+
context: { tool: 'book-flight', riskLevel: a.riskLevel },
|
|
3073
3182
|
}),
|
|
3074
3183
|
getBundle,
|
|
3075
3184
|
);
|
|
@@ -3423,7 +3532,9 @@ action outside the mandate entirely. Every step says what it expects and flags a
|
|
|
3423
3532
|
: `None of your rules could be staged in the demo (see below), so it shows only a BLOCK for an
|
|
3424
3533
|
action outside the mandate. Your rules are still enforced; see \`metamynd-rules.json\`.`)
|
|
3425
3534
|
: `You should see an ALLOW, a BLOCK (over the per-transaction cap), an ESCALATE (high risk —
|
|
3426
|
-
open the dashboard to approve it), and a BLOCK (an action outside the mandate entirely)
|
|
3535
|
+
open the dashboard to approve it), and a BLOCK (an action outside the mandate entirely).
|
|
3536
|
+
|
|
3537
|
+
${RISK_LEVEL_NOTE}`}
|
|
3427
3538
|
|
|
3428
3539
|
## Files
|
|
3429
3540
|
|
|
@@ -3784,6 +3895,7 @@ async function runClaim(args) {
|
|
|
3784
3895
|
if (args.financial && issuedNonFinancial) {
|
|
3785
3896
|
keepConfigAndStop('--financial was passed, but the approved agent has NO spending authority (the request was non-financial).');
|
|
3786
3897
|
}
|
|
3898
|
+
if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndStop(NO_POLICY_KEY(base));
|
|
3787
3899
|
const slug = slugify(state.name || 'metamynd-agent');
|
|
3788
3900
|
const outDir = resolve(String(args.out || `./${slug}`));
|
|
3789
3901
|
if (issuedNonFinancial) {
|
|
@@ -4019,6 +4131,7 @@ async function main() {
|
|
|
4019
4131
|
console.log(` ${c.green('✓')} derived ${demo.cases.length} demo case(s) from your rules`);
|
|
4020
4132
|
for (const n of demo.notDemonstrated) console.log(` ${c.yellow('!')} not staged in the demo: ${n.rule} ${c.dim('— ' + n.why + '; still enforced')}`);
|
|
4021
4133
|
}
|
|
4134
|
+
if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndFail(outDir, config, NO_POLICY_KEY(base));
|
|
4022
4135
|
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 });
|
|
4023
4136
|
}
|
|
4024
4137
|
|
|
@@ -4028,6 +4141,7 @@ export {
|
|
|
4028
4141
|
buildPolicyCases, resolveFinancial, policyMolecules, ruleToMolecule,
|
|
4029
4142
|
generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
|
|
4030
4143
|
scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
|
|
4144
|
+
ensurePolicyKey,
|
|
4031
4145
|
};
|
|
4032
4146
|
if (!process.env.CREATE_METAMYND_AGENT_NO_MAIN) {
|
|
4033
4147
|
// fail() can also be reached from a callback outside main()'s promise chain (readline, child process).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.6",
|
|
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": {
|