@zackbart/connecta 0.15.1 → 0.16.1

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 (335) hide show
  1. package/AGENTS.md +40 -20
  2. package/CHANGELOG.md +653 -0
  3. package/README.md +25 -5
  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 +41 -5
  27. package/dist/catalog.d.ts +11 -1
  28. package/dist/catalog.js +20 -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 +30 -3
  42. package/dist/errors.js +1 -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 +261 -299
  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 +92 -10
  129. package/documentation/code-first-exploration.md +2 -2
  130. package/documentation/code-mode.md +14 -14
  131. package/documentation/connector-guides.md +172 -5
  132. package/documentation/connectors.md +89 -0
  133. package/documentation/linear.md +20 -2
  134. package/documentation/mcp-ui-design.md +2 -2
  135. package/documentation/meta-tools.md +7 -1
  136. package/documentation/mixpanel.md +42 -9
  137. package/documentation/notion.md +23 -2
  138. package/documentation/operations.md +325 -5
  139. package/documentation/operator-ui.md +120 -5
  140. package/documentation/provider-audit.md +160 -0
  141. package/documentation/provider-conventions.md +667 -0
  142. package/documentation/request-admission.md +167 -5
  143. package/documentation/rich-output-design.md +1 -2
  144. package/documentation/stripe.md +15 -0
  145. package/documentation/upgrading.md +454 -0
  146. package/ethos.md +14 -9
  147. package/examples/worker/README.md +102 -12
  148. package/examples/worker/src/index.ts +46 -1
  149. package/examples/worker/wrangler.jsonc +14 -1
  150. package/package.json +10 -6
  151. package/templates/node/.dockerignore +10 -0
  152. package/templates/node/.env.example +40 -3
  153. package/templates/node/AGENTS.md +16 -1
  154. package/templates/node/Dockerfile +49 -0
  155. package/templates/node/README.md +117 -2
  156. package/templates/node/docker-compose.yml +39 -0
  157. package/templates/node/package.json +1 -1
  158. package/templates/node/src/file-activity.ts +182 -0
  159. package/templates/node/src/index.ts +80 -4
  160. package/assets/connecta-clay-hero.png +0 -0
  161. package/dist/access-tokens.d.ts.map +0 -1
  162. package/dist/access-tokens.js.map +0 -1
  163. package/dist/activity.d.ts.map +0 -1
  164. package/dist/activity.js.map +0 -1
  165. package/dist/apps-shell.d.ts.map +0 -1
  166. package/dist/apps-shell.js.map +0 -1
  167. package/dist/auth/bearer.d.ts.map +0 -1
  168. package/dist/auth/bearer.js.map +0 -1
  169. package/dist/auth/clerk.d.ts.map +0 -1
  170. package/dist/auth/clerk.js.map +0 -1
  171. package/dist/auth/downstream-oauth.d.ts.map +0 -1
  172. package/dist/auth/downstream-oauth.js.map +0 -1
  173. package/dist/call-admission.d.ts.map +0 -1
  174. package/dist/call-admission.js.map +0 -1
  175. package/dist/catalog-fingerprint.d.ts.map +0 -1
  176. package/dist/catalog-fingerprint.js.map +0 -1
  177. package/dist/catalog-limits.d.ts.map +0 -1
  178. package/dist/catalog-limits.js.map +0 -1
  179. package/dist/catalog-service.d.ts.map +0 -1
  180. package/dist/catalog-service.js.map +0 -1
  181. package/dist/catalog.d.ts.map +0 -1
  182. package/dist/catalog.js.map +0 -1
  183. package/dist/concurrency.d.ts.map +0 -1
  184. package/dist/concurrency.js.map +0 -1
  185. package/dist/connector-scope.d.ts.map +0 -1
  186. package/dist/connector-scope.js.map +0 -1
  187. package/dist/connectors/api.d.ts.map +0 -1
  188. package/dist/connectors/api.js.map +0 -1
  189. package/dist/connectors/remote-mcp.d.ts.map +0 -1
  190. package/dist/connectors/remote-mcp.js.map +0 -1
  191. package/dist/credentials.d.ts.map +0 -1
  192. package/dist/credentials.js.map +0 -1
  193. package/dist/errors.d.ts.map +0 -1
  194. package/dist/errors.js.map +0 -1
  195. package/dist/execute.d.ts.map +0 -1
  196. package/dist/execute.js.map +0 -1
  197. package/dist/executor-admission.d.ts.map +0 -1
  198. package/dist/executor-admission.js.map +0 -1
  199. package/dist/executor-result.d.ts.map +0 -1
  200. package/dist/executor-result.js.map +0 -1
  201. package/dist/executors/quickjs-child.d.ts.map +0 -1
  202. package/dist/executors/quickjs-child.js.map +0 -1
  203. package/dist/executors/quickjs-protocol.d.ts.map +0 -1
  204. package/dist/executors/quickjs-protocol.js.map +0 -1
  205. package/dist/executors/quickjs-runtime.d.ts.map +0 -1
  206. package/dist/executors/quickjs-runtime.js.map +0 -1
  207. package/dist/executors/quickjs.d.ts.map +0 -1
  208. package/dist/executors/quickjs.js.map +0 -1
  209. package/dist/favicon.d.ts.map +0 -1
  210. package/dist/favicon.js.map +0 -1
  211. package/dist/index.d.ts.map +0 -1
  212. package/dist/index.js.map +0 -1
  213. package/dist/invocation.d.ts.map +0 -1
  214. package/dist/invocation.js.map +0 -1
  215. package/dist/json-schema.d.ts.map +0 -1
  216. package/dist/json-schema.js.map +0 -1
  217. package/dist/mcp-result.d.ts.map +0 -1
  218. package/dist/mcp-result.js.map +0 -1
  219. package/dist/meta-tools.d.ts.map +0 -1
  220. package/dist/meta-tools.js.map +0 -1
  221. package/dist/node.d.ts.map +0 -1
  222. package/dist/node.js.map +0 -1
  223. package/dist/operator-ui/generated.d.ts.map +0 -1
  224. package/dist/operator-ui/generated.js.map +0 -1
  225. package/dist/operator-ui/model.d.ts.map +0 -1
  226. package/dist/operator-ui/model.js.map +0 -1
  227. package/dist/providers/cloudflare.d.ts.map +0 -1
  228. package/dist/providers/cloudflare.js.map +0 -1
  229. package/dist/providers/linear.d.ts.map +0 -1
  230. package/dist/providers/linear.js.map +0 -1
  231. package/dist/providers/mixpanel.d.ts.map +0 -1
  232. package/dist/providers/mixpanel.js.map +0 -1
  233. package/dist/providers/notion.d.ts.map +0 -1
  234. package/dist/providers/notion.js.map +0 -1
  235. package/dist/providers/stripe.d.ts.map +0 -1
  236. package/dist/providers/stripe.js.map +0 -1
  237. package/dist/registry.d.ts.map +0 -1
  238. package/dist/registry.js.map +0 -1
  239. package/dist/retired-toolkits.d.ts.map +0 -1
  240. package/dist/retired-toolkits.js.map +0 -1
  241. package/dist/routes/access-tokens.d.ts.map +0 -1
  242. package/dist/routes/access-tokens.js.map +0 -1
  243. package/dist/routes/activity.d.ts.map +0 -1
  244. package/dist/routes/activity.js.map +0 -1
  245. package/dist/routes/credentials.d.ts.map +0 -1
  246. package/dist/routes/credentials.js.map +0 -1
  247. package/dist/routes/mcp.d.ts.map +0 -1
  248. package/dist/routes/mcp.js.map +0 -1
  249. package/dist/routes/oauth.d.ts.map +0 -1
  250. package/dist/routes/oauth.js.map +0 -1
  251. package/dist/routes/shared.d.ts.map +0 -1
  252. package/dist/routes/shared.js.map +0 -1
  253. package/dist/routes/ui.d.ts.map +0 -1
  254. package/dist/routes/ui.js.map +0 -1
  255. package/dist/server.d.ts.map +0 -1
  256. package/dist/server.js.map +0 -1
  257. package/dist/skills.d.ts.map +0 -1
  258. package/dist/skills.js.map +0 -1
  259. package/dist/storage/file.d.ts.map +0 -1
  260. package/dist/storage/file.js.map +0 -1
  261. package/dist/storage/memory.d.ts.map +0 -1
  262. package/dist/storage/memory.js.map +0 -1
  263. package/dist/timeout.d.ts.map +0 -1
  264. package/dist/timeout.js.map +0 -1
  265. package/dist/tool-safety.d.ts.map +0 -1
  266. package/dist/tool-safety.js.map +0 -1
  267. package/dist/types.d.ts.map +0 -1
  268. package/dist/types.js.map +0 -1
  269. package/dist/ui.d.ts.map +0 -1
  270. package/dist/ui.js.map +0 -1
  271. package/dist/validate.d.ts.map +0 -1
  272. package/dist/validate.js.map +0 -1
  273. package/dist/version.d.ts.map +0 -1
  274. package/dist/version.js.map +0 -1
  275. package/examples/node/README.md +0 -52
  276. package/examples/node/src/index.ts +0 -71
  277. package/src/access-tokens.ts +0 -289
  278. package/src/activity.ts +0 -234
  279. package/src/apps-shell.ts +0 -370
  280. package/src/auth/bearer.ts +0 -61
  281. package/src/auth/clerk.ts +0 -553
  282. package/src/auth/downstream-oauth.ts +0 -575
  283. package/src/call-admission.ts +0 -521
  284. package/src/catalog-fingerprint.ts +0 -52
  285. package/src/catalog-limits.ts +0 -11
  286. package/src/catalog-service.ts +0 -1153
  287. package/src/catalog.ts +0 -790
  288. package/src/concurrency.ts +0 -39
  289. package/src/connector-scope.ts +0 -66
  290. package/src/connectors/api.ts +0 -159
  291. package/src/connectors/remote-mcp.ts +0 -1090
  292. package/src/credentials.ts +0 -428
  293. package/src/errors.ts +0 -364
  294. package/src/execute.ts +0 -1406
  295. package/src/executor-admission.ts +0 -328
  296. package/src/executor-result.ts +0 -107
  297. package/src/executors/quickjs-child.ts +0 -172
  298. package/src/executors/quickjs-protocol.ts +0 -99
  299. package/src/executors/quickjs-runtime.ts +0 -470
  300. package/src/executors/quickjs.ts +0 -776
  301. package/src/favicon.ts +0 -53
  302. package/src/index.ts +0 -666
  303. package/src/invocation.ts +0 -655
  304. package/src/json-schema.ts +0 -11
  305. package/src/mcp-result.ts +0 -43
  306. package/src/meta-tools.ts +0 -1607
  307. package/src/node.ts +0 -206
  308. package/src/operator-ui/browser.css +0 -423
  309. package/src/operator-ui/browser.ts +0 -1281
  310. package/src/operator-ui/generated.ts +0 -4
  311. package/src/operator-ui/model.ts +0 -111
  312. package/src/providers/cloudflare.ts +0 -3999
  313. package/src/providers/linear.ts +0 -302
  314. package/src/providers/mixpanel.ts +0 -229
  315. package/src/providers/notion.ts +0 -1879
  316. package/src/providers/stripe.ts +0 -307
  317. package/src/registry.ts +0 -1085
  318. package/src/retired-toolkits.ts +0 -14
  319. package/src/routes/access-tokens.ts +0 -115
  320. package/src/routes/activity.ts +0 -215
  321. package/src/routes/credentials.ts +0 -254
  322. package/src/routes/mcp.ts +0 -429
  323. package/src/routes/oauth.ts +0 -322
  324. package/src/routes/shared.ts +0 -247
  325. package/src/routes/ui.ts +0 -152
  326. package/src/server.ts +0 -195
  327. package/src/skills.ts +0 -271
  328. package/src/storage/file.ts +0 -123
  329. package/src/storage/memory.ts +0 -44
  330. package/src/timeout.ts +0 -84
  331. package/src/tool-safety.ts +0 -15
  332. package/src/types.ts +0 -523
  333. package/src/ui.ts +0 -744
  334. package/src/validate.ts +0 -387
  335. package/src/version.ts +0 -7
@@ -1,7 +1,327 @@
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
+ Both executor packages are optional peers: they never install with connecta,
22
+ and a deployment installs the one its runtime needs. The manifest publishes the
23
+ range each release supports — `^0.32.0` for `quickjs-emscripten`,
24
+ `^0.4.4 || ^0.5.0` for `@cloudflare/codemode` — so npm answers the version
25
+ question at install time rather than leaving a deployment to discover the skew
26
+ when a program runs ([#376](https://github.com/zackbart/connecta/issues/376)).
27
+ The version this repository tests against is one the published range admits,
28
+ and `test/package-surface.test.ts` fails if the two ever drift apart.
29
+
30
+ There are exactly two deployment shapes.
31
+ [`templates/node/`](../templates/node/) is what `connecta init` copies — the
32
+ one standalone Node project, Docker-ready rather than Docker-only — and
33
+ [`examples/worker/`](../examples/worker/) is the Cloudflare shape. Both ship
34
+ the whole operator feature set; each README walks through its own enablement.
35
+ A third scaffold that is a diff away from either is the shape
36
+ [#344](https://github.com/zackbart/connecta/issues/344) deleted, so do not add
37
+ one.
38
+
39
+ ### The CLI
40
+
41
+ ```sh
42
+ npx @zackbart/connecta init my-deployment
43
+ cd my-deployment && npm install && npm start
44
+ CONNECTA_TOKEN=… npx connecta doctor --url http://localhost:8787
45
+ ```
46
+
47
+ `init` copies the template, pins the generated deployment to the CLI package's
48
+ exact version, restores the template `.gitignore` (npm renames it in a
49
+ tarball), and refuses to merge into an existing path.
50
+
51
+ `doctor` verifies a *running* deployment: `/health` reports ok, `tools/list` is
52
+ exactly the seven prescribed names, and `execute_code` actually runs a trivial
53
+ program. The executor it names is the one the deployment reports on `/health`,
54
+ from that executor's own `name` or its constructor name, sanitized and bounded
55
+ on the way out: `QuickJS` on the Node template, `DynamicWorkerExecutor` on the
56
+ Worker example, and `code executed` when an executor identifies as nothing —
57
+ a checker that asserts a sandbox it never saw is worse than one that says it
58
+ does not know ([#368](https://github.com/zackbart/connecta/issues/368)). It
59
+ refuses to send a bearer token over remote plaintext HTTP, and it
60
+ *reports* catalog drift without failing on it — an unclassified downstream tool
61
+ already fails closed onto `call_destructive_tool`, so drift is a maintainer's
62
+ next task rather than a broken deployment
63
+ ([#343](https://github.com/zackbart/connecta/issues/343)).
64
+
65
+ ### Configuration
66
+
67
+ Structural seams stay top-level; tuning is grouped by subsystem. Every group is
68
+ optional.
69
+
70
+ | Option | Default | What it does |
71
+ | --- | --- | --- |
72
+ | `connectors` | — (required) | the connector set ([connectors](./connectors.md)) |
73
+ | `executor` | — (required) | the sandbox `execute_code` runs in ([code mode](./code-mode.md#what-an-executor-must-implement)) |
74
+ | `auth?` | none ⇒ open (dev only) | one `InboundAuth` or an array; bearer providers are checked before Clerk ([inbound auth](./auth.md)) |
75
+ | `storage?` | `memoryStorage()` | the one state seam ([storage](./storage-and-credentials.md)) |
76
+ | `publicUrl?` | per-request origin | public base URL; an HTTPS value also redirects inbound HTTP |
77
+ | `logger?` | `console`, prefixed `[connecta]` | `{ debug, info, warn, error }` |
78
+ | `branding?` | neutral Connecta defaults | operator-page and OAuth result-page labels and marks |
79
+ | `serverInfo?` | `connecta` / package version | `{ name, version, title?, websiteUrl?, icons? }` per the MCP icons spec |
80
+ | `deploymentInfo?` | unset | arbitrary metadata exposed by `/health` |
81
+ | `activity?` | unset | `{ store, readGate?, deploymentId? }` — payload-free activity storage, an optional operator-read gate, and a stable event label |
82
+ | `credentials.encryptionKey?` | unset | base64 32-byte AES key for the connector vault. Without it, connectors declaring `credential` warn and their slots stay unmanageable |
83
+ | `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)) |
84
+ | `discovery.concurrency?` | 4 | connector catalogs/status probes in flight at once |
85
+ | `discovery.catalogTtlSeconds?` | 300 | fresh TTL for cached tool lists |
86
+ | `discovery.persistCatalog?` | true | persist serializable catalogs as a manifest plus revision-addressed chunks |
87
+ | `discovery.staleCatalogSeconds?` | 3600 | how long an expired catalog stays usable as a failure fallback |
88
+ | `discovery.probeTimeoutMs?` | 30_000 | per-connector deadline for catalog fan-out; a timed-out connector degrades alone. Not a tool-call deadline |
89
+ | `calls.defaultTimeoutMs?` | **unset (opt-in)** | deadline for calls that pass no `timeoutMs`. Bounds one attempt, so retries can still extend total duration |
90
+ | `calls.maxResultBytes?` | 50_000 | inline result cap before truncation and `get_result` paging; a connector may override it. Invalid values warn and fall back |
91
+ | `execute.maxEmittedBytes?` | 4_000_000 | aggregate `connecta.emit` bytes per run — a transport bound, not a context bound |
92
+ | `execute.maxEmittedBlocks?` | 32 | content blocks `connecta.emit` accepts per run |
93
+ | `admission.requests?` | 16 active / 32 queued / 5 s / 1 s | global FIFO `/mcp` capacity, taken before auth ([request admission](./request-admission.md)) |
94
+ | `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 |
95
+
96
+ Options removed in earlier releases throw with their migration named rather
97
+ than falling back to a default: `toolkits`
98
+ ([#178](https://github.com/zackbart/connecta/issues/178)), `credentials.health`
99
+ ([#179](https://github.com/zackbart/connecta/issues/179)), `surface` and
100
+ `calls.maxBatchResultBytes`
101
+ ([#273](https://github.com/zackbart/connecta/issues/273)), and the flat v0.6
102
+ paths. Silently ignoring a removed option is how a deployment ends up running a
103
+ policy its config file says it has.
104
+
105
+ ### Deployment as a release unit
106
+
107
+ Treat the package and each running instance as separate release units:
108
+
109
+ ```
110
+ @zackbart/connecta release
111
+ ↓ exact version
112
+ deployment repository
113
+ src/index.ts connector and auth configuration
114
+ package-lock.json reproducible package graph
115
+ wrangler.jsonc (Worker) domain, bindings
116
+ migrations/ (Worker) deployment-owned D1 schema history
117
+ ```
118
+
119
+ An upgrade is an intentional dependency change followed by a normal build.
120
+ Doing it to a deployment somebody else generated — reading its pin, diffing it
121
+ against the template generation it came from, and crossing the version
122
+ boundaries that break construction — is [upgrading](./upgrading.md), which is
123
+ written for the agent sitting inside that deployment rather than inside this
124
+ repository. Instances must not share KV namespaces, D1 databases, secrets, or
125
+ encryption keys. Keeping deployment configuration private is sensible even
126
+ though this package is public.
127
+
128
+ ## Verification
129
+
130
+ `npm run check` must pass before anything is claimed done. In order:
131
+
132
+ | Script | What it gates |
133
+ | --- | --- |
134
+ | `check:docs` | local Markdown targets and fragments — including the `github.com` and `raw.githubusercontent.com` URLs that point back into this repository — guide and ethos size caps, duplicate heading anchors, a resurrected `docs/`, stale manual references |
135
+ | `check:operator-ui` | the committed browser bundle matches its source, byte for byte |
136
+ | `check:lint` | Oxlint's correctness category only — style is authored, not enforced |
137
+ | `check:unused` | Knip's unused-export and dependency gate |
138
+ | `typecheck` | `tsc --noEmit` for the package and the separate DOM-lib browser project |
139
+ | `test` | both vitest projects |
140
+ | `build` | the operator bundle, then `tsc -p tsconfig.build.json` into `dist/` |
141
+ | `check:examples` | the Node template and the Worker example typecheck against the built package |
142
+
143
+ `npm run release:check` adds `check:security` (`npm audit --omit=dev
144
+ --audit-level=moderate`) and `check:package`, and is what CI runs on every push
145
+ and pull request. `check:package` packs the tarball, asserts the required files
146
+ are in it and that no unshippable path leaked in — including any
147
+ Cloudflare-named connector or storage path (`connectors/cloudflare`,
148
+ `storage/cloudflare`) anywhere in the artifact, `dist/` and `examples/` alike —
149
+ derives the shipped guide list from which guides still carry a stub marker,
150
+ hands the packed path list to `scripts/check-packed-links.mjs`, and then runs
151
+ `connecta init` and builds and runs the generated deployment's own container.
152
+
153
+ That last step enforces the packed-link policy, which is one sentence: **every
154
+ relative link in shipped Markdown must resolve to a path the tarball carries,
155
+ and a target that is repository-only is cited as an absolute
156
+ `https://github.com/zackbart/connecta/blob/main/...` URL** (the
157
+ `raw.githubusercontent.com` form for an image, which is how the README hero
158
+ still renders on npmjs.com). The tarball is built output, not a checkout: it
159
+ carries no `eval/`, `test/`, `scripts/`, or `assets/`, so a relative pointer
160
+ into any of them is a dead end for the reader who installed the package, and
161
+ the fix is never to ship those directories — that would undo the trim of
162
+ [#346](https://github.com/zackbart/connecta/issues/346). A repository URL keeps
163
+ the citation verifiable in both directions: an outside reader can follow it, and
164
+ `check:docs` resolves it back to the checkout and fails when the cited file
165
+ moves ([#378](https://github.com/zackbart/connecta/issues/378)). `CHANGELOG.md`
166
+ is exempt from both gates, because release notes quote the paths that existed
167
+ when they shipped.
168
+
169
+ The Worker example ships in the tarball, its Cloudflare KV and D1 adapters
170
+ included: it is the Workers starting template a consumer copies. That is not a
171
+ hole in the published surface, because nothing under `examples/` appears in the
172
+ `exports` map — every export target resolves into `dist/`, so those adapters
173
+ are reference source rather than an importable subpath. They also clear the
174
+ platform-specific gate above on their names (`cloudflare-kv.ts`,
175
+ `d1-activity.ts`) rather than by exemption: that gate is a blunt pattern over
176
+ the whole artifact, so an example file renamed into `storage/cloudflare` would
177
+ fail the pack even though nothing about the published surface had changed.
178
+
179
+ Two more runners are deliberately outside `check`:
180
+
181
+ - `npm run test:browser` — Playwright against a real headless Chromium
182
+ (`npm run test:browser:install` once). It covers the embedded bundle without
183
+ adding a browser download to both CI Node-version jobs.
184
+ - `npm run drift:check` — the maintainer-run provider drift check, with local
185
+ provider credentials exported. No credential goes near CI and nothing files
186
+ itself; findings are read by a human and become issues
187
+ ([provider conventions](./provider-conventions.md#the-maintainer-run-drift-check)).
188
+ - `npm run load:admission` — the opt-in capacity matrix and soak
189
+ ([request admission](./request-admission.md#measuring-capacity)).
190
+
191
+ Releases: `npm run release:check`, tag `v<version>` matching `package.json`
192
+ exactly (the publish workflow verifies this and fails otherwise), and
193
+ publishing fires on GitHub **Release publication**, not on the tag push.
194
+
195
+ ## The test map
196
+
197
+ Suites live in `test/` and run as two vitest projects. `WORKERS_SUITES` holds
198
+ runtime-portable suites; `NODE_ONLY_SUITES` holds Node-bound suites, each with
199
+ a stated reason. The `node` project runs their union; the `workers` project
200
+ re-runs the portable list inside workerd against the Worker example's
201
+ compatibility settings — so a Workers-only regression, the class of bug the
202
+ `CfWorkerJsonSchemaValidator` workaround exists for, fails CI instead of being
203
+ found by hand. `test/suite-partition.test.ts` walks the directory and refuses
204
+ an unclassified, double-classified, stale, or reasonless entry.
205
+
206
+ **New behavior gets a row here.** A suite that is not in this table is either
207
+ new and undocumented or dead, and neither is a state to leave the repository
208
+ in.
209
+
210
+ ### Runtime-portable (`WORKERS_SUITES`)
211
+
212
+ | Suite | Covers |
213
+ | --- | --- |
214
+ | `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 |
215
+ | `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 |
216
+ | `api-connector.test.ts` | `api()` — kind, description, tool defs, dispatch, default args, unknown tools, handler throws, argument validation, and the construction contract |
217
+ | `bearer.test.ts` | constant-time bearer compare, case-insensitive scheme, 401 challenges, and the retired audience options refusing rather than silently unbinding |
218
+ | `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 |
219
+ | `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 |
220
+ | `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 |
221
+ | `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 |
222
+ | `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 |
223
+ | `cloudflare-provider.test.ts` | `cloudflare()` construction, tool surface, request building, projections, typed failures, and credential test |
224
+ | `cloudflare-registry.test.ts` | the same provider inside a real deployment: discovery, addressing, and admission through the registry |
225
+ | `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 |
226
+ | `codemode-compat.test.ts` | the `Executor` seam staying structurally compatible with `@cloudflare/codemode`'s `DynamicWorkerExecutor`, enforced by `tsc` |
227
+ | `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 |
228
+ | `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 |
229
+ | `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 |
230
+ | `downstream-oauth.test.ts` | `KvOAuthProvider` round-trips and races, `auth_required` versus `error`, `startAuth`/`finishAuth`, callback refusal equality, bounded diagnostics, and HTML escaping |
231
+ | `errors.test.ts` | `ConnectorCallError` codes, retryable defaults and overrides, `retryAfterMs` round-trip, typed-over-heuristic classification, `AbortError` as a retryable timeout, and framing errors |
232
+ | `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 |
233
+ | `execute-emit.test.ts` | `connecta.emit` (M1–M10) — block validation, budgets, the provider, delivery after the result envelope on success only, and the defaults |
234
+ | `execute-ui.test.ts` | `connecta.ui` (U1–U9) — validation, multiplicity and budget, the provider, `_meta` delivery, and the Apps shell |
235
+ | `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 |
236
+ | `guarded-fetch.test.ts` | the guarded transport — construction, request building, destination confinement, and response handling |
237
+ | `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 |
238
+ | `linear-provider.test.ts` / `linear-registry.test.ts` | the Linear proxy's construction, classification, and guide; then the same connector inside a real deployment |
239
+ | `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 |
240
+ | `mixpanel-provider.test.ts` / `mixpanel-registry.test.ts` | the Mixpanel proxy, then the same connector inside a real deployment |
241
+ | `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 |
242
+ | `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 |
243
+ | `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 |
244
+ | `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) |
245
+ | `registry.test.ts` | construction and id validation, startup convention and result-cap warnings, address resolution, tool-cache TTL, and broken-connector isolation |
246
+ | `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 |
247
+ | `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 |
248
+ | `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 |
249
+ | `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 |
250
+ | `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 |
251
+ | `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` |
252
+ | `stripe-provider.test.ts` / `stripe-registry.test.ts` | the Stripe proxy's endpoint modes and admission, then the connector in a real deployment |
253
+ | `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 |
254
+ | `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 |
255
+
256
+ ### Node-bound (`NODE_ONLY_SUITES`)
257
+
258
+ Each entry carries its reason in `vitest.config.ts`; the reason is the
259
+ justification for *not* re-running it in workerd, so "it was easier" is not one.
260
+
261
+ | Suite | Covers | Why Node |
262
+ | --- | --- | --- |
263
+ | `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 |
264
+ | `doc-links.test.ts` | the documentation checker itself — local file and fragment resolution, repository URLs resolved back to the checkout, duplicate heading slugs, fenced-code exclusion, and useful failures | spawns the Node checker against filesystem fixtures |
265
+ | `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 |
266
+ | `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 |
267
+ | `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 |
268
+ | `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 |
269
+ | `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 |
270
+ | `packed-links.test.ts` | the packed-link gate itself — shipped targets and repository URLs accepted, relative links into unshipped paths and directories rejected with the citation to write instead, reference definitions seen, fenced examples ignored, the changelog exempt | spawns the Node packed-link gate against filesystem fixtures |
271
+ | `package-surface.test.ts` | the published boundary — built output shipped, the `exports` map carrying exactly the documented subpaths plus `./package.json`, 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 |
272
+ | `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 |
273
+ | `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 |
274
+ | `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 |
275
+ | `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 |
276
+ | `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 |
277
+ | `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 |
278
+ | `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 |
279
+ | `upgrade-guide.test.ts` | the [upgrade guide](./upgrading.md)'s claims about somebody else's deployment — the generated file inventory, the seven tool names doctor demands, version boundaries that actually shipped, a bump target that is this release, and the three places a reader finds it | reads the guide, the template tree, and the CLI with Node filesystem APIs |
280
+ | `version.test.ts` | `CONNECTA_VERSION` matching `package.json` | reads `package.json` with Node filesystem APIs |
281
+
282
+ ### Outside `npm run check`
283
+
284
+ | Suite | Covers |
285
+ | --- | --- |
286
+ | `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 |
287
+ | `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)) |
288
+
289
+ **The `_transportFactory` seam.** `RemoteMcpOptions._transportFactory` is
290
+ internal, not public API: when set, `remoteMcp()` uses that `Transport` instead
291
+ of building an HTTP one. Tests link an in-memory transport to an in-process MCP
292
+ server, so remote-MCP behavior is exercised without a network or a real OAuth
293
+ server. Two consequences worth knowing before you use it: an in-memory
294
+ transport has no session semantics, so anything about `Mcp-Session-Id` needs
295
+ the real HTTP transport, and anything about redirects or destination
296
+ confinement does too.
297
+
298
+ ## Troubleshooting
299
+
300
+ - **MCP clients cache the tool list.** After adding a connector or completing a
301
+ downstream OAuth flow, restart the client. It will not re-list on its own.
302
+ Connecta declares a one-hour private `tools/list` cache hint, which is a
303
+ ceiling on how long a well-behaved client may wait, not a promise it will.
304
+ - **`auth_required` that never clears.** Confirm `publicUrl` is set and
305
+ `GET <publicUrl>/oauth/callback/<connectorId>` is reachable from a browser,
306
+ and that storage is durable rather than `memoryStorage()` across restarts.
307
+ Then `authorize_connector` to restart the flow; `force: true` wipes stored
308
+ credentials for a clean retry.
309
+ - **A connector with no `verifyState` refuses every callback.** That is the
310
+ designed behavior, not a bug: handing an unverified code to `finishAuth` is
311
+ the vulnerability. The startup warning names the connector.
312
+ - **401 loops from a client that cannot discover auth.** The client must reach
313
+ the open `/.well-known/oauth-protected-resource` (and the `/mcp` variant);
314
+ confirm CORS and the Clerk keys, and that DCR is enabled on the Clerk
315
+ instance.
316
+ - **No sessions and no server push, by design.** The transport is stateless.
317
+ Scope resolves per request, which is also where the MCP spec has arrived.
318
+ - **A tool that should be callable from a program is not.** Only tools
319
+ explicitly annotated `readOnlyHint: true` are admissible inside the sandbox.
320
+ A missing, false, or contradictory annotation fails closed, every time, and
321
+ the recovery is `call_destructive_tool` — not a wider sandbox.
322
+ - **`check:operator-ui` fails after a UI change.** Run
323
+ `npm run build:operator-ui` and commit the regenerated
324
+ `src/operator-ui/generated.ts` ([operator UI](./operator-ui.md#why-the-bundle-is-committed)).
325
+ - **Upgrade the MCP SDK and Zod together**, then run `npm run release:check`.
326
+ The SDK packages are pinned exactly and paired with Zod 4 to keep the
327
+ 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.