create-metamynd-agent 0.14.2 → 0.14.4
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/README.md +5 -0
- package/index.mjs +63 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -175,6 +175,11 @@ the whole protocol.
|
|
|
175
175
|
Named precisely, not left implicit:
|
|
176
176
|
|
|
177
177
|
- **Direct call.** `bookFlight()` doesn't exist in the agent's process.
|
|
178
|
+
- **Forged or replayed policy (every gateway is pinned, 0.14.3).** The gateway's `policyPublicKey` is baked in from your
|
|
179
|
+
provisioning response (or, from an older API, `GET /magp/policy/pubkey`), and with `@metamynd/agentsafe-mcp-guard` 0.18.1 an
|
|
180
|
+
unsigned, tampered or stale bundle is refused for every action, amount 0 included — so a non-financial gateway cannot be granted
|
|
181
|
+
an action by whoever sits on its bundle fetch. If no key can be had, the CLI scaffolds no gateway at all (it keeps the
|
|
182
|
+
provisioned config and says so) rather than writing an unpinned one.
|
|
178
183
|
- **Payload binding is ON by default (0.11.0, MAGP §8.3.9).** The generated agent signs the COMPLETE body it sends as a payload, the
|
|
179
184
|
authorization it gets from the gate is bound to that same body, and the generated gateway sets `requirePayloadBinding: true`: it
|
|
180
185
|
digests the body it is about to run and the issuer refuses the claim unless that is the digest the agent signed. The eight
|
package/index.mjs
CHANGED
|
@@ -59,7 +59,9 @@ const GUARD_PKG = '@metamynd/agentsafe-guard';
|
|
|
59
59
|
// 0.17.0 signs the agent's context (envelopeSignature, MAGP 8.3.13) by default; the scaffolded agent gets it with no template
|
|
60
60
|
// change (signContext: false opts out). A daemon-custody agent needs agentsafe-signer >= 0.19.0 for its no-amount requests.
|
|
61
61
|
// 0.18.0 exports keepAliveFetch. No template change.
|
|
62
|
-
|
|
62
|
+
// 0.18.1 parks a local-first ESCALATE at the gate. Required: every scaffold's demo says "approve it in the dashboard and
|
|
63
|
+
// the action resumes", and below this the escalation never reached the owner's queue (decided in-process, no escalationId).
|
|
64
|
+
const GUARD_VERSION = '^0.18.1';
|
|
63
65
|
/** The harness entry point's config load, shared by both harness templates: a fresh clone has no
|
|
64
66
|
* agent.metamynd.json (it is gitignored), so say what to do instead of a bare ENOENT (BR-004). */
|
|
65
67
|
function harnessConfigLoad() {
|
|
@@ -120,7 +122,10 @@ const MCP_GUARD_PKG = '@metamynd/agentsafe-mcp-guard';
|
|
|
120
122
|
// requireContextSignature option. No template change (the scaffolded agent does not sign its context by default).
|
|
121
123
|
// 0.18.0: an opt-in bundleCache (push-invalidated by the issuer's events stream) and keepAliveFetch exported. No
|
|
122
124
|
// template change (the scaffolded gateways don't enable the cache).
|
|
123
|
-
|
|
125
|
+
// 0.18.1: the pinned policyPublicKey every scaffolded gateway sets now refuses an unsigned or stale bundle for an
|
|
126
|
+
// amount-0 action too. Required: the non-financial gateway's every request is amount 0, so below this a stripped
|
|
127
|
+
// bundle could grant it an action the agent never had.
|
|
128
|
+
const MCP_GUARD_VERSION = '^0.18.1';
|
|
124
129
|
const GATEWAY_PKG = '@metamynd/agentsafe-http-gateway';
|
|
125
130
|
// 0.2.0 fixes a confused-deputy gap (payload not bound to the signed request) — the CLI must
|
|
126
131
|
// never scaffold a range that could resolve below it.
|
|
@@ -972,6 +977,50 @@ async function registerGatewayCounterparty(base, token, did, label) {
|
|
|
972
977
|
}
|
|
973
978
|
}
|
|
974
979
|
|
|
980
|
+
/**
|
|
981
|
+
* Every scaffolded gateway pins the issuer's policy-signing key, so it refuses any bundle MetaMynd did not sign —
|
|
982
|
+
* an unpinned gateway trusts whatever its bundle fetch returns, and over plain http:// that is whatever an
|
|
983
|
+
* interceptor wrote (mcp-guard only refuses VALUE-bearing actions there, and a non-financial agent has none).
|
|
984
|
+
* The provisioning response carries the key (`issuer.policyKey`); when it does not (an older backend), it is read
|
|
985
|
+
* from GET /magp/policy/pubkey on the same API this CLI just provisioned through, then saved into the config.
|
|
986
|
+
* Returns the key, or null when neither source has one — the caller must then not scaffold a gateway.
|
|
987
|
+
*/
|
|
988
|
+
async function ensurePolicyKey(config, base) {
|
|
989
|
+
const isKey = (k) => typeof k === 'string' && /^[0-9a-f]{64}$/i.test(k);
|
|
990
|
+
if (isKey(config.issuer?.policyKey)) return config.issuer.policyKey;
|
|
991
|
+
let key = null;
|
|
992
|
+
try {
|
|
993
|
+
const res = await fetch(`${String(base).replace(/\/+$/, '')}/magp/policy/pubkey`);
|
|
994
|
+
key = (await res.json().catch(() => null))?.data?.publicKey ?? null;
|
|
995
|
+
} catch {
|
|
996
|
+
key = null;
|
|
997
|
+
}
|
|
998
|
+
if (!isKey(key)) return null;
|
|
999
|
+
config.issuer = { ...(config.issuer ?? {}), policyKey: key.toLowerCase() };
|
|
1000
|
+
return config.issuer.policyKey;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const NO_POLICY_KEY = (base) =>
|
|
1004
|
+
`Could not get MetaMynd's policy-signing key: the provisioning response carried none, and GET ${base}/magp/policy/pubkey ` +
|
|
1005
|
+
'did not return one. A gateway without it would trust whatever policy bundle its network fetch returns, so none was ' +
|
|
1006
|
+
'scaffolded. Retry when the API is reachable, or pass --no-gateway for the single-process example (not an enforcement boundary).';
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Stop after provisioning WITHOUT losing what it returned: a managed agent key is handed back once and cannot be
|
|
1010
|
+
* fetched again. Saves the config into the project directory (never over an existing file), then fails with `why`.
|
|
1011
|
+
*/
|
|
1012
|
+
function keepConfigAndFail(outDir, config, why) {
|
|
1013
|
+
let kept = null;
|
|
1014
|
+
try {
|
|
1015
|
+
mkdirSync(outDir, { recursive: true });
|
|
1016
|
+
kept = join(outDir, existsSync(join(outDir, 'agent.metamynd.json')) ? `agent.metamynd.${Date.now()}.json` : 'agent.metamynd.json');
|
|
1017
|
+
writeFileSync(kept, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
1018
|
+
} catch {
|
|
1019
|
+
kept = null;
|
|
1020
|
+
}
|
|
1021
|
+
fail(`${why}${kept ? `\n The agent was provisioned; its config (with its key) was saved to ${kept.replace(/\\/g, '/')}.` : ''}`);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
975
1024
|
// ---------- scaffolding ----------
|
|
976
1025
|
/**
|
|
977
1026
|
* The --no-gateway / --sandbox variant: the tool is a local function in the SAME process as
|
|
@@ -1561,7 +1610,7 @@ const routes = [{ method: 'POST', path: '/perform', action: '${scope}', valueFie
|
|
|
1561
1610
|
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1562
1611
|
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1563
1612
|
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
1564
|
-
const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false
|
|
1613
|
+
const guard = createMcpGuard({ serviceDid: 'did:local:${scope}-gateway', issuerApi: MAGP_API, requireAuthorization: false, policyPublicKey: '${policyKey}' });
|
|
1565
1614
|
|
|
1566
1615
|
const gateway = createHttpGateway({
|
|
1567
1616
|
guard,
|
|
@@ -1966,7 +2015,7 @@ const identity = JSON.parse(readFileSync(new URL('./service.metamynd.json', impo
|
|
|
1966
2015
|
// forged bundle). Without it, verifyBundle() never runs at all: an attacker who can intercept the
|
|
1967
2016
|
// fetch to \`\${MAGP_API}/policy/bundle/...\` — a MITM, a compromised DNS/proxy — can hand this gateway
|
|
1968
2017
|
// a bundle with a higher cap or no rules, and it would be trusted the same as the real one.
|
|
1969
|
-
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true
|
|
2018
|
+
const guard = createMcpGuard({ serviceDid: identity.serviceDid, serviceKey: identity.serviceKey ?? undefined, issuerApi: MAGP_API, requireAuthorization: true, policyPublicKey: '${policyKey}' });
|
|
1970
2019
|
|
|
1971
2020
|
const gateway = createHttpGateway({
|
|
1972
2021
|
guard,
|
|
@@ -2262,12 +2311,14 @@ function scaffoldProject({ outDir, config, slug, scope, perTxnMax, currency = 'U
|
|
|
2262
2311
|
|
|
2263
2312
|
if (withGateway) {
|
|
2264
2313
|
const apiBase = config.apiBase ?? config.api ?? DEFAULT_API;
|
|
2265
|
-
// The key that signs every policy bundle (magp.policy-signer.ts)
|
|
2266
|
-
//
|
|
2267
|
-
//
|
|
2268
|
-
//
|
|
2269
|
-
// the generated gateway is unchanged from before rather than baking in nothing and crashing.
|
|
2314
|
+
// The key that signs every policy bundle (magp.policy-signer.ts), baked into the generated gateway so it pins the
|
|
2315
|
+
// bundle rather than trusting whatever the fetch returns. REQUIRED: an unpinned gateway takes an interceptor's
|
|
2316
|
+
// bundle as its rules (2026-10-02 — a non-financial gateway executed an action the agent was never granted). The
|
|
2317
|
+
// interactive flows fill it in with ensurePolicyKey() before calling this, so a missing key here is a caller bug.
|
|
2270
2318
|
const policyKey = config.issuer?.policyKey ?? null;
|
|
2319
|
+
if (!policyKey) {
|
|
2320
|
+
throw new Error("scaffoldProject: a gateway needs the issuer's policy-signing key (config.issuer.policyKey) to pin its bundle — call ensurePolicyKey() first, or pass withGateway: false");
|
|
2321
|
+
}
|
|
2271
2322
|
const gwDir = join(outDir, 'gateway');
|
|
2272
2323
|
if (!existsSync(gwDir)) mkdirSync(gwDir, { recursive: true });
|
|
2273
2324
|
if (!neutral) {
|
|
@@ -3784,6 +3835,7 @@ async function runClaim(args) {
|
|
|
3784
3835
|
if (args.financial && issuedNonFinancial) {
|
|
3785
3836
|
keepConfigAndStop('--financial was passed, but the approved agent has NO spending authority (the request was non-financial).');
|
|
3786
3837
|
}
|
|
3838
|
+
if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndStop(NO_POLICY_KEY(base));
|
|
3787
3839
|
const slug = slugify(state.name || 'metamynd-agent');
|
|
3788
3840
|
const outDir = resolve(String(args.out || `./${slug}`));
|
|
3789
3841
|
if (issuedNonFinancial) {
|
|
@@ -4019,6 +4071,7 @@ async function main() {
|
|
|
4019
4071
|
console.log(` ${c.green('✓')} derived ${demo.cases.length} demo case(s) from your rules`);
|
|
4020
4072
|
for (const n of demo.notDemonstrated) console.log(` ${c.yellow('!')} not staged in the demo: ${n.rule} ${c.dim('— ' + n.why + '; still enforced')}`);
|
|
4021
4073
|
}
|
|
4074
|
+
if (!args['no-gateway'] && !(await ensurePolicyKey(config, config.apiBase ?? base))) keepConfigAndFail(outDir, config, NO_POLICY_KEY(base));
|
|
4022
4075
|
scaffoldProject({ demo, outDir, config, slug, scope, perTxnMax, currency, merchant: financial ? merchants[0] || 'demo-merchant' : merchants[0], sandbox: false, withGateway: !args['no-gateway'], gatewayPort: Number(args['gateway-port']) || DEFAULT_GATEWAY_PORT, force: !!args.force, gatewayIdentity });
|
|
4023
4076
|
}
|
|
4024
4077
|
|
|
@@ -4028,6 +4081,7 @@ export {
|
|
|
4028
4081
|
buildPolicyCases, resolveFinancial, policyMolecules, ruleToMolecule,
|
|
4029
4082
|
generateAgentKeypair, harnessAgentDid, harnessMandate, harnessRulesFile, harnessDefaultSopNeutral,
|
|
4030
4083
|
scaffoldProject, defaultNeutralDemo, exampleIndexNeutral, exampleReadmeNeutral, gatewayServerFileNeutral, gatewayReadmeNeutral,
|
|
4084
|
+
ensurePolicyKey,
|
|
4031
4085
|
};
|
|
4032
4086
|
if (!process.env.CREATE_METAMYND_AGENT_NO_MAIN) {
|
|
4033
4087
|
// fail() can also be reached from a callback outside main()'s promise chain (readline, child process).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-metamynd-agent",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.4",
|
|
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": {
|