@zackbart/connecta 0.37.1 → 0.39.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 (265) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -4
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/agent-routes.js +231 -0
  8. package/dist/auth/clerk-sdk/generated.js +2 -1
  9. package/dist/auth/downstream-oauth/consent.js +519 -0
  10. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  11. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  12. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  13. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  14. package/dist/auth/downstream-oauth.js +66 -2516
  15. package/dist/auth/downstream-revocation.js +1 -1
  16. package/dist/auth/native.js +27 -0
  17. package/dist/auth/oauth-grant.js +24 -0
  18. package/dist/auth/oauth-token-response.js +1 -1
  19. package/dist/auth/static-oauth.js +153 -131
  20. package/dist/auth/vercel-agent.js +2 -1
  21. package/dist/auth-elicitation.js +9 -3
  22. package/dist/authorization-handoff.js +11 -5
  23. package/dist/branding.d.ts +1 -15
  24. package/dist/branding.js +2 -2
  25. package/dist/bytes.js +42 -0
  26. package/dist/call-admission-context.js +2 -5
  27. package/dist/call-recovery.js +0 -3
  28. package/dist/catalog-cache.js +130 -43
  29. package/dist/catalog-fingerprint.js +2 -18
  30. package/dist/catalog-policy.js +18 -0
  31. package/dist/catalog-service.js +285 -48
  32. package/dist/catalog.js +18 -2
  33. package/dist/config-defaults.d.ts +1 -0
  34. package/dist/config-defaults.js +1 -0
  35. package/dist/config-schema.d.ts +1 -1
  36. package/dist/config-schema.js +6 -2
  37. package/dist/config.d.ts +57 -25
  38. package/dist/config.js +30 -77
  39. package/dist/connector-caller.d.ts +1 -1
  40. package/dist/connector-health.js +59 -0
  41. package/dist/connector-status.js +117 -0
  42. package/dist/connectors/api-connector.d.ts +7 -2
  43. package/dist/connectors/api-connector.js +11 -7
  44. package/dist/connectors/guarded-fetch.d.ts +0 -6
  45. package/dist/connectors/guarded-fetch.js +1 -11
  46. package/dist/connectors/negotiation-cache.js +22 -4
  47. package/dist/connectors/remote-mcp/call.js +164 -0
  48. package/dist/connectors/remote-mcp/catalog.js +278 -0
  49. package/dist/connectors/remote-mcp/config.js +263 -0
  50. package/dist/connectors/remote-mcp/connection.js +671 -0
  51. package/dist/connectors/remote-mcp/errors.js +488 -0
  52. package/dist/connectors/remote-mcp/oauth.js +469 -0
  53. package/dist/connectors/remote-mcp/resources.js +473 -0
  54. package/dist/connectors/remote-mcp/runtime.js +1 -0
  55. package/dist/connectors/remote-mcp/transport.js +544 -0
  56. package/dist/connectors/remote-mcp.js +61 -2898
  57. package/dist/connectors/resource-uri.js +0 -4
  58. package/dist/credential-contract.d.ts +23 -0
  59. package/dist/credential-reads.js +17 -0
  60. package/dist/credential-verification.js +53 -0
  61. package/dist/credentials.d.ts +7 -0
  62. package/dist/credentials.js +60 -25
  63. package/dist/d1.d.ts +0 -89
  64. package/dist/d1.js +0 -34
  65. package/dist/describe-config.d.ts +12 -2
  66. package/dist/describe-config.js +2 -0
  67. package/dist/discovery-health.js +199 -0
  68. package/dist/downstream-input.js +4 -2
  69. package/dist/errors.d.ts +55 -17
  70. package/dist/errors.js +95 -41
  71. package/dist/execute.js +172 -125
  72. package/dist/executor-result.js +5 -2
  73. package/dist/executors/quickjs-runtime.js +2 -25
  74. package/dist/executors/quickjs.js +1 -1
  75. package/dist/guest-functions.js +83 -0
  76. package/dist/guest-runtime.js +63 -20
  77. package/dist/guest-types.d.ts +24 -10
  78. package/dist/http/argument-problems.js +35 -0
  79. package/dist/http/client.js +1 -1
  80. package/dist/http/errors.js +7 -6
  81. package/dist/http/fields.js +192 -13
  82. package/dist/http/hatch-response.js +73 -0
  83. package/dist/http/hatch.js +88 -23
  84. package/dist/http/json-schema.js +1 -1
  85. package/dist/http/node-transport.js +0 -1
  86. package/dist/http/oauth.js +4 -3
  87. package/dist/http/paging.js +6 -9
  88. package/dist/http/provider.js +33 -5
  89. package/dist/http/safety.js +9 -1
  90. package/dist/http/services.js +1 -8
  91. package/dist/http/tools.js +45 -30
  92. package/dist/http/vendor.js +7 -7
  93. package/dist/identity.js +2 -6
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +9 -2
  96. package/dist/invocation-auth.js +2 -1
  97. package/dist/invocation.js +21 -6
  98. package/dist/membership.d.ts +13 -5
  99. package/dist/membership.js +34 -2
  100. package/dist/meta-tools.js +50 -35
  101. package/dist/native-recovery.js +4 -16
  102. package/dist/native-tools.js +6 -2
  103. package/dist/oauth-handoff.js +2 -2
  104. package/dist/oauth-sealing.js +10 -0
  105. package/dist/operation-vocabulary.js +136 -14
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +2 -49
  108. package/dist/operator-ui/contract.d.ts +12 -2
  109. package/dist/operator-ui/generated.js +3 -3
  110. package/dist/operator-ui/model.d.ts +22 -1
  111. package/dist/program-writes.js +3 -8
  112. package/dist/provider-call.js +0 -7
  113. package/dist/provider.d.ts +1 -1
  114. package/dist/providers/_shared/google/kit.js +1 -6
  115. package/dist/providers/_shared/google/service-account.js +1 -3
  116. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  117. package/dist/providers/_shared/guide.js +3 -0
  118. package/dist/providers/_shared/json.js +12 -0
  119. package/dist/providers/_shared/rest/compact-index.js +1 -1
  120. package/dist/providers/_shared/rest/kit.js +1 -3
  121. package/dist/providers/_shared/rest/operation-index.js +49 -53
  122. package/dist/providers/_shared/rest/value-safety.js +106 -28
  123. package/dist/providers/breeze/api.js +9 -8
  124. package/dist/providers/breeze/index.js +4 -2
  125. package/dist/providers/breeze/skill.generated.js +1 -1
  126. package/dist/providers/breeze/tools.js +7 -7
  127. package/dist/providers/ccb/api.js +3 -7
  128. package/dist/providers/ccb/index.js +7 -1
  129. package/dist/providers/ccb/skill.generated.js +1 -1
  130. package/dist/providers/ccb/tools.js +1 -6
  131. package/dist/providers/cloudflare/api.js +10 -3
  132. package/dist/providers/cloudflare/index.js +2 -1
  133. package/dist/providers/cloudflare/safety.js +3 -3
  134. package/dist/providers/cloudflare/skill.generated.js +1 -1
  135. package/dist/providers/docs/lib/schemas.js +24 -22
  136. package/dist/providers/docs/safety.js +2 -0
  137. package/dist/providers/docs/skill.generated.js +1 -1
  138. package/dist/providers/docs/tools.js +4 -14
  139. package/dist/providers/drive/api.js +1 -0
  140. package/dist/providers/drive/lib/contracts.js +8 -5
  141. package/dist/providers/drive/skill.generated.js +1 -1
  142. package/dist/providers/drive/tools.js +23 -41
  143. package/dist/providers/forms/api.js +1 -0
  144. package/dist/providers/forms/skill.generated.js +1 -1
  145. package/dist/providers/forms/tools.js +11 -16
  146. package/dist/providers/github/api.js +1 -0
  147. package/dist/providers/github/lib/discovery.js +1 -1
  148. package/dist/providers/github/skill.generated.js +1 -1
  149. package/dist/providers/github/tools.js +61 -17
  150. package/dist/providers/gmail/lib/schemas.js +60 -50
  151. package/dist/providers/gmail/skill.generated.js +1 -1
  152. package/dist/providers/gmail/tools.js +12 -26
  153. package/dist/providers/granola/index.js +2 -0
  154. package/dist/providers/granola/skill.generated.js +1 -1
  155. package/dist/providers/overflow/api.js +8 -4
  156. package/dist/providers/overflow/index.js +2 -1
  157. package/dist/providers/overflow/skill.generated.js +1 -1
  158. package/dist/providers/overflow/tools.js +3 -7
  159. package/dist/providers/planning-center/api.js +5 -2
  160. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  161. package/dist/providers/planning-center/lib/json-api.js +1 -9
  162. package/dist/providers/planning-center/skill.generated.js +1 -1
  163. package/dist/providers/planning-center/tools.js +2 -1
  164. package/dist/providers/quickbooks/safety.js +80 -39
  165. package/dist/providers/quickbooks/skill.generated.js +1 -1
  166. package/dist/providers/sheets/api.js +1 -0
  167. package/dist/providers/sheets/skill.generated.js +1 -1
  168. package/dist/providers/sheets/tools.js +14 -23
  169. package/dist/providers/slides/skill.generated.js +1 -1
  170. package/dist/providers/slides/tools.js +19 -26
  171. package/dist/providers/stripe/index.js +2 -1
  172. package/dist/providers/stripe/safety.js +2 -2
  173. package/dist/providers/stripe/skill.generated.js +1 -1
  174. package/dist/providers/tithely/api.js +5 -8
  175. package/dist/providers/tithely/index.js +3 -2
  176. package/dist/providers/tithely/skill.generated.js +1 -1
  177. package/dist/providers/tithely/tools.js +7 -5
  178. package/dist/providers/vercel/api.js +2 -7
  179. package/dist/providers/vercel/lib/pin.js +1 -1
  180. package/dist/providers/vercel/safety.js +1 -1
  181. package/dist/providers/vercel/skill.generated.js +1 -1
  182. package/dist/providers/vercel/tools.js +6 -4
  183. package/dist/redaction-marker.js +2 -2
  184. package/dist/registry.d.ts +11 -5
  185. package/dist/registry.js +46 -20
  186. package/dist/request-principal.js +1 -1
  187. package/dist/request-state.js +2 -2
  188. package/dist/routes/access-tokens.js +1 -1
  189. package/dist/routes/activity-disclosure.js +1 -1
  190. package/dist/routes/connect.js +2 -2
  191. package/dist/routes/credentials.js +130 -61
  192. package/dist/routes/dispatch.js +5 -1
  193. package/dist/routes/mcp.js +34 -17
  194. package/dist/routes/oauth-management.js +1 -1
  195. package/dist/routes/operator.js +6 -5
  196. package/dist/routes/shared.d.ts +13 -8
  197. package/dist/routes/shared.js +15 -9
  198. package/dist/routes/ui-config.js +3 -3
  199. package/dist/routes/ui-data.js +126 -20
  200. package/dist/routes/ui.js +17 -1
  201. package/dist/runtime/call-admission.js +1 -1
  202. package/dist/runtime/call.js +8 -3
  203. package/dist/runtime/native-storage.js +9 -0
  204. package/dist/runtime/request-reads.js +182 -0
  205. package/dist/runtime/request-scope.js +16 -0
  206. package/dist/runtime/settlement.js +1 -1
  207. package/dist/runtime/shared-read.js +2 -15
  208. package/dist/runtime/storage.js +12 -0
  209. package/dist/sent-secrets.d.ts +1 -2
  210. package/dist/sent-secrets.js +3 -8
  211. package/dist/server.js +4 -4
  212. package/dist/skills.js +44 -54
  213. package/dist/sqlite.d.ts +0 -19
  214. package/dist/sqlite.js +2 -83
  215. package/dist/storage/keys.js +69 -128
  216. package/dist/storage/memory.js +8 -2
  217. package/dist/storage/sql-native.js +27 -101
  218. package/dist/storage/sql.d.ts +1 -24
  219. package/dist/storage/sql.js +13 -14
  220. package/dist/storage-digest.js +32 -0
  221. package/dist/timeout.js +8 -0
  222. package/dist/tool-error.js +4 -0
  223. package/dist/types.d.ts +33 -0
  224. package/dist/url-safety.js +0 -4
  225. package/dist/usage-guide.js +18 -37
  226. package/dist/validate.d.ts +3 -0
  227. package/dist/validate.js +220 -158
  228. package/dist/version.d.ts +1 -1
  229. package/dist/version.js +1 -1
  230. package/dist/worker.js +1 -1
  231. package/dist/write-outcome.js +0 -3
  232. package/documentation/README.md +19 -55
  233. package/documentation/architecture.md +436 -515
  234. package/documentation/auth.md +229 -88
  235. package/documentation/code-mode.md +201 -94
  236. package/documentation/deploying.md +25 -43
  237. package/documentation/integrating.md +18 -19
  238. package/documentation/meta-tools.md +225 -51
  239. package/documentation/migration-0.33.md +10 -6
  240. package/documentation/migration-0.34.md +5 -5
  241. package/documentation/migration-retired-layouts.md +74 -0
  242. package/documentation/operator-tests.md +20 -120
  243. package/documentation/operator-ui.md +108 -16
  244. package/documentation/pool-membership-upgrade.md +44 -0
  245. package/documentation/provider-auth-migration.md +4 -10
  246. package/documentation/provider-migration-0.29.md +4 -15
  247. package/documentation/providers.md +105 -0
  248. package/examples/worker/AGENTS.md +14 -16
  249. package/examples/worker/README.md +24 -118
  250. package/examples/worker/src/connecta.config.ts +1 -0
  251. package/guides/connecting-agents.md +9 -31
  252. package/guides/how-it-works.md +6 -4
  253. package/guides/safety-and-trust.md +4 -1
  254. package/package.json +19 -19
  255. package/templates/node/AGENTS.md +13 -12
  256. package/templates/node/README.md +9 -26
  257. package/templates/node/package.json +1 -1
  258. package/templates/node/src/connecta.config.ts +1 -0
  259. package/dist/auth/oauth-v2-migration.js +0 -135
  260. package/dist/provider-call-facts.d.ts +0 -115
  261. package/dist/provider-call.d.ts +0 -112
  262. package/dist/storage/d1-copy.js +0 -255
  263. package/dist/storage/sql-schema.d.ts +0 -6
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -31,9 +31,8 @@ implementation boundary. Use these sections for the affected subsystem.
31
31
  | Find code and deployment evidence | [Source map](#where-else-to-look), [sharp edges](#sharp-edges), [repository policy](https://github.com/zackbart/connecta/blob/main/AGENTS.md) |
32
32
 
33
33
  Source paths and test names in each section identify its implementation and
34
- regression evidence. Recorded production probes and bundle measurements are
35
- historical observations tied to their stated date or version, not current
36
- benchmarks or proof that planned replacements have shipped.
34
+ regression evidence. Decision records preserve historical measurements; current
35
+ contracts and executable evidence describe the implementation.
37
36
 
38
37
  ## The two lifetimes
39
38
 
@@ -48,7 +47,8 @@ as the isolate — on Workers a lazy module-scope singleton, which is why both
48
47
  deployment shapes construct it outside the request handler.
49
48
 
50
49
  An OAuth connector, `remoteMcp()` or `api()` with a static grant, owns a
51
- runtime-local refresh completion gate in `src/auth/downstream-oauth.ts`.
50
+ runtime-local refresh completion gate in
51
+ `src/auth/downstream-oauth/refresh-coordinator.ts`.
52
52
  Joined scopes share the outcome while keeping their own request I/O and
53
53
  cancellation. Grant use and the caller's wait belong to the call. Renewal
54
54
  preparation is call-owned only until the first durable coordinator write
@@ -100,6 +100,18 @@ never retried and requires re-consent. Valid rotations retry their grant commit
100
100
  up to 32 times without another HTTP request; exhausted commits and all dispatched
101
101
  provider failures require re-consent. [Auth](./auth.md#refresh-failures) describes the lease and failure
102
102
  contracts.
103
+ The grant body stores `expiresAt` beside the tokens it describes; reads never
104
+ add it (INV-10). A call renews a token within `OAUTH_REFRESH_SKEW_MS` (60 s)
105
+ of it before sending: the invocation's authorization check (`registerInvocationAuth`
106
+ in `src/connectors/remote-mcp.ts` and `src/connectors/api-connector.ts`) runs
107
+ the 401 flow before entering the connector, so a failed renewal is a
108
+ pre-dispatch authorization failure, never an unknown write (INV-9).
109
+ `KvOAuthProvider.renewalDue` decides from the grant read that check already
110
+ made, in memory. The token request carries the access token it replaces
111
+ (`earlyRenewalFetch`), and the coordinator adopts any stored token that differs
112
+ instead of redeeming again. Status, listing, discovery and the connectionless
113
+ catalog read never renew. [Auth](./auth.md#refreshing-before-expiry) has the
114
+ operator contract.
103
115
 
104
116
  **Per request, and no longer.** The MCP server, its transport, downstream MCP
105
117
  clients, abort signals, and the connector scope a probe opens all belong to the
@@ -108,7 +120,10 @@ across requests on Workers is a cross-request capability leak, and a promise awa
108
120
  work the runtime may already have torn down. Deferred work has one sanctioned
109
121
  channel, `ctx.waitUntil`, threaded through `fetch(request, env, ctx)` — activity
110
122
  writes and OAuth renewals from claim submission use it. Catalog listing and SQL cache
111
- publication finish in the requesting scope. A fresh
123
+ publication finish in the requesting scope. The operator surface's Promise
124
+ edge (`ui.handle`) owns the request's signal and is awaited to its end even
125
+ when the caller leaves, so a storage write it has started, such as a stored
126
+ status observation, settles before the request does. A fresh
112
127
  `McpServer` per request is what makes the deployment stateless: no sessions, no
113
128
  server push, no stream resumability, scope resolved rather than remembered.
114
129
 
@@ -226,7 +241,7 @@ An admitted non-preflight `/mcp` request then takes five steps in
226
241
  ([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
227
242
  handed out, and retiring a scoping boundary into fail-open is worse than any
228
243
  404 — then register the six meta-tools on a fresh `McpServer`
229
- (`test/server.test.ts`, `test/code-first-surface.test.ts`).
244
+ (`test/server.test.ts`).
230
245
 
231
246
  ### Modern request metadata and transport refusals
232
247
 
@@ -443,139 +458,24 @@ Only an omitted or `undefined` `callAdmission` selects a connector's default.
443
458
  The shared schema rejects malformed defined policies at factory construction,
444
459
  and the registry validates custom connector policies before registration.
445
460
  Falsy values such as `false`, `null`, `0`, and `""` are construction errors
446
- (INV-11), never a way to disable admission. The generated provider fixtures
447
- exercise these refusals and unchanged defaults in
448
- `test/call-admission-options.test.ts`.
461
+ (INV-11), never a way to disable admission.
449
462
 
450
463
  ### Production Worker disconnects
451
464
 
452
- The original production probe for [#573](https://github.com/zackbart/connecta/issues/573)
453
- ran on 2026-09-25 with compatibility date `2025-01-01`, first with no flags and
454
- then with `enable_request_signal`. It used Wrangler 4.114.0 and a Node 26.9.0
455
- client. A raw Worker stream established the runtime behavior; a custom auth
456
- provider returning a synthetic streaming 401 exercised Connecta's real admission
457
- wrapper. Each case had its own Connecta instance with one request permit and no
458
- queue. Health checks and subsequent MCP requests confirmed the same isolate ID.
459
- The [recorded observations](https://github.com/zackbart/connecta/blob/1ab92695f2636f904274d900ce209553358ff1f3/scripts/probes/worker-disconnect-2026-09-25.json)
460
- contain all twelve cases, including served flags and client-end timing.
461
-
462
- | Response and flag | Raw stream abort / cancel | Connecta source abort / cancel | Admission after response; next request |
463
- | ----------------------------------------------------- | ------------------------- | ------------------------------ | -------------------------------------- |
464
- | Heartbeats, no flag | neither | neither | active 1; 503 |
465
- | Idle with a pending timer, no flag | neither | neither | active 1; 503 |
466
- | Heartbeats, request signal enabled | abort only | both | active 0; 200 |
467
- | Idle with a pending timer, request signal enabled | abort only | both | active 0; 200 |
468
- | One chunk, then no timer or I/O, either configuration | neither | neither | active 1; 503 |
469
-
470
- The last row is a different failure. The client observed the response end
471
- _before_ its planned disconnect, although the source never closed itself.
472
- The runtime ended the response without running JavaScript cleanup. This does
473
- not show a missing abort on a still-live request. The other cases remained open
474
- until the client aborted. Admission was sampled 1.5 seconds later, followed by
475
- an actual MCP request; this measures retained capacity, not an infinite leak.
476
-
477
- Enable `enable_request_signal` when deploying on Workers: it let Connecta's
478
- abort listener cancel the source and release admission for live responses in
479
- this experiment. It does not solve runtime-ended streams. The admitted-request
480
- bound above recovers capacity on the first later request at or after its
465
+ Enable `enable_request_signal` when deploying on Workers so Connecta can cancel
466
+ the source and release admission for live responses. It does not solve
467
+ runtime-ended streams. The admitted-request bound above recovers capacity on the first later request at or after its
481
468
  deadline, even if the old request's timer never ran. This is capacity recovery,
482
469
  not proof that workerd cancelled an old stream. A still-live request reaches
483
470
  the same hard deadline and is aborted in its own context.
484
471
  Cloudflare documents the flag in its
485
472
  [Request API](https://developers.cloudflare.com/workers/runtime-apis/request/).
486
473
 
487
- ### Authentication release by collection on workerd
488
-
489
- Measured on 2026-10-20 with workerd 1.20260722.1 (Miniflare 4.20260722.0),
490
- compatibility date `2025-01-01`:
491
-
492
- - `FinalizationRegistry` and `WeakRef` are undefined without
493
- `enable_weak_ref`; the flag's default-on date is 2025-05-05.
494
- - With `enable_weak_ref` and `enable_request_signal`, an object reachable only
495
- through a fetch continuation is finalized once its invocation ends and
496
- workerd cancels the fetch; while the invocation runs it is not. Pending
497
- timers and listeners on the request's signal do not keep it.
498
- - Strong values in a WeakMap keyed by a `Request` (even one the handler
499
- created), properties set on a `Request`, and WeakMap values keyed by its
500
- `Headers`, `ctx` or `env` stay reachable after the invocation ends. The
501
- lifetime registry in `src/auth-admission.ts` therefore stores `WeakRef`s.
502
- - No `gc()` is exposed. Allocating large arrays (32 × 1 MiB per request)
503
- triggers collections, but the most recent unreachable object often survives
504
- them; V8 in this build scans the native stack conservatively. The inspector's
505
- precise `HeapProfiler.collectGarbage` freed it.
506
- - In the vitest-pool-workers harness, an isolate that had already collected
507
- one scenario's cancelled work sometimes kept a later scenario's cancelled
508
- work reachable through 400 rounds of pressure; standalone Miniflare never
509
- did. `test/auth-admission-binding.test.ts` gives each collection scenario a
510
- fresh isolate and requires two of four sentinels collected before asserting
511
- that live charges stayed counted.
512
-
513
- A second production run on the same date tested the request bound at 2,000 ms.
514
- All twelve raw-stream and MCP cases passed with both flag configurations. Live
515
- MCP requests still rejected a competing request with 503 before the deadline;
516
- the first MCP request after the deadline returned all eight tools with 200 in
517
- the original isolate. Admission then reported zero active requests. Without the
518
- flag, the source still recorded no abort or cancellation, so recovery did not
519
- rely on runtime cleanup. The [recovery observations](https://github.com/zackbart/connecta/blob/1ab92695f2636f904274d900ce209553358ff1f3/scripts/probes/worker-disconnect-recovered-2026-09-25.json)
520
- also retain two inconclusive queue-handoff cases: concurrent client connections
521
- reached different isolates and could not establish an abandoned queued waiter.
522
- A [second trial using HTTP/2](https://github.com/zackbart/connecta/blob/1ab92695f2636f904274d900ce209553358ff1f3/scripts/probes/worker-disconnect-h2-2026-09-25.json)
523
- repeated the twelve passing core cases and verified queued cancellation with
524
- request signals enabled. Even on one HTTP/2 connection, the no-signal queue case
525
- crossed isolates and remained inconclusive. The Node and workerd regression
526
- suites cover expired waiters, orphaned handoffs, and identity-safe late release
527
- deterministically.
528
-
529
- The [historical probe source](https://github.com/zackbart/connecta/blob/1ab92695f2636f904274d900ce209553358ff1f3/scripts/probes/worker-disconnect.mjs)
530
- and observations are retained in Git history, outside the current checkout and
531
- npm package. The probe deployed a disposable Worker containing only synthetic
532
- data, verified the served configuration, saved observations, and deleted the
533
- Worker in `finally`. It used no `waitUntil`, which would change the lifetime
534
- being measured. The production runtime build number was not exposed by the
535
- deployment; these findings are tied to the date and flags above.
536
-
537
474
  ### Production Access bypass for agent endpoints
538
475
 
539
- P0 of [#837](https://github.com/zackbart/connecta/issues/837) asked whether
540
- `/agent/*` can skip a Worker-level Cloudflare Access application, so an agent
541
- sends only its `cta_` token, while every other path keeps Access and
542
- `ctx.access`. It can. The probe ran on 2026-10-15 against a disposable Worker on
543
- its `workers.dev` hostname, with compatibility date `2026-09-01`, `cf`
544
- 1.0.0-beta.14, Wrangler 4.114.0, and curl 8.7.1 with `--path-as-is`, so raw
545
- paths reached the edge unnormalized. The Worker-level application allowed only
546
- a probe service token. Its unauthenticated refusal was therefore Access's 403
547
- page rather than a login redirect. The [observations](https://github.com/zackbart/connecta/blob/main/scripts/probes/access-agent-bypass-2026-10-15.json)
548
- record three identical rounds after each configuration change had produced
549
- three consecutive matching responses.
550
-
551
- - **Precedence.** A self-hosted application whose destination is
552
- `{ "type": "public", "uri": "<host>/agent/*" }`, with one `bypass` policy
553
- including `everyone`, overrides the Worker-level application on matching
554
- paths only. Unauthenticated requests reach the Worker there. `/mcp`, `/`,
555
- and every unmatched path still get the Access refusal. A service token is
556
- still admitted on them with `ctx.access` from the Worker-level application.
557
- This confirms Cloudflare's documented hierarchy, and that Worker-level
558
- protection still covers the `workers.dev` hostname.
559
- - **`ctx.access` on a bypassed request is present, not absent.** Its `aud` is
560
- nonempty and is the _path_ application's AUD, not the Worker-level one.
561
- `getIdentity()` resolved to `undefined` for every credential tested: none, a
562
- valid service token, a `cta_`-style bearer token, and a forged
563
- `Cf-Access-Jwt-Assertion`. A service token on a protected path also gave
564
- `undefined` identity, carrying the Worker-level AUD. A human
565
- `CF_Authorization` cookie on a bypassed path was not tested, so what
566
- `getIdentity()` returns there is unknown. That is one more reason the agent
567
- route ignores Access context rather than relying on its contents.
568
- - **API shape.** A Worker destination is exclusive to one application. A
569
- second application with `{ "type": "worker", "worker_id" }` plus `uri` or
570
- `path` was refused with 409 `destination belongs to another application`. The
571
- bypass must be hostname-and-path, stored with domain `<host>/agent/*`, and it
572
- covers only that exact hostname.
573
- - **Propagation.** Creating or deleting each application took 15–19 seconds to
574
- settle across runs. While a deployment or application change was propagating,
575
- the edge returned transient 500 (`error code: 1104`) and 404 (`error code:
576
- 1042` or `1043`) responses. The recorded run has one such 404, on the
577
- Worker-level-only service-token control for `/agent/x`. Deleting the path
578
- application restored the 403 on `/agent/x`.
476
+ Access uses a hostname-and-path bypass application for `/agent/*`; the
477
+ Worker-level application protects other paths. A bypassed request can still
478
+ carry `ctx.access`, so agent routes ignore that context.
579
479
 
580
480
  | Raw request path | Access outcome | Worker pathname (+ search) |
581
481
  | -------------------------- | -------------- | -------------------------- |
@@ -607,139 +507,32 @@ leading `//`, and treats `%2f` as data, not a separator. The effective Worker
607
507
  pathname, `new URL(request.url).pathname`, is the raw request path with case,
608
508
  escapes, and slashes untouched, except that dot segments arrive resolved:
609
509
  `/mcp/../agent/x` arrives as `/agent/x`. Every path that
610
- resolves outside `/agent/*`, including every `/mcp` form, stayed protected. The
510
+ resolves outside `/agent/*`, including every `/mcp` form, remains protected. The
611
511
  inverse is the risk: Access bypasses paths
612
512
  that connecta must not treat as an agent pool. Connecta's router compares raw
613
513
  pathnames without decoding, case folding, or slash collapsing, so an `/agent/`
614
- route must keep that property. It should accept exactly
615
- `/agent/<pool>`, with `<pool>` byte-equal to a declared pool name. It should
616
- refuse an empty segment, a trailing slash, any extra segment, `%`, uppercase
617
- variants, and a leading `//`, all with the same 404. It must ignore
514
+ route keeps that property. It accepts exactly
515
+ `/agent/<pool>`, with `<pool>` byte-equal to a declared pool name. It refuses
516
+ an empty segment, a trailing slash, any extra segment, `%`, uppercase
517
+ variants, and a leading `//`, all with the same 404. It ignores
618
518
  `ctx.access` entirely, since a bypassed request still carries an AUD.
619
519
  [Agent endpoints](./auth.md#agent-endpoints) implement exactly this in
620
520
  `src/routes/agent-path.ts`, and `test/agent-endpoints.test.ts` replays every
621
521
  row of the table above. The hyphenated row stands for any two-part pool name
622
- such as `<agent>-<user>`. The pool names in the recorded paths were replaced
623
- with placeholders after the run; the edge behavior for that path shape was
624
- unaffected.
625
-
626
- To configure the bypass, keep the Worker-level application described in
627
- [Cloudflare Access on Workers](./auth.md#cloudflare-access-on-workers). Then,
628
- for each hostname agents use (the `workers.dev` hostname, each Custom Domain,
629
- or route host), add a self-hosted Access application. In the dashboard, set
630
- domain to that hostname and path to `agent/*`, with one policy whose action is
631
- **Bypass** and whose include rule is **Everyone**. Through the API, that is:
632
-
633
- ```json
634
- {
635
- "type": "self_hosted",
636
- "name": "connecta agents",
637
- "destinations": [{ "type": "public", "uri": "connecta.example.com/agent/*" }],
638
- "policies": [{ "name": "agents bypass", "decision": "bypass", "include": [{ "everyone": {} }] }]
639
- }
640
- ```
522
+ such as `<agent>-<user>`.
641
523
 
642
- Only the `workers.dev` hostname was probed. A Custom Domain application is the
643
- same documented hostname-and-path form, but it was not observed here. Do not
644
- rely on a fixed wait after a change. Changes settled in 15–19 seconds, yet one
645
- earlier round still saw a 403 on a bypassed path more than 30 seconds after
646
- the change. Test only after the positive and negative controls agree together
647
- for several consecutive rounds. The positive controls are bypassed paths
648
- reaching the Worker with the path AUD and a service token reaching a protected
649
- path with the Worker-level AUD; the negative controls are `/mcp` and `/`
650
- returning the Access refusal. The current script requires five such rounds
651
- after creating the bypass and three after other changes. The recorded run
652
- predates this: after the bypass it converged only the positive `/agent/x`
653
- control, then waited a fixed 90 seconds, and its negative controls were checked
654
- in the recorded rounds rather than before them.
655
-
656
- To repeat from a repository checkout, run
657
- `node scripts/probes/access-agent-bypass.mjs --profile <name> [output.json]`.
658
- `--profile` is required and has no default; every `cf` call passes it
659
- explicitly. Name the profile for the account that should host the probe; it
660
- needs
661
- Access application, policy, and service-token writes, plus Workers script
662
- writes. The script deploys and deletes the Worker with `cf`. `cf deploy` builds
663
- through Wrangler 4.136.0 or newer, so the probe installs Wrangler 4.145.0 into
664
- its temporary project. Wrangler needs no login. The script records only the
665
- count of existing Access applications, then names everything it creates
666
- `connecta-probe-<run>`. It force-deletes and verifies each resource in
667
- `finally`, then checks that the non-probe application ids are unchanged.
668
- Observations record statuses, outcome classes, pathnames, and which
669
- application's AUD matched, compared by digest inside the Worker. They contain no
670
- hostnames, account ids, tokens, cookies, or JWTs. `cf` delete commands prompt
671
- for confirmation and exit 0 without deleting when stdin is not a terminal, so
672
- the script passes `--force`. `cf deploy` printed `Deploy complete` and then
673
- kept running, so the script stops it after that line. The recorded run deployed
674
- with Wrangler 4.114.0 and used `cf` for Access. A standalone deploy and delete
675
- verified the `cf` deploy and delete path. The full probe has not been rerun on
676
- that path.
677
-
678
- Every subprocess (`cf`, `curl`, `npm`) runs through one runner. It enforces a
679
- deadline by killing the process group, keeps stdout and stderr in memory, and
680
- returns only facts: success, exit code, timeout, HTTP status, numeric API
681
- code, and an error slug from a fixed set (anything else is `unknown`). Commands
682
- that return data are parsed inside the runner against schemas that copy only
683
- whitelisted fields: ids, names, counts, booleans, and the Worker's `ctx.access`
684
- summary. Credentials stay in a holder that prints as `[secret]`. An unexpected
685
- shape, including `null` where a list belongs, is a failure, never an empty
686
- result. Errors are fixed diagnostics plus those facts. Teardown deletes every
687
- tracked id on its own. Then Access applications, service tokens, and Workers
688
- each get their own bounded discovery-and-delete pass by name, for resources
689
- whose create response was lost, so a failure in one family never skips
690
- another. Each discovered resource is deleted independently. Access policies are
691
- inline on their applications and go with them. A resource counts as gone only
692
- when a successful lookup omits it, and the final inventory verifies each family
693
- separately. Run
694
- `node scripts/probes/access-agent-bypass.mjs --self-test` to check the runner
695
- offline. It feeds canary secrets, suffixed slugs, malformed JSON, and a stalled
696
- process tree through it. It also runs teardown against a fake `cf` whose
697
- application listing always fails, after a lost token-create response, and
698
- checks that the token is still found and deleted. It also checks that a Worker
699
- that refuses deletion does not block another Worker's deletion. It runs no real
700
- `cf`, `curl`, or network command. This
701
- hardening and the joint convergence are verified offline only and have not run
702
- against Cloudflare.
524
+ For the operator procedure and Access application JSON, see
525
+ [Agent endpoints behind Cloudflare Access](./deploying.md#agent-endpoints-behind-cloudflare-access).
703
526
 
704
527
  ### Alchemy Worker deployment
705
528
 
706
- Two throwaway deployments on 2026-10-20 verified the Worker example's
707
- Alchemy path with alchemy 2.0.0-beta.83, effect 4.0.5, and Alchemy local
708
- state, then deleted every resource and confirmed by re-listing. Alchemy
709
- uploads the full Worker with `PUT /workers/scripts/<name>`, the endpoint an
710
- earlier full deploy had been suspected of losing `ctx.access` through.
711
-
712
- The first deployment ran a near copy of the stack with a temporary diagnostic
713
- route. With an Access service token in a non-identity policy, the Worker saw
714
- `ctx.access` carrying the Worker-level application's AUD and no identity, as
715
- expected for a service token. D1, the Worker Loader, R2, and secret bindings
716
- worked; `/agent/*` reached connecta through the bypass; `/ui/api/config`
717
- returned 403 `Cloudflare Access human identity required`; and
718
- `connecta doctor` passed with a `cta_` token issued inside the Worker. A code
719
- update through `alchemy deploy` kept the script tag, `ctx.access`, and D1 rows.
720
- Three consecutive probe rounds agreed.
721
-
722
- The second deployment ran the shipped `alchemy.run.ts` from the packed package
723
- in a separate project on Node 26, changing only resource names and the Access policy
724
- (a service token). The edge checks in the example's "Verify after deploying"
725
- held for three or more rounds after each deploy. `alchemy destroy` deleted the
726
- Worker and both Access applications and orphaned the retained D1 database; the
727
- next deploy adopted it by name with its rows. After deleting local state, a
728
- deploy reused the Worker with the same script tag, and `applicationId` took
729
- both Access applications over with unchanged AUDs and no duplicates. Removing
730
- `applicationId` afterwards planned an in-place update, and a further deploy
731
- planned no changes. The account API token needed only Workers Scripts Write,
732
- D1 Write, Access: Apps and Policies Write, and Access: Organizations, Identity
733
- Providers, and Groups Read.
734
-
735
529
  Four findings shape the operator rules. Alchemy local state under
736
530
  `.alchemy/state/` records secret binding values in plaintext. Alchemy sends no
737
531
  `keep_bindings`, so each deploy deletes secrets set outside the stack.
738
532
  `alchemy dev` does not provide `ctx.access` to a plain fetch Worker, so local
739
533
  development stays on `wrangler dev` and its `access.dev` identity. D1 needs
740
534
  `RemovalPolicy.retain()` so `alchemy destroy` cannot delete a deployment's
741
- state. Not verified: `Cloudflare.state()` remote state, custom domains, and
742
- taking over a Worker first deployed with Wrangler.
535
+ state.
743
536
 
744
537
  npm strips `.gitignore` from the package, so the copied Worker example's
745
538
  ignore rules ship as `examples/worker/gitignore`, renamed by the README's copy
@@ -760,15 +553,8 @@ activity table beside it. `src/storage/sql.ts` owns the Promise edges.
760
553
  Each store creates its tables on first use. `memoryStorage()` is the default
761
554
  and the test double. Workers KV and the 0.28 JSON file store are gone: KV is
762
555
  eventually consistent and cannot compare-and-set, and the file store rewrote
763
- its whole file on every write. Each has a one-shot copy into its replacement,
764
- `copyKvToD1` on `/d1` and `connecta migrate-state` on Node. Both copy live
765
- entries verbatim, because physical keys did not change, and both keep any key
766
- the target already holds. `copyKvToD1` reports counts per key family
767
- (`familyOfKey` in `src/storage/keys.ts`), never a key. It resumes through a
768
- source-bound, atomically claimed token, because Workers KV's own list cursor
769
- can spell a key. Copy under maintenance after KV stabilizes, verify hashes
770
- and expiries, and mark cutover before reopening traffic; the permanent D1
771
- marker refuses stale copies afterward.
556
+ its whole file on every write. Their one-shot copies were removed; deployments with old state must finish
557
+ conversion on 0.37.1 before upgrading. See [Retired layout migrations](./migration-retired-layouts.md).
772
558
 
773
559
  `KVStorage` is `get`/`set`/`delete`/`list(prefix)`/`compareAndSet`, all
774
560
  required, and `createConnecta` refuses storage missing one (INV-11).
@@ -808,12 +594,10 @@ Keys and list prefixes must not contain U+0000 (NUL). D1, SQLite, and memory
808
594
  storage reject them with `TypeError` before accessing storage: Node 22's
809
595
  `node:sqlite` truncates TEXT results at NUL. Builders reject NUL in unencoded
810
596
  components.
811
- State-file import validates all keys before writing. The `connecta_kv` table
812
- keeps its existing TEXT keys, including compatibility with the 0.28 schema.
813
- Storage writes no log lines. A refused import names the file and an entry's
814
- position, never a key, a value, or the JSON parser's account, which quotes
815
- the file (INV-6). Database failures report only a fixed import step and an
816
- entry's position, including errors raised by target-database triggers.
597
+ The `connecta_kv` table keeps its existing TEXT keys, including compatibility
598
+ with the 0.28 schema. Storage writes no log lines. JSON state-file import is
599
+ retired; deployments needing it must use the 0.37.1 bridge release before
600
+ upgrading. See [Retired layout migrations](./migration-retired-layouts.md).
817
601
 
818
602
  Remote MCP catalogs use the SDK `ResponseCacheStore` over this same SQL KV.
819
603
  The `response-cache:v1:` family binds connector id, a hash of its configured
@@ -835,6 +619,142 @@ Only complete, intake-redacted tools/list, resources/list and
835
619
  resources/templates/list successes are stored. Resource payloads are not cached. SDK default
836
620
  in-memory caching is replaced, so raw catalogs never enter it.
837
621
 
622
+ Stored catalog and negotiation keys use deployment-keyed partitions. A remote
623
+ connector's whole cache configuration and negotiation input enter a stored key
624
+ only through HMAC-SHA-256 (`src/storage-digest.ts`) under a key that never
625
+ enters storage: every configured URL in full (the endpoint, a client metadata
626
+ document, a static client's issuer and the redirect URI), static auth header
627
+ values, resource headers, and a static OAuth client's secret. The
628
+ operator-managed or request-resolved credential (joined with the forwarded
629
+ principal), a custom connector's credential values, and the OAuth token
630
+ response that fences a listing are keyed the same way. The key is the vault's
631
+ `storageDigestKey()`, which `encryptedCredentialVault()` derives with HKDF from
632
+ its credential key. Without a vault, or with a custom vault that lacks the
633
+ method, a random key held only in the process is used: the partitions stay
634
+ exact, but another process or isolate addresses different entries and fills
635
+ its own cache, so cross-isolate reuse of remote catalogs, verdicts and health
636
+ records needs a vault. A different URL, header, credential, token response or
637
+ epoch addresses a different entry at once, including a value changed by
638
+ redeploy, and an earlier entry is never served under the new value; rotating
639
+ back to an earlier value within its TTL finds its entries again. A new
640
+ deployment key addresses new entries. Entries written by earlier releases are
641
+ not read and expire on their own TTLs (catalogs within `catalogMaxTtlSeconds`,
642
+ refresh baselines within 48 hours, verdicts within six hours), so each remote
643
+ connector's partitions refill once after the upgrade with one handshake and
644
+ listing.
645
+
646
+ A remote connector's `listTools` without a live connection in its request
647
+ reads this cache before connecting. It derives the same partition a connect
648
+ binds (the admitted principal, the caller's credential digest, or the owner's
649
+ OAuth epoch and token digest) from storage reads only, then makes the SDK's
650
+ two-probe read with the pre-connect empty server identity: the private slot,
651
+ then a shared connector's public slot, served only when written public. OAuth
652
+ grants are read by a non-mutating snapshot: no plaintext grant is sealed in
653
+ place, no epoch is bound, nothing is refreshed. Every identity check during
654
+ cache acceptance reads the grant again and refuses the entry unless the epoch
655
+ its partition was derived from and the token digest both still hold. A missing
656
+ or expired generation is a miss at every check and is never claimed or
657
+ recreated. An unexpired complete entry is returned with no transport,
658
+ handshake, session cleanup or storage write. A miss, an expired entry, an
659
+ explicit `ttlMs: 0`, an invalidated generation, a credential, token or epoch
660
+ change, an operator-disconnected or token-less grant, a cache read failure,
661
+ and a downstream whose stored negotiation verdict is modern all connect as
662
+ before. Modern downstreams connect without a handshake and read the cache
663
+ through the SDK. The connect reuses the credential and negotiation verdict that
664
+ read resolved. `list_changed` arrives only over a live connection, so an
665
+ unconnected catalog changes at its TTL or on invalidation.
666
+
667
+ A catalog served from this cache, before or after connecting, does not
668
+ validate downstream credential health. An expired or vendor-revoked credential
669
+ can still expose cached tools, and discovery reports no auth-related
670
+ `catalogErrors` entry for it until the cache misses. Tool calls and resource
671
+ reads reach the downstream and report auth failures with their handoffs. A
672
+ status probe connects: a legacy downstream answers a handshake, while a modern
673
+ downstream with a stored verdict connects without a request. A credential test
674
+ always sends a live `tools/list` that bypasses the cache in both directions.
675
+
676
+ In `"auto"` mode a remote connector stores its negotiation verdict, data only,
677
+ in the `negotiation` key family, partitioned by connector configuration,
678
+ credential and OAuth epoch, with secret-bearing inputs keyed as above. A later connect reuses it instead of probing with
679
+ `server/discover`. A modern verdict lasts five minutes. A legacy verdict lasts
680
+ six hours, because a server that adds the modern era still answers the legacy
681
+ handshake. Two signals end a verdict early:
682
+
683
+ - A legacy-verdict handshake refused with a 4xx other than 401, 403 or 429 (a
684
+ modern-only server answers 400 with its supported versions) is negotiated
685
+ again at once in the same connect. The new verdict replaces the old.
686
+ - On a modern connection (adopted from the verdict or just negotiated), an
687
+ HTTP 400 is read through a bounded clone of its body, as the transport's
688
+ backward-compatibility rule requires. A modern server also answers 400 with
689
+ `HeaderMismatch` (-32020) and `MissingRequiredClientCapability` (-32021),
690
+ which concern the request alone and keep the verdict and the connection.
691
+ `UnsupportedProtocolVersion` (-32022) keeps them only while its
692
+ `data.supported` list (at most 64 strings) still names the request's
693
+ `MCP-Protocol-Version`; a missing, malformed or other list makes the verdict
694
+ unusable. Any other body, empty or not a recognized modern error (a legacy
695
+ server's sessionless refusal, a gateway's page), is a legacy refusal. Either
696
+ way the verdict is deleted and that client retired. The refused request
697
+ fails and is never repeated. The next connect, in the same request or a
698
+ later one, probes again. The clone read ends with the call, the request or
699
+ the connection scope: an abort cancels it, cancels the unread original and
700
+ takes no evidence (INV-7).
701
+
702
+ An upgrade to a server that still answers the legacy handshake is therefore
703
+ adopted within six hours. Losing the legacy handshake costs one refused
704
+ request, and a downgrade costs the one request that met the refusal.
705
+
706
+ **Discovery health.** An unscoped search (`search_tools` or `connecta.search`
707
+ without `connector`) probes each connector for at most 5,000 ms, or
708
+ `discovery.probeTimeoutMs` when that is shorter, and its probes end within a
709
+ 9,000 ms discovery budget. A tenth of the budget, at most 500 ms, is kept for
710
+ recording outcomes: a probe that starts late gets only what remains before
711
+ that slice, and one that cannot start reports `timeout`. Health reads, claims
712
+ and writes start only while budget remains and the caller is still waiting;
713
+ health is otherwise unknown. Once started, each is awaited to its end before
714
+ the search returns, never raced or detached, since a storage write cannot be
715
+ withdrawn once sent. The budget bounds when health work starts, not when it
716
+ finishes, so slow storage can carry a search past it by one operation's
717
+ latency. A scoped search,
718
+ `connecta.describe`, native `tools/list` and tool calls keep the full
719
+ `discovery.probeTimeoutMs`. A probe that times out on its own deadline, or a
720
+ retryable failure that itself took at least one second or named its own wait,
721
+ writes a record in the `connector-health:v1:` family: the connecta
722
+ classification code, observation time, retry time (60 seconds later, or the
723
+ downstream's shorter named wait, at least one second), and the connector's
724
+ catalog invalidation generation. Each probe's time is its own, measured as it
725
+ settles. Records hold no downstream text, tools, arguments or credentials.
726
+ Each is keyed by connector id and the deployment-keyed digest (as for the
727
+ catalog cache above) of the catalog owner partition (principal, subject,
728
+ caller identity and pool, origin and public URL), the admitted endpoint (plain
729
+ `/mcp`, `/mcp/<pool>` or `/agent/<pool>`), the connector's described
730
+ configuration, whose endpoint keeps its path, and the package version. One owner's
731
+ or endpoint's record never skips another's search. A change `describe()`
732
+ cannot see, such as a static header rotated in deployment code, takes effect
733
+ on redeploy; a record written before it keeps skipping for at most its retry
734
+ time, about 60 seconds. Until its retry time an unscoped search skips that connector
735
+ and reports it in `catalogErrors` with the recorded code, `retryable: true`,
736
+ `retryAfterMs`, and a fixed message that names the scoped search to try it
737
+ now; no tools from the failed walk are ever served. A scoped search still
738
+ probes and settles the record. A record whose generation no longer matches
739
+ lapses, so a vault credential change, OAuth connect or disconnect, or operator
740
+ invalidation clears every partition at once. A success, or an auth or
741
+ permission failure that shows the connector answers, clears the record this
742
+ request saw. Caller cancellation, a probe the budget cut short, a fast
743
+ failure, and a non-retryable failure record nothing. After its retry time a
744
+ record lingers for 60 seconds: the request that dispatches the next probe
745
+ claims it by compare-and-set, with a lease to an absolute time, the end of
746
+ that request's discovery budget plus two seconds, so a slow claim write cannot
747
+ shorten it; the others keep skipping until it settles or the lease lapses. Health reads are one `getMany` per unscoped search, made
748
+ once per request; a health storage failure is logged and never fails
749
+ discovery.
750
+
751
+ A healthy connector whose complete catalog always takes longer than 5 seconds
752
+ to list can therefore stay out of unscoped discovery: each unscoped probe
753
+ times out and records it. A scoped search, or any operation with the longer
754
+ `discovery.probeTimeoutMs` deadline that completes the listing (describe, a
755
+ tool call), warms a reusable complete catalog that later unscoped searches
756
+ read without the slow listing, and a success clears the record.
757
+
838
758
  `discovery.catalogTtlSeconds` (300 by default) is the fallback for legacy
839
759
  listings without `ttlMs`. Positive hints are bounded by
840
760
  `catalogMinTtlSeconds` (0) and `catalogMaxTtlSeconds` (86400, also the SDK
@@ -885,7 +805,15 @@ activity write. The first complete catalog is a baseline.
885
805
  Hash-only baselines publish bounded chunks before a manifest. They store the
886
806
  catalog digest and hashes of each tool name and fact, so added, removed and changed counts require no persisted tool names or
887
807
  schemas. They use the exact host auth/config cache partition, survive catalog
888
- expiry and invalidation, and expire after 48 hours. There is no connection-wide
808
+ expiry and invalidation, and expire 48 hours after they are written. A refresh
809
+ reads the manifest first. When its digest matches a baseline written within the
810
+ last hour, the refresh writes nothing; an older matching baseline is rewritten
811
+ with fresh chunks, so every refresh leaves a baseline for at least 47 hours.
812
+ A manifest's lifetime is never extended without new chunks, so its chunks
813
+ always outlive it. A changed digest writes the new baseline and reports drift
814
+ against the previous one. An unchanged zero-TTL listing therefore writes no
815
+ baseline; it still lists live every time and deletes its cache slots, since a
816
+ concurrent request may have published an older reusable catalog there. There is no connection-wide
889
817
  fallback baseline across partitions. Private catalog events use the admitted
890
818
  principal actor basis even for shared-auth connectors; activity disclosure
891
819
  requires that same owner and the recorded pool's grants. Ownerless private
@@ -1283,9 +1211,8 @@ parse as text, so connecta reads downstream bodies as bytes
1283
1211
  (`src/byte-read-response.ts`), `ctx.oauth.fetch()` answers included. The
1284
1212
  operator page may show catalog metadata the operator loaded (descriptions,
1285
1213
  schemas), but withholds a tool name outside the grammar as it does in records.
1286
- `test/operator-record-sources.node.test.ts` is a secondary lint over every other
1287
- log call in `src/`. Fix the sink, not the source: a filter at each source missed
1288
- the next one. Recorded HTTP status facts survive the bare write-401 fallback,
1214
+ Fix the sink, not the source: a filter at each source missed the next one.
1215
+ Recorded HTTP status facts survive the bare write-401 fallback,
1289
1216
  Google's uncertain-write wording, and oversized-response mapping; see
1290
1217
  [Read failure records](./operating.md#read-failure-records) for their operator
1291
1218
  meaning. These facts do not change agent error fields or write outcomes.
@@ -1361,7 +1288,11 @@ switched by the environment, and keep their entries under 30 lines.
1361
1288
  A maintained provider is one `defineProvider()` call (`src/provider.ts`): a
1362
1289
  name, title, `kind: "api"`, maintained skill, closed `options` shape and a
1363
1290
  synchronous `create`. Common values are checked before construction and the
1364
- factory stamps its provider name onto `describe().source.provider`. API
1291
+ factory stamps its provider name onto `describe().source.provider`. The kit's
1292
+ `defineApiProvider` also takes optional `searchSynonyms` groups (single
1293
+ lowercase words, at most 32 groups of 2–8), checks them at definition and
1294
+ attaches them to each connector for
1295
+ [lexical discovery](./meta-tools.md#lexical-discovery). API
1365
1296
  implementations and their contract evidence remain vendored. Hosted MCP servers
1366
1297
  use generic `remoteMcp()` entries; provider reviews and hosted defaults are
1367
1298
  removed. Legacy hosted selectors fail at construction with the generic migration
@@ -1456,9 +1387,7 @@ acknowledgement, then reconciles vendor state with exactly one physical send.
1456
1387
  Focused provider suites retain MIME/draft restrictions, revision conflicts,
1457
1388
  CCB routing, Planning Center product exclusions and deployment pins. Native
1458
1389
  HTTP framing, cursor and request-lifetime fault probes remain in the HTTP suites.
1459
- The catalog-wide status sweep and its argument roster, catalog/ranking copies
1460
- and unused paging strategy tests are retired under #1072. Provider workflows
1461
- belong in the existing shared outcomes; a new tool requires no failure-matrix row.
1390
+ Provider workflows belong in the existing shared outcomes.
1462
1391
  No legacy REST tool adapter remains.
1463
1392
  Large documents opt into generator options that drop restated operation ids
1464
1393
  and plain path parameters and cap one operation's details (`opBudget`).
@@ -1732,8 +1661,7 @@ path, or fields a schema leaves undeclared; such paths still apply at
1732
1661
  runtime).
1733
1662
 
1734
1663
  `test/fixtures/value-safety.ts` is the shared harness each vendor's
1735
- `provider.safety.node.test.ts` runs; `reviewProblems` is pure, and
1736
- `test/value-safety-harness.node.test.ts` proves wrong paths fail it. It fails
1664
+ `provider.safety.node.test.ts` runs; `reviewProblems` is pure. It fails
1737
1665
  on candidates from another pin, format, or option set; an unreviewed
1738
1666
  candidate; a verdict for an operation the index lacks; a table path the stamp
1739
1667
  lacks (unresolved, or edited since `providers:spec`); a flagged field no path,
@@ -1761,12 +1689,6 @@ and reports only added, removed and changed counts in the caller's partition.
1761
1689
 
1762
1690
  #### Behavioral evidence (contributor)
1763
1691
 
1764
- The characterization snapshot engine, provider JSON snapshots, source scheduler
1765
- and its self-tests are retired under
1766
- [#1072](https://github.com/zackbart/connecta/issues/1072). Provider changes no
1767
- longer require golden enrollment, snapshot updates, byte equivalence or nightly
1768
- capture determinism. Production invariants remain unchanged.
1769
-
1770
1692
  Use the existing stateful fixture vendors and published-edge deployment adapters:
1771
1693
 
1772
1694
  - `pnpm run eval:outcomes` drives representative joins, retained paging, endpoint
@@ -1785,7 +1707,10 @@ Use the existing stateful fixture vendors and published-edge deployment adapters
1785
1707
  Node/SQLite/QuickJS and native Workers/D1. It covers transcript-too-large
1786
1708
  recovery, empty intermediate pages, selected fields, withheld photo/credential
1787
1709
  values and shared-vault reuse after application reconstruction. No-op,
1788
- fabricated-read and erased-delivery controls must fail.
1710
+ fabricated-read and erased-delivery controls must fail. Its fake, tasks and
1711
+ deployment wiring live in `src/providers/granola/eval/`, unshipped;
1712
+ `providers:generate` registers a provider's `eval/` wiring with the harness
1713
+ (`eval/README.md`, provider-local eval wiring).
1789
1714
  - `test/identity-write-journey.node.test.ts` uses the existing Node deployment
1790
1715
  adapter, stateful MCP fake, SQLite vault and QuickJS to grade admitted human
1791
1716
  and machine identities, pool isolation, fail-closed discovery, endpoint and
@@ -1794,14 +1719,6 @@ Use the existing stateful fixture vendors and published-edge deployment adapters
1794
1719
  returned private values and credential echoes; positive record facts must
1795
1720
  still arrive. An explicitly writable pool also exercises reopening a
1796
1721
  read-only deployment without bypassing connector policy.
1797
- - `test/cancellation-journey.node.test.ts` observes a queued client's departure
1798
- without another vendor send, and an active client's departure after the
1799
- vendor commits. The vendor must see its held response socket close before
1800
- releasing it; a later MCP read recovers the committed intent exactly once.
1801
- `test/cancellation-faults.test.ts` keeps signal-less token-resolution,
1802
- warmed-credential digest, stalled body-cleanup and abort/grant races on Node
1803
- and workerd. These replace the universal Promise-method/seam/owner matrix
1804
- and its enrollment self-test, plus migration-only runtime wrapper checks.
1805
1722
  - `test/browser/operator-flows.spec.ts` serves the actual operator shell and
1806
1723
  routes over TCP with file-backed SQLite. Sign-in, Settings provenance, OAuth
1807
1724
  connect/disconnect and credential save/test/replace/remove survive closing and
@@ -1835,17 +1752,10 @@ Use the existing stateful fixture vendors and published-edge deployment adapters
1835
1752
  after HTML, malformed JSON and oversized acknowledgements with one physical
1836
1753
  send. It distinguishes Stripe's reviewed pre-execution 401 refusal, and checks
1837
1754
  read failure facts, secrecy and recovery through direct and QuickJS calls.
1838
- `test/provider-failure-sweep.test.ts` retains only concrete auxiliary, composite,
1839
- in-flight and untracked-call fault probes; no provider/tool argument roster or
1840
- catalog/finalizer agreement sweep remains. Existing
1841
- `test/sent-secrets.test.ts`, `test/sent-secret-output-boundary.test.ts`,
1842
- `test/request-secret-redaction.test.ts` and `test/catalog-secret-redaction.test.ts`
1843
- cover secrecy before projection, truncation, caching, paging and diagnostics.
1844
- - `test/write-evidence.node.test.ts`, `test/remote-mcp-attempts.node.test.ts` and
1845
- `test/oauth-credential-attempts.node.test.ts` inspect physical socket sends.
1846
- Native transport parity, OAuth and write-provenance suites retain their
1847
- workerd coverage. The shared throwaway RSA key in `test/fixtures/rsa.ts` is
1848
- used by write-provenance and unknown-write-advice fixtures.
1755
+ Existing `test/sent-secrets.test.ts` covers secrecy before projection,
1756
+ truncation, caching, paging and diagnostics.
1757
+ - The shared throwaway RSA key in `test/fixtures/rsa.ts` is used by
1758
+ write-provenance and unknown-write-advice fixtures.
1849
1759
  - `test/executor-requests.test.ts` runs overlapping `execute_code` programs
1850
1760
  as separate workerd requests through the real Worker Loader executor, with
1851
1761
  real TCP host calls that read their own request's signal. Queued programs
@@ -1860,23 +1770,21 @@ Use the existing stateful fixture vendors and published-edge deployment adapters
1860
1770
  `test/request-context.node.test.ts` pins the same wake order and async context
1861
1771
  on Node for executor and call admission and QuickJS host calls, and gives back
1862
1772
  grants that shutdown overtook.
1863
- - The request, catalog, auxiliary-call and renewal lifetime
1864
- suites, plus `test/auth-admission-binding.test.ts`, exercise cancellation,
1865
- held native work and garbage collection. These test actual request ownership;
1866
- the retired capture scheduler's source guards were only harness constraints.
1773
+ - `test/request-context.node.test.ts` and `test/auth-admission-binding.test.ts`
1774
+ exercise cancellation, held native work and garbage collection. These test
1775
+ actual request ownership.
1867
1776
 
1868
1777
  The remaining probes defend distinct reachable failures: auth admission and
1869
1778
  native scheduler ownership, grant/callback fencing and refresh coordination,
1870
1779
  SDK replay/redirect/SSE attempts, nested write provenance and refusal evidence,
1871
1780
  unawaited program drain and failed-program accounting, catalog drift and paging
1872
1781
  revocation, and hostile serializers, structural echoes, truncation and persisted
1873
- pages. Broad scripted-executor policy/privacy cross-products and repeated
1874
- post-401 result/error-shape tables are retired. Protocol claims link executable
1782
+ pages. Protocol claims link executable
1875
1783
  evidence files in `spec/coverage.json`; the invariant contracts above are defended
1876
1784
  by the actual behavior assertions in these journeys and focused probes.
1877
1785
  The required CI gate includes build/declarations, Node and Workers probes,
1878
1786
  outcomes, browser flows, installed consumers and Docker, audits and real Eve
1879
- compatibility. Further contributor cleanup is tracked in #1072.
1787
+ compatibility.
1880
1788
 
1881
1789
  Add a compact fault journey only for a meaningful outcome those suites cannot
1882
1790
  observe. Reuse their fixtures and adapters; do not reintroduce a snapshot
@@ -1904,8 +1812,7 @@ behind the same redirect wrapper and a same-origin guard.
1904
1812
  client replacement, transport confinement and secrecy on Node, and a real
1905
1813
  service binding on workerd.
1906
1814
  `test/google-service-account-token.test.ts` exercises signing/cache behavior on
1907
- Node and workerd; `test/google-service-account-remote.node.test.ts` checks
1908
- real-socket token/MCP requests, invalidation and one physical attempt per write.
1815
+ Node.
1909
1816
  Operator setup lives in [Google hosted MCP](./auth.md#google-service-account-tokens-and-hosted-mcp).
1910
1817
 
1911
1818
  `createConnecta` takes closed typed `ui`, `vault`, and `activity`
@@ -2070,10 +1977,10 @@ storage is the root's namespaced to its principal.
2070
1977
  | 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. |
2071
1978
  | 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`. |
2072
1979
  | `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. Public executor methods stay Promise-shaped; registered built-in execution composes directly as an Effect under the signal and `execute.watchdogMs`. Queued acquisition retains its Promise request handoff. Each guest host call is a fiber of its own. |
2073
- | 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. |
1980
+ | 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; unscoped search adds the discovery budget and health record (`discovery-health.ts`). |
2074
1981
  | 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. |
2075
- | 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. |
2076
- | Downstream OAuth (`auth/downstream-oauth.ts`, `auth/static-oauth.ts`) | A refresh flight is a Deferred. Before the first durable coordinator write (claim CAS) is submitted, call/request closure stops preparation without a CAS, token request or state change. Submission transfers renewal to the storage coordinator under its own 35-second preparation deadline and 20-second token-request/body deadline, followed by the fenced rotation commit through the runtime deferred-work hook. A closed caller promptly fails without consuming the result, marking its ledger renewed or sending/resending a resource request. |
1982
+ | Remote MCP (`connectors/remote-mcp.ts`, `connectors/remote-mcp/`) | 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. |
1983
+ | Downstream OAuth (`auth/downstream-oauth*`, `auth/static-oauth.ts`) | A refresh flight is a Deferred. Before the first durable coordinator write (claim CAS) is submitted, call/request closure stops preparation without a CAS, token request or state change. Submission transfers renewal to the storage coordinator under its own 35-second preparation deadline and 20-second token-request/body deadline, followed by the fenced rotation commit through the runtime deferred-work hook. A closed caller promptly fails without consuming the result, marking its ledger renewed or sending/resending a resource request. |
2077
1984
  | 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. |
2078
1985
  | 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`. |
2079
1986
 
@@ -2186,10 +2093,8 @@ request fixture and native-output suites supply runtime parity evidence.
2186
2093
 
2187
2094
  ### Native SQL storage (contributor)
2188
2095
 
2189
- `src/storage/sql-native.ts` composes KV, activity schema upgrades, retention,
2190
- and bounded SQL copies as Effects. `src/storage/d1-copy.ts` composes the
2191
- Workers KV copy, source-bound resume claims, verification, cutover guard, and
2192
- recovery. Public factories and copy functions retain their Promise contracts;
2096
+ `src/storage/sql-native.ts` composes KV, activity schema upgrades, and retention
2097
+ as Effects. Public factories retain their Promise contracts;
2193
2098
  custom Promise SQL drivers retain the fallback lift.
2194
2099
 
2195
2100
  `src/storage/sql-driver.ts` constructs a public Effect 4.0.5 `SqlClient`
@@ -2208,17 +2113,8 @@ is the existing exact `effect` pin. Connecta does not adopt `@effect/sql-d1`:
2208
2113
  its 4.0.5 batch API drops metadata and its binding contract is wider than
2209
2114
  Connecta's published structural interface.
2210
2115
 
2211
- The D.2 architecture allowance was measured against `0e314247` using cached
2212
- official `node:22-slim` (Node 22.23.4, zlib 1.3.1-e00f703), no network, and
2213
- read-only bundle artifacts: `/d1` grew from 62,516 to 88,305 bytes gzip
2214
- (+25,789); the Worker example grew from 729,847 to 756,375 (+26,528).
2215
- Final typed-driver failure normalization measures 88,319 and 756,377 bytes
2216
- on that same official runtime. Executing real services and statement operations requires those runtime
2217
- semantics; the earlier identity-compiler facade did not provide them. The
2218
- explicit allowance changes only `/d1`'s cap from 62,542 to 90,000 bytes and
2219
- the Worker example's cap from 743,000 to 760,000. Historical baselines and
2220
- all other caps stay fixed. Final integration must remeasure the combined
2221
- implementation and satisfy the same compatibility and privacy contracts.
2116
+ Bundle caps and baselines are authoritative in
2117
+ [`scripts/bundle-budget.json`](https://github.com/zackbart/connecta/blob/main/scripts/bundle-budget.json).
2222
2118
 
2223
2119
  The platform connection capability contains no request state. D1 prepares in
2224
2120
  the current request and retains no prepared handles or promises. SQLite keeps
@@ -2245,14 +2141,16 @@ value, matching that fallback. Interruption remains interruption. The lookup
2245
2141
  module imports no SQL or platform implementation and does not change the
2246
2142
  published storage interfaces.
2247
2143
 
2248
- The synchronous state-file parser, validation, file read, and one atomic
2249
- SQLite import remain native synchronous leaves because `importStateFile` is a
2250
- published synchronous API. `openSqlite` retains file modes, WAL, busy timeout,
2144
+ `NativeStorage.getMany` reads several keys in one statement, answering by
2145
+ position exactly as one `get` per key would; the SQL drivers share it, a
2146
+ replaced `get` answers each key itself, and other stores fall back to one `get`
2147
+ at a time. Catalog reads fetch up to eight chunks per round trip with it.
2148
+
2149
+ `openSqlite` retains file modes, WAL, busy timeout,
2251
2150
  and database ownership. `test/sql-effect-contract.ts` retains affected-row,
2252
2151
  50-claim, typed-failure, cancellation, and tracing probes on SQLite and native
2253
- D1. `test/kv-to-d1.test.ts` also reuses stores across separate workerd requests;
2254
- the existing SQL contracts prove rollback, database-clock TTL, encodings,
2255
- activity upgrades, and copy recovery.
2152
+ D1. The existing SQL contracts prove rollback, database-clock TTL, encodings,
2153
+ and activity upgrades.
2256
2154
 
2257
2155
  ### Internal provider HTTP failures (contributor)
2258
2156
 
@@ -2293,7 +2191,7 @@ Stripe declares HTTP 409 as a conflict, alongside its fact-driven 429
2293
2191
  classification to an oversized 424 reply, preserving the dependency-failure
2294
2192
  meaning when the bounded body cannot be read. The original ceiling diagnostic
2295
2193
  and answered status remain; the core still decides write outcomes. It does not
2296
- apply to 401 or alter Q8. A fixed read-phase fact distinguishes failed body
2194
+ apply to 401 or change write-outcome settlement. A fixed read-phase fact distinguishes failed body
2297
2195
  reads from malformed JSON only for vendors declaring that wording; raw
2298
2196
  exceptions never enter diagnostics. Other providers retain their mapping and
2299
2197
  wording.
@@ -2310,14 +2208,13 @@ The mapping records each request's own served provenance and raw 401 facts.
2310
2208
  reservation's answer even for a read-classified tool issuing a physical write.
2311
2209
  An accepted write's blocked retry retains its original HTTP status; a slot
2312
2210
  occupied by an auxiliary read instead names the separate write step needed.
2313
- Structured resend advice reaches output only when the core permits it. Generic
2314
- unknown writes retain the core's diagnostic wait; Q8 removes it. The mapping
2315
- adds no Promise entry or lifetime owner. `provider-failure-sweep.test.ts`,
2316
- `guarded-attempts.node.test.ts`, `call-services.test.ts` and the native
2211
+ Structured resend advice reaches output only when the core permits it. Unknown
2212
+ write outcomes are never retryable; a generic one keeps the core's diagnostic
2213
+ `retryAfterMs`, and one settled by a 401 drops it. The mapping
2214
+ adds no Promise entry or lifetime owner. `call-services.test.ts` and the native
2317
2215
  transport suites exercise these contracts through invocation, physical sockets
2318
2216
  and request closure. `http-kit.test.ts` retains focused numeric/text fact,
2319
- request-equivalence and bulk-capacity fault probes. The migration-era
2320
- kit/core verdict comparison is retired under #1072. Maintained API providers use the kit through their
2217
+ request-equivalence and bulk-capacity fault probes. Maintained API providers use the kit through their
2321
2218
  explicit provider subpaths; it stays off the root import graph and published
2322
2219
  declarations expose Promises.
2323
2220
 
@@ -2328,8 +2225,7 @@ declarations expose Promises.
2328
2225
  are Promises. A connector is called with `Effect.tryPromise`, never asked to
2329
2226
  return an Effect.
2330
2227
  - **Inbound authorization.** `authorize` in `src/routes/shared.ts` ships in
2331
- the declarations and in the `./activity` bundle; converting it cost 32 KB
2332
- gzip there for nothing a caller could see.
2228
+ the declarations and in the `./activity` bundle.
2333
2229
  - **Pure synchronous code.** Ranking, schema rendering, classification,
2334
2230
  `validateToolInput`, the host-call budget, the emit collector, failure
2335
2231
  framing, and result shaping. The microtask scheduler never yields to I/O, so
@@ -2409,6 +2305,93 @@ within five minutes of expiry in the same auth partition. Partial walks and
2409
2305
  auth or permission failures refuse fallback; resources require unexpired
2410
2306
  inventories, and stale fallback cannot authorize post-entry auth recovery.
2411
2307
 
2308
+ **Repeated reads share one request's memo.** `src/runtime/request-reads.ts`
2309
+ gives each request owner a memo of storage reads, emptied when the request
2310
+ closes (INV-7). It holds plain strings keyed by the physical key in the
2311
+ underlying store, so namespaced views of one partition share an entry and two
2312
+ principals' partitions never do. A read consults it only where the call site
2313
+ opts in and states why staleness within one request is safe; every other read
2314
+ goes to storage.
2315
+
2316
+ - Every write through the SQL drivers, `memoryStorage()` or a namespaced view
2317
+ over them drops that key from every open memo in the same isolate, before
2318
+ submission and again on settlement. A read that such a write overlapped is
2319
+ not recorded.
2320
+ - Only the latest read of a key that the request started records. An earlier
2321
+ read that settles later never replaces a newer fresh observation.
2322
+ - A fresh read records what it saw, so a memoized reader is never older than
2323
+ its request's latest fresh read of the key.
2324
+ - A custom store, or a built-in one with a replaced method, is never memoized.
2325
+ - A runtime without `WeakRef` and `FinalizationRegistry` memoizes nothing:
2326
+ workerd before compatibility date 2025-05-05 without `enable_weak_ref` reads
2327
+ storage every time.
2328
+
2329
+ The memo cannot see another isolate, another process sharing the database, or
2330
+ an entry expiring. After the first read, a memoized key can lag such a change
2331
+ for the rest of the request. So nothing that gates authority, sends a
2332
+ credential, or decides a write is memoized.
2333
+
2334
+ Memoized:
2335
+
2336
+ - the negotiation verdict, which only orders handshake versions;
2337
+ - catalog generation reads that only locate an entry, accepted or refused by a
2338
+ fresh fence;
2339
+ - the credential reads behind connector status, drift and the operator UI card,
2340
+ which only label a view. Only the built-in vault with its original `getAll`
2341
+ and `metadata` shares a read; a custom vault, a subclass, or an instance
2342
+ that overrides either method is called itself for every read, dispatch
2343
+ included.
2344
+
2345
+ Always fresh:
2346
+
2347
+ - every OAuth grant read, including the token and epoch check behind each SDK
2348
+ message (a disconnect need not revoke the vendor token), `peekGrant` and
2349
+ every cache-acceptance identity check, compare-and-set loops, refresh claims
2350
+ and leases, resets and disconnects;
2351
+ - the catalog generation fence before a cached entry is served or published;
2352
+ - the credential a call sends.
2353
+
2354
+ `test/request-reads.node.test.ts` holds the memo's contract, including a
2355
+ disconnect written by another isolate between paginated `tools/list` pages.
2356
+
2357
+ ### Request budgets (contributor)
2358
+
2359
+ `test/request-reads.node.test.ts` also guards the common request shapes the way
2360
+ bundle caps guard size. Each case counts downstream HTTP requests, D1 round
2361
+ trips (`all`, `run` or `batch`) and SQL statements separately, since a batch is
2362
+ one round trip but several statements. The fixture is production-shaped: D1
2363
+ over `node:sqlite`, a sealed vault holding the API credential and the OAuth
2364
+ grant, Cloudflare Access with `accessTokens()`, a D1 activity sink settled
2365
+ through `waitUntil`, the operator UI and QuickJS standing in for Dynamic Worker
2366
+ startup. "Cold isolate" is a new deployment instance over the populated
2367
+ database. The ceilings, measured value plus about 10% D1 headroom and exact
2368
+ downstream counts, live in its `BUDGETS` table. Measured on main in October 2026:
2369
+
2370
+ | Request | Downstream | D1 round trips | SQL statements |
2371
+ | ---------------------------------------- | ---------: | -------------: | -------------: |
2372
+ | Legacy remote MCP call, warm | 5 | 6 | 7 |
2373
+ | Legacy remote MCP call, cold isolate | 5 | 9 | 12 |
2374
+ | Modern remote MCP call, warm | 1 | 7 | 8 |
2375
+ | Modern remote MCP call, cold isolate | 1 | 10 | 13 |
2376
+ | OAuth remote MCP call, warm | 5 | 19 | 20 |
2377
+ | OAuth remote MCP call, cold isolate | 5 | 22 | 25 |
2378
+ | OAuth call after a 401, with refresh | 9 | 39 | 48 |
2379
+ | OAuth call with proactive refresh | 6 | 35 | 43 |
2380
+ | `api()` credential call, warm | 1 | 3 | 4 |
2381
+ | `api()` credential call, cold isolate | 1 | 6 | 9 |
2382
+ | Unscoped search, warm (3 remote, 1 api) | 0 | 20 | 20 |
2383
+ | `execute_code`, 5 calls to one connector | 9 | 10 | 15 |
2384
+ | UI connector detail, `api()` credential | 0 | 2 | 2 |
2385
+ | UI connector detail, credential remote | 4 | 9 | 9 |
2386
+ | UI connector detail, OAuth remote | 4 | 18 | 18 |
2387
+
2388
+ Header-auth calls to the legacy (session) server handshake per request:
2389
+ `initialize`, `notifications/initialized`, the SSE `GET`, the call and the
2390
+ session `DELETE`. The modern server advertises a catalog TTL, so a warm call is
2391
+ the call alone. OAuth calls read the grant fresh before every SDK message.
2392
+ Run with `D1_BUDGET_REPORT=1` to print each measurement; a failing case prints
2393
+ its request and statement log.
2394
+
2412
2395
  ### Contributor provider kit
2413
2396
 
2414
2397
  Provider declarations use `src/http/vendor.ts` for HttpApi operations and
@@ -2427,17 +2410,29 @@ exact vendor count as `page.total` from its page callback. The kit validates a
2427
2410
  nonnegative safe integer and includes it on continuing and terminal pages;
2428
2411
  without a vendor count the property is absent. Lists without the opt-in keep
2429
2412
  their existing schema and result bytes. Never derive an exact total from a
2430
- partial page or an estimate. `src/http/fields.ts` refuses an unknown first
2431
- segment before dispatch; a selected
2432
- result renders against a sparse output schema. `src/http/hatch.ts` is the
2413
+ partial page or an estimate. `src/http/fields.ts` derives each `fields`
2414
+ description's anchor and example from the contract that validates it. It
2415
+ refuses before dispatch a path whose first segment is not a declared property,
2416
+ and a deeper segment only inside an object the schema closes with
2417
+ `additionalProperties: false`; a selected result renders against a sparse
2418
+ output schema. `src/http/hatch.ts` is the
2433
2419
  standard hatch (`_api_search`, `_api_details`, `_api_read`, `_api_write`) over
2434
2420
  an `OperationIndex` with explicit verdicts. It keeps the REST family's
2435
2421
  argument and envelope names and validates `fields` against the operation's
2436
- declared response roots before dispatch. Search answers stay within 16 KiB
2422
+ declared response roots before dispatch. A deeper miss is reported in
2423
+ bounded `warnings` naming declared properties only, never refused, because the
2424
+ pinned index declares paths only to a bounded depth. Search answers stay within 16 KiB
2437
2425
  (at most eight compact entries, long labels cut and marked, continuation by
2438
2426
  cursor); details stay within 32 KiB, paging whole parameters and body
2439
2427
  properties, and chunking any piece larger than one answer, behind a bound
2440
- cursor. Standard hatch search removes conversational framing; framing-only
2428
+ cursor. `src/http/hatch-response.ts` builds details' capped `response` section
2429
+ from the index's declared response paths in `fields` coordinates. It lists
2430
+ selectable paths and promises no readability. Its `withheld` hint comes from
2431
+ `ResultSafety.withholds`: `ValueSafety.withholds` reads the rules `redact`
2432
+ applies that change every non-null value they reach, matched with list
2433
+ markers. Custom redactions may declare their own hint. Rules that can pass a
2434
+ value through (`url:`, `origin:`, `[?…]`, `@keys`, string-only scrubbing) are
2435
+ never hints. Standard hatch search removes conversational framing; framing-only
2441
2436
  queries return no matches. It admits partial lexical matches alongside full
2442
2437
  matches. Action/read queries require all content nouns in the operation text,
2443
2438
  or any noun in the path/id with at least half the nouns in the text. Their
@@ -2584,9 +2579,10 @@ A binding may opt into `truncated: true` to supply reviewed size-cut paths
2584
2579
  (such as `data.content`) in its answer. The hatch then declares and propagates
2585
2580
  an optional top-level `truncated: string[]` on reads and writes. Without the
2586
2581
  opt-in, the schemas and results are unchanged; size-cut metadata is not inferred
2587
- from vendor booleans or added to other providers. A separate `warnings: true`
2588
- opt-in declares and preserves reviewed `warnings: string[]` beside `data`, even
2589
- when `fields` narrows the vendor body.
2582
+ from vendor booleans or added to other providers. Every hatch read and write
2583
+ declares an optional `warnings: string[]` for `fields` entries that matched
2584
+ nothing in a nonempty body. A separate `warnings: true` opt-in also preserves
2585
+ reviewed vendor warnings there, even when `fields` narrows the vendor body.
2590
2586
 
2591
2587
  A binding may expand deployment-owned path defaults (Cloudflare) or canonicalize
2592
2588
  repository contents paths (GitHub) before indexed resolution, and may prepare a
@@ -3089,17 +3085,9 @@ including structural lookalikes and genuine router errors, retain their identity
3089
3085
 
3090
3086
  Four public `effect/http/*` modules are allowed only in the private dispatcher;
3091
3087
  all other root-purity and optional-implementation boundaries remain enforced.
3092
- Direct public module imports avoid the area's barrel cost. The local D3
3093
- measurement over the reviewed D1/D2 stack is root 373,435 to 393,470 bytes gzip
3094
- and Worker 758,274 to 774,384 (official Node 22.23.4/zlib 1.3.1-e00f703, identical
3095
- repository bundle options). The barrel alternative measures 423,348/787,614.
3096
- The parent accepted this measured native-routing cost with a Worker cap of
3097
- 785,000 bytes, including room for separately measured native-auth and reviewed
3098
- host/transport additions. Root and all other caps and historical baselines are
3099
- unchanged by D3. Separate-input costs are not a combined gzip measurement;
3100
- the integrated deployment must be measured again.
3101
-
3102
- `test/server.test.ts` asserts production routing directly on Node and workerd:
3088
+ Direct public module imports avoid the area's barrel cost.
3089
+
3090
+ `test/server.test.ts` asserts production routing directly on Node:
3103
3091
  raw prefixes and malformed suffixes, extension methods, CIMD before auth/UI/body
3104
3092
  reads, Response/error identity, unmatched fallthrough, concurrent request
3105
3093
  isolation, and body-held permits through end/failure/cancel/abort/abandonment.
@@ -3111,85 +3099,65 @@ Root purity, declaration and bundle checks own import boundaries and size caps.
3111
3099
  The frozen legacy dispatcher, source transforms, diagnostic latency loops and
3112
3100
  rejected Effect MCP adapters were migration evidence; their historical source
3113
3101
  is linked from decision 0016 rather than rebuilt by ordinary tests.
3114
- The upward MCP protocol leaves remain SDK implementations, with D7's separate
3115
- native-orchestration work and compatibility evidence unchanged.
3116
-
3117
- ### Why not Schema or HttpApi for core routes
3118
-
3119
- Both were measured and both lost. `effect` is imported through its barrel,
3120
- which defeats tree-shaking: the first conversion step measured about +20 ms
3121
- of Worker cold start for it, and that cost was accepted over deep imports at
3122
- every call site. The catch is that a module the barrel reaches is paid for in
3123
- full, however little of it is used. Effect Schema for config validation and the
3124
- meta-tool inputs came to about +62 KB gzip and +13 ms of cold start in the
3125
- root, for a validator no clearer than the hand-written schema in `src/config.ts`; the
3126
- meta-tool inputs stay zod, which the MCP SDK bundles regardless. `HttpApi` for
3127
- the operator and activity routes cost about +100 KB gzip on `./ui` and +130 KB
3128
- on `./activity`, and matching the wire format meant opting out of most of what
3129
- it does: unowned paths fall through rather than 404, a wrong method is a JSON
3130
- 405, and the content-type and size checks run before a body is read. The root
3131
- entry imports `effect` and the four public HTTP modules confined to the private
3132
- dispatcher (`test/purity.node.test.ts`).
3133
- Effect 4.x is a long-term-support line, but its area barrels `effect/http`,
3134
- `effect/http-api`, `effect/schema`, and `effect/sql` are still tagged
3135
- `@stability unstable` and can change within it; only core modules such as
3136
- `effect/Schema` are stable. That is why Effect is pinned to one exact version
3137
- and every Effect upgrade is its own deliberate pull request. A deployment
3138
- that also depends on Effect, such as the Worker example's Alchemy stack, must pin the
3139
- same version to resolve a single copy. Connecta's own lockfile and packed
3140
- install are checked for one copy (`test/package-surface.node.test.ts` and
3141
- `scripts/check-package.mjs`).
3142
-
3143
- The stable `4.0.0` release was re-evaluated on 2026-10-01 with this
3144
- [historical probe source](https://github.com/zackbart/connecta/blob/1ab92695f2636f904274d900ce209553358ff1f3/scripts/probes/effect-v4-evaluation.mjs),
3145
- retained in Git history rather than the current checkout.
3146
- The [raw observations](https://github.com/zackbart/connecta/blob/1ab92695f2636f904274d900ce209553358ff1f3/scripts/probes/effect-v4-evaluation-2026-10-01.json)
3147
- are synthetic Node requests and neutral esbuild bundles, not live-client or
3148
- workerd lifetime verification. Bundle figures add representative prototypes
3149
- alongside the current code; they do not claim savings from removing old code.
3150
-
3151
- Two native input schemas with validation and JSON Schema rendering add 62,148
3152
- bytes gzip to the root, taking it from 303,083 to 365,231 against a 339,526
3153
- cap. Effect can reject excess properties when both decoding and rendering use
3154
- `onExcessProperty: "error"`; its default strips them. The generated schemas
3155
- still differ from the published meta-tool schemas, including record rendering and
3156
- composed numeric bounds. A Schema replacement would need to preserve that
3157
- contract and memoized rendering. Both MCP SDK packages still depend on Zod,
3158
- so changing our inputs alone does not remove Zod from an installation.
3159
-
3160
- A one-endpoint `HttpApi` prototype adds 106,634 bytes gzip to `./ui` and 106,453
3161
- to `./activity`. Even plain `HttpRouter` adds 45,324 to `./ui`, taking it to
3162
- 130,023 against a 128,983 cap. Both default handlers return an empty 404 for a
3163
- wrong method and an unowned path; our routes need a JSON 405 for the former
3164
- and module fallthrough for the latter. Their `toWebHandler` also builds its
3165
- layer immediately and owns a runner, so adopting it would require proving
3166
- global-scope safety and preserving our scheduler and response-lifetime rules.
3167
- Those historical defaults are not the production adapter: the narrow public
3168
- HttpRouter dispatch above preserves explicit method refusals, unowned fallthrough
3169
- and the existing edge runner without `toWebHandler`.
3170
-
3171
- Effect's MCP server has a separate compatibility gate. Its `2026-07-28`
3172
- adapter accepted a stateless `tools/list`, but its `2025-06-18` adapter refused
3173
- a fresh request with 400. Initialization issued a session, and that session's
3174
- next request returned 404 on a fresh handler; Connecta served the same fresh
3175
- legacy list with 200 JSON and no session. The stable package exports no MCP
3176
- client, so it also cannot replace our downstream SDK and OAuth implementation.
3177
- [Issue #622](https://github.com/zackbart/connecta/issues/622) records the server
3178
- parity requirements. These historical measurements supported retaining MCP protocol and validation
3179
- implementations. Production HTTP dispatch now uses the separately proven narrow
3180
- public adapter above; stable v4 alone did not establish its compatibility.
3181
-
3182
- The core's cost is recorded rather than guessed. Against 0.24.4 the root entry
3183
- grew from 235,346 to 279,526 bytes gzip, the Worker example from 263,949 to
3184
- 314,336, and each provider by about 36 KB, since each reaches Effect through
3185
- `remoteMcp()`. `pnpm run check:bundle` caps the deployed shapes (the root,
3186
- `./worker`, `./ui`, the operator UI assets, `./d1`, `./auth/access-tokens`,
3187
- the Worker example, and the other non-provider entries) against
3188
- `scripts/bundle-budget.json`. Per-provider entries are reported with their
3189
- size and change from baseline but not capped: every deployment already imports
3190
- the root, which carries the Effect core, so a provider's standalone bundle
3191
- counts that core a second time. An install carries about 53 MB of `effect`, with
3192
- no dependencies of its own.
3102
+ The upward MCP protocol leaves remain SDK implementations with native
3103
+ orchestration and compatibility evidence.
3104
+
3105
+ ### Validation and HTTP boundaries
3106
+
3107
+ Config validation uses `src/config.ts`; meta-tool inputs use Zod, which the MCP
3108
+ SDK also depends on. Core routing uses the narrow public HTTP adapter above.
3109
+ Effect is pinned exactly; deployments using Effect must resolve the same version.
3110
+ `test/package-surface.node.test.ts` and `scripts/check-package.mjs` check one copy.
3111
+ Per-provider bundles are reported, not capped, because their standalone graphs
3112
+ include the shared core again. Deployed non-provider shapes are capped by
3113
+ `scripts/bundle-budget.json`.
3114
+ The always-loaded agent surface has fixed-cost caps of the same kind: the
3115
+ `always-loaded agent surface budgets` cases in
3116
+ `test/meta-tool-schemas.test.ts` cap the instructions plus `tools/list` total,
3117
+ each tool description and output schema, the `usage` skill, and one default
3118
+ and one compact search row, measured on a pinned four-connector fixture.
3119
+
3120
+ ### Route names
3121
+
3122
+ Connecta-authored agent text names a route in the wording of the surface that
3123
+ serves it, resolved when the text is built (`src/agent-routes.ts`). Nothing
3124
+ renders afterwards: no code scans or rewrites replies, program values,
3125
+ downstream results, resources, guides or caller data, so paging, previews,
3126
+ totals and offsets measure the final text.
3127
+
3128
+ Two tables per surface. `AgentRouteNames` names the tools (`call_tool`,
3129
+ `call_destructive_tool`, `search_tools`, `execute_code`, `authorize_connector`,
3130
+ `skills` on the meta surface); a sentence uses a name only when written for a
3131
+ surface that lists that tool. `RoutePhrases` holds complete phrases for
3132
+ sentences every surface shares, such as "for a direct call_tool only" against
3133
+ "when calling this tool directly", "call directly (call_tool, connecta.result
3134
+ to page)" against "call this tool directly and page with read_result", and the
3135
+ authorization sentences, which on native pools tell the agent to give the
3136
+ error's nextAction to the user or operator. `program` is undefined on a native
3137
+ pool without `execute_code`, so text about program capacity branches on it.
3138
+ The eval code arm's phrases keep the text its comparator protocol recorded.
3139
+
3140
+ Where each text gets its wording:
3141
+
3142
+ | Text | Wording |
3143
+ | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3144
+ | Invocation refusals, authorization handoffs, paging notices | The request's names, from the MCP route through meta-tools, programs and native pools |
3145
+ | Connector and provider call text | `routePhrases(ctx)`; inside a call's Effect, the `Routes` reference; vendor tables' `words(detail, routes)`; an `api()` connection's write-401 refusal, configured at construction, receives the call's `Routes` |
3146
+ | Contexts the registry builds | The deployment's names (meta, or the eval code arm's); a call carries its request's names |
3147
+ | Static provider schema descriptions | `routeDescription(build)`: the declared meta text; each request's catalog sets them on its own copy of each `api()` tool, which keeps the tool's source classification, built for its serving surface. Description paths are cached per connector and tool name, and rendered text per declared description and surface. Downstream catalogs are never touched |
3148
+ | Meta instructions, meta-only tool descriptions, built-in guides | Meta names, built once; built-in guides keep one digest across deployments |
3149
+ | Native instructions, the `execute_code` description | Built per surface |
3150
+ | Program-return and program-call paging notes | Meta names; they name only `execute_code`, which every surface running programs lists |
3151
+ | The OAuth adapter's `UnauthorizedError` | Meta names; only its class reaches callers, which word their own error from the call |
3152
+
3153
+ The provider standard's S7 check refuses a spelled-out meta-tool name in
3154
+ provider code. Remaining literals are not agent route text: `execute_code`'s own
3155
+ budget and timeout errors, operator log lines, construction errors, the
3156
+ configuration reference, the OAuth callback page and operator fix prompts. The
3157
+ eval code arm marks its configuration with `renderRoutesAsPrograms`, an internal
3158
+ hook no package entry exports; no configuration key selects it.
3159
+
3160
+ [Decision 0019](https://github.com/zackbart/connecta/blob/main/decisions/0019-core-validation-http-boundaries.md) records the rejected Schema/HttpApi measurements.
3193
3161
 
3194
3162
  ## Where else to look
3195
3163
 
@@ -3243,9 +3211,7 @@ compiling and configuring the real thing.
3243
3211
  accounting and claim ownership keep their existing ordering. Late
3244
3212
  rejections are observed; dispatch and lease guards still contain detached
3245
3213
  work. Per-await I/O cancellation remains useful for releasing resources,
3246
- while prompt refusal is enforced at the boundary
3247
- (`test/cancellation-faults.test.ts`, `test/operation-cancellation.test.ts`
3248
- and `test/cancellation-journey.node.test.ts`). Invocation owns
3214
+ while prompt refusal is enforced at the boundary. Invocation owns
3249
3215
  the recovery and logical-call records it reads after teardown. Closing an
3250
3216
  outer call also revokes nested calls paused in preparation. Request-owned
3251
3217
  MCP sessions and token flights outlive individual calls and close with the
@@ -3262,11 +3228,9 @@ compiling and configuring the real thing.
3262
3228
  transfers preparation, token exchange and fenced rotation commit to the
3263
3229
  storage coordinator under its own deadlines and deferred-work hook; only
3264
3230
  the caller's wait still closes with the call. That completion
3265
- cannot resume a closed call or resend its resource request
3266
- (`test/call-renewal-lifetime.test.ts`,
3267
- `test/remote-mcp-renewal-lifetime.test.ts`). Explicit request revocation also
3268
- aborts pending authorization-code exchange and refuses late token writes
3269
- (`test/operation-cancellation.test.ts`). Request closure
3231
+ cannot resume a closed call or resend its resource request. Explicit request
3232
+ revocation also aborts pending authorization-code exchange and refuses late
3233
+ token writes. Request closure
3270
3234
  revokes views and calls first, then runs every finalizer in reverse order,
3271
3235
  collecting failures. The caller's original cancellation signal is retained
3272
3236
  separately from the lease signal, so normal teardown never looks like caller
@@ -3326,7 +3290,10 @@ compiling and configuring the real thing.
3326
3290
  is a known failure only on refusal provenance recorded where the evidence is
3327
3291
  observed, never inferred from an error code: (a) the ledger proves it never
3328
3292
  left, or (b) the downstream served an HTTP refusal, a 4xx other than 408
3329
- (`refusedStatus` and `vendorRefusal` in `src/provider-call.ts`), under the
3293
+ (`refusedStatus` and `vendorRefusal` in `src/provider-call.ts`); for
3294
+ `remoteMcp()` only when that 4xx carries a JSON-RPC error body, since a bare
3295
+ one may be a gateway's answer after the server acted (`ambiguous` on the
3296
+ call's dispatch in `src/connectors/remote-mcp/transport.ts`), under the
3330
3297
  provider's existing served classification (a write's HTTP 409 is a `conflict`). One helper, `servedRefusal` in `src/write-outcome.ts`, decides
3331
3298
  (b) for the ledger and for a transport outside one: a refusing status the
3332
3299
  provider reads as `unavailable` or a timeout (Stripe's 424) is not proof, so
@@ -3340,13 +3307,13 @@ compiling and configuring the real thing.
3340
3307
  as a `manual` check in its `drift.json`): an API v1 answer carrying Stripe's
3341
3308
  `Request-Id`. The real HTTP hostile-response journey checks this refusal
3342
3309
  against an ambiguous committed write; there is no copied declaration list.
3343
- Q8 is decided in one place, `settleWrite` in
3310
+ Write outcomes are decided in one place, `settleWrite` in
3344
3311
  `src/write-outcome.ts`, once per logical call and over every request it
3345
3312
  recorded. The invocation calls it for every failure of a served write: a
3346
3313
  thrown or mapped error, a custom connector's, a program's call, and its
3347
3314
  own deadline or the caller's cancellation, when no connector failure
3348
3315
  arrives at all. It reads the logical call's requests
3349
- (`logicalCallRequests`), which hold every resource request dispatched
3316
+ (`LogicalLedger`'s `sent`), which hold every resource request dispatched
3350
3317
  under the call from any ledger: nested `api()` calls, remote MCP, guarded
3351
3318
  transports, `ctx.fetch` and the kit. Without an invocation (a direct call,
3352
3319
  or a context no invocation opened), only the outermost boundary calls it,
@@ -3415,9 +3382,8 @@ compiling and configuring the real thing.
3415
3382
  Remote MCP captures
3416
3383
  each physical read's entry before awaiting its response; a later send
3417
3384
  cannot overwrite it. The kit's response identity and Google's init identity
3418
- identify the request whose credential was renewed. The legacy guarded-fetch
3419
- `answeredRequest` helper has no production caller; tests retain it to exercise
3420
- renewal provenance outside the kit.
3385
+ identify the request whose credential was renewed. Guarded mappers that throw
3386
+ an authorization refusal identify its request through `refusalRequest(error)`.
3421
3387
  The decider normalizes every failure it settles,
3422
3388
  typed or not (a raw network error, an `AbortError`, a thrown string). Its
3423
3389
  verdict is final (`settledWrite`, which follows copies and redaction): the
@@ -3591,23 +3557,8 @@ compiling and configuring the real thing.
3591
3557
  unchanged for the invocation to report.
3592
3558
  Shared provider outcomes and the real HTTP hostile-response journey check
3593
3559
  returned data, failure/recovery, privacy and physical writes without a
3594
- catalog-wide status matrix. `test/provider-failure-sweep.test.ts` retains
3595
- fault probes for Docs creation with initial text, composite success followed
3596
- by refusal, GitHub preflight failures, typed handler errors after successful
3597
- sends, Forms cursor conflicts after 200, generic-code vendor refusals,
3598
- auxiliary failures after resource dispatch and concurrent direct-call isolation.
3599
- It also holds siblings awaiting credentials or already in flight when another
3600
- write fails, refuses requests after call closure, and probes network errors,
3601
- refusal-shaped abort reasons and untracked sends. Native transport and
3602
- request-lifetime suites retain their platform-specific evidence.
3603
- `test/write-evidence.test.ts` uses real refused, reset, and held sockets on
3604
- both runtimes: a refused write is unknown and a refused read retryable,
3605
- including a write aborted with another fetch's real refusal as its reason
3606
- after the server read its body; `test/write-evidence.node.test.ts` covers a
3607
- Planning Center POST that undici resends after a 421 to a closed listener, a
3608
- DELETE that a `RetryAgent` resends after a reset, and a streamed body and a
3609
- `Blob` with an overridden `stream()` that undici partly sends before its
3610
- producer fails. Each has the server holding the write and reports unknown.
3560
+ catalog-wide status matrix. Native transport and request-lifetime suites
3561
+ retain their platform-specific evidence.
3611
3562
  Additional tests:
3612
3563
  `test/remote-mcp-write-once.test.ts` (real SDK transport),
3613
3564
  `test/api-oauth.test.ts`, `test/google-workspace-delegation.test.ts`, and each
@@ -3694,10 +3645,10 @@ OAuth retry, grant, response-decoding or ownership orchestration.
3694
3645
  there before claiming a code/refresh grant, so an unsupported Node host consumes
3695
3646
  neither. The base-fetch leaves in static OAuth and remote MCP call
3696
3647
  `sendCredentialPost`; remote registration and disconnect revocation do so too.
3697
- D4 must preserve this preflight and the distinction between request-owned code
3648
+ The adapters preserve this preflight and the distinction between request-owned code
3698
3649
  exchange and coordinator-owned refresh after claim submission. The existing
3699
3650
  spent-token algorithm intentionally remains conservative after a submitted claim,
3700
- even if a later CONNECT fails before destination dispatch. D6 must preserve the
3651
+ even if a later CONNECT fails before destination dispatch. Remote MCP preserves the
3701
3652
  SDK JSON-RPC envelope distinction at the leaf: protocol POSTs are not credential
3702
3653
  POSTs; forms and registration JSON are. Discovery GETs remain native fetch.
3703
3654
 
@@ -3785,27 +3736,22 @@ unchanged. A narrow machine check restores only those two exact adjacent
3785
3736
  pairs and requires byte equality, with no other type, export or reference
3786
3737
  change. All 88 declarations still exclude Effect and have complete references.
3787
3738
 
3788
- The superseding 2026-10-25 D3/D4 cost ruling authorizes only Worker
3789
- `maxGzip: 785000` (previously 760000) and Clerk `maxGzip: 85000` (previously
3790
- 63087). Official cached Node 22.23.4 / zlib 1.3.1-e00f703 measured Clerk at
3791
- 47,688 -> 80,961 bytes gzip (+33,273): the generated SDK is unchanged, while
3792
- native Effect adds 91.8 KB raw plus the native helper and small auth-source
3793
- growth. The isolated D4 candidate measured Worker at 762,666, root at 377,177
3794
- and D1 at 88,321 bytes gzip. Optional-graph optimization brought UI to 342,955,
3795
- so the earlier unused UI allowance is revoked and its original 345000 cap
3796
- remains. Historical baselines, root/D1 and all other caps, dependency pins and
3797
- security/API gates remain unchanged. These independent measurements do not
3798
- predict a combined gzip by addition; final artifacts must be rebuilt. Native
3799
- registration, decorated fallback and request/native-storage semantics also
3800
- require a smoke through an actual minified production bundle. Final post-hook
3801
- official artifacts measure root 377,290, D1 88,321, Clerk 80,959, UI 342,959
3802
- and Worker 762,774 bytes gzip, all within those caps.
3803
-
3804
3739
  ### Native remote MCP programs (contributor)
3805
3740
 
3806
3741
  Remote MCP connection joins, credential and epoch checks, catalog intake,
3807
- negotiation, calls, resources, Skills, auth and status compose native programs in
3808
- `connectors/remote-mcp.ts`. `catalog-cache.ts` owns native SDK cache reads,
3742
+ negotiation, calls, resources, Skills, auth and status compose native programs.
3743
+ `connectors/remote-mcp.ts` keeps the published options, the redirect guard and
3744
+ `remoteMcp()`, which builds one runtime from per-seam factories in
3745
+ `connectors/remote-mcp/`: `config.ts` (options and validation), `errors.ts`
3746
+ (boundary error mapping), `transport.ts` (transport and write-once dispatch),
3747
+ `connection.ts` (the per-scope connection state machine), `catalog.ts`
3748
+ (catalog intake), `resources.ts` (Skills and resource reads), `oauth.ts` (the
3749
+ OAuth surface) and `call.ts` (`callTool`); `runtime.ts` types what they share.
3750
+ `auth/downstream-oauth.ts` keeps `KvOAuthProvider`'s state and declared
3751
+ methods; `auth/downstream-oauth/` holds the refresh coordinator and, by seam,
3752
+ the provider's native method bodies (grant and epoch store, consent records,
3753
+ refresh lease and spent records, SDK hooks), installed once each by
3754
+ `authMethods`. `catalog-cache.ts` owns native SDK cache reads,
3809
3755
  chunked publication, generation fencing, complete refresh acceptance and listing
3810
3756
  serialization; `connectors/negotiation-cache.ts` owns native negotiation storage.
3811
3757
  The published connector, cache and SDK callback methods remain Promise shells.
@@ -3824,7 +3770,7 @@ restore that same owner. Every operation captures its own generation, credential
3824
3770
  identity and listing binding before waiting; a later listing cannot adopt an
3825
3771
  abandoned publication. Request closure revokes the connector and cache scopes.
3826
3772
  No client, transport, response, signal, promise or live request Context is shared
3827
- across inbound requests. D4's bounded refresh coordinator retains its separate
3773
+ across inbound requests. The bounded refresh coordinator retains its separate
3828
3774
  ownership, synchronous submission gate and waiting-request handoff.
3829
3775
 
3830
3776
  Two deliberate Promise waits remain: a connect waiter receives the Deferred's
@@ -3863,16 +3809,11 @@ select the underlying current methods at dispatch, including namespaces created
3863
3809
  while a source method is decorated. Original SQL shells call their own native
3864
3810
  program directly. Tests cover current and previously selected programs, own and
3865
3811
  inherited saved-original delegates, get/set/CAS and activity record/list,
3866
- restoration, typed failures and interruption. The D4 native proof fixtures install
3812
+ restoration, typed failures and interruption. The native auth proof fixtures install
3867
3813
  their intentional throwing Promise shells before registration, so they establish
3868
3814
  native composition without authorizing bypass of later replacements.
3869
3815
 
3870
- `remote-mcp-native.test.ts` runs native SDK/cache/request-boundary cases on Node
3871
- and workerd. `remote-mcp-native-bundle.node.test.ts` additionally loads an actual
3872
- minified production bundle to verify call/cache registration, request Context,
3873
- native storage, saved-original delegates, custom replacements and closed-owner
3874
- refusal. Existing wire, physical-attempt, cache and OAuth lifetime suites remain
3875
- the compatibility evidence.
3816
+ Existing wire, cache and OAuth suites remain the compatibility evidence.
3876
3817
 
3877
3818
  ### Native upward MCP orchestration (contributor)
3878
3819
 
@@ -3926,19 +3867,9 @@ identity, credential, storage, executor and tool callbacks. Connecta's OAuth-lin
3926
3867
  binding, TTL, progress, closing and one-use claims are native orchestration around
3927
3868
  those leaves. The supported protocol and unsupported-feature refusals remain
3928
3869
  unchanged. Native consumer, failure, decorator, sharing, close and minified-bundle
3929
- regressions live in `upward-native.test.ts` and
3930
- `upward-native-bundle.node.test.ts`; existing wire, continuation, redaction,
3870
+ regressions live in `upward-native.test.ts`; existing wire, continuation, redaction,
3931
3871
  physical-attempt and body/admission suites remain compatibility evidence.
3932
3872
 
3933
- The combined native/operator graph has a measured contributor bundle allowance
3934
- in PR #1060: the UI gzip cap is 346,000 bytes, with its historical baseline
3935
- unchanged. The official full checker measured 345,228 bytes after regenerating
3936
- the merged UI assets. Removing unused Invocation/Catalog registration retention
3937
- returned the public Worker entry from 198,744 to 77,426 bytes; live minified
3938
- Context, inherited-method, decorator and restoration proofs retain native
3939
- composition. Root, example Worker, public Worker, D1 and Clerk caps remain
3940
- unchanged. This cost decision grants no later request-body-limit allowance.
3941
-
3942
3873
  ### Catalog operation vocabulary (contributor)
3943
3874
 
3944
3875
  The standard hatch privately registers bounded vendor operation text against its
@@ -3953,13 +3884,3 @@ cache. Exact authored names and complete authored matches precede API pointers;
3953
3884
  ordinary result limits, offsets, catalog failure disclosure and byte bounds apply.
3954
3885
  The published discovery contract states compilation, query and cache bounds.
3955
3886
  No request identity, signal, credential or promise enters a vocabulary cache.
3956
-
3957
- ### Authenticated body-bound cost (contributor)
3958
-
3959
- The separate #860 change has a measured allowance only for the example Worker
3960
- gzip cap, from 785,000 to 786,000 bytes, with its historical baseline unchanged.
3961
- The official full checker measures 785,036 bytes against D7's exact 784,948-byte
3962
- lower graph, an actual 88-byte increase. It reuses the bounded platform body
3963
- inspector, preserves native orchestration and avoids a new reader lifecycle.
3964
- Root, UI, public Worker, D1, Clerk and asset caps remain fixed. The cost ruling
3965
- does not approve protocol, cancellation or authority semantics.