create-metamynd-agent 0.1.0 → 0.3.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 (2) hide show
  1. package/index.mjs +120 -24
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -12,13 +12,13 @@
12
12
  // npx create-metamynd-agent
13
13
  // npx create-metamynd-agent --api http://localhost:9926/api/v1 --email you@x.com \
14
14
  // --name "Support Bot" --scope flight-purchase --per-txn-max 500 --out ./support-bot --yes
15
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
15
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
16
16
  import { join, resolve } from 'node:path';
17
17
  import readline from 'node:readline';
18
18
  import crypto from 'node:crypto';
19
19
 
20
20
  const GUARD_PKG = '@metamynd/agentsafe-guard';
21
- const GUARD_VERSION = '^0.1.0';
21
+ const GUARD_VERSION = '^0.3.0';
22
22
  const DEFAULT_API = 'https://metamynd.ai/api/v1';
23
23
 
24
24
  // ---------- tiny ANSI ----------
@@ -39,6 +39,8 @@ function parseArgs(argv) {
39
39
  if (a === '-h' || a === '--help') { out.help = true; continue; }
40
40
  if (a === '-v' || a === '--version') { out.version = true; continue; }
41
41
  if (a === '-y' || a === '--yes' || a === '--non-interactive') { out.yes = true; continue; }
42
+ // Explicit, so `--force ./dir` cannot swallow the path as this flag's value.
43
+ if (a === '-f' || a === '--force') { out.force = true; continue; }
42
44
  if (a.startsWith('--')) {
43
45
  const eq = a.indexOf('=');
44
46
  if (eq !== -1) { out[a.slice(2, eq)] = a.slice(eq + 1); continue; }
@@ -62,6 +64,7 @@ ${c.b('Options')}
62
64
  --request Delegated: request an agent for an owner's org (--owner <email>, +--byok)
63
65
  --claim [--watch] Delegated: claim the config once the owner approves (reads metamynd-request.json)
64
66
  --owner <email> Target owner's email (with --request)
67
+ --force, -f Scaffold into a non-empty directory, overwriting existing files
65
68
  --api <url> API base (default ${DEFAULT_API})
66
69
  --email <email> Owner login email
67
70
  --password <pw> Owner password (prefer the interactive prompt or METAMYND_PASSWORD)
@@ -186,6 +189,7 @@ function exampleIndex(scope, perTxnMax) {
186
189
  // Every governed tool call is checked (allow / block / escalate) before it runs.
187
190
  import { createGuardFromConfig } from '${GUARD_PKG}';
188
191
 
192
+ // Loads agent.metamynd.json: the agent's DID, its signing key, and the gate to call.
189
193
  const guard = await createGuardFromConfig('./agent.metamynd.json'); // no env vars
190
194
 
191
195
  // --- Your real tool. Replace the body with your actual implementation. ---
@@ -205,22 +209,85 @@ const gatedBookFlight = guard.guardTool(
205
209
  }),
206
210
  );
207
211
 
208
- async function ask(label, args) {
212
+ const dim = (t) => '\\x1b[2m' + t + '\\x1b[0m';
213
+ const bold = (t) => '\\x1b[1m' + t + '\\x1b[0m';
214
+ const rule = (n) => ' ' + '-'.repeat(n);
215
+
216
+ // Plain-English meaning for the reason codes this demo can produce.
217
+ const WHY = {
218
+ AUTHORIZED: 'inside the mandate and under the SOP spend cap',
219
+ SOP_SPEND_CAP: 'your SOP caps a single transaction at $${perTxnMax}',
220
+ RISK_REVIEW: 'your SOP sends high-risk actions to a human first',
221
+ MERCHANT_NOT_ALLOWED: 'the mandate lists which merchants this agent may pay',
222
+ };
223
+
224
+ // ---------------------------------------------------------------- 1. CONTEXT
225
+ console.log('');
226
+ console.log(bold(' What this simulation shows'));
227
+ console.log('');
228
+ console.log(' An agent should not be the thing that decides what it is allowed to do.');
229
+ console.log(' This run makes that concrete: the SAME code path is attempted three times');
230
+ console.log(' and produces three different outcomes, because the decision is made');
231
+ console.log(' outside your program - and cannot be argued with from inside it.');
232
+
233
+ // ---------------------------------------------------------------- 2. MECHANISM
234
+ console.log('');
235
+ console.log(bold(' How it does that'));
236
+ console.log('');
237
+ console.log(dim(' 1. this project holds an agent identity (a DID) and its signing key'));
238
+ console.log(dim(' 2. that agent has a mandate - a scope it may act in, and a spend cap'));
239
+ console.log(dim(' 3. guardTool() wraps your tool, so nothing calls the raw handler'));
240
+ console.log(dim(' 4. each attempt is signed here, then decided by MetaMynd remotely'));
241
+ console.log(dim(' 5. your tool runs ONLY if that decision is ALLOW'));
242
+ console.log('');
243
+ console.log(dim(' scope ${scope}'));
244
+ console.log(dim(' cap $${perTxnMax} per transaction, set by your SOP'));
245
+
246
+ // ---------------------------------------------------------------- 3. THE STEPS
247
+ async function attempt(n, intent, args) {
248
+ console.log('');
249
+ console.log(bold(' Step ' + n + ' of 3') + ' - ' + intent);
250
+ console.log(dim(' signing the request locally, then asking the gate to decide...'));
209
251
  try {
210
252
  const r = await gatedBookFlight(args);
211
- console.log(' \\x1b[32m✅ ALLOW\\x1b[0m ' + label.padEnd(26) + ' → booked ' + r.pnr);
253
+ console.log('\\x1b[32m ALLOWED\\x1b[0m your tool ran and returned ' + r.pnr);
254
+ console.log(dim(' ' + WHY.AUTHORIZED));
212
255
  } catch (e) {
213
256
  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));
257
+ const why = WHY[g.reasonCode] ?? e.message;
258
+ if (g.decision === 'escalate') {
259
+ console.log('\\x1b[33m ESCALATED\\x1b[0m held for a human - ' + g.reasonCode);
260
+ console.log(dim(' ' + why));
261
+ console.log(dim(' not a failure: approve it in the dashboard and the action resumes.'));
262
+ } else {
263
+ console.log('\\x1b[31m BLOCKED\\x1b[0m ' + (g.reasonCode ?? 'refused'));
264
+ console.log(dim(' ' + why));
265
+ console.log(dim(' your tool never ran - the gate refused before execution.'));
266
+ }
216
267
  }
217
268
  }
218
269
 
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');
270
+ console.log('');
271
+ console.log(rule(66));
272
+ await attempt(1, 'a $${under} booking, low risk. Expected to pass.', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'low' });
273
+ await attempt(2, 'a $${over} booking, deliberately over the cap.', { amount: ${over}, merchant: 'skyward-air', riskLevel: 'low' });
274
+ await attempt(3, 'a $${under} booking, but flagged high risk.', { amount: ${under}, merchant: 'skyward-air', riskLevel: 'high' });
275
+ console.log('');
276
+ console.log(rule(66));
277
+
278
+ // ---------------------------------------------------------------- 4. RESULT
279
+ console.log('');
280
+ console.log(bold(' What this proved'));
281
+ console.log('');
282
+ console.log(dim(' - one code path, three outcomes. The rules decided, not this file'));
283
+ console.log(dim(' and not the model driving it.'));
284
+ console.log(dim(' - the blocked call never reached your tool at all.'));
285
+ console.log(dim(' - every decision was recorded as tamper-evident evidence.'));
286
+ console.log(dim(' - if the gate were unreachable the guard fails CLOSED: it blocks.'));
287
+ console.log('');
288
+ console.log(' Change the cap in the dashboard (Legal Entity -> SOPs) and run again.');
289
+ console.log(dim(' The outcome changes. This file does not. That is the point.'));
290
+ console.log('');
224
291
  `;
225
292
  }
226
293
 
@@ -272,22 +339,48 @@ function gitignore() {
272
339
  return `node_modules/\nagent.metamynd.json\n.env\n`;
273
340
  }
274
341
 
275
- function writeFileSafe(dir, name, content) {
342
+ function writeFileSafe(dir, name, content, force = false) {
276
343
  const p = join(dir, name);
277
- if (existsSync(p)) { console.log(` ${c.yellow('skip')} ${name} ${c.dim('(exists)')}`); return; }
344
+ const exists = existsSync(p);
345
+ if (exists && !force) { console.log(` ${c.yellow('skip')} ${name} ${c.dim('(exists)')}`); return; }
278
346
  writeFileSync(p, content);
279
- console.log(` ${c.green('create')} ${name}`);
347
+ console.log(` ${exists ? c.yellow('overwrite') : c.green('create')} ${name}`);
348
+ }
349
+
350
+ /**
351
+ * Refuse to scaffold into a non-empty directory unless --force.
352
+ *
353
+ * Silently skipping an existing agent.metamynd.json is worse than it sounds:
354
+ * provisioning has already minted a NEW agent server-side, so the scaffold prints
355
+ * success while leaving the OLD config in place. Every later gate call then runs as
356
+ * the previous identity, against whatever apiBase that file happens to carry — which
357
+ * is exactly how a stale http:// base survived a re-scaffold and 404'd every call.
358
+ */
359
+ function assertScaffoldTarget(outDir, force) {
360
+ if (force || !existsSync(outDir)) return;
361
+ const entries = readdirSync(outDir);
362
+ if (entries.length === 0) return;
363
+ const rel = outDir.replace(resolve('.'), '.').replace(/\\/g, '/');
364
+ fail(
365
+ `${rel} is not empty (${entries.length} item${entries.length === 1 ? '' : 's'}).\n\n` +
366
+ ` Scaffolding here would KEEP the existing files — including any agent.metamynd.json —\n` +
367
+ ` so this project would keep running as the identity in that file, against the apiBase\n` +
368
+ ` in that file, and the newly provisioned agent would go unused.\n\n` +
369
+ ` Scaffold somewhere new: --out ./another-dir\n` +
370
+ ` or overwrite this one on purpose: --force`,
371
+ );
280
372
  }
281
373
 
282
374
  /** Write the scaffolded project + print next steps. Shared by the provision and sandbox paths. */
283
- function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox }) {
375
+ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox, force = false }) {
376
+ assertScaffoldTarget(outDir, force);
284
377
  console.log(`\n ${c.b('Scaffolding')} ${c.dim(outDir)}`);
285
378
  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));
379
+ writeFileSafe(outDir, 'agent.metamynd.json', JSON.stringify(config, null, 2) + '\n', force);
380
+ writeFileSafe(outDir, 'index.mjs', exampleIndex(scope, perTxnMax), force);
381
+ writeFileSafe(outDir, 'package.json', examplePackageJson(slug), force);
382
+ writeFileSafe(outDir, '.gitignore', gitignore(), force);
383
+ writeFileSafe(outDir, 'README.md', exampleReadme(slug, scope), force);
291
384
 
292
385
  const rel = outDir.replace(resolve('.'), '.').replace(/\\/g, '/');
293
386
  console.log(`\n${c.green(c.b(' ✓ Done.'))} Your governed agent is ready.\n`);
@@ -307,6 +400,10 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox }) {
307
400
  async function runSandbox(args) {
308
401
  const apiRaw = (typeof args.api === 'string' ? args.api : undefined) ?? process.env.METAMYND_API ?? DEFAULT_API;
309
402
  const base = String(apiRaw).replace(/\/+$/, '');
403
+ // Check the target BEFORE provisioning: refusing afterwards would mint an agent
404
+ // server-side and then throw it away.
405
+ const outDir = resolve(String(args.out || './metamynd-sandbox'));
406
+ assertScaffoldTarget(outDir, !!args.force);
310
407
  console.log(c.dim(` → requesting a sandbox agent from ${base} …`));
311
408
  const provisioned = await apiPost(base, '/onboarding/sandbox', {}, null);
312
409
  const config = provisioned?.data;
@@ -314,8 +411,7 @@ async function runSandbox(args) {
314
411
  console.log(` ${c.green('✓')} sandbox agent ${c.b(config.agentDid)} ${c.dim('(shared test identity)')}`);
315
412
  const scope = config.mandate?.scope || 'flight-purchase';
316
413
  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 });
414
+ scaffoldProject({ outDir, config, slug: 'metamynd-sandbox', scope, perTxnMax, sandbox: true, force: !!args.force });
319
415
  }
320
416
 
321
417
  // ---------- delegated issuance (#6) ----------
@@ -429,7 +525,7 @@ async function runClaim(args) {
429
525
 
430
526
  const slug = slugify(state.name || 'metamynd-agent');
431
527
  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 });
528
+ scaffoldProject({ outDir, config, slug, scope: state.scope || config.mandate?.scope || 'flight-purchase', perTxnMax: Number(state.perTxnMax) || 500, sandbox: false, force: !!args.force });
433
529
  }
434
530
 
435
531
  // ---------- main ----------
@@ -536,7 +632,7 @@ async function main() {
536
632
  }
537
633
 
538
634
  // 4. Scaffold + next steps
539
- scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox: false });
635
+ scaffoldProject({ outDir, config, slug, scope, perTxnMax, sandbox: false, force: !!args.force });
540
636
  }
541
637
 
542
638
  main().catch((e) => fail(e?.stack || e?.message || String(e)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-metamynd-agent",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
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, and drops a runnable example.",
5
5
  "type": "module",
6
6
  "bin": {