History

Merge remote-tracking branch 'origin/main' into changelog-page

Rhys Sullivan committed 2 months ago

Commit d1203972 parents 5b90890 d5946f11099 files · +113836 −11239Browse files

Unified · Split

889 files
  1. .changeset/cli-credential-store-permissions.md +11 −0
  2. .changeset/concurrent-data-migration-stamps.md +5 −0
  3. .changeset/config.json +2 −2
  4. .changeset/connection-edit-sheet-agent-preview.md +9 −0
  5. .changeset/desktop-settings-owner-only.md +9 −0
  6. .changeset/graph-slice-urls-first-class.md +5 −0
  7. .changeset/graph-spec-slices.md +5 −0
  8. .changeset/host-evaluate-timeout.md +0 −5
  9. .changeset/lazy-mcp-client-module.md +5 −0
  10. .changeset/mcp-abort-releases-response-body.md +7 −0
  11. .changeset/mcp-health-missing-credential.md +11 −0
  12. .changeset/mcp-namespace-search-tools.md +8 −0
  13. .changeset/mcp-pool-key-credential-retention.md +11 −0
  14. .changeset/mcp-priming-reconnect.md +0 −5
  15. .changeset/mcp-session-idle-runtime-reclaim.md +15 −0
  16. .changeset/mcp-sse-result-replay.md +0 −5
  17. .changeset/oauth-dcr-application-type.md +9 −0
  18. .changeset/oauth-popup-localstorage-residue.md +9 −0
  19. .changeset/onepassword-service-account-token-lifetime.md +11 −0
  20. .changeset/openapi-streaming-preview.md +5 −0
  21. .changeset/slim-search-tools.md +6 −0
  22. .changeset/storage-error-shaping.md +8 −0
  23. .changeset/tools-proxy-enumeration-hint.md +0 −5
  24. .changeset/worker-bundler-staging-contract.md +11 −0
  25. .changeset/workos-blip-session-survival.md +0 −5
  26. .claude/skills/emulate/SKILL.md +15 −12
  27. .claude/skills/prod-telemetry/SKILL.md +18 −1
  28. .github/workflows/ci.yml +144 −57
  29. .github/workflows/deploy.yml +29 −1
  30. .github/workflows/graph-slices.yml +47 −0
  31. .github/workflows/pkg-pr-new.yml +10 −0
  32. .github/workflows/preview-sweep.yml +10 −0
  33. .github/workflows/preview.yml +10 −0
  34. .github/workflows/publish-desktop.yml +12 −0
  35. .github/workflows/publish-executor-package.yml +13 −1
  36. .github/workflows/publish-selfhost-docker.yml +142 −4
  37. .github/workflows/release.yml +15 −1
  38. .oxfmtrc.json +1 −0
  39. .oxlintrc.jsonc +26 −0
  40. AGENTS.md +43 −68
  41. README.md +8 −8
  42. TELEMETRY.md +101 −0
  43. apps/cli/CHANGELOG.md +239 −0
  44. apps/cli/package.json +4 −1
  45. apps/cli/src/build.ts +163 −1
  46. apps/cli/src/device-login.test.ts +118 −2
  47. apps/cli/src/device-login.ts +57 −8
  48. apps/cli/src/main.ts +263 −69
  49. apps/cli/src/native-bindings.ts +36 −5
  50. apps/cli/src/server-connection.test.ts +84 −0
  51. apps/cli/src/server-connection.ts +49 −0
  52. apps/cli/src/server-profile.test.ts +86 −5
  53. apps/cli/src/server-profile.ts +22 −4
  54. apps/cli/src/service.test.ts +30 −0
  55. apps/cli/src/service.ts +11 −0
  56. apps/cli/src/tooling.ts +2 −2
  57. apps/cli/src/worker-bundler-artifact.test.ts +59 −0
  58. apps/cli/src/worker-bundler-artifact.ts +55 −0
  59. apps/cli/tsconfig.json +1 −1
  60. apps/cloud/CHANGELOG.md +293 −0
  61. apps/cloud/drizzle/0010_ndjson_output_arrays.sql +23 −0
  62. apps/cloud/drizzle/0011_tense_caretaker.sql +1 −0
  63. apps/cloud/drizzle/0012_woozy_shiva.sql +10 −0
  64. apps/cloud/drizzle/0013_curly_rocket_racer.sql +14 −0
  65. apps/cloud/drizzle/0014_new_karma.sql +1 −0
  66. apps/cloud/drizzle/0015_equal_the_leader.sql +1 −0
  67. apps/cloud/drizzle/meta/0010_snapshot.json +1290 −0
  68. apps/cloud/drizzle/meta/0011_snapshot.json +1296 −0
  69. apps/cloud/drizzle/meta/0012_snapshot.json +1367 −0
  70. apps/cloud/drizzle/meta/0013_snapshot.json +1474 −0
  71. apps/cloud/drizzle/meta/0014_snapshot.json +1480 −0
  72. apps/cloud/drizzle/meta/0015_snapshot.json +1486 −0
  73. apps/cloud/drizzle/meta/_journal.json +42 −0
  74. apps/cloud/executor.config.ts +13 −15
  75. apps/cloud/package.json +9 −4
  76. apps/cloud/scripts/backfill-subjects.ts +87 −0
  77. apps/cloud/scripts/build.mjs +20 −1
  78. apps/cloud/scripts/dev-db.ts +145 −14
  79. apps/cloud/scripts/start-closure.mjs +124 −0
  80. apps/cloud/scripts/test-globalsetup.ts +20 −1
  81. apps/cloud/scripts/vendor-quickjs-wasm.ts +18 −0
  82. apps/cloud/src/account/org-api-key-revoke.node.test.ts +195 −0
  83. apps/cloud/src/account/workos-account-service.ts +65 −8
  84. apps/cloud/src/admin/admin-users-api.ts +354 −0
  85. apps/cloud/src/admin/admin-users-email.node.test.ts +127 −0
  86. apps/cloud/src/analytics/ema-rollout.test.ts +394 −0
  87. apps/cloud/src/analytics/ema-rollout.ts +255 −0
  88. apps/cloud/src/api/protected-api-key-auth.node.test.ts +6 −1
  89. apps/cloud/src/api/protected-jwt-auth.node.test.ts +5 −1
  90. apps/cloud/src/api/protected.test.ts +2 −0
  91. apps/cloud/src/app-paths.test.ts +37 −1
  92. apps/cloud/src/app-paths.ts +28 −0
  93. apps/cloud/src/app.ts +2 −7
  94. apps/cloud/src/auth/api-keys.node.test.ts +23 −8
  95. apps/cloud/src/auth/api-keys.ts +214 −12
  96. apps/cloud/src/auth/api.ts +9 −1
  97. apps/cloud/src/auth/context.ts +9 −4
  98. apps/cloud/src/auth/doc-gate.ts +71 −23
  99. apps/cloud/src/auth/errors.ts +95 −2
  100. apps/cloud/src/auth/handlers.ts +88 −25
  101. apps/cloud/src/auth/jwks-cache.node.test.ts +182 −1
  102. apps/cloud/src/auth/jwks-cache.ts +256 −26
  103. apps/cloud/src/auth/last-org-cookie.ts +32 −0
  104. apps/cloud/src/auth/org-api-key-auth.node.test.ts +159 −0
  105. apps/cloud/src/auth/org-api-key-mint.node.test.ts +316 −0
  106. apps/cloud/src/auth/org-selector-auth.node.test.ts +17 −7
  107. apps/cloud/src/auth/organization.ts +5 −3
  108. apps/cloud/src/auth/user-store-error.node.test.ts +128 −0
  109. apps/cloud/src/auth/workos-auth-provider.ts +132 −19
  110. apps/cloud/src/auth/workos.node.test.ts +282 −1
  111. apps/cloud/src/auth/workos.ts +394 −44
  112. apps/cloud/src/db/db.schema.test.ts +2 −2
  113. apps/cloud/src/db/db.ts +1 −1
  114. apps/cloud/src/db/dev-db-socket-concurrency.node.test.ts +272 −8
  115. apps/cloud/src/db/executor-schema.ts +40 −1
  116. apps/cloud/src/db/org-deletion.test.ts +113 −1
  117. apps/cloud/src/db/org-deletion.ts +4 −0
  118. apps/cloud/src/edge/docs.ts +9 −29
  119. apps/cloud/src/edge/index.ts +4 −5
  120. apps/cloud/src/edge/marketing.test.ts +55 −1
  121. apps/cloud/src/edge/marketing.ts +25 −34
  122. apps/cloud/src/edge/passthrough.test.ts +85 −0
  123. apps/cloud/src/edge/passthrough.ts +138 −0
  124. apps/cloud/src/edge/posthog.ts +8 −21
  125. apps/cloud/src/engine/execution-gate.ts +7 −0
  126. apps/cloud/src/engine/execution-rate-limit.node.test.ts +435 −0
  127. apps/cloud/src/engine/execution-rate-limit.ts +335 −37
  128. apps/cloud/src/engine/execution-stack-metered.ts +15 −2
  129. apps/cloud/src/engine/execution-stack.ts +116 −2
  130. apps/cloud/src/engine/execution-usage.ts +8 −2
  131. apps/cloud/src/engine/first-party-oauth-clients.test.ts +87 −0
  132. apps/cloud/src/env-augment.d.ts +35 −0
  133. apps/cloud/src/extensions/billing/route.node.test.ts +9 −14
  134. apps/cloud/src/extensions/billing/route.ts +5 −2
  135. apps/cloud/src/extensions/billing/service.ts +95 −10
  136. apps/cloud/src/extensions/docs.ts +7 −1
  137. apps/cloud/src/extensions/routes.ts +18 −1
  138. apps/cloud/src/mcp-session.e2e.node.test.ts +1 −1
  139. apps/cloud/src/mcp/agent-handler.ts +130 −11
  140. apps/cloud/src/mcp/auth-provider.test.ts +4 −2
  141. apps/cloud/src/mcp/auth-provider.ts +23 −13
  142. apps/cloud/src/mcp/auth.ts +32 −4
  143. apps/cloud/src/mcp/session-durable-object.ts +95 −53
  144. apps/cloud/src/mcp/session-meta.node.test.ts +167 −0
  145. apps/cloud/src/mcp/session-meta.ts +209 −0
  146. apps/cloud/src/mcp/telemetry.test.ts +160 −0
  147. apps/cloud/src/mcp/telemetry.ts +51 −3
  148. apps/cloud/src/mcp/traceparent.ts +29 −0
  149. apps/cloud/src/observability/header-redaction.ts +19 −0
  150. apps/cloud/src/observability/index.ts +195 −2
  151. apps/cloud/src/observability/observability.test.ts +309 −0
  152. apps/cloud/src/observability/telemetry.ts +52 −11
  153. apps/cloud/src/org/auth-middleware.ts +5 −1
  154. apps/cloud/src/org/handlers.test.ts +1 −0
  155. apps/cloud/src/org/handlers.ts +5 −0
  156. apps/cloud/src/quickjs-engine.wasm +0 −0
  157. apps/cloud/src/quickjs.ts +35 −0
  158. apps/cloud/src/routeTree.gen.ts +109 −0
  159. apps/cloud/src/routes/__root.tsx +58 −57
  160. apps/cloud/src/routes/app/api-keys.tsx +9 −3
  161. apps/cloud/src/routes/app/billing_.plans.tsx +28 −9
  162. apps/cloud/src/routes/bare/login.tsx +1 −1
  163. apps/cloud/src/server.ts +289 −71
  164. apps/cloud/src/start.ts +25 −18
  165. apps/cloud/src/test-globalsetup-exit.node.test.ts +41 −0
  166. apps/cloud/src/wasm.d.ts +6 −0
  167. apps/cloud/src/web/auth.tsx +11 −0
  168. apps/cloud/src/web/pages/login.tsx +3 −1
  169. apps/cloud/src/web/shell.tsx +20 −5
  170. apps/cloud/test-fixtures/test-globalsetup-exit/fixture.test.ts +7 −0
  171. apps/cloud/test-fixtures/test-globalsetup-exit/vitest.config.ts +12 −0
  172. apps/cloud/tsconfig.json +1 −0
  173. apps/cloud/vite.config.ts +19 −1
  174. apps/cloud/wrangler.jsonc +30 −0
  175. apps/desktop/CHANGELOG.md +28 −0
  176. apps/desktop/build/entitlements.mac.plist +0 −21
  177. apps/desktop/build/icon.png +0 −0
  178. apps/desktop/package.json +1 −3
  179. apps/desktop/src/main/app-quit.ts +23 −0
  180. apps/desktop/src/main/crash-fingerprint.test.ts +143 −0
  181. apps/desktop/src/main/crash-fingerprint.ts +80 −0
  182. apps/desktop/src/main/crash-screen.test.ts +26 −0
  183. apps/desktop/src/main/diagnostics.ts +9 −12
  184. apps/desktop/src/main/index.ts +51 −6
  185. apps/desktop/src/main/navigation-errors.test.ts +40 −0
  186. apps/desktop/src/main/navigation-errors.ts +21 −0
  187. apps/desktop/src/main/settings-permissions.test.ts +92 −0
  188. apps/desktop/src/main/settings.ts +10 −0
  189. apps/desktop/src/main/sidecar-exit-wiring.test.ts +163 −0
  190. apps/desktop/src/main/sidecar-exit.test.ts +70 −0
  191. apps/desktop/src/main/sidecar-exit.ts +50 −0
  192. apps/desktop/src/main/sidecar.ts +31 −8
  193. apps/desktop/src/sidecar/native-bindings.ts +18 −5
  194. apps/docs/concepts/integrations.mdx +1 −1
  195. apps/docs/docs.json +1 −1
  196. apps/docs/hosted/docker.mdx +3 −0
  197. apps/docs/hosted/tracing.mdx +115 −0
  198. apps/docs/local/cli.mdx +3 −3
  199. apps/host-cloudflare/README.md +13 −9
  200. apps/host-cloudflare/executor.config.ts +12 −5
  201. apps/host-cloudflare/package.json +6 −4
  202. apps/host-cloudflare/scripts/assert-shell-asset.mjs +17 −0
  203. apps/host-cloudflare/scripts/deploy.sh +11 −7
  204. apps/host-cloudflare/src/account/account-provider.ts +5 −0
  205. apps/host-cloudflare/src/auth/cloudflare-access.ts +6 −1
  206. apps/host-cloudflare/src/config.test.ts +74 −0
  207. apps/host-cloudflare/src/config.ts +48 −5
  208. apps/host-cloudflare/src/db/d1.test.ts +207 −0
  209. apps/host-cloudflare/src/db/d1.ts +84 −5
  210. apps/host-cloudflare/src/db/data-migrations.test.ts +60 −5
  211. apps/host-cloudflare/src/db/data-migrations.ts +101 −7
  212. apps/host-cloudflare/src/execution.ts +10 −5
  213. apps/host-cloudflare/src/mcp/agent-handler.ts +4 −0
  214. apps/host-cloudflare/src/mcp/session-durable-object.ts +44 −3
  215. apps/host-cloudflare/src/plugins.ts +12 −5
  216. apps/host-cloudflare/src/worker.e2e.node.test.ts +127 −13
  217. apps/host-cloudflare/src/worker.ts +19 −1
  218. apps/host-cloudflare/test-stubs/cloudflare-workers.ts +7 −0
  219. apps/host-cloudflare/tsconfig.json +4 −0
  220. apps/host-cloudflare/tsr.routes.ts +5 −1
  221. apps/host-cloudflare/vite.config.ts +8 −1
  222. apps/host-cloudflare/vitest.config.ts +7 −0
  223. apps/host-cloudflare/web/routeTree.gen.ts +86 −0
  224. apps/host-cloudflare/web/routes/__root.tsx +15 −1
  225. apps/host-cloudflare/wrangler.jsonc +22 −7
  226. apps/host-selfhost/.env.example +6 −0
  227. apps/host-selfhost/CHANGELOG.md +290 −0
  228. apps/host-selfhost/Dockerfile +1 −1
  229. apps/host-selfhost/executor.config.ts +22 −14
  230. apps/host-selfhost/package.json +9 −5
  231. apps/host-selfhost/scripts/assert-shell-asset.mjs +18 −0
  232. apps/host-selfhost/scripts/package-runtime.ts +55 −1
  233. apps/host-selfhost/src/account/better-auth-account-provider.ts +27 −0
  234. apps/host-selfhost/src/admin/admin-escalation.node.test.ts +184 −0
  235. apps/host-selfhost/src/admin/admin-users-api.ts +225 −0
  236. apps/host-selfhost/src/admin/admin-users.node.test.ts +234 −0
  237. apps/host-selfhost/src/admin/handlers.ts +14 −14
  238. apps/host-selfhost/src/admin/require-admin.ts +126 −0
  239. apps/host-selfhost/src/analytics.ts +83 −0
  240. apps/host-selfhost/src/app.ts +28 −4
  241. apps/host-selfhost/src/auth/better-auth.ts +20 −1
  242. apps/host-selfhost/src/auth/identity.ts +1 −0
  243. apps/host-selfhost/src/auth/return-to.test.ts +45 −1
  244. apps/host-selfhost/src/auth/return-to.ts +29 −0
  245. apps/host-selfhost/src/config.ts +25 −0
  246. apps/host-selfhost/src/db/data-migrations.ts +11 −1
  247. apps/host-selfhost/src/execution.ts +20 −9
  248. apps/host-selfhost/src/executor-config.test.ts +59 −0
  249. apps/host-selfhost/src/integrations-mcp.test.ts +2 −2
  250. apps/host-selfhost/src/integrations.test.ts +0 −0
  251. apps/host-selfhost/src/mcp/auth.ts +7 −2
  252. apps/host-selfhost/src/mcp/mcp.test.ts +1 −0
  253. apps/host-selfhost/src/mcp/org-path.test.ts +18 −1
  254. apps/host-selfhost/src/mcp/org-path.ts +12 −0
  255. apps/host-selfhost/src/mcp/session-store.ts +11 −0
  256. apps/host-selfhost/src/platform-credential.test.ts +210 −0
  257. apps/host-selfhost/src/serve.ts +37 −2
  258. apps/host-selfhost/src/subject-sightings.test.ts +82 −0
  259. apps/host-selfhost/src/telemetry.test.ts +142 −0
  260. apps/host-selfhost/src/telemetry.ts +122 −0
  261. apps/host-selfhost/src/testing/test-app.ts +43 −2
  262. apps/host-selfhost/tsconfig.json +4 −0
  263. apps/host-selfhost/vite.config.ts +7 −0
  264. apps/host-selfhost/web/login.tsx +5 −10
  265. apps/host-selfhost/web/routeTree.gen.ts +109 −0
  266. apps/host-selfhost/web/routes/__root.tsx +25 −2
  267. apps/local/CHANGELOG.md +110 −0
  268. apps/local/drizzle/0004_cultured_shadowcat.sql +1 −0
  269. apps/local/drizzle/meta/0004_snapshot.json +927 −0
  270. apps/local/drizzle/meta/_journal.json +7 −0
  271. apps/local/executor.config.ts +18 −6
  272. apps/local/package.json +5 −4
  273. apps/local/src/analytics.ts +68 −0
  274. apps/local/src/app.ts +23 −6
  275. apps/local/src/auth-tool-failures.test.ts +2 −2
  276. apps/local/src/db/data-migrations.ts +7 −1
  277. apps/local/src/db/db-file-permissions.test.ts +99 −0
  278. apps/local/src/db/executor-schema.ts +1 −0
  279. apps/local/src/db/libsql.ts +38 −0
  280. apps/local/src/db/sqlite-fumadb.ts +6 −0
  281. apps/local/src/executor.ts +48 −18
  282. apps/local/src/identity.ts +1 −0
  283. apps/local/src/installation.ts +5 −4
  284. apps/local/src/main.ts +72 −12
  285. apps/local/src/mcp-browser-resume.test.ts +1 −1
  286. apps/local/src/mcp.ts +13 −1
  287. apps/local/src/serve.test.ts +1 −1
  288. apps/local/src/serve.ts +42 −11
  289. apps/marketing/src/assets/yc-backed-by.svg +1 −15
  290. apps/marketing/src/components/self-host-contact-modal.tsx +0 −126
  291. apps/marketing/src/layouts/Layout.astro +11 −4
  292. apps/marketing/src/pages/about-executor.astro +158 −0
  293. apps/marketing/src/pages/google-oauth.astro +554 −0
  294. apps/marketing/src/pages/google-workspace.astro +218 −0
  295. apps/marketing/src/pages/index.astro +8 −4
  296. apps/marketing/src/pages/privacy.astro +35 −2
  297. assets/executor-demo.gif +0 −0
  298. autumn.config.ts +2 −2
  299. bun.lock +363 −215
  300. design.md +6 −6
  301. e2e/AGENTS.md +42 −280
  302. e2e/CHANGELOG.md +168 −0
  303. e2e/RUNNING.md +283 −0
  304. e2e/cli/service-lifecycle.test.ts +2 −2
  305. e2e/cloud/admin-users-console.test.ts +390 −0
  306. e2e/cloud/admin-users.test.ts +322 −0
  307. e2e/cloud/auth-hint.test.ts +21 −3
  308. e2e/cloud/auth-routing-flow.test.ts +3 −2
  309. e2e/cloud/auth-session.test.ts +21 −3
  310. e2e/cloud/billing-customer-provisioning.test.ts +281 −0
  311. e2e/cloud/billing-trial-checkout-stale.test.ts +3 −2
  312. e2e/cloud/cli-device-login.test.ts +3 −3
  313. e2e/cloud/connect-card-origin.test.ts +55 −0
  314. e2e/cloud/connect-card-ssr-origin.test.ts +0 −79
  315. e2e/cloud/connect-link-multi-org.test.ts +232 −0
  316. e2e/cloud/connect-panel.test.ts +4 −3
  317. e2e/cloud/connection-modal-oauth-abandon.test.ts +3 −2
  318. e2e/cloud/connection-owner-isolation.test.ts +37 −15
  319. e2e/cloud/credential-write-durability.test.ts +570 −0
  320. e2e/cloud/frontend-error-reporting.test.ts +132 −0
  321. e2e/cloud/integrations-api.test.ts +0 −0
  322. e2e/cloud/integrations-refresh.test.ts +0 −0
  323. e2e/cloud/logout-stale-session.test.ts +166 −0
  324. e2e/cloud/mcp-browser-approval-org-scope.test.ts +4 −3
  325. e2e/cloud/mcp-browser-resume-page.test.ts +64 −57
  326. e2e/cloud/mcp-destroyed-session-envelope.test.ts +365 −0
  327. e2e/cloud/mcp-execution-limits.test.ts +179 −2
  328. e2e/cloud/mcp-oversize-response.test.ts +136 −0
  329. e2e/cloud/mcp-session-cold-init.test.ts +191 −0
  330. e2e/cloud/mcp-session-idle-runtime-disposal.test.ts +252 −0
  331. e2e/cloud/mcp-telemetry-contract.test.ts +106 −0
  332. e2e/cloud/member-invite-seat-limit.test.ts +3 −2
  333. e2e/cloud/oauth-callback-org-scope.test.ts +5 −6
  334. e2e/cloud/oauth-callback-unauthenticated.test.ts +1 −1
  335. e2e/cloud/onboarding-mcp-url.test.ts +2 −1
  336. e2e/cloud/org-api-keys-console.test.ts +142 −0
  337. e2e/cloud/org-delete.test.ts +4 −3
  338. e2e/cloud/org-last-visited.test.ts +96 −0
  339. e2e/cloud/org-limit.test.ts +3 −2
  340. e2e/cloud/org-multitab-cookie.test.ts +4 −3
  341. e2e/cloud/org-slug-foreign.test.ts +3 −2
  342. e2e/cloud/org-switcher.test.ts +4 −3
  343. e2e/cloud/repro-listener-supersede.test.ts +130 −0
  344. e2e/cloud/spec-update-convergence.test.ts +20 −6
  345. e2e/cloud/storage-error-report-shape.test.ts +280 −0
  346. e2e/cloud/support/session.ts +205 −0
  347. e2e/cloud/surface-reachability.test.ts +21 −0
  348. e2e/cloud/toolkit-opencode-real.test.ts +3 −3
  349. e2e/cloud/toolkit-policy-perf.test.ts +8 −8
  350. e2e/cloud/unauthenticated-skeleton.test.ts +14 −5
  351. e2e/cloudflare/mcp-native-elicitation.test.ts +108 −0
  352. e2e/local/auth.test.ts +6 −4
  353. e2e/local/boot-process.test.ts +101 −0
  354. e2e/local/cli-call-logged-out.test.ts +160 −0
  355. e2e/local/cli-mcp-daemon-attach-stress.test.ts +52 −62
  356. e2e/local/cli-telemetry-optout.test.ts +27 −0
  357. e2e/local/daemon-process.ts +55 −0
  358. e2e/local/local-server.ts +25 −5
  359. e2e/local/mcp-native-elicitation.test.ts +128 −0
  360. e2e/local/oauth-token-durability.test.ts +252 −0
  361. e2e/local/server-profile-header-env.test.ts +109 −0
  362. e2e/local/stdio-mcp.test.ts +30 −0
  363. e2e/local/update-notice.test.ts +1 −1
  364. e2e/local/vite-dev-routing.test.ts +3 −2
  365. e2e/mcp-apps/.gitignore +5 −0
  366. e2e/mcp-apps/README.md +67 −0
  367. e2e/mcp-apps/package.json +17 −0
  368. e2e/mcp-apps/playwright.config.ts +31 −0
  369. e2e/mcp-apps/scripts/check-sunpeak.mjs +45 −0
  370. e2e/mcp-apps/tests/create-artifact.spec.ts +87 −0
  371. e2e/package.json +3 −4
  372. e2e/recordings/custom-tools-console.mp4 +0 −0
  373. e2e/scenarios/api-tools.test.ts +1 −1
  374. e2e/scenarios/artifact-approval.test.ts +228 −0
  375. e2e/scenarios/artifact-loading-surface.test.ts +331 −0
  376. e2e/scenarios/artifact-preview-gallery.test.ts +282 −0
  377. e2e/scenarios/artifacts.test.ts +854 −0
  378. e2e/scenarios/auth-methods-ui.test.ts +2 −3
  379. e2e/scenarios/auth-methods.test.ts +4 −4
  380. e2e/scenarios/browser-approval.test.ts +2 −1
  381. e2e/scenarios/connect-deep-link.test.ts +169 −0
  382. e2e/scenarios/connect-handoff-session.test.ts +26 −23
  383. e2e/scenarios/connect-handoff.test.ts +37 −24
  384. e2e/scenarios/connection-health-verdict.test.ts +531 −0
  385. e2e/scenarios/connection-remove-confirm.test.ts +122 −0
  386. e2e/scenarios/docs-link.test.ts +2 −1
  387. e2e/scenarios/first-party-oauth.test.ts +382 −0
  388. e2e/scenarios/google-health-checks.test.ts +425 −0
  389. e2e/scenarios/google-photos-preset-ui.test.ts +22 −21
  390. e2e/scenarios/graphql-introspection-health.test.ts +146 −0
  391. e2e/scenarios/health-checks-ui.test.ts +12 −11
  392. e2e/scenarios/integrations-catalog-search.test.ts +77 −0
  393. e2e/scenarios/mcp-catalog-sync-ui.test.ts +3 −4
  394. e2e/scenarios/mcp-cloudflare-codemode-warning.test.ts +41 −0
  395. e2e/scenarios/mcp-session-state.test.ts +157 −0
  396. e2e/scenarios/microsoft-emulator.test.ts +25 −22
  397. e2e/scenarios/microsoft-graph-default.test.ts +65 −59
  398. e2e/scenarios/microsoft-graph-full.test.ts +56 −48
  399. e2e/scenarios/namespace-enumeration.test.ts +167 −0
  400. e2e/scenarios/namespace-search-tools.test.ts +146 −0
  401. e2e/scenarios/oauth-callback-url.test.ts +2 −3
  402. e2e/scenarios/oauth-client-handoff.test.ts +31 −28
  403. e2e/scenarios/oauth-refresh-on-401.test.ts +326 −0
  404. e2e/scenarios/oauth-refresh-rejected-non-json.test.ts +503 −0
  405. e2e/scenarios/oauth-refresh-rejected.test.ts +291 −0
  406. e2e/scenarios/oauth-scope-insufficient.test.ts +336 −0
  407. e2e/scenarios/oauth-second-connection.test.ts +233 −0
  408. e2e/scenarios/openapi-add-integration-action-bar.test.ts +6 −5
  409. e2e/scenarios/openapi-integration.test.ts +0 −0
  410. e2e/scenarios/openapi-multi-key-auth-ui.test.ts +2 −1
  411. e2e/scenarios/openapi-ndjson-output.test.ts +250 −0
  412. e2e/scenarios/openapi-server-selection-ui.test.ts +4 −3
  413. e2e/scenarios/openapi-unknown-args.test.ts +162 −0
  414. e2e/scenarios/org-slug-routing.test.ts +5 −4
  415. e2e/scenarios/policies-ui.test.ts +61 −5
  416. e2e/scenarios/provider-plugins-ui.test.ts +37 −42
  417. e2e/scenarios/resume-after-sandbox-deadline.test.ts +165 −0
  418. e2e/scenarios/run-panel-auto-approve.test.ts +8 −21
  419. e2e/scenarios/shape-memory.test.ts +178 −0
  420. e2e/scenarios/support/large-catalog.ts +29 −29
  421. e2e/scenarios/tool-descriptions.test.ts +2 −2
  422. e2e/scenarios/toolkits-mcp.test.ts +67 −0
  423. e2e/scripts/ci-shard-durations.ts +94 −0
  424. e2e/scripts/ci-shard.test.ts +126 −0
  425. e2e/scripts/ci-shard.ts +231 −0
  426. e2e/scripts/ci-shard.vitest.config.ts +9 −0
  427. e2e/scripts/cli.ts +5 −0
  428. e2e/scripts/record-connect-card.ts +2 −1
  429. e2e/scripts/run-ci-shard.ts +37 −0
  430. e2e/selfhost/admin-users-console.test.ts +331 −0
  431. e2e/selfhost/admin-users-pager.test.ts +167 −0
  432. e2e/selfhost/api-keys-feedback.test.ts +4 −3
  433. e2e/selfhost/auth-methods-ui.test.ts +5 −12
  434. e2e/selfhost/cli-device-login.test.ts +3 −3
  435. e2e/selfhost/connect-modal-credential-ux.test.ts +46 −2
  436. e2e/selfhost/detected-auth-immutable-ui.test.ts +3 −4
  437. e2e/selfhost/mcp-auth-required-add.test.ts +2 −3
  438. e2e/selfhost/mcp-browser-approval-ownership.test.ts +4 −45
  439. e2e/selfhost/mcp-connect-card-url.test.ts +2 −1
  440. e2e/selfhost/mcp-enterprise-managed-auth.test.ts +443 −0
  441. e2e/selfhost/mcp-oauth-cimd-connect.test.ts +109 −0
  442. e2e/selfhost/mcp-oauth-consent.test.ts +3 −2
  443. e2e/selfhost/mcp-oauth-reconnect-health.test.ts +222 −19
  444. e2e/selfhost/mcp-oauth-slow-connect.test.ts +133 −0
  445. e2e/selfhost/mcp-opencode-consent.test.ts +2 −1
  446. e2e/selfhost/oauth-app-modal.test.ts +29 −1
  447. e2e/selfhost/oauth-callback-unauthenticated.test.ts +2 −1
  448. e2e/selfhost/oauth-org-connection-cross-principal.test.ts +275 −0
  449. e2e/selfhost/posthog-mcp-oauth.test.ts +45 −21
  450. e2e/selfhost/toolkits-ui.test.ts +6 −1
  451. e2e/setup/boot.ts +122 −13
  452. e2e/setup/cloud.boot.ts +41 −4
  453. e2e/setup/cloud.globalsetup.ts +16 −3
  454. e2e/setup/cloudflare.boot.ts +7 −2
  455. e2e/setup/desktop-linux.globalsetup.ts +1 −0
  456. e2e/setup/desktop-macos.globalsetup.ts +1 −0
  457. e2e/setup/execution-limits.ts +15 −0
  458. e2e/setup/mcp-session-timeouts.ts +3 −1
  459. e2e/setup/motel.ts +3 −3
  460. e2e/setup/sandbox-timeout.ts +28 −0
  461. e2e/setup/selfhost.boot.ts +10 −2
  462. e2e/setup/selfhost.globalsetup.ts +8 −2
  463. e2e/src/emulator-instance.ts +71 −0
  464. e2e/src/ports.ts +31 −7
  465. e2e/src/surfaces/autumn.ts +80 −6
  466. e2e/src/surfaces/browser.ts +123 −1
  467. e2e/src/surfaces/cli.ts +10 −1
  468. e2e/src/surfaces/mcp.ts +24 −7
  469. e2e/src/update-card-render.ts +2 −1
  470. e2e/targets/cloud.ts +9 −1
  471. e2e/targets/selfhost-docker.ts +7 −6
  472. e2e/targets/selfhost.ts +50 −1
  473. examples/all-plugins/CHANGELOG.md +196 −0
  474. examples/all-plugins/package.json +1 −2
  475. examples/all-plugins/src/main.ts +13 −4
  476. examples/docs-sdk-quickstart/CHANGELOG.md +112 −0
  477. examples/docs-sdk-quickstart/package.json +1 −1
  478. knip.config.ts +0 −3
  479. package.json +5 −2
  480. packages/app/package.json +6 −1
  481. packages/app/src/crash-reporting.test.ts +115 −0
  482. packages/app/src/crash-reporting.ts +35 −3
  483. packages/app/src/entry-client.tsx +5 −0
  484. packages/app/src/preload-error.test.ts +132 −0
  485. packages/app/src/preload-error.ts +90 −0
  486. packages/app/src/routeTree.gen.ts +81 −0
  487. packages/app/src/routes/__root.tsx +15 −4
  488. packages/app/src/server-disconnected.test.ts +184 −0
  489. packages/app/src/server-disconnected.ts +113 −0
  490. packages/app/src/web/shell.tsx +22 −7
  491. packages/app/tsr.routes.ts +6 −4
  492. packages/app/vite.ts +8 −1
  493. packages/app/vitest.config.ts +8 −0
  494. packages/core/analytics/CHANGELOG.md +52 −0
  495. packages/core/analytics/package.json +51 −0
  496. packages/core/analytics/src/analytics.test.ts +233 −0
  497. packages/core/analytics/src/analytics.ts +240 −0
  498. packages/core/analytics/src/engine.test.ts +159 −0
  499. packages/core/analytics/src/engine.ts +63 −0
  500. packages/core/analytics/src/events.ts +71 −0
  501. packages/core/analytics/src/index.ts +12 −0
  502. packages/core/analytics/tsconfig.json +24 −0
  503. packages/core/analytics/tsup.config.ts +12 −0
  504. packages/core/analytics/vitest.config.ts +7 −0
  505. packages/core/api/CHANGELOG.md +128 −0
  506. packages/core/api/package.json +1 −1
  507. packages/core/api/src/account/api.ts +37 −0
  508. packages/core/api/src/account/handlers.ts +18 −0
  509. packages/core/api/src/account/org-slug.test.ts +28 −1
  510. packages/core/api/src/account/org-slug.ts +7 −2
  511. packages/core/api/src/account/service.ts +12 −0
  512. packages/core/api/src/admin/admin-users.test.ts +1194 −0
  513. packages/core/api/src/admin/api.ts +362 −0
  514. packages/core/api/src/admin/handlers.ts +69 −0
  515. packages/core/api/src/admin/reads.ts +387 −0
  516. packages/core/api/src/admin/service.ts +81 −0
  517. packages/core/api/src/api.ts +2 −0
  518. packages/core/api/src/artifacts/api.ts +165 −0
  519. packages/core/api/src/client.ts +13 −0
  520. packages/core/api/src/connections/api.ts +1 −0
  521. packages/core/api/src/executions/api.ts +51 −2
  522. packages/core/api/src/handlers/artifacts.ts +119 −0
  523. packages/core/api/src/handlers/connections.ts +1 −0
  524. packages/core/api/src/handlers/executions.ts +194 −3
  525. packages/core/api/src/handlers/index.ts +3 −0
  526. packages/core/api/src/handlers/integrations.ts +1 −0
  527. packages/core/api/src/handlers/oauth.ts +6 −0
  528. packages/core/api/src/index.ts +17 −0
  529. packages/core/api/src/integrations/api.ts +8 −0
  530. packages/core/api/src/oauth-popup.test.ts +114 −3
  531. packages/core/api/src/oauth-popup.ts +15 −4
  532. packages/core/api/src/oauth/api.ts +22 −2
  533. packages/core/api/src/observability.test.ts +25 −1
  534. packages/core/api/src/observability.ts +13 −2
  535. packages/core/api/src/plugin-routes.ts +1 −1
  536. packages/core/api/src/server.ts +35 −1
  537. packages/core/api/src/server/execution-stack-middleware.ts +148 −18
  538. packages/core/api/src/server/execution-stack.ts +58 −9
  539. packages/core/api/src/server/executor-app.ts +7 −2
  540. packages/core/api/src/server/fixed-execution-middleware.ts +28 −5
  541. packages/core/api/src/server/host-foundation.ts +42 −0
  542. packages/core/api/src/server/identity.ts +71 −4
  543. packages/core/api/src/server/mcp-build.ts +57 −6
  544. packages/core/api/src/server/scoped-executor.ts +114 −5
  545. packages/core/api/src/services.ts +18 −1
  546. packages/core/cli/CHANGELOG.md +98 −0
  547. packages/core/cli/package.json +1 −1
  548. packages/core/config/CHANGELOG.md +98 −0
  549. packages/core/config/package.json +1 −1
  550. packages/core/config/src/config.test.ts +37 −35
  551. packages/core/config/src/index.ts +7 −7
  552. packages/core/config/src/schema.ts +16 −16
  553. packages/core/config/src/sink.ts +10 −10
  554. packages/core/config/src/write.ts +24 −24
  555. packages/core/execution/CHANGELOG.md +114 −0
  556. packages/core/execution/package.json +1 −1
  557. packages/core/execution/src/description.test.ts +50 −1
  558. packages/core/execution/src/description.ts +22 −0
  559. packages/core/execution/src/engine.test.ts +1 −1
  560. packages/core/execution/src/engine.ts +119 −12
  561. packages/core/execution/src/index.ts +16 −3
  562. packages/core/execution/src/provided-globals.ts +274 −0
  563. packages/core/execution/src/skills.test.ts +27 −1
  564. packages/core/execution/src/skills.ts +579 −11
  565. packages/core/execution/src/tool-invoker.leak.test.ts +1 −1
  566. packages/core/execution/src/tool-invoker.test.ts +434 −54
  567. packages/core/execution/src/tool-invoker.ts +108 −34
  568. packages/core/integrations-registry/src/registry.test.ts +10 −2
  569. packages/core/integrations-registry/src/registry.ts +1 −1
  570. packages/core/sdk/CHANGELOG.md +87 −0
  571. packages/core/sdk/README.md +3 −3
  572. packages/core/sdk/package.json +9 −2
  573. packages/core/sdk/src/artifact-preview.test.ts +304 −0
  574. packages/core/sdk/src/artifact-preview.ts +574 −0
  575. packages/core/sdk/src/artifact-table.test.ts +132 −0
  576. packages/core/sdk/src/artifact.ts +211 −0
  577. packages/core/sdk/src/artifacts.test.ts +353 −0
  578. packages/core/sdk/src/auth-tool-failure.ts +55 −17
  579. packages/core/sdk/src/blob.ts +1 −1
  580. packages/core/sdk/src/client.test.ts +2 −2
  581. packages/core/sdk/src/client.ts +34 −5
  582. packages/core/sdk/src/connection-name-identifier.ts +4 −0
  583. packages/core/sdk/src/connection.ts +7 −4
  584. packages/core/sdk/src/connections.test.ts +212 −2
  585. packages/core/sdk/src/core-schema.ts +129 −4
  586. packages/core/sdk/src/core-tools.ts +78 −17
  587. packages/core/sdk/src/errors.ts +47 −4
  588. packages/core/sdk/src/executor.test.ts +259 −1
  589. packages/core/sdk/src/executor.ts +1724 −194
  590. packages/core/sdk/src/fuma-runtime.test.ts +265 −0
  591. packages/core/sdk/src/fuma-runtime.ts +167 −12
  592. packages/core/sdk/src/host-internal.ts +15 −0
  593. packages/core/sdk/src/hosted-http-client.test.ts +68 −0
  594. packages/core/sdk/src/hosted-http-client.ts +71 −2
  595. packages/core/sdk/src/ids.ts +4 −0
  596. packages/core/sdk/src/index.ts +91 −2
  597. packages/core/sdk/src/insufficient-scope.test.ts +377 −0
  598. packages/core/sdk/src/insufficient-scope.ts +274 −0
  599. packages/core/sdk/src/integration.ts +26 −0
  600. packages/core/sdk/src/migration-spec.test.ts +2 −2
  601. packages/core/sdk/src/migration-spec.ts +2 −2
  602. packages/core/sdk/src/oauth-client.ts +199 −3
  603. packages/core/sdk/src/oauth-discovery.test.ts +26 −0
  604. packages/core/sdk/src/oauth-discovery.ts +58 −9
  605. packages/core/sdk/src/oauth-ema-lifecycle.test.ts +336 −0
  606. packages/core/sdk/src/oauth-ema-rollout.test.ts +467 −0
  607. packages/core/sdk/src/oauth-ema.test.ts +502 −0
  608. packages/core/sdk/src/oauth-ema.ts +476 −0
  609. packages/core/sdk/src/oauth-first-party.test.ts +471 −0
  610. packages/core/sdk/src/oauth-flow.test.ts +985 −8
  611. packages/core/sdk/src/oauth-helpers.test.ts +340 −0
  612. packages/core/sdk/src/oauth-helpers.ts +731 −49
  613. packages/core/sdk/src/oauth-register-dynamic.test.ts +211 −0
  614. packages/core/sdk/src/oauth-remove-client.test.ts +66 −1
  615. packages/core/sdk/src/oauth-scope-union.test.ts +93 −2
  616. packages/core/sdk/src/oauth-service.ts +773 −68
  617. packages/core/sdk/src/owner-policy-reach.test.ts +318 −0
  618. packages/core/sdk/src/owner-policy.ts +78 −1
  619. packages/core/sdk/src/pending-approval.test.ts +142 −0
  620. packages/core/sdk/src/pending-approval.ts +110 −0
  621. packages/core/sdk/src/platform-view.test.ts +901 −0
  622. packages/core/sdk/src/plugin.ts +114 −11
  623. packages/core/sdk/src/policies.test.ts +43 −2
  624. packages/core/sdk/src/policies.ts +41 −0
  625. packages/core/sdk/src/promise-executor.ts +1 −1
  626. packages/core/sdk/src/promise.test.ts +2 −2
  627. packages/core/sdk/src/provider-call-timeout.test.ts +212 −0
  628. packages/core/sdk/src/provider-item-owner.ts +47 −0
  629. packages/core/sdk/src/sentry-grouping.test.ts +269 −0
  630. packages/core/sdk/src/sentry-grouping.ts +169 −0
  631. packages/core/sdk/src/server-connection.test.ts +54 −0
  632. packages/core/sdk/src/server-connection.ts +69 −1
  633. packages/core/sdk/src/shape-inference.test.ts +136 −0
  634. packages/core/sdk/src/shape-inference.ts +219 −0
  635. packages/core/sdk/src/shape-memory.ts +90 −0
  636. packages/core/sdk/src/shared.ts +34 −0
  637. packages/core/sdk/src/sqlite-config-blob-migration.test.ts +2 −2
  638. packages/core/sdk/src/sqlite-data-migrations.test.ts +61 −0
  639. packages/core/sdk/src/sqlite-data-migrations.ts +12 −6
  640. packages/core/sdk/src/subject-registry.test.ts +418 −0
  641. packages/core/sdk/src/subject-registry.ts +0 −0
  642. packages/core/sdk/src/subject-table.test.ts +103 −0
  643. packages/core/sdk/src/test-config.ts +6 −0
  644. packages/core/sdk/src/testing/id-jag-test-support.ts +251 −0
  645. packages/core/sdk/src/testing/oauth-test-server.ts +340 −2
  646. packages/core/sdk/src/tool-not-found-empty-catalog.test.ts +113 −0
  647. packages/core/sdk/src/types.ts +4 −0
  648. packages/core/sdk/tsup.config.ts +1 −0
  649. packages/core/vite-plugin/CHANGELOG.md +98 −0
  650. packages/core/vite-plugin/package.json +1 −1
  651. packages/hosts/cloudflare/CHANGELOG.md +144 −0
  652. packages/hosts/cloudflare/package.json +6 −2
  653. packages/hosts/cloudflare/src/mcp/agent-session-durable-object.test.ts +582 −4
  654. packages/hosts/cloudflare/src/mcp/agent-session-durable-object.ts +439 −37
  655. packages/hosts/cloudflare/src/mcp/agent-session-model-resume.test.ts +2 −0
  656. packages/hosts/cloudflare/src/mcp/agents-event-store.test.ts +41 −12
  657. packages/hosts/cloudflare/src/mcp/agents-priming-event.test.ts +11 −4
  658. packages/hosts/cloudflare/src/mcp/do-headers.test.ts +49 −0
  659. packages/hosts/cloudflare/src/mcp/do-headers.ts +41 −6
  660. packages/hosts/cloudflare/src/mcp/durable-object-errors.test.ts +275 −0
  661. packages/hosts/cloudflare/src/mcp/durable-object-errors.ts +237 −0
  662. packages/hosts/cloudflare/src/mcp/session-runtime-residency.ts +96 −0
  663. packages/hosts/mcp-apps-shell/.gitignore +1 −0
  664. packages/hosts/mcp-apps-shell/CHANGELOG.md +57 −0
  665. packages/hosts/mcp-apps-shell/package.json +102 −0
  666. packages/hosts/mcp-apps-shell/scripts/gen-provided-globals.ts +23 −0
  667. packages/hosts/mcp-apps-shell/scripts/gen-smoke-harness.ts +45 −0
  668. packages/hosts/mcp-apps-shell/scripts/subset-fonts.ts +96 −0
  669. packages/hosts/mcp-apps-shell/src/index.ts +13 −0
  670. packages/hosts/mcp-apps-shell/src/shell-asset-path.ts +35 −0
  671. packages/hosts/mcp-apps-shell/src/shell-dev-html.d.ts +14 −0
  672. packages/hosts/mcp-apps-shell/src/shell-html.ts +67 −0
  673. packages/hosts/mcp-apps-shell/src/shell-resource.smoke.test.ts +98 −0
  674. packages/hosts/mcp-apps-shell/src/shell/artifact-renderer.tsx +432 −0
  675. packages/hosts/mcp-apps-shell/src/shell/artifact-states.tsx +217 −0
  676. packages/hosts/mcp-apps-shell/src/shell/component-runtime.test.ts +53 −0
  677. packages/hosts/mcp-apps-shell/src/shell/component-runtime.ts +190 −0
  678. packages/hosts/mcp-apps-shell/src/shell/components.ts +283 −0
  679. packages/hosts/mcp-apps-shell/src/shell/display-mode.ts +104 −0
  680. packages/hosts/mcp-apps-shell/src/shell/fonts/README.md +29 −0
  681. packages/hosts/mcp-apps-shell/src/shell/fonts/geist-mono.woff2 +0 −0
  682. packages/hosts/mcp-apps-shell/src/shell/fonts/geist-sans.woff2 +0 −0
  683. packages/hosts/mcp-apps-shell/src/shell/globals.css +53 −0
  684. packages/hosts/mcp-apps-shell/src/shell/hooks.ts +158 −0
  685. packages/hosts/mcp-apps-shell/src/shell/inner-renderer-source.d.ts +14 −0
  686. packages/hosts/mcp-apps-shell/src/shell/inner-renderer.tsx +432 −0
  687. packages/hosts/mcp-apps-shell/src/shell/mcp-app.browser.test.ts +2649 −0
  688. packages/hosts/mcp-apps-shell/src/shell/mcp-app.html +17 −0
  689. packages/hosts/mcp-apps-shell/src/shell/mcp-app.tsx +101 −0
  690. packages/hosts/mcp-apps-shell/src/shell/preview-capture.ts +36 −0
  691. packages/hosts/mcp-apps-shell/src/shell/provided-globals.pin.test.ts +40 −0
  692. packages/hosts/mcp-apps-shell/src/shell/proxy.ts +181 −0
  693. packages/hosts/mcp-apps-shell/src/shell/render-signal.ts +60 −0
  694. packages/hosts/mcp-apps-shell/src/shell/shell-app.tsx +1098 −0
  695. packages/hosts/mcp-apps-shell/src/shell/shell-html-url.d.ts +13 −0
  696. packages/hosts/mcp-apps-shell/src/shell/smoke-harness-bundle.gen.ts +17 −0
  697. packages/hosts/mcp-apps-shell/src/shell/smoke-harness-shims.ts +274 −0
  698. packages/hosts/mcp-apps-shell/src/shell/smoke-harness.tsx +168 −0
  699. packages/hosts/mcp-apps-shell/src/shell/smoke-render-result.ts +36 −0
  700. packages/hosts/mcp-apps-shell/src/shell/smoke-render.test.tsx +388 −0
  701. packages/hosts/mcp-apps-shell/src/shell/smoke-render.ts +189 −0
  702. packages/hosts/mcp-apps-shell/src/shell/theme-tokens.pin.test.ts +196 −0
  703. packages/hosts/mcp-apps-shell/src/shell/theme.css +317 −0
  704. packages/hosts/mcp-apps-shell/src/shell/tool-call-grammar.pin.test.ts +110 −0
  705. packages/hosts/mcp-apps-shell/src/shell/tools-proxy.ts +261 −0
  706. packages/hosts/mcp-apps-shell/src/smoke-render-entry.ts +43 −0
  707. packages/hosts/mcp-apps-shell/src/vite.test.ts +28 −0
  708. packages/hosts/mcp-apps-shell/src/vite.ts +411 −0
  709. packages/hosts/mcp-apps-shell/src/worker.test.ts +85 −0
  710. packages/hosts/mcp-apps-shell/src/worker.ts +75 −0
  711. packages/hosts/mcp-apps-shell/tsconfig.json +16 −0
  712. packages/hosts/mcp-apps-shell/vite.config.shell.ts +46 −0
  713. packages/hosts/mcp-apps-shell/vitest.config.ts +12 −0
  714. packages/hosts/mcp/package.json +14 −1
  715. packages/hosts/mcp/src/artifact-action.ts +87 −0
  716. packages/hosts/mcp/src/artifact-bindings.test.ts +398 −0
  717. packages/hosts/mcp/src/artifact-bindings.ts +419 −0
  718. packages/hosts/mcp/src/artifacts-tools.test.ts +2557 −0
  719. packages/hosts/mcp/src/browser-approval.ts +28 −0
  720. packages/hosts/mcp/src/create-artifact.ts +489 −0
  721. packages/hosts/mcp/src/envelope.test.ts +7 −0
  722. packages/hosts/mcp/src/envelope.ts +53 −19
  723. packages/hosts/mcp/src/in-memory-session-store.test.ts +54 −0
  724. packages/hosts/mcp/src/in-memory-session-store.ts +17 −1
  725. packages/hosts/mcp/src/namespace-search-tools.test.ts +222 −0
  726. packages/hosts/mcp/src/tool-call-code.ts +119 −0
  727. packages/hosts/mcp/src/tool-server.test.ts +228 −13
  728. packages/hosts/mcp/src/tool-server.ts +1364 −54
  729. packages/kernel/core/CHANGELOG.md +28 −0
  730. packages/kernel/core/package.json +2 −1
  731. packages/kernel/core/src/error-kind.test.ts +25 −0
  732. packages/kernel/core/src/error-kind.ts +37 −0
  733. packages/kernel/core/src/index.ts +1 −0
  734. packages/kernel/core/src/json-schema.ts +27 −8
  735. packages/kernel/core/src/strip-types.test.ts +1 −1
  736. packages/kernel/core/src/types.ts +3 −0
  737. packages/kernel/ir/src/registry.ts +2 −2
  738. packages/kernel/ir/src/serialize.ts +2 −2
  739. packages/kernel/runtime-deno-subprocess/src/deno-subprocess-worker.mjs +1 −1
  740. packages/kernel/runtime-deno-subprocess/src/deno-worker-process.ts +47 −2
  741. packages/kernel/runtime-deno-subprocess/src/fixtures/close-stdin-worker.mjs +37 −0
  742. packages/kernel/runtime-deno-subprocess/src/index.test.ts +68 −13
  743. packages/kernel/runtime-deno-subprocess/src/index.ts +78 −13
  744. packages/kernel/runtime-dynamic-worker/src/executor.ts +37 −3
  745. packages/kernel/runtime-dynamic-worker/src/invocation.test.ts +86 −2
  746. packages/kernel/runtime-dynamic-worker/src/module-template.ts +26 −6
  747. packages/kernel/runtime-quickjs/CHANGELOG.md +100 −0
  748. packages/kernel/runtime-quickjs/package.json +1 −1
  749. packages/kernel/runtime-quickjs/src/index.test.ts +50 −2
  750. packages/kernel/runtime-quickjs/src/index.ts +94 −21
  751. packages/kernel/runtime-quickjs/src/sealed-bundle.test.ts +113 −0
  752. packages/kernel/runtime-quickjs/src/sealed-bundle.ts +209 −0
  753. packages/kernel/runtime-workerd-subprocess/CHANGELOG.md +99 −0
  754. packages/kernel/runtime-workerd-subprocess/package.json +26 −0
  755. packages/kernel/runtime-workerd-subprocess/src/index.test.ts +251 −0
  756. packages/kernel/runtime-workerd-subprocess/src/index.ts +740 −0
  757. packages/kernel/runtime-workerd-subprocess/tsconfig.json +5 −6
  758. packages/kernel/runtime-workerd-subprocess/vitest.config.ts +7 −0
  759. packages/plugins/desktop-settings/CHANGELOG.md +98 −0
  760. packages/plugins/desktop-settings/package.json +1 −1
  761. packages/plugins/desktop-settings/src/client.tsx +5 −5
  762. packages/plugins/desktop-settings/src/server.ts +1 −1
  763. packages/plugins/encrypted-secrets/CHANGELOG.md +98 −0
  764. packages/plugins/encrypted-secrets/package.json +1 −1
  765. packages/plugins/encrypted-secrets/src/index.test.ts +53 −2
  766. packages/plugins/encrypted-secrets/src/index.ts +20 −13
  767. packages/plugins/encrypted-secrets/src/repartition-migration.test.ts +208 −0
  768. packages/plugins/encrypted-secrets/src/repartition-migration.ts +123 −0
  769. packages/plugins/example/CHANGELOG.md +98 −0
  770. packages/plugins/example/package.json +1 −1
  771. packages/plugins/file-secrets/CHANGELOG.md +100 −0
  772. packages/plugins/file-secrets/package.json +1 −1
  773. packages/plugins/file-secrets/src/data-dir.test.ts +115 −0
  774. packages/plugins/file-secrets/src/index.test.ts +208 −0
  775. packages/plugins/file-secrets/src/index.ts +170 −57
  776. packages/plugins/google/CHANGELOG.md +0 −143
  777. packages/plugins/google/package.json +0 −98
  778. packages/plugins/google/src/api/group.ts +0 −142
  779. packages/plugins/google/src/api/handlers.ts +0 −105
  780. packages/plugins/google/src/api/index.ts +0 −15
  781. packages/plugins/google/src/promise.ts +0 −1
  782. packages/plugins/google/src/react/AddGoogleSource.tsx +0 −178
  783. packages/plugins/google/src/react/GoogleAccountsPanel.tsx +0 −148
  784. packages/plugins/google/src/react/GoogleProductPicker.tsx +0 −363
  785. packages/plugins/google/src/react/atoms.ts +0 −30
  786. packages/plugins/google/src/react/client.ts +0 −13
  787. packages/plugins/google/src/react/index.ts +0 −12
  788. packages/plugins/google/src/react/plugin-client.tsx +0 −8
  789. packages/plugins/google/src/react/source-plugin.ts +0 −18
  790. packages/plugins/google/src/sdk/oauth-batches.test.ts +0 −98
  791. packages/plugins/google/src/sdk/oauth-batches.ts +0 −69
  792. packages/plugins/google/src/sdk/oauth-scopes.test.ts +0 −34
  793. packages/plugins/google/src/sdk/oauth-scopes.ts +0 −66
  794. packages/plugins/google/src/sdk/plugin.test.ts +0 −799
  795. packages/plugins/google/src/sdk/plugin.ts +0 −469
  796. packages/plugins/google/src/sdk/presets.test.ts +0 −29
  797. packages/plugins/google/src/sdk/product-picker-scopes.test.ts +0 −122
  798. packages/plugins/google/tsup.config.ts +0 −14
  799. packages/plugins/google/vitest.config.ts +0 −12
  800. packages/plugins/graphql/CHANGELOG.md +140 −0
  801. packages/plugins/graphql/README.md +2 −2
  802. packages/plugins/graphql/package.json +1 −1
  803. packages/plugins/graphql/src/react/AddGraphqlIntegration.tsx +8 −8
  804. packages/plugins/graphql/src/react/GraphqlAccountsPanel.tsx +3 −3
  805. packages/plugins/graphql/src/react/GraphqlIntegrationFields.tsx +1 −1
  806. packages/plugins/graphql/src/react/atoms.ts +3 −3
  807. packages/plugins/graphql/src/react/index.ts +2 −2
  808. packages/plugins/graphql/src/react/integration-plugin.ts +1 −1
  809. packages/plugins/graphql/src/react/plugin-client.tsx +1 −1
  810. packages/plugins/graphql/src/sdk/errors.ts +14 −0
  811. packages/plugins/graphql/src/sdk/index.ts +6 −1
  812. packages/plugins/graphql/src/sdk/introspect.ts +14 −1
  813. packages/plugins/graphql/src/sdk/invocation-timeout.test.ts +107 −0
  814. packages/plugins/graphql/src/sdk/invoke.ts +67 −33
  815. packages/plugins/graphql/src/sdk/multi-placement-auth.test.ts +50 −0
  816. packages/plugins/graphql/src/sdk/plugin.test.ts +359 −0
  817. packages/plugins/graphql/src/sdk/plugin.ts +334 −41
  818. packages/plugins/graphql/src/sdk/presets.ts +1 −1
  819. packages/plugins/graphql/src/sdk/types.ts +6 −0
  820. packages/plugins/keychain/CHANGELOG.md +98 −0
  821. packages/plugins/keychain/package.json +1 −1
  822. packages/plugins/mcp/CHANGELOG.md +160 −0
  823. packages/plugins/mcp/README.md +4 −4
  824. packages/plugins/mcp/package.json +6 −2
  825. packages/plugins/mcp/scripts/gen-call-tool-result-schema.ts +26 −0
  826. packages/plugins/mcp/src/api/group.ts +4 −0
  827. packages/plugins/mcp/src/api/handlers.ts +2 −0
  828. packages/plugins/mcp/src/react/AddMcpIntegration.tsx +19 −3
  829. packages/plugins/mcp/src/react/EditMcpIntegration.tsx +8 −5
  830. packages/plugins/mcp/src/react/McpAccountsPanel.tsx +3 −3
  831. packages/plugins/mcp/src/react/McpRemoteIntegrationFields.tsx +3 −3
  832. packages/plugins/mcp/src/react/McpSignInButton.tsx +2 −2
  833. packages/plugins/mcp/src/react/index.ts +2 −2
  834. packages/plugins/mcp/src/react/integration-plugin.tsx +5 −5
  835. packages/plugins/mcp/src/react/plugin-client.tsx +2 −2
  836. packages/plugins/mcp/src/sdk/call-tool-result-schema.gen.ts +395 −0
  837. packages/plugins/mcp/src/sdk/call-tool-result-schema.test.ts +16 −0
  838. packages/plugins/mcp/src/sdk/catalog-sync.test.ts +19 −0
  839. packages/plugins/mcp/src/sdk/client-module.ts +39 −0
  840. packages/plugins/mcp/src/sdk/cloudflare-codemode.test.ts +42 −0
  841. packages/plugins/mcp/src/sdk/cloudflare-codemode.ts +15 −0
  842. packages/plugins/mcp/src/sdk/connection-pool-key.test.ts +154 −0
  843. packages/plugins/mcp/src/sdk/connection-pool.test.ts +197 −0
  844. packages/plugins/mcp/src/sdk/connection-pool.ts +147 −0
  845. packages/plugins/mcp/src/sdk/connection-socket-release.test.ts +64 −0
  846. packages/plugins/mcp/src/sdk/connection.test.ts +85 −0
  847. packages/plugins/mcp/src/sdk/connection.ts +274 −37
  848. packages/plugins/mcp/src/sdk/describe-auth-methods.test.ts +37 −0
  849. packages/plugins/mcp/src/sdk/discover.ts +32 −21
  850. packages/plugins/mcp/src/sdk/elicitation.test.ts +56 −81
  851. packages/plugins/mcp/src/sdk/errors.ts +38 −0
  852. packages/plugins/mcp/src/sdk/health-missing-credential.test.ts +111 −0
  853. packages/plugins/mcp/src/sdk/http-status.test.ts +75 −0
  854. packages/plugins/mcp/src/sdk/http-status.ts +68 −11
  855. packages/plugins/mcp/src/sdk/invoke.test.ts +17 −6
  856. packages/plugins/mcp/src/sdk/invoke.ts +85 −27
  857. packages/plugins/mcp/src/sdk/multi-placement-auth.test.ts +6 −2
  858. packages/plugins/mcp/src/sdk/plugin.test.ts +106 −4
  859. packages/plugins/mcp/src/sdk/plugin.ts +195 −28
  860. packages/plugins/mcp/src/sdk/presets.ts +7 −4
  861. packages/plugins/mcp/src/sdk/probe-shape-real-servers.live.test.ts +1 −1
  862. packages/plugins/mcp/src/sdk/probe-shape.test.ts +90 −2
  863. packages/plugins/mcp/src/sdk/probe-shape.ts +51 −10
  864. packages/plugins/mcp/src/sdk/slack-connect-file.integration.test.ts +182 −0
  865. packages/plugins/mcp/src/sdk/slack-connect-file.test.ts +171 −0
  866. packages/plugins/mcp/src/sdk/slack-connect-file.ts +153 −0
  867. packages/plugins/mcp/src/sdk/stdio-connector.ts +4 −3
  868. packages/plugins/mcp/src/sdk/stdio-interrupt-cleanup.test.ts +112 −0
  869. packages/plugins/mcp/src/sdk/stdio-interrupt-test-server.ts +65 −0
  870. packages/plugins/mcp/src/sdk/stdio-negotiation-test-server.ts +24 −0
  871. packages/plugins/mcp/src/sdk/stdio-negotiation.test.ts +64 −0
  872. packages/plugins/mcp/src/sdk/testing-fixtures.test.ts +5 −3
  873. packages/plugins/mcp/src/sdk/types.ts +38 −1
  874. packages/plugins/mcp/src/testing/server.ts +78 −6
  875. packages/plugins/mcp/tsconfig.json +1 −1
  876. packages/plugins/microsoft/CHANGELOG.md +0 −141
  877. packages/plugins/microsoft/package.json +0 −98
  878. packages/plugins/microsoft/src/api/group.ts +0 −163
  879. packages/plugins/microsoft/src/api/handlers.ts +0 −144
  880. packages/plugins/microsoft/src/api/index.ts +0 −15
  881. packages/plugins/microsoft/src/promise.ts +0 −1
  882. packages/plugins/microsoft/src/react/AddMicrosoftSource.tsx +0 −155
  883. packages/plugins/microsoft/src/react/MicrosoftAccountsPanel.tsx +0 −118
  884. packages/plugins/microsoft/src/react/MicrosoftScopePicker.tsx +0 −286
  885. packages/plugins/microsoft/src/react/atoms.ts +0 −30
  886. packages/plugins/microsoft/src/react/client.ts +0 −13
  887. packages/plugins/microsoft/src/react/index.ts +0 −12
  888. packages/plugins/microsoft/src/react/plugin-client.tsx +0 −8
  889. packages/plugins/microsoft/src/react/source-plugin.ts +0 −18
.changeset/cli-credential-store-permissions.md+11−0
@@ -0,0 +1,11 @@
1+---
2+"executor": patch
3+---
4+
5+**The CLI's server-connection store is written owner-only, and two of its tests now actually run**
6+
7+`~/.executor/server-connections.json` holds live credentials for a hosted server — a bearer token, or an OAuth access token together with its long-lived refresh token, rewritten on every silent refresh. It was created with no explicit mode, so the process umask applied and it landed world-readable (0644 by default). Any other account on the machine — or anything that copies a home directory, such as a backup or a container layer — could read a durable credential until the user ran `executor logout`.
8+
9+It is now created `0600` with a follow-up `chmod`, matching what the local-server manifest already does for the sibling secret it keeps under `server-control/`. Both steps are needed: `mode` applies only on create, and the `chmod` covers rewriting a store that already exists with looser permissions — which is the common path here, since the file is rewritten on every token refresh.
10+
11+Separately, two tests in `server-profile.test.ts` were written as `it("…", () => Effect.gen(…))`. An `Effect` is not a thenable, so Vitest treated each as passing without ever running its body — a deliberately falsified assertion still passed. They are now `it.effect` and execute for real. No production behaviour was wrong; the tests simply were not checking it.
.changeset/concurrent-data-migration-stamps.md+5−0
@@ -0,0 +1,5 @@
1+---
2+"executor": patch
3+---
4+
5+Prevent concurrent SQLite data-migration runners from failing when another runner commits the same ledger stamp first.
.changeset/config.json+2−2
@@ -18,7 +18,8 @@ · 17 unmodified lines
1818 "@executor-js/plugin-openapi",
1919 "@executor-js/plugin-example",
2020 "@executor-js/plugin-desktop-settings",
21− "@executor-js/desktop"
21+ "@executor-js/desktop",
22+ "@executor-js/local"
2223 ]
2324 ],
2425 "linked": [],
@@ -29,7 +30,6 @@ · 4 unmodified lines
2930 "onlyUpdatePeerDependentsWhenOutOfRange": true
3031 },
3132 "ignore": [
32− "@executor-js/local",
3333 "@executor-js/host-mcp",
3434 "@executor-js/ir",
3535 "@executor-js/runtime-deno-subprocess",
.changeset/connection-edit-sheet-agent-preview.md+9−0
@@ -0,0 +1,9 @@
1+---
2+"@executor-js/react": patch
3+---
4+
5+**The connection edit sheet now previews what agents actually read**
6+
7+The "What agents see" preview in the connection edit sheet rendered a `- \`<prefix>\` — <description>`inventory line. That line left the`execute`tool description when the inventory was slimmed to bare integration slugs, so the preview showed text no agent reads. The account label was also marked "Display-only", but`connections.list` returns it to agents alongside the description.
8+
9+The preview now mirrors the `connections.list` item for the connection (`name`, `identityLabel`, `description`), and the sheet copy says that both fields are agent-visible while the callable name stays as it was at connect time.
.changeset/desktop-settings-owner-only.md+9−0
@@ -0,0 +1,9 @@
1+---
2+"executor": patch
3+---
4+
5+**The desktop settings store is written owner-only**
6+
7+`settings.json` holds `serverProfiles`, which carries a remote server's credential — a bearer token, or a basic-auth username and password — for any "Custom server" the user connects to. `conf` (under electron-store) defaults to `configFileMode: 0o666`, so with no explicit mode the file was created `0644`. On Linux, where `~/.config` is not reliably `0700`, that is readable by every other account on the machine. macOS is protected by `~/Library` being `0700` and Windows by ACLs, so this is primarily a Linux-desktop exposure — but owner-only credential files are already this app's standard: `local-auth.ts` writes `auth.json` at `0o600`, and the sidecar manifest is chmodded the same way.
8+
9+One option is sufficient here, rather than the mode-plus-chmod pair used elsewhere: `atomically` chmods the temp inode only when the requested mode differs from its own default, and the atomic rename then carries the tight mode onto an already-loose file. Verified against the installed `conf` — with no option a fresh file lands `0644`; with `configFileMode: 0o600` a fresh file lands `0600` and an existing `0644` file becomes `0600` on the next write.
.changeset/graph-slice-urls-first-class.md+5−0
@@ -0,0 +1,5 @@
1+---
2+"@executor-js/plugin-openapi": patch
3+---
4+
5+Make Microsoft Graph slice URLs first-class spec sources instead of a hidden substitution. Catalog tiles now point directly at the slice release assets, the stored specUrl is exactly what gets fetched, and selection narrowing travels visibly in the URL fragment; requesting the upstream monolith URL fetches the monolith, never a silently swapped slice.
.changeset/graph-spec-slices.md+5−0
@@ -0,0 +1,5 @@
1+---
2+"@executor-js/plugin-openapi": patch
3+---
4+
5+Serve Microsoft Graph preset selections from precomputed slice release assets instead of the 43MB upstream monolith. The monolith fetch almost never survives a 128MB Workers isolate (production traces show one completion in 30 days), so covered selections — every catalog preset, plus any combination within the default bundle — now read a 4–19MB filtered document built offline by the graph-slices workflow, with the monolith path kept only as a fallback and for full-graph/custom-scope selections.
.changeset/host-evaluate-timeout.md+0−5
@@ -1,5 +0,0 @@
1−---
2−"executor": patch
3−---
4−
5−Back-stop code execution with a host-side timeout so a wedged sandbox delivers a descriptive error instead of hanging silently.
.changeset/lazy-mcp-client-module.md+5−0
@@ -0,0 +1,5 @@
1+---
2+"@executor-js/plugin-mcp": patch
3+---
4+
5+Load the MCP client SDK lazily on first outbound connection instead of at module evaluation. Runtimes that bundle the plugin (notably Cloudflare Workers) no longer pay the client package's module-eval memory and CPU on startup or on code paths that never dial an MCP server.
.changeset/mcp-abort-releases-response-body.md+7−0
@@ -0,0 +1,7 @@
1+---
2+"@executor-js/plugin-mcp": patch
3+---
4+
5+**Closing a remote MCP connection now ends its streamable-http SSE request**
6+
7+On a supplied `httpClientLayer`, the fetch adapter wired the caller's `AbortSignal` only to the pending response promise, never to the response body, so closing a connection left the long-lived `GET` channel in flight forever — one abandoned request per dial. Under Bun each holds one of the 256 concurrent-request slots, so a long-running process eventually exhausts the pool and every connection starts failing with `MCP discovery timed out after 15000ms`. The response stream is now interrupted when the signal aborts.
.changeset/mcp-health-missing-credential.md+11−0
@@ -0,0 +1,11 @@
1+---
2+"executor": patch
3+---
4+
5+**An MCP health check no longer reports `healthy` when the connection's credential is missing**
6+
7+Rendering skips an auth placement whose value is unresolved — that is the renderer's documented behaviour, and callers own the missing-value policy. The MCP health check had no such policy, so it dialled unauthenticated, and any server that lists tools without auth answered. `discoverTools` succeeding maps straight to `healthy`, so a connection whose credential was gone reported as healthy.
8+
9+Health status is the signal telling a user to re-authenticate, which makes `healthy` the one answer it must never give in that state. The check now reports `expired` with the unresolved input names, mirroring the OpenAPI health check, which already did exactly this.
10+
11+The MCP tool-invocation path already refused for the same reason. `resolveTools` is deliberately left alone — its own comment records that discovery tolerating unresolved credentials is intended, since an open server lists tools unauthenticated.
.changeset/mcp-namespace-search-tools.md+8−0
@@ -0,0 +1,8 @@
1+---
2+"@executor-js/execution": patch
3+"executor": patch
4+---
5+
6+**Opt-in per-integration search tools on the MCP surface**
7+
8+Connecting with `?search_tools=true` (stdio: `executor mcp --search-tools`) adds one minimally-described `search_<integration>` MCP tool per connected integration, so the integration namespaces reach the model as tool names it can see without calling anything. Each call routes through the same flow as `tools.search({ namespace })` inside `execute`, and the tool list comes from the same inventory the `execute` description shows. Off by default; a clean endpoint URL is unchanged.
.changeset/mcp-pool-key-credential-retention.md+11−0
@@ -0,0 +1,11 @@
1+---
2+"executor": patch
3+---
4+
5+**The MCP connection pool no longer keeps credentials in its cache key**
6+
7+A pooled remote MCP session is looked up by a key describing the connection's identity, and that key included the connection's resolved credential values — plus the headers and query params those same secrets had already been rendered into. The key is retained as a `Map` key for the pool's lifetime, so the secret stayed readable in process memory long after the call that needed it had finished, with nothing left to read it.
8+
9+The key is now the SHA-256 digest of that identity rather than the identity itself. Reuse is unchanged, because equal identities still produce equal keys, and separation is unchanged too: a rotated access token, a different rendered auth header and a credential carried in a query param each still dial a fresh session instead of reusing one authenticated as somebody else. Hashing the whole identity rather than only the fields known to be sensitive means a field added later is covered without anyone having to remember it carries a secret.
10+
11+Nothing reads the key back — the pool only compares it, and it reaches no log, span or error message — so nothing observable changes.
.changeset/mcp-priming-reconnect.md+0−5
@@ -1,5 +0,0 @@
1−---
2−"executor": patch
3−---
4−
5−Let stock MCP clients auto-reconnect and recover a tool result when a POST stream drops mid-call.
.changeset/mcp-session-idle-runtime-reclaim.md+15−0
@@ -0,0 +1,15 @@
1+---
2+"executor": patch
3+---
4+
5+**Idle MCP session runtimes are actually reclaimed**
6+
7+The MCP session Durable Object has an idle timeout that disposes a session's execution runtime — the execution engine and its executor closure, the built tool catalog, and a live database handle — once the session has gone quiet. That timeout never ran.
8+
9+The session arms an idle alarm on every request. The agents framework independently recomputes the Durable Object alarm from its own schedule table and keep-alive refcount, and when it finds neither it does not leave the alarm alone: it deletes it. It releases the last keep-alive reference at the end of every ordinary tool call, from a `waitUntil` that runs just after the response goes out — so the idle alarm the session had armed moments earlier was erased, and a session that had just served a request was left with no alarm at all. Its runtime then stayed resident until the platform evicted the whole object.
10+
11+Durable Objects are colocated many-to-one onto an isolate with a single heap, so runtimes that are never reclaimed accumulate there. When the heap is exhausted the allocation that fails is whichever comes next, anywhere in the isolate — which is why the failure tended to surface from storage rather than from the runtimes that had consumed the memory.
12+
13+The idle deadline belongs to the session, not to the framework's scheduler, so it is now re-asserted after the framework has arranged whatever it needs — and only while a runtime is actually resident, since once there is nothing left to reclaim the framework's answer is correct.
14+
15+Disposal also now emits a span carrying a per-isolate resident-runtime gauge, alongside the same gauge on runtime build, so the reclaim can be confirmed in production rather than inferred.
.changeset/mcp-sse-result-replay.md+0−5
@@ -1,5 +0,0 @@
1−---
2−"executor": patch
3−---
4−
5−Preserve MCP tool results across dropped streamable HTTP SSE connections.
.changeset/oauth-dcr-application-type.md+9−0
@@ -0,0 +1,9 @@
1+---
2+"executor": patch
3+---
4+
5+**Fix: declare the OAuth application type during dynamic client registration**
6+
7+Dynamic OAuth registrations now identify HTTPS callbacks as web applications
8+and loopback HTTP callbacks as native applications. This lets strict OAuth
9+servers validate Executor's redirect URI against the correct client type.
.changeset/oauth-popup-localstorage-residue.md+9−0
@@ -0,0 +1,9 @@
1+---
2+"executor": patch
3+---
4+
5+**The OAuth popup clears its result out of `localStorage` after handing it over**
6+
7+The popup writes its result to `localStorage` as the fallback completion channel, because `postMessage` is severed when a provider's consent page sets COOP and `BroadcastChannel` can be partitioned or raced by the auto-close. Nothing removed that entry afterwards, so the payload — which carries the identity label, an email, and on failure the error preview — stayed parked in the user's browser profile.
8+
9+The entry is now cleared once the handover has had time to land, and on `pagehide` as a backstop — the failure page never auto-closes so the user can read the error, and closing it by hand would otherwise cancel the pending timer and strand the entry. This cannot cost a listener the result: a `storage` event captures `newValue` at dispatch, so an opener that has been notified already holds it.
.changeset/onepassword-service-account-token-lifetime.md+11−0
@@ -0,0 +1,11 @@
1+---
2+"executor": patch
3+---
4+
5+**The 1Password service-account token is cleared from the op-js global after each call**
6+
7+`@1password/op-js` keeps the service-account token on a module-level CLI instance (`cli.serviceAccountToken`) and reads it when it spawns `op`. The CLI backend set that global before each call and never cleared it, so one reachable reference to the token stayed live for the rest of the process.
8+
9+It is now cleared as soon as the call that needed it is done, on success, failure and interruption alike. Authentication is unaffected: every read and write of that global already happens inside the backend's semaphore, so the next operation re-sets the token before it spawns anything.
10+
11+This is hygiene rather than a boundary change. No unrelated `op` child ever received a stale token — every call routes through the same critical section that sets the correct one immediately before invoking — and the token is separately persisted in plaintext in the plugin's config blob, so an attacker's reach is unchanged. What it removes is a long-lived reachable reference that nothing needed to keep.
.changeset/openapi-streaming-preview.md+5−0
@@ -0,0 +1,5 @@
1+---
2+"@executor-js/plugin-openapi": patch
3+---
4+
5+Preview OpenAPI spec-format selections (Microsoft Graph) through the streaming structural-split path instead of a whole-document parse, and guard generic whole-document parses by parsed-tree size (line count for block YAML, text size for JSON). Previewing a Graph preset URL previously parsed the 43MB source whole and killed the 128MB Workers isolate mid-request, surfacing as an empty 503; it now streams within budget, and oversized generic specs fail with an actionable error instead of taking down the isolate.
.changeset/slim-search-tools.md+6−0
@@ -0,0 +1,6 @@
1+---
2+"@executor-js/execution": patch
3+"executor": patch
4+---
5+
6+Slim the per-integration `search_<integration>` tool definitions to under half their size: one shared one-line description (the tool name already carries the namespace) and a single bare `query` parameter, dropping the `limit`/`offset` knobs. A session pays for these definitions once per connected integration, so the surface now costs ~2k tokens instead of ~5k at 30 integrations; paging through a namespace belongs in `execute`.
.changeset/storage-error-shaping.md+8−0
@@ -0,0 +1,8 @@
1+---
2+"@executor-js/sdk": patch
3+"@executor-js/api": patch
4+---
5+
6+Build `StorageError.message` from the call-site label plus the driver's error code instead of the driver's raw text. The driver text is drizzle's `Failed query: <sql>\nparams: <bound values>`, so error reporting grouped one storage defect by statement shape and printed bound parameters into issue titles. The full driver error stays on `cause`.
7+
8+Add `StorageConnectionError`, a `StorageFailure` variant for postgres.js connection faults (`CONNECTION_ENDED`, `CONNECTION_CLOSED`, `CONNECTION_DESTROYED`, `CONNECT_TIMEOUT`, `ECONNREFUSED`, `ECONNRESET`) and workerd's cross-request I/O rejection. It carries the fault `code` and a `retryable` flag so a lost socket can be told apart from a pool-lifetime bug.
.changeset/tools-proxy-enumeration-hint.md+0−5
@@ -1,5 +0,0 @@
1−---
2−"executor": patch
3−---
4−
5−Throw a guidance error when sandbox code enumerates the `tools` proxy (`Object.keys`, spread, `for...in`) instead of returning an empty list, pointing agents at `tools.search()`.
.changeset/worker-bundler-staging-contract.md+11−0
@@ -0,0 +1,11 @@
1+---
2+"executor": patch
3+---
4+
5+**The packed Worker toolchain is verified at build time, and an incomplete copy is now reported instead of silently ignored**
6+
7+`@cloudflare/worker-bundler` cannot live inside the compiled binary: bunfs has no `node_modules`, so a bare specifier is unresolvable there by construction. The build instead copies the package's `dist/` next to the executable and `native-bindings.ts` publishes that path as `EXECUTOR_WORKER_BUNDLER_DIR` for consumers to load from.
8+
9+That handoff was described in two places that were free to drift, and did. The build writes `dist/index.bundled.js` — the entry consumers actually load, packed so it has no bare imports of its own — while the runtime check only looked for `dist/index.js` and `dist/esbuild.wasm`. Nothing verified the staged copy after the compile, so a partial staging produced a binary that looked fine on the build machine and failed on the user's, at startup. Worse, the runtime check failed open: when a file was missing it silently declined to set the environment variable, leaving a consumer to fall through to the bare specifier and crash.
10+
11+The required file list is now one shared contract used by both sides, so they cannot disagree. The build asserts the staged copy after compiling each target — every required file present, a size floor on the packed entry, and the `\0asm` magic on the wasm so a truncated copy cannot pass — turning a packaging slip into a failed build rather than a broken install. At runtime, a directory that is present but incomplete is reported on stderr naming the missing files, instead of being swallowed. An absent directory stays quiet, since that is the normal non-packaged path.
.changeset/workos-blip-session-survival.md+0−5
@@ -1,5 +0,0 @@
1−---
2−"executor": patch
3−---
4−
5−Treat a transient WorkOS outage during the MCP live-membership check as a retryable 503 instead of a Forbidden that destroys the session.
.claude/skills/emulate/SKILL.md+15−12
@@ -32,18 +32,22 @@ const github = await createEmulator({ service: "github", port: 4501 }); · 31 unmodified lines
3232 `servers`) when a proxy fronts the emulator — the bind stays on `port`.
3333
3434 **Attach to a running one** (another process, or hosted on Cloudflare with
35−Durable Object state at `https://<service>.emulators.dev` /
36−`https://<service>.<instance>.emulators.dev` — catalog at
37−`GET https://emulators.dev/_emulate/services`):
35+Durable Object state at `https://<service>.<instance>.emulators.dev` —
36+catalog at `GET https://emulators.dev/_emulate/services`). Service hosts
37+(`https://<service>.emulators.dev`) are control plane only, with no shared
38+default instance behind them: create a private instance first and connect to
39+the returned `providerBaseUrl`.
3840
3941 ```ts
4042 import { connectEmulator } from "@executor-js/emulate";
41−const resend = await connectEmulator({ baseUrl: "https://resend.emulators.dev" });
43+const created = await fetch("https://resend.emulators.dev/_emulate/instances", { method: "POST" });
44+const { providerBaseUrl } = await created.json();
45+const resend = await connectEmulator({ baseUrl: providerBaseUrl });
4246 // optional: { service: "resend" } verifies the manifest on connect
4347 ```
4448
45−Create a private hosted instance with `POST /_emulate/instances` on the
46−service host.
49+In e2e scenarios use `createEmulatorInstance` from
50+`e2e/src/emulator-instance.ts`, which wraps this.
4751
4852 ## The typed control-plane client
4953
@@ -113,12 +117,11 @@ The emulate repo's own `AGENTS.md` / `README.md` carry the current build, · 63 unmodified lines
113117 publish, and deploy commands (npm + Cloudflare creds are in 1Password). Read
114118 them there rather than memorizing flags here — they move.
115119
116−**A hot deploy can redden other people's e2e.** `*.emulators.dev` service
117−hosts are shared infrastructure; a control-plane regression there has failed
118−unrelated PRs' suites before. When a scenario needs isolation or a behavior
119−that isn't deployed yet, pin to a published package version or mint a private
120−per-run instance (`POST /_emulate/instances`) instead of mutating the shared
121−service host.
120+**A hot deploy can redden other people's e2e.** The `emulate-hosts` worker
121+behind `*.emulators.dev` is shared infrastructure; a control-plane regression
122+there has failed unrelated PRs' suites before. Scenarios always run on private
123+per-run instances (`POST /_emulate/instances`); when a scenario needs behavior
124+that isn't deployed yet, pin to a published package version.
122125
123126 ## Gotchas
124127
.claude/skills/prod-telemetry/SKILL.md+18−1
@@ -29,6 +29,20 @@ join the same traces via traceparent). · 28 unmodified lines
2929
3030 **Span names worth querying** (and their custom attrs):
3131
32+- `mcp.execute` / `mcp.execute.resume` — `mcp.execute.mode`
33+ (`pausable`/`inline`), `mcp.execute.code_length`, and
34+ `mcp.execute.outcome` (`ok`/`fail`/`paused`) with, on failures,
35+ `mcp.execute.error_kind` (`type_error` | `reference_error` |
36+ `syntax_error` | `range_error` | `tool_error` | `timeout` |
37+ `resource_limit` | `serialization_error` | `thrown` | `unknown`).
38+ Sandbox script failures ride the MCP success channel, so `status.code`
39+ stays OK — filter on these attributes, not span status. Spans from
40+ before the attributes shipped carry neither; absence is not success.
41+ Also `mcp.execute.result_chars` (compact-JSON size of the returned
42+ value, pre-truncation; -1 = unmeasurable), `mcp.execute.log_chars`,
43+ `mcp.execute.emitted` — the dump-vs-narrow signal (the model preview
44+ truncates at 30k chars, so `result_chars > 30000` means the model tried
45+ to pull a truncated blob into context).
3246 - `executor.tool.execute` — `mcp.tool.name` (full address), and since
3347 PR #992: `executor.tool.outcome` (`ok`/`fail`),
3448 `executor.tool.error_code`, `executor.tool.error_status`,
@@ -39,7 +53,10 @@ join the same traces via traceparent). · 4 unmodified lines
3953 `base_url`, and since PR #992 `http.status_code`.
4054 - `mcp.request` (outer) — `mcp.auth.organization_id`,
4155 `mcp.auth.account_id`, `mcp.tool.name`, CF edge fields (`cf.country`…),
42− MCP client fingerprint (`mcp.client.name`…).
56+ MCP client fingerprint (`mcp.client.name`…), and on managed-cloud
57+ `execute`/`execute-action` calls `mcp.execute.code` (the script itself,
58+ capped at 10k chars — cloud-only content capture; local/self-host
59+ telemetry never records content).
4360
4461 **Recipe — error signatures by class (the daily-digest query):**
4562
.github/workflows/ci.yml+144−57
@@ -10,6 +10,18 @@ concurrency: · 9 unmodified lines
1010 group: ci-${{ github.ref }}
1111 cancel-in-progress: true
1212
13+# CI is not a user. Without this, executor's own product telemetry reports to the
14+# production PostHog project from every e2e shard: the analytics layer
15+# (integration_added, execution_completed, artifact_*) and the
16+# integrations-registry fetch, which lands as a `hit` event. Each shard is a
17+# fresh container, so it mints a fresh anonymous id and arrives on a fresh
18+# runner IP — inflating machine and user counts by roughly an order of
19+# magnitude. Both opt-outs are the packages' documented CI/test hooks.
20+env:
21+ DO_NOT_TRACK: "1"
22+ EXECUTOR_DISABLE_ANALYTICS: "1"
23+ EXECUTOR_DISABLE_INTEGRATIONS_FETCH: "1"
24+
1325 jobs:
1426 changes:
1527 name: Changed paths
@@ -18,6 +30,7 @@ jobs: · 2 unmodified lines
1830 outputs:
1931 desktop_smoke: ${{ github.event_name != 'pull_request' || steps.filter.outputs.desktop_smoke == 'true' }}
2032 selfhost_docker_smoke: ${{ github.event_name != 'pull_request' || steps.filter.outputs.selfhost_docker_smoke == 'true' }}
33+ cloud_closure: ${{ github.event_name != 'pull_request' || steps.filter.outputs.cloud_closure == 'true' }}
2134 steps:
2235 - uses: actions/checkout@v4
2336
@@ -41,6 +54,14 @@ jobs: · 17 unmodified lines
4154 - "packages/kernel/runtime-quickjs/**"
4255 - "packages/plugins/**"
4356 - "packages/react/**"
57+ cloud_closure:
58+ - ".github/workflows/**"
59+ - "bun.lock"
60+ - "package.json"
61+ - "turbo.json"
62+ - "apps/cloud/**"
63+ - "packages/**"
64+
4465 selfhost_docker_smoke:
4566 - ".github/workflows/**"
4667 - ".dockerignore"
@@ -136,17 +157,26 @@ jobs: · 89 unmodified lines
136157
137158 - run: bun run typecheck
138159
160+ # The committed routeTree.gen.ts files are generated but checked in, and
161+ # they carry @ts-nocheck — so the typecheck above structurally CANNOT see
162+ # a route that the contract declares and the tree lacks. That drift has
163+ # already shipped a live 404 once. Regenerate and diff instead.
164+ - run: bun run check:routes
165+
139166 test:
140167 name: Test
141− runs-on: blacksmith-4vcpu-ubuntu-2404
168+ runs-on: blacksmith-16vcpu-ubuntu-2404
142169 timeout-minutes: 15
143− # Tuned for Blacksmith's 4 vCPU runners.
170+ # Run eight independent packages at once and cap each Vitest process at two
171+ # workers. This uses all 16 vCPUs without every nested runner seeing the
172+ # whole machine and oversubscribing it.
144173 env:
145174 TURBO_API: ${{ vars.TURBO_API }}
146175 TURBO_TEAM: ${{ vars.TURBO_TEAM }}
147176 TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
148177 TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
149− TURBO_TEST_CONCURRENCY: 4
178+ TURBO_TEST_CONCURRENCY: 8
179+ VITEST_MAX_WORKERS: 2
150180 steps:
151181 - uses: actions/checkout@v4
152182
@@ -178,6 +208,8 @@ jobs: · 25 unmodified lines
178208 # no signal. This is just a few file reads plus greps, so ~zero cost.
179209 - run: bun run check:patches
180210
211+ - run: bun run --cwd e2e test:ci-shard
212+
181213 - run: bun run test
182214
183215 e2e:
@@ -186,14 +218,41 @@ jobs: · 2 unmodified lines
186218 fail-fast: false
187219 matrix:
188220 include:
189− # Each cloud shard boots its own fresh dev stack. On 4 vCPU runners,
190− # four fatter shards keep the longest shard below selfhost while saving
191− # four runner boots and four warm cache restores.
192− - { target: cloud, shard: 1/4, shard-name: 1of4 }
193− - { target: cloud, shard: 2/4, shard-name: 2of4 }
194− - { target: cloud, shard: 3/4, shard-name: 3of4 }
195− - { target: cloud, shard: 4/4, shard-name: 4of4 }
196− - target: selfhost
221+ # The planner assigns every file exactly once using recorded slow-file
222+ # durations plus a conservative weight for new tests. The cloud DB's
223+ # connection teardown and concurrent socket protocol have dedicated
224+ # regression tests; these shards balance wall clock, not hide retries.
225+ - { target: cloud, shard-index: 1, shard-name: 1of16 }
226+ - { target: cloud, shard-index: 2, shard-name: 2of16 }
227+ - { target: cloud, shard-index: 3, shard-name: 3of16 }
228+ - { target: cloud, shard-index: 4, shard-name: 4of16 }
229+ - { target: cloud, shard-index: 5, shard-name: 5of16 }
230+ - { target: cloud, shard-index: 6, shard-name: 6of16 }
231+ - { target: cloud, shard-index: 7, shard-name: 7of16 }
232+ - { target: cloud, shard-index: 8, shard-name: 8of16 }
233+ - { target: cloud, shard-index: 9, shard-name: 9of16 }
234+ - { target: cloud, shard-index: 10, shard-name: 10of16 }
235+ - { target: cloud, shard-index: 11, shard-name: 11of16 }
236+ - { target: cloud, shard-index: 12, shard-name: 12of16 }
237+ - { target: cloud, shard-index: 13, shard-name: 13of16 }
238+ - { target: cloud, shard-index: 14, shard-name: 14of16 }
239+ - { target: cloud, shard-index: 15, shard-name: 15of16 }
240+ - { target: cloud, shard-index: 16, shard-name: 16of16 }
241+ - { target: selfhost, shard-index: 1, shard-name: 1of10 }
242+ - { target: selfhost, shard-index: 2, shard-name: 2of10 }
243+ - { target: selfhost, shard-index: 3, shard-name: 3of10 }
244+ - { target: selfhost, shard-index: 4, shard-name: 4of10 }
245+ - { target: selfhost, shard-index: 5, shard-name: 5of10 }
246+ - { target: selfhost, shard-index: 6, shard-name: 6of10 }
247+ - { target: selfhost, shard-index: 7, shard-name: 7of10 }
248+ - { target: selfhost, shard-index: 8, shard-name: 8of10 }
249+ - { target: selfhost, shard-index: 9, shard-name: 9of10 }
250+ - { target: selfhost, shard-index: 10, shard-name: 10of10 }
251+ # Local files own their server, browser and data directory. Separate
252+ # runners preserve that isolation while removing its 69-second serial
253+ # lane from the two-minute critical path.
254+ - { target: local, shard-index: 1, shard-name: 1of2 }
255+ - { target: local, shard-index: 2, shard-name: 2of2 }
197256 runs-on: blacksmith-4vcpu-ubuntu-2404
198257 timeout-minutes: 30
199258 steps:
@@ -229,26 +288,31 @@ jobs: · 29 unmodified lines
229288
230289 # Install from e2e so bunx resolves ITS pinned playwright (the version
231290 # the tests run against) rather than floating to the latest.
291+ # Blacksmith's Ubuntu image carries the official GitHub runner system
292+ # dependencies. Restore the pinned browser binaries without apt-updating
293+ # every matrix machine.
232294 - name: Install Playwright Chromium
233− run: bunx playwright install --with-deps chromium chromium-headless-shell
295+ run: bunx playwright install chromium chromium-headless-shell
234296 working-directory: e2e
235297
236− # The globalsetup boots the target's own dev server (ports are claimed
237− # per checkout, so this is hermetic) and tears it down after the run.
238− # --retry=2: browser scenarios can still hit isolated waitFor timeouts
239− # (single-test waitFor timeouts, not systemic failures); a retry on the
240− # same booted stack clears them.
241− - name: Run cloud scenarios
298+ # Each target either boots its own shared dev server or lets each file own
299+ # its server. Ports and data paths are hermetic in both cases.
300+ # Do not retry scenarios: retries hide flakes and multiply slow timeout
301+ # failures. The fixtures and process lifecycle are deterministic enough
302+ # that the first result is the result.
303+ - name: Run cloud shard
242304 if: matrix.target == 'cloud'
243305 env:
244306 MCP_SESSION_TIMEOUT_MS: "3000"
245− MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS: "6000"
246− run: bunx vitest run --project cloud --retry=2 ${{ matrix.shard && format('--shard={0}', matrix.shard) || '' }}
307+ # Still 18x shorter than production, but long enough for a cold Vite
308+ # resume route to compile under a fully loaded CI runner.
309+ MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS: "30000"
310+ run: bun scripts/run-ci-shard.ts cloud ${{ matrix['shard-index'] }}
247311 working-directory: e2e
248312
249− - name: Run selfhost scenarios
250− if: matrix.target == 'selfhost'
251− run: bunx vitest run --project selfhost --retry=2
313+ - name: Run scenarios
314+ if: matrix.target != 'cloud'
315+ run: bun scripts/run-ci-shard.ts ${{ matrix.target }} ${{ matrix['shard-index'] }}
252316 working-directory: e2e
253317
254318 # Failed runs keep their trace.zip / session.mp4 / step screenshots in
@@ -261,13 +325,23 @@ jobs: · 6 unmodified lines
261325 path: e2e/runs/
262326 retention-days: 7
263327
264− e2e-local:
265− name: E2E (stdio MCP)
266− # Skipped on pull_request: the local scenario boots a real `executor web`
267− # plus a browser and is currently flaky on PRs. Still runs on push to main.
268− if: github.event_name != 'pull_request'
328+ cloud-closure:
329+ name: Cloud evaluated closure
330+ needs: changes
331+ if: needs.changes.outputs.cloud_closure == 'true'
269332 runs-on: blacksmith-4vcpu-ubuntu-2404
270333 timeout-minutes: 20
334+ env:
335+ TURBO_API: ${{ vars.TURBO_API }}
336+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
337+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
338+ TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
339+ # Every cold isolate evaluates this closure before it can answer a
340+ # request, and it only ever grows by accident - a barrel export or a new
341+ # module-scope import quietly pulls megabytes into the server graph. The
342+ # budget is a ratchet just above today's size, not a discovered limit:
343+ # when it trips, make the new dependency lazy rather than raising it.
344+ START_CLOSURE_BUDGET_MB: 13.5
271345 steps:
272346 - uses: actions/checkout@v4
273347
@@ -283,41 +357,17 @@ jobs: · 9 unmodified lines
283357 restore-keys: |
284358 ${{ runner.os }}-bun-1.3.11-
285359
286− # The local scenarios boot a real `executor web` (which spawns a Node
287− # sidecar) and some drive a browser, so pin Node 24 and install Chromium.
288360 - uses: actions/setup-node@v4
289361 with:
290362 node-version: 24
291363
292364 - run: bun install --frozen-lockfile
293365
294− - name: Cache Playwright browsers
295− uses: actions/cache@v4
296− with:
297− path: ~/.cache/ms-playwright
298− key: ${{ runner.os }}-playwright-1.60.0
299− restore-keys: |
300− ${{ runner.os }}-playwright-
366+ - run: bun run build
367+ working-directory: apps/cloud
301368
302− # `chromium` and the new `chromium-headless-shell` ship as separate
303− # downloads; the browser-driven scenarios launch the headless shell.
304− # Install from e2e so bunx resolves ITS pinned playwright (the version the
305− # tests run against) rather than floating to the latest, which would fetch
306− # a browser build the test runtime does not look for.
307− - name: Install Playwright Chromium
308− run: bunx playwright install --with-deps chromium chromium-headless-shell
309− working-directory: e2e
310−
311− # The `local` project is excluded from the default `test` chain (each
312− # scenario boots its own `executor web`). Run just the stdio MCP scenario
313− # here: it is the auto-connect / env-as-secret regression guard, and
314− # running it alone avoids the boot-resource accumulation and the
315− # pre-existing browser flakiness of the rest of the local suite. Expanding
316− # to the full `local` project (bun run test:local) is a follow-up once
317− # those are stabilized.
318− - name: Run the stdio MCP scenario
319− run: bunx vitest run --project local local/stdio-mcp.test.ts
320− working-directory: e2e
369+ - run: node scripts/start-closure.mjs
370+ working-directory: apps/cloud
321371
322372 desktop-smoke:
323373 name: Desktop smoke build
@@ -369,7 +419,13 @@ jobs: · 45 unmodified lines
369419 - name: Run sidecar outside the workspace
370420 working-directory: apps/desktop
371421 run: |
372− docker run --rm -d --name sidecar-smoke -p 45841:45841 -e HOME=/tmp \
422+ # Containers do not inherit the job environment. Keep the compiled
423+ # product smoke from becoming a production analytics/integrations hit.
424+ docker run --rm -d --name sidecar-smoke -p 45841:45841 \
425+ -e HOME=/tmp \
426+ -e DO_NOT_TRACK=1 \
427+ -e EXECUTOR_DISABLE_ANALYTICS=1 \
428+ -e EXECUTOR_DISABLE_INTEGRATIONS_FETCH=1 \
373429 -v "$PWD/resources/executor:/opt/executor:ro" \
374430 debian:bookworm-slim /opt/executor/executor daemon run --foreground \
375431 --port 45841 --hostname 0.0.0.0 --auth-token=ci-smoke
@@ -414,4 +470,35 @@ jobs: · 38 unmodified lines
414470 context: .
415471 file: apps/host-selfhost/Dockerfile
416472 push: false
473+ load: true
417474 tags: executor-selfhost:ci
475+
476+ # Building alone doesn't catch startup crashes (e.g. a runtime-resolved
477+ # package missing from the packaged runtime), so boot the container and
478+ # require a healthy /api/health before calling the image good.
479+ - name: Boot image and check health
480+ shell: bash
481+ run: |
482+ set -euo pipefail
483+ docker run -d --name selfhost-smoke -p 4788:4788 \
484+ -e BETTER_AUTH_SECRET="$(openssl rand -hex 32)" \
485+ -e EXECUTOR_BOOTSTRAP_ADMIN_EMAIL=smoke@example.com \
486+ -e EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD="$(openssl rand -hex 16)" \
487+ -e EXECUTOR_WEB_BASE_URL=http://localhost:4788 \
488+ executor-selfhost:ci
489+ for i in $(seq 1 30); do
490+ if [ "$(docker inspect -f '{{.State.Running}}' selfhost-smoke)" != "true" ]; then
491+ echo "container exited during startup"
492+ docker logs selfhost-smoke
493+ exit 1
494+ fi
495+ if curl -fsS http://localhost:4788/api/health >/dev/null 2>&1; then
496+ echo "healthy after ${i}s"
497+ docker logs selfhost-smoke
498+ exit 0
499+ fi
500+ sleep 1
501+ done
502+ echo "health check never passed"
503+ docker logs selfhost-smoke
504+ exit 1
.github/workflows/deploy.yml+29−1
@@ -9,6 +9,16 @@ concurrency: · 8 unmodified lines
99 group: deploy-production
1010 cancel-in-progress: false
1111
12+# CI is not a user — see the note in ci.yml. Applied to every workflow rather
13+# than the ones that look like they run product code: the first pass guessed,
14+# missed preview/deploy/pkg-pr-new, and kept leaking. These vars are inert
15+# where the product is not executed, so the blanket application is the cheap
16+# structural answer.
17+env:
18+ DO_NOT_TRACK: "1"
19+ EXECUTOR_DISABLE_ANALYTICS: "1"
20+ EXECUTOR_DISABLE_INTEGRATIONS_FETCH: "1"
21+
1222 jobs:
1323 migrate:
1424 name: Migrate database
@@ -77,12 +87,30 @@ jobs: · 62 unmodified lines
7787 VITE_PUBLIC_SENTRY_DSN: ${{ secrets.VITE_PUBLIC_SENTRY_DSN }}
7888
7989 - name: Deploy cloud
80− run: bun run wrangler deploy -c dist/server/wrangler.json
90+ run: bun run wrangler deploy -c dist/server/wrangler.json --var GIT_COMMIT_SHA:${{ github.sha }}
8191 working-directory: apps/cloud
8292 env:
8393 CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
8494 CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
8595
96+ # Deploy marker: one event per deploy into the same Axiom dataset the
97+ # worker traces land in, so a latency step-change lines up with its
98+ # deploy in one query. Skipped (not failed) when the secret is absent,
99+ # and never blocks the deploy.
100+ - name: Record deploy marker in Axiom
101+ env:
102+ AXIOM_INGEST_TOKEN: ${{ secrets.AXIOM_INGEST_TOKEN }}
103+ run: |
104+ if [ -z "$AXIOM_INGEST_TOKEN" ]; then
105+ echo "AXIOM_INGEST_TOKEN not configured; skipping deploy marker"
106+ exit 0
107+ fi
108+ curl -sf -X POST "https://api.axiom.co/v1/datasets/executor-cloud/ingest" \
109+ -H "Authorization: Bearer $AXIOM_INGEST_TOKEN" \
110+ -H "Content-Type: application/json" \
111+ -d "[{\"event\":\"deploy\",\"service\":\"executor-cloud\",\"commit_sha\":\"${{ github.sha }}\",\"actor\":\"${{ github.actor }}\",\"run_id\":\"${{ github.run_id }}\"}]" \
112+ || echo "deploy marker ingest failed (non-blocking)"
113+
86114 deploy-marketing:
87115 name: Deploy marketing
88116 runs-on: blacksmith-4vcpu-ubuntu-2404
.github/workflows/graph-slices.yml+47−0
@@ -0,0 +1,47 @@
1+# Refresh the Microsoft Graph slice release assets.
2+#
3+# The Graph OpenAPI monolith (~43MB) cannot be processed inside a Workers
4+# isolate, so the runtime reads per-selection slices published on the
5+# `graph-slices` release tag (see packages/plugins/openapi/src/providers/
6+# microsoft/slices.ts). This workflow rebuilds the slices from the current
7+# upstream spec on a schedule and on demand.
8+name: Graph slices
9+
10+on:
11+ schedule:
12+ # Weekly; Microsoft's msgraph-metadata automation lands upstream refreshes
13+ # on a similar cadence. A failed run leaves the previous assets serving.
14+ - cron: "17 6 * * 1"
15+ workflow_dispatch: {}
16+
17+permissions:
18+ contents: write
19+
20+jobs:
21+ slices:
22+ runs-on: ubuntu-latest
23+ timeout-minutes: 20
24+ steps:
25+ - uses: actions/checkout@v4
26+
27+ - uses: oven-sh/setup-bun@v2
28+ with:
29+ bun-version: 1.3.11
30+
31+ - name: Install dependencies
32+ run: bun install --frozen-lockfile
33+
34+ - name: Generate slices
35+ working-directory: packages/plugins/openapi
36+ run: bun scripts/generate-graph-slices.ts --out "$RUNNER_TEMP/graph-slices"
37+
38+ - name: Publish to the graph-slices release
39+ env:
40+ GH_TOKEN: ${{ github.token }}
41+ run: |
42+ gh release view graph-slices --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 || \
43+ gh release create graph-slices --repo "$GITHUB_REPOSITORY" \
44+ --title "Microsoft Graph slices" --latest=false \
45+ --notes "Generated per-preset Microsoft Graph OpenAPI slices. Data release consumed by the openapi plugin's Microsoft adapter; refreshed by the graph-slices workflow."
46+ gh release upload graph-slices "$RUNNER_TEMP/graph-slices"/* \
47+ --repo "$GITHUB_REPOSITORY" --clobber
.github/workflows/pkg-pr-new.yml+10−0
@@ -10,6 +10,16 @@ concurrency: · 9 unmodified lines
1010 group: pkg-pr-new-${{ github.event.pull_request.number }}
1111 cancel-in-progress: true
1212
13+# CI is not a user — see the note in ci.yml. Applied to every workflow rather
14+# than the ones that look like they run product code: the first pass guessed,
15+# missed preview/deploy/pkg-pr-new, and kept leaking. These vars are inert
16+# where the product is not executed, so the blanket application is the cheap
17+# structural answer.
18+env:
19+ DO_NOT_TRACK: "1"
20+ EXECUTOR_DISABLE_ANALYTICS: "1"
21+ EXECUTOR_DISABLE_INTEGRATIONS_FETCH: "1"
22+
1323 jobs:
1424 # Per-platform matrix: build the executor binary, tar it, upload to R2.
1525 # The wrapper npm package is built later by the `publish` job which just
.github/workflows/preview-sweep.yml+10−0
@@ -16,6 +16,16 @@ permissions: · 15 unmodified lines
1616 contents: read
1717 pull-requests: read
1818
19+# CI is not a user — see the note in ci.yml. Applied to every workflow rather
20+# than the ones that look like they run product code: the first pass guessed,
21+# missed preview/deploy/pkg-pr-new, and kept leaking. These vars are inert
22+# where the product is not executed, so the blanket application is the cheap
23+# structural answer.
24+env:
25+ DO_NOT_TRACK: "1"
26+ EXECUTOR_DISABLE_ANALYTICS: "1"
27+ EXECUTOR_DISABLE_INTEGRATIONS_FETCH: "1"
28+
1929 jobs:
2030 sweep:
2131 name: Destroy previews for closed PRs
.github/workflows/preview.yml+10−0
@@ -23,6 +23,16 @@ concurrency: · 22 unmodified lines
2323 group: preview-${{ github.event.pull_request.number }}
2424 cancel-in-progress: ${{ github.event.action != 'closed' }}
2525
26+# CI is not a user — see the note in ci.yml. Applied to every workflow rather
27+# than the ones that look like they run product code: the first pass guessed,
28+# missed preview/deploy/pkg-pr-new, and kept leaking. These vars are inert
29+# where the product is not executed, so the blanket application is the cheap
30+# structural answer.
31+env:
32+ DO_NOT_TRACK: "1"
33+ EXECUTOR_DISABLE_ANALYTICS: "1"
34+ EXECUTOR_DISABLE_INTEGRATIONS_FETCH: "1"
35+
2636 jobs:
2737 deploy:
2838 name: Deploy preview
.github/workflows/publish-desktop.yml+12−0
@@ -21,6 +21,18 @@ concurrency: · 20 unmodified lines
2121 group: publish-desktop-${{ github.ref }}
2222 cancel-in-progress: false
2323
24+# CI is not a user. Without this, executor's own product telemetry reports to the
25+# production PostHog project from every e2e shard: the analytics layer
26+# (integration_added, execution_completed, artifact_*) and the
27+# integrations-registry fetch, which lands as a `hit` event. Each shard is a
28+# fresh container, so it mints a fresh anonymous id and arrives on a fresh
29+# runner IP — inflating machine and user counts by roughly an order of
30+# magnitude. Both opt-outs are the packages' documented CI/test hooks.
31+env:
32+ DO_NOT_TRACK: "1"
33+ EXECUTOR_DISABLE_ANALYTICS: "1"
34+ EXECUTOR_DISABLE_INTEGRATIONS_FETCH: "1"
35+
2436 jobs:
2537 build:
2638 permissions:
.github/workflows/publish-executor-package.yml+13−1
@@ -21,6 +21,16 @@ concurrency: · 20 unmodified lines
2121 group: publish-executor-package-${{ github.ref }}
2222 cancel-in-progress: false
2323
24+# CI is not a user — see the note in ci.yml. Applied to every workflow rather
25+# than the ones that look like they run product code: the first pass guessed,
26+# missed preview/deploy/pkg-pr-new, and kept leaking. These vars are inert
27+# where the product is not executed, so the blanket application is the cheap
28+# structural answer.
29+env:
30+ DO_NOT_TRACK: "1"
31+ EXECUTOR_DISABLE_ANALYTICS: "1"
32+ EXECUTOR_DISABLE_INTEGRATIONS_FETCH: "1"
33+
2434 jobs:
2535 publish:
2636 runs-on: ubuntu-latest
@@ -62,7 +72,9 @@ jobs: · 35 unmodified lines
6272
6373 - name: Update npm for trusted publishing
6474 run: |
65− npm install -g npm@latest
75+ # npm 12.0.0 fails `publish --provenance` with "Cannot find module
76+ # 'sigstore'" (npm/cli#9722); 11.x supports trusted publishing and works.
77+ npm install -g npm@11
6678 npm --version
6779
6880 - name: Install dependencies
.github/workflows/publish-selfhost-docker.yml+142−4
@@ -16,6 +16,18 @@ concurrency: · 15 unmodified lines
1616 group: publish-selfhost-docker-${{ inputs.tag }}
1717 cancel-in-progress: false
1818
19+# CI is not a user. Without this, executor's own product telemetry reports to the
20+# production PostHog project from every e2e shard: the analytics layer
21+# (integration_added, execution_completed, artifact_*) and the
22+# integrations-registry fetch, which lands as a `hit` event. Each shard is a
23+# fresh container, so it mints a fresh anonymous id and arrives on a fresh
24+# runner IP — inflating machine and user counts by roughly an order of
25+# magnitude. Both opt-outs are the packages' documented CI/test hooks.
26+env:
27+ DO_NOT_TRACK: "1"
28+ EXECUTOR_DISABLE_ANALYTICS: "1"
29+ EXECUTOR_DISABLE_INTEGRATIONS_FETCH: "1"
30+
1931 jobs:
2032 metadata:
2133 runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -163,16 +175,124 @@ jobs: · 141 unmodified lines
163175 if-no-files-found: error
164176 retention-days: 1
165177
178+ # Release gate: run the full selfhost-docker e2e project against the exact
179+ # by-digest image each build leg pushed, before any user-visible tag moves.
180+ # Build-only smoke misses startup crashes and feature-path breaks (a package
181+ # resolved at runtime but missing from the packaged runtime shipped broken in
182+ # v1.5.30 exactly this way); per-arch matters too — the libsql native-addon
183+ # bug was arm64-only.
184+ e2e:
185+ needs:
186+ - metadata
187+ - build
188+ strategy:
189+ fail-fast: false
190+ matrix:
191+ include:
192+ - arch: amd64
193+ runner: blacksmith-4vcpu-ubuntu-2404
194+ - arch: arm64
195+ runner: blacksmith-4vcpu-ubuntu-2404-arm
196+ runs-on: ${{ matrix.runner }}
197+ timeout-minutes: 40
198+ permissions:
199+ contents: read
200+ packages: read
201+
202+ steps:
203+ - name: Checkout release tag
204+ uses: actions/checkout@v4
205+ with:
206+ ref: ${{ needs.metadata.outputs.release_tag }}
207+ persist-credentials: false
208+
209+ - uses: oven-sh/setup-bun@v2
210+ with:
211+ bun-version: 1.3.11
212+
213+ - name: Cache Bun package cache
214+ uses: actions/cache@v4
215+ with:
216+ path: ~/.bun/install/cache
217+ key: ${{ runner.os }}-${{ runner.arch }}-bun-1.3.11-${{ hashFiles('bun.lock') }}
218+ restore-keys: |
219+ ${{ runner.os }}-${{ runner.arch }}-bun-1.3.11-
220+
221+ # Scenario helpers spawn Node sidecars; pin the same Node 24 runtime the
222+ # other workflows use.
223+ - uses: actions/setup-node@v4
224+ with:
225+ node-version: 24
226+
227+ - run: bun install --frozen-lockfile
228+
229+ - name: Cache Playwright browsers
230+ uses: actions/cache@v4
231+ with:
232+ path: ~/.cache/ms-playwright
233+ key: ${{ runner.os }}-${{ runner.arch }}-playwright-1.60.0
234+ restore-keys: |
235+ ${{ runner.os }}-${{ runner.arch }}-playwright-
236+
237+ # Install from e2e so bunx resolves ITS pinned playwright (the version
238+ # the tests run against) rather than floating to the latest.
239+ - name: Install Playwright Chromium
240+ run: bunx playwright install --with-deps chromium chromium-headless-shell
241+ working-directory: e2e
242+
243+ - name: Log in to GHCR
244+ uses: docker/login-action@v3
245+ with:
246+ registry: ghcr.io
247+ username: ${{ github.actor }}
248+ password: ${{ secrets.GITHUB_TOKEN }}
249+
250+ - name: Download image digest
251+ uses: actions/download-artifact@v4
252+ with:
253+ name: digests-${{ matrix.arch }}
254+ path: ${{ runner.temp }}/digests
255+
256+ - name: Resolve image reference
257+ id: image_ref
258+ shell: bash
259+ env:
260+ IMAGE: ${{ needs.metadata.outputs.image }}
261+ run: |
262+ set -euo pipefail
263+ digest_file="$(find "$RUNNER_TEMP/digests" -maxdepth 1 -type f | head -n 1)"
264+ if [ -z "$digest_file" ]; then
265+ echo "No image digest was downloaded."
266+ exit 1
267+ fi
268+ ref="${IMAGE}@sha256:$(basename "$digest_file")"
269+ echo "ref=$ref" >> "$GITHUB_OUTPUT"
270+ docker pull "$ref"
271+
272+ - name: Run selfhost-docker scenarios against the published digest
273+ env:
274+ E2E_SELFHOST_DOCKER_IMAGE: ${{ steps.image_ref.outputs.ref }}
275+ run: bunx vitest run --project selfhost-docker --retry=2
276+ working-directory: e2e
277+
278+ - name: Upload run artifacts
279+ if: failure()
280+ uses: actions/upload-artifact@v4
281+ with:
282+ name: e2e-runs-selfhost-docker-${{ matrix.arch }}
283+ path: |
284+ e2e/runs/
285+ e2e/selfhost-docker.boot.log
286+ retention-days: 7
287+
166288 merge:
167289 needs:
168290 - metadata
169291 - build
292+ - e2e
170293 runs-on: blacksmith-4vcpu-ubuntu-2404
171294 permissions:
172295 packages: write
173− env:
174− HAS_LEGACY_TOKEN: ${{ secrets.GHCR_LEGACY_TOKEN != '' }}
175−
176296 steps:
177297 - name: Download image digests
178298 uses: actions/download-artifact@v4
@@ -219,10 +339,28 @@ jobs: · 40 unmodified lines
219339
220340 docker buildx imagetools create "${tag_args[@]}" "${sources[@]}"
221341
342+ # Mirrors each release to the pre-org-move namespace
343+ # (ghcr.io/rhyssullivan/executor-selfhost), which GHCR cannot redirect.
344+ # Deliberately a separate job: the canonical publish above never depends on
345+ # it, and a failure here (an expired GHCR_LEGACY_TOKEN, historically) shows
346+ # as one red job pointing at exactly what to fix.
347+ mirror-legacy:
348+ needs:
349+ - metadata
350+ - merge
351+ runs-on: blacksmith-4vcpu-ubuntu-2404
352+ env:
353+ HAS_LEGACY_TOKEN: ${{ secrets.GHCR_LEGACY_TOKEN != '' }}
354+
355+ steps:
222356 - name: Skip legacy GHCR mirror (GHCR_LEGACY_TOKEN not configured)
223357 if: env.HAS_LEGACY_TOKEN != 'true'
224358 run: echo "GHCR_LEGACY_TOKEN is not configured; skipping legacy GHCR mirror."
225359
360+ - name: Set up Docker Buildx
361+ if: env.HAS_LEGACY_TOKEN == 'true'
362+ uses: docker/setup-buildx-action@v3
363+
226364 - name: Log in to legacy GHCR namespace
227365 if: env.HAS_LEGACY_TOKEN == 'true'
228366 uses: docker/login-action@v3
@@ -231,7 +369,7 @@ jobs: · 2 unmodified lines
231369 username: rhyssullivan
232370 password: ${{ secrets.GHCR_LEGACY_TOKEN }}
233371
234− - name: Mirror self-host image to legacy GHCR namespace
372+ - name: Mirror release tags to the legacy namespace
235373 if: env.HAS_LEGACY_TOKEN == 'true'
236374 shell: bash
237375 env:
.github/workflows/release.yml+15−1
@@ -13,6 +13,18 @@ concurrency: · 12 unmodified lines
1313 group: release-${{ github.ref }}
1414 cancel-in-progress: false
1515
16+# CI is not a user. Without this, executor's own product telemetry reports to the
17+# production PostHog project from every e2e shard: the analytics layer
18+# (integration_added, execution_completed, artifact_*) and the
19+# integrations-registry fetch, which lands as a `hit` event. Each shard is a
20+# fresh container, so it mints a fresh anonymous id and arrives on a fresh
21+# runner IP — inflating machine and user counts by roughly an order of
22+# magnitude. Both opt-outs are the packages' documented CI/test hooks.
23+env:
24+ DO_NOT_TRACK: "1"
25+ EXECUTOR_DISABLE_ANALYTICS: "1"
26+ EXECUTOR_DISABLE_INTEGRATIONS_FETCH: "1"
27+
1628 jobs:
1729 release:
1830 runs-on: ubuntu-latest
@@ -42,7 +54,9 @@ jobs: · 23 unmodified lines
4254
4355 - name: Update npm for trusted publishing
4456 run: |
45− npm install -g npm@latest
57+ # npm 12.0.0 fails `publish --provenance` with "Cannot find module
58+ # 'sigstore'" (npm/cli#9722); 11.x supports trusted publishing and works.
59+ npm install -g npm@11
4660 npm --version
4761
4862 - name: Install dependencies
.oxfmtrc.json+1−0
@@ -13,6 +13,7 @@ · 12 unmodified lines
1313 "*.tsbuildinfo",
1414 "executor-*.tgz",
1515 "**/routeTree.gen.ts",
16+ "**/smoke-harness-bundle.gen.ts",
1617 "apps/cloud/src/services/executor-schema.ts"
1718 ]
1819 }
.oxlintrc.jsonc+26−0
@@ -64,6 +64,7 @@ · 63 unmodified lines
6464 "apps/desktop/src/main.ts",
6565 "scripts/**/*.{ts,js}",
6666 "apps/*/scripts/**/*.{ts,js}",
67+ "packages/*/*/scripts/**/*.{ts,js}",
6768 "packages/kernel/runtime-*/src/**/*.{ts,tsx,js,mjs}",
6869 ],
6970 "rules": {
@@ -119,6 +120,31 @@ · 49 unmodified lines
119120 "executor/no-try-catch-or-throw": "off",
120121 },
121122 },
123+ {
124+ // boundary: the MCP-Apps shell is browser code, not Effect domain code.
125+ // It runs inside a sandboxed iframe and talks to the host over
126+ // postMessage and the MCP bridge — both of which are throw/Promise APIs
127+ // with no Effect runtime present. `new Function` evaluation of generated
128+ // JSX, CSS harvesting off `document.styleSheets`, and JSON on the wire
129+ // all surface untyped JS errors that must be caught and rendered rather
130+ // than modelled. Scoped to the iframe payload plus its build config; the
131+ // package's Effect-facing loader (`src/shell-html.ts`) is deliberately
132+ // NOT listed and uses inline suppressions instead.
133+ "files": [
134+ "packages/hosts/mcp-apps-shell/src/shell/**/*.{ts,tsx}",
135+ "packages/hosts/mcp-apps-shell/vite.config.shell.ts",
136+ ],
137+ "rules": {
138+ "executor/no-double-cast": "off",
139+ "executor/no-error-constructor": "off",
140+ "executor/no-instanceof-error": "off",
141+ "executor/no-json-parse": "off",
142+ "executor/no-promise-catch": "off",
143+ "executor/no-promise-reject": "off",
144+ "executor/no-try-catch-or-throw": "off",
145+ "executor/no-unknown-error-message": "off",
146+ },
147+ },
122148 {
123149 "files": ["packages/kernel/core/src/code-recovery.ts"],
124150 "rules": {
AGENTS.md+43−68
@@ -1,83 +1,58 @@
1−# AGENTS.md
1+# Executor policy
22
3−Principles: the contracts that stay true while implementations churn. For how to
4−run, boot, share, or navigate things today, see [RUNNING.md](RUNNING.md); for
5−e2e scenarios, [e2e/AGENTS.md](e2e/AGENTS.md). Run `bun run bootstrap` first in
6−any fresh checkout or worktree.
3+Stable contracts live here. Current setup and server mechanics are in
4+[RUNNING.md](RUNNING.md); e2e invariants are in
5+[e2e/AGENTS.md](e2e/AGENTS.md). Run `bun run bootstrap` in every fresh rift.
76
8−## Task Completion
7+## Verification and evidence
98
10−- Tests use Effect Vitest. Run scoped tests with `vitest run ...`. `bun run test`
11− is fine (it delegates to Vitest); NEVER run `bun test`.
12−- Run the narrowest useful verification for a change. For broad or merge-ready
13− work the full gates are `bun run format:check`, `lint`, `typecheck`, `test`.
14−- Run `bun run format` before opening a PR, staging only files your branch
15− touched (leave unrelated files `format` rewrites out of the PR).
9+- Tests use Effect Vitest. Run scoped tests with `vitest run ...` or the
10+ package script. Never use `bun test`.
11+- Use the narrowest meaningful verification while iterating. Merge-ready gates
12+ are `bun run format:check`, `lint`, `typecheck`, and `test`.
13+- e2e is slow. Avoid running it often during development: optimize for
14+ iteration speed first, then test. Save e2e for when the change is settled,
15+ and run the narrowest scenario rather than the full suite.
16+- Run `bun run format` before a PR and include only files owned by the branch.
17+- User-visible work requires a specific e2e run, a browsable dev instance, and
18+ the recording or trace showing what to inspect. Add a scenario when none
19+ covers the changed behavior.
1620
17−## Handing Back Work: Evidence, Not Assertions
21+## Service emulators
1822
19−"Done" is something the user can open, not a claim. When work changes what a user
20−sees, hand back: an e2e run that proves it (link the specific run with one line
21−on what to look at, not a wall of green), the dev server left running so they can
22−poke at it, and the paths worth trying by hand including ones no scenario covers
23−yet. If no scenario covers the change, write one, and embed its recording in the
24−PR. The machinery runs in reverse too: seed an environment into a state
25−(reproduce a bug live, stage data) and hand across the link.
23+Tests and demos that need an upstream API, OAuth/OIDC provider, or webhook use
24+the published `@executor-js/emulate` emulators rather than stubs. They provide
25+wire-level state, real-shaped credentials, OpenAPI descriptions, and request
26+ledgers. Create per-run hosted instances through the service's
27+`/_emulate/instances` control route.
2628
27−## Service Emulators
29+Emulate is a separate project. Make emulator changes there and consume the
30+published package here; never re-vendor or create a parallel fake inside
31+executor.
2832
29−For any test or demo needing an upstream API, OAuth/OIDC provider, or webhook
30−source, use the `@executor-js/emulate` emulators instead of writing a stub:
31−wire-level and stateful, real SDKs run unmodified, each serves a full OpenAPI
32−spec, mints real-shaped credentials, and records every call in a request ledger
33−to assert against. Hosted at `https://<service>.emulators.dev`. See the `emulate`
34−skill (`.claude/skills/emulate/SKILL.md`). They are a standalone project
35−(`github.com/UsefulSoftwareCo/emulate`) consumed here as the published package:
36−full autonomy to change, publish, and deploy them on their `main`; don't re-vendor.
37−
38−## Attribution
39−
40−No AI/Claude/Anthropic/Co-Authored-By attribution in commits, messages, PRs, or
41−generated files. PR titles and descriptions go to a public repo: no internal info
42−or specific names unless explicitly stated.
43−
44−## Collaboration Notes
45−
46−- The user uses speech-to-text; infer likely intent from odd wording, ask only
47− when needed.
48−- Code is cheap to write: no time estimates, implementation time isn't a blocker.
49−- Never use em-dashes anywhere. Use commas, colons, parentheses, or separate
50− sentences.
51−
52−## Reference Repos
53−
54−Repos in `.reference` (Effect, effect-atom, …) are available for patterns. Clone
55−a given Git URL into `.reference` and pull latest before using it.
56−
57−## Engineering Priorities
33+## Engineering boundaries
5834
5935 - Prefer correctness and predictable behavior over short-term convenience.
60−- Preserve runtime behavior when changing lint, typing, or test structure.
61−- Keep package boundaries clear; use public package exports, not cross-package
62− relative imports.
63−- Extract shared logic only when the shared behavior is real; avoid broad generic
36+- Preserve runtime behavior during lint, typing, and test-structure changes.
37+- Use public package exports; never cross package boundaries with relative
38+ imports.
39+- Extract shared logic only for genuinely shared behavior. Avoid generic
6440 abstractions for one-off duplication.
41+- Public PRs, commits, generated files, and documentation contain no private
42+ names, internal context, customer-derived data, or AI attribution.
6543
66−## Package Roles
44+## Package ownership
6745
68−- `packages/core/sdk`: core contracts, plugin wiring, scopes, sources, secrets,
69− policies, fixtures. `@executor-js/sdk/http-auth` carries the shared auth-method
70− vocabulary the HTTP protocol plugins compose (core never imports it, keeping it
71− carrier-agnostic).
46+- `packages/core/sdk`: contracts, plugin wiring, scopes, integrations, secrets,
47+ policies, and fixtures. Shared HTTP auth vocabulary lives in
48+ `@executor-js/sdk/http-auth`; core remains carrier-agnostic.
7249 - `packages/core/storage-*`: storage adapters and test support.
73−- `packages/plugins/*`: protocol and provider plugins; their runtime, React, API,
74− and testing helpers live with the owning plugin.
75−- `packages/react`: shared React UI and atom/client integration.
50+- `packages/plugins/*`: protocol/provider implementations and their runtime,
51+ React, API, and testing helpers.
52+- `packages/react`: shared React UI and client/atom integration.
7653 - `packages/hosts/mcp`: MCP host surface.
77−- `packages/kernel/*`: execution runtimes and code execution substrate.
78−- `apps/{local,cloud,cli,desktop}`: product entry points composing the packages.
79−
80−## Other
54+- `packages/kernel/*`: execution runtimes and code-execution substrate.
55+- `apps/{local,cloud,cli,desktop}`: product composition roots.
8156
82−Note mistakes in MISTAKES.md, missing context or tools in DESIRES.md, and env
83−learnings in LEARNINGS.md.
57+Record mistakes in `MISTAKES.md`, missing capabilities in `DESIRES.md`, and
58+environment discoveries in `LEARNINGS.md`.
README.md+8−8
@@ -8,7 +8,7 @@ and per-tool policies, then use that one catalog from any MCP-compatible agent. · 7 unmodified lines
88
99 [Website](https://executor.sh) · [Documentation](https://executor.sh/docs) · [Discord](https://discord.gg/eF29HBHwM6)
1010
11−[https://github.com/user-attachments/assets/11225f83-e848-42ba-99b2-a993bcc88dad](https://github.com/user-attachments/assets/11225f83-e848-42ba-99b2-a993bcc88dad)
11+![Executor demo](https://raw.githubusercontent.com/UsefulSoftwareCo/executor/main/assets/executor-demo.gif)
1212
1313 ## Why Executor
1414
@@ -21,9 +21,9 @@ set its policy once, and every agent shares it over MCP. Your integrations, · 6 unmodified lines
2121 auth, and policies live in one place instead of being scattered across each
2222 client.
2323
24−- **Any source.** First-party support for MCP servers, OpenAPI, GraphQL, and
24+- **Any integration.** First-party support for MCP servers, OpenAPI, GraphQL, and
2525 Google Discovery. If you can describe it with a JSON schema, it can be an
26− integration. The plugin system is open to any source type.
26+ integration. The plugin system is open to any integration type.
2727 - **One catalog, every agent.** Anything MCP-compatible connects to the same
2828 set of tools.
2929 - **Governed by policy.** Each tool is allowed, gated behind approval, or
@@ -100,12 +100,12 @@ appear. · 70 unmodified lines
100100
101101 ## Add an integration
102102
103−From the web UI, click **Add Source**, paste an OpenAPI, GraphQL, or MCP URL,
103+From the web UI, click **Add Integration**, paste an OpenAPI, GraphQL, or MCP URL,
104104 and Executor detects the type, indexes the tools, and handles auth. Or add one
105105 from the CLI:
106106
107107 ```bash
108−executor call executor openapi addSource '{
108+executor call executor openapi addIntegration '{
109109 "spec": "https://petstore3.swagger.io/api/v3/openapi.json",
110110 "namespace": "petstore",
111111 "baseUrl": "https://petstore3.swagger.io/api/v3"
@@ -113,7 +113,7 @@ executor call executor openapi addSource '{ · 1 unmodified line
113113 ```
114114
115115 Use `baseUrl` when the OpenAPI document has relative `servers` entries (for
116−example `"/api/v3"`). Confirm it is live with `executor tools sources`.
116+example `"/api/v3"`). Confirm it is live with `executor tools integrations`.
117117
118118 ## Using tools
119119
@@ -169,7 +169,7 @@ executor call <path...> --help # browse namespaces/resources/methods · 49 unmodified lines
169169 executor call <path...> --help --match "<text>" --limit <n> # narrow huge namespaces
170170 executor resume --execution-id <id> # resume paused execution
171171 executor tools search "<query>" # search tools by intent
172−executor tools sources # list configured sources + tool counts
172+executor tools integrations # list configured integrations + tool counts
173173 executor tools describe <path> # show tool TypeScript/JSON schema
174174 ```
175175
@@ -190,7 +190,7 @@ apps/ · 14 unmodified lines
190190 packages/
191191 core/ contracts, plugin wiring, scopes, policies, SDK, API, CLI core
192192 kernel/ execution runtimes (QuickJS, Deno subprocess, dynamic worker)
193− plugins/ source and provider plugins (openapi, graphql, mcp, google,
193+ plugins/ integration and provider plugins (openapi, graphql, mcp, google,
194194 microsoft, 1password, keychain, encrypted/file secrets, ...)
195195 hosts/ host adapters (MCP surface, Cloudflare)
196196 react/ shared React UI
TELEMETRY.md+101−0
@@ -0,0 +1,101 @@
1+# Telemetry
2+
3+Executor's local products (the CLI, the desktop app) and self-hosted
4+deployments send a small set of anonymous usage events. This document is the
5+complete account of what is sent, what is deliberately not sent, why the
6+feature exists, and how to turn it off.
7+
8+The hosted cloud product has its own browser-side analytics, disclosed
9+separately in its terms; this document covers the software that runs on your
10+machines.
11+
12+## Why
13+
14+Executor is used mostly outside our infrastructure. Without some signal from
15+local and self-hosted installs, every product decision about them is a guess:
16+we cannot tell whether a feature ships broken, whether anyone uses toolkits,
17+whether executions fail at unusual rates after a release, or whether the
18+product is growing anywhere except cloud.
19+
20+The events exist to answer exactly one kind of question: **how are product
21+features being used?** They are metadata about the product, not about you.
22+Anything that would answer "what is this user doing" — which APIs you call,
23+what your tools are named, what your code does — is out of scope by design,
24+not by omission.
25+
26+## What is sent
27+
28+Each event carries its named properties plus, on every event: a random
29+per-install id, the product surface (`cli`, `desktop`, or `selfhost`), the
30+release channel, and the app version.
31+
32+| Event | Properties | Meaning |
33+| --------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
34+| `execution_completed` | `ok`, `plane` (`mcp`/`api`), `toolkit` (boolean) | A code execution finished, split by whether an agent (MCP) or a human-facing API triggered it, and whether a toolkit-scoped endpoint served it. |
35+| `integration_added` | `plugin_key` | An integration was added, by kind (`openapi`, `mcp`, `graphql`, ...). |
36+| `integration_removed` | `plugin_key` | An integration was removed, by kind. |
37+| `artifact_created` | `via` (`agent`/`ui`) | A generative-UI artifact was saved. |
38+| `artifact_viewed` | `via` | An artifact was opened for its content. |
39+| `artifact_updated` | `via` | An artifact was overwritten or renamed. |
40+| `artifact_deleted` | `via` | An artifact was deleted. |
41+
42+That table is exhaustive. The typed catalog the code compiles against is
43+[`packages/core/analytics/src/events.ts`](packages/core/analytics/src/events.ts);
44+an event that is not in that file cannot be sent.
45+
46+## What is never sent
47+
48+- No code, tool arguments, tool results, or error messages.
49+- No secrets, tokens, or credentials.
50+- No names you typed: no integration slugs, connection names, toolkit slugs,
51+ tool names, or artifact titles. The **kind** of integration (`openapi`,
52+ `mcp`, ...) is a product question; **which** service you connected it to is
53+ your business.
54+- No identity: no emails, usernames, hostnames, IP-derived location, or org
55+ names. Events are marked so the analytics backend builds no person profile.
56+
57+## The anonymous id
58+
59+A random UUID is minted the first time the daemon or server starts and stored
60+as `analytics-id` in the data directory (`~/.executor` for local installs, the
61+configured data dir for self-host). It exists so that ten events from one
62+install count as one install, not ten. It is not derived from your machine,
63+account, or network, and deleting the file resets it. When telemetry is
64+disabled the file is never created.
65+
66+## Opting out
67+
68+Set either environment variable to `1`, `true`, or `yes`:
69+
70+- `DO_NOT_TRACK` — the [cross-tool convention](https://consoledonottrack.com),
71+ which Executor also honors for its other outbound calls (crash reporting,
72+ the integrations.sh catalog fetch).
73+- `EXECUTOR_DISABLE_ANALYTICS` — telemetry only, if you want the catalog
74+ fetch and crash reporting to keep working.
75+
76+Opting out is total: the analytics service becomes a no-op, nothing is
77+buffered, nothing is sent, and no id file is written. The CLI's managed
78+service (launchd/systemd) forwards both variables into the supervised
79+daemon's environment, so an opted-out install stays opted out.
80+
81+## Delivery mechanics
82+
83+Events buffer in memory (bounded) and flush in batches on a fixed cadence,
84+plus once at shutdown. Delivery is best-effort: a failed flush re-queues and
85+retries later, failures are swallowed, and no user-facing operation ever
86+waits on — or can be failed by — analytics. The ingest endpoint is PostHog
87+(`us.i.posthog.com`); the project key in the source identifies the project
88+and grants no read access.
89+
90+## Where the line is enforced
91+
92+Structurally, not by convention:
93+
94+- The event catalog is a closed, typed interface — adding a property means
95+ editing [`events.ts`](packages/core/analytics/src/events.ts) in a reviewed
96+ change, next to the property rules at the top of that file.
97+- Attribution (`plane`, `via`) is bound where each serving surface is
98+ composed, so events cannot carry request-controlled labels.
99+- Observers hang off neutral seams (an engine wrapper, post-commit hooks); the
100+ core SDK carries no analytics vocabulary and hosts that do not opt in — like
101+ every test — compose with no analytics at all.
apps/cli/CHANGELOG.md+239−0
@@ -1,5 +1,244 @@
11 # executor
22
3+## 1.6.0
4+
5+### Patch Changes
6+
7+- [#1737](https://github.com/UsefulSoftwareCo/executor/pull/1737) [`9296f36`](https://github.com/UsefulSoftwareCo/executor/commit/9296f36a8adbfdeec700ce33c37987127857b2fd) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Scope the `skills` tool to Executor's own documentation. Its description, argument, index, and unknown-name error now state that it serves a fixed catalog of how-to docs for this server's tools, so an agent on a host without a skill tool of its own no longer reads it as a general reader for the harness's or the user's skills.
8+
9+- Updated dependencies [[`a2d1417`](https://github.com/UsefulSoftwareCo/executor/commit/a2d141758e478274813c8c24d354e1fd0f66af49)]:
10+ - @executor-js/sdk@1.6.0
11+ - @executor-js/local@1.6.0
12+ - @executor-js/api@1.4.63
13+ - @executor-js/runtime-quickjs@1.6.0
14+
15+## 1.5.42
16+
17+### Patch Changes
18+
19+- Updated dependencies [[`d3f0617`](https://github.com/UsefulSoftwareCo/executor/commit/d3f0617deec06c57e0d6e1479fe668f79daf977d)]:
20+ - @executor-js/sdk@1.5.42
21+ - @executor-js/local@1.5.42
22+ - @executor-js/api@1.4.62
23+ - @executor-js/runtime-quickjs@1.5.42
24+
25+## 1.5.41
26+
27+### Patch Changes
28+
29+- [#1600](https://github.com/UsefulSoftwareCo/executor/pull/1600) [`1b5f931`](https://github.com/UsefulSoftwareCo/executor/commit/1b5f931d90b52fa9eca7b6f53359a117d757c7c1) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - **Add `integrations.remove` to the core tools so an agent can drop a catalog integration**
30+
31+ `integrations.list` advertises `canRemove` per integration, but nothing on the agent surface could act on it: removal existed only on the HTTP API and the web console, so an agent that could add an integration could never take one back out. Cleaning up a catalog meant clicking through the UI once per integration.
32+
33+ The core-tools plugin now contributes `integrations.remove`, taking the `slug` reported by `integrations.list` and cascading to every connection under the integration and the tools those produced. It is approval-gated, being strictly more destructive than `connections.remove`. The `removed` flag is honest rather than always-true: `false` means no catalog row matched, so an already-absent slug and a built-in namespace like `executor` are distinguishable from a real removal, and an integration pinned with `canRemove: false` is refused with `IntegrationRemovalNotAllowedError` instead of silently surviving.
34+
35+- [#1556](https://github.com/UsefulSoftwareCo/executor/pull/1556) [`f674fb8`](https://github.com/UsefulSoftwareCo/executor/commit/f674fb80eebd597f922edd5ec21b8035ab195a78) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - **Fix: native MCP elicitation now reaches clients on the local HTTP endpoint instead of timing out**
36+
37+ The local daemon's Streamable HTTP transport ran with `enableJsonResponse: true`, which buffers a `tools/call` into a single JSON body and leaves no open stream for the server to write on. A server-to-client `elicitation/create` raised during that call was therefore never delivered, and approval-gated tools failed with a `-32001` request timeout even though the session had negotiated `elicitation_mode=native` and the client's `elicitation.form` capability. The transport now uses the spec-default SSE streaming, so the reverse request rides the originating tool call's stream — matching the Cloudflare host's behaviour.
38+
39+- [#1603](https://github.com/UsefulSoftwareCo/executor/pull/1603) [`624e85f`](https://github.com/UsefulSoftwareCo/executor/commit/624e85f033632a7624c2bddf0944112166b1f481) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - **Fix: `oauth.clients.remove` reported success for clients it never removed**
40+
41+ The tool returned `{ removed: true }` unconditionally. `oauth.removeClient` is idempotent by design at the storage layer — `deleteMany` on a missing row is a no-op, which is the right behaviour for a delete — but the tool mapped that silence to success, so a typo'd slug, an already-deleted client, and the wrong owner were all indistinguishable from a real deletion.
42+
43+ This bites hardest because clients are keyed by BOTH owner and slug, so the same slug can exist separately under `org` and `user`. An agent sweeping a list of slugs under one hardcoded owner would delete only half of them and report every call as a success, leaving org-owned OAuth apps registered after everything they authorized was gone.
44+
45+ The tool now checks the caller-visible client set first and returns `removed: false` when nothing matched that `(owner, slug)` pair. The service-level `removeClient` is unchanged and stays idempotent.
46+
47+- Updated dependencies [[`d572658`](https://github.com/UsefulSoftwareCo/executor/commit/d572658d74097917412256f10a3ea2e3974f44dd)]:
48+ - @executor-js/sdk@1.5.41
49+ - @executor-js/local@1.5.41
50+ - @executor-js/api@1.4.61
51+ - @executor-js/runtime-quickjs@1.5.41
52+
53+## 1.5.40
54+
55+### Patch Changes
56+
57+- [#1528](https://github.com/UsefulSoftwareCo/executor/pull/1528) [`676af1a`](https://github.com/UsefulSoftwareCo/executor/commit/676af1a78301d83cdab52b0389b4c67ed07ae872) Thanks [@baggiiiie](https://github.com/baggiiiie)! - Prevent execute agents from attempting unsupported base64 decoding by directing file payloads through ToolFile emission and bodyBase64 forwarding.
58+
59+- [#1545](https://github.com/UsefulSoftwareCo/executor/pull/1545) [`df62bb3`](https://github.com/UsefulSoftwareCo/executor/commit/df62bb3c8753edf2db32cb45961cf1723114ea2d) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - **Fix: reconnecting an OAuth connection now refreshes its health status in place — no page reload needed**
60+
61+ Completing a reconnect previously left the stale "Expired" verdict on the connection row (and the integrations-list summary) until a hard refresh. Re-minting now clears the persisted verdict, and the UI re-probes as soon as the refreshed connection arrives.
62+
63+- [#1534](https://github.com/UsefulSoftwareCo/executor/pull/1534) [`80e5530`](https://github.com/UsefulSoftwareCo/executor/commit/80e553026278b1ecd7807f1ba99ba13b19d2c336) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Report the real product surface and version in the integrations.sh registry user-agent. The daemon previously sent `local` with a version frozen at 1.4.4; it now reports `cli` or `desktop` (matching analytics surfaces) and `@executor-js/local` is versioned with the release train.
64+
65+- Updated dependencies [[`8ba64f6`](https://github.com/UsefulSoftwareCo/executor/commit/8ba64f675f6d6ab5302d4f68390c0b055d006f4a), [`80e5530`](https://github.com/UsefulSoftwareCo/executor/commit/80e553026278b1ecd7807f1ba99ba13b19d2c336)]:
66+ - @executor-js/sdk@1.5.40
67+ - @executor-js/local@1.5.40
68+ - @executor-js/api@1.4.60
69+ - @executor-js/runtime-quickjs@1.5.40
70+
71+## 1.5.39
72+
73+### Patch Changes
74+
75+- Updated dependencies [[`6c316c7`](https://github.com/UsefulSoftwareCo/executor/commit/6c316c77a9efc98784976236852b58c6156e016e)]:
76+ - @executor-js/sdk@1.5.39
77+ - @executor-js/local@1.4.4
78+ - @executor-js/api@1.4.59
79+ - @executor-js/runtime-quickjs@1.5.39
80+
81+## 1.5.38
82+
83+### Patch Changes
84+
85+- [#1417](https://github.com/UsefulSoftwareCo/executor/pull/1417) [`046d67d`](https://github.com/UsefulSoftwareCo/executor/commit/046d67d75c3a8bc4cf0ab9dc4e723bc26ff130a3) Thanks [@morluto](https://github.com/morluto)! - Show policy and OAuth app removal failures in the UI, and keep success-only state unchanged when those writes fail.
86+
87+- [#1511](https://github.com/UsefulSoftwareCo/executor/pull/1511) [`7eb795d`](https://github.com/UsefulSoftwareCo/executor/commit/7eb795dda19c6177ad3bd590005eca1e326f760c) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - **Fix: `execute` scripts that both `emit()` output and `return` a value no longer lose the returned value in MCP clients that ignore `structuredContent` — the return value is now appended to the tool-result content after the emitted items**
88+
89+- [#1418](https://github.com/UsefulSoftwareCo/executor/pull/1418) [`d3610e3`](https://github.com/UsefulSoftwareCo/executor/commit/d3610e386324891ccfde111e1ff519ec9218d30f) Thanks [@morluto](https://github.com/morluto)! - Return an execution error when a Deno subprocess closes stdin instead of emitting an unhandled write failure.
90+
91+- [#1507](https://github.com/UsefulSoftwareCo/executor/pull/1507) [`541549a`](https://github.com/UsefulSoftwareCo/executor/commit/541549a5dd8806f45b1a01ea6f4fa18ac41f53b1) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - **Fix: OAuth refresh rejections with non-spec error bodies (e.g. Datadog) now surface as expired connections with a reconnect path, and definitively dead refresh tokens are no longer retried against the authorization server**
92+
93+- [#1517](https://github.com/UsefulSoftwareCo/executor/pull/1517) [`59a6640`](https://github.com/UsefulSoftwareCo/executor/commit/59a6640e575c740d22e7804cdb67cd76efe8332b) Thanks [@kitze](https://github.com/kitze)! - **Fix: OpenAPI query parameters that use form-style exploded objects now serialize each object field as a query parameter.**
94+
95+- [#1416](https://github.com/UsefulSoftwareCo/executor/pull/1416) [`f1b617c`](https://github.com/UsefulSoftwareCo/executor/commit/f1b617ce82475d4fe35be7a98c6bf9f468dbbd60) Thanks [@morluto](https://github.com/morluto)! - Prevent provider service migration row loss caused by generated ID conflicts.
96+
97+- [#1420](https://github.com/UsefulSoftwareCo/executor/pull/1420) [`8c71744`](https://github.com/UsefulSoftwareCo/executor/commit/8c7174452fe05f32815950ed06f38516883a7c8f) Thanks [@morluto](https://github.com/morluto)! - Abort GraphQL tool calls that exceed the configured invocation timeout instead of waiting indefinitely for an upstream response.
98+
99+- Updated dependencies [[`6a924dd`](https://github.com/UsefulSoftwareCo/executor/commit/6a924dd98de916d6ff8cea2329bf672f149b64f4)]:
100+ - @executor-js/sdk@1.5.38
101+ - @executor-js/local@1.4.4
102+ - @executor-js/api@1.4.58
103+ - @executor-js/runtime-quickjs@1.5.38
104+
105+## 1.5.37
106+
107+### Patch Changes
108+
109+- [#1506](https://github.com/UsefulSoftwareCo/executor/pull/1506) [`c05a1cf`](https://github.com/UsefulSoftwareCo/executor/commit/c05a1cfa629b7f28a7d870c584998cb9cbbaf303) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - **Artifacts are now on by default for MCP connections.** A plain endpoint URL serves the full artifact surface — the artifact tools, the app shell resource, and the artifact skills. Connections that don't want it opt out with `?artifacts=false` (or `--no-artifacts` on the stdio CLI); `?artifacts=true` remains accepted as the explicit default. Previously the surface required a `?artifacts=true` opt-in.
110+
111+- [#1498](https://github.com/UsefulSoftwareCo/executor/pull/1498) [`657b913`](https://github.com/UsefulSoftwareCo/executor/commit/657b9135b8b841495b362936bf60bdca998c16eb) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Add anonymous product analytics to the local daemon (CLI + desktop) and self-host: execution counts split by MCP/API plane, toolkit usage, integration add/remove, and artifact usage (created/viewed/updated/deleted, attributed to agent tools vs the console UI), filed under a persisted per-install anonymous id. Opt out with DO_NOT_TRACK or EXECUTOR_DISABLE_ANALYTICS.
112+
113+- [#1500](https://github.com/UsefulSoftwareCo/executor/pull/1500) [`5eb2ca3`](https://github.com/UsefulSoftwareCo/executor/commit/5eb2ca36f93d2dae6eb8b3506c5de04ca141bb20) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - **Fix: the artifact migration no longer narrows `definition.name` to varchar(255), which failed on existing long definition names**
114+
115+- [#1472](https://github.com/UsefulSoftwareCo/executor/pull/1472) [`1178e3b`](https://github.com/UsefulSoftwareCo/executor/commit/1178e3b31cd62cd2c05d6504d1586c2d8f018692) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Add an Artifacts tab. Interactive components an agent generates with `render-ui` are saved and listed in the console, and each one has its own page that renders it live — the page an MCP client without MCP Apps support deep-links to. Artifacts can be renamed and deleted from the console, and agents find them again by title.
116+
117+- [#1505](https://github.com/UsefulSoftwareCo/executor/pull/1505) [`9bd4a5b`](https://github.com/UsefulSoftwareCo/executor/commit/9bd4a5b6063bd98f5ae8070baf0dd3ee3e110d68) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Artifact tool results now include the web deep link beside the inline widget payload, not just on the no-apps fallback. Clients can lose a rendered widget in ways the server never sees — a reopened transcript that skips the resource re-read shows raw JSON — and the URL in the result is the model's way to point the user back at the artifact.
118+
119+- [#1483](https://github.com/UsefulSoftwareCo/executor/pull/1483) [`54df2e3`](https://github.com/UsefulSoftwareCo/executor/commit/54df2e3e99509008759269b27484ee6581ce8827) Thanks [@davidwrossiter](https://github.com/davidwrossiter)! - **Fix: GraphQL connections now reject credentials when schema introspection fails and show actionable tool sync diagnostics**
120+
121+- [#1499](https://github.com/UsefulSoftwareCo/executor/pull/1499) [`010ea98`](https://github.com/UsefulSoftwareCo/executor/commit/010ea98e520643731b07e68e21119f12b8ef1505) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - **Fix: the add-connection wizard no longer wipes a pasted credential when the key check saves a health check mid-flow**
122+
123+- [#1503](https://github.com/UsefulSoftwareCo/executor/pull/1503) [`a7c4689`](https://github.com/UsefulSoftwareCo/executor/commit/a7c468944837cfe097f03f69d612bde31903f284) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - **Fix: MCP responses larger than Durable Object storage's 128 KiB per-value cap hung the client instead of being delivered**
124+
125+ The DO transport persisted every outbound message for reconnect replay before writing the live SSE frame, and `storage.put` of an oversize value throws — so a large response (the `ui://executor/shell.html` resource is ~5 MB) was neither stored nor sent, and the client waited on keepalives forever. The transport now delivers the live frame first and treats persistence as best-effort: an oversize message skips the event store with a logged warning and arrives without a replay id, which only costs replayability if the connection drops mid-delivery.
126+
127+- [#1502](https://github.com/UsefulSoftwareCo/executor/pull/1502) [`25270b1`](https://github.com/UsefulSoftwareCo/executor/commit/25270b1f1f091778ba6584e41b041092fc1bdd00) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - **Fix: MCP clients of the cloud host got a "Shell not built" placeholder as the `ui://executor/shell.html` resource, so every artifact rendered as a widget that never finished loading**
128+
129+ The deployed Worker has no filesystem, and the shell loader silently fell back to an inert placeholder document when its `fs.readFile` failed. Workers hosts now fetch the built shell through the static-assets binding (the app build emits a stable-named copy alongside the hashed one), the self-host image reads the same emitted asset from its SPA dist, and a host that cannot produce the shell now fails the resource read with an actionable error instead of serving a document that hangs the client. App builds fail if the shell asset was not emitted.
130+
131+- Updated dependencies [[`657b913`](https://github.com/UsefulSoftwareCo/executor/commit/657b9135b8b841495b362936bf60bdca998c16eb)]:
132+ - @executor-js/sdk@1.5.37
133+ - @executor-js/api@1.4.57
134+ - @executor-js/local@1.4.4
135+ - @executor-js/runtime-quickjs@1.5.37
136+
137+## 1.5.36
138+
139+### Patch Changes
140+
141+- [#1478](https://github.com/UsefulSoftwareCo/executor/pull/1478) [`8ecbfd6`](https://github.com/UsefulSoftwareCo/executor/commit/8ecbfd65f2c1393c75661c792723961877866cc5) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Store minted OAuth tokens in the durable file secret store (`auth.json` under `EXECUTOR_DATA_DIR`) instead of the system keychain. On sandbox/headless hosts the keychain can be an in-memory keyring that a stop/recreate wipes, leaving OAuth connections expired with "Stored refresh token could not be resolved." Existing keychain-backed connections migrate with one clean reconnect.
142+
143+- [#1462](https://github.com/UsefulSoftwareCo/executor/pull/1462) [`5a70675`](https://github.com/UsefulSoftwareCo/executor/commit/5a706756c66e53c9a929e9a8c30e57166b8d121b) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - **Fix: org OAuth connections on self-host worked only for whoever ran the consent**
144+
145+ The encrypted-secrets credential provider (the writable provider on the self-hosted and Cloudflare hosts) filed token rows under the _acting user's_ private partition instead of the credential's own owner. An org-owned OAuth connection whose consent completed in one member's browser session therefore resolved only for that member — every other principal failed with `oauth_connection_missing`, while the UI showed the connection healthy. The provider now partitions by the owner embedded in the item id (`oauth:org:…` → org-shared), matching the WorkOS Vault provider, and a boot-time data migration re-files rows already written wrong. The encrypted value itself was never affected.
146+
147+- [#1459](https://github.com/UsefulSoftwareCo/executor/pull/1459) [`fc1e589`](https://github.com/UsefulSoftwareCo/executor/commit/fc1e589613a14750c2ca8c34838a71c758544c8d) Thanks [@wan0net](https://github.com/wan0net)! - Preserve `elicitation_mode=native` when creating self-hosted MCP sessions.
148+
149+- [#1475](https://github.com/UsefulSoftwareCo/executor/pull/1475) [`77b0821`](https://github.com/UsefulSoftwareCo/executor/commit/77b0821ff9bddd6fb419d81a18b9e1af804fdb55) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Refresh OAuth tokens when the upstream rejects them with HTTP 401, not only when the stored expiry says they are due. Connections whose authorization server omits `expires_in` can now recover without a manual reconnect, and the refresh path is traced.
150+
151+- [#1476](https://github.com/UsefulSoftwareCo/executor/pull/1476) [`167d899`](https://github.com/UsefulSoftwareCo/executor/commit/167d899162794064eeac0a755697c2c943f1b9ac) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Remove the custom apps plugin. Git and local-directory app sources are no
152+ longer supported. The packed binary still ships the workerd and worker-bundler
153+ sidecars.
154+- Updated dependencies []:
155+ - @executor-js/sdk@1.5.36
156+ - @executor-js/runtime-quickjs@1.5.36
157+ - @executor-js/local@1.4.4
158+ - @executor-js/api@1.4.56
159+
160+## 1.5.35
161+
162+### Patch Changes
163+
164+- Updated dependencies [[`1b9b1f1`](https://github.com/UsefulSoftwareCo/executor/commit/1b9b1f10313834a625a411169ebf83f6181589df), [`99c808f`](https://github.com/UsefulSoftwareCo/executor/commit/99c808f09d3cf2263945efa4f6592cc4e78c9e08)]:
165+ - @executor-js/sdk@1.5.35
166+ - @executor-js/runtime-quickjs@1.5.35
167+ - @executor-js/local@1.4.4
168+ - @executor-js/api@1.4.55
169+
170+## 1.5.34
171+
172+### Patch Changes
173+
174+- Updated dependencies [[`e2712db`](https://github.com/UsefulSoftwareCo/executor/commit/e2712dbff98145c5c340832ffbdcb21113b9dd78), [`7207347`](https://github.com/UsefulSoftwareCo/executor/commit/720734756a70b1b4f1564bdf82dc4118e5de2b76), [`0c4e9b4`](https://github.com/UsefulSoftwareCo/executor/commit/0c4e9b49fecb35ad71c92a464c3ea01131ff9d6f)]:
175+ - @executor-js/sdk@1.5.34
176+ - @executor-js/local@1.4.4
177+ - @executor-js/api@1.4.54
178+ - @executor-js/runtime-quickjs@1.5.34
179+
180+## 1.5.33
181+
182+### Patch Changes
183+
184+- [#1404](https://github.com/UsefulSoftwareCo/executor/pull/1404) [`5e0dd15`](https://github.com/UsefulSoftwareCo/executor/commit/5e0dd15291daaedf10f6eb8e03c5afdca8787764) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - The provider service split boot migration now skips an org whose Google or Microsoft integration cannot be migrated (for example a config without a stored specHash) instead of failing the whole migration and blocking server startup. A daemon that does fail during boot now exits with the underlying error message instead of hanging with a generic "Unknown error".
185+
186+- Updated dependencies []:
187+ - @executor-js/local@1.4.4
188+ - @executor-js/sdk@1.5.33
189+ - @executor-js/runtime-quickjs@1.5.33
190+ - @executor-js/api@1.4.53
191+
192+## 1.5.32
193+
194+### Patch Changes
195+
196+- [#1395](https://github.com/UsefulSoftwareCo/executor/pull/1395) [`d90d8be`](https://github.com/UsefulSoftwareCo/executor/commit/d90d8be38fa26d3a6b8a5ac648af815191f537bb) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Policy create now defaults a new rule's position below any more-specific existing rule on the server, so a broad rule written without an explicit position (stale UI, API, agent tool) cannot shadow an existing narrower rule.
197+
198+- [#1394](https://github.com/UsefulSoftwareCo/executor/pull/1394) [`1ca5111`](https://github.com/UsefulSoftwareCo/executor/commit/1ca511101fad057d129db9941b5c9bce963baf0a) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Ship the platform workerd binary in the self-host Docker runtime; without it custom app tools failed to sync or invoke with "workerd is unavailable on this platform".
199+
200+- Updated dependencies []:
201+ - @executor-js/sdk@1.5.32
202+ - @executor-js/runtime-quickjs@1.5.32
203+ - @executor-js/local@1.4.4
204+ - @executor-js/api@1.4.52
205+
206+## 1.5.31
207+
208+### Patch Changes
209+
210+- [#1390](https://github.com/UsefulSoftwareCo/executor/pull/1390) [`d95e63c`](https://github.com/UsefulSoftwareCo/executor/commit/d95e63cc33ade4ce0996678cfa51a5e4c784a9ea) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Ship @cloudflare/worker-bundler in the self-host Docker runtime so the server starts; it was resolved at runtime since the dynamic Worker bundler change but never copied into the image.
211+
212+- Updated dependencies []:
213+ - @executor-js/local@1.4.4
214+ - @executor-js/sdk@1.5.31
215+ - @executor-js/runtime-quickjs@1.5.31
216+ - @executor-js/api@1.4.51
217+
218+## 1.5.30
219+
220+### Patch Changes
221+
222+- [#1371](https://github.com/UsefulSoftwareCo/executor/pull/1371) [`262fc3e`](https://github.com/UsefulSoftwareCo/executor/commit/262fc3edcad31a53bd8aecacf8fe784b276fb745) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Explain 401s from a hosted server as a sign-in problem with the exact `executor login` command to run, instead of surfacing a raw decode error. `executor login` now defaults to https://executor.sh when no server is specified, and profile plumbing stays out of messages unless you address servers by name.
223+
224+- [#1375](https://github.com/UsefulSoftwareCo/executor/pull/1375) [`0f81165`](https://github.com/UsefulSoftwareCo/executor/commit/0f81165138c1c987f13ff9839e46539f80b229b0) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Run cloud custom tool bundling in a dynamically loaded Worker so dependency installation and bundling do not share the serving request isolate.
225+
226+- [#1349](https://github.com/UsefulSoftwareCo/executor/pull/1349) [`a7e3091`](https://github.com/UsefulSoftwareCo/executor/commit/a7e3091a94fbdf032ef134989ceaba4f0b1b3231) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Back-stop code execution with a host-side timeout so a wedged sandbox delivers a descriptive error instead of hanging silently.
227+
228+- [#1351](https://github.com/UsefulSoftwareCo/executor/pull/1351) [`93b000a`](https://github.com/UsefulSoftwareCo/executor/commit/93b000a4ba24317f5a1f08fd8d7f72457d115f06) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Let stock MCP clients auto-reconnect and recover a tool result when a POST stream drops mid-call.
229+
230+- [#1345](https://github.com/UsefulSoftwareCo/executor/pull/1345) [`c46730b`](https://github.com/UsefulSoftwareCo/executor/commit/c46730b5d48cc62dae1abdbe32136f3c229d79f6) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Preserve MCP tool results across dropped streamable HTTP SSE connections.
231+
232+- [#1357](https://github.com/UsefulSoftwareCo/executor/pull/1357) [`4c319ee`](https://github.com/UsefulSoftwareCo/executor/commit/4c319ee2e4a8d062402a80419dd3e1d829908ad8) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Throw a guidance error when sandbox code enumerates the `tools` proxy (`Object.keys`, spread, `for...in`) instead of returning an empty list, pointing agents at `tools.search()`.
233+
234+- [#1352](https://github.com/UsefulSoftwareCo/executor/pull/1352) [`8bdf315`](https://github.com/UsefulSoftwareCo/executor/commit/8bdf31550f702e6bce6c3460bb9d26fcce925d7b) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Treat a transient WorkOS outage during the MCP live-membership check as a retryable 503 instead of a Forbidden that destroys the session.
235+
236+- Updated dependencies []:
237+ - @executor-js/sdk@1.5.30
238+ - @executor-js/runtime-quickjs@1.5.30
239+ - @executor-js/local@1.4.4
240+ - @executor-js/api@1.4.50
241+
3242 ## 1.5.29
4243
5244 ### Patch Changes
apps/cli/package.json+4−1
@@ -1,6 +1,6 @@
11 {
22 "name": "executor",
3− "version": "1.5.29",
3+ "version": "1.6.0",
44 "private": true,
55 "bin": {
66 "executor": "./bin/executor.ts"
@@ -37,8 +37,11 @@ · 30 unmodified lines
3737 "quickjs-emscripten": "catalog:"
3838 },
3939 "devDependencies": {
40+ "@cloudflare/worker-bundler": "0.2.1",
4041 "@effect/vitest": "catalog:",
42+ "@types/node": "catalog:",
4143 "bun-types": "catalog:",
44+ "esbuild": "^0.27.3",
4245 "typescript": "catalog:",
4346 "vitest": "catalog:"
4447 }
apps/cli/src/build.ts+163−1
@@ -2,15 +2,17 @@ import { cp, mkdir, rm, writeFile, chmod } from "node:fs/promises"; · 1 unmodified line
22 import { existsSync } from "node:fs";
33 import { createRequire } from "node:module";
44 import { resolve, join, dirname } from "node:path";
5−import { fileURLToPath } from "node:url";
5+import { fileURLToPath, pathToFileURL } from "node:url";
66 import { parseArgs } from "node:util";
77 import { $ } from "bun";
8+import { WORKER_BUNDLER_DIRNAME, missingWorkerBundlerFiles } from "./worker-bundler-artifact";
89
910 const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
1011 const cliRoot = resolve(repoRoot, "apps/cli");
1112 const webRoot = resolve(repoRoot, "apps/local");
1213 const distDir = resolve(cliRoot, "dist");
1314 const ONEPASSWORD_CORE_WASM_FILENAME = "onepassword-core_bg.wasm";
15+const WORKERD_VERSION = "1.20260708.1";
1416
1517 const resolveQuickJsWasmPath = (): string => {
1618 const req = createRequire(join(repoRoot, "packages/kernel/runtime-quickjs/package.json"));
@@ -23,6 +25,32 @@ const resolveQuickJsWasmPath = (): string => { · 6 unmodified lines
2325 return wasmPath;
2426 };
2527
28+// ---------------------------------------------------------------------------
29+// MCP-Apps shell (mcp-app.html)
30+//
31+// The MCP host serves the shell as the `ui://executor/shell.html` resource, and
32+// `@executor-js/mcp-apps-shell` reads it from disk at runtime. That runtime
33+// `fs.readFile` is invisible to `bun build --compile`, so without this the
34+// packaged binary would fail every artifact resource read. Build it if missing
35+// and copy it next to the executable, where the loader finds it via
36+// `process.execPath` — the same colocation trick used for `libsql.node` /
37+// `emscripten-module.wasm`.
38+// ---------------------------------------------------------------------------
39+
40+const mcpAppsShellDir = resolve(repoRoot, "packages/hosts/mcp-apps-shell");
41+const mcpAppsShellHtml = join(mcpAppsShellDir, "dist/mcp-app.html");
42+
43+const ensureMcpAppsShell = async (): Promise<string> => {
44+ if (!existsSync(mcpAppsShellHtml)) {
45+ console.log("Building MCP-Apps shell (mcp-app.html)...");
46+ await $`bun run --cwd ${mcpAppsShellDir} build:shell`.quiet();
47+ }
48+ if (!existsSync(mcpAppsShellHtml)) {
49+ throw new Error(`MCP-Apps shell missing at ${mcpAppsShellHtml} after build:shell`);
50+ }
51+ return mcpAppsShellHtml;
52+};
53+
2654 const resolveOnePasswordCoreWasmPath = (): string => {
2755 const req = createRequire(join(repoRoot, "packages/plugins/onepassword/package.json"));
2856 const sdkPkg = req.resolve("@1password/sdk/package.json");
@@ -33,6 +61,82 @@ const resolveOnePasswordCoreWasmPath = (): string => { · 4 unmodified lines
3361 return wasmPath;
3462 };
3563
64+const resolveWorkerBundlerDistPath = (): string => {
65+ const req = createRequire(join(repoRoot, "apps/cli/package.json"));
66+ const pkgJson = req.resolve("@cloudflare/worker-bundler/package.json");
67+ const distPath = join(dirname(pkgJson), "dist");
68+ if (!existsSync(join(distPath, "index.js")) || !existsSync(join(distPath, "esbuild.wasm"))) {
69+ throw new Error(`@cloudflare/worker-bundler dist files not found at ${distPath}`);
70+ }
71+ return distPath;
72+};
73+
74+const createPackedWorkerBundlerSource = async (distPath: string): Promise<string> => {
75+ const esbuildPath = join(repoRoot, "node_modules/.bun/node_modules/esbuild/lib/main.js");
76+ if (!existsSync(esbuildPath)) throw new Error(`esbuild not found at ${esbuildPath}`);
77+ const { build } = await import(pathToFileURL(esbuildPath).href);
78+ const result = await build({
79+ entryPoints: [join(distPath, "index.js")],
80+ bundle: true,
81+ format: "esm",
82+ platform: "browser",
83+ external: ["./esbuild.wasm"],
84+ logLevel: "silent",
85+ write: false,
86+ });
87+ const source = result.outputFiles[0]?.text;
88+ if (source === undefined) throw new Error("failed to bundle @cloudflare/worker-bundler");
89+ return source;
90+};
91+
92+/**
93+ * Verify the staged worker-bundler beside a freshly compiled binary.
94+ *
95+ * The compiled binary cannot resolve `@cloudflare/worker-bundler` by name —
96+ * bunfs has no node_modules, so a bare specifier throws ResolveMessage at
97+ * runtime. The colocated copy IS the delivery mechanism, and until now nothing
98+ * checked it after the copy: a partial staging shipped a binary that only fails
99+ * on the user's machine, at startup. Assert it here, where a failure costs a
100+ * red build instead of a crash loop.
101+ *
102+ * Checks match the depth of the asset assertions elsewhere in the repo:
103+ * presence of every file the runtime contract names, a size floor on the packed
104+ * entry, and the `\0asm` magic on the wasm so a truncated or LFS-pointer copy
105+ * cannot pass as real.
106+ */
107+const assertPackedWorkerBundler = async (binDir: string, targetName: string): Promise<void> => {
108+ const stagedDir = join(binDir, WORKER_BUNDLER_DIRNAME);
109+
110+ const missing = missingWorkerBundlerFiles(stagedDir, existsSync);
111+ if (missing.length > 0) {
112+ throw new Error(
113+ `Packed @cloudflare/worker-bundler is incomplete for ${targetName}: missing ${missing.join(", ")} under ${stagedDir}. ` +
114+ `The compiled binary resolves this package from disk, not from bunfs, so shipping it partial means a runtime failure on the user's machine.`,
115+ );
116+ }
117+
118+ const bundledEntry = join(stagedDir, "dist", "index.bundled.js");
119+ const bundledSize = (await Bun.file(bundledEntry).arrayBuffer()).byteLength;
120+ if (bundledSize < 100_000) {
121+ throw new Error(
122+ `Packed worker-bundler entry for ${targetName} is implausibly small (${bundledSize} bytes at ${bundledEntry}). Expected the esbuild-packed bundle.`,
123+ );
124+ }
125+
126+ const wasmPath = join(stagedDir, "dist", "esbuild.wasm");
127+ const wasmHead = new Uint8Array((await Bun.file(wasmPath).arrayBuffer()).slice(0, 4));
128+ if (
129+ wasmHead[0] !== 0x00 ||
130+ wasmHead[1] !== 0x61 ||
131+ wasmHead[2] !== 0x73 ||
132+ wasmHead[3] !== 0x6d
133+ ) {
134+ throw new Error(
135+ `Staged esbuild.wasm for ${targetName} is not a WebAssembly module (bad magic at ${wasmPath}).`,
136+ );
137+ }
138+};
139+
36140 // ---------------------------------------------------------------------------
37141 // Metadata
38142 // ---------------------------------------------------------------------------
@@ -119,6 +223,7 @@ const bunTarget = (t: Target): Bun.Build.CompileTarget => { · 80 unmodified lines
119223 };
120224
121225 const binaryName = (t: Target) => (t.os === "win32" ? "executor.exe" : "executor");
226+const workerdBinaryName = (t: Target) => (t.os === "win32" ? "workerd.exe" : "workerd");
122227
123228 const isCurrentPlatform = (t: Target) =>
124229 t.os === process.platform && t.arch === process.arch && !t.abi;
@@ -211,6 +316,41 @@ const resolveLibsqlNative = (t: Target): string | null => { · 86 unmodified lines
211316 }
212317 };
213318
319+const resolveWorkerdBinary = (t: Target): string | null => {
320+ const platformMap: Record<string, string> = {
321+ "darwin-arm64": "@cloudflare/workerd-darwin-arm64",
322+ "darwin-x64": "@cloudflare/workerd-darwin-64",
323+ "linux-arm64": "@cloudflare/workerd-linux-arm64",
324+ "linux-x64": "@cloudflare/workerd-linux-64",
325+ "win32-x64": "@cloudflare/workerd-windows-64",
326+ };
327+ const key = [t.os, t.arch, t.abi].filter(Boolean).join("-");
328+ const pkg = platformMap[key];
329+ if (!pkg) return null;
330+ const binary = workerdBinaryName(t);
331+ try {
332+ const req = createRequire(
333+ join(repoRoot, "packages/kernel/runtime-workerd-subprocess/package.json"),
334+ );
335+ const pkgJson = req.resolve(`${pkg}/package.json`);
336+ for (const candidate of [
337+ join(dirname(pkgJson), "bin", binary),
338+ join(dirname(pkgJson), binary),
339+ ]) {
340+ if (existsSync(candidate)) return candidate;
341+ }
342+ } catch {
343+ const packageRoot = join(
344+ repoRoot,
345+ `node_modules/.bun/${pkg.replace("/", "+")}@${WORKERD_VERSION}/node_modules/${pkg}`,
346+ );
347+ for (const candidate of [join(packageRoot, "bin", binary), join(packageRoot, binary)]) {
348+ if (existsSync(candidate)) return candidate;
349+ }
350+ }
351+ return null;
352+};
353+
214354 // ---------------------------------------------------------------------------
215355 // Build mode
216356 // ---------------------------------------------------------------------------
@@ -332,7 +472,10 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => { · 115 unmodified lines
332472 await writeFile(embeddedMigrationsPath, `${embeddedMigrations}\n`);
333473
334474 const quickJsWasmPath = resolveQuickJsWasmPath();
475+ const mcpAppsShellPath = await ensureMcpAppsShell();
335476 const onePasswordCoreWasmPath = resolveOnePasswordCoreWasmPath();
477+ const workerBundlerDistPath = resolveWorkerBundlerDistPath();
478+ const packedWorkerBundlerSource = await createPackedWorkerBundlerSource(workerBundlerDistPath);
336479
337480 try {
338481 for (const target of targets) {
@@ -355,6 +498,10 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => { · 16 unmodified lines
355498 // Copy QuickJS WASM next to binary — loaded at runtime by the server
356499 await cp(quickJsWasmPath, join(binDir, "emscripten-module.wasm"));
357500
501+ // Copy the MCP-Apps shell next to the binary. Platform-independent HTML,
502+ // read from execDir at runtime by @executor-js/mcp-apps-shell.
503+ await cp(mcpAppsShellPath, join(binDir, "mcp-app.html"));
504+
358505 // Copy 1Password's sdk-core WASM next to the binary. The patched
359506 // sdk-core loader falls back to this sibling file when bun --compile
360507 // bakes a build-machine node_modules path into __dirname.
@@ -375,6 +522,21 @@ const buildBinaries = async (targets: Target[], mode: BuildMode) => { · 14 unmodified lines
375522 await cp(libsqlNative, join(binDir, "libsql.node"));
376523 }
377524
525+ const workerdBinary = resolveWorkerdBinary(target);
526+ if (workerdBinary && existsSync(workerdBinary)) {
527+ await cp(workerdBinary, join(binDir, workerdBinaryName(target)));
528+ if (target.os !== "win32") await chmod(join(binDir, workerdBinaryName(target)), 0o755);
529+ }
530+
531+ await cp(workerBundlerDistPath, join(binDir, WORKER_BUNDLER_DIRNAME, "dist"), {
532+ recursive: true,
533+ });
534+ await writeFile(
535+ join(binDir, WORKER_BUNDLER_DIRNAME, "dist", "index.bundled.js"),
536+ packedWorkerBundlerSource,
537+ );
538+ await assertPackedWorkerBundler(binDir, name);
539+
378540 // Smoke test on current platform
379541 if (isCurrentPlatform(target)) {
380542 const bin = join(binDir, binaryName(target));
apps/cli/src/device-login.test.ts+118−2
@@ -1,6 +1,44 @@
1−import { describe, expect, it } from "@effect/vitest";
1+import { afterEach, describe, expect, it } from "@effect/vitest";
22
3−import { browserOpenCommand } from "./device-login";
3+import {
4+ browserOpenCommand,
5+ discoverCliLogin,
6+ refreshDeviceTokens,
7+ requestDeviceCode,
8+ type CliLoginDiscovery,
9+} from "./device-login";
10+
11+const originalFetch = globalThis.fetch;
12+
13+interface FetchCall {
14+ readonly url: string;
15+ readonly headers: Record<string, string>;
16+}
17+
18+const responseJson = (body: Record<string, unknown>, status = 200): Response =>
19+ new Response(JSON.stringify(body), {
20+ status,
21+ headers: { "content-type": "application/json" },
22+ });
23+
24+const installFetch = (handler: (url: string, init: RequestInit | undefined) => Response) => {
25+ globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => {
26+ const url =
27+ typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
28+ return Promise.resolve(handler(url, init));
29+ }) as typeof fetch;
30+};
31+
32+const recordCall = (calls: Array<FetchCall>, url: string, init: RequestInit | undefined): void => {
33+ calls.push({
34+ url,
35+ headers: Object.fromEntries(new Headers(init?.headers).entries()),
36+ });
37+};
38+
39+afterEach(() => {
40+ globalThis.fetch = originalFetch;
41+});
442
543 describe("browserOpenCommand", () => {
644 it("opens Windows browser URLs without cmd.exe", () => {
@@ -32,3 +70,81 @@ describe("browserOpenCommand", () => { · 25 unmodified lines
3270 expect(browserOpenCommand("not a url", "win32")).toBeUndefined();
3371 });
3472 });
73+
74+describe("device login headers", () => {
75+ it("sends configured headers when discovering CLI login", async () => {
76+ const calls: Array<FetchCall> = [];
77+ installFetch((url, init) => {
78+ recordCall(calls, url, init);
79+ return responseJson({
80+ provider: "better-auth",
81+ deviceAuthorizationEndpoint: "https://executor.example/api/auth/device/code",
82+ tokenEndpoint: "https://executor.example/api/auth/device/token",
83+ clientId: "executor-cli",
84+ requestFormat: "json",
85+ });
86+ });
87+
88+ const discovery = await discoverCliLogin("https://executor.example", {
89+ headers: { "CF-Access-Client-Id": "client-id" },
90+ });
91+
92+ expect(discovery.clientId).toBe("executor-cli");
93+ expect(calls).toHaveLength(1);
94+ expect(calls[0]?.url).toBe("https://executor.example/api/auth/cli-login");
95+ expect(calls[0]?.headers).toMatchObject({
96+ accept: "application/json",
97+ "cf-access-client-id": "client-id",
98+ });
99+ });
100+
101+ it("sends configured headers only to same-origin device endpoints", async () => {
102+ const calls: Array<FetchCall> = [];
103+ installFetch((url, init) => {
104+ recordCall(calls, url, init);
105+ if (url.endsWith("/api/auth/device/code")) {
106+ return responseJson({
107+ device_code: "device-code",
108+ user_code: "USER-CODE",
109+ verification_uri: "https://executor.example/device",
110+ expires_in: 300,
111+ interval: 5,
112+ });
113+ }
114+ return responseJson({
115+ access_token: "access-token",
116+ refresh_token: "refresh-token-2",
117+ expires_in: 600,
118+ });
119+ });
120+ const discovery: CliLoginDiscovery = {
121+ provider: "better-auth",
122+ deviceAuthorizationEndpoint: "https://executor.example/api/auth/device/code",
123+ tokenEndpoint: "https://accounts.example/oauth/token",
124+ clientId: "executor-cli",
125+ requestFormat: "form",
126+ };
127+ const headers = { "CF-Access-Client-Id": "client-id" };
128+
129+ await requestDeviceCode(discovery, { serverOrigin: "https://executor.example", headers });
130+ await refreshDeviceTokens({
131+ tokenEndpoint: discovery.tokenEndpoint,
132+ clientId: discovery.clientId,
133+ refreshToken: "refresh-token",
134+ serverOrigin: "https://executor.example",
135+ headers,
136+ });
137+
138+ expect(calls).toHaveLength(2);
139+ expect(calls[0]?.headers).toMatchObject({
140+ accept: "application/json",
141+ "content-type": "application/x-www-form-urlencoded",
142+ "cf-access-client-id": "client-id",
143+ });
144+ expect(calls[1]?.headers).toMatchObject({
145+ accept: "application/json",
146+ "content-type": "application/x-www-form-urlencoded",
147+ });
148+ expect(calls[1]?.headers["cf-access-client-id"]).toBeUndefined();
149+ });
150+});
apps/cli/src/device-login.ts+57−8
@@ -50,6 +50,15 @@ export interface DeviceTokens { · 49 unmodified lines
5050 readonly organizationId?: string;
5151 }
5252
53+export interface DeviceLoginHttpOptions {
54+ readonly headers?: Readonly<Record<string, string>>;
55+ readonly serverOrigin?: string;
56+}
57+
58+export interface PollForDeviceTokensOptions extends DeviceLoginHttpOptions {
59+ readonly now?: () => number;
60+}
61+
5362 const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
5463 const DEFAULT_INTERVAL_SECONDS = 5;
5564
@@ -126,17 +135,43 @@ const definedFields = (fields: Record<string, string | undefined>): Record<strin · 70 unmodified lines
126135 string
127136 >;
128137
138+const isSameOrigin = (url: string, origin: string): boolean => {
139+ try {
140+ return new URL(url).origin === new URL(origin).origin;
141+ } catch {
142+ return false;
143+ }
144+};
145+
146+const headersForUrl = (
147+ url: string,
148+ baseHeaders: Record<string, string>,
149+ options: DeviceLoginHttpOptions = {},
150+): Record<string, string> => {
151+ const configured =
152+ options.headers && (!options.serverOrigin || isSameOrigin(url, options.serverOrigin))
153+ ? options.headers
154+ : {};
155+ return { ...configured, ...baseHeaders };
156+};
157+
129158 const post = async (
130159 url: string,
131160 fields: Record<string, string | undefined>,
132161 format: "form" | "json",
162+ options: DeviceLoginHttpOptions = {},
133163 ) =>
134164 fetch(url, {
135165 method: "POST",
136− headers: {
137− "content-type": format === "json" ? "application/json" : "application/x-www-form-urlencoded",
138− accept: "application/json",
139− },
166+ headers: headersForUrl(
167+ url,
168+ {
169+ "content-type":
170+ format === "json" ? "application/json" : "application/x-www-form-urlencoded",
171+ accept: "application/json",
172+ },
173+ options,
174+ ),
140175 body: format === "json" ? JSON.stringify(definedFields(fields)) : formBody(fields),
141176 });
142177
@@ -150,10 +185,16 @@ const readJson = async (response: Response): Promise<Record<string, unknown>> => · 7 unmodified lines
150185 }
151186 };
152187
153−export const discoverCliLogin = async (origin: string): Promise<CliLoginDiscovery> => {
188+export const discoverCliLogin = async (
189+ origin: string,
190+ options: DeviceLoginHttpOptions = {},
191+): Promise<CliLoginDiscovery> => {
154192 let response: Response;
193+ const url = cliLoginUrl(origin);
155194 try {
156− response = await fetch(cliLoginUrl(origin), { headers: { accept: "application/json" } });
195+ response = await fetch(url, {
196+ headers: headersForUrl(url, { accept: "application/json" }, options),
197+ });
157198 } catch (cause) {
158199 throw new DeviceLoginError(
159200 `Could not reach ${origin} to start login: ${cause instanceof Error ? cause.message : String(cause)}`,
@@ -182,11 +223,15 @@ export const discoverCliLogin = async (origin: string): Promise<CliLoginDiscover · 22 unmodified lines
182223 };
183224 };
184225
185−export const requestDeviceCode = async (discovery: CliLoginDiscovery): Promise<DeviceCodeGrant> => {
226+export const requestDeviceCode = async (
227+ discovery: CliLoginDiscovery,
228+ options: DeviceLoginHttpOptions = {},
229+): Promise<DeviceCodeGrant> => {
186230 const response = await post(
187231 discovery.deviceAuthorizationEndpoint,
188232 { client_id: discovery.clientId, scope: discovery.scope },
189233 discovery.requestFormat,
234+ options,
190235 );
191236 const body = await readJson(response);
192237 if (!response.ok) {
@@ -220,7 +265,7 @@ const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout · 27 unmodified lines
220265 export const pollForDeviceTokens = async (
221266 discovery: CliLoginDiscovery,
222267 grant: DeviceCodeGrant,
223− options: { readonly now?: () => number } = {},
268+ options: PollForDeviceTokensOptions = {},
224269 ): Promise<DeviceTokens> => {
225270 const now = options.now ?? (() => Date.now());
226271 const deadline = now() + grant.expiresInSeconds * 1000;
@@ -240,6 +285,7 @@ export const pollForDeviceTokens = async ( · 13 unmodified lines
240285 client_id: discovery.clientId,
241286 },
242287 discovery.requestFormat,
288+ options,
243289 );
244290 const body = await readJson(response);
245291
@@ -286,6 +332,8 @@ export const refreshDeviceTokens = async (input: { · 40 unmodified lines
286332 readonly tokenEndpoint: string;
287333 readonly clientId: string;
288334 readonly refreshToken: string;
335+ readonly headers?: Readonly<Record<string, string>>;
336+ readonly serverOrigin?: string;
289337 }): Promise<DeviceTokens> => {
290338 const response = await post(
291339 input.tokenEndpoint,
@@ -295,6 +343,7 @@ export const refreshDeviceTokens = async (input: { · 3 unmodified lines
295343 client_id: input.clientId,
296344 },
297345 "form",
346+ input,
298347 );
299348 const body = await readJson(response);
300349 if (!response.ok) {
apps/cli/src/main.ts+263−69
@@ -64,7 +64,12 @@ if (sentryDsn) { · 63 unmodified lines
6464 import { Argument as Args, Command, Flag as Options } from "effect/unstable/cli";
6565 import { BunRuntime, BunServices } from "@effect/platform-bun";
6666 import { HttpApiClient } from "effect/unstable/httpapi";
67−import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
67+import {
68+ FetchHttpClient,
69+ HttpClient,
70+ HttpClientError,
71+ HttpClientRequest,
72+} from "effect/unstable/http";
6873 import { FileSystem, Path as PlatformPath } from "effect";
6974 import type { PlatformError } from "effect/PlatformError";
7075 import * as Effect from "effect/Effect";
@@ -79,10 +84,13 @@ import { · 8 unmodified lines
7984 getExecutorServerAuthorizationHeader,
8085 normalizeExecutorServerConnection,
8186 normalizeExecutorServerOrigin,
87+ resolveExecutorServerConfiguredHeaders,
88+ resolveExecutorServerRequestHeaders,
8289 type ExecutorLocalServerKind,
8390 type ExecutorLocalServerManifest,
8491 type ExecutorServerConnection,
8592 type ExecutorServerConnectionInput,
93+ type ExecutorServerHeaders,
8694 } from "@executor-js/sdk/shared";
8795 import {
8896 decodeAccessTokenClaims,
@@ -131,7 +139,9 @@ import { · 42 unmodified lines
131139 import {
132140 canAutoStartCliServerConnection,
133141 chooseCliServerConnectionWithActiveLocal,
142+ describeUnauthorizedCliServer,
134143 parseCliExecutorServerConnection,
144+ profileNameFromConnectionKey,
135145 type CliServerConnectionSource,
136146 withCliServerAuthFallback,
137147 } from "./server-connection";
@@ -162,7 +172,7 @@ import { · 24 unmodified lines
162172 buildDescribeToolCode,
163173 filterToolPathChildren,
164174 buildInvokeToolCode,
165− buildListSourcesCode,
175+ buildListIntegrationsCode,
166176 buildSearchToolsCode,
167177 extractExecutionId,
168178 extractPausedInteraction,
@@ -205,9 +215,11 @@ const waitForShutdownSignal = () => · 36 unmodified lines
205215 const shutdown = () => resume(Effect.void);
206216 process.once("SIGINT", shutdown);
207217 process.once("SIGTERM", shutdown);
218+ process.once("SIGHUP", shutdown);
208219 return Effect.sync(() => {
209220 process.off("SIGINT", shutdown);
210221 process.off("SIGTERM", shutdown);
222+ process.off("SIGHUP", shutdown);
211223 });
212224 });
213225
@@ -743,12 +755,9 @@ const resolveRequestedExecutorServerConnection = ( · 529 unmodified lines
743755 // login. The refreshed tokens are written back to the originating profile.
744756 const OAUTH_REFRESH_SKEW_SECONDS = 60;
745757
746−const profileNameFromKey = (key: string): string | null =>
747− key.startsWith("profile:") ? key.slice("profile:".length) : null;
748−
749758 const refreshOAuthConnection = (
750759 connection: ExecutorServerConnection,
751−): Effect.Effect<ExecutorServerConnection, never, FileSystem.FileSystem | PlatformPath.Path> =>
760+): Effect.Effect<ExecutorServerConnection, Error, FileSystem.FileSystem | PlatformPath.Path> =>
752761 Effect.gen(function* () {
753762 const auth = connection.auth;
754763 if (!auth || auth.kind !== "oauth") return connection;
@@ -759,8 +768,19 @@ const refreshOAuthConnection = ( · 4 unmodified lines
759768 const { refreshToken, tokenEndpoint, clientId } = auth;
760769 if (!refreshToken || !tokenEndpoint || !clientId) return connection;
761770
771+ const headers = yield* resolveExecutorServerConfiguredHeaders(connection, process.env).pipe(
772+ Effect.mapError(toError),
773+ );
774+
762775 const refreshed = yield* Effect.tryPromise({
763− try: () => refreshDeviceTokens({ tokenEndpoint, clientId, refreshToken }),
776+ try: () =>
777+ refreshDeviceTokens({
778+ tokenEndpoint,
779+ clientId,
780+ refreshToken,
781+ serverOrigin: connection.origin,
782+ headers,
783+ }),
764784 catch: toError,
765785 // On a failed refresh, keep the existing token and let the eventual 401
766786 // surface, better than blocking the command on a transient hiccup.
@@ -780,7 +800,7 @@ const refreshOAuthConnection = ( · 13 unmodified lines
780800 },
781801 });
782802
783− const profileName = profileNameFromKey(connection.key);
803+ const profileName = profileNameFromConnectionKey(connection.key);
784804 if (profileName) {
785805 yield* upsertCliServerConnectionProfile({
786806 name: profileName,
@@ -961,7 +981,7 @@ const executeCode = (input: { · 174 unmodified lines
961981 }): Effect.Effect<ExecuteCodeResult, Error, FileSystem.FileSystem | PlatformPath.Path> =>
962982 Effect.gen(function* () {
963983 const connection = yield* resolveExecutorServerConnection(input.target);
964− const client = yield* makeApiClient(connection);
984+ const client = yield* makeApiClient(connection, input.target);
965985 const response = yield* client.executions.execute({
966986 payload: {
967987 code: input.code,
@@ -1050,19 +1070,36 @@ const printExecutionOutcome = (input: { · 82 unmodified lines
10501070 // Typed API client
10511071 // ---------------------------------------------------------------------------
10521072
1053−const makeApiClient = (connection: ExecutorServerConnection) => {
1054− const authorization = getExecutorServerAuthorizationHeader(connection);
1055− return HttpApiClient.make(ExecutorApi, {
1056− baseUrl: connection.apiBaseUrl,
1057− ...(authorization
1058− ? {
1059− transformClient: HttpClient.mapRequest((request) =>
1060− HttpClientRequest.setHeader(request, "authorization", authorization),
1061− ),
1062− }
1063− : {}),
1073+const makeApiClient = (connection: ExecutorServerConnection, target: ServerTarget = {}) =>
1074+ Effect.gen(function* () {
1075+ const headers = yield* resolveExecutorServerRequestHeaders(connection, process.env).pipe(
1076+ Effect.mapError(toError),
1077+ );
1078+ return yield* HttpApiClient.make(ExecutorApi, {
1079+ baseUrl: connection.apiBaseUrl,
1080+ ...(Object.keys(headers).length > 0
1081+ ? {
1082+ transformClient: HttpClient.mapRequest((request) =>
1083+ HttpClientRequest.setHeaders(request, headers),
1084+ ),
1085+ }
1086+ : {}),
1087+ // A 401 on an endpoint that doesn't model it is a sign-in problem: rewrite
1088+ // the transport-level error into the login hint. Without this the client
1089+ // fails decoding the unexpected status and prints the opaque
1090+ // `Decode error (401 GET .../api/tools)`. Declared 401s (typed API errors)
1091+ // decode before this catch and pass through untouched.
1092+ transformResponse: (effect) =>
1093+ Effect.catchIf(
1094+ effect,
1095+ (cause) => HttpClientError.isHttpClientError(cause) && cause.response?.status === 401,
1096+ () =>
1097+ Effect.fail(
1098+ new Error(describeUnauthorizedCliServer({ connection, cliPrefix, target })),
1099+ ),
1100+ ),
1101+ });
10641102 }).pipe(Effect.provide(FetchHttpClient.layer));
1065−};
10661103
10671104 // ---------------------------------------------------------------------------
10681105 // Foreground session
@@ -1321,14 +1358,26 @@ const runBackgroundDaemonStart = (input: { · 252 unmodified lines
13211358 // Stdio MCP session: a pure stdio <-> HTTP bridge to the owning local daemon.
13221359 // ---------------------------------------------------------------------------
13231360
1324−const mcpUrlForActiveLocalServer = (
1325− connection: ExecutorServerConnection,
1326− elicitationMode: "browser" | "model",
1327−): URL => {
1328− const url = new URL("/mcp", connection.origin);
1329− if (elicitationMode === "browser") {
1361+const mcpUrlForActiveLocalServer = (input: {
1362+ readonly connection: ExecutorServerConnection;
1363+ readonly elicitationMode: "browser" | "model";
1364+ readonly artifacts: boolean;
1365+ readonly searchTools: boolean;
1366+}): URL => {
1367+ const url = new URL("/mcp", input.connection.origin);
1368+ if (input.elicitationMode === "browser") {
13301369 url.searchParams.set("elicitation_mode", "browser");
13311370 }
1371+ // Artifacts are on by default; only the opt-out is spelled out, so the
1372+ // default endpoint stays clean.
1373+ if (!input.artifacts) {
1374+ url.searchParams.set("artifacts", "false");
1375+ }
1376+ // Per-integration search tools are off by default; only the opt-in is
1377+ // spelled out.
1378+ if (input.searchTools) {
1379+ url.searchParams.set("search_tools", "true");
1380+ }
13321381 return url;
13331382 };
13341383
@@ -1343,11 +1392,18 @@ const mcpUrlForActiveLocalServer = ( · 8 unmodified lines
13431392 const runMcpHttpBridge = async (input: {
13441393 readonly manifest: ExecutorLocalServerManifest;
13451394 readonly elicitationMode: "browser" | "model";
1395+ readonly artifacts: boolean;
1396+ readonly searchTools: boolean;
13461397 }): Promise<void> => {
13471398 const stdio = new StdioServerTransport();
13481399 const authorization = getExecutorServerAuthorizationHeader(input.manifest.connection);
13491400 const http = new StreamableHTTPClientTransport(
1350− mcpUrlForActiveLocalServer(input.manifest.connection, input.elicitationMode),
1401+ mcpUrlForActiveLocalServer({
1402+ connection: input.manifest.connection,
1403+ elicitationMode: input.elicitationMode,
1404+ artifacts: input.artifacts,
1405+ searchTools: input.searchTools,
1406+ }),
13511407 authorization ? { requestInit: { headers: { Authorization: authorization } } } : undefined,
13521408 );
13531409
@@ -1422,7 +1478,11 @@ const runMcpHttpBridge = async (input: { · 68 unmodified lines
14221478 }
14231479 };
14241480
1425−const runStdioMcpSession = (input: { readonly elicitationMode: "browser" | "model" }) =>
1481+const runStdioMcpSession = (input: {
1482+ readonly elicitationMode: "browser" | "model";
1483+ readonly artifacts: boolean;
1484+ readonly searchTools: boolean;
1485+}) =>
14261486 Effect.gen(function* () {
14271487 // `executor mcp` never owns the local database. If a local server is already
14281488 // running, bridge this stdio client to it; otherwise ensure a durable
@@ -1434,7 +1494,12 @@ const runStdioMcpSession = (input: { readonly elicitationMode: "browser" | "mode · 5 unmodified lines
14341494 const active = yield* readActiveLocalServerManifest();
14351495 if (active) {
14361496 yield* Effect.promise(() =>
1437− runMcpHttpBridge({ manifest: active, elicitationMode: input.elicitationMode }),
1497+ runMcpHttpBridge({
1498+ manifest: active,
1499+ elicitationMode: input.elicitationMode,
1500+ artifacts: input.artifacts,
1501+ searchTools: input.searchTools,
1502+ }),
14381503 );
14391504 return;
14401505 }
@@ -1456,7 +1521,12 @@ const runStdioMcpSession = (input: { readonly elicitationMode: "browser" | "mode · 15 unmodified lines
14561521 );
14571522 }
14581523 yield* Effect.promise(() =>
1459− runMcpHttpBridge({ manifest: elected, elicitationMode: input.elicitationMode }),
1524+ runMcpHttpBridge({
1525+ manifest: elected,
1526+ elicitationMode: input.elicitationMode,
1527+ artifacts: input.artifacts,
1528+ searchTools: input.searchTools,
1529+ }),
14601530 );
14611531 });
14621532
@@ -1504,14 +1574,45 @@ const parseOptionalJsonObject = ( · 41 unmodified lines
15041574 Effect.mapError((error) => new Error(`Invalid --content JSON: ${error.message}`)),
15051575 );
15061576
1577+const ownMessage = (value: unknown): string => {
1578+ if (typeof value === "string") return value.trim();
1579+ if (typeof value === "object" && value !== null && "message" in value) {
1580+ const message = (value as { message: unknown }).message;
1581+ if (typeof message === "string") return message.trim();
1582+ }
1583+ return "";
1584+};
1585+
1586+/**
1587+ * Render an unknown failure by walking its `cause` chain. Wrapper errors
1588+ * (tagged errors carrying only `{ cause }`) have an empty own message; without
1589+ * descending, a boot failure like a data-migration error printed as literally
1590+ * "Unknown error" (issue #1403). Nested messages that merely repeat their
1591+ * parent are dropped.
1592+ */
15071593 const formatUnknownMessage = (cause: unknown): string => {
1508− if (cause instanceof Error) return cause.message;
1509− if (typeof cause === "string") return cause;
1510− if (typeof cause === "object" && cause !== null && "message" in cause) {
1511− const message = cause.message;
1512− if (typeof message === "string") return message;
1594+ const messages: string[] = [];
1595+ const seen = new Set<unknown>();
1596+ let current: unknown = cause;
1597+ while (current !== null && current !== undefined && !seen.has(current)) {
1598+ seen.add(current);
1599+ const message = ownMessage(current);
1600+ if (message.length > 0 && !messages.some((existing) => existing.includes(message))) {
1601+ messages.push(message);
1602+ }
1603+ current =
1604+ typeof current === "object" && "cause" in current
1605+ ? (current as { cause: unknown }).cause
1606+ : undefined;
15131607 }
1514− return String(cause);
1608+ if (messages.length === 0) {
1609+ return typeof cause === "string"
1610+ ? cause
1611+ : cause instanceof Error
1612+ ? cause.message
1613+ : String(cause);
1614+ }
1615+ return messages.join("\ncaused by: ");
15151616 };
15161617
15171618 const readCliLogLevel = (argv: ReadonlyArray<string>): string | undefined => {
@@ -1802,11 +1903,9 @@ const runCallHelp = ( · 284 unmodified lines
18021903 Effect.gen(function* () {
18031904 if (args.scopeDir) process.env.EXECUTOR_SCOPE_DIR = resolve(args.scopeDir);
18041905
1805− const connection = yield* resolveExecutorServerConnection({
1806− baseUrl: args.baseUrl,
1807− serverName: args.serverName,
1808− });
1809− const client = yield* makeApiClient(connection);
1906+ const target: ServerTarget = { baseUrl: args.baseUrl, serverName: args.serverName };
1907+ const connection = yield* resolveExecutorServerConnection(target);
1908+ const client = yield* makeApiClient(connection, target);
18101909 const tools = yield* client.tools.list({ query: {} });
18111910 const toolPaths = tools.map((tool) => tool.address);
18121911
@@ -1987,7 +2086,7 @@ const resumeCommand = Command.make( · 174 unmodified lines
19872086
19882087 const contentObj = yield* parseOptionalJsonObject(Option.getOrUndefined(content));
19892088
1990− const client = yield* makeApiClient(connection);
2089+ const client = yield* makeApiClient(connection, target);
19912090 const result = yield* client.executions.resume({
19922091 params: { executionId },
19932092 payload: { action, content: contentObj },
@@ -2052,8 +2151,8 @@ const toolsSearchCommand = Command.make( · 58 unmodified lines
20522151 }),
20532152 ).pipe(Command.withDescription("Search tools by natural-language query"));
20542153
2055−const toolsSourcesCommand = Command.make(
2056− "sources",
2154+const toolsIntegrationsCommand = Command.make(
2155+ "integrations",
20572156 {
20582157 query: Options.string("query").pipe(Options.optional),
20592158 limit: Options.integer("limit").pipe(Options.withDefault(50)),
@@ -2065,7 +2164,7 @@ const toolsSourcesCommand = Command.make( · 5 unmodified lines
20652164 Effect.gen(function* () {
20662165 applyScope(scope);
20672166 const target = serverTargetFromOptions({ baseUrl, server });
2068− const code = buildListSourcesCode({
2167+ const code = buildListIntegrationsCode({
20692168 query: Option.getOrUndefined(query),
20702169 limit,
20712170 });
@@ -2077,7 +2176,7 @@ const toolsSourcesCommand = Command.make( · 5 unmodified lines
20772176 outcome: result.outcome,
20782177 });
20792178 }),
2080−).pipe(Command.withDescription("List configured sources and tool counts"));
2179+).pipe(Command.withDescription("List configured integrations and tool counts"));
20812180
20822181 const toolsDescribeCommand = Command.make(
20832182 "describe",
@@ -2102,21 +2201,55 @@ const toolsDescribeCommand = Command.make( · 18 unmodified lines
21022201 ).pipe(Command.withDescription("Describe a tool's TypeScript and JSON schema"));
21032202
21042203 const toolsCommand = Command.make("tools").pipe(
2105− Command.withSubcommands([toolsSearchCommand, toolsSourcesCommand, toolsDescribeCommand] as const),
2106− Command.withDescription("Discover available tools and sources"),
2204+ Command.withSubcommands([
2205+ toolsSearchCommand,
2206+ toolsIntegrationsCommand,
2207+ toolsDescribeCommand,
2208+ ] as const),
2209+ Command.withDescription("Discover available tools and integrations"),
21072210 );
21082211
2212+const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
2213+const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
2214+
2215+const parseHeaderEnvOption = (
2216+ headerEnv: Option.Option<Record<string, string>>,
2217+): ExecutorServerHeaders | undefined => {
2218+ const values = Option.getOrUndefined(headerEnv);
2219+ if (!values) return undefined;
2220+ const headers: Record<string, { readonly kind: "env"; readonly name: string }> = {};
2221+ for (const [rawHeaderName, rawEnvName] of Object.entries(values)) {
2222+ const headerName = rawHeaderName.trim();
2223+ const envName = rawEnvName.trim();
2224+ if (!HEADER_NAME_PATTERN.test(headerName)) {
2225+ throw new Error(
2226+ `Invalid --header-env header name "${rawHeaderName}". Use an HTTP header token like CF-Access-Client-Id.`,
2227+ );
2228+ }
2229+ if (!ENV_NAME_PATTERN.test(envName)) {
2230+ throw new Error(
2231+ `Invalid --header-env env name "${rawEnvName}". Use an environment variable name like EXECUTOR_CF_ACCESS_CLIENT_ID.`,
2232+ );
2233+ }
2234+ headers[headerName] = { kind: "env", name: envName };
2235+ }
2236+ return Object.keys(headers).length > 0 ? headers : undefined;
2237+};
2238+
21092239 const profileConnectionInput = (input: {
21102240 readonly origin: string;
21112241 readonly displayName: Option.Option<string>;
21122242 readonly kind: Option.Option<"http" | "desktop-sidecar">;
2243+ readonly headerEnv: Option.Option<Record<string, string>>;
21132244 }): ExecutorServerConnectionInput => {
21142245 const selectedKind = Option.getOrUndefined(input.kind);
21152246 const displayName = Option.getOrUndefined(input.displayName);
2247+ const headers = parseHeaderEnvOption(input.headerEnv);
21162248 return {
21172249 kind: selectedKind ?? "http",
21182250 origin: input.origin,
21192251 ...(displayName ? { displayName } : {}),
2252+ ...(headers ? { headers } : {}),
21202253 };
21212254 };
21222255
@@ -2136,13 +2269,16 @@ const printServerProfiles = () => · 13 unmodified lines
21362269 origin: profile.connection.origin,
21372270 displayName: profile.connection.displayName,
21382271 auth: profile.connection.auth ? "stored-auth" : "env-auth",
2272+ headers: profile.connection.headers
2273+ ? `${Object.keys(profile.connection.headers).length} header-env`
2274+ : "no-headers",
21392275 }));
21402276 const nameWidth = rows.reduce((max, row) => Math.max(max, row.name.length), 4);
21412277 const kindWidth = rows.reduce((max, row) => Math.max(max, row.kind.length), 4);
21422278
21432279 for (const row of rows) {
21442280 console.log(
2145− `${row.marker} ${row.name.padEnd(nameWidth)} ${row.kind.padEnd(kindWidth)} ${row.origin} ${row.displayName} ${row.auth}`,
2281+ `${row.marker} ${row.name.padEnd(nameWidth)} ${row.kind.padEnd(kindWidth)} ${row.origin} ${row.displayName} ${row.auth} ${row.headers}`,
21462282 );
21472283 }
21482284 });
@@ -2160,17 +2296,23 @@ const serverAddCommand = Command.make( · 11 unmodified lines
21602296 Options.optional,
21612297 Options.withDescription("Server kind. Defaults to http."),
21622298 ),
2299+ headerEnv: Options.keyValuePair("header-env").pipe(
2300+ Options.optional,
2301+ Options.withDescription(
2302+ "HTTP header mapping header-name=ENV_VAR. Repeat for Cloudflare Access service tokens.",
2303+ ),
2304+ ),
21632305 makeDefault: Options.boolean("default").pipe(
21642306 Options.withDefault(false),
21652307 Options.withDescription("Make this profile the default server."),
21662308 ),
21672309 },
2168− ({ name, origin, displayName, kind, makeDefault }) =>
2310+ ({ name, origin, displayName, kind, headerEnv, makeDefault }) =>
21692311 Effect.gen(function* () {
21702312 const profileName = validateCliServerConnectionProfileName(name);
21712313 const store = yield* upsertCliServerConnectionProfile({
21722314 name: profileName,
2173− connection: profileConnectionInput({ origin, displayName, kind }),
2315+ connection: profileConnectionInput({ origin, displayName, kind, headerEnv }),
21742316 makeDefault,
21752317 });
21762318 const profile = findCliServerConnectionProfile(store, profileName);
@@ -2307,9 +2449,13 @@ const chooseLoginProfileName = ( · 130 unmodified lines
23072449 }
23082450 };
23092451
2452+/** Where `executor login` lands with no flags and no stored profiles. */
2453+const DEFAULT_LOGIN_ORIGIN = "https://executor.sh";
2454+
23102455 // Resolve which server a login/logout targets: an existing profile (--server
2311−// or the default) or a bare origin (--base-url). The profile name is decided
2312−// later, from the authenticated account.
2456+// or the default), a bare origin (--base-url), or hosted Executor when
2457+// nothing is configured yet. The profile name is decided later, from the
2458+// authenticated account.
23132459 const resolveLoginOrigin = (input: {
23142460 readonly baseUrl: Option.Option<string>;
23152461 readonly server: Option.Option<string>;
@@ -2337,11 +2483,7 @@ const resolveLoginOrigin = (input: { · 21 unmodified lines
23372483 const store = yield* readCliServerConnectionStore();
23382484 const profile = defaultCliServerConnectionProfile(store);
23392485 if (profile) return { origin: profile.connection.origin, profile };
2340− return yield* Effect.fail(
2341− new Error(
2342− "No server to log in to. Pass --base-url <https://your-host> or --server <profile>.",
2343− ),
2344− );
2486+ return { origin: DEFAULT_LOGIN_ORIGIN, profile: null };
23452487 });
23462488
23472489 const loginNameOption = Options.string("name").pipe(
@@ -2366,12 +2508,21 @@ const loginCommand = Command.make( · 18 unmodified lines
23662508 Effect.gen(function* () {
23672509 const target = yield* resolveLoginOrigin({ baseUrl, server });
23682510 const explicitName = Option.getOrUndefined(name);
2511+ // The target may have been picked implicitly (default profile, or the
2512+ // hosted fallback): say where the login is going before the device flow.
2513+ console.log(`Signing in to ${target.origin}`);
2514+ const targetProfile = target.profile;
2515+ const headers = targetProfile
2516+ ? yield* resolveExecutorServerConfiguredHeaders(targetProfile.connection, process.env).pipe(
2517+ Effect.mapError(toError),
2518+ )
2519+ : {};
23692520 const discovery = yield* Effect.tryPromise({
2370− try: () => discoverCliLogin(target.origin),
2521+ try: () => discoverCliLogin(target.origin, { headers }),
23712522 catch: toError,
23722523 });
23732524 const grant = yield* Effect.tryPromise({
2374− try: () => requestDeviceCode(discovery),
2525+ try: () => requestDeviceCode(discovery, { serverOrigin: target.origin, headers }),
23752526 catch: toError,
23762527 });
23772528 const verifyUrl = grant.verificationUriComplete ?? grant.verificationUri;
@@ -2382,7 +2533,7 @@ const loginCommand = Command.make( · 4 unmodified lines
23822533 if (!noBrowser) openBrowser(verifyUrl);
23832534 console.log("Waiting for you to approve in the browser...");
23842535 const tokens = yield* Effect.tryPromise({
2385− try: () => pollForDeviceTokens(discovery, grant),
2536+ try: () => pollForDeviceTokens(discovery, grant, { serverOrigin: target.origin, headers }),
23862537 catch: toError,
23872538 });
23882539
@@ -2407,6 +2558,9 @@ const loginCommand = Command.make( · 18 unmodified lines
24072558 kind: "http",
24082559 origin: target.origin,
24092560 ...(email ? { displayName: email } : {}),
2561+ ...(target.profile?.connection.headers
2562+ ? { headers: target.profile.connection.headers }
2563+ : {}),
24102564 auth: {
24112565 kind: "oauth",
24122566 accessToken: tokens.accessToken,
@@ -2420,12 +2574,24 @@ const loginCommand = Command.make( · 7 unmodified lines
24202574 });
24212575
24222576 console.log("");
2423− console.log(`Logged in to ${target.origin} (profile "${profileName}", now the default).`);
2577+ // Profile bookkeeping stays backstage unless the user works with named
2578+ // profiles (--name, or a store that already has several).
2579+ const mentionProfile =
2580+ explicitName !== undefined || (yield* readCliServerConnectionStore()).profiles.length > 1;
2581+ console.log(
2582+ mentionProfile
2583+ ? `Logged in to ${target.origin} (profile "${profileName}", now the default).`
2584+ : `Logged in to ${target.origin}.`,
2585+ );
24242586 if (email) console.log(`Account: ${email}`);
24252587 if (org) console.log(`Organization: ${org}`);
24262588 else if (sub) console.log(`User: ${sub}`);
24272589 }),
2428−).pipe(Command.withDescription("Sign in to a hosted Executor server in the browser (device flow)"));
2590+).pipe(
2591+ Command.withDescription(
2592+ "Sign in to a hosted Executor server in the browser (device flow). Defaults to https://executor.sh.",
2593+ ),
2594+);
24292595
24302596 const logoutCommand = Command.make(
24312597 "logout",
@@ -2441,8 +2607,14 @@ const logoutCommand = Command.make( · 9 unmodified lines
24412607 console.log(`No stored login for ${target.origin}.`);
24422608 return;
24432609 }
2610+ // Profile bookkeeping stays backstage unless the user addressed one.
2611+ const mentionProfile = Option.isSome(server) || store.profiles.length > 1;
24442612 if (!profile.connection.auth) {
2445− console.log(`Profile "${profile.name}" has no stored credentials.`);
2613+ console.log(
2614+ mentionProfile
2615+ ? `Profile "${profile.name}" has no stored credentials.`
2616+ : `Not signed in to ${target.origin}.`,
2617+ );
24462618 return;
24472619 }
24482620 yield* upsertCliServerConnectionProfile({
@@ -2451,11 +2623,14 @@ const logoutCommand = Command.make( · 2 unmodified lines
24512623 kind: profile.connection.kind,
24522624 origin: profile.connection.origin,
24532625 displayName: profile.connection.displayName,
2626+ ...(profile.connection.headers ? { headers: profile.connection.headers } : {}),
24542627 },
24552628 makeDefault: store.defaultProfile === profile.name,
24562629 });
24572630 console.log(
2458− `Logged out of ${profile.connection.origin} (cleared credentials for "${profile.name}").`,
2631+ mentionProfile
2632+ ? `Logged out of ${profile.connection.origin} (cleared credentials for "${profile.name}").`
2633+ : `Logged out of ${profile.connection.origin}.`,
24592634 );
24602635 }),
24612636 ).pipe(Command.withDescription("Clear stored credentials for a server profile"));
@@ -2709,11 +2884,25 @@ const mcpCommand = Command.make( · 247 unmodified lines
27092884 "Choose the stdio approval flow: browser approval or a CLI resume tool exposed to the model.",
27102885 ),
27112886 ),
2887+ noArtifacts: Options.boolean("no-artifacts")
2888+ .pipe(Options.withDefault(false))
2889+ .pipe(
2890+ Options.withDescription(
2891+ "Withhold the artifact surface from this connection: the artifact tools, the app shell resource, and the artifact skills. Served by default.",
2892+ ),
2893+ ),
2894+ searchTools: Options.boolean("search-tools")
2895+ .pipe(Options.withDefault(false))
2896+ .pipe(
2897+ Options.withDescription(
2898+ "Serve one search_<integration> tool per connected integration. Off by default; each routes through the same flow as tools.search inside execute.",
2899+ ),
2900+ ),
27122901 },
2713− ({ scope, elicitationMode }) =>
2902+ ({ scope, elicitationMode, noArtifacts, searchTools }) =>
27142903 Effect.gen(function* () {
27152904 applyScope(scope);
2716− yield* runStdioMcpSession({ elicitationMode });
2905+ yield* runStdioMcpSession({ elicitationMode, artifacts: !noArtifacts, searchTools });
27172906 }),
27182907 ).pipe(Command.withDescription("Start an MCP server over stdio"));
27192908
@@ -3164,7 +3353,12 @@ const program = ( · 444 unmodified lines
31643353 } else {
31653354 console.error(renderCliError(cause));
31663355 }
3167− process.exitCode = 1;
3356+ // Exit hard, not via exitCode: this handler converts the failure into a
3357+ // success, so runMain's teardown never force-exits, and background fibers
3358+ // (the integrations-refresh fork) keep the event loop alive. A daemon
3359+ // that failed boot then lingers with its port unbound, and the desktop
3360+ // waits on the ready sentinel forever (issue #1403).
3361+ process.exit(1);
31683362 }),
31693363 ),
31703364 );
apps/cli/src/native-bindings.ts+36−5
@@ -1,11 +1,10 @@
11 // ---------------------------------------------------------------------------
22 // Native-binding bootstrap for the `bun build --compile` binary.
33 //
4−// `bun --compile` bundles JS into bunfs but does NOT include `.node` native
5−// addons, so a dynamic `require('@libsql/<platform>')` / keyring walk inside
6−// the binary fails. build.ts copies each platform's `.node` next to the
7−// executable (`libsql.node`, `keyring.node`); here we publish their on-disk
8−// paths via env vars the loaders read.
4+// `bun --compile` bundles JS into bunfs but does NOT include native binaries,
5+// wasm sidecars, or runtime-read package files. build.ts copies those artifacts
6+// next to the executable; here we publish their on-disk paths via env vars the
7+// loaders read.
98 //
109 // This MUST be the FIRST import in main.ts. ES modules evaluate every import
1110 // before the importer's own body, and libSQL resolves its native addon EAGERLY
@@ -18,6 +17,8 @@ · 6 unmodified lines
1817 import { existsSync } from "node:fs";
1918 import { dirname, join } from "node:path";
2019
20+import { WORKER_BUNDLER_DIRNAME, missingWorkerBundlerFiles } from "./worker-bundler-artifact";
21+
2122 const execDir = dirname(process.execPath);
2223
2324 // libSQL: our `libsql` patch reads EXECUTOR_LIBSQL_NATIVE_PATH and loads the
@@ -43,3 +44,33 @@ if ( · 19 unmodified lines
4344 ) {
4445 process.env.EXECUTOR_KEYRING_NATIVE_PATH = keyringNodeOnDisk;
4546 }
47+
48+const workerdOnDisk = join(execDir, process.platform === "win32" ? "workerd.exe" : "workerd");
49+if (typeof Bun !== "undefined" && !process.env.EXECUTOR_WORKERD_BIN && existsSync(workerdOnDisk)) {
50+ process.env.EXECUTOR_WORKERD_BIN = workerdOnDisk;
51+}
52+
53+// worker-bundler: the compiled binary cannot resolve `@cloudflare/worker-bundler`
54+// by name (bunfs has no node_modules), so build.ts stages the package's dist
55+// beside the executable and we publish its path here. An absent directory is
56+// normal off the packaged path (dev, `bun run`), so that stays quiet — but a
57+// directory that is PRESENT AND INCOMPLETE is a broken install, and swallowing
58+// it is what turns a packaging slip into an unresolvable bare import at
59+// startup. Report it on stderr instead of failing open.
60+const workerBundlerOnDisk = join(execDir, WORKER_BUNDLER_DIRNAME);
61+if (
62+ typeof Bun !== "undefined" &&
63+ !process.env.EXECUTOR_WORKER_BUNDLER_DIR &&
64+ existsSync(workerBundlerOnDisk)
65+) {
66+ const missing = missingWorkerBundlerFiles(workerBundlerOnDisk, existsSync);
67+ if (missing.length === 0) {
68+ process.env.EXECUTOR_WORKER_BUNDLER_DIR = workerBundlerOnDisk;
69+ } else {
70+ process.stderr.write(
71+ `executor: the bundled Worker toolchain at ${workerBundlerOnDisk} is incomplete ` +
72+ `(missing ${missing.join(", ")}). Features that build Workers will be unavailable; ` +
73+ `reinstall or update executor to repair it.\n`,
74+ );
75+ }
76+}
apps/cli/src/server-connection.test.ts+84−0
@@ -1,8 +1,11 @@
11 import { describe, expect, it } from "@effect/vitest";
22
3+import { normalizeExecutorServerConnection } from "@executor-js/sdk/shared";
4+
35 import {
46 canAutoStartCliServerConnection,
57 chooseCliServerConnectionWithActiveLocal,
8+ describeUnauthorizedCliServer,
69 parseCliExecutorServerConnection,
710 withCliServerAuthFallback,
811 } from "./server-connection";
@@ -135,3 +138,84 @@ describe("CLI server connection", () => { · 126 unmodified lines
135138 ).toBe("conflict");
136139 });
137140 });
141+
142+describe("describeUnauthorizedCliServer", () => {
143+ it("hints plain `executor login` when the server was picked implicitly", () => {
144+ const connection = normalizeExecutorServerConnection({
145+ key: "profile:rhys-executor.sh",
146+ origin: "https://executor.sh",
147+ auth: { kind: "oauth", accessToken: "stale", expiresAt: 1 },
148+ });
149+
150+ const message = describeUnauthorizedCliServer({
151+ connection,
152+ cliPrefix: "executor",
153+ target: {},
154+ });
155+ expect(message).toContain("You're signed out of https://executor.sh");
156+ expect(message).toContain("Run `executor login` to sign in again.");
157+ // Profile names are plumbing: never surfaced when the user didn't type one.
158+ expect(message).not.toContain("--server");
159+ expect(message).not.toContain("profile");
160+ });
161+
162+ it("echoes --server back when the user targeted a named profile", () => {
163+ const connection = normalizeExecutorServerConnection({
164+ key: "profile:work",
165+ origin: "https://executor.example",
166+ auth: { kind: "oauth", accessToken: "stale", expiresAt: 1 },
167+ });
168+
169+ const message = describeUnauthorizedCliServer({
170+ connection,
171+ cliPrefix: "executor",
172+ target: { serverName: "work" },
173+ });
174+ expect(message).toContain("You're signed out of https://executor.example");
175+ expect(message).toContain("executor login --server work");
176+ });
177+
178+ it("echoes --base-url back when the user targeted an origin", () => {
179+ const connection = normalizeExecutorServerConnection({
180+ origin: "https://executor.example",
181+ });
182+
183+ const message = describeUnauthorizedCliServer({
184+ connection,
185+ cliPrefix: "executor",
186+ target: { baseUrl: "https://executor.example" },
187+ });
188+ expect(message).toContain("no credentials are stored");
189+ expect(message).toContain("executor login --base-url https://executor.example");
190+ });
191+
192+ it("mentions the env vars when a profile-less bearer key is rejected", () => {
193+ const connection = normalizeExecutorServerConnection({
194+ origin: "https://executor.example",
195+ auth: { kind: "bearer", token: "key_bad" },
196+ });
197+
198+ const message = describeUnauthorizedCliServer({
199+ connection,
200+ cliPrefix: "executor",
201+ target: {},
202+ });
203+ expect(message).toContain("rejected the stored bearer credentials");
204+ expect(message).toContain("EXECUTOR_API_KEY");
205+ });
206+
207+ it("uses the dev entrypoint prefix verbatim", () => {
208+ const connection = normalizeExecutorServerConnection({
209+ key: "profile:hosted",
210+ origin: "https://executor.example",
211+ auth: { kind: "oauth", accessToken: "stale" },
212+ });
213+
214+ const message = describeUnauthorizedCliServer({
215+ connection,
216+ cliPrefix: "bun run apps/cli/src/main.ts",
217+ target: {},
218+ });
219+ expect(message).toContain("bun run apps/cli/src/main.ts login");
220+ });
221+});
apps/cli/src/server-connection.ts+49−0
@@ -52,6 +52,55 @@ export const canAutoStartCliServerConnection = (connection: ExecutorServerConnec · 51 unmodified lines
5252 return url.protocol === "http:" && canAutoStartLocalDaemonForHost(url.hostname);
5353 };
5454
55+export const profileNameFromConnectionKey = (key: string): string | null =>
56+ key.startsWith("profile:") ? key.slice("profile:".length) : null;
57+
58+// A 401 from the server means the stored credential is stale, revoked, or
59+// missing: a sign-in problem, not a transport problem. Rendered instead of
60+// the raw HTTP client error so the user learns the recovery command, not the
61+// failing endpoint.
62+//
63+// The hint mirrors how the user addressed the server. The common case types
64+// no flags (the default profile, normally executor.sh), and login re-targets
65+// that same default, so the fix is plain `executor login`: profile names are
66+// plumbing and stay invisible. Only an explicit --server / --base-url comes
67+// back in the hint.
68+export const describeUnauthorizedCliServer = (input: {
69+ readonly connection: ExecutorServerConnection;
70+ readonly cliPrefix: string;
71+ readonly target: { readonly baseUrl?: string; readonly serverName?: string };
72+}): string => {
73+ const { connection, cliPrefix, target } = input;
74+ const loginCommand = target.serverName
75+ ? `${cliPrefix} login --server ${target.serverName}`
76+ : target.baseUrl
77+ ? `${cliPrefix} login --base-url ${connection.origin}`
78+ : `${cliPrefix} login`;
79+
80+ if (!connection.auth) {
81+ return [
82+ `${connection.origin} requires authentication, and no credentials are stored for it.`,
83+ `Run \`${loginCommand}\` to sign in.`,
84+ ].join("\n");
85+ }
86+
87+ if (connection.auth.kind === "oauth") {
88+ return [
89+ `You're signed out of ${connection.origin}.`,
90+ `Run \`${loginCommand}\` to sign in again.`,
91+ ].join("\n");
92+ }
93+
94+ const envHint =
95+ connection.auth.kind === "bearer" && !profileNameFromConnectionKey(connection.key)
96+ ? " If the key came from EXECUTOR_API_KEY or EXECUTOR_AUTH_TOKEN, check that value."
97+ : "";
98+ return [
99+ `${connection.origin} rejected the stored ${connection.auth.kind} credentials (401).${envHint}`,
100+ `Run \`${loginCommand}\` to sign in again.`,
101+ ].join("\n");
102+};
103+
55104 export type CliServerConnectionSource =
56105 | "explicit"
57106 | "default-profile"
apps/cli/src/server-profile.test.ts+86−5
@@ -1,6 +1,6 @@
11 import { afterEach, describe, expect, it } from "@effect/vitest";
22 import { BunServices } from "@effect/platform-bun";
3−import { mkdtempSync, rmSync } from "node:fs";
3+import { chmodSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
44 import { tmpdir } from "node:os";
55 import { join } from "node:path";
66 import * as Effect from "effect/Effect";
@@ -25,7 +25,7 @@ afterEach(() => { · 18 unmodified lines
2525 });
2626
2727 describe("CLI server connection profiles", () => {
28− it("round-trips named server connections and default selection", () =>
28+ it.effect("round-trips named server connections and default selection", () =>
2929 Effect.gen(function* () {
3030 const dataDir = mkdtempSync(join(tmpdir(), "executor-server-profiles-"));
3131 process.env.EXECUTOR_DATA_DIR = dataDir;
@@ -36,6 +36,9 @@ describe("CLI server connection profiles", () => { · 4 unmodified lines
3636 connection: {
3737 origin: "https://executor.example/api",
3838 auth: { kind: "bearer", token: "key_123" },
39+ headers: {
40+ "CF-Access-Client-Id": { kind: "env", name: "EXECUTOR_CF_ACCESS_CLIENT_ID" },
41+ },
3942 },
4043 makeDefault: true,
4144 });
@@ -50,13 +53,17 @@ describe("CLI server connection profiles", () => { · 8 unmodified lines
5053 kind: "bearer",
5154 token: "key_123",
5255 });
56+ expect(store.profiles[0]?.connection.headers).toEqual({
57+ "CF-Access-Client-Id": { kind: "env", name: "EXECUTOR_CF_ACCESS_CLIENT_ID" },
58+ });
5359 expect(defaultCliServerConnectionProfile(store)?.name).toBe("remote");
5460 } finally {
5561 rmSync(dataDir, { recursive: true, force: true });
5662 }
57− }).pipe(Effect.provide(BunServices.layer)));
63+ }).pipe(Effect.provide(BunServices.layer)),
64+ );
5865
59− it("switches and removes the default profile", () =>
66+ it.effect("switches and removes the default profile", () =>
6067 Effect.gen(function* () {
6168 const dataDir = mkdtempSync(join(tmpdir(), "executor-server-profiles-"));
6269 process.env.EXECUTOR_DATA_DIR = dataDir;
@@ -82,7 +89,8 @@ describe("CLI server connection profiles", () => { · 19 unmodified lines
8289 } finally {
8390 rmSync(dataDir, { recursive: true, force: true });
8491 }
85− }).pipe(Effect.provide(BunServices.layer)));
92+ }).pipe(Effect.provide(BunServices.layer)),
93+ );
8694
8795 it("drops malformed profiles when parsing", () => {
8896 const store = parseCliServerConnectionStore(
@@ -114,6 +122,12 @@ describe("CLI server connection profiles", () => { · 25 unmodified lines
114122 key: "desktop-sidecar",
115123 origin: "http://127.0.0.1:4789",
116124 auth: { kind: "basic", username: "executor", password: "secret" },
125+ headers: {
126+ "CF-Access-Client-Id": {
127+ kind: "env",
128+ name: "EXECUTOR_CF_ACCESS_CLIENT_ID",
129+ },
130+ },
117131 },
118132 },
119133 ],
@@ -127,5 +141,72 @@ describe("CLI server connection profiles", () => { · 7 unmodified lines
127141 username: "executor",
128142 password: "secret",
129143 });
144+ expect(store.profiles[0]?.connection.headers).toEqual({
145+ "CF-Access-Client-Id": { kind: "env", name: "EXECUTOR_CF_ACCESS_CLIENT_ID" },
146+ });
130147 });
148+
149+ // -------------------------------------------------------------------------
150+ // File permissions.
151+ //
152+ // This store holds live credentials for a hosted server — a bearer token, or
153+ // an OAuth access token and its long-lived refresh token. Nothing pinned its
154+ // mode before, so it was created with the process umask (0644 by default) and
155+ // readable by every other account on the machine.
156+ //
157+ // Both cases are covered because they need different mechanisms: `mode` on
158+ // create, and a `chmod` for the overwrite. The overwrite is the common path
159+ // here, since the store is rewritten on every silent token refresh.
160+ // -------------------------------------------------------------------------
161+
162+ it.effect("creates the credential store owner-only", () =>
163+ Effect.gen(function* () {
164+ const dataDir = mkdtempSync(join(tmpdir(), "executor-server-profiles-mode-"));
165+ process.env.EXECUTOR_DATA_DIR = dataDir;
166+
167+ try {
168+ yield* upsertCliServerConnectionProfile({
169+ name: "remote",
170+ connection: {
171+ origin: "https://executor.example",
172+ auth: { kind: "bearer", token: "key_should_not_be_world_readable" },
173+ },
174+ makeDefault: true,
175+ });
176+
177+ const mode = statSync(join(dataDir, "server-connections.json")).mode & 0o777;
178+ expect(mode).toBe(0o600);
179+ } finally {
180+ rmSync(dataDir, { recursive: true, force: true });
181+ }
182+ }).pipe(Effect.provide(BunServices.layer)),
183+ );
184+
185+ it.effect("tightens a pre-existing world-readable credential store on rewrite", () =>
186+ Effect.gen(function* () {
187+ const dataDir = mkdtempSync(join(tmpdir(), "executor-server-profiles-chmod-"));
188+ process.env.EXECUTOR_DATA_DIR = dataDir;
189+ const storePath = join(dataDir, "server-connections.json");
190+
191+ try {
192+ // A store left behind by an older version, world-readable. `mode` on
193+ // write is ignored for an existing file, so only the chmod fixes this.
194+ writeFileSync(storePath, JSON.stringify({ version: 1, profiles: [] }));
195+ chmodSync(storePath, 0o644);
196+
197+ yield* upsertCliServerConnectionProfile({
198+ name: "remote",
199+ connection: {
200+ origin: "https://executor.example",
201+ auth: { kind: "bearer", token: "key_should_not_be_world_readable" },
202+ },
203+ makeDefault: true,
204+ });
205+
206+ expect(statSync(storePath).mode & 0o777).toBe(0o600);
207+ } finally {
208+ rmSync(dataDir, { recursive: true, force: true });
209+ }
210+ }).pipe(Effect.provide(BunServices.layer)),
211+ );
131212 });
apps/cli/src/server-profile.ts+22−4
@@ -62,6 +62,11 @@ const PersistedAuth = Schema.Union([ · 61 unmodified lines
6262 }),
6363 ]);
6464
65+const PersistedHeader = Schema.Struct({
66+ kind: Schema.Literal("env"),
67+ name: Schema.String,
68+});
69+
6570 const PersistedConnection = Schema.Struct({
6671 kind: Schema.optional(Schema.Literals(["http", "desktop-sidecar"])),
6772 key: Schema.optional(Schema.String),
@@ -69,6 +74,7 @@ const PersistedConnection = Schema.Struct({ · 1 unmodified line
6974 apiBaseUrl: Schema.optional(Schema.String),
7075 displayName: Schema.optional(Schema.String),
7176 auth: Schema.optional(PersistedAuth),
77+ headers: Schema.optional(Schema.Record(Schema.String, PersistedHeader)),
7278 });
7379
7480 const PersistedProfile = Schema.Struct({
@@ -144,10 +150,22 @@ export const writeCliServerConnectionStore = ( · 69 unmodified lines
144150 const path = yield* Path.Path;
145151 const dataDir = resolveDataDir(path);
146152 yield* fs.makeDirectory(dataDir, { recursive: true });
147− yield* fs.writeFileString(
148− serverConnectionStorePath(path),
149− serializeCliServerConnectionStore(store),
150− );
153+ const storePath = serverConnectionStorePath(path);
154+ // This store holds live credentials for a hosted server — a bearer token, or
155+ // an OAuth access token AND its long-lived refresh token — so create it
156+ // owner-only, exactly as the local-server manifest does for the sibling
157+ // secret it keeps under `server-control/`.
158+ //
159+ // Both steps are needed, and the second matters more here than it does
160+ // there. `mode` applies only when the file is CREATED, so it closes the
161+ // window where a fresh store is briefly world-readable. The `chmod` covers
162+ // overwriting a store that already exists with looser permissions — and this
163+ // file is rewritten on every silent token refresh, so overwrite is the
164+ // common path, not the rare one.
165+ yield* fs.writeFileString(storePath, serializeCliServerConnectionStore(store), {
166+ mode: 0o600,
167+ });
168+ yield* fs.chmod(storePath, 0o600).pipe(Effect.ignore);
151169 });
152170
153171 export const upsertCliServerConnectionProfile = (input: {
apps/cli/src/service.test.ts+30−0
@@ -185,6 +185,36 @@ describe("service unit generation", () => { · 184 unmodified lines
185185 expect(wrapper).not.toContain("EXECUTOR_AUTH_PASSWORD");
186186 });
187187
188+ it("preserves custom TLS trust paths in the supervised environment", () => {
189+ const previousNodeExtraCaCerts = process.env.NODE_EXTRA_CA_CERTS;
190+ const previousSslCertFile = process.env.SSL_CERT_FILE;
191+ const previousSslCertDir = process.env.SSL_CERT_DIR;
192+ process.env.NODE_EXTRA_CA_CERTS = "/Users/x/.certs/node-extra.pem";
193+ process.env.SSL_CERT_FILE = "/Users/x/.certs/combined.pem";
194+ process.env.SSL_CERT_DIR = "/Users/x/.certs";
195+
196+ const wrapper = generateWindowsDaemonWrapper(
197+ {
198+ executablePath: "C:\\Program Files\\Executor\\executor.exe",
199+ port: 4789,
200+ version: "1.5.10",
201+ },
202+ "C:\\Users\\x\\.executor",
203+ "C:\\Users\\x\\.executor\\logs",
204+ );
205+
206+ if (previousNodeExtraCaCerts === undefined) delete process.env.NODE_EXTRA_CA_CERTS;
207+ else process.env.NODE_EXTRA_CA_CERTS = previousNodeExtraCaCerts;
208+ if (previousSslCertFile === undefined) delete process.env.SSL_CERT_FILE;
209+ else process.env.SSL_CERT_FILE = previousSslCertFile;
210+ if (previousSslCertDir === undefined) delete process.env.SSL_CERT_DIR;
211+ else process.env.SSL_CERT_DIR = previousSslCertDir;
212+
213+ expect(wrapper).toContain('set "NODE_EXTRA_CA_CERTS=/Users/x/.certs/node-extra.pem"');
214+ expect(wrapper).toContain('set "SSL_CERT_FILE=/Users/x/.certs/combined.pem"');
215+ expect(wrapper).toContain('set "SSL_CERT_DIR=/Users/x/.certs"');
216+ });
217+
188218 it("sanitizes cmd.exe metacharacters in baked env values (cmdSetValue)", () => {
189219 // A `"` in PATH would close the `set "PATH=..."` quote early and let a
190220 // `& cmd &` fragment run at boot as the user; strip it (illegal in a path
apps/cli/src/service.ts+11−0
@@ -177,6 +177,17 @@ const serviceEnvironment = ( · 176 unmodified lines
177177 "EXECUTOR_SENTRY_RELEASE",
178178 "EXECUTOR_SENTRY_ENVIRONMENT",
179179 "EXECUTOR_RUN_ID",
180+ // Analytics opt-out must survive into the supervised unit's minimal env,
181+ // or an opted-out install would silently re-enable analytics under launchd.
182+ "DO_NOT_TRACK",
183+ "EXECUTOR_DISABLE_ANALYTICS",
184+ // Preserve the caller's TLS trust configuration. Corporate/intercepting
185+ // CAs commonly live outside the OS trust store; dropping these paths in a
186+ // minimal launchd/systemd environment makes HTTPS integrations fail only
187+ // after service installation while the same CLI call succeeds.
188+ "NODE_EXTRA_CA_CERTS",
189+ "SSL_CERT_FILE",
190+ "SSL_CERT_DIR",
180191 ] as const;
181192 const passThrough = Object.fromEntries(
182193 passThroughKeys.flatMap((key) => {
apps/cli/src/tooling.ts+2−2
@@ -408,14 +408,14 @@ export const buildSearchToolsCode = (input: { · 407 unmodified lines
408408 return `return await tools.search(${JSON.stringify(payload)});`;
409409 };
410410
411−export const buildListSourcesCode = (input: { query?: string; limit: number }): string => {
411+export const buildListIntegrationsCode = (input: { query?: string; limit: number }): string => {
412412 const payload: Record<string, unknown> = {
413413 limit: input.limit,
414414 };
415415 if (input.query && input.query.trim().length > 0) {
416416 payload.query = input.query;
417417 }
418− return `return await tools.executor.sources.list(${JSON.stringify(payload)});`;
418+ return `return await tools.executor.integrations.list(${JSON.stringify(payload)});`;
419419 };
420420
421421 export const buildDescribeToolCode = (toolPath: string): string =>
apps/cli/src/worker-bundler-artifact.test.ts+59−0
@@ -0,0 +1,59 @@
1+import { describe, expect, it } from "@effect/vitest";
2+import { join } from "node:path";
3+
4+import {
5+ WORKER_BUNDLER_DIRNAME,
6+ WORKER_BUNDLER_RUNTIME_FILES,
7+ missingWorkerBundlerFiles,
8+} from "./worker-bundler-artifact";
9+
10+const STAGED_DIR = join("/opt/executor/bin", WORKER_BUNDLER_DIRNAME);
11+
12+/** An existence probe that reports only the given POSIX-relative paths. */
13+const staged =
14+ (...relatives: readonly string[]) =>
15+ (path: string): boolean =>
16+ relatives.some((relative) => path === join(STAGED_DIR, ...relative.split("/")));
17+
18+describe("missingWorkerBundlerFiles", () => {
19+ it("reports nothing when the full artifact is staged", () => {
20+ expect(missingWorkerBundlerFiles(STAGED_DIR, staged(...WORKER_BUNDLER_RUNTIME_FILES))).toEqual(
21+ [],
22+ );
23+ });
24+
25+ it("reports every file when the directory is empty", () => {
26+ expect(missingWorkerBundlerFiles(STAGED_DIR, () => false)).toEqual([
27+ ...WORKER_BUNDLER_RUNTIME_FILES,
28+ ]);
29+ });
30+
31+ // The regression this whole seam exists for: the packaged daemon reads the
32+ // wasm from disk beside the entry. A copy that brought the JS but dropped the
33+ // wasm used to pass the build and fail on the user's machine at startup.
34+ it("reports the esbuild wasm when only the JS entrypoints are staged", () => {
35+ expect(
36+ missingWorkerBundlerFiles(STAGED_DIR, staged("dist/index.js", "dist/index.bundled.js")),
37+ ).toEqual(["dist/esbuild.wasm"]);
38+ });
39+
40+ // `index.bundled.js` is the file consumers load — the package's own
41+ // `dist/index.js` still has bare imports nothing can resolve inside the
42+ // compiled binary. Staging index.js alone is not enough.
43+ it("reports the packed entry when only the unbundled package entry is staged", () => {
44+ expect(
45+ missingWorkerBundlerFiles(STAGED_DIR, staged("dist/index.js", "dist/esbuild.wasm")),
46+ ).toEqual(["dist/index.bundled.js"]);
47+ });
48+
49+ it("names files relative to the staged directory so errors are quotable", () => {
50+ for (const relative of WORKER_BUNDLER_RUNTIME_FILES) {
51+ expect(relative.startsWith("dist/")).toBe(true);
52+ }
53+ });
54+
55+ it("requires the packed entry and the wasm, not just the package entry", () => {
56+ expect(WORKER_BUNDLER_RUNTIME_FILES).toContain("dist/index.bundled.js");
57+ expect(WORKER_BUNDLER_RUNTIME_FILES).toContain("dist/esbuild.wasm");
58+ });
59+});
apps/cli/src/worker-bundler-artifact.ts+55−0
@@ -0,0 +1,55 @@
1+// ---------------------------------------------------------------------------
2+// The packed `@cloudflare/worker-bundler` artifact that ships NEXT TO the
3+// compiled binary.
4+//
5+// `bun build --compile` cannot embed a package that is only ever addressed by
6+// bare specifier at runtime: bunfs has no `node_modules`, so an
7+// `import("@cloudflare/worker-bundler")` evaluated inside the binary resolves
8+// against the virtual root and throws
9+// ResolveMessage: Cannot find module '@cloudflare/worker-bundler'
10+// The build therefore copies the package's `dist/` next to the executable and
11+// `native-bindings.ts` publishes its absolute path as
12+// EXECUTOR_WORKER_BUNDLER_DIR. Consumers must load from that directory; a bare
13+// specifier is unresolvable in the packaged daemon by construction.
14+//
15+// This module is the single source of truth for which files that contract
16+// needs, so the build-time staging check and the runtime lookup cannot drift.
17+// They previously did: the build wrote `dist/index.bundled.js` (the file
18+// consumers actually load) while the runtime check only looked for
19+// `dist/index.js` and `dist/esbuild.wasm`. That drift fails OPEN — an
20+// incomplete staged copy passes the build, the runtime silently declines to
21+// publish the env var, and the consumer falls through to the bare specifier.
22+// ---------------------------------------------------------------------------
23+
24+import { join } from "node:path";
25+
26+/** Directory name staged beside the executable, and looked up from execDir. */
27+export const WORKER_BUNDLER_DIRNAME = "worker-bundler";
28+
29+/**
30+ * Files the colocated copy must contain to be usable.
31+ *
32+ * `index.bundled.js` is the entrypoint consumers load (the build packs it with
33+ * esbuild so it has no bare-specifier imports of its own); `esbuild.wasm` is
34+ * read at runtime beside it; `index.js` is the package's own entry, kept so the
35+ * staged directory stays a faithful copy rather than a lookalike.
36+ */
37+export const WORKER_BUNDLER_RUNTIME_FILES = [
38+ "dist/index.js",
39+ "dist/index.bundled.js",
40+ "dist/esbuild.wasm",
41+] as const;
42+
43+/**
44+ * Which required files are absent from a staged worker-bundler directory.
45+ *
46+ * Takes the existence probe as a parameter so the build script, the runtime
47+ * bootstrap, and tests all agree on the requirement without touching a real
48+ * filesystem. Returns the POSIX-relative names, which is what error messages
49+ * should quote.
50+ */
51+export const missingWorkerBundlerFiles = (
52+ dir: string,
53+ exists: (path: string) => boolean,
54+): readonly string[] =>
55+ WORKER_BUNDLER_RUNTIME_FILES.filter((relative) => !exists(join(dir, ...relative.split("/"))));
apps/cli/tsconfig.json+1−1
@@ -8,7 +8,7 @@ · 7 unmodified lines
88 "skipLibCheck": true,
99 "noEmit": true,
1010 "declaration": true,
11− "types": ["bun-types"]
11+ "types": ["bun-types", "node"]
1212 },
1313 "include": ["src", "bin"]
1414 }
apps/cloud/CHANGELOG.md+293−0
@@ -1,5 +1,298 @@
11 # @executor-js/cloud
22
3+## 1.4.61
4+
5+### Patch Changes
6+
7+- Updated dependencies [[`c11bef2`](https://github.com/UsefulSoftwareCo/executor/commit/c11bef2cd049db7bbf51b15e18761b14acccb534), [`46cea2c`](https://github.com/UsefulSoftwareCo/executor/commit/46cea2cbb1f414ae58ac876819a51b11967909a6), [`a2d1417`](https://github.com/UsefulSoftwareCo/executor/commit/a2d141758e478274813c8c24d354e1fd0f66af49), [`2bdbedf`](https://github.com/UsefulSoftwareCo/executor/commit/2bdbedf257f54d7c209e8c856c618174c10d6bb3), [`0b0b74f`](https://github.com/UsefulSoftwareCo/executor/commit/0b0b74f673b8098c5248159be36c648097f3c87b), [`256e25e`](https://github.com/UsefulSoftwareCo/executor/commit/256e25e7b291b0c023bc7547d092004b66781bba)]:
8+ - @executor-js/plugin-mcp@1.6.0
9+ - @executor-js/plugin-openapi@1.6.0
10+ - @executor-js/sdk@1.6.0
11+ - @executor-js/react@1.4.63
12+ - @executor-js/runtime-dynamic-worker@1.4.4
13+ - @executor-js/api@1.4.63
14+ - @executor-js/execution@1.6.0
15+ - @executor-js/vite-plugin@0.0.60
16+ - @executor-js/cloudflare@0.0.42
17+ - @executor-js/host-mcp@1.4.4
18+ - @executor-js/mcp-apps-shell@1.4.11
19+ - @executor-js/plugin-graphql@1.6.0
20+ - @executor-js/plugin-toolkits@1.5.35
21+ - @executor-js/plugin-workos-vault@0.0.2
22+ - @executor-js/runtime-quickjs@1.6.0
23+
24+## 1.4.60
25+
26+### Patch Changes
27+
28+- Updated dependencies [[`d3f0617`](https://github.com/UsefulSoftwareCo/executor/commit/d3f0617deec06c57e0d6e1479fe668f79daf977d), [`86c68af`](https://github.com/UsefulSoftwareCo/executor/commit/86c68afef9bf8b7c19ab58f59acfedca0b3c4ca7), [`32206c7`](https://github.com/UsefulSoftwareCo/executor/commit/32206c7f78654f638bfd27c25c71c30c3d6354be), [`9ecc7cb`](https://github.com/UsefulSoftwareCo/executor/commit/9ecc7cb8b30375ffa960e3fefe4d211e0254e691)]:
29+ - @executor-js/sdk@1.5.42
30+ - @executor-js/cloudflare@0.0.41
31+ - @executor-js/plugin-openapi@1.5.42
32+ - @executor-js/plugin-mcp@1.5.42
33+ - @executor-js/api@1.4.62
34+ - @executor-js/execution@1.5.42
35+ - @executor-js/vite-plugin@0.0.59
36+ - @executor-js/host-mcp@1.4.4
37+ - @executor-js/mcp-apps-shell@1.4.10
38+ - @executor-js/runtime-dynamic-worker@1.4.4
39+ - @executor-js/plugin-graphql@1.5.42
40+ - @executor-js/plugin-toolkits@1.5.34
41+ - @executor-js/plugin-workos-vault@0.0.2
42+ - @executor-js/react@1.4.62
43+ - @executor-js/runtime-quickjs@1.5.42
44+
45+## 1.4.59
46+
47+### Patch Changes
48+
49+- Updated dependencies [[`d572658`](https://github.com/UsefulSoftwareCo/executor/commit/d572658d74097917412256f10a3ea2e3974f44dd), [`a9b33d2`](https://github.com/UsefulSoftwareCo/executor/commit/a9b33d25c32fbb4a292b7e8963e22392f862a16f)]:
50+ - @executor-js/sdk@1.5.41
51+ - @executor-js/plugin-mcp@1.5.41
52+ - @executor-js/api@1.4.61
53+ - @executor-js/execution@1.5.41
54+ - @executor-js/vite-plugin@0.0.58
55+ - @executor-js/cloudflare@0.0.40
56+ - @executor-js/host-mcp@1.4.4
57+ - @executor-js/mcp-apps-shell@1.4.9
58+ - @executor-js/runtime-dynamic-worker@1.4.4
59+ - @executor-js/plugin-graphql@1.5.41
60+ - @executor-js/plugin-openapi@1.5.41
61+ - @executor-js/plugin-toolkits@1.5.33
62+ - @executor-js/plugin-workos-vault@0.0.2
63+ - @executor-js/react@1.4.61
64+ - @executor-js/runtime-quickjs@1.5.41
65+
66+## 1.4.58
67+
68+### Patch Changes
69+
70+- Updated dependencies [[`8ba64f6`](https://github.com/UsefulSoftwareCo/executor/commit/8ba64f675f6d6ab5302d4f68390c0b055d006f4a)]:
71+ - @executor-js/sdk@1.5.40
72+ - @executor-js/api@1.4.60
73+ - @executor-js/execution@1.5.40
74+ - @executor-js/vite-plugin@0.0.57
75+ - @executor-js/cloudflare@0.0.39
76+ - @executor-js/host-mcp@1.4.4
77+ - @executor-js/mcp-apps-shell@1.4.8
78+ - @executor-js/runtime-dynamic-worker@1.4.4
79+ - @executor-js/plugin-graphql@1.5.40
80+ - @executor-js/plugin-mcp@1.5.40
81+ - @executor-js/plugin-openapi@1.5.40
82+ - @executor-js/plugin-toolkits@1.5.32
83+ - @executor-js/plugin-workos-vault@0.0.2
84+ - @executor-js/react@1.4.60
85+ - @executor-js/runtime-quickjs@1.5.40
86+
87+## 1.4.57
88+
89+### Patch Changes
90+
91+- Updated dependencies [[`6c316c7`](https://github.com/UsefulSoftwareCo/executor/commit/6c316c77a9efc98784976236852b58c6156e016e)]:
92+ - @executor-js/sdk@1.5.39
93+ - @executor-js/api@1.4.59
94+ - @executor-js/execution@1.5.39
95+ - @executor-js/vite-plugin@0.0.56
96+ - @executor-js/cloudflare@0.0.38
97+ - @executor-js/host-mcp@1.4.4
98+ - @executor-js/mcp-apps-shell@1.4.7
99+ - @executor-js/runtime-dynamic-worker@1.4.4
100+ - @executor-js/plugin-graphql@1.5.39
101+ - @executor-js/plugin-mcp@1.5.39
102+ - @executor-js/plugin-openapi@1.5.39
103+ - @executor-js/plugin-toolkits@1.5.31
104+ - @executor-js/plugin-workos-vault@0.0.2
105+ - @executor-js/react@1.4.59
106+ - @executor-js/runtime-quickjs@1.5.39
107+
108+## 1.4.56
109+
110+### Patch Changes
111+
112+- Updated dependencies [[`df01d91`](https://github.com/UsefulSoftwareCo/executor/commit/df01d9197e7b4fca9bd0adaca0705a80435e188c), [`6a924dd`](https://github.com/UsefulSoftwareCo/executor/commit/6a924dd98de916d6ff8cea2329bf672f149b64f4), [`1de85fc`](https://github.com/UsefulSoftwareCo/executor/commit/1de85fc0201c0c23c0e71e003c49228d406af6c8)]:
113+ - @executor-js/plugin-openapi@1.5.38
114+ - @executor-js/sdk@1.5.38
115+ - @executor-js/react@1.4.58
116+ - @executor-js/runtime-dynamic-worker@1.4.4
117+ - @executor-js/api@1.4.58
118+ - @executor-js/execution@1.5.38
119+ - @executor-js/vite-plugin@0.0.55
120+ - @executor-js/cloudflare@0.0.37
121+ - @executor-js/host-mcp@1.4.4
122+ - @executor-js/mcp-apps-shell@1.4.6
123+ - @executor-js/plugin-graphql@1.5.38
124+ - @executor-js/plugin-mcp@1.5.38
125+ - @executor-js/plugin-toolkits@1.5.30
126+ - @executor-js/plugin-workos-vault@0.0.2
127+ - @executor-js/runtime-quickjs@1.5.38
128+
129+## 1.4.55
130+
131+### Patch Changes
132+
133+- Updated dependencies [[`657b913`](https://github.com/UsefulSoftwareCo/executor/commit/657b9135b8b841495b362936bf60bdca998c16eb)]:
134+ - @executor-js/sdk@1.5.37
135+ - @executor-js/api@1.4.57
136+ - @executor-js/execution@1.5.37
137+ - @executor-js/vite-plugin@0.0.54
138+ - @executor-js/cloudflare@0.0.36
139+ - @executor-js/host-mcp@1.4.4
140+ - @executor-js/mcp-apps-shell@1.4.5
141+ - @executor-js/runtime-dynamic-worker@1.4.4
142+ - @executor-js/plugin-graphql@1.5.37
143+ - @executor-js/plugin-mcp@1.5.37
144+ - @executor-js/plugin-openapi@1.5.37
145+ - @executor-js/plugin-toolkits@1.5.29
146+ - @executor-js/plugin-workos-vault@0.0.2
147+ - @executor-js/react@1.4.57
148+ - @executor-js/runtime-quickjs@1.5.37
149+
150+## 1.4.54
151+
152+### Patch Changes
153+
154+- Updated dependencies []:
155+ - @executor-js/sdk@1.5.36
156+ - @executor-js/runtime-quickjs@1.5.36
157+ - @executor-js/execution@1.5.36
158+ - @executor-js/plugin-graphql@1.5.36
159+ - @executor-js/plugin-mcp@1.5.36
160+ - @executor-js/plugin-openapi@1.5.36
161+ - @executor-js/api@1.4.56
162+ - @executor-js/vite-plugin@0.0.53
163+ - @executor-js/cloudflare@0.0.35
164+ - @executor-js/host-mcp@1.4.4
165+ - @executor-js/runtime-dynamic-worker@1.4.4
166+ - @executor-js/plugin-toolkits@1.5.28
167+ - @executor-js/plugin-workos-vault@0.0.2
168+ - @executor-js/react@1.4.56
169+
170+## 1.4.53
171+
172+### Patch Changes
173+
174+- Updated dependencies [[`1b9b1f1`](https://github.com/UsefulSoftwareCo/executor/commit/1b9b1f10313834a625a411169ebf83f6181589df), [`af95edb`](https://github.com/UsefulSoftwareCo/executor/commit/af95edbb0bbde544bb1f4c6e18e9d64a2bcab0f8), [`99c808f`](https://github.com/UsefulSoftwareCo/executor/commit/99c808f09d3cf2263945efa4f6592cc4e78c9e08)]:
175+ - @executor-js/sdk@1.5.35
176+ - @executor-js/plugin-mcp@1.5.35
177+ - @executor-js/runtime-quickjs@1.5.35
178+ - @executor-js/api@1.4.55
179+ - @executor-js/execution@1.5.35
180+ - @executor-js/vite-plugin@0.0.52
181+ - @executor-js/cloudflare@0.0.34
182+ - @executor-js/host-mcp@1.4.4
183+ - @executor-js/runtime-dynamic-worker@1.4.4
184+ - @executor-js/plugin-apps@0.1.6
185+ - @executor-js/plugin-graphql@1.5.35
186+ - @executor-js/plugin-openapi@1.5.35
187+ - @executor-js/plugin-toolkits@1.5.27
188+ - @executor-js/plugin-workos-vault@0.0.2
189+ - @executor-js/react@1.4.55
190+
191+## 1.4.52
192+
193+### Patch Changes
194+
195+- Updated dependencies [[`e2712db`](https://github.com/UsefulSoftwareCo/executor/commit/e2712dbff98145c5c340832ffbdcb21113b9dd78), [`a86cc4e`](https://github.com/UsefulSoftwareCo/executor/commit/a86cc4e6d0252c90834f40ee09837d8a19cab7fe), [`7207347`](https://github.com/UsefulSoftwareCo/executor/commit/720734756a70b1b4f1564bdf82dc4118e5de2b76), [`0c4e9b4`](https://github.com/UsefulSoftwareCo/executor/commit/0c4e9b49fecb35ad71c92a464c3ea01131ff9d6f), [`171de20`](https://github.com/UsefulSoftwareCo/executor/commit/171de204725d10405c693549febc3a1cce2c24d8)]:
196+ - @executor-js/sdk@1.5.34
197+ - @executor-js/execution@1.5.34
198+ - @executor-js/plugin-openapi@1.5.34
199+ - @executor-js/api@1.4.54
200+ - @executor-js/vite-plugin@0.0.51
201+ - @executor-js/cloudflare@0.0.33
202+ - @executor-js/host-mcp@1.4.4
203+ - @executor-js/runtime-dynamic-worker@1.4.4
204+ - @executor-js/plugin-apps@0.1.5
205+ - @executor-js/plugin-graphql@1.5.34
206+ - @executor-js/plugin-mcp@1.5.34
207+ - @executor-js/plugin-toolkits@1.5.26
208+ - @executor-js/plugin-workos-vault@0.0.2
209+ - @executor-js/react@1.4.54
210+ - @executor-js/runtime-quickjs@1.5.34
211+
212+## 1.4.51
213+
214+### Patch Changes
215+
216+- Updated dependencies []:
217+ - @executor-js/sdk@1.5.33
218+ - @executor-js/runtime-quickjs@1.5.33
219+ - @executor-js/execution@1.5.33
220+ - @executor-js/plugin-graphql@1.5.33
221+ - @executor-js/plugin-mcp@1.5.33
222+ - @executor-js/plugin-openapi@1.5.33
223+ - @executor-js/api@1.4.53
224+ - @executor-js/vite-plugin@0.0.50
225+ - @executor-js/cloudflare@0.0.32
226+ - @executor-js/host-mcp@1.4.4
227+ - @executor-js/runtime-dynamic-worker@1.4.4
228+ - @executor-js/plugin-apps@0.1.4
229+ - @executor-js/plugin-toolkits@1.5.25
230+ - @executor-js/plugin-workos-vault@0.0.2
231+ - @executor-js/react@1.4.53
232+
233+## 1.4.50
234+
235+### Patch Changes
236+
237+- Updated dependencies []:
238+ - @executor-js/sdk@1.5.32
239+ - @executor-js/runtime-quickjs@1.5.32
240+ - @executor-js/execution@1.5.32
241+ - @executor-js/plugin-graphql@1.5.32
242+ - @executor-js/plugin-mcp@1.5.32
243+ - @executor-js/plugin-openapi@1.5.32
244+ - @executor-js/api@1.4.52
245+ - @executor-js/vite-plugin@0.0.49
246+ - @executor-js/cloudflare@0.0.31
247+ - @executor-js/host-mcp@1.4.4
248+ - @executor-js/runtime-dynamic-worker@1.4.4
249+ - @executor-js/plugin-apps@0.1.3
250+ - @executor-js/plugin-toolkits@1.5.24
251+ - @executor-js/plugin-workos-vault@0.0.2
252+ - @executor-js/react@1.4.52
253+
254+## 1.4.49
255+
256+### Patch Changes
257+
258+- Updated dependencies [[`9e38928`](https://github.com/UsefulSoftwareCo/executor/commit/9e38928f0fda9032b64b26990270c5d2b6690d13)]:
259+ - @executor-js/plugin-openapi@1.5.31
260+ - @executor-js/runtime-dynamic-worker@1.4.4
261+ - @executor-js/sdk@1.5.31
262+ - @executor-js/runtime-quickjs@1.5.31
263+ - @executor-js/execution@1.5.31
264+ - @executor-js/plugin-graphql@1.5.31
265+ - @executor-js/plugin-mcp@1.5.31
266+ - @executor-js/api@1.4.51
267+ - @executor-js/vite-plugin@0.0.48
268+ - @executor-js/cloudflare@0.0.30
269+ - @executor-js/host-mcp@1.4.4
270+ - @executor-js/plugin-apps@0.1.2
271+ - @executor-js/plugin-toolkits@1.5.23
272+ - @executor-js/plugin-workos-vault@0.0.2
273+ - @executor-js/react@1.4.51
274+
275+## 1.4.48
276+
277+### Patch Changes
278+
279+- Updated dependencies []:
280+ - @executor-js/sdk@1.5.30
281+ - @executor-js/runtime-quickjs@1.5.30
282+ - @executor-js/execution@1.5.30
283+ - @executor-js/plugin-graphql@1.5.30
284+ - @executor-js/plugin-mcp@1.5.30
285+ - @executor-js/plugin-openapi@1.5.30
286+ - @executor-js/api@1.4.50
287+ - @executor-js/vite-plugin@0.0.47
288+ - @executor-js/cloudflare@0.0.29
289+ - @executor-js/host-mcp@1.4.4
290+ - @executor-js/runtime-dynamic-worker@1.4.4
291+ - @executor-js/plugin-apps@0.1.1
292+ - @executor-js/plugin-toolkits@1.5.22
293+ - @executor-js/plugin-workos-vault@0.0.2
294+ - @executor-js/react@1.4.50
295+
3296 ## 1.4.47
4297
5298 ### Patch Changes
apps/cloud/drizzle/0010_ndjson_output_arrays.sql+23−0
@@ -0,0 +1,23 @@
1+-- Extraction used to persist an NDJSON operation's PER-LINE response schema
2+-- as the tool's output schema, while the invoke path returns an ARRAY of
3+-- parsed lines, so describe previews promised a single object invocations
4+-- never returned. The producer now wraps those schemas in an array. Persisted
5+-- `tool` rows can't be recognized as NDJSON-derived from their schema alone,
6+-- but the stored operation bindings kept the response content type, so the
7+-- affected connections are findable: stale-mark `tools_synced_at` and the
8+-- next read rebuilds their tool rows through the fixed producer.
9+--
10+-- Effectively idempotent: re-running re-marks the same connections, and a
11+-- rebuild from unchanged bindings writes identical rows.
12+UPDATE "connection" c
13+SET "tools_synced_at" = NULL
14+FROM (
15+ SELECT DISTINCT ps."tenant", ps."data"::jsonb ->> 'integration' AS integration
16+ FROM "plugin_storage" ps
17+ WHERE ps."collection" = 'operation'
18+ AND (ps."data"::text LIKE '%application/stream+json%'
19+ OR ps."data"::text LIKE '%application/x-ndjson%'
20+ OR ps."data"::text LIKE '%application/jsonl%')
21+) ndjson
22+WHERE c."tenant" = ndjson."tenant"
23+ AND c."integration" = ndjson."integration";
apps/cloud/drizzle/0011_tense_caretaker.sql+1−0
@@ -0,0 +1 @@
1+ALTER TABLE "oauth_client" ADD COLUMN "origin_redirect_uri" text;
apps/cloud/drizzle/0012_woozy_shiva.sql+10−0
@@ -0,0 +1,10 @@
1+CREATE TABLE "subject" (
2+ "external_id" varchar(255) NOT NULL,
3+ "created_at" timestamp NOT NULL,
4+ "last_seen_at" bigint,
5+ "status" text,
6+ "row_id" varchar(255) PRIMARY KEY NOT NULL,
7+ "tenant" varchar(255) NOT NULL
8+);
9+--> statement-breakpoint
10+CREATE UNIQUE INDEX "subject_uidx" ON "subject" USING btree ("tenant","external_id");
apps/cloud/drizzle/0013_curly_rocket_racer.sql+14−0
@@ -0,0 +1,14 @@
1+CREATE TABLE "artifact" (
2+ "id" varchar(255) NOT NULL,
3+ "title" text NOT NULL,
4+ "description" text,
5+ "code" text NOT NULL,
6+ "created_at" timestamp NOT NULL,
7+ "updated_at" timestamp NOT NULL,
8+ "row_id" varchar(255) PRIMARY KEY NOT NULL,
9+ "tenant" varchar(255) NOT NULL,
10+ "owner" varchar(255) NOT NULL,
11+ "subject" varchar(255) NOT NULL
12+);
13+--> statement-breakpoint
14+CREATE UNIQUE INDEX "artifact_uidx" ON "artifact" USING btree ("tenant","owner","subject","id");
apps/cloud/drizzle/0014_new_karma.sql+1−0
@@ -0,0 +1 @@
1+ALTER TABLE "artifact" ADD COLUMN "bindings" json;
apps/cloud/drizzle/0015_equal_the_leader.sql+1−0
@@ -0,0 +1 @@
1+ALTER TABLE "artifact" ADD COLUMN "preview" text;
apps/cloud/drizzle/meta/0010_snapshot.json+1290−0
@@ -0,0 +1,1290 @@
1+{
2+ "id": "c27a8958-ad73-4590-9b9b-d1b2584e0bc6",
3+ "prevId": "b07ddbda-be11-4679-9f00-16d1b3b5e879",
4+ "version": "7",
5+ "dialect": "postgresql",
6+ "tables": {
7+ "public.accounts": {
8+ "name": "accounts",
9+ "schema": "",
10+ "columns": {
11+ "id": {
12+ "name": "id",
13+ "type": "text",
14+ "primaryKey": true,
15+ "notNull": true
16+ },
17+ "created_at": {
18+ "name": "created_at",
19+ "type": "timestamp with time zone",
20+ "primaryKey": false,
21+ "notNull": true,
22+ "default": "now()"
23+ }
24+ },
25+ "indexes": {},
26+ "foreignKeys": {},
27+ "compositePrimaryKeys": {},
28+ "uniqueConstraints": {},
29+ "policies": {},
30+ "checkConstraints": {},
31+ "isRLSEnabled": false
32+ },
33+ "public.memberships": {
34+ "name": "memberships",
35+ "schema": "",
36+ "columns": {
37+ "account_id": {
38+ "name": "account_id",
39+ "type": "text",
40+ "primaryKey": false,
41+ "notNull": true
42+ },
43+ "organization_id": {
44+ "name": "organization_id",
45+ "type": "text",
46+ "primaryKey": false,
47+ "notNull": true
48+ },
49+ "created_at": {
50+ "name": "created_at",
51+ "type": "timestamp with time zone",
52+ "primaryKey": false,
53+ "notNull": true,
54+ "default": "now()"
55+ }
56+ },
57+ "indexes": {},
58+ "foreignKeys": {
59+ "memberships_account_id_accounts_id_fk": {
60+ "name": "memberships_account_id_accounts_id_fk",
61+ "tableFrom": "memberships",
62+ "tableTo": "accounts",
63+ "columnsFrom": ["account_id"],
64+ "columnsTo": ["id"],
65+ "onDelete": "cascade",
66+ "onUpdate": "no action"
67+ },
68+ "memberships_organization_id_organizations_id_fk": {
69+ "name": "memberships_organization_id_organizations_id_fk",
70+ "tableFrom": "memberships",
71+ "tableTo": "organizations",
72+ "columnsFrom": ["organization_id"],
73+ "columnsTo": ["id"],
74+ "onDelete": "cascade",
75+ "onUpdate": "no action"
76+ }
77+ },
78+ "compositePrimaryKeys": {
79+ "memberships_account_id_organization_id_pk": {
80+ "name": "memberships_account_id_organization_id_pk",
81+ "columns": ["account_id", "organization_id"]
82+ }
83+ },
84+ "uniqueConstraints": {},
85+ "policies": {},
86+ "checkConstraints": {},
87+ "isRLSEnabled": false
88+ },
89+ "public.organizations": {
90+ "name": "organizations",
91+ "schema": "",
92+ "columns": {
93+ "id": {
94+ "name": "id",
95+ "type": "text",
96+ "primaryKey": true,
97+ "notNull": true
98+ },
99+ "name": {
100+ "name": "name",
101+ "type": "text",
102+ "primaryKey": false,
103+ "notNull": true
104+ },
105+ "slug": {
106+ "name": "slug",
107+ "type": "text",
108+ "primaryKey": false,
109+ "notNull": true
110+ },
111+ "created_at": {
112+ "name": "created_at",
113+ "type": "timestamp with time zone",
114+ "primaryKey": false,
115+ "notNull": true,
116+ "default": "now()"
117+ }
118+ },
119+ "indexes": {
120+ "organizations_slug_unique": {
121+ "name": "organizations_slug_unique",
122+ "columns": [
123+ {
124+ "expression": "slug",
125+ "isExpression": false,
126+ "asc": true,
127+ "nulls": "last"
128+ }
129+ ],
130+ "isUnique": true,
131+ "concurrently": false,
132+ "method": "btree",
133+ "with": {}
134+ }
135+ },
136+ "foreignKeys": {},
137+ "compositePrimaryKeys": {},
138+ "uniqueConstraints": {},
139+ "policies": {},
140+ "checkConstraints": {},
141+ "isRLSEnabled": false
142+ },
143+ "public.blob": {
144+ "name": "blob",
145+ "schema": "",
146+ "columns": {
147+ "namespace": {
148+ "name": "namespace",
149+ "type": "varchar(255)",
150+ "primaryKey": false,
151+ "notNull": true
152+ },
153+ "key": {
154+ "name": "key",
155+ "type": "varchar(255)",
156+ "primaryKey": false,
157+ "notNull": true
158+ },
159+ "value": {
160+ "name": "value",
161+ "type": "text",
162+ "primaryKey": false,
163+ "notNull": true
164+ },
165+ "row_id": {
166+ "name": "row_id",
167+ "type": "varchar(255)",
168+ "primaryKey": true,
169+ "notNull": true
170+ },
171+ "id": {
172+ "name": "id",
173+ "type": "varchar(255)",
174+ "primaryKey": false,
175+ "notNull": true
176+ }
177+ },
178+ "indexes": {
179+ "blob_id_uidx": {
180+ "name": "blob_id_uidx",
181+ "columns": [
182+ {
183+ "expression": "id",
184+ "isExpression": false,
185+ "asc": true,
186+ "nulls": "last"
187+ }
188+ ],
189+ "isUnique": true,
190+ "concurrently": false,
191+ "method": "btree",
192+ "with": {}
193+ }
194+ },
195+ "foreignKeys": {},
196+ "compositePrimaryKeys": {},
197+ "uniqueConstraints": {},
198+ "policies": {},
199+ "checkConstraints": {},
200+ "isRLSEnabled": false
201+ },
202+ "public.connection": {
203+ "name": "connection",
204+ "schema": "",
205+ "columns": {
206+ "integration": {
207+ "name": "integration",
208+ "type": "varchar(255)",
209+ "primaryKey": false,
210+ "notNull": true
211+ },
212+ "name": {
213+ "name": "name",
214+ "type": "varchar(255)",
215+ "primaryKey": false,
216+ "notNull": true
217+ },
218+ "template": {
219+ "name": "template",
220+ "type": "text",
221+ "primaryKey": false,
222+ "notNull": true
223+ },
224+ "provider": {
225+ "name": "provider",
226+ "type": "text",
227+ "primaryKey": false,
228+ "notNull": true
229+ },
230+ "item_ids": {
231+ "name": "item_ids",
232+ "type": "json",
233+ "primaryKey": false,
234+ "notNull": true
235+ },
236+ "identity_label": {
237+ "name": "identity_label",
238+ "type": "text",
239+ "primaryKey": false,
240+ "notNull": false
241+ },
242+ "description": {
243+ "name": "description",
244+ "type": "text",
245+ "primaryKey": false,
246+ "notNull": false
247+ },
248+ "last_health": {
249+ "name": "last_health",
250+ "type": "json",
251+ "primaryKey": false,
252+ "notNull": false
253+ },
254+ "tools_synced_at": {
255+ "name": "tools_synced_at",
256+ "type": "bigint",
257+ "primaryKey": false,
258+ "notNull": false
259+ },
260+ "oauth_client": {
261+ "name": "oauth_client",
262+ "type": "text",
263+ "primaryKey": false,
264+ "notNull": false
265+ },
266+ "oauth_client_owner": {
267+ "name": "oauth_client_owner",
268+ "type": "text",
269+ "primaryKey": false,
270+ "notNull": false
271+ },
272+ "refresh_item_id": {
273+ "name": "refresh_item_id",
274+ "type": "text",
275+ "primaryKey": false,
276+ "notNull": false
277+ },
278+ "expires_at": {
279+ "name": "expires_at",
280+ "type": "bigint",
281+ "primaryKey": false,
282+ "notNull": false
283+ },
284+ "oauth_scope": {
285+ "name": "oauth_scope",
286+ "type": "text",
287+ "primaryKey": false,
288+ "notNull": false
289+ },
290+ "oauth_token_url": {
291+ "name": "oauth_token_url",
292+ "type": "text",
293+ "primaryKey": false,
294+ "notNull": false
295+ },
296+ "provider_state": {
297+ "name": "provider_state",
298+ "type": "json",
299+ "primaryKey": false,
300+ "notNull": false
301+ },
302+ "created_at": {
303+ "name": "created_at",
304+ "type": "timestamp",
305+ "primaryKey": false,
306+ "notNull": true
307+ },
308+ "updated_at": {
309+ "name": "updated_at",
310+ "type": "timestamp",
311+ "primaryKey": false,
312+ "notNull": true
313+ },
314+ "row_id": {
315+ "name": "row_id",
316+ "type": "varchar(255)",
317+ "primaryKey": true,
318+ "notNull": true
319+ },
320+ "tenant": {
321+ "name": "tenant",
322+ "type": "varchar(255)",
323+ "primaryKey": false,
324+ "notNull": true
325+ },
326+ "owner": {
327+ "name": "owner",
328+ "type": "varchar(255)",
329+ "primaryKey": false,
330+ "notNull": true
331+ },
332+ "subject": {
333+ "name": "subject",
334+ "type": "varchar(255)",
335+ "primaryKey": false,
336+ "notNull": true
337+ }
338+ },
339+ "indexes": {
340+ "connection_uidx": {
341+ "name": "connection_uidx",
342+ "columns": [
343+ {
344+ "expression": "tenant",
345+ "isExpression": false,
346+ "asc": true,
347+ "nulls": "last"
348+ },
349+ {
350+ "expression": "owner",
351+ "isExpression": false,
352+ "asc": true,
353+ "nulls": "last"
354+ },
355+ {
356+ "expression": "subject",
357+ "isExpression": false,
358+ "asc": true,
359+ "nulls": "last"
360+ },
361+ {
362+ "expression": "integration",
363+ "isExpression": false,
364+ "asc": true,
365+ "nulls": "last"
366+ },
367+ {
368+ "expression": "name",
369+ "isExpression": false,
370+ "asc": true,
371+ "nulls": "last"
372+ }
373+ ],
374+ "isUnique": true,
375+ "concurrently": false,
376+ "method": "btree",
377+ "with": {}
378+ }
379+ },
380+ "foreignKeys": {},
381+ "compositePrimaryKeys": {},
382+ "uniqueConstraints": {},
383+ "policies": {},
384+ "checkConstraints": {},
385+ "isRLSEnabled": false
386+ },
387+ "public.definition": {
388+ "name": "definition",
389+ "schema": "",
390+ "columns": {
391+ "integration": {
392+ "name": "integration",
393+ "type": "varchar(255)",
394+ "primaryKey": false,
395+ "notNull": true
396+ },
397+ "connection": {
398+ "name": "connection",
399+ "type": "varchar(255)",
400+ "primaryKey": false,
401+ "notNull": true
402+ },
403+ "plugin_id": {
404+ "name": "plugin_id",
405+ "type": "text",
406+ "primaryKey": false,
407+ "notNull": true
408+ },
409+ "name": {
410+ "name": "name",
411+ "type": "text",
412+ "primaryKey": false,
413+ "notNull": true
414+ },
415+ "schema": {
416+ "name": "schema",
417+ "type": "json",
418+ "primaryKey": false,
419+ "notNull": true
420+ },
421+ "created_at": {
422+ "name": "created_at",
423+ "type": "timestamp",
424+ "primaryKey": false,
425+ "notNull": true
426+ },
427+ "row_id": {
428+ "name": "row_id",
429+ "type": "varchar(255)",
430+ "primaryKey": true,
431+ "notNull": true
432+ },
433+ "tenant": {
434+ "name": "tenant",
435+ "type": "varchar(255)",
436+ "primaryKey": false,
437+ "notNull": true
438+ },
439+ "owner": {
440+ "name": "owner",
441+ "type": "varchar(255)",
442+ "primaryKey": false,
443+ "notNull": true
444+ },
445+ "subject": {
446+ "name": "subject",
447+ "type": "varchar(255)",
448+ "primaryKey": false,
449+ "notNull": true
450+ }
451+ },
452+ "indexes": {
453+ "definition_uidx": {
454+ "name": "definition_uidx",
455+ "columns": [
456+ {
457+ "expression": "tenant",
458+ "isExpression": false,
459+ "asc": true,
460+ "nulls": "last"
461+ },
462+ {
463+ "expression": "owner",
464+ "isExpression": false,
465+ "asc": true,
466+ "nulls": "last"
467+ },
468+ {
469+ "expression": "subject",
470+ "isExpression": false,
471+ "asc": true,
472+ "nulls": "last"
473+ },
474+ {
475+ "expression": "integration",
476+ "isExpression": false,
477+ "asc": true,
478+ "nulls": "last"
479+ },
480+ {
481+ "expression": "connection",
482+ "isExpression": false,
483+ "asc": true,
484+ "nulls": "last"
485+ },
486+ {
487+ "expression": "name",
488+ "isExpression": false,
489+ "asc": true,
490+ "nulls": "last"
491+ }
492+ ],
493+ "isUnique": true,
494+ "concurrently": false,
495+ "method": "btree",
496+ "with": {}
497+ }
498+ },
499+ "foreignKeys": {},
500+ "compositePrimaryKeys": {},
501+ "uniqueConstraints": {},
502+ "policies": {},
503+ "checkConstraints": {},
504+ "isRLSEnabled": false
505+ },
506+ "public.integration": {
507+ "name": "integration",
508+ "schema": "",
509+ "columns": {
510+ "slug": {
511+ "name": "slug",
512+ "type": "varchar(255)",
513+ "primaryKey": false,
514+ "notNull": true
515+ },
516+ "plugin_id": {
517+ "name": "plugin_id",
518+ "type": "text",
519+ "primaryKey": false,
520+ "notNull": true
521+ },
522+ "name": {
523+ "name": "name",
524+ "type": "text",
525+ "primaryKey": false,
526+ "notNull": false
527+ },
528+ "description": {
529+ "name": "description",
530+ "type": "text",
531+ "primaryKey": false,
532+ "notNull": false
533+ },
534+ "config_revised_at": {
535+ "name": "config_revised_at",
536+ "type": "bigint",
537+ "primaryKey": false,
538+ "notNull": false
539+ },
540+ "config": {
541+ "name": "config",
542+ "type": "json",
543+ "primaryKey": false,
544+ "notNull": false
545+ },
546+ "health_check": {
547+ "name": "health_check",
548+ "type": "json",
549+ "primaryKey": false,
550+ "notNull": false
551+ },
552+ "can_remove": {
553+ "name": "can_remove",
554+ "type": "boolean",
555+ "primaryKey": false,
556+ "notNull": true,
557+ "default": true
558+ },
559+ "can_refresh": {
560+ "name": "can_refresh",
561+ "type": "boolean",
562+ "primaryKey": false,
563+ "notNull": true,
564+ "default": false
565+ },
566+ "created_at": {
567+ "name": "created_at",
568+ "type": "timestamp",
569+ "primaryKey": false,
570+ "notNull": true
571+ },
572+ "updated_at": {
573+ "name": "updated_at",
574+ "type": "timestamp",
575+ "primaryKey": false,
576+ "notNull": true
577+ },
578+ "row_id": {
579+ "name": "row_id",
580+ "type": "varchar(255)",
581+ "primaryKey": true,
582+ "notNull": true
583+ },
584+ "tenant": {
585+ "name": "tenant",
586+ "type": "varchar(255)",
587+ "primaryKey": false,
588+ "notNull": true
589+ }
590+ },
591+ "indexes": {
592+ "integration_uidx": {
593+ "name": "integration_uidx",
594+ "columns": [
595+ {
596+ "expression": "tenant",
597+ "isExpression": false,
598+ "asc": true,
599+ "nulls": "last"
600+ },
601+ {
602+ "expression": "slug",
603+ "isExpression": false,
604+ "asc": true,
605+ "nulls": "last"
606+ }
607+ ],
608+ "isUnique": true,
609+ "concurrently": false,
610+ "method": "btree",
611+ "with": {}
612+ }
613+ },
614+ "foreignKeys": {},
615+ "compositePrimaryKeys": {},
616+ "uniqueConstraints": {},
617+ "policies": {},
618+ "checkConstraints": {},
619+ "isRLSEnabled": false
620+ },
621+ "public.oauth_client": {
622+ "name": "oauth_client",
623+ "schema": "",
624+ "columns": {
625+ "slug": {
626+ "name": "slug",
627+ "type": "varchar(255)",
628+ "primaryKey": false,
629+ "notNull": true
630+ },
631+ "authorization_url": {
632+ "name": "authorization_url",
633+ "type": "text",
634+ "primaryKey": false,
635+ "notNull": true
636+ },
637+ "token_url": {
638+ "name": "token_url",
639+ "type": "text",
640+ "primaryKey": false,
641+ "notNull": true
642+ },
643+ "grant": {
644+ "name": "grant",
645+ "type": "text",
646+ "primaryKey": false,
647+ "notNull": true
648+ },
649+ "client_id": {
650+ "name": "client_id",
651+ "type": "text",
652+ "primaryKey": false,
653+ "notNull": true
654+ },
655+ "client_secret_item_id": {
656+ "name": "client_secret_item_id",
657+ "type": "text",
658+ "primaryKey": false,
659+ "notNull": false
660+ },
661+ "resource": {
662+ "name": "resource",
663+ "type": "text",
664+ "primaryKey": false,
665+ "notNull": false
666+ },
667+ "origin_kind": {
668+ "name": "origin_kind",
669+ "type": "text",
670+ "primaryKey": false,
671+ "notNull": false
672+ },
673+ "origin_integration": {
674+ "name": "origin_integration",
675+ "type": "text",
676+ "primaryKey": false,
677+ "notNull": false
678+ },
679+ "origin_issuer": {
680+ "name": "origin_issuer",
681+ "type": "text",
682+ "primaryKey": false,
683+ "notNull": false
684+ },
685+ "created_at": {
686+ "name": "created_at",
687+ "type": "timestamp",
688+ "primaryKey": false,
689+ "notNull": true
690+ },
691+ "row_id": {
692+ "name": "row_id",
693+ "type": "varchar(255)",
694+ "primaryKey": true,
695+ "notNull": true
696+ },
697+ "tenant": {
698+ "name": "tenant",
699+ "type": "varchar(255)",
700+ "primaryKey": false,
701+ "notNull": true
702+ },
703+ "owner": {
704+ "name": "owner",
705+ "type": "varchar(255)",
706+ "primaryKey": false,
707+ "notNull": true
708+ },
709+ "subject": {
710+ "name": "subject",
711+ "type": "varchar(255)",
712+ "primaryKey": false,
713+ "notNull": true
714+ }
715+ },
716+ "indexes": {
717+ "oauth_client_uidx": {
718+ "name": "oauth_client_uidx",
719+ "columns": [
720+ {
721+ "expression": "tenant",
722+ "isExpression": false,
723+ "asc": true,
724+ "nulls": "last"
725+ },
726+ {
727+ "expression": "owner",
728+ "isExpression": false,
729+ "asc": true,
730+ "nulls": "last"
731+ },
732+ {
733+ "expression": "subject",
734+ "isExpression": false,
735+ "asc": true,
736+ "nulls": "last"
737+ },
738+ {
739+ "expression": "slug",
740+ "isExpression": false,
741+ "asc": true,
742+ "nulls": "last"
743+ }
744+ ],
745+ "isUnique": true,
746+ "concurrently": false,
747+ "method": "btree",
748+ "with": {}
749+ }
750+ },
751+ "foreignKeys": {},
752+ "compositePrimaryKeys": {},
753+ "uniqueConstraints": {},
754+ "policies": {},
755+ "checkConstraints": {},
756+ "isRLSEnabled": false
757+ },
758+ "public.oauth_session": {
759+ "name": "oauth_session",
760+ "schema": "",
761+ "columns": {
762+ "state": {
763+ "name": "state",
764+ "type": "varchar(255)",
765+ "primaryKey": false,
766+ "notNull": true
767+ },
768+ "client_slug": {
769+ "name": "client_slug",
770+ "type": "text",
771+ "primaryKey": false,
772+ "notNull": true
773+ },
774+ "integration": {
775+ "name": "integration",
776+ "type": "text",
777+ "primaryKey": false,
778+ "notNull": true
779+ },
780+ "name": {
781+ "name": "name",
782+ "type": "text",
783+ "primaryKey": false,
784+ "notNull": true
785+ },
786+ "template": {
787+ "name": "template",
788+ "type": "text",
789+ "primaryKey": false,
790+ "notNull": true
791+ },
792+ "redirect_url": {
793+ "name": "redirect_url",
794+ "type": "text",
795+ "primaryKey": false,
796+ "notNull": true
797+ },
798+ "pkce_verifier": {
799+ "name": "pkce_verifier",
800+ "type": "text",
801+ "primaryKey": false,
802+ "notNull": false
803+ },
804+ "identity_label": {
805+ "name": "identity_label",
806+ "type": "text",
807+ "primaryKey": false,
808+ "notNull": false
809+ },
810+ "payload": {
811+ "name": "payload",
812+ "type": "json",
813+ "primaryKey": false,
814+ "notNull": true
815+ },
816+ "expires_at": {
817+ "name": "expires_at",
818+ "type": "bigint",
819+ "primaryKey": false,
820+ "notNull": true
821+ },
822+ "created_at": {
823+ "name": "created_at",
824+ "type": "timestamp",
825+ "primaryKey": false,
826+ "notNull": true
827+ },
828+ "row_id": {
829+ "name": "row_id",
830+ "type": "varchar(255)",
831+ "primaryKey": true,
832+ "notNull": true
833+ },
834+ "tenant": {
835+ "name": "tenant",
836+ "type": "varchar(255)",
837+ "primaryKey": false,
838+ "notNull": true
839+ },
840+ "owner": {
841+ "name": "owner",
842+ "type": "varchar(255)",
843+ "primaryKey": false,
844+ "notNull": true
845+ },
846+ "subject": {
847+ "name": "subject",
848+ "type": "varchar(255)",
849+ "primaryKey": false,
850+ "notNull": true
851+ }
852+ },
853+ "indexes": {
854+ "oauth_session_uidx": {
855+ "name": "oauth_session_uidx",
856+ "columns": [
857+ {
858+ "expression": "tenant",
859+ "isExpression": false,
860+ "asc": true,
861+ "nulls": "last"
862+ },
863+ {
864+ "expression": "state",
865+ "isExpression": false,
866+ "asc": true,
867+ "nulls": "last"
868+ }
869+ ],
870+ "isUnique": true,
871+ "concurrently": false,
872+ "method": "btree",
873+ "with": {}
874+ }
875+ },
876+ "foreignKeys": {},
877+ "compositePrimaryKeys": {},
878+ "uniqueConstraints": {},
879+ "policies": {},
880+ "checkConstraints": {},
881+ "isRLSEnabled": false
882+ },
883+ "public.plugin_storage": {
884+ "name": "plugin_storage",
885+ "schema": "",
886+ "columns": {
887+ "plugin_id": {
888+ "name": "plugin_id",
889+ "type": "varchar(255)",
890+ "primaryKey": false,
891+ "notNull": true
892+ },
893+ "collection": {
894+ "name": "collection",
895+ "type": "varchar(255)",
896+ "primaryKey": false,
897+ "notNull": true
898+ },
899+ "key": {
900+ "name": "key",
901+ "type": "varchar(255)",
902+ "primaryKey": false,
903+ "notNull": true
904+ },
905+ "data": {
906+ "name": "data",
907+ "type": "json",
908+ "primaryKey": false,
909+ "notNull": true
910+ },
911+ "created_at": {
912+ "name": "created_at",
913+ "type": "timestamp",
914+ "primaryKey": false,
915+ "notNull": true
916+ },
917+ "updated_at": {
918+ "name": "updated_at",
919+ "type": "timestamp",
920+ "primaryKey": false,
921+ "notNull": true
922+ },
923+ "row_id": {
924+ "name": "row_id",
925+ "type": "varchar(255)",
926+ "primaryKey": true,
927+ "notNull": true
928+ },
929+ "tenant": {
930+ "name": "tenant",
931+ "type": "varchar(255)",
932+ "primaryKey": false,
933+ "notNull": true
934+ },
935+ "owner": {
936+ "name": "owner",
937+ "type": "varchar(255)",
938+ "primaryKey": false,
939+ "notNull": true
940+ },
941+ "subject": {
942+ "name": "subject",
943+ "type": "varchar(255)",
944+ "primaryKey": false,
945+ "notNull": true
946+ }
947+ },
948+ "indexes": {
949+ "plugin_storage_uidx": {
950+ "name": "plugin_storage_uidx",
951+ "columns": [
952+ {
953+ "expression": "tenant",
954+ "isExpression": false,
955+ "asc": true,
956+ "nulls": "last"
957+ },
958+ {
959+ "expression": "owner",
960+ "isExpression": false,
961+ "asc": true,
962+ "nulls": "last"
963+ },
964+ {
965+ "expression": "subject",
966+ "isExpression": false,
967+ "asc": true,
968+ "nulls": "last"
969+ },
970+ {
971+ "expression": "plugin_id",
972+ "isExpression": false,
973+ "asc": true,
974+ "nulls": "last"
975+ },
976+ {
977+ "expression": "collection",
978+ "isExpression": false,
979+ "asc": true,
980+ "nulls": "last"
981+ },
982+ {
983+ "expression": "key",
984+ "isExpression": false,
985+ "asc": true,
986+ "nulls": "last"
987+ }
988+ ],
989+ "isUnique": true,
990+ "concurrently": false,
991+ "method": "btree",
992+ "with": {}
993+ }
994+ },
995+ "foreignKeys": {},
996+ "compositePrimaryKeys": {},
997+ "uniqueConstraints": {},
998+ "policies": {},
999+ "checkConstraints": {},
1000+ "isRLSEnabled": false
1001+ },
1002+ "public.private_executor_cloud_settings": {
1003+ "name": "private_executor_cloud_settings",
1004+ "schema": "",
1005+ "columns": {
1006+ "id": {
1007+ "name": "id",
1008+ "type": "varchar(255)",
1009+ "primaryKey": true,
1010+ "notNull": true
1011+ },
1012+ "version": {
1013+ "name": "version",
1014+ "type": "varchar(255)",
1015+ "primaryKey": false,
1016+ "notNull": true,
1017+ "default": "'1.0.0'"
1018+ }
1019+ },
1020+ "indexes": {},
1021+ "foreignKeys": {},
1022+ "compositePrimaryKeys": {},
1023+ "uniqueConstraints": {},
1024+ "policies": {},
1025+ "checkConstraints": {},
1026+ "isRLSEnabled": false
1027+ },
1028+ "public.tool": {
1029+ "name": "tool",
1030+ "schema": "",
1031+ "columns": {
1032+ "integration": {
1033+ "name": "integration",
1034+ "type": "varchar(255)",
1035+ "primaryKey": false,
1036+ "notNull": true
1037+ },
1038+ "connection": {
1039+ "name": "connection",
1040+ "type": "varchar(255)",
1041+ "primaryKey": false,
1042+ "notNull": true
1043+ },
1044+ "plugin_id": {
1045+ "name": "plugin_id",
1046+ "type": "text",
1047+ "primaryKey": false,
1048+ "notNull": true
1049+ },
1050+ "name": {
1051+ "name": "name",
1052+ "type": "varchar(255)",
1053+ "primaryKey": false,
1054+ "notNull": true
1055+ },
1056+ "description": {
1057+ "name": "description",
1058+ "type": "text",
1059+ "primaryKey": false,
1060+ "notNull": true
1061+ },
1062+ "input_schema": {
1063+ "name": "input_schema",
1064+ "type": "json",
1065+ "primaryKey": false,
1066+ "notNull": false
1067+ },
1068+ "output_schema": {
1069+ "name": "output_schema",
1070+ "type": "json",
1071+ "primaryKey": false,
1072+ "notNull": false
1073+ },
1074+ "annotations": {
1075+ "name": "annotations",
1076+ "type": "json",
1077+ "primaryKey": false,
1078+ "notNull": false
1079+ },
1080+ "created_at": {
1081+ "name": "created_at",
1082+ "type": "timestamp",
1083+ "primaryKey": false,
1084+ "notNull": true
1085+ },
1086+ "updated_at": {
1087+ "name": "updated_at",
1088+ "type": "timestamp",
1089+ "primaryKey": false,
1090+ "notNull": true
1091+ },
1092+ "row_id": {
1093+ "name": "row_id",
1094+ "type": "varchar(255)",
1095+ "primaryKey": true,
1096+ "notNull": true
1097+ },
1098+ "tenant": {
1099+ "name": "tenant",
1100+ "type": "varchar(255)",
1101+ "primaryKey": false,
1102+ "notNull": true
1103+ },
1104+ "owner": {
1105+ "name": "owner",
1106+ "type": "varchar(255)",
1107+ "primaryKey": false,
1108+ "notNull": true
1109+ },
1110+ "subject": {
1111+ "name": "subject",
1112+ "type": "varchar(255)",
1113+ "primaryKey": false,
1114+ "notNull": true
1115+ }
1116+ },
1117+ "indexes": {
1118+ "tool_uidx": {
1119+ "name": "tool_uidx",
1120+ "columns": [
1121+ {
1122+ "expression": "tenant",
1123+ "isExpression": false,
1124+ "asc": true,
1125+ "nulls": "last"
1126+ },
1127+ {
1128+ "expression": "owner",
1129+ "isExpression": false,
1130+ "asc": true,
1131+ "nulls": "last"
1132+ },
1133+ {
1134+ "expression": "subject",
1135+ "isExpression": false,
1136+ "asc": true,
1137+ "nulls": "last"
1138+ },
1139+ {
1140+ "expression": "integration",
1141+ "isExpression": false,
1142+ "asc": true,
1143+ "nulls": "last"
1144+ },
1145+ {
1146+ "expression": "connection",
1147+ "isExpression": false,
1148+ "asc": true,
1149+ "nulls": "last"
1150+ },
1151+ {
1152+ "expression": "name",
1153+ "isExpression": false,
1154+ "asc": true,
1155+ "nulls": "last"
1156+ }
1157+ ],
1158+ "isUnique": true,
1159+ "concurrently": false,
1160+ "method": "btree",
1161+ "with": {}
1162+ }
1163+ },
1164+ "foreignKeys": {},
1165+ "compositePrimaryKeys": {},
1166+ "uniqueConstraints": {},
1167+ "policies": {},
1168+ "checkConstraints": {},
1169+ "isRLSEnabled": false
1170+ },
1171+ "public.tool_policy": {
1172+ "name": "tool_policy",
1173+ "schema": "",
1174+ "columns": {
1175+ "id": {
1176+ "name": "id",
1177+ "type": "varchar(255)",
1178+ "primaryKey": false,
1179+ "notNull": true
1180+ },
1181+ "pattern": {
1182+ "name": "pattern",
1183+ "type": "text",
1184+ "primaryKey": false,
1185+ "notNull": true
1186+ },
1187+ "action": {
1188+ "name": "action",
1189+ "type": "text",
1190+ "primaryKey": false,
1191+ "notNull": true
1192+ },
1193+ "position": {
1194+ "name": "position",
1195+ "type": "text",
1196+ "primaryKey": false,
1197+ "notNull": true
1198+ },
1199+ "created_at": {
1200+ "name": "created_at",
1201+ "type": "timestamp",
1202+ "primaryKey": false,
1203+ "notNull": true
1204+ },
1205+ "updated_at": {
1206+ "name": "updated_at",
1207+ "type": "timestamp",
1208+ "primaryKey": false,
1209+ "notNull": true
1210+ },
1211+ "row_id": {
1212+ "name": "row_id",
1213+ "type": "varchar(255)",
1214+ "primaryKey": true,
1215+ "notNull": true
1216+ },
1217+ "tenant": {
1218+ "name": "tenant",
1219+ "type": "varchar(255)",
1220+ "primaryKey": false,
1221+ "notNull": true
1222+ },
1223+ "owner": {
1224+ "name": "owner",
1225+ "type": "varchar(255)",
1226+ "primaryKey": false,
1227+ "notNull": true
1228+ },
1229+ "subject": {
1230+ "name": "subject",
1231+ "type": "varchar(255)",
1232+ "primaryKey": false,
1233+ "notNull": true
1234+ }
1235+ },
1236+ "indexes": {
1237+ "tool_policy_uidx": {
1238+ "name": "tool_policy_uidx",
1239+ "columns": [
1240+ {
1241+ "expression": "tenant",
1242+ "isExpression": false,
1243+ "asc": true,
1244+ "nulls": "last"
1245+ },
1246+ {
1247+ "expression": "owner",
1248+ "isExpression": false,
1249+ "asc": true,
1250+ "nulls": "last"
1251+ },
1252+ {
1253+ "expression": "subject",
1254+ "isExpression": false,
1255+ "asc": true,
1256+ "nulls": "last"
1257+ },
1258+ {
1259+ "expression": "id",
1260+ "isExpression": false,
1261+ "asc": true,
1262+ "nulls": "last"
1263+ }
1264+ ],
1265+ "isUnique": true,
1266+ "concurrently": false,
1267+ "method": "btree",
1268+ "with": {}
1269+ }
1270+ },
1271+ "foreignKeys": {},
1272+ "compositePrimaryKeys": {},
1273+ "uniqueConstraints": {},
1274+ "policies": {},
1275+ "checkConstraints": {},
1276+ "isRLSEnabled": false
1277+ }
1278+ },
1279+ "enums": {},
1280+ "schemas": {},
1281+ "sequences": {},
1282+ "roles": {},
1283+ "policies": {},
1284+ "views": {},
1285+ "_meta": {
1286+ "columns": {},
1287+ "schemas": {},
1288+ "tables": {}
1289+ }
1290+}
apps/cloud/drizzle/meta/0011_snapshot.json+1296−0
@@ -0,0 +1,1296 @@
1+{
2+ "id": "e285be7d-2427-4b1b-8b86-50a80ada25af",
3+ "prevId": "c27a8958-ad73-4590-9b9b-d1b2584e0bc6",
4+ "version": "7",
5+ "dialect": "postgresql",
6+ "tables": {
7+ "public.accounts": {
8+ "name": "accounts",
9+ "schema": "",
10+ "columns": {
11+ "id": {
12+ "name": "id",
13+ "type": "text",
14+ "primaryKey": true,
15+ "notNull": true
16+ },
17+ "created_at": {
18+ "name": "created_at",
19+ "type": "timestamp with time zone",
20+ "primaryKey": false,
21+ "notNull": true,
22+ "default": "now()"
23+ }
24+ },
25+ "indexes": {},
26+ "foreignKeys": {},
27+ "compositePrimaryKeys": {},
28+ "uniqueConstraints": {},
29+ "policies": {},
30+ "checkConstraints": {},
31+ "isRLSEnabled": false
32+ },
33+ "public.memberships": {
34+ "name": "memberships",
35+ "schema": "",
36+ "columns": {
37+ "account_id": {
38+ "name": "account_id",
39+ "type": "text",
40+ "primaryKey": false,
41+ "notNull": true
42+ },
43+ "organization_id": {
44+ "name": "organization_id",
45+ "type": "text",
46+ "primaryKey": false,
47+ "notNull": true
48+ },
49+ "created_at": {
50+ "name": "created_at",
51+ "type": "timestamp with time zone",
52+ "primaryKey": false,
53+ "notNull": true,
54+ "default": "now()"
55+ }
56+ },
57+ "indexes": {},
58+ "foreignKeys": {
59+ "memberships_account_id_accounts_id_fk": {
60+ "name": "memberships_account_id_accounts_id_fk",
61+ "tableFrom": "memberships",
62+ "tableTo": "accounts",
63+ "columnsFrom": ["account_id"],
64+ "columnsTo": ["id"],
65+ "onDelete": "cascade",
66+ "onUpdate": "no action"
67+ },
68+ "memberships_organization_id_organizations_id_fk": {
69+ "name": "memberships_organization_id_organizations_id_fk",
70+ "tableFrom": "memberships",
71+ "tableTo": "organizations",
72+ "columnsFrom": ["organization_id"],
73+ "columnsTo": ["id"],
74+ "onDelete": "cascade",
75+ "onUpdate": "no action"
76+ }
77+ },
78+ "compositePrimaryKeys": {
79+ "memberships_account_id_organization_id_pk": {
80+ "name": "memberships_account_id_organization_id_pk",
81+ "columns": ["account_id", "organization_id"]
82+ }
83+ },
84+ "uniqueConstraints": {},
85+ "policies": {},
86+ "checkConstraints": {},
87+ "isRLSEnabled": false
88+ },
89+ "public.organizations": {
90+ "name": "organizations",
91+ "schema": "",
92+ "columns": {
93+ "id": {
94+ "name": "id",
95+ "type": "text",
96+ "primaryKey": true,
97+ "notNull": true
98+ },
99+ "name": {
100+ "name": "name",
101+ "type": "text",
102+ "primaryKey": false,
103+ "notNull": true
104+ },
105+ "slug": {
106+ "name": "slug",
107+ "type": "text",
108+ "primaryKey": false,
109+ "notNull": true
110+ },
111+ "created_at": {
112+ "name": "created_at",
113+ "type": "timestamp with time zone",
114+ "primaryKey": false,
115+ "notNull": true,
116+ "default": "now()"
117+ }
118+ },
119+ "indexes": {
120+ "organizations_slug_unique": {
121+ "name": "organizations_slug_unique",
122+ "columns": [
123+ {
124+ "expression": "slug",
125+ "isExpression": false,
126+ "asc": true,
127+ "nulls": "last"
128+ }
129+ ],
130+ "isUnique": true,
131+ "concurrently": false,
132+ "method": "btree",
133+ "with": {}
134+ }
135+ },
136+ "foreignKeys": {},
137+ "compositePrimaryKeys": {},
138+ "uniqueConstraints": {},
139+ "policies": {},
140+ "checkConstraints": {},
141+ "isRLSEnabled": false
142+ },
143+ "public.blob": {
144+ "name": "blob",
145+ "schema": "",
146+ "columns": {
147+ "namespace": {
148+ "name": "namespace",
149+ "type": "varchar(255)",
150+ "primaryKey": false,
151+ "notNull": true
152+ },
153+ "key": {
154+ "name": "key",
155+ "type": "varchar(255)",
156+ "primaryKey": false,
157+ "notNull": true
158+ },
159+ "value": {
160+ "name": "value",
161+ "type": "text",
162+ "primaryKey": false,
163+ "notNull": true
164+ },
165+ "row_id": {
166+ "name": "row_id",
167+ "type": "varchar(255)",
168+ "primaryKey": true,
169+ "notNull": true
170+ },
171+ "id": {
172+ "name": "id",
173+ "type": "varchar(255)",
174+ "primaryKey": false,
175+ "notNull": true
176+ }
177+ },
178+ "indexes": {
179+ "blob_id_uidx": {
180+ "name": "blob_id_uidx",
181+ "columns": [
182+ {
183+ "expression": "id",
184+ "isExpression": false,
185+ "asc": true,
186+ "nulls": "last"
187+ }
188+ ],
189+ "isUnique": true,
190+ "concurrently": false,
191+ "method": "btree",
192+ "with": {}
193+ }
194+ },
195+ "foreignKeys": {},
196+ "compositePrimaryKeys": {},
197+ "uniqueConstraints": {},
198+ "policies": {},
199+ "checkConstraints": {},
200+ "isRLSEnabled": false
201+ },
202+ "public.connection": {
203+ "name": "connection",
204+ "schema": "",
205+ "columns": {
206+ "integration": {
207+ "name": "integration",
208+ "type": "varchar(255)",
209+ "primaryKey": false,
210+ "notNull": true
211+ },
212+ "name": {
213+ "name": "name",
214+ "type": "varchar(255)",
215+ "primaryKey": false,
216+ "notNull": true
217+ },
218+ "template": {
219+ "name": "template",
220+ "type": "text",
221+ "primaryKey": false,
222+ "notNull": true
223+ },
224+ "provider": {
225+ "name": "provider",
226+ "type": "text",
227+ "primaryKey": false,
228+ "notNull": true
229+ },
230+ "item_ids": {
231+ "name": "item_ids",
232+ "type": "json",
233+ "primaryKey": false,
234+ "notNull": true
235+ },
236+ "identity_label": {
237+ "name": "identity_label",
238+ "type": "text",
239+ "primaryKey": false,
240+ "notNull": false
241+ },
242+ "description": {
243+ "name": "description",
244+ "type": "text",
245+ "primaryKey": false,
246+ "notNull": false
247+ },
248+ "last_health": {
249+ "name": "last_health",
250+ "type": "json",
251+ "primaryKey": false,
252+ "notNull": false
253+ },
254+ "tools_synced_at": {
255+ "name": "tools_synced_at",
256+ "type": "bigint",
257+ "primaryKey": false,
258+ "notNull": false
259+ },
260+ "oauth_client": {
261+ "name": "oauth_client",
262+ "type": "text",
263+ "primaryKey": false,
264+ "notNull": false
265+ },
266+ "oauth_client_owner": {
267+ "name": "oauth_client_owner",
268+ "type": "text",
269+ "primaryKey": false,
270+ "notNull": false
271+ },
272+ "refresh_item_id": {
273+ "name": "refresh_item_id",
274+ "type": "text",
275+ "primaryKey": false,
276+ "notNull": false
277+ },
278+ "expires_at": {
279+ "name": "expires_at",
280+ "type": "bigint",
281+ "primaryKey": false,
282+ "notNull": false
283+ },
284+ "oauth_scope": {
285+ "name": "oauth_scope",
286+ "type": "text",
287+ "primaryKey": false,
288+ "notNull": false
289+ },
290+ "oauth_token_url": {
291+ "name": "oauth_token_url",
292+ "type": "text",
293+ "primaryKey": false,
294+ "notNull": false
295+ },
296+ "provider_state": {
297+ "name": "provider_state",
298+ "type": "json",
299+ "primaryKey": false,
300+ "notNull": false
301+ },
302+ "created_at": {
303+ "name": "created_at",
304+ "type": "timestamp",
305+ "primaryKey": false,
306+ "notNull": true
307+ },
308+ "updated_at": {
309+ "name": "updated_at",
310+ "type": "timestamp",
311+ "primaryKey": false,
312+ "notNull": true
313+ },
314+ "row_id": {
315+ "name": "row_id",
316+ "type": "varchar(255)",
317+ "primaryKey": true,
318+ "notNull": true
319+ },
320+ "tenant": {
321+ "name": "tenant",
322+ "type": "varchar(255)",
323+ "primaryKey": false,
324+ "notNull": true
325+ },
326+ "owner": {
327+ "name": "owner",
328+ "type": "varchar(255)",
329+ "primaryKey": false,
330+ "notNull": true
331+ },
332+ "subject": {
333+ "name": "subject",
334+ "type": "varchar(255)",
335+ "primaryKey": false,
336+ "notNull": true
337+ }
338+ },
339+ "indexes": {
340+ "connection_uidx": {
341+ "name": "connection_uidx",
342+ "columns": [
343+ {
344+ "expression": "tenant",
345+ "isExpression": false,
346+ "asc": true,
347+ "nulls": "last"
348+ },
349+ {
350+ "expression": "owner",
351+ "isExpression": false,
352+ "asc": true,
353+ "nulls": "last"
354+ },
355+ {
356+ "expression": "subject",
357+ "isExpression": false,
358+ "asc": true,
359+ "nulls": "last"
360+ },
361+ {
362+ "expression": "integration",
363+ "isExpression": false,
364+ "asc": true,
365+ "nulls": "last"
366+ },
367+ {
368+ "expression": "name",
369+ "isExpression": false,
370+ "asc": true,
371+ "nulls": "last"
372+ }
373+ ],
374+ "isUnique": true,
375+ "concurrently": false,
376+ "method": "btree",
377+ "with": {}
378+ }
379+ },
380+ "foreignKeys": {},
381+ "compositePrimaryKeys": {},
382+ "uniqueConstraints": {},
383+ "policies": {},
384+ "checkConstraints": {},
385+ "isRLSEnabled": false
386+ },
387+ "public.definition": {
388+ "name": "definition",
389+ "schema": "",
390+ "columns": {
391+ "integration": {
392+ "name": "integration",
393+ "type": "varchar(255)",
394+ "primaryKey": false,
395+ "notNull": true
396+ },
397+ "connection": {
398+ "name": "connection",
399+ "type": "varchar(255)",
400+ "primaryKey": false,
401+ "notNull": true
402+ },
403+ "plugin_id": {
404+ "name": "plugin_id",
405+ "type": "text",
406+ "primaryKey": false,
407+ "notNull": true
408+ },
409+ "name": {
410+ "name": "name",
411+ "type": "text",
412+ "primaryKey": false,
413+ "notNull": true
414+ },
415+ "schema": {
416+ "name": "schema",
417+ "type": "json",
418+ "primaryKey": false,
419+ "notNull": true
420+ },
421+ "created_at": {
422+ "name": "created_at",
423+ "type": "timestamp",
424+ "primaryKey": false,
425+ "notNull": true
426+ },
427+ "row_id": {
428+ "name": "row_id",
429+ "type": "varchar(255)",
430+ "primaryKey": true,
431+ "notNull": true
432+ },
433+ "tenant": {
434+ "name": "tenant",
435+ "type": "varchar(255)",
436+ "primaryKey": false,
437+ "notNull": true
438+ },
439+ "owner": {
440+ "name": "owner",
441+ "type": "varchar(255)",
442+ "primaryKey": false,
443+ "notNull": true
444+ },
445+ "subject": {
446+ "name": "subject",
447+ "type": "varchar(255)",
448+ "primaryKey": false,
449+ "notNull": true
450+ }
451+ },
452+ "indexes": {
453+ "definition_uidx": {
454+ "name": "definition_uidx",
455+ "columns": [
456+ {
457+ "expression": "tenant",
458+ "isExpression": false,
459+ "asc": true,
460+ "nulls": "last"
461+ },
462+ {
463+ "expression": "owner",
464+ "isExpression": false,
465+ "asc": true,
466+ "nulls": "last"
467+ },
468+ {
469+ "expression": "subject",
470+ "isExpression": false,
471+ "asc": true,
472+ "nulls": "last"
473+ },
474+ {
475+ "expression": "integration",
476+ "isExpression": false,
477+ "asc": true,
478+ "nulls": "last"
479+ },
480+ {
481+ "expression": "connection",
482+ "isExpression": false,
483+ "asc": true,
484+ "nulls": "last"
485+ },
486+ {
487+ "expression": "name",
488+ "isExpression": false,
489+ "asc": true,
490+ "nulls": "last"
491+ }
492+ ],
493+ "isUnique": true,
494+ "concurrently": false,
495+ "method": "btree",
496+ "with": {}
497+ }
498+ },
499+ "foreignKeys": {},
500+ "compositePrimaryKeys": {},
501+ "uniqueConstraints": {},
502+ "policies": {},
503+ "checkConstraints": {},
504+ "isRLSEnabled": false
505+ },
506+ "public.integration": {
507+ "name": "integration",
508+ "schema": "",
509+ "columns": {
510+ "slug": {
511+ "name": "slug",
512+ "type": "varchar(255)",
513+ "primaryKey": false,
514+ "notNull": true
515+ },
516+ "plugin_id": {
517+ "name": "plugin_id",
518+ "type": "text",
519+ "primaryKey": false,
520+ "notNull": true
521+ },
522+ "name": {
523+ "name": "name",
524+ "type": "text",
525+ "primaryKey": false,
526+ "notNull": false
527+ },
528+ "description": {
529+ "name": "description",
530+ "type": "text",
531+ "primaryKey": false,
532+ "notNull": false
533+ },
534+ "config": {
535+ "name": "config",
536+ "type": "json",
537+ "primaryKey": false,
538+ "notNull": false
539+ },
540+ "health_check": {
541+ "name": "health_check",
542+ "type": "json",
543+ "primaryKey": false,
544+ "notNull": false
545+ },
546+ "config_revised_at": {
547+ "name": "config_revised_at",
548+ "type": "bigint",
549+ "primaryKey": false,
550+ "notNull": false
551+ },
552+ "can_remove": {
553+ "name": "can_remove",
554+ "type": "boolean",
555+ "primaryKey": false,
556+ "notNull": true,
557+ "default": true
558+ },
559+ "can_refresh": {
560+ "name": "can_refresh",
561+ "type": "boolean",
562+ "primaryKey": false,
563+ "notNull": true,
564+ "default": false
565+ },
566+ "created_at": {
567+ "name": "created_at",
568+ "type": "timestamp",
569+ "primaryKey": false,
570+ "notNull": true
571+ },
572+ "updated_at": {
573+ "name": "updated_at",
574+ "type": "timestamp",
575+ "primaryKey": false,
576+ "notNull": true
577+ },
578+ "row_id": {
579+ "name": "row_id",
580+ "type": "varchar(255)",
581+ "primaryKey": true,
582+ "notNull": true
583+ },
584+ "tenant": {
585+ "name": "tenant",
586+ "type": "varchar(255)",
587+ "primaryKey": false,
588+ "notNull": true
589+ }
590+ },
591+ "indexes": {
592+ "integration_uidx": {
593+ "name": "integration_uidx",
594+ "columns": [
595+ {
596+ "expression": "tenant",
597+ "isExpression": false,
598+ "asc": true,
599+ "nulls": "last"
600+ },
601+ {
602+ "expression": "slug",
603+ "isExpression": false,
604+ "asc": true,
605+ "nulls": "last"
606+ }
607+ ],
608+ "isUnique": true,
609+ "concurrently": false,
610+ "method": "btree",
611+ "with": {}
612+ }
613+ },
614+ "foreignKeys": {},
615+ "compositePrimaryKeys": {},
616+ "uniqueConstraints": {},
617+ "policies": {},
618+ "checkConstraints": {},
619+ "isRLSEnabled": false
620+ },
621+ "public.oauth_client": {
622+ "name": "oauth_client",
623+ "schema": "",
624+ "columns": {
625+ "slug": {
626+ "name": "slug",
627+ "type": "varchar(255)",
628+ "primaryKey": false,
629+ "notNull": true
630+ },
631+ "authorization_url": {
632+ "name": "authorization_url",
633+ "type": "text",
634+ "primaryKey": false,
635+ "notNull": true
636+ },
637+ "token_url": {
638+ "name": "token_url",
639+ "type": "text",
640+ "primaryKey": false,
641+ "notNull": true
642+ },
643+ "grant": {
644+ "name": "grant",
645+ "type": "text",
646+ "primaryKey": false,
647+ "notNull": true
648+ },
649+ "client_id": {
650+ "name": "client_id",
651+ "type": "text",
652+ "primaryKey": false,
653+ "notNull": true
654+ },
655+ "client_secret_item_id": {
656+ "name": "client_secret_item_id",
657+ "type": "text",
658+ "primaryKey": false,
659+ "notNull": false
660+ },
661+ "resource": {
662+ "name": "resource",
663+ "type": "text",
664+ "primaryKey": false,
665+ "notNull": false
666+ },
667+ "origin_kind": {
668+ "name": "origin_kind",
669+ "type": "text",
670+ "primaryKey": false,
671+ "notNull": false
672+ },
673+ "origin_integration": {
674+ "name": "origin_integration",
675+ "type": "text",
676+ "primaryKey": false,
677+ "notNull": false
678+ },
679+ "origin_issuer": {
680+ "name": "origin_issuer",
681+ "type": "text",
682+ "primaryKey": false,
683+ "notNull": false
684+ },
685+ "origin_redirect_uri": {
686+ "name": "origin_redirect_uri",
687+ "type": "text",
688+ "primaryKey": false,
689+ "notNull": false
690+ },
691+ "created_at": {
692+ "name": "created_at",
693+ "type": "timestamp",
694+ "primaryKey": false,
695+ "notNull": true
696+ },
697+ "row_id": {
698+ "name": "row_id",
699+ "type": "varchar(255)",
700+ "primaryKey": true,
701+ "notNull": true
702+ },
703+ "tenant": {
704+ "name": "tenant",
705+ "type": "varchar(255)",
706+ "primaryKey": false,
707+ "notNull": true
708+ },
709+ "owner": {
710+ "name": "owner",
711+ "type": "varchar(255)",
712+ "primaryKey": false,
713+ "notNull": true
714+ },
715+ "subject": {
716+ "name": "subject",
717+ "type": "varchar(255)",
718+ "primaryKey": false,
719+ "notNull": true
720+ }
721+ },
722+ "indexes": {
723+ "oauth_client_uidx": {
724+ "name": "oauth_client_uidx",
725+ "columns": [
726+ {
727+ "expression": "tenant",
728+ "isExpression": false,
729+ "asc": true,
730+ "nulls": "last"
731+ },
732+ {
733+ "expression": "owner",
734+ "isExpression": false,
735+ "asc": true,
736+ "nulls": "last"
737+ },
738+ {
739+ "expression": "subject",
740+ "isExpression": false,
741+ "asc": true,
742+ "nulls": "last"
743+ },
744+ {
745+ "expression": "slug",
746+ "isExpression": false,
747+ "asc": true,
748+ "nulls": "last"
749+ }
750+ ],
751+ "isUnique": true,
752+ "concurrently": false,
753+ "method": "btree",
754+ "with": {}
755+ }
756+ },
757+ "foreignKeys": {},
758+ "compositePrimaryKeys": {},
759+ "uniqueConstraints": {},
760+ "policies": {},
761+ "checkConstraints": {},
762+ "isRLSEnabled": false
763+ },
764+ "public.oauth_session": {
765+ "name": "oauth_session",
766+ "schema": "",
767+ "columns": {
768+ "state": {
769+ "name": "state",
770+ "type": "varchar(255)",
771+ "primaryKey": false,
772+ "notNull": true
773+ },
774+ "client_slug": {
775+ "name": "client_slug",
776+ "type": "text",
777+ "primaryKey": false,
778+ "notNull": true
779+ },
780+ "integration": {
781+ "name": "integration",
782+ "type": "text",
783+ "primaryKey": false,
784+ "notNull": true
785+ },
786+ "name": {
787+ "name": "name",
788+ "type": "text",
789+ "primaryKey": false,
790+ "notNull": true
791+ },
792+ "template": {
793+ "name": "template",
794+ "type": "text",
795+ "primaryKey": false,
796+ "notNull": true
797+ },
798+ "redirect_url": {
799+ "name": "redirect_url",
800+ "type": "text",
801+ "primaryKey": false,
802+ "notNull": true
803+ },
804+ "pkce_verifier": {
805+ "name": "pkce_verifier",
806+ "type": "text",
807+ "primaryKey": false,
808+ "notNull": false
809+ },
810+ "identity_label": {
811+ "name": "identity_label",
812+ "type": "text",
813+ "primaryKey": false,
814+ "notNull": false
815+ },
816+ "payload": {
817+ "name": "payload",
818+ "type": "json",
819+ "primaryKey": false,
820+ "notNull": true
821+ },
822+ "expires_at": {
823+ "name": "expires_at",
824+ "type": "bigint",
825+ "primaryKey": false,
826+ "notNull": true
827+ },
828+ "created_at": {
829+ "name": "created_at",
830+ "type": "timestamp",
831+ "primaryKey": false,
832+ "notNull": true
833+ },
834+ "row_id": {
835+ "name": "row_id",
836+ "type": "varchar(255)",
837+ "primaryKey": true,
838+ "notNull": true
839+ },
840+ "tenant": {
841+ "name": "tenant",
842+ "type": "varchar(255)",
843+ "primaryKey": false,
844+ "notNull": true
845+ },
846+ "owner": {
847+ "name": "owner",
848+ "type": "varchar(255)",
849+ "primaryKey": false,
850+ "notNull": true
851+ },
852+ "subject": {
853+ "name": "subject",
854+ "type": "varchar(255)",
855+ "primaryKey": false,
856+ "notNull": true
857+ }
858+ },
859+ "indexes": {
860+ "oauth_session_uidx": {
861+ "name": "oauth_session_uidx",
862+ "columns": [
863+ {
864+ "expression": "tenant",
865+ "isExpression": false,
866+ "asc": true,
867+ "nulls": "last"
868+ },
869+ {
870+ "expression": "state",
871+ "isExpression": false,
872+ "asc": true,
873+ "nulls": "last"
874+ }
875+ ],
876+ "isUnique": true,
877+ "concurrently": false,
878+ "method": "btree",
879+ "with": {}
880+ }
881+ },
882+ "foreignKeys": {},
883+ "compositePrimaryKeys": {},
884+ "uniqueConstraints": {},
885+ "policies": {},
886+ "checkConstraints": {},
887+ "isRLSEnabled": false
888+ },
889+ "public.plugin_storage": {
890+ "name": "plugin_storage",
891+ "schema": "",
892+ "columns": {
893+ "plugin_id": {
894+ "name": "plugin_id",
895+ "type": "varchar(255)",
896+ "primaryKey": false,
897+ "notNull": true
898+ },
899+ "collection": {
900+ "name": "collection",
901+ "type": "varchar(255)",
902+ "primaryKey": false,
903+ "notNull": true
904+ },
905+ "key": {
906+ "name": "key",
907+ "type": "varchar(255)",
908+ "primaryKey": false,
909+ "notNull": true
910+ },
911+ "data": {
912+ "name": "data",
913+ "type": "json",
914+ "primaryKey": false,
915+ "notNull": true
916+ },
917+ "created_at": {
918+ "name": "created_at",
919+ "type": "timestamp",
920+ "primaryKey": false,
921+ "notNull": true
922+ },
923+ "updated_at": {
924+ "name": "updated_at",
925+ "type": "timestamp",
926+ "primaryKey": false,
927+ "notNull": true
928+ },
929+ "row_id": {
930+ "name": "row_id",
931+ "type": "varchar(255)",
932+ "primaryKey": true,
933+ "notNull": true
934+ },
935+ "tenant": {
936+ "name": "tenant",
937+ "type": "varchar(255)",
938+ "primaryKey": false,
939+ "notNull": true
940+ },
941+ "owner": {
942+ "name": "owner",
943+ "type": "varchar(255)",
944+ "primaryKey": false,
945+ "notNull": true
946+ },
947+ "subject": {
948+ "name": "subject",
949+ "type": "varchar(255)",
950+ "primaryKey": false,
951+ "notNull": true
952+ }
953+ },
954+ "indexes": {
955+ "plugin_storage_uidx": {
956+ "name": "plugin_storage_uidx",
957+ "columns": [
958+ {
959+ "expression": "tenant",
960+ "isExpression": false,
961+ "asc": true,
962+ "nulls": "last"
963+ },
964+ {
965+ "expression": "owner",
966+ "isExpression": false,
967+ "asc": true,
968+ "nulls": "last"
969+ },
970+ {
971+ "expression": "subject",
972+ "isExpression": false,
973+ "asc": true,
974+ "nulls": "last"
975+ },
976+ {
977+ "expression": "plugin_id",
978+ "isExpression": false,
979+ "asc": true,
980+ "nulls": "last"
981+ },
982+ {
983+ "expression": "collection",
984+ "isExpression": false,
985+ "asc": true,
986+ "nulls": "last"
987+ },
988+ {
989+ "expression": "key",
990+ "isExpression": false,
991+ "asc": true,
992+ "nulls": "last"
993+ }
994+ ],
995+ "isUnique": true,
996+ "concurrently": false,
997+ "method": "btree",
998+ "with": {}
999+ }
1000+ },
1001+ "foreignKeys": {},
1002+ "compositePrimaryKeys": {},
1003+ "uniqueConstraints": {},
1004+ "policies": {},
1005+ "checkConstraints": {},
1006+ "isRLSEnabled": false
1007+ },
1008+ "public.private_executor_cloud_settings": {
1009+ "name": "private_executor_cloud_settings",
1010+ "schema": "",
1011+ "columns": {
1012+ "id": {
1013+ "name": "id",
1014+ "type": "varchar(255)",
1015+ "primaryKey": true,
1016+ "notNull": true
1017+ },
1018+ "version": {
1019+ "name": "version",
1020+ "type": "varchar(255)",
1021+ "primaryKey": false,
1022+ "notNull": true,
1023+ "default": "'1.0.0'"
1024+ }
1025+ },
1026+ "indexes": {},
1027+ "foreignKeys": {},
1028+ "compositePrimaryKeys": {},
1029+ "uniqueConstraints": {},
1030+ "policies": {},
1031+ "checkConstraints": {},
1032+ "isRLSEnabled": false
1033+ },
1034+ "public.tool": {
1035+ "name": "tool",
1036+ "schema": "",
1037+ "columns": {
1038+ "integration": {
1039+ "name": "integration",
1040+ "type": "varchar(255)",
1041+ "primaryKey": false,
1042+ "notNull": true
1043+ },
1044+ "connection": {
1045+ "name": "connection",
1046+ "type": "varchar(255)",
1047+ "primaryKey": false,
1048+ "notNull": true
1049+ },
1050+ "plugin_id": {
1051+ "name": "plugin_id",
1052+ "type": "text",
1053+ "primaryKey": false,
1054+ "notNull": true
1055+ },
1056+ "name": {
1057+ "name": "name",
1058+ "type": "varchar(255)",
1059+ "primaryKey": false,
1060+ "notNull": true
1061+ },
1062+ "description": {
1063+ "name": "description",
1064+ "type": "text",
1065+ "primaryKey": false,
1066+ "notNull": true
1067+ },
1068+ "input_schema": {
1069+ "name": "input_schema",
1070+ "type": "json",
1071+ "primaryKey": false,
1072+ "notNull": false
1073+ },
1074+ "output_schema": {
1075+ "name": "output_schema",
1076+ "type": "json",
1077+ "primaryKey": false,
1078+ "notNull": false
1079+ },
1080+ "annotations": {
1081+ "name": "annotations",
1082+ "type": "json",
1083+ "primaryKey": false,
1084+ "notNull": false
1085+ },
1086+ "created_at": {
1087+ "name": "created_at",
1088+ "type": "timestamp",
1089+ "primaryKey": false,
1090+ "notNull": true
1091+ },
1092+ "updated_at": {
1093+ "name": "updated_at",
1094+ "type": "timestamp",
1095+ "primaryKey": false,
1096+ "notNull": true
1097+ },
1098+ "row_id": {
1099+ "name": "row_id",
1100+ "type": "varchar(255)",
1101+ "primaryKey": true,
1102+ "notNull": true
1103+ },
1104+ "tenant": {
1105+ "name": "tenant",
1106+ "type": "varchar(255)",
1107+ "primaryKey": false,
1108+ "notNull": true
1109+ },
1110+ "owner": {
1111+ "name": "owner",
1112+ "type": "varchar(255)",
1113+ "primaryKey": false,
1114+ "notNull": true
1115+ },
1116+ "subject": {
1117+ "name": "subject",
1118+ "type": "varchar(255)",
1119+ "primaryKey": false,
1120+ "notNull": true
1121+ }
1122+ },
1123+ "indexes": {
1124+ "tool_uidx": {
1125+ "name": "tool_uidx",
1126+ "columns": [
1127+ {
1128+ "expression": "tenant",
1129+ "isExpression": false,
1130+ "asc": true,
1131+ "nulls": "last"
1132+ },
1133+ {
1134+ "expression": "owner",
1135+ "isExpression": false,
1136+ "asc": true,
1137+ "nulls": "last"
1138+ },
1139+ {
1140+ "expression": "subject",
1141+ "isExpression": false,
1142+ "asc": true,
1143+ "nulls": "last"
1144+ },
1145+ {
1146+ "expression": "integration",
1147+ "isExpression": false,
1148+ "asc": true,
1149+ "nulls": "last"
1150+ },
1151+ {
1152+ "expression": "connection",
1153+ "isExpression": false,
1154+ "asc": true,
1155+ "nulls": "last"
1156+ },
1157+ {
1158+ "expression": "name",
1159+ "isExpression": false,
1160+ "asc": true,
1161+ "nulls": "last"
1162+ }
1163+ ],
1164+ "isUnique": true,
1165+ "concurrently": false,
1166+ "method": "btree",
1167+ "with": {}
1168+ }
1169+ },
1170+ "foreignKeys": {},
1171+ "compositePrimaryKeys": {},
1172+ "uniqueConstraints": {},
1173+ "policies": {},
1174+ "checkConstraints": {},
1175+ "isRLSEnabled": false
1176+ },
1177+ "public.tool_policy": {
1178+ "name": "tool_policy",
1179+ "schema": "",
1180+ "columns": {
1181+ "id": {
1182+ "name": "id",
1183+ "type": "varchar(255)",
1184+ "primaryKey": false,
1185+ "notNull": true
1186+ },
1187+ "pattern": {
1188+ "name": "pattern",
1189+ "type": "text",
1190+ "primaryKey": false,
1191+ "notNull": true
1192+ },
1193+ "action": {
1194+ "name": "action",
1195+ "type": "text",
1196+ "primaryKey": false,
1197+ "notNull": true
1198+ },
1199+ "position": {
1200+ "name": "position",
1201+ "type": "text",
1202+ "primaryKey": false,
1203+ "notNull": true
1204+ },
1205+ "created_at": {
1206+ "name": "created_at",
1207+ "type": "timestamp",
1208+ "primaryKey": false,
1209+ "notNull": true
1210+ },
1211+ "updated_at": {
1212+ "name": "updated_at",
1213+ "type": "timestamp",
1214+ "primaryKey": false,
1215+ "notNull": true
1216+ },
1217+ "row_id": {
1218+ "name": "row_id",
1219+ "type": "varchar(255)",
1220+ "primaryKey": true,
1221+ "notNull": true
1222+ },
1223+ "tenant": {
1224+ "name": "tenant",
1225+ "type": "varchar(255)",
1226+ "primaryKey": false,
1227+ "notNull": true
1228+ },
1229+ "owner": {
1230+ "name": "owner",
1231+ "type": "varchar(255)",
1232+ "primaryKey": false,
1233+ "notNull": true
1234+ },
1235+ "subject": {
1236+ "name": "subject",
1237+ "type": "varchar(255)",
1238+ "primaryKey": false,
1239+ "notNull": true
1240+ }
1241+ },
1242+ "indexes": {
1243+ "tool_policy_uidx": {
1244+ "name": "tool_policy_uidx",
1245+ "columns": [
1246+ {
1247+ "expression": "tenant",
1248+ "isExpression": false,
1249+ "asc": true,
1250+ "nulls": "last"
1251+ },
1252+ {
1253+ "expression": "owner",
1254+ "isExpression": false,
1255+ "asc": true,
1256+ "nulls": "last"
1257+ },
1258+ {
1259+ "expression": "subject",
1260+ "isExpression": false,
1261+ "asc": true,
1262+ "nulls": "last"
1263+ },
1264+ {
1265+ "expression": "id",
1266+ "isExpression": false,
1267+ "asc": true,
1268+ "nulls": "last"
1269+ }
1270+ ],
1271+ "isUnique": true,
1272+ "concurrently": false,
1273+ "method": "btree",
1274+ "with": {}
1275+ }
1276+ },
1277+ "foreignKeys": {},
1278+ "compositePrimaryKeys": {},
1279+ "uniqueConstraints": {},
1280+ "policies": {},
1281+ "checkConstraints": {},
1282+ "isRLSEnabled": false
1283+ }
1284+ },
1285+ "enums": {},
1286+ "schemas": {},
1287+ "sequences": {},
1288+ "roles": {},
1289+ "policies": {},
1290+ "views": {},
1291+ "_meta": {
1292+ "columns": {},
1293+ "schemas": {},
1294+ "tables": {}
1295+ }
1296+}
apps/cloud/drizzle/meta/0012_snapshot.json+1367−0
@@ -0,0 +1,1367 @@
1+{
2+ "id": "97114994-f0fc-4419-80bf-21c529a6cf0b",
3+ "prevId": "e285be7d-2427-4b1b-8b86-50a80ada25af",
4+ "version": "7",
5+ "dialect": "postgresql",
6+ "tables": {
7+ "public.accounts": {
8+ "name": "accounts",
9+ "schema": "",
10+ "columns": {
11+ "id": {
12+ "name": "id",
13+ "type": "text",
14+ "primaryKey": true,
15+ "notNull": true
16+ },
17+ "created_at": {
18+ "name": "created_at",
19+ "type": "timestamp with time zone",
20+ "primaryKey": false,
21+ "notNull": true,
22+ "default": "now()"
23+ }
24+ },
25+ "indexes": {},
26+ "foreignKeys": {},
27+ "compositePrimaryKeys": {},
28+ "uniqueConstraints": {},
29+ "policies": {},
30+ "checkConstraints": {},
31+ "isRLSEnabled": false
32+ },
33+ "public.memberships": {
34+ "name": "memberships",
35+ "schema": "",
36+ "columns": {
37+ "account_id": {
38+ "name": "account_id",
39+ "type": "text",
40+ "primaryKey": false,
41+ "notNull": true
42+ },
43+ "organization_id": {
44+ "name": "organization_id",
45+ "type": "text",
46+ "primaryKey": false,
47+ "notNull": true
48+ },
49+ "created_at": {
50+ "name": "created_at",
51+ "type": "timestamp with time zone",
52+ "primaryKey": false,
53+ "notNull": true,
54+ "default": "now()"
55+ }
56+ },
57+ "indexes": {},
58+ "foreignKeys": {
59+ "memberships_account_id_accounts_id_fk": {
60+ "name": "memberships_account_id_accounts_id_fk",
61+ "tableFrom": "memberships",
62+ "tableTo": "accounts",
63+ "columnsFrom": ["account_id"],
64+ "columnsTo": ["id"],
65+ "onDelete": "cascade",
66+ "onUpdate": "no action"
67+ },
68+ "memberships_organization_id_organizations_id_fk": {
69+ "name": "memberships_organization_id_organizations_id_fk",
70+ "tableFrom": "memberships",
71+ "tableTo": "organizations",
72+ "columnsFrom": ["organization_id"],
73+ "columnsTo": ["id"],
74+ "onDelete": "cascade",
75+ "onUpdate": "no action"
76+ }
77+ },
78+ "compositePrimaryKeys": {
79+ "memberships_account_id_organization_id_pk": {
80+ "name": "memberships_account_id_organization_id_pk",
81+ "columns": ["account_id", "organization_id"]
82+ }
83+ },
84+ "uniqueConstraints": {},
85+ "policies": {},
86+ "checkConstraints": {},
87+ "isRLSEnabled": false
88+ },
89+ "public.organizations": {
90+ "name": "organizations",
91+ "schema": "",
92+ "columns": {
93+ "id": {
94+ "name": "id",
95+ "type": "text",
96+ "primaryKey": true,
97+ "notNull": true
98+ },
99+ "name": {
100+ "name": "name",
101+ "type": "text",
102+ "primaryKey": false,
103+ "notNull": true
104+ },
105+ "slug": {
106+ "name": "slug",
107+ "type": "text",
108+ "primaryKey": false,
109+ "notNull": true
110+ },
111+ "created_at": {
112+ "name": "created_at",
113+ "type": "timestamp with time zone",
114+ "primaryKey": false,
115+ "notNull": true,
116+ "default": "now()"
117+ }
118+ },
119+ "indexes": {
120+ "organizations_slug_unique": {
121+ "name": "organizations_slug_unique",
122+ "columns": [
123+ {
124+ "expression": "slug",
125+ "isExpression": false,
126+ "asc": true,
127+ "nulls": "last"
128+ }
129+ ],
130+ "isUnique": true,
131+ "concurrently": false,
132+ "method": "btree",
133+ "with": {}
134+ }
135+ },
136+ "foreignKeys": {},
137+ "compositePrimaryKeys": {},
138+ "uniqueConstraints": {},
139+ "policies": {},
140+ "checkConstraints": {},
141+ "isRLSEnabled": false
142+ },
143+ "public.blob": {
144+ "name": "blob",
145+ "schema": "",
146+ "columns": {
147+ "namespace": {
148+ "name": "namespace",
149+ "type": "varchar(255)",
150+ "primaryKey": false,
151+ "notNull": true
152+ },
153+ "key": {
154+ "name": "key",
155+ "type": "varchar(255)",
156+ "primaryKey": false,
157+ "notNull": true
158+ },
159+ "value": {
160+ "name": "value",
161+ "type": "text",
162+ "primaryKey": false,
163+ "notNull": true
164+ },
165+ "row_id": {
166+ "name": "row_id",
167+ "type": "varchar(255)",
168+ "primaryKey": true,
169+ "notNull": true
170+ },
171+ "id": {
172+ "name": "id",
173+ "type": "varchar(255)",
174+ "primaryKey": false,
175+ "notNull": true
176+ }
177+ },
178+ "indexes": {
179+ "blob_id_uidx": {
180+ "name": "blob_id_uidx",
181+ "columns": [
182+ {
183+ "expression": "id",
184+ "isExpression": false,
185+ "asc": true,
186+ "nulls": "last"
187+ }
188+ ],
189+ "isUnique": true,
190+ "concurrently": false,
191+ "method": "btree",
192+ "with": {}
193+ }
194+ },
195+ "foreignKeys": {},
196+ "compositePrimaryKeys": {},
197+ "uniqueConstraints": {},
198+ "policies": {},
199+ "checkConstraints": {},
200+ "isRLSEnabled": false
201+ },
202+ "public.connection": {
203+ "name": "connection",
204+ "schema": "",
205+ "columns": {
206+ "integration": {
207+ "name": "integration",
208+ "type": "varchar(255)",
209+ "primaryKey": false,
210+ "notNull": true
211+ },
212+ "name": {
213+ "name": "name",
214+ "type": "varchar(255)",
215+ "primaryKey": false,
216+ "notNull": true
217+ },
218+ "template": {
219+ "name": "template",
220+ "type": "text",
221+ "primaryKey": false,
222+ "notNull": true
223+ },
224+ "provider": {
225+ "name": "provider",
226+ "type": "text",
227+ "primaryKey": false,
228+ "notNull": true
229+ },
230+ "item_ids": {
231+ "name": "item_ids",
232+ "type": "json",
233+ "primaryKey": false,
234+ "notNull": true
235+ },
236+ "identity_label": {
237+ "name": "identity_label",
238+ "type": "text",
239+ "primaryKey": false,
240+ "notNull": false
241+ },
242+ "description": {
243+ "name": "description",
244+ "type": "text",
245+ "primaryKey": false,
246+ "notNull": false
247+ },
248+ "last_health": {
249+ "name": "last_health",
250+ "type": "json",
251+ "primaryKey": false,
252+ "notNull": false
253+ },
254+ "tools_synced_at": {
255+ "name": "tools_synced_at",
256+ "type": "bigint",
257+ "primaryKey": false,
258+ "notNull": false
259+ },
260+ "oauth_client": {
261+ "name": "oauth_client",
262+ "type": "text",
263+ "primaryKey": false,
264+ "notNull": false
265+ },
266+ "oauth_client_owner": {
267+ "name": "oauth_client_owner",
268+ "type": "text",
269+ "primaryKey": false,
270+ "notNull": false
271+ },
272+ "refresh_item_id": {
273+ "name": "refresh_item_id",
274+ "type": "text",
275+ "primaryKey": false,
276+ "notNull": false
277+ },
278+ "expires_at": {
279+ "name": "expires_at",
280+ "type": "bigint",
281+ "primaryKey": false,
282+ "notNull": false
283+ },
284+ "oauth_scope": {
285+ "name": "oauth_scope",
286+ "type": "text",
287+ "primaryKey": false,
288+ "notNull": false
289+ },
290+ "oauth_token_url": {
291+ "name": "oauth_token_url",
292+ "type": "text",
293+ "primaryKey": false,
294+ "notNull": false
295+ },
296+ "provider_state": {
297+ "name": "provider_state",
298+ "type": "json",
299+ "primaryKey": false,
300+ "notNull": false
301+ },
302+ "created_at": {
303+ "name": "created_at",
304+ "type": "timestamp",
305+ "primaryKey": false,
306+ "notNull": true
307+ },
308+ "updated_at": {
309+ "name": "updated_at",
310+ "type": "timestamp",
311+ "primaryKey": false,
312+ "notNull": true
313+ },
314+ "row_id": {
315+ "name": "row_id",
316+ "type": "varchar(255)",
317+ "primaryKey": true,
318+ "notNull": true
319+ },
320+ "tenant": {
321+ "name": "tenant",
322+ "type": "varchar(255)",
323+ "primaryKey": false,
324+ "notNull": true
325+ },
326+ "owner": {
327+ "name": "owner",
328+ "type": "varchar(255)",
329+ "primaryKey": false,
330+ "notNull": true
331+ },
332+ "subject": {
333+ "name": "subject",
334+ "type": "varchar(255)",
335+ "primaryKey": false,
336+ "notNull": true
337+ }
338+ },
339+ "indexes": {
340+ "connection_uidx": {
341+ "name": "connection_uidx",
342+ "columns": [
343+ {
344+ "expression": "tenant",
345+ "isExpression": false,
346+ "asc": true,
347+ "nulls": "last"
348+ },
349+ {
350+ "expression": "owner",
351+ "isExpression": false,
352+ "asc": true,
353+ "nulls": "last"
354+ },
355+ {
356+ "expression": "subject",
357+ "isExpression": false,
358+ "asc": true,
359+ "nulls": "last"
360+ },
361+ {
362+ "expression": "integration",
363+ "isExpression": false,
364+ "asc": true,
365+ "nulls": "last"
366+ },
367+ {
368+ "expression": "name",
369+ "isExpression": false,
370+ "asc": true,
371+ "nulls": "last"
372+ }
373+ ],
374+ "isUnique": true,
375+ "concurrently": false,
376+ "method": "btree",
377+ "with": {}
378+ }
379+ },
380+ "foreignKeys": {},
381+ "compositePrimaryKeys": {},
382+ "uniqueConstraints": {},
383+ "policies": {},
384+ "checkConstraints": {},
385+ "isRLSEnabled": false
386+ },
387+ "public.definition": {
388+ "name": "definition",
389+ "schema": "",
390+ "columns": {
391+ "integration": {
392+ "name": "integration",
393+ "type": "varchar(255)",
394+ "primaryKey": false,
395+ "notNull": true
396+ },
397+ "connection": {
398+ "name": "connection",
399+ "type": "varchar(255)",
400+ "primaryKey": false,
401+ "notNull": true
402+ },
403+ "plugin_id": {
404+ "name": "plugin_id",
405+ "type": "text",
406+ "primaryKey": false,
407+ "notNull": true
408+ },
409+ "name": {
410+ "name": "name",
411+ "type": "text",
412+ "primaryKey": false,
413+ "notNull": true
414+ },
415+ "schema": {
416+ "name": "schema",
417+ "type": "json",
418+ "primaryKey": false,
419+ "notNull": true
420+ },
421+ "created_at": {
422+ "name": "created_at",
423+ "type": "timestamp",
424+ "primaryKey": false,
425+ "notNull": true
426+ },
427+ "row_id": {
428+ "name": "row_id",
429+ "type": "varchar(255)",
430+ "primaryKey": true,
431+ "notNull": true
432+ },
433+ "tenant": {
434+ "name": "tenant",
435+ "type": "varchar(255)",
436+ "primaryKey": false,
437+ "notNull": true
438+ },
439+ "owner": {
440+ "name": "owner",
441+ "type": "varchar(255)",
442+ "primaryKey": false,
443+ "notNull": true
444+ },
445+ "subject": {
446+ "name": "subject",
447+ "type": "varchar(255)",
448+ "primaryKey": false,
449+ "notNull": true
450+ }
451+ },
452+ "indexes": {
453+ "definition_uidx": {
454+ "name": "definition_uidx",
455+ "columns": [
456+ {
457+ "expression": "tenant",
458+ "isExpression": false,
459+ "asc": true,
460+ "nulls": "last"
461+ },
462+ {
463+ "expression": "owner",
464+ "isExpression": false,
465+ "asc": true,
466+ "nulls": "last"
467+ },
468+ {
469+ "expression": "subject",
470+ "isExpression": false,
471+ "asc": true,
472+ "nulls": "last"
473+ },
474+ {
475+ "expression": "integration",
476+ "isExpression": false,
477+ "asc": true,
478+ "nulls": "last"
479+ },
480+ {
481+ "expression": "connection",
482+ "isExpression": false,
483+ "asc": true,
484+ "nulls": "last"
485+ },
486+ {
487+ "expression": "name",
488+ "isExpression": false,
489+ "asc": true,
490+ "nulls": "last"
491+ }
492+ ],
493+ "isUnique": true,
494+ "concurrently": false,
495+ "method": "btree",
496+ "with": {}
497+ }
498+ },
499+ "foreignKeys": {},
500+ "compositePrimaryKeys": {},
501+ "uniqueConstraints": {},
502+ "policies": {},
503+ "checkConstraints": {},
504+ "isRLSEnabled": false
505+ },
506+ "public.integration": {
507+ "name": "integration",
508+ "schema": "",
509+ "columns": {
510+ "slug": {
511+ "name": "slug",
512+ "type": "varchar(255)",
513+ "primaryKey": false,
514+ "notNull": true
515+ },
516+ "plugin_id": {
517+ "name": "plugin_id",
518+ "type": "text",
519+ "primaryKey": false,
520+ "notNull": true
521+ },
522+ "name": {
523+ "name": "name",
524+ "type": "text",
525+ "primaryKey": false,
526+ "notNull": false
527+ },
528+ "description": {
529+ "name": "description",
530+ "type": "text",
531+ "primaryKey": false,
532+ "notNull": false
533+ },
534+ "config": {
535+ "name": "config",
536+ "type": "json",
537+ "primaryKey": false,
538+ "notNull": false
539+ },
540+ "health_check": {
541+ "name": "health_check",
542+ "type": "json",
543+ "primaryKey": false,
544+ "notNull": false
545+ },
546+ "config_revised_at": {
547+ "name": "config_revised_at",
548+ "type": "bigint",
549+ "primaryKey": false,
550+ "notNull": false
551+ },
552+ "can_remove": {
553+ "name": "can_remove",
554+ "type": "boolean",
555+ "primaryKey": false,
556+ "notNull": true,
557+ "default": true
558+ },
559+ "can_refresh": {
560+ "name": "can_refresh",
561+ "type": "boolean",
562+ "primaryKey": false,
563+ "notNull": true,
564+ "default": false
565+ },
566+ "created_at": {
567+ "name": "created_at",
568+ "type": "timestamp",
569+ "primaryKey": false,
570+ "notNull": true
571+ },
572+ "updated_at": {
573+ "name": "updated_at",
574+ "type": "timestamp",
575+ "primaryKey": false,
576+ "notNull": true
577+ },
578+ "row_id": {
579+ "name": "row_id",
580+ "type": "varchar(255)",
581+ "primaryKey": true,
582+ "notNull": true
583+ },
584+ "tenant": {
585+ "name": "tenant",
586+ "type": "varchar(255)",
587+ "primaryKey": false,
588+ "notNull": true
589+ }
590+ },
591+ "indexes": {
592+ "integration_uidx": {
593+ "name": "integration_uidx",
594+ "columns": [
595+ {
596+ "expression": "tenant",
597+ "isExpression": false,
598+ "asc": true,
599+ "nulls": "last"
600+ },
601+ {
602+ "expression": "slug",
603+ "isExpression": false,
604+ "asc": true,
605+ "nulls": "last"
606+ }
607+ ],
608+ "isUnique": true,
609+ "concurrently": false,
610+ "method": "btree",
611+ "with": {}
612+ }
613+ },
614+ "foreignKeys": {},
615+ "compositePrimaryKeys": {},
616+ "uniqueConstraints": {},
617+ "policies": {},
618+ "checkConstraints": {},
619+ "isRLSEnabled": false
620+ },
621+ "public.oauth_client": {
622+ "name": "oauth_client",
623+ "schema": "",
624+ "columns": {
625+ "slug": {
626+ "name": "slug",
627+ "type": "varchar(255)",
628+ "primaryKey": false,
629+ "notNull": true
630+ },
631+ "authorization_url": {
632+ "name": "authorization_url",
633+ "type": "text",
634+ "primaryKey": false,
635+ "notNull": true
636+ },
637+ "token_url": {
638+ "name": "token_url",
639+ "type": "text",
640+ "primaryKey": false,
641+ "notNull": true
642+ },
643+ "grant": {
644+ "name": "grant",
645+ "type": "text",
646+ "primaryKey": false,
647+ "notNull": true
648+ },
649+ "client_id": {
650+ "name": "client_id",
651+ "type": "text",
652+ "primaryKey": false,
653+ "notNull": true
654+ },
655+ "client_secret_item_id": {
656+ "name": "client_secret_item_id",
657+ "type": "text",
658+ "primaryKey": false,
659+ "notNull": false
660+ },
661+ "resource": {
662+ "name": "resource",
663+ "type": "text",
664+ "primaryKey": false,
665+ "notNull": false
666+ },
667+ "origin_kind": {
668+ "name": "origin_kind",
669+ "type": "text",
670+ "primaryKey": false,
671+ "notNull": false
672+ },
673+ "origin_integration": {
674+ "name": "origin_integration",
675+ "type": "text",
676+ "primaryKey": false,
677+ "notNull": false
678+ },
679+ "origin_issuer": {
680+ "name": "origin_issuer",
681+ "type": "text",
682+ "primaryKey": false,
683+ "notNull": false
684+ },
685+ "origin_redirect_uri": {
686+ "name": "origin_redirect_uri",
687+ "type": "text",
688+ "primaryKey": false,
689+ "notNull": false
690+ },
691+ "created_at": {
692+ "name": "created_at",
693+ "type": "timestamp",
694+ "primaryKey": false,
695+ "notNull": true
696+ },
697+ "row_id": {
698+ "name": "row_id",
699+ "type": "varchar(255)",
700+ "primaryKey": true,
701+ "notNull": true
702+ },
703+ "tenant": {
704+ "name": "tenant",
705+ "type": "varchar(255)",
706+ "primaryKey": false,
707+ "notNull": true
708+ },
709+ "owner": {
710+ "name": "owner",
711+ "type": "varchar(255)",
712+ "primaryKey": false,
713+ "notNull": true
714+ },
715+ "subject": {
716+ "name": "subject",
717+ "type": "varchar(255)",
718+ "primaryKey": false,
719+ "notNull": true
720+ }
721+ },
722+ "indexes": {
723+ "oauth_client_uidx": {
724+ "name": "oauth_client_uidx",
725+ "columns": [
726+ {
727+ "expression": "tenant",
728+ "isExpression": false,
729+ "asc": true,
730+ "nulls": "last"
731+ },
732+ {
733+ "expression": "owner",
734+ "isExpression": false,
735+ "asc": true,
736+ "nulls": "last"
737+ },
738+ {
739+ "expression": "subject",
740+ "isExpression": false,
741+ "asc": true,
742+ "nulls": "last"
743+ },
744+ {
745+ "expression": "slug",
746+ "isExpression": false,
747+ "asc": true,
748+ "nulls": "last"
749+ }
750+ ],
751+ "isUnique": true,
752+ "concurrently": false,
753+ "method": "btree",
754+ "with": {}
755+ }
756+ },
757+ "foreignKeys": {},
758+ "compositePrimaryKeys": {},
759+ "uniqueConstraints": {},
760+ "policies": {},
761+ "checkConstraints": {},
762+ "isRLSEnabled": false
763+ },
764+ "public.oauth_session": {
765+ "name": "oauth_session",
766+ "schema": "",
767+ "columns": {
768+ "state": {
769+ "name": "state",
770+ "type": "varchar(255)",
771+ "primaryKey": false,
772+ "notNull": true
773+ },
774+ "client_slug": {
775+ "name": "client_slug",
776+ "type": "text",
777+ "primaryKey": false,
778+ "notNull": true
779+ },
780+ "integration": {
781+ "name": "integration",
782+ "type": "text",
783+ "primaryKey": false,
784+ "notNull": true
785+ },
786+ "name": {
787+ "name": "name",
788+ "type": "text",
789+ "primaryKey": false,
790+ "notNull": true
791+ },
792+ "template": {
793+ "name": "template",
794+ "type": "text",
795+ "primaryKey": false,
796+ "notNull": true
797+ },
798+ "redirect_url": {
799+ "name": "redirect_url",
800+ "type": "text",
801+ "primaryKey": false,
802+ "notNull": true
803+ },
804+ "pkce_verifier": {
805+ "name": "pkce_verifier",
806+ "type": "text",
807+ "primaryKey": false,
808+ "notNull": false
809+ },
810+ "identity_label": {
811+ "name": "identity_label",
812+ "type": "text",
813+ "primaryKey": false,
814+ "notNull": false
815+ },
816+ "payload": {
817+ "name": "payload",
818+ "type": "json",
819+ "primaryKey": false,
820+ "notNull": true
821+ },
822+ "expires_at": {
823+ "name": "expires_at",
824+ "type": "bigint",
825+ "primaryKey": false,
826+ "notNull": true
827+ },
828+ "created_at": {
829+ "name": "created_at",
830+ "type": "timestamp",
831+ "primaryKey": false,
832+ "notNull": true
833+ },
834+ "row_id": {
835+ "name": "row_id",
836+ "type": "varchar(255)",
837+ "primaryKey": true,
838+ "notNull": true
839+ },
840+ "tenant": {
841+ "name": "tenant",
842+ "type": "varchar(255)",
843+ "primaryKey": false,
844+ "notNull": true
845+ },
846+ "owner": {
847+ "name": "owner",
848+ "type": "varchar(255)",
849+ "primaryKey": false,
850+ "notNull": true
851+ },
852+ "subject": {
853+ "name": "subject",
854+ "type": "varchar(255)",
855+ "primaryKey": false,
856+ "notNull": true
857+ }
858+ },
859+ "indexes": {
860+ "oauth_session_uidx": {
861+ "name": "oauth_session_uidx",
862+ "columns": [
863+ {
864+ "expression": "tenant",
865+ "isExpression": false,
866+ "asc": true,
867+ "nulls": "last"
868+ },
869+ {
870+ "expression": "state",
871+ "isExpression": false,
872+ "asc": true,
873+ "nulls": "last"
874+ }
875+ ],
876+ "isUnique": true,
877+ "concurrently": false,
878+ "method": "btree",
879+ "with": {}
880+ }
881+ },
882+ "foreignKeys": {},
883+ "compositePrimaryKeys": {},
884+ "uniqueConstraints": {},
885+ "policies": {},
886+ "checkConstraints": {},
887+ "isRLSEnabled": false
888+ },
889+ "public.plugin_storage": {
890+ "name": "plugin_storage",
891+ "schema": "",
892+ "columns": {
893+ "plugin_id": {
894+ "name": "plugin_id",
895+ "type": "varchar(255)",
896+ "primaryKey": false,
897+ "notNull": true
898+ },
899+ "collection": {
900+ "name": "collection",
901+ "type": "varchar(255)",
902+ "primaryKey": false,
903+ "notNull": true
904+ },
905+ "key": {
906+ "name": "key",
907+ "type": "varchar(255)",
908+ "primaryKey": false,
909+ "notNull": true
910+ },
911+ "data": {
912+ "name": "data",
913+ "type": "json",
914+ "primaryKey": false,
915+ "notNull": true
916+ },
917+ "created_at": {
918+ "name": "created_at",
919+ "type": "timestamp",
920+ "primaryKey": false,
921+ "notNull": true
922+ },
923+ "updated_at": {
924+ "name": "updated_at",
925+ "type": "timestamp",
926+ "primaryKey": false,
927+ "notNull": true
928+ },
929+ "row_id": {
930+ "name": "row_id",
931+ "type": "varchar(255)",
932+ "primaryKey": true,
933+ "notNull": true
934+ },
935+ "tenant": {
936+ "name": "tenant",
937+ "type": "varchar(255)",
938+ "primaryKey": false,
939+ "notNull": true
940+ },
941+ "owner": {
942+ "name": "owner",
943+ "type": "varchar(255)",
944+ "primaryKey": false,
945+ "notNull": true
946+ },
947+ "subject": {
948+ "name": "subject",
949+ "type": "varchar(255)",
950+ "primaryKey": false,
951+ "notNull": true
952+ }
953+ },
954+ "indexes": {
955+ "plugin_storage_uidx": {
956+ "name": "plugin_storage_uidx",
957+ "columns": [
958+ {
959+ "expression": "tenant",
960+ "isExpression": false,
961+ "asc": true,
962+ "nulls": "last"
963+ },
964+ {
965+ "expression": "owner",
966+ "isExpression": false,
967+ "asc": true,
968+ "nulls": "last"
969+ },
970+ {
971+ "expression": "subject",
972+ "isExpression": false,
973+ "asc": true,
974+ "nulls": "last"
975+ },
976+ {
977+ "expression": "plugin_id",
978+ "isExpression": false,
979+ "asc": true,
980+ "nulls": "last"
981+ },
982+ {
983+ "expression": "collection",
984+ "isExpression": false,
985+ "asc": true,
986+ "nulls": "last"
987+ },
988+ {
989+ "expression": "key",
990+ "isExpression": false,
991+ "asc": true,
992+ "nulls": "last"
993+ }
994+ ],
995+ "isUnique": true,
996+ "concurrently": false,
997+ "method": "btree",
998+ "with": {}
999+ }
1000+ },
1001+ "foreignKeys": {},
1002+ "compositePrimaryKeys": {},
1003+ "uniqueConstraints": {},
1004+ "policies": {},
1005+ "checkConstraints": {},
1006+ "isRLSEnabled": false
1007+ },
1008+ "public.private_executor_cloud_settings": {
1009+ "name": "private_executor_cloud_settings",
1010+ "schema": "",
1011+ "columns": {
1012+ "id": {
1013+ "name": "id",
1014+ "type": "varchar(255)",
1015+ "primaryKey": true,
1016+ "notNull": true
1017+ },
1018+ "version": {
1019+ "name": "version",
1020+ "type": "varchar(255)",
1021+ "primaryKey": false,
1022+ "notNull": true,
1023+ "default": "'1.0.0'"
1024+ }
1025+ },
1026+ "indexes": {},
1027+ "foreignKeys": {},
1028+ "compositePrimaryKeys": {},
1029+ "uniqueConstraints": {},
1030+ "policies": {},
1031+ "checkConstraints": {},
1032+ "isRLSEnabled": false
1033+ },
1034+ "public.subject": {
1035+ "name": "subject",
1036+ "schema": "",
1037+ "columns": {
1038+ "external_id": {
1039+ "name": "external_id",
1040+ "type": "varchar(255)",
1041+ "primaryKey": false,
1042+ "notNull": true
1043+ },
1044+ "created_at": {
1045+ "name": "created_at",
1046+ "type": "timestamp",
1047+ "primaryKey": false,
1048+ "notNull": true
1049+ },
1050+ "last_seen_at": {
1051+ "name": "last_seen_at",
1052+ "type": "bigint",
1053+ "primaryKey": false,
1054+ "notNull": false
1055+ },
1056+ "status": {
1057+ "name": "status",
1058+ "type": "text",
1059+ "primaryKey": false,
1060+ "notNull": false
1061+ },
1062+ "row_id": {
1063+ "name": "row_id",
1064+ "type": "varchar(255)",
1065+ "primaryKey": true,
1066+ "notNull": true
1067+ },
1068+ "tenant": {
1069+ "name": "tenant",
1070+ "type": "varchar(255)",
1071+ "primaryKey": false,
1072+ "notNull": true
1073+ }
1074+ },
1075+ "indexes": {
1076+ "subject_uidx": {
1077+ "name": "subject_uidx",
1078+ "columns": [
1079+ {
1080+ "expression": "tenant",
1081+ "isExpression": false,
1082+ "asc": true,
1083+ "nulls": "last"
1084+ },
1085+ {
1086+ "expression": "external_id",
1087+ "isExpression": false,
1088+ "asc": true,
1089+ "nulls": "last"
1090+ }
1091+ ],
1092+ "isUnique": true,
1093+ "concurrently": false,
1094+ "method": "btree",
1095+ "with": {}
1096+ }
1097+ },
1098+ "foreignKeys": {},
1099+ "compositePrimaryKeys": {},
1100+ "uniqueConstraints": {},
1101+ "policies": {},
1102+ "checkConstraints": {},
1103+ "isRLSEnabled": false
1104+ },
1105+ "public.tool": {
1106+ "name": "tool",
1107+ "schema": "",
1108+ "columns": {
1109+ "integration": {
1110+ "name": "integration",
1111+ "type": "varchar(255)",
1112+ "primaryKey": false,
1113+ "notNull": true
1114+ },
1115+ "connection": {
1116+ "name": "connection",
1117+ "type": "varchar(255)",
1118+ "primaryKey": false,
1119+ "notNull": true
1120+ },
1121+ "plugin_id": {
1122+ "name": "plugin_id",
1123+ "type": "text",
1124+ "primaryKey": false,
1125+ "notNull": true
1126+ },
1127+ "name": {
1128+ "name": "name",
1129+ "type": "varchar(255)",
1130+ "primaryKey": false,
1131+ "notNull": true
1132+ },
1133+ "description": {
1134+ "name": "description",
1135+ "type": "text",
1136+ "primaryKey": false,
1137+ "notNull": true
1138+ },
1139+ "input_schema": {
1140+ "name": "input_schema",
1141+ "type": "json",
1142+ "primaryKey": false,
1143+ "notNull": false
1144+ },
1145+ "output_schema": {
1146+ "name": "output_schema",
1147+ "type": "json",
1148+ "primaryKey": false,
1149+ "notNull": false
1150+ },
1151+ "annotations": {
1152+ "name": "annotations",
1153+ "type": "json",
1154+ "primaryKey": false,
1155+ "notNull": false
1156+ },
1157+ "created_at": {
1158+ "name": "created_at",
1159+ "type": "timestamp",
1160+ "primaryKey": false,
1161+ "notNull": true
1162+ },
1163+ "updated_at": {
1164+ "name": "updated_at",
1165+ "type": "timestamp",
1166+ "primaryKey": false,
1167+ "notNull": true
1168+ },
1169+ "row_id": {
1170+ "name": "row_id",
1171+ "type": "varchar(255)",
1172+ "primaryKey": true,
1173+ "notNull": true
1174+ },
1175+ "tenant": {
1176+ "name": "tenant",
1177+ "type": "varchar(255)",
1178+ "primaryKey": false,
1179+ "notNull": true
1180+ },
1181+ "owner": {
1182+ "name": "owner",
1183+ "type": "varchar(255)",
1184+ "primaryKey": false,
1185+ "notNull": true
1186+ },
1187+ "subject": {
1188+ "name": "subject",
1189+ "type": "varchar(255)",
1190+ "primaryKey": false,
1191+ "notNull": true
1192+ }
1193+ },
1194+ "indexes": {
1195+ "tool_uidx": {
1196+ "name": "tool_uidx",
1197+ "columns": [
1198+ {
1199+ "expression": "tenant",
1200+ "isExpression": false,
1201+ "asc": true,
1202+ "nulls": "last"
1203+ },
1204+ {
1205+ "expression": "owner",
1206+ "isExpression": false,
1207+ "asc": true,
1208+ "nulls": "last"
1209+ },
1210+ {
1211+ "expression": "subject",
1212+ "isExpression": false,
1213+ "asc": true,
1214+ "nulls": "last"
1215+ },
1216+ {
1217+ "expression": "integration",
1218+ "isExpression": false,
1219+ "asc": true,
1220+ "nulls": "last"
1221+ },
1222+ {
1223+ "expression": "connection",
1224+ "isExpression": false,
1225+ "asc": true,
1226+ "nulls": "last"
1227+ },
1228+ {
1229+ "expression": "name",
1230+ "isExpression": false,
1231+ "asc": true,
1232+ "nulls": "last"
1233+ }
1234+ ],
1235+ "isUnique": true,
1236+ "concurrently": false,
1237+ "method": "btree",
1238+ "with": {}
1239+ }
1240+ },
1241+ "foreignKeys": {},
1242+ "compositePrimaryKeys": {},
1243+ "uniqueConstraints": {},
1244+ "policies": {},
1245+ "checkConstraints": {},
1246+ "isRLSEnabled": false
1247+ },
1248+ "public.tool_policy": {
1249+ "name": "tool_policy",
1250+ "schema": "",
1251+ "columns": {
1252+ "id": {
1253+ "name": "id",
1254+ "type": "varchar(255)",
1255+ "primaryKey": false,
1256+ "notNull": true
1257+ },
1258+ "pattern": {
1259+ "name": "pattern",
1260+ "type": "text",
1261+ "primaryKey": false,
1262+ "notNull": true
1263+ },
1264+ "action": {
1265+ "name": "action",
1266+ "type": "text",
1267+ "primaryKey": false,
1268+ "notNull": true
1269+ },
1270+ "position": {
1271+ "name": "position",
1272+ "type": "text",
1273+ "primaryKey": false,
1274+ "notNull": true
1275+ },
1276+ "created_at": {
1277+ "name": "created_at",
1278+ "type": "timestamp",
1279+ "primaryKey": false,
1280+ "notNull": true
1281+ },
1282+ "updated_at": {
1283+ "name": "updated_at",
1284+ "type": "timestamp",
1285+ "primaryKey": false,
1286+ "notNull": true
1287+ },
1288+ "row_id": {
1289+ "name": "row_id",
1290+ "type": "varchar(255)",
1291+ "primaryKey": true,
1292+ "notNull": true
1293+ },
1294+ "tenant": {
1295+ "name": "tenant",
1296+ "type": "varchar(255)",
1297+ "primaryKey": false,
1298+ "notNull": true
1299+ },
1300+ "owner": {
1301+ "name": "owner",
1302+ "type": "varchar(255)",
1303+ "primaryKey": false,
1304+ "notNull": true
1305+ },
1306+ "subject": {
1307+ "name": "subject",
1308+ "type": "varchar(255)",
1309+ "primaryKey": false,
1310+ "notNull": true
1311+ }
1312+ },
1313+ "indexes": {
1314+ "tool_policy_uidx": {
1315+ "name": "tool_policy_uidx",
1316+ "columns": [
1317+ {
1318+ "expression": "tenant",
1319+ "isExpression": false,
1320+ "asc": true,
1321+ "nulls": "last"
1322+ },
1323+ {
1324+ "expression": "owner",
1325+ "isExpression": false,
1326+ "asc": true,
1327+ "nulls": "last"
1328+ },
1329+ {
1330+ "expression": "subject",
1331+ "isExpression": false,
1332+ "asc": true,
1333+ "nulls": "last"
1334+ },
1335+ {
1336+ "expression": "id",
1337+ "isExpression": false,
1338+ "asc": true,
1339+ "nulls": "last"
1340+ }
1341+ ],
1342+ "isUnique": true,
1343+ "concurrently": false,
1344+ "method": "btree",
1345+ "with": {}
1346+ }
1347+ },
1348+ "foreignKeys": {},
1349+ "compositePrimaryKeys": {},
1350+ "uniqueConstraints": {},
1351+ "policies": {},
1352+ "checkConstraints": {},
1353+ "isRLSEnabled": false
1354+ }
1355+ },
1356+ "enums": {},
1357+ "schemas": {},
1358+ "sequences": {},
1359+ "roles": {},
1360+ "policies": {},
1361+ "views": {},
1362+ "_meta": {
1363+ "columns": {},
1364+ "schemas": {},
1365+ "tables": {}
1366+ }
1367+}
apps/cloud/drizzle/meta/0013_snapshot.json+1474−0
@@ -0,0 +1,1474 @@
1+{
2+ "id": "edc90067-822c-443a-8fcd-3bac77bca317",
3+ "prevId": "97114994-f0fc-4419-80bf-21c529a6cf0b",
4+ "version": "7",
5+ "dialect": "postgresql",
6+ "tables": {
7+ "public.accounts": {
8+ "name": "accounts",
9+ "schema": "",
10+ "columns": {
11+ "id": {
12+ "name": "id",
13+ "type": "text",
14+ "primaryKey": true,
15+ "notNull": true
16+ },
17+ "created_at": {
18+ "name": "created_at",
19+ "type": "timestamp with time zone",
20+ "primaryKey": false,
21+ "notNull": true,
22+ "default": "now()"
23+ }
24+ },
25+ "indexes": {},
26+ "foreignKeys": {},
27+ "compositePrimaryKeys": {},
28+ "uniqueConstraints": {},
29+ "policies": {},
30+ "checkConstraints": {},
31+ "isRLSEnabled": false
32+ },
33+ "public.memberships": {
34+ "name": "memberships",
35+ "schema": "",
36+ "columns": {
37+ "account_id": {
38+ "name": "account_id",
39+ "type": "text",
40+ "primaryKey": false,
41+ "notNull": true
42+ },
43+ "organization_id": {
44+ "name": "organization_id",
45+ "type": "text",
46+ "primaryKey": false,
47+ "notNull": true
48+ },
49+ "created_at": {
50+ "name": "created_at",
51+ "type": "timestamp with time zone",
52+ "primaryKey": false,
53+ "notNull": true,
54+ "default": "now()"
55+ }
56+ },
57+ "indexes": {},
58+ "foreignKeys": {
59+ "memberships_account_id_accounts_id_fk": {
60+ "name": "memberships_account_id_accounts_id_fk",
61+ "tableFrom": "memberships",
62+ "tableTo": "accounts",
63+ "columnsFrom": ["account_id"],
64+ "columnsTo": ["id"],
65+ "onDelete": "cascade",
66+ "onUpdate": "no action"
67+ },
68+ "memberships_organization_id_organizations_id_fk": {
69+ "name": "memberships_organization_id_organizations_id_fk",
70+ "tableFrom": "memberships",
71+ "tableTo": "organizations",
72+ "columnsFrom": ["organization_id"],
73+ "columnsTo": ["id"],
74+ "onDelete": "cascade",
75+ "onUpdate": "no action"
76+ }
77+ },
78+ "compositePrimaryKeys": {
79+ "memberships_account_id_organization_id_pk": {
80+ "name": "memberships_account_id_organization_id_pk",
81+ "columns": ["account_id", "organization_id"]
82+ }
83+ },
84+ "uniqueConstraints": {},
85+ "policies": {},
86+ "checkConstraints": {},
87+ "isRLSEnabled": false
88+ },
89+ "public.organizations": {
90+ "name": "organizations",
91+ "schema": "",
92+ "columns": {
93+ "id": {
94+ "name": "id",
95+ "type": "text",
96+ "primaryKey": true,
97+ "notNull": true
98+ },
99+ "name": {
100+ "name": "name",
101+ "type": "text",
102+ "primaryKey": false,
103+ "notNull": true
104+ },
105+ "slug": {
106+ "name": "slug",
107+ "type": "text",
108+ "primaryKey": false,
109+ "notNull": true
110+ },
111+ "created_at": {
112+ "name": "created_at",
113+ "type": "timestamp with time zone",
114+ "primaryKey": false,
115+ "notNull": true,
116+ "default": "now()"
117+ }
118+ },
119+ "indexes": {
120+ "organizations_slug_unique": {
121+ "name": "organizations_slug_unique",
122+ "columns": [
123+ {
124+ "expression": "slug",
125+ "isExpression": false,
126+ "asc": true,
127+ "nulls": "last"
128+ }
129+ ],
130+ "isUnique": true,
131+ "concurrently": false,
132+ "method": "btree",
133+ "with": {}
134+ }
135+ },
136+ "foreignKeys": {},
137+ "compositePrimaryKeys": {},
138+ "uniqueConstraints": {},
139+ "policies": {},
140+ "checkConstraints": {},
141+ "isRLSEnabled": false
142+ },
143+ "public.artifact": {
144+ "name": "artifact",
145+ "schema": "",
146+ "columns": {
147+ "id": {
148+ "name": "id",
149+ "type": "varchar(255)",
150+ "primaryKey": false,
151+ "notNull": true
152+ },
153+ "title": {
154+ "name": "title",
155+ "type": "text",
156+ "primaryKey": false,
157+ "notNull": true
158+ },
159+ "description": {
160+ "name": "description",
161+ "type": "text",
162+ "primaryKey": false,
163+ "notNull": false
164+ },
165+ "code": {
166+ "name": "code",
167+ "type": "text",
168+ "primaryKey": false,
169+ "notNull": true
170+ },
171+ "created_at": {
172+ "name": "created_at",
173+ "type": "timestamp",
174+ "primaryKey": false,
175+ "notNull": true
176+ },
177+ "updated_at": {
178+ "name": "updated_at",
179+ "type": "timestamp",
180+ "primaryKey": false,
181+ "notNull": true
182+ },
183+ "row_id": {
184+ "name": "row_id",
185+ "type": "varchar(255)",
186+ "primaryKey": true,
187+ "notNull": true
188+ },
189+ "tenant": {
190+ "name": "tenant",
191+ "type": "varchar(255)",
192+ "primaryKey": false,
193+ "notNull": true
194+ },
195+ "owner": {
196+ "name": "owner",
197+ "type": "varchar(255)",
198+ "primaryKey": false,
199+ "notNull": true
200+ },
201+ "subject": {
202+ "name": "subject",
203+ "type": "varchar(255)",
204+ "primaryKey": false,
205+ "notNull": true
206+ }
207+ },
208+ "indexes": {
209+ "artifact_uidx": {
210+ "name": "artifact_uidx",
211+ "columns": [
212+ {
213+ "expression": "tenant",
214+ "isExpression": false,
215+ "asc": true,
216+ "nulls": "last"
217+ },
218+ {
219+ "expression": "owner",
220+ "isExpression": false,
221+ "asc": true,
222+ "nulls": "last"
223+ },
224+ {
225+ "expression": "subject",
226+ "isExpression": false,
227+ "asc": true,
228+ "nulls": "last"
229+ },
230+ {
231+ "expression": "id",
232+ "isExpression": false,
233+ "asc": true,
234+ "nulls": "last"
235+ }
236+ ],
237+ "isUnique": true,
238+ "concurrently": false,
239+ "method": "btree",
240+ "with": {}
241+ }
242+ },
243+ "foreignKeys": {},
244+ "compositePrimaryKeys": {},
245+ "uniqueConstraints": {},
246+ "policies": {},
247+ "checkConstraints": {},
248+ "isRLSEnabled": false
249+ },
250+ "public.blob": {
251+ "name": "blob",
252+ "schema": "",
253+ "columns": {
254+ "namespace": {
255+ "name": "namespace",
256+ "type": "varchar(255)",
257+ "primaryKey": false,
258+ "notNull": true
259+ },
260+ "key": {
261+ "name": "key",
262+ "type": "varchar(255)",
263+ "primaryKey": false,
264+ "notNull": true
265+ },
266+ "value": {
267+ "name": "value",
268+ "type": "text",
269+ "primaryKey": false,
270+ "notNull": true
271+ },
272+ "row_id": {
273+ "name": "row_id",
274+ "type": "varchar(255)",
275+ "primaryKey": true,
276+ "notNull": true
277+ },
278+ "id": {
279+ "name": "id",
280+ "type": "varchar(255)",
281+ "primaryKey": false,
282+ "notNull": true
283+ }
284+ },
285+ "indexes": {
286+ "blob_id_uidx": {
287+ "name": "blob_id_uidx",
288+ "columns": [
289+ {
290+ "expression": "id",
291+ "isExpression": false,
292+ "asc": true,
293+ "nulls": "last"
294+ }
295+ ],
296+ "isUnique": true,
297+ "concurrently": false,
298+ "method": "btree",
299+ "with": {}
300+ }
301+ },
302+ "foreignKeys": {},
303+ "compositePrimaryKeys": {},
304+ "uniqueConstraints": {},
305+ "policies": {},
306+ "checkConstraints": {},
307+ "isRLSEnabled": false
308+ },
309+ "public.connection": {
310+ "name": "connection",
311+ "schema": "",
312+ "columns": {
313+ "integration": {
314+ "name": "integration",
315+ "type": "varchar(255)",
316+ "primaryKey": false,
317+ "notNull": true
318+ },
319+ "name": {
320+ "name": "name",
321+ "type": "varchar(255)",
322+ "primaryKey": false,
323+ "notNull": true
324+ },
325+ "template": {
326+ "name": "template",
327+ "type": "text",
328+ "primaryKey": false,
329+ "notNull": true
330+ },
331+ "provider": {
332+ "name": "provider",
333+ "type": "text",
334+ "primaryKey": false,
335+ "notNull": true
336+ },
337+ "item_ids": {
338+ "name": "item_ids",
339+ "type": "json",
340+ "primaryKey": false,
341+ "notNull": true
342+ },
343+ "identity_label": {
344+ "name": "identity_label",
345+ "type": "text",
346+ "primaryKey": false,
347+ "notNull": false
348+ },
349+ "description": {
350+ "name": "description",
351+ "type": "text",
352+ "primaryKey": false,
353+ "notNull": false
354+ },
355+ "last_health": {
356+ "name": "last_health",
357+ "type": "json",
358+ "primaryKey": false,
359+ "notNull": false
360+ },
361+ "tools_synced_at": {
362+ "name": "tools_synced_at",
363+ "type": "bigint",
364+ "primaryKey": false,
365+ "notNull": false
366+ },
367+ "oauth_client": {
368+ "name": "oauth_client",
369+ "type": "text",
370+ "primaryKey": false,
371+ "notNull": false
372+ },
373+ "oauth_client_owner": {
374+ "name": "oauth_client_owner",
375+ "type": "text",
376+ "primaryKey": false,
377+ "notNull": false
378+ },
379+ "refresh_item_id": {
380+ "name": "refresh_item_id",
381+ "type": "text",
382+ "primaryKey": false,
383+ "notNull": false
384+ },
385+ "expires_at": {
386+ "name": "expires_at",
387+ "type": "bigint",
388+ "primaryKey": false,
389+ "notNull": false
390+ },
391+ "oauth_scope": {
392+ "name": "oauth_scope",
393+ "type": "text",
394+ "primaryKey": false,
395+ "notNull": false
396+ },
397+ "oauth_token_url": {
398+ "name": "oauth_token_url",
399+ "type": "text",
400+ "primaryKey": false,
401+ "notNull": false
402+ },
403+ "provider_state": {
404+ "name": "provider_state",
405+ "type": "json",
406+ "primaryKey": false,
407+ "notNull": false
408+ },
409+ "created_at": {
410+ "name": "created_at",
411+ "type": "timestamp",
412+ "primaryKey": false,
413+ "notNull": true
414+ },
415+ "updated_at": {
416+ "name": "updated_at",
417+ "type": "timestamp",
418+ "primaryKey": false,
419+ "notNull": true
420+ },
421+ "row_id": {
422+ "name": "row_id",
423+ "type": "varchar(255)",
424+ "primaryKey": true,
425+ "notNull": true
426+ },
427+ "tenant": {
428+ "name": "tenant",
429+ "type": "varchar(255)",
430+ "primaryKey": false,
431+ "notNull": true
432+ },
433+ "owner": {
434+ "name": "owner",
435+ "type": "varchar(255)",
436+ "primaryKey": false,
437+ "notNull": true
438+ },
439+ "subject": {
440+ "name": "subject",
441+ "type": "varchar(255)",
442+ "primaryKey": false,
443+ "notNull": true
444+ }
445+ },
446+ "indexes": {
447+ "connection_uidx": {
448+ "name": "connection_uidx",
449+ "columns": [
450+ {
451+ "expression": "tenant",
452+ "isExpression": false,
453+ "asc": true,
454+ "nulls": "last"
455+ },
456+ {
457+ "expression": "owner",
458+ "isExpression": false,
459+ "asc": true,
460+ "nulls": "last"
461+ },
462+ {
463+ "expression": "subject",
464+ "isExpression": false,
465+ "asc": true,
466+ "nulls": "last"
467+ },
468+ {
469+ "expression": "integration",
470+ "isExpression": false,
471+ "asc": true,
472+ "nulls": "last"
473+ },
474+ {
475+ "expression": "name",
476+ "isExpression": false,
477+ "asc": true,
478+ "nulls": "last"
479+ }
480+ ],
481+ "isUnique": true,
482+ "concurrently": false,
483+ "method": "btree",
484+ "with": {}
485+ }
486+ },
487+ "foreignKeys": {},
488+ "compositePrimaryKeys": {},
489+ "uniqueConstraints": {},
490+ "policies": {},
491+ "checkConstraints": {},
492+ "isRLSEnabled": false
493+ },
494+ "public.definition": {
495+ "name": "definition",
496+ "schema": "",
497+ "columns": {
498+ "integration": {
499+ "name": "integration",
500+ "type": "varchar(255)",
501+ "primaryKey": false,
502+ "notNull": true
503+ },
504+ "connection": {
505+ "name": "connection",
506+ "type": "varchar(255)",
507+ "primaryKey": false,
508+ "notNull": true
509+ },
510+ "plugin_id": {
511+ "name": "plugin_id",
512+ "type": "text",
513+ "primaryKey": false,
514+ "notNull": true
515+ },
516+ "name": {
517+ "name": "name",
518+ "type": "text",
519+ "primaryKey": false,
520+ "notNull": true
521+ },
522+ "schema": {
523+ "name": "schema",
524+ "type": "json",
525+ "primaryKey": false,
526+ "notNull": true
527+ },
528+ "created_at": {
529+ "name": "created_at",
530+ "type": "timestamp",
531+ "primaryKey": false,
532+ "notNull": true
533+ },
534+ "row_id": {
535+ "name": "row_id",
536+ "type": "varchar(255)",
537+ "primaryKey": true,
538+ "notNull": true
539+ },
540+ "tenant": {
541+ "name": "tenant",
542+ "type": "varchar(255)",
543+ "primaryKey": false,
544+ "notNull": true
545+ },
546+ "owner": {
547+ "name": "owner",
548+ "type": "varchar(255)",
549+ "primaryKey": false,
550+ "notNull": true
551+ },
552+ "subject": {
553+ "name": "subject",
554+ "type": "varchar(255)",
555+ "primaryKey": false,
556+ "notNull": true
557+ }
558+ },
559+ "indexes": {
560+ "definition_uidx": {
561+ "name": "definition_uidx",
562+ "columns": [
563+ {
564+ "expression": "tenant",
565+ "isExpression": false,
566+ "asc": true,
567+ "nulls": "last"
568+ },
569+ {
570+ "expression": "owner",
571+ "isExpression": false,
572+ "asc": true,
573+ "nulls": "last"
574+ },
575+ {
576+ "expression": "subject",
577+ "isExpression": false,
578+ "asc": true,
579+ "nulls": "last"
580+ },
581+ {
582+ "expression": "integration",
583+ "isExpression": false,
584+ "asc": true,
585+ "nulls": "last"
586+ },
587+ {
588+ "expression": "connection",
589+ "isExpression": false,
590+ "asc": true,
591+ "nulls": "last"
592+ },
593+ {
594+ "expression": "name",
595+ "isExpression": false,
596+ "asc": true,
597+ "nulls": "last"
598+ }
599+ ],
600+ "isUnique": true,
601+ "concurrently": false,
602+ "method": "btree",
603+ "with": {}
604+ }
605+ },
606+ "foreignKeys": {},
607+ "compositePrimaryKeys": {},
608+ "uniqueConstraints": {},
609+ "policies": {},
610+ "checkConstraints": {},
611+ "isRLSEnabled": false
612+ },
613+ "public.integration": {
614+ "name": "integration",
615+ "schema": "",
616+ "columns": {
617+ "slug": {
618+ "name": "slug",
619+ "type": "varchar(255)",
620+ "primaryKey": false,
621+ "notNull": true
622+ },
623+ "plugin_id": {
624+ "name": "plugin_id",
625+ "type": "text",
626+ "primaryKey": false,
627+ "notNull": true
628+ },
629+ "name": {
630+ "name": "name",
631+ "type": "text",
632+ "primaryKey": false,
633+ "notNull": false
634+ },
635+ "description": {
636+ "name": "description",
637+ "type": "text",
638+ "primaryKey": false,
639+ "notNull": false
640+ },
641+ "config": {
642+ "name": "config",
643+ "type": "json",
644+ "primaryKey": false,
645+ "notNull": false
646+ },
647+ "health_check": {
648+ "name": "health_check",
649+ "type": "json",
650+ "primaryKey": false,
651+ "notNull": false
652+ },
653+ "config_revised_at": {
654+ "name": "config_revised_at",
655+ "type": "bigint",
656+ "primaryKey": false,
657+ "notNull": false
658+ },
659+ "can_remove": {
660+ "name": "can_remove",
661+ "type": "boolean",
662+ "primaryKey": false,
663+ "notNull": true,
664+ "default": true
665+ },
666+ "can_refresh": {
667+ "name": "can_refresh",
668+ "type": "boolean",
669+ "primaryKey": false,
670+ "notNull": true,
671+ "default": false
672+ },
673+ "created_at": {
674+ "name": "created_at",
675+ "type": "timestamp",
676+ "primaryKey": false,
677+ "notNull": true
678+ },
679+ "updated_at": {
680+ "name": "updated_at",
681+ "type": "timestamp",
682+ "primaryKey": false,
683+ "notNull": true
684+ },
685+ "row_id": {
686+ "name": "row_id",
687+ "type": "varchar(255)",
688+ "primaryKey": true,
689+ "notNull": true
690+ },
691+ "tenant": {
692+ "name": "tenant",
693+ "type": "varchar(255)",
694+ "primaryKey": false,
695+ "notNull": true
696+ }
697+ },
698+ "indexes": {
699+ "integration_uidx": {
700+ "name": "integration_uidx",
701+ "columns": [
702+ {
703+ "expression": "tenant",
704+ "isExpression": false,
705+ "asc": true,
706+ "nulls": "last"
707+ },
708+ {
709+ "expression": "slug",
710+ "isExpression": false,
711+ "asc": true,
712+ "nulls": "last"
713+ }
714+ ],
715+ "isUnique": true,
716+ "concurrently": false,
717+ "method": "btree",
718+ "with": {}
719+ }
720+ },
721+ "foreignKeys": {},
722+ "compositePrimaryKeys": {},
723+ "uniqueConstraints": {},
724+ "policies": {},
725+ "checkConstraints": {},
726+ "isRLSEnabled": false
727+ },
728+ "public.oauth_client": {
729+ "name": "oauth_client",
730+ "schema": "",
731+ "columns": {
732+ "slug": {
733+ "name": "slug",
734+ "type": "varchar(255)",
735+ "primaryKey": false,
736+ "notNull": true
737+ },
738+ "authorization_url": {
739+ "name": "authorization_url",
740+ "type": "text",
741+ "primaryKey": false,
742+ "notNull": true
743+ },
744+ "token_url": {
745+ "name": "token_url",
746+ "type": "text",
747+ "primaryKey": false,
748+ "notNull": true
749+ },
750+ "grant": {
751+ "name": "grant",
752+ "type": "text",
753+ "primaryKey": false,
754+ "notNull": true
755+ },
756+ "client_id": {
757+ "name": "client_id",
758+ "type": "text",
759+ "primaryKey": false,
760+ "notNull": true
761+ },
762+ "client_secret_item_id": {
763+ "name": "client_secret_item_id",
764+ "type": "text",
765+ "primaryKey": false,
766+ "notNull": false
767+ },
768+ "resource": {
769+ "name": "resource",
770+ "type": "text",
771+ "primaryKey": false,
772+ "notNull": false
773+ },
774+ "origin_kind": {
775+ "name": "origin_kind",
776+ "type": "text",
777+ "primaryKey": false,
778+ "notNull": false
779+ },
780+ "origin_integration": {
781+ "name": "origin_integration",
782+ "type": "text",
783+ "primaryKey": false,
784+ "notNull": false
785+ },
786+ "origin_issuer": {
787+ "name": "origin_issuer",
788+ "type": "text",
789+ "primaryKey": false,
790+ "notNull": false
791+ },
792+ "origin_redirect_uri": {
793+ "name": "origin_redirect_uri",
794+ "type": "text",
795+ "primaryKey": false,
796+ "notNull": false
797+ },
798+ "created_at": {
799+ "name": "created_at",
800+ "type": "timestamp",
801+ "primaryKey": false,
802+ "notNull": true
803+ },
804+ "row_id": {
805+ "name": "row_id",
806+ "type": "varchar(255)",
807+ "primaryKey": true,
808+ "notNull": true
809+ },
810+ "tenant": {
811+ "name": "tenant",
812+ "type": "varchar(255)",
813+ "primaryKey": false,
814+ "notNull": true
815+ },
816+ "owner": {
817+ "name": "owner",
818+ "type": "varchar(255)",
819+ "primaryKey": false,
820+ "notNull": true
821+ },
822+ "subject": {
823+ "name": "subject",
824+ "type": "varchar(255)",
825+ "primaryKey": false,
826+ "notNull": true
827+ }
828+ },
829+ "indexes": {
830+ "oauth_client_uidx": {
831+ "name": "oauth_client_uidx",
832+ "columns": [
833+ {
834+ "expression": "tenant",
835+ "isExpression": false,
836+ "asc": true,
837+ "nulls": "last"
838+ },
839+ {
840+ "expression": "owner",
841+ "isExpression": false,
842+ "asc": true,
843+ "nulls": "last"
844+ },
845+ {
846+ "expression": "subject",
847+ "isExpression": false,
848+ "asc": true,
849+ "nulls": "last"
850+ },
851+ {
852+ "expression": "slug",
853+ "isExpression": false,
854+ "asc": true,
855+ "nulls": "last"
856+ }
857+ ],
858+ "isUnique": true,
859+ "concurrently": false,
860+ "method": "btree",
861+ "with": {}
862+ }
863+ },
864+ "foreignKeys": {},
865+ "compositePrimaryKeys": {},
866+ "uniqueConstraints": {},
867+ "policies": {},
868+ "checkConstraints": {},
869+ "isRLSEnabled": false
870+ },
871+ "public.oauth_session": {
872+ "name": "oauth_session",
873+ "schema": "",
874+ "columns": {
875+ "state": {
876+ "name": "state",
877+ "type": "varchar(255)",
878+ "primaryKey": false,
879+ "notNull": true
880+ },
881+ "client_slug": {
882+ "name": "client_slug",
883+ "type": "text",
884+ "primaryKey": false,
885+ "notNull": true
886+ },
887+ "integration": {
888+ "name": "integration",
889+ "type": "text",
890+ "primaryKey": false,
891+ "notNull": true
892+ },
893+ "name": {
894+ "name": "name",
895+ "type": "text",
896+ "primaryKey": false,
897+ "notNull": true
898+ },
899+ "template": {
900+ "name": "template",
901+ "type": "text",
902+ "primaryKey": false,
903+ "notNull": true
904+ },
905+ "redirect_url": {
906+ "name": "redirect_url",
907+ "type": "text",
908+ "primaryKey": false,
909+ "notNull": true
910+ },
911+ "pkce_verifier": {
912+ "name": "pkce_verifier",
913+ "type": "text",
914+ "primaryKey": false,
915+ "notNull": false
916+ },
917+ "identity_label": {
918+ "name": "identity_label",
919+ "type": "text",
920+ "primaryKey": false,
921+ "notNull": false
922+ },
923+ "payload": {
924+ "name": "payload",
925+ "type": "json",
926+ "primaryKey": false,
927+ "notNull": true
928+ },
929+ "expires_at": {
930+ "name": "expires_at",
931+ "type": "bigint",
932+ "primaryKey": false,
933+ "notNull": true
934+ },
935+ "created_at": {
936+ "name": "created_at",
937+ "type": "timestamp",
938+ "primaryKey": false,
939+ "notNull": true
940+ },
941+ "row_id": {
942+ "name": "row_id",
943+ "type": "varchar(255)",
944+ "primaryKey": true,
945+ "notNull": true
946+ },
947+ "tenant": {
948+ "name": "tenant",
949+ "type": "varchar(255)",
950+ "primaryKey": false,
951+ "notNull": true
952+ },
953+ "owner": {
954+ "name": "owner",
955+ "type": "varchar(255)",
956+ "primaryKey": false,
957+ "notNull": true
958+ },
959+ "subject": {
960+ "name": "subject",
961+ "type": "varchar(255)",
962+ "primaryKey": false,
963+ "notNull": true
964+ }
965+ },
966+ "indexes": {
967+ "oauth_session_uidx": {
968+ "name": "oauth_session_uidx",
969+ "columns": [
970+ {
971+ "expression": "tenant",
972+ "isExpression": false,
973+ "asc": true,
974+ "nulls": "last"
975+ },
976+ {
977+ "expression": "state",
978+ "isExpression": false,
979+ "asc": true,
980+ "nulls": "last"
981+ }
982+ ],
983+ "isUnique": true,
984+ "concurrently": false,
985+ "method": "btree",
986+ "with": {}
987+ }
988+ },
989+ "foreignKeys": {},
990+ "compositePrimaryKeys": {},
991+ "uniqueConstraints": {},
992+ "policies": {},
993+ "checkConstraints": {},
994+ "isRLSEnabled": false
995+ },
996+ "public.plugin_storage": {
997+ "name": "plugin_storage",
998+ "schema": "",
999+ "columns": {
1000+ "plugin_id": {
1001+ "name": "plugin_id",
1002+ "type": "varchar(255)",
1003+ "primaryKey": false,
1004+ "notNull": true
1005+ },
1006+ "collection": {
1007+ "name": "collection",
1008+ "type": "varchar(255)",
1009+ "primaryKey": false,
1010+ "notNull": true
1011+ },
1012+ "key": {
1013+ "name": "key",
1014+ "type": "varchar(255)",
1015+ "primaryKey": false,
1016+ "notNull": true
1017+ },
1018+ "data": {
1019+ "name": "data",
1020+ "type": "json",
1021+ "primaryKey": false,
1022+ "notNull": true
1023+ },
1024+ "created_at": {
1025+ "name": "created_at",
1026+ "type": "timestamp",
1027+ "primaryKey": false,
1028+ "notNull": true
1029+ },
1030+ "updated_at": {
1031+ "name": "updated_at",
1032+ "type": "timestamp",
1033+ "primaryKey": false,
1034+ "notNull": true
1035+ },
1036+ "row_id": {
1037+ "name": "row_id",
1038+ "type": "varchar(255)",
1039+ "primaryKey": true,
1040+ "notNull": true
1041+ },
1042+ "tenant": {
1043+ "name": "tenant",
1044+ "type": "varchar(255)",
1045+ "primaryKey": false,
1046+ "notNull": true
1047+ },
1048+ "owner": {
1049+ "name": "owner",
1050+ "type": "varchar(255)",
1051+ "primaryKey": false,
1052+ "notNull": true
1053+ },
1054+ "subject": {
1055+ "name": "subject",
1056+ "type": "varchar(255)",
1057+ "primaryKey": false,
1058+ "notNull": true
1059+ }
1060+ },
1061+ "indexes": {
1062+ "plugin_storage_uidx": {
1063+ "name": "plugin_storage_uidx",
1064+ "columns": [
1065+ {
1066+ "expression": "tenant",
1067+ "isExpression": false,
1068+ "asc": true,
1069+ "nulls": "last"
1070+ },
1071+ {
1072+ "expression": "owner",
1073+ "isExpression": false,
1074+ "asc": true,
1075+ "nulls": "last"
1076+ },
1077+ {
1078+ "expression": "subject",
1079+ "isExpression": false,
1080+ "asc": true,
1081+ "nulls": "last"
1082+ },
1083+ {
1084+ "expression": "plugin_id",
1085+ "isExpression": false,
1086+ "asc": true,
1087+ "nulls": "last"
1088+ },
1089+ {
1090+ "expression": "collection",
1091+ "isExpression": false,
1092+ "asc": true,
1093+ "nulls": "last"
1094+ },
1095+ {
1096+ "expression": "key",
1097+ "isExpression": false,
1098+ "asc": true,
1099+ "nulls": "last"
1100+ }
1101+ ],
1102+ "isUnique": true,
1103+ "concurrently": false,
1104+ "method": "btree",
1105+ "with": {}
1106+ }
1107+ },
1108+ "foreignKeys": {},
1109+ "compositePrimaryKeys": {},
1110+ "uniqueConstraints": {},
1111+ "policies": {},
1112+ "checkConstraints": {},
1113+ "isRLSEnabled": false
1114+ },
1115+ "public.private_executor_cloud_settings": {
1116+ "name": "private_executor_cloud_settings",
1117+ "schema": "",
1118+ "columns": {
1119+ "id": {
1120+ "name": "id",
1121+ "type": "varchar(255)",
1122+ "primaryKey": true,
1123+ "notNull": true
1124+ },
1125+ "version": {
1126+ "name": "version",
1127+ "type": "varchar(255)",
1128+ "primaryKey": false,
1129+ "notNull": true,
1130+ "default": "'1.0.0'"
1131+ }
1132+ },
1133+ "indexes": {},
1134+ "foreignKeys": {},
1135+ "compositePrimaryKeys": {},
1136+ "uniqueConstraints": {},
1137+ "policies": {},
1138+ "checkConstraints": {},
1139+ "isRLSEnabled": false
1140+ },
1141+ "public.subject": {
1142+ "name": "subject",
1143+ "schema": "",
1144+ "columns": {
1145+ "external_id": {
1146+ "name": "external_id",
1147+ "type": "varchar(255)",
1148+ "primaryKey": false,
1149+ "notNull": true
1150+ },
1151+ "created_at": {
1152+ "name": "created_at",
1153+ "type": "timestamp",
1154+ "primaryKey": false,
1155+ "notNull": true
1156+ },
1157+ "last_seen_at": {
1158+ "name": "last_seen_at",
1159+ "type": "bigint",
1160+ "primaryKey": false,
1161+ "notNull": false
1162+ },
1163+ "status": {
1164+ "name": "status",
1165+ "type": "text",
1166+ "primaryKey": false,
1167+ "notNull": false
1168+ },
1169+ "row_id": {
1170+ "name": "row_id",
1171+ "type": "varchar(255)",
1172+ "primaryKey": true,
1173+ "notNull": true
1174+ },
1175+ "tenant": {
1176+ "name": "tenant",
1177+ "type": "varchar(255)",
1178+ "primaryKey": false,
1179+ "notNull": true
1180+ }
1181+ },
1182+ "indexes": {
1183+ "subject_uidx": {
1184+ "name": "subject_uidx",
1185+ "columns": [
1186+ {
1187+ "expression": "tenant",
1188+ "isExpression": false,
1189+ "asc": true,
1190+ "nulls": "last"
1191+ },
1192+ {
1193+ "expression": "external_id",
1194+ "isExpression": false,
1195+ "asc": true,
1196+ "nulls": "last"
1197+ }
1198+ ],
1199+ "isUnique": true,
1200+ "concurrently": false,
1201+ "method": "btree",
1202+ "with": {}
1203+ }
1204+ },
1205+ "foreignKeys": {},
1206+ "compositePrimaryKeys": {},
1207+ "uniqueConstraints": {},
1208+ "policies": {},
1209+ "checkConstraints": {},
1210+ "isRLSEnabled": false
1211+ },
1212+ "public.tool": {
1213+ "name": "tool",
1214+ "schema": "",
1215+ "columns": {
1216+ "integration": {
1217+ "name": "integration",
1218+ "type": "varchar(255)",
1219+ "primaryKey": false,
1220+ "notNull": true
1221+ },
1222+ "connection": {
1223+ "name": "connection",
1224+ "type": "varchar(255)",
1225+ "primaryKey": false,
1226+ "notNull": true
1227+ },
1228+ "plugin_id": {
1229+ "name": "plugin_id",
1230+ "type": "text",
1231+ "primaryKey": false,
1232+ "notNull": true
1233+ },
1234+ "name": {
1235+ "name": "name",
1236+ "type": "varchar(255)",
1237+ "primaryKey": false,
1238+ "notNull": true
1239+ },
1240+ "description": {
1241+ "name": "description",
1242+ "type": "text",
1243+ "primaryKey": false,
1244+ "notNull": true
1245+ },
1246+ "input_schema": {
1247+ "name": "input_schema",
1248+ "type": "json",
1249+ "primaryKey": false,
1250+ "notNull": false
1251+ },
1252+ "output_schema": {
1253+ "name": "output_schema",
1254+ "type": "json",
1255+ "primaryKey": false,
1256+ "notNull": false
1257+ },
1258+ "annotations": {
1259+ "name": "annotations",
1260+ "type": "json",
1261+ "primaryKey": false,
1262+ "notNull": false
1263+ },
1264+ "created_at": {
1265+ "name": "created_at",
1266+ "type": "timestamp",
1267+ "primaryKey": false,
1268+ "notNull": true
1269+ },
1270+ "updated_at": {
1271+ "name": "updated_at",
1272+ "type": "timestamp",
1273+ "primaryKey": false,
1274+ "notNull": true
1275+ },
1276+ "row_id": {
1277+ "name": "row_id",
1278+ "type": "varchar(255)",
1279+ "primaryKey": true,
1280+ "notNull": true
1281+ },
1282+ "tenant": {
1283+ "name": "tenant",
1284+ "type": "varchar(255)",
1285+ "primaryKey": false,
1286+ "notNull": true
1287+ },
1288+ "owner": {
1289+ "name": "owner",
1290+ "type": "varchar(255)",
1291+ "primaryKey": false,
1292+ "notNull": true
1293+ },
1294+ "subject": {
1295+ "name": "subject",
1296+ "type": "varchar(255)",
1297+ "primaryKey": false,
1298+ "notNull": true
1299+ }
1300+ },
1301+ "indexes": {
1302+ "tool_uidx": {
1303+ "name": "tool_uidx",
1304+ "columns": [
1305+ {
1306+ "expression": "tenant",
1307+ "isExpression": false,
1308+ "asc": true,
1309+ "nulls": "last"
1310+ },
1311+ {
1312+ "expression": "owner",
1313+ "isExpression": false,
1314+ "asc": true,
1315+ "nulls": "last"
1316+ },
1317+ {
1318+ "expression": "subject",
1319+ "isExpression": false,
1320+ "asc": true,
1321+ "nulls": "last"
1322+ },
1323+ {
1324+ "expression": "integration",
1325+ "isExpression": false,
1326+ "asc": true,
1327+ "nulls": "last"
1328+ },
1329+ {
1330+ "expression": "connection",
1331+ "isExpression": false,
1332+ "asc": true,
1333+ "nulls": "last"
1334+ },
1335+ {
1336+ "expression": "name",
1337+ "isExpression": false,
1338+ "asc": true,
1339+ "nulls": "last"
1340+ }
1341+ ],
1342+ "isUnique": true,
1343+ "concurrently": false,
1344+ "method": "btree",
1345+ "with": {}
1346+ }
1347+ },
1348+ "foreignKeys": {},
1349+ "compositePrimaryKeys": {},
1350+ "uniqueConstraints": {},
1351+ "policies": {},
1352+ "checkConstraints": {},
1353+ "isRLSEnabled": false
1354+ },
1355+ "public.tool_policy": {
1356+ "name": "tool_policy",
1357+ "schema": "",
1358+ "columns": {
1359+ "id": {
1360+ "name": "id",
1361+ "type": "varchar(255)",
1362+ "primaryKey": false,
1363+ "notNull": true
1364+ },
1365+ "pattern": {
1366+ "name": "pattern",
1367+ "type": "text",
1368+ "primaryKey": false,
1369+ "notNull": true
1370+ },
1371+ "action": {
1372+ "name": "action",
1373+ "type": "text",
1374+ "primaryKey": false,
1375+ "notNull": true
1376+ },
1377+ "position": {
1378+ "name": "position",
1379+ "type": "text",
1380+ "primaryKey": false,
1381+ "notNull": true
1382+ },
1383+ "created_at": {
1384+ "name": "created_at",
1385+ "type": "timestamp",
1386+ "primaryKey": false,
1387+ "notNull": true
1388+ },
1389+ "updated_at": {
1390+ "name": "updated_at",
1391+ "type": "timestamp",
1392+ "primaryKey": false,
1393+ "notNull": true
1394+ },
1395+ "row_id": {
1396+ "name": "row_id",
1397+ "type": "varchar(255)",
1398+ "primaryKey": true,
1399+ "notNull": true
1400+ },
1401+ "tenant": {
1402+ "name": "tenant",
1403+ "type": "varchar(255)",
1404+ "primaryKey": false,
1405+ "notNull": true
1406+ },
1407+ "owner": {
1408+ "name": "owner",
1409+ "type": "varchar(255)",
1410+ "primaryKey": false,
1411+ "notNull": true
1412+ },
1413+ "subject": {
1414+ "name": "subject",
1415+ "type": "varchar(255)",
1416+ "primaryKey": false,
1417+ "notNull": true
1418+ }
1419+ },
1420+ "indexes": {
1421+ "tool_policy_uidx": {
1422+ "name": "tool_policy_uidx",
1423+ "columns": [
1424+ {
1425+ "expression": "tenant",
1426+ "isExpression": false,
1427+ "asc": true,
1428+ "nulls": "last"
1429+ },
1430+ {
1431+ "expression": "owner",
1432+ "isExpression": false,
1433+ "asc": true,
1434+ "nulls": "last"
1435+ },
1436+ {
1437+ "expression": "subject",
1438+ "isExpression": false,
1439+ "asc": true,
1440+ "nulls": "last"
1441+ },
1442+ {
1443+ "expression": "id",
1444+ "isExpression": false,
1445+ "asc": true,
1446+ "nulls": "last"
1447+ }
1448+ ],
1449+ "isUnique": true,
1450+ "concurrently": false,
1451+ "method": "btree",
1452+ "with": {}
1453+ }
1454+ },
1455+ "foreignKeys": {},
1456+ "compositePrimaryKeys": {},
1457+ "uniqueConstraints": {},
1458+ "policies": {},
1459+ "checkConstraints": {},
1460+ "isRLSEnabled": false
1461+ }
1462+ },
1463+ "enums": {},
1464+ "schemas": {},
1465+ "sequences": {},
1466+ "roles": {},
1467+ "policies": {},
1468+ "views": {},
1469+ "_meta": {
1470+ "columns": {},
1471+ "schemas": {},
1472+ "tables": {}
1473+ }
1474+}
apps/cloud/drizzle/meta/0014_snapshot.json+1480−0
@@ -0,0 +1,1480 @@
1+{
2+ "id": "0857c83f-0124-4739-bd88-a0a4a92aab84",
3+ "prevId": "edc90067-822c-443a-8fcd-3bac77bca317",
4+ "version": "7",
5+ "dialect": "postgresql",
6+ "tables": {
7+ "public.accounts": {
8+ "name": "accounts",
9+ "schema": "",
10+ "columns": {
11+ "id": {
12+ "name": "id",
13+ "type": "text",
14+ "primaryKey": true,
15+ "notNull": true
16+ },
17+ "created_at": {
18+ "name": "created_at",
19+ "type": "timestamp with time zone",
20+ "primaryKey": false,
21+ "notNull": true,
22+ "default": "now()"
23+ }
24+ },
25+ "indexes": {},
26+ "foreignKeys": {},
27+ "compositePrimaryKeys": {},
28+ "uniqueConstraints": {},
29+ "policies": {},
30+ "checkConstraints": {},
31+ "isRLSEnabled": false
32+ },
33+ "public.memberships": {
34+ "name": "memberships",
35+ "schema": "",
36+ "columns": {
37+ "account_id": {
38+ "name": "account_id",
39+ "type": "text",
40+ "primaryKey": false,
41+ "notNull": true
42+ },
43+ "organization_id": {
44+ "name": "organization_id",
45+ "type": "text",
46+ "primaryKey": false,
47+ "notNull": true
48+ },
49+ "created_at": {
50+ "name": "created_at",
51+ "type": "timestamp with time zone",
52+ "primaryKey": false,
53+ "notNull": true,
54+ "default": "now()"
55+ }
56+ },
57+ "indexes": {},
58+ "foreignKeys": {
59+ "memberships_account_id_accounts_id_fk": {
60+ "name": "memberships_account_id_accounts_id_fk",
61+ "tableFrom": "memberships",
62+ "tableTo": "accounts",
63+ "columnsFrom": ["account_id"],
64+ "columnsTo": ["id"],
65+ "onDelete": "cascade",
66+ "onUpdate": "no action"
67+ },
68+ "memberships_organization_id_organizations_id_fk": {
69+ "name": "memberships_organization_id_organizations_id_fk",
70+ "tableFrom": "memberships",
71+ "tableTo": "organizations",
72+ "columnsFrom": ["organization_id"],
73+ "columnsTo": ["id"],
74+ "onDelete": "cascade",
75+ "onUpdate": "no action"
76+ }
77+ },
78+ "compositePrimaryKeys": {
79+ "memberships_account_id_organization_id_pk": {
80+ "name": "memberships_account_id_organization_id_pk",
81+ "columns": ["account_id", "organization_id"]
82+ }
83+ },
84+ "uniqueConstraints": {},
85+ "policies": {},
86+ "checkConstraints": {},
87+ "isRLSEnabled": false
88+ },
89+ "public.organizations": {
90+ "name": "organizations",
91+ "schema": "",
92+ "columns": {
93+ "id": {
94+ "name": "id",
95+ "type": "text",
96+ "primaryKey": true,
97+ "notNull": true
98+ },
99+ "name": {
100+ "name": "name",
101+ "type": "text",
102+ "primaryKey": false,
103+ "notNull": true
104+ },
105+ "slug": {
106+ "name": "slug",
107+ "type": "text",
108+ "primaryKey": false,
109+ "notNull": true
110+ },
111+ "created_at": {
112+ "name": "created_at",
113+ "type": "timestamp with time zone",
114+ "primaryKey": false,
115+ "notNull": true,
116+ "default": "now()"
117+ }
118+ },
119+ "indexes": {
120+ "organizations_slug_unique": {
121+ "name": "organizations_slug_unique",
122+ "columns": [
123+ {
124+ "expression": "slug",
125+ "isExpression": false,
126+ "asc": true,
127+ "nulls": "last"
128+ }
129+ ],
130+ "isUnique": true,
131+ "concurrently": false,
132+ "method": "btree",
133+ "with": {}
134+ }
135+ },
136+ "foreignKeys": {},
137+ "compositePrimaryKeys": {},
138+ "uniqueConstraints": {},
139+ "policies": {},
140+ "checkConstraints": {},
141+ "isRLSEnabled": false
142+ },
143+ "public.artifact": {
144+ "name": "artifact",
145+ "schema": "",
146+ "columns": {
147+ "id": {
148+ "name": "id",
149+ "type": "varchar(255)",
150+ "primaryKey": false,
151+ "notNull": true
152+ },
153+ "title": {
154+ "name": "title",
155+ "type": "text",
156+ "primaryKey": false,
157+ "notNull": true
158+ },
159+ "description": {
160+ "name": "description",
161+ "type": "text",
162+ "primaryKey": false,
163+ "notNull": false
164+ },
165+ "code": {
166+ "name": "code",
167+ "type": "text",
168+ "primaryKey": false,
169+ "notNull": true
170+ },
171+ "bindings": {
172+ "name": "bindings",
173+ "type": "json",
174+ "primaryKey": false,
175+ "notNull": false
176+ },
177+ "created_at": {
178+ "name": "created_at",
179+ "type": "timestamp",
180+ "primaryKey": false,
181+ "notNull": true
182+ },
183+ "updated_at": {
184+ "name": "updated_at",
185+ "type": "timestamp",
186+ "primaryKey": false,
187+ "notNull": true
188+ },
189+ "row_id": {
190+ "name": "row_id",
191+ "type": "varchar(255)",
192+ "primaryKey": true,
193+ "notNull": true
194+ },
195+ "tenant": {
196+ "name": "tenant",
197+ "type": "varchar(255)",
198+ "primaryKey": false,
199+ "notNull": true
200+ },
201+ "owner": {
202+ "name": "owner",
203+ "type": "varchar(255)",
204+ "primaryKey": false,
205+ "notNull": true
206+ },
207+ "subject": {
208+ "name": "subject",
209+ "type": "varchar(255)",
210+ "primaryKey": false,
211+ "notNull": true
212+ }
213+ },
214+ "indexes": {
215+ "artifact_uidx": {
216+ "name": "artifact_uidx",
217+ "columns": [
218+ {
219+ "expression": "tenant",
220+ "isExpression": false,
221+ "asc": true,
222+ "nulls": "last"
223+ },
224+ {
225+ "expression": "owner",
226+ "isExpression": false,
227+ "asc": true,
228+ "nulls": "last"
229+ },
230+ {
231+ "expression": "subject",
232+ "isExpression": false,
233+ "asc": true,
234+ "nulls": "last"
235+ },
236+ {
237+ "expression": "id",
238+ "isExpression": false,
239+ "asc": true,
240+ "nulls": "last"
241+ }
242+ ],
243+ "isUnique": true,
244+ "concurrently": false,
245+ "method": "btree",
246+ "with": {}
247+ }
248+ },
249+ "foreignKeys": {},
250+ "compositePrimaryKeys": {},
251+ "uniqueConstraints": {},
252+ "policies": {},
253+ "checkConstraints": {},
254+ "isRLSEnabled": false
255+ },
256+ "public.blob": {
257+ "name": "blob",
258+ "schema": "",
259+ "columns": {
260+ "namespace": {
261+ "name": "namespace",
262+ "type": "varchar(255)",
263+ "primaryKey": false,
264+ "notNull": true
265+ },
266+ "key": {
267+ "name": "key",
268+ "type": "varchar(255)",
269+ "primaryKey": false,
270+ "notNull": true
271+ },
272+ "value": {
273+ "name": "value",
274+ "type": "text",
275+ "primaryKey": false,
276+ "notNull": true
277+ },
278+ "row_id": {
279+ "name": "row_id",
280+ "type": "varchar(255)",
281+ "primaryKey": true,
282+ "notNull": true
283+ },
284+ "id": {
285+ "name": "id",
286+ "type": "varchar(255)",
287+ "primaryKey": false,
288+ "notNull": true
289+ }
290+ },
291+ "indexes": {
292+ "blob_id_uidx": {
293+ "name": "blob_id_uidx",
294+ "columns": [
295+ {
296+ "expression": "id",
297+ "isExpression": false,
298+ "asc": true,
299+ "nulls": "last"
300+ }
301+ ],
302+ "isUnique": true,
303+ "concurrently": false,
304+ "method": "btree",
305+ "with": {}
306+ }
307+ },
308+ "foreignKeys": {},
309+ "compositePrimaryKeys": {},
310+ "uniqueConstraints": {},
311+ "policies": {},
312+ "checkConstraints": {},
313+ "isRLSEnabled": false
314+ },
315+ "public.connection": {
316+ "name": "connection",
317+ "schema": "",
318+ "columns": {
319+ "integration": {
320+ "name": "integration",
321+ "type": "varchar(255)",
322+ "primaryKey": false,
323+ "notNull": true
324+ },
325+ "name": {
326+ "name": "name",
327+ "type": "varchar(255)",
328+ "primaryKey": false,
329+ "notNull": true
330+ },
331+ "template": {
332+ "name": "template",
333+ "type": "text",
334+ "primaryKey": false,
335+ "notNull": true
336+ },
337+ "provider": {
338+ "name": "provider",
339+ "type": "text",
340+ "primaryKey": false,
341+ "notNull": true
342+ },
343+ "item_ids": {
344+ "name": "item_ids",
345+ "type": "json",
346+ "primaryKey": false,
347+ "notNull": true
348+ },
349+ "identity_label": {
350+ "name": "identity_label",
351+ "type": "text",
352+ "primaryKey": false,
353+ "notNull": false
354+ },
355+ "description": {
356+ "name": "description",
357+ "type": "text",
358+ "primaryKey": false,
359+ "notNull": false
360+ },
361+ "last_health": {
362+ "name": "last_health",
363+ "type": "json",
364+ "primaryKey": false,
365+ "notNull": false
366+ },
367+ "tools_synced_at": {
368+ "name": "tools_synced_at",
369+ "type": "bigint",
370+ "primaryKey": false,
371+ "notNull": false
372+ },
373+ "oauth_client": {
374+ "name": "oauth_client",
375+ "type": "text",
376+ "primaryKey": false,
377+ "notNull": false
378+ },
379+ "oauth_client_owner": {
380+ "name": "oauth_client_owner",
381+ "type": "text",
382+ "primaryKey": false,
383+ "notNull": false
384+ },
385+ "refresh_item_id": {
386+ "name": "refresh_item_id",
387+ "type": "text",
388+ "primaryKey": false,
389+ "notNull": false
390+ },
391+ "expires_at": {
392+ "name": "expires_at",
393+ "type": "bigint",
394+ "primaryKey": false,
395+ "notNull": false
396+ },
397+ "oauth_scope": {
398+ "name": "oauth_scope",
399+ "type": "text",
400+ "primaryKey": false,
401+ "notNull": false
402+ },
403+ "oauth_token_url": {
404+ "name": "oauth_token_url",
405+ "type": "text",
406+ "primaryKey": false,
407+ "notNull": false
408+ },
409+ "provider_state": {
410+ "name": "provider_state",
411+ "type": "json",
412+ "primaryKey": false,
413+ "notNull": false
414+ },
415+ "created_at": {
416+ "name": "created_at",
417+ "type": "timestamp",
418+ "primaryKey": false,
419+ "notNull": true
420+ },
421+ "updated_at": {
422+ "name": "updated_at",
423+ "type": "timestamp",
424+ "primaryKey": false,
425+ "notNull": true
426+ },
427+ "row_id": {
428+ "name": "row_id",
429+ "type": "varchar(255)",
430+ "primaryKey": true,
431+ "notNull": true
432+ },
433+ "tenant": {
434+ "name": "tenant",
435+ "type": "varchar(255)",
436+ "primaryKey": false,
437+ "notNull": true
438+ },
439+ "owner": {
440+ "name": "owner",
441+ "type": "varchar(255)",
442+ "primaryKey": false,
443+ "notNull": true
444+ },
445+ "subject": {
446+ "name": "subject",
447+ "type": "varchar(255)",
448+ "primaryKey": false,
449+ "notNull": true
450+ }
451+ },
452+ "indexes": {
453+ "connection_uidx": {
454+ "name": "connection_uidx",
455+ "columns": [
456+ {
457+ "expression": "tenant",
458+ "isExpression": false,
459+ "asc": true,
460+ "nulls": "last"
461+ },
462+ {
463+ "expression": "owner",
464+ "isExpression": false,
465+ "asc": true,
466+ "nulls": "last"
467+ },
468+ {
469+ "expression": "subject",
470+ "isExpression": false,
471+ "asc": true,
472+ "nulls": "last"
473+ },
474+ {
475+ "expression": "integration",
476+ "isExpression": false,
477+ "asc": true,
478+ "nulls": "last"
479+ },
480+ {
481+ "expression": "name",
482+ "isExpression": false,
483+ "asc": true,
484+ "nulls": "last"
485+ }
486+ ],
487+ "isUnique": true,
488+ "concurrently": false,
489+ "method": "btree",
490+ "with": {}
491+ }
492+ },
493+ "foreignKeys": {},
494+ "compositePrimaryKeys": {},
495+ "uniqueConstraints": {},
496+ "policies": {},
497+ "checkConstraints": {},
498+ "isRLSEnabled": false
499+ },
500+ "public.definition": {
501+ "name": "definition",
502+ "schema": "",
503+ "columns": {
504+ "integration": {
505+ "name": "integration",
506+ "type": "varchar(255)",
507+ "primaryKey": false,
508+ "notNull": true
509+ },
510+ "connection": {
511+ "name": "connection",
512+ "type": "varchar(255)",
513+ "primaryKey": false,
514+ "notNull": true
515+ },
516+ "plugin_id": {
517+ "name": "plugin_id",
518+ "type": "text",
519+ "primaryKey": false,
520+ "notNull": true
521+ },
522+ "name": {
523+ "name": "name",
524+ "type": "text",
525+ "primaryKey": false,
526+ "notNull": true
527+ },
528+ "schema": {
529+ "name": "schema",
530+ "type": "json",
531+ "primaryKey": false,
532+ "notNull": true
533+ },
534+ "created_at": {
535+ "name": "created_at",
536+ "type": "timestamp",
537+ "primaryKey": false,
538+ "notNull": true
539+ },
540+ "row_id": {
541+ "name": "row_id",
542+ "type": "varchar(255)",
543+ "primaryKey": true,
544+ "notNull": true
545+ },
546+ "tenant": {
547+ "name": "tenant",
548+ "type": "varchar(255)",
549+ "primaryKey": false,
550+ "notNull": true
551+ },
552+ "owner": {
553+ "name": "owner",
554+ "type": "varchar(255)",
555+ "primaryKey": false,
556+ "notNull": true
557+ },
558+ "subject": {
559+ "name": "subject",
560+ "type": "varchar(255)",
561+ "primaryKey": false,
562+ "notNull": true
563+ }
564+ },
565+ "indexes": {
566+ "definition_uidx": {
567+ "name": "definition_uidx",
568+ "columns": [
569+ {
570+ "expression": "tenant",
571+ "isExpression": false,
572+ "asc": true,
573+ "nulls": "last"
574+ },
575+ {
576+ "expression": "owner",
577+ "isExpression": false,
578+ "asc": true,
579+ "nulls": "last"
580+ },
581+ {
582+ "expression": "subject",
583+ "isExpression": false,
584+ "asc": true,
585+ "nulls": "last"
586+ },
587+ {
588+ "expression": "integration",
589+ "isExpression": false,
590+ "asc": true,
591+ "nulls": "last"
592+ },
593+ {
594+ "expression": "connection",
595+ "isExpression": false,
596+ "asc": true,
597+ "nulls": "last"
598+ },
599+ {
600+ "expression": "name",
601+ "isExpression": false,
602+ "asc": true,
603+ "nulls": "last"
604+ }
605+ ],
606+ "isUnique": true,
607+ "concurrently": false,
608+ "method": "btree",
609+ "with": {}
610+ }
611+ },
612+ "foreignKeys": {},
613+ "compositePrimaryKeys": {},
614+ "uniqueConstraints": {},
615+ "policies": {},
616+ "checkConstraints": {},
617+ "isRLSEnabled": false
618+ },
619+ "public.integration": {
620+ "name": "integration",
621+ "schema": "",
622+ "columns": {
623+ "slug": {
624+ "name": "slug",
625+ "type": "varchar(255)",
626+ "primaryKey": false,
627+ "notNull": true
628+ },
629+ "plugin_id": {
630+ "name": "plugin_id",
631+ "type": "text",
632+ "primaryKey": false,
633+ "notNull": true
634+ },
635+ "name": {
636+ "name": "name",
637+ "type": "text",
638+ "primaryKey": false,
639+ "notNull": false
640+ },
641+ "description": {
642+ "name": "description",
643+ "type": "text",
644+ "primaryKey": false,
645+ "notNull": false
646+ },
647+ "config": {
648+ "name": "config",
649+ "type": "json",
650+ "primaryKey": false,
651+ "notNull": false
652+ },
653+ "health_check": {
654+ "name": "health_check",
655+ "type": "json",
656+ "primaryKey": false,
657+ "notNull": false
658+ },
659+ "config_revised_at": {
660+ "name": "config_revised_at",
661+ "type": "bigint",
662+ "primaryKey": false,
663+ "notNull": false
664+ },
665+ "can_remove": {
666+ "name": "can_remove",
667+ "type": "boolean",
668+ "primaryKey": false,
669+ "notNull": true,
670+ "default": true
671+ },
672+ "can_refresh": {
673+ "name": "can_refresh",
674+ "type": "boolean",
675+ "primaryKey": false,
676+ "notNull": true,
677+ "default": false
678+ },
679+ "created_at": {
680+ "name": "created_at",
681+ "type": "timestamp",
682+ "primaryKey": false,
683+ "notNull": true
684+ },
685+ "updated_at": {
686+ "name": "updated_at",
687+ "type": "timestamp",
688+ "primaryKey": false,
689+ "notNull": true
690+ },
691+ "row_id": {
692+ "name": "row_id",
693+ "type": "varchar(255)",
694+ "primaryKey": true,
695+ "notNull": true
696+ },
697+ "tenant": {
698+ "name": "tenant",
699+ "type": "varchar(255)",
700+ "primaryKey": false,
701+ "notNull": true
702+ }
703+ },
704+ "indexes": {
705+ "integration_uidx": {
706+ "name": "integration_uidx",
707+ "columns": [
708+ {
709+ "expression": "tenant",
710+ "isExpression": false,
711+ "asc": true,
712+ "nulls": "last"
713+ },
714+ {
715+ "expression": "slug",
716+ "isExpression": false,
717+ "asc": true,
718+ "nulls": "last"
719+ }
720+ ],
721+ "isUnique": true,
722+ "concurrently": false,
723+ "method": "btree",
724+ "with": {}
725+ }
726+ },
727+ "foreignKeys": {},
728+ "compositePrimaryKeys": {},
729+ "uniqueConstraints": {},
730+ "policies": {},
731+ "checkConstraints": {},
732+ "isRLSEnabled": false
733+ },
734+ "public.oauth_client": {
735+ "name": "oauth_client",
736+ "schema": "",
737+ "columns": {
738+ "slug": {
739+ "name": "slug",
740+ "type": "varchar(255)",
741+ "primaryKey": false,
742+ "notNull": true
743+ },
744+ "authorization_url": {
745+ "name": "authorization_url",
746+ "type": "text",
747+ "primaryKey": false,
748+ "notNull": true
749+ },
750+ "token_url": {
751+ "name": "token_url",
752+ "type": "text",
753+ "primaryKey": false,
754+ "notNull": true
755+ },
756+ "grant": {
757+ "name": "grant",
758+ "type": "text",
759+ "primaryKey": false,
760+ "notNull": true
761+ },
762+ "client_id": {
763+ "name": "client_id",
764+ "type": "text",
765+ "primaryKey": false,
766+ "notNull": true
767+ },
768+ "client_secret_item_id": {
769+ "name": "client_secret_item_id",
770+ "type": "text",
771+ "primaryKey": false,
772+ "notNull": false
773+ },
774+ "resource": {
775+ "name": "resource",
776+ "type": "text",
777+ "primaryKey": false,
778+ "notNull": false
779+ },
780+ "origin_kind": {
781+ "name": "origin_kind",
782+ "type": "text",
783+ "primaryKey": false,
784+ "notNull": false
785+ },
786+ "origin_integration": {
787+ "name": "origin_integration",
788+ "type": "text",
789+ "primaryKey": false,
790+ "notNull": false
791+ },
792+ "origin_issuer": {
793+ "name": "origin_issuer",
794+ "type": "text",
795+ "primaryKey": false,
796+ "notNull": false
797+ },
798+ "origin_redirect_uri": {
799+ "name": "origin_redirect_uri",
800+ "type": "text",
801+ "primaryKey": false,
802+ "notNull": false
803+ },
804+ "created_at": {
805+ "name": "created_at",
806+ "type": "timestamp",
807+ "primaryKey": false,
808+ "notNull": true
809+ },
810+ "row_id": {
811+ "name": "row_id",
812+ "type": "varchar(255)",
813+ "primaryKey": true,
814+ "notNull": true
815+ },
816+ "tenant": {
817+ "name": "tenant",
818+ "type": "varchar(255)",
819+ "primaryKey": false,
820+ "notNull": true
821+ },
822+ "owner": {
823+ "name": "owner",
824+ "type": "varchar(255)",
825+ "primaryKey": false,
826+ "notNull": true
827+ },
828+ "subject": {
829+ "name": "subject",
830+ "type": "varchar(255)",
831+ "primaryKey": false,
832+ "notNull": true
833+ }
834+ },
835+ "indexes": {
836+ "oauth_client_uidx": {
837+ "name": "oauth_client_uidx",
838+ "columns": [
839+ {
840+ "expression": "tenant",
841+ "isExpression": false,
842+ "asc": true,
843+ "nulls": "last"
844+ },
845+ {
846+ "expression": "owner",
847+ "isExpression": false,
848+ "asc": true,
849+ "nulls": "last"
850+ },
851+ {
852+ "expression": "subject",
853+ "isExpression": false,
854+ "asc": true,
855+ "nulls": "last"
856+ },
857+ {
858+ "expression": "slug",
859+ "isExpression": false,
860+ "asc": true,
861+ "nulls": "last"
862+ }
863+ ],
864+ "isUnique": true,
865+ "concurrently": false,
866+ "method": "btree",
867+ "with": {}
868+ }
869+ },
870+ "foreignKeys": {},
871+ "compositePrimaryKeys": {},
872+ "uniqueConstraints": {},
873+ "policies": {},
874+ "checkConstraints": {},
875+ "isRLSEnabled": false
876+ },
877+ "public.oauth_session": {
878+ "name": "oauth_session",
879+ "schema": "",
880+ "columns": {
881+ "state": {
882+ "name": "state",
883+ "type": "varchar(255)",
884+ "primaryKey": false,
885+ "notNull": true
886+ },
887+ "client_slug": {
888+ "name": "client_slug",
889+ "type": "text",
890+ "primaryKey": false,
891+ "notNull": true
892+ },
893+ "integration": {
894+ "name": "integration",
895+ "type": "text",
896+ "primaryKey": false,
897+ "notNull": true
898+ },
899+ "name": {
900+ "name": "name",
901+ "type": "text",
902+ "primaryKey": false,
903+ "notNull": true
904+ },
905+ "template": {
906+ "name": "template",
907+ "type": "text",
908+ "primaryKey": false,
909+ "notNull": true
910+ },
911+ "redirect_url": {
912+ "name": "redirect_url",
913+ "type": "text",
914+ "primaryKey": false,
915+ "notNull": true
916+ },
917+ "pkce_verifier": {
918+ "name": "pkce_verifier",
919+ "type": "text",
920+ "primaryKey": false,
921+ "notNull": false
922+ },
923+ "identity_label": {
924+ "name": "identity_label",
925+ "type": "text",
926+ "primaryKey": false,
927+ "notNull": false
928+ },
929+ "payload": {
930+ "name": "payload",
931+ "type": "json",
932+ "primaryKey": false,
933+ "notNull": true
934+ },
935+ "expires_at": {
936+ "name": "expires_at",
937+ "type": "bigint",
938+ "primaryKey": false,
939+ "notNull": true
940+ },
941+ "created_at": {
942+ "name": "created_at",
943+ "type": "timestamp",
944+ "primaryKey": false,
945+ "notNull": true
946+ },
947+ "row_id": {
948+ "name": "row_id",
949+ "type": "varchar(255)",
950+ "primaryKey": true,
951+ "notNull": true
952+ },
953+ "tenant": {
954+ "name": "tenant",
955+ "type": "varchar(255)",
956+ "primaryKey": false,
957+ "notNull": true
958+ },
959+ "owner": {
960+ "name": "owner",
961+ "type": "varchar(255)",
962+ "primaryKey": false,
963+ "notNull": true
964+ },
965+ "subject": {
966+ "name": "subject",
967+ "type": "varchar(255)",
968+ "primaryKey": false,
969+ "notNull": true
970+ }
971+ },
972+ "indexes": {
973+ "oauth_session_uidx": {
974+ "name": "oauth_session_uidx",
975+ "columns": [
976+ {
977+ "expression": "tenant",
978+ "isExpression": false,
979+ "asc": true,
980+ "nulls": "last"
981+ },
982+ {
983+ "expression": "state",
984+ "isExpression": false,
985+ "asc": true,
986+ "nulls": "last"
987+ }
988+ ],
989+ "isUnique": true,
990+ "concurrently": false,
991+ "method": "btree",
992+ "with": {}
993+ }
994+ },
995+ "foreignKeys": {},
996+ "compositePrimaryKeys": {},
997+ "uniqueConstraints": {},
998+ "policies": {},
999+ "checkConstraints": {},
1000+ "isRLSEnabled": false
1001+ },
1002+ "public.plugin_storage": {
1003+ "name": "plugin_storage",
1004+ "schema": "",
1005+ "columns": {
1006+ "plugin_id": {
1007+ "name": "plugin_id",
1008+ "type": "varchar(255)",
1009+ "primaryKey": false,
1010+ "notNull": true
1011+ },
1012+ "collection": {
1013+ "name": "collection",
1014+ "type": "varchar(255)",
1015+ "primaryKey": false,
1016+ "notNull": true
1017+ },
1018+ "key": {
1019+ "name": "key",
1020+ "type": "varchar(255)",
1021+ "primaryKey": false,
1022+ "notNull": true
1023+ },
1024+ "data": {
1025+ "name": "data",
1026+ "type": "json",
1027+ "primaryKey": false,
1028+ "notNull": true
1029+ },
1030+ "created_at": {
1031+ "name": "created_at",
1032+ "type": "timestamp",
1033+ "primaryKey": false,
1034+ "notNull": true
1035+ },
1036+ "updated_at": {
1037+ "name": "updated_at",
1038+ "type": "timestamp",
1039+ "primaryKey": false,
1040+ "notNull": true
1041+ },
1042+ "row_id": {
1043+ "name": "row_id",
1044+ "type": "varchar(255)",
1045+ "primaryKey": true,
1046+ "notNull": true
1047+ },
1048+ "tenant": {
1049+ "name": "tenant",
1050+ "type": "varchar(255)",
1051+ "primaryKey": false,
1052+ "notNull": true
1053+ },
1054+ "owner": {
1055+ "name": "owner",
1056+ "type": "varchar(255)",
1057+ "primaryKey": false,
1058+ "notNull": true
1059+ },
1060+ "subject": {
1061+ "name": "subject",
1062+ "type": "varchar(255)",
1063+ "primaryKey": false,
1064+ "notNull": true
1065+ }
1066+ },
1067+ "indexes": {
1068+ "plugin_storage_uidx": {
1069+ "name": "plugin_storage_uidx",
1070+ "columns": [
1071+ {
1072+ "expression": "tenant",
1073+ "isExpression": false,
1074+ "asc": true,
1075+ "nulls": "last"
1076+ },
1077+ {
1078+ "expression": "owner",
1079+ "isExpression": false,
1080+ "asc": true,
1081+ "nulls": "last"
1082+ },
1083+ {
1084+ "expression": "subject",
1085+ "isExpression": false,
1086+ "asc": true,
1087+ "nulls": "last"
1088+ },
1089+ {
1090+ "expression": "plugin_id",
1091+ "isExpression": false,
1092+ "asc": true,
1093+ "nulls": "last"
1094+ },
1095+ {
1096+ "expression": "collection",
1097+ "isExpression": false,
1098+ "asc": true,
1099+ "nulls": "last"
1100+ },
1101+ {
1102+ "expression": "key",
1103+ "isExpression": false,
1104+ "asc": true,
1105+ "nulls": "last"
1106+ }
1107+ ],
1108+ "isUnique": true,
1109+ "concurrently": false,
1110+ "method": "btree",
1111+ "with": {}
1112+ }
1113+ },
1114+ "foreignKeys": {},
1115+ "compositePrimaryKeys": {},
1116+ "uniqueConstraints": {},
1117+ "policies": {},
1118+ "checkConstraints": {},
1119+ "isRLSEnabled": false
1120+ },
1121+ "public.private_executor_cloud_settings": {
1122+ "name": "private_executor_cloud_settings",
1123+ "schema": "",
1124+ "columns": {
1125+ "id": {
1126+ "name": "id",
1127+ "type": "varchar(255)",
1128+ "primaryKey": true,
1129+ "notNull": true
1130+ },
1131+ "version": {
1132+ "name": "version",
1133+ "type": "varchar(255)",
1134+ "primaryKey": false,
1135+ "notNull": true,
1136+ "default": "'1.0.0'"
1137+ }
1138+ },
1139+ "indexes": {},
1140+ "foreignKeys": {},
1141+ "compositePrimaryKeys": {},
1142+ "uniqueConstraints": {},
1143+ "policies": {},
1144+ "checkConstraints": {},
1145+ "isRLSEnabled": false
1146+ },
1147+ "public.subject": {
1148+ "name": "subject",
1149+ "schema": "",
1150+ "columns": {
1151+ "external_id": {
1152+ "name": "external_id",
1153+ "type": "varchar(255)",
1154+ "primaryKey": false,
1155+ "notNull": true
1156+ },
1157+ "created_at": {
1158+ "name": "created_at",
1159+ "type": "timestamp",
1160+ "primaryKey": false,
1161+ "notNull": true
1162+ },
1163+ "last_seen_at": {
1164+ "name": "last_seen_at",
1165+ "type": "bigint",
1166+ "primaryKey": false,
1167+ "notNull": false
1168+ },
1169+ "status": {
1170+ "name": "status",
1171+ "type": "text",
1172+ "primaryKey": false,
1173+ "notNull": false
1174+ },
1175+ "row_id": {
1176+ "name": "row_id",
1177+ "type": "varchar(255)",
1178+ "primaryKey": true,
1179+ "notNull": true
1180+ },
1181+ "tenant": {
1182+ "name": "tenant",
1183+ "type": "varchar(255)",
1184+ "primaryKey": false,
1185+ "notNull": true
1186+ }
1187+ },
1188+ "indexes": {
1189+ "subject_uidx": {
1190+ "name": "subject_uidx",
1191+ "columns": [
1192+ {
1193+ "expression": "tenant",
1194+ "isExpression": false,
1195+ "asc": true,
1196+ "nulls": "last"
1197+ },
1198+ {
1199+ "expression": "external_id",
1200+ "isExpression": false,
1201+ "asc": true,
1202+ "nulls": "last"
1203+ }
1204+ ],
1205+ "isUnique": true,
1206+ "concurrently": false,
1207+ "method": "btree",
1208+ "with": {}
1209+ }
1210+ },
1211+ "foreignKeys": {},
1212+ "compositePrimaryKeys": {},
1213+ "uniqueConstraints": {},
1214+ "policies": {},
1215+ "checkConstraints": {},
1216+ "isRLSEnabled": false
1217+ },
1218+ "public.tool": {
1219+ "name": "tool",
1220+ "schema": "",
1221+ "columns": {
1222+ "integration": {
1223+ "name": "integration",
1224+ "type": "varchar(255)",
1225+ "primaryKey": false,
1226+ "notNull": true
1227+ },
1228+ "connection": {
1229+ "name": "connection",
1230+ "type": "varchar(255)",
1231+ "primaryKey": false,
1232+ "notNull": true
1233+ },
1234+ "plugin_id": {
1235+ "name": "plugin_id",
1236+ "type": "text",
1237+ "primaryKey": false,
1238+ "notNull": true
1239+ },
1240+ "name": {
1241+ "name": "name",
1242+ "type": "varchar(255)",
1243+ "primaryKey": false,
1244+ "notNull": true
1245+ },
1246+ "description": {
1247+ "name": "description",
1248+ "type": "text",
1249+ "primaryKey": false,
1250+ "notNull": true
1251+ },
1252+ "input_schema": {
1253+ "name": "input_schema",
1254+ "type": "json",
1255+ "primaryKey": false,
1256+ "notNull": false
1257+ },
1258+ "output_schema": {
1259+ "name": "output_schema",
1260+ "type": "json",
1261+ "primaryKey": false,
1262+ "notNull": false
1263+ },
1264+ "annotations": {
1265+ "name": "annotations",
1266+ "type": "json",
1267+ "primaryKey": false,
1268+ "notNull": false
1269+ },
1270+ "created_at": {
1271+ "name": "created_at",
1272+ "type": "timestamp",
1273+ "primaryKey": false,
1274+ "notNull": true
1275+ },
1276+ "updated_at": {
1277+ "name": "updated_at",
1278+ "type": "timestamp",
1279+ "primaryKey": false,
1280+ "notNull": true
1281+ },
1282+ "row_id": {
1283+ "name": "row_id",
1284+ "type": "varchar(255)",
1285+ "primaryKey": true,
1286+ "notNull": true
1287+ },
1288+ "tenant": {
1289+ "name": "tenant",
1290+ "type": "varchar(255)",
1291+ "primaryKey": false,
1292+ "notNull": true
1293+ },
1294+ "owner": {
1295+ "name": "owner",
1296+ "type": "varchar(255)",
1297+ "primaryKey": false,
1298+ "notNull": true
1299+ },
1300+ "subject": {
1301+ "name": "subject",
1302+ "type": "varchar(255)",
1303+ "primaryKey": false,
1304+ "notNull": true
1305+ }
1306+ },
1307+ "indexes": {
1308+ "tool_uidx": {
1309+ "name": "tool_uidx",
1310+ "columns": [
1311+ {
1312+ "expression": "tenant",
1313+ "isExpression": false,
1314+ "asc": true,
1315+ "nulls": "last"
1316+ },
1317+ {
1318+ "expression": "owner",
1319+ "isExpression": false,
1320+ "asc": true,
1321+ "nulls": "last"
1322+ },
1323+ {
1324+ "expression": "subject",
1325+ "isExpression": false,
1326+ "asc": true,
1327+ "nulls": "last"
1328+ },
1329+ {
1330+ "expression": "integration",
1331+ "isExpression": false,
1332+ "asc": true,
1333+ "nulls": "last"
1334+ },
1335+ {
1336+ "expression": "connection",
1337+ "isExpression": false,
1338+ "asc": true,
1339+ "nulls": "last"
1340+ },
1341+ {
1342+ "expression": "name",
1343+ "isExpression": false,
1344+ "asc": true,
1345+ "nulls": "last"
1346+ }
1347+ ],
1348+ "isUnique": true,
1349+ "concurrently": false,
1350+ "method": "btree",
1351+ "with": {}
1352+ }
1353+ },
1354+ "foreignKeys": {},
1355+ "compositePrimaryKeys": {},
1356+ "uniqueConstraints": {},
1357+ "policies": {},
1358+ "checkConstraints": {},
1359+ "isRLSEnabled": false
1360+ },
1361+ "public.tool_policy": {
1362+ "name": "tool_policy",
1363+ "schema": "",
1364+ "columns": {
1365+ "id": {
1366+ "name": "id",
1367+ "type": "varchar(255)",
1368+ "primaryKey": false,
1369+ "notNull": true
1370+ },
1371+ "pattern": {
1372+ "name": "pattern",
1373+ "type": "text",
1374+ "primaryKey": false,
1375+ "notNull": true
1376+ },
1377+ "action": {
1378+ "name": "action",
1379+ "type": "text",
1380+ "primaryKey": false,
1381+ "notNull": true
1382+ },
1383+ "position": {
1384+ "name": "position",
1385+ "type": "text",
1386+ "primaryKey": false,
1387+ "notNull": true
1388+ },
1389+ "created_at": {
1390+ "name": "created_at",
1391+ "type": "timestamp",
1392+ "primaryKey": false,
1393+ "notNull": true
1394+ },
1395+ "updated_at": {
1396+ "name": "updated_at",
1397+ "type": "timestamp",
1398+ "primaryKey": false,
1399+ "notNull": true
1400+ },
1401+ "row_id": {
1402+ "name": "row_id",
1403+ "type": "varchar(255)",
1404+ "primaryKey": true,
1405+ "notNull": true
1406+ },
1407+ "tenant": {
1408+ "name": "tenant",
1409+ "type": "varchar(255)",
1410+ "primaryKey": false,
1411+ "notNull": true
1412+ },
1413+ "owner": {
1414+ "name": "owner",
1415+ "type": "varchar(255)",
1416+ "primaryKey": false,
1417+ "notNull": true
1418+ },
1419+ "subject": {
1420+ "name": "subject",
1421+ "type": "varchar(255)",
1422+ "primaryKey": false,
1423+ "notNull": true
1424+ }
1425+ },
1426+ "indexes": {
1427+ "tool_policy_uidx": {
1428+ "name": "tool_policy_uidx",
1429+ "columns": [
1430+ {
1431+ "expression": "tenant",
1432+ "isExpression": false,
1433+ "asc": true,
1434+ "nulls": "last"
1435+ },
1436+ {
1437+ "expression": "owner",
1438+ "isExpression": false,
1439+ "asc": true,
1440+ "nulls": "last"
1441+ },
1442+ {
1443+ "expression": "subject",
1444+ "isExpression": false,
1445+ "asc": true,
1446+ "nulls": "last"
1447+ },
1448+ {
1449+ "expression": "id",
1450+ "isExpression": false,
1451+ "asc": true,
1452+ "nulls": "last"
1453+ }
1454+ ],
1455+ "isUnique": true,
1456+ "concurrently": false,
1457+ "method": "btree",
1458+ "with": {}
1459+ }
1460+ },
1461+ "foreignKeys": {},
1462+ "compositePrimaryKeys": {},
1463+ "uniqueConstraints": {},
1464+ "policies": {},
1465+ "checkConstraints": {},
1466+ "isRLSEnabled": false
1467+ }
1468+ },
1469+ "enums": {},
1470+ "schemas": {},
1471+ "sequences": {},
1472+ "roles": {},
1473+ "policies": {},
1474+ "views": {},
1475+ "_meta": {
1476+ "columns": {},
1477+ "schemas": {},
1478+ "tables": {}
1479+ }
1480+}
apps/cloud/drizzle/meta/0015_snapshot.json+1486−0
@@ -0,0 +1,1486 @@
1+{
2+ "id": "d666b31a-c3d1-4bd7-9bd6-85f2abc4fb55",
3+ "prevId": "0857c83f-0124-4739-bd88-a0a4a92aab84",
4+ "version": "7",
5+ "dialect": "postgresql",
6+ "tables": {
7+ "public.accounts": {
8+ "name": "accounts",
9+ "schema": "",
10+ "columns": {
11+ "id": {
12+ "name": "id",
13+ "type": "text",
14+ "primaryKey": true,
15+ "notNull": true
16+ },
17+ "created_at": {
18+ "name": "created_at",
19+ "type": "timestamp with time zone",
20+ "primaryKey": false,
21+ "notNull": true,
22+ "default": "now()"
23+ }
24+ },
25+ "indexes": {},
26+ "foreignKeys": {},
27+ "compositePrimaryKeys": {},
28+ "uniqueConstraints": {},
29+ "policies": {},
30+ "checkConstraints": {},
31+ "isRLSEnabled": false
32+ },
33+ "public.memberships": {
34+ "name": "memberships",
35+ "schema": "",
36+ "columns": {
37+ "account_id": {
38+ "name": "account_id",
39+ "type": "text",
40+ "primaryKey": false,
41+ "notNull": true
42+ },
43+ "organization_id": {
44+ "name": "organization_id",
45+ "type": "text",
46+ "primaryKey": false,
47+ "notNull": true
48+ },
49+ "created_at": {
50+ "name": "created_at",
51+ "type": "timestamp with time zone",
52+ "primaryKey": false,
53+ "notNull": true,
54+ "default": "now()"
55+ }
56+ },
57+ "indexes": {},
58+ "foreignKeys": {
59+ "memberships_account_id_accounts_id_fk": {
60+ "name": "memberships_account_id_accounts_id_fk",
61+ "tableFrom": "memberships",
62+ "tableTo": "accounts",
63+ "columnsFrom": ["account_id"],
64+ "columnsTo": ["id"],
65+ "onDelete": "cascade",
66+ "onUpdate": "no action"
67+ },
68+ "memberships_organization_id_organizations_id_fk": {
69+ "name": "memberships_organization_id_organizations_id_fk",
70+ "tableFrom": "memberships",
71+ "tableTo": "organizations",
72+ "columnsFrom": ["organization_id"],
73+ "columnsTo": ["id"],
74+ "onDelete": "cascade",
75+ "onUpdate": "no action"
76+ }
77+ },
78+ "compositePrimaryKeys": {
79+ "memberships_account_id_organization_id_pk": {
80+ "name": "memberships_account_id_organization_id_pk",
81+ "columns": ["account_id", "organization_id"]
82+ }
83+ },
84+ "uniqueConstraints": {},
85+ "policies": {},
86+ "checkConstraints": {},
87+ "isRLSEnabled": false
88+ },
89+ "public.organizations": {
90+ "name": "organizations",
91+ "schema": "",
92+ "columns": {
93+ "id": {
94+ "name": "id",
95+ "type": "text",
96+ "primaryKey": true,
97+ "notNull": true
98+ },
99+ "name": {
100+ "name": "name",
101+ "type": "text",
102+ "primaryKey": false,
103+ "notNull": true
104+ },
105+ "slug": {
106+ "name": "slug",
107+ "type": "text",
108+ "primaryKey": false,
109+ "notNull": true
110+ },
111+ "created_at": {
112+ "name": "created_at",
113+ "type": "timestamp with time zone",
114+ "primaryKey": false,
115+ "notNull": true,
116+ "default": "now()"
117+ }
118+ },
119+ "indexes": {
120+ "organizations_slug_unique": {
121+ "name": "organizations_slug_unique",
122+ "columns": [
123+ {
124+ "expression": "slug",
125+ "isExpression": false,
126+ "asc": true,
127+ "nulls": "last"
128+ }
129+ ],
130+ "isUnique": true,
131+ "concurrently": false,
132+ "method": "btree",
133+ "with": {}
134+ }
135+ },
136+ "foreignKeys": {},
137+ "compositePrimaryKeys": {},
138+ "uniqueConstraints": {},
139+ "policies": {},
140+ "checkConstraints": {},
141+ "isRLSEnabled": false
142+ },
143+ "public.artifact": {
144+ "name": "artifact",
145+ "schema": "",
146+ "columns": {
147+ "id": {
148+ "name": "id",
149+ "type": "varchar(255)",
150+ "primaryKey": false,
151+ "notNull": true
152+ },
153+ "title": {
154+ "name": "title",
155+ "type": "text",
156+ "primaryKey": false,
157+ "notNull": true
158+ },
159+ "description": {
160+ "name": "description",
161+ "type": "text",
162+ "primaryKey": false,
163+ "notNull": false
164+ },
165+ "code": {
166+ "name": "code",
167+ "type": "text",
168+ "primaryKey": false,
169+ "notNull": true
170+ },
171+ "bindings": {
172+ "name": "bindings",
173+ "type": "json",
174+ "primaryKey": false,
175+ "notNull": false
176+ },
177+ "preview": {
178+ "name": "preview",
179+ "type": "text",
180+ "primaryKey": false,
181+ "notNull": false
182+ },
183+ "created_at": {
184+ "name": "created_at",
185+ "type": "timestamp",
186+ "primaryKey": false,
187+ "notNull": true
188+ },
189+ "updated_at": {
190+ "name": "updated_at",
191+ "type": "timestamp",
192+ "primaryKey": false,
193+ "notNull": true
194+ },
195+ "row_id": {
196+ "name": "row_id",
197+ "type": "varchar(255)",
198+ "primaryKey": true,
199+ "notNull": true
200+ },
201+ "tenant": {
202+ "name": "tenant",
203+ "type": "varchar(255)",
204+ "primaryKey": false,
205+ "notNull": true
206+ },
207+ "owner": {
208+ "name": "owner",
209+ "type": "varchar(255)",
210+ "primaryKey": false,
211+ "notNull": true
212+ },
213+ "subject": {
214+ "name": "subject",
215+ "type": "varchar(255)",
216+ "primaryKey": false,
217+ "notNull": true
218+ }
219+ },
220+ "indexes": {
221+ "artifact_uidx": {
222+ "name": "artifact_uidx",
223+ "columns": [
224+ {
225+ "expression": "tenant",
226+ "isExpression": false,
227+ "asc": true,
228+ "nulls": "last"
229+ },
230+ {
231+ "expression": "owner",
232+ "isExpression": false,
233+ "asc": true,
234+ "nulls": "last"
235+ },
236+ {
237+ "expression": "subject",
238+ "isExpression": false,
239+ "asc": true,
240+ "nulls": "last"
241+ },
242+ {
243+ "expression": "id",
244+ "isExpression": false,
245+ "asc": true,
246+ "nulls": "last"
247+ }
248+ ],
249+ "isUnique": true,
250+ "concurrently": false,
251+ "method": "btree",
252+ "with": {}
253+ }
254+ },
255+ "foreignKeys": {},
256+ "compositePrimaryKeys": {},
257+ "uniqueConstraints": {},
258+ "policies": {},
259+ "checkConstraints": {},
260+ "isRLSEnabled": false
261+ },
262+ "public.blob": {
263+ "name": "blob",
264+ "schema": "",
265+ "columns": {
266+ "namespace": {
267+ "name": "namespace",
268+ "type": "varchar(255)",
269+ "primaryKey": false,
270+ "notNull": true
271+ },
272+ "key": {
273+ "name": "key",
274+ "type": "varchar(255)",
275+ "primaryKey": false,
276+ "notNull": true
277+ },
278+ "value": {
279+ "name": "value",
280+ "type": "text",
281+ "primaryKey": false,
282+ "notNull": true
283+ },
284+ "row_id": {
285+ "name": "row_id",
286+ "type": "varchar(255)",
287+ "primaryKey": true,
288+ "notNull": true
289+ },
290+ "id": {
291+ "name": "id",
292+ "type": "varchar(255)",
293+ "primaryKey": false,
294+ "notNull": true
295+ }
296+ },
297+ "indexes": {
298+ "blob_id_uidx": {
299+ "name": "blob_id_uidx",
300+ "columns": [
301+ {
302+ "expression": "id",
303+ "isExpression": false,
304+ "asc": true,
305+ "nulls": "last"
306+ }
307+ ],
308+ "isUnique": true,
309+ "concurrently": false,
310+ "method": "btree",
311+ "with": {}
312+ }
313+ },
314+ "foreignKeys": {},
315+ "compositePrimaryKeys": {},
316+ "uniqueConstraints": {},
317+ "policies": {},
318+ "checkConstraints": {},
319+ "isRLSEnabled": false
320+ },
321+ "public.connection": {
322+ "name": "connection",
323+ "schema": "",
324+ "columns": {
325+ "integration": {
326+ "name": "integration",
327+ "type": "varchar(255)",
328+ "primaryKey": false,
329+ "notNull": true
330+ },
331+ "name": {
332+ "name": "name",
333+ "type": "varchar(255)",
334+ "primaryKey": false,
335+ "notNull": true
336+ },
337+ "template": {
338+ "name": "template",
339+ "type": "text",
340+ "primaryKey": false,
341+ "notNull": true
342+ },
343+ "provider": {
344+ "name": "provider",
345+ "type": "text",
346+ "primaryKey": false,
347+ "notNull": true
348+ },
349+ "item_ids": {
350+ "name": "item_ids",
351+ "type": "json",
352+ "primaryKey": false,
353+ "notNull": true
354+ },
355+ "identity_label": {
356+ "name": "identity_label",
357+ "type": "text",
358+ "primaryKey": false,
359+ "notNull": false
360+ },
361+ "description": {
362+ "name": "description",
363+ "type": "text",
364+ "primaryKey": false,
365+ "notNull": false
366+ },
367+ "last_health": {
368+ "name": "last_health",
369+ "type": "json",
370+ "primaryKey": false,
371+ "notNull": false
372+ },
373+ "tools_synced_at": {
374+ "name": "tools_synced_at",
375+ "type": "bigint",
376+ "primaryKey": false,
377+ "notNull": false
378+ },
379+ "oauth_client": {
380+ "name": "oauth_client",
381+ "type": "text",
382+ "primaryKey": false,
383+ "notNull": false
384+ },
385+ "oauth_client_owner": {
386+ "name": "oauth_client_owner",
387+ "type": "text",
388+ "primaryKey": false,
389+ "notNull": false
390+ },
391+ "refresh_item_id": {
392+ "name": "refresh_item_id",
393+ "type": "text",
394+ "primaryKey": false,
395+ "notNull": false
396+ },
397+ "expires_at": {
398+ "name": "expires_at",
399+ "type": "bigint",
400+ "primaryKey": false,
401+ "notNull": false
402+ },
403+ "oauth_scope": {
404+ "name": "oauth_scope",
405+ "type": "text",
406+ "primaryKey": false,
407+ "notNull": false
408+ },
409+ "oauth_token_url": {
410+ "name": "oauth_token_url",
411+ "type": "text",
412+ "primaryKey": false,
413+ "notNull": false
414+ },
415+ "provider_state": {
416+ "name": "provider_state",
417+ "type": "json",
418+ "primaryKey": false,
419+ "notNull": false
420+ },
421+ "created_at": {
422+ "name": "created_at",
423+ "type": "timestamp",
424+ "primaryKey": false,
425+ "notNull": true
426+ },
427+ "updated_at": {
428+ "name": "updated_at",
429+ "type": "timestamp",
430+ "primaryKey": false,
431+ "notNull": true
432+ },
433+ "row_id": {
434+ "name": "row_id",
435+ "type": "varchar(255)",
436+ "primaryKey": true,
437+ "notNull": true
438+ },
439+ "tenant": {
440+ "name": "tenant",
441+ "type": "varchar(255)",
442+ "primaryKey": false,
443+ "notNull": true
444+ },
445+ "owner": {
446+ "name": "owner",
447+ "type": "varchar(255)",
448+ "primaryKey": false,
449+ "notNull": true
450+ },
451+ "subject": {
452+ "name": "subject",
453+ "type": "varchar(255)",
454+ "primaryKey": false,
455+ "notNull": true
456+ }
457+ },
458+ "indexes": {
459+ "connection_uidx": {
460+ "name": "connection_uidx",
461+ "columns": [
462+ {
463+ "expression": "tenant",
464+ "isExpression": false,
465+ "asc": true,
466+ "nulls": "last"
467+ },
468+ {
469+ "expression": "owner",
470+ "isExpression": false,
471+ "asc": true,
472+ "nulls": "last"
473+ },
474+ {
475+ "expression": "subject",
476+ "isExpression": false,
477+ "asc": true,
478+ "nulls": "last"
479+ },
480+ {
481+ "expression": "integration",
482+ "isExpression": false,
483+ "asc": true,
484+ "nulls": "last"
485+ },
486+ {
487+ "expression": "name",
488+ "isExpression": false,
489+ "asc": true,
490+ "nulls": "last"
491+ }
492+ ],
493+ "isUnique": true,
494+ "concurrently": false,
495+ "method": "btree",
496+ "with": {}
497+ }
498+ },
499+ "foreignKeys": {},
500+ "compositePrimaryKeys": {},
501+ "uniqueConstraints": {},
502+ "policies": {},
503+ "checkConstraints": {},
504+ "isRLSEnabled": false
505+ },
506+ "public.definition": {
507+ "name": "definition",
508+ "schema": "",
509+ "columns": {
510+ "integration": {
511+ "name": "integration",
512+ "type": "varchar(255)",
513+ "primaryKey": false,
514+ "notNull": true
515+ },
516+ "connection": {
517+ "name": "connection",
518+ "type": "varchar(255)",
519+ "primaryKey": false,
520+ "notNull": true
521+ },
522+ "plugin_id": {
523+ "name": "plugin_id",
524+ "type": "text",
525+ "primaryKey": false,
526+ "notNull": true
527+ },
528+ "name": {
529+ "name": "name",
530+ "type": "text",
531+ "primaryKey": false,
532+ "notNull": true
533+ },
534+ "schema": {
535+ "name": "schema",
536+ "type": "json",
537+ "primaryKey": false,
538+ "notNull": true
539+ },
540+ "created_at": {
541+ "name": "created_at",
542+ "type": "timestamp",
543+ "primaryKey": false,
544+ "notNull": true
545+ },
546+ "row_id": {
547+ "name": "row_id",
548+ "type": "varchar(255)",
549+ "primaryKey": true,
550+ "notNull": true
551+ },
552+ "tenant": {
553+ "name": "tenant",
554+ "type": "varchar(255)",
555+ "primaryKey": false,
556+ "notNull": true
557+ },
558+ "owner": {
559+ "name": "owner",
560+ "type": "varchar(255)",
561+ "primaryKey": false,
562+ "notNull": true
563+ },
564+ "subject": {
565+ "name": "subject",
566+ "type": "varchar(255)",
567+ "primaryKey": false,
568+ "notNull": true
569+ }
570+ },
571+ "indexes": {
572+ "definition_uidx": {
573+ "name": "definition_uidx",
574+ "columns": [
575+ {
576+ "expression": "tenant",
577+ "isExpression": false,
578+ "asc": true,
579+ "nulls": "last"
580+ },
581+ {
582+ "expression": "owner",
583+ "isExpression": false,
584+ "asc": true,
585+ "nulls": "last"
586+ },
587+ {
588+ "expression": "subject",
589+ "isExpression": false,
590+ "asc": true,
591+ "nulls": "last"
592+ },
593+ {
594+ "expression": "integration",
595+ "isExpression": false,
596+ "asc": true,
597+ "nulls": "last"
598+ },
599+ {
600+ "expression": "connection",
601+ "isExpression": false,
602+ "asc": true,
603+ "nulls": "last"
604+ },
605+ {
606+ "expression": "name",
607+ "isExpression": false,
608+ "asc": true,
609+ "nulls": "last"
610+ }
611+ ],
612+ "isUnique": true,
613+ "concurrently": false,
614+ "method": "btree",
615+ "with": {}
616+ }
617+ },
618+ "foreignKeys": {},
619+ "compositePrimaryKeys": {},
620+ "uniqueConstraints": {},
621+ "policies": {},
622+ "checkConstraints": {},
623+ "isRLSEnabled": false
624+ },
625+ "public.integration": {
626+ "name": "integration",
627+ "schema": "",
628+ "columns": {
629+ "slug": {
630+ "name": "slug",
631+ "type": "varchar(255)",
632+ "primaryKey": false,
633+ "notNull": true
634+ },
635+ "plugin_id": {
636+ "name": "plugin_id",
637+ "type": "text",
638+ "primaryKey": false,
639+ "notNull": true
640+ },
641+ "name": {
642+ "name": "name",
643+ "type": "text",
644+ "primaryKey": false,
645+ "notNull": false
646+ },
647+ "description": {
648+ "name": "description",
649+ "type": "text",
650+ "primaryKey": false,
651+ "notNull": false
652+ },
653+ "config": {
654+ "name": "config",
655+ "type": "json",
656+ "primaryKey": false,
657+ "notNull": false
658+ },
659+ "health_check": {
660+ "name": "health_check",
661+ "type": "json",
662+ "primaryKey": false,
663+ "notNull": false
664+ },
665+ "config_revised_at": {
666+ "name": "config_revised_at",
667+ "type": "bigint",
668+ "primaryKey": false,
669+ "notNull": false
670+ },
671+ "can_remove": {
672+ "name": "can_remove",
673+ "type": "boolean",
674+ "primaryKey": false,
675+ "notNull": true,
676+ "default": true
677+ },
678+ "can_refresh": {
679+ "name": "can_refresh",
680+ "type": "boolean",
681+ "primaryKey": false,
682+ "notNull": true,
683+ "default": false
684+ },
685+ "created_at": {
686+ "name": "created_at",
687+ "type": "timestamp",
688+ "primaryKey": false,
689+ "notNull": true
690+ },
691+ "updated_at": {
692+ "name": "updated_at",
693+ "type": "timestamp",
694+ "primaryKey": false,
695+ "notNull": true
696+ },
697+ "row_id": {
698+ "name": "row_id",
699+ "type": "varchar(255)",
700+ "primaryKey": true,
701+ "notNull": true
702+ },
703+ "tenant": {
704+ "name": "tenant",
705+ "type": "varchar(255)",
706+ "primaryKey": false,
707+ "notNull": true
708+ }
709+ },
710+ "indexes": {
711+ "integration_uidx": {
712+ "name": "integration_uidx",
713+ "columns": [
714+ {
715+ "expression": "tenant",
716+ "isExpression": false,
717+ "asc": true,
718+ "nulls": "last"
719+ },
720+ {
721+ "expression": "slug",
722+ "isExpression": false,
723+ "asc": true,
724+ "nulls": "last"
725+ }
726+ ],
727+ "isUnique": true,
728+ "concurrently": false,
729+ "method": "btree",
730+ "with": {}
731+ }
732+ },
733+ "foreignKeys": {},
734+ "compositePrimaryKeys": {},
735+ "uniqueConstraints": {},
736+ "policies": {},
737+ "checkConstraints": {},
738+ "isRLSEnabled": false
739+ },
740+ "public.oauth_client": {
741+ "name": "oauth_client",
742+ "schema": "",
743+ "columns": {
744+ "slug": {
745+ "name": "slug",
746+ "type": "varchar(255)",
747+ "primaryKey": false,
748+ "notNull": true
749+ },
750+ "authorization_url": {
751+ "name": "authorization_url",
752+ "type": "text",
753+ "primaryKey": false,
754+ "notNull": true
755+ },
756+ "token_url": {
757+ "name": "token_url",
758+ "type": "text",
759+ "primaryKey": false,
760+ "notNull": true
761+ },
762+ "grant": {
763+ "name": "grant",
764+ "type": "text",
765+ "primaryKey": false,
766+ "notNull": true
767+ },
768+ "client_id": {
769+ "name": "client_id",
770+ "type": "text",
771+ "primaryKey": false,
772+ "notNull": true
773+ },
774+ "client_secret_item_id": {
775+ "name": "client_secret_item_id",
776+ "type": "text",
777+ "primaryKey": false,
778+ "notNull": false
779+ },
780+ "resource": {
781+ "name": "resource",
782+ "type": "text",
783+ "primaryKey": false,
784+ "notNull": false
785+ },
786+ "origin_kind": {
787+ "name": "origin_kind",
788+ "type": "text",
789+ "primaryKey": false,
790+ "notNull": false
791+ },
792+ "origin_integration": {
793+ "name": "origin_integration",
794+ "type": "text",
795+ "primaryKey": false,
796+ "notNull": false
797+ },
798+ "origin_issuer": {
799+ "name": "origin_issuer",
800+ "type": "text",
801+ "primaryKey": false,
802+ "notNull": false
803+ },
804+ "origin_redirect_uri": {
805+ "name": "origin_redirect_uri",
806+ "type": "text",
807+ "primaryKey": false,
808+ "notNull": false
809+ },
810+ "created_at": {
811+ "name": "created_at",
812+ "type": "timestamp",
813+ "primaryKey": false,
814+ "notNull": true
815+ },
816+ "row_id": {
817+ "name": "row_id",
818+ "type": "varchar(255)",
819+ "primaryKey": true,
820+ "notNull": true
821+ },
822+ "tenant": {
823+ "name": "tenant",
824+ "type": "varchar(255)",
825+ "primaryKey": false,
826+ "notNull": true
827+ },
828+ "owner": {
829+ "name": "owner",
830+ "type": "varchar(255)",
831+ "primaryKey": false,
832+ "notNull": true
833+ },
834+ "subject": {
835+ "name": "subject",
836+ "type": "varchar(255)",
837+ "primaryKey": false,
838+ "notNull": true
839+ }
840+ },
841+ "indexes": {
842+ "oauth_client_uidx": {
843+ "name": "oauth_client_uidx",
844+ "columns": [
845+ {
846+ "expression": "tenant",
847+ "isExpression": false,
848+ "asc": true,
849+ "nulls": "last"
850+ },
851+ {
852+ "expression": "owner",
853+ "isExpression": false,
854+ "asc": true,
855+ "nulls": "last"
856+ },
857+ {
858+ "expression": "subject",
859+ "isExpression": false,
860+ "asc": true,
861+ "nulls": "last"
862+ },
863+ {
864+ "expression": "slug",
865+ "isExpression": false,
866+ "asc": true,
867+ "nulls": "last"
868+ }
869+ ],
870+ "isUnique": true,
871+ "concurrently": false,
872+ "method": "btree",
873+ "with": {}
874+ }
875+ },
876+ "foreignKeys": {},
877+ "compositePrimaryKeys": {},
878+ "uniqueConstraints": {},
879+ "policies": {},
880+ "checkConstraints": {},
881+ "isRLSEnabled": false
882+ },
883+ "public.oauth_session": {
884+ "name": "oauth_session",
885+ "schema": "",
886+ "columns": {
887+ "state": {
888+ "name": "state",
889+ "type": "varchar(255)",
890+ "primaryKey": false,
891+ "notNull": true
892+ },
893+ "client_slug": {
894+ "name": "client_slug",
895+ "type": "text",
896+ "primaryKey": false,
897+ "notNull": true
898+ },
899+ "integration": {
900+ "name": "integration",
901+ "type": "text",
902+ "primaryKey": false,
903+ "notNull": true
904+ },
905+ "name": {
906+ "name": "name",
907+ "type": "text",
908+ "primaryKey": false,
909+ "notNull": true
910+ },
911+ "template": {
912+ "name": "template",
913+ "type": "text",
914+ "primaryKey": false,
915+ "notNull": true
916+ },
917+ "redirect_url": {
918+ "name": "redirect_url",
919+ "type": "text",
920+ "primaryKey": false,
921+ "notNull": true
922+ },
923+ "pkce_verifier": {
924+ "name": "pkce_verifier",
925+ "type": "text",
926+ "primaryKey": false,
927+ "notNull": false
928+ },
929+ "identity_label": {
930+ "name": "identity_label",
931+ "type": "text",
932+ "primaryKey": false,
933+ "notNull": false
934+ },
935+ "payload": {
936+ "name": "payload",
937+ "type": "json",
938+ "primaryKey": false,
939+ "notNull": true
940+ },
941+ "expires_at": {
942+ "name": "expires_at",
943+ "type": "bigint",
944+ "primaryKey": false,
945+ "notNull": true
946+ },
947+ "created_at": {
948+ "name": "created_at",
949+ "type": "timestamp",
950+ "primaryKey": false,
951+ "notNull": true
952+ },
953+ "row_id": {
954+ "name": "row_id",
955+ "type": "varchar(255)",
956+ "primaryKey": true,
957+ "notNull": true
958+ },
959+ "tenant": {
960+ "name": "tenant",
961+ "type": "varchar(255)",
962+ "primaryKey": false,
963+ "notNull": true
964+ },
965+ "owner": {
966+ "name": "owner",
967+ "type": "varchar(255)",
968+ "primaryKey": false,
969+ "notNull": true
970+ },
971+ "subject": {
972+ "name": "subject",
973+ "type": "varchar(255)",
974+ "primaryKey": false,
975+ "notNull": true
976+ }
977+ },
978+ "indexes": {
979+ "oauth_session_uidx": {
980+ "name": "oauth_session_uidx",
981+ "columns": [
982+ {
983+ "expression": "tenant",
984+ "isExpression": false,
985+ "asc": true,
986+ "nulls": "last"
987+ },
988+ {
989+ "expression": "state",
990+ "isExpression": false,
991+ "asc": true,
992+ "nulls": "last"
993+ }
994+ ],
995+ "isUnique": true,
996+ "concurrently": false,
997+ "method": "btree",
998+ "with": {}
999+ }
1000+ },
1001+ "foreignKeys": {},
1002+ "compositePrimaryKeys": {},
1003+ "uniqueConstraints": {},
1004+ "policies": {},
1005+ "checkConstraints": {},
1006+ "isRLSEnabled": false
1007+ },
1008+ "public.plugin_storage": {
1009+ "name": "plugin_storage",
1010+ "schema": "",
1011+ "columns": {
1012+ "plugin_id": {
1013+ "name": "plugin_id",
1014+ "type": "varchar(255)",
1015+ "primaryKey": false,
1016+ "notNull": true
1017+ },
1018+ "collection": {
1019+ "name": "collection",
1020+ "type": "varchar(255)",
1021+ "primaryKey": false,
1022+ "notNull": true
1023+ },
1024+ "key": {
1025+ "name": "key",
1026+ "type": "varchar(255)",
1027+ "primaryKey": false,
1028+ "notNull": true
1029+ },
1030+ "data": {
1031+ "name": "data",
1032+ "type": "json",
1033+ "primaryKey": false,
1034+ "notNull": true
1035+ },
1036+ "created_at": {
1037+ "name": "created_at",
1038+ "type": "timestamp",
1039+ "primaryKey": false,
1040+ "notNull": true
1041+ },
1042+ "updated_at": {
1043+ "name": "updated_at",
1044+ "type": "timestamp",
1045+ "primaryKey": false,
1046+ "notNull": true
1047+ },
1048+ "row_id": {
1049+ "name": "row_id",
1050+ "type": "varchar(255)",
1051+ "primaryKey": true,
1052+ "notNull": true
1053+ },
1054+ "tenant": {
1055+ "name": "tenant",
1056+ "type": "varchar(255)",
1057+ "primaryKey": false,
1058+ "notNull": true
1059+ },
1060+ "owner": {
1061+ "name": "owner",
1062+ "type": "varchar(255)",
1063+ "primaryKey": false,
1064+ "notNull": true
1065+ },
1066+ "subject": {
1067+ "name": "subject",
1068+ "type": "varchar(255)",
1069+ "primaryKey": false,
1070+ "notNull": true
1071+ }
1072+ },
1073+ "indexes": {
1074+ "plugin_storage_uidx": {
1075+ "name": "plugin_storage_uidx",
1076+ "columns": [
1077+ {
1078+ "expression": "tenant",
1079+ "isExpression": false,
1080+ "asc": true,
1081+ "nulls": "last"
1082+ },
1083+ {
1084+ "expression": "owner",
1085+ "isExpression": false,
1086+ "asc": true,
1087+ "nulls": "last"
1088+ },
1089+ {
1090+ "expression": "subject",
1091+ "isExpression": false,
1092+ "asc": true,
1093+ "nulls": "last"
1094+ },
1095+ {
1096+ "expression": "plugin_id",
1097+ "isExpression": false,
1098+ "asc": true,
1099+ "nulls": "last"
1100+ },
1101+ {
1102+ "expression": "collection",
1103+ "isExpression": false,
1104+ "asc": true,
1105+ "nulls": "last"
1106+ },
1107+ {
1108+ "expression": "key",
1109+ "isExpression": false,
1110+ "asc": true,
1111+ "nulls": "last"
1112+ }
1113+ ],
1114+ "isUnique": true,
1115+ "concurrently": false,
1116+ "method": "btree",
1117+ "with": {}
1118+ }
1119+ },
1120+ "foreignKeys": {},
1121+ "compositePrimaryKeys": {},
1122+ "uniqueConstraints": {},
1123+ "policies": {},
1124+ "checkConstraints": {},
1125+ "isRLSEnabled": false
1126+ },
1127+ "public.private_executor_cloud_settings": {
1128+ "name": "private_executor_cloud_settings",
1129+ "schema": "",
1130+ "columns": {
1131+ "id": {
1132+ "name": "id",
1133+ "type": "varchar(255)",
1134+ "primaryKey": true,
1135+ "notNull": true
1136+ },
1137+ "version": {
1138+ "name": "version",
1139+ "type": "varchar(255)",
1140+ "primaryKey": false,
1141+ "notNull": true,
1142+ "default": "'1.0.0'"
1143+ }
1144+ },
1145+ "indexes": {},
1146+ "foreignKeys": {},
1147+ "compositePrimaryKeys": {},
1148+ "uniqueConstraints": {},
1149+ "policies": {},
1150+ "checkConstraints": {},
1151+ "isRLSEnabled": false
1152+ },
1153+ "public.subject": {
1154+ "name": "subject",
1155+ "schema": "",
1156+ "columns": {
1157+ "external_id": {
1158+ "name": "external_id",
1159+ "type": "varchar(255)",
1160+ "primaryKey": false,
1161+ "notNull": true
1162+ },
1163+ "created_at": {
1164+ "name": "created_at",
1165+ "type": "timestamp",
1166+ "primaryKey": false,
1167+ "notNull": true
1168+ },
1169+ "last_seen_at": {
1170+ "name": "last_seen_at",
1171+ "type": "bigint",
1172+ "primaryKey": false,
1173+ "notNull": false
1174+ },
1175+ "status": {
1176+ "name": "status",
1177+ "type": "text",
1178+ "primaryKey": false,
1179+ "notNull": false
1180+ },
1181+ "row_id": {
1182+ "name": "row_id",
1183+ "type": "varchar(255)",
1184+ "primaryKey": true,
1185+ "notNull": true
1186+ },
1187+ "tenant": {
1188+ "name": "tenant",
1189+ "type": "varchar(255)",
1190+ "primaryKey": false,
1191+ "notNull": true
1192+ }
1193+ },
1194+ "indexes": {
1195+ "subject_uidx": {
1196+ "name": "subject_uidx",
1197+ "columns": [
1198+ {
1199+ "expression": "tenant",
1200+ "isExpression": false,
1201+ "asc": true,
1202+ "nulls": "last"
1203+ },
1204+ {
1205+ "expression": "external_id",
1206+ "isExpression": false,
1207+ "asc": true,
1208+ "nulls": "last"
1209+ }
1210+ ],
1211+ "isUnique": true,
1212+ "concurrently": false,
1213+ "method": "btree",
1214+ "with": {}
1215+ }
1216+ },
1217+ "foreignKeys": {},
1218+ "compositePrimaryKeys": {},
1219+ "uniqueConstraints": {},
1220+ "policies": {},
1221+ "checkConstraints": {},
1222+ "isRLSEnabled": false
1223+ },
1224+ "public.tool": {
1225+ "name": "tool",
1226+ "schema": "",
1227+ "columns": {
1228+ "integration": {
1229+ "name": "integration",
1230+ "type": "varchar(255)",
1231+ "primaryKey": false,
1232+ "notNull": true
1233+ },
1234+ "connection": {
1235+ "name": "connection",
1236+ "type": "varchar(255)",
1237+ "primaryKey": false,
1238+ "notNull": true
1239+ },
1240+ "plugin_id": {
1241+ "name": "plugin_id",
1242+ "type": "text",
1243+ "primaryKey": false,
1244+ "notNull": true
1245+ },
1246+ "name": {
1247+ "name": "name",
1248+ "type": "varchar(255)",
1249+ "primaryKey": false,
1250+ "notNull": true
1251+ },
1252+ "description": {
1253+ "name": "description",
1254+ "type": "text",
1255+ "primaryKey": false,
1256+ "notNull": true
1257+ },
1258+ "input_schema": {
1259+ "name": "input_schema",
1260+ "type": "json",
1261+ "primaryKey": false,
1262+ "notNull": false
1263+ },
1264+ "output_schema": {
1265+ "name": "output_schema",
1266+ "type": "json",
1267+ "primaryKey": false,
1268+ "notNull": false
1269+ },
1270+ "annotations": {
1271+ "name": "annotations",
1272+ "type": "json",
1273+ "primaryKey": false,
1274+ "notNull": false
1275+ },
1276+ "created_at": {
1277+ "name": "created_at",
1278+ "type": "timestamp",
1279+ "primaryKey": false,
1280+ "notNull": true
1281+ },
1282+ "updated_at": {
1283+ "name": "updated_at",
1284+ "type": "timestamp",
1285+ "primaryKey": false,
1286+ "notNull": true
1287+ },
1288+ "row_id": {
1289+ "name": "row_id",
1290+ "type": "varchar(255)",
1291+ "primaryKey": true,
1292+ "notNull": true
1293+ },
1294+ "tenant": {
1295+ "name": "tenant",
1296+ "type": "varchar(255)",
1297+ "primaryKey": false,
1298+ "notNull": true
1299+ },
1300+ "owner": {
1301+ "name": "owner",
1302+ "type": "varchar(255)",
1303+ "primaryKey": false,
1304+ "notNull": true
1305+ },
1306+ "subject": {
1307+ "name": "subject",
1308+ "type": "varchar(255)",
1309+ "primaryKey": false,
1310+ "notNull": true
1311+ }
1312+ },
1313+ "indexes": {
1314+ "tool_uidx": {
1315+ "name": "tool_uidx",
1316+ "columns": [
1317+ {
1318+ "expression": "tenant",
1319+ "isExpression": false,
1320+ "asc": true,
1321+ "nulls": "last"
1322+ },
1323+ {
1324+ "expression": "owner",
1325+ "isExpression": false,
1326+ "asc": true,
1327+ "nulls": "last"
1328+ },
1329+ {
1330+ "expression": "subject",
1331+ "isExpression": false,
1332+ "asc": true,
1333+ "nulls": "last"
1334+ },
1335+ {
1336+ "expression": "integration",
1337+ "isExpression": false,
1338+ "asc": true,
1339+ "nulls": "last"
1340+ },
1341+ {
1342+ "expression": "connection",
1343+ "isExpression": false,
1344+ "asc": true,
1345+ "nulls": "last"
1346+ },
1347+ {
1348+ "expression": "name",
1349+ "isExpression": false,
1350+ "asc": true,
1351+ "nulls": "last"
1352+ }
1353+ ],
1354+ "isUnique": true,
1355+ "concurrently": false,
1356+ "method": "btree",
1357+ "with": {}
1358+ }
1359+ },
1360+ "foreignKeys": {},
1361+ "compositePrimaryKeys": {},
1362+ "uniqueConstraints": {},
1363+ "policies": {},
1364+ "checkConstraints": {},
1365+ "isRLSEnabled": false
1366+ },
1367+ "public.tool_policy": {
1368+ "name": "tool_policy",
1369+ "schema": "",
1370+ "columns": {
1371+ "id": {
1372+ "name": "id",
1373+ "type": "varchar(255)",
1374+ "primaryKey": false,
1375+ "notNull": true
1376+ },
1377+ "pattern": {
1378+ "name": "pattern",
1379+ "type": "text",
1380+ "primaryKey": false,
1381+ "notNull": true
1382+ },
1383+ "action": {
1384+ "name": "action",
1385+ "type": "text",
1386+ "primaryKey": false,
1387+ "notNull": true
1388+ },
1389+ "position": {
1390+ "name": "position",
1391+ "type": "text",
1392+ "primaryKey": false,
1393+ "notNull": true
1394+ },
1395+ "created_at": {
1396+ "name": "created_at",
1397+ "type": "timestamp",
1398+ "primaryKey": false,
1399+ "notNull": true
1400+ },
1401+ "updated_at": {
1402+ "name": "updated_at",
1403+ "type": "timestamp",
1404+ "primaryKey": false,
1405+ "notNull": true
1406+ },
1407+ "row_id": {
1408+ "name": "row_id",
1409+ "type": "varchar(255)",
1410+ "primaryKey": true,
1411+ "notNull": true
1412+ },
1413+ "tenant": {
1414+ "name": "tenant",
1415+ "type": "varchar(255)",
1416+ "primaryKey": false,
1417+ "notNull": true
1418+ },
1419+ "owner": {
1420+ "name": "owner",
1421+ "type": "varchar(255)",
1422+ "primaryKey": false,
1423+ "notNull": true
1424+ },
1425+ "subject": {
1426+ "name": "subject",
1427+ "type": "varchar(255)",
1428+ "primaryKey": false,
1429+ "notNull": true
1430+ }
1431+ },
1432+ "indexes": {
1433+ "tool_policy_uidx": {
1434+ "name": "tool_policy_uidx",
1435+ "columns": [
1436+ {
1437+ "expression": "tenant",
1438+ "isExpression": false,
1439+ "asc": true,
1440+ "nulls": "last"
1441+ },
1442+ {
1443+ "expression": "owner",
1444+ "isExpression": false,
1445+ "asc": true,
1446+ "nulls": "last"
1447+ },
1448+ {
1449+ "expression": "subject",
1450+ "isExpression": false,
1451+ "asc": true,
1452+ "nulls": "last"
1453+ },
1454+ {
1455+ "expression": "id",
1456+ "isExpression": false,
1457+ "asc": true,
1458+ "nulls": "last"
1459+ }
1460+ ],
1461+ "isUnique": true,
1462+ "concurrently": false,
1463+ "method": "btree",
1464+ "with": {}
1465+ }
1466+ },
1467+ "foreignKeys": {},
1468+ "compositePrimaryKeys": {},
1469+ "uniqueConstraints": {},
1470+ "policies": {},
1471+ "checkConstraints": {},
1472+ "isRLSEnabled": false
1473+ }
1474+ },
1475+ "enums": {},
1476+ "schemas": {},
1477+ "sequences": {},
1478+ "roles": {},
1479+ "policies": {},
1480+ "views": {},
1481+ "_meta": {
1482+ "columns": {},
1483+ "schemas": {},
1484+ "tables": {}
1485+ }
1486+}
apps/cloud/drizzle/meta/_journal.json+42−0
@@ -71,6 +71,48 @@ · 70 unmodified lines
7171 "when": 1783025157233,
7272 "tag": "0009_true_lake",
7373 "breakpoints": true
74+ },
75+ {
76+ "idx": 10,
77+ "version": "7",
78+ "when": 1783660669921,
79+ "tag": "0010_ndjson_output_arrays",
80+ "breakpoints": true
81+ },
82+ {
83+ "idx": 11,
84+ "version": "7",
85+ "when": 1784581232732,
86+ "tag": "0011_tense_caretaker",
87+ "breakpoints": true
88+ },
89+ {
90+ "idx": 12,
91+ "version": "7",
92+ "when": 1785193313601,
93+ "tag": "0012_woozy_shiva",
94+ "breakpoints": true
95+ },
96+ {
97+ "idx": 13,
98+ "version": "7",
99+ "when": 1785354087138,
100+ "tag": "0013_curly_rocket_racer",
101+ "breakpoints": true
102+ },
103+ {
104+ "idx": 14,
105+ "version": "7",
106+ "when": 1785355312973,
107+ "tag": "0014_new_karma",
108+ "breakpoints": true
109+ },
110+ {
111+ "idx": 15,
112+ "version": "7",
113+ "when": 1785355354955,
114+ "tag": "0015_equal_the_leader",
115+ "breakpoints": true
74116 }
75117 ]
76118 }
apps/cloud/executor.config.ts+13−15
@@ -1,7 +1,13 @@
11 import { defineExecutorConfig } from "@executor-js/sdk";
22 import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api";
3−import { googleHttpPlugin } from "@executor-js/plugin-google/api";
4−import { microsoftHttpPlugin } from "@executor-js/plugin-microsoft/api";
3+import {
4+ googleCatalog,
5+ googleDiscoveryAdapter,
6+} from "@executor-js/plugin-openapi/providers/google";
7+import {
8+ microsoftCatalog,
9+ microsoftGraphAdapter,
10+} from "@executor-js/plugin-openapi/providers/microsoft";
511 import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api";
612 import { graphqlHttpPlugin } from "@executor-js/plugin-graphql/api";
713 import { workosVaultPlugin, type WorkOSVaultClient } from "@executor-js/plugin-workos-vault";
@@ -42,23 +48,15 @@ interface CloudPluginDeps { · 34 unmodified lines
4248 * falls back to the credential-driven default. */
4349 readonly workosVaultClient?: WorkOSVaultClient;
4450 readonly activeToolkitSlug?: string;
45− /** Mirrors `HostConfig.allowLocalNetwork` (`ALLOW_LOCAL_NETWORK`): lets
46− * `microsoft.addGraph` point at a loopback emulator instead of the pinned
47− * Microsoft Graph URLs. Off by default; production leaves it unset. */
48− readonly allowLocalNetwork?: boolean;
4951 }
5052
5153 export default defineExecutorConfig({
52− plugins: ({
53− workosCredentials,
54− workosVaultClient,
55− activeToolkitSlug,
56− allowLocalNetwork,
57− }: CloudPluginDeps = {}) =>
54+ plugins: ({ workosCredentials, workosVaultClient, activeToolkitSlug }: CloudPluginDeps = {}) =>
5855 [
59− openApiHttpPlugin(),
60− googleHttpPlugin(),
61− microsoftHttpPlugin({ allowUnsafeUrlOverrides: allowLocalNetwork === true }),
56+ openApiHttpPlugin({
57+ presets: [...googleCatalog, ...microsoftCatalog],
58+ specFormats: [googleDiscoveryAdapter, microsoftGraphAdapter],
59+ }),
6260 mcpHttpPlugin({
6361 dangerouslyAllowStdioMCP: false,
6462 }),
apps/cloud/package.json+9−4
@@ -1,6 +1,6 @@
11 {
22 "name": "@executor-js/cloud",
3− "version": "1.4.47",
3+ "version": "1.4.61",
44 "private": true,
55 "type": "module",
66 "scripts": {
@@ -30,7 +30,10 @@ · 23 unmodified lines
3030 "typecheck:slow": "tsc --noEmit",
3131 "db:backfill-org-slugs:prod": "op run --env-file=.env.production -- bun run scripts/backfill-org-slugs.ts",
3232 "db:backfill-org-slugs:dev": "op run --env-file=.env.op -- bun run scripts/backfill-org-slugs.ts",
33− "routes:gen": "bun scripts/gen-routes.ts"
33+ "db:backfill-subjects:prod": "op run --env-file=.env.production -- bun run scripts/backfill-subjects.ts",
34+ "db:backfill-subjects:dev": "op run --env-file=.env.op -- bun run scripts/backfill-subjects.ts",
35+ "routes:gen": "bun scripts/gen-routes.ts",
36+ "vendor-wasm": "bun run scripts/vendor-quickjs-wasm.ts"
3437 },
3538 "dependencies": {
3639 "@cloudflare/vite-plugin": "^1.31.1",
@@ -41,10 +44,9 @@ · 4 unmodified lines
4144 "@executor-js/execution": "workspace:*",
4245 "@executor-js/fumadb": "workspace:*",
4346 "@executor-js/host-mcp": "workspace:*",
44− "@executor-js/plugin-google": "workspace:*",
47+ "@executor-js/mcp-apps-shell": "workspace:*",
4548 "@executor-js/plugin-graphql": "workspace:*",
4649 "@executor-js/plugin-mcp": "workspace:*",
47− "@executor-js/plugin-microsoft": "workspace:*",
4850 "@executor-js/plugin-openapi": "workspace:*",
4951 "@executor-js/plugin-toolkits": "workspace:*",
5052 "@executor-js/plugin-workos-vault": "workspace:*",
@@ -53,6 +55,7 @@ · 2 unmodified lines
5355 "@executor-js/runtime-quickjs": "workspace:*",
5456 "@executor-js/sdk": "workspace:*",
5557 "@executor-js/vite-plugin": "workspace:*",
58+ "@jitl/quickjs-wasmfile-release-sync": "catalog:",
5659 "@modelcontextprotocol/sdk": "^1.29.0",
5760 "@opentelemetry/api": "~1.9.0",
5861 "@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
@@ -70,9 +73,11 @@ · 11 unmodified lines
7073 "autumn-js": "^1.2.8",
7174 "drizzle-orm": "catalog:",
7275 "effect": "catalog:",
76+ "iron-webcrypto": "2.0.0",
7377 "jose": "^5.6.3",
7478 "postgres": "^3.4.9",
7579 "posthog-js": "^1.372.5",
80+ "quickjs-emscripten-core": "0.31.0",
7681 "react": "catalog:",
7782 "react-dom": "catalog:",
7883 "sonner": "^2.0.7"
apps/cloud/scripts/backfill-subjects.ts+87−0
@@ -0,0 +1,87 @@
1+/* oxlint-disable executor/no-try-catch-or-throw -- boundary: out-of-band migration script over a raw postgres connection */
2+// ---------------------------------------------------------------------------
3+// One-off data backfill: mint `subject` rows for principals that predate the
4+// subject table (added by migration 0012, written only on sightings from then
5+// on).
6+//
7+// bun run db:backfill-subjects:prod # op run --env-file=.env.production
8+// bun run db:backfill-subjects:dev # against the local PGlite dev db
9+//
10+// Source of truth is the `connection` table: before 0012, "which users exist
11+// under this tenant" was answerable only through connection rows, so a distinct
12+// (tenant, subject) there — excluding the org sentinel '' — is exactly the set
13+// of users the admin console under-reports until their next request.
14+//
15+// created_at ← the tenant+subject's oldest connection, the closest honest
16+// answer to "when did this user first appear".
17+// last_seen_at ← the tenant+subject's newest connection `updated_at`, the
18+// latest activity storage can attest to. Rendering every
19+// pre-0012 user as "Never" was literally true of the sighting
20+// column but reads as "inactive user" to an operator, which
21+// is the wrong answer for a tenant full of active people. The
22+// first real sighting overwrites this with request truth.
23+//
24+// Idempotent — the insert's ON CONFLICT on the (tenant, external_id) unique
25+// index leaves rows the runtime has since minted (or a prior run inserted)
26+// untouched, and the last_seen_at repair only fills NULLs — so re-running is
27+// safe and never rewinds a live row's fields.
28+// Pass --dry-run to print the plan without writing.
29+// ---------------------------------------------------------------------------
30+
31+import postgres from "postgres";
32+import { createId } from "@executor-js/fumadb/cuid";
33+
34+const dryRun = process.argv.includes("--dry-run");
35+
36+const connectionString = process.env.DATABASE_URL;
37+if (!connectionString) {
38+ console.error("DATABASE_URL is not set");
39+ process.exit(1);
40+}
41+
42+const sql = postgres(connectionString, { max: 1, prepare: false, ssl: "require" });
43+
44+const rows = await sql<
45+ { tenant: string; subject: string; first_connection_at: Date; last_activity_at: Date }[]
46+>`
47+ SELECT tenant, subject,
48+ MIN(created_at) AS first_connection_at,
49+ MAX(updated_at) AS last_activity_at
50+ FROM connection
51+ WHERE subject <> ''
52+ GROUP BY tenant, subject
53+ ORDER BY tenant, subject
54+`;
55+console.log(`${rows.length} (tenant, subject) pair(s) with connections`);
56+
57+let inserted = 0;
58+let repaired = 0;
59+for (const row of rows) {
60+ console.log(
61+ `${row.tenant} ${row.subject} first connection ${row.first_connection_at.toISOString()} last activity ${row.last_activity_at.toISOString()}`,
62+ );
63+ if (dryRun) continue;
64+ const lastSeenMs = row.last_activity_at.getTime();
65+ const result = await sql`
66+ INSERT INTO subject (row_id, tenant, external_id, created_at, last_seen_at, status)
67+ VALUES (${createId()}, ${row.tenant}, ${row.subject}, ${row.first_connection_at}, ${lastSeenMs}, NULL)
68+ ON CONFLICT (tenant, external_id) DO NOTHING
69+ `;
70+ inserted += result.count;
71+ if (result.count > 0) continue;
72+ // The row predates this run but may still carry NULL from a run of the
73+ // pre-repair version of this script. Fill it in; a sighted row keeps its
74+ // request-truth timestamp.
75+ const repair = await sql`
76+ UPDATE subject SET last_seen_at = ${lastSeenMs}
77+ WHERE tenant = ${row.tenant} AND external_id = ${row.subject} AND last_seen_at IS NULL
78+ `;
79+ repaired += repair.count;
80+}
81+
82+console.log(
83+ dryRun
84+ ? `dry run — would insert up to ${rows.length} row(s)`
85+ : `inserted ${inserted} row(s), repaired last_seen_at on ${repaired} row(s)`,
86+);
87+await sql.end();
apps/cloud/scripts/build.mjs+20−1
@@ -9,7 +9,7 @@ · 8 unmodified lines
99
1010 import { spawnSync } from "node:child_process";
1111 import { randomBytes } from "node:crypto";
12−import { rmSync } from "node:fs";
12+import { existsSync, readFileSync, rmSync } from "node:fs";
1313
1414 if (!process.env.VITE_PUBLIC_ANALYTICS_PATH) {
1515 process.env.VITE_PUBLIC_ANALYTICS_PATH = randomBytes(4).toString("hex");
@@ -32,3 +32,22 @@ for (const step of steps) { · 16 unmodified lines
3232 process.exit(result.status ?? 1);
3333 }
3434 }
35+
36+// The deployed Worker serves the MCP-Apps shell (`ui://executor/shell.html`)
37+// by fetching this stable-named asset through the ASSETS binding — it has no
38+// filesystem to read the shell from. A build that failed to emit it would
39+// deploy a Worker whose artifact resource reads all fail, so make the miss a
40+// build failure here instead of a production incident. Same shape as the
41+// worker-bundler asset assertion from #1378.
42+const shellAsset = new URL(
43+ "../dist/client/assets/executor-mcp-apps-shell-stable.html",
44+ import.meta.url,
45+);
46+if (!existsSync(shellAsset)) {
47+ console.error(`[build] missing MCP-Apps shell asset at ${shellAsset.pathname}`);
48+ process.exit(1);
49+}
50+if (!readFileSync(shellAsset, "utf8").includes('name="executor-mcp-apps-shell"')) {
51+ console.error(`[build] ${shellAsset.pathname} is not the MCP-Apps shell document`);
52+ process.exit(1);
53+}
apps/cloud/scripts/dev-db.ts+145−14
@@ -15,6 +15,7 @@ import { PGlite } from "@electric-sql/pglite"; · 14 unmodified lines
1515 import { PGLiteSocketServer } from "@electric-sql/pglite-socket";
1616 import { drizzle } from "drizzle-orm/pglite";
1717 import { migrate } from "drizzle-orm/pglite/migrator";
18+import postgres from "postgres";
1819
1920 const __dirname = dirname(fileURLToPath(import.meta.url));
2021 // Port + data dir default to the dev values but are env-overridable so a second
@@ -104,25 +105,46 @@ await migrate(drizzle(db), { migrationsFolder: MIGRATIONS_FOLDER }); · 83 unmodified lines
104105 // but prepared statement requires M" -> random 500s on whichever request lost
105106 // the race). The patch in patches/@electric-sql%2Fpglite-socket@0.1.4.patch
106107 // batches each socket data event into one queue entry and holds handler
107−// affinity while a pipeline is open;
108−// src/db/dev-db-socket-concurrency.node.test.ts is the regression test.
109−const server = new PGLiteSocketServer({
110− db,
111− port: PORT,
112− host: "127.0.0.1",
113− maxConnections: Number(process.env.DEV_DB_MAX_CONNECTIONS ?? 1000),
114− // Backstop for pipeline affinity: a client that stalls mid-pipeline (Parse
115− // sent, no Sync) with its socket still OPEN would hold the queue's handler
116− // affinity forever and starve every other connection, since affinity only
117− // releases on detach and detach needs close/error/idle-timeout. In ms; the
118− // timer resets on every data event, so only a genuinely dead client trips it.
119− idleTimeout: Number(process.env.DEV_DB_IDLE_TIMEOUT_MS ?? 30_000),
120−});
108+// affinity while a pipeline is open. The patch also fixes the queue's two
109+// self-bricking failure paths — both surfaced in CI as the e2e "cloud signIn:
110+// callback set no session (500)" cascade, where new connections' startup
111+// packets sat unanswered (postgres.js CONNECT_TIMEOUT) until restart:
112+// 1. Stock 0.1.4 `return`ed out of the drain loop when a query REJECTED at
113+// the JS level, leaving its `processing` flag latched true; nothing was
114+// ever dequeued again.
115+// 2. A client whose socket died WHILE its pipeline-opening entry executed:
116+// detach() cleared affinity before the entry finished, the queue then
117+// took affinity for the already-dead handler, and no timer was left to
118+// release it. The queue now tracks detached handlers and repairs any
119+// transaction or pipeline affinity they can no longer release.
120+// src/db/dev-db-socket-concurrency.node.test.ts is the regression test for
121+// all of the above.
122+const makeServer = () =>
123+ new PGLiteSocketServer({
124+ db,
125+ port: PORT,
126+ host: "127.0.0.1",
127+ maxConnections: Number(process.env.DEV_DB_MAX_CONNECTIONS ?? 1000),
128+ // Backstop for pipeline affinity: a client that stalls mid-pipeline (Parse
129+ // sent, no Sync) with its socket still OPEN would hold the queue's handler
130+ // affinity forever and starve every other connection, since affinity only
131+ // releases on detach and detach needs close/error/idle-timeout. In ms; the
132+ // timer resets on every data event. The patch scopes the reap to connections
133+ // actually HOLDING affinity (open pipeline or transaction): an idle-at-rest
134+ // connection is the normal state of a healthy postgres.js pool held by a
135+ // long-lived scope (SSE), and reaping those raced live queries into
136+ // sporadic `write CONNECTION_ENDED` 500s.
137+ idleTimeout: Number(process.env.DEV_DB_IDLE_TIMEOUT_MS ?? 30_000),
138+ });
121139
140+let server = makeServer();
122141 await server.start();
123142 console.log(`[dev-db] Listening on postgresql://postgres:postgres@127.0.0.1:${PORT}/postgres`);
124143
144+let stopping = false;
145+
125146 const shutdown = async () => {
147+ stopping = true;
126148 console.log("\n[dev-db] Shutting down");
127149 await server.stop();
128150 await db.close();
@@ -131,3 +153,112 @@ const shutdown = async () => { · 2 unmodified lines
131153
132154 process.on("SIGINT", shutdown);
133155 process.on("SIGTERM", shutdown);
156+
157+// ---------------------------------------------------------------------------
158+// Wedge watchdog
159+// ---------------------------------------------------------------------------
160+//
161+// Twice the socket server has shipped a state machine that could stop
162+// answering NEW connections while the process, the port, and PGlite all stayed
163+// up (the CI e2e "cloud signIn: callback set no session (500)" cascades: every
164+// in-flight query dies once, then every fresh connection's startup packet
165+// times out — CONNECT_TIMEOUT — for the rest of the shard). The known paths
166+// are patched with regression tests, but each recurrence so far has found a
167+// new path, and a wedged front-end turns ONE infra hiccup into a failure of
168+// every remaining test in the shard.
169+//
170+// So: probe the server the way the app does — a fresh TCP connection, real
171+// startup handshake, `select 1` — and when several consecutive probes fail,
172+// dump the server's internals to the boot log and swap in a fresh socket
173+// server on the same PGlite instance (all state is in PGlite; the front-end is
174+// stateless, so this drops only already-doomed connections). If a restart
175+// doesn't restore service, exit non-zero: the boot supervisor logs the exit
176+// loudly and the run fails fast with an attributable cause instead of minutes
177+// of anonymous CONNECT_TIMEOUTs. The wedge itself stays visible in the
178+// server-logs artifact via the [dev-db][watchdog] lines.
179+const WATCHDOG_INTERVAL_MS = Number(process.env.DEV_DB_WATCHDOG_INTERVAL_MS ?? 5_000);
180+// 3 consecutive failures ≈ 15s+ of hard unavailability. PGlite serves queries
181+// in milliseconds; even a deep queue clears in well under one probe interval,
182+// so consecutive startup failures this sustained only happen wedged.
183+const WATCHDOG_FAILURES_TO_RESTART = 3;
184+const WATCHDOG_MAX_RESTARTS = 3;
185+
186+const probe = async (): Promise<void> => {
187+ const sql = postgres(`postgres://postgres:postgres@127.0.0.1:${PORT}/postgres`, {
188+ max: 1,
189+ idle_timeout: 0,
190+ connect_timeout: 5,
191+ fetch_types: false,
192+ prepare: false,
193+ onnotice: () => undefined,
194+ });
195+ try {
196+ // connect_timeout only bounds the handshake; race the query too so a
197+ // post-startup wedge cannot hang the watchdog itself.
198+ await Promise.race([
199+ sql.unsafe("select 1"),
200+ sleep(10_000).then(() => {
201+ throw new Error("probe query timed out after 10s");
202+ }),
203+ ]);
204+ } finally {
205+ await sql.end({ timeout: 5 }).catch(() => {});
206+ }
207+};
208+
209+const watchdog = async () => {
210+ let consecutiveFailures = 0;
211+ let restarts = 0;
212+ for (;;) {
213+ await sleep(WATCHDOG_INTERVAL_MS);
214+ if (stopping) return;
215+ try {
216+ await probe();
217+ consecutiveFailures = 0;
218+ } catch (cause) {
219+ consecutiveFailures += 1;
220+ console.error(
221+ `[dev-db][watchdog] probe failed (${consecutiveFailures}/${WATCHDOG_FAILURES_TO_RESTART}): ${String(cause)}`,
222+ );
223+ if (consecutiveFailures < WATCHDOG_FAILURES_TO_RESTART) continue;
224+ console.error(
225+ `[dev-db][watchdog] socket server wedged; stats: ${JSON.stringify(server.getStats())}`,
226+ );
227+ if (restarts >= WATCHDOG_MAX_RESTARTS) {
228+ console.error(
229+ `[dev-db][watchdog] still wedged after ${restarts} restarts — giving up so the boot supervisor reports it`,
230+ );
231+ process.exit(1);
232+ }
233+ restarts += 1;
234+ consecutiveFailures = 0;
235+ console.error(
236+ `[dev-db][watchdog] restarting socket server (${restarts}/${WATCHDOG_MAX_RESTARTS})`,
237+ );
238+ // stop() itself goes through the query queue (detach rolls back open
239+ // transactions), so a wedge deep enough can hang the restart too —
240+ // bound it and treat that as fatal rather than hanging the watchdog.
241+ const restart = async () => {
242+ await server.stop();
243+ server = makeServer();
244+ await server.start();
245+ };
246+ try {
247+ await Promise.race([
248+ restart(),
249+ sleep(15_000).then(() => {
250+ throw new Error("restart timed out after 15s");
251+ }),
252+ ]);
253+ console.error(`[dev-db][watchdog] socket server restarted`);
254+ } catch (restartCause) {
255+ console.error(
256+ `[dev-db][watchdog] restart failed (${String(restartCause)}) — exiting so the boot supervisor reports it`,
257+ );
258+ process.exit(1);
259+ }
260+ }
261+ }
262+};
263+
264+void watchdog();
apps/cloud/scripts/start-closure.mjs+124−0
@@ -0,0 +1,124 @@
1+// ---------------------------------------------------------------------------
2+// Measure the Worker's *evaluated* module closures from the build output.
3+// ---------------------------------------------------------------------------
4+//
5+// Upload size does not predict cold-isolate cost: a Worker can ship megabytes
6+// that never load, and the bytes that matter are the ones an isolate must
7+// evaluate before it can answer. Rollup already separates static from dynamic
8+// edges, so we can compute that split directly instead of reading totals.
9+//
10+// Two closures matter:
11+// startup - statically reachable from the Worker entry. Evaluated on every
12+// cold isolate before any request is served.
13+// start - the TanStack Start server graph, reached through the lazy
14+// `loadEntries` dynamic imports. Evaluated on the first request
15+// that enters the app - i.e. a page request.
16+// app - the Effect app plane. `/api/*` dispatches at the Worker entry and
17+// skips Start entirely, so an API request evaluates this instead of
18+// `start`; reported separately because the two planes now diverge.
19+//
20+// Anything reachable only through a dynamic import is not counted: making a
21+// heavy dependency lazy is exactly the outcome this rewards.
22+//
23+// Scope note: this measures bytes, which is a proxy for cold-start cost, not a
24+// proven cause of any particular regression. The Aug 2026 MCP incident was
25+// NOT explained by this number - reverting the offending packages moved the
26+// evaluated closure by 0.02 MB while restoring production latency, so
27+// module-scope execution cost, not size, drove that one. Treat a budget breach
28+// as "this will make cold starts worse", not as "this is why the site is slow".
29+import { readFileSync, readdirSync, statSync } from "node:fs";
30+import { dirname, join, relative, resolve } from "node:path";
31+
32+const DIST = resolve(process.argv[2] ?? "dist/server");
33+const ENTRY = join(DIST, "index.js");
34+
35+// Rollup emits `from "./x.js"`, bare `import "./x.js"`, `export ... from "./x.js"`
36+// (all static) and `import("./x.js")` (dynamic). Matching the emitted output
37+// rather than source means we see the graph as the runtime sees it.
38+const STATIC_RE = /(?:from|import)\s*["'](\.[^"']+)["']/g;
39+const DYNAMIC_RE = /import\(\s*["'](\.[^"']+)["']\s*\)/g;
40+
41+const listChunks = (dir) =>
42+ readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
43+ e.isDirectory()
44+ ? listChunks(join(dir, e.name))
45+ : e.name.endsWith(".js")
46+ ? [join(dir, e.name)]
47+ : [],
48+ );
49+
50+const graph = new Map();
51+for (const file of listChunks(DIST)) {
52+ const code = readFileSync(file, "utf8");
53+ const dynamic = new Set([...code.matchAll(DYNAMIC_RE)].map((m) => resolve(dirname(file), m[1])));
54+ // A specifier inside `import(...)` also matches STATIC_RE's `import` branch,
55+ // so subtract the dynamic set rather than trusting the static matches alone.
56+ const staticDeps = new Set(
57+ [...code.matchAll(STATIC_RE)]
58+ .map((m) => resolve(dirname(file), m[1]))
59+ .filter((p) => !dynamic.has(p)),
60+ );
61+ graph.set(file, { size: statSync(file).size, static: staticDeps, dynamic });
62+}
63+
64+/** Bytes evaluated when `roots` are loaded, following static edges only. */
65+const closure = (roots) => {
66+ const seen = new Set();
67+ const queue = [...roots];
68+ while (queue.length > 0) {
69+ const file = queue.pop();
70+ if (seen.has(file) || !graph.has(file)) continue;
71+ seen.add(file);
72+ queue.push(...graph.get(file).static);
73+ }
74+ return seen;
75+};
76+
77+const bytes = (files) => [...files].reduce((sum, f) => sum + (graph.get(f)?.size ?? 0), 0);
78+const mb = (n) => `${(n / 1024 / 1024).toFixed(2)} MB`;
79+const name = (f) => relative(DIST, f);
80+
81+const startup = closure([ENTRY]);
82+// The lazy server-graph entries Start pulls on first request.
83+const startRoots = [...graph.get(ENTRY).dynamic].filter((f) =>
84+ /(start|router|tanstack)/.test(name(f)),
85+);
86+const start = closure(startRoots);
87+const appRoots = [...graph.get(ENTRY).dynamic].filter((f) => /\/app-[A-Za-z0-9_-]+\.js$/.test(f));
88+const app = closure([ENTRY, ...appRoots]);
89+// The budget tracks the worst plane: whichever costs a cold isolate more.
90+const evaluated = new Set([...startup, ...start]);
91+
92+const report = (label, files) => {
93+ console.log(`\n${label}: ${mb(bytes(files))} (${files.size} chunks)`);
94+ const own = [...files].filter((f) => !startup.has(f) || label === "startup");
95+ for (const f of own.sort((a, b) => graph.get(b).size - graph.get(a).size).slice(0, 12)) {
96+ console.log(` ${(graph.get(f).size / 1024).toFixed(0).padStart(6)} KB ${name(f)}`);
97+ }
98+};
99+
100+report("startup", startup);
101+report("start", start);
102+console.log(`\npage request (startup + start): ${mb(bytes(evaluated))}`);
103+console.log(
104+ `API request (startup + app): ${mb(bytes(app))}${appRoots.length ? "" : " [no app chunk - /api still routes through Start]"}`,
105+);
106+const lazyOnly = [...graph.keys()].filter((f) => !evaluated.has(f));
107+console.log(
108+ `deferred behind dynamic import: ${mb(bytes(lazyOnly))} (${lazyOnly.length} chunks)`,
109+);
110+
111+const budget = Number(process.env.START_CLOSURE_BUDGET_MB ?? 0);
112+if (budget > 0) {
113+ const actual = bytes(evaluated) / 1024 / 1024;
114+ console.log(`\nbudget ${budget} MB — actual ${actual.toFixed(2)} MB`);
115+ if (actual > budget) {
116+ console.error(
117+ `\nFAIL: evaluated closure ${actual.toFixed(2)} MB exceeds the ${budget} MB budget.\n` +
118+ `Every cold isolate pays to evaluate this closure before it can answer. Move the\n` +
119+ `new weight behind a dynamic import rather than raising the budget; run this\n` +
120+ `script with no budget set to see the biggest members and what is already lazy.`,
121+ );
122+ process.exit(1);
123+ }
124+}
apps/cloud/scripts/test-globalsetup.ts+20−1
@@ -13,12 +13,26 @@ import { fileURLToPath } from "node:url"; · 12 unmodified lines
1313
1414 const __dirname = dirname(fileURLToPath(import.meta.url));
1515
16−const PORT = 5434;
16+const parsePort = (input: string | undefined): number => {
17+ if (input === undefined) return 5434;
18+ if (!/^\d+$/.test(input)) throw new Error("CLOUD_TEST_DB_PORT must be an integer");
19+ const port = Number(input);
20+ if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {
21+ throw new Error("CLOUD_TEST_DB_PORT must be between 1 and 65535");
22+ }
23+ return port;
24+};
25+
26+const PORT = parsePort(process.env.CLOUD_TEST_DB_PORT);
1727 const MIGRATIONS_FOLDER = resolve(__dirname, "../drizzle");
1828
1929 let db: PGlite | undefined;
2030 let server: PGLiteSocketServer | undefined;
2131
32+/**
33+ * Starts the cloud unit-test database and returns teardown that releases every
34+ * resource without allowing PGlite shutdown to erase Vitest's failure status.
35+ */
2236 export default async function setup() {
2337 db = await PGlite.create();
2438 await migrate(drizzle(db), { migrationsFolder: MIGRATIONS_FOLDER });
@@ -30,7 +44,12 @@ export default async function setup() { · 5 unmodified lines
3044 console.log(`[test-db] PGlite socket server listening on 127.0.0.1:${PORT}`);
3145
3246 return async () => {
47+ // PGlite sets an internal 99 sentinel on startup and replaces it with 0 on
48+ // close. Preserve Vitest's failure status across that close so
49+ // teardown can turn neither a red test green nor a green test red.
50+ const testsFailed = process.exitCode === 1;
3351 await server?.stop();
3452 await db?.close();
53+ if (testsFailed) process.exitCode = 1;
3554 };
3655 }
apps/cloud/scripts/vendor-quickjs-wasm.ts+18−0
@@ -0,0 +1,18 @@
1+// Vendors the QuickJS engine WASM into src/ so wrangler's CompiledWasm module
2+// rule (rooted at the app dir) can statically compile it at build time. Workers
3+// forbid runtime WASM compilation, and the rule's glob won't reach the
4+// monorepo-root node_modules, so the bytes must live inside this app.
5+//
6+// Re-run after bumping @jitl/quickjs-wasmfile-release-sync:
7+// bun run scripts/vendor-quickjs-wasm.ts
8+import { copyFileSync } from "node:fs";
9+import { createRequire } from "node:module";
10+import { dirname, join } from "node:path";
11+import { fileURLToPath } from "node:url";
12+
13+const require = createRequire(import.meta.url);
14+const source = require.resolve("@jitl/quickjs-wasmfile-release-sync/wasm");
15+const dest = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "quickjs-engine.wasm");
16+
17+copyFileSync(source, dest);
18+console.log(`vendored ${source} -> ${dest}`);
apps/cloud/src/account/org-api-key-revoke.node.test.ts+195−0
@@ -0,0 +1,195 @@
1+import { describe, expect, it } from "@effect/vitest";
2+import { Effect, Layer } from "effect";
3+
4+import { AccountProvider } from "@executor-js/api/server";
5+import { AccountError, AccountForbidden } from "@executor-js/api";
6+
7+import { ApiKeyService, OrgApiKeyNotFound } from "../auth/api-keys";
8+import { UserStoreService } from "../auth/context";
9+import { ORG_SELECTOR_HEADER } from "../auth/organization";
10+import { WorkOSClient, type WorkOSClientService } from "../auth/workos";
11+import { AutumnService } from "../extensions/billing/service";
12+import { AccountCaller, workosAccountProvider } from "./workos-account-service";
13+
14+// ---------------------------------------------------------------------------
15+// `DELETE /account/org-api-keys/:apiKeyId` at the PROVIDER boundary.
16+//
17+// An org key reads every user and connection in the tenant through `/admin/*`,
18+// so it is the most privileged credential the product issues. Two properties
19+// have to hold together, and only this seam sees both:
20+//
21+// 1. Revoke is gated on org ADMIN membership — the same gate as the mint. A
22+// plain member calling it gets 403 and no delete goes out.
23+// 2. Revoke resolves ownership against the ORG's keys. `revokeApiKey` (the
24+// personal path) checks the caller's OWN key list, so it can never revoke
25+// an org key; conversely this path must never destroy a member's personal
26+// key. The scope-blind `deleteApiKey` would do exactly that if handed a
27+// user key id, so a wrong id has to fail cleanly instead.
28+//
29+// Ownership filtering itself is pinned per-shape in
30+// `auth/org-api-key-mint.node.test.ts`; here it is asserted end-to-end through
31+// the gate, on the provider the HTTP handler actually calls.
32+// ---------------------------------------------------------------------------
33+
34+const ORG = "org_123";
35+const ADMIN = "user_admin";
36+const MEMBER = "user_member";
37+const ORG_KEY = "key_org_1";
38+const USER_KEY = "key_user_1";
39+const createdAt = new Date("2026-01-01T00:00:00.000Z");
40+const orgHeaders = { [ORG_SELECTOR_HEADER]: ORG };
41+
42+const session = (accountId: string) => ({
43+ accountId,
44+ email: `${accountId}@example.test`,
45+ name: null,
46+ avatarUrl: null,
47+ organizationId: ORG,
48+ sealedSession: "sealed",
49+ refreshedSession: null,
50+});
51+
52+/** Membership roles: only ADMIN carries the `admin` role slug. */
53+const stubWorkOS = Layer.succeed(
54+ WorkOSClient,
55+ new Proxy({} as WorkOSClientService, {
56+ get: (_target, prop) => {
57+ if (prop === "listUserMemberships") {
58+ return (userId: string) =>
59+ Effect.succeed({
60+ data: [{ userId, organizationId: ORG, status: "active" }],
61+ });
62+ }
63+ if (prop === "getUserOrgMembership") {
64+ return (organizationId: string, userId: string) =>
65+ Effect.succeed(
66+ organizationId === ORG
67+ ? {
68+ id: `om_${userId}`,
69+ userId,
70+ organizationId,
71+ role: { slug: userId === ADMIN ? "admin" : "member" },
72+ }
73+ : null,
74+ );
75+ }
76+ return () => Effect.die(`unexpected WorkOSClient.${String(prop)} call`);
77+ },
78+ }),
79+);
80+
81+const stubUsers = Layer.succeed(UserStoreService)({
82+ use: (_op, fn) =>
83+ Effect.promise(() =>
84+ fn({
85+ ensureAccount: async (id: string) => ({ id, createdAt }),
86+ getAccount: async (id: string) => ({ id, createdAt }),
87+ upsertOrganization: async (org: { id: string; name: string }) => ({
88+ ...org,
89+ slug: org.id,
90+ createdAt,
91+ }),
92+ getOrganization: async (id: string) => ({
93+ id,
94+ name: `Org ${id}`,
95+ slug: id,
96+ createdAt,
97+ }),
98+ getOrganizationBySlug: async (slug: string) => ({
99+ id: slug,
100+ name: `Org ${slug}`,
101+ slug,
102+ createdAt,
103+ }),
104+ deleteOrganizationCascade: async () => {},
105+ }),
106+ ),
107+});
108+
109+const stubAutumn = Layer.succeed(AutumnService)({
110+ use: () => Effect.die("revoke does not touch billing"),
111+ ensureCustomer: () => Effect.die("revoke does not touch billing"),
112+ checkExecutionBalance: () => Effect.die("revoke does not touch billing"),
113+ trackExecution: () => Effect.void,
114+});
115+
116+/**
117+ * The provider under test, over an `ApiKeyService` whose org listing holds a
118+ * single org-owned key. Revoked ids land in `revoked` so "no delete happened"
119+ * is assertable, not merely inferred from the error.
120+ */
121+const providerWith = (accountId: string) => {
122+ const revoked: string[] = [];
123+ const stubApiKeys = Layer.succeed(ApiKeyService)({
124+ validate: () => Effect.die("revoke test does not validate keys"),
125+ listUserKeys: () => Effect.succeed([]),
126+ createUserKey: () => Effect.die("revoke test does not create user keys"),
127+ revokeUserKey: () => Effect.die("the ORG path must not call the personal revoke"),
128+ listOrgKeys: () => Effect.die("revoke test does not list org keys"),
129+ createOrgKey: () => Effect.die("revoke test does not create org keys"),
130+ revokeOrgKey: ({ keyId }) =>
131+ keyId === ORG_KEY
132+ ? Effect.sync(() => {
133+ revoked.push(keyId);
134+ })
135+ : Effect.fail(new OrgApiKeyNotFound({ keyId })),
136+ });
137+
138+ const provider = AccountProvider.asEffect().pipe(
139+ Effect.provide(
140+ workosAccountProvider.pipe(
141+ Layer.provide(
142+ Layer.mergeAll(
143+ stubWorkOS,
144+ stubUsers,
145+ stubApiKeys,
146+ stubAutumn,
147+ Layer.succeed(AccountCaller)({ session: session(accountId) }),
148+ ),
149+ ),
150+ ),
151+ ),
152+ );
153+
154+ return { provider, revoked };
155+};
156+
157+describe("revokeOrgApiKey · provider boundary", () => {
158+ it.effect("an org admin revokes an org-owned key", () =>
159+ Effect.gen(function* () {
160+ const { provider, revoked } = providerWith(ADMIN);
161+ const account = yield* provider;
162+
163+ const result = yield* account.revokeOrgApiKey(orgHeaders, ORG_KEY);
164+
165+ expect(result).toEqual({ success: true });
166+ expect(revoked, "the revoke reached the key service").toEqual([ORG_KEY]);
167+ }),
168+ );
169+
170+ it.effect("a plain member is forbidden, and nothing is revoked", () =>
171+ Effect.gen(function* () {
172+ const { provider, revoked } = providerWith(MEMBER);
173+ const account = yield* provider;
174+
175+ const error = yield* Effect.flip(account.revokeOrgApiKey(orgHeaders, ORG_KEY));
176+
177+ expect(error, "same admin gate as the mint").toBeInstanceOf(AccountForbidden);
178+ expect(revoked, "the gate runs BEFORE the key service is touched").toEqual([]);
179+ }),
180+ );
181+
182+ it.effect("a user-scoped key id fails cleanly through this path", () =>
183+ Effect.gen(function* () {
184+ // Not found — not a 500, and emphatically not a silent success that would
185+ // report a member's live personal credential as destroyed.
186+ const { provider, revoked } = providerWith(ADMIN);
187+ const account = yield* provider;
188+
189+ const error = yield* Effect.flip(account.revokeOrgApiKey(orgHeaders, USER_KEY));
190+
191+ expect(error).toBeInstanceOf(AccountError);
192+ expect(revoked).toEqual([]);
193+ }),
194+ );
195+});
apps/cloud/src/account/workos-account-service.ts+65−8
@@ -91,16 +91,19 @@ export const workosAccountProvider: Layer.Layer< · 90 unmodified lines
9191 ? Effect.succeed(caller.session)
9292 : Effect.fail<AccountUnauthorized>(new AccountUnauthorized());
9393
94− // The org scope for an org-scoped request: the console URL's org (sent in
95− // the selector header) when present, else the session's own org. Membership
96− // is re-checked live, so the header is a selector, not a trust boundary —
94+ // The org scope for an org-scoped request: the console URL's org, sent in
95+ // the selector header. FAIL CLOSED when it's missing — the session's own
96+ // org is a browser-global pinned to whichever org WorkOS last touched, so
97+ // falling back to it scopes a multi-org user's request to the WRONG org
98+ // (see workos-auth-provider.resolveSessionPrincipal). Membership is
99+ // re-checked live, so the header is a selector, not a trust boundary —
97100 // and two browser tabs on different orgs each send their own header, so
98101 // they stay independent (see organization.ts). Yields the session +
99102 // resolved org, or AccountNoOrganization.
100103 const requireOrganization = (headers: AccountHeaders) =>
101104 Effect.gen(function* () {
102105 const session = yield* requireSession();
103− const selector = headers[ORG_SELECTOR_HEADER] ?? session.organizationId;
106+ const selector = headers[ORG_SELECTOR_HEADER];
104107 if (!selector) {
105108 return yield* new AccountNoOrganization();
106109 }
@@ -180,9 +183,15 @@ export const workosAccountProvider: Layer.Layer< · 73 unmodified lines
180183 me: (headers) =>
181184 Effect.gen(function* () {
182185 const session = yield* requireSession();
183− // Same selector precedence as requireOrganization: the URL's org
184− // (header) drives /account/me so the shell reflects the org the tab
185− // is viewing, not a session-global active org.
186+ // The ONE sanctioned fallback to the session org. /account/me is
187+ // identity, not tenant data: on a bare URL (no org slug yet, so no
188+ // header) it answers "which org should this browser land in", and
189+ // the session's pinned org is the only candidate. Every org-scoped
190+ // DATA read fails closed instead (requireOrganization above,
191+ // resolveSessionPrincipal) — serving tenant data from this fallback
192+ // is exactly the wrong-org connection-list bug (2026-07). With a
193+ // header (any slugged URL), the URL's org drives the answer so the
194+ // shell reflects the org the tab is viewing.
186195 const selector = headers[ORG_SELECTOR_HEADER] ?? session.organizationId;
187196 const org = selector
188197 ? yield* authorizeOrganizationSelector(session.accountId, selector).pipe(
@@ -237,6 +246,52 @@ export const workosAccountProvider: Layer.Layer< · 48 unmodified lines
237246 return { success: true };
238247 }),
239248
249+ // Org keys read every user in the tenant through the `/admin/*` plane, so
250+ // both paths are admin-gated — unlike personal keys, which any member may
251+ // mint for themselves.
252+ listOrgApiKeys: (headers) =>
253+ Effect.gen(function* () {
254+ const { session, org } = yield* requireOrganization(headers);
255+ yield* requireAdmin(session.accountId, org.id);
256+ const keys = yield* apiKeys
257+ .listOrgKeys({ organizationId: org.id })
258+ .pipe(Effect.catchTag("ApiKeyManagementError", toAccountError));
259+ return { apiKeys: keys };
260+ }),
261+
262+ createOrgApiKey: (headers, name) =>
263+ Effect.gen(function* () {
264+ const { session, org } = yield* requireOrganization(headers);
265+ yield* requireAdmin(session.accountId, org.id);
266+ const trimmed = name.trim().slice(0, MAX_API_KEY_NAME_LENGTH);
267+ if (!trimmed) {
268+ return yield* new AccountError({ message: "API key name is required" });
269+ }
270+ return yield* apiKeys
271+ .createOrgKey({ organizationId: org.id, name: trimmed })
272+ .pipe(Effect.catchTag("ApiKeyManagementError", toAccountError));
273+ }),
274+
275+ // Same admin gate as the mint. Note this deliberately does NOT reuse
276+ // `revokeApiKey`: that path resolves ownership against the caller's
277+ // personal keys, so an org key id is never in its owned set. Ownership
278+ // here is resolved against the ORG's keys inside `revokeOrgKey`, which is
279+ // what keeps a user-scoped (or foreign-org) key id from being destroyed
280+ // through the org route — it surfaces as a 404-shaped AccountError, not a
281+ // silent success and not a 500.
282+ revokeOrgApiKey: (headers, apiKeyId) =>
283+ Effect.gen(function* () {
284+ const { session, org } = yield* requireOrganization(headers);
285+ yield* requireAdmin(session.accountId, org.id);
286+ yield* apiKeys.revokeOrgKey({ organizationId: org.id, keyId: apiKeyId }).pipe(
287+ Effect.catchTag("ApiKeyManagementError", toAccountError),
288+ Effect.catchTag("OrgApiKeyNotFound", () =>
289+ Effect.fail(new AccountError({ message: "Organization API key not found" })),
290+ ),
291+ );
292+ return { success: true };
293+ }),
294+
240295 listMembers: (headers) =>
241296 Effect.gen(function* () {
242297 const { session, org } = yield* requireOrganization(headers);
@@ -331,7 +386,9 @@ export const workosAccountProvider: Layer.Layer< · 88 unmodified lines
331386 .updateOrganization(org.id, name)
332387 .pipe(Effect.catchTag("WorkOSError", toAccountError));
333388 yield* users
334− .use((s) => s.upsertOrganization({ id: updated.id, name: updated.name }))
389+ .use("upsertOrganization", (s) =>
390+ s.upsertOrganization({ id: updated.id, name: updated.name }),
391+ )
335392 .pipe(Effect.catchTag("UserStoreError", toAccountError));
336393 return { name: updated.name };
337394 }),
apps/cloud/src/admin/admin-users-api.ts+354−0
@@ -0,0 +1,354 @@
1+// ---------------------------------------------------------------------------
2+// Cloud admin users API — the shared, provider-neutral `AdminUsersHandlers`
3+// backed by a WorkOS-authorized platform view, mounted at `/api/admin/users*`.
4+//
5+// TWO credentials reach this plane, and they are the two an operator actually
6+// has:
7+// 1. an ORG-SCOPED api key -> `PlatformAuth`. The key IS the authority: WorkOS
8+// validated it and reported which org owns it, and there is no member
9+// behind it to check membership for. This is the machine credential
10+// (a customer's backend calling us).
11+// 2. an admin SESSION member -> the console. Requires a live `getUserOrgMembership`
12+// whose role slug is `admin` AND whose status is `active`, matching the
13+// strictest existing cloud guard (`auth/handlers.ts`'s org-delete check) —
14+// a pending admin invite is not an admin.
15+// A plain member session, or a USER-scoped api key, is refused: both name one
16+// acting member, and this plane deliberately serves the whole tenant.
17+//
18+// The executor is built by `makePlatformExecutor` — `{ tenant, subject:
19+// undefined, platformView: true }` — so the reads are tenant-wide and read-only
20+// by storage policy, and no `subject` row is minted for the caller.
21+//
22+// Cross-tenant isolation is structural, not a check in this file: the tenant is
23+// taken from the resolved credential (the key's own org, or the session's
24+// authorized org), never from client input, and `reach: "tenant"` still filters
25+// every query by that tenant.
26+// ---------------------------------------------------------------------------
27+
28+import { env } from "cloudflare:workers";
29+import { HttpRouter } from "effect/unstable/http";
30+import { Context, Effect, Layer, Option } from "effect";
31+
32+import {
33+ AdminUsersProvider,
34+ DbProvider,
35+ HostConfig,
36+ PluginsProvider,
37+ getAdminUser,
38+ listAdminUserConnections,
39+ listAdminUsers,
40+ listAdminUsersWithConnections,
41+ makeAdminUsersApiLayer,
42+ makePlatformExecutor,
43+ normalizeAdminUserEmail,
44+ platformViewOf,
45+ requestScopedMiddleware,
46+ type AdminEmailResolver,
47+ type AdminIdentityDirectory,
48+ type AdminUserDirectory,
49+ type AdminUserIdentity,
50+ type AdminUsersHeaders,
51+} from "@executor-js/api/server";
52+import {
53+ AdminUserNotFound,
54+ AdminUsersError,
55+ AdminUsersForbidden,
56+ AdminUsersUnauthorized,
57+} from "@executor-js/api";
58+import type { Executor } from "@executor-js/sdk";
59+
60+import { ApiKeyService } from "../auth/api-keys";
61+import { UserStoreService } from "../auth/context";
62+import { isPlatformAuth, resolveBearerAuth } from "../auth/workos-auth-provider";
63+import { orgSelectorFromRequest, authorizeOrganizationSelector } from "../auth/organization";
64+import { WorkOSClient } from "../auth/workos";
65+import { DbService } from "../db/db";
66+import { CloudExecutionSeamsLayer } from "../engine/execution-stack";
67+
68+/**
69+ * Resolve the tenant this request may read, or fail with the neutral 401/403.
70+ *
71+ * Returns only the organization id: nothing downstream needs to know WHICH of
72+ * the two credentials got the caller here, and keeping the acting member out of
73+ * the return value means no admin read can accidentally become subject-scoped.
74+ */
75+const authorizeTenant = (
76+ request: Request,
77+): Effect.Effect<
78+ string,
79+ AdminUsersUnauthorized | AdminUsersForbidden,
80+ WorkOSClient | ApiKeyService | UserStoreService
81+> =>
82+ Effect.gen(function* () {
83+ // (1) The bearer path. `resolveBearerAuth` (not `resolveApiKeyPrincipal`,
84+ // which rejects org keys for the product plane) is what distinguishes an
85+ // org key from a user key.
86+ const bearer = yield* resolveBearerAuth(request).pipe(
87+ // Every rejected-credential and infra failure collapses to one refusal:
88+ // this plane must not report whether a key exists, belongs to another
89+ // org, or merely lacks privilege.
90+ Effect.catchCause(() => Effect.succeed(null)),
91+ );
92+ if (bearer !== null) {
93+ if (isPlatformAuth(bearer)) return bearer.organizationId;
94+ // A user-scoped key authenticated fine but names one member; the platform
95+ // plane has no honest way to serve it.
96+ return yield* new AdminUsersForbidden();
97+ }
98+
99+ // (2) The session path: a live admin membership in the selected org.
100+ const workos = yield* WorkOSClient;
101+ const session = yield* workos
102+ .authenticateRequest(request)
103+ .pipe(Effect.catchCause(() => Effect.succeed(null)));
104+ if (!session) return yield* new AdminUsersUnauthorized();
105+
106+ const selector = orgSelectorFromRequest(request) ?? session.organizationId;
107+ if (!selector) return yield* new AdminUsersForbidden();
108+ // Re-checks live membership, so the org selector header can only ever name
109+ // an org the caller already belongs to.
110+ const org = yield* authorizeOrganizationSelector(session.userId, selector).pipe(
111+ Effect.catchCause(() => Effect.succeed(null)),
112+ );
113+ if (!org) return yield* new AdminUsersForbidden();
114+
115+ const membership = yield* workos
116+ .getUserOrgMembership(org.id, session.userId)
117+ .pipe(Effect.catchCause(() => Effect.succeed(null)));
118+ // A pending admin invite is not an active admin — require both.
119+ if (!membership || membership.status !== "active" || membership.role?.slug !== "admin") {
120+ return yield* new AdminUsersForbidden();
121+ }
122+ return org.id;
123+ });
124+
125+/**
126+ * How many user-detail reads run at once. Matches the account plane's own
127+ * member listing (`workos-account-service.ts`), which fans out the same way for
128+ * the same reason.
129+ */
130+const IDENTITY_CONCURRENCY = 5;
131+
132+/**
133+ * Cloud's member directory: `externalId` → email/name.
134+ *
135+ * THE JOIN KEY is the membership's `userId` — the WorkOS `user_...` that
136+ * `workos-auth-provider.ts` binds as `accountId` on every credential path, and
137+ * therefore what the subject table records in `external_id`. The membership's
138+ * own `id` is an `om_...` row id and joins to nothing.
139+ *
140+ * WHY THIS IS TWO CALLS AND NOT ONE. The membership list is read once per
141+ * request and is the authority on who belongs to the org, but WorkOS's
142+ * `listOrganizationMemberships` carries no user detail and offers no
143+ * include/expand — email and name only exist on the user resource. The SDK does
144+ * expose a batched `listUsers({ organizationId })`, but the pinned
145+ * `@executor-js/emulate` WorkOS emulator serves only `GET
146+ * /user_management/users/:id`, so taking that path would leave every cloud e2e
147+ * user unnamed. So: ONE membership read per request, then user detail fetched
148+ * only for the ids ON THIS PAGE — never for the whole org, and never once per
149+ * row of some larger list. An id that is not an active/pending member is not
150+ * fetched at all and reports absent identity, which is the honest answer for a
151+ * member who left while their connections remain.
152+ */
153+const identityDirectory =
154+ (organizationId: string, context: Context.Context<WorkOSClient>): AdminIdentityDirectory =>
155+ (externalIds) =>
156+ Effect.gen(function* () {
157+ const workos = yield* WorkOSClient;
158+ const memberships = yield* workos.listOrgMembers(organizationId);
159+ const wanted = new Set(externalIds);
160+ const memberIds = memberships.data
161+ .map((membership) => membership.userId)
162+ .filter((userId) => wanted.has(userId));
163+
164+ const resolved = yield* Effect.all(
165+ memberIds.map((userId) =>
166+ workos.getUser(userId).pipe(
167+ Effect.map(
168+ (user) =>
169+ [
170+ userId,
171+ {
172+ email: user.email,
173+ displayName: [user.firstName, user.lastName].filter(Boolean).join(" ") || null,
174+ },
175+ ] as const,
176+ ),
177+ // One unreadable user must not cost the whole page its names.
178+ Effect.catchCause(() => Effect.succeed(null)),
179+ ),
180+ ),
181+ { concurrency: IDENTITY_CONCURRENCY },
182+ );
183+
184+ const identities = new Map<string, AdminUserIdentity>();
185+ for (const entry of resolved) if (entry) identities.set(entry[0], entry[1]);
186+ return identities;
187+ }).pipe(Effect.provideContext(context));
188+
189+/**
190+ * Cloud's REVERSE directory lookup: email → the WorkOS `user_...` id.
191+ *
192+ * Production asks WorkOS for the email AND organization in one request. Both
193+ * filters matter: email makes the lookup indexed rather than one `getUser`
194+ * request per member, while organization keeps the reverse lookup bound to the
195+ * same tenant as the platform view.
196+ *
197+ * The pinned `@executor-js/emulate` WorkOS emulator has no list-users route.
198+ * `WORKOS_API_URL` is the explicit test/dev emulator override, so that path
199+ * retains the membership scan until the emulator supports the production
200+ * query. The fallback still starts from the tenant's membership list and can
201+ * never return a user from another organization.
202+ *
203+ * CASING: WorkOS preserves whatever casing an email was created with (and the
204+ * emulator compares byte-exact), so the directory value is normalized here
205+ * before comparison, against an argument the seam already normalized.
206+ */
207+export const emailResolver =
208+ (organizationId: string, context: Context.Context<WorkOSClient>): AdminEmailResolver =>
209+ (email) =>
210+ Effect.gen(function* () {
211+ const workos = yield* WorkOSClient;
212+
213+ if (!env.WORKOS_API_URL) {
214+ const users = yield* workos.listUsers({ email, organizationId });
215+ return users.data[0]?.id ?? null;
216+ }
217+
218+ const memberships = yield* workos.listOrgMembers(organizationId);
219+ const userIds = memberships.data.map((membership) => membership.userId);
220+
221+ // Emulator compatibility only. Short-circuit once the normalized email
222+ // matches so the fallback makes as few unsupported-detail reads as it can.
223+ const match = yield* Effect.findFirst(userIds, (userId) =>
224+ workos.getUser(userId).pipe(
225+ Effect.map((user) => normalizeAdminUserEmail(user.email ?? "") === email),
226+ // One unreadable user must not fail the whole lookup — it simply
227+ // cannot be the match.
228+ Effect.catchCause(() => Effect.succeed(false)),
229+ ),
230+ );
231+ return Option.getOrNull(match);
232+ }).pipe(Effect.provideContext(context));
233+
234+/** Both directions of cloud's directory, built once per authorized request. */
235+const userDirectory = (
236+ organizationId: string,
237+ context: Context.Context<WorkOSClient>,
238+): AdminUserDirectory => ({
239+ identities: identityDirectory(organizationId, context),
240+ resolveEmail: emailResolver(organizationId, context),
241+});
242+
243+/**
244+ * Authorize, then run `body` against the tenant's platform view.
245+ *
246+ * The executor is built per request and closed after, like every other cloud
247+ * execution stack: it holds the per-request postgres socket, which Cloudflare's
248+ * I/O isolation forbids sharing across requests.
249+ */
250+const withPlatformView = <A, E extends AdminUsersError | AdminUserNotFound = AdminUsersError>(
251+ headers: AdminUsersHeaders,
252+ body: (executor: Executor, organizationId: string) => Effect.Effect<A, E>,
253+): Effect.Effect<
254+ A,
255+ // `AdminUsersError` unconditionally: opening the platform view can fail that
256+ // way regardless of what `body` itself raises.
257+ E | AdminUsersError | AdminUsersUnauthorized | AdminUsersForbidden,
258+ WorkOSClient | ApiKeyService | UserStoreService | DbProvider | PluginsProvider | HostConfig
259+> =>
260+ Effect.gen(function* () {
261+ const organizationId = yield* authorizeTenant(
262+ new Request("https://admin.invalid", { headers }),
263+ );
264+ const executor = yield* makePlatformExecutor(organizationId).pipe(
265+ Effect.mapError(() => new AdminUsersError({ message: "Failed to open the platform view" })),
266+ );
267+ // The authorized tenant is handed to the body so an identity join reads the
268+ // SAME org the reads are scoped to — never one named by client input.
269+ return yield* Effect.ensuring(
270+ body(executor, organizationId),
271+ executor.close().pipe(Effect.ignore),
272+ );
273+ });
274+
275+/**
276+ * Cloud's `AdminUsersProvider`, built per request so the platform executor
277+ * closes over the per-request postgres socket.
278+ */
279+export const workosAdminUsersProvider: Layer.Layer<
280+ AdminUsersProvider,
281+ never,
282+ WorkOSClient | ApiKeyService | UserStoreService | DbProvider | PluginsProvider | HostConfig
283+> = Layer.effect(AdminUsersProvider)(
284+ Effect.gen(function* () {
285+ const context = yield* Effect.context<
286+ WorkOSClient | ApiKeyService | UserStoreService | DbProvider | PluginsProvider | HostConfig
287+ >();
288+ return AdminUsersProvider.of({
289+ listUsers: (headers, options) =>
290+ withPlatformView(headers, (executor, organizationId) =>
291+ platformViewOf(executor).pipe(
292+ Effect.flatMap((admin) =>
293+ listAdminUsers(admin, options, userDirectory(organizationId, context)),
294+ ),
295+ ),
296+ ).pipe(Effect.provideContext(context)),
297+ listUsersWithConnections: (headers, options) =>
298+ withPlatformView(headers, (executor, organizationId) =>
299+ platformViewOf(executor).pipe(
300+ Effect.flatMap((admin) =>
301+ listAdminUsersWithConnections(admin, options, userDirectory(organizationId, context)),
302+ ),
303+ ),
304+ ).pipe(Effect.provideContext(context)),
305+ listUserConnections: (headers, externalId) =>
306+ withPlatformView(headers, (executor) =>
307+ platformViewOf(executor).pipe(
308+ Effect.flatMap((admin) => listAdminUserConnections(admin, externalId)),
309+ ),
310+ ).pipe(Effect.provideContext(context)),
311+ getUser: (headers, identifier) =>
312+ withPlatformView(headers, (executor, organizationId) =>
313+ platformViewOf(executor).pipe(
314+ Effect.flatMap((admin) =>
315+ getAdminUser(admin, identifier, userDirectory(organizationId, context)),
316+ ),
317+ ),
318+ ).pipe(Effect.provideContext(context)),
319+ });
320+ }),
321+);
322+
323+// Builds the provider per request, providing it to the handlers. Long-lived
324+// `WorkOSClient | ApiKeyService` come from the surrounding boot context; the
325+// per-request `DbService`/`UserStoreService` (and the execution seams built
326+// over them) are supplied by the combined `requestScopedMiddleware`.
327+const AdminUsersProviderMiddleware = HttpRouter.middleware<{ provides: AdminUsersProvider }>()(
328+ Effect.gen(function* () {
329+ const longLived = yield* Effect.context<WorkOSClient | ApiKeyService>();
330+ return (httpEffect) =>
331+ Effect.gen(function* () {
332+ // Built inside the request body so the execution seams close over the
333+ // per-request postgres socket.
334+ const provider = yield* Effect.provide(
335+ AdminUsersProvider.asEffect(),
336+ workosAdminUsersProvider.pipe(Layer.provide(CloudExecutionSeamsLayer)),
337+ );
338+ return yield* Effect.provideService(httpEffect, AdminUsersProvider, provider);
339+ }).pipe(Effect.provideContext(longLived));
340+ }),
341+);
342+
343+/**
344+ * The cloud admin-users route layer, mounted as an app extension under the same
345+ * `/api` prefix as the rest of the cloud router.
346+ */
347+export const makeCloudAdminUsersRoutes = (
348+ rsLive: Layer.Layer<DbService | UserStoreService>,
349+ options: Parameters<typeof makeAdminUsersApiLayer>[1] = {},
350+) =>
351+ makeAdminUsersApiLayer(
352+ AdminUsersProviderMiddleware.combine(requestScopedMiddleware(rsLive)).layer,
353+ options,
354+ );
apps/cloud/src/admin/admin-users-email.node.test.ts+127−0
@@ -0,0 +1,127 @@
1+import { env } from "cloudflare:workers";
2+import { expect, it } from "@effect/vitest";
3+import { Data, Effect, Layer } from "effect";
4+
5+import { WorkOSClient, type WorkOSClientService } from "../auth/workos";
6+import { emailResolver } from "./admin-users-api";
7+
8+// Cloud's REVERSE directory lookup: email -> the WorkOS `user_...` id that the
9+// subject table records in `external_id`. Production resolves it with one
10+// tenant-scoped list-users query. The WorkOS emulator lacks that route, so
11+// tests/dev retain the membership-backed scan exercised below.
12+
13+const ORG = "org_placeholder";
14+const OTHER_ORG = "org_other";
15+
16+class WorkOSUnavailable extends Data.TaggedError("WorkOSUnavailable")<{
17+ readonly userId: string;
18+}> {}
19+
20+const DIRECTORY = [
21+ // Same email in another tenant must never win either lookup path.
22+ { id: "user_foreign", email: "ada@placeholder.test", organizationId: OTHER_ORG },
23+ // WorkOS preserves submitted casing, while the resolver seam is normalized.
24+ { id: "user_ada", email: "Ada@Placeholder.test", organizationId: ORG },
25+ { id: "user_grace", email: "grace@placeholder.test", organizationId: ORG },
26+ { id: "user_nameless", email: null, organizationId: ORG },
27+] as const;
28+
29+const stubWorkOS = (calls: string[], unreadableUserIds: ReadonlySet<string>) =>
30+ Layer.succeed(
31+ WorkOSClient,
32+ new Proxy({} as WorkOSClientService, {
33+ get: (_target, prop) => {
34+ if (prop === "listUsers") {
35+ return (params: { email: string; organizationId: string }) => {
36+ calls.push(`listUsers:${params.organizationId}:${params.email}`);
37+ return Effect.succeed({
38+ data: DIRECTORY.filter(
39+ (user) =>
40+ user.organizationId === params.organizationId &&
41+ user.email?.toLowerCase() === params.email,
42+ ),
43+ });
44+ };
45+ }
46+ if (prop === "listOrgMembers") {
47+ return (organizationId: string) => {
48+ calls.push(`listOrgMembers:${organizationId}`);
49+ return Effect.succeed({
50+ data: DIRECTORY.filter((user) => user.organizationId === organizationId).map(
51+ (user) => ({ userId: user.id, organizationId }),
52+ ),
53+ });
54+ };
55+ }
56+ if (prop === "getUser") {
57+ return (userId: string) => {
58+ calls.push(`getUser:${userId}`);
59+ if (unreadableUserIds.has(userId)) {
60+ return Effect.fail(new WorkOSUnavailable({ userId }));
61+ }
62+ const user = DIRECTORY.find((candidate) => candidate.id === userId);
63+ if (!user) return Effect.die(`unexpected user ${userId}`);
64+ return Effect.succeed(user);
65+ };
66+ }
67+ return () => Effect.die(`unexpected WorkOSClient.${String(prop)} call`);
68+ },
69+ }),
70+ );
71+
72+const resolve = (
73+ email: string,
74+ calls: string[],
75+ emulator = false,
76+ unreadableUserIds: ReadonlySet<string> = new Set(),
77+) => {
78+ const previousApiUrl = env.WORKOS_API_URL;
79+ return Effect.gen(function* () {
80+ yield* Effect.sync(() =>
81+ Object.assign(env, {
82+ WORKOS_API_URL: emulator ? "http://workos-emulator.invalid" : undefined,
83+ }),
84+ );
85+ const context = yield* Effect.context<WorkOSClient>();
86+ return yield* emailResolver(ORG, context)(email);
87+ }).pipe(
88+ Effect.provide(stubWorkOS(calls, unreadableUserIds)),
89+ Effect.ensuring(Effect.sync(() => Object.assign(env, { WORKOS_API_URL: previousApiUrl }))),
90+ );
91+};
92+
93+it.effect("resolves an email with one tenant-scoped WorkOS query", () =>
94+ Effect.gen(function* () {
95+ const calls: string[] = [];
96+ expect(yield* resolve("ada@placeholder.test", calls)).toBe("user_ada");
97+ expect(calls).toEqual([`listUsers:${ORG}:ada@placeholder.test`]);
98+ }),
99+);
100+
101+it.effect("returns null from one query when the organization has no matching email", () =>
102+ Effect.gen(function* () {
103+ const calls: string[] = [];
104+ expect(yield* resolve("nobody@placeholder.test", calls)).toBeNull();
105+ expect(calls).toEqual([`listUsers:${ORG}:nobody@placeholder.test`]);
106+ }),
107+);
108+
109+it.effect("keeps the emulator fallback tenant-scoped and case-insensitive", () =>
110+ Effect.gen(function* () {
111+ const calls: string[] = [];
112+ expect(yield* resolve("ada@placeholder.test", calls, true)).toBe("user_ada");
113+ expect(calls).toEqual([`listOrgMembers:${ORG}`, "getUser:user_ada"]);
114+ expect(calls).not.toContain("getUser:user_foreign");
115+ expect(calls.some((call) => call.startsWith("listUsers:"))).toBe(false);
116+ }),
117+);
118+
119+it.effect("lets the emulator fallback continue past one unreadable member", () =>
120+ Effect.gen(function* () {
121+ const calls: string[] = [];
122+ expect(yield* resolve("grace@placeholder.test", calls, true, new Set(["user_ada"]))).toBe(
123+ "user_grace",
124+ );
125+ expect(calls).toEqual([`listOrgMembers:${ORG}`, "getUser:user_ada", "getUser:user_grace"]);
126+ }),
127+);
apps/cloud/src/analytics/ema-rollout.test.ts+394−0
@@ -0,0 +1,394 @@
1+// ---------------------------------------------------------------------------
2+// Cloud's PostHog-backed enterprise-managed rollout gate.
3+//
4+// The gate is exercised through its production seam: the real
5+// `makePostHogEnterpriseManagedRollout` with an injected `fetch` and an
6+// injected `waitUntil`, exactly as `auth/jwks-cache.node.test.ts` drives the
7+// JWKS client. Nothing is module-mocked, so the request these tests read is the
8+// request PostHog would receive.
9+// ---------------------------------------------------------------------------
10+
11+import { describe, expect, it } from "@effect/vitest";
12+import { Effect } from "effect";
13+
14+import type { EnterpriseManagedRolloutContext } from "@executor-js/sdk";
15+
16+import {
17+ ENTERPRISE_MANAGED_AUTH_FLAG_KEY,
18+ cloudEnterpriseManagedRollout,
19+ makePostHogEnterpriseManagedRollout,
20+} from "./ema-rollout";
21+
22+const HOST = "https://us.i.posthog.com";
23+const PROJECT_KEY = "phc_test_project_key";
24+
25+const CONTEXT: EnterpriseManagedRolloutContext = {
26+ userId: "user_01ABC",
27+ organizationId: "org_01XYZ",
28+ integration: "slack" as EnterpriseManagedRolloutContext["integration"],
29+};
30+
31+interface CapturedRequest {
32+ readonly url: string;
33+ readonly body: Record<string, unknown>;
34+ /** The bound the gate put on this call, when it set one. */
35+ readonly signal: AbortSignal | null;
36+}
37+
38+interface FetchHarness {
39+ readonly fetch: typeof globalThis.fetch;
40+ readonly requests: () => readonly CapturedRequest[];
41+ /** Resolve every request the gate detached, so an assertion cannot race it. */
42+ readonly settle: () => Promise<void>;
43+ readonly waitUntil: (work: Promise<unknown>) => void;
44+}
45+
46+/**
47+ * Stands in for PostHog. `respond` decides what each call returns; throwing
48+ * from it models a transport failure, and an aborted signal models the timeout.
49+ */
50+const makeFetchHarness = (
51+ respond: (request: CapturedRequest) => Response | Promise<Response>,
52+): FetchHarness => {
53+ const requests: CapturedRequest[] = [];
54+ const detached: Promise<unknown>[] = [];
55+ return {
56+ requests: () => requests,
57+ settle: async () => {
58+ await Promise.all(detached);
59+ },
60+ waitUntil: (work) => {
61+ detached.push(work);
62+ },
63+ fetch: async (input, init) => {
64+ const raw = typeof init?.body === "string" ? init.body : "{}";
65+ const captured: CapturedRequest = {
66+ url: String(input),
67+ // oxlint-disable-next-line executor/no-json-parse -- boundary: test fixture reads back the exact JSON body the gate serialized for PostHog
68+ body: JSON.parse(raw) as Record<string, unknown>,
69+ signal: init?.signal instanceof AbortSignal ? init.signal : null,
70+ };
71+ requests.push(captured);
72+ return respond(captured);
73+ },
74+ };
75+};
76+
77+const jsonResponse = (body: unknown, status = 200): Response =>
78+ new Response(JSON.stringify(body), {
79+ status,
80+ headers: { "content-type": "application/json" },
81+ });
82+
83+const flagsBody = (enabled: boolean) => ({
84+ flags: {
85+ [ENTERPRISE_MANAGED_AUTH_FLAG_KEY]: {
86+ key: ENTERPRISE_MANAGED_AUTH_FLAG_KEY,
87+ enabled,
88+ reason: { code: "condition_match" },
89+ },
90+ },
91+ errorsWhileComputingFlags: false,
92+});
93+
94+const gate = (harness: FetchHarness, timeoutMs?: number) =>
95+ makePostHogEnterpriseManagedRollout({
96+ projectKey: PROJECT_KEY,
97+ host: HOST,
98+ fetch: harness.fetch,
99+ waitUntil: harness.waitUntil,
100+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
101+ });
102+
103+describe("flag evaluation", () => {
104+ it.effect("keys the rollout on the user and carries the org as group context", () =>
105+ Effect.gen(function* () {
106+ const harness = makeFetchHarness(() => jsonResponse(flagsBody(true)));
107+
108+ const decision = yield* gate(harness).decide(CONTEXT);
109+
110+ expect(decision).toEqual({ kind: "enabled" });
111+ const [request] = harness.requests();
112+ expect(request?.url).toBe(`${HOST}/flags?v=2`);
113+ expect(
114+ request?.body.distinct_id,
115+ "the rollout unit is the user, matching posthog.identify(user.id) in the browser",
116+ ).toBe(CONTEXT.userId);
117+ expect(
118+ request?.body.groups,
119+ "the org rides along so group targeting stays available without changing the rollout unit",
120+ ).toEqual({ organization: CONTEXT.organizationId });
121+ expect(request?.body.api_key).toBe(PROJECT_KEY);
122+ expect(request?.signal, "the evaluation is always bounded").not.toBeNull();
123+ }),
124+ );
125+
126+ it.effect("omits group context when the host named no organization", () =>
127+ Effect.gen(function* () {
128+ const harness = makeFetchHarness(() => jsonResponse(flagsBody(true)));
129+
130+ yield* gate(harness).decide({ ...CONTEXT, organizationId: null });
131+
132+ expect(harness.requests()[0]?.body.groups).toBeUndefined();
133+ }),
134+ );
135+
136+ it.effect("withholds when the flag is off for this user", () =>
137+ Effect.gen(function* () {
138+ const harness = makeFetchHarness(() => jsonResponse(flagsBody(false)));
139+
140+ expect(yield* gate(harness).decide(CONTEXT)).toEqual({
141+ kind: "withheld",
142+ reason: "disabled",
143+ });
144+ }),
145+ );
146+
147+ it.effect("withholds when PostHog returned no verdict for the flag at all", () =>
148+ Effect.gen(function* () {
149+ const harness = makeFetchHarness(() =>
150+ // What a quota-limited project answers.
151+ jsonResponse({
152+ flags: {},
153+ errorsWhileComputingFlags: false,
154+ quotaLimited: ["feature_flags"],
155+ }),
156+ );
157+
158+ expect(yield* gate(harness).decide(CONTEXT)).toEqual({
159+ kind: "withheld",
160+ reason: "disabled",
161+ });
162+ }),
163+ );
164+});
165+
166+describe("failing closed", () => {
167+ it.effect("withholds on a non-2xx answer", () =>
168+ Effect.gen(function* () {
169+ const harness = makeFetchHarness(() => new Response("nope", { status: 503 }));
170+
171+ expect(yield* gate(harness).decide(CONTEXT)).toEqual({
172+ kind: "withheld",
173+ reason: "evaluation-unavailable",
174+ });
175+ }),
176+ );
177+
178+ it.effect("withholds on a transport failure", () =>
179+ Effect.gen(function* () {
180+ const harness = makeFetchHarness(() => {
181+ // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: models `fetch` rejecting with a TypeError, which is exactly how a Worker sees a dead upstream
182+ throw new TypeError("network error");
183+ });
184+
185+ expect(yield* gate(harness).decide(CONTEXT)).toEqual({
186+ kind: "withheld",
187+ reason: "evaluation-unavailable",
188+ });
189+ }),
190+ );
191+
192+ it.effect("withholds when the evaluation times out", () =>
193+ Effect.gen(function* () {
194+ // Never answers. Only the gate's own AbortSignal ends this call, so the
195+ // test fails by hanging if the gate ever stops bounding the evaluation.
196+ const harness = makeFetchHarness(
197+ (request) =>
198+ new Promise<Response>((_resolve, reject) => {
199+ request.signal?.addEventListener("abort", () =>
200+ // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: fetch-compatible fixture mirrors the platform's abort rejection semantics
201+ reject(new DOMException("aborted", "AbortError")),
202+ );
203+ }),
204+ );
205+
206+ expect(
207+ yield* gate(harness, 1).decide(CONTEXT),
208+ "a slow flag service must degrade the rollout, never hold up a connect",
209+ ).toEqual({ kind: "withheld", reason: "evaluation-unavailable" });
210+ }),
211+ );
212+
213+ it.effect("withholds on a body that is not a flags response", () =>
214+ Effect.gen(function* () {
215+ const harness = makeFetchHarness(() => jsonResponse({ flags: "not-an-object" }));
216+
217+ expect(yield* gate(harness).decide(CONTEXT)).toEqual({
218+ kind: "withheld",
219+ reason: "evaluation-unavailable",
220+ });
221+ }),
222+ );
223+
224+ it.effect("withholds when there is no acting user to roll out by", () =>
225+ Effect.gen(function* () {
226+ const harness = makeFetchHarness(() => jsonResponse(flagsBody(true)));
227+
228+ expect(yield* gate(harness).decide({ ...CONTEXT, userId: null })).toEqual({
229+ kind: "withheld",
230+ reason: "evaluation-unavailable",
231+ });
232+ expect(harness.requests().length, "and asks PostHog nothing").toBe(0);
233+ }),
234+ );
235+
236+ it.effect("withholds when the deployment has no PostHog configuration", () =>
237+ Effect.gen(function* () {
238+ // The cloud test env carries no VITE_PUBLIC_POSTHOG_KEY, so this builds
239+ // the misconfigured-deployment gate — which is deliberately NOT the same
240+ // as a host that injects no gate at all.
241+ expect(yield* cloudEnterpriseManagedRollout().decide(CONTEXT)).toEqual({
242+ kind: "withheld",
243+ reason: "evaluation-unavailable",
244+ });
245+ }),
246+ );
247+});
248+
249+describe("rollout events", () => {
250+ it.effect("captures the connect outcome with the flag decision attached", () =>
251+ Effect.gen(function* () {
252+ const harness = makeFetchHarness(() => jsonResponse({ status: 1 }));
253+ const rollout = gate(harness);
254+
255+ yield* rollout.record({
256+ kind: "connected",
257+ context: CONTEXT,
258+ decision: { kind: "enabled" },
259+ });
260+ yield* Effect.promise(() => harness.settle());
261+
262+ const [request] = harness.requests();
263+ expect(request?.url).toBe(`${HOST}/i/v0/e/`);
264+ expect(request?.body.event).toBe("ema_connect_connected");
265+ expect(request?.body.distinct_id).toBe(CONTEXT.userId);
266+ const properties = request?.body.properties as Record<string, unknown>;
267+ expect(properties.ema_flag_enabled).toBe(true);
268+ expect(properties[`$feature/${ENTERPRISE_MANAGED_AUTH_FLAG_KEY}`]).toBe(true);
269+ expect(properties.$groups).toEqual({
270+ organization: CONTEXT.organizationId,
271+ });
272+ expect(properties.integration_slug).toBe("slack");
273+ }),
274+ );
275+
276+ it.effect("carries the withheld reason on the attempt that never ran", () =>
277+ Effect.gen(function* () {
278+ const harness = makeFetchHarness(() => jsonResponse({ status: 1 }));
279+
280+ yield* gate(harness).record({
281+ kind: "attempted",
282+ context: CONTEXT,
283+ decision: { kind: "withheld", reason: "evaluation-unavailable" },
284+ });
285+ yield* Effect.promise(() => harness.settle());
286+
287+ const properties = harness.requests()[0]?.body.properties as Record<string, unknown>;
288+ expect(harness.requests()[0]?.body.event).toBe("ema_connect_attempted");
289+ expect(properties.ema_flag_enabled).toBe(false);
290+ expect(properties.ema_flag_withheld_reason).toBe("evaluation-unavailable");
291+ }),
292+ );
293+
294+ it.effect("carries the identity provider's own error code on a blocked connect", () =>
295+ Effect.gen(function* () {
296+ const harness = makeFetchHarness(() => jsonResponse({ status: 1 }));
297+
298+ yield* gate(harness).record({
299+ kind: "blocked-by-admin",
300+ context: CONTEXT,
301+ decision: { kind: "enabled" },
302+ oauthErrorCode: "unauthorized_client",
303+ });
304+ yield* Effect.promise(() => harness.settle());
305+
306+ const [request] = harness.requests();
307+ expect(request?.body.event).toBe("ema_connect_blocked_by_admin");
308+ const properties = request?.body.properties as Record<string, unknown> | undefined;
309+ expect(properties?.oauth_error_code).toBe("unauthorized_client");
310+ }),
311+ );
312+
313+ it.effect("sends nothing beyond identity, the integration and the flag decision", () =>
314+ Effect.gen(function* () {
315+ const harness = makeFetchHarness(() => jsonResponse({ status: 1 }));
316+
317+ yield* gate(harness).record({
318+ kind: "blocked-by-admin",
319+ context: CONTEXT,
320+ decision: { kind: "enabled" },
321+ oauthErrorCode: "access_denied",
322+ });
323+ yield* Effect.promise(() => harness.settle());
324+
325+ const properties = harness.requests()[0]?.body.properties as Record<string, unknown>;
326+ expect(
327+ Object.keys(properties).sort(),
328+ "a closed property set is what keeps a token or an assertion from ever being added by accident",
329+ ).toEqual(
330+ [
331+ "$feature/mcp-enterprise-managed-auth",
332+ "$groups",
333+ "ema_flag_enabled",
334+ "integration_slug",
335+ "oauth_error_code",
336+ ].sort(),
337+ );
338+ }),
339+ );
340+
341+ it.effect("hands the capture to the platform instead of awaiting it", () =>
342+ Effect.gen(function* () {
343+ let resolveCapture: (() => void) | undefined;
344+ const harness = makeFetchHarness(
345+ () =>
346+ new Promise<Response>((resolve) => {
347+ resolveCapture = () => resolve(jsonResponse({ status: 1 }));
348+ }),
349+ );
350+
351+ // Returns while the capture is still in flight: an analytics call can
352+ // never be on the critical path of a connect.
353+ yield* gate(harness).record({
354+ kind: "attempted",
355+ context: CONTEXT,
356+ decision: { kind: "enabled" },
357+ });
358+
359+ expect(harness.requests().length).toBe(1);
360+ resolveCapture?.();
361+ yield* Effect.promise(() => harness.settle());
362+ }),
363+ );
364+
365+ it.effect("survives a capture that fails outright", () =>
366+ Effect.gen(function* () {
367+ const harness = makeFetchHarness(() => {
368+ // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: models the ingest endpoint being unreachable
369+ throw new TypeError("network error");
370+ });
371+
372+ yield* gate(harness).record({
373+ kind: "connected",
374+ context: CONTEXT,
375+ decision: { kind: "enabled" },
376+ });
377+ yield* Effect.promise(() => harness.settle());
378+ }),
379+ );
380+
381+ it.effect("records nothing when there is no person to attach the event to", () =>
382+ Effect.gen(function* () {
383+ const harness = makeFetchHarness(() => jsonResponse({ status: 1 }));
384+
385+ yield* gate(harness).record({
386+ kind: "attempted",
387+ context: { ...CONTEXT, userId: null },
388+ decision: { kind: "withheld", reason: "evaluation-unavailable" },
389+ });
390+
391+ expect(harness.requests().length).toBe(0);
392+ }),
393+ );
394+});
apps/cloud/src/analytics/ema-rollout.ts+255−0
@@ -0,0 +1,255 @@
1+// ---------------------------------------------------------------------------
2+// Cloud's rollout gate for MCP Enterprise-Managed Authorization.
3+//
4+// Implements the vendor-free `EnterpriseManagedRollout` port from
5+// `@executor-js/sdk` against PostHog, which is the only flag/analytics service
6+// this deployment operates. Two deliberate shapes:
7+//
8+// 1. NO SDK. This is a hand-written `fetch` against two documented PostHog
9+// endpoints, not `posthog-node`. Cloud already ate an unexplained 3-5s
10+// page-load regression from adding one dependency to the worker bundle and
11+// the mechanism was never identified (see the notes on the MCP SDK v2
12+// revert). Until that is understood, a new runtime dependency in this bundle
13+// is a cost we are not willing to pay for a boolean, and the whole client we
14+// would be importing is two POSTs wide.
15+//
16+// 2. FAIL CLOSED, ALWAYS. Every way of not getting an answer — timeout,
17+// transport failure, non-2xx, a body we cannot parse, no PostHog key
18+// configured, no acting user to key the rollout on — withholds the
19+// enterprise-managed path and falls back to ordinary interactive OAuth. A
20+// PostHog outage therefore degrades the rollout and never fails a connect,
21+// and (because the SDK freezes the verdict onto the connection) never
22+// touches an enterprise-managed connection that already exists.
23+//
24+// The rollout UNIT is the user: `distinct_id` is the acting user's id, the same
25+// id `posthog.identify(user.id, …)` uses in the browser, so a percentage
26+// rollout here means the same population it means everywhere else in the
27+// project. The organization rides along as group context so org-level targeting
28+// stays available in the PostHog UI without changing that unit.
29+// ---------------------------------------------------------------------------
30+
31+import { env } from "cloudflare:workers";
32+import { Effect, Schema } from "effect";
33+
34+import type {
35+ EnterpriseManagedRollout,
36+ EnterpriseManagedRolloutContext,
37+ EnterpriseManagedRolloutDecision,
38+ EnterpriseManagedRolloutEvent,
39+} from "@executor-js/sdk";
40+
41+import { POSTHOG_INGEST_HOST } from "../edge/passthrough";
42+
43+/** The flag that gates enterprise-managed authorization. */
44+export const ENTERPRISE_MANAGED_AUTH_FLAG_KEY = "mcp-enterprise-managed-auth";
45+
46+/** PostHog group type for organizations — the same one
47+ * `posthog.group("organization", …)` establishes in the browser. */
48+const ORGANIZATION_GROUP_TYPE = "organization";
49+
50+/**
51+ * How long a flag evaluation may take before the gate gives up and withholds.
52+ * Short on purpose: this call sits in front of an interactive connect, and the
53+ * safe answer is already known, so waiting is strictly worse than answering.
54+ */
55+export const DEFAULT_FLAG_EVALUATION_TIMEOUT_MS = 1_000;
56+
57+/** Wire names of the rollout events, in the product's `object_verb` style. */
58+const EVENT_NAMES = {
59+ attempted: "ema_connect_attempted",
60+ connected: "ema_connect_connected",
61+ "blocked-by-admin": "ema_connect_blocked_by_admin",
62+} as const satisfies Record<EnterpriseManagedRolloutEvent["kind"], string>;
63+
64+// ---------------------------------------------------------------------------
65+// Boundary parsing
66+// ---------------------------------------------------------------------------
67+
68+/** The slice of PostHog's `POST /flags?v=2` response this gate reads. Unknown
69+ * keys are ignored, so PostHog adding fields cannot make the gate fail closed;
70+ * a body that is not this shape at all can, which is the intent. */
71+const FlagsResponse = Schema.Struct({
72+ flags: Schema.Record(Schema.String, Schema.Struct({ enabled: Schema.Boolean })),
73+});
74+
75+const decodeFlagsResponse = Schema.decodeUnknownEffect(FlagsResponse);
76+
77+// ---------------------------------------------------------------------------
78+// Detached work
79+// ---------------------------------------------------------------------------
80+
81+/**
82+ * Hand a request to the platform and stop caring about it.
83+ *
84+ * `waitUntil` is the correct owner when the caller has an `ExecutionContext`;
85+ * the executor composition seam this gate is installed through does not receive
86+ * one, so the default owner attaches a terminal rejection handler and lets the
87+ * in-flight request ride out the rest of the response. Either way the promise
88+ * is owned by something that cannot report back, which is what makes an event
89+ * structurally unable to fail or delay a connect.
90+ */
91+const detach = (
92+ work: Promise<unknown>,
93+ waitUntil: ((work: Promise<unknown>) => void) | undefined,
94+): void => {
95+ const settled = work.then(
96+ () => undefined,
97+ () => undefined,
98+ );
99+ if (waitUntil) waitUntil(settled);
100+};
101+
102+// ---------------------------------------------------------------------------
103+// The gate
104+// ---------------------------------------------------------------------------
105+
106+export interface PostHogRolloutConfig {
107+ /** Public project key (`phc_…`). Public by design — it is already shipped to
108+ * every browser — so this is a var, not a secret. */
109+ readonly projectKey: string;
110+ /** PostHog API origin (no trailing slash), e.g. `https://us.i.posthog.com`. */
111+ readonly host: string;
112+ readonly fetch: typeof globalThis.fetch;
113+ readonly timeoutMs?: number;
114+ /** Platform post-response hook, when the caller has one. */
115+ readonly waitUntil?: (work: Promise<unknown>) => void;
116+}
117+
118+const withheld = (
119+ reason: "disabled" | "evaluation-unavailable",
120+): EnterpriseManagedRolloutDecision => ({ kind: "withheld", reason });
121+
122+const ENABLED: EnterpriseManagedRolloutDecision = { kind: "enabled" };
123+
124+/** Group context for a connect whose organization the host named. */
125+const groupsFor = (context: EnterpriseManagedRolloutContext): Record<string, string> | undefined =>
126+ context.organizationId === null
127+ ? undefined
128+ : { [ORGANIZATION_GROUP_TYPE]: context.organizationId };
129+
130+/**
131+ * Build cloud's enterprise-managed rollout gate over an explicit PostHog
132+ * configuration. Everything environmental is a parameter so the composition
133+ * root parses it once and tests drive the real code path with an injected
134+ * `fetch`.
135+ */
136+export const makePostHogEnterpriseManagedRollout = (
137+ config: PostHogRolloutConfig,
138+): EnterpriseManagedRollout => {
139+ const timeoutMs = config.timeoutMs ?? DEFAULT_FLAG_EVALUATION_TIMEOUT_MS;
140+
141+ const evaluate = Effect.fn("Cloud.EnterpriseManagedRollout.decide")(function* (
142+ context: EnterpriseManagedRolloutContext,
143+ ) {
144+ // The rollout unit is the user. With no acting user there is no
145+ // `distinct_id` to key it on, so there is no answer to be had — withhold
146+ // rather than invent a rollout bucket.
147+ const distinctId = context.userId;
148+ if (distinctId === null) return withheld("evaluation-unavailable");
149+
150+ const groups = groupsFor(context);
151+ const response = yield* Effect.tryPromise(() =>
152+ config.fetch(`${config.host}/flags?v=2`, {
153+ method: "POST",
154+ headers: {
155+ "content-type": "application/json",
156+ // Declares this a server-side evaluation, which is what selects
157+ // server-runtime flags in PostHog's runtime detection.
158+ "user-agent": "executor-cloud",
159+ },
160+ body: JSON.stringify({
161+ api_key: config.projectKey,
162+ distinct_id: distinctId,
163+ ...(groups === undefined ? {} : { groups }),
164+ }),
165+ signal: AbortSignal.timeout(timeoutMs),
166+ }),
167+ );
168+ if (!response.ok) return withheld("evaluation-unavailable");
169+
170+ const body = yield* Effect.tryPromise(() => response.json() as Promise<unknown>);
171+ const decoded = yield* decodeFlagsResponse(body);
172+ const flag = decoded.flags[ENTERPRISE_MANAGED_AUTH_FLAG_KEY];
173+ // A flag PostHog did not return is a flag that is not on for this user
174+ // (it may not exist yet, or the project may be quota-limited). That is a
175+ // real "no", not a failure to answer.
176+ if (flag === undefined || !flag.enabled) return withheld("disabled");
177+ return ENABLED;
178+ });
179+
180+ return {
181+ decide: (context) =>
182+ evaluate(context).pipe(
183+ // Timeout, transport failure and an unparseable body all land here and
184+ // all mean the same thing: no answer, so no enterprise-managed attempt.
185+ // `catchCause` rather than `catchAll` because a defect in this adapter
186+ // must not become a failed connect either.
187+ Effect.catchCause(() => Effect.succeed(withheld("evaluation-unavailable"))),
188+ ),
189+ record: (event) =>
190+ Effect.sync(() => {
191+ const distinctId = event.context.userId;
192+ // Same reason `decide` withholds: an event with no person to attach to
193+ // would only pollute the project with anonymous rows.
194+ if (distinctId === null) return;
195+ const groups = groupsFor(event.context);
196+ const decision = event.decision;
197+ detach(
198+ config.fetch(`${config.host}/i/v0/e/`, {
199+ method: "POST",
200+ headers: { "content-type": "application/json" },
201+ body: JSON.stringify({
202+ api_key: config.projectKey,
203+ event: EVENT_NAMES[event.kind],
204+ distinct_id: distinctId,
205+ properties: {
206+ // Product metadata only. There is no field on
207+ // `EnterpriseManagedRolloutEvent` that can carry a token, an
208+ // identity assertion, a client secret or a scope, and none is
209+ // synthesized here.
210+ integration_slug: String(event.context.integration),
211+ ema_flag_enabled: decision.kind === "enabled",
212+ ...(decision.kind === "withheld"
213+ ? { ema_flag_withheld_reason: decision.reason }
214+ : {}),
215+ // PostHog's own convention, so these events can be filtered by
216+ // flag value in the UI alongside every other flag.
217+ [`$feature/${ENTERPRISE_MANAGED_AUTH_FLAG_KEY}`]: decision.kind === "enabled",
218+ ...(event.kind === "blocked-by-admin" && event.oauthErrorCode !== undefined
219+ ? { oauth_error_code: event.oauthErrorCode }
220+ : {}),
221+ ...(groups === undefined ? {} : { $groups: groups }),
222+ },
223+ }),
224+ }),
225+ config.waitUntil,
226+ );
227+ }),
228+ };
229+};
230+
231+/** A gate that answers "no" to everything, for a deployment that has no PostHog
232+ * configuration to evaluate against. Cloud is SUPPOSED to have one, so a
233+ * missing key is a misconfiguration, and failing closed is the same choice the
234+ * outage paths make. This is NOT the same as injecting no gate at all, which
235+ * is how the hosts with no flag service (local, desktop, CLI, self-host) keep
236+ * their existing behavior. */
237+export const unavailableEnterpriseManagedRollout: EnterpriseManagedRollout = {
238+ decide: () => Effect.succeed(withheld("evaluation-unavailable")),
239+ record: () => Effect.void,
240+};
241+
242+/**
243+ * The gate as the cloud worker composes it: the public project key already
244+ * shipped in `wrangler.jsonc`, and the same PostHog ingest origin the
245+ * adblock-dodging passthrough proxies to.
246+ */
247+export const cloudEnterpriseManagedRollout = (): EnterpriseManagedRollout => {
248+ const projectKey = env.VITE_PUBLIC_POSTHOG_KEY;
249+ if (!projectKey) return unavailableEnterpriseManagedRollout;
250+ return makePostHogEnterpriseManagedRollout({
251+ projectKey,
252+ host: env.VITE_PUBLIC_POSTHOG_HOST ?? `https://${POSTHOG_INGEST_HOST}`,
253+ fetch: (input, init) => globalThis.fetch(input, init),
254+ });
255+};
apps/cloud/src/api/protected-api-key-auth.node.test.ts+6−1
@@ -13,6 +13,7 @@ const stubApiKeys = Layer.succeed(ApiKeyService)({ · 12 unmodified lines
1313 Effect.succeed(
1414 value === "valid_user_key"
1515 ? {
16+ scope: "user" as const,
1617 accountId: "user_123",
1718 organizationId: "org_123",
1819 keyId: "api_key_123",
@@ -22,6 +23,9 @@ const stubApiKeys = Layer.succeed(ApiKeyService)({ · 3 unmodified lines
2223 listUserKeys: () => Effect.succeed([]),
2324 createUserKey: () => Effect.die("protected API auth test does not create API keys"),
2425 revokeUserKey: () => Effect.void,
26+ listOrgKeys: () => Effect.die("auth resolution test does not list org API keys"),
27+ createOrgKey: () => Effect.die("auth resolution test does not create org API keys"),
28+ revokeOrgKey: () => Effect.die("auth resolution test does not revoke org API keys"),
2529 });
2630
2731 const stubWorkOS = Layer.succeed(
@@ -43,7 +47,7 @@ const stubWorkOS = Layer.succeed( · 15 unmodified lines
4347 );
4448
4549 const stubUsers = Layer.succeed(UserStoreService)({
46− use: (fn) =>
50+ use: (_op, fn) =>
4751 Effect.promise(() =>
4852 fn({
4953 ensureAccount: async (id: string) => ({ id, createdAt }),
@@ -85,6 +89,7 @@ describe("protected API key auth", () => { · 35 unmodified lines
8589 );
8690
8791 expect(identity).toEqual({
92+ kind: "member",
8893 accountId: "user_123",
8994 organizationId: "org_123",
9095 organizationName: "Org org_123",
apps/cloud/src/api/protected-jwt-auth.node.test.ts+5−1
@@ -40,6 +40,9 @@ const stubApiKeys = Layer.succeed(ApiKeyService)({ · 39 unmodified lines
4040 listUserKeys: () => Effect.succeed([]),
4141 createUserKey: () => Effect.die("JWT auth test does not create API keys"),
4242 revokeUserKey: () => Effect.void,
43+ listOrgKeys: () => Effect.die("auth resolution test does not list org API keys"),
44+ createOrgKey: () => Effect.die("auth resolution test does not create org API keys"),
45+ revokeOrgKey: () => Effect.die("auth resolution test does not revoke org API keys"),
4346 });
4447
4548 const stubWorkOS = Layer.succeed(
@@ -61,7 +64,7 @@ const stubWorkOS = Layer.succeed( · 15 unmodified lines
6164 );
6265
6366 const stubUsers = Layer.succeed(UserStoreService)({
64− use: (fn) =>
67+ use: (_op, fn) =>
6568 Effect.promise(() =>
6669 fn({
6770 ensureAccount: async (id: string) => ({ id, createdAt }),
@@ -107,6 +110,7 @@ describe("protected JWT (device-login) auth", () => { · 39 unmodified lines
107110 const identity = yield* run(request(token), config);
108111
109112 expect(identity).toEqual({
113+ kind: "member",
110114 accountId: "user_123",
111115 organizationId: "org_123",
112116 organizationName: "Org org_123",
apps/cloud/src/api/protected.test.ts+2−0
@@ -20,6 +20,8 @@ const makeBaseEngine = (): ExecutionEngine => · 19 unmodified lines
2020 pausedExecutionCount: () => Effect.succeed(0),
2121 hasPausedExecutions: () => Effect.succeed(false),
2222 getDescription: Effect.succeed("desc"),
23+ // The fake forks nothing, so there is no sandbox fiber to end.
24+ shutdown: Effect.void,
2325 }) as ExecutionEngine;
2426
2527 describe("withExecutionUsageTracking", () => {
apps/cloud/src/app-paths.test.ts+37−1
@@ -1,6 +1,6 @@
11 import { describe, expect, it } from "@effect/vitest";
22
3−import { isAppOwnedPath } from "./app-paths";
3+import { isAppOwnedPath, servedByAppPlane } from "./app-paths";
44
55 // Guards the start.ts dispatch decision: every surface the unified app handler
66 // serves must be classified app-owned (forwarded to `app.handler`), and Start's
@@ -62,3 +62,39 @@ describe("isAppOwnedPath", () => { · 55 unmodified lines
6262 });
6363 }
6464 });
65+
66+describe("app-plane dispatch", () => {
67+ // These two are the whole risk of dispatching `/api` before Start: both still
68+ // return a response if routed early, just the wrong one, so nothing else would
69+ // catch a regression here.
70+ it("leaves the Sentry tunnel POST to Start's middleware", () => {
71+ expect(servedByAppPlane("/api/sentry-tunnel", "POST")).toBe(false);
72+ // Only the POST is claimed; anything else under that path is ordinary API.
73+ expect(servedByAppPlane("/api/sentry-tunnel", "GET")).toBe(true);
74+ });
75+
76+ it("leaves the OAuth callback to Start, for the signed-out redirect", () => {
77+ expect(servedByAppPlane("/api/oauth/callback", "GET")).toBe(false);
78+ expect(servedByAppPlane("/api/oauth/callback", "POST")).toBe(false);
79+ });
80+
81+ const appPlane = [
82+ "/api/connections",
83+ "/api/tools",
84+ "/api/integrations",
85+ "/api/account/members",
86+ "/api/docs",
87+ "/api/billing/checkout",
88+ ];
89+ for (const pathname of appPlane) {
90+ it(`serves ${pathname} without entering Start`, () => {
91+ expect(servedByAppPlane(pathname, "GET")).toBe(true);
92+ });
93+ }
94+
95+ it("never claims a non-API path, however app-owned", () => {
96+ expect(servedByAppPlane("/mcp", "POST")).toBe(false);
97+ expect(servedByAppPlane("/", "GET")).toBe(false);
98+ expect(servedByAppPlane("/.well-known/oauth-authorization-server", "GET")).toBe(false);
99+ });
100+});
apps/cloud/src/app-paths.ts+28−0
@@ -17,3 +17,31 @@ export const isApiPath = (pathname: string) => pathname === "/api" || pathname.s · 16 unmodified lines
1717
1818 export const isAppOwnedPath = (pathname: string) =>
1919 isApiPath(pathname) || classifyMcpPath(pathname) !== null;
20+
21+// ---------------------------------------------------------------------------
22+// Which plane serves an app-owned path: the Effect app directly, or TanStack
23+// Start's middleware chain.
24+//
25+// Everything under `/api` is pure Effect and touches no part of the router,
26+// React, or SSR — so `server.ts` dispatches it at the Worker entry and skips
27+// Start's lazy `loadEntries` import entirely. Two paths must NOT take that
28+// shortcut, because Start's request middleware claims them BEFORE the app
29+// handler would ever see them:
30+//
31+// POST /api/sentry-tunnel - `sentryTunnelMiddleware` forwards the envelope
32+// to Sentry; the app has no such route.
33+// /api/oauth/callback - `oauthCallbackSignInMiddleware` redirects a
34+// signed-out visitor to /login, and start.ts
35+// rewrites the org-scoped `state` before handing
36+// off. Routing it early would drop both.
37+//
38+// Getting this wrong is silent: the request still gets a response, just the
39+// wrong one, which is why it is classified here and tested rather than being
40+// an inline condition at the dispatch site.
41+// ---------------------------------------------------------------------------
42+
43+export const isStartOwnedApiPath = (pathname: string, method: string): boolean =>
44+ (pathname === "/api/sentry-tunnel" && method === "POST") || pathname === "/api/oauth/callback";
45+
46+export const servedByAppPlane = (pathname: string, method: string): boolean =>
47+ isApiPath(pathname) && !isStartOwnedApiPath(pathname, method);
apps/cloud/src/app.ts+2−7
@@ -13,7 +13,6 @@ import { ApiKeyService } from "./auth/api-keys"; · 12 unmodified lines
1313 import { cloudIdentityFailureStrategy, workosIdentityLayer } from "./auth/workos-auth-provider";
1414 import { DbService } from "./db/db";
1515 import { cloudMcpAuth } from "./mcp";
16−import { McpSessionDOSqlite } from "./mcp/session-durable-object";
1716 import { ErrorCaptureLive } from "./observability";
1817 import { AutumnService } from "./extensions/billing/service";
1918 import {
@@ -31,7 +30,7 @@ import { WorkerTelemetryLive } from "./observability/telemetry"; · 11 unmodified lines
3130 // The whole scenario in 60 seconds: WorkOS identity (api-key Bearer OR sealed-
3231 // session cookie, api-key wins) over a per-request Hyperdrive→Postgres socket,
3332 // the Cloudflare dynamic-worker code substrate, MCP served by a Durable-Object
34−// session store (the DO surfaced via `config.mcpExport`), console+Sentry error
33+// session store (the DO class is exported straight from server.ts), console+Sentry error
3534 // capture — and Autumn BILLING entering ONLY as extensions: the engine
3635 // metering decorator, the account seat-gate, the `/api/billing/*` proxy route,
3736 // and the createOrganization free-limit gate. `diff` against
@@ -66,7 +65,7 @@ const controlPlane = Layer.mergeAll(CoreSharedServices, apiKeyService); · 28 unmodified lines
6665 // satisfied by `boot`, `DbService` per request via `requestScoped`).
6766 const cloudDb: Layer.Layer<DbProvider, never, DbService | AutumnService> = CloudDbProvider;
6867
69−const { appLayer, toWebHandler, mcpExport } = ExecutorApp.make({
68+const { appLayer, toWebHandler } = ExecutorApp.make({
7069 plugins: cloudPlugins,
7170 providers: {
7271 // Identity: the NEUTRAL `IdentityProvider`. WorkOS api-key Bearer BEATS
@@ -112,7 +111,6 @@ const { appLayer, toWebHandler, mcpExport } = ExecutorApp.make({ · 39 unmodified lines
112111 failure: cloudIdentityFailureStrategy,
113112 // The MCP session Durable Object class — a top-level Workers export a Layer
114113 // can't return; surfaced so `server.ts` can re-export it.
115− mcpExport: McpSessionDOSqlite,
116114 },
117115 // The long-lived (boot-scoped) context provideMerge'd under everything: the
118116 // WorkOS control plane (the raw `WorkOSClient` + `ApiKeyService` the per-request
@@ -135,10 +133,7 @@ const { appLayer, toWebHandler, mcpExport } = ExecutorApp.make({ · 16 unmodified lines
135133 requestScoped: RequestScopedServicesLive,
136134 });
137135
138−export { McpSessionDOSqlite };
139−
140136 export const CloudAppLayer = appLayer;
141−export const cloudMcpExport = mcpExport;
142137
143138 // The unified cloud web handler: serves /api/* (incl. /api/billing/*, /api/docs),
144139 // /mcp, /.well-known/* — everything the worker dispatches.
apps/cloud/src/auth/api-keys.node.test.ts+23−8
@@ -42,6 +42,7 @@ describe("ApiKeyService.WorkOS", () => { · 41 unmodified lines
4242 });
4343
4444 expect(principal).toEqual({
45+ scope: "user",
4546 accountId: "user_123",
4647 organizationId: "org_123",
4748 keyId: "api_key_123",
@@ -66,15 +67,9 @@ describe("ApiKeyService.WorkOS", () => { · 18 unmodified lines
6667 }),
6768 );
6869
69− it.effect("rejects missing, organization-owned, and org-less keys", () =>
70+ it.effect("rejects missing and org-less keys", () =>
7071 Effect.gen(function* () {
7172 const missing = yield* validate({ apiKey: null });
72− const orgOwned = yield* validate({
73− apiKey: {
74− id: "api_key_org",
75− owner: { type: "organization", id: "org_123" },
76− },
77− });
7873 const orgLess = yield* validate({
7974 apiKey: {
8075 id: "api_key_no_org",
@@ -83,11 +78,31 @@ describe("ApiKeyService.WorkOS", () => { · 2 unmodified lines
8378 });
8479
8580 expect(missing).toBeNull();
86− expect(orgOwned).toBeNull();
8781 expect(orgLess).toBeNull();
8882 }),
8983 );
9084
85+ it.effect("resolves organization-owned keys to the org scope with no account", () =>
86+ Effect.gen(function* () {
87+ // An org key USED to fail the decode and come back null — indistinguishable
88+ // from an invalid key. It now resolves explicitly: `owner.id` IS the
89+ // organization, and there is no member behind it.
90+ const principal = yield* validate({
91+ apiKey: {
92+ id: "api_key_org",
93+ owner: { type: "organization", id: "org_123" },
94+ },
95+ });
96+
97+ expect(principal).toEqual({
98+ scope: "org",
99+ accountId: null,
100+ organizationId: "org_123",
101+ keyId: "api_key_org",
102+ });
103+ }),
104+ );
105+
91106 it.effect("lists and creates user-owned keys", () =>
92107 Effect.gen(function* () {
93108 const program = Effect.gen(function* () {
apps/cloud/src/auth/api-keys.ts+214−12
@@ -3,10 +3,24 @@ import { Context, Data, Effect, Layer, Option, Schema } from "effect"; · 2 unmodified lines
33 import { ApiKeyManagementError } from "./errors";
44 import { WorkOSClient } from "./workos";
55
6+/**
7+ * Which view a validated key resolves to.
8+ * - `"user"` — the product view. The key belongs to a member; the request
9+ * binds to that member as the acting subject. Every key today is this.
10+ * - `"org"` — the PLATFORM view. The key belongs to the organization itself,
11+ * so there is no acting member: the request gets tenant-wide READ-ONLY
12+ * reach and no bound subject. Privileged; minted separately (PR 1.5).
13+ */
14+export type ApiKeyScope = "user" | "org";
15+
616 /** The owner an api key resolves to — NOT a full {@link Principal} (no email /
717 * name / roles), so it carries an honest, distinct name. */
818 export type ApiKeyOwner = {
9− readonly accountId: string;
19+ readonly scope: ApiKeyScope;
20+ /** The acting member for a `"user"` key; `null` for an `"org"` key, which
21+ * has no member behind it. Nullable rather than a sentinel so nothing can
22+ * accidentally bind an org key to a subject. */
23+ readonly accountId: string | null;
1024 readonly organizationId: string;
1125 readonly keyId: string;
1226 };
@@ -28,6 +42,21 @@ export class ApiKeyValidationError extends Data.TaggedError("ApiKeyValidationErr · 15 unmodified lines
2842 readonly cause: unknown;
2943 }> {}
3044
45+/**
46+ * The requested key id is not an org-owned key of this organization — it does
47+ * not exist, belongs to a member rather than the org, or belongs to a different
48+ * org in the same WorkOS workspace. Distinct from `ApiKeyManagementError` (an
49+ * upstream failure) so callers can answer "not found" without a 500.
50+ */
51+export class OrgApiKeyNotFound extends Data.TaggedError("OrgApiKeyNotFound")<{
52+ readonly keyId: string;
53+}> {}
54+
55+// WorkOS keys are owned by either a user or an organization. The user shape
56+// carries the member id in `id` and the org in `organization_id`; the org shape
57+// carries the ORG in `id` and no member at all. Both decode here — the
58+// distinction is a branch (`ownerFromApiKey`), not a decode failure, so an
59+// org-owned key is no longer indistinguishable from an invalid one.
3160 const UserApiKeyOwner = Schema.Struct({
3261 type: Schema.Literal("user"),
3362 id: Schema.String,
@@ -35,6 +64,13 @@ const UserApiKeyOwner = Schema.Struct({ · 1 unmodified line
3564 organization_id: Schema.optional(Schema.String),
3665 });
3766
67+const OrgApiKeyOwner = Schema.Struct({
68+ type: Schema.Literal("organization"),
69+ id: Schema.String,
70+});
71+
72+const WorkOSApiKeyOwner = Schema.Union([UserApiKeyOwner, OrgApiKeyOwner]);
73+
3874 const ApiKey = Schema.Struct({
3975 id: Schema.String,
4076 owner: UserApiKeyOwner,
@@ -49,8 +85,17 @@ const ApiKey = Schema.Struct({ · 8 unmodified lines
4985 last_used_at: Schema.optional(Schema.NullOr(Schema.String)),
5086 });
5187
88+// Validation accepts BOTH owner shapes. The user list/create paths below stay
89+// user-only on purpose (they are the console's personal-key CRUD); the ORG
90+// list/create paths are the separate, privileged surface beside them, gated to
91+// admins at the account-provider boundary.
92+const ValidatedApiKey = Schema.Struct({
93+ id: Schema.String,
94+ owner: WorkOSApiKeyOwner,
95+});
96+
5297 const ValidateApiKeyResponse = Schema.Struct({
53− apiKey: Schema.NullOr(ApiKey),
98+ apiKey: Schema.NullOr(ValidatedApiKey),
5499 });
55100
56101 const RawCreatedApiKey = Schema.Struct({
@@ -72,6 +117,45 @@ const ListApiKeysResponse = Schema.Struct({ · 15 unmodified lines
72117 data: Schema.Array(ApiKey),
73118 });
74119
120+// The ORG-owned mirror of `ApiKey` / `RawCreatedApiKey`. A separate shape
121+// rather than a widened one: `summaryFromApiKey` reads the user shape's
122+// `organization_id` to prove the key belongs to the caller's org, and an
123+// org-owned row carries the org in `owner.id` instead. Merging them would mean
124+// one nullable field standing for two different things.
125+const OrgApiKey = Schema.Struct({
126+ id: Schema.String,
127+ owner: OrgApiKeyOwner,
128+ name: Schema.optional(Schema.String),
129+ obfuscatedValue: Schema.optional(Schema.String),
130+ obfuscated_value: Schema.optional(Schema.String),
131+ createdAt: Schema.optional(Schema.String),
132+ created_at: Schema.optional(Schema.String),
133+ updatedAt: Schema.optional(Schema.String),
134+ updated_at: Schema.optional(Schema.String),
135+ lastUsedAt: Schema.optional(Schema.NullOr(Schema.String)),
136+ last_used_at: Schema.optional(Schema.NullOr(Schema.String)),
137+});
138+
139+const RawCreatedOrgApiKey = Schema.Struct({
140+ ...OrgApiKey.fields,
141+ value: Schema.String,
142+});
143+
144+// `data` is decoded element-by-element, NOT as `Schema.Array(OrgApiKey)`: WorkOS
145+// may return user-owned keys alongside org-owned ones, and a whole-array decode
146+// fails on the first non-org element — blanking the entire list rather than
147+// dropping the one row that doesn't belong. Same per-row tolerance the user-key
148+// path gets from `summaryFromApiKey` returning null.
149+const ListOrgApiKeysResponse = Schema.Struct({
150+ data: Schema.Array(Schema.Unknown),
151+});
152+
153+const CreateOrgApiKeyResponse = Schema.Union([
154+ RawCreatedOrgApiKey,
155+ Schema.Struct({ apiKey: RawCreatedOrgApiKey }),
156+ Schema.Struct({ api_key: RawCreatedOrgApiKey }),
157+]);
158+
75159 const CreateApiKeyResponse = Schema.Union([
76160 RawCreatedApiKey,
77161 Schema.Struct({ apiKey: RawCreatedApiKey }),
@@ -81,20 +165,35 @@ const CreateApiKeyResponse = Schema.Union([ · 3 unmodified lines
81165 const decodeValidateApiKeyResponse = Schema.decodeUnknownOption(ValidateApiKeyResponse);
82166 const decodeListApiKeysResponse = Schema.decodeUnknownOption(ListApiKeysResponse);
83167 const decodeCreateApiKeyResponse = Schema.decodeUnknownOption(CreateApiKeyResponse);
168+const decodeListOrgApiKeysResponse = Schema.decodeUnknownOption(ListOrgApiKeysResponse);
169+const decodeOrgApiKey = Schema.decodeUnknownOption(OrgApiKey);
170+const decodeCreateOrgApiKeyResponse = Schema.decodeUnknownOption(CreateOrgApiKeyResponse);
171+
172+const ownerFromApiKey = (apiKey: typeof ValidatedApiKey.Type): ApiKeyOwner | null => {
173+ if (apiKey.owner.type === "organization") {
174+ // An org-owned key IS the org: `owner.id` is the organization, and there
175+ // is no member to act as. This resolves to the platform view.
176+ return {
177+ scope: "org",
178+ accountId: null,
179+ organizationId: apiKey.owner.id,
180+ keyId: apiKey.id,
181+ };
182+ }
183+ const organizationId = apiKey.owner.organizationId ?? apiKey.owner.organization_id;
184+ if (!organizationId) return null;
185+ return {
186+ scope: "user",
187+ accountId: apiKey.owner.id,
188+ organizationId,
189+ keyId: apiKey.id,
190+ };
191+};
84192
85193 const ownerFromResponse = (value: unknown): ApiKeyOwner | null =>
86194 Option.match(decodeValidateApiKeyResponse(value), {
87195 onNone: () => null,
88− onSome: ({ apiKey }) => {
89− if (!apiKey) return null;
90− const organizationId = apiKey.owner.organizationId ?? apiKey.owner.organization_id;
91− if (!organizationId) return null;
92− return {
93− accountId: apiKey.owner.id,
94− organizationId,
95− keyId: apiKey.id,
96− };
97− },
196+ onSome: ({ apiKey }) => (apiKey ? ownerFromApiKey(apiKey) : null),
98197 });
99198
100199 const summaryFromApiKey = (apiKey: typeof ApiKey.Type): ApiKeySummary | null => {
@@ -120,6 +219,53 @@ const listFromResponse = (value: unknown): readonly ApiKeySummary[] => · 19 unmodified lines
120219 }),
121220 });
122221
222+// The org-owned mirror of `summaryFromApiKey`. `organizationId` is checked by
223+// the CALLER against the org it asked for: WorkOS's workspace api key is
224+// workspace-wide, so a response naming a different org than the one requested
225+// must not be surfaced as that org's key.
226+const summaryFromOrgApiKey = (
227+ apiKey: typeof OrgApiKey.Type,
228+ organizationId: string,
229+): ApiKeySummary | null => {
230+ if (apiKey.owner.id !== organizationId) return null;
231+ return {
232+ id: apiKey.id,
233+ name: apiKey.name ?? "API key",
234+ obfuscatedValue: apiKey.obfuscatedValue ?? apiKey.obfuscated_value ?? "",
235+ createdAt: apiKey.createdAt ?? apiKey.created_at ?? "",
236+ updatedAt: apiKey.updatedAt ?? apiKey.updated_at ?? "",
237+ lastUsedAt: apiKey.lastUsedAt ?? apiKey.last_used_at ?? null,
238+ };
239+};
240+
241+const orgListFromResponse = (value: unknown, organizationId: string): readonly ApiKeySummary[] =>
242+ Option.match(decodeListOrgApiKeysResponse(value), {
243+ onNone: () => [],
244+ onSome: ({ data }) =>
245+ data.flatMap((entry) =>
246+ Option.match(decodeOrgApiKey(entry), {
247+ // Not an org-owned key (a user key sharing the listing, or a shape we
248+ // don't recognize) — skip the row, keep the rest.
249+ onNone: () => [],
250+ onSome: (apiKey) => {
251+ const summary = summaryFromOrgApiKey(apiKey, organizationId);
252+ return summary ? [summary] : [];
253+ },
254+ }),
255+ ),
256+ });
257+
258+const orgCreatedFromResponse = (value: unknown, organizationId: string): CreatedApiKey | null =>
259+ Option.match(decodeCreateOrgApiKeyResponse(value), {
260+ onNone: () => null,
261+ onSome: (response) => {
262+ const apiKey =
263+ "value" in response ? response : "apiKey" in response ? response.apiKey : response.api_key;
264+ const summary = summaryFromOrgApiKey(apiKey, organizationId);
265+ return summary ? { ...summary, value: apiKey.value } : null;
266+ },
267+ });
268+
123269 const createdFromResponse = (value: unknown): CreatedApiKey | null =>
124270 Option.match(decodeCreateApiKeyResponse(value), {
125271 onNone: () => null,
@@ -147,6 +293,28 @@ export class ApiKeyService extends Context.Service< · 21 unmodified lines
147293 readonly revokeUserKey: (input: {
148294 readonly keyId: string;
149295 }) => Effect.Effect<void, ApiKeyManagementError>;
296+ /**
297+ * The org-owned keys of an organization — the credentials that resolve to
298+ * the read-only platform view. Privileged: callers gate on admin membership
299+ * before reaching this.
300+ */
301+ readonly listOrgKeys: (input: {
302+ readonly organizationId: string;
303+ }) => Effect.Effect<readonly ApiKeySummary[], ApiKeyManagementError>;
304+ readonly createOrgKey: (input: {
305+ readonly organizationId: string;
306+ readonly name: string;
307+ }) => Effect.Effect<CreatedApiKey, ApiKeyManagementError>;
308+ /**
309+ * Revoke an org-owned key. Ownership is resolved against the ORG's own key
310+ * listing — never the caller's personal keys — so a member's user key id
311+ * (or another org's key id, since the WorkOS workspace key is
312+ * workspace-wide) cannot be destroyed through this path.
313+ */
314+ readonly revokeOrgKey: (input: {
315+ readonly organizationId: string;
316+ readonly keyId: string;
317+ }) => Effect.Effect<void, ApiKeyManagementError | OrgApiKeyNotFound>;
150318 }
151319 >()("@executor-js/cloud/ApiKeyService") {
152320 static WorkOS = Layer.effect(this)(
@@ -177,6 +345,40 @@ export class ApiKeyService extends Context.Service< · 24 unmodified lines
177345 workos
178346 .deleteApiKey(keyId)
179347 .pipe(Effect.mapError((cause) => new ApiKeyManagementError({ cause }))),
348+ listOrgKeys: ({ organizationId }) =>
349+ workos.listOrgApiKeys(organizationId).pipe(
350+ Effect.map((response) => orgListFromResponse(response, organizationId)),
351+ Effect.mapError((cause) => new ApiKeyManagementError({ cause })),
352+ ),
353+ createOrgKey: ({ organizationId, name }) =>
354+ workos.createOrgApiKey({ organizationId, name }).pipe(
355+ Effect.mapError((cause) => new ApiKeyManagementError({ cause })),
356+ Effect.flatMap((response) => {
357+ const created = orgCreatedFromResponse(response, organizationId);
358+ return created
359+ ? Effect.succeed(created)
360+ : Effect.fail(new ApiKeyManagementError({ cause: "invalid_create_response" }));
361+ }),
362+ ),
363+ // Ownership is checked against the ORG's decoded key listing before the
364+ // delete goes out. `deleteApiKey` is scope-blind — handed any key id it
365+ // would happily destroy a member's personal credential, or a key
366+ // belonging to a sibling org in the same workspace. The listing already
367+ // drops both of those (`orgListFromResponse` filters on owner type AND
368+ // organization), so filtering through it is the whole authorization.
369+ revokeOrgKey: ({ organizationId, keyId }) =>
370+ Effect.gen(function* () {
371+ const response = yield* workos
372+ .listOrgApiKeys(organizationId)
373+ .pipe(Effect.mapError((cause) => new ApiKeyManagementError({ cause })));
374+ const owned = orgListFromResponse(response, organizationId);
375+ if (!owned.some((key) => key.id === keyId)) {
376+ return yield* new OrgApiKeyNotFound({ keyId });
377+ }
378+ yield* workos
379+ .deleteApiKey(keyId)
380+ .pipe(Effect.mapError((cause) => new ApiKeyManagementError({ cause })));
381+ }),
180382 };
181383 }),
182384 );
apps/cloud/src/auth/api.ts+9−1
@@ -181,6 +181,15 @@ const McpApprovalErrors = [ · 180 unmodified lines
181181 /** Public auth endpoints — no authentication required */
182182 export class CloudAuthPublicApi extends HttpApiGroup.make("cloudAuthPublic")
183183 .add(HttpApiEndpoint.get("login", "/auth/login", { query: AuthLoginSearch }))
184+ // Sign-out is PUBLIC on purpose. The console posts it as a top-level form
185+ // navigation (the WorkOS hop is cross-origin, so it can't be a fetch), which
186+ // means an error response is rendered as the page — and a browser whose
187+ // session has already ended is exactly the browser most likely to click it
188+ // (a second tab, a re-submit from history, an expired or revoked session).
189+ // Behind SessionAuth all of those got a raw `{"_tag":"Unauthorized"}` screen
190+ // instead of being signed out. There is nothing to authorize here anyway:
191+ // the request can only end the session whose cookie it presents.
192+ .add(HttpApiEndpoint.post("logout", "/auth/logout"))
184193 .add(
185194 HttpApiEndpoint.get("callback", "/auth/callback", {
186195 query: AuthCallbackSearch,
@@ -201,7 +210,6 @@ export class CloudAuthApi extends HttpApiGroup.make("cloudAuth") · 14 unmodified lines
201210 error: AuthErrors,
202211 }),
203212 )
204− .add(HttpApiEndpoint.post("logout", "/auth/logout"))
205213 .add(
206214 HttpApiEndpoint.get("organizations", "/auth/organizations", {
207215 success: AuthOrganizationsResponse,
apps/cloud/src/auth/context.ts+9−4
@@ -1,7 +1,7 @@
11 import { Context, Effect, Layer } from "effect";
22 import { makeUserStore } from "../auth/user-store";
33 import { DbService } from "../db/db";
4−import { UserStoreError, tryPromiseService, withServiceLogging } from "./errors";
4+import { tryPromiseService, userStoreErrorFromFailure, withServiceLogging } from "./errors";
55
66 // ---------------------------------------------------------------------------
77 // UserStoreService — wraps the Drizzle-backed user store with Effect
@@ -9,11 +9,16 @@ import { UserStoreError, tryPromiseService, withServiceLogging } from "./errors" · 1 unmodified line
99
1010 type RawStore = ReturnType<typeof makeUserStore>;
1111
12+// `op` names the store call so every span reads `user_store.<operation>`
13+// instead of one undifferentiated "user_store" bucket, failures log which
14+// query actually failed, and — because the same `op` is threaded onto the
15+// public error alongside the classified driver reason — an error report is
16+// diagnosable without the trace.
1217 const makeService = (store: RawStore) => ({
13− use: <A>(fn: (s: RawStore) => Promise<A>) =>
18+ use: <A>(op: string, fn: (s: RawStore) => Promise<A>) =>
1419 withServiceLogging(
15− "user_store",
16− () => new UserStoreError(),
20+ `user_store.${op}`,
21+ (failure) => userStoreErrorFromFailure(op, failure),
1722 tryPromiseService(() => fn(store)),
1823 ),
1924 });
apps/cloud/src/auth/doc-gate.tsrenamed from apps/cloud/src/auth/ssr-gate.ts+71−23
@@ -1,19 +1,23 @@
11 // ---------------------------------------------------------------------------
2−// SSR auth gate — the server-side session check for DOCUMENT requests.
2+// Document auth gate — the server-side session check for DOCUMENT requests.
33 //
44 // The sealed `wos-session` cookie is verified right here in the worker
55 // (unseal + JWT check against cached JWKS — no per-request WorkOS round trip
6−// except token refresh), so by the time the SPA is served the server KNOWS
7−// who it's serving:
6+// except token refresh), so by the time the SPA shell is served the server
7+// KNOWS who it's serving:
88 //
99 // - signed out → 302 /login (carrying ?returnTo=) before any app HTML exists
1010 // - org-less → 302 /create-org (onboarding owns those sessions)
11−// - signed in → the document is served WITH the verified identity: the
12−// auth-hint travels to the SSR render via request-middleware context (the
13−// root loader picks it up), and is minted as a cookie when the browser
14−// doesn't hold a current one — so the very first paint is the real app
15−// shell, never a skeleton. The hint is display-only; /account/me remains
16−// the authority and the client keeps it fresh from then on.
11+// - signed in → the prerendered shell is served, and the auth-hint cookie is
12+// minted when the browser doesn't hold a current one — the client seeds its
13+// auth state from that cookie right after mount (there is no per-request
14+// render to dehydrate it into). The hint is display-only; /account/me
15+// remains the authority and the client keeps it fresh from then on.
16+//
17+// Beyond redirects, this gate is load-bearing for SESSION LIFETIME: WorkOS
18+// refresh tokens are single-use, and a verify that rotates the sealed session
19+// must deliver the new cookie to the browser or the next expiry logs the user
20+// out. Document navigations are where that rotation lands.
1721 //
1822 // Scope: GET/HEAD requests that are document navigations (sec-fetch-dest /
1923 // accept), excluding app-owned paths (/api, /mcp — they answer for themselves
@@ -23,6 +27,7 @@ · 3 unmodified lines
2327 import { createMiddleware } from "@tanstack/react-start";
2428 import { Effect, Exit, Layer, ManagedRuntime } from "effect";
2529
30+import { isValidOrgSlug } from "@executor-js/api";
2631 import {
2732 AUTH_HINT_COOKIE,
2833 AUTH_HINT_MAX_AGE_SECONDS,
@@ -35,8 +40,9 @@ import { isAppOwnedPath } from "../app-paths"; · 6 unmodified lines
3540 import { makeDbLayer } from "../db/db";
3641 import { makeUserStoreLayer, UserStoreService } from "./context";
3742 import { parseCookie } from "./cookies";
43+import { LAST_ORG_COOKIE } from "./last-org-cookie";
3844 import { sealedSessionDisplayName } from "./middleware";
39−import { browserOriginFromRequest } from "./request-origin";
45+import { authorizeOrganizationSelector } from "./organization";
4046 import { loginPath, safeReturnTo } from "./return-to";
4147 import { ONBOARDING_PATHS, PUBLIC_PATHS } from "./route-paths";
4248 import { WorkOSClient } from "./workos";
@@ -150,7 +156,7 @@ const organizationDisplay = async ( · 107 unmodified lines
150156 ): Promise<{ name: string; slug: string }> => {
151157 const exit = await getRuntime().runPromiseExit(
152158 Effect.flatMap(UserStoreService.asEffect(), (users) =>
153− users.use((store) => store.getOrganization(organizationId)),
159+ users.use("getOrganization", (store) => store.getOrganization(organizationId)),
154160 ).pipe(Effect.provide(Layer.provide(makeUserStoreLayer(), makeDbLayer()))),
155161 );
156162 return Exit.isSuccess(exit)
@@ -158,6 +164,23 @@ const organizationDisplay = async ( · 1 unmodified line
158164 : { name: "", slug: "" };
159165 };
160166
167+// Live membership check for the last-org cookie's slug. Same authorize path
168+// as any org selector — the cookie is a preference, so a slug the user can't
169+// access (stale after removal/deletion, or forged) resolves to null and the
170+// bare path falls through to today's canonicalize-onto-session-org behavior.
171+// Per-request store layers for the same reason as organizationDisplay.
172+const authorizeLastOrgSlug = async (
173+ userId: string,
174+ slug: string,
175+): Promise<{ readonly id: string } | null> => {
176+ const exit = await getRuntime().runPromiseExit(
177+ authorizeOrganizationSelector(userId, slug).pipe(
178+ Effect.provide(Layer.provide(makeUserStoreLayer(), makeDbLayer())),
179+ ),
180+ );
181+ return Exit.isSuccess(exit) ? exit.value : null;
182+};
183+
161184 const hintSetCookie = (hint: AuthHint) =>
162185 `${AUTH_HINT_COOKIE}=${encodeAuthHint(hint)}; ${HINT_COOKIE_ATTRIBUTES}; Max-Age=${AUTH_HINT_MAX_AGE_SECONDS}`;
163186
@@ -226,19 +249,44 @@ export const authGateMiddleware = createMiddleware({ type: "request" }).server( · 62 unmodified lines
226249 return redirect("/create-org", { refreshedSession: session.refreshedSession });
227250 }
228251
229− // Serve the document WITH the verified identity: the hint rides to the
230− // SSR render through middleware context (the root loader reads it), so
231− // the server paints the real authenticated shell — no loading state, no
232− // skeleton. The request origin rides along too: it's what the connect
233− // card's MCP URL is built from, and the server knows it (the SPA only
234− // learns `window.location.origin` after mount), so passing it here lets
235− // SSR render the real `https://…/<org>/mcp` instead of the client-side
236− // `http://127.0.0.1:4000` default — which would otherwise flash until
237− // hydration corrected it. Set-cookie writes ride on the rendered response.
252+ // A BARE console path (no org slug in the URL) canonicalizes onto the org
253+ // this browser last worked in (the last-org cookie), not the org pinned in
254+ // the session at login time — a multi-org user re-entering at `/` lands
255+ // where they left off. Slugged URLs never enter here (the URL names its
256+ // own scope), so this costs nothing on the steady state; the not-found
257+ // contract is untouched because an unknown-but-valid slug in the URL reads
258+ // as slugged, not bare. When the cookie matches the session's own org (the
259+ // overwhelmingly common single-org case) the client-side OrgSlugGate
260+ // already canonicalizes onto it, so skip the live membership check and the
261+ // redirect entirely.
262+ const lastOrgSlug = parseCookie(cookieHeader, LAST_ORG_COOKIE);
263+ const firstSegment = pathname.split("/")[1] ?? "";
264+ if (
265+ session.organizationId &&
266+ lastOrgSlug &&
267+ isValidOrgSlug(lastOrgSlug) &&
268+ !isValidOrgSlug(firstSegment) &&
269+ !ONBOARDING_PATHS.has(pathname)
270+ ) {
271+ const sessionOrgSlug = (await organizationDisplay(session.organizationId)).slug;
272+ if (lastOrgSlug !== sessionOrgSlug) {
273+ // The cookie is a preference, not an authority: only redirect onto an
274+ // org the caller actively belongs to (stale/forged values fall through
275+ // to today's session-org canonicalization).
276+ const lastOrg = await authorizeLastOrgSlug(session.userId, lastOrgSlug);
277+ if (lastOrg) {
278+ return redirect(`/${lastOrgSlug}${pathname === "/" ? "" : pathname}${url.search}`, {
279+ refreshedSession: session.refreshedSession,
280+ });
281+ }
282+ }
283+ }
284+
285+ // Serve the shell, minting the auth-hint cookie when the browser lacks a
286+ // current one so the client's post-mount cookie read seeds the verified
287+ // identity. Set-cookie writes ride on the shell response.
238288 const { hint, mint } = await resolveAuthHint(session, cookieHeader);
239− const result = await next({
240− context: { authHint: hint, origin: browserOriginFromRequest(request) },
241− });
289+ const result = await next();
242290 if (!mint && !session.refreshedSession) return result;
243291
244292 const response = new Response(result.response.body, result.response);
apps/cloud/src/auth/errors.ts+95−2
@@ -1,10 +1,103 @@
11 import { Data, Effect, Option, Predicate, Schema } from "effect";
22
3+// How a user-store call failed, classified from the driver cause. Safe to put
4+// on the wire and on a Sentry tag: it names a failure MODE, never a query, a
5+// value, or a customer.
6+export const USER_STORE_FAILURE_REASONS = [
7+ "connect_timeout",
8+ "connection_closed",
9+ "query",
10+ "unknown",
11+] as const;
12+
13+export type UserStoreFailureReason = (typeof USER_STORE_FAILURE_REASONS)[number];
14+
15+/**
16+ * The public failure of every cloud user-store call.
17+ *
18+ * It carries the two fields that make an issue diagnosable from the error
19+ * alone: which store call failed, and how. Before those existed the error had
20+ * an empty field set, so Sentry showed a titleless, messageless issue and the
21+ * only cause detail (the pretty-printed Effect cause in a Sentry `extra`) is
22+ * scrubbed server-side — the failing operation and the driver reason existed
23+ * only in the trace store. Same shape as `WorkOSError.status`: a small, safe
24+ * classification field threaded at the service boundary.
25+ */
326 export class UserStoreError extends Schema.TaggedErrorClass<UserStoreError>()(
427 "UserStoreError",
5− {},
28+ {
29+ /** The store call that failed, e.g. `getOrganization`. */
30+ operation: Schema.String,
31+ /** How it failed, classified from the driver cause chain. */
32+ reason: Schema.Literals(USER_STORE_FAILURE_REASONS),
33+ },
634 { httpApiStatus: 500 },
7−) {}
35+) {
36+ override get message(): string {
37+ return `user store ${this.operation} failed: ${this.reason}`;
38+ }
39+}
40+
41+/** Reasons a retry can plausibly clear: the query never reached a healthy
42+ * server. A `query` failure is deterministic and must not be retried. */
43+export const isTransientUserStoreReason = (reason: UserStoreFailureReason): boolean =>
44+ reason === "connect_timeout" || reason === "connection_closed";
45+
46+// postgres.js tags its connection failures with a string `code`
47+// (`CONNECT_TIMEOUT`, `CONNECTION_CLOSED`, …) and its query failures with the
48+// SQLSTATE. Drizzle re-throws both wrapped in its own "Failed query" error with
49+// the driver error in `.cause`, and the service adapter wraps that again, so
50+// the classification walks the chain rather than inspecting one level.
51+const MAX_CAUSE_DEPTH = 8;
52+
53+const REASON_BY_DRIVER_CODE: Readonly<Record<string, UserStoreFailureReason>> = {
54+ CONNECT_TIMEOUT: "connect_timeout",
55+ ETIMEDOUT: "connect_timeout",
56+ CONNECTION_CLOSED: "connection_closed",
57+ CONNECTION_ENDED: "connection_closed",
58+ CONNECTION_DESTROYED: "connection_closed",
59+ ECONNREFUSED: "connection_closed",
60+ ECONNRESET: "connection_closed",
61+};
62+
63+const stringCodeOf = (value: unknown): string | undefined => {
64+ if (typeof value !== "object" || value === null) return undefined;
65+ const code = (value as { readonly code?: unknown }).code;
66+ return typeof code === "string" ? code : undefined;
67+};
68+
69+const driverCodesOf = (failure: unknown): readonly string[] => {
70+ const codes: string[] = [];
71+ let current: unknown = isServiceAdapterError(failure) ? failure.cause : failure;
72+ for (
73+ let depth = 0;
74+ depth < MAX_CAUSE_DEPTH && current !== undefined && current !== null;
75+ depth++
76+ ) {
77+ const code = stringCodeOf(current);
78+ if (code !== undefined) codes.push(code);
79+ current =
80+ typeof current === "object" ? (current as { readonly cause?: unknown }).cause : undefined;
81+ }
82+ return codes;
83+};
84+
85+/** Classify a raw store failure. A recognised connection code wins; any other
86+ * driver code (a SQLSTATE) is a deterministic query failure; nothing at all is
87+ * `unknown`. */
88+export const userStoreReasonFromCause = (failure: unknown): UserStoreFailureReason => {
89+ const codes = driverCodesOf(failure);
90+ for (const code of codes) {
91+ const reason = REASON_BY_DRIVER_CODE[code];
92+ if (reason !== undefined) return reason;
93+ }
94+ return codes.length > 0 ? "query" : "unknown";
95+};
96+
97+/** Build the public `UserStoreError` for a store-adapter failure, naming the
98+ * operation the call site already knows and classifying the driver cause. */
99+export const userStoreErrorFromFailure = (operation: string, failure: unknown): UserStoreError =>
100+ new UserStoreError({ operation, reason: userStoreReasonFromCause(failure) });
8101
9102 export class WorkOSError extends Schema.TaggedErrorClass<WorkOSError>()(
10103 "WorkOSError",
apps/cloud/src/auth/handlers.ts+88−25
@@ -22,11 +22,13 @@ import { env } from "cloudflare:workers"; · 21 unmodified lines
2222 import { WorkOSError } from "./errors";
2323 import { WorkOSClient } from "./workos";
2424 import { AutumnService } from "../extensions/billing/service";
25+import { captureCauseEffect } from "../observability";
2526 import {
2627 hasPaidOrganizationSubscription,
2728 isOverFreeOrganizationLimit,
2829 shouldApplyFreeOrganizationLimit,
2930 } from "../extensions/billing/plans";
31+import { LAST_ORG_COOKIE } from "./last-org-cookie";
3032 import {
3133 ORG_SELECTOR_HEADER,
3234 authorizeOrganization,
@@ -143,8 +145,8 @@ const deleteResponseCookie = (response: HttpServerResponse.HttpServerResponse, n · 110 unmodified lines
143145 HttpServerResponse.setCookieUnsafe(response, name, "", DELETE_COOKIE_OPTIONS);
144146
145147 // ---------------------------------------------------------------------------
146−// Single non-protected API surface — public (login/callback) + session
147−// (me/logout/organizations/switch-organization). The session group has SessionAuth on it.
148+// Single non-protected API surface — public (login/callback/logout) + session
149+// (me/organizations/switch-organization). The session group has SessionAuth on it.
148150 // ---------------------------------------------------------------------------
149151
150152 export const NonProtectedApi = HttpApi.make("cloudWeb").add(CloudAuthPublicApi).add(CloudAuthApi);
@@ -201,7 +203,7 @@ export const CloudAuthPublicHandlers = HttpApiBuilder.group( · 50 unmodified lines
201203 const result = yield* workos.authenticateWithCode(query.code);
202204
203205 // Mirror the account locally
204− yield* users.use((s) => s.ensureAccount(result.user.id));
206+ yield* users.use("ensureAccount", (s) => s.ensureAccount(result.user.id));
205207
206208 let sealedSession = result.sealedSession;
207209
@@ -218,11 +220,25 @@ export const CloudAuthPublicHandlers = HttpApiBuilder.group( · 10 unmodified lines
218220 : null;
219221
220222 // Prefer the org in the URL that sent the user to login. If the URL
221− // is bare, or not an org route, fall back to WorkOS's org and then to
222− // the first active membership for org-less sessions. Pending
223− // memberships are skipped because refreshing into one 400s and would
224− // bypass invite consent.
225− let targetOrganizationId = requestedOrg?.id ?? result.organizationId ?? null;
223+ // is bare, or not an org route, prefer the org this browser last
224+ // worked in (the last-org cookie — it outlives the session precisely
225+ // so a fresh login lands where the user left off), then WorkOS's
226+ // org, then the first active membership for org-less sessions.
227+ // Pending memberships are skipped because refreshing into one 400s
228+ // and would bypass invite consent. The cookie is membership-checked
229+ // like any selector, so a stale one just falls through.
230+ let targetOrganizationId = requestedOrg?.id ?? null;
231+ if (!targetOrganizationId && !requestedOrgSelector) {
232+ const lastOrgSlug = request.cookies[LAST_ORG_COOKIE];
233+ const lastOrg =
234+ lastOrgSlug && isValidOrgSlug(lastOrgSlug)
235+ ? yield* authorizeOrganizationSelector(result.user.id, lastOrgSlug).pipe(
236+ Effect.orElseSucceed(() => null),
237+ )
238+ : null;
239+ targetOrganizationId = lastOrg?.id ?? null;
240+ }
241+ targetOrganizationId ??= result.organizationId ?? null;
226242 if (!targetOrganizationId && !requestedOrgSelector) {
227243 const memberships = yield* workos.listUserMemberships(result.user.id);
228244 const existingActive = memberships.data.find((m) => m.status === "active");
@@ -259,6 +275,42 @@ export const CloudAuthPublicHandlers = HttpApiBuilder.group( · 30 unmodified lines
259275 );
260276 }),
261277 )
278+ .handleRaw("logout", ({ request }) =>
279+ Effect.gen(function* () {
280+ const workos = yield* WorkOSClient;
281+ // The session this browser presents, NOT one the middleware vouched
282+ // for — signing out of a session that has already ended must still
283+ // sign the browser out (see the group declaration in ./api.ts).
284+ const sealedSession = request.cookies["wos-session"] ?? "";
285+
286+ // WorkOS's documented sign-out: send the browser through the WorkOS
287+ // logout endpoint, which ends the AuthKit session upstream and then
288+ // redirects to the registered sign-out URL. Without this hop, the
289+ // hosted session survives and the next "Sign in" silently
290+ // re-authenticates (issue #1445). Fail-open when the cookie won't
291+ // unseal — there is then nothing to end upstream, and local sign-out
292+ // must still complete, so fall back to "/".
293+ const origin = env.VITE_PUBLIC_SITE_URL ?? "";
294+ const logoutUrl = sealedSession
295+ ? yield* workos.logoutUrl(sealedSession, origin ? `${origin}/` : undefined)
296+ : null;
297+
298+ const response = HttpServerResponse.redirect(logoutUrl ?? "/", { status: 302 });
299+
300+ // Drop only what this browser actually presented. Both cookies are
301+ // SameSite=Lax, so a cross-site form POST carries neither — it gets
302+ // the bare redirect and cannot be used to sign anyone out.
303+ if (!sealedSession && request.cookies[AUTH_HINT_COOKIE] === undefined) return response;
304+
305+ // The auth-hint travels with the session: leaving it behind would
306+ // make the next page load optimistically paint the app shell for a
307+ // signed-out browser.
308+ return deleteResponseCookie(
309+ deleteResponseCookie(response, "wos-session"),
310+ AUTH_HINT_COOKIE,
311+ );
312+ }),
313+ )
262314 // CLI device-login discovery. The WorkOS device endpoints live on the
263315 // WorkOS API host (`WORKOS_API_URL`, or api.workos.com in production,
264316 // the SAME base the SDK uses, so e2e points the CLI at the emulator with
@@ -304,17 +356,6 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 39 unmodified lines
304356 };
305357 }),
306358 )
307− .handleRaw("logout", () =>
308− Effect.succeed(
309− // The auth-hint travels with the session: leaving it behind would
310− // make the next page load optimistically paint the app shell for a
311− // signed-out browser.
312− deleteResponseCookie(
313− deleteResponseCookie(HttpServerResponse.redirect("/", { status: 302 }), "wos-session"),
314− AUTH_HINT_COOKIE,
315− ),
316− ),
317− )
318359 .handle("organizations", () =>
319360 Effect.gen(function* () {
320361 const workos = yield* WorkOSClient;
@@ -375,7 +416,9 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 54 unmodified lines
375416 ),
376417 { concurrency: 3 },
377418 ).pipe(
378− Effect.catchTag("AutumnError", () => Effect.fail(new WorkOSError())),
419+ // Any Autumn failure here (outage or missing customer) leaves the
420+ // paid/free split unknown, and the limit must fail closed.
421+ Effect.mapError(() => new WorkOSError()),
379422 Effect.map((ids) => new Set(ids.filter(Predicate.isNotNull))),
380423 );
381424
@@ -387,10 +430,28 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 5 unmodified lines
387430 const org = yield* workos.createOrganization(name);
388431 yield* workos.createMembership(org.id, session.accountId, "admin");
389432 // `upsertOrganization` mints the slug at insert — no separate heal step.
390− const mirrored = yield* users.use((s) =>
433+ const mirrored = yield* users.use("upsertOrganization", (s) =>
391434 s.upsertOrganization({ id: org.id, name: org.name }),
392435 );
393436
437+ // Provision the org's billing customer while we're the ones creating
438+ // the org. Without this the first billing call an org ever makes is a
439+ // non-creating one (balance check / usage track), which 404s and keeps
440+ // 404ing — unlimited unbilled executions. Non-fatal: a billing blip
441+ // must not block signup, and the billing seam heals a customer that
442+ // is still missing later.
443+ yield* autumn.ensureCustomer(org.id).pipe(
444+ Effect.catch((error) =>
445+ Effect.gen(function* () {
446+ yield* Effect.logWarning(
447+ "createOrganization: could not provision the Autumn customer",
448+ { organizationId: org.id, error },
449+ );
450+ yield* captureCauseEffect(error);
451+ }),
452+ ),
453+ );
454+
394455 // Try to attach the new org to the current session. This can fail
395456 // (or silently return a session still scoped to the old org) when
396457 // the caller's current session is stale — most commonly after the
@@ -441,7 +502,7 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 44 unmodified lines
441502
442503 // The typed confirmation must match the org's current name — the same
443504 // label the settings page shows. Trimmed on both sides.
444− const org = yield* users.use((s) => s.getOrganization(organizationId));
505+ const org = yield* users.use("getOrganization", (s) => s.getOrganization(organizationId));
445506 if (!org || payload.confirmName.trim() !== org.name.trim()) {
446507 return yield* new OrganizationDeletionForbidden();
447508 }
@@ -459,7 +520,7 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 11 unmodified lines
459520 // everyone (unreachable) but its secrets/tenant rows linger orphaned —
460521 // alert loudly so that window gets swept, then surface the failure.
461522 yield* users
462− .use((s) => s.deleteOrganizationCascade(organizationId))
523+ .use("deleteOrganizationCascade", (s) => s.deleteOrganizationCascade(organizationId))
463524 .pipe(
464525 Effect.tapError((error) =>
465526 Effect.logError(
@@ -475,7 +536,9 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 9 unmodified lines
475536 yield* autumn
476537 .use((client) => client.customers.delete({ customerId: organizationId }))
477538 .pipe(
478− Effect.catchTag("AutumnError", (error) =>
539+ // Includes the "customer never existed" answer: nothing to cancel
540+ // is a fine outcome for a deleted org, and it is still worth a line.
541+ Effect.catch((error) =>
479542 Effect.logWarning("deleteOrganization: failed to delete Autumn customer", {
480543 organizationId,
481544 error,
@@ -560,7 +623,7 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 78 unmodified lines
560623 // Mirror the org locally so domain tables can FK against it; the
561624 // upsert mints the slug at insert — no separate heal step.
562625 const org = yield* workos.getOrganization(invitation.organizationId);
563− const mirrored = yield* users.use((s) =>
626+ const mirrored = yield* users.use("upsertOrganization", (s) =>
564627 s.upsertOrganization({ id: org.id, name: org.name }),
565628 );
566629
apps/cloud/src/auth/jwks-cache.node.test.ts+182−1
@@ -9,7 +9,7 @@ import { · 8 unmodified lines
99 type KeyLike,
1010 } from "jose";
1111
12−import { createCachedRemoteJWKSet } from "./jwks-cache";
12+import { createCachedRemoteJWKSet, type JwksStore, type StoredJwks } from "./jwks-cache";
1313
1414 const issuer = "https://test-authkit.example.com";
1515 const audience = "client_test_fixture";
@@ -65,6 +65,40 @@ const makeFetchHarness = (initialKeys: ReadonlyArray<JWK>): FetchHarness => { · 49 unmodified lines
6565 };
6666 };
6767
68+interface StoreHarness extends JwksStore {
69+ readonly seed: (stored: StoredJwks) => void;
70+ readonly reads: () => number;
71+ readonly writes: () => number;
72+}
73+
74+/** Stands in for the Workers Cache API: shared across "isolates", in memory. */
75+const makeStoreHarness = (): StoreHarness => {
76+ const entries = new Map<string, StoredJwks>();
77+ let reads = 0;
78+ let writes = 0;
79+ return {
80+ get: async (url) => {
81+ reads++;
82+ return entries.get(url.toString()) ?? null;
83+ },
84+ put: async (url, stored) => {
85+ writes++;
86+ entries.set(url.toString(), stored);
87+ },
88+ seed: (stored) => {
89+ entries.set(jwksUrl.toString(), stored);
90+ },
91+ reads: () => reads,
92+ writes: () => writes,
93+ };
94+};
95+
96+/** A fetch that always fails, standing in for a slow/down key server. */
97+const failingFetch: typeof globalThis.fetch = async () => {
98+ // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- test double: `fetch` signals an unreachable key server by rejecting
99+ throw new Error("JWKS endpoint unreachable");
100+};
101+
68102 describe("createCachedRemoteJWKSet", () => {
69103 it("FAILING-WITHOUT-CACHE: N verifications hit JWKS endpoint only once within TTL", async () => {
70104 const kp = await generateRotatableKeypair("k1");
@@ -168,4 +202,151 @@ describe("createCachedRemoteJWKSet", () => { · 97 unmodified lines
168202 await jwtVerify(t1, jwks, { issuer, audience });
169203 expect(harness.callCount()).toBe(2);
170204 });
205+
206+ // -------------------------------------------------------------------------
207+ // Cross-isolate store — the cold-isolate path that caused the 2026-08-18
208+ // 500s: no module-scope entry, so every verify paid an upstream fetch.
209+ // -------------------------------------------------------------------------
210+
211+ it("a cold isolate serves from the store instead of going upstream", async () => {
212+ const kp = await generateRotatableKeypair("k1");
213+ const store = makeStoreHarness();
214+
215+ // First isolate: cold everywhere, so it fetches and populates the store.
216+ const warm = makeFetchHarness([kp.publicJwk]);
217+ const first = createCachedRemoteJWKSet(jwksUrl, { fetch: warm.fetch, store });
218+ const t1 = await sign(kp);
219+ await jwtVerify(t1, first, { issuer, audience });
220+ expect(warm.callCount()).toBe(1);
221+ expect(store.writes()).toBe(1);
222+
223+ // A brand-new isolate (fresh module scope). If it reaches upstream at all
224+ // the fetch fails, so verifying proves the store answered.
225+ const second = createCachedRemoteJWKSet(jwksUrl, { fetch: failingFetch, store });
226+ const { payload } = await jwtVerify(t1, second, { issuer, audience });
227+ expect(payload.sub).toBe("user_test");
228+ expect(store.reads()).toBeGreaterThan(0);
229+ });
230+
231+ it("keeps serving the last good keys when the key server is down", async () => {
232+ const kp = await generateRotatableKeypair("k1");
233+ const harness = makeFetchHarness([kp.publicJwk]);
234+ let upstreamUp = true;
235+ const flaky: typeof globalThis.fetch = (...args) =>
236+ upstreamUp ? harness.fetch(...args) : failingFetch(...args);
237+
238+ // ttl short enough that the next call is past it, stale window generous.
239+ const jwks = createCachedRemoteJWKSet(jwksUrl, {
240+ fetch: flaky,
241+ store: null,
242+ ttlMs: 10,
243+ staleMaxMs: 60_000,
244+ });
245+
246+ const token = await sign(kp);
247+ await jwtVerify(token, jwks, { issuer, audience });
248+
249+ upstreamUp = false;
250+ await new Promise((r) => setTimeout(r, 20));
251+
252+ // Past the TTL with a dead upstream: the cached keys still verify.
253+ const { payload } = await jwtVerify(token, jwks, { issuer, audience });
254+ expect(payload.sub).toBe("user_test");
255+ });
256+
257+ it("stops serving stale keys once the stale window closes", async () => {
258+ const kp = await generateRotatableKeypair("k1");
259+ const harness = makeFetchHarness([kp.publicJwk]);
260+ let upstreamUp = true;
261+ const flaky: typeof globalThis.fetch = (...args) =>
262+ upstreamUp ? harness.fetch(...args) : failingFetch(...args);
263+
264+ const jwks = createCachedRemoteJWKSet(jwksUrl, {
265+ fetch: flaky,
266+ store: null,
267+ ttlMs: 5,
268+ staleMaxMs: 10,
269+ });
270+
271+ const token = await sign(kp);
272+ await jwtVerify(token, jwks, { issuer, audience });
273+
274+ upstreamUp = false;
275+ await new Promise((r) => setTimeout(r, 30));
276+
277+ // Beyond staleMaxMs the keys are no longer trustworthy — fail, don't
278+ // silently honour a key set we can no longer confirm.
279+ await expect(jwtVerify(token, jwks, { issuer, audience })).rejects.toThrow();
280+ });
281+
282+ it("attributes background revalidation to fetchCount but not blockingFetchCount", async () => {
283+ const kp = await generateRotatableKeypair("k1");
284+ const harness = makeFetchHarness([kp.publicJwk]);
285+ const jwks = createCachedRemoteJWKSet(jwksUrl, {
286+ fetch: harness.fetch,
287+ store: null,
288+ ttlMs: 10,
289+ staleMaxMs: 60_000,
290+ });
291+
292+ const token = await sign(kp);
293+ await jwtVerify(token, jwks, { issuer, audience });
294+ expect(jwks.inspect().blockingFetchCount).toBe(1);
295+
296+ await new Promise((r) => setTimeout(r, 20));
297+
298+ // Past the TTL: served from the stale entry, revalidated behind it. The
299+ // caller waited on nothing, so latency must not be attributed to it.
300+ const before = jwks.inspect();
301+ await jwtVerify(token, jwks, { issuer, audience });
302+ const after = jwks.inspect();
303+
304+ expect(after.blockingFetchCount).toBe(before.blockingFetchCount);
305+ expect(after.fetchCount).toBeGreaterThan(before.fetchCount);
306+ });
307+
308+ it("records a cold-isolate store read as a store hit, not a fetch", async () => {
309+ const kp = await generateRotatableKeypair("k1");
310+ const store = makeStoreHarness();
311+ const warm = makeFetchHarness([kp.publicJwk]);
312+
313+ const first = createCachedRemoteJWKSet(jwksUrl, { fetch: warm.fetch, store });
314+ const token = await sign(kp);
315+ await jwtVerify(token, first, { issuer, audience });
316+
317+ const second = createCachedRemoteJWKSet(jwksUrl, { fetch: failingFetch, store });
318+ await jwtVerify(token, second, { issuer, audience });
319+
320+ const stats = second.inspect();
321+ expect(stats.storeHitCount).toBe(1);
322+ expect(stats.blockingFetchCount).toBe(0);
323+ });
324+
325+ it("does not block a request on revalidation once keys are cached", async () => {
326+ const kp = await generateRotatableKeypair("k1");
327+ const harness = makeFetchHarness([kp.publicJwk]);
328+ let hang = false;
329+ const slowAfterFirst: typeof globalThis.fetch = async (...args) => {
330+ if (hang) await new Promise((r) => setTimeout(r, 5_000));
331+ return harness.fetch(...args);
332+ };
333+
334+ const jwks = createCachedRemoteJWKSet(jwksUrl, {
335+ fetch: slowAfterFirst,
336+ store: null,
337+ ttlMs: 10,
338+ staleMaxMs: 60_000,
339+ });
340+
341+ const token = await sign(kp);
342+ await jwtVerify(token, jwks, { issuer, audience });
343+
344+ hang = true;
345+ await new Promise((r) => setTimeout(r, 20));
346+
347+ // The revalidation behind this call hangs for 5s; the verify must not.
348+ const startedAt = Date.now();
349+ await jwtVerify(token, jwks, { issuer, audience });
350+ expect(Date.now() - startedAt).toBeLessThan(1_000);
351+ });
171352 });
apps/cloud/src/auth/jwks-cache.ts+256−26
@@ -1,5 +1,5 @@
11 // ---------------------------------------------------------------------------
2−// In-memory JWKS cache for MCP JWT verification.
2+// JWKS cache for JWT verification (session + MCP auth).
33 // ---------------------------------------------------------------------------
44 //
55 // Cloudflare Workers boot many short-lived isolates. `createRemoteJWKSet`'s
@@ -7,14 +7,32 @@ · 1 unmodified line
77 // fetches per hour because each new isolate starts cold. Production p99 for
88 // `mcp.auth.jwt_verify` was 1.7s — almost entirely the JWKS fetch.
99 //
10−// This module offers a drop-in `createCachedRemoteJWKSet` that:
10+// Module-scope memory alone only helps while an isolate stays warm. When
11+// `workos.session.local_verify` started missing ~92% of the time (2026-08-18),
12+// every miss paid a fresh upstream fetch — p50 3s, and the tail crossed the 5s
13+// request timeout, turning a slow key-server into `AbortError` → 500 on every
14+// authenticated API route and 503 on `/mcp`. A transient key-server blip must
15+// never read as an auth failure, and a cold isolate must not have to go
16+// upstream at all.
1117 //
12−// * Caches the JSON Web Key Set in module-scope memory for a configurable
13−// TTL (default 1 hour).
14−// * Single-flights concurrent fetches so a stampede of verifies during a
15−// cache miss only fires one upstream request.
16−// * Force-refreshes once when verification fails with a cached key, so
17−// genuine key rotation isn't blocked by the TTL.
18+// So this module layers three caches, fastest first:
19+//
20+// 1. Module-scope memory — free, but dies with the isolate.
21+// 2. A cross-isolate store (the Workers Cache API by default) — colo-local,
22+// survives isolate recycling, so a cold isolate reads keys in ~1ms
23+// instead of paying an upstream round trip.
24+// 3. The upstream JWKS endpoint.
25+//
26+// On top of that it is stale-while-revalidate: once past `ttlMs` a usable key
27+// set is served immediately and refreshed in the background, and if a refresh
28+// fails we keep serving the last good keys until `staleMaxMs`. Only a fully
29+// cold path (no memory, no store) ever blocks on the network.
30+//
31+// Serving stale keys is safe in a way that serving a stale *token* would not
32+// be: key sets rotate on the order of days, tokens are still signature- and
33+// expiry-checked against them, and a token whose `kid` is absent forces a real
34+// refresh before it is rejected (see `get`). `staleMaxMs` bounds how long a
35+// retired key can still be honoured.
1836 //
1937 // The returned function is a `JWTVerifyGetKey` and slots directly into
2038 // `jose.jwtVerify`. It also exposes `forceRefresh()` so the verify path can
@@ -32,6 +50,21 @@ import { · 11 unmodified lines
3250 import { Schema } from "effect";
3351 import { JWKSNoMatchingKey } from "jose/errors";
3452
53+/**
54+ * A cross-isolate key-set store. Defaults to the Workers Cache API; tests and
55+ * non-Workers hosts pass their own, or `null` to stay memory-only.
56+ */
57+export interface JwksStore {
58+ readonly get: (url: URL) => Promise<StoredJwks | null>;
59+ readonly put: (url: URL, stored: StoredJwks) => Promise<void>;
60+}
61+
62+export interface StoredJwks {
63+ readonly jwks: JSONWebKeySet;
64+ /** Epoch ms of the upstream fetch these keys came from. */
65+ readonly fetchedAt: number;
66+}
67+
3568 export interface CachedRemoteJWKSetOptions {
3669 /**
3770 * How long a successful fetch is considered fresh. Defaults to 1 hour —
@@ -39,20 +72,56 @@ export interface CachedRemoteJWKSetOptions { · 1 unmodified line
3972 * failure handles unscheduled rotations.
4073 */
4174 readonly ttlMs?: number;
75+ /**
76+ * How long past `ttlMs` a key set may still be served while refreshes are
77+ * failing. Defaults to 24h — long enough to ride out a key-server outage,
78+ * short enough to bound how long a retired key stays honoured.
79+ */
80+ readonly staleMaxMs?: number;
4281 /** Override the fetch implementation for tests. */
4382 readonly fetch?: typeof globalThis.fetch;
4483 /** HTTP request timeout. Defaults to 5s, matching jose. */
4584 readonly timeoutMs?: number;
85+ /**
86+ * Cross-isolate store. Defaults to the Workers Cache API when available,
87+ * `null` disables the layer (memory-only).
88+ */
89+ readonly store?: JwksStore | null;
4690 }
4791
4892 export interface CachedRemoteJWKSet extends JWTVerifyGetKey {
4993 /** Drop the cached JWKS so the next call refetches. */
5094 readonly forceRefresh: () => void;
51− /** Inspect the current cache state (testing/diagnostics). */
52− readonly inspect: () => { fetchedAt: number | null; hasJwks: boolean };
95+ /**
96+ * Inspect the current cache state (testing/diagnostics/span annotation).
97+ * `fetchCount`/`fetchFailureCount` are lifetime counters for this resolver
98+ * instance; callers snapshot them around a verify to tell a cache hit from
99+ * a live upstream fetch (the Aug 2026 latency regression was exactly this
100+ * cache silently missing on ~92% of verifies, invisible in traces).
101+ *
102+ * `blockingFetchCount` counts only fetches a verify actually WAITED on.
103+ * Under stale-while-revalidate `fetchCount` also moves for background
104+ * revalidation the caller never paid for, so latency attribution wants
105+ * `blockingFetchCount`. `storeHitCount` counts cold-isolate reads answered
106+ * by the cross-isolate store instead of going upstream.
107+ */
108+ readonly inspect: () => {
109+ fetchedAt: number | null;
110+ hasJwks: boolean;
111+ fetchCount: number;
112+ fetchFailureCount: number;
113+ blockingFetchCount: number;
114+ storeHitCount: number;
115+ lastFetchDurationMs: number | null;
116+ /** Wall-clock of the last cross-isolate store read (I/O). */
117+ lastStoreReadMs: number | null;
118+ /** Wall-clock of the last key resolution — WebCrypto `importKey`. */
119+ lastResolveMs: number | null;
120+ };
53121 }
54122
55123 const DEFAULT_TTL_MS = 60 * 60 * 1000;
124+const DEFAULT_STALE_MAX_MS = 24 * 60 * 60 * 1000;
56125 const DEFAULT_TIMEOUT_MS = 5000;
57126
58127 const JsonWebKey = Schema.Record(Schema.String, Schema.Unknown);
@@ -73,6 +142,26 @@ interface CacheEntry { · 14 unmodified lines
73142 resolver: (protectedHeader: JWTHeaderParameters, token?: FlattenedJWSInput) => Promise<KeyLike>;
74143 }
75144
145+const entryFrom = (stored: StoredJwks): CacheEntry => ({
146+ jwks: stored.jwks,
147+ fetchedAt: stored.fetchedAt,
148+ resolver: createLocalJWKSet(stored.jwks),
149+});
150+
151+/**
152+ * Cache upkeep (store writes, background revalidation) is best effort: it must
153+ * never fail or delay the verify that happened to trigger it.
154+ */
155+const ignoreFailure = async (work: Promise<unknown>): Promise<void> => {
156+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: best-effort cache upkeep must not surface as a verify failure
157+ try {
158+ await work;
159+ } catch {
160+ // Deliberately swallowed — the caller already has usable keys, or will
161+ // take the upstream path on its next miss.
162+ }
163+};
164+
76165 const fetchJwksOnce = async (
77166 url: URL,
78167 fetchImpl: typeof globalThis.fetch,
@@ -107,17 +196,74 @@ const fetchJwksOnce = async ( · 28 unmodified lines
107196 }
108197 };
109198
199+// ---------------------------------------------------------------------------
200+// Workers Cache API store
201+// ---------------------------------------------------------------------------
202+//
203+// Keyed by the JWKS URL itself. We store our own Response rather than the
204+// upstream one so the body is already-validated JSON and the cache headers are
205+// ours: `max-age` covers the stale window, because an entry past `ttlMs` is
206+// still useful to us as a stale fallback.
207+
208+const STORE_HEADER_FETCHED_AT = "x-jwks-fetched-at";
209+
210+/** `caches.default` is a Workers extension the standard lib type omits. */
211+type WorkersCacheStorage = CacheStorage & { readonly default?: Cache };
212+
213+const workersCacheStore = (staleMaxMs: number): JwksStore | null => {
214+ if (typeof caches === "undefined") return null;
215+ const open = (): Cache | null => (caches as WorkersCacheStorage).default ?? null;
216+
217+ return {
218+ get: async (url) => {
219+ const cache = open();
220+ if (!cache) return null;
221+ const hit = await cache.match(url.toString());
222+ if (!hit) return null;
223+ const fetchedAtHeader = hit.headers.get(STORE_HEADER_FETCHED_AT);
224+ const fetchedAt = fetchedAtHeader === null ? Number.NaN : Number(fetchedAtHeader);
225+ if (!Number.isFinite(fetchedAt)) return null;
226+ const body = await hit.json();
227+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: a corrupt cache entry must degrade to a miss, never fail the verify
228+ try {
229+ await decodeJsonWebKeySetPayload(body);
230+ } catch {
231+ return null;
232+ }
233+ return { jwks: body as JSONWebKeySet, fetchedAt };
234+ },
235+ put: async (url, stored) => {
236+ const cache = open();
237+ if (!cache) return;
238+ const maxAgeSeconds = Math.max(1, Math.floor(staleMaxMs / 1000));
239+ await cache.put(
240+ url.toString(),
241+ new Response(JSON.stringify(stored.jwks), {
242+ status: 200,
243+ headers: {
244+ "content-type": "application/json",
245+ "cache-control": `max-age=${maxAgeSeconds}`,
246+ [STORE_HEADER_FETCHED_AT]: String(stored.fetchedAt),
247+ },
248+ }),
249+ );
250+ },
251+ };
252+};
253+
110254 /**
111255 * Creates a cached, single-flight, force-refreshable JWKS resolver compatible
112256 * with `jose.jwtVerify`. Drop-in replacement for `createRemoteJWKSet` for the
113− * MCP auth path — see module header for why we don't just use jose's built-in.
257+ * auth paths — see module header for why we don't just use jose's built-in.
114258 */
115259 export const createCachedRemoteJWKSet = (
116260 url: URL,
117261 options: CachedRemoteJWKSetOptions = {},
118262 ): CachedRemoteJWKSet => {
119263 const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
264+ const staleMaxMs = Math.max(options.staleMaxMs ?? DEFAULT_STALE_MAX_MS, ttlMs);
120265 const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
266+ const store = options.store === undefined ? workersCacheStore(staleMaxMs) : options.store;
121267 // Capture the fetch impl lazily so consumers can swap globalThis.fetch
122268 // (tests do this) without us snapshotting a stale reference.
123269 const fetchImpl = (): typeof globalThis.fetch =>
@@ -125,39 +271,116 @@ export const createCachedRemoteJWKSet = ( · 1 unmodified line
125271
126272 let entry: CacheEntry | null = null;
127273 let inflight: Promise<CacheEntry> | null = null;
274+ let fetchCount = 0;
275+ let fetchFailureCount = 0;
276+ let blockingFetchCount = 0;
277+ let storeHitCount = 0;
278+ let lastFetchDurationMs: number | null = null;
279+ let lastStoreReadMs: number | null = null;
280+ let lastResolveMs: number | null = null;
281+
282+ const isFresh = (candidate: CacheEntry): boolean => Date.now() - candidate.fetchedAt < ttlMs;
283+ const isUsable = (candidate: CacheEntry): boolean =>
284+ Date.now() - candidate.fetchedAt < staleMaxMs;
128285
129286 const refresh = (): Promise<CacheEntry> => {
130287 if (inflight) return inflight;
288+ const startedAt = Date.now();
289+ fetchCount += 1;
131290 inflight = (async () => {
132291 const jwks = await fetchJwksOnce(url, fetchImpl(), timeoutMs);
133− const next: CacheEntry = {
134− jwks,
135− fetchedAt: Date.now(),
136− resolver: createLocalJWKSet(jwks),
137− };
292+ const next = entryFrom({ jwks, fetchedAt: Date.now() });
138293 entry = next;
294+ if (store) {
295+ await ignoreFailure(store.put(url, { jwks: next.jwks, fetchedAt: next.fetchedAt }));
296+ }
139297 return next;
140− })().finally(() => {
141− inflight = null;
142− });
298+ })()
299+ .then(
300+ (next) => next,
301+ (error: unknown) => {
302+ fetchFailureCount += 1;
303+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: counting a fetch failure must preserve the original rejection for jose
304+ throw error;
305+ },
306+ )
307+ .finally(() => {
308+ lastFetchDurationMs = Date.now() - startedAt;
309+ inflight = null;
310+ });
143311 return inflight;
144312 };
145313
146− const ensureFresh = async (forceRefresh: boolean): Promise<CacheEntry> => {
147− if (forceRefresh) return refresh();
148− if (entry && Date.now() - entry.fetchedAt < ttlMs) return entry;
314+ /** Fire-and-forget revalidation behind a stale hit. */
315+ const refreshInBackground = (): void => {
316+ void ignoreFailure(refresh());
317+ };
318+
319+ const loadFromStore = async (): Promise<CacheEntry | null> => {
320+ if (!store) return null;
321+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: the L2 store is an optimization; any failure degrades to an upstream fetch
322+ try {
323+ const storeReadStartedAt = Date.now();
324+ const stored = await store.get(url);
325+ lastStoreReadMs = Date.now() - storeReadStartedAt;
326+ if (!stored) return null;
327+ const candidate = entryFrom(stored);
328+ if (!isUsable(candidate)) return null;
329+ entry = candidate;
330+ storeHitCount += 1;
331+ return candidate;
332+ } catch {
333+ return null;
334+ }
335+ };
336+
337+ /** A refresh the caller waits on — the only kind that costs it latency. */
338+ const refreshBlocking = (): Promise<CacheEntry> => {
339+ blockingFetchCount += 1;
149340 return refresh();
150341 };
151342
343+ const ensureFresh = async (forceRefresh: boolean): Promise<CacheEntry> => {
344+ if (forceRefresh) return refreshBlocking();
345+ if (entry && isFresh(entry)) return entry;
346+
347+ // Memory is stale but still usable: serve it now, revalidate behind it.
348+ if (entry && isUsable(entry)) {
349+ refreshInBackground();
350+ return entry;
351+ }
352+
353+ // Cold isolate — the L2 store saves us the upstream round trip.
354+ const stored = await loadFromStore();
355+ if (stored) {
356+ if (!isFresh(stored)) refreshInBackground();
357+ return stored;
358+ }
359+
360+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: a failed refresh must fall back to stale keys rather than fail the verify
361+ try {
362+ return await refreshBlocking();
363+ } catch (error) {
364+ // Upstream is slow or down. Last good keys beat failing every request.
365+ if (entry && isUsable(entry)) return entry;
366+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: nothing usable is cached, so the upstream failure is the real answer
367+ throw error;
368+ }
369+ };
370+
152371 const get: JWTVerifyGetKey = async (protectedHeader, token) => {
153372 const current = await ensureFresh(false);
154373 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: jose JWTVerifyGetKey retry path is defined by thrown resolver failures
155374 try {
156− return await current.resolver(protectedHeader, token);
375+ const resolveStartedAt = Date.now();
376+ const key = await current.resolver(protectedHeader, token);
377+ lastResolveMs = Date.now() - resolveStartedAt;
378+ return key;
157379 } catch (error) {
158− // Likely cause: keys rotated upstream after our TTL window started.
159− // Refetch once and try again. Anything still failing bubbles up so
160− // jose can classify it (we do not silently swallow real failures).
380+ // Likely cause: keys rotated upstream after our TTL window started, or
381+ // we answered from a stale entry. Refetch once and try again. Anything
382+ // still failing bubbles up so jose can classify it (we do not silently
383+ // swallow real failures).
161384 if (!isJwksNoMatchingKey(error)) {
162385 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: jose JWTVerifyGetKey requires preserving upstream resolver rejection
163386 throw error;
@@ -177,6 +400,13 @@ export const createCachedRemoteJWKSet = ( · 13 unmodified lines
177400 value: () => ({
178401 fetchedAt: entry?.fetchedAt ?? null,
179402 hasJwks: entry !== null,
403+ fetchCount,
404+ fetchFailureCount,
405+ blockingFetchCount,
406+ storeHitCount,
407+ lastFetchDurationMs,
408+ lastStoreReadMs,
409+ lastResolveMs,
180410 }),
181411 });
182412 return result;
apps/cloud/src/auth/last-org-cookie.ts+32−0
@@ -0,0 +1,32 @@
1+// ---------------------------------------------------------------------------
2+// Last-visited-org cookie — the browser's "which org was I working in" memory.
3+//
4+// Org switching is stateless by design (the URL slug scopes every request; see
5+// organization.ts), which leaves nothing that remembers the last org across
6+// entries: the sealed session still carries the org pinned at LOGIN time, so a
7+// bare URL (`executor.sh/`) would always canonicalize onto that first org. This
8+// cookie fills the gap: the client records the slug of the org it's verifiably
9+// viewing, and the two bare-entry deciders honor it —
10+//
11+// - the document auth gate redirects bare document paths onto it (doc-gate.ts)
12+// - the login callback prefers it when picking the org for a fresh session
13+// with a bare returnTo (handlers.ts)
14+//
15+// It is a PREFERENCE, never an authority: both readers re-check live membership
16+// through the same authorize path as any org selector, so a stale or forged
17+// value at worst falls back to today's behavior. Not HttpOnly — the client is
18+// the writer. Deliberately NOT cleared on logout: surviving the session is what
19+// lets the next login land on the last-used org.
20+// ---------------------------------------------------------------------------
21+
22+export const LAST_ORG_COOKIE = "executor-last-org";
23+
24+/** Outlives the 7d session on purpose — it spans logins. */
25+const LAST_ORG_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
26+
27+/** Browser-side write. Slugs are `[a-z0-9-]` by grammar, so no encoding. */
28+export const writeLastOrgCookie = (slug: string): void => {
29+ document.cookie = `${LAST_ORG_COOKIE}=${slug}; Path=/; Max-Age=${LAST_ORG_MAX_AGE_SECONDS}; SameSite=Lax${
30+ window.location.protocol === "https:" ? "; Secure" : ""
31+ }`;
32+};
apps/cloud/src/auth/org-api-key-auth.node.test.ts+159−0
@@ -0,0 +1,159 @@
1+import { describe, expect, it } from "@effect/vitest";
2+import { Effect, Layer } from "effect";
3+
4+import { ApiKeyService } from "./api-keys";
5+import { UserStoreService } from "./context";
6+import { WorkOSClient, type WorkOSClientService } from "./workos";
7+import { isPlatformAuth, resolveApiKeyPrincipal, resolveBearerAuth } from "./workos-auth-provider";
8+
9+// Groundwork for the PRIVILEGED, org-level API key: it resolves to the platform
10+// view (tenant-wide, read-only, NO acting member) rather than to a member
11+// `Principal`. The product surfaces must reject it outright rather than
12+// inventing a subject for it — that is the property these tests pin.
13+
14+const createdAt = new Date("2026-01-01T00:00:00.000Z");
15+
16+const stubApiKeys = Layer.succeed(ApiKeyService)({
17+ validate: (value: string) => {
18+ if (value === "valid_org_key") {
19+ return Effect.succeed({
20+ scope: "org" as const,
21+ accountId: null,
22+ organizationId: "org_123",
23+ keyId: "api_key_org",
24+ });
25+ }
26+ if (value === "valid_user_key") {
27+ return Effect.succeed({
28+ scope: "user" as const,
29+ accountId: "user_123",
30+ organizationId: "org_123",
31+ keyId: "api_key_123",
32+ });
33+ }
34+ return Effect.succeed(null);
35+ },
36+ listUserKeys: () => Effect.succeed([]),
37+ createUserKey: () => Effect.die("org api key test does not create API keys"),
38+ revokeUserKey: () => Effect.void,
39+ listOrgKeys: () => Effect.die("auth resolution test does not list org API keys"),
40+ createOrgKey: () => Effect.die("auth resolution test does not create org API keys"),
41+ revokeOrgKey: () => Effect.die("auth resolution test does not revoke org API keys"),
42+});
43+
44+const stubWorkOS = Layer.succeed(
45+ WorkOSClient,
46+ new Proxy({} as WorkOSClientService, {
47+ get: (_target, prop) => {
48+ if (prop === "listUserMemberships") {
49+ return (userId: string) =>
50+ Effect.succeed({
51+ data:
52+ userId === "user_123"
53+ ? [{ userId, organizationId: "org_123", status: "active" }]
54+ : [],
55+ });
56+ }
57+ // An org key must NOT trigger a membership check — there is no user to
58+ // check. Any such call dies here, which is the assertion.
59+ return () => Effect.die(`unexpected WorkOSClient.${String(prop)} call`);
60+ },
61+ }),
62+);
63+
64+const stubUsers = Layer.succeed(UserStoreService)({
65+ use: (_op, fn) =>
66+ Effect.promise(() =>
67+ fn({
68+ ensureAccount: async (id: string) => ({ id, createdAt }),
69+ getAccount: async (id: string) => ({ id, createdAt }),
70+ upsertOrganization: async (org: { id: string; name: string }) => ({
71+ ...org,
72+ slug: `org-slug-${org.id}`,
73+ createdAt,
74+ }),
75+ getOrganization: async (id: string) => ({
76+ id,
77+ name: `Org ${id}`,
78+ slug: `org-slug-${id}`,
79+ createdAt,
80+ }),
81+ getOrganizationBySlug: async (slug: string) => ({
82+ id: "org_by_slug",
83+ name: `Org ${slug}`,
84+ slug,
85+ createdAt,
86+ }),
87+ deleteOrganizationCascade: async () => {},
88+ }),
89+ ),
90+});
91+
92+const layers = Layer.mergeAll(stubApiKeys, stubWorkOS, stubUsers);
93+
94+const bearer = (token: string) =>
95+ new Request("https://executor.test/api/tools", {
96+ headers: { authorization: `Bearer ${token}` },
97+ });
98+
99+describe("org-level API keys", () => {
100+ it.effect("resolve to the platform view with no acting member", () =>
101+ Effect.gen(function* () {
102+ const auth = yield* resolveBearerAuth(bearer("valid_org_key")).pipe(Effect.provide(layers));
103+
104+ expect(isPlatformAuth(auth)).toBe(true);
105+ expect(auth).toEqual({
106+ kind: "platform",
107+ organizationId: "org_123",
108+ organizationName: "Org org_123",
109+ organizationSlug: "org-slug-org_123",
110+ keyId: "api_key_org",
111+ });
112+ // No `accountId` anywhere in the shape: nothing downstream can bind a
113+ // subject from it by accident.
114+ expect(auth).not.toHaveProperty("accountId");
115+ }),
116+ );
117+
118+ it.effect("user keys still resolve to a bound member principal", () =>
119+ Effect.gen(function* () {
120+ const auth = yield* resolveBearerAuth(bearer("valid_user_key")).pipe(Effect.provide(layers));
121+
122+ expect(isPlatformAuth(auth)).toBe(false);
123+ expect(auth).toMatchObject({ accountId: "user_123", organizationId: "org_123" });
124+ }),
125+ );
126+
127+ it.effect("resolve on the product surface as a platform principal, never a subject", () =>
128+ Effect.gen(function* () {
129+ // THE security property of the api-key half, updated for platform reads:
130+ // a product request carrying an org key resolves to the NEUTRAL platform
131+ // shape — which the shared middleware routes to the subject-less,
132+ // GET-only platform executor — and must never carry an accountId a
133+ // handler could bind as an acting member.
134+ const principal = yield* resolveApiKeyPrincipal(bearer("valid_org_key")).pipe(
135+ Effect.provide(layers),
136+ );
137+
138+ expect(principal).toEqual({
139+ kind: "platform",
140+ organizationId: "org_123",
141+ organizationName: "Org org_123",
142+ organizationSlug: "org-slug-org_123",
143+ keyId: "api_key_org",
144+ });
145+ expect(principal).not.toHaveProperty("accountId");
146+ }),
147+ );
148+
149+ it.effect("do not trigger a user membership check", () =>
150+ Effect.gen(function* () {
151+ // `authorizeOrganization` checks a USER's live membership; there is no
152+ // user here. The WorkOS stub dies on any call other than the user path,
153+ // so a clean resolution proves the org branch never took it.
154+ const auth = yield* resolveBearerAuth(bearer("valid_org_key")).pipe(Effect.provide(layers));
155+
156+ expect(isPlatformAuth(auth)).toBe(true);
157+ }),
158+ );
159+});
apps/cloud/src/auth/org-api-key-mint.node.test.ts+316−0
@@ -0,0 +1,316 @@
1+import { describe, expect, it } from "@effect/vitest";
2+import { Effect, Layer } from "effect";
3+
4+import { ApiKeyService, OrgApiKeyNotFound } from "./api-keys";
5+import { ApiKeyManagementError } from "./errors";
6+import { WorkOSClient, type WorkOSClientService } from "./workos";
7+
8+// ---------------------------------------------------------------------------
9+// Minting, listing, and revoking ORGANIZATION-owned api keys — the privileged
10+// credential that resolves to the read-only platform view (`/admin/*`).
11+//
12+// The decode boundary is what these pin. WorkOS returns an org-owned key with
13+// the ORG in `owner.id` and no member at all, which is a different shape from
14+// the user-owned keys the console's personal-key CRUD handles. The workspace
15+// api key is also workspace-WIDE, so a response naming a different org than the
16+// one asked for must not be surfaced as that org's key.
17+//
18+// NOTE (emulator gap): the WorkOS emulator's `serializeApiKey` hardcodes
19+// `owner.type: "user"` and exposes no org-key mint route, so this path cannot
20+// be exercised over the wire in e2e today. These stubs encode the REAL WorkOS
21+// API shape (`organizations.createOrganizationApiKey` /
22+// `listOrganizationApiKeys`, per the installed SDK's types).
23+// ---------------------------------------------------------------------------
24+
25+const ORG = "org_123";
26+
27+const orgKeyResponse = (organizationId: string, id: string, name: string) => ({
28+ object: "api_key",
29+ id,
30+ owner: { type: "organization", id: organizationId },
31+ name,
32+ obfuscatedValue: "sk_org…a1b2",
33+ lastUsedAt: null,
34+ permissions: [],
35+ createdAt: "2026-01-01T00:00:00.000Z",
36+ updatedAt: "2026-01-01T00:00:00.000Z",
37+});
38+
39+const stubWorkOS = (overrides: Record<string, unknown> = {}) =>
40+ Layer.succeed(
41+ WorkOSClient,
42+ new Proxy({} as WorkOSClientService, {
43+ get: (_target, prop) => {
44+ const override = overrides[String(prop)];
45+ if (override) return override;
46+ return () => Effect.die(`unexpected WorkOSClient.${String(prop)} call`);
47+ },
48+ }),
49+ );
50+
51+const serviceWith = (overrides: Record<string, unknown>) =>
52+ ApiKeyService.asEffect().pipe(
53+ Effect.provide(ApiKeyService.WorkOS.pipe(Layer.provide(stubWorkOS(overrides)))),
54+ );
55+
56+describe("organization API keys", () => {
57+ it.effect("mints an org-owned key and returns the one-time secret", () =>
58+ Effect.gen(function* () {
59+ const apiKeys = yield* serviceWith({
60+ createOrgApiKey: (params: { organizationId: string; name: string }) =>
61+ Effect.succeed({
62+ ...orgKeyResponse(params.organizationId, "key_org_1", params.name),
63+ value: "sk_org_plaintext",
64+ }),
65+ });
66+
67+ const created = yield* apiKeys.createOrgKey({ organizationId: ORG, name: "acme-backend" });
68+
69+ expect(created.id).toBe("key_org_1");
70+ expect(created.name).toBe("acme-backend");
71+ expect(created.value, "create returns the one-time plaintext secret").toBe(
72+ "sk_org_plaintext",
73+ );
74+ expect(created.obfuscatedValue, "the display value is masked, not the secret").not.toBe(
75+ created.value,
76+ );
77+ }),
78+ );
79+
80+ it.effect("lists the organization's own keys, masked", () =>
81+ Effect.gen(function* () {
82+ const apiKeys = yield* serviceWith({
83+ listOrgApiKeys: (organizationId: string) =>
84+ Effect.succeed({
85+ object: "list",
86+ data: [
87+ orgKeyResponse(organizationId, "key_org_1", "acme-backend"),
88+ orgKeyResponse(organizationId, "key_org_2", "ci"),
89+ ],
90+ listMetadata: { before: null, after: null },
91+ }),
92+ });
93+
94+ const keys = yield* apiKeys.listOrgKeys({ organizationId: ORG });
95+
96+ expect(keys.map((key) => key.name)).toEqual(["acme-backend", "ci"]);
97+ expect(JSON.stringify(keys), "the list never carries a plaintext secret").not.toContain(
98+ "sk_org_plaintext",
99+ );
100+ }),
101+ );
102+
103+ it.effect("drops a key owned by a DIFFERENT organization", () =>
104+ Effect.gen(function* () {
105+ // The WorkOS workspace api key is workspace-wide, so a response naming
106+ // another org must never be surfaced as this org's key.
107+ const apiKeys = yield* serviceWith({
108+ listOrgApiKeys: () =>
109+ Effect.succeed({
110+ object: "list",
111+ data: [
112+ orgKeyResponse(ORG, "key_mine", "mine"),
113+ orgKeyResponse("org_other", "key_theirs", "theirs"),
114+ ],
115+ listMetadata: { before: null, after: null },
116+ }),
117+ });
118+
119+ const keys = yield* apiKeys.listOrgKeys({ organizationId: ORG });
120+
121+ expect(
122+ keys.map((key) => key.id),
123+ "only this org's key survives",
124+ ).toEqual(["key_mine"]);
125+ }),
126+ );
127+
128+ it.effect("refuses a create response that names another organization", () =>
129+ Effect.gen(function* () {
130+ const apiKeys = yield* serviceWith({
131+ createOrgApiKey: () =>
132+ Effect.succeed({
133+ ...orgKeyResponse("org_other", "key_theirs", "theirs"),
134+ value: "sk_org_plaintext",
135+ }),
136+ });
137+
138+ const error = yield* Effect.flip(
139+ apiKeys.createOrgKey({ organizationId: ORG, name: "acme-backend" }),
140+ );
141+
142+ expect(error).toBeInstanceOf(ApiKeyManagementError);
143+ }),
144+ );
145+
146+ it.effect("ignores a user-owned key in an org-key listing", () =>
147+ Effect.gen(function* () {
148+ // The org and user shapes are decoded separately on purpose; a user key
149+ // appearing here would mean the caller could mistake a member's personal
150+ // credential for a workspace one.
151+ const apiKeys = yield* serviceWith({
152+ listOrgApiKeys: () =>
153+ Effect.succeed({
154+ object: "list",
155+ data: [
156+ orgKeyResponse(ORG, "key_mine", "mine"),
157+ {
158+ object: "api_key",
159+ id: "key_user",
160+ owner: { type: "user", id: "user_1", organization_id: ORG },
161+ name: "personal",
162+ obfuscatedValue: "sk_user…c3d4",
163+ lastUsedAt: null,
164+ createdAt: "2026-01-01T00:00:00.000Z",
165+ updatedAt: "2026-01-01T00:00:00.000Z",
166+ },
167+ ],
168+ listMetadata: { before: null, after: null },
169+ }),
170+ });
171+
172+ const keys = yield* apiKeys.listOrgKeys({ organizationId: ORG });
173+
174+ expect(
175+ keys.map((key) => key.id),
176+ "a user-owned key is not an org key",
177+ ).toEqual(["key_mine"]);
178+ }),
179+ );
180+});
181+
182+// ---------------------------------------------------------------------------
183+// Revoking an org key. The mint is only half a credential lifecycle: an org key
184+// reads every user and connection in the tenant, so it has to be destroyable.
185+//
186+// Revoke cannot go through `revokeUserKey`/`revokeApiKey` — that path resolves
187+// ownership against the CALLER's personal keys, so an org key id is never in
188+// its owned set. It also cannot just call `deleteApiKey` directly: that route is
189+// scope-blind and would happily destroy a member's personal key, or a key
190+// belonging to a sibling org in the same WorkOS workspace. So ownership is
191+// resolved against the ORG's decoded listing first, and that filter IS the
192+// authorization. These pin that filter.
193+// ---------------------------------------------------------------------------
194+
195+describe("revoking an organization API key", () => {
196+ const listing = (...keys: ReadonlyArray<unknown>) => ({
197+ object: "list",
198+ data: keys,
199+ listMetadata: { before: null, after: null },
200+ });
201+
202+ it.effect("revokes a key the organization owns", () =>
203+ Effect.gen(function* () {
204+ const deleted: string[] = [];
205+ const apiKeys = yield* serviceWith({
206+ listOrgApiKeys: (organizationId: string) =>
207+ Effect.succeed(listing(orgKeyResponse(organizationId, "key_org_1", "acme-backend"))),
208+ deleteApiKey: (id: string) =>
209+ Effect.sync(() => {
210+ deleted.push(id);
211+ }),
212+ });
213+
214+ yield* apiKeys.revokeOrgKey({ organizationId: ORG, keyId: "key_org_1" });
215+
216+ expect(deleted, "the delete reaches WorkOS exactly once, for that key").toEqual([
217+ "key_org_1",
218+ ]);
219+ }),
220+ );
221+
222+ it.effect("refuses a USER-owned key id, without deleting anything", () =>
223+ Effect.gen(function* () {
224+ // The regression this guards: a member's personal credential must not be
225+ // destroyable through the org route just because an admin can call it.
226+ const deleted: string[] = [];
227+ const apiKeys = yield* serviceWith({
228+ listOrgApiKeys: () =>
229+ Effect.succeed(
230+ listing(orgKeyResponse(ORG, "key_org_1", "acme-backend"), {
231+ object: "api_key",
232+ id: "key_user",
233+ owner: { type: "user", id: "user_1", organization_id: ORG },
234+ name: "personal",
235+ obfuscatedValue: "sk_user…c3d4",
236+ lastUsedAt: null,
237+ createdAt: "2026-01-01T00:00:00.000Z",
238+ updatedAt: "2026-01-01T00:00:00.000Z",
239+ }),
240+ ),
241+ deleteApiKey: (id: string) =>
242+ Effect.sync(() => {
243+ deleted.push(id);
244+ }),
245+ });
246+
247+ const error = yield* Effect.flip(
248+ apiKeys.revokeOrgKey({ organizationId: ORG, keyId: "key_user" }),
249+ );
250+
251+ expect(error).toBeInstanceOf(OrgApiKeyNotFound);
252+ expect(deleted, "no delete is issued for a key the org does not own").toEqual([]);
253+ }),
254+ );
255+
256+ it.effect("refuses a key belonging to ANOTHER organization", () =>
257+ Effect.gen(function* () {
258+ // The WorkOS workspace key is workspace-wide, so an admin of one org must
259+ // not be able to revoke a sibling org's credential by id.
260+ const deleted: string[] = [];
261+ const apiKeys = yield* serviceWith({
262+ listOrgApiKeys: () =>
263+ Effect.succeed(
264+ listing(
265+ orgKeyResponse(ORG, "key_mine", "mine"),
266+ orgKeyResponse("org_other", "key_theirs", "theirs"),
267+ ),
268+ ),
269+ deleteApiKey: (id: string) =>
270+ Effect.sync(() => {
271+ deleted.push(id);
272+ }),
273+ });
274+
275+ const error = yield* Effect.flip(
276+ apiKeys.revokeOrgKey({ organizationId: ORG, keyId: "key_theirs" }),
277+ );
278+
279+ expect(error).toBeInstanceOf(OrgApiKeyNotFound);
280+ expect(deleted).toEqual([]);
281+ }),
282+ );
283+
284+ it.effect("refuses an unknown key id", () =>
285+ Effect.gen(function* () {
286+ const apiKeys = yield* serviceWith({
287+ listOrgApiKeys: () => Effect.succeed(listing()),
288+ deleteApiKey: () => Effect.die("nothing should be deleted"),
289+ });
290+
291+ const error = yield* Effect.flip(
292+ apiKeys.revokeOrgKey({ organizationId: ORG, keyId: "key_nope" }),
293+ );
294+
295+ expect(error).toBeInstanceOf(OrgApiKeyNotFound);
296+ }),
297+ );
298+
299+ it.effect("surfaces an upstream delete failure as a management error", () =>
300+ Effect.gen(function* () {
301+ // A failed delete must not read as a successful revoke — the console
302+ // would tell an admin a live credential is gone.
303+ const apiKeys = yield* serviceWith({
304+ listOrgApiKeys: (organizationId: string) =>
305+ Effect.succeed(listing(orgKeyResponse(organizationId, "key_org_1", "acme-backend"))),
306+ deleteApiKey: () => Effect.fail("workos_unavailable"),
307+ });
308+
309+ const error = yield* Effect.flip(
310+ apiKeys.revokeOrgKey({ organizationId: ORG, keyId: "key_org_1" }),
311+ );
312+
313+ expect(error).toBeInstanceOf(ApiKeyManagementError);
314+ }),
315+ );
316+});
apps/cloud/src/auth/org-selector-auth.node.test.ts+17−7
@@ -7,9 +7,11 @@ import { resolveSessionPrincipal } from "./workos-auth-provider"; · 6 unmodified lines
77 import { WorkOSClient, type WorkOSClientService } from "./workos";
88
99 // The org a console request resolves to is the URL's org (sent in the
10−// `x-executor-organization` selector header), not the session's stored org —
11−// with the session org as the fallback for non-console callers, and live
12−// membership re-checked either way. This is what makes two browser tabs on
10+// `x-executor-organization` selector header) — NEVER the session's stored org.
11+// The sealed cookie's org is a browser-global pinned to whichever org WorkOS
12+// last touched, so a fallback to it silently scopes a multi-org user's request
13+// to the wrong org; a header-less request fails closed instead. Live
14+// membership is re-checked either way. This is what makes two browser tabs on
1315 // different orgs independent.
1416
1517 const createdAt = new Date("2026-01-01T00:00:00.000Z");
@@ -27,6 +29,9 @@ const stubApiKeys = Layer.succeed(ApiKeyService)({ · 11 unmodified lines
2729 listUserKeys: () => Effect.succeed([]),
2830 createUserKey: () => Effect.die("not used"),
2931 revokeUserKey: () => Effect.void,
32+ listOrgKeys: () => Effect.die("auth resolution test does not list org API keys"),
33+ createOrgKey: () => Effect.die("auth resolution test does not create org API keys"),
34+ revokeOrgKey: () => Effect.die("auth resolution test does not revoke org API keys"),
3035 });
3136
3237 const stubWorkOS = Layer.succeed(
@@ -59,7 +64,7 @@ const stubWorkOS = Layer.succeed( · 26 unmodified lines
5964 );
6065
6166 const stubUsers = Layer.succeed(UserStoreService)({
62− use: (fn) =>
67+ use: (_op, fn) =>
6368 Effect.promise(() =>
6469 fn({
6570 ensureAccount: async (id: string) => ({ id, createdAt }),
@@ -95,10 +100,15 @@ const run = (headers: Record<string, string>) => · 29 unmodified lines
95100 );
96101
97102 describe("resolveSessionPrincipal · URL org selector", () => {
98− it.effect("falls back to the session org when no selector header is sent", () =>
103+ it.effect("fails closed when no selector header is sent", () =>
99104 Effect.gen(function* () {
100− const principal = yield* run({ cookie: "wos-session=x" });
101− expect(principal.organizationId, "scopes to the session org").toBe(SESSION_ORG);
105+ // No fallback to the session org: the cookie's org is browser-global
106+ // and can name a different org than the tab's URL for a multi-org user.
107+ const error = yield* Effect.flip(run({ cookie: "wos-session=x" }));
108+ expect(error, "rejects instead of scoping to the session org").toMatchObject({
109+ _tag: "NoOrganization",
110+ code: "no_organization",
111+ });
102112 }),
103113 );
104114
apps/cloud/src/auth/organization.ts+5−3
@@ -38,12 +38,14 @@ import { WorkOSClient } from "./workos"; · 37 unmodified lines
3838 export const resolveOrganization = (organizationId: string) =>
3939 Effect.gen(function* () {
4040 const users = yield* UserStoreService;
41− const existing = yield* users.use((s) => s.getOrganization(organizationId));
41+ const existing = yield* users.use("getOrganization", (s) => s.getOrganization(organizationId));
4242 if (existing) return existing;
4343
4444 const workos = yield* WorkOSClient;
4545 const fresh = yield* workos.getOrganization(organizationId);
46− return yield* users.use((s) => s.upsertOrganization({ id: fresh.id, name: fresh.name }));
46+ return yield* users.use("upsertOrganization", (s) =>
47+ s.upsertOrganization({ id: fresh.id, name: fresh.name }),
48+ );
4749 });
4850
4951 // ---------------------------------------------------------------------------
@@ -125,7 +127,7 @@ export const authorizeOrganizationSelector = (userId: string, selector: string) · 75 unmodified lines
125127 return yield* authorizeOrganization(userId, selector);
126128 }
127129 const users = yield* UserStoreService;
128− const org = yield* users.use((s) => s.getOrganizationBySlug(selector));
130+ const org = yield* users.use("getOrganizationBySlug", (s) => s.getOrganizationBySlug(selector));
129131 if (!org) return null;
130132 return yield* authorizeOrganization(userId, org.id);
131133 });
apps/cloud/src/auth/user-store-error.node.test.ts+128−0
@@ -0,0 +1,128 @@
1+// `UserStoreError` is the public failure of every cloud user-store call, and it
2+// used to carry NOTHING: no operation, no reason, no message. Sentry grouped
3+// every store failure — a connect timeout, a missing table, a constraint
4+// violation — into one titleless issue, and the only cause detail (the
5+// pretty-printed Effect cause stuffed into a Sentry `extra`) is scrubbed
6+// server-side, so the issue was undiagnosable from Sentry alone.
7+//
8+// This pins the two safe classification fields it must carry instead:
9+// `operation` (already in hand at the call site) and `reason` (classified from
10+// the driver cause the way `statusFromWorkOSCause` classifies WorkOS causes).
11+import { createServer, type Server, type Socket } from "node:net";
12+
13+import { describe, expect, it } from "@effect/vitest";
14+import { Effect, Layer, Result } from "effect";
15+import postgres from "postgres";
16+
17+import { UserStoreService } from "./context";
18+import { ServiceAdapterError, userStoreReasonFromCause, type UserStoreError } from "./errors";
19+import { DbService } from "../db/db";
20+
21+// A socket that completes the TCP handshake and then says nothing — exactly
22+// what a wedged Hyperdrive/Postgres endpoint looks like to postgres.js, and the
23+// only way to obtain the driver's REAL connect-timeout error object rather than
24+// a hand-written imitation of it.
25+const blackHolePort = async (): Promise<{
26+ readonly port: number;
27+ readonly close: () => Promise<void>;
28+}> => {
29+ const sockets = new Set<Socket>();
30+ const server: Server = createServer((socket) => {
31+ sockets.add(socket);
32+ socket.on("close", () => sockets.delete(socket));
33+ });
34+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
35+ const address = server.address();
36+ // oxlint-disable-next-line executor/no-error-constructor, executor/no-try-catch-or-throw -- boundary: the fixture cannot run without a bound port
37+ if (address === null || typeof address === "string") throw new Error("no port");
38+ return {
39+ port: address.port,
40+ close: () =>
41+ new Promise<void>((resolve) => {
42+ // The timed-out client leaves its half-open socket attached; without
43+ // dropping it first, `close` waits for a peer that will never speak.
44+ for (const socket of sockets) socket.destroy();
45+ server.close(() => resolve());
46+ }),
47+ };
48+};
49+
50+const realConnectTimeoutError = async (): Promise<unknown> => {
51+ const hole = await blackHolePort();
52+ const sql = postgres(`postgresql://postgres:postgres@127.0.0.1:${hole.port}/postgres`, {
53+ max: 1,
54+ connect_timeout: 1,
55+ fetch_types: false,
56+ onnotice: () => undefined,
57+ });
58+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: capturing the driver's own thrown error IS the fixture
59+ try {
60+ await sql`select 1`;
61+ // oxlint-disable-next-line executor/no-error-constructor, executor/no-try-catch-or-throw -- boundary: the fixture is unusable if the socket answered
62+ throw new Error("expected the connect to time out");
63+ } catch (error) {
64+ return error;
65+ } finally {
66+ // oxlint-disable-next-line executor/no-promise-catch -- boundary: best-effort teardown of a connection that never opened
67+ await sql.end({ timeout: 0 }).catch(() => undefined);
68+ await hole.close();
69+ }
70+};
71+
72+// Drizzle re-throws driver failures wrapped in its own error with the failing
73+// SQL in the message and the driver error in `.cause` — the shape production
74+// actually reports (`Failed query: select … -> write CONNECT_TIMEOUT …`).
75+const wrappedLikeDrizzle = (cause: unknown): Error =>
76+ // oxlint-disable-next-line executor/no-error-constructor -- boundary: reproducing the driver wrapper shape the store really fails with
77+ Object.assign(new Error('Failed query: select "id" from "organizations" where "id" = $1'), {
78+ cause,
79+ });
80+
81+const stubDb = Layer.succeed(DbService)({ db: {} as never });
82+
83+const failingStoreCall = (
84+ operation: string,
85+ failure: unknown,
86+): Effect.Effect<Result.Result<never, UserStoreError>> =>
87+ Effect.gen(function* () {
88+ const users = yield* UserStoreService;
89+ // oxlint-disable-next-line executor/no-promise-reject -- boundary: the store adapter lifts a REJECTING promise; rejecting is the fixture
90+ return yield* users.use(operation, () => Promise.reject(failure));
91+ }).pipe(
92+ Effect.provide(UserStoreService.Live.pipe(Layer.provide(stubDb))),
93+ Effect.result,
94+ ) as Effect.Effect<Result.Result<never, UserStoreError>>;
95+
96+describe("UserStoreError classification", () => {
97+ it("classifies the driver cause chain", async () => {
98+ const driverError = await realConnectTimeoutError();
99+
100+ expect(userStoreReasonFromCause(wrappedLikeDrizzle(driverError))).toBe("connect_timeout");
101+ expect(
102+ userStoreReasonFromCause(new ServiceAdapterError({ cause: wrappedLikeDrizzle(driverError) })),
103+ ).toBe("connect_timeout");
104+ expect(
105+ userStoreReasonFromCause(
106+ // oxlint-disable-next-line executor/no-error-constructor -- boundary: a SQLSTATE failure shape
107+ Object.assign(new Error('relation "organizations" does not exist'), { code: "42P01" }),
108+ ),
109+ ).toBe("query");
110+ // oxlint-disable-next-line executor/no-error-constructor -- boundary: a failure with nothing to classify
111+ expect(userStoreReasonFromCause(new Error("something else"))).toBe("unknown");
112+ }, 20_000);
113+
114+ it("carries operation, reason and a stable message on the public error", async () => {
115+ const driverError = await realConnectTimeoutError();
116+
117+ const result = await Effect.runPromise(
118+ failingStoreCall("getOrganization", wrappedLikeDrizzle(driverError)),
119+ );
120+
121+ expect(Result.isFailure(result)).toBe(true);
122+ if (!Result.isFailure(result)) return;
123+ expect(result.failure.operation).toBe("getOrganization");
124+ expect(result.failure.reason).toBe("connect_timeout");
125+ expect(result.failure.message).toContain("getOrganization");
126+ expect(result.failure.message).toContain("connect_timeout");
127+ }, 20_000);
128+});
apps/cloud/src/auth/workos-auth-provider.ts+132−19
@@ -16,6 +16,7 @@ · 15 unmodified lines
1616 // - invalid api key -> Unauthorized 401 invalid_api_key
1717 // - api-key org not authorized -> NoOrganization 403 no_organization
1818 // - no/invalid session -> NoOrganization 403 no_organization
19+// - session without org header -> NoOrganization 403 no_organization (fail closed)
1920 // - session org not authorized -> NoOrganization 403 no_organization
2021 // - no auth header -> falls through to the sealed-session path
2122 // The org-resolution infra errors (`UserStoreError` / `WorkOSError`) are
@@ -37,7 +38,13 @@ import { · 15 unmodified lines
3738 Unauthorized,
3839 Unavailable,
3940 } from "@executor-js/api/server";
40−import type { FailureRenderingStrategy, IdentityFailure, Principal } from "@executor-js/api/server";
41+import type {
42+ FailureRenderingStrategy,
43+ IdentityFailure,
44+ PlatformPrincipal,
45+ Principal,
46+ ResolvedPrincipal,
47+} from "@executor-js/api/server";
4148
4249 import { ApiKeyService } from "./api-keys";
4350 import { workosApiJwtBearerConfig } from "./api-jwt-bearer";
@@ -46,6 +53,7 @@ import { · 2 unmodified lines
4653 authorizeOrganization,
4754 authorizeOrganizationSelector,
4855 orgSelectorFromRequest,
56+ resolveOrganization,
4957 } from "./organization";
5058 import { UserStoreService } from "./context";
5159 import { sealedSessionDisplayName } from "./middleware";
@@ -86,6 +94,10 @@ const NO_ORGANIZATION_IN_SESSION = { · 34 unmodified lines
8694 code: "no_organization",
8795 message: "No organization in session",
8896 };
97+const NO_ORGANIZATION_SELECTOR = {
98+ code: "no_organization",
99+ message: "No organization selector in request",
100+};
89101 const INVALID_ACCESS_TOKEN = {
90102 code: "invalid_access_token",
91103 message: "Invalid or expired access token",
@@ -98,7 +110,6 @@ const NO_ORGANIZATION_IN_ACCESS_TOKEN = { · 6 unmodified lines
98110 code: "no_organization",
99111 message: "No organization in access token",
100112 };
101−
102113 // A bearer value with three dot-separated segments is a JWT (a WorkOS access
103114 // token from the CLI device-login); anything else is treated as an API key.
104115 // Same discriminator the MCP plane uses (`mcp/auth.ts`).
@@ -134,6 +145,7 @@ const resolveJwtPrincipal = (token: string, jwt: JwtBearerConfig) => · 29 unmodified lines
134145 if (!org) return yield* new NoOrganization(NO_ORGANIZATION_IN_ACCESS_TOKEN);
135146
136147 return {
148+ kind: "member",
137149 accountId: verified.accountId,
138150 organizationId: org.id,
139151 organizationName: org.name,
@@ -146,13 +158,49 @@ const resolveJwtPrincipal = (token: string, jwt: JwtBearerConfig) => · 6 unmodified lines
146158 });
147159
148160 /**
149− * Resolve a `Bearer` credential into a `Principal`: a WorkOS access-token JWT
150− * (when `jwt` config is supplied and the value looks like a JWT) or a WorkOS
151− * API key. Returns `null` when there is no `Authorization` header, so the
152− * caller falls through to the sealed-session path. (Kept the historical name,
153− * the re-export and resolver tests reference it.)
161+ * What an ORG-level API key resolves to: the platform view. Deliberately NOT a
162+ * `Principal` — a `Principal` names an acting member (`accountId: string`), and
163+ * an org key has none. Keeping it a separate shape means no product code path
164+ * can accidentally treat it as a member, and the executor built from it binds
165+ * `subject: null` + the read-only tenant reach rather than inventing a subject.
166+ *
167+ * The `/admin/*` mount turns this into an executor with `{ tenant:
168+ * organizationId, subject: undefined, platformView: true }`.
169+ */
170+export interface PlatformAuth {
171+ readonly kind: "platform";
172+ readonly organizationId: string;
173+ readonly organizationName: string;
174+ readonly organizationSlug?: string;
175+ readonly keyId: string;
176+}
177+
178+/** A resolved bearer credential: an acting member, or the org-level platform
179+ * view. `null` means no `Authorization` header at all. */
180+export type BearerAuth = Principal | PlatformAuth | null;
181+
182+export const isPlatformAuth = (value: BearerAuth): value is PlatformAuth =>
183+ value !== null && "kind" in value && value.kind === "platform";
184+
185+/**
186+ * Resolve a `Bearer` credential into either a member `Principal` or the
187+ * org-level {@link PlatformAuth}. Returns `null` when there is no
188+ * `Authorization` header, so the caller falls through to the sealed-session
189+ * path.
190+ *
191+ * The org branch does NOT call `authorizeOrganization`: that checks a USER's
192+ * live membership, and there is no user here. The key itself is the authority —
193+ * WorkOS validated it and reported which org owns it — so the org row is merely
194+ * resolved (mirrored on first read) for its name and slug.
154195 */
155−export const resolveApiKeyPrincipal = (request: Request, jwt: JwtBearerConfig | null = null) =>
196+export const resolveBearerAuth = (
197+ request: Request,
198+ jwt: JwtBearerConfig | null = null,
199+): Effect.Effect<
200+ BearerAuth,
201+ Unauthorized | NoOrganization | Unavailable | UserStoreError | WorkOSError,
202+ WorkOSClient | ApiKeyService | UserStoreService
203+> =>
156204 Effect.gen(function* () {
157205 const authHeader = request.headers.get("authorization");
158206 if (!authHeader) return null;
@@ -167,7 +215,7 @@ export const resolveApiKeyPrincipal = (request: Request, jwt: JwtBearerConfig | · 8 unmodified lines
167215 if (jwt && looksLikeJwt(value)) return yield* resolveJwtPrincipal(value, jwt);
168216
169217 const apiKeys = yield* ApiKeyService;
170− const principal = yield* apiKeys
218+ const owner = yield* apiKeys
171219 .validate(value)
172220 .pipe(
173221 Effect.catchTag("ApiKeyValidationError", () =>
@@ -175,13 +223,29 @@ export const resolveApiKeyPrincipal = (request: Request, jwt: JwtBearerConfig | · 1 unmodified line
175223 ),
176224 );
177225
178− if (!principal) return yield* new Unauthorized(INVALID_API_KEY);
226+ if (!owner) return yield* new Unauthorized(INVALID_API_KEY);
179227
180− const org = yield* authorizeOrganization(principal.accountId, principal.organizationId);
228+ if (owner.scope === "org") {
229+ const org = yield* resolveOrganization(owner.organizationId);
230+ return {
231+ kind: "platform",
232+ organizationId: org.id,
233+ organizationName: org.name,
234+ ...(org.slug === undefined || org.slug === null ? {} : { organizationSlug: org.slug }),
235+ keyId: owner.keyId,
236+ } satisfies PlatformAuth;
237+ }
238+
239+ // A `"user"` key always carries an accountId (see `ownerFromApiKey`); the
240+ // guard keeps the narrowing honest rather than asserting.
241+ if (owner.accountId == null) return yield* new Unauthorized(INVALID_API_KEY);
242+
243+ const org = yield* authorizeOrganization(owner.accountId, owner.organizationId);
181244 if (!org) return yield* new NoOrganization(NO_ORGANIZATION_IN_API_KEY);
182245
183246 return {
184− accountId: principal.accountId,
247+ kind: "member",
248+ accountId: owner.accountId,
185249 organizationId: org.id,
186250 organizationName: org.name,
187251 organizationSlug: org.slug,
@@ -192,6 +256,40 @@ export const resolveApiKeyPrincipal = (request: Request, jwt: JwtBearerConfig | · 4 unmodified lines
192256 } satisfies Principal;
193257 });
194258
259+/**
260+ * The PRODUCT-plane bearer resolver. An org-level key resolves to the neutral
261+ * seam's {@link PlatformPrincipal} — NOT a member `Principal` — and the shared
262+ * middleware routes it to the subject-less, read-only platform executor
263+ * (refusing non-GET up front). Previously the product plane rejected org keys
264+ * outright; serving tenant-level READS to them is deliberate: the catalog,
265+ * tools, policies, and org-owned connection listings are tenant-shared answers
266+ * a machine credential can honestly receive, while everything subject-bound
267+ * stays structurally out of reach (a platform executor binds no subject, so no
268+ * member's personal rows resolve). (Kept the historical name; the re-export and
269+ * resolver tests reference it.)
270+ */
271+export const resolveApiKeyPrincipal = (
272+ request: Request,
273+ jwt: JwtBearerConfig | null = null,
274+): Effect.Effect<
275+ ResolvedPrincipal | null,
276+ Unauthorized | NoOrganization | Unavailable | UserStoreError | WorkOSError,
277+ WorkOSClient | ApiKeyService | UserStoreService
278+> =>
279+ Effect.gen(function* () {
280+ const auth = yield* resolveBearerAuth(request, jwt);
281+ if (isPlatformAuth(auth)) {
282+ return {
283+ kind: "platform",
284+ organizationId: auth.organizationId,
285+ organizationName: auth.organizationName,
286+ ...(auth.organizationSlug === undefined ? {} : { organizationSlug: auth.organizationSlug }),
287+ keyId: auth.keyId,
288+ } satisfies PlatformPrincipal;
289+ }
290+ return auth;
291+ });
292+
195293 export const resolveSessionPrincipal = (request: Request) =>
196294 Effect.gen(function* () {
197295 const workos = yield* WorkOSClient;
@@ -199,17 +297,27 @@ export const resolveSessionPrincipal = (request: Request) => · 1 unmodified line
199297 if (!session) {
200298 return yield* new NoOrganization(NO_ORGANIZATION_IN_SESSION);
201299 }
202− // The console URL's org is the scope authority (sent as a header); the
203− // session's own org is the fallback for non-console callers. Membership is
204− // re-checked live either way — the header is a selector, not a trust
205− // boundary (see organization.ts).
206− const selector = orgSelectorFromRequest(request) ?? session.organizationId;
300+ // The console URL's org is the scope authority, sent as a header on every
301+ // request. FAIL CLOSED when it's missing: the sealed cookie's org is
302+ // browser-global and pinned to whichever org WorkOS last touched, so
303+ // falling back to it silently serves ANOTHER org's data to a multi-org
304+ // user (the wrong-tenant connection-list bug, 2026-07). A header-less
305+ // session call gets a clear 403 instead. Membership is re-checked live —
306+ // the header is a selector, not a trust boundary (see organization.ts).
307+ // A bare-URL first paint (no org in the path yet) may 403 here; that's
308+ // the safe outcome — OrgSlugGate immediately canonicalizes the URL onto
309+ // an org slug, the org-keyed atom registry remounts, and everything
310+ // refetches with the header. `/account/me` — which deliberately DOES fall
311+ // back to the session org to pick that landing org — lives on the account
312+ // plane, not here.
313+ const selector = orgSelectorFromRequest(request);
207314 if (!selector) {
208− return yield* new NoOrganization(NO_ORGANIZATION_IN_SESSION);
315+ return yield* new NoOrganization(NO_ORGANIZATION_SELECTOR);
209316 }
210317 const org = yield* authorizeOrganizationSelector(session.userId, selector);
211318 if (!org) return yield* new NoOrganization(NO_ORGANIZATION_IN_SESSION);
212319 return {
320+ kind: "member",
213321 accountId: session.userId,
214322 organizationId: org.id,
215323 organizationName: org.name,
@@ -236,7 +344,7 @@ export const resolveProtectedPrincipal = ( · 20 unmodified lines
236344 request: Request,
237345 jwt: JwtBearerConfig | null = null,
238346 ): Effect.Effect<
239− Principal,
347+ ResolvedPrincipal,
240348 Unauthorized | NoOrganization | Unavailable | UserStoreError | WorkOSError,
241349 WorkOSClient | ApiKeyService | UserStoreService
242350 > =>
@@ -317,6 +425,11 @@ export const cloudIdentityFailureStrategy: FailureRenderingStrategy<IdentityFail · 74 unmodified lines
317425 "service_unavailable",
318426 "Service temporarily unavailable",
319427 ),
428+ ReadOnlyCredential: renderIdentityFailure(
429+ 403,
430+ "read_only_credential",
431+ "Organization API keys are read-only",
432+ ),
320433 }),
321434 ),
322435 };
apps/cloud/src/auth/workos.node.test.ts+282−1
@@ -1,6 +1,287 @@
1+import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
2+import type { AddressInfo } from "node:net";
3+
14 import { describe, expect, it } from "@effect/vitest";
5+import { env } from "cloudflare:workers";
6+import { Effect, Schema } from "effect";
7+import { defaults as ironDefaults, seal as sealIron } from "iron-webcrypto";
8+import {
9+ SignJWT,
10+ exportJWK,
11+ generateKeyPair,
12+ type JSONWebKeySet,
13+ type JWK,
14+ type KeyLike,
15+} from "jose";
16+
17+import { WorkOSClient, collectRawWorkOSList, collectWorkOSList } from "./workos";
18+
19+const COOKIE_PASSWORD = "test_cookie_password_at_least_32_chars!";
20+const CLIENT_ID = "client_test";
21+const API_KEY = "sk_test";
22+const USER = {
23+ id: "user_test",
24+ email: "test@example.com",
25+ firstName: "Test",
26+ lastName: "User",
27+ profilePictureUrl: "https://example.com/avatar.png",
28+} as const;
29+
30+interface Keypair {
31+ readonly kid: string;
32+ readonly publicJwk: JWK;
33+ readonly privateKey: KeyLike;
34+}
35+
36+interface RecordedRequest {
37+ readonly method: string;
38+ readonly path: string;
39+ readonly body: unknown;
40+}
41+
42+interface WorkOSStub {
43+ readonly baseUrl: string;
44+ readonly requests: () => readonly RecordedRequest[];
45+ readonly setKeys: (keys: ReadonlyArray<JWK>) => void;
46+}
47+
48+const generateKeypair = async (kid: string): Promise<Keypair> => {
49+ const { publicKey, privateKey } = await generateKeyPair("RS256");
50+ const jwk = await exportJWK(publicKey);
51+ return { kid, publicJwk: { ...jwk, kid, alg: "RS256" }, privateKey };
52+};
53+
54+const signAccessToken = (
55+ keypair: Keypair,
56+ claims: {
57+ readonly subject?: string;
58+ readonly organizationId?: string;
59+ readonly sessionId?: string;
60+ readonly expiresIn?: string | number;
61+ } = {},
62+) => {
63+ const jwt = new SignJWT({
64+ org_id: claims.organizationId ?? "org_test",
65+ sid: claims.sessionId ?? "session_test",
66+ })
67+ .setProtectedHeader({ alg: "RS256", kid: keypair.kid })
68+ .setSubject(claims.subject ?? USER.id)
69+ .setIssuedAt();
70+
71+ return (
72+ typeof claims.expiresIn === "number"
73+ ? jwt.setExpirationTime(claims.expiresIn)
74+ : jwt.setExpirationTime(claims.expiresIn ?? "5m")
75+ ).sign(keypair.privateKey);
76+};
77+
78+const sealSession = async (accessToken: string, refreshToken = "refresh_test"): Promise<string> =>
79+ `${await sealIron(
80+ {
81+ accessToken,
82+ refreshToken,
83+ user: USER,
84+ },
85+ { id: "1", secret: COOKIE_PASSWORD },
86+ { ...ironDefaults, ttl: 0, encode: JSON.stringify },
87+ )}~2`;
88+
89+const workosUserResponse = {
90+ object: "user",
91+ id: USER.id,
92+ email: USER.email,
93+ email_verified: true,
94+ profile_picture_url: USER.profilePictureUrl,
95+ first_name: USER.firstName,
96+ last_name: USER.lastName,
97+ last_sign_in_at: null,
98+ locale: null,
99+ created_at: "2026-07-09T00:00:00.000Z",
100+ updated_at: "2026-07-09T00:00:00.000Z",
101+ external_id: null,
102+ metadata: {},
103+} as const;
104+
105+const decodeJsonBody = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
106+
107+const readJsonBody = async (request: IncomingMessage): Promise<unknown> => {
108+ const chunks: Buffer[] = [];
109+ for await (const chunk of request) {
110+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
111+ }
112+ const body = Buffer.concat(chunks).toString("utf8");
113+ return body ? decodeJsonBody(body) : null;
114+};
115+
116+const writeJson = (response: ServerResponse, status: number, body: unknown): void => {
117+ response.writeHead(status, { "content-type": "application/json" });
118+ response.end(JSON.stringify(body));
119+};
120+
121+const withWorkOSStub = async <A>(
122+ keypair: Keypair,
123+ use: (stub: WorkOSStub) => Promise<A>,
124+): Promise<A> => {
125+ let keys: ReadonlyArray<JWK> = [keypair.publicJwk];
126+ const requests: RecordedRequest[] = [];
127+ const server = createServer((request, response) => {
128+ // oxlint-disable-next-line executor/no-promise-catch -- boundary: Node's request callback cannot await, so the fixture converts async failures into a stable HTTP 500
129+ void (async () => {
130+ const path = request.url ?? "/";
131+ const body = request.method === "POST" ? await readJsonBody(request) : null;
132+ requests.push({ method: request.method ?? "GET", path, body });
133+
134+ if (request.method === "GET" && path === `/sso/jwks/${CLIENT_ID}`) {
135+ writeJson(response, 200, { keys: keys.map((key) => ({ ...key })) } satisfies JSONWebKeySet);
136+ return;
137+ }
138+
139+ if (request.method === "POST" && path === "/user_management/authenticate") {
140+ const refreshed = await signAccessToken(keypair, {
141+ sessionId: "session_refreshed",
142+ });
143+ writeJson(response, 200, {
144+ user: workosUserResponse,
145+ organization_id: "org_test",
146+ access_token: refreshed,
147+ refresh_token: "refresh_next",
148+ authentication_method: "Password",
149+ });
150+ return;
151+ }
152+
153+ writeJson(response, 404, { error: "unexpected request" });
154+ })().catch((_error: unknown) => {
155+ writeJson(response, 500, {
156+ error: "fixture server failed",
157+ });
158+ });
159+ });
160+
161+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
162+ const address = server.address() as AddressInfo;
163+ const stub: WorkOSStub = {
164+ baseUrl: `http://127.0.0.1:${address.port}`,
165+ requests: () => requests,
166+ setKeys: (next) => {
167+ keys = next;
168+ },
169+ };
170+
171+ return use(stub).finally(() => {
172+ server.closeAllConnections();
173+ server.close();
174+ });
175+};
2176
3−import { collectRawWorkOSList, collectWorkOSList } from "./workos";
177+const runAuthenticate = (sessionData: string, baseUrl: string) => {
178+ Object.assign(env, {
179+ WORKOS_API_KEY: API_KEY,
180+ WORKOS_CLIENT_ID: CLIENT_ID,
181+ WORKOS_COOKIE_PASSWORD: COOKIE_PASSWORD,
182+ WORKOS_API_URL: baseUrl,
183+ });
184+
185+ return Effect.runPromise(
186+ Effect.gen(function* () {
187+ const workos = yield* WorkOSClient;
188+ return yield* workos.authenticateSealedSession(sessionData);
189+ }).pipe(Effect.provide(WorkOSClient.Default)),
190+ );
191+};
192+
193+describe("authenticateSealedSession", () => {
194+ it("validates a sealed session locally with the cached JWKS", async () => {
195+ const keypair = await generateKeypair("k_valid");
196+ await withWorkOSStub(keypair, async (stub) => {
197+ const token = await signAccessToken(keypair, {
198+ organizationId: "org_test",
199+ sessionId: "session_valid",
200+ });
201+ const session = await sealSession(token);
202+
203+ const result = await runAuthenticate(session, stub.baseUrl);
204+
205+ expect(result).toEqual({
206+ userId: USER.id,
207+ email: USER.email,
208+ firstName: USER.firstName,
209+ lastName: USER.lastName,
210+ avatarUrl: USER.profilePictureUrl,
211+ organizationId: "org_test",
212+ sessionId: "session_valid",
213+ refreshedSession: undefined,
214+ });
215+ expect(stub.requests()).toEqual([
216+ { method: "GET", path: `/sso/jwks/${CLIENT_ID}`, body: null },
217+ ]);
218+ });
219+ });
220+
221+ it("reuses the module cached JWKS for a second sealed session", async () => {
222+ const keypair = await generateKeypair("k_cached");
223+ await withWorkOSStub(keypair, async (stub) => {
224+ const first = await sealSession(
225+ await signAccessToken(keypair, {
226+ sessionId: "session_first",
227+ }),
228+ );
229+ const second = await sealSession(
230+ await signAccessToken(keypair, {
231+ sessionId: "session_second",
232+ }),
233+ );
234+
235+ await runAuthenticate(first, stub.baseUrl);
236+ const afterFirst = stub.requests().length;
237+ const result = await runAuthenticate(second, stub.baseUrl);
238+
239+ expect(afterFirst).toBe(1);
240+ expect(result?.sessionId).toBe("session_second");
241+ expect(stub.requests()).toEqual([
242+ { method: "GET", path: `/sso/jwks/${CLIENT_ID}`, body: null },
243+ ]);
244+ });
245+ });
246+
247+ it("falls through to SDK refresh when the access token is expired", async () => {
248+ const keypair = await generateKeypair("k_expired");
249+ await withWorkOSStub(keypair, async (stub) => {
250+ const expiredAt = Math.floor(Date.now() / 1000) - 60;
251+ const expired = await signAccessToken(keypair, {
252+ expiresIn: expiredAt,
253+ sessionId: "session_expired",
254+ });
255+ const session = await sealSession(expired, "refresh_expired");
256+
257+ const result = await runAuthenticate(session, stub.baseUrl);
258+
259+ expect(result?.sessionId).toBe("session_refreshed");
260+ expect(result?.refreshedSession).toEqual(expect.any(String));
261+ expect(stub.requests().map((request) => [request.method, request.path])).toEqual([
262+ ["GET", `/sso/jwks/${CLIENT_ID}`],
263+ ["POST", "/user_management/authenticate"],
264+ ]);
265+ expect(stub.requests()[1]?.body).toMatchObject({
266+ grant_type: "refresh_token",
267+ client_id: CLIENT_ID,
268+ client_secret: API_KEY,
269+ refresh_token: "refresh_expired",
270+ organization_id: "org_test",
271+ });
272+ });
273+ });
274+
275+ it("returns null for garbage session data", async () => {
276+ const keypair = await generateKeypair("k_garbage");
277+ await withWorkOSStub(keypair, async (stub) => {
278+ const result = await runAuthenticate("not-a-sealed-session", stub.baseUrl);
279+
280+ expect(result).toBeNull();
281+ expect(stub.requests()).toEqual([]);
282+ });
283+ });
284+});
4285
5286 describe("collectWorkOSList", () => {
6287 it("collects memberships beyond the first WorkOS page", async () => {
apps/cloud/src/auth/workos.ts+394−44
@@ -3,13 +3,23 @@ · 2 unmodified lines
33 // ---------------------------------------------------------------------------
44
55 import { env } from "cloudflare:workers";
6−import { Context, Data, Effect, Layer, Option, Schema } from "effect";
6+import { Context, Data, Effect, Layer, Option, Predicate, Schema } from "effect";
77 import { GeneratePortalLinkIntent, WorkOS } from "@workos-inc/node/worker";
8+import { defaults as ironDefaults, unseal as unsealIron } from "iron-webcrypto";
9+import { decodeJwt, jwtVerify } from "jose";
10+import { JWKSInvalid, JWKSNoMatchingKey, JWKSTimeout } from "jose/errors";
811 import { parseCookie } from "./cookies";
9−import { tryPromiseService, withServiceLogging, workosErrorFromFailure } from "./errors";
12+import { createCachedRemoteJWKSet, type CachedRemoteJWKSet } from "./jwks-cache";
13+import {
14+ ServiceAdapterError,
15+ tryPromiseService,
16+ withServiceLogging,
17+ workosErrorFromFailure,
18+} from "./errors";
1019
1120 const COOKIE_NAME = "wos-session";
1221 const INVALID_COOKIE_PASSWORD_MESSAGE = "WORKOS_COOKIE_PASSWORD must be at least 32 characters";
22+const WORKOS_SEAL_VERSION_DELIMITER = "~";
1323
1424 type RawWorkOS = WorkOS & {
1525 readonly get: (
@@ -59,6 +69,239 @@ const RawWorkOSListResponse = Schema.Struct({ · 43 unmodified lines
5969
6070 const decodeRawWorkOSListResponse = Schema.decodeUnknownOption(RawWorkOSListResponse);
6171
72+const SealedSessionUser = Schema.Struct({
73+ id: Schema.String,
74+ email: Schema.String,
75+ firstName: Schema.NullOr(Schema.String),
76+ lastName: Schema.NullOr(Schema.String),
77+ profilePictureUrl: Schema.NullOr(Schema.String),
78+});
79+
80+const SealedSessionPayload = Schema.Struct({
81+ accessToken: Schema.String,
82+ refreshToken: Schema.String,
83+ user: SealedSessionUser,
84+ authenticationMethod: Schema.optional(Schema.Unknown),
85+ impersonator: Schema.optional(Schema.Unknown),
86+});
87+
88+const decodeSealedSessionPayload = Schema.decodeUnknownOption(SealedSessionPayload);
89+
90+const JwtClaims = Schema.Struct({
91+ sid: Schema.String,
92+ org_id: Schema.optional(Schema.String),
93+});
94+
95+const decodeJwtClaims = Schema.decodeUnknownOption(JwtClaims);
96+
97+const LegacySealedSessionPayload = Schema.Struct({
98+ persistent: Schema.Unknown,
99+});
100+
101+const decodeLegacySealedSessionPayload = Schema.decodeUnknownOption(LegacySealedSessionPayload);
102+
103+type SealedSessionPayload = typeof SealedSessionPayload.Type;
104+
105+type LocalSessionVerification =
106+ | {
107+ readonly _tag: "Valid";
108+ readonly session: SealedSessionPayload;
109+ readonly organizationId: string | undefined;
110+ readonly sessionId: string;
111+ }
112+ | { readonly _tag: "Refresh" }
113+ | { readonly _tag: "InvalidCookie" };
114+
115+class LocalSessionCookieError extends Data.TaggedError("LocalSessionCookieError")<{
116+ readonly cause: unknown;
117+}> {}
118+
119+const isLocalSessionValid = Predicate.isTagged("Valid") as (
120+ value: LocalSessionVerification,
121+) => value is Extract<LocalSessionVerification, { readonly _tag: "Valid" }>;
122+
123+const isLocalSessionInvalidCookie = Predicate.isTagged("InvalidCookie") as (
124+ value: LocalSessionVerification,
125+) => value is Extract<LocalSessionVerification, { readonly _tag: "InvalidCookie" }>;
126+
127+const ErrorWithCode = Schema.Struct({ code: Schema.String });
128+const isErrorWithCode = Schema.is(ErrorWithCode);
129+
130+const isJwtVerificationFailure = (cause: unknown): boolean =>
131+ isErrorWithCode(cause) &&
132+ (cause.code.startsWith("ERR_JWT_") ||
133+ cause.code.startsWith("ERR_JWS_") ||
134+ cause.code === JWKSNoMatchingKey.code);
135+
136+const isJwksSystemFailure = (cause: unknown): boolean =>
137+ isErrorWithCode(cause) && (cause.code === JWKSTimeout.code || cause.code === JWKSInvalid.code);
138+
139+const parseWorkOSSeal = (
140+ seal: string,
141+): { readonly sealWithoutVersion: string; readonly tokenVersion: number | null } => {
142+ const [sealWithoutVersion = "", tokenVersionAsString] = seal.split(WORKOS_SEAL_VERSION_DELIMITER);
143+ return {
144+ sealWithoutVersion,
145+ tokenVersion:
146+ tokenVersionAsString === undefined ? null : Number.parseInt(tokenVersionAsString, 10),
147+ };
148+};
149+
150+const unsealWorkOSSession = async (
151+ sessionData: string,
152+ cookiePassword: string,
153+): Promise<unknown> => {
154+ const { sealWithoutVersion, tokenVersion } = parseWorkOSSeal(sessionData);
155+ const data =
156+ (await unsealIron(sealWithoutVersion, { 1: cookiePassword }, { ...ironDefaults, ttl: 0 })) ??
157+ {};
158+ // Mirrors the SDK's unsealData version handling: current (v2) seals hold the
159+ // payload directly, OTHER versioned seals nest it under `persistent`, and an
160+ // unversioned seal is the payload itself.
161+ if (tokenVersion === 2 || tokenVersion === null) return data;
162+ return Option.match(decodeLegacySealedSessionPayload(data), {
163+ onNone: () => data,
164+ onSome: (legacy) => legacy.persistent,
165+ });
166+};
167+
168+const getWorkOSSessionJwks = (() => {
169+ const resolvers = new Map<string, CachedRemoteJWKSet>();
170+ return (url: URL): CachedRemoteJWKSet => {
171+ const key = url.toString();
172+ const existing = resolvers.get(key);
173+ if (existing) return existing;
174+ const created = createCachedRemoteJWKSet(url);
175+ resolvers.set(key, created);
176+ return created;
177+ };
178+})();
179+
180+const verifyJwtOnce = (accessToken: string, jwks: CachedRemoteJWKSet) =>
181+ Effect.tryPromise({
182+ try: () => jwtVerify(accessToken, jwks),
183+ catch: (cause) => new ServiceAdapterError({ cause }),
184+ });
185+
186+const verifyJwtWithRefreshRetry = (
187+ accessToken: string,
188+ jwks: CachedRemoteJWKSet,
189+): Effect.Effect<boolean, ServiceAdapterError> =>
190+ verifyJwtOnce(accessToken, jwks).pipe(
191+ Effect.as(true),
192+ Effect.catchTag("ServiceAdapterError", (error) => {
193+ if (isErrorWithCode(error.cause) && error.cause.code === JWKSNoMatchingKey.code) {
194+ return Effect.sync(() => jwks.forceRefresh()).pipe(
195+ Effect.flatMap(() => verifyJwtOnce(accessToken, jwks)),
196+ Effect.as(true),
197+ Effect.catchTag("ServiceAdapterError", (retryError) =>
198+ isJwtVerificationFailure(retryError.cause)
199+ ? Effect.succeed(false)
200+ : Effect.fail(retryError),
201+ ),
202+ );
203+ }
204+ if (isJwtVerificationFailure(error.cause)) return Effect.succeed(false);
205+ if (isJwksSystemFailure(error.cause)) return Effect.fail(error);
206+ return Effect.fail(error);
207+ }),
208+ );
209+
210+const verifySealedSessionLocally = (
211+ sessionData: string,
212+ cookiePassword: string,
213+ jwks: CachedRemoteJWKSet,
214+): Effect.Effect<LocalSessionVerification, ServiceAdapterError> =>
215+ Effect.gen(function* () {
216+ // Phase timings, not just child spans. `local_verify` is a leaf in
217+ // production traces, so a ~3.3s verify has nothing under it to blame —
218+ // and it stayed 3.3s after the JWKS fetch was eliminated entirely
219+ // (jwks.fetch_count == 0), so the cost is one of the phases below. Under
220+ // workerd `Date.now()` only advances at I/O boundaries, which is exactly
221+ // what makes a raw span duration misleading here: recording each phase
222+ // explicitly says which await the wall-clock actually crossed.
223+ const verifyStartedAt = Date.now();
224+
225+ const unsealStartedAt = Date.now();
226+ const unsealed = yield* Effect.tryPromise({
227+ try: () => unsealWorkOSSession(sessionData, cookiePassword),
228+ catch: (cause) => new LocalSessionCookieError({ cause }),
229+ }).pipe(
230+ Effect.catchTag("LocalSessionCookieError", () => Effect.succeed(null as unknown | null)),
231+ Effect.withSpan("workos.session.unseal"),
232+ );
233+ const unsealMs = Date.now() - unsealStartedAt;
234+ yield* Effect.annotateCurrentSpan({ "verify.unseal_ms": unsealMs });
235+ if (!unsealed) return { _tag: "InvalidCookie" };
236+
237+ const decodeStartedAt = Date.now();
238+ const session = Option.match(decodeSealedSessionPayload(unsealed), {
239+ onNone: (): SealedSessionPayload | null => null,
240+ onSome: (payload) => payload,
241+ });
242+ yield* Effect.annotateCurrentSpan({ "verify.decode_ms": Date.now() - decodeStartedAt });
243+ if (!session) return { _tag: "InvalidCookie" };
244+
245+ // Snapshot the JWKS cache around the verify so the `local_verify` span
246+ // says whether THIS verify was a warm-cache signature check or paid for a
247+ // live upstream JWKS fetch. The Aug 2026 latency regression was the cache
248+ // silently missing on most verifies, and no span attribute distinguished
249+ // the two paths.
250+ const jwksBefore = jwks.inspect();
251+ // Entry-state annotation goes on BEFORE the verify so a failing verify
252+ // (the case worth debugging) still records whether the cache was warm.
253+ yield* Effect.annotateCurrentSpan({
254+ "jwks.cache_populated_at_start": jwksBefore.hasJwks,
255+ ...(jwksBefore.fetchedAt === null
256+ ? {}
257+ : { "jwks.cache_age_ms": Date.now() - jwksBefore.fetchedAt }),
258+ });
259+ const jwtStartedAt = Date.now();
260+ const verified = yield* verifyJwtWithRefreshRetry(session.accessToken, jwks).pipe(
261+ Effect.withSpan("workos.session.jwt_verify"),
262+ Effect.onExit(() => {
263+ const jwksAfter = jwks.inspect();
264+ const finishedAt = Date.now();
265+ return Effect.annotateCurrentSpan({
266+ "verify.jwt_ms": finishedAt - jwtStartedAt,
267+ "verify.total_ms": finishedAt - verifyStartedAt,
268+ // Blocking, not total: under stale-while-revalidate a background
269+ // refresh moves `fetchCount` without costing this verify anything.
270+ // Attribute latency to what the caller actually waited on.
271+ "jwks.fetched_during_verify":
272+ jwksAfter.blockingFetchCount > jwksBefore.blockingFetchCount,
273+ "jwks.served_from_store": jwksAfter.storeHitCount > jwksBefore.storeHitCount,
274+ "jwks.fetch_count": jwksAfter.fetchCount,
275+ "jwks.blocking_fetch_count": jwksAfter.blockingFetchCount,
276+ "jwks.fetch_failure_count": jwksAfter.fetchFailureCount,
277+ ...(jwksAfter.lastFetchDurationMs === null
278+ ? {}
279+ : { "jwks.last_fetch_ms": jwksAfter.lastFetchDurationMs }),
280+ // Splits the ~3.4s that sits inside jwt_verify with zero upstream
281+ // fetches: the cross-isolate store read (I/O) vs WebCrypto key
282+ // import vs the signature check itself.
283+ ...(jwksAfter.lastStoreReadMs === null
284+ ? {}
285+ : { "jwks.store_read_ms": jwksAfter.lastStoreReadMs }),
286+ ...(jwksAfter.lastResolveMs === null
287+ ? {}
288+ : { "jwks.key_resolve_ms": jwksAfter.lastResolveMs }),
289+ });
290+ }),
291+ );
292+ if (!verified) return { _tag: "Refresh" };
293+
294+ const claims = Option.getOrNull(decodeJwtClaims(decodeJwt(session.accessToken)));
295+ if (!claims) return { _tag: "Refresh" };
296+
297+ return {
298+ _tag: "Valid",
299+ session,
300+ organizationId: claims.org_id,
301+ sessionId: claims.sid,
302+ };
303+ });
304+
62305 const completedListMetadata = {
63306 before: null,
64307 after: null,
@@ -150,14 +393,18 @@ const make = Effect.gen(function* () { · 85 unmodified lines
150393 clientId,
151394 ...workosApiUrlOptions(env.WORKOS_API_URL),
152395 });
396+ const sessionJwks = getWorkOSSessionJwks(new URL(workos.userManagement.getJwksUrl(clientId)));
153397
154398 // The public `WorkOSError` carries the upstream HTTP status when the SDK
155399 // exception had one (all its typed exceptions do), so consumers can tell a
156400 // definitive WorkOS denial (401/403/404 — fail closed) from a transient
157401 // failure (429/5xx/network — retryable).
158− const use = <A>(fn: (wos: WorkOS) => Promise<A>) =>
402+ // `op` names the SDK call (mirroring its `namespace.method` path) so every
403+ // span reads `workos.<operation>` instead of one undifferentiated "workos"
404+ // bucket, and failures log which call actually failed.
405+ const use = <A>(op: string, fn: (wos: WorkOS) => Promise<A>) =>
159406 withServiceLogging(
160− "workos",
407+ `workos.${op}`,
161408 workosErrorFromFailure,
162409 tryPromiseService(() => fn(workos)),
163410 );
@@ -171,25 +418,29 @@ const make = Effect.gen(function* () { · 7 unmodified lines
171418 cookiePassword,
172419 });
173420
174− const result = yield* use(() => session.authenticate());
421+ const local = yield* withServiceLogging(
422+ "workos.session.local_verify",
423+ workosErrorFromFailure,
424+ verifySealedSessionLocally(sessionData, cookiePassword, sessionJwks),
425+ );
175426
176− if (result.authenticated) {
427+ if (isLocalSessionValid(local)) {
177428 return {
178− userId: result.user.id,
179− email: result.user.email,
180− firstName: result.user.firstName,
181− lastName: result.user.lastName,
182− avatarUrl: result.user.profilePictureUrl,
183− organizationId: result.organizationId,
184− sessionId: result.sessionId,
429+ userId: local.session.user.id,
430+ email: local.session.user.email,
431+ firstName: local.session.user.firstName,
432+ lastName: local.session.user.lastName,
433+ avatarUrl: local.session.user.profilePictureUrl,
434+ organizationId: local.organizationId,
435+ sessionId: local.sessionId,
185436 refreshedSession: undefined as string | undefined,
186437 };
187438 }
188439
189− if (result.reason === "no_session_cookie_provided") return null;
440+ if (isLocalSessionInvalidCookie(local)) return null;
190441
191442 // Try refreshing
192− const refreshed = yield* use(() => session.refresh()).pipe(
443+ const refreshed = yield* use("session.refresh", () => session.refresh()).pipe(
193444 Effect.orElseSucceed(() => ({ authenticated: false as const })),
194445 );
195446
@@ -218,7 +469,7 @@ const make = Effect.gen(function* () { · 22 unmodified lines
218469 }),
219470
220471 authenticateWithCode: (code: string) =>
221− use((wos) =>
472+ use("userManagement.authenticateWithCode", (wos) =>
222473 wos.userManagement.authenticateWithCode({
223474 code,
224475 clientId,
@@ -228,11 +479,13 @@ const make = Effect.gen(function* () { · 3 unmodified lines
228479
229480 /** Create a new organization in WorkOS. */
230481 createOrganization: (name: string) =>
231− use((wos) => wos.organizations.createOrganization({ name })),
482+ use("organizations.createOrganization", (wos) =>
483+ wos.organizations.createOrganization({ name }),
484+ ),
232485
233486 /** Add a user to an organization. */
234487 createMembership: (organizationId: string, userId: string, roleSlug?: string) =>
235− use((wos) =>
488+ use("userManagement.createOrganizationMembership", (wos) =>
236489 wos.userManagement.createOrganizationMembership({
237490 organizationId,
238491 userId,
@@ -242,7 +495,7 @@ const make = Effect.gen(function* () { · 3 unmodified lines
242495
243496 /** List organization memberships for a user. */
244497 listUserMemberships: (userId: string) =>
245− use(async (wos) =>
498+ use("userManagement.listOrganizationMemberships", async (wos) =>
246499 collectWorkOSList(
247500 await wos.userManagement.listOrganizationMemberships({
248501 userId,
@@ -261,13 +514,53 @@ const make = Effect.gen(function* () { · 12 unmodified lines
261514 sessionData,
262515 cookiePassword,
263516 });
264− const refreshed = yield* use(() =>
517+ const refreshed = yield* use("session.refresh", () =>
265518 session.refresh(organizationId ? { organizationId } : undefined),
266519 );
267520 if (!refreshed.authenticated || !("sealedSession" in refreshed)) return null;
268521 return refreshed.sealedSession ?? null;
269522 }),
270523
524+ /**
525+ * The WorkOS logout URL for a sealed session, or null when the cookie
526+ * doesn't unseal or carries no session id (fail-open: the caller clears
527+ * local cookies and redirects home instead). The access token is decoded
528+ * WITHOUT signature verification, deliberately — WorkOS's documented
529+ * logout flow reads `sid` from a possibly-expired token, and logout must
530+ * still work for a stale session. (The SDK's `session.getLogoutUrl()` is
531+ * deliberately NOT used: it re-authenticates first and throws on an
532+ * expired token.) Pure URL construction, no network call; hitting the
533+ * URL is the browser's navigation, which is what ends the AuthKit
534+ * session upstream.
535+ */
536+ logoutUrl: (sessionData: string, returnTo?: string) =>
537+ Effect.gen(function* () {
538+ const unsealed = yield* Effect.tryPromise({
539+ try: () => unsealWorkOSSession(sessionData, cookiePassword),
540+ catch: (cause) => new LocalSessionCookieError({ cause }),
541+ }).pipe(
542+ Effect.catchTag("LocalSessionCookieError", () => Effect.succeed(null as unknown | null)),
543+ );
544+ if (!unsealed) return null;
545+
546+ const session = Option.getOrNull(decodeSealedSessionPayload(unsealed));
547+ if (!session) return null;
548+
549+ const claims = yield* Effect.try({
550+ try: () => decodeJwt(session.accessToken),
551+ catch: (cause) => new LocalSessionCookieError({ cause }),
552+ }).pipe(
553+ Effect.map((jwt) => Option.getOrNull(decodeJwtClaims(jwt))),
554+ Effect.catchTag("LocalSessionCookieError", () => Effect.succeed(null)),
555+ );
556+ if (!claims) return null;
557+
558+ return workos.userManagement.getLogoutUrl({
559+ sessionId: claims.sid,
560+ ...(returnTo ? { returnTo } : {}),
561+ });
562+ }),
563+
271564 /**
272565 * Authenticate a sealed session string. Returns the user info plus
273566 * any refreshed session that needs to be set on the response.
@@ -290,10 +583,13 @@ const make = Effect.gen(function* () { · 16 unmodified lines
290583 * auth/api-keys.ts.
291584 */
292585 validateApiKey: (value: string) =>
293− use((wos) => wos.apiKeys.validateApiKey({ value }) as Promise<unknown>),
586+ use(
587+ "apiKeys.validateApiKey",
588+ (wos) => wos.apiKeys.validateApiKey({ value }) as Promise<unknown>,
589+ ),
294590
295591 listUserApiKeys: (userId: string, organizationId: string) =>
296− use(async (wos) => {
592+ use("userManagement.listUserApiKeys", async (wos) => {
297593 const raw = wos as RawWorkOS;
298594 return collectRawWorkOSList(async (after) => {
299595 const response = await raw.get(`/user_management/users/${userId}/api_keys`, {
@@ -308,7 +604,7 @@ const make = Effect.gen(function* () { · 8 unmodified lines
308604 }),
309605
310606 createUserApiKey: (params: { userId: string; organizationId: string; name: string }) =>
311− use(async (wos) => {
607+ use("userManagement.createUserApiKey", async (wos) => {
312608 const raw = wos as RawWorkOS;
313609 const response = await raw.post(`/user_management/users/${params.userId}/api_keys`, {
314610 name: params.name,
@@ -317,11 +613,40 @@ const make = Effect.gen(function* () { · 2 unmodified lines
317613 return response.data;
318614 }),
319615
320− deleteApiKey: (id: string) => use((wos) => wos.apiKeys.deleteApiKey(id)),
616+ /**
617+ * List the ORGANIZATION-owned api keys of an org. Unlike the user-key
618+ * paths above, org keys are first-class in the installed SDK
619+ * (`organizations.listOrganizationApiKeys`), so no raw escape hatch is
620+ * needed. Returned unknown and decoded in `auth/api-keys.ts` like every
621+ * other key response.
622+ */
623+ listOrgApiKeys: (organizationId: string) =>
624+ use("organizations.listOrganizationApiKeys", async (wos) =>
625+ collectWorkOSList(await wos.organizations.listOrganizationApiKeys({ organizationId })),
626+ ),
627+
628+ /**
629+ * Mint an ORGANIZATION-owned api key: the credential that resolves to the
630+ * read-only platform view (`PlatformAuth`) instead of an acting member.
631+ * Privileged — the caller must be an admin of the org, which is enforced at
632+ * the account-provider boundary, not here.
633+ */
634+ createOrgApiKey: (params: { organizationId: string; name: string }) =>
635+ use(
636+ "organizations.createOrganizationApiKey",
637+ (wos) =>
638+ wos.organizations.createOrganizationApiKey({
639+ organizationId: params.organizationId,
640+ name: params.name,
641+ }) as Promise<unknown>,
642+ ),
643+
644+ deleteApiKey: (id: string) =>
645+ use("apiKeys.deleteApiKey", (wos) => wos.apiKeys.deleteApiKey(id)),
321646
322647 /** List organization memberships with user details. */
323648 listOrgMembers: (organizationId: string) =>
324− use(async (wos) =>
649+ use("userManagement.listOrganizationMemberships", async (wos) =>
325650 collectWorkOSList(
326651 await wos.userManagement.listOrganizationMemberships({
327652 organizationId,
@@ -332,7 +657,7 @@ const make = Effect.gen(function* () { · 4 unmodified lines
332657
333658 /** Get a user's membership in an organization. */
334659 getUserOrgMembership: (organizationId: string, userId: string) =>
335− use(async (wos) => {
660+ use("userManagement.listOrganizationMemberships", async (wos) => {
336661 const response = await wos.userManagement.listOrganizationMemberships({
337662 organizationId,
338663 userId,
@@ -342,11 +667,23 @@ const make = Effect.gen(function* () { · 3 unmodified lines
342667 }),
343668
344669 /** Get a user by ID. */
345− getUser: (userId: string) => use((wos) => wos.userManagement.getUser(userId)),
670+ getUser: (userId: string) =>
671+ use("userManagement.getUser", (wos) => wos.userManagement.getUser(userId)),
672+
673+ /** List users matching an email within one organization. */
674+ listUsers: (params: { email: string; organizationId: string }) =>
675+ use("userManagement.listUsers", async (wos) =>
676+ collectWorkOSList(
677+ await wos.userManagement.listUsers({
678+ email: params.email,
679+ organizationId: params.organizationId,
680+ }),
681+ ),
682+ ),
346683
347684 /** Send an organization invitation. */
348685 sendInvitation: (params: { email: string; organizationId: string; roleSlug?: string }) =>
349− use((wos) =>
686+ use("userManagement.sendInvitation", (wos) =>
350687 wos.userManagement.sendInvitation({
351688 email: params.email,
352689 organizationId: params.organizationId,
@@ -360,7 +697,7 @@ const make = Effect.gen(function* () { · 7 unmodified lines
360697 * API level, so we filter after.
361698 */
362699 listPendingInvitations: (organizationId: string) =>
363− use(async (wos) =>
700+ use("userManagement.listInvitations", async (wos) =>
364701 collectWorkOSList(
365702 await wos.userManagement.listInvitations({
366703 organizationId,
@@ -375,7 +712,7 @@ const make = Effect.gen(function* () { · 8 unmodified lines
375712
376713 /** List invitations for an email address (across all orgs). */
377714 listInvitationsByEmail: (email: string) =>
378− use(async (wos) =>
715+ use("userManagement.listInvitations", async (wos) =>
379716 collectWorkOSList(
380717 await wos.userManagement.listInvitations({
381718 email,
@@ -385,19 +722,25 @@ const make = Effect.gen(function* () { · 3 unmodified lines
385722
386723 /** Accept an invitation; returns the (now accepted) invitation. */
387724 acceptInvitation: (invitationId: string) =>
388− use((wos) => wos.userManagement.acceptInvitation(invitationId)),
725+ use("userManagement.acceptInvitation", (wos) =>
726+ wos.userManagement.acceptInvitation(invitationId),
727+ ),
389728
390729 /** Remove an organization membership. */
391730 deleteOrgMembership: (membershipId: string) =>
392− use((wos) => wos.userManagement.deleteOrganizationMembership(membershipId)),
731+ use("userManagement.deleteOrganizationMembership", (wos) =>
732+ wos.userManagement.deleteOrganizationMembership(membershipId),
733+ ),
393734
394735 /** Get the role for a membership. */
395736 getOrgMembership: (membershipId: string) =>
396− use((wos) => wos.userManagement.getOrganizationMembership(membershipId)),
737+ use("userManagement.getOrganizationMembership", (wos) =>
738+ wos.userManagement.getOrganizationMembership(membershipId),
739+ ),
397740
398741 /** Update a membership's role. */
399742 updateOrgMembershipRole: (membershipId: string, roleSlug: string) =>
400− use((wos) =>
743+ use("userManagement.updateOrganizationMembership", (wos) =>
401744 wos.userManagement.updateOrganizationMembership(membershipId, {
402745 roleSlug,
403746 }),
@@ -405,15 +748,19 @@ const make = Effect.gen(function* () { · 1 unmodified line
405748
406749 /** List available roles for an organization. */
407750 listOrgRoles: (organizationId: string) =>
408− use((wos) => wos.organizations.listOrganizationRoles({ organizationId })),
751+ use("organizations.listOrganizationRoles", (wos) =>
752+ wos.organizations.listOrganizationRoles({ organizationId }),
753+ ),
409754
410755 /** Get an organization (includes domains). */
411756 getOrganization: (organizationId: string) =>
412− use((wos) => wos.organizations.getOrganization(organizationId)),
757+ use("organizations.getOrganization", (wos) =>
758+ wos.organizations.getOrganization(organizationId),
759+ ),
413760
414761 /** Update an organization. */
415762 updateOrganization: (organizationId: string, name: string) =>
416− use((wos) =>
763+ use("organizations.updateOrganization", (wos) =>
417764 wos.organizations.updateOrganization({
418765 organization: organizationId,
419766 name,
@@ -425,11 +772,13 @@ const make = Effect.gen(function* () { · 5 unmodified lines
425772 * invitations, and domains go with it, so every member loses access.
426773 */
427774 deleteOrganization: (organizationId: string) =>
428− use((wos) => wos.organizations.deleteOrganization(organizationId)),
775+ use("organizations.deleteOrganization", (wos) =>
776+ wos.organizations.deleteOrganization(organizationId),
777+ ),
429778
430779 /** Generate an Admin Portal link for domain verification. */
431780 generateDomainVerificationPortalLink: (organizationId: string, returnUrl: string) =>
432− use((wos) =>
781+ use("portal.generateLink", (wos) =>
433782 wos.portal.generateLink({
434783 organization: organizationId,
435784 intent: GeneratePortalLinkIntent.DomainVerification,
@@ -439,11 +788,11 @@ const make = Effect.gen(function* () { · 3 unmodified lines
439788
440789 /** Get a domain by ID. */
441790 getOrganizationDomain: (domainId: string) =>
442− use((wos) => wos.organizationDomains.get(domainId)),
791+ use("organizationDomains.get", (wos) => wos.organizationDomains.get(domainId)),
443792
444793 /** Delete a domain claim. */
445794 deleteOrganizationDomain: (domainId: string) =>
446− use((wos) => wos.organizationDomains.delete(domainId)),
795+ use("organizationDomains.delete", (wos) => wos.organizationDomains.delete(domainId)),
447796 };
448797 });
449798
@@ -452,9 +801,10 @@ export type WorkOSClientService = Effect.Success<typeof make>; · 2 unmodified lines
452801 export class WorkOSClient extends Context.Service<WorkOSClient, WorkOSClientService>()(
453802 "@executor-js/cloud/WorkOSClient",
454803 ) {
455− static Default = Layer.effect(this)(make).pipe(
456− Layer.withSpan("WorkOSClient", { attributes: { module: "WorkOSClient" } }),
457− );
804+ // Deliberately unspanned: client construction is synchronous and ran per
805+ // layer build, which produced one of the highest-volume zero-duration span
806+ // names in the whole trace corpus while telling us nothing.
807+ static Default = Layer.effect(this)(make);
458808 }
459809
460810 // The boot-scoped WorkOS client root — the one neutral service the stateless
apps/cloud/src/db/db.schema.test.ts+2−2
@@ -2,7 +2,7 @@ · 1 unmodified line
22 // merge of cloud + executor schemas. If ANY call site builds its own
33 // `{ schema }` without spreading both, `db._.fullSchema` comes back
44 // missing tables and the drizzle adapter throws
5−// `unknown model "source"` at the first request that touches scoped data.
5+// `unknown model "integration"` at the first request that touches scoped data.
66 //
77 // We hit this in prod (MCP endpoint) when `mcp-session.ts` built
88 // `combinedSchema = { ...cloudSchema }` and forgot to spread the executor
@@ -56,7 +56,7 @@ describe("combinedSchema", () => { · 47 unmodified lines
5656 }
5757 });
5858
59− // Executor-schema drives the scope-sharded tables (source/tool/etc). If
59+ // Executor-schema drives the scope-sharded tables (integration/tool/etc). If
6060 // any of these go missing the drizzle adapter's `getTable` lookup throws.
6161 it("includes scope-sharded executor tables", () => {
6262 for (const key of Object.keys(executorSchema)) {
apps/cloud/src/db/db.ts+1−1
@@ -24,7 +24,7 @@ import * as executorSchema from "./executor-schema"; · 23 unmodified lines
2424
2525 // Exported so every drizzle() call in the cloud app shares one schema
2626 // object. Historically `mcp-session.ts` built its own and forgot to spread
27−// `executorSchema`, producing runtime "unknown model source" errors that
27+// `executorSchema`, producing runtime "unknown model integration" errors that
2828 // only surfaced in prod. See apps/cloud/src/db/db.schema.test.ts.
2929 export const combinedSchema = { ...cloudSchema, ...executorSchema };
3030
apps/cloud/src/db/dev-db-socket-concurrency.node.test.ts+272−8
@@ -21,6 +21,8 @@ · 20 unmodified lines
2121 // with DIFFERENT parameter counts (the exact drizzle/postgres-js shape) through
2222 // one PGLiteSocketServer and asserts zero protocol corruption.
2323
24+import { setTimeout as sleep } from "node:timers/promises";
25+import { connect, type Socket } from "node:net";
2426 import { describe, expect, it } from "@effect/vitest";
2527 import { PGlite } from "@electric-sql/pglite";
2628 import { PGLiteSocketServer } from "@electric-sql/pglite-socket";
@@ -30,6 +32,50 @@ const PORT = 45998; · 3 unmodified lines
3032 const CLIENTS = 6;
3133 const QUERIES_PER_CLIENT = 40;
3234
35+const makeClient = (port: number, connectTimeout = 5) =>
36+ postgres(`postgres://postgres:postgres@127.0.0.1:${port}/postgres`, {
37+ max: 1,
38+ idle_timeout: 0,
39+ connect_timeout: connectTimeout,
40+ fetch_types: false,
41+ prepare: true,
42+ onnotice: () => undefined,
43+ });
44+
45+// Hand-rolled wire client: connect and complete the trust-auth startup, so a
46+// test can then speak raw protocol frames (e.g. a lone Parse) that postgres.js
47+// would never emit on its own. Resolves after ReadyForQuery so the next write
48+// is its own data event — and its own queue entry — on the server.
49+const openWireClient = async (port: number): Promise<Socket> => {
50+ const socket: Socket = connect(port, "127.0.0.1");
51+ await new Promise<void>((res, rej) => {
52+ socket.once("connect", res);
53+ socket.once("error", rej);
54+ });
55+ const startupBody = Buffer.concat([
56+ Buffer.from([0, 3, 0, 0]),
57+ Buffer.from("user\0postgres\0database\0postgres\0\0"),
58+ ]);
59+ const startup = Buffer.concat([Buffer.alloc(4), startupBody]);
60+ startup.writeInt32BE(startup.length, 0);
61+ socket.write(startup);
62+ await new Promise<void>((res) => {
63+ socket.on("data", (chunk: Buffer) => {
64+ if (chunk.includes(0x5a)) res(); // 'Z' = ReadyForQuery
65+ });
66+ });
67+ return socket;
68+};
69+
70+// A Parse frame for an unnamed statement: opens an extended-protocol pipeline
71+// that only a later Sync (or the server's recovery) closes.
72+const parseFrame = (query: string): Buffer => {
73+ const body = Buffer.concat([Buffer.from(`\0${query}\0`), Buffer.from([0, 0])]);
74+ const frame = Buffer.concat([Buffer.from("P"), Buffer.alloc(4), body]);
75+ frame.writeInt32BE(4 + body.length, 1);
76+ return frame;
77+};
78+
3379 describe("dev-db PGlite socket under concurrent connections", () => {
3480 it(
3581 "serves interleaved multi-connection pipelines without protocol corruption",
@@ -48,14 +94,7 @@ describe("dev-db PGlite socket under concurrent connections", () => { · 12 unmodified lines
4894 const errors: string[] = [];
4995
5096 const worker = async (id: number) => {
51− const sql = postgres(`postgres://postgres:postgres@127.0.0.1:${PORT}/postgres`, {
52− max: 1,
53− idle_timeout: 0,
54− connect_timeout: 10,
55− fetch_types: false,
56− prepare: true,
57− onnotice: () => undefined,
58− });
97+ const sql = makeClient(PORT, 10);
5998 // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: postgres.js is promise-native and the socket must be closed on every path
6099 try {
61100 for (let q = 0; q < QUERIES_PER_CLIENT; q++) {
@@ -91,4 +130,229 @@ describe("dev-db PGlite socket under concurrent connections", () => { · 29 unmodified lines
91130 expect(ok).toBe(CLIENTS * QUERIES_PER_CLIENT);
92131 },
93132 );
133+
134+ // Regression for the CI e2e "cloud signIn: callback set no session (500)"
135+ // cascade: QueryQueueManager.processQueue used to `return` out of its drain
136+ // loop when a query REJECTED (as opposed to returning a wire-level
137+ // ErrorResponse), leaving `processing` latched true. From then on every
138+ // enqueue — including brand-new connections' startup packets — sat in the
139+ // queue forever: in-flight requests hung, postgres.js reconnects died with
140+ // CONNECT_TIMEOUT, and the whole dev stack was bricked until restart. The
141+ // patch rejects the one entry, drops pipeline affinity, and keeps draining.
142+ it(
143+ "a rejected query fails one client, not the whole socket server",
144+ { timeout: 30_000 },
145+ async () => {
146+ const port = 45997;
147+ const db = await PGlite.create();
148+ const server = new PGLiteSocketServer({ db, port, host: "127.0.0.1", maxConnections: 100 });
149+ await server.start();
150+
151+ const first = makeClient(port);
152+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: sockets must be closed on every path
153+ try {
154+ expect((await first.unsafe(`select 1 as one`))[0]).toEqual({ one: 1 });
155+
156+ // Force the NEXT protocol exchange to reject at the JS level, the shape
157+ // PGlite produces when the shared session is broken mid-run.
158+ const real = db.execProtocolRawStream.bind(db);
159+ let arm = true;
160+ (db as { execProtocolRawStream: typeof real }).execProtocolRawStream = (...args) => {
161+ if (arm) {
162+ arm = false;
163+ // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- test boundary: simulating a PGlite internal failure requires a raw throw
164+ throw new Error("synthetic PGlite failure");
165+ }
166+ return real(...args);
167+ };
168+
169+ await expect(first.unsafe(`select 2 as two`)).rejects.toThrow();
170+
171+ // The poisoned entry must take down only its own connection: a fresh
172+ // client (new socket, full startup handshake) still gets served.
173+ const second = makeClient(port);
174+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: sockets must be closed on every path
175+ try {
176+ expect((await second.unsafe(`select 3 as three`))[0]).toEqual({ three: 3 });
177+ } finally {
178+ // oxlint-disable-next-line executor/no-promise-catch -- test boundary: a failed teardown must not mask the assertion
179+ await second.end({ timeout: 5 }).catch(() => {});
180+ }
181+ } finally {
182+ // oxlint-disable-next-line executor/no-promise-catch -- test boundary: a failed teardown must not mask the assertion
183+ await first.end({ timeout: 5 }).catch(() => {});
184+ await server.stop();
185+ await db.close();
186+ }
187+ },
188+ );
189+
190+ // Regression for the sporadic `write CONNECTION_ENDED` 500s: the server's
191+ // idleTimeout backstop used to kill ANY connection with no traffic for the
192+ // window, which is the resting state of every healthy postgres.js pool
193+ // connection (idle_timeout: 0) held by a long-lived scope. The backstop now
194+ // only fires on a connection that is actually blocking the shared session —
195+ // an open pipeline or an open transaction.
196+ it("an idle-at-rest connection outlives the idle backstop", { timeout: 30_000 }, async () => {
197+ const port = 45996;
198+ const db = await PGlite.create();
199+ const server = new PGLiteSocketServer({
200+ db,
201+ port,
202+ host: "127.0.0.1",
203+ maxConnections: 100,
204+ idleTimeout: 250,
205+ });
206+ await server.start();
207+
208+ const sql = makeClient(port);
209+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: sockets must be closed on every path
210+ try {
211+ expect((await sql.unsafe(`select 1 as one`))[0]).toEqual({ one: 1 });
212+ await sleep(900);
213+ expect((await sql.unsafe(`select 2 as two`))[0]).toEqual({ two: 2 });
214+ } finally {
215+ // oxlint-disable-next-line executor/no-promise-catch -- test boundary: a failed teardown must not mask the assertion
216+ await sql.end({ timeout: 5 }).catch(() => {});
217+ await server.stop();
218+ await db.close();
219+ }
220+ });
221+
222+ // The backstop's actual job still works: a client that opens a pipeline
223+ // (Parse sent, never Sync) and goes silent holds queue affinity, which
224+ // starves every other connection. The idle timer must reap exactly that
225+ // client and hand the queue back.
226+ it(
227+ "a client stalled mid-pipeline is reaped and the queue recovers",
228+ { timeout: 30_000 },
229+ async () => {
230+ const port = 45995;
231+ const db = await PGlite.create();
232+ const server = new PGLiteSocketServer({
233+ db,
234+ port,
235+ host: "127.0.0.1",
236+ maxConnections: 100,
237+ idleTimeout: 250,
238+ });
239+ await server.start();
240+
241+ // Hand-rolled wire client: complete the trust-auth startup, then send a
242+ // lone Parse. Its last frame type ('P') marks the pipeline open, so the
243+ // handler takes affinity and every other connection queues behind it.
244+ const staller = await openWireClient(port);
245+ staller.write(parseFrame("select 1"));
246+
247+ const bystander = makeClient(port, 10);
248+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: sockets must be closed on every path
249+ try {
250+ // Connects and queries only once the staller is reaped (~250ms).
251+ expect((await bystander.unsafe(`select 4 as four`))[0]).toEqual({ four: 4 });
252+ } finally {
253+ // oxlint-disable-next-line executor/no-promise-catch -- test boundary: a failed teardown must not mask the assertion
254+ await bystander.end({ timeout: 5 }).catch(() => {});
255+ staller.destroy();
256+ await server.stop();
257+ await db.close();
258+ }
259+ },
260+ );
261+
262+ // Regression for the reap SLOT LEAK: detach(true) removes the socket's
263+ // listeners before destroying it, so a server-initiated teardown (the idle
264+ // backstop) never fired the server's 'close' bookkeeping — the reaped
265+ // handler stayed in the server's handlers set forever, burning one
266+ // maxConnections slot per reap. Enough reaps over a long run and the server
267+ // answers every NEW connection with "Too many connections" while the
268+ // process, the port, and PGlite are all healthy — postgres.js surfaces that
269+ // as the same CONNECT_TIMEOUT cascade as the queue wedges. The server now
270+ // drops the handler when it dispatches its terminal error.
271+ it("reaped handlers release their connection slots", { timeout: 30_000 }, async () => {
272+ const port = 45993;
273+ const db = await PGlite.create();
274+ const server = new PGLiteSocketServer({
275+ db,
276+ port,
277+ host: "127.0.0.1",
278+ maxConnections: 2,
279+ idleTimeout: 250,
280+ });
281+ await server.start();
282+
283+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: sockets must be closed on every path
284+ try {
285+ // Burn through more reaps than there are slots: each staller opens a
286+ // pipeline and goes silent, so the idle backstop reaps it (the server
287+ // destroys the socket — its 'close' marks that reap complete).
288+ for (let i = 0; i < 3; i++) {
289+ const staller = await openWireClient(port);
290+ staller.write(parseFrame(`select ${i + 1}`));
291+ await new Promise<void>((res) => staller.once("close", res));
292+ }
293+
294+ expect(
295+ server.getStats().activeConnections,
296+ "reaped handlers stay counted against maxConnections",
297+ ).toBe(0);
298+
299+ const sql = makeClient(port);
300+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: sockets must be closed on every path
301+ try {
302+ expect((await sql.unsafe(`select 6 as six`))[0]).toEqual({ six: 6 });
303+ } finally {
304+ // oxlint-disable-next-line executor/no-promise-catch -- test boundary: a failed teardown must not mask the assertion
305+ await sql.end({ timeout: 5 }).catch(() => {});
306+ }
307+ } finally {
308+ await server.stop();
309+ await db.close();
310+ }
311+ });
312+
313+ // Regression for the second wedge mode behind the same CI cascade: a client
314+ // whose socket dies WHILE its pipeline-opening entry is executing. detach()
315+ // clears pipeline affinity before the entry finishes, so the queue then
316+ // assigned affinity to the already-dead handler — and nothing ever cleared
317+ // it: the dead handler has no timers left, and every other connection
318+ // (including fresh startups) queued behind the ghost forever. The queue now
319+ // tracks detached handlers and repairs affinity they can no longer release.
320+ it(
321+ "a client that dies mid-execution does not leave the queue pinned to its ghost",
322+ { timeout: 30_000 },
323+ async () => {
324+ const port = 45994;
325+ const db = await PGlite.create();
326+
327+ // Hold the marker query in flight long enough that the disconnect below
328+ // reliably lands while the entry is EXECUTING (after detach's cleanup,
329+ // before the queue takes affinity for it).
330+ const real = db.execProtocolRawStream.bind(db);
331+ (db as { execProtocolRawStream: typeof real }).execProtocolRawStream = async (...args) => {
332+ if (Buffer.from(args[0]).includes("ghost_marker")) await sleep(300);
333+ return real(...args);
334+ };
335+
336+ const server = new PGLiteSocketServer({ db, port, host: "127.0.0.1", maxConnections: 100 });
337+ await server.start();
338+
339+ const ghost = await openWireClient(port);
340+ ghost.write(parseFrame("select 'ghost_marker'"));
341+ // Give the data event time to reach the queue and start executing, then
342+ // die without a trace mid-flight.
343+ await sleep(100);
344+ ghost.destroy();
345+
346+ const bystander = makeClient(port);
347+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: sockets must be closed on every path
348+ try {
349+ expect((await bystander.unsafe(`select 5 as five`))[0]).toEqual({ five: 5 });
350+ } finally {
351+ // oxlint-disable-next-line executor/no-promise-catch -- test boundary: a failed teardown must not mask the assertion
352+ await bystander.end({ timeout: 5 }).catch(() => {});
353+ await server.stop();
354+ await db.close();
355+ }
356+ },
357+ );
94358 });
apps/cloud/src/db/executor-schema.ts+40−1
@@ -33,6 +33,22 @@ export const integration = pgTable( · 32 unmodified lines
3333 (table) => [uniqueIndex("integration_uidx").on(table.tenant, table.slug)],
3434 );
3535
36+export const subject = pgTable(
37+ "subject",
38+ {
39+ external_id: varchar("external_id", { length: 255 }).notNull(),
40+ created_at: timestamp("created_at").notNull(),
41+ last_seen_at: bigint("last_seen_at", { mode: "bigint" }),
42+ status: text("status"),
43+ row_id: varchar("row_id", { length: 255 })
44+ .primaryKey()
45+ .notNull()
46+ .$defaultFn(() => createId()),
47+ tenant: varchar("tenant", { length: 255 }).notNull(),
48+ },
49+ (table) => [uniqueIndex("subject_uidx").on(table.tenant, table.external_id)],
50+);
51+
3652 export const connection = pgTable(
3753 "connection",
3854 {
@@ -86,6 +102,7 @@ export const oauth_client = pgTable( · 47 unmodified lines
86102 origin_kind: text("origin_kind"),
87103 origin_integration: text("origin_integration"),
88104 origin_issuer: text("origin_issuer"),
105+ origin_redirect_uri: text("origin_redirect_uri"),
89106 created_at: timestamp("created_at").notNull(),
90107 row_id: varchar("row_id", { length: 255 })
91108 .primaryKey()
@@ -164,7 +181,7 @@ export const definition = pgTable( · 72 unmodified lines
164181 integration: varchar("integration", { length: 255 }).notNull(),
165182 connection: varchar("connection", { length: 255 }).notNull(),
166183 plugin_id: text("plugin_id").notNull(),
167− name: varchar("name", { length: 255 }).notNull(),
184+ name: text("name").notNull(),
168185 schema: json("schema").notNull(),
169186 created_at: timestamp("created_at").notNull(),
170187 row_id: varchar("row_id", { length: 255 })
@@ -209,6 +226,28 @@ export const tool_policy = pgTable( · 38 unmodified lines
209226 ],
210227 );
211228
229+export const artifact = pgTable(
230+ "artifact",
231+ {
232+ id: varchar("id", { length: 255 }).notNull(),
233+ title: text("title").notNull(),
234+ description: text("description"),
235+ code: text("code").notNull(),
236+ bindings: json("bindings"),
237+ preview: text("preview"),
238+ created_at: timestamp("created_at").notNull(),
239+ updated_at: timestamp("updated_at").notNull(),
240+ row_id: varchar("row_id", { length: 255 })
241+ .primaryKey()
242+ .notNull()
243+ .$defaultFn(() => createId()),
244+ tenant: varchar("tenant", { length: 255 }).notNull(),
245+ owner: varchar("owner", { length: 255 }).notNull(),
246+ subject: varchar("subject", { length: 255 }).notNull(),
247+ },
248+ (table) => [uniqueIndex("artifact_uidx").on(table.tenant, table.owner, table.subject, table.id)],
249+);
250+
212251 export const plugin_storage = pgTable(
213252 "plugin_storage",
214253 {
apps/cloud/src/db/org-deletion.test.ts+113−1
@@ -11,16 +11,24 @@ · 10 unmodified lines
1111 // - a second org's data is completely untouched
1212 // - the blob prefix match escapes LIKE wildcards (a `_` in the org id must
1313 // not widen the match to a look-alike namespace)
14+//
15+// Plus a SCHEMA-DRIFT guard (see "purge coverage" below): the purge list is
16+// hand-written, so a table added to the schema without being added to it would
17+// leave that org's rows behind forever, silently. The guard derives the set of
18+// org-owned tables from the schema itself rather than restating it.
1419
1520 import { describe, it, expect } from "@effect/vitest";
1621 import { Effect } from "effect";
17−import { eq, inArray } from "drizzle-orm";
22+import { eq, getTableColumns, getTableName, inArray } from "drizzle-orm";
1823
1924 import { DbService } from "./db";
2025 import type { DrizzleDb } from "./db";
2126 import { makeUserStore } from "../auth/user-store";
27+import * as cloudSchema from "./schema";
28+import * as executorSchema from "./executor-schema";
2229 import { memberships, accounts } from "./schema";
2330 import {
31+ artifact,
2432 blob,
2533 connection,
2634 definition,
@@ -28,6 +36,7 @@ import { · 1 unmodified line
2836 oauth_client,
2937 oauth_session,
3038 plugin_storage,
39+ subject,
3140 tool,
3241 tool_policy,
3342 } from "./executor-schema";
@@ -131,6 +140,24 @@ const seedTenant = async (db: DrizzleDb, tenant: string, tag: string) => { · 97 unmodified lines
131140 subject: "s",
132141 });
133142
143+ await db.insert(subject).values({
144+ external_id: `acct-${tag}`,
145+ created_at: now,
146+ last_seen_at: BigInt(now.getTime()),
147+ tenant,
148+ });
149+
150+ await db.insert(artifact).values({
151+ id: `art-${tag}`,
152+ title: "Dashboard",
153+ code: "export default function App() { return null; }",
154+ created_at: now,
155+ updated_at: now,
156+ tenant,
157+ owner: "o",
158+ subject: "s",
159+ });
160+
134161 const orgNs = `o:${tenant}/plugin`;
135162 const userNs = `u:${tenant}:subject/plugin`;
136163 await db.insert(blob).values({
@@ -156,8 +183,26 @@ const TENANT_TABLES = [ · 19 unmodified lines
156183 definition,
157184 tool_policy,
158185 plugin_storage,
186+ subject,
187+ artifact,
159188 ] as const;
160189
190+// Tables that are NOT purged by org id, each with the reason it is exempt. Any
191+// schema table not carrying a `tenant` column must be listed here — that is
192+// what makes the drift guard below total rather than a spot check.
193+const NOT_ORG_OWNED: Record<string, string> = {
194+ // Purged, but by NAMESPACE PREFIX (`o:<org>/…` / `u:<org>:…`) rather than a
195+ // tenant column, so it cannot be checked by the column rule.
196+ blob: "org-scoped by namespace prefix, purged via the LIKE match",
197+ // Instance-wide, not owned by any org.
198+ private_executor_cloud_settings: "singleton instance settings, not org-scoped",
199+ // Identity mirror. `organizations` is deleted directly and `memberships`
200+ // cascades from its FK; `accounts` deliberately outlives the org.
201+ organizations: "the identity row itself, deleted directly",
202+ memberships: "cascades from the organizations FK",
203+ accounts: "shared across orgs — deliberately survives",
204+};
205+
161206 const countTenantRows = async (db: DrizzleDb, tenant: string): Promise<number> => {
162207 let total = 0;
163208 for (const table of TENANT_TABLES) {
@@ -249,3 +294,70 @@ describe("purgeOrganizationData", () => { · 85 unmodified lines
249294 );
250295 }, 20_000);
251296 });
297+
298+// ---------------------------------------------------------------------------
299+// Purge coverage vs the schema
300+// ---------------------------------------------------------------------------
301+//
302+// The purge list in `org-deletion.ts` is hand-written, one `tx.delete(...)` per
303+// table. Adding a tenant table to the schema and forgetting that line leaves
304+// every deleted org's rows in it forever — no error, no failing test, and the
305+// data is exactly what a deletion was supposed to remove.
306+//
307+// So the expected set is DERIVED: a `tenant` column is what makes a table
308+// org-owned, and every table without one must be named in `NOT_ORG_OWNED` with
309+// its reason. A new table lands in one of the two buckets automatically, and
310+// both buckets fail loudly until it is handled.
311+describe("purge coverage", () => {
312+ // Every drizzle table the cloud DB is built from (the same two modules
313+ // `combinedSchema` spreads), by table name.
314+ const allTables = Object.values({ ...cloudSchema, ...executorSchema });
315+
316+ const orgOwnedTables = allTables.filter((table) => "tenant" in getTableColumns(table));
317+ const orgOwnedNames = orgOwnedTables.map(getTableName).sort();
318+ const seededNames = TENANT_TABLES.map(getTableName).sort();
319+
320+ it("classifies every schema table as org-owned or explicitly exempt", () => {
321+ const unclassified = allTables
322+ .map(getTableName)
323+ .filter((name) => !orgOwnedNames.includes(name) && !(name in NOT_ORG_OWNED));
324+ expect(
325+ unclassified,
326+ "a new table must either carry `tenant` (and be purged) or be listed in NOT_ORG_OWNED with a reason",
327+ ).toEqual([]);
328+ });
329+
330+ it("purges every org-owned table in the schema", () => {
331+ // `TENANT_TABLES` is what this suite seeds and then asserts is empty after
332+ // the purge, so keeping it equal to the schema-derived set is what makes
333+ // the cascade test above cover the WHOLE schema rather than a snapshot of
334+ // it. A tenant table added tomorrow fails here first — the fix is to seed
335+ // it, which then fails the cascade test until `purgeOrganizationData`
336+ // deletes it too.
337+ expect(seededNames, "every org-owned table is seeded and checked by the cascade test").toEqual(
338+ orgOwnedNames,
339+ );
340+ });
341+
342+ it("seeds a row in every org-owned table, so the cascade assertions are not vacuous", async () => {
343+ // Guards the other half: a table listed in TENANT_TABLES but never seeded
344+ // would count zero rows before AND after the purge, passing while proving
345+ // nothing. Assert the seed actually populates each one.
346+ const tenant = `org_seed_${crypto.randomUUID().slice(0, 8)}`;
347+ const empty = await program(
348+ Effect.gen(function* () {
349+ const { db } = yield* DbService;
350+ return yield* Effect.promise(async () => {
351+ await seedTenant(db, tenant, `seed${crypto.randomUUID().slice(0, 6)}`);
352+ const missing: Array<string> = [];
353+ for (const table of TENANT_TABLES) {
354+ const rows = await db.select().from(table).where(eq(table.tenant, tenant));
355+ if (rows.length === 0) missing.push(getTableName(table));
356+ }
357+ return missing;
358+ });
359+ }),
360+ );
361+ expect(empty, "seedTenant must insert a row into every table the cascade checks").toEqual([]);
362+ }, 20_000);
363+});
apps/cloud/src/db/org-deletion.ts+4−0
@@ -16,6 +16,7 @@ import { eq, or, sql } from "drizzle-orm"; · 15 unmodified lines
1616 import type { DrizzleDb } from "./db";
1717 import { organizations } from "./schema";
1818 import {
19+ artifact,
1920 blob,
2021 connection,
2122 definition,
@@ -23,6 +24,7 @@ import { · 1 unmodified line
2324 oauth_client,
2425 oauth_session,
2526 plugin_storage,
27+ subject,
2628 tool,
2729 tool_policy,
2830 } from "./executor-schema";
@@ -48,6 +50,8 @@ export const purgeOrganizationData = (db: DrizzleDb, organizationId: string): Pr · 19 unmodified lines
4850 await tx.delete(oauth_session).where(eq(oauth_session.tenant, organizationId));
4951 await tx.delete(tool_policy).where(eq(tool_policy.tenant, organizationId));
5052 await tx.delete(plugin_storage).where(eq(plugin_storage.tenant, organizationId));
53+ await tx.delete(subject).where(eq(subject.tenant, organizationId));
54+ await tx.delete(artifact).where(eq(artifact.tenant, organizationId));
5155
5256 // Secrets, OAuth tokens, and cached specs live in `blob`, namespaced by
5357 // owner: `o:<org>/<plugin>` (org scope) and `u:<org>:<subject>/<plugin>`
apps/cloud/src/edge/docs.ts+9−29
@@ -6,43 +6,23 @@ · 5 unmodified lines
66 // base path, so the pathname is forwarded UNCHANGED — only the host/proto swap
77 // to the upstream origin (unlike the PostHog proxy, which strips its prefix).
88 //
9−// Like the PostHog/Sentry tunnels (and unlike the marketing proxy, which needs
10−// the prod-only `env.MARKETING` service binding), this is a plain external
11−// `fetch`, so it runs on every host — `/docs` previews against live Mintlify in
12−// local dev too. `/docs` is distinct from the app-owned `/api/docs` (Swagger),
13−// so this never shadows an Effect-served route.
9+// The matching, upstream construction, and client span live in `./passthrough`,
10+// which server.ts dispatches BEFORE Start loads: forwarding a docs page must
11+// not pay for the whole server graph. This middleware stays registered so hosts
12+// that reach Start by another entry (local dev) keep identical behavior; in the
13+// deployed Worker it is unreachable. `/docs` is distinct from the app-owned
14+// `/api/docs` (Swagger), so this never shadows an Effect-served route.
1415 // ---------------------------------------------------------------------------
1516
1617 import { createMiddleware } from "@tanstack/react-start";
1718
18−const DOCS_UPSTREAM_HOST = "executor.mintlify.dev";
19+import { docsProxyResponse, isDocsPath } from "./passthrough";
1920
20−export const isDocsPath = (pathname: string) =>
21− pathname === "/docs" || pathname.startsWith("/docs/");
22−
23−// Build the upstream request for an already-classified `/docs` path. Caller
24−// guarantees `isDocsPath(pathname)` — we only swap the origin and fix up the
25−// forwarding headers, preserving method, body, path, and query.
26−export const buildDocsUpstream = (request: Request): Request => {
27− const url = new URL(request.url);
28− const forwardedHost = url.host;
29−
30− url.hostname = DOCS_UPSTREAM_HOST;
31− url.protocol = "https:";
32− url.port = "";
33−
34− const upstream = new Request(url, request);
35− // Mintlify keys canonical links off the public host; tell it the real one.
36− upstream.headers.set("X-Forwarded-Host", forwardedHost);
37− upstream.headers.set("X-Forwarded-Proto", "https");
38− // Never leak the executor.sh session cookie to the docs origin.
39− upstream.headers.delete("cookie");
40− return upstream;
41−};
21+export { buildDocsUpstream, isDocsPath } from "./passthrough";
4222
4323 export const docsProxyMiddleware = createMiddleware({ type: "request" }).server(
4424 ({ pathname, request, next }) => {
4525 if (!isDocsPath(pathname)) return next();
46− return fetch(buildDocsUpstream(request));
26+ return docsProxyResponse(request, pathname);
4727 },
4828 );
apps/cloud/src/edge/index.ts+4−5
@@ -1,11 +1,10 @@
11 // ---------------------------------------------------------------------------
2−// Edge concerns — the analytics/marketing/docs request middlewares that run at
3−// the worker edge BEFORE the app's own mcp + api dispatch. None of these touch
4−// the Effect app layer; they proxy or tunnel to external services (the
5−// marketing worker, Sentry, PostHog, Mintlify docs).
2+// Edge concerns — request middleware that runs before the app's own mcp + api
3+// dispatch. These proxy or tunnel to external services without touching the
4+// Effect app layer. Marketing is dispatched even earlier, from server.ts, so a
5+// public page never loads the TanStack Start graph.
66 // ---------------------------------------------------------------------------
77
8−export { marketingMiddleware } from "./marketing";
98 export { sentryTunnelMiddleware } from "./sentry-tunnel";
109 export { posthogProxyMiddleware } from "./posthog";
1110 export { docsProxyMiddleware } from "./docs";
apps/cloud/src/edge/marketing.test.ts+55−1
@@ -1,6 +1,6 @@
11 import { describe, expect, it } from "@effect/vitest";
22
3−import { isMarketingPath } from "./marketing";
3+import { isMarketingPath, marketingProxyRequest } from "./marketing";
44
55 // On executor.sh the marketing middleware proxies an allow-list of paths to the
66 // `executor-marketing` worker; everything else falls through to the auth-gated
@@ -12,6 +12,9 @@ describe("isMarketingPath", () => { · 5 unmodified lines
1212 "/home",
1313 "/privacy",
1414 "/terms",
15+ "/about-executor",
16+ "/google-oauth",
17+ "/google-workspace",
1518 "/blog",
1619 "/blog/",
1720 "/blog/some-post",
@@ -37,3 +40,54 @@ describe("isMarketingPath", () => { · 19 unmodified lines
3740 });
3841 }
3942 });
43+
44+describe("marketingProxyRequest", () => {
45+ it("routes a signed-out homepage request", () => {
46+ const request = new Request("https://executor.sh/?source=test");
47+
48+ const proxied = marketingProxyRequest(request);
49+
50+ expect(proxied?.url).toBe("https://executor.sh/?source=test");
51+ });
52+
53+ it("leaves the signed-in homepage with the cloud application", () => {
54+ const request = new Request("https://executor.sh/", {
55+ headers: { cookie: "other=value; wos-session=sealed" },
56+ });
57+
58+ expect(marketingProxyRequest(request)).toBeNull();
59+ });
60+
61+ it("routes public content even when a session cookie is present", () => {
62+ const request = new Request("https://executor.sh/blog/post", {
63+ headers: { cookie: "wos-session=sealed" },
64+ });
65+
66+ expect(marketingProxyRequest(request)?.url).toBe("https://executor.sh/blog/post");
67+ });
68+
69+ it("rewrites the public home alias to the marketing root", () => {
70+ const request = new Request("https://executor.sh/home?source=test");
71+
72+ expect(marketingProxyRequest(request)?.url).toBe("https://executor.sh/?source=test");
73+ });
74+
75+ it("preserves the request method, headers, and body", async () => {
76+ const request = new Request("https://executor.sh/_astro/_ph/capture", {
77+ method: "POST",
78+ headers: { "content-type": "application/json", "x-request-id": "request-1" },
79+ body: JSON.stringify({ event: "test" }),
80+ });
81+
82+ const proxied = marketingProxyRequest(request);
83+
84+ expect(proxied?.method).toBe("POST");
85+ expect(proxied?.headers.get("x-request-id")).toBe("request-1");
86+ await expect(proxied?.json()).resolves.toEqual({ event: "test" });
87+ });
88+
89+ it("does not proxy non-production hosts or app-owned paths", () => {
90+ expect(marketingProxyRequest(new Request("http://executor-cloud.localhost/"))).toBeNull();
91+ expect(marketingProxyRequest(new Request("https://executor.sh/login"))).toBeNull();
92+ });
93+});
apps/cloud/src/edge/marketing.ts+25−34
@@ -3,14 +3,10 @@ · 2 unmodified lines
33 //
44 // On the production domain (`executor.sh`), marketing paths and the
55 // unauthenticated landing page are served by the separate `executor-marketing`
6−// worker (bound as `env.MARKETING`). In local dev that worker isn't running, so
7−// unauthenticated visits fall through to the cloud app's routes (the sign-in
8−// page).
6+// worker. This module deliberately has no TanStack Start or cloud application
7+// imports: the Worker entry calls it before loading the Start server graph.
98 // ---------------------------------------------------------------------------
109
11−import { env } from "cloudflare:workers";
12−import { createMiddleware } from "@tanstack/react-start";
13−
1410 import { parseCookie } from "../auth/cookies";
1511
1612 const MARKETING_PATHS = [
@@ -18,6 +14,9 @@ const MARKETING_PATHS = [ · 1 unmodified line
1814 "/setup",
1915 "/privacy",
2016 "/terms",
17+ "/about-executor",
18+ "/google-oauth",
19+ "/google-workspace",
2120 "/blog",
2221 "/changelog",
2322 "/changelog.json",
@@ -29,33 +28,25 @@ const MARKETING_PATHS = [ · 5 unmodified lines
2928 "/pattern-graph-paper.svg",
3029 ];
3130
32−export const isMarketingPath = (pathname: string) =>
33− MARKETING_PATHS.some((p) => pathname === p || pathname.startsWith(`${p}/`));
34−
35−const getMarketingWorker = () => env.MARKETING as { fetch: typeof fetch } | undefined;
36−
37−export const marketingMiddleware = createMiddleware({ type: "request" }).server(
38− async ({ pathname, request, next }) => {
39− // Only proxy to the marketing worker on the production domain. In local
40− // dev we don't run `executor-marketing`, so unauthenticated visits fall
41− // through to the cloud app's routes (which show the sign-in page).
42− const host = new URL(request.url).hostname;
43− if (host !== "executor.sh") return next();
31+const SESSION_COOKIE = "wos-session";
4432
45− const shouldProxyToMarketing =
46− isMarketingPath(pathname) ||
47− (pathname === "/" && !parseCookie(request.headers.get("cookie"), "wos-session"));
48−
49− if (!shouldProxyToMarketing) return next();
50−
51− const marketing = getMarketingWorker();
52− if (!marketing) return next();
33+/** Whether an exact pathname belongs to the public marketing worker. */
34+export const isMarketingPath = (pathname: string): boolean =>
35+ MARKETING_PATHS.some((p) => pathname === p || pathname.startsWith(`${p}/`));
5336
54− const url = new URL(request.url);
55− // Rewrite /home to / so marketing worker serves its homepage
56− if (pathname === "/home") {
57− url.pathname = "/";
58− }
59− return marketing.fetch(new Request(url, request));
60− },
61−);
37+/**
38+ * Project a production request onto the marketing service-binding request.
39+ * Returns `null` when the cloud application owns the request instead.
40+ */
41+export const marketingProxyRequest = (request: Request): Request | null => {
42+ const url = new URL(request.url);
43+ if (url.hostname !== "executor.sh") return null;
44+
45+ const shouldProxy =
46+ isMarketingPath(url.pathname) ||
47+ (url.pathname === "/" && !parseCookie(request.headers.get("cookie"), SESSION_COOKIE));
48+ if (!shouldProxy) return null;
49+
50+ if (url.pathname === "/home") url.pathname = "/";
51+ return new Request(url, request);
52+};
apps/cloud/src/edge/passthrough.test.ts+85−0
@@ -0,0 +1,85 @@
1+import { readFileSync } from "node:fs";
2+import { describe, expect, it } from "@effect/vitest";
3+
4+import {
5+ buildDocsUpstream,
6+ buildPosthogUpstream,
7+ isDocsPath,
8+ isPosthogPath,
9+ passthroughResponse,
10+ POSTHOG_PROXY_PATH,
11+} from "./passthrough";
12+
13+describe("passthrough matching", () => {
14+ it("claims /docs and everything under it, but not /api/docs", () => {
15+ expect(isDocsPath("/docs")).toBe(true);
16+ expect(isDocsPath("/docs/concepts/policies")).toBe(true);
17+ // The app-owned Swagger route must keep reaching the Effect app.
18+ expect(isDocsPath("/api/docs")).toBe(false);
19+ expect(isDocsPath("/docsearch")).toBe(false);
20+ });
21+
22+ it("claims the PostHog proxy path and its subtree only", () => {
23+ expect(isPosthogPath(POSTHOG_PROXY_PATH)).toBe(true);
24+ expect(isPosthogPath(`${POSTHOG_PROXY_PATH}/i/v0/e/`)).toBe(true);
25+ expect(isPosthogPath(`${POSTHOG_PROXY_PATH}extra`)).toBe(false);
26+ expect(isPosthogPath("/api/connections")).toBe(false);
27+ });
28+
29+ it("returns null for app paths so they fall through to normal dispatch", () => {
30+ expect(passthroughResponse(new Request("https://executor.sh/"), "/")).toBeNull();
31+ expect(
32+ passthroughResponse(new Request("https://executor.sh/api/connections"), "/api/connections"),
33+ ).toBeNull();
34+ expect(passthroughResponse(new Request("https://executor.sh/mcp"), "/mcp")).toBeNull();
35+ });
36+});
37+
38+describe("upstream construction", () => {
39+ it("forwards the docs path unchanged and strips the session cookie", () => {
40+ const upstream = buildDocsUpstream(
41+ new Request("https://executor.sh/docs/concepts/policies?x=1", {
42+ headers: { cookie: "wos-session=secret" },
43+ }),
44+ );
45+ const url = new URL(upstream.url);
46+ expect(url.hostname).toBe("executor.mintlify.dev");
47+ expect(url.pathname).toBe("/docs/concepts/policies");
48+ expect(url.search).toBe("?x=1");
49+ expect(upstream.headers.get("X-Forwarded-Host")).toBe("executor.sh");
50+ expect(upstream.headers.get("cookie")).toBeNull();
51+ });
52+
53+ it("strips the proxy prefix for PostHog and splits ingest from assets", () => {
54+ const ingest = buildPosthogUpstream(
55+ new Request(`https://executor.sh${POSTHOG_PROXY_PATH}/i/v0/e/`),
56+ `${POSTHOG_PROXY_PATH}/i/v0/e/`,
57+ );
58+ expect(new URL(ingest.url).hostname).toBe("us.i.posthog.com");
59+ expect(new URL(ingest.url).pathname).toBe("/i/v0/e/");
60+
61+ const assets = buildPosthogUpstream(
62+ new Request(`https://executor.sh${POSTHOG_PROXY_PATH}/static/array.js`),
63+ `${POSTHOG_PROXY_PATH}/static/array.js`,
64+ );
65+ expect(new URL(assets.url).hostname).toBe("us-assets.i.posthog.com");
66+ expect(new URL(assets.url).pathname).toBe("/static/array.js");
67+ });
68+});
69+
70+describe("Start-graph independence", () => {
71+ // The entire point of this module is that server.ts can answer a proxy
72+ // request WITHOUT importing TanStack Start. An import of the app or of
73+ // `@tanstack/react-start` here silently reintroduces the ~3.1s cold-isolate
74+ // `loadEntries` cost this module exists to avoid, and nothing else would
75+ // catch it — the behavior stays correct, only slow.
76+ it("imports neither TanStack Start nor an app module", () => {
77+ const source = readFileSync(new URL("./passthrough.ts", import.meta.url), "utf8");
78+ const imports = [...source.matchAll(/^\s*import[^"']*["']([^"']+)["']/gm)].map((m) => m[1]);
79+ expect(imports.length).toBeGreaterThan(0);
80+ for (const specifier of imports) {
81+ expect(specifier).not.toMatch(/@tanstack/);
82+ expect(specifier).not.toMatch(/^\.\.\//);
83+ }
84+ });
85+});
apps/cloud/src/edge/passthrough.ts+138−0
@@ -0,0 +1,138 @@
1+// ---------------------------------------------------------------------------
2+// Pure passthrough proxies — dispatched from the Worker entry, before Start.
3+// ---------------------------------------------------------------------------
4+//
5+// `/docs` and the PostHog proxy forward to an external origin and never touch
6+// the router, React, or the Effect app. They lived in Start's request
7+// middleware, which meant each one still paid Start's lazy `loadEntries`
8+// import of the whole server graph before it could forward a request.
9+//
10+// Measured on production (2026-08-18), splitting the two costs apart on a cold
11+// isolate: the graph import is p50 **3.1s** while the request's own work is
12+// p50 **33ms**. And essentially every request is cold — `worker.dispatch` ran
13+// 1,666 requests across 1,608 isolates (1.04 req/isolate), because `/mcp`
14+// dispatches before `fetchHandler` and so never warms the graph. A `/docs`
15+// page took 3-6s through the Worker against 0.098s straight from the upstream.
16+//
17+// So these move to the Worker entry, exactly as marketing did: classify and
18+// forward before anything imports Start. This module therefore must NOT import
19+// `@tanstack/react-start` or any app module — that import is the cost it
20+// exists to avoid.
21+//
22+// The middleware wrappers in `./docs` and `./posthog` stay registered. In the
23+// deployed Worker they become unreachable (server.ts answers first), but they
24+// keep the behavior identical on any host that reaches Start by another entry
25+// (local dev), and they source their matching from here so the two can't drift.
26+// ---------------------------------------------------------------------------
27+
28+import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
29+
30+const DOCS_UPSTREAM_HOST = "executor.mintlify.dev";
31+/** PostHog's US ingest origin. Exported because the server-side feature-flag
32+ * gate (`../analytics/ema-rollout`) calls the same origin directly, and the
33+ * two must not be able to drift onto different PostHog regions. */
34+export const POSTHOG_INGEST_HOST = "us.i.posthog.com";
35+const POSTHOG_ASSETS_HOST = "us-assets.i.posthog.com";
36+
37+export const POSTHOG_PROXY_PATH = `/api/${(
38+ import.meta.env.VITE_PUBLIC_ANALYTICS_PATH ?? "a"
39+).replace(/^\/+|\/+$/g, "")}`;
40+
41+// The proxy fetch gets its own client span: `/docs` requests otherwise render
42+// as a single opaque server span, and during the Aug 2026 regression there
43+// was no way to tell upstream (Mintlify/Vercel) latency from worker-side
44+// dispatch cost. The noop tracer applies when no provider is installed
45+// (local dev without AXIOM_TOKEN), so this is free there.
46+const tracer = trace.getTracer("executor-cloud-docs-proxy");
47+
48+export const isDocsPath = (pathname: string): boolean =>
49+ pathname === "/docs" || pathname.startsWith("/docs/");
50+
51+export const isPosthogPath = (pathname: string): boolean =>
52+ pathname === POSTHOG_PROXY_PATH || pathname.startsWith(`${POSTHOG_PROXY_PATH}/`);
53+
54+/**
55+ * Build the upstream request for an already-classified `/docs` path. Caller
56+ * guarantees `isDocsPath(pathname)` — we only swap the origin and fix up the
57+ * forwarding headers, preserving method, body, path, and query.
58+ */
59+export const buildDocsUpstream = (request: Request): Request => {
60+ const url = new URL(request.url);
61+ const forwardedHost = url.host;
62+
63+ url.hostname = DOCS_UPSTREAM_HOST;
64+ url.protocol = "https:";
65+ url.port = "";
66+
67+ const upstream = new Request(url, request);
68+ // Mintlify keys canonical links off the public host; tell it the real one.
69+ upstream.headers.set("X-Forwarded-Host", forwardedHost);
70+ upstream.headers.set("X-Forwarded-Proto", "https");
71+ // Never leak the executor.sh session cookie to the docs origin.
72+ upstream.headers.delete("cookie");
73+ return upstream;
74+};
75+
76+/** Build the upstream request for an already-classified PostHog proxy path. */
77+export const buildPosthogUpstream = (request: Request, pathname: string): Request => {
78+ const url = new URL(request.url);
79+ url.hostname = pathname.startsWith(`${POSTHOG_PROXY_PATH}/static/`)
80+ ? POSTHOG_ASSETS_HOST
81+ : POSTHOG_INGEST_HOST;
82+ url.protocol = "https:";
83+ url.port = "";
84+ url.pathname = pathname.slice(POSTHOG_PROXY_PATH.length) || "/";
85+
86+ const upstream = new Request(url, request);
87+ upstream.headers.delete("cookie");
88+ return upstream;
89+};
90+
91+export const docsProxyResponse = (request: Request, pathname: string): Promise<Response> =>
92+ tracer.startActiveSpan(
93+ `http.client ${request.method}`,
94+ {
95+ kind: SpanKind.CLIENT,
96+ attributes: {
97+ "server.address": DOCS_UPSTREAM_HOST,
98+ "url.path": pathname,
99+ "http.request.method": request.method,
100+ },
101+ },
102+ async (span) => {
103+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary; observe upstream response/error for span status, then pass both through unchanged
104+ try {
105+ const response = await fetch(buildDocsUpstream(request));
106+ span.setAttribute("http.response.status_code", response.status);
107+ if (response.status >= 500) {
108+ span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${response.status}` });
109+ }
110+ return response;
111+ } catch (err) {
112+ // oxlint-disable-next-line executor/no-instanceof-error, executor/no-unknown-error-message -- adapter boundary: fetch rejects untyped; normalized only for the OTel span record, the original error is rethrown below
113+ const cause = err instanceof Error ? err : String(err);
114+ span.recordException(cause);
115+ // oxlint-disable-next-line executor/no-unknown-error-message -- adapter boundary: same normalization as the recordException line above
116+ const message = typeof cause === "string" ? cause : cause.message;
117+ span.setStatus({ code: SpanStatusCode.ERROR, message });
118+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary; preserve the original rejection for the platform handler
119+ throw err;
120+ } finally {
121+ span.end();
122+ }
123+ },
124+ );
125+
126+/**
127+ * Answer a pure passthrough request without loading the Start server graph.
128+ * Returns `null` when the request belongs to the app, so the caller falls
129+ * through to normal dispatch.
130+ */
131+export const passthroughResponse = (
132+ request: Request,
133+ pathname: string,
134+): Promise<Response> | null => {
135+ if (isDocsPath(pathname)) return docsProxyResponse(request, pathname);
136+ if (isPosthogPath(pathname)) return fetch(buildPosthogUpstream(request, pathname));
137+ return null;
138+};
apps/cloud/src/edge/posthog.ts+8−21
@@ -3,33 +3,20 @@ · 2 unmodified lines
33 // first-party path and we forward to PostHog's ingest + asset hosts. Keeps
44 // events flowing past adblockers that match *.posthog.com. See
55 // https://posthog.com/docs/advanced/proxy/cloudflare
6+//
7+// The matching and forwarding live in `./passthrough`, which server.ts
8+// dispatches BEFORE Start loads (a proxy must not pay for the server graph).
9+// This middleware stays registered so hosts that reach Start by another entry
10+// keep identical behavior; in the deployed Worker it is unreachable.
611 // ---------------------------------------------------------------------------
712
813 import { createMiddleware } from "@tanstack/react-start";
914
10−const POSTHOG_INGEST_HOST = "us.i.posthog.com";
11−const POSTHOG_ASSETS_HOST = "us-assets.i.posthog.com";
12−const POSTHOG_PROXY_PATH = `/api/${(import.meta.env.VITE_PUBLIC_ANALYTICS_PATH ?? "a").replace(
13− /^\/+|\/+$/g,
14− "",
15−)}`;
15+import { buildPosthogUpstream, isPosthogPath } from "./passthrough";
1616
1717 export const posthogProxyMiddleware = createMiddleware({ type: "request" }).server(
1818 ({ pathname, request, next }) => {
19− if (pathname !== POSTHOG_PROXY_PATH && !pathname.startsWith(`${POSTHOG_PROXY_PATH}/`)) {
20− return next();
21− }
22−
23− const url = new URL(request.url);
24− url.hostname = pathname.startsWith(`${POSTHOG_PROXY_PATH}/static/`)
25− ? POSTHOG_ASSETS_HOST
26− : POSTHOG_INGEST_HOST;
27− url.protocol = "https:";
28− url.port = "";
29− url.pathname = pathname.slice(POSTHOG_PROXY_PATH.length) || "/";
30−
31− const upstream = new Request(url, request);
32− upstream.headers.delete("cookie");
33− return fetch(upstream);
19+ if (!isPosthogPath(pathname)) return next();
20+ return fetch(buildPosthogUpstream(request, pathname));
3421 },
3522 );
apps/cloud/src/engine/execution-gate.ts+7−0
@@ -100,10 +100,17 @@ export const withPreExecutionGate = <E extends Cause.YieldableError>( · 99 unmodified lines
100100 ),
101101 // resume is never gated: paused executions must be able to complete.
102102 resume: (executionId, response) => engine.resume(executionId, response),
103+ // Optional member, so it must be forwarded explicitly — a decorator that
104+ // rebuilds the object literal drops it, and the host then reads every settled
105+ // execution as "never existed".
106+ isExecutionSettled: engine.isExecutionSettled,
103107 getPausedExecution: (executionId) => engine.getPausedExecution(executionId),
104108 pausedExecutionCount: () => engine.pausedExecutionCount(),
105109 hasPausedExecutions: () => engine.hasPausedExecutions(),
106110 getDescription: engine.getDescription,
111+ // Forwarded, not re-implemented: the wrapped engine owns the sandbox fibers,
112+ // so the host's request-scope teardown has to reach through this decorator.
113+ shutdown: engine.shutdown,
107114 });
108115
109116 // ---------------------------------------------------------------------------
apps/cloud/src/engine/execution-rate-limit.node.test.ts+435−0
@@ -0,0 +1,435 @@
1+import { afterEach, describe, expect, it } from "@effect/vitest";
2+import { env } from "cloudflare:workers";
3+import { Data, Effect } from "effect";
4+import type * as Tracer from "effect/Tracer";
5+
6+import type { ExecutionEngine } from "@executor-js/execution";
7+
8+import {
9+ ExecutionRateLimiterDO,
10+ makeCloudExecutionRateLimiter,
11+ makeExecutionRateLimiter,
12+ RateLimitCounterError,
13+} from "./execution-rate-limit";
14+import { RATE_LIMIT_BLOCKED_MESSAGE } from "./execution-limit-messages";
15+
16+const ORG = "org_test";
17+
18+/** Stands in for whatever the real lookups fail with (Autumn down, DO down). */
19+class UpstreamDownError extends Data.TaggedError("UpstreamDownError")<{
20+ readonly which: string;
21+}> {}
22+
23+// A stand-in engine: `execute` resolves to a marker, so a test tells an allowed
24+// execution (marker) from a blocked one (the gate's error result) by which of
25+// the two came back. A blocked decision never reaches the engine at all.
26+const engineStub: ExecutionEngine = {
27+ execute: () => Effect.succeed({ result: "ran" }),
28+ executeWithPause: () => Effect.succeed({ status: "completed", result: { result: "ran" } }),
29+ resume: () => Effect.succeed(null),
30+ getPausedExecution: () => Effect.succeed(null),
31+ pausedExecutionCount: () => Effect.succeed(0),
32+ hasPausedExecutions: () => Effect.succeed(false),
33+ getDescription: Effect.succeed("stub"),
34+ // The stub forks nothing, so there is no sandbox fiber to end.
35+ shutdown: Effect.void,
36+};
37+
38+/** Counter that hands out a caller-controlled sequence of counts. */
39+const countingIncrement = (counts: ReadonlyArray<number>) => {
40+ let calls = 0;
41+ return () => Effect.succeed(counts[Math.min(calls++, counts.length - 1)] ?? 0);
42+};
43+
44+const runExecute = (limiter: ReturnType<typeof makeExecutionRateLimiter>) =>
45+ Effect.runPromise(
46+ limiter.decorate(ORG, engineStub).execute("code", {
47+ // Never invoked: the stub ignores it, and a blocked execution never runs.
48+ onElicitation: () => Effect.die("elicitation is not exercised here"),
49+ }),
50+ );
51+
52+describe("execution rate limiter — paid exemption", () => {
53+ it("allows executions under the cap without consulting the exemption", async () => {
54+ let exemptionCalls = 0;
55+ const limiter = makeExecutionRateLimiter(countingIncrement([1]), {
56+ limit: 10,
57+ isExempt: () => {
58+ exemptionCalls += 1;
59+ return Effect.succeed(false);
60+ },
61+ });
62+
63+ expect(await runExecute(limiter)).toMatchObject({ result: "ran" });
64+ // The whole point of resolving lazily: the common path costs no lookup.
65+ expect(exemptionCalls).toBe(0);
66+ });
67+
68+ it("blocks a non-exempt org over the cap", async () => {
69+ const limiter = makeExecutionRateLimiter(countingIncrement([11]), {
70+ limit: 10,
71+ isExempt: () => Effect.succeed(false),
72+ });
73+
74+ expect(await runExecute(limiter)).toMatchObject({
75+ result: null,
76+ error: RATE_LIMIT_BLOCKED_MESSAGE,
77+ });
78+ });
79+
80+ it("allows an exempt org over the cap", async () => {
81+ const limiter = makeExecutionRateLimiter(countingIncrement([11]), {
82+ limit: 10,
83+ isExempt: () => Effect.succeed(true),
84+ });
85+
86+ expect(await runExecute(limiter)).toMatchObject({ result: "ran" });
87+ });
88+
89+ it("caches the exemption so a paid org past the cap looks it up once", async () => {
90+ let exemptionCalls = 0;
91+ const limiter = makeExecutionRateLimiter(countingIncrement([11, 12, 13]), {
92+ limit: 10,
93+ exemptionTtlMs: 60_000,
94+ now: () => 1_000,
95+ isExempt: () => {
96+ exemptionCalls += 1;
97+ return Effect.succeed(true);
98+ },
99+ });
100+
101+ for (let i = 0; i < 3; i += 1)
102+ expect(await runExecute(limiter)).toMatchObject({ result: "ran" });
103+ expect(exemptionCalls).toBe(1);
104+ });
105+
106+ it("re-resolves once the cached exemption expires", async () => {
107+ let exemptionCalls = 0;
108+ let clock = 1_000;
109+ const limiter = makeExecutionRateLimiter(countingIncrement([11, 12]), {
110+ limit: 10,
111+ exemptionTtlMs: 1_000,
112+ now: () => clock,
113+ isExempt: () => {
114+ exemptionCalls += 1;
115+ return Effect.succeed(true);
116+ },
117+ });
118+
119+ await runExecute(limiter);
120+ clock += 5_000;
121+ await runExecute(limiter);
122+ expect(exemptionCalls).toBe(2);
123+ });
124+
125+ it("blocks when the exemption cannot be resolved and nothing is cached", async () => {
126+ // Deliberately NOT fail-open: an unresolvable exemption during an Autumn
127+ // outage must not switch the backstop off, which is the one scenario it
128+ // exists to cover.
129+ const limiter = makeExecutionRateLimiter(countingIncrement([11]), {
130+ limit: 10,
131+ isExempt: () => Effect.fail(new UpstreamDownError({ which: "autumn" })),
132+ });
133+
134+ expect(await runExecute(limiter)).toMatchObject({
135+ result: null,
136+ error: RATE_LIMIT_BLOCKED_MESSAGE,
137+ });
138+ });
139+
140+ it("honours a stale exemption when a later lookup fails", async () => {
141+ let clock = 1_000;
142+ let shouldFail = false;
143+ const limiter = makeExecutionRateLimiter(countingIncrement([11, 12]), {
144+ limit: 10,
145+ exemptionTtlMs: 1_000,
146+ now: () => clock,
147+ isExempt: () =>
148+ shouldFail ? Effect.fail(new UpstreamDownError({ which: "autumn" })) : Effect.succeed(true),
149+ });
150+
151+ expect(await runExecute(limiter)).toMatchObject({ result: "ran" });
152+
153+ // Cache expires and Autumn is now unreachable: the known-paid org keeps
154+ // running rather than getting blocked mid-workload by a blip.
155+ clock += 5_000;
156+ shouldFail = true;
157+ expect(await runExecute(limiter)).toMatchObject({ result: "ran" });
158+ });
159+
160+ it("fails open when the counter itself is unreachable", async () => {
161+ const limiter = makeExecutionRateLimiter(
162+ (organizationId) =>
163+ Effect.fail(
164+ new RateLimitCounterError({
165+ organizationId,
166+ code: "unknown",
167+ reason: "counter DO unreachable",
168+ cause: null,
169+ }),
170+ ),
171+ {
172+ limit: 10,
173+ isExempt: () => Effect.succeed(false),
174+ },
175+ );
176+
177+ expect(await runExecute(limiter)).toMatchObject({ result: "ran" });
178+ });
179+
180+ it("applies the cap when no exemption predicate is wired", async () => {
181+ const limiter = makeExecutionRateLimiter(countingIncrement([11]), { limit: 10 });
182+
183+ expect(await runExecute(limiter)).toMatchObject({
184+ result: null,
185+ error: RATE_LIMIT_BLOCKED_MESSAGE,
186+ });
187+ });
188+});
189+
190+// ---------------------------------------------------------------------------
191+// Counter observability — the production DO wiring.
192+//
193+// The counter increment used to be a bare one-argument `Effect.tryPromise`
194+// with no span, so a Durable Object fault reached error reporting as
195+// `UnknownError: An error occurred in Effect.tryPromise` with no application
196+// frames, and the 2s check budget was invisible in traces. These tests pin
197+// the named spans, the typed/classified failure, and the timeout override —
198+// all read through the REAL `makeCloudExecutionRateLimiter` wiring against a
199+// fake `EXECUTION_RATE_LIMITER` binding.
200+// ---------------------------------------------------------------------------
201+
202+type RecordedSpan = {
203+ readonly name: string;
204+ readonly attributes: Map<string, unknown>;
205+};
206+
207+/** A tracer that keeps every span it is asked to open, with its attributes. */
208+const recordingTracer = (recorded: Array<RecordedSpan>): Tracer.Tracer => {
209+ let nextId = 1;
210+ return {
211+ span: (options) => {
212+ let status: Tracer.SpanStatus = { _tag: "Started", startTime: options.startTime };
213+ const attributes = new Map<string, unknown>();
214+ recorded.push({ name: options.name, attributes });
215+ const id = String(nextId++).padStart(16, "0");
216+ return {
217+ _tag: "Span",
218+ name: options.name,
219+ spanId: id,
220+ traceId: "00000000000000000000000000000001",
221+ parent: options.parent,
222+ annotations: options.annotations,
223+ get status() {
224+ return status;
225+ },
226+ attributes,
227+ links: options.links,
228+ sampled: options.sampled,
229+ kind: options.kind,
230+ end: (endTime, exit) => {
231+ status = { _tag: "Ended", startTime: options.startTime, endTime, exit };
232+ },
233+ attribute: (key, value) => {
234+ attributes.set(key, value);
235+ },
236+ event: () => undefined,
237+ addLinks: () => undefined,
238+ };
239+ },
240+ };
241+};
242+
243+const spanNamed = (recorded: ReadonlyArray<RecordedSpan>, name: string): RecordedSpan => {
244+ const span = recorded.find((candidate) => candidate.name === name);
245+ expect(
246+ span,
247+ `a span named ${name} is recorded (got: ${recorded.map((s) => s.name).join(", ") || "none"})`,
248+ ).toBeDefined();
249+ return span ?? { name, attributes: new Map() };
250+};
251+
252+/** A counter-DO namespace whose single RPC method behaves as the test says. */
253+const namespaceReturning = (increment: () => Promise<number>) => ({
254+ idFromName: (name: string) => ({ toString: () => name }),
255+ get: () => ({ increment }),
256+});
257+
258+/** The three worker vars the production limiter reads at construction. */
259+type CounterEnv = {
260+ EXECUTION_RATE_LIMITER?: unknown;
261+ EXECUTION_RATE_LIMIT_PER_HOUR?: string;
262+ EXECUTION_RATE_LIMIT_CHECK_TIMEOUT_MS?: string;
263+};
264+
265+const cloudEnv: CounterEnv = env;
266+const savedEnv: CounterEnv = { ...cloudEnv };
267+
268+// Restore rather than leak: the limiter is built from the worker env, and a
269+// stale binding or budget would silently retune a later test.
270+afterEach(() => {
271+ delete cloudEnv.EXECUTION_RATE_LIMITER;
272+ delete cloudEnv.EXECUTION_RATE_LIMIT_PER_HOUR;
273+ delete cloudEnv.EXECUTION_RATE_LIMIT_CHECK_TIMEOUT_MS;
274+ Object.assign(cloudEnv, savedEnv);
275+});
276+
277+// The exact platform fault behind the production issue: Cloudflare resets the
278+// object and the RPC rejects with a plain Error. The reference id is synthetic.
279+const DO_STORAGE_RESET =
280+ "Internal error in Durable Object storage caused object to be reset; reference = 0000000000000000";
281+
282+// oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: the Durable Object RPC is a promise that rejects with a platform Error; the test double has to fail the same way for the classification to mean anything
283+const rejectStorageReset = (): Promise<number> => Promise.reject(new Error(DO_STORAGE_RESET));
284+
285+/** Build the production limiter against a fake binding and worker env. */
286+const cloudLimiter = (options: {
287+ readonly namespace: ReturnType<typeof namespaceReturning>;
288+ readonly limit: string;
289+ readonly timeoutMs?: string;
290+}): ReturnType<typeof makeExecutionRateLimiter> => {
291+ cloudEnv.EXECUTION_RATE_LIMITER = options.namespace;
292+ cloudEnv.EXECUTION_RATE_LIMIT_PER_HOUR = options.limit;
293+ if (options.timeoutMs !== undefined)
294+ cloudEnv.EXECUTION_RATE_LIMIT_CHECK_TIMEOUT_MS = options.timeoutMs;
295+ return makeCloudExecutionRateLimiter(() => Effect.succeed(false));
296+};
297+
298+const runExecuteTraced = (
299+ limiter: ReturnType<typeof makeExecutionRateLimiter>,
300+ recorded: Array<RecordedSpan>,
301+) =>
302+ Effect.runPromise(
303+ limiter
304+ .decorate(ORG, engineStub)
305+ .execute("code", { onElicitation: () => Effect.die("elicitation is not exercised here") })
306+ .pipe(Effect.withTracer(recordingTracer(recorded))),
307+ );
308+
309+describe("execution rate limiter — counter observability", () => {
310+ it("reports a counter-DO fault as a typed, classified error on a named span", async () => {
311+ const recorded: Array<RecordedSpan> = [];
312+ const limiter = cloudLimiter({
313+ namespace: namespaceReturning(rejectStorageReset),
314+ limit: "10",
315+ });
316+ const result = await runExecuteTraced(limiter, recorded);
317+
318+ // Fail-open semantics are unchanged: a broken counter never blocks.
319+ expect(result).toMatchObject({ result: "ran" });
320+
321+ const increment = spanNamed(recorded, "rate_limit.increment");
322+ expect(increment.attributes.get("rate_limit.counter.error_tag")).toBe("RateLimitCounterError");
323+ expect(increment.attributes.get("rate_limit.counter.error_code")).toBe("storage_reset");
324+
325+ const check = spanNamed(recorded, "rate_limit.check");
326+ expect(check.attributes.get("rate_limit.check.failed_open")).toBe(true);
327+ expect(check.attributes.get("rate_limit.check.timed_out")).toBe(false);
328+ expect(check.attributes.get("rate_limit.check.error_tag")).toBe("RateLimitCounterError");
329+ });
330+
331+ it("honours the EXECUTION_RATE_LIMIT_CHECK_TIMEOUT_MS override", async () => {
332+ const recorded: Array<RecordedSpan> = [];
333+ const limiter = cloudLimiter({
334+ // Answers well past the tiny budget, with a count far over the cap. If
335+ // the override were ignored the default 2s budget would let that count
336+ // through and BLOCK the execution; honouring it times the check out and
337+ // fails open instead, so the two outcomes are distinguishable without
338+ // measuring wall time.
339+ namespace: namespaceReturning(
340+ () => new Promise((resolve) => setTimeout(() => resolve(999), 200)),
341+ ),
342+ limit: "10",
343+ timeoutMs: "5",
344+ });
345+ const result = await runExecuteTraced(limiter, recorded);
346+
347+ expect(result).toMatchObject({ result: "ran" });
348+
349+ const check = spanNamed(recorded, "rate_limit.check");
350+ expect(check.attributes.get("rate_limit.check.timed_out")).toBe(true);
351+ expect(check.attributes.get("rate_limit.check.error_tag")).toBe("RateLimitCheckTimeoutError");
352+ });
353+
354+ // The healthy check's span (count / limit / blocked / failed_open) is NOT
355+ // asserted here: the cloud e2e scenario "the rate-limit counter check is
356+ // visible in the exported spans" pins it on the real workerd + Durable
357+ // Object topology, against the spans the worker actually exports. The two
358+ // cases above stay because neither is reachable from the e2e harness — it
359+ // has no fault seam for a Durable Object RPC, and the check budget is a
360+ // process-wide worker var that the shared cloud boot cannot vary per
361+ // scenario without disabling the backstop for the whole run.
362+});
363+
364+// ---------------------------------------------------------------------------
365+// Counter Durable Object
366+// ---------------------------------------------------------------------------
367+
368+/** Minimal DO storage that counts the calls the increment path makes. */
369+const fakeStorage = () => {
370+ const values = new Map<string, unknown>();
371+ const calls = { put: 0, setAlarm: 0, deleteAll: 0 };
372+ return {
373+ calls,
374+ storage: {
375+ get: (key: string) => Promise.resolve(values.get(key)),
376+ put: (key: string, value: unknown) => {
377+ calls.put += 1;
378+ values.set(key, value);
379+ return Promise.resolve();
380+ },
381+ setAlarm: () => {
382+ calls.setAlarm += 1;
383+ return Promise.resolve();
384+ },
385+ deleteAll: () => {
386+ calls.deleteAll += 1;
387+ values.clear();
388+ return Promise.resolve();
389+ },
390+ },
391+ };
392+};
393+
394+const makeCounter = (storage: ReturnType<typeof fakeStorage>["storage"]) =>
395+ // oxlint-disable-next-line executor/no-double-cast -- test double: only the four storage methods the counter uses are implemented
396+ new ExecutionRateLimiterDO({ storage } as unknown as DurableObjectState, {} as Env);
397+
398+describe("execution rate-limit counter DO", () => {
399+ it("writes the purge alarm once per window instead of on every increment", async () => {
400+ // The alarm only has to outlive the window; rewriting it on every call put
401+ // a second durable write on the hot path with the input gate closed.
402+ const fake = fakeStorage();
403+ const counter = makeCounter(fake.storage);
404+
405+ expect(await counter.increment(7)).toBe(1);
406+ expect(await counter.increment(7)).toBe(2);
407+ expect(await counter.increment(7)).toBe(3);
408+
409+ expect(fake.calls.put, "every increment still persists the count").toBe(3);
410+ expect(fake.calls.setAlarm, "the purge alarm is written once for the window").toBe(1);
411+ });
412+
413+ it("moves the purge alarm when the window rolls", async () => {
414+ const fake = fakeStorage();
415+ const counter = makeCounter(fake.storage);
416+
417+ await counter.increment(7);
418+ await counter.increment(7);
419+ expect(await counter.increment(8), "a new window restarts the count").toBe(1);
420+
421+ expect(fake.calls.setAlarm, "one alarm write per window, not per increment").toBe(2);
422+ });
423+
424+ it("re-arms the purge alarm after it has fired", async () => {
425+ const fake = fakeStorage();
426+ const counter = makeCounter(fake.storage);
427+
428+ await counter.increment(7);
429+ await counter.alarm();
430+ expect(fake.calls.deleteAll, "the alarm purges the counter's storage").toBe(1);
431+
432+ expect(await counter.increment(7), "the purge reset the window's count").toBe(1);
433+ expect(fake.calls.setAlarm, "a purged counter schedules a fresh purge").toBe(2);
434+ });
435+});
apps/cloud/src/engine/execution-rate-limit.ts+335−37
@@ -1,5 +1,5 @@
11 // ---------------------------------------------------------------------------
2−// Per-org execution rate limit — an abuse backstop independent of billing.
2+// Per-org execution rate limit — a free-tier abuse backstop.
33 //
44 // The balance gate (execution-gate.ts) depends on Autumn and fails open, so a
55 // billing outage plus runaway automation could still run unbounded executions.
@@ -8,13 +8,26 @@ · 2 unmodified lines
88 // each MCP session lives in its own DO instance, so an in-memory counter
99 // would be per-session and trivially bypassed by opening more sessions).
1010 //
11−// Like the balance gate it FAILS OPEN: an unreachable counter DO, a missing
11+// Paid organizations are EXEMPT. The cap was sized for free-tier abuse but
12+// applied to everyone, and on 2026-08-18 it blocked a paying customer
13+// mid-workload — their agent gave up on Executor and routed around it. Paid
14+// usage is what the balance gate and metered overage are for; this backstop
15+// has no business capping it.
16+//
17+// The exemption is resolved ONLY once the counter reports an org over the cap,
18+// so the common path (under the cap) costs the counter increment and nothing
19+// else. `isExempt` is an opaque predicate: this module still names no billing
20+// concept, and the Autumn coupling lives in `execution-stack-metered.ts`,
21+// which already owns that dependency.
22+//
23+// FAIL OPEN applies to the COUNTER: an unreachable counter DO, a missing
1224 // binding, or a slow call allows the execution (warn + Sentry). The backstop
13−// must never take executions down with it.
25+// must never take executions down with it. An unresolved EXEMPTION is the one
26+// thing that does not fail open — see `resolveExemption`.
1427 // ---------------------------------------------------------------------------
1528
1629 import { DurableObject, env } from "cloudflare:workers";
17−import { Data, Effect } from "effect";
30+import { Data, Effect, Predicate } from "effect";
1831 import type * as Cause from "effect/Cause";
1932
2033 import type { ExecutionEngine } from "@executor-js/execution";
@@ -25,14 +38,26 @@ import { RATE_LIMIT_BLOCKED_MESSAGE } from "./execution-limit-messages"; · 4 unmodified lines
2538
2639 // Fixed window: all executions in the same clock hour share one counter.
2740 export const RATE_LIMIT_WINDOW_MS = 3_600_000;
28−// Calibration: the heaviest legitimate org runs ~1.1k executions per MONTH,
29−// so 1000 per HOUR is far above any human-driven usage and only trips on
30−// runaway automation (the incident this backstops: ~18k in 30 days would
31−// still pass, which is fine — that class of overrun is the balance gate's
32−// job; this catches tight loops).
41+// The cap for organizations WITHOUT a paid subscription.
42+//
43+// The original calibration ("the heaviest legitimate org runs ~1.1k executions
44+// per MONTH, so 1000 per HOUR is far above any human-driven usage") went stale
45+// inside six weeks: by 2026-08-18 a paying org was sustaining ~5.3k executions
46+// per DAY and crossed this cap in a single hour. Sizing a shared number
47+// against the largest customer is a losing game, so the number no longer tries
48+// to describe them — paid orgs are exempt below, and this now has only
49+// free-tier abuse to cover, which is what it was picked for.
3350 export const EXECUTIONS_PER_ORG_PER_HOUR = 1000;
3451 // Counter DO slower than this => fail open rather than stall executions.
3552 const RATE_LIMIT_CHECK_TIMEOUT_MS = 2_000;
53+// Exemption lookup slower than this => treat as unresolved.
54+const EXEMPTION_CHECK_TIMEOUT_MS = 2_000;
55+// An org over the cap is checked at most once per TTL rather than once per
56+// execution, so a paid org running far past it doesn't hammer the lookup.
57+const EXEMPTION_CACHE_TTL_MS = 60_000;
58+// Sweep guard, mirroring the balance gate's cache: one long-lived isolate can
59+// serve many orgs.
60+const EXEMPTION_CACHE_MAX_ENTRIES = 10_000;
3661 // The DO purges its storage this long after the last increment, so idle orgs
3762 // cost nothing. Two windows: long enough that an active window never purges.
3863 const COUNTER_PURGE_AFTER_MS = 2 * RATE_LIMIT_WINDOW_MS;
@@ -51,6 +76,80 @@ class RateLimitCheckTimeoutError extends Data.TaggedError("RateLimitCheckTimeout · 12 unmodified lines
5176 readonly timeoutMs: number;
5277 }> {}
5378
79+/**
80+ * Why a counter DO call failed, as a small closed vocabulary.
81+ *
82+ * The counter's failures are overwhelmingly transient Cloudflare platform
83+ * faults, and they used to arrive at error reporting as one untyped
84+ * `UnknownError: An error occurred in Effect.tryPromise` with no application
85+ * frames — a group that says nothing and would eventually swallow a real
86+ * misconfiguration too. The code is what makes a storage reset (retryable,
87+ * expected) distinguishable from an overload or an outright unknown fault.
88+ */
89+export type RateLimitCounterErrorCode =
90+ | "storage_reset"
91+ | "overloaded"
92+ | "exceeded_memory"
93+ | "network"
94+ | "unknown";
95+
96+/** A counter DO call that failed, carrying the classification and the org. */
97+export class RateLimitCounterError extends Data.TaggedError("RateLimitCounterError")<{
98+ readonly organizationId: string;
99+ readonly code: RateLimitCounterErrorCode;
100+ readonly reason: string;
101+ readonly cause: unknown;
102+}> {}
103+
104+// Cloudflare surfaces these as plain `Error`s with documented message text;
105+// there is no structured code to read, so the message is the only signal.
106+// (A shared `classifyDurableObjectError` would be the right home for this once
107+// one exists.)
108+const counterErrorCode = (reason: string): RateLimitCounterErrorCode => {
109+ if (/caused object to be reset/i.test(reason)) return "storage_reset";
110+ if (/overloaded/i.test(reason)) return "overloaded";
111+ if (/exceeded (its )?memory|out of memory/i.test(reason)) return "exceeded_memory";
112+ if (/network connection lost|connection.*(lost|reset)/i.test(reason)) return "network";
113+ return "unknown";
114+};
115+
116+/**
117+ * The fail-open landing: record the outcome on the check span, warn, and allow
118+ * the execution. Only failures that are NOT deliberate degradation reach the
119+ * error reporter — see the call sites in `decide`.
120+ */
121+const failOpen = (
122+ error: unknown,
123+ outcome: { readonly errorTag: string; readonly timedOut: boolean },
124+): Effect.Effect<GateDecision> =>
125+ Effect.gen(function* () {
126+ yield* Effect.annotateCurrentSpan({
127+ "rate_limit.blocked": false,
128+ "rate_limit.check.failed_open": true,
129+ "rate_limit.check.timed_out": outcome.timedOut,
130+ "rate_limit.check.error_tag": outcome.errorTag,
131+ });
132+ yield* Effect.sync(() => {
133+ console.warn("[rate-limit] execution rate limit check failed open:", error);
134+ });
135+ if (!outcome.timedOut) yield* captureCauseEffect(error);
136+ return { blocked: false } as const satisfies GateDecision;
137+ });
138+
139+/** Internal sentinel for an exemption lookup that exceeded its time budget. */
140+class ExemptionCheckTimeoutError extends Data.TaggedError("ExemptionCheckTimeoutError")<{
141+ readonly timeoutMs: number;
142+}> {}
143+
144+/**
145+ * Whether an organization is exempt from the cap. Production passes a paid-
146+ * subscription check; keeping it an opaque predicate is what lets this module
147+ * stay free of any billing import.
148+ */
149+export type ExecutionRateLimitExemption = (
150+ organizationId: string,
151+) => Effect.Effect<boolean, unknown>;
152+
54153 // ---------------------------------------------------------------------------
55154 // Counter Durable Object — one instance per organization (idFromName(orgId)).
56155 // Stores a single { windowId, count } record: an increment in a new window
@@ -67,6 +166,12 @@ type WindowRecord = { · 10 unmodified lines
67166
68167 export class ExecutionRateLimiterDO extends DurableObject {
69168 private readonly counterStorage: DurableObjectState["storage"];
169+ /**
170+ * The window this instance has already armed the purge alarm for. In-memory
171+ * on purpose: it costs no storage read, and a fresh instance (eviction, cold
172+ * start) simply re-arms on its first increment.
173+ */
174+ private purgeArmedForWindow: number | null = null;
70175
71176 constructor(ctx: DurableObjectState, doEnv: Env) {
72177 super(ctx, doEnv);
@@ -80,11 +185,21 @@ export class ExecutionRateLimiterDO extends DurableObject { · 7 unmodified lines
80185 const stored = await this.counterStorage.get<WindowRecord>(WINDOW_RECORD_KEY);
81186 const count = stored && stored.windowId === windowId ? stored.count + 1 : 1;
82187 await this.counterStorage.put(WINDOW_RECORD_KEY, { windowId, count });
83− await this.counterStorage.setAlarm(Date.now() + COUNTER_PURGE_AFTER_MS);
188+ // The alarm only has to outlive the window, and it is set two windows out,
189+ // so once per window is enough — rewriting it on every increment put a
190+ // second durable write and an alarm-manager update on the hot path of
191+ // every execution, with the input gate closed across all three. `count`
192+ // back at 1 means the window rolled (or a purge already ran), so the
193+ // deadline moves with it.
194+ if (count === 1 || this.purgeArmedForWindow !== windowId) {
195+ await this.counterStorage.setAlarm(Date.now() + COUNTER_PURGE_AFTER_MS);
196+ this.purgeArmedForWindow = windowId;
197+ }
84198 return count;
85199 }
86200
87201 async alarm(): Promise<void> {
202+ this.purgeArmedForWindow = null;
88203 await this.counterStorage.deleteAll();
89204 }
90205 }
@@ -93,11 +208,17 @@ export class ExecutionRateLimiterDO extends DurableObject { · 2 unmodified lines
93208 // Client
94209 // ---------------------------------------------------------------------------
95210
96−/** Count one execution for (organizationId, windowId); returns the new count. */
211+/**
212+ * Count one execution for (organizationId, windowId); returns the new count.
213+ *
214+ * The failure channel is typed rather than `unknown` so the fail-open path can
215+ * tell a counter fault from a blown budget by tag, and so error reporting
216+ * groups by cause instead of by one opaque `UnknownError`.
217+ */
97218 export type RateLimitIncrement = (
98219 organizationId: string,
99220 windowId: number,
100−) => Effect.Effect<number, unknown>;
221+) => Effect.Effect<number, RateLimitCounterError>;
101222
102223 export type ExecutionRateLimiter = {
103224 readonly decorate: <E extends Cause.YieldableError>(
@@ -119,12 +240,76 @@ export const makeExecutionRateLimiter = ( · 15 unmodified lines
119240 readonly windowMs?: number;
120241 readonly timeoutMs?: number;
121242 readonly now?: () => number;
243+ readonly isExempt?: ExecutionRateLimitExemption;
244+ readonly exemptionTtlMs?: number;
122245 },
123246 ): ExecutionRateLimiter => {
124247 const limit = options?.limit ?? EXECUTIONS_PER_ORG_PER_HOUR;
125248 const windowMs = options?.windowMs ?? RATE_LIMIT_WINDOW_MS;
126249 const timeoutMs = options?.timeoutMs ?? RATE_LIMIT_CHECK_TIMEOUT_MS;
127250 const now = options?.now ?? Date.now;
251+ const isExempt = options?.isExempt;
252+ const exemptionTtlMs = options?.exemptionTtlMs ?? EXEMPTION_CACHE_TTL_MS;
253+
254+ const exemptionCache = new Map<
255+ string,
256+ { readonly exempt: boolean; readonly expiresAtMs: number }
257+ >([]);
258+
259+ const writeExemptionCache = (organizationId: string, exempt: boolean, nowMs: number): void => {
260+ if (exemptionCache.size >= EXEMPTION_CACHE_MAX_ENTRIES) {
261+ for (const [key, entry] of exemptionCache) {
262+ if (entry.expiresAtMs <= nowMs) exemptionCache.delete(key);
263+ }
264+ // Still saturated after dropping expired entries: reset rather than grow.
265+ if (exemptionCache.size >= EXEMPTION_CACHE_MAX_ENTRIES) exemptionCache.clear();
266+ }
267+ exemptionCache.set(organizationId, { exempt, expiresAtMs: nowMs + exemptionTtlMs });
268+ };
269+
270+ /**
271+ * Resolved only for orgs already over the cap, so the lookup never touches
272+ * the common path.
273+ *
274+ * This is the one place that does NOT fail open. The balance gate already
275+ * allows executions when Autumn is unreachable; if the exemption did too,
276+ * an Autumn outage would switch this backstop off entirely — precisely the
277+ * "billing outage plus runaway automation" case it exists to cover. A stale
278+ * positive is honoured ahead of that fallback, so a blip can't flip a
279+ * known-paid org into a block mid-workload.
280+ */
281+ const resolveExemption = (organizationId: string): Effect.Effect<boolean> =>
282+ Effect.suspend(() => {
283+ if (!isExempt) return Effect.succeed(false);
284+ const nowMs = now();
285+ const cached = exemptionCache.get(organizationId);
286+ if (cached && cached.expiresAtMs > nowMs) return Effect.succeed(cached.exempt);
287+ return isExempt(organizationId).pipe(
288+ Effect.timeoutOrElse({
289+ duration: `${EXEMPTION_CHECK_TIMEOUT_MS} millis`,
290+ orElse: () =>
291+ Effect.fail(new ExemptionCheckTimeoutError({ timeoutMs: EXEMPTION_CHECK_TIMEOUT_MS })),
292+ }),
293+ Effect.map((exempt) => {
294+ writeExemptionCache(organizationId, exempt, nowMs);
295+ return exempt;
296+ }),
297+ Effect.catch((error: unknown) =>
298+ Effect.gen(function* () {
299+ yield* Effect.sync(() => {
300+ console.warn(
301+ `[rate-limit] exemption lookup failed for ${organizationId}; treating as ${
302+ cached ? "last known" : "not exempt"
303+ }:`,
304+ error,
305+ );
306+ });
307+ yield* captureCauseEffect(error);
308+ return cached?.exempt ?? false;
309+ }),
310+ ),
311+ );
312+ });
128313
129314 const decide = (organizationId: string): Effect.Effect<GateDecision> =>
130315 Effect.suspend(() => {
@@ -134,29 +319,86 @@ export const makeExecutionRateLimiter = ( · 3 unmodified lines
134319 duration: `${timeoutMs} millis`,
135320 orElse: () => Effect.fail(new RateLimitCheckTimeoutError({ timeoutMs })),
136321 }),
137− Effect.map(
138− (count): GateDecision =>
139− count > limit
140− ? {
141− blocked: true,
142− error: new ExecutionRateLimitExceededError({
143− organizationId,
144− message: RATE_LIMIT_BLOCKED_MESSAGE,
145− }),
146− }
147− : { blocked: false },
148− ),
322+ Effect.flatMap((count): Effect.Effect<GateDecision> => {
323+ // Under the cap: no exemption lookup, no extra I/O.
324+ if (count <= limit)
325+ return Effect.as(
326+ Effect.annotateCurrentSpan({
327+ "rate_limit.count": count,
328+ "rate_limit.blocked": false,
329+ "rate_limit.check.failed_open": false,
330+ }),
331+ { blocked: false },
332+ );
333+ return Effect.gen(function* () {
334+ yield* Effect.annotateCurrentSpan({
335+ "rate_limit.count": count,
336+ "rate_limit.check.failed_open": false,
337+ });
338+ if (yield* resolveExemption(organizationId)) {
339+ yield* Effect.annotateCurrentSpan({
340+ "rate_limit.blocked": false,
341+ "rate_limit.exempt": true,
342+ });
343+ return { blocked: false } as const satisfies GateDecision;
344+ }
345+ // The only record that the backstop fired. A blocked execution is
346+ // never usage-tracked (the gate short-circuits before the tracker)
347+ // and deliberately not sent to Sentry — a backstop stopping
348+ // runaway automation is expected, not exceptional — so without
349+ // this line a blocked org is invisible outside a bug report, which
350+ // is how the 2026-08-18 block went unnoticed until a customer
351+ // sent a screenshot.
352+ yield* Effect.sync(() => {
353+ console.warn(
354+ `[rate-limit] blocked execution for ${organizationId}: ${count} > ${limit} in window ${windowId}`,
355+ );
356+ });
357+ yield* Effect.annotateCurrentSpan({
358+ "rate_limit.blocked": true,
359+ "rate_limit.exempt": false,
360+ });
361+ return {
362+ blocked: true,
363+ error: new ExecutionRateLimitExceededError({
364+ organizationId,
365+ message: RATE_LIMIT_BLOCKED_MESSAGE,
366+ }),
367+ } as const satisfies GateDecision;
368+ });
369+ }),
149370 // FAIL OPEN: the backstop must never block executions because its
150371 // counter is unreachable or slow.
372+ //
373+ // A check that blew its own budget is DELIBERATE degradation, not an
374+ // exception — the timeout exists precisely so a slow counter can't
375+ // stall a user-facing execution. It is measured on the span
376+ // (`rate_limit.check.timed_out`), where a step change in the rate is
377+ // alertable, rather than paged per occurrence, which is what buried
378+ // real counter failures under one opaque group. Everything else (RPC
379+ // faults, a missing binding) still reports.
380+ Effect.catchTag("RateLimitCheckTimeoutError", (error) =>
381+ failOpen(error, { errorTag: "RateLimitCheckTimeoutError", timedOut: true }),
382+ ),
383+ // A catch-all rather than a second `catchTag`: fail-open is a hard
384+ // requirement and must not depend on the failure being one the types
385+ // predicted.
151386 Effect.catch((error: unknown) =>
152− Effect.gen(function* () {
153− yield* Effect.sync(() => {
154− console.warn("[rate-limit] execution rate limit check failed open:", error);
155− });
156− yield* captureCauseEffect(error);
157− return { blocked: false } as const satisfies GateDecision;
387+ failOpen(error, {
388+ errorTag: Predicate.isTagged(error, "RateLimitCounterError")
389+ ? "RateLimitCounterError"
390+ : "unknown",
391+ timedOut: false,
158392 }),
159393 ),
394+ Effect.withSpan("rate_limit.check", {
395+ attributes: {
396+ "rate_limit.organization_id": organizationId,
397+ "rate_limit.window_id": windowId,
398+ "rate_limit.limit": limit,
399+ "rate_limit.check.timeout_ms": timeoutMs,
400+ },
401+ }),
160402 );
161403 });
162404
@@ -185,8 +427,13 @@ type RateLimiterNamespace = { · 22 unmodified lines
185427 * Production rate limiter backed by the `EXECUTION_RATE_LIMITER` counter DO.
186428 * When the binding is absent (unit-test workers, older local setups) the
187429 * limiter is disabled: every check passes, logged once at construction.
430+ *
431+ * `isExempt` decides which orgs the cap skips; production passes a paid-
432+ * subscription check from `execution-stack-metered.ts`.
188433 */
189−export const makeCloudExecutionRateLimiter = (): ExecutionRateLimiter => {
434+export const makeCloudExecutionRateLimiter = (
435+ isExempt: ExecutionRateLimitExemption,
436+): ExecutionRateLimiter => {
190437 const limit = resolveRateLimit();
191438 const namespace = (env as { EXECUTION_RATE_LIMITER?: RateLimiterNamespace })
192439 .EXECUTION_RATE_LIMITER;
@@ -196,17 +443,53 @@ export const makeCloudExecutionRateLimiter = (): ExecutionRateLimiter => { · 3 unmodified lines
196443 );
197444 return makeExecutionRateLimiter(() => Effect.succeed(0));
198445 }
199− return makeExecutionRateLimiter(
200− (organizationId, windowId) =>
201− Effect.tryPromise(() => {
446+ return makeExecutionRateLimiter(counterIncrement(namespace), {
447+ limit,
448+ timeoutMs: resolveCheckTimeoutMs(),
449+ isExempt,
450+ });
451+};
452+
453+/**
454+ * The counter DO RPC, as a traced and typed increment.
455+ *
456+ * The span is the whole point: this call is a blocking, cold-startable hop on
457+ * the execute hot path, and until it had one nothing about its duration was
458+ * measurable — the only evidence it was slow was the fail-open warning 2s
459+ * later. The typed error replaces Effect's generic `UnknownError`, which
460+ * reported no org, no window, and no hint that a Durable Object was involved.
461+ */
462+const counterIncrement =
463+ (namespace: RateLimiterNamespace): RateLimitIncrement =>
464+ (organizationId, windowId) =>
465+ Effect.tryPromise({
466+ try: () => {
202467 const stub = namespace.get(
203468 namespace.idFromName(organizationId),
204469 ) as ExecutionRateLimiterStub;
205470 return stub.increment(windowId);
471+ },
472+ catch: (cause) => {
473+ // oxlint-disable-next-line executor/no-instanceof-error, executor/no-unknown-error-message -- boundary: the Durable Object RPC rejects with a plain platform Error whose message text is the only classification signal Cloudflare gives
474+ const reason = cause instanceof Error ? cause.message : String(cause);
475+ return new RateLimitCounterError({
476+ organizationId,
477+ code: counterErrorCode(reason),
478+ reason,
479+ cause,
480+ });
481+ },
482+ }).pipe(
483+ Effect.tapError((error) =>
484+ Effect.annotateCurrentSpan({
485+ "rate_limit.counter.error_tag": "RateLimitCounterError",
486+ "rate_limit.counter.error_code": error.code,
487+ }),
488+ ),
489+ Effect.withSpan("rate_limit.increment", {
490+ attributes: { "rate_limit.window_id": windowId },
206491 }),
207− { limit },
208− );
209−};
492+ );
210493
211494 /**
212495 * The per-org hourly cap: the `EXECUTION_RATE_LIMIT_PER_HOUR` env override
@@ -220,3 +503,18 @@ const resolveRateLimit = (): number => { · 7 unmodified lines
220503 const parsed = Number.parseInt(raw, 10);
221504 return Number.isInteger(parsed) && parsed > 0 ? parsed : EXECUTIONS_PER_ORG_PER_HOUR;
222505 };
506+
507+/**
508+ * The counter's time budget: the `EXECUTION_RATE_LIMIT_CHECK_TIMEOUT_MS` env
509+ * override or `RATE_LIMIT_CHECK_TIMEOUT_MS` when it's unset or unparseable.
510+ * Same precedent and same purpose as `EXECUTION_RATE_LIMIT_PER_HOUR`: the
511+ * production 2s budget can't be blown on demand, so tests set a tiny one to
512+ * drive the fail-open path deterministically. Production leaves it unset.
513+ */
514+const resolveCheckTimeoutMs = (): number => {
515+ const raw = (env as { EXECUTION_RATE_LIMIT_CHECK_TIMEOUT_MS?: string })
516+ .EXECUTION_RATE_LIMIT_CHECK_TIMEOUT_MS;
517+ if (raw === undefined) return RATE_LIMIT_CHECK_TIMEOUT_MS;
518+ const parsed = Number.parseInt(raw, 10);
519+ return Number.isInteger(parsed) && parsed > 0 ? parsed : RATE_LIMIT_CHECK_TIMEOUT_MS;
520+};
apps/cloud/src/engine/execution-stack-metered.ts+15−2
@@ -26,6 +26,7 @@ import { · 25 unmodified lines
2626 } from "@executor-js/api/server";
2727
2828 import { AutumnService } from "../extensions/billing/service";
29+import { hasPaidOrganizationSubscription } from "../extensions/billing/plans";
2930 import type { DbService } from "../db/db";
3031 import { CloudExecutionSeamsLayer } from "../engine/execution-stack";
3132 import { makeExecutionLimitGate } from "./execution-gate";
@@ -34,7 +35,8 @@ import { withExecutionUsageTracking } from "./execution-usage"; · 2 unmodified lines
3435
3536 // Usage-metering decorator bound to the billing service, plus the two
3637 // pre-execution guards this layer owns, ordered cheapest first:
37−// 1. rate-limit backstop (counter DO, independent of billing)
38+// 1. rate-limit backstop (counter DO; free-tier abuse only — paid orgs are
39+// exempt via the subscription lookup below)
3840 // 2. execution balance gate (Autumn check, cached 60s, fails open)
3941 // 3. usage tracking — fire-and-forget (`Effect.runFork`) so the billing
4042 // call can't stall a user-facing execution.
@@ -48,7 +50,18 @@ export const CloudMeteringEngineDecorator: Layer.Layer<EngineDecorator, never, A · 7 unmodified lines
4850 const balanceGate = makeExecutionLimitGate((organizationId) =>
4951 autumn.checkExecutionBalance(organizationId),
5052 );
51− const rateLimiter = makeCloudExecutionRateLimiter();
53+ // The limiter's paid-org exemption. This is the billing coupling the
54+ // limiter module deliberately avoids owning, and it reads the same
55+ // `PAID_AUTUMN_PLAN_IDS` config as the org-creation and seat gates so
56+ // "paid" means one thing across the app. The limiter calls this only
57+ // for orgs already over the cap and caches the answer, so the extra
58+ // Autumn round trip stays off the hot path.
59+ const rateLimiter = makeCloudExecutionRateLimiter((organizationId) =>
60+ Effect.map(
61+ autumn.use((client) => client.customers.getOrCreate({ customerId: organizationId })),
62+ (customer) => hasPaidOrganizationSubscription(customer.subscriptions),
63+ ),
64+ );
5265 return {
5366 decorate: (engine, identity: EngineStackIdentity) =>
5467 rateLimiter.decorate(
apps/cloud/src/engine/execution-stack.ts+116−2
@@ -42,9 +42,17 @@ import { · 41 unmodified lines
4242 PluginsProvider,
4343 collectTables,
4444 } from "@executor-js/api/server";
45+import { googleCatalogOAuthScopesForPreset } from "@executor-js/plugin-openapi/providers/google";
46+import { slackMcpUserScopes } from "@executor-js/react/lib/slack-mcp-oauth";
4547 import { makeDynamicWorkerExecutor } from "@executor-js/runtime-dynamic-worker";
48+import {
49+ IntegrationSlug,
50+ type AnyPlugin,
51+ type FirstPartyOAuthClientConfig,
52+} from "@executor-js/sdk";
4653
4754 import executorConfig from "../../executor.config";
55+import { cloudEnterpriseManagedRollout } from "../analytics/ema-rollout";
4856 import { DbService } from "../db/db";
4957 import { cloudDbProviderLayer } from "../db/fuma";
5058
@@ -54,11 +62,20 @@ export { makeExecutionStack } from "@executor-js/api/server"; · 3 unmodified lines
5462 // DbProvider rebuilds the fuma client over the same schema.
5563 export const CloudDbProvider = cloudDbProviderLayer(collectTables());
5664
65+const cloudPluginFactory = executorConfig.plugins as (deps: {
66+ readonly workosCredentials: {
67+ readonly apiKey: string;
68+ readonly clientId: string;
69+ readonly apiUrl?: string;
70+ };
71+ readonly activeToolkitSlug?: string;
72+}) => readonly AnyPlugin[];
73+
5774 // Fresh plugin instances per request, carrying the Worker env's WorkOS Vault
5875 // credentials. Matches the old `createScopedExecutor`'s `orgPlugins()`.
5976 export const CloudPluginsProvider: Layer.Layer<PluginsProvider> = Layer.succeed(PluginsProvider)({
6077 plugins: (context) =>
61− executorConfig.plugins({
78+ cloudPluginFactory({
6279 workosCredentials: {
6380 apiKey: env.WORKOS_API_KEY,
6481 clientId: env.WORKOS_CLIENT_ID,
@@ -66,7 +83,6 @@ export const CloudPluginsProvider: Layer.Layer<PluginsProvider> = Layer.succeed( · 1 unmodified line
6683 },
6784 activeToolkitSlug:
6885 context?.mcpResource?.kind === "toolkit" ? context.mcpResource.slug : undefined,
69− allowLocalNetwork: env.ALLOW_LOCAL_NETWORK === "true",
7086 }),
7187 });
7288
@@ -79,6 +95,98 @@ export const CloudPluginsProvider: Layer.Layer<PluginsProvider> = Layer.succeed( · 6 unmodified lines
7995 */
8096 export const CLOUD_MOUNT_PREFIX = "/api" as const;
8197
98+// Consumer Google launch boundary. Keep this list aligned with the scopes
99+// submitted for the Executor-owned production app: ordinary Workspace services
100+// plus Photos, Meet, and Search Console. Admin, Classroom, YouTube, Apps Script,
101+// BigQuery, and Cloud Resource Manager have materially different audiences or
102+// provider requirements and remain BYO OAuth. The same scope source builds each
103+// catalog auth template, preventing picker/start drift.
104+const GOOGLE_FIRST_PARTY_PRESET_IDS = [
105+ "google-calendar",
106+ "google-meet",
107+ "google-gmail",
108+ "google-sheets",
109+ "google-drive",
110+ "google-docs",
111+ "google-slides",
112+ "google-forms",
113+ "google-tasks",
114+ "google-people",
115+ "google-photos-library",
116+ "google-photos-picker",
117+ "google-search-console",
118+] as const;
119+
120+const GOOGLE_FIRST_PARTY_ALLOWED_SCOPES: readonly string[] = [
121+ ...new Set([
122+ ...GOOGLE_FIRST_PARTY_PRESET_IDS.flatMap(googleCatalogOAuthScopesForPreset),
123+ // Connections created before the full-Gmail review retain this declared
124+ // scope on reconnect. New Gmail presets request `mail.google.com`.
125+ "https://www.googleapis.com/auth/gmail.modify",
126+ ]),
127+];
128+
129+// Executor-owned provider apps, enabled per provider by setting BOTH env vars
130+// (id + secret). Each provider-side registration must list
131+// `${VITE_PUBLIC_SITE_URL}/api/oauth/callback` as its callback; the org slug
132+// travels inside OAuth `state`, so the single static callback serves every org.
133+//
134+// The endpoint URLs default to the real provider; the `_AUTHORIZE_URL` /
135+// `_TOKEN_URL` overrides exist so tests and dev instances can point the app at
136+// an emulated provider (`@executor-js/emulate`) and run the complete flow.
137+// Production leaves them unset.
138+export const cloudFirstPartyOAuthClients = (): readonly FirstPartyOAuthClientConfig[] => [
139+ ...(env.FIRST_PARTY_GITHUB_CLIENT_ID && env.FIRST_PARTY_GITHUB_CLIENT_SECRET
140+ ? [
141+ {
142+ name: "github",
143+ authorizationUrl:
144+ env.FIRST_PARTY_GITHUB_AUTHORIZE_URL ?? "https://github.com/login/oauth/authorize",
145+ tokenUrl:
146+ env.FIRST_PARTY_GITHUB_TOKEN_URL ?? "https://github.com/login/oauth/access_token",
147+ clientId: env.FIRST_PARTY_GITHUB_CLIENT_ID,
148+ clientSecret: env.FIRST_PARTY_GITHUB_CLIENT_SECRET,
149+ integrations: [IntegrationSlug.make("github_rest")],
150+ // GitHub App user access tokens do not use classic OAuth scopes;
151+ // their capabilities come from the app's registered permissions.
152+ authorizationScopes: [],
153+ },
154+ ]
155+ : []),
156+ ...(env.FIRST_PARTY_GOOGLE_CLIENT_ID && env.FIRST_PARTY_GOOGLE_CLIENT_SECRET
157+ ? [
158+ {
159+ name: "google",
160+ authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
161+ tokenUrl: "https://oauth2.googleapis.com/token",
162+ clientId: env.FIRST_PARTY_GOOGLE_CLIENT_ID,
163+ clientSecret: env.FIRST_PARTY_GOOGLE_CLIENT_SECRET,
164+ allowedScopes: GOOGLE_FIRST_PARTY_ALLOWED_SCOPES,
165+ // Withdrawn from the connect picker: no new connection is offered the
166+ // Executor-owned Google app. The entry stays declared on purpose —
167+ // every connection already minted against it keeps refreshing and
168+ // reconnecting through it. Deleting this block, or unsetting the env
169+ // vars, would strand those connections instead.
170+ unlisted: true,
171+ },
172+ ]
173+ : []),
174+ ...(env.FIRST_PARTY_SLACK_CLIENT_ID && env.FIRST_PARTY_SLACK_CLIENT_SECRET
175+ ? [
176+ {
177+ name: "slack",
178+ authorizationUrl: "https://slack.com/oauth/v2_user/authorize",
179+ tokenUrl: "https://slack.com/api/oauth.v2.user.access",
180+ resource: "https://mcp.slack.com",
181+ clientId: env.FIRST_PARTY_SLACK_CLIENT_ID,
182+ clientSecret: env.FIRST_PARTY_SLACK_CLIENT_SECRET,
183+ integrations: [IntegrationSlug.make("slack")],
184+ allowedScopes: slackMcpUserScopes,
185+ },
186+ ]
187+ : []),
188+];
189+
82190 export const CloudHostConfig: Layer.Layer<HostConfig> = Layer.sync(HostConfig, () => ({
83191 // SSRF / private-network egress guard. Config-driven, NOT a test flag:
84192 // production leaves `ALLOW_LOCAL_NETWORK` unset so the guard stays ON (`false`);
@@ -90,6 +198,12 @@ export const CloudHostConfig: Layer.Layer<HostConfig> = Layer.sync(HostConfig, ( · 5 unmodified lines
90198 // WorkOS Vault is cloud's credential storage implementation detail, not a
91199 // user-selectable provider surface.
92200 exposeCredentialProviders: false,
201+ firstPartyOAuthClients: cloudFirstPartyOAuthClients(),
202+ // Enterprise-managed authorization ships behind a PostHog flag. Cloud is the
203+ // one host with a flag service, so cloud is the one host that installs a
204+ // gate; everywhere else the seam stays empty and the profile is attempted as
205+ // before. Gating happens at connect only — see the SDK contract.
206+ enterpriseManagedRollout: cloudEnterpriseManagedRollout(),
93207 }));
94208
95209 export const CloudCodeExecutorProvider: Layer.Layer<CodeExecutorProvider> = Layer.sync(
apps/cloud/src/engine/execution-usage.ts+8−2
@@ -12,14 +12,20 @@ export const withExecutionUsageTracking = <E extends Cause.YieldableError>( · 11 unmodified lines
1212 engine
1313 .execute(code, options)
1414 .pipe(Effect.tap(() => Effect.sync(() => trackUsage(organizationId)))),
15− executeWithPause: (code) =>
15+ // `options` MUST be forwarded: it carries `autoApprove`, and dropping it
16+ // silently turned every auto-approved execution back into a paused one.
17+ executeWithPause: (code, options) =>
1618 engine
17− .executeWithPause(code)
19+ .executeWithPause(code, options)
1820 .pipe(Effect.tap(() => Effect.sync(() => trackUsage(organizationId)))),
1921 // resume doesn't count as usage
2022 resume: (executionId, response) => engine.resume(executionId, response),
23+ isExecutionSettled: engine.isExecutionSettled,
2124 getPausedExecution: (executionId) => engine.getPausedExecution(executionId),
2225 pausedExecutionCount: () => engine.pausedExecutionCount(),
2326 hasPausedExecutions: () => engine.hasPausedExecutions(),
2427 getDescription: engine.getDescription,
28+ // Forwarded, not re-implemented: the wrapped engine owns the sandbox fibers,
29+ // so the host's request-scope teardown has to reach through this decorator.
30+ shutdown: engine.shutdown,
2531 });
apps/cloud/src/engine/first-party-oauth-clients.test.ts+87−0
@@ -0,0 +1,87 @@
1+import { env } from "cloudflare:workers";
2+import { beforeAll, describe, expect, it } from "@effect/vitest";
3+
4+import { cloudFirstPartyOAuthClients } from "./execution-stack";
5+
6+// The reviewed consumer scope boundary of the Executor-owned Google app.
7+//
8+// These assertions used to live in `e2e/scenarios/first-party-oauth.test.ts`,
9+// read off `listClients`. The app is now `unlisted`, so it has no read surface
10+// to introspect — the bundle is only observable on the config it is built from,
11+// which is here. The e2e still owns the BEHAVIOUR the boundary produces (which
12+// scopes an `oauth.start` requests, and that admin scopes are refused).
13+const GOOGLE_SCOPE = (suffix: string) => `https://www.googleapis.com/auth/${suffix}`;
14+
15+describe("cloud first-party oauth clients", () => {
16+ beforeAll(() => {
17+ env.FIRST_PARTY_GOOGLE_CLIENT_ID = "test-google-client";
18+ env.FIRST_PARTY_GOOGLE_CLIENT_SECRET = "test-google-secret";
19+ });
20+
21+ const google = () => cloudFirstPartyOAuthClients().find((client) => client.name === "google");
22+
23+ it("declares the Google app but withholds it from every listing", () => {
24+ const client = google();
25+ expect(client, "the env-declared first-party Google app is configured").toBeDefined();
26+ // The entry MUST stay declared: `loadClient` resolves it by slug for every
27+ // existing connection's refresh and reconnect. `unlisted` is what stops it
28+ // being offered for new connections.
29+ expect(client?.unlisted).toBe(true);
30+ });
31+
32+ it("covers the reviewed consumer bundle", () => {
33+ const allowed = google()?.allowedScopes;
34+ expect(allowed).toBeDefined();
35+ for (const scope of [
36+ "calendar",
37+ "meetings.space.readonly",
38+ "spreadsheets",
39+ "drive.file",
40+ "drive",
41+ "documents",
42+ "presentations",
43+ "forms.body",
44+ "forms.responses.readonly",
45+ "tasks",
46+ "contacts",
47+ "contacts.other.readonly",
48+ "directory.readonly",
49+ "user.addresses.read",
50+ "user.birthday.read",
51+ "user.emails.read",
52+ "user.gender.read",
53+ "user.organization.read",
54+ "user.phonenumbers.read",
55+ "photoslibrary.appendonly",
56+ "photoslibrary.edit.appcreateddata",
57+ "photospicker.mediaitems.readonly",
58+ "webmasters",
59+ "gmail.settings.basic",
60+ ]) {
61+ expect(allowed).toContain(GOOGLE_SCOPE(scope));
62+ }
63+ // `gmail.modify` stays in the host-enforced allowlist on purpose: a
64+ // connection created before the full-Gmail review still declares it, and
65+ // `resolveFirstPartyScopes` filters discovered scopes through this list, so

22 more lines aren’t shown here.

apps/cloud/src/env-augment.d.ts+35−0

This diff is large: its 56 lines aren’t shown here.

apps/cloud/src/extensions/billing/route.node.test.ts+9−14

This diff is large: its 51 lines aren’t shown here.

apps/cloud/src/extensions/billing/route.ts+5−2

This diff is large: its 18 lines aren’t shown here.

apps/cloud/src/extensions/billing/service.ts+95−10

This diff is large: its 149 lines aren’t shown here.

apps/cloud/src/extensions/docs.ts+7−1

This diff is large: its 20 lines aren’t shown here.

apps/cloud/src/extensions/routes.ts+18−1

This diff is large: its 45 lines aren’t shown here.

apps/cloud/src/mcp-session.e2e.node.test.ts+1−1

This diff is large: its 9 lines aren’t shown here.

apps/cloud/src/mcp/agent-handler.ts+130−11

This diff is large: its 213 lines aren’t shown here.

apps/cloud/src/mcp/auth-provider.test.ts+4−2

This diff is large: its 14 lines aren’t shown here.

apps/cloud/src/mcp/auth-provider.ts+23−13

This diff is large: its 66 lines aren’t shown here.

apps/cloud/src/mcp/auth.ts+32−4

This diff is large: its 75 lines aren’t shown here.

apps/cloud/src/mcp/session-durable-object.ts+95−53

This diff is large: its 237 lines aren’t shown here.

apps/cloud/src/mcp/session-meta.node.test.ts+167−0

This diff is large: its 168 lines aren’t shown here.

apps/cloud/src/mcp/session-meta.ts+209−0

This diff is large: its 210 lines aren’t shown here.

apps/cloud/src/mcp/telemetry.test.ts+160−0

This diff is large: its 161 lines aren’t shown here.

apps/cloud/src/mcp/telemetry.ts+51−3

This diff is large: its 95 lines aren’t shown here.

apps/cloud/src/mcp/traceparent.ts+29−0

This diff is large: its 30 lines aren’t shown here.

apps/cloud/src/observability/header-redaction.ts+19−0

This diff is large: its 20 lines aren’t shown here.

apps/cloud/src/observability/index.ts+195−2

This diff is large: its 253 lines aren’t shown here.

apps/cloud/src/observability/observability.test.ts+309−0

This diff is large: its 329 lines aren’t shown here.

apps/cloud/src/observability/telemetry.ts+52−11

This diff is large: its 91 lines aren’t shown here.

apps/cloud/src/org/auth-middleware.ts+5−1

This diff is large: its 13 lines aren’t shown here.

apps/cloud/src/org/handlers.test.ts+1−0

This diff is large: its 8 lines aren’t shown here.

apps/cloud/src/org/handlers.ts+5−0

This diff is large: its 12 lines aren’t shown here.

apps/cloud/src/quickjs-engine.wasm+0−0

Binary file not shown.

apps/cloud/src/quickjs.ts+35−0

This diff is large: its 36 lines aren’t shown here.

apps/cloud/src/routeTree.gen.ts+109−0

This diff is large: its 310 lines aren’t shown here.

apps/cloud/src/routes/__root.tsx+58−57

This diff is large: its 212 lines aren’t shown here.

apps/cloud/src/routes/app/api-keys.tsx+9−3

This diff is large: its 18 lines aren’t shown here.

apps/cloud/src/routes/app/billing_.plans.tsx+28−9

This diff is large: its 70 lines aren’t shown here.

apps/cloud/src/routes/bare/login.tsx+1−1

This diff is large: its 9 lines aren’t shown here.

apps/cloud/src/server.ts+289−71

This diff is large: its 450 lines aren’t shown here.

apps/cloud/src/start.ts+25−18

This diff is large: its 82 lines aren’t shown here.

apps/cloud/src/test-globalsetup-exit.node.test.ts+41−0

This diff is large: its 42 lines aren’t shown here.

apps/cloud/src/wasm.d.ts+6−0

This diff is large: its 7 lines aren’t shown here.

apps/cloud/src/web/auth.tsx+11−0

This diff is large: its 25 lines aren’t shown here.

apps/cloud/src/web/pages/login.tsx+3−1

This diff is large: its 11 lines aren’t shown here.

apps/cloud/src/web/shell.tsx+20−5

This diff is large: its 53 lines aren’t shown here.

apps/cloud/test-fixtures/test-globalsetup-exit/fixture.test.ts+7−0

This diff is large: its 8 lines aren’t shown here.

apps/cloud/test-fixtures/test-globalsetup-exit/vitest.config.ts+12−0

This diff is large: its 13 lines aren’t shown here.

apps/cloud/tsconfig.json+1−0

This diff is large: its 8 lines aren’t shown here.

apps/cloud/vite.config.ts+19−1

This diff is large: its 48 lines aren’t shown here.

apps/cloud/wrangler.jsonc+30−0

This diff is large: its 60 lines aren’t shown here.

apps/desktop/CHANGELOG.md+28−0

This diff is large: its 34 lines aren’t shown here.

apps/desktop/build/entitlements.mac.plist+0−21

This diff is large: its 22 lines aren’t shown here.

apps/desktop/build/icon.png+0−0

Binary file not shown.

apps/desktop/package.json+1−3

This diff is large: its 20 lines aren’t shown here.

apps/desktop/src/main/app-quit.ts+23−0

This diff is large: its 24 lines aren’t shown here.

apps/desktop/src/main/crash-fingerprint.test.ts+143−0

This diff is large: its 144 lines aren’t shown here.

apps/desktop/src/main/crash-fingerprint.ts+80−0

This diff is large: its 81 lines aren’t shown here.

apps/desktop/src/main/crash-screen.test.ts+26−0

This diff is large: its 27 lines aren’t shown here.

apps/desktop/src/main/diagnostics.ts+9−12

This diff is large: its 35 lines aren’t shown here.

apps/desktop/src/main/index.ts+51−6

This diff is large: its 100 lines aren’t shown here.

apps/desktop/src/main/navigation-errors.test.ts+40−0

This diff is large: its 41 lines aren’t shown here.

apps/desktop/src/main/navigation-errors.ts+21−0

This diff is large: its 22 lines aren’t shown here.

apps/desktop/src/main/settings-permissions.test.ts+92−0

This diff is large: its 93 lines aren’t shown here.

apps/desktop/src/main/settings.ts+10−0

This diff is large: its 17 lines aren’t shown here.

apps/desktop/src/main/sidecar-exit-wiring.test.ts+163−0

This diff is large: its 164 lines aren’t shown here.

apps/desktop/src/main/sidecar-exit.test.ts+70−0

This diff is large: its 71 lines aren’t shown here.

apps/desktop/src/main/sidecar-exit.ts+50−0

This diff is large: its 51 lines aren’t shown here.

apps/desktop/src/main/sidecar.ts+31−8

This diff is large: its 65 lines aren’t shown here.

apps/desktop/src/sidecar/native-bindings.ts+18−5

This diff is large: its 34 lines aren’t shown here.

apps/docs/concepts/integrations.mdx+1−1

This diff is large: its 9 lines aren’t shown here.

apps/docs/docs.json+1−1

This diff is large: its 9 lines aren’t shown here.

apps/docs/hosted/docker.mdx+3−0

This diff is large: its 10 lines aren’t shown here.

apps/docs/hosted/tracing.mdx+115−0

This diff is large: its 116 lines aren’t shown here.

apps/docs/local/cli.mdx+3−3

This diff is large: its 24 lines aren’t shown here.

apps/host-cloudflare/README.md+13−9

This diff is large: its 42 lines aren’t shown here.

apps/host-cloudflare/executor.config.ts+12−5

This diff is large: its 30 lines aren’t shown here.

apps/host-cloudflare/package.json+6−4

This diff is large: its 37 lines aren’t shown here.

apps/host-cloudflare/scripts/assert-shell-asset.mjs+17−0

This diff is large: its 18 lines aren’t shown here.

apps/host-cloudflare/scripts/deploy.sh+11−7

This diff is large: its 38 lines aren’t shown here.

apps/host-cloudflare/src/account/account-provider.ts+5−0

This diff is large: its 12 lines aren’t shown here.

apps/host-cloudflare/src/auth/cloudflare-access.ts+6−1

This diff is large: its 33 lines aren’t shown here.

apps/host-cloudflare/src/config.test.ts+74−0

This diff is large: its 75 lines aren’t shown here.

apps/host-cloudflare/src/config.ts+48−5

This diff is large: its 101 lines aren’t shown here.

apps/host-cloudflare/src/db/d1.test.ts+207−0

This diff is large: its 208 lines aren’t shown here.

apps/host-cloudflare/src/db/d1.ts+84−5

This diff is large: its 110 lines aren’t shown here.

apps/host-cloudflare/src/db/data-migrations.test.ts+60−5

This diff is large: its 103 lines aren’t shown here.

apps/host-cloudflare/src/db/data-migrations.ts+101−7

This diff is large: its 156 lines aren’t shown here.

apps/host-cloudflare/src/execution.ts+10−5

This diff is large: its 37 lines aren’t shown here.

apps/host-cloudflare/src/mcp/agent-handler.ts+4−0

This diff is large: its 19 lines aren’t shown here.

apps/host-cloudflare/src/mcp/session-durable-object.ts+44−3

This diff is large: its 99 lines aren’t shown here.

apps/host-cloudflare/src/plugins.ts+12−5

This diff is large: its 29 lines aren’t shown here.

apps/host-cloudflare/src/worker.e2e.node.test.ts+127−13

This diff is large: its 188 lines aren’t shown here.

apps/host-cloudflare/src/worker.ts+19−1

This diff is large: its 34 lines aren’t shown here.

apps/host-cloudflare/test-stubs/cloudflare-workers.ts+7−0

This diff is large: its 8 lines aren’t shown here.

apps/host-cloudflare/tsconfig.json+4−0

This diff is large: its 11 lines aren’t shown here.

apps/host-cloudflare/tsr.routes.ts+5−1

This diff is large: its 12 lines aren’t shown here.

apps/host-cloudflare/vite.config.ts+8−1

This diff is large: its 27 lines aren’t shown here.

apps/host-cloudflare/vitest.config.ts+7−0

This diff is large: its 14 lines aren’t shown here.

apps/host-cloudflare/web/routeTree.gen.ts+86−0

This diff is large: its 229 lines aren’t shown here.

apps/host-cloudflare/web/routes/__root.tsx+15−1

This diff is large: its 37 lines aren’t shown here.

apps/host-cloudflare/wrangler.jsonc+22−7

This diff is large: its 61 lines aren’t shown here.

apps/host-selfhost/.env.example+6−0

This diff is large: its 10 lines aren’t shown here.

apps/host-selfhost/CHANGELOG.md+290−0

This diff is large: its 296 lines aren’t shown here.

apps/host-selfhost/Dockerfile+1−1

This diff is large: its 7 lines aren’t shown here.

apps/host-selfhost/executor.config.ts+22−14

This diff is large: its 58 lines aren’t shown here.

apps/host-selfhost/package.json+9−5

This diff is large: its 53 lines aren’t shown here.

apps/host-selfhost/scripts/assert-shell-asset.mjs+18−0

This diff is large: its 19 lines aren’t shown here.

apps/host-selfhost/scripts/package-runtime.ts+55−1

This diff is large: its 90 lines aren’t shown here.

apps/host-selfhost/src/account/better-auth-account-provider.ts+27−0

This diff is large: its 34 lines aren’t shown here.

apps/host-selfhost/src/admin/admin-escalation.node.test.ts+184−0

This diff is large: its 185 lines aren’t shown here.

apps/host-selfhost/src/admin/admin-users-api.ts+225−0

This diff is large: its 226 lines aren’t shown here.

apps/host-selfhost/src/admin/admin-users.node.test.ts+234−0

This diff is large: its 235 lines aren’t shown here.

apps/host-selfhost/src/admin/handlers.ts+14−14

This diff is large: its 50 lines aren’t shown here.

apps/host-selfhost/src/admin/require-admin.ts+126−0

This diff is large: its 127 lines aren’t shown here.

apps/host-selfhost/src/analytics.ts+83−0

This diff is large: its 84 lines aren’t shown here.

apps/host-selfhost/src/app.ts+28−4

This diff is large: its 70 lines aren’t shown here.

apps/host-selfhost/src/auth/better-auth.ts+20−1

This diff is large: its 28 lines aren’t shown here.

apps/host-selfhost/src/auth/identity.ts+1−0

This diff is large: its 8 lines aren’t shown here.

apps/host-selfhost/src/auth/return-to.test.ts+45−1

This diff is large: its 59 lines aren’t shown here.

apps/host-selfhost/src/auth/return-to.ts+29−0

This diff is large: its 33 lines aren’t shown here.

apps/host-selfhost/src/config.ts+25−0

This diff is large: its 42 lines aren’t shown here.

apps/host-selfhost/src/db/data-migrations.ts+11−1

This diff is large: its 29 lines aren’t shown here.

apps/host-selfhost/src/execution.ts+20−9

This diff is large: its 79 lines aren’t shown here.

apps/host-selfhost/src/executor-config.test.ts+59−0

This diff is large: its 60 lines aren’t shown here.

apps/host-selfhost/src/integrations-mcp.test.tsrenamed from apps/host-selfhost/src/sources-mcp.test.ts+2−2

This diff is large: its 18 lines aren’t shown here.

apps/host-selfhost/src/integrations.test.tsrenamed from apps/host-selfhost/src/sources.test.ts+0−0

File renamed without changes.

apps/host-selfhost/src/mcp/auth.ts+7−2

This diff is large: its 28 lines aren’t shown here.

apps/host-selfhost/src/mcp/mcp.test.ts+1−0

This diff is large: its 8 lines aren’t shown here.

apps/host-selfhost/src/mcp/org-path.test.ts+18−1

This diff is large: its 29 lines aren’t shown here.

apps/host-selfhost/src/mcp/org-path.ts+12−0

This diff is large: its 19 lines aren’t shown here.

apps/host-selfhost/src/mcp/session-store.ts+11−0

This diff is large: its 32 lines aren’t shown here.

apps/host-selfhost/src/platform-credential.test.ts+210−0

This diff is large: its 211 lines aren’t shown here.

apps/host-selfhost/src/serve.ts+37−2

This diff is large: its 75 lines aren’t shown here.

apps/host-selfhost/src/subject-sightings.test.ts+82−0

This diff is large: its 83 lines aren’t shown here.

apps/host-selfhost/src/telemetry.test.ts+142−0

This diff is large: its 143 lines aren’t shown here.

apps/host-selfhost/src/telemetry.ts+122−0

This diff is large: its 123 lines aren’t shown here.

apps/host-selfhost/src/testing/test-app.ts+43−2

This diff is large: its 111 lines aren’t shown here.

apps/host-selfhost/tsconfig.json+4−0

This diff is large: its 11 lines aren’t shown here.

apps/host-selfhost/vite.config.ts+7−0

This diff is large: its 21 lines aren’t shown here.

apps/host-selfhost/web/login.tsx+5−10

This diff is large: its 29 lines aren’t shown here.

apps/host-selfhost/web/routeTree.gen.ts+109−0

This diff is large: its 275 lines aren’t shown here.

apps/host-selfhost/web/routes/__root.tsx+25−2

This diff is large: its 74 lines aren’t shown here.

apps/local/CHANGELOG.md+110−0

This diff is large: its 112 lines aren’t shown here.

apps/local/drizzle/0004_cultured_shadowcat.sql+1−0

This diff is large: its 2 lines aren’t shown here.

apps/local/drizzle/meta/0004_snapshot.json+927−0

This diff is large: its 928 lines aren’t shown here.

apps/local/drizzle/meta/_journal.json+7−0

This diff is large: its 14 lines aren’t shown here.

apps/local/executor.config.ts+18−6

This diff is large: its 41 lines aren’t shown here.

apps/local/package.json+5−4

This diff is large: its 43 lines aren’t shown here.

apps/local/src/analytics.ts+68−0

This diff is large: its 69 lines aren’t shown here.

apps/local/src/app.ts+23−6

This diff is large: its 58 lines aren’t shown here.

apps/local/src/auth-tool-failures.test.ts+2−2

This diff is large: its 18 lines aren’t shown here.

apps/local/src/db/data-migrations.ts+7−1

This diff is large: its 28 lines aren’t shown here.

apps/local/src/db/db-file-permissions.test.ts+99−0

This diff is large: its 100 lines aren’t shown here.

apps/local/src/db/executor-schema.ts+1−0

This diff is large: its 8 lines aren’t shown here.

apps/local/src/db/libsql.ts+38−0

This diff is large: its 57 lines aren’t shown here.

apps/local/src/db/sqlite-fumadb.ts+6−0

This diff is large: its 13 lines aren’t shown here.

apps/local/src/executor.ts+48−18

This diff is large: its 117 lines aren’t shown here.

apps/local/src/identity.ts+1−0

This diff is large: its 8 lines aren’t shown here.

apps/local/src/installation.ts+5−4

This diff is large: its 17 lines aren’t shown here.

apps/local/src/main.ts+72−12

This diff is large: its 105 lines aren’t shown here.

apps/local/src/mcp-browser-resume.test.ts+1−1

This diff is large: its 9 lines aren’t shown here.

apps/local/src/mcp.ts+13−1

This diff is large: its 37 lines aren’t shown here.

apps/local/src/serve.test.ts+1−1

This diff is large: its 9 lines aren’t shown here.

apps/local/src/serve.ts+42−11

This diff is large: its 113 lines aren’t shown here.

apps/marketing/src/assets/yc-backed-by.svg+1−15

This diff is large: its 25 lines aren’t shown here.

apps/marketing/src/components/self-host-contact-modal.tsx+0−126

This diff is large: its 127 lines aren’t shown here.

apps/marketing/src/layouts/Layout.astro+11−4

This diff is large: its 29 lines aren’t shown here.

apps/marketing/src/pages/about-executor.astro+158−0

This diff is large: its 159 lines aren’t shown here.

apps/marketing/src/pages/google-oauth.astro+554−0

This diff is large: its 555 lines aren’t shown here.

apps/marketing/src/pages/google-workspace.astro+218−0

This diff is large: its 219 lines aren’t shown here.

apps/marketing/src/pages/index.astro+8−4

This diff is large: its 44 lines aren’t shown here.

apps/marketing/src/pages/privacy.astro+35−2

This diff is large: its 58 lines aren’t shown here.

assets/executor-demo.gif+0−0

Binary file not shown.

autumn.config.ts+2−2

This diff is large: its 18 lines aren’t shown here.

bun.lock+363−215

This diff is large: its 1477 lines aren’t shown here.

design.md+6−6

This diff is large: its 30 lines aren’t shown here.

e2e/AGENTS.md+42−280

This diff is large: its 323 lines aren’t shown here.

e2e/CHANGELOG.md+168−0

This diff is large: its 174 lines aren’t shown here.

e2e/RUNNING.md+283−0

This diff is large: its 284 lines aren’t shown here.

e2e/cli/service-lifecycle.test.ts+2−2

This diff is large: its 18 lines aren’t shown here.

e2e/cloud/admin-users-console.test.ts+390−0

This diff is large: its 391 lines aren’t shown here.

e2e/cloud/admin-users.test.ts+322−0

This diff is large: its 323 lines aren’t shown here.

e2e/cloud/auth-hint.test.ts+21−3

This diff is large: its 31 lines aren’t shown here.

e2e/cloud/auth-routing-flow.test.ts+3−2

This diff is large: its 26 lines aren’t shown here.

e2e/cloud/auth-session.test.ts+21−3

This diff is large: its 40 lines aren’t shown here.

e2e/cloud/billing-customer-provisioning.test.ts+281−0

This diff is large: its 282 lines aren’t shown here.

e2e/cloud/billing-trial-checkout-stale.test.ts+3−2

This diff is large: its 23 lines aren’t shown here.

e2e/cloud/cli-device-login.test.ts+3−3

This diff is large: its 24 lines aren’t shown here.

e2e/cloud/connect-card-origin.test.ts+55−0

This diff is large: its 56 lines aren’t shown here.

e2e/cloud/connect-card-ssr-origin.test.ts+0−79

This diff is large: its 80 lines aren’t shown here.

e2e/cloud/connect-link-multi-org.test.ts+232−0

This diff is large: its 233 lines aren’t shown here.

e2e/cloud/connect-panel.test.ts+4−3

This diff is large: its 35 lines aren’t shown here.

e2e/cloud/connection-modal-oauth-abandon.test.ts+3−2

This diff is large: its 26 lines aren’t shown here.

e2e/cloud/connection-owner-isolation.test.ts+37−15

This diff is large: its 110 lines aren’t shown here.

e2e/cloud/credential-write-durability.test.ts+570−0

This diff is large: its 571 lines aren’t shown here.

e2e/cloud/frontend-error-reporting.test.ts+132−0

This diff is large: its 133 lines aren’t shown here.

e2e/cloud/integrations-api.test.tsrenamed from e2e/cloud/sources-api.test.ts+0−0

File renamed without changes.

e2e/cloud/integrations-refresh.test.tsrenamed from e2e/cloud/sources-refresh.test.ts+0−0

File renamed without changes.

e2e/cloud/logout-stale-session.test.ts+166−0

This diff is large: its 167 lines aren’t shown here.

e2e/cloud/mcp-browser-approval-org-scope.test.ts+4−3

This diff is large: its 33 lines aren’t shown here.

e2e/cloud/mcp-browser-resume-page.test.ts+64−57

This diff is large: its 133 lines aren’t shown here.

e2e/cloud/mcp-destroyed-session-envelope.test.ts+365−0

This diff is large: its 366 lines aren’t shown here.

e2e/cloud/mcp-execution-limits.test.ts+179−2

This diff is large: its 201 lines aren’t shown here.

e2e/cloud/mcp-oversize-response.test.ts+136−0

This diff is large: its 137 lines aren’t shown here.

e2e/cloud/mcp-session-cold-init.test.ts+191−0

This diff is large: its 192 lines aren’t shown here.

e2e/cloud/mcp-session-idle-runtime-disposal.test.ts+252−0

This diff is large: its 253 lines aren’t shown here.

e2e/cloud/mcp-telemetry-contract.test.ts+106−0

This diff is large: its 107 lines aren’t shown here.

e2e/cloud/member-invite-seat-limit.test.ts+3−2

This diff is large: its 26 lines aren’t shown here.

e2e/cloud/oauth-callback-org-scope.test.ts+5−6

This diff is large: its 44 lines aren’t shown here.

e2e/cloud/oauth-callback-unauthenticated.test.ts+1−1

This diff is large: its 9 lines aren’t shown here.

e2e/cloud/onboarding-mcp-url.test.ts+2−1

This diff is large: its 17 lines aren’t shown here.

e2e/cloud/org-api-keys-console.test.ts+142−0

This diff is large: its 143 lines aren’t shown here.

e2e/cloud/org-delete.test.ts+4−3

This diff is large: its 33 lines aren’t shown here.

e2e/cloud/org-last-visited.test.ts+96−0

This diff is large: its 97 lines aren’t shown here.

e2e/cloud/org-limit.test.ts+3−2

This diff is large: its 26 lines aren’t shown here.

e2e/cloud/org-multitab-cookie.test.ts+4−3

This diff is large: its 35 lines aren’t shown here.

e2e/cloud/org-slug-foreign.test.ts+3−2

This diff is large: its 21 lines aren’t shown here.

e2e/cloud/org-switcher.test.ts+4−3

This diff is large: its 35 lines aren’t shown here.

e2e/cloud/repro-listener-supersede.test.ts+130−0

This diff is large: its 131 lines aren’t shown here.

e2e/cloud/spec-update-convergence.test.ts+20−6

This diff is large: its 63 lines aren’t shown here.

e2e/cloud/storage-error-report-shape.test.ts+280−0

This diff is large: its 281 lines aren’t shown here.

e2e/cloud/support/session.ts+205−0

This diff is large: its 206 lines aren’t shown here.

e2e/cloud/surface-reachability.test.ts+21−0

This diff is large: its 28 lines aren’t shown here.

e2e/cloud/toolkit-opencode-real.test.ts+3−3

This diff is large: its 18 lines aren’t shown here.

e2e/cloud/toolkit-policy-perf.test.ts+8−8

This diff is large: its 52 lines aren’t shown here.

e2e/cloud/unauthenticated-skeleton.test.ts+14−5

This diff is large: its 47 lines aren’t shown here.

e2e/cloudflare/mcp-native-elicitation.test.ts+108−0

This diff is large: its 109 lines aren’t shown here.

e2e/local/auth.test.ts+6−4

This diff is large: its 24 lines aren’t shown here.

e2e/local/boot-process.test.ts+101−0

This diff is large: its 102 lines aren’t shown here.

e2e/local/cli-call-logged-out.test.ts+160−0

This diff is large: its 161 lines aren’t shown here.

e2e/local/cli-mcp-daemon-attach-stress.test.ts+52−62

This diff is large: its 188 lines aren’t shown here.

e2e/local/cli-telemetry-optout.test.ts+27−0

This diff is large: its 28 lines aren’t shown here.

e2e/local/daemon-process.ts+55−0

This diff is large: its 56 lines aren’t shown here.

e2e/local/local-server.ts+25−5

This diff is large: its 62 lines aren’t shown here.

e2e/local/mcp-native-elicitation.test.ts+128−0

This diff is large: its 129 lines aren’t shown here.

e2e/local/oauth-token-durability.test.ts+252−0

This diff is large: its 253 lines aren’t shown here.

e2e/local/server-profile-header-env.test.ts+109−0

This diff is large: its 110 lines aren’t shown here.

e2e/local/stdio-mcp.test.ts+30−0

This diff is large: its 37 lines aren’t shown here.

e2e/local/update-notice.test.ts+1−1

This diff is large: its 9 lines aren’t shown here.

e2e/local/vite-dev-routing.test.ts+3−2

This diff is large: its 14 lines aren’t shown here.

e2e/mcp-apps/.gitignore+5−0

This diff is large: its 6 lines aren’t shown here.

e2e/mcp-apps/README.md+67−0

This diff is large: its 68 lines aren’t shown here.

e2e/mcp-apps/package.json+17−0

This diff is large: its 18 lines aren’t shown here.

e2e/mcp-apps/playwright.config.ts+31−0

This diff is large: its 32 lines aren’t shown here.

e2e/mcp-apps/scripts/check-sunpeak.mjs+45−0

This diff is large: its 46 lines aren’t shown here.

e2e/mcp-apps/tests/create-artifact.spec.ts+87−0

This diff is large: its 88 lines aren’t shown here.

e2e/package.json+3−4

This diff is large: its 30 lines aren’t shown here.

e2e/recordings/custom-tools-console.mp4+0−0

Binary file not shown.

e2e/scenarios/api-tools.test.ts+1−1

This diff is large: its 9 lines aren’t shown here.

e2e/scenarios/artifact-approval.test.ts+228−0

This diff is large: its 229 lines aren’t shown here.

e2e/scenarios/artifact-loading-surface.test.ts+331−0

This diff is large: its 332 lines aren’t shown here.

e2e/scenarios/artifact-preview-gallery.test.ts+282−0

This diff is large: its 283 lines aren’t shown here.

e2e/scenarios/artifacts.test.ts+854−0

This diff is large: its 855 lines aren’t shown here.

e2e/scenarios/auth-methods-ui.test.ts+2−3

This diff is large: its 19 lines aren’t shown here.

e2e/scenarios/auth-methods.test.ts+4−4

This diff is large: its 36 lines aren’t shown here.

e2e/scenarios/browser-approval.test.ts+2−1

This diff is large: its 17 lines aren’t shown here.

e2e/scenarios/connect-deep-link.test.ts+169−0

This diff is large: its 170 lines aren’t shown here.

e2e/scenarios/connect-handoff-session.test.ts+26−23

This diff is large: its 110 lines aren’t shown here.

e2e/scenarios/connect-handoff.test.ts+37−24

This diff is large: its 141 lines aren’t shown here.

e2e/scenarios/connection-health-verdict.test.ts+531−0

This diff is large: its 532 lines aren’t shown here.

e2e/scenarios/connection-remove-confirm.test.ts+122−0

This diff is large: its 123 lines aren’t shown here.

e2e/scenarios/docs-link.test.ts+2−1

This diff is large: its 17 lines aren’t shown here.

e2e/scenarios/first-party-oauth.test.ts+382−0

This diff is large: its 383 lines aren’t shown here.

e2e/scenarios/google-health-checks.test.ts+425−0

This diff is large: its 426 lines aren’t shown here.

e2e/scenarios/google-photos-preset-ui.test.ts+22−21

This diff is large: its 63 lines aren’t shown here.

e2e/scenarios/graphql-introspection-health.test.ts+146−0

This diff is large: its 147 lines aren’t shown here.

e2e/scenarios/health-checks-ui.test.ts+12−11

This diff is large: its 107 lines aren’t shown here.

e2e/scenarios/integrations-catalog-search.test.ts+77−0

This diff is large: its 78 lines aren’t shown here.

e2e/scenarios/mcp-catalog-sync-ui.test.ts+3−4

This diff is large: its 28 lines aren’t shown here.

e2e/scenarios/mcp-cloudflare-codemode-warning.test.ts+41−0

This diff is large: its 42 lines aren’t shown here.

e2e/scenarios/mcp-session-state.test.ts+157−0

This diff is large: its 158 lines aren’t shown here.

e2e/scenarios/microsoft-emulator.test.ts+25−22

This diff is large: its 96 lines aren’t shown here.

e2e/scenarios/microsoft-graph-default.test.ts+65−59

This diff is large: its 199 lines aren’t shown here.

e2e/scenarios/microsoft-graph-full.test.ts+56−48

This diff is large: its 165 lines aren’t shown here.

e2e/scenarios/namespace-enumeration.test.ts+167−0

This diff is large: its 168 lines aren’t shown here.

e2e/scenarios/namespace-search-tools.test.ts+146−0

This diff is large: its 147 lines aren’t shown here.

e2e/scenarios/oauth-callback-url.test.ts+2−3

This diff is large: its 19 lines aren’t shown here.

e2e/scenarios/oauth-client-handoff.test.ts+31−28

This diff is large: its 126 lines aren’t shown here.

e2e/scenarios/oauth-refresh-on-401.test.ts+326−0

This diff is large: its 327 lines aren’t shown here.

e2e/scenarios/oauth-refresh-rejected-non-json.test.ts+503−0

This diff is large: its 504 lines aren’t shown here.

e2e/scenarios/oauth-refresh-rejected.test.ts+291−0

This diff is large: its 292 lines aren’t shown here.

e2e/scenarios/oauth-scope-insufficient.test.ts+336−0

This diff is large: its 337 lines aren’t shown here.

e2e/scenarios/oauth-second-connection.test.ts+233−0

This diff is large: its 234 lines aren’t shown here.

e2e/scenarios/openapi-add-integration-action-bar.test.ts+6−5

This diff is large: its 43 lines aren’t shown here.

e2e/scenarios/openapi-integration.test.tsrenamed from e2e/scenarios/openapi-source.test.ts+0−0

File renamed without changes.

e2e/scenarios/openapi-multi-key-auth-ui.test.ts+2−1

This diff is large: its 17 lines aren’t shown here.

e2e/scenarios/openapi-ndjson-output.test.ts+250−0

This diff is large: its 251 lines aren’t shown here.

e2e/scenarios/openapi-server-selection-ui.test.ts+4−3

This diff is large: its 26 lines aren’t shown here.

e2e/scenarios/openapi-unknown-args.test.ts+162−0

This diff is large: its 163 lines aren’t shown here.

e2e/scenarios/org-slug-routing.test.ts+5−4

This diff is large: its 36 lines aren’t shown here.

e2e/scenarios/policies-ui.test.ts+61−5

This diff is large: its 103 lines aren’t shown here.

e2e/scenarios/provider-plugins-ui.test.ts+37−42

This diff is large: its 109 lines aren’t shown here.

e2e/scenarios/resume-after-sandbox-deadline.test.ts+165−0

This diff is large: its 166 lines aren’t shown here.

e2e/scenarios/run-panel-auto-approve.test.ts+8−21

This diff is large: its 38 lines aren’t shown here.

e2e/scenarios/shape-memory.test.ts+178−0

This diff is large: its 179 lines aren’t shown here.

e2e/scenarios/support/large-catalog.ts+29−29

This diff is large: its 137 lines aren’t shown here.

e2e/scenarios/tool-descriptions.test.ts+2−2

This diff is large: its 18 lines aren’t shown here.

e2e/scenarios/toolkits-mcp.test.ts+67−0

This diff is large: its 74 lines aren’t shown here.

e2e/scripts/ci-shard-durations.ts+94−0

This diff is large: its 95 lines aren’t shown here.

e2e/scripts/ci-shard.test.ts+126−0

This diff is large: its 127 lines aren’t shown here.

e2e/scripts/ci-shard.ts+231−0

This diff is large: its 232 lines aren’t shown here.

e2e/scripts/ci-shard.vitest.config.ts+9−0

This diff is large: its 10 lines aren’t shown here.

e2e/scripts/cli.ts+5−0

This diff is large: its 12 lines aren’t shown here.

e2e/scripts/record-connect-card.ts+2−1

This diff is large: its 17 lines aren’t shown here.

e2e/scripts/run-ci-shard.ts+37−0

This diff is large: its 38 lines aren’t shown here.

e2e/selfhost/admin-users-console.test.ts+331−0

This diff is large: its 332 lines aren’t shown here.

e2e/selfhost/admin-users-pager.test.ts+167−0

This diff is large: its 168 lines aren’t shown here.

e2e/selfhost/api-keys-feedback.test.ts+4−3

This diff is large: its 35 lines aren’t shown here.

e2e/selfhost/auth-methods-ui.test.ts+5−12

This diff is large: its 52 lines aren’t shown here.

e2e/selfhost/cli-device-login.test.ts+3−3

This diff is large: its 27 lines aren’t shown here.

e2e/selfhost/connect-modal-credential-ux.test.ts+46−2

This diff is large: its 82 lines aren’t shown here.

e2e/selfhost/detected-auth-immutable-ui.test.ts+3−4

This diff is large: its 28 lines aren’t shown here.

e2e/selfhost/mcp-auth-required-add.test.ts+2−3

This diff is large: its 19 lines aren’t shown here.

e2e/selfhost/mcp-browser-approval-ownership.test.ts+4−45

This diff is large: its 77 lines aren’t shown here.

e2e/selfhost/mcp-connect-card-url.test.ts+2−1

This diff is large: its 17 lines aren’t shown here.

e2e/selfhost/mcp-enterprise-managed-auth.test.ts+443−0

This diff is large: its 444 lines aren’t shown here.

e2e/selfhost/mcp-oauth-cimd-connect.test.ts+109−0

This diff is large: its 110 lines aren’t shown here.

e2e/selfhost/mcp-oauth-consent.test.ts+3−2

This diff is large: its 26 lines aren’t shown here.

e2e/selfhost/mcp-oauth-reconnect-health.test.ts+222−19

This diff is large: its 306 lines aren’t shown here.

e2e/selfhost/mcp-oauth-slow-connect.test.ts+133−0

This diff is large: its 134 lines aren’t shown here.

e2e/selfhost/mcp-opencode-consent.test.ts+2−1

This diff is large: its 17 lines aren’t shown here.

e2e/selfhost/oauth-app-modal.test.ts+29−1

This diff is large: its 51 lines aren’t shown here.

e2e/selfhost/oauth-callback-unauthenticated.test.ts+2−1

This diff is large: its 17 lines aren’t shown here.

e2e/selfhost/oauth-org-connection-cross-principal.test.ts+275−0

This diff is large: its 276 lines aren’t shown here.

e2e/selfhost/posthog-mcp-oauth.test.ts+45−21

This diff is large: its 115 lines aren’t shown here.

e2e/selfhost/toolkits-ui.test.ts+6−1

This diff is large: its 16 lines aren’t shown here.

e2e/setup/boot.ts+122−13

This diff is large: its 188 lines aren’t shown here.

e2e/setup/cloud.boot.ts+41−4

This diff is large: its 81 lines aren’t shown here.

e2e/setup/cloud.globalsetup.ts+16−3

This diff is large: its 41 lines aren’t shown here.

e2e/setup/cloudflare.boot.ts+7−2

This diff is large: its 23 lines aren’t shown here.

e2e/setup/desktop-linux.globalsetup.ts+1−0

This diff is large: its 8 lines aren’t shown here.

e2e/setup/desktop-macos.globalsetup.ts+1−0

This diff is large: its 8 lines aren’t shown here.

e2e/setup/execution-limits.ts+15−0

This diff is large: its 19 lines aren’t shown here.

e2e/setup/mcp-session-timeouts.ts+3−1

This diff is large: its 9 lines aren’t shown here.

e2e/setup/motel.ts+3−3

This diff is large: its 27 lines aren’t shown here.

e2e/setup/sandbox-timeout.ts+28−0

This diff is large: its 29 lines aren’t shown here.

e2e/setup/selfhost.boot.ts+10−2

This diff is large: its 40 lines aren’t shown here.

e2e/setup/selfhost.globalsetup.ts+8−2

This diff is large: its 34 lines aren’t shown here.

e2e/src/emulator-instance.ts+71−0

This diff is large: its 72 lines aren’t shown here.

e2e/src/ports.ts+31−7

This diff is large: its 78 lines aren’t shown here.

e2e/src/surfaces/autumn.ts+80−6

This diff is large: its 136 lines aren’t shown here.

e2e/src/surfaces/browser.ts+123−1

This diff is large: its 138 lines aren’t shown here.

e2e/src/surfaces/cli.ts+10−1

This diff is large: its 25 lines aren’t shown here.

e2e/src/surfaces/mcp.ts+24−7

This diff is large: its 45 lines aren’t shown here.

e2e/src/update-card-render.ts+2−1

This diff is large: its 17 lines aren’t shown here.

e2e/targets/cloud.ts+9−1

This diff is large: its 28 lines aren’t shown here.

e2e/targets/selfhost-docker.ts+7−6

This diff is large: its 32 lines aren’t shown here.

e2e/targets/selfhost.ts+50−1

This diff is large: its 72 lines aren’t shown here.

examples/all-plugins/CHANGELOG.md+196−0

This diff is large: its 202 lines aren’t shown here.

examples/all-plugins/package.json+1−2

This diff is large: its 16 lines aren’t shown here.

examples/all-plugins/src/main.ts+13−4

This diff is large: its 45 lines aren’t shown here.

examples/docs-sdk-quickstart/CHANGELOG.md+112−0

This diff is large: its 118 lines aren’t shown here.

examples/docs-sdk-quickstart/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

knip.config.ts+0−3

This diff is large: its 10 lines aren’t shown here.

package.json+5−2

This diff is large: its 34 lines aren’t shown here.

packages/app/package.json+6−1

This diff is large: its 33 lines aren’t shown here.

packages/app/src/crash-reporting.test.ts+115−0

This diff is large: its 116 lines aren’t shown here.

packages/app/src/crash-reporting.ts+35−3

This diff is large: its 60 lines aren’t shown here.

packages/app/src/entry-client.tsx+5−0

This diff is large: its 15 lines aren’t shown here.

packages/app/src/preload-error.test.ts+132−0

This diff is large: its 133 lines aren’t shown here.

packages/app/src/preload-error.ts+90−0

This diff is large: its 91 lines aren’t shown here.

packages/app/src/routeTree.gen.ts+81−0

This diff is large: its 224 lines aren’t shown here.

packages/app/src/routes/__root.tsx+15−4

This diff is large: its 37 lines aren’t shown here.

packages/app/src/server-disconnected.test.ts+184−0

This diff is large: its 185 lines aren’t shown here.

packages/app/src/server-disconnected.ts+113−0

This diff is large: its 114 lines aren’t shown here.

packages/app/src/web/shell.tsx+22−7

This diff is large: its 83 lines aren’t shown here.

packages/app/tsr.routes.ts+6−4

This diff is large: its 20 lines aren’t shown here.

packages/app/vite.ts+8−1

This diff is large: its 30 lines aren’t shown here.

packages/app/vitest.config.ts+8−0

This diff is large: its 9 lines aren’t shown here.

packages/core/analytics/CHANGELOG.md+52−0

This diff is large: its 53 lines aren’t shown here.

packages/core/analytics/package.json+51−0

This diff is large: its 52 lines aren’t shown here.

packages/core/analytics/src/analytics.test.ts+233−0

This diff is large: its 234 lines aren’t shown here.

packages/core/analytics/src/analytics.ts+240−0

This diff is large: its 241 lines aren’t shown here.

packages/core/analytics/src/engine.test.ts+159−0

This diff is large: its 160 lines aren’t shown here.

packages/core/analytics/src/engine.ts+63−0

This diff is large: its 64 lines aren’t shown here.

packages/core/analytics/src/events.ts+71−0

This diff is large: its 72 lines aren’t shown here.

packages/core/analytics/src/index.ts+12−0

This diff is large: its 13 lines aren’t shown here.

packages/core/analytics/tsconfig.json+24−0

This diff is large: its 25 lines aren’t shown here.

packages/core/analytics/tsup.config.ts+12−0

This diff is large: its 13 lines aren’t shown here.

packages/core/analytics/vitest.config.ts+7−0

This diff is large: its 8 lines aren’t shown here.

packages/core/api/CHANGELOG.md+128−0

This diff is large: its 134 lines aren’t shown here.

packages/core/api/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

packages/core/api/src/account/api.ts+37−0

This diff is large: its 51 lines aren’t shown here.

packages/core/api/src/account/handlers.ts+18−0

This diff is large: its 25 lines aren’t shown here.

packages/core/api/src/account/org-slug.test.ts+28−1

This diff is large: its 36 lines aren’t shown here.

packages/core/api/src/account/org-slug.ts+7−2

This diff is large: its 27 lines aren’t shown here.

packages/core/api/src/account/service.ts+12−0

This diff is large: its 33 lines aren’t shown here.

packages/core/api/src/admin/admin-users.test.ts+1194−0

This diff is large: its 1195 lines aren’t shown here.

packages/core/api/src/admin/api.ts+362−0

This diff is large: its 363 lines aren’t shown here.

packages/core/api/src/admin/handlers.ts+69−0

This diff is large: its 70 lines aren’t shown here.

packages/core/api/src/admin/reads.ts+387−0

This diff is large: its 388 lines aren’t shown here.

packages/core/api/src/admin/service.ts+81−0

This diff is large: its 82 lines aren’t shown here.

packages/core/api/src/api.ts+2−0

This diff is large: its 16 lines aren’t shown here.

packages/core/api/src/artifacts/api.ts+165−0

This diff is large: its 166 lines aren’t shown here.

packages/core/api/src/client.ts+13−0

This diff is large: its 17 lines aren’t shown here.

packages/core/api/src/connections/api.ts+1−0

This diff is large: its 8 lines aren’t shown here.

packages/core/api/src/executions/api.ts+51−2

This diff is large: its 80 lines aren’t shown here.

packages/core/api/src/handlers/artifacts.ts+119−0

This diff is large: its 120 lines aren’t shown here.

packages/core/api/src/handlers/connections.ts+1−0

This diff is large: its 8 lines aren’t shown here.

packages/core/api/src/handlers/executions.ts+194−3

This diff is large: its 234 lines aren’t shown here.

packages/core/api/src/handlers/index.ts+3−0

This diff is large: its 22 lines aren’t shown here.

packages/core/api/src/handlers/integrations.ts+1−0

This diff is large: its 8 lines aren’t shown here.

packages/core/api/src/handlers/oauth.ts+6−0

This diff is large: its 21 lines aren’t shown here.

packages/core/api/src/index.ts+17−0

This diff is large: its 38 lines aren’t shown here.

packages/core/api/src/integrations/api.ts+8−0

This diff is large: its 29 lines aren’t shown here.

packages/core/api/src/oauth-popup.test.ts+114−3

This diff is large: its 131 lines aren’t shown here.

packages/core/api/src/oauth-popup.ts+15−4

This diff is large: its 33 lines aren’t shown here.

packages/core/api/src/oauth/api.ts+22−2

This diff is large: its 67 lines aren’t shown here.

packages/core/api/src/observability.test.ts+25−1

This diff is large: its 40 lines aren’t shown here.

packages/core/api/src/observability.ts+13−2

This diff is large: its 36 lines aren’t shown here.

packages/core/api/src/plugin-routes.ts+1−1

This diff is large: its 9 lines aren’t shown here.

packages/core/api/src/server.ts+35−1

This diff is large: its 86 lines aren’t shown here.

packages/core/api/src/server/execution-stack-middleware.ts+148−18

This diff is large: its 240 lines aren’t shown here.

packages/core/api/src/server/execution-stack.ts+58−9

This diff is large: its 89 lines aren’t shown here.

packages/core/api/src/server/executor-app.ts+7−2

This diff is large: its 23 lines aren’t shown here.

packages/core/api/src/server/fixed-execution-middleware.ts+28−5

This diff is large: its 56 lines aren’t shown here.

packages/core/api/src/server/host-foundation.ts+42−0

This diff is large: its 56 lines aren’t shown here.

packages/core/api/src/server/identity.ts+71−4

This diff is large: its 121 lines aren’t shown here.

packages/core/api/src/server/mcp-build.ts+57−6

This diff is large: its 91 lines aren’t shown here.

packages/core/api/src/server/scoped-executor.ts+114−5

This diff is large: its 167 lines aren’t shown here.

packages/core/api/src/services.ts+18−1

This diff is large: its 30 lines aren’t shown here.

packages/core/cli/CHANGELOG.md+98−0

This diff is large: its 104 lines aren’t shown here.

packages/core/cli/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

packages/core/config/CHANGELOG.md+98−0

This diff is large: its 104 lines aren’t shown here.

packages/core/config/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

packages/core/config/src/config.test.ts+37−35

This diff is large: its 222 lines aren’t shown here.

packages/core/config/src/index.ts+7−7

This diff is large: its 28 lines aren’t shown here.

packages/core/config/src/schema.ts+16−16

This diff is large: its 85 lines aren’t shown here.

packages/core/config/src/sink.ts+10−10

This diff is large: its 51 lines aren’t shown here.

packages/core/config/src/write.ts+24−24

This diff is large: its 114 lines aren’t shown here.

packages/core/execution/CHANGELOG.md+114−0

This diff is large: its 120 lines aren’t shown here.

packages/core/execution/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

packages/core/execution/src/description.test.ts+50−1

This diff is large: its 62 lines aren’t shown here.

packages/core/execution/src/description.ts+22−0

This diff is large: its 29 lines aren’t shown here.

packages/core/execution/src/engine.test.ts+1−1

This diff is large: its 9 lines aren’t shown here.

packages/core/execution/src/engine.ts+119−12

This diff is large: its 262 lines aren’t shown here.

packages/core/execution/src/index.ts+16−3

This diff is large: its 31 lines aren’t shown here.

packages/core/execution/src/provided-globals.ts+274−0

This diff is large: its 275 lines aren’t shown here.

packages/core/execution/src/skills.test.ts+27−1

This diff is large: its 39 lines aren’t shown here.

packages/core/execution/src/skills.ts+579−11

This diff is large: its 617 lines aren’t shown here.

packages/core/execution/src/tool-invoker.leak.test.ts+1−1

This diff is large: its 9 lines aren’t shown here.

packages/core/execution/src/tool-invoker.test.ts+434−54

This diff is large: its 611 lines aren’t shown here.

packages/core/execution/src/tool-invoker.ts+108−34

This diff is large: its 276 lines aren’t shown here.

packages/core/integrations-registry/src/registry.test.ts+10−2

This diff is large: its 33 lines aren’t shown here.

packages/core/integrations-registry/src/registry.ts+1−1

This diff is large: its 9 lines aren’t shown here.

packages/core/sdk/CHANGELOG.md+87−0

This diff is large: its 93 lines aren’t shown here.

packages/core/sdk/README.md+3−3

This diff is large: its 26 lines aren’t shown here.

packages/core/sdk/package.json+9−2

This diff is large: its 31 lines aren’t shown here.

packages/core/sdk/src/artifact-preview.test.ts+304−0

This diff is large: its 305 lines aren’t shown here.

packages/core/sdk/src/artifact-preview.ts+574−0

This diff is large: its 575 lines aren’t shown here.

packages/core/sdk/src/artifact-table.test.ts+132−0

This diff is large: its 133 lines aren’t shown here.

packages/core/sdk/src/artifact.ts+211−0

This diff is large: its 212 lines aren’t shown here.

packages/core/sdk/src/artifacts.test.ts+353−0

This diff is large: its 354 lines aren’t shown here.

packages/core/sdk/src/auth-tool-failure.ts+55−17

This diff is large: its 111 lines aren’t shown here.

packages/core/sdk/src/blob.ts+1−1

This diff is large: its 8 lines aren’t shown here.

packages/core/sdk/src/client.test.ts+2−2

This diff is large: its 16 lines aren’t shown here.

packages/core/sdk/src/client.ts+34−5

This diff is large: its 90 lines aren’t shown here.

packages/core/sdk/src/connection-name-identifier.ts+4−0

This diff is large: its 11 lines aren’t shown here.

packages/core/sdk/src/connection.ts+7−4

This diff is large: its 26 lines aren’t shown here.

packages/core/sdk/src/connections.test.ts+212−2

This diff is large: its 268 lines aren’t shown here.

packages/core/sdk/src/core-schema.ts+129−4

This diff is large: its 203 lines aren’t shown here.

packages/core/sdk/src/core-tools.ts+78−17

This diff is large: its 210 lines aren’t shown here.

packages/core/sdk/src/errors.ts+47−4

This diff is large: its 89 lines aren’t shown here.

packages/core/sdk/src/executor.test.ts+259−1

This diff is large: its 306 lines aren’t shown here.

packages/core/sdk/src/executor.ts+1724−194

This diff is large: its 2569 lines aren’t shown here.

packages/core/sdk/src/fuma-runtime.test.ts+265−0

This diff is large: its 266 lines aren’t shown here.

packages/core/sdk/src/fuma-runtime.ts+167−12

This diff is large: its 226 lines aren’t shown here.

packages/core/sdk/src/host-internal.ts+15−0

This diff is large: its 33 lines aren’t shown here.

packages/core/sdk/src/hosted-http-client.test.ts+68−0

This diff is large: its 79 lines aren’t shown here.

packages/core/sdk/src/hosted-http-client.ts+71−2

This diff is large: its 88 lines aren’t shown here.

packages/core/sdk/src/ids.ts+4−0

This diff is large: its 11 lines aren’t shown here.

packages/core/sdk/src/index.ts+91−2

This diff is large: its 217 lines aren’t shown here.

packages/core/sdk/src/insufficient-scope.test.ts+377−0

This diff is large: its 378 lines aren’t shown here.

packages/core/sdk/src/insufficient-scope.ts+274−0

This diff is large: its 275 lines aren’t shown here.

packages/core/sdk/src/integration.ts+26−0

This diff is large: its 59 lines aren’t shown here.

packages/core/sdk/src/migration-spec.test.ts+2−2

This diff is large: its 16 lines aren’t shown here.

packages/core/sdk/src/migration-spec.ts+2−2

This diff is large: its 18 lines aren’t shown here.

packages/core/sdk/src/oauth-client.ts+199−3

This diff is large: its 255 lines aren’t shown here.

packages/core/sdk/src/oauth-discovery.test.ts+26−0

This diff is large: its 33 lines aren’t shown here.

packages/core/sdk/src/oauth-discovery.ts+58−9

This diff is large: its 95 lines aren’t shown here.

packages/core/sdk/src/oauth-ema-lifecycle.test.ts+336−0

This diff is large: its 337 lines aren’t shown here.

packages/core/sdk/src/oauth-ema-rollout.test.ts+467−0

This diff is large: its 468 lines aren’t shown here.

packages/core/sdk/src/oauth-ema.test.ts+502−0

This diff is large: its 503 lines aren’t shown here.

packages/core/sdk/src/oauth-ema.ts+476−0

This diff is large: its 477 lines aren’t shown here.

packages/core/sdk/src/oauth-first-party.test.ts+471−0

This diff is large: its 472 lines aren’t shown here.

packages/core/sdk/src/oauth-flow.test.ts+985−8

This diff is large: its 1026 lines aren’t shown here.

packages/core/sdk/src/oauth-helpers.test.ts+340−0

This diff is large: its 383 lines aren’t shown here.

packages/core/sdk/src/oauth-helpers.ts+731−49

This diff is large: its 902 lines aren’t shown here.

packages/core/sdk/src/oauth-register-dynamic.test.ts+211−0

This diff is large: its 232 lines aren’t shown here.

packages/core/sdk/src/oauth-remove-client.test.ts+66−1

This diff is large: its 79 lines aren’t shown here.

packages/core/sdk/src/oauth-scope-union.test.ts+93−2

This diff is large: its 130 lines aren’t shown here.

packages/core/sdk/src/oauth-service.ts+773−68

This diff is large: its 1207 lines aren’t shown here.

packages/core/sdk/src/owner-policy-reach.test.ts+318−0

This diff is large: its 319 lines aren’t shown here.

packages/core/sdk/src/owner-policy.ts+78−1

This diff is large: its 123 lines aren’t shown here.

packages/core/sdk/src/pending-approval.test.ts+142−0

This diff is large: its 143 lines aren’t shown here.

packages/core/sdk/src/pending-approval.ts+110−0

This diff is large: its 111 lines aren’t shown here.

packages/core/sdk/src/platform-view.test.ts+901−0

This diff is large: its 902 lines aren’t shown here.

packages/core/sdk/src/plugin.ts+114−11

This diff is large: its 258 lines aren’t shown here.

packages/core/sdk/src/policies.test.ts+43−2

This diff is large: its 66 lines aren’t shown here.

packages/core/sdk/src/policies.ts+41−0

This diff is large: its 55 lines aren’t shown here.

packages/core/sdk/src/promise-executor.ts+1−1

This diff is large: its 9 lines aren’t shown here.

packages/core/sdk/src/promise.test.ts+2−2

This diff is large: its 18 lines aren’t shown here.

packages/core/sdk/src/provider-call-timeout.test.ts+212−0

This diff is large: its 213 lines aren’t shown here.

packages/core/sdk/src/provider-item-owner.ts+47−0

This diff is large: its 48 lines aren’t shown here.

packages/core/sdk/src/sentry-grouping.test.ts+269−0

This diff is large: its 270 lines aren’t shown here.

packages/core/sdk/src/sentry-grouping.ts+169−0

This diff is large: its 170 lines aren’t shown here.

packages/core/sdk/src/server-connection.test.ts+54−0

This diff is large: its 80 lines aren’t shown here.

packages/core/sdk/src/server-connection.ts+69−1

This diff is large: its 137 lines aren’t shown here.

packages/core/sdk/src/shape-inference.test.ts+136−0

This diff is large: its 137 lines aren’t shown here.

packages/core/sdk/src/shape-inference.ts+219−0

This diff is large: its 220 lines aren’t shown here.

packages/core/sdk/src/shape-memory.ts+90−0

This diff is large: its 91 lines aren’t shown here.

packages/core/sdk/src/shared.ts+34−0

This diff is large: its 87 lines aren’t shown here.

packages/core/sdk/src/sqlite-config-blob-migration.test.ts+2−2

This diff is large: its 18 lines aren’t shown here.

packages/core/sdk/src/sqlite-data-migrations.test.ts+61−0

This diff is large: its 73 lines aren’t shown here.

packages/core/sdk/src/sqlite-data-migrations.ts+12−6

This diff is large: its 33 lines aren’t shown here.

packages/core/sdk/src/subject-registry.test.ts+418−0

This diff is large: its 419 lines aren’t shown here.

packages/core/sdk/src/subject-registry.ts+0−0

Binary file not shown.

packages/core/sdk/src/subject-table.test.ts+103−0

This diff is large: its 104 lines aren’t shown here.

packages/core/sdk/src/test-config.ts+6−0

This diff is large: its 22 lines aren’t shown here.

packages/core/sdk/src/testing/id-jag-test-support.ts+251−0

This diff is large: its 252 lines aren’t shown here.

packages/core/sdk/src/testing/oauth-test-server.ts+340−2

This diff is large: its 467 lines aren’t shown here.

packages/core/sdk/src/tool-not-found-empty-catalog.test.ts+113−0

This diff is large: its 114 lines aren’t shown here.

packages/core/sdk/src/types.ts+4−0

This diff is large: its 11 lines aren’t shown here.

packages/core/sdk/tsup.config.ts+1−0

This diff is large: its 8 lines aren’t shown here.

packages/core/vite-plugin/CHANGELOG.md+98−0

This diff is large: its 104 lines aren’t shown here.

packages/core/vite-plugin/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

packages/hosts/cloudflare/CHANGELOG.md+144−0

This diff is large: its 150 lines aren’t shown here.

packages/hosts/cloudflare/package.json+6−2

This diff is large: its 28 lines aren’t shown here.

packages/hosts/cloudflare/src/mcp/agent-session-durable-object.test.ts+582−4

This diff is large: its 637 lines aren’t shown here.

packages/hosts/cloudflare/src/mcp/agent-session-durable-object.ts+439−37

This diff is large: its 709 lines aren’t shown here.

packages/hosts/cloudflare/src/mcp/agent-session-model-resume.test.ts+2−0

This diff is large: its 9 lines aren’t shown here.

packages/hosts/cloudflare/src/mcp/agents-event-store.test.ts+41−12

This diff is large: its 83 lines aren’t shown here.

packages/hosts/cloudflare/src/mcp/agents-priming-event.test.ts+11−4

This diff is large: its 40 lines aren’t shown here.

packages/hosts/cloudflare/src/mcp/do-headers.test.ts+49−0

This diff is large: its 50 lines aren’t shown here.

packages/hosts/cloudflare/src/mcp/do-headers.ts+41−6

This diff is large: its 62 lines aren’t shown here.

packages/hosts/cloudflare/src/mcp/durable-object-errors.test.ts+275−0

This diff is large: its 276 lines aren’t shown here.

packages/hosts/cloudflare/src/mcp/durable-object-errors.ts+237−0

This diff is large: its 238 lines aren’t shown here.

packages/hosts/cloudflare/src/mcp/session-runtime-residency.ts+96−0

This diff is large: its 97 lines aren’t shown here.

packages/hosts/mcp-apps-shell/.gitignore+1−0

This diff is large: its 2 lines aren’t shown here.

packages/hosts/mcp-apps-shell/CHANGELOG.md+57−0

This diff is large: its 58 lines aren’t shown here.

packages/hosts/mcp-apps-shell/package.json+102−0

This diff is large: its 103 lines aren’t shown here.

packages/hosts/mcp-apps-shell/scripts/gen-provided-globals.ts+23−0

This diff is large: its 24 lines aren’t shown here.

packages/hosts/mcp-apps-shell/scripts/gen-smoke-harness.ts+45−0

This diff is large: its 46 lines aren’t shown here.

packages/hosts/mcp-apps-shell/scripts/subset-fonts.ts+96−0

This diff is large: its 97 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/index.ts+13−0

This diff is large: its 14 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell-asset-path.ts+35−0

This diff is large: its 36 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell-dev-html.d.ts+14−0

This diff is large: its 15 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell-html.ts+67−0

This diff is large: its 68 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell-resource.smoke.test.ts+98−0

This diff is large: its 99 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/artifact-renderer.tsx+432−0

This diff is large: its 433 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/artifact-states.tsx+217−0

This diff is large: its 218 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/component-runtime.test.ts+53−0

This diff is large: its 54 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/component-runtime.ts+190−0

This diff is large: its 191 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/components.ts+283−0

This diff is large: its 284 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/display-mode.ts+104−0

This diff is large: its 105 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/fonts/README.md+29−0

This diff is large: its 30 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/fonts/geist-mono.woff2+0−0

Binary file not shown.

packages/hosts/mcp-apps-shell/src/shell/fonts/geist-sans.woff2+0−0

Binary file not shown.

packages/hosts/mcp-apps-shell/src/shell/globals.css+53−0

This diff is large: its 54 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/hooks.ts+158−0

This diff is large: its 159 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/inner-renderer-source.d.ts+14−0

This diff is large: its 15 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/inner-renderer.tsx+432−0

This diff is large: its 433 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/mcp-app.browser.test.ts+2649−0

This diff is large: its 2650 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/mcp-app.html+17−0

This diff is large: its 18 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/mcp-app.tsx+101−0

This diff is large: its 102 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/preview-capture.ts+36−0

This diff is large: its 37 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/provided-globals.pin.test.ts+40−0

This diff is large: its 41 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/proxy.ts+181−0

This diff is large: its 182 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/render-signal.ts+60−0

This diff is large: its 61 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/shell-app.tsx+1098−0

This diff is large: its 1099 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/shell-html-url.d.ts+13−0

This diff is large: its 14 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/smoke-harness-bundle.gen.ts+17−0

This diff is large: its 18 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/smoke-harness-shims.ts+274−0

This diff is large: its 275 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/smoke-harness.tsx+168−0

This diff is large: its 169 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/smoke-render-result.ts+36−0

This diff is large: its 37 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/smoke-render.test.tsx+388−0

This diff is large: its 389 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/smoke-render.ts+189−0

This diff is large: its 190 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/theme-tokens.pin.test.ts+196−0

This diff is large: its 197 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/theme.css+317−0

This diff is large: its 318 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/tool-call-grammar.pin.test.ts+110−0

This diff is large: its 111 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/shell/tools-proxy.ts+261−0

This diff is large: its 262 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/smoke-render-entry.ts+43−0

This diff is large: its 44 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/vite.test.ts+28−0

This diff is large: its 29 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/vite.ts+411−0

This diff is large: its 412 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/worker.test.ts+85−0

This diff is large: its 86 lines aren’t shown here.

packages/hosts/mcp-apps-shell/src/worker.ts+75−0

This diff is large: its 76 lines aren’t shown here.

packages/hosts/mcp-apps-shell/tsconfig.json+16−0

This diff is large: its 17 lines aren’t shown here.

packages/hosts/mcp-apps-shell/vite.config.shell.ts+46−0

This diff is large: its 47 lines aren’t shown here.

packages/hosts/mcp-apps-shell/vitest.config.ts+12−0

This diff is large: its 13 lines aren’t shown here.

packages/hosts/mcp/package.json+14−1

This diff is large: its 29 lines aren’t shown here.

packages/hosts/mcp/src/artifact-action.ts+87−0

This diff is large: its 88 lines aren’t shown here.

packages/hosts/mcp/src/artifact-bindings.test.ts+398−0

This diff is large: its 399 lines aren’t shown here.

packages/hosts/mcp/src/artifact-bindings.ts+419−0

This diff is large: its 420 lines aren’t shown here.

packages/hosts/mcp/src/artifacts-tools.test.ts+2557−0

This diff is large: its 2558 lines aren’t shown here.

packages/hosts/mcp/src/browser-approval.ts+28−0

This diff is large: its 35 lines aren’t shown here.

packages/hosts/mcp/src/create-artifact.ts+489−0

This diff is large: its 490 lines aren’t shown here.

packages/hosts/mcp/src/envelope.test.ts+7−0

This diff is large: its 14 lines aren’t shown here.

packages/hosts/mcp/src/envelope.ts+53−19

This diff is large: its 160 lines aren’t shown here.

packages/hosts/mcp/src/in-memory-session-store.test.ts+54−0

This diff is large: its 55 lines aren’t shown here.

packages/hosts/mcp/src/in-memory-session-store.ts+17−1

This diff is large: its 41 lines aren’t shown here.

packages/hosts/mcp/src/namespace-search-tools.test.ts+222−0

This diff is large: its 223 lines aren’t shown here.

packages/hosts/mcp/src/tool-call-code.ts+119−0

This diff is large: its 120 lines aren’t shown here.

packages/hosts/mcp/src/tool-server.test.ts+228−13

This diff is large: its 327 lines aren’t shown here.

packages/hosts/mcp/src/tool-server.ts+1364−54

This diff is large: its 1657 lines aren’t shown here.

packages/kernel/core/CHANGELOG.md+28−0

This diff is large: its 34 lines aren’t shown here.

packages/kernel/core/package.json+2−1

This diff is large: its 16 lines aren’t shown here.

packages/kernel/core/src/error-kind.test.ts+25−0

This diff is large: its 26 lines aren’t shown here.

packages/kernel/core/src/error-kind.ts+37−0

This diff is large: its 38 lines aren’t shown here.

packages/kernel/core/src/index.ts+1−0

This diff is large: its 6 lines aren’t shown here.

packages/kernel/core/src/json-schema.ts+27−8

This diff is large: its 50 lines aren’t shown here.

packages/kernel/core/src/strip-types.test.ts+1−1

This diff is large: its 9 lines aren’t shown here.

packages/kernel/core/src/types.ts+3−0

This diff is large: its 17 lines aren’t shown here.

packages/kernel/ir/src/registry.ts+2−2

This diff is large: its 18 lines aren’t shown here.

packages/kernel/ir/src/serialize.ts+2−2

This diff is large: its 18 lines aren’t shown here.

packages/kernel/runtime-deno-subprocess/src/deno-subprocess-worker.mjs+1−1

This diff is large: its 9 lines aren’t shown here.

packages/kernel/runtime-deno-subprocess/src/deno-worker-process.ts+47−2

This diff is large: its 99 lines aren’t shown here.

packages/kernel/runtime-deno-subprocess/src/fixtures/close-stdin-worker.mjs+37−0

This diff is large: its 38 lines aren’t shown here.

packages/kernel/runtime-deno-subprocess/src/index.test.ts+68−13

This diff is large: its 109 lines aren’t shown here.

packages/kernel/runtime-deno-subprocess/src/index.ts+78−13

This diff is large: its 156 lines aren’t shown here.

packages/kernel/runtime-dynamic-worker/src/executor.ts+37−3

This diff is large: its 81 lines aren’t shown here.

packages/kernel/runtime-dynamic-worker/src/invocation.test.ts+86−2

This diff is large: its 145 lines aren’t shown here.

packages/kernel/runtime-dynamic-worker/src/module-template.ts+26−6

This diff is large: its 68 lines aren’t shown here.

packages/kernel/runtime-quickjs/CHANGELOG.md+100−0

This diff is large: its 106 lines aren’t shown here.

packages/kernel/runtime-quickjs/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

packages/kernel/runtime-quickjs/src/index.test.ts+50−2

This diff is large: its 67 lines aren’t shown here.

packages/kernel/runtime-quickjs/src/index.ts+94−21

This diff is large: its 263 lines aren’t shown here.

packages/kernel/runtime-quickjs/src/sealed-bundle.test.ts+113−0

This diff is large: its 114 lines aren’t shown here.

packages/kernel/runtime-quickjs/src/sealed-bundle.ts+209−0

This diff is large: its 210 lines aren’t shown here.

packages/kernel/runtime-workerd-subprocess/CHANGELOG.md+99−0

This diff is large: its 100 lines aren’t shown here.

packages/kernel/runtime-workerd-subprocess/package.json+26−0

This diff is large: its 27 lines aren’t shown here.

packages/kernel/runtime-workerd-subprocess/src/index.test.ts+251−0

This diff is large: its 252 lines aren’t shown here.

packages/kernel/runtime-workerd-subprocess/src/index.ts+740−0

This diff is large: its 741 lines aren’t shown here.

packages/kernel/runtime-workerd-subprocess/tsconfig.jsonrenamed from packages/plugins/google/tsconfig.json+5−6

This diff is large: its 26 lines aren’t shown here.

packages/kernel/runtime-workerd-subprocess/vitest.config.ts+7−0

This diff is large: its 8 lines aren’t shown here.

packages/plugins/desktop-settings/CHANGELOG.md+98−0

This diff is large: its 104 lines aren’t shown here.

packages/plugins/desktop-settings/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

packages/plugins/desktop-settings/src/client.tsx+5−5

This diff is large: its 31 lines aren’t shown here.

packages/plugins/desktop-settings/src/server.ts+1−1

This diff is large: its 9 lines aren’t shown here.

packages/plugins/encrypted-secrets/CHANGELOG.md+98−0

This diff is large: its 104 lines aren’t shown here.

packages/plugins/encrypted-secrets/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

packages/plugins/encrypted-secrets/src/index.test.ts+53−2

This diff is large: its 73 lines aren’t shown here.

packages/plugins/encrypted-secrets/src/index.ts+20−13

This diff is large: its 78 lines aren’t shown here.

packages/plugins/encrypted-secrets/src/repartition-migration.test.ts+208−0

This diff is large: its 209 lines aren’t shown here.

packages/plugins/encrypted-secrets/src/repartition-migration.ts+123−0

This diff is large: its 124 lines aren’t shown here.

packages/plugins/example/CHANGELOG.md+98−0

This diff is large: its 104 lines aren’t shown here.

packages/plugins/example/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

packages/plugins/file-secrets/CHANGELOG.md+100−0

This diff is large: its 106 lines aren’t shown here.

packages/plugins/file-secrets/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

packages/plugins/file-secrets/src/data-dir.test.ts+115−0

This diff is large: its 116 lines aren’t shown here.

packages/plugins/file-secrets/src/index.test.ts+208−0

This diff is large: its 209 lines aren’t shown here.

packages/plugins/file-secrets/src/index.ts+170−57

This diff is large: its 324 lines aren’t shown here.

packages/plugins/google/CHANGELOG.md+0−143

This diff is large: its 144 lines aren’t shown here.

packages/plugins/google/package.json+0−98

This diff is large: its 99 lines aren’t shown here.

packages/plugins/google/src/api/group.ts+0−142

This diff is large: its 143 lines aren’t shown here.

packages/plugins/google/src/api/handlers.ts+0−105

This diff is large: its 106 lines aren’t shown here.

packages/plugins/google/src/api/index.ts+0−15

This diff is large: its 16 lines aren’t shown here.

packages/plugins/google/src/promise.ts+0−1

This diff is large: its 2 lines aren’t shown here.

packages/plugins/google/src/react/AddGoogleSource.tsx+0−178

This diff is large: its 179 lines aren’t shown here.

packages/plugins/google/src/react/GoogleAccountsPanel.tsx+0−148

This diff is large: its 149 lines aren’t shown here.

packages/plugins/google/src/react/GoogleProductPicker.tsx+0−363

This diff is large: its 364 lines aren’t shown here.

packages/plugins/google/src/react/atoms.ts+0−30

This diff is large: its 31 lines aren’t shown here.

packages/plugins/google/src/react/client.ts+0−13

This diff is large: its 14 lines aren’t shown here.

packages/plugins/google/src/react/index.ts+0−12

This diff is large: its 13 lines aren’t shown here.

packages/plugins/google/src/react/plugin-client.tsx+0−8

This diff is large: its 9 lines aren’t shown here.

packages/plugins/google/src/react/source-plugin.ts+0−18

This diff is large: its 19 lines aren’t shown here.

packages/plugins/google/src/sdk/oauth-batches.test.ts+0−98

This diff is large: its 99 lines aren’t shown here.

packages/plugins/google/src/sdk/oauth-batches.ts+0−69

This diff is large: its 70 lines aren’t shown here.

packages/plugins/google/src/sdk/oauth-scopes.test.ts+0−34

This diff is large: its 35 lines aren’t shown here.

packages/plugins/google/src/sdk/oauth-scopes.ts+0−66

This diff is large: its 67 lines aren’t shown here.

packages/plugins/google/src/sdk/plugin.test.ts+0−799

This diff is large: its 800 lines aren’t shown here.

packages/plugins/google/src/sdk/plugin.ts+0−469

This diff is large: its 470 lines aren’t shown here.

packages/plugins/google/src/sdk/presets.test.ts+0−29

This diff is large: its 30 lines aren’t shown here.

packages/plugins/google/src/sdk/product-picker-scopes.test.ts+0−122

This diff is large: its 123 lines aren’t shown here.

packages/plugins/google/tsup.config.ts+0−14

This diff is large: its 15 lines aren’t shown here.

packages/plugins/google/vitest.config.ts+0−12

This diff is large: its 13 lines aren’t shown here.

packages/plugins/graphql/CHANGELOG.md+140−0

This diff is large: its 146 lines aren’t shown here.

packages/plugins/graphql/README.md+2−2

This diff is large: its 18 lines aren’t shown here.

packages/plugins/graphql/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

packages/plugins/graphql/src/react/AddGraphqlIntegration.tsxrenamed from packages/plugins/graphql/src/react/AddGraphqlSource.tsx+8−8

This diff is large: its 65 lines aren’t shown here.

packages/plugins/graphql/src/react/GraphqlAccountsPanel.tsx+3−3

This diff is large: its 16 lines aren’t shown here.

packages/plugins/graphql/src/react/GraphqlIntegrationFields.tsxrenamed from packages/plugins/graphql/src/react/GraphqlSourceFields.tsx+1−1

This diff is large: its 9 lines aren’t shown here.

packages/plugins/graphql/src/react/atoms.ts+3−3

This diff is large: its 26 lines aren’t shown here.

packages/plugins/graphql/src/react/index.ts+2−2

This diff is large: its 6 lines aren’t shown here.

packages/plugins/graphql/src/react/integration-plugin.tsrenamed from packages/plugins/graphql/src/react/source-plugin.ts+1−1

This diff is large: its 9 lines aren’t shown here.

packages/plugins/graphql/src/react/plugin-client.tsx+1−1

This diff is large: its 8 lines aren’t shown here.

packages/plugins/graphql/src/sdk/errors.ts+14−0

This diff is large: its 28 lines aren’t shown here.

packages/plugins/graphql/src/sdk/index.ts+6−1

This diff is large: its 13 lines aren’t shown here.

packages/plugins/graphql/src/sdk/introspect.ts+14−1

This diff is large: its 58 lines aren’t shown here.

packages/plugins/graphql/src/sdk/invocation-timeout.test.ts+107−0

This diff is large: its 108 lines aren’t shown here.

packages/plugins/graphql/src/sdk/invoke.ts+67−33

This diff is large: its 132 lines aren’t shown here.

packages/plugins/graphql/src/sdk/multi-placement-auth.test.ts+50−0

This diff is large: its 55 lines aren’t shown here.

packages/plugins/graphql/src/sdk/plugin.test.ts+359−0

This diff is large: its 380 lines aren’t shown here.

packages/plugins/graphql/src/sdk/plugin.ts+334−41

This diff is large: its 499 lines aren’t shown here.

packages/plugins/graphql/src/sdk/presets.ts+1−1

This diff is large: its 9 lines aren’t shown here.

packages/plugins/graphql/src/sdk/types.ts+6−0

This diff is large: its 12 lines aren’t shown here.

packages/plugins/keychain/CHANGELOG.md+98−0

This diff is large: its 104 lines aren’t shown here.

packages/plugins/keychain/package.json+1−1

This diff is large: its 8 lines aren’t shown here.

packages/plugins/mcp/CHANGELOG.md+160−0

This diff is large: its 166 lines aren’t shown here.

packages/plugins/mcp/README.md+4−4

This diff is large: its 35 lines aren’t shown here.

packages/plugins/mcp/package.json+6−2

This diff is large: its 34 lines aren’t shown here.

packages/plugins/mcp/scripts/gen-call-tool-result-schema.ts+26−0

This diff is large: its 27 lines aren’t shown here.

packages/plugins/mcp/src/api/group.ts+4−0

This diff is large: its 11 lines aren’t shown here.

packages/plugins/mcp/src/api/handlers.ts+2−0

This diff is large: its 16 lines aren’t shown here.

packages/plugins/mcp/src/react/AddMcpIntegration.tsxrenamed from packages/plugins/mcp/src/react/AddMcpSource.tsx+19−3

This diff is large: its 58 lines aren’t shown here.

packages/plugins/mcp/src/react/EditMcpIntegration.tsxrenamed from packages/plugins/mcp/src/react/EditMcpSource.tsx+8−5

This diff is large: its 29 lines aren’t shown here.

packages/plugins/mcp/src/react/McpAccountsPanel.tsx+3−3

This diff is large: its 16 lines aren’t shown here.

packages/plugins/mcp/src/react/McpRemoteIntegrationFields.tsxrenamed from packages/plugins/mcp/src/react/McpRemoteSourceFields.tsx+3−3

This diff is large: its 27 lines aren’t shown here.

packages/plugins/mcp/src/react/McpSignInButton.tsx+2−2

This diff is large: its 11 lines aren’t shown here.

packages/plugins/mcp/src/react/index.ts+2−2

This diff is large: its 8 lines aren’t shown here.

packages/plugins/mcp/src/react/integration-plugin.tsxrenamed from packages/plugins/mcp/src/react/source-plugin.tsx+5−5

This diff is large: its 33 lines aren’t shown here.

packages/plugins/mcp/src/react/plugin-client.tsx+2−2

This diff is large: its 15 lines aren’t shown here.

packages/plugins/mcp/src/sdk/call-tool-result-schema.gen.ts+395−0

This diff is large: its 396 lines aren’t shown here.

packages/plugins/mcp/src/sdk/call-tool-result-schema.test.ts+16−0

This diff is large: its 17 lines aren’t shown here.

packages/plugins/mcp/src/sdk/catalog-sync.test.ts+19−0

This diff is large: its 26 lines aren’t shown here.

packages/plugins/mcp/src/sdk/client-module.ts+39−0

This diff is large: its 40 lines aren’t shown here.

packages/plugins/mcp/src/sdk/cloudflare-codemode.test.ts+42−0

This diff is large: its 43 lines aren’t shown here.

packages/plugins/mcp/src/sdk/cloudflare-codemode.ts+15−0

This diff is large: its 16 lines aren’t shown here.

packages/plugins/mcp/src/sdk/connection-pool-key.test.ts+154−0

This diff is large: its 155 lines aren’t shown here.

packages/plugins/mcp/src/sdk/connection-pool.test.ts+197−0

This diff is large: its 198 lines aren’t shown here.

packages/plugins/mcp/src/sdk/connection-pool.ts+147−0

This diff is large: its 148 lines aren’t shown here.

packages/plugins/mcp/src/sdk/connection-socket-release.test.ts+64−0

This diff is large: its 65 lines aren’t shown here.

packages/plugins/mcp/src/sdk/connection.test.ts+85−0

This diff is large: its 86 lines aren’t shown here.

packages/plugins/mcp/src/sdk/connection.ts+274−37

This diff is large: its 411 lines aren’t shown here.

packages/plugins/mcp/src/sdk/describe-auth-methods.test.ts+37−0

This diff is large: its 44 lines aren’t shown here.

packages/plugins/mcp/src/sdk/discover.ts+32−21

This diff is large: its 67 lines aren’t shown here.

packages/plugins/mcp/src/sdk/elicitation.test.ts+56−81

This diff is large: its 202 lines aren’t shown here.

packages/plugins/mcp/src/sdk/errors.ts+38−0

This diff is large: its 71 lines aren’t shown here.

packages/plugins/mcp/src/sdk/health-missing-credential.test.ts+111−0

This diff is large: its 112 lines aren’t shown here.

packages/plugins/mcp/src/sdk/http-status.test.ts+75−0

This diff is large: its 76 lines aren’t shown here.

packages/plugins/mcp/src/sdk/http-status.ts+68−11

This diff is large: its 102 lines aren’t shown here.

packages/plugins/mcp/src/sdk/invoke.test.ts+17−6

This diff is large: its 47 lines aren’t shown here.

packages/plugins/mcp/src/sdk/invoke.ts+85−27

This diff is large: its 204 lines aren’t shown here.

packages/plugins/mcp/src/sdk/multi-placement-auth.test.ts+6−2

This diff is large: its 22 lines aren’t shown here.

packages/plugins/mcp/src/sdk/plugin.test.ts+106−4

This diff is large: its 143 lines aren’t shown here.

packages/plugins/mcp/src/sdk/plugin.ts+195−28

This diff is large: its 433 lines aren’t shown here.

packages/plugins/mcp/src/sdk/presets.ts+7−4

This diff is large: its 31 lines aren’t shown here.

packages/plugins/mcp/src/sdk/probe-shape-real-servers.live.test.ts+1−1

This diff is large: its 9 lines aren’t shown here.

packages/plugins/mcp/src/sdk/probe-shape.test.ts+90−2

This diff is large: its 104 lines aren’t shown here.

packages/plugins/mcp/src/sdk/probe-shape.ts+51−10

This diff is large: its 108 lines aren’t shown here.

packages/plugins/mcp/src/sdk/slack-connect-file.integration.test.ts+182−0

This diff is large: its 183 lines aren’t shown here.

packages/plugins/mcp/src/sdk/slack-connect-file.test.ts+171−0

This diff is large: its 172 lines aren’t shown here.

packages/plugins/mcp/src/sdk/slack-connect-file.ts+153−0

This diff is large: its 154 lines aren’t shown here.

packages/plugins/mcp/src/sdk/stdio-connector.ts+4−3

This diff is large: its 21 lines aren’t shown here.

packages/plugins/mcp/src/sdk/stdio-interrupt-cleanup.test.ts+112−0

This diff is large: its 113 lines aren’t shown here.

packages/plugins/mcp/src/sdk/stdio-interrupt-test-server.ts+65−0

This diff is large: its 66 lines aren’t shown here.

packages/plugins/mcp/src/sdk/stdio-negotiation-test-server.ts+24−0

This diff is large: its 25 lines aren’t shown here.

packages/plugins/mcp/src/sdk/stdio-negotiation.test.ts+64−0

This diff is large: its 65 lines aren’t shown here.

packages/plugins/mcp/src/sdk/testing-fixtures.test.ts+5−3

This diff is large: its 21 lines aren’t shown here.

packages/plugins/mcp/src/sdk/types.ts+38−1

This diff is large: its 75 lines aren’t shown here.

packages/plugins/mcp/src/testing/server.ts+78−6

This diff is large: its 172 lines aren’t shown here.

packages/plugins/mcp/tsconfig.json+1−1

This diff is large: its 9 lines aren’t shown here.

packages/plugins/microsoft/CHANGELOG.md+0−141

This diff is large: its 142 lines aren’t shown here.

packages/plugins/microsoft/package.json+0−98

This diff is large: its 99 lines aren’t shown here.

packages/plugins/microsoft/src/api/group.ts+0−163

This diff is large: its 164 lines aren’t shown here.

packages/plugins/microsoft/src/api/handlers.ts+0−144

This diff is large: its 145 lines aren’t shown here.

packages/plugins/microsoft/src/api/index.ts+0−15

This diff is large: its 16 lines aren’t shown here.

packages/plugins/microsoft/src/promise.ts+0−1

This diff is large: its 2 lines aren’t shown here.

packages/plugins/microsoft/src/react/AddMicrosoftSource.tsx+0−155

This diff is large: its 156 lines aren’t shown here.

packages/plugins/microsoft/src/react/MicrosoftAccountsPanel.tsx+0−118

This diff is large: its 119 lines aren’t shown here.

packages/plugins/microsoft/src/react/MicrosoftScopePicker.tsx+0−286

This diff is large: its 287 lines aren’t shown here.

packages/plugins/microsoft/src/react/atoms.ts+0−30

This diff is large: its 31 lines aren’t shown here.

packages/plugins/microsoft/src/react/client.ts+0−13

This diff is large: its 14 lines aren’t shown here.

packages/plugins/microsoft/src/react/index.ts+0−12

This diff is large: its 13 lines aren’t shown here.

packages/plugins/microsoft/src/react/plugin-client.tsx+0−8

This diff is large: its 9 lines aren’t shown here.

packages/plugins/microsoft/src/react/source-plugin.ts+0−18

This diff is large: its 19 lines aren’t shown here.

This diff is larger than a page shows: the files after these aren’t listed. Clone the repository to see all of it.