@zackbart/connecta 0.38.0 → 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 (177) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +3 -2
  3. package/bin/version.mjs +1 -1
  4. package/dist/agent-routes.js +231 -0
  5. package/dist/auth/downstream-oauth/consent.js +519 -0
  6. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  7. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  8. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  9. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  10. package/dist/auth/downstream-oauth.js +66 -2382
  11. package/dist/auth/native.js +27 -0
  12. package/dist/auth/oauth-grant.js +13 -0
  13. package/dist/auth/static-oauth.js +153 -131
  14. package/dist/auth-elicitation.js +7 -1
  15. package/dist/authorization-handoff.js +11 -5
  16. package/dist/catalog-cache.js +130 -43
  17. package/dist/catalog-policy.js +18 -0
  18. package/dist/catalog-service.js +284 -47
  19. package/dist/catalog.js +18 -2
  20. package/dist/config.d.ts +16 -11
  21. package/dist/config.js +9 -6
  22. package/dist/connector-caller.d.ts +1 -1
  23. package/dist/connector-health.js +59 -0
  24. package/dist/connector-status.js +117 -0
  25. package/dist/connectors/api-connector.d.ts +7 -2
  26. package/dist/connectors/api-connector.js +7 -4
  27. package/dist/connectors/negotiation-cache.js +22 -4
  28. package/dist/connectors/remote-mcp/call.js +164 -0
  29. package/dist/connectors/remote-mcp/catalog.js +278 -0
  30. package/dist/connectors/remote-mcp/config.js +263 -0
  31. package/dist/connectors/remote-mcp/connection.js +671 -0
  32. package/dist/connectors/remote-mcp/errors.js +488 -0
  33. package/dist/connectors/remote-mcp/oauth.js +469 -0
  34. package/dist/connectors/remote-mcp/resources.js +473 -0
  35. package/dist/connectors/remote-mcp/runtime.js +1 -0
  36. package/dist/connectors/remote-mcp/transport.js +544 -0
  37. package/dist/connectors/remote-mcp.js +59 -2863
  38. package/dist/credential-contract.d.ts +23 -0
  39. package/dist/credential-reads.js +17 -0
  40. package/dist/credential-verification.js +53 -0
  41. package/dist/credentials.d.ts +7 -0
  42. package/dist/credentials.js +60 -25
  43. package/dist/describe-config.d.ts +12 -2
  44. package/dist/describe-config.js +2 -0
  45. package/dist/discovery-health.js +199 -0
  46. package/dist/downstream-input.js +2 -0
  47. package/dist/errors.d.ts +55 -17
  48. package/dist/errors.js +95 -41
  49. package/dist/execute.js +152 -114
  50. package/dist/executor-result.js +3 -1
  51. package/dist/guest-functions.js +83 -0
  52. package/dist/guest-runtime.js +60 -19
  53. package/dist/guest-types.d.ts +19 -5
  54. package/dist/http/argument-problems.js +35 -0
  55. package/dist/http/errors.js +5 -4
  56. package/dist/http/fields.js +192 -13
  57. package/dist/http/hatch-response.js +73 -0
  58. package/dist/http/hatch.js +88 -23
  59. package/dist/http/oauth.js +3 -2
  60. package/dist/http/provider.js +32 -4
  61. package/dist/http/safety.js +8 -0
  62. package/dist/http/tools.js +44 -23
  63. package/dist/index.d.ts +1 -1
  64. package/dist/index.js +9 -2
  65. package/dist/invocation.js +21 -6
  66. package/dist/membership.d.ts +13 -5
  67. package/dist/membership.js +34 -2
  68. package/dist/meta-tools.js +23 -8
  69. package/dist/native-recovery.js +4 -16
  70. package/dist/native-tools.js +4 -1
  71. package/dist/oauth-sealing.js +10 -0
  72. package/dist/operation-vocabulary.js +135 -13
  73. package/dist/operator-record.d.ts +2 -2
  74. package/dist/operator-record.js +1 -0
  75. package/dist/operator-ui/contract.d.ts +10 -0
  76. package/dist/operator-ui/generated.js +2 -2
  77. package/dist/operator-ui/model.d.ts +22 -1
  78. package/dist/providers/_shared/rest/operation-index.js +48 -50
  79. package/dist/providers/_shared/rest/value-safety.js +80 -0
  80. package/dist/providers/breeze/api.js +7 -1
  81. package/dist/providers/breeze/skill.generated.js +1 -1
  82. package/dist/providers/breeze/tools.js +4 -3
  83. package/dist/providers/ccb/api.js +2 -1
  84. package/dist/providers/ccb/index.js +5 -0
  85. package/dist/providers/ccb/skill.generated.js +1 -1
  86. package/dist/providers/cloudflare/api.js +8 -1
  87. package/dist/providers/cloudflare/skill.generated.js +1 -1
  88. package/dist/providers/docs/lib/schemas.js +24 -22
  89. package/dist/providers/docs/safety.js +2 -0
  90. package/dist/providers/docs/skill.generated.js +1 -1
  91. package/dist/providers/docs/tools.js +2 -4
  92. package/dist/providers/drive/api.js +1 -0
  93. package/dist/providers/drive/lib/contracts.js +8 -5
  94. package/dist/providers/drive/skill.generated.js +1 -1
  95. package/dist/providers/drive/tools.js +11 -16
  96. package/dist/providers/forms/api.js +1 -0
  97. package/dist/providers/forms/skill.generated.js +1 -1
  98. package/dist/providers/forms/tools.js +10 -7
  99. package/dist/providers/github/api.js +1 -0
  100. package/dist/providers/github/lib/discovery.js +1 -1
  101. package/dist/providers/github/skill.generated.js +1 -1
  102. package/dist/providers/github/tools.js +61 -17
  103. package/dist/providers/gmail/lib/schemas.js +60 -50
  104. package/dist/providers/gmail/skill.generated.js +1 -1
  105. package/dist/providers/gmail/tools.js +7 -5
  106. package/dist/providers/granola/index.js +2 -0
  107. package/dist/providers/granola/skill.generated.js +1 -1
  108. package/dist/providers/overflow/api.js +7 -1
  109. package/dist/providers/overflow/skill.generated.js +1 -1
  110. package/dist/providers/planning-center/api.js +3 -1
  111. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  112. package/dist/providers/planning-center/skill.generated.js +1 -1
  113. package/dist/providers/quickbooks/safety.js +80 -39
  114. package/dist/providers/quickbooks/skill.generated.js +1 -1
  115. package/dist/providers/sheets/api.js +1 -0
  116. package/dist/providers/sheets/skill.generated.js +1 -1
  117. package/dist/providers/sheets/tools.js +13 -14
  118. package/dist/providers/slides/skill.generated.js +1 -1
  119. package/dist/providers/slides/tools.js +15 -13
  120. package/dist/providers/stripe/skill.generated.js +1 -1
  121. package/dist/providers/tithely/api.js +3 -1
  122. package/dist/providers/tithely/skill.generated.js +1 -1
  123. package/dist/providers/vercel/api.js +1 -1
  124. package/dist/providers/vercel/skill.generated.js +1 -1
  125. package/dist/providers/vercel/tools.js +4 -3
  126. package/dist/registry.d.ts +10 -4
  127. package/dist/registry.js +41 -13
  128. package/dist/routes/credentials.js +129 -60
  129. package/dist/routes/dispatch.js +5 -1
  130. package/dist/routes/mcp.js +30 -14
  131. package/dist/routes/operator.js +2 -2
  132. package/dist/routes/shared.d.ts +6 -1
  133. package/dist/routes/shared.js +7 -1
  134. package/dist/routes/ui-config.js +2 -2
  135. package/dist/routes/ui-data.js +126 -20
  136. package/dist/routes/ui.js +16 -0
  137. package/dist/runtime/call.js +7 -2
  138. package/dist/runtime/native-storage.js +9 -0
  139. package/dist/runtime/request-reads.js +182 -0
  140. package/dist/runtime/request-scope.js +16 -0
  141. package/dist/runtime/storage.js +12 -0
  142. package/dist/skills.js +37 -20
  143. package/dist/storage/keys.js +53 -3
  144. package/dist/storage/memory.js +8 -2
  145. package/dist/storage/sql-native.js +27 -0
  146. package/dist/storage/sql.js +12 -2
  147. package/dist/storage-digest.js +32 -0
  148. package/dist/timeout.js +8 -0
  149. package/dist/types.d.ts +33 -0
  150. package/dist/usage-guide.js +16 -35
  151. package/dist/validate.d.ts +3 -0
  152. package/dist/validate.js +220 -158
  153. package/dist/version.d.ts +1 -1
  154. package/dist/version.js +1 -1
  155. package/dist/write-outcome.js +0 -3
  156. package/documentation/README.md +13 -13
  157. package/documentation/architecture.md +350 -19
  158. package/documentation/auth.md +217 -63
  159. package/documentation/code-mode.md +122 -39
  160. package/documentation/deploying.md +6 -3
  161. package/documentation/integrating.md +15 -1
  162. package/documentation/meta-tools.md +220 -46
  163. package/documentation/migration-0.33.md +5 -1
  164. package/documentation/migration-0.34.md +3 -1
  165. package/documentation/operator-tests.md +1 -1
  166. package/documentation/operator-ui.md +105 -12
  167. package/documentation/pool-membership-upgrade.md +44 -0
  168. package/documentation/providers.md +105 -0
  169. package/examples/worker/AGENTS.md +10 -10
  170. package/examples/worker/README.md +4 -3
  171. package/examples/worker/src/connecta.config.ts +1 -0
  172. package/guides/connecting-agents.md +2 -1
  173. package/guides/how-it-works.md +4 -2
  174. package/package.json +1 -1
  175. package/templates/node/AGENTS.md +13 -12
  176. package/templates/node/package.json +1 -1
  177. 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
 
@@ -603,6 +619,142 @@ 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
+ 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
+
606
758
  `discovery.catalogTtlSeconds` (300 by default) is the fallback for legacy
607
759
  listings without `ttlMs`. Positive hints are bounded by
608
760
  `catalogMinTtlSeconds` (0) and `catalogMaxTtlSeconds` (86400, also the SDK
@@ -653,7 +805,15 @@ activity write. The first complete catalog is a baseline.
653
805
  Hash-only baselines publish bounded chunks before a manifest. They store the
654
806
  catalog digest and hashes of each tool name and fact, so added, removed and changed counts require no persisted tool names or
655
807
  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
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
657
817
  fallback baseline across partitions. Private catalog events use the admitted
658
818
  principal actor basis even for shared-auth connectors; activity disclosure
659
819
  requires that same owner and the recorded pool's grants. Ownerless private
@@ -1128,7 +1288,11 @@ switched by the environment, and keep their entries under 30 lines.
1128
1288
  A maintained provider is one `defineProvider()` call (`src/provider.ts`): a
1129
1289
  name, title, `kind: "api"`, maintained skill, closed `options` shape and a
1130
1290
  synchronous `create`. Common values are checked before construction and the
1131
- 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
1132
1296
  implementations and their contract evidence remain vendored. Hosted MCP servers
1133
1297
  use generic `remoteMcp()` entries; provider reviews and hosted defaults are
1134
1298
  removed. Legacy hosted selectors fail at construction with the generic migration
@@ -1543,7 +1707,10 @@ Use the existing stateful fixture vendors and published-edge deployment adapters
1543
1707
  Node/SQLite/QuickJS and native Workers/D1. It covers transcript-too-large
1544
1708
  recovery, empty intermediate pages, selected fields, withheld photo/credential
1545
1709
  values and shared-vault reuse after application reconstruction. No-op,
1546
- 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).
1547
1714
  - `test/identity-write-journey.node.test.ts` uses the existing Node deployment
1548
1715
  adapter, stateful MCP fake, SQLite vault and QuickJS to grade admitted human
1549
1716
  and machine identities, pool isolation, fail-closed discovery, endpoint and
@@ -1810,10 +1977,10 @@ storage is the root's namespaced to its principal.
1810
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. |
1811
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`. |
1812
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. |
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. |
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`). |
1814
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. |
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. |
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. |
1817
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. |
1818
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`. |
1819
1986
 
@@ -1974,6 +2141,11 @@ value, matching that fallback. Interruption remains interruption. The lookup
1974
2141
  module imports no SQL or platform implementation and does not change the
1975
2142
  published storage interfaces.
1976
2143
 
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
+
1977
2149
  `openSqlite` retains file modes, WAL, busy timeout,
1978
2150
  and database ownership. `test/sql-effect-contract.ts` retains affected-row,
1979
2151
  50-claim, typed-failure, cancellation, and tracing probes on SQLite and native
@@ -2133,6 +2305,93 @@ within five minutes of expiry in the same auth partition. Partial walks and
2133
2305
  auth or permission failures refuse fallback; resources require unexpired
2134
2306
  inventories, and stale fallback cannot authorize post-entry auth recovery.
2135
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
+
2136
2395
  ### Contributor provider kit
2137
2396
 
2138
2397
  Provider declarations use `src/http/vendor.ts` for HttpApi operations and
@@ -2151,17 +2410,29 @@ exact vendor count as `page.total` from its page callback. The kit validates a
2151
2410
  nonnegative safe integer and includes it on continuing and terminal pages;
2152
2411
  without a vendor count the property is absent. Lists without the opt-in keep
2153
2412
  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
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
2157
2419
  standard hatch (`_api_search`, `_api_details`, `_api_read`, `_api_write`) over
2158
2420
  an `OperationIndex` with explicit verdicts. It keeps the REST family's
2159
2421
  argument and envelope names and validates `fields` against the operation's
2160
- 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
2161
2425
  (at most eight compact entries, long labels cut and marked, continuation by
2162
2426
  cursor); details stay within 32 KiB, paging whole parameters and body
2163
2427
  properties, and chunking any piece larger than one answer, behind a bound
2164
- 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
2165
2436
  queries return no matches. It admits partial lexical matches alongside full
2166
2437
  matches. Action/read queries require all content nouns in the operation text,
2167
2438
  or any noun in the path/id with at least half the nouns in the text. Their
@@ -2308,9 +2579,10 @@ A binding may opt into `truncated: true` to supply reviewed size-cut paths
2308
2579
  (such as `data.content`) in its answer. The hatch then declares and propagates
2309
2580
  an optional top-level `truncated: string[]` on reads and writes. Without the
2310
2581
  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.
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.
2314
2586
 
2315
2587
  A binding may expand deployment-owned path defaults (Cloudflare) or canonicalize
2316
2588
  repository contents paths (GitHub) before indexed resolution, and may prepare a
@@ -2839,6 +3111,51 @@ Effect is pinned exactly; deployments using Effect must resolve the same version
2839
3111
  Per-provider bundles are reported, not capped, because their standalone graphs
2840
3112
  include the shared core again. Deployed non-provider shapes are capped by
2841
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.
2842
3159
 
2843
3160
  [Decision 0019](https://github.com/zackbart/connecta/blob/main/decisions/0019-core-validation-http-boundaries.md) records the rejected Schema/HttpApi measurements.
2844
3161
 
@@ -2973,7 +3290,10 @@ compiling and configuring the real thing.
2973
3290
  is a known failure only on refusal provenance recorded where the evidence is
2974
3291
  observed, never inferred from an error code: (a) the ledger proves it never
2975
3292
  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
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
2977
3297
  provider's existing served classification (a write's HTTP 409 is a `conflict`). One helper, `servedRefusal` in `src/write-outcome.ts`, decides
2978
3298
  (b) for the ledger and for a transport outside one: a refusing status the
2979
3299
  provider reads as `unavailable` or a timeout (Stripe's 424) is not proof, so
@@ -3419,8 +3739,19 @@ change. All 88 declarations still exclude Effect and have complete references.
3419
3739
  ### Native remote MCP programs (contributor)
3420
3740
 
3421
3741
  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,
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,
3424
3755
  chunked publication, generation fencing, complete refresh acceptance and listing
3425
3756
  serialization; `connectors/negotiation-cache.ts` owns native negotiation storage.
3426
3757
  The published connector, cache and SDK callback methods remain Promise shells.