@suveren/gateway 0.3.17 → 0.4.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/bin/suveren-gateway.js +419 -28
- package/content/integrations/calendar.json +20 -8
- package/content/integrations/crm.json +97 -23
- package/content/integrations/gmail.json +166 -29
- package/content/integrations/linkedin.json +15 -3
- package/content/integrations/mollie.json +2 -2
- package/dist/control-plane/index.mjs +333 -41
- package/dist/mcp-server/http.mjs +880 -210
- package/dist/ui/assets/index-BDQY7Z3H.js +105 -0
- package/dist/ui/assets/{index-DOt3SNnl.css → index-JHaCddDE.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/node_modules/@hap/core/dist/index.d.mts +27 -0
- package/node_modules/@hap/core/dist/index.d.ts +27 -0
- package/node_modules/@hap/core/dist/index.js +13 -1
- package/node_modules/@hap/core/dist/index.mjs +13 -1
- package/node_modules/@hap/core/package.json +1 -1
- package/node_modules/@hap/core/src/gatekeeper.ts +47 -1
- package/node_modules/@hap/core/src/types.ts +27 -0
- package/node_modules/@hono/node-server/README.md +58 -25
- package/node_modules/@hono/node-server/dist/conninfo.cjs +22 -0
- package/node_modules/@hono/node-server/dist/{conninfo.d.ts → conninfo.d.cts} +4 -3
- package/node_modules/@hono/node-server/dist/conninfo.d.mts +4 -3
- package/node_modules/@hono/node-server/dist/conninfo.mjs +19 -16
- package/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs +5 -0
- package/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs +11 -0
- package/node_modules/@hono/node-server/dist/index.cjs +1173 -0
- package/node_modules/@hono/node-server/dist/index.d.cts +101 -0
- package/node_modules/@hono/node-server/dist/index.d.mts +101 -8
- package/node_modules/@hono/node-server/dist/index.mjs +1143 -637
- package/node_modules/@hono/node-server/dist/serve-static.cjs +151 -0
- package/node_modules/@hono/node-server/dist/serve-static.d.cts +18 -0
- package/node_modules/@hono/node-server/dist/serve-static.d.mts +14 -13
- package/node_modules/@hono/node-server/dist/serve-static.mjs +143 -145
- package/node_modules/@hono/node-server/dist/utils/response.cjs +8 -0
- package/node_modules/@hono/node-server/dist/utils/response.d.cts +4 -0
- package/node_modules/@hono/node-server/dist/utils/response.d.mts +3 -2
- package/node_modules/@hono/node-server/dist/utils/response.mjs +6 -9
- package/node_modules/@hono/node-server/dist/utils/stream.cjs +65 -0
- package/node_modules/@hono/node-server/dist/utils/stream.d.cts +6 -0
- package/node_modules/@hono/node-server/dist/utils/stream.d.mts +6 -0
- package/node_modules/@hono/node-server/dist/utils/stream.mjs +64 -0
- package/node_modules/@hono/node-server/package.json +65 -54
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/index.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/index.js +1 -12
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/index.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/stdio.d.ts +7 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/stdio.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/stdio.js +9 -3
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/stdio.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.js +5 -3
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/index.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/index.js +1 -12
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/index.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/sseKeepAlive.d.ts +5 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/sseKeepAlive.d.ts.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/sseKeepAlive.js +17 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/sseKeepAlive.js.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/stdio.d.ts +9 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/stdio.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/stdio.js +10 -4
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/stdio.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.d.ts +21 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.js +239 -41
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/zod-compat.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/zod-compat.js +25 -6
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/zod-compat.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/mediaType.d.ts +32 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/mediaType.d.ts.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/mediaType.js +64 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/mediaType.js.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/stdio.d.ts +5 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/stdio.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/stdio.js +10 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/stdio.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/index.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/index.js +2 -13
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/index.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.d.ts +7 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.js +9 -3
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js +5 -3
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js +2 -13
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/sseKeepAlive.d.ts +5 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/sseKeepAlive.d.ts.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/sseKeepAlive.js +13 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/sseKeepAlive.js.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.d.ts +9 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js +10 -4
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.d.ts +21 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js +239 -41
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js +25 -6
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/mediaType.d.ts +32 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/mediaType.d.ts.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/mediaType.js +57 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/mediaType.js.map +1 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.d.ts +5 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.d.ts.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js +9 -0
- package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js.map +1 -1
- package/node_modules/@modelcontextprotocol/sdk/node_modules/media-typer/README.md +15 -14
- package/node_modules/@modelcontextprotocol/sdk/node_modules/media-typer/index.js +0 -8
- package/node_modules/@modelcontextprotocol/sdk/node_modules/media-typer/package.json +16 -12
- package/node_modules/@modelcontextprotocol/sdk/package.json +4 -3
- package/node_modules/@types/node/README.md +1 -1
- package/node_modules/@types/node/crypto.d.ts +1 -1
- package/node_modules/@types/node/package.json +2 -2
- package/node_modules/body-parser/HISTORY.md +7 -1
- package/node_modules/body-parser/lib/types/json.js +7 -3
- package/node_modules/body-parser/lib/types/raw.js +7 -3
- package/node_modules/body-parser/lib/types/text.js +7 -3
- package/node_modules/body-parser/lib/types/urlencoded.js +7 -3
- package/node_modules/body-parser/package.json +1 -1
- package/node_modules/express-rate-limit/dist/index.cjs +69 -5
- package/node_modules/express-rate-limit/dist/index.d.cts +16 -0
- package/node_modules/express-rate-limit/dist/index.d.mts +16 -0
- package/node_modules/express-rate-limit/dist/index.d.ts +16 -0
- package/node_modules/express-rate-limit/dist/index.mjs +55 -5
- package/node_modules/express-rate-limit/node_modules/debug/LICENSE +20 -0
- package/node_modules/express-rate-limit/node_modules/debug/README.md +481 -0
- package/node_modules/express-rate-limit/node_modules/debug/package.json +64 -0
- package/node_modules/express-rate-limit/node_modules/debug/src/browser.js +272 -0
- package/node_modules/express-rate-limit/node_modules/debug/src/common.js +292 -0
- package/node_modules/express-rate-limit/node_modules/debug/src/index.js +10 -0
- package/node_modules/express-rate-limit/node_modules/debug/src/node.js +263 -0
- package/node_modules/express-rate-limit/node_modules/ms/index.js +162 -0
- package/node_modules/express-rate-limit/node_modules/ms/license.md +21 -0
- package/node_modules/express-rate-limit/node_modules/ms/package.json +38 -0
- package/node_modules/express-rate-limit/node_modules/ms/readme.md +59 -0
- package/node_modules/express-rate-limit/package.json +12 -9
- package/node_modules/fast-uri/index.js +17 -0
- package/node_modules/fast-uri/package.json +1 -1
- package/node_modules/fast-uri/test/security.test.js +62 -0
- package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -4
- package/node_modules/hono/dist/adapter/lambda-edge/handler.js +12 -2
- package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -4
- package/node_modules/hono/dist/cjs/adapter/lambda-edge/handler.js +12 -2
- package/node_modules/hono/dist/cjs/client/client.js +10 -1
- package/node_modules/hono/dist/cjs/client/utils.js +1 -1
- package/node_modules/hono/dist/cjs/helper/streaming/sse.js +5 -4
- package/node_modules/hono/dist/cjs/middleware/cache/index.js +1 -1
- package/node_modules/hono/dist/cjs/middleware/compress/index.js +2 -1
- package/node_modules/hono/dist/cjs/middleware/etag/index.js +2 -1
- package/node_modules/hono/dist/cjs/middleware/method-override/index.js +5 -3
- package/node_modules/hono/dist/cjs/middleware/secure-headers/secure-headers.js +10 -4
- package/node_modules/hono/dist/cjs/request.js +8 -3
- package/node_modules/hono/dist/cjs/router/trie-router/node.js +9 -0
- package/node_modules/hono/dist/cjs/utils/accept.js +1 -1
- package/node_modules/hono/dist/cjs/utils/body.js +6 -0
- package/node_modules/hono/dist/cjs/utils/url.js +1 -1
- package/node_modules/hono/dist/client/client.js +10 -1
- package/node_modules/hono/dist/client/utils.js +1 -1
- package/node_modules/hono/dist/helper/streaming/sse.js +5 -4
- package/node_modules/hono/dist/middleware/cache/index.js +1 -1
- package/node_modules/hono/dist/middleware/compress/index.js +2 -1
- package/node_modules/hono/dist/middleware/etag/index.js +2 -1
- package/node_modules/hono/dist/middleware/method-override/index.js +5 -3
- package/node_modules/hono/dist/middleware/secure-headers/secure-headers.js +10 -4
- package/node_modules/hono/dist/request.js +8 -3
- package/node_modules/hono/dist/router/trie-router/node.js +9 -0
- package/node_modules/hono/dist/types/adapter/aws-lambda/types.d.ts +9 -0
- package/node_modules/hono/dist/types/adapter/lambda-edge/handler.d.ts +1 -1
- package/node_modules/hono/dist/types/helper/websocket/index.d.ts +1 -1
- package/node_modules/hono/dist/types/middleware/combine/index.d.ts +1 -1
- package/node_modules/hono/dist/types/middleware/language/language.d.ts +18 -0
- package/node_modules/hono/dist/types/utils/types.d.ts +1 -1
- package/node_modules/hono/dist/utils/accept.js +1 -1
- package/node_modules/hono/dist/utils/body.js +6 -0
- package/node_modules/hono/dist/utils/url.js +1 -1
- package/node_modules/hono/package.json +4 -4
- package/node_modules/ip-address/README.md +102 -96
- package/node_modules/ip-address/dist/common.d.ts +23 -0
- package/node_modules/ip-address/dist/common.js +27 -4
- package/node_modules/ip-address/dist/common.js.map +1 -1
- package/node_modules/ip-address/dist/ipv4.d.ts +8 -1
- package/node_modules/ip-address/dist/ipv4.js +21 -8
- package/node_modules/ip-address/dist/ipv4.js.map +1 -1
- package/node_modules/ip-address/dist/ipv6.d.ts +59 -1
- package/node_modules/ip-address/dist/ipv6.js +149 -41
- package/node_modules/ip-address/dist/ipv6.js.map +1 -1
- package/node_modules/ip-address/dist/v4/constants.js +5 -1
- package/node_modules/ip-address/dist/v4/constants.js.map +1 -1
- package/node_modules/ip-address/dist/v6/constants.js +3 -2
- package/node_modules/ip-address/dist/v6/constants.js.map +1 -1
- package/node_modules/ip-address/package.json +2 -2
- package/node_modules/jose/README.md +1 -4
- package/node_modules/jose/dist/types/jwks/remote.d.ts +3 -3
- package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
- package/node_modules/jose/dist/webapi/lib/key_to_jwk.js +4 -1
- package/node_modules/jose/package.json +1 -1
- package/package.json +2 -2
- package/profiles/customers/0.4.profile.json +7 -4
- package/profiles/customers/0.5.profile.json +131 -0
- package/profiles/customers/0.6.profile.json +153 -0
- package/profiles/email/0.4.profile.json +8 -4
- package/profiles/index.json +2 -0
- package/server.js +10 -0
- package/dist/ui/assets/index-CTmWS7W4.js +0 -105
- package/node_modules/@hono/node-server/dist/conninfo.js +0 -42
- package/node_modules/@hono/node-server/dist/globals.d.mts +0 -2
- package/node_modules/@hono/node-server/dist/globals.d.ts +0 -2
- package/node_modules/@hono/node-server/dist/globals.js +0 -29
- package/node_modules/@hono/node-server/dist/globals.mjs +0 -5
- package/node_modules/@hono/node-server/dist/index.d.ts +0 -8
- package/node_modules/@hono/node-server/dist/index.js +0 -702
- package/node_modules/@hono/node-server/dist/listener.d.mts +0 -13
- package/node_modules/@hono/node-server/dist/listener.d.ts +0 -13
- package/node_modules/@hono/node-server/dist/listener.js +0 -670
- package/node_modules/@hono/node-server/dist/listener.mjs +0 -635
- package/node_modules/@hono/node-server/dist/request.d.mts +0 -25
- package/node_modules/@hono/node-server/dist/request.d.ts +0 -25
- package/node_modules/@hono/node-server/dist/request.js +0 -238
- package/node_modules/@hono/node-server/dist/request.mjs +0 -206
- package/node_modules/@hono/node-server/dist/response.d.mts +0 -26
- package/node_modules/@hono/node-server/dist/response.d.ts +0 -26
- package/node_modules/@hono/node-server/dist/response.js +0 -112
- package/node_modules/@hono/node-server/dist/response.mjs +0 -85
- package/node_modules/@hono/node-server/dist/serve-static.d.ts +0 -17
- package/node_modules/@hono/node-server/dist/serve-static.js +0 -177
- package/node_modules/@hono/node-server/dist/server.d.mts +0 -10
- package/node_modules/@hono/node-server/dist/server.d.ts +0 -10
- package/node_modules/@hono/node-server/dist/server.js +0 -696
- package/node_modules/@hono/node-server/dist/server.mjs +0 -660
- package/node_modules/@hono/node-server/dist/types.d.mts +0 -44
- package/node_modules/@hono/node-server/dist/types.d.ts +0 -44
- package/node_modules/@hono/node-server/dist/types.js +0 -18
- package/node_modules/@hono/node-server/dist/types.mjs +0 -0
- package/node_modules/@hono/node-server/dist/utils/response/constants.d.mts +0 -3
- package/node_modules/@hono/node-server/dist/utils/response/constants.d.ts +0 -3
- package/node_modules/@hono/node-server/dist/utils/response/constants.js +0 -30
- package/node_modules/@hono/node-server/dist/utils/response/constants.mjs +0 -5
- package/node_modules/@hono/node-server/dist/utils/response.d.ts +0 -3
- package/node_modules/@hono/node-server/dist/utils/response.js +0 -37
- package/node_modules/@hono/node-server/dist/utils.d.mts +0 -9
- package/node_modules/@hono/node-server/dist/utils.d.ts +0 -9
- package/node_modules/@hono/node-server/dist/utils.js +0 -99
- package/node_modules/@hono/node-server/dist/utils.mjs +0 -71
- package/node_modules/@hono/node-server/dist/vercel.d.mts +0 -7
- package/node_modules/@hono/node-server/dist/vercel.d.ts +0 -7
- package/node_modules/@hono/node-server/dist/vercel.js +0 -677
- package/node_modules/@hono/node-server/dist/vercel.mjs +0 -640
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -34,6 +34,19 @@ var SPClient = class {
|
|
|
34
34
|
setSessionCookie(cookie) {
|
|
35
35
|
this.sessionCookie = cookie;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Has anyone signed in yet this run?
|
|
39
|
+
*
|
|
40
|
+
* The control-plane pushes the session cookie on login, so its absence means
|
|
41
|
+
* the vault is locked — the gateway is up and answering, but it cannot read
|
|
42
|
+
* authorizations or credentials. Callers MUST distinguish that from "this
|
|
43
|
+
* user has no authorizations": the two look identical from the outside, and
|
|
44
|
+
* reporting the latter sends someone off to create authorizations when all
|
|
45
|
+
* they needed to do was enter their API key.
|
|
46
|
+
*/
|
|
47
|
+
isUnlocked() {
|
|
48
|
+
return this.sessionCookie !== "";
|
|
49
|
+
}
|
|
37
50
|
async fetch(path, init) {
|
|
38
51
|
const url = `${this.baseUrl}${path}`;
|
|
39
52
|
const headers = {
|
|
@@ -643,11 +656,131 @@ function windowCutoff(window, now) {
|
|
|
643
656
|
return now - 24 * 60 * 60;
|
|
644
657
|
case "weekly":
|
|
645
658
|
return now - 7 * 24 * 60 * 60;
|
|
646
|
-
case "monthly":
|
|
647
|
-
|
|
659
|
+
case "monthly": {
|
|
660
|
+
const d = new Date(now * 1e3);
|
|
661
|
+
return Math.floor(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1, 0, 0, 0, 0) / 1e3);
|
|
662
|
+
}
|
|
648
663
|
}
|
|
649
664
|
}
|
|
650
665
|
|
|
666
|
+
// src/lib/denial-log.ts
|
|
667
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
668
|
+
import { homedir as homedir3 } from "os";
|
|
669
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
670
|
+
import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, randomBytes as randomBytes3 } from "crypto";
|
|
671
|
+
var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join3(homedir3(), ".suveren");
|
|
672
|
+
var MAX_RECORDS = 200;
|
|
673
|
+
var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
674
|
+
var DenialLog = class {
|
|
675
|
+
records = [];
|
|
676
|
+
baseDir;
|
|
677
|
+
vaultKey = null;
|
|
678
|
+
constructor(filePath) {
|
|
679
|
+
this.baseDir = filePath && filePath.endsWith(".json") ? dirname3(filePath) : filePath ?? DEFAULT_DIR3;
|
|
680
|
+
this.loadPlaintext();
|
|
681
|
+
}
|
|
682
|
+
setVaultKey(key) {
|
|
683
|
+
this.vaultKey = key;
|
|
684
|
+
if (existsSync3(this.encryptedFilePath)) {
|
|
685
|
+
this.loadEncrypted();
|
|
686
|
+
} else if (this.records.length > 0) {
|
|
687
|
+
this.persistEncrypted();
|
|
688
|
+
if (existsSync3(this.plaintextFilePath)) {
|
|
689
|
+
try {
|
|
690
|
+
unlinkSync3(this.plaintextFilePath);
|
|
691
|
+
} catch {
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/** Record a read denial. Never throws — recording must not break the denial. */
|
|
697
|
+
record(rec) {
|
|
698
|
+
try {
|
|
699
|
+
this.records.push(rec);
|
|
700
|
+
this.prune(rec.ts);
|
|
701
|
+
this.persist();
|
|
702
|
+
} catch (err) {
|
|
703
|
+
console.error("[DenialLog] failed to record denial:", err);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/** Recent denials, newest first. `sinceMs` and `limit` are optional filters. */
|
|
707
|
+
getRecent(sinceMs, limit = MAX_RECORDS) {
|
|
708
|
+
let out = [...this.records].sort((a, b) => b.ts - a.ts);
|
|
709
|
+
if (sinceMs !== void 0) out = out.filter((r) => r.ts >= sinceMs);
|
|
710
|
+
return out.slice(0, limit);
|
|
711
|
+
}
|
|
712
|
+
get size() {
|
|
713
|
+
return this.records.length;
|
|
714
|
+
}
|
|
715
|
+
// ─── Retention ────────────────────────────────────────────────────────────
|
|
716
|
+
prune(now) {
|
|
717
|
+
const cutoff = now - MAX_AGE_MS;
|
|
718
|
+
this.records = this.records.filter((r) => r.ts >= cutoff);
|
|
719
|
+
if (this.records.length > MAX_RECORDS) {
|
|
720
|
+
this.records = this.records.slice(this.records.length - MAX_RECORDS);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// ─── Encryption (mirrors ExecutionLog) ──────────────────────────────────────
|
|
724
|
+
encrypt(plaintext) {
|
|
725
|
+
if (!this.vaultKey) throw new Error("No vault key");
|
|
726
|
+
const iv = randomBytes3(12);
|
|
727
|
+
const cipher = createCipheriv3("aes-256-gcm", this.vaultKey, iv);
|
|
728
|
+
const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
729
|
+
return { iv: iv.toString("hex"), ciphertext: enc.toString("hex"), tag: cipher.getAuthTag().toString("hex") };
|
|
730
|
+
}
|
|
731
|
+
decrypt(blob) {
|
|
732
|
+
if (!this.vaultKey) throw new Error("No vault key");
|
|
733
|
+
const decipher = createDecipheriv3("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
|
|
734
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
735
|
+
return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
|
|
736
|
+
}
|
|
737
|
+
get plaintextFilePath() {
|
|
738
|
+
return join3(this.baseDir, "denials.json");
|
|
739
|
+
}
|
|
740
|
+
get encryptedFilePath() {
|
|
741
|
+
return join3(this.baseDir, "denials.enc.json");
|
|
742
|
+
}
|
|
743
|
+
loadPlaintext() {
|
|
744
|
+
if (!existsSync3(this.plaintextFilePath)) {
|
|
745
|
+
mkdirSync3(this.baseDir, { recursive: true });
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
const data = JSON.parse(readFileSync3(this.plaintextFilePath, "utf-8"));
|
|
750
|
+
this.records = data.records ?? [];
|
|
751
|
+
} catch {
|
|
752
|
+
console.error(`[DenialLog] Could not parse ${this.plaintextFilePath}, starting fresh`);
|
|
753
|
+
this.records = [];
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
loadEncrypted() {
|
|
757
|
+
if (!existsSync3(this.encryptedFilePath)) return;
|
|
758
|
+
try {
|
|
759
|
+
const data = JSON.parse(readFileSync3(this.encryptedFilePath, "utf-8"));
|
|
760
|
+
const logFile = JSON.parse(this.decrypt(data.blob));
|
|
761
|
+
this.records = logFile.records ?? [];
|
|
762
|
+
} catch (err) {
|
|
763
|
+
console.error(`[DenialLog] Could not decrypt ${this.encryptedFilePath}:`, err);
|
|
764
|
+
this.records = [];
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
persist() {
|
|
768
|
+
if (this.vaultKey) this.persistEncrypted();
|
|
769
|
+
else this.persistPlaintext();
|
|
770
|
+
}
|
|
771
|
+
persistPlaintext() {
|
|
772
|
+
const data = { version: 1, records: this.records };
|
|
773
|
+
mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
|
|
774
|
+
writeFileSync3(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
775
|
+
}
|
|
776
|
+
persistEncrypted() {
|
|
777
|
+
const logFile = { version: 1, records: this.records };
|
|
778
|
+
const data = { version: 1, blob: this.encrypt(JSON.stringify(logFile)) };
|
|
779
|
+
mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
|
|
780
|
+
writeFileSync3(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
651
784
|
// src/lib/gatekeeper.ts
|
|
652
785
|
import { verify } from "@hap/core";
|
|
653
786
|
var MCPGatekeeper = class {
|
|
@@ -699,7 +832,15 @@ var MCPGatekeeper = class {
|
|
|
699
832
|
frame,
|
|
700
833
|
attestations: auth.attestations.map((a) => a.blob),
|
|
701
834
|
execution,
|
|
702
|
-
context: resolvedContext
|
|
835
|
+
context: resolvedContext,
|
|
836
|
+
// Scopes the local cumulative check to the same (profileId, path) key the
|
|
837
|
+
// ExecutionLog records under (see tool-proxy's record call). It cannot go
|
|
838
|
+
// inside `frame`: that is validated against the profile's boundsSchema,
|
|
839
|
+
// which declares no `path` field in any shipped profile, so an extra key
|
|
840
|
+
// there fails verification with "Unknown field". Omitting it entirely —
|
|
841
|
+
// the previous behaviour — made every cumulative lookup return zero, so
|
|
842
|
+
// the local gate never fired and the AS was doing all the enforcing.
|
|
843
|
+
path: auth.path
|
|
703
844
|
};
|
|
704
845
|
const result = await verify(request, publicKeyHex, void 0, this.executionLog);
|
|
705
846
|
return { result, authorization: auth };
|
|
@@ -712,12 +853,14 @@ var SharedState = class {
|
|
|
712
853
|
cache;
|
|
713
854
|
gateStore;
|
|
714
855
|
executionLog;
|
|
856
|
+
denialLog;
|
|
715
857
|
gatekeeper;
|
|
716
858
|
constructor(spUrl2, gateStorePath) {
|
|
717
859
|
this.spClient = new SPClient(spUrl2);
|
|
718
860
|
this.cache = new AttestationCache(this.spClient);
|
|
719
861
|
this.gateStore = new GateStore(gateStorePath);
|
|
720
862
|
this.executionLog = new ExecutionLog(gateStorePath);
|
|
863
|
+
this.denialLog = new DenialLog(gateStorePath);
|
|
721
864
|
this.gatekeeper = new MCPGatekeeper(this.cache, this.executionLog);
|
|
722
865
|
}
|
|
723
866
|
setGateContent(path, authorizationId, profileId, content, opts) {
|
|
@@ -771,6 +914,7 @@ function getConsumptionState(auth, executionLog, profile) {
|
|
|
771
914
|
const fieldDef = rawFieldDef;
|
|
772
915
|
const bt = fieldDef.boundType;
|
|
773
916
|
if (!bt) continue;
|
|
917
|
+
if (fieldDef.enforced === false) continue;
|
|
774
918
|
if (bt.kind !== "cumulative_sum" && bt.kind !== "cumulative_count") continue;
|
|
775
919
|
const boundValue = auth.frame[fieldName];
|
|
776
920
|
const limit = typeof boundValue === "number" ? boundValue : null;
|
|
@@ -783,7 +927,10 @@ function getConsumptionState(auth, executionLog, profile) {
|
|
|
783
927
|
now
|
|
784
928
|
);
|
|
785
929
|
entries.push({
|
|
786
|
-
|
|
930
|
+
// displayName FIRST: description is prose meant for a human reading the
|
|
931
|
+
// profile, and rendering it here produced a paragraph where a label
|
|
932
|
+
// belongs ("Not currently enforced: reads carry no receipt… : 0 / 7").
|
|
933
|
+
label: fieldDef.displayName ?? fieldDef.description ?? fieldName,
|
|
787
934
|
current,
|
|
788
935
|
limit,
|
|
789
936
|
window: bt.window,
|
|
@@ -821,17 +968,17 @@ function formatConsumptionFull(entries) {
|
|
|
821
968
|
}
|
|
822
969
|
|
|
823
970
|
// src/lib/context-loader.ts
|
|
824
|
-
import { readFileSync as
|
|
825
|
-
import { homedir as
|
|
826
|
-
import { join as
|
|
827
|
-
var
|
|
971
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
972
|
+
import { homedir as homedir4 } from "os";
|
|
973
|
+
import { join as join4 } from "path";
|
|
974
|
+
var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join4(homedir4(), ".suveren");
|
|
828
975
|
var BRIEF_MAX_CHARS = 16 * 1024;
|
|
829
976
|
function readContextFile(dataDir) {
|
|
830
|
-
const dir = dataDir ??
|
|
831
|
-
const filePath =
|
|
832
|
-
if (!
|
|
977
|
+
const dir = dataDir ?? DEFAULT_DIR4;
|
|
978
|
+
const filePath = join4(dir, "context.md");
|
|
979
|
+
if (!existsSync4(filePath)) return null;
|
|
833
980
|
try {
|
|
834
|
-
const content =
|
|
981
|
+
const content = readFileSync4(filePath, "utf-8").trim();
|
|
835
982
|
return content || null;
|
|
836
983
|
} catch {
|
|
837
984
|
return null;
|
|
@@ -929,8 +1076,22 @@ function buildMandateBrief(opts) {
|
|
|
929
1076
|
return lines.join("\n");
|
|
930
1077
|
}
|
|
931
1078
|
|
|
1079
|
+
// src/lib/locked-notice.ts
|
|
1080
|
+
function uiUrl() {
|
|
1081
|
+
const port2 = process.env.SUVEREN_CP_PORT ?? "3400";
|
|
1082
|
+
return `http://localhost:${port2}`;
|
|
1083
|
+
}
|
|
1084
|
+
function lockedNotice(action) {
|
|
1085
|
+
const what = action ? `Cannot ${action}: the` : "The";
|
|
1086
|
+
return `${what} Suveren gateway is running but LOCKED, so it cannot read any authorizations or credentials.
|
|
1087
|
+
|
|
1088
|
+
This is not the same as having no authorizations \u2014 yours are on disk, encrypted, and unreadable until someone unlocks them.
|
|
1089
|
+
|
|
1090
|
+
TELL THE USER: open ${uiUrl()} and enter your Suveren API key to unlock the gateway. It boots locked by design after every restart; the key is never stored.`;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
932
1093
|
// src/tools/authorizations.ts
|
|
933
|
-
import { getProfile as
|
|
1094
|
+
import { getProfile as getProfile4 } from "@hap/core";
|
|
934
1095
|
|
|
935
1096
|
// src/lib/receipt-footer.ts
|
|
936
1097
|
import { deriveIdentityLine } from "@hap/core";
|
|
@@ -1012,7 +1173,168 @@ function attachReceiptId(tool, args, receiptId) {
|
|
|
1012
1173
|
return { ...args, receipt_id: receiptId };
|
|
1013
1174
|
}
|
|
1014
1175
|
|
|
1176
|
+
// src/lib/scope-specificity.ts
|
|
1177
|
+
function tokenSet(value) {
|
|
1178
|
+
if (value === void 0 || value === null) return /* @__PURE__ */ new Set();
|
|
1179
|
+
return new Set(
|
|
1180
|
+
String(value).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
function subsetOrEqual(a, b) {
|
|
1184
|
+
if (b.size === 0) return true;
|
|
1185
|
+
if (a.size === 0) return false;
|
|
1186
|
+
for (const x of a) if (!b.has(x)) return false;
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
function atLeastAsSpecific(keys, a, b) {
|
|
1190
|
+
for (const k of keys) {
|
|
1191
|
+
if (!subsetOrEqual(tokenSet(a[k]), tokenSet(b[k]))) return false;
|
|
1192
|
+
}
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
function strictlyMoreSpecific(keys, a, b) {
|
|
1196
|
+
return atLeastAsSpecific(keys, a, b) && !atLeastAsSpecific(keys, b, a);
|
|
1197
|
+
}
|
|
1198
|
+
function selectAuthorization(contextKeys, passers) {
|
|
1199
|
+
if (passers.length === 1) {
|
|
1200
|
+
return { chosen: passers[0], superseded: [], reason: "sole" };
|
|
1201
|
+
}
|
|
1202
|
+
const dominators = passers.filter(
|
|
1203
|
+
(a) => passers.every((b) => b === a || strictlyMoreSpecific(contextKeys, a.context, b.context))
|
|
1204
|
+
);
|
|
1205
|
+
if (dominators.length === 1) {
|
|
1206
|
+
const chosen2 = dominators[0];
|
|
1207
|
+
return { chosen: chosen2, superseded: passers.filter((p) => p !== chosen2), reason: "most-specific" };
|
|
1208
|
+
}
|
|
1209
|
+
const approvers = passers.filter((p) => p.requiresApproval);
|
|
1210
|
+
const pool = approvers.length > 0 ? approvers : passers;
|
|
1211
|
+
const chosen = pool.slice().sort((a, b) => a.id.localeCompare(b.id))[0];
|
|
1212
|
+
return {
|
|
1213
|
+
chosen,
|
|
1214
|
+
superseded: passers.filter((p) => p !== chosen),
|
|
1215
|
+
reason: approvers.length > 0 ? "fail-safe-approval" : "fail-safe-tiebreak"
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/lib/read-gate.ts
|
|
1220
|
+
function boundsSatisfyReadGate(bounds, gate) {
|
|
1221
|
+
if (!gate.boundField) return true;
|
|
1222
|
+
const actual = bounds?.[gate.boundField];
|
|
1223
|
+
return actual !== void 0 && String(actual) === gate.requiredValue;
|
|
1224
|
+
}
|
|
1225
|
+
function readToolIsGoverned(g) {
|
|
1226
|
+
if (!g) return false;
|
|
1227
|
+
return Boolean(g.boundField) || g.read != null || g.readGovernance === "none";
|
|
1228
|
+
}
|
|
1229
|
+
function allowedResources(contexts, boundField) {
|
|
1230
|
+
const out = /* @__PURE__ */ new Set();
|
|
1231
|
+
for (const ctx of contexts) {
|
|
1232
|
+
for (const v of tokenSet(ctx?.[boundField])) out.add(v);
|
|
1233
|
+
}
|
|
1234
|
+
return out;
|
|
1235
|
+
}
|
|
1236
|
+
function deniedResources(requested, allowed) {
|
|
1237
|
+
return requested.filter((r) => !allowed.has(r));
|
|
1238
|
+
}
|
|
1239
|
+
function filterItemsByResource(items, idPath, allowed) {
|
|
1240
|
+
return items.filter((it) => {
|
|
1241
|
+
const id = getByDottedPath(it, idPath);
|
|
1242
|
+
return typeof id === "string" && allowed.has(id);
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
function hasBlockedValue(item, valuesPath, blocked) {
|
|
1246
|
+
const v = getByDottedPath(item, valuesPath);
|
|
1247
|
+
const arr = Array.isArray(v) ? v : v != null ? [v] : [];
|
|
1248
|
+
return arr.some((x) => typeof x === "string" && blocked.has(x));
|
|
1249
|
+
}
|
|
1250
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
1251
|
+
function parseMessageTimestamp(value) {
|
|
1252
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1253
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
1254
|
+
const n = Number(value);
|
|
1255
|
+
if (Number.isFinite(n)) return n;
|
|
1256
|
+
const iso = Date.parse(value);
|
|
1257
|
+
if (Number.isFinite(iso)) return iso;
|
|
1258
|
+
}
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
function ageInDays(timestampMs, nowMs) {
|
|
1262
|
+
return Math.floor((nowMs - timestampMs) / MS_PER_DAY);
|
|
1263
|
+
}
|
|
1264
|
+
function maxReadAgeDays(boundsList, field) {
|
|
1265
|
+
let max = null;
|
|
1266
|
+
for (const b of boundsList) {
|
|
1267
|
+
const raw = b?.[field];
|
|
1268
|
+
const n = typeof raw === "number" ? raw : raw !== void 0 ? Number(raw) : NaN;
|
|
1269
|
+
if (Number.isFinite(n)) max = max === null ? n : Math.max(max, n);
|
|
1270
|
+
}
|
|
1271
|
+
return max;
|
|
1272
|
+
}
|
|
1273
|
+
function isOlderThanMaxAge(timestampMs, maxAgeDays, nowMs) {
|
|
1274
|
+
if (timestampMs === null) return true;
|
|
1275
|
+
return ageInDays(timestampMs, nowMs) > maxAgeDays;
|
|
1276
|
+
}
|
|
1277
|
+
var TRAILING_OPERATOR = /(?:^|\s)(?:OR|AND|NOT)$|[-+|&:]$/i;
|
|
1278
|
+
function parensBalanced(s) {
|
|
1279
|
+
let depth = 0;
|
|
1280
|
+
let inQuote = false;
|
|
1281
|
+
for (const c of s) {
|
|
1282
|
+
if (c === '"') {
|
|
1283
|
+
inQuote = !inQuote;
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
if (inQuote) continue;
|
|
1287
|
+
if (c === "(") depth++;
|
|
1288
|
+
else if (c === ")" && --depth < 0) return false;
|
|
1289
|
+
}
|
|
1290
|
+
return !inQuote && depth === 0;
|
|
1291
|
+
}
|
|
1292
|
+
function composeReadQuery(agentQuery, clauses) {
|
|
1293
|
+
const injected = clauses.filter((c) => typeof c === "string" && c.trim() !== "").map((c) => c.trim());
|
|
1294
|
+
const base = (agentQuery ?? "").trim();
|
|
1295
|
+
if (base === "") return { ok: true, query: injected.join(" ") };
|
|
1296
|
+
if (!parensBalanced(base)) {
|
|
1297
|
+
return { ok: false, reason: "the requested search query has unbalanced parentheses" };
|
|
1298
|
+
}
|
|
1299
|
+
if (TRAILING_OPERATOR.test(base)) {
|
|
1300
|
+
return { ok: false, reason: "the requested search query ends in a dangling operator" };
|
|
1301
|
+
}
|
|
1302
|
+
if (injected.length === 0) return { ok: true, query: base };
|
|
1303
|
+
return { ok: true, query: `(${base}) ${injected.join(" ")}` };
|
|
1304
|
+
}
|
|
1305
|
+
function detectAgeConflict(agentQuery, conflictPattern, maxAgeDays) {
|
|
1306
|
+
if (!agentQuery || !conflictPattern || maxAgeDays === null) return null;
|
|
1307
|
+
let re;
|
|
1308
|
+
try {
|
|
1309
|
+
re = new RegExp(conflictPattern, "gi");
|
|
1310
|
+
} catch {
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
let worst = null;
|
|
1314
|
+
for (const match of agentQuery.matchAll(re)) {
|
|
1315
|
+
const days = Number(match[1]);
|
|
1316
|
+
if (!Number.isFinite(days)) continue;
|
|
1317
|
+
if (days >= maxAgeDays && (worst === null || days > worst)) worst = days;
|
|
1318
|
+
}
|
|
1319
|
+
return worst;
|
|
1320
|
+
}
|
|
1321
|
+
function resolveAgeBoundField(boundsSchema, ageExecField) {
|
|
1322
|
+
const fields = boundsSchema?.fields;
|
|
1323
|
+
if (!fields) return null;
|
|
1324
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
1325
|
+
if (def?.boundType?.kind === "per_transaction" && def.boundType.of === ageExecField) return name;
|
|
1326
|
+
}
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
function getByDottedPath(obj, path) {
|
|
1330
|
+
return path.split(".").reduce(
|
|
1331
|
+
(o, k) => o == null ? void 0 : o[k],
|
|
1332
|
+
obj
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1015
1336
|
// src/lib/tool-proxy.ts
|
|
1337
|
+
import { getProfile as getProfile3 } from "@hap/core";
|
|
1016
1338
|
import { readFile } from "fs/promises";
|
|
1017
1339
|
import { extname } from "path";
|
|
1018
1340
|
import { randomUUID } from "crypto";
|
|
@@ -1075,6 +1397,30 @@ function applyMapping(m, value, execution) {
|
|
|
1075
1397
|
function profileMatches(profileId, shortName) {
|
|
1076
1398
|
return profileId === shortName || profileId.includes("/" + shortName + "@") || profileId.endsWith("/" + shortName);
|
|
1077
1399
|
}
|
|
1400
|
+
function parseFirstJson(result) {
|
|
1401
|
+
const text = result.content?.find((c) => c.type === "text")?.text;
|
|
1402
|
+
if (typeof text !== "string") return void 0;
|
|
1403
|
+
try {
|
|
1404
|
+
return JSON.parse(text);
|
|
1405
|
+
} catch {
|
|
1406
|
+
return void 0;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
function denyRead(state2, tool, reason, detail, target) {
|
|
1410
|
+
try {
|
|
1411
|
+
state2.denialLog?.record({
|
|
1412
|
+
ts: Date.now(),
|
|
1413
|
+
tool: tool.originalName,
|
|
1414
|
+
integrationId: tool.integrationId,
|
|
1415
|
+
profile: tool.gating?.profile ?? null,
|
|
1416
|
+
reason,
|
|
1417
|
+
detail,
|
|
1418
|
+
target
|
|
1419
|
+
});
|
|
1420
|
+
} catch {
|
|
1421
|
+
}
|
|
1422
|
+
return { content: [{ type: "text", text: `Read blocked by Gatekeeper: ${detail}` }], isError: true };
|
|
1423
|
+
}
|
|
1078
1424
|
function createGatedToolHandler(tool, integrationManager2, state2) {
|
|
1079
1425
|
if (!tool.gating || !tool.gating.profile) {
|
|
1080
1426
|
return async () => {
|
|
@@ -1088,8 +1434,37 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
|
|
|
1088
1434
|
};
|
|
1089
1435
|
}
|
|
1090
1436
|
const { profile, executionMapping, staticExecution, category } = tool.gating;
|
|
1437
|
+
const lockedGuard = (inner) => async (args) => {
|
|
1438
|
+
if (!state2.spClient.isUnlocked()) {
|
|
1439
|
+
return {
|
|
1440
|
+
content: [{ type: "text", text: lockedNotice(`use ${tool.namespacedName}`) }],
|
|
1441
|
+
isError: true
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
return inner(args);
|
|
1445
|
+
};
|
|
1446
|
+
if (category === "disabled") {
|
|
1447
|
+
return async () => ({
|
|
1448
|
+
content: [{
|
|
1449
|
+
type: "text",
|
|
1450
|
+
text: `Tool "${tool.namespacedName}" is disabled by the integration manifest and cannot be used.`
|
|
1451
|
+
}],
|
|
1452
|
+
isError: true
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1091
1455
|
if (category === "read") {
|
|
1092
|
-
|
|
1456
|
+
const readGate = { boundField: tool.gating.boundField, requiredValue: tool.gating.requiredValue };
|
|
1457
|
+
const readAdapter = tool.gating.read;
|
|
1458
|
+
const governed = readToolIsGoverned(tool.gating);
|
|
1459
|
+
return lockedGuard(async (args) => {
|
|
1460
|
+
if (!governed) {
|
|
1461
|
+
return denyRead(
|
|
1462
|
+
state2,
|
|
1463
|
+
tool,
|
|
1464
|
+
"ungoverned",
|
|
1465
|
+
`${tool.originalName} declares no read governance (no static gate, no read adapter, no explicit exemption), so the gateway cannot bound what it returns. This is a manifest defect \u2014 the tool must declare how its reads are limited before it can be used.`
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1093
1468
|
const auths = state2.getEnrichedAuthorizations();
|
|
1094
1469
|
const matchingAuths = auths.filter(
|
|
1095
1470
|
(a) => a.complete && profileMatches(a.profileId, profile)
|
|
@@ -1103,10 +1478,135 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
|
|
|
1103
1478
|
isError: true
|
|
1104
1479
|
};
|
|
1105
1480
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1481
|
+
if (readGate.boundField) {
|
|
1482
|
+
const permitted = matchingAuths.some(
|
|
1483
|
+
(a) => boundsSatisfyReadGate(a.bounds ?? a.frame, readGate)
|
|
1484
|
+
);
|
|
1485
|
+
if (!permitted) {
|
|
1486
|
+
return denyRead(
|
|
1487
|
+
state2,
|
|
1488
|
+
tool,
|
|
1489
|
+
"read_gate",
|
|
1490
|
+
`using ${tool.originalName} requires an authorization with "${readGate.boundField}: ${readGate.requiredValue}", but no active authorization grants it.`
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
const readProfile = readAdapter ? getProfile3(matchingAuths[0].profileId) : void 0;
|
|
1495
|
+
const boundsSchema = readProfile?.boundsSchema;
|
|
1496
|
+
const ageBoundField = readAdapter?.ageField ? resolveAgeBoundField(boundsSchema, readAdapter.ageField) : null;
|
|
1497
|
+
let readMaxAge = null;
|
|
1498
|
+
if (ageBoundField) {
|
|
1499
|
+
readMaxAge = maxReadAgeDays(
|
|
1500
|
+
matchingAuths.map((a) => a.bounds ?? a.frame),
|
|
1501
|
+
ageBoundField
|
|
1502
|
+
);
|
|
1503
|
+
if (readMaxAge === null) {
|
|
1504
|
+
return denyRead(
|
|
1505
|
+
state2,
|
|
1506
|
+
tool,
|
|
1507
|
+
"unset_age",
|
|
1508
|
+
`no read-age window is set on your authorization (${ageBoundField}). Reads are not permitted with an unbounded window \u2014 set how far back the agent may read, then try again. Nothing was read.`
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
let outgoing = args;
|
|
1513
|
+
if (readAdapter?.resourceBound && (readAdapter.resourceArg || readAdapter.resourceArrayArg)) {
|
|
1514
|
+
const allowed = allowedResources(
|
|
1515
|
+
matchingAuths.map((a) => a.context ?? {}),
|
|
1516
|
+
readAdapter.resourceBound
|
|
1517
|
+
);
|
|
1518
|
+
const isArray = Boolean(readAdapter.resourceArrayArg);
|
|
1519
|
+
const argName = readAdapter.resourceArrayArg ?? readAdapter.resourceArg;
|
|
1520
|
+
const raw = args[argName];
|
|
1521
|
+
let requested;
|
|
1522
|
+
if (isArray) {
|
|
1523
|
+
requested = Array.isArray(raw) && raw.length > 0 ? raw.map(String) : readAdapter.resourceDefault ? [readAdapter.resourceDefault] : [];
|
|
1524
|
+
} else {
|
|
1525
|
+
requested = typeof raw === "string" && raw.trim() !== "" ? [raw] : readAdapter.resourceDefault ? [readAdapter.resourceDefault] : [];
|
|
1526
|
+
}
|
|
1527
|
+
const denied = deniedResources(requested, allowed);
|
|
1528
|
+
if (requested.length === 0 || denied.length > 0) {
|
|
1529
|
+
const why = requested.length === 0 ? "no container was resolved for this read" : `not in your permitted set (${denied.join(", ")})`;
|
|
1530
|
+
return denyRead(
|
|
1531
|
+
state2,
|
|
1532
|
+
tool,
|
|
1533
|
+
"resource",
|
|
1534
|
+
`${tool.originalName} may only read the containers your authorization permits (${readAdapter.resourceBound}); the requested one is ${why}. Nothing was read.`,
|
|
1535
|
+
denied.length > 0 ? denied.join(", ") : void 0
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
outgoing = { ...args, [argName]: isArray ? requested : requested[0] };
|
|
1539
|
+
}
|
|
1540
|
+
if (readAdapter?.pinnedArgs) {
|
|
1541
|
+
outgoing = { ...outgoing, ...readAdapter.pinnedArgs };
|
|
1542
|
+
}
|
|
1543
|
+
if (readAdapter?.queryArg) {
|
|
1544
|
+
const clauses = [];
|
|
1545
|
+
if (ageBoundField && readAdapter.ageConstraint && readMaxAge !== null) {
|
|
1546
|
+
clauses.push(readAdapter.ageConstraint.replace("{days}", String(readMaxAge)));
|
|
1547
|
+
}
|
|
1548
|
+
if (clauses.length > 0) {
|
|
1549
|
+
const base = typeof args[readAdapter.queryArg] === "string" ? args[readAdapter.queryArg] : "";
|
|
1550
|
+
const conflictDays = detectAgeConflict(base, readAdapter.ageConflictPattern, readMaxAge);
|
|
1551
|
+
if (conflictDays !== null) {
|
|
1552
|
+
return denyRead(
|
|
1553
|
+
state2,
|
|
1554
|
+
tool,
|
|
1555
|
+
"age_window",
|
|
1556
|
+
`you asked for items older than ${conflictDays} days, but this authorization allows ${readMaxAge} days. Nothing outside that window can be read, so the search was not run \u2014 it would have returned an empty result that looks like "none exist". Items may well exist beyond ${readMaxAge} days; widen read_max_age_days if you need them.`
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
const composed = composeReadQuery(base, clauses);
|
|
1560
|
+
if (!composed.ok) {
|
|
1561
|
+
return denyRead(
|
|
1562
|
+
state2,
|
|
1563
|
+
tool,
|
|
1564
|
+
"query_unsafe",
|
|
1565
|
+
`${composed.reason}. The gateway must AND its read limits onto your search, and this query could not be safely combined. Re-send it without a trailing operator or unbalanced parenthesis.`
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
outgoing = { ...outgoing, [readAdapter.queryArg]: composed.query };
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
const result = await integrationManager2.callTool(tool.integrationId, tool.originalName, outgoing);
|
|
1572
|
+
if (readAdapter?.blockResultValues && readAdapter.resultValuesPath) {
|
|
1573
|
+
const parsed = parseFirstJson(result);
|
|
1574
|
+
const blocked = new Set(readAdapter.blockResultValues);
|
|
1575
|
+
if (parsed !== void 0 && hasBlockedValue(parsed, readAdapter.resultValuesPath, blocked)) {
|
|
1576
|
+
return denyRead(
|
|
1577
|
+
state2,
|
|
1578
|
+
tool,
|
|
1579
|
+
"spam",
|
|
1580
|
+
`this item is in a container your authorization excludes (e.g. spam/trash). Its contents were not returned.`
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
if (readAdapter && ageBoundField && readAdapter.resultDatePath && readMaxAge !== null) {
|
|
1585
|
+
const parsed = parseFirstJson(result);
|
|
1586
|
+
const rawDate = parsed === void 0 ? void 0 : getByDottedPath(parsed, readAdapter.resultDatePath);
|
|
1587
|
+
if (isOlderThanMaxAge(parseMessageTimestamp(rawDate), readMaxAge, Date.now())) {
|
|
1588
|
+
return denyRead(
|
|
1589
|
+
state2,
|
|
1590
|
+
tool,
|
|
1591
|
+
"age",
|
|
1592
|
+
`this item is older than the ${readMaxAge}-day read window your authorization grants (read_max_age_days). Its contents were not returned.`
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
if (readAdapter?.resourceBound && readAdapter.resultResourcePath && !readAdapter.resourceArg && !readAdapter.resourceArrayArg) {
|
|
1597
|
+
const allowed = allowedResources(
|
|
1598
|
+
matchingAuths.map((a) => a.context ?? {}),
|
|
1599
|
+
readAdapter.resourceBound
|
|
1600
|
+
);
|
|
1601
|
+
const parsed = parseFirstJson(result);
|
|
1602
|
+
const items = Array.isArray(parsed) ? parsed : [];
|
|
1603
|
+
const kept = filterItemsByResource(items, readAdapter.resultResourcePath, allowed);
|
|
1604
|
+
return { ...result, content: [{ type: "text", text: JSON.stringify(kept) }] };
|
|
1605
|
+
}
|
|
1606
|
+
return result;
|
|
1607
|
+
});
|
|
1108
1608
|
}
|
|
1109
|
-
return async (args) => {
|
|
1609
|
+
return lockedGuard(async (args) => {
|
|
1110
1610
|
const execution = { ...staticExecution };
|
|
1111
1611
|
for (const [argName, mapping] of Object.entries(executionMapping)) {
|
|
1112
1612
|
const value = args[argName];
|
|
@@ -1137,170 +1637,191 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
|
|
|
1137
1637
|
};
|
|
1138
1638
|
}
|
|
1139
1639
|
const errors = [];
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1640
|
+
const passers = [];
|
|
1641
|
+
for (const candidate of matchingAuths) {
|
|
1642
|
+
const { result } = await state2.gatekeeper.verifyExecution(candidate.authorizationId, execution, {
|
|
1643
|
+
bounds: candidate.bounds,
|
|
1644
|
+
context: candidate.context
|
|
1144
1645
|
});
|
|
1145
|
-
if (!result.approved) {
|
|
1146
|
-
}
|
|
1147
1646
|
if (result.approved) {
|
|
1148
|
-
|
|
1149
|
-
|
|
1647
|
+
passers.push(candidate);
|
|
1648
|
+
} else {
|
|
1649
|
+
const reasons = result.errors.map((e) => {
|
|
1650
|
+
if (e.code === "BOUND_EXCEEDED") {
|
|
1651
|
+
return `${candidate.path}: ${e.field}: ${e.message}`;
|
|
1652
|
+
}
|
|
1653
|
+
return `${candidate.path}: ${e.message}`;
|
|
1654
|
+
});
|
|
1655
|
+
errors.push(...reasons);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
if (passers.length === 0) {
|
|
1659
|
+
return {
|
|
1660
|
+
content: [{
|
|
1661
|
+
type: "text",
|
|
1662
|
+
text: `Tool call rejected by Gatekeeper. Tried ${matchingAuths.length} authorization(s):
|
|
1663
|
+
` + errors.map((e) => ` - ${e}`).join("\n")
|
|
1664
|
+
}],
|
|
1665
|
+
isError: true
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
const contextKeys = getProfile3(passers[0].profileId)?.contextSchema?.keyOrder ?? [];
|
|
1669
|
+
const selection = selectAuthorization(
|
|
1670
|
+
contextKeys,
|
|
1671
|
+
passers.map((a) => ({
|
|
1672
|
+
id: a.authorizationId,
|
|
1673
|
+
auth: a,
|
|
1674
|
+
context: a.context ?? {},
|
|
1675
|
+
requiresApproval: (a.deferredCommitmentDomains ?? []).length > 0
|
|
1676
|
+
}))
|
|
1677
|
+
);
|
|
1678
|
+
const auth = selection.chosen.auth;
|
|
1679
|
+
if (selection.superseded.length > 0) {
|
|
1680
|
+
console.error(
|
|
1681
|
+
`[Suveren MCP] selection(${tool.namespacedName}): chose ${auth.authorizationId} (${selection.reason}) over [${selection.superseded.map((s) => s.id).join(", ")}]`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
{
|
|
1685
|
+
const authzId = auth.authorizationId;
|
|
1686
|
+
if (isCommitmentDowngrade(auth)) {
|
|
1687
|
+
return {
|
|
1688
|
+
content: [{
|
|
1689
|
+
type: "text",
|
|
1690
|
+
text: `Refusing to execute ${tool.originalName}: the signed authorization requires review (commitment_mode="${auth.signedCommitmentMode}") but the Authority Server returned no pending approvers. This inconsistency (a possible commitment-mode downgrade) is rejected fail-closed. Re-fetch the authorization or contact the Authority Server operator.`
|
|
1691
|
+
}],
|
|
1692
|
+
isError: true
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
if ((auth.deferredCommitmentDomains ?? []).length > 0) {
|
|
1696
|
+
try {
|
|
1697
|
+
const enrichedArgs = await attachImagePreview(args);
|
|
1698
|
+
const { proposal } = await state2.spClient.submitProposal({
|
|
1699
|
+
authorizationId: authzId,
|
|
1700
|
+
profileId: auth.profileId,
|
|
1701
|
+
path: auth.path,
|
|
1702
|
+
pendingDomains: auth.deferredCommitmentDomains,
|
|
1703
|
+
tool: tool.namespacedName,
|
|
1704
|
+
toolArgs: enrichedArgs,
|
|
1705
|
+
executionContext: { ...execution }
|
|
1706
|
+
});
|
|
1150
1707
|
return {
|
|
1151
1708
|
content: [{
|
|
1152
1709
|
type: "text",
|
|
1153
|
-
text: `
|
|
1154
|
-
|
|
1710
|
+
text: `Awaiting commitment from domain${auth.deferredCommitmentDomains.length > 1 ? "s" : ""} "${auth.deferredCommitmentDomains.join('", "')}" for tool ${tool.originalName}.
|
|
1711
|
+
Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposal_id: "${proposal.id}").`
|
|
1712
|
+
}]
|
|
1713
|
+
};
|
|
1714
|
+
} catch (err) {
|
|
1715
|
+
return {
|
|
1716
|
+
content: [{ type: "text", text: `Failed to submit proposal: ${err instanceof Error ? err.message : String(err)}` }],
|
|
1155
1717
|
isError: true
|
|
1156
1718
|
};
|
|
1157
1719
|
}
|
|
1158
|
-
|
|
1720
|
+
}
|
|
1721
|
+
let receiptId;
|
|
1722
|
+
try {
|
|
1723
|
+
const actionType = typeof execution.action_type === "string" ? execution.action_type : void 0;
|
|
1724
|
+
if (!actionType) {
|
|
1725
|
+
console.error(
|
|
1726
|
+
`[Suveren MCP] Warning: tool ${tool.namespacedName} has no action_type in staticExecution. Bounds check may be skipped. Fix the integration manifest.`
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
const binding = computeContentBinding(auth.profileId, tool, args);
|
|
1730
|
+
const { receipt } = await state2.spClient.postReceipt({
|
|
1731
|
+
authorizationId: authzId,
|
|
1732
|
+
// Optional cross-check — the AS fails closed on a mismatch.
|
|
1733
|
+
boundsHash: auth.boundsHash,
|
|
1734
|
+
profileId: auth.profileId,
|
|
1735
|
+
action: tool.namespacedName,
|
|
1736
|
+
actionType,
|
|
1737
|
+
executionContext: { ...execution },
|
|
1738
|
+
amount: typeof execution.amount === "number" ? execution.amount : void 0,
|
|
1739
|
+
idempotencyKey: randomUUID(),
|
|
1740
|
+
...binding ?? {}
|
|
1741
|
+
});
|
|
1742
|
+
receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
if (err instanceof SPReceiptError && err.statusCode === 409) {
|
|
1745
|
+
const spBody = err.body;
|
|
1746
|
+
let pendingApprovers = spBody.approvers ?? [];
|
|
1747
|
+
if (pendingApprovers.length === 0) {
|
|
1748
|
+
try {
|
|
1749
|
+
const summary = await state2.spClient.getAuthorizationSummary(authzId);
|
|
1750
|
+
if (summary?.approvers_frozen) {
|
|
1751
|
+
pendingApprovers = summary.approvers_frozen;
|
|
1752
|
+
}
|
|
1753
|
+
if (summary?.created_by) {
|
|
1754
|
+
pendingApprovers = [summary.created_by, ...pendingApprovers];
|
|
1755
|
+
}
|
|
1756
|
+
} catch {
|
|
1757
|
+
}
|
|
1758
|
+
} else {
|
|
1759
|
+
try {
|
|
1760
|
+
const summary = await state2.spClient.getAuthorizationSummary(authzId);
|
|
1761
|
+
if (summary?.created_by) {
|
|
1762
|
+
pendingApprovers = [summary.created_by, ...pendingApprovers];
|
|
1763
|
+
}
|
|
1764
|
+
} catch {
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
const uniqueApprovers = [...new Set(pendingApprovers)];
|
|
1159
1768
|
try {
|
|
1160
1769
|
const enrichedArgs = await attachImagePreview(args);
|
|
1161
1770
|
const { proposal } = await state2.spClient.submitProposal({
|
|
1162
1771
|
authorizationId: authzId,
|
|
1163
1772
|
profileId: auth.profileId,
|
|
1164
1773
|
path: auth.path,
|
|
1165
|
-
pendingDomains:
|
|
1774
|
+
pendingDomains: [],
|
|
1166
1775
|
tool: tool.namespacedName,
|
|
1167
1776
|
toolArgs: enrichedArgs,
|
|
1168
|
-
executionContext: { ...execution }
|
|
1777
|
+
executionContext: { ...execution },
|
|
1778
|
+
pendingApprovers: uniqueApprovers
|
|
1169
1779
|
});
|
|
1170
1780
|
return {
|
|
1171
1781
|
content: [{
|
|
1172
1782
|
type: "text",
|
|
1173
|
-
text: `
|
|
1174
|
-
Proposal ID: ${proposal.id}.
|
|
1783
|
+
text: `Action exceeds team cap. Approval required from ${uniqueApprovers.length} reviewer(s).
|
|
1784
|
+
Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
|
|
1175
1785
|
}]
|
|
1176
1786
|
};
|
|
1177
|
-
} catch (
|
|
1787
|
+
} catch (proposalErr) {
|
|
1178
1788
|
return {
|
|
1179
|
-
content: [{ type: "text", text: `Failed to submit proposal: ${
|
|
1789
|
+
content: [{ type: "text", text: `Failed to submit approval proposal: ${proposalErr instanceof Error ? proposalErr.message : String(proposalErr)}` }],
|
|
1180
1790
|
isError: true
|
|
1181
1791
|
};
|
|
1182
1792
|
}
|
|
1183
1793
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
const { receipt } = await state2.spClient.postReceipt({
|
|
1194
|
-
authorizationId: authzId,
|
|
1195
|
-
// Optional cross-check — the AS fails closed on a mismatch.
|
|
1196
|
-
boundsHash: auth.boundsHash,
|
|
1197
|
-
profileId: auth.profileId,
|
|
1198
|
-
action: tool.namespacedName,
|
|
1199
|
-
actionType,
|
|
1200
|
-
executionContext: { ...execution },
|
|
1201
|
-
amount: typeof execution.amount === "number" ? execution.amount : void 0,
|
|
1202
|
-
idempotencyKey: randomUUID(),
|
|
1203
|
-
...binding ?? {}
|
|
1204
|
-
});
|
|
1205
|
-
receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
|
|
1206
|
-
} catch (err) {
|
|
1207
|
-
if (err instanceof SPReceiptError && err.statusCode === 409) {
|
|
1208
|
-
const spBody = err.body;
|
|
1209
|
-
let pendingApprovers = spBody.approvers ?? [];
|
|
1210
|
-
if (pendingApprovers.length === 0) {
|
|
1211
|
-
try {
|
|
1212
|
-
const summary = await state2.spClient.getAuthorizationSummary(authzId);
|
|
1213
|
-
if (summary?.approvers_frozen) {
|
|
1214
|
-
pendingApprovers = summary.approvers_frozen;
|
|
1215
|
-
}
|
|
1216
|
-
if (summary?.created_by) {
|
|
1217
|
-
pendingApprovers = [summary.created_by, ...pendingApprovers];
|
|
1218
|
-
}
|
|
1219
|
-
} catch {
|
|
1220
|
-
}
|
|
1221
|
-
} else {
|
|
1222
|
-
try {
|
|
1223
|
-
const summary = await state2.spClient.getAuthorizationSummary(authzId);
|
|
1224
|
-
if (summary?.created_by) {
|
|
1225
|
-
pendingApprovers = [summary.created_by, ...pendingApprovers];
|
|
1226
|
-
}
|
|
1227
|
-
} catch {
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
const uniqueApprovers = [...new Set(pendingApprovers)];
|
|
1231
|
-
try {
|
|
1232
|
-
const enrichedArgs = await attachImagePreview(args);
|
|
1233
|
-
const { proposal } = await state2.spClient.submitProposal({
|
|
1234
|
-
authorizationId: authzId,
|
|
1235
|
-
profileId: auth.profileId,
|
|
1236
|
-
path: auth.path,
|
|
1237
|
-
pendingDomains: [],
|
|
1238
|
-
tool: tool.namespacedName,
|
|
1239
|
-
toolArgs: enrichedArgs,
|
|
1240
|
-
executionContext: { ...execution },
|
|
1241
|
-
pendingApprovers: uniqueApprovers
|
|
1242
|
-
});
|
|
1243
|
-
return {
|
|
1244
|
-
content: [{
|
|
1245
|
-
type: "text",
|
|
1246
|
-
text: `Action exceeds team cap. Approval required from ${uniqueApprovers.length} reviewer(s).
|
|
1247
|
-
Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
|
|
1248
|
-
}]
|
|
1249
|
-
};
|
|
1250
|
-
} catch (proposalErr) {
|
|
1251
|
-
return {
|
|
1252
|
-
content: [{ type: "text", text: `Failed to submit approval proposal: ${proposalErr instanceof Error ? proposalErr.message : String(proposalErr)}` }],
|
|
1253
|
-
isError: true
|
|
1254
|
-
};
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
if (err instanceof SPReceiptError && err.statusCode === 422) {
|
|
1258
|
-
return {
|
|
1259
|
-
content: [{ type: "text", text: `Action blocked: ${err.message} (hard team ceiling \u2014 contact the team admin)` }],
|
|
1260
|
-
isError: true
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
if (err instanceof SPReceiptError && err.statusCode === 403) {
|
|
1264
|
-
if (/revoked/i.test(err.message)) {
|
|
1265
|
-
state2.cache.invalidate(auth.authorizationId);
|
|
1266
|
-
}
|
|
1267
|
-
return {
|
|
1268
|
-
content: [{ type: "text", text: `Blocked by SP: ${err.message}` }],
|
|
1269
|
-
isError: true
|
|
1270
|
-
};
|
|
1794
|
+
if (err instanceof SPReceiptError && err.statusCode === 422) {
|
|
1795
|
+
return {
|
|
1796
|
+
content: [{ type: "text", text: `Action blocked: ${err.message} (hard team ceiling \u2014 contact the team admin)` }],
|
|
1797
|
+
isError: true
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
if (err instanceof SPReceiptError && err.statusCode === 403) {
|
|
1801
|
+
if (/revoked/i.test(err.message)) {
|
|
1802
|
+
state2.cache.invalidate(auth.authorizationId);
|
|
1271
1803
|
}
|
|
1272
1804
|
return {
|
|
1273
|
-
content: [{ type: "text", text: `
|
|
1805
|
+
content: [{ type: "text", text: `Blocked by SP: ${err.message}` }],
|
|
1274
1806
|
isError: true
|
|
1275
1807
|
};
|
|
1276
1808
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
timestamp: Math.floor(Date.now() / 1e3)
|
|
1282
|
-
});
|
|
1283
|
-
let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
|
|
1284
|
-
if (receiptId) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId);
|
|
1285
|
-
return integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
|
|
1809
|
+
return {
|
|
1810
|
+
content: [{ type: "text", text: `SP unavailable \u2014 tool call blocked. ${err instanceof Error ? err.message : ""}` }],
|
|
1811
|
+
isError: true
|
|
1812
|
+
};
|
|
1286
1813
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1814
|
+
state2.executionLog.record({
|
|
1815
|
+
profileId: auth.profileId,
|
|
1816
|
+
path: auth.path,
|
|
1817
|
+
execution: { ...execution },
|
|
1818
|
+
timestamp: Math.floor(Date.now() / 1e3)
|
|
1292
1819
|
});
|
|
1293
|
-
|
|
1820
|
+
let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
|
|
1821
|
+
if (receiptId) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId);
|
|
1822
|
+
return integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
|
|
1294
1823
|
}
|
|
1295
|
-
|
|
1296
|
-
content: [{
|
|
1297
|
-
type: "text",
|
|
1298
|
-
text: `Tool call rejected by Gatekeeper. Tried ${matchingAuths.length} authorization(s):
|
|
1299
|
-
` + errors.map((e) => ` - ${e}`).join("\n")
|
|
1300
|
-
}],
|
|
1301
|
-
isError: true
|
|
1302
|
-
};
|
|
1303
|
-
};
|
|
1824
|
+
});
|
|
1304
1825
|
}
|
|
1305
1826
|
function buildProxiedToolDescription(tool, state2) {
|
|
1306
1827
|
if (!tool.gating || !tool.gating.profile) {
|
|
@@ -1392,6 +1913,9 @@ function buildCapabilityMap(profileId, toolGating, integrationManager2) {
|
|
|
1392
1913
|
}
|
|
1393
1914
|
function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
1394
1915
|
return async (args) => {
|
|
1916
|
+
if (!state2.spClient.isUnlocked()) {
|
|
1917
|
+
return { content: [{ type: "text", text: lockedNotice("list authorizations") }] };
|
|
1918
|
+
}
|
|
1395
1919
|
const authorizations = state2.getEnrichedAuthorizations();
|
|
1396
1920
|
const now = Math.floor(Date.now() / 1e3);
|
|
1397
1921
|
const domain = args?.domain;
|
|
@@ -1435,7 +1959,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
|
1435
1959
|
} catch {
|
|
1436
1960
|
}
|
|
1437
1961
|
const shortName = shortProfileName2(auth.profileId);
|
|
1438
|
-
const profile =
|
|
1962
|
+
const profile = getProfile4(auth.profileId) ?? getProfile4(shortName);
|
|
1439
1963
|
if (profile) {
|
|
1440
1964
|
const consumption = getConsumptionState(auth, state2.executionLog, profile);
|
|
1441
1965
|
const consumptionText = formatConsumptionFull(consumption);
|
|
@@ -1491,7 +2015,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
|
1491
2015
|
lines.push(" Mode: review (each action requires your approval)");
|
|
1492
2016
|
}
|
|
1493
2017
|
const shortName = shortProfileName2(auth.profileId);
|
|
1494
|
-
const profile =
|
|
2018
|
+
const profile = getProfile4(auth.profileId) ?? getProfile4(shortName);
|
|
1495
2019
|
if (profile) {
|
|
1496
2020
|
const consumption = getConsumptionState(auth, state2.executionLog, profile);
|
|
1497
2021
|
const compact = formatConsumptionCompact(consumption);
|
|
@@ -1583,6 +2107,9 @@ function shortProfileName3(profileId) {
|
|
|
1583
2107
|
}
|
|
1584
2108
|
function listIntegrationsHandler(state2, integrationManager2) {
|
|
1585
2109
|
return async () => {
|
|
2110
|
+
if (!state2.spClient.isUnlocked()) {
|
|
2111
|
+
return { content: [{ type: "text", text: lockedNotice("list integrations") }] };
|
|
2112
|
+
}
|
|
1586
2113
|
if (!integrationManager2) {
|
|
1587
2114
|
return { content: [{ type: "text", text: "No integration manager available." }] };
|
|
1588
2115
|
}
|
|
@@ -1700,6 +2227,9 @@ Result: ${resultText}` };
|
|
|
1700
2227
|
}
|
|
1701
2228
|
function checkPendingCommitmentsHandler(state2, integrationManager2) {
|
|
1702
2229
|
return async (args) => {
|
|
2230
|
+
if (!state2.spClient.isUnlocked()) {
|
|
2231
|
+
return { content: [{ type: "text", text: lockedNotice("check commitments") }] };
|
|
2232
|
+
}
|
|
1703
2233
|
try {
|
|
1704
2234
|
if (args.proposal_id) {
|
|
1705
2235
|
const committed2 = await state2.spClient.getCommittedProposals();
|
|
@@ -1938,16 +2468,16 @@ function verifyGateContentHashes(content, auth) {
|
|
|
1938
2468
|
}
|
|
1939
2469
|
|
|
1940
2470
|
// src/lib/integration-registry.ts
|
|
1941
|
-
import { mkdirSync as
|
|
1942
|
-
import { homedir as
|
|
1943
|
-
import { dirname as
|
|
1944
|
-
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ??
|
|
2471
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
|
|
2472
|
+
import { homedir as homedir5 } from "os";
|
|
2473
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
2474
|
+
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join5(homedir5(), ".suveren");
|
|
1945
2475
|
var IntegrationRegistry = class {
|
|
1946
2476
|
integrations = /* @__PURE__ */ new Map();
|
|
1947
2477
|
filePath;
|
|
1948
2478
|
constructor(dataDir) {
|
|
1949
2479
|
const dir = dataDir ?? DEFAULT_DATA_DIR;
|
|
1950
|
-
this.filePath =
|
|
2480
|
+
this.filePath = join5(dir, "integrations.json");
|
|
1951
2481
|
this.load();
|
|
1952
2482
|
}
|
|
1953
2483
|
// ─── CRUD ───────────────────────────────────────────────────────────────
|
|
@@ -1978,12 +2508,12 @@ var IntegrationRegistry = class {
|
|
|
1978
2508
|
}
|
|
1979
2509
|
// ─── Load / Save ────────────────────────────────────────────────────────
|
|
1980
2510
|
load() {
|
|
1981
|
-
if (!
|
|
1982
|
-
|
|
2511
|
+
if (!existsSync5(this.filePath)) {
|
|
2512
|
+
mkdirSync4(dirname4(this.filePath), { recursive: true });
|
|
1983
2513
|
return;
|
|
1984
2514
|
}
|
|
1985
2515
|
try {
|
|
1986
|
-
const raw =
|
|
2516
|
+
const raw = readFileSync5(this.filePath, "utf-8");
|
|
1987
2517
|
const data = JSON.parse(raw);
|
|
1988
2518
|
const migrated = data.integrations.map((i) => {
|
|
1989
2519
|
if ("toolGating" in i && !("profile" in i)) {
|
|
@@ -2012,37 +2542,38 @@ var IntegrationRegistry = class {
|
|
|
2012
2542
|
version: 1,
|
|
2013
2543
|
integrations: Array.from(this.integrations.values())
|
|
2014
2544
|
};
|
|
2015
|
-
|
|
2016
|
-
|
|
2545
|
+
mkdirSync4(dirname4(this.filePath), { recursive: true });
|
|
2546
|
+
writeFileSync4(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
|
|
2017
2547
|
}
|
|
2018
2548
|
};
|
|
2019
2549
|
|
|
2020
2550
|
// src/lib/integration-manager.ts
|
|
2021
|
-
import { homedir as
|
|
2022
|
-
import { join as
|
|
2023
|
-
import { existsSync as
|
|
2024
|
-
import {
|
|
2551
|
+
import { homedir as homedir6 } from "os";
|
|
2552
|
+
import { join as join6, delimiter } from "path";
|
|
2553
|
+
import { existsSync as existsSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync } from "fs";
|
|
2554
|
+
import { execFile } from "child_process";
|
|
2025
2555
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2026
2556
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2027
|
-
import { getProfile as
|
|
2028
|
-
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ??
|
|
2029
|
-
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ??
|
|
2030
|
-
var INTEGRATIONS_BIN =
|
|
2557
|
+
import { getProfile as getProfile5 } from "@hap/core";
|
|
2558
|
+
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join6(homedir6(), ".suveren");
|
|
2559
|
+
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join6(DEFAULT_DATA_DIR2, "integrations");
|
|
2560
|
+
var INTEGRATIONS_BIN = join6(INTEGRATIONS_DIR, "node_modules", ".bin");
|
|
2031
2561
|
function buildPath() {
|
|
2032
2562
|
const base = process.env.PATH ?? "";
|
|
2033
|
-
return [INTEGRATIONS_BIN, base].join(
|
|
2563
|
+
return [INTEGRATIONS_BIN, base].join(delimiter);
|
|
2034
2564
|
}
|
|
2035
2565
|
function ensureIntegrationsDir() {
|
|
2036
|
-
if (!
|
|
2037
|
-
|
|
2566
|
+
if (!existsSync6(INTEGRATIONS_DIR)) {
|
|
2567
|
+
mkdirSync5(INTEGRATIONS_DIR, { recursive: true });
|
|
2038
2568
|
}
|
|
2039
|
-
const pkgPath =
|
|
2040
|
-
if (!
|
|
2041
|
-
|
|
2569
|
+
const pkgPath = join6(INTEGRATIONS_DIR, "package.json");
|
|
2570
|
+
if (!existsSync6(pkgPath)) {
|
|
2571
|
+
writeFileSync5(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
|
|
2042
2572
|
}
|
|
2043
2573
|
}
|
|
2044
2574
|
var RESPAWN_DELAYS = [2e3, 4e3, 6e3];
|
|
2045
2575
|
var MAX_RESPAWN_ATTEMPTS = 3;
|
|
2576
|
+
var CONNECT_TIMEOUT_MS = 3e4;
|
|
2046
2577
|
var IntegrationManager = class {
|
|
2047
2578
|
constructor(serviceCredentials2) {
|
|
2048
2579
|
this.serviceCredentials = serviceCredentials2;
|
|
@@ -2057,25 +2588,141 @@ var IntegrationManager = class {
|
|
|
2057
2588
|
this.onToolsChanged = cb;
|
|
2058
2589
|
}
|
|
2059
2590
|
/**
|
|
2060
|
-
*
|
|
2061
|
-
*
|
|
2591
|
+
* Is `npmPackage` present AND usable?
|
|
2592
|
+
*
|
|
2593
|
+
* The directory existing is not enough: an install that was interrupted
|
|
2594
|
+
* (timeout, Ctrl+C, antivirus lock) leaves the package directory behind
|
|
2595
|
+
* with no package.json and no bin shim. Treating that as "installed" made
|
|
2596
|
+
* every later start fail instantly on a missing binary, with no way to
|
|
2597
|
+
* self-repair — the user saw "Not running" and a Start button that did
|
|
2598
|
+
* nothing, forever. So we verify the manifest parses and its entry points
|
|
2599
|
+
* exist, and treat anything else as not-installed (and reinstallable).
|
|
2062
2600
|
*/
|
|
2063
|
-
|
|
2601
|
+
isUsableInstall(npmPackage) {
|
|
2602
|
+
const pkgDir = join6(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
2603
|
+
const pkgJson = join6(pkgDir, "package.json");
|
|
2604
|
+
if (!existsSync6(pkgDir) || !existsSync6(pkgJson)) return false;
|
|
2605
|
+
try {
|
|
2606
|
+
const pkg = JSON.parse(readFileSync6(pkgJson, "utf8"));
|
|
2607
|
+
const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
|
|
2608
|
+
const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
|
|
2609
|
+
const binEntries = Object.entries(binMap);
|
|
2610
|
+
for (const [binName, target] of binEntries) {
|
|
2611
|
+
if (!existsSync6(join6(pkgDir, target))) return false;
|
|
2612
|
+
if (!this.binShimExists(binName)) return false;
|
|
2613
|
+
}
|
|
2614
|
+
if (binEntries.length === 0 && pkg.main && !existsSync6(join6(pkgDir, pkg.main))) return false;
|
|
2615
|
+
return true;
|
|
2616
|
+
} catch {
|
|
2617
|
+
return false;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Does the node_modules/.bin shim for `binName` exist? On Windows npm writes
|
|
2622
|
+
* `<name>`, `<name>.cmd`, and `<name>.ps1`; the .cmd variant is what actually
|
|
2623
|
+
* runs, so any of them present counts.
|
|
2624
|
+
*/
|
|
2625
|
+
binShimExists(binName) {
|
|
2626
|
+
if (!binName) return false;
|
|
2627
|
+
const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
|
|
2628
|
+
return candidates.some((c) => existsSync6(join6(INTEGRATIONS_BIN, c)));
|
|
2629
|
+
}
|
|
2630
|
+
/**
|
|
2631
|
+
* Install an npm package into the managed integrations directory if not
|
|
2632
|
+
* already present. Called automatically before spawning when
|
|
2633
|
+
* config.npmPackage is set.
|
|
2634
|
+
*
|
|
2635
|
+
* Asynchronous ON PURPOSE. This used to be execSync, which blocked the MCP
|
|
2636
|
+
* server's event loop for the entire install — the port stayed bound but
|
|
2637
|
+
* nothing was answered, so the control plane's /internal/manifests call
|
|
2638
|
+
* failed and the UI showed "Couldn't load integrations". Measured on clean
|
|
2639
|
+
* CI runners: ~3.6s (Linux), ~5.4s (macOS), ~14s (Windows), and far worse
|
|
2640
|
+
* on machines with real-time antivirus.
|
|
2641
|
+
*/
|
|
2642
|
+
async ensureInstalled(npmPackage) {
|
|
2064
2643
|
ensureIntegrationsDir();
|
|
2065
|
-
|
|
2066
|
-
const
|
|
2067
|
-
if (
|
|
2644
|
+
if (this.isUsableInstall(npmPackage)) return;
|
|
2645
|
+
const pkgDir = join6(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
2646
|
+
if (existsSync6(pkgDir)) {
|
|
2647
|
+
console.error(`[IntegrationManager] Removing unusable install of ${npmPackage}`);
|
|
2648
|
+
try {
|
|
2649
|
+
rmSync(pkgDir, { recursive: true, force: true });
|
|
2650
|
+
} catch (err) {
|
|
2651
|
+
console.error(`[IntegrationManager] Could not clean ${pkgDir}:`, err);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
if (!/^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i.test(npmPackage)) {
|
|
2655
|
+
throw new Error(`Refusing to install unsafe package name: ${JSON.stringify(npmPackage)}`);
|
|
2656
|
+
}
|
|
2068
2657
|
console.error(`[IntegrationManager] Installing ${npmPackage}...`);
|
|
2658
|
+
const startedAt = Date.now();
|
|
2659
|
+
const isWin = process.platform === "win32";
|
|
2660
|
+
await new Promise((resolve3, reject) => {
|
|
2661
|
+
const child = execFile(
|
|
2662
|
+
"npm",
|
|
2663
|
+
["install", "--no-fund", "--no-audit", npmPackage],
|
|
2664
|
+
{
|
|
2665
|
+
cwd: INTEGRATIONS_DIR,
|
|
2666
|
+
// Windows: a shell so cmd.exe finds npm.cmd (see EINVAL note above).
|
|
2667
|
+
// POSIX: no shell — npm is a normal executable and keeping shell off
|
|
2668
|
+
// means the validated package name is still passed as a bare argv.
|
|
2669
|
+
shell: isWin,
|
|
2670
|
+
// No timeout: a slow install on a machine with antivirus is not an
|
|
2671
|
+
// error, and killing it midway is what created broken installs in
|
|
2672
|
+
// the first place. Progress is visible in the log.
|
|
2673
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2674
|
+
windowsHide: true
|
|
2675
|
+
},
|
|
2676
|
+
(err, _stdout, stderr) => {
|
|
2677
|
+
if (err) {
|
|
2678
|
+
reject(new Error(`Failed to install ${npmPackage}: ${err.message}${stderr ? ` \u2014 ${stderr.trim()}` : ""}`));
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
resolve3();
|
|
2682
|
+
}
|
|
2683
|
+
);
|
|
2684
|
+
child.on("error", (err) => reject(new Error(`Failed to run npm for ${npmPackage}: ${err.message}`)));
|
|
2685
|
+
});
|
|
2686
|
+
if (!this.isUsableInstall(npmPackage)) {
|
|
2687
|
+
throw new Error(
|
|
2688
|
+
`Installed ${npmPackage} but it is not usable \u2014 package.json or its bin target is missing. Check ${INTEGRATIONS_DIR} for a partial install.`
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
console.error(`[IntegrationManager] Installed ${npmPackage} (${((Date.now() - startedAt) / 1e3).toFixed(1)}s)`);
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Connect a client to its transport, but never wait indefinitely. Resolves
|
|
2695
|
+
* on a successful handshake; rejects if the transport errors (child crash /
|
|
2696
|
+
* spawn failure) or the handshake exceeds CONNECT_TIMEOUT_MS. On failure the
|
|
2697
|
+
* transport is closed so the child process can't linger.
|
|
2698
|
+
*/
|
|
2699
|
+
async connectWithGuard(client, transport, config) {
|
|
2700
|
+
let timer;
|
|
2701
|
+
let settled = false;
|
|
2702
|
+
const guard = new Promise((_resolve, reject) => {
|
|
2703
|
+
timer = setTimeout(() => {
|
|
2704
|
+
if (settled) return;
|
|
2705
|
+
reject(new Error(
|
|
2706
|
+
`Timed out after ${CONNECT_TIMEOUT_MS}ms connecting to ${config.id} (${config.command} ${config.args.join(" ")}) \u2014 the downstream server did not complete the MCP handshake. It may have crashed on startup.`
|
|
2707
|
+
));
|
|
2708
|
+
}, CONNECT_TIMEOUT_MS);
|
|
2709
|
+
transport.onerror = (err) => {
|
|
2710
|
+
if (settled) return;
|
|
2711
|
+
reject(new Error(`Transport error while connecting to ${config.id}: ${err.message}`));
|
|
2712
|
+
};
|
|
2713
|
+
});
|
|
2069
2714
|
try {
|
|
2070
|
-
|
|
2071
|
-
cwd: INTEGRATIONS_DIR,
|
|
2072
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2073
|
-
timeout: 12e4
|
|
2074
|
-
});
|
|
2075
|
-
console.error(`[IntegrationManager] Installed ${npmPackage}`);
|
|
2715
|
+
await Promise.race([client.connect(transport), guard]);
|
|
2076
2716
|
} catch (err) {
|
|
2077
|
-
|
|
2078
|
-
|
|
2717
|
+
try {
|
|
2718
|
+
await transport.close();
|
|
2719
|
+
} catch {
|
|
2720
|
+
}
|
|
2721
|
+
throw err;
|
|
2722
|
+
} finally {
|
|
2723
|
+
settled = true;
|
|
2724
|
+
if (timer) clearTimeout(timer);
|
|
2725
|
+
transport.onerror = void 0;
|
|
2079
2726
|
}
|
|
2080
2727
|
}
|
|
2081
2728
|
/**
|
|
@@ -2088,12 +2735,14 @@ var IntegrationManager = class {
|
|
|
2088
2735
|
await this.stopIntegration(config.id);
|
|
2089
2736
|
}
|
|
2090
2737
|
if (config.npmPackage) {
|
|
2091
|
-
this.ensureInstalled(config.npmPackage);
|
|
2738
|
+
await this.ensureInstalled(config.npmPackage);
|
|
2092
2739
|
}
|
|
2093
2740
|
const env = this.resolveEnvKeys(config);
|
|
2741
|
+
const interpolate = (s) => s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => env[name] ?? process.env[name] ?? "");
|
|
2742
|
+
const args = config.args.map(interpolate);
|
|
2094
2743
|
const transport = new StdioClientTransport({
|
|
2095
2744
|
command: config.command,
|
|
2096
|
-
args
|
|
2745
|
+
args,
|
|
2097
2746
|
env: {
|
|
2098
2747
|
...process.env,
|
|
2099
2748
|
PATH: buildPath(),
|
|
@@ -2106,10 +2755,10 @@ var IntegrationManager = class {
|
|
|
2106
2755
|
{ name: "suveren-gateway", version: "0.1.0" },
|
|
2107
2756
|
{ capabilities: {} }
|
|
2108
2757
|
);
|
|
2109
|
-
await
|
|
2758
|
+
await this.connectWithGuard(client, transport, config);
|
|
2110
2759
|
console.error(`[IntegrationManager] Connected to ${config.id} (${config.command} ${config.args.join(" ")})`);
|
|
2111
2760
|
const toolsResult = await client.listTools();
|
|
2112
|
-
const profileGating = config.toolGating ?? (config.profile ?
|
|
2761
|
+
const profileGating = config.toolGating ?? (config.profile ? getProfile5(config.profile)?.toolGating ?? null : null);
|
|
2113
2762
|
const tools = (toolsResult.tools ?? []).map((tool) => {
|
|
2114
2763
|
const gating = this.resolveToolGating(config.profile, profileGating, tool.name);
|
|
2115
2764
|
return {
|
|
@@ -2188,6 +2837,16 @@ var IntegrationManager = class {
|
|
|
2188
2837
|
}
|
|
2189
2838
|
return tools;
|
|
2190
2839
|
}
|
|
2840
|
+
/**
|
|
2841
|
+
* RESERVED / NOT YET WIRED (see IntegrationConfig.readAgeDays). The local
|
|
2842
|
+
* read-age window (days) for a running integration, for a deferred feature
|
|
2843
|
+
* that moves the read age off the signed grant. The read path does NOT call
|
|
2844
|
+
* this yet — it still enforces the signed `read_max_age_days` bound.
|
|
2845
|
+
*/
|
|
2846
|
+
getReadAgeDays(integrationId) {
|
|
2847
|
+
const days = this.running.get(integrationId)?.config.readAgeDays;
|
|
2848
|
+
return typeof days === "number" && Number.isFinite(days) ? days : null;
|
|
2849
|
+
}
|
|
2191
2850
|
/**
|
|
2192
2851
|
* Get status info for all known integrations.
|
|
2193
2852
|
*/
|
|
@@ -2279,11 +2938,20 @@ var IntegrationManager = class {
|
|
|
2279
2938
|
if (!profileId || !profileGating) return null;
|
|
2280
2939
|
if (profileGating.overrides && toolName in profileGating.overrides) {
|
|
2281
2940
|
const override = profileGating.overrides[toolName];
|
|
2282
|
-
|
|
2941
|
+
const ext = override ?? {};
|
|
2942
|
+
if (ext.category === "disabled") {
|
|
2943
|
+
return { profile: profileId, executionMapping: {}, category: "disabled" };
|
|
2944
|
+
}
|
|
2945
|
+
if (override === null || ext.category === "read") {
|
|
2283
2946
|
return {
|
|
2284
2947
|
profile: profileId,
|
|
2285
2948
|
executionMapping: {},
|
|
2286
|
-
category: "read"
|
|
2949
|
+
category: "read",
|
|
2950
|
+
boundField: ext.boundField,
|
|
2951
|
+
requiredValue: ext.requiredValue,
|
|
2952
|
+
read: ext.read,
|
|
2953
|
+
readGovernance: ext.readGovernance,
|
|
2954
|
+
readGovernanceReason: ext.readGovernanceReason
|
|
2287
2955
|
};
|
|
2288
2956
|
}
|
|
2289
2957
|
return {
|
|
@@ -2337,28 +3005,28 @@ var IntegrationManager = class {
|
|
|
2337
3005
|
};
|
|
2338
3006
|
|
|
2339
3007
|
// src/lib/profile-loader.ts
|
|
2340
|
-
import { readFileSync as
|
|
2341
|
-
import { join as
|
|
3008
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
3009
|
+
import { join as join7, resolve } from "path";
|
|
2342
3010
|
import { registerProfile } from "@hap/core";
|
|
2343
3011
|
function loadProfiles(profilesDir) {
|
|
2344
|
-
const dir = resolve(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ??
|
|
2345
|
-
const indexPath =
|
|
2346
|
-
if (!
|
|
3012
|
+
const dir = resolve(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join7(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
|
|
3013
|
+
const indexPath = join7(dir, "index.json");
|
|
3014
|
+
if (!existsSync7(indexPath)) {
|
|
2347
3015
|
warnNoProfiles(`No index.json at ${indexPath}`);
|
|
2348
3016
|
return 0;
|
|
2349
3017
|
}
|
|
2350
3018
|
let index;
|
|
2351
3019
|
try {
|
|
2352
|
-
index = JSON.parse(
|
|
3020
|
+
index = JSON.parse(readFileSync7(indexPath, "utf-8"));
|
|
2353
3021
|
} catch (err) {
|
|
2354
3022
|
console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
|
|
2355
3023
|
return 0;
|
|
2356
3024
|
}
|
|
2357
3025
|
let loaded = 0;
|
|
2358
3026
|
for (const [profileId, relativePath] of Object.entries(index.profiles)) {
|
|
2359
|
-
const profilePath =
|
|
3027
|
+
const profilePath = join7(dir, relativePath);
|
|
2360
3028
|
try {
|
|
2361
|
-
const profile = JSON.parse(
|
|
3029
|
+
const profile = JSON.parse(readFileSync7(profilePath, "utf-8"));
|
|
2362
3030
|
registerProfile(profileId, profile);
|
|
2363
3031
|
const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
|
|
2364
3032
|
if (shortName && shortName !== profileId) {
|
|
@@ -2391,30 +3059,30 @@ function warnNoProfiles(reason) {
|
|
|
2391
3059
|
}
|
|
2392
3060
|
|
|
2393
3061
|
// src/lib/manifest-loader.ts
|
|
2394
|
-
import { readFileSync as
|
|
2395
|
-
import { join as
|
|
3062
|
+
import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
|
|
3063
|
+
import { join as join8, resolve as resolve2 } from "path";
|
|
2396
3064
|
var manifests = /* @__PURE__ */ new Map();
|
|
2397
3065
|
function loadManifests(integrationsDir) {
|
|
2398
3066
|
const dir = resolve2(
|
|
2399
|
-
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ??
|
|
3067
|
+
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join8(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
|
|
2400
3068
|
);
|
|
2401
|
-
const indexPath =
|
|
2402
|
-
if (!
|
|
3069
|
+
const indexPath = join8(dir, "index.json");
|
|
3070
|
+
if (!existsSync8(indexPath)) {
|
|
2403
3071
|
console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
|
|
2404
3072
|
return 0;
|
|
2405
3073
|
}
|
|
2406
3074
|
let index;
|
|
2407
3075
|
try {
|
|
2408
|
-
index = JSON.parse(
|
|
3076
|
+
index = JSON.parse(readFileSync8(indexPath, "utf-8"));
|
|
2409
3077
|
} catch (err) {
|
|
2410
3078
|
console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
|
|
2411
3079
|
return 0;
|
|
2412
3080
|
}
|
|
2413
3081
|
let loaded = 0;
|
|
2414
3082
|
for (const [id, relativePath] of Object.entries(index.integrations)) {
|
|
2415
|
-
const manifestPath =
|
|
3083
|
+
const manifestPath = join8(dir, relativePath);
|
|
2416
3084
|
try {
|
|
2417
|
-
const manifest = JSON.parse(
|
|
3085
|
+
const manifest = JSON.parse(readFileSync8(manifestPath, "utf-8"));
|
|
2418
3086
|
manifests.set(id, manifest);
|
|
2419
3087
|
loaded++;
|
|
2420
3088
|
} catch (err) {
|
|
@@ -2492,8 +3160,10 @@ app.post("/internal/configure", internalOnly, (req, res) => {
|
|
|
2492
3160
|
state.spClient.setSessionCookie(sessionCookie);
|
|
2493
3161
|
console.error("[Suveren MCP] Session cookie configured by control-plane");
|
|
2494
3162
|
if (vaultKeyHex) {
|
|
2495
|
-
|
|
2496
|
-
|
|
3163
|
+
const key = Buffer.from(vaultKeyHex, "hex");
|
|
3164
|
+
state.gateStore.setVaultKey(key);
|
|
3165
|
+
state.denialLog.setVaultKey(key);
|
|
3166
|
+
console.error("[Suveren MCP] Vault key configured \u2014 gate store + denial log encryption active");
|
|
2497
3167
|
}
|
|
2498
3168
|
if (apiKey) {
|
|
2499
3169
|
state.spClient.setApiKey(apiKey);
|