@suveren/gateway 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/bin/suveren-gateway.js +86 -5
  2. package/content/integrations/calendar.json +116 -22
  3. package/content/integrations/deploy-github.json +164 -0
  4. package/content/integrations/gmail.json +13 -4
  5. package/content/integrations/index.json +2 -1
  6. package/dist/control-plane/index.mjs +84 -7
  7. package/dist/mcp-server/http.mjs +147 -21
  8. package/dist/ui/assets/index-DecSrutK.js +105 -0
  9. package/dist/ui/index.html +1 -1
  10. package/node_modules/@hap/core/dist/index.d.mts +21 -0
  11. package/node_modules/@hap/core/dist/index.d.ts +21 -0
  12. package/node_modules/@hap/core/dist/index.js +17 -0
  13. package/node_modules/@hap/core/dist/index.mjs +17 -0
  14. package/node_modules/@hap/core/package.json +1 -1
  15. package/node_modules/@hap/core/src/gatekeeper.ts +28 -0
  16. package/node_modules/@hap/core/src/types.ts +21 -0
  17. package/node_modules/fast-uri/index.js +37 -1
  18. package/node_modules/fast-uri/package.json +1 -1
  19. package/node_modules/fast-uri/test/security.test.js +136 -0
  20. package/node_modules/hono/dist/cjs/helper/proxy/index.js +4 -0
  21. package/node_modules/hono/dist/cjs/jsx/base.js +1 -9
  22. package/node_modules/hono/dist/cjs/jsx/hooks/index.js +14 -11
  23. package/node_modules/hono/dist/cjs/middleware/cors/index.js +1 -1
  24. package/node_modules/hono/dist/cjs/middleware/language/language.js +10 -6
  25. package/node_modules/hono/dist/cjs/utils/cookie.js +2 -1
  26. package/node_modules/hono/dist/helper/proxy/index.js +4 -0
  27. package/node_modules/hono/dist/jsx/base.js +1 -9
  28. package/node_modules/hono/dist/jsx/hooks/index.js +14 -11
  29. package/node_modules/hono/dist/middleware/cors/index.js +1 -1
  30. package/node_modules/hono/dist/middleware/language/language.js +10 -6
  31. package/node_modules/hono/dist/utils/cookie.js +2 -1
  32. package/node_modules/hono/package.json +2 -3
  33. package/node_modules/ip-address/README.md +142 -128
  34. package/node_modules/ip-address/dist/common.d.ts +6 -0
  35. package/node_modules/ip-address/dist/common.js +17 -1
  36. package/node_modules/ip-address/dist/common.js.map +1 -1
  37. package/node_modules/ip-address/dist/ipv4.js +3 -12
  38. package/node_modules/ip-address/dist/ipv4.js.map +1 -1
  39. package/node_modules/ip-address/dist/ipv6.d.ts +9 -2
  40. package/node_modules/ip-address/dist/ipv6.js +35 -16
  41. package/node_modules/ip-address/dist/ipv6.js.map +1 -1
  42. package/node_modules/ip-address/package.json +5 -3
  43. package/node_modules/jose/README.md +13 -13
  44. package/node_modules/jose/dist/types/index.d.ts +6 -5
  45. package/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts +17 -11
  46. package/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts +7 -19
  47. package/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts +17 -11
  48. package/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts +7 -19
  49. package/node_modules/jose/dist/types/jwe/general/decrypt.d.ts +18 -13
  50. package/node_modules/jose/dist/types/jwe/general/encrypt.d.ts +20 -21
  51. package/node_modules/jose/dist/types/jwk/embedded.d.ts +8 -12
  52. package/node_modules/jose/dist/types/jwk/thumbprint.d.ts +0 -15
  53. package/node_modules/jose/dist/types/jwks/local.d.ts +12 -21
  54. package/node_modules/jose/dist/types/jwks/remote.d.ts +34 -165
  55. package/node_modules/jose/dist/types/jws/compact/sign.d.ts +2 -13
  56. package/node_modules/jose/dist/types/jws/compact/verify.d.ts +17 -13
  57. package/node_modules/jose/dist/types/jws/flattened/sign.d.ts +2 -13
  58. package/node_modules/jose/dist/types/jws/flattened/verify.d.ts +17 -13
  59. package/node_modules/jose/dist/types/jws/general/sign.d.ts +16 -17
  60. package/node_modules/jose/dist/types/jws/general/verify.d.ts +18 -15
  61. package/node_modules/jose/dist/types/jwt/decrypt.d.ts +17 -11
  62. package/node_modules/jose/dist/types/jwt/encrypt.d.ts +10 -34
  63. package/node_modules/jose/dist/types/jwt/sign.d.ts +2 -13
  64. package/node_modules/jose/dist/types/jwt/unsecured.d.ts +4 -13
  65. package/node_modules/jose/dist/types/jwt/verify.d.ts +14 -13
  66. package/node_modules/jose/dist/types/key/export.d.ts +0 -14
  67. package/node_modules/jose/dist/types/key/generate_key_pair.d.ts +7 -14
  68. package/node_modules/jose/dist/types/key/generate_secret.d.ts +16 -14
  69. package/node_modules/jose/dist/types/key/import.d.ts +18 -28
  70. package/node_modules/jose/dist/types/types.d.ts +239 -222
  71. package/node_modules/jose/dist/types/util/base64url.d.ts +11 -4
  72. package/node_modules/jose/dist/types/util/decode_jwt.d.ts +1 -9
  73. package/node_modules/jose/dist/types/util/decode_protected_header.d.ts +0 -8
  74. package/node_modules/jose/dist/types/util/errors.d.ts +128 -121
  75. package/node_modules/jose/dist/webapi/jwe/compact/decrypt.js +4 -22
  76. package/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js +5 -153
  77. package/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js +16 -111
  78. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +15 -3
  79. package/node_modules/jose/dist/webapi/jwe/general/encrypt.js +65 -81
  80. package/node_modules/jose/dist/webapi/jwk/embedded.js +5 -3
  81. package/node_modules/jose/dist/webapi/jwks/local.js +31 -72
  82. package/node_modules/jose/dist/webapi/jwks/remote.js +19 -28
  83. package/node_modules/jose/dist/webapi/jws/compact/sign.js +5 -2
  84. package/node_modules/jose/dist/webapi/jws/compact/verify.js +4 -16
  85. package/node_modules/jose/dist/webapi/jws/flattened/sign.js +7 -65
  86. package/node_modules/jose/dist/webapi/jws/flattened/verify.js +3 -89
  87. package/node_modules/jose/dist/webapi/jws/general/sign.js +19 -15
  88. package/node_modules/jose/dist/webapi/jws/general/verify.js +29 -9
  89. package/node_modules/jose/dist/webapi/jwt/decrypt.js +5 -5
  90. package/node_modules/jose/dist/webapi/jwt/sign.js +2 -3
  91. package/node_modules/jose/dist/webapi/jwt/unsecured.js +4 -3
  92. package/node_modules/jose/dist/webapi/jwt/verify.js +6 -6
  93. package/node_modules/jose/dist/webapi/key/export.js +37 -4
  94. package/node_modules/jose/dist/webapi/key/generate_key_pair.js +33 -82
  95. package/node_modules/jose/dist/webapi/key/import.js +8 -8
  96. package/node_modules/jose/dist/webapi/lib/asn1.js +53 -122
  97. package/node_modules/jose/dist/webapi/lib/buffer_utils.js +1 -0
  98. package/node_modules/jose/dist/webapi/lib/content_encryption.js +34 -109
  99. package/node_modules/jose/dist/webapi/lib/crypto_key.js +18 -126
  100. package/node_modules/jose/dist/webapi/lib/helpers.js +23 -0
  101. package/node_modules/jose/dist/webapi/lib/invalid_key_input.js +0 -1
  102. package/node_modules/jose/dist/webapi/lib/jwe_algorithms.js +116 -0
  103. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +186 -0
  104. package/node_modules/jose/dist/webapi/lib/jwe_encrypt.js +110 -0
  105. package/node_modules/jose/dist/webapi/lib/jwk_to_key.js +8 -98
  106. package/node_modules/jose/dist/webapi/lib/jws_algorithms.js +76 -0
  107. package/node_modules/jose/dist/webapi/lib/jws_sign.js +74 -0
  108. package/node_modules/jose/dist/webapi/lib/jws_verify.js +107 -0
  109. package/node_modules/jose/dist/webapi/lib/jwt_claims_set.js +68 -109
  110. package/node_modules/jose/dist/webapi/lib/key.js +132 -0
  111. package/node_modules/jose/dist/webapi/lib/key_algorithm.js +10 -0
  112. package/node_modules/jose/dist/webapi/lib/key_descriptor.js +7 -0
  113. package/node_modules/jose/dist/webapi/lib/key_management.js +162 -92
  114. package/node_modules/jose/dist/webapi/lib/options.js +48 -0
  115. package/node_modules/jose/dist/webapi/lib/signing.js +14 -56
  116. package/node_modules/jose/dist/webapi/lib/type_checks.js +13 -18
  117. package/node_modules/jose/dist/webapi/util/base64url.js +13 -4
  118. package/node_modules/jose/dist/webapi/util/decode_jwt.js +2 -2
  119. package/node_modules/jose/dist/webapi/util/decode_protected_header.js +5 -15
  120. package/node_modules/jose/dist/webapi/util/errors.js +1 -1
  121. package/node_modules/jose/package.json +2 -4
  122. package/package.json +2 -2
  123. package/profiles/deploy/0.6.profile.json +168 -0
  124. package/profiles/deploy/0.7.profile.json +168 -0
  125. package/profiles/email/0.4.profile.json +9 -2
  126. package/profiles/index.json +3 -1
  127. package/dist/ui/assets/index-Cog54HOo.js +0 -105
  128. package/node_modules/jose/dist/webapi/lib/aesgcmkw.js +0 -15
  129. package/node_modules/jose/dist/webapi/lib/aeskw.js +0 -25
  130. package/node_modules/jose/dist/webapi/lib/check_key_type.js +0 -122
  131. package/node_modules/jose/dist/webapi/lib/ecdhes.js +0 -52
  132. package/node_modules/jose/dist/webapi/lib/key_to_jwk.js +0 -34
  133. package/node_modules/jose/dist/webapi/lib/normalize_key.js +0 -166
  134. package/node_modules/jose/dist/webapi/lib/pbes2kw.js +0 -42
  135. package/node_modules/jose/dist/webapi/lib/rsaes.js +0 -24
  136. package/node_modules/jose/dist/webapi/lib/validate_algorithms.js +0 -10
  137. package/node_modules/jose/dist/webapi/lib/validate_crit.js +0 -33
@@ -356,6 +356,7 @@ async function activateIntegration(manifest) {
356
356
  profile: manifest.profile,
357
357
  toolGating: manifest.toolGating,
358
358
  npmPackage: manifest.npmPackage,
359
+ ...typeof manifest.readPolicy?.defaultAgeDays === "number" ? { readAgeDays: manifest.readPolicy.defaultAgeDays } : {},
359
360
  enabled: true
360
361
  });
361
362
  }
@@ -391,6 +392,18 @@ async function removeIntegration(id) {
391
392
  }
392
393
  return res.json();
393
394
  }
395
+ async function setReadPolicy(id, readAgeDays) {
396
+ const res = await fetch(`${MCP_BASE}/internal/integration/${encodeURIComponent(id)}/read-policy`, {
397
+ method: "PATCH",
398
+ headers: { ...internalHeaders(), "Content-Type": "application/json" },
399
+ body: JSON.stringify({ readAgeDays })
400
+ });
401
+ if (!res.ok) {
402
+ const err = await res.json().catch(() => ({ error: "Unknown error" }));
403
+ throw new Error(err.error);
404
+ }
405
+ return res.json();
406
+ }
394
407
  async function getGateContent(path) {
395
408
  const qs = path ? `?path=${encodeURIComponent(path)}` : "";
396
409
  const res = await fetch(`${MCP_BASE}/internal/gate-content${qs}`, {
@@ -1364,6 +1377,30 @@ function requireAuthQueryOrHeader(vault2) {
1364
1377
  };
1365
1378
  }
1366
1379
 
1380
+ // src/lib/read-policy-defaults.ts
1381
+ async function backfillReadPolicyDefaults(data) {
1382
+ const defaults = /* @__PURE__ */ new Map();
1383
+ for (const m of data.manifests ?? []) {
1384
+ const days = m.readPolicy?.defaultAgeDays;
1385
+ if (typeof m.id === "string" && typeof days === "number" && Number.isInteger(days) && days >= 0) {
1386
+ defaults.set(m.id, days);
1387
+ }
1388
+ }
1389
+ if (defaults.size === 0) return;
1390
+ try {
1391
+ const current = await getIntegrations();
1392
+ for (const integration of current.integrations ?? []) {
1393
+ const fallback = defaults.get(integration.id);
1394
+ if (fallback === void 0) continue;
1395
+ if (integration.readAgeDays != null) continue;
1396
+ await setReadPolicy(integration.id, fallback);
1397
+ console.error(`[Control Plane] Read policy default applied: ${integration.id} \u2192 ${fallback}d`);
1398
+ }
1399
+ } catch (err) {
1400
+ console.error(`[Control Plane] \u26A0 Could not apply read-policy defaults: ${err instanceof Error ? err.message : err}`);
1401
+ }
1402
+ }
1403
+
1367
1404
  // src/routes/mcp.ts
1368
1405
  import { Router as Router5 } from "express";
1369
1406
 
@@ -1563,16 +1600,23 @@ function createAutostartRouter() {
1563
1600
  }
1564
1601
 
1565
1602
  // src/lib/desktop-notify.ts
1566
- import { spawn } from "child_process";
1603
+ import { spawn, spawnSync } from "child_process";
1567
1604
  function osaQuote(value) {
1568
1605
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1569
1606
  }
1570
1607
  function psQuote(value) {
1571
1608
  return `'${value.replace(/'/g, "''")}'`;
1572
1609
  }
1573
- function buildNotifyCommand(platform, title, message) {
1610
+ function buildNotifyCommand(platform, title, message, options = {}) {
1611
+ const { url, hasTerminalNotifier: hasTerminalNotifier2 = false } = options;
1574
1612
  switch (platform) {
1575
1613
  case "darwin":
1614
+ if (url && hasTerminalNotifier2) {
1615
+ return {
1616
+ cmd: "terminal-notifier",
1617
+ args: ["-title", title, "-message", message, "-open", url, "-group", "ai.suveren.gateway"]
1618
+ };
1619
+ }
1576
1620
  return {
1577
1621
  cmd: "osascript",
1578
1622
  args: ["-e", `display notification ${osaQuote(message)} with title ${osaQuote(title)}`]
@@ -1594,13 +1638,30 @@ function buildNotifyCommand(platform, title, message) {
1594
1638
  }
1595
1639
  }
1596
1640
  function lockedNotification(port2) {
1641
+ const url = `http://localhost:${port2}`;
1597
1642
  return {
1598
1643
  title: "Suveren is running but locked",
1599
- message: `Your agent has no authority until you unlock it. Open http://localhost:${port2} and enter your API key.`
1644
+ message: `Your agent has no authority until you unlock it. Open ${url} and enter your API key.`,
1645
+ url
1600
1646
  };
1601
1647
  }
1602
- function notify(title, message, platform = process.platform) {
1603
- const command = buildNotifyCommand(platform, title, message);
1648
+ var terminalNotifierCache = null;
1649
+ function hasTerminalNotifier() {
1650
+ if (terminalNotifierCache !== null) return terminalNotifierCache;
1651
+ try {
1652
+ const probe = spawnSync("terminal-notifier", ["-help"], { stdio: "ignore" });
1653
+ terminalNotifierCache = probe.error === void 0;
1654
+ } catch {
1655
+ terminalNotifierCache = false;
1656
+ }
1657
+ return terminalNotifierCache;
1658
+ }
1659
+ function notify(title, message, platform = process.platform, url) {
1660
+ const command = buildNotifyCommand(platform, title, message, {
1661
+ url,
1662
+ // Only probe when a URL could actually be attached.
1663
+ hasTerminalNotifier: url !== void 0 && platform === "darwin" ? hasTerminalNotifier() : false
1664
+ });
1604
1665
  if (!command) return;
1605
1666
  try {
1606
1667
  const child = spawn(command.cmd, command.args, { detached: true, stdio: "ignore" });
@@ -2195,6 +2256,21 @@ app.use("/autostart", jsonParser, authGuard, createAutostartRouter());
2195
2256
  app.use("/api/encrypt-intent", jsonParser, authGuard, createEncryptIntentRouter());
2196
2257
  app.use("/api/decrypt-intent", jsonParser, authGuard, createDecryptIntentRouter(vault));
2197
2258
  app.use("/api/approved-intents", jsonParser, authGuard, createApprovedIntentsRouter(vault));
2259
+ app.put("/integrations/:id/read-policy", jsonParser, authGuard, async (req, res) => {
2260
+ const id = req.params.id;
2261
+ const { readAgeDays } = req.body ?? {};
2262
+ if (readAgeDays !== null && !(typeof readAgeDays === "number" && Number.isInteger(readAgeDays) && readAgeDays >= 0)) {
2263
+ res.status(400).json({ error: "readAgeDays must be a non-negative integer, or null to clear it" });
2264
+ return;
2265
+ }
2266
+ try {
2267
+ const result = await setReadPolicy(id, readAgeDays);
2268
+ res.json(result);
2269
+ } catch (err) {
2270
+ const message = err instanceof Error ? err.message : "Unknown error";
2271
+ res.status(502).json({ error: `Could not set read policy: ${message}` });
2272
+ }
2273
+ });
2198
2274
  app.get("/integrations/:id/discover/:field", authGuard, async (req, res) => {
2199
2275
  const integrationId = req.params.id;
2200
2276
  const fieldName = req.params.field;
@@ -2494,8 +2570,8 @@ if (existsSync5(UI_DIST)) {
2494
2570
  app.listen(port, "0.0.0.0", () => {
2495
2571
  console.error(`[Control Plane] Listening on http://0.0.0.0:${port}`);
2496
2572
  if (process.env.SUVEREN_AUTOSTART === "1" && !vault.isUnlocked()) {
2497
- const { title, message } = lockedNotification(port);
2498
- notify(title, message);
2573
+ const { title, message, url } = lockedNotification(port);
2574
+ notify(title, message, process.platform, url);
2499
2575
  }
2500
2576
  console.error(`[Control Plane] SP proxy: ${SP_URL2}`);
2501
2577
  console.error(`[Control Plane] UI dist: ${UI_DIST}`);
@@ -2507,6 +2583,7 @@ app.listen(port, "0.0.0.0", () => {
2507
2583
  console.error(`[Control Plane] \u26A0 MCP server at ${MCP_BASE} returned 0 integration manifests \u2014 wrong SUVEREN_MCP_INTERNAL_URL? (dev MCP is :3431, npm is :3430)`);
2508
2584
  } else {
2509
2585
  console.error(`[Control Plane] Integrations: ${n} manifests loaded from ${MCP_BASE}`);
2586
+ void backfillReadPolicyDefaults(d);
2510
2587
  }
2511
2588
  }).catch((err) => {
2512
2589
  console.error(`[Control Plane] \u26A0 Could not reach MCP server at ${MCP_BASE} for manifests \u2014 wrong SUVEREN_MCP_INTERNAL_URL? (dev=:3431, npm=:3430). ${err instanceof Error ? err.message : err}`);
@@ -1157,9 +1157,11 @@ function computeContentBinding(profileId, tool, toolArgs) {
1157
1157
  if (binding.kind === "jcs") {
1158
1158
  contentHash = computeContentHash(binding, toolArgs);
1159
1159
  } else {
1160
- const field = tool ? detectContentField(tool) : null;
1160
+ const declared = tool?.gating?.contentField;
1161
+ const field = declared ?? (tool ? detectContentField(tool) : null);
1161
1162
  if (!field) return void 0;
1162
1163
  const raw = typeof toolArgs[field] === "string" ? toolArgs[field] : "";
1164
+ if (raw === "") return void 0;
1163
1165
  contentHash = computeContentHash(binding, raw);
1164
1166
  }
1165
1167
  return {
@@ -1332,6 +1334,29 @@ function getByDottedPath(obj, path) {
1332
1334
  obj
1333
1335
  );
1334
1336
  }
1337
+ function firstParsableDate(obj, paths) {
1338
+ for (const path of Array.isArray(paths) ? paths : [paths]) {
1339
+ const parsed = parseMessageTimestamp(getByDottedPath(obj, path));
1340
+ if (parsed !== null) return parsed;
1341
+ }
1342
+ return null;
1343
+ }
1344
+ function renderAgeConstraint(template, maxAgeDays, nowMs) {
1345
+ const floorIso = new Date(nowMs - maxAgeDays * MS_PER_DAY).toISOString();
1346
+ return template.replace(/\{days\}/g, String(maxAgeDays)).replace(/\{date\}/g, floorIso.slice(0, 10));
1347
+ }
1348
+ function ageFloorValue(maxAgeDays, nowMs, format = "iso") {
1349
+ const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
1350
+ if (format === "epoch_ms") return floorMs;
1351
+ if (format === "epoch_s") return String(Math.floor(floorMs / 1e3));
1352
+ return new Date(floorMs).toISOString();
1353
+ }
1354
+ function clampAgeFloor(requested, maxAgeDays, nowMs, format = "iso") {
1355
+ const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
1356
+ const asked = parseMessageTimestamp(requested);
1357
+ if (asked !== null && asked >= floorMs) return requested;
1358
+ return ageFloorValue(maxAgeDays, nowMs, format);
1359
+ }
1335
1360
 
1336
1361
  // src/lib/tool-proxy.ts
1337
1362
  import { getProfile as getProfile3 } from "@hap/core";
@@ -1422,6 +1447,22 @@ function denyRead(state2, tool, reason, detail, target) {
1422
1447
  return { content: [{ type: "text", text: `Read blocked by Gatekeeper: ${detail}` }], isError: true };
1423
1448
  }
1424
1449
  function createGatedToolHandler(tool, integrationManager2, state2) {
1450
+ const inner = createGatedToolHandlerInner(tool, integrationManager2, state2);
1451
+ const blocked = tool.gating?.blockedArgs ?? [];
1452
+ if (blocked.length === 0) return inner;
1453
+ return async (args) => {
1454
+ const present = blocked.filter((a) => a in (args ?? {}));
1455
+ if (present.length === 0) return inner(args);
1456
+ return {
1457
+ content: [{
1458
+ type: "text",
1459
+ text: `Refused: "${tool.namespacedName}" was called with ${present.map((b) => `"${b}"`).join(", ")}, which the integration manifest blocks. That argument carries the whole request in one opaque value, so the recipients, the content and the limits on this authorization cannot be read from it \u2014 the call could not be shown to stay in scope, and could not be shown to a human for approval in any form they could judge. Use the individual arguments instead.`
1460
+ }],
1461
+ isError: true
1462
+ };
1463
+ };
1464
+ }
1465
+ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
1425
1466
  if (!tool.gating || !tool.gating.profile) {
1426
1467
  return async () => {
1427
1468
  return {
@@ -1494,18 +1535,20 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
1494
1535
  const readProfile = readAdapter ? getProfile3(matchingAuths[0].profileId) : void 0;
1495
1536
  const boundsSchema = readProfile?.boundsSchema;
1496
1537
  const ageBoundField = readAdapter?.ageField ? resolveAgeBoundField(boundsSchema, readAdapter.ageField) : null;
1538
+ const hasAgeDimension = Boolean(readAdapter?.ageField);
1497
1539
  let readMaxAge = null;
1498
- if (ageBoundField) {
1499
- readMaxAge = maxReadAgeDays(
1540
+ if (hasAgeDimension) {
1541
+ const localAge = integrationManager2.getReadAgeDays(tool.integrationId);
1542
+ readMaxAge = localAge ?? (ageBoundField ? maxReadAgeDays(
1500
1543
  matchingAuths.map((a) => a.bounds ?? a.frame),
1501
1544
  ageBoundField
1502
- );
1545
+ ) : null);
1503
1546
  if (readMaxAge === null) {
1504
1547
  return denyRead(
1505
1548
  state2,
1506
1549
  tool,
1507
1550
  "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.`
1551
+ `no read-age window is set for this integration${ageBoundField ? ` (or on your authorization's ${ageBoundField})` : ""}. Reads are not permitted with an unbounded window \u2014 set how far back the agent may read under the integration's Read policy, then try again. Nothing was read.`
1509
1552
  );
1510
1553
  }
1511
1554
  }
@@ -1540,10 +1583,21 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
1540
1583
  if (readAdapter?.pinnedArgs) {
1541
1584
  outgoing = { ...outgoing, ...readAdapter.pinnedArgs };
1542
1585
  }
1586
+ if (readAdapter?.ageFloorArg && hasAgeDimension && readMaxAge !== null) {
1587
+ outgoing = {
1588
+ ...outgoing,
1589
+ [readAdapter.ageFloorArg]: clampAgeFloor(
1590
+ outgoing[readAdapter.ageFloorArg],
1591
+ readMaxAge,
1592
+ Date.now(),
1593
+ readAdapter.ageFloorFormat
1594
+ )
1595
+ };
1596
+ }
1543
1597
  if (readAdapter?.queryArg) {
1544
1598
  const clauses = [];
1545
- if (ageBoundField && readAdapter.ageConstraint && readMaxAge !== null) {
1546
- clauses.push(readAdapter.ageConstraint.replace("{days}", String(readMaxAge)));
1599
+ if (hasAgeDimension && readAdapter.ageConstraint && readMaxAge !== null) {
1600
+ clauses.push(renderAgeConstraint(readAdapter.ageConstraint, readMaxAge, Date.now()));
1547
1601
  }
1548
1602
  if (clauses.length > 0) {
1549
1603
  const base = typeof args[readAdapter.queryArg] === "string" ? args[readAdapter.queryArg] : "";
@@ -1581,10 +1635,10 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
1581
1635
  );
1582
1636
  }
1583
1637
  }
1584
- if (readAdapter && ageBoundField && readAdapter.resultDatePath && readMaxAge !== null) {
1638
+ if (readAdapter && hasAgeDimension && readAdapter.resultDatePath && readMaxAge !== null) {
1585
1639
  const parsed = parseFirstJson(result);
1586
- const rawDate = parsed === void 0 ? void 0 : getByDottedPath(parsed, readAdapter.resultDatePath);
1587
- if (isOlderThanMaxAge(parseMessageTimestamp(rawDate), readMaxAge, Date.now())) {
1640
+ const itemDate = parsed === void 0 ? null : firstParsableDate(parsed, readAdapter.resultDatePath);
1641
+ if (isOlderThanMaxAge(itemDate, readMaxAge, Date.now())) {
1588
1642
  return denyRead(
1589
1643
  state2,
1590
1644
  tool,
@@ -2571,6 +2625,23 @@ function ensureIntegrationsDir() {
2571
2625
  writeFileSync5(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
2572
2626
  }
2573
2627
  }
2628
+ function readAgeOf(config) {
2629
+ const days = config.readAgeDays;
2630
+ return typeof days === "number" && Number.isFinite(days) && days >= 0 ? days : null;
2631
+ }
2632
+ function withoutBlockedArgs(schema, blocked) {
2633
+ if (!blocked?.length) return schema;
2634
+ const props = schema.properties;
2635
+ if (!props) return schema;
2636
+ const kept = Object.fromEntries(Object.entries(props).filter(([k]) => !blocked.includes(k)));
2637
+ const out = { ...schema, properties: kept };
2638
+ if (Array.isArray(schema.required)) {
2639
+ out.required = schema.required.filter(
2640
+ (r) => typeof r !== "string" || !blocked.includes(r)
2641
+ );
2642
+ }
2643
+ return out;
2644
+ }
2574
2645
  var RESPAWN_DELAYS = [2e3, 4e3, 6e3];
2575
2646
  var MAX_RESPAWN_ATTEMPTS = 3;
2576
2647
  var CONNECT_TIMEOUT_MS = 3e4;
@@ -2766,7 +2837,14 @@ var IntegrationManager = class {
2766
2837
  namespacedName: `${config.id}__${tool.name}`,
2767
2838
  integrationId: config.id,
2768
2839
  description: tool.description ?? "",
2769
- inputSchema: tool.inputSchema ?? {},
2840
+ // A downstream schema otherwise reaches the agent verbatim, so a
2841
+ // connector can offer a control-bypassing argument and we would pass it
2842
+ // straight on. Anything the manifest blocks is removed here, before the
2843
+ // agent ever sees it exists.
2844
+ inputSchema: withoutBlockedArgs(
2845
+ tool.inputSchema ?? {},
2846
+ gating?.blockedArgs
2847
+ ),
2770
2848
  gating
2771
2849
  };
2772
2850
  });
@@ -2838,14 +2916,32 @@ var IntegrationManager = class {
2838
2916
  return tools;
2839
2917
  }
2840
2918
  /**
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.
2919
+ * The LOCAL read-age window (days) for a running integration, or null when
2920
+ * none is set here (the read path then falls back to the signed grant bound
2921
+ * see `IntegrationConfig.readAgeDays`).
2922
+ *
2923
+ * 0 is a real value ("read nothing") and MUST survive this call, so the
2924
+ * guard tests the type, never truthiness.
2845
2925
  */
2846
2926
  getReadAgeDays(integrationId) {
2847
- const days = this.running.get(integrationId)?.config.readAgeDays;
2848
- return typeof days === "number" && Number.isFinite(days) ? days : null;
2927
+ const config = this.running.get(integrationId)?.config;
2928
+ return config ? readAgeOf(config) : null;
2929
+ }
2930
+ /**
2931
+ * Apply a new local read-age window to the RUNNING snapshot, so the change
2932
+ * takes effect on the next read without restarting the subprocess. Read
2933
+ * policy is enforced gateway-side and never reaches the downstream MCP
2934
+ * server, so nothing has to be re-spawned.
2935
+ *
2936
+ * Persistence is the registry's job — callers update both. Returns false if
2937
+ * the integration is not running (registry-only update still applies).
2938
+ */
2939
+ setReadAgeDays(integrationId, days) {
2940
+ const entry = this.running.get(integrationId);
2941
+ if (!entry) return false;
2942
+ if (days === null) delete entry.config.readAgeDays;
2943
+ else entry.config.readAgeDays = days;
2944
+ return true;
2849
2945
  }
2850
2946
  /**
2851
2947
  * Get status info for all known integrations.
@@ -2857,7 +2953,8 @@ var IntegrationManager = class {
2857
2953
  id: entry.config.id,
2858
2954
  name: entry.config.name,
2859
2955
  running: true,
2860
- toolCount: entry.tools.length
2956
+ toolCount: entry.tools.length,
2957
+ readAgeDays: readAgeOf(entry.config)
2861
2958
  });
2862
2959
  }
2863
2960
  if (allConfigs) {
@@ -2867,7 +2964,8 @@ var IntegrationManager = class {
2867
2964
  id: config.id,
2868
2965
  name: config.name,
2869
2966
  running: false,
2870
- toolCount: 0
2967
+ toolCount: 0,
2968
+ readAgeDays: readAgeOf(config)
2871
2969
  });
2872
2970
  }
2873
2971
  }
@@ -2951,13 +3049,20 @@ var IntegrationManager = class {
2951
3049
  requiredValue: ext.requiredValue,
2952
3050
  read: ext.read,
2953
3051
  readGovernance: ext.readGovernance,
2954
- readGovernanceReason: ext.readGovernanceReason
3052
+ readGovernanceReason: ext.readGovernanceReason,
3053
+ blockedArgs: ext.blockedArgs
2955
3054
  };
2956
3055
  }
2957
3056
  return {
2958
3057
  profile: profileId,
2959
3058
  executionMapping: override.executionMapping,
2960
- staticExecution: override.staticExecution
3059
+ staticExecution: override.staticExecution,
3060
+ // Which argument the receipt binds to. This resolver is an explicit
3061
+ // whitelist, so a field added to the manifest and the type but not
3062
+ // copied here is silently dropped — the receipt is still issued and
3063
+ // simply carries no binding, which only surfaces when a verifier asks.
3064
+ contentField: ext.contentField,
3065
+ blockedArgs: ext.blockedArgs
2961
3066
  };
2962
3067
  }
2963
3068
  return {
@@ -3325,6 +3430,27 @@ app.delete("/internal/remove-integration/:id", internalOnly, async (req, res) =>
3325
3430
  console.error(`[Suveren MCP] Integration ${id} removed`);
3326
3431
  res.json({ ok: true, id });
3327
3432
  });
3433
+ app.patch("/internal/integration/:id/read-policy", internalOnly, (req, res) => {
3434
+ const id = req.params.id;
3435
+ const raw = req.body.readAgeDays;
3436
+ let readAgeDays;
3437
+ if (raw === null) {
3438
+ readAgeDays = null;
3439
+ } else if (typeof raw === "number" && Number.isInteger(raw) && raw >= 0) {
3440
+ readAgeDays = raw;
3441
+ } else {
3442
+ res.status(400).json({ error: "readAgeDays must be a non-negative integer, or null to clear it" });
3443
+ return;
3444
+ }
3445
+ const updated = integrationRegistry.update(id, { readAgeDays: readAgeDays ?? void 0 });
3446
+ if (!updated) {
3447
+ res.status(404).json({ error: `Integration "${id}" not found` });
3448
+ return;
3449
+ }
3450
+ integrationManager.setReadAgeDays(id, readAgeDays);
3451
+ console.error(`[Suveren MCP] Read policy for ${id}: ${readAgeDays === null ? "cleared" : `${readAgeDays}d`}`);
3452
+ res.json({ ok: true, id, readAgeDays });
3453
+ });
3328
3454
  app.get("/internal/integrations", internalOnly, (_req, res) => {
3329
3455
  const configs = integrationRegistry.getAll();
3330
3456
  const statuses = integrationManager.getStatus(configs);