@suveren/gateway 0.19.5 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/integrations/crm.json +1 -1
- package/content/integrations/deploy-github.json +1 -1
- package/content/integrations/erp.json +1 -1
- package/content/integrations/mail.json +1 -1
- package/content/integrations/records.json +1 -1
- package/dist/control-plane/index.mjs +21 -13
- package/dist/mcp-server/{chunk-ZRRZFGRL.mjs → chunk-APIQIEUA.mjs} +39 -27
- package/dist/mcp-server/http.mjs +308 -97
- package/dist/mcp-server/report-verify-cli.mjs +1 -1
- package/dist/ui/assets/index-1s1q-XLL.js +116 -0
- package/dist/ui/index.html +1 -1
- package/node_modules/@hap/core/README.md +92 -29
- package/node_modules/@hap/core/dist/index.d.mts +614 -593
- package/node_modules/@hap/core/dist/index.d.ts +614 -593
- package/node_modules/@hap/core/dist/index.js +774 -749
- package/node_modules/@hap/core/dist/index.mjs +744 -724
- package/node_modules/@hap/core/package.json +1 -1
- package/node_modules/@hap/core/src/base64url.ts +39 -0
- package/node_modules/@hap/core/src/commitment-modes.ts +0 -21
- package/node_modules/@hap/core/src/errors.ts +76 -0
- package/node_modules/@hap/core/src/frame.ts +47 -178
- package/node_modules/@hap/core/src/gatekeeper.ts +219 -600
- package/node_modules/@hap/core/src/identity.ts +9 -12
- package/node_modules/@hap/core/src/index.ts +8 -4
- package/node_modules/@hap/core/src/mandate.ts +177 -166
- package/node_modules/@hap/core/src/owner-signature.ts +282 -0
- package/node_modules/@hap/core/src/profile.ts +234 -0
- package/node_modules/@hap/core/src/ticket.ts +148 -0
- package/node_modules/@hap/core/src/types.ts +191 -344
- package/node_modules/@hap/core/src/versions.ts +47 -0
- package/package.json +2 -2
- package/profiles/calendar/0.6.profile.json +191 -0
- package/profiles/charge/0.6.profile.json +211 -0
- package/profiles/customers/0.9.profile.json +182 -0
- package/profiles/delegation/0.3.profile.json +118 -0
- package/profiles/deploy/0.11.profile.json +177 -0
- package/profiles/email/0.8.profile.json +226 -0
- package/profiles/index.json +12 -1
- package/profiles/publish/0.6.profile.json +187 -0
- package/profiles/purchase/0.7.profile.json +235 -0
- package/profiles/records/0.6.profile.json +118 -0
- package/profiles/reporting/0.3.profile.json +98 -0
- package/profiles/sales/0.4.profile.json +291 -0
- package/dist/ui/assets/index-Du68zJhH.js +0 -116
- package/node_modules/@hap/core/src/attestation.ts +0 -173
- package/node_modules/@hap/core/src/receipt.ts +0 -74
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -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-
|
|
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
|
-
*
|
|
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
|
|
854
|
-
const res = await this.fetch(
|
|
855
|
-
if (!res.ok) throw new Error(`SP
|
|
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
|
|
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/
|
|
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
|
-
|
|
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/
|
|
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.
|
|
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 {
|
|
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 =
|
|
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.
|
|
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: "
|
|
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: "
|
|
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
|
|
2118
|
+
let trustedIssuer;
|
|
2054
2119
|
try {
|
|
2055
|
-
|
|
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
|
|
2075
|
-
const
|
|
2139
|
+
const resolvedScope = override?.context ?? auth.context;
|
|
2140
|
+
const bounds = { ...resolvedBounds, profile: auth.profileId };
|
|
2076
2141
|
const request = {
|
|
2077
|
-
|
|
2078
|
-
|
|
2142
|
+
bounds,
|
|
2143
|
+
mandates: auth.attestations.map((a) => a.blob),
|
|
2079
2144
|
execution,
|
|
2080
|
-
|
|
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 `
|
|
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,
|
|
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
|
-
*
|
|
2594
|
-
*
|
|
2595
|
-
*
|
|
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
|
-
|
|
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}/
|
|
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
|
|
3058
|
+
function attachTicketId(tool, args, ticketId) {
|
|
2942
3059
|
const schema = tool.inputSchema;
|
|
2943
|
-
if (!schema?.properties || !("
|
|
2944
|
-
return { ...args,
|
|
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 {
|
|
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
|
|
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
|
|
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 === "
|
|
3609
|
+
if (err.statusCode === 403 && (code === "MANDATE_REVOKED" || code === "MANDATE_EXPIRED")) {
|
|
3475
3610
|
return true;
|
|
3476
3611
|
}
|
|
3477
|
-
if (err.statusCode === 404 && code === "
|
|
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)?.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
4190
|
-
if (!integrationManager2
|
|
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
|
|
4204
|
-
|
|
4205
|
-
if (override === null) {
|
|
4390
|
+
const gating = tool.gating;
|
|
4391
|
+
if (gating.category === "read") {
|
|
4206
4392
|
readOnly.push(tool.originalName);
|
|
4207
|
-
} else if (
|
|
4208
|
-
|
|
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 =
|
|
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 ? "" : " (
|
|
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
|
|
4503
|
+
output2.push(` Missing mandates: ${missing.join(", ")}`);
|
|
4315
4504
|
}
|
|
4316
4505
|
if (profile && integrationManager2) {
|
|
4317
4506
|
output2.push("");
|
|
4318
|
-
output2.push(buildCapabilityMap(auth.profileId,
|
|
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
|
|
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(", ")}
|
|
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
|
|
4587
|
+
text: `No pending mandates for domain "${domain}".`
|
|
4395
4588
|
}]
|
|
4396
4589
|
};
|
|
4397
4590
|
}
|
|
4398
|
-
const lines = [`Pending
|
|
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
|
|
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 =
|
|
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-
|
|
5085
|
+
"check-pending-mandates",
|
|
4879
5086
|
{
|
|
4880
|
-
description: "Check if any
|
|
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 {
|
|
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 =
|
|
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 ??
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
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) {
|