@zackbart/connecta 0.38.0 → 0.40.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 (180) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +3 -2
  3. package/bin/connecta.mjs +4 -8
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +6 -4
  6. package/dist/agent-routes.js +231 -0
  7. package/dist/auth/downstream-oauth/consent.js +519 -0
  8. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  9. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  10. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  11. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  12. package/dist/auth/downstream-oauth.js +66 -2382
  13. package/dist/auth/native.js +37 -0
  14. package/dist/auth/oauth-grant.js +13 -0
  15. package/dist/auth/static-oauth.js +153 -131
  16. package/dist/auth-elicitation.js +7 -1
  17. package/dist/authorization-handoff.js +11 -5
  18. package/dist/catalog-cache.js +219 -59
  19. package/dist/catalog-policy.js +18 -0
  20. package/dist/catalog-service.js +284 -47
  21. package/dist/catalog.js +18 -2
  22. package/dist/config.d.ts +16 -11
  23. package/dist/config.js +9 -6
  24. package/dist/connector-caller.d.ts +1 -1
  25. package/dist/connector-health.js +59 -0
  26. package/dist/connector-status.js +117 -0
  27. package/dist/connectors/api-connector.d.ts +7 -2
  28. package/dist/connectors/api-connector.js +7 -4
  29. package/dist/connectors/negotiation-cache.js +22 -4
  30. package/dist/connectors/remote-mcp/call.js +164 -0
  31. package/dist/connectors/remote-mcp/catalog.js +278 -0
  32. package/dist/connectors/remote-mcp/config.js +263 -0
  33. package/dist/connectors/remote-mcp/connection.js +682 -0
  34. package/dist/connectors/remote-mcp/errors.js +488 -0
  35. package/dist/connectors/remote-mcp/oauth.js +473 -0
  36. package/dist/connectors/remote-mcp/resources.js +473 -0
  37. package/dist/connectors/remote-mcp/runtime.js +1 -0
  38. package/dist/connectors/remote-mcp/transport.js +555 -0
  39. package/dist/connectors/remote-mcp.js +62 -2863
  40. package/dist/credential-contract.d.ts +23 -0
  41. package/dist/credential-reads.js +17 -0
  42. package/dist/credential-verification.js +53 -0
  43. package/dist/credentials.d.ts +7 -0
  44. package/dist/credentials.js +60 -25
  45. package/dist/describe-config.d.ts +12 -2
  46. package/dist/describe-config.js +2 -0
  47. package/dist/discovery-health.js +199 -0
  48. package/dist/downstream-input.js +2 -0
  49. package/dist/errors.d.ts +55 -17
  50. package/dist/errors.js +95 -41
  51. package/dist/execute.js +152 -114
  52. package/dist/executor-result.js +3 -1
  53. package/dist/guest-functions.js +83 -0
  54. package/dist/guest-runtime.js +60 -19
  55. package/dist/guest-types.d.ts +19 -5
  56. package/dist/http/argument-problems.js +35 -0
  57. package/dist/http/errors.js +5 -4
  58. package/dist/http/fields.js +192 -13
  59. package/dist/http/hatch-response.js +73 -0
  60. package/dist/http/hatch.js +88 -23
  61. package/dist/http/oauth.js +3 -2
  62. package/dist/http/provider.js +32 -4
  63. package/dist/http/safety.js +8 -0
  64. package/dist/http/tools.js +44 -23
  65. package/dist/index.d.ts +1 -1
  66. package/dist/index.js +9 -2
  67. package/dist/invocation.js +26 -8
  68. package/dist/membership.d.ts +13 -5
  69. package/dist/membership.js +34 -2
  70. package/dist/meta-tools.js +54 -31
  71. package/dist/native-recovery.js +4 -16
  72. package/dist/native-tools.js +5 -2
  73. package/dist/oauth-sealing.js +10 -0
  74. package/dist/operation-vocabulary.js +135 -13
  75. package/dist/operator-record.d.ts +2 -2
  76. package/dist/operator-record.js +1 -0
  77. package/dist/operator-ui/contract.d.ts +10 -0
  78. package/dist/operator-ui/generated.js +2 -2
  79. package/dist/operator-ui/model.d.ts +22 -1
  80. package/dist/provider-call-facts.js +6 -2
  81. package/dist/providers/_shared/rest/operation-index.js +48 -50
  82. package/dist/providers/_shared/rest/value-safety.js +80 -0
  83. package/dist/providers/breeze/api.js +7 -1
  84. package/dist/providers/breeze/skill.generated.js +1 -1
  85. package/dist/providers/breeze/tools.js +4 -3
  86. package/dist/providers/ccb/api.js +2 -1
  87. package/dist/providers/ccb/index.js +5 -0
  88. package/dist/providers/ccb/skill.generated.js +1 -1
  89. package/dist/providers/cloudflare/api.js +8 -1
  90. package/dist/providers/cloudflare/skill.generated.js +1 -1
  91. package/dist/providers/docs/lib/schemas.js +24 -22
  92. package/dist/providers/docs/safety.js +2 -0
  93. package/dist/providers/docs/skill.generated.js +1 -1
  94. package/dist/providers/docs/tools.js +2 -4
  95. package/dist/providers/drive/api.js +1 -0
  96. package/dist/providers/drive/lib/contracts.js +8 -5
  97. package/dist/providers/drive/skill.generated.js +1 -1
  98. package/dist/providers/drive/tools.js +11 -16
  99. package/dist/providers/forms/api.js +1 -0
  100. package/dist/providers/forms/skill.generated.js +1 -1
  101. package/dist/providers/forms/tools.js +10 -7
  102. package/dist/providers/github/api.js +1 -0
  103. package/dist/providers/github/lib/discovery.js +1 -1
  104. package/dist/providers/github/skill.generated.js +1 -1
  105. package/dist/providers/github/tools.js +61 -17
  106. package/dist/providers/gmail/lib/schemas.js +60 -50
  107. package/dist/providers/gmail/skill.generated.js +1 -1
  108. package/dist/providers/gmail/tools.js +7 -5
  109. package/dist/providers/granola/index.js +2 -0
  110. package/dist/providers/granola/skill.generated.js +1 -1
  111. package/dist/providers/overflow/api.js +7 -1
  112. package/dist/providers/overflow/skill.generated.js +1 -1
  113. package/dist/providers/planning-center/api.js +3 -1
  114. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  115. package/dist/providers/planning-center/skill.generated.js +1 -1
  116. package/dist/providers/quickbooks/safety.js +80 -39
  117. package/dist/providers/quickbooks/skill.generated.js +1 -1
  118. package/dist/providers/sheets/api.js +1 -0
  119. package/dist/providers/sheets/skill.generated.js +1 -1
  120. package/dist/providers/sheets/tools.js +13 -14
  121. package/dist/providers/slides/skill.generated.js +1 -1
  122. package/dist/providers/slides/tools.js +15 -13
  123. package/dist/providers/stripe/skill.generated.js +1 -1
  124. package/dist/providers/tithely/api.js +3 -1
  125. package/dist/providers/tithely/skill.generated.js +1 -1
  126. package/dist/providers/vercel/api.js +1 -1
  127. package/dist/providers/vercel/skill.generated.js +1 -1
  128. package/dist/providers/vercel/tools.js +4 -3
  129. package/dist/registry.d.ts +10 -4
  130. package/dist/registry.js +41 -13
  131. package/dist/routes/credentials.js +129 -60
  132. package/dist/routes/dispatch.js +5 -1
  133. package/dist/routes/mcp.js +30 -14
  134. package/dist/routes/operator.js +2 -2
  135. package/dist/routes/shared.d.ts +6 -1
  136. package/dist/routes/shared.js +7 -1
  137. package/dist/routes/ui-config.js +2 -2
  138. package/dist/routes/ui-data.js +126 -20
  139. package/dist/routes/ui.js +16 -0
  140. package/dist/runtime/call.js +7 -2
  141. package/dist/runtime/native-storage.js +12 -0
  142. package/dist/runtime/request-reads.js +207 -0
  143. package/dist/runtime/request-scope.js +16 -0
  144. package/dist/runtime/storage.js +12 -0
  145. package/dist/skills.js +37 -20
  146. package/dist/storage/keys.js +53 -3
  147. package/dist/storage/memory.js +8 -2
  148. package/dist/storage/sql-native.js +53 -1
  149. package/dist/storage/sql.js +12 -2
  150. package/dist/storage-digest.js +32 -0
  151. package/dist/timeout.js +8 -0
  152. package/dist/types.d.ts +33 -0
  153. package/dist/usage-guide.js +16 -35
  154. package/dist/validate.d.ts +3 -0
  155. package/dist/validate.js +220 -158
  156. package/dist/version.d.ts +1 -1
  157. package/dist/version.js +1 -1
  158. package/dist/write-outcome.js +0 -3
  159. package/documentation/README.md +13 -13
  160. package/documentation/architecture.md +385 -20
  161. package/documentation/auth.md +233 -64
  162. package/documentation/code-mode.md +122 -39
  163. package/documentation/deploying.md +6 -3
  164. package/documentation/integrating.md +16 -2
  165. package/documentation/meta-tools.md +240 -49
  166. package/documentation/migration-0.33.md +5 -1
  167. package/documentation/migration-0.34.md +3 -1
  168. package/documentation/operator-tests.md +1 -1
  169. package/documentation/operator-ui.md +105 -12
  170. package/documentation/pool-membership-upgrade.md +44 -0
  171. package/documentation/providers.md +105 -0
  172. package/examples/worker/AGENTS.md +10 -10
  173. package/examples/worker/README.md +4 -3
  174. package/examples/worker/src/connecta.config.ts +1 -0
  175. package/guides/connecting-agents.md +2 -1
  176. package/guides/how-it-works.md +4 -2
  177. package/package.json +1 -1
  178. package/templates/node/AGENTS.md +13 -12
  179. package/templates/node/package.json +1 -1
  180. package/templates/node/src/connecta.config.ts +1 -0
@@ -47,7 +47,8 @@ as the isolate — on Workers a lazy module-scope singleton, which is why both
47
47
  deployment shapes construct it outside the request handler.
48
48
 
49
49
  An OAuth connector, `remoteMcp()` or `api()` with a static grant, owns a
50
- 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`.
51
52
  Joined scopes share the outcome while keeping their own request I/O and
52
53
  cancellation. Grant use and the caller's wait belong to the call. Renewal
53
54
  preparation is call-owned only until the first durable coordinator write
@@ -99,6 +100,18 @@ never retried and requires re-consent. Valid rotations retry their grant commit
99
100
  up to 32 times without another HTTP request; exhausted commits and all dispatched
100
101
  provider failures require re-consent. [Auth](./auth.md#refresh-failures) describes the lease and failure
101
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.
102
115
 
103
116
  **Per request, and no longer.** The MCP server, its transport, downstream MCP
104
117
  clients, abort signals, and the connector scope a probe opens all belong to the
@@ -107,7 +120,10 @@ across requests on Workers is a cross-request capability leak, and a promise awa
107
120
  work the runtime may already have torn down. Deferred work has one sanctioned
108
121
  channel, `ctx.waitUntil`, threaded through `fetch(request, env, ctx)` — activity
109
122
  writes and OAuth renewals from claim submission use it. Catalog listing and SQL cache
110
- 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
111
127
  `McpServer` per request is what makes the deployment stateless: no sessions, no
112
128
  server push, no stream resumability, scope resolved rather than remembered.
113
129
 
@@ -324,7 +340,7 @@ without storage reads, decryption or hashing. Catalog invalidation (including
324
340
  operator refresh and OAuth connect/disconnect/restart) advances a counter for the
325
341
  registry's cache store; built-in vault set/clear advances a counter for that vault.
326
342
  These weakly held counters contain no catalogs, credentials or caller identity;
327
- catalogs and derived views remain request-local. A moved fence replaces the
343
+ registry catalogs and derived views remain request-local. A moved fence replaces the
328
344
  retained read and its search views, and prevents joining an older registry flight.
329
345
  Changes made through another process/isolate or directly in storage need not be
330
346
  observed mid-request; the next request uses the durable generation and auth checks.
@@ -603,6 +619,176 @@ Only complete, intake-redacted tools/list, resources/list and
603
619
  resources/templates/list successes are stored. Resource payloads are not cached. SDK default
604
620
  in-memory caching is replaced, so raw catalogs never enter it.
605
621
 
622
+ A warm isolate retains at most four validated stored JSON catalogs, with a
623
+ combined accounting budget of 8 MiB (twice serialized input bytes, key bytes and
624
+ 64 bytes per object property/array slot; larger entries bypass reuse).
625
+ These deeply frozen facts contain no request context, transport, signal,
626
+ callback or promise, and require neither weak references nor cleanup timers.
627
+ Reuse follows the same manifest TTL checks, complete chunk reads, recomputed
628
+ SHA-256 fingerprint and fresh generation/credential acceptance fence as a cold
629
+ read. Its key binds the stored entry, current private owner/deployment partition
630
+ (even for public entries), and chunk fingerprint. Request-specific credential
631
+ redaction and TTL intake still run every time. Eviction affects performance
632
+ only; SQL remains authoritative.
633
+
634
+ Stored catalog and negotiation keys use deployment-keyed partitions. A remote
635
+ connector's whole cache configuration and negotiation input enter a stored key
636
+ only through HMAC-SHA-256 (`src/storage-digest.ts`) under a key that never
637
+ enters storage: every configured URL in full (the endpoint, a client metadata
638
+ document, a static client's issuer and the redirect URI), static auth header
639
+ values, resource headers, and a static OAuth client's secret. The
640
+ operator-managed or request-resolved credential (joined with the forwarded
641
+ principal), a custom connector's credential values, and the OAuth token
642
+ response that fences a listing are keyed the same way. The key is the vault's
643
+ `storageDigestKey()`, which `encryptedCredentialVault()` derives with HKDF from
644
+ its credential key. Without a vault, or with a custom vault that lacks the
645
+ method, a random key held only in the process is used: the partitions stay
646
+ exact, but another process or isolate addresses different entries and fills
647
+ its own cache, so cross-isolate reuse of remote catalogs, verdicts and health
648
+ records needs a vault. A different URL, header, credential, token response or
649
+ epoch addresses a different entry at once, including a value changed by
650
+ redeploy, and an earlier entry is never served under the new value; rotating
651
+ back to an earlier value within its TTL finds its entries again. A new
652
+ deployment key addresses new entries. Entries written by earlier releases are
653
+ not read and expire on their own TTLs (catalogs within `catalogMaxTtlSeconds`,
654
+ refresh baselines within 48 hours, verdicts within six hours), so each remote
655
+ connector's partitions refill once after the upgrade with one handshake and
656
+ listing.
657
+
658
+ Warm remote resolution batches the negotiation verdict and locating generation
659
+ when their physical keys share an observed SQL store. Private and public
660
+ manifest probes share a `getMany` statement; their normal precedence and public
661
+ scope check still apply. These batches contain only reads. Catalog addresses
662
+ use the listing's pinned OAuth identity and locating generation; they authorize
663
+ nothing. After chunk verification, acceptance re-reads the OAuth identity and
664
+ generation separately and refuses a changed epoch or token digest. The invocation
665
+ gate checks disconnect first, then reads tokens separately to refuse absent
666
+ credentials and observe current expiry and the invalidation fingerprint. Token
667
+ removal, expiry, scope and replacement can change without advancing the epoch;
668
+ epoch fences alone cannot protect these values. The SDK's later token read may
669
+ return no tokens and omit Authorization, so it does not replace invocation's
670
+ token-presence refusal. The transport also refuses tokenless MCP requests on
671
+ passive OAuth paths, regardless of provider history; an explicit authorization
672
+ start may probe for the SDK's challenge. A refused tool send binds its private
673
+ failure provenance to the undispatched physical request. Invocation accepts it
674
+ only for the owning boundary and whole logical call; error fields, copied
675
+ connectors, and refusals after a send cannot supply that proof.
676
+ Connection reuse and SDK dispatch retain their fresh
677
+ epoch or token reads; no credential read is prefetched.
678
+ Stores with replaced methods or separate backing stores retain individual reads.
679
+
680
+ A remote connector's `listTools` without a live connection in its request
681
+ reads this cache before connecting. It derives the same partition a connect
682
+ binds (the admitted principal, the caller's credential digest, or the owner's
683
+ OAuth epoch and token digest) from storage reads only, then makes the SDK's
684
+ two-probe read with the pre-connect empty server identity: the private slot,
685
+ then a shared connector's public slot, served only when written public. OAuth
686
+ grants are read by a non-mutating snapshot: no plaintext grant is sealed in
687
+ place, no epoch is bound, nothing is refreshed. Every identity check during
688
+ cache acceptance reads the grant again and refuses the entry unless the epoch
689
+ its partition was derived from and the token digest both still hold. A missing
690
+ or expired generation is a miss at every check and is never claimed or
691
+ recreated. An unexpired complete entry is returned with no transport,
692
+ handshake, session cleanup or storage write. A miss, an expired entry, an
693
+ explicit `ttlMs: 0`, an invalidated generation, a credential, token or epoch
694
+ change, an operator-disconnected or token-less grant, a cache read failure,
695
+ and a downstream whose stored negotiation verdict is modern all connect as
696
+ before. Modern downstreams connect without a handshake and read the cache
697
+ through the SDK. The connect reuses the credential and negotiation verdict that
698
+ read resolved. `list_changed` arrives only over a live connection, so an
699
+ unconnected catalog changes at its TTL or on invalidation.
700
+
701
+ A catalog served from this cache, before or after connecting, does not
702
+ validate downstream credential health. An expired or vendor-revoked credential
703
+ can still expose cached tools, and discovery reports no auth-related
704
+ `catalogErrors` entry for it until the cache misses. Tool calls and resource
705
+ reads reach the downstream and report auth failures with their handoffs. A
706
+ status probe connects: a legacy downstream answers a handshake, while a modern
707
+ downstream with a stored verdict connects without a request. A credential test
708
+ always sends a live `tools/list` that bypasses the cache in both directions.
709
+
710
+ In `"auto"` mode a remote connector stores its negotiation verdict, data only,
711
+ in the `negotiation` key family, partitioned by connector configuration,
712
+ credential and OAuth epoch, with secret-bearing inputs keyed as above. A later connect reuses it instead of probing with
713
+ `server/discover`. A modern verdict lasts five minutes. A legacy verdict lasts
714
+ six hours, because a server that adds the modern era still answers the legacy
715
+ handshake. Two signals end a verdict early:
716
+
717
+ - A legacy-verdict handshake refused with a 4xx other than 401, 403 or 429 (a
718
+ modern-only server answers 400 with its supported versions) is negotiated
719
+ again at once in the same connect. The new verdict replaces the old.
720
+ - On a modern connection (adopted from the verdict or just negotiated), an
721
+ HTTP 400 is read through a bounded clone of its body, as the transport's
722
+ backward-compatibility rule requires. A modern server also answers 400 with
723
+ `HeaderMismatch` (-32020) and `MissingRequiredClientCapability` (-32021),
724
+ which concern the request alone and keep the verdict and the connection.
725
+ `UnsupportedProtocolVersion` (-32022) keeps them only while its
726
+ `data.supported` list (at most 64 strings) still names the request's
727
+ `MCP-Protocol-Version`; a missing, malformed or other list makes the verdict
728
+ unusable. Any other body, empty or not a recognized modern error (a legacy
729
+ server's sessionless refusal, a gateway's page), is a legacy refusal. Either
730
+ way the verdict is deleted and that client retired. The refused request
731
+ fails and is never repeated. The next connect, in the same request or a
732
+ later one, probes again. The clone read ends with the call, the request or
733
+ the connection scope: an abort cancels it, cancels the unread original and
734
+ takes no evidence (INV-7).
735
+
736
+ An upgrade to a server that still answers the legacy handshake is therefore
737
+ adopted within six hours. Losing the legacy handshake costs one refused
738
+ request, and a downgrade costs the one request that met the refusal.
739
+
740
+ **Discovery health.** An unscoped search (`search_tools` or `connecta.search`
741
+ without `connector`) probes each connector for at most 5,000 ms, or
742
+ `discovery.probeTimeoutMs` when that is shorter, and its probes end within a
743
+ 9,000 ms discovery budget. A tenth of the budget, at most 500 ms, is kept for
744
+ recording outcomes: a probe that starts late gets only what remains before
745
+ that slice, and one that cannot start reports `timeout`. Health reads, claims
746
+ and writes start only while budget remains and the caller is still waiting;
747
+ health is otherwise unknown. Once started, each is awaited to its end before
748
+ the search returns, never raced or detached, since a storage write cannot be
749
+ withdrawn once sent. The budget bounds when health work starts, not when it
750
+ finishes, so slow storage can carry a search past it by one operation's
751
+ latency. A scoped search,
752
+ `connecta.describe`, native `tools/list` and tool calls keep the full
753
+ `discovery.probeTimeoutMs`. A probe that times out on its own deadline, or a
754
+ retryable failure that itself took at least one second or named its own wait,
755
+ writes a record in the `connector-health:v1:` family: the connecta
756
+ classification code, observation time, retry time (60 seconds later, or the
757
+ downstream's shorter named wait, at least one second), and the connector's
758
+ catalog invalidation generation. Each probe's time is its own, measured as it
759
+ settles. Records hold no downstream text, tools, arguments or credentials.
760
+ Each is keyed by connector id and the deployment-keyed digest (as for the
761
+ catalog cache above) of the catalog owner partition (principal, subject,
762
+ caller identity and pool, origin and public URL), the admitted endpoint (plain
763
+ `/mcp`, `/mcp/<pool>` or `/agent/<pool>`), the connector's described
764
+ configuration, whose endpoint keeps its path, and the package version. One owner's
765
+ or endpoint's record never skips another's search. A change `describe()`
766
+ cannot see, such as a static header rotated in deployment code, takes effect
767
+ on redeploy; a record written before it keeps skipping for at most its retry
768
+ time, about 60 seconds. Until its retry time an unscoped search skips that connector
769
+ and reports it in `catalogErrors` with the recorded code, `retryable: true`,
770
+ `retryAfterMs`, and a fixed message that names the scoped search to try it
771
+ now; no tools from the failed walk are ever served. A scoped search still
772
+ probes and settles the record. A record whose generation no longer matches
773
+ lapses, so a vault credential change, OAuth connect or disconnect, or operator
774
+ invalidation clears every partition at once. A success, or an auth or
775
+ permission failure that shows the connector answers, clears the record this
776
+ request saw. Caller cancellation, a probe the budget cut short, a fast
777
+ failure, and a non-retryable failure record nothing. After its retry time a
778
+ record lingers for 60 seconds: the request that dispatches the next probe
779
+ claims it by compare-and-set, with a lease to an absolute time, the end of
780
+ that request's discovery budget plus two seconds, so a slow claim write cannot
781
+ shorten it; the others keep skipping until it settles or the lease lapses. Health reads are one `getMany` per unscoped search, made
782
+ once per request; a health storage failure is logged and never fails
783
+ discovery.
784
+
785
+ A healthy connector whose complete catalog always takes longer than 5 seconds
786
+ to list can therefore stay out of unscoped discovery: each unscoped probe
787
+ times out and records it. A scoped search, or any operation with the longer
788
+ `discovery.probeTimeoutMs` deadline that completes the listing (describe, a
789
+ tool call), warms a reusable complete catalog that later unscoped searches
790
+ read without the slow listing, and a success clears the record.
791
+
606
792
  `discovery.catalogTtlSeconds` (300 by default) is the fallback for legacy
607
793
  listings without `ttlMs`. Positive hints are bounded by
608
794
  `catalogMinTtlSeconds` (0) and `catalogMaxTtlSeconds` (86400, also the SDK
@@ -653,7 +839,15 @@ activity write. The first complete catalog is a baseline.
653
839
  Hash-only baselines publish bounded chunks before a manifest. They store the
654
840
  catalog digest and hashes of each tool name and fact, so added, removed and changed counts require no persisted tool names or
655
841
  schemas. They use the exact host auth/config cache partition, survive catalog
656
- expiry and invalidation, and expire after 48 hours. There is no connection-wide
842
+ expiry and invalidation, and expire 48 hours after they are written. A refresh
843
+ reads the manifest first. When its digest matches a baseline written within the
844
+ last hour, the refresh writes nothing; an older matching baseline is rewritten
845
+ with fresh chunks, so every refresh leaves a baseline for at least 47 hours.
846
+ A manifest's lifetime is never extended without new chunks, so its chunks
847
+ always outlive it. A changed digest writes the new baseline and reports drift
848
+ against the previous one. An unchanged zero-TTL listing therefore writes no
849
+ baseline; it still lists live every time and deletes its cache slots, since a
850
+ concurrent request may have published an older reusable catalog there. There is no connection-wide
657
851
  fallback baseline across partitions. Private catalog events use the admitted
658
852
  principal actor basis even for shared-auth connectors; activity disclosure
659
853
  requires that same owner and the recorded pool's grants. Ownerless private
@@ -1128,7 +1322,11 @@ switched by the environment, and keep their entries under 30 lines.
1128
1322
  A maintained provider is one `defineProvider()` call (`src/provider.ts`): a
1129
1323
  name, title, `kind: "api"`, maintained skill, closed `options` shape and a
1130
1324
  synchronous `create`. Common values are checked before construction and the
1131
- factory stamps its provider name onto `describe().source.provider`. API
1325
+ factory stamps its provider name onto `describe().source.provider`. The kit's
1326
+ `defineApiProvider` also takes optional `searchSynonyms` groups (single
1327
+ lowercase words, at most 32 groups of 2–8), checks them at definition and
1328
+ attaches them to each connector for
1329
+ [lexical discovery](./meta-tools.md#lexical-discovery). API
1132
1330
  implementations and their contract evidence remain vendored. Hosted MCP servers
1133
1331
  use generic `remoteMcp()` entries; provider reviews and hosted defaults are
1134
1332
  removed. Legacy hosted selectors fail at construction with the generic migration
@@ -1543,7 +1741,10 @@ Use the existing stateful fixture vendors and published-edge deployment adapters
1543
1741
  Node/SQLite/QuickJS and native Workers/D1. It covers transcript-too-large
1544
1742
  recovery, empty intermediate pages, selected fields, withheld photo/credential
1545
1743
  values and shared-vault reuse after application reconstruction. No-op,
1546
- fabricated-read and erased-delivery controls must fail.
1744
+ fabricated-read and erased-delivery controls must fail. Its fake, tasks and
1745
+ deployment wiring live in `src/providers/granola/eval/`, unshipped;
1746
+ `providers:generate` registers a provider's `eval/` wiring with the harness
1747
+ (`eval/README.md`, provider-local eval wiring).
1547
1748
  - `test/identity-write-journey.node.test.ts` uses the existing Node deployment
1548
1749
  adapter, stateful MCP fake, SQLite vault and QuickJS to grade admitted human
1549
1750
  and machine identities, pool isolation, fail-closed discovery, endpoint and
@@ -1810,10 +2011,10 @@ storage is the root's namespaced to its principal.
1810
2011
  | 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. |
1811
2012
  | 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`. |
1812
2013
  | `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. |
1813
- | 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. |
2014
+ | 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`). |
1814
2015
  | 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. |
1815
- | 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. |
1816
- | 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. |
2016
+ | 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. |
2017
+ | 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. |
1817
2018
  | 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. |
1818
2019
  | 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`. |
1819
2020
 
@@ -1974,6 +2175,11 @@ value, matching that fallback. Interruption remains interruption. The lookup
1974
2175
  module imports no SQL or platform implementation and does not change the
1975
2176
  published storage interfaces.
1976
2177
 
2178
+ `NativeStorage.getMany` reads several keys in one statement, answering by
2179
+ position exactly as one `get` per key would; the SQL drivers share it, a
2180
+ replaced `get` answers each key itself, and other stores fall back to one `get`
2181
+ at a time. Catalog reads fetch up to eight chunks per round trip with it.
2182
+
1977
2183
  `openSqlite` retains file modes, WAL, busy timeout,
1978
2184
  and database ownership. `test/sql-effect-contract.ts` retains affected-row,
1979
2185
  50-claim, typed-failure, cancellation, and tracing probes on SQLite and native
@@ -2133,6 +2339,93 @@ within five minutes of expiry in the same auth partition. Partial walks and
2133
2339
  auth or permission failures refuse fallback; resources require unexpired
2134
2340
  inventories, and stale fallback cannot authorize post-entry auth recovery.
2135
2341
 
2342
+ **Repeated reads share one request's memo.** `src/runtime/request-reads.ts`
2343
+ gives each request owner a memo of storage reads, emptied when the request
2344
+ closes (INV-7). It holds plain strings keyed by the physical key in the
2345
+ underlying store, so namespaced views of one partition share an entry and two
2346
+ principals' partitions never do. A read consults it only where the call site
2347
+ opts in and states why staleness within one request is safe; every other read
2348
+ goes to storage.
2349
+
2350
+ - Every write through the SQL drivers, `memoryStorage()` or a namespaced view
2351
+ over them drops that key from every open memo in the same isolate, before
2352
+ submission and again on settlement. A read that such a write overlapped is
2353
+ not recorded.
2354
+ - Only the latest read of a key that the request started records. An earlier
2355
+ read that settles later never replaces a newer fresh observation.
2356
+ - A fresh read records what it saw, so a memoized reader is never older than
2357
+ its request's latest fresh read of the key.
2358
+ - A custom store, or a built-in one with a replaced method, is never memoized.
2359
+ - A runtime without `WeakRef` and `FinalizationRegistry` memoizes nothing:
2360
+ workerd before compatibility date 2025-05-05 without `enable_weak_ref` reads
2361
+ storage every time.
2362
+
2363
+ The memo cannot see another isolate, another process sharing the database, or
2364
+ an entry expiring. After the first read, a memoized key can lag such a change
2365
+ for the rest of the request. So nothing that gates authority, sends a
2366
+ credential, or decides a write is memoized.
2367
+
2368
+ Memoized:
2369
+
2370
+ - the negotiation verdict, which only orders handshake versions;
2371
+ - catalog generation reads that only locate an entry, accepted or refused by a
2372
+ fresh fence;
2373
+ - the credential reads behind connector status, drift and the operator UI card,
2374
+ which only label a view. Only the built-in vault with its original `getAll`
2375
+ and `metadata` shares a read; a custom vault, a subclass, or an instance
2376
+ that overrides either method is called itself for every read, dispatch
2377
+ included.
2378
+
2379
+ Always fresh:
2380
+
2381
+ - every OAuth grant read, including the token and epoch check behind each SDK
2382
+ message (a disconnect need not revoke the vendor token), `peekGrant` and
2383
+ every cache-acceptance identity check, compare-and-set loops, refresh claims
2384
+ and leases, resets and disconnects;
2385
+ - the catalog generation fence before a cached entry is served or published;
2386
+ - the credential a call sends.
2387
+
2388
+ `test/request-reads.node.test.ts` holds the memo's contract, including a
2389
+ disconnect written by another isolate between paginated `tools/list` pages.
2390
+
2391
+ ### Request budgets (contributor)
2392
+
2393
+ `test/request-reads.node.test.ts` also guards the common request shapes the way
2394
+ bundle caps guard size. Each case counts downstream HTTP requests, D1 round
2395
+ trips (`all`, `run` or `batch`) and SQL statements separately, since a batch is
2396
+ one round trip but several statements. The fixture is production-shaped: D1
2397
+ over `node:sqlite`, a sealed vault holding the API credential and the OAuth
2398
+ grant, Cloudflare Access with `accessTokens()`, a D1 activity sink settled
2399
+ through `waitUntil`, the operator UI and QuickJS standing in for Dynamic Worker
2400
+ startup. "Cold isolate" is a new deployment instance over the populated
2401
+ database. The ceilings, measured value plus about 10% D1 headroom and exact
2402
+ downstream counts, live in its `BUDGETS` table. Measured on main in October 2026:
2403
+
2404
+ | Request | Downstream | D1 round trips | SQL statements |
2405
+ | ---------------------------------------- | ---------: | -------------: | -------------: |
2406
+ | Legacy remote MCP call, warm | 5 | 6 | 7 |
2407
+ | Legacy remote MCP call, cold isolate | 5 | 9 | 12 |
2408
+ | Modern remote MCP call, warm | 1 | 7 | 8 |
2409
+ | Modern remote MCP call, cold isolate | 1 | 10 | 13 |
2410
+ | OAuth remote MCP call, warm | 5 | 19 | 20 |
2411
+ | OAuth remote MCP call, cold isolate | 5 | 22 | 25 |
2412
+ | OAuth call after a 401, with refresh | 9 | 39 | 48 |
2413
+ | OAuth call with proactive refresh | 6 | 35 | 43 |
2414
+ | `api()` credential call, warm | 1 | 3 | 4 |
2415
+ | `api()` credential call, cold isolate | 1 | 6 | 9 |
2416
+ | Unscoped search, warm (3 remote, 1 api) | 0 | 20 | 20 |
2417
+ | `execute_code`, 5 calls to one connector | 9 | 10 | 15 |
2418
+ | UI connector detail, `api()` credential | 0 | 2 | 2 |
2419
+ | UI connector detail, credential remote | 4 | 9 | 9 |
2420
+ | UI connector detail, OAuth remote | 4 | 18 | 18 |
2421
+
2422
+ Header-auth calls to the legacy (session) server handshake per request:
2423
+ `initialize`, `notifications/initialized`, the SSE `GET`, the call and the
2424
+ session `DELETE`. The modern server advertises a catalog TTL, so a warm call is
2425
+ the call alone. OAuth calls read the grant fresh before every SDK message.
2426
+ Run with `D1_BUDGET_REPORT=1` to print each measurement; a failing case prints
2427
+ its request and statement log.
2428
+
2136
2429
  ### Contributor provider kit
2137
2430
 
2138
2431
  Provider declarations use `src/http/vendor.ts` for HttpApi operations and
@@ -2151,17 +2444,29 @@ exact vendor count as `page.total` from its page callback. The kit validates a
2151
2444
  nonnegative safe integer and includes it on continuing and terminal pages;
2152
2445
  without a vendor count the property is absent. Lists without the opt-in keep
2153
2446
  their existing schema and result bytes. Never derive an exact total from a
2154
- partial page or an estimate. `src/http/fields.ts` refuses an unknown first
2155
- segment before dispatch; a selected
2156
- result renders against a sparse output schema. `src/http/hatch.ts` is the
2447
+ partial page or an estimate. `src/http/fields.ts` derives each `fields`
2448
+ description's anchor and example from the contract that validates it. It
2449
+ refuses before dispatch a path whose first segment is not a declared property,
2450
+ and a deeper segment only inside an object the schema closes with
2451
+ `additionalProperties: false`; a selected result renders against a sparse
2452
+ output schema. `src/http/hatch.ts` is the
2157
2453
  standard hatch (`_api_search`, `_api_details`, `_api_read`, `_api_write`) over
2158
2454
  an `OperationIndex` with explicit verdicts. It keeps the REST family's
2159
2455
  argument and envelope names and validates `fields` against the operation's
2160
- declared response roots before dispatch. Search answers stay within 16 KiB
2456
+ declared response roots before dispatch. A deeper miss is reported in
2457
+ bounded `warnings` naming declared properties only, never refused, because the
2458
+ pinned index declares paths only to a bounded depth. Search answers stay within 16 KiB
2161
2459
  (at most eight compact entries, long labels cut and marked, continuation by
2162
2460
  cursor); details stay within 32 KiB, paging whole parameters and body
2163
2461
  properties, and chunking any piece larger than one answer, behind a bound
2164
- cursor. Standard hatch search removes conversational framing; framing-only
2462
+ cursor. `src/http/hatch-response.ts` builds details' capped `response` section
2463
+ from the index's declared response paths in `fields` coordinates. It lists
2464
+ selectable paths and promises no readability. Its `withheld` hint comes from
2465
+ `ResultSafety.withholds`: `ValueSafety.withholds` reads the rules `redact`
2466
+ applies that change every non-null value they reach, matched with list
2467
+ markers. Custom redactions may declare their own hint. Rules that can pass a
2468
+ value through (`url:`, `origin:`, `[?…]`, `@keys`, string-only scrubbing) are
2469
+ never hints. Standard hatch search removes conversational framing; framing-only
2165
2470
  queries return no matches. It admits partial lexical matches alongside full
2166
2471
  matches. Action/read queries require all content nouns in the operation text,
2167
2472
  or any noun in the path/id with at least half the nouns in the text. Their
@@ -2308,9 +2613,10 @@ A binding may opt into `truncated: true` to supply reviewed size-cut paths
2308
2613
  (such as `data.content`) in its answer. The hatch then declares and propagates
2309
2614
  an optional top-level `truncated: string[]` on reads and writes. Without the
2310
2615
  opt-in, the schemas and results are unchanged; size-cut metadata is not inferred
2311
- from vendor booleans or added to other providers. A separate `warnings: true`
2312
- opt-in declares and preserves reviewed `warnings: string[]` beside `data`, even
2313
- when `fields` narrows the vendor body.
2616
+ from vendor booleans or added to other providers. Every hatch read and write
2617
+ declares an optional `warnings: string[]` for `fields` entries that matched
2618
+ nothing in a nonempty body. A separate `warnings: true` opt-in also preserves
2619
+ reviewed vendor warnings there, even when `fields` narrows the vendor body.
2314
2620
 
2315
2621
  A binding may expand deployment-owned path defaults (Cloudflare) or canonicalize
2316
2622
  repository contents paths (GitHub) before indexed resolution, and may prepare a
@@ -2839,6 +3145,51 @@ Effect is pinned exactly; deployments using Effect must resolve the same version
2839
3145
  Per-provider bundles are reported, not capped, because their standalone graphs
2840
3146
  include the shared core again. Deployed non-provider shapes are capped by
2841
3147
  `scripts/bundle-budget.json`.
3148
+ The always-loaded agent surface has fixed-cost caps of the same kind: the
3149
+ `always-loaded agent surface budgets` cases in
3150
+ `test/meta-tool-schemas.test.ts` cap the instructions plus `tools/list` total,
3151
+ each tool description and output schema, the `usage` skill, and one default
3152
+ and one compact search row, measured on a pinned four-connector fixture.
3153
+
3154
+ ### Route names
3155
+
3156
+ Connecta-authored agent text names a route in the wording of the surface that
3157
+ serves it, resolved when the text is built (`src/agent-routes.ts`). Nothing
3158
+ renders afterwards: no code scans or rewrites replies, program values,
3159
+ downstream results, resources, guides or caller data, so paging, previews,
3160
+ totals and offsets measure the final text.
3161
+
3162
+ Two tables per surface. `AgentRouteNames` names the tools (`call_tool`,
3163
+ `call_destructive_tool`, `search_tools`, `execute_code`, `authorize_connector`,
3164
+ `skills` on the meta surface); a sentence uses a name only when written for a
3165
+ surface that lists that tool. `RoutePhrases` holds complete phrases for
3166
+ sentences every surface shares, such as "for a direct call_tool only" against
3167
+ "when calling this tool directly", "call directly (call_tool, connecta.result
3168
+ to page)" against "call this tool directly and page with read_result", and the
3169
+ authorization sentences, which on native pools tell the agent to give the
3170
+ error's nextAction to the user or operator. `program` is undefined on a native
3171
+ pool without `execute_code`, so text about program capacity branches on it.
3172
+ The eval code arm's phrases keep the text its comparator protocol recorded.
3173
+
3174
+ Where each text gets its wording:
3175
+
3176
+ | Text | Wording |
3177
+ | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3178
+ | Invocation refusals, authorization handoffs, paging notices | The request's names, from the MCP route through meta-tools, programs and native pools |
3179
+ | 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` |
3180
+ | Contexts the registry builds | The deployment's names (meta, or the eval code arm's); a call carries its request's names |
3181
+ | 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 |
3182
+ | Meta instructions, meta-only tool descriptions, built-in guides | Meta names, built once; built-in guides keep one digest across deployments |
3183
+ | Native instructions, the `execute_code` description | Built per surface |
3184
+ | Program-return and program-call paging notes | Meta names; they name only `execute_code`, which every surface running programs lists |
3185
+ | The OAuth adapter's `UnauthorizedError` | Meta names; only its class reaches callers, which word their own error from the call |
3186
+
3187
+ The provider standard's S7 check refuses a spelled-out meta-tool name in
3188
+ provider code. Remaining literals are not agent route text: `execute_code`'s own
3189
+ budget and timeout errors, operator log lines, construction errors, the
3190
+ configuration reference, the OAuth callback page and operator fix prompts. The
3191
+ eval code arm marks its configuration with `renderRoutesAsPrograms`, an internal
3192
+ hook no package entry exports; no configuration key selects it.
2842
3193
 
2843
3194
  [Decision 0019](https://github.com/zackbart/connecta/blob/main/decisions/0019-core-validation-http-boundaries.md) records the rejected Schema/HttpApi measurements.
2844
3195
 
@@ -2973,7 +3324,10 @@ compiling and configuring the real thing.
2973
3324
  is a known failure only on refusal provenance recorded where the evidence is
2974
3325
  observed, never inferred from an error code: (a) the ledger proves it never
2975
3326
  left, or (b) the downstream served an HTTP refusal, a 4xx other than 408
2976
- (`refusedStatus` and `vendorRefusal` in `src/provider-call.ts`), under the
3327
+ (`refusedStatus` and `vendorRefusal` in `src/provider-call.ts`); for
3328
+ `remoteMcp()` only when that 4xx carries a JSON-RPC error body, since a bare
3329
+ one may be a gateway's answer after the server acted (`ambiguous` on the
3330
+ call's dispatch in `src/connectors/remote-mcp/transport.ts`), under the
2977
3331
  provider's existing served classification (a write's HTTP 409 is a `conflict`). One helper, `servedRefusal` in `src/write-outcome.ts`, decides
2978
3332
  (b) for the ledger and for a transport outside one: a refusing status the
2979
3333
  provider reads as `unavailable` or a timeout (Stripe's 424) is not proof, so
@@ -3419,8 +3773,19 @@ change. All 88 declarations still exclude Effect and have complete references.
3419
3773
  ### Native remote MCP programs (contributor)
3420
3774
 
3421
3775
  Remote MCP connection joins, credential and epoch checks, catalog intake,
3422
- negotiation, calls, resources, Skills, auth and status compose native programs in
3423
- `connectors/remote-mcp.ts`. `catalog-cache.ts` owns native SDK cache reads,
3776
+ negotiation, calls, resources, Skills, auth and status compose native programs.
3777
+ `connectors/remote-mcp.ts` keeps the published options, the redirect guard and
3778
+ `remoteMcp()`, which builds one runtime from per-seam factories in
3779
+ `connectors/remote-mcp/`: `config.ts` (options and validation), `errors.ts`
3780
+ (boundary error mapping), `transport.ts` (transport and write-once dispatch),
3781
+ `connection.ts` (the per-scope connection state machine), `catalog.ts`
3782
+ (catalog intake), `resources.ts` (Skills and resource reads), `oauth.ts` (the
3783
+ OAuth surface) and `call.ts` (`callTool`); `runtime.ts` types what they share.
3784
+ `auth/downstream-oauth.ts` keeps `KvOAuthProvider`'s state and declared
3785
+ methods; `auth/downstream-oauth/` holds the refresh coordinator and, by seam,
3786
+ the provider's native method bodies (grant and epoch store, consent records,
3787
+ refresh lease and spent records, SDK hooks), installed once each by
3788
+ `authMethods`. `catalog-cache.ts` owns native SDK cache reads,
3424
3789
  chunked publication, generation fencing, complete refresh acceptance and listing
3425
3790
  serialization; `connectors/negotiation-cache.ts` owns native negotiation storage.
3426
3791
  The published connector, cache and SDK callback methods remain Promise shells.