@suveren/gateway 0.19.4 → 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 (51) hide show
  1. package/content/guides/1-interview.md +6 -0
  2. package/content/guides/2-package.md +4 -1
  3. package/content/guides/3-mandates.md +3 -0
  4. package/content/guides/6-risks.md +2 -0
  5. package/content/integrations/crm.json +1 -1
  6. package/content/integrations/deploy-github.json +1 -1
  7. package/content/integrations/erp.json +1 -1
  8. package/content/integrations/mail.json +1 -1
  9. package/content/integrations/records.json +1 -1
  10. package/dist/control-plane/index.mjs +21 -13
  11. package/dist/mcp-server/{chunk-ZRRZFGRL.mjs → chunk-APIQIEUA.mjs} +39 -27
  12. package/dist/mcp-server/http.mjs +382 -97
  13. package/dist/mcp-server/report-verify-cli.mjs +1 -1
  14. package/dist/ui/assets/index-1gCMlbLl.js +116 -0
  15. package/dist/ui/assets/{index-RCFQUjaF.css → index-blnpvM2g.css} +1 -1
  16. package/dist/ui/index.html +2 -2
  17. package/node_modules/@hap/core/README.md +92 -29
  18. package/node_modules/@hap/core/dist/index.d.mts +614 -593
  19. package/node_modules/@hap/core/dist/index.d.ts +614 -593
  20. package/node_modules/@hap/core/dist/index.js +774 -749
  21. package/node_modules/@hap/core/dist/index.mjs +744 -724
  22. package/node_modules/@hap/core/package.json +1 -1
  23. package/node_modules/@hap/core/src/base64url.ts +39 -0
  24. package/node_modules/@hap/core/src/commitment-modes.ts +0 -21
  25. package/node_modules/@hap/core/src/errors.ts +76 -0
  26. package/node_modules/@hap/core/src/frame.ts +47 -178
  27. package/node_modules/@hap/core/src/gatekeeper.ts +219 -600
  28. package/node_modules/@hap/core/src/identity.ts +9 -12
  29. package/node_modules/@hap/core/src/index.ts +8 -4
  30. package/node_modules/@hap/core/src/mandate.ts +177 -166
  31. package/node_modules/@hap/core/src/owner-signature.ts +282 -0
  32. package/node_modules/@hap/core/src/profile.ts +234 -0
  33. package/node_modules/@hap/core/src/ticket.ts +148 -0
  34. package/node_modules/@hap/core/src/types.ts +191 -344
  35. package/node_modules/@hap/core/src/versions.ts +47 -0
  36. package/package.json +2 -2
  37. package/profiles/calendar/0.6.profile.json +191 -0
  38. package/profiles/charge/0.6.profile.json +211 -0
  39. package/profiles/customers/0.9.profile.json +182 -0
  40. package/profiles/delegation/0.3.profile.json +118 -0
  41. package/profiles/deploy/0.11.profile.json +177 -0
  42. package/profiles/email/0.8.profile.json +226 -0
  43. package/profiles/index.json +12 -1
  44. package/profiles/publish/0.6.profile.json +187 -0
  45. package/profiles/purchase/0.7.profile.json +235 -0
  46. package/profiles/records/0.6.profile.json +118 -0
  47. package/profiles/reporting/0.3.profile.json +98 -0
  48. package/profiles/sales/0.4.profile.json +291 -0
  49. package/dist/ui/assets/index-BseWI4iJ.js +0 -116
  50. package/node_modules/@hap/core/src/attestation.ts +0 -173
  51. 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({
@@ -4079,6 +4248,12 @@ function agentVisibleAuthorizations(authorizations, integrationManager2) {
4079
4248
  );
4080
4249
  }
4081
4250
 
4251
+ // src/lib/agent-rules.ts
4252
+ var AGENT_RULES = [
4253
+ "Never open or operate the Suveren Gateway's web page \u2014 not in a browser you control, not with computer use. Never ask for, read or type the person's API key. Never approve a proposal: approvals are the person's alone.",
4254
+ "Use only the Suveren tools. No terminal, HTTP or browser calls to the gateway's ports (for example `curl localhost:3500/mcp`)."
4255
+ ];
4256
+
4082
4257
  // src/lib/mandate-brief.ts
4083
4258
  function shortProfileName(profileId) {
4084
4259
  const withoutVersion = profileId.replace(/@.*$/, "");
@@ -4112,6 +4287,12 @@ function buildMandateBrief(opts) {
4112
4287
  "You have bounded authorities granted by human decision owners.",
4113
4288
  "You MUST stay within these bounds \u2014 the Gatekeeper will reject actions that exceed them."
4114
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
+ }
4115
4296
  const { brief: contextBrief } = getContextForBrief(contextDir);
4116
4297
  if (contextBrief) {
4117
4298
  lines.push("");
@@ -4122,6 +4303,18 @@ function buildMandateBrief(opts) {
4122
4303
  const active = authorizations.filter((a) => a.complete);
4123
4304
  const pending = authorizations.filter((a) => !a.complete);
4124
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
+ }
4125
4318
  if (active.length > 0) {
4126
4319
  lines.push("");
4127
4320
  lines.push("=== ACTIVE AUTHORITIES ===");
@@ -4150,7 +4343,7 @@ function buildMandateBrief(opts) {
4150
4343
  }
4151
4344
  }
4152
4345
  if (pending.length > 0) {
4153
- lines.push("=== PENDING (awaiting attestations) ===");
4346
+ lines.push("=== PENDING (awaiting mandates) ===");
4154
4347
  lines.push("");
4155
4348
  for (const auth of pending) {
4156
4349
  const missing = auth.requiredDomains.filter((d) => !auth.attestedDomains.includes(d));
@@ -4165,6 +4358,10 @@ function buildMandateBrief(opts) {
4165
4358
  lines.push(canSetUp ? "If the person asks how to start with Suveren or how to set up their work with it: call setup__get_guide first and follow its steps in order." : `If the person asks how to start with Suveren: they first create a "Delegation" mandate in the Suveren Gateway at ${uiUrl()}. With it you can read the setup guide and lead them through the setup.`);
4166
4359
  lines.push("");
4167
4360
  }
4361
+ lines.push("=== RULES ===");
4362
+ lines.push("");
4363
+ for (const r of AGENT_RULES) lines.push(`- ${r}`);
4364
+ lines.push("");
4168
4365
  lines.push("When you receive a task, call list-authorizations(domain) to load full details for the relevant domain.");
4169
4366
  return lines.join("\n");
4170
4367
  }
@@ -4176,8 +4373,8 @@ function shortProfileName2(profileId) {
4176
4373
  const parts = withoutVersion.split("/");
4177
4374
  return parts[parts.length - 1];
4178
4375
  }
4179
- function buildCapabilityMap(profileId, toolGating, integrationManager2) {
4180
- if (!integrationManager2 || !toolGating) return "";
4376
+ function buildCapabilityMap(profileId, integrationManager2) {
4377
+ if (!integrationManager2) return "";
4181
4378
  const allTools = integrationManager2.getAllTools();
4182
4379
  const shortName = shortProfileName2(profileId);
4183
4380
  const gated = [];
@@ -4190,21 +4387,20 @@ function buildCapabilityMap(profileId, toolGating, integrationManager2) {
4190
4387
  if (!profileMatches(tool.gating.profile, shortName) && tool.gating.profile !== profileId) {
4191
4388
  continue;
4192
4389
  }
4193
- const overrides = toolGating.overrides ?? {};
4194
- const override = overrides[tool.originalName];
4195
- if (override === null) {
4390
+ const gating = tool.gating;
4391
+ if (gating.category === "read") {
4196
4392
  readOnly.push(tool.originalName);
4197
- } else if (override !== void 0) {
4198
- 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]) => {
4199
4397
  if (typeof mapping === "string") return `${mapping} from ${arg}`;
4200
4398
  if (Array.isArray(mapping)) return `${mapping.map((m) => m.field).join("+")} from ${arg}`;
4201
4399
  if ("divisor" in mapping) return `${mapping.field} from ${arg} (/${mapping.divisor})`;
4202
4400
  return `${mapping.field} from ${arg}`;
4203
4401
  }).join(", ");
4204
- const actionType = override.staticExecution?.action_type ?? "unknown";
4402
+ const actionType = gating.staticExecution?.action_type ?? "unknown";
4205
4403
  gated.push(` - ${tool.originalName}: ${actionType}${mappingDesc ? `, ${mappingDesc}` : ""}`);
4206
- } else {
4207
- undescribed.push(tool.originalName);
4208
4404
  }
4209
4405
  }
4210
4406
  const lines = [];
@@ -4260,8 +4456,11 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4260
4456
  const earliestExpiry = Math.min(...auth.attestations.map((a) => a.expiresAt));
4261
4457
  const remainingMin = Math.max(0, Math.round((earliestExpiry - now) / 60));
4262
4458
  const boundsDesc = Object.entries(auth.frame).filter(([key]) => key !== "profile" && key !== "path").map(([key, value]) => `${key}: ${value}`).join(", ");
4263
- const statusLabel = auth.complete ? "" : " (PENDING)";
4459
+ const statusLabel = !auth.complete ? " (PENDING)" : auth.needsReapproval ? " (NEEDS RE-APPROVAL)" : "";
4264
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
+ }
4265
4464
  output2.push("");
4266
4465
  output2.push(` Bounds: ${boundsDesc}`);
4267
4466
  const reviewDomains = auth.deferredCommitmentDomains ?? [];
@@ -4301,11 +4500,11 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4301
4500
  if (!auth.complete) {
4302
4501
  const missing = auth.requiredDomains.filter((d) => !auth.attestedDomains.includes(d));
4303
4502
  output2.push("");
4304
- output2.push(` Missing attestations: ${missing.join(", ")}`);
4503
+ output2.push(` Missing mandates: ${missing.join(", ")}`);
4305
4504
  }
4306
4505
  if (profile && integrationManager2) {
4307
4506
  output2.push("");
4308
- output2.push(buildCapabilityMap(auth.profileId, profile.toolGating, integrationManager2));
4507
+ output2.push(buildCapabilityMap(auth.profileId, integrationManager2));
4309
4508
  }
4310
4509
  output2.push("");
4311
4510
  }
@@ -4324,8 +4523,12 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4324
4523
  const remainingMin = Math.max(0, Math.round((earliestExpiry - now) / 60));
4325
4524
  const boundsDesc = Object.entries(auth.frame).filter(([key]) => key !== "profile" && key !== "path").map(([key, value]) => `${key}: ${value}`).join(", ");
4326
4525
  if (auth.complete) {
4327
- 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}`];
4328
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
+ }
4329
4532
  if ((auth.deferredCommitmentDomains ?? []).length > 0) {
4330
4533
  lines.push(" Mode: review (each action requires your approval)");
4331
4534
  }
@@ -4343,7 +4546,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4343
4546
  } else {
4344
4547
  const missing = auth.requiredDomains.filter((d) => !auth.attestedDomains.includes(d));
4345
4548
  pending.push(
4346
- ` ${auth.path}: ${boundsDesc} \u2014 needs ${missing.join(", ")} attestation, ${remainingMin} min remaining`
4549
+ ` ${auth.path}: ${boundsDesc} \u2014 needs ${missing.join(", ")} mandate, ${remainingMin} min remaining`
4347
4550
  );
4348
4551
  }
4349
4552
  }
@@ -4381,11 +4584,11 @@ function checkPendingHandler(cache) {
4381
4584
  return {
4382
4585
  content: [{
4383
4586
  type: "text",
4384
- text: `No pending attestations for domain "${domain}".`
4587
+ text: `No pending mandates for domain "${domain}".`
4385
4588
  }]
4386
4589
  };
4387
4590
  }
4388
- const lines = [`Pending attestations needing your domain (${domain}):`];
4591
+ const lines = [`Pending mandates needing your domain (${domain}):`];
4389
4592
  for (const item of pending) {
4390
4593
  const boundsDesc = Object.entries(item.frame).filter(([key]) => key !== "profile" && key !== "path").map(([key, value]) => `${key}: ${value}`).join(", ");
4391
4594
  const attested = item.attested_domains.join(", ");
@@ -4406,7 +4609,7 @@ function checkPendingHandler(cache) {
4406
4609
  return {
4407
4610
  content: [{
4408
4611
  type: "text",
4409
- text: `Failed to check pending attestations: ${error}`
4612
+ text: `Failed to check pending mandates: ${error}`
4410
4613
  }],
4411
4614
  isError: true
4412
4615
  };
@@ -4580,6 +4783,12 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4580
4783
  isError: true
4581
4784
  };
4582
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
+ }
4583
4792
  if (err instanceof ContentBindingError2) {
4584
4793
  return {
4585
4794
  text: `Proposal ${proposal.id}: blocked \u2014 the approved content cannot be bound to the receipt. ${err.message}`,
@@ -4593,6 +4802,13 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4593
4802
  text: `Proposal ${proposal.id} has already been executed by another request.`
4594
4803
  };
4595
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
+ }
4596
4812
  return {
4597
4813
  text: `Proposal ${proposal.id}: SP rejected receipt \u2014 ${err.message}`,
4598
4814
  isError: true
@@ -4639,7 +4855,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
4639
4855
  if (shouldAttachFooter()) {
4640
4856
  outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId, cachedAuth?.subjects?.[0]);
4641
4857
  }
4642
- outgoingArgs = attachReceiptId(discovered, outgoingArgs, receiptId);
4858
+ outgoingArgs = attachTicketId(discovered, outgoingArgs, receiptId);
4643
4859
  outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
4644
4860
  }
4645
4861
  let result;
@@ -4848,7 +5064,8 @@ function createMcpServer(state2, integrationManager2) {
4848
5064
  const instructions = buildMandateBrief({
4849
5065
  authorizations: enriched,
4850
5066
  executionLog: state2.executionLog,
4851
- integrationManager: integrationManager2
5067
+ integrationManager: integrationManager2,
5068
+ asVersionRefusal: state2.asVersionRefusal
4852
5069
  });
4853
5070
  const server = new McpServer(
4854
5071
  { name: "suveren-gateway", version: "0.1.0" },
@@ -4865,9 +5082,9 @@ function createMcpServer(state2, integrationManager2) {
4865
5082
  listAuthorizationsHandler(state2, integrationManager2)
4866
5083
  );
4867
5084
  server.registerTool(
4868
- "check-pending-attestations",
5085
+ "check-pending-mandates",
4869
5086
  {
4870
- 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.",
4871
5088
  inputSchema: {
4872
5089
  domain: z2.string().describe(`The owner's domain (e.g., "compliance")`)
4873
5090
  }
@@ -5004,7 +5221,7 @@ var AgentContactStore = class {
5004
5221
 
5005
5222
  // src/lib/gate-content.ts
5006
5223
  import { computeIntentHash } from "@hap/core";
5007
- import { decodeAttestationBlob as decodeAttestationBlob2 } from "@hap/core";
5224
+ import { decodeMandateBlob as decodeMandateBlob2 } from "@hap/core";
5008
5225
  function hashGateContent(text) {
5009
5226
  return computeIntentHash(text);
5010
5227
  }
@@ -5013,7 +5230,7 @@ function verifyGateContentHashes(content, auth) {
5013
5230
  if (auth.attestations.length === 0) {
5014
5231
  return { valid: false, errors: ["No attestations available to verify against"] };
5015
5232
  }
5016
- const attestation = decodeAttestationBlob2(auth.attestations[0].blob);
5233
+ const attestation = decodeMandateBlob2(auth.attestations[0].blob);
5017
5234
  const expectedHashes = attestation.payload.gate_content_hashes;
5018
5235
  if (!expectedHashes?.intent) {
5019
5236
  return { valid: false, errors: ["Attestation does not contain intent hash"] };
@@ -5115,7 +5332,6 @@ import { existsSync as existsSync15, writeFileSync as writeFileSync12, mkdirSync
5115
5332
  import { execFile } from "child_process";
5116
5333
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5117
5334
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5118
- import { getProfile as getProfile5 } from "@hap/core";
5119
5335
 
5120
5336
  // src/lib/manifest-loader.ts
5121
5337
  import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
@@ -5725,7 +5941,7 @@ var IntegrationManager = class {
5725
5941
  await this.connectWithGuard(client, transport, config);
5726
5942
  console.error(`[IntegrationManager] Connected to ${config.id} (${config.command} ${config.args.join(" ")})`);
5727
5943
  const toolsResult = await client.listTools();
5728
- const profileGating = config.toolGating ?? (config.profile ? getProfile5(config.profile)?.toolGating ?? null : null);
5944
+ const profileGating = config.toolGating ?? null;
5729
5945
  const tools = (toolsResult.tools ?? []).map((tool) => {
5730
5946
  const gating = this.resolveToolGating(config.profile, profileGating, tool.name);
5731
5947
  return {
@@ -6230,11 +6446,11 @@ async function buildTicketDetails(archive, ticketIds) {
6230
6446
  }
6231
6447
 
6232
6448
  // src/lib/report/window.ts
6233
- import { getProfile as getProfile6 } from "@hap/core";
6449
+ import { getProfile as getProfile5 } from "@hap/core";
6234
6450
  var REPORT_AGE_FIELD = "read_age_days";
6235
6451
  var REPORTING_PROFILE = "reporting";
6236
6452
  var END_SKEW_SECONDS = 120;
6237
- 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.";
6238
6454
  var DAY = 86400;
6239
6455
  function boundsOf(a) {
6240
6456
  return a.bounds ?? a.frame;
@@ -6244,7 +6460,7 @@ function windowLabel(start, days, loadedAt) {
6244
6460
  if (loadedAt !== null && loadedAt === start) return `${since} (when the test data was loaded)`;
6245
6461
  return `${since} (the last ${days} ${days === 1 ? "day" : "days"})`;
6246
6462
  }
6247
- var defaultLookup = (id) => getProfile6(id);
6463
+ var defaultLookup = (id) => getProfile5(id);
6248
6464
  function reportingMandateRefusal(m, lookup = defaultLookup) {
6249
6465
  const schema = lookup(m.profileId)?.boundsSchema;
6250
6466
  const field = resolveAgeBoundField(schema, REPORT_AGE_FIELD);
@@ -6727,7 +6943,7 @@ function writeReportTool(deps) {
6727
6943
  type: "object",
6728
6944
  properties: {
6729
6945
  html: { type: "string", description: "The full report HTML \u2014 see the tool description for how to write it." },
6730
- receipt_id: { type: "string" }
6946
+ ticket_id: { type: "string" }
6731
6947
  },
6732
6948
  required: ["html"]
6733
6949
  },
@@ -6862,7 +7078,7 @@ function loadGuides(builtinDir = builtinGuidesDir(), overrideDir = process.env.S
6862
7078
  }
6863
7079
  return [...byTopic.values()].sort((a, b) => a.order - b.order || a.topic.localeCompare(b.topic));
6864
7080
  }
6865
- function guideHeader(systems) {
7081
+ function guideHeader(systems, inactive = []) {
6866
7082
  const lines = [
6867
7083
  "> This guide is in English. Talk to the person in their language, and write the brief and mandate intents in the language the team uses.",
6868
7084
  "",
@@ -6880,12 +7096,67 @@ function guideHeader(systems) {
6880
7096
  lines.push(`- **${s.id}** \u2014 ${parts.join("; ")}`);
6881
7097
  }
6882
7098
  }
7099
+ if (inactive.length) {
7100
+ lines.push("");
7101
+ lines.push("**Available but not activated:** " + inactive.map((n) => `**${n}**`).join(", ") + ". Before anything else, ask the person to activate the ones the work needs: in the Suveren Gateway, Integrations \u2192 the system's card \u2192 Activate. Then read this guide again \u2014 a system you cannot see here does not exist for the test.");
7102
+ }
6883
7103
  lines.push("");
6884
7104
  lines.push(`**Approvals:** every mandate and brief you propose waits until the person approves it in the Suveren Gateway at ${uiUrl()}/approvals. Tell them each time you propose one.`);
7105
+ lines.push("");
7106
+ lines.push("**Rules:**");
7107
+ for (const r of AGENT_RULES) lines.push(`- ${r}`);
6885
7108
  return lines.join("\n");
6886
7109
  }
7110
+ function scopeFieldsOf(profile) {
7111
+ return profile.scopeSchema;
7112
+ }
7113
+ function fieldLine(name, f) {
7114
+ const kind = [];
7115
+ const values = f.boundType?.values ?? f.enum;
7116
+ if (Array.isArray(values)) kind.push(`one of ${values.map((v) => `\`${String(v)}\``).join(", ")}`);
7117
+ else if (f.type) kind.push(f.type);
7118
+ if (f.unit) kind.push(f.unit);
7119
+ if (f.boundType?.window) kind.push(`per ${f.boundType.window === "daily" ? "day" : f.boundType.window}`);
7120
+ if (typeof f.maximum === "number") kind.push(`at most ${f.maximum}`);
7121
+ if (f.default !== void 0) kind.push(`default \`${String(f.default)}\``);
7122
+ const label = f.displayName ? ` (${f.displayName})` : "";
7123
+ return `- \`${name}\`${label} \u2014 ${kind.join(", ") || "value"}${f.description ? `. ${f.description}` : ""}`;
7124
+ }
7125
+ function fieldLines(schema) {
7126
+ const fields = schema?.fields ?? {};
7127
+ const order = (schema?.keyOrder ?? Object.keys(fields)).filter((k) => k !== "profile" && k !== "path" && fields[k]);
7128
+ return order.map((k) => fieldLine(k, fields[k]));
7129
+ }
7130
+ function limitsSection(systems) {
7131
+ const out = ["## Limits and scope per system", "", "From the profiles \u2014 use exactly these names in `create_mandate` (`limits` and `scope`). Every limit must be set.", ""];
7132
+ for (const s of systems) {
7133
+ if (!s.profile) continue;
7134
+ out.push(`### ${s.id} \u2014 profile \`${s.profile.id}\``, "", "**Limits:**", ...fieldLines(s.profile.boundsSchema));
7135
+ const scope2 = fieldLines(scopeFieldsOf(s.profile));
7136
+ out.push("", "**Scope:**", ...scope2.length ? scope2 : ["- none"], "");
7137
+ }
7138
+ return out.join("\n");
7139
+ }
7140
+ function packageSection(tool) {
7141
+ if (!tool?.inputSchema) return "## The package format\n\nNo simulated system with `load_simulation` is active \u2014 ask the person to activate one first.";
7142
+ const pkg = tool.inputSchema.properties?.package ?? tool.inputSchema;
7143
+ return [
7144
+ "## The package format",
7145
+ "",
7146
+ ...tool.description ? ["What the simulated systems say about it:", "", `> ${tool.description}`, ""] : [],
7147
+ "The `package` argument of `load_simulation` \u2014 the same package for every simulated system:",
7148
+ "",
7149
+ "```json",
7150
+ JSON.stringify(pkg, null, 2),
7151
+ "```"
7152
+ ].join("\n");
7153
+ }
7154
+ function fillGenerated(body, sections) {
7155
+ return body.replace(/<!--\s*generated:\s*([a-z-]+)\s*-->/g, (m, key) => sections[key] ? sections[key]() : m);
7156
+ }
6887
7157
 
6888
7158
  // src/lib/builtins/setup.ts
7159
+ import { getProfile as getProfile6 } from "@hap/core";
6889
7160
  var DELEGATION_PROFILE = "delegation";
6890
7161
  var DELEGATION_INTENT_HINT = [
6891
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.",
@@ -6974,7 +7245,10 @@ function setupBuiltin(deps) {
6974
7245
  handler: async (args) => {
6975
7246
  if (!isSimulationMode()) return { ...builtinText("Refused: not available outside simulation mode."), isError: true };
6976
7247
  const guides = loadGuides();
6977
- const header = guideHeader(connectedSystems(deps.integrationManager));
7248
+ const systems = connectedSystems(deps.integrationManager);
7249
+ const running = new Set(systems.map((s) => s.id));
7250
+ const inactive = getAllManifests().filter((m) => manifestIsSimulated(m) && !running.has(m.id)).map((m) => m.name);
7251
+ const header = guideHeader(systems, inactive);
6978
7252
  const topic = typeof args.topic === "string" ? args.topic.trim().toLowerCase() : "";
6979
7253
  if (!topic) {
6980
7254
  const list = guides.map((g, i) => `${i + 1}. **${g.topic}** \u2014 ${g.summary}`).join("\n");
@@ -6990,9 +7264,15 @@ Read each topic with get_guide(topic) before doing that step.`);
6990
7264
  if (!guide) {
6991
7265
  return { ...builtinText(`Unknown topic "${topic}". Topics: ${guides.map((g) => g.topic).join(", ") || "none installed"}.`), isError: true };
6992
7266
  }
7267
+ const body = fillGenerated(guide.body.trim(), {
7268
+ limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile: getProfile6(s.profile) }))),
7269
+ package: () => packageSection(
7270
+ deps.integrationManager.getAllTools().find((t) => !deps.integrationManager.isBuiltin(t.integrationId) && t.originalName === "load_simulation")
7271
+ )
7272
+ });
6993
7273
  return builtinText(`${header}
6994
7274
 
6995
- ${guide.body.trim()}`);
7275
+ ${body}`);
6996
7276
  }
6997
7277
  },
6998
7278
  {
@@ -7002,7 +7282,7 @@ ${guide.body.trim()}`);
7002
7282
  type: "object",
7003
7283
  properties: {
7004
7284
  content: { type: "string", description: "The complete new agent brief, in markdown (at most 16 KB)." },
7005
- receipt_id: {
7285
+ ticket_id: {
7006
7286
  type: "string",
7007
7287
  description: "Authorization reference for this call, set by the governing gateway \u2014 agents do not set this."
7008
7288
  }
@@ -7034,7 +7314,7 @@ ${guide.body.trim()}`);
7034
7314
  mode: { type: "string", enum: ["review", "automatic"], description: "review = each action needs approval; automatic = within the limits." },
7035
7315
  duration_hours: { type: "number", description: "How long the mandate is valid, in hours. Omit for the profile default." },
7036
7316
  title: { type: "string", description: "A short name for the mandate." },
7037
- receipt_id: {
7317
+ ticket_id: {
7038
7318
  type: "string",
7039
7319
  description: "Authorization reference for this call, set by the governing gateway \u2014 agents do not set this."
7040
7320
  }
@@ -7087,7 +7367,7 @@ function registerBuiltins(deps, factories = BUILTIN_FACTORIES) {
7087
7367
  }
7088
7368
 
7089
7369
  // bin/http.ts
7090
- import { decodeAttestationBlob as decodeAttestationBlob3 } from "@hap/core";
7370
+ import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
7091
7371
 
7092
7372
  // src/lib/committed-executor.ts
7093
7373
  import { existsSync as existsSync18, mkdirSync as mkdirSync13, readFileSync as readFileSync19, unlinkSync as unlinkSync6, writeFileSync as writeFileSync13 } from "fs";
@@ -7224,6 +7504,7 @@ var reportSources = {
7224
7504
  }
7225
7505
  writeMcpPairedAsUrl(dataDir, spUrl);
7226
7506
  })();
7507
+ void state.checkAsCompat();
7227
7508
  var spApiKey = process.env.SUVEREN_AS_API_KEY ?? "";
7228
7509
  if (spApiKey) {
7229
7510
  state.spClient.setApiKey(spApiKey);
@@ -7589,7 +7870,10 @@ app.get("/internal/authorizations", internalOnly, (_req, res) => {
7589
7870
  bounds: a.frame,
7590
7871
  context: a.context ?? {},
7591
7872
  intent: a.gateContent?.intent ?? null,
7592
- 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
7593
7877
  }));
7594
7878
  res.json({ authorizations });
7595
7879
  });
@@ -7614,7 +7898,7 @@ app.get("/internal/evidence", internalOnly, (_req, res) => {
7614
7898
  ...a,
7615
7899
  attestations: a.attestations.map((att) => {
7616
7900
  try {
7617
- return { ...att, decoded: decodeAttestationBlob3(att.blob) };
7901
+ return { ...att, decoded: decodeMandateBlob3(att.blob) };
7618
7902
  } catch {
7619
7903
  return att;
7620
7904
  }
@@ -7781,7 +8065,8 @@ app.get("/internal/brief", internalOnly, (_req, res) => {
7781
8065
  const brief = buildMandateBrief({
7782
8066
  authorizations: enriched,
7783
8067
  executionLog: state.executionLog,
7784
- integrationManager
8068
+ integrationManager,
8069
+ asVersionRefusal: state.asVersionRefusal
7785
8070
  });
7786
8071
  res.json({ brief });
7787
8072
  } catch (err) {