@zackbart/connecta 0.28.1 → 0.30.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 (332) hide show
  1. package/CHANGELOG.md +855 -5
  2. package/README.md +125 -116
  3. package/bin/connecta.mjs +121 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/argument-redaction.js +230 -0
  12. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  13. package/dist/auth/clerk-sdk/client.js +5 -0
  14. package/dist/auth/clerk-sdk/generated.js +1845 -0
  15. package/dist/auth/clerk-transport.js +19 -0
  16. package/dist/auth/clerk.d.ts +17 -7
  17. package/dist/auth/clerk.js +189 -107
  18. package/dist/auth/cloudflare-access.js +25 -26
  19. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  20. package/dist/auth/downstream-client-metadata.js +133 -0
  21. package/dist/auth/downstream-oauth.js +1584 -1580
  22. package/dist/auth/downstream-revocation.js +106 -0
  23. package/dist/auth/oauth-token-response.js +231 -0
  24. package/dist/auth/oauth-v2-migration.js +116 -0
  25. package/dist/auth/static-oauth.js +93 -71
  26. package/dist/auth-elicitation.js +288 -0
  27. package/dist/authorization-handoff.js +111 -0
  28. package/dist/branding.d.ts +8 -16
  29. package/dist/branding.js +26 -59
  30. package/dist/byte-read-response.js +39 -0
  31. package/dist/call-admission.js +12 -16
  32. package/dist/catalog-cache.js +739 -0
  33. package/dist/catalog-drift.d.ts +50 -15
  34. package/dist/catalog-drift.js +365 -128
  35. package/dist/catalog-fingerprint.js +2 -4
  36. package/dist/catalog-freshness.js +13 -0
  37. package/dist/catalog-limits.js +2 -2
  38. package/dist/catalog-service.js +409 -225
  39. package/dist/catalog.js +43 -84
  40. package/dist/concurrency.js +3 -4
  41. package/dist/config-defaults.d.ts +39 -0
  42. package/dist/config-defaults.js +46 -0
  43. package/dist/config-schema.d.ts +181 -0
  44. package/dist/config-schema.js +435 -0
  45. package/dist/config-value-sources.js +102 -0
  46. package/dist/config.d.ts +633 -0
  47. package/dist/config.js +508 -0
  48. package/dist/connector-access.d.ts +1 -0
  49. package/dist/connector-access.js +13 -15
  50. package/dist/connector-caller.d.ts +8 -0
  51. package/dist/connector-caller.js +15 -6
  52. package/dist/connector-scope.js +1 -1
  53. package/dist/connectors/api-connector.d.ts +59 -7
  54. package/dist/connectors/api-connector.js +106 -19
  55. package/dist/connectors/api.js +4 -1
  56. package/dist/connectors/guarded-fetch.d.ts +144 -0
  57. package/dist/connectors/guarded-fetch.js +87 -38
  58. package/dist/connectors/negotiation-cache.js +46 -0
  59. package/dist/connectors/option-shapes.js +27 -0
  60. package/dist/connectors/remote-mcp.d.ts +30 -4
  61. package/dist/connectors/remote-mcp.js +1774 -320
  62. package/dist/connectors/resource-uri.js +335 -0
  63. package/dist/credential-contract.d.ts +10 -0
  64. package/dist/credential-rules.js +1 -3
  65. package/dist/credential-url.js +108 -0
  66. package/dist/credentials.d.ts +6 -1
  67. package/dist/credentials.js +27 -18
  68. package/dist/d1.d.ts +130 -0
  69. package/dist/d1.js +290 -0
  70. package/dist/describe-config.d.ts +165 -0
  71. package/dist/describe-config.js +315 -0
  72. package/dist/described.js +106 -0
  73. package/dist/downstream-input-context.js +128 -0
  74. package/dist/downstream-input.js +325 -0
  75. package/dist/errors.d.ts +132 -31
  76. package/dist/errors.js +175 -87
  77. package/dist/execute.js +798 -318
  78. package/dist/executor-admission.d.ts +1 -0
  79. package/dist/executor-admission.js +11 -15
  80. package/dist/executor-contract.d.ts +20 -0
  81. package/dist/executor-contract.js +48 -0
  82. package/dist/executor-result.js +30 -13
  83. package/dist/executors/quickjs-child.js +15 -9
  84. package/dist/executors/quickjs-protocol.js +5 -6
  85. package/dist/executors/quickjs-runtime.js +152 -59
  86. package/dist/executors/quickjs.js +49 -39
  87. package/dist/favicon.js +1 -2
  88. package/dist/fix-prompt.js +1 -3
  89. package/dist/guest-runtime.js +238 -0
  90. package/dist/guest-types.d.ts +218 -0
  91. package/dist/guest-types.js +1 -0
  92. package/dist/html-security.js +24 -0
  93. package/dist/identity.js +1 -3
  94. package/dist/inbound-credential.js +52 -0
  95. package/dist/index.d.ts +24 -256
  96. package/dist/index.js +65 -478
  97. package/dist/invocation-auth.js +69 -0
  98. package/dist/invocation.js +262 -79
  99. package/dist/json-schema.d.ts +1 -1
  100. package/dist/mcp-client-context.js +35 -0
  101. package/dist/mcp-result.js +11 -7
  102. package/dist/meta-output.js +61 -0
  103. package/dist/meta-tool-names.d.ts +2 -0
  104. package/dist/meta-tool-names.js +3 -0
  105. package/dist/meta-tools.js +419 -272
  106. package/dist/module-contracts.d.ts +8 -23
  107. package/dist/node.d.ts +0 -2
  108. package/dist/node.js +2 -2
  109. package/dist/oauth-callback-outcome.js +1 -3
  110. package/dist/oauth-handoff.js +194 -0
  111. package/dist/oauth-sealing.js +20 -0
  112. package/dist/operator-record.d.ts +137 -0
  113. package/dist/operator-record.js +420 -0
  114. package/dist/operator-ui/assets.js +31 -0
  115. package/dist/operator-ui/contract.d.ts +72 -0
  116. package/dist/operator-ui/contract.js +1 -0
  117. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  118. package/dist/operator-ui/generated.js +5 -4
  119. package/dist/operator-ui/model.d.ts +12 -87
  120. package/dist/operator-ui/model.js +2 -9
  121. package/dist/operator-ui/setup-commands.js +1 -3
  122. package/dist/operator-ui/view.d.ts +323 -0
  123. package/dist/operator-ui/view.js +62 -189
  124. package/dist/page-styles.js +2 -3
  125. package/dist/program-result.js +5 -0
  126. package/dist/program-source.js +14 -2
  127. package/dist/{exempt-writes.js → program-writes.js} +60 -19
  128. package/dist/provider.d.ts +142 -0
  129. package/dist/provider.js +187 -0
  130. package/dist/providers/_shared/google/result-size.js +57 -0
  131. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  132. package/dist/providers/_shared/google/service-account.js +500 -0
  133. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  134. package/dist/providers/_shared/google/workspace.js +518 -0
  135. package/dist/providers/_shared/request-token.js +83 -0
  136. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  137. package/dist/providers/basecamp/index.js +997 -0
  138. package/dist/providers/basecamp/skill.generated.js +9 -0
  139. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  140. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  141. package/dist/providers/breeze/skill.generated.js +9 -0
  142. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  143. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  144. package/dist/providers/ccb/skill.generated.js +9 -0
  145. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  146. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  147. package/dist/providers/cloudflare/skill.generated.js +10 -0
  148. package/dist/providers/docs/index.d.ts +54 -0
  149. package/dist/providers/docs/index.js +1237 -0
  150. package/dist/providers/docs/skill.generated.js +9 -0
  151. package/dist/providers/drive/index.d.ts +53 -0
  152. package/dist/providers/drive/index.js +1523 -0
  153. package/dist/providers/drive/skill.generated.js +9 -0
  154. package/dist/providers/forms/index.d.ts +54 -0
  155. package/dist/providers/forms/index.js +1217 -0
  156. package/dist/providers/forms/skill.generated.js +11 -0
  157. package/dist/providers/github/auth.d.ts +32 -0
  158. package/dist/providers/github/auth.js +361 -0
  159. package/dist/providers/github/hosted.js +218 -0
  160. package/dist/providers/github/index.d.ts +13 -0
  161. package/dist/providers/github/index.js +140 -0
  162. package/dist/providers/github/key.js +100 -0
  163. package/dist/providers/github/rest.js +321 -0
  164. package/dist/providers/github/scope.d.ts +29 -0
  165. package/dist/providers/github/scope.js +124 -0
  166. package/dist/providers/github/skill.generated.js +8 -0
  167. package/dist/providers/gmail/index.d.ts +51 -0
  168. package/dist/providers/gmail/index.js +1436 -0
  169. package/dist/providers/gmail/skill.generated.js +9 -0
  170. package/dist/providers/infisical/index.d.ts +7 -0
  171. package/dist/providers/infisical/index.js +713 -0
  172. package/dist/providers/infisical/skill.generated.js +8 -0
  173. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  174. package/dist/providers/linear/index.js +352 -0
  175. package/dist/providers/linear/skill.generated.js +8 -0
  176. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  177. package/dist/providers/mixpanel/index.js +416 -0
  178. package/dist/providers/mixpanel/skill.generated.js +9 -0
  179. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  180. package/dist/providers/{notion.js → notion/index.js} +226 -239
  181. package/dist/providers/notion/skill.generated.js +10 -0
  182. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  183. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  184. package/dist/providers/overflow/skill.generated.js +8 -0
  185. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  186. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  187. package/dist/providers/planning-center/skill.generated.js +10 -0
  188. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  189. package/dist/providers/revenuecat/index.js +557 -0
  190. package/dist/providers/revenuecat/skill.generated.js +11 -0
  191. package/dist/providers/sheets/index.d.ts +54 -0
  192. package/dist/providers/sheets/index.js +1237 -0
  193. package/dist/providers/sheets/skill.generated.js +10 -0
  194. package/dist/providers/slides/index.d.ts +53 -0
  195. package/dist/providers/slides/index.js +2645 -0
  196. package/dist/providers/slides/skill.generated.js +9 -0
  197. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  198. package/dist/providers/stripe/index.js +266 -0
  199. package/dist/providers/stripe/skill.generated.js +11 -0
  200. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  201. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  202. package/dist/providers/tithely/skill.generated.js +8 -0
  203. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  204. package/dist/providers/vercel/index.js +1038 -0
  205. package/dist/providers/vercel/mcp-ownership.js +212 -0
  206. package/dist/providers/vercel/skill.generated.js +11 -0
  207. package/dist/registry.d.ts +110 -171
  208. package/dist/registry.js +588 -927
  209. package/dist/request-state.js +21 -0
  210. package/dist/resource-read.js +103 -0
  211. package/dist/result-shapes.js +17 -49
  212. package/dist/retired-toolkits.js +1 -1
  213. package/dist/routes/access-tokens.js +3 -6
  214. package/dist/routes/activity-context.js +21 -0
  215. package/dist/routes/activity-disclosure.js +40 -0
  216. package/dist/routes/activity.js +59 -21
  217. package/dist/routes/actor-labels.js +3 -50
  218. package/dist/routes/connect.js +221 -0
  219. package/dist/routes/credentials.js +15 -27
  220. package/dist/routes/mcp.js +365 -130
  221. package/dist/routes/oauth-client-metadata.js +15 -0
  222. package/dist/routes/oauth-management.js +45 -107
  223. package/dist/routes/oauth.js +110 -57
  224. package/dist/routes/operator.js +3 -3
  225. package/dist/routes/shared.d.ts +25 -64
  226. package/dist/routes/shared.js +160 -80
  227. package/dist/routes/skills.js +30 -0
  228. package/dist/routes/ui-config.js +225 -0
  229. package/dist/routes/ui-data.js +40 -35
  230. package/dist/routes/ui.js +66 -56
  231. package/dist/runtime/admission.js +4 -2
  232. package/dist/runtime/call-admission.js +3 -1
  233. package/dist/runtime/connector-scope.js +1 -4
  234. package/dist/runtime/run.js +10 -39
  235. package/dist/runtime/services.js +12 -117
  236. package/dist/runtime/shared-read.js +1 -3
  237. package/dist/runtime/storage.js +8 -1
  238. package/dist/sent-secrets.d.ts +93 -0
  239. package/dist/sent-secrets.js +1013 -0
  240. package/dist/server.js +28 -71
  241. package/dist/skills.js +416 -174
  242. package/dist/sqlite.d.ts +48 -0
  243. package/dist/sqlite.js +181 -0
  244. package/dist/storage/keys.js +443 -0
  245. package/dist/storage/memory.js +16 -4
  246. package/dist/storage/sql.d.ts +66 -0
  247. package/dist/storage/sql.js +543 -0
  248. package/dist/timeout.js +4 -2
  249. package/dist/tool-name.js +17 -0
  250. package/dist/tool-safety.d.ts +9 -35
  251. package/dist/tool-safety.js +20 -40
  252. package/dist/types.d.ts +336 -56
  253. package/dist/types.js +0 -1
  254. package/dist/ui.d.ts +7 -19
  255. package/dist/ui.js +76 -90
  256. package/dist/url-safety.js +3 -6
  257. package/dist/usage-guide.js +146 -0
  258. package/dist/validate.d.ts +7 -0
  259. package/dist/validate.js +309 -47
  260. package/dist/version.d.ts +1 -7
  261. package/dist/version.js +3 -7
  262. package/dist/worker.d.ts +11 -0
  263. package/dist/worker.js +435 -0
  264. package/documentation/README.md +63 -0
  265. package/documentation/architecture.md +696 -231
  266. package/documentation/auth.md +1069 -546
  267. package/documentation/code-mode.md +662 -356
  268. package/documentation/deploying.md +146 -0
  269. package/documentation/integrating.md +159 -0
  270. package/documentation/meta-tools.md +570 -216
  271. package/documentation/operating.md +135 -0
  272. package/documentation/operator-tests.md +97 -0
  273. package/documentation/operator-ui.md +132 -0
  274. package/documentation/provider-migration-0.29.md +65 -0
  275. package/examples/worker/AGENTS.md +34 -12
  276. package/examples/worker/README.md +293 -255
  277. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  278. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  279. package/examples/worker/src/connecta.config.ts +151 -0
  280. package/examples/worker/src/index.ts +14 -197
  281. package/examples/worker/wrangler.jsonc +15 -35
  282. package/package.json +126 -54
  283. package/templates/node/.dockerignore +1 -2
  284. package/templates/node/.env.example +20 -21
  285. package/templates/node/AGENTS.md +21 -17
  286. package/templates/node/Dockerfile +3 -5
  287. package/templates/node/README.md +140 -58
  288. package/templates/node/docker-compose.yml +11 -12
  289. package/templates/node/package.json +4 -2
  290. package/templates/node/src/connecta.config.ts +119 -0
  291. package/templates/node/src/index.ts +9 -154
  292. package/templates/node/src/provision-token.ts +17 -0
  293. package/AGENTS.md +0 -170
  294. package/dist/artifacts/connector.d.ts +0 -58
  295. package/dist/artifacts/connector.js +0 -730
  296. package/dist/artifacts/document.js +0 -86
  297. package/dist/artifacts/guide.js +0 -140
  298. package/dist/artifacts/html-scan.js +0 -329
  299. package/dist/artifacts/json.js +0 -78
  300. package/dist/artifacts/kv-store.d.ts +0 -20
  301. package/dist/artifacts/kv-store.js +0 -140
  302. package/dist/artifacts/markdown.js +0 -478
  303. package/dist/artifacts/module.d.ts +0 -37
  304. package/dist/artifacts/module.js +0 -66
  305. package/dist/artifacts/operations.d.ts +0 -197
  306. package/dist/artifacts/operations.js +0 -1002
  307. package/dist/artifacts/refresh.d.ts +0 -55
  308. package/dist/artifacts/refresh.js +0 -236
  309. package/dist/artifacts/routes.js +0 -206
  310. package/dist/artifacts/types.d.ts +0 -214
  311. package/dist/artifacts/types.js +0 -22
  312. package/dist/artifacts/validate.d.ts +0 -62
  313. package/dist/artifacts/validate.js +0 -651
  314. package/dist/artifacts.d.ts +0 -6
  315. package/dist/artifacts.js +0 -9
  316. package/dist/auth/bearer.d.ts +0 -11
  317. package/dist/auth/bearer.js +0 -50
  318. package/dist/providers/basecamp.js +0 -445
  319. package/dist/providers/linear.js +0 -217
  320. package/dist/providers/mixpanel.js +0 -247
  321. package/dist/providers/revenuecat.js +0 -317
  322. package/dist/providers/stripe.js +0 -253
  323. package/dist/providers/vercel.js +0 -1253
  324. package/dist/storage/file.d.ts +0 -15
  325. package/dist/storage/file.js +0 -455
  326. package/ethos.md +0 -93
  327. package/examples/worker/src/cloudflare-kv.ts +0 -43
  328. package/examples/worker/src/d1-activity-row.ts +0 -155
  329. package/examples/worker/src/d1-activity.ts +0 -147
  330. package/examples/worker/src/d1-storage.ts +0 -138
  331. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  332. package/templates/node/src/file-activity.ts +0 -182
package/dist/config.js ADDED
@@ -0,0 +1,508 @@
1
+ // ConnectaConfig as one schema.
2
+ //
3
+ // The schema below is the canonical record of the configuration surface. It
4
+ // produces the TypeScript type a deployment writes (`ConnectaConfig`), the
5
+ // unknown-key rejection createConnecta runs before reading anything, every
6
+ // default (src/config-defaults.ts), and one validation policy: a present value
7
+ // that is wrong throws with its path at construction (INV-11). Each field's
8
+ // default and operator-facing meaning belongs in its doc comment here, where a
9
+ // deployment author reads it from the editor.
10
+ //
11
+ // `resolveConfig` turns an accepted config into the `ResolvedConfig` core
12
+ // reads, with defaults applied once, so no later layer re-derives a value or
13
+ // re-plumbs a field.
14
+ import { CONFIG_DEFAULTS as D } from "./config-defaults.js";
15
+ import { array, ConfigError, fn, object, opaque, record, required, seconds, readPlain, text, whole, } from "./config-schema.js";
16
+ import { assertExecutor } from "./executor-contract.js";
17
+ import { memoryStorage } from "./storage/memory.js";
18
+ import { CONNECTA_VERSION } from "./version.js";
19
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
20
+ const STORAGE_METHODS = ["get", "set", "delete", "list", "compareAndSet"];
21
+ const hasMethods = (value, methods) => isObject(value) && methods.every((method) => typeof value[method] === "function");
22
+ /** An absolute http(s) URL without credentials; a request's Host never substitutes for it. */
23
+ function httpUrl(value, path) {
24
+ let url;
25
+ try {
26
+ url = typeof value === "string" ? new URL(value) : undefined;
27
+ }
28
+ catch {
29
+ url = undefined;
30
+ }
31
+ if (!url || (url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password) {
32
+ throw new ConfigError(`${path} must be an absolute http(s) URL without credentials.`);
33
+ }
34
+ }
35
+ function isExactOrigin(value) {
36
+ if (typeof value !== "string")
37
+ return false;
38
+ try {
39
+ const url = new URL(value);
40
+ return (url.protocol === "http:" || url.protocol === "https:") && url.origin === value;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ /** Explicit machine credentials take precedence over ambient browser identity. */
47
+ function normalizeAuth(auth) {
48
+ const rank = (provider) => (provider.kind === "access_token" ? 0 : 1);
49
+ return Object.freeze([...auth].sort((a, b) => rank(a) - rank(b)));
50
+ }
51
+ /**
52
+ * Resolve `logger`. `"silent"` discards every line; an omitted logger writes
53
+ * to the console with the `[connecta]` prefix.
54
+ */
55
+ function resolveLogger(logger) {
56
+ if (logger === "silent") {
57
+ return { debug() { }, info() { }, warn() { }, error() { } };
58
+ }
59
+ return (logger ?? {
60
+ debug: (...a) => console.debug("[connecta]", ...a),
61
+ info: (...a) => console.info("[connecta]", ...a),
62
+ warn: (...a) => console.warn("[connecta]", ...a),
63
+ error: (...a) => console.error("[connecta]", ...a),
64
+ });
65
+ }
66
+ const discovery = {
67
+ /**
68
+ * Maximum connector catalogs/status probes fetched at once by discovery
69
+ * operations. Default 4.
70
+ */
71
+ concurrency: whole({ min: 1, default: D.discovery.concurrency }),
72
+ /** Fallback tool-list TTL when the downstream omits ttlMs. Default 300 seconds. */
73
+ catalogTtlSeconds: seconds({ default: D.discovery.catalogTtlSeconds }),
74
+ /** Floor for positive downstream hints. Zero hints always disable reuse. Default 0. */
75
+ catalogMinTtlSeconds: seconds({ default: D.discovery.catalogMinTtlSeconds }),
76
+ /** Ceiling for downstream hints and the fallback, capped by the SDK at 24h. Default 86400. */
77
+ catalogMaxTtlSeconds: seconds({ default: D.discovery.catalogMaxTtlSeconds }),
78
+ /**
79
+ * Deadline (ms) for each downstream probe/catalog call fanned out by
80
+ * `search_tools` and by `connecta.search`/`connecta.describe` inside
81
+ * `execute_code`. Defaults to 30_000. A timed-out connector degrades
82
+ * independently; this does not apply to tool calls. Catalog walks receive the
83
+ * same cancellation signal, which aborts an in-flight page where supported and
84
+ * prevents another from starting.
85
+ */
86
+ probeTimeoutMs: whole({ min: 1, default: D.discovery.probeTimeoutMs }),
87
+ };
88
+ const calls = {
89
+ /**
90
+ * Deadline (ms) for `call_tool`/`call_destructive_tool` calls that pass no
91
+ * `timeoutMs`. An explicit per-call value wins. Opt-in: unset by default, so
92
+ * existing long-running calls gain no surprise deadline.
93
+ *
94
+ * Each call makes one attempt. `execute_code` host calls are
95
+ * unaffected because they already carry their own bound.
96
+ */
97
+ defaultTimeoutMs: whole({ min: 1 }),
98
+ /**
99
+ * Max inline result size (bytes) before truncation and `connecta.result` paging.
100
+ * A positive whole number; default 24_000. Connectors may override it
101
+ * individually.
102
+ */
103
+ maxResultBytes: whole({ min: 1, default: D.calls.maxResultBytes }),
104
+ };
105
+ const results = {
106
+ /** Stored bytes, including the paging envelope. Default 8 MiB. Zero disables stashing. */
107
+ maxStashBytes: whole({ min: 0, default: D.results.maxStashBytes }),
108
+ /** Stored or in-flight entries. Default 64. Zero disables stashing. */
109
+ maxStashEntries: whole({ min: 0, default: D.results.maxStashEntries }),
110
+ };
111
+ const execute = {
112
+ /**
113
+ * Aggregate serialized bytes `connecta.emit` accepts per run. Default
114
+ * 4_000_000 — a transport bound, not a context bound: emitted image/audio
115
+ * blocks reach the model as media, not base64 text.
116
+ */
117
+ maxEmittedBytes: whole({ min: 1, default: D.execute.maxEmittedBytes }),
118
+ /** Content blocks `connecta.emit` accepts per run. Default 32. */
119
+ maxEmittedBlocks: whole({ min: 1, default: D.execute.maxEmittedBlocks }),
120
+ /** Host calls one program may make. Default 20. */
121
+ maxHostCalls: whole({ min: 1, default: D.execute.maxHostCalls }),
122
+ /**
123
+ * Deadline for each host call a program makes, in milliseconds. Default
124
+ * 15_000. Raise it for providers whose legitimate calls run longer, such as
125
+ * analytics queries; `call_tool`'s own `timeoutMs` is unaffected.
126
+ */
127
+ hostCallTimeoutMs: whole({ min: 1, default: D.execute.hostCallTimeoutMs }),
128
+ /**
129
+ * Hard ceiling on one execution, in milliseconds, enforced outside the
130
+ * sandbox. Default 120_000, above both executors' own deadlines (QuickJS
131
+ * 30_000, the Dynamic Worker 60_000), so it only ends a run whose executor
132
+ * never settled; that run fails as unresponsive and its admission slot is
133
+ * released. Keep it above any raised executor deadline.
134
+ */
135
+ watchdogMs: whole({ min: 1, default: D.execute.watchdogMs }),
136
+ /**
137
+ * Trusted-pool writes one program may send, on top of the host-call
138
+ * budget every call already spends. Default 10.
139
+ */
140
+ maxWrites: whole({ min: 1, default: D.execute.maxWrites }),
141
+ };
142
+ function admissionPool(defaults) {
143
+ return {
144
+ /** Simultaneous work admitted to this pool. */
145
+ concurrency: whole({ min: 1, default: defaults.concurrency }),
146
+ /** Callers allowed to wait behind active work. Set zero to fail fast. */
147
+ maxQueueSize: whole({ min: 0, default: defaults.maxQueueSize }),
148
+ /** Maximum queue wait in milliseconds. */
149
+ queueTimeoutMs: whole({ min: 1, default: defaults.queueTimeoutMs }),
150
+ /** Retry hint returned with overload failures, in milliseconds. */
151
+ retryAfterMs: whole({ min: 0, default: defaults.retryAfterMs }),
152
+ };
153
+ }
154
+ const codeAdmission = admissionPool(D.admission.code);
155
+ const requestAdmission = {
156
+ ...admissionPool(D.admission.requests),
157
+ /** Hard lifetime of an admitted /mcp request. Default 300,000 ms. */
158
+ maxDurationMs: whole({
159
+ min: 1,
160
+ max: 2_147_483_647,
161
+ unit: "milliseconds",
162
+ default: D.admission.requests.maxDurationMs,
163
+ }),
164
+ };
165
+ const admission = {
166
+ /**
167
+ * The `/mcp` request boundary. Defaults to 16 active, 32 queued, and a
168
+ * 5-second maximum wait.
169
+ */
170
+ requests: object(requestAdmission),
171
+ /**
172
+ * Fallback pool for an `executor` that does not implement its own `acquire`.
173
+ * Defaults to 2 active, 8 queued, and a 5-second maximum wait. Bounded
174
+ * executors (including `quickJsExecutor`) keep their own tighter pool.
175
+ */
176
+ code: object(codeAdmission),
177
+ };
178
+ const identity = {
179
+ /**
180
+ * What this admitted identity may discover and call: `"all"`, or a list
181
+ * whose entries are connector ids (the whole connector) and `connector.tool`
182
+ * addresses (that tool only), or `{ tool: "connector.tool",
183
+ * requireReadOnly: true }` for an exact tool only while its loaded catalog
184
+ * explicitly classifies it read-only. Grants are additive: an unrestricted
185
+ * connector or address grant wins over the guarded form. An address naming
186
+ * a tool the catalog lacks is unreachable and warned once, never widened.
187
+ */
188
+ connectorAccess: fn(),
189
+ /** Global payload-free activity reads. Defaults to interactive humans. */
190
+ activityAccess: fn(),
191
+ /** Shared credential and OAuth administration. Defaults to none. */
192
+ credentialAdministration: fn(),
193
+ /** Client-token lifecycle management by interactive humans. Defaults to false. */
194
+ accessTokenManagement: fn(),
195
+ /** Connecting or changing the caller's personal account. Defaults to none. */
196
+ personalConnection: fn(),
197
+ };
198
+ const trust = () => opaque({
199
+ check: (value, path) => {
200
+ if (value !== "trusted" && value !== "read-only") {
201
+ throw new ConfigError(`${path} must be "trusted" or "read-only".`);
202
+ }
203
+ },
204
+ resolve: (value) => value ?? "read-only",
205
+ });
206
+ const pool = {
207
+ /** Programs may write only in trusted pools. Default read-only. */
208
+ trust: trust(),
209
+ /** Connector ids and exact `connector.tool` addresses in this pool. */
210
+ tools: required(opaque({
211
+ check: (value, path) => {
212
+ if (!Array.isArray(value)) {
213
+ throw new ConfigError(`${path} must be an array of connector ids or connector.tool addresses.`);
214
+ }
215
+ },
216
+ })),
217
+ /**
218
+ * Whether this admitted identity may open the pool. Denied by default:
219
+ * a pool with no grant serves nobody. A false return, a throw, and an
220
+ * undeclared pool name are the same 404.
221
+ */
222
+ grant: fn(),
223
+ };
224
+ const icon = {
225
+ src: required(text()),
226
+ mimeType: text(),
227
+ sizes: array(text()),
228
+ };
229
+ const serverInfo = {
230
+ name: text(),
231
+ version: text(),
232
+ /** Human-readable name clients may show instead of `name`. */
233
+ title: text(),
234
+ /** Homepage clients may link from the server listing: an absolute http(s) URL. */
235
+ websiteUrl: opaque({ check: httpUrl }),
236
+ /** MCP icons-spec entries; clients render these instead of a scraped favicon. */
237
+ icons: array(object(icon)),
238
+ };
239
+ const connectaConfig = {
240
+ connectors: required(array(opaque({
241
+ check: (value, path) => {
242
+ if (isObject(value) && Object.hasOwn(value, "approval")) {
243
+ throw new ConfigError(`${path}.approval was removed; configure pool trust instead.`);
244
+ }
245
+ if (!isObject(value) ||
246
+ typeof value.id !== "string" ||
247
+ typeof value.listTools !== "function" ||
248
+ typeof value.callTool !== "function") {
249
+ throw new ConfigError(`${path} must be a Connector: an object with an id, listTools, and callTool.`);
250
+ }
251
+ },
252
+ }))),
253
+ /** Inbound auth adapters, such as clerkAuth(...); omit for open (dev). */
254
+ auth: opaque({
255
+ check: (value, path) => {
256
+ const list = Array.isArray(value) ? value : [value];
257
+ list.forEach((provider, index) => {
258
+ if (!isObject(provider) ||
259
+ typeof provider.kind !== "string" ||
260
+ typeof provider.authorize !== "function" ||
261
+ ["recognizesCredential", "handleMetadata", "challenge"].some((key) => provider[key] !== undefined && typeof provider[key] !== "function")) {
262
+ throw new ConfigError(`${Array.isArray(value) ? `${path}[${index}]` : path} must be an inbound auth adapter.`);
263
+ }
264
+ if (provider.recognizesCredential !== undefined &&
265
+ Object.prototype.toString.call(provider.recognizesCredential) === "[object AsyncFunction]") {
266
+ throw new ConfigError(`${Array.isArray(value) ? `${path}[${index}]` : path}.recognizesCredential must be synchronous.`);
267
+ }
268
+ if ("finalRefusals" in provider) {
269
+ throw new ConfigError(`${Array.isArray(value) ? `${path}[${index}]` : path}.finalRefusals is retired; use synchronous recognizesCredential.`);
270
+ }
271
+ });
272
+ },
273
+ resolve: (value) => normalizeAuth(value === undefined ? [] : Array.isArray(value) ? value : [value]),
274
+ }),
275
+ /** Code-derived connection visibility and independent management permissions. */
276
+ identity: object(identity),
277
+ /** Named tool pools, each served at `/mcp/<name>` to identities its grant admits. */
278
+ pools: record(object(pool)),
279
+ /** Trust of the default /mcp endpoint. Default read-only. */
280
+ trust: trust(),
281
+ /** Exact connector id -> tool name -> verdict. Unknown names fail at catalog publication. */
282
+ classification: record(record(opaque({
283
+ check: (value, path) => {
284
+ if (value !== "read" && value !== "write") {
285
+ throw new ConfigError(`${path} must be "read" or "write".`);
286
+ }
287
+ },
288
+ }))),
289
+ /**
290
+ * The deployment's one store: `d1Storage(env.CONNECTA_DB)` from
291
+ * `@zackbart/connecta/d1` on Workers, `sqliteStorage(path)` from
292
+ * `@zackbart/connecta/sqlite` on Node. Defaults to memoryStorage(), which
293
+ * forgets everything on restart.
294
+ * Custom adapters must declare `capabilities.absoluteExpiry: true` and honor
295
+ * `expiresAtMs` on set and compareAndSet; older adapters fail construction.
296
+ */
297
+ storage: opaque({
298
+ // Every subsystem relies on the atomic claim and enumeration, so storage
299
+ // without them is refused here rather than at the first OAuth callback.
300
+ // The likeliest cause is a 0.28 deployment still passing its Workers KV
301
+ // adapter, which could never offer either guarantee.
302
+ check: (value, path) => {
303
+ const missing = STORAGE_METHODS.filter((method) => !hasMethods(value, [method]));
304
+ if (missing.length > 0)
305
+ throw new ConfigError(`${path} must implement ${missing.join(" and ")}. ` +
306
+ "Use d1Storage(env.CONNECTA_DB) from @zackbart/connecta/d1 on Workers, " +
307
+ "sqliteStorage(path) from @zackbart/connecta/sqlite on Node, or " +
308
+ "memoryStorage() in tests. Workers KV is not supported: it is eventually " +
309
+ "consistent and cannot compare-and-set.");
310
+ if (!isObject(value) || !isObject(value.capabilities) || value.capabilities.absoluteExpiry !== true) {
311
+ throw new ConfigError(`${path} must declare capabilities.absoluteExpiry: true and honor expiresAtMs on set and compareAndSet. ` +
312
+ "Upgrade the storage adapter before declaring this capability.");
313
+ }
314
+ },
315
+ resolve: (value) => value ?? memoryStorage(),
316
+ }),
317
+ /**
318
+ * Public base URL. Defaults to the request origin per-request. Configuring an
319
+ * HTTPS URL also redirects matching inbound HTTP requests to HTTPS.
320
+ */
321
+ publicUrl: opaque({ check: httpUrl }),
322
+ /**
323
+ * Exact browser MCP origins, or "*". Defaults to publicUrl's origin and
324
+ * HTTP(S) loopback origins at any port. Originless clients are admitted.
325
+ */
326
+ allowedOrigins: opaque({
327
+ check: (value) => {
328
+ if (value !== "*" && (!Array.isArray(value) || !value.every(isExactOrigin))) {
329
+ throw new ConfigError('ConnectaConfig.allowedOrigins must be an array of exact HTTP(S) origins or "*".');
330
+ }
331
+ },
332
+ }),
333
+ /** Optional recorder and reader, created by activityHistory() from /activity. */
334
+ activity: opaque({
335
+ check: (value) => {
336
+ if (!isObject(value) || typeof value.recordTool !== "function") {
337
+ throw new ConfigError("ConnectaConfig.activity must be created with activityHistory(...)");
338
+ }
339
+ },
340
+ }),
341
+ /** Replaceable owner-partitioned credential storage. Omit for config-owned secrets. */
342
+ vault: opaque({
343
+ check: (value) => {
344
+ if (!hasMethods(value, ["get", "getAll", "set", "setAll", "metadata", "delete"]) ||
345
+ ["seal", "open", "signOAuthHandoff", "verifyOAuthHandoff"].some((key) => !["undefined", "function"].includes(typeof value[key]))) {
346
+ throw new ConfigError("ConnectaConfig.vault must implement CredentialVault");
347
+ }
348
+ },
349
+ }),
350
+ /** Optional connection UI, created by operatorUi() from /ui. */
351
+ ui: opaque({
352
+ check: (value) => {
353
+ if (!hasMethods(value, ["handle", "credentialHandoffUrl"]) ||
354
+ !Array.isArray(value.reservedPaths)) {
355
+ throw new ConfigError("ConnectaConfig.ui must be created with operatorUi(...)");
356
+ }
357
+ },
358
+ }),
359
+ /** Optional managed client tokens, created by accessTokens() from /auth/access-tokens. */
360
+ accessTokens: opaque({
361
+ check: (value) => {
362
+ const auth = isObject(value) ? value.auth : undefined;
363
+ if (!hasMethods(value, ["handle"]) ||
364
+ !isObject(auth) ||
365
+ typeof auth.authorize !== "function" ||
366
+ auth.interactiveOperator) {
367
+ throw new ConfigError("ConnectaConfig.accessTokens must be created with accessTokens(storage) from " +
368
+ "@zackbart/connecta/auth/access-tokens; reuse your existing storage to preserve tokens");
369
+ }
370
+ },
371
+ }),
372
+ /** Tool-catalog TTL bounds and probe deadlines. */
373
+ discovery: object(discovery),
374
+ /** Deployment-wide call deadlines and result paging threshold. */
375
+ calls: object(calls),
376
+ /** Runtime-wide transient result stash limits, shared across subjects. */
377
+ results: object(results),
378
+ /** Budgets for execute_code programs: host calls, writes, and `connecta.emit`. */
379
+ execute: object(execute),
380
+ /**
381
+ * Runtime-portable server-memory boundaries. `/health` and operator routes
382
+ * do not consume these permits, so they remain responsive during MCP
383
+ * saturation.
384
+ */
385
+ admission: object(admission),
386
+ /** Diagnostic output. Use "silent" to disable all diagnostic logging. */
387
+ logger: opaque({
388
+ check: (value, path) => {
389
+ if (value !== "silent" && !hasMethods(value, ["debug", "info", "warn", "error"])) {
390
+ throw new ConfigError(`${path} must be a Logger or "silent".`);
391
+ }
392
+ },
393
+ resolve: resolveLogger,
394
+ }),
395
+ serverInfo: object(serverInfo),
396
+ /** Deployment metadata exposed by /health (for example a Worker version). */
397
+ deploymentInfo: opaque({
398
+ check: (value, path) => {
399
+ if (!isObject(value))
400
+ throw new ConfigError(`${path} must be an object.`);
401
+ },
402
+ }),
403
+ /**
404
+ * Required sandbox for `execute_code`. Workers use
405
+ * `workerExecutor({ loader: env.LOADER })` from
406
+ * `@zackbart/connecta/worker`; Node uses `quickJsExecutor()` from
407
+ * `@zackbart/connecta/quickjs`. Direct upstream DynamicWorkerExecutor
408
+ * construction throws because its request-owned handles cannot be released.
409
+ * Custom sandboxes explicitly opt in with
410
+ * `customExecutor(myExecutor, { lifecycle: "self-managed" })`.
411
+ */
412
+ executor: required(opaque({
413
+ check: (value) => assertExecutor(value),
414
+ }), () => "ConnectaConfig.executor is required. Configure quickJsExecutor() from " +
415
+ '"@zackbart/connecta/quickjs" on Node, or ' +
416
+ "workerExecutor({ loader: env.LOADER }) from " +
417
+ '"@zackbart/connecta/worker" around DynamicWorkerExecutor on Workers.'),
418
+ };
419
+ const CONFIG = object(connectaConfig);
420
+ /** Pause-only options #672 removed with `resume_execution`. */
421
+ const RETIRED_PAUSE_OPTIONS = ["ConnectaConfig.execute.resumableWrites", "ConnectaConfig.execute.pausedRunTtlSeconds"];
422
+ function rejectUnknownOptions(paths) {
423
+ if (paths.length === 0)
424
+ return;
425
+ throw new ConfigError(`Unknown Connecta configuration option${paths.length === 1 ? "" : "s"}:\n` +
426
+ paths.map((path) => `- ${path}`).join("\n") +
427
+ (paths.includes("ConnectaConfig.credentials")
428
+ ? "\nUse vault: encryptedCredentialVault(storage, key) from @zackbart/connecta/credentials."
429
+ : "") +
430
+ (paths.includes("ConnectaConfig.branding")
431
+ ? "\nMove branding into ui: operatorUi({ branding }) from @zackbart/connecta/ui."
432
+ : "") +
433
+ (paths.includes("ConnectaConfig.execute.approval")
434
+ ? '\nexecute.approval was removed. Set trust: "trusted" on a pool to allow program writes; the default is read-only.'
435
+ : "") +
436
+ (paths.some((path) => RETIRED_PAUSE_OPTIONS.includes(path))
437
+ ? "\nPrograms no longer pause at writes, so there is nothing to configure: read-only pools route writes through call_destructive_tool; trusted pools allow program writes (issue #672). Delete the option."
438
+ : ""));
439
+ }
440
+ /** Checks that span fields; each value is already individually valid. */
441
+ function assertCoherent(config) {
442
+ if (config.discovery.catalogMinTtlSeconds > config.discovery.catalogMaxTtlSeconds ||
443
+ config.discovery.catalogMaxTtlSeconds > 86_400) {
444
+ throw new ConfigError("ConnectaConfig.discovery requires catalogMinTtlSeconds <= catalogMaxTtlSeconds <= 86400.");
445
+ }
446
+ }
447
+ /**
448
+ * Validate a configuration and apply every default. Unknown keys are refused
449
+ * first, before any value is read, so a typo beside a getter or a secret
450
+ * neither runs it nor echoes it; then each present value is checked in
451
+ * schema order and the first mistake throws with its path.
452
+ */
453
+ export function resolveConfig(config) {
454
+ return readConfig(config).resolved;
455
+ }
456
+ /**
457
+ * {@link resolveConfig}, also returning the plain copy it resolved: the
458
+ * configuration as read through property descriptors, which is what any later
459
+ * question about what the deployment set explicitly must read, never the
460
+ * caller's object, whose getters or Proxy traps must not run.
461
+ */
462
+ export function readConfig(config) {
463
+ const read = readPlain(config, "ConnectaConfig", CONFIG);
464
+ rejectUnknownOptions(read.unknown);
465
+ const input = read.value;
466
+ const parsed = CONFIG.parse(input, "ConnectaConfig");
467
+ assertCoherent(parsed);
468
+ const info = {
469
+ ...parsed.serverInfo,
470
+ name: parsed.serverInfo.name ?? "connecta",
471
+ version: parsed.serverInfo.version ?? CONNECTA_VERSION,
472
+ };
473
+ const resolved = Object.freeze({
474
+ ...parsed,
475
+ // A managed-token module contributes one more provider, ordered with the rest.
476
+ auth: parsed.accessTokens ? normalizeAuth([parsed.accessTokens.auth, ...parsed.auth]) : parsed.auth,
477
+ serverInfo: Object.freeze(info),
478
+ });
479
+ return { input, resolved };
480
+ }
481
+ /**
482
+ * Declare a deployment's configuration as a function of its environment:
483
+ * `process.env` on Node, the Worker's `env` bindings on Workers. Optional
484
+ * modules are ordinary type-checked expressions —
485
+ * `vault: env.KEY ? encryptedCredentialVault(storage, env.KEY) : undefined` —
486
+ * rather than commented blocks, and the entry file stays a few lines: it
487
+ * imports the default export of its `connecta.config.ts` and calls
488
+ * `createConnecta(config(process.env))`.
489
+ *
490
+ * The factory runs when the entry calls it, never at import, so a Worker can
491
+ * import it at global scope. Validation still happens in createConnecta.
492
+ */
493
+ export function defineConfig(factory) {
494
+ return factory;
495
+ }
496
+ /** The execute_code budgets a runner enforces, read from one resolved config. */
497
+ export function executeLimits(config) {
498
+ return {
499
+ discoveryConcurrency: config.discovery.concurrency,
500
+ probeTimeoutMs: config.discovery.probeTimeoutMs,
501
+ maxEmittedBytes: config.execute.maxEmittedBytes,
502
+ maxEmittedBlocks: config.execute.maxEmittedBlocks,
503
+ maxHostCalls: config.execute.maxHostCalls,
504
+ hostCallTimeoutMs: config.execute.hostCallTimeoutMs,
505
+ watchdogMs: config.execute.watchdogMs,
506
+ maxWrites: config.execute.maxWrites,
507
+ };
508
+ }
@@ -2,6 +2,7 @@ import type { ToolAccess } from "./registry.js";
2
2
  import type { AuthenticatedIdentity } from "./types.js";
3
3
  /** A declared pool after construction-time validation. */
4
4
  export interface ResolvedPool {
5
+ trust: import("./tool-safety.js").PoolTrust;
5
6
  access: ConnectorAccess;
6
7
  grant(identity: Readonly<AuthenticatedIdentity>): boolean | Promise<boolean>;
7
8
  }
@@ -1,13 +1,10 @@
1
+ import { hasControlCharacters } from "./tool-name.js";
1
2
  export const POOL_NAME_RE = /^[a-z0-9_-]+$/;
2
3
  const CONNECTOR_ID_RE = /^[a-z0-9_-]+$/;
3
4
  // MCP does not restrict tool names, and remote servers ship spaced and
4
5
  // non-ASCII ones. Only control characters are refused, so a grant for a
5
6
  // legitimately named tool cannot 403 the whole identity at request time.
6
7
  const TOOL_ADDRESS_RE = /^[a-z0-9_-]+\..{1,256}$/su;
7
- const hasControlCharacter = (value) => [...value].some((ch) => {
8
- const code = ch.codePointAt(0);
9
- return code < 0x20 || code === 0x7f;
10
- });
11
8
  /**
12
9
  * Normalize a grant list. A bare connector id grants every tool on that
13
10
  * connector; a `connector.tool` address grants one tool. Grants are additive,
@@ -32,14 +29,19 @@ export function parseConnectorAccess(value, options = {}) {
32
29
  continue;
33
30
  }
34
31
  const isGuarded = typeof entry !== "string";
35
- if (isGuarded && (!options.allowReadOnly || entry === null ||
36
- typeof entry !== "object" || Array.isArray(entry) ||
37
- !Object.hasOwn(entry, "tool") || !Object.hasOwn(entry, "requireReadOnly") ||
38
- Object.keys(entry).length !== 2 || entry.requireReadOnly !== true)) {
32
+ if (isGuarded &&
33
+ (!options.allowReadOnly ||
34
+ entry === null ||
35
+ typeof entry !== "object" ||
36
+ Array.isArray(entry) ||
37
+ !Object.hasOwn(entry, "tool") ||
38
+ !Object.hasOwn(entry, "requireReadOnly") ||
39
+ Object.keys(entry).length !== 2 ||
40
+ entry.requireReadOnly !== true)) {
39
41
  throw new Error("invalid connector permission");
40
42
  }
41
43
  const address = isGuarded ? entry.tool : entry;
42
- if (typeof address !== "string" || !TOOL_ADDRESS_RE.test(address) || hasControlCharacter(address)) {
44
+ if (typeof address !== "string" || !TOOL_ADDRESS_RE.test(address) || hasControlCharacters(address)) {
43
45
  throw new Error("invalid connector permission");
44
46
  }
45
47
  const dot = address.indexOf(".");
@@ -85,9 +87,7 @@ export function parseConnectorAccess(value, options = {}) {
85
87
  export function intersectAccess(ceiling, pool) {
86
88
  if (pool.connectorIds === "all")
87
89
  return ceiling;
88
- const allowedIds = ceiling.connectorIds === "all"
89
- ? null
90
- : new Set(ceiling.connectorIds);
90
+ const allowedIds = ceiling.connectorIds === "all" ? null : new Set(ceiling.connectorIds);
91
91
  const connectorIds = [];
92
92
  const toolAccess = new Map();
93
93
  const guardedToolAccess = new Map();
@@ -108,9 +108,7 @@ export function intersectAccess(ceiling, pool) {
108
108
  const guarded = ceiling.guardedToolAccess?.get(id);
109
109
  if (guarded) {
110
110
  const allowed = toolAccess.get(id);
111
- const retained = allowed
112
- ? new Set([...guarded].filter((name) => allowed.has(name)))
113
- : guarded;
111
+ const retained = allowed ? new Set([...guarded].filter((name) => allowed.has(name))) : guarded;
114
112
  if (retained.size > 0)
115
113
  guardedToolAccess.set(id, retained);
116
114
  }
@@ -1,6 +1,14 @@
1
1
  import type { AuthenticatedIdentity, ConnectorContext } from "./types.js";
2
2
  export interface ConnectorCaller {
3
3
  identity: AuthenticatedIdentity;
4
+ /**
5
+ * Whether an inbound auth provider admitted this request. An open
6
+ * deployment admits every request as `{ kind: "anonymous" }`, and that is
7
+ * a caller without being anyone: code that acts *as* the caller downstream
8
+ * refuses it. Any provider's `ok` counts — an interactive user, a bearer
9
+ * with or without a subject, a Connecta-issued access token.
10
+ */
11
+ authenticated: boolean;
4
12
  /** The `/mcp/<pool>` the call arrived on, when it was a pool. */
5
13
  pool?: string;
6
14
  }
@@ -2,12 +2,21 @@
2
2
  //
3
3
  // `ConnectorContext` deliberately carries no identity: a deployment's own
4
4
  // connector acts for the deployment, and one that could read its caller would
5
- // invite per-user behavior the access rules never saw. The built-in artifacts
6
- // connector is the exception — every version it writes records who wrote it —
7
- // so the identity rides beside the context, the way the OAuth sealer does,
8
- // where only in-repo code can read it. Core sets it from the authorization
9
- // that admitted the request, on the scoped registry view that request
10
- // receives; no argument, header, or program can name it.
5
+ // invite per-user behavior the access rules never saw. Maintained providers are the exception, so the identity rides beside the context, the way
6
+ // the OAuth sealer does, where only in-repo code can read it:
7
+ //
8
+ // - maintained providers that act *as* the caller downstream — Google
9
+ // Workspace through domain-wide delegation (`src/providers/_shared/google/`) — which
10
+ // hand the identity to a deployment-config function that names the
11
+ // downstream subject (PRINCIPLES.md INV-3, #678). The
12
+ // provider never decides whose account to open; config does, from this.
13
+ //
14
+ // Core sets it from the authorization that admitted the request, on the scoped
15
+ // registry view that request receives, and `api()` hands handlers that same
16
+ // context object; no argument or program can name it, and a header only
17
+ // through an inbound-auth provider configured to read one (a bearer's asserted
18
+ // principal). A context no request admitted, such as an operator probe, has no caller, and a delegated provider fails closed on it,
19
+ // as it does on the anonymous caller an open deployment admits.
11
20
  const callers = new WeakMap();
12
21
  export function attachCaller(ctx, caller) {
13
22
  if (caller)
@@ -8,7 +8,7 @@ export function claimConnectorScopeCleanup(ctx, connectorId) {
8
8
  const scope = ctx.requestScope ?? ctx;
9
9
  let owners = cleanupOwners.get(scope);
10
10
  if (!owners)
11
- cleanupOwners.set(scope, owners = new Map());
11
+ cleanupOwners.set(scope, (owners = new Map()));
12
12
  owners.set(connectorId, (owners.get(connectorId) ?? 0) + 1);
13
13
  let released = false;
14
14
  return () => {