create-metamynd-agent 0.8.0 → 0.10.3
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 +24 -4
- package/index.mjs +171 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -236,6 +236,8 @@ METAMYND_PASSWORD='…' npx create-metamynd-agent --yes …
|
|
|
236
236
|
| `--merchants <a,b>` | — | any |
|
|
237
237
|
| `--byok` | — | generate the keypair locally, provision + prove control |
|
|
238
238
|
| `--public-key <hex>` | — | BYOK with a key you already hold (you prove control yourself) |
|
|
239
|
+
| `--daemon-socket <p>` | — | `--byok` via an already-running agentsafe-signer daemon instead of locally (needs `--daemon-admin-socket` too) |
|
|
240
|
+
| `--daemon-admin-socket <p>` | — | that daemon's admin socket, for `generate-key` |
|
|
239
241
|
| `--out <dir>` | — | `./<agent-slug>` |
|
|
240
242
|
| `--yes`, `-y` | — | non-interactive |
|
|
241
243
|
|
|
@@ -284,10 +286,28 @@ npx create-metamynd-agent --byok --email you@example.com --name "Support Bot"
|
|
|
284
286
|
```
|
|
285
287
|
|
|
286
288
|
Generates an Ed25519 keypair **on your machine**, provisions the agent with only the public key, then
|
|
287
|
-
proves control (signs the one-time challenge → `verify-key`). MetaMynd never sees the private key.
|
|
288
|
-
generated private key is written into `agent.metamynd.json` (gitignored)
|
|
289
|
-
|
|
290
|
-
prints the challenge
|
|
289
|
+
proves control (signs the one-time challenge → `verify-key`). MetaMynd never sees the private key. By
|
|
290
|
+
default the generated private key is written into `agent.metamynd.json` (gitignored) — the same
|
|
291
|
+
process this CLI runs in holds it, at least briefly. Pass `--public-key <hex>` instead to register a
|
|
292
|
+
key you already hold elsewhere — then you complete `verify-key` yourself (the CLI prints the challenge
|
|
293
|
+
+ endpoint).
|
|
294
|
+
|
|
295
|
+
### Keeping the key out of this process entirely (`--daemon-socket`)
|
|
296
|
+
|
|
297
|
+
If you already have an [agentsafe-signer](../agentsafe-signer/README.md) daemon running for this
|
|
298
|
+
agent (`agentsafe-signer start --admin`, per its own install guide), point `--byok` at it instead:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
npx create-metamynd-agent --byok --daemon-socket ./.agentsafe-signer/signer.sock \
|
|
302
|
+
--daemon-admin-socket ./.agentsafe-signer/signer-admin.sock \
|
|
303
|
+
--email you@example.com --name "Support Bot"
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The daemon generates the key and signs the `verify-key` challenge itself — the private key never
|
|
307
|
+
enters this CLI's process at all, not even briefly. `agent.metamynd.json` gets `keyProvider: 'daemon'`
|
|
308
|
+
+ `daemonSocketPath` instead of a plaintext key (see `@metamynd/agentsafe-guard`'s `key-providers.mjs`
|
|
309
|
+
for how the scaffolded guard resolves that). Requires both flags together, and only applies when no
|
|
310
|
+
`--public-key` is given (an external key has nothing for the daemon to generate).
|
|
291
311
|
|
|
292
312
|
## Delegated issuance (`--request` / `--claim`)
|
|
293
313
|
|
package/index.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from
|
|
|
18
18
|
import { join, resolve } from 'node:path';
|
|
19
19
|
import readline from 'node:readline';
|
|
20
20
|
import crypto from 'node:crypto';
|
|
21
|
+
import net from 'node:net';
|
|
21
22
|
|
|
22
23
|
const GUARD_PKG = '@metamynd/agentsafe-guard';
|
|
23
24
|
// Must track the guard's MINOR line, not just its major. On a 0.x package `^0.4.0` means
|
|
@@ -29,13 +30,39 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
|
|
|
29
30
|
// 0.7.0 adds the opt-in `signContext` envelope signature (Tier 1 context-claim binding) —
|
|
30
31
|
// no scaffolded behavior changes (off by default), but the floor must still cover the real
|
|
31
32
|
// current version regardless, per this repo's standing internal-pin invariant.
|
|
32
|
-
|
|
33
|
+
// 0.8.0 adds an optional `currency` scope to the amount-over/cumulative-over atoms
|
|
34
|
+
// (harnessDefaultSop, below, now sets it) — a guard below this version can't evaluate that
|
|
35
|
+
// field, so a scaffolded currency-scoped cap would silently never fire on a currency mismatch.
|
|
36
|
+
// 0.9.0 makes buildSignedRequest() async (the keyProvider seam, docs/design/
|
|
37
|
+
// agent-key-custody-local-signer-daemon-plan.md) — this scaffold's own bookFlightViaGateway/
|
|
38
|
+
// callGateway templates now `await` it, so a guard below this version would hand back a
|
|
39
|
+
// Promise object where a signed request is expected instead of failing loudly.
|
|
40
|
+
// 0.10.0 adds `resource` as a genuinely signed field (mirrors the mandate's own
|
|
41
|
+
// ResourceService.scopeConstraint()) — no scaffolded template passes it yet, but the floor
|
|
42
|
+
// must still cover the real current version regardless, per this repo's standing
|
|
43
|
+
// internal-pin invariant.
|
|
44
|
+
// 0.11.0 adds `signLocalDecision` support to `createDaemonKeyProvider` — this scaffold's
|
|
45
|
+
// `--byok --daemon-socket` path can now get local-decision audit reporting too, but no
|
|
46
|
+
// template code changes yet; the floor must still cover the real current version.
|
|
47
|
+
// 0.12.0 adds passphrase-encrypted managed key delivery (createGuardFromConfig's
|
|
48
|
+
// `{ passphrase }`) — no scaffolded template passes one yet, but the floor must still cover
|
|
49
|
+
// the real current version regardless, per this repo's standing internal-pin invariant.
|
|
50
|
+
const GUARD_VERSION = '^0.12.0';
|
|
33
51
|
// The default hosted scaffold's SECOND process — the tool gateway (see scaffoldProject).
|
|
34
52
|
const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
35
53
|
// 0.2.0 adds requireAuthorization (closes replay + cumulative spend) — this scaffold sets that
|
|
36
54
|
// option, so a range that could resolve below 0.2.0 would silently scaffold a no-op.
|
|
37
55
|
// 0.3.0 adds the same amount-unknown atom as the guard, above — same reasoning, same miss.
|
|
38
|
-
|
|
56
|
+
// 0.4.0 adds the same amount-over/cumulative-over `currency` scope as the guard, above —
|
|
57
|
+
// same reasoning, same miss.
|
|
58
|
+
// 0.5.0 adds the keyProvider seam alongside the guard's own 0.9.0 (same design doc) — this
|
|
59
|
+
// scaffold's createMcpGuard() calls never use a handshake here, so no template code changes,
|
|
60
|
+
// but the floor must still cover the real current version regardless, per this repo's
|
|
61
|
+
// standing internal-pin invariant.
|
|
62
|
+
// 0.6.0 brings buildAuthMessage's `resource` field and buildLocalDecisionMessage into this
|
|
63
|
+
// package's own bundled policy-core.mjs (alongside the guard's own 0.10.0) — no scaffolded
|
|
64
|
+
// template code changes, but the floor must still cover the real current version.
|
|
65
|
+
const MCP_GUARD_VERSION = '^0.6.0';
|
|
39
66
|
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
40
67
|
// 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
|
|
41
68
|
// never scaffold a range that could resolve below it.
|
|
@@ -113,6 +140,13 @@ ${c.b('Options')}
|
|
|
113
140
|
--byok Bring-your-own-key: generate the keypair locally, provision + prove control
|
|
114
141
|
(MetaMynd never sees the private key). Overridden by --public-key.
|
|
115
142
|
--public-key <hex> BYOK with a key you already hold (SPKI/raw hex); you prove control yourself
|
|
143
|
+
--daemon-socket <p> --byok only: use an already-running agentsafe-signer daemon (started
|
|
144
|
+
separately, e.g. \`agentsafe-signer start --admin\`) to generate the key and
|
|
145
|
+
prove control instead — the private key never enters this CLI's process at
|
|
146
|
+
all, and agent.metamynd.json gets keyProvider:'daemon' instead of a
|
|
147
|
+
plaintext key. Requires --daemon-admin-socket too. See README#byok.
|
|
148
|
+
--daemon-admin-socket <p> The same daemon's admin socket (for generate-key) — required with
|
|
149
|
+
--daemon-socket.
|
|
116
150
|
--out <dir> Output project directory (default ./<agent-slug>)
|
|
117
151
|
--no-gateway Hosted flow only: skip the separate tool-gateway process (see
|
|
118
152
|
README#separate-tool-gateway-default) and scaffold the old
|
|
@@ -305,6 +339,72 @@ function signChallengeHex(privateKeyHex, challenge) {
|
|
|
305
339
|
return crypto.sign(null, Buffer.from(challenge, 'utf8'), key).toString('hex');
|
|
306
340
|
}
|
|
307
341
|
|
|
342
|
+
// ---------- BYOK via an already-running agentsafe-signer daemon (--daemon-socket) ----------
|
|
343
|
+
// Opt-in alternative to generateAgentKeypair() above: instead of generating the keypair in THIS
|
|
344
|
+
// process and writing it in plaintext into agent.metamynd.json, ask an already-running
|
|
345
|
+
// agentsafe-signer daemon (docs/design/agent-key-custody-local-signer-daemon-plan.md — started
|
|
346
|
+
// separately, e.g. `agentsafe-signer start --admin`) to generate the key and sign the
|
|
347
|
+
// proof-of-possession challenge. The private key never enters this process at all, and the
|
|
348
|
+
// scaffolded config gets `keyProvider: 'daemon'` instead of a plaintext `agentKey` — see
|
|
349
|
+
// agentsafe-guard/key-providers.mjs's resolveKeyProvider(), which reads that field exactly.
|
|
350
|
+
//
|
|
351
|
+
// Vendored rather than depending on @metamynd/agentsafe-signer or @metamynd/agentsafe-guard for
|
|
352
|
+
// it — this CLI is intentionally zero-dependency, and this is the SAME small, self-contained
|
|
353
|
+
// reimplementation of the daemon's local JSON-over-socket protocol that agentsafe-guard/
|
|
354
|
+
// key-providers.mjs and agentsafe-mcp-guard/key-providers.mjs already each carry their own copy
|
|
355
|
+
// of, rather than a fourth package depending on a signer package built for a persistent service,
|
|
356
|
+
// not a one-shot scaffolding command.
|
|
357
|
+
function toPlatformSocketPath(logicalPath) {
|
|
358
|
+
if (process.platform !== 'win32') return logicalPath;
|
|
359
|
+
const name = crypto.createHash('sha256').update(resolve(logicalPath)).digest('hex').slice(0, 32);
|
|
360
|
+
return `\\\\.\\pipe\\agentsafe-signer-${name}`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function daemonRequest(socketPath, op, params, { connectTimeoutMs = 5000 } = {}) {
|
|
364
|
+
return new Promise((resolve_, reject) => {
|
|
365
|
+
const deadline = Date.now() + connectTimeoutMs;
|
|
366
|
+
let settled = false;
|
|
367
|
+
const overallTimer = setTimeout(() => {
|
|
368
|
+
settled = true;
|
|
369
|
+
reject(Object.assign(new Error(`agentsafe-signer daemon unreachable at ${socketPath}: timed out after ${connectTimeoutMs}ms`), { code: 'DAEMON_UNREACHABLE' }));
|
|
370
|
+
}, connectTimeoutMs);
|
|
371
|
+
function attempt() {
|
|
372
|
+
if (settled) return;
|
|
373
|
+
const sock = net.connect(toPlatformSocketPath(socketPath));
|
|
374
|
+
const requestId = crypto.randomUUID();
|
|
375
|
+
let buf = '';
|
|
376
|
+
const cleanup = () => sock.destroy();
|
|
377
|
+
sock.once('error', (err) => {
|
|
378
|
+
cleanup();
|
|
379
|
+
if (settled) return;
|
|
380
|
+
if (err.code === 'ENOENT' && Date.now() < deadline) { setTimeout(attempt, 20); return; }
|
|
381
|
+
settled = true;
|
|
382
|
+
clearTimeout(overallTimer);
|
|
383
|
+
reject(Object.assign(new Error(`agentsafe-signer daemon unreachable at ${socketPath}: ${err.message}`), { code: 'DAEMON_UNREACHABLE' }));
|
|
384
|
+
});
|
|
385
|
+
sock.once('connect', () => {
|
|
386
|
+
if (settled) return;
|
|
387
|
+
sock.write(JSON.stringify({ protocolVersion: 1, requestId, op, params }) + '\n');
|
|
388
|
+
});
|
|
389
|
+
sock.on('data', (chunk) => {
|
|
390
|
+
if (settled) return;
|
|
391
|
+
buf += chunk.toString('utf8');
|
|
392
|
+
const idx = buf.indexOf('\n');
|
|
393
|
+
if (idx === -1) return;
|
|
394
|
+
let res;
|
|
395
|
+
try { res = JSON.parse(buf.slice(0, idx)); }
|
|
396
|
+
catch (err) { cleanup(); settled = true; clearTimeout(overallTimer); reject(err); return; }
|
|
397
|
+
cleanup();
|
|
398
|
+
settled = true;
|
|
399
|
+
clearTimeout(overallTimer);
|
|
400
|
+
if (res.ok) resolve_(res.result);
|
|
401
|
+
else reject(Object.assign(new Error(res.error?.message || res.error?.code || 'daemon rejected request'), { code: res.error?.code }));
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
attempt();
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
308
408
|
// ---------- API ----------
|
|
309
409
|
async function apiPost(base, path, body, token) {
|
|
310
410
|
let res;
|
|
@@ -517,7 +617,7 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
|
|
|
517
617
|
// --- the gateway atomically claim single-use execution, closing replay + cumulative spend, not
|
|
518
618
|
// --- just re-checking policy. See ./gateway/README.md.
|
|
519
619
|
async function bookFlightViaGateway(args, decision) {
|
|
520
|
-
const signed = guard.buildSignedRequest({
|
|
620
|
+
const signed = await guard.buildSignedRequest({
|
|
521
621
|
action: '${scope}',
|
|
522
622
|
amount: args.amount,
|
|
523
623
|
currency: 'USD',
|
|
@@ -693,7 +793,13 @@ function examplePackageJson(slug) {
|
|
|
693
793
|
) + '\n';
|
|
694
794
|
}
|
|
695
795
|
|
|
696
|
-
function exampleReadme(slug, scope, withGateway, gatewayPort) {
|
|
796
|
+
function exampleReadme(slug, scope, withGateway, gatewayPort, daemonKey = false) {
|
|
797
|
+
const configFileLine = daemonKey
|
|
798
|
+
? `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
|
|
799
|
+
**Holds no secret key.** Signing goes through your already-running agentsafe-signer daemon
|
|
800
|
+
(\`daemonSocketPath\`) instead — see \`docs/integration/INSTALL-AGENTSAFE-SIGNER.md\`.`
|
|
801
|
+
: `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
|
|
802
|
+
**Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.`;
|
|
697
803
|
const gatewaySection = withGateway
|
|
698
804
|
? `## Run
|
|
699
805
|
|
|
@@ -716,8 +822,7 @@ an ESCALATE (high risk). The BLOCK and ESCALATE never reach the gateway at all
|
|
|
716
822
|
|
|
717
823
|
## Files
|
|
718
824
|
|
|
719
|
-
|
|
720
|
-
**Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.
|
|
825
|
+
${configFileLine}
|
|
721
826
|
- \`index.mjs\` — signs each request and calls \`./gateway\` for it; \`guard.guardTool()\` here is a
|
|
722
827
|
fast local pre-check, not the enforcement boundary.
|
|
723
828
|
- \`gateway/\` — a **separate process**. It holds the real tool and independently re-verifies every
|
|
@@ -738,8 +843,7 @@ You should see an ALLOW, a BLOCK (over the per-transaction cap), and an ESCALATE
|
|
|
738
843
|
|
|
739
844
|
## Files
|
|
740
845
|
|
|
741
|
-
|
|
742
|
-
**Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.
|
|
846
|
+
${configFileLine}
|
|
743
847
|
- \`index.mjs\` — wraps a tool with \`guard.guardTool(...)\`; the tool only runs when the gate allows.
|
|
744
848
|
|
|
745
849
|
## What this is not
|
|
@@ -1050,7 +1154,7 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox, with
|
|
|
1050
1154
|
writeFileSafe(outDir, 'index.mjs', withGateway ? exampleIndex(scope, perTxnMax, gatewayPort) : exampleIndexNoGateway(scope, perTxnMax), force);
|
|
1051
1155
|
writeFileSafe(outDir, 'package.json', examplePackageJson(slug), force);
|
|
1052
1156
|
writeFileSafe(outDir, '.gitignore', gitignore(), force);
|
|
1053
|
-
writeFileSafe(outDir, 'README.md', exampleReadme(slug, scope, withGateway, gatewayPort), force);
|
|
1157
|
+
writeFileSafe(outDir, 'README.md', exampleReadme(slug, scope, withGateway, gatewayPort, config.keyProvider === 'daemon'), force);
|
|
1054
1158
|
|
|
1055
1159
|
if (withGateway) {
|
|
1056
1160
|
const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
|
|
@@ -1067,6 +1171,8 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox, with
|
|
|
1067
1171
|
console.log(`\n${c.green(c.b(' ✓ Done.'))} Your governed agent is ready.\n`);
|
|
1068
1172
|
if (sandbox) {
|
|
1069
1173
|
console.log(` ${c.dim('Shared sandbox agent — for trying MetaMynd only. Provision your own (drop --sandbox) for anything real.')}\n`);
|
|
1174
|
+
} else if (config.keyProvider === 'daemon') {
|
|
1175
|
+
console.log(` ${c.dim('agent.metamynd.json holds no secret key — signing goes through your agentsafe-signer daemon at')} ${c.b(config.daemonSocketPath)}${c.dim('.')}\n`);
|
|
1070
1176
|
} else if (config.agentKey) {
|
|
1071
1177
|
console.log(` ${c.yellow('⚠ agent.metamynd.json holds the agent secret key')} — it is gitignored; never commit it.\n`);
|
|
1072
1178
|
}
|
|
@@ -1128,12 +1234,21 @@ async function runSandbox(args) {
|
|
|
1128
1234
|
* entirely client-side with no schema boundary in front of it, so nothing stops a caller from
|
|
1129
1235
|
* passing amount: "5000" (a string) or omitting amount entirely — `amount-over` silently does
|
|
1130
1236
|
* not fire on either (`typeof c.amount === 'number'` is false), so the cap passes untested,
|
|
1131
|
-
* not safe. Ordering amount-unknown first blocks that instead of letting it through.
|
|
1132
|
-
|
|
1237
|
+
* not safe. Ordering amount-unknown first blocks that instead of letting it through.
|
|
1238
|
+
*
|
|
1239
|
+
* The per-transaction cap is scoped to `currency` (atom-catalog.ts's `amount-over` currency
|
|
1240
|
+
* config), mirroring the hosted default exactly — see mandate-eval.ts for why an
|
|
1241
|
+
* unscoped numeric cap can be cleared just by naming a different currency. The atom's
|
|
1242
|
+
* `currency` config is declared `type: 'string[]'` (always an array, e.g. `['USD']`, never
|
|
1243
|
+
* a bare string — see atom-catalog.ts's own field doc); `harnessMandate` below correctly
|
|
1244
|
+
* passes the bare string straight through to the ODRL `unit` field, which is a DIFFERENT,
|
|
1245
|
+
* genuinely string-or-array field — the two must not be confused (see the backend's own
|
|
1246
|
+
* currencyScopeFor/currencyUnitFor split in currency-unit.ts for the same distinction). */
|
|
1247
|
+
function harnessDefaultSop(perTxnMax, currency) {
|
|
1133
1248
|
return {
|
|
1134
1249
|
molecules: [
|
|
1135
1250
|
{ id: 'amount-known', name: 'Amount must be determinable', combinator: 'any', atoms: [{ id: 'a0', predicate: 'amount-unknown' }], decision: 'block', reasonCode: 'AMOUNT_NOT_DETERMINABLE' },
|
|
1136
|
-
{ id: 'cap', name: 'Per-transaction cap', combinator: 'any', atoms: [{ id: 'a1', predicate: 'amount-over', config: { limit: perTxnMax } }], decision: 'block', reasonCode: 'SOP_SPEND_CAP' },
|
|
1251
|
+
{ id: 'cap', name: 'Per-transaction cap', combinator: 'any', atoms: [{ id: 'a1', predicate: 'amount-over', config: { limit: perTxnMax, currency: [currency] } }], decision: 'block', reasonCode: 'SOP_SPEND_CAP' },
|
|
1137
1252
|
{ id: 'review', name: 'High-risk review', combinator: 'any', atoms: [{ id: 'a2', predicate: 'risk-at-or-above', config: { level: 'high' } }], decision: 'escalate', reasonCode: 'RISK_REVIEW' },
|
|
1138
1253
|
],
|
|
1139
1254
|
};
|
|
@@ -1716,7 +1831,7 @@ const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://localhost:${gatewayPo
|
|
|
1716
1831
|
// issuer): it builds and signs the same canonical message a real gate would verify, entirely
|
|
1717
1832
|
// offline, using this agent's own did:key — the gateway verifies that signature for itself.
|
|
1718
1833
|
async function callGateway(path, action, args) {
|
|
1719
|
-
const signed = guard.buildSignedRequest({ action, amount: args.amount, currency: 'USD', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' } });
|
|
1834
|
+
const signed = await guard.buildSignedRequest({ action, amount: args.amount, currency: 'USD', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' } });
|
|
1720
1835
|
const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ signed, args }) });
|
|
1721
1836
|
const body = await res.json().catch(() => null);
|
|
1722
1837
|
if (!res.ok) {
|
|
@@ -1988,7 +2103,7 @@ async function runHarness(args) {
|
|
|
1988
2103
|
console.log(` ${c.green('✓')} local agent ${c.b(agentDid)}`);
|
|
1989
2104
|
|
|
1990
2105
|
const sopFields = configFileSopFields(fileConfig);
|
|
1991
|
-
const sopDocument = sopFields.sop ? sopFields.sop.documentJson : harnessDefaultSop(perTxnMax);
|
|
2106
|
+
const sopDocument = sopFields.sop ? sopFields.sop.documentJson : harnessDefaultSop(perTxnMax, currency);
|
|
1992
2107
|
if (sopFields.sop) console.log(` ${c.green('✓')} compiled ${sopDocument.molecules.length} rule(s) from the config file`);
|
|
1993
2108
|
const mandate = harnessMandate({ scope, currency, maxAmount, perTxnMax, merchants });
|
|
1994
2109
|
|
|
@@ -2210,11 +2325,35 @@ async function main() {
|
|
|
2210
2325
|
);
|
|
2211
2326
|
const merchants = String(merchantsRaw).split(',').map((s) => s.trim()).filter(Boolean);
|
|
2212
2327
|
|
|
2213
|
-
// BYOK: --byok generates a keypair on THIS machine (MetaMynd never sees the private key)
|
|
2214
|
-
//
|
|
2328
|
+
// BYOK: --byok generates a keypair on THIS machine (MetaMynd never sees the private key) —
|
|
2329
|
+
// either locally in this process (default) or, opt-in, via an already-running agentsafe-signer
|
|
2330
|
+
// daemon (--daemon-socket + --daemon-admin-socket, see their own help text) so the private key
|
|
2331
|
+
// never enters this process at all. An explicit --public-key means the caller holds the key
|
|
2332
|
+
// elsewhere and will prove it themselves — daemon flags are meaningless with it.
|
|
2215
2333
|
let publicKey = typeof args['public-key'] === 'string' ? args['public-key'] : undefined;
|
|
2334
|
+
const daemonSocket = typeof args['daemon-socket'] === 'string' ? args['daemon-socket'] : undefined;
|
|
2335
|
+
const daemonAdminSocket = typeof args['daemon-admin-socket'] === 'string' ? args['daemon-admin-socket'] : undefined;
|
|
2336
|
+
if (Boolean(daemonSocket) !== Boolean(daemonAdminSocket)) {
|
|
2337
|
+
rl?.close();
|
|
2338
|
+
fail('--daemon-socket and --daemon-admin-socket must be used together.');
|
|
2339
|
+
}
|
|
2340
|
+
if (daemonSocket && !args.byok) {
|
|
2341
|
+
rl?.close();
|
|
2342
|
+
fail('--daemon-socket requires --byok.');
|
|
2343
|
+
}
|
|
2344
|
+
if (daemonSocket && publicKey) {
|
|
2345
|
+
rl?.close();
|
|
2346
|
+
fail('--daemon-socket generates its own key — pass --byok alone, not --public-key.');
|
|
2347
|
+
}
|
|
2216
2348
|
let generatedKey = null;
|
|
2217
|
-
|
|
2349
|
+
let daemonPublicKeyHex = null;
|
|
2350
|
+
if (args.byok && !publicKey && daemonSocket) {
|
|
2351
|
+
console.log(c.dim(' → asking the agentsafe-signer daemon to generate a key …'));
|
|
2352
|
+
const { publicKeyHex } = await daemonRequest(daemonAdminSocket, 'generate-key', { allowRekey: false });
|
|
2353
|
+
daemonPublicKeyHex = publicKeyHex;
|
|
2354
|
+
publicKey = publicKeyHex;
|
|
2355
|
+
console.log(` ${c.green('✓')} generated an Ed25519 keypair via the signer daemon ${c.dim('(the private key never left it)')}`);
|
|
2356
|
+
} else if (args.byok && !publicKey) {
|
|
2218
2357
|
generatedKey = generateAgentKeypair();
|
|
2219
2358
|
publicKey = generatedKey.publicKeyHex;
|
|
2220
2359
|
console.log(` ${c.green('✓')} generated an Ed25519 keypair locally ${c.dim('(private key stays on this machine)')}`);
|
|
@@ -2239,7 +2378,21 @@ async function main() {
|
|
|
2239
2378
|
if (config.standards?.length) console.log(` ${c.green('✓')} enforced Standards: ${config.standards.join(', ')}`);
|
|
2240
2379
|
|
|
2241
2380
|
// 3b. BYOK: prove control of the key (verify-key), else the gate blocks with AGENT_KEY_UNVERIFIED.
|
|
2242
|
-
if (
|
|
2381
|
+
if (daemonPublicKeyHex) {
|
|
2382
|
+
// The daemon holds the private key — it never entered this process. Point the scaffolded
|
|
2383
|
+
// guard at the daemon instead of embedding a plaintext key (agentsafe-guard/key-providers.mjs's
|
|
2384
|
+
// resolveKeyProvider() reads these two fields and never looks for `agentKey` when present).
|
|
2385
|
+
config.keyProvider = 'daemon';
|
|
2386
|
+
config.daemonSocketPath = daemonSocket;
|
|
2387
|
+
if (config.challenge) {
|
|
2388
|
+
console.log(c.dim(' → proving key control via the daemon (verify-key) …'));
|
|
2389
|
+
const { signature } = await daemonRequest(daemonSocket, 'sign-key-control-challenge', { challenge: config.challenge });
|
|
2390
|
+
await apiPost(base, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature }, token);
|
|
2391
|
+
config.keyVerified = true;
|
|
2392
|
+
delete config.challenge; // one-time; consumed
|
|
2393
|
+
console.log(` ${c.green('✓')} key verified — MetaMynd never saw your private key, and neither did this CLI`);
|
|
2394
|
+
}
|
|
2395
|
+
} else if (generatedKey) {
|
|
2243
2396
|
// We hold the private key — inject it into the config so the scaffolded guard can sign, and
|
|
2244
2397
|
// prove possession by signing the issued challenge.
|
|
2245
2398
|
config.agentKey = generatedKey.privateKeyHex;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Scaffold a MetaMynd/AgentSafe-governed AI agent in one command — logs in, provisions the agent (identity + mandate + SOP + Standards) in a single call, writes agent.metamynd.json plus a runnable agent + separate tool-gateway process that closes direct-call, confused-deputy, replay, and cumulative-spend bypasses. --harness scaffolds a free, local, zero-network governance harness instead.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-metamynd-agent": "index.mjs"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "node harness-gateway.smoke.mjs"
|
|
10
|
+
"test": "node harness-gateway.smoke.mjs && node byok-daemon.smoke.mjs"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"index.mjs",
|