create-metamynd-agent 0.9.1 → 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.
Files changed (3) hide show
  1. package/README.md +24 -4
  2. package/index.mjs +153 -14
  3. 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. 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).
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
@@ -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.
@@ -118,6 +140,13 @@ ${c.b('Options')}
118
140
  --byok Bring-your-own-key: generate the keypair locally, provision + prove control
119
141
  (MetaMynd never sees the private key). Overridden by --public-key.
120
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.
121
150
  --out <dir> Output project directory (default ./<agent-slug>)
122
151
  --no-gateway Hosted flow only: skip the separate tool-gateway process (see
123
152
  README#separate-tool-gateway-default) and scaffold the old
@@ -310,6 +339,72 @@ function signChallengeHex(privateKeyHex, challenge) {
310
339
  return crypto.sign(null, Buffer.from(challenge, 'utf8'), key).toString('hex');
311
340
  }
312
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
+
313
408
  // ---------- API ----------
314
409
  async function apiPost(base, path, body, token) {
315
410
  let res;
@@ -522,7 +617,7 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
522
617
  // --- the gateway atomically claim single-use execution, closing replay + cumulative spend, not
523
618
  // --- just re-checking policy. See ./gateway/README.md.
524
619
  async function bookFlightViaGateway(args, decision) {
525
- const signed = guard.buildSignedRequest({
620
+ const signed = await guard.buildSignedRequest({
526
621
  action: '${scope}',
527
622
  amount: args.amount,
528
623
  currency: 'USD',
@@ -698,7 +793,13 @@ function examplePackageJson(slug) {
698
793
  ) + '\n';
699
794
  }
700
795
 
701
- 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\`.`;
702
803
  const gatewaySection = withGateway
703
804
  ? `## Run
704
805
 
@@ -721,8 +822,7 @@ an ESCALATE (high risk). The BLOCK and ESCALATE never reach the gateway at all
721
822
 
722
823
  ## Files
723
824
 
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\`.
825
+ ${configFileLine}
726
826
  - \`index.mjs\` — signs each request and calls \`./gateway\` for it; \`guard.guardTool()\` here is a
727
827
  fast local pre-check, not the enforcement boundary.
728
828
  - \`gateway/\` — a **separate process**. It holds the real tool and independently re-verifies every
@@ -743,8 +843,7 @@ You should see an ALLOW, a BLOCK (over the per-transaction cap), and an ESCALATE
743
843
 
744
844
  ## Files
745
845
 
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\`.
846
+ ${configFileLine}
748
847
  - \`index.mjs\` — wraps a tool with \`guard.guardTool(...)\`; the tool only runs when the gate allows.
749
848
 
750
849
  ## What this is not
@@ -1055,7 +1154,7 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox, with
1055
1154
  writeFileSafe(outDir, 'index.mjs', withGateway ? exampleIndex(scope, perTxnMax, gatewayPort) : exampleIndexNoGateway(scope, perTxnMax), force);
1056
1155
  writeFileSafe(outDir, 'package.json', examplePackageJson(slug), force);
1057
1156
  writeFileSafe(outDir, '.gitignore', gitignore(), force);
1058
- writeFileSafe(outDir, 'README.md', exampleReadme(slug, scope, withGateway, gatewayPort), force);
1157
+ writeFileSafe(outDir, 'README.md', exampleReadme(slug, scope, withGateway, gatewayPort, config.keyProvider === 'daemon'), force);
1059
1158
 
1060
1159
  if (withGateway) {
1061
1160
  const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
@@ -1072,6 +1171,8 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox, with
1072
1171
  console.log(`\n${c.green(c.b(' ✓ Done.'))} Your governed agent is ready.\n`);
1073
1172
  if (sandbox) {
1074
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`);
1075
1176
  } else if (config.agentKey) {
1076
1177
  console.log(` ${c.yellow('⚠ agent.metamynd.json holds the agent secret key')} — it is gitignored; never commit it.\n`);
1077
1178
  }
@@ -1730,7 +1831,7 @@ const GATEWAY = process.env.HARNESS_GATEWAY_URL || 'http://localhost:${gatewayPo
1730
1831
  // issuer): it builds and signs the same canonical message a real gate would verify, entirely
1731
1832
  // offline, using this agent's own did:key — the gateway verifies that signature for itself.
1732
1833
  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' } });
1834
+ const signed = await guard.buildSignedRequest({ action, amount: args.amount, currency: 'USD', merchant: args.merchant, context: { tool: '${scope}', riskLevel: args.riskLevel ?? 'low' } });
1734
1835
  const res = await fetch(GATEWAY + path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ signed, args }) });
1735
1836
  const body = await res.json().catch(() => null);
1736
1837
  if (!res.ok) {
@@ -2224,11 +2325,35 @@ async function main() {
2224
2325
  );
2225
2326
  const merchants = String(merchantsRaw).split(',').map((s) => s.trim()).filter(Boolean);
2226
2327
 
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.
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.
2229
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
+ }
2230
2348
  let generatedKey = null;
2231
- if (args.byok && !publicKey) {
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) {
2232
2357
  generatedKey = generateAgentKeypair();
2233
2358
  publicKey = generatedKey.publicKeyHex;
2234
2359
  console.log(` ${c.green('✓')} generated an Ed25519 keypair locally ${c.dim('(private key stays on this machine)')}`);
@@ -2253,7 +2378,21 @@ async function main() {
2253
2378
  if (config.standards?.length) console.log(` ${c.green('✓')} enforced Standards: ${config.standards.join(', ')}`);
2254
2379
 
2255
2380
  // 3b. BYOK: prove control of the key (verify-key), else the gate blocks with AGENT_KEY_UNVERIFIED.
2256
- if (generatedKey) {
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) {
2257
2396
  // We hold the private key — inject it into the config so the scaffolded guard can sign, and
2258
2397
  // prove possession by signing the issued challenge.
2259
2398
  config.agentKey = generatedKey.privateKeyHex;
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.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",