@zackbart/connecta 0.15.1 → 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 +466 -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 +0 -1
  78. package/dist/providers/cloudflare.js +250 -276
  79. package/dist/providers/linear.d.ts +26 -5
  80. package/dist/providers/linear.js +32 -61
  81. package/dist/providers/mixpanel.d.ts +35 -4
  82. package/dist/providers/mixpanel.js +53 -79
  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 -60
  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 +58 -8
  129. package/documentation/code-mode.md +12 -12
  130. package/documentation/connector-guides.md +172 -5
  131. package/documentation/connectors.md +89 -0
  132. package/documentation/linear.md +20 -2
  133. package/documentation/mcp-ui-design.md +2 -2
  134. package/documentation/mixpanel.md +42 -9
  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 +15 -0
  143. package/ethos.md +13 -8
  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 -3999
  310. package/src/providers/linear.ts +0 -302
  311. package/src/providers/mixpanel.ts +0 -229
  312. package/src/providers/notion.ts +0 -1879
  313. package/src/providers/stripe.ts +0 -307
  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
@@ -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:
@@ -68,9 +74,36 @@ approval copy the host shows a human.
68
74
  Experiments and Feature Flags — 15 of the 63 classified tools — are Mixpanel
69
75
  beta surfaces. Expect their names and schemas to move faster than the rest.
70
76
 
71
- The connection also declares a per-runtime call-admission budget matching
72
- Mixpanel's documented 600 requests per hour — a best-effort approximation of
73
- the per-user limit, not an enforcement of it. Each runtime keeps its own
74
- counter, so N Worker isolates or Node processes serving one deployment can each
75
- admit up to 600. Discovery traffic is outside connector call admission and
76
- 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.
@@ -1,7 +1,122 @@
1
1
  # Operator UI
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/operator-ui.md`.
3
+ The browser surface a human uses to see what a deployment exposes and to manage
4
+ the authentication material behind it. It is a small Preact app compiled by the
5
+ repository's own esbuild step and inlined into a data-free server shell.
6
+
7
+ Read [`ethos.md`](../ethos.md) first. The boundary this subsystem lives inside
8
+ is the operator row in its decisions table: **operator routes may manage
9
+ authentication material for capabilities declared in deployment configuration,
10
+ and may not change the connector set, the tool catalog or annotations, requested
11
+ OAuth scopes, admission policy, authorization rules, or caller tool scope.**
12
+ `test/operator-boundary.test.ts` proves it after every mutation route.
13
+
14
+ Both deployment shapes ship the whole feature set behind it, because pages for
15
+ things a deployment cannot do are worse than no pages
16
+ ([#345](https://github.com/zackbart/connecta/issues/345)). The
17
+ [Node template](../templates/node/) carries sign-in, vault, tokens, and
18
+ activity as commented blocks in `src/index.ts` — plus a deployment-owned
19
+ `src/file-activity.ts` that is compiled rather than commented — and the
20
+ [Worker example](../examples/worker/) wires the first three and comments the
21
+ fourth, which needs a D1 database. Each README walks through its own
22
+ enablement.
23
+
24
+ The vault is the one whose page needs a second thing. `/credentials` lists
25
+ connector credential slots, so `credentialManagement` stays `no_slots` — and
26
+ the nav entry stays hidden — until a connector declares `credential`, however
27
+ well-configured the vault is. Neither shape's shipped connectors need a secret,
28
+ so both carry the slot's shape as a comment beside a connector and say so in
29
+ their walkthrough, which is the honest version of the same page count.
30
+
31
+ ## The shape
32
+
33
+ | Piece | What it owns |
34
+ | --- | --- |
35
+ | `src/ui.ts` | The served HTML: branding, gated URLs, CSP-nonced script tags, the four page titles, and `buildUiData` — the `/ui/data` payload. |
36
+ | `src/operator-ui/model.ts` | The transport types both sides share, plus connector filtering. |
37
+ | `src/operator-ui/view.ts` | The app's state shape and every pure rule over it. No DOM, so `test/ui.test.ts` calls it directly. |
38
+ | `src/operator-ui/app/` | The browser app: `store.ts` (state and every request), `main.tsx` (shell, gate, router), and one component file per page. |
39
+ | `src/operator-ui/browser.css` | One stylesheet, inlined into the shell. |
40
+ | `src/operator-ui/generated.ts` | The build output: the bundle and the stylesheet as two exported strings. |
41
+
42
+ The server renders a mount point, not a page. Branding, the Clerk loader, and
43
+ every operator-configured URL stay in `src/ui.ts`, where they are gated before
44
+ they can become an attribute; the bundle renders everything that has a state.
45
+ Two roots share one store: `#operatorNav` and `#operatorContent`.
46
+
47
+ ## Rules that are not obvious
48
+
49
+ - **No operator data in the shell.** Every page serves the same markup. Connector,
50
+ credential, token, and activity data arrives only through the authenticated
51
+ `/ui/*` APIs, and the shell is identical whether or not a caller is signed in.
52
+ - **One store, one identity.** `store.ts` is the only file that touches `fetch`,
53
+ `localStorage`, or Clerk. Every request carries the current session's token,
54
+ and every response is dropped unless the identity that asked for it is still
55
+ the one on screen. `resetIdentity` replaces all identity-scoped state at once
56
+ and bumps a generation that work already in flight compares itself against.
57
+ - **Escaping is structural.** Components return elements; nothing builds HTML
58
+ from strings. A value that could be a URL passes `safeHttpHref` before it may
59
+ become an `href`, mirroring the server-side gate in `src/ui.ts`.
60
+ - **Secrets are shown once.** A created access token lives in state only, and
61
+ leaving the page — by navigation or by `pagehide`, which covers the
62
+ back-forward cache — unmounts it.
63
+ - **Every flow has four states.** Loading, error, empty, and success, with no
64
+ dead end: a failed save keeps the form and its typed value, a failed list
65
+ offers a retry, and an empty collection says what would fill it. A mutation
66
+ that fails is still a resolved promise — `mutate` lands the failure in state
67
+ rather than rejecting — so a caller that clears a form must clear it on a
68
+ confirmed success, never on resolution. `createAccessToken` returns that
69
+ answer as a boolean for exactly this reason.
70
+ - **Drift is counts, and absence is its own answer.** The connector card reads
71
+ `catalogDrift` ([#343](https://github.com/zackbart/connecta/issues/343)) as
72
+ four category counts and a timestamp. There is no drill-down, because a tool
73
+ name or a schema here would make an operator page the payload surface the
74
+ drift model refuses to be. A connector with no report renders as *not
75
+ observed*, never as clean: this runtime having seen no refresh is not the
76
+ same claim as a refresh having found nothing.
77
+
78
+ ## Working on it
79
+
80
+ Source changes require a rebuild: `npm run build:operator-ui` regenerates
81
+ `src/operator-ui/generated.ts`, and `npm run check:operator-ui` fails when the
82
+ committed artifact is stale. Both run through the same esbuild call, so the
83
+ check compares byte for byte.
84
+
85
+ Tests split along the DOM line, because `test/ui.test.ts` runs in workerd as
86
+ well as Node and there is no DOM in either:
87
+
88
+ - `test/ui.test.ts` — the server shell, the `/ui/*` routes, and the app's pure
89
+ state rules from `view.ts`.
90
+ - `test/operator-store.test.ts` — `store.ts` itself, against a fake browser: the
91
+ Clerk listener, `gate()`, the generation fence, and the request path. The
92
+ rules in `view.ts` prove what an identity change *erases*; this suite proves
93
+ something calls them when the identity actually changes. It typechecks in the
94
+ DOM-lib program (`tsconfig.operator-ui.json`) because it imports the store.
95
+ - `test/browser/operator-ui.spec.ts` — the wiring, in a real browser:
96
+ credential, token, and OAuth flows end to end, including their failure and
97
+ empty states. Run it with `npm run test:browser` (`npm run test:browser:install`
98
+ once, for Chromium). It is not part of `npm run check`.
99
+
100
+ ## Why the bundle is committed
101
+
102
+ `src/operator-ui/generated.ts` is generated and checked in. The alternatives
103
+ were considered and lost:
104
+
105
+ - **Build during `prepack`.** The artifact would still have to exist before
106
+ `tsc` runs, so every contributor and every CI job would need the browser build
107
+ before typechecking — and a published tarball would carry a build output
108
+ nobody could diff against its source.
109
+ - **Exclude it from the tarball.** `dist/operator-ui/generated.js` is imported
110
+ by `src/ui.ts`; a deployment that installs the package needs it. Excluding it
111
+ ships a broken import.
112
+
113
+ Committing keeps one prebuilt string that Node and Workers read unchanged, with
114
+ no browser toolchain at install, pack, or deploy time. The cost is a large
115
+ generated diff on UI changes, paid deliberately: the bundle is not minified, so
116
+ what actually reaches an operator's page can be read in review, and
117
+ `check:operator-ui` is what keeps it honest.
118
+
119
+ Preact is a `devDependency` for the same reason. It is inlined into the bundle
120
+ at build time and never appears in a deployment's dependency tree — the package
121
+ has no runtime dependency on it, and `test/package-surface.test.ts` keeps the
122
+ published surface that way.