create-metamynd-agent 0.6.2 → 0.7.1
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 +79 -15
- package/index.mjs +490 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Scaffold a **MetaMynd/AgentSafe-governed** AI agent in one command. It logs you in, provisions the
|
|
4
4
|
agent in a **single call** (identity + mandate + starter SOP + all enforced Standards), writes a
|
|
5
|
-
portable `agent.metamynd.json`, and drops a runnable
|
|
6
|
-
[`@metamynd/agentsafe-guard`](https://www.npmjs.com/package/@metamynd/agentsafe-guard)
|
|
5
|
+
portable `agent.metamynd.json`, and drops a runnable agent that gates a tool through the
|
|
6
|
+
[`@metamynd/agentsafe-guard`](https://www.npmjs.com/package/@metamynd/agentsafe-guard) — **plus, by
|
|
7
|
+
default, a second `gateway/` process** built on
|
|
8
|
+
[`@metamynd/agentsafe-mcp-guard`](https://www.npmjs.com/package/@metamynd/agentsafe-mcp-guard) and
|
|
9
|
+
[`@metamynd/agentsafe-http-gateway`](https://www.npmjs.com/package/@metamynd/agentsafe-http-gateway).
|
|
10
|
+
The agent's own `guardTool()` call is a fast, local, client-side check; the gateway is the real
|
|
11
|
+
enforcement boundary — see [Separate tool gateway](#separate-tool-gateway-default) below.
|
|
7
12
|
|
|
8
13
|
> **Prerequisite:** an agent is always owned by a **KYB-verified owner** — a person/org with a
|
|
9
14
|
> MetaMynd account. If that's you and you're verified, you're ready. Verify once in the dashboard if
|
|
@@ -11,6 +16,10 @@ portable `agent.metamynd.json`, and drops a runnable example that gates a tool t
|
|
|
11
16
|
|
|
12
17
|
## Free local harness (no account, no network, `--harness`)
|
|
13
18
|
|
|
19
|
+
> **Without MetaMynd, you can be bypassed.** `--harness` proves your policy logic works — it does
|
|
20
|
+
> not enforce it against a caller trying to get around it. See [What this is not](#what-this-is-not)
|
|
21
|
+
> below before you rely on it for anything beyond testing rules.
|
|
22
|
+
|
|
14
23
|
```bash
|
|
15
24
|
npm create metamynd-agent@latest -- --harness # or: npx create-metamynd-agent --harness
|
|
16
25
|
```
|
|
@@ -34,6 +43,8 @@ my-agent/
|
|
|
34
43
|
|
|
35
44
|
### What this is not
|
|
36
45
|
|
|
46
|
+
**Without MetaMynd, you can be bypassed.** Everything below is why, precisely.
|
|
47
|
+
|
|
37
48
|
No anchored or cross-party-verifiable identity, no dashboard reachable when your machine is off, no
|
|
38
49
|
owner queue someone *else* can approve from, no anchored evidence, no enforced platform Standards.
|
|
39
50
|
That set of things is the hosted platform — and getting there later is a **config change, not a
|
|
@@ -45,13 +56,13 @@ It is also **not a separate enforcement boundary**, and this matters more than t
|
|
|
45
56
|
`guardToolLocal()` is a cooperative library your own process embeds — call the raw handler directly
|
|
46
57
|
instead of the guarded one and nothing stops you, because there is no second party in the loop to
|
|
47
58
|
disagree with you. Confirmed by direct testing: a bypass attempt (skip the guard, call the tool
|
|
48
|
-
function underneath it) succeeds every time, structurally, not as a bug.
|
|
49
|
-
|
|
50
|
-
authority for itself rather than trusting that the agent's own guard ran
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
function underneath it) succeeds every time, structurally, not as a bug. What actually closes this
|
|
60
|
+
is a **counterparty** — a separate process holding the tool, that independently re-verifies the
|
|
61
|
+
agent's signed authority for itself rather than trusting that the agent's own guard ran. `--harness`
|
|
62
|
+
never has one, by design (there's no second party on one machine with no network). **Just dropping
|
|
63
|
+
`--harness` is not enough on its own to get one either** — see
|
|
64
|
+
[Separate tool gateway](#separate-tool-gateway-default) below for what actually provides it, and
|
|
65
|
+
`--no-gateway`'s own caveat for what happens if you opt out of it.
|
|
55
66
|
|
|
56
67
|
Works with `--config` too — its `rules` become the harness's starter rules file, same as the hosted
|
|
57
68
|
flow. See [Policy config file](#policy-config-file---config) below.
|
|
@@ -74,6 +85,12 @@ call the hosted API (a shared demo identity) — it's a first look at the *hoste
|
|
|
74
85
|
local/offline mode. Great for a first look; use the full flow below when you want your own governed
|
|
75
86
|
agent with your own limits.
|
|
76
87
|
|
|
88
|
+
`--sandbox` always scaffolds the single-process shape (no `gateway/`) — it's a shared identity never
|
|
89
|
+
meant to hold real credentials, so there's nothing here worth a separate enforcement boundary for.
|
|
90
|
+
Note this is a narrower gap than `--harness`'s: `--sandbox` DOES call MetaMynd for the decision, it
|
|
91
|
+
just still has nowhere else for the tool to live, so the same in-process bypass applies regardless.
|
|
92
|
+
The generated project's own README says so. The full flow below is what scaffolds one by default.
|
|
93
|
+
|
|
77
94
|
## Use
|
|
78
95
|
|
|
79
96
|
```bash
|
|
@@ -82,18 +99,32 @@ npm create metamynd-agent@latest
|
|
|
82
99
|
npx create-metamynd-agent
|
|
83
100
|
```
|
|
84
101
|
|
|
85
|
-
Answer a few prompts (API, owner email/password, agent name, scope, per-transaction cap) and you get
|
|
102
|
+
Answer a few prompts (API, owner email/password, agent name, scope, per-transaction cap) and you get
|
|
103
|
+
**two** scaffolded projects — the agent, and its tool gateway:
|
|
86
104
|
|
|
87
105
|
```
|
|
88
106
|
my-agent/
|
|
89
107
|
├─ agent.metamynd.json # portable guard config — HOLDS THE AGENT SECRET KEY (gitignored)
|
|
90
|
-
├─ index.mjs # runnable example:
|
|
108
|
+
├─ index.mjs # runnable example: signs + calls ./gateway; guardTool() here is a
|
|
109
|
+
│ # fast local pre-check, NOT the enforcement boundary
|
|
91
110
|
├─ package.json # depends on @metamynd/agentsafe-guard
|
|
92
111
|
├─ .gitignore
|
|
93
|
-
|
|
112
|
+
├─ README.md
|
|
113
|
+
└─ gateway/ # a SEPARATE process — the real enforcement boundary. Read its
|
|
114
|
+
├─ server.mjs # README first if you only read one.
|
|
115
|
+
├─ package.json # depends on @metamynd/agentsafe-mcp-guard + @metamynd/agentsafe-http-gateway
|
|
116
|
+
├─ .env.example # real tool credentials go here, never in the agent directory
|
|
117
|
+
├─ .gitignore
|
|
118
|
+
└─ README.md
|
|
94
119
|
```
|
|
95
120
|
|
|
96
|
-
Then:
|
|
121
|
+
Then, in **two terminals** — the gateway first:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
cd my-agent/gateway
|
|
125
|
+
npm install
|
|
126
|
+
npm start
|
|
127
|
+
```
|
|
97
128
|
|
|
98
129
|
```bash
|
|
99
130
|
cd my-agent
|
|
@@ -101,6 +132,32 @@ npm install
|
|
|
101
132
|
npm start
|
|
102
133
|
```
|
|
103
134
|
|
|
135
|
+
### Separate tool gateway (default)
|
|
136
|
+
|
|
137
|
+
This is the other half of **without MetaMynd, you can be bypassed**: WITH it — specifically, with
|
|
138
|
+
`gateway/`, the second process this scaffolds by default — you can't be, the same way the hosted
|
|
139
|
+
platform's own MCP counterparty can't be talked around by a compromised agent.
|
|
140
|
+
|
|
141
|
+
`guard.guardTool()` in `index.mjs` still runs — it's a fast, local, client-side pre-check that gives
|
|
142
|
+
good UX (fail fast on an obviously-blocked call, no round trip) — but it is **not** what stops a
|
|
143
|
+
bypass. It still calls its handler in the SAME process regardless of where the decision came from,
|
|
144
|
+
so anything able to call that handler directly gets the same result the gate would have given it.
|
|
145
|
+
|
|
146
|
+
What actually stops a bypass is that `bookFlight()` doesn't exist in the agent's process at all.
|
|
147
|
+
It exists only in `gateway/server.mjs` — a separate process, started separately, holding any real
|
|
148
|
+
tool credentials the agent process never sees — which independently re-verifies every request
|
|
149
|
+
against the agent's own published policy bundle before running it (same shape as the mutual
|
|
150
|
+
counterparty check in [`@metamynd/agentsafe-mcp-guard`](https://www.npmjs.com/package/@metamynd/agentsafe-mcp-guard),
|
|
151
|
+
built with [`@metamynd/agentsafe-http-gateway`](https://www.npmjs.com/package/@metamynd/agentsafe-http-gateway)).
|
|
152
|
+
It's a minimal slice of the same pattern proven end to end in `demo/duffel-mcp-gateway` in the
|
|
153
|
+
AgentSafe repo (mutual handshake, x402 payment binding, capability tokens) — this scaffold gives you
|
|
154
|
+
just the part that closes the bypass, not the whole protocol.
|
|
155
|
+
|
|
156
|
+
Pass `--no-gateway` to opt out and get the old single-process scaffold instead — e.g. if you're
|
|
157
|
+
already running your own separate gateway and don't need this one. **You are back to being
|
|
158
|
+
bypassable if you do**, for the same structural reason `--harness` is; the generated project's own
|
|
159
|
+
README says so plainly.
|
|
160
|
+
|
|
104
161
|
## Non-interactive
|
|
105
162
|
|
|
106
163
|
Every prompt has a flag or environment-variable fallback, so it scripts cleanly in CI:
|
|
@@ -123,6 +180,8 @@ METAMYND_PASSWORD='…' npx create-metamynd-agent --yes …
|
|
|
123
180
|
| `--harness` | — | off (no login/KYB/network at all; free local governance — see above) |
|
|
124
181
|
| `--sandbox` | — | off (skips login/KYB; shared sandbox agent, still hosted) |
|
|
125
182
|
| `--config <file>` | — | a JSON policy file — see [Policy config file](#policy-config-file---config) |
|
|
183
|
+
| `--no-gateway` | — | off — hosted flow only; skips the default separate tool gateway (see above) |
|
|
184
|
+
| `--gateway-port <n>` | — | `4401` — hosted flow only, the gateway process's port |
|
|
126
185
|
| `--port <n>` | — | `4400` — `--harness` only, the local dashboard's port |
|
|
127
186
|
| `--api <url>` | `METAMYND_API` | `https://metamynd.ai/api/v1` |
|
|
128
187
|
| `--email <email>` | `METAMYND_EMAIL` | — (required) |
|
|
@@ -200,14 +259,19 @@ npx create-metamynd-agent --claim --watch
|
|
|
200
259
|
```
|
|
201
260
|
|
|
202
261
|
`--request` submits the request (as your own authed user) and stores the claim token locally; `--claim`
|
|
203
|
-
polls until the owner approves, then scaffolds the project
|
|
204
|
-
|
|
262
|
+
polls until the owner approves, then scaffolds the project (the same default two-process shape as
|
|
263
|
+
the full flow above — `--no-gateway`/`--gateway-port` work here too). With `--byok` the keypair is
|
|
264
|
+
generated locally and control is proven on claim — MetaMynd never sees the private key.
|
|
205
265
|
|
|
206
266
|
## Security
|
|
207
267
|
|
|
208
268
|
`agent.metamynd.json` contains the agent's **secret key** (a managed key, or — with `--byok` — the one
|
|
209
269
|
generated locally). The scaffolded project gitignores it. Never commit it or paste it anywhere public.
|
|
210
270
|
|
|
271
|
+
Any REAL tool credential (an airline API key, a payment key, ...) belongs in `gateway/.env` — never
|
|
272
|
+
in the agent directory. That's the whole point of the default two-process shape: the agent process
|
|
273
|
+
should never be able to hold, or leak, a credential it doesn't have.
|
|
274
|
+
|
|
211
275
|
## Full guide
|
|
212
276
|
|
|
213
277
|
`docs/integration/INTEGRATE-WITH-METAMYND.md` — the complete integration front-door (payments,
|
package/index.mjs
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Logs a KYB-verified owner in, provisions the agent in ONE call
|
|
5
5
|
// (POST /onboarding/agent → identity + mandate + starter SOP + enforced Standards),
|
|
6
|
-
// writes the portable `agent.metamynd.json
|
|
7
|
-
//
|
|
6
|
+
// writes the portable `agent.metamynd.json` and a runnable agent example, PLUS (by default)
|
|
7
|
+
// a separate `gateway/` process — a second, independent guard that re-verifies every request
|
|
8
|
+
// and holds the real tool, so the agent's own guardTool() call is a convenience, not the
|
|
9
|
+
// enforcement boundary. `--no-gateway` skips it (see README#separate-tool-gateway-default).
|
|
8
10
|
//
|
|
9
11
|
// ZERO dependencies: Node ≥ 18 built-ins only (fetch, readline).
|
|
10
12
|
//
|
|
@@ -22,7 +24,13 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
|
|
|
22
24
|
// >=0.4.0 <0.5.0, so leaving this at ^0.4.0 would scaffold an agent whose `npm test` runs
|
|
23
25
|
// `agentsafe-guard verify` against a guard that has no such command.
|
|
24
26
|
const GUARD_VERSION = '^0.5.0';
|
|
27
|
+
// The default hosted scaffold's SECOND process — the tool gateway (see scaffoldProject).
|
|
28
|
+
const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
29
|
+
const MCP_GUARD_VERSION = '^0.1.0';
|
|
30
|
+
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
31
|
+
const GATEWAY_VERSION = '^0.1.0';
|
|
25
32
|
const DEFAULT_API = 'https://metamynd.ai/api/v1';
|
|
33
|
+
const DEFAULT_GATEWAY_PORT = 4401; // distinct from --harness's dashboard (4400)
|
|
26
34
|
|
|
27
35
|
// ---------- tiny ANSI ----------
|
|
28
36
|
const c = {
|
|
@@ -88,6 +96,10 @@ ${c.b('Options')}
|
|
|
88
96
|
(MetaMynd never sees the private key). Overridden by --public-key.
|
|
89
97
|
--public-key <hex> BYOK with a key you already hold (SPKI/raw hex); you prove control yourself
|
|
90
98
|
--out <dir> Output project directory (default ./<agent-slug>)
|
|
99
|
+
--no-gateway Hosted flow only: skip the separate tool-gateway process (see
|
|
100
|
+
README#separate-tool-gateway-default) and scaffold the old
|
|
101
|
+
single-process example instead. Not a separate enforcement boundary.
|
|
102
|
+
--gateway-port <n> Hosted flow only: the gateway process's port (default 4401)
|
|
91
103
|
--port <n> --harness only: the local dashboard's port (default 4400)
|
|
92
104
|
--yes, -y Non-interactive: use flags/env/defaults, never prompt
|
|
93
105
|
-h, --help Show this help
|
|
@@ -99,7 +111,9 @@ ${c.b('Environment')}
|
|
|
99
111
|
${c.b('What it does')}
|
|
100
112
|
1. Logs in as a KYB-verified owner → owner access token
|
|
101
113
|
2. POST /onboarding/agent (one call) → identity + mandate + SOP + Standards
|
|
102
|
-
3. Writes agent.metamynd.json +
|
|
114
|
+
3. Writes agent.metamynd.json + index.mjs, PLUS (by default) a separate gateway/ process —
|
|
115
|
+
the real enforcement boundary, not index.mjs's own guard.guardTool() call. --no-gateway
|
|
116
|
+
skips it.
|
|
103
117
|
`;
|
|
104
118
|
|
|
105
119
|
// ---------- prompts ----------
|
|
@@ -257,7 +271,17 @@ async function apiPost(base, path, body, token) {
|
|
|
257
271
|
}
|
|
258
272
|
|
|
259
273
|
// ---------- scaffolding ----------
|
|
260
|
-
|
|
274
|
+
/**
|
|
275
|
+
* The --no-gateway / --sandbox variant: the tool is a local function in the SAME process as
|
|
276
|
+
* guard.guardTool(). Fine for a demo with nothing real behind it (--sandbox always uses this —
|
|
277
|
+
* it's a shared identity, never meant to hold real credentials). For anything that touches a
|
|
278
|
+
* real credential, guard.guardTool() alone is a client-side convenience, not a boundary: it
|
|
279
|
+
* still calls this handler in-process regardless of where the decision came from, so an agent
|
|
280
|
+
* that skips it and calls bookFlight() directly gets the same result the gate would have given
|
|
281
|
+
* it — the same shape of gap --harness's README documents. See exampleIndex() below, which is
|
|
282
|
+
* what the real (non-sandbox) flow scaffolds by default instead.
|
|
283
|
+
*/
|
|
284
|
+
function exampleIndexNoGateway(scope, perTxnMax) {
|
|
261
285
|
const under = Math.max(1, Math.round(perTxnMax * 0.5));
|
|
262
286
|
const over = Math.round(perTxnMax + 100);
|
|
263
287
|
return `// index.mjs — your agent, governed by MetaMynd/AgentSafe.
|
|
@@ -268,6 +292,11 @@ import { createGuardFromConfig } from '${GUARD_PKG}';
|
|
|
268
292
|
const guard = await createGuardFromConfig('./agent.metamynd.json'); // no env vars
|
|
269
293
|
|
|
270
294
|
// --- Your real tool. Replace the body with your actual implementation. ---
|
|
295
|
+
// --- If that implementation touches a real credential, this in-process call is NOT an
|
|
296
|
+
// --- enforcement boundary: guard.guardTool() below still calls this function directly in
|
|
297
|
+
// --- THIS process regardless of the decision's source, so anything that can call it directly
|
|
298
|
+
// --- gets the same result the gate would have given it. A real (non --sandbox) scaffold
|
|
299
|
+
// --- without --no-gateway moves this behind a separate process instead. See README.
|
|
271
300
|
async function bookFlight(args) {
|
|
272
301
|
return { pnr: 'PNR-DEMO', ...args };
|
|
273
302
|
}
|
|
@@ -391,6 +420,193 @@ console.log(dim(' - the blocked call never reached your tool at all.'));
|
|
|
391
420
|
console.log(dim(' - every decision was recorded as tamper-evident evidence.'));
|
|
392
421
|
console.log(dim(' - if the gate were unreachable the guard fails CLOSED: it blocks.'));
|
|
393
422
|
console.log('');
|
|
423
|
+
console.log(bold(' Without MetaMynd, you can be bypassed.') + ' bookFlight() runs in THIS process -');
|
|
424
|
+
console.log(dim(' call it directly instead of gatedBookFlight and nothing above stops you.'));
|
|
425
|
+
console.log(dim(' Re-scaffold without --sandbox/--no-gateway for the default shape, which does.'));
|
|
426
|
+
console.log('');
|
|
427
|
+
console.log(' Change the cap in the dashboard (Legal Entity -> SOPs) and run again.');
|
|
428
|
+
console.log(dim(' The outcome changes. This file does not. That is the point.'));
|
|
429
|
+
console.log('');
|
|
430
|
+
`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* The DEFAULT hosted scaffold: the tool lives in a separate process (./gateway), not here.
|
|
435
|
+
* guard.guardTool() below is still called — it is a fast, local, client-side pre-check that
|
|
436
|
+
* gives good UX (fail fast, no round trip for an obviously-blocked call) — but it is not what
|
|
437
|
+
* stops a bypass. What stops a bypass is that there is no bookFlight() in THIS process to call
|
|
438
|
+
* directly: it only exists in ./gateway, which independently re-verifies every request against
|
|
439
|
+
* this agent's own policy bundle before it runs, and holds any real credentials the tool needs.
|
|
440
|
+
*/
|
|
441
|
+
function exampleIndex(scope, perTxnMax, gatewayPort) {
|
|
442
|
+
const under = Math.max(1, Math.round(perTxnMax * 0.5));
|
|
443
|
+
const over = Math.round(perTxnMax + 100);
|
|
444
|
+
return `// index.mjs — your agent, governed by MetaMynd/AgentSafe.
|
|
445
|
+
// Every governed tool call is checked TWICE before it runs: once here (fast, local, client-side),
|
|
446
|
+
// and independently again by ./gateway — a SEPARATE process that holds the real tool and its
|
|
447
|
+
// credentials. That second check is the actual enforcement boundary; see ./gateway/README.md.
|
|
448
|
+
import { createGuardFromConfig } from '${GUARD_PKG}';
|
|
449
|
+
|
|
450
|
+
// Loads agent.metamynd.json: the agent's DID, its signing key, and the gate to call.
|
|
451
|
+
const guard = await createGuardFromConfig('./agent.metamynd.json'); // no env vars
|
|
452
|
+
|
|
453
|
+
const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
|
|
454
|
+
|
|
455
|
+
// --- Calls the gateway process instead of a local function. There is no raw bookFlight() in
|
|
456
|
+
// --- this file to call directly — the tool, and any real credentials it needs, live only in
|
|
457
|
+
// --- ./gateway, which independently re-verifies this signed request itself.
|
|
458
|
+
async function bookFlightViaGateway(args) {
|
|
459
|
+
const signed = guard.buildSignedRequest({
|
|
460
|
+
action: '${scope}',
|
|
461
|
+
amount: args.amount,
|
|
462
|
+
currency: 'USD',
|
|
463
|
+
merchant: args.merchant,
|
|
464
|
+
context: { tool: 'book-flight', riskLevel: args.riskLevel ?? 'low' },
|
|
465
|
+
});
|
|
466
|
+
const res = await fetch(GATEWAY + '/book-flight', {
|
|
467
|
+
method: 'POST',
|
|
468
|
+
headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
|
|
469
|
+
body: JSON.stringify(args),
|
|
470
|
+
});
|
|
471
|
+
const body = await res.json().catch(() => null);
|
|
472
|
+
if (!res.ok) {
|
|
473
|
+
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
474
|
+
err.name = 'GovernanceBlocked';
|
|
475
|
+
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
|
|
476
|
+
throw err;
|
|
477
|
+
}
|
|
478
|
+
return body;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// --- The GATED version. Register THIS with your agent instead of calling the gateway directly.
|
|
482
|
+
// --- This local check and the gateway's own re-check are independent; neither trusts the other.
|
|
483
|
+
const gatedBookFlight = guard.guardTool(
|
|
484
|
+
'${scope}', // = your mandate scope
|
|
485
|
+
bookFlightViaGateway,
|
|
486
|
+
(a) => ({ // map tool args → gate inputs
|
|
487
|
+
amount: a.amount,
|
|
488
|
+
currency: 'USD',
|
|
489
|
+
merchant: a.merchant,
|
|
490
|
+
context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
|
|
491
|
+
}),
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
// --- A tool the agent was NEVER granted. Wrapping it is the demonstration: there is no
|
|
495
|
+
// --- rule anywhere forbidding this. The mandate simply never mentioned the action.
|
|
496
|
+
async function raiseOwnLimit(args) {
|
|
497
|
+
return { updated: true, ...args }; // never runs, and that is the point
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const gatedRaiseOwnLimit = guard.guardTool(
|
|
501
|
+
'permissions.update', // an action NOT in the mandate
|
|
502
|
+
raiseOwnLimit,
|
|
503
|
+
(a) => ({
|
|
504
|
+
amount: a.amount,
|
|
505
|
+
currency: 'USD',
|
|
506
|
+
merchant: a.merchant,
|
|
507
|
+
context: { tool: 'permissions-update' },
|
|
508
|
+
}),
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
const dim = (t) => '\\x1b[2m' + t + '\\x1b[0m';
|
|
512
|
+
const bold = (t) => '\\x1b[1m' + t + '\\x1b[0m';
|
|
513
|
+
const rule = (n) => ' ' + '-'.repeat(n);
|
|
514
|
+
|
|
515
|
+
// Plain-English meaning for the reason codes this demo can produce. The gateway re-evaluates
|
|
516
|
+
// the SAME policy bundle with the SAME evaluator the gate uses, so it produces these same codes.
|
|
517
|
+
const WHY = {
|
|
518
|
+
AUTHORIZED: 'inside the mandate and under the SOP spend cap',
|
|
519
|
+
SOP_SPEND_CAP: 'your SOP caps a single transaction at $${perTxnMax}',
|
|
520
|
+
RISK_REVIEW: 'your SOP sends high-risk actions to a human first',
|
|
521
|
+
MERCHANT_NOT_ALLOWED: 'the mandate lists which merchants this agent may pay',
|
|
522
|
+
// Both say the same thing from where you are standing: the mandate does not cover that
|
|
523
|
+
// action. Which one you see depends on whether the verdict was reached here or at the
|
|
524
|
+
// gate, and neither of them depends on the amount.
|
|
525
|
+
NO_PERMISSION_FOR_ACTION: 'the mandate never granted this action - at any amount',
|
|
526
|
+
NO_MANDATE: 'there is no mandate for this action at all',
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
// ---------------------------------------------------------------- 1. CONTEXT
|
|
530
|
+
console.log('');
|
|
531
|
+
console.log(bold(' What this simulation shows'));
|
|
532
|
+
console.log('');
|
|
533
|
+
console.log(' An agent should not be the thing that decides what it is allowed to do — and');
|
|
534
|
+
console.log(' it should not be the thing that RUNS what it decided, either. This run makes');
|
|
535
|
+
console.log(' both concrete. Three attempts take the SAME code path and produce three');
|
|
536
|
+
console.log(' different outcomes. The fourth asks for something the agent was never granted');
|
|
537
|
+
console.log(' at all - and that is the one a prompt could not have stopped, because the');
|
|
538
|
+
console.log(' decision is not made inside your program, and the tool is not either.');
|
|
539
|
+
|
|
540
|
+
// ---------------------------------------------------------------- 2. MECHANISM
|
|
541
|
+
console.log('');
|
|
542
|
+
console.log(bold(' How it does that'));
|
|
543
|
+
console.log('');
|
|
544
|
+
console.log(dim(' 1. this project holds an agent identity (a DID) and its signing key'));
|
|
545
|
+
console.log(dim(' 2. that agent has a mandate - a scope it may act in, and a spend cap'));
|
|
546
|
+
console.log(dim(' 3. guardTool() wraps your tool call, giving a fast local pre-check'));
|
|
547
|
+
console.log(dim(' 4. each attempt is ALSO signed and sent to ./gateway - a separate process'));
|
|
548
|
+
console.log(dim(' 5. the gateway independently re-verifies before your tool runs there'));
|
|
549
|
+
console.log(dim(' 6. there is no local bookFlight() to call directly - only the gateway has it'));
|
|
550
|
+
console.log('');
|
|
551
|
+
console.log(dim(' scope ${scope}'));
|
|
552
|
+
console.log(dim(' cap $${perTxnMax} per transaction, set by your SOP'));
|
|
553
|
+
console.log(dim(' gateway ' + GATEWAY + ' (run it in a separate terminal - see ./gateway)'));
|
|
554
|
+
|
|
555
|
+
// ---------------------------------------------------------------- 3. THE STEPS
|
|
556
|
+
async function attempt(n, intent, args, tool = gatedBookFlight) {
|
|
557
|
+
console.log('');
|
|
558
|
+
console.log(bold(' Step ' + n + ' of 4') + ' - ' + intent);
|
|
559
|
+
console.log(dim(' signing the request locally, then asking the gate to decide...'));
|
|
560
|
+
try {
|
|
561
|
+
const r = await tool(args);
|
|
562
|
+
console.log('\\x1b[32m ALLOWED\\x1b[0m your tool ran (in ./gateway) and returned ' + (r.pnr ?? 'ok'));
|
|
563
|
+
console.log(dim(' ' + WHY.AUTHORIZED));
|
|
564
|
+
} catch (e) {
|
|
565
|
+
const g = e.governance ?? {};
|
|
566
|
+
const why = WHY[g.reasonCode] ?? e.message;
|
|
567
|
+
if (g.decision === 'escalate') {
|
|
568
|
+
console.log('\\x1b[33m ESCALATED\\x1b[0m held for a human - ' + g.reasonCode);
|
|
569
|
+
console.log(dim(' ' + why));
|
|
570
|
+
console.log(dim(' not a failure: approve it in the dashboard and the action resumes.'));
|
|
571
|
+
} else {
|
|
572
|
+
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
|
|
573
|
+
console.log(dim(' ' + why));
|
|
574
|
+
console.log(dim(' your tool never ran - refused before execution.'));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
console.log('');
|
|
580
|
+
console.log(rule(66));
|
|
581
|
+
await attempt(1, 'a $${under} booking, low risk. Expected to pass.', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'low' });
|
|
582
|
+
await attempt(2, 'a $${over} booking, deliberately over the cap.', { amount: ${over}, merchant: 'skyward-air', riskLevel: 'low' });
|
|
583
|
+
await attempt(3, 'a $${under} booking, but flagged high risk.', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'high' });
|
|
584
|
+
await attempt(
|
|
585
|
+
4,
|
|
586
|
+
'the agent stops booking flights and asks to raise its OWN limit.',
|
|
587
|
+
{ amount: 100000, merchant: 'skyward-air' },
|
|
588
|
+
gatedRaiseOwnLimit,
|
|
589
|
+
);
|
|
590
|
+
console.log('');
|
|
591
|
+
console.log(rule(66));
|
|
592
|
+
|
|
593
|
+
// ---------------------------------------------------------------- 4. RESULT
|
|
594
|
+
console.log('');
|
|
595
|
+
console.log(bold(' What this proved'));
|
|
596
|
+
console.log('');
|
|
597
|
+
console.log(dim(' - one code path, three outcomes. The rules decided, not this file'));
|
|
598
|
+
console.log(dim(' and not the model driving it.'));
|
|
599
|
+
console.log(dim(' - step 1 ran in ./gateway, a process this file cannot reach into. There'));
|
|
600
|
+
console.log(dim(' is no rawBookFlight() here to call instead - that is what actually'));
|
|
601
|
+
console.log(dim(' stops a bypass, not the guardTool() call above it.'));
|
|
602
|
+
console.log(dim(' - step 4 needed no rule to stop it. The agent could not widen its own'));
|
|
603
|
+
console.log(dim(' authority, because it cannot name an action nobody delegated to it.'));
|
|
604
|
+
console.log(dim(' - every blocked/escalated call never reached a real tool at all.'));
|
|
605
|
+
console.log(dim(' - if the gate were unreachable the guard fails CLOSED: it blocks.'));
|
|
606
|
+
console.log('');
|
|
607
|
+
console.log(bold(' With MetaMynd, you can\\'t be bypassed.') + ' ./gateway is why - it independently');
|
|
608
|
+
console.log(dim(' re-verified step 1 before running it, and holds the tool this file never can.'));
|
|
609
|
+
console.log('');
|
|
394
610
|
console.log(' Change the cap in the dashboard (Legal Entity -> SOPs) and run again.');
|
|
395
611
|
console.log(dim(' The outcome changes. This file does not. That is the point.'));
|
|
396
612
|
console.log('');
|
|
@@ -415,12 +631,41 @@ function examplePackageJson(slug) {
|
|
|
415
631
|
) + '\n';
|
|
416
632
|
}
|
|
417
633
|
|
|
418
|
-
function exampleReadme(slug, scope) {
|
|
419
|
-
|
|
634
|
+
function exampleReadme(slug, scope, withGateway, gatewayPort) {
|
|
635
|
+
const gatewaySection = withGateway
|
|
636
|
+
? `## Run
|
|
420
637
|
|
|
421
|
-
|
|
638
|
+
Two processes — start the gateway first, in its own terminal:
|
|
422
639
|
|
|
423
|
-
|
|
640
|
+
\`\`\`bash
|
|
641
|
+
cd gateway && npm install && npm start # the REAL enforcement boundary — see gateway/README.md
|
|
642
|
+
\`\`\`
|
|
643
|
+
|
|
644
|
+
Then, in this directory:
|
|
645
|
+
|
|
646
|
+
\`\`\`bash
|
|
647
|
+
npm install
|
|
648
|
+
npm start
|
|
649
|
+
\`\`\`
|
|
650
|
+
|
|
651
|
+
You should see an ALLOW (fulfilled by \`./gateway\`), a BLOCK (over the per-transaction cap), and
|
|
652
|
+
an ESCALATE (high risk). The BLOCK and ESCALATE never reach the gateway at all — this file's own
|
|
653
|
+
\`guard.guardTool()\` refuses them first. Only the ALLOW crosses into the other process.
|
|
654
|
+
|
|
655
|
+
## Files
|
|
656
|
+
|
|
657
|
+
- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
|
|
658
|
+
**Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.
|
|
659
|
+
- \`index.mjs\` — signs each request and calls \`./gateway\` for it; \`guard.guardTool()\` here is a
|
|
660
|
+
fast local pre-check, not the enforcement boundary.
|
|
661
|
+
- \`gateway/\` — a **separate process**. It holds the real tool and independently re-verifies every
|
|
662
|
+
request against this agent's own policy before running it. See \`gateway/README.md\` — read that
|
|
663
|
+
one first if you're only going to read one.
|
|
664
|
+
|
|
665
|
+
## What this is not
|
|
666
|
+
|
|
667
|
+
`
|
|
668
|
+
: `## Run
|
|
424
669
|
|
|
425
670
|
\`\`\`bash
|
|
426
671
|
npm install
|
|
@@ -435,6 +680,36 @@ You should see an ALLOW, a BLOCK (over the per-transaction cap), and an ESCALATE
|
|
|
435
680
|
**Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.
|
|
436
681
|
- \`index.mjs\` — wraps a tool with \`guard.guardTool(...)\`; the tool only runs when the gate allows.
|
|
437
682
|
|
|
683
|
+
## What this is not
|
|
684
|
+
|
|
685
|
+
`;
|
|
686
|
+
return `# ${slug}
|
|
687
|
+
|
|
688
|
+
A MetaMynd/AgentSafe-governed agent, scaffolded with \`create-metamynd-agent\`.
|
|
689
|
+
|
|
690
|
+
${gatewaySection}${
|
|
691
|
+
withGateway
|
|
692
|
+
? `**With MetaMynd's gateway, you can't be bypassed** — that's what this section is about.
|
|
693
|
+
This scaffold's default shape (agent + separate gateway process, port ${gatewayPort} by
|
|
694
|
+
default) is the actual enforcement boundary: \`guard.guardTool()\` in \`index.mjs\` is a
|
|
695
|
+
client-side convenience, not a boundary — it still runs its handler in-process regardless of
|
|
696
|
+
where the decision came from. What actually stops a bypass is that \`bookFlight()\` itself only
|
|
697
|
+
exists in \`./gateway\`, a process this one cannot reach into, which independently re-verifies
|
|
698
|
+
every request against this agent's own policy bundle. Re-scaffold with \`--no-gateway\` for the
|
|
699
|
+
old single-process shape — it is NOT a separate enforcement boundary; see its own generated
|
|
700
|
+
README for why.`
|
|
701
|
+
: `**Without MetaMynd, you can be bypassed** — this is that case. This scaffold has no
|
|
702
|
+
separate gateway process (either \`--sandbox\`, which never provisions real credentials, or
|
|
703
|
+
\`--no-gateway\` was passed): \`guard.guardTool()\` wraps a tool in the SAME process as the check
|
|
704
|
+
itself. That is a client-side convenience, not a boundary — it still runs your tool's handler
|
|
705
|
+
in-process regardless of where the decision came from, so anything able to call \`bookFlight()\`
|
|
706
|
+
directly gets the same result the gate would have given it. If this tool ever holds a real
|
|
707
|
+
credential, provision for real (drop \`--sandbox\`) without \`--no-gateway\` for the default
|
|
708
|
+
shape, which puts the tool behind a separate process instead. This is the same structural gap
|
|
709
|
+
\`--harness\`'s README documents, for the same reason: a cooperative in-process check has no
|
|
710
|
+
counterparty to disagree with a caller that skips it.`
|
|
711
|
+
}
|
|
712
|
+
|
|
438
713
|
## Change the rules
|
|
439
714
|
|
|
440
715
|
Edit the agent's SOPs in the dashboard (Legal Entity → SOPs). The agent's behaviour changes live —
|
|
@@ -448,6 +723,173 @@ function gitignore() {
|
|
|
448
723
|
return `node_modules/\nagent.metamynd.json\n.env\n`;
|
|
449
724
|
}
|
|
450
725
|
|
|
726
|
+
// ---------- the default hosted scaffold's second process: a separate tool gateway ----------
|
|
727
|
+
//
|
|
728
|
+
// Not a new protocol — @metamynd/agentsafe-mcp-guard (trustless verifyRequest, already public)
|
|
729
|
+
// and @metamynd/agentsafe-http-gateway (the generic reverse-proxy built on it, already public)
|
|
730
|
+
// do the real work. This just wires up the smallest useful shape: one protected route, one
|
|
731
|
+
// tool, re-verified independently of the agent that's calling it. See demo/duffel-mcp-gateway
|
|
732
|
+
// in the AgentSafe repo for the full pattern (mutual handshake, x402 payment, capability
|
|
733
|
+
// binding) this is a minimal slice of.
|
|
734
|
+
|
|
735
|
+
function gatewayServerFile(scope, port, apiBase) {
|
|
736
|
+
return `#!/usr/bin/env node
|
|
737
|
+
// gateway/server.mjs — the REAL enforcement boundary for this agent's tool(s).
|
|
738
|
+
//
|
|
739
|
+
// This is a SEPARATE process from the agent. It holds the tool's real credentials (the agent
|
|
740
|
+
// process never does), and it independently re-verifies every request against this agent's OWN
|
|
741
|
+
// published policy bundle — it does not trust the agent's own guard.guardTool() check. A
|
|
742
|
+
// compromised or dishonest agent calling its own local function gets nothing here, because
|
|
743
|
+
// there is no local function: the tool only runs in this process.
|
|
744
|
+
import http from 'node:http';
|
|
745
|
+
import { createMcpGuard } from '${MCP_GUARD_PKG}';
|
|
746
|
+
import { createHttpGateway } from '${GATEWAY_PKG}';
|
|
747
|
+
|
|
748
|
+
const PORT = Number(process.env.PORT || ${port});
|
|
749
|
+
const MAGP_API = process.env.MAGP_API || '${apiBase}';
|
|
750
|
+
|
|
751
|
+
// --- Your real tool. Real credentials (an airline API key, a payment key, ...) belong ONLY
|
|
752
|
+
// --- here, read from process.env (see .env.example) — never in the agent process.
|
|
753
|
+
async function bookFlight(args) {
|
|
754
|
+
return { pnr: 'PNR-DEMO', ...args };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// One protected route: only a request signed by this agent, for exactly this action, and
|
|
758
|
+
// re-verified against this agent's own mandate/SOP, reaches bookFlight() below.
|
|
759
|
+
const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}' }];
|
|
760
|
+
|
|
761
|
+
// No serviceKey: this minimal gateway only calls verifyRequest() (re-check a signed request),
|
|
762
|
+
// not the mutual-handshake methods, which are the only thing that needs it.
|
|
763
|
+
const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API });
|
|
764
|
+
|
|
765
|
+
const gateway = createHttpGateway({
|
|
766
|
+
guard,
|
|
767
|
+
routes,
|
|
768
|
+
forward: async (req) => {
|
|
769
|
+
let args = {};
|
|
770
|
+
try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
|
|
771
|
+
const result = await bookFlight(args);
|
|
772
|
+
return { status: 200, body: result };
|
|
773
|
+
},
|
|
774
|
+
// This gateway IS the tool, not a proxy in front of one — an unmatched path has nothing to
|
|
775
|
+
// pass through TO. Without this, any path a route doesn't match falls through ungoverned
|
|
776
|
+
// straight to forward() above, which would run bookFlight() with no check at all.
|
|
777
|
+
denyByDefault: true,
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
function readBody(req) {
|
|
781
|
+
return new Promise((resolve, reject) => {
|
|
782
|
+
const chunks = [];
|
|
783
|
+
req.on('data', (c) => chunks.push(c));
|
|
784
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
785
|
+
req.on('error', reject);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const server = http.createServer(async (req, res) => {
|
|
790
|
+
try {
|
|
791
|
+
const rawBody = await readBody(req);
|
|
792
|
+
const result = await gateway({ method: req.method, path: req.url, headers: req.headers, rawBody });
|
|
793
|
+
const headers = { 'content-type': 'application/json' };
|
|
794
|
+
if (result.governance) headers['x-agentsafe-decision'] = result.governance.decision;
|
|
795
|
+
res.writeHead(result.status, headers);
|
|
796
|
+
res.end(JSON.stringify(result.body ?? {}));
|
|
797
|
+
} catch (err) {
|
|
798
|
+
// Fail CLOSED on any gateway error.
|
|
799
|
+
res.writeHead(502, { 'content-type': 'application/json' });
|
|
800
|
+
res.end(JSON.stringify({ decision: 'block', reasonCode: 'GATEWAY_ERROR', error: String(err?.message ?? err) }));
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
server.listen(PORT, () => {
|
|
805
|
+
console.log('[gateway] listening on :' + PORT + ' -> the only place bookFlight() runs.');
|
|
806
|
+
console.log('[gateway] every request is independently re-verified against this agent\\'s own policy.');
|
|
807
|
+
});
|
|
808
|
+
`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function gatewayPackageJson(slug) {
|
|
812
|
+
return JSON.stringify(
|
|
813
|
+
{
|
|
814
|
+
name: slug + '-gateway',
|
|
815
|
+
version: '0.1.0',
|
|
816
|
+
private: true,
|
|
817
|
+
type: 'module',
|
|
818
|
+
scripts: { start: 'node server.mjs' },
|
|
819
|
+
dependencies: { [MCP_GUARD_PKG]: MCP_GUARD_VERSION, [GATEWAY_PKG]: GATEWAY_VERSION },
|
|
820
|
+
},
|
|
821
|
+
null,
|
|
822
|
+
2,
|
|
823
|
+
) + '\n';
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function gatewayEnvExample() {
|
|
827
|
+
return `# Real tool credentials belong HERE, read from process.env in server.mjs — never in the
|
|
828
|
+
# agent process one directory up.
|
|
829
|
+
# AIRLINE_API_KEY=
|
|
830
|
+
`;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function gatewayGitignore() {
|
|
834
|
+
return `node_modules/\n.env\n`;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function gatewayReadme(slug, scope, port) {
|
|
838
|
+
return `# ${slug}-gateway
|
|
839
|
+
|
|
840
|
+
**With MetaMynd, you can't be bypassed.** This process is why. It is the **real enforcement
|
|
841
|
+
boundary** for \`${slug}\`'s tool(s) — not \`../index.mjs\`.
|
|
842
|
+
|
|
843
|
+
## Why this exists
|
|
844
|
+
|
|
845
|
+
\`guard.guardTool()\` in the agent's \`index.mjs\` is a client-side convenience: it gives fast,
|
|
846
|
+
local ALLOW/BLOCK/ESCALATE feedback, but it still runs its handler in the SAME process
|
|
847
|
+
regardless of where that decision came from. Anything able to call the agent's tool function
|
|
848
|
+
directly — a bug, a compromised dependency, a dishonest fork of the agent's own code — gets the
|
|
849
|
+
same result the gate would have given it. That is not a defect in \`guardTool()\`; a cooperative
|
|
850
|
+
in-process check has no counterparty to disagree with a caller that skips it. See \`--harness\`'s
|
|
851
|
+
own README for the same structural point in the free local-demo mode.
|
|
852
|
+
|
|
853
|
+
This process closes that gap by being a **separate** one. The agent has no way to reach into it
|
|
854
|
+
and call \`bookFlight()\` directly, because \`bookFlight()\` doesn't exist in the agent's process —
|
|
855
|
+
it exists only here, and every request that reaches it has already been independently
|
|
856
|
+
re-verified against this agent's OWN published policy bundle, fetched over the network by THIS
|
|
857
|
+
process, not trusted from the agent's say-so.
|
|
858
|
+
|
|
859
|
+
## Run
|
|
860
|
+
|
|
861
|
+
\`\`\`bash
|
|
862
|
+
npm install
|
|
863
|
+
npm start
|
|
864
|
+
\`\`\`
|
|
865
|
+
|
|
866
|
+
Listens on \`:${port}\` by default (\`PORT\` env var to change it — keep \`../index.mjs\`'s
|
|
867
|
+
\`GATEWAY_URL\` in sync if you do).
|
|
868
|
+
|
|
869
|
+
## Add real credentials
|
|
870
|
+
|
|
871
|
+
Edit \`server.mjs\`'s \`bookFlight()\` with your real implementation, reading any credentials it
|
|
872
|
+
needs from \`process.env\` (see \`.env.example\`). Load \`.env\` however you prefer (e.g.
|
|
873
|
+
\`node --env-file=.env server.mjs\`, Node ≥ 20.6) — it is already in \`.gitignore\`. The agent
|
|
874
|
+
directory one level up must never hold these credentials; if it needs to call a DIFFERENT tool,
|
|
875
|
+
add another protected route here rather than adding a local function back in \`index.mjs\`.
|
|
876
|
+
|
|
877
|
+
## Files
|
|
878
|
+
|
|
879
|
+
- \`server.mjs\` — the gateway: one protected route (\`POST /book-flight\`, action \`${scope}\`),
|
|
880
|
+
\`@metamynd/agentsafe-mcp-guard\`'s \`verifyRequest()\` re-checking every request, and the real
|
|
881
|
+
\`bookFlight()\`.
|
|
882
|
+
- \`.env.example\` — where real tool credentials go (copy to \`.env\`, fill in, never commit).
|
|
883
|
+
|
|
884
|
+
## Beyond this minimal slice
|
|
885
|
+
|
|
886
|
+
This gateway only re-verifies a signed request (§9.3/§9.6 of the MAGP spec). It does not do the
|
|
887
|
+
mutual DID handshake, x402 payment binding, or commitment-bound capability tokens that a
|
|
888
|
+
production Service integration would add — see \`@metamynd/agentsafe-mcp-guard\`'s own README for
|
|
889
|
+
those, and \`demo/duffel-mcp-gateway\` in the AgentSafe repo for a full worked example.
|
|
890
|
+
`;
|
|
891
|
+
}
|
|
892
|
+
|
|
451
893
|
function writeFileSafe(dir, name, content, force = false) {
|
|
452
894
|
const p = join(dir, name);
|
|
453
895
|
const exists = existsSync(p);
|
|
@@ -480,16 +922,32 @@ function assertScaffoldTarget(outDir, force) {
|
|
|
480
922
|
);
|
|
481
923
|
}
|
|
482
924
|
|
|
483
|
-
/**
|
|
484
|
-
|
|
925
|
+
/**
|
|
926
|
+
* Write the scaffolded project + print next steps. Shared by the provision and sandbox paths.
|
|
927
|
+
* `withGateway`: scaffold the default two-process shape (agent + ./gateway) — the real
|
|
928
|
+
* enforcement boundary. Off for --sandbox (shared demo identity, never real credentials
|
|
929
|
+
* anyway) and --no-gateway (opt out, e.g. you're already running your own separate gateway).
|
|
930
|
+
*/
|
|
931
|
+
function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox, withGateway, gatewayPort = DEFAULT_GATEWAY_PORT, force = false }) {
|
|
485
932
|
assertScaffoldTarget(outDir, force);
|
|
486
933
|
console.log(`\n ${c.b('Scaffolding')} ${c.dim(outDir)}`);
|
|
487
934
|
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
488
935
|
writeFileSafe(outDir, 'agent.metamynd.json', JSON.stringify(config, null, 2) + '\n', force);
|
|
489
|
-
writeFileSafe(outDir, 'index.mjs', exampleIndex(scope, perTxnMax), force);
|
|
936
|
+
writeFileSafe(outDir, 'index.mjs', withGateway ? exampleIndex(scope, perTxnMax, gatewayPort) : exampleIndexNoGateway(scope, perTxnMax), force);
|
|
490
937
|
writeFileSafe(outDir, 'package.json', examplePackageJson(slug), force);
|
|
491
938
|
writeFileSafe(outDir, '.gitignore', gitignore(), force);
|
|
492
|
-
writeFileSafe(outDir, 'README.md', exampleReadme(slug, scope), force);
|
|
939
|
+
writeFileSafe(outDir, 'README.md', exampleReadme(slug, scope, withGateway, gatewayPort), force);
|
|
940
|
+
|
|
941
|
+
if (withGateway) {
|
|
942
|
+
const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
|
|
943
|
+
const gwDir = join(outDir, 'gateway');
|
|
944
|
+
if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
|
|
945
|
+
writeFileSafe(gwDir, 'server.mjs', gatewayServerFile(scope, gatewayPort, apiBase), force);
|
|
946
|
+
writeFileSafe(gwDir, 'package.json', gatewayPackageJson(slug), force);
|
|
947
|
+
writeFileSafe(gwDir, '.env.example', gatewayEnvExample(), force);
|
|
948
|
+
writeFileSafe(gwDir, '.gitignore', gatewayGitignore(), force);
|
|
949
|
+
writeFileSafe(gwDir, 'README.md', gatewayReadme(slug, scope, gatewayPort), force);
|
|
950
|
+
}
|
|
493
951
|
|
|
494
952
|
const rel = outDir.replace(resolve('.'), '.').replace(/\\/g, '/');
|
|
495
953
|
console.log(`\n${c.green(c.b(' ✓ Done.'))} Your governed agent is ready.\n`);
|
|
@@ -498,7 +956,13 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox, forc
|
|
|
498
956
|
} else if (config.agentKey) {
|
|
499
957
|
console.log(` ${c.yellow('⚠ agent.metamynd.json holds the agent secret key')} — it is gitignored; never commit it.\n`);
|
|
500
958
|
}
|
|
959
|
+
if (withGateway) {
|
|
960
|
+
console.log(` ${c.yellow('⚠ two processes now')} — \`gateway/\` is the real enforcement boundary, not \`index.mjs\`. Read \`gateway/README.md\`.\n`);
|
|
961
|
+
}
|
|
501
962
|
console.log(` Next:`);
|
|
963
|
+
if (withGateway) {
|
|
964
|
+
console.log(c.cyan(` cd ${rel}/gateway && npm install && npm start`) + c.dim(' (separate terminal — start this first)'));
|
|
965
|
+
}
|
|
502
966
|
console.log(c.cyan(` cd ${rel}`));
|
|
503
967
|
console.log(c.cyan(` npm install`));
|
|
504
968
|
// The example runs FOUR attempts. This summary promised three, so the one carrying the
|
|
@@ -524,7 +988,7 @@ async function runSandbox(args) {
|
|
|
524
988
|
console.log(` ${c.green('✓')} sandbox agent ${c.b(config.agentDid)} ${c.dim('(shared test identity)')}`);
|
|
525
989
|
const scope = config.mandate?.scope || 'flight-purchase';
|
|
526
990
|
const perTxnMax = Number(config.perTxnMax) || 500;
|
|
527
|
-
scaffoldProject({ outDir, config, slug: 'metamynd-sandbox', scope, perTxnMax, sandbox: true, force: !!args.force });
|
|
991
|
+
scaffoldProject({ outDir, config, slug: 'metamynd-sandbox', scope, perTxnMax, sandbox: true, withGateway: false, force: !!args.force });
|
|
528
992
|
}
|
|
529
993
|
|
|
530
994
|
// ---------- --harness: a free, local, zero-network governance harness ----------
|
|
@@ -1098,12 +1562,17 @@ console.log(dim(' authority, because it cannot name an action nobody delegat
|
|
|
1098
1562
|
console.log(dim(' - the blocked call never reached your tool at all.'));
|
|
1099
1563
|
console.log(dim(' - every decision is in ./metamynd-harness.log.jsonl - yours, locally.'));
|
|
1100
1564
|
console.log('');
|
|
1565
|
+
console.log(bold(' Without MetaMynd, you can be bypassed.') + ' bookFlight() above runs in THIS');
|
|
1566
|
+
console.log(dim(' process - call it directly instead of gatedBookFlight and nothing stops you.'));
|
|
1567
|
+
console.log(dim(' --harness proves your policy logic; it does not enforce it against that.'));
|
|
1568
|
+
console.log('');
|
|
1101
1569
|
console.log(' Edit ./metamynd-rules.json (or the dashboard) and run again - the outcome');
|
|
1102
1570
|
console.log(dim(' changes. This file does not. That is the point.'));
|
|
1103
1571
|
console.log('');
|
|
1104
1572
|
console.log(dim(' Ready for more than one machine, a queue someone else can approve from,'));
|
|
1105
|
-
console.log(dim(' anchored evidence, or KYC/KYB-backed identity
|
|
1106
|
-
console.log(dim('
|
|
1573
|
+
console.log(dim(' anchored evidence, or KYC/KYB-backed identity, AND a separate gateway process'));
|
|
1574
|
+
console.log(dim(' that closes the bypass above? That is the hosted platform - drop --harness'));
|
|
1575
|
+
console.log(dim(' and provision there; the same guardTool() call keeps working.'));
|
|
1107
1576
|
console.log('');
|
|
1108
1577
|
dashboard.close();
|
|
1109
1578
|
`;
|
|
@@ -1153,6 +1622,8 @@ open the dashboard to approve it), and a BLOCK (an action outside the mandate en
|
|
|
1153
1622
|
|
|
1154
1623
|
## What this is not
|
|
1155
1624
|
|
|
1625
|
+
**Without MetaMynd, you can be bypassed.** Everything below is why, precisely.
|
|
1626
|
+
|
|
1156
1627
|
No anchored/verifiable identity, no cross-party trust, no evidence anyone but you can audit,
|
|
1157
1628
|
no dashboard reachable when this machine is off, no owner queue someone else can approve from.
|
|
1158
1629
|
That's the hosted platform (\`npx create-metamynd-agent\`, without \`--harness\`) — same
|
|
@@ -1163,8 +1634,8 @@ cooperative library this process embeds — call the tool handler directly inste
|
|
|
1163
1634
|
one and nothing stops you, because there is no second party in the loop to disagree with you.
|
|
1164
1635
|
That's structural, not a bug: use this harness to govern your own agent's own honest behavior,
|
|
1165
1636
|
not as a defense against an agent (or a person) actively trying to get around it. The hosted
|
|
1166
|
-
platform's
|
|
1167
|
-
agent's signed authority for itself instead of trusting that the agent's own guard ran.
|
|
1637
|
+
platform's default scaffold doesn't have this gap, because a SEPARATE gateway process re-verifies
|
|
1638
|
+
the agent's signed authority for itself instead of trusting that the agent's own guard ran.
|
|
1168
1639
|
`;
|
|
1169
1640
|
}
|
|
1170
1641
|
|
|
@@ -1336,7 +1807,7 @@ async function runClaim(args) {
|
|
|
1336
1807
|
|
|
1337
1808
|
const slug = slugify(state.name || 'metamynd-agent');
|
|
1338
1809
|
const outDir = resolve(String(args.out || `./${slug}`));
|
|
1339
|
-
scaffoldProject({ outDir, config, slug, scope: state.scope || config.mandate?.scope || 'flight-purchase', perTxnMax: Number(state.perTxnMax) || 500, sandbox: false, force: !!args.force });
|
|
1810
|
+
scaffoldProject({ outDir, config, slug, scope: state.scope || config.mandate?.scope || 'flight-purchase', perTxnMax: Number(state.perTxnMax) || 500, sandbox: false, withGateway: !args['no-gateway'], gatewayPort: Number(args['gateway-port']) || DEFAULT_GATEWAY_PORT, force: !!args.force });
|
|
1340
1811
|
}
|
|
1341
1812
|
|
|
1342
1813
|
// ---------- main ----------
|
|
@@ -1459,7 +1930,7 @@ async function main() {
|
|
|
1459
1930
|
}
|
|
1460
1931
|
|
|
1461
1932
|
// 4. Scaffold + next steps
|
|
1462
|
-
scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox: false, force: !!args.force });
|
|
1933
|
+
scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox: false, withGateway: !args['no-gateway'], gatewayPort: Number(args['gateway-port']) || DEFAULT_GATEWAY_PORT, force: !!args.force });
|
|
1463
1934
|
}
|
|
1464
1935
|
|
|
1465
1936
|
main().catch((e) => fail(e?.stack || e?.message || String(e)));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.
|
|
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
|
|
3
|
+
"version": "0.7.1",
|
|
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 (the real enforcement boundary). --harness scaffolds a free, local, zero-network governance harness instead.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-metamynd-agent": "index.mjs"
|