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.
- package/index.mjs +120 -24
- 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.
|
|
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
|
-
|
|
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('
|
|
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
|
|
215
|
-
|
|
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('
|
|
220
|
-
|
|
221
|
-
await
|
|
222
|
-
await
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|