create-metamynd-agent 0.14.31 → 0.14.40
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 +162 -64
- package/package.json +2 -2
package/index.mjs
CHANGED
|
@@ -82,7 +82,20 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
|
|
|
82
82
|
// it with a full-amount capture. Required: every scaffolded agent calls its gateway, which claims and settles.
|
|
83
83
|
// 0.28.0: .resume() takes the approval's one resume at the issuer, so two processes resuming it cannot both run the tool.
|
|
84
84
|
// Required: npm run resume relies on it (its file rename only covers one machine's working directory).
|
|
85
|
-
|
|
85
|
+
// 0.29.0: .resume() runs only the CONTEXT the owner approved too (the escalation's contextDigest), so an approval of one
|
|
86
|
+
// operation cannot be resumed as another. Required: with an issuer that binds it, the gateway claim needs the same context.
|
|
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
|
+
// suspension or reinstatement reaches a running agent within seconds, not within the bundle's 10-minute maxStaleness.
|
|
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. 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
|
+
const GUARD_VERSION = '^0.34.0';
|
|
86
99
|
/** The harness entry point's config load, shared by both harness templates: a fresh clone has no
|
|
87
100
|
* agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
|
|
88
101
|
function harnessConfigLoad() {
|
|
@@ -163,7 +176,14 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
|
163
176
|
// 0.25.0: honourApprovals — a Service without requireAuthorization (the value-less gateway) still runs an owner-approved
|
|
164
177
|
// escalation, exactly once (MAGP §8.7.18). Required: the non-financial gateway sets it and asserts guard.honoursApprovals.
|
|
165
178
|
// 0.26.0: a gateway judges the same derived risk floor as the gate (owner tier + amount share of the cap).
|
|
166
|
-
|
|
179
|
+
// 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
|
|
180
|
+
// another (or none) is released and refused. Required: the issuer refuses AUTHORIZATION_CONTEXT_REQUIRED to a claimer that
|
|
181
|
+
// states none, so an older gateway could no longer run an approved escalation.
|
|
182
|
+
// 0.27.1: reportOutcome() reports an authorization a request only NAMED as presented, never as the one it ran under (FW6-3).
|
|
183
|
+
// 0.28.0: a gateway judges owner-keyed operation rules (context-value-in / -not-in) from its bundle like the gate
|
|
184
|
+
// (NF-RISK-SELF). Required: a gateway on an older guard would read an operation rule's atom as unknown and let it run.
|
|
185
|
+
// 0.29.0: a claim the issuer refuses is logged in one line and its block carries the issuer's detail (FW6-1).
|
|
186
|
+
const MCP_GUARD_VERSION = '^0.29.0';
|
|
167
187
|
/** A DID as it may appear inside a generated string literal (the gateway's allowedAgents pin): no quote, backslash or space. */
|
|
168
188
|
const SAFE_DID = /^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/;
|
|
169
189
|
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
@@ -196,7 +216,11 @@ const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
|
196
216
|
// 0.23.0: refusals aggregated with their count, and `reportSpool` (both hosted templates set it); depends on agentsafe-mcp-guard ^0.24.0.
|
|
197
217
|
// 0.24.0: depends on agentsafe-mcp-guard ^0.25.0 (one guard in the tree with MCP_GUARD_VERSION above). No template change.
|
|
198
218
|
// 0.25.0: depends on agentsafe-mcp-guard ^0.26.0 (one guard in the tree). No template change.
|
|
199
|
-
|
|
219
|
+
// 0.26.0: depends on agentsafe-mcp-guard ^0.27.0 (the approved-context binding). Required with MCP_GUARD_VERSION above.
|
|
220
|
+
// 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.
|
|
221
|
+
// 0.27.0: depends on agentsafe-mcp-guard ^0.28.0 (operation rules). Required with MCP_GUARD_VERSION above.
|
|
222
|
+
// 0.28.0: a 403 for a refused claim carries the issuer's detail (FW6-1); depends on agentsafe-mcp-guard ^0.29.0.
|
|
223
|
+
const GATEWAY_VERSION = '^0.28.0';
|
|
200
224
|
|
|
201
225
|
/** Appended to every scaffolded gateway server: give hold settlements still running a bounded moment on shutdown. */
|
|
202
226
|
const DRAIN_ON_SHUTDOWN = `
|
|
@@ -931,7 +955,9 @@ function daemonRequest(socketPath, op, params, { connectTimeoutMs = 5000 } = {})
|
|
|
931
955
|
sock.once('error', (err) => {
|
|
932
956
|
cleanup();
|
|
933
957
|
if (settled) return;
|
|
934
|
-
|
|
958
|
+
// ECONNREFUSED too: the one-shot admin socket is called right as `start --admin` builds it,
|
|
959
|
+
// and a Unix socket refuses between bind() and listen(). A refused connect sent nothing.
|
|
960
|
+
if ((err.code === 'ENOENT' || err.code === 'ECONNREFUSED') && Date.now() < deadline) { setTimeout(attempt, 20); return; }
|
|
935
961
|
settled = true;
|
|
936
962
|
clearTimeout(overallTimer);
|
|
937
963
|
reject(Object.assign(new Error(`agentsafe-signer daemon unreachable at ${socketPath}: ${err.message}`), { code: 'DAEMON_UNREACHABLE' }));
|
|
@@ -971,6 +997,10 @@ async function apiPost(base, path, body, token) {
|
|
|
971
997
|
} catch (e) {
|
|
972
998
|
fail(`Cannot reach ${base}${path} — is the API up? (${e.message})`);
|
|
973
999
|
}
|
|
1000
|
+
return readApiJson(res, path);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
async function readApiJson(res, path) {
|
|
974
1004
|
const text = await res.text();
|
|
975
1005
|
let json = null;
|
|
976
1006
|
try { json = text ? JSON.parse(text) : null; } catch { /* non-JSON */ }
|
|
@@ -981,6 +1011,70 @@ async function apiPost(base, path, body, token) {
|
|
|
981
1011
|
return json;
|
|
982
1012
|
}
|
|
983
1013
|
|
|
1014
|
+
// ---------- owner session ----------
|
|
1015
|
+
// An owner access token lives 15 minutes (pre-beta rerun 6, OBS session TTL, #926); the refresh token from the same
|
|
1016
|
+
// sign-in renews it (POST /auth/refresh-token, which rotates both). Every owner-authenticated call this CLI makes goes
|
|
1017
|
+
// through ownerFetch: on a 401 it renews the session ONCE — the refresh token first, else a fresh sign-in with the
|
|
1018
|
+
// credentials already given to this run — and retries the call once. The password stays in this process's memory
|
|
1019
|
+
// for that purpose only; it is never written anywhere (pre-beta rerun 6, CLI login expiry).
|
|
1020
|
+
async function ownerLogin(base, email, password) {
|
|
1021
|
+
const login = await apiPost(base, '/auth/login', { username: email, password }, null);
|
|
1022
|
+
const token = login?.data?.accessToken;
|
|
1023
|
+
if (!token) fail('Login succeeded but no access token was returned.');
|
|
1024
|
+
return { base, email, password, token, refreshToken: login?.data?.refreshToken ?? null };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/** Renew an expired owner session in place; true when a new access token was obtained. Never fatal. */
|
|
1028
|
+
async function renewOwnerSession(session) {
|
|
1029
|
+
const post = (path, body) => fetch(`${session.base}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
1030
|
+
if (session.refreshToken) {
|
|
1031
|
+
try {
|
|
1032
|
+
const res = await post('/auth/refresh-token', { refreshToken: session.refreshToken });
|
|
1033
|
+
const json = res.ok ? await res.json().catch(() => null) : null;
|
|
1034
|
+
// The refresh endpoint answers at the top level ({ accessToken, refreshToken }); accept a `data` envelope too.
|
|
1035
|
+
const token = json?.accessToken ?? json?.data?.accessToken;
|
|
1036
|
+
if (token) {
|
|
1037
|
+
session.token = token;
|
|
1038
|
+
session.refreshToken = json?.refreshToken ?? json?.data?.refreshToken ?? session.refreshToken;
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
} catch { /* fall through to a fresh sign-in */ }
|
|
1042
|
+
}
|
|
1043
|
+
if (typeof session.password === 'string') {
|
|
1044
|
+
try {
|
|
1045
|
+
const res = await post('/auth/login', { username: session.email, password: session.password });
|
|
1046
|
+
const json = res.ok ? await res.json().catch(() => null) : null;
|
|
1047
|
+
if (json?.data?.accessToken) {
|
|
1048
|
+
session.token = json.data.accessToken;
|
|
1049
|
+
session.refreshToken = json.data.refreshToken ?? null;
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
} catch { /* reported by the caller's retry */ }
|
|
1053
|
+
}
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/** fetch() with the owner's bearer token; a 401 renews the session once and retries once. */
|
|
1058
|
+
async function ownerFetch(session, path, init = {}) {
|
|
1059
|
+
const send = () => fetch(`${session.base}${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...(init.headers ?? {}), Authorization: `Bearer ${session.token}` } });
|
|
1060
|
+
const res = await send();
|
|
1061
|
+
if (res.status !== 401) return res;
|
|
1062
|
+
if (!(await renewOwnerSession(session))) return res;
|
|
1063
|
+
console.log(c.dim(' → your owner session had expired; renewed it and retried'));
|
|
1064
|
+
return send();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/** apiPost for an owner-authenticated call: fatal on failure, renewed once on an expired token. */
|
|
1068
|
+
async function ownerPost(session, path, body) {
|
|
1069
|
+
let res;
|
|
1070
|
+
try {
|
|
1071
|
+
res = await ownerFetch(session, path, { method: 'POST', body: JSON.stringify(body) });
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
fail(`Cannot reach ${session.base}${path} — is the API up? (${e.message})`);
|
|
1074
|
+
}
|
|
1075
|
+
return readApiJson(res, path);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
984
1078
|
/** GET a public, read-only endpoint; null on ANY failure (unreachable, non-2xx, non-JSON) — never fatal. */
|
|
985
1079
|
async function softGet(base, path) {
|
|
986
1080
|
try {
|
|
@@ -1049,24 +1143,22 @@ async function resolveHostedRulePack(base, key, { financial, perTxnMax, maxAmoun
|
|
|
1049
1143
|
* dashboard registration shows its own dialog before setting the flag); see the printed message
|
|
1050
1144
|
* either way.
|
|
1051
1145
|
*/
|
|
1052
|
-
async function registerGatewayCounterparty(
|
|
1146
|
+
async function registerGatewayCounterparty(session, did, label, purpose = 'claim', privateKeyHex = null, agents = null) {
|
|
1053
1147
|
try {
|
|
1054
1148
|
// The gateway accepts its own registration (MAGP §8.7.6): ask for a challenge naming this owner, sign it with the gateway's
|
|
1055
1149
|
// key (generated here, so this process holds it), and register with that proof. A DID is never registered on its text.
|
|
1056
1150
|
let proof;
|
|
1057
1151
|
if (privateKeyHex) {
|
|
1058
|
-
const ch = await
|
|
1152
|
+
const ch = await ownerFetch(session, '/policy/counterparties/challenge', {
|
|
1059
1153
|
method: 'POST',
|
|
1060
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
1061
1154
|
body: JSON.stringify({ did, purpose }),
|
|
1062
1155
|
});
|
|
1063
1156
|
const chJson = await ch.json().catch(() => null);
|
|
1064
1157
|
if (!ch.ok || !chJson?.data?.message) return { ok: false, message: chJson?.message ?? `challenge HTTP ${ch.status}` };
|
|
1065
1158
|
proof = { challengeToken: chJson.data.challengeToken, signature: signChallengeHex(privateKeyHex, chJson.data.message) };
|
|
1066
1159
|
}
|
|
1067
|
-
const res = await
|
|
1160
|
+
const res = await ownerFetch(session, '/policy/counterparties', {
|
|
1068
1161
|
method: 'POST',
|
|
1069
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
1070
1162
|
// `report` (a non-financial gateway, which never claims): it may only report to this owner, and it changes nothing about
|
|
1071
1163
|
// who may claim their holds, so it needs no enforcement confirmation (MAGP §8.7.6, §16.4).
|
|
1072
1164
|
// `agents` (report only): the agents this gateway serves — the issuer lets it claim an approved value-less hold of THOSE
|
|
@@ -1442,31 +1534,24 @@ const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${gatewayPort}';
|
|
|
1442
1534
|
// --- value-bearing action (sealValueActions, on by default) — its authorizationId is what lets
|
|
1443
1535
|
// --- the gateway atomically claim single-use execution, closing replay + cumulative spend, not
|
|
1444
1536
|
// --- just re-checking policy. See ./gateway/README.md.
|
|
1537
|
+
// --- decision.governanceHeaders() is that verdict as headers: the SAME request the gate checked (the
|
|
1538
|
+
// --- gatedBookFlight mapping below, payload included), freshly signed, carrying its authorizationId.
|
|
1539
|
+
// --- So this file never rebuilds or re-signs the request, and an approved escalation resumed with
|
|
1540
|
+
// --- gatedBookFlight.resume() reaches the gateway with the approval's authorization the same way.
|
|
1445
1541
|
async function bookFlightViaGateway(args, decision) {
|
|
1446
|
-
// The COMPLETE body the tool receives
|
|
1447
|
-
//
|
|
1448
|
-
// refuses to run the tool on a body that is not exactly this one.
|
|
1542
|
+
// The COMPLETE body the tool receives — the payload gatedBookFlight signs (MAGP 8.3.9). The gateway refuses to run the
|
|
1543
|
+
// 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.
|
|
1449
1544
|
const payload = { amount: args.amount, merchant: args.merchant, currency: args.currency ?? '${currency}' };
|
|
1450
|
-
const signed = await guard.buildSignedRequest({
|
|
1451
|
-
action: '${scope}',
|
|
1452
|
-
amount: args.amount,
|
|
1453
|
-
currency: args.currency ?? '${currency}',
|
|
1454
|
-
merchant: args.merchant,
|
|
1455
|
-
// Passed through as-is (never defaulted): a missing riskLevel must reach the gateway as missing.
|
|
1456
|
-
context: { tool: 'book-flight', riskLevel: args.riskLevel },
|
|
1457
|
-
payload,
|
|
1458
|
-
});
|
|
1459
|
-
signed.authorizationId = decision?.authorizationId;
|
|
1460
1545
|
const res = await fetch(GATEWAY + '/book-flight', {
|
|
1461
1546
|
method: 'POST',
|
|
1462
|
-
headers: { 'content-type': 'application/json',
|
|
1547
|
+
headers: { 'content-type': 'application/json', ...(await decision.governanceHeaders()) },
|
|
1463
1548
|
body: JSON.stringify(payload),
|
|
1464
1549
|
});
|
|
1465
1550
|
const body = await res.json().catch(() => null);
|
|
1466
1551
|
if (!res.ok) {
|
|
1467
1552
|
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
1468
1553
|
err.name = 'GovernanceBlocked';
|
|
1469
|
-
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
|
|
1554
|
+
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
|
|
1470
1555
|
throw err;
|
|
1471
1556
|
}
|
|
1472
1557
|
return body;
|
|
@@ -1486,7 +1571,9 @@ const gatedBookFlight = guard.guardTool(
|
|
|
1486
1571
|
// the action for review — never 'low'.
|
|
1487
1572
|
context: { tool: 'book-flight', riskLevel: a.riskLevel },
|
|
1488
1573
|
// The authorization is bound to the SAME body bookFlightViaGateway() sends, so the gate records what this agent
|
|
1489
|
-
// signed and the gateway can prove it is running exactly that.
|
|
1574
|
+
// signed and the gateway can prove it is running exactly that. This mapping is also what governanceHeaders() signs
|
|
1575
|
+
// for the gateway (riskLevel passed through as-is: a missing one reaches the gateway as missing). Keep the two in
|
|
1576
|
+
// step if you add a field.
|
|
1490
1577
|
payload: { amount: a.amount, merchant: a.merchant, currency: a.currency ?? '${currency}' },
|
|
1491
1578
|
}),
|
|
1492
1579
|
);
|
|
@@ -1680,20 +1767,21 @@ ${withGateway ? `const GATEWAY = process.env.GATEWAY_URL || 'http://localhost:${
|
|
|
1680
1767
|
// --- Calls the gateway process instead of a local function. There is no raw performAction() in
|
|
1681
1768
|
// --- this file to call directly — the tool, and any real credentials it needs, live only in
|
|
1682
1769
|
// --- ./gateway, which independently re-verifies this signed request itself.
|
|
1770
|
+
// --- decision.governanceHeaders() is the request gatedAction checked (its jurisdiction SIGNED, a top-level
|
|
1771
|
+
// --- field, never context), freshly signed, with the authorization it was granted (none for a value-less
|
|
1772
|
+
// --- action — see ./gateway/README.md). So this file never rebuilds or re-signs it, and npm run resume
|
|
1773
|
+
// --- reaches the gateway through here the same way.
|
|
1683
1774
|
async function performViaGateway(args, decision) {
|
|
1684
|
-
const { jurisdiction, ...context } = args; // the jurisdiction is SIGNED (a top-level field), never context
|
|
1685
|
-
const signed = await guard.buildSignedRequest({ action: '${scope}', merchant: MERCHANT, jurisdiction, context });
|
|
1686
|
-
signed.authorizationId = decision?.authorizationId; // none for a value-less action — see ./gateway/README.md
|
|
1687
1775
|
const res = await fetch(GATEWAY + '/perform', {
|
|
1688
1776
|
method: 'POST',
|
|
1689
|
-
headers: { 'content-type': 'application/json',
|
|
1777
|
+
headers: { 'content-type': 'application/json', ...(await decision.governanceHeaders()) },
|
|
1690
1778
|
body: '{}', // this tool reads nothing from the body; the request's fields travel in the SIGNED context
|
|
1691
1779
|
});
|
|
1692
1780
|
const body = await res.json().catch(() => null);
|
|
1693
1781
|
if (!res.ok) {
|
|
1694
1782
|
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
1695
1783
|
err.name = 'GovernanceBlocked';
|
|
1696
|
-
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
|
|
1784
|
+
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
|
|
1697
1785
|
throw err;
|
|
1698
1786
|
}
|
|
1699
1787
|
return body;
|
|
@@ -2518,7 +2606,11 @@ re-register this gateway yourself — after removing it from the registry, say.
|
|
|
2518
2606
|
Registration needs **proof of control** (MAGP §8.7.6): you ask for a challenge naming you, this
|
|
2519
2607
|
gateway signs it with its own key from \`service.metamynd.json\`, and you register with that
|
|
2520
2608
|
signature. A DID is never registered on its text alone, so nobody can register a gateway they do
|
|
2521
|
-
not hold the key for. Run this from this directory (bash; \`TOKEN\` is your owner access token)
|
|
2609
|
+
not hold the key for. Run this from this directory (bash; \`TOKEN\` is your owner access token).
|
|
2610
|
+
An owner access token lasts **15 minutes**, so get a fresh one just before you run this:
|
|
2611
|
+
\`POST /auth/login\` with \`{ "username": "<your email>", "password": "<your password>" }\` returns it
|
|
2612
|
+
as \`data.accessToken\` (that sign-in also signs your account out of the dashboard — one session at a
|
|
2613
|
+
time). A \`401\` from the snippet means the token expired: sign in again and re-run it.
|
|
2522
2614
|
|
|
2523
2615
|
\`\`\`bash
|
|
2524
2616
|
MAGP_API=https://metamynd.ai/api/v1 TOKEN=<owner access token> node --input-type=module -e "
|
|
@@ -3452,7 +3544,7 @@ async function callGateway(path, action, args) {
|
|
|
3452
3544
|
if (!res.ok) {
|
|
3453
3545
|
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
3454
3546
|
err.name = 'GovernanceBlocked';
|
|
3455
|
-
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
|
|
3547
|
+
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
|
|
3456
3548
|
throw err;
|
|
3457
3549
|
}
|
|
3458
3550
|
return body;
|
|
@@ -3664,7 +3756,7 @@ async function callGateway(path, action, args) {
|
|
|
3664
3756
|
if (!res.ok) {
|
|
3665
3757
|
const err = new Error('gateway ' + res.status + ': ' + (body?.reasonCode ?? 'refused'));
|
|
3666
3758
|
err.name = 'GovernanceBlocked';
|
|
3667
|
-
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR' };
|
|
3759
|
+
err.governance = { decision: body?.decision ?? 'block', reasonCode: body?.reasonCode ?? 'GATEWAY_ERROR', ...(typeof body?.detail === 'string' ? { detail: body.detail } : {}) };
|
|
3668
3760
|
throw err;
|
|
3669
3761
|
}
|
|
3670
3762
|
return body;
|
|
@@ -4060,11 +4152,9 @@ async function authFlow(args) {
|
|
|
4060
4152
|
rl?.resume();
|
|
4061
4153
|
}
|
|
4062
4154
|
rl?.close();
|
|
4063
|
-
const
|
|
4064
|
-
const token = login?.data?.accessToken;
|
|
4065
|
-
if (!token) fail('Login succeeded but no access token was returned.');
|
|
4155
|
+
const session = await ownerLogin(base, email, password);
|
|
4066
4156
|
console.log(c.dim(` ${SESSION_REPLACED_NOTE}`));
|
|
4067
|
-
return { base,
|
|
4157
|
+
return { base, session, email };
|
|
4068
4158
|
}
|
|
4069
4159
|
|
|
4070
4160
|
// One session per account: a CLI login replaces the account's dashboard session (the dashboard then answers
|
|
@@ -4080,7 +4170,7 @@ const REQUEST_STATE_FILE = 'metamynd-request.json';
|
|
|
4080
4170
|
async function runRequest(args) {
|
|
4081
4171
|
const owner = (typeof args.owner === 'string' ? args.owner : undefined) ?? process.env.METAMYND_OWNER;
|
|
4082
4172
|
if (!owner) fail('--owner <ownerEmail> is required for a delegated request.');
|
|
4083
|
-
const { base,
|
|
4173
|
+
const { base, session } = await authFlow(args);
|
|
4084
4174
|
|
|
4085
4175
|
// The same decision --harness and the hosted flow make (see announceFinancial): a non-financial request asks the
|
|
4086
4176
|
// OWNER to approve no spending authority at all, and says so explicitly - an omitted amount means "use the
|
|
@@ -4108,7 +4198,7 @@ async function runRequest(args) {
|
|
|
4108
4198
|
}
|
|
4109
4199
|
|
|
4110
4200
|
console.log(c.dim(` → requesting "${name}" for ${owner} …`));
|
|
4111
|
-
const res = await
|
|
4201
|
+
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 } : {}) });
|
|
4112
4202
|
const d = res.data;
|
|
4113
4203
|
// A server that predates non-financial requests ignores the field and files the request WITH default spend
|
|
4114
4204
|
// limits. The developer cannot withdraw it, so say exactly what now exists and who can deny it; do not save a
|
|
@@ -4258,7 +4348,9 @@ async function main() {
|
|
|
4258
4348
|
return ask(rl, prompt, def);
|
|
4259
4349
|
};
|
|
4260
4350
|
|
|
4261
|
-
// 1. Connection +
|
|
4351
|
+
// 1. Connection + credentials. The sign-in itself waits until every prompt is answered (step 3): an owner access
|
|
4352
|
+
// token lives 15 minutes, and an owner who took longer over the prompts used to reach provisioning with an expired
|
|
4353
|
+
// one (pre-beta rerun 6, CLI login expiry follow-up to #926).
|
|
4262
4354
|
const apiRaw = await pick('api', 'METAMYND_API', 'API base URL', DEFAULT_API);
|
|
4263
4355
|
const base = String(apiRaw).replace(/\/+$/, '');
|
|
4264
4356
|
const email = await pick('email', 'METAMYND_EMAIL', 'Owner email', '');
|
|
@@ -4272,13 +4364,6 @@ async function main() {
|
|
|
4272
4364
|
rl?.resume();
|
|
4273
4365
|
}
|
|
4274
4366
|
|
|
4275
|
-
console.log(c.dim(`\n → logging in to ${base} …`));
|
|
4276
|
-
const login = await apiPost(base, '/auth/login', { username: email, password }, null);
|
|
4277
|
-
const token = login?.data?.accessToken;
|
|
4278
|
-
if (!token) { rl?.close(); fail('Login succeeded but no access token was returned.'); }
|
|
4279
|
-
console.log(` ${c.green('✓')} authenticated as ${email}`);
|
|
4280
|
-
console.log(c.dim(` ${SESSION_REPLACED_NOTE}`));
|
|
4281
|
-
|
|
4282
4367
|
// 2. Agent details — a --config file's fields are the default at every prompt/flag below.
|
|
4283
4368
|
const name = await pick('name', null, 'Agent name', fileConfig?.name ?? 'Support Bot');
|
|
4284
4369
|
const scope = await pick('scope', null, 'Mandate scope (governed action)', fileConfig?.scope ?? (financial ? 'flight-purchase' : 'perform-action'));
|
|
@@ -4323,19 +4408,6 @@ async function main() {
|
|
|
4323
4408
|
rl?.close();
|
|
4324
4409
|
fail('--daemon-socket generates its own key — pass --byok alone, not --public-key.');
|
|
4325
4410
|
}
|
|
4326
|
-
let generatedKey = null;
|
|
4327
|
-
let daemonPublicKeyHex = null;
|
|
4328
|
-
if (args.byok && !publicKey && daemonSocket) {
|
|
4329
|
-
console.log(c.dim(' → asking the agentsafe-signer daemon to generate a key …'));
|
|
4330
|
-
const { publicKeyHex } = await daemonRequest(daemonAdminSocket, 'generate-key', { allowRekey: false });
|
|
4331
|
-
daemonPublicKeyHex = publicKeyHex;
|
|
4332
|
-
publicKey = publicKeyHex;
|
|
4333
|
-
console.log(` ${c.green('✓')} generated an Ed25519 keypair via the signer daemon ${c.dim('(the private key never left it)')}`);
|
|
4334
|
-
} else if (args.byok && !publicKey) {
|
|
4335
|
-
generatedKey = generateAgentKeypair();
|
|
4336
|
-
publicKey = generatedKey.publicKeyHex;
|
|
4337
|
-
console.log(` ${c.green('✓')} generated an Ed25519 keypair locally ${c.dim('(private key stays on this machine)')}`);
|
|
4338
|
-
}
|
|
4339
4411
|
|
|
4340
4412
|
const slug = slugify(name);
|
|
4341
4413
|
const outDir = resolve(String(args.out || (interactive ? await ask(rl, 'Output directory', `./${slug}`) : `./${slug}`)));
|
|
@@ -4358,6 +4430,30 @@ async function main() {
|
|
|
4358
4430
|
if (pack.reviewedMoleculeIds) sopFields.reviewedMoleculeIds = pack.reviewedMoleculeIds;
|
|
4359
4431
|
}
|
|
4360
4432
|
}
|
|
4433
|
+
|
|
4434
|
+
// Sign in only now, with every answer in hand (see step 1). Should the token still expire before the last
|
|
4435
|
+
// owner call, ownerFetch renews it once and retries.
|
|
4436
|
+
console.log(c.dim(`\n → logging in to ${base} …`));
|
|
4437
|
+
const session = await ownerLogin(base, email, password);
|
|
4438
|
+
console.log(` ${c.green('✓')} authenticated as ${email}`);
|
|
4439
|
+
console.log(c.dim(` ${SESSION_REPLACED_NOTE}`));
|
|
4440
|
+
|
|
4441
|
+
// The key is generated after the sign-in succeeds, as it always was: a daemon refuses to re-key (allowRekey: false),
|
|
4442
|
+
// so a key minted for a run that then failed to sign in would block the next attempt.
|
|
4443
|
+
let generatedKey = null;
|
|
4444
|
+
let daemonPublicKeyHex = null;
|
|
4445
|
+
if (args.byok && !publicKey && daemonSocket) {
|
|
4446
|
+
console.log(c.dim(' → asking the agentsafe-signer daemon to generate a key …'));
|
|
4447
|
+
const { publicKeyHex } = await daemonRequest(daemonAdminSocket, 'generate-key', { allowRekey: false });
|
|
4448
|
+
daemonPublicKeyHex = publicKeyHex;
|
|
4449
|
+
publicKey = publicKeyHex;
|
|
4450
|
+
console.log(` ${c.green('✓')} generated an Ed25519 keypair via the signer daemon ${c.dim('(the private key never left it)')}`);
|
|
4451
|
+
} else if (args.byok && !publicKey) {
|
|
4452
|
+
generatedKey = generateAgentKeypair();
|
|
4453
|
+
publicKey = generatedKey.publicKeyHex;
|
|
4454
|
+
console.log(` ${c.green('✓')} generated an Ed25519 keypair locally ${c.dim('(private key stays on this machine)')}`);
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4361
4457
|
console.log(c.dim(`\n → provisioning "${name}" (identity + mandate + SOP + Standards) …`));
|
|
4362
4458
|
const body = {
|
|
4363
4459
|
name, scope,
|
|
@@ -4376,7 +4472,7 @@ async function main() {
|
|
|
4376
4472
|
...(publicKey ? { publicKey } : {}),
|
|
4377
4473
|
...sopFields,
|
|
4378
4474
|
};
|
|
4379
|
-
const provisioned = await
|
|
4475
|
+
const provisioned = await ownerPost(session, '/onboarding/agent', body);
|
|
4380
4476
|
const config = provisioned?.data;
|
|
4381
4477
|
if (!config?.agentDid) fail('Provisioning did not return a config with an agentDid.');
|
|
4382
4478
|
console.log(` ${c.green('✓')} agent DID ${c.b(config.agentDid)}`);
|
|
@@ -4397,7 +4493,7 @@ async function main() {
|
|
|
4397
4493
|
if (config.challenge) {
|
|
4398
4494
|
console.log(c.dim(' → proving key control via the daemon (verify-key) …'));
|
|
4399
4495
|
const { signature } = await daemonRequest(daemonSocket, 'sign-key-control-challenge', { challenge: config.challenge });
|
|
4400
|
-
await
|
|
4496
|
+
await ownerPost(session, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature });
|
|
4401
4497
|
config.keyVerified = true;
|
|
4402
4498
|
delete config.challenge; // one-time; consumed
|
|
4403
4499
|
console.log(` ${c.green('✓')} key verified — MetaMynd never saw your private key, and neither did this CLI`);
|
|
@@ -4409,7 +4505,7 @@ async function main() {
|
|
|
4409
4505
|
if (config.challenge) {
|
|
4410
4506
|
console.log(c.dim(' → proving key control (verify-key) …'));
|
|
4411
4507
|
const signature = signChallengeHex(generatedKey.privateKeyHex, config.challenge);
|
|
4412
|
-
await
|
|
4508
|
+
await ownerPost(session, `/agent-identity/${encodeURIComponent(config.identityId)}/verify-key`, { signature });
|
|
4413
4509
|
config.keyVerified = true;
|
|
4414
4510
|
delete config.challenge; // one-time; consumed
|
|
4415
4511
|
console.log(` ${c.green('✓')} key verified — MetaMynd never saw your private key`);
|
|
@@ -4420,6 +4516,7 @@ async function main() {
|
|
|
4420
4516
|
console.log(c.dim(` sign this challenge with your private key (Ed25519 over its UTF-8 bytes, hex):`));
|
|
4421
4517
|
console.log(c.dim(` challenge: ${config.challenge ?? '(none returned)'}`));
|
|
4422
4518
|
console.log(c.dim(` POST ${base}/agent-identity/${config.identityId}/verify-key { "signature": "<hex>" } (owner token)`));
|
|
4519
|
+
console.log(c.dim(` (an owner access token lasts 15 minutes — sign in again with POST ${base}/auth/login for a fresh one)`));
|
|
4423
4520
|
}
|
|
4424
4521
|
|
|
4425
4522
|
// 3c. The gateway's OWN identity — a real did:key, separate from the agent's own (an agent must
|
|
@@ -4432,7 +4529,7 @@ async function main() {
|
|
|
4432
4529
|
const gatewayDid = buildDidKey(rawPublicKeyFromSpkiHex(gwKeypair.publicKeyHex));
|
|
4433
4530
|
gatewayIdentity = { did: gatewayDid, keyHex: gwKeypair.privateKeyHex };
|
|
4434
4531
|
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'}) …`));
|
|
4435
|
-
const reg = await registerGatewayCounterparty(
|
|
4532
|
+
const reg = await registerGatewayCounterparty(session, gatewayDid, `${scope}-gateway`, financial ? 'claim' : 'report', gwKeypair.privateKeyHex, financial ? null : [config.agentDid]);
|
|
4436
4533
|
if (reg.ok) {
|
|
4437
4534
|
console.log(` ${c.green('✓')} gateway DID ${c.b(gatewayDid)} — ${reg.message}`);
|
|
4438
4535
|
} else {
|
|
@@ -4467,6 +4564,7 @@ export {
|
|
|
4467
4564
|
generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
|
|
4468
4565
|
scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
|
|
4469
4566
|
ensurePolicyKey,
|
|
4567
|
+
ownerLogin, ownerFetch, ownerPost,
|
|
4470
4568
|
};
|
|
4471
4569
|
if (!process.env.CREATE_METAMYND_AGENT_NO_MAIN) {
|
|
4472
4570
|
// 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.40",
|
|
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",
|