@zackbart/connecta 0.37.1 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -4
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/agent-routes.js +231 -0
  8. package/dist/auth/clerk-sdk/generated.js +2 -1
  9. package/dist/auth/downstream-oauth/consent.js +519 -0
  10. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  11. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  12. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  13. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  14. package/dist/auth/downstream-oauth.js +66 -2516
  15. package/dist/auth/downstream-revocation.js +1 -1
  16. package/dist/auth/native.js +27 -0
  17. package/dist/auth/oauth-grant.js +24 -0
  18. package/dist/auth/oauth-token-response.js +1 -1
  19. package/dist/auth/static-oauth.js +153 -131
  20. package/dist/auth/vercel-agent.js +2 -1
  21. package/dist/auth-elicitation.js +9 -3
  22. package/dist/authorization-handoff.js +11 -5
  23. package/dist/branding.d.ts +1 -15
  24. package/dist/branding.js +2 -2
  25. package/dist/bytes.js +42 -0
  26. package/dist/call-admission-context.js +2 -5
  27. package/dist/call-recovery.js +0 -3
  28. package/dist/catalog-cache.js +130 -43
  29. package/dist/catalog-fingerprint.js +2 -18
  30. package/dist/catalog-policy.js +18 -0
  31. package/dist/catalog-service.js +285 -48
  32. package/dist/catalog.js +18 -2
  33. package/dist/config-defaults.d.ts +1 -0
  34. package/dist/config-defaults.js +1 -0
  35. package/dist/config-schema.d.ts +1 -1
  36. package/dist/config-schema.js +6 -2
  37. package/dist/config.d.ts +57 -25
  38. package/dist/config.js +30 -77
  39. package/dist/connector-caller.d.ts +1 -1
  40. package/dist/connector-health.js +59 -0
  41. package/dist/connector-status.js +117 -0
  42. package/dist/connectors/api-connector.d.ts +7 -2
  43. package/dist/connectors/api-connector.js +11 -7
  44. package/dist/connectors/guarded-fetch.d.ts +0 -6
  45. package/dist/connectors/guarded-fetch.js +1 -11
  46. package/dist/connectors/negotiation-cache.js +22 -4
  47. package/dist/connectors/remote-mcp/call.js +164 -0
  48. package/dist/connectors/remote-mcp/catalog.js +278 -0
  49. package/dist/connectors/remote-mcp/config.js +263 -0
  50. package/dist/connectors/remote-mcp/connection.js +671 -0
  51. package/dist/connectors/remote-mcp/errors.js +488 -0
  52. package/dist/connectors/remote-mcp/oauth.js +469 -0
  53. package/dist/connectors/remote-mcp/resources.js +473 -0
  54. package/dist/connectors/remote-mcp/runtime.js +1 -0
  55. package/dist/connectors/remote-mcp/transport.js +544 -0
  56. package/dist/connectors/remote-mcp.js +61 -2898
  57. package/dist/connectors/resource-uri.js +0 -4
  58. package/dist/credential-contract.d.ts +23 -0
  59. package/dist/credential-reads.js +17 -0
  60. package/dist/credential-verification.js +53 -0
  61. package/dist/credentials.d.ts +7 -0
  62. package/dist/credentials.js +60 -25
  63. package/dist/d1.d.ts +0 -89
  64. package/dist/d1.js +0 -34
  65. package/dist/describe-config.d.ts +12 -2
  66. package/dist/describe-config.js +2 -0
  67. package/dist/discovery-health.js +199 -0
  68. package/dist/downstream-input.js +4 -2
  69. package/dist/errors.d.ts +55 -17
  70. package/dist/errors.js +95 -41
  71. package/dist/execute.js +172 -125
  72. package/dist/executor-result.js +5 -2
  73. package/dist/executors/quickjs-runtime.js +2 -25
  74. package/dist/executors/quickjs.js +1 -1
  75. package/dist/guest-functions.js +83 -0
  76. package/dist/guest-runtime.js +63 -20
  77. package/dist/guest-types.d.ts +24 -10
  78. package/dist/http/argument-problems.js +35 -0
  79. package/dist/http/client.js +1 -1
  80. package/dist/http/errors.js +7 -6
  81. package/dist/http/fields.js +192 -13
  82. package/dist/http/hatch-response.js +73 -0
  83. package/dist/http/hatch.js +88 -23
  84. package/dist/http/json-schema.js +1 -1
  85. package/dist/http/node-transport.js +0 -1
  86. package/dist/http/oauth.js +4 -3
  87. package/dist/http/paging.js +6 -9
  88. package/dist/http/provider.js +33 -5
  89. package/dist/http/safety.js +9 -1
  90. package/dist/http/services.js +1 -8
  91. package/dist/http/tools.js +45 -30
  92. package/dist/http/vendor.js +7 -7
  93. package/dist/identity.js +2 -6
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +9 -2
  96. package/dist/invocation-auth.js +2 -1
  97. package/dist/invocation.js +21 -6
  98. package/dist/membership.d.ts +13 -5
  99. package/dist/membership.js +34 -2
  100. package/dist/meta-tools.js +50 -35
  101. package/dist/native-recovery.js +4 -16
  102. package/dist/native-tools.js +6 -2
  103. package/dist/oauth-handoff.js +2 -2
  104. package/dist/oauth-sealing.js +10 -0
  105. package/dist/operation-vocabulary.js +136 -14
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +2 -49
  108. package/dist/operator-ui/contract.d.ts +12 -2
  109. package/dist/operator-ui/generated.js +3 -3
  110. package/dist/operator-ui/model.d.ts +22 -1
  111. package/dist/program-writes.js +3 -8
  112. package/dist/provider-call.js +0 -7
  113. package/dist/provider.d.ts +1 -1
  114. package/dist/providers/_shared/google/kit.js +1 -6
  115. package/dist/providers/_shared/google/service-account.js +1 -3
  116. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  117. package/dist/providers/_shared/guide.js +3 -0
  118. package/dist/providers/_shared/json.js +12 -0
  119. package/dist/providers/_shared/rest/compact-index.js +1 -1
  120. package/dist/providers/_shared/rest/kit.js +1 -3
  121. package/dist/providers/_shared/rest/operation-index.js +49 -53
  122. package/dist/providers/_shared/rest/value-safety.js +106 -28
  123. package/dist/providers/breeze/api.js +9 -8
  124. package/dist/providers/breeze/index.js +4 -2
  125. package/dist/providers/breeze/skill.generated.js +1 -1
  126. package/dist/providers/breeze/tools.js +7 -7
  127. package/dist/providers/ccb/api.js +3 -7
  128. package/dist/providers/ccb/index.js +7 -1
  129. package/dist/providers/ccb/skill.generated.js +1 -1
  130. package/dist/providers/ccb/tools.js +1 -6
  131. package/dist/providers/cloudflare/api.js +10 -3
  132. package/dist/providers/cloudflare/index.js +2 -1
  133. package/dist/providers/cloudflare/safety.js +3 -3
  134. package/dist/providers/cloudflare/skill.generated.js +1 -1
  135. package/dist/providers/docs/lib/schemas.js +24 -22
  136. package/dist/providers/docs/safety.js +2 -0
  137. package/dist/providers/docs/skill.generated.js +1 -1
  138. package/dist/providers/docs/tools.js +4 -14
  139. package/dist/providers/drive/api.js +1 -0
  140. package/dist/providers/drive/lib/contracts.js +8 -5
  141. package/dist/providers/drive/skill.generated.js +1 -1
  142. package/dist/providers/drive/tools.js +23 -41
  143. package/dist/providers/forms/api.js +1 -0
  144. package/dist/providers/forms/skill.generated.js +1 -1
  145. package/dist/providers/forms/tools.js +11 -16
  146. package/dist/providers/github/api.js +1 -0
  147. package/dist/providers/github/lib/discovery.js +1 -1
  148. package/dist/providers/github/skill.generated.js +1 -1
  149. package/dist/providers/github/tools.js +61 -17
  150. package/dist/providers/gmail/lib/schemas.js +60 -50
  151. package/dist/providers/gmail/skill.generated.js +1 -1
  152. package/dist/providers/gmail/tools.js +12 -26
  153. package/dist/providers/granola/index.js +2 -0
  154. package/dist/providers/granola/skill.generated.js +1 -1
  155. package/dist/providers/overflow/api.js +8 -4
  156. package/dist/providers/overflow/index.js +2 -1
  157. package/dist/providers/overflow/skill.generated.js +1 -1
  158. package/dist/providers/overflow/tools.js +3 -7
  159. package/dist/providers/planning-center/api.js +5 -2
  160. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  161. package/dist/providers/planning-center/lib/json-api.js +1 -9
  162. package/dist/providers/planning-center/skill.generated.js +1 -1
  163. package/dist/providers/planning-center/tools.js +2 -1
  164. package/dist/providers/quickbooks/safety.js +80 -39
  165. package/dist/providers/quickbooks/skill.generated.js +1 -1
  166. package/dist/providers/sheets/api.js +1 -0
  167. package/dist/providers/sheets/skill.generated.js +1 -1
  168. package/dist/providers/sheets/tools.js +14 -23
  169. package/dist/providers/slides/skill.generated.js +1 -1
  170. package/dist/providers/slides/tools.js +19 -26
  171. package/dist/providers/stripe/index.js +2 -1
  172. package/dist/providers/stripe/safety.js +2 -2
  173. package/dist/providers/stripe/skill.generated.js +1 -1
  174. package/dist/providers/tithely/api.js +5 -8
  175. package/dist/providers/tithely/index.js +3 -2
  176. package/dist/providers/tithely/skill.generated.js +1 -1
  177. package/dist/providers/tithely/tools.js +7 -5
  178. package/dist/providers/vercel/api.js +2 -7
  179. package/dist/providers/vercel/lib/pin.js +1 -1
  180. package/dist/providers/vercel/safety.js +1 -1
  181. package/dist/providers/vercel/skill.generated.js +1 -1
  182. package/dist/providers/vercel/tools.js +6 -4
  183. package/dist/redaction-marker.js +2 -2
  184. package/dist/registry.d.ts +11 -5
  185. package/dist/registry.js +46 -20
  186. package/dist/request-principal.js +1 -1
  187. package/dist/request-state.js +2 -2
  188. package/dist/routes/access-tokens.js +1 -1
  189. package/dist/routes/activity-disclosure.js +1 -1
  190. package/dist/routes/connect.js +2 -2
  191. package/dist/routes/credentials.js +130 -61
  192. package/dist/routes/dispatch.js +5 -1
  193. package/dist/routes/mcp.js +34 -17
  194. package/dist/routes/oauth-management.js +1 -1
  195. package/dist/routes/operator.js +6 -5
  196. package/dist/routes/shared.d.ts +13 -8
  197. package/dist/routes/shared.js +15 -9
  198. package/dist/routes/ui-config.js +3 -3
  199. package/dist/routes/ui-data.js +126 -20
  200. package/dist/routes/ui.js +17 -1
  201. package/dist/runtime/call-admission.js +1 -1
  202. package/dist/runtime/call.js +8 -3
  203. package/dist/runtime/native-storage.js +9 -0
  204. package/dist/runtime/request-reads.js +182 -0
  205. package/dist/runtime/request-scope.js +16 -0
  206. package/dist/runtime/settlement.js +1 -1
  207. package/dist/runtime/shared-read.js +2 -15
  208. package/dist/runtime/storage.js +12 -0
  209. package/dist/sent-secrets.d.ts +1 -2
  210. package/dist/sent-secrets.js +3 -8
  211. package/dist/server.js +4 -4
  212. package/dist/skills.js +44 -54
  213. package/dist/sqlite.d.ts +0 -19
  214. package/dist/sqlite.js +2 -83
  215. package/dist/storage/keys.js +69 -128
  216. package/dist/storage/memory.js +8 -2
  217. package/dist/storage/sql-native.js +27 -101
  218. package/dist/storage/sql.d.ts +1 -24
  219. package/dist/storage/sql.js +13 -14
  220. package/dist/storage-digest.js +32 -0
  221. package/dist/timeout.js +8 -0
  222. package/dist/tool-error.js +4 -0
  223. package/dist/types.d.ts +33 -0
  224. package/dist/url-safety.js +0 -4
  225. package/dist/usage-guide.js +18 -37
  226. package/dist/validate.d.ts +3 -0
  227. package/dist/validate.js +220 -158
  228. package/dist/version.d.ts +1 -1
  229. package/dist/version.js +1 -1
  230. package/dist/worker.js +1 -1
  231. package/dist/write-outcome.js +0 -3
  232. package/documentation/README.md +19 -55
  233. package/documentation/architecture.md +436 -515
  234. package/documentation/auth.md +229 -88
  235. package/documentation/code-mode.md +201 -94
  236. package/documentation/deploying.md +25 -43
  237. package/documentation/integrating.md +18 -19
  238. package/documentation/meta-tools.md +225 -51
  239. package/documentation/migration-0.33.md +10 -6
  240. package/documentation/migration-0.34.md +5 -5
  241. package/documentation/migration-retired-layouts.md +74 -0
  242. package/documentation/operator-tests.md +20 -120
  243. package/documentation/operator-ui.md +108 -16
  244. package/documentation/pool-membership-upgrade.md +44 -0
  245. package/documentation/provider-auth-migration.md +4 -10
  246. package/documentation/provider-migration-0.29.md +4 -15
  247. package/documentation/providers.md +105 -0
  248. package/examples/worker/AGENTS.md +14 -16
  249. package/examples/worker/README.md +24 -118
  250. package/examples/worker/src/connecta.config.ts +1 -0
  251. package/guides/connecting-agents.md +9 -31
  252. package/guides/how-it-works.md +6 -4
  253. package/guides/safety-and-trust.md +4 -1
  254. package/package.json +19 -19
  255. package/templates/node/AGENTS.md +13 -12
  256. package/templates/node/README.md +9 -26
  257. package/templates/node/package.json +1 -1
  258. package/templates/node/src/connecta.config.ts +1 -0
  259. package/dist/auth/oauth-v2-migration.js +0 -135
  260. package/dist/provider-call-facts.d.ts +0 -115
  261. package/dist/provider-call.d.ts +0 -112
  262. package/dist/storage/d1-copy.js +0 -255
  263. package/dist/storage/sql-schema.d.ts +0 -6
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -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)`
@@ -727,8 +787,17 @@ bypasses the scoped registry. These refusals happen before any request leaves. A
727
787
  never reach it; a deployment's own connectors still cannot read the caller.
728
788
 
729
789
  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
790
+ `maintainers` lists the people who receive the whole catalog at plain `/mcp`
791
+ with the deployment's `readOnly`, and who reach the operator routes. A
792
+ non-maintainer person listed in exactly one pool receives that pool at plain
793
+ `/mcp` exactly as `/mcp/<pool>` serves it: its `tools`, `readOnly`, `surface`,
794
+ `executeCode` and `description`. A maintainer keeps the whole catalog even when
795
+ also listed in pools. Membership is matched on the admitted, resolved
796
+ `{ namespace, id }`, so a bare `"bob"` and the explicit reference it resolves
797
+ to are the same person, and the same id in another namespace is not. Authentication, OAuth challenges and resource audiences are plain `/mcp`'s;
798
+ the pool is chosen after authentication. Construction warns, naming each
799
+ non-maintainer person listed in several pools, and `describeConfig()` lists them
800
+ in `peopleInSeveralPools`. Each pool's
732
801
  `members` lists who reaches that pool, people at `/mcp/<pool>` and agents at
733
802
  `/agent/<pool>`, which serve exactly that pool's `tools`. A maintainer opens no pool implicitly; list them as a member to use
734
803
  one. Nothing intersects these lists with anything else, and a request never
@@ -776,6 +845,12 @@ replacement. A guarded read grant becomes a pool with `readOnly: true`.
776
845
 
777
846
  ### Team pools
778
847
 
848
+ Every person in exactly one pool can configure plain `/mcp`; their sign-in
849
+ selects that pool, so one client configuration serves every team. A person in
850
+ several pools uses each pool's `/mcp/<pool>`. Only people pools count: a pool
851
+ lists only people or only agents, so an agent's pools never change where a
852
+ person's plain `/mcp` routes.
853
+
779
854
  Keep membership in deployment code; static lists can be built with ordinary
780
855
  code at construction:
781
856
 
@@ -800,9 +875,9 @@ Marketing's every `campaigns` tool and only `issues.search`. Neither pool
800
875
  serves `billing`; only the owner's plain `/mcp` does. A person in both pools
801
876
  uses two endpoints, never a union.
802
877
 
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)).
878
+ A human team pool and an agent tier are separate pools. Share a tool-list
879
+ constant between them, with people in `team` and only agent references in
880
+ `assistant-team` ([pool layout](#organizing-an-agents-pools)).
806
881
 
807
882
  | Catalog change | Exact `connector.tool` in a pool |
808
883
  | ----------------------- | ----------------------------------------------------------------------- |
@@ -891,16 +966,16 @@ warning says, every pool path answers the 404 because nobody can be a member,
891
966
  and every agent endpoint answers the machine-only 401 because no agent can be
892
967
  admitted.
893
968
 
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. |
969
+ | Rule | Contract |
970
+ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
971
+ | 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. |
972
+ | 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. |
973
+ | Agents | An agent member reaches its pools only at `/agent/<pool>`, with its `cta_` token. Human endpoints refuse `cta_`; see [agent endpoints](#agent-endpoints). |
974
+ | 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). |
975
+ | 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. |
976
+ | 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. |
977
+ | 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. |
978
+ | 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
979
 
905
980
  Pool descriptions are trimmed before validation. The remaining text must be
906
981
  1 to 500 characters and contain no C0 or C1 control characters (including tabs,
@@ -947,7 +1022,12 @@ Protected-resource metadata stays public at both default aliases,
947
1022
  Discovery does not authenticate callers or evaluate membership, so fresh
948
1023
  OAuth clients can discover how to authenticate. Admission applies after auth
949
1024
  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
1025
+ originating requested pathname and admitted pool; paging re-admits the caller
1026
+ at that endpoint and re-resolves their pool, which must still match. A handle
1027
+ minted at plain `/mcp` never pages at `/mcp/<pool>` or the reverse, even for the
1028
+ same pool, and a membership change fails it closed. Handles minted before this
1029
+ binding existed fail closed. Auth-elicitation and downstream-input
1030
+ continuations carry the same pathname and pool binding. A pool
951
1031
  cannot read a `/mcp` handle, and `/mcp` cannot read a pool's handle.
952
1032
 
953
1033
  Operator catalog (`/ui/data`, `/ui/connectors/<id>`), activity, shared
@@ -964,9 +1044,10 @@ activity. Anyone else gets the same 404, and an agent token the machine-only
964
1044
  shell carries no catalog data.
965
1045
 
966
1046
  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
1047
+ plain `/mcp` endpoint for maintainers and is required: a deployment that omits it fails at
968
1048
  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
1049
+ pool sets its own `readOnly`, in either direction. A person routed to their
1050
+ pool at plain `/mcp` gets that pool's effective `readOnly`. A writable endpoint lets
970
1051
  `call_destructive_tool` and programs write, and the MCP client decides what to
971
1052
  approve. On a read-only endpoint, write tools are absent from `search_tools`
972
1053
  results, `connecta.search`, and `connecta.describe`, and `call_tool`,
@@ -997,9 +1078,9 @@ for agents, just as an `authScope: "personal"` connector is: `search_tools`,
997
1078
  `connecta.search`, `connecta.describe`, `skills`, and connector discovery omit
998
1079
  it, `authorize_connector` answers "Unknown connector", and a call by address
999
1080
  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.
1081
+ People use a separate people-only pool, where they see the connector and
1082
+ act as themselves. Share the same tool-list constant between the people pool
1083
+ and an agent-only pool when both need the same tools.
1003
1084
 
1004
1085
  `owner` is a Workspace email address, checked at construction and listed in
1005
1086
  `describeConfig()` (`null` when unset). Over HTTP, `/ui/api/config` serves it
@@ -1025,7 +1106,7 @@ createConnecta({
1025
1106
  pools: Object.fromEntries(
1026
1107
  team.map((person) => [
1027
1108
  `assistant-${person.handle}`,
1028
- { tools: ["gmail", "drive"], owner: person.email, members: [person.id, "agent:assistant"] },
1109
+ { tools: ["gmail", "drive"], owner: person.email, members: ["agent:assistant"] },
1029
1110
  ]),
1030
1111
  ),
1031
1112
  connectors: [
@@ -1037,12 +1118,11 @@ createConnecta({
1037
1118
  ```
1038
1119
 
1039
1120
  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)).
1121
+ keeps one person's mailbox from another's. Personal agent pools list only
1122
+ the agent, whether or not they set an `owner`. People use separate pools;
1123
+ their Workspace calls use the mapping and always act as themselves. A shared-auth
1124
+ connector holding a user's own account runs as that account for every admitted
1125
+ agent ([personal pools](#organizing-an-agents-pools)).
1046
1126
 
1047
1127
  A fixed `subject` address is the explicit shared-account exception: every
1048
1128
  caller, person or agent, acts as it in every pool that lists the connector,
@@ -1067,10 +1147,10 @@ section holds the complete rules.
1067
1147
  2. **Tiers.** An agent's tiers are named pools ordered from least to most
1068
1148
  capable. Recommended names are `<agent>` for the lowest tier and
1069
1149
  `<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`.
1150
+ Every tier is agent-only. A human team with the same reach uses a separate
1151
+ pool sharing the same tool-list constant (rule 10). An agent with one tier
1152
+ uses it as the lowest. Tiers never hold per-user connectors and never set
1153
+ an `owner`.
1074
1154
  3. **Personal pool `<agent>-<user>`.** `/agent/<agent>-<user>` serves pool
1075
1155
  `<agent>-<user>`: the highest tier that user is cleared for, plus that
1076
1156
  user's own connectors. Use it only in a one-on-one with that user.
@@ -1103,26 +1183,24 @@ section holds the complete rules.
1103
1183
  reaches every tier above. For example, billing restricted to leads goes
1104
1184
  in the leads tier. Per-user connectors (`notes_alice`) go only in personal
1105
1185
  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.
1186
+ the agent uses. People use a separate pool for their own accounts.
1107
1187
  8. **Changes stay in config.** Adding an account for a user adds its
1108
1188
  connector and lists it in that user's pool `tools`. Adding a user adds one
1109
1189
  `<agent>-<user>` pool. Adding a tier adds one pool and one constant, and
1110
1190
  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.
1191
+ 9. **Personal pools list only the agent.** An `<agent>-<user>` pool lists
1192
+ only `"agent:<agent>"`, with or without an `owner`. No person belongs
1193
+ there: mixed membership throws `ConfigError` at construction. A person's
1194
+ own use goes through a separate people-only pool or plain `/mcp`.
1195
+ 10. **People use separate team pools.** A tier is never a human team pool.
1196
+ Define `teamTools` once, then use it for `team` with people in
1197
+ `members` and `assistant-team` with only the agent. People open
1198
+ `/mcp/team`; the agent opens `/agent/assistant-team`. Every person
1199
+ listed must be cleared for every shared connector in their pool.
1200
+ The human pool may also list Workspace mapping connectors and
1201
+ `authScope: "personal"` connectors, on which each person acts as
1202
+ themselves. Keep them out of the tier: they are absent for agents,
1203
+ since an agent has no principal and a tier has no owner.
1126
1204
 
1127
1205
  **Choosing the pool each turn**
1128
1206
 
@@ -1176,8 +1254,9 @@ createConnecta({
1176
1254
  pools: {
1177
1255
  // Lowest tier and fallback: reads guests may use.
1178
1256
  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"] },
1257
+ // Same reach, separate membership: people and the agent each have a pool.
1258
+ team: { tools: teamTools, members: ["user_alice", "user_bob"] },
1259
+ "assistant-team": { tools: teamTools, members: [agent] },
1181
1260
  "assistant-leads": { tools: leadsTools, members: [agent] },
1182
1261
  // Each personal pool spreads its user's highest tier.
1183
1262
  "assistant-alice": {
@@ -1235,8 +1314,8 @@ gets `assistant`, even if the platform marks the chat as direct. Rule 9 is
1235
1314
  why each personal pool lists only the agent: people act as themselves only
1236
1315
  through a Workspace mapping, and a shared-auth connector such as
1237
1316
  `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.
1317
+ Rule 10 gives Alice and Bob a separate `team` pool sharing `teamTools`;
1318
+ `poolFor` selects only agent pools and its tier selection stays the same.
1240
1319
 
1241
1320
  ## Shared and personal auth
1242
1321
 
@@ -1257,8 +1336,8 @@ A personal connector is absent — not refused — from any request without a
1257
1336
  stable namespaced principal. For one that can see it, connector storage, vault
1258
1337
  records, OAuth generations, and observed result shapes are
1259
1338
  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
1339
+ include that owner and the admitted pool, plus the keyed credential digest and
1340
+ OAuth epoch (see [cache keys](./architecture.md#storage-credentials-and-connectors)); refreshes coalesce within
1262
1341
  one owner, and another owner's epoch cannot retire them. Keep namespaces and
1263
1342
  principal ids stable across upgrades, since changing either selects different
1264
1343
  partitions. Literal `auth: { type: "headers" }` keeps its secret in deployment
@@ -1286,16 +1365,24 @@ connector](#downstream-oauth-on-api) stores no client. A vault without the
1286
1365
  optional `seal`/`open` members keeps the body plaintext with a startup
1287
1366
  warning; no vault, plaintext as always.
1288
1367
 
1289
- **From 0.28 (layout 2).** The first read of an owner's grant that finds no
1290
- record reads layout 2 once: the active generation's issuer-stamped client,
1291
- tokens, and discovery (sealed values open under their old keys), kept only
1292
- when every credential carries a stamp and the stamps agree with each other
1293
- and with discovery. It writes them as the grant record by compare-and-set,
1294
- then deletes every layout-2 key, cleanup lineage included. A disconnected
1295
- connector stays disconnected; a cancelled request migrates nothing. Grants
1296
- from before issuer binding (v0.8.1 and earlier) and pending consents are not
1297
- carried, so those connectors need Connect again. Rolling back past this
1298
- release finds no layout-2 grant and needs consent.
1368
+ The body also holds `expiresAt`, the epoch milliseconds the stored access token
1369
+ expires: its `expires_in`, counted from just before the request that issued it
1370
+ was sent (the code exchange, or the refresh's token request), never from when
1371
+ its answer arrived or was saved, so a slow answer cannot push the expiry past
1372
+ the token's real one. An `expires_in` of `0` expires at issuance. It is written
1373
+ with the tokens it describes, replaced with them and dropped with them, in the
1374
+ sealed and plaintext layouts alike. A grant stored before this field existed,
1375
+ or one whose token response carried no `expires_in`, has none. Reading a grant
1376
+ never adds it (INV-10), so such a grant gains one only at its next token write,
1377
+ and until then is refreshed only after a 401.
1378
+
1379
+ **Pre-layout-3 grants.** The layout-2 migration and historical-key cleanup
1380
+ are removed. An owner with only old keys is treated as unauthorized; a warning
1381
+ names the unsupported layout and the need to Connect again or migrate on
1382
+ 0.37.1. The check inspects key names only, never old credential values, and
1383
+ leaves them untouched. Existing layout-3 records remain readable even with an
1384
+ obsolete cleanup marker. Complete every owner's grant migration on 0.37.1
1385
+ before upgrading, or reconnect afterward; follow [Retired layout migrations](./migration-retired-layouts.md).
1299
1386
 
1300
1387
  ## Starting, restarting, and disconnecting
1301
1388
 
@@ -1397,12 +1484,8 @@ exchange. Both built-in `finishAuth` adapters require `callbackParams` with a
1397
1484
  nonempty `state`, even with PKCE disabled or after a separate `verifyState` call.
1398
1485
  A missing state is refused before discovery, registration, or token dispatch.
1399
1486
 
1400
- Layout-2 migration commits `cleanupPending` in the new grant before deleting
1401
- historical keys. Every later grant read retries deletion, then clears the
1402
- marker by CAS only when deletion succeeds. Restart preserves this obligation.
1403
- Disconnect commits a tombstone with the marker and always removes historical
1404
- keys for the connector and owner, including when a modern grant already exists.
1405
- A crash during cleanup cannot restore the disconnected grant.
1487
+ Disconnect commits a layout-3 tombstone. Restart and Disconnect never read or
1488
+ restore historical layout-2 keys.
1406
1489
 
1407
1490
  Disconnect first removes the grant and fences its epoch with CAS. When that
1408
1491
  removed grant's saved authorization-server metadata advertises a revocation
@@ -1426,6 +1509,47 @@ removal and revocation flow when deployment configuration supplies
1426
1509
  `revocationEndpoint`; without it, removal remains local. The configured
1427
1510
  client secret is used in memory and is never saved with the grant.
1428
1511
 
1512
+ ## Refreshing before expiry
1513
+
1514
+ A call refreshes the grant before it sends a token that is within 60 seconds of
1515
+ its stored `expiresAt`, so a write never meets the 401 it could not be repeated
1516
+ after (INV-9). Sixty seconds covers the refresh's own 20-second token-request
1517
+ deadline and the call's send with margin, and matches the early refresh of
1518
+ Google service-account tokens; for a typical one-hour token it is under 2% of
1519
+ its life. A token that lives under a minute, or arrives already expired, is
1520
+ refreshed once per call, in the invocation's check. Later in the same request,
1521
+ `callTool` and `ctx.oauth.fetch` send the token that renewal produced without
1522
+ renewing it again until its stored expiry passes (or, for a token already
1523
+ expired when issued, for the next check only); a token another request rotated
1524
+ in since is checked as usual.
1525
+
1526
+ The renewal is the flow a 401 would start, through the same refresh
1527
+ coordinator: one redemption per owner partition and epoch in an isolate, the
1528
+ shared-storage lease and spent-fingerprint CAS across isolates, and the commit
1529
+ before anyone is released. Its token request names the access token it
1530
+ replaces, so a caller whose own read came before another request's rotation
1531
+ adopts that rotation instead of redeeming again. Two isolates that find the
1532
+ same token due spend one refresh token between them.
1533
+
1534
+ | Where | When | A failed renewal |
1535
+ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1536
+ | 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`. |
1537
+ | `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. |
1538
+ | `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. |
1539
+
1540
+ Whether a token is due is decided from the grant read the step already made: a
1541
+ grant not yet due costs no extra storage read. A due token costs one more grant
1542
+ read for the renewal's own flow, and then the refresh's own reads and writes.
1543
+ Status, listing (including the stored-catalog read without a connection),
1544
+ discovery, the operator UI, and `authorize_connector` never renew a token
1545
+ (INV-10). Nothing renews in the background: renewal belongs to the call that
1546
+ found the token due, which owns its preparation until the claim CAS is
1547
+ submitted, as for a 401.
1548
+
1549
+ A grant without `expiresAt` keeps the earlier behavior: it is refreshed after a
1550
+ downstream 401, a write's 401 is never resent, and the refresh that follows
1551
+ stores `expiresAt` for every later call.
1552
+
1429
1553
  ## Refresh failures
1430
1554
 
1431
1555
  A refresh token is spent when dispatch begins. Every refresh request passes
@@ -1434,7 +1558,7 @@ lease's `claimed` to `dispatched` CAS and a separate fingerprint CAS at
1434
1558
  `oauth:refresh-spent:<sha256(refresh_token)>`. The latter must succeed before
1435
1559
  any HTTP request leaves. Connector and owner storage namespaces partition both
1436
1560
  records. Spent records have **no TTL**, contain no token, and are never deleted
1437
- by completion, failure, migration cleanup, Restart, Disconnect, or epoch sweeps.
1561
+ by completion, failure, Restart, Disconnect, or epoch sweeps.
1438
1562
  Copy these durable records during storage migration.
1439
1563
 
1440
1564
  Each fingerprint record names its dispatch epoch, holder, and resolution state:
@@ -1500,10 +1624,20 @@ a dispatched record. The refresh HTTP request and response body have a
1500
1624
  20-second deadline. After the response or a dispatched exchange failure,
1501
1625
  grant commit and cleanup share a separate 20-second deadline, including all
1502
1626
  storage reads, CAS retries, fingerprint resolution, and lease release.
1503
- Contenders wait at most 35 seconds with 10 ms to 250 ms
1504
- backoff. A waiter deadline returns retryable `unavailable`, keeps the grant,
1505
- and starts no consent. It never permits resending a spent token.
1506
-
1627
+ Contenders wait at most 35 seconds, and never past the caller's own signal,
1628
+ which carries the request's deadline. A waiter polls the lease from 50 ms,
1629
+ doubling to at most 2 seconds between polls, so a wait that runs the full 35
1630
+ seconds polls at most 22 times. Each poll is one batched storage read of the
1631
+ lease, the grant, the spent fingerprint, and the holder's liveness key named by
1632
+ the previous poll (one D1 statement); a store without batched reads reads them
1633
+ one at a time, lease first, so a completed lease is never paired with the grant
1634
+ from before its commit. A waiter deadline returns retryable `unavailable`,
1635
+ keeps the grant, and starts no consent. It never permits resending a spent
1636
+ token.
1637
+
1638
+ A grant with `expiresAt` is normally renewed before its token expires
1639
+ ([Refreshing before expiry](#refreshing-before-expiry)). A token revoked early,
1640
+ or one stored without an expiry, can still draw a 401.
1507
1641
  A `remoteMcp()` write the downstream answered with HTTP 401 is never sent
1508
1642
  again (INV-9). The SDK still refreshes the grant, and connecta stops its
1509
1643
  resend before it leaves, so any fresh call after the target is reconciled uses
@@ -1981,8 +2115,8 @@ anything, so SDK hook order cannot mix servers or cross a reset.
1981
2115
 
1982
2116
  Management follows `maintainers` and pool `members`; no resolver configures it. The former `identity.credentialAdministration`,
1983
2117
  `identity.personalConnection`, `identity.accessTokenManagement`, and
1984
- `identity.activityAccess` each fail construction naming their replacement,
1985
- and so does the `identity` section itself, even empty.
2118
+ `identity.activityAccess` fail construction as unknown options under `identity`,
2119
+ including an empty `identity` section.
1986
2120
 
1987
2121
  | Action | Who may |
1988
2122
  | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -2417,6 +2551,15 @@ Before an accepted retry restarts any call, it rechecks every entered read.
2417
2551
  A write classification, changed classification digest, or stale catalog returns
2418
2552
  `auth_replay_refused` with reconciliation guidance, including in programs on a writable endpoint.
2419
2553
  Pre-invocation credential-resolution eligibility is unchanged.
2554
+ Custom vaults may implement the host-only `storageDigestKey` method, returning a
2555
+ non-extractable HMAC-SHA-256 signing `CryptoKey` that is stable across the
2556
+ deployment's processes, purpose-specific and never stored.
2557
+ `encryptedCredentialVault()` derives it with HKDF from its credential key.
2558
+ Remote catalog, negotiation-verdict and connector-health keys digest their
2559
+ inputs (configured URLs and headers, credentials, request tokens, OAuth token
2560
+ responses, described configuration) with it, so their partitions are
2561
+ deployment-keyed. Without it they use a random per-process key: partitions stay
2562
+ exact, but each process or isolate keeps its own entries.
2420
2563
  Custom vaults may implement the host-only `requestStateKey` method; without it, the
2421
2564
  deployment retains the ordinary handoff. It must return a stable,
2422
2565
  purpose-specific deployment key of at least 32 bytes. Only the MCP boundary
@@ -2722,6 +2865,9 @@ owner's access token as `Authorization: Bearer` with these rules:
2722
2865
 
2723
2866
  - Only `apiOrigins` receive tokens; refuse other origins before sending, including
2724
2867
  untrusted response URLs. Return redirects unfollowed; handlers cannot set `Authorization`.
2868
+ - A token within a minute of its stored expiry is renewed before the handler
2869
+ runs, and again before any request that finds it due
2870
+ ([Refreshing before expiry](#refreshing-before-expiry)).
2725
2871
  - On 401, recover exactly once using an already rotated token or one coalesced
2726
2872
  refresh through `remoteMcp()`'s coordinator. Persist rotation even when its
2727
2873
  owner is cancelled after the answer. Only a positively read-classified
@@ -3127,10 +3273,6 @@ The encoding lattice is exactly this, and anything outside it is out of scope:
3127
3273
  lattice, so redaction is not a defense against a downstream that reshapes a
3128
3274
  value on purpose.
3129
3275
 
3130
- `test/sent-secret-output-boundary.test.ts` holds the evidence, including its
3131
- `review round 3`, `review round 4` and `review round 5` blocks for these limits;
3132
- `test/writeonly-wire-redaction.node.test.ts` runs the QuickJS guest cases.
3133
-
3134
3276
  ### Source and tests
3135
3277
 
3136
3278
  [Decision 0007](https://github.com/zackbart/connecta/blob/main/decisions/0007-agent-pool-layout.md)
@@ -3151,10 +3293,9 @@ for credential precedence and disclosure boundaries.
3151
3293
 
3152
3294
  Downstream OAuth implementation and evidence are in
3153
3295
  [downstream-oauth.ts](https://github.com/zackbart/connecta/blob/main/src/auth/downstream-oauth.ts),
3296
+ its [seam modules](https://github.com/zackbart/connecta/tree/main/src/auth/downstream-oauth),
3154
3297
  [static-oauth.ts](https://github.com/zackbart/connecta/blob/main/src/auth/static-oauth.ts),
3155
3298
  [OAuth connect tests](https://github.com/zackbart/connecta/blob/main/test/oauth-connect.test.ts),
3156
- [callback fencing](https://github.com/zackbart/connecta/blob/main/test/oauth-callback-fencing.test.ts),
3157
- [refresh leases](https://github.com/zackbart/connecta/blob/main/test/oauth-refresh-lease.test.ts),
3158
3299
  and [API OAuth tests](https://github.com/zackbart/connecta/blob/main/test/api-oauth.test.ts).
3159
3300
  Use the test pointers within each section for narrower failure cases.
3160
3301