@metamynd/agentsafe-guard 0.2.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/LICENSE +21 -21
- package/README.md +303 -279
- package/agentsafe-guard.mjs +606 -480
- package/example-openclaw-agent.mjs +47 -47
- package/magp-did.mjs +157 -157
- package/package.json +59 -59
- package/policy-core.mjs +83 -4
- package/x402.mjs +43 -43
package/policy-core.mjs
CHANGED
|
@@ -31,6 +31,25 @@ ${c.output ?? ""}`.toLowerCase();
|
|
|
31
31
|
"tool-not-allowed": (c, cfg) => notInAllowList(c.tool, cfg?.allowed),
|
|
32
32
|
"pii-present": (c) => c.piiPresent === true,
|
|
33
33
|
"rate-limit-exceeded": (c, cfg) => typeof c.callCount === "number" && c.callCount > Number(cfg?.max ?? 0),
|
|
34
|
+
// --- Evidence-quality atoms (SAFR §24). Unlike the allow-list atoms, these fire on ABSENCE:
|
|
35
|
+
// a REQUIRE semantic — "the action must be backed by this evidence; if it isn't, fire"
|
|
36
|
+
// (author with escalate/block). Opt-in: they only run when a rule keys them. ---
|
|
37
|
+
// Fires when any REQUIRED evidence type is not among the attested `evidenceTypes` (missing
|
|
38
|
+
// evidence — including none supplied at all → all required missing → fires).
|
|
39
|
+
"evidence-requirement": (c, cfg) => {
|
|
40
|
+
const required = (cfg?.required ?? []).map((t) => String(t).toLowerCase().trim()).filter(Boolean);
|
|
41
|
+
if (required.length === 0) return false;
|
|
42
|
+
const have = new Set((Array.isArray(c.evidenceTypes) ? c.evidenceTypes : []).map((t) => String(t).toLowerCase().trim()));
|
|
43
|
+
return required.some((r) => !have.has(r));
|
|
44
|
+
},
|
|
45
|
+
// Fires when a required minimum confidence (min > 0) is not met — the attested confidence is
|
|
46
|
+
// below it, or absent (a required confidence that was never supplied fails the bar). A min of
|
|
47
|
+
// 0 / unset is no requirement and never fires.
|
|
48
|
+
"evidence-confidence-below": (c, cfg) => {
|
|
49
|
+
const min = Number(cfg?.min ?? 0);
|
|
50
|
+
if (!(min > 0)) return false;
|
|
51
|
+
return typeof c.evidenceConfidence !== "number" || c.evidenceConfidence < min;
|
|
52
|
+
},
|
|
34
53
|
// Trust guidance (MetaMynd Trust Index / HCS-28). Fires when the counterparty's trust score is
|
|
35
54
|
// below a soft REVIEW line — intended to author an ESCALATE (route to a human), NOT a hard block.
|
|
36
55
|
// The score is server-derived (signed-last) so the agent's itinerary can't fake it; when no score
|
|
@@ -145,6 +164,20 @@ var ATOM_SPECS = [
|
|
|
145
164
|
description: "Routes to human review when the counterparty's MetaMynd Trust Index (HCS-28) score is below a soft review line. Guidance, not a hard block \u2014 author it with an ESCALATE decision. The score is resolved server-side; no counterparty score \u2192 the atom does not fire.",
|
|
146
165
|
config: [{ key: "reviewBelow", type: "number", required: true, description: "Trust score (0\u2013100) below which a human is asked to decide" }],
|
|
147
166
|
requiredContext: ["holTrustScore"]
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
predicate: "evidence-requirement",
|
|
170
|
+
label: "Required evidence missing",
|
|
171
|
+
description: "Fires when the action is not backed by every REQUIRED evidence type the agent attests to in `evidenceTypes` (missing evidence \u2014 including none supplied). A REQUIRE control (SAFR \xA724): author it with ESCALATE or BLOCK so an under-evidenced action is stopped or reviewed.",
|
|
172
|
+
config: [{ key: "required", type: "string[]", required: true, description: "Evidence types that must all be present (e.g. kyc, source-doc, signature)" }],
|
|
173
|
+
requiredContext: ["evidenceTypes"]
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
predicate: "evidence-confidence-below",
|
|
177
|
+
label: "Evidence confidence below minimum",
|
|
178
|
+
description: "Fires when the attested evidence confidence is below a required minimum \u2014 or absent (SAFR \xA724). A min of 0 / unset is no requirement. Author with ESCALATE to route low-confidence actions to review.",
|
|
179
|
+
config: [{ key: "min", type: "number", required: true, description: "Minimum evidence confidence (0\u20131) required" }],
|
|
180
|
+
requiredContext: ["evidenceConfidence"]
|
|
148
181
|
}
|
|
149
182
|
];
|
|
150
183
|
var CATALOGUED_ATOMS = ATOM_SPECS.filter((s) => !!ATOM_REGISTRY[s.predicate]);
|
|
@@ -158,7 +191,7 @@ function requiredContextFor(predicates) {
|
|
|
158
191
|
}
|
|
159
192
|
|
|
160
193
|
// src/policy-core/standards-rules.ts
|
|
161
|
-
var PRECEDENCE = { allow: 0,
|
|
194
|
+
var PRECEDENCE = { allow: 0, observe: 1, escalate: 2, block: 3, suspend: 4, quarantine: 5 };
|
|
162
195
|
function atomFires(atom, ctx) {
|
|
163
196
|
const pred = ATOM_REGISTRY[atom.predicate];
|
|
164
197
|
if (!pred) return false;
|
|
@@ -244,8 +277,8 @@ function validateMolecules(molecules) {
|
|
|
244
277
|
if (!["all", "any", "none"].includes(m.combinator)) {
|
|
245
278
|
issues.push({ moleculeId: m.id, message: `invalid combinator '${m.combinator}' (all|any|none)` });
|
|
246
279
|
}
|
|
247
|
-
if (!["block", "escalate"].includes(m.decision)) {
|
|
248
|
-
issues.push({ moleculeId: m.id, message: `invalid decision '${m.decision}' (block|escalate)` });
|
|
280
|
+
if (!["observe", "block", "escalate", "suspend", "quarantine"].includes(m.decision)) {
|
|
281
|
+
issues.push({ moleculeId: m.id, message: `invalid decision '${m.decision}' (observe|block|escalate|suspend|quarantine)` });
|
|
249
282
|
}
|
|
250
283
|
if (!m.reasonCode) issues.push({ moleculeId: m.id, message: "molecule is missing a reasonCode" });
|
|
251
284
|
if (!m.atoms || m.atoms.length === 0) {
|
|
@@ -359,7 +392,7 @@ function sumEventField(events, type, field) {
|
|
|
359
392
|
}
|
|
360
393
|
|
|
361
394
|
// src/policy-core/evaluate.ts
|
|
362
|
-
var PRECEDENCE2 = { allow: 0,
|
|
395
|
+
var PRECEDENCE2 = { allow: 0, observe: 1, escalate: 2, block: 3, suspend: 4, quarantine: 5 };
|
|
363
396
|
function evaluate(input) {
|
|
364
397
|
let decision = "allow";
|
|
365
398
|
let reasonCode = "AUTHORIZED";
|
|
@@ -389,20 +422,66 @@ function buildAuthMessage(f) {
|
|
|
389
422
|
function applySignedLast(unsigned, signed) {
|
|
390
423
|
return { ...unsigned ?? {}, ...signed };
|
|
391
424
|
}
|
|
425
|
+
|
|
426
|
+
// src/policy-core/operating-mode.ts
|
|
427
|
+
var MODE_RANK = {
|
|
428
|
+
read_only: 0,
|
|
429
|
+
restricted: 1,
|
|
430
|
+
supervised: 2,
|
|
431
|
+
autonomous: 3
|
|
432
|
+
};
|
|
433
|
+
var MODES_BY_RANK = ["read_only", "restricted", "supervised", "autonomous"];
|
|
434
|
+
function isOperatingMode(v) {
|
|
435
|
+
return typeof v === "string" && Object.prototype.hasOwnProperty.call(MODE_RANK, v);
|
|
436
|
+
}
|
|
437
|
+
function asOperatingMode(v) {
|
|
438
|
+
return isOperatingMode(v) ? v : "autonomous";
|
|
439
|
+
}
|
|
440
|
+
function moreRestrictive(a, b) {
|
|
441
|
+
return MODE_RANK[a] <= MODE_RANK[b] ? a : b;
|
|
442
|
+
}
|
|
443
|
+
var SUPERVISED_AMOUNT_CAP = 100;
|
|
444
|
+
var RISK_RANK2 = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
445
|
+
function riskAtOrAboveHigh(riskLevel) {
|
|
446
|
+
const r = typeof riskLevel === "string" ? RISK_RANK2[riskLevel.toLowerCase()] : void 0;
|
|
447
|
+
return r !== void 0 && r >= RISK_RANK2.high;
|
|
448
|
+
}
|
|
449
|
+
function operatingModeGate(mode, ctx) {
|
|
450
|
+
const m = asOperatingMode(mode);
|
|
451
|
+
const valueBearing = (ctx.amount ?? 0) > 0;
|
|
452
|
+
if (!valueBearing || m === "autonomous") return { decision: "allow", reasonCode: null };
|
|
453
|
+
switch (m) {
|
|
454
|
+
case "read_only":
|
|
455
|
+
return { decision: "block", reasonCode: "MODE_READ_ONLY" };
|
|
456
|
+
case "restricted":
|
|
457
|
+
return { decision: "escalate", reasonCode: "MODE_RESTRICTED_REVIEW" };
|
|
458
|
+
case "supervised":
|
|
459
|
+
return riskAtOrAboveHigh(ctx.riskLevel) || (ctx.amount ?? 0) >= SUPERVISED_AMOUNT_CAP ? { decision: "escalate", reasonCode: "MODE_SUPERVISED_REVIEW" } : { decision: "allow", reasonCode: null };
|
|
460
|
+
default:
|
|
461
|
+
return { decision: "allow", reasonCode: null };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
392
464
|
export {
|
|
393
465
|
ATOM_REGISTRY,
|
|
394
466
|
ATOM_SPECS,
|
|
395
467
|
CATALOGUED_ATOMS,
|
|
468
|
+
MODES_BY_RANK,
|
|
469
|
+
MODE_RANK,
|
|
470
|
+
SUPERVISED_AMOUNT_CAP,
|
|
396
471
|
applyCapture,
|
|
397
472
|
applyHold,
|
|
398
473
|
applySignedLast,
|
|
474
|
+
asOperatingMode,
|
|
399
475
|
buildAuthMessage,
|
|
400
476
|
canAuthorize,
|
|
401
477
|
evaluate,
|
|
402
478
|
evaluateBoundStandards,
|
|
403
479
|
evaluateMandate,
|
|
404
480
|
evaluateStandardRules,
|
|
481
|
+
isOperatingMode,
|
|
405
482
|
moleculeFires,
|
|
483
|
+
moreRestrictive,
|
|
484
|
+
operatingModeGate,
|
|
406
485
|
releaseHold,
|
|
407
486
|
remainingBudget,
|
|
408
487
|
requiredContextFor,
|
package/x402.mjs
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
// GENERATED from backend/src/features/magp/x402-binding.ts — do not edit. Regenerate: npm run build:guard-core
|
|
2
|
-
|
|
3
|
-
// src/features/magp/x402-binding.ts
|
|
4
|
-
function toMinorUnits(amount, decimals) {
|
|
5
|
-
if (!Number.isFinite(amount) || amount < 0) throw new Error("amount must be a non-negative number");
|
|
6
|
-
const factor = 10 ** decimals;
|
|
7
|
-
return String(Math.round(amount * factor));
|
|
8
|
-
}
|
|
9
|
-
function buildPaymentRequirements(input) {
|
|
10
|
-
if (!input.authorizationId) throw new Error("buildPaymentRequirements requires an authorizationId");
|
|
11
|
-
const decimals = input.decimals ?? 6;
|
|
12
|
-
return {
|
|
13
|
-
x402Version: 1,
|
|
14
|
-
accepts: [
|
|
15
|
-
{
|
|
16
|
-
scheme: "exact",
|
|
17
|
-
network: input.network ?? "hedera-testnet",
|
|
18
|
-
maxAmountRequired: toMinorUnits(input.amount, decimals),
|
|
19
|
-
payTo: input.payTo,
|
|
20
|
-
asset: input.asset,
|
|
21
|
-
resource: input.resource,
|
|
22
|
-
extra: { magpAuthorizationId: input.authorizationId, magpAgentDid: input.agentDid }
|
|
23
|
-
}
|
|
24
|
-
]
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
function checkSettlementBinding(requirements, claim) {
|
|
28
|
-
const accept = requirements?.accepts?.[0];
|
|
29
|
-
if (!accept?.extra?.magpAuthorizationId) return { ok: false, reasonCode: "MISSING_BINDING" };
|
|
30
|
-
if (accept.extra.magpAuthorizationId !== claim.authorizationId) {
|
|
31
|
-
return { ok: false, reasonCode: "AUTHORIZATION_MISMATCH" };
|
|
32
|
-
}
|
|
33
|
-
const authorizedMinor = Number(accept.maxAmountRequired);
|
|
34
|
-
const paidMinor = Number(claim.paidAmountMinor);
|
|
35
|
-
if (!Number.isFinite(paidMinor) || paidMinor < 0) return { ok: false, reasonCode: "AMOUNT_INVALID" };
|
|
36
|
-
if (paidMinor > authorizedMinor) return { ok: false, reasonCode: "AMOUNT_MISMATCH" };
|
|
37
|
-
return { ok: true, reasonCode: "BINDING_OK" };
|
|
38
|
-
}
|
|
39
|
-
export {
|
|
40
|
-
buildPaymentRequirements,
|
|
41
|
-
checkSettlementBinding,
|
|
42
|
-
toMinorUnits
|
|
43
|
-
};
|
|
1
|
+
// GENERATED from backend/src/features/magp/x402-binding.ts — do not edit. Regenerate: npm run build:guard-core
|
|
2
|
+
|
|
3
|
+
// src/features/magp/x402-binding.ts
|
|
4
|
+
function toMinorUnits(amount, decimals) {
|
|
5
|
+
if (!Number.isFinite(amount) || amount < 0) throw new Error("amount must be a non-negative number");
|
|
6
|
+
const factor = 10 ** decimals;
|
|
7
|
+
return String(Math.round(amount * factor));
|
|
8
|
+
}
|
|
9
|
+
function buildPaymentRequirements(input) {
|
|
10
|
+
if (!input.authorizationId) throw new Error("buildPaymentRequirements requires an authorizationId");
|
|
11
|
+
const decimals = input.decimals ?? 6;
|
|
12
|
+
return {
|
|
13
|
+
x402Version: 1,
|
|
14
|
+
accepts: [
|
|
15
|
+
{
|
|
16
|
+
scheme: "exact",
|
|
17
|
+
network: input.network ?? "hedera-testnet",
|
|
18
|
+
maxAmountRequired: toMinorUnits(input.amount, decimals),
|
|
19
|
+
payTo: input.payTo,
|
|
20
|
+
asset: input.asset,
|
|
21
|
+
resource: input.resource,
|
|
22
|
+
extra: { magpAuthorizationId: input.authorizationId, magpAgentDid: input.agentDid }
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function checkSettlementBinding(requirements, claim) {
|
|
28
|
+
const accept = requirements?.accepts?.[0];
|
|
29
|
+
if (!accept?.extra?.magpAuthorizationId) return { ok: false, reasonCode: "MISSING_BINDING" };
|
|
30
|
+
if (accept.extra.magpAuthorizationId !== claim.authorizationId) {
|
|
31
|
+
return { ok: false, reasonCode: "AUTHORIZATION_MISMATCH" };
|
|
32
|
+
}
|
|
33
|
+
const authorizedMinor = Number(accept.maxAmountRequired);
|
|
34
|
+
const paidMinor = Number(claim.paidAmountMinor);
|
|
35
|
+
if (!Number.isFinite(paidMinor) || paidMinor < 0) return { ok: false, reasonCode: "AMOUNT_INVALID" };
|
|
36
|
+
if (paidMinor > authorizedMinor) return { ok: false, reasonCode: "AMOUNT_MISMATCH" };
|
|
37
|
+
return { ok: true, reasonCode: "BINDING_OK" };
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
buildPaymentRequirements,
|
|
41
|
+
checkSettlementBinding,
|
|
42
|
+
toMinorUnits
|
|
43
|
+
};
|