create-metamynd-agent 0.10.10 → 0.10.16
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 +10 -0
- package/index.mjs +82 -36
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -388,6 +388,16 @@ polls until the owner approves, then scaffolds the project (the same default two
|
|
|
388
388
|
the full flow above — `--no-gateway`/`--gateway-port` work here too). With `--byok` the keypair is
|
|
389
389
|
generated locally and control is proven on claim — MetaMynd never sees the private key.
|
|
390
390
|
|
|
391
|
+
## Risk: say it, or have your owner set it
|
|
392
|
+
|
|
393
|
+
The starter rules include a high-risk review. A request that sends **no** `riskLevel` (or an unrecognised one)
|
|
394
|
+
is no longer waved through: it is **escalated** (`CONTEXT_UNVERIFIABLE`) for a human, because an agent that omits
|
|
395
|
+
its risk is indistinguishable from one hiding it. The scaffolded financial examples send `riskLevel` (`'low'`
|
|
396
|
+
unless you say otherwise) — that default is a placeholder, not an assessment — and the neutral template passes
|
|
397
|
+
your own `args` as the context, so include it there. A real integration should send an honest one — or, better, not depend on the agent at all: a mandate
|
|
398
|
+
permission can carry an owner-set `riskTier` that no claim can lower, and a gateway route can derive the risk
|
|
399
|
+
itself with `route.trustedContext` (`@metamynd/agentsafe-http-gateway` 0.10.0). See MAGP §6.3.
|
|
400
|
+
|
|
391
401
|
## Security
|
|
392
402
|
|
|
393
403
|
`agent.metamynd.json` contains the agent's **secret key** (a managed key, or — with `--byok` — the one
|
package/index.mjs
CHANGED
|
@@ -53,7 +53,7 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
|
|
|
53
53
|
// requires request inputs blocks a bare baseline request).
|
|
54
54
|
// 0.12.4 makes `verify` probe in the currency the mandate's caps name (it assumed USD), which a scaffolded
|
|
55
55
|
// non-USD agent's `npm test` needs.
|
|
56
|
-
const GUARD_VERSION = '^0.
|
|
56
|
+
const GUARD_VERSION = '^0.14.0';
|
|
57
57
|
/** The harness entry point's config load, shared by both harness templates: a fresh clone has no
|
|
58
58
|
* agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
|
|
59
59
|
function harnessConfigLoad() {
|
|
@@ -102,7 +102,7 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
|
102
102
|
// 0.6.0 brings buildAuthMessage's `resource` field and buildLocalDecisionMessage into this
|
|
103
103
|
// package's own bundled policy-core.mjs (alongside the guard's own 0.10.0) — no scaffolded
|
|
104
104
|
// template code changes, but the floor must still cover the real current version.
|
|
105
|
-
const MCP_GUARD_VERSION = '^0.
|
|
105
|
+
const MCP_GUARD_VERSION = '^0.11.0';
|
|
106
106
|
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
107
107
|
// 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
|
|
108
108
|
// never scaffold a range that could resolve below it.
|
|
@@ -114,7 +114,7 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
|
114
114
|
// 0.5.0 adds the OPTIONAL Credential Vault `resolveCredential` hook on createHttpGateway (Module
|
|
115
115
|
// G) — additive and backward-compatible (every existing consumer sees zero behavior change), but
|
|
116
116
|
// the floor must still cover the real current version per this repo's own package-version check.
|
|
117
|
-
const GATEWAY_VERSION = '^0.
|
|
117
|
+
const GATEWAY_VERSION = '^0.11.0';
|
|
118
118
|
const DEFAULT_API = 'https://metamynd.ai/api/v1';
|
|
119
119
|
const DEFAULT_GATEWAY_PORT = 4401; // distinct from --harness's dashboard (4400)
|
|
120
120
|
|
|
@@ -998,7 +998,7 @@ async function bookFlightViaGateway(args, decision) {
|
|
|
998
998
|
const res = await fetch(GATEWAY + '/book-flight', {
|
|
999
999
|
method: 'POST',
|
|
1000
1000
|
headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
|
|
1001
|
-
body: JSON.stringify(args),
|
|
1001
|
+
body: JSON.stringify({ amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' }),
|
|
1002
1002
|
});
|
|
1003
1003
|
const body = await res.json().catch(() => null);
|
|
1004
1004
|
if (!res.ok) {
|
|
@@ -1207,7 +1207,7 @@ async function performViaGateway(args, decision) {
|
|
|
1207
1207
|
const res = await fetch(GATEWAY + '/perform', {
|
|
1208
1208
|
method: 'POST',
|
|
1209
1209
|
headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
|
|
1210
|
-
body:
|
|
1210
|
+
body: '{}', // this tool reads nothing from the body; the request's fields travel in the SIGNED context
|
|
1211
1211
|
});
|
|
1212
1212
|
const body = await res.json().catch(() => null);
|
|
1213
1213
|
if (!res.ok) {
|
|
@@ -1357,7 +1357,11 @@ async function performAction(args) {
|
|
|
1357
1357
|
//
|
|
1358
1358
|
// valueFields: [] because this action carries no amount or merchant to bind the body to — say so
|
|
1359
1359
|
// explicitly rather than lean on the library default, which would demand both.
|
|
1360
|
-
|
|
1360
|
+
//
|
|
1361
|
+
// allowedFields: [] because performAction() reads nothing from the body, so any key the agent adds is
|
|
1362
|
+
// refused (PAYLOAD_UNBINDABLE): nothing signed covers it. When your real tool reads body fields, list
|
|
1363
|
+
// exactly those keys here — the gateway refuses every top-level key you do not name.
|
|
1364
|
+
const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFields: [], allowedFields: [] }];
|
|
1361
1365
|
|
|
1362
1366
|
// requireAuthorization is OFF on purpose. It makes the gateway claim a single-use, stateful
|
|
1363
1367
|
// authorization before running the tool — but the agent's guard only seals one for a value-bearing
|
|
@@ -1739,7 +1743,13 @@ async function bookFlight(args) {
|
|
|
1739
1743
|
// this exact list anyway): a route with a real amount/merchant should always say so itself,
|
|
1740
1744
|
// rather than relying on a library default to guess right. A route with NO value concept at
|
|
1741
1745
|
// all (a read, a status check) should set valueFields: [] instead — see the gateway's README.
|
|
1742
|
-
|
|
1746
|
+
//
|
|
1747
|
+
// allowedFields is the COMPLETE list of top-level body keys bookFlight() reads. The gateway refuses
|
|
1748
|
+
// every other key (PAYLOAD_UNBINDABLE) because nothing the agent signed covers it — that is what
|
|
1749
|
+
// stops a request signed for $250 from carrying \`surcharge: 4750\` through to your tool. Add a key
|
|
1750
|
+
// here only when your real tool reads it; things policy needs to see (a risk level) travel in the
|
|
1751
|
+
// signed context, not the tool body.
|
|
1752
|
+
const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}', valueFields: ['amount', 'merchant'], allowedFields: ['amount', 'currency', 'merchant'] }];
|
|
1743
1753
|
|
|
1744
1754
|
// No serviceKey: this minimal gateway only calls verifyRequest() (re-check a signed request),
|
|
1745
1755
|
// not the mutual-handshake methods, which are the only thing that needs it.
|
|
@@ -1899,6 +1909,15 @@ own code, or a network attacker) might attempt:
|
|
|
1899
1909
|
already checked it against the mandate's TOTAL budget when it was minted — not just this one
|
|
1900
1910
|
request's amount. Many small legal-looking calls can't add up past the mandate cap this way,
|
|
1901
1911
|
because each needed its own real authorization first.
|
|
1912
|
+
- **Settlement (\`@metamynd/agentsafe-http-gateway\` ≥ 0.6.0).** A claimed hold stays against the
|
|
1913
|
+
mandate's cap until it is settled — it does NOT lapse after 15 minutes. So this gateway closes the
|
|
1914
|
+
hold it claimed once the upstream answers: a 2xx is captured at the authorized amount, and anything
|
|
1915
|
+
else (a rejection, a 5xx, a dropped connection) is parked as UNKNOWN so the spend stays committed.
|
|
1916
|
+
A rejected call therefore keeps its budget until it is reconciled; if a given upstream status
|
|
1917
|
+
guarantees nothing was executed, list it in \`releaseOnStatus\` on the route (e.g. \`[400, 422]\`) and
|
|
1918
|
+
the gateway will release that hold. Only the gateway that claimed the hold can do either — the agent
|
|
1919
|
+
cannot capture it lower or void it, which is what stopped it recovering the budget of a purchase it
|
|
1920
|
+
had just had executed.
|
|
1902
1921
|
- **Amount unknown.** \`amount-unknown\` (\`@metamynd/agentsafe-mcp-guard\` ≥ 0.3.0) blocks a
|
|
1903
1922
|
platform tool by default when its raw bytes or a nested payload hide the amount from a naive
|
|
1904
1923
|
spend cap — AND this agent's OWN starter SOP (see \`agent.metamynd.json\` /
|
|
@@ -2211,11 +2230,18 @@ function harnessGatewayServerFile(scope, gatewayPort, agentDid, neutral = false)
|
|
|
2211
2230
|
// harness-gateway.mjs — a SEPARATE process from your agent. It holds the tool (${neutral ? 'the action below' : 'bookFlight below'}
|
|
2212
2231
|
// never runs anywhere else) and independently re-verifies every request against
|
|
2213
2232
|
// ../metamynd-rules.json using the REAL @metamynd/agentsafe-mcp-guard — the same package a
|
|
2214
|
-
// production Service uses, just pointed at a local file instead of a hosted issuer.
|
|
2215
|
-
//
|
|
2233
|
+
// production Service uses, just pointed at a local file instead of a hosted issuer.
|
|
2234
|
+
//
|
|
2235
|
+
// There is NO enforcement logic of your own in this file. Routing, payload binding and the
|
|
2236
|
+
// deny-by-default posture come from @metamynd/agentsafe-http-gateway — the same component the
|
|
2237
|
+
// hosted scaffold uses — so what it refuses is what production refuses: a body that does not match
|
|
2238
|
+
// what the agent signed (PAYLOAD_NOT_BOUND), a value it cannot find (PAYLOAD_UNBINDABLE), and any
|
|
2239
|
+
// top-level key you did not list in allowedFields. See ../README.md#--gateway for what this closes.
|
|
2216
2240
|
import { readFileSync } from 'node:fs';
|
|
2217
2241
|
import http from 'node:http';
|
|
2218
2242
|
import { createMcpGuard } from '${MCP_GUARD_PKG}';
|
|
2243
|
+
import { createHttpGateway } from '${GATEWAY_PKG}';
|
|
2244
|
+
import { matchRoute } from '${GATEWAY_PKG}/route-match';
|
|
2219
2245
|
|
|
2220
2246
|
const PORT = Number(process.env.PORT || ${gatewayPort});
|
|
2221
2247
|
// The agent's did:key, fixed at scaffold time — a request claiming to be any OTHER agentDid
|
|
@@ -2249,12 +2275,31 @@ const guard = createMcpGuard({
|
|
|
2249
2275
|
},
|
|
2250
2276
|
});
|
|
2251
2277
|
|
|
2252
|
-
// One protected route per gated action in index.mjs.
|
|
2253
|
-
//
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2278
|
+
// One protected route per gated action in index.mjs. This gateway IS the tool, not a proxy in
|
|
2279
|
+
// front of one, so a path with no route below is refused (denyByDefault) — nothing to fall through TO.
|
|
2280
|
+
//
|
|
2281
|
+
// valueFields the fields the SIGNATURE covers that this route's body must carry and match
|
|
2282
|
+
// allowedFields the COMPLETE list of top-level body keys the tool reads. Anything else is refused,
|
|
2283
|
+
// because nothing signed covers it. Add a key here only when your tool reads it.
|
|
2284
|
+
const ROUTES = [
|
|
2285
|
+
${neutral
|
|
2286
|
+
? `{ method: 'POST', path: '/perform', action: '${scope}', valueFields: [], allowedFields: [], run: async () => ({ done: true, action: '${scope}' }) },`
|
|
2287
|
+
: `{ method: 'POST', path: '/book-flight', action: '${scope}', valueFields: ['amount', 'merchant'], allowedFields: ['amount', 'currency', 'merchant'], run: async (args) => ({ pnr: 'PNR-DEMO', ...args }) },`}
|
|
2288
|
+
{ method: 'POST', path: '/raise-limit', action: 'permissions.update', valueFields: [], allowedFields: ['amount', 'currency', 'merchant'], run: async (args) => ({ updated: true, ...args }) },
|
|
2289
|
+
];
|
|
2290
|
+
|
|
2291
|
+
const gateway = createHttpGateway({
|
|
2292
|
+
guard,
|
|
2293
|
+
routes: ROUTES,
|
|
2294
|
+
denyByDefault: true,
|
|
2295
|
+
// Reached ONLY after the guard allowed the request AND the body was bound to what was signed.
|
|
2296
|
+
forward: async (req) => {
|
|
2297
|
+
const route = matchRoute(ROUTES, req.method, req.path);
|
|
2298
|
+
let args = {};
|
|
2299
|
+
try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
|
|
2300
|
+
return { status: 200, body: await route.run(args) };
|
|
2301
|
+
},
|
|
2302
|
+
});
|
|
2258
2303
|
|
|
2259
2304
|
function readBody(req) {
|
|
2260
2305
|
return new Promise((resolve, reject) => {
|
|
@@ -2266,26 +2311,19 @@ function readBody(req) {
|
|
|
2266
2311
|
}
|
|
2267
2312
|
|
|
2268
2313
|
const server = http.createServer(async (req, res) => {
|
|
2269
|
-
|
|
2270
|
-
|
|
2314
|
+
try {
|
|
2315
|
+
const rawBody = await readBody(req);
|
|
2316
|
+
const result = await gateway({ method: req.method, path: req.url, headers: req.headers, rawBody });
|
|
2271
2317
|
const headers = { 'content-type': 'application/json' };
|
|
2318
|
+
const decision = result.governance?.decision;
|
|
2272
2319
|
if (decision) headers['x-agentsafe-decision'] = decision;
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
try {
|
|
2278
|
-
const raw = await readBody(req);
|
|
2279
|
-
const { signed, args } = JSON.parse(raw.toString('utf8') || '{}');
|
|
2280
|
-
const verdict = await guard.verifyRequest({ ...signed, action: route.action });
|
|
2281
|
-
if (verdict.decision !== 'allow' && verdict.decision !== 'observe') {
|
|
2282
|
-
console.log('[harness-gateway] ' + verdict.decision.toUpperCase() + ' ' + req.url + ' — ' + verdict.reasonCode + ' (re-evaluated independently, did not trust the agent)');
|
|
2283
|
-
return send(403, verdict, verdict.decision);
|
|
2284
|
-
}
|
|
2285
|
-
console.log('[harness-gateway] ALLOW ' + req.url + ' — running the real tool here, not in the agent process');
|
|
2286
|
-
return send(200, await route.run(args ?? {}), verdict.decision);
|
|
2320
|
+
if (result.status === 200) console.log('[harness-gateway] ALLOW ' + req.url + ' — running the real tool here, not in the agent process');
|
|
2321
|
+
else console.log('[harness-gateway] ' + String(result.body?.decision ?? 'block').toUpperCase() + ' ' + req.url + ' — ' + (result.body?.reasonCode ?? 'refused') + ' (re-evaluated independently, did not trust the agent)');
|
|
2322
|
+
res.writeHead(result.status, headers);
|
|
2323
|
+
res.end(JSON.stringify(result.body ?? {}));
|
|
2287
2324
|
} catch (err) {
|
|
2288
|
-
|
|
2325
|
+
res.writeHead(502, { 'content-type': 'application/json' });
|
|
2326
|
+
res.end(JSON.stringify({ decision: 'block', reasonCode: 'GATEWAY_ERROR', error: String(err?.message ?? err) }));
|
|
2289
2327
|
}
|
|
2290
2328
|
});
|
|
2291
2329
|
|
|
@@ -2298,7 +2336,7 @@ server.listen(PORT, () => {
|
|
|
2298
2336
|
|
|
2299
2337
|
function harnessGatewayPackageJson(slug) {
|
|
2300
2338
|
return JSON.stringify(
|
|
2301
|
-
{ name: slug + '-harness-gateway', version: '0.1.0', private: true, type: 'module', scripts: { start: 'node harness-gateway.mjs' }, dependencies: { [MCP_GUARD_PKG]: MCP_GUARD_VERSION } },
|
|
2339
|
+
{ name: slug + '-harness-gateway', version: '0.1.0', private: true, type: 'module', scripts: { start: 'node harness-gateway.mjs' }, dependencies: { [MCP_GUARD_PKG]: MCP_GUARD_VERSION, [GATEWAY_PKG]: GATEWAY_VERSION } },
|
|
2302
2340
|
null,
|
|
2303
2341
|
2,
|
|
2304
2342
|
) + '\n';
|
|
@@ -2714,7 +2752,7 @@ const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://localhost:${gatewayPo
|
|
|
2714
2752
|
// offline, using this agent's own did:key — the gateway verifies that signature for itself.
|
|
2715
2753
|
async function callGateway(path, action, args) {
|
|
2716
2754
|
const signed = await guard.buildSignedRequest({ action, amount: args.amount, currency: args.currency ?? '${currency}', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' } });
|
|
2717
|
-
const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({
|
|
2755
|
+
const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: JSON.stringify({ amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' }) });
|
|
2718
2756
|
const body = await res.json().catch(() => null);
|
|
2719
2757
|
if (!res.ok) {
|
|
2720
2758
|
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
@@ -2916,7 +2954,7 @@ ${withGateway ? `const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://loca
|
|
|
2916
2954
|
// offline with this agent's own did:key, and the gateway verifies that signature for itself.
|
|
2917
2955
|
async function callGateway(path, action, args) {
|
|
2918
2956
|
const signed = await guard.buildSignedRequest({ action, merchant: MERCHANT, context: args });
|
|
2919
|
-
const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json'
|
|
2957
|
+
const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) }, body: '{}' });
|
|
2920
2958
|
const body = await res.json().catch(() => null);
|
|
2921
2959
|
if (!res.ok) {
|
|
2922
2960
|
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
@@ -3132,7 +3170,15 @@ directly (or skip \`index.mjs\` and hand a forged/altered request straight to
|
|
|
3132
3170
|
\`harness-gateway.mjs\`) — either way, the gateway independently re-verifies the Ed25519
|
|
3133
3171
|
signature and re-evaluates the SAME rules file for itself. There is no raw \`${tool}()\` left
|
|
3134
3172
|
in \`index.mjs\` to call for a shortcut, and a signature over an altered request fails
|
|
3135
|
-
verification regardless of which process sent it
|
|
3173
|
+
verification regardless of which process sent it.
|
|
3174
|
+
|
|
3175
|
+
**Also closed — the executed arguments are bound to what was signed.** \`harness-gateway.mjs\` contains
|
|
3176
|
+
no enforcement logic of its own: routing, payload binding and deny-by-default come from
|
|
3177
|
+
\`@metamynd/agentsafe-http-gateway\`, the same component the hosted scaffold uses. A request signed for
|
|
3178
|
+
one amount and merchant but carrying another in the body is refused (\`PAYLOAD_NOT_BOUND\`) before your tool
|
|
3179
|
+
runs, and so is any top-level body key you did not list in that route's \`allowedFields\`
|
|
3180
|
+
(\`PAYLOAD_UNBINDABLE\`) — nothing signed covers it. When your real tool reads more body fields, list exactly
|
|
3181
|
+
those keys; the gateway refuses the rest.${neutral ? `
|
|
3136
3182
|
|
|
3137
3183
|
**Also NOT closed — rule inputs are not signed.** The request fields your rules read (\`consent\`,
|
|
3138
3184
|
\`piiPresent\`, \`jurisdiction\`, …) travel in the signed request's context, which the signature does
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.16",
|
|
4
4
|
"description": "Scaffold a MetaMynd/AgentSafe-governed AI agent in one command — logs in, provisions the agent (identity + mandate + SOP + Standards) in a single call, writes agent.metamynd.json plus a runnable agent + separate tool-gateway process that closes direct-call, confused-deputy, replay, and cumulative-spend bypasses. --harness scaffolds a free, local, zero-network governance harness instead.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-metamynd-agent": "index.mjs"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "node harness-gateway.smoke.mjs && node policy-demo.smoke.mjs && node hosted-nonfinancial.smoke.mjs && node modes-nonfinancial.smoke.mjs && node byok-daemon.smoke.mjs"
|
|
10
|
+
"test": "node harness-gateway.smoke.mjs && node harness-gateway-bind.smoke.mjs && node policy-demo.smoke.mjs && node hosted-nonfinancial.smoke.mjs && node modes-nonfinancial.smoke.mjs && node byok-daemon.smoke.mjs"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"index.mjs",
|