create-metamynd-agent 0.14.34 → 0.14.43
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 +159 -65
- package/package.json +2 -2
package/index.mjs
CHANGED
|
@@ -87,8 +87,17 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
|
|
|
87
87
|
// 0.30.0: a local permit or containment refusal is only given on a bundle at most lifecycleMaxAgeMs (5 s) old, so a
|
|
88
88
|
// suspension or reinstatement reaches a running agent within seconds, not within the bundle's 10-minute maxStaleness.
|
|
89
89
|
// 0.31.0: a guarded tool's decision carries governanceHeaders() — the signed request and its authorization, ready for a
|
|
90
|
-
// gateway — so a tool no longer rebuilds and re-signs it.
|
|
91
|
-
|
|
90
|
+
// gateway — so a tool no longer rebuilds and re-signs it. Required: the scaffold's gateway calls (bookFlightViaGateway,
|
|
91
|
+
// performViaGateway, and npm run resume through them) send decision.governanceHeaders() (pre-beta rerun 6, FW6-2).
|
|
92
|
+
// 0.32.0: resume() signs the in-process resume claim as MAGP-RESUME-CLAIM-v2, binding the approved request and context at the
|
|
93
|
+
// issuer (pre-beta rerun 6 F-1-NF-R); an issuer that requires it refuses the v1 claim older guards sign.
|
|
94
|
+
// 0.33.0: the bundled policy-core judges owner-keyed operation rules (context-value-in / -not-in, NF-RISK-SELF) like the
|
|
95
|
+
// gate, so a local check escalates an owner-marked op (e.g. delete-all) whatever riskLevel the agent declares.
|
|
96
|
+
// 0.34.0: a tool whose gateway predates the approved-context binding (AUTHORIZATION_CONTEXT_REQUIRED) no longer voids the approval;
|
|
97
|
+
// resume() works again once the gateway is upgraded, and a voided approval is reported AUTHORIZATION_VOIDED (pre-beta rerun 6 FW6-1).
|
|
98
|
+
// 0.35.0: a payment in another currency than a currency-scoped cap is refused CURRENCY_NOT_ALLOWED, not SOP_SPEND_CAP (M2).
|
|
99
|
+
// 0.36.0: ships TypeScript declarations (agentsafe-guard.d.mts), so a TypeScript agent gets types with no @types package (L5).
|
|
100
|
+
const GUARD_VERSION = '^0.36.0';
|
|
92
101
|
/** The harness entry point's config load, shared by both harness templates: a fresh clone has no
|
|
93
102
|
* agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
|
|
94
103
|
function harnessConfigLoad() {
|
|
@@ -172,7 +181,12 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
|
172
181
|
// 0.27.0: a claim of a person-approved hold states the digest of the context it executes (MAGP §9a.5), and a grant naming
|
|
173
182
|
// another (or none) is released and refused. Required: the issuer refuses AUTHORIZATION_CONTEXT_REQUIRED to a claimer that
|
|
174
183
|
// states none, so an older gateway could no longer run an approved escalation.
|
|
175
|
-
|
|
184
|
+
// 0.27.1: reportOutcome() reports an authorization a request only NAMED as presented, never as the one it ran under (FW6-3).
|
|
185
|
+
// 0.28.0: a gateway judges owner-keyed operation rules (context-value-in / -not-in) from its bundle like the gate
|
|
186
|
+
// (NF-RISK-SELF). Required: a gateway on an older guard would read an operation rule's atom as unknown and let it run.
|
|
187
|
+
// 0.29.0: a claim the issuer refuses is logged in one line and its block carries the issuer's detail (FW6-1).
|
|
188
|
+
// 0.30.0: same CURRENCY_NOT_ALLOWED code in a gateway's local judgement as at the gate (M2).
|
|
189
|
+
const MCP_GUARD_VERSION = '^0.30.0';
|
|
176
190
|
/** A DID as it may appear inside a generated string literal (the gateway's allowedAgents pin): no quote, backslash or space. */
|
|
177
191
|
const SAFE_DID = /^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/;
|
|
178
192
|
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
@@ -206,7 +220,13 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
|
206
220
|
// 0.24.0: depends on agentsafe-mcp-guard ^0.25.0 (one guard in the tree with MCP_GUARD_VERSION above). No template change.
|
|
207
221
|
// 0.25.0: depends on agentsafe-mcp-guard ^0.26.0 (one guard in the tree). No template change.
|
|
208
222
|
// 0.26.0: depends on agentsafe-mcp-guard ^0.27.0 (the approved-context binding). Required with MCP_GUARD_VERSION above.
|
|
209
|
-
|
|
223
|
+
// 0.26.1: a report names an authorization as claimed only when this gateway claimed it (FW6-3); depends on agentsafe-mcp-guard ^0.27.1.
|
|
224
|
+
// 0.27.0: depends on agentsafe-mcp-guard ^0.28.0 (operation rules). Required with MCP_GUARD_VERSION above.
|
|
225
|
+
// 0.28.0: a 403 for a refused claim carries the issuer's detail (FW6-1); depends on agentsafe-mcp-guard ^0.29.0.
|
|
226
|
+
// 0.29.0: a request signed for another action than the route's is refused GATEWAY_ACTION_MISMATCH, not SIGNATURE_INVALID (M2);
|
|
227
|
+
// depends on agentsafe-mcp-guard ^0.30.0.
|
|
228
|
+
// 0.30.0: `agentsafe-gateway service-id` / `accept-challenge` / `serve`, SERVICE_IDENTITY_FILE and AGENTSAFE_UPSTREAM_CREDENTIAL (M4).
|
|
229
|
+
const GATEWAY_VERSION = '^0.30.0';
|
|
210
230
|
|
|
211
231
|
/** Appended to every scaffolded gateway server: give hold settlements still running a bounded moment on shutdown. */
|
|
212
232
|
const DRAIN_ON_SHUTDOWN = `
|
|
@@ -941,7 +961,9 @@ function daemonRequest(socketPath, op, params, { connectTimeoutMs = 5000 } = {})
|
|
|
941
961
|
sock.once('error', (err) => {
|
|
942
962
|
cleanup();
|
|
943
963
|
if (settled) return;
|
|
944
|
-
|
|
964
|
+
// ECONNREFUSED too: the one-shot admin socket is called right as `start --admin` builds it,
|
|
965
|
+
// and a Unix socket refuses between bind() and listen(). A refused connect sent nothing.
|
|
966
|
+
if ((err.code === 'ENOENT' || err.code === 'ECONNREFUSED') && Date.now() < deadline) { setTimeout(attempt, 20); return; }
|
|
945
967
|
settled = true;
|
|
946
968
|
clearTimeout(overallTimer);
|
|
947
969
|
reject(Object.assign(new Error(`agentsafe-signer daemon unreachable at ${socketPath}: ${err.message}`), { code: 'DAEMON_UNREACHABLE' }));
|
|
@@ -981,6 +1003,10 @@ async function apiPost(base, path, body, token) {
|
|
|
981
1003
|
} catch (e) {
|
|
982
1004
|
fail(`Cannot reach ${base}${path} — is the API up? (${e.message})`);
|
|
983
1005
|
}
|
|
1006
|
+
return readApiJson(res, path);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
async function readApiJson(res, path) {
|
|
984
1010
|
const text = await res.text();
|
|
985
1011
|
let json = null;
|
|
986
1012
|
try { json = text ? JSON.parse(text) : null; } catch { /* non-JSON */ }
|
|
@@ -991,6 +1017,70 @@ async function apiPost(base, path, body, token) {
|
|
|
991
1017
|
return json;
|
|
992
1018
|
}
|
|
993
1019
|
|
|
1020
|
+
// ---------- owner session ----------
|
|
1021
|
+
// An owner access token lives 15 minutes (pre-beta rerun 6, OBS session TTL, #926); the refresh token from the same
|
|
1022
|
+
// sign-in renews it (POST /auth/refresh-token, which rotates both). Every owner-authenticated call this CLI makes goes
|
|
1023
|
+
// through ownerFetch: on a 401 it renews the session ONCE — the refresh token first, else a fresh sign-in with the
|
|
1024
|
+
// credentials already given to this run — and retries the call once. The password stays in this process's memory
|
|
1025
|
+
// for that purpose only; it is never written anywhere (pre-beta rerun 6, CLI login expiry).
|
|
1026
|
+
async function ownerLogin(base, email, password) {
|
|
1027
|
+
const login = await apiPost(base, '/auth/login', { username: email, password }, null);
|
|
1028
|
+
const token = login?.data?.accessToken;
|
|
1029
|
+
if (!token) fail('Login succeeded but no access token was returned.');
|
|
1030
|
+
return { base, email, password, token, refreshToken: login?.data?.refreshToken ?? null };
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/** Renew an expired owner session in place; true when a new access token was obtained. Never fatal. */
|
|
1034
|
+
async function renewOwnerSession(session) {
|
|
1035
|
+
const post = (path, body) => fetch(`${session.base}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
1036
|
+
if (session.refreshToken) {
|
|
1037
|
+
try {
|
|
1038
|
+
const res = await post('/auth/refresh-token', { refreshToken: session.refreshToken });
|
|
1039
|
+
const json = res.ok ? await res.json().catch(() => null) : null;
|
|
1040
|
+
// The refresh endpoint answers at the top level ({ accessToken, refreshToken }); accept a `data` envelope too.
|
|
1041
|
+
const token = json?.accessToken ?? json?.data?.accessToken;
|
|
1042
|
+
if (token) {
|
|
1043
|
+
session.token = token;
|
|
1044
|
+
session.refreshToken = json?.refreshToken ?? json?.data?.refreshToken ?? session.refreshToken;
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
} catch { /* fall through to a fresh sign-in */ }
|
|
1048
|
+
}
|
|
1049
|
+
if (typeof session.password === 'string') {
|
|
1050
|
+
try {
|
|
1051
|
+
const res = await post('/auth/login', { username: session.email, password: session.password });
|
|
1052
|
+
const json = res.ok ? await res.json().catch(() => null) : null;
|
|
1053
|
+
if (json?.data?.accessToken) {
|
|
1054
|
+
session.token = json.data.accessToken;
|
|
1055
|
+
session.refreshToken = json.data.refreshToken ?? null;
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
} catch { /* reported by the caller's retry */ }
|
|
1059
|
+
}
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/** fetch() with the owner's bearer token; a 401 renews the session once and retries once. */
|
|
1064
|
+
async function ownerFetch(session, path, init = {}) {
|
|
1065
|
+
const send = () => fetch(`${session.base}${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...(init.headers ?? {}), Authorization: `Bearer ${session.token}` } });
|
|
1066
|
+
const res = await send();
|
|
1067
|
+
if (res.status !== 401) return res;
|
|
1068
|
+
if (!(await renewOwnerSession(session))) return res;
|
|
1069
|
+
console.log(c.dim(' → your owner session had expired; renewed it and retried'));
|
|
1070
|
+
return send();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/** apiPost for an owner-authenticated call: fatal on failure, renewed once on an expired token. */
|
|
1074
|
+
async function ownerPost(session, path, body) {
|
|
1075
|
+
let res;
|
|
1076
|
+
try {
|
|
1077
|
+
res = await ownerFetch(session, path, { method: 'POST', body: JSON.stringify(body) });
|
|
1078
|
+
} catch (e) {
|
|
1079
|
+
fail(`Cannot reach ${session.base}${path} — is the API up? (${e.message})`);
|
|
1080
|
+
}
|
|
1081
|
+
return readApiJson(res, path);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
994
1084
|
/** GET a public, read-only endpoint; null on ANY failure (unreachable, non-2xx, non-JSON) — never fatal. */
|
|
995
1085
|
async function softGet(base, path) {
|
|
996
1086
|
try {
|
|
@@ -1059,24 +1149,22 @@ async function resolveHostedRulePack(base, key, { financial, perTxnMax, maxAmoun
|
|
|
1059
1149
|
* dashboard registration shows its own dialog before setting the flag); see the printed message
|
|
1060
1150
|
* either way.
|
|
1061
1151
|
*/
|
|
1062
|
-
async function registerGatewayCounterparty(
|
|
1152
|
+
async function registerGatewayCounterparty(session, did, label, purpose = 'claim', privateKeyHex = null, agents = null) {
|
|
1063
1153
|
try {
|
|
1064
1154
|
// The gateway accepts its own registration (MAGP §8.7.6): ask for a challenge naming this owner, sign it with the gateway's
|
|
1065
1155
|
// key (generated here, so this process holds it), and register with that proof. A DID is never registered on its text.
|
|
1066
1156
|
let proof;
|
|
1067
1157
|
if (privateKeyHex) {
|
|
1068
|
-
const ch = await
|
|
1158
|
+
const ch = await ownerFetch(session, '/policy/counterparties/challenge', {
|
|
1069
1159
|
method: 'POST',
|
|
1070
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
1071
1160
|
body: JSON.stringify({ did, purpose }),
|
|
1072
1161
|
});
|
|
1073
1162
|
const chJson = await ch.json().catch(() => null);
|
|
1074
1163
|
if (!ch.ok || !chJson?.data?.message) return { ok: false, message: chJson?.message ?? `challenge HTTP ${ch.status}` };
|
|
1075
1164
|
proof = { challengeToken: chJson.data.challengeToken, signature: signChallengeHex(privateKeyHex, chJson.data.message) };
|
|
1076
1165
|
}
|
|
1077
|
-
const res = await
|
|
1166
|
+
const res = await ownerFetch(session, '/policy/counterparties', {
|
|
1078
1167
|
method: 'POST',
|
|
1079
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
1080
1168
|
// `report` (a non-financial gateway, which never claims): it may only report to this owner, and it changes nothing about
|
|
1081
1169
|
// who may claim their holds, so it needs no enforcement confirmation (MAGP §8.7.6, §16.4).
|
|
1082
1170
|
// `agents` (report only): the agents this gateway serves — the issuer lets it claim an approved value-less hold of THOSE
|
|
@@ -1452,31 +1540,24 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
|
|
|
1452
1540
|
// --- value-bearing action (sealValueActions, on by default) — its authorizationId is what lets
|
|
1453
1541
|
// --- the gateway atomically claim single-use execution, closing replay + cumulative spend, not
|
|
1454
1542
|
// --- just re-checking policy. See ./gateway/README.md.
|
|
1543
|
+
// --- decision.governanceHeaders() is that verdict as headers: the SAME request the gate checked (the
|
|
1544
|
+
// --- gatedBookFlight mapping below, payload included), freshly signed, carrying its authorizationId.
|
|
1545
|
+
// --- So this file never rebuilds or re-signs the request, and an approved escalation resumed with
|
|
1546
|
+
// --- gatedBookFlight.resume() reaches the gateway with the approval's authorization the same way.
|
|
1455
1547
|
async function bookFlightViaGateway(args, decision) {
|
|
1456
|
-
// The COMPLETE body the tool receives
|
|
1457
|
-
//
|
|
1458
|
-
// refuses to run the tool on a body that is not exactly this one.
|
|
1548
|
+
// The COMPLETE body the tool receives — the payload gatedBookFlight signs (MAGP 8.3.9). The gateway refuses to run the
|
|
1549
|
+
// tool on a body that is not exactly the one signed, so keep this in step with the mapping below if you add a field.
|
|
1459
1550
|
const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
|
|
1460
|
-
const signed = await guard.buildSignedRequest({
|
|
1461
|
-
action: '${scope}',
|
|
1462
|
-
amount: args.amount,
|
|
1463
|
-
currency: args.currency ?? '${currency}',
|
|
1464
|
-
merchant: args.merchant,
|
|
1465
|
-
// Passed through as-is (never defaulted): a missing riskLevel must reach the gateway as missing.
|
|
1466
|
-
context: { tool: 'book-flight', riskLevel: args.riskLevel },
|
|
1467
|
-
payload,
|
|
1468
|
-
});
|
|
1469
|
-
signed.authorizationId = decision?.authorizationId;
|
|
1470
1551
|
const res = await fetch(GATEWAY + '/book-flight', {
|
|
1471
1552
|
method: 'POST',
|
|
1472
|
-
headers: { 'content-type': 'application/json',
|
|
1553
|
+
headers: { 'content-type': 'application/json', ...(await decision.governanceHeaders()) },
|
|
1473
1554
|
body: JSON.stringify(payload),
|
|
1474
1555
|
});
|
|
1475
1556
|
const body = await res.json().catch(() => null);
|
|
1476
1557
|
if (!res.ok) {
|
|
1477
1558
|
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
1478
1559
|
err.name = 'GovernanceBlocked';
|
|
1479
|
-
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
|
|
1560
|
+
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
|
|
1480
1561
|
throw err;
|
|
1481
1562
|
}
|
|
1482
1563
|
return body;
|
|
@@ -1496,7 +1577,9 @@ const gatedBookFlight = guard.guardTool(
|
|
|
1496
1577
|
// the action for review — never 'low'.
|
|
1497
1578
|
context: { tool: 'book-flight', riskLevel: a.riskLevel },
|
|
1498
1579
|
// The authorization is bound to the SAME body bookFlightViaGateway() sends, so the gate records what this agent
|
|
1499
|
-
// signed and the gateway can prove it is running exactly that.
|
|
1580
|
+
// signed and the gateway can prove it is running exactly that. This mapping is also what governanceHeaders() signs
|
|
1581
|
+
// for the gateway (riskLevel passed through as-is: a missing one reaches the gateway as missing). Keep the two in
|
|
1582
|
+
// step if you add a field.
|
|
1500
1583
|
payload: { amount: a.amount, merchant: a.merchant, currency: a.currency ?? '${currency}' },
|
|
1501
1584
|
}),
|
|
1502
1585
|
);
|
|
@@ -1690,20 +1773,21 @@ ${withGateway ? `const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${
|
|
|
1690
1773
|
// --- Calls the gateway process instead of a local function. There is no raw performAction() in
|
|
1691
1774
|
// --- this file to call directly — the tool, and any real credentials it needs, live only in
|
|
1692
1775
|
// --- ./gateway, which independently re-verifies this signed request itself.
|
|
1776
|
+
// --- decision.governanceHeaders() is the request gatedAction checked (its jurisdiction SIGNED, a top-level
|
|
1777
|
+
// --- field, never context), freshly signed, with the authorization it was granted (none for a value-less
|
|
1778
|
+
// --- action — see ./gateway/README.md). So this file never rebuilds or re-signs it, and npm run resume
|
|
1779
|
+
// --- reaches the gateway through here the same way.
|
|
1693
1780
|
async function performViaGateway(args, decision) {
|
|
1694
|
-
const { jurisdiction, ...context } = args; // the jurisdiction is SIGNED (a top-level field), never context
|
|
1695
|
-
const signed = await guard.buildSignedRequest({ action: '${scope}', merchant: MERCHANT, jurisdiction, context });
|
|
1696
|
-
signed.authorizationId = decision?.authorizationId; // none for a value-less action — see ./gateway/README.md
|
|
1697
1781
|
const res = await fetch(GATEWAY + '/perform', {
|
|
1698
1782
|
method: 'POST',
|
|
1699
|
-
headers: { 'content-type': 'application/json',
|
|
1783
|
+
headers: { 'content-type': 'application/json', ...(await decision.governanceHeaders()) },
|
|
1700
1784
|
body: '{}', // this tool reads nothing from the body; the request's fields travel in the SIGNED context
|
|
1701
1785
|
});
|
|
1702
1786
|
const body = await res.json().catch(() => null);
|
|
1703
1787
|
if (!res.ok) {
|
|
1704
1788
|
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
1705
1789
|
err.name = 'GovernanceBlocked';
|
|
1706
|
-
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
|
|
1790
|
+
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
|
|
1707
1791
|
throw err;
|
|
1708
1792
|
}
|
|
1709
1793
|
return body;
|
|
@@ -2528,7 +2612,11 @@ re-register this gateway yourself — after removing it from the registry, say.
|
|
|
2528
2612
|
Registration needs **proof of control** (MAGP §8.7.6): you ask for a challenge naming you, this
|
|
2529
2613
|
gateway signs it with its own key from \`service.metamynd.json\`, and you register with that
|
|
2530
2614
|
signature. A DID is never registered on its text alone, so nobody can register a gateway they do
|
|
2531
|
-
not hold the key for. Run this from this directory (bash; \`TOKEN\` is your owner access token)
|
|
2615
|
+
not hold the key for. Run this from this directory (bash; \`TOKEN\` is your owner access token).
|
|
2616
|
+
An owner access token lasts **15 minutes**, so get a fresh one just before you run this:
|
|
2617
|
+
\`POST /auth/login\` with \`{ "username": "<your email>", "password": "<your password>" }\` returns it
|
|
2618
|
+
as \`data.accessToken\` (that sign-in also signs your account out of the dashboard — one session at a
|
|
2619
|
+
time). A \`401\` from the snippet means the token expired: sign in again and re-run it.
|
|
2532
2620
|
|
|
2533
2621
|
\`\`\`bash
|
|
2534
2622
|
MAGP_API=https://metamynd.ai/api/v1 TOKEN=<owner access token> node --input-type=module -e "
|
|
@@ -3462,7 +3550,7 @@ async function callGateway(path, action, args) {
|
|
|
3462
3550
|
if (!res.ok) {
|
|
3463
3551
|
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
3464
3552
|
err.name = 'GovernanceBlocked';
|
|
3465
|
-
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
|
|
3553
|
+
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
|
|
3466
3554
|
throw err;
|
|
3467
3555
|
}
|
|
3468
3556
|
return body;
|
|
@@ -3674,7 +3762,7 @@ async function callGateway(path, action, args) {
|
|
|
3674
3762
|
if (!res.ok) {
|
|
3675
3763
|
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
3676
3764
|
err.name = 'GovernanceBlocked';
|
|
3677
|
-
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
|
|
3765
|
+
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
|
|
3678
3766
|
throw err;
|
|
3679
3767
|
}
|
|
3680
3768
|
return body;
|
|
@@ -4070,11 +4158,9 @@ async function authFlow(args) {
|
|
|
4070
4158
|
rl?.resume();
|
|
4071
4159
|
}
|
|
4072
4160
|
rl?.close();
|
|
4073
|
-
const
|
|
4074
|
-
const token = login?.data?.accessToken;
|
|
4075
|
-
if (!token) fail('Login succeeded but no access token was returned.');
|
|
4161
|
+
const session = await ownerLogin(base, email, password);
|
|
4076
4162
|
console.log(c.dim(` ${SESSION_REPLACED_NOTE}`));
|
|
4077
|
-
return { base,
|
|
4163
|
+
return { base, session, email };
|
|
4078
4164
|
}
|
|
4079
4165
|
|
|
4080
4166
|
// One session per account: a CLI login replaces the account's dashboard session (the dashboard then answers
|
|
@@ -4090,7 +4176,7 @@ const REQUEST_STATE_FILE = 'metamynd-request.json';
|
|
|
4090
4176
|
async function runRequest(args) {
|
|
4091
4177
|
const owner = (typeof args.owner === 'string' ? args.owner : undefined) ?? process.env.METAMYND_OWNER;
|
|
4092
4178
|
if (!owner) fail('--owner <ownerEmail> is required for a delegated request.');
|
|
4093
|
-
const { base,
|
|
4179
|
+
const { base, session } = await authFlow(args);
|
|
4094
4180
|
|
|
4095
4181
|
// The same decision --harness and the hosted flow make (see announceFinancial): a non-financial request asks the
|
|
4096
4182
|
// OWNER to approve no spending authority at all, and says so explicitly - an omitted amount means "use the
|
|
@@ -4118,7 +4204,7 @@ async function runRequest(args) {
|
|
|
4118
4204
|
}
|
|
4119
4205
|
|
|
4120
4206
|
console.log(c.dim(` → requesting "${name}" for ${owner} …`));
|
|
4121
|
-
const res = await
|
|
4207
|
+
const res = await ownerPost(session, '/onboarding/requests', { ownerEmail: owner, name, scope, ...(financial ? { perTxnMax, ...(maxAmountReq !== undefined && Number.isFinite(maxAmountReq) ? { maxAmount: maxAmountReq } : {}), ...(currencyReq ? { currency: currencyReq } : {}) } : { financial: false }), ...(merchantsReq.length ? { merchants: merchantsReq } : {}), ...(publicKey ? { publicKey } : {}) });
|
|
4122
4208
|
const d = res.data;
|
|
4123
4209
|
// A server that predates non-financial requests ignores the field and files the request WITH default spend
|
|
4124
4210
|
// limits. The developer cannot withdraw it, so say exactly what now exists and who can deny it; do not save a
|
|
@@ -4268,7 +4354,9 @@ async function main() {
|
|
|
4268
4354
|
return ask(rl, prompt, def);
|
|
4269
4355
|
};
|
|
4270
4356
|
|
|
4271
|
-
// 1. Connection +
|
|
4357
|
+
// 1. Connection + credentials. The sign-in itself waits until every prompt is answered (step 3): an owner access
|
|
4358
|
+
// token lives 15 minutes, and an owner who took longer over the prompts used to reach provisioning with an expired
|
|
4359
|
+
// one (pre-beta rerun 6, CLI login expiry follow-up to #926).
|
|
4272
4360
|
const apiRaw = await pick('api', 'METAMYND_API', 'API base URL', DEFAULT_API);
|
|
4273
4361
|
const base = String(apiRaw).replace(/\/+$/, '');
|
|
4274
4362
|
const email = await pick('email', 'METAMYND_EMAIL', 'Owner email', '');
|
|
@@ -4282,13 +4370,6 @@ async function main() {
|
|
|
4282
4370
|
rl?.resume();
|
|
4283
4371
|
}
|
|
4284
4372
|
|
|
4285
|
-
console.log(c.dim(`\n → logging in to ${base} …`));
|
|
4286
|
-
const login = await apiPost(base, '/auth/login', { username: email, password }, null);
|
|
4287
|
-
const token = login?.data?.accessToken;
|
|
4288
|
-
if (!token) { rl?.close(); fail('Login succeeded but no access token was returned.'); }
|
|
4289
|
-
console.log(` ${c.green('✓')} authenticated as ${email}`);
|
|
4290
|
-
console.log(c.dim(` ${SESSION_REPLACED_NOTE}`));
|
|
4291
|
-
|
|
4292
4373
|
// 2. Agent details — a --config file's fields are the default at every prompt/flag below.
|
|
4293
4374
|
const name = await pick('name', null, 'Agent name', fileConfig?.name ?? 'Support Bot');
|
|
4294
4375
|
const scope = await pick('scope', null, 'Mandate scope (governed action)', fileConfig?.scope ?? (financial ? 'flight-purchase' : 'perform-action'));
|
|
@@ -4333,19 +4414,6 @@ async function main() {
|
|
|
4333
4414
|
rl?.close();
|
|
4334
4415
|
fail('--daemon-socket generates its own key — pass --byok alone, not --public-key.');
|
|
4335
4416
|
}
|
|
4336
|
-
let generatedKey = null;
|
|
4337
|
-
let daemonPublicKeyHex = null;
|
|
4338
|
-
if (args.byok && !publicKey && daemonSocket) {
|
|
4339
|
-
console.log(c.dim(' → asking the agentsafe-signer daemon to generate a key …'));
|
|
4340
|
-
const { publicKeyHex } = await daemonRequest(daemonAdminSocket, 'generate-key', { allowRekey: false });
|
|
4341
|
-
daemonPublicKeyHex = publicKeyHex;
|
|
4342
|
-
publicKey = publicKeyHex;
|
|
4343
|
-
console.log(` ${c.green('✓')} generated an Ed25519 keypair via the signer daemon ${c.dim('(the private key never left it)')}`);
|
|
4344
|
-
} else if (args.byok && !publicKey) {
|
|
4345
|
-
generatedKey = generateAgentKeypair();
|
|
4346
|
-
publicKey = generatedKey.publicKeyHex;
|
|
4347
|
-
console.log(` ${c.green('✓')} generated an Ed25519 keypair locally ${c.dim('(private key stays on this machine)')}`);
|
|
4348
|
-
}
|
|
4349
4417
|
|
|
4350
4418
|
const slug = slugify(name);
|
|
4351
4419
|
const outDir = resolve(String(args.out || (interactive ? await ask(rl, 'Output directory', `./${slug}`) : `./${slug}`)));
|
|
@@ -4368,6 +4436,30 @@ async function main() {
|
|
|
4368
4436
|
if (pack.reviewedMoleculeIds) sopFields.reviewedMoleculeIds = pack.reviewedMoleculeIds;
|
|
4369
4437
|
}
|
|
4370
4438
|
}
|
|
4439
|
+
|
|
4440
|
+
// Sign in only now, with every answer in hand (see step 1). Should the token still expire before the last
|
|
4441
|
+
// owner call, ownerFetch renews it once and retries.
|
|
4442
|
+
console.log(c.dim(`\n → logging in to ${base} …`));
|
|
4443
|
+
const session = await ownerLogin(base, email, password);
|
|
4444
|
+
console.log(` ${c.green('✓')} authenticated as ${email}`);
|
|
4445
|
+
console.log(c.dim(` ${SESSION_REPLACED_NOTE}`));
|
|
4446
|
+
|
|
4447
|
+
// The key is generated after the sign-in succeeds, as it always was: a daemon refuses to re-key (allowRekey: false),
|
|
4448
|
+
// so a key minted for a run that then failed to sign in would block the next attempt.
|
|
4449
|
+
let generatedKey = null;
|
|
4450
|
+
let daemonPublicKeyHex = null;
|
|
4451
|
+
if (args.byok && !publicKey && daemonSocket) {
|
|
4452
|
+
console.log(c.dim(' → asking the agentsafe-signer daemon to generate a key …'));
|
|
4453
|
+
const { publicKeyHex } = await daemonRequest(daemonAdminSocket, 'generate-key', { allowRekey: false });
|
|
4454
|
+
daemonPublicKeyHex = publicKeyHex;
|
|
4455
|
+
publicKey = publicKeyHex;
|
|
4456
|
+
console.log(` ${c.green('✓')} generated an Ed25519 keypair via the signer daemon ${c.dim('(the private key never left it)')}`);
|
|
4457
|
+
} else if (args.byok && !publicKey) {
|
|
4458
|
+
generatedKey = generateAgentKeypair();
|
|
4459
|
+
publicKey = generatedKey.publicKeyHex;
|
|
4460
|
+
console.log(` ${c.green('✓')} generated an Ed25519 keypair locally ${c.dim('(private key stays on this machine)')}`);
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4371
4463
|
console.log(c.dim(`\n → provisioning "${name}" (identity + mandate + SOP + Standards) …`));
|
|
4372
4464
|
const body = {
|
|
4373
4465
|
name, scope,
|
|
@@ -4386,7 +4478,7 @@ async function main() {
|
|
|
4386
4478
|
...(publicKey ? { publicKey } : {}),
|
|
4387
4479
|
...sopFields,
|
|
4388
4480
|
};
|
|
4389
|
-
const provisioned = await
|
|
4481
|
+
const provisioned = await ownerPost(session, '/onboarding/agent', body);
|
|
4390
4482
|
const config = provisioned?.data;
|
|
4391
4483
|
if (!config?.agentDid) fail('Provisioning did not return a config with an agentDid.');
|
|
4392
4484
|
console.log(` ${c.green('✓')} agent DID ${c.b(config.agentDid)}`);
|
|
@@ -4407,7 +4499,7 @@ async function main() {
|
|
|
4407
4499
|
if (config.challenge) {
|
|
4408
4500
|
console.log(c.dim(' → proving key control via the daemon (verify-key) …'));
|
|
4409
4501
|
const { signature } = await daemonRequest(daemonSocket, 'sign-key-control-challenge', { challenge: config.challenge });
|
|
4410
|
-
await
|
|
4502
|
+
await ownerPost(session, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature });
|
|
4411
4503
|
config.keyVerified = true;
|
|
4412
4504
|
delete config.challenge; // one-time; consumed
|
|
4413
4505
|
console.log(` ${c.green('✓')} key verified — MetaMynd never saw your private key, and neither did this CLI`);
|
|
@@ -4419,7 +4511,7 @@ async function main() {
|
|
|
4419
4511
|
if (config.challenge) {
|
|
4420
4512
|
console.log(c.dim(' → proving key control (verify-key) …'));
|
|
4421
4513
|
const signature = signChallengeHex(generatedKey.privateKeyHex, config.challenge);
|
|
4422
|
-
await
|
|
4514
|
+
await ownerPost(session, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature });
|
|
4423
4515
|
config.keyVerified = true;
|
|
4424
4516
|
delete config.challenge; // one-time; consumed
|
|
4425
4517
|
console.log(` ${c.green('✓')} key verified — MetaMynd never saw your private key`);
|
|
@@ -4430,6 +4522,7 @@ async function main() {
|
|
|
4430
4522
|
console.log(c.dim(` sign this challenge with your private key (Ed25519 over its UTF-8 bytes, hex):`));
|
|
4431
4523
|
console.log(c.dim(` challenge: ${config.challenge ?? '(none returned)'}`));
|
|
4432
4524
|
console.log(c.dim(` POST ${base}/agent-identity/${config.identityId}/verify-key { "signature": "<hex>" } (owner token)`));
|
|
4525
|
+
console.log(c.dim(` (an owner access token lasts 15 minutes — sign in again with POST ${base}/auth/login for a fresh one)`));
|
|
4433
4526
|
}
|
|
4434
4527
|
|
|
4435
4528
|
// 3c. The gateway's OWN identity — a real did:key, separate from the agent's own (an agent must
|
|
@@ -4442,7 +4535,7 @@ async function main() {
|
|
|
4442
4535
|
const gatewayDid = buildDidKey(rawPublicKeyFromSpkiHex(gwKeypair.publicKeyHex));
|
|
4443
4536
|
gatewayIdentity = { did: gatewayDid, keyHex: gwKeypair.privateKeyHex };
|
|
4444
4537
|
console.log(c.dim(`\n → registering the gateway's identity as a trusted counterparty (${financial ? "so it can claim this agent's holds and report to you" : 'so its reports reach your Activity Log'}) …`));
|
|
4445
|
-
const reg = await registerGatewayCounterparty(
|
|
4538
|
+
const reg = await registerGatewayCounterparty(session, gatewayDid, `${scope}-gateway`, financial ? 'claim' : 'report', gwKeypair.privateKeyHex, financial ? null : [config.agentDid]);
|
|
4446
4539
|
if (reg.ok) {
|
|
4447
4540
|
console.log(` ${c.green('✓')} gateway DID ${c.b(gatewayDid)} — ${reg.message}`);
|
|
4448
4541
|
} else {
|
|
@@ -4477,6 +4570,7 @@ export {
|
|
|
4477
4570
|
generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
|
|
4478
4571
|
scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
|
|
4479
4572
|
ensurePolicyKey,
|
|
4573
|
+
ownerLogin, ownerFetch, ownerPost,
|
|
4480
4574
|
};
|
|
4481
4575
|
if (!process.env.CREATE_METAMYND_AGENT_NO_MAIN) {
|
|
4482
4576
|
// fail() can also be reached from a callback outside main()'s promise chain (readline, child process).
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.43",
|
|
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 generated-code-parses.smoke.mjs && node harness-gateway.smoke.mjs && node harness-gateway-bind.smoke.mjs && node policy-demo.smoke.mjs && node hosted-nonfinancial.smoke.mjs && node modes-nonfinancial.smoke.mjs && node byok-daemon.smoke.mjs"
|
|
10
|
+
"test": "node generated-code-parses.smoke.mjs && node harness-gateway.smoke.mjs && node harness-gateway-bind.smoke.mjs && node policy-demo.smoke.mjs && node hosted-nonfinancial.smoke.mjs && node modes-nonfinancial.smoke.mjs && node byok-daemon.smoke.mjs && node owner-session.smoke.mjs"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"index.mjs",
|