@suveren/gateway 0.2.12 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +21 -7
  2. package/dist/control-plane/index.mjs +146 -2
  3. package/dist/mcp-server/http.mjs +85 -38
  4. package/dist/ui/assets/index-RkZBhIxU.js +105 -0
  5. package/dist/ui/index.html +1 -1
  6. package/node_modules/@hap/core/dist/index.d.mts +147 -3
  7. package/node_modules/@hap/core/dist/index.d.ts +147 -3
  8. package/node_modules/@hap/core/dist/index.js +74 -10
  9. package/node_modules/@hap/core/dist/index.mjs +69 -10
  10. package/node_modules/@hap/core/package.json +1 -1
  11. package/node_modules/@hap/core/src/gatekeeper.ts +4 -4
  12. package/node_modules/@hap/core/src/identity.ts +77 -0
  13. package/node_modules/@hap/core/src/index.ts +2 -0
  14. package/node_modules/@hap/core/src/intent-disclosure.ts +69 -0
  15. package/node_modules/@hap/core/src/types.ts +62 -2
  16. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/README.md +16 -1
  17. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/index.js +4 -20
  18. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/lib/read.js +17 -17
  19. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/lib/types/json.js +60 -32
  20. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/lib/types/raw.js +3 -3
  21. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/lib/types/text.js +3 -3
  22. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/lib/types/urlencoded.js +16 -20
  23. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/lib/utils.js +18 -16
  24. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/LICENSE +22 -0
  25. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/README.md +69 -0
  26. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/dist/index.d.ts +26 -0
  27. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/dist/index.js +170 -0
  28. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/dist/index.js.map +1 -0
  29. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/package.json +52 -0
  30. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/package.json +23 -10
  31. package/node_modules/@types/node/README.md +1 -1
  32. package/node_modules/@types/node/assert/strict.d.ts +10 -1
  33. package/node_modules/@types/node/assert.d.ts +144 -17
  34. package/node_modules/@types/node/async_hooks.d.ts +9 -9
  35. package/node_modules/@types/node/buffer.d.ts +0 -5
  36. package/node_modules/@types/node/child_process.d.ts +17 -21
  37. package/node_modules/@types/node/cluster.d.ts +13 -13
  38. package/node_modules/@types/node/console.d.ts +1 -1
  39. package/node_modules/@types/node/crypto.d.ts +454 -571
  40. package/node_modules/@types/node/diagnostics_channel.d.ts +37 -29
  41. package/node_modules/@types/node/dns.d.ts +14 -14
  42. package/node_modules/@types/node/events.d.ts +3 -3
  43. package/node_modules/@types/node/fs/promises.d.ts +4 -4
  44. package/node_modules/@types/node/fs.d.ts +3 -12
  45. package/node_modules/@types/node/globals.d.ts +4 -4
  46. package/node_modules/@types/node/http.d.ts +5 -5
  47. package/node_modules/@types/node/http2.d.ts +5 -1
  48. package/node_modules/@types/node/index.d.ts +0 -3
  49. package/node_modules/@types/node/inspector.d.ts +1 -1
  50. package/node_modules/@types/node/module.d.ts +16 -45
  51. package/node_modules/@types/node/net.d.ts +1 -1
  52. package/node_modules/@types/node/os.d.ts +2 -2
  53. package/node_modules/@types/node/package.json +4 -4
  54. package/node_modules/@types/node/perf_hooks.d.ts +1 -9
  55. package/node_modules/@types/node/process.d.ts +11 -11
  56. package/node_modules/@types/node/readline.d.ts +12 -11
  57. package/node_modules/@types/node/repl.d.ts +10 -10
  58. package/node_modules/@types/node/sqlite.d.ts +1 -1
  59. package/node_modules/@types/node/stream/promises.d.ts +2 -2
  60. package/node_modules/@types/node/stream/web.d.ts +1 -1
  61. package/node_modules/@types/node/stream.d.ts +5 -5
  62. package/node_modules/@types/node/test/reporters.d.ts +1 -1
  63. package/node_modules/@types/node/test.d.ts +8 -8
  64. package/node_modules/@types/node/tls.d.ts +1 -1
  65. package/node_modules/@types/node/ts5.6/index.d.ts +1 -4
  66. package/node_modules/@types/node/ts5.7/index.d.ts +0 -3
  67. package/node_modules/@types/node/url.d.ts +3 -3
  68. package/node_modules/@types/node/util/types.d.ts +1 -1
  69. package/node_modules/@types/node/util.d.ts +3 -20
  70. package/node_modules/@types/node/v8.d.ts +1 -1
  71. package/node_modules/@types/node/vm.d.ts +9 -9
  72. package/node_modules/@types/node/web-globals/domexception.d.ts +15 -0
  73. package/node_modules/@types/node/worker_threads.d.ts +2 -5
  74. package/node_modules/@types/node/zlib.d.ts +3 -3
  75. package/node_modules/fast-uri/index.js +1 -1
  76. package/node_modules/fast-uri/package.json +1 -1
  77. package/node_modules/fast-uri/test/security.test.js +28 -0
  78. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +14 -20
  79. package/node_modules/hono/dist/adapter/lambda-edge/handler.js +8 -2
  80. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +14 -20
  81. package/node_modules/hono/dist/cjs/adapter/lambda-edge/handler.js +8 -2
  82. package/node_modules/hono/dist/cjs/helper/css/common.js +3 -1
  83. package/node_modules/hono/dist/cjs/helper/css/index.js +9 -1
  84. package/node_modules/hono/dist/cjs/jsx/base.js +8 -14
  85. package/node_modules/hono/dist/cjs/jsx/components.js +41 -21
  86. package/node_modules/hono/dist/cjs/jsx/context.js +131 -5
  87. package/node_modules/hono/dist/cjs/jsx/streaming.js +9 -7
  88. package/node_modules/hono/dist/cjs/middleware/bearer-auth/index.js +1 -1
  89. package/node_modules/hono/dist/cjs/middleware/cors/index.js +2 -5
  90. package/node_modules/hono/dist/cjs/middleware/language/language.js +10 -32
  91. package/node_modules/hono/dist/cjs/middleware/serve-static/index.js +1 -1
  92. package/node_modules/hono/dist/cjs/middleware/timing/timing.js +3 -1
  93. package/node_modules/hono/dist/cjs/utils/ipaddr.js +6 -1
  94. package/node_modules/hono/dist/helper/css/common.js +3 -1
  95. package/node_modules/hono/dist/helper/css/index.js +9 -1
  96. package/node_modules/hono/dist/jsx/base.js +15 -15
  97. package/node_modules/hono/dist/jsx/components.js +42 -22
  98. package/node_modules/hono/dist/jsx/context.js +129 -5
  99. package/node_modules/hono/dist/jsx/streaming.js +10 -8
  100. package/node_modules/hono/dist/middleware/bearer-auth/index.js +1 -1
  101. package/node_modules/hono/dist/middleware/cors/index.js +2 -5
  102. package/node_modules/hono/dist/middleware/language/language.js +10 -32
  103. package/node_modules/hono/dist/middleware/serve-static/index.js +1 -1
  104. package/node_modules/hono/dist/middleware/timing/timing.js +3 -1
  105. package/node_modules/hono/dist/tsconfig.build.tsbuildinfo +1 -1
  106. package/node_modules/hono/dist/types/adapter/aws-lambda/handler.d.ts +1 -1
  107. package/node_modules/hono/dist/types/jsx/base.d.ts +1 -3
  108. package/node_modules/hono/dist/types/jsx/context.d.ts +39 -0
  109. package/node_modules/hono/dist/utils/ipaddr.js +6 -1
  110. package/node_modules/hono/package.json +19 -13
  111. package/node_modules/http-proxy-middleware/README.md +3 -10
  112. package/node_modules/http-proxy-middleware/dist/errors.d.ts +4 -0
  113. package/node_modules/http-proxy-middleware/dist/errors.js +16 -1
  114. package/node_modules/http-proxy-middleware/dist/factory.d.ts +2 -2
  115. package/node_modules/http-proxy-middleware/dist/handlers/fix-request-body-utils/stringify-form-data.d.ts +7 -0
  116. package/node_modules/http-proxy-middleware/dist/handlers/fix-request-body-utils/stringify-form-data.js +45 -0
  117. package/node_modules/http-proxy-middleware/dist/handlers/fix-request-body.d.ts +1 -1
  118. package/node_modules/http-proxy-middleware/dist/handlers/fix-request-body.js +23 -23
  119. package/node_modules/http-proxy-middleware/dist/handlers/response-interceptor.d.ts +1 -1
  120. package/node_modules/http-proxy-middleware/dist/handlers/response-interceptor.js +1 -1
  121. package/node_modules/http-proxy-middleware/dist/http-proxy-middleware.d.ts +1 -1
  122. package/node_modules/http-proxy-middleware/dist/http-proxy-middleware.js +2 -2
  123. package/node_modules/http-proxy-middleware/dist/legacy/create-proxy-middleware.d.ts +1 -1
  124. package/node_modules/http-proxy-middleware/dist/legacy/create-proxy-middleware.js +1 -1
  125. package/node_modules/http-proxy-middleware/dist/legacy/options-adapter.js +1 -1
  126. package/node_modules/http-proxy-middleware/dist/legacy/types.d.ts +1 -1
  127. package/node_modules/http-proxy-middleware/dist/path-filter.d.ts +1 -1
  128. package/node_modules/http-proxy-middleware/dist/path-filter.js +1 -1
  129. package/node_modules/http-proxy-middleware/dist/path-rewriter.js +1 -1
  130. package/node_modules/http-proxy-middleware/dist/plugins/default/error-response-plugin.js +2 -1
  131. package/node_modules/http-proxy-middleware/dist/plugins/default/logger-plugin.js +3 -3
  132. package/node_modules/http-proxy-middleware/dist/router.js +26 -8
  133. package/node_modules/http-proxy-middleware/dist/status-code.js +15 -12
  134. package/node_modules/http-proxy-middleware/dist/types.d.ts +3 -3
  135. package/node_modules/http-proxy-middleware/dist/types.js +0 -4
  136. package/node_modules/http-proxy-middleware/dist/utils/sanitize.d.ts +1 -0
  137. package/node_modules/http-proxy-middleware/dist/utils/sanitize.js +6 -0
  138. package/node_modules/http-proxy-middleware/package.json +30 -28
  139. package/node_modules/qs/CHANGELOG.md +11 -0
  140. package/node_modules/qs/dist/qs.js +17 -17
  141. package/node_modules/qs/lib/parse.js +19 -8
  142. package/node_modules/qs/lib/utils.js +47 -8
  143. package/node_modules/qs/package.json +6 -5
  144. package/node_modules/qs/test/parse.js +230 -0
  145. package/node_modules/qs/test/utils.js +194 -0
  146. package/node_modules/side-channel/CHANGELOG.md +10 -0
  147. package/node_modules/side-channel/README.md +1 -1
  148. package/node_modules/side-channel/index.js +5 -2
  149. package/node_modules/side-channel/package.json +10 -10
  150. package/node_modules/side-channel/test/index.js +16 -0
  151. package/node_modules/undici-types/agent.d.ts +0 -2
  152. package/node_modules/undici-types/client.d.ts +25 -19
  153. package/node_modules/undici-types/dispatcher.d.ts +7 -33
  154. package/node_modules/undici-types/dispatcher1-wrapper.d.ts +7 -0
  155. package/node_modules/undici-types/fetch.d.ts +24 -4
  156. package/node_modules/undici-types/h2c-client.d.ts +6 -6
  157. package/node_modules/undici-types/handlers.d.ts +1 -2
  158. package/node_modules/undici-types/header.d.ts +5 -0
  159. package/node_modules/undici-types/index.d.ts +3 -1
  160. package/node_modules/undici-types/interceptors.d.ts +1 -1
  161. package/node_modules/undici-types/package.json +1 -1
  162. package/node_modules/undici-types/pool.d.ts +0 -2
  163. package/node_modules/undici-types/proxy-agent.d.ts +2 -2
  164. package/node_modules/undici-types/round-robin-pool.d.ts +0 -2
  165. package/node_modules/undici-types/snapshot-agent.d.ts +4 -0
  166. package/node_modules/undici-types/socks5-proxy-agent.d.ts +2 -2
  167. package/node_modules/undici-types/webidl.d.ts +0 -1
  168. package/package.json +3 -3
  169. package/dist/ui/assets/index-CS16uhqb.js +0 -102
  170. package/node_modules/@types/node/compatibility/iterators.d.ts +0 -21
package/README.md CHANGED
@@ -1,6 +1,20 @@
1
1
  # Suveren Gateway
2
2
 
3
- **Suveren** is a commercial service built on the [Human Agency Protocol (HAP)](https://humanagencyprotocol.org) — the open protocol for bounded AI agent authority. The gateway is a Suveren product. The `@hap/core` protocol library inside this repo stays HAP-named because it re-exports the open HAP library. When you see "HAP-compliant" or references to the protocol spec, those describe the open standard, not the Suveren brand.
3
+ > **HAP is the protocol. Suveren is an implementation of it.**
4
+ >
5
+ > The Human Agency Protocol (HAP) is the open standard for bounded AI-agent
6
+ > authority — it defines the roles (**Authority Server**, **Gatekeeper**,
7
+ > **Executor**) and the concepts (profiles, gates, attestations, bounds,
8
+ > context, receipts). **Suveren** implements them: this repo is the **Gateway**,
9
+ > and [`suveren-as`](https://www.suveren.ai) is the **Authority Server**. The
10
+ > protocol is open — anyone can build their own compliant gateway.
11
+
12
+ This repository is the **Suveren Gateway** — Suveren's implementation of the HAP
13
+ **Gatekeeper + Executor** roles. It runs locally and verifies every tool call
14
+ against its authorization before the call reaches an external service. (The
15
+ `@hap/core` library inside this repo keeps its HAP name because it re-exports the
16
+ open protocol library — "HAP-compliant" and spec references describe the open
17
+ standard, not the Suveren brand.)
4
18
 
5
19
  Part of [suveren.ai](https://www.suveren.ai).
6
20
 
@@ -16,11 +30,11 @@ Works with any MCP-compatible agent. Define and authorize what they're allowed t
16
30
 
17
31
  Set the threshold. Routine actions execute automatically within the bounds you defined. High-stakes actions pause for your review before the agent acts.
18
32
 
19
- **Automatic** — You commit to specific bounds upfront: max amounts, allowed actions, time windows. The agent executes autonomously within those bounds. Each tool call is verified against your authorization and produces a signed receipt.
33
+ **Automatic** — You commit to specific bounds upfront: max amounts, allowed actions, time windows. The agent executes autonomously within those bounds. For each tool call the gateway verifies your authorization and requests a receipt from the Authority Server, which issues the signed receipt before the call runs — no receipt, no execution.
20
34
 
21
35
  **Review each action** — You define bounds but defer full commitment. When the agent proposes an action, you review it in the gateway UI — seeing exactly which tool, which arguments, which context. You approve or reject. Execution only proceeds after your decision.
22
36
 
23
- Both modes are bounded. Both produce receipts. Both create a full audit trail. The difference is whether you trust the bounds enough for autonomous execution, or want to review each action individually.
37
+ Both modes are bounded. In both, the Authority Server issues a signed receipt before the action runs — no receipt, no execution — and that signed history is a full audit trail. The difference is whether you trust the bounds enough for autonomous execution, or want to review each action individually.
24
38
 
25
39
  ---
26
40
 
@@ -33,7 +47,7 @@ Human AI Agent
33
47
  | articulate direction, |
34
48
  | commit (or defer) |
35
49
  v |
36
- Service Provider |
50
+ Authority Server |
37
51
  | 2. Sign attestation (Ed25519) |
38
52
  v |
39
53
  Gateway |
@@ -41,14 +55,14 @@ Gateway |
41
55
  | 4. Tool call <-----------|
42
56
  | |
43
57
  | 5. Fully committed: |
44
- | Gatekeeper verifies bounds |
45
- | -> receipt issued, execute |
58
+ | Gatekeeper checks bounds, asks AS |
59
+ | -> AS issues receipt, execute |
46
60
  | |
47
61
  | Deferred commitment: |
48
62
  | -> proposal created |
49
63
  | -> human reviews in UI |
50
64
  | -> commit or reject |
51
- | -> execute on commit |
65
+ | -> on commit: AS receipt, execute |
52
66
  ```
53
67
 
54
68
  The agent never holds credentials or signing authority. It acts within the bounds you set — high autonomy without losing accountability.
@@ -362,6 +362,13 @@ async function getGateContent(path) {
362
362
  if (!res.ok) throw new Error("Failed to fetch gate content");
363
363
  return res.json();
364
364
  }
365
+ async function getEnrichedAuthorizations() {
366
+ const res = await fetch(`${MCP_BASE}/internal/authorizations`, {
367
+ headers: internalHeaders()
368
+ });
369
+ if (!res.ok) throw new Error("Failed to fetch authorizations");
370
+ return res.json();
371
+ }
365
372
  async function getMcpHealth() {
366
373
  const res = await fetch(`${MCP_BASE}/health`);
367
374
  if (!res.ok) throw new Error("MCP server unreachable");
@@ -375,6 +382,7 @@ import {
375
382
  HkdfSha256,
376
383
  Aes256Gcm
377
384
  } from "@hpke/core";
385
+ import { canonicalizeText } from "@hap/core";
378
386
  function getSuite() {
379
387
  return new CipherSuite({
380
388
  kem: new DhkemX25519HkdfSha256(),
@@ -975,6 +983,84 @@ ${request.currentText}
975
983
  };
976
984
  }
977
985
  }
986
+ async function callModel(config, messages, opts) {
987
+ if (config.provider === "ollama") {
988
+ const response2 = await fetch(`${config.endpoint}/api/chat`, {
989
+ method: "POST",
990
+ headers: { "Content-Type": "application/json" },
991
+ body: JSON.stringify({
992
+ model: config.model,
993
+ messages,
994
+ stream: false,
995
+ options: { temperature: opts.temperature, num_predict: opts.maxTokens }
996
+ }),
997
+ signal: AbortSignal.timeout(opts.timeoutMs)
998
+ });
999
+ if (!response2.ok) throw new Error(`Ollama: ${response2.status}`);
1000
+ const data2 = await response2.json();
1001
+ return data2.message?.content?.trim() || "No response generated.";
1002
+ }
1003
+ const headers = { "Content-Type": "application/json" };
1004
+ if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
1005
+ const response = await fetch(`${config.endpoint}/chat/completions`, {
1006
+ method: "POST",
1007
+ headers,
1008
+ body: JSON.stringify({ model: config.model, messages, temperature: opts.temperature, max_tokens: opts.maxTokens }),
1009
+ signal: AbortSignal.timeout(opts.timeoutMs)
1010
+ });
1011
+ if (!response.ok) {
1012
+ const errorText = await response.text();
1013
+ throw new Error(`AI provider: ${response.status} - ${errorText}`);
1014
+ }
1015
+ const data = await response.json();
1016
+ return data.choices?.[0]?.message?.content?.trim() || "No response generated.";
1017
+ }
1018
+ var INTENT_REVIEW_PROMPT = `You are a reviewing assistant for Suveren. A human is creating a NEW agent authorization. Compare the MEANING of its Intent against the Intent of their EXISTING authorizations on the same profile.
1019
+
1020
+ Focus ONLY on the intent prose \u2014 the stated situation, goal, and soft rules / watch-outs. Surface:
1021
+ - Contradictions: a soft rule or goal in one intent that conflicts with another (e.g. one says "never email on weekends", the new one implies weekend sends; one says "formal tone only", another "keep it casual").
1022
+ - Tension: intent that works against, or ignores a caution stated in, another authorization.
1023
+ - Redundant purpose: the new intent describes essentially the same purpose as an existing one.
1024
+
1025
+ Do NOT report:
1026
+ - That scopes/recipients overlap, or that numeric limits differ \u2014 those are detected separately and already shown to the user. Ignore them entirely.
1027
+ - Anything not grounded in the actual intent text.
1028
+
1029
+ Use each authorization's scope ONLY to judge whether two intents' rules would actually collide in practice (overlapping targets) versus being independent \u2014 never report the scope overlap itself.
1030
+
1031
+ Rules:
1032
+ - Be concise \u2014 a few short bullets at most.
1033
+ - If the new intent has no real content yet (just placeholder prompts like "Why \u2014 \u2026", "Goal \u2014 \u2026", "Watch out \u2014 \u2026"), reply EXACTLY: "Not enough intent written yet to review."
1034
+ - If the intents are compatible, reply EXACTLY: "No intent conflicts found with your existing authorizations."
1035
+ - Refer to each existing authorization by its number in square brackets \u2014 e.g. [1], [2] \u2014 matching the numbered list provided, and you may also state its scope. Never invent names. Do NOT rewrite intent or make the decision \u2014 you surface; the human decides.`;
1036
+ async function getIntentReview(config, request) {
1037
+ const disclaimer = "AI surfaces reality. You decide.";
1038
+ const lines = [];
1039
+ lines.push(`NEW authorization on profile "${request.profileName}":`);
1040
+ if (request.scope) lines.push(` Scope (for context only): ${request.scope}`);
1041
+ lines.push(` Intent: ${request.newIntent.trim() || "(empty)"}`);
1042
+ lines.push("");
1043
+ lines.push("EXISTING authorizations on the same profile:");
1044
+ request.existingGrants.forEach((g, i) => {
1045
+ lines.push(` [${i + 1}]`);
1046
+ if (g.scope) lines.push(` Scope (for context only): ${g.scope}`);
1047
+ lines.push(` Intent: ${g.intent.trim() || "(empty)"}`);
1048
+ });
1049
+ const messages = [
1050
+ { role: "system", content: INTENT_REVIEW_PROMPT },
1051
+ { role: "user", content: lines.join("\n") }
1052
+ ];
1053
+ try {
1054
+ const review = await callModel(config, messages, { temperature: 0.2, maxTokens: 400, timeoutMs: 45e3 });
1055
+ return { success: true, review, disclaimer };
1056
+ } catch (error) {
1057
+ return {
1058
+ success: false,
1059
+ error: error instanceof Error ? error.message : "Unknown error",
1060
+ disclaimer
1061
+ };
1062
+ }
1063
+ }
978
1064
  async function testAIConnectivity(config) {
979
1065
  try {
980
1066
  if (config.provider === "ollama") {
@@ -999,6 +1085,10 @@ async function testAIConnectivity(config) {
999
1085
  }
1000
1086
 
1001
1087
  // src/routes/ai.ts
1088
+ var describeScope = (c = {}) => {
1089
+ const vals = Object.values(c).map((v) => String(v).trim()).filter(Boolean);
1090
+ return vals.length > 0 ? vals.join(", ") : "all (unscoped)";
1091
+ };
1002
1092
  function createAIRouter(vault2) {
1003
1093
  const router = Router3();
1004
1094
  function loadAIConfig() {
@@ -1043,6 +1133,46 @@ function createAIRouter(vault2) {
1043
1133
  const result = await getAIChatResponse(config, request);
1044
1134
  res.json(result);
1045
1135
  });
1136
+ router.post("/intent-review", async (req, res) => {
1137
+ const config = loadAIConfig();
1138
+ if (!config) {
1139
+ res.status(400).json({ error: "AI not configured. Save AI settings in Settings > General." });
1140
+ return;
1141
+ }
1142
+ const { profileId, newIntent, context } = req.body;
1143
+ if (!profileId) {
1144
+ res.status(400).json({ error: "Missing profileId" });
1145
+ return;
1146
+ }
1147
+ let existing = [];
1148
+ try {
1149
+ const data = await getEnrichedAuthorizations();
1150
+ existing = (data.authorizations ?? []).filter((a) => a.profileId === profileId && !!a.intent?.trim());
1151
+ } catch {
1152
+ existing = [];
1153
+ }
1154
+ if (existing.length === 0) {
1155
+ res.json({ success: true, review: null, note: "No other authorizations on this profile to compare against." });
1156
+ return;
1157
+ }
1158
+ const result = await getIntentReview(config, {
1159
+ profileName: profileId.split("/").pop() ?? profileId,
1160
+ newIntent: newIntent ?? "",
1161
+ scope: describeScope(context),
1162
+ existingGrants: existing.map((a) => ({
1163
+ intent: a.intent ?? "",
1164
+ scope: describeScope(a.context)
1165
+ }))
1166
+ });
1167
+ res.json({
1168
+ ...result,
1169
+ grants: existing.map((a) => ({
1170
+ scope: describeScope(a.context),
1171
+ intent: a.intent,
1172
+ bounds: a.bounds
1173
+ }))
1174
+ });
1175
+ });
1046
1176
  router.post("/test", async (req, res) => {
1047
1177
  const body = req.body;
1048
1178
  let config;
@@ -1234,6 +1364,7 @@ function createMCPRouter() {
1234
1364
 
1235
1365
  // src/routes/encrypt-intent.ts
1236
1366
  import { Router as Router6 } from "express";
1367
+ import { computeIntentDisclosureHash } from "@hap/core";
1237
1368
  function createEncryptIntentRouter() {
1238
1369
  const router = Router6();
1239
1370
  router.post("/", async (req, res) => {
@@ -1279,10 +1410,14 @@ function createEncryptIntentRouter() {
1279
1410
  enc: Buffer.from(wrap.enc).toString("base64")
1280
1411
  };
1281
1412
  }
1413
+ const intentCiphertextB64 = Buffer.from(encrypted.intentCiphertext).toString("base64");
1414
+ const approversFrozen = parsed.map((r) => r.userId);
1415
+ const intentDisclosureHash = computeIntentDisclosureHash(intentCiphertextB64, approversFrozen);
1282
1416
  res.json({
1283
- intentCiphertext: Buffer.from(encrypted.intentCiphertext).toString("base64"),
1417
+ intentCiphertext: intentCiphertextB64,
1284
1418
  encryptedKeys,
1285
- approversFrozen: parsed.map((r) => r.userId)
1419
+ approversFrozen,
1420
+ intentDisclosureHash
1286
1421
  });
1287
1422
  } catch (err) {
1288
1423
  console.error("[Control Plane] encrypt-intent error:", err);
@@ -1863,6 +1998,15 @@ app.get("/gate-content", authGuard, async (req, res) => {
1863
1998
  res.status(500).json({ error: "Failed to fetch gate content from MCP server" });
1864
1999
  }
1865
2000
  });
2001
+ app.get("/active-authorizations", authGuard, async (_req, res) => {
2002
+ try {
2003
+ const data = await getEnrichedAuthorizations();
2004
+ res.json(data);
2005
+ } catch (err) {
2006
+ console.error("[Control Plane] Authorizations retrieval error:", err);
2007
+ res.status(500).json({ error: "Failed to fetch authorizations from MCP server" });
2008
+ }
2009
+ });
1866
2010
  app.post("/gate-content", jsonParser, authGuard, async (req, res) => {
1867
2011
  try {
1868
2012
  const { frameHash, boundsHash, contextHash, context, path, gateContent } = req.body;
@@ -182,6 +182,11 @@ var SPClient = class {
182
182
  };
183
183
 
184
184
  // src/lib/attestation-cache.ts
185
+ import { decodeAttestationBlob } from "@hap/core";
186
+ function isCommitmentDowngrade(auth) {
187
+ const requiresReview = auth.signedCommitmentMode === "review" || auth.signedCommitmentMode === "review_above_cap";
188
+ return requiresReview && (auth.deferredCommitmentDomains ?? []).length === 0;
189
+ }
185
190
  var AttestationCache = class {
186
191
  constructor(spClient) {
187
192
  this.spClient = spClient;
@@ -226,8 +231,25 @@ var AttestationCache = class {
226
231
  async syncAuthorization(frameHash) {
227
232
  const result = await this.spClient.getAttestations(frameHash);
228
233
  if (!result.profile_id || !result.frame) return null;
234
+ if (result.revoked) {
235
+ this.authorizations.delete(frameHash);
236
+ if (result.frame_hash) this.authorizations.delete(result.frame_hash);
237
+ return null;
238
+ }
229
239
  const storageHash = result.frame_hash ?? result.bounds_hash;
230
240
  const bounds = result.bounds ?? result.frame;
241
+ let signedCommitmentMode;
242
+ let subjects;
243
+ const firstBlob = result.attestations[0]?.blob;
244
+ if (firstBlob) {
245
+ try {
246
+ const payload = decodeAttestationBlob(firstBlob).payload;
247
+ signedCommitmentMode = payload.commitment_mode;
248
+ subjects = payload.subjects;
249
+ } catch {
250
+ signedCommitmentMode = void 0;
251
+ }
252
+ }
231
253
  const auth = {
232
254
  frameHash: storageHash,
233
255
  boundsHash: result.bounds_hash,
@@ -247,9 +269,11 @@ var AttestationCache = class {
247
269
  requiredDomains: result.required_domains ?? [],
248
270
  attestedDomains: result.attested_domains ?? [],
249
271
  deferredCommitmentDomains: result.deferred_commitment_domains ?? [],
272
+ signedCommitmentMode,
273
+ subjects,
250
274
  complete: result.complete
251
275
  };
252
- this.authorizations.set(auth.path, auth);
276
+ this.authorizations.set(auth.frameHash ?? auth.path, auth);
253
277
  return auth;
254
278
  }
255
279
  /**
@@ -278,7 +302,7 @@ var AttestationCache = class {
278
302
  * Cache an authorization directly (e.g., from SP response after creation).
279
303
  */
280
304
  cacheAuthorization(auth) {
281
- this.authorizations.set(auth.path, auth);
305
+ this.authorizations.set(auth.frameHash ?? auth.path, auth);
282
306
  }
283
307
  /**
284
308
  * Remove a cached authorization by path. Called when the SP reports the
@@ -715,7 +739,7 @@ var SharedState = class {
715
739
  getEnrichedAuthorizations() {
716
740
  const authorizations = this.cache.getAllAuthorizations();
717
741
  return authorizations.map((auth) => {
718
- const gateEntry = this.gateStore.get(auth.path) ?? this.gateStore.get(auth.profileId) ?? (auth.boundsHash ? this.gateStore.get(auth.boundsHash) : null) ?? (auth.frameHash ? this.gateStore.get(auth.frameHash) : null) ?? this.gateStore.getAll().find((g) => g.profileId === auth.profileId) ?? null;
742
+ const gateEntry = (auth.frameHash ? this.gateStore.get(auth.frameHash) : null) ?? (auth.boundsHash ? this.gateStore.get(auth.boundsHash) : null) ?? this.gateStore.get(auth.path) ?? null;
719
743
  return {
720
744
  ...auth,
721
745
  gateContent: gateEntry?.gateContent ?? null,
@@ -905,20 +929,23 @@ function buildMandateBrief(opts) {
905
929
  import { getProfile as getProfile3 } from "@hap/core";
906
930
 
907
931
  // src/lib/receipt-footer.ts
932
+ import { deriveIdentityLine } from "@hap/core";
908
933
  var AS_BASE = (process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai").replace(/\/$/, "");
934
+ var OPERATOR_NAME = process.env.SUVEREN_OPERATOR_NAME ?? "Suveren";
909
935
  var FOOTER_PROFILES = /* @__PURE__ */ new Set(["email", "calendar", "publish"]);
910
936
  var CONTENT_FIELD_CANDIDATES = ["body", "text", "description", "content"];
911
- var FOOTER_MARKER = "\u2014 Sent by an AI agent via Suveren";
937
+ var FOOTER_MARKER = "\u2014 Sent by an AI agent";
912
938
  function shouldAttachFooter() {
913
939
  return true;
914
940
  }
915
941
  function verifyUrl(receiptId) {
916
942
  return `${AS_BASE}/r/${receiptId}`;
917
943
  }
918
- function footerText(receiptId) {
944
+ function footerText(receiptId, subject) {
945
+ const line = deriveIdentityLine(subject, { operatorName: OPERATOR_NAME });
919
946
  return `
920
947
 
921
- ${FOOTER_MARKER}. Verify: ${verifyUrl(receiptId)}`;
948
+ \u2014 ${line}. Verify: ${verifyUrl(receiptId)}`;
922
949
  }
923
950
  function isStringType(t) {
924
951
  return t === "string" || Array.isArray(t) && t.includes("string");
@@ -937,45 +964,34 @@ function stripFooter(value) {
937
964
  if (i === -1) return value;
938
965
  return value.slice(0, i).replace(/\n+$/, "");
939
966
  }
940
- function appendVerificationFooter(tool, args, receiptId) {
967
+ function appendVerificationFooter(tool, args, receiptId, subject) {
941
968
  const profile = tool.gating?.profile;
942
969
  if (!profile || !FOOTER_PROFILES.has(profile)) return args;
943
970
  const field = detectContentField(tool);
944
971
  if (!field) return args;
945
972
  const current = typeof args[field] === "string" ? args[field] : "";
946
- return { ...args, [field]: stripFooter(current) + footerText(receiptId) };
973
+ return { ...args, [field]: stripFooter(current) + footerText(receiptId, subject) };
947
974
  }
948
975
 
949
976
  // src/lib/content-binding.ts
950
- import { createHash } from "crypto";
951
- import { canonicalize, getProfile as getProfile2 } from "@hap/core";
952
- function canonicalizeText(input) {
953
- const nfc = input.normalize("NFC");
954
- const lf = nfc.replace(/\r\n?/g, "\n");
955
- const lines = lf.split("\n").map((line) => line.replace(/[ \t]+$/, ""));
956
- return lines.join("\n").replace(/\n+$/, "");
957
- }
958
- function sha256Hex(bytes) {
959
- return createHash("sha256").update(bytes, "utf8").digest("hex");
960
- }
977
+ import { computeContentHash, getProfile as getProfile2 } from "@hap/core";
961
978
  function getContentBinding(profileId) {
962
- const profile = getProfile2(profileId);
963
- return profile?.content_binding;
979
+ return getProfile2(profileId)?.content_binding;
964
980
  }
965
981
  function computeContentBinding(profileId, tool, toolArgs) {
966
982
  const binding = getContentBinding(profileId);
967
983
  if (!binding) return void 0;
968
- let canonicalBytes;
984
+ let contentHash;
969
985
  if (binding.kind === "jcs") {
970
- canonicalBytes = canonicalize(toolArgs);
986
+ contentHash = computeContentHash(binding, toolArgs);
971
987
  } else {
972
988
  const field = tool ? detectContentField(tool) : null;
973
989
  if (!field) return void 0;
974
990
  const raw = typeof toolArgs[field] === "string" ? toolArgs[field] : "";
975
- canonicalBytes = canonicalizeText(raw);
991
+ contentHash = computeContentHash(binding, raw);
976
992
  }
977
993
  return {
978
- contentHash: `sha256:${sha256Hex(canonicalBytes)}`,
994
+ contentHash,
979
995
  contentBinding: { version: binding.version, kind: binding.kind }
980
996
  };
981
997
  }
@@ -1111,7 +1127,7 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
1111
1127
  }
1112
1128
  const errors = [];
1113
1129
  for (const auth of matchingAuths) {
1114
- const { result } = await state2.gatekeeper.verifyExecution(auth.path, execution, {
1130
+ const { result } = await state2.gatekeeper.verifyExecution(auth.frameHash ?? auth.path, execution, {
1115
1131
  bounds: auth.bounds,
1116
1132
  context: auth.context
1117
1133
  });
@@ -1119,6 +1135,15 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
1119
1135
  }
1120
1136
  if (result.approved) {
1121
1137
  const authHash = auth.frameHash ?? auth.boundsHash;
1138
+ if (isCommitmentDowngrade(auth)) {
1139
+ return {
1140
+ content: [{
1141
+ type: "text",
1142
+ 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.`
1143
+ }],
1144
+ isError: true
1145
+ };
1146
+ }
1122
1147
  if ((auth.deferredCommitmentDomains ?? []).length > 0) {
1123
1148
  try {
1124
1149
  const enrichedArgs = await attachImagePreview(args);
@@ -1227,7 +1252,7 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
1227
1252
  }
1228
1253
  if (err instanceof SPReceiptError && err.statusCode === 403) {
1229
1254
  if (/revoked/i.test(err.message)) {
1230
- state2.cache.invalidate(auth.path);
1255
+ state2.cache.invalidate(auth.frameHash ?? auth.path);
1231
1256
  }
1232
1257
  return {
1233
1258
  content: [{ type: "text", text: `Blocked by SP: ${err.message}` }],
@@ -1245,7 +1270,7 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
1245
1270
  execution: { ...execution },
1246
1271
  timestamp: Math.floor(Date.now() / 1e3)
1247
1272
  });
1248
- let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId) : args;
1273
+ let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
1249
1274
  if (receiptId) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId);
1250
1275
  return integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
1251
1276
  }
@@ -1390,6 +1415,15 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
1390
1415
  output2.push(`[${auth.path}] ${auth.profileId} (${remainingMin} min remaining)${statusLabel}`);
1391
1416
  output2.push("");
1392
1417
  output2.push(` Bounds: ${boundsDesc}`);
1418
+ const reviewDomains = auth.deferredCommitmentDomains ?? [];
1419
+ output2.push(reviewDomains.length > 0 ? " Mode: review \u2014 each action requires your approval before it runs (a proposal is created, not executed)" : " Mode: automatic \u2014 actions run immediately within bounds");
1420
+ try {
1421
+ const meta = await state2.spClient.getFrameMetadata(auth.frameHash ?? auth.boundsHash ?? auth.path);
1422
+ if (meta?.aboveCap) {
1423
+ output2.push(" \u26A0 Above team cap \u2014 actions here require approval even within these bounds.");
1424
+ }
1425
+ } catch {
1426
+ }
1393
1427
  const shortName = shortProfileName2(auth.profileId);
1394
1428
  const profile = getProfile3(auth.profileId) ?? getProfile3(shortName);
1395
1429
  if (profile) {
@@ -1443,6 +1477,9 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
1443
1477
  if (auth.complete) {
1444
1478
  const lines = [` [${auth.path}] ${auth.profileId} \u2014 ${remainingMin} min remaining`];
1445
1479
  lines.push(` Bounds: ${boundsDesc}`);
1480
+ if ((auth.deferredCommitmentDomains ?? []).length > 0) {
1481
+ lines.push(" Mode: review (each action requires your approval)");
1482
+ }
1446
1483
  const shortName = shortProfileName2(auth.profileId);
1447
1484
  const profile = getProfile3(auth.profileId) ?? getProfile3(shortName);
1448
1485
  if (profile) {
@@ -1866,18 +1903,17 @@ function createMcpServer(state2, integrationManager2) {
1866
1903
  }
1867
1904
 
1868
1905
  // src/lib/gate-content.ts
1869
- import { createHash as createHash2 } from "crypto";
1870
- import { decodeAttestationBlob } from "@hap/core";
1906
+ import { computeIntentHash } from "@hap/core";
1907
+ import { decodeAttestationBlob as decodeAttestationBlob2 } from "@hap/core";
1871
1908
  function hashGateContent(text) {
1872
- const hex = createHash2("sha256").update(text, "utf-8").digest("hex");
1873
- return `sha256:${hex}`;
1909
+ return computeIntentHash(text);
1874
1910
  }
1875
1911
  function verifyGateContentHashes(content, auth) {
1876
1912
  const errors = [];
1877
1913
  if (auth.attestations.length === 0) {
1878
1914
  return { valid: false, errors: ["No attestations available to verify against"] };
1879
1915
  }
1880
- const attestation = decodeAttestationBlob(auth.attestations[0].blob);
1916
+ const attestation = decodeAttestationBlob2(auth.attestations[0].blob);
1881
1917
  const expectedHashes = attestation.payload.gate_content_hashes;
1882
1918
  if (!expectedHashes?.intent) {
1883
1919
  return { valid: false, errors: ["Attestation does not contain intent hash"] };
@@ -2477,7 +2513,7 @@ app.post("/internal/gate-content", internalOnly, async (req, res) => {
2477
2513
  res.status(400).json({ error: "Gate content hash mismatch", details: verification.errors });
2478
2514
  return;
2479
2515
  }
2480
- const path = rawPath || auth.profileId;
2516
+ const path = rawPath || storageHash;
2481
2517
  state.setGateContent(path, storageHash, auth.profileId, gateContent, {
2482
2518
  boundsHash,
2483
2519
  contextHash,
@@ -2537,7 +2573,7 @@ app.post("/internal/resync-gates", internalOnly, async (_req, res) => {
2537
2573
  const syncHash = gate.frameHash ?? gate.boundsHash;
2538
2574
  const auth = await state.cache.syncAuthorization(syncHash);
2539
2575
  if (auth) {
2540
- state.setGateContent(gate.path, syncHash, auth.profileId, gate.gateContent, {
2576
+ state.setGateContent(syncHash, syncHash, auth.profileId, gate.gateContent, {
2541
2577
  boundsHash: gate.boundsHash,
2542
2578
  contextHash: gate.contextHash,
2543
2579
  context: gate.context
@@ -2545,8 +2581,8 @@ app.post("/internal/resync-gates", internalOnly, async (_req, res) => {
2545
2581
  synced++;
2546
2582
  console.error(`[Suveren MCP] Re-synced gate: ${gate.path}`);
2547
2583
  } else {
2548
- state.cache.invalidate(gate.path);
2549
- state.gateStore.delete(gate.path);
2584
+ state.cache.invalidate(syncHash);
2585
+ state.gateStore.delete(syncHash);
2550
2586
  orphaned++;
2551
2587
  console.error(`[Suveren MCP] Orphan gate purged (SP attestation deleted): ${gate.path}`);
2552
2588
  }
@@ -2709,6 +2745,17 @@ app.get("/internal/gate-content", internalOnly, (req, res) => {
2709
2745
  res.json({ entries: gates });
2710
2746
  }
2711
2747
  });
2748
+ app.get("/internal/authorizations", internalOnly, (_req, res) => {
2749
+ const authorizations = state.getEnrichedAuthorizations().map((a) => ({
2750
+ profileId: a.profileId,
2751
+ frameHash: a.frameHash,
2752
+ bounds: a.frame,
2753
+ context: a.context ?? {},
2754
+ intent: a.gateContent?.intent ?? null,
2755
+ deferredCommitmentDomains: a.deferredCommitmentDomains ?? []
2756
+ }));
2757
+ res.json({ authorizations });
2758
+ });
2712
2759
  app.get("/internal/manifests", internalOnly, (_req, res) => {
2713
2760
  res.json({ manifests: getAllManifests() });
2714
2761
  });
@@ -2782,7 +2829,7 @@ app.listen(port, "0.0.0.0", () => {
2782
2829
  console.error(`[Suveren MCP] SP server: ${spUrl}`);
2783
2830
  profilesLoaded = loadProfiles();
2784
2831
  loadManifests();
2785
- if (integrationRegistry.getEnabled().length === 0) {
2832
+ if (process.env.SUVEREN_DISABLE_AUTO_INTEGRATIONS !== "1" && integrationRegistry.getEnabled().length === 0) {
2786
2833
  const personalManifests = getAllManifests().filter((m) => m.personalDefault);
2787
2834
  for (const manifest of personalManifests) {
2788
2835
  const optionalKeys = new Set(