create-metamynd-agent 0.9.1 → 0.10.5

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.
Files changed (3) hide show
  1. package/README.md +32 -6
  2. package/index.mjs +201 -51
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -104,7 +104,11 @@ Answer a few prompts (API, owner email/password, agent name, scope, per-transact
104
104
 
105
105
  ```
106
106
  my-agent/
107
- ├─ agent.metamynd.json # portable guard config — HOLDS THE AGENT SECRET KEY (gitignored)
107
+ ├─ agent.metamynd.json # portable guard config — HOLDS THE AGENT SECRET KEY (gitignored). This
108
+ │ # is the freshly-minted key from THIS scaffold, not a re-download — the
109
+ │ # dashboard's own "Redownload config" for an EXISTING agent never re-issues
110
+ │ # the key into a downloaded file (it's excluded there by design). --byok /
111
+ │ # --daemon-socket instead keep the key off this CLI's process entirely.
108
112
  ├─ index.mjs # runnable example: signs + calls ./gateway; guardTool() here is a
109
113
  │ # fast local pre-check, NOT the enforcement boundary
110
114
  ├─ package.json # depends on @metamynd/agentsafe-guard
@@ -220,10 +224,12 @@ METAMYND_PASSWORD='…' npx create-metamynd-agent --yes …
220
224
  | Flag | Env | Default |
221
225
  |---|---|---|
222
226
  | `--harness` | — | off (no login/KYB/network at all; free local governance — see above) |
227
+ | `--gateway` | — | off — `--harness` only; ALSO scaffold a second local process (still zero network, zero account) that independently re-verifies every request via the real `@metamynd/agentsafe-mcp-guard`. Does not close nonce replay/cumulative spend — see the generated `harness-gateway/README.md#--gateway`. |
223
228
  | `--sandbox` | — | off (skips login/KYB; shared sandbox agent, still hosted) |
224
229
  | `--config <file>` | — | a JSON policy file — see [Policy config file](#policy-config-file---config) |
225
230
  | `--no-gateway` | — | off — hosted flow only; skips the default separate tool gateway (see above) |
226
- | `--gateway-port <n>` | — | `4401` — hosted flow only, the gateway process's port |
231
+ | `--gateway-port <n>` | — | `4401` — hosted flow or `--harness --gateway`, the gateway process's port |
232
+ | `--force`, `-f` | — | off — scaffold into a non-empty directory, overwriting existing files |
227
233
  | `--port <n>` | — | `4400` — `--harness` only, the local dashboard's port |
228
234
  | `--api <url>` | `METAMYND_API` | `https://metamynd.ai/api/v1` |
229
235
  | `--email <email>` | `METAMYND_EMAIL` | — (required) |
@@ -236,6 +242,8 @@ METAMYND_PASSWORD='…' npx create-metamynd-agent --yes …
236
242
  | `--merchants <a,b>` | — | any |
237
243
  | `--byok` | — | generate the keypair locally, provision + prove control |
238
244
  | `--public-key <hex>` | — | BYOK with a key you already hold (you prove control yourself) |
245
+ | `--daemon-socket <p>` | — | `--byok` via an already-running agentsafe-signer daemon instead of locally (needs `--daemon-admin-socket` too) |
246
+ | `--daemon-admin-socket <p>` | — | that daemon's admin socket, for `generate-key` |
239
247
  | `--out <dir>` | — | `./<agent-slug>` |
240
248
  | `--yes`, `-y` | — | non-interactive |
241
249
 
@@ -284,10 +292,28 @@ npx create-metamynd-agent --byok --email you@example.com --name "Support Bot"
284
292
  ```
285
293
 
286
294
  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. The
288
- generated private key is written into `agent.metamynd.json` (gitignored). Pass `--public-key <hex>`
289
- instead to register a key you already hold elsewhere then you complete `verify-key` yourself (the CLI
290
- prints the challenge + endpoint).
295
+ proves control (signs the one-time challenge → `verify-key`). MetaMynd never sees the private key. By
296
+ default the generated private key is written into `agent.metamynd.json` (gitignored) the same
297
+ process this CLI runs in holds it, at least briefly. Pass `--public-key <hex>` instead to register a
298
+ key you already hold elsewhere — then you complete `verify-key` yourself (the CLI prints the challenge
299
+ + endpoint).
300
+
301
+ ### Keeping the key out of this process entirely (`--daemon-socket`)
302
+
303
+ If you already have an [agentsafe-signer](../agentsafe-signer/README.md) daemon running for this
304
+ agent (`agentsafe-signer start --admin`, per its own install guide), point `--byok` at it instead:
305
+
306
+ ```bash
307
+ npx create-metamynd-agent --byok --daemon-socket ./.agentsafe-signer/signer.sock \
308
+ --daemon-admin-socket ./.agentsafe-signer/signer-admin.sock \
309
+ --email you@example.com --name "Support Bot"
310
+ ```
311
+
312
+ The daemon generates the key and signs the `verify-key` challenge itself — the private key never
313
+ enters this CLI's process at all, not even briefly. `agent.metamynd.json` gets `keyProvider: 'daemon'`
314
+ + `daemonSocketPath` instead of a plaintext key (see `@metamynd/agentsafe-guard`'s `key-providers.mjs`
315
+ for how the scaffolded guard resolves that). Requires both flags together, and only applies when no
316
+ `--public-key` is given (an external key has nothing for the daemon to generate).
291
317
 
292
318
  ## Delegated issuance (`--request` / `--claim`)
293
319
 
package/index.mjs CHANGED
@@ -14,10 +14,11 @@
14
14
  // npx create-metamynd-agent
15
15
  // npx create-metamynd-agent --api http://localhost:9926/api/v1 --email you@x.com \
16
16
  // --name "Support Bot" --scope flight-purchase --per-txn-max 500 --out ./support-bot --yes
17
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
17
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, chmodSync } from 'node:fs';
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
@@ -32,7 +33,21 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
32
33
  // 0.8.0 adds an optional `currency` scope to the amount-over/cumulative-over atoms
33
34
  // (harnessDefaultSop, below, now sets it) — a guard below this version can't evaluate that
34
35
  // field, so a scaffolded currency-scoped cap would silently never fire on a currency mismatch.
35
- const GUARD_VERSION = '^0.8.0';
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';
36
51
  // The default hosted scaffold's SECOND process — the tool gateway (see scaffoldProject).
37
52
  const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
38
53
  // 0.2.0 adds requireAuthorization (closes replay + cumulative spend) — this scaffold sets that
@@ -40,7 +55,14 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
40
55
  // 0.3.0 adds the same amount-unknown atom as the guard, above — same reasoning, same miss.
41
56
  // 0.4.0 adds the same amount-over/cumulative-over `currency` scope as the guard, above —
42
57
  // same reasoning, same miss.
43
- const MCP_GUARD_VERSION = '^0.4.0';
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';
44
66
  const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
45
67
  // 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
46
68
  // never scaffold a range that could resolve below it.
@@ -49,7 +71,10 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
49
71
  // is the actual fix: requires amount/merchant specifically, whenever the signature names a real
50
72
  // value for them. Re-tested live and closed same day; ^0.3.0 here would still resolve to the
51
73
  // broken version.
52
- const GATEWAY_VERSION = '^0.4.0';
74
+ // 0.5.0 adds the OPTIONAL Credential Vault `resolveCredential` hook on createHttpGateway (Module
75
+ // G) — additive and backward-compatible (every existing consumer sees zero behavior change), but
76
+ // the floor must still cover the real current version per this repo's own package-version check.
77
+ const GATEWAY_VERSION = '^0.5.0';
53
78
  const DEFAULT_API = 'https://metamynd.ai/api/v1';
54
79
  const DEFAULT_GATEWAY_PORT = 4401; // distinct from --harness's dashboard (4400)
55
80
 
@@ -118,6 +143,13 @@ ${c.b('Options')}
118
143
  --byok Bring-your-own-key: generate the keypair locally, provision + prove control
119
144
  (MetaMynd never sees the private key). Overridden by --public-key.
120
145
  --public-key <hex> BYOK with a key you already hold (SPKI/raw hex); you prove control yourself
146
+ --daemon-socket <p> --byok only: use an already-running agentsafe-signer daemon (started
147
+ separately, e.g. \`agentsafe-signer start --admin\`) to generate the key and
148
+ prove control instead — the private key never enters this CLI's process at
149
+ all, and agent.metamynd.json gets keyProvider:'daemon' instead of a
150
+ plaintext key. Requires --daemon-admin-socket too. See README#byok.
151
+ --daemon-admin-socket <p> The same daemon's admin socket (for generate-key) — required with
152
+ --daemon-socket.
121
153
  --out <dir> Output project directory (default ./<agent-slug>)
122
154
  --no-gateway Hosted flow only: skip the separate tool-gateway process (see
123
155
  README#separate-tool-gateway-default) and scaffold the old
@@ -310,6 +342,72 @@ function signChallengeHex(privateKeyHex, challenge) {
310
342
  return crypto.sign(null, Buffer.from(challenge, 'utf8'), key).toString('hex');
311
343
  }
312
344
 
345
+ // ---------- BYOK via an already-running agentsafe-signer daemon (--daemon-socket) ----------
346
+ // Opt-in alternative to generateAgentKeypair() above: instead of generating the keypair in THIS
347
+ // process and writing it in plaintext into agent.metamynd.json, ask an already-running
348
+ // agentsafe-signer daemon (docs/design/agent-key-custody-local-signer-daemon-plan.md — started
349
+ // separately, e.g. `agentsafe-signer start --admin`) to generate the key and sign the
350
+ // proof-of-possession challenge. The private key never enters this process at all, and the
351
+ // scaffolded config gets `keyProvider: 'daemon'` instead of a plaintext `agentKey` — see
352
+ // agentsafe-guard/key-providers.mjs's resolveKeyProvider(), which reads that field exactly.
353
+ //
354
+ // Vendored rather than depending on @metamynd/agentsafe-signer or @metamynd/agentsafe-guard for
355
+ // it — this CLI is intentionally zero-dependency, and this is the SAME small, self-contained
356
+ // reimplementation of the daemon's local JSON-over-socket protocol that agentsafe-guard/
357
+ // key-providers.mjs and agentsafe-mcp-guard/key-providers.mjs already each carry their own copy
358
+ // of, rather than a fourth package depending on a signer package built for a persistent service,
359
+ // not a one-shot scaffolding command.
360
+ function toPlatformSocketPath(logicalPath) {
361
+ if (process.platform !== 'win32') return logicalPath;
362
+ const name = crypto.createHash('sha256').update(resolve(logicalPath)).digest('hex').slice(0, 32);
363
+ return `\\\\.\\pipe\\agentsafe-signer-${name}`;
364
+ }
365
+
366
+ function daemonRequest(socketPath, op, params, { connectTimeoutMs = 5000 } = {}) {
367
+ return new Promise((resolve_, reject) => {
368
+ const deadline = Date.now() + connectTimeoutMs;
369
+ let settled = false;
370
+ const overallTimer = setTimeout(() => {
371
+ settled = true;
372
+ reject(Object.assign(new Error(`agentsafe-signer daemon unreachable at ${socketPath}: timed out after ${connectTimeoutMs}ms`), { code: 'DAEMON_UNREACHABLE' }));
373
+ }, connectTimeoutMs);
374
+ function attempt() {
375
+ if (settled) return;
376
+ const sock = net.connect(toPlatformSocketPath(socketPath));
377
+ const requestId = crypto.randomUUID();
378
+ let buf = '';
379
+ const cleanup = () => sock.destroy();
380
+ sock.once('error', (err) => {
381
+ cleanup();
382
+ if (settled) return;
383
+ if (err.code === 'ENOENT' && Date.now() < deadline) { setTimeout(attempt, 20); return; }
384
+ settled = true;
385
+ clearTimeout(overallTimer);
386
+ reject(Object.assign(new Error(`agentsafe-signer daemon unreachable at ${socketPath}: ${err.message}`), { code: 'DAEMON_UNREACHABLE' }));
387
+ });
388
+ sock.once('connect', () => {
389
+ if (settled) return;
390
+ sock.write(JSON.stringify({ protocolVersion: 1, requestId, op, params }) + '\n');
391
+ });
392
+ sock.on('data', (chunk) => {
393
+ if (settled) return;
394
+ buf += chunk.toString('utf8');
395
+ const idx = buf.indexOf('\n');
396
+ if (idx === -1) return;
397
+ let res;
398
+ try { res = JSON.parse(buf.slice(0, idx)); }
399
+ catch (err) { cleanup(); settled = true; clearTimeout(overallTimer); reject(err); return; }
400
+ cleanup();
401
+ settled = true;
402
+ clearTimeout(overallTimer);
403
+ if (res.ok) resolve_(res.result);
404
+ else reject(Object.assign(new Error(res.error?.message || res.error?.code || 'daemon rejected request'), { code: res.error?.code }));
405
+ });
406
+ }
407
+ attempt();
408
+ });
409
+ }
410
+
313
411
  // ---------- API ----------
314
412
  async function apiPost(base, path, body, token) {
315
413
  let res;
@@ -343,7 +441,7 @@ async function apiPost(base, path, body, token) {
343
441
  * it — the same shape of gap --harness's README documents. See exampleIndex() below, which is
344
442
  * what the real (non-sandbox) flow scaffolds by default instead.
345
443
  */
346
- function exampleIndexNoGateway(scope, perTxnMax) {
444
+ function exampleIndexNoGateway(scope, perTxnMax, currency, merchant) {
347
445
  const under = Math.max(1, Math.round(perTxnMax * 0.5));
348
446
  const over = Math.round(perTxnMax + 100);
349
447
  return `// index.mjs — your agent, governed by MetaMynd/AgentSafe.
@@ -369,7 +467,7 @@ const gatedBookFlight = guard.guardTool(
369
467
  bookFlight,
370
468
  (a) => ({ // map tool args → gate inputs
371
469
  amount: a.amount,
372
- currency: 'USD',
470
+ currency: '${currency}',
373
471
  merchant: a.merchant,
374
472
  context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
375
473
  }),
@@ -386,7 +484,7 @@ const gatedRaiseOwnLimit = guard.guardTool(
386
484
  raiseOwnLimit,
387
485
  (a) => ({
388
486
  amount: a.amount,
389
- currency: 'USD',
487
+ currency: '${currency}',
390
488
  merchant: a.merchant,
391
489
  context: { tool: 'permissions-update' },
392
490
  }),
@@ -399,7 +497,7 @@ const rule = (n) => ' ' + '-'.repeat(n);
399
497
  // Plain-English meaning for the reason codes this demo can produce.
400
498
  const WHY = {
401
499
  AUTHORIZED: 'inside the mandate and under the SOP spend cap',
402
- SOP_SPEND_CAP: 'your SOP caps a single transaction at $${perTxnMax}',
500
+ SOP_SPEND_CAP: 'your SOP caps a single transaction at ${currency} ${perTxnMax}',
403
501
  RISK_REVIEW: 'your SOP sends high-risk actions to a human first',
404
502
  MERCHANT_NOT_ALLOWED: 'the mandate lists which merchants this agent may pay',
405
503
  // Both say the same thing from where you are standing: the mandate does not cover that
@@ -430,7 +528,7 @@ console.log(dim(' 4. each attempt is signed here, then decided by MetaMynd rem
430
528
  console.log(dim(' 5. your tool runs ONLY if that decision is ALLOW'));
431
529
  console.log('');
432
530
  console.log(dim(' scope ${scope}'));
433
- console.log(dim(' cap $${perTxnMax} per transaction, set by your SOP'));
531
+ console.log(dim(' cap ${currency} ${perTxnMax} per transaction, set by your SOP'));
434
532
 
435
533
  // ---------------------------------------------------------------- 3. THE STEPS
436
534
  async function attempt(n, intent, args, tool = gatedBookFlight) {
@@ -458,13 +556,13 @@ async function attempt(n, intent, args, tool = gatedBookFlight) {
458
556
 
459
557
  console.log('');
460
558
  console.log(rule(66));
461
- await attempt(1, 'a $${under} booking, low risk. Expected to pass.', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'low' });
462
- await attempt(2, 'a $${over} booking, deliberately over the cap.', { amount: ${over}, merchant: 'skyward-air', riskLevel: 'low' });
463
- await attempt(3, 'a $${under} booking, but flagged high risk.', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'high' });
559
+ await attempt(1, 'a ${currency} ${under} booking, low risk. Expected to pass.', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
560
+ await attempt(2, 'a ${currency} ${over} booking, deliberately over the cap.', { amount: ${over}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
561
+ await attempt(3, 'a ${currency} ${under} booking, but flagged high risk.', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'high' });
464
562
  await attempt(
465
563
  4,
466
564
  'the agent stops booking flights and asks to raise its OWN limit.',
467
- { amount: 100000, merchant: 'skyward-air' },
565
+ { amount: 100000, currency: '${currency}', merchant: '${merchant}' },
468
566
  gatedRaiseOwnLimit,
469
567
  );
470
568
  console.log('');
@@ -500,7 +598,7 @@ console.log('');
500
598
  * directly: it only exists in ./gateway, which independently re-verifies every request against
501
599
  * this agent's own policy bundle before it runs, and holds any real credentials the tool needs.
502
600
  */
503
- function exampleIndex(scope, perTxnMax, gatewayPort) {
601
+ function exampleIndex(scope, perTxnMax, gatewayPort, currency, merchant) {
504
602
  const under = Math.max(1, Math.round(perTxnMax * 0.5));
505
603
  const over = Math.round(perTxnMax + 100);
506
604
  return `// index.mjs — your agent, governed by MetaMynd/AgentSafe.
@@ -522,10 +620,10 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
522
620
  // --- the gateway atomically claim single-use execution, closing replay + cumulative spend, not
523
621
  // --- just re-checking policy. See ./gateway/README.md.
524
622
  async function bookFlightViaGateway(args, decision) {
525
- const signed = guard.buildSignedRequest({
623
+ const signed = await guard.buildSignedRequest({
526
624
  action: '${scope}',
527
625
  amount: args.amount,
528
- currency: 'USD',
626
+ currency: args.currency ?? '${currency}',
529
627
  merchant: args.merchant,
530
628
  context: { tool: 'book-flight', riskLevel: args.riskLevel ?? 'low' },
531
629
  });
@@ -552,7 +650,7 @@ const gatedBookFlight = guard.guardTool(
552
650
  bookFlightViaGateway,
553
651
  (a) => ({ // map tool args → gate inputs
554
652
  amount: a.amount,
555
- currency: 'USD',
653
+ currency: a.currency ?? '${currency}',
556
654
  merchant: a.merchant,
557
655
  context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
558
656
  }),
@@ -569,7 +667,7 @@ const gatedRaiseOwnLimit = guard.guardTool(
569
667
  raiseOwnLimit,
570
668
  (a) => ({
571
669
  amount: a.amount,
572
- currency: 'USD',
670
+ currency: a.currency ?? '${currency}',
573
671
  merchant: a.merchant,
574
672
  context: { tool: 'permissions-update' },
575
673
  }),
@@ -616,7 +714,7 @@ console.log(dim(' 5. the gateway independently re-verifies before your tool ru
616
714
  console.log(dim(' 6. there is no local bookFlight() to call directly - only the gateway has it'));
617
715
  console.log('');
618
716
  console.log(dim(' scope ${scope}'));
619
- console.log(dim(' cap $${perTxnMax} per transaction, set by your SOP'));
717
+ console.log(dim(' cap ${currency} ${perTxnMax} per transaction, set by your SOP'));
620
718
  console.log(dim(' gateway ' + GATEWAY + ' (run it in a separate terminal - see ./gateway)'));
621
719
 
622
720
  // ---------------------------------------------------------------- 3. THE STEPS
@@ -645,13 +743,13 @@ async function attempt(n, intent, args, tool = gatedBookFlight) {
645
743
 
646
744
  console.log('');
647
745
  console.log(rule(66));
648
- await attempt(1, 'a $${under} booking, low risk. Expected to pass.', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'low' });
649
- await attempt(2, 'a $${over} booking, deliberately over the cap.', { amount: ${over}, merchant: 'skyward-air', riskLevel: 'low' });
650
- await attempt(3, 'a $${under} booking, but flagged high risk.', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'high' });
746
+ await attempt(1, 'a ${currency} ${under} booking, low risk. Expected to pass.', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
747
+ await attempt(2, 'a ${currency} ${over} booking, deliberately over the cap.', { amount: ${over}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
748
+ await attempt(3, 'a ${currency} ${under} booking, but flagged high risk.', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'high' });
651
749
  await attempt(
652
750
  4,
653
751
  'the agent stops booking flights and asks to raise its OWN limit.',
654
- { amount: 100000, merchant: 'skyward-air' },
752
+ { amount: 100000, currency: '${currency}', merchant: '${merchant}' },
655
753
  gatedRaiseOwnLimit,
656
754
  );
657
755
  console.log('');
@@ -698,7 +796,13 @@ function examplePackageJson(slug) {
698
796
  ) + '\n';
699
797
  }
700
798
 
701
- function exampleReadme(slug, scope, withGateway, gatewayPort) {
799
+ function exampleReadme(slug, scope, withGateway, gatewayPort, daemonKey = false) {
800
+ const configFileLine = daemonKey
801
+ ? `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
802
+ **Holds no secret key.** Signing goes through your already-running agentsafe-signer daemon
803
+ (\`daemonSocketPath\`) instead — see \`docs/integration/INSTALL-AGENTSAFE-SIGNER.md\`.`
804
+ : `- \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
805
+ **Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.`;
702
806
  const gatewaySection = withGateway
703
807
  ? `## Run
704
808
 
@@ -721,8 +825,7 @@ an ESCALATE (high risk). The BLOCK and ESCALATE never reach the gateway at all
721
825
 
722
826
  ## Files
723
827
 
724
- - \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
725
- **Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.
828
+ ${configFileLine}
726
829
  - \`index.mjs\` — signs each request and calls \`./gateway\` for it; \`guard.guardTool()\` here is a
727
830
  fast local pre-check, not the enforcement boundary.
728
831
  - \`gateway/\` — a **separate process**. It holds the real tool and independently re-verifies every
@@ -743,8 +846,7 @@ You should see an ALLOW, a BLOCK (over the per-transaction cap), and an ESCALATE
743
846
 
744
847
  ## Files
745
848
 
746
- - \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
747
- **Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.
849
+ ${configFileLine}
748
850
  - \`index.mjs\` — wraps a tool with \`guard.guardTool(...)\`; the tool only runs when the gate allows.
749
851
 
750
852
  ## What this is not
@@ -1009,11 +1111,16 @@ and \`demo/duffel-mcp-gateway\` in the AgentSafe repo for the fuller pattern thi
1009
1111
  `;
1010
1112
  }
1011
1113
 
1012
- function writeFileSafe(dir, name, content, force = false) {
1114
+ function writeFileSafe(dir, name, content, force = false, mode) {
1013
1115
  const p = join(dir, name);
1014
1116
  const exists = existsSync(p);
1015
1117
  if (exists && !force) { console.log(` ${c.yellow('skip')} ${name} ${c.dim('(exists)')}`); return; }
1016
- writeFileSync(p, content);
1118
+ // `mode` (e.g. 0o600 for a private-key-bearing file) only narrows perms at CREATE time —
1119
+ // writeFileSync ignores its own `mode` option on an existing file, so an --force overwrite
1120
+ // needs an explicit chmod or a stale world-readable mode from the file's first creation
1121
+ // would otherwise survive untouched.
1122
+ writeFileSync(p, content, mode !== undefined ? { mode } : undefined);
1123
+ if (mode !== undefined) chmodSync(p, mode);
1017
1124
  console.log(` ${exists ? c.yellow('overwrite') : c.green('create')} ${name}`);
1018
1125
  }
1019
1126
 
@@ -1047,15 +1154,15 @@ function assertScaffoldTarget(outDir, force) {
1047
1154
  * enforcement boundary. Off for --sandbox (shared demo identity, never real credentials
1048
1155
  * anyway) and --no-gateway (opt out, e.g. you're already running your own separate gateway).
1049
1156
  */
1050
- function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox, withGateway, gatewayPort = DEFAULT_GATEWAY_PORT, force = false }) {
1157
+ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'USD', merchant = 'skyward-air', sandbox, withGateway, gatewayPort = DEFAULT_GATEWAY_PORT, force = false }) {
1051
1158
  assertScaffoldTarget(outDir, force);
1052
1159
  console.log(`\n ${c.b('Scaffolding')} ${c.dim(outDir)}`);
1053
1160
  if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
1054
- writeFileSafe(outDir, 'agent.metamynd.json', JSON.stringify(config, null, 2) + '\n', force);
1055
- writeFileSafe(outDir, 'index.mjs', withGateway ? exampleIndex(scope, perTxnMax, gatewayPort) : exampleIndexNoGateway(scope, perTxnMax), force);
1161
+ writeFileSafe(outDir, 'agent.metamynd.json', JSON.stringify(config, null, 2) + '\n', force, 0o600);
1162
+ writeFileSafe(outDir, 'index.mjs', withGateway ? exampleIndex(scope, perTxnMax, gatewayPort, currency, merchant) : exampleIndexNoGateway(scope, perTxnMax, currency, merchant), force);
1056
1163
  writeFileSafe(outDir, 'package.json', examplePackageJson(slug), force);
1057
1164
  writeFileSafe(outDir, '.gitignore', gitignore(), force);
1058
- writeFileSafe(outDir, 'README.md', exampleReadme(slug, scope, withGateway, gatewayPort), force);
1165
+ writeFileSafe(outDir, 'README.md', exampleReadme(slug, scope, withGateway, gatewayPort, config.keyProvider === 'daemon'), force);
1059
1166
 
1060
1167
  if (withGateway) {
1061
1168
  const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
@@ -1072,6 +1179,8 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox, with
1072
1179
  console.log(`\n${c.green(c.b(' ✓ Done.'))} Your governed agent is ready.\n`);
1073
1180
  if (sandbox) {
1074
1181
  console.log(` ${c.dim('Shared sandbox agent — for trying MetaMynd only. Provision your own (drop --sandbox) for anything real.')}\n`);
1182
+ } else if (config.keyProvider === 'daemon') {
1183
+ 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`);
1075
1184
  } else if (config.agentKey) {
1076
1185
  console.log(` ${c.yellow('⚠ agent.metamynd.json holds the agent secret key')} — it is gitignored; never commit it.\n`);
1077
1186
  }
@@ -1691,7 +1800,7 @@ setInterval(refresh, 3000);
1691
1800
  `;
1692
1801
  }
1693
1802
 
1694
- function harnessIndexFile(scope, perTxnMax, port, withGateway, gatewayPort) {
1803
+ function harnessIndexFile(scope, perTxnMax, port, withGateway, gatewayPort, currency, merchant) {
1695
1804
  const under = Math.max(1, Math.round(perTxnMax * 0.5));
1696
1805
  const over = Math.round(perTxnMax + 100);
1697
1806
  return `// index.mjs — your agent, governed entirely on this machine. No account, no network call
@@ -1730,7 +1839,7 @@ const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://localhost:${gatewayPo
1730
1839
  // issuer): it builds and signs the same canonical message a real gate would verify, entirely
1731
1840
  // offline, using this agent's own did:key — the gateway verifies that signature for itself.
1732
1841
  async function callGateway(path, action, args) {
1733
- const signed = guard.buildSignedRequest({ action, amount: args.amount, currency: 'USD', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' } });
1842
+ const signed = await guard.buildSignedRequest({ action, amount: args.amount, currency: args.currency ?? '${currency}', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' } });
1734
1843
  const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ signed, args }) });
1735
1844
  const body = await res.json().catch(() => null);
1736
1845
  if (!res.ok) {
@@ -1753,6 +1862,7 @@ const gatedBookFlight = guard.guardToolLocal(
1753
1862
  ${withGateway ? `(args) => callGateway('/book-flight', '${scope}', args)` : 'bookFlight'},
1754
1863
  (a) => ({ // map tool args → gate inputs
1755
1864
  amount: a.amount,
1865
+ currency: a.currency ?? '${currency}',
1756
1866
  merchant: a.merchant,
1757
1867
  context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
1758
1868
  }),
@@ -1768,7 +1878,7 @@ async function raiseOwnLimit(args) {
1768
1878
  const gatedRaiseOwnLimit = guard.guardToolLocal(
1769
1879
  'permissions.update', // an action NOT in the mandate
1770
1880
  ${withGateway ? `(args) => callGateway('/raise-limit', 'permissions.update', args)` : 'raiseOwnLimit'},
1771
- (a) => ({ amount: a.amount, merchant: a.merchant, context: { tool: 'permissions-update' } }),
1881
+ (a) => ({ amount: a.amount, currency: a.currency ?? '${currency}', merchant: a.merchant, context: { tool: 'permissions-update' } }),
1772
1882
  getBundle,
1773
1883
  );
1774
1884
 
@@ -1778,7 +1888,7 @@ const rule = (n) => ' ' + '-'.repeat(n);
1778
1888
 
1779
1889
  const WHY = {
1780
1890
  AUTHORIZED: 'inside the mandate and under the SOP spend cap',
1781
- SOP_SPEND_CAP: 'your SOP caps a single transaction at $${perTxnMax}',
1891
+ SOP_SPEND_CAP: 'your SOP caps a single transaction at ${currency} ${perTxnMax}',
1782
1892
  RISK_REVIEW: 'your SOP sends high-risk actions to a human first',
1783
1893
  MERCHANT_NOT_ALLOWED: 'the mandate lists which merchants this agent may pay',
1784
1894
  NO_PERMISSION_FOR_ACTION: 'the mandate never granted this action - at any amount',
@@ -1824,14 +1934,14 @@ console.log(' for something never granted at all - the one a prompt could not h
1824
1934
  console.log(' because the decision is not made inside your program, and not on a server either.');
1825
1935
  console.log('');
1826
1936
  console.log(dim(' scope ${scope}'));
1827
- console.log(dim(' cap $${perTxnMax} per transaction, from ./metamynd-rules.json'));
1937
+ console.log(dim(' cap ${currency} ${perTxnMax} per transaction, from ./metamynd-rules.json'));
1828
1938
 
1829
1939
  console.log('');
1830
1940
  console.log(rule(66));
1831
- await attempt(1, 'a $${under} booking, low risk. Expected to pass.', '${scope}', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'low' });
1832
- await attempt(2, 'a $${over} booking, deliberately over the cap.', '${scope}', { amount: ${over}, merchant: 'skyward-air', riskLevel: 'low' });
1833
- await attempt(3, 'a $${under} booking, but flagged high risk.', '${scope}', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'high' });
1834
- await attempt(4, 'the agent stops booking flights and asks to raise its OWN limit.', 'permissions.update', { amount: 100000, merchant: 'skyward-air' }, gatedRaiseOwnLimit);
1941
+ await attempt(1, 'a ${currency} ${under} booking, low risk. Expected to pass.', '${scope}', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
1942
+ await attempt(2, 'a ${currency} ${over} booking, deliberately over the cap.', '${scope}', { amount: ${over}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'low' });
1943
+ await attempt(3, 'a ${currency} ${under} booking, but flagged high risk.', '${scope}', { amount: ${under}, currency: '${currency}', merchant: '${merchant}', riskLevel: 'high' });
1944
+ await attempt(4, 'the agent stops booking flights and asks to raise its OWN limit.', 'permissions.update', { amount: 100000, currency: '${currency}', merchant: '${merchant}' }, gatedRaiseOwnLimit);
1835
1945
  console.log('');
1836
1946
  console.log(rule(66));
1837
1947
 
@@ -2008,10 +2118,10 @@ async function runHarness(args) {
2008
2118
 
2009
2119
  console.log(`\n ${c.b('Scaffolding')} ${c.dim(outDir)}`);
2010
2120
  if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
2011
- writeFileSafe(outDir, 'agent.metamynd.json', JSON.stringify({ agentDid, agentKey: privateKeyHex, mode: 'harness' }, null, 2) + '\n', !!args.force);
2121
+ writeFileSafe(outDir, 'agent.metamynd.json', JSON.stringify({ agentDid, agentKey: privateKeyHex, mode: 'harness' }, null, 2) + '\n', !!args.force, 0o600);
2012
2122
  writeFileSafe(outDir, 'metamynd-rules.json', harnessRulesFile(mandate, sopDocument), !!args.force);
2013
2123
  writeFileSafe(outDir, 'harness-server.mjs', harnessServerFile(), !!args.force);
2014
- writeFileSafe(outDir, 'index.mjs', harnessIndexFile(scope, perTxnMax, port, withGateway, gatewayPort), !!args.force);
2124
+ writeFileSafe(outDir, 'index.mjs', harnessIndexFile(scope, perTxnMax, port, withGateway, gatewayPort, currency, merchants[0] || 'demo-merchant'), !!args.force);
2015
2125
  writeFileSafe(outDir, 'package.json', harnessPackageJson(slug), !!args.force);
2016
2126
  writeFileSafe(outDir, '.gitignore', gitignore(), !!args.force);
2017
2127
  writeFileSafe(outDir, 'README.md', harnessReadme(slug, scope, port, withGateway, gatewayPort), !!args.force);
@@ -2103,7 +2213,9 @@ async function runRequest(args) {
2103
2213
  const d = res.data;
2104
2214
  const state = { api: base, requestId: d.requestId, claimToken: d.claimToken, byok: !!generated, privateKey: generated?.privateKeyHex ?? null, name, scope, perTxnMax };
2105
2215
  const file = resolve(String(args.out || '.'), REQUEST_STATE_FILE);
2106
- writeFileSync(file, JSON.stringify(state, null, 2) + '\n');
2216
+ // May carry a BYOK private key (state.privateKey) same 0600 treatment as agent.metamynd.json.
2217
+ writeFileSync(file, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
2218
+ if (existsSync(file)) chmodSync(file, 0o600);
2107
2219
 
2108
2220
  console.log(` ${c.green('✓')} request ${c.b(d.requestId)} submitted — awaiting ${owner}'s approval`);
2109
2221
  console.log(` ${c.yellow('⚠ saved the one-time claim token to')} ${file.replace(resolve('.'), '.').replace(/\\/g, '/')} ${c.dim('(secret — do not commit)')}\n`);
@@ -2224,11 +2336,35 @@ async function main() {
2224
2336
  );
2225
2337
  const merchants = String(merchantsRaw).split(',').map((s) => s.trim()).filter(Boolean);
2226
2338
 
2227
- // BYOK: --byok generates a keypair on THIS machine (MetaMynd never sees the private key). An
2228
- // explicit --public-key means the caller holds the key elsewhere and will prove it themselves.
2339
+ // BYOK: --byok generates a keypair on THIS machine (MetaMynd never sees the private key)
2340
+ // either locally in this process (default) or, opt-in, via an already-running agentsafe-signer
2341
+ // daemon (--daemon-socket + --daemon-admin-socket, see their own help text) so the private key
2342
+ // never enters this process at all. An explicit --public-key means the caller holds the key
2343
+ // elsewhere and will prove it themselves — daemon flags are meaningless with it.
2229
2344
  let publicKey = typeof args['public-key'] === 'string' ? args['public-key'] : undefined;
2345
+ const daemonSocket = typeof args['daemon-socket'] === 'string' ? args['daemon-socket'] : undefined;
2346
+ const daemonAdminSocket = typeof args['daemon-admin-socket'] === 'string' ? args['daemon-admin-socket'] : undefined;
2347
+ if (Boolean(daemonSocket) !== Boolean(daemonAdminSocket)) {
2348
+ rl?.close();
2349
+ fail('--daemon-socket and --daemon-admin-socket must be used together.');
2350
+ }
2351
+ if (daemonSocket && !args.byok) {
2352
+ rl?.close();
2353
+ fail('--daemon-socket requires --byok.');
2354
+ }
2355
+ if (daemonSocket && publicKey) {
2356
+ rl?.close();
2357
+ fail('--daemon-socket generates its own key — pass --byok alone, not --public-key.');
2358
+ }
2230
2359
  let generatedKey = null;
2231
- if (args.byok && !publicKey) {
2360
+ let daemonPublicKeyHex = null;
2361
+ if (args.byok && !publicKey && daemonSocket) {
2362
+ console.log(c.dim(' → asking the agentsafe-signer daemon to generate a key …'));
2363
+ const { publicKeyHex } = await daemonRequest(daemonAdminSocket, 'generate-key', { allowRekey: false });
2364
+ daemonPublicKeyHex = publicKeyHex;
2365
+ publicKey = publicKeyHex;
2366
+ console.log(` ${c.green('✓')} generated an Ed25519 keypair via the signer daemon ${c.dim('(the private key never left it)')}`);
2367
+ } else if (args.byok && !publicKey) {
2232
2368
  generatedKey = generateAgentKeypair();
2233
2369
  publicKey = generatedKey.publicKeyHex;
2234
2370
  console.log(` ${c.green('✓')} generated an Ed25519 keypair locally ${c.dim('(private key stays on this machine)')}`);
@@ -2253,7 +2389,21 @@ async function main() {
2253
2389
  if (config.standards?.length) console.log(` ${c.green('✓')} enforced Standards: ${config.standards.join(', ')}`);
2254
2390
 
2255
2391
  // 3b. BYOK: prove control of the key (verify-key), else the gate blocks with AGENT_KEY_UNVERIFIED.
2256
- if (generatedKey) {
2392
+ if (daemonPublicKeyHex) {
2393
+ // The daemon holds the private key — it never entered this process. Point the scaffolded
2394
+ // guard at the daemon instead of embedding a plaintext key (agentsafe-guard/key-providers.mjs's
2395
+ // resolveKeyProvider() reads these two fields and never looks for `agentKey` when present).
2396
+ config.keyProvider = 'daemon';
2397
+ config.daemonSocketPath = daemonSocket;
2398
+ if (config.challenge) {
2399
+ console.log(c.dim(' → proving key control via the daemon (verify-key) …'));
2400
+ const { signature } = await daemonRequest(daemonSocket, 'sign-key-control-challenge', { challenge: config.challenge });
2401
+ await apiPost(base, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature }, token);
2402
+ config.keyVerified = true;
2403
+ delete config.challenge; // one-time; consumed
2404
+ console.log(` ${c.green('✓')} key verified — MetaMynd never saw your private key, and neither did this CLI`);
2405
+ }
2406
+ } else if (generatedKey) {
2257
2407
  // We hold the private key — inject it into the config so the scaffolded guard can sign, and
2258
2408
  // prove possession by signing the issued challenge.
2259
2409
  config.agentKey = generatedKey.privateKeyHex;
@@ -2274,7 +2424,7 @@ async function main() {
2274
2424
  }
2275
2425
 
2276
2426
  // 4. Scaffold + next steps
2277
- scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox: false, withGateway: !args['no-gateway'], gatewayPort: Number(args['gateway-port']) || DEFAULT_GATEWAY_PORT, force: !!args.force });
2427
+ scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency, merchant: merchants[0] || 'demo-merchant', sandbox: false, withGateway: !args['no-gateway'], gatewayPort: Number(args['gateway-port']) || DEFAULT_GATEWAY_PORT, force: !!args.force });
2278
2428
  }
2279
2429
 
2280
2430
  main().catch((e) => fail(e?.stack || e?.message || String(e)));
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "create-metamynd-agent",
3
- "version": "0.9.1",
3
+ "version": "0.10.5",
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",