@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
@@ -4,72 +4,107 @@ One Web-standard `fetch(request) => Promise<Response>` handler, a long-lived
4
4
  registry behind it, and a strict rule about what may be imported. Everything
5
5
  else in this repository is a detail of those three things.
6
6
 
7
- Read [`ethos.md`](../ethos.md) first; this guide says how the shape it describes
8
- is assembled and where each subsystem lives. The surface itself belongs to
7
+ Read [PRINCIPLES.md](https://github.com/zackbart/connecta/blob/main/PRINCIPLES.md)
8
+ first; this guide explains the shape and where each subsystem lives. The surface itself belongs to
9
9
  [meta-tools](./meta-tools.md), [code mode](./code-mode.md), and
10
10
  [inbound auth](./auth.md).
11
11
 
12
+ ## Task routes
13
+
14
+ [Agent index](./README.md) routes endpoint use, deployment, integration, and
15
+ repository maintenance. Read this guide when changing the core or checking an
16
+ implementation boundary. Use these sections for the affected subsystem.
17
+
18
+ | Change or question | Sections |
19
+ | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | Request handling, transport, cleanup | [Two lifetimes](#the-two-lifetimes), [request lifecycle](#request-lifecycle), [admission](#admission-in-two-places) |
21
+ | Discovery, invocation, classification | [Layers below meta-tools](#layers-below-the-meta-tools), [providers and classification](#providers-and-reviewed-classification) |
22
+ | Storage, credentials, catalogs, paging | [Storage and connectors](#storage-credentials-and-connectors), [errors and records](#errors-and-records) |
23
+ | Config validation and optional features | [Configuration](#configuration), [modules](#optional-deployment-modules), [operator contract](./operator-ui.md#operator-data-contract) |
24
+ | Imports, executors, or Effect | [Purity](#import-graph-purity), [Effect](#effect-inside), [sharp edges](#sharp-edges) |
25
+ | Find code and deployment evidence | [Source map](#where-else-to-look), [sharp edges](#sharp-edges), [repository policy](https://github.com/zackbart/connecta/blob/main/AGENTS.md) |
26
+
27
+ Source paths and test names in each section identify its implementation and
28
+ regression evidence. Recorded production probes and bundle measurements are
29
+ historical observations tied to their stated date or version, not current
30
+ benchmarks or proof that planned replacements have shipped.
31
+
12
32
  ## The two lifetimes
13
33
 
14
34
  Almost every bug in this codebase is a lifetime mistake, so the split is worth
15
35
  stating before anything else.
16
36
 
17
37
  **Per isolate, built once.** `createConnecta(config)` returns
18
- `{ fetch, registry, close }` (`src/index.ts`). The `Registry` owns the connector
19
- set, address resolution, catalog caches, observed output schemas, connector
38
+ `{ fetch, registry, describeConfig, close }` (`src/index.ts`). The `Registry` owns the connector
39
+ set, address resolution, request-local catalog reads, observed output schemas, connector
20
40
  health, and the per-connector call limiters. It is built once and lives as long
21
41
  as the isolate — on Workers a lazy module-scope singleton, which is why both
22
42
  deployment shapes construct it outside the request handler.
23
43
 
24
- An OAuth connector — `remoteMcp()`, or `api()` with a static grant
25
- (`src/auth/static-oauth.ts`, the same provider with its discovery and client
26
- answered from config) — also owns a runtime-local refresh completion gate
27
- (`src/auth/downstream-oauth.ts`). It coordinates credential mutation across
28
- concurrent request scopes while sharing no client, transport, or response, and
29
- never lets a follower cancel the owner. The subtle part is that a valid token
30
- response consumes the refresh token whether or not the owner survives to save it,
31
- so the accepted tokens live on the flight: cancelling the owner *before* a valid
32
- response fails the joiners, because promoting one could replay a token the
33
- authorization server already consumed, while cancelling it *after* one does not —
34
- the host persists the rotation on its own write, holds contenders behind a
35
- generation-keyed pending-mutation marker until that write lands, and hands them
36
- the saved rotation ([#526](https://github.com/zackbart/connecta/issues/526)).
44
+ An OAuth connector, `remoteMcp()` or `api()` with a static grant, owns a
45
+ runtime-local refresh completion gate in `src/auth/downstream-oauth.ts`.
46
+ Joined scopes share the outcome while keeping their own request I/O and
47
+ cancellation. A shared-storage CAS lease prevents independent isolates from
48
+ redeeming the same refresh token concurrently. The holder commits an accepted
49
+ rotation to the grant by CAS before releasing waiters, even when the owner
50
+ cancels after dispatch. The HTTP exchange uses its own 20-second deadline,
51
+ detached from caller cancellation, and the runtime passes its completion and
52
+ commit to Workers `waitUntil`. Contenders read the committed tokens or typed
53
+ verdict. An unsent claim can be taken over after expiry. Dispatch uses a lease CAS
54
+ transition and a CAS of the token fingerprint into permanent spent storage
55
+ before sending. Fingerprint records have no TTL and carry outstanding,
56
+ ambiguous, or resolved state across Restart and Disconnect. Outstanding and
57
+ ambiguous fingerprints block every epoch. A resolved fingerprint can be sent
58
+ once in a later epoch only when its code exchange observed the resolved
59
+ storage write before completing. Holder liveness survives reset until
60
+ completion or expiry. Credential-bearing
61
+ token requests use manual fetch and bypass resource redirect handling. Every
62
+ 3xx requires re-consent after a refresh and refuses a code exchange. The HTTP
63
+ deadline is 20 seconds. A sent request
64
+ whose 120-second storage-owned liveness record expired without a commit is
65
+ never retried and requires re-consent. Valid rotations retry their grant commit
66
+ up to 32 times without another HTTP request; exhausted commits and all dispatched
67
+ provider failures require re-consent. [Auth](./auth.md#refresh-failures) describes the lease and failure
68
+ contracts.
37
69
 
38
70
  **Per request, and no longer.** The MCP server, its transport, downstream MCP
39
71
  clients, abort signals, and the connector scope a probe opens all belong to the
40
- request that created them. `Nothing request-bound survives a request` is an ethos
41
- invariant, not a style preference: a client retained across requests on Workers
42
- is a cross-request capability leak, and a promise awaited after the response is
72
+ request that created them. INV-7 requires this lifetime: a client retained
73
+ across requests on Workers is a cross-request capability leak, and a promise awaited after the response is
43
74
  work the runtime may already have torn down. Deferred work has one sanctioned
44
75
  channel, `ctx.waitUntil`, threaded through `fetch(request, env, ctx)` — activity
45
- writes use it, as does a stale-window catalog refresh, which owns a fresh scope
46
- and deadline rather than carrying the inbound one past the request. And a fresh
76
+ writes and dispatched OAuth grant commits use it. Catalog listing and SQL cache
77
+ publication finish in the requesting scope. A fresh
47
78
  `McpServer` per request is what makes the deployment stateless: no sessions, no
48
79
  server push, no stream resumability, scope resolved rather than remembered.
49
80
 
50
81
  ## Request lifecycle
51
82
 
52
83
  `src/server.ts` is the composition root: MCP origin check, scheme upgrade, route
53
- table, security headers. Route *order* is the contract — several routes would
84
+ table, security headers. Route _order_ is the contract — several routes would
54
85
  behave differently if they were reachable in another order — so read the table
55
86
  top to bottom.
56
87
 
57
- | Order | Route | Notes |
58
- | --- | --- | --- |
59
- | 0 | MCP Origin check | A disallowed `Origin` on `/mcp*` is a fixed 403 before redirects, admission, auth, or preflight — costing no permit and no auth lookup. Originless requests are admitted. |
60
- | 0 | HTTPS upgrade | 308 to an HTTPS `publicUrl`, with path and query *assigned* onto it rather than resolved against it, so a `//host` pathname cannot replace the origin. `/health` is exempt: a loopback probe must not need public DNS. |
61
- | 0 | Cloudflare Access (Worker, when enabled) | Edge admission ahead of this table; an admitted invocation carries trusted identity in `ctx.access`. |
62
- | 1 | Mounted UI routes | Before wildcard OPTIONS, so mutation routes refuse preflight rather than inheriting MCP CORS. No UI module, no routes. |
63
- | 2 | MCP preflight | Allowed `OPTIONS` on `/mcp*`: 204 without admission or auth. |
64
- | 2 | Other `OPTIONS` | Auth metadata first, otherwise compatibility CORS preflight. |
65
- | 3 | `/.well-known/*` | Auth metadata, or 404. |
66
- | 4 | `/health` | Open and payload-free: health, executor, admission, and deployment metadata, with drift as stable short hashes. |
67
- | 5 | `/oauth/callback/<connectorId>` | Core downstream OAuth completion, state and personal-ownership checked, independent of the UI. |
68
- | 6 | `/mcp`, `/mcp/<pool>` | Admission, then auth, then a request-local MCP server. An undeclared pool, a refusing grant, and a throwing grant are one identical 404; see [pools](./auth.md#pools). |
69
- | 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
70
-
71
- Every response leaves through `withSecurityHeaders`, and the UI module adds a
72
- nonce-based script CSP and framing denial to its shells.
88
+ | Order | Route | Notes |
89
+ | ----- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
90
+ | 0 | MCP Origin check | A disallowed `Origin` on `/mcp*` is a fixed 403 before redirects, admission, auth, or preflight — costing no permit and no auth lookup. Originless requests are admitted. |
91
+ | 0 | HTTPS upgrade | 308 to an HTTPS `publicUrl`, with path and query _assigned_ onto it rather than resolved against it, so a `//host` pathname cannot replace the origin. `/health` is exempt: a loopback probe must not need public DNS. |
92
+ | 0 | Cloudflare Access (Worker, when enabled) | Edge admission ahead of this table; an admitted invocation carries trusted identity in `ctx.access`. |
93
+ | 1 | `/oauth/client-metadata/<connectorId>` | Public GET-only CIMD for configured self-hosted OAuth connectors; fixed `publicUrl`, no auth or storage. |
94
+ | 1 | Mounted UI routes | Before wildcard OPTIONS, so mutation routes refuse preflight rather than inheriting MCP CORS. No UI module, no routes. |
95
+ | 1 | `/connect/<connectorId>`, `/oauth/callback/<connectorId>` | GET-only browser OAuth routes, before wildcard OPTIONS. Both verify the initiating user and management permission, independent of the UI. |
96
+ | 2 | MCP preflight | Allowed `OPTIONS` on `/mcp*`: 204 without admission or auth. |
97
+ | 2 | Other `OPTIONS` | Auth metadata first, otherwise compatibility CORS preflight. |
98
+ | 3 | `/.well-known/*` | Auth metadata, or 404. |
99
+ | 4 | `/health` | Open and payload-free: health, executor, admission, and deployment metadata. |
100
+ | 6 | `/mcp`, `/mcp/<pool>` | Admission, then auth, then a request-local MCP server. Body-confirmed modern listens skip admission and are refused after auth and SDK validation. An undeclared pool, a refusing grant, and a throwing grant are one identical 404; see [pools](./auth.md#pools). |
101
+ | 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
102
+
103
+ Every response leaves through `withSecurityHeaders`. HTML uses one common
104
+ security-header helper, including status and error pages. Operator shells
105
+ allow same-origin scripts and deny framing. Clerk pages also admit the validated
106
+ loader origin, the exact Cloudflare CAPTCHA script and frame host, Clerk images,
107
+ and first-party or blob workers. Local Clerk sign-in adds a nonce for its bootstrap.
73
108
  `test/server-route-contracts.test.ts` pins the ordering and the exact refusal
74
109
  bodies; it exists because the ordering is invisible in any one file and a
75
110
  reordering reads like a harmless refactor.
@@ -77,41 +112,112 @@ reordering reads like a harmless refactor.
77
112
  An admitted non-preflight `/mcp` request then takes five steps in
78
113
  `src/routes/mcp.ts`:
79
114
 
80
- 1. **Admit.** One permit from the deployment-wide pool, taken before auth so an
115
+ 1. **Admit.** Modern `subscriptions/listen` requests take no permit: their
116
+ mirrored method and parsed body must both name the unsupported method.
117
+ They still pass auth, pool grants, and SDK validation, then receive HTTP
118
+ 404 with JSON-RPC `-32601`. Their body classification and auth share the
119
+ configured request lifetime, starting before classification. Other requests
120
+ take one permit from the deployment-wide pool, taken before auth so an
81
121
  unauthenticated flood costs a permit rather than a Clerk lookup, and held
82
- until the response *body* completes, the caller leaves, or its configured
122
+ until the response _body_ completes, the caller leaves, or its configured
83
123
  lifetime ends, not until the handler returns.
84
- 2. **Authorize.** Each `InboundAuth` provider's `authorize` in order, bearer
85
- before interactive. First `ok` admits; if all fail, the last provider's
86
- challenge is returned. No providers means open — development only, and it
87
- warns at construction.
124
+ 2. **Authorize.** Machine tokens precede interactive providers. A provider's
125
+ synchronous credential recognition selects the sole verifier for an explicit
126
+ credential; a refusal never falls back to another identity. Human routes
127
+ reject machine credentials without verification. With no recognized
128
+ credential, configured providers are tried in order. A 401 takes its
129
+ challenge from the actual protected-resource metadata owner. No providers
130
+ means open development and warns at construction.
88
131
  3. **Narrow to the pool.** On `/mcp/<pool>`, look the name up, run its grant
89
132
  against the identity, then `intersectAccess` the pool with the identity's own
90
133
  access. A pool can never widen a view; anything else is a 404 naming no pool.
91
134
  4. **Derive the registry view.** One `registry.scoped(...)` call with those
92
135
  connector ids, exact `connector.tool` addresses when the identity declares a
93
136
  narrower slice, and the subject and principal keys. Personal connectors use
94
- the principal partition, result paging the subject partition, and no caller
95
- parameter selects either (`test/identity-scope.test.ts`).
137
+ the principal partition and result paging the subject partition with principal,
138
+ endpoint/pool, origin, connector, and tool bindings. Paging rechecks current
139
+ grants, pool membership, and trust; no caller parameter selects a partition (`test/identity-scope.test.ts`).
96
140
  5. **Serve.** Refuse `?toolkit=` with a 404 — the toolkits are gone
97
141
  ([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
98
142
  handed out, and retiring a scoping boundary into fail-open is worse than any
99
- 404 — then register the seven meta-tools on a fresh `McpServer`
143
+ 404 — then register the six meta-tools on a fresh `McpServer`
100
144
  (`test/server.test.ts`, `test/code-first-surface.test.ts`).
101
145
 
146
+ ### Modern request metadata and transport refusals
147
+
148
+ The SDK lifts each modern request's client declarations from `_meta` into
149
+ `ctx.mcpReq.envelope`. Meta-tool handlers bind `clientCapabilities` and
150
+ `clientInfo` to their request-local context before dispatch. These declarations
151
+ never select an identity, grant, connector view, or pool. Direct and program
152
+ activity records retain only the self-declared `clientName` and `clientVersion`,
153
+ validated by one ASCII grammar at the record builder, SQL write/read boundaries,
154
+ and authenticated activity read route. Names match
155
+ `[A-Za-z0-9][A-Za-z0-9 ._@/+-]{0,63}`; versions match
156
+ `[A-Za-z0-9][A-Za-z0-9._+-]{0,31}`, each as a whole string. Names allow up to
157
+ 64 characters, versions up to 32. `__proto__`, `constructor`, and `prototype`
158
+ are reserved. Invalid values are absent, never truncated or escaped. Capabilities and other client metadata never enter activity.
159
+ Legacy requests without an envelope leave client facts absent.
160
+ Every emitted activity event also carries `packageVersion`, generated from
161
+ `package.json` before build and independent of the display-only `serverInfo`.
162
+ Doctor uses and reports that same generated version. SQL stores validate
163
+ package versions against a bounded release-version grammar on write and read;
164
+ the nullable `package_version` migration leaves old rows unknown rather than
165
+ assigning today's version to history. `/ui/api/activity` serves these facts
166
+ read-only with the same authorization and validation as `/ui/activity`.
167
+ Catalog changes emit discrete, payload-free activity events after accepted
168
+ refreshes. Reviewed drift observations remain in authenticated connector status
169
+ and the maintainer-run provider check; public health counts are removed.
170
+
171
+ `server/discover` advertises `io.modelcontextprotocol/skills` and `resources: {}`
172
+ with private one-hour discovery cache hints. It includes the configured identity, icons, title,
173
+ and website in `io.modelcontextprotocol/serverInfo` result metadata. The Skills
174
+ extension and its skill-only resources share `src/skills.ts` with the meta-tool
175
+ and guest API. Both direct
176
+ call tools declare `address` as `x-mcp-header: "Address"`, which the SDK mirrors
177
+ and validates as `Mcp-Param-Address` on modern requests.
178
+
179
+ Connecta's own admission, deadline, pool, and access refusals use JSON-RPC error
180
+ bodies with `Cache-Control: no-store` and the HTTP statuses below. They omit the
181
+ unknown RPC id because the route refuses before decoding the RPC body. An aborted
182
+ request releases its resources without cancelling a newly created deadline
183
+ error body. Auth adapters continue to own their challenges and refusal bodies.
184
+ Admission and Connecta-owned refusal bodies share the MCP exchange's
185
+ request-scoped sent-credential set and pass through `redactAgentOutput` before
186
+ serialization. The SDK's `server/discover` response and registered meta-tool
187
+ results use that same set; connector contexts retain only an opaque scope.
188
+
189
+ | Refusal | HTTP status | Application code | Previous code |
190
+ | ----------------------------------- | ----------- | ---------------- | ------------- |
191
+ | Request capacity exhausted | 503 | `-33001` | `-31001` |
192
+ | Server shutting down | 503 | `-33002` | `-31002` |
193
+ | Request lifetime exceeded | 504 | `-33003` | none |
194
+ | Pool missing or refused | 404 | `-33004` | none |
195
+ | Origin or admitted access forbidden | 403 | `-33005` | none |
196
+
197
+ These allocations are outside JSON-RPC's reserved `-32768..-32000` range and
198
+ MCP's `-32020..-32099` range. The prior admission codes were also outside those
199
+ ranges; the migration changes the application allocation, not a reserved-code
200
+ violation. See the [2026-07-28 error-code policy](https://modelcontextprotocol.io/specification/2026-07-28/basic#error-codes).
201
+
202
+ `connecta doctor` uses SDK auto negotiation, checks the exported
203
+ `META_TOOL_NAMES` set, and reports the negotiated revision. It closes its client
204
+ on success or failure and refuses credential-bearing redirects during discovery,
205
+ legacy initialization, and tool calls.
206
+
102
207
  ## Layers below the meta-tools
103
208
 
104
209
  The meta-tool handlers are thin. The work sits in six modules the registry owns
105
210
  or hands out, and a change usually belongs in exactly one of them:
106
211
 
107
- | Module | Owns |
108
- | --- | --- |
109
- | `src/registry.ts` | The connector set, identity-scoped views, personal storage partitions, address resolution, catalog TTL/persistence/completeness, refresh single-flight, connector health, per-connector call limiters, and drift. Construction-time refusals live here. |
110
- | `src/catalog-service.ts` | Request-local listing, search, and describe. Caches catalogs inside one request, fans discovery probes out under deadlines, and opts agent reads into the runtime's deferred catalog channel when one exists. |
111
- | `src/invocation.ts` | One tool call: argument validation, call admission, one-attempt timeout, provider retry hints, result unwrapping, size capping, and the activity record. |
112
- | `src/catalog.ts` | Ranking, description summarizing, and the compact and TypeScript schema renderers discovery shows. |
113
- | `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
114
- | `src/exempt-writes.ts` | The writes a program may send, which config exempted from asking: the write budget's default, what a dispatched write's outcome says about whether it landed, and the tracking that lets one outlive the program without hiding an unknown outcome. Every other program write is refused before it reaches this. |
212
+ | Module | Owns |
213
+ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
214
+ | `src/registry.ts` | The connector set, identity-scoped views, personal storage partitions, address resolution, request-local complete-catalog reads, connector health, per-connector call limiters, and drift. Construction-time refusals live here. |
215
+ | `src/catalog-cache.ts` | SQL-backed SDK tools/resource/template listing cache, intake redaction, TTL bounds, host auth partitions, invalidation generations, and completed-refresh observations. |
216
+ | `src/catalog-service.ts` | Request-local listing, search, and describe. Caches catalogs inside one request and fans discovery probes out under deadlines. |
217
+ | `src/invocation.ts` | One tool call: argument validation, call admission, one-attempt timeout, provider retry hints, result unwrapping, size capping, and the activity record. |
218
+ | `src/catalog.ts` | Ranking, description summarizing, and the compact and TypeScript schema renderers discovery shows. |
219
+ | `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
220
+ | `src/program-writes.ts` | Trusted-pool write accounting: write budgets, dispatch draining, and known or unknown outcomes. |
115
221
 
116
222
  `src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same two
117
223
  services, `CatalogService` and `InvocationService`. That is the point: a
@@ -121,11 +227,18 @@ same credential resolution, and the same fail-closed read-only check.
121
227
  `test/execute.test.ts` asserts the parity directly, because a sandbox path that
122
228
  quietly diverges is how generated code would mint a capability.
123
229
 
230
+ Program result paging shares `meta-tools.ts`'s direct-call stash envelope and
231
+ reader. `program-result.ts` owns executor-independent byte limits;
232
+ `execute.ts` issues bounded transfer cursors, and `guest-runtime.ts` reconstructs
233
+ values through provider preludes. Cursors hold only an ID and offset for the
234
+ current run. Result bytes, expiry, authority and capacity charges stay in the
235
+ existing storage and ledger; no executor-specific store or storage key is added.
236
+
124
237
  ## Admission, in two places
125
238
 
126
239
  Request admission (`src/executor-admission.ts`, applied in `src/routes/mcp.ts`)
127
240
  bounds the MCP envelope: one deployment-wide FIFO pool, plus a deliberately
128
- smaller code pool a program takes a *second* permit from, so one request cannot
241
+ smaller code pool a program takes a _second_ permit from, so one request cannot
129
242
  trade ordinary capacity for unbounded sandboxes. `admission.code` is only a
130
243
  fallback — an executor implementing `acquire()` owns a bounded pool already, its
131
244
  settings win, and connecta warns the fallback was ignored. Invalid bounds throw
@@ -166,16 +279,16 @@ queue. Health checks and subsequent MCP requests confirmed the same isolate ID.
166
279
  The [recorded observations](https://github.com/zackbart/connecta/blob/a0ac904513fac735582b4bc9c817a48300134328/scripts/probes/worker-disconnect-2026-09-25.json)
167
280
  contain all twelve cases, including served flags and client-end timing.
168
281
 
169
- | Response and flag | Raw stream abort / cancel | Connecta source abort / cancel | Admission after response; next request |
170
- | --- | --- | --- | --- |
171
- | Heartbeats, no flag | neither | neither | active 1; 503 |
172
- | Idle with a pending timer, no flag | neither | neither | active 1; 503 |
173
- | Heartbeats, request signal enabled | abort only | both | active 0; 200 |
174
- | Idle with a pending timer, request signal enabled | abort only | both | active 0; 200 |
175
- | One chunk, then no timer or I/O, either configuration | neither | neither | active 1; 503 |
282
+ | Response and flag | Raw stream abort / cancel | Connecta source abort / cancel | Admission after response; next request |
283
+ | ----------------------------------------------------- | ------------------------- | ------------------------------ | -------------------------------------- |
284
+ | Heartbeats, no flag | neither | neither | active 1; 503 |
285
+ | Idle with a pending timer, no flag | neither | neither | active 1; 503 |
286
+ | Heartbeats, request signal enabled | abort only | both | active 0; 200 |
287
+ | Idle with a pending timer, request signal enabled | abort only | both | active 0; 200 |
288
+ | One chunk, then no timer or I/O, either configuration | neither | neither | active 1; 503 |
176
289
 
177
290
  The last row is a different failure. The client observed the response end
178
- *before* its planned disconnect, although the source never closed itself.
291
+ _before_ its planned disconnect, although the source never closed itself.
179
292
  The runtime ended the response without running JavaScript cleanup. This does
180
293
  not show a missing abort on a still-live request. The other cases remained open
181
294
  until the client aborted. Admission was sampled 1.5 seconds later, followed by
@@ -217,26 +330,193 @@ exposed by the deployment; these findings are tied to the date and flags above.
217
330
 
218
331
  ## Storage, credentials, and connectors
219
332
 
220
- `KVStorage` is `get`/`set`/`delete` with optional `list(prefix)`; core uses it for
221
- connector state, catalogs, and result paging — a 15-minute TTL with one
222
- runtime-wide accounting of stash bytes and entries, where a full stash returns the
223
- successful call's preview and a paging-unavailable notice rather than a result id.
224
- A second optional method, `compareAndSet(key, expected, next, options?)`, is an
225
- atomic claim: `null` means absent (expired counts) on the way in and delete on
226
- the way out. A successful write accepts the same optional `ttlSeconds` as
227
- `set`. Artifacts require it, because every write commits by swapping a head,
228
- and so does issuing access tokens; a read and a write standing in for the
229
- claim would let two writers both win. Downstream OAuth uses it where the store
230
- has it, so
231
- resealing legacy plaintext and discarding a refused grant cannot overwrite a
232
- consent that landed in between, and falls back to a read and a write where it
233
- does not.
234
- Adapters: `src/storage/memory.ts` and `src/storage/file.ts` (Node) both provide
235
- it, and the namespaced views core hands connectors forward it only when the
236
- underlying store has it. `examples/worker/` carries two more: Cloudflare KV,
237
- eventually consistent and so declaring none, and a D1 store that provides it —
238
- copyable reference source beside the D1 activity store, deliberately not an
239
- importable subpath. The shared cases live in `test/storage-contract.ts`.
333
+ A deployment has one store. On Workers it is one D1 database
334
+ (`d1Storage(env.CONNECTA_DB)` from `@zackbart/connecta/d1`); on Node it is one
335
+ SQLite file (`sqliteStorage(path)` from `@zackbart/connecta/sqlite`, over the
336
+ built-in `node:sqlite`). Both are the same SQL key-value store
337
+ (`src/storage/sql.ts`) over a three-method driver, so every statement runs
338
+ unchanged on each, and the same module carries the activity table beside it.
339
+ Each store creates its tables on first use. `memoryStorage()` is the default
340
+ and the test double. Workers KV and the 0.28 JSON file store are gone: KV is
341
+ eventually consistent and cannot compare-and-set, and the file store rewrote
342
+ its whole file on every write. Each has a one-shot copy into its replacement,
343
+ `copyKvToD1` on `/d1` and `connecta migrate-state` on Node. Both copy live
344
+ entries verbatim, because physical keys did not change, and both keep any key
345
+ the target already holds. `copyKvToD1` reports counts per key family
346
+ (`familyOfKey` in `src/storage/keys.ts`), never a key. It resumes through a
347
+ source-bound, atomically claimed token, because Workers KV's own list cursor
348
+ can spell a key. Copy under maintenance after KV stabilizes, verify hashes
349
+ and expiries, and mark cutover before reopening traffic; the permanent D1
350
+ marker refuses stale copies afterward.
351
+
352
+ `KVStorage` is `get`/`set`/`delete`/`list(prefix)`/`compareAndSet`, all
353
+ required, and `createConnecta` refuses storage missing one (INV-11).
354
+ Adapters must also declare `capabilities: { absoluteExpiry: true }`.
355
+ Construction rejects missing or false capability declarations with an error
356
+ naming `capabilities.absoluteExpiry`. The memory, SQLite, and D1 adapters carry
357
+ this marker. Custom adapters may declare it only after implementing absolute
358
+ expiry on both writes; a legacy `ttlSeconds`-only adapter is rejected by the
359
+ published types and at runtime.
360
+ `compareAndSet(key, expected, next, options?)` is an atomic claim: `null` means
361
+ absent (expired counts) on the way in and delete on the way out, and a
362
+ successful write takes the same expiry options as `set`. Relative `ttlSeconds`
363
+ is created and checked by the database clock inside each statement, so isolate
364
+ clock skew cannot expire a live holder's record. Absolute `expiresAtMs` is a
365
+ safe-integer epoch-millisecond deadline stored verbatim and checked against the
366
+ storage clock. Supply at most one option. Custom adapters must implement both;
367
+ a delayed absolute-expiry write must not restart a relative TTL at commit time. Each SQL claim
368
+ is one statement, which SQLite executes atomically and D1 serializes on its
369
+ primary. Everything that must claim a key exactly once relies on it with no
370
+ read-then-write fallback: access-token issuance and
371
+ capacity, OAuth handoff ownership, single-use connect links, every downstream
372
+ OAuth grant write and consent claim, and the result stash.
373
+
374
+ Every key is built in `src/storage/keys.ts`, which lists each family with its
375
+ scope, version, codec, and TTL policy; `test/storage-keys.node.test.ts` fails when
376
+ two families overlap, another source file spells a prefix (constant concatenation
377
+ folded), or a storage call passes a key holding literal text. Core hands
378
+ subsystems namespaced views: `conn:<id>:` per connector, `principal:<key>:` per
379
+ personal registry, `results:` and `subject:<key>:` for result paging.
380
+
381
+ Keys and list prefixes must not contain U+0000 (NUL). D1, SQLite, and memory
382
+ storage reject them with `TypeError` before accessing storage: Node 22's
383
+ `node:sqlite` truncates TEXT results at NUL. Builders reject NUL in unencoded
384
+ components.
385
+ State-file import validates all keys before writing. The `connecta_kv` table
386
+ keeps its existing TEXT keys, including compatibility with the 0.28 schema.
387
+ Storage writes no log lines. A refused import names the file and an entry's
388
+ position, never a key, a value, or the JSON parser's account, which quotes
389
+ the file (INV-6). Database failures report only a fixed import step and an
390
+ entry's position, including errors raised by target-database triggers.
391
+
392
+ Remote MCP catalogs use the SDK `ResponseCacheStore` over this same SQL KV.
393
+ The `response-cache:v1:` family binds connector id, a hash of its configured
394
+ endpoint/auth/protocol settings and TTL policy, an invalidation generation,
395
+ and a hash of the listing method and host-computed auth partition. Later pages
396
+ can only narrow the aggregate TTL and sharing within that partition. Request
397
+ tokens and personal OAuth or credential auth always use private entries,
398
+ including when the downstream declares `public`. Private partitions bind the
399
+ admitted principal, subject, identity, pool, endpoint, credential digest, and
400
+ OAuth epoch. Operator-shared credentials and unauthenticated connectors may
401
+ share public-hinted listings across principals within the same admitted pool
402
+ and endpoint. Private hints retain principal isolation even with shared auth.
403
+ The adapter refuses the SDK's empty shared slot for private auth modes. OAuth
404
+ listings resolve the current credential digest even within one epoch; a
405
+ credential change during a walk prevents publication under the old digest.
406
+ Each remote connector instance owns its request's cache lifetime separately,
407
+ so closing a temporary client does not end another instance with the same id.
408
+ Only complete, intake-redacted tools/list, resources/list and
409
+ resources/templates/list successes are stored. Resource payloads are not cached. SDK default
410
+ in-memory caching is replaced, so raw catalogs never enter it.
411
+
412
+ `discovery.catalogTtlSeconds` (300 by default) is the fallback for legacy
413
+ listings without `ttlMs`. Positive hints are bounded by
414
+ `catalogMinTtlSeconds` (0) and `catalogMaxTtlSeconds` (86400, also the SDK
415
+ ceiling); an explicit zero is never reused. Both bounds are nonnegative finite
416
+ seconds (fractions round down to milliseconds), with min <= max <= 86400. `persistCatalog` and `staleCatalogSeconds`
417
+ have been removed. SQL storage is always used for remote catalogs; dynamic
418
+ custom connectors are listed anew in each request; authenticated callers may
419
+ retain a complete private fallback, and static catalogs remain
420
+ in configuration. After a transient first-page tool listing failure, ordinary calls
421
+ may use a complete catalog for up to five minutes after its expiry in the same
422
+ auth partition. Partial walks, permission and auth failures do not use this
423
+ fallback. Resources require unexpired inventories. There is no deferred refresh.
424
+ Host-only provenance carries each catalog's original fetch time, TTL and expiry,
425
+ and whether it was a fallback, through classification and request-local copies.
426
+ Zero TTL and stale fallbacks cannot authorize post-entry auth recovery.
427
+
428
+ A manifest is published by CAS only after every UTF-8 chunk is written. It
429
+ records the original fetch time, absolute expiry, revision, fingerprint, and
430
+ monotonic stamp. Readers verify all chunks and fingerprint before accepting it.
431
+ Freshness expires at the bounded TTL; tool facts remain for the five-minute
432
+ fallback window, and generations expire after 48 hours; invalidation
433
+ rotates the generation, and old chunks expire without becoming reachable
434
+ again. Remote clients pin that generation before listing, including SDK cache
435
+ refreshes. Each new listing pins the current generation, so a live client can
436
+ publish a new catalog after invalidation without letting an older listing
437
+ adopt its generation. A public transport observer starts a generation rotation
438
+ before the SDK processes tool or resource list-changed notifications; its lifetime is
439
+ the connection's, including after a successful resource read or listing ends.
440
+ Opposite-scope cleanup only deletes its entry. SDK listings serialize per client
441
+ because the cache store has no per-call options. Their operation binding lasts
442
+ through the SDK's cleanup, and each cache method captures that binding before
443
+ awaiting. Cache I/O checks cancellation and scope teardown at every await
444
+ boundary. Cancelled storage waits release the listing without starting more
445
+ I/O when an already-dispatched storage operation settles. Teardown waits a
446
+ bounded time for generation rotations already started. Catalog age remains the
447
+ original fetch age across process restarts and cache hits.
448
+ `test/catalog-cache.test.ts` runs against real SQLite and D1.
449
+
450
+ `observeCompletedCatalogRefresh` in `src/catalog-cache.ts` is the accepted
451
+ refresh publication hook. The SDK tools/list wrapper calls it once after a
452
+ complete, unfenced wire refresh, including zero-TTL results. Custom dynamic
453
+ connectors publish their complete intake through the same hook. Cache hits,
454
+ rejected or abandoned walks, and identical catalogs emit no change event.
455
+ `Registry.recordCatalogDrift` compares the previous and next tool fingerprints
456
+ from this hook, preserving the requesting actor, request id, pool and deferred
457
+ activity write. The first complete catalog is a baseline.
458
+
459
+ Hash-only baselines publish bounded chunks before a manifest. They store the
460
+ catalog digest and hashes of each tool name and fact, so added, removed and changed counts require no persisted tool names or
461
+ schemas. They use the exact host auth/config cache partition, survive catalog
462
+ expiry and invalidation, and expire after 48 hours. There is no connection-wide
463
+ fallback baseline across partitions. Private catalog events use the admitted
464
+ principal actor basis even for shared-auth connectors; activity disclosure
465
+ requires that same owner and the recorded pool's grants. Ownerless private
466
+ observations cannot be disclosed. Public-hinted shared-auth catalogs retain
467
+ shared history within their admitted pool and endpoint.
468
+
469
+ Result paging stores each oversized result for 15 minutes, chunked so a page
470
+ reads only what it covers. Its bounds (`results.maxStashBytes`,
471
+ `results.maxStashEntries`) are the deployment's, not an isolate's: every charge
472
+ is a row in one ledger record, booked by compare-and-set before any chunk is
473
+ written, so isolates and processes sharing the store see one count. Every
474
+ reservation starts with a finite deadline of booking time plus the 30-second
475
+ write budget plus the chunk TTL, normally 15 minutes. Pending writes retain
476
+ that charge until its deadline. Booking retries recognize their own reservation
477
+ ID and reuse its stored deadline, including a final read after an ambiguous
478
+ last CAS. A lost swap or transient storage error backs off and re-reads, up to
479
+ 32 attempts. A full ledger or unconfirmed exhausted booking returns no result id.
480
+
481
+ Caller time only nominates charges for pruning. Before discarding one, the
482
+ ledger writes a probe with that charge's absolute deadline and reads it back.
483
+ Only an absent probe proves storage has passed the deadline; a live probe keeps
484
+ the charge, and a storage error cannot free capacity. Each deadline has its own
485
+ key in `stashExpiryKeys`, so concurrent probes cannot overwrite a different
486
+ deadline's proof. Probes expire at the deadline they check and need no cleanup.
487
+ This uses the same absolute-expiry contract on memory, SQLite and D1 without a
488
+ storage API or schema change. An ahead caller clock cannot admit a second stash
489
+ while the first remains readable.
490
+
491
+ The whole chunk-write phase has a 30-second timeout measured from booking, with
492
+ a clock check before and after each write. Trailing chunks precede the header;
493
+ failure or timeout returns no result id and stops the write loop. Chunks use an
494
+ absolute expiry at booking time plus their TTL, so slow writes shorten the
495
+ remaining paging window. Storage promises cannot guarantee cancellation of an
496
+ already-dispatched database operation, and a rejected client response does not
497
+ prove that operation cannot commit later. Even a commit after timeout or process
498
+ interruption keeps that absolute expiry, rather than starting another TTL.
499
+
500
+ Settlement and release only shorten or remove a reservation. Successful writes
501
+ attempt to shorten the charge to the fixed chunk expiry. Cleanup releases it
502
+ only after all deletions succeed and every dispatched write returned confirmed
503
+ success. Failed cleanup or exhausted release attempts finite settlement when
504
+ no write is unconfirmed. A rejected or timed-out write keeps its original
505
+ reservation because it may still commit after deletion. Each completion path
506
+ has one 15-second budget covering deletion, release, fallback settlement, and
507
+ their storage waits and retries. Successful-write settlement has the same
508
+ 15-second budget. A stalled operation stops being awaited when the budget
509
+ expires; its late response starts no further I/O, and no background cleanup
510
+ fiber is started. The caller returns its write outcome even if these
511
+ optimizations cannot finish. Ledger updates retry within the same 32-attempt
512
+ budget. If they fail, time out, crash, or exhaust,
513
+ capacity may be over-held until the booked deadline, at most 15 minutes and
514
+ 30 seconds for the normal TTL. The ledger row itself has an absolute expiry at
515
+ its latest live reservation deadline. There are no completion receipts or
516
+ separate recovery rows; expiry probes carry no completion state. Expired data reads as absent and normal storage sweeps
517
+ reclaim the physical rows. A full stash returns the successful call's preview
518
+ and a paging-unavailable notice rather than a result id. Shared failure cases
519
+ live in `test/stash-charge-contract.ts`, exercised by the memory and SQL suites.
240
520
 
241
521
  `src/credentials.ts` is the AES-GCM vault behind the root-exported
242
522
  `CredentialVault` contract, selected through the `vault` slot. It binds connector
@@ -255,23 +535,259 @@ produce instances that take the same catalog, read-only, credential, storage,
255
535
  invocation, result-size, and activity paths. Every one is deployment
256
536
  configuration, never runtime registration. `authScope: "shared" | "personal"`
257
537
  partitions connecta-owned context — state, credentials, OAuth, catalogs, observed
258
- shapes — by principal, and *only* connecta-owned context: a secret a custom
538
+ shapes — by principal, and _only_ connecta-owned context: a secret a custom
259
539
  handler closes over is shared JavaScript state, and `remoteMcp()` refuses the
260
540
  literal-headers-plus-personal version of that mistake. Visibility
261
541
  (`identity.connectorAccess`) is a separate rule; hiding a connector does not
262
542
  change who owns its auth.
263
543
 
544
+ ### Errors and records
545
+
546
+ A failure has two audiences with two rules. The agent that made a call may read
547
+ the downstream's own answer to it: a JSON-RPC error message, an HTTP 4xx
548
+ refusal, `isError` content, or the words a handler put in a
549
+ `ConnectorCallError`. Each upstream request owns a memory-only sent-secrets
550
+ set. Every connector context links its local set into that request, including
551
+ listing, discovery, registry refresh, OAuth refresh, and provider handlers.
552
+ Direct meta-tool invocations and program runs get fresh request identities;
553
+ all operations within one HTTP request share its identity. Credential-slot reads, static
554
+ auth headers, raw confidential OAuth client IDs and secrets, and outbound
555
+ bearer tokens join the set, including tokens rotated during a call. Token and
556
+ revocation requests register client credentials before dispatch, decoding OAuth
557
+ Basic form components so a raw echo is covered too. Public client IDs remain
558
+ visible. Before any diagnostic is truncated or returned, the
559
+ agent boundary replaces these values and their auth prefixes, mixed JSON
560
+ escapes, URL encodings, and base64/base64url forms with `[redacted]`. Final
561
+ transports register sensitive headers and query values after assembling the
562
+ request; custom API handlers use `ctx.fetch`. Values shorter than eight
563
+ characters do not enter the matcher, including Basic usernames, passwords
564
+ and explicitly secret fields such as `client_secret`. Configured short
565
+ credentials produce one payload-free warning per connector; use secrets of
566
+ at least eight characters. Encoded and JSON-escaped echoes of registered
567
+ credentials follow the same redaction rules. Connecta's own messages
568
+ never quote a credential.
569
+ Core uncertainty and retry envelopes share one schema-filtered argument snapshot,
570
+ captured before dispatch and byte budgeting. `writeOnly: true` omits values of
571
+ every length, including nested properties, array items and prefixes, local
572
+ `$ref`/`$defs`, and `allOf`. `oneOf`/`anyOf` must agree on sensitivity. Unresolved
573
+ references, sensitive dynamic-property schemas or other unsupported applicators,
574
+ and traversal limits withhold the entire echo. A private array element withholds
575
+ its containing array to preserve indices. `argsRedacted: true` marks a partial
576
+ or withheld echo as reconciliation context; retries require the original arguments.
577
+ OAuth discovery and consent URLs are refused as a typed, non-retryable failure
578
+ when they contain a known credential, before persistence or reuse. This guard
579
+ checks the request's sent credentials and the current grant's credentials,
580
+ including confidential client secrets. The consent URL's expected `client_id`
581
+ parameter remains public, while echoes elsewhere are refused. URLs are never
582
+ rewritten. OAuth failure
583
+ origins are recorded only for the configured MCP or issuer origin, or the
584
+ origin of discovery metadata whose issuer matches its authorization server.
585
+ An origin matching a sent credential is omitted.
586
+ The matcher is cached until its set changes; the empty set has a fast path.
587
+ `redactAgentOutput` is the agent-facing choke point. Every meta-tool exit,
588
+ including errors, and every host-to-guest value or rejection passes through
589
+ it. Both MCP transports also pass their serialized response through it, covering
590
+ JSON-RPC errors, HTTP diagnostics, structured content, and paging responses.
591
+ Per-call redaction remains before diagnostic truncation, paging, and emits. Paging stores only request-redacted text before encoding
592
+ chunks, so a later request needs no original credentials. Programs retain the
593
+ request set only for the run so later outputs cannot reconstruct an echo.
594
+ Discovery registers sent credentials under the same rules, including
595
+ `server/discover`, legacy initialization, and every `tools/list` page on a
596
+ reused transport. Remote MCP sanitizes every list page before SDK aggregation, automatic cache
597
+ writes, derived indexes, or request-local definitions. Invalid `x-mcp-header`
598
+ declarations refuse the entire listing before the SDK can log downstream names
599
+ or silently exclude a tool. Registry intake also sanitizes custom, API and
600
+ provider listings before observation or request-local storage. Failed listings
601
+ are redacted in their own request and are never cached. Successful catalog caches and result
602
+ stashes retain only intake-redacted values; there is no tool-response cache.
603
+ Every nested string and object key passes through the redactor,
604
+ including titles, descriptions, schemas and annotations. If a tool name would
605
+ change, the complete catalog is refused. Rewriting a name changes dispatch;
606
+ dropping only that tool would publish a partial catalog, against INV-8. Later
607
+ cached reads need no credential set because the stored facts are already clean.
608
+ Echoed sensitive header lines are also withheld. The set is never persisted
609
+ or logged. Anything else (a transport, parser, stream, validator, or
610
+ runtime error) reaches it in connecta's words: step, origin, HTTP status, and
611
+ class, classified as the original would have been ([auth](./auth.md#what-a-servers-errors-may-say)).
612
+ Operators read logs, status messages, and activity, and none of them carries
613
+ any error's text, allowed or not (INV-6). `src/operator-record.ts` builds every
614
+ failure record from an explicit list of fields, each checked against a constant
615
+ table or grammar (connector, catalog-listed tool, step, origin, HTTP status,
616
+ class, code, retryability, errno, counts), and `logFailure` writes a fixed
617
+ `<rejected>` for a record it did not build, never throwing. Provenance is by
618
+ identity, never by shape: an error's class is read from its prototype chain,
619
+ never its `name`; a classification only from one connecta registered; a tool
620
+ only from the catalog entry a call resolved to, and only if it fits MCP's
621
+ tool-name grammar (else `<withheld>`, in activity rows too); and a status
622
+ message only from the snapshot connecta took of a status it created, so a
623
+ plugin `status()` or a decorator contributes its state alone. workerd quotes a
624
+ body's Content-Type in its own output when a text read meets a type it does not
625
+ parse as text, so connecta reads downstream bodies as bytes
626
+ (`src/byte-read-response.ts`), `ctx.oauth.fetch()` answers included. The
627
+ operator page may show catalog metadata the operator loaded (descriptions,
628
+ schemas), but withholds a tool name outside the grammar as it does in records.
629
+ `test/operator-record-sources.node.test.ts` is a secondary lint over every other
630
+ log call in `src/`. Fix the sink, not the source: a filter at each source missed
631
+ the next one.
632
+
633
+ ## Configuration
634
+
635
+ `ConnectaConfig` is one schema (`src/config.ts`, combinators in
636
+ `src/config-schema.ts`). The same value produces the TypeScript type a
637
+ deployment writes, the unknown-key walk createConnecta runs before reading any
638
+ value, every default (`src/config-defaults.ts`, which the enforcing modules
639
+ read too), and one validation policy: a present value that is wrong throws
640
+ with its path at construction (INV-11). Nothing warns and falls back. The walk
641
+ reads property descriptors, never properties, and builds the plain copy that
642
+ is resolved in place of the caller's object, so no getter or Proxy trap runs;
643
+ an accessor on a config object or array is refused by path, as is an object
644
+ that cannot be inspected. A closed object or record slot takes a plain object
645
+ only: an array or a class instance there is refused by path, unread. String
646
+ maps (static `headers`, `api()`'s `authorizationParams` and
647
+ `tokenRequestHeaders`) are copied the same way and each value must be a
648
+ string. A discriminated union (`remoteMcp()`'s `auth.type`,
649
+ a provider's `surface`) with a missing or unrecognized discriminant throws
650
+ with the valid values. Only plain data is copied. Connectors, modules,
651
+ executors, storage, loggers, handlers, and schemas are opaque and never
652
+ entered. An `api()` tool carries its handler, so it is checked in place (own
653
+ keys declared, no accessor at a declared key, inherited ones included) and
654
+ passed through as given: a class-instance tool keeps its prototype `handler()`
655
+ and its private fields. Configuration is operator-authored and trusted, as in
656
+ #698; the walk refuses mistakes by path and never echoes a value, and is not
657
+ a sandbox for hostile objects. Records keyed by deployment
658
+ names (pools, `classification`) have no prototype, so `__proto__` is a name.
659
+ Built-in factories (`api()`, `remoteMcp()`, every provider, and each module
660
+ factory) walk their own options the same way, against shapes the compiler
661
+ checks against their option types (`optionsOf<T>()`). Provider definitions
662
+ declare their closed `options` shape; `defineProvider()` validates it by the
663
+ same descriptor walk before `create` reads a value and stamps the definition
664
+ name onto `describe().source.provider`. `classify` is accepted by the remote
665
+ shape and validated by the shared reviewed-classification validator. Custom
666
+ connectors remain opaque, including their `classification` field, which the
667
+ registry validates. Checks
668
+ that need the connector set — pool members, exact classification overrides, a
669
+ connector's own `maxResultBytes` — throw from `src/index.ts` and the registry
670
+ under the same policy. `storage` stays opaque, but its check requires `list`
671
+ and `compareAndSet` as well as `get`, `set`, and `delete`, so a leftover
672
+ Workers KV adapter is refused at boot with the replacements named, not at the
673
+ first OAuth callback.
674
+
675
+ `resolveConfig` returns a frozen `ResolvedConfig`: defaults applied, auth
676
+ ordered, `serverInfo` named and versioned.
677
+ Routes and meta-tools read limits from it (`ServerOptions.config`) instead of
678
+ from fields copied out one at a time. `Connecta.describeConfig()` is a
679
+ secret-free snapshot of it, built once (`src/describe-config.ts`): an allowlist
680
+ serializer that copies named fields and never spreads a config object, plus
681
+ `Connector.describe()` on `remoteMcp()`, `api()`, and the providers. Header values, keys, client secrets, credentials, URL
682
+ userinfo, queries, and fragments, and function bodies never appear; every URL
683
+ a description emits, from `describeConfig()` or a direct `describe()`, passes
684
+ through the sanitizers in `src/described.ts`, which keep http(s) URLs only
685
+ (`blob:` wraps a URL with its userinfo in the path);
686
+ `test/describe-config.test.ts` plants one in each position. Both deployment
687
+ shapes write their configuration as `defineConfig((env) => …)` in
688
+ `src/connecta.config.ts`, with optional modules as type-checked expressions
689
+ switched by the environment, and keep their entries under 30 lines.
690
+
691
+ ### Providers and reviewed classification
692
+
693
+ Infisical separates explicit value reads from identifier results. `get_secret`
694
+ and `list_secrets` with `includeValues: true` return secret values and comments.
695
+ All other results, including imported secrets and pending approvals, allow only
696
+ checked IDs, keys, environment and tag slugs, paths, timestamps, positive versions
697
+ and enum fields. They omit project slugs derived from free-text names, comments,
698
+ descriptions, free-form names, reminder notes and arbitrary nested objects.
699
+ `metadataOmitted: true` reports withheld or unavailable metadata.
700
+ This assumes an honest server and protects against careless human entry in free
701
+ text. A malicious server encoding values into valid identifiers is out of scope.
702
+ Known, accepted residual risk: environment slugs are returned as required
703
+ routing identifiers. Infisical's UI derives them from environment names by
704
+ default, so secrets entered in those names can appear in returned slugs.
705
+ Operators must not put secrets in environment names or slugs. Secrets placed in
706
+ identifiers or key names are out of scope.
707
+ Write input schemas mark secret values, secret comments and folder descriptions
708
+ as `writeOnly`, so recovery argument echoes omit them without changing dispatch.
709
+ Submitted values never join the request's sent-credential set, preserving later
710
+ program arguments. Client credentials and tokens still join that set on every
711
+ call. The [provider guide](https://github.com/zackbart/connecta/blob/main/src/providers/infisical/SKILL.md) owns routing and
712
+ value-read conventions.
713
+
714
+ A maintained provider is one `defineProvider()` call (`src/provider.ts`): a
715
+ name, title, kind (`"mcp"`, `"api"`, or `"composed"`), a maintained skill, an
716
+ optional reviewed classification, a closed `options` shape declared with
717
+ `optionsOf<O>()`, and a synchronous `create`. The root exports the option-shape
718
+ combinators and `PROVIDER_COMMON` so provider authors use the same validation
719
+ path. The factory refuses undeclared keys and accessors before reading any
720
+ option, copies plain data, preserves behaviour objects, then validates common
721
+ values (purpose, title, instructions, `authScope`) before `create` runs, and renders the guide: heading, the
722
+ connection context `create` supplies, the maintained text, then deployment
723
+ instructions, which append and never replace. `src/provider.ts` imports neither
724
+ transport, so an `api()` provider gains no MCP client or Effect graph from it
725
+ (`test/purity.node.test.ts`). The factory carries its `definition`, which build and
726
+ check tools read instead of keeping provider lists. All twenty providers
727
+ live in their own folders. The eight hosted implementations, including the
728
+ MCP branches of Notion, Vercel, and Cloudflare, use reviewed presets over
729
+ `remoteMcp({ classify })`. The eleven API-only factories retain an internal
730
+ adapter to the same definition and description-stamping path. GitHub composes hosted tools with a scope-enforced REST complement.
731
+ Notion, Vercel and Cloudflare default to hosted MCP and select explicit REST
732
+ complements with `surface: "api"`. Configure both under distinct connector ids
733
+ with independent credentials when both are needed. Notion preserves its
734
+ internal-integration identity under `integration_*` names; Vercel and Cloudflare
735
+ retain documented REST gaps rather than duplicate hosted operations. See the
736
+ [provider migration guide](./provider-migration-0.29.md).
737
+
738
+ `remoteMcp({ classify })` is the public way to declare what a downstream's
739
+ tools do: `{ tools: { name: "read" | "write" | "destructive" | { verdict,
740
+ reason?, schemaDigest? } }, unlisted?: "hide" }`, validated at construction. It fails closed
741
+ (INV-1): a reviewed read fills silence but yields to an explicit write
742
+ annotation, a reviewed write stays a write whatever the downstream claims, an
743
+ unlisted tool is a read only when it says so, and a reviewed tool whose
744
+ `schemaDigest` no longer matches, or cannot be checked, is a write on discovery
745
+ and every invocation path until a release reviews it again. A digest covers
746
+ the whole schema; one too large to hash whole is unchecked. `unlisted: "hide"`
747
+ removes every unreviewed name, even an explicitly annotated read, on every
748
+ registry read, including persisted catalogs. `remoteMcp` also refuses direct
749
+ calls to unlisted names before authentication. Tool classification does not
750
+ authorize targets; providers such as GitHub check arguments and configured
751
+ scopes separately before minting credentials.
752
+
753
+ Connectors report facts; the registry is the only classifier. A reviewed
754
+ connector carries its review as data, the deep-frozen
755
+ `Connector.classification`, and its `listTools` returns the downstream's
756
+ listing unclassified. The registry validates the field when it first reads a
757
+ connector (INV-11) and classifies the intake-redacted facts on every read into
758
+ fresh objects. Remote MCP caches the underlying SDK response before connector
759
+ decorators; decorators run again on every read. Neither a cache layer nor a
760
+ decorator holding a served or listed tool can carry a verdict. The request-scoped catalog also owns a deep copy
761
+ and returns fresh copies to discovery and invocation. Each connector call
762
+ receives a separate deep copy of its definition, so mutations cannot change
763
+ classification, schemas, or write accounting later in the same program.
764
+ A restart applies the current review to cached SDK facts. Retired registry
765
+ manifest versions 2 and 3 are ignored and expire on their original TTL.
766
+ During successful listings the registry counts drift against the same record, and `scripts/drift-check.mjs` compares
767
+ its names with published inventories. The internal `withVettedCatalog()`
768
+ helper has been removed. Public provider `*_VETTED_CATALOG` exports remain
769
+ deprecated aliases derived from each definition
770
+ (`test/classified-decorators.test.ts`, `test/hosted-presets.test.ts`).
771
+
772
+ Wrappers must forward `classification` to keep the review. `{ ...connector }`,
773
+ `Object.assign`, and `Object.create` keep it. A forwarding class that omits it
774
+ serves an unreviewed connector: the downstream's annotations are its own
775
+ claims and fail closed when absent, and nothing it persists carries safety.
776
+ Phase 2's deployment-level classifier overrides, keyed by connector id and
777
+ tool ([#706](https://github.com/zackbart/connecta/issues/706)), apply
778
+ regardless of wrapping.
779
+
264
780
  ## Optional deployment modules
265
781
 
266
- `createConnecta` takes closed typed `ui`, `vault`, `activity`, and `artifacts`
267
- slots, with factories at `/ui`, `/credentials`, `/activity`, and `/artifacts`
268
- and bearer auth at `/auth/bearer`. Root exports the contracts, never the implementations, and there
782
+ `createConnecta` takes closed typed `ui`, `vault`, and `activity`
783
+ slots, with factories at `/ui`, `/credentials`, and `/activity`
784
+ and machine tokens at `/auth/access-tokens`. Root exports the contracts, never the implementations, and there
269
785
  is no module array, runtime registration, or plugin lifecycle. Core keeps
270
786
  discovery, the executor contract, invocation, permissions, and OAuth callback
271
787
  verification; an omitted module contributes no runtime work at all.
272
788
 
273
789
  The operator UI — `src/ui.ts` (data-free shell and `/ui/data` payload),
274
- `src/routes/ui.ts`, `src/operator-ui/` (the Preact app and its pure rules) —
790
+ `src/routes/ui.ts`, `src/operator-ui/` (the React app, owned Radix primitives and its pure rules) —
275
791
  shows a human what a deployment exposes and manages only the authentication
276
792
  material code explicitly permitted; it never edits the connector set, catalog,
277
793
  annotations, scopes, or permission rules. Two invariants shape it: a status read
@@ -280,16 +796,16 @@ and each lazy details request owns a bounded downstream scope, so one failing
280
796
  provider leaves the other connections usable. A connector's status message
281
797
  never reaches the page: it can quote a downstream error body, and that body can
282
798
  quote the secret it rejected, so the details payload carries only a classified
283
- `problem` — which picks fixed on-screen copy and a fixed fix prompt — and the raw
284
- text goes to the server log. The OAuth and credential Test notices keep the
285
- same rule: those routes answer a downstream's failure in fixed words (a Test
286
- answers only `{ ok }`), log its text, and the page picks a sentence by outcome
287
- without ever rendering what the server sent. Credential handoff URLs exist only while the UI is
799
+ `problem`, which picks fixed on-screen copy and a fixed fix prompt. Server
800
+ logs contain only checked failure records, with no raw text. OAuth and
801
+ credential Test notices keep the same rule: those routes answer failures in
802
+ fixed words (a Test answers only `{ ok }`), log checked facts, and let the page
803
+ pick a sentence by outcome. Credential handoff URLs exist only while the UI is
288
804
  mounted; OAuth callbacks never need it.
289
805
 
290
806
  Its appearance is one token layer, and it is not the UI's alone: every page
291
807
  connecta shows a person — the operator shell, the OAuth callback, a browser's
292
- 404, the artifact frame and Markdown pages — reads it. `src/operator-ui/tokens.css`
808
+ 404 — reads it. `src/operator-ui/tokens.css`
293
809
  resolves every color, radius, and font through a custom property and mixes the
294
810
  rest from those with `color-mix`; `page.css` holds the base typography and the
295
811
  primitives (shell, masthead, buttons, badges, messages, the one-message status
@@ -303,111 +819,49 @@ block after the stylesheet. The five tokens it accepts are gated in
303
819
  `src/branding.ts`, each by a narrow syntactic check: deployment config reaches a
304
820
  `<style>` element here, and an unvalidated value would be CSS injection. The
305
821
  dark palette is the same tokens under `prefers-color-scheme`; `colorScheme` pins
306
- one with a `data-scheme` attribute on the page, and the artifact viewer passes
307
- the same attribute into a Markdown page, never into authored HTML.
308
-
309
- The artifacts module — `artifacts()` from `/artifacts`, implemented in
310
- `src/artifacts/` — is the one module that contributes a connector. Core
311
- appends its prebuilt `artifacts` connector to the configured set, so its reads
312
- and writes take the same catalog, invocation, admission, and activity paths as
313
- any other connector; the slot binds its optional refresh runner once, after the
314
- registry and executor exist, and a deployment
315
- without it has no connector, no guide, and no artifact storage traffic. Its
316
- writes are not read-only anywhere — `call_tool` refuses them and discovery
317
- lists them approval-required — but they skip approval inside programs through
318
- the connector's own `approval: "never"`, which `execute.approval` switches off,
319
- because every write is a new immutable version anyone can roll back. The
320
- module needs `publicUrl`: the links it hands out are shared, so they must never
321
- come from a request's `Host`.
322
-
323
- An artifact can version an `execute_code` program and target data document with
324
- `set_refresh`, on a manual, daily, or weekly schedule. The deployment calls
325
- `artifacts.runDue()` from a Node timer or Worker `scheduled()` handler; core
326
- starts no background jobs. One tick scans at most 1,000 heads and starts at
327
- most 10 due runs. Every run uses the configured executor, host-call budget,
328
- and watchdog, with a registry view containing only shared connectors. The
329
- owner admitted to `set_refresh` is stored with the program. Every run rechecks
330
- that identity's current `connectorAccess` and pool grant, including continued
331
- access to `artifacts.set_refresh`, then intersects those grants with shared
332
- connectors. Revoking either grant stops later runs.
333
- Approval exemptions are off, so a refresh writes nothing. A refused call fails the
334
- entire refresh even if its program catches the error. The head CAS claims one
335
- run per artifact; the data commit checks the claim ID and program version in
336
- the same CAS. The claim deadline starts before executor admission and aborts
337
- a queued or active play when it expires. An expired old run cannot publish over
338
- a newer claim. Run history retains 50 records with bounded logs. A failure leaves the
339
- last good data intact and marks the artifact stale. Library and viewer show
340
- only outcome enums and times, never downstream messages or logs.
341
-
342
- With the operator UI mounted, `/artifacts` lists the team pages and
343
- `/artifacts/<id>` opens one. The shell contains no page data. It reads the
344
- artifact through an authenticated JSON route with the operator UI's inbound
345
- identity and connector visibility, then passes the document into a sandboxed
346
- frame. Snapshot links pin the view and document versions. The frame's response
347
- CSP gives scripts an opaque origin, refuses fetch, XHR, images and other
348
- unlisted requests. External script, style, and font origins default to none;
349
- a deployment opts in to each one it trusts with page data. The shell tightens
350
- its own `frame-src` to `'none'` after the fixed bootstrap loads and before it
351
- hands over the page,
352
- so a page script cannot navigate even its own frame to send data in a URL.
353
- This browser boundary is covered by Chromium tests, including a
354
- `document.open/write` rewrite.
355
-
356
- `artifactOrigin` optionally names a distinct HTTPS origin for these pages.
357
- The main host redirects every `/artifacts` path to it; the artifact host
358
- answers every other path, including `/mcp` and operator APIs, with 404. The
359
- artifact shell still uses the same inbound auth providers and checks the
360
- identity on each data read. The bare shell and fixed frame bootstrap carry no
361
- artifact data, so a visitor can reach sign-in before an authenticated read.
362
- Bearer tokens in browser localStorage belong to one origin: signing in on the
363
- main host does not sign in on the artifact host. A redirected visitor enters
364
- the token again there; the redirect carries no credential. Cookie or session
365
- auth likewise depends on the provider admitting that separate origin.
366
-
367
- Storage is an `ArtifactStore`, and `kvArtifactStore` is the reference: over any
368
- `KVStorage` with `compareAndSet` and `list`, one head record per artifact is
369
- the only key ever compared-and-set, earlier versions sit beside it immutable,
370
- and bodies are content-addressed — in the key-value store, or in R2 through the
371
- Worker example's `r2-artifact-blobs.ts`. Workers KV is refused at construction,
372
- because a write that cannot swap its head can lose a teammate's edit. Every
373
- rule lives once, in `src/artifacts/operations.ts`: a write checks its base
374
- against the one stream it touches, so of two writes from the same base exactly
375
- one wins and the other is a `conflict` carrying a bounded `current` map, while
376
- a head that moved for an unrelated reason is re-read and retried rather than
377
- reported. Patches match exactly once or change nothing, rollback is a new
378
- version reusing an old body, and the limits are checked on every write.
379
- Validation is a hand-written tokenizer and CommonMark subset rather than a
380
- dependency, because it is a lint: the page's CSP is the boundary.
381
-
382
- Who made a version is the one fact a built-in connector needs that
383
- `ConnectorContext` deliberately does not carry. `src/connector-caller.ts`
384
- attaches the admitted identity beside the context on the request's scoped
385
- view, the way the OAuth sealer rides beside it — readable only by in-repo
386
- code, set from the authorization and never from arguments
387
- (`test/identity-scope.test.ts`, `test/artifacts-connector.test.ts`).
822
+ one with a `data-scheme` attribute on the page.
823
+
824
+ Maintained providers act as the caller downstream. `src/connector-caller.ts`
825
+ attaches the admitted identity beside the request-scoped context, readable only
826
+ by in-repo code and set from authorization, never arguments. Google Workspace
827
+ domain-wide delegation
828
+ (`src/providers/google/`, first consumed by `./providers/gmail`) hands the
829
+ admitted identity to a deployment-config `subject` function and mints a
830
+ service-account token as whatever Workspace address it returns. The caller
831
+ carries whether an inbound provider _authenticated_ it: an open deployment
832
+ admits everyone as the anonymous actor, and the function is never asked about
833
+ them. No caller, an unauthenticated one, or no address fails `auth_required`
834
+ before any request leaves. The provider never chooses the account and a
835
+ deployment's own connector still cannot read the caller (PRINCIPLES.md, INV-3; `test/google-workspace-delegation.test.ts`). Its tokens live in a
836
+ bounded module-level map, in memory only, keyed by the key's own digest as well
837
+ as the account, subject, and scopes. A mint in flight is shared across requests
838
+ the way OAuth flights are: the owner settles a plain outcome
839
+ inside its own request — a token, Google's refusal, or `abandoned` — and each
840
+ follower waits on a promise of its own under its own signal and timer to the
841
+ flight's deadline, never touching the owner's signal or response.
388
842
 
389
843
  ## Import-graph purity
390
844
 
391
845
  Nothing reachable from `src/index.ts` may import a `node:` builtin, so the same
392
846
  core runs unchanged in workerd and in Node. The Node-touching paths — `src/node.ts`
393
- (the `node:http` adapter), `src/storage/file.ts`, and the QuickJS process pool
847
+ (the `node:http` adapter), `src/sqlite.ts` (`node:sqlite`), and the QuickJS process pool
394
848
  (`src/executors/quickjs.ts` plus its child) — each sit behind an explicit subpath
395
849
  and must stay unreachable from the root. `./auth/clerk` is separate because
396
850
  `@clerk/backend` is an optional peer rather than a dependency, and
397
851
  `./auth/cloudflare-access` for a third reason: it is Web-API-pure, but its trust
398
852
  contract is specific to a direct Worker invocation carrying `ctx.access`.
399
853
 
400
- `test/purity.test.ts` walks the relative-import graph and fails on any `node:`
854
+ `test/purity.node.test.ts` walks the relative-import graph and fails on any `node:`
401
855
  specifier in a reachable file, or on any of those modules — plus the UI bundle,
402
856
  encrypted vault, and activity implementation — being reachable at all;
403
- `test/package-surface.test.ts` and `scripts/check-package.mjs` guard the same
857
+ `test/package-surface.node.test.ts` and `scripts/check-package.mjs` guard the same
404
858
  boundary in the published tarball. The failure mode is not theoretical: one
405
859
  convenience import of `node:crypto` in a shared helper stops the whole Worker
406
860
  shape from building, in someone else's repository rather than this one.
407
861
 
408
862
  ## Effect inside
409
863
 
410
- The core runs on stable Effect v4, exact-pinned to `4.0.0`; no API a deployment
864
+ The core runs on stable Effect v4 through a compatible `^4.0.0` range; no API a deployment
411
865
  touches does. `createConnecta`,
412
866
  `remoteMcp()`, `api()`, the `Connector` contract, and every shipped `.d.ts`
413
867
  are Promise-shaped and name no Effect type, so a connector author never meets a
@@ -441,7 +895,7 @@ and `npm run check:declarations` fails if what remains names an Effect type.
441
895
  ### One runner
442
896
 
443
897
  `src/runtime/run.ts` is the only place a fiber starts, and
444
- `test/purity.test.ts` fails if any other file calls `Effect.run*`, `runFork`,
898
+ `test/purity.node.test.ts` fails if any other file calls `Effect.run*`, `runFork`,
445
899
  `forkDaemon`, or `ManagedRuntime.make`. It exports two ways out:
446
900
 
447
901
  - **`runEdge(effect, { signal, runtime? })`** runs an effect at a Promise
@@ -456,39 +910,35 @@ and `npm run check:declarations` fails if what remains names an Effect type.
456
910
  to `ctx.waitUntil` when there is one.
457
911
 
458
912
  Beside them sit `withDeadlineEffect`, which aborts the operation's own signal
459
- with the labelled timeout error *before* interrupting it, so work that honors
913
+ with the labelled timeout error _before_ interrupting it, so work that honors
460
914
  the signal sees the same reason the caller does, and `fromSignal`, which turns
461
915
  an `AbortSignal` into a failure to race against. Nothing runs Effect at module
462
916
  scope, where a Worker may not start work, and nothing logs through
463
917
  `Effect.log`: logging goes through the configured `Logger`, which is what
464
918
  honors `logger: "silent"`.
465
919
 
466
- Each Connecta also gets a runtime of its own (`src/runtime/services.ts`):
467
- `Storage`, `Vault`, `ActivityRecorder` (one that records nothing when the module
468
- is omitted), `Logger`, and `ResolvedConfig`. Creating it builds nothing, since
469
- a Worker constructs its Connecta at global scope; the first run that needs the
470
- services builds them, and `close()` disposes the runtime last. Two things
471
- deliberately do not run on it. The request pipeline runs on no runtime, so
472
- `/health` and a closed deployment's 503 keep answering after `close()`. And a
473
- registry provides its own storage and logger to its programs
920
+ There is no per-Connecta runtime. createConnecta resolves its configuration
921
+ into plain values once (`src/config.ts`), and nothing needs a service context
922
+ built from them. The request pipeline runs on no runtime, so `/health` and a
923
+ closed deployment's 503 keep answering after `close()`. A registry provides its
924
+ own `Storage` and `Logger` services (`src/runtime/services.ts`) to its programs
474
925
  (`runOnPartition` in `src/runtime/storage.ts`), because a personal registry's
475
- storage is the root's namespaced to its principal and the runtime's `Storage`
476
- would be another partition's.
926
+ storage is the root's namespaced to its principal.
477
927
 
478
928
  ### What runs on Effect
479
929
 
480
- | Area | Shape |
481
- | --- | --- |
930
+ | Area | Shape |
931
+ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
482
932
  | Requests (`src/server.ts`, `src/routes/mcp.ts`, `src/routes/oauth.ts`) | One fiber per `fetch`, tied to `request.signal`. An `/mcp` request's admission permit and every `McpServer` it builds live in a Scope the response carries out, closed when the body ends, fails, or is cancelled, when the signal aborts, when its admitted-request lifetime ends in that request, or at once if the handler fails first. If workerd runs none of those callbacks, the next request reclaims only the expired admission record; it cannot close another request's Scope. The OAuth callback is uninterruptible: a single-use code's exchange and catalog invalidation are one commitment. |
483
- | Admission (`executor-admission.ts`, `call-admission.ts`) | Queued waiters are Deferreds settled by whoever removes them from the queue; a wait is one flat race of grant, Clock timeout, and signal. The uncontended path stays synchronous. Two controllers, as [#453](https://github.com/zackbart/connecta/issues/453) requires. |
484
- | One tool call (`invocation.ts`) | One fiber: resolution, the read-only and schema refusals, admission, and the downstream attempt sit under a single `withDeadlineEffect`, whose expiry interrupts the call wherever it is. The permit is an `acquireRelease`; the connector call is `Effect.tryPromise` over the unchanged `Connector`. |
485
- | `execute_code` (`execute.ts`) | One fiber whose Scope owns the run's signal and executor lease, so a result, a throw, the watchdog, and cancellation all release the lease and abort the signal the same way. The executor's `acquire()` and `execute()` stay Promises raced against the signal and `execute.watchdogMs`. Each guest host call is a fiber of its own. |
486
- | Discovery (`catalog-service.ts`) | A request-scoped cache: one shared read per connector (`runtime/shared-read.ts`), settled by the read itself and carrying its own signal and the probe timeout whichever asker starts it. Each asker waits under its own deadline and signal, so one that times out or is cancelled fails alone, and the read is cancelled only once every asker has gone. Fan-out is `Effect.forEach` under the discovery concurrency. |
487
- | Registry (`registry.ts`) | Catalog persistence and the result stash are programs over `Storage`. A refresh flight is a Deferred its publishing request completes, bounded by its owner's deadline (the default probe timeout when it has none); persisted-catalog writes take per-connector turns, each a Deferred its own request completes. Same-request loads share one read the way discovery's do. |
488
- | Remote MCP (`connectors/remote-mcp.ts`) | Each request scope's state holds a Scope, each connection is a lease forked from it, and a connect in flight is a Deferred carrying the client it connected. Closing a session and the transport are each bounded to a second. |
489
- | Downstream OAuth (`auth/downstream-oauth.ts`) | A refresh flight is a Deferred; the owner's redemption is a fiber its abort interrupts, and committing an answer that already exists is uninterruptible. |
490
- | Operator and activity data (`routes/operator.ts`) | Each JSON route is one program run by `serveOperator` behind the Promise `handle()`. Reads run under the request's signal; writes do not, so a vault write or OAuth disconnect that started reaches its cache invalidation. |
491
- | QuickJS pool (`executors/quickjs.ts`, Node only) | Each child is a scoped resource whose release sends SIGTERM, then SIGKILL after a second; crash respawn backoff is a `Schedule`. |
933
+ | Admission (`executor-admission.ts`, `call-admission.ts`) | Queued waiters are Deferreds settled by whoever removes them from the queue; a wait is one flat race of grant, Clock timeout, and signal. The uncontended path stays synchronous. Two controllers, as [#453](https://github.com/zackbart/connecta/issues/453) requires. |
934
+ | One tool call (`invocation.ts`) | One fiber: resolution, the read-only and schema refusals, admission, and the downstream attempt sit under a single `withDeadlineEffect`, whose expiry interrupts the call wherever it is. The permit is an `acquireRelease`; the connector call is `Effect.tryPromise` over the unchanged `Connector`. |
935
+ | `execute_code` (`execute.ts`) | One fiber whose Scope owns the run's signal and executor lease, so a result, a throw, the watchdog, and cancellation all release the lease and abort the signal the same way. The executor's `acquire()` and `execute()` stay Promises raced against the signal and `execute.watchdogMs`. Each guest host call is a fiber of its own. |
936
+ | Discovery (`catalog-service.ts`) | A request-scoped cache: one shared read per connector (`runtime/shared-read.ts`), settled by the read itself and carrying its own signal and the probe timeout whichever asker starts it. Each asker waits under its own deadline and signal, so one that times out or is cancelled fails alone, and the read is cancelled only once every asker has gone. Fan-out is `Effect.forEach` under the discovery concurrency. |
937
+ | Registry (`registry.ts`) | The result stash is a program over `Storage`. Same-request catalog loads share one read the way discovery's do. Remote MCP caching uses the SDK ResponseCacheStore adapter over SQL-backed KV, with no cross-request catalog flight. |
938
+ | Remote MCP (`connectors/remote-mcp.ts`) | Each request scope's state holds a Scope, each connection is a lease forked from it, and a connect in flight is a Deferred carrying the client it connected. Closing a session and the transport are each bounded to a second. |
939
+ | Downstream OAuth (`auth/downstream-oauth.ts`) | A refresh flight is a Deferred. Preparation follows caller cancellation; after dispatch, the HTTP exchange owns a 20-second deadline and the grant commit continues through the runtime deferred-work hook. |
940
+ | Operator and activity data (`routes/operator.ts`) | Each JSON route is one program run by `serveOperator` behind the Promise `handle()`. Reads run under the request's signal; writes do not, so a vault write or OAuth disconnect that started reaches its cache invalidation. |
941
+ | QuickJS pool (`executors/quickjs.ts`, Node only) | Each child is a scoped resource whose release sends SIGTERM, then SIGKILL after a second; crash respawn backoff is a `Schedule`. |
492
942
 
493
943
  ### What stays plain
494
944
 
@@ -505,7 +955,7 @@ would be another partition's.
505
955
  wrapping them would buy nothing and could starve a request.
506
956
  - **The MCP edges.** `@modelcontextprotocol/server` upward and the SDK client
507
957
  downstream; Effect's own `McpServer` is not used.
508
- - **The Node and browser leaves.** `node.ts`, `fileStorage`, the QuickJS
958
+ - **The Node and browser leaves.** `node.ts`, `sqlite.ts`, the QuickJS
509
959
  child, runtime, and protocol, and the operator UI's browser app.
510
960
 
511
961
  ### Across requests
@@ -541,19 +991,16 @@ when its only pending work is waiting on another request and it has no timer or
541
991
  I/O of its own. Connecta's deadline timers are what keep a waiting request
542
992
  alive and bounded, so no wait may lose its timer.
543
993
 
544
- **A shared flight has a lifetime of its own.** A catalog refresh flight runs in
545
- its owner's request, under its owner's scope and signal, and lives only as long
546
- as its owner's deadline. Past that the owner may have answered and gone, taking
547
- its I/O with it on workerd, and a connector that ignores its abort signal would
548
- otherwise hold every later reader on a flight nothing will ever settle. So each
549
- joiner waits on its own timer, set to the flight's bound: when it fires, the
550
- joiner abandons the flight and makes a fresh attempt, and a reader that arrives
551
- after the bound starts one without joining. Joiners also leave when the owner
552
- fails for a reason that was only its own, such as its cancellation, rather than
553
- inheriting it. A flight's result enters the caches only while the flight is
554
- current, checked at the same points as the catalog generation, so a stuck
555
- listing that lands late is never cached or persisted and never overwrites the
556
- fresh attempt's.
994
+ **Catalog I/O belongs to its request.** Listings share work only within one
995
+ request and authorization partition. Independent requests may read the same
996
+ complete SQL cache entry, but never await another request's listing, retain
997
+ its client, or inherit its sent-secret set. A rotated cache generation makes
998
+ late publication from an invalidated request unreachable. Missing chunks,
999
+ expired entries, storage errors, or a failed page trigger a fresh complete
1000
+ listing. A transient first-page tool-listing failure may use a complete catalog
1001
+ within five minutes of expiry in the same auth partition. Partial walks and
1002
+ auth or permission failures refuse fallback; resources require unexpired
1003
+ inventories, and stale fallback cannot authorize post-entry auth recovery.
557
1004
 
558
1005
  ### Why not Schema or HttpApi
559
1006
 
@@ -563,16 +1010,18 @@ of Worker cold start for it, and that cost was accepted over deep imports at
563
1010
  every call site. The catch is that a module the barrel reaches is paid for in
564
1011
  full, however little of it is used. Effect Schema for config validation and the
565
1012
  meta-tool inputs came to about +62 KB gzip and +13 ms of cold start in the
566
- root, for a validator no clearer than the hand-written `CONFIG_SCHEMA`; the
1013
+ root, for a validator no clearer than the hand-written schema in `src/config.ts`; the
567
1014
  meta-tool inputs stay zod, which the MCP SDK bundles regardless. `HttpApi` for
568
1015
  the operator and activity routes cost about +100 KB gzip on `./ui` and +130 KB
569
1016
  on `./activity`, and matching the wire format meant opting out of most of what
570
1017
  it does: unowned paths fall through rather than 404, a wrong method is a JSON
571
1018
  405, and the content-type and size checks run before a body is read. The root
572
- entry may import only `effect` itself (`test/purity.test.ts`). Effect v4's
1019
+ entry may import only `effect` itself (`test/purity.node.test.ts`). Effect v4's
573
1020
  area imports, such as `effect/http-api` and `effect/ai`, would have to stay
574
1021
  behind a subpath. APIs tagged `@stability unstable` can still change in minor
575
- releases, so stable v4 keeps the exact pin.
1022
+ releases, which is why the root may import only `effect` itself: that is what
1023
+ makes a caret range safe, and it lets a deployment that also uses Effect
1024
+ resolve a single copy. Adopting an unstable subpath is its own pull request.
576
1025
 
577
1026
  The stable `4.0.0` release was re-evaluated on 2026-10-01 with
578
1027
  [`scripts/probes/effect-v4-evaluation.mjs`](https://github.com/zackbart/connecta/blob/main/scripts/probes/effect-v4-evaluation.mjs).
@@ -627,32 +1076,48 @@ src/
627
1076
  server.ts route ordering, HTTPS upgrade, security wrapper
628
1077
  routes/ one file per surface; shared.ts holds the auth gate
629
1078
  skills.ts MCP instructions, the usage skill, connector guides
630
- catalog-drift.ts vetted manifests and the counts a refresh produces
1079
+ catalog-drift.ts reviewed classifications and the drift counts a refresh produces
1080
+ provider.ts defineProvider(): the one maintained-provider shape
631
1081
  activity.ts optional history factory and best-effort recorder
632
1082
  auth/ bearer, Cloudflare Access, clerk (optional peer), downstream OAuth (remote and static)
633
1083
  executors/ the QuickJS pool and child (Node only)
634
- node.ts listen() + fileStorage re-export (Node only)
1084
+ node.ts listen() (Node only)
1085
+ d1.ts, sqlite.ts the two storage drivers; storage/ holds the SQL core and key families
635
1086
  ```
636
1087
 
637
1088
  There are exactly two deployment shapes — `templates/node/`, which
638
1089
  `connecta init` copies with its container files, and `examples/worker/` — and
639
- `test/deployment-shapes.test.ts` with `npm run check:examples` keeps both
1090
+ `test/deployment-shapes.node.test.ts` with `npm run check:examples` keeps both
640
1091
  compiling and configuring the real thing.
641
1092
 
642
1093
  ## Sharp edges
643
1094
 
644
- - **The root registry is shared; identity views are partitioned.** Shared
645
- connector caches are visible to later requests in the isolate; personal
646
- connectors use a bounded principal registry and transient results the
647
- authenticated subject. Putting a downstream client or credential on the wrong
1095
+ - **The root registry is shared; identity views are partitioned.** Public remote catalogs are shared only after intake redaction, keyed by the
1096
+ connector configuration. Private entries include the admitted principal and
1097
+ pool even for shared-auth connectors. Personal connectors use a bounded
1098
+ principal registry and transient results the authenticated subject. Putting a downstream client or credential on the wrong
648
1099
  side of those lines is the highest-severity mistake available here.
649
1100
  - **Route order is behavior.** Moving a mutation route below the wildcard
650
1101
  `OPTIONS` opts it into CORS preflight; reordering admission after auth makes
651
1102
  the cheapest possible attack the most expensive request.
652
1103
  - **`close()` is idempotent and ordered.** Both admission pools, then the
653
- connector limiters, then the executor, then the Connecta's runtime; Node's `listen()` calls it on
1104
+ connector limiters, then the executor; Node's `listen()` calls it on
654
1105
  SIGTERM/SIGINT.
655
1106
  - **Structural mistakes throw at construction.** A duplicate connector id, an
656
- invalid admission rule, the old boolean `accessTokens` option, a missing executor:
1107
+ invalid admission rule or limit, an unknown option at any depth, the old
1108
+ boolean `accessTokens` option, a missing executor, or an executor without a
1109
+ lifecycle brand:
657
1110
  all refuse to boot (`test/config.test.ts`, `test/registry.test.ts`). Starting
658
1111
  in the wrong shape is worse than not starting.
1112
+ Shipped `/worker` and `/quickjs` executors carry a non-enumerable global-symbol
1113
+ brand that survives package duplication and bundling. Custom sandboxes opt in
1114
+ through `customExecutor(executor, { lifecycle: "self-managed" })` from the root
1115
+ entry and own their termination and cleanup. Plain upstream executors need
1116
+ the `/worker` adapter, regardless of constructor name.
1117
+
1118
+ ## Operator data contract
1119
+
1120
+ The contract now lives in [Operator UI](./operator-ui.md#operator-data-contract),
1121
+ including configuration provenance, live facts, activity disclosure, doctor,
1122
+ and the browser implementation. [Optional deployment modules](#optional-deployment-modules)
1123
+ keeps the module composition and request-lifetime rules here.