create-metamynd-agent 0.1.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.
Files changed (3) hide show
  1. package/README.md +120 -0
  2. package/index.mjs +542 -0
  3. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # create-metamynd-agent
2
+
3
+ Scaffold a **MetaMynd/AgentSafe-governed** AI agent in one command. It logs you in, provisions the
4
+ agent in a **single call** (identity + mandate + starter SOP + all enforced Standards), writes a
5
+ portable `agent.metamynd.json`, and drops a runnable example that gates a tool through the
6
+ [`@metamynd/agentsafe-guard`](https://www.npmjs.com/package/@metamynd/agentsafe-guard).
7
+
8
+ > **Prerequisite:** an agent is always owned by a **KYB-verified owner** — a person/org with a
9
+ > MetaMynd account. If that's you and you're verified, you're ready. Verify once in the dashboard if
10
+ > not; it's the only gate.
11
+
12
+ ## Try it instantly — sandbox (no account, no KYB)
13
+
14
+ ```bash
15
+ npm create metamynd-agent@latest -- --sandbox # or: npx create-metamynd-agent --sandbox
16
+ ```
17
+
18
+ Skips login and provisioning entirely — fetches a **shared sandbox agent** config from the public
19
+ `POST /onboarding/sandbox` endpoint and scaffolds a runnable example. Great for a first look; use
20
+ the full flow below when you want your own governed agent with your own limits.
21
+
22
+ ## Use
23
+
24
+ ```bash
25
+ npm create metamynd-agent@latest
26
+ # or
27
+ npx create-metamynd-agent
28
+ ```
29
+
30
+ Answer a few prompts (API, owner email/password, agent name, scope, per-transaction cap) and you get:
31
+
32
+ ```
33
+ my-agent/
34
+ ├─ agent.metamynd.json # portable guard config — HOLDS THE AGENT SECRET KEY (gitignored)
35
+ ├─ index.mjs # runnable example: ALLOW · BLOCK (over cap) · ESCALATE (high risk)
36
+ ├─ package.json # depends on @metamynd/agentsafe-guard
37
+ ├─ .gitignore
38
+ └─ README.md
39
+ ```
40
+
41
+ Then:
42
+
43
+ ```bash
44
+ cd my-agent
45
+ npm install
46
+ npm start
47
+ ```
48
+
49
+ ## Non-interactive
50
+
51
+ Every prompt has a flag or environment-variable fallback, so it scripts cleanly in CI:
52
+
53
+ ```bash
54
+ npx create-metamynd-agent \
55
+ --api http://localhost:9926/api/v1 \
56
+ --email owner@example.com \
57
+ --name "Support Bot" \
58
+ --scope flight-purchase \
59
+ --per-txn-max 500 \
60
+ --out ./support-bot \
61
+ --yes
62
+ # password via env (never on the command line where it lands in shell history):
63
+ METAMYND_PASSWORD='…' npx create-metamynd-agent --yes …
64
+ ```
65
+
66
+ | Flag | Env | Default |
67
+ |---|---|---|
68
+ | `--sandbox` | — | off (skips login/KYB; shared sandbox agent) |
69
+ | `--api <url>` | `METAMYND_API` | `https://metamynd.ai/api/v1` |
70
+ | `--email <email>` | `METAMYND_EMAIL` | — (required) |
71
+ | `--password <pw>` | `METAMYND_PASSWORD` | interactive masked prompt |
72
+ | `--name <name>` | — | `Support Bot` |
73
+ | `--scope <scope>` | — | `flight-purchase` |
74
+ | `--per-txn-max <n>` | — | `500` |
75
+ | `--max-amount <n>` | — | `10000` |
76
+ | `--currency <cur>` | — | `USD` |
77
+ | `--merchants <a,b>` | — | any |
78
+ | `--byok` | — | generate the keypair locally, provision + prove control |
79
+ | `--public-key <hex>` | — | BYOK with a key you already hold (you prove control yourself) |
80
+ | `--out <dir>` | — | `./<agent-slug>` |
81
+ | `--yes`, `-y` | — | non-interactive |
82
+
83
+ Run `npx create-metamynd-agent --help` for the full list.
84
+
85
+ ## Bring your own key (`--byok`)
86
+
87
+ ```bash
88
+ npx create-metamynd-agent --byok --email you@example.com --name "Support Bot"
89
+ ```
90
+
91
+ Generates an Ed25519 keypair **on your machine**, provisions the agent with only the public key, then
92
+ proves control (signs the one-time challenge → `verify-key`). MetaMynd never sees the private key. The
93
+ generated private key is written into `agent.metamynd.json` (gitignored). Pass `--public-key <hex>`
94
+ instead to register a key you already hold elsewhere — then you complete `verify-key` yourself (the CLI
95
+ prints the challenge + endpoint).
96
+
97
+ ## Delegated issuance (`--request` / `--claim`)
98
+
99
+ Request an agent for an owner's org when you're **not** the owner — no shared credentials:
100
+
101
+ ```bash
102
+ npx create-metamynd-agent --request --owner owner@example.com --name "Support Bot" # +--byok optional
103
+ # → saves metamynd-request.json (holds a one-time claim token — do not commit)
104
+ # → the owner approves in their dashboard (AgentSafe → Agent Requests), then:
105
+ npx create-metamynd-agent --claim --watch
106
+ ```
107
+
108
+ `--request` submits the request (as your own authed user) and stores the claim token locally; `--claim`
109
+ polls until the owner approves, then scaffolds the project. With `--byok` the keypair is generated
110
+ locally and control is proven on claim — MetaMynd never sees the private key.
111
+
112
+ ## Security
113
+
114
+ `agent.metamynd.json` contains the agent's **secret key** (a managed key, or — with `--byok` — the one
115
+ generated locally). The scaffolded project gitignores it. Never commit it or paste it anywhere public.
116
+
117
+ ## Full guide
118
+
119
+ `docs/integration/INTEGRATE-WITH-METAMYND.md` — the complete integration front-door (payments,
120
+ handshake, edge evaluation, escalation).
package/index.mjs ADDED
@@ -0,0 +1,542 @@
1
+ #!/usr/bin/env node
2
+ // create-metamynd-agent — scaffold a MetaMynd/AgentSafe-governed agent in one command.
3
+ //
4
+ // Logs a KYB-verified owner in, provisions the agent in ONE call
5
+ // (POST /onboarding/agent → identity + mandate + starter SOP + enforced Standards),
6
+ // writes the portable `agent.metamynd.json`, and drops a runnable example that gates a
7
+ // tool through the guard (allow / block / escalate).
8
+ //
9
+ // ZERO dependencies: Node ≥ 18 built-ins only (fetch, readline).
10
+ //
11
+ // npm create metamynd-agent@latest
12
+ // npx create-metamynd-agent
13
+ // npx create-metamynd-agent --api http://localhost:9926/api/v1 --email you@x.com \
14
+ // --name "Support Bot" --scope flight-purchase --per-txn-max 500 --out ./support-bot --yes
15
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
16
+ import { join, resolve } from 'node:path';
17
+ import readline from 'node:readline';
18
+ import crypto from 'node:crypto';
19
+
20
+ const GUARD_PKG = '@metamynd/agentsafe-guard';
21
+ const GUARD_VERSION = '^0.1.0';
22
+ const DEFAULT_API = 'https://metamynd.ai/api/v1';
23
+
24
+ // ---------- tiny ANSI ----------
25
+ const c = {
26
+ b: (s) => `\x1b[1m${s}\x1b[0m`,
27
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
28
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
29
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
30
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
31
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
32
+ };
33
+
34
+ // ---------- args ----------
35
+ function parseArgs(argv) {
36
+ const out = { _: [] };
37
+ for (let i = 0; i < argv.length; i++) {
38
+ const a = argv[i];
39
+ if (a === '-h' || a === '--help') { out.help = true; continue; }
40
+ if (a === '-v' || a === '--version') { out.version = true; continue; }
41
+ if (a === '-y' || a === '--yes' || a === '--non-interactive') { out.yes = true; continue; }
42
+ if (a.startsWith('--')) {
43
+ const eq = a.indexOf('=');
44
+ if (eq !== -1) { out[a.slice(2, eq)] = a.slice(eq + 1); continue; }
45
+ const key = a.slice(2);
46
+ const next = argv[i + 1];
47
+ if (next === undefined || next.startsWith('-')) { out[key] = true; }
48
+ else { out[key] = next; i++; }
49
+ } else { out._.push(a); }
50
+ }
51
+ return out;
52
+ }
53
+
54
+ const HELP = `${c.b('create-metamynd-agent')} — scaffold a governed AI agent
55
+
56
+ ${c.b('Usage')}
57
+ npm create metamynd-agent@latest
58
+ npx create-metamynd-agent [options]
59
+
60
+ ${c.b('Options')}
61
+ --sandbox No login, no KYB: scaffold against the shared sandbox agent (fastest start)
62
+ --request Delegated: request an agent for an owner's org (--owner <email>, +--byok)
63
+ --claim [--watch] Delegated: claim the config once the owner approves (reads metamynd-request.json)
64
+ --owner <email> Target owner's email (with --request)
65
+ --api <url> API base (default ${DEFAULT_API})
66
+ --email <email> Owner login email
67
+ --password <pw> Owner password (prefer the interactive prompt or METAMYND_PASSWORD)
68
+ --name <name> Agent name (e.g. "Support Bot")
69
+ --scope <scope> Mandate action scope (e.g. flight-purchase)
70
+ --per-txn-max <n> Per-transaction cap (default 500)
71
+ --max-amount <n> Total mandate budget (default 10000)
72
+ --currency <cur> Currency (default USD)
73
+ --merchants <a,b> Allowed merchants, comma-separated (optional)
74
+ --byok Bring-your-own-key: generate the keypair locally, provision + prove control
75
+ (MetaMynd never sees the private key). Overridden by --public-key.
76
+ --public-key <hex> BYOK with a key you already hold (SPKI/raw hex); you prove control yourself
77
+ --out <dir> Output project directory (default ./<agent-slug>)
78
+ --yes, -y Non-interactive: use flags/env/defaults, never prompt
79
+ -h, --help Show this help
80
+ -v, --version Show version
81
+
82
+ ${c.b('Environment')}
83
+ METAMYND_API, METAMYND_EMAIL, METAMYND_PASSWORD — fallbacks for the flags above
84
+
85
+ ${c.b('What it does')}
86
+ 1. Logs in as a KYB-verified owner → owner access token
87
+ 2. POST /onboarding/agent (one call) → identity + mandate + SOP + Standards
88
+ 3. Writes agent.metamynd.json + a runnable example that gates a tool through the guard.
89
+ `;
90
+
91
+ // ---------- prompts ----------
92
+ function makeRl() {
93
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
94
+ }
95
+ function ask(rl, query, def) {
96
+ const suffix = def !== undefined && def !== '' ? c.dim(` (${def})`) : '';
97
+ return new Promise((res) => rl.question(`${query}${suffix}: `, (a) => res(a.trim() || (def ?? ''))));
98
+ }
99
+ // Hidden input (password) — raw mode, masks with '*', handles backspace/paste/Ctrl-C.
100
+ function askHidden(query) {
101
+ return new Promise((resolve) => {
102
+ const stdin = process.stdin;
103
+ process.stdout.write(`${query}: `);
104
+ const wasRaw = stdin.isRaw;
105
+ if (stdin.setRawMode) stdin.setRawMode(true);
106
+ stdin.resume();
107
+ stdin.setEncoding('utf8');
108
+ let input = '';
109
+ const done = () => {
110
+ if (stdin.setRawMode) stdin.setRawMode(Boolean(wasRaw));
111
+ stdin.pause();
112
+ stdin.removeListener('data', onData);
113
+ process.stdout.write('\n');
114
+ resolve(input);
115
+ };
116
+ const onData = (chunk) => {
117
+ for (const ch of chunk) {
118
+ const code = ch.charCodeAt(0);
119
+ if (code === 13 || code === 10 || code === 4) { done(); return; } // Enter / Ctrl-D
120
+ if (code === 3) { process.stdout.write('\n'); process.exit(130); } // Ctrl-C
121
+ if (code === 127 || code === 8) { if (input.length) { input = input.slice(0, -1); process.stdout.write('\b \b'); } continue; } // backspace
122
+ if (code < 32) continue; // ignore other control chars
123
+ input += ch;
124
+ process.stdout.write('*');
125
+ }
126
+ };
127
+ stdin.on('data', onData);
128
+ });
129
+ }
130
+
131
+ function fail(msg) {
132
+ console.error(`\n${c.red('✖')} ${msg}\n`);
133
+ process.exit(1);
134
+ }
135
+
136
+ function slugify(name) {
137
+ return String(name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'metamynd-agent';
138
+ }
139
+
140
+ // ---------- BYOK (bring-your-own-key) ----------
141
+ // Generate an Ed25519 keypair CLIENT-SIDE — the private key never leaves this machine, so MetaMynd
142
+ // never sees it. The public key is sent as SPKI DER hex (algorithm-tagged Ed25519, unambiguous to
143
+ // the Hedera SDK); the private key is PKCS8 DER hex, the exact format the guard's createGuard loads.
144
+ function generateAgentKeypair() {
145
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
146
+ return {
147
+ publicKeyHex: publicKey.export({ format: 'der', type: 'spki' }).toString('hex'),
148
+ privateKeyHex: privateKey.export({ format: 'der', type: 'pkcs8' }).toString('hex'),
149
+ };
150
+ }
151
+
152
+ // Sign a BYOK challenge exactly as the gate verifies it: Ed25519 over the UTF-8 bytes of the raw
153
+ // challenge nonce, hex-encoded. Mirrors agentsafe-guard's sign().
154
+ function signChallengeHex(privateKeyHex, challenge) {
155
+ const key = crypto.createPrivateKey({ key: Buffer.from(privateKeyHex, 'hex'), format: 'der', type: 'pkcs8' });
156
+ return crypto.sign(null, Buffer.from(challenge, 'utf8'), key).toString('hex');
157
+ }
158
+
159
+ // ---------- API ----------
160
+ async function apiPost(base, path, body, token) {
161
+ let res;
162
+ try {
163
+ res = await fetch(`${base}${path}`, {
164
+ method: 'POST',
165
+ headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
166
+ body: JSON.stringify(body),
167
+ });
168
+ } catch (e) {
169
+ fail(`Cannot reach ${base}${path} — is the API up? (${e.message})`);
170
+ }
171
+ const text = await res.text();
172
+ let json = null;
173
+ try { json = text ? JSON.parse(text) : null; } catch { /* non-JSON */ }
174
+ if (!res.ok) {
175
+ const detail = json?.message ? (typeof json.message === 'string' ? json.message : JSON.stringify(json.message)) : text.slice(0, 300);
176
+ fail(`${path} → HTTP ${res.status}${detail ? `: ${detail}` : ''}`);
177
+ }
178
+ return json;
179
+ }
180
+
181
+ // ---------- scaffolding ----------
182
+ function exampleIndex(scope, perTxnMax) {
183
+ const under = Math.max(1, Math.round(perTxnMax * 0.5));
184
+ const over = Math.round(perTxnMax + 100);
185
+ return `// index.mjs — your agent, governed by MetaMynd/AgentSafe.
186
+ // Every governed tool call is checked (allow / block / escalate) before it runs.
187
+ import { createGuardFromConfig } from '${GUARD_PKG}';
188
+
189
+ const guard = await createGuardFromConfig('./agent.metamynd.json'); // no env vars
190
+
191
+ // --- Your real tool. Replace the body with your actual implementation. ---
192
+ async function bookFlight(args) {
193
+ return { pnr: 'PNR-DEMO', ...args };
194
+ }
195
+
196
+ // --- The GATED version. Register THIS with your agent instead of the raw handler. ---
197
+ const gatedBookFlight = guard.guardTool(
198
+ '${scope}', // = your mandate scope
199
+ bookFlight,
200
+ (a) => ({ // map tool args → gate inputs
201
+ amount: a.amount,
202
+ currency: 'USD',
203
+ merchant: a.merchant,
204
+ context: { tool: 'book-flight', riskLevel: a.riskLevel ?? 'low' },
205
+ }),
206
+ );
207
+
208
+ async function ask(label, args) {
209
+ try {
210
+ const r = await gatedBookFlight(args);
211
+ console.log(' \\x1b[32m✅ ALLOW\\x1b[0m ' + label.padEnd(26) + ' → booked ' + r.pnr);
212
+ } catch (e) {
213
+ const g = e.governance ?? {};
214
+ const tag = g.decision === 'escalate' ? '\\x1b[33m⚠ ESCALATE\\x1b[0m' : '\\x1b[31m⛔ BLOCK\\x1b[0m';
215
+ console.log(' ' + tag + ' ' + label.padEnd(26) + ' → ' + (g.reasonCode ?? e.message));
216
+ }
217
+ }
218
+
219
+ console.log('\\n ${scope} — every action passes through the MetaMynd gate\\n ' + '─'.repeat(58));
220
+ await ask('$${under} low risk', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'low' });
221
+ await ask('$${over} over cap', { amount: ${over}, merchant: 'skyward-air', riskLevel: 'low' });
222
+ await ask('$${under} high risk', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'high' });
223
+ console.log('\\n Change the rules any time in the dashboard (Legal Entity → SOPs) — no redeploy.\\n');
224
+ `;
225
+ }
226
+
227
+ function examplePackageJson(slug) {
228
+ return JSON.stringify(
229
+ {
230
+ name: slug,
231
+ version: '0.1.0',
232
+ private: true,
233
+ type: 'module',
234
+ scripts: { start: 'node index.mjs' },
235
+ dependencies: { [GUARD_PKG]: GUARD_VERSION },
236
+ },
237
+ null,
238
+ 2,
239
+ ) + '\n';
240
+ }
241
+
242
+ function exampleReadme(slug, scope) {
243
+ return `# ${slug}
244
+
245
+ A MetaMynd/AgentSafe-governed agent, scaffolded with \`create-metamynd-agent\`.
246
+
247
+ ## Run
248
+
249
+ \`\`\`bash
250
+ npm install
251
+ npm start
252
+ \`\`\`
253
+
254
+ You should see an ALLOW, a BLOCK (over the per-transaction cap), and an ESCALATE (high risk).
255
+
256
+ ## Files
257
+
258
+ - \`agent.metamynd.json\` — your portable guard config (identity, mandate scope \`${scope}\`, issuer keys).
259
+ **Contains the agent's secret key — never commit it.** It is already in \`.gitignore\`.
260
+ - \`index.mjs\` — wraps a tool with \`guard.guardTool(...)\`; the tool only runs when the gate allows.
261
+
262
+ ## Change the rules
263
+
264
+ Edit the agent's SOPs in the dashboard (Legal Entity → SOPs). The agent's behaviour changes live —
265
+ no redeploy. An \`escalate\` verdict is held for an owner to approve; poll \`guard.escalationStatus(id)\`.
266
+
267
+ Full integration guide: \`docs/integration/INTEGRATE-WITH-METAMYND.md\`.
268
+ `;
269
+ }
270
+
271
+ function gitignore() {
272
+ return `node_modules/\nagent.metamynd.json\n.env\n`;
273
+ }
274
+
275
+ function writeFileSafe(dir, name, content) {
276
+ const p = join(dir, name);
277
+ if (existsSync(p)) { console.log(` ${c.yellow('skip')} ${name} ${c.dim('(exists)')}`); return; }
278
+ writeFileSync(p, content);
279
+ console.log(` ${c.green('create')} ${name}`);
280
+ }
281
+
282
+ /** Write the scaffolded project + print next steps. Shared by the provision and sandbox paths. */
283
+ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox }) {
284
+ console.log(`\n ${c.b('Scaffolding')} ${c.dim(outDir)}`);
285
+ if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
286
+ writeFileSafe(outDir, 'agent.metamynd.json', JSON.stringify(config, null, 2) + '\n');
287
+ writeFileSafe(outDir, 'index.mjs', exampleIndex(scope, perTxnMax));
288
+ writeFileSafe(outDir, 'package.json', examplePackageJson(slug));
289
+ writeFileSafe(outDir, '.gitignore', gitignore());
290
+ writeFileSafe(outDir, 'README.md', exampleReadme(slug, scope));
291
+
292
+ const rel = outDir.replace(resolve('.'), '.').replace(/\\/g, '/');
293
+ console.log(`\n${c.green(c.b(' ✓ Done.'))} Your governed agent is ready.\n`);
294
+ if (sandbox) {
295
+ console.log(` ${c.dim('Shared sandbox agent — for trying MetaMynd only. Provision your own (drop --sandbox) for anything real.')}\n`);
296
+ } else if (config.agentKey) {
297
+ console.log(` ${c.yellow('⚠ agent.metamynd.json holds the agent secret key')} — it is gitignored; never commit it.\n`);
298
+ }
299
+ console.log(` Next:`);
300
+ console.log(c.cyan(` cd ${rel}`));
301
+ console.log(c.cyan(` npm install`));
302
+ console.log(c.cyan(` npm start`) + c.dim(' → ALLOW · BLOCK (over cap) · ESCALATE (high risk)\n'));
303
+ console.log(c.dim(` Change the rules any time in the dashboard (Legal Entity → SOPs) — no redeploy.\n`));
304
+ }
305
+
306
+ /** --sandbox: no login, no KYB — fetch the shared sandbox agent config and scaffold. */
307
+ async function runSandbox(args) {
308
+ const apiRaw = (typeof args.api === 'string' ? args.api : undefined) ?? process.env.METAMYND_API ?? DEFAULT_API;
309
+ const base = String(apiRaw).replace(/\/+$/, '');
310
+ console.log(c.dim(` → requesting a sandbox agent from ${base} …`));
311
+ const provisioned = await apiPost(base, '/onboarding/sandbox', {}, null);
312
+ const config = provisioned?.data;
313
+ if (!config?.agentDid) fail('Sandbox did not return a config with an agentDid.');
314
+ console.log(` ${c.green('✓')} sandbox agent ${c.b(config.agentDid)} ${c.dim('(shared test identity)')}`);
315
+ const scope = config.mandate?.scope || 'flight-purchase';
316
+ const perTxnMax = Number(config.perTxnMax) || 500;
317
+ const outDir = resolve(String(args.out || './metamynd-sandbox'));
318
+ scaffoldProject({ outDir, config, slug: 'metamynd-sandbox', scope, perTxnMax, sandbox: true });
319
+ }
320
+
321
+ // ---------- delegated issuance (#6) ----------
322
+ async function apiGet(base, path, { claimToken } = {}) {
323
+ let res;
324
+ try {
325
+ res = await fetch(`${base}${path}`, { headers: { ...(claimToken ? { 'x-claim-token': claimToken } : {}) } });
326
+ } catch (e) {
327
+ fail(`Cannot reach ${base}${path} — is the API up? (${e.message})`);
328
+ }
329
+ const text = await res.text();
330
+ let json = null;
331
+ try { json = text ? JSON.parse(text) : null; } catch { /* non-JSON */ }
332
+ if (!res.ok) fail(`${path} → HTTP ${res.status}${json?.message ? `: ${json.message}` : ''}`);
333
+ return json;
334
+ }
335
+
336
+ // Minimal auth for the request flow (flags/env, interactive fallback) — mirrors main()'s login.
337
+ async function authFlow(args) {
338
+ const apiRaw = (typeof args.api === 'string' ? args.api : undefined) ?? process.env.METAMYND_API ?? DEFAULT_API;
339
+ const base = String(apiRaw).replace(/\/+$/, '');
340
+ const interactive = !args.yes && process.stdin.isTTY;
341
+ const rl = interactive ? makeRl() : null;
342
+ let email = (typeof args.email === 'string' ? args.email : undefined) ?? process.env.METAMYND_EMAIL;
343
+ if (!email && interactive) email = await ask(rl, 'Your email', '');
344
+ if (!email) { rl?.close(); fail('An email is required (--email or METAMYND_EMAIL).'); }
345
+ let password = typeof args.password === 'string' ? args.password : process.env.METAMYND_PASSWORD;
346
+ if (password === undefined) {
347
+ if (!interactive) { rl?.close(); fail('A password is required (--password or METAMYND_PASSWORD).'); }
348
+ rl?.pause();
349
+ password = await askHidden('Password');
350
+ rl?.resume();
351
+ }
352
+ rl?.close();
353
+ const login = await apiPost(base, '/auth/login', { username: email, password }, null);
354
+ const token = login?.data?.accessToken;
355
+ if (!token) fail('Login succeeded but no access token was returned.');
356
+ return { base, token, email };
357
+ }
358
+
359
+ const REQUEST_STATE_FILE = 'metamynd-request.json';
360
+
361
+ // --request: a developer requests an agent for an owner's org (the owner approves in the dashboard).
362
+ // Writes metamynd-request.json (requestId + one-time claim token, and the local private key for --byok)
363
+ // so `--claim` can finish once the owner approves.
364
+ async function runRequest(args) {
365
+ const owner = (typeof args.owner === 'string' ? args.owner : undefined) ?? process.env.METAMYND_OWNER;
366
+ if (!owner) fail('--owner <ownerEmail> is required for a delegated request.');
367
+ const { base, token } = await authFlow(args);
368
+
369
+ const name = (typeof args.name === 'string' ? args.name : undefined) ?? 'Delegated Agent';
370
+ const scope = (typeof args.scope === 'string' ? args.scope : undefined) ?? 'flight-purchase';
371
+ const perTxnMax = Number(args['per-txn-max']) || 500;
372
+ let publicKey, generated;
373
+ if (args.byok) {
374
+ generated = generateAgentKeypair();
375
+ publicKey = generated.publicKeyHex;
376
+ console.log(` ${c.green('✓')} generated an Ed25519 keypair locally ${c.dim('(private key stays on this machine)')}`);
377
+ }
378
+
379
+ console.log(c.dim(` → requesting "${name}" for ${owner} …`));
380
+ const res = await apiPost(base, '/onboarding/requests', { ownerEmail: owner, name, scope, perTxnMax, ...(publicKey ? { publicKey } : {}) }, token);
381
+ const d = res.data;
382
+ const state = { api: base, requestId: d.requestId, claimToken: d.claimToken, byok: !!generated, privateKey: generated?.privateKeyHex ?? null, name, scope, perTxnMax };
383
+ const file = resolve(String(args.out || '.'), REQUEST_STATE_FILE);
384
+ writeFileSync(file, JSON.stringify(state, null, 2) + '\n');
385
+
386
+ console.log(` ${c.green('✓')} request ${c.b(d.requestId)} submitted — awaiting ${owner}'s approval`);
387
+ console.log(` ${c.yellow('⚠ saved the one-time claim token to')} ${file.replace(resolve('.'), '.').replace(/\\/g, '/')} ${c.dim('(secret — do not commit)')}\n`);
388
+ console.log(` The owner approves in the dashboard (AgentSafe → Agent Requests). Then run:`);
389
+ console.log(c.cyan(` npx create-metamynd-agent --claim --watch\n`));
390
+ }
391
+
392
+ // --claim: poll for the owner's approval, then scaffold. Reads metamynd-request.json (or flags).
393
+ async function runClaim(args) {
394
+ const file = resolve(String(args['request-file'] || `./${REQUEST_STATE_FILE}`));
395
+ const state = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {};
396
+ const base = String((typeof args.api === 'string' ? args.api : undefined) ?? state.api ?? DEFAULT_API).replace(/\/+$/, '');
397
+ const requestId = (typeof args['request-id'] === 'string' ? args['request-id'] : undefined) ?? state.requestId;
398
+ const claimToken = (typeof args.token === 'string' ? args.token : undefined) ?? state.claimToken;
399
+ if (!requestId || !claimToken) fail(`Need a requestId + claim token (--request-id/--token, or a ${REQUEST_STATE_FILE}).`);
400
+
401
+ const watch = !!args.watch;
402
+ let claimed;
403
+ for (;;) {
404
+ const res = await apiGet(base, `/onboarding/requests/${encodeURIComponent(requestId)}/claim`, { claimToken });
405
+ const d = res.data;
406
+ if (d.status === 'approved') { claimed = d; break; }
407
+ if (d.status === 'denied' || d.status === 'expired') fail(`Request was ${d.status}.`);
408
+ if (!watch) {
409
+ console.log(` ${c.dim(`request is still ${d.status} — the owner hasn't approved yet. Re-run, or add --watch to poll.`)}`);
410
+ return;
411
+ }
412
+ process.stdout.write(c.dim(` · ${d.status}, waiting for approval …\r`));
413
+ await new Promise((r) => setTimeout(r, 5000));
414
+ }
415
+
416
+ const config = claimed.config;
417
+ if (!config?.agentDid) fail('Approved, but no config was returned.');
418
+ console.log(`\n ${c.green('✓')} approved — claimed config for ${c.b(config.agentDid)}`);
419
+
420
+ // BYOK: inject the local private key and prove control via the claim token.
421
+ if (state.byok && state.privateKey && config.challenge) {
422
+ config.agentKey = state.privateKey;
423
+ const signature = signChallengeHex(state.privateKey, config.challenge);
424
+ await apiPost(base, `/onboarding/requests/${encodeURIComponent(requestId)}/verify-key`, { signature, claimToken }, null);
425
+ config.keyVerified = true;
426
+ delete config.challenge;
427
+ console.log(` ${c.green('✓')} key verified — MetaMynd never saw your private key`);
428
+ }
429
+
430
+ const slug = slugify(state.name || 'metamynd-agent');
431
+ const outDir = resolve(String(args.out || `./${slug}`));
432
+ scaffoldProject({ outDir, config, slug, scope: state.scope || config.mandate?.scope || 'flight-purchase', perTxnMax: Number(state.perTxnMax) || 500, sandbox: false });
433
+ }
434
+
435
+ // ---------- main ----------
436
+ async function main() {
437
+ const args = parseArgs(process.argv.slice(2));
438
+ if (args.help) { console.log(HELP); return; }
439
+ if (args.version) {
440
+ try { console.log(JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version); }
441
+ catch { console.log('unknown'); }
442
+ return;
443
+ }
444
+
445
+ console.log(`\n${c.b(c.cyan(' create-metamynd-agent'))} ${c.dim('— provision a governed agent in ~2 minutes')}\n`);
446
+
447
+ // --sandbox: skip login + provisioning entirely.
448
+ if (args.sandbox) { await runSandbox(args); return; }
449
+ // Delegated issuance (#6): request an agent for an owner's org / claim it once approved.
450
+ if (args.request) { await runRequest(args); return; }
451
+ if (args.claim) { await runClaim(args); return; }
452
+
453
+ const interactive = !args.yes && process.stdin.isTTY;
454
+ const rl = interactive ? makeRl() : null;
455
+ const pick = async (flag, envVar, prompt, def) => {
456
+ const fromFlag = typeof args[flag] === 'string' ? args[flag] : undefined;
457
+ const fromEnv = envVar ? process.env[envVar] : undefined;
458
+ if (fromFlag !== undefined) return fromFlag;
459
+ if (fromEnv !== undefined && fromEnv !== '') return fromEnv;
460
+ if (!interactive) return def;
461
+ return ask(rl, prompt, def);
462
+ };
463
+
464
+ // 1. Connection + login
465
+ const apiRaw = await pick('api', 'METAMYND_API', 'API base URL', DEFAULT_API);
466
+ const base = String(apiRaw).replace(/\/+$/, '');
467
+ const email = await pick('email', 'METAMYND_EMAIL', 'Owner email', '');
468
+ if (!email) { rl?.close(); fail('An owner email is required (--email or METAMYND_EMAIL).'); }
469
+ let password = typeof args.password === 'string' ? args.password : process.env.METAMYND_PASSWORD;
470
+ if (password === undefined) {
471
+ if (!interactive) { rl?.close(); fail('A password is required in --yes mode (--password or METAMYND_PASSWORD).'); }
472
+ // Pause the readline interface so it doesn't consume the raw keystrokes.
473
+ rl?.pause();
474
+ password = await askHidden('Owner password');
475
+ rl?.resume();
476
+ }
477
+
478
+ console.log(c.dim(`\n → logging in to ${base} …`));
479
+ const login = await apiPost(base, '/auth/login', { username: email, password }, null);
480
+ const token = login?.data?.accessToken;
481
+ if (!token) { rl?.close(); fail('Login succeeded but no access token was returned.'); }
482
+ console.log(` ${c.green('✓')} authenticated as ${email}`);
483
+
484
+ // 2. Agent details
485
+ const name = await pick('name', null, 'Agent name', 'Support Bot');
486
+ const scope = await pick('scope', null, 'Mandate scope (governed action)', 'flight-purchase');
487
+ const perTxnMax = Number(await pick('per-txn-max', null, 'Per-transaction cap', '500')) || 500;
488
+ const maxAmount = Number(await pick('max-amount', null, 'Total mandate budget', '10000')) || 10000;
489
+ const currency = (await pick('currency', null, 'Currency', 'USD')) || 'USD';
490
+ const merchantsRaw = await pick('merchants', null, 'Allowed merchants (comma-sep, blank = any)', '');
491
+ const merchants = String(merchantsRaw).split(',').map((s) => s.trim()).filter(Boolean);
492
+
493
+ // BYOK: --byok generates a keypair on THIS machine (MetaMynd never sees the private key). An
494
+ // explicit --public-key means the caller holds the key elsewhere and will prove it themselves.
495
+ let publicKey = typeof args['public-key'] === 'string' ? args['public-key'] : undefined;
496
+ let generatedKey = null;
497
+ if (args.byok && !publicKey) {
498
+ generatedKey = generateAgentKeypair();
499
+ publicKey = generatedKey.publicKeyHex;
500
+ console.log(` ${c.green('✓')} generated an Ed25519 keypair locally ${c.dim('(private key stays on this machine)')}`);
501
+ }
502
+
503
+ const slug = slugify(name);
504
+ const outDir = resolve(String(args.out || (interactive ? await ask(rl, 'Output directory', `./${slug}`) : `./${slug}`)));
505
+
506
+ rl?.close();
507
+
508
+ // 3. Provision (one call)
509
+ console.log(c.dim(`\n → provisioning "${name}" (identity + mandate + SOP + Standards) …`));
510
+ const body = { name, scope, currency, maxAmount, perTxnMax, merchants, ...(publicKey ? { publicKey } : {}) };
511
+ const provisioned = await apiPost(base, '/onboarding/agent', body, token);
512
+ const config = provisioned?.data;
513
+ if (!config?.agentDid) fail('Provisioning did not return a config with an agentDid.');
514
+ console.log(` ${c.green('✓')} agent DID ${c.b(config.agentDid)}`);
515
+ if (config.standards?.length) console.log(` ${c.green('✓')} enforced Standards: ${config.standards.join(', ')}`);
516
+
517
+ // 3b. BYOK: prove control of the key (verify-key), else the gate blocks with AGENT_KEY_UNVERIFIED.
518
+ if (generatedKey) {
519
+ // We hold the private key — inject it into the config so the scaffolded guard can sign, and
520
+ // prove possession by signing the issued challenge.
521
+ config.agentKey = generatedKey.privateKeyHex;
522
+ if (config.challenge) {
523
+ console.log(c.dim(' → proving key control (verify-key) …'));
524
+ const signature = signChallengeHex(generatedKey.privateKeyHex, config.challenge);
525
+ await apiPost(base, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature }, token);
526
+ config.keyVerified = true;
527
+ delete config.challenge; // one-time; consumed
528
+ console.log(` ${c.green('✓')} key verified — MetaMynd never saw your private key`);
529
+ }
530
+ } else if (publicKey) {
531
+ // External BYOK key the CLI can't sign — tell the operator how to finish proving control.
532
+ console.log(` ${c.yellow('⚠ bring-your-own-key:')} no managed key minted. Prove control before the gate accepts the agent:`);
533
+ console.log(c.dim(` sign this challenge with your private key (Ed25519 over its UTF-8 bytes, hex):`));
534
+ console.log(c.dim(` challenge: ${config.challenge ?? '(none returned)'}`));
535
+ console.log(c.dim(` POST ${base}/agent-identity/${config.identityId}/verify-key { "signature": "<hex>" } (owner token)`));
536
+ }
537
+
538
+ // 4. Scaffold + next steps
539
+ scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox: false });
540
+ }
541
+
542
+ main().catch((e) => fail(e?.stack || e?.message || String(e)));
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "create-metamynd-agent",
3
+ "version": "0.1.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, and drops a runnable example.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-metamynd-agent": "index.mjs"
8
+ },
9
+ "files": [
10
+ "index.mjs",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "keywords": [
17
+ "create",
18
+ "scaffold",
19
+ "cli",
20
+ "ai",
21
+ "agent",
22
+ "governance",
23
+ "metamynd",
24
+ "agentsafe",
25
+ "mandate",
26
+ "policy"
27
+ ],
28
+ "author": "MetaMynd",
29
+ "license": "MIT",
30
+ "homepage": "https://github.com/jasimp18/AgentSafe/tree/main/integrations/create-metamynd-agent#readme",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/jasimp18/AgentSafe.git",
34
+ "directory": "integrations/create-metamynd-agent"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/jasimp18/AgentSafe/issues"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }