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