@metamynd/agentsafe-guard 0.3.1 → 0.4.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.
@@ -14,6 +14,26 @@ import { evaluate, buildAuthMessage, applySignedLast, operatingModeGate } from '
14
14
  import { verifyDidSignature } from './magp-did.mjs';
15
15
  import { checkSettlementBinding } from './x402.mjs';
16
16
 
17
+ /**
18
+ * Replay a Merkle sibling chain and report whether it reconstructs `root`.
19
+ *
20
+ * Byte-identical to backend/src/features/magp/merkle.ts: leaves and siblings are hex
21
+ * sha256 digests, and an internal node is sha256 over the CONCATENATED RAW BYTES of its
22
+ * children (not the hex text), in left-then-right order. Hashing the hex strings instead
23
+ * would produce a self-consistent but incompatible tree — one that verified nothing the
24
+ * backend ever anchored, while appearing to work.
25
+ */
26
+ function verifyMerkleInclusion(leaf, proof, root) {
27
+ const sha = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
28
+ const hashNodes = (a, b) => sha(Buffer.concat([Buffer.from(a, 'hex'), Buffer.from(b, 'hex')]));
29
+ let computed = leaf;
30
+ for (const step of proof) {
31
+ if (!step || typeof step.sibling !== 'string') return false;
32
+ computed = step.position === 'left' ? hashNodes(step.sibling, computed) : hashNodes(computed, step.sibling);
33
+ }
34
+ return computed === root;
35
+ }
36
+
17
37
  /**
18
38
  * ExecutionAdapter (SAFR §19, Phase-4 PR-4) — the seam between a PERMITTING verdict
19
39
  * (allow / observe) and the real side-effect. Before this, a guarded tool called its
@@ -538,6 +558,71 @@ export function createGuard(opts = {}) {
538
558
  }
539
559
  }
540
560
 
561
+ /**
562
+ * Merkle inclusion proof for a decision's evidence record — fetched, then VERIFIED
563
+ * HERE rather than taken on trust.
564
+ *
565
+ * The point of an inclusion proof is that its holder can check it WITHOUT trusting the
566
+ * party that issued it. A helper that returned the server's payload as-is would look
567
+ * like proof and function as assertion: the caller would be believing MetaMynd's claim
568
+ * that the record is in the anchored batch, which is exactly the thing the proof exists
569
+ * to make unnecessary. So the sibling chain is replayed locally and the recomputed root
570
+ * is compared to the anchored one; `verified` is this SDK's own conclusion.
571
+ *
572
+ * Absence and falsification are reported as DIFFERENT outcomes, because they mean
573
+ * opposite things to whoever is asking:
574
+ *
575
+ * status 'verified' the record is provably in the batch anchored at anchorTxId
576
+ * status 'pending' no anchored batch contains it YET — anchoring is asynchronous
577
+ * (§10.2), so a recent decision is normally pending, not missing
578
+ * status 'failed' a proof was returned and it does NOT reconstruct the root.
579
+ * This is the alarming one and must never be conflated with
580
+ * 'pending'
581
+ * status 'unreachable' the gate could not be asked; nothing is implied either way
582
+ *
583
+ * Note the trust boundary this does NOT cross: it proves the record belongs to the
584
+ * batch that claims `root`. Proving that root was published on Hedera is a separate,
585
+ * stronger check against the mirror node — see integrations/magp-evidence/, the offline
586
+ * auditor, which does it with MetaMynd entirely absent.
587
+ */
588
+ async function proof(eventId) {
589
+ if (!eventId) throw new Error('proof requires the evidence eventId');
590
+ let body;
591
+ let httpStatus;
592
+ try {
593
+ const res = await fetch(`${base}/magp/evidence/${encodeURIComponent(eventId)}/proof`);
594
+ httpStatus = res.status;
595
+ body = await res.json().catch(() => null);
596
+ } catch (err) {
597
+ return { status: 'unreachable', verified: false, eventId, reason: 'GATE_UNREACHABLE', error: String(err?.message ?? err) };
598
+ }
599
+
600
+ if (httpStatus === 404) {
601
+ // Not an error: batching is asynchronous, so a decision made seconds ago has
602
+ // genuinely not been anchored yet. Saying "unverified" here would read as doubt
603
+ // about a record that is simply young.
604
+ return { status: 'pending', verified: false, eventId, reason: 'NOT_YET_ANCHORED' };
605
+ }
606
+ const data = body?.data;
607
+ if (!data?.leaf || !data?.root || !Array.isArray(data?.proof)) {
608
+ return { status: 'unreachable', verified: false, eventId, reason: `GATE_HTTP_${httpStatus}` };
609
+ }
610
+
611
+ const verified = verifyMerkleInclusion(data.leaf, data.proof, data.root);
612
+ return {
613
+ status: verified ? 'verified' : 'failed',
614
+ verified,
615
+ eventId,
616
+ leaf: data.leaf,
617
+ root: data.root,
618
+ proof: data.proof,
619
+ anchorTxId: data.anchorTxId ?? null,
620
+ anchorRef: data.anchorRef ?? null,
621
+ anchoredAt: data.anchoredAt ?? null,
622
+ ...(verified ? {} : { reason: 'MERKLE_ROOT_MISMATCH' }),
623
+ };
624
+ }
625
+
541
626
  /**
542
627
  * Effect-safety runtime (E2): report the external-effect lifecycle so an AMBIGUOUS
543
628
  * connector outcome never becomes a blind capture/void. Call effectDispatching() just
@@ -602,5 +687,5 @@ export function createGuard(opts = {}) {
602
687
  return sign(challenge);
603
688
  }
604
689
 
605
- return { authorize, authorizeLocal, check, loadBundle, policyAnchor: _currentAnchor, watchPolicy, mode, verifyOnChain, buildSignedRequest, capture, guardTool, evaluateLocally, guardToolLocal, handshake, preparePayment, escalationStatus, effectDispatching, effectDispatched, effectUnknown, effectStatus, verifyKey, signChallenge, agentDid, executionAdapter: defaultExecutionAdapter };
690
+ return { authorize, authorizeLocal, check, loadBundle, policyAnchor: _currentAnchor, watchPolicy, mode, verifyOnChain, buildSignedRequest, capture, guardTool, evaluateLocally, guardToolLocal, handshake, preparePayment, escalationStatus, proof, effectDispatching, effectDispatched, effectUnknown, effectStatus, verifyKey, signChallenge, agentDid, executionAdapter: defaultExecutionAdapter };
606
691
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@metamynd/agentsafe-guard",
3
- "version": "0.3.1",
4
- "description": "Zero-dependency runtime governance for any Node AI agent gate tool calls through MetaMynd/AgentSafe (allow / block / escalate) against the agent's mandate, enforced Standards, and SOPs. Ed25519-signed, deterministic, fail-closed.",
3
+ "version": "0.4.0",
4
+ "description": "Zero-dependency runtime governance for any Node AI agent \u2014 gate tool calls through MetaMynd/AgentSafe (allow / block / escalate) against the agent's mandate, enforced Standards, and SOPs. Ed25519-signed, deterministic, fail-closed.",
5
5
  "type": "module",
6
6
  "main": "./agentsafe-guard.mjs",
7
7
  "module": "./agentsafe-guard.mjs",