@metamynd/agentsafe-guard 0.1.2 → 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/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, escalate: 1, block: 2 };
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, escalate: 1, block: 2 };
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
+ };