create-metamynd-agent 0.6.2 → 0.7.0
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 +66 -15
- package/index.mjs +469 -15
- 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
|
|
@@ -45,13 +50,13 @@ It is also **not a separate enforcement boundary**, and this matters more than t
|
|
|
45
50
|
`guardToolLocal()` is a cooperative library your own process embeds — call the raw handler directly
|
|
46
51
|
instead of the guarded one and nothing stops you, because there is no second party in the loop to
|
|
47
52
|
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
|
-
|
|
53
|
+
function underneath it) succeeds every time, structurally, not as a bug. What actually closes this
|
|
54
|
+
is a **counterparty** — a separate process holding the tool, that independently re-verifies the
|
|
55
|
+
agent's signed authority for itself rather than trusting that the agent's own guard ran. `--harness`
|
|
56
|
+
never has one, by design (there's no second party on one machine with no network). **Just dropping
|
|
57
|
+
`--harness` is not enough on its own to get one either** — see
|
|
58
|
+
[Separate tool gateway](#separate-tool-gateway-default) below for what actually provides it, and
|
|
59
|
+
`--no-gateway`'s own caveat for what happens if you opt out of it.
|
|
55
60
|
|
|
56
61
|
Works with `--config` too — its `rules` become the harness's starter rules file, same as the hosted
|
|
57
62
|
flow. See [Policy config file](#policy-config-file---config) below.
|
|
@@ -74,6 +79,10 @@ call the hosted API (a shared demo identity) — it's a first look at the *hoste
|
|
|
74
79
|
local/offline mode. Great for a first look; use the full flow below when you want your own governed
|
|
75
80
|
agent with your own limits.
|
|
76
81
|
|
|
82
|
+
`--sandbox` always scaffolds the single-process shape (no `gateway/`) — it's a shared identity never
|
|
83
|
+
meant to hold real credentials, so there's nothing here worth a separate enforcement boundary for.
|
|
84
|
+
The generated project's own README says so. The full flow below is what scaffolds one by default.
|
|
85
|
+
|
|
77
86
|
## Use
|
|
78
87
|
|
|
79
88
|
```bash
|
|
@@ -82,18 +91,32 @@ npm create metamynd-agent@latest
|
|
|
82
91
|
npx create-metamynd-agent
|
|
83
92
|
```
|
|
84
93
|
|
|
85
|
-
Answer a few prompts (API, owner email/password, agent name, scope, per-transaction cap) and you get
|
|
94
|
+
Answer a few prompts (API, owner email/password, agent name, scope, per-transaction cap) and you get
|
|
95
|
+
**two** scaffolded projects — the agent, and its tool gateway:
|
|
86
96
|
|
|
87
97
|
```
|
|
88
98
|
my-agent/
|
|
89
99
|
├─ agent.metamynd.json # portable guard config — HOLDS THE AGENT SECRET KEY (gitignored)
|
|
90
|
-
├─ index.mjs # runnable example:
|
|
100
|
+
├─ index.mjs # runnable example: signs + calls ./gateway; guardTool() here is a
|
|
101
|
+
│ # fast local pre-check, NOT the enforcement boundary
|
|
91
102
|
├─ package.json # depends on @metamynd/agentsafe-guard
|
|
92
103
|
├─ .gitignore
|
|
93
|
-
|
|
104
|
+
├─ README.md
|
|
105
|
+
└─ gateway/ # a SEPARATE process — the real enforcement boundary. Read its
|
|
106
|
+
├─ server.mjs # README first if you only read one.
|
|
107
|
+
├─ package.json # depends on @metamynd/agentsafe-mcp-guard + @metamynd/agentsafe-http-gateway
|
|
108
|
+
├─ .env.example # real tool credentials go here, never in the agent directory
|
|
109
|
+
├─ .gitignore
|
|
110
|
+
└─ README.md
|
|
94
111
|
```
|
|
95
112
|
|
|
96
|
-
Then:
|
|
113
|
+
Then, in **two terminals** — the gateway first:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
cd my-agent/gateway
|
|
117
|
+
npm install
|
|
118
|
+
npm start
|
|
119
|
+
```
|
|
97
120
|
|
|
98
121
|
```bash
|
|
99
122
|
cd my-agent
|
|
@@ -101,6 +124,27 @@ npm install
|
|
|
101
124
|
npm start
|
|
102
125
|
```
|
|
103
126
|
|
|
127
|
+
### Separate tool gateway (default)
|
|
128
|
+
|
|
129
|
+
`guard.guardTool()` in `index.mjs` still runs — it's a fast, local, client-side pre-check that gives
|
|
130
|
+
good UX (fail fast on an obviously-blocked call, no round trip) — but it is **not** what stops a
|
|
131
|
+
bypass. It still calls its handler in the SAME process regardless of where the decision came from,
|
|
132
|
+
so anything able to call that handler directly gets the same result the gate would have given it.
|
|
133
|
+
|
|
134
|
+
What actually stops a bypass is that `bookFlight()` doesn't exist in the agent's process at all.
|
|
135
|
+
It exists only in `gateway/server.mjs` — a separate process, started separately, holding any real
|
|
136
|
+
tool credentials the agent process never sees — which independently re-verifies every request
|
|
137
|
+
against the agent's own published policy bundle before running it (same shape as the mutual
|
|
138
|
+
counterparty check in [`@metamynd/agentsafe-mcp-guard`](https://www.npmjs.com/package/@metamynd/agentsafe-mcp-guard),
|
|
139
|
+
built with [`@metamynd/agentsafe-http-gateway`](https://www.npmjs.com/package/@metamynd/agentsafe-http-gateway)).
|
|
140
|
+
It's a minimal slice of the same pattern proven end to end in `demo/duffel-mcp-gateway` in the
|
|
141
|
+
AgentSafe repo (mutual handshake, x402 payment binding, capability tokens) — this scaffold gives you
|
|
142
|
+
just the part that closes the bypass, not the whole protocol.
|
|
143
|
+
|
|
144
|
+
Pass `--no-gateway` to opt out and get the old single-process scaffold instead — e.g. if you're
|
|
145
|
+
already running your own separate gateway and don't need this one. The generated project's own
|
|
146
|
+
README says plainly that this is *not* a separate enforcement boundary if you do.
|
|
147
|
+
|
|
104
148
|
## Non-interactive
|
|
105
149
|
|
|
106
150
|
Every prompt has a flag or environment-variable fallback, so it scripts cleanly in CI:
|
|
@@ -123,6 +167,8 @@ METAMYND_PASSWORD='…' npx create-metamynd-agent --yes …
|
|
|
123
167
|
| `--harness` | — | off (no login/KYB/network at all; free local governance — see above) |
|
|
124
168
|
| `--sandbox` | — | off (skips login/KYB; shared sandbox agent, still hosted) |
|
|
125
169
|
| `--config <file>` | — | a JSON policy file — see [Policy config file](#policy-config-file---config) |
|
|
170
|
+
| `--no-gateway` | — | off — hosted flow only; skips the default separate tool gateway (see above) |
|
|
171
|
+
| `--gateway-port <n>` | — | `4401` — hosted flow only, the gateway process's port |
|
|
126
172
|
| `--port <n>` | — | `4400` — `--harness` only, the local dashboard's port |
|
|
127
173
|
| `--api <url>` | `METAMYND_API` | `https://metamynd.ai/api/v1` |
|
|
128
174
|
| `--email <email>` | `METAMYND_EMAIL` | — (required) |
|
|
@@ -200,14 +246,19 @@ npx create-metamynd-agent --claim --watch
|
|
|
200
246
|
```
|
|
201
247
|
|
|
202
248
|
`--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
|
-
|
|
249
|
+
polls until the owner approves, then scaffolds the project (the same default two-process shape as
|
|
250
|
+
the full flow above — `--no-gateway`/`--gateway-port` work here too). With `--byok` the keypair is
|
|
251
|
+
generated locally and control is proven on claim — MetaMynd never sees the private key.
|
|
205
252
|
|
|
206
253
|
## Security
|
|
207
254
|
|
|
208
255
|
`agent.metamynd.json` contains the agent's **secret key** (a managed key, or — with `--byok` — the one
|
|
209
256
|
generated locally). The scaffolded project gitignores it. Never commit it or paste it anywhere public.
|
|
210
257
|
|
|
258
|
+
Any REAL tool credential (an airline API key, a payment key, ...) belongs in `gateway/.env` — never
|
|
259
|
+
in the agent directory. That's the whole point of the default two-process shape: the agent process
|
|
260
|
+
should never be able to hold, or leak, a credential it doesn't have.
|
|
261
|
+
|
|
211
262
|
## Full guide
|
|
212
263
|
|
|
213
264
|
`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
|
}
|
|
@@ -397,6 +426,186 @@ console.log('');
|
|
|
397
426
|
`;
|
|
398
427
|
}
|
|
399
428
|
|
|
429
|
+
/**
|
|
430
|
+
* The DEFAULT hosted scaffold: the tool lives in a separate process (./gateway), not here.
|
|
431
|
+
* guard.guardTool() below is still called — it is a fast, local, client-side pre-check that
|
|
432
|
+
* gives good UX (fail fast, no round trip for an obviously-blocked call) — but it is not what
|
|
433
|
+
* stops a bypass. What stops a bypass is that there is no bookFlight() in THIS process to call
|
|
434
|
+
* directly: it only exists in ./gateway, which independently re-verifies every request against
|
|
435
|
+
* this agent's own policy bundle before it runs, and holds any real credentials the tool needs.
|
|
436
|
+
*/
|
|
437
|
+
function exampleIndex(scope, perTxnMax, gatewayPort) {
|
|
438
|
+
const under = Math.max(1, Math.round(perTxnMax * 0.5));
|
|
439
|
+
const over = Math.round(perTxnMax + 100);
|
|
440
|
+
return `// index.mjs — your agent, governed by MetaMynd/AgentSafe.
|
|
441
|
+
// Every governed tool call is checked TWICE before it runs: once here (fast, local, client-side),
|
|
442
|
+
// and independently again by ./gateway — a SEPARATE process that holds the real tool and its
|
|
443
|
+
// credentials. That second check is the actual enforcement boundary; see ./gateway/README.md.
|
|
444
|
+
import { createGuardFromConfig } from '${GUARD_PKG}';
|
|
445
|
+
|
|
446
|
+
// Loads agent.metamynd.json: the agent's DID, its signing key, and the gate to call.
|
|
447
|
+
const guard = await createGuardFromConfig('./agent.metamynd.json'); // no env vars
|
|
448
|
+
|
|
449
|
+
const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
|
|
450
|
+
|
|
451
|
+
// --- Calls the gateway process instead of a local function. There is no raw bookFlight() in
|
|
452
|
+
// --- this file to call directly — the tool, and any real credentials it needs, live only in
|
|
453
|
+
// --- ./gateway, which independently re-verifies this signed request itself.
|
|
454
|
+
async function bookFlightViaGateway(args) {
|
|
455
|
+
const signed = guard.buildSignedRequest({
|
|
456
|
+
action: '${scope}',
|
|
457
|
+
amount: args.amount,
|
|
458
|
+
currency: 'USD',
|
|
459
|
+
merchant: args.merchant,
|
|
460
|
+
context: { tool: 'book-flight', riskLevel: args.riskLevel ?? 'low' },
|
|
461
|
+
});
|
|
462
|
+
const res = await fetch(GATEWAY + '/book-flight', {
|
|
463
|
+
method: 'POST',
|
|
464
|
+
headers: { 'content-type': 'application/json', 'x-magp-request': JSON.stringify(signed) },
|
|
465
|
+
body: JSON.stringify(args),
|
|
466
|
+
});
|
|
467
|
+
const body = await res.json().catch(() => null);
|
|
468
|
+
if (!res.ok) {
|
|
469
|
+
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
470
|
+
err.name = 'GovernanceBlocked';
|
|
471
|
+
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
|
|
472
|
+
throw err;
|
|
473
|
+
}
|
|
474
|
+
return body;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// --- The GATED version. Register THIS with your agent instead of calling the gateway directly.
|
|
478
|
+
// --- This local check and the gateway's own re-check are independent; neither trusts the other.
|
|
479
|
+
const gatedBookFlight = guard.guardTool(
|
|
480
|
+
'${scope}', // = your mandate scope
|
|
481
|
+
bookFlightViaGateway,
|
|
482
|
+
(a) => ({ // map tool args → gate inputs
|
|
483
|
+
amount: a.amount,
|
|
484
|
+
currency: 'USD',
|
|
485
|
+
merchant: a.merchant,
|
|
486
|
+
context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
|
|
487
|
+
}),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
// --- A tool the agent was NEVER granted. Wrapping it is the demonstration: there is no
|
|
491
|
+
// --- rule anywhere forbidding this. The mandate simply never mentioned the action.
|
|
492
|
+
async function raiseOwnLimit(args) {
|
|
493
|
+
return { updated: true, ...args }; // never runs, and that is the point
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const gatedRaiseOwnLimit = guard.guardTool(
|
|
497
|
+
'permissions.update', // an action NOT in the mandate
|
|
498
|
+
raiseOwnLimit,
|
|
499
|
+
(a) => ({
|
|
500
|
+
amount: a.amount,
|
|
501
|
+
currency: 'USD',
|
|
502
|
+
merchant: a.merchant,
|
|
503
|
+
context: { tool: 'permissions-update' },
|
|
504
|
+
}),
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
const dim = (t) => '\\x1b[2m' + t + '\\x1b[0m';
|
|
508
|
+
const bold = (t) => '\\x1b[1m' + t + '\\x1b[0m';
|
|
509
|
+
const rule = (n) => ' ' + '-'.repeat(n);
|
|
510
|
+
|
|
511
|
+
// Plain-English meaning for the reason codes this demo can produce. The gateway re-evaluates
|
|
512
|
+
// the SAME policy bundle with the SAME evaluator the gate uses, so it produces these same codes.
|
|
513
|
+
const WHY = {
|
|
514
|
+
AUTHORIZED: 'inside the mandate and under the SOP spend cap',
|
|
515
|
+
SOP_SPEND_CAP: 'your SOP caps a single transaction at $${perTxnMax}',
|
|
516
|
+
RISK_REVIEW: 'your SOP sends high-risk actions to a human first',
|
|
517
|
+
MERCHANT_NOT_ALLOWED: 'the mandate lists which merchants this agent may pay',
|
|
518
|
+
// Both say the same thing from where you are standing: the mandate does not cover that
|
|
519
|
+
// action. Which one you see depends on whether the verdict was reached here or at the
|
|
520
|
+
// gate, and neither of them depends on the amount.
|
|
521
|
+
NO_PERMISSION_FOR_ACTION: 'the mandate never granted this action - at any amount',
|
|
522
|
+
NO_MANDATE: 'there is no mandate for this action at all',
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// ---------------------------------------------------------------- 1. CONTEXT
|
|
526
|
+
console.log('');
|
|
527
|
+
console.log(bold(' What this simulation shows'));
|
|
528
|
+
console.log('');
|
|
529
|
+
console.log(' An agent should not be the thing that decides what it is allowed to do — and');
|
|
530
|
+
console.log(' it should not be the thing that RUNS what it decided, either. This run makes');
|
|
531
|
+
console.log(' both concrete. Three attempts take the SAME code path and produce three');
|
|
532
|
+
console.log(' different outcomes. The fourth asks for something the agent was never granted');
|
|
533
|
+
console.log(' at all - and that is the one a prompt could not have stopped, because the');
|
|
534
|
+
console.log(' decision is not made inside your program, and the tool is not either.');
|
|
535
|
+
|
|
536
|
+
// ---------------------------------------------------------------- 2. MECHANISM
|
|
537
|
+
console.log('');
|
|
538
|
+
console.log(bold(' How it does that'));
|
|
539
|
+
console.log('');
|
|
540
|
+
console.log(dim(' 1. this project holds an agent identity (a DID) and its signing key'));
|
|
541
|
+
console.log(dim(' 2. that agent has a mandate - a scope it may act in, and a spend cap'));
|
|
542
|
+
console.log(dim(' 3. guardTool() wraps your tool call, giving a fast local pre-check'));
|
|
543
|
+
console.log(dim(' 4. each attempt is ALSO signed and sent to ./gateway - a separate process'));
|
|
544
|
+
console.log(dim(' 5. the gateway independently re-verifies before your tool runs there'));
|
|
545
|
+
console.log(dim(' 6. there is no local bookFlight() to call directly - only the gateway has it'));
|
|
546
|
+
console.log('');
|
|
547
|
+
console.log(dim(' scope ${scope}'));
|
|
548
|
+
console.log(dim(' cap $${perTxnMax} per transaction, set by your SOP'));
|
|
549
|
+
console.log(dim(' gateway ' + GATEWAY + ' (run it in a separate terminal - see ./gateway)'));
|
|
550
|
+
|
|
551
|
+
// ---------------------------------------------------------------- 3. THE STEPS
|
|
552
|
+
async function attempt(n, intent, args, tool = gatedBookFlight) {
|
|
553
|
+
console.log('');
|
|
554
|
+
console.log(bold(' Step ' + n + ' of 4') + ' - ' + intent);
|
|
555
|
+
console.log(dim(' signing the request locally, then asking the gate to decide...'));
|
|
556
|
+
try {
|
|
557
|
+
const r = await tool(args);
|
|
558
|
+
console.log('\\x1b[32m ALLOWED\\x1b[0m your tool ran (in ./gateway) and returned ' + (r.pnr ?? 'ok'));
|
|
559
|
+
console.log(dim(' ' + WHY.AUTHORIZED));
|
|
560
|
+
} catch (e) {
|
|
561
|
+
const g = e.governance ?? {};
|
|
562
|
+
const why = WHY[g.reasonCode] ?? e.message;
|
|
563
|
+
if (g.decision === 'escalate') {
|
|
564
|
+
console.log('\\x1b[33m ESCALATED\\x1b[0m held for a human - ' + g.reasonCode);
|
|
565
|
+
console.log(dim(' ' + why));
|
|
566
|
+
console.log(dim(' not a failure: approve it in the dashboard and the action resumes.'));
|
|
567
|
+
} else {
|
|
568
|
+
console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
|
|
569
|
+
console.log(dim(' ' + why));
|
|
570
|
+
console.log(dim(' your tool never ran - refused before execution.'));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
console.log('');
|
|
576
|
+
console.log(rule(66));
|
|
577
|
+
await attempt(1, 'a $${under} booking, low risk. Expected to pass.', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'low' });
|
|
578
|
+
await attempt(2, 'a $${over} booking, deliberately over the cap.', { amount: ${over}, merchant: 'skyward-air', riskLevel: 'low' });
|
|
579
|
+
await attempt(3, 'a $${under} booking, but flagged high risk.', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'high' });
|
|
580
|
+
await attempt(
|
|
581
|
+
4,
|
|
582
|
+
'the agent stops booking flights and asks to raise its OWN limit.',
|
|
583
|
+
{ amount: 100000, merchant: 'skyward-air' },
|
|
584
|
+
gatedRaiseOwnLimit,
|
|
585
|
+
);
|
|
586
|
+
console.log('');
|
|
587
|
+
console.log(rule(66));
|
|
588
|
+
|
|
589
|
+
// ---------------------------------------------------------------- 4. RESULT
|
|
590
|
+
console.log('');
|
|
591
|
+
console.log(bold(' What this proved'));
|
|
592
|
+
console.log('');
|
|
593
|
+
console.log(dim(' - one code path, three outcomes. The rules decided, not this file'));
|
|
594
|
+
console.log(dim(' and not the model driving it.'));
|
|
595
|
+
console.log(dim(' - step 1 ran in ./gateway, a process this file cannot reach into. There'));
|
|
596
|
+
console.log(dim(' is no rawBookFlight() here to call instead - that is what actually'));
|
|
597
|
+
console.log(dim(' stops a bypass, not the guardTool() call above it.'));
|
|
598
|
+
console.log(dim(' - step 4 needed no rule to stop it. The agent could not widen its own'));
|
|
599
|
+
console.log(dim(' authority, because it cannot name an action nobody delegated to it.'));
|
|
600
|
+
console.log(dim(' - every blocked/escalated call never reached a real tool at all.'));
|
|
601
|
+
console.log(dim(' - if the gate were unreachable the guard fails CLOSED: it blocks.'));
|
|
602
|
+
console.log('');
|
|
603
|
+
console.log(' Change the cap in the dashboard (Legal Entity -> SOPs) and run again.');
|
|
604
|
+
console.log(dim(' The outcome changes. This file does not. That is the point.'));
|
|
605
|
+
console.log('');
|
|
606
|
+
`;
|
|
607
|
+
}
|
|
608
|
+
|
|
400
609
|
function examplePackageJson(slug) {
|
|
401
610
|
return JSON.stringify(
|
|
402
611
|
{
|
|
@@ -415,12 +624,41 @@ function examplePackageJson(slug) {
|
|
|
415
624
|
) + '\n';
|
|
416
625
|
}
|
|
417
626
|
|
|
418
|
-
function exampleReadme(slug, scope) {
|
|
419
|
-
|
|
627
|
+
function exampleReadme(slug, scope, withGateway, gatewayPort) {
|
|
628
|
+
const gatewaySection = withGateway
|
|
629
|
+
? `## Run
|
|
420
630
|
|
|
421
|
-
|
|
631
|
+
Two processes — start the gateway first, in its own terminal:
|
|
422
632
|
|
|
423
|
-
|
|
633
|
+
\`\`\`bash
|
|
634
|
+
cd gateway && npm install && npm start # the REAL enforcement boundary — see gateway/README.md
|
|
635
|
+
\`\`\`
|
|
636
|
+
|
|
637
|
+
Then, in this directory:
|
|
638
|
+
|
|
639
|
+
\`\`\`bash
|
|
640
|
+
npm install
|
|
641
|
+
npm start
|
|
642
|
+
\`\`\`
|
|
643
|
+
|
|
644
|
+
You should see an ALLOW (fulfilled by \`./gateway\`), a BLOCK (over the per-transaction cap), and
|
|
645
|
+
an ESCALATE (high risk). The BLOCK and ESCALATE never reach the gateway at all — this file's own
|
|
646
|
+
\`guard.guardTool()\` refuses them first. Only the ALLOW crosses into the other process.
|
|
647
|
+
|
|
648
|
+
## Files
|
|
649
|
+
|
|
650
|
+
- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
|
|
651
|
+
**Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.
|
|
652
|
+
- \`index.mjs\` — signs each request and calls \`./gateway\` for it; \`guard.guardTool()\` here is a
|
|
653
|
+
fast local pre-check, not the enforcement boundary.
|
|
654
|
+
- \`gateway/\` — a **separate process**. It holds the real tool and independently re-verifies every
|
|
655
|
+
request against this agent's own policy before running it. See \`gateway/README.md\` — read that
|
|
656
|
+
one first if you're only going to read one.
|
|
657
|
+
|
|
658
|
+
## What this is not
|
|
659
|
+
|
|
660
|
+
`
|
|
661
|
+
: `## Run
|
|
424
662
|
|
|
425
663
|
\`\`\`bash
|
|
426
664
|
npm install
|
|
@@ -435,6 +673,34 @@ You should see an ALLOW, a BLOCK (over the per-transaction cap), and an ESCALATE
|
|
|
435
673
|
**Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.
|
|
436
674
|
- \`index.mjs\` — wraps a tool with \`guard.guardTool(...)\`; the tool only runs when the gate allows.
|
|
437
675
|
|
|
676
|
+
## What this is not
|
|
677
|
+
|
|
678
|
+
`;
|
|
679
|
+
return `# ${slug}
|
|
680
|
+
|
|
681
|
+
A MetaMynd/AgentSafe-governed agent, scaffolded with \`create-metamynd-agent\`.
|
|
682
|
+
|
|
683
|
+
${gatewaySection}${
|
|
684
|
+
withGateway
|
|
685
|
+
? `This scaffold's default shape (agent + separate gateway process, port ${gatewayPort} by
|
|
686
|
+
default) is the actual enforcement boundary: \`guard.guardTool()\` in \`index.mjs\` is a
|
|
687
|
+
client-side convenience, not a boundary — it still runs its handler in-process regardless of
|
|
688
|
+
where the decision came from. What actually stops a bypass is that \`bookFlight()\` itself only
|
|
689
|
+
exists in \`./gateway\`, a process this one cannot reach into, which independently re-verifies
|
|
690
|
+
every request against this agent's own policy bundle. Re-scaffold with \`--no-gateway\` for the
|
|
691
|
+
old single-process shape — it is NOT a separate enforcement boundary; see its own generated
|
|
692
|
+
README for why.`
|
|
693
|
+
: `This scaffold has no separate gateway process (either \`--sandbox\`, which never
|
|
694
|
+
provisions real credentials, or \`--no-gateway\` was passed): \`guard.guardTool()\` wraps a tool
|
|
695
|
+
in the SAME process as the check itself. That is a client-side convenience, not a boundary — it
|
|
696
|
+
still runs your tool's handler in-process regardless of where the decision came from, so
|
|
697
|
+
anything able to call \`bookFlight()\` directly gets the same result the gate would have given
|
|
698
|
+
it. If this tool ever holds a real credential, provision for real (drop \`--sandbox\`) without
|
|
699
|
+
\`--no-gateway\` for the default shape, which puts the tool behind a separate process instead.
|
|
700
|
+
This is the same structural gap \`--harness\`'s README documents, for the same reason: a
|
|
701
|
+
cooperative in-process check has no counterparty to disagree with a caller that skips it.`
|
|
702
|
+
}
|
|
703
|
+
|
|
438
704
|
## Change the rules
|
|
439
705
|
|
|
440
706
|
Edit the agent's SOPs in the dashboard (Legal Entity → SOPs). The agent's behaviour changes live —
|
|
@@ -448,6 +714,172 @@ function gitignore() {
|
|
|
448
714
|
return `node_modules/\nagent.metamynd.json\n.env\n`;
|
|
449
715
|
}
|
|
450
716
|
|
|
717
|
+
// ---------- the default hosted scaffold's second process: a separate tool gateway ----------
|
|
718
|
+
//
|
|
719
|
+
// Not a new protocol — @metamynd/agentsafe-mcp-guard (trustless verifyRequest, already public)
|
|
720
|
+
// and @metamynd/agentsafe-http-gateway (the generic reverse-proxy built on it, already public)
|
|
721
|
+
// do the real work. This just wires up the smallest useful shape: one protected route, one
|
|
722
|
+
// tool, re-verified independently of the agent that's calling it. See demo/duffel-mcp-gateway
|
|
723
|
+
// in the AgentSafe repo for the full pattern (mutual handshake, x402 payment, capability
|
|
724
|
+
// binding) this is a minimal slice of.
|
|
725
|
+
|
|
726
|
+
function gatewayServerFile(scope, port, apiBase) {
|
|
727
|
+
return `#!/usr/bin/env node
|
|
728
|
+
// gateway/server.mjs — the REAL enforcement boundary for this agent's tool(s).
|
|
729
|
+
//
|
|
730
|
+
// This is a SEPARATE process from the agent. It holds the tool's real credentials (the agent
|
|
731
|
+
// process never does), and it independently re-verifies every request against this agent's OWN
|
|
732
|
+
// published policy bundle — it does not trust the agent's own guard.guardTool() check. A
|
|
733
|
+
// compromised or dishonest agent calling its own local function gets nothing here, because
|
|
734
|
+
// there is no local function: the tool only runs in this process.
|
|
735
|
+
import http from 'node:http';
|
|
736
|
+
import { createMcpGuard } from '${MCP_GUARD_PKG}';
|
|
737
|
+
import { createHttpGateway } from '${GATEWAY_PKG}';
|
|
738
|
+
|
|
739
|
+
const PORT = Number(process.env.PORT || ${port});
|
|
740
|
+
const MAGP_API = process.env.MAGP_API || '${apiBase}';
|
|
741
|
+
|
|
742
|
+
// --- Your real tool. Real credentials (an airline API key, a payment key, ...) belong ONLY
|
|
743
|
+
// --- here, read from process.env (see .env.example) — never in the agent process.
|
|
744
|
+
async function bookFlight(args) {
|
|
745
|
+
return { pnr: 'PNR-DEMO', ...args };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// One protected route: only a request signed by this agent, for exactly this action, and
|
|
749
|
+
// re-verified against this agent's own mandate/SOP, reaches bookFlight() below.
|
|
750
|
+
const routes = [{ method: 'POST', path: '/book-flight', action: '${scope}' }];
|
|
751
|
+
|
|
752
|
+
// No serviceKey: this minimal gateway only calls verifyRequest() (re-check a signed request),
|
|
753
|
+
// not the mutual-handshake methods, which are the only thing that needs it.
|
|
754
|
+
const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API });
|
|
755
|
+
|
|
756
|
+
const gateway = createHttpGateway({
|
|
757
|
+
guard,
|
|
758
|
+
routes,
|
|
759
|
+
forward: async (req) => {
|
|
760
|
+
let args = {};
|
|
761
|
+
try { args = JSON.parse(req.rawBody?.toString('utf8') || '{}'); } catch { /* empty body */ }
|
|
762
|
+
const result = await bookFlight(args);
|
|
763
|
+
return { status: 200, body: result };
|
|
764
|
+
},
|
|
765
|
+
// This gateway IS the tool, not a proxy in front of one — an unmatched path has nothing to
|
|
766
|
+
// pass through TO. Without this, any path a route doesn't match falls through ungoverned
|
|
767
|
+
// straight to forward() above, which would run bookFlight() with no check at all.
|
|
768
|
+
denyByDefault: true,
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
function readBody(req) {
|
|
772
|
+
return new Promise((resolve, reject) => {
|
|
773
|
+
const chunks = [];
|
|
774
|
+
req.on('data', (c) => chunks.push(c));
|
|
775
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
776
|
+
req.on('error', reject);
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const server = http.createServer(async (req, res) => {
|
|
781
|
+
try {
|
|
782
|
+
const rawBody = await readBody(req);
|
|
783
|
+
const result = await gateway({ method: req.method, path: req.url, headers: req.headers, rawBody });
|
|
784
|
+
const headers = { 'content-type': 'application/json' };
|
|
785
|
+
if (result.governance) headers['x-agentsafe-decision'] = result.governance.decision;
|
|
786
|
+
res.writeHead(result.status, headers);
|
|
787
|
+
res.end(JSON.stringify(result.body ?? {}));
|
|
788
|
+
} catch (err) {
|
|
789
|
+
// Fail CLOSED on any gateway error.
|
|
790
|
+
res.writeHead(502, { 'content-type': 'application/json' });
|
|
791
|
+
res.end(JSON.stringify({ decision: 'block', reasonCode: 'GATEWAY_ERROR', error: String(err?.message ?? err) }));
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
server.listen(PORT, () => {
|
|
796
|
+
console.log('[gateway] listening on :' + PORT + ' -> the only place bookFlight() runs.');
|
|
797
|
+
console.log('[gateway] every request is independently re-verified against this agent\\'s own policy.');
|
|
798
|
+
});
|
|
799
|
+
`;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function gatewayPackageJson(slug) {
|
|
803
|
+
return JSON.stringify(
|
|
804
|
+
{
|
|
805
|
+
name: slug + '-gateway',
|
|
806
|
+
version: '0.1.0',
|
|
807
|
+
private: true,
|
|
808
|
+
type: 'module',
|
|
809
|
+
scripts: { start: 'node server.mjs' },
|
|
810
|
+
dependencies: { [MCP_GUARD_PKG]: MCP_GUARD_VERSION, [GATEWAY_PKG]: GATEWAY_VERSION },
|
|
811
|
+
},
|
|
812
|
+
null,
|
|
813
|
+
2,
|
|
814
|
+
) + '\n';
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function gatewayEnvExample() {
|
|
818
|
+
return `# Real tool credentials belong HERE, read from process.env in server.mjs — never in the
|
|
819
|
+
# agent process one directory up.
|
|
820
|
+
# AIRLINE_API_KEY=
|
|
821
|
+
`;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function gatewayGitignore() {
|
|
825
|
+
return `node_modules/\n.env\n`;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function gatewayReadme(slug, scope, port) {
|
|
829
|
+
return `# ${slug}-gateway
|
|
830
|
+
|
|
831
|
+
This is the **real enforcement boundary** for \`${slug}\`'s tool(s) — not \`../index.mjs\`.
|
|
832
|
+
|
|
833
|
+
## Why this exists
|
|
834
|
+
|
|
835
|
+
\`guard.guardTool()\` in the agent's \`index.mjs\` is a client-side convenience: it gives fast,
|
|
836
|
+
local ALLOW/BLOCK/ESCALATE feedback, but it still runs its handler in the SAME process
|
|
837
|
+
regardless of where that decision came from. Anything able to call the agent's tool function
|
|
838
|
+
directly — a bug, a compromised dependency, a dishonest fork of the agent's own code — gets the
|
|
839
|
+
same result the gate would have given it. That is not a defect in \`guardTool()\`; a cooperative
|
|
840
|
+
in-process check has no counterparty to disagree with a caller that skips it. See \`--harness\`'s
|
|
841
|
+
own README for the same structural point in the free local-demo mode.
|
|
842
|
+
|
|
843
|
+
This process closes that gap by being a **separate** one. The agent has no way to reach into it
|
|
844
|
+
and call \`bookFlight()\` directly, because \`bookFlight()\` doesn't exist in the agent's process —
|
|
845
|
+
it exists only here, and every request that reaches it has already been independently
|
|
846
|
+
re-verified against this agent's OWN published policy bundle, fetched over the network by THIS
|
|
847
|
+
process, not trusted from the agent's say-so.
|
|
848
|
+
|
|
849
|
+
## Run
|
|
850
|
+
|
|
851
|
+
\`\`\`bash
|
|
852
|
+
npm install
|
|
853
|
+
npm start
|
|
854
|
+
\`\`\`
|
|
855
|
+
|
|
856
|
+
Listens on \`:${port}\` by default (\`PORT\` env var to change it — keep \`../index.mjs\`'s
|
|
857
|
+
\`GATEWAY_URL\` in sync if you do).
|
|
858
|
+
|
|
859
|
+
## Add real credentials
|
|
860
|
+
|
|
861
|
+
Edit \`server.mjs\`'s \`bookFlight()\` with your real implementation, reading any credentials it
|
|
862
|
+
needs from \`process.env\` (see \`.env.example\`). Load \`.env\` however you prefer (e.g.
|
|
863
|
+
\`node --env-file=.env server.mjs\`, Node ≥ 20.6) — it is already in \`.gitignore\`. The agent
|
|
864
|
+
directory one level up must never hold these credentials; if it needs to call a DIFFERENT tool,
|
|
865
|
+
add another protected route here rather than adding a local function back in \`index.mjs\`.
|
|
866
|
+
|
|
867
|
+
## Files
|
|
868
|
+
|
|
869
|
+
- \`server.mjs\` — the gateway: one protected route (\`POST /book-flight\`, action \`${scope}\`),
|
|
870
|
+
\`@metamynd/agentsafe-mcp-guard\`'s \`verifyRequest()\` re-checking every request, and the real
|
|
871
|
+
\`bookFlight()\`.
|
|
872
|
+
- \`.env.example\` — where real tool credentials go (copy to \`.env\`, fill in, never commit).
|
|
873
|
+
|
|
874
|
+
## Beyond this minimal slice
|
|
875
|
+
|
|
876
|
+
This gateway only re-verifies a signed request (§9.3/§9.6 of the MAGP spec). It does not do the
|
|
877
|
+
mutual DID handshake, x402 payment binding, or commitment-bound capability tokens that a
|
|
878
|
+
production Service integration would add — see \`@metamynd/agentsafe-mcp-guard\`'s own README for
|
|
879
|
+
those, and \`demo/duffel-mcp-gateway\` in the AgentSafe repo for a full worked example.
|
|
880
|
+
`;
|
|
881
|
+
}
|
|
882
|
+
|
|
451
883
|
function writeFileSafe(dir, name, content, force = false) {
|
|
452
884
|
const p = join(dir, name);
|
|
453
885
|
const exists = existsSync(p);
|
|
@@ -480,16 +912,32 @@ function assertScaffoldTarget(outDir, force) {
|
|
|
480
912
|
);
|
|
481
913
|
}
|
|
482
914
|
|
|
483
|
-
/**
|
|
484
|
-
|
|
915
|
+
/**
|
|
916
|
+
* Write the scaffolded project + print next steps. Shared by the provision and sandbox paths.
|
|
917
|
+
* `withGateway`: scaffold the default two-process shape (agent + ./gateway) — the real
|
|
918
|
+
* enforcement boundary. Off for --sandbox (shared demo identity, never real credentials
|
|
919
|
+
* anyway) and --no-gateway (opt out, e.g. you're already running your own separate gateway).
|
|
920
|
+
*/
|
|
921
|
+
function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox, withGateway, gatewayPort = DEFAULT_GATEWAY_PORT, force = false }) {
|
|
485
922
|
assertScaffoldTarget(outDir, force);
|
|
486
923
|
console.log(`\n ${c.b('Scaffolding')} ${c.dim(outDir)}`);
|
|
487
924
|
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
488
925
|
writeFileSafe(outDir, 'agent.metamynd.json', JSON.stringify(config, null, 2) + '\n', force);
|
|
489
|
-
writeFileSafe(outDir, 'index.mjs', exampleIndex(scope, perTxnMax), force);
|
|
926
|
+
writeFileSafe(outDir, 'index.mjs', withGateway ? exampleIndex(scope, perTxnMax, gatewayPort) : exampleIndexNoGateway(scope, perTxnMax), force);
|
|
490
927
|
writeFileSafe(outDir, 'package.json', examplePackageJson(slug), force);
|
|
491
928
|
writeFileSafe(outDir, '.gitignore', gitignore(), force);
|
|
492
|
-
writeFileSafe(outDir, 'README.md', exampleReadme(slug, scope), force);
|
|
929
|
+
writeFileSafe(outDir, 'README.md', exampleReadme(slug, scope, withGateway, gatewayPort), force);
|
|
930
|
+
|
|
931
|
+
if (withGateway) {
|
|
932
|
+
const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
|
|
933
|
+
const gwDir = join(outDir, 'gateway');
|
|
934
|
+
if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
|
|
935
|
+
writeFileSafe(gwDir, 'server.mjs', gatewayServerFile(scope, gatewayPort, apiBase), force);
|
|
936
|
+
writeFileSafe(gwDir, 'package.json', gatewayPackageJson(slug), force);
|
|
937
|
+
writeFileSafe(gwDir, '.env.example', gatewayEnvExample(), force);
|
|
938
|
+
writeFileSafe(gwDir, '.gitignore', gatewayGitignore(), force);
|
|
939
|
+
writeFileSafe(gwDir, 'README.md', gatewayReadme(slug, scope, gatewayPort), force);
|
|
940
|
+
}
|
|
493
941
|
|
|
494
942
|
const rel = outDir.replace(resolve('.'), '.').replace(/\\/g, '/');
|
|
495
943
|
console.log(`\n${c.green(c.b(' ✓ Done.'))} Your governed agent is ready.\n`);
|
|
@@ -498,7 +946,13 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox, forc
|
|
|
498
946
|
} else if (config.agentKey) {
|
|
499
947
|
console.log(` ${c.yellow('⚠ agent.metamynd.json holds the agent secret key')} — it is gitignored; never commit it.\n`);
|
|
500
948
|
}
|
|
949
|
+
if (withGateway) {
|
|
950
|
+
console.log(` ${c.yellow('⚠ two processes now')} — \`gateway/\` is the real enforcement boundary, not \`index.mjs\`. Read \`gateway/README.md\`.\n`);
|
|
951
|
+
}
|
|
501
952
|
console.log(` Next:`);
|
|
953
|
+
if (withGateway) {
|
|
954
|
+
console.log(c.cyan(` cd ${rel}/gateway && npm install && npm start`) + c.dim(' (separate terminal — start this first)'));
|
|
955
|
+
}
|
|
502
956
|
console.log(c.cyan(` cd ${rel}`));
|
|
503
957
|
console.log(c.cyan(` npm install`));
|
|
504
958
|
// The example runs FOUR attempts. This summary promised three, so the one carrying the
|
|
@@ -524,7 +978,7 @@ async function runSandbox(args) {
|
|
|
524
978
|
console.log(` ${c.green('✓')} sandbox agent ${c.b(config.agentDid)} ${c.dim('(shared test identity)')}`);
|
|
525
979
|
const scope = config.mandate?.scope || 'flight-purchase';
|
|
526
980
|
const perTxnMax = Number(config.perTxnMax) || 500;
|
|
527
|
-
scaffoldProject({ outDir, config, slug: 'metamynd-sandbox', scope, perTxnMax, sandbox: true, force: !!args.force });
|
|
981
|
+
scaffoldProject({ outDir, config, slug: 'metamynd-sandbox', scope, perTxnMax, sandbox: true, withGateway: false, force: !!args.force });
|
|
528
982
|
}
|
|
529
983
|
|
|
530
984
|
// ---------- --harness: a free, local, zero-network governance harness ----------
|
|
@@ -1336,7 +1790,7 @@ async function runClaim(args) {
|
|
|
1336
1790
|
|
|
1337
1791
|
const slug = slugify(state.name || 'metamynd-agent');
|
|
1338
1792
|
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 });
|
|
1793
|
+
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
1794
|
}
|
|
1341
1795
|
|
|
1342
1796
|
// ---------- main ----------
|
|
@@ -1459,7 +1913,7 @@ async function main() {
|
|
|
1459
1913
|
}
|
|
1460
1914
|
|
|
1461
1915
|
// 4. Scaffold + next steps
|
|
1462
|
-
scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox: false, force: !!args.force });
|
|
1916
|
+
scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox: false, withGateway: !args['no-gateway'], gatewayPort: Number(args['gateway-port']) || DEFAULT_GATEWAY_PORT, force: !!args.force });
|
|
1463
1917
|
}
|
|
1464
1918
|
|
|
1465
1919
|
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.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 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"
|