@zackbart/connecta 0.15.0 → 0.16.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 (332) hide show
  1. package/AGENTS.md +28 -15
  2. package/CHANGELOG.md +493 -0
  3. package/README.md +19 -4
  4. package/bin/connecta.mjs +44 -3
  5. package/dist/access-tokens.d.ts +0 -1
  6. package/dist/access-tokens.js +0 -1
  7. package/dist/activity.d.ts +50 -2
  8. package/dist/activity.js +34 -1
  9. package/dist/apps-shell.d.ts +0 -1
  10. package/dist/apps-shell.js +0 -1
  11. package/dist/auth/bearer.d.ts +0 -1
  12. package/dist/auth/bearer.js +0 -1
  13. package/dist/auth/clerk.d.ts +0 -1
  14. package/dist/auth/clerk.js +45 -9
  15. package/dist/auth/downstream-oauth.d.ts +0 -1
  16. package/dist/auth/downstream-oauth.js +0 -1
  17. package/dist/call-admission.d.ts +0 -1
  18. package/dist/call-admission.js +0 -1
  19. package/dist/catalog-drift.d.ts +99 -0
  20. package/dist/catalog-drift.js +251 -0
  21. package/dist/catalog-fingerprint.d.ts +0 -1
  22. package/dist/catalog-fingerprint.js +0 -1
  23. package/dist/catalog-limits.d.ts +0 -1
  24. package/dist/catalog-limits.js +0 -1
  25. package/dist/catalog-service.d.ts +0 -1
  26. package/dist/catalog-service.js +0 -1
  27. package/dist/catalog.d.ts +0 -1
  28. package/dist/catalog.js +0 -1
  29. package/dist/concurrency.d.ts +0 -1
  30. package/dist/concurrency.js +0 -1
  31. package/dist/connector-scope.d.ts +0 -1
  32. package/dist/connector-scope.js +0 -1
  33. package/dist/connectors/api.d.ts +29 -21
  34. package/dist/connectors/api.js +45 -19
  35. package/dist/connectors/guarded-fetch.d.ts +106 -0
  36. package/dist/connectors/guarded-fetch.js +289 -0
  37. package/dist/connectors/remote-mcp.d.ts +0 -1
  38. package/dist/connectors/remote-mcp.js +0 -1
  39. package/dist/credentials.d.ts +0 -1
  40. package/dist/credentials.js +0 -1
  41. package/dist/errors.d.ts +0 -1
  42. package/dist/errors.js +0 -1
  43. package/dist/execute.d.ts +0 -1
  44. package/dist/execute.js +0 -1
  45. package/dist/executor-admission.d.ts +11 -1
  46. package/dist/executor-admission.js +28 -1
  47. package/dist/executor-result.d.ts +0 -1
  48. package/dist/executor-result.js +0 -1
  49. package/dist/executors/quickjs-child.d.ts +0 -1
  50. package/dist/executors/quickjs-child.js +0 -1
  51. package/dist/executors/quickjs-protocol.d.ts +0 -1
  52. package/dist/executors/quickjs-protocol.js +0 -1
  53. package/dist/executors/quickjs-runtime.d.ts +0 -1
  54. package/dist/executors/quickjs-runtime.js +0 -1
  55. package/dist/executors/quickjs.d.ts +0 -1
  56. package/dist/executors/quickjs.js +2 -1
  57. package/dist/favicon.d.ts +0 -1
  58. package/dist/favicon.js +0 -1
  59. package/dist/index.d.ts +2 -3
  60. package/dist/index.js +24 -7
  61. package/dist/invocation.d.ts +0 -1
  62. package/dist/invocation.js +0 -1
  63. package/dist/json-schema.d.ts +0 -1
  64. package/dist/json-schema.js +0 -1
  65. package/dist/mcp-result.d.ts +0 -1
  66. package/dist/mcp-result.js +0 -1
  67. package/dist/meta-tools.d.ts +0 -1
  68. package/dist/meta-tools.js +0 -1
  69. package/dist/node.d.ts +0 -1
  70. package/dist/node.js +0 -1
  71. package/dist/operator-ui/generated.d.ts +2 -3
  72. package/dist/operator-ui/generated.js +3 -4
  73. package/dist/operator-ui/model.d.ts +10 -1
  74. package/dist/operator-ui/model.js +0 -1
  75. package/dist/operator-ui/view.d.ts +167 -0
  76. package/dist/operator-ui/view.js +270 -0
  77. package/dist/providers/cloudflare.d.ts +5 -2
  78. package/dist/providers/cloudflare.js +435 -328
  79. package/dist/providers/linear.d.ts +26 -5
  80. package/dist/providers/linear.js +32 -60
  81. package/dist/providers/mixpanel.d.ts +35 -4
  82. package/dist/providers/mixpanel.js +53 -78
  83. package/dist/providers/notion.d.ts +0 -1
  84. package/dist/providers/notion.js +87 -42
  85. package/dist/providers/stripe.d.ts +13 -1
  86. package/dist/providers/stripe.js +31 -59
  87. package/dist/registry.d.ts +34 -2
  88. package/dist/registry.js +85 -5
  89. package/dist/retired-toolkits.d.ts +0 -1
  90. package/dist/retired-toolkits.js +0 -1
  91. package/dist/routes/access-tokens.d.ts +0 -1
  92. package/dist/routes/access-tokens.js +0 -1
  93. package/dist/routes/activity.d.ts +0 -1
  94. package/dist/routes/activity.js +0 -1
  95. package/dist/routes/credentials.d.ts +0 -1
  96. package/dist/routes/credentials.js +0 -1
  97. package/dist/routes/mcp.d.ts +0 -1
  98. package/dist/routes/mcp.js +0 -1
  99. package/dist/routes/oauth.d.ts +0 -1
  100. package/dist/routes/oauth.js +0 -1
  101. package/dist/routes/shared.d.ts +2 -1
  102. package/dist/routes/shared.js +0 -1
  103. package/dist/routes/ui.d.ts +0 -1
  104. package/dist/routes/ui.js +0 -1
  105. package/dist/server.d.ts +0 -1
  106. package/dist/server.js +11 -1
  107. package/dist/skills.d.ts +0 -1
  108. package/dist/skills.js +0 -1
  109. package/dist/storage/file.d.ts +0 -1
  110. package/dist/storage/file.js +0 -1
  111. package/dist/storage/memory.d.ts +0 -1
  112. package/dist/storage/memory.js +0 -1
  113. package/dist/timeout.d.ts +0 -1
  114. package/dist/timeout.js +0 -1
  115. package/dist/tool-safety.d.ts +0 -1
  116. package/dist/tool-safety.js +0 -1
  117. package/dist/types.d.ts +45 -1
  118. package/dist/types.js +0 -1
  119. package/dist/ui.d.ts +0 -1
  120. package/dist/ui.js +13 -137
  121. package/dist/validate.d.ts +13 -15
  122. package/dist/validate.js +24 -11
  123. package/dist/version.d.ts +1 -2
  124. package/dist/version.js +1 -2
  125. package/documentation/architecture.md +186 -5
  126. package/documentation/auth.md +11 -0
  127. package/documentation/call-admission.md +142 -6
  128. package/documentation/cloudflare.md +95 -27
  129. package/documentation/code-mode.md +12 -12
  130. package/documentation/connector-guides.md +172 -5
  131. package/documentation/connectors.md +98 -7
  132. package/documentation/linear.md +26 -9
  133. package/documentation/mcp-ui-design.md +2 -2
  134. package/documentation/mixpanel.md +48 -16
  135. package/documentation/notion.md +16 -0
  136. package/documentation/operations.md +283 -5
  137. package/documentation/operator-ui.md +120 -5
  138. package/documentation/provider-audit.md +160 -0
  139. package/documentation/provider-conventions.md +631 -0
  140. package/documentation/request-admission.md +167 -5
  141. package/documentation/rich-output-design.md +1 -2
  142. package/documentation/stripe.md +27 -12
  143. package/ethos.md +14 -9
  144. package/examples/worker/README.md +96 -12
  145. package/examples/worker/src/index.ts +46 -1
  146. package/examples/worker/wrangler.jsonc +14 -1
  147. package/package.json +4 -5
  148. package/templates/node/.dockerignore +10 -0
  149. package/templates/node/.env.example +40 -3
  150. package/templates/node/AGENTS.md +11 -1
  151. package/templates/node/Dockerfile +49 -0
  152. package/templates/node/README.md +117 -2
  153. package/templates/node/docker-compose.yml +39 -0
  154. package/templates/node/package.json +1 -1
  155. package/templates/node/src/file-activity.ts +182 -0
  156. package/templates/node/src/index.ts +80 -4
  157. package/assets/connecta-clay-hero.png +0 -0
  158. package/dist/access-tokens.d.ts.map +0 -1
  159. package/dist/access-tokens.js.map +0 -1
  160. package/dist/activity.d.ts.map +0 -1
  161. package/dist/activity.js.map +0 -1
  162. package/dist/apps-shell.d.ts.map +0 -1
  163. package/dist/apps-shell.js.map +0 -1
  164. package/dist/auth/bearer.d.ts.map +0 -1
  165. package/dist/auth/bearer.js.map +0 -1
  166. package/dist/auth/clerk.d.ts.map +0 -1
  167. package/dist/auth/clerk.js.map +0 -1
  168. package/dist/auth/downstream-oauth.d.ts.map +0 -1
  169. package/dist/auth/downstream-oauth.js.map +0 -1
  170. package/dist/call-admission.d.ts.map +0 -1
  171. package/dist/call-admission.js.map +0 -1
  172. package/dist/catalog-fingerprint.d.ts.map +0 -1
  173. package/dist/catalog-fingerprint.js.map +0 -1
  174. package/dist/catalog-limits.d.ts.map +0 -1
  175. package/dist/catalog-limits.js.map +0 -1
  176. package/dist/catalog-service.d.ts.map +0 -1
  177. package/dist/catalog-service.js.map +0 -1
  178. package/dist/catalog.d.ts.map +0 -1
  179. package/dist/catalog.js.map +0 -1
  180. package/dist/concurrency.d.ts.map +0 -1
  181. package/dist/concurrency.js.map +0 -1
  182. package/dist/connector-scope.d.ts.map +0 -1
  183. package/dist/connector-scope.js.map +0 -1
  184. package/dist/connectors/api.d.ts.map +0 -1
  185. package/dist/connectors/api.js.map +0 -1
  186. package/dist/connectors/remote-mcp.d.ts.map +0 -1
  187. package/dist/connectors/remote-mcp.js.map +0 -1
  188. package/dist/credentials.d.ts.map +0 -1
  189. package/dist/credentials.js.map +0 -1
  190. package/dist/errors.d.ts.map +0 -1
  191. package/dist/errors.js.map +0 -1
  192. package/dist/execute.d.ts.map +0 -1
  193. package/dist/execute.js.map +0 -1
  194. package/dist/executor-admission.d.ts.map +0 -1
  195. package/dist/executor-admission.js.map +0 -1
  196. package/dist/executor-result.d.ts.map +0 -1
  197. package/dist/executor-result.js.map +0 -1
  198. package/dist/executors/quickjs-child.d.ts.map +0 -1
  199. package/dist/executors/quickjs-child.js.map +0 -1
  200. package/dist/executors/quickjs-protocol.d.ts.map +0 -1
  201. package/dist/executors/quickjs-protocol.js.map +0 -1
  202. package/dist/executors/quickjs-runtime.d.ts.map +0 -1
  203. package/dist/executors/quickjs-runtime.js.map +0 -1
  204. package/dist/executors/quickjs.d.ts.map +0 -1
  205. package/dist/executors/quickjs.js.map +0 -1
  206. package/dist/favicon.d.ts.map +0 -1
  207. package/dist/favicon.js.map +0 -1
  208. package/dist/index.d.ts.map +0 -1
  209. package/dist/index.js.map +0 -1
  210. package/dist/invocation.d.ts.map +0 -1
  211. package/dist/invocation.js.map +0 -1
  212. package/dist/json-schema.d.ts.map +0 -1
  213. package/dist/json-schema.js.map +0 -1
  214. package/dist/mcp-result.d.ts.map +0 -1
  215. package/dist/mcp-result.js.map +0 -1
  216. package/dist/meta-tools.d.ts.map +0 -1
  217. package/dist/meta-tools.js.map +0 -1
  218. package/dist/node.d.ts.map +0 -1
  219. package/dist/node.js.map +0 -1
  220. package/dist/operator-ui/generated.d.ts.map +0 -1
  221. package/dist/operator-ui/generated.js.map +0 -1
  222. package/dist/operator-ui/model.d.ts.map +0 -1
  223. package/dist/operator-ui/model.js.map +0 -1
  224. package/dist/providers/cloudflare.d.ts.map +0 -1
  225. package/dist/providers/cloudflare.js.map +0 -1
  226. package/dist/providers/linear.d.ts.map +0 -1
  227. package/dist/providers/linear.js.map +0 -1
  228. package/dist/providers/mixpanel.d.ts.map +0 -1
  229. package/dist/providers/mixpanel.js.map +0 -1
  230. package/dist/providers/notion.d.ts.map +0 -1
  231. package/dist/providers/notion.js.map +0 -1
  232. package/dist/providers/stripe.d.ts.map +0 -1
  233. package/dist/providers/stripe.js.map +0 -1
  234. package/dist/registry.d.ts.map +0 -1
  235. package/dist/registry.js.map +0 -1
  236. package/dist/retired-toolkits.d.ts.map +0 -1
  237. package/dist/retired-toolkits.js.map +0 -1
  238. package/dist/routes/access-tokens.d.ts.map +0 -1
  239. package/dist/routes/access-tokens.js.map +0 -1
  240. package/dist/routes/activity.d.ts.map +0 -1
  241. package/dist/routes/activity.js.map +0 -1
  242. package/dist/routes/credentials.d.ts.map +0 -1
  243. package/dist/routes/credentials.js.map +0 -1
  244. package/dist/routes/mcp.d.ts.map +0 -1
  245. package/dist/routes/mcp.js.map +0 -1
  246. package/dist/routes/oauth.d.ts.map +0 -1
  247. package/dist/routes/oauth.js.map +0 -1
  248. package/dist/routes/shared.d.ts.map +0 -1
  249. package/dist/routes/shared.js.map +0 -1
  250. package/dist/routes/ui.d.ts.map +0 -1
  251. package/dist/routes/ui.js.map +0 -1
  252. package/dist/server.d.ts.map +0 -1
  253. package/dist/server.js.map +0 -1
  254. package/dist/skills.d.ts.map +0 -1
  255. package/dist/skills.js.map +0 -1
  256. package/dist/storage/file.d.ts.map +0 -1
  257. package/dist/storage/file.js.map +0 -1
  258. package/dist/storage/memory.d.ts.map +0 -1
  259. package/dist/storage/memory.js.map +0 -1
  260. package/dist/timeout.d.ts.map +0 -1
  261. package/dist/timeout.js.map +0 -1
  262. package/dist/tool-safety.d.ts.map +0 -1
  263. package/dist/tool-safety.js.map +0 -1
  264. package/dist/types.d.ts.map +0 -1
  265. package/dist/types.js.map +0 -1
  266. package/dist/ui.d.ts.map +0 -1
  267. package/dist/ui.js.map +0 -1
  268. package/dist/validate.d.ts.map +0 -1
  269. package/dist/validate.js.map +0 -1
  270. package/dist/version.d.ts.map +0 -1
  271. package/dist/version.js.map +0 -1
  272. package/examples/node/README.md +0 -52
  273. package/examples/node/src/index.ts +0 -71
  274. package/src/access-tokens.ts +0 -289
  275. package/src/activity.ts +0 -234
  276. package/src/apps-shell.ts +0 -370
  277. package/src/auth/bearer.ts +0 -61
  278. package/src/auth/clerk.ts +0 -553
  279. package/src/auth/downstream-oauth.ts +0 -575
  280. package/src/call-admission.ts +0 -521
  281. package/src/catalog-fingerprint.ts +0 -52
  282. package/src/catalog-limits.ts +0 -11
  283. package/src/catalog-service.ts +0 -1153
  284. package/src/catalog.ts +0 -790
  285. package/src/concurrency.ts +0 -39
  286. package/src/connector-scope.ts +0 -66
  287. package/src/connectors/api.ts +0 -159
  288. package/src/connectors/remote-mcp.ts +0 -1090
  289. package/src/credentials.ts +0 -428
  290. package/src/errors.ts +0 -364
  291. package/src/execute.ts +0 -1406
  292. package/src/executor-admission.ts +0 -328
  293. package/src/executor-result.ts +0 -107
  294. package/src/executors/quickjs-child.ts +0 -172
  295. package/src/executors/quickjs-protocol.ts +0 -99
  296. package/src/executors/quickjs-runtime.ts +0 -470
  297. package/src/executors/quickjs.ts +0 -776
  298. package/src/favicon.ts +0 -53
  299. package/src/index.ts +0 -666
  300. package/src/invocation.ts +0 -655
  301. package/src/json-schema.ts +0 -11
  302. package/src/mcp-result.ts +0 -43
  303. package/src/meta-tools.ts +0 -1607
  304. package/src/node.ts +0 -206
  305. package/src/operator-ui/browser.css +0 -423
  306. package/src/operator-ui/browser.ts +0 -1281
  307. package/src/operator-ui/generated.ts +0 -4
  308. package/src/operator-ui/model.ts +0 -111
  309. package/src/providers/cloudflare.ts +0 -3803
  310. package/src/providers/linear.ts +0 -301
  311. package/src/providers/mixpanel.ts +0 -228
  312. package/src/providers/notion.ts +0 -1879
  313. package/src/providers/stripe.ts +0 -306
  314. package/src/registry.ts +0 -1085
  315. package/src/retired-toolkits.ts +0 -14
  316. package/src/routes/access-tokens.ts +0 -115
  317. package/src/routes/activity.ts +0 -215
  318. package/src/routes/credentials.ts +0 -254
  319. package/src/routes/mcp.ts +0 -429
  320. package/src/routes/oauth.ts +0 -322
  321. package/src/routes/shared.ts +0 -247
  322. package/src/routes/ui.ts +0 -152
  323. package/src/server.ts +0 -195
  324. package/src/skills.ts +0 -271
  325. package/src/storage/file.ts +0 -123
  326. package/src/storage/memory.ts +0 -44
  327. package/src/timeout.ts +0 -84
  328. package/src/tool-safety.ts +0 -15
  329. package/src/types.ts +0 -523
  330. package/src/ui.ts +0 -744
  331. package/src/validate.ts +0 -387
  332. package/src/version.ts +0 -7
@@ -47,13 +47,15 @@ choice does not grant the connection different runtime privileges. Two
47
47
  instances of the same provider are isolated in exactly the same way as two
48
48
  hand-written connectors with different ids.
49
49
 
50
- A prebuilt connection's vetted annotations are fill-in only. They classify what
51
- the downstream leaves unannotated and do not argue with what it states — not an
52
- explicit `destructiveHint: true` or `readOnlyHint: false` on a name the
53
- connection files as a read, nor an explicit `readOnlyHint: true` on a name no
54
- release has classified at all. Silence on an unclassified name still means not
55
- read-only, so catalog drift fails closed. The fail-closed read-only invariant
56
- is unchanged by the authoring path.
50
+ A prebuilt connection's vetted annotations fill in downstream silence and
51
+ otherwise preserve explicit annotations. This includes an explicit
52
+ `destructiveHint: true` or `readOnlyHint: false` on a vetted read, and an
53
+ explicit `readOnlyHint: true` on a name no release has classified. One narrow
54
+ exception stays fail-closed: a release-reviewed destructive classification
55
+ overrides a contradictory `readOnlyHint: true`, because Connecta has
56
+ independently established that the tool mutates existing state. Silence on an
57
+ unclassified name still means not read-only. The authoring path never weakens
58
+ the fail-closed read-only invariant.
57
59
 
58
60
  Prebuilt means preferred when available, not mandatory. A deployment may mix
59
61
  prebuilt connections, custom `remoteMcp()` connections, and custom `api()`
@@ -109,6 +111,11 @@ export const connecta = createConnecta({
109
111
  All three are ordinary `Connector` instances by the time the registry sees
110
112
  them. Nothing in the list is privileged by how it was authored.
111
113
 
114
+ What a maintained connection owes an agent is written down rather than
115
+ inherited from whoever wrote it last:
116
+ [provider conventions](./provider-conventions.md), one set for hand-written
117
+ HTTP surfaces and one for hosted-MCP proxies.
118
+
112
119
  Maintained provider guides:
113
120
 
114
121
  - [Cloudflare](./cloudflare.md)
@@ -117,6 +124,90 @@ Maintained provider guides:
117
124
  - [Notion](./notion.md)
118
125
  - [Stripe](./stripe.md)
119
126
 
127
+ ## The `api()` construction contract
128
+
129
+ `api()` is the path every custom integration takes, and whatever it accepts is
130
+ what an agent eventually reads. Three things are refused at construction rather
131
+ than discovered in production
132
+ ([#340](https://github.com/zackbart/connecta/issues/340)):
133
+
134
+ - **A tool with no description.** Discovery has nothing else to route on, and a
135
+ guess costs a call.
136
+ - **A tool with no explicit boolean `annotations.readOnlyHint`.** `true`
137
+ declares a read and admits the tool to `call_tool` and `execute_code`;
138
+ `false` declares work that crosses `call_destructive_tool`, where the host
139
+ can ask a human. Connecta never infers the classification from a tool name,
140
+ description, schema, HTTP method, or the other annotations — an unclassified
141
+ tool is a bug in the deployment, not a puzzle to solve.
142
+ - **An `inputSchema` the validator cannot compile.** Declaring one is optional;
143
+ declaring one that cannot be enforced is not. A schema that only reveals
144
+ itself on first use — an unresolvable `$ref`, say — fails that call as
145
+ non-retryable `invalid_args` rather than forwarding raw arguments to the
146
+ handler. `validateArgs: false` still opts out of enforcement for deployments
147
+ that want loose coercion; it does not opt out of the schema being real.
148
+
149
+ None of this reaches a proxied catalog: hosted-MCP tools arrive as their
150
+ downstream wrote them, and an unannotated or contradictory one stays
151
+ fail-closed onto `call_destructive_tool`. The contract binds the surfaces we
152
+ write, not the catalogs we relay.
153
+
154
+ ## The guarded fetch transport
155
+
156
+ Every hand-written HTTP surface re-derives the same safety machinery, and two
157
+ of them had already derived it slightly differently. `src/connectors/guarded-fetch.ts`
158
+ is that machinery extracted once ([#341](https://github.com/zackbart/connecta/issues/341)):
159
+ one `guardedFetch({ provider, baseUrl, headers, maxResponseBytes, authenticate })`
160
+ factory returning the transport a connector sends every request through.
161
+
162
+ What it owns is mechanical and provider-independent:
163
+
164
+ - **Confinement.** A request path is provider-relative, carries no query or
165
+ fragment, and is re-checked against the base origin and path prefix *after*
166
+ `new URL` normalization — because normalization is how a path escapes a
167
+ prefix, not something to trust before it.
168
+ - **Construction.** Query parameters are encoded rather than concatenated, a
169
+ JSON body is serialized with the `Content-Type` to match, and a pre-framed
170
+ body gets none, so `fetch` still picks the multipart boundary.
171
+ - **Credential forwarding.** `authenticate` is called once per request and its
172
+ headers are applied last; a request header wearing one of their names is
173
+ refused rather than allowed to shadow it. A 3xx is refused outright — a
174
+ redirect is an instruction to re-send the credential to whatever origin the
175
+ `Location` names, and a confinement a redirect can undo was never one.
176
+ - **Bounded reads.** `maxResponseBytes` is required, not defaulted: what counts
177
+ as an absurd response is a fact about the API, not about HTTP. A declared
178
+ `Content-Length` past the ceiling fails before a byte is read, and a
179
+ streaming body is abandoned at the ceiling rather than buffered past it.
180
+ - **Normalization.** An unreachable provider becomes a retryable `unavailable`
181
+ instead of whatever `TypeError` the runtime threw, and `ctx.signal` rides
182
+ every request.
183
+
184
+ What it deliberately does not own is meaning. It never reads a status code and
185
+ never invents an authentication scheme: the provider's `authenticate` callback
186
+ supplies the headers, and the provider's mapper turns one `GuardedResponse`
187
+ into a result or a typed failure. That split is not fastidiousness. Notion's
188
+ 403 means a capability the integration was never granted — re-authorizing
189
+ cannot fix it — while Cloudflare's means a token scope, and the two want
190
+ opposite next moves. A helper that guessed would be wrong for one of them.
191
+
192
+ Cloudflare and Notion both run on it. Their existing suites carried over
193
+ unchanged, which proves the migration kept the behavior those suites cover —
194
+ not that nothing changed. Three things did, and the changelog names them: a
195
+ 3xx is refused where both providers used to follow it, both now fail past
196
+ their byte ceiling, and `cloudflare()`'s `baseUrl` is validated at
197
+ construction. Each suite gained one test for the ceiling, because the one
198
+ guard the helper was written to add is the one a provider's own mapper can
199
+ most easily disarm: a bare `catch` around `response.json()` swallows the
200
+ transport's refusal along with a parse error, and turns a response nobody was
201
+ allowed to read into an empty success. A mapper re-throws
202
+ `ConnectorCallError` and swallows only what it recognizes.
203
+
204
+ It is **not exported this release**: the two migrations proved the shape
205
+ preserves behavior for connectors that already had this machinery, not that it
206
+ is the right shape for an author starting from nothing, and an unexported
207
+ symbol costs nothing to reshape while a published one is a promise.
208
+ The provider audit ([#342](https://github.com/zackbart/connecta/issues/342))
209
+ supplies the third caller that would settle it.
210
+
120
211
  ## MCP version skew
121
212
 
122
213
  Connecta deliberately sits between protocol generations
@@ -11,6 +11,7 @@ import { linear } from "@zackbart/connecta/providers/linear";
11
11
  const tracker = linear("product_tracker", {
12
12
  title: "Product issue tracking",
13
13
  purpose: "Issue and project planning for the platform team",
14
+ access: "read-write",
14
15
  instructions: "File bugs into the Platform team unless the request names another.",
15
16
  });
16
17
  ```
@@ -23,13 +24,21 @@ cannot change the connector's safety classification.
23
24
 
24
25
  ## Access modes
25
26
 
26
- Linear publishes two hosted endpoints, and `access` selects between them:
27
+ Linear publishes two hosted endpoints, and `access` selects between them. It is
28
+ **required**, with no default:
27
29
 
28
30
  | `access` | Endpoint | OAuth scopes |
29
31
  | --- | --- | --- |
30
- | `"read-write"` (default) | `https://mcp.linear.app/mcp` | `read`, `write` |
32
+ | `"read-write"` | `https://mcp.linear.app/mcp` | `read`, `write` |
31
33
  | `"read-only"` | `https://mcp.linear.app/mcp/readonly` | `read` |
32
34
 
35
+ There is no safe default to pick. Defaulting to `"read-write"` hands a
36
+ deployment write access it never asked for; defaulting to `"read-only"` turns a
37
+ deployment that does write into one whose every write fails at Linear — at
38
+ runtime, where no agent can repair it. So the operator declares which one this
39
+ is, and a deployment that forgot fails at construction with a message naming
40
+ both options.
41
+
33
42
  Read-only is not a client-side filter. The endpoint advertises the `read` scope
34
43
  alone, so the token minted for it cannot reach Linear's write APIs — a stronger
35
44
  guarantee than any annotation Connecta applies. A deployment that only reports
@@ -63,6 +72,7 @@ also accepts a bearer token or a personal API key passed directly in the
63
72
  ```ts
64
73
  linear("automation_tracker", {
65
74
  purpose: "Headless release reporting",
75
+ access: "read-only",
66
76
  auth: {
67
77
  type: "headers",
68
78
  headers: { Authorization: env.LINEAR_API_KEY },
@@ -83,18 +93,17 @@ The wrapper classifies Linear's documented `list_*`, `get_*`, and
83
93
  downstream leaves unannotated fails closed onto `call_destructive_tool` until a
84
94
  Connecta release reviews it.
85
95
 
86
- That classification is **fill-in only**, and unconditionally so: it supplies
87
- the annotations Linear leaves unset and contradicts an explicit downstream
88
- annotation in neither direction. A tool on the read allowlist arriving with
96
+ That classification fills in downstream silence and otherwise preserves
97
+ explicit annotations. A tool on the read allowlist arriving with
89
98
  `destructiveHint: true` or `readOnlyHint: false` keeps exactly what the
90
99
  downstream said and stays behind `call_destructive_tool`. A tool on neither
91
100
  maintained list arriving with `readOnlyHint: true` keeps that too, and stays
92
101
  callable from `execute_code`. Both are the downstream telling you this
93
102
  release's allowlist is stale, and on a name no release has reviewed its word is
94
- the only evidence there is. The one classification that still outranks the
95
- downstream is a name this release reviewed and filed destructive: a `save_*`
96
- tool claiming `readOnlyHint: true` is a downstream bug rather than news, and
97
- stays on the approval path.
103
+ the only evidence there is. One narrow fail-closed exception applies to a name
104
+ this release reviewed and filed destructive: a `save_*` tool claiming
105
+ `readOnlyHint: true` is a downstream bug rather than news, and stays on the
106
+ approval path.
98
107
 
99
108
  One detail of Linear's own design shapes the classification: **`save_*` tools
100
109
  are upserts.** Omitting a record id creates; supplying one updates in place.
@@ -131,6 +140,7 @@ one. An operator who knows their workspace can supply one explicitly:
131
140
  ```ts
132
141
  linear("product_tracker", {
133
142
  purpose: "Issue and project planning for the platform team",
143
+ access: "read-write",
134
144
  callAdmission: {
135
145
  rules: [
136
146
  { budget: { kind: "rolling-window", maxCalls: 1_000, windowMs: 3_600_000 } },
@@ -142,3 +152,10 @@ linear("product_tracker", {
142
152
  A budget-only rule needs no queue. If you add `maxConcurrency` you are asking
143
153
  for a queue, and the admission controller then requires the rest of the queue
144
154
  settings at construction.
155
+
156
+ ## Conventions
157
+
158
+ This connection is audited against
159
+ [the provider conventions](./provider-conventions.md). Its verdict per
160
+ convention, including every recorded exception, is the Linear section of
161
+ [the provider audit](./provider-audit.md).
@@ -375,8 +375,8 @@ buy a guarantee the protocol already gives away.
375
375
  source carries the `srcdoc` and sandbox attributes and contains no
376
376
  message-forwarding path from the inner frame to the host.
377
377
  - `U1`–`U11` fold into `code-mode.md`, the `execute_code` description documents
378
- `connecta.ui`, and the test-map row lands in
379
- [`operations.md`](./operations.md) with that guide.
378
+ `connecta.ui`, and the suite takes its row in the
379
+ [test map](./operations.md#the-test-map).
380
380
  - `npm run check` passes.
381
381
 
382
382
  The implementation issue (#277) carries these as acceptance criteria.
@@ -2,9 +2,9 @@
2
2
 
3
3
  Import `mixpanel()` independently from
4
4
  `@zackbart/connecta/providers/mixpanel`. It wraps Mixpanel's hosted MCP server
5
- with regional endpoint selection, OAuth by default, a provider-rate admission
6
- budget, a task-oriented usage guide, and a vetted safety classification. It
7
- adds no provider dependency and is not reachable from Connecta's root entry.
5
+ with regional endpoint selection, OAuth by default, a task-oriented usage
6
+ guide, and a vetted safety classification. It adds no provider dependency and
7
+ is not reachable from Connecta's root entry.
8
8
 
9
9
  ```ts
10
10
  import { mixpanel } from "@zackbart/connecta/providers/mixpanel";
@@ -25,6 +25,12 @@ connector's safety classification.
25
25
 
26
26
  `region` accepts `"us"` (the default), `"eu"`, or `"in"` and selects the
27
27
  corresponding [official hosted endpoint](https://docs.mixpanel.com/docs/mcp#mcp-server-urls).
28
+ A project lives in exactly one residency, so the region also decides what this
29
+ connection can see at all: a question pointed at the wrong one comes back empty
30
+ rather than wrong, which reads as the project having no data. That makes it a
31
+ routing fact, so it rides the default `title` (`Mixpanel (us)`, `Mixpanel
32
+ (eu)`, `Mixpanel (in)`) and opens the usage guide — `search_tools` renders a
33
+ connector's title and guide summary and never its description.
28
34
  OAuth is the recommended default and keeps each connector instance's flow and
29
35
  tokens in its connector-scoped storage. Mixpanel service accounts are also
30
36
  supported with an explicit header override:
@@ -49,18 +55,17 @@ documented create, update, edit, merge, dismiss, duplicate, and delete tools as
49
55
  writes. An unfamiliar tool the downstream leaves unannotated fails closed onto
50
56
  `call_destructive_tool` until a Connecta release reviews it.
51
57
 
52
- That classification is **fill-in only**, and unconditionally so: it supplies
53
- the annotations Mixpanel leaves unset and contradicts an explicit downstream
54
- annotation in neither direction. A tool on the read allowlist arriving with
58
+ That classification fills in downstream silence and otherwise preserves
59
+ explicit annotations. A tool on the read allowlist arriving with
55
60
  `destructiveHint: true` or `readOnlyHint: false` keeps exactly what the
56
61
  downstream said and stays behind `call_destructive_tool`. A tool on neither
57
62
  maintained list arriving with `readOnlyHint: true` keeps that too, and stays
58
63
  callable from `execute_code`. Both are the downstream telling you this
59
64
  release's allowlist is stale, and on a name no release has reviewed its word is
60
- the only evidence there is. The one classification that still outranks the
61
- downstream is a name this release reviewed and filed destructive: a
62
- `Delete-Dashboard` claiming `readOnlyHint: true` is a downstream bug rather
63
- than news, and stays on the approval path. Maintained writes that only create
65
+ the only evidence there is. One narrow fail-closed exception applies to a name
66
+ this release reviewed and filed destructive: a `Delete-Dashboard` claiming
67
+ `readOnlyHint: true` is a downstream bug rather than news, and stays on the
68
+ approval path. Maintained writes that only create
64
69
  something new (`Create-Dashboard`, `Create-Cohort`, `Create-Metric`, and the
65
70
  rest) leave `destructiveHint` unset; `readOnlyHint: false` already routes them
66
71
  through the destructive path, and asserting destruction only inflates the
@@ -69,9 +74,36 @@ approval copy the host shows a human.
69
74
  Experiments and Feature Flags — 15 of the 63 classified tools — are Mixpanel
70
75
  beta surfaces. Expect their names and schemas to move faster than the rest.
71
76
 
72
- The connection also declares a per-runtime call-admission budget matching
73
- Mixpanel's documented 600 requests per hour — a best-effort approximation of
74
- the per-user limit, not an enforcement of it. Each runtime keeps its own
75
- counter, so N Worker isolates or Node processes serving one deployment can each
76
- admit up to 600. Discovery traffic is outside connector call admission and
77
- still needs restrained use.
77
+ ## Rate limits
78
+
79
+ Mixpanel meters its MCP server **per user per hour**, shared with everything
80
+ else that credential does. Connecta's counter is per runtime, not per user, and
81
+ the two cannot be reconciled in either direction: one runtime serving several
82
+ users under-counts, and several Worker isolates or Node processes sharing one
83
+ credential each admit a full budget. A hardcoded ceiling would therefore either
84
+ throttle a healthy deployment or fail to protect a busy one, so this connection
85
+ declares **no call-admission budget by default**. An operator who knows the
86
+ account can supply one explicitly:
87
+
88
+ ```ts
89
+ mixpanel("product_analytics", {
90
+ purpose: "Product and growth decisions for the production app",
91
+ callAdmission: {
92
+ rules: [
93
+ { budget: { kind: "rolling-window", maxCalls: 300, windowMs: 3_600_000 } },
94
+ ],
95
+ },
96
+ });
97
+ ```
98
+
99
+ A budget-only rule needs no queue. If you add `maxConcurrency` you are asking
100
+ for a queue, and the admission controller then requires the rest of the queue
101
+ settings at construction. Discovery traffic is outside connector call admission
102
+ either way and still needs restrained use.
103
+
104
+ ## Conventions
105
+
106
+ This connection is audited against
107
+ [the provider conventions](./provider-conventions.md). Its verdict per
108
+ convention, including every recorded exception, is the Mixpanel section of
109
+ [the provider audit](./provider-audit.md).
@@ -231,3 +231,19 @@ block updates or deletes, no page moves. Those are all real Notion endpoints
231
231
  and all deliberately absent: this is a deliberate tool surface, not a mirror of
232
232
  the API. Anything missing is reachable through a custom `api()` connector
233
233
  beside this one, which remains a first-class path.
234
+
235
+ There is also **no guarded raw-REST escape hatch** — no `notion_api_get`, no
236
+ `notion_api_mutate`. The convention that permits one
237
+ ([H14](./provider-conventions.md#h14--a-named-tool-must-beat-the-escape-hatch-and-the-escape-hatch-splits-by-safety))
238
+ also permits a small provider to have none, provided it says so, and Notion's
239
+ public API is finite and slow-moving enough that a named surface can cover it.
240
+ The usage guide says it too, because an agent that assumes a hatch exists
241
+ spends a search proving it does not: absent from the tool list means absent
242
+ from this connection, not hidden behind a generic call.
243
+
244
+ ## Conventions
245
+
246
+ This connection is audited against
247
+ [the provider conventions](./provider-conventions.md). Its verdict per
248
+ convention, including every recorded exception, is the Notion section of
249
+ [the provider audit](./provider-audit.md).
@@ -1,7 +1,285 @@
1
1
  # Operations
2
2
 
3
- > **Stub.** The old manual was retired in the phase-1 docs restructure. This
4
- > document will be rewritten as an agent-facing guide what the subsystem is
5
- > for, how to work on it, and what it must never do — once the ideas in
6
- > [ethos.md](../ethos.md) settle. The prior text lives in git history as
7
- > `docs/operations.md`.
3
+ Configuring, running, verifying, and upgrading a deployment and the map of
4
+ which suite proves what, which is the part an agent changing this repository
5
+ needs most.
6
+
7
+ ## Running it
8
+
9
+ `createConnecta(config)` returns `{ fetch, registry, close }`. `fetch` takes
10
+ the Workers `(request, env, ctx)` signature; passing `ctx` through is what lets
11
+ connecta hand deferred work — best-effort activity writes — to `ctx.waitUntil`
12
+ instead of losing it when the response returns.
13
+
14
+ An `executor` is required. A deployment without one throws at construction
15
+ rather than serving a smaller surface
16
+ ([#273](https://github.com/zackbart/connecta/issues/273)): Node uses
17
+ `quickJsExecutor()` from `@zackbart/connecta/quickjs`, Workers use
18
+ `new DynamicWorkerExecutor({ loader: env.LOADER })` from
19
+ `@cloudflare/codemode`.
20
+
21
+ There are exactly two deployment shapes.
22
+ [`templates/node/`](../templates/node/) is what `connecta init` copies — the
23
+ one standalone Node project, Docker-ready rather than Docker-only — and
24
+ [`examples/worker/`](../examples/worker/) is the Cloudflare shape. Both ship
25
+ the whole operator feature set; each README walks through its own enablement.
26
+ A third scaffold that is a diff away from either is the shape
27
+ [#344](https://github.com/zackbart/connecta/issues/344) deleted, so do not add
28
+ one.
29
+
30
+ ### The CLI
31
+
32
+ ```sh
33
+ npx @zackbart/connecta init my-deployment
34
+ cd my-deployment && npm install && npm start
35
+ CONNECTA_TOKEN=… npx connecta doctor --url http://localhost:8787
36
+ ```
37
+
38
+ `init` copies the template, pins the generated deployment to the CLI package's
39
+ exact version, restores the template `.gitignore` (npm renames it in a
40
+ tarball), and refuses to merge into an existing path.
41
+
42
+ `doctor` verifies a *running* deployment: `/health` reports ok, `tools/list` is
43
+ exactly the seven prescribed names, and `execute_code` actually runs a trivial
44
+ program. The executor it names is the one the deployment reports on `/health`,
45
+ from that executor's own `name` or its constructor name, sanitized and bounded
46
+ on the way out: `QuickJS` on the Node template, `DynamicWorkerExecutor` on the
47
+ Worker example, and `code executed` when an executor identifies as nothing —
48
+ a checker that asserts a sandbox it never saw is worse than one that says it
49
+ does not know ([#368](https://github.com/zackbart/connecta/issues/368)). It
50
+ refuses to send a bearer token over remote plaintext HTTP, and it
51
+ *reports* catalog drift without failing on it — an unclassified downstream tool
52
+ already fails closed onto `call_destructive_tool`, so drift is a maintainer's
53
+ next task rather than a broken deployment
54
+ ([#343](https://github.com/zackbart/connecta/issues/343)).
55
+
56
+ ### Configuration
57
+
58
+ Structural seams stay top-level; tuning is grouped by subsystem. Every group is
59
+ optional.
60
+
61
+ | Option | Default | What it does |
62
+ | --- | --- | --- |
63
+ | `connectors` | — (required) | the connector set ([connectors](./connectors.md)) |
64
+ | `executor` | — (required) | the sandbox `execute_code` runs in ([code mode](./code-mode.md#what-an-executor-must-implement)) |
65
+ | `auth?` | none ⇒ open (dev only) | one `InboundAuth` or an array; bearer providers are checked before Clerk ([inbound auth](./auth.md)) |
66
+ | `storage?` | `memoryStorage()` | the one state seam ([storage](./storage-and-credentials.md)) |
67
+ | `publicUrl?` | per-request origin | public base URL; an HTTPS value also redirects inbound HTTP |
68
+ | `logger?` | `console`, prefixed `[connecta]` | `{ debug, info, warn, error }` |
69
+ | `branding?` | neutral Connecta defaults | operator-page and OAuth result-page labels and marks |
70
+ | `serverInfo?` | `connecta` / package version | `{ name, version, title?, websiteUrl?, icons? }` per the MCP icons spec |
71
+ | `deploymentInfo?` | unset | arbitrary metadata exposed by `/health` |
72
+ | `activity?` | unset | `{ store, readGate?, deploymentId? }` — payload-free activity storage, an optional operator-read gate, and a stable event label |
73
+ | `credentials.encryptionKey?` | unset | base64 32-byte AES key for the connector vault. Without it, connectors declaring `credential` warn and their slots stay unmanageable |
74
+ | `accessTokens?` | unset | `{ maxActive? }` (default 100) for operator-issued MCP bearer tokens. Requires a Clerk provider, or construction throws ([access tokens](./auth.md#operator-issued-access-tokens)) |
75
+ | `discovery.concurrency?` | 4 | connector catalogs/status probes in flight at once |
76
+ | `discovery.catalogTtlSeconds?` | 300 | fresh TTL for cached tool lists |
77
+ | `discovery.persistCatalog?` | true | persist serializable catalogs as a manifest plus revision-addressed chunks |
78
+ | `discovery.staleCatalogSeconds?` | 3600 | how long an expired catalog stays usable as a failure fallback |
79
+ | `discovery.probeTimeoutMs?` | 30_000 | per-connector deadline for catalog fan-out; a timed-out connector degrades alone. Not a tool-call deadline |
80
+ | `calls.defaultTimeoutMs?` | **unset (opt-in)** | deadline for calls that pass no `timeoutMs`. Bounds one attempt, so retries can still extend total duration |
81
+ | `calls.maxResultBytes?` | 50_000 | inline result cap before truncation and `get_result` paging; a connector may override it. Invalid values warn and fall back |
82
+ | `execute.maxEmittedBytes?` | 4_000_000 | aggregate `connecta.emit` bytes per run — a transport bound, not a context bound |
83
+ | `execute.maxEmittedBlocks?` | 32 | content blocks `connecta.emit` accepts per run |
84
+ | `admission.requests?` | 16 active / 32 queued / 5 s / 1 s | global FIFO `/mcp` capacity, taken before auth ([request admission](./request-admission.md)) |
85
+ | `admission.code?` | 2 active / 8 queued / 5 s / 1 s | fallback pool for an executor that owns no `acquire()`; ignored with a warning when it does |
86
+
87
+ Options removed in earlier releases throw with their migration named rather
88
+ than falling back to a default: `toolkits`
89
+ ([#178](https://github.com/zackbart/connecta/issues/178)), `credentials.health`
90
+ ([#179](https://github.com/zackbart/connecta/issues/179)), `surface` and
91
+ `calls.maxBatchResultBytes`
92
+ ([#273](https://github.com/zackbart/connecta/issues/273)), and the flat v0.6
93
+ paths. Silently ignoring a removed option is how a deployment ends up running a
94
+ policy its config file says it has.
95
+
96
+ ### Deployment as a release unit
97
+
98
+ Treat the package and each running instance as separate release units:
99
+
100
+ ```
101
+ @zackbart/connecta release
102
+ ↓ exact version
103
+ deployment repository
104
+ src/index.ts connector and auth configuration
105
+ package-lock.json reproducible package graph
106
+ wrangler.jsonc (Worker) domain, bindings
107
+ migrations/ (Worker) deployment-owned D1 schema history
108
+ ```
109
+
110
+ An upgrade is an intentional dependency change followed by a normal build.
111
+ Instances must not share KV namespaces, D1 databases, secrets, or encryption
112
+ keys. Keeping deployment configuration private is sensible even though this
113
+ package is public.
114
+
115
+ ## Verification
116
+
117
+ `npm run check` must pass before anything is claimed done. In order:
118
+
119
+ | Script | What it gates |
120
+ | --- | --- |
121
+ | `check:docs` | local Markdown targets and fragments, guide and ethos size caps, duplicate heading anchors, a resurrected `docs/`, stale manual references |
122
+ | `check:operator-ui` | the committed browser bundle matches its source, byte for byte |
123
+ | `check:lint` | Oxlint's correctness category only — style is authored, not enforced |
124
+ | `check:unused` | Knip's unused-export and dependency gate |
125
+ | `typecheck` | `tsc --noEmit` for the package and the separate DOM-lib browser project |
126
+ | `test` | both vitest projects |
127
+ | `build` | the operator bundle, then `tsc -p tsconfig.build.json` into `dist/` |
128
+ | `check:examples` | the Node template and the Worker example typecheck against the built package |
129
+
130
+ `npm run release:check` adds `check:security` (`npm audit --omit=dev
131
+ --audit-level=moderate`) and `check:package`, and is what CI runs on every push
132
+ and pull request. `check:package` packs the tarball, asserts the required files
133
+ are in it and that no platform-specific implementation or unshippable path
134
+ leaked in, derives the shipped guide list from which guides still carry a stub
135
+ marker, checks that every packed doc's `documentation/` link resolves to
136
+ something the tarball carries, and then runs `connecta init` and builds and
137
+ runs the generated deployment's own container.
138
+
139
+ Two more runners are deliberately outside `check`:
140
+
141
+ - `npm run test:browser` — Playwright against a real headless Chromium
142
+ (`npm run test:browser:install` once). It covers the embedded bundle without
143
+ adding a browser download to both CI Node-version jobs.
144
+ - `npm run drift:check` — the maintainer-run provider drift check, with local
145
+ provider credentials exported. No credential goes near CI and nothing files
146
+ itself; findings are read by a human and become issues
147
+ ([provider conventions](./provider-conventions.md#the-maintainer-run-drift-check)).
148
+ - `npm run load:admission` — the opt-in capacity matrix and soak
149
+ ([request admission](./request-admission.md#measuring-capacity)).
150
+
151
+ Releases: `npm run release:check`, tag `v<version>` matching `package.json`
152
+ exactly (the publish workflow verifies this and fails otherwise), and
153
+ publishing fires on GitHub **Release publication**, not on the tag push.
154
+
155
+ ## The test map
156
+
157
+ Suites live in `test/` and run as two vitest projects. `WORKERS_SUITES` holds
158
+ runtime-portable suites; `NODE_ONLY_SUITES` holds Node-bound suites, each with
159
+ a stated reason. The `node` project runs their union; the `workers` project
160
+ re-runs the portable list inside workerd against the Worker example's
161
+ compatibility settings — so a Workers-only regression, the class of bug the
162
+ `CfWorkerJsonSchemaValidator` workaround exists for, fails CI instead of being
163
+ found by hand. `test/suite-partition.test.ts` walks the directory and refuses
164
+ an unclassified, double-classified, stale, or reasonless entry.
165
+
166
+ **New behavior gets a row here.** A suite that is not in this table is either
167
+ new and undocumented or dead, and neither is a state to leave the repository
168
+ in.
169
+
170
+ ### Runtime-portable (`WORKERS_SUITES`)
171
+
172
+ | Suite | Covers |
173
+ | --- | --- |
174
+ | `access-tokens.test.ts` | the `AccessTokenManager` — a one-time secret created, authenticated, renamed, and revoked, bounded names and active count, enumerable storage required, a deployment with no Clerk operator refused — and the Clerk-only routes, down to historical activity still resolving a revoked token's name |
175
+ | `activity.test.ts` | payload-free delivery: a rejected async write attaches to `waitUntil` instead of throwing, approved destructive calls record under their real entry point, result-size friction records without retaining the result, and a hallucinated connector id or invented identity is clamped so the event still cannot carry a payload |
176
+ | `api-connector.test.ts` | `api()` — kind, description, tool defs, dispatch, default args, unknown tools, handler throws, argument validation, and the construction contract |
177
+ | `bearer.test.ts` | constant-time bearer compare, case-insensitive scheme, 401 challenges, and the retired audience options refusing rather than silently unbinding |
178
+ | `branding.test.ts` | branding fallbacks and overrides across the operator shells, OAuth result pages, `/favicon.*`, page titles, and escaping — branding is not an injection vector |
179
+ | `call-admission.test.ts` | connector-scoped per-runtime downstream admission ([call admission](./call-admission.md)): independent partitions, exact rolling-window reset, cancellation that charges no budget, bounded partition state, local-refusal health isolation, one shared limiter across direct and program calls, and payload-free `/health` aggregates |
180
+ | `catalog-drift.test.ts` | `vettedCatalog()`, `detectCatalogDrift()`, and `withVettedCatalog()`; drift on the registry surface and on `/health`; the connector seam projected rather than echoed; and the drift types being public |
181
+ | `catalog.test.ts` | lexical ranking and the compact schema renderer — `const`, `allOf` beside siblings, `$ref`, the depth limit, per-schema caching, and 2020-12 keyword compatibility |
182
+ | `clerk.test.ts` | protected-resource metadata, the browser sign-in config, OAuth and session tokens, cached best-effort activity labels with their caps, the hand-applied `azp` rejection, and the `allowedDomains` allowlist including every lookalike that must not be repaired into a match |
183
+ | `cloudflare-provider.test.ts` | `cloudflare()` construction, tool surface, request building, projections, typed failures, and credential test |
184
+ | `cloudflare-registry.test.ts` | the same provider inside a real deployment: discovery, addressing, and admission through the registry |
185
+ | `code-first-surface.test.ts` | the seven-tool surface itself — an executor required and both runtime configurations named, every removed option and removed top-level tool refused, and `connecta.ui` findable before an agent chooses catalog search |
186
+ | `codemode-compat.test.ts` | the `Executor` seam staying structurally compatible with `@cloudflare/codemode`'s `DynamicWorkerExecutor`, enforced by `tsc` |
187
+ | `config.test.ts` | the grouped `ConnectaConfig` boundary — each group forwarding to its internals, malformed admission bounds failing construction, and one complete migration error for legacy own-properties |
188
+ | `credentials.test.ts` | the pure stored-shape classifier (containment, not equality) and the AES-GCM vault: round-trip, ciphertext bound to its connector id, named field sets, masked metadata, wrong-key rejection, deletion, coexistence with OAuth keys |
189
+ | `d1-activity-example.test.ts` | the Worker example's deployment-owned D1 activity store: actor namespace round-trip, payload-free friction reconstructed from the persisted code, and agreement with the package's friction table |
190
+ | `downstream-oauth.test.ts` | `KvOAuthProvider` round-trips and races, `auth_required` versus `error`, `startAuth`/`finishAuth`, callback refusal equality, bounded diagnostics, and HTML escaping |
191
+ | `errors.test.ts` | `ConnectorCallError` codes, retryable defaults and overrides, `retryAfterMs` round-trip, typed-over-heuristic classification, `AbortError` as a retryable timeout, and framing errors |
192
+ | `execute.test.ts` | the code-mode host bridge: identifier sanitization, MCP-result unwrapping, sandbox provider construction, fail-closed filtering of destructive and unannotated tools, and MCP/code-mode invocation parity |
193
+ | `execute-emit.test.ts` | `connecta.emit` (M1–M10) — block validation, budgets, the provider, delivery after the result envelope on success only, and the defaults |
194
+ | `execute-ui.test.ts` | `connecta.ui` (U1–U9) — validation, multiplicity and budget, the provider, `_meta` delivery, and the Apps shell |
195
+ | `executor-admission.test.ts` | the portable bounded FIFO both pools use: active and queue ceilings, stable retryable overload, queue timeout, cancellation removal, idempotent release, shutdown |
196
+ | `guarded-fetch.test.ts` | the guarded transport — construction, request building, destination confinement, and response handling |
197
+ | `guest-api-contract.test.ts` | the executor-independent half of the guest API contract cases, including the serialized truncation envelope, capped logs, truncation reported as success, and an in-flight host call failing when the run ends |
198
+ | `linear-provider.test.ts` / `linear-registry.test.ts` | the Linear proxy's construction, classification, and guide; then the same connector inside a real deployment |
199
+ | `meta-tools.test.ts` | the registry-backed meta-tools: bounded discovery with page and address maxima, concise and full descriptions, compact and JSON schemas, structured errors, `skills` and connector-guide selection, stored-credential drift, catalog-lookup health accounting, `fields` selection, truncation and `get_result` offset validation and character alignment, per-connector `maxResultBytes`, probe timeouts, and empty-query browse of an unavailable or unconfigured catalog |
200
+ | `mixpanel-provider.test.ts` / `mixpanel-registry.test.ts` | the Mixpanel proxy, then the same connector inside a real deployment |
201
+ | `notion-provider.test.ts` / `notion-registry.test.ts` | Notion's tool surface, request construction, lean projections, both pagination conventions, error mapping, and writes; then the connector in a real deployment |
202
+ | `operator-boundary.test.ts` | the operator row of the decisions table, after every mutation route: authentication material managed without moving a declared structure, and the one honest exception — a credential write making a remote catalog appear, which is discovery arriving, not an operator editing the deployment |
203
+ | `operator-store.test.ts` | `src/operator-ui/app/store.ts` against a fake browser: the Clerk listener, `gate()`, the generation fence, and the request path |
204
+ | `provider-conventions.test.ts` | the conventions a test can hold: hand-written providers refusing schemas they cannot enforce (H5), Cloudflare stating its second pagination convention in the schema (H10), and Notion saying it has no escape hatch (H14) |
205
+ | `registry.test.ts` | construction and id validation, startup convention and result-cap warnings, address resolution, tool-cache TTL, and broken-connector isolation |
206
+ | `remote-mcp.test.ts` | `remoteMcp()` against an in-process server through the `_transportFactory` seam: passthrough, downstream `isError`, Workers-safe output-schema validation, request-scoped client reuse and at-most-once scope close; plus the real transport's manual redirect policy, destination guard, credential containment, and downstream session termination |
207
+ | `remote-mcp-pagination.test.ts` | the `tools/list` cursor chain in both directions — exact cursor handoff, first-wins dedup, a failed later page rejecting rather than returning its prefix, the runaway backstops, the tool-metadata re-prime across pages, and paginated catalogs reaching the discovery path |
208
+ | `request-admission.test.ts` | `/mcp` bounded before auth, the stable 503 and `Retry-After`, health and operator responsiveness under saturation, payload-free counters, queued cancellation, shutdown rejection while active work drains, and the separate fallback code pool |
209
+ | `server.test.ts` | end-to-end `/mcp` (401 → initialize instructions → seven tools → usage skill → `call_tool`), the open routes, Clerk `.well-known` metadata with no network, and an end-to-end code-mode run |
210
+ | `server-route-contracts.test.ts` | the route contracts `server.ts` must keep byte-identical: every built-in answered ahead of connector routes inside the security wrapper, open data-free shells with framing denied, per-route auth and same-origin requirements with exact 401/403/405 bodies, and OAuth `verifyState`-before-`finishAuth` ordering |
211
+ | `startup-warnings.test.ts` | every construction-time `logger.warn` and, as importantly, the conditions that must *not* trigger one: open mode with a credential or OAuth connector, `publicUrl` unset beside OAuth, dropped branding and `uiAuth` URLs, a missing `verifyState`, a credential test-hook mismatch, and an unusable `calls.maxResultBytes` |
212
+ | `stripe-provider.test.ts` / `stripe-registry.test.ts` | the Stripe proxy's endpoint modes and admission, then the connector in a real deployment |
213
+ | `ui.test.ts` | the server shell and `/ui/*` routes and the app's pure state rules from `view.ts` — filtering, page routing and capability states, credential management, gated `/ui/data` with broken-connector isolation, and the URL safety gates |
214
+ | `validate.test.ts` | `validateToolInput()` — a returned (not thrown) `invalid_args` naming the path, `additionalProperties: false` enforcement, per-schema validator caching, and an unusable schema passed through with one warning |
215
+
216
+ ### Node-bound (`NODE_ONLY_SUITES`)
217
+
218
+ Each entry carries its reason in `vitest.config.ts`; the reason is the
219
+ justification for *not* re-running it in workerd, so "it was easier" is not one.
220
+
221
+ | Suite | Covers | Why Node |
222
+ | --- | --- | --- |
223
+ | `deployment-shapes.test.ts` | the Worker as the only example, one Node template that is also its own container, the same source running locally and in the container, the full operator surface in both, a template that cannot start on its own `.env.example`, a Worker README naming every optional peer its entrypoint imports, and the initializer's `.gitignore` staying in step | walks the template and example trees with Node filesystem APIs |
224
+ | `doc-links.test.ts` | the documentation checker itself — local file and fragment resolution, duplicate heading slugs, fenced-code exclusion, and useful failures | spawns the Node checker against filesystem fixtures |
225
+ | `doctor-cli.test.ts` | `connecta doctor`'s executor line end to end — the sandbox the deployment reports is the one named, an unidentifiable executor gets an executor-neutral line, and a hostile name is bounded and stripped before it reaches a terminal | spawns the CLI against a Node HTTP deployment over real sockets |
226
+ | `drift-check.test.ts` | the maintainer drift checker — recorded touched endpoints, a quiet revision bump, clear failures for an unavailable spec/manifest/credential, `$ref` traversal, and one well-formed row per endpoint | spawns the Node checker against filesystem fixtures |
227
+ | `file-storage.test.ts` | `fileStorage()` across instances, logical TTL plus physical pruning without clobbering a newer value, and corrupt-file quarantine | exercises the Node filesystem storage adapter |
228
+ | `guest-api-contract-quickjs.test.ts` | the shared guest-contract cases on the real QuickJS executor | runs the contract cases on the Node QuickJS executor |
229
+ | `node.test.ts` | the `listen()` adapter propagating an HTTP client disconnect through the Web `Request` and the MCP handler into a program's connector call, releasing both admission permits | exercises the Node HTTP adapter over real TCP sockets |
230
+ | `package-surface.test.ts` | the published boundary — built output shipped, only generic factories, platform storage kept in examples, Clerk and QuickJS behind optional subpaths, every provider independently importable, and the Cloudflare provider free of bare specifiers | walks the package tree with Node filesystem APIs |
231
+ | `purity.test.ts` | the import-graph guardrail ([architecture](./architecture.md#import-graph-purity)) — the core stays Workers-clean | walks the source import graph with Node filesystem APIs |
232
+ | `quickjs-child-entry.test.ts` | a missing QuickJS child entry failing before `fork()`, with the expected path and the bundler-externalization constraint | mocks Node child-process and filesystem APIs |
233
+ | `quickjs-child-stderr.test.ts` | abnormal child exits retaining only an 8 KiB stderr tail, included in the parent-side diagnostic | mocks Node child-process streams |
234
+ | `quickjs-executor.test.ts` | the child-process sandbox — code normalization, lazy namespace proxies, bounded IPC, separate guest-CPU and wall budgets, saturation, cancellation and shutdown, crash and OOM recovery, host-call hangs, stalled-promise detection | runs the Node QuickJS child-process executor |
235
+ | `quickjs-log-limits.test.ts` | bounded `console.*` capture — per-entry cut, cumulative character and transport budgets, escape-heavy floods preserving the guest result | runs the Node QuickJS child-process executor |
236
+ | `suite-partition.test.ts` | this partition, including itself: every `*.test.ts` in exactly one list, stale entries and empty reasons refused | walks the test directory to guard the partition |
237
+ | `template-file-activity.test.ts` | the Node template's own activity store — persistence across restart, torn-line repair, newest-first paging, and compaction past the slack window | runs it against real files |
238
+ | `version.test.ts` | `CONNECTA_VERSION` matching `package.json` | reads `package.json` with Node filesystem APIs |
239
+
240
+ ### Outside `npm run check`
241
+
242
+ | Suite | Covers |
243
+ | --- | --- |
244
+ | `browser/operator-ui.spec.ts` | the operator wiring in a real browser: the shell staying open until authentication, credential and access-token and OAuth flows end to end, drift shown without naming a tool, and every failure and empty state |
245
+ | `browser/program-ui.spec.ts` | the Apps shell in a real browser: a bound view merging fixed and declared arguments and correlating concurrent reads, and the one-string payload receiving no read bridge ([program UI read calls](./program-ui-read-calls.md)) |
246
+
247
+ **The `_transportFactory` seam.** `RemoteMcpOptions._transportFactory` is
248
+ internal, not public API: when set, `remoteMcp()` uses that `Transport` instead
249
+ of building an HTTP one. Tests link an in-memory transport to an in-process MCP
250
+ server, so remote-MCP behavior is exercised without a network or a real OAuth
251
+ server. Two consequences worth knowing before you use it: an in-memory
252
+ transport has no session semantics, so anything about `Mcp-Session-Id` needs
253
+ the real HTTP transport, and anything about redirects or destination
254
+ confinement does too.
255
+
256
+ ## Troubleshooting
257
+
258
+ - **MCP clients cache the tool list.** After adding a connector or completing a
259
+ downstream OAuth flow, restart the client. It will not re-list on its own.
260
+ Connecta declares a one-hour private `tools/list` cache hint, which is a
261
+ ceiling on how long a well-behaved client may wait, not a promise it will.
262
+ - **`auth_required` that never clears.** Confirm `publicUrl` is set and
263
+ `GET <publicUrl>/oauth/callback/<connectorId>` is reachable from a browser,
264
+ and that storage is durable rather than `memoryStorage()` across restarts.
265
+ Then `authorize_connector` to restart the flow; `force: true` wipes stored
266
+ credentials for a clean retry.
267
+ - **A connector with no `verifyState` refuses every callback.** That is the
268
+ designed behavior, not a bug: handing an unverified code to `finishAuth` is
269
+ the vulnerability. The startup warning names the connector.
270
+ - **401 loops from a client that cannot discover auth.** The client must reach
271
+ the open `/.well-known/oauth-protected-resource` (and the `/mcp` variant);
272
+ confirm CORS and the Clerk keys, and that DCR is enabled on the Clerk
273
+ instance.
274
+ - **No sessions and no server push, by design.** The transport is stateless.
275
+ Scope resolves per request, which is also where the MCP spec has arrived.
276
+ - **A tool that should be callable from a program is not.** Only tools
277
+ explicitly annotated `readOnlyHint: true` are admissible inside the sandbox.
278
+ A missing, false, or contradictory annotation fails closed, every time, and
279
+ the recovery is `call_destructive_tool` — not a wider sandbox.
280
+ - **`check:operator-ui` fails after a UI change.** Run
281
+ `npm run build:operator-ui` and commit the regenerated
282
+ `src/operator-ui/generated.ts` ([operator UI](./operator-ui.md#why-the-bundle-is-committed)).
283
+ - **Upgrade the MCP SDK and Zod together**, then run `npm run release:check`.
284
+ The SDK packages are pinned exactly and paired with Zod 4 to keep the
285
+ optional code-mode peer graph valid.