@metamynd/agentsafe-mcp-guard 0.4.0 → 0.6.1

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 CHANGED
@@ -35,8 +35,8 @@ const guard = createMcpGuard({
35
35
  });
36
36
 
37
37
  // Responder side, over your HTTP transport:
38
- const challenge = guard.handshakeChallenge(hello); // POST /magp/handshake (HELLO → CHALLENGE)
39
- const ready = guard.handshakeVerify(prove); // POST /magp/handshake (PROVE → READY, or throws)
38
+ const challenge = await guard.handshakeChallenge(hello); // POST /magp/handshake (HELLO → CHALLENGE)
39
+ const ready = guard.handshakeVerify(prove); // POST /magp/handshake (PROVE → READY, or throws)
40
40
  ```
41
41
 
42
42
  The agent drives the initiator side with `createGuard(...).handshake()` from `agentsafe-guard`.
@@ -110,6 +110,55 @@ $5,000" protection never engaged, verifier configured or not. New `requireCapabi
110
110
  an omitted capability a hard block (`CAPABILITY_REQUIRED`) instead of a silent pass-through. Both
111
111
  off by default — existing embeds are unchanged.
112
112
 
113
+ **0.5.0 — the `keyProvider` seam
114
+ ([docs/design/agent-key-custody-local-signer-daemon-plan.md](../../docs/design/agent-key-custody-local-signer-daemon-plan.md)).**
115
+ `serviceKey` no longer has to be a raw hex key living in this process. Pass
116
+ `keyProvider: 'daemon'` + `daemonSocketPath` instead, and `handshakeChallenge` gets its signature
117
+ from a separate `@metamynd/agentsafe-signer` daemon (`role: 'service'`) over a local socket — the
118
+ key never enters this process at all. `serviceKey` (unchanged) still works exactly as before and
119
+ remains the default. **Breaking, disclosed plainly**: `handshakeChallenge` and
120
+ `createHandshakeInitiator(...).prove()` are now `async` (a daemon-backed provider needs a socket
121
+ round trip); every caller needs an `await` added. New internal module `key-providers.mjs` — still
122
+ zero external dependencies.
123
+
124
+ **0.5.3 — `requireAuthorization`'s claim now actually enforces revoked authority and mandate
125
+ expiry, and the claim-binding check closes the action axis too.** A four-client readiness review's
126
+ P0 finding required a tested counterparty-verification pattern covering, by name, replay, wrong
127
+ identity, wrong action, expired mandate, and revoked authority. Building the end-to-end trial
128
+ against a real backend (not the mocked cases below) surfaced that two of those five were silently
129
+ unenforced at the claim step itself, not just untested:
130
+
131
+ - **Revoked authority.** Revoking a mandate (`POST /mandate/:ref/revoke`) only ever flipped the
132
+ hold's `mandate_event.status` to `voided` — it never touched the `effect_transition` chain, and
133
+ `markEffect`'s transition check only validated the state-machine shape (`authorized →
134
+ dispatching` is always syntactically legal), never the hold's own status. A resource relying on
135
+ `requireAuthorization` could successfully claim and execute a hold whose mandate had already been
136
+ revoked. Now refused with `AUTHORIZATION_VOIDED`.
137
+ - **Expired mandate.** A hold's expiry (`isWithinHoldWindow`/the TTL) is a *derived* property —
138
+ nothing writes it back to the row when time passes, so the same claim step had no way to see a
139
+ hold was stale. Now refused with `AUTHORIZATION_EXPIRED`.
140
+ - **Wrong action.** `claimAuthorization()`'s mismatch checks compared the claimed hold's
141
+ `agentDid`/`amount`/`currency`/`merchant` against the request being executed, but never `action`
142
+ — because the backend's claim response never carried it. A real, unclaimed authorization minted
143
+ for one action (e.g. `office-supplies-purchase`) could be claimed while executing a *different*
144
+ action at the identical agent/amount/currency/merchant — the same confused-deputy shape the 0.3.6
145
+ merchant check closed, one axis short. The backend now resolves and returns the hold's authorized
146
+ `action` on claim (backward-compatible — an older backend simply omits it, same
147
+ skip-when-absent tolerance as every other field here). Now refused with
148
+ `AUTHORIZATION_ACTION_MISMATCH`.
149
+
150
+ All three are backend-side (`MandateService.markEffect`) except the action check, which also
151
+ needed this guard to compare the new field. See
152
+ [`docs/design/mcp-reverification-quickstart.md`](../../docs/design/mcp-reverification-quickstart.md) (§7)
153
+ for the end-to-end trial these gaps were found and closed against.
154
+
155
+ **0.5.1 — `keyProvider: 'daemon'` retries a transient connect failure.** Same fix as
156
+ `@metamynd/agentsafe-guard` 0.9.1: on Windows the signer daemon's socket is a pool of independent
157
+ named-pipe instances, each consumed by one connection and replaced asynchronously, so two
158
+ signing requests close together could race that replacement window and fail with
159
+ `DAEMON_UNREACHABLE` even though the daemon was healthy. `key-providers.mjs` now retries a
160
+ connection that fails with `ENOENT` for up to 3 seconds before giving up. No API change.
161
+
113
162
  ### Replay, cumulative spend, rate limits, breakers, spend anomalies (`requireAuthorization`)
114
163
 
115
164
  Re-evaluating policy per request (above) proves the request is well-formed and in-policy — it
@@ -16,6 +16,7 @@ import { evaluate, buildAuthMessage, applySignedLast, operatingModeGate } from '
16
16
  import { verifyDidSignature } from './magp-did.mjs';
17
17
  import { buildPaymentRequirements, checkSettlementBinding } from './x402.mjs';
18
18
  import { verifyBundle } from './magp-policy.mjs';
19
+ import { resolveKeyProvider } from './key-providers.mjs';
19
20
 
20
21
  /** Freshness window for signed requests and handshake nonces (spec §7.7). How far `issuedAt`
21
22
  * may be BEHIND server time — network/processing delay. */
@@ -57,19 +58,15 @@ const CLOCK_SKEW_TOLERANCE_MS = 30 * 1000;
57
58
  * integrator's un-capability-aware callers must keep working); set true on any Service where
58
59
  * capability binding is meant to be mandatory, not opt-in.
59
60
  */
60
- export function createMcpGuard({ serviceDid, serviceKey, issuerApi, fetchBundle, policyPublicKey, settlementStore, verifyCapability, requireAuthorization = false, requireCapability = false } = {}) {
61
+ export function createMcpGuard({ serviceDid, serviceKey, keyProvider: keyProviderOpt, daemonSocketPath, issuerApi, fetchBundle, policyPublicKey, settlementStore, verifyCapability, requireAuthorization = false, requireCapability = false } = {}) {
61
62
  if (!serviceDid) throw new Error('createMcpGuard requires { serviceDid }');
62
63
  const base = issuerApi ? issuerApi.replace(/\/$/, '') : null;
63
- const privateKey = serviceKey
64
- ? crypto.createPrivateKey({ key: Buffer.from(serviceKey, 'hex'), format: 'der', type: 'pkcs8' })
65
- : null;
64
+ // keyProvider seam (docs/design/agent-key-custody-local-signer-daemon-plan.md): null when
65
+ // neither `serviceKey` nor `keyProvider` is configured handshakeChallenge throws its own
66
+ // clear error only if actually called, matching the original lazy-throw behavior exactly.
67
+ const keyProvider = resolveKeyProvider({ keyProvider: keyProviderOpt, serviceKey, daemonSocketPath });
66
68
  const pending = new Map(); // handshakeId -> { fromDid, nonceB, expiresAt }
67
69
 
68
- function sign(message) {
69
- if (!privateKey) throw new Error('serviceKey is required to sign handshake messages');
70
- return crypto.sign(null, Buffer.from(message, 'utf8'), privateKey).toString('hex');
71
- }
72
-
73
70
  // --- Mutual handshake, RESPONDER side (spec §8.2) ---
74
71
  // A → B HELLO { fromDid, nonceA }
75
72
  // B → A CHALLENGE { toDid, nonceB, sigB(nonceA) } ← proves B controls toDid
@@ -77,12 +74,13 @@ export function createMcpGuard({ serviceDid, serviceKey, issuerApi, fetchBundle,
77
74
  // B → A READY { channelId }
78
75
 
79
76
  /** Step 1 (B): on HELLO, sign nonceA to prove control of serviceDid, issue nonceB. */
80
- function handshakeChallenge({ fromDid, nonceA, protoVersion } = {}) {
77
+ async function handshakeChallenge({ fromDid, nonceA, protoVersion } = {}) {
81
78
  if (!fromDid || !nonceA) throw new Error('HELLO requires { fromDid, nonceA }');
79
+ if (!keyProvider) throw new Error('serviceKey (or keyProvider) is required to sign handshake messages');
82
80
  const handshakeId = crypto.randomUUID();
83
81
  const nonceB = crypto.randomUUID();
84
82
  pending.set(handshakeId, { fromDid, nonceB, expiresAt: Date.now() + FRESHNESS_MS });
85
- return { handshakeId, toDid: serviceDid, nonceB, sigB: sign(nonceA), protoVersion: protoVersion ?? '1.0' };
83
+ return { handshakeId, toDid: serviceDid, nonceB, sigB: await keyProvider.signHandshakeNonce(nonceA), protoVersion: protoVersion ?? '1.0' };
86
84
  }
87
85
 
88
86
  /** Step 2 (B): on PROVE, verify sigA over nonceB against fromDid's key-in-DID. */
@@ -110,12 +108,12 @@ export function createMcpGuard({ serviceDid, serviceKey, issuerApi, fetchBundle,
110
108
  * against the stateful issuer gate already checked both (agentsafe-guard.mjs's authorizeLocal()
111
109
  * seals any value-bearing action through the real remote authorize() by default).
112
110
  *
113
- * On success, also returns the hold's OWN bound `agentDid`/`amount`/`currency`/`merchant` — the
114
- * caller MUST compare these to the request actually being executed. A claim alone only proves
115
- * "some real, unclaimed authorization exists"; without this check, a legitimately-obtained
116
- * authorization for a small, honest transaction could be presented to unlock a completely
117
- * different one the same confused-deputy shape payload binding closes at the request layer,
118
- * recurring one layer deeper.
111
+ * On success, also returns the hold's OWN bound `agentDid`/`action`/`amount`/`currency`/
112
+ * `merchant` — the caller MUST compare these to the request actually being executed. A claim
113
+ * alone only proves "some real, unclaimed authorization exists"; without this check, a
114
+ * legitimately-obtained authorization for a small, honest transaction (or a DIFFERENT action
115
+ * entirely) could be presented to unlock a completely different one the same confused-deputy
116
+ * shape payload binding closes at the request layer, recurring one layer deeper.
119
117
  */
120
118
  async function claimAuthorization({ authorizationId } = {}) {
121
119
  if (!authorizationId) return { claimed: false, reasonCode: 'AUTHORIZATION_REQUIRED' };
@@ -124,7 +122,14 @@ export function createMcpGuard({ serviceDid, serviceKey, issuerApi, fetchBundle,
124
122
  const res = await fetch(`${base}/policy/mandate/authorize/${encodeURIComponent(authorizationId)}/effect/dispatching`, { method: 'POST' });
125
123
  const body = await res.json().catch(() => null);
126
124
  if (!res.ok) return { claimed: false, reasonCode: body?.message ?? body?.data?.reasonCode ?? `AUTHORIZATION_CLAIM_HTTP_${res.status}` };
127
- return { claimed: true, agentDid: body?.data?.agentDid, amount: body?.data?.amount, currency: body?.data?.currency, merchant: body?.data?.merchant };
125
+ return {
126
+ claimed: true,
127
+ agentDid: body?.data?.agentDid,
128
+ action: body?.data?.action,
129
+ amount: body?.data?.amount,
130
+ currency: body?.data?.currency,
131
+ merchant: body?.data?.merchant,
132
+ };
128
133
  } catch (err) {
129
134
  return { claimed: false, reasonCode: 'AUTHORIZATION_CLAIM_UNREACHABLE', error: String(err?.message ?? err) };
130
135
  }
@@ -253,10 +258,12 @@ export function createMcpGuard({ serviceDid, serviceKey, issuerApi, fetchBundle,
253
258
  // re-open the confused-deputy gap this claim exists to close, and nothing else here
254
259
  // would notice.
255
260
  if (claim.agentDid === undefined) console.warn('[mcp-guard] claim response omitted agentDid — binding degraded to "some valid unclaimed authorization exists"');
261
+ if (claim.action === undefined) console.warn('[mcp-guard] claim response omitted action — action binding degraded');
256
262
  if (Number(amount) > 0 && claim.amount === undefined) console.warn('[mcp-guard] claim response omitted amount for a value-bearing request — amount binding degraded');
257
263
  if (Number(amount) > 0 && claim.currency === undefined) console.warn('[mcp-guard] claim response omitted currency for a value-bearing request — currency binding degraded');
258
264
  if (merchant && claim.merchant === undefined) console.warn('[mcp-guard] claim response omitted merchant for a request that signed one — merchant binding degraded');
259
265
  if (claim.agentDid !== undefined && claim.agentDid !== agentDid) return { decision: 'block', reasonCode: 'AUTHORIZATION_AGENT_MISMATCH' };
266
+ if (claim.action !== undefined && claim.action !== action) return { decision: 'block', reasonCode: 'AUTHORIZATION_ACTION_MISMATCH' };
260
267
  if (claim.amount !== undefined && Number(claim.amount) !== Number(amount)) return { decision: 'block', reasonCode: 'AUTHORIZATION_AMOUNT_MISMATCH' };
261
268
  if (claim.currency !== undefined && claim.currency !== currency) return { decision: 'block', reasonCode: 'AUTHORIZATION_CURRENCY_MISMATCH' };
262
269
  if (claim.merchant !== undefined && claim.merchant !== merchant) return { decision: 'block', reasonCode: 'AUTHORIZATION_MERCHANT_MISMATCH' };
@@ -274,7 +281,17 @@ export function createMcpGuard({ serviceDid, serviceKey, issuerApi, fetchBundle,
274
281
  */
275
282
  function guardIncomingTool(action, handler) {
276
283
  return async (signed, ...rest) => {
277
- const decision = await verifyRequest({ ...signed, action: signed?.action ?? action });
284
+ // The WRAPPED TOOL's own `action` is authoritative — never `signed?.action` (the caller's
285
+ // own claim). A Service that wraps more than one tool with ONE guard instance (the normal
286
+ // MCP-server shape: many tools, one guard) previously let a genuinely-valid signature for
287
+ // action A verify successfully — correctly, it really was valid for A — and then run
288
+ // action B's handler, because verifyRequest was asked to check whatever the SIGNED payload
289
+ // claimed instead of which wrapped function was actually being invoked. A caller with a
290
+ // real, cheap, in-policy authorization (e.g. a $0 read) could invoke ANY other tool sharing
291
+ // this guard (e.g. a wire transfer) and have it execute under that unrelated verification.
292
+ // Mirrors gateway.mjs's `route.action ?? signed.action` — "the route pins the action ...
293
+ // the client can't pick it" — for exactly the same reason, one layer down at the tool call.
294
+ const decision = await verifyRequest({ ...signed, action });
278
295
  // allow/observe both PERMIT the tool call; observe is permit-but-flag (SAFR §11).
279
296
  if (decision.decision !== 'allow' && decision.decision !== 'observe') {
280
297
  const err = new Error(`MCP guard ${decision.decision.toUpperCase()} "${action}": ${decision.reasonCode}`);
@@ -389,7 +406,9 @@ export function createMcpGuard({ serviceDid, serviceKey, issuerApi, fetchBundle,
389
406
  * then, given the responder's CHALLENGE, verifies the responder proved control of
390
407
  * its DID before producing PROVE.
391
408
  *
392
- * @param {{fromDid:string, sign:(msg:string)=>string}} p sign() uses the initiator's own key
409
+ * @param {{fromDid:string, sign:(msg:string)=>(string|Promise<string>)}} p sign() uses the
410
+ * initiator's own key — may be sync (a raw local key) or async (e.g. a keyProvider backed by
411
+ * agentsafe-signer); `prove()` awaits it either way, see key-providers.mjs.
393
412
  */
394
413
  export function createHandshakeInitiator({ fromDid, sign } = {}) {
395
414
  if (!fromDid || typeof sign !== 'function') throw new Error('createHandshakeInitiator requires { fromDid, sign }');
@@ -400,7 +419,7 @@ export function createHandshakeInitiator({ fromDid, sign } = {}) {
400
419
  return { nonceA, message: { fromDid, nonceA, protoVersion: '1.0' } };
401
420
  },
402
421
  /** Step 3 (A): verify CHALLENGE proves the responder controls toDid, then PROVE. */
403
- prove({ nonceA, challenge } = {}) {
422
+ async prove({ nonceA, challenge } = {}) {
404
423
  const { toDid, nonceB, sigB, handshakeId } = challenge ?? {};
405
424
  if (!toDid || !nonceB || !sigB) throw new Error('malformed CHALLENGE');
406
425
  if (!verifyDidSignature(toDid, nonceA, sigB)) {
@@ -408,7 +427,7 @@ export function createHandshakeInitiator({ fromDid, sign } = {}) {
408
427
  e.name = 'HandshakeFailed';
409
428
  throw e;
410
429
  }
411
- return { handshakeId, sigA: sign(nonceB), remoteDid: toDid };
430
+ return { handshakeId, sigA: await sign(nonceB), remoteDid: toDid };
412
431
  },
413
432
  };
414
433
  }
@@ -0,0 +1,91 @@
1
+ // key-providers.mjs — the keyProvider seam for the SERVICE side (docs/design/
2
+ // agent-key-custody-local-signer-daemon-plan.md). Much smaller than agentsafe-guard's own copy:
3
+ // serviceKey is only ever used for one thing (signing a handshake nonce, MAGP §8.2), so there is
4
+ // only one provider method here, not four.
5
+ import crypto from 'node:crypto';
6
+ import net from 'node:net';
7
+ import path from 'node:path';
8
+
9
+ export function createStaticKeyProvider(serviceKeyHex) {
10
+ const privateKey = crypto.createPrivateKey({ key: Buffer.from(serviceKeyHex, 'hex'), format: 'der', type: 'pkcs8' });
11
+ return {
12
+ async signHandshakeNonce(nonce) {
13
+ return crypto.sign(null, Buffer.from(nonce, 'utf8'), privateKey).toString('hex');
14
+ },
15
+ };
16
+ }
17
+
18
+ /** Must match agentsafe-guard/key-providers.mjs's and agentsafe-signer/daemon.mjs's own copy — see either's own comment for why this isn't a shared import. */
19
+ function toPlatformSocketPath(logicalPath) {
20
+ if (process.platform !== 'win32') return logicalPath;
21
+ const name = crypto.createHash('sha256').update(path.resolve(logicalPath)).digest('hex').slice(0, 32);
22
+ return `\\\\.\\pipe\\agentsafe-signer-${name}`;
23
+ }
24
+
25
+ const PROTOCOL_VERSION = 1;
26
+
27
+ function daemonRequest(socketPath, op, params, { connectTimeoutMs = 3000 } = {}) {
28
+ return new Promise((resolve, reject) => {
29
+ const deadline = Date.now() + connectTimeoutMs;
30
+ function attempt() {
31
+ const sock = net.connect(toPlatformSocketPath(socketPath));
32
+ const requestId = crypto.randomUUID();
33
+ let buf = '';
34
+ const cleanup = () => sock.destroy();
35
+ sock.once('error', (err) => {
36
+ cleanup();
37
+ // See agentsafe-guard/key-providers.mjs's own copy of this function for why: on Windows
38
+ // the signing socket is a pool of independent named-pipe instances, each consumed by one
39
+ // connection and replaced asynchronously, so a request can transiently race that
40
+ // replacement window (ENOENT) even though the daemon is healthy. Only ENOENT retries — a
41
+ // genuinely down daemon still fails fast.
42
+ if (err.code === 'ENOENT' && Date.now() < deadline) {
43
+ setTimeout(attempt, 20);
44
+ return;
45
+ }
46
+ reject(Object.assign(new Error(`agentsafe-signer daemon unreachable at ${socketPath}: ${err.message}`), { code: 'DAEMON_UNREACHABLE' }));
47
+ });
48
+ sock.once('connect', () => {
49
+ sock.write(JSON.stringify({ protocolVersion: PROTOCOL_VERSION, requestId, op, params }) + '\n');
50
+ });
51
+ sock.on('data', (chunk) => {
52
+ buf += chunk.toString('utf8');
53
+ const idx = buf.indexOf('\n');
54
+ if (idx === -1) return;
55
+ let res;
56
+ try {
57
+ res = JSON.parse(buf.slice(0, idx));
58
+ } catch (err) {
59
+ cleanup();
60
+ reject(err);
61
+ return;
62
+ }
63
+ cleanup();
64
+ if (res.ok) resolve(res.result);
65
+ else reject(Object.assign(new Error(res.error?.message || res.error?.code || 'daemon rejected request'), { code: res.error?.code }));
66
+ });
67
+ }
68
+ attempt();
69
+ });
70
+ }
71
+
72
+ /** The key never enters this process — a service-role agentsafe-signer daemon signs instead. */
73
+ export function createDaemonKeyProvider({ socketPath }) {
74
+ if (!socketPath) throw new Error('createDaemonKeyProvider requires { socketPath }');
75
+ return {
76
+ async signHandshakeNonce(nonce) {
77
+ const { signature } = await daemonRequest(socketPath, 'sign-handshake-nonce', { nonce });
78
+ return signature;
79
+ },
80
+ };
81
+ }
82
+
83
+ /** Resolves a keyProvider from createMcpGuard's opts — null (not thrown) when neither
84
+ * keyProvider nor serviceKey is configured, matching today's "handshakeChallenge throws only if
85
+ * actually called without one" behavior rather than failing construction eagerly. */
86
+ export function resolveKeyProvider({ keyProvider, serviceKey, daemonSocketPath } = {}) {
87
+ if (keyProvider && typeof keyProvider === 'object' && typeof keyProvider.signHandshakeNonce === 'function') return keyProvider;
88
+ if (keyProvider === 'daemon') return createDaemonKeyProvider({ socketPath: daemonSocketPath });
89
+ if (!serviceKey) return null;
90
+ return createStaticKeyProvider(serviceKey);
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamynd/agentsafe-mcp-guard",
3
- "version": "0.4.0",
3
+ "version": "0.6.1",
4
4
  "description": "Zero-dependency trustless governance for the SERVICE side. An MCP server or API re-verifies a calling agent's signed request against the agent's own published policy \u00e2\u20ac\u201d so an agent that ignores its own guard still cannot make your service act.",
5
5
  "type": "module",
6
6
  "main": "./agentsafe-mcp-guard.mjs",
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "files": [
17
17
  "agentsafe-mcp-guard.mjs",
18
+ "key-providers.mjs",
18
19
  "magp-policy.mjs",
19
20
  "magp-did.mjs",
20
21
  "policy-core.mjs",
@@ -23,7 +24,7 @@
23
24
  "LICENSE"
24
25
  ],
25
26
  "scripts": {
26
- "test": "node mcp-guard.smoke.mjs && node pay.smoke.mjs && node decision-token.smoke.mjs && node claim-authorization.smoke.mjs"
27
+ "test": "node mcp-guard.smoke.mjs && node pay.smoke.mjs && node decision-token.smoke.mjs && node claim-authorization.smoke.mjs && node guard-incoming-tool.smoke.mjs"
27
28
  },
28
29
  "engines": {
29
30
  "node": ">=18"
package/policy-core.mjs CHANGED
@@ -514,7 +514,15 @@ function escapeField(v) {
514
514
  return v.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
515
515
  }
516
516
  function buildAuthMessage(f) {
517
- return [f.agentDid, f.action, f.amount, f.currency, f.merchant ?? "", f.nonce, f.issuedAt].map((v) => escapeField(String(v))).join("|");
517
+ return [f.agentDid, f.action, f.amount, f.currency, f.merchant ?? "", f.resource ?? "", f.nonce, f.issuedAt].map((v) => escapeField(String(v))).join("|");
518
+ }
519
+ function buildLocalDecisionMessage(f) {
520
+ return [f.agentDid, f.action, f.decision, f.reasonCode, f.nonce, f.issuedAt].map((v) => escapeField(String(v))).join("|");
521
+ }
522
+
523
+ // src/policy-core/checkpoint-anchor.ts
524
+ function buildCheckpointAnchorMessage(f) {
525
+ return [f.agentDid, f.checkpointHash, f.previousCheckpointHash, f.entryCount, f.nonce, f.issuedAt].map((v) => escapeField(String(v))).join("|");
518
526
  }
519
527
 
520
528
  // src/policy-core/context.ts
@@ -573,6 +581,8 @@ export {
573
581
  asOperatingMode,
574
582
  authorityFailure,
575
583
  buildAuthMessage,
584
+ buildCheckpointAnchorMessage,
585
+ buildLocalDecisionMessage,
576
586
  canAuthorize,
577
587
  evaluate,
578
588
  evaluateBoundStandards,