@zackbart/connecta 0.28.0 → 0.29.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 (320) hide show
  1. package/CHANGELOG.md +961 -5
  2. package/README.md +62 -39
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1632 -1509
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +526 -0
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +220 -0
  52. package/dist/connectors/api-connector.js +214 -0
  53. package/dist/connectors/api.d.ts +8 -71
  54. package/dist/connectors/api.js +13 -99
  55. package/dist/connectors/guarded-fetch.d.ts +144 -0
  56. package/dist/connectors/guarded-fetch.js +108 -25
  57. package/dist/connectors/negotiation-cache.js +46 -0
  58. package/dist/connectors/option-shapes.js +27 -0
  59. package/dist/connectors/remote-mcp.d.ts +30 -4
  60. package/dist/connectors/remote-mcp.js +1780 -351
  61. package/dist/connectors/resource-uri.js +335 -0
  62. package/dist/credential-contract.d.ts +10 -0
  63. package/dist/credential-rules.js +1 -3
  64. package/dist/credential-url.js +108 -0
  65. package/dist/credentials.d.ts +6 -1
  66. package/dist/credentials.js +27 -18
  67. package/dist/d1.d.ts +130 -0
  68. package/dist/d1.js +290 -0
  69. package/dist/describe-config.d.ts +165 -0
  70. package/dist/describe-config.js +315 -0
  71. package/dist/described.js +106 -0
  72. package/dist/downstream-input-context.js +119 -0
  73. package/dist/downstream-input.js +323 -0
  74. package/dist/errors.d.ts +97 -20
  75. package/dist/errors.js +158 -80
  76. package/dist/execute.js +453 -305
  77. package/dist/executor-admission.d.ts +1 -0
  78. package/dist/executor-admission.js +11 -15
  79. package/dist/executor-contract.d.ts +20 -0
  80. package/dist/executor-contract.js +48 -0
  81. package/dist/executor-result.js +24 -9
  82. package/dist/executors/quickjs-child.js +3 -4
  83. package/dist/executors/quickjs-runtime.js +147 -59
  84. package/dist/executors/quickjs.js +29 -35
  85. package/dist/favicon.js +1 -2
  86. package/dist/fix-prompt.js +1 -3
  87. package/dist/guest-runtime.js +188 -0
  88. package/dist/guest-types.d.ts +176 -0
  89. package/dist/guest-types.js +1 -0
  90. package/dist/html-security.js +24 -0
  91. package/dist/identity.js +1 -3
  92. package/dist/inbound-credential.js +52 -0
  93. package/dist/index.d.ts +25 -257
  94. package/dist/index.js +65 -478
  95. package/dist/invocation-auth.js +69 -0
  96. package/dist/invocation.js +230 -73
  97. package/dist/json-schema.d.ts +1 -1
  98. package/dist/mcp-client-context.js +35 -0
  99. package/dist/mcp-result.js +11 -7
  100. package/dist/meta-output.js +76 -0
  101. package/dist/meta-tool-names.d.ts +2 -0
  102. package/dist/meta-tool-names.js +3 -0
  103. package/dist/meta-tools.js +285 -209
  104. package/dist/module-contracts.d.ts +8 -23
  105. package/dist/node.d.ts +0 -2
  106. package/dist/node.js +2 -2
  107. package/dist/oauth-callback-outcome.js +1 -3
  108. package/dist/oauth-handoff.js +117 -0
  109. package/dist/oauth-partition.js +17 -0
  110. package/dist/oauth-sealing.js +20 -0
  111. package/dist/operator-record.d.ts +137 -0
  112. package/dist/operator-record.js +420 -0
  113. package/dist/operator-ui/assets.js +31 -0
  114. package/dist/operator-ui/contract.d.ts +72 -0
  115. package/dist/operator-ui/contract.js +1 -0
  116. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  117. package/dist/operator-ui/generated.js +5 -4
  118. package/dist/operator-ui/model.d.ts +12 -87
  119. package/dist/operator-ui/model.js +2 -9
  120. package/dist/operator-ui/setup-commands.js +1 -3
  121. package/dist/operator-ui/view.d.ts +323 -0
  122. package/dist/operator-ui/view.js +62 -189
  123. package/dist/page-styles.js +2 -3
  124. package/dist/program-source.js +14 -2
  125. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  126. package/dist/provider.d.ts +142 -0
  127. package/dist/provider.js +187 -0
  128. package/dist/providers/_shared/google/result-size.js +57 -0
  129. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  130. package/dist/providers/_shared/google/service-account.js +500 -0
  131. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  132. package/dist/providers/_shared/google/workspace.js +518 -0
  133. package/dist/providers/basecamp/index.d.ts +43 -0
  134. package/dist/providers/basecamp/index.js +997 -0
  135. package/dist/providers/basecamp/skill.generated.js +9 -0
  136. package/dist/providers/breeze/index.d.ts +34 -0
  137. package/dist/providers/breeze/index.js +1446 -0
  138. package/dist/providers/breeze/skill.generated.js +9 -0
  139. package/dist/providers/ccb/index.d.ts +103 -0
  140. package/dist/providers/ccb/index.js +1725 -0
  141. package/dist/providers/ccb/skill.generated.js +9 -0
  142. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  143. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  144. package/dist/providers/cloudflare/skill.generated.js +10 -0
  145. package/dist/providers/docs/index.d.ts +54 -0
  146. package/dist/providers/docs/index.js +1237 -0
  147. package/dist/providers/docs/skill.generated.js +9 -0
  148. package/dist/providers/drive/index.d.ts +53 -0
  149. package/dist/providers/drive/index.js +1523 -0
  150. package/dist/providers/drive/skill.generated.js +9 -0
  151. package/dist/providers/forms/index.d.ts +54 -0
  152. package/dist/providers/forms/index.js +1217 -0
  153. package/dist/providers/forms/skill.generated.js +11 -0
  154. package/dist/providers/github/auth.d.ts +32 -0
  155. package/dist/providers/github/auth.js +361 -0
  156. package/dist/providers/github/hosted.js +218 -0
  157. package/dist/providers/github/index.d.ts +13 -0
  158. package/dist/providers/github/index.js +140 -0
  159. package/dist/providers/github/key.js +100 -0
  160. package/dist/providers/github/rest.js +321 -0
  161. package/dist/providers/github/scope.d.ts +29 -0
  162. package/dist/providers/github/scope.js +124 -0
  163. package/dist/providers/github/skill.generated.js +8 -0
  164. package/dist/providers/gmail/index.d.ts +51 -0
  165. package/dist/providers/gmail/index.js +1436 -0
  166. package/dist/providers/gmail/skill.generated.js +9 -0
  167. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  168. package/dist/providers/linear/index.js +352 -0
  169. package/dist/providers/linear/skill.generated.js +8 -0
  170. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  171. package/dist/providers/mixpanel/index.js +416 -0
  172. package/dist/providers/mixpanel/skill.generated.js +9 -0
  173. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  174. package/dist/providers/{notion.js → notion/index.js} +226 -239
  175. package/dist/providers/notion/skill.generated.js +10 -0
  176. package/dist/providers/overflow/index.d.ts +60 -0
  177. package/dist/providers/overflow/index.js +1535 -0
  178. package/dist/providers/overflow/skill.generated.js +8 -0
  179. package/dist/providers/planning-center/index.d.ts +59 -0
  180. package/dist/providers/planning-center/index.js +2199 -0
  181. package/dist/providers/planning-center/skill.generated.js +10 -0
  182. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  183. package/dist/providers/revenuecat/index.js +557 -0
  184. package/dist/providers/revenuecat/skill.generated.js +11 -0
  185. package/dist/providers/sheets/index.d.ts +54 -0
  186. package/dist/providers/sheets/index.js +1237 -0
  187. package/dist/providers/sheets/skill.generated.js +10 -0
  188. package/dist/providers/slides/index.d.ts +53 -0
  189. package/dist/providers/slides/index.js +2645 -0
  190. package/dist/providers/slides/skill.generated.js +9 -0
  191. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  192. package/dist/providers/stripe/index.js +266 -0
  193. package/dist/providers/stripe/skill.generated.js +11 -0
  194. package/dist/providers/tithely/index.d.ts +49 -0
  195. package/dist/providers/tithely/index.js +1003 -0
  196. package/dist/providers/tithely/skill.generated.js +8 -0
  197. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  198. package/dist/providers/vercel/index.js +1038 -0
  199. package/dist/providers/vercel/mcp-ownership.js +212 -0
  200. package/dist/providers/vercel/skill.generated.js +11 -0
  201. package/dist/registry.d.ts +108 -171
  202. package/dist/registry.js +513 -925
  203. package/dist/request-state.js +21 -0
  204. package/dist/resource-read.js +100 -0
  205. package/dist/result-shapes.js +17 -49
  206. package/dist/retired-toolkits.js +1 -1
  207. package/dist/routes/access-tokens.js +3 -6
  208. package/dist/routes/activity-context.js +21 -0
  209. package/dist/routes/activity-disclosure.js +40 -0
  210. package/dist/routes/activity.js +59 -21
  211. package/dist/routes/actor-labels.js +3 -50
  212. package/dist/routes/connect.js +221 -0
  213. package/dist/routes/credentials.js +15 -27
  214. package/dist/routes/mcp.js +356 -130
  215. package/dist/routes/oauth-client-metadata.js +15 -0
  216. package/dist/routes/oauth-management.js +45 -107
  217. package/dist/routes/oauth.js +136 -57
  218. package/dist/routes/operator.js +3 -3
  219. package/dist/routes/shared.d.ts +25 -64
  220. package/dist/routes/shared.js +160 -80
  221. package/dist/routes/skills.js +30 -0
  222. package/dist/routes/ui-config.js +225 -0
  223. package/dist/routes/ui-data.js +40 -35
  224. package/dist/routes/ui.js +66 -56
  225. package/dist/runtime/admission.js +4 -2
  226. package/dist/runtime/call-admission.js +3 -1
  227. package/dist/runtime/connector-scope.js +1 -4
  228. package/dist/runtime/run.js +10 -39
  229. package/dist/runtime/services.js +12 -117
  230. package/dist/runtime/shared-read.js +1 -3
  231. package/dist/runtime/storage.js +8 -1
  232. package/dist/sent-secrets.d.ts +52 -0
  233. package/dist/sent-secrets.js +474 -0
  234. package/dist/server.js +28 -71
  235. package/dist/skills.js +406 -174
  236. package/dist/sqlite.d.ts +48 -0
  237. package/dist/sqlite.js +166 -0
  238. package/dist/storage/keys.js +428 -0
  239. package/dist/storage/memory.js +7 -0
  240. package/dist/storage/sql.d.ts +66 -0
  241. package/dist/storage/sql.js +534 -0
  242. package/dist/timeout.js +4 -2
  243. package/dist/tool-name.js +17 -0
  244. package/dist/tool-safety.d.ts +9 -35
  245. package/dist/tool-safety.js +20 -40
  246. package/dist/types.d.ts +320 -54
  247. package/dist/types.js +0 -1
  248. package/dist/ui.d.ts +7 -19
  249. package/dist/ui.js +76 -90
  250. package/dist/url-safety.js +3 -6
  251. package/dist/usage-guide.js +137 -0
  252. package/dist/validate.d.ts +7 -0
  253. package/dist/validate.js +309 -47
  254. package/dist/version.d.ts +1 -7
  255. package/dist/version.js +3 -7
  256. package/dist/worker.d.ts +11 -0
  257. package/dist/worker.js +434 -0
  258. package/documentation/architecture.md +665 -230
  259. package/documentation/auth.md +962 -458
  260. package/documentation/code-mode.md +501 -312
  261. package/documentation/meta-tools.md +449 -200
  262. package/documentation/operator-tests.md +93 -0
  263. package/documentation/provider-migration-0.29.md +60 -0
  264. package/examples/worker/AGENTS.md +34 -12
  265. package/examples/worker/README.md +286 -255
  266. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  267. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  268. package/examples/worker/src/connecta.config.ts +151 -0
  269. package/examples/worker/src/index.ts +14 -197
  270. package/examples/worker/wrangler.jsonc +15 -35
  271. package/package.json +125 -35
  272. package/templates/node/.dockerignore +1 -2
  273. package/templates/node/.env.example +20 -21
  274. package/templates/node/AGENTS.md +21 -17
  275. package/templates/node/Dockerfile +3 -5
  276. package/templates/node/README.md +133 -58
  277. package/templates/node/docker-compose.yml +11 -12
  278. package/templates/node/package.json +4 -2
  279. package/templates/node/src/connecta.config.ts +119 -0
  280. package/templates/node/src/index.ts +9 -154
  281. package/templates/node/src/provision-token.ts +17 -0
  282. package/AGENTS.md +0 -170
  283. package/dist/artifacts/connector.d.ts +0 -58
  284. package/dist/artifacts/connector.js +0 -730
  285. package/dist/artifacts/document.js +0 -86
  286. package/dist/artifacts/guide.js +0 -140
  287. package/dist/artifacts/html-scan.js +0 -329
  288. package/dist/artifacts/json.js +0 -78
  289. package/dist/artifacts/kv-store.d.ts +0 -20
  290. package/dist/artifacts/kv-store.js +0 -140
  291. package/dist/artifacts/markdown.js +0 -478
  292. package/dist/artifacts/module.d.ts +0 -37
  293. package/dist/artifacts/module.js +0 -66
  294. package/dist/artifacts/operations.d.ts +0 -197
  295. package/dist/artifacts/operations.js +0 -1002
  296. package/dist/artifacts/refresh.d.ts +0 -55
  297. package/dist/artifacts/refresh.js +0 -236
  298. package/dist/artifacts/routes.js +0 -206
  299. package/dist/artifacts/types.d.ts +0 -214
  300. package/dist/artifacts/types.js +0 -22
  301. package/dist/artifacts/validate.d.ts +0 -62
  302. package/dist/artifacts/validate.js +0 -651
  303. package/dist/artifacts.d.ts +0 -6
  304. package/dist/artifacts.js +0 -9
  305. package/dist/auth/bearer.d.ts +0 -11
  306. package/dist/auth/bearer.js +0 -50
  307. package/dist/providers/linear.js +0 -217
  308. package/dist/providers/mixpanel.js +0 -247
  309. package/dist/providers/revenuecat.js +0 -317
  310. package/dist/providers/stripe.js +0 -253
  311. package/dist/providers/vercel.js +0 -1253
  312. package/dist/storage/file.d.ts +0 -15
  313. package/dist/storage/file.js +0 -455
  314. package/ethos.md +0 -93
  315. package/examples/worker/src/cloudflare-kv.ts +0 -43
  316. package/examples/worker/src/d1-activity-row.ts +0 -155
  317. package/examples/worker/src/d1-activity.ts +0 -147
  318. package/examples/worker/src/d1-storage.ts +0 -138
  319. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  320. package/templates/node/src/file-activity.ts +0 -182
package/CHANGELOG.md CHANGED
@@ -2,6 +2,962 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.29.0 — 2026-10-08
6
+
7
+ Connecta 0.29.0 adopts the MCP 2026-07-28 request envelope, binding client capabilities and client identity to each request. `server/discover` advertises the extensions served and private cache hints. Connecta-owned access, pool and deadline refusals return JSON-RPC errors; admission codes move to `-33001` and `-33002`. Modern clients must preserve the protocol-version and declared argument headers. `tools.listChanged` is false, and `subscriptions/listen` remains unsupported.
8
+
9
+ The tool list is six: `execute_code`, `call_tool`, `call_destructive_tool`, `search_tools`, `authorize_connector` and `skills`. `get_result` is removed; programs page retained direct-call results with `connecta.result(id, options)`. `resume_execution` remains removed, with no paused programs to resume. Guest calls return `{ data, format: "json" | "text" }`, programs report host-call counts, and `connecta.emit` delivers media without requiring `await`. All six tools advertise output schemas.
10
+
11
+ One fail-closed classifier decides `read` or `write`, with exact deployment overrides ahead of provider review and downstream annotations. Per-tool approval exemptions are gone. Root and named pools independently default to `read-only`; `trusted` pools allow program writes and annotate `execute_code` as a write. Approval belongs to the MCP host. Trust widens no connector or identity grant. Dispatched writes are never automatically replayed, and unknown outcomes require reconciliation.
12
+
13
+ Errors distinguish host authentication, downstream OAuth and provider permissions. Routing, argument and recoverable authentication failures carry a repair `nextAction`; other failures retain typed retry or reconciliation guidance. Argument refusals include bounded schema repair details. Discovery returns one flat page with catalog failures before tools, ranks exact tool names and addresses first, and reports endpoint-scoped service absences without substituting lookalikes. Program discovery defaults to JSON Schema; compact schemas are labeled as text.
14
+
15
+ Humans authenticate through Clerk or Cloudflare Access. Access requires trusted direct Worker context; Node uses Clerk. Machines use stored `cta_` tokens, and the generic static bearer adapter is retired. Downstream OAuth starts through an identity-checked, single-use `/connect/<id>` page. Capable hosts receive MRTR URL elicitation; other hosts receive a connection handoff. Public HTTPS deployments default to self-hosted CIMD where the authorization server supports it, with DCR fallback and issuer-bound static clients available. Disconnect removes local authorization and attempts revocation once when the saved server advertises it. State is one grant record per connector and owner, plus one expiring flow record per consent.
16
+
17
+ Direct calls relay downstream `input_required` through connecta's sealed state, bound to the principal, pool, target and original arguments. Only declared form and URL input capabilities are forwarded. Programs receive direct-call repair guidance instead. A consumed continuation cannot be resent; continuation failures are not retryable. The Skills extension serves connecta guides, connector guides and opted-in downstream skills through one caller-scoped registry, shared with `skills` and `connecta.skill()`. Upward resource reads remain skill-only.
18
+
19
+ Downstream clients reuse SDK negotiation verdicts and complete SQL-backed catalog caches. Host-computed identity, credential and pool partitions bound sharing; downstream cache hints cannot widen it. Programs read advertised downstream resources with `connecta.read("resource://<connectorId>/<encodedUri>")`. URI template matching uses bounded forward scans and refuses ambiguous matches, authority changes and unsafe paths before dispatch.
20
+
21
+ Persistent storage is one SQL key-value store with D1 on Workers and SQLite on Node. Workers KV, the JSON file store and optional compare-and-set paths are removed. Result-stash bounds now apply across processes or isolates sharing the store. One configuration schema supplies types, validation and defaults; unknown keys and invalid values fail construction. `describeConfig()` supplies a secret-free snapshot for the operator UI and doctor.
22
+
23
+ Maintained providers use `defineProvider` and folders containing implementation, `SKILL.md`, drift evidence and tests; exports and check inventories are generated. Hosted MCP implementations are reviewed presets. Notion, Vercel and Cloudflare default to hosted MCP with explicit REST complements. The GitHub provider combines hosted tools and a REST complement through a GitHub App, supporting organization and repository scopes together with repository-narrowed installation tokens. It acts as the App; repository creation and forks are absent, and workflow-file writes are off by default. Provider drift checks run locally, on provider pull requests and weekly, reporting findings without filing issues. Google Workspace providers retain domain-wide delegation, including draft-only Gmail and Slides comments.
24
+
25
+ The operator UI is rebuilt on React, Radix and Tailwind with hashed immutable assets on both runtimes. It shows caller-scoped configuration, catalogs, access and activity; capability changes remain in deployment code. Activity records the real package version independently of display `serverInfo`, validated client name and version, call classification and result size, without payloads. Shipped executors carry lifecycle brands; custom sandboxes explicitly accept cleanup responsibility through `customExecutor(executor, { lifecycle: "self-managed" })`.
26
+
27
+ The built-in artifacts feature, package subpath, pages, scheduled refresh and agent tools are removed. A leftover `artifacts:` or `artifactOrigin` option throws `ConfigError`. Existing artifact data is ignored and untouched. Deployments without the built-in feature need no artifact migration; dedicated downstream publishing services remain ordinary connectors.
28
+
29
+ Agent evals now use the 0.29 tools and guest API with correctness grading for destination, evidence and final answers. Sonnet 5.5 and GPT-6-Luna baselines and their measured failures are recorded in `eval/baselines/notes-0.29.md`. Runner limitations are explicit N/A outcomes. These diagnostic trials do not establish a model ranking or native CLI URL-elicitation support.
30
+
31
+ Security fixes harden Clerk audience binding and downstream OAuth completion binding. Credential redaction and payload-free operator records cover shared request boundaries. GitHub grants bind to repository IDs, catalog sharing respects host auth partitions, and resource URI validation is tightened. OAuth token requests refuse redirects, and refresh coordination preserves one-time dispatch across cancellation and grant resets.
32
+
33
+ Contributors use oxfmt through `npm run format`; `npm run format:check` joins the verification gates. Generated files, fixtures, baseline data and released changelog history retain their existing serialization. Deployments need no formatting migration.
34
+
35
+ Before upgrading an existing deployment, back up its state and database, preserve the credential-encryption key and identity namespaces, and prepare the replacement configuration and imports. Delete `discovery.persistCatalog` and `discovery.staleCatalogSeconds`. Replace the old registry freshness policy with `discovery.catalogMinTtlSeconds` and `discovery.catalogMaxTtlSeconds`, defaulting to 0 and 86400; `catalogTtlSeconds` remains the 300-second fallback when a downstream omits TTL hints. Explicit zero hints disable reuse. Delete `execute.approval`, `Connector.approval`, `execute.resumableWrites` and `execute.pausedRunTtlSeconds`. Set root `trust` and each `pools.<name>.trust` deliberately; named pools do not inherit root trust. Remove artifacts configuration, the `@zackbart/connecta/artifacts` import, artifact bindings and refresh jobs. Replace legacy `credentials` with `vault`, move root branding into `operatorUi({ branding })`, and replace boolean or object `accessTokens` configuration with `accessTokens(storage)`. Fix rejected options before starting the new version.
36
+
37
+ Move persistent storage before reopening traffic. On Node 22.13 or later, stop the old server and run `connecta migrate-state <state.json> <connecta.sqlite>` once, then use `sqliteStorage` and `sqliteActivityStore` from `@zackbart/connecta/sqlite`. Replace `CONNECTA_STATE_FILE` and `CONNECTA_ACTIVITY_FILE` with `CONNECTA_DATABASE`; the old JSONL activity log is not imported. Workers use `d1Storage` and `d1ActivityStore` from `@zackbart/connecta/d1`, replacing copied KV/D1 adapters. Existing `connecta_kv` and `tool_call_activity` tables remain readable; the example uses `CONNECTA_DB` for both. `fileStorage` is no longer exported from `/node`, and custom stores must implement `list` and `compareAndSet`. Deployments using `memoryStorage()` can ignore the persistent-store conversion.
38
+
39
+ For Workers KV, use the Worker example's `scripts/copy-kv-to-d1.mjs` or `copyKvToD1` from `/d1` under maintenance. Back up values with expiration metadata and D1, stop background writers, and drain requests, callbacks and refreshes. Wait at least 60 seconds or the longest KV cache TTL after the last write, and require stable listing/hash passes. Copy before switching to D1, resolve invalid entries and conflicts, and verify exact value hashes and absolute expiries. Preserve the existing vault key. Keep maintenance through startup and credential checks, then mark the source live with `--mark-live` or `markKvToD1Live` before reopening traffic. Later copies refuse stale KV state. Keep backups until the upgrade is verified. OAuth layout migration runs automatically on each owner's first grant read; issuer-stamped grants carry over, pending links and pre-issuer-binding grants need Connect again. Rolling back after that migration requires consent again.
40
+
41
+ Replace `@zackbart/connecta/auth/bearer`, `bearerToken` and `assertedPrincipal` with `accessTokens(storage)`. Provision a separate stored token per machine or represented human, update grants to its metadata ID and `access_token` actor, then rotate clients to `Authorization: Bearer cta_…` and remove the static secret and `X-Connecta-Principal`. Keep exact principal namespaces and IDs when preserving personal state; existing unrevoked `cta_` tokens survive the storage copy. Worker machines also need Access service credentials at the edge. Protect `/connect/*` and `/oauth/callback/*` with the intended Worker Access application. Clerk deployments enable `aud_claim_enabled`, reconnect using the advertised endpoint URL as `resource`, and verify refresh. Custom auth adapters replace `final` and `finalRefusals` with synchronous credential recognition; consolidate Access wrappers that depended on refusal fallthrough.
42
+
43
+ Replace upstream `DynamicWorkerExecutor` construction with `workerExecutor({ loader: env.LOADER })` from `@zackbart/connecta/worker`, keeping the optional `@cloudflare/codemode` peer. Wrap custom executors with `customExecutor(..., { lifecycle: "self-managed" })` and implement termination and cleanup. Existing `quickJsExecutor()` configurations need no change. Public provider import names survive the folder move. Existing Notion integration workflows select `surface: "api"` and rename REST tools to `integration_*`; Vercel and Cloudflare users migrate removed operations to live hosted schemas and configure retained REST complements under separate connector IDs. Follow `documentation/provider-migration-0.29.md`. Update client workflows to the flat discovery page and `{ data, format }` guest results, page direct results through `connecta.result`, and remove calls to `get_result` or `resume_execution`.
44
+
45
+ Start the upgraded deployment under maintenance and run `connecta doctor` with its required machine and edge credentials. Doctor checks health, SDK protocol negotiation, the exact current six-tool set and an actual executor program, and reports the negotiated revision and package version. With the UI enabled, `doctor --config` prints the caller-scoped secret-free snapshot instead of running diagnostics. Check existing OAuth and vault credentials, intended identity and pool grants, token revocation and activity, reissue pending connection links, then reopen traffic. UI, provider and contributor changes require no additional migration for deployments that do not use them. These notes apply whenever a deployment chooses to upgrade; the release includes no owner deployment cutover.
46
+
47
+ ### Added
48
+
49
+ - Add seven Google Slides comment tools: `list_comments`, `create_comment`, `create_comment_reply`, `update_comment_thread`, `update_comment_post`, `delete_comment`, and `delete_comment_reply` (#696). Reads include authors, replies and anchors, with revision/content-bound cursors and raw thread paging. Comment mutations are writes; trusted pools may run them in programs, and the host controls approval. The raw batch hatch accepts comment requests and preserves reply ids ahead of text under its result budget. Oversized direct write results can be paged with program `connecta.result` on trusted endpoints. Read-only endpoints return an inline truncation notice and preview without a write stash. Results distinguish batch acceptance from comment save confirmation; a comment write without `ALL_SAVED` reports `commentUpdateState` and instructs callers to re-read instead of repeating it.
50
+ - **`Connector.classification`: connectors report facts, the registry
51
+ classifies** ([#705](https://github.com/zackbart/connecta/issues/705)). A
52
+ connector may carry a reviewed `ToolClassification` as the readonly
53
+ `classification` field. `remoteMcp({ classify })` and maintained providers set
54
+ it to a deep-frozen copy, and their `listTools` now returns the downstream's
55
+ listing unclassified. The registry validates the field when it first reads a
56
+ connector, rejects it beside `staticTools`, and keeps request-local classified entries built from intake-redacted listing facts.
57
+ The SQL-backed SDK cache stores complete intake-redacted facts without registry
58
+ classification verdicts, and each request classifies them into fresh objects. It also observes catalog drift for such connectors itself, and
59
+ ignores their `catalogDrift()`. A decorator that filters, copies, or annotates a
60
+ listing, or mutates tools it listed or was handed, cannot turn a reviewed write
61
+ into a read. Wrappers keep the review only by forwarding the field:
62
+ `{ ...connector }`, `Object.assign`, and `Object.create` do; a forwarding class
63
+ that omits it serves an unreviewed connector, whose downstream annotations fail
64
+ closed when absent. Exact deployment-level overrides
65
+ ([#706](https://github.com/zackbart/connecta/issues/706)) apply ahead of review
66
+ and downstream annotations, regardless of wrapping.
67
+
68
+ Request-scoped catalogs own their definitions and serve deep copies to
69
+ discovery and invocation. Each connector dispatch also receives a fresh deep
70
+ copy of its definition. Mutating a dispatched definition or a discovery
71
+ result cannot change later discovery, schema validation, or write accounting
72
+ within the same program.
73
+ - Add a GitHub provider with mixed org and repository scopes through a GitHub App, owner-routed and repository-narrowed installation tokens, exact hosted-tool allowlisting, scope discovery, confined search, release writes, and workflow-file writes disabled unless explicitly configured. Add request-local Bearer authentication and an opt-in classification allowlist that hides unlisted tools.
74
+ - `copyKvToD1(kv, db, { source, cursor, overwriteFamilies, maxKeys, verify })`
75
+ on `@zackbart/connecta/d1` copies a 0.28 Workers KV deployment's state into D1
76
+ during a maintenance window, preserving OAuth grants, vault credentials,
77
+ `cta_` tokens, and absolute expiries. It skips oversized UTF-8 strings/rows,
78
+ bounds buffered bytes, and reports invalid/conflict/verification counts by
79
+ family only. Resume tokens require the same source id and an atomic claim;
80
+ error labels use class identity. Overwrite requires an explicit family list.
81
+ `markKvToD1Live` seals the source in D1 before traffic reopens, refusing later
82
+ copies unless explicitly overridden. The Worker runbook backs up and drains
83
+ writers, waits for stable KV, copies and verifies before deployment, checks
84
+ existing credentials under maintenance, then reopens traffic (#709).
85
+ - **Google Docs connection.** `@zackbart/connecta/providers/docs` exports
86
+ `docs(id, options)` with the same Workspace options as `gmail()`, plus
87
+ `DOCS_SCOPES` (exactly `documents`) and `DOCS_API_BASE_URL`
88
+ ([#681](https://github.com/zackbart/connecta/issues/681)). Six hand-written
89
+ tools: `get_document` (every tab, or one, rendered as markdown-ish text —
90
+ headings, lists, links, tables as pipe rows, footnotes, and every inline chip
91
+ by what it shows, a dropdown's selected value included — capped by
92
+ `maxChars` with an explicit marker, with `title`, `revisionId`, on
93
+ `withIndexes` the UTF-16 start and end index of every paragraph, table, and
94
+ table of contents, paged by cursor in 64 KiB pages so a default read always
95
+ fits the 256 KiB `execute_code` bridge, and a per-tab `notRendered` naming headers, footers,
96
+ floating images, unmarked suggestions, or unknown elements the text leaves
97
+ out; `raw: true` returns Google's resource instead, up to 4 MB),
98
+ `create_document` (title and an optional body; a body that fails after the
99
+ document exists names its id instead of inviting a duplicate), `append_text`
100
+ and `insert_text` (additive), `replace_all_text` (string or RE2, by tab,
101
+ reporting the count), and `batch_update_document`, a raw
102
+ `documents.batchUpdate` passthrough annotated as destructive and classified as a write, takes 1–100
103
+ requests of known generally available kinds (contents unvalidated), returns
104
+ Google's replies within the shared result budget (projected to ids and
105
+ counts, then cut to a counted prefix, still saying the batch applied), and
106
+ requires `requiredRevisionId`. Every other edit takes it optionally, and a
107
+ write naming one is revision-guarded, so a stale revision fails `conflict`
108
+ on Google's own reason code. Failures are classified by how far the request
109
+ got, from the shared client's outcome facts: a 4xx refusal passes through
110
+ as mapped and applied nothing; an edit sent with no answer back, answered
111
+ with a 5xx (whatever reason it carries), or redirected says its outcome is
112
+ unknown, non-retryably, because no Docs write is safe to send twice; only a
113
+ 2xx whose reply broke off, overflowed, or would not parse says the edit was
114
+ applied; and a create that may have left a document behind never invites a
115
+ second one. Ids Google sends back are copied only if a tool could take them
116
+ as input again, and otherwise dropped whole and named in `dropped`; titles
117
+ are clamped; and every result passes one final size check, which a read
118
+ answers with its way out and a write with a small acknowledgment that it
119
+ applied. There is no search or
120
+ list tool: finding a document is Drive's job, and `documents` cannot list. A
121
+ 404 stays `connector_call_failed`, because Google answers it for an id that
122
+ is unknown and for one not shared with the caller alike. Setup, including
123
+ the Admin console scope, is documented on `docs()`; drift is checked against
124
+ the Docs Discovery document.
125
+
126
+ Trusted pools permit program writes; read-only pools use `call_destructive_tool`.
127
+ The MCP host controls approval.
128
+ - **Draft-only Gmail connection.** `@zackbart/connecta/providers/gmail`
129
+ exports `gmail(id, { purpose, serviceAccount, subject, title?,
130
+ instructions?, callAdmission?, maxResultBytes?, baseUrl? })`, plus
131
+ `GMAIL_SCOPES` and `GMAIL_API_BASE_URL`. Eight hand-written tools:
132
+ `search_threads` (Gmail search syntax, cursor-paged, each thread summarized),
133
+ `get_thread`, `get_message` (the message's own body — never an attached or
134
+ forwarded email's — decoded from its charset, HTML converted only when no
135
+ text part exists, fetched from the attachments endpoint when Gmail stored it
136
+ apart, reported as `bodyFormat: "unavailable"` rather than empty when too
137
+ large to read, capped by `maxBodyChars` with an explicit marker, and
138
+ metadata only for attachments, which include unnamed inline images and any
139
+ other part that is not body text), `list_labels`, `list_drafts`,
140
+ `get_draft`, `create_draft`
141
+ (To/Cc/Bcc, RFC 2047-encoded headers, an optional HTML alternative, and
142
+ `replyToMessageId`, which sets the thread, `In-Reply-To`, `References`, a
143
+ `Re:` subject, and every recipient of the replied message's Reply-To or
144
+ From, parsed as an RFC 5322 address list), and `update_draft` (replaces the
145
+ body; keeps From, To, Cc, Bcc, Reply-To, Subject, the thread, and the reply
146
+ headers unless restated, and no other header; refuses, unchanged, a draft
147
+ with attachments, inline images, or anything else Gmail's whole-message
148
+ update would delete, and one nested deeper than it inspects). Every result
149
+ is built to stay under 192 KiB of JSON, so it reaches a program through
150
+ `execute_code`'s 256 KiB bridge as well as `call_tool` — the whole result,
151
+ wrapper and cursor included, measured as sent rather than estimated:
152
+ `get_thread` and `list_labels` page by cursor when a thread or label set is
153
+ larger, every string field is bounded, an identifier far past any Gmail id
154
+ is dropped rather than cut and the drop is said — `omittedIds` names the
155
+ field, `labelIdsOmitted`, `labelsOmitted`, and `attachmentsOmitted` count
156
+ what a list left out — a saved draft whose id Gmail returned unusably still
157
+ reads `saved: true` with a note pointing to `list_drafts`, a body past
158
+ the limit ends with a marker at any `maxBodyChars`, and an untouched
159
+ `raw: true` message past it is refused with the way forward. Every cursor is
160
+ bound to the tool and arguments that issued it, and a thread or label page
161
+ resumes by the identity of what came before: a message or label added or
162
+ removed before that point answers `conflict` rather than repeating or
163
+ skipping one. There is no send, delete, or label tool and no raw
164
+ hatch; `gmail.compose` technically permits sending, and the tool surface is
165
+ what forbids it. `create_draft` is an additive write and `update_draft` a
166
+ destructive one. Both classify as writes; trusted pools permit them in programs,
167
+ and the host controls approval. The transport is confined beneath `users/me`, so the
168
+ token's subject is the only mailbox a request can reach. Setup — Cloud
169
+ project, API, service account with no IAM roles, JSON key (and the
170
+ `iam.disableServiceAccountKeyCreation` override some organizations need), and
171
+ the Admin console's domain-wide delegation entry with exactly
172
+ `gmail.readonly` and `gmail.compose` — is documented on `gmail()` itself.
173
+ - **Google Drive connection.** `@zackbart/connecta/providers/drive` exports
174
+ `drive(id, options)` with the same Workspace delegation options, plus
175
+ `DRIVE_SCOPES` (exactly `https://www.googleapis.com/auth/drive`) and
176
+ `DRIVE_API_BASE_URL`. Seventeen hand-written tools, every one reaching shared
177
+ drives as well as My Drive. Reads: `search_files` (Drive query syntax, trash
178
+ left out unless asked, `user`/`drive`/`allDrives`/`domain` corpora, Drive's
179
+ own `incompleteSearch` surfaced), `list_folder_items`, `get_file`,
180
+ `get_file_content` (Docs exported as Markdown, Sheets as CSV of the first
181
+ sheet only, Slides as text, text files read by range and cut at `maxChars`
182
+ code points, never inside a character, binaries as base64 when the encoded
183
+ file fits the result and never past 1 MiB, and anything else — a larger
184
+ binary, a drawing, a form, a folder, a shortcut — as `format: "unavailable"`
185
+ with a note rather than an empty success; the caps hold on the bytes the
186
+ download returns, never on the size metadata reported, so a file that grew,
187
+ shrank, or emptied between the two reads is still read right — a download
188
+ is read only one byte past its cap and the rest of the stream cancelled
189
+ unread, even from a server that ignores the range, and a range answered
190
+ `416` with `Content-Range: bytes */0` is a verified empty file — and Drive's
191
+ 10 MB export limit is named from Google's own reason code),
192
+ `list_permissions`, and `list_shared_drives`. Every read is built to stay
193
+ under `maxBytes` of JSON, envelope and cursor included: 192 KiB by default,
194
+ which a program inside `execute_code` always receives, from 64 KiB to 4 MiB
195
+ on request, above the default for a direct `call_tool` only. A final guard
196
+ around every tool refuses any result past that bound rather than returning
197
+ it; a write it stops has applied, and says to re-read rather than repeat. A listing that would outgrow it stops early with a
198
+ cursor that resumes at the first row left out, and fails `conflict` rather
199
+ than skip or repeat a row if that Drive page has changed since — the cursor
200
+ carries fingerprints of Drive's raw ids for that page, in order, and of the
201
+ last one returned. A cursor is never issued that could not be accepted back:
202
+ a Drive page token too long to carry fails, saying so.
203
+ Every cursor is bound to its tool and the arguments that decide its rows,
204
+ and refused elsewhere. Content is cut with a marker naming the bound that
205
+ cut it. Names are cut at 2 KiB in listings and content results and 32 KiB in
206
+ `get_file`, descriptions at 64 KiB, other strings Drive does not bound at
207
+ 1 KiB, and owners and parents at ten; identifiers are never cut, and one that
208
+ is malformed is left out. Every such field is named in the result's
209
+ `truncatedFields`.
210
+ Additive writes:
211
+ `create_folder` and `restore_file`. Destructive writes: `create_file` (text
212
+ or base64 content as one multipart upload, or an empty file; `convertTo`
213
+ imports it as a Google Doc, Sheet, or Slides file) and `copy_file`, because
214
+ a new file takes its folder's sharing and so discloses its content to
215
+ everyone a shared destination reaches. These classify as writes, as do `update_file_content`,
216
+ `update_file` (rename, description), `move_file` (a move changes inherited
217
+ sharing), `trash_file`, `share_file` (user, group, domain, or anyone with
218
+ the link, up to writer or organizer, emailing no one unless
219
+ `sendNotificationEmail`), `update_permission`, and `delete_permission`.
220
+ A create, copy, or share that may have landed — sent with no answer,
221
+ answered with a redirect it never follows, answered 5xx (a rate-limit reason
222
+ on a 5xx included), or accepted with a reply that broke — is never
223
+ retryable and names the read to check before repeating it, since a repeat
224
+ makes a second one. Only a write Google answered 2xx is ever said to have
225
+ applied. Writes that set fixed values (`update_file_content`, `update_file`,
226
+ `trash_file`, `restore_file`, `update_permission`) are sent as idempotent, so
227
+ a 5xx to them stays a retryable outage.
228
+ There is no permanent delete,
229
+ no empty-trash, no ownership transfer, and no raw hatch. A 404 says the file
230
+ may be missing or hidden from this account, because Drive does not say
231
+ which. Setup — the Google Drive API and the one scope on the delegation
232
+ entry — is documented on `drive()` itself.
233
+
234
+ Trusted pools permit program writes; read-only pools use `call_destructive_tool`.
235
+ The MCP host controls approval.
236
+ - **Google Sheets connection** ([#682](https://github.com/zackbart/connecta/issues/682)).
237
+ `@zackbart/connecta/providers/sheets` exports `sheets(id, options)` with the
238
+ same Workspace options as `gmail()`, plus `SHEETS_SCOPES` and
239
+ `SHEETS_API_BASE_URL`. Nine hand-written tools: `get_spreadsheet` (title,
240
+ sheets with ids and grid sizes, named ranges as A1 with the sheet title
241
+ always quoted, never cell values; `raw: true` for Google's untouched
242
+ metadata) and `get_values` (one to twenty A1 ranges through
243
+ `values:batchGet`, value and date render options, paged by whole rows under
244
+ `maxCells` and `maxBytes` — never more, an oversized row is refused — with
245
+ the default byte budget sized to what `execute_code` can receive and at most
246
+ 4 MiB for a direct `call_tool` read, which the result stash can still page,
247
+ and a cursor bound to the call's spreadsheet, ranges, and render options
248
+ that only ever continues the caller's own ranges; cells over 5,000 characters cut with a marker, and
249
+ `maxCellChars` up to 50,000 to read one whole) are read-only;
250
+ `create_spreadsheet`, `add_sheet`, and `append_values` (pinned to
251
+ `INSERT_ROWS`, so nothing below the table is overwritten) are additive
252
+ writes; `update_values`, `batch_update_values`, `clear_values`, and
253
+ `batch_update_spreadsheet` — Google's own `batchUpdate` requests, passed
254
+ through untouched and classified as a write, its replies cut to their kind,
255
+ ids, and counts when too large to deliver — are destructive. Writes take a
256
+ required `RAW` or `USER_ENTERED` and at most 50,000 cells per call. Value
257
+ updates and clears of fixed ranges are idempotent, so a 5xx stays
258
+ retryable; an append or create whose outcome is unknown says what to read
259
+ before repeating it — the table, or a Drive search for the title. A 404
260
+ never claims absence, since a spreadsheet is a Drive file that may simply not
261
+ be shared with the caller. Listing and finding spreadsheets is Drive's job.
262
+ The one scope, `https://www.googleapis.com/auth/spreadsheets`, and its setup
263
+ are documented on `sheets()`.
264
+
265
+ Trusted pools permit program writes; read-only pools use `call_destructive_tool`.
266
+ The MCP host controls approval.
267
+ - **Google Forms connection.** `@zackbart/connecta/providers/forms` exports
268
+ `forms(id, options)`, taking the same Workspace options as `gmail()`, plus
269
+ `FORMS_SCOPES` and `FORMS_API_BASE_URL`. Six hand-written tools: `get_form`
270
+ (title, description, quiz and publish state, `revisionId`, and every item
271
+ with the question ids answers are keyed by — one per grid row — their types,
272
+ options, and required flags; long descriptions and option lists cut with
273
+ explicit markers; `raw: true` for Google's whole Form), `list_responses`
274
+ (cursor-paged, an optional exclusive `submittedAfter` instant, each answer
275
+ labeled with its question's title in form order, with any quiz grade and
276
+ grader feedback, answers and feedback over 2,000 characters cut) and
277
+ `get_response` (one whole, with file ids, quiz grades, and feedback text and
278
+ links; `questionIds` narrows it), `create_form` (title, Drive file name,
279
+ optionally unpublished — all Google accepts at creation), `update_form_info`
280
+ (replaces the title or description), and `batch_update_form` (Google's own
281
+ batchUpdate requests, each exactly one of its six kinds, all or none, under a
282
+ required `requiredRevisionId` so an edit never lands on a form someone
283
+ changed since it was read; a stale revision fails `conflict`). Every result
284
+ stays inside 192 KiB, under `execute_code`'s 256 KiB host-result bridge,
285
+ measured whole, cursor included: a response page ends early and continues
286
+ after the last response it returned, only while Google's page still holds
287
+ the same responses in the same order (otherwise `conflict`), every
288
+ projected text is bounded in bytes as well as characters and cut without
289
+ splitting a surrogate pair, a large form pages its items under a cursor
290
+ bound to its revision — continuing inside a grid too large for one page, so
291
+ every question id is reachable — a response too large to read whole names the answers
292
+ it left out in `omittedQuestionIds`, `raw: true` refuses a form too large
293
+ for one result, and a batch reply too large to return whole drops question
294
+ ids, then everything but counts, marked `truncated` and still reported as
295
+ applied. `create_form` is additive; the other two writes are
296
+ destructive. A write Google may have applied — no answer, a 5xx or redirect after it
297
+ arrived, or a reply that broke — is reported as not retryable (no Forms
298
+ write is sent as idempotent), and a create says to look in Drive before
299
+ creating again. There is no list, delete, share, publish, or watch tool: Drive
300
+ lists forms, and this connection requests no Drive scope. A form is a Drive
301
+ file, so a 404 is reported as unknown-or-not-visible, never as absence.
302
+ Setup, with exactly `forms.body` and `forms.responses.readonly` for the
303
+ domain-wide delegation entry, is documented on `forms()` itself.
304
+ - **Google Workspace domain-wide delegation, shared by every Workspace
305
+ provider.** `serviceAccount` is `{ clientEmail, privateKey, clientId? }` or
306
+ the downloaded JSON key's text, from deployment secrets rather than the vault,
307
+ because one key serves every Workspace connector; anything but a complete,
308
+ exactly-encoded PKCS#8 RSA key of at least 2048 bits throws at construction,
309
+ and no key ever appears in
310
+ a message. `subject` is a function from the authenticated
311
+ `AuthenticatedIdentity` to a Workspace address — sync or async, for a
312
+ directory lookup, with the call's abort signal — or one fixed address for
313
+ shared or scheduled use. The function is never called for an open
314
+ deployment's anonymous requests or a call no request admitted; those, and an
315
+ `undefined` answer, fail `auth_required` before any request leaves.
316
+ Arguments, headers, and programs cannot choose the subject. The RS256
317
+ JWT-bearer assertion is signed with Web Crypto, so it runs unchanged on Node
318
+ and Workers with no new dependency. Access tokens are cached in memory only —
319
+ never in storage — per service account, key, subject, and scope set,
320
+ replaced a minute before expiry, and bounded at 512. Concurrent callers share
321
+ one mint through plain outcomes and a deadline, never another request's
322
+ signal, so a cancelled owner sends a waiting caller in another Worker request
323
+ to mint for itself. A 401 forgets only the token it rejected and replays the
324
+ request once, so a streamed request body is refused up front. A write that
325
+ names the revision it was made against passes `{ revisionGuarded: true }`,
326
+ and a stale revision — FAILED_PRECONDITION or ABORTED on HTTP 400 or 409 —
327
+ then arrives as `conflict` with fixed words to re-read and retry, unless
328
+ Google names a more specific reason — a disabled API, a quota, a missing
329
+ scope, or a precise permission refusal — which always decides first; otherwise
330
+ a refused precondition is reported neutrally in Google's own words. A
331
+ product can read Google's reason codes for any mapped failure — sanitized
332
+ tokens only, never its prose — and `exportSizeLimitExceeded`,
333
+ `domainPolicy`, and `insufficientFilePermissions`/`forbidden` refusals
334
+ name themselves precisely. A download or export can pass `{ maxBytes }` to
335
+ read only a prefix: the body is streamed, the rest is cancelled unread, and
336
+ the result says whether it was `truncated`, with its HTTP status and
337
+ `Content-Range`, so an empty file's 416 arrives as an empty result. A write
338
+ is never told to retry when it may have landed: one Google accepted whose
339
+ reply broke off or overflowed, one answered with a redirect, one sent with
340
+ no answer at all, and one Google answered with any 5xx — whatever reason it
341
+ named — fail as non-retryable with words saying to re-read its target first,
342
+ the verdict core's `write_outcome_unknown` gives a trusted-pool program write.
343
+ Only a 429, or a 4xx naming a quota, is a rate limit for a write. A read
344
+ stays retryable, as does a write the provider marks `{ idempotent: true }`.
345
+ An error status is a refusal even when its body cannot be read. A product
346
+ can ask how far any failed request got: before sending, awaiting a
347
+ response, reading an accepted reply, redirected, a server error on a write,
348
+ or refused. Google's
349
+ refusals map to what fixes them: `unauthorized_client`
350
+ names the client ID and the exact scopes to authorize, `invalid_grant` names an
351
+ unknown or suspended user, a deleted key, or clock skew, and a disabled API or
352
+ a missing scope says so.
353
+ - **Google Slides connection.** `@zackbart/connecta/providers/slides` exports
354
+ `slides(id, options)` with the same Workspace delegation options as `gmail()`,
355
+ plus `SLIDES_SCOPES` and `SLIDES_API_BASE_URL`. It requests exactly
356
+ `https://www.googleapis.com/auth/presentations`. Fifteen hand-written
357
+ tools, seven of them for comments (below). Of the other eight, four are
358
+ reads: `get_presentation` (title, page size, `revisionId`, a
359
+ layout preview, and each slide's text in reading order — top to bottom,
360
+ then left to right, rotated and nested groups composed — with table cells,
361
+ alt text, linked charts' spreadsheet ids, speaker notes, and empty
362
+ placeholders' ids for filling a new slide, under a field mask that leaves
363
+ styles behind, capped per slide), `get_page` (any slide, layout, master,
364
+ notes page, or the notes master by objectId: every element in reading order with its group and
365
+ placeholder type, index, and parent — what `placeholderIdMappings` needs —
366
+ and text continued across pages), `list_layouts` (masters, each followed by
367
+ its layouts), and `get_slide_thumbnail` (the short-lived link and size,
368
+ never the image). `create_presentation` and `create_slide` are additive
369
+ writes; `replace_all_text` (literal, case-sensitive by default, several
370
+ replacements in one atomic batch, optionally at a required revision) and
371
+ `batch_update_presentation` (1 to 100 raw Slides requests, each refused
372
+ locally unless it is one known Request kind, always at a required
373
+ `revisionId`, its replies bounded — every id at every depth kept whole,
374
+ large fields named in `cut`, overflow counted, and the write reported as
375
+ applied) are annotated as destructive and classified as writes. Every write result is size-checked too: copied
376
+ ids and revisions are whole or flagged, never cut, and a result that still
377
+ cannot be delivered after Google's 2xx is refused with "applied — do not
378
+ repeat it; re-read". A write refused because the deck changed since its
379
+ revision is a `conflict`, and a create whose outcome is unknown says what to
380
+ look for before creating again.
381
+ Every read result is built under the shared Workspace result budget, so it
382
+ is deliverable inside a program and directly alike, and nothing it cannot
383
+ carry is lost: cut text carries the `get_page` cursor that continues it
384
+ character by character, a crowded slide names the cursor for the elements
385
+ not shown, the layout preview continues in `list_layouts`, and `raw: true`
386
+ pages Slides' own JSON, sending an element too large for one result in
387
+ chunks that concatenate and parse. Every cursor is bound to the deck, page,
388
+ mode, and the revision it was read at — or, for a viewer Slides gives no
389
+ revision, a SHA-256 of the exact content its paging depends on — so a deck that
390
+ changed between pages is a `conflict` to restart, never a skipped or
391
+ repeated slide. Slides cannot list decks, and the guide says that is
392
+ Drive's job. A 404 is reported as unknown-or-not-visible, because a deck is
393
+ a Drive file. Setup is documented on `slides()`.
394
+
395
+ Trusted pools permit program writes; read-only pools use `call_destructive_tool`.
396
+ The MCP host controls approval.
397
+ - **Google Discovery drift checks.** `npm run providers:check -- --provider
398
+ gmail` reads Gmail's credential-free Discovery document and digests the nine
399
+ methods the tools call like any other touched endpoint, and also reports a
400
+ touched method that stops accepting the provider's delegated scopes. A
401
+ manifest with `"format": "google-discovery"` and a `scopes` list is all a
402
+ further Workspace product needs.
403
+ - `Connecta.describeConfig()` returns a secret-free snapshot of the running
404
+ configuration — limits with their `default`/`config` source, auth providers,
405
+ identity rules, pools, modules (with the activity store's kind and
406
+ retention), the storage adapter's kind, branding, and each
407
+ connector's source, endpoint, auth mode, credential slot labels, call
408
+ admission, and static tools — for the operator UI and `connecta doctor`.
409
+ `Connector.describe()` reports a connector's own facts and is implemented by
410
+ `remoteMcp()`, `api()`, and every provider. Every URL
411
+ either reports keeps origin and path only (a root-relative favicon, its path),
412
+ and a URL of any scheme but http(s) is omitted.
413
+ `defineConfig((env) => …)` declares a deployment's configuration as a
414
+ function of its environment (#705).
415
+ - Evaluate hosted Google Workspace and Planning Center capabilities while retaining delegated/PAT API providers, and report public provider contract drift on provider pull requests and weekly without blocking the check gate.
416
+ - `@zackbart/connecta/d1` exports `d1Storage(db)` and
417
+ `d1ActivityStore(db, { retentionDays })`; `@zackbart/connecta/sqlite`
418
+ (Node-only, on the built-in `node:sqlite`) exports `openSqlite(path)`,
419
+ `sqliteStorage(db | path)`, `sqliteActivityStore(db | path, { retentionDays })`,
420
+ and `importStateFile(db, statePath)`. Both drivers share one SQL store; each
421
+ creates its tables on first use and reads a 0.28 `connecta_kv` or
422
+ `tool_call_activity` table as is, adding activity columns it lacks. Activity
423
+ writes prune a bounded batch past the retention window, 90 days by default, so
424
+ no cron is needed. `connecta migrate-state <state.json> <connecta.sqlite>`
425
+ copies a 0.28 `fileStorage` state file into SQLite once, keeping any key the
426
+ database already holds. Every storage key is built in `src/storage/keys.ts`,
427
+ which lists each key family's scope, version, codec, and TTL (#705).
428
+ - Relay downstream form and URL input requests on direct MCP calls using encrypted, signed, single-use state bound to the principal, pool, target, and original arguments. Gate elicitation by the host's declared capabilities, bound rounds and payloads, and keep program input requests unsupported with direct-call guidance.
429
+
430
+ Refuse private-state echoes before publishing prompts, results, or catalogs, and prevent OAuth refresh or redirects from resending write continuations.
431
+ - Ask MCP 2026-07-28 hosts with URL elicitation support to connect a service when a call needs authentication. Authenticate retries with a deployment-held key and bind them to the principal, endpoint, connector, tool, arguments or code, round, and expiry. Decline and cancel stop without dispatch. Writes elicit only for Connecta-owned credential resolution failures before connector invocation. Programs also stop recovery after any write-classified handler was entered; each retry checks its own round. Raw fetches and custom transports cannot establish retry eligibility. These failures require reconciliation before retrying and retain manual connection guidance. Reads may re-run after dispatch only when classified from a catalog accepted within its TTL. The host records every entered call's classification and freshness; stale-fallback entries block program recovery. Accept rechecks every entered read before restarting any call and refuses changed classifications or digests with `auth_replay_refused` and reconciliation guidance. Normal stale-fallback calls retain their existing behavior. Other hosts receive the existing recovery envelope with a connection link.
432
+ - Programs can read downstream MCP resources with `connecta.read("resource://<connectorId>/<encodeURIComponent(downstreamUri)>")`. Reads require a whole-connector grant, share host-call and connector admission limits, and redact credential echoes before reaching the guest or agent.
433
+ - Serve usage, connector guides and opted-in downstream skills through one caller-scoped registry. Advertise the Skills extension and skill-only resources, with private cache hints, five-entry skill pages and preserved downstream digests. Both the skills meta-tool and connecta.skill read the same documents. Connector guide bodies gain Agent Skills frontmatter; connector:<id> remains a one-release lookup alias. Sent credential echoes pass through the existing agent redaction boundary, including binary supporting files.
434
+
435
+ Downstream Skills listing and file reads bypass the SDK response cache while sharing request-scoped auth with ordinary resource reads. Negotiation reuse includes the Skills opt-in.
436
+ - Record the build-time package version independently of display serverInfo on every activity event. Persist package and validated client facts in SQL history, migrate old tables with a nullable column, and expose them read-only at /ui/api/activity and in Activity row details. Older calls without request IDs remain separate rows. Doctor uses and reports the same generated package version.
437
+ - Add the typed operator UI contract and read-only `/ui/api/config` endpoint with a caller-scoped config snapshot, classified live tools, catalog age, payload-free last-call facts, and effective grants, pools, trust tiers, and permissions. `connecta doctor --config` prints the same secret-free snapshot as JSON.
438
+ - Add operator pages for deployment health, connectors, tool catalogs, access, request activity, and configuration on the typed, viewer-scoped contract. Preserve factory option presence so omitted token and transport options are marked as defaults.
439
+
440
+ ### Changed
441
+
442
+ - **Breaking:** **`defineProvider()` and `remoteMcp({ classify })`**
443
+ ([#705](https://github.com/zackbart/connecta/issues/705)). The root entry
444
+ exports `defineProvider()`, the one shape for a maintained provider: name,
445
+ title, kind, maintained skill, optional reviewed classification, and a
446
+ synchronous `create`. It validates the options every provider shares, renders
447
+ the usage guide with deployment instructions appended, and exposes its
448
+ `definition` on the factory. It imports neither transport, so `api()`
449
+ providers built with it gain no MCP or Effect graph. `remoteMcp()` accepts
450
+ `classify: { tools: { name: "read" | "write" | "destructive" | { verdict,
451
+ reason?, schemaDigest? } } }`, validated at construction. It fails closed,
452
+ and catalog drift is reported against it. Linear is the first provider
453
+ converted. Its tool names, verdicts, titles, descriptions, credential slot,
454
+ guides, and drift counts match 0.28 (`test/linear-snapshot.test.ts`). The one
455
+ change is stricter: a reviewed Linear create such as `create_issue_label`
456
+ now stays a write even when the downstream annotates it `readOnlyHint: true`.
457
+ `linear()` reports a blank purpose as `linear("<id>") requires a non-empty
458
+ purpose`, and rejects a blank `title`, a non-string `instructions`, and an
459
+ unknown `authScope` at construction. `LINEAR_VETTED_CATALOG` is deprecated in
460
+ favor of `linear.definition.classify`.
461
+ - **Reviewed hosted writes stay writes**
462
+ ([#705](https://github.com/zackbart/connecta/issues/705)). The Basecamp,
463
+ Cloudflare, Mixpanel, Notion, RevenueCat, Stripe, and Vercel providers now
464
+ apply the same rule as Linear: a tool their release reviewed as a write stays
465
+ a write when the downstream annotates it `readOnlyHint: true`. Before, such a
466
+ create was served as a read and ran unapproved. Their reads, destructive
467
+ tools, unlisted tools, and drift counts are unchanged.
468
+ - Contributors: CI runs Chromium only for relevant PR paths and always on `main` and publish, with a versioned browser cache and bounded installation retries; local `check` still runs the full suite (#705).
469
+ - Validate Clerk OAuth audiences on `/mcp` and pool endpoints for both JWT
470
+ and opaque tokens. Enable Clerk's `aud_claim_enabled` setting and request the
471
+ endpoint's canonical URL as `resource`. Omitted or empty `allowedOAuthClientIds`
472
+ requires bound tokens; an explicit list admits unbound tokens from dedicated
473
+ clients only. Clerk browser session tokens authenticate operator routes only.
474
+ Rejections log fixed reason codes, and the MCP `401` metadata challenge is
475
+ unchanged.
476
+ - Reject direct upstream `DynamicWorkerExecutor` construction, including
477
+ subclasses, before creating runtime resources. The error names the required
478
+ `/worker` import and configuration migration; custom executors remain
479
+ supported (#704).
480
+ - Require a non-enumerable, versioned lifecycle brand on executors before
481
+ creating runtime resources. `/worker` and `/quickjs` carry the brand across
482
+ package copies and bundles; unbranded executors fail with all migration
483
+ options. Custom sandboxes opt in with
484
+ `customExecutor(myExecutor, { lifecycle: "self-managed" })` (#704).
485
+ - Replace the contributor ethos with principles, decision history, and test-backed
486
+ invariant and MCP coverage records. Evidence comes from passing cases in the
487
+ full Node run, so fixtures, non-registering calls, and runtime skips cannot
488
+ certify coverage. Deferred MCP features remain declined for 0.29.0 (#705).
489
+ - Upgrade `@modelcontextprotocol/client` and `@modelcontextprotocol/server` to 2.3.1. The SDK binds stored credentials to their issuing authorization server and propagates token-save failures. Resource transport redirects remain confined to the same origin; credential-bearing OAuth token requests refuse every redirect. Connecta overrides the server SDK's 4 MiB request-body default on both protocol legs, preserving `listen()`'s `maxBodyBytes` and the Workers platform bound. The SDK owns the closed-transport legacy-handshake race, removing connecta's workaround for typescript-sdk#2864. Teardown regressions cover workerd rejection handling.
490
+ - **A 2026-07-28 request must carry `MCP-Protocol-Version`.** The 2026-07-28
491
+ transport requires the header on every POST and has the server reject a
492
+ request without it, so an intermediary routing on the header and the server
493
+ executing the body cannot disagree; server 2.0.0 served such a request
494
+ anyway. A client breaks here only if it puts the
495
+ `io.modelcontextprotocol/protocolVersion` claim in the request's `_meta`,
496
+ sends `Mcp-Method`, and omits `MCP-Protocol-Version` — a hand-rolled modern
497
+ client, or a proxy that strips the header. It must send the header with the
498
+ same value as the claim. Defaulting the header for it is not open to
499
+ connecta: the spec's allowance to assume `2025-03-26` covers only clients
500
+ older than 2025-06-18, which send no 2026-07-28 claim and are still served
501
+ on the legacy leg.
502
+ - **A JSON-RPC batch holds at most 100 messages.** The cap is the server
503
+ SDK's, not the protocol's: batching left the protocol in 2025-06-18, which
504
+ sets no size for the 2025-03-26 clients that may still send one. Server 2.1.0
505
+ answers a longer batch `400` with `-32600` and dispatches none of it; the cap
506
+ is a constant with no option, and connecta keeps it rather than reimplement
507
+ the SDK's body handling. A client that batches more must split the batch.
508
+ - **Breaking:** `api()`, `remoteMcp()`, every maintained provider, `operatorUi()`,
509
+ `accessTokens()`, `activityHistory()`, `d1ActivityStore()`, and
510
+ `sqliteActivityStore()` refuse an unknown
511
+ option at construction, naming its path — `api("crm").maxResultByte`,
512
+ `operatorUi().branding.theme.accentColor` — instead of accepting and ignoring
513
+ it. A discriminated option with a missing or unrecognized discriminant —
514
+ `remoteMcp()`'s `auth.type`, a provider's `auth.type` or `surface` — throws
515
+ with the valid values instead of constructing with no authentication.
516
+ Configuration and factory options are read once, by property descriptor, into
517
+ the plain copy core resolves: a getter or setter on a configuration object or
518
+ array refuses to construct and is never run, and no Proxy trap is invoked
519
+ beyond inspection. An array or class instance where an options object belongs
520
+ (`remoteMcp([...])`, `callAdmission: []`) throws with its path, and every
521
+ string map (static `headers`, `authorizationParams`, `tokenRequestHeaders`)
522
+ must hold string values. An `api()` tool is checked in place and passed
523
+ through as given, so a class-instance tool keeps its prototype handler and
524
+ private fields; a tool without a handler function refuses to construct. Pool names and `classification` map keys such as `__proto__` and
525
+ `constructor` are ordinary entries (#705).
526
+
527
+ Provider definitions declare a closed `options` shape with `optionsOf<T>()`.
528
+ `defineProvider()` and the remaining providers use one descriptor-validation
529
+ path and stamp the maintained name onto `describe().source.provider`. The root
530
+ exports the option-shape combinators and `PROVIDER_COMMON` for provider authors.
531
+ `remoteMcp()` accepts `classify`; its review is still validated by the shared
532
+ classification validator and applied only by the registry. Custom connectors
533
+ remain opaque, preserving `Connector.classification` (#705).
534
+ - Depend on `effect` through `^4.0.0` instead of exactly `4.0.0`, so a
535
+ deployment that also uses Effect resolves one copy. The root still imports
536
+ only the stable `effect` module (#705).
537
+ - **Breaking:** Validate `ConnectaConfig` against one schema that also produces its TypeScript
538
+ type and every default, under one policy: a wrong value refuses to construct,
539
+ naming its path. `execute.*` limits, `discovery.*`, `calls.maxResultBytes`, and
540
+ a connector's own `maxResultBytes` used to warn and fall back to a default;
541
+ they now throw. `connectors`, `auth`, `storage`, `logger`, `publicUrl`,
542
+ `deploymentInfo`, and `serverInfo` are checked by shape too (`storage` must
543
+ implement all five `KVStorage` methods, the same refusal #722 added, now
544
+ reported by the schema);
545
+ `serverInfo.websiteUrl` and a `remoteMcp()` `url` must be http(s). An explicitly
546
+ `undefined` optional value counts as omitted at any depth, and a misspelled
547
+ pool option is reported as `ConnectaConfig.pools.<name>.<key>` with the other
548
+ unknown options (#705).
549
+ - **Breaking:** Both deployment shapes keep their configuration in `src/connecta.config.ts`
550
+ and start it from an entry under 30 lines; optional modules are type-checked
551
+ code switched on by the environment instead of commented blocks. The Node
552
+ template turns Clerk on with both `CLERK_PUBLISHABLE_KEY` and
553
+ `CLERK_SECRET_KEY` (one alone refuses to start), the vault with
554
+ `CONNECTA_CREDENTIAL_KEY`, and activity with `CONNECTA_ACTIVITY=on`, all in its one `CONNECTA_DATABASE` SQLite file;
555
+ it always serves `cta_` access tokens and depends on `@clerk/backend`. The
556
+ Worker example's vault follows the `CREDENTIAL_ENCRYPTION_KEY` secret, and
557
+ activity follows the `CONNECTA_ACTIVITY`
558
+ var (`"on"`), in `CONNECTA_DB`. Activity in both shapes keeps 90 days
559
+ (`retentionDays`), pruned on write (#705).
560
+ - Contributors add changelog fragments in `.changes/`; release authors assemble
561
+ them with a hand-written narrative using `npm run changelog:assemble`.
562
+ Vitest runs every suite on Node and excludes `*.node.test.ts` from Workers,
563
+ with each Node-only reason recorded in the file instead of shared suite lists (#705).
564
+ - Convert the eight hosted MCP implementations to reviewed `defineProvider()` presets. Preserve authentication, endpoint choices, tool classifications, schema digests, results, and API defaults. Add usage prerequisites and pool-trust guidance. Hosted factories now reject blank titles and malformed common options at construction. Remove the internal `withVettedCatalog` helper; public `*_VETTED_CATALOG` exports remain deprecated aliases of each provider definition's classification.
565
+ - Contributors get `npm run check:fast`, a partial inner loop that runs the
566
+ static checks concurrently with Vitest suites related to the branch's changes
567
+ and the always-on package guards; `npm run check` remains the gate. Pull
568
+ requests audit dependencies when root or nested npm manifests, lockfiles,
569
+ shrinkwraps, or configuration change; a nightly workflow and publishing still run `npm audit`. The Chromium
570
+ install retry waits for a leftover apt process instead of failing on its lock,
571
+ and repository skills for providers, spec features, releases, and CI triage
572
+ live in `.claude/skills/` (#705).
573
+ - Give all maintained providers their own folders and generate provider exports, build registrations, package smoke fixtures, conventions, and the README inventory. Preserve provider metadata, guides, registry classifications, public import names, and reviewed bundle caps. Standardize construction errors and report public-contract parser failures per provider, including an explicit manual review record for Breeze.
574
+ - **Breaking:** Make hosted MCP the default for Notion, Vercel and Cloudflare, and remove duplicate named REST operations. `surface: "api"` now selects a documented REST complement. Configure complements under distinct connector ids with their own credentials; hosted OAuth never falls back to an integration token or replays a write. Notion retains every internal-integration capability under explicit `integration_*` names because hosted user OAuth is not equivalent, including headless content writes, full REST pagination and exact JSON blocks. Vercel retains value-safe environment tools and verified deletion/domain gaps; deployment promotion moves to hosted `request_promote`. Cloudflare retains read-only-program reads, byte/header-compatible uploads and legacy Global API Key and R2 jurisdiction mutations; `execute` remains a write. Existing Notion bot workflows must select `surface: "api"` and migrate names to `integration_*`; Cloudflare writes move to hosted `execute` while REST reads can keep their explicit API connector. See `documentation/provider-migration-0.29.md`.
575
+ - **Breaking:** The result stash's byte and entry bounds are booked in one ledger record in
576
+ storage by compare-and-set, so they bound the deployment rather than each
577
+ isolate or process. Every chunk expires by its charge's deadline, concurrent
578
+ stashes retry a lost swap instead of refusing, and a stash write that fails
579
+ releases its charge once it has deleted what it may have written. `engines.node` is `>=22.13.0`, the first
580
+ Node 22 with `node:sqlite` unflagged (#705).
581
+ - **Breaking:** Classify tools once per request-scoped catalog entry, with exact deployment overrides ahead of provider review and downstream annotations. Add read-only and trusted pool tiers, defaulting to read-only, and annotate execute_code as a write on trusted endpoints. Remove connector and execute approval exemptions while preserving write budgets and unknown-outcome accounting. Deployments must explicitly enable trusted pools for program writes.
582
+ - **Breaking:** Discovery returns one flat `{ catalogErrors, tools, total, offset, limit, hasMore }` page from both `search_tools` and `connecta.search`, replacing top-level connector groups. Exact tool names and canonical addresses rank before connector identities, then partial matches. Exact connector ID or title queries browse that connector's tools even when individual descriptions mention its identity. Unknown connector scopes and recognized absent services return an explicit endpoint-scoped absence instead of lookalikes. Catalog credential and permission failures appear before tools with recovery instructions and fixed messages derived only from error codes and connector IDs. Program search and describe default to JSON Schema values; compact schemas and TypeScript signatures are labeled `schemaFormat: "text"`.
583
+ - **Breaking:** Make call errors actionable with agent-only schema keys, enum values, bounds, received types, validated examples and known conditional requirements. List only the current endpoint's configured connectors on address failures. Distinguish host authentication (`host_auth_required`), downstream OAuth (`downstream_oauth_required`) and provider permission (`provider_permission_denied`) recovery. A dispatched write timeout returns `write_outcome_unknown`, never invites an automatic retry, and echoes bounded arguments only to its caller.
584
+
585
+ Decide retryability from HTTP status, registered OAuth codes, typed SDK errors and runtime network facts, never prose (#700). Untyped errors whose text mentions timeout, 429, 502, 503, 504, rate limits, temporary failures or cross-request cancellation are now generic non-retryable failures; typed and structured transport failures retain their verdicts. A 400 registration refusal saying "temporarily unavailable" is non-retryable; 429/502/503/504 refusals remain retryable regardless of body or URL wording. An SDK `RequestTimeout` is a timeout regardless of message wording. An OAuth token endpoint HTTP 403 is now `provider_permission_denied`, keeps the stored grant, and does not start consent. Drop downstream `ProtocolError.data` on errors rethrown by connecta. Operator records remain payload-free (INV-6).
586
+
587
+ Preserve complete error envelopes in MCP structured content and agent-readable text in every direct-call result mode. Maintained API providers distinguish permission refusals from invalid credentials, including Workspace delegation and insufficient scopes, with provider-specific recovery guidance.
588
+ - **Breaking:** Complete the JavaScript guest API with `connecta.result(id, options)` and `connecta.skill(name)`, support object-form calls, and remove the `get_result` meta-tool. Guest calls now return `{ data, format: "json" | "text" }`; direct results also declare their format. Every program reports host-call counts, and guest failures use `program_error` with source locations and fixed repair hints. Uncaught host errors retain their type through executor-owned Error identities and host outcome channels. Programs and direct calls share configurable deadlines and report operation, stage, elapsed time, and effective deadline. Unawaited emission delivers MCP media content; emitted text shares the program result cap. All six meta-tools advertise output schemas. The shared discovery and doctor tool-name set excludes `get_result`, retaining negotiated doctor checks. Spec coverage records tool calls with both address headers and output schemas.
589
+
590
+ Result paging binds stashes to the admitted identity, endpoint, origin, connector, and tool, and rechecks current grants, pool membership, and trust before returning data. Legacy unbound stashes are unavailable after upgrade. Truncated direct writes on read-only pools return inline notices and bounded previews without storing results or advertising unavailable paging. Refresh refusals retain write accounting, including uncertain outcomes.
591
+
592
+ Worker guest modules cannot resolve runner modules through dynamic imports or builtin module loaders. A private runner module captures its references before guest module evaluation, and the adapter refuses splices containing more than one expression before loading. Worker typed failure details stay in a per-run host map; the runner retains only random failure IDs by Error identity, and unknown IDs attach no host details. The Worker codec reads only own binary-envelope properties, and completion captures safe array iteration even without a guest prelude. QuickJS checks interrupt and deadline facts before safely describing rejections without guest getters, serialization hooks, or Proxy traps, including interruptions during private diagnostic initialization. Images require a supported MIME type and canonical base64 before collection. Catalog search advertises the shared flat schema; eval programs and graders use typed data and result paging.
593
+
594
+ Worker startup refuses upstream binary codec changes that would bypass own-property checks. Required-field validation messages retain ordinary JavaScript guidance. Bundle baselines record Acorn for the Worker import router and single-expression validation; the parser remains outside the root import graph.
595
+ - Teach the guest global, positional calls, discovery envelopes, JSON and text results, partial failures, and terminal host-call budgets in a shorter usage guide. Route single known reads and writes directly. Check every JavaScript example on QuickJS and Workers, and keep the tool's API notation aligned with the shipped guest declaration.
596
+ - **Breaking:** Use SDK Client.listTools aggregation and a SQL-backed ResponseCacheStore for complete, intake-redacted remote MCP catalogs. Honor bounded downstream ttlMs and public/private scope within host-computed auth partitions; request tokens and personal auth remain private even under public hints. Partition private entries by admitted principal and pool even with shared auth, preserve original fetch age, and refuse invalid header declarations before SDK logging. Replace the registry's memory/stale/persist cache with request-local reads. Remove discovery.persistCatalog and discovery.staleCatalogSeconds; add catalogMinTtlSeconds and catalogMaxTtlSeconds (defaults 0 and 86400). catalogTtlSeconds remains the legacy fallback. Retired registry catalogs are ignored. Transient first-page tool listing failures may use complete SQL-backed facts in the same auth partition for five minutes after expiry. Expiry provenance survives classification; stale fallback and zero-TTL classifications cannot authorize post-entry auth recovery.
597
+ - Default downstream OAuth to per-connector client metadata documents on public HTTPS deployments, retain DCR fallback, support issuer-bound static remote MCP clients, and report the selected registration path. Basecamp no longer requires an external metadata document.
598
+
599
+ Validate callback state and issuer before interpreting authorization errors, then atomically consume the verified consent and discard its PKCE verifier. Error and code callbacks share one claim; Continue starts a new consent after an error.
600
+
601
+ On Disconnect, remove local authorization first and attempt issuer-bound RFC 7009 revocation once when advertised. Failed revocation reports `oauth_revocation_failed` without provider text or credentials.
602
+
603
+ Register raw confidential client IDs and secrets before token and revocation dispatch, including form-encoded Basic credentials, so downstream echoes remain redacted after refresh.
604
+ - Cache downstream protocol verdicts for five minutes within the connector, credential generation, admitted identity and pool. Modern verdicts skip discovery on later requests. Legacy verdicts skip the probe while retaining a fresh initialize handshake. Automatic negotiation retries a probe HTTP 5xx with a fresh legacy transport.
605
+ - **Breaking:** Support Clerk and Cloudflare Access for inbound human authentication, with Access limited to trusted direct Worker context. Machines use stored `cta_` tokens. Remove `/auth/bearer`, `bearerToken`, and request-selected `assertedPrincipal` identities. Install `accessTokens(storage)`, provision a token per machine or human owner, update actor-id grants, rotate clients, and delete old secrets. Access service credentials satisfy the edge only and need a `cta_` token inside connecta. Replace custom auth `final`/`finalRefusals` with synchronous `recognizesCredential`. Select 401 challenges from the provider that actually serves protected-resource metadata, include Clerk scopes, and remove the Clerk authorization-server metadata proxy. See the exact migration steps in `documentation/auth.md`.
606
+ - **Breaking:** **Downstream OAuth state is one grant record per owner and one flow record per
607
+ consent.** Each OAuth connector's owner has one `oauth:grant` record (epoch,
608
+ the latest consent's state digest, and one authorization server's client,
609
+ tokens, and discovery, sealed as one body with the vault), and each consent
610
+ has an `oauth:flow:<sha256(state)>` record expiring with its link. Every grant
611
+ write is a compare-and-set against the record it read, and Restart and
612
+ Disconnect replace the epoch in one, so the epoch namespaces, cleanup lineage
613
+ (`oauth:generation`, `oauth:cleanup:`, `oauth:cleanup-at:`), grace sweeps, and
614
+ flow-entry grant retirement are gone. A grant holds one server: saving
615
+ discovery, a client, or tokens for another replaces it. The refresh
616
+ coordinator stores an accepted rotation before releasing anyone and hands
617
+ later readers that result; the 503 "previous credentials commit" answer is
618
+ gone. The first read of each owner's grant migrates the 0.28 layout once and
619
+ deletes it: issuer-stamped client, tokens, and discovery carry over (sealed
620
+ values reopen under their old keys); a disconnected connector stays
621
+ disconnected; grants from v0.8.1 and earlier and pending consents do not, so
622
+ reissue pending Connect links after upgrading. Rolling back needs consent
623
+ again. A programmatic `finishAuth` names its consent by the callback's
624
+ `state`; one that names no pending consent is refused before anything is sent
625
+ (#707).
626
+ - **Breaking:** Bind modern MCP client capabilities and identity to request-local meta-tool context; record only allowlisted ASCII client name/version facts in activity, including SQLite and D1 storage. Client names are bounded to 64 characters and versions to 32; invalid values are absent at recording, storage write/read, and operator UI boundaries. Advertise the served extension map and private discovery cache hints, declare direct-call address headers, and make doctor negotiate through the SDK and report its revision using the exported META_TOOL_NAMES set. Connecta-owned 403, pool 404, and deadline 504 refusals now return JSON-RPC error bodies with unknown request IDs omitted. Admission codes change from -31001/-31002 to -33001/-33002; deadline, pool, and access refusal codes are -33003/-33004/-33005.
627
+ - Test operator pages with typed visual fixtures and real-server sign-in, OAuth connect, and credential save/test flows. Generate and compare screenshot baselines on Linux Chromium, with a CI workflow for snapshot updates.
628
+ - Rebuild the operator shell on React, Radix and Tailwind v4 with an Inter token layer, light/dark sidebar, accessible dialogs, tabs, tables and a ⌘K command palette. Serve hashed immutable assets on Node and Workers with same-origin script CSP and generate the asset manifest before build and test. Clerk pages admit its validated loader origin and the exact Cloudflare CAPTCHA host, with Clerk image and worker permissions. Apply common HTML security headers to status and error pages, including browser 404s (#708).
629
+ - Use signed-in Claude Code and Codex CLI logins for agent evals, default to Sonnet 5.5 and GPT-6-Luna, and activate the merged resource-read task.
630
+ Grade record facts together across prose, lists and tables, reject conflicting record facts, and require structured absent-service answers backed by genuine discovery.
631
+ - Adopt oxfmt for repository formatting. Contributors should run `npm run format` before committing; `format:check` runs in the fast and full CI checks.
632
+
633
+ ### Fixed
634
+
635
+ - **Breaking:** Sanitize connector failures at shared request boundaries (#695). Transport, parser and unexpected handler failures expose checked failure facts and fixed descriptions instead of downstream-derived causes or nested errors. Agent-facing downstream refusals retain their classification and pass through request credential redaction. Callers that inspected error causes must use typed details instead.
636
+ - **Breaking:** Make logs, status and activity payload-free (INV-6, #695, #716). Failure records contain checked typed facts instead of messages, stacks or raw downstream error text. `ActivityEventInput.errorCode` is a closed union, and call-failure logs replace `message` with those facts. Status text and tool names follow the same disclosure checks. Downstream body reads use UTF-8 byte decoding, and catalog drift timestamps are validated before disclosure.
637
+ - Return fixed OAuth registration and discovery failure descriptions with checked origin, status, error code and step facts (#695). Google delegated-token failures use the same rule. Downstream response text does not enter these diagnostics; classification and retryability remain intact.
638
+ - **The test gate fails for behavior, not for load.** On a busy machine —
639
+ several `npm run check` runs at once, or a crowded CI runner — suites failed
640
+ with default workers that passed alone, which teaches a rerun-until-green
641
+ habit that hides real regressions
642
+ ([#692](https://github.com/zackbart/connecta/issues/692)). Request
643
+ lifetimes, probe deadlines, refresh claims, and the QuickJS runtime's own
644
+ wall deadline are now tested on a fake clock that moves only when a test
645
+ advances it, each pinned a tick either side of its boundary. A QuickJS pool
646
+ run that outlives its deadline may end at the child's own report or at the
647
+ parent's termination after its fixed grace, and pool cases accept either.
648
+ QuickJS suites not about the guest CPU budget run under a generous one,
649
+ because QuickJS charges that budget by wall clock and a loaded host billed
650
+ its own contention to the guest; the heap-cap case allocates a bounded
651
+ amount, so only an enforced cap fails it, and the event-loop checks derive
652
+ their bound from the guest budget. Real subprocesses, local workerd I/O,
653
+ whole-tree walks, and worst-case payload sweeps keep hang guards sized for a
654
+ loaded host instead of vitest's 5s default. Nothing that ships changes.
655
+ - **Breaking:** Bind downstream OAuth completion to the initiating user for personal and
656
+ shared connectors. Return connecta `/connect` links to agents and operator
657
+ actions; keep status reads passive.
658
+ - Drop downstream tools whose names contain C0, DEL, or C1 control characters at fresh and cached catalog intake. Report only the dropped count in connector drift and keep space and non-ASCII names callable with withheld operator records.
659
+
660
+ Reject control characters in configured static tool names at construction, before startup logs, with a path error that omits the tool name.
661
+ - Keep Clerk SDK upstream error text and workerd native body-reader diagnostics out of operator output. Read all Clerk SDK responses as bytes, route SDK diagnostics through checked failure records, and preserve browser handshake and JWT authentication. Verify response-wrapper fidelity on Node and Workers.
662
+
663
+ The bundled gate client has Connecta-owned declarations for user lookup. The optional Clerk peer accepts compatible 3.x releases independently of the generator's exact internal SDK dependency.
664
+ - Record Clerk authentication and admission denials with checked fixed reason codes. Exclude provider user IDs, email addresses, and email domains from denial logs, including email lookup failures. Add denial canaries for Clerk and Cloudflare Access across logs, activity, and status.
665
+ - Make explicit Authorization headers decisive across inbound auth providers and protected routes. Normalize the case-insensitive Bearer scheme with one shared parser, reject malformed and unsupported headers with a 401 challenge, and exclude ambient cookies, Clerk browser handshake credentials, and Access context. Preserve explicit-header 401 challenges on OAuth starts and callbacks. Keep cookie-only human authentication and stored machine-token ownership.
666
+ - Record undeclared, denied, and throwing pool grants with checked fixed reason codes. Keep identity-provider text out of operator logs and status, while preserving typed activity actors. Cover Clerk OAuth and Access UUID/email pool refusals on Node and Workers. Preserve the operator shell sign-in and CSP contracts after rebasing.
667
+ - Keep downstream public catalog hints within host-computed credential and pool partitions. Request tokens and personal auth always use private cache entries, and OAuth credential changes fence cache reuse even within one epoch. Validate remote resource reads against complete, partitioned advertised resources and URI templates before dispatch; refuse unmatched URIs and encoded traversal with a fixed typed failure. Add a completed SDK catalog refresh hook with previous and new digests for drift-event integration.
668
+ - Keep downstream catalog notifications active for the connection after a successful resource read or listing. Give temporary remote connector instances separate cache lifetimes within the same request. Bind SDK cache I/O and cleanup to each serialized listing's cancellation and generation, let later listings publish a fresh redacted catalog after invalidation, and stop cancelled storage waits from blocking the next listing.
669
+ - Preserve Cloudflare R2 bucket storage-class updates through the REST complement with a validated cf-r2-storage-class header, without requiring jurisdiction for ordinary buckets.
670
+ - **One completed OAuth MCP call no longer aborts its siblings.** A shared
671
+ downstream transport and OAuth provider retained the first call's deadline
672
+ signal, which ends even on success. Subsequent calls and pending parallel
673
+ calls could therefore fail with "The operation was aborted" well before
674
+ their deadlines. The connection now owns that signal; each call keeps its
675
+ own SDK cancellation. A cancelled handshake waiter leaves other waiters
676
+ alone, and the last cancelled waiter still abandons the connection and its
677
+ refresh (#704).
678
+ - Preserve downstream tool definitions in the complete SQL-backed SDK catalog cache. Output-schema validation, `Mcp-Param-*` header mirroring, and refusal of tools requiring task-based execution use those definitions on warm reads as well as after a same-request listing. Registry manifests v2/v3 are retired and refreshed rather than reused (#704).
679
+ - **Tool catalog changes are no longer advertised.** Modern `server/discover`
680
+ and legacy `initialize` report `tools.listChanged: false`, because a fresh
681
+ server per request cannot publish those notifications. Modern
682
+ `subscriptions/listen` requests receive HTTP 404 with JSON-RPC `-32601`
683
+ (Method not found), without acquiring a request-admission permit or opening
684
+ an SSE stream. Authentication and SDK protocol/header validation still run.
685
+ Legacy listens remain unsupported (#704).
686
+ - End `execute_code` at the first host call beyond its budget with one typed
687
+ `budget_exceeded` error, even when the program catches and ignores failures.
688
+ The host suspends the refusing bridge call, closes further host access, and
689
+ releases the sandbox lease. The failure reports payload-free attempted,
690
+ admitted, succeeded, and failed host-call counts (#704).
691
+ - Require consent again for historical OAuth credentials without consistent issuer stamps. The one-shot legacy migration carries only issuer-bound grants and does not adopt unstamped credentials under newly discovered metadata.
692
+ - Keep each OAuth grant bound to one issuer by construction. Flow entry binds the current epoch; reads, writes and consent completion remain fenced to that epoch, so superseded flows cannot change a newer grant or return its consent URL. On the first grant read, the one-shot layout-2 migration preserves consistently issuer-stamped credentials, rejects unstamped or inconsistent credentials, and discards pending consents.
693
+ - Sanitize OAuth token-endpoint failures before they reach the SDK or its console diagnostics. Failures retain the HTTP status, registered OAuth error code and `Retry-After`, with fixed descriptions instead of downstream response text. Invalid code-exchange responses receive the same treatment, and migrated grants retain their issuer stamps.
694
+ - Retry accepted OAuth refresh rotations through bounded grant-commit attempts without sending another refresh request. Exhausted commits require re-consent; waiter deadlines remain retryable `unavailable` failures. Failed credential writes return fixed text without storage error causes.
695
+ - **A token endpoint cannot stamp its own issuer.** A refresh answer carrying
696
+ `issuer` had that field kept when connecta persisted a rotation for a
697
+ cancelled caller with no issuer to stamp. Only the client binds a grant, so
698
+ the field is dropped, as the SDK drops it.
699
+ - Make the optional-module probe deadline test deterministic by waiting for its timer before advancing fake time and awaiting response settlement at the deadline. Preserve the deadline boundary and verify connector scope cleanup.
700
+ - Provider description stamping preserves connector prototypes and inherited
701
+ classification. It defines `describe()` in place when possible, keeping class
702
+ method receivers, and otherwise inherits through a wrapper with only its own
703
+ `describe()` property. Discovery and calls retain the reviewed classification.
704
+ - Changelog assembly requires committed inputs and prints the git restore command
705
+ for recovery after a failure or interruption. The suite guard checks Vitest's
706
+ actual Node and Workers collection, including excluded directories and symlink
707
+ targets (#705).
708
+ - Fence late downstream catalog publications when the SDK receives tools/list_changed, including first cache refreshes. Stop subsequent cache storage operations after cancellation or scope teardown, preserving complete catalogs and ordinary public/private cache hits.
709
+ - Keep core-protocol JSON-RPC refusals, server discovery, registered meta-tool results, and both MCP transports behind the request-scoped credential-echo redaction boundary. Modern direct-call security tests send the required mirrored address header and keep their credential-echo assertions.
710
+ - Complete dispatched downstream OAuth refreshes after caller cancellation and retain their grant commits through Workers waitUntil. Permanently refuse refresh fingerprints after ambiguous network, response-body, or deadline outcomes so later requests and isolates require re-consent without resending the token.
711
+ - Fence downstream OAuth refresh dispatch with a durable CAS state and a 20-second HTTP deadline. Expired unsent claims can be taken over; ambiguous dispatched tokens require re-consent and are never resent. Waiter timeouts, including blocked storage reads and local joiners, return retryable unavailable while keeping the grant. SQLite and D1 create and check TTLs with database time so isolate clock skew cannot expire a live holder.
712
+ - Block outstanding and ambiguous downstream OAuth refresh fingerprints across Restart and re-consent. A resolved fingerprint can be used once in a later epoch only when the code exchange observed its resolution before completing. Preserve pending holder liveness across resets and CAS-guard every resolution transition.
713
+ - Record downstream OAuth refresh-token fingerprints before dispatch with outstanding, ambiguous, and resolved state across grant epochs. Retry valid rotation commits without another HTTP request and require re-consent after every dispatched failure or exhausted commit. Waiter deadlines remain retryable without permitting token replay within the epoch or while the outcome remains outstanding or ambiguous.
714
+ - Bind downstream OAuth consents to their issuer, client, token endpoint, and discovery; advance the epoch when the issuer changes. Coordinate refresh redemption across isolates with shared CAS leases. Retry layout-2 credential cleanup after interrupted migration or reset, and require callback state for both OAuth adapters.
715
+ - **A downstream OAuth callback can no longer redeem its code after a restart,
716
+ and a duplicate can no longer destroy the grant another completed.** The
717
+ exchange claims its consent by compare-and-set after every read it depends
718
+ on, then re-reads the epoch and sends the code in the reaction to that read:
719
+ of duplicate callbacks exactly one sends, the rest get the flat 400 for an
720
+ already-used link, and a restart published before the send fails the callback
721
+ with nothing sent. A refused code invalidates only the client or tokens the
722
+ exchange began with, and consumption is recorded on that consent alone, so a
723
+ delayed duplicate never deletes or invalidates a newer consent Continue
724
+ published (#697). A forced restart still reuses an issuer-bound registration
725
+ and recovers from a refused one by registering again (#611).
726
+ - Preserve discrete catalog-change activity after SDK cache refreshes. Compare hash-only baselines per host-computed cache partition, retain request, owner, pool and deferred-write attribution, and disclose private catalog changes only to their admitted owner. Initial, identical, rejected and abandoned catalogs emit no change event.
727
+ - Match advertised resource URI templates with deterministic forward scans. Refuse ambiguous expression boundaries with resource_template_ambiguous and cap each read at 262144 template-plus-URI characters with resource_match_budget_exceeded. Operator diagnostics retain each typed refusal once without URI or template text. Keep scheme and authority literal, reject Unicode controls and format characters at every decoding layer, and allow safe multi-segment reserved and exploded paths.
728
+ - Show empty credential slots as credential_required. Permit Activity reads for authenticated non-interactive principals explicitly granted activityAccess, with connector, tool, personal-owner and pool disclosure checks. Record call-time classification, result byte counts and discrete catalog changes in nullable activity columns, retaining request grouping and legacy rows.
729
+ - Correct agent eval destination aliases and final-answer evidence grading, record runner limitations as typed N/A skips, and support offline regrading with complete saved grading inputs for future trials. Reject contradictory requested record facts and require a structured refusal outcome, and grade simulated URL-auth behavior for both runners. Commit sanitized final live Sonnet 5.5 and GPT-6-Luna baselines with per-task scores, concrete failure triage and the legacy join prompt ambiguity.
730
+ - Mark failed downstream input continuations as not retryable once their single-use nonce is consumed. Read failures without existing recovery prerequisites direct a fresh input round through the original call. Write reconciliation and auth recovery guidance stay unchanged; spent continuations must not be resent. Typed failure codes and first-call retryability are preserved.
731
+ - Classify a reviewed read as a write when its live schema digest changes or cannot be checked (#705). `call_tool` refuses it toward `call_destructive_tool`; `execute_code` refuses it before dispatch in read-only pools, while trusted programs may dispatch it. Legacy discovery filters such as `approval-required` select write classification, not host approval policy. Digests cover the whole schema, and oversized unchecked schemas fail closed. Request-local entries classify intake-redacted facts using the running release's review and deployment overrides. Complete SQL-backed SDK caches retain facts, not classification verdicts; retired registry catalogs are refreshed instead of preserving old read claims.
732
+ - Storage keys and list prefixes reject U+0000 (NUL) with `TypeError` on D1,
733
+ SQLite, and memory storage before any storage access. Key builders reject
734
+ unencoded NUL components, and state-file migration validates every key before
735
+ importing. This prevents Node 22's `node:sqlite` TEXT results from truncating
736
+ keys at NUL while preserving the existing `connecta_kv` TEXT table (#705).
737
+ - Use plain positional SQL parameters with values bound in statement order on
738
+ both storage drivers. Node's SQLite driver prepares the shared SQL unchanged,
739
+ without rewriting quoted text, on every supported Node version (#705).
740
+
741
+ ### Removed
742
+
743
+ - **Breaking:** Workers KV, `fileStorage` from `@zackbart/connecta/node`, and the Worker
744
+ example's copied `cloudflare-kv.ts`, `d1-storage.ts`, `d1-activity.ts`, and
745
+ `d1-activity-row.ts` are removed, as is the Node template's
746
+ `src/file-activity.ts`. `KVStorage.list` and `KVStorage.compareAndSet` are
747
+ required, and `createConnecta` refuses storage missing either at construction.
748
+ Every fallback for stores without compare-and-set is gone: downstream OAuth
749
+ grant discard, stale-write cleanup, and generation fencing; OAuth handoffs and
750
+ single-use connect links; and access-token rename and revocation.
751
+ The Worker example binds one D1 database, `CONNECTA_DB`; the Node template
752
+ keeps everything, activity included, in `CONNECTA_DATABASE` (default
753
+ `./.connecta.sqlite`, `/data/connecta.sqlite` in the container), replacing
754
+ `CONNECTA_STATE_FILE` and `CONNECTA_ACTIVITY_FILE` (#705).
755
+ - **Breaking:** Remove catalog drift counts from `/health`, `connecta doctor`, and activity. Remove the old optional drift-count channel and its event type. Keep discrete catalog-change events in the ordinary activity timeline. Authenticated connector status and the maintainer-run provider check retain drift observations.
756
+ - **Breaking:** Remove built-in artifacts: the `connecta/artifacts` subpath, `artifacts:` config
757
+ slot and `artifactOrigin`, operator pages and navigation, scheduled refresh,
758
+ and agent tools and guide. Publishing belongs in dedicated services.
759
+
760
+ Delete the `connecta/artifacts` import and `artifacts:` configuration, plus
761
+ `artifactOrigin` if set; a configuration that still contains either key fails
762
+ at construction with a `ConfigError`. Remove `CONNECTA_ARTIFACTS` and refresh timers or cron
763
+ handlers. Dedicated artifact bindings or storage can optionally be dropped.
764
+ Existing artifact data is left untouched and ignored; nothing migrates, reads,
765
+ or deletes it as part of this removal (#709).
766
+
767
+ ### Security
768
+
769
+ - Bind GitHub repository grants to repository IDs and narrow installation tokens with `repository_ids`. Renames retain their grants; transfers and replacement repositories at reused names cannot inherit cached access. Verify discovery and search repository IDs before returning data.
770
+ - Collect sent credentials across every connector context in one upstream request
771
+ and redact meta-tool, guest bridge, and serialized MCP outputs at one shared
772
+ boundary. Redact failed catalog listings before shared refresh publication and
773
+ keep catalogs and result stashes sanitized before caching.
774
+ - Refuse credential echoes in normalized downstream OAuth metadata and saved authorization URLs, and restrict operator failure origins. Use an eight-character floor for all echo matching and warn once when configured secrets are below it.
775
+ - Resource URI template matches now re-check the final authority, host interpretation, and path. Matching refuses Unicode separators, URI syntax lookalikes, and dot segments while preserving encoded-percent literals, ordinary percent-encoded UTF-8 path segments, and bounded matching.
776
+ - Redact credentials used by a call from agent-facing downstream diagnostics,
777
+ tool results, and nested errors across all connectors and guest program calls.
778
+ The per-call secret set stays in memory and covers credential slots, auth
779
+ headers, bearer tokens, URL encodings, and base64 forms. Register final outgoing
780
+ requests, including auxiliary OAuth headers and query credentials. Redact mixed
781
+ JSON escapes and joined text blocks after unwrapping and before paging, emits,
782
+ or program returns. Cache one matcher per secret set. Match
783
+ credentials of at least eight characters to avoid corrupting ordinary text with
784
+ short Basic usernames; Connecta's own messages never quote credential values.
785
+ - Register credentials sent during remote MCP discovery and retries, and redact every catalog metadata string before retaining definitions or caching catalogs. Refuse the complete catalog when redaction would change a tool name, preserving dispatch names and catalog completeness.
786
+ - Refuse redirects for credential-bearing OAuth token requests in remote MCP and static API connectors. A redirected refresh requires re-consent without another send. Keep spent fingerprints across grant epochs. A new consent can use a byte-identical refresh token only after the earlier dispatch resolved and the consent observed that resolution.
787
+
788
+ ## 0.28.1 — 2026-10-05
789
+
790
+ This patch adds six maintained connections for church operations and the
791
+ downstream OAuth they needed. Planning Center, Overflow, Tithe.ly, Breeze ChMS,
792
+ and Church Community Builder (Pushpay ChMS) are hand-written `api()` surfaces
793
+ over their whole APIs, with every money-moving write behind approval; Basecamp
794
+ proxies 37signals' hosted MCP server. Church Community Builder accepts only
795
+ OAuth, so `api()` connectors can now declare a static OAuth 2.0
796
+ authorization-code grant that reuses the `remoteMcp()` machinery. Each
797
+ connection is a new opt-in provider subpath and the OAuth grant is opt-in per
798
+ connector: existing imports, connector declarations, credentials, and
799
+ deployment behavior do not change. Two fixes a deployment may notice: a forced
800
+ OAuth restart no longer reuses a client registration the provider has deleted
801
+ ([#611](https://github.com/zackbart/connecta/issues/611)), and a failed OAuth
802
+ code exchange is now logged as fixed text rather than the provider's message.
803
+ Internally, the guarded transport can repeat a query key for an array value,
804
+ and `api()`'s builder moved into its own module so OAuth stays out of bundles
805
+ that never use it.
806
+
807
+ ### Added
808
+
809
+ - **Downstream OAuth on `api()` connectors.** A hand-written connector can
810
+ declare `oauth: { authorizationEndpoint, tokenEndpoint, clientId,
811
+ clientSecret?, tokenEndpointAuthMethod?, scope?, pkce?, authorizationParams?,
812
+ tokenRequestHeaders?, apiOrigins }` for a REST API that accepts only the authorization code grant,
813
+ such as Church Community Builder. Nothing is discovered or registered: the
814
+ endpoints and the provider-issued client are configuration, and the client
815
+ secret comes from the deployment's environment and is never stored. Handlers
816
+ call `ctx.oauth.fetch(url, init)`, which sends the calling owner's token as a
817
+ bearer to a declared origin only, answers a 401 with one coordinated refresh
818
+ and one replay, and otherwise fails `auth_required`. The grant is the
819
+ `remoteMcp()` grant underneath — the same sealed epochs, shared and personal
820
+ ownership, rotating-refresh coordination, dead-versus-outage verdicts,
821
+ `/oauth/callback/<id>`, `authorize_connector`, and operator Connect,
822
+ Restart, and Disconnect. `oauth` and `credential` are exclusive on one
823
+ connector. See [downstream OAuth on `api()`](./documentation/auth.md#downstream-oauth-on-api).
824
+ - **Church Community Builder (Pushpay ChMS) connection.**
825
+ `@zackbart/connecta/providers/ccb` exports `ccb(id, { purpose, environment,
826
+ mode, clientId, clientSecret, access?, scopes?, subdomain? })`, a
827
+ hand-written v2 REST connection over `api()`'s OAuth grant. `environment`
828
+ (`production` or `sandbox`) and `mode` (`system`, one church-wide grant an
829
+ administrator approves; `identity`, each person signing in as themselves
830
+ with a personal grant) are required, because CCB credentials do not cross
831
+ environments and the two modes reach different data. Twenty-four named reads
832
+ cover people and families, groups and participants, events and attendance,
833
+ forms and responses, processes and queues, scheduling, notes, and giving
834
+ metrics and pledges, with projections that drop allergies, giving
835
+ identifiers, and prayer requests unless `raw: true`. `ccb_api_get` and
836
+ `ccb_api_search` reach every GET and advanced search read-only, and
837
+ `ccb_api_mutate` ships only when a write scope is requested. A default
838
+ 60-per-minute admission window per endpoint transcribes CCB's documented
839
+ limit, and `npm run providers:check -- --provider ccb` compares the 41
840
+ touched endpoints with Pushpay's published OpenAPI document. Clients are
841
+ issued by hand through Pushpay's API access request form.
842
+ - **Maintained Planning Center connection.**
843
+ `@zackbart/connecta/providers/planning-center` exports `planningCenter(id,
844
+ options)`, a hand-written `api()` surface over Planning Center Online's REST
845
+ API with no SDK and no new dependency. Named tools cover the jobs church
846
+ staff run — People search, profiles, contact records, lists, workflows and
847
+ card actions, notes, custom fields, and form submissions; Services plans,
848
+ teams, schedules, songs, and scheduling; Groups memberships and events;
849
+ Check-Ins attendance; Calendar occurrences; Registrations signups; and Giving
850
+ donations, funds, batches, pledges, and recurring gifts as reads only — with
851
+ JSON:API flattened into lean records and `raw: true` beside them.
852
+ `pco_api_get` (read-only) and `pco_api_mutate` (always approval-gated) reach
853
+ every other endpoint, including every write that moves money. The credential
854
+ is a personal access token as two labeled fields, and the Test action names
855
+ the person and organization it acts as. Every request sends a reviewed
856
+ `X-PCO-API-Version` per product and a configurable `User-Agent`, and the
857
+ default call budget transcribes Planning Center's 100 requests per 20
858
+ seconds. `npm run providers:check -- --provider planning-center` compares
859
+ the touched endpoints with Planning Center's per-product OpenAPI documents
860
+ and reports a newly published version of any pinned product. Planning
861
+ Center's own hosted MCP server was considered and is not wrapped: its writes
862
+ stop at People.
863
+ - **Maintained Overflow connection.** `@zackbart/connecta/providers/overflow`
864
+ ships 16 projected reads over contributions, deposits and their line items
865
+ and totals, refunds, chargebacks, donors, recurring gifts, payment methods,
866
+ campaigns, locations, Tap events, and webhook delivery logs, plus a
867
+ read-only GET hatch and a destructive JSON-mutation hatch that carries every
868
+ Overflow write — charges, payment authorization, refunds, recurring-gift
869
+ changes, and donor edits — to the host's approval prompt. `environment`
870
+ (`"production"` or `"staging"`) is required with no default, the credential
871
+ is one operator slot with `clientId` and `apiKey` fields, list rows keep
872
+ donor contact details and webhook payloads out unless `raw: true`, and the
873
+ default admission budget transcribes Overflow's 120 requests per minute per
874
+ client. `npm run providers:check -- --provider overflow` compares all 38
875
+ operations with the OpenAPI document Overflow publishes on staging.
876
+ - A maintained Tithe.ly giving connection, `tithely(id, { purpose, environment })`
877
+ from `@zackbart/connecta/providers/tithely`: a hand-written `api()` surface
878
+ over Tithe.ly's v1 REST API. `environment` (`"live"` for tithe.ly, `"test"`
879
+ for tithelydev.com) is required with no default, because Tithe.ly's key pairs
880
+ are environment-specific and carry nothing that says which. The credential
881
+ slot takes the public and private keys as two fields, refuses an obviously
882
+ swapped pair without a request, and its Test action reports the organization
883
+ the keys reach; API access is by request through support@tithe.ly. Named
884
+ read-only tools cover organizations and their funds, donor accounts, charges
885
+ by organization or donor and created-date range, recurring gifts, and
886
+ payment-method metadata, with amounts as integer cents, dates as ISO 8601,
887
+ `hasMore` paging, and organization projections that omit payout bank and
888
+ legal-contact details. Every write moves money or donor payment state and is
889
+ reached only through the always-destructive, form-encoded
890
+ `tithely_api_mutate`; `tithely_api_get` reaches every v1 read. No call budget
891
+ is declared, since Tithe.ly publishes no rate limit.
892
+ `npm run providers:check -- --provider tithely` digests the OpenAPI snippet
893
+ each reference page embeds, for every documented v1 operation.
894
+ - A maintained Breeze ChMS connection (Tithely Church Management since
895
+ 2025-11-11) at `@zackbart/connecta/providers/breeze`:
896
+ `breeze(id, { subdomain, purpose })` binds one church's
897
+ `<subdomain>.breezechms.com` — validated as a single hostname label at
898
+ construction, so no argument can steer the API key elsewhere — with an
899
+ operator `Api-Key` slot tested against the account summary. Named reads cover
900
+ people, profile fields, tags, events, calendars, attendance, forms,
901
+ volunteers, the account log, and giving (`list_contributions`, `list_funds`,
902
+ which call endpoints Breeze stopped documenting in 2023 and still serves);
903
+ named writes add and update people, assign and unassign tags, and record a
904
+ check-in. Because Breeze sends writes as GET too, the escape hatches split by
905
+ endpoint rather than method: `breeze_api_get` admits only a reviewed
906
+ allowlist of read endpoints and their documented parameters, and the
907
+ destructive `breeze_api_mutate` reaches everything else. No admission budget
908
+ is declared, since Breeze documents no rate limit; drift is review-only,
909
+ since it publishes no machine-readable specification.
910
+ - **`basecamp()` — a maintained Basecamp hosted-MCP connection** at
911
+ `@zackbart/connecta/providers/basecamp`, proxying
912
+ `https://mcp.basecamp.com/mcp` — a server 37signals runs but has not
913
+ documented or announced. OAuth only, and `clientMetadataUrl` is required:
914
+ Basecamp restricts dynamic client registration for HTTPS callbacks, so a
915
+ deployment hosts a Client ID Metadata Document listing
916
+ `<publicUrl>/oauth/callback/<id>`, and construction throws without one. A
917
+ grant reaches exactly one Basecamp account, chosen at consent, so the guide
918
+ leads with that, names `get_me` to confirm it, and a second account is a
919
+ second connector. All 227 tools in the live catalog are classified (105
920
+ read-only, 34 additive, 88 destructive); the four verdicts that part from the
921
+ server's own annotations, and `create_stream_ticket`'s agreement with its
922
+ read-only claim, are argued beside their rows. No access mode ships: the MCP
923
+ client requests the scope Basecamp's challenge names, so a `read` option
924
+ would be a label rather than a limit. Personal access tokens are not offered,
925
+ because nothing shows this endpoint accepts one. No default `callAdmission`:
926
+ Basecamp's limits are dynamic, multi-tier, and metered at its own server's
927
+ IP. With no public setup page or inventory, `npm run providers:check` reads
928
+ the server's OAuth discovery instead — the resource names the endpoint and
929
+ the authorization server still accepts metadata-document clients — and
930
+ catalog drift is runtime-only.
931
+
932
+ ### Fixed
933
+
934
+ - **A forced OAuth restart can no longer wedge on a purged client
935
+ registration** ([#611](https://github.com/zackbart/connecta/issues/611)).
936
+ Restart already carried an issuer-bound dynamic registration into the new
937
+ epoch; it carried it unconditionally, and a provider that had forgotten the
938
+ client refuses it at consent without ever redirecting back, so every later
939
+ Restart reused the same dead client and only Disconnect escaped. A carried
940
+ client is now carried again only once a grant in its epoch proves the
941
+ provider still knows it, so two restarts in a row with no consent between
942
+ them register at most once more. A URL-based client (`clientMetadataUrl`)
943
+ and a client whose secret has expired are never carried. Continue no longer
944
+ hands back a pending consent URL whose client a callback's `invalid_client`
945
+ has dropped; it starts a flow that registers instead. Static `api()` OAuth
946
+ clients are configuration and unaffected.
947
+ - **A refresh rotation saved after its request was cancelled keeps its
948
+ issuer.** When the token endpoint answered after the owning request left,
949
+ connecta persisted the rotated tokens itself but unbound, and the next
950
+ issuer-aware read adopted them for whichever authorization server the
951
+ connector named by then — so a connector repointed at another server would
952
+ send the rotated refresh token there. The recovery save now stamps the issuer
953
+ the refresh answered to, as the SDK's own save does, and a repointed connector
954
+ fences the grant instead.
955
+ - **A failed OAuth code exchange no longer writes the provider's message to the
956
+ operator log.** The SDK quotes a token endpoint's `error_description` or raw
957
+ body, and a provider that echoes a `client_secret_post` request put the client
958
+ secret, code, and verifier in the log. The callback now logs fixed text and,
959
+ when the error carries one, a standard OAuth error code from an allowlist.
960
+
5
961
  ## 0.28.0 — 2026-10-04
6
962
 
7
963
  This minor release removes program pauses ([#672](https://github.com/zackbart/connecta/issues/672)).
@@ -1480,7 +2436,7 @@ residue from #179, and the v1 persisted-catalog reader, unreachable since
1480
2436
  - **Package and docs.** Six finished design records moved to
1481
2437
  [`records/`](https://github.com/zackbart/connecta/tree/main/records), outside
1482
2438
  the published files, and packed-link validation folded into the documentation
1483
- checker: −1,342 shipped lines, −91 KB (#464). `ethos.md` is the constitution
2439
+ checker: −1,342 shipped lines, −91 KB (#464). the ethos is the constitution
1484
2440
  again — refusals, invariants, and what this is, at 1,188 words instead of
1485
2441
  3,174, with every decision intact and a word cap replacing the line cap
1486
2442
  (#471).
@@ -3026,7 +3982,7 @@ the new text is the entire release (#282, PR #284).
3026
3982
  says to fetch first and check the shape in code, and on a surprise — empty
3027
3983
  array, missing key — to return a trimmed first record instead of rendering.
3028
3984
  The wrong view becomes the sample you needed.
3029
- - **Result sampling on the catalog surface is refused.** The `ethos.md`
3985
+ - **Result sampling on the catalog surface is refused.** The former ethos
3030
3986
  decisions table records `sample` / `dryRun` as refused: sampling is execution
3031
3987
  and cannot ride a catalog read, most tools carry required arguments no
3032
3988
  sampler can invent, and a program that checks the shape before rendering
@@ -3383,7 +4339,7 @@ nine tools with the same descriptions and the same instructions as before, byte
3383
4339
  for byte. `surface: "classic"` alongside an executor restores the ten-tool shape.
3384
4340
 
3385
4341
  The default was flipped by owner decision for a single-operator deployment rather
3386
- than by the repeated per-model eval that [`ethos.md`](./ethos.md) had gated it on;
4342
+ than by the repeated per-model eval that [former ethos](https://github.com/zackbart/connecta/blob/main/decisions/0001-ethos-verdict-table.md) had gated it on;
3387
4343
  that row now records the decision and its reasoning, and
3388
4344
  [`eval/code-first-gate`](./eval/code-first-gate/README.md) remains as measurement
3389
4345
  with all three of its arms now real deployment shapes rather than harness
@@ -3648,13 +4604,13 @@ optional property checks, and an automated Workers test-partition guard. A
3648
4604
  deployment can take this release without touching its config: entrypoints,
3649
4605
  options, storage formats, and the tool surface are unchanged. The docs were
3650
4606
  restructured — the old numbered manual is retired in favor of a terse
3651
- [`ethos.md`](./ethos.md) and stub guides in `documentation/` — and the
4607
+ [former ethos](https://github.com/zackbart/connecta/blob/main/decisions/0001-ethos-verdict-table.md) and stub guides in `documentation/` — and the
3652
4608
  temporary `@hono/node-server` override is gone now that the SDK (1.30.0)
3653
4609
  depends on a patched version upstream.
3654
4610
 
3655
4611
  ### Added
3656
4612
 
3657
- - **`ethos.md`** — what connecta is, what it refuses to be, a decisions table,
4613
+ - **The former ethos document** — what connecta is, what it refuses to be, a decisions table,
3658
4614
  and the invariants, each enforced or reviewer-owned (#175, PR #180). CI caps
3659
4615
  its length; terseness is the point.
3660
4616
  - **Route-contract and operator-UI browser tests** pinning server behavior