@suveren/gateway 0.19.5 → 0.20.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.
Files changed (46) hide show
  1. package/content/integrations/crm.json +1 -1
  2. package/content/integrations/deploy-github.json +1 -1
  3. package/content/integrations/erp.json +1 -1
  4. package/content/integrations/mail.json +1 -1
  5. package/content/integrations/records.json +1 -1
  6. package/dist/control-plane/index.mjs +21 -13
  7. package/dist/mcp-server/{chunk-ZRRZFGRL.mjs → chunk-APIQIEUA.mjs} +39 -27
  8. package/dist/mcp-server/http.mjs +308 -97
  9. package/dist/mcp-server/report-verify-cli.mjs +1 -1
  10. package/dist/ui/assets/index-1gCMlbLl.js +116 -0
  11. package/dist/ui/index.html +1 -1
  12. package/node_modules/@hap/core/README.md +92 -29
  13. package/node_modules/@hap/core/dist/index.d.mts +614 -593
  14. package/node_modules/@hap/core/dist/index.d.ts +614 -593
  15. package/node_modules/@hap/core/dist/index.js +774 -749
  16. package/node_modules/@hap/core/dist/index.mjs +744 -724
  17. package/node_modules/@hap/core/package.json +1 -1
  18. package/node_modules/@hap/core/src/base64url.ts +39 -0
  19. package/node_modules/@hap/core/src/commitment-modes.ts +0 -21
  20. package/node_modules/@hap/core/src/errors.ts +76 -0
  21. package/node_modules/@hap/core/src/frame.ts +47 -178
  22. package/node_modules/@hap/core/src/gatekeeper.ts +219 -600
  23. package/node_modules/@hap/core/src/identity.ts +9 -12
  24. package/node_modules/@hap/core/src/index.ts +8 -4
  25. package/node_modules/@hap/core/src/mandate.ts +177 -166
  26. package/node_modules/@hap/core/src/owner-signature.ts +282 -0
  27. package/node_modules/@hap/core/src/profile.ts +234 -0
  28. package/node_modules/@hap/core/src/ticket.ts +148 -0
  29. package/node_modules/@hap/core/src/types.ts +191 -344
  30. package/node_modules/@hap/core/src/versions.ts +47 -0
  31. package/package.json +2 -2
  32. package/profiles/calendar/0.6.profile.json +191 -0
  33. package/profiles/charge/0.6.profile.json +211 -0
  34. package/profiles/customers/0.9.profile.json +182 -0
  35. package/profiles/delegation/0.3.profile.json +118 -0
  36. package/profiles/deploy/0.11.profile.json +177 -0
  37. package/profiles/email/0.8.profile.json +226 -0
  38. package/profiles/index.json +12 -1
  39. package/profiles/publish/0.6.profile.json +187 -0
  40. package/profiles/purchase/0.7.profile.json +235 -0
  41. package/profiles/records/0.6.profile.json +118 -0
  42. package/profiles/reporting/0.3.profile.json +98 -0
  43. package/profiles/sales/0.4.profile.json +291 -0
  44. package/dist/ui/assets/index-Du68zJhH.js +0 -116
  45. package/node_modules/@hap/core/src/attestation.ts +0 -173
  46. package/node_modules/@hap/core/src/receipt.ts +0 -74
@@ -16,6 +16,7 @@ import {
16
16
  fingerprintOf,
17
17
  formatDateTime,
18
18
  glossaryUsage,
19
+ issuerFromPublicKeyHex,
19
20
  mandateFields,
20
21
  parseCaseAttrs,
21
22
  parseElements,
@@ -30,7 +31,7 @@ import {
30
31
  sanitizeReport,
31
32
  suggestedFilename,
32
33
  ticketFields
33
- } from "./chunk-ZRRZFGRL.mjs";
34
+ } from "./chunk-APIQIEUA.mjs";
34
35
 
35
36
  // bin/http.ts
36
37
  import { randomUUID as randomUUID2 } from "crypto";
@@ -848,20 +849,40 @@ var SPClient = class {
848
849
  return data.publicKey;
849
850
  }
850
851
  /**
851
- * Get all attestations for a frame hash.
852
+ * GET /api/as/compat — what this Authority Server speaks (protocol.md ->
853
+ * Version negotiation). Unauthenticated, so a gateway can check it at
854
+ * startup before any session exists (V7: fail closed when the AS does
855
+ * not list our protocol version among `supportedVersions`, rather than
856
+ * discovering it call-by-call once a tool is actually invoked).
852
857
  */
853
- async getAttestations(authorizationId) {
854
- const res = await this.fetch(`/api/attestations?authorization_id=${encodeURIComponent(authorizationId)}`);
855
- if (!res.ok) throw new Error(`SP attestations request failed: ${res.status}`);
858
+ async getCompat() {
859
+ const res = await this.fetch("/api/as/compat");
860
+ if (!res.ok) throw new Error(`SP compat request failed: ${res.status}`);
856
861
  return res.json();
857
862
  }
858
863
  /**
859
- * Get pending attestations for a domain.
864
+ * Get all mandates for an authorization id.
865
+ *
866
+ * v0.7: moved from GET /api/attestations (which now answers 410) to
867
+ * GET /api/mandates.
868
+ */
869
+ async getAttestations(authorizationId) {
870
+ const res = await this.fetch(`/api/mandates?authorization_id=${encodeURIComponent(authorizationId)}`);
871
+ if (!res.ok) throw new Error(`SP mandates request failed: ${res.status}`);
872
+ const body = await res.json();
873
+ return { ...body, attestations: body.mandates ?? body.attestations ?? [] };
874
+ }
875
+ /**
876
+ * Get pending mandates for a domain.
877
+ *
878
+ * v0.7: moved from GET /api/attestations/pending (which now answers 410)
879
+ * to GET /api/mandates/pending.
860
880
  */
861
881
  async getPendingAttestations(domain) {
862
- const res = await this.fetch(`/api/attestations/pending?domain=${encodeURIComponent(domain)}`);
882
+ const res = await this.fetch(`/api/mandates/pending?domain=${encodeURIComponent(domain)}`);
863
883
  if (!res.ok) throw new Error(`SP pending request failed: ${res.status}`);
864
- return res.json();
884
+ const data = await res.json();
885
+ return data.pending;
865
886
  }
866
887
  /**
867
888
  * Request a signed receipt from the SP.
@@ -890,7 +911,7 @@ var SPClient = class {
890
911
  const isLast = attempt === maxAttempts;
891
912
  let res;
892
913
  try {
893
- res = await this.fetch("/api/as/receipt", { method: "POST", body });
914
+ res = await this.fetch("/api/as/ticket", { method: "POST", body });
894
915
  } catch (err) {
895
916
  lastError = err;
896
917
  if (isLast) throw err;
@@ -914,7 +935,7 @@ var SPClient = class {
914
935
  throw new SPReceiptError(message, res.status, respBody);
915
936
  }
916
937
  return {
917
- receipt: respBody.receipt,
938
+ receipt: respBody.ticket,
918
939
  idempotent: respBody.idempotent === true
919
940
  };
920
941
  }
@@ -989,7 +1010,7 @@ var SPClient = class {
989
1010
  };
990
1011
 
991
1012
  // src/lib/attestation-cache.ts
992
- import { decodeAttestationBlob } from "@hap/core";
1013
+ import { decodeMandateBlob } from "@hap/core";
993
1014
  var AsKeyMismatchError = class extends Error {
994
1015
  constructor(message) {
995
1016
  super(message);
@@ -1018,6 +1039,19 @@ var AttestationCache = class {
1018
1039
  /** Cache of authorizations by path (e.g., "payment-routine") */
1019
1040
  authorizations = /* @__PURE__ */ new Map();
1020
1041
  lastSync = 0;
1042
+ /**
1043
+ * Re-approval UX (V9/item 9) — authorization ids the AS has told us, via a
1044
+ * VERSION_UNSUPPORTED ticket refusal, carry only pre-0.7 mandate blobs.
1045
+ * Learned lazily (only once an execution is actually attempted under
1046
+ * them — nothing proactively re-verifies every cached blob's version),
1047
+ * never cleared explicitly: re-approving replaces the authorization with
1048
+ * a NEW id (the existing edit/replace ceremony), so the flagged id
1049
+ * simply drops out of the active set on its own once revoked. In-memory
1050
+ * only, same lifetime as the cache it annotates — a restart re-learns it
1051
+ * on the next attempted execution, which is the gateway's only source for
1052
+ * this fact to begin with.
1053
+ */
1054
+ reapprovalNeeded = /* @__PURE__ */ new Set();
1021
1055
  /**
1022
1056
  * Get the SP public key, fetching from SP if not cached or expired, then —
1023
1057
  * when a pin exists for this exact AS URL — verifying the live key still
@@ -1044,6 +1078,21 @@ var AttestationCache = class {
1044
1078
  }
1045
1079
  return this.spPublicKey;
1046
1080
  }
1081
+ /**
1082
+ * The pinned Authority Server, as a `did:key` — what hap-core 0.12's
1083
+ * `verifyMandateSignature` / `verifyTicketSignature` take as
1084
+ * `trustedIssuers` (they resolve the verification key from the artifact's
1085
+ * OWN `issuer` DID, never from a key supplied alongside it — protocol.md ->
1086
+ * *Ticket Verification* step 1). This derives the same did:key from the
1087
+ * same pinned hex key {@link getPublicKey} already enforces, so a mandate
1088
+ * or ticket whose signature verifies under a DIFFERENT key — even one that
1089
+ * encodes a valid did:key of its own — still fails: the issuer named on the
1090
+ * artifact must BE the pinned key, not merely resolve to some key.
1091
+ */
1092
+ async getTrustedIssuer() {
1093
+ const hex = await this.getPublicKey();
1094
+ return issuerFromPublicKeyHex(hex);
1095
+ }
1047
1096
  /**
1048
1097
  * Get a cached authorization by path. If not cached, returns null.
1049
1098
  * Use syncAuthorization() to fetch from SP.
@@ -1080,7 +1129,7 @@ var AttestationCache = class {
1080
1129
  const firstBlob = result.attestations[0]?.blob;
1081
1130
  if (firstBlob) {
1082
1131
  try {
1083
- const payload = decodeAttestationBlob(firstBlob).payload;
1132
+ const payload = decodeMandateBlob(firstBlob).payload;
1084
1133
  signedCommitmentMode = payload.commitment_mode;
1085
1134
  subjects = payload.subjects;
1086
1135
  } catch {
@@ -1091,7 +1140,8 @@ var AttestationCache = class {
1091
1140
  authorizationId: result.authorization_id,
1092
1141
  boundsHash: result.bounds_hash,
1093
1142
  // content fingerprint (undefined for pre-v0.4 records)
1094
- contextHash: result.context_hash,
1143
+ contextHash: result.scope_hash,
1144
+ // v0.7: wire field renamed context_hash -> scope_hash
1095
1145
  profileId: result.profile_id,
1096
1146
  path: result.profile_id,
1097
1147
  frame: bounds,
@@ -1148,6 +1198,15 @@ var AttestationCache = class {
1148
1198
  invalidate(path) {
1149
1199
  this.authorizations.delete(path);
1150
1200
  }
1201
+ /** Flag an authorization as needing re-approval (V9/item 9) — see
1202
+ * {@link reapprovalNeeded}'s doc comment. */
1203
+ markNeedsReapproval(authorizationId) {
1204
+ this.reapprovalNeeded.add(authorizationId);
1205
+ }
1206
+ /** Whether this authorization was flagged by {@link markNeedsReapproval}. */
1207
+ needsReapproval(authorizationId) {
1208
+ return this.reapprovalNeeded.has(authorizationId);
1209
+ }
1151
1210
  };
1152
1211
 
1153
1212
  // src/lib/gate-store.ts
@@ -1640,6 +1699,12 @@ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync a
1640
1699
  import { homedir as homedir4 } from "os";
1641
1700
  import { dirname as dirname5, join as join7 } from "path";
1642
1701
  import { createCipheriv as createCipheriv4, createDecipheriv as createDecipheriv4, randomBytes as randomBytes4 } from "crypto";
1702
+ var ArchiveWriteError = class extends Error {
1703
+ constructor(message) {
1704
+ super(message);
1705
+ this.name = "ArchiveWriteError";
1706
+ }
1707
+ };
1643
1708
  var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join7(homedir4(), ".suveren");
1644
1709
  function stripPreviews(proposal) {
1645
1710
  const args = proposal.toolArgs;
@@ -2030,7 +2095,7 @@ var MCPGatekeeper = class {
2030
2095
  result: {
2031
2096
  approved: false,
2032
2097
  errors: [{
2033
- code: "DOMAIN_NOT_COVERED",
2098
+ code: "MANDATE_NOT_FOUND",
2034
2099
  message: `No active authorization for "${authorizationPath}". A decision owner must grant authority via the Authority UI.`
2035
2100
  }]
2036
2101
  },
@@ -2043,16 +2108,16 @@ var MCPGatekeeper = class {
2043
2108
  result: {
2044
2109
  approved: false,
2045
2110
  errors: [{
2046
- code: "DOMAIN_NOT_COVERED",
2111
+ code: "COVERAGE_INSUFFICIENT",
2047
2112
  message: `Authorization "${authorizationPath}" is pending. Missing domains: ${missing.join(", ")}`
2048
2113
  }]
2049
2114
  },
2050
2115
  authorization: auth
2051
2116
  };
2052
2117
  }
2053
- let publicKeyHex;
2118
+ let trustedIssuer;
2054
2119
  try {
2055
- publicKeyHex = await this.cache.getPublicKey();
2120
+ trustedIssuer = await this.cache.getTrustedIssuer();
2056
2121
  } catch (err) {
2057
2122
  if (err instanceof AsKeyMismatchError) {
2058
2123
  void notifyControlPlane("as-key-mismatch");
@@ -2071,22 +2136,22 @@ var MCPGatekeeper = class {
2071
2136
  throw err;
2072
2137
  }
2073
2138
  const resolvedBounds = override?.bounds ?? auth.bounds ?? auth.frame;
2074
- const resolvedContext = override?.context ?? auth.context;
2075
- const frame = { ...resolvedBounds, profile: auth.profileId };
2139
+ const resolvedScope = override?.context ?? auth.context;
2140
+ const bounds = { ...resolvedBounds, profile: auth.profileId };
2076
2141
  const request = {
2077
- frame,
2078
- attestations: auth.attestations.map((a) => a.blob),
2142
+ bounds,
2143
+ mandates: auth.attestations.map((a) => a.blob),
2079
2144
  execution,
2080
- context: resolvedContext,
2145
+ scope: resolvedScope,
2081
2146
  // Identifies the grant whose bounds are being checked. hap-core reads it
2082
2147
  // only to scope a cumulative lookup, which no longer happens here (see
2083
2148
  // below); it is kept because it belongs to the request, not to the
2084
- // dropped check. It cannot go inside `frame`: that is validated against
2149
+ // dropped check. It cannot go inside `bounds`: that is validated against
2085
2150
  // the profile's boundsSchema, which declares no `path` field in any
2086
2151
  // shipped profile, so an extra key there fails with "Unknown field".
2087
2152
  path: auth.path
2088
2153
  };
2089
- const result = await verify(request, publicKeyHex);
2154
+ const result = await verify(request, { trustedIssuers: [trustedIssuer] });
2090
2155
  return { result, authorization: auth };
2091
2156
  }
2092
2157
  };
@@ -2551,6 +2616,7 @@ var ReportStore = class {
2551
2616
  };
2552
2617
 
2553
2618
  // src/lib/shared-state.ts
2619
+ import { PROTOCOL_VERSION } from "@hap/core";
2554
2620
  var SharedState = class {
2555
2621
  spClient;
2556
2622
  cache;
@@ -2567,6 +2633,21 @@ var SharedState = class {
2567
2633
  * see ticket-verify.ts / commitments.ts. */
2568
2634
  proposalSubmissions;
2569
2635
  gatekeeper;
2636
+ /**
2637
+ * V7 — set by {@link checkAsCompat} (called once at process start, and
2638
+ * again whenever the AS URL this process pairs with changes — which, in
2639
+ * practice, only happens across a restart; see bin/http.ts's
2640
+ * `repairIfAsUrlChanged`). Non-null means the AS this gateway is paired
2641
+ * with does not list {@link PROTOCOL_VERSION} among its
2642
+ * `supportedVersions` — every gated tool call MUST refuse with this
2643
+ * message rather than reach the AS at all (checked in tool-proxy.ts,
2644
+ * before anything else). `null` until the first check completes, which
2645
+ * reads the SAME as "compatible" — fail-closed behaviour starts only
2646
+ * once a check has actually run and failed, not merely "has not run
2647
+ * yet" (the compat endpoint is best-effort: an AS that is briefly
2648
+ * unreachable at boot must not permanently brick the gateway).
2649
+ */
2650
+ asVersionRefusal = null;
2570
2651
  /**
2571
2652
  * @param dataDir Passed to the AttestationCache so it can enforce AS key
2572
2653
  * pinning (as-pairing.ts), and to the SPClient so it can enforce opt-in
@@ -2586,22 +2667,61 @@ var SharedState = class {
2586
2667
  this.proposalSubmissions = new ProposalSubmissionStore(gateStorePath);
2587
2668
  this.gatekeeper = new MCPGatekeeper(this.cache);
2588
2669
  }
2670
+ /**
2671
+ * V7 — call once at startup (and again if the AS URL this process pairs
2672
+ * with changes). Fetches GET /api/as/compat and sets
2673
+ * {@link asVersionRefusal} when the AS does not support this package's
2674
+ * protocol version. A network failure (AS unreachable at boot) does NOT
2675
+ * set a refusal — that is a connectivity problem every gated call
2676
+ * already surfaces on its own merits (SPReceiptError etc.); this check
2677
+ * exists to catch the specific, otherwise-silent-until-first-tool-call
2678
+ * case of a genuinely incompatible AS, not to duplicate "AS is down".
2679
+ */
2680
+ async checkAsCompat() {
2681
+ let compat;
2682
+ try {
2683
+ compat = await this.spClient.getCompat();
2684
+ } catch {
2685
+ return;
2686
+ }
2687
+ const supported = Array.isArray(compat.supportedVersions) ? compat.supportedVersions : [];
2688
+ if (!supported.includes(PROTOCOL_VERSION)) {
2689
+ this.asVersionRefusal = supported.length > 0 ? `This Authority Server supports protocol version(s) [${supported.join(", ")}], not ${PROTOCOL_VERSION}. Update the gateway or the Authority Server so the two agree, then restart. No gated action can run until then.` : `This Authority Server's /api/as/compat does not name a supportedVersions list (it answered ${JSON.stringify(compat.protocolVersion ?? "unknown")}), so it cannot be confirmed to support protocol ${PROTOCOL_VERSION}. Update the gateway or the Authority Server, then restart. No gated action can run until then.`;
2690
+ console.error(`[Suveren MCP] ${this.asVersionRefusal}`);
2691
+ } else {
2692
+ this.asVersionRefusal = null;
2693
+ }
2694
+ }
2589
2695
  /**
2590
2696
  * Archive a signed receipt into the local receipt archive — the subject's
2591
2697
  * own durable copy of the evidence (see receipt-archive.ts).
2592
2698
  *
2593
- * Best-effort by contract: the AS has already issued the receipt and holds
2594
- * the authoritative copy, so an archive failure logs loudly but MUST NOT
2595
- * block the execution it documents.
2699
+ * FAIL-CLOSED (V8, reversed from the original "best-effort" contract):
2700
+ * callers MUST call this BEFORE executing the downstream tool, and MUST
2701
+ * NOT execute it if this throws. The AS's copy of the TICKET is
2702
+ * authoritative, but this archive is the subject's ONLY copy of the
2703
+ * intent/context plaintext the ticket's hashes commit to, and of the
2704
+ * issuer key needed to verify it offline — an execution that ran with no
2705
+ * way to produce that evidence later is worse than one that was refused
2706
+ * up front.
2707
+ *
2708
+ * @throws AsKeyMismatchError when the pinned key is unavailable or
2709
+ * mismatched (propagated as-is — callers already react to this
2710
+ * specifically: refuse + lock, not a generic archive failure).
2711
+ * @throws ArchiveWriteError for every other failure to produce a complete,
2712
+ * verifiable entry (no public key resolved, or the write itself failed).
2596
2713
  */
2597
2714
  async archiveReceipt(receipt, opts) {
2715
+ let asPublicKey;
2716
+ try {
2717
+ asPublicKey = await this.cache.getPublicKey();
2718
+ } catch (err) {
2719
+ if (err instanceof AsKeyMismatchError) throw err;
2720
+ throw new ArchiveWriteError(
2721
+ `Could not resolve the Authority Server's public key to archive this receipt against: ${err instanceof Error ? err.message : String(err)}`
2722
+ );
2723
+ }
2598
2724
  try {
2599
- let asPublicKey;
2600
- try {
2601
- asPublicKey = await this.cache.getPublicKey();
2602
- } catch (err) {
2603
- if (err instanceof AsKeyMismatchError) throw err;
2604
- }
2605
2725
  this.receiptArchive.record({
2606
2726
  receipt,
2607
2727
  authorizationId: opts.authorizationId,
@@ -2622,11 +2742,7 @@ var SharedState = class {
2622
2742
  } : void 0
2623
2743
  });
2624
2744
  } catch (err) {
2625
- if (err instanceof AsKeyMismatchError) throw err;
2626
- console.error(
2627
- "[Suveren MCP] Receipt archive write failed (execution proceeds \u2014 AS retains authoritative copy):",
2628
- err
2629
- );
2745
+ throw new ArchiveWriteError(`Receipt archive write failed: ${err instanceof Error ? err.message : String(err)}`);
2630
2746
  }
2631
2747
  }
2632
2748
  setGateContent(path, authorizationId, profileId, content, opts) {
@@ -2657,7 +2773,8 @@ var SharedState = class {
2657
2773
  ...auth,
2658
2774
  gateContent: gateEntry?.gateContent ?? null,
2659
2775
  context: auth.context ?? gateEntry?.context,
2660
- contextHash: auth.contextHash ?? gateEntry?.contextHash
2776
+ contextHash: auth.contextHash ?? gateEntry?.contextHash,
2777
+ needsReapproval: this.cache.needsReapproval(auth.authorizationId)
2661
2778
  };
2662
2779
  }).filter((auth) => auth.gateContent !== null);
2663
2780
  }
@@ -2848,7 +2965,7 @@ function shouldAttachFooter() {
2848
2965
  return true;
2849
2966
  }
2850
2967
  function receiptUrl(receiptId) {
2851
- return `${AS_BASE}/r/${receiptId}`;
2968
+ return `${AS_BASE}/t/${receiptId}`;
2852
2969
  }
2853
2970
  function verbForProfile(profile) {
2854
2971
  return profile === "publish" ? "Published" : "Sent";
@@ -2938,14 +3055,14 @@ function computeContentBinding(profileId, tool, toolArgs, actionType) {
2938
3055
  boundContent
2939
3056
  };
2940
3057
  }
2941
- function attachReceiptId(tool, args, receiptId) {
3058
+ function attachTicketId(tool, args, ticketId) {
2942
3059
  const schema = tool.inputSchema;
2943
- if (!schema?.properties || !("receipt_id" in schema.properties)) return args;
2944
- return { ...args, receipt_id: receiptId };
3060
+ if (!schema?.properties || !("ticket_id" in schema.properties)) return args;
3061
+ return { ...args, ticket_id: ticketId };
2945
3062
  }
2946
3063
 
2947
3064
  // src/lib/ticket-verify.ts
2948
- import { verifyReceiptSignature } from "@hap/core";
3065
+ import { verifyTicketSignature } from "@hap/core";
2949
3066
  var TicketBindingMismatchError = class extends Error {
2950
3067
  constructor(message) {
2951
3068
  super(message);
@@ -2955,12 +3072,12 @@ var TicketBindingMismatchError = class extends Error {
2955
3072
  var TICKET_MAX_AGE_SECONDS = 5 * 60;
2956
3073
  var TICKET_MAX_CLOCK_SKEW_SECONDS = 30;
2957
3074
  async function verifyTicket(cache, receipt, expected) {
2958
- const publicKeyHex = await cache.getPublicKey();
3075
+ const trustedIssuer = await cache.getTrustedIssuer();
2959
3076
  if (typeof receipt.signature !== "string" || !receipt.signature) {
2960
3077
  throw new AsKeyMismatchError("Ticket carries no signature \u2014 refusing to trust it.");
2961
3078
  }
2962
3079
  try {
2963
- await verifyReceiptSignature(receipt, publicKeyHex);
3080
+ await verifyTicketSignature(receipt, { trustedIssuers: [trustedIssuer] });
2964
3081
  } catch (err) {
2965
3082
  throw new AsKeyMismatchError(
2966
3083
  `Ticket signature does not verify against the pinned Authority Server key \u2014 ${err instanceof Error ? err.message : String(err)}. Refusing to execute.`
@@ -3380,6 +3497,24 @@ function applyMapping(m, value, execution) {
3380
3497
  function profileMatches(profileId, shortName) {
3381
3498
  return profileId === shortName || profileId.includes("/" + shortName + "@") || profileId.endsWith("/" + shortName);
3382
3499
  }
3500
+ function wireExecutionContext(profileId, execution) {
3501
+ const profile = getProfile2(profileId);
3502
+ const allowed = /* @__PURE__ */ new Set(["action_type"]);
3503
+ const fields = profile?.boundsSchema?.fields;
3504
+ if (fields) {
3505
+ for (const fieldDef of Object.values(fields)) {
3506
+ const bt = fieldDef.boundType;
3507
+ if (bt && (bt.kind === "per_transaction" || bt.kind === "cumulative_sum") && bt.of) {
3508
+ allowed.add(bt.of);
3509
+ }
3510
+ }
3511
+ }
3512
+ const out = {};
3513
+ for (const [key, value] of Object.entries(execution)) {
3514
+ if (allowed.has(key)) out[key] = value;
3515
+ }
3516
+ return out;
3517
+ }
3383
3518
  function countBoundsFor(profileId, actionType) {
3384
3519
  const fields = getProfile2(profileId)?.boundsSchema?.fields;
3385
3520
  if (!fields) return null;
@@ -3471,14 +3606,18 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
3471
3606
  function isStaleMandateRefusal(err) {
3472
3607
  const errors = err.body?.errors;
3473
3608
  const code = errors?.[0]?.code;
3474
- if (err.statusCode === 403 && (code === "ATTESTATION_REVOKED" || code === "ATTESTATION_EXPIRED")) {
3609
+ if (err.statusCode === 403 && (code === "MANDATE_REVOKED" || code === "MANDATE_EXPIRED")) {
3475
3610
  return true;
3476
3611
  }
3477
- if (err.statusCode === 404 && code === "ATTESTATION_NOT_FOUND") {
3612
+ if (err.statusCode === 404 && code === "MANDATE_NOT_FOUND") {
3478
3613
  return true;
3479
3614
  }
3480
3615
  return false;
3481
3616
  }
3617
+ function isVersionUnsupportedRefusal(err) {
3618
+ const errors = err.body?.errors;
3619
+ return err.statusCode === 409 && errors?.[0]?.code === "VERSION_UNSUPPORTED";
3620
+ }
3482
3621
  function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3483
3622
  if (!tool.gating || !tool.gating.profile) {
3484
3623
  return async () => {
@@ -3518,6 +3657,9 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3518
3657
  isError: true
3519
3658
  };
3520
3659
  }
3660
+ if (state2.asVersionRefusal) {
3661
+ return { content: [{ type: "text", text: `Refused: ${state2.asVersionRefusal}` }], isError: true };
3662
+ }
3521
3663
  return inner(args);
3522
3664
  };
3523
3665
  if (category === "disabled") {
@@ -3760,7 +3902,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3760
3902
  isError: true
3761
3903
  };
3762
3904
  }
3763
- const contextKeys = getProfile2(passers[0].profileId)?.contextSchema?.keyOrder ?? [];
3905
+ const contextKeys = getProfile2(passers[0].profileId)?.scopeSchema?.keyOrder ?? [];
3764
3906
  return attemptWithCandidates(passers);
3765
3907
  async function attemptWithCandidates(candidates) {
3766
3908
  const selection = selectAuthorization(
@@ -3791,6 +3933,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3791
3933
  if ((auth.deferredCommitmentDomains ?? []).length > 0) {
3792
3934
  try {
3793
3935
  const enrichedArgs = await attachImagePreview(args);
3936
+ const wireExecCtx = wireExecutionContext(auth.profileId, execution);
3794
3937
  const { proposal } = await state2.spClient.submitProposal({
3795
3938
  authorizationId: authzId,
3796
3939
  profileId: auth.profileId,
@@ -3798,13 +3941,13 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3798
3941
  pendingDomains: auth.deferredCommitmentDomains,
3799
3942
  tool: tool.namespacedName,
3800
3943
  toolArgs: enrichedArgs,
3801
- executionContext: { ...execution }
3944
+ executionContext: wireExecCtx
3802
3945
  });
3803
3946
  state2.proposalSubmissions.record({
3804
3947
  proposalId: proposal.id,
3805
3948
  tool: tool.namespacedName,
3806
3949
  toolArgs: enrichedArgs,
3807
- executionContext: { ...execution },
3950
+ executionContext: wireExecCtx,
3808
3951
  authorizationId: authzId,
3809
3952
  profileId: auth.profileId
3810
3953
  });
@@ -3833,6 +3976,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
3833
3976
  const actionType = declaredActionType;
3834
3977
  const binding = computeContentBinding(auth.profileId, tool, args, actionType);
3835
3978
  const idempotencyKey = randomUUID();
3979
+ const wireExecCtx = wireExecutionContext(auth.profileId, execution);
3836
3980
  const { receipt } = await state2.spClient.postReceipt({
3837
3981
  authorizationId: authzId,
3838
3982
  // Optional cross-check — the AS fails closed on a mismatch.
@@ -3840,7 +3984,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
3840
3984
  profileId: auth.profileId,
3841
3985
  action: tool.namespacedName,
3842
3986
  actionType,
3843
- executionContext: { ...execution },
3987
+ executionContext: wireExecCtx,
3844
3988
  amount: typeof execution.amount === "number" ? execution.amount : void 0,
3845
3989
  idempotencyKey,
3846
3990
  // Privacy: send the hash and how to reproduce it — never the
@@ -3853,7 +3997,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
3853
3997
  receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
3854
3998
  await verifyTicket(state2.cache, receipt, {
3855
3999
  action: tool.namespacedName,
3856
- executionContext: { ...execution },
4000
+ executionContext: wireExecCtx,
3857
4001
  authorizationId: authzId,
3858
4002
  profileId: auth.profileId,
3859
4003
  idempotencyKey,
@@ -3881,6 +4025,12 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
3881
4025
  isError: true
3882
4026
  };
3883
4027
  }
4028
+ if (err instanceof ArchiveWriteError) {
4029
+ return {
4030
+ content: [{ type: "text", text: `Blocked: the ticket could not be archived locally \u2014 ${err.message}. The action was not executed.` }],
4031
+ isError: true
4032
+ };
4033
+ }
3884
4034
  if (err instanceof ContentBindingError) {
3885
4035
  return {
3886
4036
  content: [{
@@ -3890,6 +4040,23 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
3890
4040
  isError: true
3891
4041
  };
3892
4042
  }
4043
+ if (err instanceof SPReceiptError && isVersionUnsupportedRefusal(err)) {
4044
+ state2.cache.markNeedsReapproval(auth.authorizationId);
4045
+ const remaining = candidates.filter((c) => c.authorizationId !== auth.authorizationId);
4046
+ if (remaining.length === 0) {
4047
+ return {
4048
+ content: [{
4049
+ type: "text",
4050
+ text: `Blocked: mandate ${auth.authorizationId} needs re-approval \u2014 the Authority Server no longer verifies its protocol version. Ask the decision owner to re-approve it (see list-authorizations). ${err.message}`
4051
+ }],
4052
+ isError: true
4053
+ };
4054
+ }
4055
+ console.error(
4056
+ `[Suveren MCP] fallback(${tool.namespacedName}): mandate ${auth.authorizationId} needs re-approval (VERSION_UNSUPPORTED) \u2192 retrying with [${remaining.map((c) => c.authorizationId).join(", ")}]`
4057
+ );
4058
+ return attemptWithCandidates(remaining);
4059
+ }
3893
4060
  if (err instanceof SPReceiptError && isStaleMandateRefusal(err)) {
3894
4061
  state2.cache.invalidate(auth.authorizationId);
3895
4062
  const code = err.body?.errors?.[0]?.code ?? "unknown";
@@ -3934,6 +4101,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
3934
4101
  const uniqueApprovers = [...new Set(pendingApprovers)];
3935
4102
  try {
3936
4103
  const enrichedArgs = await attachImagePreview(args);
4104
+ const wireExecCtx = wireExecutionContext(auth.profileId, execution);
3937
4105
  const { proposal } = await state2.spClient.submitProposal({
3938
4106
  authorizationId: authzId,
3939
4107
  profileId: auth.profileId,
@@ -3941,14 +4109,14 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
3941
4109
  pendingDomains: [],
3942
4110
  tool: tool.namespacedName,
3943
4111
  toolArgs: enrichedArgs,
3944
- executionContext: { ...execution },
4112
+ executionContext: wireExecCtx,
3945
4113
  pendingApprovers: uniqueApprovers
3946
4114
  });
3947
4115
  state2.proposalSubmissions.record({
3948
4116
  proposalId: proposal.id,
3949
4117
  tool: tool.namespacedName,
3950
4118
  toolArgs: enrichedArgs,
3951
- executionContext: { ...execution },
4119
+ executionContext: wireExecCtx,
3952
4120
  authorizationId: authzId,
3953
4121
  profileId: auth.profileId
3954
4122
  });
@@ -3979,7 +4147,8 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
3979
4147
  };
3980
4148
  }
3981
4149
  if (err instanceof SPReceiptError && err.statusCode === 403) {
3982
- if (/revoked/i.test(err.message)) {
4150
+ const errors403 = err.body?.errors;
4151
+ if (errors403?.[0]?.code === "MANDATE_REVOKED") {
3983
4152
  state2.cache.invalidate(auth.authorizationId);
3984
4153
  }
3985
4154
  return {
@@ -4005,7 +4174,7 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
4005
4174
  timestamp: Math.floor(Date.now() / 1e3)
4006
4175
  });
4007
4176
  let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
4008
- if (receiptId) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId);
4177
+ if (receiptId) outgoingArgs = attachTicketId(tool, outgoingArgs, receiptId);
4009
4178
  outgoingArgs = encodeOutgoingArgs(tool, outgoingArgs);
4010
4179
  if (receiptId) {
4011
4180
  const begun = state2.executionJournal.begin({
@@ -4118,6 +4287,12 @@ function buildMandateBrief(opts) {
4118
4287
  "You have bounded authorities granted by human decision owners.",
4119
4288
  "You MUST stay within these bounds \u2014 the Gatekeeper will reject actions that exceed them."
4120
4289
  ];
4290
+ if (opts.asVersionRefusal) {
4291
+ lines.push("");
4292
+ lines.push("=== AUTHORITY SERVER INCOMPATIBLE \u2014 NO GATED ACTION WILL RUN ===");
4293
+ lines.push("");
4294
+ lines.push(opts.asVersionRefusal);
4295
+ }
4121
4296
  const { brief: contextBrief } = getContextForBrief(contextDir);
4122
4297
  if (contextBrief) {
4123
4298
  lines.push("");
@@ -4128,6 +4303,18 @@ function buildMandateBrief(opts) {
4128
4303
  const active = authorizations.filter((a) => a.complete);
4129
4304
  const pending = authorizations.filter((a) => !a.complete);
4130
4305
  const now = Math.floor(Date.now() / 1e3);
4306
+ const needingReapproval = active.filter((a) => a.needsReapproval);
4307
+ if (needingReapproval.length > 0) {
4308
+ lines.push("");
4309
+ lines.push("=== NEEDS RE-APPROVAL ===");
4310
+ lines.push("");
4311
+ for (const auth of needingReapproval) {
4312
+ lines.push(
4313
+ `[${shortProfileName(auth.profileId)}] ${auth.authorizationId}: the Authority Server no longer verifies this mandate's protocol version. Ask the decision owner to re-approve it.`
4314
+ );
4315
+ }
4316
+ lines.push("");
4317
+ }
4131
4318
  if (active.length > 0) {
4132
4319
  lines.push("");
4133
4320
  lines.push("=== ACTIVE AUTHORITIES ===");
@@ -4156,7 +4343,7 @@ function buildMandateBrief(opts) {
4156
4343
  }
4157
4344
  }
4158
4345
  if (pending.length > 0) {
4159
- lines.push("=== PENDING (awaiting attestations) ===");
4346
+ lines.push("=== PENDING (awaiting mandates) ===");
4160
4347
  lines.push("");
4161
4348
  for (const auth of pending) {
4162
4349
  const missing = auth.requiredDomains.filter((d) => !auth.attestedDomains.includes(d));
@@ -4186,8 +4373,8 @@ function shortProfileName2(profileId) {
4186
4373
  const parts = withoutVersion.split("/");
4187
4374
  return parts[parts.length - 1];
4188
4375
  }
4189
- function buildCapabilityMap(profileId, toolGating, integrationManager2) {
4190
- if (!integrationManager2 || !toolGating) return "";
4376
+ function buildCapabilityMap(profileId, integrationManager2) {
4377
+ if (!integrationManager2) return "";
4191
4378
  const allTools = integrationManager2.getAllTools();
4192
4379
  const shortName = shortProfileName2(profileId);
4193
4380
  const gated = [];
@@ -4200,21 +4387,20 @@ function buildCapabilityMap(profileId, toolGating, integrationManager2) {
4200
4387
  if (!profileMatches(tool.gating.profile, shortName) && tool.gating.profile !== profileId) {
4201
4388
  continue;
4202
4389
  }
4203
- const overrides = toolGating.overrides ?? {};
4204
- const override = overrides[tool.originalName];
4205
- if (override === null) {
4390
+ const gating = tool.gating;
4391
+ if (gating.category === "read") {
4206
4392
  readOnly.push(tool.originalName);
4207
- } else if (override !== void 0) {
4208
- const mappingDesc = Object.entries(override.executionMapping ?? {}).map(([arg, mapping]) => {
4393
+ } else if (gating.category === "disabled") {
4394
+ undescribed.push(tool.originalName);
4395
+ } else {
4396
+ const mappingDesc = Object.entries(gating.executionMapping ?? {}).map(([arg, mapping]) => {
4209
4397
  if (typeof mapping === "string") return `${mapping} from ${arg}`;
4210
4398
  if (Array.isArray(mapping)) return `${mapping.map((m) => m.field).join("+")} from ${arg}`;
4211
4399
  if ("divisor" in mapping) return `${mapping.field} from ${arg} (/${mapping.divisor})`;
4212
4400
  return `${mapping.field} from ${arg}`;
4213
4401
  }).join(", ");
4214
- const actionType = override.staticExecution?.action_type ?? "unknown";
4402
+ const actionType = gating.staticExecution?.action_type ?? "unknown";
4215
4403
  gated.push(` - ${tool.originalName}: ${actionType}${mappingDesc ? `, ${mappingDesc}` : ""}`);
4216
- } else {
4217
- undescribed.push(tool.originalName);
4218
4404
  }
4219
4405
  }
4220
4406
  const lines = [];
@@ -4270,8 +4456,11 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4270
4456
  const earliestExpiry = Math.min(...auth.attestations.map((a) => a.expiresAt));
4271
4457
  const remainingMin = Math.max(0, Math.round((earliestExpiry - now) / 60));
4272
4458
  const boundsDesc = Object.entries(auth.frame).filter(([key]) => key !== "profile" && key !== "path").map(([key, value]) => `${key}: ${value}`).join(", ");
4273
- const statusLabel = auth.complete ? "" : " (PENDING)";
4459
+ const statusLabel = !auth.complete ? " (PENDING)" : auth.needsReapproval ? " (NEEDS RE-APPROVAL)" : "";
4274
4460
  output2.push(`[${auth.path}] ${auth.profileId} (${remainingMin} min remaining)${statusLabel}`);
4461
+ if (auth.needsReapproval) {
4462
+ output2.push(" The Authority Server no longer verifies this mandate's protocol version. Ask the decision owner to re-approve it.");
4463
+ }
4275
4464
  output2.push("");
4276
4465
  output2.push(` Bounds: ${boundsDesc}`);
4277
4466
  const reviewDomains = auth.deferredCommitmentDomains ?? [];
@@ -4311,11 +4500,11 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4311
4500
  if (!auth.complete) {
4312
4501
  const missing = auth.requiredDomains.filter((d) => !auth.attestedDomains.includes(d));
4313
4502
  output2.push("");
4314
- output2.push(` Missing attestations: ${missing.join(", ")}`);
4503
+ output2.push(` Missing mandates: ${missing.join(", ")}`);
4315
4504
  }
4316
4505
  if (profile && integrationManager2) {
4317
4506
  output2.push("");
4318
- output2.push(buildCapabilityMap(auth.profileId, profile.toolGating, integrationManager2));
4507
+ output2.push(buildCapabilityMap(auth.profileId, integrationManager2));
4319
4508
  }
4320
4509
  output2.push("");
4321
4510
  }
@@ -4334,8 +4523,12 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4334
4523
  const remainingMin = Math.max(0, Math.round((earliestExpiry - now) / 60));
4335
4524
  const boundsDesc = Object.entries(auth.frame).filter(([key]) => key !== "profile" && key !== "path").map(([key, value]) => `${key}: ${value}`).join(", ");
4336
4525
  if (auth.complete) {
4337
- const lines = [` [${auth.path}] ${auth.profileId} \u2014 ${remainingMin} min remaining`];
4526
+ const statusLabel = auth.needsReapproval ? " (NEEDS RE-APPROVAL)" : "";
4527
+ const lines = [` [${auth.path}] ${auth.profileId} \u2014 ${remainingMin} min remaining${statusLabel}`];
4338
4528
  lines.push(` Bounds: ${boundsDesc}`);
4529
+ if (auth.needsReapproval) {
4530
+ lines.push(" The Authority Server no longer verifies this mandate's protocol version. Ask the decision owner to re-approve it.");
4531
+ }
4339
4532
  if ((auth.deferredCommitmentDomains ?? []).length > 0) {
4340
4533
  lines.push(" Mode: review (each action requires your approval)");
4341
4534
  }
@@ -4353,7 +4546,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4353
4546
  } else {
4354
4547
  const missing = auth.requiredDomains.filter((d) => !auth.attestedDomains.includes(d));
4355
4548
  pending.push(
4356
- ` ${auth.path}: ${boundsDesc} \u2014 needs ${missing.join(", ")} attestation, ${remainingMin} min remaining`
4549
+ ` ${auth.path}: ${boundsDesc} \u2014 needs ${missing.join(", ")} mandate, ${remainingMin} min remaining`
4357
4550
  );
4358
4551
  }
4359
4552
  }
@@ -4391,11 +4584,11 @@ function checkPendingHandler(cache) {
4391
4584
  return {
4392
4585
  content: [{
4393
4586
  type: "text",
4394
- text: `No pending attestations for domain "${domain}".`
4587
+ text: `No pending mandates for domain "${domain}".`
4395
4588
  }]
4396
4589
  };
4397
4590
  }
4398
- const lines = [`Pending attestations needing your domain (${domain}):`];
4591
+ const lines = [`Pending mandates needing your domain (${domain}):`];
4399
4592
  for (const item of pending) {
4400
4593
  const boundsDesc = Object.entries(item.frame).filter(([key]) => key !== "profile" && key !== "path").map(([key, value]) => `${key}: ${value}`).join(", ");
4401
4594
  const attested = item.attested_domains.join(", ");
@@ -4416,7 +4609,7 @@ function checkPendingHandler(cache) {
4416
4609
  return {
4417
4610
  content: [{
4418
4611
  type: "text",
4419
- text: `Failed to check pending attestations: ${error}`
4612
+ text: `Failed to check pending mandates: ${error}`
4420
4613
  }],
4421
4614
  isError: true
4422
4615
  };
@@ -4590,6 +4783,12 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4590
4783
  isError: true
4591
4784
  };
4592
4785
  }
4786
+ if (err instanceof ArchiveWriteError) {
4787
+ return {
4788
+ text: `Proposal ${proposal.id}: blocked \u2014 the ticket could not be archived locally \u2014 ${err.message}. The action was not executed.`,
4789
+ isError: true
4790
+ };
4791
+ }
4593
4792
  if (err instanceof ContentBindingError2) {
4594
4793
  return {
4595
4794
  text: `Proposal ${proposal.id}: blocked \u2014 the approved content cannot be bound to the receipt. ${err.message}`,
@@ -4603,6 +4802,13 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4603
4802
  text: `Proposal ${proposal.id} has already been executed by another request.`
4604
4803
  };
4605
4804
  }
4805
+ if (err.statusCode === 409 && code === "VERSION_UNSUPPORTED") {
4806
+ state2.cache.markNeedsReapproval(proposal.authorizationId);
4807
+ return {
4808
+ text: `Proposal ${proposal.id}: blocked \u2014 mandate ${proposal.authorizationId} needs re-approval \u2014 the Authority Server no longer verifies its protocol version. Ask the decision owner to re-approve it (see list-authorizations). ${err.message}`,
4809
+ isError: true
4810
+ };
4811
+ }
4606
4812
  return {
4607
4813
  text: `Proposal ${proposal.id}: SP rejected receipt \u2014 ${err.message}`,
4608
4814
  isError: true
@@ -4649,7 +4855,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4649
4855
  if (shouldAttachFooter()) {
4650
4856
  outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId, cachedAuth?.subjects?.[0]);
4651
4857
  }
4652
- outgoingArgs = attachReceiptId(discovered, outgoingArgs, receiptId);
4858
+ outgoingArgs = attachTicketId(discovered, outgoingArgs, receiptId);
4653
4859
  outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
4654
4860
  }
4655
4861
  let result;
@@ -4858,7 +5064,8 @@ function createMcpServer(state2, integrationManager2) {
4858
5064
  const instructions = buildMandateBrief({
4859
5065
  authorizations: enriched,
4860
5066
  executionLog: state2.executionLog,
4861
- integrationManager: integrationManager2
5067
+ integrationManager: integrationManager2,
5068
+ asVersionRefusal: state2.asVersionRefusal
4862
5069
  });
4863
5070
  const server = new McpServer(
4864
5071
  { name: "suveren-gateway", version: "0.1.0" },
@@ -4875,9 +5082,9 @@ function createMcpServer(state2, integrationManager2) {
4875
5082
  listAuthorizationsHandler(state2, integrationManager2)
4876
5083
  );
4877
5084
  server.registerTool(
4878
- "check-pending-attestations",
5085
+ "check-pending-mandates",
4879
5086
  {
4880
- description: "Check if any attestations are waiting for your owner's approval.",
5087
+ description: "Check if any mandates are waiting for your owner's approval.",
4881
5088
  inputSchema: {
4882
5089
  domain: z2.string().describe(`The owner's domain (e.g., "compliance")`)
4883
5090
  }
@@ -5014,7 +5221,7 @@ var AgentContactStore = class {
5014
5221
 
5015
5222
  // src/lib/gate-content.ts
5016
5223
  import { computeIntentHash } from "@hap/core";
5017
- import { decodeAttestationBlob as decodeAttestationBlob2 } from "@hap/core";
5224
+ import { decodeMandateBlob as decodeMandateBlob2 } from "@hap/core";
5018
5225
  function hashGateContent(text) {
5019
5226
  return computeIntentHash(text);
5020
5227
  }
@@ -5023,7 +5230,7 @@ function verifyGateContentHashes(content, auth) {
5023
5230
  if (auth.attestations.length === 0) {
5024
5231
  return { valid: false, errors: ["No attestations available to verify against"] };
5025
5232
  }
5026
- const attestation = decodeAttestationBlob2(auth.attestations[0].blob);
5233
+ const attestation = decodeMandateBlob2(auth.attestations[0].blob);
5027
5234
  const expectedHashes = attestation.payload.gate_content_hashes;
5028
5235
  if (!expectedHashes?.intent) {
5029
5236
  return { valid: false, errors: ["Attestation does not contain intent hash"] };
@@ -5125,7 +5332,6 @@ import { existsSync as existsSync15, writeFileSync as writeFileSync12, mkdirSync
5125
5332
  import { execFile } from "child_process";
5126
5333
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5127
5334
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5128
- import { getProfile as getProfile5 } from "@hap/core";
5129
5335
 
5130
5336
  // src/lib/manifest-loader.ts
5131
5337
  import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
@@ -5735,7 +5941,7 @@ var IntegrationManager = class {
5735
5941
  await this.connectWithGuard(client, transport, config);
5736
5942
  console.error(`[IntegrationManager] Connected to ${config.id} (${config.command} ${config.args.join(" ")})`);
5737
5943
  const toolsResult = await client.listTools();
5738
- const profileGating = config.toolGating ?? (config.profile ? getProfile5(config.profile)?.toolGating ?? null : null);
5944
+ const profileGating = config.toolGating ?? null;
5739
5945
  const tools = (toolsResult.tools ?? []).map((tool) => {
5740
5946
  const gating = this.resolveToolGating(config.profile, profileGating, tool.name);
5741
5947
  return {
@@ -6240,11 +6446,11 @@ async function buildTicketDetails(archive, ticketIds) {
6240
6446
  }
6241
6447
 
6242
6448
  // src/lib/report/window.ts
6243
- import { getProfile as getProfile6 } from "@hap/core";
6449
+ import { getProfile as getProfile5 } from "@hap/core";
6244
6450
  var REPORT_AGE_FIELD = "read_age_days";
6245
6451
  var REPORTING_PROFILE = "reporting";
6246
6452
  var END_SKEW_SECONDS = 120;
6247
- var OLD_PROFILE_REFUSAL = "this reporting mandate is from an older profile version \u2014 create a new reporting mandate (reporting@0.2).";
6453
+ var OLD_PROFILE_REFUSAL = "this reporting mandate is from an older profile version \u2014 create a new reporting mandate.";
6248
6454
  var DAY = 86400;
6249
6455
  function boundsOf(a) {
6250
6456
  return a.bounds ?? a.frame;
@@ -6254,7 +6460,7 @@ function windowLabel(start, days, loadedAt) {
6254
6460
  if (loadedAt !== null && loadedAt === start) return `${since} (when the test data was loaded)`;
6255
6461
  return `${since} (the last ${days} ${days === 1 ? "day" : "days"})`;
6256
6462
  }
6257
- var defaultLookup = (id) => getProfile6(id);
6463
+ var defaultLookup = (id) => getProfile5(id);
6258
6464
  function reportingMandateRefusal(m, lookup = defaultLookup) {
6259
6465
  const schema = lookup(m.profileId)?.boundsSchema;
6260
6466
  const field = resolveAgeBoundField(schema, REPORT_AGE_FIELD);
@@ -6737,7 +6943,7 @@ function writeReportTool(deps) {
6737
6943
  type: "object",
6738
6944
  properties: {
6739
6945
  html: { type: "string", description: "The full report HTML \u2014 see the tool description for how to write it." },
6740
- receipt_id: { type: "string" }
6946
+ ticket_id: { type: "string" }
6741
6947
  },
6742
6948
  required: ["html"]
6743
6949
  },
@@ -6902,7 +7108,7 @@ function guideHeader(systems, inactive = []) {
6902
7108
  return lines.join("\n");
6903
7109
  }
6904
7110
  function scopeFieldsOf(profile) {
6905
- return profile.scopeSchema ?? profile.contextSchema;
7111
+ return profile.scopeSchema;
6906
7112
  }
6907
7113
  function fieldLine(name, f) {
6908
7114
  const kind = [];
@@ -6950,7 +7156,7 @@ function fillGenerated(body, sections) {
6950
7156
  }
6951
7157
 
6952
7158
  // src/lib/builtins/setup.ts
6953
- import { getProfile as getProfile7 } from "@hap/core";
7159
+ import { getProfile as getProfile6 } from "@hap/core";
6954
7160
  var DELEGATION_PROFILE = "delegation";
6955
7161
  var DELEGATION_INTENT_HINT = [
6956
7162
  "Why \u2014 With test data only, I want to find out which routine parts of my work an AI could take over, so people gain time \u2014 before anything touches real systems.",
@@ -7059,7 +7265,7 @@ Read each topic with get_guide(topic) before doing that step.`);
7059
7265
  return { ...builtinText(`Unknown topic "${topic}". Topics: ${guides.map((g) => g.topic).join(", ") || "none installed"}.`), isError: true };
7060
7266
  }
7061
7267
  const body = fillGenerated(guide.body.trim(), {
7062
- limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile: getProfile7(s.profile) }))),
7268
+ limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile: getProfile6(s.profile) }))),
7063
7269
  package: () => packageSection(
7064
7270
  deps.integrationManager.getAllTools().find((t) => !deps.integrationManager.isBuiltin(t.integrationId) && t.originalName === "load_simulation")
7065
7271
  )
@@ -7076,7 +7282,7 @@ ${body}`);
7076
7282
  type: "object",
7077
7283
  properties: {
7078
7284
  content: { type: "string", description: "The complete new agent brief, in markdown (at most 16 KB)." },
7079
- receipt_id: {
7285
+ ticket_id: {
7080
7286
  type: "string",
7081
7287
  description: "Authorization reference for this call, set by the governing gateway \u2014 agents do not set this."
7082
7288
  }
@@ -7108,7 +7314,7 @@ ${body}`);
7108
7314
  mode: { type: "string", enum: ["review", "automatic"], description: "review = each action needs approval; automatic = within the limits." },
7109
7315
  duration_hours: { type: "number", description: "How long the mandate is valid, in hours. Omit for the profile default." },
7110
7316
  title: { type: "string", description: "A short name for the mandate." },
7111
- receipt_id: {
7317
+ ticket_id: {
7112
7318
  type: "string",
7113
7319
  description: "Authorization reference for this call, set by the governing gateway \u2014 agents do not set this."
7114
7320
  }
@@ -7161,7 +7367,7 @@ function registerBuiltins(deps, factories = BUILTIN_FACTORIES) {
7161
7367
  }
7162
7368
 
7163
7369
  // bin/http.ts
7164
- import { decodeAttestationBlob as decodeAttestationBlob3 } from "@hap/core";
7370
+ import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
7165
7371
 
7166
7372
  // src/lib/committed-executor.ts
7167
7373
  import { existsSync as existsSync18, mkdirSync as mkdirSync13, readFileSync as readFileSync19, unlinkSync as unlinkSync6, writeFileSync as writeFileSync13 } from "fs";
@@ -7298,6 +7504,7 @@ var reportSources = {
7298
7504
  }
7299
7505
  writeMcpPairedAsUrl(dataDir, spUrl);
7300
7506
  })();
7507
+ void state.checkAsCompat();
7301
7508
  var spApiKey = process.env.SUVEREN_AS_API_KEY ?? "";
7302
7509
  if (spApiKey) {
7303
7510
  state.spClient.setApiKey(spApiKey);
@@ -7663,7 +7870,10 @@ app.get("/internal/authorizations", internalOnly, (_req, res) => {
7663
7870
  bounds: a.frame,
7664
7871
  context: a.context ?? {},
7665
7872
  intent: a.gateContent?.intent ?? null,
7666
- deferredCommitmentDomains: a.deferredCommitmentDomains ?? []
7873
+ deferredCommitmentDomains: a.deferredCommitmentDomains ?? [],
7874
+ // Item 9 (re-approval UX) — the AS told us, via a VERSION_UNSUPPORTED
7875
+ // ticket refusal, that this authorization's mandate blob is pre-0.7.
7876
+ needsReapproval: a.needsReapproval ?? false
7667
7877
  }));
7668
7878
  res.json({ authorizations });
7669
7879
  });
@@ -7688,7 +7898,7 @@ app.get("/internal/evidence", internalOnly, (_req, res) => {
7688
7898
  ...a,
7689
7899
  attestations: a.attestations.map((att) => {
7690
7900
  try {
7691
- return { ...att, decoded: decodeAttestationBlob3(att.blob) };
7901
+ return { ...att, decoded: decodeMandateBlob3(att.blob) };
7692
7902
  } catch {
7693
7903
  return att;
7694
7904
  }
@@ -7855,7 +8065,8 @@ app.get("/internal/brief", internalOnly, (_req, res) => {
7855
8065
  const brief = buildMandateBrief({
7856
8066
  authorizations: enriched,
7857
8067
  executionLog: state.executionLog,
7858
- integrationManager
8068
+ integrationManager,
8069
+ asVersionRefusal: state.asVersionRefusal
7859
8070
  });
7860
8071
  res.json({ brief });
7861
8072
  } catch (err) {