@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.
- package/content/guides/1-interview.md +6 -0
- package/content/guides/2-package.md +4 -1
- package/content/guides/3-mandates.md +3 -0
- package/content/guides/6-risks.md +2 -0
- 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 +382 -97
- package/dist/mcp-server/report-verify-cli.mjs +1 -1
- package/dist/ui/assets/index-1gCMlbLl.js +116 -0
- package/dist/ui/assets/{index-RCFQUjaF.css → index-blnpvM2g.css} +1 -1
- package/dist/ui/index.html +2 -2
- 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-BseWI4iJ.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({
|
|
@@ -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
|
|
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,
|
|
4180
|
-
if (!integrationManager2
|
|
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
|
|
4194
|
-
|
|
4195
|
-
if (override === null) {
|
|
4390
|
+
const gating = tool.gating;
|
|
4391
|
+
if (gating.category === "read") {
|
|
4196
4392
|
readOnly.push(tool.originalName);
|
|
4197
|
-
} else if (
|
|
4198
|
-
|
|
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 =
|
|
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 ? "" : " (
|
|
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
|
|
4503
|
+
output2.push(` Missing mandates: ${missing.join(", ")}`);
|
|
4305
4504
|
}
|
|
4306
4505
|
if (profile && integrationManager2) {
|
|
4307
4506
|
output2.push("");
|
|
4308
|
-
output2.push(buildCapabilityMap(auth.profileId,
|
|
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
|
|
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(", ")}
|
|
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
|
|
4587
|
+
text: `No pending mandates for domain "${domain}".`
|
|
4385
4588
|
}]
|
|
4386
4589
|
};
|
|
4387
4590
|
}
|
|
4388
|
-
const lines = [`Pending
|
|
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
|
|
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 =
|
|
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-
|
|
5085
|
+
"check-pending-mandates",
|
|
4869
5086
|
{
|
|
4870
|
-
description: "Check if any
|
|
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 {
|
|
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 =
|
|
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 ??
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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
|
-
${
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
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) {
|