@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
@@ -13,7 +13,7 @@ credential contracts; use [deployment setup](./deploying.md) for runnable steps.
13
13
  | Lay out an agent's pools | [Organizing an agent's pools](#organizing-an-agents-pools) |
14
14
  | Set up human sign-in | [Access on Workers](#cloudflare-access-on-workers), [Clerk tokens and sessions](#clerk-oauth-tokens-and-operator-sessions), [Clerk Organizations](#one-clerk-instance-one-organization-per-deployment), [management](#human-authentication-management) |
15
15
  | Enable credential or connection controls | [Management permissions](#management-permissions), [start/restart/disconnect](#starting-restarting-and-disconnecting) |
16
- | Diagnose consent or token refresh | [State at rest](#downstream-oauth-state-at-rest), [callbacks](#consents-and-callbacks), [refresh failures](#refresh-failures), [safe errors](#what-a-servers-errors-may-say) |
16
+ | Diagnose consent or token refresh | [State at rest](#downstream-oauth-state-at-rest), [callbacks](#consents-and-callbacks), [refreshing before expiry](#refreshing-before-expiry), [refresh failures](#refresh-failures), [safe errors](#what-a-servers-errors-may-say) |
17
17
  | Configure a downstream OAuth client or HTTP handler | [Client registration](#downstream-oauth-client-registration), [api() OAuth](#downstream-oauth-on-api), [request-local bearer tokens](#request-local-downstream-bearer-tokens), [principal forwarding](#forwarding-the-admitted-principal), [Google hosted MCP](#google-service-account-tokens-and-hosted-mcp) |
18
18
  | Review browser or downstream URL boundaries | [Origins](#origins), [advertised URLs](#urls-a-downstream-advertises), [one server per grant](#one-server-per-grant) |
19
19
 
@@ -44,7 +44,9 @@ auth through the operator UI.
44
44
  with several (Clerk's Frontend API URL or `cloudflare-access` as namespace).
45
45
  Keep these stable: personal credentials are partitioned by principal.
46
46
  4. Typecheck and restart/redeploy. Sign in as the person and verify their
47
- intended endpoint and tools. A pool-only member at plain `/mcp` gets 404.
47
+ intended endpoint and tools. A non-maintainer listed in exactly one pool
48
+ may use plain `/mcp`, which serves that pool; with zero or several pools,
49
+ plain `/mcp` answers the private 404 and they use `/mcp/<pool>`.
48
50
  With the UI enabled, the [member page](./operator-ui.md#use-the-member-page)
49
51
  shows their pool URLs and client setup; `/ui/api/config` returns 200 with
50
52
  `role: "member"`. It does not grant the maintainer pages, shared credential
@@ -66,9 +68,11 @@ In the Node template and Worker example, `CONNECTA_MAINTAINERS` is a
66
68
  comma-separated list of these ids; update that environment variable when
67
69
  using their roster wiring. Pool membership remains code. To remove a person,
68
70
  remove their memberships and maintainer entry, restart/redeploy, and remove
69
- their sign-in admission as appropriate. Never add another person to an
70
- agent's `<agent>-<user>` pool, and add a person to an agent's tier only if
71
- they are cleared for it; follow [the pool layout](#organizing-an-agents-pools).
71
+ their sign-in admission as appropriate. Never add a person to an agent's
72
+ pool: a pool lists only people or only agents, and mixing them throws at
73
+ construction. Give people who share an agent tier's reach their own people-only
74
+ pool that reuses the tier's tool-list constant, and keep each `<agent>-<user>`
75
+ pool agent-only; follow [the pool layout](#organizing-an-agents-pools).
72
76
 
73
77
  ## Connect a shared credential
74
78
 
@@ -119,6 +123,62 @@ recovery. These changes do not change
119
123
  pool membership. Environment-backed header auth has no editable vault slot:
120
124
  rotate that secret in the deployment's secret store and restart/redeploy.
121
125
 
126
+ ### Test then save, and last verification
127
+
128
+ When the connector can test its credential (`testCredential` or
129
+ `testCredentials`, which every credential `remoteMcp()` and kit provider
130
+ has), the Connection tab's **Test and save** tests the entered value before
131
+ storing it. The value is tested in memory, under `discovery.probeTimeoutMs`,
132
+ and stored only if the test passes; a failure stores nothing. If the test
133
+ fails, **Save without testing** stores the value anyway, marked unverified,
134
+ so an outage at the service never blocks a replacement.
135
+
136
+ `PUT /ui/credentials/<id>` takes `{ "value": "…" }` (or `{ "values": {…} }`
137
+ for a multi-field slot) and an optional `"verify"`:
138
+
139
+ - `"test"`: test the candidate, then store it. A failure answers 200
140
+ `{ "saved": false, "failure": "<class>" }` and stores nothing.
141
+ - `"skip"` (the default when `verify` is absent, which keeps earlier API
142
+ callers working): store it unverified.
143
+
144
+ A stored save answers 200 `{ "saved": true, "credential": <metadata>,
145
+ "verification": { "outcome": "ok" | "unverified", "observedAt": "…" } }`.
146
+ `POST /ui/credentials/<id>/test` tests the stored value and answers
147
+ `{ "ok": true }` or `{ "ok": false, "failure": "<class>" }`. A connector
148
+ without a test hook refuses `verify: "test"` with 400, as its test route
149
+ refuses to test.
150
+
151
+ Failure classes are typed facts, never downstream text:
152
+
153
+ | Class | Meaning |
154
+ | ------------- | ------------------------------------------------------------------ |
155
+ | `rejected` | The service refused the credential. |
156
+ | `scope` | It accepted the credential but refused a needed permission. |
157
+ | `unreachable` | The service could not be reached, was unavailable or rate limited. |
158
+ | `timeout` | No answer within the probe deadline. |
159
+ | `failed` | Anything else. |
160
+
161
+ A custom hook may return `{ ok: false, failure: "scope" }` to name its class;
162
+ an unclassified failure is `failed`, and a thrown error is classified from its
163
+ `ConnectorCallError` code. Neither the response nor the log carries the value,
164
+ the hook's `message` or the downstream's reply.
165
+
166
+ Each test of the stored value and each save records a last-verified
167
+ observation for that connector and owner: its outcome class, its time and the
168
+ revision of the save it observed. A Test reads the stored values and their
169
+ revision from one envelope, and a save records the revision its own write
170
+ sealed, so a concurrent replacement is never reported as verified. The
171
+ operator page's connector data reports the record as
172
+ `verification: { outcome, observedAt }` only while that save is the stored
173
+ one, so a replacement reads as unverified until tested. Removing the
174
+ credential removes the record. A custom vault records verification only when
175
+ its `metadata` carries a per-save `revision` and, for Test, it implements
176
+ `snapshot`; otherwise its credentials are tested but never shown verified.
177
+
178
+ Reading the record is side-effect free: the operator page only reads it,
179
+ never probes the downstream or writes (INV-10). OAuth connectors keep no
180
+ verification record; their status is unchanged.
181
+
122
182
  ## Issue, rotate, or revoke an agent token
123
183
 
124
184
  Each declared agent has exactly one token slot. Keep `accessTokens(storage)`
@@ -591,6 +651,11 @@ agent's slot is `issued` naming exactly that record. Nothing else counts: a
591
651
  lookup or record the slot does not name is inert garbage. Every mutation
592
652
  commits by one `compareAndSet` on the slot and only moves forward; no security
593
653
  outcome depends on cleanup or on undoing a write.
654
+ Admission reads this state afresh on every request; successful token verdicts
655
+ are never cached. D1 and SQLite read the lookup, record, and slot in one
656
+ statement, with the same validation and expiry checks. Custom storage adapters
657
+ retain the sequential reads. Rotation and revocation therefore retain their
658
+ next-request refusal bound.
594
659
 
595
660
  - **Rotate** writes the new record and its lookup (inert), checks the
596
661
  request's signal, then compare-and-sets the slot from what it read to
@@ -727,8 +792,17 @@ bypasses the scoped registry. These refusals happen before any request leaves. A
727
792
  never reach it; a deployment's own connectors still cannot read the caller.
728
793
 
729
794
  Access is read from one place, deployment code. Top-level
730
- `maintainers` lists the people who reach plain `/mcp`, which serves the whole
731
- catalog with the deployment's `readOnly`, and the operator routes. Each pool's
795
+ `maintainers` lists the people who receive the whole catalog at plain `/mcp`
796
+ with the deployment's `readOnly`, and who reach the operator routes. A
797
+ non-maintainer person listed in exactly one pool receives that pool at plain
798
+ `/mcp` exactly as `/mcp/<pool>` serves it: its `tools`, `readOnly`, `surface`,
799
+ `executeCode` and `description`. A maintainer keeps the whole catalog even when
800
+ also listed in pools. Membership is matched on the admitted, resolved
801
+ `{ namespace, id }`, so a bare `"bob"` and the explicit reference it resolves
802
+ to are the same person, and the same id in another namespace is not. Authentication, OAuth challenges and resource audiences are plain `/mcp`'s;
803
+ the pool is chosen after authentication. Construction warns, naming each
804
+ non-maintainer person listed in several pools, and `describeConfig()` lists them
805
+ in `peopleInSeveralPools`. Each pool's
732
806
  `members` lists who reaches that pool, people at `/mcp/<pool>` and agents at
733
807
  `/agent/<pool>`, which serve exactly that pool's `tools`. A maintainer opens no pool implicitly; list them as a member to use
734
808
  one. Nothing intersects these lists with anything else, and a request never
@@ -776,6 +850,12 @@ replacement. A guarded read grant becomes a pool with `readOnly: true`.
776
850
 
777
851
  ### Team pools
778
852
 
853
+ Every person in exactly one pool can configure plain `/mcp`; their sign-in
854
+ selects that pool, so one client configuration serves every team. A person in
855
+ several pools uses each pool's `/mcp/<pool>`. Only people pools count: a pool
856
+ lists only people or only agents, so an agent's pools never change where a
857
+ person's plain `/mcp` routes.
858
+
779
859
  Keep membership in deployment code; static lists can be built with ordinary
780
860
  code at construction:
781
861
 
@@ -800,9 +880,9 @@ Marketing's every `campaigns` tool and only `issues.search`. Neither pool
800
880
  serves `billing`; only the owner's plain `/mcp` does. A person in both pools
801
881
  uses two endpoints, never a union.
802
882
 
803
- A team pool can also be one of an agent's tiers: the same pool lists the
804
- cleared people and the agent, and each reaches it at their own address
805
- ([people in a tier](#organizing-an-agents-pools)).
883
+ A human team pool and an agent tier are separate pools. Share a tool-list
884
+ constant between them, with people in `team` and only agent references in
885
+ `assistant-team` ([pool layout](#organizing-an-agents-pools)).
806
886
 
807
887
  | Catalog change | Exact `connector.tool` in a pool |
808
888
  | ----------------------- | ----------------------------------------------------------------------- |
@@ -891,16 +971,16 @@ warning says, every pool path answers the 404 because nobody can be a member,
891
971
  and every agent endpoint answers the machine-only 401 because no agent can be
892
972
  admitted.
893
973
 
894
- | Rule | Contract |
895
- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
896
- | Scope | `/mcp/<pool>` and `/agent/<pool>` serve exactly the pool's `tools` (connector ids or exact `connector.tool` addresses), with the pool's `readOnly`, to its member people and member agents respectively. Plain `/mcp` serves the whole catalog to maintainers. Neither intersects with or widens anything. |
897
- | Refusal | A non-maintainer at `/mcp`, a non-member, and an undeclared pool get an identical 404 status, body, and headers, after auth. The operator log records `not_maintainer`, `not_pool_member`, or `pool_not_declared`. Names are not anonymously enumerable; valid credentials cannot distinguish these cases by content. |
898
- | Agents | An agent member reaches its pools only at `/agent/<pool>`, with its `cta_` token. Human endpoints refuse `cta_`; see [agent endpoints](#agent-endpoints). |
899
- | Surface | From 0.35.0, optional `surface`: `"meta"` (default) serves the six meta-tools; `"native"` lists each of the pool's downstream tools directly, for clients with their own tool search, with optional `executeCode: false` to replace `execute_code` with `read_result` (invalid on meta pools). Calls take the same paths either way. See the [native tool surface](./meta-tools.md#native-tool-surface). |
900
- | Description | Optional `description`: plain text telling the model what this endpoint is for. `/mcp/<pool>` and `/agent/<pool>` append it to the MCP `initialize` instructions after `This endpoint: `; plain `/mcp` and a pool without one serve the shared instructions unchanged. It grants, hides, and refuses nothing, and changes no tool or tool description. |
901
- | Construction | Malformed names, unknown options or connectors, empty pools, empty or missing `members`, unresolvable member references, a non-boolean `readOnly`, an `owner` that is not a Workspace email address, a `description` that is not 1 to 500 characters of one-line text after trimming, a retired `trust` or `grant`, and addresses absent from an `api()` static catalog throw. Remote catalogs are checked at lazy load. |
902
- | Discovery | Clerk's pool 401 names `/.well-known/oauth-protected-resource/mcp/<pool>` with the pool URL as `resource`, matching RFC 9728. Cloudflare Managed OAuth is application-level and needs no pool setup. |
903
- | Drift | No wildcards; pool tools are exact names. Missing live tools are unreachable and warned once while in a bounded 1,024-entry FIFO. |
974
+ | Rule | Contract |
975
+ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
976
+ | Scope | `/mcp/<pool>` and `/agent/<pool>` serve exactly the pool's `tools` (connector ids or exact `connector.tool` addresses), with the pool's `readOnly`, to its member people and member agents respectively. Plain `/mcp` serves the whole catalog to maintainers and the unique member pool to other people. Neither intersects with or widens anything. |
977
+ | Refusal | A person with zero or several memberships at `/mcp`, a non-member, and an undeclared pool get an identical 404 status, body, and headers, after auth. The operator log records `not_maintainer` (zero memberships), `ambiguous_pool` (several memberships), `not_pool_member`, or `pool_not_declared`. Names are not anonymously enumerable; valid credentials cannot distinguish these cases by content. |
978
+ | Agents | An agent member reaches its pools only at `/agent/<pool>`, with its `cta_` token. Human endpoints refuse `cta_`; see [agent endpoints](#agent-endpoints). |
979
+ | Surface | From 0.35.0, optional `surface`: `"meta"` (default) serves the six meta-tools; `"native"` lists each of the pool's downstream tools directly, for clients with their own tool search, with optional `executeCode: false` to replace `execute_code` with `read_result` (invalid on meta pools). Calls take the same paths either way. See the [native tool surface](./meta-tools.md#native-tool-surface). |
980
+ | Description | Optional `description`: plain text telling the model what this endpoint is for. `/mcp/<pool>` and `/agent/<pool>` append it to the MCP `initialize` instructions after `This endpoint: `; a maintainer at plain `/mcp` and a pool without one serve the shared instructions unchanged. A routed person receives their pool description. It grants, hides, and refuses nothing, and changes no tool or tool description. |
981
+ | Construction | Malformed names, unknown options or connectors, empty pools, empty or missing `members`, mixed people and agent membership (a `ConfigError` naming the pool), unresolvable member references, a non-boolean `readOnly`, an `owner` that is not a Workspace email address, a `description` that is not 1 to 500 characters of one-line text after trimming, a retired `trust` or `grant`, and addresses absent from an `api()` static catalog throw. Remote catalogs are checked at lazy load. |
982
+ | Discovery | Clerk's pool 401 names `/.well-known/oauth-protected-resource/mcp/<pool>` with the pool URL as `resource`, matching RFC 9728. Cloudflare Managed OAuth is application-level and needs no pool setup. |
983
+ | Drift | No wildcards; pool tools are exact names. Missing live tools are unreachable and warned once while in a bounded 1,024-entry FIFO. |
904
984
 
905
985
  Pool descriptions are trimmed before validation. The remaining text must be
906
986
  1 to 500 characters and contain no C0 or C1 control characters (including tabs,
@@ -947,7 +1027,12 @@ Protected-resource metadata stays public at both default aliases,
947
1027
  Discovery does not authenticate callers or evaluate membership, so fresh
948
1028
  OAuth clients can discover how to authenticate. Admission applies after auth
949
1029
  to MCP requests and result/page handles. Handles remain bound to their
950
- originating endpoint; paging re-admits the caller at that endpoint. A pool
1030
+ originating requested pathname and admitted pool; paging re-admits the caller
1031
+ at that endpoint and re-resolves their pool, which must still match. A handle
1032
+ minted at plain `/mcp` never pages at `/mcp/<pool>` or the reverse, even for the
1033
+ same pool, and a membership change fails it closed. Handles minted before this
1034
+ binding existed fail closed. Auth-elicitation and downstream-input
1035
+ continuations carry the same pathname and pool binding. A pool
951
1036
  cannot read a `/mcp` handle, and `/mcp` cannot read a pool's handle.
952
1037
 
953
1038
  Operator catalog (`/ui/data`, `/ui/connectors/<id>`), activity, shared
@@ -964,9 +1049,10 @@ activity. Anyone else gets the same 404, and an agent token the machine-only
964
1049
  shell carries no catalog data.
965
1050
 
966
1051
  Each endpoint is writable or read-only. The deployment's `readOnly` sets the
967
- plain `/mcp` endpoint and is required: a deployment that omits it fails at
1052
+ plain `/mcp` endpoint for maintainers and is required: a deployment that omits it fails at
968
1053
  construction and must choose `true` or `false`. A pool inherits it unless the
969
- pool sets its own `readOnly`, in either direction. A writable endpoint lets
1054
+ pool sets its own `readOnly`, in either direction. A person routed to their
1055
+ pool at plain `/mcp` gets that pool's effective `readOnly`. A writable endpoint lets
970
1056
  `call_destructive_tool` and programs write, and the MCP client decides what to
971
1057
  approve. On a read-only endpoint, write tools are absent from `search_tools`
972
1058
  results, `connecta.search`, and `connecta.describe`, and `call_tool`,
@@ -997,9 +1083,9 @@ for agents, just as an `authScope: "personal"` connector is: `search_tools`,
997
1083
  `connecta.search`, `connecta.describe`, `skills`, and connector discovery omit
998
1084
  it, `authorize_connector` answers "Unknown connector", and a call by address
999
1085
  fails `unknown_address`, the same as for a connector that does not exist.
1000
- People in the same pool still see it and act as themselves. This is what lets
1001
- one pool be a team pool for people, each acting as themselves on Workspace, and
1002
- also list an agent that reaches only the pool's shared connectors.
1086
+ People use a separate people-only pool, where they see the connector and
1087
+ act as themselves. Share the same tool-list constant between the people pool
1088
+ and an agent-only pool when both need the same tools.
1003
1089
 
1004
1090
  `owner` is a Workspace email address, checked at construction and listed in
1005
1091
  `describeConfig()` (`null` when unset). Over HTTP, `/ui/api/config` serves it
@@ -1025,7 +1111,7 @@ createConnecta({
1025
1111
  pools: Object.fromEntries(
1026
1112
  team.map((person) => [
1027
1113
  `assistant-${person.handle}`,
1028
- { tools: ["gmail", "drive"], owner: person.email, members: [person.id, "agent:assistant"] },
1114
+ { tools: ["gmail", "drive"], owner: person.email, members: ["agent:assistant"] },
1029
1115
  ]),
1030
1116
  ),
1031
1117
  connectors: [
@@ -1037,12 +1123,11 @@ createConnecta({
1037
1123
  ```
1038
1124
 
1039
1125
  The agent harness, not the model, picks the pool for each turn, which is what
1040
- keeps one person's mailbox from another's. Adding a person to someone else's
1041
- pool opens nothing of the owner's Workspace account to them, since people act
1042
- as themselves through the mapping. That holds only for Workspace mappings:
1043
- every other connector in the pool, including a shared-auth connector holding
1044
- the owner's own account, runs the same for every member
1045
- ([members of a personal pool](#organizing-an-agents-pools)).
1126
+ keeps one person's mailbox from another's. Personal agent pools list only
1127
+ the agent, whether or not they set an `owner`. People use separate pools;
1128
+ their Workspace calls use the mapping and always act as themselves. A shared-auth
1129
+ connector holding a user's own account runs as that account for every admitted
1130
+ agent ([personal pools](#organizing-an-agents-pools)).
1046
1131
 
1047
1132
  A fixed `subject` address is the explicit shared-account exception: every
1048
1133
  caller, person or agent, acts as it in every pool that lists the connector,
@@ -1067,10 +1152,10 @@ section holds the complete rules.
1067
1152
  2. **Tiers.** An agent's tiers are named pools ordered from least to most
1068
1153
  capable. Recommended names are `<agent>` for the lowest tier and
1069
1154
  `<agent>-<tier>` for higher tiers (`assistant-team`, `assistant-leads`).
1070
- A tier may be any named pool, including an existing team pool such as
1071
- `team` or `leadership`, as long as its human members are all cleared for
1072
- it (rule 10). An agent with one tier uses it as the lowest. Tiers never
1073
- hold per-user connectors and never set an `owner`.
1155
+ Every tier is agent-only. A human team with the same reach uses a separate
1156
+ pool sharing the same tool-list constant (rule 10). An agent with one tier
1157
+ uses it as the lowest. Tiers never hold per-user connectors and never set
1158
+ an `owner`.
1074
1159
  3. **Personal pool `<agent>-<user>`.** `/agent/<agent>-<user>` serves pool
1075
1160
  `<agent>-<user>`: the highest tier that user is cleared for, plus that
1076
1161
  user's own connectors. Use it only in a one-on-one with that user.
@@ -1103,26 +1188,24 @@ section holds the complete rules.
1103
1188
  reaches every tier above. For example, billing restricted to leads goes
1104
1189
  in the leads tier. Per-user connectors (`notes_alice`) go only in personal
1105
1190
  pools, and so do the Workspace connectors with a mapping `subject` that
1106
- the agent uses; rule 10 covers listing them in a tier for its people.
1191
+ the agent uses. People use a separate pool for their own accounts.
1107
1192
  8. **Changes stay in config.** Adding an account for a user adds its
1108
1193
  connector and lists it in that user's pool `tools`. Adding a user adds one
1109
1194
  `<agent>-<user>` pool. Adding a tier adds one pool and one constant, and
1110
1195
  the tier's place in the harness's tier list.
1111
- 9. **No other person in a personal pool.** The only human member an
1112
- `<agent>-<user>` pool may have is that same user, and usually it has none.
1113
- Never add another person: its per-user connectors (`notes_alice`) use that
1114
- user's account for everyone the pool admits, so another member would read
1115
- and write the user's account as their own. A person's own use normally
1116
- goes through their own pools or plain `/mcp`.
1117
- 10. **People in a tier are cleared for it.** A tier can also be a human team
1118
- pool: list the people cleared for that tier in its `members` beside the
1119
- agent. They use it at `/mcp/<pool>`, the agent at `/agent/<pool>`.
1120
- List no one else: a member reaches
1121
- every shared connector the tier holds, on the deployment's account. Such a
1122
- pool may also list Workspace mapping connectors or `authScope: "personal"`
1123
- connectors for its people, who act as themselves on them; those connectors
1124
- are absent for the agent, since a tier has no owner and an agent has no
1125
- principal.
1196
+ 9. **Personal pools list only the agent.** An `<agent>-<user>` pool lists
1197
+ only `"agent:<agent>"`, with or without an `owner`. No person belongs
1198
+ there: mixed membership throws `ConfigError` at construction. A person's
1199
+ own use goes through a separate people-only pool or plain `/mcp`.
1200
+ 10. **People use separate team pools.** A tier is never a human team pool.
1201
+ Define `teamTools` once, then use it for `team` with people in
1202
+ `members` and `assistant-team` with only the agent. People open
1203
+ `/mcp/team`; the agent opens `/agent/assistant-team`. Every person
1204
+ listed must be cleared for every shared connector in their pool.
1205
+ The human pool may also list Workspace mapping connectors and
1206
+ `authScope: "personal"` connectors, on which each person acts as
1207
+ themselves. Keep them out of the tier: they are absent for agents,
1208
+ since an agent has no principal and a tier has no owner.
1126
1209
 
1127
1210
  **Choosing the pool each turn**
1128
1211
 
@@ -1176,8 +1259,9 @@ createConnecta({
1176
1259
  pools: {
1177
1260
  // Lowest tier and fallback: reads guests may use.
1178
1261
  assistant: { tools: guestTools, readOnly: true, members: [agent] },
1179
- // Also the team's own pool at /mcp/assistant-team.
1180
- "assistant-team": { tools: teamTools, members: [agent, "user_alice", "user_bob"] },
1262
+ // Same reach, separate membership: people and the agent each have a pool.
1263
+ team: { tools: teamTools, members: ["user_alice", "user_bob"] },
1264
+ "assistant-team": { tools: teamTools, members: [agent] },
1181
1265
  "assistant-leads": { tools: leadsTools, members: [agent] },
1182
1266
  // Each personal pool spreads its user's highest tier.
1183
1267
  "assistant-alice": {
@@ -1235,8 +1319,8 @@ gets `assistant`, even if the platform marks the chat as direct. Rule 9 is
1235
1319
  why each personal pool lists only the agent: people act as themselves only
1236
1320
  through a Workspace mapping, and a shared-auth connector such as
1237
1321
  `notes_alice` acts as Alice for whoever the pool admits.
1238
- Rule 10 is why `assistant-team` may list Alice and Bob: it holds only
1239
- connectors that every member may use on the deployment's account.
1322
+ Rule 10 gives Alice and Bob a separate `team` pool sharing `teamTools`;
1323
+ `poolFor` selects only agent pools and its tier selection stays the same.
1240
1324
 
1241
1325
  ## Shared and personal auth
1242
1326
 
@@ -1257,8 +1341,8 @@ A personal connector is absent — not refused — from any request without a
1257
1341
  stable namespaced principal. For one that can see it, connector storage, vault
1258
1342
  records, OAuth generations, and observed result shapes are
1259
1343
  partitioned under an opaque SHA-256 identity key. Remote catalog cache keys
1260
- include that owner and the admitted pool, plus the credential digest and OAuth
1261
- epoch; refreshes coalesce within
1344
+ include that owner and the admitted pool, plus the keyed credential digest and
1345
+ OAuth epoch (see [cache keys](./architecture.md#storage-credentials-and-connectors)); refreshes coalesce within
1262
1346
  one owner, and another owner's epoch cannot retire them. Keep namespaces and
1263
1347
  principal ids stable across upgrades, since changing either selects different
1264
1348
  partitions. Literal `auth: { type: "headers" }` keeps its secret in deployment
@@ -1266,6 +1350,16 @@ code, so `remoteMcp()` refuses it as personal at construction.
1266
1350
 
1267
1351
  ## Downstream OAuth state at rest
1268
1352
 
1353
+ An OAuth remote MCP connector requires an access token on passive MCP requests,
1354
+ including handshakes, catalog requests, and tool dispatches, even if the server
1355
+ accepts anonymous requests. This requirement survives provider replacement.
1356
+ If the SDK cannot obtain a token at send time, Connecta returns OAuth recovery
1357
+ before sending. A locally refused write counts as failed, without reconciliation;
1358
+ a write that may already have left remains unknown and is never replayed.
1359
+ Only an explicit authorization start may probe the MCP endpoint without a token
1360
+ for the SDK's challenge, never to call a tool. Authorization-server discovery,
1361
+ client registration, token exchange, and refresh retain their OAuth flows.
1362
+
1269
1363
  Each owner of an OAuth connector (the deployment when shared, each principal
1270
1364
  when personal) has one **grant record**, `oauth:grant` in the connector's
1271
1365
  namespace: the live **epoch**, the latest consent's state digest, and one
@@ -1286,6 +1380,17 @@ connector](#downstream-oauth-on-api) stores no client. A vault without the
1286
1380
  optional `seal`/`open` members keeps the body plaintext with a startup
1287
1381
  warning; no vault, plaintext as always.
1288
1382
 
1383
+ The body also holds `expiresAt`, the epoch milliseconds the stored access token
1384
+ expires: its `expires_in`, counted from just before the request that issued it
1385
+ was sent (the code exchange, or the refresh's token request), never from when
1386
+ its answer arrived or was saved, so a slow answer cannot push the expiry past
1387
+ the token's real one. An `expires_in` of `0` expires at issuance. It is written
1388
+ with the tokens it describes, replaced with them and dropped with them, in the
1389
+ sealed and plaintext layouts alike. A grant stored before this field existed,
1390
+ or one whose token response carried no `expires_in`, has none. Reading a grant
1391
+ never adds it (INV-10), so such a grant gains one only at its next token write,
1392
+ and until then is refreshed only after a 401.
1393
+
1289
1394
  **Pre-layout-3 grants.** The layout-2 migration and historical-key cleanup
1290
1395
  are removed. An owner with only old keys is treated as unauthorized; a warning
1291
1396
  names the unsupported layout and the need to Connect again or migrate on
@@ -1342,7 +1447,7 @@ with the gate a call passes:
1342
1447
  | Stored grant | Status | Start (Connect, Continue, Restart, `authorize_connector`) |
1343
1448
  | ------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1344
1449
  | Disconnected | `auth_required`; the downstream is not asked | Replaces the epoch, then proceeds as for no tokens |
1345
- | No tokens | Connects; `auth_required` even when the downstream answers | Connects first, so a 401's `resource_metadata` starts consent as before. A downstream that answers without credentials still gets consent, discovered from RFC 9728 metadata at the configured URL's well-known locations. The start answers `auth_required` with the consent URL. |
1450
+ | No tokens | `auth_required`; tokenless MCP requests are refused locally | Connects first, so a 401's `resource_metadata` starts consent as before. A downstream that answers without credentials still gets consent, discovered from RFC 9728 metadata at the configured URL's well-known locations. The start answers `auth_required` with the consent URL. |
1346
1451
  | Tokens (any expiry) | Connects; `ok`, or `auth_required` when a 401 needs consent | Connects; `ok` without starting a flow when healthy |
1347
1452
 
1348
1453
  A start without a challenge runs the same SDK flow as a 401 without
@@ -1419,6 +1524,47 @@ removal and revocation flow when deployment configuration supplies
1419
1524
  `revocationEndpoint`; without it, removal remains local. The configured
1420
1525
  client secret is used in memory and is never saved with the grant.
1421
1526
 
1527
+ ## Refreshing before expiry
1528
+
1529
+ A call refreshes the grant before it sends a token that is within 60 seconds of
1530
+ its stored `expiresAt`, so a write never meets the 401 it could not be repeated
1531
+ after (INV-9). Sixty seconds covers the refresh's own 20-second token-request
1532
+ deadline and the call's send with margin, and matches the early refresh of
1533
+ Google service-account tokens; for a typical one-hour token it is under 2% of
1534
+ its life. A token that lives under a minute, or arrives already expired, is
1535
+ refreshed once per call, in the invocation's check. Later in the same request,
1536
+ `callTool` and `ctx.oauth.fetch` send the token that renewal produced without
1537
+ renewing it again until its stored expiry passes (or, for a token already
1538
+ expired when issued, for the next check only); a token another request rotated
1539
+ in since is checked as usual.
1540
+
1541
+ The renewal is the flow a 401 would start, through the same refresh
1542
+ coordinator: one redemption per owner partition and epoch in an isolate, the
1543
+ shared-storage lease and spent-fingerprint CAS across isolates, and the commit
1544
+ before anyone is released. Its token request names the access token it
1545
+ replaces, so a caller whose own read came before another request's rotation
1546
+ adopts that rotation instead of redeeming again. Two isolates that find the
1547
+ same token due spend one refresh token between them.
1548
+
1549
+ | Where | When | A failed renewal |
1550
+ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1551
+ | The invocation's authorization check, before it enters the connector | Every `call_tool`, `call_destructive_tool`, and program call, after the grant read that check already makes | Fails the call before anything is sent: `downstream_oauth_required` (or retryable `unavailable` for a waiter deadline), with no `writeOutcome` and no `reconciliationRequired`. |
1552
+ | `remoteMcp()`'s `callTool`, before it sends `tools/call` on a reused client | The token the connection last read has come due since | Ends the call before `tools/call` is sent. A call already entered reports it as the invocation reports any failure after entry. |
1553
+ | `api()`'s `ctx.oauth.fetch`, before each request | The token the request just read is due, as in a handler that runs long | Is thrown into the handler, which may already have sent other requests, so a write keeps the invocation's conservative outcome. |
1554
+
1555
+ Whether a token is due is decided from the grant read the step already made: a
1556
+ grant not yet due costs no extra storage read. A due token costs one more grant
1557
+ read for the renewal's own flow, and then the refresh's own reads and writes.
1558
+ Status, listing (including the stored-catalog read without a connection),
1559
+ discovery, the operator UI, and `authorize_connector` never renew a token
1560
+ (INV-10). Nothing renews in the background: renewal belongs to the call that
1561
+ found the token due, which owns its preparation until the claim CAS is
1562
+ submitted, as for a 401.
1563
+
1564
+ A grant without `expiresAt` keeps the earlier behavior: it is refreshed after a
1565
+ downstream 401, a write's 401 is never resent, and the refresh that follows
1566
+ stores `expiresAt` for every later call.
1567
+
1422
1568
  ## Refresh failures
1423
1569
 
1424
1570
  A refresh token is spent when dispatch begins. Every refresh request passes
@@ -1493,10 +1639,20 @@ a dispatched record. The refresh HTTP request and response body have a
1493
1639
  20-second deadline. After the response or a dispatched exchange failure,
1494
1640
  grant commit and cleanup share a separate 20-second deadline, including all
1495
1641
  storage reads, CAS retries, fingerprint resolution, and lease release.
1496
- Contenders wait at most 35 seconds with 10 ms to 250 ms
1497
- backoff. A waiter deadline returns retryable `unavailable`, keeps the grant,
1498
- and starts no consent. It never permits resending a spent token.
1499
-
1642
+ Contenders wait at most 35 seconds, and never past the caller's own signal,
1643
+ which carries the request's deadline. A waiter polls the lease from 50 ms,
1644
+ doubling to at most 2 seconds between polls, so a wait that runs the full 35
1645
+ seconds polls at most 22 times. Each poll is one batched storage read of the
1646
+ lease, the grant, the spent fingerprint, and the holder's liveness key named by
1647
+ the previous poll (one D1 statement); a store without batched reads reads them
1648
+ one at a time, lease first, so a completed lease is never paired with the grant
1649
+ from before its commit. A waiter deadline returns retryable `unavailable`,
1650
+ keeps the grant, and starts no consent. It never permits resending a spent
1651
+ token.
1652
+
1653
+ A grant with `expiresAt` is normally renewed before its token expires
1654
+ ([Refreshing before expiry](#refreshing-before-expiry)). A token revoked early,
1655
+ or one stored without an expiry, can still draw a 401.
1500
1656
  A `remoteMcp()` write the downstream answered with HTTP 401 is never sent
1501
1657
  again (INV-9). The SDK still refreshes the grant, and connecta stops its
1502
1658
  resend before it leaves, so any fresh call after the target is reconciled uses
@@ -2410,6 +2566,15 @@ Before an accepted retry restarts any call, it rechecks every entered read.
2410
2566
  A write classification, changed classification digest, or stale catalog returns
2411
2567
  `auth_replay_refused` with reconciliation guidance, including in programs on a writable endpoint.
2412
2568
  Pre-invocation credential-resolution eligibility is unchanged.
2569
+ Custom vaults may implement the host-only `storageDigestKey` method, returning a
2570
+ non-extractable HMAC-SHA-256 signing `CryptoKey` that is stable across the
2571
+ deployment's processes, purpose-specific and never stored.
2572
+ `encryptedCredentialVault()` derives it with HKDF from its credential key.
2573
+ Remote catalog, negotiation-verdict and connector-health keys digest their
2574
+ inputs (configured URLs and headers, credentials, request tokens, OAuth token
2575
+ responses, described configuration) with it, so their partitions are
2576
+ deployment-keyed. Without it they use a random per-process key: partitions stay
2577
+ exact, but each process or isolate keeps its own entries.
2413
2578
  Custom vaults may implement the host-only `requestStateKey` method; without it, the
2414
2579
  deployment retains the ordinary handoff. It must return a stable,
2415
2580
  purpose-specific deployment key of at least 32 bytes. Only the MCP boundary
@@ -2715,6 +2880,9 @@ owner's access token as `Authorization: Bearer` with these rules:
2715
2880
 
2716
2881
  - Only `apiOrigins` receive tokens; refuse other origins before sending, including
2717
2882
  untrusted response URLs. Return redirects unfollowed; handlers cannot set `Authorization`.
2883
+ - A token within a minute of its stored expiry is renewed before the handler
2884
+ runs, and again before any request that finds it due
2885
+ ([Refreshing before expiry](#refreshing-before-expiry)).
2718
2886
  - On 401, recover exactly once using an already rotated token or one coalesced
2719
2887
  refresh through `remoteMcp()`'s coordinator. Persist rotation even when its
2720
2888
  owner is cancelled after the answer. Only a positively read-classified
@@ -3140,6 +3308,7 @@ for credential precedence and disclosure boundaries.
3140
3308
 
3141
3309
  Downstream OAuth implementation and evidence are in
3142
3310
  [downstream-oauth.ts](https://github.com/zackbart/connecta/blob/main/src/auth/downstream-oauth.ts),
3311
+ its [seam modules](https://github.com/zackbart/connecta/tree/main/src/auth/downstream-oauth),
3143
3312
  [static-oauth.ts](https://github.com/zackbart/connecta/blob/main/src/auth/static-oauth.ts),
3144
3313
  [OAuth connect tests](https://github.com/zackbart/connecta/blob/main/test/oauth-connect.test.ts),
3145
3314
  and [API OAuth tests](https://github.com/zackbart/connecta/blob/main/test/api-oauth.test.ts).