History

fmt

Rhys Sullivan committed 6 months ago

Commit c92c57d1 parent 87ab5c7265 files · +5705 −8140Browse files

Unified · Split

265 files
  1. .agents/skills/wrdn-effect-atom-optimistic/SKILL.md +10 −8
  2. .agents/skills/wrdn-effect-typed-errors/SKILL.md +14 −13
  3. .skills/cli-release/SKILL.md +9 −0
  4. .skills/effect-atom-optimistic-updates/SKILL.md +4 −10
  5. .skills/graphite/SKILL.md +36 −6
  6. RELEASING.md +6 −2
  7. apps/cli/release-notes/next.md +15 −0
  8. apps/cli/src/build.ts +15 −16
  9. apps/cli/src/daemon-state.ts +22 −14
  10. apps/cli/src/daemon.ts +7 −2
  11. apps/cli/src/main.ts +3 −6
  12. apps/cli/src/tooling.ts +6 −7
  13. apps/cloud/drizzle/meta/0000_snapshot.json +20 −73
  14. apps/cloud/drizzle/meta/0001_snapshot.json +20 −73
  15. apps/cloud/drizzle/meta/0002_snapshot.json +20 −73
  16. apps/cloud/drizzle/meta/0003_snapshot.json +21 −77
  17. apps/cloud/drizzle/meta/0007_snapshot.json +21 −77
  18. apps/cloud/drizzle/meta/0008_snapshot.json +27 −104
  19. apps/cloud/executor.config.ts +1 −4
  20. apps/cloud/src/api.request-scope.node.test.ts +7 −17
  21. apps/cloud/src/api.test.ts +2 −7
  22. apps/cloud/src/api/autumn.ts +2 −10
  23. apps/cloud/src/api/error-response.ts +1 −4
  24. apps/cloud/src/api/layers.ts +4 −14
  25. apps/cloud/src/api/protected-layers.ts +1 −5
  26. apps/cloud/src/api/protected.ts +3 −12
  27. apps/cloud/src/api/router.ts +2 −7
  28. apps/cloud/src/api/slack.ts +1 −5
  29. apps/cloud/src/auth/context.ts +3 −4
  30. apps/cloud/src/auth/handlers.ts +8 −14
  31. apps/cloud/src/auth/middleware.ts +11 −10
  32. apps/cloud/src/auth/resolve-organization.ts +1 −3
  33. apps/cloud/src/auth/workos.ts +4 −6
  34. apps/cloud/src/jwks-cache.node.test.ts +1 −3
  35. apps/cloud/src/mcp-auth.node.test.ts +6 −4
  36. apps/cloud/src/mcp-flow.test.ts +7 −12
  37. apps/cloud/src/mcp-session.e2e.node.test.ts +7 −10
  38. apps/cloud/src/routes/billing_.plans.tsx +74 −63
  39. apps/cloud/src/routes/sources.add.$pluginKey.tsx +1 −8
  40. apps/cloud/src/secrets-isolation.e2e.node.test.ts +153 −165
  41. apps/cloud/src/services/__test-harness__/api-harness.ts +9 −35
  42. apps/cloud/src/services/db.schema.test.ts +1 −4
  43. apps/cloud/src/services/db.ts +5 −5
  44. apps/cloud/src/services/execution-stack.ts +3 −5
  45. apps/cloud/src/services/executor-schema.ts +430 −327
  46. apps/cloud/src/services/executor.ts +2 −10
  47. apps/cloud/src/services/mcp-oauth.node.test.ts +11 −34
  48. apps/cloud/src/services/mcp-worker-transport.test.ts +1 −3
  49. apps/cloud/src/services/mcp-worker-transport.ts +2 −6
  50. apps/cloud/src/services/slack.ts +8 −18
  51. apps/cloud/src/services/sources-api.node.test.ts +7 −13
  52. apps/cloud/src/services/telemetry.ts +1 −4
  53. apps/cloud/src/test-worker.ts +5 −6
  54. apps/cloud/src/web/pages/onboarding.tsx +4 −8
  55. apps/cloud/vite.config.ts +1 −4
  56. apps/cloud/wrangler.miniflare.jsonc +4 −4
  57. apps/cloud/wrangler.test.jsonc +4 −4
  58. apps/desktop/src/main.ts +10 −7
  59. apps/local/drizzle/meta/0000_snapshot.json +42 −140
  60. apps/local/drizzle/meta/0001_snapshot.json +42 −140
  61. apps/local/drizzle/meta/0002_snapshot.json +46 −153
  62. apps/local/drizzle/meta/0003_snapshot.json +49 −164
  63. apps/local/drizzle/meta/0004_snapshot.json +51 −172
  64. apps/local/drizzle/meta/0005_snapshot.json +51 −172
  65. apps/local/drizzle/meta/0006_snapshot.json +50 −165
  66. apps/local/drizzle/meta/0007_snapshot.json +95 −311
  67. apps/local/src/serve.test.ts +4 −1
  68. apps/local/src/serve.ts +8 −6
  69. apps/local/src/server/config-sync.ts +46 −50
  70. apps/local/src/server/db-upgrade.test.ts +16 −12
  71. apps/local/src/server/db-upgrade.ts +3 −3
  72. apps/local/src/server/executor-schema.ts +479 −370
  73. apps/local/src/server/executor.ts +9 −26
  74. apps/local/src/server/main.ts +5 −3
  75. apps/local/src/server/mcp-oauth.test.ts +19 −60
  76. apps/local/src/server/migrate-connections.test.ts +3 −9
  77. apps/local/src/server/migrate-connections.ts +26 −62
  78. apps/local/src/server/migrate-graphql-bindings.test.ts +12 −23
  79. apps/local/src/server/migrate-openapi-bindings.test.ts +1 −3
  80. apps/local/src/server/observability.ts +1 −1
  81. apps/marketing/src/styles/global.css +3 −2
  82. autumn.config.ts +64 −64
  83. examples/all-plugins/src/main.ts +3 −10
  84. notes/artifacts-workflows-and-generated-ui.md +45 −71
  85. notes/dynamic-plugin-loading-v1.md +177 −187
  86. notes/mcp-conn-pool.md +9 −7
  87. notes/old/connections-migration.md +1 −1
  88. notes/old/error-handling.md +4 −4
  89. notes/old/errored-sources.md +2 −1
  90. notes/old/indexes.md +1 −1
  91. notes/old/mcp-testing.md +6 −6
  92. notes/old/pluggable-storage.md +45 −37
  93. notes/old/promise-sdk-typed-errors.md +3 −1
  94. notes/old/rls.md +3 −3
  95. notes/old/scoped-source-auth.md +2 −7
  96. notes/old/scopes.md +6 −6
  97. notes/old/search.md +6 −2
  98. notes/old/storage-migration.md +23 −15
  99. notes/research/plugins/manifest-schema.md +92 −94
  100. packages/core/api/src/connections/api.ts +6 −15
  101. packages/core/api/src/handlers/connections.ts +27 −30
  102. packages/core/api/src/handlers/executions.ts +46 −42
  103. packages/core/api/src/handlers/oauth.ts +2 −1
  104. packages/core/api/src/handlers/scope.ts +20 −18
  105. packages/core/api/src/handlers/secrets.ts +42 −32
  106. packages/core/api/src/handlers/sources.ts +69 −59
  107. packages/core/api/src/handlers/tools.ts +32 −28
  108. packages/core/api/src/oauth-popup.test.ts +6 −4
  109. packages/core/api/src/oauth-popup.ts +1 −4
  110. packages/core/api/src/oauth/api.ts +1 −3
  111. packages/core/api/src/observability.test.ts +15 −18
  112. packages/core/api/src/observability.ts +22 −33
  113. packages/core/api/src/plugin-routes.ts +18 −16
  114. packages/core/api/src/secrets/api.ts +2 −6
  115. packages/core/api/src/sources/api.ts +2 −8
  116. packages/core/api/src/tools/api.ts +1 −3
  117. packages/core/cli/package.json +1 −1
  118. packages/core/cli/src/commands/generate.ts +4 −18
  119. packages/core/cli/src/generators/drizzle.ts +62 −104
  120. packages/core/cli/src/generators/index.ts +1 −4
  121. packages/core/cli/tsup.config.ts +1 −6
  122. packages/core/config/src/index.ts +1 −5
  123. packages/core/config/src/load-plugins.test.ts +2 −6
  124. packages/core/config/src/load-plugins.ts +3 −8
  125. packages/core/config/src/sink.ts +2 −6
  126. packages/core/execution/src/description.test.ts +32 −42
  127. packages/core/execution/src/description.ts +5 −7
  128. packages/core/execution/src/engine.test.ts +2 −4
  129. packages/core/execution/src/promise.ts +4 −12
  130. packages/core/execution/src/tool-invoker.test.ts +9 −18
  131. packages/core/execution/src/tool-invoker.ts +6 −4
  132. packages/core/sdk/README.md +21 −29
  133. packages/core/sdk/package.json +12 −12
  134. packages/core/sdk/src/blob.test.ts +2 −6
  135. packages/core/sdk/src/blob.ts +4 −16
  136. packages/core/sdk/src/client.ts +8 −20
  137. packages/core/sdk/src/connections.test.ts +358 −422
  138. packages/core/sdk/src/connections.ts +1 −3
  139. packages/core/sdk/src/core-schema.ts +6 −17
  140. packages/core/sdk/src/error-handling.test.ts +4 −11
  141. packages/core/sdk/src/errors.ts +4 −7
  142. packages/core/sdk/src/executor.test.ts +190 −276
  143. packages/core/sdk/src/index.ts +1 −5
  144. packages/core/sdk/src/oauth-discovery.test.ts +34 −66
  145. packages/core/sdk/src/oauth-helpers.test.ts +10 −11
  146. packages/core/sdk/src/oauth-helpers.ts +19 −46
  147. packages/core/sdk/src/oauth-popup-types.ts +1 −3
  148. packages/core/sdk/src/oauth.ts +12 −29
  149. packages/core/sdk/src/plugin.ts +25 −105
  150. packages/core/sdk/src/policies.test.ts +74 −83
  151. packages/core/sdk/src/policies.ts +2 −8
  152. packages/core/sdk/src/promise-executor.ts +22 −33
  153. packages/core/sdk/src/promise.test.ts +2 −4
  154. packages/core/sdk/src/promise.ts +1 −5
  155. packages/core/sdk/src/schema-types.ts +0 −1
  156. packages/core/sdk/src/scoped-adapter.test.ts +2 −6
  157. packages/core/sdk/src/scoped-adapter.ts +7 −34
  158. packages/core/sdk/src/secrets.ts +5 −20
  159. packages/core/sdk/src/testing.ts +8 −11
  160. packages/core/sdk/src/types.ts +1 −3
  161. packages/core/storage-core/LICENSE.md +21 −20
  162. packages/core/storage-core/README.md +1 −5
  163. packages/core/storage-core/package.json +6 −6
  164. packages/core/storage-core/src/adapter.ts +2 −7
  165. packages/core/storage-core/src/errors.ts +1 −3
  166. packages/core/storage-core/src/memory.test.ts +1 −4
  167. packages/core/storage-core/src/schema.ts +10 −28
  168. packages/core/storage-core/src/typed.ts +4 −14
  169. packages/core/storage-drizzle/src/adapter.test.ts +4 −11
  170. packages/core/storage-file/src/adapter.ts +2 −6
  171. packages/core/storage-file/src/blob-store.ts +5 −27
  172. packages/core/storage-file/src/index.test.ts +3 −16
  173. packages/core/storage-postgres/src/adapter.ts +2 −6
  174. packages/core/storage-postgres/src/blob-store.ts +5 −27
  175. packages/core/storage-postgres/src/index.test.ts +3 −8
  176. packages/core/storage-postgres/src/index.ts +1 −4
  177. packages/core/vite-plugin/package.json +3 −3
  178. packages/core/vite-plugin/src/index.ts +8 −23
  179. packages/hosts/mcp/src/stdio-integration.test.ts +35 −33
  180. packages/kernel/core/README.md +1 −5
  181. packages/kernel/core/src/code-recovery.test.ts +6 −13
  182. packages/kernel/core/src/code-recovery.ts +14 −10
  183. packages/kernel/core/src/strip-types.test.ts +4 −2
  184. packages/kernel/runtime-deno-subprocess/src/index.test.ts +19 −17
  185. packages/kernel/runtime-deno-subprocess/src/index.ts +1 −3
  186. packages/kernel/runtime-quickjs/README.md +5 −5
  187. packages/plugins/example/package.json +8 −8
  188. packages/plugins/example/src/client.tsx +2 −8
  189. packages/plugins/example/src/server.ts +1 −7
  190. packages/plugins/google-discovery/src/api/handlers.test.ts +20 −26
  191. packages/plugins/google-discovery/src/api/handlers.ts +4 −5
  192. packages/plugins/google-discovery/src/react/client.ts +3 −4
  193. packages/plugins/google-discovery/src/react/index.ts +1 −4
  194. packages/plugins/google-discovery/src/sdk/document.ts +22 −22
  195. packages/plugins/graphql/src/api/group.ts +6 −13
  196. packages/plugins/graphql/src/api/handlers.ts +50 −62
  197. packages/plugins/graphql/src/react/GraphqlSignInButton.tsx +2 −1
  198. packages/plugins/graphql/src/react/defaults.ts +1 −2
  199. packages/plugins/graphql/src/sdk/introspect.ts +1 −3
  200. packages/plugins/graphql/src/sdk/invoke.ts +1 −2
  201. packages/plugins/graphql/src/sdk/types.ts +7 −9
  202. packages/plugins/keychain/src/index.test.ts +3 −1
  203. packages/plugins/keychain/src/index.ts +24 −37
  204. packages/plugins/mcp/src/api/group.ts +10 −11
  205. packages/plugins/mcp/src/api/handlers.test.ts +16 −18
  206. packages/plugins/mcp/src/api/handlers.ts +3 −2
  207. packages/plugins/mcp/src/react/index.ts +1 −6
  208. packages/plugins/mcp/src/react/source-plugin.tsx +1 −3
  209. packages/plugins/mcp/src/sdk/connection-pool.test.ts +26 −44
  210. packages/plugins/mcp/src/sdk/cross-user-isolation.test.ts +2 −6
  211. packages/plugins/mcp/src/sdk/elicitation.test.ts +1 −2
  212. packages/plugins/mcp/src/sdk/per-user-auth-isolation.test.ts +136 −146
  213. packages/plugins/mcp/src/sdk/probe-shape.test.ts +3 −4
  214. packages/plugins/mcp/src/sdk/probe-shape.ts +1 −2
  215. packages/plugins/mcp/src/sdk/stdio-connector.ts +1 −3
  216. packages/plugins/onepassword/src/api/handlers.ts +38 −26
  217. packages/plugins/openapi/src/api/group.ts +7 −12
  218. packages/plugins/openapi/src/api/handlers.ts +3 −1
  219. packages/plugins/openapi/src/react/AddOpenApiSource.tsx +113 −113
  220. packages/plugins/openapi/src/react/OpenApiSourceSummary.tsx +1 −3
  221. packages/plugins/openapi/src/sdk/client-credentials-oauth.test.ts +43 −37
  222. packages/plugins/openapi/src/sdk/credential-status.test.ts +1 −6
  223. packages/plugins/openapi/src/sdk/credential-status.ts +5 −21
  224. packages/plugins/openapi/src/sdk/extract.ts +2 −6
  225. packages/plugins/openapi/src/sdk/form-urlencoded-body.test.ts +52 −58
  226. packages/plugins/openapi/src/sdk/index.test.ts +4 −3
  227. packages/plugins/openapi/src/sdk/invoke.ts +7 −20
  228. packages/plugins/openapi/src/sdk/multi-scope-bearer.test.ts +406 −424
  229. packages/plugins/openapi/src/sdk/multi-scope-oauth.test.ts +71 −57
  230. packages/plugins/openapi/src/sdk/non-json-body.test.ts +29 −53
  231. packages/plugins/openapi/src/sdk/openapi-utils.ts +2 −7
  232. packages/plugins/openapi/src/sdk/plugin.test.ts +101 −119
  233. packages/plugins/openapi/src/sdk/preview-oauth2.test.ts +1 −3
  234. packages/plugins/openapi/src/sdk/preview.ts +4 −10
  235. packages/plugins/openapi/tsconfig.json +1 −4
  236. packages/plugins/workos-vault/src/react/WorkOSVaultSettings.tsx +2 −2
  237. packages/plugins/workos-vault/src/sdk/plugin.ts +26 −27
  238. packages/plugins/workos-vault/src/sdk/secret-store.test.ts +102 −131
  239. packages/plugins/workos-vault/src/sdk/secret-store.ts +26 −49
  240. packages/plugins/workos-vault/src/sdk/testing.ts +11 −31
  241. packages/react/src/api/reactivity-keys.tsx +2 −8
  242. packages/react/src/components/calendar.tsx +1 −4
  243. packages/react/src/components/chart.tsx +1 −4
  244. packages/react/src/components/code-block.tsx +1 −3
  245. packages/react/src/components/markdown.tsx +3 −1
  246. packages/react/src/components/schema-explorer.tsx +1 −5
  247. packages/react/src/components/tool-tree.tsx +5 −9
  248. packages/react/src/hooks/use-policy-actions.ts +7 −6
  249. packages/react/src/pages/secrets.tsx +3 −13
  250. packages/react/src/plugins/headers-list.tsx +1 −3
  251. packages/react/src/plugins/namespace.ts +1 −3
  252. packages/react/src/plugins/secret-id.test.ts +2 −8
  253. packages/react/src/plugins/secret-id.tsx +1 −4
  254. packages/react/src/plugins/source-identity.tsx +1 −5
  255. scripts/check-release-notes.ts +2 −8
  256. scripts/oxlint-plugin-executor/rules/no-redundant-primitive-cast.js +2 −9
  257. scripts/oxlint-plugin-executor/rules/no-unsupported-effect-api.js +1 −2
  258. scripts/oxlint-plugin-executor/utils.js +3 −3
  259. scripts/publish-packages.ts +2 −6
  260. tests/daemon-bootstrap.test.ts +30 −23
  261. tests/daemon-state.test.ts +2 −1
  262. tests/presets-reachable.test.ts +2 −1
  263. tests/release-bootstrap-smoke.test.ts +118 −144
  264. tests/source-identity.test.ts +1 −4
  265. tests/tools-cli.test.ts +10 −6
.agents/skills/wrdn-effect-atom-optimistic/SKILL.md+10−8
@@ -46,10 +46,10 @@ When the trace cannot resolve with the files at hand, drop the finding. · 45 unmodified lines
46 46
47## Severity ladder47## Severity ladder
48 48
49| Level | Criteria |49| Level | Criteria |
50|-------|----------|50| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51| **medium** | New optimistic-mutation code that bypasses `Atom.optimistic` / `Atom.optimisticFn` and rolls its own pending state. Or a mixed read/write where the read goes through the plain query atom and the write goes through the optimistic mutation. |51| **medium** | New optimistic-mutation code that bypasses `Atom.optimistic` / `Atom.optimisticFn` and rolls its own pending state. Or a mixed read/write where the read goes through the plain query atom and the write goes through the optimistic mutation. |
52| **low** | Subtle defects in an `Atom.optimisticFn` reducer that work today but degrade under racing (clock-based identity, missing `Atom.family` wrapper, computed-once captures of `scopeId`). |52| **low** | Subtle defects in an `Atom.optimisticFn` reducer that work today but degrade under racing (clock-based identity, missing `Atom.family` wrapper, computed-once captures of `scopeId`). |
53 53
54Do not invent `high`. Pick `low` when in doubt and explain why.54Do not invent `high`. Pick `low` when in doubt and explain why.
55 55
@@ -103,9 +103,7 @@ export const updateSecretOptimistic = Atom.family((scopeId: ScopeId) => · 47 unmodified lines
103 Atom.optimisticFn({103 Atom.optimisticFn({
104 reducer: (current, arg: { path: { secretId: SecretId }; payload: { value: string } }) =>104 reducer: (current, arg: { path: { secretId: SecretId }; payload: { value: string } }) =>
105 Result.map(current, (rows) =>105 Result.map(current, (rows) =>
106 rows.map((r) =>106 rows.map((r) => (r.id === arg.path.secretId ? { ...r, value: arg.payload.value } : r)),
107 r.id === arg.path.secretId ? { ...r, value: arg.payload.value } : r,
108 ),
109 ),107 ),
110 fn: updateSecret,108 fn: updateSecret,
111 }),109 }),
@@ -136,8 +134,12 @@ export interface PendingSecret { · 24 unmodified lines
136 readonly value: string;134 readonly value: string;
137}135}
138 136
139export const useSecretsWithPending = (scopeId: ScopeId) => { /* ... */ };137export const useSecretsWithPending = (scopeId: ScopeId) => {
140export const usePendingSecrets = () => { /* ... */ };138 /* ... */
139};
140export const usePendingSecrets = () => {
141 /* ... */
142};
141```143```
142 144
143Flag any new entry in `PendingResource` and any new `use<X>WithPending` / `usePending<X>` hook. The file is closed for new patterns.145Flag any new entry in `PendingResource` and any new `use<X>WithPending` / `usePending<X>` hook. The file is closed for new patterns.
.agents/skills/wrdn-effect-typed-errors/SKILL.md+14−13
@@ -96,7 +96,7 @@ throw new Error("Missing source"); · 95 unmodified lines
96Good in `Effect.gen`:96Good in `Effect.gen`:
97 97
98```ts98```ts
99return yield* new SourceNotFoundError({ sourceId });99return yield * new SourceNotFoundError({ sourceId });
100```100```
101 101
102Good in combinators:102Good in combinators:
@@ -112,13 +112,13 @@ If a third-party interface requires throwing, keep the throw at the adapter edge · 9 unmodified lines
112Prefer yielding the error directly in generator code:112Prefer yielding the error directly in generator code:
113 113
114```ts114```ts
115return yield* new SourceNotFoundError({ sourceId });115return yield * new SourceNotFoundError({ sourceId });
116```116```
117 117
118Do not write:118Do not write:
119 119
120```ts120```ts
121return yield* Effect.fail(new SourceNotFoundError({ sourceId }));121return yield * Effect.fail(new SourceNotFoundError({ sourceId }));
122```122```
123 123
124Use `Effect.fail(...)` in non-generator combinator code:124Use `Effect.fail(...)` in non-generator combinator code:
@@ -167,16 +167,14 @@ Effect.ignore( · 42 unmodified lines
167Bad in domain code:167Bad in domain code:
168 168
169```ts169```ts
170program.pipe(Effect.orDie)170program.pipe(Effect.orDie);
171Effect.die(error)171Effect.die(error);
172```172```
173 173
174Good:174Good:
175 175
176```ts176```ts
177program.pipe(177program.pipe(Effect.mapError((cause) => new DomainError({ message: "Operation failed", cause })));
178 Effect.mapError((cause) => new DomainError({ message: "Operation failed", cause })),
179)
180```178```
181 179
182`Effect.die`, `Effect.dieMessage`, `Effect.orDie`, and `Effect.orDieWith` turn typed failures into defects. Use them only at a true runtime boundary where the host cannot represent typed failures, and keep that usage behind a narrow lint suppression with a `boundary:` reason. Do not use `orDie` to avoid threading an error type through normal Effect code.180`Effect.die`, `Effect.dieMessage`, `Effect.orDie`, and `Effect.orDieWith` turn typed failures into defects. Use them only at a true runtime boundary where the host cannot represent typed failures, and keep that usage behind a narrow lint suppression with a `boundary:` reason. Do not use `orDie` to avoid threading an error type through normal Effect code.
@@ -305,16 +303,19 @@ Bad: · 122 unmodified lines
305const connectionError = (message: string) =>303const connectionError = (message: string) =>
306 new McpConnectionError({ transport: "remote", message });304 new McpConnectionError({ transport: "remote", message });
307 305
308return yield* connectionError("Endpoint URL is required");306return yield * connectionError("Endpoint URL is required");
309```307```
310 308
311Good:309Good:
312 310
313```ts311```ts
314return yield* new McpConnectionError({312return (
315 transport: "remote",313 yield *
316 message: "Endpoint URL is required",314 new McpConnectionError({
317});315 transport: "remote",
316 message: "Endpoint URL is required",
317 })
318);
318```319```
319 320
320Helpers are allowed only when they do real work, such as:321Helpers are allowed only when they do real work, such as:
.skills/cli-release/SKILL.md+9−0
@@ -6,16 +6,19 @@ description: Runbook for releasing the `executor` CLI package (stable and beta). · 5 unmodified lines
6# Executor CLI release runbook6# Executor CLI release runbook
7 7
8## Authoritative doc8## Authoritative doc
9
9`RELEASING.md` at repo root is the source of truth. This skill encodes the owner's preferences on top of it.10`RELEASING.md` at repo root is the source of truth. This skill encodes the owner's preferences on top of it.
10 11
11## What the `executor` CLI actually ships12## What the `executor` CLI actually ships
12 13
13The CLI binary bundles:14The CLI binary bundles:
15
14- `apps/cli/**` — CLI source + daemon16- `apps/cli/**` — CLI source + daemon
15- `apps/local/**` — the web UI (embedded as a virtual module via `apps/cli/src/build.ts:178`) + drizzle migrations (`build.ts:205`)17- `apps/local/**` — the web UI (embedded as a virtual module via `apps/cli/src/build.ts:178`) + drizzle migrations (`build.ts:205`)
16- `packages/**` — `core`, `kernel`, `hosts/mcp`, `runtime-quickjs`, and every plugin under `packages/plugins/**`18- `packages/**` — `core`, `kernel`, `hosts/mcp`, `runtime-quickjs`, and every plugin under `packages/plugins/**`
17 19
18Does **not** ship in the CLI:20Does **not** ship in the CLI:
21
19- `apps/cloud/**` (Cloudflare Workers deployment)22- `apps/cloud/**` (Cloudflare Workers deployment)
20- `apps/marketing/**`, `apps/desktop/**`23- `apps/marketing/**`, `apps/desktop/**`
21- `examples/**`, `tests/**`24- `examples/**`, `tests/**`
@@ -37,6 +40,7 @@ The owner doesn't want GitHub's auto-generated "PR title by @user" list. Release · 15 unmodified lines
37**`apps/cli/release-notes/next.md` is the canonical user-facing changelog.** Per-package workspace `CHANGELOG.md` files are one-line stubs required by `changesets/action@v1` (the GitHub Action wrapping the CLI in `release.yml`) — it reads each bumped package's `CHANGELOG.md` to build the Version Packages PR description and crashes with `ENOENT` if any is missing. The stubs satisfy that read; don't put release content in them.40**`apps/cli/release-notes/next.md` is the canonical user-facing changelog.** Per-package workspace `CHANGELOG.md` files are one-line stubs required by `changesets/action@v1` (the GitHub Action wrapping the CLI in `release.yml`) — it reads each bumped package's `CHANGELOG.md` to build the Version Packages PR description and crashes with `ENOENT` if any is missing. The stubs satisfy that read; don't put release content in them.
38 41
39### How it's wired42### How it's wired
43
40`apps/cli/src/release.ts` reads `apps/cli/release-notes/next.md` and uses44`apps/cli/src/release.ts` reads `apps/cli/release-notes/next.md` and uses
41its contents as the GitHub Release body. If the file is missing or empty,45its contents as the GitHub Release body. If the file is missing or empty,
42falls back to `gh release create --generate-notes`. There's no46falls back to `gh release create --generate-notes`. There's no
@@ -44,7 +48,9 @@ per-version archive in the repo — historical release bodies live on · 1 unmodified line
44GitHub Releases (durable, indexed, linkable).48GitHub Releases (durable, indexed, linkable).
45 49
46### Writing conventions50### Writing conventions
51
47Structure release-notes files as:52Structure release-notes files as:
53
48```54```
49## Highlights55## Highlights
50### <user-facing story> # e.g. "Per-user OAuth for OpenAPI and MCP sources"56### <user-facing story> # e.g. "Per-user OAuth for OpenAPI and MCP sources"
@@ -74,17 +80,20 @@ External contributor bullets end with `Thanks @<user> (#PR)`: · 23 unmodified lines
74Do not `Thanks` maintainers, bots, or the repo owner — the lint script rejects `@claude`, `@anthropic`, `@github-actions`, `@dependabot`, `@renovate`, `@rhyssullivan`, `@rhys-sullivan`. Run `bun run lint:release-notes` before pushing notes.80Do not `Thanks` maintainers, bots, or the repo owner — the lint script rejects `@claude`, `@anthropic`, `@github-actions`, `@dependabot`, `@renovate`, `@rhyssullivan`, `@rhys-sullivan`. Run `bun run lint:release-notes` before pushing notes.
75 81
76### When drafting from `git log`82### When drafting from `git log`
83
77- Look at `git diff v<last>..HEAD -- README.md` first — it's the best single view of user-facing changes.84- Look at `git diff v<last>..HEAD -- README.md` first — it's the best single view of user-facing changes.
78- Read commit messages in bulk (`git log --oneline v<last>..HEAD -- apps/cli apps/local packages`), then bucket by theme before writing prose.85- Read commit messages in bulk (`git log --oneline v<last>..HEAD -- apps/cli apps/local packages`), then bucket by theme before writing prose.
79- Don't list every commit. Merge PRs and refactor-chain commits into one line.86- Don't list every commit. Merge PRs and refactor-chain commits into one line.
80 87
81### Pairing with changesets88### Pairing with changesets
89
82- A `.changeset/*.md` describes the version bump (semver level + a short summary for the Version Packages PR description). It is **not** the user-facing changelog.90- A `.changeset/*.md` describes the version bump (semver level + a short summary for the Version Packages PR description). It is **not** the user-facing changelog.
83- If your PR adds a `.changeset/*.md` for the `executor` package, also edit `apps/cli/release-notes/next.md` for the user-facing story. They have different audiences.91- If your PR adds a `.changeset/*.md` for the `executor` package, also edit `apps/cli/release-notes/next.md` for the user-facing story. They have different audiences.
84- The `.changeset/*.md` body can be a one-liner pointing at the release-notes section it expands; users read the GitHub release body, not the changeset.92- The `.changeset/*.md` body can be a one-liner pointing at the release-notes section it expands; users read the GitHub release body, not the changeset.
85- Frontmatter is `"executor": patch` (or `minor`/`major` if owner says so).93- Frontmatter is `"executor": patch` (or `minor`/`major` if owner says so).
86 94
87### Starting a new release cycle95### Starting a new release cycle
96
88There's no post-release rename step. When you start work on the next97There's no post-release rename step. When you start work on the next
89release, replace the existing `next.md` content with new entries — the98release, replace the existing `next.md` content with new entries — the
90previous cycle's content is already preserved on the matching `vX.Y.Z`99previous cycle's content is already preserved on the matching `vX.Y.Z`
.skills/effect-atom-optimistic-updates/SKILL.md+4−10
@@ -16,7 +16,7 @@ sources/connections. · 15 unmodified lines
16 16
17- A list query atom (`Atom.optimistic` wraps the query)17- A list query atom (`Atom.optimistic` wraps the query)
18- Plus one or more mutation atoms that change rows in that list (create / update / delete)18- Plus one or more mutation atoms that change rows in that list (create / update / delete)
19- And you want the UI to reflect the change *immediately* on click, before the server roundtrip19- And you want the UI to reflect the change _immediately_ on click, before the server roundtrip
20 20
21If you only need the UI to be "eventually consistent" (i.e. you're fine waiting21If you only need the UI to be "eventually consistent" (i.e. you're fine waiting
22~200ms for the server response and the existing reactivity refetch), skip22~200ms for the server response and the existing reactivity refetch), skip
@@ -109,10 +109,7 @@ existing `reactivityKeys` plumbing still applies — pass them in the call. · 86 unmodified lines
109```typescript109```typescript
110import { useAtomValue, useAtomSet } from "@effect-atom/atom-react";110import { useAtomValue, useAtomSet } from "@effect-atom/atom-react";
111 111
112import {112import { policiesOptimisticAtom, updatePolicyOptimistic } from "../api/atoms";
113 policiesOptimisticAtom,
114 updatePolicyOptimistic,
115} from "../api/atoms";
116import { policyWriteKeys } from "../api/reactivity-keys";113import { policyWriteKeys } from "../api/reactivity-keys";
117 114
118export function PoliciesPage() {115export function PoliciesPage() {
@@ -203,10 +200,7 @@ export const removePolicyOptimistic = Atom.family((scopeId: ScopeId) => · 84 unmodified lines
203 path: { scopeId: ScopeId; policyId: PolicyId };200 path: { scopeId: ScopeId; policyId: PolicyId };
204 reactivityKeys?: ReadonlyArray<unknown>;201 reactivityKeys?: ReadonlyArray<unknown>;
205 },202 },
206 ) =>203 ) => Result.map(current, (rows) => rows.filter((r) => r.id !== arg.path.policyId)),
207 Result.map(current, (rows) =>
208 rows.filter((r) => r.id !== arg.path.policyId),
209 ),
210 fn: removePolicy,204 fn: removePolicy,
211 }),205 }),
212 ),206 ),
@@ -244,7 +238,7 @@ read, optimistic create/update/remove, no custom state. · 31 unmodified lines
244 238
245## When you must NOT use this239## When you must NOT use this
246 240
247- The mutation has cross-cutting effects on data that *isn't* in the same list241- The mutation has cross-cutting effects on data that _isn't_ in the same list
248 (e.g. a single mutation invalidates `tools` AND `policies`). Reactivity keys242 (e.g. a single mutation invalidates `tools` AND `policies`). Reactivity keys
249 still handle that — optimistic only paints the list-local change.243 still handle that — optimistic only paints the list-local change.
250- You need to show transient UI state that isn't a row property (toasts,244- You need to show transient UI state that isn't a row property (toasts,
.skills/graphite/SKILL.md+36−6
@@ -9,12 +9,12 @@ description: "Manage Git branches and PRs using Graphite CLI (gt) instead of raw · 8 unmodified lines
9 9
10Use `gt` commands instead of their `git` counterparts for commit and branch management. Raw git bypasses Graphite's stack metadata, leaving descendants un-restacked and the CLI's view of the stack out of sync.10Use `gt` commands instead of their `git` counterparts for commit and branch management. Raw git bypasses Graphite's stack metadata, leaving descendants un-restacked and the CLI's view of the stack out of sync.
11 11
12| Instead of | Use |12| Instead of | Use |
13|------------|-----|13| ------------------------ | ----------------------- |
14| `git checkout -b <name>` | `gt create <name>` |14| `git checkout -b <name>` | `gt create <name>` |
15| `git commit -m "msg"` | `gt modify -c -m "msg"` |15| `git commit -m "msg"` | `gt modify -c -m "msg"` |
16| `git commit --amend` | `gt modify --amend` |16| `git commit --amend` | `gt modify --amend` |
17| `git commit -am "msg"` | `gt modify -cam "msg"` |17| `git commit -am "msg"` | `gt modify -cam "msg"` |
18 18
19Why: `gt` keeps stack metadata consistent and auto-restacks descendant branches when a parent changes.19Why: `gt` keeps stack metadata consistent and auto-restacks descendant branches when a parent changes.
20 20
@@ -54,6 +54,7 @@ To bail out: `gt abort`. · 33 unmodified lines
54Create a new branch stacked on current branch with staged changes.54Create a new branch stacked on current branch with staged changes.
55 55
56**Flags:**56**Flags:**
57
57- `-a, --all` - Stage all unstaged changes including untracked files58- `-a, --all` - Stage all unstaged changes including untracked files
58- `-m, --message` - Commit message59- `-m, --message` - Commit message
59- `-i, --insert` - Insert between current branch and its child60- `-i, --insert` - Insert between current branch and its child
@@ -67,6 +68,7 @@ Create a new branch stacked on current branch with staged changes. · 7 unmodified lines
67Amend current branch's commit or create new commit. Auto-restacks descendants.68Amend current branch's commit or create new commit. Auto-restacks descendants.
68 69
69**Flags:**70**Flags:**
71
70- `-a, --all` - Stage all changes72- `-a, --all` - Stage all changes
71- `-c, --commit` - Create new commit instead of amending73- `-c, --commit` - Create new commit instead of amending
72- `-e, --edit` - Open editor for commit message74- `-e, --edit` - Open editor for commit message
@@ -82,6 +84,7 @@ Amend current branch's commit or create new commit. Auto-restacks descendants. · 9 unmodified lines
82Amend staged changes to relevant commits in current stack.84Amend staged changes to relevant commits in current stack.
83 85
84**Flags:**86**Flags:**
87
85- `-a, --all` - Stage all unstaged changes (not untracked)88- `-a, --all` - Stage all unstaged changes (not untracked)
86- `-d, --dry-run` - Print what would happen89- `-d, --dry-run` - Print what would happen
87- `-f, --force` - Skip confirmation90- `-f, --force` - Skip confirmation
@@ -92,6 +95,7 @@ Amend staged changes to relevant commits in current stack. · 4 unmodified lines
92Squash all commits in current branch into single commit.95Squash all commits in current branch into single commit.
93 96
94**Flags:**97**Flags:**
98
95- `-m, --message` - Commit message99- `-m, --message` - Commit message
96- `--edit` - Modify existing message100- `--edit` - Modify existing message
97- `-n, --no-edit` - Keep existing message101- `-n, --no-edit` - Keep existing message
@@ -101,6 +105,7 @@ Squash all commits in current branch into single commit. · 3 unmodified lines
101Fold branch's changes into parent, restack descendants.105Fold branch's changes into parent, restack descendants.
102 106
103**Flags:**107**Flags:**
108
104- `-k, --keep` - Keep current branch name instead of parent's109- `-k, --keep` - Keep current branch name instead of parent's
105 110
106### gt split111### gt split
@@ -108,6 +113,7 @@ Fold branch's changes into parent, restack descendants. · 1 unmodified line
108Split current branch into multiple branches.113Split current branch into multiple branches.
109 114
110**Flags:**115**Flags:**
116
111- `-c, --commit, --by-commit` - Split by commit history117- `-c, --commit, --by-commit` - Split by commit history
112- `-h, --hunk, --by-hunk` - Split by hunk interactively118- `-h, --hunk, --by-hunk` - Split by hunk interactively
113- `-f, --file, --by-file` - Split files matching pathspecs119- `-f, --file, --by-file` - Split files matching pathspecs
@@ -119,6 +125,7 @@ Split current branch into multiple branches. · 5 unmodified lines
119Push branches and create/update PRs on GitHub.125Push branches and create/update PRs on GitHub.
120 126
121**Flags:**127**Flags:**
128
122- `-s, --stack` - Include descendant branches129- `-s, --stack` - Include descendant branches
123- `-d, --draft` - Create PRs as drafts130- `-d, --draft` - Create PRs as drafts
124- `-p, --publish` - Publish draft PRs131- `-p, --publish` - Publish draft PRs
@@ -148,6 +155,7 @@ Push branches and create/update PRs on GitHub. · 23 unmodified lines
148Merge PRs from trunk to current branch via Graphite.155Merge PRs from trunk to current branch via Graphite.
149 156
150**Flags:**157**Flags:**
158
151- `-c, --confirm` - Confirm before merging159- `-c, --confirm` - Confirm before merging
152- `--dry-run` - Report what would merge160- `--dry-run` - Report what would merge
153 161
@@ -158,6 +166,7 @@ Merge PRs from trunk to current branch via Graphite. · 4 unmodified lines
158Switch to branch. Interactive selector if no branch provided.166Switch to branch. Interactive selector if no branch provided.
159 167
160**Flags:**168**Flags:**
169
161- `-a, --all` - Show all trunks in selection170- `-a, --all` - Show all trunks in selection
162- `-u, --show-untracked` - Include untracked branches171- `-u, --show-untracked` - Include untracked branches
163- `-s, --stack` - Only show current stack172- `-s, --stack` - Only show current stack
@@ -168,6 +177,7 @@ Switch to branch. Interactive selector if no branch provided. · 4 unmodified lines
168Switch to child of current branch.177Switch to child of current branch.
169 178
170**Flags:**179**Flags:**
180
171- `-n, --steps` - Levels to traverse181- `-n, --steps` - Levels to traverse
172- `--to` - Target branch to navigate toward182- `--to` - Target branch to navigate toward
173 183
@@ -176,6 +186,7 @@ Switch to child of current branch. · 2 unmodified lines
176Switch to parent of current branch.186Switch to parent of current branch.
177 187
178**Flags:**188**Flags:**
189
179- `-n, --steps` - Levels to traverse190- `-n, --steps` - Levels to traverse
180 191
181### gt top192### gt top
@@ -193,6 +204,7 @@ Switch to branch closest to trunk in current stack. · 11 unmodified lines
193Ensure each branch has parent in Git history, rebasing if needed.204Ensure each branch has parent in Git history, rebasing if needed.
194 205
195**Flags:**206**Flags:**
207
196- `--branch` - Run from specified branch208- `--branch` - Run from specified branch
197- `--downstack` - Only restack branch and ancestors209- `--downstack` - Only restack branch and ancestors
198- `--upstack` - Only restack branch and descendants210- `--upstack` - Only restack branch and descendants
@@ -203,6 +215,7 @@ Ensure each branch has parent in Git history, rebasing if needed. · 4 unmodified lines
203Rebase current branch onto target, restack descendants.215Rebase current branch onto target, restack descendants.
204 216
205**Flags:**217**Flags:**
218
206- `-o, --onto` - Target branch219- `-o, --onto` - Target branch
207- `--source` - Branch to move (default: current)220- `--source` - Branch to move (default: current)
208- `-a, --all` - Show all trunks in selection221- `-a, --all` - Show all trunks in selection
@@ -218,6 +231,7 @@ Reorder branches between trunk and current via editor. · 9 unmodified lines
218Sync all branches with remote, clean merged/closed PRs, restack.231Sync all branches with remote, clean merged/closed PRs, restack.
219 232
220**Flags:**233**Flags:**
234
221- `-a, --all` - Sync across all trunks235- `-a, --all` - Sync across all trunks
222- `-f, --force` - Skip confirmations236- `-f, --force` - Skip confirmations
223- `--restack` / `--no-restack` - Restack after sync (default: true)237- `--restack` / `--no-restack` - Restack after sync (default: true)
@@ -227,6 +241,7 @@ Sync all branches with remote, clean merged/closed PRs, restack. · 3 unmodified lines
227Sync branches from trunk to specified branch/PR from remote.241Sync branches from trunk to specified branch/PR from remote.
228 242
229**Flags:**243**Flags:**
244
230- `-d, --downstack` - Don't sync upstack branches245- `-d, --downstack` - Don't sync upstack branches
231- `-f, --force` - Overwrite with remote246- `-f, --force` - Overwrite with remote
232- `--restack` / `--no-restack` - Restack after (default: true)247- `--restack` / `--no-restack` - Restack after (default: true)
@@ -239,6 +254,7 @@ Sync branches from trunk to specified branch/PR from remote. · 6 unmodified lines
239Delete branch and Graphite metadata. Children restack to parent.254Delete branch and Graphite metadata. Children restack to parent.
240 255
241**Flags:**256**Flags:**
257
242- `-f, --force` - Delete even if not merged/closed258- `-f, --force` - Delete even if not merged/closed
243- `-c, --close` - Close associated PR on GitHub259- `-c, --close` - Close associated PR on GitHub
244- `--downstack` - Also delete ancestors260- `--downstack` - Also delete ancestors
@@ -249,6 +265,7 @@ Delete branch and Graphite metadata. Children restack to parent. · 4 unmodified lines
249Rename branch. Removes PR association.265Rename branch. Removes PR association.
250 266
251**Flags:**267**Flags:**
268
252- `-f, --force` - Allow rename with open PR269- `-f, --force` - Allow rename with open PR
253 270
254### gt track [branch]271### gt track [branch]
@@ -256,6 +273,7 @@ Rename branch. Removes PR association. · 1 unmodified line
256Start tracking branch with Graphite by selecting parent.273Start tracking branch with Graphite by selecting parent.
257 274
258**Flags:**275**Flags:**
276
259- `-f, --force` - Use most recent tracked ancestor as parent277- `-f, --force` - Use most recent tracked ancestor as parent
260- `-p, --parent` - Specify parent branch278- `-p, --parent` - Specify parent branch
261 279
@@ -264,6 +282,7 @@ Start tracking branch with Graphite by selecting parent. · 2 unmodified lines
264Stop tracking branch. Children also untracked.282Stop tracking branch. Children also untracked.
265 283
266**Flags:**284**Flags:**
285
267- `-f, --force` - Skip confirmation286- `-f, --force` - Skip confirmation
268 287
269### gt pop288### gt pop
@@ -285,6 +304,7 @@ Unfreeze branch and upstack. · 15 unmodified lines
285Show stacks. Forms: `gt log`, `gt log short`, `gt log long`.304Show stacks. Forms: `gt log`, `gt log short`, `gt log long`.
286 305
287**Flags:**306**Flags:**
307
288- `-a, --all` - Show all trunks308- `-a, --all` - Show all trunks
289- `-r, --reverse` - Print upside down309- `-r, --reverse` - Print upside down
290- `-u, --show-untracked` - Include untracked310- `-u, --show-untracked` - Include untracked
@@ -297,6 +317,7 @@ Show stacks. Forms: `gt log`, `gt log short`, `gt log long`. · 6 unmodified lines
297Display branch information.317Display branch information.
298 318
299**Flags:**319**Flags:**
320
300- `-b, --body` - Show PR body321- `-b, --body` - Show PR body
301- `-d, --diff` - Show diff vs parent322- `-d, --diff` - Show diff vs parent
302- `-p, --patch` - Show commit changes323- `-p, --patch` - Show commit changes
@@ -307,6 +328,7 @@ Display branch information. · 4 unmodified lines
307Show parent, children, or trunk of current branch.328Show parent, children, or trunk of current branch.
308 329
309`gt trunk` flags:330`gt trunk` flags:
331
310- `--add` - Add trunk332- `--add` - Add trunk
311- `-a, --all` - Show all trunks333- `-a, --all` - Show all trunks
312 334
@@ -315,6 +337,7 @@ Show parent, children, or trunk of current branch. · 2 unmodified lines
315Open PR page for branch or PR number.337Open PR page for branch or PR number.
316 338
317**Flags:**339**Flags:**
340
318- `--stack` - Open stack page341- `--stack` - Open stack page
319 342
320## Conflict Resolution343## Conflict Resolution
@@ -324,6 +347,7 @@ Open PR page for branch or PR number. · 3 unmodified lines
324Continue command halted by rebase conflict.347Continue command halted by rebase conflict.
325 348
326**Flags:**349**Flags:**
350
327- `-a, --all` - Stage all changes first351- `-a, --all` - Stage all changes first
328 352
329### gt abort353### gt abort
@@ -331,6 +355,7 @@ Continue command halted by rebase conflict. · 1 unmodified line
331Abort current command halted by conflict.355Abort current command halted by conflict.
332 356
333**Flags:**357**Flags:**
358
334- `-f, --force` - Skip confirmation359- `-f, --force` - Skip confirmation
335 360
336### gt undo361### gt undo
@@ -338,6 +363,7 @@ Abort current command halted by conflict. · 1 unmodified line
338Undo most recent Graphite mutations.363Undo most recent Graphite mutations.
339 364
340**Flags:**365**Flags:**
366
341- `-f, --force` - Skip confirmation367- `-f, --force` - Skip confirmation
342 368
343## Setup369## Setup
@@ -347,6 +373,7 @@ Undo most recent Graphite mutations. · 3 unmodified lines
347Initialize Graphite by selecting trunk branch.373Initialize Graphite by selecting trunk branch.
348 374
349**Flags:**375**Flags:**
376
350- `--trunk` - Trunk branch name377- `--trunk` - Trunk branch name
351- `--reset` - Untrack all branches378- `--reset` - Untrack all branches
352 379
@@ -355,6 +382,7 @@ Initialize Graphite by selecting trunk branch. · 2 unmodified lines
355Add auth token for GitHub.382Add auth token for GitHub.
356 383
357**Flags:**384**Flags:**
385
358- `-t, --token` - Auth token from https://app.graphite.com/activate386- `-t, --token` - Auth token from https://app.graphite.com/activate
359 387
360### gt config388### gt config
@@ -368,6 +396,7 @@ Update CLI to latest stable version. · 7 unmodified lines
368## Git Passthroughs396## Git Passthroughs
369 397
370These pass arguments directly to git:398These pass arguments directly to git:
399
371- `gt add [args..]`400- `gt add [args..]`
372- `gt cherry-pick [args..]`401- `gt cherry-pick [args..]`
373- `gt rebase [args..]`402- `gt rebase [args..]`
@@ -379,4 +408,5 @@ These pass arguments directly to git: · 5 unmodified lines
379Create branch reverting a trunk commit.408Create branch reverting a trunk commit.
380 409
381**Flags:**410**Flags:**
411
382- `-e, --edit` - Edit commit message412- `-e, --edit` - Edit commit message
RELEASING.md+6−2
@@ -74,14 +74,18 @@ Use this section structure (mirrors what's already in `next.md`): · 73 unmodified lines
74 74
75```markdown75```markdown
76## Highlights76## Highlights
77
77### <user-facing story>78### <user-facing story>
78 bullets of concrete user value79
80bullets of concrete user value
79 81
80## Fixes82## Fixes
81 83
82## Breaking changes84## Breaking changes
85
83### <specific surface>86### <specific surface>
84 before / after code blocks for migrations87
88before / after code blocks for migrations
85```89```
86 90
87Lead with **user-visible stories**, not commit subjects. Group related91Lead with **user-visible stories**, not commit subjects. Group related
apps/cli/release-notes/next.md+15−0
@@ -1,17 +1,21 @@
1## Highlights1## Highlights
2 2
3### Install paths fixed3### Install paths fixed
4
4`npm i -g executor` and `bun i -g executor` both work cleanly on a fresh machine. For machines without node, `curl … install.sh | bash` does the same thing.5`npm i -g executor` and `bun i -g executor` both work cleanly on a fresh machine. For machines without node, `curl … install.sh | bash` does the same thing.
5 6
6> 1.4.13 and 1.4.14 were partial releases that didn't make it to npm cleanly. If you tried `npm i -g executor@latest` and got an older version, that's why. 1.4.15 is the first complete release of the new packaging.7> 1.4.13 and 1.4.14 were partial releases that didn't make it to npm cleanly. If you tried `npm i -g executor@latest` and got an older version, that's why. 1.4.15 is the first complete release of the new packaging.
7 8
8### MCP sources honor upstream `destructiveHint`9### MCP sources honor upstream `destructiveHint`
10
9MCP sources now read `destructiveHint` from upstream tool annotations. Tools marked destructive will require approval before running, surfaced via MCP elicitation. Refresh existing sources (or remove + re-add) to pick up annotations on tools added before this change.11MCP sources now read `destructiveHint` from upstream tool annotations. Tools marked destructive will require approval before running, surfaced via MCP elicitation. Refresh existing sources (or remove + re-add) to pick up annotations on tools added before this change.
10 12
11### Set tool policies from the Tools page13### Set tool policies from the Tools page
14
12The local UI gains a **Policies** tab for managing approval rules, plus a per-row action menu on the Tools tree. Hover any tool or category and pick **Always run / Require approval / Block / Clear** — leaf rows save a rule for the exact tool id, group rows save a `prefix.*` wildcard. New rules are auto-placed by specificity so a freshly-added group rule never silently shadows an existing leaf rule. The same menu is available from the tool detail header and from any source-detail page.15The local UI gains a **Policies** tab for managing approval rules, plus a per-row action menu on the Tools tree. Hover any tool or category and pick **Always run / Require approval / Block / Clear** — leaf rows save a rule for the exact tool id, group rows save a `prefix.*` wildcard. New rules are auto-placed by specificity so a freshly-added group rule never silently shadows an existing leaf rule. The same menu is available from the tool detail header and from any source-detail page.
13 16
14### Per-user OAuth for OpenAPI and MCP sources17### Per-user OAuth for OpenAPI and MCP sources
18
15OpenAPI and MCP sources now carry first-class **Connections** — a per-user sign-in state decoupled from the source definition itself.19OpenAPI and MCP sources now carry first-class **Connections** — a per-user sign-in state decoupled from the source definition itself.
16 20
17- Save an OAuth2 OpenAPI or MCP source **before** signing in; users sign in later from the source page.21- Save an OAuth2 OpenAPI or MCP source **before** signing in; users sign in later from the source page.
@@ -20,15 +24,18 @@ OpenAPI and MCP sources now carry first-class **Connections** — a per-user sig · 2 unmodified lines
20- Existing OpenAPI + MCP + google-discovery OAuth rows migrate into Connections automatically on first launch — no user action required.24- Existing OpenAPI + MCP + google-discovery OAuth rows migrate into Connections automatically on first launch — no user action required.
21 25
22### OpenAPI: client-credentials, non-JSON bodies, source refresh26### OpenAPI: client-credentials, non-JSON bodies, source refresh
27
23- Full **OAuth2 client-credentials** flow end-to-end.28- Full **OAuth2 client-credentials** flow end-to-end.
24- **Non-JSON request bodies** dispatch correctly by content type; Executor honors OAS3 `encoding` and multi-content operations, and lets the caller pick which content type to send.29- **Non-JSON request bodies** dispatch correctly by content type; Executor honors OAS3 `encoding` and multi-content operations, and lets the caller pick which content type to send.
25- Relative OAuth2 URLs resolve against the source's `baseUrl`.30- Relative OAuth2 URLs resolve against the source's `baseUrl`.
26- Refresh a source by re-fetching its origin URL from the edit page.31- Refresh a source by re-fetching its origin URL from the edit page.
27 32
28### Layered scope isolation33### Layered scope isolation
34
29Multi-tenant deployments get a proper security primitive. Every read and write now passes through a layered `ScopeStack`, with the write scope declared explicitly. Plugins have adopted the API; the UI exposes it via `CreatableSecretPicker`; and WorkOS sources enforce tenant-ownership on every access. Per-scope blob and secret lookups are batched into single `IN` queries, so the extra check doesn't cost a round-trip.35Multi-tenant deployments get a proper security primitive. Every read and write now passes through a layered `ScopeStack`, with the write scope declared explicitly. Plugins have adopted the API; the UI exposes it via `CreatableSecretPicker`; and WorkOS sources enforce tenant-ownership on every access. Per-scope blob and secret lookups are batched into single `IN` queries, so the extra check doesn't cost a round-trip.
30 36
31### Natural CLI for tool discovery and invocation37### Natural CLI for tool discovery and invocation
38
32Call tools by path instead of writing TypeScript:39Call tools by path instead of writing TypeScript:
33 40
34```bash41```bash
@@ -41,6 +48,7 @@ executor tools describe github.issues.create · 6 unmodified lines
41`executor call <path> --help` browses namespaces → resources → methods, with `--match <text> --limit <n>` to narrow huge namespaces. Errors are normalized for agent consumption, and the resume / help UX is cleaner for non-interactive flows.48`executor call <path> --help` browses namespaces → resources → methods, with `--match <text> --limit <n>` to narrow huge namespaces. Errors are normalized for agent consumption, and the resume / help UX is cleaner for non-interactive flows.
42 49
43### Daemon lifecycle50### Daemon lifecycle
51
44```bash52```bash
45executor daemon run53executor daemon run
46executor daemon status54executor daemon status
@@ -53,16 +61,20 @@ executor daemon restart · 6 unmodified lines
53`executor daemon run` now backgrounds by default. Pass `--foreground` to keep it attached for log inspection.61`executor daemon run` now backgrounds by default. Pass `--foreground` to keep it attached for log inspection.
54 62
55### OpenTelemetry everywhere63### OpenTelemetry everywhere
64
56Tool dispatch, plugins, storage, schema, and transport are now fully instrumented with OTEL spans, and the runtime is threaded through dispatch so spans actually export in all runtimes.65Tool dispatch, plugins, storage, schema, and transport are now fully instrumented with OTEL spans, and the runtime is threaded through dispatch so spans actually export in all runtimes.
57 66
58## New presets67## New presets
68
59- **Notion** is now a featured MCP preset.69- **Notion** is now a featured MCP preset.
60 70
61## Performance71## Performance
72
62- `buildExecuteDescription` no longer calls `executor.tools.list`, making tool-description generation measurably faster on large workspaces.73- `buildExecuteDescription` no longer calls `executor.tools.list`, making tool-description generation measurably faster on large workspaces.
63- Per-scope blob and secret lookups now use a single `IN` query instead of N per-scope round-trips.74- Per-scope blob and secret lookups now use a single `IN` query instead of N per-scope round-trips.
64 75
65## Fixes76## Fixes
77
66- Upgrade: preserve legacy OAuth connection backfills after the `connection.kind` column is removed.78- Upgrade: preserve legacy OAuth connection backfills after the `connection.kind` column is removed.
67- OpenAPI: refreshing or editing sources with legacy inline secret/OAuth config now materializes the new source binding rows instead of dropping credentials.79- OpenAPI: refreshing or editing sources with legacy inline secret/OAuth config now materializes the new source binding rows instead of dropping credentials.
68- Keychain: skip provider registration when the OS backend is unreachable (no more startup failure when running headless on Linux without a keyring).80- Keychain: skip provider registration when the OS backend is unreachable (no more startup failure when running headless on Linux without a keyring).
@@ -72,6 +84,7 @@ Tool dispatch, plugins, storage, schema, and transport are now fully instrumente · 3 unmodified lines
72## Breaking changes84## Breaking changes
73 85
74### `executor call` no longer accepts inline code86### `executor call` no longer accepts inline code
87
75The old TypeScript-as-argument forms are gone:88The old TypeScript-as-argument forms are gone:
76 89
77```bash90```bash
@@ -92,6 +105,7 @@ executor call github issues list '{"owner":"octocat","repo":"Hello-World"}' · 14 unmodified lines
92`tools.discover(...)` becomes `executor tools search "<query>"`.105`tools.discover(...)` becomes `executor tools search "<query>"`.
93 106
94### `sources.add` CLI form simplified107### `sources.add` CLI form simplified
108
95Use the dedicated tool:109Use the dedicated tool:
96 110
97```bash111```bash
@@ -105,4 +119,5 @@ executor call openapi addSource '{ · 7 unmodified lines
105Pass `baseUrl` when the OpenAPI document has relative `servers` entries.119Pass `baseUrl` when the OpenAPI document has relative `servers` entries.
106 120
107### SDK: layered scope121### SDK: layered scope
122
108Every SDK write now takes an explicit scope. If you have plugins or host code calling the SDK directly, they'll need to adopt the new layered-scope API (see the in-tree plugins for reference — they've all been migrated). This does not affect users of the CLI or web UI.123Every SDK write now takes an explicit scope. If you have plugins or host code calling the SDK directly, they'll need to adopt the new layered-scope API (see the in-tree plugins for reference — they've all been migrated). This does not affect users of the CLI or web UI.
apps/cli/src/build.ts+15−16
@@ -68,8 +68,7 @@ const platformName = (t: Target) => (t.os === "win32" ? "windows" : t.os); · 67 unmodified lines
68/** Per-platform suffix used in dist directory names, npm dist-tags, and the68/** Per-platform suffix used in dist directory names, npm dist-tags, and the
69 * semver prerelease segment of variant package versions. e.g. "linux-x64",69 * semver prerelease segment of variant package versions. e.g. "linux-x64",
70 * "linux-x64-musl", "darwin-arm64". */70 * "linux-x64-musl", "darwin-arm64". */
71const platformTag = (t: Target) =>71const platformTag = (t: Target) => [platformName(t), t.arch, t.abi].filter(Boolean).join("-");
72 [platformName(t), t.arch, t.abi].filter(Boolean).join("-");
73 72
74/** Dist directory name (e.g. dist/executor-linux-x64). Only used as a build73/** Dist directory name (e.g. dist/executor-linux-x64). Only used as a build
75 * artifact convention; the actual npm package name inside is `executor`. */74 * artifact convention; the actual npm package name inside is `executor`. */
@@ -126,7 +125,10 @@ const resolveKeyringNative = (t: Target): string | null => { · 50 unmodified lines
126 const platformMap: Record<string, { pkg: string; node: string }> = {125 const platformMap: Record<string, { pkg: string; node: string }> = {
127 "darwin-arm64": { pkg: "@napi-rs/keyring-darwin-arm64", node: "keyring.darwin-arm64.node" },126 "darwin-arm64": { pkg: "@napi-rs/keyring-darwin-arm64", node: "keyring.darwin-arm64.node" },
128 "darwin-x64": { pkg: "@napi-rs/keyring-darwin-x64", node: "keyring.darwin-x64.node" },127 "darwin-x64": { pkg: "@napi-rs/keyring-darwin-x64", node: "keyring.darwin-x64.node" },
129 "linux-arm64": { pkg: "@napi-rs/keyring-linux-arm64-gnu", node: "keyring.linux-arm64-gnu.node" },128 "linux-arm64": {
129 pkg: "@napi-rs/keyring-linux-arm64-gnu",
130 node: "keyring.linux-arm64-gnu.node",
131 },
130 "linux-x64": { pkg: "@napi-rs/keyring-linux-x64-gnu", node: "keyring.linux-x64-gnu.node" },132 "linux-x64": { pkg: "@napi-rs/keyring-linux-x64-gnu", node: "keyring.linux-x64-gnu.node" },
131 "linux-arm64-musl": {133 "linux-arm64-musl": {
132 pkg: "@napi-rs/keyring-linux-arm64-musl",134 pkg: "@napi-rs/keyring-linux-arm64-musl",
@@ -146,9 +148,7 @@ const resolveKeyringNative = (t: Target): string | null => { · 13 unmodified lines
146 const entry = platformMap[key];148 const entry = platformMap[key];
147 if (!entry) return null;149 if (!entry) return null;
148 try {150 try {
149 const req = createRequire(151 const req = createRequire(join(repoRoot, "node_modules", "@napi-rs/keyring", "package.json"));
150 join(repoRoot, "node_modules", "@napi-rs/keyring", "package.json"),
151 );
152 const pkgJson = req.resolve(`${entry.pkg}/package.json`);152 const pkgJson = req.resolve(`${entry.pkg}/package.json`);
153 return join(dirname(pkgJson), entry.node);153 return join(dirname(pkgJson), entry.node);
154 } catch {154 } catch {
@@ -455,7 +455,10 @@ const buildPreviewTarballs = async (binaries: Record<string, string>) => { · 300 unmodified lines
455 455
456const resolveTargetsFromEnv = (env: string | undefined): Target[] => {456const resolveTargetsFromEnv = (env: string | undefined): Target[] => {
457 if (!env) throw new Error("EXECUTOR_PREVIEW_TARGETS must be set (comma-separated package names)");457 if (!env) throw new Error("EXECUTOR_PREVIEW_TARGETS must be set (comma-separated package names)");
458 const names = env.split(",").map((s) => s.trim()).filter(Boolean);458 const names = env
459 .split(",")
460 .map((s) => s.trim())
461 .filter(Boolean);
459 const resolved = names.map((name) => {462 const resolved = names.map((name) => {
460 const match = ALL_TARGETS.find((t) => targetPackageName(t) === name);463 const match = ALL_TARGETS.find((t) => targetPackageName(t) === name);
461 if (!match) throw new Error(`Unknown preview target: ${name}`);464 if (!match) throw new Error(`Unknown preview target: ${name}`);
@@ -482,10 +485,7 @@ const buildPreviewWrapperPackage = async (targets: Target[]) => { · 20 unmodified lines
482 await writeFile(join(binDir, "executor"), NODE_SHIM);485 await writeFile(join(binDir, "executor"), NODE_SHIM);
483 await chmod(join(binDir, "executor"), 0o755);486 await chmod(join(binDir, "executor"), 0o755);
484 487
485 const postinstall = PREVIEW_POSTINSTALL_SCRIPT.replaceAll(488 const postinstall = PREVIEW_POSTINSTALL_SCRIPT.replaceAll("__CDN_BASE_URL__", `${cdnUrl}/${sha}`);
486 "__CDN_BASE_URL__",
487 `${cdnUrl}/${sha}`,
488 );
489 await writeFile(join(wrapperDir, "postinstall.cjs"), postinstall);489 await writeFile(join(wrapperDir, "postinstall.cjs"), postinstall);
490 490
491 // Restrict os/cpu to platforms we actually built this run so npm refuses491 // Restrict os/cpu to platforms we actually built this run so npm refuses
@@ -565,11 +565,10 @@ const publishPackedPackage = async (pkgDir: string, channel: string) => { · 73 unmodified lines
565 await $`bun pm pack`.cwd(pkgDir);565 await $`bun pm pack`.cwd(pkgDir);
566 566
567 const provenance = process.env.GITHUB_ACTIONS === "true" ? ["--provenance"] : [];567 const provenance = process.env.GITHUB_ACTIONS === "true" ? ["--provenance"] : [];
568 const result =568 const result = await $`npm publish *.tgz --access public --tag ${channel} ${provenance}`
569 await $`npm publish *.tgz --access public --tag ${channel} ${provenance}`569 .cwd(pkgDir)
570 .cwd(pkgDir)570 .nothrow()
571 .nothrow()571 .quiet();
572 .quiet();
573 572
574 const stdout = result.stdout.toString();573 const stdout = result.stdout.toString();
575 const stderr = result.stderr.toString();574 const stderr = result.stderr.toString();
apps/cli/src/daemon-state.ts+22−14
@@ -59,7 +59,8 @@ export const currentDaemonScopeId = (): string => { · 58 unmodified lines
59const resolveDaemonDataDir = (path: Path.Path): string =>59const resolveDaemonDataDir = (path: Path.Path): string =>
60 process.env.EXECUTOR_DATA_DIR ?? path.join(homedir(), ".executor");60 process.env.EXECUTOR_DATA_DIR ?? path.join(homedir(), ".executor");
61 61
62const sanitizeHostForPath = (hostname: string): string => hostname.replaceAll(/[^a-z0-9.-]+/gi, "_");62const sanitizeHostForPath = (hostname: string): string =>
63 hostname.replaceAll(/[^a-z0-9.-]+/gi, "_");
63const sanitizeScopeForPath = (scopeId: string): string => scopeId.replaceAll(/[^a-z0-9.-]+/gi, "_");64const sanitizeScopeForPath = (scopeId: string): string => scopeId.replaceAll(/[^a-z0-9.-]+/gi, "_");
64 65
65const daemonRecordPath = (path: Path.Path, input: { hostname: string; port: number }): string => {66const daemonRecordPath = (path: Path.Path, input: { hostname: string; port: number }): string => {
@@ -67,14 +68,19 @@ const daemonRecordPath = (path: Path.Path, input: { hostname: string; port: numb · 1 unmodified line
67 return path.join(resolveDaemonDataDir(path), `daemon-${host}-${input.port}.json`);68 return path.join(resolveDaemonDataDir(path), `daemon-${host}-${input.port}.json`);
68};69};
69 70
70const daemonPointerPath = (path: Path.Path, input: { hostname: string; scopeId: string }): string => {71const daemonPointerPath = (
72 path: Path.Path,
73 input: { hostname: string; scopeId: string },
74): string => {
71 const host = sanitizeHostForPath(canonicalDaemonHost(input.hostname));75 const host = sanitizeHostForPath(canonicalDaemonHost(input.hostname));
72 const scope = sanitizeScopeForPath(input.scopeId);76 const scope = sanitizeScopeForPath(input.scopeId);
73 return path.join(resolveDaemonDataDir(path), `daemon-active-${host}-${scope}.json`);77 return path.join(resolveDaemonDataDir(path), `daemon-active-${host}-${scope}.json`);
74};78};
75 79
76const daemonStartLockPath = (path: Path.Path, input: { hostname: string; scopeId: string }): string =>80const daemonStartLockPath = (
77 `${daemonPointerPath(path, input)}.lock`;81 path: Path.Path,
82 input: { hostname: string; scopeId: string },
83): string => `${daemonPointerPath(path, input)}.lock`;
78 84
79// ---------------------------------------------------------------------------85// ---------------------------------------------------------------------------
80// Persistence86// Persistence
@@ -195,9 +201,9 @@ export const readDaemonRecord = (input: { · 114 unmodified lines
195 Effect.gen(function* () {201 Effect.gen(function* () {
196 const fs = yield* FileSystem.FileSystem;202 const fs = yield* FileSystem.FileSystem;
197 const path = yield* Path.Path;203 const path = yield* Path.Path;
198 const raw = yield* fs.readFileString(daemonRecordPath(path, input)).pipe(204 const raw = yield* fs
199 Effect.catchCause(() => Effect.succeed(null)),205 .readFileString(daemonRecordPath(path, input))
200 );206 .pipe(Effect.catchCause(() => Effect.succeed(null)));
201 if (raw === null) return null;207 if (raw === null) return null;
202 return parseRecord(raw);208 return parseRecord(raw);
203 });209 });
@@ -322,7 +328,9 @@ export const acquireDaemonStartLock = (input: { · 118 unmodified lines
322 };328 };
323 }329 }
324 330
325 const existingRaw = yield* fs.readFileString(lockPath).pipe(Effect.catchCause(() => Effect.succeed(null)));331 const existingRaw = yield* fs
332 .readFileString(lockPath)
333 .pipe(Effect.catchCause(() => Effect.succeed(null)));
326 if (existingRaw !== null) {334 if (existingRaw !== null) {
327 const existingPid = parseLockPid(existingRaw);335 const existingPid = parseLockPid(existingRaw);
328 if (existingPid !== null && !isPidAlive(existingPid)) {336 if (existingPid !== null && !isPidAlive(existingPid)) {
@@ -344,11 +352,9 @@ export const acquireDaemonStartLock = (input: { · 15 unmodified lines
344 );352 );
345 });353 });
346 354
347export const releaseDaemonStartLock = (input: DaemonStartLock): Effect.Effect<355export const releaseDaemonStartLock = (
348 void,356 input: DaemonStartLock,
349 PlatformError,357): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
350 FileSystem.FileSystem | Path.Path
351> =>
352 Effect.gen(function* () {358 Effect.gen(function* () {
353 const fs = yield* FileSystem.FileSystem;359 const fs = yield* FileSystem.FileSystem;
354 yield* fs.remove(input.path, { force: true });360 yield* fs.remove(input.path, { force: true });
@@ -374,5 +380,7 @@ export const terminatePid = (pid: number): Effect.Effect<void, Error> => · 19 unmodified lines
374 process.kill(pid, "SIGTERM");380 process.kill(pid, "SIGTERM");
375 },381 },
376 catch: (cause) =>382 catch: (cause) =>
377 cause instanceof Error ? cause : new Error(`Failed to terminate pid ${pid}: ${String(cause)}`),383 cause instanceof Error
384 ? cause
385 : new Error(`Failed to terminate pid ${pid}: ${String(cause)}`),
378 });386 });
apps/cli/src/daemon.ts+7−2
@@ -167,7 +167,10 @@ const toProbeHost = (hostname: string): string => { · 166 unmodified lines
167 return hostname;167 return hostname;
168};168};
169 169
170const isPortAvailable = (input: { hostname: string; port: number }): Effect.Effect<boolean, Error> =>170const isPortAvailable = (input: {
171 hostname: string;
172 port: number;
173}): Effect.Effect<boolean, Error> =>
171 Effect.tryPromise({174 Effect.tryPromise({
172 try: () =>175 try: () =>
173 new Promise<boolean>((resolve) => {176 new Promise<boolean>((resolve) => {
@@ -215,7 +218,9 @@ const pickEphemeralPort = (hostname: string): Effect.Effect<number, Error> => · 41 unmodified lines
215 server.listen({ port: 0, host: toProbeHost(hostname) });218 server.listen({ port: 0, host: toProbeHost(hostname) });
216 }),219 }),
217 catch: (cause) =>220 catch: (cause) =>
218 cause instanceof Error ? cause : new Error(`Failed selecting ephemeral port: ${String(cause)}`),221 cause instanceof Error
222 ? cause
223 : new Error(`Failed selecting ephemeral port: ${String(cause)}`),
219 });224 });
220 225
221export const chooseDaemonPort = (input: {226export const chooseDaemonPort = (input: {
apps/cli/src/main.ts+3−6
@@ -33,13 +33,10 @@ if (typeof Bun !== "undefined" && (await Bun.file(wasmOnDisk).exists())) { · 32 unmodified lines
33 type QuickJSSyncVariant = import("quickjs-emscripten").QuickJSSyncVariant;33 type QuickJSSyncVariant = import("quickjs-emscripten").QuickJSSyncVariant;
34 const wasmBinary = await Bun.file(wasmOnDisk).arrayBuffer();34 const wasmBinary = await Bun.file(wasmOnDisk).arrayBuffer();
35 const importFFI: QuickJSSyncVariant["importFFI"] = () =>35 const importFFI: QuickJSSyncVariant["importFFI"] = () =>
36 import("@jitl/quickjs-wasmfile-release-sync/ffi").then(36 import("@jitl/quickjs-wasmfile-release-sync/ffi").then((m) => m.QuickJSFFI);
37 (m) => m.QuickJSFFI,
38 );
39 const importModuleLoader: QuickJSSyncVariant["importModuleLoader"] = async () => {37 const importModuleLoader: QuickJSSyncVariant["importModuleLoader"] = async () => {
40 const { default: original } = await import(38 const { default: original } =
41 "@jitl/quickjs-wasmfile-release-sync/emscripten-module"39 await import("@jitl/quickjs-wasmfile-release-sync/emscripten-module");
42 );
43 return (moduleArg = {}) => original({ ...moduleArg, wasmBinary });40 return (moduleArg = {}) => original({ ...moduleArg, wasmBinary });
44 };41 };
45 const variant: QuickJSSyncVariant = {42 const variant: QuickJSSyncVariant = {
apps/cli/src/tooling.ts+6−7
@@ -42,7 +42,10 @@ export const inspectToolPath = (input: { · 41 unmodified lines
42}): ToolPathInspection => {42}): ToolPathInspection => {
43 const prefixSegments =43 const prefixSegments =
44 input.rawPrefixParts.length === 0 ? [] : buildToolPath(input.rawPrefixParts).split(".");44 input.rawPrefixParts.length === 0 ? [] : buildToolPath(input.rawPrefixParts).split(".");
45 const children = new Map<string, { invokable: boolean; hasChildren: boolean; toolCount: number }>();45 const children = new Map<
46 string,
47 { invokable: boolean; hasChildren: boolean; toolCount: number }
48 >();
46 let exactPath: string | undefined = undefined;49 let exactPath: string | undefined = undefined;
47 let matchingToolCount = 0;50 let matchingToolCount = 0;
48 51
@@ -97,9 +100,7 @@ export const buildToolPath = (parts: ReadonlyArray<string>): string => { · 48 unmodified lines
97 throw new Error("Tool path must include at least one segment");100 throw new Error("Tool path must include at least one segment");
98 }101 }
99 if (segments.some((segment) => !TOOL_PATH_TOKEN.test(segment))) {102 if (segments.some((segment) => !TOOL_PATH_TOKEN.test(segment))) {
100 throw new Error(103 throw new Error("Tool path segments must contain only letters, numbers, '.', '_' or '-'");
101 "Tool path segments must contain only letters, numbers, '.', '_' or '-'",
102 );
103 }104 }
104 return segments.join(".");105 return segments.join(".");
105};106};
@@ -186,9 +187,7 @@ export interface PausedInteraction { · 80 unmodified lines
186 readonly requestedSchema?: Record<string, unknown>;187 readonly requestedSchema?: Record<string, unknown>;
187}188}
188 189
189export const extractPausedInteraction = (190export const extractPausedInteraction = (structured: unknown): PausedInteraction | undefined => {
190 structured: unknown,
191): PausedInteraction | undefined => {
192 if (!isRecord(structured) || !isRecord(structured.interaction)) {191 if (!isRecord(structured) || !isRecord(structured.interaction)) {
193 return undefined;192 return undefined;
194 }193 }
apps/cloud/drizzle/meta/0000_snapshot.json+20−73
@@ -60,12 +60,8 @@ · 59 unmodified lines
60 "name": "memberships_account_id_accounts_id_fk",60 "name": "memberships_account_id_accounts_id_fk",
61 "tableFrom": "memberships",61 "tableFrom": "memberships",
62 "tableTo": "accounts",62 "tableTo": "accounts",
63 "columnsFrom": [63 "columnsFrom": ["account_id"],
64 "account_id"64 "columnsTo": ["id"],
65 ],
66 "columnsTo": [
67 "id"
68 ],
69 "onDelete": "cascade",65 "onDelete": "cascade",
70 "onUpdate": "no action"66 "onUpdate": "no action"
71 },67 },
@@ -73,12 +69,8 @@ · 1 unmodified line
73 "name": "memberships_organization_id_organizations_id_fk",69 "name": "memberships_organization_id_organizations_id_fk",
74 "tableFrom": "memberships",70 "tableFrom": "memberships",
75 "tableTo": "organizations",71 "tableTo": "organizations",
76 "columnsFrom": [72 "columnsFrom": ["organization_id"],
77 "organization_id"73 "columnsTo": ["id"],
78 ],
79 "columnsTo": [
80 "id"
81 ],
82 "onDelete": "cascade",74 "onDelete": "cascade",
83 "onUpdate": "no action"75 "onUpdate": "no action"
84 }76 }
@@ -86,10 +78,7 @@ · 1 unmodified line
86 "compositePrimaryKeys": {78 "compositePrimaryKeys": {
87 "memberships_account_id_organization_id_pk": {79 "memberships_account_id_organization_id_pk": {
88 "name": "memberships_account_id_organization_id_pk",80 "name": "memberships_account_id_organization_id_pk",
89 "columns": [81 "columns": ["account_id", "organization_id"]
90 "account_id",
91 "organization_id"
92 ]
93 }82 }
94 },83 },
95 "uniqueConstraints": {},84 "uniqueConstraints": {},
@@ -157,10 +146,7 @@ · 61 unmodified lines
157 "compositePrimaryKeys": {146 "compositePrimaryKeys": {
158 "blob_namespace_key_pk": {147 "blob_namespace_key_pk": {
159 "name": "blob_namespace_key_pk",148 "name": "blob_namespace_key_pk",
160 "columns": [149 "columns": ["namespace", "key"]
161 "namespace",
162 "key"
163 ]
164 }150 }
165 },151 },
166 "uniqueConstraints": {},152 "uniqueConstraints": {},
@@ -266,10 +252,7 @@ · 99 unmodified lines
266 "compositePrimaryKeys": {252 "compositePrimaryKeys": {
267 "definition_scope_id_id_pk": {253 "definition_scope_id_id_pk": {
268 "name": "definition_scope_id_id_pk",254 "name": "definition_scope_id_id_pk",
269 "columns": [255 "columns": ["scope_id", "id"]
270 "scope_id",
271 "id"
272 ]
273 }256 }
274 },257 },
275 "uniqueConstraints": {},258 "uniqueConstraints": {},
@@ -342,10 +325,7 @@ · 66 unmodified lines
342 "compositePrimaryKeys": {325 "compositePrimaryKeys": {
343 "graphql_operation_scope_id_id_pk": {326 "graphql_operation_scope_id_id_pk": {
344 "name": "graphql_operation_scope_id_id_pk",327 "name": "graphql_operation_scope_id_id_pk",
345 "columns": [328 "columns": ["scope_id", "id"]
346 "scope_id",
347 "id"
348 ]
349 }329 }
350 },330 },
351 "uniqueConstraints": {},331 "uniqueConstraints": {},
@@ -409,10 +389,7 @@ · 57 unmodified lines
409 "compositePrimaryKeys": {389 "compositePrimaryKeys": {
410 "graphql_source_scope_id_id_pk": {390 "graphql_source_scope_id_id_pk": {
411 "name": "graphql_source_scope_id_id_pk",391 "name": "graphql_source_scope_id_id_pk",
412 "columns": [392 "columns": ["scope_id", "id"]
413 "scope_id",
414 "id"
415 ]
416 }393 }
417 },394 },
418 "uniqueConstraints": {},395 "uniqueConstraints": {},
@@ -491,10 +468,7 @@ · 72 unmodified lines
491 "compositePrimaryKeys": {468 "compositePrimaryKeys": {
492 "mcp_binding_scope_id_id_pk": {469 "mcp_binding_scope_id_id_pk": {
493 "name": "mcp_binding_scope_id_id_pk",470 "name": "mcp_binding_scope_id_id_pk",
494 "columns": [471 "columns": ["scope_id", "id"]
495 "scope_id",
496 "id"
497 ]
498 }472 }
499 },473 },
500 "uniqueConstraints": {},474 "uniqueConstraints": {},
@@ -558,10 +532,7 @@ · 57 unmodified lines
558 "compositePrimaryKeys": {532 "compositePrimaryKeys": {
559 "mcp_oauth_session_scope_id_id_pk": {533 "mcp_oauth_session_scope_id_id_pk": {
560 "name": "mcp_oauth_session_scope_id_id_pk",534 "name": "mcp_oauth_session_scope_id_id_pk",
561 "columns": [535 "columns": ["scope_id", "id"]
562 "scope_id",
563 "id"
564 ]
565 }536 }
566 },537 },
567 "uniqueConstraints": {},538 "uniqueConstraints": {},
@@ -625,10 +596,7 @@ · 57 unmodified lines
625 "compositePrimaryKeys": {596 "compositePrimaryKeys": {
626 "mcp_source_scope_id_id_pk": {597 "mcp_source_scope_id_id_pk": {
627 "name": "mcp_source_scope_id_id_pk",598 "name": "mcp_source_scope_id_id_pk",
628 "columns": [599 "columns": ["scope_id", "id"]
629 "scope_id",
630 "id"
631 ]
632 }600 }
633 },601 },
634 "uniqueConstraints": {},602 "uniqueConstraints": {},
@@ -686,10 +654,7 @@ · 51 unmodified lines
686 "compositePrimaryKeys": {654 "compositePrimaryKeys": {
687 "openapi_oauth_session_scope_id_id_pk": {655 "openapi_oauth_session_scope_id_id_pk": {
688 "name": "openapi_oauth_session_scope_id_id_pk",656 "name": "openapi_oauth_session_scope_id_id_pk",
689 "columns": [657 "columns": ["scope_id", "id"]
690 "scope_id",
691 "id"
692 ]
693 }658 }
694 },659 },
695 "uniqueConstraints": {},660 "uniqueConstraints": {},
@@ -762,10 +727,7 @@ · 66 unmodified lines
762 "compositePrimaryKeys": {727 "compositePrimaryKeys": {
763 "openapi_operation_scope_id_id_pk": {728 "openapi_operation_scope_id_id_pk": {
764 "name": "openapi_operation_scope_id_id_pk",729 "name": "openapi_operation_scope_id_id_pk",
765 "columns": [730 "columns": ["scope_id", "id"]
766 "scope_id",
767 "id"
768 ]
769 }731 }
770 },732 },
771 "uniqueConstraints": {},733 "uniqueConstraints": {},
@@ -847,10 +809,7 @@ · 75 unmodified lines
847 "compositePrimaryKeys": {809 "compositePrimaryKeys": {
848 "openapi_source_scope_id_id_pk": {810 "openapi_source_scope_id_id_pk": {
849 "name": "openapi_source_scope_id_id_pk",811 "name": "openapi_source_scope_id_id_pk",
850 "columns": [812 "columns": ["scope_id", "id"]
851 "scope_id",
852 "id"
853 ]
854 }813 }
855 },814 },
856 "uniqueConstraints": {},815 "uniqueConstraints": {},
@@ -929,10 +888,7 @@ · 72 unmodified lines
929 "compositePrimaryKeys": {888 "compositePrimaryKeys": {
930 "secret_scope_id_id_pk": {889 "secret_scope_id_id_pk": {
931 "name": "secret_scope_id_id_pk",890 "name": "secret_scope_id_id_pk",
932 "columns": [891 "columns": ["scope_id", "id"]
933 "scope_id",
934 "id"
935 ]
936 }892 }
937 },893 },
938 "uniqueConstraints": {},894 "uniqueConstraints": {},
@@ -1050,10 +1006,7 @@ · 111 unmodified lines
1050 "compositePrimaryKeys": {1006 "compositePrimaryKeys": {
1051 "source_scope_id_id_pk": {1007 "source_scope_id_id_pk": {
1052 "name": "source_scope_id_id_pk",1008 "name": "source_scope_id_id_pk",
1053 "columns": [1009 "columns": ["scope_id", "id"]
1054 "scope_id",
1055 "id"
1056 ]
1057 }1010 }
1058 },1011 },
1059 "uniqueConstraints": {},1012 "uniqueConstraints": {},
@@ -1177,10 +1130,7 @@ · 117 unmodified lines
1177 "compositePrimaryKeys": {1130 "compositePrimaryKeys": {
1178 "tool_scope_id_id_pk": {1131 "tool_scope_id_id_pk": {
1179 "name": "tool_scope_id_id_pk",1132 "name": "tool_scope_id_id_pk",
1180 "columns": [1133 "columns": ["scope_id", "id"]
1181 "scope_id",
1182 "id"
1183 ]
1184 }1134 }
1185 },1135 },
1186 "uniqueConstraints": {},1136 "uniqueConstraints": {},
@@ -1244,10 +1194,7 @@ · 57 unmodified lines
1244 "compositePrimaryKeys": {1194 "compositePrimaryKeys": {
1245 "workos_vault_metadata_scope_id_id_pk": {1195 "workos_vault_metadata_scope_id_id_pk": {
1246 "name": "workos_vault_metadata_scope_id_id_pk",1196 "name": "workos_vault_metadata_scope_id_id_pk",
1247 "columns": [1197 "columns": ["scope_id", "id"]
1248 "scope_id",
1249 "id"
1250 ]
1251 }1198 }
1252 },1199 },
1253 "uniqueConstraints": {},1200 "uniqueConstraints": {},
@@ -1267,4 +1214,4 @@ · 13 unmodified lines
1267 "schemas": {},1214 "schemas": {},
1268 "tables": {}1215 "tables": {}
1269 }1216 }
1270}1217}
apps/cloud/drizzle/meta/0001_snapshot.json+20−73
@@ -60,12 +60,8 @@ · 59 unmodified lines
60 "name": "memberships_account_id_accounts_id_fk",60 "name": "memberships_account_id_accounts_id_fk",
61 "tableFrom": "memberships",61 "tableFrom": "memberships",
62 "tableTo": "accounts",62 "tableTo": "accounts",
63 "columnsFrom": [63 "columnsFrom": ["account_id"],
64 "account_id"64 "columnsTo": ["id"],
65 ],
66 "columnsTo": [
67 "id"
68 ],
69 "onDelete": "cascade",65 "onDelete": "cascade",
70 "onUpdate": "no action"66 "onUpdate": "no action"
71 },67 },
@@ -73,12 +69,8 @@ · 1 unmodified line
73 "name": "memberships_organization_id_organizations_id_fk",69 "name": "memberships_organization_id_organizations_id_fk",
74 "tableFrom": "memberships",70 "tableFrom": "memberships",
75 "tableTo": "organizations",71 "tableTo": "organizations",
76 "columnsFrom": [72 "columnsFrom": ["organization_id"],
77 "organization_id"73 "columnsTo": ["id"],
78 ],
79 "columnsTo": [
80 "id"
81 ],
82 "onDelete": "cascade",74 "onDelete": "cascade",
83 "onUpdate": "no action"75 "onUpdate": "no action"
84 }76 }
@@ -86,10 +78,7 @@ · 1 unmodified line
86 "compositePrimaryKeys": {78 "compositePrimaryKeys": {
87 "memberships_account_id_organization_id_pk": {79 "memberships_account_id_organization_id_pk": {
88 "name": "memberships_account_id_organization_id_pk",80 "name": "memberships_account_id_organization_id_pk",
89 "columns": [81 "columns": ["account_id", "organization_id"]
90 "account_id",
91 "organization_id"
92 ]
93 }82 }
94 },83 },
95 "uniqueConstraints": {},84 "uniqueConstraints": {},
@@ -157,10 +146,7 @@ · 61 unmodified lines
157 "compositePrimaryKeys": {146 "compositePrimaryKeys": {
158 "blob_namespace_key_pk": {147 "blob_namespace_key_pk": {
159 "name": "blob_namespace_key_pk",148 "name": "blob_namespace_key_pk",
160 "columns": [149 "columns": ["namespace", "key"]
161 "namespace",
162 "key"
163 ]
164 }150 }
165 },151 },
166 "uniqueConstraints": {},152 "uniqueConstraints": {},
@@ -266,10 +252,7 @@ · 99 unmodified lines
266 "compositePrimaryKeys": {252 "compositePrimaryKeys": {
267 "definition_scope_id_id_pk": {253 "definition_scope_id_id_pk": {
268 "name": "definition_scope_id_id_pk",254 "name": "definition_scope_id_id_pk",
269 "columns": [255 "columns": ["scope_id", "id"]
270 "scope_id",
271 "id"
272 ]
273 }256 }
274 },257 },
275 "uniqueConstraints": {},258 "uniqueConstraints": {},
@@ -342,10 +325,7 @@ · 66 unmodified lines
342 "compositePrimaryKeys": {325 "compositePrimaryKeys": {
343 "graphql_operation_scope_id_id_pk": {326 "graphql_operation_scope_id_id_pk": {
344 "name": "graphql_operation_scope_id_id_pk",327 "name": "graphql_operation_scope_id_id_pk",
345 "columns": [328 "columns": ["scope_id", "id"]
346 "scope_id",
347 "id"
348 ]
349 }329 }
350 },330 },
351 "uniqueConstraints": {},331 "uniqueConstraints": {},
@@ -409,10 +389,7 @@ · 57 unmodified lines
409 "compositePrimaryKeys": {389 "compositePrimaryKeys": {
410 "graphql_source_scope_id_id_pk": {390 "graphql_source_scope_id_id_pk": {
411 "name": "graphql_source_scope_id_id_pk",391 "name": "graphql_source_scope_id_id_pk",
412 "columns": [392 "columns": ["scope_id", "id"]
413 "scope_id",
414 "id"
415 ]
416 }393 }
417 },394 },
418 "uniqueConstraints": {},395 "uniqueConstraints": {},
@@ -491,10 +468,7 @@ · 72 unmodified lines
491 "compositePrimaryKeys": {468 "compositePrimaryKeys": {
492 "mcp_binding_scope_id_id_pk": {469 "mcp_binding_scope_id_id_pk": {
493 "name": "mcp_binding_scope_id_id_pk",470 "name": "mcp_binding_scope_id_id_pk",
494 "columns": [471 "columns": ["scope_id", "id"]
495 "scope_id",
496 "id"
497 ]
498 }472 }
499 },473 },
500 "uniqueConstraints": {},474 "uniqueConstraints": {},
@@ -558,10 +532,7 @@ · 57 unmodified lines
558 "compositePrimaryKeys": {532 "compositePrimaryKeys": {
559 "mcp_oauth_session_scope_id_id_pk": {533 "mcp_oauth_session_scope_id_id_pk": {
560 "name": "mcp_oauth_session_scope_id_id_pk",534 "name": "mcp_oauth_session_scope_id_id_pk",
561 "columns": [535 "columns": ["scope_id", "id"]
562 "scope_id",
563 "id"
564 ]
565 }536 }
566 },537 },
567 "uniqueConstraints": {},538 "uniqueConstraints": {},
@@ -625,10 +596,7 @@ · 57 unmodified lines
625 "compositePrimaryKeys": {596 "compositePrimaryKeys": {
626 "mcp_source_scope_id_id_pk": {597 "mcp_source_scope_id_id_pk": {
627 "name": "mcp_source_scope_id_id_pk",598 "name": "mcp_source_scope_id_id_pk",
628 "columns": [599 "columns": ["scope_id", "id"]
629 "scope_id",
630 "id"
631 ]
632 }600 }
633 },601 },
634 "uniqueConstraints": {},602 "uniqueConstraints": {},
@@ -686,10 +654,7 @@ · 51 unmodified lines
686 "compositePrimaryKeys": {654 "compositePrimaryKeys": {
687 "openapi_oauth_session_scope_id_id_pk": {655 "openapi_oauth_session_scope_id_id_pk": {
688 "name": "openapi_oauth_session_scope_id_id_pk",656 "name": "openapi_oauth_session_scope_id_id_pk",
689 "columns": [657 "columns": ["scope_id", "id"]
690 "scope_id",
691 "id"
692 ]
693 }658 }
694 },659 },
695 "uniqueConstraints": {},660 "uniqueConstraints": {},
@@ -762,10 +727,7 @@ · 66 unmodified lines
762 "compositePrimaryKeys": {727 "compositePrimaryKeys": {
763 "openapi_operation_scope_id_id_pk": {728 "openapi_operation_scope_id_id_pk": {
764 "name": "openapi_operation_scope_id_id_pk",729 "name": "openapi_operation_scope_id_id_pk",
765 "columns": [730 "columns": ["scope_id", "id"]
766 "scope_id",
767 "id"
768 ]
769 }731 }
770 },732 },
771 "uniqueConstraints": {},733 "uniqueConstraints": {},
@@ -853,10 +815,7 @@ · 81 unmodified lines
853 "compositePrimaryKeys": {815 "compositePrimaryKeys": {
854 "openapi_source_scope_id_id_pk": {816 "openapi_source_scope_id_id_pk": {
855 "name": "openapi_source_scope_id_id_pk",817 "name": "openapi_source_scope_id_id_pk",
856 "columns": [818 "columns": ["scope_id", "id"]
857 "scope_id",
858 "id"
859 ]
860 }819 }
861 },820 },
862 "uniqueConstraints": {},821 "uniqueConstraints": {},
@@ -935,10 +894,7 @@ · 72 unmodified lines
935 "compositePrimaryKeys": {894 "compositePrimaryKeys": {
936 "secret_scope_id_id_pk": {895 "secret_scope_id_id_pk": {
937 "name": "secret_scope_id_id_pk",896 "name": "secret_scope_id_id_pk",
938 "columns": [897 "columns": ["scope_id", "id"]
939 "scope_id",
940 "id"
941 ]
942 }898 }
943 },899 },
944 "uniqueConstraints": {},900 "uniqueConstraints": {},
@@ -1056,10 +1012,7 @@ · 111 unmodified lines
1056 "compositePrimaryKeys": {1012 "compositePrimaryKeys": {
1057 "source_scope_id_id_pk": {1013 "source_scope_id_id_pk": {
1058 "name": "source_scope_id_id_pk",1014 "name": "source_scope_id_id_pk",
1059 "columns": [1015 "columns": ["scope_id", "id"]
1060 "scope_id",
1061 "id"
1062 ]
1063 }1016 }
1064 },1017 },
1065 "uniqueConstraints": {},1018 "uniqueConstraints": {},
@@ -1183,10 +1136,7 @@ · 117 unmodified lines
1183 "compositePrimaryKeys": {1136 "compositePrimaryKeys": {
1184 "tool_scope_id_id_pk": {1137 "tool_scope_id_id_pk": {
1185 "name": "tool_scope_id_id_pk",1138 "name": "tool_scope_id_id_pk",
1186 "columns": [1139 "columns": ["scope_id", "id"]
1187 "scope_id",
1188 "id"
1189 ]
1190 }1140 }
1191 },1141 },
1192 "uniqueConstraints": {},1142 "uniqueConstraints": {},
@@ -1250,10 +1200,7 @@ · 57 unmodified lines
1250 "compositePrimaryKeys": {1200 "compositePrimaryKeys": {
1251 "workos_vault_metadata_scope_id_id_pk": {1201 "workos_vault_metadata_scope_id_id_pk": {
1252 "name": "workos_vault_metadata_scope_id_id_pk",1202 "name": "workos_vault_metadata_scope_id_id_pk",
1253 "columns": [1203 "columns": ["scope_id", "id"]
1254 "scope_id",
1255 "id"
1256 ]
1257 }1204 }
1258 },1205 },
1259 "uniqueConstraints": {},1206 "uniqueConstraints": {},
@@ -1273,4 +1220,4 @@ · 13 unmodified lines
1273 "schemas": {},1220 "schemas": {},
1274 "tables": {}1221 "tables": {}
1275 }1222 }
1276}1223}
apps/cloud/drizzle/meta/0002_snapshot.json+20−73
@@ -60,12 +60,8 @@ · 59 unmodified lines
60 "name": "memberships_account_id_accounts_id_fk",60 "name": "memberships_account_id_accounts_id_fk",
61 "tableFrom": "memberships",61 "tableFrom": "memberships",
62 "tableTo": "accounts",62 "tableTo": "accounts",
63 "columnsFrom": [63 "columnsFrom": ["account_id"],
64 "account_id"64 "columnsTo": ["id"],
65 ],
66 "columnsTo": [
67 "id"
68 ],
69 "onDelete": "cascade",65 "onDelete": "cascade",
70 "onUpdate": "no action"66 "onUpdate": "no action"
71 },67 },
@@ -73,12 +69,8 @@ · 1 unmodified line
73 "name": "memberships_organization_id_organizations_id_fk",69 "name": "memberships_organization_id_organizations_id_fk",
74 "tableFrom": "memberships",70 "tableFrom": "memberships",
75 "tableTo": "organizations",71 "tableTo": "organizations",
76 "columnsFrom": [72 "columnsFrom": ["organization_id"],
77 "organization_id"73 "columnsTo": ["id"],
78 ],
79 "columnsTo": [
80 "id"
81 ],
82 "onDelete": "cascade",74 "onDelete": "cascade",
83 "onUpdate": "no action"75 "onUpdate": "no action"
84 }76 }
@@ -86,10 +78,7 @@ · 1 unmodified line
86 "compositePrimaryKeys": {78 "compositePrimaryKeys": {
87 "memberships_account_id_organization_id_pk": {79 "memberships_account_id_organization_id_pk": {
88 "name": "memberships_account_id_organization_id_pk",80 "name": "memberships_account_id_organization_id_pk",
89 "columns": [81 "columns": ["account_id", "organization_id"]
90 "account_id",
91 "organization_id"
92 ]
93 }82 }
94 },83 },
95 "uniqueConstraints": {},84 "uniqueConstraints": {},
@@ -157,10 +146,7 @@ · 61 unmodified lines
157 "compositePrimaryKeys": {146 "compositePrimaryKeys": {
158 "blob_namespace_key_pk": {147 "blob_namespace_key_pk": {
159 "name": "blob_namespace_key_pk",148 "name": "blob_namespace_key_pk",
160 "columns": [149 "columns": ["namespace", "key"]
161 "namespace",
162 "key"
163 ]
164 }150 }
165 },151 },
166 "uniqueConstraints": {},152 "uniqueConstraints": {},
@@ -266,10 +252,7 @@ · 99 unmodified lines
266 "compositePrimaryKeys": {252 "compositePrimaryKeys": {
267 "definition_scope_id_id_pk": {253 "definition_scope_id_id_pk": {
268 "name": "definition_scope_id_id_pk",254 "name": "definition_scope_id_id_pk",
269 "columns": [255 "columns": ["scope_id", "id"]
270 "scope_id",
271 "id"
272 ]
273 }256 }
274 },257 },
275 "uniqueConstraints": {},258 "uniqueConstraints": {},
@@ -342,10 +325,7 @@ · 66 unmodified lines
342 "compositePrimaryKeys": {325 "compositePrimaryKeys": {
343 "graphql_operation_scope_id_id_pk": {326 "graphql_operation_scope_id_id_pk": {
344 "name": "graphql_operation_scope_id_id_pk",327 "name": "graphql_operation_scope_id_id_pk",
345 "columns": [328 "columns": ["scope_id", "id"]
346 "scope_id",
347 "id"
348 ]
349 }329 }
350 },330 },
351 "uniqueConstraints": {},331 "uniqueConstraints": {},
@@ -409,10 +389,7 @@ · 57 unmodified lines
409 "compositePrimaryKeys": {389 "compositePrimaryKeys": {
410 "graphql_source_scope_id_id_pk": {390 "graphql_source_scope_id_id_pk": {
411 "name": "graphql_source_scope_id_id_pk",391 "name": "graphql_source_scope_id_id_pk",
412 "columns": [392 "columns": ["scope_id", "id"]
413 "scope_id",
414 "id"
415 ]
416 }393 }
417 },394 },
418 "uniqueConstraints": {},395 "uniqueConstraints": {},
@@ -491,10 +468,7 @@ · 72 unmodified lines
491 "compositePrimaryKeys": {468 "compositePrimaryKeys": {
492 "mcp_binding_scope_id_id_pk": {469 "mcp_binding_scope_id_id_pk": {
493 "name": "mcp_binding_scope_id_id_pk",470 "name": "mcp_binding_scope_id_id_pk",
494 "columns": [471 "columns": ["scope_id", "id"]
495 "scope_id",
496 "id"
497 ]
498 }472 }
499 },473 },
500 "uniqueConstraints": {},474 "uniqueConstraints": {},
@@ -558,10 +532,7 @@ · 57 unmodified lines
558 "compositePrimaryKeys": {532 "compositePrimaryKeys": {
559 "mcp_oauth_session_scope_id_id_pk": {533 "mcp_oauth_session_scope_id_id_pk": {
560 "name": "mcp_oauth_session_scope_id_id_pk",534 "name": "mcp_oauth_session_scope_id_id_pk",
561 "columns": [535 "columns": ["scope_id", "id"]
562 "scope_id",
563 "id"
564 ]
565 }536 }
566 },537 },
567 "uniqueConstraints": {},538 "uniqueConstraints": {},
@@ -625,10 +596,7 @@ · 57 unmodified lines
625 "compositePrimaryKeys": {596 "compositePrimaryKeys": {
626 "mcp_source_scope_id_id_pk": {597 "mcp_source_scope_id_id_pk": {
627 "name": "mcp_source_scope_id_id_pk",598 "name": "mcp_source_scope_id_id_pk",
628 "columns": [599 "columns": ["scope_id", "id"]
629 "scope_id",
630 "id"
631 ]
632 }600 }
633 },601 },
634 "uniqueConstraints": {},602 "uniqueConstraints": {},
@@ -686,10 +654,7 @@ · 51 unmodified lines
686 "compositePrimaryKeys": {654 "compositePrimaryKeys": {
687 "openapi_oauth_session_scope_id_id_pk": {655 "openapi_oauth_session_scope_id_id_pk": {
688 "name": "openapi_oauth_session_scope_id_id_pk",656 "name": "openapi_oauth_session_scope_id_id_pk",
689 "columns": [657 "columns": ["scope_id", "id"]
690 "scope_id",
691 "id"
692 ]
693 }658 }
694 },659 },
695 "uniqueConstraints": {},660 "uniqueConstraints": {},
@@ -762,10 +727,7 @@ · 66 unmodified lines
762 "compositePrimaryKeys": {727 "compositePrimaryKeys": {
763 "openapi_operation_scope_id_id_pk": {728 "openapi_operation_scope_id_id_pk": {
764 "name": "openapi_operation_scope_id_id_pk",729 "name": "openapi_operation_scope_id_id_pk",
765 "columns": [730 "columns": ["scope_id", "id"]
766 "scope_id",
767 "id"
768 ]
769 }731 }
770 },732 },
771 "uniqueConstraints": {},733 "uniqueConstraints": {},
@@ -853,10 +815,7 @@ · 81 unmodified lines
853 "compositePrimaryKeys": {815 "compositePrimaryKeys": {
854 "openapi_source_scope_id_id_pk": {816 "openapi_source_scope_id_id_pk": {
855 "name": "openapi_source_scope_id_id_pk",817 "name": "openapi_source_scope_id_id_pk",
856 "columns": [818 "columns": ["scope_id", "id"]
857 "scope_id",
858 "id"
859 ]
860 }819 }
861 },820 },
862 "uniqueConstraints": {},821 "uniqueConstraints": {},
@@ -935,10 +894,7 @@ · 72 unmodified lines
935 "compositePrimaryKeys": {894 "compositePrimaryKeys": {
936 "secret_scope_id_id_pk": {895 "secret_scope_id_id_pk": {
937 "name": "secret_scope_id_id_pk",896 "name": "secret_scope_id_id_pk",
938 "columns": [897 "columns": ["scope_id", "id"]
939 "scope_id",
940 "id"
941 ]
942 }898 }
943 },899 },
944 "uniqueConstraints": {},900 "uniqueConstraints": {},
@@ -1056,10 +1012,7 @@ · 111 unmodified lines
1056 "compositePrimaryKeys": {1012 "compositePrimaryKeys": {
1057 "source_scope_id_id_pk": {1013 "source_scope_id_id_pk": {
1058 "name": "source_scope_id_id_pk",1014 "name": "source_scope_id_id_pk",
1059 "columns": [1015 "columns": ["scope_id", "id"]
1060 "scope_id",
1061 "id"
1062 ]
1063 }1016 }
1064 },1017 },
1065 "uniqueConstraints": {},1018 "uniqueConstraints": {},
@@ -1183,10 +1136,7 @@ · 117 unmodified lines
1183 "compositePrimaryKeys": {1136 "compositePrimaryKeys": {
1184 "tool_scope_id_id_pk": {1137 "tool_scope_id_id_pk": {
1185 "name": "tool_scope_id_id_pk",1138 "name": "tool_scope_id_id_pk",
1186 "columns": [1139 "columns": ["scope_id", "id"]
1187 "scope_id",
1188 "id"
1189 ]
1190 }1140 }
1191 },1141 },
1192 "uniqueConstraints": {},1142 "uniqueConstraints": {},
@@ -1250,10 +1200,7 @@ · 57 unmodified lines
1250 "compositePrimaryKeys": {1200 "compositePrimaryKeys": {
1251 "workos_vault_metadata_scope_id_id_pk": {1201 "workos_vault_metadata_scope_id_id_pk": {
1252 "name": "workos_vault_metadata_scope_id_id_pk",1202 "name": "workos_vault_metadata_scope_id_id_pk",
1253 "columns": [1203 "columns": ["scope_id", "id"]
1254 "scope_id",
1255 "id"
1256 ]
1257 }1204 }
1258 },1205 },
1259 "uniqueConstraints": {},1206 "uniqueConstraints": {},
@@ -1273,4 +1220,4 @@ · 13 unmodified lines
1273 "schemas": {},1220 "schemas": {},
1274 "tables": {}1221 "tables": {}
1275 }1222 }
1276}1223}
apps/cloud/drizzle/meta/0003_snapshot.json+21−77
@@ -60,12 +60,8 @@ · 59 unmodified lines
60 "name": "memberships_account_id_accounts_id_fk",60 "name": "memberships_account_id_accounts_id_fk",
61 "tableFrom": "memberships",61 "tableFrom": "memberships",
62 "tableTo": "accounts",62 "tableTo": "accounts",
63 "columnsFrom": [63 "columnsFrom": ["account_id"],
64 "account_id"64 "columnsTo": ["id"],
65 ],
66 "columnsTo": [
67 "id"
68 ],
69 "onDelete": "cascade",65 "onDelete": "cascade",
70 "onUpdate": "no action"66 "onUpdate": "no action"
71 },67 },
@@ -73,12 +69,8 @@ · 1 unmodified line
73 "name": "memberships_organization_id_organizations_id_fk",69 "name": "memberships_organization_id_organizations_id_fk",
74 "tableFrom": "memberships",70 "tableFrom": "memberships",
75 "tableTo": "organizations",71 "tableTo": "organizations",
76 "columnsFrom": [72 "columnsFrom": ["organization_id"],
77 "organization_id"73 "columnsTo": ["id"],
78 ],
79 "columnsTo": [
80 "id"
81 ],
82 "onDelete": "cascade",74 "onDelete": "cascade",
83 "onUpdate": "no action"75 "onUpdate": "no action"
84 }76 }
@@ -86,10 +78,7 @@ · 1 unmodified line
86 "compositePrimaryKeys": {78 "compositePrimaryKeys": {
87 "memberships_account_id_organization_id_pk": {79 "memberships_account_id_organization_id_pk": {
88 "name": "memberships_account_id_organization_id_pk",80 "name": "memberships_account_id_organization_id_pk",
89 "columns": [81 "columns": ["account_id", "organization_id"]
90 "account_id",
91 "organization_id"
92 ]
93 }82 }
94 },83 },
95 "uniqueConstraints": {},84 "uniqueConstraints": {},
@@ -157,10 +146,7 @@ · 61 unmodified lines
157 "compositePrimaryKeys": {146 "compositePrimaryKeys": {
158 "blob_namespace_key_pk": {147 "blob_namespace_key_pk": {
159 "name": "blob_namespace_key_pk",148 "name": "blob_namespace_key_pk",
160 "columns": [149 "columns": ["namespace", "key"]
161 "namespace",
162 "key"
163 ]
164 }150 }
165 },151 },
166 "uniqueConstraints": {},152 "uniqueConstraints": {},
@@ -281,10 +267,7 @@ · 114 unmodified lines
281 "compositePrimaryKeys": {267 "compositePrimaryKeys": {
282 "connection_scope_id_id_pk": {268 "connection_scope_id_id_pk": {
283 "name": "connection_scope_id_id_pk",269 "name": "connection_scope_id_id_pk",
284 "columns": [270 "columns": ["scope_id", "id"]
285 "scope_id",
286 "id"
287 ]
288 }271 }
289 },272 },
290 "uniqueConstraints": {},273 "uniqueConstraints": {},
@@ -390,10 +373,7 @@ · 99 unmodified lines
390 "compositePrimaryKeys": {373 "compositePrimaryKeys": {
391 "definition_scope_id_id_pk": {374 "definition_scope_id_id_pk": {
392 "name": "definition_scope_id_id_pk",375 "name": "definition_scope_id_id_pk",
393 "columns": [376 "columns": ["scope_id", "id"]
394 "scope_id",
395 "id"
396 ]
397 }377 }
398 },378 },
399 "uniqueConstraints": {},379 "uniqueConstraints": {},
@@ -466,10 +446,7 @@ · 66 unmodified lines
466 "compositePrimaryKeys": {446 "compositePrimaryKeys": {
467 "graphql_operation_scope_id_id_pk": {447 "graphql_operation_scope_id_id_pk": {
468 "name": "graphql_operation_scope_id_id_pk",448 "name": "graphql_operation_scope_id_id_pk",
469 "columns": [449 "columns": ["scope_id", "id"]
470 "scope_id",
471 "id"
472 ]
473 }450 }
474 },451 },
475 "uniqueConstraints": {},452 "uniqueConstraints": {},
@@ -533,10 +510,7 @@ · 57 unmodified lines
533 "compositePrimaryKeys": {510 "compositePrimaryKeys": {
534 "graphql_source_scope_id_id_pk": {511 "graphql_source_scope_id_id_pk": {
535 "name": "graphql_source_scope_id_id_pk",512 "name": "graphql_source_scope_id_id_pk",
536 "columns": [513 "columns": ["scope_id", "id"]
537 "scope_id",
538 "id"
539 ]
540 }514 }
541 },515 },
542 "uniqueConstraints": {},516 "uniqueConstraints": {},
@@ -615,10 +589,7 @@ · 72 unmodified lines
615 "compositePrimaryKeys": {589 "compositePrimaryKeys": {
616 "mcp_binding_scope_id_id_pk": {590 "mcp_binding_scope_id_id_pk": {
617 "name": "mcp_binding_scope_id_id_pk",591 "name": "mcp_binding_scope_id_id_pk",
618 "columns": [592 "columns": ["scope_id", "id"]
619 "scope_id",
620 "id"
621 ]
622 }593 }
623 },594 },
624 "uniqueConstraints": {},595 "uniqueConstraints": {},
@@ -682,10 +653,7 @@ · 57 unmodified lines
682 "compositePrimaryKeys": {653 "compositePrimaryKeys": {
683 "mcp_oauth_session_scope_id_id_pk": {654 "mcp_oauth_session_scope_id_id_pk": {
684 "name": "mcp_oauth_session_scope_id_id_pk",655 "name": "mcp_oauth_session_scope_id_id_pk",
685 "columns": [656 "columns": ["scope_id", "id"]
686 "scope_id",
687 "id"
688 ]
689 }657 }
690 },658 },
691 "uniqueConstraints": {},659 "uniqueConstraints": {},
@@ -749,10 +717,7 @@ · 57 unmodified lines
749 "compositePrimaryKeys": {717 "compositePrimaryKeys": {
750 "mcp_source_scope_id_id_pk": {718 "mcp_source_scope_id_id_pk": {
751 "name": "mcp_source_scope_id_id_pk",719 "name": "mcp_source_scope_id_id_pk",
752 "columns": [720 "columns": ["scope_id", "id"]
753 "scope_id",
754 "id"
755 ]
756 }721 }
757 },722 },
758 "uniqueConstraints": {},723 "uniqueConstraints": {},
@@ -810,10 +775,7 @@ · 51 unmodified lines
810 "compositePrimaryKeys": {775 "compositePrimaryKeys": {
811 "openapi_oauth_session_scope_id_id_pk": {776 "openapi_oauth_session_scope_id_id_pk": {
812 "name": "openapi_oauth_session_scope_id_id_pk",777 "name": "openapi_oauth_session_scope_id_id_pk",
813 "columns": [778 "columns": ["scope_id", "id"]
814 "scope_id",
815 "id"
816 ]
817 }779 }
818 },780 },
819 "uniqueConstraints": {},781 "uniqueConstraints": {},
@@ -886,10 +848,7 @@ · 66 unmodified lines
886 "compositePrimaryKeys": {848 "compositePrimaryKeys": {
887 "openapi_operation_scope_id_id_pk": {849 "openapi_operation_scope_id_id_pk": {
888 "name": "openapi_operation_scope_id_id_pk",850 "name": "openapi_operation_scope_id_id_pk",
889 "columns": [851 "columns": ["scope_id", "id"]
890 "scope_id",
891 "id"
892 ]
893 }852 }
894 },853 },
895 "uniqueConstraints": {},854 "uniqueConstraints": {},
@@ -977,10 +936,7 @@ · 81 unmodified lines
977 "compositePrimaryKeys": {936 "compositePrimaryKeys": {
978 "openapi_source_scope_id_id_pk": {937 "openapi_source_scope_id_id_pk": {
979 "name": "openapi_source_scope_id_id_pk",938 "name": "openapi_source_scope_id_id_pk",
980 "columns": [939 "columns": ["scope_id", "id"]
981 "scope_id",
982 "id"
983 ]
984 }940 }
985 },941 },
986 "uniqueConstraints": {},942 "uniqueConstraints": {},
@@ -1080,10 +1036,7 @@ · 93 unmodified lines
1080 "compositePrimaryKeys": {1036 "compositePrimaryKeys": {
1081 "secret_scope_id_id_pk": {1037 "secret_scope_id_id_pk": {
1082 "name": "secret_scope_id_id_pk",1038 "name": "secret_scope_id_id_pk",
1083 "columns": [1039 "columns": ["scope_id", "id"]
1084 "scope_id",
1085 "id"
1086 ]
1087 }1040 }
1088 },1041 },
1089 "uniqueConstraints": {},1042 "uniqueConstraints": {},
@@ -1201,10 +1154,7 @@ · 111 unmodified lines
1201 "compositePrimaryKeys": {1154 "compositePrimaryKeys": {
1202 "source_scope_id_id_pk": {1155 "source_scope_id_id_pk": {
1203 "name": "source_scope_id_id_pk",1156 "name": "source_scope_id_id_pk",
1204 "columns": [1157 "columns": ["scope_id", "id"]
1205 "scope_id",
1206 "id"
1207 ]
1208 }1158 }
1209 },1159 },
1210 "uniqueConstraints": {},1160 "uniqueConstraints": {},
@@ -1328,10 +1278,7 @@ · 117 unmodified lines
1328 "compositePrimaryKeys": {1278 "compositePrimaryKeys": {
1329 "tool_scope_id_id_pk": {1279 "tool_scope_id_id_pk": {
1330 "name": "tool_scope_id_id_pk",1280 "name": "tool_scope_id_id_pk",
1331 "columns": [1281 "columns": ["scope_id", "id"]
1332 "scope_id",
1333 "id"
1334 ]
1335 }1282 }
1336 },1283 },
1337 "uniqueConstraints": {},1284 "uniqueConstraints": {},
@@ -1395,10 +1342,7 @@ · 57 unmodified lines
1395 "compositePrimaryKeys": {1342 "compositePrimaryKeys": {
1396 "workos_vault_metadata_scope_id_id_pk": {1343 "workos_vault_metadata_scope_id_id_pk": {
1397 "name": "workos_vault_metadata_scope_id_id_pk",1344 "name": "workos_vault_metadata_scope_id_id_pk",
1398 "columns": [1345 "columns": ["scope_id", "id"]
1399 "scope_id",
1400 "id"
1401 ]
1402 }1346 }
1403 },1347 },
1404 "uniqueConstraints": {},1348 "uniqueConstraints": {},
@@ -1418,4 +1362,4 @@ · 13 unmodified lines
1418 "schemas": {},1362 "schemas": {},
1419 "tables": {}1363 "tables": {}
1420 }1364 }
1421}1365}
apps/cloud/drizzle/meta/0007_snapshot.json+21−77
@@ -60,12 +60,8 @@ · 59 unmodified lines
60 "name": "memberships_account_id_accounts_id_fk",60 "name": "memberships_account_id_accounts_id_fk",
61 "tableFrom": "memberships",61 "tableFrom": "memberships",
62 "tableTo": "accounts",62 "tableTo": "accounts",
63 "columnsFrom": [63 "columnsFrom": ["account_id"],
64 "account_id"64 "columnsTo": ["id"],
65 ],
66 "columnsTo": [
67 "id"
68 ],
69 "onDelete": "cascade",65 "onDelete": "cascade",
70 "onUpdate": "no action"66 "onUpdate": "no action"
71 },67 },
@@ -73,12 +69,8 @@ · 1 unmodified line
73 "name": "memberships_organization_id_organizations_id_fk",69 "name": "memberships_organization_id_organizations_id_fk",
74 "tableFrom": "memberships",70 "tableFrom": "memberships",
75 "tableTo": "organizations",71 "tableTo": "organizations",
76 "columnsFrom": [72 "columnsFrom": ["organization_id"],
77 "organization_id"73 "columnsTo": ["id"],
78 ],
79 "columnsTo": [
80 "id"
81 ],
82 "onDelete": "cascade",74 "onDelete": "cascade",
83 "onUpdate": "no action"75 "onUpdate": "no action"
84 }76 }
@@ -86,10 +78,7 @@ · 1 unmodified line
86 "compositePrimaryKeys": {78 "compositePrimaryKeys": {
87 "memberships_account_id_organization_id_pk": {79 "memberships_account_id_organization_id_pk": {
88 "name": "memberships_account_id_organization_id_pk",80 "name": "memberships_account_id_organization_id_pk",
89 "columns": [81 "columns": ["account_id", "organization_id"]
90 "account_id",
91 "organization_id"
92 ]
93 }82 }
94 },83 },
95 "uniqueConstraints": {},84 "uniqueConstraints": {},
@@ -157,10 +146,7 @@ · 61 unmodified lines
157 "compositePrimaryKeys": {146 "compositePrimaryKeys": {
158 "blob_namespace_key_pk": {147 "blob_namespace_key_pk": {
159 "name": "blob_namespace_key_pk",148 "name": "blob_namespace_key_pk",
160 "columns": [149 "columns": ["namespace", "key"]
161 "namespace",
162 "key"
163 ]
164 }150 }
165 },151 },
166 "uniqueConstraints": {},152 "uniqueConstraints": {},
@@ -275,10 +261,7 @@ · 108 unmodified lines
275 "compositePrimaryKeys": {261 "compositePrimaryKeys": {
276 "connection_scope_id_id_pk": {262 "connection_scope_id_id_pk": {
277 "name": "connection_scope_id_id_pk",263 "name": "connection_scope_id_id_pk",
278 "columns": [264 "columns": ["scope_id", "id"]
279 "scope_id",
280 "id"
281 ]
282 }265 }
283 },266 },
284 "uniqueConstraints": {},267 "uniqueConstraints": {},
@@ -384,10 +367,7 @@ · 99 unmodified lines
384 "compositePrimaryKeys": {367 "compositePrimaryKeys": {
385 "definition_scope_id_id_pk": {368 "definition_scope_id_id_pk": {
386 "name": "definition_scope_id_id_pk",369 "name": "definition_scope_id_id_pk",
387 "columns": [370 "columns": ["scope_id", "id"]
388 "scope_id",
389 "id"
390 ]
391 }371 }
392 },372 },
393 "uniqueConstraints": {},373 "uniqueConstraints": {},
@@ -460,10 +440,7 @@ · 66 unmodified lines
460 "compositePrimaryKeys": {440 "compositePrimaryKeys": {
461 "graphql_operation_scope_id_id_pk": {441 "graphql_operation_scope_id_id_pk": {
462 "name": "graphql_operation_scope_id_id_pk",442 "name": "graphql_operation_scope_id_id_pk",
463 "columns": [443 "columns": ["scope_id", "id"]
464 "scope_id",
465 "id"
466 ]
467 }444 }
468 },445 },
469 "uniqueConstraints": {},446 "uniqueConstraints": {},
@@ -539,10 +516,7 @@ · 69 unmodified lines
539 "compositePrimaryKeys": {516 "compositePrimaryKeys": {
540 "graphql_source_scope_id_id_pk": {517 "graphql_source_scope_id_id_pk": {
541 "name": "graphql_source_scope_id_id_pk",518 "name": "graphql_source_scope_id_id_pk",
542 "columns": [519 "columns": ["scope_id", "id"]
543 "scope_id",
544 "id"
545 ]
546 }520 }
547 },521 },
548 "uniqueConstraints": {},522 "uniqueConstraints": {},
@@ -621,10 +595,7 @@ · 72 unmodified lines
621 "compositePrimaryKeys": {595 "compositePrimaryKeys": {
622 "mcp_binding_scope_id_id_pk": {596 "mcp_binding_scope_id_id_pk": {
623 "name": "mcp_binding_scope_id_id_pk",597 "name": "mcp_binding_scope_id_id_pk",
624 "columns": [598 "columns": ["scope_id", "id"]
625 "scope_id",
626 "id"
627 ]
628 }599 }
629 },600 },
630 "uniqueConstraints": {},601 "uniqueConstraints": {},
@@ -688,10 +659,7 @@ · 57 unmodified lines
688 "compositePrimaryKeys": {659 "compositePrimaryKeys": {
689 "mcp_source_scope_id_id_pk": {660 "mcp_source_scope_id_id_pk": {
690 "name": "mcp_source_scope_id_id_pk",661 "name": "mcp_source_scope_id_id_pk",
691 "columns": [662 "columns": ["scope_id", "id"]
692 "scope_id",
693 "id"
694 ]
695 }663 }
696 },664 },
697 "uniqueConstraints": {},665 "uniqueConstraints": {},
@@ -815,10 +783,7 @@ · 117 unmodified lines
815 "compositePrimaryKeys": {783 "compositePrimaryKeys": {
816 "oauth2_session_scope_id_id_pk": {784 "oauth2_session_scope_id_id_pk": {
817 "name": "oauth2_session_scope_id_id_pk",785 "name": "oauth2_session_scope_id_id_pk",
818 "columns": [786 "columns": ["scope_id", "id"]
819 "scope_id",
820 "id"
821 ]
822 }787 }
823 },788 },
824 "uniqueConstraints": {},789 "uniqueConstraints": {},
@@ -891,10 +856,7 @@ · 66 unmodified lines
891 "compositePrimaryKeys": {856 "compositePrimaryKeys": {
892 "openapi_operation_scope_id_id_pk": {857 "openapi_operation_scope_id_id_pk": {
893 "name": "openapi_operation_scope_id_id_pk",858 "name": "openapi_operation_scope_id_id_pk",
894 "columns": [859 "columns": ["scope_id", "id"]
895 "scope_id",
896 "id"
897 ]
898 }860 }
899 },861 },
900 "uniqueConstraints": {},862 "uniqueConstraints": {},
@@ -988,10 +950,7 @@ · 87 unmodified lines
988 "compositePrimaryKeys": {950 "compositePrimaryKeys": {
989 "openapi_source_scope_id_id_pk": {951 "openapi_source_scope_id_id_pk": {
990 "name": "openapi_source_scope_id_id_pk",952 "name": "openapi_source_scope_id_id_pk",
991 "columns": [953 "columns": ["scope_id", "id"]
992 "scope_id",
993 "id"
994 ]
995 }954 }
996 },955 },
997 "uniqueConstraints": {},956 "uniqueConstraints": {},
@@ -1213,10 +1172,7 @@ · 215 unmodified lines
1213 "compositePrimaryKeys": {1172 "compositePrimaryKeys": {
1214 "secret_scope_id_id_pk": {1173 "secret_scope_id_id_pk": {
1215 "name": "secret_scope_id_id_pk",1174 "name": "secret_scope_id_id_pk",
1216 "columns": [1175 "columns": ["scope_id", "id"]
1217 "scope_id",
1218 "id"
1219 ]
1220 }1176 }
1221 },1177 },
1222 "uniqueConstraints": {},1178 "uniqueConstraints": {},
@@ -1334,10 +1290,7 @@ · 111 unmodified lines
1334 "compositePrimaryKeys": {1290 "compositePrimaryKeys": {
1335 "source_scope_id_id_pk": {1291 "source_scope_id_id_pk": {
1336 "name": "source_scope_id_id_pk",1292 "name": "source_scope_id_id_pk",
1337 "columns": [1293 "columns": ["scope_id", "id"]
1338 "scope_id",
1339 "id"
1340 ]
1341 }1294 }
1342 },1295 },
1343 "uniqueConstraints": {},1296 "uniqueConstraints": {},
@@ -1461,10 +1414,7 @@ · 117 unmodified lines
1461 "compositePrimaryKeys": {1414 "compositePrimaryKeys": {
1462 "tool_scope_id_id_pk": {1415 "tool_scope_id_id_pk": {
1463 "name": "tool_scope_id_id_pk",1416 "name": "tool_scope_id_id_pk",
1464 "columns": [1417 "columns": ["scope_id", "id"]
1465 "scope_id",
1466 "id"
1467 ]
1468 }1418 }
1469 },1419 },
1470 "uniqueConstraints": {},1420 "uniqueConstraints": {},
@@ -1546,10 +1496,7 @@ · 75 unmodified lines
1546 "compositePrimaryKeys": {1496 "compositePrimaryKeys": {
1547 "tool_policy_scope_id_id_pk": {1497 "tool_policy_scope_id_id_pk": {
1548 "name": "tool_policy_scope_id_id_pk",1498 "name": "tool_policy_scope_id_id_pk",
1549 "columns": [1499 "columns": ["scope_id", "id"]
1550 "scope_id",
1551 "id"
1552 ]
1553 }1500 }
1554 },1501 },
1555 "uniqueConstraints": {},1502 "uniqueConstraints": {},
@@ -1613,10 +1560,7 @@ · 57 unmodified lines
1613 "compositePrimaryKeys": {1560 "compositePrimaryKeys": {
1614 "workos_vault_metadata_scope_id_id_pk": {1561 "workos_vault_metadata_scope_id_id_pk": {
1615 "name": "workos_vault_metadata_scope_id_id_pk",1562 "name": "workos_vault_metadata_scope_id_id_pk",
1616 "columns": [1563 "columns": ["scope_id", "id"]
1617 "scope_id",
1618 "id"
1619 ]
1620 }1564 }
1621 },1565 },
1622 "uniqueConstraints": {},1566 "uniqueConstraints": {},
@@ -1636,4 +1580,4 @@ · 13 unmodified lines
1636 "schemas": {},1580 "schemas": {},
1637 "tables": {}1581 "tables": {}
1638 }1582 }
1639}1583}
apps/cloud/drizzle/meta/0008_snapshot.json+27−104
@@ -60,12 +60,8 @@ · 59 unmodified lines
60 "name": "memberships_account_id_accounts_id_fk",60 "name": "memberships_account_id_accounts_id_fk",
61 "tableFrom": "memberships",61 "tableFrom": "memberships",
62 "tableTo": "accounts",62 "tableTo": "accounts",
63 "columnsFrom": [63 "columnsFrom": ["account_id"],
64 "account_id"64 "columnsTo": ["id"],
65 ],
66 "columnsTo": [
67 "id"
68 ],
69 "onDelete": "cascade",65 "onDelete": "cascade",
70 "onUpdate": "no action"66 "onUpdate": "no action"
71 },67 },
@@ -73,12 +69,8 @@ · 1 unmodified line
73 "name": "memberships_organization_id_organizations_id_fk",69 "name": "memberships_organization_id_organizations_id_fk",
74 "tableFrom": "memberships",70 "tableFrom": "memberships",
75 "tableTo": "organizations",71 "tableTo": "organizations",
76 "columnsFrom": [72 "columnsFrom": ["organization_id"],
77 "organization_id"73 "columnsTo": ["id"],
78 ],
79 "columnsTo": [
80 "id"
81 ],
82 "onDelete": "cascade",74 "onDelete": "cascade",
83 "onUpdate": "no action"75 "onUpdate": "no action"
84 }76 }
@@ -86,10 +78,7 @@ · 1 unmodified line
86 "compositePrimaryKeys": {78 "compositePrimaryKeys": {
87 "memberships_account_id_organization_id_pk": {79 "memberships_account_id_organization_id_pk": {
88 "name": "memberships_account_id_organization_id_pk",80 "name": "memberships_account_id_organization_id_pk",
89 "columns": [81 "columns": ["account_id", "organization_id"]
90 "account_id",
91 "organization_id"
92 ]
93 }82 }
94 },83 },
95 "uniqueConstraints": {},84 "uniqueConstraints": {},
@@ -157,10 +146,7 @@ · 61 unmodified lines
157 "compositePrimaryKeys": {146 "compositePrimaryKeys": {
158 "blob_namespace_key_pk": {147 "blob_namespace_key_pk": {
159 "name": "blob_namespace_key_pk",148 "name": "blob_namespace_key_pk",
160 "columns": [149 "columns": ["namespace", "key"]
161 "namespace",
162 "key"
163 ]
164 }150 }
165 },151 },
166 "uniqueConstraints": {},152 "uniqueConstraints": {},
@@ -275,10 +261,7 @@ · 108 unmodified lines
275 "compositePrimaryKeys": {261 "compositePrimaryKeys": {
276 "connection_scope_id_id_pk": {262 "connection_scope_id_id_pk": {
277 "name": "connection_scope_id_id_pk",263 "name": "connection_scope_id_id_pk",
278 "columns": [264 "columns": ["scope_id", "id"]
279 "scope_id",
280 "id"
281 ]
282 }265 }
283 },266 },
284 "uniqueConstraints": {},267 "uniqueConstraints": {},
@@ -384,10 +367,7 @@ · 99 unmodified lines
384 "compositePrimaryKeys": {367 "compositePrimaryKeys": {
385 "definition_scope_id_id_pk": {368 "definition_scope_id_id_pk": {
386 "name": "definition_scope_id_id_pk",369 "name": "definition_scope_id_id_pk",
387 "columns": [370 "columns": ["scope_id", "id"]
388 "scope_id",
389 "id"
390 ]
391 }371 }
392 },372 },
393 "uniqueConstraints": {},373 "uniqueConstraints": {},
@@ -460,10 +440,7 @@ · 66 unmodified lines
460 "compositePrimaryKeys": {440 "compositePrimaryKeys": {
461 "graphql_operation_scope_id_id_pk": {441 "graphql_operation_scope_id_id_pk": {
462 "name": "graphql_operation_scope_id_id_pk",442 "name": "graphql_operation_scope_id_id_pk",
463 "columns": [443 "columns": ["scope_id", "id"]
464 "scope_id",
465 "id"
466 ]
467 }444 }
468 },445 },
469 "uniqueConstraints": {},446 "uniqueConstraints": {},
@@ -549,10 +526,7 @@ · 79 unmodified lines
549 "compositePrimaryKeys": {526 "compositePrimaryKeys": {
550 "graphql_source_scope_id_id_pk": {527 "graphql_source_scope_id_id_pk": {
551 "name": "graphql_source_scope_id_id_pk",528 "name": "graphql_source_scope_id_id_pk",
552 "columns": [529 "columns": ["scope_id", "id"]
553 "scope_id",
554 "id"
555 ]
556 }530 }
557 },531 },
558 "uniqueConstraints": {},532 "uniqueConstraints": {},
@@ -631,10 +605,7 @@ · 72 unmodified lines
631 "compositePrimaryKeys": {605 "compositePrimaryKeys": {
632 "mcp_binding_scope_id_id_pk": {606 "mcp_binding_scope_id_id_pk": {
633 "name": "mcp_binding_scope_id_id_pk",607 "name": "mcp_binding_scope_id_id_pk",
634 "columns": [608 "columns": ["scope_id", "id"]
635 "scope_id",
636 "id"
637 ]
638 }609 }
639 },610 },
640 "uniqueConstraints": {},611 "uniqueConstraints": {},
@@ -801,10 +772,7 @@ · 160 unmodified lines
801 "compositePrimaryKeys": {772 "compositePrimaryKeys": {
802 "mcp_source_scope_id_id_pk": {773 "mcp_source_scope_id_id_pk": {
803 "name": "mcp_source_scope_id_id_pk",774 "name": "mcp_source_scope_id_id_pk",
804 "columns": [775 "columns": ["scope_id", "id"]
805 "scope_id",
806 "id"
807 ]
808 }776 }
809 },777 },
810 "uniqueConstraints": {},778 "uniqueConstraints": {},
@@ -928,10 +896,7 @@ · 117 unmodified lines
928 "compositePrimaryKeys": {896 "compositePrimaryKeys": {
929 "oauth2_session_scope_id_id_pk": {897 "oauth2_session_scope_id_id_pk": {
930 "name": "oauth2_session_scope_id_id_pk",898 "name": "oauth2_session_scope_id_id_pk",
931 "columns": [899 "columns": ["scope_id", "id"]
932 "scope_id",
933 "id"
934 ]
935 }900 }
936 },901 },
937 "uniqueConstraints": {},902 "uniqueConstraints": {},
@@ -1004,10 +969,7 @@ · 66 unmodified lines
1004 "compositePrimaryKeys": {969 "compositePrimaryKeys": {
1005 "openapi_operation_scope_id_id_pk": {970 "openapi_operation_scope_id_id_pk": {
1006 "name": "openapi_operation_scope_id_id_pk",971 "name": "openapi_operation_scope_id_id_pk",
1007 "columns": [972 "columns": ["scope_id", "id"]
1008 "scope_id",
1009 "id"
1010 ]
1011 }973 }
1012 },974 },
1013 "uniqueConstraints": {},975 "uniqueConstraints": {},
@@ -1089,10 +1051,7 @@ · 75 unmodified lines
1089 "compositePrimaryKeys": {1051 "compositePrimaryKeys": {
1090 "openapi_source_scope_id_id_pk": {1052 "openapi_source_scope_id_id_pk": {
1091 "name": "openapi_source_scope_id_id_pk",1053 "name": "openapi_source_scope_id_id_pk",
1092 "columns": [1054 "columns": ["scope_id", "id"]
1093 "scope_id",
1094 "id"
1095 ]
1096 }1055 }
1097 },1056 },
1098 "uniqueConstraints": {},1057 "uniqueConstraints": {},
@@ -1363,10 +1322,7 @@ · 264 unmodified lines
1363 "compositePrimaryKeys": {1322 "compositePrimaryKeys": {
1364 "secret_scope_id_id_pk": {1323 "secret_scope_id_id_pk": {
1365 "name": "secret_scope_id_id_pk",1324 "name": "secret_scope_id_id_pk",
1366 "columns": [1325 "columns": ["scope_id", "id"]
1367 "scope_id",
1368 "id"
1369 ]
1370 }1326 }
1371 },1327 },
1372 "uniqueConstraints": {},1328 "uniqueConstraints": {},
@@ -1484,10 +1440,7 @@ · 111 unmodified lines
1484 "compositePrimaryKeys": {1440 "compositePrimaryKeys": {
1485 "source_scope_id_id_pk": {1441 "source_scope_id_id_pk": {
1486 "name": "source_scope_id_id_pk",1442 "name": "source_scope_id_id_pk",
1487 "columns": [1443 "columns": ["scope_id", "id"]
1488 "scope_id",
1489 "id"
1490 ]
1491 }1444 }
1492 },1445 },
1493 "uniqueConstraints": {},1446 "uniqueConstraints": {},
@@ -1611,10 +1564,7 @@ · 117 unmodified lines
1611 "compositePrimaryKeys": {1564 "compositePrimaryKeys": {
1612 "tool_scope_id_id_pk": {1565 "tool_scope_id_id_pk": {
1613 "name": "tool_scope_id_id_pk",1566 "name": "tool_scope_id_id_pk",
1614 "columns": [1567 "columns": ["scope_id", "id"]
1615 "scope_id",
1616 "id"
1617 ]
1618 }1568 }
1619 },1569 },
1620 "uniqueConstraints": {},1570 "uniqueConstraints": {},
@@ -1696,10 +1646,7 @@ · 75 unmodified lines
1696 "compositePrimaryKeys": {1646 "compositePrimaryKeys": {
1697 "tool_policy_scope_id_id_pk": {1647 "tool_policy_scope_id_id_pk": {
1698 "name": "tool_policy_scope_id_id_pk",1648 "name": "tool_policy_scope_id_id_pk",
1699 "columns": [1649 "columns": ["scope_id", "id"]
1700 "scope_id",
1701 "id"
1702 ]
1703 }1650 }
1704 },1651 },
1705 "uniqueConstraints": {},1652 "uniqueConstraints": {},
@@ -1763,10 +1710,7 @@ · 57 unmodified lines
1763 "compositePrimaryKeys": {1710 "compositePrimaryKeys": {
1764 "workos_vault_metadata_scope_id_id_pk": {1711 "workos_vault_metadata_scope_id_id_pk": {
1765 "name": "workos_vault_metadata_scope_id_id_pk",1712 "name": "workos_vault_metadata_scope_id_id_pk",
1766 "columns": [1713 "columns": ["scope_id", "id"]
1767 "scope_id",
1768 "id"
1769 ]
1770 }1714 }
1771 },1715 },
1772 "uniqueConstraints": {},1716 "uniqueConstraints": {},
@@ -1878,10 +1822,7 @@ · 105 unmodified lines
1878 "compositePrimaryKeys": {1822 "compositePrimaryKeys": {
1879 "graphql_source_header_scope_id_id_pk": {1823 "graphql_source_header_scope_id_id_pk": {
1880 "name": "graphql_source_header_scope_id_id_pk",1824 "name": "graphql_source_header_scope_id_id_pk",
1881 "columns": [1825 "columns": ["scope_id", "id"]
1882 "scope_id",
1883 "id"
1884 ]
1885 }1826 }
1886 },1827 },
1887 "uniqueConstraints": {},1828 "uniqueConstraints": {},
@@ -1993,10 +1934,7 @@ · 105 unmodified lines
1993 "compositePrimaryKeys": {1934 "compositePrimaryKeys": {
1994 "graphql_source_query_param_scope_id_id_pk": {1935 "graphql_source_query_param_scope_id_id_pk": {
1995 "name": "graphql_source_query_param_scope_id_id_pk",1936 "name": "graphql_source_query_param_scope_id_id_pk",
1996 "columns": [1937 "columns": ["scope_id", "id"]
1997 "scope_id",
1998 "id"
1999 ]
2000 }1938 }
2001 },1939 },
2002 "uniqueConstraints": {},1940 "uniqueConstraints": {},
@@ -2108,10 +2046,7 @@ · 105 unmodified lines
2108 "compositePrimaryKeys": {2046 "compositePrimaryKeys": {
2109 "openapi_source_query_param_scope_id_id_pk": {2047 "openapi_source_query_param_scope_id_id_pk": {
2110 "name": "openapi_source_query_param_scope_id_id_pk",2048 "name": "openapi_source_query_param_scope_id_id_pk",
2111 "columns": [2049 "columns": ["scope_id", "id"]
2112 "scope_id",
2113 "id"
2114 ]
2115 }2050 }
2116 },2051 },
2117 "uniqueConstraints": {},2052 "uniqueConstraints": {},
@@ -2223,10 +2158,7 @@ · 105 unmodified lines
2223 "compositePrimaryKeys": {2158 "compositePrimaryKeys": {
2224 "openapi_source_spec_fetch_header_scope_id_id_pk": {2159 "openapi_source_spec_fetch_header_scope_id_id_pk": {
2225 "name": "openapi_source_spec_fetch_header_scope_id_id_pk",2160 "name": "openapi_source_spec_fetch_header_scope_id_id_pk",
2226 "columns": [2161 "columns": ["scope_id", "id"]
2227 "scope_id",
2228 "id"
2229 ]
2230 }2162 }
2231 },2163 },
2232 "uniqueConstraints": {},2164 "uniqueConstraints": {},
@@ -2338,10 +2270,7 @@ · 105 unmodified lines
2338 "compositePrimaryKeys": {2270 "compositePrimaryKeys": {
2339 "openapi_source_spec_fetch_query_param_scope_id_id_pk": {2271 "openapi_source_spec_fetch_query_param_scope_id_id_pk": {
2340 "name": "openapi_source_spec_fetch_query_param_scope_id_id_pk",2272 "name": "openapi_source_spec_fetch_query_param_scope_id_id_pk",
2341 "columns": [2273 "columns": ["scope_id", "id"]
2342 "scope_id",
2343 "id"
2344 ]
2345 }2274 }
2346 },2275 },
2347 "uniqueConstraints": {},2276 "uniqueConstraints": {},
@@ -2453,10 +2382,7 @@ · 105 unmodified lines
2453 "compositePrimaryKeys": {2382 "compositePrimaryKeys": {
2454 "mcp_source_header_scope_id_id_pk": {2383 "mcp_source_header_scope_id_id_pk": {
2455 "name": "mcp_source_header_scope_id_id_pk",2384 "name": "mcp_source_header_scope_id_id_pk",
2456 "columns": [2385 "columns": ["scope_id", "id"]
2457 "scope_id",
2458 "id"
2459 ]
2460 }2386 }
2461 },2387 },
2462 "uniqueConstraints": {},2388 "uniqueConstraints": {},
@@ -2568,10 +2494,7 @@ · 105 unmodified lines
2568 "compositePrimaryKeys": {2494 "compositePrimaryKeys": {
2569 "mcp_source_query_param_scope_id_id_pk": {2495 "mcp_source_query_param_scope_id_id_pk": {
2570 "name": "mcp_source_query_param_scope_id_id_pk",2496 "name": "mcp_source_query_param_scope_id_id_pk",
2571 "columns": [2497 "columns": ["scope_id", "id"]
2572 "scope_id",
2573 "id"
2574 ]
2575 }2498 }
2576 },2499 },
2577 "uniqueConstraints": {},2500 "uniqueConstraints": {},
apps/cloud/executor.config.ts+1−4
@@ -2,10 +2,7 @@ import { defineExecutorConfig } from "@executor-js/sdk"; · 1 unmodified line
2import { openApiPlugin } from "@executor-js/plugin-openapi";2import { openApiPlugin } from "@executor-js/plugin-openapi";
3import { mcpPlugin } from "@executor-js/plugin-mcp";3import { mcpPlugin } from "@executor-js/plugin-mcp";
4import { graphqlPlugin } from "@executor-js/plugin-graphql";4import { graphqlPlugin } from "@executor-js/plugin-graphql";
5import {5import { workosVaultPlugin, type WorkOSVaultClient } from "@executor-js/plugin-workos-vault";
6 workosVaultPlugin,
7 type WorkOSVaultClient,
8} from "@executor-js/plugin-workos-vault";
9 6
10// ---------------------------------------------------------------------------7// ---------------------------------------------------------------------------
11// Single source of truth for the cloud app's plugin list.8// Single source of truth for the cloud app's plugin list.
apps/cloud/src/api.request-scope.node.test.ts+7−17
@@ -29,15 +29,9 @@ import { RequestScopedServicesLive } from "./api/layers"; · 28 unmodified lines
29import { requestScopedMiddleware } from "./api/request-scoped";29import { requestScopedMiddleware } from "./api/request-scoped";
30import { makeApiLive } from "./api/router";30import { makeApiLive } from "./api/router";
31 31
32class Counter extends Context.Service<32class Counter extends Context.Service<Counter, { readonly id: number }>()("test/Counter") {}
33 Counter,33
34 { readonly id: number }34const makeCounterLive = (counts: { acquires: number; releases: number }, acquireDelayMs = 0) =>
35>()("test/Counter") {}
36
37const makeCounterLive = (
38 counts: { acquires: number; releases: number },
39 acquireDelayMs = 0,
40) =>
41 Layer.effect(Counter)(35 Layer.effect(Counter)(
42 Effect.acquireRelease(36 Effect.acquireRelease(
43 Effect.gen(function* () {37 Effect.gen(function* () {
@@ -72,8 +66,7 @@ describe("HttpRouter.toWebHandler request scoping", () => { · 28 unmodified lines
72 Layer.provideMerge(makeCounterLive(counts)),66 Layer.provideMerge(makeCounterLive(counts)),
73 Layer.provideMerge(HttpServer.layerServices),67 Layer.provideMerge(HttpServer.layerServices),
74 );68 );
75 const handler = HttpRouter.toWebHandler(App, { disableLogger: true })69 const handler = HttpRouter.toWebHandler(App, { disableLogger: true }).handler;
76 .handler;
77 70
78 const a = await handler(new Request("http://test.local/"));71 const a = await handler(new Request("http://test.local/"));
79 const b = await handler(new Request("http://test.local/"));72 const b = await handler(new Request("http://test.local/"));
@@ -91,8 +84,7 @@ describe("HttpRouter.toWebHandler request scoping", () => { · 11 unmodified lines
91 HttpRouter.provideRequest(makeCounterLive(counts)),84 HttpRouter.provideRequest(makeCounterLive(counts)),
92 Layer.provideMerge(HttpServer.layerServices),85 Layer.provideMerge(HttpServer.layerServices),
93 );86 );
94 const handler = HttpRouter.toWebHandler(App, { disableLogger: true })87 const handler = HttpRouter.toWebHandler(App, { disableLogger: true }).handler;
95 .handler;
96 88
97 const a = await handler(new Request("http://test.local/"));89 const a = await handler(new Request("http://test.local/"));
98 const b = await handler(new Request("http://test.local/"));90 const b = await handler(new Request("http://test.local/"));
@@ -111,8 +103,7 @@ describe("HttpRouter.toWebHandler request scoping", () => { · 12 unmodified lines
111 Layer.provide(requestScopedMiddleware(makeCounterLive(counts)).layer),103 Layer.provide(requestScopedMiddleware(makeCounterLive(counts)).layer),
112 Layer.provideMerge(HttpServer.layerServices),104 Layer.provideMerge(HttpServer.layerServices),
113 );105 );
114 const handler = HttpRouter.toWebHandler(App, { disableLogger: true })106 const handler = HttpRouter.toWebHandler(App, { disableLogger: true }).handler;
115 .handler;
116 107
117 const a = await handler(new Request("http://test.local/"));108 const a = await handler(new Request("http://test.local/"));
118 const b = await handler(new Request("http://test.local/"));109 const b = await handler(new Request("http://test.local/"));
@@ -144,8 +135,7 @@ describe("HttpRouter.toWebHandler request scoping", () => { · 25 unmodified lines
144 Layer.provide(requestScopedMiddleware(makeCounterLive(counts, 5)).layer),135 Layer.provide(requestScopedMiddleware(makeCounterLive(counts, 5)).layer),
145 Layer.provideMerge(HttpServer.layerServices),136 Layer.provideMerge(HttpServer.layerServices),
146 );137 );
147 const handler = HttpRouter.toWebHandler(App, { disableLogger: true })138 const handler = HttpRouter.toWebHandler(App, { disableLogger: true }).handler;
148 .handler;
149 139
150 const [a, b] = await Promise.all([140 const [a, b] = await Promise.all([
151 handler(new Request("http://test.local/")),141 handler(new Request("http://test.local/")),
apps/cloud/src/api.test.ts+2−7
@@ -93,9 +93,7 @@ const TestProtectedGate = HttpRouter.middleware()((httpEffect) => · 92 unmodified lines
93 );93 );
94 }94 }
95 if (testState.mode === "error") {95 if (testState.mode === "error") {
96 return Effect.succeed(96 return Effect.succeed(HttpServerResponse.jsonUnsafe({ error: "boom" }, { status: 500 }));
97 HttpServerResponse.jsonUnsafe({ error: "boom" }, { status: 500 }),
98 );
99 }97 }
100 if (testState.mode === "bad-status") {98 if (testState.mode === "bad-status") {
101 return Effect.succeed(99 return Effect.succeed(
@@ -130,10 +128,7 @@ const TestApiLive = Layer.mergeAll( · 28 unmodified lines
130 AuthTestLive,128 AuthTestLive,
131 ProtectedTestLive,129 ProtectedTestLive,
132 AutumnTestRoutesLive,130 AutumnTestRoutesLive,
133).pipe(131).pipe(Layer.provideMerge(RouterConfig), Layer.provideMerge(HttpServer.layerServices));
134 Layer.provideMerge(RouterConfig),
135 Layer.provideMerge(HttpServer.layerServices),
136);
137 132
138const requestHandler = HttpRouter.toWebHandler(TestApiLive, { disableLogger: true }).handler;133const requestHandler = HttpRouter.toWebHandler(TestApiLive, { disableLogger: true }).handler;
139 134
apps/cloud/src/api/autumn.ts+2−10
@@ -1,18 +1,10 @@
1import { env } from "cloudflare:workers";1import { env } from "cloudflare:workers";
2import { Cause, Effect } from "effect";2import { Cause, Effect } from "effect";
3import {3import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
4 HttpRouter,
5 HttpServerRequest,
6 HttpServerResponse,
7} from "effect/unstable/http";
8import { autumnHandler } from "autumn-js/backend";4import { autumnHandler } from "autumn-js/backend";
9 5
10import { WorkOSAuth } from "../auth/workos";6import { WorkOSAuth } from "../auth/workos";
11import {7import { HttpResponseError, isServerError, toErrorServerResponse } from "./error-response";
12 HttpResponseError,
13 isServerError,
14 toErrorServerResponse,
15} from "./error-response";
16 8
17const handler = Effect.gen(function* () {9const handler = Effect.gen(function* () {
18 const request = yield* HttpServerRequest.HttpServerRequest;10 const request = yield* HttpServerRequest.HttpServerRequest;
apps/cloud/src/api/error-response.ts+1−4
@@ -1,8 +1,5 @@
1import { Cause, Data, Effect, Predicate, Result } from "effect";1import { Cause, Data, Effect, Predicate, Result } from "effect";
2import {2import { HttpServerRespondable, HttpServerResponse } from "effect/unstable/http";
3 HttpServerRespondable,
4 HttpServerResponse,
5} from "effect/unstable/http";
6 3
7import { captureCause } from "../observability";4import { captureCause } from "../observability";
8 5
apps/cloud/src/api/layers.ts+4−14
@@ -18,11 +18,7 @@ import { CoreSharedServices } from "./core-shared-services"; · 17 unmodified lines
18import { ProtectedCloudApi, RouterConfig } from "./protected-layers";18import { ProtectedCloudApi, RouterConfig } from "./protected-layers";
19import { requestScopedMiddleware } from "./request-scoped";19import { requestScopedMiddleware } from "./request-scoped";
20 20
21export {21export { CoreSharedServices, ProtectedCloudApi, RouterConfig };
22 CoreSharedServices,
23 ProtectedCloudApi,
24 RouterConfig,
25};
26 22
27const DbLive = DbService.Live;23const DbLive = DbService.Live;
28const UserStoreLive = UserStoreService.Live.pipe(Layer.provide(DbLive));24const UserStoreLive = UserStoreService.Live.pipe(Layer.provide(DbLive));
@@ -51,9 +47,7 @@ export const BootSharedServices = Layer.mergeAll( · 22 unmodified lines
51// without per-request scoping the postgres.js socket pins to the worker's47// without per-request scoping the postgres.js socket pins to the worker's
52// boot scope and Cloudflare Workers' I/O isolation kills the second48// boot scope and Cloudflare Workers' I/O isolation kills the second
53// request.49// request.
54export const makeNonProtectedApiLive = (50export const makeNonProtectedApiLive = (rsLive: Layer.Layer<DbService | UserStoreService>) =>
55 rsLive: Layer.Layer<DbService | UserStoreService>,
56) =>
57 HttpApiBuilder.layer(NonProtectedApi).pipe(51 HttpApiBuilder.layer(NonProtectedApi).pipe(
58 Layer.provide(Layer.mergeAll(CloudAuthPublicHandlers, CloudSessionAuthHandlers)),52 Layer.provide(Layer.mergeAll(CloudAuthPublicHandlers, CloudSessionAuthHandlers)),
59 Layer.provide(requestScopedMiddleware(rsLive).layer),53 Layer.provide(requestScopedMiddleware(rsLive).layer),
@@ -62,9 +56,7 @@ export const makeNonProtectedApiLive = ( · 2 unmodified lines
62 56
63// Routes scoped to a specific org (membership management, switching, etc.).57// Routes scoped to a specific org (membership management, switching, etc.).
64// Auth is enforced by `OrgAuth` middleware declared on `OrgHttpApi`.58// Auth is enforced by `OrgAuth` middleware declared on `OrgHttpApi`.
65export const makeOrgApiLive = (59export const makeOrgApiLive = (rsLive: Layer.Layer<DbService | UserStoreService>) =>
66 rsLive: Layer.Layer<DbService | UserStoreService>,
67) =>
68 HttpApiBuilder.layer(OrgHttpApi).pipe(60 HttpApiBuilder.layer(OrgHttpApi).pipe(
69 Layer.provide(OrgHandlers),61 Layer.provide(OrgHandlers),
70 Layer.provide(requestScopedMiddleware(rsLive).layer),62 Layer.provide(requestScopedMiddleware(rsLive).layer),
@@ -74,7 +66,5 @@ export const makeOrgApiLive = ( · 3 unmodified lines
74// Default exports use the production per-request layer. Existing callers66// Default exports use the production per-request layer. Existing callers
75// that import `NonProtectedApiLive`/`OrgApiLive` continue to work; the67// that import `NonProtectedApiLive`/`OrgApiLive` continue to work; the
76// `make*` factories exist for tests that need to swap in a fake.68// `make*` factories exist for tests that need to swap in a fake.
77export const NonProtectedApiLive = makeNonProtectedApiLive(69export const NonProtectedApiLive = makeNonProtectedApiLive(RequestScopedServicesLive);
78 RequestScopedServicesLive,
79);
80export const OrgApiLive = makeOrgApiLive(RequestScopedServicesLive);70export const OrgApiLive = makeOrgApiLive(RequestScopedServicesLive);
apps/cloud/src/api/protected-layers.ts+1−5
@@ -8,11 +8,7 @@ import { HttpRouter, HttpServer } from "effect/unstable/http"; · 7 unmodified lines
8import { Layer } from "effect";8import { Layer } from "effect";
9 9
10import { observabilityMiddleware } from "@executor-js/api";10import { observabilityMiddleware } from "@executor-js/api";
11import {11import { CoreHandlers, composePluginApi, composePluginHandlerLayer } from "@executor-js/api/server";
12 CoreHandlers,
13 composePluginApi,
14 composePluginHandlerLayer,
15} from "@executor-js/api/server";
16 12
17import { cloudPlugins } from "./cloud-plugins";13import { cloudPlugins } from "./cloud-plugins";
18import { UserStoreService } from "../auth/context";14import { UserStoreService } from "../auth/context";
apps/cloud/src/api/protected.ts+3−12
@@ -3,10 +3,7 @@ · 2 unmodified lines
3// harness can't load in the workerd test runtime.3// harness can't load in the workerd test runtime.
4 4
5import { HttpApiSwagger } from "effect/unstable/httpapi";5import { HttpApiSwagger } from "effect/unstable/httpapi";
6import {6import { HttpRouter, HttpServerRequest } from "effect/unstable/http";
7 HttpRouter,
8 HttpServerRequest,
9} from "effect/unstable/http";
10import { Effect, Layer } from "effect";7import { Effect, Layer } from "effect";
11 8
12import {9import {
@@ -26,11 +23,7 @@ import { DbService } from "../services/db"; · 13 unmodified lines
26import { makeExecutionStack } from "../services/execution-stack";23import { makeExecutionStack } from "../services/execution-stack";
27import { HttpResponseError } from "./error-response";24import { HttpResponseError } from "./error-response";
28import { RequestScopedServicesLive } from "./layers";25import { RequestScopedServicesLive } from "./layers";
29import {26import { ProtectedCloudApi, ProtectedCloudApiLive, RouterConfig } from "./protected-layers";
30 ProtectedCloudApi,
31 ProtectedCloudApiLive,
32 RouterConfig,
33} from "./protected-layers";
34import { requestScopedMiddleware } from "./request-scoped";27import { requestScopedMiddleware } from "./request-scoped";
35 28
36// Pre-compute the per-plugin `Effect.provideService(extensionService,29// Pre-compute the per-plugin `Effect.provideService(extensionService,
@@ -119,9 +112,7 @@ const ExecutionStackMiddleware = HttpRouter.middleware<{ · 82 unmodified lines
119// the layer rebuilds per HTTP request, satisfying Cloudflare Workers'112// the layer rebuilds per HTTP request, satisfying Cloudflare Workers'
120// I/O isolation. Exposed as a factory so tests can swap in a counting113// I/O isolation. Exposed as a factory so tests can swap in a counting
121// fake — see `apps/cloud/src/api.request-scope.node.test.ts`.114// fake — see `apps/cloud/src/api.request-scope.node.test.ts`.
122export const makeProtectedApiLive = (115export const makeProtectedApiLive = (rsLive: Layer.Layer<DbService | UserStoreService>) => {
123 rsLive: Layer.Layer<DbService | UserStoreService>,
124) => {
125 const protectedMiddleware = ExecutionStackMiddleware.combine(116 const protectedMiddleware = ExecutionStackMiddleware.combine(
126 requestScopedMiddleware(rsLive),117 requestScopedMiddleware(rsLive),
127 ).layer;118 ).layer;
apps/cloud/src/api/router.ts+2−7
@@ -25,18 +25,13 @@ import { SlackContactRoutesLive } from "./slack"; · 24 unmodified lines
25// so tests can substitute a counting fake for `DbService.Live` and25// so tests can substitute a counting fake for `DbService.Live` and
26// assert per-request semantics — see26// assert per-request semantics — see
27// `apps/cloud/src/api.request-scope.node.test.ts`.27// `apps/cloud/src/api.request-scope.node.test.ts`.
28export const makeApiLive = (28export const makeApiLive = (requestScopedLive: Layer.Layer<DbService | UserStoreService>) =>
29 requestScopedLive: Layer.Layer<DbService | UserStoreService>,
30) =>
31 Layer.mergeAll(29 Layer.mergeAll(
32 makeNonProtectedApiLive(requestScopedLive),30 makeNonProtectedApiLive(requestScopedLive),
33 makeOrgApiLive(requestScopedLive),31 makeOrgApiLive(requestScopedLive),
34 makeProtectedApiLive(requestScopedLive),32 makeProtectedApiLive(requestScopedLive),
35 AutumnRoutesLive,33 AutumnRoutesLive,
36 SlackContactRoutesLive,34 SlackContactRoutesLive,
37 ).pipe(35 ).pipe(Layer.provideMerge(RouterConfig), Layer.provideMerge(BootSharedServices));
38 Layer.provideMerge(RouterConfig),
39 Layer.provideMerge(BootSharedServices),
40 );
41 36
42export const ApiLive = makeApiLive(RequestScopedServicesLive);37export const ApiLive = makeApiLive(RequestScopedServicesLive);
apps/cloud/src/api/slack.ts+1−5
@@ -3,11 +3,7 @@ import { Cause, Effect } from "effect"; · 2 unmodified lines
3import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";3import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
4 4
5import { SlackService } from "../services/slack";5import { SlackService } from "../services/slack";
6import {6import { HttpResponseError, isServerError, toErrorServerResponse } from "./error-response";
7 HttpResponseError,
8 isServerError,
9 toErrorServerResponse,
10} from "./error-response";
11 7
12const isValidEmail = (s: string): boolean => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(s);8const isValidEmail = (s: string): boolean => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(s);
13 9
apps/cloud/src/auth/context.ts+3−4
@@ -23,10 +23,9 @@ const makeService = (store: RawStore) => ({ · 22 unmodified lines
23 23
24type UserStoreServiceType = ReturnType<typeof makeService>;24type UserStoreServiceType = ReturnType<typeof makeService>;
25 25
26export class UserStoreService extends Context.Service<26export class UserStoreService extends Context.Service<UserStoreService, UserStoreServiceType>()(
27 UserStoreService,27 "@executor-js/cloud/UserStoreService",
28 UserStoreServiceType28) {
29>()("@executor-js/cloud/UserStoreService") {
30 static Live = Layer.effect(this)(29 static Live = Layer.effect(this)(
31 Effect.map(DbService.asEffect(), ({ db }) => makeService(makeUserStore(db))),30 Effect.map(DbService.asEffect(), ({ db }) => makeService(makeUserStore(db))),
32 );31 );
apps/cloud/src/auth/handlers.ts+8−14
@@ -304,8 +304,7 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 303 unmodified lines
304 ? yield* workos.getUser(inv.inviterUserId).pipe(304 ? yield* workos.getUser(inv.inviterUserId).pipe(
305 Effect.map((u) => ({305 Effect.map((u) => ({
306 email: u.email,306 email: u.email,
307 name:307 name: [u.firstName, u.lastName].filter(Boolean).join(" ") || null,
308 [u.firstName, u.lastName].filter(Boolean).join(" ") || null,
309 })),308 })),
310 Effect.orElseSucceed(() => null),309 Effect.orElseSucceed(() => null),
311 )310 )
@@ -354,20 +353,15 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 42 unmodified lines
354 // through login again. The acceptance has already succeeded353 // through login again. The acceptance has already succeeded
355 // server-side, so the next login will pick up the membership.354 // server-side, so the next login will pick up the membership.
356 const refreshed = yield* workos.refreshSession(session.sealedSession, org.id);355 const refreshed = yield* workos.refreshSession(session.sealedSession, org.id);
357 const verified = refreshed356 const verified = refreshed ? yield* workos.authenticateSealedSession(refreshed) : null;
358 ? yield* workos.authenticateSealedSession(refreshed)
359 : null;
360 357
361 if (!refreshed || !verified || verified.organizationId !== org.id) {358 if (!refreshed || !verified || verified.organizationId !== org.id) {
362 yield* Effect.logWarning(359 yield* Effect.logWarning("acceptInvitation: unable to attach org to current session", {
363 "acceptInvitation: unable to attach org to current session",360 userId: session.accountId,
364 {361 orgId: org.id,
365 userId: session.accountId,362 refreshReturnedSession: refreshed != null,
366 orgId: org.id,363 verifiedOrgId: verified?.organizationId ?? null,
367 refreshReturnedSession: refreshed != null,364 });
368 verifiedOrgId: verified?.organizationId ?? null,
369 },
370 );
371 deleteCookie("wos-session", { path: "/" });365 deleteCookie("wos-session", { path: "/" });
372 return yield* new WorkOSError();366 return yield* new WorkOSError();
373 }367 }
apps/cloud/src/auth/middleware.ts+11−10
@@ -23,8 +23,9 @@ export type Session = { · 22 unmodified lines
23 readonly refreshedSession: string | null;23 readonly refreshedSession: string | null;
24};24};
25 25
26export class SessionContext extends Context.Service<SessionContext, Session26export class SessionContext extends Context.Service<SessionContext, Session>()(
27>()("@executor-js/cloud/Session") {}27 "@executor-js/cloud/Session",
28) {}
28 29
29// ---------------------------------------------------------------------------30// ---------------------------------------------------------------------------
30// Errors31// Errors
@@ -71,12 +72,12 @@ export class AuthContext extends Context.Service< · 40 unmodified lines
71 }72 }
72>()("@executor-js/cloud/AuthContext") {}73>()("@executor-js/cloud/AuthContext") {}
73 74
74export class OrgAuth extends HttpApiMiddleware.Service<75export class OrgAuth extends HttpApiMiddleware.Service<OrgAuth, { provides: AuthContext }>()(
75 OrgAuth,76 "OrgAuth",
76 { provides: AuthContext }77 {
77>()("OrgAuth", {78 error: [Unauthorized, NoOrganization],
78 error: [Unauthorized, NoOrganization],79 security: {
79 security: {80 cookie: HttpApiSecurity.apiKey({ in: "cookie", key: "wos-session" }),
80 cookie: HttpApiSecurity.apiKey({ in: "cookie", key: "wos-session" }),81 },
81 },82 },
82}) {}83) {}
apps/cloud/src/auth/resolve-organization.ts+1−3
@@ -24,7 +24,5 @@ export const resolveOrganization = (organizationId: string) => · 23 unmodified lines
24 24
25 const workos = yield* WorkOSAuth;25 const workos = yield* WorkOSAuth;
26 const fresh = yield* workos.getOrganization(organizationId);26 const fresh = yield* workos.getOrganization(organizationId);
27 return yield* users.use((s) =>27 return yield* users.use((s) => s.upsertOrganization({ id: fresh.id, name: fresh.name }));
28 s.upsertOrganization({ id: fresh.id, name: fresh.name }),
29 );
30 });28 });
apps/cloud/src/auth/workos.ts+4−6
@@ -8,8 +8,7 @@ import { GeneratePortalLinkIntent, WorkOS } from "@workos-inc/node/worker"; · 7 unmodified lines
8import { WorkOSError, tryPromiseService, withServiceLogging } from "./errors";8import { WorkOSError, tryPromiseService, withServiceLogging } from "./errors";
9 9
10const COOKIE_NAME = "wos-session";10const COOKIE_NAME = "wos-session";
11const INVALID_COOKIE_PASSWORD_MESSAGE =11const INVALID_COOKIE_PASSWORD_MESSAGE = "WORKOS_COOKIE_PASSWORD must be at least 32 characters";
12 "WORKOS_COOKIE_PASSWORD must be at least 32 characters";
13 12
14class WorkOSAuthConfigurationError extends Data.TaggedError("WorkOSAuthConfigurationError")<{13class WorkOSAuthConfigurationError extends Data.TaggedError("WorkOSAuthConfigurationError")<{
15 readonly message: string;14 readonly message: string;
@@ -247,10 +246,9 @@ const make = Effect.gen(function* () { · 231 unmodified lines
247 246
248export type WorkOSAuthService = Effect.Success<typeof make>;247export type WorkOSAuthService = Effect.Success<typeof make>;
249 248
250export class WorkOSAuth extends Context.Service<249export class WorkOSAuth extends Context.Service<WorkOSAuth, WorkOSAuthService>()(
251 WorkOSAuth,250 "@executor-js/cloud/WorkOSAuth",
252 WorkOSAuthService251) {
253>()("@executor-js/cloud/WorkOSAuth") {
254 static Default = Layer.effect(this)(make).pipe(252 static Default = Layer.effect(this)(make).pipe(
255 Layer.withSpan("WorkOSAuth", { attributes: { module: "WorkOSAuth" } }),253 Layer.withSpan("WorkOSAuth", { attributes: { module: "WorkOSAuth" } }),
256 );254 );
apps/cloud/src/jwks-cache.node.test.ts+1−3
@@ -102,9 +102,7 @@ describe("createCachedRemoteJWKSet", () => { · 101 unmodified lines
102 const jwks = createCachedRemoteJWKSet(jwksUrl, { fetch });102 const jwks = createCachedRemoteJWKSet(jwksUrl, { fetch });
103 const token = await sign(kp);103 const token = await sign(kp);
104 104
105 const verifies = Array.from({ length: 10 }, () =>105 const verifies = Array.from({ length: 10 }, () => jwtVerify(token, jwks, { issuer, audience }));
106 jwtVerify(token, jwks, { issuer, audience }),
107 );
108 // Let microtasks settle so all 10 calls hit the cache miss path.106 // Let microtasks settle so all 10 calls hit the cache miss path.
109 await new Promise((r) => setTimeout(r, 10));107 await new Promise((r) => setTimeout(r, 10));
110 resolveFetch();108 resolveFetch();
apps/cloud/src/mcp-auth.node.test.ts+6−4
@@ -36,10 +36,12 @@ describe("MCP AuthKit token verification", () => { · 35 unmodified lines
36 const { jwks, sign } = yield* Effect.promise(() => makeVerifier());36 const { jwks, sign } = yield* Effect.promise(() => makeVerifier());
37 const token = yield* Effect.promise(() => sign({ aud: otherResource }));37 const token = yield* Effect.promise(() => sign({ aud: otherResource }));
38 38
39 const error = yield* Effect.flip(verifyMcpAccessToken(token, jwks, {39 const error = yield* Effect.flip(
40 issuer,40 verifyMcpAccessToken(token, jwks, {
41 audience: resource,41 issuer,
42 }));42 audience: resource,
43 }),
44 );
43 45
44 expect(error).toBeInstanceOf(McpJwtVerificationError);46 expect(error).toBeInstanceOf(McpJwtVerificationError);
45 }),47 }),
apps/cloud/src/mcp-flow.test.ts+7−12
@@ -45,10 +45,11 @@ const SESSION_TIMEOUT_MS = 5 * 60 * 1000; · 44 unmodified lines
45const SESSION_META_KEY = "session-meta";45const SESSION_META_KEY = "session-meta";
46const LAST_ACTIVITY_KEY = "last-activity-ms";46const LAST_ACTIVITY_KEY = "last-activity-ms";
47 47
48const doRuntimeControls = (instance: unknown): {48const doRuntimeControls = (
49 instance: unknown,
50): {
49 closeRuntime: () => Effect.Effect<void>;51 closeRuntime: () => Effect.Effect<void>;
50} =>52} => instance as { closeRuntime: () => Effect.Effect<void> };
51 instance as { closeRuntime: () => Effect.Effect<void> };
52 53
53const doActivityState = (instance: unknown): { lastActivityMs: number } =>54const doActivityState = (instance: unknown): { lastActivityMs: number } =>
54 instance as { lastActivityMs: number };55 instance as { lastActivityMs: number };
@@ -340,9 +341,7 @@ describe("/mcp session restore", () => { · 285 unmodified lines
340 const ns = env.MCP_SESSION;341 const ns = env.MCP_SESSION;
341 const stub = ns.get(ns.idFromString(sessionId!));342 const stub = ns.get(ns.idFromString(sessionId!));
342 await runInDurableObject(stub, async (instance) => {343 await runInDurableObject(stub, async (instance) => {
343 await Effect.runPromise(344 await Effect.runPromise(doRuntimeControls(instance).closeRuntime());
344 doRuntimeControls(instance).closeRuntime(),
345 );
346 });345 });
347 346
348 const response = await mcpPost({347 const response = await mcpPost({
@@ -376,9 +375,7 @@ describe("/mcp session restore", () => { · 27 unmodified lines
376 const ns = env.MCP_SESSION;375 const ns = env.MCP_SESSION;
377 const stub = ns.get(ns.idFromString(sessionId!));376 const stub = ns.get(ns.idFromString(sessionId!));
378 await runInDurableObject(stub, async (instance) => {377 await runInDurableObject(stub, async (instance) => {
379 await Effect.runPromise(378 await Effect.runPromise(doRuntimeControls(instance).closeRuntime());
380 doRuntimeControls(instance).closeRuntime(),
381 );
382 });379 });
383 380
384 const getResponse = await mcpGet({ bearer, sessionId: sessionId! });381 const getResponse = await mcpGet({ bearer, sessionId: sessionId! });
@@ -438,9 +435,7 @@ describe("/mcp session restore", () => { · 53 unmodified lines
438 const ns = env.MCP_SESSION;435 const ns = env.MCP_SESSION;
439 const stub = ns.get(ns.idFromString(sessionId!));436 const stub = ns.get(ns.idFromString(sessionId!));
440 await runInDurableObject(stub, async (instance) => {437 await runInDurableObject(stub, async (instance) => {
441 await Effect.runPromise(438 await Effect.runPromise(doRuntimeControls(instance).closeRuntime());
442 doRuntimeControls(instance).closeRuntime(),
443 );
444 });439 });
445 440
446 const response = await mcpPost({441 const response = await mcpPost({
apps/cloud/src/mcp-session.e2e.node.test.ts+7−10
@@ -33,10 +33,7 @@ import { · 32 unmodified lines
33 createExecutor,33 createExecutor,
34 definePlugin,34 definePlugin,
35} from "@executor-js/sdk";35} from "@executor-js/sdk";
36import {36import { makePostgresAdapter, makePostgresBlobStore } from "@executor-js/storage-postgres";
37 makePostgresAdapter,
38 makePostgresBlobStore,
39} from "@executor-js/storage-postgres";
40import { makeTestWorkOSVaultClient } from "@executor-js/plugin-workos-vault/testing";37import { makeTestWorkOSVaultClient } from "@executor-js/plugin-workos-vault/testing";
41import executorConfig from "../executor.config";38import executorConfig from "../executor.config";
42import { DbService } from "./services/db";39import { DbService } from "./services/db";
@@ -65,7 +62,11 @@ const elicitingTestPlugin = definePlugin(() => ({ · 22 unmodified lines
65 name: "needsApproval",62 name: "needsApproval",
66 description: "Tool that asks the caller to approve before returning.",63 description: "Tool that asks the caller to approve before returning.",
67 inputSchema: EMPTY_INPUT_SCHEMA,64 inputSchema: EMPTY_INPUT_SCHEMA,
68 handler: ({ elicit }: { elicit: (r: FormElicitation) => Effect.Effect<typeof ElicitationResponse.Type, unknown> }) =>65 handler: ({
66 elicit,
67 }: {
68 elicit: (r: FormElicitation) => Effect.Effect<typeof ElicitationResponse.Type, unknown>;
69 }) =>
69 Effect.gen(function* () {70 Effect.gen(function* () {
70 const response = yield* elicit(71 const response = yield* elicit(
71 new FormElicitation({72 new FormElicitation({
@@ -94,11 +95,7 @@ const ELICITATION_CAPS: ClientCapabilities = { · 22 unmodified lines
94 95
95type BuildOptions = { readonly withElicitingPlugin?: boolean };96type BuildOptions = { readonly withElicitingPlugin?: boolean };
96 97
97const buildScopedExecutor = (98const buildScopedExecutor = (scopeId: string, scopeName: string, options: BuildOptions = {}) =>
98 scopeId: string,
99 scopeName: string,
100 options: BuildOptions = {},
101) =>
102 Effect.gen(function* () {99 Effect.gen(function* () {
103 const { db } = yield* DbService;100 const { db } = yield* DbService;
104 const basePlugins = executorConfig.plugins({101 const basePlugins = executorConfig.plugins({
apps/cloud/src/routes/billing_.plans.tsx+74−63
@@ -381,69 +381,69 @@ function SlackContactCta() { · 380 unmodified lines
381 <span aria-hidden>→</span>381 <span aria-hidden>→</span>
382 </Button>382 </Button>
383 </DialogTrigger>383 </DialogTrigger>
384 <DialogContent>384 <DialogContent>
385 {inviteUrl ? (385 {inviteUrl ? (
386 <>386 <>
387 <DialogHeader>387 <DialogHeader>
388 <DialogTitle>Check your inbox</DialogTitle>388 <DialogTitle>Check your inbox</DialogTitle>
389 <DialogDescription>389 <DialogDescription>
390 We've created a private Slack channel and emailed you an invite. You can also390 We've created a private Slack channel and emailed you an invite. You can also
391 open it directly:391 open it directly:
392 </DialogDescription>392 </DialogDescription>
393 </DialogHeader>393 </DialogHeader>
394 <a394 <a
395 href={inviteUrl}395 href={inviteUrl}
396 target="_blank"396 target="_blank"
397 rel="noreferrer"397 rel="noreferrer"
398 className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-md bg-primary text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"398 className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-md bg-primary text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
399 >399 >
400 <SlackMark className="size-4" />400 <SlackMark className="size-4" />
401 Open Slack invite401 Open Slack invite
402 </a>402 </a>
403 <DialogFooter>403 <DialogFooter>
404 <DialogClose asChild>404 <DialogClose asChild>
405 <Button type="button" variant="outline">405 <Button type="button" variant="outline">
406 Done406 Done
407 </Button>407 </Button>
408 </DialogClose>408 </DialogClose>
409 </DialogFooter>409 </DialogFooter>
410 </>410 </>
411 ) : (411 ) : (
412 <form onSubmit={onSubmit}>412 <form onSubmit={onSubmit}>
413 <DialogHeader>413 <DialogHeader>
414 <DialogTitle>Get in touch on Slack</DialogTitle>414 <DialogTitle>Get in touch on Slack</DialogTitle>
415 <DialogDescription>415 <DialogDescription>
416 We'll create a private Slack Connect channel between you and the Executor team.416 We'll create a private Slack Connect channel between you and the Executor team.
417 </DialogDescription>417 </DialogDescription>
418 </DialogHeader>418 </DialogHeader>
419 <div className="grid gap-4 py-4">419 <div className="grid gap-4 py-4">
420 <div className="grid gap-2">420 <div className="grid gap-2">
421 <Label htmlFor="slack-contact-email">Work email</Label>421 <Label htmlFor="slack-contact-email">Work email</Label>
422 <Input422 <Input
423 id="slack-contact-email"423 id="slack-contact-email"
424 type="email"424 type="email"
425 autoComplete="email"425 autoComplete="email"
426 required426 required
427 value={email}427 value={email}
428 onChange={(e) => setEmail(e.currentTarget.value)}428 onChange={(e) => setEmail(e.currentTarget.value)}
429 placeholder="you@company.com"429 placeholder="you@company.com"
430 />430 />
431 </div>
432 {error && <p className="text-sm text-destructive">{error}</p>}
431 </div>433 </div>
432 {error && <p className="text-sm text-destructive">{error}</p>}434 <DialogFooter>
433 </div>435 <DialogClose asChild>
434 <DialogFooter>436 <Button type="button" variant="outline" disabled={submitting}>
435 <DialogClose asChild>437 Cancel
436 <Button type="button" variant="outline" disabled={submitting}>438 </Button>
437 Cancel439 </DialogClose>
440 <Button type="submit" disabled={submitting || !email}>
441 {submitting ? "Sending…" : "Send invite"}
438 </Button>442 </Button>
439 </DialogClose>443 </DialogFooter>
440 <Button type="submit" disabled={submitting || !email}>444 </form>
441 {submitting ? "Sending…" : "Send invite"}445 )}
442 </Button>446 </DialogContent>
443 </DialogFooter>
444 </form>
445 )}
446 </DialogContent>
447 </Dialog>447 </Dialog>
448 <span className="text-muted-foreground/60" aria-hidden>448 <span className="text-muted-foreground/60" aria-hidden>
449 ·449 ·
@@ -463,8 +463,19 @@ function SlackContactCta() { · 13 unmodified lines
463 463
464function MailIcon({ className }: { className?: string }) {464function MailIcon({ className }: { className?: string }) {
465 return (465 return (
466 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className={className} aria-hidden>466 <svg
467 <path strokeLinecap="round" strokeLinejoin="round" d="M3 7l9 6 9-6M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />467 viewBox="0 0 24 24"
468 fill="none"
469 stroke="currentColor"
470 strokeWidth="1.8"
471 className={className}
472 aria-hidden
473 >
474 <path
475 strokeLinecap="round"
476 strokeLinejoin="round"
477 d="M3 7l9 6 9-6M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
478 />
468 </svg>479 </svg>
469 );480 );
470}481}
apps/cloud/src/routes/sources.add.$pluginKey.tsx+1−8
@@ -15,13 +15,6 @@ export const Route = createFileRoute("/sources/add/$pluginKey")({ · 14 unmodified lines
15 component: () => {15 component: () => {
16 const { pluginKey } = Route.useParams();16 const { pluginKey } = Route.useParams();
17 const { url, preset, namespace } = Route.useSearch();17 const { url, preset, namespace } = Route.useSearch();
18 return (18 return <SourcesAddPage pluginKey={pluginKey} url={url} preset={preset} namespace={namespace} />;
19 <SourcesAddPage
20 pluginKey={pluginKey}
21 url={url}
22 preset={preset}
23 namespace={namespace}
24 />
25 );
26 },19 },
27});20});
apps/cloud/src/secrets-isolation.e2e.node.test.ts+153−165
@@ -31,192 +31,180 @@ import { Effect, Result } from "effect"; · 30 unmodified lines
31 31
32import { ScopeId, SecretId } from "@executor-js/sdk";32import { ScopeId, SecretId } from "@executor-js/sdk";
33 33
34import {34import { asUser, testUserOrgScopeId } from "./services/__test-harness__/api-harness";
35 asUser,
36 testUserOrgScopeId,
37} from "./services/__test-harness__/api-harness";
38 35
39const uniq = () => crypto.randomUUID().slice(0, 8);36const uniq = () => crypto.randomUUID().slice(0, 8);
40const nextOrgId = () => `org_iso_${uniq()}`;37const nextOrgId = () => `org_iso_${uniq()}`;
41const nextUserId = () => `user_iso_${uniq()}`;38const nextUserId = () => `user_iso_${uniq()}`;
42 39
43describe("cloud secret isolation (HTTP, user-org scope stack)", () => {40describe("cloud secret isolation (HTTP, user-org scope stack)", () => {
44 it.effect(41 it.effect("users in different orgs cannot read each other's org-scoped secrets", () =>
45 "users in different orgs cannot read each other's org-scoped secrets",42 Effect.gen(function* () {
46 () =>43 const orgA = nextOrgId();
47 Effect.gen(function* () {44 const orgB = nextOrgId();
48 const orgA = nextOrgId();45 const alice = nextUserId();
49 const orgB = nextOrgId();46 const charlie = nextUserId();
50 const alice = nextUserId();47 const id = `sec_${uniq()}`;
51 const charlie = nextUserId();48
52 const id = `sec_${uniq()}`;49 yield* asUser(alice, orgA, (client) =>
53 50 client.secrets.set({
54 yield* asUser(alice, orgA, (client) =>51 params: { scopeId: ScopeId.make(orgA) },
55 client.secrets.set({52 payload: {
56 params: { scopeId: ScopeId.make(orgA) },53 id: SecretId.make(id),
57 payload: {54 name: "Shared",
58 id: SecretId.make(id),55 value: "alice-org-secret",
59 name: "Shared",56 },
60 value: "alice-org-secret",57 }),
61 },58 );
62 }),
63 );
64
65 const charlieStatus = yield* asUser(charlie, orgB, (client) =>
66 client.secrets.status({
67 params: { scopeId: ScopeId.make(orgB), secretId: SecretId.make(id) },
68 }),
69 );
70 expect(charlieStatus.status).toBe("missing");
71 59
72 const charlieList = yield* asUser(charlie, orgB, (client) =>60 const charlieStatus = yield* asUser(charlie, orgB, (client) =>
73 client.secrets.list({ params: { scopeId: ScopeId.make(orgB) } }),61 client.secrets.status({
74 );62 params: { scopeId: ScopeId.make(orgB), secretId: SecretId.make(id) },
75 expect(charlieList.map((s) => s.id)).not.toContain(id);63 }),
64 );
65 expect(charlieStatus.status).toBe("missing");
76 66
77 }),67 const charlieList = yield* asUser(charlie, orgB, (client) =>
68 client.secrets.list({ params: { scopeId: ScopeId.make(orgB) } }),
69 );
70 expect(charlieList.map((s) => s.id)).not.toContain(id);
71 }),
78 );72 );
79 73
80 it.effect(74 it.effect("users in same org cannot read each other's user-scoped secrets", () =>
81 "users in same org cannot read each other's user-scoped secrets",75 Effect.gen(function* () {
82 () =>76 const orgId = nextOrgId();
83 Effect.gen(function* () {77 const aliceId = nextUserId();
84 const orgId = nextOrgId();78 const bobId = nextUserId();
85 const aliceId = nextUserId();79 const id = `sec_${uniq()}`;
86 const bobId = nextUserId();80
87 const id = `sec_${uniq()}`;81 // Alice writes at her per-user scope — where OAuth tokens land.
88 82 yield* asUser(aliceId, orgId, (client) =>
89 // Alice writes at her per-user scope — where OAuth tokens land.83 client.secrets.set({
90 yield* asUser(aliceId, orgId, (client) =>84 params: { scopeId: ScopeId.make(testUserOrgScopeId(aliceId, orgId)) },
91 client.secrets.set({85 payload: {
92 params: { scopeId: ScopeId.make(testUserOrgScopeId(aliceId, orgId)) },86 id: SecretId.make(id),
93 payload: {87 name: "Alice's token",
94 id: SecretId.make(id),88 value: "alice-token-value",
95 name: "Alice's token",89 },
96 value: "alice-token-value",90 }),
97 },91 );
98 }),
99 );
100 92
101 // Bob is in the same org — his user-org scope differs. He should93 // Bob is in the same org — his user-org scope differs. He should
102 // not see the token in a list.94 // not see the token in a list.
103 const bobList = yield* asUser(bobId, orgId, (client) =>95 const bobList = yield* asUser(bobId, orgId, (client) =>
104 client.secrets.list({96 client.secrets.list({
105 params: { scopeId: ScopeId.make(testUserOrgScopeId(bobId, orgId)) },97 params: { scopeId: ScopeId.make(testUserOrgScopeId(bobId, orgId)) },
106 }),98 }),
107 );99 );
108 expect(bobList.map((s) => s.id)).not.toContain(id);100 expect(bobList.map((s) => s.id)).not.toContain(id);
109 101
110 const bobStatus = yield* asUser(bobId, orgId, (client) =>102 const bobStatus = yield* asUser(bobId, orgId, (client) =>
111 client.secrets.status({103 client.secrets.status({
112 params: {104 params: {
113 scopeId: ScopeId.make(testUserOrgScopeId(bobId, orgId)),105 scopeId: ScopeId.make(testUserOrgScopeId(bobId, orgId)),
114 secretId: SecretId.make(id),106 secretId: SecretId.make(id),
115 },107 },
116 }),108 }),
117 );109 );
118 expect(bobStatus.status).toBe("missing");110 expect(bobStatus.status).toBe("missing");
119 111
120 // And Alice still sees her own token metadata.112 // And Alice still sees her own token metadata.
121 const aliceStatus = yield* asUser(aliceId, orgId, (client) =>113 const aliceStatus = yield* asUser(aliceId, orgId, (client) =>
122 client.secrets.status({114 client.secrets.status({
123 params: {115 params: {
124 scopeId: ScopeId.make(testUserOrgScopeId(aliceId, orgId)),116 scopeId: ScopeId.make(testUserOrgScopeId(aliceId, orgId)),
125 secretId: SecretId.make(id),117 secretId: SecretId.make(id),
126 },118 },
127 }),119 }),
128 );120 );
129 expect(aliceStatus.status).toBe("resolved");121 expect(aliceStatus.status).toBe("resolved");
130 }),122 }),
131 );123 );
132 124
133 it.effect(125 it.effect("org-scoped secrets are visible to every user in that org", () =>
134 "org-scoped secrets are visible to every user in that org",126 Effect.gen(function* () {
135 () =>127 const orgId = nextOrgId();
136 Effect.gen(function* () {128 const adminId = nextUserId();
137 const orgId = nextOrgId();129 const memberId = nextUserId();
138 const adminId = nextUserId();130 const id = `sec_${uniq()}`;
139 const memberId = nextUserId();131
140 const id = `sec_${uniq()}`;132 yield* asUser(adminId, orgId, (client) =>
141 133 client.secrets.set({
142 yield* asUser(adminId, orgId, (client) =>134 params: { scopeId: ScopeId.make(orgId) },
143 client.secrets.set({135 payload: {
144 params: { scopeId: ScopeId.make(orgId) },136 id: SecretId.make(id),
145 payload: {137 name: "Org API Key",
146 id: SecretId.make(id),138 value: "shared-org-key",
147 name: "Org API Key",139 },
148 value: "shared-org-key",140 }),
149 },141 );
150 }),
151 );
152 142
153 const adminStatus = yield* asUser(adminId, orgId, (client) =>143 const adminStatus = yield* asUser(adminId, orgId, (client) =>
154 client.secrets.status({144 client.secrets.status({
155 params: { scopeId: ScopeId.make(orgId), secretId: SecretId.make(id) },145 params: { scopeId: ScopeId.make(orgId), secretId: SecretId.make(id) },
156 }),146 }),
157 );147 );
158 const memberStatus = yield* asUser(memberId, orgId, (client) =>148 const memberStatus = yield* asUser(memberId, orgId, (client) =>
159 client.secrets.status({149 client.secrets.status({
160 params: { scopeId: ScopeId.make(orgId), secretId: SecretId.make(id) },150 params: { scopeId: ScopeId.make(orgId), secretId: SecretId.make(id) },
161 }),151 }),
162 );152 );
163 expect(adminStatus.status).toBe("resolved");153 expect(adminStatus.status).toBe("resolved");
164 expect(memberStatus.status).toBe("resolved");154 expect(memberStatus.status).toBe("resolved");
165 }),155 }),
166 );156 );
167 157
168 it.effect(158 it.effect("same userId in different orgs gets distinct per-user scopes", () =>
169 "same userId in different orgs gets distinct per-user scopes",159 Effect.gen(function* () {
170 () =>160 const userId = nextUserId();
171 Effect.gen(function* () {161 const orgA = nextOrgId();
172 const userId = nextUserId();162 const orgB = nextOrgId();
173 const orgA = nextOrgId();163 const id = `sec_${uniq()}`;
174 const orgB = nextOrgId();164
175 const id = `sec_${uniq()}`;165 yield* asUser(userId, orgA, (client) =>
176 166 client.secrets.set({
177 yield* asUser(userId, orgA, (client) =>167 params: { scopeId: ScopeId.make(testUserOrgScopeId(userId, orgA)) },
178 client.secrets.set({168 payload: {
179 params: { scopeId: ScopeId.make(testUserOrgScopeId(userId, orgA)) },169 id: SecretId.make(id),
180 payload: {170 name: "A token",
181 id: SecretId.make(id),171 value: "value-in-a",
182 name: "A token",172 },
183 value: "value-in-a",173 }),
184 },174 );
185 }),
186 );
187 175
188 // Same user id, different org → distinct user-org scope. The176 // Same user id, different org → distinct user-org scope. The
189 // secret written in org A must not be visible when the same user177 // secret written in org A must not be visible when the same user
190 // logs into org B.178 // logs into org B.
191 const listInB = yield* asUser(userId, orgB, (client) =>179 const listInB = yield* asUser(userId, orgB, (client) =>
192 client.secrets.list({180 client.secrets.list({
193 params: { scopeId: ScopeId.make(testUserOrgScopeId(userId, orgB)) },181 params: { scopeId: ScopeId.make(testUserOrgScopeId(userId, orgB)) },
194 }),182 }),
195 );183 );
196 expect(listInB.map((s) => s.id)).not.toContain(id);184 expect(listInB.map((s) => s.id)).not.toContain(id);
197 185
198 const statusInB = yield* asUser(userId, orgB, (client) =>186 const statusInB = yield* asUser(userId, orgB, (client) =>
199 client.secrets.status({187 client.secrets.status({
200 params: {188 params: {
201 scopeId: ScopeId.make(testUserOrgScopeId(userId, orgB)),189 scopeId: ScopeId.make(testUserOrgScopeId(userId, orgB)),
202 secretId: SecretId.make(id),190 secretId: SecretId.make(id),
203 },191 },
204 }),192 }),
205 );193 );
206 expect(statusInB.status).toBe("missing");194 expect(statusInB.status).toBe("missing");
207 195
208 // Sanity: the original write is still visible under the org-A196 // Sanity: the original write is still visible under the org-A
209 // user-org scope.197 // user-org scope.
210 const statusInA = yield* asUser(userId, orgA, (client) =>198 const statusInA = yield* asUser(userId, orgA, (client) =>
211 client.secrets.status({199 client.secrets.status({
212 params: {200 params: {
213 scopeId: ScopeId.make(testUserOrgScopeId(userId, orgA)),201 scopeId: ScopeId.make(testUserOrgScopeId(userId, orgA)),
214 secretId: SecretId.make(id),202 secretId: SecretId.make(id),
215 },203 },
216 }),204 }),
217 );205 );
218 expect(statusInA.status).toBe("resolved");206 expect(statusInA.status).toBe("resolved");
219 }),207 }),
220 );208 );
221 209
222 it.effect("secrets.set rejects a scope outside the executor's stack", () =>210 it.effect("secrets.set rejects a scope outside the executor's stack", () =>
apps/cloud/src/services/__test-harness__/api-harness.ts+9−35
@@ -16,12 +16,7 @@ · 15 unmodified lines
16 16
17import { Effect, Layer } from "effect";17import { Effect, Layer } from "effect";
18import { HttpApiBuilder, HttpApiClient, HttpApiSwagger } from "effect/unstable/httpapi";18import { HttpApiBuilder, HttpApiClient, HttpApiSwagger } from "effect/unstable/httpapi";
19import {19import { FetchHttpClient, HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http";
20 FetchHttpClient,
21 HttpRouter,
22 HttpServer,
23 HttpServerRequest,
24} from "effect/unstable/http";
25 20
26import {21import {
27 ExecutionEngineService,22 ExecutionEngineService,
@@ -31,16 +26,8 @@ import { · 3 unmodified lines
31} from "@executor-js/api/server";26} from "@executor-js/api/server";
32import { createExecutionEngine } from "@executor-js/execution";27import { createExecutionEngine } from "@executor-js/execution";
33import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";28import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";
34import {29import { Scope, ScopeId, collectSchemas, createExecutor } from "@executor-js/sdk";
35 Scope,30import { makePostgresAdapter, makePostgresBlobStore } from "@executor-js/storage-postgres";
36 ScopeId,
37 collectSchemas,
38 createExecutor,
39} from "@executor-js/sdk";
40import {
41 makePostgresAdapter,
42 makePostgresBlobStore,
43} from "@executor-js/storage-postgres";
44import { makeTestWorkOSVaultClient } from "@executor-js/plugin-workos-vault/testing";31import { makeTestWorkOSVaultClient } from "@executor-js/plugin-workos-vault/testing";
45 32
46import executorConfig from "../../../executor.config";33import executorConfig from "../../../executor.config";
@@ -59,8 +46,7 @@ export const TEST_USER_HEADER = "x-test-user-id"; · 12 unmodified lines
59// Mirrors apps/cloud/src/services/executor.ts#createScopedExecutor — the46// Mirrors apps/cloud/src/services/executor.ts#createScopedExecutor — the
60// per-user scope id bakes in the org so the same user id in a different47// per-user scope id bakes in the org so the same user id in a different
61// org gets a distinct scope row.48// org gets a distinct scope row.
62const userOrgScopeId = (userId: string, orgId: string) =>49const userOrgScopeId = (userId: string, orgId: string) => `user-org:${userId}:${orgId}`;
63 `user-org:${userId}:${orgId}`;
64 50
65// `asOrg(orgId, …)` callers don't care which specific user they are, only51// `asOrg(orgId, …)` callers don't care which specific user they are, only
66// that the executor has a valid user-org scope. We give each org a stable52// that the executor has a valid user-org scope. We give each org a stable
@@ -77,11 +63,7 @@ const defaultUserFor = (orgId: string) => `default_user_${orgId}`; · 10 unmodified lines
77const fakeVault = makeTestWorkOSVaultClient();63const fakeVault = makeTestWorkOSVaultClient();
78const testPlugins = executorConfig.plugins({ workosVaultClient: fakeVault });64const testPlugins = executorConfig.plugins({ workosVaultClient: fakeVault });
79 65
80const createTestScopedExecutor = (66const createTestScopedExecutor = (userId: string, orgId: string, orgName: string) =>
81 userId: string,
82 orgId: string,
83 orgName: string,
84) =>
85 Effect.gen(function* () {67 Effect.gen(function* () {
86 const { db } = yield* DbService;68 const { db } = yield* DbService;
87 const plugins = testPlugins;69 const plugins = testPlugins;
@@ -188,10 +170,7 @@ export const fetchForOrg = (orgId: string): typeof globalThis.fetch => · 100 unmodified lines
188 return handler(req);170 return handler(req);
189 }) as typeof globalThis.fetch;171 }) as typeof globalThis.fetch;
190 172
191export const fetchForUser = (173export const fetchForUser = (userId: string, orgId: string): typeof globalThis.fetch =>
192 userId: string,
193 orgId: string,
194): typeof globalThis.fetch =>
195 ((input: RequestInfo | URL, init?: RequestInit) => {174 ((input: RequestInfo | URL, init?: RequestInit) => {
196 const base = input instanceof Request ? input : new Request(input, init);175 const base = input instanceof Request ? input : new Request(input, init);
197 const req = new Request(base, {176 const req = new Request(base, {
@@ -211,9 +190,7 @@ export const clientLayerForOrg = (orgId: string) => · 13 unmodified lines
211 190
212export const clientLayerForUser = (userId: string, orgId: string) =>191export const clientLayerForUser = (userId: string, orgId: string) =>
213 FetchHttpClient.layer.pipe(192 FetchHttpClient.layer.pipe(
214 Layer.provide(193 Layer.provide(Layer.succeed(FetchHttpClient.Fetch)(fetchForUser(userId, orgId))),
215 Layer.succeed(FetchHttpClient.Fetch)(fetchForUser(userId, orgId)),
216 ),
217 );194 );
218 195
219// Constructs an HttpApiClient bound to the given org, hands it to `body`,196// Constructs an HttpApiClient bound to the given org, hands it to `body`,
@@ -242,14 +219,11 @@ export const asUser = <A, E>( · 22 unmodified lines
242 Effect.gen(function* () {219 Effect.gen(function* () {
243 const client = yield* HttpApiClient.make(ProtectedCloudApi, { baseUrl: TEST_BASE_URL });220 const client = yield* HttpApiClient.make(ProtectedCloudApi, { baseUrl: TEST_BASE_URL });
244 return yield* body(client);221 return yield* body(client);
245 }).pipe(222 }).pipe(Effect.provide(clientLayerForUser(userId, orgId))) as Effect.Effect<A, E>;
246 Effect.provide(clientLayerForUser(userId, orgId)),
247 ) as Effect.Effect<A, E>;
248 223
249// Exposed so tests can build the same user-org scope id the harness uses224// Exposed so tests can build the same user-org scope id the harness uses
250// when writing at a specific user's scope.225// when writing at a specific user's scope.
251export const testUserOrgScopeId = (userId: string, orgId: string) =>226export const testUserOrgScopeId = (userId: string, orgId: string) => userOrgScopeId(userId, orgId);
252 userOrgScopeId(userId, orgId);
253 227
254// Re-exports so call sites don't need a second import.228// Re-exports so call sites don't need a second import.
255export { ProtectedCloudApi };229export { ProtectedCloudApi };
apps/cloud/src/services/db.schema.test.ts+1−4
@@ -24,10 +24,7 @@ import { combinedSchema } from "./db"; · 23 unmodified lines
24 24
25describe("combinedSchema", () => {25describe("combinedSchema", () => {
26 it("spreads every cloud + executor schema export", () => {26 it("spreads every cloud + executor schema export", () => {
27 const expected = new Set([27 const expected = new Set([...Object.keys(cloudSchema), ...Object.keys(executorSchema)]);
28 ...Object.keys(cloudSchema),
29 ...Object.keys(executorSchema),
30 ]);
31 for (const key of expected) {28 for (const key of expected) {
32 expect(combinedSchema, `combinedSchema missing "${key}"`).toHaveProperty(key);29 expect(combinedSchema, `combinedSchema missing "${key}"`).toHaveProperty(key);
33 }30 }
apps/cloud/src/services/db.ts+5−5
@@ -60,11 +60,11 @@ const makeSql = (): Sql => · 59 unmodified lines
60 onnotice: () => undefined,60 onnotice: () => undefined,
61 });61 });
62 62
63export class DbService extends Context.Service<63export class DbService extends Context.Service<DbService, DbServiceShape>()(
64 DbService,64 "@executor-js/cloud/DbService",
65 DbServiceShape65) {
66>()("@executor-js/cloud/DbService") {66 static Live = Layer.effect(this)(
67 static Live = Layer.effect(this)(Effect.acquireRelease(67 Effect.acquireRelease(
68 Effect.sync((): DbServiceShape => {68 Effect.sync((): DbServiceShape => {
69 const sql = makeSql();69 const sql = makeSql();
70 return { sql, db: drizzle(sql, { schema: combinedSchema }) as DrizzleDb };70 return { sql, db: drizzle(sql, { schema: combinedSchema }) as DrizzleDb };
apps/cloud/src/services/execution-stack.ts+3−5
@@ -20,11 +20,9 @@ export const makeExecutionStack = ( · 19 unmodified lines
20 organizationName: string,20 organizationName: string,
21) =>21) =>
22 Effect.gen(function* () {22 Effect.gen(function* () {
23 const executor = yield* createScopedExecutor(23 const executor = yield* createScopedExecutor(userId, organizationId, organizationName).pipe(
24 userId,24 Effect.withSpan("McpSessionDO.createScopedExecutor"),
25 organizationId,25 );
26 organizationName,
27 ).pipe(Effect.withSpan("McpSessionDO.createScopedExecutor"));
28 const codeExecutor = makeDynamicWorkerExecutor({ loader: env.LOADER });26 const codeExecutor = makeDynamicWorkerExecutor({ loader: env.LOADER });
29 const autumn = yield* AutumnService;27 const autumn = yield* AutumnService;
30 const engine = withExecutionUsageTracking(28 const engine = withExecutionUsageTracking(
apps/cloud/src/services/executor-schema.ts+430−327
@@ -1,350 +1,453 @@
1import { pgTable, text, boolean, timestamp, bigint, jsonb, index, primaryKey } from "drizzle-orm/pg-core";1import {
2 pgTable,
3 text,
4 boolean,
5 timestamp,
6 bigint,
7 jsonb,
8 index,
9 primaryKey,
10} from "drizzle-orm/pg-core";
2 11
3export const source = pgTable("source", {12export const source = pgTable(
4 id: text('id').notNull(),13 "source",
5 scope_id: text('scope_id').notNull(),14 {
6 plugin_id: text('plugin_id').notNull(),15 id: text("id").notNull(),
7 kind: text('kind').notNull(),16 scope_id: text("scope_id").notNull(),
8 name: text('name').notNull(),17 plugin_id: text("plugin_id").notNull(),
9 url: text('url'),18 kind: text("kind").notNull(),
10 can_remove: boolean('can_remove').default(true).notNull(),19 name: text("name").notNull(),
11 can_refresh: boolean('can_refresh').default(false).notNull(),20 url: text("url"),
12 can_edit: boolean('can_edit').default(false).notNull(),21 can_remove: boolean("can_remove").default(true).notNull(),
13 created_at: timestamp('created_at').notNull(),22 can_refresh: boolean("can_refresh").default(false).notNull(),
14 updated_at: timestamp('updated_at').notNull()23 can_edit: boolean("can_edit").default(false).notNull(),
15}, (table) => [24 created_at: timestamp("created_at").notNull(),
16 primaryKey({ columns: [table.scope_id, table.id] }),25 updated_at: timestamp("updated_at").notNull(),
17 index("source_scope_id_idx").on(table.scope_id),26 },
18 index("source_plugin_id_idx").on(table.plugin_id),27 (table) => [
19]);28 primaryKey({ columns: [table.scope_id, table.id] }),
29 index("source_scope_id_idx").on(table.scope_id),
30 index("source_plugin_id_idx").on(table.plugin_id),
31 ],
32);
20 33
21export const tool = pgTable("tool", {34export const tool = pgTable(
22 id: text('id').notNull(),35 "tool",
23 scope_id: text('scope_id').notNull(),36 {
24 source_id: text('source_id').notNull(),37 id: text("id").notNull(),
25 plugin_id: text('plugin_id').notNull(),38 scope_id: text("scope_id").notNull(),
26 name: text('name').notNull(),39 source_id: text("source_id").notNull(),
27 description: text('description').notNull(),40 plugin_id: text("plugin_id").notNull(),
28 input_schema: jsonb('input_schema'),41 name: text("name").notNull(),
29 output_schema: jsonb('output_schema'),42 description: text("description").notNull(),
30 created_at: timestamp('created_at').notNull(),43 input_schema: jsonb("input_schema"),
31 updated_at: timestamp('updated_at').notNull()44 output_schema: jsonb("output_schema"),
32}, (table) => [45 created_at: timestamp("created_at").notNull(),
33 primaryKey({ columns: [table.scope_id, table.id] }),46 updated_at: timestamp("updated_at").notNull(),
34 index("tool_scope_id_idx").on(table.scope_id),47 },
35 index("tool_source_id_idx").on(table.source_id),48 (table) => [
36 index("tool_plugin_id_idx").on(table.plugin_id),49 primaryKey({ columns: [table.scope_id, table.id] }),
37]);50 index("tool_scope_id_idx").on(table.scope_id),
51 index("tool_source_id_idx").on(table.source_id),
52 index("tool_plugin_id_idx").on(table.plugin_id),
53 ],
54);
38 55
39export const definition = pgTable("definition", {56export const definition = pgTable(
40 id: text('id').notNull(),57 "definition",
41 scope_id: text('scope_id').notNull(),58 {
42 source_id: text('source_id').notNull(),59 id: text("id").notNull(),
43 plugin_id: text('plugin_id').notNull(),60 scope_id: text("scope_id").notNull(),
44 name: text('name').notNull(),61 source_id: text("source_id").notNull(),
45 schema: jsonb('schema').notNull(),62 plugin_id: text("plugin_id").notNull(),
46 created_at: timestamp('created_at').notNull()63 name: text("name").notNull(),
47}, (table) => [64 schema: jsonb("schema").notNull(),
48 primaryKey({ columns: [table.scope_id, table.id] }),65 created_at: timestamp("created_at").notNull(),
49 index("definition_scope_id_idx").on(table.scope_id),66 },
50 index("definition_source_id_idx").on(table.source_id),67 (table) => [
51 index("definition_plugin_id_idx").on(table.plugin_id),68 primaryKey({ columns: [table.scope_id, table.id] }),
52]);69 index("definition_scope_id_idx").on(table.scope_id),
70 index("definition_source_id_idx").on(table.source_id),
71 index("definition_plugin_id_idx").on(table.plugin_id),
72 ],
73);
53 74
54export const secret = pgTable("secret", {75export const secret = pgTable(
55 id: text('id').notNull(),76 "secret",
56 scope_id: text('scope_id').notNull(),77 {
57 name: text('name').notNull(),78 id: text("id").notNull(),
58 provider: text('provider').notNull(),79 scope_id: text("scope_id").notNull(),
59 owned_by_connection_id: text('owned_by_connection_id'),80 name: text("name").notNull(),
60 created_at: timestamp('created_at').notNull()81 provider: text("provider").notNull(),
61}, (table) => [82 owned_by_connection_id: text("owned_by_connection_id"),
62 primaryKey({ columns: [table.scope_id, table.id] }),83 created_at: timestamp("created_at").notNull(),
63 index("secret_scope_id_idx").on(table.scope_id),84 },
64 index("secret_provider_idx").on(table.provider),85 (table) => [
65 index("secret_owned_by_connection_id_idx").on(table.owned_by_connection_id),86 primaryKey({ columns: [table.scope_id, table.id] }),
66]);87 index("secret_scope_id_idx").on(table.scope_id),
88 index("secret_provider_idx").on(table.provider),
89 index("secret_owned_by_connection_id_idx").on(table.owned_by_connection_id),
90 ],
91);
67 92
68export const connection = pgTable("connection", {93export const connection = pgTable(
69 id: text('id').notNull(),94 "connection",
70 scope_id: text('scope_id').notNull(),95 {
71 provider: text('provider').notNull(),96 id: text("id").notNull(),
72 identity_label: text('identity_label'),97 scope_id: text("scope_id").notNull(),
73 access_token_secret_id: text('access_token_secret_id').notNull(),98 provider: text("provider").notNull(),
74 refresh_token_secret_id: text('refresh_token_secret_id'),99 identity_label: text("identity_label"),
75 expires_at: bigint('expires_at', { mode: 'number' }),100 access_token_secret_id: text("access_token_secret_id").notNull(),
76 scope: text('scope'),101 refresh_token_secret_id: text("refresh_token_secret_id"),
77 provider_state: jsonb('provider_state'),102 expires_at: bigint("expires_at", { mode: "number" }),
78 created_at: timestamp('created_at').notNull(),103 scope: text("scope"),
79 updated_at: timestamp('updated_at').notNull()104 provider_state: jsonb("provider_state"),
80}, (table) => [105 created_at: timestamp("created_at").notNull(),
81 primaryKey({ columns: [table.scope_id, table.id] }),106 updated_at: timestamp("updated_at").notNull(),
82 index("connection_scope_id_idx").on(table.scope_id),107 },
83 index("connection_provider_idx").on(table.provider),108 (table) => [
84]);109 primaryKey({ columns: [table.scope_id, table.id] }),
110 index("connection_scope_id_idx").on(table.scope_id),
111 index("connection_provider_idx").on(table.provider),
112 ],
113);
85 114
86export const oauth2_session = pgTable("oauth2_session", {115export const oauth2_session = pgTable(
87 id: text('id').notNull(),116 "oauth2_session",
88 scope_id: text('scope_id').notNull(),117 {
89 plugin_id: text('plugin_id').notNull(),118 id: text("id").notNull(),
90 strategy: text('strategy').notNull(),119 scope_id: text("scope_id").notNull(),
91 connection_id: text('connection_id').notNull(),120 plugin_id: text("plugin_id").notNull(),
92 token_scope: text('token_scope').notNull(),121 strategy: text("strategy").notNull(),
93 redirect_url: text('redirect_url').notNull(),122 connection_id: text("connection_id").notNull(),
94 payload: jsonb('payload').notNull(),123 token_scope: text("token_scope").notNull(),
95 expires_at: bigint('expires_at', { mode: 'number' }).notNull(),124 redirect_url: text("redirect_url").notNull(),
96 created_at: timestamp('created_at').notNull()125 payload: jsonb("payload").notNull(),
97}, (table) => [126 expires_at: bigint("expires_at", { mode: "number" }).notNull(),
98 primaryKey({ columns: [table.scope_id, table.id] }),127 created_at: timestamp("created_at").notNull(),
99 index("oauth2_session_scope_id_idx").on(table.scope_id),128 },
100 index("oauth2_session_plugin_id_idx").on(table.plugin_id),129 (table) => [
101 index("oauth2_session_connection_id_idx").on(table.connection_id),130 primaryKey({ columns: [table.scope_id, table.id] }),
102]);131 index("oauth2_session_scope_id_idx").on(table.scope_id),
132 index("oauth2_session_plugin_id_idx").on(table.plugin_id),
133 index("oauth2_session_connection_id_idx").on(table.connection_id),
134 ],
135);
103 136
104export const tool_policy = pgTable("tool_policy", {137export const tool_policy = pgTable(
105 id: text('id').notNull(),138 "tool_policy",
106 scope_id: text('scope_id').notNull(),139 {
107 pattern: text('pattern').notNull(),140 id: text("id").notNull(),
108 action: text('action').notNull(),141 scope_id: text("scope_id").notNull(),
109 position: text('position').notNull(),142 pattern: text("pattern").notNull(),
110 created_at: timestamp('created_at').notNull(),143 action: text("action").notNull(),
111 updated_at: timestamp('updated_at').notNull()144 position: text("position").notNull(),
112}, (table) => [145 created_at: timestamp("created_at").notNull(),
113 primaryKey({ columns: [table.scope_id, table.id] }),146 updated_at: timestamp("updated_at").notNull(),
114 index("tool_policy_scope_id_position_idx").on(table.scope_id, table.position),147 },
115]);148 (table) => [
149 primaryKey({ columns: [table.scope_id, table.id] }),
150 index("tool_policy_scope_id_position_idx").on(table.scope_id, table.position),
151 ],
152);
116 153
117export const openapi_source = pgTable("openapi_source", {154export const openapi_source = pgTable(
118 id: text('id').notNull(),155 "openapi_source",
119 scope_id: text('scope_id').notNull(),156 {
120 name: text('name').notNull(),157 id: text("id").notNull(),
121 spec: text('spec').notNull(),158 scope_id: text("scope_id").notNull(),
122 source_url: text('source_url'),159 name: text("name").notNull(),
123 base_url: text('base_url'),160 spec: text("spec").notNull(),
124 headers: jsonb('headers'),161 source_url: text("source_url"),
125 oauth2: jsonb('oauth2')162 base_url: text("base_url"),
126}, (table) => [163 headers: jsonb("headers"),
127 primaryKey({ columns: [table.scope_id, table.id] }),164 oauth2: jsonb("oauth2"),
128 index("openapi_source_scope_id_idx").on(table.scope_id),165 },
129]);166 (table) => [
167 primaryKey({ columns: [table.scope_id, table.id] }),
168 index("openapi_source_scope_id_idx").on(table.scope_id),
169 ],
170);
130 171
131export const openapi_operation = pgTable("openapi_operation", {172export const openapi_operation = pgTable(
132 id: text('id').notNull(),173 "openapi_operation",
133 scope_id: text('scope_id').notNull(),174 {
134 source_id: text('source_id').notNull(),175 id: text("id").notNull(),
135 binding: jsonb('binding').notNull()176 scope_id: text("scope_id").notNull(),
136}, (table) => [177 source_id: text("source_id").notNull(),
137 primaryKey({ columns: [table.scope_id, table.id] }),178 binding: jsonb("binding").notNull(),
138 index("openapi_operation_scope_id_idx").on(table.scope_id),179 },
139 index("openapi_operation_source_id_idx").on(table.source_id),180 (table) => [
140]);181 primaryKey({ columns: [table.scope_id, table.id] }),
182 index("openapi_operation_scope_id_idx").on(table.scope_id),
183 index("openapi_operation_source_id_idx").on(table.source_id),
184 ],
185);
141 186
142export const openapi_source_binding = pgTable("openapi_source_binding", {187export const openapi_source_binding = pgTable(
143 id: text('id').primaryKey(),188 "openapi_source_binding",
144 source_id: text('source_id').notNull(),189 {
145 source_scope_id: text('source_scope_id').notNull(),190 id: text("id").primaryKey(),
146 target_scope_id: text('target_scope_id').notNull(),191 source_id: text("source_id").notNull(),
147 slot: text('slot').notNull(),192 source_scope_id: text("source_scope_id").notNull(),
148 kind: text('kind', { enum: ['secret', 'connection', 'text'] }).notNull(),193 target_scope_id: text("target_scope_id").notNull(),
149 secret_id: text('secret_id'),194 slot: text("slot").notNull(),
150 connection_id: text('connection_id'),195 kind: text("kind", { enum: ["secret", "connection", "text"] }).notNull(),
151 text_value: text('text_value'),196 secret_id: text("secret_id"),
152 created_at: timestamp('created_at').notNull(),197 connection_id: text("connection_id"),
153 updated_at: timestamp('updated_at').notNull()198 text_value: text("text_value"),
154}, (table) => [199 created_at: timestamp("created_at").notNull(),
155 index("openapi_source_binding_source_id_idx").on(table.source_id),200 updated_at: timestamp("updated_at").notNull(),
156 index("openapi_source_binding_source_scope_id_idx").on(table.source_scope_id),201 },
157 index("openapi_source_binding_target_scope_id_idx").on(table.target_scope_id),202 (table) => [
158 index("openapi_source_binding_slot_idx").on(table.slot),203 index("openapi_source_binding_source_id_idx").on(table.source_id),
159 index("openapi_source_binding_secret_id_idx").on(table.secret_id),204 index("openapi_source_binding_source_scope_id_idx").on(table.source_scope_id),
160 index("openapi_source_binding_connection_id_idx").on(table.connection_id),205 index("openapi_source_binding_target_scope_id_idx").on(table.target_scope_id),
161]);206 index("openapi_source_binding_slot_idx").on(table.slot),
207 index("openapi_source_binding_secret_id_idx").on(table.secret_id),
208 index("openapi_source_binding_connection_id_idx").on(table.connection_id),
209 ],
210);
162 211
163export const openapi_source_query_param = pgTable("openapi_source_query_param", {212export const openapi_source_query_param = pgTable(
164 id: text('id').notNull(),213 "openapi_source_query_param",
165 scope_id: text('scope_id').notNull(),214 {
166 source_id: text('source_id').notNull(),215 id: text("id").notNull(),
167 name: text('name').notNull(),216 scope_id: text("scope_id").notNull(),
168 kind: text('kind', { enum: ['text', 'secret'] }).notNull(),217 source_id: text("source_id").notNull(),
169 text_value: text('text_value'),218 name: text("name").notNull(),
170 secret_id: text('secret_id'),219 kind: text("kind", { enum: ["text", "secret"] }).notNull(),
171 secret_prefix: text('secret_prefix')220 text_value: text("text_value"),
172}, (table) => [221 secret_id: text("secret_id"),
173 primaryKey({ columns: [table.scope_id, table.id] }),222 secret_prefix: text("secret_prefix"),
174 index("openapi_source_query_param_scope_id_idx").on(table.scope_id),223 },
175 index("openapi_source_query_param_source_id_idx").on(table.source_id),224 (table) => [
176 index("openapi_source_query_param_secret_id_idx").on(table.secret_id),225 primaryKey({ columns: [table.scope_id, table.id] }),
177]);226 index("openapi_source_query_param_scope_id_idx").on(table.scope_id),
227 index("openapi_source_query_param_source_id_idx").on(table.source_id),
228 index("openapi_source_query_param_secret_id_idx").on(table.secret_id),
229 ],
230);
178 231
179export const openapi_source_spec_fetch_header = pgTable("openapi_source_spec_fetch_header", {232export const openapi_source_spec_fetch_header = pgTable(
180 id: text('id').notNull(),233 "openapi_source_spec_fetch_header",
181 scope_id: text('scope_id').notNull(),234 {
182 source_id: text('source_id').notNull(),235 id: text("id").notNull(),
183 name: text('name').notNull(),236 scope_id: text("scope_id").notNull(),
184 kind: text('kind', { enum: ['text', 'secret'] }).notNull(),237 source_id: text("source_id").notNull(),
185 text_value: text('text_value'),238 name: text("name").notNull(),
186 secret_id: text('secret_id'),239 kind: text("kind", { enum: ["text", "secret"] }).notNull(),
187 secret_prefix: text('secret_prefix')240 text_value: text("text_value"),
188}, (table) => [241 secret_id: text("secret_id"),
189 primaryKey({ columns: [table.scope_id, table.id] }),242 secret_prefix: text("secret_prefix"),
190 index("openapi_source_spec_fetch_header_scope_id_idx").on(table.scope_id),243 },
191 index("openapi_source_spec_fetch_header_source_id_idx").on(table.source_id),244 (table) => [
192 index("openapi_source_spec_fetch_header_secret_id_idx").on(table.secret_id),245 primaryKey({ columns: [table.scope_id, table.id] }),
193]);246 index("openapi_source_spec_fetch_header_scope_id_idx").on(table.scope_id),
247 index("openapi_source_spec_fetch_header_source_id_idx").on(table.source_id),
248 index("openapi_source_spec_fetch_header_secret_id_idx").on(table.secret_id),
249 ],
250);
194 251
195export const openapi_source_spec_fetch_query_param = pgTable("openapi_source_spec_fetch_query_param", {252export const openapi_source_spec_fetch_query_param = pgTable(
196 id: text('id').notNull(),253 "openapi_source_spec_fetch_query_param",
197 scope_id: text('scope_id').notNull(),254 {
198 source_id: text('source_id').notNull(),255 id: text("id").notNull(),
199 name: text('name').notNull(),256 scope_id: text("scope_id").notNull(),
200 kind: text('kind', { enum: ['text', 'secret'] }).notNull(),257 source_id: text("source_id").notNull(),
201 text_value: text('text_value'),258 name: text("name").notNull(),
202 secret_id: text('secret_id'),259 kind: text("kind", { enum: ["text", "secret"] }).notNull(),
203 secret_prefix: text('secret_prefix')260 text_value: text("text_value"),
204}, (table) => [261 secret_id: text("secret_id"),
205 primaryKey({ columns: [table.scope_id, table.id] }),262 secret_prefix: text("secret_prefix"),
206 index("openapi_source_spec_fetch_query_param_scope_id_idx").on(table.scope_id),263 },
207 index("openapi_source_spec_fetch_query_param_source_id_idx").on(table.source_id),264 (table) => [
208 index("openapi_source_spec_fetch_query_param_secret_id_idx").on(table.secret_id),265 primaryKey({ columns: [table.scope_id, table.id] }),
209]);266 index("openapi_source_spec_fetch_query_param_scope_id_idx").on(table.scope_id),
267 index("openapi_source_spec_fetch_query_param_source_id_idx").on(table.source_id),
268 index("openapi_source_spec_fetch_query_param_secret_id_idx").on(table.secret_id),
269 ],
270);
210 271
211export const mcp_source = pgTable("mcp_source", {272export const mcp_source = pgTable(
212 id: text('id').notNull(),273 "mcp_source",
213 scope_id: text('scope_id').notNull(),274 {
214 name: text('name').notNull(),275 id: text("id").notNull(),
215 config: jsonb('config').notNull(),276 scope_id: text("scope_id").notNull(),
216 auth_kind: text('auth_kind', { enum: ['none', 'header', 'oauth2'] }).default("none").notNull(),277 name: text("name").notNull(),
217 auth_header_name: text('auth_header_name'),278 config: jsonb("config").notNull(),
218 auth_secret_id: text('auth_secret_id'),279 auth_kind: text("auth_kind", { enum: ["none", "header", "oauth2"] })
219 auth_secret_prefix: text('auth_secret_prefix'),280 .default("none")
220 auth_connection_id: text('auth_connection_id'),281 .notNull(),
221 auth_client_id_secret_id: text('auth_client_id_secret_id'),282 auth_header_name: text("auth_header_name"),
222 auth_client_secret_secret_id: text('auth_client_secret_secret_id'),283 auth_secret_id: text("auth_secret_id"),
223 created_at: timestamp('created_at').notNull()284 auth_secret_prefix: text("auth_secret_prefix"),
224}, (table) => [285 auth_connection_id: text("auth_connection_id"),
225 primaryKey({ columns: [table.scope_id, table.id] }),286 auth_client_id_secret_id: text("auth_client_id_secret_id"),
226 index("mcp_source_scope_id_idx").on(table.scope_id),287 auth_client_secret_secret_id: text("auth_client_secret_secret_id"),
227 index("mcp_source_auth_secret_id_idx").on(table.auth_secret_id),288 created_at: timestamp("created_at").notNull(),
228 index("mcp_source_auth_connection_id_idx").on(table.auth_connection_id),289 },
229 index("mcp_source_auth_client_id_secret_id_idx").on(table.auth_client_id_secret_id),290 (table) => [
230 index("mcp_source_auth_client_secret_secret_id_idx").on(table.auth_client_secret_secret_id),291 primaryKey({ columns: [table.scope_id, table.id] }),
231]);292 index("mcp_source_scope_id_idx").on(table.scope_id),
293 index("mcp_source_auth_secret_id_idx").on(table.auth_secret_id),
294 index("mcp_source_auth_connection_id_idx").on(table.auth_connection_id),
295 index("mcp_source_auth_client_id_secret_id_idx").on(table.auth_client_id_secret_id),
296 index("mcp_source_auth_client_secret_secret_id_idx").on(table.auth_client_secret_secret_id),
297 ],
298);
232 299
233export const mcp_source_header = pgTable("mcp_source_header", {300export const mcp_source_header = pgTable(
234 id: text('id').notNull(),301 "mcp_source_header",
235 scope_id: text('scope_id').notNull(),302 {
236 source_id: text('source_id').notNull(),303 id: text("id").notNull(),
237 name: text('name').notNull(),304 scope_id: text("scope_id").notNull(),
238 kind: text('kind', { enum: ['text', 'secret'] }).notNull(),305 source_id: text("source_id").notNull(),
239 text_value: text('text_value'),306 name: text("name").notNull(),
240 secret_id: text('secret_id'),307 kind: text("kind", { enum: ["text", "secret"] }).notNull(),
241 secret_prefix: text('secret_prefix')308 text_value: text("text_value"),
242}, (table) => [309 secret_id: text("secret_id"),
243 primaryKey({ columns: [table.scope_id, table.id] }),310 secret_prefix: text("secret_prefix"),
244 index("mcp_source_header_scope_id_idx").on(table.scope_id),311 },
245 index("mcp_source_header_source_id_idx").on(table.source_id),312 (table) => [
246 index("mcp_source_header_secret_id_idx").on(table.secret_id),313 primaryKey({ columns: [table.scope_id, table.id] }),
247]);314 index("mcp_source_header_scope_id_idx").on(table.scope_id),
315 index("mcp_source_header_source_id_idx").on(table.source_id),
316 index("mcp_source_header_secret_id_idx").on(table.secret_id),
317 ],
318);
248 319
249export const mcp_source_query_param = pgTable("mcp_source_query_param", {320export const mcp_source_query_param = pgTable(
250 id: text('id').notNull(),321 "mcp_source_query_param",
251 scope_id: text('scope_id').notNull(),322 {
252 source_id: text('source_id').notNull(),323 id: text("id").notNull(),
253 name: text('name').notNull(),324 scope_id: text("scope_id").notNull(),
254 kind: text('kind', { enum: ['text', 'secret'] }).notNull(),325 source_id: text("source_id").notNull(),
255 text_value: text('text_value'),326 name: text("name").notNull(),
256 secret_id: text('secret_id'),327 kind: text("kind", { enum: ["text", "secret"] }).notNull(),
257 secret_prefix: text('secret_prefix')328 text_value: text("text_value"),
258}, (table) => [329 secret_id: text("secret_id"),
259 primaryKey({ columns: [table.scope_id, table.id] }),330 secret_prefix: text("secret_prefix"),
260 index("mcp_source_query_param_scope_id_idx").on(table.scope_id),331 },
261 index("mcp_source_query_param_source_id_idx").on(table.source_id),332 (table) => [
262 index("mcp_source_query_param_secret_id_idx").on(table.secret_id),333 primaryKey({ columns: [table.scope_id, table.id] }),
263]);334 index("mcp_source_query_param_scope_id_idx").on(table.scope_id),
335 index("mcp_source_query_param_source_id_idx").on(table.source_id),
336 index("mcp_source_query_param_secret_id_idx").on(table.secret_id),
337 ],
338);
264 339
265export const mcp_binding = pgTable("mcp_binding", {340export const mcp_binding = pgTable(
266 id: text('id').notNull(),341 "mcp_binding",
267 scope_id: text('scope_id').notNull(),342 {
268 source_id: text('source_id').notNull(),343 id: text("id").notNull(),
269 binding: jsonb('binding').notNull(),344 scope_id: text("scope_id").notNull(),
270 created_at: timestamp('created_at').notNull()345 source_id: text("source_id").notNull(),
271}, (table) => [346 binding: jsonb("binding").notNull(),
272 primaryKey({ columns: [table.scope_id, table.id] }),347 created_at: timestamp("created_at").notNull(),
273 index("mcp_binding_scope_id_idx").on(table.scope_id),348 },
274 index("mcp_binding_source_id_idx").on(table.source_id),349 (table) => [
275]);350 primaryKey({ columns: [table.scope_id, table.id] }),
351 index("mcp_binding_scope_id_idx").on(table.scope_id),
352 index("mcp_binding_source_id_idx").on(table.source_id),
353 ],
354);
276 355
277export const graphql_source = pgTable("graphql_source", {356export const graphql_source = pgTable(
278 id: text('id').notNull(),357 "graphql_source",
279 scope_id: text('scope_id').notNull(),358 {
280 name: text('name').notNull(),359 id: text("id").notNull(),
281 endpoint: text('endpoint').notNull(),360 scope_id: text("scope_id").notNull(),
282 auth_kind: text('auth_kind', { enum: ['none', 'oauth2'] }).default("none").notNull(),361 name: text("name").notNull(),
283 auth_connection_id: text('auth_connection_id')362 endpoint: text("endpoint").notNull(),
284}, (table) => [363 auth_kind: text("auth_kind", { enum: ["none", "oauth2"] })
285 primaryKey({ columns: [table.scope_id, table.id] }),364 .default("none")
286 index("graphql_source_scope_id_idx").on(table.scope_id),365 .notNull(),
287 index("graphql_source_auth_connection_id_idx").on(table.auth_connection_id),366 auth_connection_id: text("auth_connection_id"),
288]);367 },
368 (table) => [
369 primaryKey({ columns: [table.scope_id, table.id] }),
370 index("graphql_source_scope_id_idx").on(table.scope_id),
371 index("graphql_source_auth_connection_id_idx").on(table.auth_connection_id),
372 ],
373);
289 374
290export const graphql_source_header = pgTable("graphql_source_header", {375export const graphql_source_header = pgTable(
291 id: text('id').notNull(),376 "graphql_source_header",
292 scope_id: text('scope_id').notNull(),377 {
293 source_id: text('source_id').notNull(),378 id: text("id").notNull(),
294 name: text('name').notNull(),379 scope_id: text("scope_id").notNull(),
295 kind: text('kind', { enum: ['text', 'secret'] }).notNull(),380 source_id: text("source_id").notNull(),
296 text_value: text('text_value'),381 name: text("name").notNull(),
297 secret_id: text('secret_id'),382 kind: text("kind", { enum: ["text", "secret"] }).notNull(),
298 secret_prefix: text('secret_prefix')383 text_value: text("text_value"),
299}, (table) => [384 secret_id: text("secret_id"),
300 primaryKey({ columns: [table.scope_id, table.id] }),385 secret_prefix: text("secret_prefix"),
301 index("graphql_source_header_scope_id_idx").on(table.scope_id),386 },
302 index("graphql_source_header_source_id_idx").on(table.source_id),387 (table) => [
303 index("graphql_source_header_secret_id_idx").on(table.secret_id),388 primaryKey({ columns: [table.scope_id, table.id] }),
304]);389 index("graphql_source_header_scope_id_idx").on(table.scope_id),
390 index("graphql_source_header_source_id_idx").on(table.source_id),
391 index("graphql_source_header_secret_id_idx").on(table.secret_id),
392 ],
393);
305 394
306export const graphql_source_query_param = pgTable("graphql_source_query_param", {395export const graphql_source_query_param = pgTable(
307 id: text('id').notNull(),396 "graphql_source_query_param",
308 scope_id: text('scope_id').notNull(),397 {
309 source_id: text('source_id').notNull(),398 id: text("id").notNull(),
310 name: text('name').notNull(),399 scope_id: text("scope_id").notNull(),
311 kind: text('kind', { enum: ['text', 'secret'] }).notNull(),400 source_id: text("source_id").notNull(),
312 text_value: text('text_value'),401 name: text("name").notNull(),
313 secret_id: text('secret_id'),402 kind: text("kind", { enum: ["text", "secret"] }).notNull(),
314 secret_prefix: text('secret_prefix')403 text_value: text("text_value"),
315}, (table) => [404 secret_id: text("secret_id"),
316 primaryKey({ columns: [table.scope_id, table.id] }),405 secret_prefix: text("secret_prefix"),
317 index("graphql_source_query_param_scope_id_idx").on(table.scope_id),406 },
318 index("graphql_source_query_param_source_id_idx").on(table.source_id),407 (table) => [
319 index("graphql_source_query_param_secret_id_idx").on(table.secret_id),408 primaryKey({ columns: [table.scope_id, table.id] }),
320]);409 index("graphql_source_query_param_scope_id_idx").on(table.scope_id),
410 index("graphql_source_query_param_source_id_idx").on(table.source_id),
411 index("graphql_source_query_param_secret_id_idx").on(table.secret_id),
412 ],
413);
321 414
322export const graphql_operation = pgTable("graphql_operation", {415export const graphql_operation = pgTable(
323 id: text('id').notNull(),416 "graphql_operation",
324 scope_id: text('scope_id').notNull(),417 {
325 source_id: text('source_id').notNull(),418 id: text("id").notNull(),
326 binding: jsonb('binding').notNull()419 scope_id: text("scope_id").notNull(),
327}, (table) => [420 source_id: text("source_id").notNull(),
328 primaryKey({ columns: [table.scope_id, table.id] }),421 binding: jsonb("binding").notNull(),
329 index("graphql_operation_scope_id_idx").on(table.scope_id),422 },
330 index("graphql_operation_source_id_idx").on(table.source_id),423 (table) => [
331]);424 primaryKey({ columns: [table.scope_id, table.id] }),
425 index("graphql_operation_scope_id_idx").on(table.scope_id),
426 index("graphql_operation_source_id_idx").on(table.source_id),
427 ],
428);
332 429
333export const workos_vault_metadata = pgTable("workos_vault_metadata", {430export const workos_vault_metadata = pgTable(
334 id: text('id').notNull(),431 "workos_vault_metadata",
335 scope_id: text('scope_id').notNull(),432 {
336 name: text('name').notNull(),433 id: text("id").notNull(),
337 purpose: text('purpose'),434 scope_id: text("scope_id").notNull(),
338 created_at: timestamp('created_at').notNull()435 name: text("name").notNull(),
339}, (table) => [436 purpose: text("purpose"),
340 primaryKey({ columns: [table.scope_id, table.id] }),437 created_at: timestamp("created_at").notNull(),
341 index("workos_vault_metadata_scope_id_idx").on(table.scope_id),438 },
342]);439 (table) => [
440 primaryKey({ columns: [table.scope_id, table.id] }),
441 index("workos_vault_metadata_scope_id_idx").on(table.scope_id),
442 ],
443);
343 444
344export const blob = pgTable("blob", {445export const blob = pgTable(
345 namespace: text('namespace').notNull(),446 "blob",
346 key: text('key').notNull(),447 {
347 value: text('value').notNull()448 namespace: text("namespace").notNull(),
348}, (table) => [449 key: text("key").notNull(),
349 primaryKey({ columns: [table.namespace, table.key] }),450 value: text("value").notNull(),
350]);451 },
452 (table) => [primaryKey({ columns: [table.namespace, table.key] })],
453);
apps/cloud/src/services/executor.ts+2−10
@@ -10,16 +10,8 @@ · 9 unmodified lines
10 10
11import { Effect } from "effect";11import { Effect } from "effect";
12 12
13import {13import { Scope, ScopeId, collectSchemas, createExecutor } from "@executor-js/sdk";
14 Scope,14import { makePostgresAdapter, makePostgresBlobStore } from "@executor-js/storage-postgres";
15 ScopeId,
16 collectSchemas,
17 createExecutor,
18} from "@executor-js/sdk";
19import {
20 makePostgresAdapter,
21 makePostgresBlobStore,
22} from "@executor-js/storage-postgres";
23 15
24import { env } from "cloudflare:workers";16import { env } from "cloudflare:workers";
25import executorConfig from "../../executor.config";17import executorConfig from "../../executor.config";
apps/cloud/src/services/mcp-oauth.node.test.ts+11−34
@@ -48,21 +48,15 @@ const RegistrationBody = Schema.Struct({ · 47 unmodified lines
48 grant_types: Schema.optional(Schema.Array(Schema.String)),48 grant_types: Schema.optional(Schema.Array(Schema.String)),
49 response_types: Schema.optional(Schema.Array(Schema.String)),49 response_types: Schema.optional(Schema.Array(Schema.String)),
50});50});
51const decodeRegistrationBody = Schema.decodeUnknownOption(51const decodeRegistrationBody = Schema.decodeUnknownOption(Schema.fromJsonString(RegistrationBody));
52 Schema.fromJsonString(RegistrationBody),
53);
54 52
55const startFakeServer = async (): Promise<FakeServer> => {53const startFakeServer = async (): Promise<FakeServer> => {
56 const clients = new Map<string, { redirect_uris: readonly string[] }>();54 const clients = new Map<string, { redirect_uris: readonly string[] }>();
57 const codes = new Map<55 const codes = new Map<string, { readonly clientId: string; readonly codeChallenge: string }>();
58 string,
59 { readonly clientId: string; readonly codeChallenge: string }
60 >();
61 const accessTokens = new Map<string, { readonly refresh: string }>();56 const accessTokens = new Map<string, { readonly refresh: string }>();
62 const refreshTokens = new Map<string, string>();57 const refreshTokens = new Map<string, string>();
63 let seq = 0;58 let seq = 0;
64 const next = (p: string) =>59 const next = (p: string) => `${p}_${++seq}_${randomBytes(6).toString("hex")}`;
65 `${p}_${++seq}_${randomBytes(6).toString("hex")}`;
66 let registrations = 0;60 let registrations = 0;
67 let tokenCalls = 0;61 let tokenCalls = 0;
68 62
@@ -76,16 +70,10 @@ const startFakeServer = async (): Promise<FakeServer> => { · 7 unmodified lines
76 70
77 const server: Server = createServer(async (req, res) => {71 const server: Server = createServer(async (req, res) => {
78 const url = new URL(req.url!, `http://${req.headers.host}`);72 const url = new URL(req.url!, `http://${req.headers.host}`);
79 const send = (73 const send = (status: number, body: unknown, headers: Record<string, string> = {}) => {
80 status: number,74 const payload = typeof body === "string" ? body : JSON.stringify(body);
81 body: unknown,
82 headers: Record<string, string> = {},
83 ) => {
84 const payload =
85 typeof body === "string" ? body : JSON.stringify(body);
86 res.writeHead(status, {75 res.writeHead(status, {
87 "content-type":76 "content-type": typeof body === "string" ? "text/plain" : "application/json",
88 typeof body === "string" ? "text/plain" : "application/json",
89 ...headers,77 ...headers,
90 });78 });
91 res.end(payload);79 res.end(payload);
@@ -130,10 +118,7 @@ const startFakeServer = async (): Promise<FakeServer> => { · 38 unmodified lines
130 client_id: clientId,118 client_id: clientId,
131 client_id_issued_at: Math.floor(Date.now() / 1000),119 client_id_issued_at: Math.floor(Date.now() / 1000),
132 redirect_uris: parsed.redirect_uris ?? [],120 redirect_uris: parsed.redirect_uris ?? [],
133 grant_types: parsed.grant_types ?? [121 grant_types: parsed.grant_types ?? ["authorization_code", "refresh_token"],
134 "authorization_code",
135 "refresh_token",
136 ],
137 response_types: parsed.response_types ?? ["code"],122 response_types: parsed.response_types ?? ["code"],
138 token_endpoint_auth_method: "none",123 token_endpoint_auth_method: "none",
139 });124 });
@@ -171,9 +156,7 @@ const startFakeServer = async (): Promise<FakeServer> => { · 31 unmodified lines
171 const record = codes.get(code);156 const record = codes.get(code);
172 if (!record) return send(400, { error: "invalid_grant" });157 if (!record) return send(400, { error: "invalid_grant" });
173 codes.delete(code);158 codes.delete(code);
174 const computed = createHash("sha256")159 const computed = createHash("sha256").update(verifier).digest("base64url");
175 .update(verifier)
176 .digest("base64url");
177 if (computed !== record.codeChallenge) {160 if (computed !== record.codeChallenge) {
178 return send(400, { error: "invalid_grant" });161 return send(400, { error: "invalid_grant" });
179 }162 }
@@ -229,9 +212,7 @@ const startFakeServer = async (): Promise<FakeServer> => { · 49 unmodified lines
229 }212 }
230 });213 });
231 214
232 await new Promise<void>((resolve) =>215 await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
233 server.listen(0, "127.0.0.1", () => resolve()),
234 );
235 const address = server.address() as AddressInfo;216 const address = server.address() as AddressInfo;
236 217
237 return {218 return {
@@ -352,9 +333,7 @@ describe("mcp oauth end-to-end (node pool, real OAuth + MCP server)", () => { · 114 unmodified lines
352 },333 },
353 }),334 }),
354 );335 );
355 const redirA = yield* Effect.promise(() =>336 const redirA = yield* Effect.promise(() => followAuthorize(startedA.authorizationUrl!));
356 followAuthorize(startedA.authorizationUrl!),
357 );
358 const completedA = yield* asUser(userA, orgId, (client) =>337 const completedA = yield* asUser(userA, orgId, (client) =>
359 client.oauth.complete({338 client.oauth.complete({
360 params: { scopeId: scopeA },339 params: { scopeId: scopeA },
@@ -377,9 +356,7 @@ describe("mcp oauth end-to-end (node pool, real OAuth + MCP server)", () => { · 16 unmodified lines
377 },356 },
378 }),357 }),
379 );358 );
380 const redirB = yield* Effect.promise(() =>359 const redirB = yield* Effect.promise(() => followAuthorize(startedB.authorizationUrl!));
381 followAuthorize(startedB.authorizationUrl!),
382 );
383 const completedB = yield* asUser(userB, orgId, (client) =>360 const completedB = yield* asUser(userB, orgId, (client) =>
384 client.oauth.complete({361 client.oauth.complete({
385 params: { scopeId: scopeB },362 params: { scopeId: scopeB },
apps/cloud/src/services/mcp-worker-transport.test.ts+1−3
@@ -58,9 +58,7 @@ describe("JsonRpcRequestIdQueue", () => { · 57 unmodified lines
58 queue58 queue
59 .run(jsonRpcRequest({ jsonrpc: "2.0", id: 2, method: "tools/call" }), async () => "done")59 .run(jsonRpcRequest({ jsonrpc: "2.0", id: 2, method: "tools/call" }), async () => "done")
60 .then((v) => ({ kind: "settled" as const, v })),60 .then((v) => ({ kind: "settled" as const, v })),
61 new Promise<{ kind: "blocked" }>((r) =>61 new Promise<{ kind: "blocked" }>((r) => setTimeout(() => r({ kind: "blocked" }), 200)),
62 setTimeout(() => r({ kind: "blocked" }), 200),
63 ),
64 ]);62 ]);
65 63
66 expect(otherDone.kind).toBe("settled");64 expect(otherDone.kind).toBe("settled");
apps/cloud/src/services/mcp-worker-transport.ts+2−6
@@ -64,9 +64,7 @@ const extractJsonRpcRequestIdKeys = async (request: Request): Promise<ReadonlyAr · 63 unmodified lines
64 const contentType = request.headers.get("content-type") ?? "";64 const contentType = request.headers.get("content-type") ?? "";
65 if (!contentType.includes("application/json")) return [];65 if (!contentType.includes("application/json")) return [];
66 66
67 const parsed = await Effect.runPromiseExit(67 const parsed = await Effect.runPromiseExit(Effect.tryPromise(() => request.clone().json()));
68 Effect.tryPromise(() => request.clone().json()),
69 );
70 if (Exit.isFailure(parsed)) {68 if (Exit.isFailure(parsed)) {
71 return [];69 return [];
72 }70 }
@@ -192,8 +190,6 @@ export const makeMcpWorkerTransport = ( · 119 unmodified lines
192 try: () => transport.close(),190 try: () => transport.close(),
193 catch: (cause) => new McpWorkerTransportError({ cause }),191 catch: (cause) => new McpWorkerTransportError({ cause }),
194 }),192 }),
195 ).pipe(193 ).pipe(Effect.withSpan("mcp.worker_transport.close")),
196 Effect.withSpan("mcp.worker_transport.close"),
197 ),
198 } satisfies McpWorkerTransport;194 } satisfies McpWorkerTransport;
199 }).pipe(Effect.withSpan("mcp.worker_transport.make"));195 }).pipe(Effect.withSpan("mcp.worker_transport.make"));
apps/cloud/src/services/slack.ts+8−18
@@ -14,10 +14,7 @@ export class SlackError extends Data.TaggedError("SlackError")<{ · 13 unmodified lines
14}> {}14}> {}
15 15
16export type ISlackService = Readonly<{16export type ISlackService = Readonly<{
17 createConnectInvite: (input: {17 createConnectInvite: (input: { email: string; organization?: string }) => Effect.Effect<
18 email: string;
19 organization?: string;
20 }) => Effect.Effect<
21 {18 {
22 channel: { id: string; name: string };19 channel: { id: string; name: string };
23 invite: { invite_id: string; url: string };20 invite: { invite_id: string; url: string };
@@ -46,9 +43,7 @@ const make = Effect.sync(() => { · 22 unmodified lines
46 43
47 if (!token) {44 if (!token) {
48 const notConfigured = (method: string) =>45 const notConfigured = (method: string) =>
49 Effect.fail(46 Effect.fail(new SlackError({ method, error: "SLACK_BOT_TOKEN is not configured" }));
50 new SlackError({ method, error: "SLACK_BOT_TOKEN is not configured" }),
51 );
52 return {47 return {
53 createConnectInvite: () => notConfigured("createConnectInvite"),48 createConnectInvite: () => notConfigured("createConnectInvite"),
54 } satisfies ISlackService;49 } satisfies ISlackService;
@@ -83,20 +78,17 @@ const make = Effect.sync(() => { · 28 unmodified lines
83 Effect.withSpan(`slack.${method}`),78 Effect.withSpan(`slack.${method}`),
84 );79 );
85 80
86 const createConnectInvite: ISlackService["createConnectInvite"] = ({81 const createConnectInvite: ISlackService["createConnectInvite"] = ({ email, organization }) =>
87 email,
88 organization,
89 }) =>
90 Effect.gen(function* () {82 Effect.gen(function* () {
91 // Slack channel names: lowercase, no spaces, max 80 chars, unique per83 // Slack channel names: lowercase, no spaces, max 80 chars, unique per
92 // workspace. Use the org name when available; fall back to the email84 // workspace. Use the org name when available; fall back to the email
93 // local-part for unauthenticated submissions.85 // local-part for unauthenticated submissions.
94 const baseName = slugify(organization ?? email).slice(0, 80) || "contact";86 const baseName = slugify(organization ?? email).slice(0, 80) || "contact";
95 const tryCreate = (n: string) =>87 const tryCreate = (n: string) =>
96 call<SlackResponse & { channel: { id: string; name: string } }>(88 call<SlackResponse & { channel: { id: string; name: string } }>("conversations.create", {
97 "conversations.create",89 name: n,
98 { name: n, is_private: false },90 is_private: false,
99 );91 });
100 92
101 const created = yield* tryCreate(baseName).pipe(93 const created = yield* tryCreate(baseName).pipe(
102 Effect.catchTag("SlackError", (err) =>94 Effect.catchTag("SlackError", (err) =>
@@ -125,9 +117,7 @@ const make = Effect.sync(() => { · 22 unmodified lines
125 channel: { id: channel.id, name: channel.name },117 channel: { id: channel.id, name: channel.name },
126 invite: { invite_id: invite.invite_id, url: invite.url },118 invite: { invite_id: invite.invite_id, url: invite.url },
127 };119 };
128 }).pipe(120 }).pipe(Effect.withSpan("slack.createConnectInvite", { attributes: { "slack.email": email } }));
129 Effect.withSpan("slack.createConnectInvite", { attributes: { "slack.email": email } }),
130 );
131 121
132 return { createConnectInvite } satisfies ISlackService;122 return { createConnectInvite } satisfies ISlackService;
133});123});
apps/cloud/src/services/sources-api.node.test.ts+7−13
@@ -14,11 +14,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/ · 13 unmodified lines
14 14
15import { ScopeId, SecretId } from "@executor-js/sdk";15import { ScopeId, SecretId } from "@executor-js/sdk";
16 16
17import {17import { asOrg, asUser, testUserOrgScopeId } from "./__test-harness__/api-harness";
18 asOrg,
19 asUser,
20 testUserOrgScopeId,
21} from "./__test-harness__/api-harness";
22 18
23const MINIMAL_OPENAPI_SPEC = JSON.stringify({19const MINIMAL_OPENAPI_SPEC = JSON.stringify({
24 openapi: "3.0.0",20 openapi: "3.0.0",
@@ -108,7 +104,10 @@ const startEchoServer = () => { · 83 unmodified lines
108 104
109 return new Promise<{105 return new Promise<{
110 readonly baseUrl: string;106 readonly baseUrl: string;
111 readonly requests: () => ReadonlyArray<{ readonly path: string; readonly suffix: string | null }>;107 readonly requests: () => ReadonlyArray<{
108 readonly path: string;
109 readonly suffix: string | null;
110 }>;
112 readonly close: () => Promise<void>;111 readonly close: () => Promise<void>;
113 }>((resolveServer) => {112 }>((resolveServer) => {
114 server.listen(0, "127.0.0.1", () => {113 server.listen(0, "127.0.0.1", () => {
@@ -225,10 +224,7 @@ const startGraphqlServer = () => { · 110 unmodified lines
225};224};
226 225
227const createCloudMcpServer = () => {226const createCloudMcpServer = () => {
228 const server = new McpServer(227 const server = new McpServer({ name: "cloud-e2e-mcp", version: "1.0.0" }, { capabilities: {} });
229 { name: "cloud-e2e-mcp", version: "1.0.0" },
230 { capabilities: {} },
231 );
232 228
233 server.registerTool(229 server.registerTool(
234 "simple_echo",230 "simple_echo",
@@ -831,9 +827,7 @@ describe("sources api (HTTP)", () => { · 596 unmodified lines
831 const sources = yield* asOrg(orgId, (client) =>827 const sources = yield* asOrg(orgId, (client) =>
832 client.sources.list({ params: { scopeId: ScopeId.make(orgId) } }),828 client.sources.list({ params: { scopeId: ScopeId.make(orgId) } }),
833 );829 );
834 expect(sources.find((source) => source.id === namespace)?.scopeId).toBe(830 expect(sources.find((source) => source.id === namespace)?.scopeId).toBe(ScopeId.make(orgId));
835 ScopeId.make(orgId),
836 );
837 }),831 }),
838 );832 );
839 833
apps/cloud/src/services/telemetry.ts+1−4
@@ -49,10 +49,7 @@ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http/build · 48 unmodified lines
49import { resourceFromAttributes } from "@opentelemetry/resources";49import { resourceFromAttributes } from "@opentelemetry/resources";
50import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";50import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
51import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";51import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
52import {52import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
53 ATTR_SERVICE_NAME,
54 ATTR_SERVICE_VERSION,
55} from "@opentelemetry/semantic-conventions";
56import { env } from "cloudflare:workers";53import { env } from "cloudflare:workers";
57import { Effect, Layer } from "effect";54import { Effect, Layer } from "effect";
58 55
apps/cloud/src/test-worker.ts+5−6
@@ -53,8 +53,7 @@ const TestMcpAuthLive = Layer.succeed(McpAuth)({ · 52 unmodified lines
53});53});
54 54
55const TestMcpOrganizationAuthLive = Layer.succeed(McpOrganizationAuth)({55const TestMcpOrganizationAuthLive = Layer.succeed(McpOrganizationAuth)({
56 authorize: (_accountId, organizationId) =>56 authorize: (_accountId, organizationId) => Effect.succeed(!organizationId.startsWith("revoked_")),
57 Effect.succeed(!organizationId.startsWith("revoked_")),
58});57});
59 58
60// ---------------------------------------------------------------------------59// ---------------------------------------------------------------------------
@@ -112,14 +111,14 @@ const handleSeedOrg = async ( · 51 unmodified lines
112// OTLPTraceExporter — not Durable-Object-specific) to stand in.111// OTLPTraceExporter — not Durable-Object-specific) to stand in.
113const testMcpFetch = HttpEffect.toWebHandler(112const testMcpFetch = HttpEffect.toWebHandler(
114 mcpApp.pipe(113 mcpApp.pipe(
115 Effect.provide(114 Effect.provide(Layer.mergeAll(TestMcpAuthLive, TestMcpOrganizationAuthLive, DoTelemetryLive)),
116 Layer.mergeAll(TestMcpAuthLive, TestMcpOrganizationAuthLive, DoTelemetryLive),
117 ),
118 ),115 ),
119);116);
120 117
121const realAuthMcpFetch = HttpEffect.toWebHandler(118const realAuthMcpFetch = HttpEffect.toWebHandler(
122 mcpApp.pipe(Effect.provide(Layer.mergeAll(McpAuthLive, McpOrganizationAuthLive, DoTelemetryLive))),119 mcpApp.pipe(
120 Effect.provide(Layer.mergeAll(McpAuthLive, McpOrganizationAuthLive, DoTelemetryLive)),
121 ),
123);122);
124 123
125export default {124export default {
apps/cloud/src/web/pages/onboarding.tsx+4−8
@@ -69,9 +69,7 @@ export const OnboardingPage = () => { · 68 unmodified lines
69 };69 };
70 70
71 const suggestedName =71 const suggestedName =
72 auth.status === "authenticated" &&72 auth.status === "authenticated" && auth.user.name != null && auth.user.name.trim() !== ""
73 auth.user.name != null &&
74 auth.user.name.trim() !== ""
75 ? `${auth.user.name}'s Organization`73 ? `${auth.user.name}'s Organization`
76 : "";74 : "";
77 75
@@ -80,7 +78,8 @@ export const OnboardingPage = () => { · 2 unmodified lines
80 onSuccess: () => {},78 onSuccess: () => {},
81 });79 });
82 80
83 const isLoading = AsyncResult.isInitial(invitationsResult) || AsyncResult.isWaiting(invitationsResult);81 const isLoading =
82 AsyncResult.isInitial(invitationsResult) || AsyncResult.isWaiting(invitationsResult);
84 const invitations = AsyncResult.match(invitationsResult, {83 const invitations = AsyncResult.match(invitationsResult, {
85 onInitial: () => [] as readonly PendingInvitation[],84 onInitial: () => [] as readonly PendingInvitation[],
86 onFailure: () => [] as readonly PendingInvitation[],85 onFailure: () => [] as readonly PendingInvitation[],
@@ -130,10 +129,7 @@ export const OnboardingPage = () => { · 43 unmodified lines
130 )}129 )}
131 130
132 {!isLoading && (count === 0 || sole || count > 1) && (131 {!isLoading && (count === 0 || sole || count > 1) && (
133 <CreateOrgSection132 <CreateOrgSection isPrimary={count === 0} form={form} />
134 isPrimary={count === 0}
135 form={form}
136 />
137 )}133 )}
138 134
139 <footer className="flex items-center justify-center">135 <footer className="flex items-center justify-center">
apps/cloud/vite.config.ts+1−4
@@ -19,10 +19,7 @@ const devCrashGuard = (): Plugin => { · 18 unmodified lines
19 if (installed) return;19 if (installed) return;
20 installed = true;20 installed = true;
21 process.on("uncaughtException", (err, origin) => {21 process.on("uncaughtException", (err, origin) => {
22 console.error(22 console.error(`[dev-crash-guard] uncaughtException (origin=${origin}):`, err);
23 `[dev-crash-guard] uncaughtException (origin=${origin}):`,
24 err,
25 );
26 });23 });
27 process.on("unhandledRejection", (reason, promise) => {24 process.on("unhandledRejection", (reason, promise) => {
28 console.error("[dev-crash-guard] unhandledRejection:", reason, promise);25 console.error("[dev-crash-guard] unhandledRejection:", reason, promise);
apps/cloud/wrangler.miniflare.jsonc+4−4
@@ -16,11 +16,11 @@ · 15 unmodified lines
16 "WORKOS_COOKIE_PASSWORD": "test_cookie_password_at_least_32_chars!",16 "WORKOS_COOKIE_PASSWORD": "test_cookie_password_at_least_32_chars!",
17 "MCP_AUTHKIT_DOMAIN": "https://test-authkit.example.com",17 "MCP_AUTHKIT_DOMAIN": "https://test-authkit.example.com",
18 "MCP_RESOURCE_ORIGIN": "https://test-resource.example.com",18 "MCP_RESOURCE_ORIGIN": "https://test-resource.example.com",
19 "NODE_ENV": "test"19 "NODE_ENV": "test",
20 },20 },
21 "durable_objects": {21 "durable_objects": {
22 "bindings": [{"name": "MCP_SESSION", "class_name": "McpSessionDO"}]22 "bindings": [{ "name": "MCP_SESSION", "class_name": "McpSessionDO" }],
23 },23 },
24 "worker_loaders": [{"binding": "LOADER"}],24 "worker_loaders": [{ "binding": "LOADER" }],
25 "migrations": [{"tag": "v1", "new_classes": ["McpSessionDO"]}]25 "migrations": [{ "tag": "v1", "new_classes": ["McpSessionDO"] }],
26}26}
apps/cloud/wrangler.test.jsonc+4−4
@@ -12,11 +12,11 @@ · 11 unmodified lines
12 "WORKOS_COOKIE_PASSWORD": "test_cookie_password_at_least_32_chars!",12 "WORKOS_COOKIE_PASSWORD": "test_cookie_password_at_least_32_chars!",
13 "MCP_AUTHKIT_DOMAIN": "https://test-authkit.example.com",13 "MCP_AUTHKIT_DOMAIN": "https://test-authkit.example.com",
14 "MCP_RESOURCE_ORIGIN": "https://test-resource.example.com",14 "MCP_RESOURCE_ORIGIN": "https://test-resource.example.com",
15 "NODE_ENV": "test"15 "NODE_ENV": "test",
16 },16 },
17 "durable_objects": {17 "durable_objects": {
18 "bindings": [{"name": "MCP_SESSION", "class_name": "McpSessionDO"}]18 "bindings": [{ "name": "MCP_SESSION", "class_name": "McpSessionDO" }],
19 },19 },
20 "worker_loaders": [{"binding": "LOADER"}],20 "worker_loaders": [{ "binding": "LOADER" }],
21 "migrations": [{"tag": "v1", "new_classes": ["McpSessionDO"]}]21 "migrations": [{ "tag": "v1", "new_classes": ["McpSessionDO"] }],
22}22}
apps/desktop/src/main.ts+10−7
@@ -99,11 +99,7 @@ const installCli = (): void => { · 98 unmodified lines
99 // Patch shell profiles with PATH99 // Patch shell profiles with PATH
100 if (process.platform === "win32") {100 if (process.platform === "win32") {
101 // Add bin dir to the user PATH via registry so new terminals pick it up101 // Add bin dir to the user PATH via registry so new terminals pick it up
102 const result = spawn(102 const result = spawn("reg", ["query", "HKCU\\Environment", "/v", "Path"], { stdio: "pipe" });
103 "reg",
104 ["query", "HKCU\\Environment", "/v", "Path"],
105 { stdio: "pipe" },
106 );
107 let out = "";103 let out = "";
108 result.stdout?.on("data", (d: Buffer) => (out += d.toString()));104 result.stdout?.on("data", (d: Buffer) => (out += d.toString()));
109 result.on("close", (code) => {105 result.on("close", (code) => {
@@ -128,8 +124,15 @@ const installCli = (): void => { · 18 unmodified lines
128 if (!current.toLowerCase().includes(CLI_BIN_DIR.toLowerCase())) {124 if (!current.toLowerCase().includes(CLI_BIN_DIR.toLowerCase())) {
129 const updated = current ? `${current};${CLI_BIN_DIR}` : CLI_BIN_DIR;125 const updated = current ? `${current};${CLI_BIN_DIR}` : CLI_BIN_DIR;
130 spawn("reg", [126 spawn("reg", [
131 "add", "HKCU\\Environment", "/v", "Path",127 "add",
132 "/t", "REG_EXPAND_SZ", "/d", updated, "/f",128 "HKCU\\Environment",
129 "/v",
130 "Path",
131 "/t",
132 "REG_EXPAND_SZ",
133 "/d",
134 updated,
135 "/f",
133 ]);136 ]);
134 }137 }
135 });138 });
apps/local/drizzle/meta/0000_snapshot.json+42−140
@@ -33,10 +33,7 @@ · 32 unmodified lines
33 "foreignKeys": {},33 "foreignKeys": {},
34 "compositePrimaryKeys": {34 "compositePrimaryKeys": {
35 "blob_namespace_key_pk": {35 "blob_namespace_key_pk": {
36 "columns": [36 "columns": ["namespace", "key"],
37 "namespace",
38 "key"
39 ],
40 "name": "blob_namespace_key_pk"37 "name": "blob_namespace_key_pk"
41 }38 }
42 },39 },
@@ -99,33 +96,24 @@ · 56 unmodified lines
99 "indexes": {96 "indexes": {
100 "definition_scope_id_idx": {97 "definition_scope_id_idx": {
101 "name": "definition_scope_id_idx",98 "name": "definition_scope_id_idx",
102 "columns": [99 "columns": ["scope_id"],
103 "scope_id"
104 ],
105 "isUnique": false100 "isUnique": false
106 },101 },
107 "definition_source_id_idx": {102 "definition_source_id_idx": {
108 "name": "definition_source_id_idx",103 "name": "definition_source_id_idx",
109 "columns": [104 "columns": ["source_id"],
110 "source_id"
111 ],
112 "isUnique": false105 "isUnique": false
113 },106 },
114 "definition_plugin_id_idx": {107 "definition_plugin_id_idx": {
115 "name": "definition_plugin_id_idx",108 "name": "definition_plugin_id_idx",
116 "columns": [109 "columns": ["plugin_id"],
117 "plugin_id"
118 ],
119 "isUnique": false110 "isUnique": false
120 }111 }
121 },112 },
122 "foreignKeys": {},113 "foreignKeys": {},
123 "compositePrimaryKeys": {114 "compositePrimaryKeys": {
124 "definition_scope_id_id_pk": {115 "definition_scope_id_id_pk": {
125 "columns": [116 "columns": ["scope_id", "id"],
126 "scope_id",
127 "id"
128 ],
129 "name": "definition_scope_id_id_pk"117 "name": "definition_scope_id_id_pk"
130 }118 }
131 },119 },
@@ -174,26 +162,19 @@ · 42 unmodified lines
174 "indexes": {162 "indexes": {
175 "google_discovery_binding_scope_id_idx": {163 "google_discovery_binding_scope_id_idx": {
176 "name": "google_discovery_binding_scope_id_idx",164 "name": "google_discovery_binding_scope_id_idx",
177 "columns": [165 "columns": ["scope_id"],
178 "scope_id"
179 ],
180 "isUnique": false166 "isUnique": false
181 },167 },
182 "google_discovery_binding_source_id_idx": {168 "google_discovery_binding_source_id_idx": {
183 "name": "google_discovery_binding_source_id_idx",169 "name": "google_discovery_binding_source_id_idx",
184 "columns": [170 "columns": ["source_id"],
185 "source_id"
186 ],
187 "isUnique": false171 "isUnique": false
188 }172 }
189 },173 },
190 "foreignKeys": {},174 "foreignKeys": {},
191 "compositePrimaryKeys": {175 "compositePrimaryKeys": {
192 "google_discovery_binding_scope_id_id_pk": {176 "google_discovery_binding_scope_id_id_pk": {
193 "columns": [177 "columns": ["scope_id", "id"],
194 "scope_id",
195 "id"
196 ],
197 "name": "google_discovery_binding_scope_id_id_pk"178 "name": "google_discovery_binding_scope_id_id_pk"
198 }179 }
199 },180 },
@@ -235,19 +216,14 @@ · 35 unmodified lines
235 "indexes": {216 "indexes": {
236 "google_discovery_oauth_session_scope_id_idx": {217 "google_discovery_oauth_session_scope_id_idx": {
237 "name": "google_discovery_oauth_session_scope_id_idx",218 "name": "google_discovery_oauth_session_scope_id_idx",
238 "columns": [219 "columns": ["scope_id"],
239 "scope_id"
240 ],
241 "isUnique": false220 "isUnique": false
242 }221 }
243 },222 },
244 "foreignKeys": {},223 "foreignKeys": {},
245 "compositePrimaryKeys": {224 "compositePrimaryKeys": {
246 "google_discovery_oauth_session_scope_id_id_pk": {225 "google_discovery_oauth_session_scope_id_id_pk": {
247 "columns": [226 "columns": ["scope_id", "id"],
248 "scope_id",
249 "id"
250 ],
251 "name": "google_discovery_oauth_session_scope_id_id_pk"227 "name": "google_discovery_oauth_session_scope_id_id_pk"
252 }228 }
253 },229 },
@@ -303,19 +279,14 @@ · 49 unmodified lines
303 "indexes": {279 "indexes": {
304 "google_discovery_source_scope_id_idx": {280 "google_discovery_source_scope_id_idx": {
305 "name": "google_discovery_source_scope_id_idx",281 "name": "google_discovery_source_scope_id_idx",
306 "columns": [282 "columns": ["scope_id"],
307 "scope_id"
308 ],
309 "isUnique": false283 "isUnique": false
310 }284 }
311 },285 },
312 "foreignKeys": {},286 "foreignKeys": {},
313 "compositePrimaryKeys": {287 "compositePrimaryKeys": {
314 "google_discovery_source_scope_id_id_pk": {288 "google_discovery_source_scope_id_id_pk": {
315 "columns": [289 "columns": ["scope_id", "id"],
316 "scope_id",
317 "id"
318 ],
319 "name": "google_discovery_source_scope_id_id_pk"290 "name": "google_discovery_source_scope_id_id_pk"
320 }291 }
321 },292 },
@@ -357,26 +328,19 @@ · 35 unmodified lines
357 "indexes": {328 "indexes": {
358 "graphql_operation_scope_id_idx": {329 "graphql_operation_scope_id_idx": {
359 "name": "graphql_operation_scope_id_idx",330 "name": "graphql_operation_scope_id_idx",
360 "columns": [331 "columns": ["scope_id"],
361 "scope_id"
362 ],
363 "isUnique": false332 "isUnique": false
364 },333 },
365 "graphql_operation_source_id_idx": {334 "graphql_operation_source_id_idx": {
366 "name": "graphql_operation_source_id_idx",335 "name": "graphql_operation_source_id_idx",
367 "columns": [336 "columns": ["source_id"],
368 "source_id"
369 ],
370 "isUnique": false337 "isUnique": false
371 }338 }
372 },339 },
373 "foreignKeys": {},340 "foreignKeys": {},
374 "compositePrimaryKeys": {341 "compositePrimaryKeys": {
375 "graphql_operation_scope_id_id_pk": {342 "graphql_operation_scope_id_id_pk": {
376 "columns": [343 "columns": ["scope_id", "id"],
377 "scope_id",
378 "id"
379 ],
380 "name": "graphql_operation_scope_id_id_pk"344 "name": "graphql_operation_scope_id_id_pk"
381 }345 }
382 },346 },
@@ -425,19 +389,14 @@ · 42 unmodified lines
425 "indexes": {389 "indexes": {
426 "graphql_source_scope_id_idx": {390 "graphql_source_scope_id_idx": {
427 "name": "graphql_source_scope_id_idx",391 "name": "graphql_source_scope_id_idx",
428 "columns": [392 "columns": ["scope_id"],
429 "scope_id"
430 ],
431 "isUnique": false393 "isUnique": false
432 }394 }
433 },395 },
434 "foreignKeys": {},396 "foreignKeys": {},
435 "compositePrimaryKeys": {397 "compositePrimaryKeys": {
436 "graphql_source_scope_id_id_pk": {398 "graphql_source_scope_id_id_pk": {
437 "columns": [399 "columns": ["scope_id", "id"],
438 "scope_id",
439 "id"
440 ],
441 "name": "graphql_source_scope_id_id_pk"400 "name": "graphql_source_scope_id_id_pk"
442 }401 }
443 },402 },
@@ -486,26 +445,19 @@ · 42 unmodified lines
486 "indexes": {445 "indexes": {
487 "mcp_binding_scope_id_idx": {446 "mcp_binding_scope_id_idx": {
488 "name": "mcp_binding_scope_id_idx",447 "name": "mcp_binding_scope_id_idx",
489 "columns": [448 "columns": ["scope_id"],
490 "scope_id"
491 ],
492 "isUnique": false449 "isUnique": false
493 },450 },
494 "mcp_binding_source_id_idx": {451 "mcp_binding_source_id_idx": {
495 "name": "mcp_binding_source_id_idx",452 "name": "mcp_binding_source_id_idx",
496 "columns": [453 "columns": ["source_id"],
497 "source_id"
498 ],
499 "isUnique": false454 "isUnique": false
500 }455 }
501 },456 },
502 "foreignKeys": {},457 "foreignKeys": {},
503 "compositePrimaryKeys": {458 "compositePrimaryKeys": {
504 "mcp_binding_scope_id_id_pk": {459 "mcp_binding_scope_id_id_pk": {
505 "columns": [460 "columns": ["scope_id", "id"],
506 "scope_id",
507 "id"
508 ],
509 "name": "mcp_binding_scope_id_id_pk"461 "name": "mcp_binding_scope_id_id_pk"
510 }462 }
511 },463 },
@@ -554,19 +506,14 @@ · 42 unmodified lines
554 "indexes": {506 "indexes": {
555 "mcp_oauth_session_scope_id_idx": {507 "mcp_oauth_session_scope_id_idx": {
556 "name": "mcp_oauth_session_scope_id_idx",508 "name": "mcp_oauth_session_scope_id_idx",
557 "columns": [509 "columns": ["scope_id"],
558 "scope_id"
559 ],
560 "isUnique": false510 "isUnique": false
561 }511 }
562 },512 },
563 "foreignKeys": {},513 "foreignKeys": {},
564 "compositePrimaryKeys": {514 "compositePrimaryKeys": {
565 "mcp_oauth_session_scope_id_id_pk": {515 "mcp_oauth_session_scope_id_id_pk": {
566 "columns": [516 "columns": ["scope_id", "id"],
567 "scope_id",
568 "id"
569 ],
570 "name": "mcp_oauth_session_scope_id_id_pk"517 "name": "mcp_oauth_session_scope_id_id_pk"
571 }518 }
572 },519 },
@@ -615,19 +562,14 @@ · 42 unmodified lines
615 "indexes": {562 "indexes": {
616 "mcp_source_scope_id_idx": {563 "mcp_source_scope_id_idx": {
617 "name": "mcp_source_scope_id_idx",564 "name": "mcp_source_scope_id_idx",
618 "columns": [565 "columns": ["scope_id"],
619 "scope_id"
620 ],
621 "isUnique": false566 "isUnique": false
622 }567 }
623 },568 },
624 "foreignKeys": {},569 "foreignKeys": {},
625 "compositePrimaryKeys": {570 "compositePrimaryKeys": {
626 "mcp_source_scope_id_id_pk": {571 "mcp_source_scope_id_id_pk": {
627 "columns": [572 "columns": ["scope_id", "id"],
628 "scope_id",
629 "id"
630 ],
631 "name": "mcp_source_scope_id_id_pk"573 "name": "mcp_source_scope_id_id_pk"
632 }574 }
633 },575 },
@@ -669,19 +611,14 @@ · 35 unmodified lines
669 "indexes": {611 "indexes": {
670 "openapi_oauth_session_scope_id_idx": {612 "openapi_oauth_session_scope_id_idx": {
671 "name": "openapi_oauth_session_scope_id_idx",613 "name": "openapi_oauth_session_scope_id_idx",
672 "columns": [614 "columns": ["scope_id"],
673 "scope_id"
674 ],
675 "isUnique": false615 "isUnique": false
676 }616 }
677 },617 },
678 "foreignKeys": {},618 "foreignKeys": {},
679 "compositePrimaryKeys": {619 "compositePrimaryKeys": {
680 "openapi_oauth_session_scope_id_id_pk": {620 "openapi_oauth_session_scope_id_id_pk": {
681 "columns": [621 "columns": ["scope_id", "id"],
682 "scope_id",
683 "id"
684 ],
685 "name": "openapi_oauth_session_scope_id_id_pk"622 "name": "openapi_oauth_session_scope_id_id_pk"
686 }623 }
687 },624 },
@@ -723,26 +660,19 @@ · 35 unmodified lines
723 "indexes": {660 "indexes": {
724 "openapi_operation_scope_id_idx": {661 "openapi_operation_scope_id_idx": {
725 "name": "openapi_operation_scope_id_idx",662 "name": "openapi_operation_scope_id_idx",
726 "columns": [663 "columns": ["scope_id"],
727 "scope_id"
728 ],
729 "isUnique": false664 "isUnique": false
730 },665 },
731 "openapi_operation_source_id_idx": {666 "openapi_operation_source_id_idx": {
732 "name": "openapi_operation_source_id_idx",667 "name": "openapi_operation_source_id_idx",
733 "columns": [668 "columns": ["source_id"],
734 "source_id"
735 ],
736 "isUnique": false669 "isUnique": false
737 }670 }
738 },671 },
739 "foreignKeys": {},672 "foreignKeys": {},
740 "compositePrimaryKeys": {673 "compositePrimaryKeys": {
741 "openapi_operation_scope_id_id_pk": {674 "openapi_operation_scope_id_id_pk": {
742 "columns": [675 "columns": ["scope_id", "id"],
743 "scope_id",
744 "id"
745 ],
746 "name": "openapi_operation_scope_id_id_pk"676 "name": "openapi_operation_scope_id_id_pk"
747 }677 }
748 },678 },
@@ -812,19 +742,14 @@ · 63 unmodified lines
812 "indexes": {742 "indexes": {
813 "openapi_source_scope_id_idx": {743 "openapi_source_scope_id_idx": {
814 "name": "openapi_source_scope_id_idx",744 "name": "openapi_source_scope_id_idx",
815 "columns": [745 "columns": ["scope_id"],
816 "scope_id"
817 ],
818 "isUnique": false746 "isUnique": false
819 }747 }
820 },748 },
821 "foreignKeys": {},749 "foreignKeys": {},
822 "compositePrimaryKeys": {750 "compositePrimaryKeys": {
823 "openapi_source_scope_id_id_pk": {751 "openapi_source_scope_id_id_pk": {
824 "columns": [752 "columns": ["scope_id", "id"],
825 "scope_id",
826 "id"
827 ],
828 "name": "openapi_source_scope_id_id_pk"753 "name": "openapi_source_scope_id_id_pk"
829 }754 }
830 },755 },
@@ -873,26 +798,19 @@ · 42 unmodified lines
873 "indexes": {798 "indexes": {
874 "secret_scope_id_idx": {799 "secret_scope_id_idx": {
875 "name": "secret_scope_id_idx",800 "name": "secret_scope_id_idx",
876 "columns": [801 "columns": ["scope_id"],
877 "scope_id"
878 ],
879 "isUnique": false802 "isUnique": false
880 },803 },
881 "secret_provider_idx": {804 "secret_provider_idx": {
882 "name": "secret_provider_idx",805 "name": "secret_provider_idx",
883 "columns": [806 "columns": ["provider"],
884 "provider"
885 ],
886 "isUnique": false807 "isUnique": false
887 }808 }
888 },809 },
889 "foreignKeys": {},810 "foreignKeys": {},
890 "compositePrimaryKeys": {811 "compositePrimaryKeys": {
891 "secret_scope_id_id_pk": {812 "secret_scope_id_id_pk": {
892 "columns": [813 "columns": ["scope_id", "id"],
893 "scope_id",
894 "id"
895 ],
896 "name": "secret_scope_id_id_pk"814 "name": "secret_scope_id_id_pk"
897 }815 }
898 },816 },
@@ -986,26 +904,19 @@ · 87 unmodified lines
986 "indexes": {904 "indexes": {
987 "source_scope_id_idx": {905 "source_scope_id_idx": {
988 "name": "source_scope_id_idx",906 "name": "source_scope_id_idx",
989 "columns": [907 "columns": ["scope_id"],
990 "scope_id"
991 ],
992 "isUnique": false908 "isUnique": false
993 },909 },
994 "source_plugin_id_idx": {910 "source_plugin_id_idx": {
995 "name": "source_plugin_id_idx",911 "name": "source_plugin_id_idx",
996 "columns": [912 "columns": ["plugin_id"],
997 "plugin_id"
998 ],
999 "isUnique": false913 "isUnique": false
1000 }914 }
1001 },915 },
1002 "foreignKeys": {},916 "foreignKeys": {},
1003 "compositePrimaryKeys": {917 "compositePrimaryKeys": {
1004 "source_scope_id_id_pk": {918 "source_scope_id_id_pk": {
1005 "columns": [919 "columns": ["scope_id", "id"],
1006 "scope_id",
1007 "id"
1008 ],
1009 "name": "source_scope_id_id_pk"920 "name": "source_scope_id_id_pk"
1010 }921 }
1011 },922 },
@@ -1089,33 +1000,24 @@ · 77 unmodified lines
1089 "indexes": {1000 "indexes": {
1090 "tool_scope_id_idx": {1001 "tool_scope_id_idx": {
1091 "name": "tool_scope_id_idx",1002 "name": "tool_scope_id_idx",
1092 "columns": [1003 "columns": ["scope_id"],
1093 "scope_id"
1094 ],
1095 "isUnique": false1004 "isUnique": false
1096 },1005 },
1097 "tool_source_id_idx": {1006 "tool_source_id_idx": {
1098 "name": "tool_source_id_idx",1007 "name": "tool_source_id_idx",
1099 "columns": [1008 "columns": ["source_id"],
1100 "source_id"
1101 ],
1102 "isUnique": false1009 "isUnique": false
1103 },1010 },
1104 "tool_plugin_id_idx": {1011 "tool_plugin_id_idx": {
1105 "name": "tool_plugin_id_idx",1012 "name": "tool_plugin_id_idx",
1106 "columns": [1013 "columns": ["plugin_id"],
1107 "plugin_id"
1108 ],
1109 "isUnique": false1014 "isUnique": false
1110 }1015 }
1111 },1016 },
1112 "foreignKeys": {},1017 "foreignKeys": {},
1113 "compositePrimaryKeys": {1018 "compositePrimaryKeys": {
1114 "tool_scope_id_id_pk": {1019 "tool_scope_id_id_pk": {
1115 "columns": [1020 "columns": ["scope_id", "id"],
1116 "scope_id",
1117 "id"
1118 ],
1119 "name": "tool_scope_id_id_pk"1021 "name": "tool_scope_id_id_pk"
1120 }1022 }
1121 },1023 },
@@ -1133,4 +1035,4 @@ · 11 unmodified lines
1133 "internal": {1035 "internal": {
1134 "indexes": {}1036 "indexes": {}
1135 }1037 }
1136}1038}
apps/local/drizzle/meta/0001_snapshot.json+42−140
@@ -33,10 +33,7 @@ · 32 unmodified lines
33 "foreignKeys": {},33 "foreignKeys": {},
34 "compositePrimaryKeys": {34 "compositePrimaryKeys": {
35 "blob_namespace_key_pk": {35 "blob_namespace_key_pk": {
36 "columns": [36 "columns": ["namespace", "key"],
37 "namespace",
38 "key"
39 ],
40 "name": "blob_namespace_key_pk"37 "name": "blob_namespace_key_pk"
41 }38 }
42 },39 },
@@ -99,33 +96,24 @@ · 56 unmodified lines
99 "indexes": {96 "indexes": {
100 "definition_scope_id_idx": {97 "definition_scope_id_idx": {
101 "name": "definition_scope_id_idx",98 "name": "definition_scope_id_idx",
102 "columns": [99 "columns": ["scope_id"],
103 "scope_id"
104 ],
105 "isUnique": false100 "isUnique": false
106 },101 },
107 "definition_source_id_idx": {102 "definition_source_id_idx": {
108 "name": "definition_source_id_idx",103 "name": "definition_source_id_idx",
109 "columns": [104 "columns": ["source_id"],
110 "source_id"
111 ],
112 "isUnique": false105 "isUnique": false
113 },106 },
114 "definition_plugin_id_idx": {107 "definition_plugin_id_idx": {
115 "name": "definition_plugin_id_idx",108 "name": "definition_plugin_id_idx",
116 "columns": [109 "columns": ["plugin_id"],
117 "plugin_id"
118 ],
119 "isUnique": false110 "isUnique": false
120 }111 }
121 },112 },
122 "foreignKeys": {},113 "foreignKeys": {},
123 "compositePrimaryKeys": {114 "compositePrimaryKeys": {
124 "definition_scope_id_id_pk": {115 "definition_scope_id_id_pk": {
125 "columns": [116 "columns": ["scope_id", "id"],
126 "scope_id",
127 "id"
128 ],
129 "name": "definition_scope_id_id_pk"117 "name": "definition_scope_id_id_pk"
130 }118 }
131 },119 },
@@ -174,26 +162,19 @@ · 42 unmodified lines
174 "indexes": {162 "indexes": {
175 "google_discovery_binding_scope_id_idx": {163 "google_discovery_binding_scope_id_idx": {
176 "name": "google_discovery_binding_scope_id_idx",164 "name": "google_discovery_binding_scope_id_idx",
177 "columns": [165 "columns": ["scope_id"],
178 "scope_id"
179 ],
180 "isUnique": false166 "isUnique": false
181 },167 },
182 "google_discovery_binding_source_id_idx": {168 "google_discovery_binding_source_id_idx": {
183 "name": "google_discovery_binding_source_id_idx",169 "name": "google_discovery_binding_source_id_idx",
184 "columns": [170 "columns": ["source_id"],
185 "source_id"
186 ],
187 "isUnique": false171 "isUnique": false
188 }172 }
189 },173 },
190 "foreignKeys": {},174 "foreignKeys": {},
191 "compositePrimaryKeys": {175 "compositePrimaryKeys": {
192 "google_discovery_binding_scope_id_id_pk": {176 "google_discovery_binding_scope_id_id_pk": {
193 "columns": [177 "columns": ["scope_id", "id"],
194 "scope_id",
195 "id"
196 ],
197 "name": "google_discovery_binding_scope_id_id_pk"178 "name": "google_discovery_binding_scope_id_id_pk"
198 }179 }
199 },180 },
@@ -235,19 +216,14 @@ · 35 unmodified lines
235 "indexes": {216 "indexes": {
236 "google_discovery_oauth_session_scope_id_idx": {217 "google_discovery_oauth_session_scope_id_idx": {
237 "name": "google_discovery_oauth_session_scope_id_idx",218 "name": "google_discovery_oauth_session_scope_id_idx",
238 "columns": [219 "columns": ["scope_id"],
239 "scope_id"
240 ],
241 "isUnique": false220 "isUnique": false
242 }221 }
243 },222 },
244 "foreignKeys": {},223 "foreignKeys": {},
245 "compositePrimaryKeys": {224 "compositePrimaryKeys": {
246 "google_discovery_oauth_session_scope_id_id_pk": {225 "google_discovery_oauth_session_scope_id_id_pk": {
247 "columns": [226 "columns": ["scope_id", "id"],
248 "scope_id",
249 "id"
250 ],
251 "name": "google_discovery_oauth_session_scope_id_id_pk"227 "name": "google_discovery_oauth_session_scope_id_id_pk"
252 }228 }
253 },229 },
@@ -303,19 +279,14 @@ · 49 unmodified lines
303 "indexes": {279 "indexes": {
304 "google_discovery_source_scope_id_idx": {280 "google_discovery_source_scope_id_idx": {
305 "name": "google_discovery_source_scope_id_idx",281 "name": "google_discovery_source_scope_id_idx",
306 "columns": [282 "columns": ["scope_id"],
307 "scope_id"
308 ],
309 "isUnique": false283 "isUnique": false
310 }284 }
311 },285 },
312 "foreignKeys": {},286 "foreignKeys": {},
313 "compositePrimaryKeys": {287 "compositePrimaryKeys": {
314 "google_discovery_source_scope_id_id_pk": {288 "google_discovery_source_scope_id_id_pk": {
315 "columns": [289 "columns": ["scope_id", "id"],
316 "scope_id",
317 "id"
318 ],
319 "name": "google_discovery_source_scope_id_id_pk"290 "name": "google_discovery_source_scope_id_id_pk"
320 }291 }
321 },292 },
@@ -357,26 +328,19 @@ · 35 unmodified lines
357 "indexes": {328 "indexes": {
358 "graphql_operation_scope_id_idx": {329 "graphql_operation_scope_id_idx": {
359 "name": "graphql_operation_scope_id_idx",330 "name": "graphql_operation_scope_id_idx",
360 "columns": [331 "columns": ["scope_id"],
361 "scope_id"
362 ],
363 "isUnique": false332 "isUnique": false
364 },333 },
365 "graphql_operation_source_id_idx": {334 "graphql_operation_source_id_idx": {
366 "name": "graphql_operation_source_id_idx",335 "name": "graphql_operation_source_id_idx",
367 "columns": [336 "columns": ["source_id"],
368 "source_id"
369 ],
370 "isUnique": false337 "isUnique": false
371 }338 }
372 },339 },
373 "foreignKeys": {},340 "foreignKeys": {},
374 "compositePrimaryKeys": {341 "compositePrimaryKeys": {
375 "graphql_operation_scope_id_id_pk": {342 "graphql_operation_scope_id_id_pk": {
376 "columns": [343 "columns": ["scope_id", "id"],
377 "scope_id",
378 "id"
379 ],
380 "name": "graphql_operation_scope_id_id_pk"344 "name": "graphql_operation_scope_id_id_pk"
381 }345 }
382 },346 },
@@ -425,19 +389,14 @@ · 42 unmodified lines
425 "indexes": {389 "indexes": {
426 "graphql_source_scope_id_idx": {390 "graphql_source_scope_id_idx": {
427 "name": "graphql_source_scope_id_idx",391 "name": "graphql_source_scope_id_idx",
428 "columns": [392 "columns": ["scope_id"],
429 "scope_id"
430 ],
431 "isUnique": false393 "isUnique": false
432 }394 }
433 },395 },
434 "foreignKeys": {},396 "foreignKeys": {},
435 "compositePrimaryKeys": {397 "compositePrimaryKeys": {
436 "graphql_source_scope_id_id_pk": {398 "graphql_source_scope_id_id_pk": {
437 "columns": [399 "columns": ["scope_id", "id"],
438 "scope_id",
439 "id"
440 ],
441 "name": "graphql_source_scope_id_id_pk"400 "name": "graphql_source_scope_id_id_pk"
442 }401 }
443 },402 },
@@ -486,26 +445,19 @@ · 42 unmodified lines
486 "indexes": {445 "indexes": {
487 "mcp_binding_scope_id_idx": {446 "mcp_binding_scope_id_idx": {
488 "name": "mcp_binding_scope_id_idx",447 "name": "mcp_binding_scope_id_idx",
489 "columns": [448 "columns": ["scope_id"],
490 "scope_id"
491 ],
492 "isUnique": false449 "isUnique": false
493 },450 },
494 "mcp_binding_source_id_idx": {451 "mcp_binding_source_id_idx": {
495 "name": "mcp_binding_source_id_idx",452 "name": "mcp_binding_source_id_idx",
496 "columns": [453 "columns": ["source_id"],
497 "source_id"
498 ],
499 "isUnique": false454 "isUnique": false
500 }455 }
501 },456 },
502 "foreignKeys": {},457 "foreignKeys": {},
503 "compositePrimaryKeys": {458 "compositePrimaryKeys": {
504 "mcp_binding_scope_id_id_pk": {459 "mcp_binding_scope_id_id_pk": {
505 "columns": [460 "columns": ["scope_id", "id"],
506 "scope_id",
507 "id"
508 ],
509 "name": "mcp_binding_scope_id_id_pk"461 "name": "mcp_binding_scope_id_id_pk"
510 }462 }
511 },463 },
@@ -554,19 +506,14 @@ · 42 unmodified lines
554 "indexes": {506 "indexes": {
555 "mcp_oauth_session_scope_id_idx": {507 "mcp_oauth_session_scope_id_idx": {
556 "name": "mcp_oauth_session_scope_id_idx",508 "name": "mcp_oauth_session_scope_id_idx",
557 "columns": [509 "columns": ["scope_id"],
558 "scope_id"
559 ],
560 "isUnique": false510 "isUnique": false
561 }511 }
562 },512 },
563 "foreignKeys": {},513 "foreignKeys": {},
564 "compositePrimaryKeys": {514 "compositePrimaryKeys": {
565 "mcp_oauth_session_scope_id_id_pk": {515 "mcp_oauth_session_scope_id_id_pk": {
566 "columns": [516 "columns": ["scope_id", "id"],
567 "scope_id",
568 "id"
569 ],
570 "name": "mcp_oauth_session_scope_id_id_pk"517 "name": "mcp_oauth_session_scope_id_id_pk"
571 }518 }
572 },519 },
@@ -615,19 +562,14 @@ · 42 unmodified lines
615 "indexes": {562 "indexes": {
616 "mcp_source_scope_id_idx": {563 "mcp_source_scope_id_idx": {
617 "name": "mcp_source_scope_id_idx",564 "name": "mcp_source_scope_id_idx",
618 "columns": [565 "columns": ["scope_id"],
619 "scope_id"
620 ],
621 "isUnique": false566 "isUnique": false
622 }567 }
623 },568 },
624 "foreignKeys": {},569 "foreignKeys": {},
625 "compositePrimaryKeys": {570 "compositePrimaryKeys": {
626 "mcp_source_scope_id_id_pk": {571 "mcp_source_scope_id_id_pk": {
627 "columns": [572 "columns": ["scope_id", "id"],
628 "scope_id",
629 "id"
630 ],
631 "name": "mcp_source_scope_id_id_pk"573 "name": "mcp_source_scope_id_id_pk"
632 }574 }
633 },575 },
@@ -669,19 +611,14 @@ · 35 unmodified lines
669 "indexes": {611 "indexes": {
670 "openapi_oauth_session_scope_id_idx": {612 "openapi_oauth_session_scope_id_idx": {
671 "name": "openapi_oauth_session_scope_id_idx",613 "name": "openapi_oauth_session_scope_id_idx",
672 "columns": [614 "columns": ["scope_id"],
673 "scope_id"
674 ],
675 "isUnique": false615 "isUnique": false
676 }616 }
677 },617 },
678 "foreignKeys": {},618 "foreignKeys": {},
679 "compositePrimaryKeys": {619 "compositePrimaryKeys": {
680 "openapi_oauth_session_scope_id_id_pk": {620 "openapi_oauth_session_scope_id_id_pk": {
681 "columns": [621 "columns": ["scope_id", "id"],
682 "scope_id",
683 "id"
684 ],
685 "name": "openapi_oauth_session_scope_id_id_pk"622 "name": "openapi_oauth_session_scope_id_id_pk"
686 }623 }
687 },624 },
@@ -723,26 +660,19 @@ · 35 unmodified lines
723 "indexes": {660 "indexes": {
724 "openapi_operation_scope_id_idx": {661 "openapi_operation_scope_id_idx": {
725 "name": "openapi_operation_scope_id_idx",662 "name": "openapi_operation_scope_id_idx",
726 "columns": [663 "columns": ["scope_id"],
727 "scope_id"
728 ],
729 "isUnique": false664 "isUnique": false
730 },665 },
731 "openapi_operation_source_id_idx": {666 "openapi_operation_source_id_idx": {
732 "name": "openapi_operation_source_id_idx",667 "name": "openapi_operation_source_id_idx",
733 "columns": [668 "columns": ["source_id"],
734 "source_id"
735 ],
736 "isUnique": false669 "isUnique": false
737 }670 }
738 },671 },
739 "foreignKeys": {},672 "foreignKeys": {},
740 "compositePrimaryKeys": {673 "compositePrimaryKeys": {
741 "openapi_operation_scope_id_id_pk": {674 "openapi_operation_scope_id_id_pk": {
742 "columns": [675 "columns": ["scope_id", "id"],
743 "scope_id",
744 "id"
745 ],
746 "name": "openapi_operation_scope_id_id_pk"676 "name": "openapi_operation_scope_id_id_pk"
747 }677 }
748 },678 },
@@ -819,19 +749,14 @@ · 70 unmodified lines
819 "indexes": {749 "indexes": {
820 "openapi_source_scope_id_idx": {750 "openapi_source_scope_id_idx": {
821 "name": "openapi_source_scope_id_idx",751 "name": "openapi_source_scope_id_idx",
822 "columns": [752 "columns": ["scope_id"],
823 "scope_id"
824 ],
825 "isUnique": false753 "isUnique": false
826 }754 }
827 },755 },
828 "foreignKeys": {},756 "foreignKeys": {},
829 "compositePrimaryKeys": {757 "compositePrimaryKeys": {
830 "openapi_source_scope_id_id_pk": {758 "openapi_source_scope_id_id_pk": {
831 "columns": [759 "columns": ["scope_id", "id"],
832 "scope_id",
833 "id"
834 ],
835 "name": "openapi_source_scope_id_id_pk"760 "name": "openapi_source_scope_id_id_pk"
836 }761 }
837 },762 },
@@ -880,26 +805,19 @@ · 42 unmodified lines
880 "indexes": {805 "indexes": {
881 "secret_scope_id_idx": {806 "secret_scope_id_idx": {
882 "name": "secret_scope_id_idx",807 "name": "secret_scope_id_idx",
883 "columns": [808 "columns": ["scope_id"],
884 "scope_id"
885 ],
886 "isUnique": false809 "isUnique": false
887 },810 },
888 "secret_provider_idx": {811 "secret_provider_idx": {
889 "name": "secret_provider_idx",812 "name": "secret_provider_idx",
890 "columns": [813 "columns": ["provider"],
891 "provider"
892 ],
893 "isUnique": false814 "isUnique": false
894 }815 }
895 },816 },
896 "foreignKeys": {},817 "foreignKeys": {},
897 "compositePrimaryKeys": {818 "compositePrimaryKeys": {
898 "secret_scope_id_id_pk": {819 "secret_scope_id_id_pk": {
899 "columns": [820 "columns": ["scope_id", "id"],
900 "scope_id",
901 "id"
902 ],
903 "name": "secret_scope_id_id_pk"821 "name": "secret_scope_id_id_pk"
904 }822 }
905 },823 },
@@ -993,26 +911,19 @@ · 87 unmodified lines
993 "indexes": {911 "indexes": {
994 "source_scope_id_idx": {912 "source_scope_id_idx": {
995 "name": "source_scope_id_idx",913 "name": "source_scope_id_idx",
996 "columns": [914 "columns": ["scope_id"],
997 "scope_id"
998 ],
999 "isUnique": false915 "isUnique": false
1000 },916 },
1001 "source_plugin_id_idx": {917 "source_plugin_id_idx": {
1002 "name": "source_plugin_id_idx",918 "name": "source_plugin_id_idx",
1003 "columns": [919 "columns": ["plugin_id"],
1004 "plugin_id"
1005 ],
1006 "isUnique": false920 "isUnique": false
1007 }921 }
1008 },922 },
1009 "foreignKeys": {},923 "foreignKeys": {},
1010 "compositePrimaryKeys": {924 "compositePrimaryKeys": {
1011 "source_scope_id_id_pk": {925 "source_scope_id_id_pk": {
1012 "columns": [926 "columns": ["scope_id", "id"],
1013 "scope_id",
1014 "id"
1015 ],
1016 "name": "source_scope_id_id_pk"927 "name": "source_scope_id_id_pk"
1017 }928 }
1018 },929 },
@@ -1096,33 +1007,24 @@ · 77 unmodified lines
1096 "indexes": {1007 "indexes": {
1097 "tool_scope_id_idx": {1008 "tool_scope_id_idx": {
1098 "name": "tool_scope_id_idx",1009 "name": "tool_scope_id_idx",
1099 "columns": [1010 "columns": ["scope_id"],
1100 "scope_id"
1101 ],
1102 "isUnique": false1011 "isUnique": false
1103 },1012 },
1104 "tool_source_id_idx": {1013 "tool_source_id_idx": {
1105 "name": "tool_source_id_idx",1014 "name": "tool_source_id_idx",
1106 "columns": [1015 "columns": ["source_id"],
1107 "source_id"
1108 ],
1109 "isUnique": false1016 "isUnique": false
1110 },1017 },
1111 "tool_plugin_id_idx": {1018 "tool_plugin_id_idx": {
1112 "name": "tool_plugin_id_idx",1019 "name": "tool_plugin_id_idx",
1113 "columns": [1020 "columns": ["plugin_id"],
1114 "plugin_id"
1115 ],
1116 "isUnique": false1021 "isUnique": false
1117 }1022 }
1118 },1023 },
1119 "foreignKeys": {},1024 "foreignKeys": {},
1120 "compositePrimaryKeys": {1025 "compositePrimaryKeys": {
1121 "tool_scope_id_id_pk": {1026 "tool_scope_id_id_pk": {
1122 "columns": [1027 "columns": ["scope_id", "id"],
1123 "scope_id",
1124 "id"
1125 ],
1126 "name": "tool_scope_id_id_pk"1028 "name": "tool_scope_id_id_pk"
1127 }1029 }
1128 },1030 },
@@ -1140,4 +1042,4 @@ · 11 unmodified lines
1140 "internal": {1042 "internal": {
1141 "indexes": {}1043 "indexes": {}
1142 }1044 }
1143}1045}
apps/local/drizzle/meta/0002_snapshot.json+46−153
@@ -33,10 +33,7 @@ · 32 unmodified lines
33 "foreignKeys": {},33 "foreignKeys": {},
34 "compositePrimaryKeys": {34 "compositePrimaryKeys": {
35 "blob_namespace_key_pk": {35 "blob_namespace_key_pk": {
36 "columns": [36 "columns": ["namespace", "key"],
37 "namespace",
38 "key"
39 ],
40 "name": "blob_namespace_key_pk"37 "name": "blob_namespace_key_pk"
41 }38 }
42 },39 },
@@ -134,26 +131,19 @@ · 91 unmodified lines
134 "indexes": {131 "indexes": {
135 "connection_scope_id_idx": {132 "connection_scope_id_idx": {
136 "name": "connection_scope_id_idx",133 "name": "connection_scope_id_idx",
137 "columns": [134 "columns": ["scope_id"],
138 "scope_id"
139 ],
140 "isUnique": false135 "isUnique": false
141 },136 },
142 "connection_provider_idx": {137 "connection_provider_idx": {
143 "name": "connection_provider_idx",138 "name": "connection_provider_idx",
144 "columns": [139 "columns": ["provider"],
145 "provider"
146 ],
147 "isUnique": false140 "isUnique": false
148 }141 }
149 },142 },
150 "foreignKeys": {},143 "foreignKeys": {},
151 "compositePrimaryKeys": {144 "compositePrimaryKeys": {
152 "connection_scope_id_id_pk": {145 "connection_scope_id_id_pk": {
153 "columns": [146 "columns": ["scope_id", "id"],
154 "scope_id",
155 "id"
156 ],
157 "name": "connection_scope_id_id_pk"147 "name": "connection_scope_id_id_pk"
158 }148 }
159 },149 },
@@ -216,33 +206,24 @@ · 56 unmodified lines
216 "indexes": {206 "indexes": {
217 "definition_scope_id_idx": {207 "definition_scope_id_idx": {
218 "name": "definition_scope_id_idx",208 "name": "definition_scope_id_idx",
219 "columns": [209 "columns": ["scope_id"],
220 "scope_id"
221 ],
222 "isUnique": false210 "isUnique": false
223 },211 },
224 "definition_source_id_idx": {212 "definition_source_id_idx": {
225 "name": "definition_source_id_idx",213 "name": "definition_source_id_idx",
226 "columns": [214 "columns": ["source_id"],
227 "source_id"
228 ],
229 "isUnique": false215 "isUnique": false
230 },216 },
231 "definition_plugin_id_idx": {217 "definition_plugin_id_idx": {
232 "name": "definition_plugin_id_idx",218 "name": "definition_plugin_id_idx",
233 "columns": [219 "columns": ["plugin_id"],
234 "plugin_id"
235 ],
236 "isUnique": false220 "isUnique": false
237 }221 }
238 },222 },
239 "foreignKeys": {},223 "foreignKeys": {},
240 "compositePrimaryKeys": {224 "compositePrimaryKeys": {
241 "definition_scope_id_id_pk": {225 "definition_scope_id_id_pk": {
242 "columns": [226 "columns": ["scope_id", "id"],
243 "scope_id",
244 "id"
245 ],
246 "name": "definition_scope_id_id_pk"227 "name": "definition_scope_id_id_pk"
247 }228 }
248 },229 },
@@ -291,26 +272,19 @@ · 42 unmodified lines
291 "indexes": {272 "indexes": {
292 "google_discovery_binding_scope_id_idx": {273 "google_discovery_binding_scope_id_idx": {
293 "name": "google_discovery_binding_scope_id_idx",274 "name": "google_discovery_binding_scope_id_idx",
294 "columns": [275 "columns": ["scope_id"],
295 "scope_id"
296 ],
297 "isUnique": false276 "isUnique": false
298 },277 },
299 "google_discovery_binding_source_id_idx": {278 "google_discovery_binding_source_id_idx": {
300 "name": "google_discovery_binding_source_id_idx",279 "name": "google_discovery_binding_source_id_idx",
301 "columns": [280 "columns": ["source_id"],
302 "source_id"
303 ],
304 "isUnique": false281 "isUnique": false
305 }282 }
306 },283 },
307 "foreignKeys": {},284 "foreignKeys": {},
308 "compositePrimaryKeys": {285 "compositePrimaryKeys": {
309 "google_discovery_binding_scope_id_id_pk": {286 "google_discovery_binding_scope_id_id_pk": {
310 "columns": [287 "columns": ["scope_id", "id"],
311 "scope_id",
312 "id"
313 ],
314 "name": "google_discovery_binding_scope_id_id_pk"288 "name": "google_discovery_binding_scope_id_id_pk"
315 }289 }
316 },290 },
@@ -352,19 +326,14 @@ · 35 unmodified lines
352 "indexes": {326 "indexes": {
353 "google_discovery_oauth_session_scope_id_idx": {327 "google_discovery_oauth_session_scope_id_idx": {
354 "name": "google_discovery_oauth_session_scope_id_idx",328 "name": "google_discovery_oauth_session_scope_id_idx",
355 "columns": [329 "columns": ["scope_id"],
356 "scope_id"
357 ],
358 "isUnique": false330 "isUnique": false
359 }331 }
360 },332 },
361 "foreignKeys": {},333 "foreignKeys": {},
362 "compositePrimaryKeys": {334 "compositePrimaryKeys": {
363 "google_discovery_oauth_session_scope_id_id_pk": {335 "google_discovery_oauth_session_scope_id_id_pk": {
364 "columns": [336 "columns": ["scope_id", "id"],
365 "scope_id",
366 "id"
367 ],
368 "name": "google_discovery_oauth_session_scope_id_id_pk"337 "name": "google_discovery_oauth_session_scope_id_id_pk"
369 }338 }
370 },339 },
@@ -420,19 +389,14 @@ · 49 unmodified lines
420 "indexes": {389 "indexes": {
421 "google_discovery_source_scope_id_idx": {390 "google_discovery_source_scope_id_idx": {
422 "name": "google_discovery_source_scope_id_idx",391 "name": "google_discovery_source_scope_id_idx",
423 "columns": [392 "columns": ["scope_id"],
424 "scope_id"
425 ],
426 "isUnique": false393 "isUnique": false
427 }394 }
428 },395 },
429 "foreignKeys": {},396 "foreignKeys": {},
430 "compositePrimaryKeys": {397 "compositePrimaryKeys": {
431 "google_discovery_source_scope_id_id_pk": {398 "google_discovery_source_scope_id_id_pk": {
432 "columns": [399 "columns": ["scope_id", "id"],
433 "scope_id",
434 "id"
435 ],
436 "name": "google_discovery_source_scope_id_id_pk"400 "name": "google_discovery_source_scope_id_id_pk"
437 }401 }
438 },402 },
@@ -474,26 +438,19 @@ · 35 unmodified lines
474 "indexes": {438 "indexes": {
475 "graphql_operation_scope_id_idx": {439 "graphql_operation_scope_id_idx": {
476 "name": "graphql_operation_scope_id_idx",440 "name": "graphql_operation_scope_id_idx",
477 "columns": [441 "columns": ["scope_id"],
478 "scope_id"
479 ],
480 "isUnique": false442 "isUnique": false
481 },443 },
482 "graphql_operation_source_id_idx": {444 "graphql_operation_source_id_idx": {
483 "name": "graphql_operation_source_id_idx",445 "name": "graphql_operation_source_id_idx",
484 "columns": [446 "columns": ["source_id"],
485 "source_id"
486 ],
487 "isUnique": false447 "isUnique": false
488 }448 }
489 },449 },
490 "foreignKeys": {},450 "foreignKeys": {},
491 "compositePrimaryKeys": {451 "compositePrimaryKeys": {
492 "graphql_operation_scope_id_id_pk": {452 "graphql_operation_scope_id_id_pk": {
493 "columns": [453 "columns": ["scope_id", "id"],
494 "scope_id",
495 "id"
496 ],
497 "name": "graphql_operation_scope_id_id_pk"454 "name": "graphql_operation_scope_id_id_pk"
498 }455 }
499 },456 },
@@ -542,19 +499,14 @@ · 42 unmodified lines
542 "indexes": {499 "indexes": {
543 "graphql_source_scope_id_idx": {500 "graphql_source_scope_id_idx": {
544 "name": "graphql_source_scope_id_idx",501 "name": "graphql_source_scope_id_idx",
545 "columns": [502 "columns": ["scope_id"],
546 "scope_id"
547 ],
548 "isUnique": false503 "isUnique": false
549 }504 }
550 },505 },
551 "foreignKeys": {},506 "foreignKeys": {},
552 "compositePrimaryKeys": {507 "compositePrimaryKeys": {
553 "graphql_source_scope_id_id_pk": {508 "graphql_source_scope_id_id_pk": {
554 "columns": [509 "columns": ["scope_id", "id"],
555 "scope_id",
556 "id"
557 ],
558 "name": "graphql_source_scope_id_id_pk"510 "name": "graphql_source_scope_id_id_pk"
559 }511 }
560 },512 },
@@ -603,26 +555,19 @@ · 42 unmodified lines
603 "indexes": {555 "indexes": {
604 "mcp_binding_scope_id_idx": {556 "mcp_binding_scope_id_idx": {
605 "name": "mcp_binding_scope_id_idx",557 "name": "mcp_binding_scope_id_idx",
606 "columns": [558 "columns": ["scope_id"],
607 "scope_id"
608 ],
609 "isUnique": false559 "isUnique": false
610 },560 },
611 "mcp_binding_source_id_idx": {561 "mcp_binding_source_id_idx": {
612 "name": "mcp_binding_source_id_idx",562 "name": "mcp_binding_source_id_idx",
613 "columns": [563 "columns": ["source_id"],
614 "source_id"
615 ],
616 "isUnique": false564 "isUnique": false
617 }565 }
618 },566 },
619 "foreignKeys": {},567 "foreignKeys": {},
620 "compositePrimaryKeys": {568 "compositePrimaryKeys": {
621 "mcp_binding_scope_id_id_pk": {569 "mcp_binding_scope_id_id_pk": {
622 "columns": [570 "columns": ["scope_id", "id"],
623 "scope_id",
624 "id"
625 ],
626 "name": "mcp_binding_scope_id_id_pk"571 "name": "mcp_binding_scope_id_id_pk"
627 }572 }
628 },573 },
@@ -671,19 +616,14 @@ · 42 unmodified lines
671 "indexes": {616 "indexes": {
672 "mcp_oauth_session_scope_id_idx": {617 "mcp_oauth_session_scope_id_idx": {
673 "name": "mcp_oauth_session_scope_id_idx",618 "name": "mcp_oauth_session_scope_id_idx",
674 "columns": [619 "columns": ["scope_id"],
675 "scope_id"
676 ],
677 "isUnique": false620 "isUnique": false
678 }621 }
679 },622 },
680 "foreignKeys": {},623 "foreignKeys": {},
681 "compositePrimaryKeys": {624 "compositePrimaryKeys": {
682 "mcp_oauth_session_scope_id_id_pk": {625 "mcp_oauth_session_scope_id_id_pk": {
683 "columns": [626 "columns": ["scope_id", "id"],
684 "scope_id",
685 "id"
686 ],
687 "name": "mcp_oauth_session_scope_id_id_pk"627 "name": "mcp_oauth_session_scope_id_id_pk"
688 }628 }
689 },629 },
@@ -732,19 +672,14 @@ · 42 unmodified lines
732 "indexes": {672 "indexes": {
733 "mcp_source_scope_id_idx": {673 "mcp_source_scope_id_idx": {
734 "name": "mcp_source_scope_id_idx",674 "name": "mcp_source_scope_id_idx",
735 "columns": [675 "columns": ["scope_id"],
736 "scope_id"
737 ],
738 "isUnique": false676 "isUnique": false
739 }677 }
740 },678 },
741 "foreignKeys": {},679 "foreignKeys": {},
742 "compositePrimaryKeys": {680 "compositePrimaryKeys": {
743 "mcp_source_scope_id_id_pk": {681 "mcp_source_scope_id_id_pk": {
744 "columns": [682 "columns": ["scope_id", "id"],
745 "scope_id",
746 "id"
747 ],
748 "name": "mcp_source_scope_id_id_pk"683 "name": "mcp_source_scope_id_id_pk"
749 }684 }
750 },685 },
@@ -786,19 +721,14 @@ · 35 unmodified lines
786 "indexes": {721 "indexes": {
787 "openapi_oauth_session_scope_id_idx": {722 "openapi_oauth_session_scope_id_idx": {
788 "name": "openapi_oauth_session_scope_id_idx",723 "name": "openapi_oauth_session_scope_id_idx",
789 "columns": [724 "columns": ["scope_id"],
790 "scope_id"
791 ],
792 "isUnique": false725 "isUnique": false
793 }726 }
794 },727 },
795 "foreignKeys": {},728 "foreignKeys": {},
796 "compositePrimaryKeys": {729 "compositePrimaryKeys": {
797 "openapi_oauth_session_scope_id_id_pk": {730 "openapi_oauth_session_scope_id_id_pk": {
798 "columns": [731 "columns": ["scope_id", "id"],
799 "scope_id",
800 "id"
801 ],
802 "name": "openapi_oauth_session_scope_id_id_pk"732 "name": "openapi_oauth_session_scope_id_id_pk"
803 }733 }
804 },734 },
@@ -840,26 +770,19 @@ · 35 unmodified lines
840 "indexes": {770 "indexes": {
841 "openapi_operation_scope_id_idx": {771 "openapi_operation_scope_id_idx": {
842 "name": "openapi_operation_scope_id_idx",772 "name": "openapi_operation_scope_id_idx",
843 "columns": [773 "columns": ["scope_id"],
844 "scope_id"
845 ],
846 "isUnique": false774 "isUnique": false
847 },775 },
848 "openapi_operation_source_id_idx": {776 "openapi_operation_source_id_idx": {
849 "name": "openapi_operation_source_id_idx",777 "name": "openapi_operation_source_id_idx",
850 "columns": [778 "columns": ["source_id"],
851 "source_id"
852 ],
853 "isUnique": false779 "isUnique": false
854 }780 }
855 },781 },
856 "foreignKeys": {},782 "foreignKeys": {},
857 "compositePrimaryKeys": {783 "compositePrimaryKeys": {
858 "openapi_operation_scope_id_id_pk": {784 "openapi_operation_scope_id_id_pk": {
859 "columns": [785 "columns": ["scope_id", "id"],
860 "scope_id",
861 "id"
862 ],
863 "name": "openapi_operation_scope_id_id_pk"786 "name": "openapi_operation_scope_id_id_pk"
864 }787 }
865 },788 },
@@ -936,19 +859,14 @@ · 70 unmodified lines
936 "indexes": {859 "indexes": {
937 "openapi_source_scope_id_idx": {860 "openapi_source_scope_id_idx": {
938 "name": "openapi_source_scope_id_idx",861 "name": "openapi_source_scope_id_idx",
939 "columns": [862 "columns": ["scope_id"],
940 "scope_id"
941 ],
942 "isUnique": false863 "isUnique": false
943 }864 }
944 },865 },
945 "foreignKeys": {},866 "foreignKeys": {},
946 "compositePrimaryKeys": {867 "compositePrimaryKeys": {
947 "openapi_source_scope_id_id_pk": {868 "openapi_source_scope_id_id_pk": {
948 "columns": [869 "columns": ["scope_id", "id"],
949 "scope_id",
950 "id"
951 ],
952 "name": "openapi_source_scope_id_id_pk"870 "name": "openapi_source_scope_id_id_pk"
953 }871 }
954 },872 },
@@ -1004,33 +922,24 @@ · 49 unmodified lines
1004 "indexes": {922 "indexes": {
1005 "secret_scope_id_idx": {923 "secret_scope_id_idx": {
1006 "name": "secret_scope_id_idx",924 "name": "secret_scope_id_idx",
1007 "columns": [925 "columns": ["scope_id"],
1008 "scope_id"
1009 ],
1010 "isUnique": false926 "isUnique": false
1011 },927 },
1012 "secret_provider_idx": {928 "secret_provider_idx": {
1013 "name": "secret_provider_idx",929 "name": "secret_provider_idx",
1014 "columns": [930 "columns": ["provider"],
1015 "provider"
1016 ],
1017 "isUnique": false931 "isUnique": false
1018 },932 },
1019 "secret_owned_by_connection_id_idx": {933 "secret_owned_by_connection_id_idx": {
1020 "name": "secret_owned_by_connection_id_idx",934 "name": "secret_owned_by_connection_id_idx",
1021 "columns": [935 "columns": ["owned_by_connection_id"],
1022 "owned_by_connection_id"
1023 ],
1024 "isUnique": false936 "isUnique": false
1025 }937 }
1026 },938 },
1027 "foreignKeys": {},939 "foreignKeys": {},
1028 "compositePrimaryKeys": {940 "compositePrimaryKeys": {
1029 "secret_scope_id_id_pk": {941 "secret_scope_id_id_pk": {
1030 "columns": [942 "columns": ["scope_id", "id"],
1031 "scope_id",
1032 "id"
1033 ],
1034 "name": "secret_scope_id_id_pk"943 "name": "secret_scope_id_id_pk"
1035 }944 }
1036 },945 },
@@ -1124,26 +1033,19 @@ · 87 unmodified lines
1124 "indexes": {1033 "indexes": {
1125 "source_scope_id_idx": {1034 "source_scope_id_idx": {
1126 "name": "source_scope_id_idx",1035 "name": "source_scope_id_idx",
1127 "columns": [1036 "columns": ["scope_id"],
1128 "scope_id"
1129 ],
1130 "isUnique": false1037 "isUnique": false
1131 },1038 },
1132 "source_plugin_id_idx": {1039 "source_plugin_id_idx": {
1133 "name": "source_plugin_id_idx",1040 "name": "source_plugin_id_idx",
1134 "columns": [1041 "columns": ["plugin_id"],
1135 "plugin_id"
1136 ],
1137 "isUnique": false1042 "isUnique": false
1138 }1043 }
1139 },1044 },
1140 "foreignKeys": {},1045 "foreignKeys": {},
1141 "compositePrimaryKeys": {1046 "compositePrimaryKeys": {
1142 "source_scope_id_id_pk": {1047 "source_scope_id_id_pk": {
1143 "columns": [1048 "columns": ["scope_id", "id"],
1144 "scope_id",
1145 "id"
1146 ],
1147 "name": "source_scope_id_id_pk"1049 "name": "source_scope_id_id_pk"
1148 }1050 }
1149 },1051 },
@@ -1227,33 +1129,24 @@ · 77 unmodified lines
1227 "indexes": {1129 "indexes": {
1228 "tool_scope_id_idx": {1130 "tool_scope_id_idx": {
1229 "name": "tool_scope_id_idx",1131 "name": "tool_scope_id_idx",
1230 "columns": [1132 "columns": ["scope_id"],
1231 "scope_id"
1232 ],
1233 "isUnique": false1133 "isUnique": false
1234 },1134 },
1235 "tool_source_id_idx": {1135 "tool_source_id_idx": {
1236 "name": "tool_source_id_idx",1136 "name": "tool_source_id_idx",
1237 "columns": [1137 "columns": ["source_id"],
1238 "source_id"
1239 ],
1240 "isUnique": false1138 "isUnique": false
1241 },1139 },
1242 "tool_plugin_id_idx": {1140 "tool_plugin_id_idx": {
1243 "name": "tool_plugin_id_idx",1141 "name": "tool_plugin_id_idx",
1244 "columns": [1142 "columns": ["plugin_id"],
1245 "plugin_id"
1246 ],
1247 "isUnique": false1143 "isUnique": false
1248 }1144 }
1249 },1145 },
1250 "foreignKeys": {},1146 "foreignKeys": {},
1251 "compositePrimaryKeys": {1147 "compositePrimaryKeys": {
1252 "tool_scope_id_id_pk": {1148 "tool_scope_id_id_pk": {
1253 "columns": [1149 "columns": ["scope_id", "id"],
1254 "scope_id",
1255 "id"
1256 ],
1257 "name": "tool_scope_id_id_pk"1150 "name": "tool_scope_id_id_pk"
1258 }1151 }
1259 },1152 },
@@ -1271,4 +1164,4 @@ · 11 unmodified lines
1271 "internal": {1164 "internal": {
1272 "indexes": {}1165 "indexes": {}
1273 }1166 }
1274}1167}
apps/local/drizzle/meta/0003_snapshot.json+49−164
@@ -88,26 +88,19 @@ · 87 unmodified lines
88 "indexes": {88 "indexes": {
89 "connection_scope_id_idx": {89 "connection_scope_id_idx": {
90 "name": "connection_scope_id_idx",90 "name": "connection_scope_id_idx",
91 "columns": [91 "columns": ["scope_id"],
92 "scope_id"
93 ],
94 "isUnique": false92 "isUnique": false
95 },93 },
96 "connection_provider_idx": {94 "connection_provider_idx": {
97 "name": "connection_provider_idx",95 "name": "connection_provider_idx",
98 "columns": [96 "columns": ["provider"],
99 "provider"
100 ],
101 "isUnique": false97 "isUnique": false
102 }98 }
103 },99 },
104 "foreignKeys": {},100 "foreignKeys": {},
105 "compositePrimaryKeys": {101 "compositePrimaryKeys": {
106 "connection_scope_id_id_pk": {102 "connection_scope_id_id_pk": {
107 "columns": [103 "columns": ["scope_id", "id"],
108 "scope_id",
109 "id"
110 ],
111 "name": "connection_scope_id_id_pk"104 "name": "connection_scope_id_id_pk"
112 }105 }
113 },106 },
@@ -170,33 +163,24 @@ · 56 unmodified lines
170 "indexes": {163 "indexes": {
171 "definition_scope_id_idx": {164 "definition_scope_id_idx": {
172 "name": "definition_scope_id_idx",165 "name": "definition_scope_id_idx",
173 "columns": [166 "columns": ["scope_id"],
174 "scope_id"
175 ],
176 "isUnique": false167 "isUnique": false
177 },168 },
178 "definition_source_id_idx": {169 "definition_source_id_idx": {
179 "name": "definition_source_id_idx",170 "name": "definition_source_id_idx",
180 "columns": [171 "columns": ["source_id"],
181 "source_id"
182 ],
183 "isUnique": false172 "isUnique": false
184 },173 },
185 "definition_plugin_id_idx": {174 "definition_plugin_id_idx": {
186 "name": "definition_plugin_id_idx",175 "name": "definition_plugin_id_idx",
187 "columns": [176 "columns": ["plugin_id"],
188 "plugin_id"
189 ],
190 "isUnique": false177 "isUnique": false
191 }178 }
192 },179 },
193 "foreignKeys": {},180 "foreignKeys": {},
194 "compositePrimaryKeys": {181 "compositePrimaryKeys": {
195 "definition_scope_id_id_pk": {182 "definition_scope_id_id_pk": {
196 "columns": [183 "columns": ["scope_id", "id"],
197 "scope_id",
198 "id"
199 ],
200 "name": "definition_scope_id_id_pk"184 "name": "definition_scope_id_id_pk"
201 }185 }
202 },186 },
@@ -245,26 +229,19 @@ · 42 unmodified lines
245 "indexes": {229 "indexes": {
246 "google_discovery_binding_scope_id_idx": {230 "google_discovery_binding_scope_id_idx": {
247 "name": "google_discovery_binding_scope_id_idx",231 "name": "google_discovery_binding_scope_id_idx",
248 "columns": [232 "columns": ["scope_id"],
249 "scope_id"
250 ],
251 "isUnique": false233 "isUnique": false
252 },234 },
253 "google_discovery_binding_source_id_idx": {235 "google_discovery_binding_source_id_idx": {
254 "name": "google_discovery_binding_source_id_idx",236 "name": "google_discovery_binding_source_id_idx",
255 "columns": [237 "columns": ["source_id"],
256 "source_id"
257 ],
258 "isUnique": false238 "isUnique": false
259 }239 }
260 },240 },
261 "foreignKeys": {},241 "foreignKeys": {},
262 "compositePrimaryKeys": {242 "compositePrimaryKeys": {
263 "google_discovery_binding_scope_id_id_pk": {243 "google_discovery_binding_scope_id_id_pk": {
264 "columns": [244 "columns": ["scope_id", "id"],
265 "scope_id",
266 "id"
267 ],
268 "name": "google_discovery_binding_scope_id_id_pk"245 "name": "google_discovery_binding_scope_id_id_pk"
269 }246 }
270 },247 },
@@ -306,19 +283,14 @@ · 35 unmodified lines
306 "indexes": {283 "indexes": {
307 "google_discovery_oauth_session_scope_id_idx": {284 "google_discovery_oauth_session_scope_id_idx": {
308 "name": "google_discovery_oauth_session_scope_id_idx",285 "name": "google_discovery_oauth_session_scope_id_idx",
309 "columns": [286 "columns": ["scope_id"],
310 "scope_id"
311 ],
312 "isUnique": false287 "isUnique": false
313 }288 }
314 },289 },
315 "foreignKeys": {},290 "foreignKeys": {},
316 "compositePrimaryKeys": {291 "compositePrimaryKeys": {
317 "google_discovery_oauth_session_scope_id_id_pk": {292 "google_discovery_oauth_session_scope_id_id_pk": {
318 "columns": [293 "columns": ["scope_id", "id"],
319 "scope_id",
320 "id"
321 ],
322 "name": "google_discovery_oauth_session_scope_id_id_pk"294 "name": "google_discovery_oauth_session_scope_id_id_pk"
323 }295 }
324 },296 },
@@ -374,19 +346,14 @@ · 49 unmodified lines
374 "indexes": {346 "indexes": {
375 "google_discovery_source_scope_id_idx": {347 "google_discovery_source_scope_id_idx": {
376 "name": "google_discovery_source_scope_id_idx",348 "name": "google_discovery_source_scope_id_idx",
377 "columns": [349 "columns": ["scope_id"],
378 "scope_id"
379 ],
380 "isUnique": false350 "isUnique": false
381 }351 }
382 },352 },
383 "foreignKeys": {},353 "foreignKeys": {},
384 "compositePrimaryKeys": {354 "compositePrimaryKeys": {
385 "google_discovery_source_scope_id_id_pk": {355 "google_discovery_source_scope_id_id_pk": {
386 "columns": [356 "columns": ["scope_id", "id"],
387 "scope_id",
388 "id"
389 ],
390 "name": "google_discovery_source_scope_id_id_pk"357 "name": "google_discovery_source_scope_id_id_pk"
391 }358 }
392 },359 },
@@ -428,26 +395,19 @@ · 35 unmodified lines
428 "indexes": {395 "indexes": {
429 "graphql_operation_scope_id_idx": {396 "graphql_operation_scope_id_idx": {
430 "name": "graphql_operation_scope_id_idx",397 "name": "graphql_operation_scope_id_idx",
431 "columns": [398 "columns": ["scope_id"],
432 "scope_id"
433 ],
434 "isUnique": false399 "isUnique": false
435 },400 },
436 "graphql_operation_source_id_idx": {401 "graphql_operation_source_id_idx": {
437 "name": "graphql_operation_source_id_idx",402 "name": "graphql_operation_source_id_idx",
438 "columns": [403 "columns": ["source_id"],
439 "source_id"
440 ],
441 "isUnique": false404 "isUnique": false
442 }405 }
443 },406 },
444 "foreignKeys": {},407 "foreignKeys": {},
445 "compositePrimaryKeys": {408 "compositePrimaryKeys": {
446 "graphql_operation_scope_id_id_pk": {409 "graphql_operation_scope_id_id_pk": {
447 "columns": [410 "columns": ["scope_id", "id"],
448 "scope_id",
449 "id"
450 ],
451 "name": "graphql_operation_scope_id_id_pk"411 "name": "graphql_operation_scope_id_id_pk"
452 }412 }
453 },413 },
@@ -496,19 +456,14 @@ · 42 unmodified lines
496 "indexes": {456 "indexes": {
497 "graphql_source_scope_id_idx": {457 "graphql_source_scope_id_idx": {
498 "name": "graphql_source_scope_id_idx",458 "name": "graphql_source_scope_id_idx",
499 "columns": [459 "columns": ["scope_id"],
500 "scope_id"
501 ],
502 "isUnique": false460 "isUnique": false
503 }461 }
504 },462 },
505 "foreignKeys": {},463 "foreignKeys": {},
506 "compositePrimaryKeys": {464 "compositePrimaryKeys": {
507 "graphql_source_scope_id_id_pk": {465 "graphql_source_scope_id_id_pk": {
508 "columns": [466 "columns": ["scope_id", "id"],
509 "scope_id",
510 "id"
511 ],
512 "name": "graphql_source_scope_id_id_pk"467 "name": "graphql_source_scope_id_id_pk"
513 }468 }
514 },469 },
@@ -557,26 +512,19 @@ · 42 unmodified lines
557 "indexes": {512 "indexes": {
558 "mcp_binding_scope_id_idx": {513 "mcp_binding_scope_id_idx": {
559 "name": "mcp_binding_scope_id_idx",514 "name": "mcp_binding_scope_id_idx",
560 "columns": [515 "columns": ["scope_id"],
561 "scope_id"
562 ],
563 "isUnique": false516 "isUnique": false
564 },517 },
565 "mcp_binding_source_id_idx": {518 "mcp_binding_source_id_idx": {
566 "name": "mcp_binding_source_id_idx",519 "name": "mcp_binding_source_id_idx",
567 "columns": [520 "columns": ["source_id"],
568 "source_id"
569 ],
570 "isUnique": false521 "isUnique": false
571 }522 }
572 },523 },
573 "foreignKeys": {},524 "foreignKeys": {},
574 "compositePrimaryKeys": {525 "compositePrimaryKeys": {
575 "mcp_binding_scope_id_id_pk": {526 "mcp_binding_scope_id_id_pk": {
576 "columns": [527 "columns": ["scope_id", "id"],
577 "scope_id",
578 "id"
579 ],
580 "name": "mcp_binding_scope_id_id_pk"528 "name": "mcp_binding_scope_id_id_pk"
581 }529 }
582 },530 },
@@ -625,19 +573,14 @@ · 42 unmodified lines
625 "indexes": {573 "indexes": {
626 "mcp_oauth_session_scope_id_idx": {574 "mcp_oauth_session_scope_id_idx": {
627 "name": "mcp_oauth_session_scope_id_idx",575 "name": "mcp_oauth_session_scope_id_idx",
628 "columns": [576 "columns": ["scope_id"],
629 "scope_id"
630 ],
631 "isUnique": false577 "isUnique": false
632 }578 }
633 },579 },
634 "foreignKeys": {},580 "foreignKeys": {},
635 "compositePrimaryKeys": {581 "compositePrimaryKeys": {
636 "mcp_oauth_session_scope_id_id_pk": {582 "mcp_oauth_session_scope_id_id_pk": {
637 "columns": [583 "columns": ["scope_id", "id"],
638 "scope_id",
639 "id"
640 ],
641 "name": "mcp_oauth_session_scope_id_id_pk"584 "name": "mcp_oauth_session_scope_id_id_pk"
642 }585 }
643 },586 },
@@ -686,19 +629,14 @@ · 42 unmodified lines
686 "indexes": {629 "indexes": {
687 "mcp_source_scope_id_idx": {630 "mcp_source_scope_id_idx": {
688 "name": "mcp_source_scope_id_idx",631 "name": "mcp_source_scope_id_idx",
689 "columns": [632 "columns": ["scope_id"],
690 "scope_id"
691 ],
692 "isUnique": false633 "isUnique": false
693 }634 }
694 },635 },
695 "foreignKeys": {},636 "foreignKeys": {},
696 "compositePrimaryKeys": {637 "compositePrimaryKeys": {
697 "mcp_source_scope_id_id_pk": {638 "mcp_source_scope_id_id_pk": {
698 "columns": [639 "columns": ["scope_id", "id"],
699 "scope_id",
700 "id"
701 ],
702 "name": "mcp_source_scope_id_id_pk"640 "name": "mcp_source_scope_id_id_pk"
703 }641 }
704 },642 },
@@ -740,19 +678,14 @@ · 35 unmodified lines
740 "indexes": {678 "indexes": {
741 "openapi_oauth_session_scope_id_idx": {679 "openapi_oauth_session_scope_id_idx": {
742 "name": "openapi_oauth_session_scope_id_idx",680 "name": "openapi_oauth_session_scope_id_idx",
743 "columns": [681 "columns": ["scope_id"],
744 "scope_id"
745 ],
746 "isUnique": false682 "isUnique": false
747 }683 }
748 },684 },
749 "foreignKeys": {},685 "foreignKeys": {},
750 "compositePrimaryKeys": {686 "compositePrimaryKeys": {
751 "openapi_oauth_session_scope_id_id_pk": {687 "openapi_oauth_session_scope_id_id_pk": {
752 "columns": [688 "columns": ["scope_id", "id"],
753 "scope_id",
754 "id"
755 ],
756 "name": "openapi_oauth_session_scope_id_id_pk"689 "name": "openapi_oauth_session_scope_id_id_pk"
757 }690 }
758 },691 },
@@ -794,26 +727,19 @@ · 35 unmodified lines
794 "indexes": {727 "indexes": {
795 "openapi_operation_scope_id_idx": {728 "openapi_operation_scope_id_idx": {
796 "name": "openapi_operation_scope_id_idx",729 "name": "openapi_operation_scope_id_idx",
797 "columns": [730 "columns": ["scope_id"],
798 "scope_id"
799 ],
800 "isUnique": false731 "isUnique": false
801 },732 },
802 "openapi_operation_source_id_idx": {733 "openapi_operation_source_id_idx": {
803 "name": "openapi_operation_source_id_idx",734 "name": "openapi_operation_source_id_idx",
804 "columns": [735 "columns": ["source_id"],
805 "source_id"
806 ],
807 "isUnique": false736 "isUnique": false
808 }737 }
809 },738 },
810 "foreignKeys": {},739 "foreignKeys": {},
811 "compositePrimaryKeys": {740 "compositePrimaryKeys": {
812 "openapi_operation_scope_id_id_pk": {741 "openapi_operation_scope_id_id_pk": {
813 "columns": [742 "columns": ["scope_id", "id"],
814 "scope_id",
815 "id"
816 ],
817 "name": "openapi_operation_scope_id_id_pk"743 "name": "openapi_operation_scope_id_id_pk"
818 }744 }
819 },745 },
@@ -890,19 +816,14 @@ · 70 unmodified lines
890 "indexes": {816 "indexes": {
891 "openapi_source_scope_id_idx": {817 "openapi_source_scope_id_idx": {
892 "name": "openapi_source_scope_id_idx",818 "name": "openapi_source_scope_id_idx",
893 "columns": [819 "columns": ["scope_id"],
894 "scope_id"
895 ],
896 "isUnique": false820 "isUnique": false
897 }821 }
898 },822 },
899 "foreignKeys": {},823 "foreignKeys": {},
900 "compositePrimaryKeys": {824 "compositePrimaryKeys": {
901 "openapi_source_scope_id_id_pk": {825 "openapi_source_scope_id_id_pk": {
902 "columns": [826 "columns": ["scope_id", "id"],
903 "scope_id",
904 "id"
905 ],
906 "name": "openapi_source_scope_id_id_pk"827 "name": "openapi_source_scope_id_id_pk"
907 }828 }
908 },829 },
@@ -972,30 +893,22 @@ · 63 unmodified lines
972 "indexes": {893 "indexes": {
973 "openapi_source_binding_source_id_idx": {894 "openapi_source_binding_source_id_idx": {
974 "name": "openapi_source_binding_source_id_idx",895 "name": "openapi_source_binding_source_id_idx",
975 "columns": [896 "columns": ["source_id"],
976 "source_id"
977 ],
978 "isUnique": false897 "isUnique": false
979 },898 },
980 "openapi_source_binding_source_scope_id_idx": {899 "openapi_source_binding_source_scope_id_idx": {
981 "name": "openapi_source_binding_source_scope_id_idx",900 "name": "openapi_source_binding_source_scope_id_idx",
982 "columns": [901 "columns": ["source_scope_id"],
983 "source_scope_id"
984 ],
985 "isUnique": false902 "isUnique": false
986 },903 },
987 "openapi_source_binding_target_scope_id_idx": {904 "openapi_source_binding_target_scope_id_idx": {
988 "name": "openapi_source_binding_target_scope_id_idx",905 "name": "openapi_source_binding_target_scope_id_idx",
989 "columns": [906 "columns": ["target_scope_id"],
990 "target_scope_id"
991 ],
992 "isUnique": false907 "isUnique": false
993 },908 },
994 "openapi_source_binding_slot_idx": {909 "openapi_source_binding_slot_idx": {
995 "name": "openapi_source_binding_slot_idx",910 "name": "openapi_source_binding_slot_idx",
996 "columns": [911 "columns": ["slot"],
997 "slot"
998 ],
999 "isUnique": false912 "isUnique": false
1000 }913 }
1001 },914 },
@@ -1053,33 +966,24 @@ · 51 unmodified lines
1053 "indexes": {966 "indexes": {
1054 "secret_scope_id_idx": {967 "secret_scope_id_idx": {
1055 "name": "secret_scope_id_idx",968 "name": "secret_scope_id_idx",
1056 "columns": [969 "columns": ["scope_id"],
1057 "scope_id"
1058 ],
1059 "isUnique": false970 "isUnique": false
1060 },971 },
1061 "secret_provider_idx": {972 "secret_provider_idx": {
1062 "name": "secret_provider_idx",973 "name": "secret_provider_idx",
1063 "columns": [974 "columns": ["provider"],
1064 "provider"
1065 ],
1066 "isUnique": false975 "isUnique": false
1067 },976 },
1068 "secret_owned_by_connection_id_idx": {977 "secret_owned_by_connection_id_idx": {
1069 "name": "secret_owned_by_connection_id_idx",978 "name": "secret_owned_by_connection_id_idx",
1070 "columns": [979 "columns": ["owned_by_connection_id"],
1071 "owned_by_connection_id"
1072 ],
1073 "isUnique": false980 "isUnique": false
1074 }981 }
1075 },982 },
1076 "foreignKeys": {},983 "foreignKeys": {},
1077 "compositePrimaryKeys": {984 "compositePrimaryKeys": {
1078 "secret_scope_id_id_pk": {985 "secret_scope_id_id_pk": {
1079 "columns": [986 "columns": ["scope_id", "id"],
1080 "scope_id",
1081 "id"
1082 ],
1083 "name": "secret_scope_id_id_pk"987 "name": "secret_scope_id_id_pk"
1084 }988 }
1085 },989 },
@@ -1173,26 +1077,19 @@ · 87 unmodified lines
1173 "indexes": {1077 "indexes": {
1174 "source_scope_id_idx": {1078 "source_scope_id_idx": {
1175 "name": "source_scope_id_idx",1079 "name": "source_scope_id_idx",
1176 "columns": [1080 "columns": ["scope_id"],
1177 "scope_id"
1178 ],
1179 "isUnique": false1081 "isUnique": false
1180 },1082 },
1181 "source_plugin_id_idx": {1083 "source_plugin_id_idx": {
1182 "name": "source_plugin_id_idx",1084 "name": "source_plugin_id_idx",
1183 "columns": [1085 "columns": ["plugin_id"],
1184 "plugin_id"
1185 ],
1186 "isUnique": false1086 "isUnique": false
1187 }1087 }
1188 },1088 },
1189 "foreignKeys": {},1089 "foreignKeys": {},
1190 "compositePrimaryKeys": {1090 "compositePrimaryKeys": {
1191 "source_scope_id_id_pk": {1091 "source_scope_id_id_pk": {
1192 "columns": [1092 "columns": ["scope_id", "id"],
1193 "scope_id",
1194 "id"
1195 ],
1196 "name": "source_scope_id_id_pk"1093 "name": "source_scope_id_id_pk"
1197 }1094 }
1198 },1095 },
@@ -1276,33 +1173,24 @@ · 77 unmodified lines
1276 "indexes": {1173 "indexes": {
1277 "tool_scope_id_idx": {1174 "tool_scope_id_idx": {
1278 "name": "tool_scope_id_idx",1175 "name": "tool_scope_id_idx",
1279 "columns": [1176 "columns": ["scope_id"],
1280 "scope_id"
1281 ],
1282 "isUnique": false1177 "isUnique": false
1283 },1178 },
1284 "tool_source_id_idx": {1179 "tool_source_id_idx": {
1285 "name": "tool_source_id_idx",1180 "name": "tool_source_id_idx",
1286 "columns": [1181 "columns": ["source_id"],
1287 "source_id"
1288 ],
1289 "isUnique": false1182 "isUnique": false
1290 },1183 },
1291 "tool_plugin_id_idx": {1184 "tool_plugin_id_idx": {
1292 "name": "tool_plugin_id_idx",1185 "name": "tool_plugin_id_idx",
1293 "columns": [1186 "columns": ["plugin_id"],
1294 "plugin_id"
1295 ],
1296 "isUnique": false1187 "isUnique": false
1297 }1188 }
1298 },1189 },
1299 "foreignKeys": {},1190 "foreignKeys": {},
1300 "compositePrimaryKeys": {1191 "compositePrimaryKeys": {
1301 "tool_scope_id_id_pk": {1192 "tool_scope_id_id_pk": {
1302 "columns": [1193 "columns": ["scope_id", "id"],
1303 "scope_id",
1304 "id"
1305 ],
1306 "name": "tool_scope_id_id_pk"1194 "name": "tool_scope_id_id_pk"
1307 }1195 }
1308 },1196 },
@@ -1338,10 +1226,7 @@ · 29 unmodified lines
1338 "foreignKeys": {},1226 "foreignKeys": {},
1339 "compositePrimaryKeys": {1227 "compositePrimaryKeys": {
1340 "blob_namespace_key_pk": {1228 "blob_namespace_key_pk": {
1341 "columns": [1229 "columns": ["namespace", "key"],
1342 "namespace",
1343 "key"
1344 ],
1345 "name": "blob_namespace_key_pk"1230 "name": "blob_namespace_key_pk"
1346 }1231 }
1347 },1232 },
apps/local/drizzle/meta/0004_snapshot.json+51−172
@@ -88,26 +88,19 @@ · 87 unmodified lines
88 "indexes": {88 "indexes": {
89 "connection_scope_id_idx": {89 "connection_scope_id_idx": {
90 "name": "connection_scope_id_idx",90 "name": "connection_scope_id_idx",
91 "columns": [91 "columns": ["scope_id"],
92 "scope_id"
93 ],
94 "isUnique": false92 "isUnique": false
95 },93 },
96 "connection_provider_idx": {94 "connection_provider_idx": {
97 "name": "connection_provider_idx",95 "name": "connection_provider_idx",
98 "columns": [96 "columns": ["provider"],
99 "provider"
100 ],
101 "isUnique": false97 "isUnique": false
102 }98 }
103 },99 },
104 "foreignKeys": {},100 "foreignKeys": {},
105 "compositePrimaryKeys": {101 "compositePrimaryKeys": {
106 "connection_scope_id_id_pk": {102 "connection_scope_id_id_pk": {
107 "columns": [103 "columns": ["scope_id", "id"],
108 "scope_id",
109 "id"
110 ],
111 "name": "connection_scope_id_id_pk"104 "name": "connection_scope_id_id_pk"
112 }105 }
113 },106 },
@@ -170,33 +163,24 @@ · 56 unmodified lines
170 "indexes": {163 "indexes": {
171 "definition_scope_id_idx": {164 "definition_scope_id_idx": {
172 "name": "definition_scope_id_idx",165 "name": "definition_scope_id_idx",
173 "columns": [166 "columns": ["scope_id"],
174 "scope_id"
175 ],
176 "isUnique": false167 "isUnique": false
177 },168 },
178 "definition_source_id_idx": {169 "definition_source_id_idx": {
179 "name": "definition_source_id_idx",170 "name": "definition_source_id_idx",
180 "columns": [171 "columns": ["source_id"],
181 "source_id"
182 ],
183 "isUnique": false172 "isUnique": false
184 },173 },
185 "definition_plugin_id_idx": {174 "definition_plugin_id_idx": {
186 "name": "definition_plugin_id_idx",175 "name": "definition_plugin_id_idx",
187 "columns": [176 "columns": ["plugin_id"],
188 "plugin_id"
189 ],
190 "isUnique": false177 "isUnique": false
191 }178 }
192 },179 },
193 "foreignKeys": {},180 "foreignKeys": {},
194 "compositePrimaryKeys": {181 "compositePrimaryKeys": {
195 "definition_scope_id_id_pk": {182 "definition_scope_id_id_pk": {
196 "columns": [183 "columns": ["scope_id", "id"],
197 "scope_id",
198 "id"
199 ],
200 "name": "definition_scope_id_id_pk"184 "name": "definition_scope_id_id_pk"
201 }185 }
202 },186 },
@@ -245,26 +229,19 @@ · 42 unmodified lines
245 "indexes": {229 "indexes": {
246 "google_discovery_binding_scope_id_idx": {230 "google_discovery_binding_scope_id_idx": {
247 "name": "google_discovery_binding_scope_id_idx",231 "name": "google_discovery_binding_scope_id_idx",
248 "columns": [232 "columns": ["scope_id"],
249 "scope_id"
250 ],
251 "isUnique": false233 "isUnique": false
252 },234 },
253 "google_discovery_binding_source_id_idx": {235 "google_discovery_binding_source_id_idx": {
254 "name": "google_discovery_binding_source_id_idx",236 "name": "google_discovery_binding_source_id_idx",
255 "columns": [237 "columns": ["source_id"],
256 "source_id"
257 ],
258 "isUnique": false238 "isUnique": false
259 }239 }
260 },240 },
261 "foreignKeys": {},241 "foreignKeys": {},
262 "compositePrimaryKeys": {242 "compositePrimaryKeys": {
263 "google_discovery_binding_scope_id_id_pk": {243 "google_discovery_binding_scope_id_id_pk": {
264 "columns": [244 "columns": ["scope_id", "id"],
265 "scope_id",
266 "id"
267 ],
268 "name": "google_discovery_binding_scope_id_id_pk"245 "name": "google_discovery_binding_scope_id_id_pk"
269 }246 }
270 },247 },
@@ -306,19 +283,14 @@ · 35 unmodified lines
306 "indexes": {283 "indexes": {
307 "google_discovery_oauth_session_scope_id_idx": {284 "google_discovery_oauth_session_scope_id_idx": {
308 "name": "google_discovery_oauth_session_scope_id_idx",285 "name": "google_discovery_oauth_session_scope_id_idx",
309 "columns": [286 "columns": ["scope_id"],
310 "scope_id"
311 ],
312 "isUnique": false287 "isUnique": false
313 }288 }
314 },289 },
315 "foreignKeys": {},290 "foreignKeys": {},
316 "compositePrimaryKeys": {291 "compositePrimaryKeys": {
317 "google_discovery_oauth_session_scope_id_id_pk": {292 "google_discovery_oauth_session_scope_id_id_pk": {
318 "columns": [293 "columns": ["scope_id", "id"],
319 "scope_id",
320 "id"
321 ],
322 "name": "google_discovery_oauth_session_scope_id_id_pk"294 "name": "google_discovery_oauth_session_scope_id_id_pk"
323 }295 }
324 },296 },
@@ -374,19 +346,14 @@ · 49 unmodified lines
374 "indexes": {346 "indexes": {
375 "google_discovery_source_scope_id_idx": {347 "google_discovery_source_scope_id_idx": {
376 "name": "google_discovery_source_scope_id_idx",348 "name": "google_discovery_source_scope_id_idx",
377 "columns": [349 "columns": ["scope_id"],
378 "scope_id"
379 ],
380 "isUnique": false350 "isUnique": false
381 }351 }
382 },352 },
383 "foreignKeys": {},353 "foreignKeys": {},
384 "compositePrimaryKeys": {354 "compositePrimaryKeys": {
385 "google_discovery_source_scope_id_id_pk": {355 "google_discovery_source_scope_id_id_pk": {
386 "columns": [356 "columns": ["scope_id", "id"],
387 "scope_id",
388 "id"
389 ],
390 "name": "google_discovery_source_scope_id_id_pk"357 "name": "google_discovery_source_scope_id_id_pk"
391 }358 }
392 },359 },
@@ -428,26 +395,19 @@ · 35 unmodified lines
428 "indexes": {395 "indexes": {
429 "graphql_operation_scope_id_idx": {396 "graphql_operation_scope_id_idx": {
430 "name": "graphql_operation_scope_id_idx",397 "name": "graphql_operation_scope_id_idx",
431 "columns": [398 "columns": ["scope_id"],
432 "scope_id"
433 ],
434 "isUnique": false399 "isUnique": false
435 },400 },
436 "graphql_operation_source_id_idx": {401 "graphql_operation_source_id_idx": {
437 "name": "graphql_operation_source_id_idx",402 "name": "graphql_operation_source_id_idx",
438 "columns": [403 "columns": ["source_id"],
439 "source_id"
440 ],
441 "isUnique": false404 "isUnique": false
442 }405 }
443 },406 },
444 "foreignKeys": {},407 "foreignKeys": {},
445 "compositePrimaryKeys": {408 "compositePrimaryKeys": {
446 "graphql_operation_scope_id_id_pk": {409 "graphql_operation_scope_id_id_pk": {
447 "columns": [410 "columns": ["scope_id", "id"],
448 "scope_id",
449 "id"
450 ],
451 "name": "graphql_operation_scope_id_id_pk"411 "name": "graphql_operation_scope_id_id_pk"
452 }412 }
453 },413 },
@@ -496,19 +456,14 @@ · 42 unmodified lines
496 "indexes": {456 "indexes": {
497 "graphql_source_scope_id_idx": {457 "graphql_source_scope_id_idx": {
498 "name": "graphql_source_scope_id_idx",458 "name": "graphql_source_scope_id_idx",
499 "columns": [459 "columns": ["scope_id"],
500 "scope_id"
501 ],
502 "isUnique": false460 "isUnique": false
503 }461 }
504 },462 },
505 "foreignKeys": {},463 "foreignKeys": {},
506 "compositePrimaryKeys": {464 "compositePrimaryKeys": {
507 "graphql_source_scope_id_id_pk": {465 "graphql_source_scope_id_id_pk": {
508 "columns": [466 "columns": ["scope_id", "id"],
509 "scope_id",
510 "id"
511 ],
512 "name": "graphql_source_scope_id_id_pk"467 "name": "graphql_source_scope_id_id_pk"
513 }468 }
514 },469 },
@@ -557,26 +512,19 @@ · 42 unmodified lines
557 "indexes": {512 "indexes": {
558 "mcp_binding_scope_id_idx": {513 "mcp_binding_scope_id_idx": {
559 "name": "mcp_binding_scope_id_idx",514 "name": "mcp_binding_scope_id_idx",
560 "columns": [515 "columns": ["scope_id"],
561 "scope_id"
562 ],
563 "isUnique": false516 "isUnique": false
564 },517 },
565 "mcp_binding_source_id_idx": {518 "mcp_binding_source_id_idx": {
566 "name": "mcp_binding_source_id_idx",519 "name": "mcp_binding_source_id_idx",
567 "columns": [520 "columns": ["source_id"],
568 "source_id"
569 ],
570 "isUnique": false521 "isUnique": false
571 }522 }
572 },523 },
573 "foreignKeys": {},524 "foreignKeys": {},
574 "compositePrimaryKeys": {525 "compositePrimaryKeys": {
575 "mcp_binding_scope_id_id_pk": {526 "mcp_binding_scope_id_id_pk": {
576 "columns": [527 "columns": ["scope_id", "id"],
577 "scope_id",
578 "id"
579 ],
580 "name": "mcp_binding_scope_id_id_pk"528 "name": "mcp_binding_scope_id_id_pk"
581 }529 }
582 },530 },
@@ -625,19 +573,14 @@ · 42 unmodified lines
625 "indexes": {573 "indexes": {
626 "mcp_oauth_session_scope_id_idx": {574 "mcp_oauth_session_scope_id_idx": {
627 "name": "mcp_oauth_session_scope_id_idx",575 "name": "mcp_oauth_session_scope_id_idx",
628 "columns": [576 "columns": ["scope_id"],
629 "scope_id"
630 ],
631 "isUnique": false577 "isUnique": false
632 }578 }
633 },579 },
634 "foreignKeys": {},580 "foreignKeys": {},
635 "compositePrimaryKeys": {581 "compositePrimaryKeys": {
636 "mcp_oauth_session_scope_id_id_pk": {582 "mcp_oauth_session_scope_id_id_pk": {
637 "columns": [583 "columns": ["scope_id", "id"],
638 "scope_id",
639 "id"
640 ],
641 "name": "mcp_oauth_session_scope_id_id_pk"584 "name": "mcp_oauth_session_scope_id_id_pk"
642 }585 }
643 },586 },
@@ -686,19 +629,14 @@ · 42 unmodified lines
686 "indexes": {629 "indexes": {
687 "mcp_source_scope_id_idx": {630 "mcp_source_scope_id_idx": {
688 "name": "mcp_source_scope_id_idx",631 "name": "mcp_source_scope_id_idx",
689 "columns": [632 "columns": ["scope_id"],
690 "scope_id"
691 ],
692 "isUnique": false633 "isUnique": false
693 }634 }
694 },635 },
695 "foreignKeys": {},636 "foreignKeys": {},
696 "compositePrimaryKeys": {637 "compositePrimaryKeys": {
697 "mcp_source_scope_id_id_pk": {638 "mcp_source_scope_id_id_pk": {
698 "columns": [639 "columns": ["scope_id", "id"],
699 "scope_id",
700 "id"
701 ],
702 "name": "mcp_source_scope_id_id_pk"640 "name": "mcp_source_scope_id_id_pk"
703 }641 }
704 },642 },
@@ -740,19 +678,14 @@ · 35 unmodified lines
740 "indexes": {678 "indexes": {
741 "openapi_oauth_session_scope_id_idx": {679 "openapi_oauth_session_scope_id_idx": {
742 "name": "openapi_oauth_session_scope_id_idx",680 "name": "openapi_oauth_session_scope_id_idx",
743 "columns": [681 "columns": ["scope_id"],
744 "scope_id"
745 ],
746 "isUnique": false682 "isUnique": false
747 }683 }
748 },684 },
749 "foreignKeys": {},685 "foreignKeys": {},
750 "compositePrimaryKeys": {686 "compositePrimaryKeys": {
751 "openapi_oauth_session_scope_id_id_pk": {687 "openapi_oauth_session_scope_id_id_pk": {
752 "columns": [688 "columns": ["scope_id", "id"],
753 "scope_id",
754 "id"
755 ],
756 "name": "openapi_oauth_session_scope_id_id_pk"689 "name": "openapi_oauth_session_scope_id_id_pk"
757 }690 }
758 },691 },
@@ -794,26 +727,19 @@ · 35 unmodified lines
794 "indexes": {727 "indexes": {
795 "openapi_operation_scope_id_idx": {728 "openapi_operation_scope_id_idx": {
796 "name": "openapi_operation_scope_id_idx",729 "name": "openapi_operation_scope_id_idx",
797 "columns": [730 "columns": ["scope_id"],
798 "scope_id"
799 ],
800 "isUnique": false731 "isUnique": false
801 },732 },
802 "openapi_operation_source_id_idx": {733 "openapi_operation_source_id_idx": {
803 "name": "openapi_operation_source_id_idx",734 "name": "openapi_operation_source_id_idx",
804 "columns": [735 "columns": ["source_id"],
805 "source_id"
806 ],
807 "isUnique": false736 "isUnique": false
808 }737 }
809 },738 },
810 "foreignKeys": {},739 "foreignKeys": {},
811 "compositePrimaryKeys": {740 "compositePrimaryKeys": {
812 "openapi_operation_scope_id_id_pk": {741 "openapi_operation_scope_id_id_pk": {
813 "columns": [742 "columns": ["scope_id", "id"],
814 "scope_id",
815 "id"
816 ],
817 "name": "openapi_operation_scope_id_id_pk"743 "name": "openapi_operation_scope_id_id_pk"
818 }744 }
819 },745 },
@@ -890,19 +816,14 @@ · 70 unmodified lines
890 "indexes": {816 "indexes": {
891 "openapi_source_scope_id_idx": {817 "openapi_source_scope_id_idx": {
892 "name": "openapi_source_scope_id_idx",818 "name": "openapi_source_scope_id_idx",
893 "columns": [819 "columns": ["scope_id"],
894 "scope_id"
895 ],
896 "isUnique": false820 "isUnique": false
897 }821 }
898 },822 },
899 "foreignKeys": {},823 "foreignKeys": {},
900 "compositePrimaryKeys": {824 "compositePrimaryKeys": {
901 "openapi_source_scope_id_id_pk": {825 "openapi_source_scope_id_id_pk": {
902 "columns": [826 "columns": ["scope_id", "id"],
903 "scope_id",
904 "id"
905 ],
906 "name": "openapi_source_scope_id_id_pk"827 "name": "openapi_source_scope_id_id_pk"
907 }828 }
908 },829 },
@@ -972,30 +893,22 @@ · 63 unmodified lines
972 "indexes": {893 "indexes": {
973 "openapi_source_binding_source_id_idx": {894 "openapi_source_binding_source_id_idx": {
974 "name": "openapi_source_binding_source_id_idx",895 "name": "openapi_source_binding_source_id_idx",
975 "columns": [896 "columns": ["source_id"],
976 "source_id"
977 ],
978 "isUnique": false897 "isUnique": false
979 },898 },
980 "openapi_source_binding_source_scope_id_idx": {899 "openapi_source_binding_source_scope_id_idx": {
981 "name": "openapi_source_binding_source_scope_id_idx",900 "name": "openapi_source_binding_source_scope_id_idx",
982 "columns": [901 "columns": ["source_scope_id"],
983 "source_scope_id"
984 ],
985 "isUnique": false902 "isUnique": false
986 },903 },
987 "openapi_source_binding_target_scope_id_idx": {904 "openapi_source_binding_target_scope_id_idx": {
988 "name": "openapi_source_binding_target_scope_id_idx",905 "name": "openapi_source_binding_target_scope_id_idx",
989 "columns": [906 "columns": ["target_scope_id"],
990 "target_scope_id"
991 ],
992 "isUnique": false907 "isUnique": false
993 },908 },
994 "openapi_source_binding_slot_idx": {909 "openapi_source_binding_slot_idx": {
995 "name": "openapi_source_binding_slot_idx",910 "name": "openapi_source_binding_slot_idx",
996 "columns": [911 "columns": ["slot"],
997 "slot"
998 ],
999 "isUnique": false912 "isUnique": false
1000 }913 }
1001 },914 },
@@ -1053,33 +966,24 @@ · 51 unmodified lines
1053 "indexes": {966 "indexes": {
1054 "secret_scope_id_idx": {967 "secret_scope_id_idx": {
1055 "name": "secret_scope_id_idx",968 "name": "secret_scope_id_idx",
1056 "columns": [969 "columns": ["scope_id"],
1057 "scope_id"
1058 ],
1059 "isUnique": false970 "isUnique": false
1060 },971 },
1061 "secret_provider_idx": {972 "secret_provider_idx": {
1062 "name": "secret_provider_idx",973 "name": "secret_provider_idx",
1063 "columns": [974 "columns": ["provider"],
1064 "provider"
1065 ],
1066 "isUnique": false975 "isUnique": false
1067 },976 },
1068 "secret_owned_by_connection_id_idx": {977 "secret_owned_by_connection_id_idx": {
1069 "name": "secret_owned_by_connection_id_idx",978 "name": "secret_owned_by_connection_id_idx",
1070 "columns": [979 "columns": ["owned_by_connection_id"],
1071 "owned_by_connection_id"
1072 ],
1073 "isUnique": false980 "isUnique": false
1074 }981 }
1075 },982 },
1076 "foreignKeys": {},983 "foreignKeys": {},
1077 "compositePrimaryKeys": {984 "compositePrimaryKeys": {
1078 "secret_scope_id_id_pk": {985 "secret_scope_id_id_pk": {
1079 "columns": [986 "columns": ["scope_id", "id"],
1080 "scope_id",
1081 "id"
1082 ],
1083 "name": "secret_scope_id_id_pk"987 "name": "secret_scope_id_id_pk"
1084 }988 }
1085 },989 },
@@ -1173,26 +1077,19 @@ · 87 unmodified lines
1173 "indexes": {1077 "indexes": {
1174 "source_scope_id_idx": {1078 "source_scope_id_idx": {
1175 "name": "source_scope_id_idx",1079 "name": "source_scope_id_idx",
1176 "columns": [1080 "columns": ["scope_id"],
1177 "scope_id"
1178 ],
1179 "isUnique": false1081 "isUnique": false
1180 },1082 },
1181 "source_plugin_id_idx": {1083 "source_plugin_id_idx": {
1182 "name": "source_plugin_id_idx",1084 "name": "source_plugin_id_idx",
1183 "columns": [1085 "columns": ["plugin_id"],
1184 "plugin_id"
1185 ],
1186 "isUnique": false1086 "isUnique": false
1187 }1087 }
1188 },1088 },
1189 "foreignKeys": {},1089 "foreignKeys": {},
1190 "compositePrimaryKeys": {1090 "compositePrimaryKeys": {
1191 "source_scope_id_id_pk": {1091 "source_scope_id_id_pk": {
1192 "columns": [1092 "columns": ["scope_id", "id"],
1193 "scope_id",
1194 "id"
1195 ],
1196 "name": "source_scope_id_id_pk"1093 "name": "source_scope_id_id_pk"
1197 }1094 }
1198 },1095 },
@@ -1276,33 +1173,24 @@ · 77 unmodified lines
1276 "indexes": {1173 "indexes": {
1277 "tool_scope_id_idx": {1174 "tool_scope_id_idx": {
1278 "name": "tool_scope_id_idx",1175 "name": "tool_scope_id_idx",
1279 "columns": [1176 "columns": ["scope_id"],
1280 "scope_id"
1281 ],
1282 "isUnique": false1177 "isUnique": false
1283 },1178 },
1284 "tool_source_id_idx": {1179 "tool_source_id_idx": {
1285 "name": "tool_source_id_idx",1180 "name": "tool_source_id_idx",
1286 "columns": [1181 "columns": ["source_id"],
1287 "source_id"
1288 ],
1289 "isUnique": false1182 "isUnique": false
1290 },1183 },
1291 "tool_plugin_id_idx": {1184 "tool_plugin_id_idx": {
1292 "name": "tool_plugin_id_idx",1185 "name": "tool_plugin_id_idx",
1293 "columns": [1186 "columns": ["plugin_id"],
1294 "plugin_id"
1295 ],
1296 "isUnique": false1187 "isUnique": false
1297 }1188 }
1298 },1189 },
1299 "foreignKeys": {},1190 "foreignKeys": {},
1300 "compositePrimaryKeys": {1191 "compositePrimaryKeys": {
1301 "tool_scope_id_id_pk": {1192 "tool_scope_id_id_pk": {
1302 "columns": [1193 "columns": ["scope_id", "id"],
1303 "scope_id",
1304 "id"
1305 ],
1306 "name": "tool_scope_id_id_pk"1194 "name": "tool_scope_id_id_pk"
1307 }1195 }
1308 },1196 },
@@ -1365,20 +1253,14 @@ · 56 unmodified lines
1365 "indexes": {1253 "indexes": {
1366 "tool_policy_scope_id_position_idx": {1254 "tool_policy_scope_id_position_idx": {
1367 "name": "tool_policy_scope_id_position_idx",1255 "name": "tool_policy_scope_id_position_idx",
1368 "columns": [1256 "columns": ["scope_id", "position"],
1369 "scope_id",
1370 "position"
1371 ],
1372 "isUnique": false1257 "isUnique": false
1373 }1258 }
1374 },1259 },
1375 "foreignKeys": {},1260 "foreignKeys": {},
1376 "compositePrimaryKeys": {1261 "compositePrimaryKeys": {
1377 "tool_policy_scope_id_id_pk": {1262 "tool_policy_scope_id_id_pk": {
1378 "columns": [1263 "columns": ["scope_id", "id"],
1379 "scope_id",
1380 "id"
1381 ],
1382 "name": "tool_policy_scope_id_id_pk"1264 "name": "tool_policy_scope_id_id_pk"
1383 }1265 }
1384 },1266 },
@@ -1414,10 +1296,7 @@ · 29 unmodified lines
1414 "foreignKeys": {},1296 "foreignKeys": {},
1415 "compositePrimaryKeys": {1297 "compositePrimaryKeys": {
1416 "blob_namespace_key_pk": {1298 "blob_namespace_key_pk": {
1417 "columns": [1299 "columns": ["namespace", "key"],
1418 "namespace",
1419 "key"
1420 ],
1421 "name": "blob_namespace_key_pk"1300 "name": "blob_namespace_key_pk"
1422 }1301 }
1423 },1302 },
apps/local/drizzle/meta/0005_snapshot.json+51−172
@@ -88,26 +88,19 @@ · 87 unmodified lines
88 "indexes": {88 "indexes": {
89 "connection_scope_id_idx": {89 "connection_scope_id_idx": {
90 "name": "connection_scope_id_idx",90 "name": "connection_scope_id_idx",
91 "columns": [91 "columns": ["scope_id"],
92 "scope_id"
93 ],
94 "isUnique": false92 "isUnique": false
95 },93 },
96 "connection_provider_idx": {94 "connection_provider_idx": {
97 "name": "connection_provider_idx",95 "name": "connection_provider_idx",
98 "columns": [96 "columns": ["provider"],
99 "provider"
100 ],
101 "isUnique": false97 "isUnique": false
102 }98 }
103 },99 },
104 "foreignKeys": {},100 "foreignKeys": {},
105 "compositePrimaryKeys": {101 "compositePrimaryKeys": {
106 "connection_scope_id_id_pk": {102 "connection_scope_id_id_pk": {
107 "columns": [103 "columns": ["scope_id", "id"],
108 "scope_id",
109 "id"
110 ],
111 "name": "connection_scope_id_id_pk"104 "name": "connection_scope_id_id_pk"
112 }105 }
113 },106 },
@@ -170,33 +163,24 @@ · 56 unmodified lines
170 "indexes": {163 "indexes": {
171 "definition_scope_id_idx": {164 "definition_scope_id_idx": {
172 "name": "definition_scope_id_idx",165 "name": "definition_scope_id_idx",
173 "columns": [166 "columns": ["scope_id"],
174 "scope_id"
175 ],
176 "isUnique": false167 "isUnique": false
177 },168 },
178 "definition_source_id_idx": {169 "definition_source_id_idx": {
179 "name": "definition_source_id_idx",170 "name": "definition_source_id_idx",
180 "columns": [171 "columns": ["source_id"],
181 "source_id"
182 ],
183 "isUnique": false172 "isUnique": false
184 },173 },
185 "definition_plugin_id_idx": {174 "definition_plugin_id_idx": {
186 "name": "definition_plugin_id_idx",175 "name": "definition_plugin_id_idx",
187 "columns": [176 "columns": ["plugin_id"],
188 "plugin_id"
189 ],
190 "isUnique": false177 "isUnique": false
191 }178 }
192 },179 },
193 "foreignKeys": {},180 "foreignKeys": {},
194 "compositePrimaryKeys": {181 "compositePrimaryKeys": {
195 "definition_scope_id_id_pk": {182 "definition_scope_id_id_pk": {
196 "columns": [183 "columns": ["scope_id", "id"],
197 "scope_id",
198 "id"
199 ],
200 "name": "definition_scope_id_id_pk"184 "name": "definition_scope_id_id_pk"
201 }185 }
202 },186 },
@@ -245,26 +229,19 @@ · 42 unmodified lines
245 "indexes": {229 "indexes": {
246 "google_discovery_binding_scope_id_idx": {230 "google_discovery_binding_scope_id_idx": {
247 "name": "google_discovery_binding_scope_id_idx",231 "name": "google_discovery_binding_scope_id_idx",
248 "columns": [232 "columns": ["scope_id"],
249 "scope_id"
250 ],
251 "isUnique": false233 "isUnique": false
252 },234 },
253 "google_discovery_binding_source_id_idx": {235 "google_discovery_binding_source_id_idx": {
254 "name": "google_discovery_binding_source_id_idx",236 "name": "google_discovery_binding_source_id_idx",
255 "columns": [237 "columns": ["source_id"],
256 "source_id"
257 ],
258 "isUnique": false238 "isUnique": false
259 }239 }
260 },240 },
261 "foreignKeys": {},241 "foreignKeys": {},
262 "compositePrimaryKeys": {242 "compositePrimaryKeys": {
263 "google_discovery_binding_scope_id_id_pk": {243 "google_discovery_binding_scope_id_id_pk": {
264 "columns": [244 "columns": ["scope_id", "id"],
265 "scope_id",
266 "id"
267 ],
268 "name": "google_discovery_binding_scope_id_id_pk"245 "name": "google_discovery_binding_scope_id_id_pk"
269 }246 }
270 },247 },
@@ -306,19 +283,14 @@ · 35 unmodified lines
306 "indexes": {283 "indexes": {
307 "google_discovery_oauth_session_scope_id_idx": {284 "google_discovery_oauth_session_scope_id_idx": {
308 "name": "google_discovery_oauth_session_scope_id_idx",285 "name": "google_discovery_oauth_session_scope_id_idx",
309 "columns": [286 "columns": ["scope_id"],
310 "scope_id"
311 ],
312 "isUnique": false287 "isUnique": false
313 }288 }
314 },289 },
315 "foreignKeys": {},290 "foreignKeys": {},
316 "compositePrimaryKeys": {291 "compositePrimaryKeys": {
317 "google_discovery_oauth_session_scope_id_id_pk": {292 "google_discovery_oauth_session_scope_id_id_pk": {
318 "columns": [293 "columns": ["scope_id", "id"],
319 "scope_id",
320 "id"
321 ],
322 "name": "google_discovery_oauth_session_scope_id_id_pk"294 "name": "google_discovery_oauth_session_scope_id_id_pk"
323 }295 }
324 },296 },
@@ -374,19 +346,14 @@ · 49 unmodified lines
374 "indexes": {346 "indexes": {
375 "google_discovery_source_scope_id_idx": {347 "google_discovery_source_scope_id_idx": {
376 "name": "google_discovery_source_scope_id_idx",348 "name": "google_discovery_source_scope_id_idx",
377 "columns": [349 "columns": ["scope_id"],
378 "scope_id"
379 ],
380 "isUnique": false350 "isUnique": false
381 }351 }
382 },352 },
383 "foreignKeys": {},353 "foreignKeys": {},
384 "compositePrimaryKeys": {354 "compositePrimaryKeys": {
385 "google_discovery_source_scope_id_id_pk": {355 "google_discovery_source_scope_id_id_pk": {
386 "columns": [356 "columns": ["scope_id", "id"],
387 "scope_id",
388 "id"
389 ],
390 "name": "google_discovery_source_scope_id_id_pk"357 "name": "google_discovery_source_scope_id_id_pk"
391 }358 }
392 },359 },
@@ -428,26 +395,19 @@ · 35 unmodified lines
428 "indexes": {395 "indexes": {
429 "graphql_operation_scope_id_idx": {396 "graphql_operation_scope_id_idx": {
430 "name": "graphql_operation_scope_id_idx",397 "name": "graphql_operation_scope_id_idx",
431 "columns": [398 "columns": ["scope_id"],
432 "scope_id"
433 ],
434 "isUnique": false399 "isUnique": false
435 },400 },
436 "graphql_operation_source_id_idx": {401 "graphql_operation_source_id_idx": {
437 "name": "graphql_operation_source_id_idx",402 "name": "graphql_operation_source_id_idx",
438 "columns": [403 "columns": ["source_id"],
439 "source_id"
440 ],
441 "isUnique": false404 "isUnique": false
442 }405 }
443 },406 },
444 "foreignKeys": {},407 "foreignKeys": {},
445 "compositePrimaryKeys": {408 "compositePrimaryKeys": {
446 "graphql_operation_scope_id_id_pk": {409 "graphql_operation_scope_id_id_pk": {
447 "columns": [410 "columns": ["scope_id", "id"],
448 "scope_id",
449 "id"
450 ],
451 "name": "graphql_operation_scope_id_id_pk"411 "name": "graphql_operation_scope_id_id_pk"
452 }412 }
453 },413 },
@@ -496,19 +456,14 @@ · 42 unmodified lines
496 "indexes": {456 "indexes": {
497 "graphql_source_scope_id_idx": {457 "graphql_source_scope_id_idx": {
498 "name": "graphql_source_scope_id_idx",458 "name": "graphql_source_scope_id_idx",
499 "columns": [459 "columns": ["scope_id"],
500 "scope_id"
501 ],
502 "isUnique": false460 "isUnique": false
503 }461 }
504 },462 },
505 "foreignKeys": {},463 "foreignKeys": {},
506 "compositePrimaryKeys": {464 "compositePrimaryKeys": {
507 "graphql_source_scope_id_id_pk": {465 "graphql_source_scope_id_id_pk": {
508 "columns": [466 "columns": ["scope_id", "id"],
509 "scope_id",
510 "id"
511 ],
512 "name": "graphql_source_scope_id_id_pk"467 "name": "graphql_source_scope_id_id_pk"
513 }468 }
514 },469 },
@@ -557,26 +512,19 @@ · 42 unmodified lines
557 "indexes": {512 "indexes": {
558 "mcp_binding_scope_id_idx": {513 "mcp_binding_scope_id_idx": {
559 "name": "mcp_binding_scope_id_idx",514 "name": "mcp_binding_scope_id_idx",
560 "columns": [515 "columns": ["scope_id"],
561 "scope_id"
562 ],
563 "isUnique": false516 "isUnique": false
564 },517 },
565 "mcp_binding_source_id_idx": {518 "mcp_binding_source_id_idx": {
566 "name": "mcp_binding_source_id_idx",519 "name": "mcp_binding_source_id_idx",
567 "columns": [520 "columns": ["source_id"],
568 "source_id"
569 ],
570 "isUnique": false521 "isUnique": false
571 }522 }
572 },523 },
573 "foreignKeys": {},524 "foreignKeys": {},
574 "compositePrimaryKeys": {525 "compositePrimaryKeys": {
575 "mcp_binding_scope_id_id_pk": {526 "mcp_binding_scope_id_id_pk": {
576 "columns": [527 "columns": ["scope_id", "id"],
577 "scope_id",
578 "id"
579 ],
580 "name": "mcp_binding_scope_id_id_pk"528 "name": "mcp_binding_scope_id_id_pk"
581 }529 }
582 },530 },
@@ -625,19 +573,14 @@ · 42 unmodified lines
625 "indexes": {573 "indexes": {
626 "mcp_oauth_session_scope_id_idx": {574 "mcp_oauth_session_scope_id_idx": {
627 "name": "mcp_oauth_session_scope_id_idx",575 "name": "mcp_oauth_session_scope_id_idx",
628 "columns": [576 "columns": ["scope_id"],
629 "scope_id"
630 ],
631 "isUnique": false577 "isUnique": false
632 }578 }
633 },579 },
634 "foreignKeys": {},580 "foreignKeys": {},
635 "compositePrimaryKeys": {581 "compositePrimaryKeys": {
636 "mcp_oauth_session_scope_id_id_pk": {582 "mcp_oauth_session_scope_id_id_pk": {
637 "columns": [583 "columns": ["scope_id", "id"],
638 "scope_id",
639 "id"
640 ],
641 "name": "mcp_oauth_session_scope_id_id_pk"584 "name": "mcp_oauth_session_scope_id_id_pk"
642 }585 }
643 },586 },
@@ -686,19 +629,14 @@ · 42 unmodified lines
686 "indexes": {629 "indexes": {
687 "mcp_source_scope_id_idx": {630 "mcp_source_scope_id_idx": {
688 "name": "mcp_source_scope_id_idx",631 "name": "mcp_source_scope_id_idx",
689 "columns": [632 "columns": ["scope_id"],
690 "scope_id"
691 ],
692 "isUnique": false633 "isUnique": false
693 }634 }
694 },635 },
695 "foreignKeys": {},636 "foreignKeys": {},
696 "compositePrimaryKeys": {637 "compositePrimaryKeys": {
697 "mcp_source_scope_id_id_pk": {638 "mcp_source_scope_id_id_pk": {
698 "columns": [639 "columns": ["scope_id", "id"],
699 "scope_id",
700 "id"
701 ],
702 "name": "mcp_source_scope_id_id_pk"640 "name": "mcp_source_scope_id_id_pk"
703 }641 }
704 },642 },
@@ -740,19 +678,14 @@ · 35 unmodified lines
740 "indexes": {678 "indexes": {
741 "openapi_oauth_session_scope_id_idx": {679 "openapi_oauth_session_scope_id_idx": {
742 "name": "openapi_oauth_session_scope_id_idx",680 "name": "openapi_oauth_session_scope_id_idx",
743 "columns": [681 "columns": ["scope_id"],
744 "scope_id"
745 ],
746 "isUnique": false682 "isUnique": false
747 }683 }
748 },684 },
749 "foreignKeys": {},685 "foreignKeys": {},
750 "compositePrimaryKeys": {686 "compositePrimaryKeys": {
751 "openapi_oauth_session_scope_id_id_pk": {687 "openapi_oauth_session_scope_id_id_pk": {
752 "columns": [688 "columns": ["scope_id", "id"],
753 "scope_id",
754 "id"
755 ],
756 "name": "openapi_oauth_session_scope_id_id_pk"689 "name": "openapi_oauth_session_scope_id_id_pk"
757 }690 }
758 },691 },
@@ -794,26 +727,19 @@ · 35 unmodified lines
794 "indexes": {727 "indexes": {
795 "openapi_operation_scope_id_idx": {728 "openapi_operation_scope_id_idx": {
796 "name": "openapi_operation_scope_id_idx",729 "name": "openapi_operation_scope_id_idx",
797 "columns": [730 "columns": ["scope_id"],
798 "scope_id"
799 ],
800 "isUnique": false731 "isUnique": false
801 },732 },
802 "openapi_operation_source_id_idx": {733 "openapi_operation_source_id_idx": {
803 "name": "openapi_operation_source_id_idx",734 "name": "openapi_operation_source_id_idx",
804 "columns": [735 "columns": ["source_id"],
805 "source_id"
806 ],
807 "isUnique": false736 "isUnique": false
808 }737 }
809 },738 },
810 "foreignKeys": {},739 "foreignKeys": {},
811 "compositePrimaryKeys": {740 "compositePrimaryKeys": {
812 "openapi_operation_scope_id_id_pk": {741 "openapi_operation_scope_id_id_pk": {
813 "columns": [742 "columns": ["scope_id", "id"],
814 "scope_id",
815 "id"
816 ],
817 "name": "openapi_operation_scope_id_id_pk"743 "name": "openapi_operation_scope_id_id_pk"
818 }744 }
819 },745 },
@@ -890,19 +816,14 @@ · 70 unmodified lines
890 "indexes": {816 "indexes": {
891 "openapi_source_scope_id_idx": {817 "openapi_source_scope_id_idx": {
892 "name": "openapi_source_scope_id_idx",818 "name": "openapi_source_scope_id_idx",
893 "columns": [819 "columns": ["scope_id"],
894 "scope_id"
895 ],
896 "isUnique": false820 "isUnique": false
897 }821 }
898 },822 },
899 "foreignKeys": {},823 "foreignKeys": {},
900 "compositePrimaryKeys": {824 "compositePrimaryKeys": {
901 "openapi_source_scope_id_id_pk": {825 "openapi_source_scope_id_id_pk": {
902 "columns": [826 "columns": ["scope_id", "id"],
903 "scope_id",
904 "id"
905 ],
906 "name": "openapi_source_scope_id_id_pk"827 "name": "openapi_source_scope_id_id_pk"
907 }828 }
908 },829 },
@@ -972,30 +893,22 @@ · 63 unmodified lines
972 "indexes": {893 "indexes": {
973 "openapi_source_binding_source_id_idx": {894 "openapi_source_binding_source_id_idx": {
974 "name": "openapi_source_binding_source_id_idx",895 "name": "openapi_source_binding_source_id_idx",
975 "columns": [896 "columns": ["source_id"],
976 "source_id"
977 ],
978 "isUnique": false897 "isUnique": false
979 },898 },
980 "openapi_source_binding_source_scope_id_idx": {899 "openapi_source_binding_source_scope_id_idx": {
981 "name": "openapi_source_binding_source_scope_id_idx",900 "name": "openapi_source_binding_source_scope_id_idx",
982 "columns": [901 "columns": ["source_scope_id"],
983 "source_scope_id"
984 ],
985 "isUnique": false902 "isUnique": false
986 },903 },
987 "openapi_source_binding_target_scope_id_idx": {904 "openapi_source_binding_target_scope_id_idx": {
988 "name": "openapi_source_binding_target_scope_id_idx",905 "name": "openapi_source_binding_target_scope_id_idx",
989 "columns": [906 "columns": ["target_scope_id"],
990 "target_scope_id"
991 ],
992 "isUnique": false907 "isUnique": false
993 },908 },
994 "openapi_source_binding_slot_idx": {909 "openapi_source_binding_slot_idx": {
995 "name": "openapi_source_binding_slot_idx",910 "name": "openapi_source_binding_slot_idx",
996 "columns": [911 "columns": ["slot"],
997 "slot"
998 ],
999 "isUnique": false912 "isUnique": false
1000 }913 }
1001 },914 },
@@ -1053,33 +966,24 @@ · 51 unmodified lines
1053 "indexes": {966 "indexes": {
1054 "secret_scope_id_idx": {967 "secret_scope_id_idx": {
1055 "name": "secret_scope_id_idx",968 "name": "secret_scope_id_idx",
1056 "columns": [969 "columns": ["scope_id"],
1057 "scope_id"
1058 ],
1059 "isUnique": false970 "isUnique": false
1060 },971 },
1061 "secret_provider_idx": {972 "secret_provider_idx": {
1062 "name": "secret_provider_idx",973 "name": "secret_provider_idx",
1063 "columns": [974 "columns": ["provider"],
1064 "provider"
1065 ],
1066 "isUnique": false975 "isUnique": false
1067 },976 },
1068 "secret_owned_by_connection_id_idx": {977 "secret_owned_by_connection_id_idx": {
1069 "name": "secret_owned_by_connection_id_idx",978 "name": "secret_owned_by_connection_id_idx",
1070 "columns": [979 "columns": ["owned_by_connection_id"],
1071 "owned_by_connection_id"
1072 ],
1073 "isUnique": false980 "isUnique": false
1074 }981 }
1075 },982 },
1076 "foreignKeys": {},983 "foreignKeys": {},
1077 "compositePrimaryKeys": {984 "compositePrimaryKeys": {
1078 "secret_scope_id_id_pk": {985 "secret_scope_id_id_pk": {
1079 "columns": [986 "columns": ["scope_id", "id"],
1080 "scope_id",
1081 "id"
1082 ],
1083 "name": "secret_scope_id_id_pk"987 "name": "secret_scope_id_id_pk"
1084 }988 }
1085 },989 },
@@ -1173,26 +1077,19 @@ · 87 unmodified lines
1173 "indexes": {1077 "indexes": {
1174 "source_scope_id_idx": {1078 "source_scope_id_idx": {
1175 "name": "source_scope_id_idx",1079 "name": "source_scope_id_idx",
1176 "columns": [1080 "columns": ["scope_id"],
1177 "scope_id"
1178 ],
1179 "isUnique": false1081 "isUnique": false
1180 },1082 },
1181 "source_plugin_id_idx": {1083 "source_plugin_id_idx": {
1182 "name": "source_plugin_id_idx",1084 "name": "source_plugin_id_idx",
1183 "columns": [1085 "columns": ["plugin_id"],
1184 "plugin_id"
1185 ],
1186 "isUnique": false1086 "isUnique": false
1187 }1087 }
1188 },1088 },
1189 "foreignKeys": {},1089 "foreignKeys": {},
1190 "compositePrimaryKeys": {1090 "compositePrimaryKeys": {
1191 "source_scope_id_id_pk": {1091 "source_scope_id_id_pk": {
1192 "columns": [1092 "columns": ["scope_id", "id"],
1193 "scope_id",
1194 "id"
1195 ],
1196 "name": "source_scope_id_id_pk"1093 "name": "source_scope_id_id_pk"
1197 }1094 }
1198 },1095 },
@@ -1276,33 +1173,24 @@ · 77 unmodified lines
1276 "indexes": {1173 "indexes": {
1277 "tool_scope_id_idx": {1174 "tool_scope_id_idx": {
1278 "name": "tool_scope_id_idx",1175 "name": "tool_scope_id_idx",
1279 "columns": [1176 "columns": ["scope_id"],
1280 "scope_id"
1281 ],
1282 "isUnique": false1177 "isUnique": false
1283 },1178 },
1284 "tool_source_id_idx": {1179 "tool_source_id_idx": {
1285 "name": "tool_source_id_idx",1180 "name": "tool_source_id_idx",
1286 "columns": [1181 "columns": ["source_id"],
1287 "source_id"
1288 ],
1289 "isUnique": false1182 "isUnique": false
1290 },1183 },
1291 "tool_plugin_id_idx": {1184 "tool_plugin_id_idx": {
1292 "name": "tool_plugin_id_idx",1185 "name": "tool_plugin_id_idx",
1293 "columns": [1186 "columns": ["plugin_id"],
1294 "plugin_id"
1295 ],
1296 "isUnique": false1187 "isUnique": false
1297 }1188 }
1298 },1189 },
1299 "foreignKeys": {},1190 "foreignKeys": {},
1300 "compositePrimaryKeys": {1191 "compositePrimaryKeys": {
1301 "tool_scope_id_id_pk": {1192 "tool_scope_id_id_pk": {
1302 "columns": [1193 "columns": ["scope_id", "id"],
1303 "scope_id",
1304 "id"
1305 ],
1306 "name": "tool_scope_id_id_pk"1194 "name": "tool_scope_id_id_pk"
1307 }1195 }
1308 },1196 },
@@ -1365,20 +1253,14 @@ · 56 unmodified lines
1365 "indexes": {1253 "indexes": {
1366 "tool_policy_scope_id_position_idx": {1254 "tool_policy_scope_id_position_idx": {
1367 "name": "tool_policy_scope_id_position_idx",1255 "name": "tool_policy_scope_id_position_idx",
1368 "columns": [1256 "columns": ["scope_id", "position"],
1369 "scope_id",
1370 "position"
1371 ],
1372 "isUnique": false1257 "isUnique": false
1373 }1258 }
1374 },1259 },
1375 "foreignKeys": {},1260 "foreignKeys": {},
1376 "compositePrimaryKeys": {1261 "compositePrimaryKeys": {
1377 "tool_policy_scope_id_id_pk": {1262 "tool_policy_scope_id_id_pk": {
1378 "columns": [1263 "columns": ["scope_id", "id"],
1379 "scope_id",
1380 "id"
1381 ],
1382 "name": "tool_policy_scope_id_id_pk"1264 "name": "tool_policy_scope_id_id_pk"
1383 }1265 }
1384 },1266 },
@@ -1414,10 +1296,7 @@ · 29 unmodified lines
1414 "foreignKeys": {},1296 "foreignKeys": {},
1415 "compositePrimaryKeys": {1297 "compositePrimaryKeys": {
1416 "blob_namespace_key_pk": {1298 "blob_namespace_key_pk": {
1417 "columns": [1299 "columns": ["namespace", "key"],
1418 "namespace",
1419 "key"
1420 ],
1421 "name": "blob_namespace_key_pk"1300 "name": "blob_namespace_key_pk"
1422 }1301 }
1423 },1302 },
apps/local/drizzle/meta/0006_snapshot.json+50−165
@@ -33,10 +33,7 @@ · 32 unmodified lines
33 "foreignKeys": {},33 "foreignKeys": {},
34 "compositePrimaryKeys": {34 "compositePrimaryKeys": {
35 "blob_namespace_key_pk": {35 "blob_namespace_key_pk": {
36 "columns": [36 "columns": ["namespace", "key"],
37 "namespace",
38 "key"
39 ],
40 "name": "blob_namespace_key_pk"37 "name": "blob_namespace_key_pk"
41 }38 }
42 },39 },
@@ -127,26 +124,19 @@ · 84 unmodified lines
127 "indexes": {124 "indexes": {
128 "connection_scope_id_idx": {125 "connection_scope_id_idx": {
129 "name": "connection_scope_id_idx",126 "name": "connection_scope_id_idx",
130 "columns": [127 "columns": ["scope_id"],
131 "scope_id"
132 ],
133 "isUnique": false128 "isUnique": false
134 },129 },
135 "connection_provider_idx": {130 "connection_provider_idx": {
136 "name": "connection_provider_idx",131 "name": "connection_provider_idx",
137 "columns": [132 "columns": ["provider"],
138 "provider"
139 ],
140 "isUnique": false133 "isUnique": false
141 }134 }
142 },135 },
143 "foreignKeys": {},136 "foreignKeys": {},
144 "compositePrimaryKeys": {137 "compositePrimaryKeys": {
145 "connection_scope_id_id_pk": {138 "connection_scope_id_id_pk": {
146 "columns": [139 "columns": ["scope_id", "id"],
147 "scope_id",
148 "id"
149 ],
150 "name": "connection_scope_id_id_pk"140 "name": "connection_scope_id_id_pk"
151 }141 }
152 },142 },
@@ -209,33 +199,24 @@ · 56 unmodified lines
209 "indexes": {199 "indexes": {
210 "definition_scope_id_idx": {200 "definition_scope_id_idx": {
211 "name": "definition_scope_id_idx",201 "name": "definition_scope_id_idx",
212 "columns": [202 "columns": ["scope_id"],
213 "scope_id"
214 ],
215 "isUnique": false203 "isUnique": false
216 },204 },
217 "definition_source_id_idx": {205 "definition_source_id_idx": {
218 "name": "definition_source_id_idx",206 "name": "definition_source_id_idx",
219 "columns": [207 "columns": ["source_id"],
220 "source_id"
221 ],
222 "isUnique": false208 "isUnique": false
223 },209 },
224 "definition_plugin_id_idx": {210 "definition_plugin_id_idx": {
225 "name": "definition_plugin_id_idx",211 "name": "definition_plugin_id_idx",
226 "columns": [212 "columns": ["plugin_id"],
227 "plugin_id"
228 ],
229 "isUnique": false213 "isUnique": false
230 }214 }
231 },215 },
232 "foreignKeys": {},216 "foreignKeys": {},
233 "compositePrimaryKeys": {217 "compositePrimaryKeys": {
234 "definition_scope_id_id_pk": {218 "definition_scope_id_id_pk": {
235 "columns": [219 "columns": ["scope_id", "id"],
236 "scope_id",
237 "id"
238 ],
239 "name": "definition_scope_id_id_pk"220 "name": "definition_scope_id_id_pk"
240 }221 }
241 },222 },
@@ -284,26 +265,19 @@ · 42 unmodified lines
284 "indexes": {265 "indexes": {
285 "google_discovery_binding_scope_id_idx": {266 "google_discovery_binding_scope_id_idx": {
286 "name": "google_discovery_binding_scope_id_idx",267 "name": "google_discovery_binding_scope_id_idx",
287 "columns": [268 "columns": ["scope_id"],
288 "scope_id"
289 ],
290 "isUnique": false269 "isUnique": false
291 },270 },
292 "google_discovery_binding_source_id_idx": {271 "google_discovery_binding_source_id_idx": {
293 "name": "google_discovery_binding_source_id_idx",272 "name": "google_discovery_binding_source_id_idx",
294 "columns": [273 "columns": ["source_id"],
295 "source_id"
296 ],
297 "isUnique": false274 "isUnique": false
298 }275 }
299 },276 },
300 "foreignKeys": {},277 "foreignKeys": {},
301 "compositePrimaryKeys": {278 "compositePrimaryKeys": {
302 "google_discovery_binding_scope_id_id_pk": {279 "google_discovery_binding_scope_id_id_pk": {
303 "columns": [280 "columns": ["scope_id", "id"],
304 "scope_id",
305 "id"
306 ],
307 "name": "google_discovery_binding_scope_id_id_pk"281 "name": "google_discovery_binding_scope_id_id_pk"
308 }282 }
309 },283 },
@@ -359,19 +333,14 @@ · 49 unmodified lines
359 "indexes": {333 "indexes": {
360 "google_discovery_source_scope_id_idx": {334 "google_discovery_source_scope_id_idx": {
361 "name": "google_discovery_source_scope_id_idx",335 "name": "google_discovery_source_scope_id_idx",
362 "columns": [336 "columns": ["scope_id"],
363 "scope_id"
364 ],
365 "isUnique": false337 "isUnique": false
366 }338 }
367 },339 },
368 "foreignKeys": {},340 "foreignKeys": {},
369 "compositePrimaryKeys": {341 "compositePrimaryKeys": {
370 "google_discovery_source_scope_id_id_pk": {342 "google_discovery_source_scope_id_id_pk": {
371 "columns": [343 "columns": ["scope_id", "id"],
372 "scope_id",
373 "id"
374 ],
375 "name": "google_discovery_source_scope_id_id_pk"344 "name": "google_discovery_source_scope_id_id_pk"
376 }345 }
377 },346 },
@@ -413,26 +382,19 @@ · 35 unmodified lines
413 "indexes": {382 "indexes": {
414 "graphql_operation_scope_id_idx": {383 "graphql_operation_scope_id_idx": {
415 "name": "graphql_operation_scope_id_idx",384 "name": "graphql_operation_scope_id_idx",
416 "columns": [385 "columns": ["scope_id"],
417 "scope_id"
418 ],
419 "isUnique": false386 "isUnique": false
420 },387 },
421 "graphql_operation_source_id_idx": {388 "graphql_operation_source_id_idx": {
422 "name": "graphql_operation_source_id_idx",389 "name": "graphql_operation_source_id_idx",
423 "columns": [390 "columns": ["source_id"],
424 "source_id"
425 ],
426 "isUnique": false391 "isUnique": false
427 }392 }
428 },393 },
429 "foreignKeys": {},394 "foreignKeys": {},
430 "compositePrimaryKeys": {395 "compositePrimaryKeys": {
431 "graphql_operation_scope_id_id_pk": {396 "graphql_operation_scope_id_id_pk": {
432 "columns": [397 "columns": ["scope_id", "id"],
433 "scope_id",
434 "id"
435 ],
436 "name": "graphql_operation_scope_id_id_pk"398 "name": "graphql_operation_scope_id_id_pk"
437 }399 }
438 },400 },
@@ -495,19 +457,14 @@ · 56 unmodified lines
495 "indexes": {457 "indexes": {
496 "graphql_source_scope_id_idx": {458 "graphql_source_scope_id_idx": {
497 "name": "graphql_source_scope_id_idx",459 "name": "graphql_source_scope_id_idx",
498 "columns": [460 "columns": ["scope_id"],
499 "scope_id"
500 ],
501 "isUnique": false461 "isUnique": false
502 }462 }
503 },463 },
504 "foreignKeys": {},464 "foreignKeys": {},
505 "compositePrimaryKeys": {465 "compositePrimaryKeys": {
506 "graphql_source_scope_id_id_pk": {466 "graphql_source_scope_id_id_pk": {
507 "columns": [467 "columns": ["scope_id", "id"],
508 "scope_id",
509 "id"
510 ],
511 "name": "graphql_source_scope_id_id_pk"468 "name": "graphql_source_scope_id_id_pk"
512 }469 }
513 },470 },
@@ -556,26 +513,19 @@ · 42 unmodified lines
556 "indexes": {513 "indexes": {
557 "mcp_binding_scope_id_idx": {514 "mcp_binding_scope_id_idx": {
558 "name": "mcp_binding_scope_id_idx",515 "name": "mcp_binding_scope_id_idx",
559 "columns": [516 "columns": ["scope_id"],
560 "scope_id"
561 ],
562 "isUnique": false517 "isUnique": false
563 },518 },
564 "mcp_binding_source_id_idx": {519 "mcp_binding_source_id_idx": {
565 "name": "mcp_binding_source_id_idx",520 "name": "mcp_binding_source_id_idx",
566 "columns": [521 "columns": ["source_id"],
567 "source_id"
568 ],
569 "isUnique": false522 "isUnique": false
570 }523 }
571 },524 },
572 "foreignKeys": {},525 "foreignKeys": {},
573 "compositePrimaryKeys": {526 "compositePrimaryKeys": {
574 "mcp_binding_scope_id_id_pk": {527 "mcp_binding_scope_id_id_pk": {
575 "columns": [528 "columns": ["scope_id", "id"],
576 "scope_id",
577 "id"
578 ],
579 "name": "mcp_binding_scope_id_id_pk"529 "name": "mcp_binding_scope_id_id_pk"
580 }530 }
581 },531 },
@@ -624,19 +574,14 @@ · 42 unmodified lines
624 "indexes": {574 "indexes": {
625 "mcp_source_scope_id_idx": {575 "mcp_source_scope_id_idx": {
626 "name": "mcp_source_scope_id_idx",576 "name": "mcp_source_scope_id_idx",
627 "columns": [577 "columns": ["scope_id"],
628 "scope_id"
629 ],
630 "isUnique": false578 "isUnique": false
631 }579 }
632 },580 },
633 "foreignKeys": {},581 "foreignKeys": {},
634 "compositePrimaryKeys": {582 "compositePrimaryKeys": {
635 "mcp_source_scope_id_id_pk": {583 "mcp_source_scope_id_id_pk": {
636 "columns": [584 "columns": ["scope_id", "id"],
637 "scope_id",
638 "id"
639 ],
640 "name": "mcp_source_scope_id_id_pk"585 "name": "mcp_source_scope_id_id_pk"
641 }586 }
642 },587 },
@@ -720,33 +665,24 @@ · 77 unmodified lines
720 "indexes": {665 "indexes": {
721 "oauth2_session_scope_id_idx": {666 "oauth2_session_scope_id_idx": {
722 "name": "oauth2_session_scope_id_idx",667 "name": "oauth2_session_scope_id_idx",
723 "columns": [668 "columns": ["scope_id"],
724 "scope_id"
725 ],
726 "isUnique": false669 "isUnique": false
727 },670 },
728 "oauth2_session_plugin_id_idx": {671 "oauth2_session_plugin_id_idx": {
729 "name": "oauth2_session_plugin_id_idx",672 "name": "oauth2_session_plugin_id_idx",
730 "columns": [673 "columns": ["plugin_id"],
731 "plugin_id"
732 ],
733 "isUnique": false674 "isUnique": false
734 },675 },
735 "oauth2_session_connection_id_idx": {676 "oauth2_session_connection_id_idx": {
736 "name": "oauth2_session_connection_id_idx",677 "name": "oauth2_session_connection_id_idx",
737 "columns": [678 "columns": ["connection_id"],
738 "connection_id"
739 ],
740 "isUnique": false679 "isUnique": false
741 }680 }
742 },681 },
743 "foreignKeys": {},682 "foreignKeys": {},
744 "compositePrimaryKeys": {683 "compositePrimaryKeys": {
745 "oauth2_session_scope_id_id_pk": {684 "oauth2_session_scope_id_id_pk": {
746 "columns": [685 "columns": ["scope_id", "id"],
747 "scope_id",
748 "id"
749 ],
750 "name": "oauth2_session_scope_id_id_pk"686 "name": "oauth2_session_scope_id_id_pk"
751 }687 }
752 },688 },
@@ -788,26 +724,19 @@ · 35 unmodified lines
788 "indexes": {724 "indexes": {
789 "openapi_operation_scope_id_idx": {725 "openapi_operation_scope_id_idx": {
790 "name": "openapi_operation_scope_id_idx",726 "name": "openapi_operation_scope_id_idx",
791 "columns": [727 "columns": ["scope_id"],
792 "scope_id"
793 ],
794 "isUnique": false728 "isUnique": false
795 },729 },
796 "openapi_operation_source_id_idx": {730 "openapi_operation_source_id_idx": {
797 "name": "openapi_operation_source_id_idx",731 "name": "openapi_operation_source_id_idx",
798 "columns": [732 "columns": ["source_id"],
799 "source_id"
800 ],
801 "isUnique": false733 "isUnique": false
802 }734 }
803 },735 },
804 "foreignKeys": {},736 "foreignKeys": {},
805 "compositePrimaryKeys": {737 "compositePrimaryKeys": {
806 "openapi_operation_scope_id_id_pk": {738 "openapi_operation_scope_id_id_pk": {
807 "columns": [739 "columns": ["scope_id", "id"],
808 "scope_id",
809 "id"
810 ],
811 "name": "openapi_operation_scope_id_id_pk"740 "name": "openapi_operation_scope_id_id_pk"
812 }741 }
813 },742 },
@@ -891,19 +820,14 @@ · 77 unmodified lines
891 "indexes": {820 "indexes": {
892 "openapi_source_scope_id_idx": {821 "openapi_source_scope_id_idx": {
893 "name": "openapi_source_scope_id_idx",822 "name": "openapi_source_scope_id_idx",
894 "columns": [823 "columns": ["scope_id"],
895 "scope_id"
896 ],
897 "isUnique": false824 "isUnique": false
898 }825 }
899 },826 },
900 "foreignKeys": {},827 "foreignKeys": {},
901 "compositePrimaryKeys": {828 "compositePrimaryKeys": {
902 "openapi_source_scope_id_id_pk": {829 "openapi_source_scope_id_id_pk": {
903 "columns": [830 "columns": ["scope_id", "id"],
904 "scope_id",
905 "id"
906 ],
907 "name": "openapi_source_scope_id_id_pk"831 "name": "openapi_source_scope_id_id_pk"
908 }832 }
909 },833 },
@@ -973,30 +897,22 @@ · 63 unmodified lines
973 "indexes": {897 "indexes": {
974 "openapi_source_binding_source_id_idx": {898 "openapi_source_binding_source_id_idx": {
975 "name": "openapi_source_binding_source_id_idx",899 "name": "openapi_source_binding_source_id_idx",
976 "columns": [900 "columns": ["source_id"],
977 "source_id"
978 ],
979 "isUnique": false901 "isUnique": false
980 },902 },
981 "openapi_source_binding_source_scope_id_idx": {903 "openapi_source_binding_source_scope_id_idx": {
982 "name": "openapi_source_binding_source_scope_id_idx",904 "name": "openapi_source_binding_source_scope_id_idx",
983 "columns": [905 "columns": ["source_scope_id"],
984 "source_scope_id"
985 ],
986 "isUnique": false906 "isUnique": false
987 },907 },
988 "openapi_source_binding_target_scope_id_idx": {908 "openapi_source_binding_target_scope_id_idx": {
989 "name": "openapi_source_binding_target_scope_id_idx",909 "name": "openapi_source_binding_target_scope_id_idx",
990 "columns": [910 "columns": ["target_scope_id"],
991 "target_scope_id"
992 ],
993 "isUnique": false911 "isUnique": false
994 },912 },
995 "openapi_source_binding_slot_idx": {913 "openapi_source_binding_slot_idx": {
996 "name": "openapi_source_binding_slot_idx",914 "name": "openapi_source_binding_slot_idx",
997 "columns": [915 "columns": ["slot"],
998 "slot"
999 ],
1000 "isUnique": false916 "isUnique": false
1001 }917 }
1002 },918 },
@@ -1054,33 +970,24 @@ · 51 unmodified lines
1054 "indexes": {970 "indexes": {
1055 "secret_scope_id_idx": {971 "secret_scope_id_idx": {
1056 "name": "secret_scope_id_idx",972 "name": "secret_scope_id_idx",
1057 "columns": [973 "columns": ["scope_id"],
1058 "scope_id"
1059 ],
1060 "isUnique": false974 "isUnique": false
1061 },975 },
1062 "secret_provider_idx": {976 "secret_provider_idx": {
1063 "name": "secret_provider_idx",977 "name": "secret_provider_idx",
1064 "columns": [978 "columns": ["provider"],
1065 "provider"
1066 ],
1067 "isUnique": false979 "isUnique": false
1068 },980 },
1069 "secret_owned_by_connection_id_idx": {981 "secret_owned_by_connection_id_idx": {
1070 "name": "secret_owned_by_connection_id_idx",982 "name": "secret_owned_by_connection_id_idx",
1071 "columns": [983 "columns": ["owned_by_connection_id"],
1072 "owned_by_connection_id"
1073 ],
1074 "isUnique": false984 "isUnique": false
1075 }985 }
1076 },986 },
1077 "foreignKeys": {},987 "foreignKeys": {},
1078 "compositePrimaryKeys": {988 "compositePrimaryKeys": {
1079 "secret_scope_id_id_pk": {989 "secret_scope_id_id_pk": {
1080 "columns": [990 "columns": ["scope_id", "id"],
1081 "scope_id",
1082 "id"
1083 ],
1084 "name": "secret_scope_id_id_pk"991 "name": "secret_scope_id_id_pk"
1085 }992 }
1086 },993 },
@@ -1174,26 +1081,19 @@ · 87 unmodified lines
1174 "indexes": {1081 "indexes": {
1175 "source_scope_id_idx": {1082 "source_scope_id_idx": {
1176 "name": "source_scope_id_idx",1083 "name": "source_scope_id_idx",
1177 "columns": [1084 "columns": ["scope_id"],
1178 "scope_id"
1179 ],
1180 "isUnique": false1085 "isUnique": false
1181 },1086 },
1182 "source_plugin_id_idx": {1087 "source_plugin_id_idx": {
1183 "name": "source_plugin_id_idx",1088 "name": "source_plugin_id_idx",
1184 "columns": [1089 "columns": ["plugin_id"],
1185 "plugin_id"
1186 ],
1187 "isUnique": false1090 "isUnique": false
1188 }1091 }
1189 },1092 },
1190 "foreignKeys": {},1093 "foreignKeys": {},
1191 "compositePrimaryKeys": {1094 "compositePrimaryKeys": {
1192 "source_scope_id_id_pk": {1095 "source_scope_id_id_pk": {
1193 "columns": [1096 "columns": ["scope_id", "id"],
1194 "scope_id",
1195 "id"
1196 ],
1197 "name": "source_scope_id_id_pk"1097 "name": "source_scope_id_id_pk"
1198 }1098 }
1199 },1099 },
@@ -1277,33 +1177,24 @@ · 77 unmodified lines
1277 "indexes": {1177 "indexes": {
1278 "tool_scope_id_idx": {1178 "tool_scope_id_idx": {
1279 "name": "tool_scope_id_idx",1179 "name": "tool_scope_id_idx",
1280 "columns": [1180 "columns": ["scope_id"],
1281 "scope_id"
1282 ],
1283 "isUnique": false1181 "isUnique": false
1284 },1182 },
1285 "tool_source_id_idx": {1183 "tool_source_id_idx": {
1286 "name": "tool_source_id_idx",1184 "name": "tool_source_id_idx",
1287 "columns": [1185 "columns": ["source_id"],
1288 "source_id"
1289 ],
1290 "isUnique": false1186 "isUnique": false
1291 },1187 },
1292 "tool_plugin_id_idx": {1188 "tool_plugin_id_idx": {
1293 "name": "tool_plugin_id_idx",1189 "name": "tool_plugin_id_idx",
1294 "columns": [1190 "columns": ["plugin_id"],
1295 "plugin_id"
1296 ],
1297 "isUnique": false1191 "isUnique": false
1298 }1192 }
1299 },1193 },
1300 "foreignKeys": {},1194 "foreignKeys": {},
1301 "compositePrimaryKeys": {1195 "compositePrimaryKeys": {
1302 "tool_scope_id_id_pk": {1196 "tool_scope_id_id_pk": {
1303 "columns": [1197 "columns": ["scope_id", "id"],
1304 "scope_id",
1305 "id"
1306 ],
1307 "name": "tool_scope_id_id_pk"1198 "name": "tool_scope_id_id_pk"
1308 }1199 }
1309 },1200 },
@@ -1366,20 +1257,14 @@ · 56 unmodified lines
1366 "indexes": {1257 "indexes": {
1367 "tool_policy_scope_id_position_idx": {1258 "tool_policy_scope_id_position_idx": {
1368 "name": "tool_policy_scope_id_position_idx",1259 "name": "tool_policy_scope_id_position_idx",
1369 "columns": [1260 "columns": ["scope_id", "position"],
1370 "scope_id",
1371 "position"
1372 ],
1373 "isUnique": false1261 "isUnique": false
1374 }1262 }
1375 },1263 },
1376 "foreignKeys": {},1264 "foreignKeys": {},
1377 "compositePrimaryKeys": {1265 "compositePrimaryKeys": {
1378 "tool_policy_scope_id_id_pk": {1266 "tool_policy_scope_id_id_pk": {
1379 "columns": [1267 "columns": ["scope_id", "id"],
1380 "scope_id",
1381 "id"
1382 ],
1383 "name": "tool_policy_scope_id_id_pk"1268 "name": "tool_policy_scope_id_id_pk"
1384 }1269 }
1385 },1270 },
@@ -1397,4 +1282,4 @@ · 11 unmodified lines
1397 "internal": {1282 "internal": {
1398 "indexes": {}1283 "indexes": {}
1399 }1284 }
1400}1285}
apps/local/drizzle/meta/0007_snapshot.json+95−311
@@ -33,10 +33,7 @@ · 32 unmodified lines
33 "foreignKeys": {},33 "foreignKeys": {},
34 "compositePrimaryKeys": {34 "compositePrimaryKeys": {
35 "blob_namespace_key_pk": {35 "blob_namespace_key_pk": {
36 "columns": [36 "columns": ["namespace", "key"],
37 "namespace",
38 "key"
39 ],
40 "name": "blob_namespace_key_pk"37 "name": "blob_namespace_key_pk"
41 }38 }
42 },39 },
@@ -127,26 +124,19 @@ · 84 unmodified lines
127 "indexes": {124 "indexes": {
128 "connection_scope_id_idx": {125 "connection_scope_id_idx": {
129 "name": "connection_scope_id_idx",126 "name": "connection_scope_id_idx",
130 "columns": [127 "columns": ["scope_id"],
131 "scope_id"
132 ],
133 "isUnique": false128 "isUnique": false
134 },129 },
135 "connection_provider_idx": {130 "connection_provider_idx": {
136 "name": "connection_provider_idx",131 "name": "connection_provider_idx",
137 "columns": [132 "columns": ["provider"],
138 "provider"
139 ],
140 "isUnique": false133 "isUnique": false
141 }134 }
142 },135 },
143 "foreignKeys": {},136 "foreignKeys": {},
144 "compositePrimaryKeys": {137 "compositePrimaryKeys": {
145 "connection_scope_id_id_pk": {138 "connection_scope_id_id_pk": {
146 "columns": [139 "columns": ["scope_id", "id"],
147 "scope_id",
148 "id"
149 ],
150 "name": "connection_scope_id_id_pk"140 "name": "connection_scope_id_id_pk"
151 }141 }
152 },142 },
@@ -209,33 +199,24 @@ · 56 unmodified lines
209 "indexes": {199 "indexes": {
210 "definition_scope_id_idx": {200 "definition_scope_id_idx": {
211 "name": "definition_scope_id_idx",201 "name": "definition_scope_id_idx",
212 "columns": [202 "columns": ["scope_id"],
213 "scope_id"
214 ],
215 "isUnique": false203 "isUnique": false
216 },204 },
217 "definition_source_id_idx": {205 "definition_source_id_idx": {
218 "name": "definition_source_id_idx",206 "name": "definition_source_id_idx",
219 "columns": [207 "columns": ["source_id"],
220 "source_id"
221 ],
222 "isUnique": false208 "isUnique": false
223 },209 },
224 "definition_plugin_id_idx": {210 "definition_plugin_id_idx": {
225 "name": "definition_plugin_id_idx",211 "name": "definition_plugin_id_idx",
226 "columns": [212 "columns": ["plugin_id"],
227 "plugin_id"
228 ],
229 "isUnique": false213 "isUnique": false
230 }214 }
231 },215 },
232 "foreignKeys": {},216 "foreignKeys": {},
233 "compositePrimaryKeys": {217 "compositePrimaryKeys": {
234 "definition_scope_id_id_pk": {218 "definition_scope_id_id_pk": {
235 "columns": [219 "columns": ["scope_id", "id"],
236 "scope_id",
237 "id"
238 ],
239 "name": "definition_scope_id_id_pk"220 "name": "definition_scope_id_id_pk"
240 }221 }
241 },222 },
@@ -284,26 +265,19 @@ · 42 unmodified lines
284 "indexes": {265 "indexes": {
285 "google_discovery_binding_scope_id_idx": {266 "google_discovery_binding_scope_id_idx": {
286 "name": "google_discovery_binding_scope_id_idx",267 "name": "google_discovery_binding_scope_id_idx",
287 "columns": [268 "columns": ["scope_id"],
288 "scope_id"
289 ],
290 "isUnique": false269 "isUnique": false
291 },270 },
292 "google_discovery_binding_source_id_idx": {271 "google_discovery_binding_source_id_idx": {
293 "name": "google_discovery_binding_source_id_idx",272 "name": "google_discovery_binding_source_id_idx",
294 "columns": [273 "columns": ["source_id"],
295 "source_id"
296 ],
297 "isUnique": false274 "isUnique": false
298 }275 }
299 },276 },
300 "foreignKeys": {},277 "foreignKeys": {},
301 "compositePrimaryKeys": {278 "compositePrimaryKeys": {
302 "google_discovery_binding_scope_id_id_pk": {279 "google_discovery_binding_scope_id_id_pk": {
303 "columns": [280 "columns": ["scope_id", "id"],
304 "scope_id",
305 "id"
306 ],
307 "name": "google_discovery_binding_scope_id_id_pk"281 "name": "google_discovery_binding_scope_id_id_pk"
308 }282 }
309 },283 },
@@ -395,40 +369,29 @@ · 85 unmodified lines
395 "indexes": {369 "indexes": {
396 "google_discovery_source_scope_id_idx": {370 "google_discovery_source_scope_id_idx": {
397 "name": "google_discovery_source_scope_id_idx",371 "name": "google_discovery_source_scope_id_idx",
398 "columns": [372 "columns": ["scope_id"],
399 "scope_id"
400 ],
401 "isUnique": false373 "isUnique": false
402 },374 },
403 "google_discovery_source_auth_connection_id_idx": {375 "google_discovery_source_auth_connection_id_idx": {
404 "name": "google_discovery_source_auth_connection_id_idx",376 "name": "google_discovery_source_auth_connection_id_idx",
405 "columns": [377 "columns": ["auth_connection_id"],
406 "auth_connection_id"
407 ],
408 "isUnique": false378 "isUnique": false
409 },379 },
410 "google_discovery_source_auth_client_id_secret_id_idx": {380 "google_discovery_source_auth_client_id_secret_id_idx": {
411 "name": "google_discovery_source_auth_client_id_secret_id_idx",381 "name": "google_discovery_source_auth_client_id_secret_id_idx",
412 "columns": [382 "columns": ["auth_client_id_secret_id"],
413 "auth_client_id_secret_id"
414 ],
415 "isUnique": false383 "isUnique": false
416 },384 },
417 "google_discovery_source_auth_client_secret_secret_id_idx": {385 "google_discovery_source_auth_client_secret_secret_id_idx": {
418 "name": "google_discovery_source_auth_client_secret_secret_id_idx",386 "name": "google_discovery_source_auth_client_secret_secret_id_idx",
419 "columns": [387 "columns": ["auth_client_secret_secret_id"],
420 "auth_client_secret_secret_id"
421 ],
422 "isUnique": false388 "isUnique": false
423 }389 }
424 },390 },
425 "foreignKeys": {},391 "foreignKeys": {},
426 "compositePrimaryKeys": {392 "compositePrimaryKeys": {
427 "google_discovery_source_scope_id_id_pk": {393 "google_discovery_source_scope_id_id_pk": {
428 "columns": [394 "columns": ["scope_id", "id"],
429 "scope_id",
430 "id"
431 ],
432 "name": "google_discovery_source_scope_id_id_pk"395 "name": "google_discovery_source_scope_id_id_pk"
433 }396 }
434 },397 },
@@ -470,26 +433,19 @@ · 35 unmodified lines
470 "indexes": {433 "indexes": {
471 "graphql_operation_scope_id_idx": {434 "graphql_operation_scope_id_idx": {
472 "name": "graphql_operation_scope_id_idx",435 "name": "graphql_operation_scope_id_idx",
473 "columns": [436 "columns": ["scope_id"],
474 "scope_id"
475 ],
476 "isUnique": false437 "isUnique": false
477 },438 },
478 "graphql_operation_source_id_idx": {439 "graphql_operation_source_id_idx": {
479 "name": "graphql_operation_source_id_idx",440 "name": "graphql_operation_source_id_idx",
480 "columns": [441 "columns": ["source_id"],
481 "source_id"
482 ],
483 "isUnique": false442 "isUnique": false
484 }443 }
485 },444 },
486 "foreignKeys": {},445 "foreignKeys": {},
487 "compositePrimaryKeys": {446 "compositePrimaryKeys": {
488 "graphql_operation_scope_id_id_pk": {447 "graphql_operation_scope_id_id_pk": {
489 "columns": [448 "columns": ["scope_id", "id"],
490 "scope_id",
491 "id"
492 ],
493 "name": "graphql_operation_scope_id_id_pk"449 "name": "graphql_operation_scope_id_id_pk"
494 }450 }
495 },451 },
@@ -546,26 +502,19 @@ · 50 unmodified lines
546 "indexes": {502 "indexes": {
547 "graphql_source_scope_id_idx": {503 "graphql_source_scope_id_idx": {
548 "name": "graphql_source_scope_id_idx",504 "name": "graphql_source_scope_id_idx",
549 "columns": [505 "columns": ["scope_id"],
550 "scope_id"
551 ],
552 "isUnique": false506 "isUnique": false
553 },507 },
554 "graphql_source_auth_connection_id_idx": {508 "graphql_source_auth_connection_id_idx": {
555 "name": "graphql_source_auth_connection_id_idx",509 "name": "graphql_source_auth_connection_id_idx",
556 "columns": [510 "columns": ["auth_connection_id"],
557 "auth_connection_id"
558 ],
559 "isUnique": false511 "isUnique": false
560 }512 }
561 },513 },
562 "foreignKeys": {},514 "foreignKeys": {},
563 "compositePrimaryKeys": {515 "compositePrimaryKeys": {
564 "graphql_source_scope_id_id_pk": {516 "graphql_source_scope_id_id_pk": {
565 "columns": [517 "columns": ["scope_id", "id"],
566 "scope_id",
567 "id"
568 ],
569 "name": "graphql_source_scope_id_id_pk"518 "name": "graphql_source_scope_id_id_pk"
570 }519 }
571 },520 },
@@ -614,26 +563,19 @@ · 42 unmodified lines
614 "indexes": {563 "indexes": {
615 "mcp_binding_scope_id_idx": {564 "mcp_binding_scope_id_idx": {
616 "name": "mcp_binding_scope_id_idx",565 "name": "mcp_binding_scope_id_idx",
617 "columns": [566 "columns": ["scope_id"],
618 "scope_id"
619 ],
620 "isUnique": false567 "isUnique": false
621 },568 },
622 "mcp_binding_source_id_idx": {569 "mcp_binding_source_id_idx": {
623 "name": "mcp_binding_source_id_idx",570 "name": "mcp_binding_source_id_idx",
624 "columns": [571 "columns": ["source_id"],
625 "source_id"
626 ],
627 "isUnique": false572 "isUnique": false
628 }573 }
629 },574 },
630 "foreignKeys": {},575 "foreignKeys": {},
631 "compositePrimaryKeys": {576 "compositePrimaryKeys": {
632 "mcp_binding_scope_id_id_pk": {577 "mcp_binding_scope_id_id_pk": {
633 "columns": [578 "columns": ["scope_id", "id"],
634 "scope_id",
635 "id"
636 ],
637 "name": "mcp_binding_scope_id_id_pk"579 "name": "mcp_binding_scope_id_id_pk"
638 }580 }
639 },581 },
@@ -732,47 +674,34 @@ · 92 unmodified lines
732 "indexes": {674 "indexes": {
733 "mcp_source_scope_id_idx": {675 "mcp_source_scope_id_idx": {
734 "name": "mcp_source_scope_id_idx",676 "name": "mcp_source_scope_id_idx",
735 "columns": [677 "columns": ["scope_id"],
736 "scope_id"
737 ],
738 "isUnique": false678 "isUnique": false
739 },679 },
740 "mcp_source_auth_secret_id_idx": {680 "mcp_source_auth_secret_id_idx": {
741 "name": "mcp_source_auth_secret_id_idx",681 "name": "mcp_source_auth_secret_id_idx",
742 "columns": [682 "columns": ["auth_secret_id"],
743 "auth_secret_id"
744 ],
745 "isUnique": false683 "isUnique": false
746 },684 },
747 "mcp_source_auth_connection_id_idx": {685 "mcp_source_auth_connection_id_idx": {
748 "name": "mcp_source_auth_connection_id_idx",686 "name": "mcp_source_auth_connection_id_idx",
749 "columns": [687 "columns": ["auth_connection_id"],
750 "auth_connection_id"
751 ],
752 "isUnique": false688 "isUnique": false
753 },689 },
754 "mcp_source_auth_client_id_secret_id_idx": {690 "mcp_source_auth_client_id_secret_id_idx": {
755 "name": "mcp_source_auth_client_id_secret_id_idx",691 "name": "mcp_source_auth_client_id_secret_id_idx",
756 "columns": [692 "columns": ["auth_client_id_secret_id"],
757 "auth_client_id_secret_id"
758 ],
759 "isUnique": false693 "isUnique": false
760 },694 },
761 "mcp_source_auth_client_secret_secret_id_idx": {695 "mcp_source_auth_client_secret_secret_id_idx": {
762 "name": "mcp_source_auth_client_secret_secret_id_idx",696 "name": "mcp_source_auth_client_secret_secret_id_idx",
763 "columns": [697 "columns": ["auth_client_secret_secret_id"],
764 "auth_client_secret_secret_id"
765 ],
766 "isUnique": false698 "isUnique": false
767 }699 }
768 },700 },
769 "foreignKeys": {},701 "foreignKeys": {},
770 "compositePrimaryKeys": {702 "compositePrimaryKeys": {
771 "mcp_source_scope_id_id_pk": {703 "mcp_source_scope_id_id_pk": {
772 "columns": [704 "columns": ["scope_id", "id"],
773 "scope_id",
774 "id"
775 ],
776 "name": "mcp_source_scope_id_id_pk"705 "name": "mcp_source_scope_id_id_pk"
777 }706 }
778 },707 },
@@ -856,33 +785,24 @@ · 77 unmodified lines
856 "indexes": {785 "indexes": {
857 "oauth2_session_scope_id_idx": {786 "oauth2_session_scope_id_idx": {
858 "name": "oauth2_session_scope_id_idx",787 "name": "oauth2_session_scope_id_idx",
859 "columns": [788 "columns": ["scope_id"],
860 "scope_id"
861 ],
862 "isUnique": false789 "isUnique": false
863 },790 },
864 "oauth2_session_plugin_id_idx": {791 "oauth2_session_plugin_id_idx": {
865 "name": "oauth2_session_plugin_id_idx",792 "name": "oauth2_session_plugin_id_idx",
866 "columns": [793 "columns": ["plugin_id"],
867 "plugin_id"
868 ],
869 "isUnique": false794 "isUnique": false
870 },795 },
871 "oauth2_session_connection_id_idx": {796 "oauth2_session_connection_id_idx": {
872 "name": "oauth2_session_connection_id_idx",797 "name": "oauth2_session_connection_id_idx",
873 "columns": [798 "columns": ["connection_id"],
874 "connection_id"
875 ],
876 "isUnique": false799 "isUnique": false
877 }800 }
878 },801 },
879 "foreignKeys": {},802 "foreignKeys": {},
880 "compositePrimaryKeys": {803 "compositePrimaryKeys": {
881 "oauth2_session_scope_id_id_pk": {804 "oauth2_session_scope_id_id_pk": {
882 "columns": [805 "columns": ["scope_id", "id"],
883 "scope_id",
884 "id"
885 ],
886 "name": "oauth2_session_scope_id_id_pk"806 "name": "oauth2_session_scope_id_id_pk"
887 }807 }
888 },808 },
@@ -924,26 +844,19 @@ · 35 unmodified lines
924 "indexes": {844 "indexes": {
925 "openapi_operation_scope_id_idx": {845 "openapi_operation_scope_id_idx": {
926 "name": "openapi_operation_scope_id_idx",846 "name": "openapi_operation_scope_id_idx",
927 "columns": [847 "columns": ["scope_id"],
928 "scope_id"
929 ],
930 "isUnique": false848 "isUnique": false
931 },849 },
932 "openapi_operation_source_id_idx": {850 "openapi_operation_source_id_idx": {
933 "name": "openapi_operation_source_id_idx",851 "name": "openapi_operation_source_id_idx",
934 "columns": [852 "columns": ["source_id"],
935 "source_id"
936 ],
937 "isUnique": false853 "isUnique": false
938 }854 }
939 },855 },
940 "foreignKeys": {},856 "foreignKeys": {},
941 "compositePrimaryKeys": {857 "compositePrimaryKeys": {
942 "openapi_operation_scope_id_id_pk": {858 "openapi_operation_scope_id_id_pk": {
943 "columns": [859 "columns": ["scope_id", "id"],
944 "scope_id",
945 "id"
946 ],
947 "name": "openapi_operation_scope_id_id_pk"860 "name": "openapi_operation_scope_id_id_pk"
948 }861 }
949 },862 },
@@ -1013,19 +926,14 @@ · 63 unmodified lines
1013 "indexes": {926 "indexes": {
1014 "openapi_source_scope_id_idx": {927 "openapi_source_scope_id_idx": {
1015 "name": "openapi_source_scope_id_idx",928 "name": "openapi_source_scope_id_idx",
1016 "columns": [929 "columns": ["scope_id"],
1017 "scope_id"
1018 ],
1019 "isUnique": false930 "isUnique": false
1020 }931 }
1021 },932 },
1022 "foreignKeys": {},933 "foreignKeys": {},
1023 "compositePrimaryKeys": {934 "compositePrimaryKeys": {
1024 "openapi_source_scope_id_id_pk": {935 "openapi_source_scope_id_id_pk": {
1025 "columns": [936 "columns": ["scope_id", "id"],
1026 "scope_id",
1027 "id"
1028 ],
1029 "name": "openapi_source_scope_id_id_pk"937 "name": "openapi_source_scope_id_id_pk"
1030 }938 }
1031 },939 },
@@ -1117,44 +1025,32 @@ · 85 unmodified lines
1117 "indexes": {1025 "indexes": {
1118 "openapi_source_binding_source_id_idx": {1026 "openapi_source_binding_source_id_idx": {
1119 "name": "openapi_source_binding_source_id_idx",1027 "name": "openapi_source_binding_source_id_idx",
1120 "columns": [1028 "columns": ["source_id"],
1121 "source_id"
1122 ],
1123 "isUnique": false1029 "isUnique": false
1124 },1030 },
1125 "openapi_source_binding_source_scope_id_idx": {1031 "openapi_source_binding_source_scope_id_idx": {
1126 "name": "openapi_source_binding_source_scope_id_idx",1032 "name": "openapi_source_binding_source_scope_id_idx",
1127 "columns": [1033 "columns": ["source_scope_id"],
1128 "source_scope_id"
1129 ],
1130 "isUnique": false1034 "isUnique": false
1131 },1035 },
1132 "openapi_source_binding_target_scope_id_idx": {1036 "openapi_source_binding_target_scope_id_idx": {
1133 "name": "openapi_source_binding_target_scope_id_idx",1037 "name": "openapi_source_binding_target_scope_id_idx",
1134 "columns": [1038 "columns": ["target_scope_id"],
1135 "target_scope_id"
1136 ],
1137 "isUnique": false1039 "isUnique": false
1138 },1040 },
1139 "openapi_source_binding_slot_idx": {1041 "openapi_source_binding_slot_idx": {
1140 "name": "openapi_source_binding_slot_idx",1042 "name": "openapi_source_binding_slot_idx",
1141 "columns": [1043 "columns": ["slot"],
1142 "slot"
1143 ],
1144 "isUnique": false1044 "isUnique": false
1145 },1045 },
1146 "openapi_source_binding_secret_id_idx": {1046 "openapi_source_binding_secret_id_idx": {
1147 "name": "openapi_source_binding_secret_id_idx",1047 "name": "openapi_source_binding_secret_id_idx",
1148 "columns": [1048 "columns": ["secret_id"],
1149 "secret_id"
1150 ],
1151 "isUnique": false1049 "isUnique": false
1152 },1050 },
1153 "openapi_source_binding_connection_id_idx": {1051 "openapi_source_binding_connection_id_idx": {
1154 "name": "openapi_source_binding_connection_id_idx",1052 "name": "openapi_source_binding_connection_id_idx",
1155 "columns": [1053 "columns": ["connection_id"],
1156 "connection_id"
1157 ],
1158 "isUnique": false1054 "isUnique": false
1159 }1055 }
1160 },1056 },
@@ -1212,33 +1108,24 @@ · 51 unmodified lines
1212 "indexes": {1108 "indexes": {
1213 "secret_scope_id_idx": {1109 "secret_scope_id_idx": {
1214 "name": "secret_scope_id_idx",1110 "name": "secret_scope_id_idx",
1215 "columns": [1111 "columns": ["scope_id"],
1216 "scope_id"
1217 ],
1218 "isUnique": false1112 "isUnique": false
1219 },1113 },
1220 "secret_provider_idx": {1114 "secret_provider_idx": {
1221 "name": "secret_provider_idx",1115 "name": "secret_provider_idx",
1222 "columns": [1116 "columns": ["provider"],
1223 "provider"
1224 ],
1225 "isUnique": false1117 "isUnique": false
1226 },1118 },
1227 "secret_owned_by_connection_id_idx": {1119 "secret_owned_by_connection_id_idx": {
1228 "name": "secret_owned_by_connection_id_idx",1120 "name": "secret_owned_by_connection_id_idx",
1229 "columns": [1121 "columns": ["owned_by_connection_id"],
1230 "owned_by_connection_id"
1231 ],
1232 "isUnique": false1122 "isUnique": false
1233 }1123 }
1234 },1124 },
1235 "foreignKeys": {},1125 "foreignKeys": {},
1236 "compositePrimaryKeys": {1126 "compositePrimaryKeys": {
1237 "secret_scope_id_id_pk": {1127 "secret_scope_id_id_pk": {
1238 "columns": [1128 "columns": ["scope_id", "id"],
1239 "scope_id",
1240 "id"
1241 ],
1242 "name": "secret_scope_id_id_pk"1129 "name": "secret_scope_id_id_pk"
1243 }1130 }
1244 },1131 },
@@ -1332,26 +1219,19 @@ · 87 unmodified lines
1332 "indexes": {1219 "indexes": {
1333 "source_scope_id_idx": {1220 "source_scope_id_idx": {
1334 "name": "source_scope_id_idx",1221 "name": "source_scope_id_idx",
1335 "columns": [1222 "columns": ["scope_id"],
1336 "scope_id"
1337 ],
1338 "isUnique": false1223 "isUnique": false
1339 },1224 },
1340 "source_plugin_id_idx": {1225 "source_plugin_id_idx": {
1341 "name": "source_plugin_id_idx",1226 "name": "source_plugin_id_idx",
1342 "columns": [1227 "columns": ["plugin_id"],
1343 "plugin_id"
1344 ],
1345 "isUnique": false1228 "isUnique": false
1346 }1229 }
1347 },1230 },
1348 "foreignKeys": {},1231 "foreignKeys": {},
1349 "compositePrimaryKeys": {1232 "compositePrimaryKeys": {
1350 "source_scope_id_id_pk": {1233 "source_scope_id_id_pk": {
1351 "columns": [1234 "columns": ["scope_id", "id"],
1352 "scope_id",
1353 "id"
1354 ],
1355 "name": "source_scope_id_id_pk"1235 "name": "source_scope_id_id_pk"
1356 }1236 }
1357 },1237 },
@@ -1435,33 +1315,24 @@ · 77 unmodified lines
1435 "indexes": {1315 "indexes": {
1436 "tool_scope_id_idx": {1316 "tool_scope_id_idx": {
1437 "name": "tool_scope_id_idx",1317 "name": "tool_scope_id_idx",
1438 "columns": [1318 "columns": ["scope_id"],
1439 "scope_id"
1440 ],
1441 "isUnique": false1319 "isUnique": false
1442 },1320 },
1443 "tool_source_id_idx": {1321 "tool_source_id_idx": {
1444 "name": "tool_source_id_idx",1322 "name": "tool_source_id_idx",
1445 "columns": [1323 "columns": ["source_id"],
1446 "source_id"
1447 ],
1448 "isUnique": false1324 "isUnique": false
1449 },1325 },
1450 "tool_plugin_id_idx": {1326 "tool_plugin_id_idx": {
1451 "name": "tool_plugin_id_idx",1327 "name": "tool_plugin_id_idx",
1452 "columns": [1328 "columns": ["plugin_id"],
1453 "plugin_id"
1454 ],
1455 "isUnique": false1329 "isUnique": false
1456 }1330 }
1457 },1331 },
1458 "foreignKeys": {},1332 "foreignKeys": {},
1459 "compositePrimaryKeys": {1333 "compositePrimaryKeys": {
1460 "tool_scope_id_id_pk": {1334 "tool_scope_id_id_pk": {
1461 "columns": [1335 "columns": ["scope_id", "id"],
1462 "scope_id",
1463 "id"
1464 ],
1465 "name": "tool_scope_id_id_pk"1336 "name": "tool_scope_id_id_pk"
1466 }1337 }
1467 },1338 },
@@ -1524,20 +1395,14 @@ · 56 unmodified lines
1524 "indexes": {1395 "indexes": {
1525 "tool_policy_scope_id_position_idx": {1396 "tool_policy_scope_id_position_idx": {
1526 "name": "tool_policy_scope_id_position_idx",1397 "name": "tool_policy_scope_id_position_idx",
1527 "columns": [1398 "columns": ["scope_id", "position"],
1528 "scope_id",
1529 "position"
1530 ],
1531 "isUnique": false1399 "isUnique": false
1532 }1400 }
1533 },1401 },
1534 "foreignKeys": {},1402 "foreignKeys": {},
1535 "compositePrimaryKeys": {1403 "compositePrimaryKeys": {
1536 "tool_policy_scope_id_id_pk": {1404 "tool_policy_scope_id_id_pk": {
1537 "columns": [1405 "columns": ["scope_id", "id"],
1538 "scope_id",
1539 "id"
1540 ],
1541 "name": "tool_policy_scope_id_id_pk"1406 "name": "tool_policy_scope_id_id_pk"
1542 }1407 }
1543 },1408 },
@@ -1607,33 +1472,24 @@ · 63 unmodified lines
1607 "indexes": {1472 "indexes": {
1608 "graphql_source_header_scope_id_idx": {1473 "graphql_source_header_scope_id_idx": {
1609 "name": "graphql_source_header_scope_id_idx",1474 "name": "graphql_source_header_scope_id_idx",
1610 "columns": [1475 "columns": ["scope_id"],
1611 "scope_id"
1612 ],
1613 "isUnique": false1476 "isUnique": false
1614 },1477 },
1615 "graphql_source_header_source_id_idx": {1478 "graphql_source_header_source_id_idx": {
1616 "name": "graphql_source_header_source_id_idx",1479 "name": "graphql_source_header_source_id_idx",
1617 "columns": [1480 "columns": ["source_id"],
1618 "source_id"
1619 ],
1620 "isUnique": false1481 "isUnique": false
1621 },1482 },
1622 "graphql_source_header_secret_id_idx": {1483 "graphql_source_header_secret_id_idx": {
1623 "name": "graphql_source_header_secret_id_idx",1484 "name": "graphql_source_header_secret_id_idx",
1624 "columns": [1485 "columns": ["secret_id"],
1625 "secret_id"
1626 ],
1627 "isUnique": false1486 "isUnique": false
1628 }1487 }
1629 },1488 },
1630 "foreignKeys": {},1489 "foreignKeys": {},
1631 "compositePrimaryKeys": {1490 "compositePrimaryKeys": {
1632 "graphql_source_header_scope_id_id_pk": {1491 "graphql_source_header_scope_id_id_pk": {
1633 "columns": [1492 "columns": ["scope_id", "id"],
1634 "scope_id",
1635 "id"
1636 ],
1637 "name": "graphql_source_header_scope_id_id_pk"1493 "name": "graphql_source_header_scope_id_id_pk"
1638 }1494 }
1639 },1495 },
@@ -1703,33 +1559,24 @@ · 63 unmodified lines
1703 "indexes": {1559 "indexes": {
1704 "graphql_source_query_param_scope_id_idx": {1560 "graphql_source_query_param_scope_id_idx": {
1705 "name": "graphql_source_query_param_scope_id_idx",1561 "name": "graphql_source_query_param_scope_id_idx",
1706 "columns": [1562 "columns": ["scope_id"],
1707 "scope_id"
1708 ],
1709 "isUnique": false1563 "isUnique": false
1710 },1564 },
1711 "graphql_source_query_param_source_id_idx": {1565 "graphql_source_query_param_source_id_idx": {
1712 "name": "graphql_source_query_param_source_id_idx",1566 "name": "graphql_source_query_param_source_id_idx",
1713 "columns": [1567 "columns": ["source_id"],
1714 "source_id"
1715 ],
1716 "isUnique": false1568 "isUnique": false
1717 },1569 },
1718 "graphql_source_query_param_secret_id_idx": {1570 "graphql_source_query_param_secret_id_idx": {
1719 "name": "graphql_source_query_param_secret_id_idx",1571 "name": "graphql_source_query_param_secret_id_idx",
1720 "columns": [1572 "columns": ["secret_id"],
1721 "secret_id"
1722 ],
1723 "isUnique": false1573 "isUnique": false
1724 }1574 }
1725 },1575 },
1726 "foreignKeys": {},1576 "foreignKeys": {},
1727 "compositePrimaryKeys": {1577 "compositePrimaryKeys": {
1728 "graphql_source_query_param_scope_id_id_pk": {1578 "graphql_source_query_param_scope_id_id_pk": {
1729 "columns": [1579 "columns": ["scope_id", "id"],
1730 "scope_id",
1731 "id"
1732 ],
1733 "name": "graphql_source_query_param_scope_id_id_pk"1580 "name": "graphql_source_query_param_scope_id_id_pk"
1734 }1581 }
1735 },1582 },
@@ -1799,33 +1646,24 @@ · 63 unmodified lines
1799 "indexes": {1646 "indexes": {
1800 "openapi_source_query_param_scope_id_idx": {1647 "openapi_source_query_param_scope_id_idx": {
1801 "name": "openapi_source_query_param_scope_id_idx",1648 "name": "openapi_source_query_param_scope_id_idx",
1802 "columns": [1649 "columns": ["scope_id"],
1803 "scope_id"
1804 ],
1805 "isUnique": false1650 "isUnique": false
1806 },1651 },
1807 "openapi_source_query_param_source_id_idx": {1652 "openapi_source_query_param_source_id_idx": {
1808 "name": "openapi_source_query_param_source_id_idx",1653 "name": "openapi_source_query_param_source_id_idx",
1809 "columns": [1654 "columns": ["source_id"],
1810 "source_id"
1811 ],
1812 "isUnique": false1655 "isUnique": false
1813 },1656 },
1814 "openapi_source_query_param_secret_id_idx": {1657 "openapi_source_query_param_secret_id_idx": {
1815 "name": "openapi_source_query_param_secret_id_idx",1658 "name": "openapi_source_query_param_secret_id_idx",
1816 "columns": [1659 "columns": ["secret_id"],
1817 "secret_id"
1818 ],
1819 "isUnique": false1660 "isUnique": false
1820 }1661 }
1821 },1662 },
1822 "foreignKeys": {},1663 "foreignKeys": {},
1823 "compositePrimaryKeys": {1664 "compositePrimaryKeys": {
1824 "openapi_source_query_param_scope_id_id_pk": {1665 "openapi_source_query_param_scope_id_id_pk": {
1825 "columns": [1666 "columns": ["scope_id", "id"],
1826 "scope_id",
1827 "id"
1828 ],
1829 "name": "openapi_source_query_param_scope_id_id_pk"1667 "name": "openapi_source_query_param_scope_id_id_pk"
1830 }1668 }
1831 },1669 },
@@ -1895,33 +1733,24 @@ · 63 unmodified lines
1895 "indexes": {1733 "indexes": {
1896 "openapi_source_spec_fetch_header_scope_id_idx": {1734 "openapi_source_spec_fetch_header_scope_id_idx": {
1897 "name": "openapi_source_spec_fetch_header_scope_id_idx",1735 "name": "openapi_source_spec_fetch_header_scope_id_idx",
1898 "columns": [1736 "columns": ["scope_id"],
1899 "scope_id"
1900 ],
1901 "isUnique": false1737 "isUnique": false
1902 },1738 },
1903 "openapi_source_spec_fetch_header_source_id_idx": {1739 "openapi_source_spec_fetch_header_source_id_idx": {
1904 "name": "openapi_source_spec_fetch_header_source_id_idx",1740 "name": "openapi_source_spec_fetch_header_source_id_idx",
1905 "columns": [1741 "columns": ["source_id"],
1906 "source_id"
1907 ],
1908 "isUnique": false1742 "isUnique": false
1909 },1743 },
1910 "openapi_source_spec_fetch_header_secret_id_idx": {1744 "openapi_source_spec_fetch_header_secret_id_idx": {
1911 "name": "openapi_source_spec_fetch_header_secret_id_idx",1745 "name": "openapi_source_spec_fetch_header_secret_id_idx",
1912 "columns": [1746 "columns": ["secret_id"],
1913 "secret_id"
1914 ],
1915 "isUnique": false1747 "isUnique": false
1916 }1748 }
1917 },1749 },
1918 "foreignKeys": {},1750 "foreignKeys": {},
1919 "compositePrimaryKeys": {1751 "compositePrimaryKeys": {
1920 "openapi_source_spec_fetch_header_scope_id_id_pk": {1752 "openapi_source_spec_fetch_header_scope_id_id_pk": {
1921 "columns": [1753 "columns": ["scope_id", "id"],
1922 "scope_id",
1923 "id"
1924 ],
1925 "name": "openapi_source_spec_fetch_header_scope_id_id_pk"1754 "name": "openapi_source_spec_fetch_header_scope_id_id_pk"
1926 }1755 }
1927 },1756 },
@@ -1991,33 +1820,24 @@ · 63 unmodified lines
1991 "indexes": {1820 "indexes": {
1992 "openapi_source_spec_fetch_query_param_scope_id_idx": {1821 "openapi_source_spec_fetch_query_param_scope_id_idx": {
1993 "name": "openapi_source_spec_fetch_query_param_scope_id_idx",1822 "name": "openapi_source_spec_fetch_query_param_scope_id_idx",
1994 "columns": [1823 "columns": ["scope_id"],
1995 "scope_id"
1996 ],
1997 "isUnique": false1824 "isUnique": false
1998 },1825 },
1999 "openapi_source_spec_fetch_query_param_source_id_idx": {1826 "openapi_source_spec_fetch_query_param_source_id_idx": {
2000 "name": "openapi_source_spec_fetch_query_param_source_id_idx",1827 "name": "openapi_source_spec_fetch_query_param_source_id_idx",
2001 "columns": [1828 "columns": ["source_id"],
2002 "source_id"
2003 ],
2004 "isUnique": false1829 "isUnique": false
2005 },1830 },
2006 "openapi_source_spec_fetch_query_param_secret_id_idx": {1831 "openapi_source_spec_fetch_query_param_secret_id_idx": {
2007 "name": "openapi_source_spec_fetch_query_param_secret_id_idx",1832 "name": "openapi_source_spec_fetch_query_param_secret_id_idx",
2008 "columns": [1833 "columns": ["secret_id"],
2009 "secret_id"
2010 ],
2011 "isUnique": false1834 "isUnique": false
2012 }1835 }
2013 },1836 },
2014 "foreignKeys": {},1837 "foreignKeys": {},
2015 "compositePrimaryKeys": {1838 "compositePrimaryKeys": {
2016 "openapi_source_spec_fetch_query_param_scope_id_id_pk": {1839 "openapi_source_spec_fetch_query_param_scope_id_id_pk": {
2017 "columns": [1840 "columns": ["scope_id", "id"],
2018 "scope_id",
2019 "id"
2020 ],
2021 "name": "openapi_source_spec_fetch_query_param_scope_id_id_pk"1841 "name": "openapi_source_spec_fetch_query_param_scope_id_id_pk"
2022 }1842 }
2023 },1843 },
@@ -2087,33 +1907,24 @@ · 63 unmodified lines
2087 "indexes": {1907 "indexes": {
2088 "mcp_source_header_scope_id_idx": {1908 "mcp_source_header_scope_id_idx": {
2089 "name": "mcp_source_header_scope_id_idx",1909 "name": "mcp_source_header_scope_id_idx",
2090 "columns": [1910 "columns": ["scope_id"],
2091 "scope_id"
2092 ],
2093 "isUnique": false1911 "isUnique": false
2094 },1912 },
2095 "mcp_source_header_source_id_idx": {1913 "mcp_source_header_source_id_idx": {
2096 "name": "mcp_source_header_source_id_idx",1914 "name": "mcp_source_header_source_id_idx",
2097 "columns": [1915 "columns": ["source_id"],
2098 "source_id"
2099 ],
2100 "isUnique": false1916 "isUnique": false
2101 },1917 },
2102 "mcp_source_header_secret_id_idx": {1918 "mcp_source_header_secret_id_idx": {
2103 "name": "mcp_source_header_secret_id_idx",1919 "name": "mcp_source_header_secret_id_idx",
2104 "columns": [1920 "columns": ["secret_id"],
2105 "secret_id"
2106 ],
2107 "isUnique": false1921 "isUnique": false
2108 }1922 }
2109 },1923 },
2110 "foreignKeys": {},1924 "foreignKeys": {},
2111 "compositePrimaryKeys": {1925 "compositePrimaryKeys": {
2112 "mcp_source_header_scope_id_id_pk": {1926 "mcp_source_header_scope_id_id_pk": {
2113 "columns": [1927 "columns": ["scope_id", "id"],
2114 "scope_id",
2115 "id"
2116 ],
2117 "name": "mcp_source_header_scope_id_id_pk"1928 "name": "mcp_source_header_scope_id_id_pk"
2118 }1929 }
2119 },1930 },
@@ -2183,33 +1994,24 @@ · 63 unmodified lines
2183 "indexes": {1994 "indexes": {
2184 "mcp_source_query_param_scope_id_idx": {1995 "mcp_source_query_param_scope_id_idx": {
2185 "name": "mcp_source_query_param_scope_id_idx",1996 "name": "mcp_source_query_param_scope_id_idx",
2186 "columns": [1997 "columns": ["scope_id"],
2187 "scope_id"
2188 ],
2189 "isUnique": false1998 "isUnique": false
2190 },1999 },
2191 "mcp_source_query_param_source_id_idx": {2000 "mcp_source_query_param_source_id_idx": {
2192 "name": "mcp_source_query_param_source_id_idx",2001 "name": "mcp_source_query_param_source_id_idx",
2193 "columns": [2002 "columns": ["source_id"],
2194 "source_id"
2195 ],
2196 "isUnique": false2003 "isUnique": false
2197 },2004 },
2198 "mcp_source_query_param_secret_id_idx": {2005 "mcp_source_query_param_secret_id_idx": {
2199 "name": "mcp_source_query_param_secret_id_idx",2006 "name": "mcp_source_query_param_secret_id_idx",
2200 "columns": [2007 "columns": ["secret_id"],
2201 "secret_id"
2202 ],
2203 "isUnique": false2008 "isUnique": false
2204 }2009 }
2205 },2010 },
2206 "foreignKeys": {},2011 "foreignKeys": {},
2207 "compositePrimaryKeys": {2012 "compositePrimaryKeys": {
2208 "mcp_source_query_param_scope_id_id_pk": {2013 "mcp_source_query_param_scope_id_id_pk": {
2209 "columns": [2014 "columns": ["scope_id", "id"],
2210 "scope_id",
2211 "id"
2212 ],
2213 "name": "mcp_source_query_param_scope_id_id_pk"2015 "name": "mcp_source_query_param_scope_id_id_pk"
2214 }2016 }
2215 },2017 },
@@ -2279,33 +2081,24 @@ · 63 unmodified lines
2279 "indexes": {2081 "indexes": {
2280 "google_discovery_source_credential_header_scope_id_idx": {2082 "google_discovery_source_credential_header_scope_id_idx": {
2281 "name": "google_discovery_source_credential_header_scope_id_idx",2083 "name": "google_discovery_source_credential_header_scope_id_idx",
2282 "columns": [2084 "columns": ["scope_id"],
2283 "scope_id"
2284 ],
2285 "isUnique": false2085 "isUnique": false
2286 },2086 },
2287 "google_discovery_source_credential_header_source_id_idx": {2087 "google_discovery_source_credential_header_source_id_idx": {
2288 "name": "google_discovery_source_credential_header_source_id_idx",2088 "name": "google_discovery_source_credential_header_source_id_idx",
2289 "columns": [2089 "columns": ["source_id"],
2290 "source_id"
2291 ],
2292 "isUnique": false2090 "isUnique": false
2293 },2091 },
2294 "google_discovery_source_credential_header_secret_id_idx": {2092 "google_discovery_source_credential_header_secret_id_idx": {
2295 "name": "google_discovery_source_credential_header_secret_id_idx",2093 "name": "google_discovery_source_credential_header_secret_id_idx",
2296 "columns": [2094 "columns": ["secret_id"],
2297 "secret_id"
2298 ],
2299 "isUnique": false2095 "isUnique": false
2300 }2096 }
2301 },2097 },
2302 "foreignKeys": {},2098 "foreignKeys": {},
2303 "compositePrimaryKeys": {2099 "compositePrimaryKeys": {
2304 "google_discovery_source_credential_header_scope_id_id_pk": {2100 "google_discovery_source_credential_header_scope_id_id_pk": {
2305 "columns": [2101 "columns": ["scope_id", "id"],
2306 "scope_id",
2307 "id"
2308 ],
2309 "name": "google_discovery_source_credential_header_scope_id_id_pk"2102 "name": "google_discovery_source_credential_header_scope_id_id_pk"
2310 }2103 }
2311 },2104 },
@@ -2375,33 +2168,24 @@ · 63 unmodified lines
2375 "indexes": {2168 "indexes": {
2376 "google_discovery_source_credential_query_param_scope_id_idx": {2169 "google_discovery_source_credential_query_param_scope_id_idx": {
2377 "name": "google_discovery_source_credential_query_param_scope_id_idx",2170 "name": "google_discovery_source_credential_query_param_scope_id_idx",
2378 "columns": [2171 "columns": ["scope_id"],
2379 "scope_id"
2380 ],
2381 "isUnique": false2172 "isUnique": false
2382 },2173 },
2383 "google_discovery_source_credential_query_param_source_id_idx": {2174 "google_discovery_source_credential_query_param_source_id_idx": {
2384 "name": "google_discovery_source_credential_query_param_source_id_idx",2175 "name": "google_discovery_source_credential_query_param_source_id_idx",
2385 "columns": [2176 "columns": ["source_id"],
2386 "source_id"
2387 ],
2388 "isUnique": false2177 "isUnique": false
2389 },2178 },
2390 "google_discovery_source_credential_query_param_secret_id_idx": {2179 "google_discovery_source_credential_query_param_secret_id_idx": {
2391 "name": "google_discovery_source_credential_query_param_secret_id_idx",2180 "name": "google_discovery_source_credential_query_param_secret_id_idx",
2392 "columns": [2181 "columns": ["secret_id"],
2393 "secret_id"
2394 ],
2395 "isUnique": false2182 "isUnique": false
2396 }2183 }
2397 },2184 },
2398 "foreignKeys": {},2185 "foreignKeys": {},
2399 "compositePrimaryKeys": {2186 "compositePrimaryKeys": {
2400 "google_discovery_source_credential_query_param_scope_id_id_pk": {2187 "google_discovery_source_credential_query_param_scope_id_id_pk": {
2401 "columns": [2188 "columns": ["scope_id", "id"],
2402 "scope_id",
2403 "id"
2404 ],
2405 "name": "google_discovery_source_credential_query_param_scope_id_id_pk"2189 "name": "google_discovery_source_credential_query_param_scope_id_id_pk"
2406 }2190 }
2407 },2191 },
apps/local/src/serve.test.ts+4−1
@@ -30,7 +30,10 @@ const startTestServer = async (): Promise<string> => { · 29 unmodified lines
30beforeEach(() => {30beforeEach(() => {
31 clientDir = mkdtempSync(join(tmpdir(), "exec-local-serve-"));31 clientDir = mkdtempSync(join(tmpdir(), "exec-local-serve-"));
32 mkdirSync(join(clientDir, "assets"), { recursive: true });32 mkdirSync(join(clientDir, "assets"), { recursive: true });
33 writeFileSync(join(clientDir, "index.html"), "<!doctype html><html><body>index-shell</body></html>");33 writeFileSync(
34 join(clientDir, "index.html"),
35 "<!doctype html><html><body>index-shell</body></html>",
36 );
34 writeFileSync(join(clientDir, "assets", "app.js"), "console.log('ok')");37 writeFileSync(join(clientDir, "assets", "app.js"), "console.log('ok')");
35});38});
36 39
apps/local/src/serve.ts+8−6
@@ -17,12 +17,14 @@ import { getServerHandlers } from "./server/main"; · 16 unmodified lines
17 17
18const DEFAULT_ALLOWED_HOSTS = ["localhost", "127.0.0.1", "[::1]", "::1"];18const DEFAULT_ALLOWED_HOSTS = ["localhost", "127.0.0.1", "[::1]", "::1"];
19 19
20const makeIsAllowedHost = (allowed: ReadonlySet<string>) => (request: Request): boolean => {20const makeIsAllowedHost =
21 const host = request.headers.get("host");21 (allowed: ReadonlySet<string>) =>
22 if (!host) return true;22 (request: Request): boolean => {
23 const hostname = host.replace(/:\d+$/, "");23 const host = request.headers.get("host");
24 return allowed.has(hostname);24 if (!host) return true;
25};25 const hostname = host.replace(/:\d+$/, "");
26 return allowed.has(hostname);
27 };
26 28
27// ---------------------------------------------------------------------------29// ---------------------------------------------------------------------------
28// Static files30// Static files
apps/local/src/server/config-sync.ts+46−50
@@ -10,11 +10,7 @@ import { join } from "node:path"; · 9 unmodified lines
10import * as fs from "node:fs";10import * as fs from "node:fs";
11import * as jsonc from "jsonc-parser";11import * as jsonc from "jsonc-parser";
12 12
13import type {13import type { SourceConfig, ExecutorFileConfig, ConfigHeaderValue } from "@executor-js/config";
14 SourceConfig,
15 ExecutorFileConfig,
16 ConfigHeaderValue,
17} from "@executor-js/config";
18import { SECRET_REF_PREFIX } from "@executor-js/config";14import { SECRET_REF_PREFIX } from "@executor-js/config";
19 15
20import type { LocalExecutor } from "./executor";16import type { LocalExecutor } from "./executor";
@@ -57,8 +53,7 @@ const translateHeaders = ( · 36 unmodified lines
57// Config path resolution53// Config path resolution
58// ---------------------------------------------------------------------------54// ---------------------------------------------------------------------------
59 55
60export const resolveConfigPath = (scopeDir: string): string =>56export const resolveConfigPath = (scopeDir: string): string => join(scopeDir, "executor.jsonc");
61 join(scopeDir, "executor.jsonc");
62 57
63// ---------------------------------------------------------------------------58// ---------------------------------------------------------------------------
64// Load config (sync, no Effect deps — runs at startup)59// Load config (sync, no Effect deps — runs at startup)
@@ -91,45 +86,53 @@ const addSourceFromConfig = ( · 26 unmodified lines
91 const scope = executor.scopes.at(-1)!.id;86 const scope = executor.scopes.at(-1)!.id;
92 switch (source.kind) {87 switch (source.kind) {
93 case "openapi":88 case "openapi":
94 return executor.openapi.addSpec({89 return executor.openapi
95 spec: source.spec,90 .addSpec({
96 scope,91 spec: source.spec,
97 baseUrl: source.baseUrl,92 scope,
98 namespace: source.namespace,93 baseUrl: source.baseUrl,
99 headers: translateHeaders(source.headers),94 namespace: source.namespace,
100 }).pipe(Effect.asVoid);95 headers: translateHeaders(source.headers),
96 })
97 .pipe(Effect.asVoid);
101 98
102 case "graphql":99 case "graphql":
103 return executor.graphql.addSource({100 return executor.graphql
104 endpoint: source.endpoint,101 .addSource({
105 scope,102 endpoint: source.endpoint,
106 namespace: source.namespace,103 scope,
107 headers: translateHeaders(source.headers) as Record<string, string> | undefined,104 namespace: source.namespace,
108 }).pipe(Effect.asVoid);105 headers: translateHeaders(source.headers) as Record<string, string> | undefined,
106 })
107 .pipe(Effect.asVoid);
109 108
110 case "mcp":109 case "mcp":
111 if (source.transport === "stdio") {110 if (source.transport === "stdio") {
112 return executor.mcp.addSource({111 return executor.mcp
113 transport: "stdio",112 .addSource({
113 transport: "stdio",
114 scope,
115 name: source.name,
116 command: source.command,
117 args: source.args ? [...source.args] : undefined,
118 env: source.env,
119 cwd: source.cwd,
120 namespace: source.namespace,
121 })
122 .pipe(Effect.asVoid);
123 }
124 return executor.mcp
125 .addSource({
126 transport: "remote",
114 scope,127 scope,
115 name: source.name,128 name: source.name,
116 command: source.command,129 endpoint: source.endpoint,
117 args: source.args ? [...source.args] : undefined,130 remoteTransport: source.remoteTransport,
118 env: source.env,131 queryParams: source.queryParams,
119 cwd: source.cwd,132 headers: source.headers,
120 namespace: source.namespace,133 namespace: source.namespace,
121 }).pipe(Effect.asVoid);134 })
122 }135 .pipe(Effect.asVoid);
123 return executor.mcp.addSource({
124 transport: "remote",
125 scope,
126 name: source.name,
127 endpoint: source.endpoint,
128 remoteTransport: source.remoteTransport,
129 queryParams: source.queryParams,
130 headers: source.headers,
131 namespace: source.namespace,
132 }).pipe(Effect.asVoid);
133 }136 }
134};137};
135 138
@@ -137,10 +140,7 @@ const addSourceFromConfig = ( · 1 unmodified line
137 * Read executor.jsonc and replay all sources into the executor.140 * Read executor.jsonc and replay all sources into the executor.
138 * Each source is added independently — if one fails, the rest still load.141 * Each source is added independently — if one fails, the rest still load.
139 */142 */
140export const syncFromConfig = (143export const syncFromConfig = (executor: LocalExecutor, configPath: string): Effect.Effect<void> =>
141 executor: LocalExecutor,
142 configPath: string,
143): Effect.Effect<void> =>
144 Effect.gen(function* () {144 Effect.gen(function* () {
145 const config = loadConfigSync(configPath);145 const config = loadConfigSync(configPath);
146 if (!config?.sources?.length) {146 if (!config?.sources?.length) {
@@ -148,9 +148,7 @@ export const syncFromConfig = ( · 1 unmodified line
148 return;148 return;
149 }149 }
150 150
151 console.log(151 console.log(`[config-sync] syncing ${config.sources.length} source(s) from ${configPath}`);
152 `[config-sync] syncing ${config.sources.length} source(s) from ${configPath}`,
153 );
154 152
155 const results = yield* Effect.forEach(153 const results = yield* Effect.forEach(
156 config.sources,154 config.sources,
@@ -158,11 +156,9 @@ export const syncFromConfig = ( · 1 unmodified line
158 addSourceFromConfig(executor, source).pipe(156 addSourceFromConfig(executor, source).pipe(
159 Effect.map(() => true as const),157 Effect.map(() => true as const),
160 Effect.catchCause((cause) => {158 Effect.catchCause((cause) => {
161 const ns = "namespace" in source ? source.namespace : ("name" in source ? source.name : "unknown");159 const ns =
162 console.warn(160 "namespace" in source ? source.namespace : "name" in source ? source.name : "unknown";
163 `[config-sync] Failed to load source "${ns}":`,161 console.warn(`[config-sync] Failed to load source "${ns}":`, Cause.pretty(cause));
164 Cause.pretty(cause),
165 );
166 return Effect.succeed(false as const);162 return Effect.succeed(false as const);
167 }),163 }),
168 ),164 ),
apps/local/src/server/db-upgrade.test.ts+16−12
@@ -156,9 +156,9 @@ describe("move-aside + fresh migrate end-to-end", () => { · 155 unmodified lines
156 migrationsFolder: join(__dirname, "../../drizzle"),156 migrationsFolder: join(__dirname, "../../drizzle"),
157 });157 });
158 // migrate() should have produced the new schema — source now has scope_id.158 // migrate() should have produced the new schema — source now has scope_id.
159 const cols = db159 const cols = db.prepare("PRAGMA table_info('source')").all() as ReadonlyArray<{
160 .prepare("PRAGMA table_info('source')")160 readonly name: string;
161 .all() as ReadonlyArray<{ readonly name: string }>;161 }>;
162 expect(cols.some((c) => c.name === "scope_id")).toBe(true);162 expect(cols.some((c) => c.name === "scope_id")).toBe(true);
163 db.close();163 db.close();
164 });164 });
@@ -169,12 +169,18 @@ describe("readLegacySecrets", () => { · 4 unmodified lines
169 const path = join(workDir, "data.db");169 const path = join(workDir, "data.db");
170 seed(path, PRE_SCOPE_SCHEMA);170 seed(path, PRE_SCOPE_SCHEMA);
171 const db = new Database(path);171 const db = new Database(path);
172 db.prepare(172 db.prepare("INSERT INTO secret (id, name, provider, created_at) VALUES (?, ?, ?, ?)").run(
173 "INSERT INTO secret (id, name, provider, created_at) VALUES (?, ?, ?, ?)",173 "sec_1",
174 ).run("sec_1", "GitHub Token", "onepassword", 1700000000);174 "GitHub Token",
175 db.prepare(175 "onepassword",
176 "INSERT INTO secret (id, name, provider, created_at) VALUES (?, ?, ?, ?)",176 1700000000,
177 ).run("sec_2", "Stripe", "keychain", 1700000001);177 );
178 db.prepare("INSERT INTO secret (id, name, provider, created_at) VALUES (?, ?, ?, ?)").run(
179 "sec_2",
180 "Stripe",
181 "keychain",
182 1700000001,
183 );
178 db.close();184 db.close();
179 185
180 const rows = readLegacySecrets(path);186 const rows = readLegacySecrets(path);
@@ -248,9 +254,7 @@ describe("importLegacySecrets", () => { · 67 unmodified lines
248 it("uses INSERT OR IGNORE so a second import of the same ids is a no-op", () => {254 it("uses INSERT OR IGNORE so a second import of the same ids is a no-op", () => {
249 const path = join(workDir, "data.db");255 const path = join(workDir, "data.db");
250 const db = createScopedDb(path);256 const db = createScopedDb(path);
251 const rows = [257 const rows = [{ id: "sec_1", name: "GH", provider: "onepassword", createdAt: 1 }];
252 { id: "sec_1", name: "GH", provider: "onepassword", createdAt: 1 },
253 ];
254 importLegacySecrets(db, "scope_a", rows);258 importLegacySecrets(db, "scope_a", rows);
255 // If the user's already re-registered the secret via a different259 // If the user's already re-registered the secret via a different
256 // provider, the legacy row must NOT clobber it.260 // provider, the legacy row must NOT clobber it.
apps/local/src/server/db-upgrade.ts+3−3
@@ -27,9 +27,9 @@ export const isPreScopeSchema = (dbPath: string): boolean => { · 26 unmodified lines
27 .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='source'")27 .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='source'")
28 .get();28 .get();
29 if (!tableExists) return false;29 if (!tableExists) return false;
30 const columns = db30 const columns = db.prepare("PRAGMA table_info('source')").all() as ReadonlyArray<{
31 .prepare("PRAGMA table_info('source')")31 readonly name: string;
32 .all() as ReadonlyArray<{ readonly name: string }>;32 }>;
33 return !columns.some((c) => c.name === "scope_id");33 return !columns.some((c) => c.name === "scope_id");
34 } finally {34 } finally {
35 db.close();35 db.close();
apps/local/src/server/executor-schema.ts+479−370
@@ -1,396 +1,505 @@
1import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core";1import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core";
2 2
3export const source = sqliteTable("source", {3export const source = sqliteTable(
4 id: text('id').notNull(),4 "source",
5 scope_id: text('scope_id').notNull(),5 {
6 plugin_id: text('plugin_id').notNull(),6 id: text("id").notNull(),
7 kind: text('kind').notNull(),7 scope_id: text("scope_id").notNull(),
8 name: text('name').notNull(),8 plugin_id: text("plugin_id").notNull(),
9 url: text('url'),9 kind: text("kind").notNull(),
10 can_remove: integer('can_remove', { mode: 'boolean' }).default(true).notNull(),10 name: text("name").notNull(),
11 can_refresh: integer('can_refresh', { mode: 'boolean' }).default(false).notNull(),11 url: text("url"),
12 can_edit: integer('can_edit', { mode: 'boolean' }).default(false).notNull(),12 can_remove: integer("can_remove", { mode: "boolean" }).default(true).notNull(),
13 created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(),13 can_refresh: integer("can_refresh", { mode: "boolean" }).default(false).notNull(),
14 updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull()14 can_edit: integer("can_edit", { mode: "boolean" }).default(false).notNull(),
15}, (table) => [15 created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(),
16 primaryKey({ columns: [table.scope_id, table.id] }),16 updated_at: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
17 index("source_scope_id_idx").on(table.scope_id),17 },
18 index("source_plugin_id_idx").on(table.plugin_id),18 (table) => [
19]);19 primaryKey({ columns: [table.scope_id, table.id] }),
20 index("source_scope_id_idx").on(table.scope_id),
21 index("source_plugin_id_idx").on(table.plugin_id),
22 ],
23);
20 24
21export const tool = sqliteTable("tool", {25export const tool = sqliteTable(
22 id: text('id').notNull(),26 "tool",
23 scope_id: text('scope_id').notNull(),27 {
24 source_id: text('source_id').notNull(),28 id: text("id").notNull(),
25 plugin_id: text('plugin_id').notNull(),29 scope_id: text("scope_id").notNull(),
26 name: text('name').notNull(),30 source_id: text("source_id").notNull(),
27 description: text('description').notNull(),31 plugin_id: text("plugin_id").notNull(),
28 input_schema: text('input_schema', { mode: "json" }),32 name: text("name").notNull(),
29 output_schema: text('output_schema', { mode: "json" }),33 description: text("description").notNull(),
30 created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(),34 input_schema: text("input_schema", { mode: "json" }),
31 updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull()35 output_schema: text("output_schema", { mode: "json" }),
32}, (table) => [36 created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(),
33 primaryKey({ columns: [table.scope_id, table.id] }),37 updated_at: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
34 index("tool_scope_id_idx").on(table.scope_id),38 },
35 index("tool_source_id_idx").on(table.source_id),39 (table) => [
36 index("tool_plugin_id_idx").on(table.plugin_id),40 primaryKey({ columns: [table.scope_id, table.id] }),
37]);41 index("tool_scope_id_idx").on(table.scope_id),
42 index("tool_source_id_idx").on(table.source_id),
43 index("tool_plugin_id_idx").on(table.plugin_id),
44 ],
45);
38 46
39export const definition = sqliteTable("definition", {47export const definition = sqliteTable(
40 id: text('id').notNull(),48 "definition",
41 scope_id: text('scope_id').notNull(),49 {
42 source_id: text('source_id').notNull(),50 id: text("id").notNull(),
43 plugin_id: text('plugin_id').notNull(),51 scope_id: text("scope_id").notNull(),
44 name: text('name').notNull(),52 source_id: text("source_id").notNull(),
45 schema: text('schema', { mode: "json" }).notNull(),53 plugin_id: text("plugin_id").notNull(),
46 created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull()54 name: text("name").notNull(),
47}, (table) => [55 schema: text("schema", { mode: "json" }).notNull(),
48 primaryKey({ columns: [table.scope_id, table.id] }),56 created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(),
49 index("definition_scope_id_idx").on(table.scope_id),57 },
50 index("definition_source_id_idx").on(table.source_id),58 (table) => [
51 index("definition_plugin_id_idx").on(table.plugin_id),59 primaryKey({ columns: [table.scope_id, table.id] }),
52]);60 index("definition_scope_id_idx").on(table.scope_id),
61 index("definition_source_id_idx").on(table.source_id),
62 index("definition_plugin_id_idx").on(table.plugin_id),
63 ],
64);
53 65
54export const secret = sqliteTable("secret", {66export const secret = sqliteTable(
55 id: text('id').notNull(),67 "secret",
56 scope_id: text('scope_id').notNull(),68 {
57 name: text('name').notNull(),69 id: text("id").notNull(),
58 provider: text('provider').notNull(),70 scope_id: text("scope_id").notNull(),
59 owned_by_connection_id: text('owned_by_connection_id'),71 name: text("name").notNull(),
60 created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull()72 provider: text("provider").notNull(),
61}, (table) => [73 owned_by_connection_id: text("owned_by_connection_id"),
62 primaryKey({ columns: [table.scope_id, table.id] }),74 created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(),
63 index("secret_scope_id_idx").on(table.scope_id),75 },
64 index("secret_provider_idx").on(table.provider),76 (table) => [
65 index("secret_owned_by_connection_id_idx").on(table.owned_by_connection_id),77 primaryKey({ columns: [table.scope_id, table.id] }),
66]);78 index("secret_scope_id_idx").on(table.scope_id),
79 index("secret_provider_idx").on(table.provider),
80 index("secret_owned_by_connection_id_idx").on(table.owned_by_connection_id),
81 ],
82);
67 83
68export const connection = sqliteTable("connection", {84export const connection = sqliteTable(
69 id: text('id').notNull(),85 "connection",
70 scope_id: text('scope_id').notNull(),86 {
71 provider: text('provider').notNull(),87 id: text("id").notNull(),
72 identity_label: text('identity_label'),88 scope_id: text("scope_id").notNull(),
73 access_token_secret_id: text('access_token_secret_id').notNull(),89 provider: text("provider").notNull(),
74 refresh_token_secret_id: text('refresh_token_secret_id'),90 identity_label: text("identity_label"),
75 expires_at: integer('expires_at'),91 access_token_secret_id: text("access_token_secret_id").notNull(),
76 scope: text('scope'),92 refresh_token_secret_id: text("refresh_token_secret_id"),
77 provider_state: text('provider_state', { mode: "json" }),93 expires_at: integer("expires_at"),
78 created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(),94 scope: text("scope"),
79 updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull()95 provider_state: text("provider_state", { mode: "json" }),
80}, (table) => [96 created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(),
81 primaryKey({ columns: [table.scope_id, table.id] }),97 updated_at: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
82 index("connection_scope_id_idx").on(table.scope_id),98 },
83 index("connection_provider_idx").on(table.provider),99 (table) => [
84]);100 primaryKey({ columns: [table.scope_id, table.id] }),
101 index("connection_scope_id_idx").on(table.scope_id),
102 index("connection_provider_idx").on(table.provider),
103 ],
104);
85 105
86export const oauth2_session = sqliteTable("oauth2_session", {106export const oauth2_session = sqliteTable(
87 id: text('id').notNull(),107 "oauth2_session",
88 scope_id: text('scope_id').notNull(),108 {
89 plugin_id: text('plugin_id').notNull(),109 id: text("id").notNull(),
90 strategy: text('strategy').notNull(),110 scope_id: text("scope_id").notNull(),
91 connection_id: text('connection_id').notNull(),111 plugin_id: text("plugin_id").notNull(),
92 token_scope: text('token_scope').notNull(),112 strategy: text("strategy").notNull(),
93 redirect_url: text('redirect_url').notNull(),113 connection_id: text("connection_id").notNull(),
94 payload: text('payload', { mode: "json" }).notNull(),114 token_scope: text("token_scope").notNull(),
95 expires_at: integer('expires_at').notNull(),115 redirect_url: text("redirect_url").notNull(),
96 created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull()116 payload: text("payload", { mode: "json" }).notNull(),
97}, (table) => [117 expires_at: integer("expires_at").notNull(),
98 primaryKey({ columns: [table.scope_id, table.id] }),118 created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(),
99 index("oauth2_session_scope_id_idx").on(table.scope_id),119 },
100 index("oauth2_session_plugin_id_idx").on(table.plugin_id),120 (table) => [
101 index("oauth2_session_connection_id_idx").on(table.connection_id),121 primaryKey({ columns: [table.scope_id, table.id] }),
102]);122 index("oauth2_session_scope_id_idx").on(table.scope_id),
123 index("oauth2_session_plugin_id_idx").on(table.plugin_id),
124 index("oauth2_session_connection_id_idx").on(table.connection_id),
125 ],
126);
103 127
104export const tool_policy = sqliteTable("tool_policy", {128export const tool_policy = sqliteTable(
105 id: text('id').notNull(),129 "tool_policy",
106 scope_id: text('scope_id').notNull(),130 {
107 pattern: text('pattern').notNull(),131 id: text("id").notNull(),
108 action: text('action').notNull(),132 scope_id: text("scope_id").notNull(),
109 position: text('position').notNull(),133 pattern: text("pattern").notNull(),
110 created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(),134 action: text("action").notNull(),
111 updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull()135 position: text("position").notNull(),
112}, (table) => [136 created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(),
113 primaryKey({ columns: [table.scope_id, table.id] }),137 updated_at: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
114 index("tool_policy_scope_id_position_idx").on(table.scope_id, table.position),138 },
115]);139 (table) => [
140 primaryKey({ columns: [table.scope_id, table.id] }),
141 index("tool_policy_scope_id_position_idx").on(table.scope_id, table.position),
142 ],
143);
116 144
117export const openapi_source = sqliteTable("openapi_source", {145export const openapi_source = sqliteTable(
118 id: text('id').notNull(),146 "openapi_source",
119 scope_id: text('scope_id').notNull(),147 {
120 name: text('name').notNull(),148 id: text("id").notNull(),
121 spec: text('spec').notNull(),149 scope_id: text("scope_id").notNull(),
122 source_url: text('source_url'),150 name: text("name").notNull(),
123 base_url: text('base_url'),151 spec: text("spec").notNull(),
124 headers: text('headers', { mode: "json" }),152 source_url: text("source_url"),
125 oauth2: text('oauth2', { mode: "json" })153 base_url: text("base_url"),
126}, (table) => [154 headers: text("headers", { mode: "json" }),
127 primaryKey({ columns: [table.scope_id, table.id] }),155 oauth2: text("oauth2", { mode: "json" }),
128 index("openapi_source_scope_id_idx").on(table.scope_id),156 },
129]);157 (table) => [
158 primaryKey({ columns: [table.scope_id, table.id] }),
159 index("openapi_source_scope_id_idx").on(table.scope_id),
160 ],
161);
130 162
131export const openapi_operation = sqliteTable("openapi_operation", {163export const openapi_operation = sqliteTable(
132 id: text('id').notNull(),164 "openapi_operation",
133 scope_id: text('scope_id').notNull(),165 {
134 source_id: text('source_id').notNull(),166 id: text("id").notNull(),
135 binding: text('binding', { mode: "json" }).notNull()167 scope_id: text("scope_id").notNull(),
136}, (table) => [168 source_id: text("source_id").notNull(),
137 primaryKey({ columns: [table.scope_id, table.id] }),169 binding: text("binding", { mode: "json" }).notNull(),
138 index("openapi_operation_scope_id_idx").on(table.scope_id),170 },
139 index("openapi_operation_source_id_idx").on(table.source_id),171 (table) => [
140]);172 primaryKey({ columns: [table.scope_id, table.id] }),
173 index("openapi_operation_scope_id_idx").on(table.scope_id),
174 index("openapi_operation_source_id_idx").on(table.source_id),
175 ],
176);
141 177
142export const openapi_source_binding = sqliteTable("openapi_source_binding", {178export const openapi_source_binding = sqliteTable(
143 id: text('id').primaryKey(),179 "openapi_source_binding",
144 source_id: text('source_id').notNull(),180 {
145 source_scope_id: text('source_scope_id').notNull(),181 id: text("id").primaryKey(),
146 target_scope_id: text('target_scope_id').notNull(),182 source_id: text("source_id").notNull(),
147 slot: text('slot').notNull(),183 source_scope_id: text("source_scope_id").notNull(),
148 kind: text({ enum: ['secret', 'connection', 'text'] }).notNull(),184 target_scope_id: text("target_scope_id").notNull(),
149 secret_id: text('secret_id'),185 slot: text("slot").notNull(),
150 connection_id: text('connection_id'),186 kind: text({ enum: ["secret", "connection", "text"] }).notNull(),
151 text_value: text('text_value'),187 secret_id: text("secret_id"),
152 created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(),188 connection_id: text("connection_id"),
153 updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull()189 text_value: text("text_value"),
154}, (table) => [190 created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(),
155 index("openapi_source_binding_source_id_idx").on(table.source_id),191 updated_at: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
156 index("openapi_source_binding_source_scope_id_idx").on(table.source_scope_id),192 },
157 index("openapi_source_binding_target_scope_id_idx").on(table.target_scope_id),193 (table) => [
158 index("openapi_source_binding_slot_idx").on(table.slot),194 index("openapi_source_binding_source_id_idx").on(table.source_id),
159 index("openapi_source_binding_secret_id_idx").on(table.secret_id),195 index("openapi_source_binding_source_scope_id_idx").on(table.source_scope_id),
160 index("openapi_source_binding_connection_id_idx").on(table.connection_id),196 index("openapi_source_binding_target_scope_id_idx").on(table.target_scope_id),
161]);197 index("openapi_source_binding_slot_idx").on(table.slot),
198 index("openapi_source_binding_secret_id_idx").on(table.secret_id),
199 index("openapi_source_binding_connection_id_idx").on(table.connection_id),
200 ],
201);
162 202
163export const openapi_source_query_param = sqliteTable("openapi_source_query_param", {203export const openapi_source_query_param = sqliteTable(
164 id: text('id').notNull(),204 "openapi_source_query_param",
165 scope_id: text('scope_id').notNull(),205 {
166 source_id: text('source_id').notNull(),206 id: text("id").notNull(),
167 name: text('name').notNull(),207 scope_id: text("scope_id").notNull(),
168 kind: text({ enum: ['text', 'secret'] }).notNull(),208 source_id: text("source_id").notNull(),
169 text_value: text('text_value'),209 name: text("name").notNull(),
170 secret_id: text('secret_id'),210 kind: text({ enum: ["text", "secret"] }).notNull(),
171 secret_prefix: text('secret_prefix')211 text_value: text("text_value"),
172}, (table) => [212 secret_id: text("secret_id"),
173 primaryKey({ columns: [table.scope_id, table.id] }),213 secret_prefix: text("secret_prefix"),
174 index("openapi_source_query_param_scope_id_idx").on(table.scope_id),214 },
175 index("openapi_source_query_param_source_id_idx").on(table.source_id),215 (table) => [
176 index("openapi_source_query_param_secret_id_idx").on(table.secret_id),216 primaryKey({ columns: [table.scope_id, table.id] }),
177]);217 index("openapi_source_query_param_scope_id_idx").on(table.scope_id),
218 index("openapi_source_query_param_source_id_idx").on(table.source_id),
219 index("openapi_source_query_param_secret_id_idx").on(table.secret_id),
220 ],
221);
178 222
179export const openapi_source_spec_fetch_header = sqliteTable("openapi_source_spec_fetch_header", {223export const openapi_source_spec_fetch_header = sqliteTable(
180 id: text('id').notNull(),224 "openapi_source_spec_fetch_header",
181 scope_id: text('scope_id').notNull(),225 {
182 source_id: text('source_id').notNull(),226 id: text("id").notNull(),
183 name: text('name').notNull(),227 scope_id: text("scope_id").notNull(),
184 kind: text({ enum: ['text', 'secret'] }).notNull(),228 source_id: text("source_id").notNull(),
185 text_value: text('text_value'),229 name: text("name").notNull(),
186 secret_id: text('secret_id'),230 kind: text({ enum: ["text", "secret"] }).notNull(),
187 secret_prefix: text('secret_prefix')231 text_value: text("text_value"),
188}, (table) => [232 secret_id: text("secret_id"),
189 primaryKey({ columns: [table.scope_id, table.id] }),233 secret_prefix: text("secret_prefix"),
190 index("openapi_source_spec_fetch_header_scope_id_idx").on(table.scope_id),234 },
191 index("openapi_source_spec_fetch_header_source_id_idx").on(table.source_id),235 (table) => [
192 index("openapi_source_spec_fetch_header_secret_id_idx").on(table.secret_id),236 primaryKey({ columns: [table.scope_id, table.id] }),
193]);237 index("openapi_source_spec_fetch_header_scope_id_idx").on(table.scope_id),
238 index("openapi_source_spec_fetch_header_source_id_idx").on(table.source_id),
239 index("openapi_source_spec_fetch_header_secret_id_idx").on(table.secret_id),
240 ],
241);
194 242
195export const openapi_source_spec_fetch_query_param = sqliteTable("openapi_source_spec_fetch_query_param", {243export const openapi_source_spec_fetch_query_param = sqliteTable(
196 id: text('id').notNull(),244 "openapi_source_spec_fetch_query_param",
197 scope_id: text('scope_id').notNull(),245 {
198 source_id: text('source_id').notNull(),246 id: text("id").notNull(),
199 name: text('name').notNull(),247 scope_id: text("scope_id").notNull(),
200 kind: text({ enum: ['text', 'secret'] }).notNull(),248 source_id: text("source_id").notNull(),
201 text_value: text('text_value'),249 name: text("name").notNull(),
202 secret_id: text('secret_id'),250 kind: text({ enum: ["text", "secret"] }).notNull(),
203 secret_prefix: text('secret_prefix')251 text_value: text("text_value"),
204}, (table) => [252 secret_id: text("secret_id"),
205 primaryKey({ columns: [table.scope_id, table.id] }),253 secret_prefix: text("secret_prefix"),
206 index("openapi_source_spec_fetch_query_param_scope_id_idx").on(table.scope_id),254 },
207 index("openapi_source_spec_fetch_query_param_source_id_idx").on(table.source_id),255 (table) => [
208 index("openapi_source_spec_fetch_query_param_secret_id_idx").on(table.secret_id),256 primaryKey({ columns: [table.scope_id, table.id] }),
209]);257 index("openapi_source_spec_fetch_query_param_scope_id_idx").on(table.scope_id),
258 index("openapi_source_spec_fetch_query_param_source_id_idx").on(table.source_id),
259 index("openapi_source_spec_fetch_query_param_secret_id_idx").on(table.secret_id),
260 ],
261);
210 262
211export const mcp_source = sqliteTable("mcp_source", {263export const mcp_source = sqliteTable(
212 id: text('id').notNull(),264 "mcp_source",
213 scope_id: text('scope_id').notNull(),265 {
214 name: text('name').notNull(),266 id: text("id").notNull(),
215 config: text('config', { mode: "json" }).notNull(),267 scope_id: text("scope_id").notNull(),
216 auth_kind: text({ enum: ['none', 'header', 'oauth2'] }).default("none").notNull(),268 name: text("name").notNull(),
217 auth_header_name: text('auth_header_name'),269 config: text("config", { mode: "json" }).notNull(),
218 auth_secret_id: text('auth_secret_id'),270 auth_kind: text({ enum: ["none", "header", "oauth2"] })
219 auth_secret_prefix: text('auth_secret_prefix'),271 .default("none")
220 auth_connection_id: text('auth_connection_id'),272 .notNull(),
221 auth_client_id_secret_id: text('auth_client_id_secret_id'),273 auth_header_name: text("auth_header_name"),
222 auth_client_secret_secret_id: text('auth_client_secret_secret_id'),274 auth_secret_id: text("auth_secret_id"),
223 created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull()275 auth_secret_prefix: text("auth_secret_prefix"),
224}, (table) => [276 auth_connection_id: text("auth_connection_id"),
225 primaryKey({ columns: [table.scope_id, table.id] }),277 auth_client_id_secret_id: text("auth_client_id_secret_id"),
226 index("mcp_source_scope_id_idx").on(table.scope_id),278 auth_client_secret_secret_id: text("auth_client_secret_secret_id"),
227 index("mcp_source_auth_secret_id_idx").on(table.auth_secret_id),279 created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(),
228 index("mcp_source_auth_connection_id_idx").on(table.auth_connection_id),280 },
229 index("mcp_source_auth_client_id_secret_id_idx").on(table.auth_client_id_secret_id),281 (table) => [
230 index("mcp_source_auth_client_secret_secret_id_idx").on(table.auth_client_secret_secret_id),282 primaryKey({ columns: [table.scope_id, table.id] }),
231]);283 index("mcp_source_scope_id_idx").on(table.scope_id),
284 index("mcp_source_auth_secret_id_idx").on(table.auth_secret_id),
285 index("mcp_source_auth_connection_id_idx").on(table.auth_connection_id),
286 index("mcp_source_auth_client_id_secret_id_idx").on(table.auth_client_id_secret_id),
287 index("mcp_source_auth_client_secret_secret_id_idx").on(table.auth_client_secret_secret_id),
288 ],
289);
232 290
233export const mcp_source_header = sqliteTable("mcp_source_header", {291export const mcp_source_header = sqliteTable(
234 id: text('id').notNull(),292 "mcp_source_header",
235 scope_id: text('scope_id').notNull(),293 {
236 source_id: text('source_id').notNull(),294 id: text("id").notNull(),
237 name: text('name').notNull(),295 scope_id: text("scope_id").notNull(),
238 kind: text({ enum: ['text', 'secret'] }).notNull(),296 source_id: text("source_id").notNull(),
239 text_value: text('text_value'),297 name: text("name").notNull(),
240 secret_id: text('secret_id'),298 kind: text({ enum: ["text", "secret"] }).notNull(),
241 secret_prefix: text('secret_prefix')299 text_value: text("text_value"),
242}, (table) => [300 secret_id: text("secret_id"),
243 primaryKey({ columns: [table.scope_id, table.id] }),301 secret_prefix: text("secret_prefix"),
244 index("mcp_source_header_scope_id_idx").on(table.scope_id),302 },
245 index("mcp_source_header_source_id_idx").on(table.source_id),303 (table) => [
246 index("mcp_source_header_secret_id_idx").on(table.secret_id),304 primaryKey({ columns: [table.scope_id, table.id] }),
247]);305 index("mcp_source_header_scope_id_idx").on(table.scope_id),
306 index("mcp_source_header_source_id_idx").on(table.source_id),
307 index("mcp_source_header_secret_id_idx").on(table.secret_id),
308 ],
309);
248 310
249export const mcp_source_query_param = sqliteTable("mcp_source_query_param", {311export const mcp_source_query_param = sqliteTable(
250 id: text('id').notNull(),312 "mcp_source_query_param",
251 scope_id: text('scope_id').notNull(),313 {
252 source_id: text('source_id').notNull(),314 id: text("id").notNull(),
253 name: text('name').notNull(),315 scope_id: text("scope_id").notNull(),
254 kind: text({ enum: ['text', 'secret'] }).notNull(),316 source_id: text("source_id").notNull(),
255 text_value: text('text_value'),317 name: text("name").notNull(),
256 secret_id: text('secret_id'),318 kind: text({ enum: ["text", "secret"] }).notNull(),
257 secret_prefix: text('secret_prefix')319 text_value: text("text_value"),
258}, (table) => [320 secret_id: text("secret_id"),
259 primaryKey({ columns: [table.scope_id, table.id] }),321 secret_prefix: text("secret_prefix"),
260 index("mcp_source_query_param_scope_id_idx").on(table.scope_id),322 },
261 index("mcp_source_query_param_source_id_idx").on(table.source_id),323 (table) => [
262 index("mcp_source_query_param_secret_id_idx").on(table.secret_id),324 primaryKey({ columns: [table.scope_id, table.id] }),
263]);325 index("mcp_source_query_param_scope_id_idx").on(table.scope_id),
326 index("mcp_source_query_param_source_id_idx").on(table.source_id),
327 index("mcp_source_query_param_secret_id_idx").on(table.secret_id),
328 ],
329);
264 330
265export const mcp_binding = sqliteTable("mcp_binding", {331export const mcp_binding = sqliteTable(
266 id: text('id').notNull(),332 "mcp_binding",
267 scope_id: text('scope_id').notNull(),333 {
268 source_id: text('source_id').notNull(),334 id: text("id").notNull(),
269 binding: text('binding', { mode: "json" }).notNull(),335 scope_id: text("scope_id").notNull(),
270 created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull()336 source_id: text("source_id").notNull(),
271}, (table) => [337 binding: text("binding", { mode: "json" }).notNull(),
272 primaryKey({ columns: [table.scope_id, table.id] }),338 created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(),
273 index("mcp_binding_scope_id_idx").on(table.scope_id),339 },
274 index("mcp_binding_source_id_idx").on(table.source_id),340 (table) => [
275]);341 primaryKey({ columns: [table.scope_id, table.id] }),
342 index("mcp_binding_scope_id_idx").on(table.scope_id),
343 index("mcp_binding_source_id_idx").on(table.source_id),
344 ],
345);
276 346
277export const google_discovery_source = sqliteTable("google_discovery_source", {347export const google_discovery_source = sqliteTable(
278 id: text('id').notNull(),348 "google_discovery_source",
279 scope_id: text('scope_id').notNull(),349 {
280 name: text('name').notNull(),350 id: text("id").notNull(),
281 config: text('config', { mode: "json" }).notNull(),351 scope_id: text("scope_id").notNull(),
282 auth_kind: text({ enum: ['none', 'oauth2'] }).default("none").notNull(),352 name: text("name").notNull(),
283 auth_connection_id: text('auth_connection_id'),353 config: text("config", { mode: "json" }).notNull(),
284 auth_client_id_secret_id: text('auth_client_id_secret_id'),354 auth_kind: text({ enum: ["none", "oauth2"] })
285 auth_client_secret_secret_id: text('auth_client_secret_secret_id'),355 .default("none")
286 auth_scopes: text('auth_scopes', { mode: "json" }),356 .notNull(),
287 created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull(),357 auth_connection_id: text("auth_connection_id"),
288 updated_at: integer('updated_at', { mode: 'timestamp_ms' }).notNull()358 auth_client_id_secret_id: text("auth_client_id_secret_id"),
289}, (table) => [359 auth_client_secret_secret_id: text("auth_client_secret_secret_id"),
290 primaryKey({ columns: [table.scope_id, table.id] }),360 auth_scopes: text("auth_scopes", { mode: "json" }),
291 index("google_discovery_source_scope_id_idx").on(table.scope_id),361 created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(),
292 index("google_discovery_source_auth_connection_id_idx").on(table.auth_connection_id),362 updated_at: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
293 index("google_discovery_source_auth_client_id_secret_id_idx").on(table.auth_client_id_secret_id),363 },
294 index("google_discovery_source_auth_client_secret_secret_id_idx").on(table.auth_client_secret_secret_id),364 (table) => [
295]);365 primaryKey({ columns: [table.scope_id, table.id] }),
366 index("google_discovery_source_scope_id_idx").on(table.scope_id),
367 index("google_discovery_source_auth_connection_id_idx").on(table.auth_connection_id),
368 index("google_discovery_source_auth_client_id_secret_id_idx").on(
369 table.auth_client_id_secret_id,
370 ),
371 index("google_discovery_source_auth_client_secret_secret_id_idx").on(
372 table.auth_client_secret_secret_id,
373 ),
374 ],
375);
296 376
297export const google_discovery_source_credential_header = sqliteTable("google_discovery_source_credential_header", {377export const google_discovery_source_credential_header = sqliteTable(
298 id: text('id').notNull(),378 "google_discovery_source_credential_header",
299 scope_id: text('scope_id').notNull(),379 {
300 source_id: text('source_id').notNull(),380 id: text("id").notNull(),
301 name: text('name').notNull(),381 scope_id: text("scope_id").notNull(),
302 kind: text({ enum: ['text', 'secret'] }).notNull(),382 source_id: text("source_id").notNull(),
303 text_value: text('text_value'),383 name: text("name").notNull(),
304 secret_id: text('secret_id'),384 kind: text({ enum: ["text", "secret"] }).notNull(),
305 secret_prefix: text('secret_prefix')385 text_value: text("text_value"),
306}, (table) => [386 secret_id: text("secret_id"),
307 primaryKey({ columns: [table.scope_id, table.id] }),387 secret_prefix: text("secret_prefix"),
308 index("google_discovery_source_credential_header_scope_id_idx").on(table.scope_id),388 },
309 index("google_discovery_source_credential_header_source_id_idx").on(table.source_id),389 (table) => [
310 index("google_discovery_source_credential_header_secret_id_idx").on(table.secret_id),390 primaryKey({ columns: [table.scope_id, table.id] }),
311]);391 index("google_discovery_source_credential_header_scope_id_idx").on(table.scope_id),
392 index("google_discovery_source_credential_header_source_id_idx").on(table.source_id),
393 index("google_discovery_source_credential_header_secret_id_idx").on(table.secret_id),
394 ],
395);
312 396
313export const google_discovery_source_credential_query_param = sqliteTable("google_discovery_source_credential_query_param", {397export const google_discovery_source_credential_query_param = sqliteTable(
314 id: text('id').notNull(),398 "google_discovery_source_credential_query_param",
315 scope_id: text('scope_id').notNull(),399 {
316 source_id: text('source_id').notNull(),400 id: text("id").notNull(),
317 name: text('name').notNull(),401 scope_id: text("scope_id").notNull(),
318 kind: text({ enum: ['text', 'secret'] }).notNull(),402 source_id: text("source_id").notNull(),
319 text_value: text('text_value'),403 name: text("name").notNull(),
320 secret_id: text('secret_id'),404 kind: text({ enum: ["text", "secret"] }).notNull(),
321 secret_prefix: text('secret_prefix')405 text_value: text("text_value"),
322}, (table) => [406 secret_id: text("secret_id"),
323 primaryKey({ columns: [table.scope_id, table.id] }),407 secret_prefix: text("secret_prefix"),
324 index("google_discovery_source_credential_query_param_scope_id_idx").on(table.scope_id),408 },
325 index("google_discovery_source_credential_query_param_source_id_idx").on(table.source_id),409 (table) => [
326 index("google_discovery_source_credential_query_param_secret_id_idx").on(table.secret_id),410 primaryKey({ columns: [table.scope_id, table.id] }),
327]);411 index("google_discovery_source_credential_query_param_scope_id_idx").on(table.scope_id),
412 index("google_discovery_source_credential_query_param_source_id_idx").on(table.source_id),
413 index("google_discovery_source_credential_query_param_secret_id_idx").on(table.secret_id),
414 ],
415);
328 416
329export const google_discovery_binding = sqliteTable("google_discovery_binding", {417export const google_discovery_binding = sqliteTable(
330 id: text('id').notNull(),418 "google_discovery_binding",
331 scope_id: text('scope_id').notNull(),419 {
332 source_id: text('source_id').notNull(),420 id: text("id").notNull(),
333 binding: text('binding', { mode: "json" }).notNull(),421 scope_id: text("scope_id").notNull(),
334 created_at: integer('created_at', { mode: 'timestamp_ms' }).notNull()422 source_id: text("source_id").notNull(),
335}, (table) => [423 binding: text("binding", { mode: "json" }).notNull(),
336 primaryKey({ columns: [table.scope_id, table.id] }),424 created_at: integer("created_at", { mode: "timestamp_ms" }).notNull(),
337 index("google_discovery_binding_scope_id_idx").on(table.scope_id),425 },
338 index("google_discovery_binding_source_id_idx").on(table.source_id),426 (table) => [
339]);427 primaryKey({ columns: [table.scope_id, table.id] }),
428 index("google_discovery_binding_scope_id_idx").on(table.scope_id),
429 index("google_discovery_binding_source_id_idx").on(table.source_id),
430 ],
431);
340 432
341export const graphql_source = sqliteTable("graphql_source", {433export const graphql_source = sqliteTable(
342 id: text('id').notNull(),434 "graphql_source",
343 scope_id: text('scope_id').notNull(),435 {
344 name: text('name').notNull(),436 id: text("id").notNull(),
345 endpoint: text('endpoint').notNull(),437 scope_id: text("scope_id").notNull(),
346 auth_kind: text({ enum: ['none', 'oauth2'] }).default("none").notNull(),438 name: text("name").notNull(),
347 auth_connection_id: text('auth_connection_id')439 endpoint: text("endpoint").notNull(),
348}, (table) => [440 auth_kind: text({ enum: ["none", "oauth2"] })
349 primaryKey({ columns: [table.scope_id, table.id] }),441 .default("none")
350 index("graphql_source_scope_id_idx").on(table.scope_id),442 .notNull(),
351 index("graphql_source_auth_connection_id_idx").on(table.auth_connection_id),443 auth_connection_id: text("auth_connection_id"),
352]);444 },
445 (table) => [
446 primaryKey({ columns: [table.scope_id, table.id] }),
447 index("graphql_source_scope_id_idx").on(table.scope_id),
448 index("graphql_source_auth_connection_id_idx").on(table.auth_connection_id),
449 ],
450);
353 451
354export const graphql_source_header = sqliteTable("graphql_source_header", {452export const graphql_source_header = sqliteTable(
355 id: text('id').notNull(),453 "graphql_source_header",
356 scope_id: text('scope_id').notNull(),454 {
357 source_id: text('source_id').notNull(),455 id: text("id").notNull(),
358 name: text('name').notNull(),456 scope_id: text("scope_id").notNull(),
359 kind: text({ enum: ['text', 'secret'] }).notNull(),457 source_id: text("source_id").notNull(),
360 text_value: text('text_value'),458 name: text("name").notNull(),
361 secret_id: text('secret_id'),459 kind: text({ enum: ["text", "secret"] }).notNull(),
362 secret_prefix: text('secret_prefix')460 text_value: text("text_value"),
363}, (table) => [461 secret_id: text("secret_id"),
364 primaryKey({ columns: [table.scope_id, table.id] }),462 secret_prefix: text("secret_prefix"),
365 index("graphql_source_header_scope_id_idx").on(table.scope_id),463 },
366 index("graphql_source_header_source_id_idx").on(table.source_id),464 (table) => [
367 index("graphql_source_header_secret_id_idx").on(table.secret_id),465 primaryKey({ columns: [table.scope_id, table.id] }),
368]);466 index("graphql_source_header_scope_id_idx").on(table.scope_id),
467 index("graphql_source_header_source_id_idx").on(table.source_id),
468 index("graphql_source_header_secret_id_idx").on(table.secret_id),
469 ],
470);
369 471
370export const graphql_source_query_param = sqliteTable("graphql_source_query_param", {472export const graphql_source_query_param = sqliteTable(
371 id: text('id').notNull(),473 "graphql_source_query_param",
372 scope_id: text('scope_id').notNull(),474 {
373 source_id: text('source_id').notNull(),475 id: text("id").notNull(),
374 name: text('name').notNull(),476 scope_id: text("scope_id").notNull(),
375 kind: text({ enum: ['text', 'secret'] }).notNull(),477 source_id: text("source_id").notNull(),
376 text_value: text('text_value'),478 name: text("name").notNull(),
377 secret_id: text('secret_id'),479 kind: text({ enum: ["text", "secret"] }).notNull(),
378 secret_prefix: text('secret_prefix')480 text_value: text("text_value"),
379}, (table) => [481 secret_id: text("secret_id"),
380 primaryKey({ columns: [table.scope_id, table.id] }),482 secret_prefix: text("secret_prefix"),
381 index("graphql_source_query_param_scope_id_idx").on(table.scope_id),483 },
382 index("graphql_source_query_param_source_id_idx").on(table.source_id),484 (table) => [
383 index("graphql_source_query_param_secret_id_idx").on(table.secret_id),485 primaryKey({ columns: [table.scope_id, table.id] }),
384]);486 index("graphql_source_query_param_scope_id_idx").on(table.scope_id),
385 487 index("graphql_source_query_param_source_id_idx").on(table.source_id),
386export const graphql_operation = sqliteTable("graphql_operation", {488 index("graphql_source_query_param_secret_id_idx").on(table.secret_id),
387 id: text('id').notNull(),489 ],
388 scope_id: text('scope_id').notNull(),490);
389 source_id: text('source_id').notNull(),
390 binding: text('binding', { mode: "json" }).notNull()
391}, (table) => [
392 primaryKey({ columns: [table.scope_id, table.id] }),
393 index("graphql_operation_scope_id_idx").on(table.scope_id),
394 index("graphql_operation_source_id_idx").on(table.source_id),
395]);
396 491
492export const graphql_operation = sqliteTable(
493 "graphql_operation",
494 {
495 id: text("id").notNull(),
496 scope_id: text("scope_id").notNull(),
497 source_id: text("source_id").notNull(),
498 binding: text("binding", { mode: "json" }).notNull(),
499 },
500 (table) => [
501 primaryKey({ columns: [table.scope_id, table.id] }),
502 index("graphql_operation_scope_id_idx").on(table.scope_id),
503 index("graphql_operation_source_id_idx").on(table.source_id),
504 ],
505);
apps/local/src/server/executor.ts+9−26
@@ -16,17 +16,8 @@ import { · 15 unmodified lines
16} from "./db-upgrade";16} from "./db-upgrade";
17import { migrateLegacyConnections } from "./migrate-connections";17import { migrateLegacyConnections } from "./migrate-connections";
18 18
19import {19import { Scope, ScopeId, type AnyPlugin, collectSchemas, createExecutor } from "@executor-js/sdk";
20 Scope,20import { makeSqliteAdapter, makeSqliteBlobStore } from "@executor-js/storage-file";
21 ScopeId,
22 type AnyPlugin,
23 collectSchemas,
24 createExecutor,
25} from "@executor-js/sdk";
26import {
27 makeSqliteAdapter,
28 makeSqliteBlobStore,
29} from "@executor-js/storage-file";
30import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";21import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
31import { loadPluginsFromJsonc, makeFileConfigSink } from "@executor-js/config";22import { loadPluginsFromJsonc, makeFileConfigSink } from "@executor-js/config";
32import * as executorSchema from "./executor-schema";23import * as executorSchema from "./executor-schema";
@@ -107,9 +98,7 @@ class LocalExecutorTag extends Context.Service<LocalExecutorTag, LocalExecutorBu · 74 unmodified lines
107 98
108export type LocalExecutor = LocalExecutorBundle["executor"];99export type LocalExecutor = LocalExecutorBundle["executor"];
109 100
110class LocalExecutorDisposeError extends Data.TaggedError(101class LocalExecutorDisposeError extends Data.TaggedError("LocalExecutorDisposeError")<{
111 "LocalExecutorDisposeError",
112)<{
113 readonly operation: "createHandle" | "disposeExecutor" | "disposeRuntime";102 readonly operation: "createHandle" | "disposeExecutor" | "disposeRuntime";
114 readonly cause: unknown;103 readonly cause: unknown;
115}> {}104}> {}
@@ -131,13 +120,10 @@ const handleOrNull = (promise: ReturnType<typeof createExecutorHandle>) => · 15 unmodified lines
131 Effect.runPromise(120 Effect.runPromise(
132 Effect.tryPromise({121 Effect.tryPromise({
133 try: () => promise,122 try: () => promise,
134 catch: (cause) =>123 catch: (cause) => new LocalExecutorDisposeError({ operation: "createHandle", cause }),
135 new LocalExecutorDisposeError({ operation: "createHandle", cause }),
136 }).pipe(124 }).pipe(
137 Effect.catch(() =>125 Effect.catch(() =>
138 Effect.succeed<Awaited<ReturnType<typeof createExecutorHandle>> | null>(126 Effect.succeed<Awaited<ReturnType<typeof createExecutorHandle>> | null>(null),
139 null,
140 ),
141 ),127 ),
142 ),128 ),
143 );129 );
@@ -145,7 +131,8 @@ const handleOrNull = (promise: ReturnType<typeof createExecutorHandle>) => · 1 unmodified line
145const createLocalExecutorLayer = () => {131const createLocalExecutorLayer = () => {
146 const { path: dbPath, legacySecrets } = resolveDbPath();132 const { path: dbPath, legacySecrets } = resolveDbPath();
147 133
148 return Layer.effect(LocalExecutorTag)(Effect.gen(function* () {134 return Layer.effect(LocalExecutorTag)(
135 Effect.gen(function* () {
149 const sqlite = yield* Effect.acquireRelease(136 const sqlite = yield* Effect.acquireRelease(
150 Effect.sync(() => new Database(dbPath)),137 Effect.sync(() => new Database(dbPath)),
151 (conn) => Effect.sync(() => conn.close()),138 (conn) => Effect.sync(() => conn.close()),
@@ -183,9 +170,7 @@ const createLocalExecutorLayer = () => { · 31 unmodified lines
183 // ordering. Without this, a package listed in both surfaces would170 // ordering. Without this, a package listed in both surfaces would
184 // boot twice (double routes, double in-memory storage).171 // boot twice (double routes, double in-memory storage).
185 const staticPackageNames = new Set(172 const staticPackageNames = new Set(
186 staticPlugins173 staticPlugins.map((p) => p.packageName).filter((n): n is string => !!n),
187 .map((p) => p.packageName)
188 .filter((n): n is string => !!n),
189 );174 );
190 const dedupedDynamic = dynamicPlugins.filter((p) => {175 const dedupedDynamic = dynamicPlugins.filter((p) => {
191 if (p.packageName && staticPackageNames.has(p.packageName)) {176 if (p.packageName && staticPackageNames.has(p.packageName)) {
@@ -261,9 +246,7 @@ export const disposeExecutor = async (): Promise<void> => { · 69 unmodified lines
261 const currentHandlePromise = sharedHandlePromise;246 const currentHandlePromise = sharedHandlePromise;
262 sharedHandlePromise = null;247 sharedHandlePromise = null;
263 248
264 const handle = currentHandlePromise249 const handle = currentHandlePromise ? await handleOrNull(currentHandlePromise) : null;
265 ? await handleOrNull(currentHandlePromise)
266 : null;
267 if (handle) {250 if (handle) {
268 await ignorePromiseFailure("disposeExecutor", () => handle.dispose());251 await ignorePromiseFailure("disposeExecutor", () => handle.dispose());
269 }252 }
apps/local/src/server/main.ts+5−3
@@ -103,10 +103,12 @@ export const createServerHandlers = async (): Promise<ServerHandlers> => { · 102 unmodified lines
103 return { api: apiHandler, mcp };103 return { api: apiHandler, mcp };
104};104};
105 105
106export class ServerHandlersService extends Context.Service<ServerHandlersService, ServerHandlers106export class ServerHandlersService extends Context.Service<ServerHandlersService, ServerHandlers>()(
107>()("@executor-js/local/ServerHandlersService") {}107 "@executor-js/local/ServerHandlersService",
108) {}
108 109
109const ServerHandlersLive = Layer.effect(ServerHandlersService)(Effect.acquireRelease(110const ServerHandlersLive = Layer.effect(ServerHandlersService)(
111 Effect.acquireRelease(
110 Effect.promise(() => createServerHandlers()),112 Effect.promise(() => createServerHandlers()),
111 (handlers) => Effect.promise(() => closeServerHandlers(handlers)),113 (handlers) => Effect.promise(() => closeServerHandlers(handlers)),
112 ),114 ),
apps/local/src/server/mcp-oauth.test.ts+19−60
@@ -34,30 +34,14 @@ import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"; · 33 unmodified lines
34import { Effect, Layer, Option, Schema } from "effect";34import { Effect, Layer, Option, Schema } from "effect";
35 35
36import { addGroup, observabilityMiddleware } from "@executor-js/api";36import { addGroup, observabilityMiddleware } from "@executor-js/api";
37import {37import { CoreHandlers, ExecutionEngineService, ExecutorService } from "@executor-js/api/server";
38 CoreHandlers,
39 ExecutionEngineService,
40 ExecutorService,
41} from "@executor-js/api/server";
42import { createExecutionEngine } from "@executor-js/execution";38import { createExecutionEngine } from "@executor-js/execution";
43import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";39import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";
44import {40import { Scope, ScopeId, collectSchemas, createExecutor } from "@executor-js/sdk";
45 Scope,41import { makeSqliteAdapter, makeSqliteBlobStore } from "@executor-js/storage-file";
46 ScopeId,
47 collectSchemas,
48 createExecutor,
49} from "@executor-js/sdk";
50import {
51 makeSqliteAdapter,
52 makeSqliteBlobStore,
53} from "@executor-js/storage-file";
54import { fileSecretsPlugin } from "@executor-js/plugin-file-secrets";42import { fileSecretsPlugin } from "@executor-js/plugin-file-secrets";
55import { mcpPlugin } from "@executor-js/plugin-mcp";43import { mcpPlugin } from "@executor-js/plugin-mcp";
56import {44import { McpExtensionService, McpGroup, McpHandlers } from "@executor-js/plugin-mcp/api";
57 McpExtensionService,
58 McpGroup,
59 McpHandlers,
60} from "@executor-js/plugin-mcp/api";
61 45
62import * as executorSchema from "./executor-schema";46import * as executorSchema from "./executor-schema";
63import { ErrorCaptureLive } from "./observability";47import { ErrorCaptureLive } from "./observability";
@@ -66,9 +50,10 @@ import { ErrorCaptureLive } from "./observability"; · 2 unmodified lines
66// the top level so `observabilityMiddleware` can land its typed-error50// the top level so `observabilityMiddleware` can land its typed-error
67// bridge on every endpoint.51// bridge on every endpoint.
68const TestApi = addGroup(McpGroup);52const TestApi = addGroup(McpGroup);
69type TestApiShape = typeof TestApi extends HttpApi.HttpApi<infer _Id, infer Groups>53type TestApiShape =
70 ? HttpApiClient.Client<Groups, never>54 typeof TestApi extends HttpApi.HttpApi<infer _Id, infer Groups>
71 : never;55 ? HttpApiClient.Client<Groups, never>
56 : never;
72 57
73// ---------------------------------------------------------------------------58// ---------------------------------------------------------------------------
74// Fake OAuth + MCP server (mirrors the cloud test)59// Fake OAuth + MCP server (mirrors the cloud test)
@@ -84,21 +69,15 @@ const RegistrationBody = Schema.Struct({ · 9 unmodified lines
84 grant_types: Schema.optional(Schema.Array(Schema.String)),69 grant_types: Schema.optional(Schema.Array(Schema.String)),
85 response_types: Schema.optional(Schema.Array(Schema.String)),70 response_types: Schema.optional(Schema.Array(Schema.String)),
86});71});
87const decodeRegistrationBody = Schema.decodeUnknownOption(72const decodeRegistrationBody = Schema.decodeUnknownOption(Schema.fromJsonString(RegistrationBody));
88 Schema.fromJsonString(RegistrationBody),
89);
90 73
91const startFakeServer = async (): Promise<FakeServer> => {74const startFakeServer = async (): Promise<FakeServer> => {
92 const clients = new Map<string, { redirect_uris: readonly string[] }>();75 const clients = new Map<string, { redirect_uris: readonly string[] }>();
93 const codes = new Map<76 const codes = new Map<string, { readonly clientId: string; readonly codeChallenge: string }>();
94 string,
95 { readonly clientId: string; readonly codeChallenge: string }
96 >();
97 const accessTokens = new Map<string, { readonly refresh: string }>();77 const accessTokens = new Map<string, { readonly refresh: string }>();
98 const refreshTokens = new Map<string, string>();78 const refreshTokens = new Map<string, string>();
99 let seq = 0;79 let seq = 0;
100 const next = (p: string) =>80 const next = (p: string) => `${p}_${++seq}_${randomBytes(6).toString("hex")}`;
101 `${p}_${++seq}_${randomBytes(6).toString("hex")}`;
102 81
103 const readBody = (req: import("node:http").IncomingMessage): Promise<string> =>82 const readBody = (req: import("node:http").IncomingMessage): Promise<string> =>
104 new Promise((resolve, reject) => {83 new Promise((resolve, reject) => {
@@ -110,15 +89,10 @@ const startFakeServer = async (): Promise<FakeServer> => { · 5 unmodified lines
110 89
111 const server: Server = createServer(async (req, res) => {90 const server: Server = createServer(async (req, res) => {
112 const url = new URL(req.url!, `http://${req.headers.host}`);91 const url = new URL(req.url!, `http://${req.headers.host}`);
113 const send = (92 const send = (status: number, body: unknown, headers: Record<string, string> = {}) => {
114 status: number,
115 body: unknown,
116 headers: Record<string, string> = {},
117 ) => {
118 const payload = typeof body === "string" ? body : JSON.stringify(body);93 const payload = typeof body === "string" ? body : JSON.stringify(body);
119 res.writeHead(status, {94 res.writeHead(status, {
120 "content-type":95 "content-type": typeof body === "string" ? "text/plain" : "application/json",
121 typeof body === "string" ? "text/plain" : "application/json",
122 ...headers,96 ...headers,
123 });97 });
124 res.end(payload);98 res.end(payload);
@@ -162,10 +136,7 @@ const startFakeServer = async (): Promise<FakeServer> => { · 37 unmodified lines
162 client_id: clientId,136 client_id: clientId,
163 client_id_issued_at: Math.floor(Date.now() / 1000),137 client_id_issued_at: Math.floor(Date.now() / 1000),
164 redirect_uris: parsed.redirect_uris ?? [],138 redirect_uris: parsed.redirect_uris ?? [],
165 grant_types: parsed.grant_types ?? [139 grant_types: parsed.grant_types ?? ["authorization_code", "refresh_token"],
166 "authorization_code",
167 "refresh_token",
168 ],
169 response_types: parsed.response_types ?? ["code"],140 response_types: parsed.response_types ?? ["code"],
170 token_endpoint_auth_method: "none",141 token_endpoint_auth_method: "none",
171 });142 });
@@ -202,9 +173,7 @@ const startFakeServer = async (): Promise<FakeServer> => { · 30 unmodified lines
202 const record = codes.get(code);173 const record = codes.get(code);
203 if (!record) return send(400, { error: "invalid_grant" });174 if (!record) return send(400, { error: "invalid_grant" });
204 codes.delete(code);175 codes.delete(code);
205 const computed = createHash("sha256")176 const computed = createHash("sha256").update(verifier).digest("base64url");
206 .update(verifier)
207 .digest("base64url");
208 if (computed !== record.codeChallenge) {177 if (computed !== record.codeChallenge) {
209 return send(400, { error: "invalid_grant" });178 return send(400, { error: "invalid_grant" });
210 }179 }
@@ -240,9 +209,7 @@ const startFakeServer = async (): Promise<FakeServer> => { · 29 unmodified lines
240 }209 }
241 });210 });
242 211
243 await new Promise<void>((resolve) =>212 await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
244 server.listen(0, "127.0.0.1", () => resolve()),
245 );
246 const address = server.address() as AddressInfo;213 const address = server.address() as AddressInfo;
247 214
248 return {215 return {
@@ -328,15 +295,9 @@ const startHarness = async (tmpDir: string): Promise<Harness> => { · 79 unmodified lines
328 )) as typeof globalThis.fetch,295 )) as typeof globalThis.fetch,
329 scopeId,296 scopeId,
330 dispose: async () => {297 dispose: async () => {
298 await Effect.runPromise(Effect.ignore(Effect.tryPromise(() => disposeHandler())));
331 await Effect.runPromise(299 await Effect.runPromise(
332 Effect.ignore(300 Effect.ignore(Effect.tryPromise(() => Effect.runPromise(executor.close()))),
333 Effect.tryPromise(() => disposeHandler()),
334 ),
335 );
336 await Effect.runPromise(
337 Effect.ignore(
338 Effect.tryPromise(() => Effect.runPromise(executor.close())),
339 ),
340 );301 );
341 sqlite.close();302 sqlite.close();
342 },303 },
@@ -400,9 +361,7 @@ describe("local mcp oauth (real OAuth + MCP server)", () => { · 57 unmodified lines
400 const redirectUrl = "http://local.test/api/mcp/oauth/callback";361 const redirectUrl = "http://local.test/api/mcp/oauth/callback";
401 const scopeId = ScopeId.make(harness.scopeId);362 const scopeId = ScopeId.make(harness.scopeId);
402 363
403 const run = <A, E>(364 const run = <A, E>(body: (client: TestApiShape) => Effect.Effect<A, E>): Effect.Effect<A, E> =>
404 body: (client: TestApiShape) => Effect.Effect<A, E>,
405 ): Effect.Effect<A, E> =>
406 Effect.gen(function* () {365 Effect.gen(function* () {
407 const client = yield* HttpApiClient.make(TestApi, {366 const client = yield* HttpApiClient.make(TestApi, {
408 baseUrl: TEST_BASE_URL,367 baseUrl: TEST_BASE_URL,
apps/local/src/server/migrate-connections.test.ts+3−9
@@ -40,9 +40,7 @@ const columnNames = (db: Database, table: string): ReadonlyArray<string> => · 39 unmodified lines
40const MigratedMcpConfig = Schema.Struct({40const MigratedMcpConfig = Schema.Struct({
41 auth: Schema.optional(Schema.Unknown),41 auth: Schema.optional(Schema.Unknown),
42});42});
43const decodeMigratedMcpConfig = Schema.decodeUnknownSync(43const decodeMigratedMcpConfig = Schema.decodeUnknownSync(Schema.fromJsonString(MigratedMcpConfig));
44 Schema.fromJsonString(MigratedMcpConfig),
45);
46 44
47const MigratedOpenApiOAuth2 = Schema.Struct({45const MigratedOpenApiOAuth2 = Schema.Struct({
48 kind: Schema.Literal("oauth2"),46 kind: Schema.Literal("oauth2"),
@@ -130,9 +128,7 @@ describe("migrateLegacyConnections", () => { · 81 unmodified lines
130 expect(source.auth_connection_id).toBe("mcp-oauth2-remote-mcp");128 expect(source.auth_connection_id).toBe("mcp-oauth2-remote-mcp");
131 129
132 const ownedSecrets = db130 const ownedSecrets = db
133 .prepare(131 .prepare("SELECT id, owned_by_connection_id FROM secret WHERE scope_id = ? ORDER BY id")
134 "SELECT id, owned_by_connection_id FROM secret WHERE scope_id = ? ORDER BY id",
135 )
136 .all("scope-1");132 .all("scope-1");
137 expect(ownedSecrets).toEqual([133 expect(ownedSecrets).toEqual([
138 {134 {
@@ -208,9 +204,7 @@ describe("migrateLegacyConnections", () => { · 69 unmodified lines
208 await migrateLegacyConnections(db);204 await migrateLegacyConnections(db);
209 205
210 const connection = db206 const connection = db
211 .prepare(207 .prepare("SELECT id, provider, access_token_secret_id FROM connection WHERE scope_id = ?")
212 "SELECT id, provider, access_token_secret_id FROM connection WHERE scope_id = ?",
213 )
214 .get("scope-1") as208 .get("scope-1") as
215 | { readonly id: string; readonly provider: string; readonly access_token_secret_id: string }209 | { readonly id: string; readonly provider: string; readonly access_token_secret_id: string }
216 | undefined;210 | undefined;
apps/local/src/server/migrate-connections.ts+26−62
@@ -16,11 +16,7 @@ import { Database } from "bun:sqlite"; · 15 unmodified lines
16import { randomUUID } from "node:crypto";16import { randomUUID } from "node:crypto";
17import { Effect, Option, Result, Schema } from "effect";17import { Effect, Option, Result, Schema } from "effect";
18import { FetchHttpClient } from "effect/unstable/http";18import { FetchHttpClient } from "effect/unstable/http";
19import {19import { parse as parseOpenApi, resolveSpecText, OAuth2Auth } from "@executor-js/plugin-openapi";
20 parse as parseOpenApi,
21 resolveSpecText,
22 OAuth2Auth,
23} from "@executor-js/plugin-openapi";
24import { McpConnectionAuth } from "@executor-js/plugin-mcp";20import { McpConnectionAuth } from "@executor-js/plugin-mcp";
25 21
26// ---------------------------------------------------------------------------22// ---------------------------------------------------------------------------
@@ -51,9 +47,9 @@ const formatBoundaryError = (error: unknown): string => { · 24 unmodified lines
51/** Pre-flight: bail unless the drizzle migration that added the Connection47/** Pre-flight: bail unless the drizzle migration that added the Connection
52 * table + `secret.owned_by_connection_id` has completed. */48 * table + `secret.owned_by_connection_id` has completed. */
53const connectionsReady = (sqlite: Database): boolean => {49const connectionsReady = (sqlite: Database): boolean => {
54 const secretColumns = sqlite50 const secretColumns = sqlite.prepare("PRAGMA table_info('secret')").all() as ReadonlyArray<{
55 .prepare("PRAGMA table_info('secret')")51 readonly name: string;
56 .all() as ReadonlyArray<{ readonly name: string }>;52 }>;
57 if (!secretColumns.some((c) => c.name === "owned_by_connection_id")) return false;53 if (!secretColumns.some((c) => c.name === "owned_by_connection_id")) return false;
58 const connectionTable = sqlite54 const connectionTable = sqlite
59 .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='connection'")55 .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='connection'")
@@ -103,16 +99,10 @@ const rewireSecrets = ( · 43 unmodified lines
103 ) VALUES (?, ?, ?, ?, ?, ?)`,99 ) VALUES (?, ?, ?, ?, ?, ?)`,
104 );100 );
105 101
106 const rows = secretIds.map(102 const rows = secretIds.map((sid) => selectSecret.get(scopeId, sid) as SecretRow | undefined);
107 (sid) => selectSecret.get(scopeId, sid) as SecretRow | undefined,
108 );
109 const alreadyOwned = rows103 const alreadyOwned = rows
110 .filter((r): r is SecretRow => !!r)104 .filter((r): r is SecretRow => !!r)
111 .filter(105 .filter((r) => r.owned_by_connection_id !== null && r.owned_by_connection_id !== connectionId);
112 (r) =>
113 r.owned_by_connection_id !== null &&
114 r.owned_by_connection_id !== connectionId,
115 );
116 if (alreadyOwned.length > 0) return "secret(s) already owned";106 if (alreadyOwned.length > 0) return "secret(s) already owned";
117 107
118 // Early-onboarded rows never got a `secret` routing row — pre-refactor108 // Early-onboarded rows never got a `secret` routing row — pre-refactor
@@ -123,9 +113,7 @@ const rewireSecrets = ( · 4 unmodified lines
123 const missingCount = rows.filter((r) => r === undefined).length;113 const missingCount = rows.filter((r) => r === undefined).length;
124 let fallbackProvider: string | null = null;114 let fallbackProvider: string | null = null;
125 if (missingCount > 0) {115 if (missingCount > 0) {
126 const existing = selectAnySecretProvider.get(scopeId) as116 const existing = selectAnySecretProvider.get(scopeId) as { provider: string } | undefined;
127 | { provider: string }
128 | undefined;
129 fallbackProvider = existing?.provider ?? "keychain";117 fallbackProvider = existing?.provider ?? "keychain";
130 }118 }
131 119
@@ -213,9 +201,7 @@ class LegacyOpenApiOAuth2 extends Schema.Class<LegacyOpenApiOAuth2>("LegacyOpenA · 81 unmodified lines
213}) {}201}) {}
214 202
215const decodeOpenApiCurrent = Schema.decodeUnknownOption(OAuth2Auth);203const decodeOpenApiCurrent = Schema.decodeUnknownOption(OAuth2Auth);
216const decodeOpenApiLegacy = decodeUnknownOptionAs<LegacyOpenApiOAuth2>(204const decodeOpenApiLegacy = decodeUnknownOptionAs<LegacyOpenApiOAuth2>(LegacyOpenApiOAuth2);
217 LegacyOpenApiOAuth2,
218);
219 205
220const extractAuthorizationUrl = async (206const extractAuthorizationUrl = async (
221 rawSpec: string,207 rawSpec: string,
@@ -234,19 +220,18 @@ const extractAuthorizationUrl = async ( · 12 unmodified lines
234 const spec = parsed.success as unknown;220 const spec = parsed.success as unknown;
235 if (!isRecord(spec)) return null;221 if (!isRecord(spec)) return null;
236 const components = isRecord(spec.components) ? spec.components : null;222 const components = isRecord(spec.components) ? spec.components : null;
237 const schemes = components && isRecord(components.securitySchemes)223 const schemes =
238 ? components.securitySchemes224 components && isRecord(components.securitySchemes) ? components.securitySchemes : null;
239 : null;225 const scheme =
240 const scheme = schemes && isRecord(schemes[securitySchemeName])226 schemes && isRecord(schemes[securitySchemeName])
241 ? (schemes[securitySchemeName] as Record<string, unknown>)227 ? (schemes[securitySchemeName] as Record<string, unknown>)
242 : null;228 : null;
243 const flows = scheme && isRecord(scheme.flows) ? scheme.flows : null;229 const flows = scheme && isRecord(scheme.flows) ? scheme.flows : null;
244 const flowObj = flows && isRecord(flows.authorizationCode)230 const flowObj =
245 ? (flows.authorizationCode as Record<string, unknown>)231 flows && isRecord(flows.authorizationCode)
246 : null;232 ? (flows.authorizationCode as Record<string, unknown>)
247 return flowObj && isString(flowObj.authorizationUrl)233 : null;
248 ? flowObj.authorizationUrl234 return flowObj && isString(flowObj.authorizationUrl) ? flowObj.authorizationUrl : null;
249 : null;
250};235};
251 236
252type OpenApiRow = {237type OpenApiRow = {
@@ -266,11 +251,7 @@ const migrateOpenApi = async (sqlite: Database): Promise<void> => { · 13 unmodified lines
266 // pointer. Pre-migration, both columns mirror each other; post-251 // pointer. Pre-migration, both columns mirror each other; post-
267 // migration, only `oauth2` is left. We read both when available and252 // migration, only `oauth2` is left. We read both when available and
268 // fall back to `oauth2` so legacy data isn't silently skipped.253 // fall back to `oauth2` so legacy data isn't silently skipped.
269 const hasInvocationConfig = columnExists(254 const hasInvocationConfig = columnExists(sqlite, "openapi_source", "invocation_config");
270 sqlite,
271 "openapi_source",
272 "invocation_config",
273 );
274 const selectCols = hasInvocationConfig255 const selectCols = hasInvocationConfig
275 ? "scope_id, id, name, spec, invocation_config, oauth2"256 ? "scope_id, id, name, spec, invocation_config, oauth2"
276 : "scope_id, id, name, spec, NULL AS invocation_config, oauth2";257 : "scope_id, id, name, spec, NULL AS invocation_config, oauth2";
@@ -283,9 +264,7 @@ const migrateOpenApi = async (sqlite: Database): Promise<void> => { · 6 unmodified lines
283 ? sqlite.prepare(264 ? sqlite.prepare(
284 "UPDATE openapi_source SET oauth2 = ?, invocation_config = ? WHERE scope_id = ? AND id = ?",265 "UPDATE openapi_source SET oauth2 = ?, invocation_config = ? WHERE scope_id = ? AND id = ?",
285 )266 )
286 : sqlite.prepare(267 : sqlite.prepare("UPDATE openapi_source SET oauth2 = ? WHERE scope_id = ? AND id = ?");
287 "UPDATE openapi_source SET oauth2 = ? WHERE scope_id = ? AND id = ?",
288 );
289 268
290 for (const row of rows) {269 for (const row of rows) {
291 let invocation: Record<string, unknown> = {};270 let invocation: Record<string, unknown> = {};
@@ -370,19 +349,13 @@ const migrateOpenApi = async (sqlite: Database): Promise<void> => { · 78 unmodified lines
370 row.id,349 row.id,
371 );350 );
372 } else {351 } else {
373 updateSource.run(352 updateSource.run(JSON.stringify(oauth2Pointer), row.scope_id, row.id);
374 JSON.stringify(oauth2Pointer),
375 row.scope_id,
376 row.id,
377 );
378 }353 }
379 });354 });
380 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: bun:sqlite transaction reports rollback failures by throwing355 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: bun:sqlite transaction reports rollback failures by throwing
381 try {356 try {
382 txn();357 txn();
383 console.log(358 console.log(`[migrate-connections] openapi ${row.scope_id}/${row.id} -> ${connectionId}`);
384 `[migrate-connections] openapi ${row.scope_id}/${row.id} -> ${connectionId}`,
385 );
386 } catch (err) {359 } catch (err) {
387 console.warn(360 console.warn(
388 `[migrate-connections] fail openapi ${row.scope_id}/${row.id}: ${formatBoundaryError(err)}`,361 `[migrate-connections] fail openapi ${row.scope_id}/${row.id}: ${formatBoundaryError(err)}`,
@@ -471,9 +444,7 @@ const migrateMcp = (sqlite: Database): void => { · 82 unmodified lines
471 444
472 const endpoint = typeof config.endpoint === "string" ? config.endpoint : null;445 const endpoint = typeof config.endpoint === "string" ? config.endpoint : null;
473 if (!endpoint) {446 if (!endpoint) {
474 console.warn(447 console.warn(`[migrate-connections] skip mcp ${row.scope_id}/${row.id}: endpoint missing`);
475 `[migrate-connections] skip mcp ${row.scope_id}/${row.id}: endpoint missing`,
476 );
477 continue;448 continue;
478 }449 }
479 const connectionId = `mcp-oauth2-${row.id}`;450 const connectionId = `mcp-oauth2-${row.id}`;
@@ -518,12 +489,7 @@ const migrateMcp = (sqlite: Database): void => { · 38 unmodified lines
518 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: bun:sqlite transaction callback must throw to roll back489 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: bun:sqlite transaction callback must throw to roll back
519 if (err) throw new Error(err);490 if (err) throw new Error(err);
520 if (updateConfigAndAuth) {491 if (updateConfigAndAuth) {
521 updateConfigAndAuth.run(492 updateConfigAndAuth.run(JSON.stringify(nextConfig), connectionId, row.scope_id, row.id);
522 JSON.stringify(nextConfig),
523 connectionId,
524 row.scope_id,
525 row.id,
526 );
527 } else {493 } else {
528 updateConfig.run(JSON.stringify(nextConfig), row.scope_id, row.id);494 updateConfig.run(JSON.stringify(nextConfig), row.scope_id, row.id);
529 }495 }
@@ -531,9 +497,7 @@ const migrateMcp = (sqlite: Database): void => { · 1 unmodified line
531 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: bun:sqlite transaction reports rollback failures by throwing497 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: bun:sqlite transaction reports rollback failures by throwing
532 try {498 try {
533 txn();499 txn();
534 console.log(500 console.log(`[migrate-connections] mcp ${row.scope_id}/${row.id} -> ${connectionId}`);
535 `[migrate-connections] mcp ${row.scope_id}/${row.id} -> ${connectionId}`,
536 );
537 } catch (err) {501 } catch (err) {
538 console.warn(502 console.warn(
539 `[migrate-connections] fail mcp ${row.scope_id}/${row.id}: ${formatBoundaryError(err)}`,503 `[migrate-connections] fail mcp ${row.scope_id}/${row.id}: ${formatBoundaryError(err)}`,
apps/local/src/server/migrate-graphql-bindings.test.ts+12−23
@@ -57,9 +57,7 @@ const decodeTableInfoRows = Schema.decodeUnknownSync(Schema.Array(TableInfoRow)) · 56 unmodified lines
57const decodeHeaderRows = Schema.decodeUnknownSync(Schema.Array(GraphqlHeaderRow));57const decodeHeaderRows = Schema.decodeUnknownSync(Schema.Array(GraphqlHeaderRow));
58const decodeQueryParamRow = Schema.decodeUnknownSync(GraphqlQueryParamRow);58const decodeQueryParamRow = Schema.decodeUnknownSync(GraphqlQueryParamRow);
59const decodeCountRow = Schema.decodeUnknownSync(CountRow);59const decodeCountRow = Schema.decodeUnknownSync(CountRow);
60const decodeHeaderIdRows = Schema.decodeUnknownSync(60const decodeHeaderIdRows = Schema.decodeUnknownSync(Schema.Array(GraphqlHeaderIdRow));
61 Schema.Array(GraphqlHeaderIdRow),
62);
63 61
64let dir: string;62let dir: string;
65 63
@@ -96,19 +94,13 @@ describe("0007_normalize_plugin_secret_refs (graphql)", () => { · 30 unmodified lines
96 const after = new Database(dbPath, { readonly: true });94 const after = new Database(dbPath, { readonly: true });
97 const row = decodeAuthRow(95 const row = decodeAuthRow(
98 after96 after
99 .prepare(97 .prepare("SELECT auth_kind, auth_connection_id FROM graphql_source WHERE id = ?")
100 "SELECT auth_kind, auth_connection_id FROM graphql_source WHERE id = ?",
101 )
102 .get("github"),98 .get("github"),
103 );99 );
104 expect(row.auth_kind).toBe("oauth2");100 expect(row.auth_kind).toBe("oauth2");
105 expect(row.auth_connection_id).toBe("conn-1");101 expect(row.auth_connection_id).toBe("conn-1");
106 // Old json column is gone.102 // Old json column is gone.
107 const cols = decodeTableInfoRows(103 const cols = decodeTableInfoRows(after.prepare("PRAGMA table_info('graphql_source')").all());
108 after
109 .prepare("PRAGMA table_info('graphql_source')")
110 .all(),
111 );
112 expect(cols.some((c) => c.name === "auth")).toBe(false);104 expect(cols.some((c) => c.name === "auth")).toBe(false);
113 expect(cols.some((c) => c.name === "headers")).toBe(false);105 expect(cols.some((c) => c.name === "headers")).toBe(false);
114 expect(cols.some((c) => c.name === "query_params")).toBe(false);106 expect(cols.some((c) => c.name === "query_params")).toBe(false);
@@ -180,9 +172,7 @@ describe("0007_normalize_plugin_secret_refs (graphql)", () => { · 65 unmodified lines
180 172
181 const paramRow = decodeQueryParamRow(173 const paramRow = decodeQueryParamRow(
182 after174 after
183 .prepare(175 .prepare("SELECT kind, secret_id FROM graphql_source_query_param WHERE source_id = ?")
184 "SELECT kind, secret_id FROM graphql_source_query_param WHERE source_id = ?",
185 )
186 .get("example"),176 .get("example"),
187 );177 );
188 expect(paramRow).toMatchObject({ kind: "secret", secret_id: "sec-key" });178 expect(paramRow).toMatchObject({ kind: "secret", secret_id: "sec-key" });
@@ -196,9 +186,12 @@ describe("0007_normalize_plugin_secret_refs (graphql)", () => { · 7 unmodified lines
196 db.exec(PRE_0007_SQL);186 db.exec(PRE_0007_SQL);
197 stampPriorMigrationsApplied(db);187 stampPriorMigrationsApplied(db);
198 188
199 db.prepare(189 db.prepare("INSERT INTO graphql_source (scope_id, id, name, endpoint) VALUES (?, ?, ?, ?)").run(
200 "INSERT INTO graphql_source (scope_id, id, name, endpoint) VALUES (?, ?, ?, ?)",190 "default-scope",
201 ).run("default-scope", "bare", "Bare", "https://bare.example/graphql");191 "bare",
192 "Bare",
193 "https://bare.example/graphql",
194 );
202 db.close();195 db.close();
203 196
204 const drizzleDb = drizzle(new Database(dbPath));197 const drizzleDb = drizzle(new Database(dbPath));
@@ -207,9 +200,7 @@ describe("0007_normalize_plugin_secret_refs (graphql)", () => { · 2 unmodified lines
207 const after = new Database(dbPath, { readonly: true });200 const after = new Database(dbPath, { readonly: true });
208 const row = decodeAuthRow(201 const row = decodeAuthRow(
209 after202 after
210 .prepare(203 .prepare("SELECT auth_kind, auth_connection_id FROM graphql_source WHERE id = ?")
211 "SELECT auth_kind, auth_connection_id FROM graphql_source WHERE id = ?",
212 )
213 .get("bare"),204 .get("bare"),
214 );205 );
215 expect(row.auth_kind).toBe("none");206 expect(row.auth_kind).toBe("none");
@@ -217,9 +208,7 @@ describe("0007_normalize_plugin_secret_refs (graphql)", () => { · 1 unmodified line
217 208
218 const headerCount = decodeCountRow(209 const headerCount = decodeCountRow(
219 after210 after
220 .prepare(211 .prepare("SELECT count(*) as n FROM graphql_source_header WHERE source_id = ?")
221 "SELECT count(*) as n FROM graphql_source_header WHERE source_id = ?",
222 )
223 .get("bare"),212 .get("bare"),
224 ).n;213 ).n;
225 expect(headerCount).toBe(0);214 expect(headerCount).toBe(0);
apps/local/src/server/migrate-openapi-bindings.test.ts+1−3
@@ -283,9 +283,7 @@ describe("0007_normalize_plugin_secret_refs (openapi)", () => { · 282 unmodified lines
283 const after = openDatabase(dbPath, { readonly: true });283 const after = openDatabase(dbPath, { readonly: true });
284 const qpCount = Schema.decodeUnknownSync(CountRow)(284 const qpCount = Schema.decodeUnknownSync(CountRow)(
285 after285 after
286 .prepare(286 .prepare("SELECT count(*) as n FROM openapi_source_query_param WHERE source_id = ?")
287 "SELECT count(*) as n FROM openapi_source_query_param WHERE source_id = ?",
288 )
289 .get("bare"),287 .get("bare"),
290 ).n;288 ).n;
291 expect(qpCount).toBe(0);289 expect(qpCount).toBe(0);
apps/local/src/server/observability.ts+1−1
@@ -24,7 +24,7 @@ export const ErrorCaptureLive: Layer.Layer<ErrorCapture> = Layer.succeed( · 23 unmodified lines
24 console.error(24 console.error(
25 `[executor ${traceId}]`,25 `[executor ${traceId}]`,
26 // oxlint-disable-next-line executor/no-instanceof-error -- boundary: console logger preserves native Error stack output26 // oxlint-disable-next-line executor/no-instanceof-error -- boundary: console logger preserves native Error stack output
27 squashed instanceof Error ? squashed.stack ?? squashed : squashed,27 squashed instanceof Error ? (squashed.stack ?? squashed) : squashed,
28 );28 );
29 console.error(`[executor ${traceId}] cause:`, Cause.pretty(cause));29 console.error(`[executor ${traceId}] cause:`, Cause.pretty(cause));
30 return traceId;30 return traceId;
apps/marketing/src/styles/global.css+3−2
@@ -218,7 +218,9 @@ body { · 217 unmodified lines
218 text-decoration: underline;218 text-decoration: underline;
219 text-decoration-color: color-mix(in srgb, var(--color-teal) 35%, transparent);219 text-decoration-color: color-mix(in srgb, var(--color-teal) 35%, transparent);
220 text-underline-offset: 3px;220 text-underline-offset: 3px;
221 transition: color 0.15s ease, text-decoration-color 0.15s ease;221 transition:
222 color 0.15s ease,
223 text-decoration-color 0.15s ease;
222}224}
223 225
224.legal-prose a:hover {226.legal-prose a:hover {
@@ -230,4 +232,3 @@ body { · 5 unmodified lines
230 color: var(--color-ink);232 color: var(--color-ink);
231 font-weight: 600;233 font-weight: 600;
232}234}
233
autumn.config.ts+64−64
@@ -2,84 +2,84 @@ import { feature, item, plan } from "atmn"; · 1 unmodified line
2 2
3// Features3// Features
4export const executions = feature({4export const executions = feature({
5 id: 'executions',5 id: "executions",
6 name: 'Executions',6 name: "Executions",
7 type: 'metered',7 type: "metered",
8 consumable: true,8 consumable: true,
9});9});
10 10
11export const domainVerification = feature({11export const domainVerification = feature({
12 id: 'domain-verification',12 id: "domain-verification",
13 name: 'Domain Verification',13 name: "Domain Verification",
14 type: 'boolean',14 type: "boolean",
15});15});
16 16
17export const members = feature({17export const members = feature({
18 id: 'members',18 id: "members",
19 name: 'Members',19 name: "Members",
20 type: 'metered',20 type: "metered",
21 consumable: false,21 consumable: false,
22 archived: true,22 archived: true,
23});23});
24 24
25// Plans25// Plans
26export const free = plan({26export const free = plan({
27 id: 'free',27 id: "free",
28 name: 'Free',28 name: "Free",
29 addOn: false,29 addOn: false,
30 autoEnable: true,30 autoEnable: true,
31 items: [31 items: [
32 item({32 item({
33 featureId: executions.id,33 featureId: executions.id,
34 included: 10000,34 included: 10000,
35 reset: {35 reset: {
36 interval: 'month',36 interval: "month",
37 },37 },
38 }),38 }),
39 ],39 ],
40});40});
41 41
42export const freePayAsYouGo = plan({42export const freePayAsYouGo = plan({
43 id: 'free-pay-as-you-go',43 id: "free-pay-as-you-go",
44 name: 'Free Pay As You Go',44 name: "Free Pay As You Go",
45 addOn: false,45 addOn: false,
46 autoEnable: false,46 autoEnable: false,
47 items: [47 items: [
48 item({48 item({
49 featureId: executions.id,49 featureId: executions.id,
50 included: 10000,50 included: 10000,
51 price: {51 price: {
52 amount: 0.2,52 amount: 0.2,
53 billingUnits: 1000,53 billingUnits: 1000,
54 billingMethod: 'usage_based',54 billingMethod: "usage_based",
55 interval: 'month',55 interval: "month",
56 },56 },
57 }),57 }),
58 ],58 ],
59});59});
60 60
61export const team = plan({61export const team = plan({
62 id: 'team',62 id: "team",
63 name: 'Team',63 name: "Team",
64 addOn: false,64 addOn: false,
65 autoEnable: false,65 autoEnable: false,
66 price: {66 price: {
67 amount: 49,67 amount: 49,
68 interval: 'month',68 interval: "month",
69 },69 },
70 items: [70 items: [
71 item({71 item({
72 featureId: executions.id,72 featureId: executions.id,
73 included: 250000,73 included: 250000,
74 price: {74 price: {
75 amount: 0.2,75 amount: 0.2,
76 billingUnits: 1000,76 billingUnits: 1000,
77 billingMethod: 'usage_based',77 billingMethod: "usage_based",
78 interval: 'month',78 interval: "month",
79 },79 },
80 }),80 }),
81 item({81 item({
82 featureId: domainVerification.id,82 featureId: domainVerification.id,
83 }),83 }),
84 ],84 ],
85});85});
examples/all-plugins/src/main.ts+3−10
@@ -141,17 +141,13 @@ const exampleOpenApiSpec = JSON.stringify({ · 140 unmodified lines
141 get: {141 get: {
142 operationId: "items.get",142 operationId: "items.get",
143 tags: ["items"],143 tags: ["items"],
144 parameters: [144 parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
145 { name: "id", in: "path", required: true, schema: { type: "string" } },
146 ],
147 responses: { "200": { description: "ok" } },145 responses: { "200": { description: "ok" } },
148 },146 },
149 delete: {147 delete: {
150 operationId: "items.delete",148 operationId: "items.delete",
151 tags: ["items"],149 tags: ["items"],
152 parameters: [150 parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
153 { name: "id", in: "path", required: true, schema: { type: "string" } },
154 ],
155 responses: { "204": { description: "deleted" } },151 responses: { "204": { description: "deleted" } },
156 },152 },
157 },153 },
@@ -400,10 +396,7 @@ const program = Effect.gen(function* () { · 242 unmodified lines
400 console.log(" executor.keychain.isSupported:", executor.keychain.isSupported);396 console.log(" executor.keychain.isSupported:", executor.keychain.isSupported);
401 console.log(" executor.keychain.displayName:", executor.keychain.displayName);397 console.log(" executor.keychain.displayName:", executor.keychain.displayName);
402 398
403 console.log(399 console.log(" executor.fileSecrets.filePath: ", executor.fileSecrets.filePath);
404 " executor.fileSecrets.filePath: ",
405 executor.fileSecrets.filePath,
406 );
407 400
408 // executor.mcp.addSource({ connector: { kind: "remote", endpoint: "..." } });401 // executor.mcp.addSource({ connector: { kind: "remote", endpoint: "..." } });
409 // executor.googleDiscovery.addSource({ discoveryUrl: "..." });402 // executor.googleDiscovery.addSource({ discoveryUrl: "..." });
notes/artifacts-workflows-and-generated-ui.md+45−71
@@ -64,13 +64,7 @@ Do not make this workflow-specific. Model the generic thing as an artifact · 63 unmodified lines
64project, with workflows as the first consumer.64project, with workflows as the first consumer.
65 65
66```ts66```ts
67type ArtifactKind =67type ArtifactKind = "workflow" | "component" | "ui" | "template" | "connector" | "document";
68 | "workflow"
69 | "component"
70 | "ui"
71 | "template"
72 | "connector"
73 | "document";
74 68
75type ArtifactProject = {69type ArtifactProject = {
76 id: string;70 id: string;
@@ -171,18 +165,13 @@ export interface ArtifactStore { · 94 unmodified lines
171 input: CreateArtifactProjectInput,165 input: CreateArtifactProjectInput,
172 ) => Effect.Effect<ArtifactProject, ArtifactError>;166 ) => Effect.Effect<ArtifactProject, ArtifactError>;
173 167
174 readonly readFile: (168 readonly readFile: (ref: ArtifactRef, path: string) => Effect.Effect<Uint8Array, ArtifactError>;
175 ref: ArtifactRef,
176 path: string,
177 ) => Effect.Effect<Uint8Array, ArtifactError>;
178 169
179 readonly writeFiles: (170 readonly writeFiles: (
180 input: WriteArtifactFilesInput,171 input: WriteArtifactFilesInput,
181 ) => Effect.Effect<ArtifactCommit, ArtifactError>;172 ) => Effect.Effect<ArtifactCommit, ArtifactError>;
182 173
183 readonly diff: (174 readonly diff: (input: ArtifactDiffInput) => Effect.Effect<ArtifactDiff, ArtifactError>;
184 input: ArtifactDiffInput,
185 ) => Effect.Effect<ArtifactDiff, ArtifactError>;
186 175
187 readonly forkProject: (176 readonly forkProject: (
188 input: ForkArtifactProjectInput,177 input: ForkArtifactProjectInput,
@@ -365,10 +354,7 @@ export interface ArtifactStore { · 176 unmodified lines
365 name: string;354 name: string;
366 }) => Effect.Effect<ArtifactProject, ArtifactError>;355 }) => Effect.Effect<ArtifactProject, ArtifactError>;
367 356
368 readonly readFile: (357 readonly readFile: (ref: ArtifactRef, path: string) => Effect.Effect<Uint8Array, ArtifactError>;
369 ref: ArtifactRef,
370 path: string,
371 ) => Effect.Effect<Uint8Array, ArtifactError>;
372 358
373 readonly writeFiles: (input: {359 readonly writeFiles: (input: {
374 projectId: string;360 projectId: string;
@@ -422,10 +408,7 @@ Feature plugin: · 47 unmodified lines
422```ts408```ts
423// @executor/workflows409// @executor/workflows
424import { definePlugin, type PluginCtx } from "@executor/sdk";410import { definePlugin, type PluginCtx } from "@executor/sdk";
425import {411import { ArtifactStoreProtocol, type ArtifactsProtocolCtx } from "@executor/artifacts";
426 ArtifactStoreProtocol,
427 type ArtifactsProtocolCtx,
428} from "@executor/artifacts";
429 412
430type WorkflowCtx<TStore> = PluginCtx<TStore, ArtifactsProtocolCtx>;413type WorkflowCtx<TStore> = PluginCtx<TStore, ArtifactsProtocolCtx>;
431 414
@@ -546,56 +529,47 @@ export async function summarizeTicket(input: SupportEmail) { · 114 unmodified lines
546E2E flow:529E2E flow:
547 530
548```ts531```ts
549it.effect(532it.effect("creates a code-first workflow artifact and indexes a visual graph", () =>
550 "creates a code-first workflow artifact and indexes a visual graph",533 Effect.gen(function* () {
551 () =>534 const executor = yield* createExecutor({
552 Effect.gen(function* () {535 scopes: [testScope],
553 const executor = yield* createExecutor({536 adapter,
554 scopes: [testScope],537 blobs,
555 adapter,538 plugins: [artifactsLocalGitPlugin({ rootDir: tempDir }), workflowsPlugin()] as const,
556 blobs,539 protocols: {
557 plugins: [540 "executor.artifacts.store": "artifacts-local-git",
558 artifactsLocalGitPlugin({ rootDir: tempDir }),541 },
559 workflowsPlugin(),542 });
560 ] as const,543
561 protocols: {544 const created = yield* executor.workflows.create({
562 "executor.artifacts.store": "artifacts-local-git",545 name: "Support triage",
546 files: [
547 {
548 type: "put",
549 path: "artifact.json",
550 contents: JSON.stringify({
551 schemaVersion: 1,
552 kind: "workflow",
553 entrypoints: ["src/workflows/support-triage.workflow.ts"],
554 }),
555 },
556 {
557 type: "put",
558 path: "src/workflows/support-triage.workflow.ts",
559 contents: supportTriageWorkflowSource,
563 },560 },
564 });561 ],
565 562 });
566 const created = yield* executor.workflows.create({563
567 name: "Support triage",564 const source = yield* executor.artifacts.readFile(
568 files: [565 { projectId: created.project.id, ref: created.commit.id },
569 {566 "src/workflows/support-triage.workflow.ts",
570 type: "put",567 );
571 path: "artifact.json",568 expect(new TextDecoder().decode(source)).toContain('"use workflow"');
572 contents: JSON.stringify({569
573 schemaVersion: 1,570 expect(created.manifest.entrypoints).toEqual(["src/workflows/support-triage.workflow.ts"]);
574 kind: "workflow",571 expect(created.manifest.graph.nodes.map((node) => node.kind)).toContain("step");
575 entrypoints: ["src/workflows/support-triage.workflow.ts"],572 }),
576 }),
577 },
578 {
579 type: "put",
580 path: "src/workflows/support-triage.workflow.ts",
581 contents: supportTriageWorkflowSource,
582 },
583 ],
584 });
585
586 const source = yield* executor.artifacts.readFile(
587 { projectId: created.project.id, ref: created.commit.id },
588 "src/workflows/support-triage.workflow.ts",
589 );
590 expect(new TextDecoder().decode(source)).toContain('"use workflow"');
591
592 expect(created.manifest.entrypoints).toEqual([
593 "src/workflows/support-triage.workflow.ts",
594 ]);
595 expect(created.manifest.graph.nodes.map((node) => node.kind)).toContain(
596 "step",
597 );
598 }),
599);573);
600```574```
601 575
notes/dynamic-plugin-loading-v1.md+177−187
@@ -89,7 +89,7 @@ Surveyed five plugin systems in `.reference/`: · 88 unmodified lines
89- **emdash** — **closest match to our setup.** Astro/Vite + React, plugins are89- **emdash** — **closest match to our setup.** Astro/Vite + React, plugins are
90 npm packages with separate `./` and `./admin` exports, registered via90 npm packages with separate `./` and `./admin` exports, registered via
91 import-and-call in `astro.config.mjs`. `definePlugin({ id, hooks, routes,91 import-and-call in `astro.config.mjs`. `definePlugin({ id, hooks, routes,
92 admin })` declarative shape.92admin })` declarative shape.
93- **dynamic-software** — most ambitious; Cloudflare Worker Loader for cloud93- **dynamic-software** — most ambitious; Cloudflare Worker Loader for cloud
94 isolation, iframe + postMessage RPC for UI, Proxy-based typed API client.94 isolation, iframe + postMessage RPC for UI, Proxy-based typed API client.
95 95
@@ -109,10 +109,10 @@ inference flows naturally; no codegen step. · 13 unmodified lines
109 109
110```ts110```ts
111// apps/local/executor.config.ts (after)111// apps/local/executor.config.ts (after)
112import { defineExecutorConfig } from "@executor-js/sdk"112import { defineExecutorConfig } from "@executor-js/sdk";
113import { openApiPlugin } from "@executor-js/plugin-openapi"113import { openApiPlugin } from "@executor-js/plugin-openapi";
114import { mcpPlugin } from "@executor-js/plugin-mcp"114import { mcpPlugin } from "@executor-js/plugin-mcp";
115import { fileSecretsPlugin } from "@executor-js/plugin-file-secrets"115import { fileSecretsPlugin } from "@executor-js/plugin-file-secrets";
116 116
117export default defineExecutorConfig({117export default defineExecutorConfig({
118 dialect: "sqlite",118 dialect: "sqlite",
@@ -121,7 +121,7 @@ export default defineExecutorConfig({ · 2 unmodified lines
121 mcpPlugin({ dangerouslyAllowStdioMCP: true }),121 mcpPlugin({ dangerouslyAllowStdioMCP: true }),
122 fileSecretsPlugin(),122 fileSecretsPlugin(),
123 ] as const,123 ] as const,
124})124});
125```125```
126 126
127"Dynamic" means npm-installable, not load-by-string-name. Same model as emdash.127"Dynamic" means npm-installable, not load-by-string-name. Same model as emdash.
@@ -137,12 +137,12 @@ config becomes a need. · 9 unmodified lines
137 "type": "module",137 "type": "module",
138 "exports": {138 "exports": {
139 "./server": "./dist/server.js",139 "./server": "./dist/server.js",
140 "./client": "./dist/client.js"140 "./client": "./dist/client.js",
141 },141 },
142 "executor": {142 "executor": {
143 "id": "foo",143 "id": "foo",
144 "version": "0.1.0"144 "version": "0.1.0",
145 }145 },
146}146}
147```147```
148 148
@@ -171,7 +171,7 @@ Client side: separate primitive `defineClientPlugin` lives in · 22 unmodified lines
171React types never leak into server bundles.171React types never leak into server bundles.
172 172
173**Layering — extension is the canonical SDK; routes/handlers is optional HTTP transport.**173**Layering — extension is the canonical SDK; routes/handlers is optional HTTP transport.**
174The HTTP layer is *not a peer* of the SDK; it's a transport over it. Plugin174The HTTP layer is _not a peer_ of the SDK; it's a transport over it. Plugin
175authors should treat extension as the implementation and write handlers as175authors should treat extension as the implementation and write handlers as
176thin wrappers that delegate via the `self` parameter (same pattern as176thin wrappers that delegate via the `self` parameter (same pattern as
177`staticSources(self) => [...]` already in the codebase). This keeps:177`staticSources(self) => [...]` already in the codebase). This keeps:
@@ -188,7 +188,7 @@ Three plugin shapes fall out of this layering: · 10 unmodified lines
188 utility for other plugins or scripts. CLI/embedded consumers use188 utility for other plugins or scripts. CLI/embedded consumers use
189 `executor.<id>.method()`. Vite plugin notices no `./client` and skips the189 `executor.<id>.method()`. Vite plugin notices no `./client` and skips the
190 plugin in the frontend bundle entirely.190 plugin in the frontend bundle entirely.
191- **Both.** Extension *and* routes/handlers *and* a `./client`. Examples:191- **Both.** Extension _and_ routes/handlers _and_ a `./client`. Examples:
192 openapi, mcp, anything that needs a frontend. Routes are thin wrappers192 openapi, mcp, anything that needs a frontend. Routes are thin wrappers
193 over extension methods.193 over extension methods.
194- **HTTP-only.** Rare — webhook receivers, OAuth callback URLs. Routes194- **HTTP-only.** Rare — webhook receivers, OAuth callback URLs. Routes
@@ -214,7 +214,7 @@ Plugin authors can write a complete plugin importing only from · 19 unmodified lines
214`@executor-js/sdk` (server) and `@executor-js/sdk/client` (frontend). The SDK214`@executor-js/sdk` (server) and `@executor-js/sdk/client` (frontend). The SDK
215re-exports `Schema`, `HttpApi`, `HttpApiGroup`, `HttpApiEndpoint`,215re-exports `Schema`, `HttpApi`, `HttpApiGroup`, `HttpApiEndpoint`,
216`HttpApiBuilder`, `Effect` so authors who don't want to dig into Effect216`HttpApiBuilder`, `Effect` so authors who don't want to dig into Effect
217don't have to. Authors who *do* want Effect-native code keep importing from217don't have to. Authors who _do_ want Effect-native code keep importing from
218`effect` directly. This keeps the door open without forcing the dependency.218`effect` directly. This keeps the door open without forcing the dependency.
219 219
220### 6. Skip `capabilities` declaration for v1220### 6. Skip `capabilities` declaration for v1
@@ -247,7 +247,7 @@ already handles this exact pattern for secrets and works fine: · 26 unmodified lines
247secretProviders: (ctx) => [makeScopedProvider(...)]247secretProviders: (ctx) => [makeScopedProvider(...)]
248```248```
249 249
250Each new pluggable capability gets its *own* typed field on `PluginSpec`,250Each new pluggable capability gets its _own_ typed field on `PluginSpec`,
251same shape. When artifacts lands, that's `artifactStore: () =>251same shape. When artifacts lands, that's `artifactStore: () =>
252ArtifactStore`. If connection providers want to be modeled this way, the252ArtifactStore`. If connection providers want to be modeled this way, the
253existing `connectionProviders` field already is.253existing `connectionProviders` field already is.
@@ -335,24 +335,21 @@ server both import it: · 81 unmodified lines
335 335
336```ts336```ts
337// @executor-js/plugin-foo/src/shared.ts337// @executor-js/plugin-foo/src/shared.ts
338import { HttpApiEndpoint, HttpApiGroup, Schema } from "@executor-js/sdk"338import { HttpApiEndpoint, HttpApiGroup, Schema } from "@executor-js/sdk";
339 339
340export const Thing = Schema.Struct({340export const Thing = Schema.Struct({
341 id: Schema.String,341 id: Schema.String,
342 name: Schema.String,342 name: Schema.String,
343})343});
344 344
345export const FooApi = HttpApiGroup.make("foo")345export const FooApi = HttpApiGroup.make("foo")
346 .add(346 .add(HttpApiEndpoint.get("listThings")`/things`.addSuccess(Schema.Array(Thing)))
347 HttpApiEndpoint.get("listThings")`/things`
348 .addSuccess(Schema.Array(Thing)),
349 )
350 .add(347 .add(
351 HttpApiEndpoint.post("syncThing")`/sync/:id`348 HttpApiEndpoint.post("syncThing")`/sync/:id`
352 .setPath(Schema.Struct({ id: Schema.String }))349 .setPath(Schema.Struct({ id: Schema.String }))
353 .addSuccess(Thing)350 .addSuccess(Thing)
354 .addError(SyncError),351 .addError(SyncError),
355 )352 );
356```353```
357 354
358```ts355```ts
@@ -429,49 +426,47 @@ Lives in `@executor-js/sdk/client`. Server bundles cannot import this module. · 70 unmodified lines
429```ts426```ts
430// packages/core/sdk/src/client.ts427// packages/core/sdk/src/client.ts
431export interface ClientPluginSpec<TId extends string = string> {428export interface ClientPluginSpec<TId extends string = string> {
432 readonly id: TId429 readonly id: TId;
433 430
434 /** Pages contributed to the host's TanStack router. Mounted under431 /** Pages contributed to the host's TanStack router. Mounted under
435 * /plugins/{id}/{path}. Sidebar nav metadata declared on the route. */432 * /plugins/{id}/{path}. Sidebar nav metadata declared on the route. */
436 readonly pages?: readonly PageDecl[]433 readonly pages?: readonly PageDecl[];
437 434
438 /** Dashboard / overview widgets the host can render in known slots. */435 /** Dashboard / overview widgets the host can render in known slots. */
439 readonly widgets?: readonly WidgetDecl[]436 readonly widgets?: readonly WidgetDecl[];
440 437
441 /** Components the host can render in named slots (e.g., source-detail438 /** Components the host can render in named slots (e.g., source-detail
442 * panels, secret-picker variants). Slot names are part of the host439 * panels, secret-picker variants). Slot names are part of the host
443 * contract — plugin opts in by registering. */440 * contract — plugin opts in by registering. */
444 readonly slots?: Record<string, SlotComponent>441 readonly slots?: Record<string, SlotComponent>;
445 442
446 /** Source-plugin contribution — populated by plugins that expose443 /** Source-plugin contribution — populated by plugins that expose
447 * `kind` rows in the core `source` table (openapi, mcp, graphql,444 * `kind` rows in the core `source` table (openapi, mcp, graphql,
448 * google-discovery). The host's sources page derives its provider445 * google-discovery). The host's sources page derives its provider
449 * list from the union of every loaded plugin's `sourcePlugin`446 * list from the union of every loaded plugin's `sourcePlugin`
450 * via `useSourcePlugins()`. */447 * via `useSourcePlugins()`. */
451 readonly sourcePlugin?: SourcePlugin448 readonly sourcePlugin?: SourcePlugin;
452 449
453 /** Secret-provider-plugin contribution — populated by plugins that450 /** Secret-provider-plugin contribution — populated by plugins that
454 * also ship a `secretProviders` server-side capability AND want to451 * also ship a `secretProviders` server-side capability AND want to
455 * expose a settings card on the host's secrets page. Surfaced via452 * expose a settings card on the host's secrets page. Surfaced via
456 * `useSecretProviderPlugins()`. */453 * `useSecretProviderPlugins()`. */
457 readonly secretProviderPlugin?: SecretProviderPlugin454 readonly secretProviderPlugin?: SecretProviderPlugin;
458}455}
459 456
460type PageDecl = {457type PageDecl = {
461 path: string // "/", "/edit/$id"458 path: string; // "/", "/edit/$id"
462 component: ComponentType459 component: ComponentType;
463 nav?: { label: string; section?: string }460 nav?: { label: string; section?: string };
464}461};
465 462
466type WidgetDecl = {463type WidgetDecl = {
467 id: string464 id: string;
468 component: ComponentType<WidgetProps>465 component: ComponentType<WidgetProps>;
469 size?: "half" | "full"466 size?: "half" | "full";
470}467};
471 468
472export const defineClientPlugin = <TId extends string>(469export const defineClientPlugin = <TId extends string>(spec: ClientPluginSpec<TId>) => spec;
473 spec: ClientPluginSpec<TId>,
474) => spec
475```470```
476 471
477### `createPluginAtomClient` — typed reactive client per plugin472### `createPluginAtomClient` — typed reactive client per plugin
@@ -483,24 +478,21 @@ authors write one line per atom. Same shape as the existing · 5 unmodified lines
483 478
484```ts479```ts
485// packages/core/sdk/src/client.ts (helper)480// packages/core/sdk/src/client.ts (helper)
486import { HttpApi } from "effect/unstable/httpapi"481import { HttpApi } from "effect/unstable/httpapi";
487import { FetchHttpClient } from "effect/unstable/http"482import { FetchHttpClient } from "effect/unstable/http";
488import * as AtomHttpApi from "effect/unstable/reactivity/AtomHttpApi"483import * as AtomHttpApi from "effect/unstable/reactivity/AtomHttpApi";
489 484
490export const createPluginAtomClient = <G extends HttpApiGroup.Any>(485export const createPluginAtomClient = <G extends HttpApiGroup.Any>(
491 group: G,486 group: G,
492 opts: { pluginId: string },487 opts: { pluginId: string },
493) => {488) => {
494 const bundle = HttpApi.make(`plugin-${opts.pluginId}`).add(group)489 const bundle = HttpApi.make(`plugin-${opts.pluginId}`).add(group);
495 return AtomHttpApi.Service<`Plugin_${string}Client`>()(490 return AtomHttpApi.Service<`Plugin_${string}Client`>()(`Plugin_${opts.pluginId}Client`, {
496 `Plugin_${opts.pluginId}Client`,491 api: bundle,
497 {492 httpClient: FetchHttpClient.layer,
498 api: bundle,493 baseUrl: `/_executor/plugins/${opts.pluginId}`,
499 httpClient: FetchHttpClient.layer,494 });
500 baseUrl: `/_executor/plugins/${opts.pluginId}`,495};
501 },
502 )
503}
504```496```
505 497
506Plugin author writes:498Plugin author writes:
@@ -513,39 +505,34 @@ import { · 6 unmodified lines
513 useAtomValue,505 useAtomValue,
514 useAtomSet,506 useAtomSet,
515 AsyncResult,507 AsyncResult,
516} from "@executor-js/sdk/client"508} from "@executor-js/sdk/client";
517import { FooApi } from "./shared"509import { FooApi } from "./shared";
518 510
519const FooClient = createPluginAtomClient(FooApi, { pluginId: "foo" })511const FooClient = createPluginAtomClient(FooApi, { pluginId: "foo" });
520 512
521export const fooThingsAtom = FooClient.query("foo", "listThings", {513export const fooThingsAtom = FooClient.query("foo", "listThings", {
522 timeToLive: "30 seconds",514 timeToLive: "30 seconds",
523 reactivityKeys: ["foo:things"],515 reactivityKeys: ["foo:things"],
524})516});
525 517
526export const fooSync = FooClient.mutation("foo", "syncThing")518export const fooSync = FooClient.mutation("foo", "syncThing");
527 519
528const FooPage = () => {520const FooPage = () => {
529 const things = useAtomValue(fooThingsAtom)521 const things = useAtomValue(fooThingsAtom);
530 const doSync = useAtomSet(fooSync, { mode: "promise" })522 const doSync = useAtomSet(fooSync, { mode: "promise" });
531 523
532 return AsyncResult.match(things, {524 return AsyncResult.match(things, {
533 onInitial: () => <Skeleton />,525 onInitial: () => <Skeleton />,
534 onFailure: () => <p>Failed to load</p>,526 onFailure: () => <p>Failed to load</p>,
535 onSuccess: ({ value }) => (527 onSuccess: ({ value }) => <Table rows={value} onSync={(id) => doSync({ path: { id } })} />,
536 <Table528 });
537 rows={value}529};
538 onSync={(id) => doSync({ path: { id } })}
539 />
540 ),
541 })
542}
543 530
544export default defineClientPlugin({531export default defineClientPlugin({
545 id: "foo" as const,532 id: "foo" as const,
546 pages: [{ path: "/", component: FooPage, nav: { label: "Foo" } }],533 pages: [{ path: "/", component: FooPage, nav: { label: "Foo" } }],
547 widgets: [{ id: "foo-status", component: FooStatus, size: "half" }],534 widgets: [{ id: "foo-status", component: FooStatus, size: "half" }],
548})535});
549```536```
550 537
551Type inference: `FooClient.query("foo", "listThings", ...)` is fully typed538Type inference: `FooClient.query("foo", "listThings", ...)` is fully typed
@@ -608,15 +595,17 @@ Single config, two consumers. · 56 unmodified lines
608 595
609```ts596```ts
610// apps/local/src/server/executor.ts (after)597// apps/local/src/server/executor.ts (after)
611import config from "../../executor.config.ts"598import config from "../../executor.config.ts";
612 599
613const executor = yield* createExecutor({600const executor =
614 scopes: [scope],601 yield *
615 adapter,602 createExecutor({
616 blobs,603 scopes: [scope],
617 plugins: config.plugins,604 adapter,
618 onElicitation: "accept-all",605 blobs,
619})606 plugins: config.plugins,
607 onElicitation: "accept-all",
608 });
620```609```
621 610
622The plugins are already configured (factory called) by the time611The plugins are already configured (factory called) by the time
@@ -631,18 +620,16 @@ export default function executorVite(): Plugin { · 8 unmodified lines
631 return {620 return {
632 name: "executor-plugins",621 name: "executor-plugins",
633 resolveId(id) {622 resolveId(id) {
634 if (id === "virtual:executor/plugins-client") return "\0" + id623 if (id === "virtual:executor/plugins-client") return "\0" + id;
635 },624 },
636 async load(id) {625 async load(id) {
637 if (id !== "\0virtual:executor/plugins-client") return626 if (id !== "\0virtual:executor/plugins-client") return;
638 const config = await loadExecutorConfig()627 const config = await loadExecutorConfig();
639 const imports = config.plugins628 const imports = config.plugins.map((p, i) => `import p${i} from "${p.id}/client"`).join("\n");
640 .map((p, i) => `import p${i} from "${p.id}/client"`)629 const list = config.plugins.map((_, i) => `p${i}`).join(", ");
641 .join("\n")630 return `${imports}\nexport const plugins = [${list}]`;
642 const list = config.plugins.map((_, i) => `p${i}`).join(", ")
643 return `${imports}\nexport const plugins = [${list}]`
644 },631 },
645 }632 };
646}633}
647```634```
648 635
@@ -655,11 +642,11 @@ file — `__root.tsx` is the only place that touches · 6 unmodified lines
655 642
656```tsx643```tsx
657// apps/local/src/routes/__root.tsx644// apps/local/src/routes/__root.tsx
658import { createRootRoute } from "@tanstack/react-router"645import { createRootRoute } from "@tanstack/react-router";
659import { ExecutorProvider } from "@executor-js/react/api/provider"646import { ExecutorProvider } from "@executor-js/react/api/provider";
660import { ExecutorPluginsProvider } from "@executor-js/sdk/client"647import { ExecutorPluginsProvider } from "@executor-js/sdk/client";
661import { plugins as clientPlugins } from "virtual:executor/plugins-client"648import { plugins as clientPlugins } from "virtual:executor/plugins-client";
662import { Shell } from "../web/shell"649import { Shell } from "../web/shell";
663 650
664export const Route = createRootRoute({651export const Route = createRootRoute({
665 component: () => (652 component: () => (
@@ -669,15 +656,15 @@ export const Route = createRootRoute({ · 3 unmodified lines
669 </ExecutorPluginsProvider>656 </ExecutorPluginsProvider>
670 </ExecutorProvider>657 </ExecutorProvider>
671 ),658 ),
672})659});
673```660```
674 661
675```tsx662```tsx
676// e.g. inside packages/react/src/pages/sources.tsx663// e.g. inside packages/react/src/pages/sources.tsx
677import { useSourcePlugins } from "@executor-js/sdk/client"664import { useSourcePlugins } from "@executor-js/sdk/client";
678 665
679export function SourcesPage() {666export function SourcesPage() {
680 const sourcePlugins = useSourcePlugins()667 const sourcePlugins = useSourcePlugins();
681 // ...668 // ...
682}669}
683```670```
@@ -701,49 +688,48 @@ needs a Vite restart (not a hot update — config changed). · 17 unmodified lines
701 688
702```ts689```ts
703// src/shared.ts — only @executor-js/sdk imports, no raw effect imports690// src/shared.ts — only @executor-js/sdk imports, no raw effect imports
704import { HttpApiEndpoint, HttpApiGroup, Schema } from "@executor-js/sdk"691import { HttpApiEndpoint, HttpApiGroup, Schema } from "@executor-js/sdk";
705 692
706export const Greeting = Schema.Struct({693export const Greeting = Schema.Struct({
707 message: Schema.String,694 message: Schema.String,
708 count: Schema.Number,695 count: Schema.Number,
709})696});
710export type Greeting = typeof Greeting.Type697export type Greeting = typeof Greeting.Type;
711 698
712export const ExampleApi = HttpApiGroup.make("example").add(699export const ExampleApi = HttpApiGroup.make("example").add(
713 HttpApiEndpoint.post("greet", "/greet", {700 HttpApiEndpoint.post("greet", "/greet", {
714 payload: Schema.Struct({ name: Schema.String }),701 payload: Schema.Struct({ name: Schema.String }),
715 success: Greeting,702 success: Greeting,
716 }),703 }),
717)704);
718```705```
719 706
720```ts707```ts
721// src/server.ts708// src/server.ts
722import {709import { Context, definePlugin, Effect, HttpApi, HttpApiBuilder } from "@executor-js/sdk";
723 Context, definePlugin, Effect, HttpApi, HttpApiBuilder,710import { ExampleApi } from "./shared";
724} from "@executor-js/sdk"
725import { ExampleApi } from "./shared"
726 711
727const ExampleApiBundle = HttpApi.make("example").add(ExampleApi)712const ExampleApiBundle = HttpApi.make("example").add(ExampleApi);
728 713
729interface ExampleExtension {714interface ExampleExtension {
730 readonly greet: (715 readonly greet: (
731 name: string,716 name: string,
732 ) => Effect.Effect<{ readonly message: string; readonly count: number }>717 ) => Effect.Effect<{ readonly message: string; readonly count: number }>;
733}718}
734 719
735export class ExampleExtensionService extends Context.Service<720export class ExampleExtensionService extends Context.Service<
736 ExampleExtensionService, ExampleExtension,721 ExampleExtensionService,
722 ExampleExtension
737>()("ExampleExtensionService") {}723>()("ExampleExtensionService") {}
738 724
739const ExampleHandlers = HttpApiBuilder.group(ExampleApiBundle, "example", (h) =>725const ExampleHandlers = HttpApiBuilder.group(ExampleApiBundle, "example", (h) =>
740 h.handle("greet", ({ payload }) =>726 h.handle("greet", ({ payload }) =>
741 Effect.gen(function* () {727 Effect.gen(function* () {
742 const ext = yield* ExampleExtensionService728 const ext = yield* ExampleExtensionService;
743 return yield* ext.greet(payload.name)729 return yield* ext.greet(payload.name);
744 }),730 }),
745 ),731 ),
746)732);
747 733
748export const examplePlugin = definePlugin(() => ({734export const examplePlugin = definePlugin(() => ({
749 id: "example" as const,735 id: "example" as const,
@@ -754,57 +740,57 @@ export const examplePlugin = definePlugin(() => ({ · 4 unmodified lines
754 extension: (ctx): ExampleExtension => ({740 extension: (ctx): ExampleExtension => ({
755 greet: (name: string) =>741 greet: (name: string) =>
756 Effect.sync(() => {742 Effect.sync(() => {
757 ctx.storage.count += 1743 ctx.storage.count += 1;
758 return {744 return {
759 message: `hello ${name}`,745 message: `hello ${name}`,
760 count: ctx.storage.count,746 count: ctx.storage.count,
761 }747 };
762 }),748 }),
763 }),749 }),
764 750
765 routes: () => ExampleApi,751 routes: () => ExampleApi,
766 handlers: () => ExampleHandlers,752 handlers: () => ExampleHandlers,
767 extensionService: ExampleExtensionService,753 extensionService: ExampleExtensionService,
768}))754}));
769 755
770export default examplePlugin756export default examplePlugin;
771```757```
772 758
773```tsx759```tsx
774// src/client.tsx760// src/client.tsx
775import {761import { defineClientPlugin, createPluginAtomClient, useAtomSet } from "@executor-js/sdk/client";
776 defineClientPlugin,762import { useState } from "react";
777 createPluginAtomClient,763import { ExampleApi } from "./shared";
778 useAtomSet,
779} from "@executor-js/sdk/client"
780import { useState } from "react"
781import { ExampleApi } from "./shared"
782 764
783const ExampleClient = createPluginAtomClient(ExampleApi, { pluginId: "example" })765const ExampleClient = createPluginAtomClient(ExampleApi, { pluginId: "example" });
784 766
785const greetAtom = ExampleClient.mutation("example", "greet")767const greetAtom = ExampleClient.mutation("example", "greet");
786 768
787const ExamplePage = () => {769const ExamplePage = () => {
788 const [name, setName] = useState("world")770 const [name, setName] = useState("world");
789 const [result, setResult] = useState<string>()771 const [result, setResult] = useState<string>();
790 const doGreet = useAtomSet(greetAtom, { mode: "promise" })772 const doGreet = useAtomSet(greetAtom, { mode: "promise" });
791 773
792 return (774 return (
793 <div>775 <div>
794 <input value={name} onChange={(e) => setName(e.target.value)} />776 <input value={name} onChange={(e) => setName(e.target.value)} />
795 <button onClick={async () => {777 <button
796 const g = await doGreet({ payload: { name } })778 onClick={async () => {
797 setResult(`${g.message} (#${g.count})`)779 const g = await doGreet({ payload: { name } });
798 }}>Greet</button>780 setResult(`${g.message} (#${g.count})`);
781 }}
782 >
783 Greet
784 </button>
799 {result && <pre>{result}</pre>}785 {result && <pre>{result}</pre>}
800 </div>786 </div>
801 )787 );
802}788};
803 789
804export default defineClientPlugin({790export default defineClientPlugin({
805 id: "example" as const,791 id: "example" as const,
806 pages: [{ path: "/", component: ExamplePage, nav: { label: "Example" } }],792 pages: [{ path: "/", component: ExamplePage, nav: { label: "Example" } }],
807})793});
808```794```
809 795
810```jsonc796```jsonc
@@ -814,13 +800,13 @@ export default defineClientPlugin({ · 3 unmodified lines
814 "type": "module",800 "type": "module",
815 "exports": {801 "exports": {
816 "./server": "./dist/server.js",802 "./server": "./dist/server.js",
817 "./client": "./dist/client.js"803 "./client": "./dist/client.js",
818 },804 },
819 "executor": { "id": "example", "version": "0.1.0" },805 "executor": { "id": "example", "version": "0.1.0" },
820 "peerDependencies": {806 "peerDependencies": {
821 "@executor-js/sdk": "workspace:*",807 "@executor-js/sdk": "workspace:*",
822 "react": "catalog:"808 "react": "catalog:",
823 }809 },
824}810}
825```811```
826 812
@@ -828,8 +814,8 @@ User adds it: · 1 unmodified line
828 814
829```ts815```ts
830// apps/local/executor.config.ts816// apps/local/executor.config.ts
831import { examplePlugin } from "@executor-js/plugin-example/server"817import { examplePlugin } from "@executor-js/plugin-example/server";
832plugins: [/* ... */, examplePlugin()]818plugins: [, /* ... */ examplePlugin()];
833```819```
834 820
835## SDK-only plugin example821## SDK-only plugin example
@@ -852,81 +838,82 @@ file with no `./client` export, no `routes`, no `handlers`. · 16 unmodified lines
852 "type": "module",838 "type": "module",
853 "exports": { "./server": "./dist/server.js" },839 "exports": { "./server": "./dist/server.js" },
854 "executor": { "id": "rateLimiter", "version": "0.1.0" },840 "executor": { "id": "rateLimiter", "version": "0.1.0" },
855 "peerDependencies": { "@executor-js/sdk": "workspace:*" }841 "peerDependencies": { "@executor-js/sdk": "workspace:*" },
856}842}
857```843```
858 844
859```ts845```ts
860// src/server.ts846// src/server.ts
861import { definePlugin, defineSchema, Effect, Schema } from "@executor-js/sdk"847import { definePlugin, defineSchema, Effect, Schema } from "@executor-js/sdk";
862 848
863interface RateLimiterConfig {849interface RateLimiterConfig {
864 defaultLimit?: number850 defaultLimit?: number;
865}851}
866 852
867class RateLimitExceeded extends Schema.TaggedError<RateLimitExceeded>()(853class RateLimitExceeded extends Schema.TaggedError<RateLimitExceeded>()("RateLimitExceeded", {
868 "RateLimitExceeded",854 key: Schema.String,
869 { key: Schema.String, retryAfterMs: Schema.Number },855 retryAfterMs: Schema.Number,
870) {}856}) {}
871 857
872export const rateLimiterPlugin = definePlugin(858export const rateLimiterPlugin = definePlugin((opts: RateLimiterConfig = {}) => ({
873 (opts: RateLimiterConfig = {}) => ({859 id: "rateLimiter" as const,
874 id: "rateLimiter" as const,860
875 861 schema: defineSchema({
876 schema: defineSchema({862 rate_buckets: {
877 rate_buckets: {863 fields: {
878 fields: {864 key: { type: "string", primary: true },
879 key: { type: "string", primary: true },865 tokens: { type: "number" },
880 tokens: { type: "number" },866 updated_at: { type: "number" },
881 updated_at: { type: "number" },
882 },
883 },867 },
884 }),868 },
869 }),
885 870
886 storage: ({ adapter }) => ({871 storage: ({ adapter }) => ({
887 adapter,872 adapter,
888 defaultLimit: opts.defaultLimit ?? 60,873 defaultLimit: opts.defaultLimit ?? 60,
889 }),874 }),
890 875
891 extension: (ctx) => ({876 extension: (ctx) => ({
892 check: (key: string, cost = 1) =>877 check: (key: string, cost = 1) =>
893 Effect.gen(function* () {878 Effect.gen(function* () {
894 const bucket = yield* readOrCreateBucket(ctx.storage, key)879 const bucket = yield* readOrCreateBucket(ctx.storage, key);
895 const refilled = refill(bucket, ctx.storage.defaultLimit)880 const refilled = refill(bucket, ctx.storage.defaultLimit);
896 if (refilled.tokens < cost) {881 if (refilled.tokens < cost) {
897 return yield* new RateLimitExceeded({882 return yield* new RateLimitExceeded({
898 key,883 key,
899 retryAfterMs: estimateRetry(refilled),884 retryAfterMs: estimateRetry(refilled),
900 })885 });
901 }886 }
902 yield* writeBucket(ctx.storage, key, refilled.tokens - cost)887 yield* writeBucket(ctx.storage, key, refilled.tokens - cost);
903 return { allowed: true, remaining: refilled.tokens - cost }888 return { allowed: true, remaining: refilled.tokens - cost };
904 }),889 }),
905 890
906 reset: (key: string) =>891 reset: (key: string) =>
907 Effect.tryPromise(() =>892 Effect.tryPromise(() =>
908 ctx.storage.adapter.delete({893 ctx.storage.adapter.delete({
909 model: "rate_buckets",894 model: "rate_buckets",
910 where: [["key", "=", key]],895 where: [["key", "=", key]],
911 }),896 }),
912 ),897 ),
913 }),
914 }),898 }),
915)899}));
916```900```
917 901
918Pure programmatic consumption — works identically from CLI, tests,902Pure programmatic consumption — works identically from CLI, tests,
919embedded library use, or another plugin's `extension`:903embedded library use, or another plugin's `extension`:
920 904
921```ts905```ts
922const result = yield* executor.rateLimiter.check("user-123", 5)906const result = yield * executor.rateLimiter.check("user-123", 5);
923// ^? { allowed: true; remaining: number }907// ^? { allowed: true; remaining: number }
924 908
925yield* executor.rateLimiter.check("user-123", 1000).pipe(909yield *
926 Effect.catchTag("RateLimitExceeded", (err) =>910 executor.rateLimiter
927 Effect.log(`hit limit on ${err.key}, retry in ${err.retryAfterMs}ms`),911 .check("user-123", 1000)
928 ),912 .pipe(
929)913 Effect.catchTag("RateLimitExceeded", (err) =>
914 Effect.log(`hit limit on ${err.key}, retry in ${err.retryAfterMs}ms`),
915 ),
916 );
930```917```
931 918
932What the host does with this plugin:919What the host does with this plugin:
@@ -1000,6 +987,7 @@ Build order — all shipped. · 67 unmodified lines
1000## References987## References
1001 988
1002**Reference research (pre-design):**989**Reference research (pre-design):**
990
1003- emdash: `astro.config.mjs` import-and-call pattern, `package.json` exports991- emdash: `astro.config.mjs` import-and-call pattern, `package.json` exports
1004 for separate admin entry — see `.reference/emdash/demos/plugins-demo/`.992 for separate admin entry — see `.reference/emdash/demos/plugins-demo/`.
1005- dynamic-software: PluginManifest + capability declarations idea (deferred);993- dynamic-software: PluginManifest + capability declarations idea (deferred);
@@ -1008,6 +996,7 @@ Build order — all shipped. · 2 unmodified lines
1008 when we add capability enforcement).996 when we add capability enforcement).
1009 997
1010**Shipped code:**998**Shipped code:**
999
1011- Plugin contract: `packages/core/sdk/src/plugin.ts` (`PluginSpec`,1000- Plugin contract: `packages/core/sdk/src/plugin.ts` (`PluginSpec`,
1012 `definePlugin`).1001 `definePlugin`).
1013- Client contract: `packages/core/sdk/src/client.ts`1002- Client contract: `packages/core/sdk/src/client.ts`
@@ -1030,6 +1019,7 @@ Build order — all shipped. · 16 unmodified lines
1030 follows the same per-capability-typed-field pattern.1019 follows the same per-capability-typed-field pattern.
1031 1020
1032**Adjacent plans:**1021**Adjacent plans:**
1022
1033- `notes/artifacts-workflows-and-generated-ui.md` — the artifacts/workflows1023- `notes/artifacts-workflows-and-generated-ui.md` — the artifacts/workflows
1034 plan. Uses "protocol" terminology that we're not adopting; read its1024 plan. Uses "protocol" terminology that we're not adopting; read its
1035 "ArtifactStoreProtocol" as "the typed interface that goes into the1025 "ArtifactStoreProtocol" as "the typed interface that goes into the
notes/mcp-conn-pool.md+9−7
@@ -4,7 +4,7 @@ · 3 unmodified lines
4 4
5The MCP plugin **already pools connections within a session DO**. Production5The MCP plugin **already pools connections within a session DO**. Production
6telemetry attributing 17% of `plugin.mcp.connection.acquire` calls to a6telemetry attributing 17% of `plugin.mcp.connection.acquire` calls to a
7fresh `plugin.mcp.connection.handshake` represents *cold-start sessions*,7fresh `plugin.mcp.connection.handshake` represents _cold-start sessions_,
8not redundant in-session handshakes. No structural change is required;8not redundant in-session handshakes. No structural change is required;
9this PR ships a strict regression test that pins the existing contract9this PR ships a strict regression test that pins the existing contract
10plus a `plugin.mcp.cache_hit` span attribute so future telemetry can10plus a `plugin.mcp.cache_hit` span attribute so future telemetry can
@@ -68,11 +68,12 @@ mutating the cache key per call) trips immediately in CI. · 57 unmodified lines
68## Re-reading the production trace68## Re-reading the production trace
69 69
70Trace `b7102047bed975da461c0519d1251de4`:70Trace `b7102047bed975da461c0519d1251de4`:
71
71- `mcp.plugin.resolve_connector` 2.72s72- `mcp.plugin.resolve_connector` 2.72s
72- `plugin.mcp.connection.acquire` 2.72s73- `plugin.mcp.connection.acquire` 2.72s
73- `plugin.mcp.connection.handshake` 2.52s74- `plugin.mcp.connection.handshake` 2.52s
74- `executor.storage.transaction` 1.02s (token persist)75- `executor.storage.transaction` 1.02s (token persist)
75- `plugin.mcp.client.call_tool` 1.48s76- `plugin.mcp.client.call_tool` 1.48s
76 77
77Span nesting plus `resolve_connector ≈ acquire ≈ handshake` durations is78Span nesting plus `resolve_connector ≈ acquire ≈ handshake` durations is
78the cache-miss path: `resolveConnector` only runs (and emits its span)79the cache-miss path: `resolveConnector` only runs (and emits its span)
@@ -80,11 +81,12 @@ when the cache lookup actually invokes the lookup closure, which only · 1 unmodified line
80happens on a miss.81happens on a miss.
81 82
828h aggregate:838h aggregate:
84
83- `plugin.mcp.connection.handshake` count 485- `plugin.mcp.connection.handshake` count 4
84- `plugin.mcp.connection.acquire` count 2486- `plugin.mcp.connection.acquire` count 24
85- `mcp.plugin.resolve_connector` count 487- `mcp.plugin.resolve_connector` count 4
86 88
87Acquire count is 6× handshake count. The cache *is* hitting on 20 of 2489Acquire count is 6× handshake count. The cache _is_ hitting on 20 of 24
88calls. The 4 misses are cold-start sessions (each MCP session DO's first90calls. The 4 misses are cold-start sessions (each MCP session DO's first
89tool call, plus any session where the connection went stale and was91tool call, plus any session where the connection went stale and was
90invalidated by the retry path). Six tool calls per session is consistent92invalidated by the retry path). Six tool calls per session is consistent
notes/old/connections-migration.md+1−1
@@ -21,7 +21,7 @@ to preserve auth state across the upgrade. · 20 unmodified lines
21 21
221. `INSERT INTO connection` with the new pointer's `connectionId`,221. `INSERT INTO connection` with the new pointer's `connectionId`,
23 `provider = "openapi:oauth2"`, and `provider_state` = `{flow,23 `provider = "openapi:oauth2"`, and `provider_state` = `{flow,
24 tokenUrl, clientIdSecretId, clientSecretSecretId, scopes}` lifted24tokenUrl, clientIdSecretId, clientSecretSecretId, scopes}` lifted
25 from the legacy row. `access_token_secret_id` + `refresh_token_secret_id`25 from the legacy row. `access_token_secret_id` + `refresh_token_secret_id`
26 reuse the legacy secret ids — tokens themselves never move, only26 reuse the legacy secret ids — tokens themselves never move, only
27 ownership links flip.27 ownership links flip.
notes/old/error-handling.md+4−4
@@ -87,7 +87,7 @@ Owns the translation, the opaque wire schema, and the capture service. · 86 unmodified lines
87 87
88 ```ts88 ```ts
89 interface ErrorCaptureShape {89 interface ErrorCaptureShape {
90 readonly captureException: (cause: Cause<unknown>) => Effect<string>90 readonly captureException: (cause: Cause<unknown>) => Effect<string>;
91 }91 }
92 ```92 ```
93 93
@@ -130,7 +130,7 @@ Plugin authors write normal Effect code. Their extension method error · 36 unmodified lines
130unions look like:130unions look like:
131 131
132```ts132```ts
133Effect.Effect<X, MyPluginTypedError | StorageError, never>133Effect.Effect<X, MyPluginTypedError | StorageError, never>;
134```134```
135 135
136Where `MyPluginTypedError` is the union of their own136Where `MyPluginTypedError` is the union of their own
@@ -152,9 +152,9 @@ class McpGroup extends HttpApiGroup.make("mcp") · 15 unmodified lines
152 // …152 // …
153 .addError(InternalError)153 .addError(InternalError)
154 .addError(McpOAuthError)154 .addError(McpOAuthError)
155 .addError(McpConnectionError)155 .addError(McpConnectionError) {
156 // …156 // …
157{}157}
158```158```
159 159
160No per-endpoint `addError`. The framework encodes each tagged error by160No per-endpoint `addError`. The framework encodes each tagged error by
notes/old/errored-sources.md+2−1
@@ -54,7 +54,7 @@ source: { · 53 unmodified lines
54Effect.catchAll((e) => {54Effect.catchAll((e) => {
55 const message = e instanceof Error ? e.message : String(e);55 const message = e instanceof Error ? e.message : String(e);
56 return executor.sources.recordError({ source, message }).pipe(Effect.asVoid);56 return executor.sources.recordError({ source, message }).pipe(Effect.asVoid);
57})57});
58```58```
59 59
60`executor.sources.recordError` is a new core method that writes a60`executor.sources.recordError` is a new core method that writes a
@@ -76,6 +76,7 @@ a special case — current `removeSource` already works regardless of · 15 unmodified lines
76## Scope76## Scope
77 77
78Not for the scope-refactor PR. Separate change:78Not for the scope-refactor PR. Separate change:
79
79- Schema migration adds two columns.80- Schema migration adds two columns.
80- `executor.sources.recordError` on the core SDK.81- `executor.sources.recordError` on the core SDK.
81- Wire up in config-sync.82- Wire up in config-sync.
notes/old/indexes.md+1−1
@@ -25,7 +25,7 @@ Tables affected (all drop candidates): · 24 unmodified lines
25- `workos_vault_metadata_scope_id_idx`25- `workos_vault_metadata_scope_id_idx`
26 26
27Root cause: `core-schema.ts` and plugin schemas mark `scope_id: {27Root cause: `core-schema.ts` and plugin schemas mark `scope_id: {
28index: true }`. The drizzle generator should probably *skip* emitting28index: true }`. The drizzle generator should probably _skip_ emitting
29a single-column index on a column that's already the leftmost column29a single-column index on a column that's already the leftmost column
30of a composite primary key. Fix in two places:30of a composite primary key. Fix in two places:
31 31
notes/old/mcp-testing.md+6−6
@@ -5,10 +5,10 @@ deleting one silently drops coverage that the others can't replace. · 4 unmodified lines
5 5
6## Suites at a glance6## Suites at a glance
7 7
8| file | pool | drives | what it proves |8| file | pool | drives | what it proves |
9|---|---|---|---|9| ----------------------------------------------- | ----------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
10| `apps/cloud/src/mcp-session.e2e.node.test.ts` | node | `InMemoryTransport` + SDK `Client` | engine + plugin wiring; schema drift; elicitation semantics |10| `apps/cloud/src/mcp-session.e2e.node.test.ts` | node | `InMemoryTransport` + SDK `Client` | engine + plugin wiring; schema drift; elicitation semantics |
11| `apps/cloud/src/mcp-flow.test.ts` | workerd (vitest-pool-workers) | `SELF.fetch` + hand-rolled JSON-RPC | edge HTTP pipeline that does not require a live multi-request DO session |11| `apps/cloud/src/mcp-flow.test.ts` | workerd (vitest-pool-workers) | `SELF.fetch` + hand-rolled JSON-RPC | edge HTTP pipeline that does not require a live multi-request DO session |
12| `apps/cloud/src/mcp-miniflare.e2e.node.test.ts` | node + miniflare-on-real-port | SDK `Client` + real `StreamableHTTPClientTransport` | the long-lived-socket DO runtime actually works end-to-end; elicitation round-trips real HTTP |12| `apps/cloud/src/mcp-miniflare.e2e.node.test.ts` | node + miniflare-on-real-port | SDK `Client` + real `StreamableHTTPClientTransport` | the long-lived-socket DO runtime actually works end-to-end; elicitation round-trips real HTTP |
13 13
14## The workerd cross-request I/O wall14## The workerd cross-request I/O wall
@@ -86,12 +86,12 @@ Pattern used in `mcp-miniflare.e2e.node.test.ts`: · 71 unmodified lines
863. Read the bound port from `HttpServer.HttpServer.address` (TcpAddress tag)863. Read the bound port from `HttpServer.HttpServer.address` (TcpAddress tag)
87 inside a `Layer.effect` that produces the test service.87 inside a `Layer.effect` that produces the test service.
884. Generate the spec with `OpenApi.fromApi(api)` and inject884. Generate the spec with `OpenApi.fromApi(api)` and inject
89 `servers: [{ url: `http://127.0.0.1:${port}` }]` before `JSON.stringify`.89 `servers: [{ url: `http://127.0.0.1:${port}` }]`before`JSON.stringify`.
905. SDK Client advertises `capabilities: { elicitation: { form: {} } }` and905. SDK Client advertises `capabilities: { elicitation: { form: {} } }` and
91 registers `client.setRequestHandler(ElicitRequestSchema, ...)` that returns91 registers `client.setRequestHandler(ElicitRequestSchema, ...)` that returns
92 `{ action: "accept", content: {} }`.92 `{ action: "accept", content: {} }`.
936. Call `execute` with code that (a) calls `tools.openapi.addSource({ spec,936. Call `execute` with code that (a) calls `tools.openapi.addSource({ spec,
94 namespace })`, (b) invokes the POST operation.94namespace })`, (b) invokes the POST operation.
95 95
96**Tool id format inside the `execute` sandbox.**96**Tool id format inside the `execute` sandbox.**
97`tools.<sourceId>.<group>.<operation>` for openapi. The Effect97`tools.<sourceId>.<group>.<operation>` for openapi. The Effect
notes/old/pluggable-storage.md+45−37
@@ -35,7 +35,9 @@ export interface CoreStore { · 34 unmodified lines
35 35
36 // definitions36 // definitions
37 readonly putDefinitions: (defs: readonly StoredDefinition[]) => Effect.Effect<void, Error>;37 readonly putDefinitions: (defs: readonly StoredDefinition[]) => Effect.Effect<void, Error>;
38 readonly listDefinitionsBySource: (sourceId: string) => Effect.Effect<readonly StoredDefinition[], Error>;38 readonly listDefinitionsBySource: (
39 sourceId: string,
40 ) => Effect.Effect<readonly StoredDefinition[], Error>;
39 readonly removeDefinitionsBySource: (sourceId: string) => Effect.Effect<void, Error>;41 readonly removeDefinitionsBySource: (sourceId: string) => Effect.Effect<void, Error>;
40 42
41 // secrets (routing rows; provider-specific data lives in provider)43 // secrets (routing rows; provider-specific data lives in provider)
@@ -45,10 +47,9 @@ export interface CoreStore { · 3 unmodified lines
45 readonly removeSecret: (id: string) => Effect.Effect<void, Error>;47 readonly removeSecret: (id: string) => Effect.Effect<void, Error>;
46}48}
47 49
48export const makeDefaultCoreStore = (deps: {50export const makeDefaultCoreStore = (deps: { adapter: DBAdapter; scope: Scope }): CoreStore => {
49 adapter: DBAdapter;51 /* typedAdapter<CoreSchema> wiring, one place */
50 scope: Scope;52};
51}): CoreStore => { /* typedAdapter<CoreSchema> wiring, one place */ };
52```53```
53 54
54## Wiring — defaults passed in deps, force the spread55## Wiring — defaults passed in deps, force the spread
@@ -70,9 +71,7 @@ export interface ExecutorConfig<TPlugins> { · 15 unmodified lines
70 71
71// Inside createExecutor:72// Inside createExecutor:
72const defaults = makeDefaultCoreStore({ adapter, scope });73const defaults = makeDefaultCoreStore({ adapter, scope });
73const core = config.coreStorage74const core = config.coreStorage ? config.coreStorage({ adapter, scope, defaults }) : defaults;
74 ? config.coreStorage({ adapter, scope, defaults })
75 : defaults;
76// Executor now calls core.putTools(...), core.removeSource(...), etc.75// Executor now calls core.putTools(...), core.removeSource(...), etc.
77// No more adapter.create({ model: "tool", ... }) scattered through executor.ts.76// No more adapter.create({ model: "tool", ... }) scattered through executor.ts.
78```77```
@@ -83,7 +82,7 @@ const core = config.coreStorage · 4 unmodified lines
83 with no type help.82 with no type help.
84- Anyone implementing from scratch (test doubles, a non-drizzle backend)83- Anyone implementing from scratch (test doubles, a non-drizzle backend)
85 gets no squigglies when they miss a method.84 gets no squigglies when they miss a method.
86- Defaults-in-deps gets "only write what you override" ergonomics *and*85- Defaults-in-deps gets "only write what you override" ergonomics _and_
87 keeps the full `CoreStore` return type enforced.86 keeps the full `CoreStore` return type enforced.
88 87
89## What it unlocks88## What it unlocks
@@ -91,30 +90,39 @@ const core = config.coreStorage · 1 unmodified line
91**Algolia sidecar — no string-match, just method overrides:**90**Algolia sidecar — no string-match, just method overrides:**
92 91
93```ts92```ts
94const executor = yield* createExecutor({93const executor =
95 scope, adapter, blobs, plugins,94 yield *
96 95 createExecutor({
97 coreStorage: ({ defaults }) => ({96 scope,
98 ...defaults,97 adapter,
99 putTools: (tools) =>98 blobs,
100 Effect.gen(function* () {99 plugins,
101 yield* defaults.putTools(tools);100
102 yield* Effect.promise(() => algolia.saveObjects({101 coreStorage: ({ defaults }) => ({
103 indexName: `tools_${scope.id}`,102 ...defaults,
104 objects: tools.map((t) => ({ objectID: t.id, ...t })),103 putTools: (tools) =>
105 }));104 Effect.gen(function* () {
106 }),105 yield* defaults.putTools(tools);
107 removeToolsBySource: (sourceId) =>106 yield* Effect.promise(() =>
108 Effect.gen(function* () {107 algolia.saveObjects({
109 const affected = yield* defaults.listTools();108 indexName: `tools_${scope.id}`,
110 yield* defaults.removeToolsBySource(sourceId);109 objects: tools.map((t) => ({ objectID: t.id, ...t })),
111 yield* Effect.promise(() => algolia.deleteObjects({110 }),
112 indexName: `tools_${scope.id}`,111 );
113 objectIDs: affected.filter((t) => t.sourceId === sourceId).map((t) => t.id),112 }),
114 }));113 removeToolsBySource: (sourceId) =>
115 }),114 Effect.gen(function* () {
116 }),115 const affected = yield* defaults.listTools();
117});116 yield* defaults.removeToolsBySource(sourceId);
117 yield* Effect.promise(() =>
118 algolia.deleteObjects({
119 indexName: `tools_${scope.id}`,
120 objectIDs: affected.filter((t) => t.sourceId === sourceId).map((t) => t.id),
121 }),
122 );
123 }),
124 }),
125 });
118```126```
119 127
120Test doubles become trivial — either hand-roll the whole `CoreStore`128Test doubles become trivial — either hand-roll the whole `CoreStore`
@@ -128,13 +136,13 @@ The factory name leaks. Rename to the defaults-in-deps shape: · 7 unmodified lines
128 136
129```ts137```ts
130// before138// before
131storage: (deps) => makeDefaultOpenapiStore(deps)139storage: (deps) => makeDefaultOpenapiStore(deps);
132 140
133// after141// after
134storage: ({ defaults }) => defaults // no-op, identical to leaving off `storage:`142storage: ({ defaults }) => defaults; // no-op, identical to leaving off `storage:`
135 143
136// override one method144// override one method
137storage: ({ defaults }) => ({ ...defaults, upsertSource: myUpsert })145storage: ({ defaults }) => ({ ...defaults, upsertSource: myUpsert });
138```146```
139 147
140Mechanical sweep across openapi / mcp / graphql / workos-vault plugins.148Mechanical sweep across openapi / mcp / graphql / workos-vault plugins.
@@ -152,6 +160,6 @@ Not intellectually hard, but a broad diff: · 11 unmodified lines
152 factory renamed to follow the `defaults` contract.160 factory renamed to follow the `defaults` contract.
153- Docs/readme updates showing the new override pattern.161- Docs/readme updates showing the new override pattern.
154 162
155Ideally lands *before* pluggable search — the search sidecar story163Ideally lands _before_ pluggable search — the search sidecar story
156depends on this refactor to avoid the `if (model === "tool")` antipattern164depends on this refactor to avoid the `if (model === "tool")` antipattern
157I'd otherwise be stuck with.165I'd otherwise be stuck with.
notes/old/promise-sdk-typed-errors.md+3−1
@@ -6,7 +6,7 @@ · 5 unmodified lines
6`Effect.runPromise(...)`. That works but it's lossy at the Promise6`Effect.runPromise(...)`. That works but it's lossy at the Promise
7boundary:7boundary:
8 8
9- The promise's *rejection type* is `unknown` (or `any` after9- The promise's _rejection type_ is `unknown` (or `any` after
10 `try/catch`). Consumers have to `instanceof InternalError` /10 `try/catch`). Consumers have to `instanceof InternalError` /
11 `err._tag === "..."` to discriminate, even though the underlying11 `err._tag === "..."` to discriminate, even though the underlying
12 Effect had a fully typed error union.12 Effect had a fully typed error union.
@@ -43,12 +43,14 @@ Do we expose Effect's `Exit` / `Cause` directly to promise consumers, · 30 unmodified lines
43or wrap them in a promise-native `Result<A, E>`?43or wrap them in a promise-native `Result<A, E>`?
44 44
45**Expose `Exit`/`Cause`:**45**Expose `Exit`/`Cause`:**
46
46- Pros: one fewer abstraction, no translation layer to maintain, cause47- Pros: one fewer abstraction, no translation layer to maintain, cause
47 preserves parallel/sequential composition.48 preserves parallel/sequential composition.
48- Cons: consumers depend on `effect`'s API even though they're using a49- Cons: consumers depend on `effect`'s API even though they're using a
49 "Promise" SDK — partly defeats the abstraction.50 "Promise" SDK — partly defeats the abstraction.
50 51
51**Wrap in a `Result<A, E>`:**52**Wrap in a `Result<A, E>`:**
53
52- Pros: `@executor-js/sdk/promise` consumers don't import `effect` at all,54- Pros: `@executor-js/sdk/promise` consumers don't import `effect` at all,
53 surface stays small.55 surface stays small.
54- Cons: another type to learn / document; loses some Cause structure56- Cons: another type to learn / document; loses some Cause structure
notes/old/rls.md+3−3
@@ -54,8 +54,8 @@ And app-side, once per request inside a transaction (so `SET LOCAL` · 53 unmodified lines
54takes):54takes):
55 55
56```ts56```ts
57yield* sql`SET LOCAL app.scope_chain = ${toPgArray(chain)}`;57yield * sql`SET LOCAL app.scope_chain = ${toPgArray(chain)}`;
58yield* sql`SET LOCAL app.write_scope = ${writeScope}`;58yield * sql`SET LOCAL app.write_scope = ${writeScope}`;
59```59```
60 60
61`current_setting('…', true)` returns null when the setting is missing,61`current_setting('…', true)` returns null when the setting is missing,
@@ -69,7 +69,7 @@ scope sees zero rows, not an error that might be handled into a 500. · 7 unmodified lines
69 tables is whatever `executor-schema.ts` exports with a `scope_id`69 tables is whatever `executor-schema.ts` exports with a `scope_id`
70 column — same set the scope adapter already knows about.70 column — same set the scope adapter already knows about.
712. Add an Effect layer that wraps the DbService to issue the two `SET712. Add an Effect layer that wraps the DbService to issue the two `SET
72 LOCAL`s at connection acquire. `postgres.js`'s `sql.begin` already72LOCAL`s at connection acquire. `postgres.js`'s `sql.begin` already
73 gives us the transaction boundary.73 gives us the transaction boundary.
743. Leave `BYPASSRLS` off the worker's role so we actually get the743. Leave `BYPASSRLS` off the worker's role so we actually get the
75 enforcement; grant it on the role used by the migration / admin75 enforcement; grant it on the role used by the migration / admin
notes/old/scoped-source-auth.md+2−7
@@ -20,7 +20,7 @@ The common primitive is not "org auth" or "user auth". It is an ordered · 19 unmodified lines
20scope stack:20scope stack:
21 21
22```ts22```ts
23[user, workspace, org]23[user, workspace, org];
24```24```
25 25
26Rows still belong to exactly one scope. Resolution walks the stack26Rows still belong to exactly one scope. Resolution walks the stack
@@ -68,12 +68,7 @@ For example: · 41 unmodified lines
68 68
69```ts69```ts
70oauth2: {70oauth2: {
71 flow: "clientCredentials",71 flow: ("clientCredentials", tokenUrl, clientIdSlot, clientSecretSlot, connectionSlot, scopes);
72 tokenUrl,
73 clientIdSlot,
74 clientSecretSlot,
75 connectionSlot,
76 scopes
77}72}
78```73```
79 74
notes/old/scopes.md+6−6
@@ -10,15 +10,15 @@ scopes by construction. · 9 unmodified lines
10 10
11- **SDK** — `createExecutor({ scope: Scope, adapter, blobs, plugins })`.11- **SDK** — `createExecutor({ scope: Scope, adapter, blobs, plugins })`.
12 One `Scope` per executor instance. `scopeAdapter(rootAdapter, {...},12 One `Scope` per executor instance. `scopeAdapter(rootAdapter, {...},
13 schema)` wraps the adapter before it reaches plugin storage or the13schema)` wraps the adapter before it reaches plugin storage or the
14 core-table writers. Every read on a scoped table gets `where scope_id14 core-table writers. Every read on a scoped table gets `where scope_id
15 = scope.id` ANDed in; every write gets `scope_id = scope.id` stamped15= scope.id` ANDed in; every write gets `scope_id = scope.id` stamped
16 into the payload. Tx handles passed into transaction callbacks are16 into the payload. Tx handles passed into transaction callbacks are
17 also wrapped, so nested writes stay inside the same scope.17 also wrapped, so nested writes stay inside the same scope.
18 18
19- **Plugins** — see a plain `DBAdapter`. They do not know or care about19- **Plugins** — see a plain `DBAdapter`. They do not know or care about
20 scope. Every plugin schema that wants isolation declares `scope_id:20 scope. Every plugin schema that wants isolation declares `scope_id:
21 { type: "string", required: true, index: true }`. Forgetting the21{ type: "string", required: true, index: true }`. Forgetting the
22 column means the adapter passes the plugin's reads/writes through22 column means the adapter passes the plugin's reads/writes through
23 unscoped (documented failure mode, not silent) — tests catch the23 unscoped (documented failure mode, not silent) — tests catch the
24 concrete cases we care about.24 concrete cases we care about.
@@ -44,8 +44,8 @@ sources, tools, secrets, and plugin-owned source detail lookups. · 19 unmodified lines
44 44
45```ts45```ts
46interface ScopeContext {46interface ScopeContext {
47 readonly read: readonly string[]; // precedence-ordered, innermost first47 readonly read: readonly string[]; // precedence-ordered, innermost first
48 readonly write: string; // exactly one scope for writes48 readonly write: string; // exactly one scope for writes
49}49}
50```50```
51 51
@@ -91,7 +91,7 @@ Writes land in exactly one scope chosen by the caller. · 39 unmodified lines
91 there. When a user invokes Gmail, secret resolution walks the scope91 there. When a user invokes Gmail, secret resolution walks the scope
92 stack (user → workspace → org) and only finds a token if that user92 stack (user → workspace → org) and only finds a token if that user
93 personally oauthed. Policy on the source row — `auth_scope_mandate:93 personally oauthed. Policy on the source row — `auth_scope_mandate:
94 "user"` — forces this: secret lookup refuses to return94"user"` — forces this: secret lookup refuses to return
95 workspace-level tokens even if one existed.95 workspace-level tokens even if one existed.
96 96
97- **Local global + per-folder.** `~/.executor/global` holds the outer97- **Local global + per-folder.** `~/.executor/global` holds the outer
notes/old/search.md+6−2
@@ -9,7 +9,7 @@ whatever) without baking any of it into the core. · 8 unmodified lines
9 9
101. `executor.tools.list()` — `SELECT * FROM tool WHERE scope_id = ?`.101. `executor.tools.list()` — `SELECT * FROM tool WHERE scope_id = ?`.
112. In JS, for every tool: normalize + tokenize `id / sourceId / name /112. In JS, for every tool: normalize + tokenize `id / sourceId / name /
12 description`, score per field with weights, apply coverage + exact-12description`, score per field with weights, apply coverage + exact-
13 phrase boosts, filter, sort, slice.13 phrase boosts, filter, sort, slice.
14 14
15O(N) per query over the scope. Fine at hundreds of tools, painful past15O(N) per query over the scope. Fine at hundreds of tools, painful past
@@ -74,11 +74,13 @@ type. Same pattern as plugin `storage:` options and the future `coreStorage:`. · 58 unmodified lines
74## Override shapes74## Override shapes
75 75
76**Replace entirely:**76**Replace entirely:**
77
77```ts78```ts
78search: ({ listTools }) => algoliaSearch({ listTools }),79search: ({ listTools }) => algoliaSearch({ listTools }),
79```80```
80 81
81**Decorate — mix in logging/timing:**82**Decorate — mix in logging/timing:**
83
82```ts84```ts
83search: ({ defaults }) => ({85search: ({ defaults }) => ({
84 query: (q) => Effect.gen(function* () {86 query: (q) => Effect.gen(function* () {
@@ -91,6 +93,7 @@ search: ({ defaults }) => ({ · 6 unmodified lines
91```93```
92 94
93**Hybrid — try Algolia, fall back to local:**95**Hybrid — try Algolia, fall back to local:**
96
94```ts97```ts
95search: ({ listTools, defaults }) => ({98search: ({ listTools, defaults }) => ({
96 query: (q) =>99 query: (q) =>
@@ -109,6 +112,7 @@ it's a storage concern. Handled by wrapping the core store (see · 12 unmodified lines
109delegate to the default and mirror to the external index.112delegate to the default and mirror to the external index.
110 113
111The two halves are independent:114The two halves are independent:
115
112- Indexing only (manual reindex): override `coreStorage`, leave `search`116- Indexing only (manual reindex): override `coreStorage`, leave `search`
113 as default.117 as default.
114- Search only (some other populates the index): override `search`, leave118- Search only (some other populates the index): override `search`, leave
@@ -122,7 +126,7 @@ scorer: · 7 unmodified lines
1221. **Projection on `tools.list`** — expose a `select` option so search1261. **Projection on `tools.list`** — expose a `select` option so search
123 doesn't pull `input_schema` / `output_schema`. Biggest per-query win.127 doesn't pull `input_schema` / `output_schema`. Biggest per-query win.
1242. **Postgres FTS** — `tsvector` generated column on `(name, description,1282. **Postgres FTS** — `tsvector` generated column on `(name, description,
125 id, source_id)`, GIN index, use `websearch_to_tsquery`. Native129id, source_id)`, GIN index, use `websearch_to_tsquery`. Native
126 ranking, fuzzy via `pg_trgm`. Would replace the in-memory scorer for130 ranking, fuzzy via `pg_trgm`. Would replace the in-memory scorer for
127 the cloud app.131 the cloud app.
1283. **Per-scope LRU** — cache scored results; invalidate on source1323. **Per-scope LRU** — cache scored results; invalidate on source
notes/old/storage-migration.md+23−15
@@ -3,11 +3,13 @@ · 2 unmodified lines
3## What was done3## What was done
4 4
5### 1. DBSchema alignment with better-auth5### 1. DBSchema alignment with better-auth
6
6- Vendored better-auth's `BetterAuthPluginDBSchema` pattern into `storage-core/src/schema.ts`7- Vendored better-auth's `BetterAuthPluginDBSchema` pattern into `storage-core/src/schema.ts`
7- Made `modelName` optional (falls back to the key), renamed `disableMigrations` → `disableMigration`, dropped `order`8- Made `modelName` optional (falls back to the key), renamed `disableMigrations` → `disableMigration`, dropped `order`
8- Removed explicit `modelName` from all plugin schemas (core-schema, openapi, mcp, graphql, google-discovery, workos-vault) — keys already match table names9- Removed explicit `modelName` from all plugin schemas (core-schema, openapi, mcp, graphql, google-discovery, workos-vault) — keys already match table names
9 10
10### 2. @executor-js/cli package (packages/core/cli/)11### 2. @executor-js/cli package (packages/core/cli/)
12
11- Mirrors better-auth's CLI approach: load config → collect plugin schemas → generate drizzle TS13- Mirrors better-auth's CLI approach: load config → collect plugin schemas → generate drizzle TS
12- `executor generate --config ./executor.config.ts --output ./src/services/executor-schema.ts`14- `executor generate --config ./executor.config.ts --output ./src/services/executor-schema.ts`
13- Generator ported from better-auth's drizzle generator, adapted for our DBSchema15- Generator ported from better-auth's drizzle generator, adapted for our DBSchema
@@ -15,11 +17,13 @@ · 1 unmodified line
15- Uses jiti for config loading (supports TS configs)17- Uses jiti for config loading (supports TS configs)
16 18
17### 3. apps/cloud wired up19### 3. apps/cloud wired up
20
18- `executor.config.ts` defines plugins with stub credentials (only used for schema shape)21- `executor.config.ts` defines plugins with stub credentials (only used for schema shape)
19- `drizzle.config.ts` points at both `schema.ts` (cloud tables) and `executor-schema.ts` (executor tables)22- `drizzle.config.ts` points at both `schema.ts` (cloud tables) and `executor-schema.ts` (executor tables)
20- Fresh `drizzle/0000_initial.sql` with all tables (cloud + core + plugin + blob), indexes, FKs23- Fresh `drizzle/0000_initial.sql` with all tables (cloud + core + plugin + blob), indexes, FKs
21 24
22### 4. apps/local wired up25### 4. apps/local wired up
26
23- `executor.config.ts` defines plugins + dialect sqlite27- `executor.config.ts` defines plugins + dialect sqlite
24- `drizzle.config.ts` for drizzle-kit generate/push28- `drizzle.config.ts` for drizzle-kit generate/push
25- Generated `executor-schema.ts` with all sqlite tables + blob table29- Generated `executor-schema.ts` with all sqlite tables + blob table
@@ -27,6 +31,7 @@ · 1 unmodified line
27- At startup: `migrate(db, { migrationsFolder })` from `drizzle-orm/bun-sqlite/migrator` applies pending migrations before constructing the adapter31- At startup: `migrate(db, { migrationsFolder })` from `drizzle-orm/bun-sqlite/migrator` applies pending migrations before constructing the adapter
28 32
29### 5. Adapter DDL removed33### 5. Adapter DDL removed
34
30- `makeSqliteAdapter` and `makePostgresAdapter` no longer run DDL — they return `DBAdapter` directly (not `Effect.Effect<DBAdapter>`)35- `makeSqliteAdapter` and `makePostgresAdapter` no longer run DDL — they return `DBAdapter` directly (not `Effect.Effect<DBAdapter>`)
31- `makeSqliteBlobStore` and `makePostgresBlobStore` no longer run DDL — they return `BlobStore` directly36- `makeSqliteBlobStore` and `makePostgresBlobStore` no longer run DDL — they return `BlobStore` directly
32- `buildCreateTableStatements` deleted from `storage-file/compile.ts`37- `buildCreateTableStatements` deleted from `storage-file/compile.ts`
@@ -39,6 +44,7 @@ · 6 unmodified lines
39The `@executor-js/config` package already maintains `executor.jsonc` as a source of truth for source configurations. The `config-store.ts` decorator intercepts `putSource`/`removeSource` on each plugin and mirrors to this file. So for existing users, `executor.jsonc` already has everything.44The `@executor-js/config` package already maintains `executor.jsonc` as a source of truth for source configurations. The `config-store.ts` decorator intercepts `putSource`/`removeSource` on each plugin and mirrors to this file. So for existing users, `executor.jsonc` already has everything.
40 45
41### Local app migration46### Local app migration
47
421. On first run, `migrate()` creates all tables from the initial migration481. On first run, `migrate()` creates all tables from the initial migration
432. Add a **sync-from-config step** after executor creation:492. Add a **sync-from-config step** after executor creation:
44 - `loadConfig(configPath)` reads `executor.jsonc`50 - `loadConfig(configPath)` reads `executor.jsonc`
@@ -47,28 +53,30 @@ The `@executor-js/config` package already maintains `executor.jsonc` as a source · 2 unmodified lines
473. Secrets survive untouched — they live in keychain/1password/file-secrets, not in the DB. The `secret` routing table gets re-populated when sources are added.533. Secrets survive untouched — they live in keychain/1password/file-secrets, not in the DB. The `secret` routing table gets re-populated when sources are added.
48 54
49### What won't survive (acceptable losses)55### What won't survive (acceptable losses)
56
50- Tool invocation history / blobs (old KV format)57- Tool invocation history / blobs (old KV format)
51- OAuth sessions (ephemeral, users re-auth)58- OAuth sessions (ephemeral, users re-auth)
52- Stale source state (re-fetching is actually a benefit)59- Stale source state (re-fetching is actually a benefit)
53 60
54### Cloud migration61### Cloud migration
62
55- Only ~2 users, just drop old tables and re-add manually63- Only ~2 users, just drop old tables and re-add manually
56- Or write a one-off SQL migration64- Or write a one-off SQL migration
57 65
58## Key files66## Key files
59 67
60| File | Role |68| File | Role |
61|------|------|69| --------------------------------------------- | ----------------------------------------- |
62| `packages/core/cli/src/commands/generate.ts` | CLI generate command |70| `packages/core/cli/src/commands/generate.ts` | CLI generate command |
63| `packages/core/cli/src/generators/drizzle.ts` | Drizzle schema generator |71| `packages/core/cli/src/generators/drizzle.ts` | Drizzle schema generator |
64| `packages/core/sdk/src/config.ts` | `defineExecutorConfig` helper |72| `packages/core/sdk/src/config.ts` | `defineExecutorConfig` helper |
65| `packages/core/config/src/schema.ts` | `executor.jsonc` schema |73| `packages/core/config/src/schema.ts` | `executor.jsonc` schema |
66| `packages/core/config/src/config-store.ts` | Plugin store → config file decorator |74| `packages/core/config/src/config-store.ts` | Plugin store → config file decorator |
67| `packages/core/config/src/load.ts` | Config loading |75| `packages/core/config/src/load.ts` | Config loading |
68| `apps/cloud/executor.config.ts` | Cloud config for CLI |76| `apps/cloud/executor.config.ts` | Cloud config for CLI |
69| `apps/cloud/src/services/executor-schema.ts` | Generated drizzle schema |77| `apps/cloud/src/services/executor-schema.ts` | Generated drizzle schema |
70| `apps/cloud/drizzle/0000_*.sql` | Fresh migration |78| `apps/cloud/drizzle/0000_*.sql` | Fresh migration |
71| `apps/local/executor.config.ts` | Local config for CLI |79| `apps/local/executor.config.ts` | Local config for CLI |
72| `apps/local/src/server/executor-schema.ts` | Generated drizzle schema |80| `apps/local/src/server/executor-schema.ts` | Generated drizzle schema |
73| `apps/local/drizzle/0000_*.sql` | Fresh migration |81| `apps/local/drizzle/0000_*.sql` | Fresh migration |
74| `apps/local/src/server/executor.ts` | Local executor bootstrap with `migrate()` |82| `apps/local/src/server/executor.ts` | Local executor bootstrap with `migrate()` |
notes/research/plugins/manifest-schema.md+92−94
@@ -72,24 +72,24 @@ The manifest should likely be a JSON-serializable object with these top-level se · 71 unmodified lines
72 72
73```ts73```ts
74type PluginManifest = {74type PluginManifest = {
75 schemaVersion: string75 schemaVersion: string;
76 id: string76 id: string;
77 name: string77 name: string;
78 version: string78 version: string;
79 description?: string79 description?: string;
80 publisher?: PluginPublisher80 publisher?: PluginPublisher;
81 license?: string81 license?: string;
82 homepage?: string82 homepage?: string;
83 repository?: string83 repository?: string;
84 categories?: PluginCategory[]84 categories?: PluginCategory[];
85 compatibility: PluginCompatibility85 compatibility: PluginCompatibility;
86 entrypoints: PluginEntrypoints86 entrypoints: PluginEntrypoints;
87 capabilities?: PluginCapabilities87 capabilities?: PluginCapabilities;
88 extensions?: PluginExtensions88 extensions?: PluginExtensions;
89 storage?: PluginStorageDeclaration89 storage?: PluginStorageDeclaration;
90 package?: PluginPackageDeclaration90 package?: PluginPackageDeclaration;
91 metadata?: Record<string, unknown>91 metadata?: Record<string, unknown>;
92}92};
93```93```
94 94
95This is a planning type, not a final API.95This is a planning type, not a final API.
@@ -135,9 +135,7 @@ The install surface should support package sources rather than assuming one cust · 39 unmodified lines
135User-facing install config can stay simple:135User-facing install config can stay simple:
136 136
137```ts137```ts
138type PluginInstallSpec =138type PluginInstallSpec = string | [string, Record<string, unknown>];
139 | string
140 | [string, Record<string, unknown>]
141```139```
142 140
143The string identifies the package source. Examples might include `@executor/openapi`, `github:owner/repo`, `https://github.com/owner/repo.git`, or `./plugins/my-plugin`. Tuple options are installation configuration, not manifest data.141The string identifies the package source. Examples might include `@executor/openapi`, `github:owner/repo`, `https://github.com/owner/repo.git`, or `./plugins/my-plugin`. Tuple options are installation configuration, not manifest data.
@@ -146,10 +144,10 @@ Resolved install metadata should be stored separately from the manifest. It shou · 2 unmodified lines
146 144
147```ts145```ts
148type PluginPackageDeclaration = {146type PluginPackageDeclaration = {
149 source?: "npm" | "git" | "github" | "local" | "registry"147 source?: "npm" | "git" | "github" | "local" | "registry";
150 packageName?: string148 packageName?: string;
151 repository?: string149 repository?: string;
152}150};
153```151```
154 152
155This `package` block is optional planning shape, not final schema. For npm packages, standard `package.json` fields may already provide most of this information.153This `package` block is optional planning shape, not final schema. For npm packages, standard `package.json` fields may already provide most of this information.
@@ -160,12 +158,12 @@ Compatibility should be explicit and checked before plugin code is loaded. · 4 unmodified lines
160 158
161```ts159```ts
162type PluginCompatibility = {160type PluginCompatibility = {
163 executor: string161 executor: string;
164 runtimeApi?: string162 runtimeApi?: string;
165 platforms?: PluginPlatform[]163 platforms?: PluginPlatform[];
166}164};
167 165
168type PluginPlatform = "local" | "cloud"166type PluginPlatform = "local" | "cloud";
169```167```
170 168
171`executor` should probably be a semver range for supported Executor versions.169`executor` should probably be a semver range for supported Executor versions.
@@ -182,27 +180,27 @@ Entrypoints describe executable code or assets the host may load. · 10 unmodified lines
182 180
183```ts181```ts
184type PluginEntrypoints = {182type PluginEntrypoints = {
185 main?: PluginCodeEntrypoint183 main?: PluginCodeEntrypoint;
186 sdk?: PluginCodeEntrypoint184 sdk?: PluginCodeEntrypoint;
187 source?: PluginCodeEntrypoint185 source?: PluginCodeEntrypoint;
188 secretStore?: PluginCodeEntrypoint186 secretStore?: PluginCodeEntrypoint;
189 apiGroup?: PluginCodeEntrypoint187 apiGroup?: PluginCodeEntrypoint;
190 apiHandlers?: PluginCodeEntrypoint188 apiHandlers?: PluginCodeEntrypoint;
191 routeHandlers?: Record<string, PluginCodeEntrypoint>189 routeHandlers?: Record<string, PluginCodeEntrypoint>;
192 backgroundServices?: Record<string, PluginCodeEntrypoint>190 backgroundServices?: Record<string, PluginCodeEntrypoint>;
193 reactSource?: PluginUiEntrypoint191 reactSource?: PluginUiEntrypoint;
194 reactSecretProvider?: PluginUiEntrypoint192 reactSecretProvider?: PluginUiEntrypoint;
195 ui?: PluginUiEntrypoint193 ui?: PluginUiEntrypoint;
196 adminUi?: PluginUiEntrypoint194 adminUi?: PluginUiEntrypoint;
197}195};
198 196
199type PluginCodeEntrypoint = {197type PluginCodeEntrypoint = {
200 module: string198 module: string;
201 export?: string199 export?: string;
202 runtime?: PluginRuntimeTarget200 runtime?: PluginRuntimeTarget;
203}201};
204 202
205type PluginRuntimeTarget = "worker" | "isolate" | "host-trusted"203type PluginRuntimeTarget = "worker" | "isolate" | "host-trusted";
206```204```
207 205
208The exact shape may change, but the manifest should avoid requiring the host to import a plugin just to discover what code exists.206The exact shape may change, but the manifest should avoid requiring the host to import a plugin just to discover what code exists.
@@ -237,21 +235,21 @@ Example planning shape: · 28 unmodified lines
237```ts235```ts
238type PluginLayerDeclaration = {236type PluginLayerDeclaration = {
239 sdk?: {237 sdk?: {
240 pluginId: string238 pluginId: string;
241 entrypoint: keyof PluginEntrypoints239 entrypoint: keyof PluginEntrypoints;
242 }240 };
243 api?: {241 api?: {
244 requiresSdkPlugin: string242 requiresSdkPlugin: string;
245 groupEntrypoint?: keyof PluginEntrypoints243 groupEntrypoint?: keyof PluginEntrypoints;
246 handlersEntrypoint?: keyof PluginEntrypoints244 handlersEntrypoint?: keyof PluginEntrypoints;
247 }245 };
248 react?: Array<{246 react?: Array<{
249 kind: "source" | "secret-provider" | "settings" | "admin"247 kind: "source" | "secret-provider" | "settings" | "admin";
250 requiresSdkPlugin?: string248 requiresSdkPlugin?: string;
251 requiresApi?: boolean249 requiresApi?: boolean;
252 entrypoint: keyof PluginEntrypoints250 entrypoint: keyof PluginEntrypoints;
253 }>251 }>;
254}252};
255```253```
256 254
257This is planning shape, not final schema. The important requirement is that hosts can opt into layers independently while still validating that the selected layers are compatible.255This is planning shape, not final schema. The important requirement is that hosts can opt into layers independently while still validating that the selected layers are compatible.
@@ -264,13 +262,13 @@ Extensions declare what the plugin contributes at a product level. · 6 unmodified lines
264 262
265```ts263```ts
266type PluginExtensions = {264type PluginExtensions = {
267 sources?: SourceExtensionDeclaration[]265 sources?: SourceExtensionDeclaration[];
268 secretStores?: SecretStoreExtensionDeclaration[]266 secretStores?: SecretStoreExtensionDeclaration[];
269 tools?: ToolExtensionDeclaration[]267 tools?: ToolExtensionDeclaration[];
270 routes?: RouteExtensionDeclaration[]268 routes?: RouteExtensionDeclaration[];
271 uiSurfaces?: UiSurfaceDeclaration[]269 uiSurfaces?: UiSurfaceDeclaration[];
272 backgroundServices?: BackgroundServiceDeclaration[]270 backgroundServices?: BackgroundServiceDeclaration[];
273}271};
274```272```
275 273
276Some extension declarations may be fully static. Others may be partial declarations that are completed by plugin registration at load time.274Some extension declarations may be fully static. Others may be partial declarations that are completed by plugin registration at load time.
@@ -283,16 +281,16 @@ Capabilities declare requested access. Grants are separate host state. · 6 unmodified lines
283 281
284```ts282```ts
285type PluginCapabilities = {283type PluginCapabilities = {
286 tools?: ToolCapabilityDeclaration[]284 tools?: ToolCapabilityDeclaration[];
287 sources?: SourceCapabilityDeclaration[]285 sources?: SourceCapabilityDeclaration[];
288 secrets?: SecretCapabilityDeclaration[]286 secrets?: SecretCapabilityDeclaration[];
289 network?: NetworkCapabilityDeclaration[]287 network?: NetworkCapabilityDeclaration[];
290 storage?: StorageCapabilityDeclaration[]288 storage?: StorageCapabilityDeclaration[];
291 scopes?: ScopeCapabilityDeclaration[]289 scopes?: ScopeCapabilityDeclaration[];
292 filesystem?: FilesystemCapabilityDeclaration[]290 filesystem?: FilesystemCapabilityDeclaration[];
293 execution?: ExecutionCapabilityDeclaration[]291 execution?: ExecutionCapabilityDeclaration[];
294 host?: HostCapabilityDeclaration[]292 host?: HostCapabilityDeclaration[];
295}293};
296```294```
297 295
298Capabilities should be deny-by-default. If a capability is missing, the runtime should deny that access.296Capabilities should be deny-by-default. If a capability is missing, the runtime should deny that access.
@@ -344,11 +342,11 @@ The manifest should let plugins declare storage needs separately from storage gr · 45 unmodified lines
344```ts342```ts
345type PluginStorageDeclaration = {343type PluginStorageDeclaration = {
346 collections?: Array<{344 collections?: Array<{
347 name: string345 name: string;
348 kind: "kv" | "document" | "sql"346 kind: "kv" | "document" | "sql";
349 description?: string347 description?: string;
350 }>348 }>;
351}349};
352```350```
353 351
354Storage declarations help the host pre-create, migrate, display, or approve plugin storage.352Storage declarations help the host pre-create, migrate, display, or approve plugin storage.
@@ -364,18 +362,18 @@ Executor does not need a custom publishing flow immediately. npm, GitHub/git, an · 9 unmodified lines
364```ts362```ts
365type PluginArtifacts = {363type PluginArtifacts = {
366 files?: Array<{364 files?: Array<{
367 path: string365 path: string;
368 size?: number366 size?: number;
369 sha256?: string367 sha256?: string;
370 kind?: "code" | "asset" | "manifest" | "source-map"368 kind?: "code" | "asset" | "manifest" | "source-map";
371 }>369 }>;
372 bundle?: {370 bundle?: {
373 format: "esm"371 format: "esm";
374 main: string372 main: string;
375 size?: number373 size?: number;
376 sha256?: string374 sha256?: string;
377 }375 };
378}376};
379```377```
380 378
381Cloud and local do not need to store cached artifacts the same way, but they should agree on immutable identity and verification where possible.379Cloud and local do not need to store cached artifacts the same way, but they should agree on immutable identity and verification where possible.
packages/core/api/src/connections/api.ts+6−15
@@ -1,12 +1,7 @@
1import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";1import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
2import { Schema } from "effect";2import { Schema } from "effect";
3 3
4import {4import { ConnectionId, ConnectionInUseError, ScopeId, Usage } from "@executor-js/sdk";
5 ConnectionId,
6 ConnectionInUseError,
7 ScopeId,
8 Usage,
9} from "@executor-js/sdk";
10 5
11import { InternalError } from "../observability";6import { InternalError } from "../observability";
12 7
@@ -54,13 +49,9 @@ export const ConnectionsApi = HttpApiGroup.make("connections") · 41 unmodified lines
54 }),49 }),
55 )50 )
56 .add(51 .add(
57 HttpApiEndpoint.get(52 HttpApiEndpoint.get("usages", "/scopes/:scopeId/connections/:connectionId/usages", {
58 "usages",53 params: ConnectionParams,
59 "/scopes/:scopeId/connections/:connectionId/usages",54 success: Schema.Array(Usage),
60 {55 error: InternalError,
61 params: ConnectionParams,56 }),
62 success: Schema.Array(Usage),
63 error: InternalError,
64 },
65 ),
66 );57 );
packages/core/api/src/handlers/connections.ts+27−30
@@ -18,35 +18,32 @@ const refToResponse = (ref: ConnectionRef) => ({ · 17 unmodified lines
18 updatedAt: ref.updatedAt.getTime(),18 updatedAt: ref.updatedAt.getTime(),
19});19});
20 20
21export const ConnectionsHandlers = HttpApiBuilder.group(21export const ConnectionsHandlers = HttpApiBuilder.group(ExecutorApi, "connections", (handlers) =>
22 ExecutorApi,22 handlers
23 "connections",23 .handle("list", () =>
24 (handlers) =>24 capture(
25 handlers25 Effect.gen(function* () {
26 .handle("list", () =>26 const executor = yield* ExecutorService;
27 capture(27 const refs = yield* executor.connections.list();
28 Effect.gen(function* () {28 return refs.map(refToResponse);
29 const executor = yield* ExecutorService;29 }),
30 const refs = yield* executor.connections.list();
31 return refs.map(refToResponse);
32 }),
33 ),
34 )
35 .handle("remove", ({ params: path }) =>
36 capture(
37 Effect.gen(function* () {
38 const executor = yield* ExecutorService;
39 yield* executor.connections.remove(path.connectionId);
40 return { removed: true };
41 }),
42 ),
43 )
44 .handle("usages", ({ params: path }) =>
45 capture(
46 Effect.gen(function* () {
47 const executor = yield* ExecutorService;
48 return yield* executor.connections.usages(path.connectionId);
49 }),
50 ),
51 ),30 ),
31 )
32 .handle("remove", ({ params: path }) =>
33 capture(
34 Effect.gen(function* () {
35 const executor = yield* ExecutorService;
36 yield* executor.connections.remove(path.connectionId);
37 return { removed: true };
38 }),
39 ),
40 )
41 .handle("usages", ({ params: path }) =>
42 capture(
43 Effect.gen(function* () {
44 const executor = yield* ExecutorService;
45 return yield* executor.connections.usages(path.connectionId);
46 }),
47 ),
48 ),
52);49);
packages/core/api/src/handlers/executions.ts+46−42
@@ -9,60 +9,64 @@ import { capture, captureEngineError } from "@executor-js/api"; · 8 unmodified lines
9export const ExecutionsHandlers = HttpApiBuilder.group(ExecutorApi, "executions", (handlers) =>9export const ExecutionsHandlers = HttpApiBuilder.group(ExecutorApi, "executions", (handlers) =>
10 handlers10 handlers
11 .handle("execute", ({ payload }) =>11 .handle("execute", ({ payload }) =>
12 capture(Effect.gen(function* () {12 capture(
13 const engine = yield* ExecutionEngineService;13 Effect.gen(function* () {
14 const outcome = yield* captureEngineError(engine.executeWithPause(payload.code));14 const engine = yield* ExecutionEngineService;
15 const outcome = yield* captureEngineError(engine.executeWithPause(payload.code));
15 16
16 if (outcome.status === "completed") {17 if (outcome.status === "completed") {
17 const formatted = formatExecuteResult(outcome.result);18 const formatted = formatExecuteResult(outcome.result);
19 return {
20 status: "completed" as const,
21 text: formatted.text,
22 structured: formatted.structured,
23 isError: formatted.isError,
24 };
25 }
26
27 const formatted = formatPausedExecution(outcome.execution);
18 return {28 return {
19 status: "completed" as const,29 status: "paused" as const,
20 text: formatted.text,30 text: formatted.text,
21 structured: formatted.structured,31 structured: formatted.structured,
22 isError: formatted.isError,
23 };32 };
24 }33 }),
25 34 ),
26 const formatted = formatPausedExecution(outcome.execution);
27 return {
28 status: "paused" as const,
29 text: formatted.text,
30 structured: formatted.structured,
31 };
32 })),
33 )35 )
34 .handle("resume", ({ params: path, payload }) =>36 .handle("resume", ({ params: path, payload }) =>
35 capture(Effect.gen(function* () {37 capture(
36 const engine = yield* ExecutionEngineService;38 Effect.gen(function* () {
37 const result = yield* captureEngineError(39 const engine = yield* ExecutionEngineService;
38 engine.resume(path.executionId, {40 const result = yield* captureEngineError(
39 action: payload.action,41 engine.resume(path.executionId, {
40 content: payload.content as Record<string, unknown> | undefined,42 action: payload.action,
41 }),43 content: payload.content as Record<string, unknown> | undefined,
42 );44 }),
45 );
43 46
44 if (!result) {47 if (!result) {
45 return yield* Effect.fail({48 return yield* Effect.fail({
46 _tag: "ExecutionNotFoundError" as const,49 _tag: "ExecutionNotFoundError" as const,
47 executionId: path.executionId,50 executionId: path.executionId,
48 });51 });
49 }52 }
50 53
51 if (result.status === "completed") {54 if (result.status === "completed") {
52 const formatted = formatExecuteResult(result.result);55 const formatted = formatExecuteResult(result.result);
56 return {
57 text: formatted.text,
58 structured: formatted.structured,
59 isError: formatted.isError,
60 };
61 }
62
63 const formatted = formatPausedExecution(result.execution);
53 return {64 return {
54 text: formatted.text,65 text: formatted.text,
55 structured: formatted.structured,66 structured: formatted.structured,
56 isError: formatted.isError,67 isError: false,
57 };68 };
58 }69 }),
59 70 ),
60 const formatted = formatPausedExecution(result.execution);
61 return {
62 text: formatted.text,
63 structured: formatted.structured,
64 isError: false,
65 };
66 })),
67 ),71 ),
68);72);
packages/core/api/src/handlers/oauth.ts+2−1
@@ -67,7 +67,8 @@ const toPopupErrorMessage = (error: unknown): string => { · 66 unmodified lines
67 if (message) return message;67 if (message) return message;
68 68
69 const sessionNotFound = decodeOAuthSessionNotFoundError(error);69 const sessionNotFound = decodeOAuthSessionNotFoundError(error);
70 if (Option.isSome(sessionNotFound)) return `OAuth session not found: ${sessionNotFound.value.sessionId}`;70 if (Option.isSome(sessionNotFound))
71 return `OAuth session not found: ${sessionNotFound.value.sessionId}`;
71 return "Authentication failed";72 return "Authentication failed";
72};73};
73 74
packages/core/api/src/handlers/scope.ts+20−18
@@ -7,23 +7,25 @@ import { capture } from "@executor-js/api"; · 6 unmodified lines
7 7
8export const ScopeHandlers = HttpApiBuilder.group(ExecutorApi, "scope", (handlers) =>8export const ScopeHandlers = HttpApiBuilder.group(ExecutorApi, "scope", (handlers) =>
9 handlers.handle("info", () =>9 handlers.handle("info", () =>
10 capture(Effect.gen(function* () {10 capture(
11 const executor = yield* ExecutorService;11 Effect.gen(function* () {
12 // `id` / `name` / `dir` continue to point at the outermost scope so12 const executor = yield* ExecutorService;
13 // existing clients keep their source writes org/workspace-scoped.13 // `id` / `name` / `dir` continue to point at the outermost scope so
14 // `stack` exposes the full innermost-first scope stack so the UI can14 // existing clients keep their source writes org/workspace-scoped.
15 // deliberately target per-user secret writes when binding credentials.15 // `stack` exposes the full innermost-first scope stack so the UI can
16 const scope = executor.scopes.at(-1)!;16 // deliberately target per-user secret writes when binding credentials.
17 return {17 const scope = executor.scopes.at(-1)!;
18 id: scope.id,18 return {
19 name: scope.name,19 id: scope.id,
20 dir: scope.name,20 name: scope.name,
21 stack: executor.scopes.map((entry) => ({21 dir: scope.name,
22 id: entry.id,22 stack: executor.scopes.map((entry) => ({
23 name: entry.name,23 id: entry.id,
24 dir: entry.name,24 name: entry.name,
25 })),25 dir: entry.name,
26 };26 })),
27 })),27 };
28 }),
29 ),
28 ),30 ),
29);31);
packages/core/api/src/handlers/secrets.ts+42−32
@@ -17,45 +17,55 @@ const refToResponse = (ref: SecretRef) => ({ · 16 unmodified lines
17export const SecretsHandlers = HttpApiBuilder.group(ExecutorApi, "secrets", (handlers) =>17export const SecretsHandlers = HttpApiBuilder.group(ExecutorApi, "secrets", (handlers) =>
18 handlers18 handlers
19 .handle("list", () =>19 .handle("list", () =>
20 capture(Effect.gen(function* () {20 capture(
21 const executor = yield* ExecutorService;21 Effect.gen(function* () {
22 const refs = yield* executor.secrets.list();22 const executor = yield* ExecutorService;
23 return refs.map(refToResponse);23 const refs = yield* executor.secrets.list();
24 })),24 return refs.map(refToResponse);
25 }),
26 ),
25 )27 )
26 .handle("status", ({ params: path }) =>28 .handle("status", ({ params: path }) =>
27 capture(Effect.gen(function* () {29 capture(
28 const executor = yield* ExecutorService;30 Effect.gen(function* () {
29 const status = yield* executor.secrets.status(path.secretId);31 const executor = yield* ExecutorService;
30 return { secretId: path.secretId, status };32 const status = yield* executor.secrets.status(path.secretId);
31 })),33 return { secretId: path.secretId, status };
34 }),
35 ),
32 )36 )
33 .handle("set", ({ params: path, payload }) =>37 .handle("set", ({ params: path, payload }) =>
34 capture(Effect.gen(function* () {38 capture(
35 const executor = yield* ExecutorService;39 Effect.gen(function* () {
36 const ref = yield* executor.secrets.set(40 const executor = yield* ExecutorService;
37 new SetSecretInput({41 const ref = yield* executor.secrets.set(
38 id: payload.id,42 new SetSecretInput({
39 scope: path.scopeId,43 id: payload.id,
40 name: payload.name,44 scope: path.scopeId,
41 value: payload.value,45 name: payload.name,
42 provider: payload.provider,46 value: payload.value,
43 }),47 provider: payload.provider,
44 );48 }),
45 return refToResponse(ref);49 );
46 })),50 return refToResponse(ref);
51 }),
52 ),
47 )53 )
48 .handle("remove", ({ params: path }) =>54 .handle("remove", ({ params: path }) =>
49 capture(Effect.gen(function* () {55 capture(
50 const executor = yield* ExecutorService;56 Effect.gen(function* () {
51 yield* executor.secrets.remove(path.secretId);57 const executor = yield* ExecutorService;
52 return { removed: true };58 yield* executor.secrets.remove(path.secretId);
53 })),59 return { removed: true };
60 }),
61 ),
54 )62 )
55 .handle("usages", ({ params: path }) =>63 .handle("usages", ({ params: path }) =>
56 capture(Effect.gen(function* () {64 capture(
57 const executor = yield* ExecutorService;65 Effect.gen(function* () {
58 return yield* executor.secrets.usages(path.secretId);66 const executor = yield* ExecutorService;
59 })),67 return yield* executor.secrets.usages(path.secretId);
68 }),
69 ),
60 ),70 ),
61);71);
packages/core/api/src/handlers/sources.ts+69−59
@@ -9,72 +9,82 @@ import { capture } from "@executor-js/api"; · 8 unmodified lines
9export const SourcesHandlers = HttpApiBuilder.group(ExecutorApi, "sources", (handlers) =>9export const SourcesHandlers = HttpApiBuilder.group(ExecutorApi, "sources", (handlers) =>
10 handlers10 handlers
11 .handle("list", () =>11 .handle("list", () =>
12 capture(Effect.gen(function* () {12 capture(
13 const executor = yield* ExecutorService;13 Effect.gen(function* () {
14 const sources = yield* executor.sources.list();14 const executor = yield* ExecutorService;
15 return sources.map((s) => ({15 const sources = yield* executor.sources.list();
16 id: s.id,16 return sources.map((s) => ({
17 scopeId: s.scopeId ? ScopeId.make(s.scopeId) : undefined,17 id: s.id,
18 name: s.name,18 scopeId: s.scopeId ? ScopeId.make(s.scopeId) : undefined,
19 kind: s.kind,19 name: s.name,
20 url: s.url,20 kind: s.kind,
21 runtime: s.runtime,21 url: s.url,
22 canRemove: s.canRemove,22 runtime: s.runtime,
23 canRefresh: s.canRefresh,23 canRemove: s.canRemove,
24 canEdit: s.canEdit,24 canRefresh: s.canRefresh,
25 }));25 canEdit: s.canEdit,
26 })),26 }));
27 }),
28 ),
27 )29 )
28 .handle("remove", ({ params: path }) =>30 .handle("remove", ({ params: path }) =>
29 capture(Effect.gen(function* () {31 capture(
30 const executor = yield* ExecutorService;32 Effect.gen(function* () {
31 yield* executor.sources.remove(path.sourceId);33 const executor = yield* ExecutorService;
32 return { removed: true };34 yield* executor.sources.remove(path.sourceId);
33 })),35 return { removed: true };
36 }),
37 ),
34 )38 )
35 .handle("refresh", ({ params: path }) =>39 .handle("refresh", ({ params: path }) =>
36 capture(Effect.gen(function* () {40 capture(
37 const executor = yield* ExecutorService;41 Effect.gen(function* () {
38 yield* executor.sources.refresh(path.sourceId);42 const executor = yield* ExecutorService;
39 return { refreshed: true };43 yield* executor.sources.refresh(path.sourceId);
40 })),44 return { refreshed: true };
45 }),
46 ),
41 )47 )
42 .handle("tools", ({ params: path }) =>48 .handle("tools", ({ params: path }) =>
43 capture(Effect.gen(function* () {49 capture(
44 const executor = yield* ExecutorService;50 Effect.gen(function* () {
45 // Source detail is a management view — include policy-blocked51 const executor = yield* ExecutorService;
46 // tools so users can see and unblock them from the same place52 // Source detail is a management view — include policy-blocked
47 // they review the source's other tools. Annotations are loaded53 // tools so users can see and unblock them from the same place
48 // so the UI can show the plugin's default approval state for54 // they review the source's other tools. Annotations are loaded
49 // tools that have no user policy override.55 // so the UI can show the plugin's default approval state for
50 const tools = yield* executor.tools.list({56 // tools that have no user policy override.
51 sourceId: path.sourceId,57 const tools = yield* executor.tools.list({
52 includeAnnotations: true,58 sourceId: path.sourceId,
53 includeBlocked: true,59 includeAnnotations: true,
54 });60 includeBlocked: true,
55 return tools.map((t) => ({61 });
56 id: ToolId.make(t.id),62 return tools.map((t) => ({
57 pluginId: t.pluginId,63 id: ToolId.make(t.id),
58 sourceId: t.sourceId,64 pluginId: t.pluginId,
59 name: t.name,65 sourceId: t.sourceId,
60 description: t.description,66 name: t.name,
61 mayElicit: t.annotations?.mayElicit,67 description: t.description,
62 requiresApproval: t.annotations?.requiresApproval,68 mayElicit: t.annotations?.mayElicit,
63 approvalDescription: t.annotations?.approvalDescription,69 requiresApproval: t.annotations?.requiresApproval,
64 }));70 approvalDescription: t.annotations?.approvalDescription,
65 })),71 }));
72 }),
73 ),
66 )74 )
67 .handle("detect", ({ payload }) =>75 .handle("detect", ({ payload }) =>
68 capture(Effect.gen(function* () {76 capture(
69 const executor = yield* ExecutorService;77 Effect.gen(function* () {
70 const results = yield* executor.sources.detect(payload.url);78 const executor = yield* ExecutorService;
71 return results.map((r) => ({79 const results = yield* executor.sources.detect(payload.url);
72 kind: r.kind,80 return results.map((r) => ({
73 confidence: r.confidence,81 kind: r.kind,
74 endpoint: r.endpoint,82 confidence: r.confidence,
75 name: r.name,83 endpoint: r.endpoint,
76 namespace: r.namespace,84 name: r.name,
77 }));85 namespace: r.namespace,
78 })),86 }));
87 }),
88 ),
79 ),89 ),
80);90);
packages/core/api/src/handlers/tools.ts+32−28
@@ -9,35 +9,39 @@ import { capture } from "@executor-js/api"; · 8 unmodified lines
9export const ToolsHandlers = HttpApiBuilder.group(ExecutorApi, "tools", (handlers) =>9export const ToolsHandlers = HttpApiBuilder.group(ExecutorApi, "tools", (handlers) =>
10 handlers10 handlers
11 .handle("list", () =>11 .handle("list", () =>
12 capture(Effect.gen(function* () {12 capture(
13 const executor = yield* ExecutorService;13 Effect.gen(function* () {
14 // Tools page is a management view — include policy-blocked tools14 const executor = yield* ExecutorService;
15 // (so users can unblock them) and load annotations so the row can15 // Tools page is a management view — include policy-blocked tools
16 // show the plugin's default approval state when no user rule16 // (so users can unblock them) and load annotations so the row can
17 // matches. Mirrors the per-source `sources.tools` handler.17 // show the plugin's default approval state when no user rule
18 const tools = yield* executor.tools.list({18 // matches. Mirrors the per-source `sources.tools` handler.
19 includeAnnotations: true,19 const tools = yield* executor.tools.list({
20 includeBlocked: true,20 includeAnnotations: true,
21 });21 includeBlocked: true,
22 return tools.map((t) => ({22 });
23 id: ToolId.make(t.id),23 return tools.map((t) => ({
24 pluginId: t.pluginId,24 id: ToolId.make(t.id),
25 sourceId: t.sourceId,25 pluginId: t.pluginId,
26 name: t.name,26 sourceId: t.sourceId,
27 description: t.description,27 name: t.name,
28 mayElicit: t.annotations?.mayElicit,28 description: t.description,
29 requiresApproval: t.annotations?.requiresApproval,29 mayElicit: t.annotations?.mayElicit,
30 }));30 requiresApproval: t.annotations?.requiresApproval,
31 })),31 }));
32 }),
33 ),
32 )34 )
33 .handle("schema", ({ params: path }) =>35 .handle("schema", ({ params: path }) =>
34 capture(Effect.gen(function* () {36 capture(
35 const executor = yield* ExecutorService;37 Effect.gen(function* () {
36 const schema = yield* executor.tools.schema(path.toolId);38 const executor = yield* ExecutorService;
37 if (schema === null) {39 const schema = yield* executor.tools.schema(path.toolId);
38 return yield* new ToolNotFoundError({ toolId: path.toolId });40 if (schema === null) {
39 }41 return yield* new ToolNotFoundError({ toolId: path.toolId });
40 return schema;42 }
41 })),43 return schema;
44 }),
45 ),
42 ),46 ),
43);47);
packages/core/api/src/oauth-popup.test.ts+6−4
@@ -172,10 +172,12 @@ describe("runOAuthCallback", () => { · 171 unmodified lines
172 class DomainError extends Data.TaggedError("DomainError")<{172 class DomainError extends Data.TaggedError("DomainError")<{
173 readonly message: string;173 readonly message: string;
174 }> {}174 }> {}
175 const isDomainError = Schema.is(Schema.Struct({175 const isDomainError = Schema.is(
176 _tag: Schema.Literal("DomainError"),176 Schema.Struct({
177 message: Schema.String,177 _tag: Schema.Literal("DomainError"),
178 }));178 message: Schema.String,
179 }),
180 );
179 const html = await Effect.runPromise(181 const html = await Effect.runPromise(
180 runOAuthCallback<GoogleAuth, DomainError, never>({182 runOAuthCallback<GoogleAuth, DomainError, never>({
181 complete: () => Effect.fail(new DomainError({ message: "Code expired" })),183 complete: () => Effect.fail(new DomainError({ message: "Code expired" })),
packages/core/api/src/oauth-popup.ts+1−4
@@ -12,10 +12,7 @@ · 11 unmodified lines
12 12
13import { Cause, Effect } from "effect";13import { Cause, Effect } from "effect";
14 14
15import {15import { OAUTH_POPUP_MESSAGE_TYPE, type OAuthPopupResult } from "@executor-js/sdk";
16 OAUTH_POPUP_MESSAGE_TYPE,
17 type OAuthPopupResult,
18} from "@executor-js/sdk";
19 16
20export { OAUTH_POPUP_MESSAGE_TYPE, isOAuthPopupResult } from "@executor-js/sdk";17export { OAUTH_POPUP_MESSAGE_TYPE, isOAuthPopupResult } from "@executor-js/sdk";
21export type { OAuthPopupResult } from "@executor-js/sdk";18export type { OAuthPopupResult } from "@executor-js/sdk";
packages/core/api/src/oauth/api.ts+1−3
@@ -36,9 +36,7 @@ const ProbePayload = Schema.Struct({ · 35 unmodified lines
36const ProbeResponse = Schema.Struct({36const ProbeResponse = Schema.Struct({
37 resourceMetadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)),37 resourceMetadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)),
38 resourceMetadataUrl: Schema.NullOr(Schema.String),38 resourceMetadataUrl: Schema.NullOr(Schema.String),
39 authorizationServerMetadata: Schema.NullOr(39 authorizationServerMetadata: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)),
40 Schema.Record(Schema.String, Schema.Unknown),
41 ),
42 authorizationServerMetadataUrl: Schema.NullOr(Schema.String),40 authorizationServerMetadataUrl: Schema.NullOr(Schema.String),
43 authorizationServerUrl: Schema.NullOr(Schema.String),41 authorizationServerUrl: Schema.NullOr(Schema.String),
44 supportsDynamicRegistration: Schema.Boolean,42 supportsDynamicRegistration: Schema.Boolean,
packages/core/api/src/observability.test.ts+15−18
@@ -20,9 +20,7 @@ const makeRecorder = (traceId = "trace-xyz") => · 19 unmodified lines
20 ErrorCapture,20 ErrorCapture,
21 ErrorCapture.of({21 ErrorCapture.of({
22 captureException: (cause) =>22 captureException: (cause) =>
23 Ref.update(seen, (prev) => [...prev, cause]).pipe(23 Ref.update(seen, (prev) => [...prev, cause]).pipe(Effect.as(traceId)),
24 Effect.as(traceId),
25 ),
26 }),24 }),
27 );25 );
28 return { layer, seen };26 return { layer, seen };
@@ -58,17 +56,19 @@ describe("capture", () => { · 29 unmodified lines
58 }),56 }),
59 );57 );
60 58
61 it.effect("UniqueViolationError dies (becomes a defect — plugins should catchTag before returning)", () =>59 it.effect(
62 Effect.gen(function* () {60 "UniqueViolationError dies (becomes a defect — plugins should catchTag before returning)",
63 const err = new UniqueViolationError({ model: "thing" });61 () =>
64 const exit = yield* Effect.exit(capture(Effect.fail(err)));62 Effect.gen(function* () {
65 expect(Exit.isFailure(exit)).toBe(true);63 const err = new UniqueViolationError({ model: "thing" });
66 if (!Exit.isFailure(exit)) return;64 const exit = yield* Effect.exit(capture(Effect.fail(err)));
67 const defect = Cause.findDefect(exit.cause);65 expect(Exit.isFailure(exit)).toBe(true);
68 expect(Result.isSuccess(defect) ? defect.success : undefined).toBeInstanceOf(66 if (!Exit.isFailure(exit)) return;
69 UniqueViolationError,67 const defect = Cause.findDefect(exit.cause);
70 );68 expect(Result.isSuccess(defect) ? defect.success : undefined).toBeInstanceOf(
71 }),69 UniqueViolationError,
70 );
71 }),
72 );72 );
73 73
74 it.effect("non-storage typed failures pass through unchanged", () =>74 it.effect("non-storage typed failures pass through unchanged", () =>
@@ -76,10 +76,7 @@ describe("capture", () => { · 1 unmodified line
76 class DomainError {76 class DomainError {
77 readonly _tag = "DomainError" as const;77 readonly _tag = "DomainError" as const;
78 }78 }
79 const eff = Effect.fail(new DomainError()) as Effect.Effect<79 const eff = Effect.fail(new DomainError()) as Effect.Effect<never, DomainError>;
80 never,
81 DomainError
82 >;
83 const result = yield* Effect.flip(capture(eff));80 const result = yield* Effect.flip(capture(eff));
84 expect(result).toBeInstanceOf(DomainError);81 expect(result).toBeInstanceOf(DomainError);
85 }),82 }),
packages/core/api/src/observability.ts+22−33
@@ -51,19 +51,16 @@ export interface ErrorCaptureShape { · 50 unmodified lines
51 * can later look up. Implementations (Sentry, console, etc.) decide51 * can later look up. Implementations (Sentry, console, etc.) decide
52 * how to persist it.52 * how to persist it.
53 */53 */
54 readonly captureException: (54 readonly captureException: (cause: Cause.Cause<unknown>) => Effect.Effect<string>;
55 cause: Cause.Cause<unknown>,
56 ) => Effect.Effect<string>;
57}55}
58 56
59export class ErrorCapture extends Context.Service<ErrorCapture, ErrorCaptureShape>()(57export class ErrorCapture extends Context.Service<ErrorCapture, ErrorCaptureShape>()(
60 "@executor-js/api/ErrorCapture",58 "@executor-js/api/ErrorCapture",
61) {59) {
62 /** No-op — used where capture isn't wired. Traces back as empty string. */60 /** No-op — used where capture isn't wired. Traces back as empty string. */
63 static readonly NoOp: Layer.Layer<ErrorCapture> = Layer.succeed(61 static readonly NoOp: Layer.Layer<ErrorCapture> = Layer.succeed(ErrorCapture, {
64 ErrorCapture,62 captureException: () => Effect.succeed(""),
65 { captureException: () => Effect.succeed("") },63 });
66 );
67}64}
68 65
69// Resolve ErrorCapture with a no-op fallback. Keeps the caller's R channel66// Resolve ErrorCapture with a no-op fallback. Keeps the caller's R channel
@@ -71,9 +68,7 @@ export class ErrorCapture extends Context.Service<ErrorCapture, ErrorCaptureShap · 1 unmodified line
71// typecheck; if it's there, we use it; if not, trace ids are empty.68// typecheck; if it's there, we use it; if not, trace ids are empty.
72const resolveCapture = Effect.serviceOption(ErrorCapture).pipe(69const resolveCapture = Effect.serviceOption(ErrorCapture).pipe(
73 Effect.map((opt) =>70 Effect.map((opt) =>
74 Option.isSome(opt)71 Option.isSome(opt) ? opt.value : ({ captureException: () => Effect.succeed("") } as const),
75 ? opt.value
76 : ({ captureException: () => Effect.succeed("") } as const),
77 ),72 ),
78);73);
79 74
@@ -102,9 +97,7 @@ export const capture = <A, E, R>( · 22 unmodified lines
102 Effect.catchTag("StorageError", (err) =>97 Effect.catchTag("StorageError", (err) =>
103 resolveCapture.pipe(98 resolveCapture.pipe(
104 Effect.flatMap((c) => c.captureException(Cause.fail(err))),99 Effect.flatMap((c) => c.captureException(Cause.fail(err))),
105 Effect.flatMap((traceId) =>100 Effect.flatMap((traceId) => Effect.fail(new InternalError({ traceId }))),
106 Effect.fail(new InternalError({ traceId })),
107 ),
108 ),101 ),
109 ),102 ),
110 ) as Effect.Effect<A, Exclude<E, StorageFailure> | InternalError, R>;103 ) as Effect.Effect<A, Exclude<E, StorageFailure> | InternalError, R>;
@@ -131,10 +124,8 @@ export const captureEngineError = <A, R>( · 20 unmodified lines
131 ? Effect.fail(err)124 ? Effect.fail(err)
132 : resolveCapture.pipe(125 : resolveCapture.pipe(
133 Effect.flatMap((c) => c.captureException(Cause.fail(err))),126 Effect.flatMap((c) => c.captureException(Cause.fail(err))),
134 Effect.flatMap((traceId) =>127 Effect.flatMap((traceId) => Effect.fail(new InternalError({ traceId }))),
135 Effect.fail(new InternalError({ traceId })),128 ),
136 ),
137 ),
138 ),129 ),
139 );130 );
140 131
@@ -160,20 +151,18 @@ export class ObservabilityMiddleware extends HttpApiMiddleware.Service<Observabi · 19 unmodified lines
160export const observabilityMiddleware = <Id extends string, Groups extends HttpApiGroup.Any>(151export const observabilityMiddleware = <Id extends string, Groups extends HttpApiGroup.Any>(
161 _api: HttpApi.HttpApi<Id, Groups>,152 _api: HttpApi.HttpApi<Id, Groups>,
162): Layer.Layer<ObservabilityMiddleware> =>153): Layer.Layer<ObservabilityMiddleware> =>
163 Layer.succeed(154 Layer.succeed(ObservabilityMiddleware, (httpApp) =>
164 ObservabilityMiddleware,155 Effect.catchCause(httpApp, (cause) =>
165 (httpApp) =>156 Effect.gen(function* () {
166 Effect.catchCause(httpApp, (cause) =>157 const defect = Cause.findDefect(cause);
167 Effect.gen(function* () {158 if (Result.isFailure(defect)) {
168 const defect = Cause.findDefect(cause);159 return yield* Effect.failCause(cause);
169 if (Result.isFailure(defect)) {160 }
170 return yield* Effect.failCause(cause);161 const c = yield* resolveCapture;
171 }162 const traceId = yield* c.captureException(cause);
172 const c = yield* resolveCapture;163 return HttpServerResponse.jsonUnsafe(new InternalError({ traceId }), {
173 const traceId = yield* c.captureException(cause);164 status: 500,
174 return HttpServerResponse.jsonUnsafe(new InternalError({ traceId }), {165 });
175 status: 500,166 }),
176 });167 ),
177 }),
178 ),
179 );168 );
packages/core/api/src/plugin-routes.ts+18−16
@@ -40,8 +40,9 @@ import { CoreExecutorApi } from "./api"; · 39 unmodified lines
40// eslint-disable-next-line @typescript-eslint/no-explicit-any40// eslint-disable-next-line @typescript-eslint/no-explicit-any
41type ExtractServiceId<S> = S extends Context.Service<infer Id, any> ? Id : never;41type ExtractServiceId<S> = S extends Context.Service<infer Id, any> ? Id : never;
42 42
43export type PluginExtensionServices<TPlugins extends readonly AnyPlugin[]> =43export type PluginExtensionServices<TPlugins extends readonly AnyPlugin[]> = ExtractServiceId<
44 ExtractServiceId<NonNullable<TPlugins[number]["extensionService"]>>;44 NonNullable<TPlugins[number]["extensionService"]>
45>;
45 46
46/** Extract the precise `HttpApiGroup` type carried by a plugin's47/** Extract the precise `HttpApiGroup` type carried by a plugin's
47 * `routes()` field. Plugins without a `routes()` field contribute48 * `routes()` field. Plugins without a `routes()` field contribute
@@ -52,8 +53,9 @@ type ExtractPluginGroup<P> = P extends { readonly routes?: () => infer G } · 4 unmodified lines
52 53
53/** Union of every plugin's contributed group — combined with the core54/** Union of every plugin's contributed group — combined with the core
54 * executor groups to type `composePluginApi(plugins)` precisely. */55 * executor groups to type `composePluginApi(plugins)` precisely. */
55export type PluginGroups<TPlugins extends readonly AnyPlugin[]> =56export type PluginGroups<TPlugins extends readonly AnyPlugin[]> = ExtractPluginGroup<
56 ExtractPluginGroup<TPlugins[number]>;57 TPlugins[number]
58>;
57 59
58/** Group identities baked into `CoreExecutorApi` (tools, sources, secrets,60/** Group identities baked into `CoreExecutorApi` (tools, sources, secrets,
59 * …). Extracted via inference so adding a core group flows through61 * …). Extracted via inference so adding a core group flows through
@@ -64,8 +66,10 @@ type CoreGroups = · 4 unmodified lines
64 66
65/** Result of `composePluginApi(plugins)` — the core API extended with67/** Result of `composePluginApi(plugins)` — the core API extended with
66 * every plugin group from `TPlugins`. */68 * every plugin group from `TPlugins`. */
67export type ComposedExecutorApi<TPlugins extends readonly AnyPlugin[]> =69export type ComposedExecutorApi<TPlugins extends readonly AnyPlugin[]> = HttpApi.HttpApi<
68 HttpApi.HttpApi<"executor", CoreGroups | PluginGroups<TPlugins>>;70 "executor",
71 CoreGroups | PluginGroups<TPlugins>
72>;
69 73
70// eslint-disable-next-line @typescript-eslint/no-explicit-any74// eslint-disable-next-line @typescript-eslint/no-explicit-any
71type AnyLayer = Layer.Layer<any, any, any>;75type AnyLayer = Layer.Layer<any, any, any>;
@@ -73,18 +77,18 @@ type AnyLayer = Layer.Layer<any, any, any>; · 1 unmodified line
73// Use the field accessor + NonNullable rather than `extends { handlers: ... }`77// Use the field accessor + NonNullable rather than `extends { handlers: ... }`
74// because the spec marks `handlers` optional (`handlers?:`); the conditional78// because the spec marks `handlers` optional (`handlers?:`); the conditional
75// form would fail the match because the field type includes `undefined`.79// form would fail the match because the field type includes `undefined`.
76type ExtractHandlerLayer<P> = NonNullable<80type ExtractHandlerLayer<P> =
77 P extends { readonly handlers?: infer F } ? F : never81 NonNullable<P extends { readonly handlers?: infer F } ? F : never> extends () => infer L
78> extends () => infer L82 ? L
79 ? L83 : never;
80 : never;
81 84
82// Compute the union of every plugin's handler-Layer type. Each plugin's85// Compute the union of every plugin's handler-Layer type. Each plugin's
83// `handlers()` returns a specific `Layer<Group, never, ExtensionService>`;86// `handlers()` returns a specific `Layer<Group, never, ExtensionService>`;
84// we union them so `Layer.mergeAll(...)`'s output type can be extracted87// we union them so `Layer.mergeAll(...)`'s output type can be extracted
85// without erasing per-plugin requirements.88// without erasing per-plugin requirements.
86type PluginHandlerLayers<TPlugins extends readonly AnyPlugin[]> =89type PluginHandlerLayers<TPlugins extends readonly AnyPlugin[]> = ExtractHandlerLayer<
87 ExtractHandlerLayer<TPlugins[number]>;90 TPlugins[number]
91>;
88 92
89// Distribute over the union of handler layers to extract each channel93// Distribute over the union of handler layers to extract each channel
90// individually, then re-pack into a single `Layer<UnionROut, UnionE,94// individually, then re-pack into a single `Layer<UnionROut, UnionE,
@@ -174,9 +178,7 @@ export const composePluginHandlers = <TPlugins extends readonly AnyPlugin[]>( · 83 unmodified lines
174 * `*ExtensionService` Tags) so the host's `HttpRouter.middleware`178 * `*ExtensionService` Tags) so the host's `HttpRouter.middleware`
175 * recognises them as per-request requires.179 * recognises them as per-request requires.
176 */180 */
177export const composePluginHandlerLayer = <181export const composePluginHandlerLayer = <TPlugins extends readonly AnyPlugin[]>(
178 TPlugins extends readonly AnyPlugin[],
179>(
180 plugins: TPlugins,182 plugins: TPlugins,
181): MergedHandlerLayer<TPlugins> => {183): MergedHandlerLayer<TPlugins> => {
182 const layers = plugins.flatMap((p) => (p.handlers ? [p.handlers()] : []));184 const layers = plugins.flatMap((p) => (p.handlers ? [p.handlers()] : []));
packages/core/api/src/secrets/api.ts+2−6
@@ -48,12 +48,8 @@ const SetSecretPayload = Schema.Struct({ · 47 unmodified lines
48// ---------------------------------------------------------------------------48// ---------------------------------------------------------------------------
49 49
50const SecretNotFound = SecretNotFoundError.annotate({ httpApiStatus: 404 });50const SecretNotFound = SecretNotFoundError.annotate({ httpApiStatus: 404 });
51const SecretResolution = SecretResolutionError.annotate(51const SecretResolution = SecretResolutionError.annotate({ httpApiStatus: 500 });
52 { httpApiStatus: 500 },52const SecretOwnedByConnection = SecretOwnedByConnectionError.annotate({ httpApiStatus: 409 });
53);
54const SecretOwnedByConnection = SecretOwnedByConnectionError.annotate(
55 { httpApiStatus: 409 },
56);
57const SecretInUse = SecretInUseError.annotate({ httpApiStatus: 409 });53const SecretInUse = SecretInUseError.annotate({ httpApiStatus: 409 });
58 54
59// ---------------------------------------------------------------------------55// ---------------------------------------------------------------------------
packages/core/api/src/sources/api.ts+2−8
@@ -1,10 +1,6 @@
1import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";1import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
2import { Schema } from "effect";2import { Schema } from "effect";
3import {3import { ScopeId, SourceRemovalNotAllowedError, ToolId } from "@executor-js/sdk";
4 ScopeId,
5 SourceRemovalNotAllowedError,
6 ToolId,
7} from "@executor-js/sdk";
8 4
9import { InternalError } from "../observability";5import { InternalError } from "../observability";
10 6
@@ -68,9 +64,7 @@ const DetectResultResponse = Schema.Struct({ · 57 unmodified lines
68// Error schemas with HTTP status annotations64// Error schemas with HTTP status annotations
69// ---------------------------------------------------------------------------65// ---------------------------------------------------------------------------
70 66
71const SourceRemovalNotAllowed = SourceRemovalNotAllowedError.annotate(67const SourceRemovalNotAllowed = SourceRemovalNotAllowedError.annotate({ httpApiStatus: 409 });
72 { httpApiStatus: 409 },
73);
74 68
75// ---------------------------------------------------------------------------69// ---------------------------------------------------------------------------
76// Group70// Group
packages/core/api/src/tools/api.ts+1−3
@@ -31,9 +31,7 @@ const ToolSchemaResponse = Schema.Struct({ · 30 unmodified lines
31 id: ToolId,31 id: ToolId,
32 inputTypeScript: Schema.optional(Schema.String),32 inputTypeScript: Schema.optional(Schema.String),
33 outputTypeScript: Schema.optional(Schema.String),33 outputTypeScript: Schema.optional(Schema.String),
34 typeScriptDefinitions: Schema.optional(34 typeScriptDefinitions: Schema.optional(Schema.Record(Schema.String, Schema.String)),
35 Schema.Record(Schema.String, Schema.String),
36 ),
37 inputSchema: Schema.optional(Schema.Unknown),35 inputSchema: Schema.optional(Schema.Unknown),
38 outputSchema: Schema.optional(Schema.Unknown),36 outputSchema: Schema.optional(Schema.Unknown),
39});37});
packages/core/cli/package.json+1−1
@@ -12,13 +12,13 @@ · 11 unmodified lines
12 "url": "git+https://github.com/RhysSullivan/executor.git",12 "url": "git+https://github.com/RhysSullivan/executor.git",
13 "directory": "packages/core/cli"13 "directory": "packages/core/cli"
14 },14 },
15 "type": "module",
16 "bin": {15 "bin": {
17 "executor": "./dist/index.js"16 "executor": "./dist/index.js"
18 },17 },
19 "files": [18 "files": [
20 "dist"19 "dist"
21 ],20 ],
21 "type": "module",
22 "exports": {22 "exports": {
23 ".": "./src/index.ts"23 ".": "./src/index.ts"
24 },24 },
packages/core/cli/src/commands/generate.ts+4−18
@@ -6,11 +6,7 @@ import { collectSchemas } from "@executor-js/sdk/core"; · 5 unmodified lines
6import { getConfig } from "../utils/get-config.js";6import { getConfig } from "../utils/get-config.js";
7import { generateDrizzleSchema } from "../generators/drizzle.js";7import { generateDrizzleSchema } from "../generators/drizzle.js";
8 8
9async function generateAction(opts: {9async function generateAction(opts: { cwd: string; config?: string; output?: string }) {
10 cwd: string;
11 config?: string;
12 output?: string;
13}) {
14 const cwd = path.resolve(opts.cwd);10 const cwd = path.resolve(opts.cwd);
15 if (!existsSync(cwd)) {11 if (!existsSync(cwd)) {
16 console.error(`The directory "${cwd}" does not exist.`);12 console.error(`The directory "${cwd}" does not exist.`);
@@ -55,17 +51,7 @@ async function generateAction(opts: { · 38 unmodified lines
55 51
56export const generate = new Command("generate")52export const generate = new Command("generate")
57 .description("Generate a drizzle schema file from the executor config")53 .description("Generate a drizzle schema file from the executor config")
58 .option(54 .option("-c, --cwd <cwd>", "the working directory", process.cwd())
59 "-c, --cwd <cwd>",55 .option("--config <config>", "path to the executor config file")
60 "the working directory",56 .option("--output <output>", "output file path for the generated schema")
61 process.cwd(),
62 )
63 .option(
64 "--config <config>",
65 "path to the executor config file",
66 )
67 .option(
68 "--output <output>",
69 "output file path for the generated schema",
70 )
71 .action(generateAction);57 .action(generateAction);
packages/core/cli/src/generators/drizzle.ts+62−104
@@ -15,14 +15,9 @@ import type { SchemaGenerator } from "./types.js"; · 14 unmodified lines
15 15
16type Dialect = "pg" | "sqlite" | "mysql";16type Dialect = "pg" | "sqlite" | "mysql";
17 17
18const getModelName = (key: string, def: DBSchema[string]): string =>18const getModelName = (key: string, def: DBSchema[string]): string => def.modelName ?? key;
19 def.modelName ?? key;19
20 20const getType = (name: string, field: DBFieldAttribute, dialect: Dialect): string => {
21const getType = (
22 name: string,
23 field: DBFieldAttribute,
24 dialect: Dialect,
25): string => {
26 if (field.references?.field === "id") {21 if (field.references?.field === "id") {
27 return `text('${name}')`;22 return `text('${name}')`;
28 }23 }
@@ -39,9 +34,7 @@ const getType = ( · 10 unmodified lines
39 }[dialect];34 }[dialect];
40 }35 }
41 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: generator rejects invalid schema input through the SchemaGenerator promise API36 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: generator rejects invalid schema input through the SchemaGenerator promise API
42 throw new TypeError(37 throw new TypeError(`Invalid field type for field ${name}`);
43 `Invalid field type for field ${name}`,
44 );
45 }38 }
46 39
47 const typeMap: Record<string, Record<Dialect, string>> = {40 const typeMap: Record<string, Record<Dialect, string>> = {
@@ -65,12 +58,8 @@ const getType = ( · 17 unmodified lines
65 },58 },
66 number: {59 number: {
67 sqlite: `integer('${name}')`,60 sqlite: `integer('${name}')`,
68 pg: field.bigint61 pg: field.bigint ? `bigint('${name}', { mode: 'number' })` : `integer('${name}')`,
69 ? `bigint('${name}', { mode: 'number' })`62 mysql: field.bigint ? `bigint('${name}', { mode: 'number' })` : `int('${name}')`,
70 : `integer('${name}')`,
71 mysql: field.bigint
72 ? `bigint('${name}', { mode: 'number' })`
73 : `int('${name}')`,
74 },63 },
75 date: {64 date: {
76 sqlite: `integer('${name}', { mode: 'timestamp_ms' })`,65 sqlite: `integer('${name}', { mode: 'timestamp_ms' })`,
@@ -99,9 +88,7 @@ const getType = ( · 22 unmodified lines
99 const dbTypeMap = typeMap[type];88 const dbTypeMap = typeMap[type];
100 if (!dbTypeMap) {89 if (!dbTypeMap) {
101 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: generator rejects unsupported schema input through the SchemaGenerator promise API90 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: generator rejects unsupported schema input through the SchemaGenerator promise API
102 throw new Error(91 throw new Error(`Unsupported field type '${field.type}' for field '${name}'.`);
103 `Unsupported field type '${field.type}' for field '${name}'.`,
104 );
105 }92 }
106 return dbTypeMap[dialect];93 return dbTypeMap[dialect];
107};94};
@@ -110,11 +97,7 @@ const getType = ( · 2 unmodified lines
110// Generator97// Generator
111// ---------------------------------------------------------------------------98// ---------------------------------------------------------------------------
112 99
113export const generateDrizzleSchema: SchemaGenerator = async ({100export const generateDrizzleSchema: SchemaGenerator = async ({ schema, dialect, file }) => {
114 schema,
115 dialect,
116 file,
117}) => {
118 const filePath = file || "./executor-schema.ts";101 const filePath = file || "./executor-schema.ts";
119 const fileExist = existsSync(filePath);102 const fileExist = existsSync(filePath);
120 103
@@ -159,69 +142,62 @@ export const generateDrizzleSchema: SchemaGenerator = async ({ · 38 unmodified lines
159 }142 }
160 143
161 const fieldLines = Object.entries(fields)144 const fieldLines = Object.entries(fields)
162 .filter(([fieldName]) => fieldName !== "id")145 .filter(([fieldName]) => fieldName !== "id")
163 .map(([fieldName, attr]) => {146 .map(([fieldName, attr]) => {
164 const physical = attr.fieldName ?? fieldName;147 const physical = attr.fieldName ?? fieldName;
165 148
166 const isToolPolicyCompositeField =149 const isToolPolicyCompositeField =
167 tableKey === "tool_policy" &&150 tableKey === "tool_policy" && (physical === "scope_id" || physical === "position");
168 (physical === "scope_id" || physical === "position");151
169 152 if (attr.index && !attr.unique && !isToolPolicyCompositeField) {
170 if (attr.index && !attr.unique && !isToolPolicyCompositeField) {153 extras.push({
171 extras.push({154 kind: "index",
172 kind: "index",155 name: `${tableKey}_${physical}_idx`,
173 name: `${tableKey}_${physical}_idx`,156 on: physical,
174 on: physical,157 });
175 });158 } else if (attr.index && attr.unique) {
176 } else if (attr.index && attr.unique) {159 extras.push({
177 extras.push({160 kind: "uniqueIndex",
178 kind: "uniqueIndex",161 name: `${tableKey}_${physical}_uidx`,
179 name: `${tableKey}_${physical}_uidx`,162 on: physical,
180 on: physical,163 });
181 });164 }
182 }165
166 let col = getType(physical, attr, dialect);
183 167
184 let col = getType(physical, attr, dialect);168 if (attr.defaultValue !== null && typeof attr.defaultValue !== "undefined") {
185 169 if (typeof attr.defaultValue === "function") {
186 if (170 if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) {
187 attr.defaultValue !== null &&171 if (dialect === "sqlite") {
188 typeof attr.defaultValue !== "undefined"172 col += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
189 ) {173 } else {
190 if (typeof attr.defaultValue === "function") {174 col += `.defaultNow()`;
191 if (175 }
192 attr.type === "date" &&
193 attr.defaultValue.toString().includes("new Date()")
194 ) {
195 if (dialect === "sqlite") {
196 col += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
197 } else {
198 col += `.defaultNow()`;
199 }176 }
177 } else if (typeof attr.defaultValue === "string") {
178 col += `.default("${attr.defaultValue}")`;
179 } else {
180 col += `.default(${attr.defaultValue})`;
200 }181 }
201 } else if (typeof attr.defaultValue === "string") {
202 col += `.default("${attr.defaultValue}")`;
203 } else {
204 col += `.default(${attr.defaultValue})`;
205 }182 }
206 }
207 183
208 if (attr.onUpdate && attr.type === "date") {184 if (attr.onUpdate && attr.type === "date") {
209 if (typeof attr.onUpdate === "function") {185 if (typeof attr.onUpdate === "function") {
210 col += `.$onUpdate(${attr.onUpdate})`;186 col += `.$onUpdate(${attr.onUpdate})`;
187 }
211 }188 }
212 }
213 189
214 return `${physical}: ${col}${attr.required !== false ? ".notNull()" : ""}${190 return `${physical}: ${col}${attr.required !== false ? ".notNull()" : ""}${
215 attr.unique ? ".unique()" : ""191 attr.unique ? ".unique()" : ""
216 }${192 }${
217 attr.references193 attr.references
218 ? `.references(()=> ${attr.references.model}.${attr.references.field ?? "id"}, { onDelete: '${194 ? `.references(()=> ${attr.references.model}.${attr.references.field ?? "id"}, { onDelete: '${
219 attr.references.onDelete || "cascade"195 attr.references.onDelete || "cascade"
220 }' })`196 }' })`
221 : ""197 : ""
222 }`;198 }`;
223 })199 })
224 .join(",\n ");200 .join(",\n ");
225 201
226 if (tableKey === "tool_policy") {202 if (tableKey === "tool_policy") {
227 extras.push({203 extras.push({
@@ -339,12 +315,7 @@ export const generateDrizzleSchema: SchemaGenerator = async ({ · 111 unmodified lines
339 const hasMany = manyRelations.length > 0;315 const hasMany = manyRelations.length > 0;
340 316
341 if (hasOne || hasMany) {317 if (hasOne || hasMany) {
342 const destructured = [318 const destructured = [hasOne ? "one" : "", hasMany ? "many" : ""].filter(Boolean).join(", ");
343 hasOne ? "one" : "",
344 hasMany ? "many" : "",
345 ]
346 .filter(Boolean)
347 .join(", ");
348 319
349 const body = [320 const body = [
350 ...singleRelations321 ...singleRelations
@@ -353,9 +324,7 @@ export const generateDrizzleSchema: SchemaGenerator = async ({ · 2 unmodified lines
353 (r) =>324 (r) =>
354 ` ${r.key}: one(${r.model}, {\n fields: [${r.reference!.field}],\n references: [${r.reference!.references}],\n })`,325 ` ${r.key}: one(${r.model}, {\n fields: [${r.reference!.field}],\n references: [${r.reference!.references}],\n })`,
355 ),326 ),
356 ...manyRelations.map(327 ...manyRelations.map(({ key, model }) => ` ${key}: many(${model})`),
357 ({ key, model }) => ` ${key}: many(${model})`,
358 ),
359 ].join(",\n");328 ].join(",\n");
360 329
361 const block = `export const ${modelName}Relations = relations(${modelName}, ({ ${destructured} }) => ({330 const block = `export const ${modelName}Relations = relations(${modelName}, ({ ${destructured} }) => ({
@@ -378,13 +347,7 @@ ${body} · 16 unmodified lines
378// Import generation — only emit what's actually used347// Import generation — only emit what's actually used
379// ---------------------------------------------------------------------------348// ---------------------------------------------------------------------------
380 349
381function generateImport({350function generateImport({ dialect, schema }: { dialect: Dialect; schema: DBSchema }) {
382 dialect,
383 schema,
384}: {
385 dialect: Dialect;
386 schema: DBSchema;
387}) {
388 const rootImports: string[] = [];351 const rootImports: string[] = [];
389 const coreImports: string[] = [];352 const coreImports: string[] = [];
390 353
@@ -403,10 +366,7 @@ function generateImport({ · 12 unmodified lines
403 if (field.bigint) hasBigint = true;366 if (field.bigint) hasBigint = true;
404 if (field.type === "json") hasJson = true;367 if (field.type === "json") hasJson = true;
405 if (field.type === "boolean") hasBoolean = true;368 if (field.type === "boolean") hasBoolean = true;
406 if (369 if ((field.type === "number" && !field.bigint) || field.type === "number[]") {
407 (field.type === "number" && !field.bigint) ||
408 field.type === "number[]"
409 ) {
410 hasNumber = true;370 hasNumber = true;
411 }371 }
412 if (field.type === "date") hasDate = true;372 if (field.type === "date") hasDate = true;
@@ -482,9 +442,7 @@ function generateImport({ · 69 unmodified lines
482 // produce relation blocks (see relationsString generation).442 // produce relation blocks (see relationsString generation).
483 if (hasReferences) rootImports.push("relations");443 if (hasReferences) rootImports.push("relations");
484 444
485 const filteredCore = coreImports445 const filteredCore = coreImports.map((x) => x.trim()).filter((x) => x !== "");
486 .map((x) => x.trim())
487 .filter((x) => x !== "");
488 446
489 // Deduplicate447 // Deduplicate
490 const uniqueCore = [...new Set(filteredCore)];448 const uniqueCore = [...new Set(filteredCore)];
packages/core/cli/src/generators/index.ts+1−4
@@ -7,10 +7,7 @@ const generators: Record<string, SchemaGenerator> = { · 6 unmodified lines
7 drizzle: generateDrizzleSchema,7 drizzle: generateDrizzleSchema,
8};8};
9 9
10export const generateSchema = (10export const generateSchema = (adapter: string, ...args: Parameters<SchemaGenerator>) => {
11 adapter: string,
12 ...args: Parameters<SchemaGenerator>
13) => {
14 const generator = generators[adapter];11 const generator = generators[adapter];
15 if (!generator) {12 if (!generator) {
16 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: synchronous CLI generator registry rejects unsupported adapter names13 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: synchronous CLI generator registry rejects unsupported adapter names
packages/core/cli/tsup.config.ts+1−6
@@ -8,10 +8,5 @@ export default defineConfig({ · 7 unmodified lines
8 dts: false,8 dts: false,
9 sourcemap: true,9 sourcemap: true,
10 clean: true,10 clean: true,
11 external: [11 external: [/^effect/, /^@effect\//, "commander", "jiti"],
12 /^effect/,
13 /^@effect\//,
14 "commander",
15 "jiti",
16 ],
17});12});
packages/core/config/src/index.ts+1−5
@@ -25,8 +25,4 @@ export { · 24 unmodified lines
25} from "./write";25} from "./write";
26 26
27export type { ConfigFileSink, ConfigFileSinkOptions } from "./sink";27export type { ConfigFileSink, ConfigFileSinkOptions } from "./sink";
28export {28export { makeFileConfigSink, headerToConfigValue, headersToConfigValues } from "./sink";
29 makeFileConfigSink,
30 headerToConfigValue,
31 headersToConfigValues,
32} from "./sink";
packages/core/config/src/load-plugins.test.ts+2−6
@@ -25,9 +25,7 @@ interface FixturePlugin { · 24 unmodified lines
25 readonly __optionsReceived: Record<string, unknown> | null;25 readonly __optionsReceived: Record<string, unknown> | null;
26}26}
27 27
28const asFixturePlugins = (28const asFixturePlugins = (plugins: readonly AnyPlugin[] | null): readonly FixturePlugin[] => {
29 plugins: readonly AnyPlugin[] | null,
30): readonly FixturePlugin[] => {
31 // Fixture servers return shapes wider than AnyPlugin (they carry29 // Fixture servers return shapes wider than AnyPlugin (they carry
32 // __optionsReceived for test assertions); narrow once here.30 // __optionsReceived for test assertions); narrow once here.
33 // oxlint-disable-next-line executor/no-double-cast31 // oxlint-disable-next-line executor/no-double-cast
@@ -71,9 +69,7 @@ describe("loadPluginsFromJsonc", () => { · 37 unmodified lines
71 69
72 it("throws on JSONC parse errors", async () => {70 it("throws on JSONC parse errors", async () => {
73 const file = writeJsonc("parse-error", `{ "plugins": [`);71 const file = writeJsonc("parse-error", `{ "plugins": [`);
74 await expect(loadPluginsFromJsonc({ path: file })).rejects.toThrow(72 await expect(loadPluginsFromJsonc({ path: file })).rejects.toThrow(/failed to parse/i);
75 /failed to parse/i,
76 );
77 });73 });
78 74
79 it("throws with a helpful message when the package can't be resolved", async () => {75 it("throws with a helpful message when the package can't be resolved", async () => {
packages/core/config/src/load-plugins.ts+3−8
@@ -66,8 +66,7 @@ export interface LoadPluginsFromJsoncOptions { · 65 unmodified lines
66 */66 */
67export const loadPluginsFromJsonc = async (67export const loadPluginsFromJsonc = async (
68 options: LoadPluginsFromJsoncOptions,68 options: LoadPluginsFromJsoncOptions,
69): Promise<readonly AnyPlugin[] | null> =>69): Promise<readonly AnyPlugin[] | null> => Effect.runPromise(loadPluginsFromJsoncEffect(options));
70 Effect.runPromise(loadPluginsFromJsoncEffect(options));
71 70
72const loadPluginsFromJsoncEffect = (71const loadPluginsFromJsoncEffect = (
73 options: LoadPluginsFromJsoncOptions,72 options: LoadPluginsFromJsoncOptions,
@@ -122,9 +121,7 @@ const loadPluginsFromJsoncEffect = ( · 48 unmodified lines
122 // require.resolve is anchored to the jsonc's directory so plugin121 // require.resolve is anchored to the jsonc's directory so plugin
123 // packages resolve from the host app's `node_modules` regardless of122 // packages resolve from the host app's `node_modules` regardless of
124 // CWD.123 // CWD.
125 const require = createRequire(124 const require = createRequire(isAbsolute(path) ? path : resolvePath(fromDir, "_anchor.js"));
126 isAbsolute(path) ? path : resolvePath(fromDir, "_anchor.js"),
127 );
128 125
129 const loaded: AnyPlugin[] = [];126 const loaded: AnyPlugin[] = [];
130 for (const entry of entries) {127 for (const entry of entries) {
@@ -146,9 +143,7 @@ const loadPluginsFromJsoncEffect = ( · 15 unmodified lines
146 message: `[load-plugins] failed to import "${serverEntry}" from ${resolved}.`,143 message: `[load-plugins] failed to import "${serverEntry}" from ${resolved}.`,
147 cause,144 cause,
148 }),145 }),
149 })) as146 })) as { default?: LooseConfiguredPlugin } | LooseConfiguredPlugin;
150 | { default?: LooseConfiguredPlugin }
151 | LooseConfiguredPlugin;
152 const factory = (147 const factory = (
153 typeof mod === "function" ? mod : (mod.default ?? null)148 typeof mod === "function" ? mod : (mod.default ?? null)
154 ) as LooseConfiguredPlugin | null;149 ) as LooseConfiguredPlugin | null;
packages/core/config/src/sink.ts+2−6
@@ -22,9 +22,7 @@ import { addSourceToConfig, removeSourceFromConfig } from "./write"; · 21 unmodified lines
22// refs) into the config file's `secret-public-ref:<id>` string form.22// refs) into the config file's `secret-public-ref:<id>` string form.
23type PluginHeaderValue = string | { secretId: string; prefix?: string };23type PluginHeaderValue = string | { secretId: string; prefix?: string };
24 24
25export const headerToConfigValue = (25export const headerToConfigValue = (value: PluginHeaderValue): ConfigHeaderValue => {
26 value: PluginHeaderValue,
27): ConfigHeaderValue => {
28 if (typeof value === "string") return value;26 if (typeof value === "string") return value;
29 const ref = `${SECRET_REF_PREFIX}${value.secretId}`;27 const ref = `${SECRET_REF_PREFIX}${value.secretId}`;
30 return value.prefix ? { value: ref, prefix: value.prefix } : ref;28 return value.prefix ? { value: ref, prefix: value.prefix } : ref;
@@ -55,9 +53,7 @@ const defaultOnError = (op: "upsert" | "remove", err: unknown): void => { · 24 unmodified lines
55 console.warn(`[config-sink] ${op} failed`, err);53 console.warn(`[config-sink] ${op} failed`, err);
56};54};
57 55
58export const makeFileConfigSink = (56export const makeFileConfigSink = (options: ConfigFileSinkOptions): ConfigFileSink => {
59 options: ConfigFileSinkOptions,
60): ConfigFileSink => {
61 const { path, fsLayer, onError = defaultOnError } = options;57 const { path, fsLayer, onError = defaultOnError } = options;
62 58
63 return {59 return {
packages/core/execution/src/description.test.ts+32−42
@@ -56,54 +56,44 @@ const slackPlugin = definePlugin(() => ({ · 55 unmodified lines
56}));56}));
57 57
58describe("buildExecuteDescription", () => {58describe("buildExecuteDescription", () => {
59 it.effect(59 it.effect("renders real source ids as namespaces (sorted) through the real executor flow", () =>
60 "renders real source ids as namespaces (sorted) through the real executor flow",60 Effect.gen(function* () {
61 () =>61 // Intentionally register in non-alphabetical order — the formatter
62 Effect.gen(function* () {62 // is expected to sort by source id.
63 // Intentionally register in non-alphabetical order — the formatter63 const executor = yield* createExecutor(
64 // is expected to sort by source id.64 makeTestConfig({ plugins: [slackPlugin(), githubPlugin()] as const }),
65 const executor = yield* createExecutor(65 );
66 makeTestConfig({ plugins: [slackPlugin(), githubPlugin()] as const }),
67 );
68 66
69 const description = yield* buildExecuteDescription(executor);67 const description = yield* buildExecuteDescription(executor);
70 68
71 // Stable anchor from the workflow preamble.69 // Stable anchor from the workflow preamble.
72 expect(description).toContain(70 expect(description).toContain("Execute TypeScript in a sandboxed runtime");
73 "Execute TypeScript in a sandboxed runtime",71 // The namespaces section header.
74 );72 expect(description).toContain("## Available namespaces");
75 // The namespaces section header.73 // Each source renders with its ACTUAL id (not pluginId / name / UUID).
76 expect(description).toContain("## Available namespaces");74 expect(description).toContain("`github` — GitHub");
77 // Each source renders with its ACTUAL id (not pluginId / name / UUID).75 expect(description).toContain("`slack` — Slack Workspace");
78 expect(description).toContain("`github` — GitHub");76 // And the plugin ids must NOT leak into the namespace list.
79 expect(description).toContain("`slack` — Slack Workspace");77 expect(description).not.toContain("`github-plugin`");
80 // And the plugin ids must NOT leak into the namespace list.78 expect(description).not.toContain("`slack-plugin`");
81 expect(description).not.toContain("`github-plugin`");
82 expect(description).not.toContain("`slack-plugin`");
83 79
84 // Sort order: `github` before `slack`.80 // Sort order: `github` before `slack`.
85 const githubIdx = description.indexOf("`github`");81 const githubIdx = description.indexOf("`github`");
86 const slackIdx = description.indexOf("`slack`");82 const slackIdx = description.indexOf("`slack`");
87 expect(githubIdx).toBeGreaterThan(-1);83 expect(githubIdx).toBeGreaterThan(-1);
88 expect(slackIdx).toBeGreaterThan(-1);84 expect(slackIdx).toBeGreaterThan(-1);
89 expect(githubIdx).toBeLessThan(slackIdx);85 expect(githubIdx).toBeLessThan(slackIdx);
90 }),86 }),
91 );87 );
92 88
93 it.effect(89 it.effect("omits the Available namespaces section when no plugins register sources", () =>
94 "omits the Available namespaces section when no plugins register sources",90 Effect.gen(function* () {
95 () =>91 const executor = yield* createExecutor(makeTestConfig({ plugins: [] as const }));
96 Effect.gen(function* () {
97 const executor = yield* createExecutor(
98 makeTestConfig({ plugins: [] as const }),
99 );
100 92
101 const description = yield* buildExecuteDescription(executor);93 const description = yield* buildExecuteDescription(executor);
102 94
103 expect(description).toContain(95 expect(description).toContain("Execute TypeScript in a sandboxed runtime");
104 "Execute TypeScript in a sandboxed runtime",96 expect(description).not.toContain("## Available namespaces");
105 );97 }),
106 expect(description).not.toContain("## Available namespaces");
107 }),
108 );98 );
109});99});
packages/core/execution/src/description.ts+5−7
@@ -10,13 +10,11 @@ import type { Executor, Source } from "@executor-js/sdk/core"; · 9 unmodified lines
10 */10 */
11export const buildExecuteDescription = (executor: Executor): Effect.Effect<string> =>11export const buildExecuteDescription = (executor: Executor): Effect.Effect<string> =>
12 Effect.gen(function* () {12 Effect.gen(function* () {
13 const sources: readonly Source[] = yield* executor.sources13 const sources: readonly Source[] = yield* executor.sources.list().pipe(
14 .list()14 // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: ExecutionEngine.getDescription currently exposes no error channel; engine typed-error widening is covered separately
15 .pipe(15 Effect.orDie,
16 // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: ExecutionEngine.getDescription currently exposes no error channel; engine typed-error widening is covered separately16 Effect.withSpan("executor.sources.list"),
17 Effect.orDie,17 );
18 Effect.withSpan("executor.sources.list"),
19 );
20 18
21 const description = yield* Effect.sync(() => formatDescription(sources)).pipe(19 const description = yield* Effect.sync(() => formatDescription(sources)).pipe(
22 Effect.withSpan("schema.compile.description", {20 Effect.withSpan("schema.compile.description", {
packages/core/execution/src/engine.test.ts+2−4
@@ -20,13 +20,11 @@ class FakeRuntimeError extends Data.TaggedError("FakeRuntimeError")<{ · 19 unmodified lines
20}> {}20}> {}
21 21
22const failingExecutor: CodeExecutor<FakeRuntimeError> = {22const failingExecutor: CodeExecutor<FakeRuntimeError> = {
23 execute: () =>23 execute: () => Effect.fail(new FakeRuntimeError({ message: "Unexpected token ':'" })),
24 Effect.fail(new FakeRuntimeError({ message: "Unexpected token ':'" })),
25};24};
26 25
27const succeedingExecutor: CodeExecutor<FakeRuntimeError> = {26const succeedingExecutor: CodeExecutor<FakeRuntimeError> = {
28 execute: () =>27 execute: () => Effect.succeed({ result: "ok", logs: [] } satisfies ExecuteResult),
29 Effect.succeed({ result: "ok", logs: [] } satisfies ExecuteResult),
30};28};
31 29
32const emptyPlugin = definePlugin(() => ({30const emptyPlugin = definePlugin(() => ({
packages/core/execution/src/promise.ts+4−12
@@ -30,13 +30,9 @@ import { · 29 unmodified lines
30 type ResumeResponse,30 type ResumeResponse,
31} from "./engine";31} from "./engine";
32 32
33export type ElicitationHandler = (33export type ElicitationHandler = (ctx: ElicitationContext) => Promise<ElicitationResponse>;
34 ctx: ElicitationContext,
35) => Promise<ElicitationResponse>;
36 34
37export type ExecutionEngineConfig<35export type ExecutionEngineConfig<E extends Cause.YieldableError = CodeExecutionError> = {
38 E extends Cause.YieldableError = CodeExecutionError,
39> = {
40 readonly executor: PromiseExecutor;36 readonly executor: PromiseExecutor;
41 readonly codeExecutor: CodeExecutor<E>;37 readonly codeExecutor: CodeExecutor<E>;
42};38};
@@ -64,9 +60,7 @@ const fromPromise = <A>(try_: () => Promise<A>): Effect.Effect<A> => · 21 unmodified lines
64type EffectInvokeOptions = Parameters<EffectExecutor["tools"]["invoke"]>[2];60type EffectInvokeOptions = Parameters<EffectExecutor["tools"]["invoke"]>[2];
65type PromiseInvokeOptions = Parameters<PromiseExecutor["tools"]["invoke"]>[2];61type PromiseInvokeOptions = Parameters<PromiseExecutor["tools"]["invoke"]>[2];
66 62
67const toPromiseInvokeOptions = (63const toPromiseInvokeOptions = (options: EffectInvokeOptions): PromiseInvokeOptions => {
68 options: EffectInvokeOptions,
69): PromiseInvokeOptions => {
70 const onElicitation = options?.onElicitation;64 const onElicitation = options?.onElicitation;
71 if (!onElicitation) return undefined;65 if (!onElicitation) return undefined;
72 if (onElicitation === "accept-all") return { onElicitation };66 if (onElicitation === "accept-all") return { onElicitation };
@@ -154,9 +148,7 @@ export const toPromiseExecutionEngine = <E extends Cause.YieldableError>( · 81 unmodified lines
154 getDescription: () => Effect.runPromise(engine.getDescription),148 getDescription: () => Effect.runPromise(engine.getDescription),
155});149});
156 150
157export const createExecutionEngine = <151export const createExecutionEngine = <E extends Cause.YieldableError = CodeExecutionError>(
158 E extends Cause.YieldableError = CodeExecutionError,
159>(
160 config: ExecutionEngineConfig<E>,152 config: ExecutionEngineConfig<E>,
161): ExecutionEngine =>153): ExecutionEngine =>
162 toPromiseExecutionEngine(154 toPromiseExecutionEngine(
packages/core/execution/src/tool-invoker.test.ts+9−18
@@ -133,9 +133,7 @@ const errorPlugin = definePlugin(() => ({ · 132 unmodified lines
133}));133}));
134 134
135const makeSearchExecutor = () =>135const makeSearchExecutor = () =>
136 createExecutor(136 createExecutor(makeTestConfig({ plugins: [githubPlugin(), crmPlugin()] as const }));
137 makeTestConfig({ plugins: [githubPlugin(), crmPlugin()] as const }),
138 );
139 137
140describe("tool discovery", () => {138describe("tool discovery", () => {
141 it.effect("ranks matches using ids, namespaces, camelCase names, and descriptions", () =>139 it.effect("ranks matches using ids, namespaces, camelCase names, and descriptions", () =>
@@ -155,9 +153,7 @@ describe("tool discovery", () => { · 13 unmodified lines
155 153
156 const crmMatches = yield* searchTools(executor, "crm create contact", 5);154 const crmMatches = yield* searchTools(executor, "crm create contact", 5);
157 expect(crmMatches.items[0]?.path).toBe("crm.createContact");155 expect(crmMatches.items[0]?.path).toBe("crm.createContact");
158 expect(crmMatches.items[0]?.score ?? 0).toBeGreaterThan(156 expect(crmMatches.items[0]?.score ?? 0).toBeGreaterThan(crmMatches.items[1]?.score ?? 0);
159 crmMatches.items[1]?.score ?? 0,
160 );
161 }),157 }),
162 );158 );
163 159
@@ -218,9 +214,7 @@ describe("tool discovery", () => { · 54 unmodified lines
218 const githubOnly = yield* searchTools(executor, "list", 5, {214 const githubOnly = yield* searchTools(executor, "list", 5, {
219 namespace: "github",215 namespace: "github",
220 });216 });
221 expect(githubOnly.items.map((match) => match.path)).toEqual([217 expect(githubOnly.items.map((match) => match.path)).toEqual(["github.listRepositoryIssues"]);
222 "github.listRepositoryIssues",
223 ]);
224 218
225 const crmOnly = yield* searchTools(executor, "list", 5, {219 const crmOnly = yield* searchTools(executor, "list", 5, {
226 namespace: "crm",220 namespace: "crm",
@@ -426,16 +420,12 @@ describe("tool discovery", () => { · 199 unmodified lines
426 420
427 it.effect("converts message-bearing tool error results into execution errors", () =>421 it.effect("converts message-bearing tool error results into execution errors", () =>
428 Effect.gen(function* () {422 Effect.gen(function* () {
429 const executor = yield* createExecutor(423 const executor = yield* createExecutor(makeTestConfig({ plugins: [errorPlugin()] as const }));
430 makeTestConfig({ plugins: [errorPlugin()] as const }),
431 );
432 const invoker = makeExecutorToolInvoker(executor, {424 const invoker = makeExecutorToolInvoker(executor, {
433 invokeOptions: { onElicitation: acceptAll },425 invokeOptions: { onElicitation: acceptAll },
434 });426 });
435 427
436 const error = yield* Effect.flip(428 const error = yield* Effect.flip(invoker.invoke({ path: "records.queryRows", args: {} }));
437 invoker.invoke({ path: "records.queryRows", args: {} }),
438 );
439 429
440 expect(error).toEqual(430 expect(error).toEqual(
441 expect.objectContaining({431 expect.objectContaining({
@@ -552,10 +542,11 @@ describe("pause/resume with multiple elicitations", () => { · 110 unmodified lines
552 542
553 // `execution.fiber` is on `InternalPausedExecution`; the exported543 // `execution.fiber` is on `InternalPausedExecution`; the exported
554 // `PausedExecution` type doesn't carry it. Cast to read.544 // `PausedExecution` type doesn't carry it. Cast to read.
555 const pausedWithFiber = (value: unknown): {545 const pausedWithFiber = (
546 value: unknown,
547 ): {
556 readonly fiber: Fiber.Fiber<unknown, unknown>;548 readonly fiber: Fiber.Fiber<unknown, unknown>;
557 } =>549 } => value as { readonly fiber: Fiber.Fiber<unknown, unknown> };
558 value as { readonly fiber: Fiber.Fiber<unknown, unknown> };
559 const sandboxFiber = pausedWithFiber(paused1.execution).fiber;550 const sandboxFiber = pausedWithFiber(paused1.execution).fiber;
560 const exitProbe = await Effect.runPromise(551 const exitProbe = await Effect.runPromise(
561 Effect.race(552 Effect.race(
packages/core/execution/src/tool-invoker.ts+6−4
@@ -60,9 +60,7 @@ type ToolResultEnvelope = { · 59 unmodified lines
60};60};
61 61
62const isToolResultEnvelope = (value: unknown): value is ToolResultEnvelope =>62const isToolResultEnvelope = (value: unknown): value is ToolResultEnvelope =>
63 value !== null &&63 value !== null && typeof value === "object" && ("error" in value || "data" in value);
64 typeof value === "object" &&
65 ("error" in value || "data" in value);
66 64
67const hasToolResultError = (65const hasToolResultError = (
68 value: ToolResultEnvelope,66 value: ToolResultEnvelope,
@@ -129,7 +127,11 @@ export const makeExecutorToolInvoker = ( · 60 unmodified lines
129 127
130const isElicitationDeclinedError = (128const isElicitationDeclinedError = (
131 value: unknown,129 value: unknown,
132): value is { readonly _tag: "ElicitationDeclinedError"; readonly toolId: string; readonly action: "cancel" | "decline" } =>130): value is {
131 readonly _tag: "ElicitationDeclinedError";
132 readonly toolId: string;
133 readonly action: "cancel" | "decline";
134} =>
133 Predicate.isTagged(value, "ElicitationDeclinedError") &&135 Predicate.isTagged(value, "ElicitationDeclinedError") &&
134 value !== null &&136 value !== null &&
135 typeof value === "object" &&137 typeof value === "object" &&
packages/core/sdk/README.md+21−29
@@ -83,35 +83,27 @@ interface MemorySecretsConfig { · 82 unmodified lines
83 83
84// definePlugin takes a factory and returns a configured-plugin function84// definePlugin takes a factory and returns a configured-plugin function
85// that the consumer calls with options.85// that the consumer calls with options.
86export const memorySecretsPlugin = definePlugin(86export const memorySecretsPlugin = definePlugin((options?: MemorySecretsConfig) => {
87 (options?: MemorySecretsConfig) => {87 const map = new Map<string, string>(Object.entries(options?.initial ?? {}));
88 const map = new Map<string, string>(88 const provider: SecretProvider = {
89 Object.entries(options?.initial ?? {}),89 key: "memory",
90 );90 writable: true,
91 const provider: SecretProvider = {91 get: (id: string) => Effect.sync(() => map.get(id) ?? null),
92 key: "memory",92 has: (id: string) => Effect.sync(() => map.has(id)),
93 writable: true,93 set: (id: string, value: string) => Effect.sync(() => void map.set(id, value)),
94 get: (id: string) => Effect.sync(() => map.get(id) ?? null),94 delete: (id: string) => Effect.sync(() => map.delete(id)),
95 has: (id: string) => Effect.sync(() => map.has(id)),95 list: () => Effect.sync(() => Array.from(map.keys()).map((k) => ({ id: k, name: k }))),
96 set: (id: string, value: string) =>96 };
97 Effect.sync(() => void map.set(id, value)),97
98 delete: (id: string) => Effect.sync(() => map.delete(id)),98 return {
99 list: () =>99 id: "memorySecrets" as const,
100 Effect.sync(() =>100 storage: () => ({}),
101 Array.from(map.keys()).map((k) => ({ id: k, name: k })),101 extension: () => ({
102 ),102 label: "in-memory secrets",
103 };103 }),
104 104 secretProviders: () => [provider],
105 return {105 };
106 id: "memorySecrets" as const,106});
107 storage: () => ({}),
108 extension: () => ({
109 label: "in-memory secrets",
110 }),
111 secretProviders: () => [provider],
112 };
113 },
114);
115 107
116// End users compose the executor exactly the same way as above —108// End users compose the executor exactly the same way as above —
117// the plugin's extension is reachable as `executor.memorySecrets`.109// the plugin's extension is reachable as `executor.memorySecrets`.
packages/core/sdk/package.json+12−12
@@ -53,8 +53,18 @@ · 52 unmodified lines
53 "dependencies": {53 "dependencies": {
54 "@executor-js/storage-core": "workspace:*",54 "@executor-js/storage-core": "workspace:*",
55 "effect": "catalog:",55 "effect": "catalog:",
56 "oauth4webapi": "^3.8.5",56 "fractional-indexing": "^3.2.0",
57 "fractional-indexing": "^3.2.0"57 "oauth4webapi": "^3.8.5"
58 },
59 "devDependencies": {
60 "@effect/atom-react": "catalog:",
61 "@effect/vitest": "catalog:",
62 "@types/node": "catalog:",
63 "@types/react": "catalog:",
64 "react": "catalog:",
65 "tsup": "catalog:",
66 "typescript": "catalog:",
67 "vitest": "catalog:"
58 },68 },
59 "peerDependencies": {69 "peerDependencies": {
60 "@effect/atom-react": "catalog:",70 "@effect/atom-react": "catalog:",
@@ -67,15 +77,5 @@ · 6 unmodified lines
67 "react": {77 "react": {
68 "optional": true78 "optional": true
69 }79 }
70 },
71 "devDependencies": {
72 "@effect/atom-react": "catalog:",
73 "@effect/vitest": "catalog:",
74 "@types/node": "catalog:",
75 "@types/react": "catalog:",
76 "react": "catalog:",
77 "tsup": "catalog:",
78 "typescript": "catalog:",
79 "vitest": "catalog:"
80 }80 }
81}81}
packages/core/sdk/src/blob.test.ts+2−6
@@ -75,9 +75,7 @@ describe("pluginBlobStore", () => { · 74 unmodified lines
75 Effect.gen(function* () {75 Effect.gen(function* () {
76 const store = makeInMemoryBlobStore();76 const store = makeInMemoryBlobStore();
77 const plugin = pluginBlobStore(store, ["inner", "outer"], "my-plugin");77 const plugin = pluginBlobStore(store, ["inner", "outer"], "my-plugin");
78 const err = yield* plugin78 const err = yield* plugin.put("k", "v", { scope: "not-in-stack" }).pipe(Effect.flip);
79 .put("k", "v", { scope: "not-in-stack" })
80 .pipe(Effect.flip);
81 expect(err).toBeInstanceOf(StorageError);79 expect(err).toBeInstanceOf(StorageError);
82 expect(err).toMatchObject({80 expect(err).toMatchObject({
83 message: expect.stringContaining("not in the"),81 message: expect.stringContaining("not in the"),
@@ -91,9 +89,7 @@ describe("pluginBlobStore", () => { · 7 unmodified lines
91 Effect.gen(function* () {89 Effect.gen(function* () {
92 const store = makeInMemoryBlobStore();90 const store = makeInMemoryBlobStore();
93 const plugin = pluginBlobStore(store, ["inner"], "my-plugin");91 const plugin = pluginBlobStore(store, ["inner"], "my-plugin");
94 const err = yield* plugin92 const err = yield* plugin.delete("k", { scope: "not-in-stack" }).pipe(Effect.flip);
95 .delete("k", { scope: "not-in-stack" })
96 .pipe(Effect.flip);
97 expect(err).toBeInstanceOf(StorageError);93 expect(err).toBeInstanceOf(StorageError);
98 }),94 }),
99 );95 );
packages/core/sdk/src/blob.ts+4−16
@@ -21,10 +21,7 @@ import { Effect } from "effect"; · 20 unmodified lines
21import { StorageError } from "@executor-js/storage-core";21import { StorageError } from "@executor-js/storage-core";
22 22
23export interface BlobStore {23export interface BlobStore {
24 readonly get: (24 readonly get: (namespace: string, key: string) => Effect.Effect<string | null, StorageError>;
25 namespace: string,
26 key: string,
27 ) => Effect.Effect<string | null, StorageError>;
28 /** Multi-namespace lookup for a single key. Backends issue one query25 /** Multi-namespace lookup for a single key. Backends issue one query
29 * (`WHERE namespace IN (...) AND key = ?`) and return the hits keyed26 * (`WHERE namespace IN (...) AND key = ?`) and return the hits keyed
30 * by namespace — the caller applies its own precedence. Lets27 * by namespace — the caller applies its own precedence. Lets
@@ -39,14 +36,8 @@ export interface BlobStore { · 8 unmodified lines
39 key: string,36 key: string,
40 value: string,37 value: string,
41 ) => Effect.Effect<void, StorageError>;38 ) => Effect.Effect<void, StorageError>;
42 readonly delete: (39 readonly delete: (namespace: string, key: string) => Effect.Effect<void, StorageError>;
43 namespace: string,40 readonly has: (namespace: string, key: string) => Effect.Effect<boolean, StorageError>;
44 key: string,
45 ) => Effect.Effect<void, StorageError>;
46 readonly has: (
47 namespace: string,
48 key: string,
49 ) => Effect.Effect<boolean, StorageError>;
50}41}
51 42
52export interface PluginBlobStore {43export interface PluginBlobStore {
@@ -69,10 +60,7 @@ export interface PluginBlobStore { · 16 unmodified lines
69 readonly has: (key: string) => Effect.Effect<boolean, StorageError>;60 readonly has: (key: string) => Effect.Effect<boolean, StorageError>;
70}61}
71 62
72const assertScope = (63const assertScope = (scope: string, scopes: readonly string[]): Effect.Effect<void, StorageError> =>
73 scope: string,
74 scopes: readonly string[],
75): Effect.Effect<void, StorageError> =>
76 scopes.includes(scope)64 scopes.includes(scope)
77 ? Effect.void65 ? Effect.void
78 : Effect.fail(66 : Effect.fail(
packages/core/sdk/src/client.ts+8−20
@@ -35,12 +35,7 @@ export * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; · 34 unmodified lines
35export * as Atom from "effect/unstable/reactivity/Atom";35export * as Atom from "effect/unstable/reactivity/Atom";
36export * as AtomHttpApi from "effect/unstable/reactivity/AtomHttpApi";36export * as AtomHttpApi from "effect/unstable/reactivity/AtomHttpApi";
37 37
38export {38export { useAtomValue, useAtomSet, useAtomMount, useAtomRefresh } from "@effect/atom-react";
39 useAtomValue,
40 useAtomSet,
41 useAtomMount,
42 useAtomRefresh,
43} from "@effect/atom-react";
44 39
45// ---------------------------------------------------------------------------40// ---------------------------------------------------------------------------
46// defineClientPlugin — declarative spec for the frontend half of a plugin.41// defineClientPlugin — declarative spec for the frontend half of a plugin.
@@ -219,14 +214,11 @@ export const createPluginAtomClient = < · 172 unmodified lines
219 const { baseUrl = "/api" } = options;214 const { baseUrl = "/api" } = options;
220 const pluginId = group.identifier;215 const pluginId = group.identifier;
221 const bundle = HttpApi.make(`plugin-${pluginId}`).add(group);216 const bundle = HttpApi.make(`plugin-${pluginId}`).add(group);
222 return AtomHttpApi.Service<`Plugin_${G["identifier"]}Client`>()(217 return AtomHttpApi.Service<`Plugin_${G["identifier"]}Client`>()(`Plugin_${pluginId}Client`, {
223 `Plugin_${pluginId}Client`,218 api: bundle,
224 {219 httpClient: FetchHttpClient.layer,
225 api: bundle,220 baseUrl,
226 httpClient: FetchHttpClient.layer,221 });
227 baseUrl,
228 },
229 );
230};222};
231 223
232// ---------------------------------------------------------------------------224// ---------------------------------------------------------------------------
@@ -250,9 +242,7 @@ interface ExecutorPluginsContextValue { · 17 unmodified lines
250 readonly secretProviderPlugins: readonly SecretProviderPlugin[];242 readonly secretProviderPlugins: readonly SecretProviderPlugin[];
251}243}
252 244
253const ExecutorPluginsContext = createContext<245const ExecutorPluginsContext = createContext<ExecutorPluginsContextValue | null>(null);
254 ExecutorPluginsContextValue | null
255>(null);
256ExecutorPluginsContext.displayName = "ExecutorPluginsContext";246ExecutorPluginsContext.displayName = "ExecutorPluginsContext";
257 247
258export interface ExecutorPluginsProviderProps {248export interface ExecutorPluginsProviderProps {
@@ -286,9 +276,7 @@ const usePluginsCtx = (hookName: string): ExecutorPluginsContextValue => { · 27 unmodified lines
286 const ctx = useContext(ExecutorPluginsContext);276 const ctx = useContext(ExecutorPluginsContext);
287 if (!ctx) {277 if (!ctx) {
288 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: React hook invariant278 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: React hook invariant
289 throw new Error(279 throw new Error(`${hookName} must be called inside an <ExecutorPluginsProvider>.`);
290 `${hookName} must be called inside an <ExecutorPluginsProvider>.`,
291 );
292 }280 }
293 return ctx;281 return ctx;
294};282};
packages/core/sdk/src/connections.test.ts+358−422
@@ -162,9 +162,7 @@ describe("connections", () => { · 161 unmodified lines
162 }),162 }),
163 )163 )
164 .pipe(Effect.flip);164 .pipe(Effect.flip);
165 expect(165 expect(Predicate.isTagged(err, "ConnectionProviderNotRegisteredError")).toBe(true);
166 Predicate.isTagged(err, "ConnectionProviderNotRegisteredError"),
167 ).toBe(true);
168 }),166 }),
169 );167 );
170 168
@@ -200,54 +198,52 @@ describe("connections", () => { · 29 unmodified lines
200 }),198 }),
201 );199 );
202 200
203 it.effect(201 it.effect("secrets.list hides connection-owned secrets but surfaces bare ones", () =>
204 "secrets.list hides connection-owned secrets but surfaces bare ones",202 Effect.gen(function* () {
205 () =>203 const { provider } = makeConnectionProvider({ key: "spotify" });
206 Effect.gen(function* () {204 const executor = yield* createExecutor(
207 const { provider } = makeConnectionProvider({ key: "spotify" });205 makeTestConfig({
208 const executor = yield* createExecutor(206 plugins: [memorySecretsPlugin(), connPlugin(provider)] as const,
209 makeTestConfig({207 }),
210 plugins: [memorySecretsPlugin(), connPlugin(provider)] as const,208 );
211 }),
212 );
213 209
214 yield* executor.secrets.set(210 yield* executor.secrets.set(
215 new SetSecretInput({211 new SetSecretInput({
216 id: sid("bare-api"),212 id: sid("bare-api"),
217 scope: scpid("test-scope"),213 scope: scpid("test-scope"),
218 name: "bare API key",214 name: "bare API key",
219 value: "bare",215 value: "bare",
220 }),216 }),
221 );217 );
222 218
223 yield* executor.connections.create(219 yield* executor.connections.create(
224 new CreateConnectionInput({220 new CreateConnectionInput({
225 id: cid("conn-1"),221 id: cid("conn-1"),
226 scope: scpid("test-scope"),222 scope: scpid("test-scope"),
227 provider: "spotify",223 provider: "spotify",
228 identityLabel: null,224 identityLabel: null,
229 accessToken: new TokenMaterial({225 accessToken: new TokenMaterial({
230 secretId: sid("conn-1.access"),226 secretId: sid("conn-1.access"),
231 name: "access",227 name: "access",
232 value: "a",228 value: "a",
233 }),
234 refreshToken: new TokenMaterial({
235 secretId: sid("conn-1.refresh"),
236 name: "refresh",
237 value: "r",
238 }),
239 expiresAt: null,
240 oauthScope: null,
241 providerState: null,
242 }),229 }),
243 );230 refreshToken: new TokenMaterial({
231 secretId: sid("conn-1.refresh"),
232 name: "refresh",
233 value: "r",
234 }),
235 expiresAt: null,
236 oauthScope: null,
237 providerState: null,
238 }),
239 );
244 240
245 const list = yield* executor.secrets.list();241 const list = yield* executor.secrets.list();
246 const ids = list.map((s) => String(s.id));242 const ids = list.map((s) => String(s.id));
247 expect(ids).toContain("bare-api");243 expect(ids).toContain("bare-api");
248 expect(ids).not.toContain("conn-1.access");244 expect(ids).not.toContain("conn-1.access");
249 expect(ids).not.toContain("conn-1.refresh");245 expect(ids).not.toContain("conn-1.refresh");
250 }),246 }),
251 );247 );
252 248
253 it.effect(249 it.effect(
@@ -279,72 +275,55 @@ describe("connections", () => { · 25 unmodified lines
279 }),275 }),
280 );276 );
281 277
282 const err = yield* executor.secrets278 const err = yield* executor.secrets.remove("conn-1.access").pipe(Effect.flip);
283 .remove("conn-1.access")279 expect(Predicate.isTagged(err, "SecretOwnedByConnectionError")).toBe(true);
284 .pipe(Effect.flip);
285 expect(Predicate.isTagged(err, "SecretOwnedByConnectionError")).toBe(
286 true,
287 );
288 }),280 }),
289 );281 );
290 282
291 it.effect(283 it.effect("connections.remove cascades through providers and deletes the core row", () =>
292 "connections.remove cascades through providers and deletes the core row",284 Effect.gen(function* () {
293 () =>285 const secretProvider = makeMemoryProvider();
294 Effect.gen(function* () {286 const { provider } = makeConnectionProvider({ key: "spotify" });
295 const secretProvider = makeMemoryProvider();287 const executor = yield* createExecutor(
296 const { provider } = makeConnectionProvider({ key: "spotify" });288 makeTestConfig({
297 const executor = yield* createExecutor(289 plugins: [memorySecretsPlugin(secretProvider), connPlugin(provider)] as const,
298 makeTestConfig({290 }),
299 plugins: [291 );
300 memorySecretsPlugin(secretProvider),
301 connPlugin(provider),
302 ] as const,
303 }),
304 );
305 292
306 yield* executor.connections.create(293 yield* executor.connections.create(
307 new CreateConnectionInput({294 new CreateConnectionInput({
308 id: cid("conn-1"),295 id: cid("conn-1"),
309 scope: scpid("test-scope"),296 scope: scpid("test-scope"),
310 provider: "spotify",297 provider: "spotify",
311 identityLabel: null,298 identityLabel: null,
312 accessToken: new TokenMaterial({299 accessToken: new TokenMaterial({
313 secretId: sid("conn-1.access"),300 secretId: sid("conn-1.access"),
314 name: "access",301 name: "access",
315 value: "access-v1",302 value: "access-v1",
316 }),
317 refreshToken: new TokenMaterial({
318 secretId: sid("conn-1.refresh"),
319 name: "refresh",
320 value: "refresh-v1",
321 }),
322 expiresAt: null,
323 oauthScope: null,
324 providerState: null,
325 }),303 }),
326 );304 refreshToken: new TokenMaterial({
305 secretId: sid("conn-1.refresh"),
306 name: "refresh",
307 value: "refresh-v1",
308 }),
309 expiresAt: null,
310 oauthScope: null,
311 providerState: null,
312 }),
313 );
327 314
328 // Pre-check: backing provider holds the tokens.315 // Pre-check: backing provider holds the tokens.
329 expect(316 expect(yield* secretProvider.get!("conn-1.access", "test-scope")).toBe("access-v1");
330 yield* secretProvider.get!("conn-1.access", "test-scope"),317 expect(yield* secretProvider.get!("conn-1.refresh", "test-scope")).toBe("refresh-v1");
331 ).toBe("access-v1");318
332 expect(319 yield* executor.connections.remove("conn-1");
333 yield* secretProvider.get!("conn-1.refresh", "test-scope"),320
334 ).toBe("refresh-v1");321 // Connection row gone.
335 322 expect(yield* executor.connections.get("conn-1")).toBeNull();
336 yield* executor.connections.remove("conn-1");323 // Backing secret values gone from the provider.
337 324 expect(yield* secretProvider.get!("conn-1.access", "test-scope")).toBeNull();
338 // Connection row gone.325 expect(yield* secretProvider.get!("conn-1.refresh", "test-scope")).toBeNull();
339 expect(yield* executor.connections.get("conn-1")).toBeNull();326 }),
340 // Backing secret values gone from the provider.
341 expect(
342 yield* secretProvider.get!("conn-1.access", "test-scope"),
343 ).toBeNull();
344 expect(
345 yield* secretProvider.get!("conn-1.refresh", "test-scope"),
346 ).toBeNull();
347 }),
348 );327 );
349 328
350 it.effect("accessToken returns the stored value when not near expiry", () =>329 it.effect("accessToken returns the stored value when not near expiry", () =>
@@ -407,10 +386,7 @@ describe("connections", () => { · 56 unmodified lines
407 });386 });
408 const executor = yield* createExecutor(387 const executor = yield* createExecutor(
409 makeTestConfig({388 makeTestConfig({
410 plugins: [389 plugins: [memorySecretsPlugin(secretProvider), connPlugin(provider)] as const,
411 memorySecretsPlugin(secretProvider),
412 connPlugin(provider),
413 ] as const,
414 }),390 }),
415 );391 );
416 392
@@ -445,12 +421,10 @@ describe("connections", () => { · 28 unmodified lines
445 expect(calls[0]!.providerState).toEqual({ rotation: "fresh" });421 expect(calls[0]!.providerState).toEqual({ rotation: "fresh" });
446 422
447 // Backing secrets got rewritten at the same ids.423 // Backing secrets got rewritten at the same ids.
448 expect(424 expect(yield* secretProvider.get!("conn-1.access", "test-scope")).toBe(
449 yield* secretProvider.get!("conn-1.access", "test-scope"),425 "rotated-refresh-v1",
450 ).toBe("rotated-refresh-v1");426 );
451 expect(427 expect(yield* secretProvider.get!("conn-1.refresh", "test-scope")).toBe("refresh-v2");
452 yield* secretProvider.get!("conn-1.refresh", "test-scope"),
453 ).toBe("refresh-v2");
454 428
455 const got = yield* executor.connections.get("conn-1");429 const got = yield* executor.connections.get("conn-1");
456 expect(got?.providerState).toEqual({ rotation: "bumped" });430 expect(got?.providerState).toEqual({ rotation: "bumped" });
@@ -458,110 +432,105 @@ describe("connections", () => { · 1 unmodified line
458 }),432 }),
459 );433 );
460 434
461 it.effect(435 it.effect("accessToken dedupes concurrent refreshes into a single provider call", () =>
462 "accessToken dedupes concurrent refreshes into a single provider call",436 Effect.gen(function* () {
463 () =>437 // A gated refresh provider. Every concurrent caller that lands
464 Effect.gen(function* () {438 // inside the skew window must converge on the single pending
465 // A gated refresh provider. Every concurrent caller that lands439 // refresh instead of hitting the token endpoint N times. The
466 // inside the skew window must converge on the single pending440 // `entered` Deferred signals that the leader fiber is parked
467 // refresh instead of hitting the token endpoint N times. The441 // inside `refresh`; we only release the `gate` once every
468 // `entered` Deferred signals that the leader fiber is parked442 // caller has had a chance to register.
469 // inside `refresh`; we only release the `gate` once every443 const gate = yield* Deferred.make<void>();
470 // caller has had a chance to register.444 const entered = yield* Deferred.make<void>();
471 const gate = yield* Deferred.make<void>();445 const calls: ConnectionRefreshInput[] = [];
472 const entered = yield* Deferred.make<void>();446 let responseCounter = 0;
473 const calls: ConnectionRefreshInput[] = [];447 const provider: ConnectionProvider = {
474 let responseCounter = 0;448 key: "spotify",
475 const provider: ConnectionProvider = {449 refresh: (input): Effect.Effect<ConnectionRefreshResult, ConnectionRefreshError> =>
476 key: "spotify",450 Effect.gen(function* () {
477 refresh: (451 calls.push(input);
478 input,452 const n = ++responseCounter;
479 ): Effect.Effect<ConnectionRefreshResult, ConnectionRefreshError> =>453 yield* Deferred.succeed(entered, undefined as void);
480 Effect.gen(function* () {454 // Block the leader inside `refresh` until the test
481 calls.push(input);455 // releases the gate. Any other fiber that concurrently
482 const n = ++responseCounter;456 // calls `accessToken` must observe the in-flight
483 yield* Deferred.succeed(entered, undefined as void);457 // Deferred instead of entering this handler.
484 // Block the leader inside `refresh` until the test458 yield* Deferred.await(gate);
485 // releases the gate. Any other fiber that concurrently459 return {
486 // calls `accessToken` must observe the in-flight460 accessToken: `rotated-${n}`,
487 // Deferred instead of entering this handler.461 refreshToken: `refresh-${n}`,
488 yield* Deferred.await(gate);462 expiresAt: Date.now() + 3_600_000,
489 return {463 oauthScope: "user-read",
490 accessToken: `rotated-${n}`,464 providerState: null,
491 refreshToken: `refresh-${n}`,465 };
492 expiresAt: Date.now() + 3_600_000,466 }),
493 oauthScope: "user-read",467 };
494 providerState: null,
495 };
496 }),
497 };
498 468
499 const executor = yield* createExecutor(469 const executor = yield* createExecutor(
500 makeTestConfig({470 makeTestConfig({
501 plugins: [memorySecretsPlugin(), connPlugin(provider)] as const,471 plugins: [memorySecretsPlugin(), connPlugin(provider)] as const,
502 }),472 }),
503 );473 );
504 474
505 yield* executor.connections.create(475 yield* executor.connections.create(
506 new CreateConnectionInput({476 new CreateConnectionInput({
507 id: cid("conn-1"),477 id: cid("conn-1"),
508 scope: scpid("test-scope"),478 scope: scpid("test-scope"),
509 provider: "spotify",479 provider: "spotify",
510 identityLabel: null,480 identityLabel: null,
511 accessToken: new TokenMaterial({481 accessToken: new TokenMaterial({
512 secretId: sid("conn-1.access"),482 secretId: sid("conn-1.access"),
513 name: "access",483 name: "access",
514 value: "stale",484 value: "stale",
515 }),
516 refreshToken: new TokenMaterial({
517 secretId: sid("conn-1.refresh"),
518 name: "refresh",
519 value: "refresh-v1",
520 }),
521 // Expired so every caller enters the refresh branch.
522 expiresAt: Date.now() - 1_000,
523 oauthScope: null,
524 providerState: null,
525 }),485 }),
526 );486 refreshToken: new TokenMaterial({
487 secretId: sid("conn-1.refresh"),
488 name: "refresh",
489 value: "refresh-v1",
490 }),
491 // Expired so every caller enters the refresh branch.
492 expiresAt: Date.now() - 1_000,
493 oauthScope: null,
494 providerState: null,
495 }),
496 );
527 497
528 // Kick off the leader first and wait for it to park inside498 // Kick off the leader first and wait for it to park inside
529 // `refresh`. Any subsequent caller is guaranteed to see the499 // `refresh`. Any subsequent caller is guaranteed to see the
530 // in-flight Deferred the leader just registered.500 // in-flight Deferred the leader just registered.
531 const leaderFiber = yield* Effect.forkDetach(501 const leaderFiber = yield* Effect.forkDetach(executor.connections.accessToken("conn-1"), {
532 executor.connections.accessToken("conn-1"),502 startImmediately: true,
533 { startImmediately: true },503 });
534 );504 yield* Deferred.await(entered);
535 yield* Deferred.await(entered);
536 505
537 const followerFibers = yield* Effect.forEach(506 const followerFibers = yield* Effect.forEach(
538 [1, 2, 3, 4],507 [1, 2, 3, 4],
539 () =>508 () =>
540 Effect.forkDetach(executor.connections.accessToken("conn-1"), {509 Effect.forkDetach(executor.connections.accessToken("conn-1"), {
541 startImmediately: true,510 startImmediately: true,
542 }),511 }),
543 { concurrency: "unbounded" },512 { concurrency: "unbounded" },
544 );513 );
545 yield* Effect.forEach([1, 2, 3, 4], () => Effect.yieldNow);514 yield* Effect.forEach([1, 2, 3, 4], () => Effect.yieldNow);
546 515
547 // Every follower is queued on the leader's Deferred. Release516 // Every follower is queued on the leader's Deferred. Release
548 // the gate — the leader resolves, waiters wake up with the517 // the gate — the leader resolves, waiters wake up with the
549 // same token, no extra `refresh` is invoked.518 // same token, no extra `refresh` is invoked.
550 yield* Deferred.succeed(gate, undefined as void);519 yield* Deferred.succeed(gate, undefined as void);
551 520
552 const leaderResult = yield* Fiber.join(leaderFiber);521 const leaderResult = yield* Fiber.join(leaderFiber);
553 const followerResults = yield* Effect.all(522 const followerResults = yield* Effect.all(
554 followerFibers.map((f) => Fiber.await(f)),523 followerFibers.map((f) => Fiber.await(f)),
555 { concurrency: "unbounded" },524 { concurrency: "unbounded" },
556 );525 );
557 expect(leaderResult).toBe("rotated-1");526 expect(leaderResult).toBe("rotated-1");
558 for (const result of followerResults) {527 for (const result of followerResults) {
559 expect(Exit.isSuccess(result)).toBe(true);528 expect(Exit.isSuccess(result)).toBe(true);
560 if (!Exit.isSuccess(result)) continue;529 if (!Exit.isSuccess(result)) continue;
561 expect(result.value).toBe("rotated-1");530 expect(result.value).toBe("rotated-1");
562 }531 }
563 expect(calls).toHaveLength(1);532 expect(calls).toHaveLength(1);
564 }),533 }),
565 );534 );
566 535
567 it.effect(536 it.effect(
@@ -609,12 +578,8 @@ describe("connections", () => { · 41 unmodified lines
609 }),578 }),
610 );579 );
611 580
612 const flipped = yield* executor.connections581 const flipped = yield* executor.connections.accessToken("conn-1").pipe(Effect.flip);
613 .accessToken("conn-1")582 expect(Predicate.isTagged(flipped, "ConnectionReauthRequiredError")).toBe(true);
614 .pipe(Effect.flip);
615 expect(
616 Predicate.isTagged(flipped, "ConnectionReauthRequiredError"),
617 ).toBe(true);
618 if (!Predicate.isTagged(flipped, "ConnectionReauthRequiredError")) {583 if (!Predicate.isTagged(flipped, "ConnectionReauthRequiredError")) {
619 return;584 return;
620 }585 }
@@ -623,57 +588,53 @@ describe("connections", () => { · 2 unmodified lines
623 }),588 }),
624 );589 );
625 590
626 it.effect(591 it.effect("accessToken preserves ConnectionRefreshError for non-reauth failures", () =>
627 "accessToken preserves ConnectionRefreshError for non-reauth failures",592 Effect.gen(function* () {
628 () =>593 // Transient failure path — the provider failed but not with a
629 Effect.gen(function* () {594 // terminal RFC 6749 code. The SDK must keep it as-is so
630 // Transient failure path — the provider failed but not with a595 // callers can tell "retry later" from "prompt for sign-in".
631 // terminal RFC 6749 code. The SDK must keep it as-is so596 const provider: ConnectionProvider = {
632 // callers can tell "retry later" from "prompt for sign-in".597 key: "spotify",
633 const provider: ConnectionProvider = {598 refresh: (input) =>
634 key: "spotify",599 Effect.fail(
635 refresh: (input) =>600 new ConnectionRefreshError({
636 Effect.fail(601 connectionId: input.connectionId,
637 new ConnectionRefreshError({602 message: "network flake",
638 connectionId: input.connectionId,603 }),
639 message: "network flake",604 ),
640 }),605 };
641 ),
642 };
643 606
644 const executor = yield* createExecutor(607 const executor = yield* createExecutor(
645 makeTestConfig({608 makeTestConfig({
646 plugins: [memorySecretsPlugin(), connPlugin(provider)] as const,609 plugins: [memorySecretsPlugin(), connPlugin(provider)] as const,
647 }),610 }),
648 );611 );
649 612
650 yield* executor.connections.create(613 yield* executor.connections.create(
651 new CreateConnectionInput({614 new CreateConnectionInput({
652 id: cid("conn-1"),615 id: cid("conn-1"),
653 scope: scpid("test-scope"),616 scope: scpid("test-scope"),
654 provider: "spotify",617 provider: "spotify",
655 identityLabel: null,618 identityLabel: null,
656 accessToken: new TokenMaterial({619 accessToken: new TokenMaterial({
657 secretId: sid("conn-1.access"),620 secretId: sid("conn-1.access"),
658 name: "access",621 name: "access",
659 value: "stale",622 value: "stale",
660 }),
661 refreshToken: new TokenMaterial({
662 secretId: sid("conn-1.refresh"),
663 name: "refresh",
664 value: "refresh-v1",
665 }),
666 expiresAt: Date.now() - 1_000,
667 oauthScope: null,
668 providerState: null,
669 }),623 }),
670 );624 refreshToken: new TokenMaterial({
625 secretId: sid("conn-1.refresh"),
626 name: "refresh",
627 value: "refresh-v1",
628 }),
629 expiresAt: Date.now() - 1_000,
630 oauthScope: null,
631 providerState: null,
632 }),
633 );
671 634
672 const err = yield* executor.connections635 const err = yield* executor.connections.accessToken("conn-1").pipe(Effect.flip);
673 .accessToken("conn-1")636 expect(Predicate.isTagged(err, "ConnectionRefreshError")).toBe(true);
674 .pipe(Effect.flip);637 }),
675 expect(Predicate.isTagged(err, "ConnectionRefreshError")).toBe(true);
676 }),
677 );638 );
678 639
679 it.effect(640 it.effect(
@@ -705,116 +666,97 @@ describe("connections", () => { · 25 unmodified lines
705 }),666 }),
706 );667 );
707 668
708 const err = yield* executor.connections669 const err = yield* executor.connections.accessToken("conn-1").pipe(Effect.flip);
709 .accessToken("conn-1")670 expect(Predicate.isTagged(err, "ConnectionRefreshNotSupportedError")).toBe(true);
710 .pipe(Effect.flip);
711 expect(
712 Predicate.isTagged(err, "ConnectionRefreshNotSupportedError"),
713 ).toBe(true);
714 }),671 }),
715 );672 );
716 673
717 it.effect(674 it.effect("accessToken fails with ConnectionNotFoundError for an unknown id", () =>
718 "accessToken fails with ConnectionNotFoundError for an unknown id",675 Effect.gen(function* () {
719 () =>676 const executor = yield* createExecutor(
720 Effect.gen(function* () {677 makeTestConfig({
721 const executor = yield* createExecutor(678 plugins: [memorySecretsPlugin()] as const,
722 makeTestConfig({679 }),
723 plugins: [memorySecretsPlugin()] as const,680 );
724 }),
725 );
726 681
727 const err = yield* executor.connections682 const err = yield* executor.connections.accessToken("does-not-exist").pipe(Effect.flip);
728 .accessToken("does-not-exist")683 expect(Predicate.isTagged(err, "ConnectionNotFoundError")).toBe(true);
729 .pipe(Effect.flip);684 }),
730 expect(Predicate.isTagged(err, "ConnectionNotFoundError")).toBe(true);
731 }),
732 );685 );
733 686
734 it.effect(687 it.effect("updateTokens writes new values but does not rotate secret ids", () =>
735 "updateTokens writes new values but does not rotate secret ids",688 Effect.gen(function* () {
736 () =>689 const secretProvider = makeMemoryProvider();
737 Effect.gen(function* () {690 const { provider } = makeConnectionProvider({ key: "spotify" });
738 const secretProvider = makeMemoryProvider();691 const executor = yield* createExecutor(
739 const { provider } = makeConnectionProvider({ key: "spotify" });692 makeTestConfig({
740 const executor = yield* createExecutor(693 plugins: [memorySecretsPlugin(secretProvider), connPlugin(provider)] as const,
741 makeTestConfig({694 }),
742 plugins: [695 );
743 memorySecretsPlugin(secretProvider),
744 connPlugin(provider),
745 ] as const,
746 }),
747 );
748 696
749 yield* executor.connections.create(697 yield* executor.connections.create(
750 new CreateConnectionInput({698 new CreateConnectionInput({
751 id: cid("conn-1"),699 id: cid("conn-1"),
752 scope: scpid("test-scope"),700 scope: scpid("test-scope"),
753 provider: "spotify",701 provider: "spotify",
754 identityLabel: null,702 identityLabel: null,
755 accessToken: new TokenMaterial({703 accessToken: new TokenMaterial({
756 secretId: sid("conn-1.access"),704 secretId: sid("conn-1.access"),
757 name: "access",705 name: "access",
758 value: "v1",706 value: "v1",
759 }),
760 refreshToken: new TokenMaterial({
761 secretId: sid("conn-1.refresh"),
762 name: "refresh",
763 value: "r1",
764 }),
765 expiresAt: null,
766 oauthScope: null,
767 providerState: null,
768 }),707 }),
769 );708 refreshToken: new TokenMaterial({
770 709 secretId: sid("conn-1.refresh"),
771 const updated = yield* executor.connections.updateTokens(710 name: "refresh",
772 new UpdateConnectionTokensInput({711 value: "r1",
773 id: cid("conn-1"),
774 accessToken: "v2",
775 refreshToken: "r2",
776 expiresAt: 1_700_000_000_000,
777 oauthScope: "new-scope",
778 providerState: { rotation: "next" },
779 }),712 }),
780 );713 expiresAt: null,
714 oauthScope: null,
715 providerState: null,
716 }),
717 );
781 718
782 expect(updated.accessTokenSecretId).toBe(sid("conn-1.access"));719 const updated = yield* executor.connections.updateTokens(
783 expect(updated.refreshTokenSecretId).toBe(sid("conn-1.refresh"));720 new UpdateConnectionTokensInput({
784 expect(updated.expiresAt).toBe(1_700_000_000_000);721 id: cid("conn-1"),
785 expect(updated.oauthScope).toBe("new-scope");722 accessToken: "v2",
786 expect(updated.providerState).toEqual({ rotation: "next" });723 refreshToken: "r2",
787 724 expiresAt: 1_700_000_000_000,
788 expect(725 oauthScope: "new-scope",
789 yield* secretProvider.get!("conn-1.access", "test-scope"),726 providerState: { rotation: "next" },
790 ).toBe("v2");727 }),
791 expect(728 );
792 yield* secretProvider.get!("conn-1.refresh", "test-scope"),729
793 ).toBe("r2");730 expect(updated.accessTokenSecretId).toBe(sid("conn-1.access"));
794 }),731 expect(updated.refreshTokenSecretId).toBe(sid("conn-1.refresh"));
732 expect(updated.expiresAt).toBe(1_700_000_000_000);
733 expect(updated.oauthScope).toBe("new-scope");
734 expect(updated.providerState).toEqual({ rotation: "next" });
735
736 expect(yield* secretProvider.get!("conn-1.access", "test-scope")).toBe("v2");
737 expect(yield* secretProvider.get!("conn-1.refresh", "test-scope")).toBe("r2");
738 }),
795 );739 );
796 740
797 it.effect(741 it.effect("updateTokens fails with ConnectionNotFoundError for unknown id", () =>
798 "updateTokens fails with ConnectionNotFoundError for unknown id",742 Effect.gen(function* () {
799 () =>743 const { provider } = makeConnectionProvider({ key: "spotify" });
800 Effect.gen(function* () {744 const executor = yield* createExecutor(
801 const { provider } = makeConnectionProvider({ key: "spotify" });745 makeTestConfig({
802 const executor = yield* createExecutor(746 plugins: [memorySecretsPlugin(), connPlugin(provider)] as const,
803 makeTestConfig({747 }),
804 plugins: [memorySecretsPlugin(), connPlugin(provider)] as const,748 );
805 }),
806 );
807 749
808 const err = yield* executor.connections750 const err = yield* executor.connections
809 .updateTokens(751 .updateTokens(
810 new UpdateConnectionTokensInput({752 new UpdateConnectionTokensInput({
811 id: cid("nope"),753 id: cid("nope"),
812 accessToken: "x",754 accessToken: "x",
813 }),755 }),
814 )756 )
815 .pipe(Effect.flip);757 .pipe(Effect.flip);
816 expect(Predicate.isTagged(err, "ConnectionNotFoundError")).toBe(true);758 expect(Predicate.isTagged(err, "ConnectionNotFoundError")).toBe(true);
817 }),759 }),
818 );760 );
819 761
820 it.effect("setIdentityLabel updates the label", () =>762 it.effect("setIdentityLabel updates the label", () =>
@@ -850,21 +792,19 @@ describe("connections", () => { · 29 unmodified lines
850 }),792 }),
851 );793 );
852 794
853 it.effect(795 it.effect("setIdentityLabel fails with ConnectionNotFoundError for unknown id", () =>
854 "setIdentityLabel fails with ConnectionNotFoundError for unknown id",796 Effect.gen(function* () {
855 () =>797 const executor = yield* createExecutor(
856 Effect.gen(function* () {798 makeTestConfig({
857 const executor = yield* createExecutor(799 plugins: [memorySecretsPlugin()] as const,
858 makeTestConfig({800 }),
859 plugins: [memorySecretsPlugin()] as const,801 );
860 }),
861 );
862 802
863 const err = yield* executor.connections803 const err = yield* executor.connections
864 .setIdentityLabel("does-not-exist", "x")804 .setIdentityLabel("does-not-exist", "x")
865 .pipe(Effect.flip);805 .pipe(Effect.flip);
866 expect(Predicate.isTagged(err, "ConnectionNotFoundError")).toBe(true);806 expect(Predicate.isTagged(err, "ConnectionNotFoundError")).toBe(true);
867 }),807 }),
868 );808 );
869 809
870 it.effect("providers() returns every registered connection provider key", () =>810 it.effect("providers() returns every registered connection provider key", () =>
@@ -936,8 +876,7 @@ const makeLayeredConnExecutors = () => · 65 unmodified lines
936describe("connections — multi-scope behaviour", () => {876describe("connections — multi-scope behaviour", () => {
937 it.effect("get picks the innermost-scope row when the same id exists at two scopes", () =>877 it.effect("get picks the innermost-scope row when the same id exists at two scopes", () =>
938 Effect.gen(function* () {878 Effect.gen(function* () {
939 const { execOuter, execInner, outerId, innerId } =879 const { execOuter, execInner, outerId, innerId } = yield* makeLayeredConnExecutors();
940 yield* makeLayeredConnExecutors();
941 880
942 yield* execOuter.connections.create(881 yield* execOuter.connections.create(
943 new CreateConnectionInput({882 new CreateConnectionInput({
@@ -991,52 +930,49 @@ describe("connections — multi-scope behaviour", () => { · 47 unmodified lines
991 }),930 }),
992 );931 );
993 932
994 it.effect(933 it.effect("remove at the inner scope does not wipe the outer-scope connection", () =>
995 "remove at the inner scope does not wipe the outer-scope connection",934 Effect.gen(function* () {
996 () =>935 const { execOuter, execInner, outerId, innerId } = yield* makeLayeredConnExecutors();
997 Effect.gen(function* () {
998 const { execOuter, execInner, outerId, innerId } =
999 yield* makeLayeredConnExecutors();
1000 936
1001 yield* execOuter.connections.create(937 yield* execOuter.connections.create(
1002 new CreateConnectionInput({938 new CreateConnectionInput({
1003 id: cid("shared"),939 id: cid("shared"),
1004 scope: outerId,940 scope: outerId,
1005 provider: "spotify",941 provider: "spotify",
1006 identityLabel: "outer",942 identityLabel: "outer",
1007 accessToken: new TokenMaterial({943 accessToken: new TokenMaterial({
1008 secretId: sid("shared.access.outer"),944 secretId: sid("shared.access.outer"),
1009 name: "access",945 name: "access",
1010 value: "outer-access",946 value: "outer-access",
1011 }),
1012 refreshToken: null,
1013 expiresAt: null,
1014 oauthScope: null,
1015 providerState: null,
1016 }),947 }),
1017 );948 refreshToken: null,
1018 yield* execInner.connections.create(949 expiresAt: null,
1019 new CreateConnectionInput({950 oauthScope: null,
1020 id: cid("shared"),951 providerState: null,
1021 scope: innerId,952 }),
1022 provider: "spotify",953 );
1023 identityLabel: "inner",954 yield* execInner.connections.create(
1024 accessToken: new TokenMaterial({955 new CreateConnectionInput({
1025 secretId: sid("shared.access.inner"),956 id: cid("shared"),
1026 name: "access",957 scope: innerId,
1027 value: "inner-access",958 provider: "spotify",
1028 }),959 identityLabel: "inner",
1029 refreshToken: null,960 accessToken: new TokenMaterial({
1030 expiresAt: null,961 secretId: sid("shared.access.inner"),
1031 oauthScope: null,962 name: "access",
1032 providerState: null,963 value: "inner-access",
1033 }),964 }),
1034 );965 refreshToken: null,
966 expiresAt: null,
967 oauthScope: null,
968 providerState: null,
969 }),
970 );
1035 971
1036 yield* execInner.connections.remove("shared");972 yield* execInner.connections.remove("shared");
1037 973
1038 const outerStill = yield* execOuter.connections.get("shared");974 const outerStill = yield* execOuter.connections.get("shared");
1039 expect(outerStill?.identityLabel).toBe("outer");975 expect(outerStill?.identityLabel).toBe("outer");
1040 }),976 }),
1041 );977 );
1042});978});
packages/core/sdk/src/connections.ts+1−3
@@ -87,9 +87,7 @@ export class CreateConnectionInput extends Schema.Class<CreateConnectionInput>( · 86 unmodified lines
87// the SDK rethrows it from `ctx.connections.accessToken` callers.87// the SDK rethrows it from `ctx.connections.accessToken` callers.
88// ---------------------------------------------------------------------------88// ---------------------------------------------------------------------------
89 89
90export class ConnectionRefreshError extends Data.TaggedError(90export class ConnectionRefreshError extends Data.TaggedError("ConnectionRefreshError")<{
91 "ConnectionRefreshError",
92)<{
93 readonly connectionId: ConnectionId;91 readonly connectionId: ConnectionId;
94 readonly message: string;92 readonly message: string;
95 /**93 /**
packages/core/sdk/src/core-schema.ts+6−17
@@ -10,10 +10,7 @@ · 9 unmodified lines
10// dynamic (runtime-registered) rows.10// dynamic (runtime-registered) rows.
11// ---------------------------------------------------------------------------11// ---------------------------------------------------------------------------
12 12
13import type {13import type { DBSchema, InferDBFieldsOutput } from "@executor-js/storage-core";
14 DBSchema,
15 InferDBFieldsOutput,
16} from "@executor-js/storage-core";
17 14
18export const coreSchema = {15export const coreSchema = {
19 source: {16 source: {
@@ -209,25 +206,18 @@ export type CoreSchema = typeof coreSchema; · 189 unmodified lines
209export type SourceRow = InferDBFieldsOutput<CoreSchema["source"]["fields"]> &206export type SourceRow = InferDBFieldsOutput<CoreSchema["source"]["fields"]> &
210 Record<string, unknown>;207 Record<string, unknown>;
211 208
212export type ToolRow = InferDBFieldsOutput<CoreSchema["tool"]["fields"]> &209export type ToolRow = InferDBFieldsOutput<CoreSchema["tool"]["fields"]> & Record<string, unknown>;
213 Record<string, unknown>;
214 210
215export type DefinitionRow = InferDBFieldsOutput<211export type DefinitionRow = InferDBFieldsOutput<CoreSchema["definition"]["fields"]> &
216 CoreSchema["definition"]["fields"]
217> &
218 Record<string, unknown>;212 Record<string, unknown>;
219 213
220export type SecretRow = InferDBFieldsOutput<CoreSchema["secret"]["fields"]> &214export type SecretRow = InferDBFieldsOutput<CoreSchema["secret"]["fields"]> &
221 Record<string, unknown>;215 Record<string, unknown>;
222 216
223export type ConnectionRow = InferDBFieldsOutput<217export type ConnectionRow = InferDBFieldsOutput<CoreSchema["connection"]["fields"]> &
224 CoreSchema["connection"]["fields"]
225> &
226 Record<string, unknown>;218 Record<string, unknown>;
227 219
228export type ToolPolicyRow = InferDBFieldsOutput<220export type ToolPolicyRow = InferDBFieldsOutput<CoreSchema["tool_policy"]["fields"]> &
229 CoreSchema["tool_policy"]["fields"]
230> &
231 Record<string, unknown>;221 Record<string, unknown>;
232 222
233// ---------------------------------------------------------------------------223// ---------------------------------------------------------------------------
@@ -251,8 +241,7 @@ export const TOOL_POLICY_ACTIONS = [ · 17 unmodified lines
251] as const satisfies readonly ToolPolicyAction[];241] as const satisfies readonly ToolPolicyAction[];
252 242
253export const isToolPolicyAction = (value: unknown): value is ToolPolicyAction =>243export const isToolPolicyAction = (value: unknown): value is ToolPolicyAction =>
254 typeof value === "string" &&244 typeof value === "string" && (TOOL_POLICY_ACTIONS as readonly string[]).includes(value);
255 (TOOL_POLICY_ACTIONS as readonly string[]).includes(value);
256 245
257// ---------------------------------------------------------------------------246// ---------------------------------------------------------------------------
258// Tool annotations — default-policy metadata the executor consults247// Tool annotations — default-policy metadata the executor consults
packages/core/sdk/src/error-handling.test.ts+4−11
@@ -43,8 +43,7 @@ import { Scope } from "./scope"; · 42 unmodified lines
43// ---------------------------------------------------------------------------43// ---------------------------------------------------------------------------
44 44
45const makeFailingAdapter = (failure: StorageFailure): DBAdapter => {45const makeFailingAdapter = (failure: StorageFailure): DBAdapter => {
46 const fail = () =>46 const fail = () => Effect.fail(failure) as Effect.Effect<never, StorageFailure, never>;
47 Effect.fail(failure) as Effect.Effect<never, StorageFailure, never>;
48 return {47 return {
49 id: "failing",48 id: "failing",
50 create: fail as DBAdapter["create"],49 create: fail as DBAdapter["create"],
@@ -91,9 +90,7 @@ describe("typed-error edge model — SDK", () => { · 40 unmodified lines
91 message: "backend lost its mind",90 message: "backend lost its mind",
92 cause: driverCause,91 cause: driverCause,
93 });92 });
94 const executor = yield* createExecutor(93 const executor = yield* createExecutor(baseConfig(makeFailingAdapter(failure)));
95 baseConfig(makeFailingAdapter(failure)),
96 );
97 94
98 const result = yield* executor.tools.list().pipe(Effect.flip);95 const result = yield* executor.tools.list().pipe(Effect.flip);
99 expect(result).toBeInstanceOf(StorageError);96 expect(result).toBeInstanceOf(StorageError);
@@ -140,9 +137,7 @@ describe("typed-error edge model — SDK", () => { · 40 unmodified lines
140 model: err.model ?? null,137 model: err.model ?? null,
141 }),138 }),
142 ),139 ),
143 Effect.map((r) =>140 Effect.map((r) => ("caught" in r ? r : { caught: false as const, model: null })),
144 "caught" in r ? r : { caught: false as const, model: null },
145 ),
146 ),141 ),
147 }),142 }),
148 extension: (ctx) => ({143 extension: (ctx) => ({
@@ -172,9 +167,7 @@ describe("typed-error edge model — SDK", () => { · 23 unmodified lines
172 // type-check (R would leak) nor run (would crash at construction).167 // type-check (R would leak) nor run (would crash at construction).
173 const executor = yield* createExecutor(168 const executor = yield* createExecutor(
174 baseConfig(169 baseConfig(
175 makeFailingAdapter(170 makeFailingAdapter(new StorageError({ message: "doesn't matter", cause: undefined })),
176 new StorageError({ message: "doesn't matter", cause: undefined }),
177 ),
178 ),171 ),
179 );172 );
180 // Sanity: the executor surface exists and methods are callable.173 // Sanity: the executor surface exists and methods are callable.
packages/core/sdk/src/errors.ts+4−7
@@ -32,13 +32,10 @@ export class PluginNotLoadedError extends Schema.TaggedErrorClass<PluginNotLoade · 31 unmodified lines
32 * the plugin only declares static tools and this one's id matched32 * the plugin only declares static tools and this one's id matched
33 * dynamically somehow. Shouldn't happen in practice; guards against33 * dynamically somehow. Shouldn't happen in practice; guards against
34 * programmer error. */34 * programmer error. */
35export class NoHandlerError extends Schema.TaggedErrorClass<NoHandlerError>()(35export class NoHandlerError extends Schema.TaggedErrorClass<NoHandlerError>()("NoHandlerError", {
36 "NoHandlerError",36 toolId: ToolId,
37 {37 pluginId: Schema.String,
38 toolId: ToolId,38}) {}
39 pluginId: Schema.String,
40 },
41) {}
42 39
43/** Tool invocation was rejected because a workspace `tool_policy` rule40/** Tool invocation was rejected because a workspace `tool_policy` rule
44 * with `action: "block"` matched. `pattern` is the matched policy41 * with `action: "block"` matched. `pattern` is the matched policy
packages/core/sdk/src/executor.test.ts+190−276
@@ -7,11 +7,7 @@ import type { DBAdapter, Where } from "@executor-js/storage-core"; · 6 unmodified lines
7import { makeInMemoryBlobStore } from "./blob";7import { makeInMemoryBlobStore } from "./blob";
8import { CreateConnectionInput, TokenMaterial } from "./connections";8import { CreateConnectionInput, TokenMaterial } from "./connections";
9import { collectSchemas, createExecutor } from "./executor";9import { collectSchemas, createExecutor } from "./executor";
10import {10import { ElicitationResponse, FormElicitation, UrlElicitation } from "./elicitation";
11 ElicitationResponse,
12 FormElicitation,
13 UrlElicitation,
14} from "./elicitation";
15import { defineSchema, definePlugin } from "./plugin";11import { defineSchema, definePlugin } from "./plugin";
16import { SetSecretInput } from "./secrets";12import { SetSecretInput } from "./secrets";
17import { makeTestConfig } from "./testing";13import { makeTestConfig } from "./testing";
@@ -29,10 +25,7 @@ class TestPluginError extends Data.TaggedError("TestPluginError")<{ · 11 unmodified lines
29 readonly message: string;25 readonly message: string;
30}> {}26}> {}
31 27
32const recordFindMany = (28const recordFindMany = (adapter: DBAdapter, calls: FindManyCall[]): DBAdapter => ({
33 adapter: DBAdapter,
34 calls: FindManyCall[],
35): DBAdapter => ({
36 ...adapter,29 ...adapter,
37 findMany: (data) => {30 findMany: (data) => {
38 calls.push({ model: data.model, where: data.where });31 calls.push({ model: data.model, where: data.where });
@@ -244,9 +237,7 @@ const opaqueRouteSecretsPlugin = definePlugin(() => ({ · 205 unmodified lines
244describe("createExecutor", () => {237describe("createExecutor", () => {
245 it.effect("invokes a static tool via the in-memory pool", () =>238 it.effect("invokes a static tool via the in-memory pool", () =>
246 Effect.gen(function* () {239 Effect.gen(function* () {
247 const executor = yield* createExecutor(240 const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));
248 makeTestConfig({ plugins: [testPlugin()] as const }),
249 );
250 const result = yield* executor.tools.invoke(241 const result = yield* executor.tools.invoke(
251 "test.control.echo",242 "test.control.echo",
252 { text: "hi" },243 { text: "hi" },
@@ -258,9 +249,7 @@ describe("createExecutor", () => { · 5 unmodified lines
258 249
259 it.effect("lists static tools alongside dynamic ones", () =>250 it.effect("lists static tools alongside dynamic ones", () =>
260 Effect.gen(function* () {251 Effect.gen(function* () {
261 const executor = yield* createExecutor(252 const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));
262 makeTestConfig({ plugins: [testPlugin()] as const }),
263 );
264 yield* executor.test.addThing("thing1", "hello");253 yield* executor.test.addThing("thing1", "hello");
265 254
266 const tools = yield* executor.tools.list();255 const tools = yield* executor.tools.list();
@@ -273,9 +262,7 @@ describe("createExecutor", () => { · 6 unmodified lines
273 262
274 it.effect("filters tools by query", () =>263 it.effect("filters tools by query", () =>
275 Effect.gen(function* () {264 Effect.gen(function* () {
276 const executor = yield* createExecutor(265 const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));
277 makeTestConfig({ plugins: [testPlugin()] as const }),
278 );
279 yield* executor.test.addThing("thing1", "hello");266 yield* executor.test.addThing("thing1", "hello");
280 267
281 const tools = yield* executor.tools.list({ query: "echo" });268 const tools = yield* executor.tools.list({ query: "echo" });
@@ -298,24 +285,17 @@ describe("createExecutor", () => { · 16 unmodified lines
298 findManyCalls.length = 0;285 findManyCalls.length = 0;
299 const tools = yield* executor.tools.list({ sourceId: "thing1" });286 const tools = yield* executor.tools.list({ sourceId: "thing1" });
300 287
301 expect(tools.map((t) => t.id).sort()).toEqual([288 expect(tools.map((t) => t.id).sort()).toEqual(["thing1.read", "thing1.write"]);
302 "thing1.read",
303 "thing1.write",
304 ]);
305 const toolRead = findManyCalls.find((call) => call.model === "tool");289 const toolRead = findManyCalls.find((call) => call.model === "tool");
306 expect(toolRead?.where).toEqual(290 expect(toolRead?.where).toEqual(
307 expect.arrayContaining([291 expect.arrayContaining([expect.objectContaining({ field: "source_id", value: "thing1" })]),
308 expect.objectContaining({ field: "source_id", value: "thing1" }),
309 ]),
310 );292 );
311 }),293 }),
312 );294 );
313 295
314 it.effect("can list tools without resolving dynamic annotations", () =>296 it.effect("can list tools without resolving dynamic annotations", () =>
315 Effect.gen(function* () {297 Effect.gen(function* () {
316 const executor = yield* createExecutor(298 const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));
317 makeTestConfig({ plugins: [testPlugin()] as const }),
318 );
319 yield* executor.test.addThing("thing1", "hello");299 yield* executor.test.addThing("thing1", "hello");
320 testAnnotationResolveCount = 0;300 testAnnotationResolveCount = 0;
321 301
@@ -325,19 +305,14 @@ describe("createExecutor", () => { · 3 unmodified lines
325 });305 });
326 306
327 expect(testAnnotationResolveCount).toBe(0);307 expect(testAnnotationResolveCount).toBe(0);
328 expect(tools.map((t) => t.id).sort()).toEqual([308 expect(tools.map((t) => t.id).sort()).toEqual(["thing1.read", "thing1.write"]);
329 "thing1.read",
330 "thing1.write",
331 ]);
332 expect(tools.every((tool) => tool.annotations === undefined)).toBe(true);309 expect(tools.every((tool) => tool.annotations === undefined)).toBe(true);
333 }),310 }),
334 );311 );
335 312
336 it.effect("invokes a dynamic tool through plugin.invokeTool", () =>313 it.effect("invokes a dynamic tool through plugin.invokeTool", () =>
337 Effect.gen(function* () {314 Effect.gen(function* () {
338 const executor = yield* createExecutor(315 const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));
339 makeTestConfig({ plugins: [testPlugin()] as const }),
340 );
341 yield* executor.test.addThing("thing1", "hello");316 yield* executor.test.addThing("thing1", "hello");
342 317
343 const result = yield* executor.tools.invoke(318 const result = yield* executor.tools.invoke(
@@ -351,9 +326,7 @@ describe("createExecutor", () => { · 7 unmodified lines
351 326
352 it.effect("enforces tool annotations before invoking", () =>327 it.effect("enforces tool annotations before invoking", () =>
353 Effect.gen(function* () {328 Effect.gen(function* () {
354 const executor = yield* createExecutor(329 const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));
355 makeTestConfig({ plugins: [testPlugin()] as const }),
356 );
357 yield* executor.test.addThing("thing1", "hello");330 yield* executor.test.addThing("thing1", "hello");
358 331
359 // requiresApproval: true → declined → ElicitationDeclinedError332 // requiresApproval: true → declined → ElicitationDeclinedError
@@ -362,8 +335,7 @@ describe("createExecutor", () => { · 2 unmodified lines
362 "thing1.write",335 "thing1.write",
363 { value: "updated" },336 { value: "updated" },
364 {337 {
365 onElicitation: () =>338 onElicitation: () => Effect.succeed(new ElicitationResponse({ action: "decline" })),
366 Effect.succeed(new ElicitationResponse({ action: "decline" })),
367 },339 },
368 )340 )
369 .pipe(Effect.flip);341 .pipe(Effect.flip);
@@ -381,9 +353,7 @@ describe("createExecutor", () => { · 11 unmodified lines
381 353
382 it.effect("sources.list unions static runtime sources and dynamic ones", () =>354 it.effect("sources.list unions static runtime sources and dynamic ones", () =>
383 Effect.gen(function* () {355 Effect.gen(function* () {
384 const executor = yield* createExecutor(356 const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));
385 makeTestConfig({ plugins: [testPlugin()] as const }),
386 );
387 yield* executor.test.addThing("thing1", "hello");357 yield* executor.test.addThing("thing1", "hello");
388 358
389 const sources = yield* executor.sources.list();359 const sources = yield* executor.sources.list();
@@ -444,12 +414,8 @@ describe("createExecutor", () => { · 54 unmodified lines
444 414
445 it.effect("rejects remove of a static source", () =>415 it.effect("rejects remove of a static source", () =>
446 Effect.gen(function* () {416 Effect.gen(function* () {
447 const executor = yield* createExecutor(417 const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));
448 makeTestConfig({ plugins: [testPlugin()] as const }),418 const err = yield* executor.sources.remove("test.control").pipe(Effect.flip);
449 );
450 const err = yield* executor.sources
451 .remove("test.control")
452 .pipe(Effect.flip);
453 expect(Predicate.isTagged(err, "SourceRemovalNotAllowedError")).toBe(true);419 expect(Predicate.isTagged(err, "SourceRemovalNotAllowedError")).toBe(true);
454 }),420 }),
455 );421 );
@@ -546,9 +512,7 @@ describe("createExecutor", () => { · 90 unmodified lines
546 512
547 it.effect("ctx.transaction commits all nested writes on success", () =>513 it.effect("ctx.transaction commits all nested writes on success", () =>
548 Effect.gen(function* () {514 Effect.gen(function* () {
549 const executor = yield* createExecutor(515 const executor = yield* createExecutor(makeTestConfig({ plugins: [testPlugin()] as const }));
550 makeTestConfig({ plugins: [testPlugin()] as const }),
551 );
552 // addThing wraps storage + sources.register in ctx.transaction.516 // addThing wraps storage + sources.register in ctx.transaction.
553 yield* executor.test.addThing("thing1", "hello");517 yield* executor.test.addThing("thing1", "hello");
554 518
@@ -560,13 +524,9 @@ describe("createExecutor", () => { · 5 unmodified lines
560 expect(tools).toContain("thing1.write");524 expect(tools).toContain("thing1.write");
561 525
562 // plugin storage row committed too526 // plugin storage row committed too
563 expect(527 expect(yield* executor.tools.invoke("thing1.read", {}, { onElicitation: "accept-all" })).toBe(
564 yield* executor.tools.invoke(528 "hello",
565 "thing1.read",529 );
566 {},
567 { onElicitation: "accept-all" },
568 ),
569 ).toBe("hello");
570 }),530 }),
571 );531 );
572 532
@@ -612,9 +572,7 @@ describe("createExecutor", () => { · 39 unmodified lines
612 makeTestConfig({ plugins: [rollbackPlugin()] as const }),572 makeTestConfig({ plugins: [rollbackPlugin()] as const }),
613 );573 );
614 574
615 const result = yield* executor.rollback575 const result = yield* executor.rollback.doFailingTx().pipe(Effect.result);
616 .doFailingTx()
617 .pipe(Effect.result);
618 expect(Result.isFailure(result)).toBe(true);576 expect(Result.isFailure(result)).toBe(true);
619 577
620 // Plugin storage row must not persist.578 // Plugin storage row must not persist.
@@ -684,18 +642,12 @@ describe("createExecutor", () => { · 63 unmodified lines
684 }),642 }),
685 );643 );
686 644
687 const leaked = yield* executor.secrets645 const leaked = yield* executor.secrets.get("conn-owned.access_token").pipe(Effect.flip);
688 .get("conn-owned.access_token")646 expect(Predicate.isTagged(leaked, "SecretOwnedByConnectionError")).toBe(true);
689 .pipe(Effect.flip);
690 expect(Predicate.isTagged(leaked, "SecretOwnedByConnectionError")).toBe(
691 true,
692 );
693 647
694 const status = yield* executor.secrets.status("conn-owned.access_token");648 const status = yield* executor.secrets.status("conn-owned.access_token");
695 expect(status).toBe("missing");649 expect(status).toBe("missing");
696 const visibleIds = (yield* executor.secrets.list()).map(650 const visibleIds = (yield* executor.secrets.list()).map((s) => String(s.id));
697 (s) => String(s.id),
698 );
699 expect(visibleIds).not.toContain("conn-owned.access_token");651 expect(visibleIds).not.toContain("conn-owned.access_token");
700 652
701 const token = yield* executor.connections.accessToken("conn-owned");653 const token = yield* executor.connections.accessToken("conn-owned");
@@ -764,9 +716,7 @@ describe("createExecutor", () => { · 62 unmodified lines
764 close: () => Effect.sync(() => void closed++),716 close: () => Effect.sync(() => void closed++),
765 }));717 }));
766 718
767 const executor = yield* createExecutor(719 const executor = yield* createExecutor(makeTestConfig({ plugins: [closeable()] as const }));
768 makeTestConfig({ plugins: [closeable()] as const }),
769 );
770 720
771 yield* executor.close();721 yield* executor.close();
772 expect(closed).toBe(1);722 expect(closed).toBe(1);
@@ -812,18 +762,14 @@ describe("createExecutor", () => { · 39 unmodified lines
812 ],762 ],
813 }));763 }));
814 764
815 const executor = yield* createExecutor(765 const executor = yield* createExecutor(makeTestConfig({ plugins: [loginPlugin()] as const }));
816 makeTestConfig({ plugins: [loginPlugin()] as const }),
817 );
818 766
819 const result = yield* executor.tools.invoke(767 const result = yield* executor.tools.invoke(
820 "login.ctl.signIn",768 "login.ctl.signIn",
821 {},769 {},
822 {770 {
823 onElicitation: (ctx) => {771 onElicitation: (ctx) => {
824 expect(Predicate.isTagged(ctx.request, "FormElicitation")).toBe(772 expect(Predicate.isTagged(ctx.request, "FormElicitation")).toBe(true);
825 true,
826 );
827 return Effect.succeed(773 return Effect.succeed(
828 new ElicitationResponse({774 new ElicitationResponse({
829 action: "accept",775 action: "accept",
@@ -872,18 +818,14 @@ describe("createExecutor", () => { · 42 unmodified lines
872 ],818 ],
873 }));819 }));
874 820
875 const executor = yield* createExecutor(821 const executor = yield* createExecutor(makeTestConfig({ plugins: [oauthPlugin()] as const }));
876 makeTestConfig({ plugins: [oauthPlugin()] as const }),
877 );
878 822
879 const result = yield* executor.tools.invoke(823 const result = yield* executor.tools.invoke(
880 "oauth.ctl.connect",824 "oauth.ctl.connect",
881 {},825 {},
882 {826 {
883 onElicitation: (ctx) => {827 onElicitation: (ctx) => {
884 expect(Predicate.isTagged(ctx.request, "UrlElicitation")).toBe(828 expect(Predicate.isTagged(ctx.request, "UrlElicitation")).toBe(true);
885 true,
886 );
887 return Effect.succeed(829 return Effect.succeed(
888 new ElicitationResponse({830 new ElicitationResponse({
889 action: "accept",831 action: "accept",
@@ -932,9 +874,7 @@ describe("createExecutor", () => { · 42 unmodified lines
932 ],874 ],
933 }));875 }));
934 876
935 const executor = yield* createExecutor(877 const executor = yield* createExecutor(makeTestConfig({ plugins: [elicitOnly()] as const }));
936 makeTestConfig({ plugins: [elicitOnly()] as const }),
937 );
938 878
939 const action = yield* executor.tools.invoke(879 const action = yield* executor.tools.invoke(
940 "elicit.ctl.ask",880 "elicit.ctl.ask",
@@ -985,10 +925,7 @@ describe("createExecutor", () => { · 44 unmodified lines
985 }),925 }),
986 );926 );
987 927
988 const result = yield* executor.rotate.rotate(928 const result = yield* executor.rotate.rotate("DB_PASSWORD", "correct-horse-battery-staple");
989 "DB_PASSWORD",
990 "correct-horse-battery-staple",
991 );
992 expect(result).toEqual({929 expect(result).toEqual({
993 oldValue: "hunter2",930 oldValue: "hunter2",
994 newValue: "correct-horse-battery-staple",931 newValue: "correct-horse-battery-staple",
@@ -1348,8 +1285,7 @@ describe("cross-scope write preservation (SDK)", () => { · 353 unmodified lines
1348 "secrets.set at the inner scope does not wipe an outer-scope row with the same id",1285 "secrets.set at the inner scope does not wipe an outer-scope row with the same id",
1349 () =>1286 () =>
1350 Effect.gen(function* () {1287 Effect.gen(function* () {
1351 const { execOuter, execInner, outerId, innerId } =1288 const { execOuter, execInner, outerId, innerId } = yield* makeLayeredExecutors();
1352 yield* makeLayeredExecutors();
1353 1289
1354 // Admin-equivalent writes the org-wide secret at the outer scope.1290 // Admin-equivalent writes the org-wide secret at the outer scope.
1355 yield* execInner.secrets.set(1291 yield* execInner.secrets.set(
@@ -1384,9 +1320,7 @@ describe("cross-scope write preservation (SDK)", () => { · 28 unmodified lines
1384 // are the shared, observable source of truth.1320 // are the shared, observable source of truth.
1385 const outerRefs = yield* execOuter.secrets.list();1321 const outerRefs = yield* execOuter.secrets.list();
1386 expect(outerRefs.map((r) => r.id)).toContain("api-token");1322 expect(outerRefs.map((r) => r.id)).toContain("api-token");
1387 expect(outerRefs.find((r) => r.id === "api-token")?.scopeId).toBe(1323 expect(outerRefs.find((r) => r.id === "api-token")?.scopeId).toBe(outerId);
1388 outerId,
1389 );
1390 1324
1391 // Inner executor's list is de-duplicated by id (innermost wins),1325 // Inner executor's list is de-duplicated by id (innermost wins),
1392 // so we only expect one ref for `api-token` — pinned at the1326 // so we only expect one ref for `api-token` — pinned at the
@@ -1530,40 +1464,35 @@ const makeMarkerExecutors = () => · 137 unmodified lines
1530 });1464 });
1531 1465
1532describe("cross-scope read precedence + remove isolation (SDK)", () => {1466describe("cross-scope read precedence + remove isolation (SDK)", () => {
1533 it.effect(1467 it.effect("secrets.remove at the inner scope does not wipe outer-scope row with same id", () =>
1534 "secrets.remove at the inner scope does not wipe outer-scope row with same id",1468 Effect.gen(function* () {
1535 () =>1469 const { execOuter, execInner, outerId, innerId } = yield* makeLayeredExecutors();
1536 Effect.gen(function* () {
1537 const { execOuter, execInner, outerId, innerId } =
1538 yield* makeLayeredExecutors();
1539 1470
1540 yield* execInner.secrets.set(1471 yield* execInner.secrets.set(
1541 new SetSecretInput({1472 new SetSecretInput({
1542 id: SecretId.make("api-token"),1473 id: SecretId.make("api-token"),
1543 scope: outerId,1474 scope: outerId,
1544 name: "Org default",1475 name: "Org default",
1545 value: "org-default",1476 value: "org-default",
1546 }),1477 }),
1547 );1478 );
1548 yield* execInner.secrets.set(1479 yield* execInner.secrets.set(
1549 new SetSecretInput({1480 new SetSecretInput({
1550 id: SecretId.make("api-token"),1481 id: SecretId.make("api-token"),
1551 scope: innerId,1482 scope: innerId,
1552 name: "Personal override",1483 name: "Personal override",
1553 value: "personal-override",1484 value: "personal-override",
1554 }),1485 }),
1555 );1486 );
1556 1487
1557 // Inner caller removes — should only drop the inner override.1488 // Inner caller removes — should only drop the inner override.
1558 yield* execInner.secrets.remove("api-token");1489 yield* execInner.secrets.remove("api-token");
1559 1490
1560 // Outer-only executor must still see its org-scope row.1491 // Outer-only executor must still see its org-scope row.
1561 const outerRefs = yield* execOuter.secrets.list();1492 const outerRefs = yield* execOuter.secrets.list();
1562 expect(outerRefs.map((r) => r.id)).toContain("api-token");1493 expect(outerRefs.map((r) => r.id)).toContain("api-token");
1563 expect(outerRefs.find((r) => r.id === "api-token")?.scopeId).toBe(1494 expect(outerRefs.find((r) => r.id === "api-token")?.scopeId).toBe(outerId);
1564 outerId,1495 }),
1565 );
1566 }),
1567 );1496 );
1568 1497
1569 it.effect(1498 it.effect(
@@ -1584,33 +1513,30 @@ describe("cross-scope read precedence + remove isolation (SDK)", () => { · 14 unmodified lines
1584 }),1513 }),
1585 );1514 );
1586 1515
1587 it.effect(1516 it.effect("ctx.core.sources.unregister at the inner scope does not wipe outer-scope row", () =>
1588 "ctx.core.sources.unregister at the inner scope does not wipe outer-scope row",1517 Effect.gen(function* () {
1589 () =>1518 const { execOuter, execInner } = yield* makeLayeredExecutors();
1590 Effect.gen(function* () {
1591 const { execOuter, execInner } = yield* makeLayeredExecutors();
1592 1519
1593 yield* execOuter.tenant.addSource("shared");1520 yield* execOuter.tenant.addSource("shared");
1594 yield* execInner.tenant.addSource("shared");1521 yield* execInner.tenant.addSource("shared");
1595 1522
1596 // Plugin-owned unregister path (ctx.core.sources.unregister) fires1523 // Plugin-owned unregister path (ctx.core.sources.unregister) fires
1597 // via a dedicated extension method. We drive it by calling1524 // via a dedicated extension method. We drive it by calling
1598 // `sources.remove` — which routes through the same deleteSourceById1525 // `sources.remove` — which routes through the same deleteSourceById
1599 // helper — but the real regression is the findOne-before-delete1526 // helper — but the real regression is the findOne-before-delete
1600 // picking the wrong scope's row. The outer row must survive.1527 // picking the wrong scope's row. The outer row must survive.
1601 yield* execInner.sources.remove("shared");1528 yield* execInner.sources.remove("shared");
1602 1529
1603 const outerSources = yield* execOuter.sources.list();1530 const outerSources = yield* execOuter.sources.list();
1604 expect(outerSources.filter((s) => s.id === "shared")).toHaveLength(1);1531 expect(outerSources.filter((s) => s.id === "shared")).toHaveLength(1);
1605 }),1532 }),
1606 );1533 );
1607 1534
1608 it.effect(1535 it.effect(
1609 "tools.invoke picks the innermost tool when the same tool id exists at two scopes",1536 "tools.invoke picks the innermost tool when the same tool id exists at two scopes",
1610 () =>1537 () =>
1611 Effect.gen(function* () {1538 Effect.gen(function* () {
1612 const { execOuter, execInner, outerId, innerId } =1539 const { execOuter, execInner, outerId, innerId } = yield* makeMarkerExecutors();
1613 yield* makeMarkerExecutors();
1614 1540
1615 yield* execOuter.marker.register("shared", "outer");1541 yield* execOuter.marker.register("shared", "outer");
1616 yield* execInner.marker.register("shared", "inner");1542 yield* execInner.marker.register("shared", "inner");
@@ -1637,153 +1563,141 @@ describe("cross-scope read precedence + remove isolation (SDK)", () => { · 20 unmodified lines
1637 }),1563 }),
1638 );1564 );
1639 1565
1640 it.effect(1566 it.effect("tools.schema — innermost shadow returns the inner description", () =>
1641 "tools.schema — innermost shadow returns the inner description",1567 Effect.gen(function* () {
1642 () =>1568 const { execOuter, execInner } = yield* makeMarkerExecutors();
1643 Effect.gen(function* () {
1644 const { execOuter, execInner } = yield* makeMarkerExecutors();
1645 1569
1646 yield* execOuter.marker.register("shared", "outer-desc");1570 yield* execOuter.marker.register("shared", "outer-desc");
1647 yield* execInner.marker.register("shared", "inner-desc");1571 yield* execInner.marker.register("shared", "inner-desc");
1648 1572
1649 const schema = yield* execInner.tools.schema("shared.t");1573 const schema = yield* execInner.tools.schema("shared.t");
1650 expect(schema?.description).toBe("inner-desc");1574 expect(schema?.description).toBe("inner-desc");
1651 }),1575 }),
1652 );1576 );
1653 1577
1654 it.effect(1578 it.effect("tools.list dedupes by id, keeping the innermost row", () =>
1655 "tools.list dedupes by id, keeping the innermost row",1579 Effect.gen(function* () {
1656 () =>1580 const { execOuter, execInner } = yield* makeMarkerExecutors();
1657 Effect.gen(function* () {
1658 const { execOuter, execInner } = yield* makeMarkerExecutors();
1659 1581
1660 yield* execOuter.marker.register("shared", "outer-desc");1582 yield* execOuter.marker.register("shared", "outer-desc");
1661 yield* execInner.marker.register("shared", "inner-desc");1583 yield* execInner.marker.register("shared", "inner-desc");
1662 1584
1663 const tools = yield* execInner.tools.list();1585 const tools = yield* execInner.tools.list();
1664 const shared = tools.filter((t) => t.id === "shared.t");1586 const shared = tools.filter((t) => t.id === "shared.t");
1665 expect(shared).toHaveLength(1);1587 expect(shared).toHaveLength(1);
1666 expect(shared[0]?.description).toBe("inner-desc");1588 expect(shared[0]?.description).toBe("inner-desc");
1667 }),1589 }),
1668 );1590 );
1669 1591
1670 it.effect(1592 it.effect("sources.list dedupes by id, keeping the innermost row", () =>
1671 "sources.list dedupes by id, keeping the innermost row",1593 Effect.gen(function* () {
1672 () =>1594 const { execOuter, execInner } = yield* makeMarkerExecutors();
1673 Effect.gen(function* () {
1674 const { execOuter, execInner } = yield* makeMarkerExecutors();
1675 1595
1676 yield* execOuter.marker.register("shared", "outer-name");1596 yield* execOuter.marker.register("shared", "outer-name");
1677 yield* execInner.marker.register("shared", "inner-name");1597 yield* execInner.marker.register("shared", "inner-name");
1678 1598
1679 const sources = yield* execInner.sources.list();1599 const sources = yield* execInner.sources.list();
1680 const shared = sources.filter((s) => s.id === "shared");1600 const shared = sources.filter((s) => s.id === "shared");
1681 expect(shared).toHaveLength(1);1601 expect(shared).toHaveLength(1);
1682 expect(shared[0]?.name).toBe("inner-name");1602 expect(shared[0]?.name).toBe("inner-name");
1683 }),1603 }),
1684 );1604 );
1685 1605
1686 it.effect(1606 it.effect("tools.definitions dedupes by (source_id, name), keeping the innermost row", () =>
1687 "tools.definitions dedupes by (source_id, name), keeping the innermost row",1607 Effect.gen(function* () {
1688 () =>1608 const { execOuter, execInner } = yield* makeLayeredExecutors();
1689 Effect.gen(function* () {1609
1690 const { execOuter, execInner } = yield* makeLayeredExecutors();1610 // Register inner first, outer second. Without precedence-aware
1691 1611 // dedup, a naive "iterate rows, last-one-wins" map would end up
1692 // Register inner first, outer second. Without precedence-aware1612 // keyed to the outer description just because outer was inserted
1693 // dedup, a naive "iterate rows, last-one-wins" map would end up1613 // into the store last.
1694 // keyed to the outer description just because outer was inserted1614 yield* execInner["tenant-defs"].addDefs("S", {
1695 // into the store last.1615 Shared: { type: "string", description: "inner" },
1696 yield* execInner["tenant-defs"].addDefs("S", {1616 });
1697 Shared: { type: "string", description: "inner" },1617 yield* execOuter["tenant-defs"].addDefs("S", {
1698 });1618 Shared: { type: "string", description: "outer" },
1699 yield* execOuter["tenant-defs"].addDefs("S", {1619 });
1700 Shared: { type: "string", description: "outer" },
1701 });
1702 1620
1703 const defs = yield* execInner.tools.definitions();1621 const defs = yield* execInner.tools.definitions();
1704 const shared = defs.S?.Shared as { description?: string } | undefined;1622 const shared = defs.S?.Shared as { description?: string } | undefined;
1705 expect(shared?.description).toBe("inner");1623 expect(shared?.description).toBe("inner");
1706 }),1624 }),
1707 );1625 );
1708 1626
1709 it.effect(1627 it.effect("tools.schema attaches innermost $defs when shadowed across scopes", () =>
1710 "tools.schema attaches innermost $defs when shadowed across scopes",1628 Effect.gen(function* () {
1711 () =>1629 // Source id "S" with a tool that references $defs/Shared, plus a
1712 Effect.gen(function* () {1630 // definition "Shared" registered at both scopes. Schema's attached
1713 // Source id "S" with a tool that references $defs/Shared, plus a1631 // $defs should come from the inner scope.
1714 // definition "Shared" registered at both scopes. Schema's attached1632 const plugins = [
1715 // $defs should come from the inner scope.1633 tenantPlugin(),
1716 const plugins = [1634 tenantPluginWithDefs(),
1717 tenantPlugin(),1635 definePlugin(() => ({
1718 tenantPluginWithDefs(),1636 id: "ref" as const,
1719 definePlugin(() => ({1637 storage: () => ({}),
1720 id: "ref" as const,1638 extension: (ctx) => ({
1721 storage: () => ({}),1639 register: (sourceId: string) =>
1722 extension: (ctx) => ({1640 ctx.transaction(
1723 register: (sourceId: string) =>1641 ctx.core.sources.register({
1724 ctx.transaction(1642 id: sourceId,
1725 ctx.core.sources.register({1643 scope: ctx.scopes[0]!.id,
1726 id: sourceId,1644 kind: "ref",
1727 scope: ctx.scopes[0]!.id,1645 name: sourceId,
1728 kind: "ref",1646 canRemove: true,
1729 name: sourceId,1647 tools: [
1730 canRemove: true,1648 {
1731 tools: [1649 name: "use",
1732 {1650 description: "uses $defs/Shared",
1733 name: "use",1651 inputSchema: {
1734 description: "uses $defs/Shared",1652 type: "object",
1735 inputSchema: {1653 properties: { x: { $ref: "#/$defs/Shared" } },
1736 type: "object",
1737 properties: { x: { $ref: "#/$defs/Shared" } },
1738 },
1739 },1654 },
1740 ],1655 },
1741 }),1656 ],
1742 ),1657 }),
1743 }),1658 ),
1744 }))(),1659 }),
1745 ] as const;1660 }))(),
1746 const schema = collectSchemas(plugins);1661 ] as const;
1747 const adapter = makeMemoryAdapter({ schema });1662 const schema = collectSchemas(plugins);
1748 const blobs = makeInMemoryBlobStore();1663 const adapter = makeMemoryAdapter({ schema });
1749 1664 const blobs = makeInMemoryBlobStore();
1750 const outerId = ScopeId.make("org");
1751 const innerId = ScopeId.make("user-org:u1:org");
1752 const execOuter = yield* createExecutor({
1753 scopes: [
1754 new Scope({ id: outerId, name: "outer", createdAt: new Date() }),
1755 ],
1756 adapter,
1757 blobs,
1758 plugins,
1759 onElicitation: "accept-all",
1760 });
1761 const execInner = yield* createExecutor({
1762 scopes: [
1763 new Scope({ id: innerId, name: "inner", createdAt: new Date() }),
1764 new Scope({ id: outerId, name: "outer", createdAt: new Date() }),
1765 ],
1766 adapter,
1767 blobs,
1768 plugins,
1769 onElicitation: "accept-all",
1770 });
1771 1665
1772 yield* execOuter.ref.register("S");1666 const outerId = ScopeId.make("org");
1773 yield* execInner.ref.register("S");1667 const innerId = ScopeId.make("user-org:u1:org");
1668 const execOuter = yield* createExecutor({
1669 scopes: [new Scope({ id: outerId, name: "outer", createdAt: new Date() })],
1670 adapter,
1671 blobs,
1672 plugins,
1673 onElicitation: "accept-all",
1674 });
1675 const execInner = yield* createExecutor({
1676 scopes: [
1677 new Scope({ id: innerId, name: "inner", createdAt: new Date() }),
1678 new Scope({ id: outerId, name: "outer", createdAt: new Date() }),
1679 ],
1680 adapter,
1681 blobs,
1682 plugins,
1683 onElicitation: "accept-all",
1684 });
1774 1685
1775 yield* execInner["tenant-defs"].addDefs("S", {1686 yield* execOuter.ref.register("S");
1776 Shared: { type: "string", description: "inner" },1687 yield* execInner.ref.register("S");
1777 });
1778 yield* execOuter["tenant-defs"].addDefs("S", {
1779 Shared: { type: "string", description: "outer" },
1780 });
1781 1688
1782 const view = yield* execInner.tools.schema("S.use");1689 yield* execInner["tenant-defs"].addDefs("S", {
1783 const input = view?.inputSchema as1690 Shared: { type: "string", description: "inner" },
1784 | { $defs?: { Shared?: { description?: string } } }1691 });
1785 | undefined;1692 yield* execOuter["tenant-defs"].addDefs("S", {
1786 expect(input?.$defs?.Shared?.description).toBe("inner");1693 Shared: { type: "string", description: "outer" },
1787 }),1694 });
1695
1696 const view = yield* execInner.tools.schema("S.use");
1697 const input = view?.inputSchema as
1698 | { $defs?: { Shared?: { description?: string } } }
1699 | undefined;
1700 expect(input?.$defs?.Shared?.description).toBe("inner");
1701 }),
1788 );1702 );
1789});1703});
packages/core/sdk/src/index.ts+1−5
@@ -121,11 +121,7 @@ export { · 120 unmodified lines
121} from "./secret-backed-value";121} from "./secret-backed-value";
122 122
123// Usage tracking — secret/connection refs across plugins123// Usage tracking — secret/connection refs across plugins
124export {124export { Usage, type UsagesForSecretInput, type UsagesForConnectionInput } from "./usages";
125 Usage,
126 type UsagesForSecretInput,
127 type UsagesForConnectionInput,
128} from "./usages";
129 125
130// Connections126// Connections
131export {127export {
packages/core/sdk/src/oauth-discovery.test.ts+34−66
@@ -15,23 +15,19 @@ const DcrRequestBody = Schema.Struct({ · 14 unmodified lines
15 redirect_uris: Schema.Array(Schema.String),15 redirect_uris: Schema.Array(Schema.String),
16 token_endpoint_auth_method: Schema.String,16 token_endpoint_auth_method: Schema.String,
17});17});
18const decodeDcrRequestBody = Schema.decodeUnknownSync(18const decodeDcrRequestBody = Schema.decodeUnknownSync(Schema.fromJsonString(DcrRequestBody));
19 Schema.fromJsonString(DcrRequestBody),
20);
21 19
22const installFetchRouter = (20const installFetchRouter = (
23 handlers: readonly { match: (url: string) => boolean; handle: Handler }[],21 handlers: readonly { match: (url: string) => boolean; handle: Handler }[],
24): { calls: Array<{ url: string; init: RequestInit }> } => {22): { calls: Array<{ url: string; init: RequestInit }> } => {
25 const calls: Array<{ url: string; init: RequestInit }> = [];23 const calls: Array<{ url: string; init: RequestInit }> = [];
26 globalThis.fetch = vi24 globalThis.fetch = vi.fn().mockImplementation(async (url: string, init: RequestInit = {}) => {
27 .fn()25 calls.push({ url, init });
28 .mockImplementation(async (url: string, init: RequestInit = {}) => {26 for (const h of handlers) {
29 calls.push({ url, init });27 if (h.match(url)) return h.handle(url, init);
30 for (const h of handlers) {28 }
31 if (h.match(url)) return h.handle(url, init);29 return new Response(null, { status: 404 });
32 }30 }) as typeof fetch;
33 return new Response(null, { status: 404 });
34 }) as typeof fetch;
35 return { calls };31 return { calls };
36};32};
37 33
@@ -45,14 +41,11 @@ describe("discoverProtectedResourceMetadata", () => { · 7 unmodified lines
45 it("fetches RFC 9728 well-known metadata on the resource's origin", async () => {41 it("fetches RFC 9728 well-known metadata on the resource's origin", async () => {
46 installFetchRouter([42 installFetchRouter([
47 {43 {
48 match: (u) =>44 match: (u) => u === "https://api.example.com/.well-known/oauth-protected-resource/graphql",
49 u ===
50 "https://api.example.com/.well-known/oauth-protected-resource/graphql",
51 handle: () => new Response(null, { status: 404 }),45 handle: () => new Response(null, { status: 404 }),
52 },46 },
53 {47 {
54 match: (u) =>48 match: (u) => u === "https://api.example.com/.well-known/oauth-protected-resource",
55 u === "https://api.example.com/.well-known/oauth-protected-resource",
56 handle: () =>49 handle: () =>
57 new Response(50 new Response(
58 JSON.stringify({51 JSON.stringify({
@@ -69,18 +62,14 @@ describe("discoverProtectedResourceMetadata", () => { · 10 unmodified lines
69 discoverProtectedResourceMetadata("https://api.example.com/graphql"),62 discoverProtectedResourceMetadata("https://api.example.com/graphql"),
70 );63 );
71 expect(result).not.toBeNull();64 expect(result).not.toBeNull();
72 expect(result!.metadata.authorization_servers?.[0]).toBe(65 expect(result!.metadata.authorization_servers?.[0]).toBe("https://api.example.com");
73 "https://api.example.com",
74 );
75 expect(result!.metadataUrl).toBe(66 expect(result!.metadataUrl).toBe(
76 "https://api.example.com/.well-known/oauth-protected-resource",67 "https://api.example.com/.well-known/oauth-protected-resource",
77 );68 );
78 });69 });
79 70
80 it("returns null when every well-known candidate 404s", async () => {71 it("returns null when every well-known candidate 404s", async () => {
81 installFetchRouter([72 installFetchRouter([{ match: () => true, handle: () => new Response(null, { status: 404 }) }]);
82 { match: () => true, handle: () => new Response(null, { status: 404 }) },
83 ]);
84 73
85 const result = await Effect.runPromise(74 const result = await Effect.runPromise(
86 discoverProtectedResourceMetadata("https://api.example.com/graphql"),75 discoverProtectedResourceMetadata("https://api.example.com/graphql"),
@@ -117,13 +106,11 @@ describe("discoverAuthorizationServerMetadata", () => { · 30 unmodified lines
117 it("falls back to openid-configuration when oauth-authorization-server is absent", async () => {106 it("falls back to openid-configuration when oauth-authorization-server is absent", async () => {
118 installFetchRouter([107 installFetchRouter([
119 {108 {
120 match: (u) =>109 match: (u) => u === "https://as.example.com/.well-known/oauth-authorization-server",
121 u === "https://as.example.com/.well-known/oauth-authorization-server",
122 handle: () => new Response(null, { status: 404 }),110 handle: () => new Response(null, { status: 404 }),
123 },111 },
124 {112 {
125 match: (u) =>113 match: (u) => u === "https://as.example.com/.well-known/openid-configuration",
126 u === "https://as.example.com/.well-known/openid-configuration",
127 handle: () =>114 handle: () =>
128 new Response(115 new Response(
129 JSON.stringify({116 JSON.stringify({
@@ -142,9 +129,7 @@ describe("discoverAuthorizationServerMetadata", () => { · 12 unmodified lines
142 discoverAuthorizationServerMetadata("https://as.example.com"),129 discoverAuthorizationServerMetadata("https://as.example.com"),
143 );130 );
144 expect(result).not.toBeNull();131 expect(result).not.toBeNull();
145 expect(result!.metadata.token_endpoint).toBe(132 expect(result!.metadata.token_endpoint).toBe("https://as.example.com/token");
146 "https://as.example.com/token",
147 );
148 expect(result!.metadataUrl.endsWith("openid-configuration")).toBe(true);133 expect(result!.metadataUrl.endsWith("openid-configuration")).toBe(true);
149 });134 });
150 135
@@ -159,9 +144,7 @@ describe("discoverAuthorizationServerMetadata", () => { · 8 unmodified lines
159 }),144 }),
160 },145 },
161 ]);146 ]);
162 const exit = await Effect.runPromiseExit(147 const exit = await Effect.runPromiseExit(discoverAuthorizationServerMetadata("https://as"));
163 discoverAuthorizationServerMetadata("https://as"),
164 );
165 expect(Exit.isFailure(exit)).toBe(true);148 expect(Exit.isFailure(exit)).toBe(true);
166 });149 });
167});150});
@@ -260,11 +243,13 @@ describe("registerDynamicClient", () => { · 92 unmodified lines
260 if (!Exit.isFailure(exit)) return;243 if (!Exit.isFailure(exit)) return;
261 const reason = exit.cause.reasons.find(Cause.isFailReason);244 const reason = exit.cause.reasons.find(Cause.isFailReason);
262 const error = reason?.error;245 const error = reason?.error;
263 expect(error).toEqual(expect.objectContaining({246 expect(error).toEqual(
264 _tag: "OAuthDiscoveryError",247 expect.objectContaining({
265 status: 400,248 _tag: "OAuthDiscoveryError",
266 message: expect.stringMatching(/invalid_client_metadata/),249 status: 400,
267 }));250 message: expect.stringMatching(/invalid_client_metadata/),
251 }),
252 );
268 });253 });
269});254});
270 255
@@ -279,14 +264,11 @@ describe("beginDynamicAuthorization", () => { · 8 unmodified lines
279 installFetchRouter([264 installFetchRouter([
280 {265 {
281 match: (u) =>266 match: (u) =>
282 u ===267 u === "https://backboard.railway.com/.well-known/oauth-protected-resource/graphql/v2",
283 "https://backboard.railway.com/.well-known/oauth-protected-resource/graphql/v2",
284 handle: () => new Response(null, { status: 404 }),268 handle: () => new Response(null, { status: 404 }),
285 },269 },
286 {270 {
287 match: (u) =>271 match: (u) => u === "https://backboard.railway.com/.well-known/oauth-protected-resource",
288 u ===
289 "https://backboard.railway.com/.well-known/oauth-protected-resource",
290 handle: () =>272 handle: () =>
291 new Response(273 new Response(
292 JSON.stringify({274 JSON.stringify({
@@ -304,18 +286,14 @@ describe("beginDynamicAuthorization", () => { · 11 unmodified lines
304 ),286 ),
305 },287 },
306 {288 {
307 match: (u) =>289 match: (u) => u === "https://backboard.railway.com/.well-known/oauth-authorization-server",
308 u ===
309 "https://backboard.railway.com/.well-known/oauth-authorization-server",
310 handle: () =>290 handle: () =>
311 new Response(291 new Response(
312 JSON.stringify({292 JSON.stringify({
313 issuer: "https://backboard.railway.com",293 issuer: "https://backboard.railway.com",
314 authorization_endpoint:294 authorization_endpoint: "https://backboard.railway.com/oauth/auth",
315 "https://backboard.railway.com/oauth/auth",
316 token_endpoint: "https://backboard.railway.com/oauth/token",295 token_endpoint: "https://backboard.railway.com/oauth/token",
317 registration_endpoint:296 registration_endpoint: "https://backboard.railway.com/oauth/register",
318 "https://backboard.railway.com/oauth/register",
319 scopes_supported: [297 scopes_supported: [
320 "openid",298 "openid",
321 "profile",299 "profile",
@@ -356,27 +334,19 @@ describe("beginDynamicAuthorization", () => { · 34 unmodified lines
356 );334 );
357 335
358 const url = new URL(result.authorizationUrl);336 const url = new URL(result.authorizationUrl);
359 expect(url.origin + url.pathname).toBe(337 expect(url.origin + url.pathname).toBe("https://backboard.railway.com/oauth/auth");
360 "https://backboard.railway.com/oauth/auth",
361 );
362 expect(url.searchParams.get("client_id")).toBe("dyn-client-42");338 expect(url.searchParams.get("client_id")).toBe("dyn-client-42");
363 expect(url.searchParams.get("redirect_uri")).toBe(339 expect(url.searchParams.get("redirect_uri")).toBe("https://app.example/cb");
364 "https://app.example/cb",
365 );
366 expect(url.searchParams.get("response_type")).toBe("code");340 expect(url.searchParams.get("response_type")).toBe("code");
367 expect(url.searchParams.get("code_challenge_method")).toBe("S256");341 expect(url.searchParams.get("code_challenge_method")).toBe("S256");
368 expect(url.searchParams.get("state")).toBe("state-xyz");342 expect(url.searchParams.get("state")).toBe("state-xyz");
369 expect(result.codeVerifier.length).toBeGreaterThanOrEqual(43);343 expect(result.codeVerifier.length).toBeGreaterThanOrEqual(43);
370 expect(result.state.authorizationServerUrl).toBe(344 expect(result.state.authorizationServerUrl).toBe("https://backboard.railway.com");
371 "https://backboard.railway.com",
372 );
373 expect(result.state.authorizationServerMetadata.token_endpoint).toBe(345 expect(result.state.authorizationServerMetadata.token_endpoint).toBe(
374 "https://backboard.railway.com/oauth/token",346 "https://backboard.railway.com/oauth/token",
375 );347 );
376 expect(result.state.clientInformation.client_id).toBe("dyn-client-42");348 expect(result.state.clientInformation.client_id).toBe("dyn-client-42");
377 expect(result.state.resourceMetadata?.resource).toBe(349 expect(result.state.resourceMetadata?.resource).toBe("https://backboard.railway.com");
378 "https://backboard.railway.com",
379 );
380 });350 });
381 351
382 it("skips discovery + DCR when previousState is provided", async () => {352 it("skips discovery + DCR when previousState is provided", async () => {
@@ -422,8 +392,7 @@ describe("beginDynamicAuthorization", () => { · 39 unmodified lines
422 new Response(392 new Response(
423 JSON.stringify({393 JSON.stringify({
424 issuer: "https://legacy.example.com",394 issuer: "https://legacy.example.com",
425 authorization_endpoint:395 authorization_endpoint: "https://legacy.example.com/authorize",
426 "https://legacy.example.com/authorize",
427 token_endpoint: "https://legacy.example.com/token",396 token_endpoint: "https://legacy.example.com/token",
428 code_challenge_methods_supported: ["plain"],397 code_challenge_methods_supported: ["plain"],
429 response_types_supported: ["code"],398 response_types_supported: ["code"],
@@ -454,8 +423,7 @@ describe("beginDynamicAuthorization", () => { · 24 unmodified lines
454 new Response(423 new Response(
455 JSON.stringify({424 JSON.stringify({
456 issuer: "https://static.example.com",425 issuer: "https://static.example.com",
457 authorization_endpoint:426 authorization_endpoint: "https://static.example.com/authorize",
458 "https://static.example.com/authorize",
459 token_endpoint: "https://static.example.com/token",427 token_endpoint: "https://static.example.com/token",
460 code_challenge_methods_supported: ["S256"],428 code_challenge_methods_supported: ["S256"],
461 response_types_supported: ["code"],429 response_types_supported: ["code"],
packages/core/sdk/src/oauth-helpers.test.ts+10−11
@@ -109,7 +109,10 @@ describe("buildAuthorizationUrl", () => { · 108 unmodified lines
109 109
110 it("preserves pre-existing query params on the authorization URL", () => {110 it("preserves pre-existing query params on the authorization URL", () => {
111 const url = new URL(111 const url = new URL(
112 buildAuthorizationUrl({ ...baseInput, authorizationUrl: "https://example.com/auth?tenant=acme" }),112 buildAuthorizationUrl({
113 ...baseInput,
114 authorizationUrl: "https://example.com/auth?tenant=acme",
115 }),
113 );116 );
114 expect(url.searchParams.get("tenant")).toBe("acme");117 expect(url.searchParams.get("tenant")).toBe("acme");
115 expect(url.searchParams.get("client_id")).toBe("client-123");118 expect(url.searchParams.get("client_id")).toBe("client-123");
@@ -124,12 +127,10 @@ type FetchArgs = { url: string; init: RequestInit }; · 8 unmodified lines
124 127
125const captureFetch = (response: Response): { calls: FetchArgs[] } => {128const captureFetch = (response: Response): { calls: FetchArgs[] } => {
126 const calls: FetchArgs[] = [];129 const calls: FetchArgs[] = [];
127 globalThis.fetch = vi130 globalThis.fetch = vi.fn().mockImplementation(async (url: string, init: RequestInit) => {
128 .fn()131 calls.push({ url, init });
129 .mockImplementation(async (url: string, init: RequestInit) => {132 return response;
130 calls.push({ url, init });133 }) as typeof fetch;
131 return response;
132 }) as typeof fetch;
133 return { calls };134 return { calls };
134};135};
135 136
@@ -138,10 +139,8 @@ const originalFetch = globalThis.fetch; · 2 unmodified lines
138const jwtPart = (value: unknown): string =>139const jwtPart = (value: unknown): string =>
139 Buffer.from(JSON.stringify(value)).toString("base64url");140 Buffer.from(JSON.stringify(value)).toString("base64url");
140 141
141const unsignedJwt = (142const unsignedJwt = (claims: Record<string, unknown>, alg = "RS256"): string =>
142 claims: Record<string, unknown>,143 `${jwtPart({ alg, typ: "JWT" })}.${jwtPart(claims)}.sig`;
143 alg = "RS256",
144): string => `${jwtPart({ alg, typ: "JWT" })}.${jwtPart(claims)}.sig`;
145 144
146describe("exchangeAuthorizationCode", () => {145describe("exchangeAuthorizationCode", () => {
147 afterEach(() => {146 afterEach(() => {
packages/core/sdk/src/oauth-helpers.ts+19−46
@@ -61,8 +61,7 @@ export const OAUTH2_DEFAULT_TIMEOUT_MS = 20_000; · 60 unmodified lines
61// PKCE (RFC 7636) — straight delegation to `oauth4webapi`61// PKCE (RFC 7636) — straight delegation to `oauth4webapi`
62// ---------------------------------------------------------------------------62// ---------------------------------------------------------------------------
63 63
64export const createPkceCodeVerifier = (): string =>64export const createPkceCodeVerifier = (): string => oauth.generateRandomCodeVerifier();
65 oauth.generateRandomCodeVerifier();
66 65
67export const createPkceCodeChallenge = (verifier: string): Promise<string> =>66export const createPkceCodeChallenge = (verifier: string): Promise<string> =>
68 oauth.calculatePKCECodeChallenge(verifier);67 oauth.calculatePKCECodeChallenge(verifier);
@@ -165,9 +164,7 @@ const asFromTokenUrlAndIssuer = ( · 96 unmodified lines
165 return options.idTokenSigningAlgValuesSupported164 return options.idTokenSigningAlgValuesSupported
166 ? {165 ? {
167 ...withIssuer,166 ...withIssuer,
168 id_token_signing_alg_values_supported: [167 id_token_signing_alg_values_supported: [...options.idTokenSigningAlgValuesSupported],
169 ...options.idTokenSigningAlgValuesSupported,
170 ],
171 }168 }
172 : withIssuer;169 : withIssuer;
173};170};
@@ -194,9 +191,7 @@ const oauth4webapiRequestOptions = ( · 20 unmodified lines
194 signal: AbortSignal.timeout(timeoutMs ?? OAUTH2_DEFAULT_TIMEOUT_MS),191 signal: AbortSignal.timeout(timeoutMs ?? OAUTH2_DEFAULT_TIMEOUT_MS),
195 };192 };
196 if (isLoopbackHttpUrl(targetUrl)) {193 if (isLoopbackHttpUrl(targetUrl)) {
197 (options as { [oauth.allowInsecureRequests]?: boolean })[194 (options as { [oauth.allowInsecureRequests]?: boolean })[oauth.allowInsecureRequests] = true;
198 oauth.allowInsecureRequests
199 ] = true;
200 }195 }
201 return options;196 return options;
202};197};
@@ -211,9 +206,7 @@ const pickClientAuth = ( · 8 unmodified lines
211 : oauth.ClientSecretPost(clientSecret);206 : oauth.ClientSecretPost(clientSecret);
212};207};
213 208
214const tokenResponseFrom = (209const tokenResponseFrom = (r: oauth.TokenEndpointResponse): OAuth2TokenResponse => ({
215 r: oauth.TokenEndpointResponse,
216): OAuth2TokenResponse => ({
217 access_token: r.access_token,210 access_token: r.access_token,
218 token_type: r.token_type,211 token_type: r.token_type,
219 refresh_token: r.refresh_token,212 refresh_token: r.refresh_token,
@@ -227,15 +220,14 @@ const tokenResponseFrom = ( · 7 unmodified lines
227// its claims against the AS metadata and rejects mismatches we don't care220// its claims against the AS metadata and rejects mismatches we don't care
228// about. Strip the field before delegation.221// about. Strip the field before delegation.
229const stripIdToken = async (response: Response): Promise<Response> => {222const stripIdToken = async (response: Response): Promise<Response> => {
230 const body = await response.clone().json().then(223 const body = await response
231 (value: unknown) => value,224 .clone()
232 () => null,225 .json()
233 );226 .then(
234 if (227 (value: unknown) => value,
235 !body ||228 () => null,
236 typeof body !== "object" ||229 );
237 !("id_token" in (body as Record<string, unknown>))230 if (!body || typeof body !== "object" || !("id_token" in (body as Record<string, unknown>))) {
238 ) {
239 return response;231 return response;
240 }232 }
241 const { id_token: _ignored, ...rest } = body as Record<string, unknown>;233 const { id_token: _ignored, ...rest } = body as Record<string, unknown>;
@@ -252,11 +244,7 @@ const processTokenEndpointResponse = async ( · 10 unmodified lines
252 response: Response,244 response: Response,
253): Promise<OAuth2TokenResponse> =>245): Promise<OAuth2TokenResponse> =>
254 tokenResponseFrom(246 tokenResponseFrom(
255 await oauth.processGenericTokenEndpointResponse(247 await oauth.processGenericTokenEndpointResponse(as, client, await stripIdToken(response)),
256 as,
257 client,
258 await stripIdToken(response),
259 ),
260 );248 );
261 249
262// ---------------------------------------------------------------------------250// ---------------------------------------------------------------------------
@@ -282,14 +270,10 @@ export const exchangeAuthorizationCode = ( · 19 unmodified lines
282 Effect.tryPromise({270 Effect.tryPromise({
283 try: async () => {271 try: async () => {
284 const as = asFromTokenUrlAndIssuer(input.tokenUrl, input.issuerUrl, {272 const as = asFromTokenUrlAndIssuer(input.tokenUrl, input.issuerUrl, {
285 idTokenSigningAlgValuesSupported:273 idTokenSigningAlgValuesSupported: input.idTokenSigningAlgValuesSupported,
286 input.idTokenSigningAlgValuesSupported,
287 });274 });
288 const client: oauth.Client = { client_id: input.clientId };275 const client: oauth.Client = { client_id: input.clientId };
289 const clientAuth = pickClientAuth(276 const clientAuth = pickClientAuth(input.clientSecret, input.clientAuth ?? "body");
290 input.clientSecret,
291 input.clientAuth ?? "body",
292 );
293 // `authorizationCodeGrantRequest` requires its `callbackParameters`277 // `authorizationCodeGrantRequest` requires its `callbackParameters`
294 // to have been returned from `validateAuthResponse`. Our public API278 // to have been returned from `validateAuthResponse`. Our public API
295 // takes the `code` directly (the UI already validated `state` by279 // takes the `code` directly (the UI already validated `state` by
@@ -334,10 +318,7 @@ export const exchangeClientCredentials = ( · 38 unmodified lines
334 try: async () => {318 try: async () => {
335 const as = asFromTokenUrl(input.tokenUrl);319 const as = asFromTokenUrl(input.tokenUrl);
336 const client: oauth.Client = { client_id: input.clientId };320 const client: oauth.Client = { client_id: input.clientId };
337 const clientAuth = pickClientAuth(321 const clientAuth = pickClientAuth(input.clientSecret, input.clientAuth ?? "body");
338 input.clientSecret,
339 input.clientAuth ?? "body",
340 );
341 const params = new URLSearchParams();322 const params = new URLSearchParams();
342 if (input.scopes && input.scopes.length > 0) {323 if (input.scopes && input.scopes.length > 0) {
343 params.set("scope", input.scopes.join(input.scopeSeparator ?? " "));324 params.set("scope", input.scopes.join(input.scopeSeparator ?? " "));
@@ -349,11 +330,7 @@ export const exchangeClientCredentials = ( · 5 unmodified lines
349 params,330 params,
350 oauth4webapiRequestOptions(input.tokenUrl, input.timeoutMs),331 oauth4webapiRequestOptions(input.tokenUrl, input.timeoutMs),
351 );332 );
352 const result = await oauth.processClientCredentialsResponse(333 const result = await oauth.processClientCredentialsResponse(as, client, response);
353 as,
354 client,
355 response,
356 );
357 return tokenResponseFrom(result);334 return tokenResponseFrom(result);
358 },335 },
359 catch: toOAuth2Error,336 catch: toOAuth2Error,
@@ -382,14 +359,10 @@ export const refreshAccessToken = ( · 22 unmodified lines
382 Effect.tryPromise({359 Effect.tryPromise({
383 try: async () => {360 try: async () => {
384 const as = asFromTokenUrlAndIssuer(input.tokenUrl, input.issuerUrl, {361 const as = asFromTokenUrlAndIssuer(input.tokenUrl, input.issuerUrl, {
385 idTokenSigningAlgValuesSupported:362 idTokenSigningAlgValuesSupported: input.idTokenSigningAlgValuesSupported,
386 input.idTokenSigningAlgValuesSupported,
387 });363 });
388 const client: oauth.Client = { client_id: input.clientId };364 const client: oauth.Client = { client_id: input.clientId };
389 const clientAuth = pickClientAuth(365 const clientAuth = pickClientAuth(input.clientSecret, input.clientAuth ?? "body");
390 input.clientSecret,
391 input.clientAuth ?? "body",
392 );
393 const additionalParameters =366 const additionalParameters =
394 input.scopes && input.scopes.length > 0367 input.scopes && input.scopes.length > 0
395 ? new URLSearchParams({368 ? new URLSearchParams({
packages/core/sdk/src/oauth-popup-types.ts+1−3
@@ -22,9 +22,7 @@ export type OAuthPopupResult<TAuth> = · 21 unmodified lines
22 readonly error: string;22 readonly error: string;
23 };23 };
24 24
25export const isOAuthPopupResult = <TAuth>(25export const isOAuthPopupResult = <TAuth>(value: unknown): value is OAuthPopupResult<TAuth> =>
26 value: unknown,
27): value is OAuthPopupResult<TAuth> =>
28 typeof value === "object" &&26 typeof value === "object" &&
29 value !== null &&27 value !== null &&
30 (value as { type?: unknown }).type === OAUTH_POPUP_MESSAGE_TYPE;28 (value as { type?: unknown }).type === OAUTH_POPUP_MESSAGE_TYPE;
packages/core/sdk/src/oauth.ts+12−29
@@ -69,15 +69,12 @@ export const OAuthAuthorizationCodeStrategy = Schema.Struct({ · 68 unmodified lines
69 scopeSeparator: Schema.optional(Schema.String),69 scopeSeparator: Schema.optional(Schema.String),
70 /** Provider-specific params injected at authorization URL build time70 /** Provider-specific params injected at authorization URL build time
71 * (Google's `access_type=offline`, `prompt=consent`, ...). */71 * (Google's `access_type=offline`, `prompt=consent`, ...). */
72 extraAuthorizationParams: Schema.optional(72 extraAuthorizationParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),
73 Schema.Record(Schema.String, Schema.String),
74 ),
75 /** `"body"` (default) sends client creds in the form body; `"basic"`73 /** `"body"` (default) sends client creds in the form body; `"basic"`
76 * uses HTTP Basic auth. Stripe-style servers require basic. */74 * uses HTTP Basic auth. Stripe-style servers require basic. */
77 clientAuth: Schema.optional(Schema.Literals(["body", "basic"])),75 clientAuth: Schema.optional(Schema.Literals(["body", "basic"])),
78});76});
79export type OAuthAuthorizationCodeStrategy =77export type OAuthAuthorizationCodeStrategy = typeof OAuthAuthorizationCodeStrategy.Type;
80 typeof OAuthAuthorizationCodeStrategy.Type;
81 78
82/** RFC 6749 §4.4 client credentials — no user redirect, no PKCE. Used79/** RFC 6749 §4.4 client credentials — no user redirect, no PKCE. Used
83 * for server-to-server integrations where the plugin has both80 * for server-to-server integrations where the plugin has both
@@ -92,8 +89,7 @@ export const OAuthClientCredentialsStrategy = Schema.Struct({ · 8 unmodified lines
92 scopeSeparator: Schema.optional(Schema.String),89 scopeSeparator: Schema.optional(Schema.String),
93 clientAuth: Schema.optional(Schema.Literals(["body", "basic"])),90 clientAuth: Schema.optional(Schema.Literals(["body", "basic"])),
94});91});
95export type OAuthClientCredentialsStrategy =92export type OAuthClientCredentialsStrategy = typeof OAuthClientCredentialsStrategy.Type;
96 typeof OAuthClientCredentialsStrategy.Type;
97 93
98/** Tagged union of every start-time strategy shape. A new strategy (e.g.94/** Tagged union of every start-time strategy shape. A new strategy (e.g.
99 * device-code) is added here; the service's start/complete routes on95 * device-code) is added here; the service's start/complete routes on
@@ -121,9 +117,7 @@ export const OAuthProviderState = Schema.Union([ · 21 unmodified lines
121 issuerUrl: Schema.optional(Schema.NullOr(Schema.String)),117 issuerUrl: Schema.optional(Schema.NullOr(Schema.String)),
122 authorizationServerUrl: Schema.optional(Schema.NullOr(Schema.String)),118 authorizationServerUrl: Schema.optional(Schema.NullOr(Schema.String)),
123 authorizationServerMetadataUrl: Schema.NullOr(Schema.String),119 authorizationServerMetadataUrl: Schema.NullOr(Schema.String),
124 idTokenSigningAlgValuesSupported: Schema.optional(120 idTokenSigningAlgValuesSupported: Schema.optional(Schema.Array(Schema.String)),
125 Schema.Array(Schema.String),
126 ),
127 /** DCR-minted client_id. Embedded inline (not a secret) — DCR121 /** DCR-minted client_id. Embedded inline (not a secret) — DCR
128 * clients are public-ish by design; the secret part (if the AS122 * clients are public-ish by design; the secret part (if the AS
129 * issued one) is a separate secret row. */123 * issued one) is a separate secret row. */
@@ -266,21 +260,15 @@ export interface OAuthCompleteResult { · 136 unmodified lines
266// capable plugin group `.addError(OAuthStartError)` etc. and the HTTP260// capable plugin group `.addError(OAuthStartError)` etc. and the HTTP
267// edge renders them with the annotated status.261// edge renders them with the annotated status.
268 262
269export class OAuthProbeError extends Schema.TaggedErrorClass<OAuthProbeError>()(263export class OAuthProbeError extends Schema.TaggedErrorClass<OAuthProbeError>()("OAuthProbeError", {
270 "OAuthProbeError",264 message: Schema.String,
271 {265}) {
272 message: Schema.String,
273 },
274) {
275 static annotations = { httpApiStatus: 400 };266 static annotations = { httpApiStatus: 400 };
276}267}
277 268
278export class OAuthStartError extends Schema.TaggedErrorClass<OAuthStartError>()(269export class OAuthStartError extends Schema.TaggedErrorClass<OAuthStartError>()("OAuthStartError", {
279 "OAuthStartError",270 message: Schema.String,
280 {271}) {
281 message: Schema.String,
282 },
283) {
284 static annotations = { httpApiStatus: 400 };272 static annotations = { httpApiStatus: 400 };
285}273}
286 274
@@ -312,9 +300,7 @@ export class OAuthSessionNotFoundError extends Schema.TaggedErrorClass<OAuthSess · 25 unmodified lines
312// ---------------------------------------------------------------------------300// ---------------------------------------------------------------------------
313 301
314export interface OAuthService {302export interface OAuthService {
315 readonly probe: (303 readonly probe: (input: OAuthProbeInput) => Effect.Effect<OAuthProbeResult, OAuthProbeError>;
316 input: OAuthProbeInput,
317 ) => Effect.Effect<OAuthProbeResult, OAuthProbeError>;
318 readonly start: (304 readonly start: (
319 input: OAuthStartInput,305 input: OAuthStartInput,
320 ) => Effect.Effect<OAuthStartResult, OAuthStartError | StorageFailure>;306 ) => Effect.Effect<OAuthStartResult, OAuthStartError | StorageFailure>;
@@ -326,10 +312,7 @@ export interface OAuthService { · 5 unmodified lines
326 >;312 >;
327 /** Drop an in-flight session without completing — used when the313 /** Drop an in-flight session without completing — used when the
328 * user cancels the popup or the source is deleted mid-onboarding. */314 * user cancels the popup or the source is deleted mid-onboarding. */
329 readonly cancel: (315 readonly cancel: (sessionId: string, tokenScope?: string) => Effect.Effect<void, StorageFailure>;
330 sessionId: string,
331 tokenScope?: string,
332 ) => Effect.Effect<void, StorageFailure>;
333}316}
334 317
335// ---------------------------------------------------------------------------318// ---------------------------------------------------------------------------
packages/core/sdk/src/plugin.ts+25−105
@@ -1,11 +1,6 @@
1import type { Context, Effect, Layer } from "effect";1import type { Context, Effect, Layer } from "effect";
2import type { HttpApiGroup } from "effect/unstable/httpapi";2import type { HttpApiGroup } from "effect/unstable/httpapi";
3import type {3import type { DBAdapter, DBSchema, StorageFailure, TypedAdapter } from "@executor-js/storage-core";
4 DBAdapter,
5 DBSchema,
6 StorageFailure,
7 TypedAdapter,
8} from "@executor-js/storage-core";
9 4
10import type { PluginBlobStore } from "./blob";5import type { PluginBlobStore } from "./blob";
11import type {6import type {
@@ -15,12 +10,7 @@ import type { · 3 unmodified lines
15 CreateConnectionInput,10 CreateConnectionInput,
16 UpdateConnectionTokensInput,11 UpdateConnectionTokensInput,
17} from "./connections";12} from "./connections";
18import type {13import type { DefinitionsInput, SourceInput, ToolAnnotations, ToolRow } from "./core-schema";
19 DefinitionsInput,
20 SourceInput,
21 ToolAnnotations,
22 ToolRow,
23} from "./core-schema";
24import type { SourceDetectionResult } from "./types";14import type { SourceDetectionResult } from "./types";
25import type {15import type {
26 ElicitationDeclinedError,16 ElicitationDeclinedError,
@@ -70,9 +60,7 @@ export interface StorageDeps<TSchema extends DBSchema | undefined = undefined> { · 43 unmodified lines
70 * `@executor-js/api` `withCapture`) is the one place that60 * `@executor-js/api` `withCapture`) is the one place that
71 * translates it to the opaque `InternalError({ traceId })`.61 * translates it to the opaque `InternalError({ traceId })`.
72 */62 */
73 readonly adapter: TSchema extends DBSchema63 readonly adapter: TSchema extends DBSchema ? TypedAdapter<TSchema, StorageFailure> : DBAdapter;
74 ? TypedAdapter<TSchema, StorageFailure>
75 : DBAdapter;
76 readonly blobs: PluginBlobStore;64 readonly blobs: PluginBlobStore;
77}65}
78 66
@@ -114,12 +102,8 @@ export interface PluginCtx<TStore = unknown> { · 35 unmodified lines
114 102
115 readonly core: {103 readonly core: {
116 readonly sources: {104 readonly sources: {
117 readonly register: (105 readonly register: (input: SourceInput) => Effect.Effect<void, StorageFailure>;
118 input: SourceInput,106 readonly unregister: (sourceId: string) => Effect.Effect<void, StorageFailure>;
119 ) => Effect.Effect<void, StorageFailure>;
120 readonly unregister: (
121 sourceId: string,
122 ) => Effect.Effect<void, StorageFailure>;
123 readonly update: (input: {107 readonly update: (input: {
124 readonly id: string;108 readonly id: string;
125 readonly scope: string;109 readonly scope: string;
@@ -134,9 +118,7 @@ export interface PluginCtx<TStore = unknown> { · 8 unmodified lines
134 * same `ctx.transaction` as `sources.register` for atomicity.118 * same `ctx.transaction` as `sources.register` for atomicity.
135 * Replaces any existing defs for the given sourceId. */119 * Replaces any existing defs for the given sourceId. */
136 readonly definitions: {120 readonly definitions: {
137 readonly register: (121 readonly register: (input: DefinitionsInput) => Effect.Effect<void, StorageFailure>;
138 input: DefinitionsInput,
139 ) => Effect.Effect<void, StorageFailure>;
140 };122 };
141 };123 };
142 124
@@ -157,9 +139,7 @@ export interface PluginCtx<TStore = unknown> { · 14 unmodified lines
157 * `executor.secrets.set` on the host surface, but OAuth2 refresh139 * `executor.secrets.set` on the host surface, but OAuth2 refresh
158 * and one-shot token capture from plugin-owned flows need it here140 * and one-shot token capture from plugin-owned flows need it here
159 * too. Same routing rules as the host-level setter. */141 * too. Same routing rules as the host-level setter. */
160 readonly set: (142 readonly set: (input: SetSecretInput) => Effect.Effect<SecretRef, StorageFailure>;
161 input: SetSecretInput,
162 ) => Effect.Effect<SecretRef, StorageFailure>;
163 /** Delete a secret from its pinned provider and the core table.143 /** Delete a secret from its pinned provider and the core table.
164 * Rejects with `SecretOwnedByConnectionError` if the row is owned144 * Rejects with `SecretOwnedByConnectionError` if the row is owned
165 * by a connection — callers must go through `connections.remove`145 * by a connection — callers must go through `connections.remove`
@@ -168,10 +148,7 @@ export interface PluginCtx<TStore = unknown> { · 2 unmodified lines
168 * the user to detach the listed sources first. */148 * the user to detach the listed sources first. */
169 readonly remove: (149 readonly remove: (
170 id: string,150 id: string,
171 ) => Effect.Effect<151 ) => Effect.Effect<void, SecretOwnedByConnectionError | SecretInUseError | StorageFailure>;
172 void,
173 SecretOwnedByConnectionError | SecretInUseError | StorageFailure
174 >;
175 };152 };
176 153
177 /** Connections — product-level sign-in state. Owns backing secret154 /** Connections — product-level sign-in state. Owns backing secret
@@ -180,22 +157,14 @@ export interface PluginCtx<TStore = unknown> { · 2 unmodified lines
180 * fresh token (the SDK handles refresh via the registered provider157 * fresh token (the SDK handles refresh via the registered provider
181 * keyed by `connection.provider`). */158 * keyed by `connection.provider`). */
182 readonly connections: {159 readonly connections: {
183 readonly get: (160 readonly get: (id: string) => Effect.Effect<ConnectionRef | null, StorageFailure>;
184 id: string,
185 ) => Effect.Effect<ConnectionRef | null, StorageFailure>;
186 readonly list: () => Effect.Effect<readonly ConnectionRef[], StorageFailure>;161 readonly list: () => Effect.Effect<readonly ConnectionRef[], StorageFailure>;
187 readonly create: (162 readonly create: (
188 input: CreateConnectionInput,163 input: CreateConnectionInput,
189 ) => Effect.Effect<164 ) => Effect.Effect<ConnectionRef, ConnectionProviderNotRegisteredError | StorageFailure>;
190 ConnectionRef,
191 ConnectionProviderNotRegisteredError | StorageFailure
192 >;
193 readonly updateTokens: (165 readonly updateTokens: (
194 input: UpdateConnectionTokensInput,166 input: UpdateConnectionTokensInput,
195 ) => Effect.Effect<167 ) => Effect.Effect<ConnectionRef, ConnectionNotFoundError | StorageFailure>;
196 ConnectionRef,
197 ConnectionNotFoundError | StorageFailure
198 >;
199 readonly setIdentityLabel: (168 readonly setIdentityLabel: (
200 id: string,169 id: string,
201 label: string | null,170 label: string | null,
@@ -217,9 +186,7 @@ export interface PluginCtx<TStore = unknown> { · 15 unmodified lines
217 /** Refuses with `ConnectionInUseError` if any plugin reports the186 /** Refuses with `ConnectionInUseError` if any plugin reports the
218 * connection as in use. Caller surfaces the `usages` list to the187 * connection as in use. Caller surfaces the `usages` list to the
219 * user. */188 * user. */
220 readonly remove: (189 readonly remove: (id: string) => Effect.Effect<void, ConnectionInUseError | StorageFailure>;
221 id: string,
222 ) => Effect.Effect<void, ConnectionInUseError | StorageFailure>;
223 };190 };
224 191
225 /** Shared OAuth service. Plugins use this to probe/start/complete OAuth192 /** Shared OAuth service. Plugins use this to probe/start/complete OAuth
@@ -230,9 +197,7 @@ export interface PluginCtx<TStore = unknown> { · 4 unmodified lines
230 * adapter's transaction method. Use this in extension methods that197 * adapter's transaction method. Use this in extension methods that
231 * need atomicity across plugin storage writes AND core source/tool198 * need atomicity across plugin storage writes AND core source/tool
232 * registration. */199 * registration. */
233 readonly transaction: <A, E>(200 readonly transaction: <A, E>(effect: Effect.Effect<A, E>) => Effect.Effect<A, E | StorageFailure>;
234 effect: Effect.Effect<A, E>,
235 ) => Effect.Effect<A, E | StorageFailure>;
236}201}
237 202
238// ---------------------------------------------------------------------------203// ---------------------------------------------------------------------------
@@ -265,9 +230,7 @@ export interface StaticToolDecl<TStore = unknown> { · 26 unmodified lines
265 * Inline because static tools have no plugin storage to resolve from;230 * Inline because static tools have no plugin storage to resolve from;
266 * the plugin author literally writes this at definition time. */231 * the plugin author literally writes this at definition time. */
267 readonly annotations?: ToolAnnotations;232 readonly annotations?: ToolAnnotations;
268 readonly handler: (233 readonly handler: (input: StaticToolHandlerInput<TStore>) => Effect.Effect<unknown, unknown>;
269 input: StaticToolHandlerInput<TStore>,
270 ) => Effect.Effect<unknown, unknown>;
271}234}
272 235
273export interface StaticSourceDecl<TStore = unknown> {236export interface StaticSourceDecl<TStore = unknown> {
@@ -388,9 +351,7 @@ export interface PluginSpec< · 114 unmodified lines
388 * Handlers close over `self` via the closure, so a control tool351 * Handlers close over `self` via the closure, so a control tool
389 * that delegates to the plugin's real API is a one-liner:352 * that delegates to the plugin's real API is a one-liner:
390 * `({ args }) => self.addSpec(args)`. */353 * `({ args }) => self.addSpec(args)`. */
391 readonly staticSources?: (354 readonly staticSources?: (self: NoInfer<TExtension>) => readonly StaticSourceDecl<TStore>[];
392 self: NoInfer<TExtension>,
393 ) => readonly StaticSourceDecl<TStore>[];
394 355
395 /** HttpApiGroup contributed by this plugin. Composed into the host's356 /** HttpApiGroup contributed by this plugin. Composed into the host's
396 * `HttpApi` via the `addGroup` helper at runtime. The host mounts357 * `HttpApi` via the `addGroup` helper at runtime. The host mounts
@@ -446,9 +407,7 @@ export interface PluginSpec< · 49 unmodified lines
446 * map doesn't have the toolId. The plugin reads its own enrichment407 * map doesn't have the toolId. The plugin reads its own enrichment
447 * via `ctx.storage` and returns the result. Optional — plugins with408 * via `ctx.storage` and returns the result. Optional — plugins with
448 * only static tools can omit it. */409 * only static tools can omit it. */
449 readonly invokeTool?: (410 readonly invokeTool?: (input: InvokeToolInput<TStore>) => Effect.Effect<unknown, unknown>;
450 input: InvokeToolInput<TStore>,
451 ) => Effect.Effect<unknown, unknown>;
452 411
453 /** Bulk resolve annotations (requiresApproval, approvalDescription,412 /** Bulk resolve annotations (requiresApproval, approvalDescription,
454 * mayElicit) for a set of tool rows under a single source. Called413 * mayElicit) for a set of tool rows under a single source. Called
@@ -497,13 +456,9 @@ export interface PluginSpec< · 42 unmodified lines
497 * by this plugin. Plugin-side cleanup only; the executor deletes456 * by this plugin. Plugin-side cleanup only; the executor deletes
498 * the core source/tool rows after this callback returns, inside457 * the core source/tool rows after this callback returns, inside
499 * the same transaction. */458 * the same transaction. */
500 readonly removeSource?: (459 readonly removeSource?: (input: SourceLifecycleInput<TStore>) => Effect.Effect<void, unknown>;
501 input: SourceLifecycleInput<TStore>,
502 ) => Effect.Effect<void, unknown>;
503 460
504 readonly refreshSource?: (461 readonly refreshSource?: (input: SourceLifecycleInput<TStore>) => Effect.Effect<void, unknown>;
505 input: SourceLifecycleInput<TStore>,
506 ) => Effect.Effect<void, unknown>;
507 462
508 /** URL autodetection hook. When the user pastes a URL in the463 /** URL autodetection hook. When the user pastes a URL in the
509 * onboarding UI, `executor.sources.detect(url)` fans out to every464 * onboarding UI, `executor.sources.detect(url)` fans out to every
@@ -526,9 +481,7 @@ export interface PluginSpec< · 16 unmodified lines
526 readonly secretProviders?:481 readonly secretProviders?:
527 | readonly SecretProvider[]482 | readonly SecretProvider[]
528 | ((ctx: PluginCtx<TStore>) => readonly SecretProvider[])483 | ((ctx: PluginCtx<TStore>) => readonly SecretProvider[])
529 | ((484 | ((ctx: PluginCtx<TStore>) => Effect.Effect<readonly SecretProvider[]>);
530 ctx: PluginCtx<TStore>,
531 ) => Effect.Effect<readonly SecretProvider[]>);
532 485
533 /** Connection providers contributed by this plugin. Same registration486 /** Connection providers contributed by this plugin. Same registration
534 * shape as `secretProviders`. Each provider's `key` is what487 * shape as `secretProviders`. Each provider's `key` is what
@@ -538,9 +491,7 @@ export interface PluginSpec< · 3 unmodified lines
538 readonly connectionProviders?:491 readonly connectionProviders?:
539 | readonly ConnectionProvider[]492 | readonly ConnectionProvider[]
540 | ((ctx: PluginCtx<TStore>) => readonly ConnectionProvider[])493 | ((ctx: PluginCtx<TStore>) => readonly ConnectionProvider[])
541 | ((494 | ((ctx: PluginCtx<TStore>) => Effect.Effect<readonly ConnectionProvider[]>);
542 ctx: PluginCtx<TStore>,
543 ) => Effect.Effect<readonly ConnectionProvider[]>);
544 495
545 readonly close?: () => Effect.Effect<void, unknown>;496 readonly close?: () => Effect.Effect<void, unknown>;
546}497}
@@ -558,15 +509,7 @@ export interface Plugin< · 11 unmodified lines
558 // eslint-disable-next-line @typescript-eslint/no-explicit-any509 // eslint-disable-next-line @typescript-eslint/no-explicit-any
559 THandlersLayer extends Layer.Layer<any, any, any> = any,510 THandlersLayer extends Layer.Layer<any, any, any> = any,
560 TGroup extends HttpApiGroup.Any = HttpApiGroup.Any,511 TGroup extends HttpApiGroup.Any = HttpApiGroup.Any,
561> extends PluginSpec<512> extends PluginSpec<TId, TExtension, TStore, TSchema, TExtensionService, THandlersLayer, TGroup> {}
562 TId,
563 TExtension,
564 TStore,
565 TSchema,
566 TExtensionService,
567 THandlersLayer,
568 TGroup
569 > {}
570 513
571// ---------------------------------------------------------------------------514// ---------------------------------------------------------------------------
572// definePlugin — factory-returning-spec. Options from the author factory515// definePlugin — factory-returning-spec. Options from the author factory
@@ -589,15 +532,7 @@ export type ConfiguredPlugin< · 16 unmodified lines
589 options?: TOptions & {532 options?: TOptions & {
590 readonly storage?: (deps: StorageDeps<TSchema>) => TStore;533 readonly storage?: (deps: StorageDeps<TSchema>) => TStore;
591 },534 },
592) => Plugin<535) => Plugin<TId, TExtension, TStore, TSchema, TExtensionService, THandlersLayer, TGroup>;
593 TId,
594 TExtension,
595 TStore,
596 TSchema,
597 TExtensionService,
598 THandlersLayer,
599 TGroup
600>;
601 536
602// eslint-disable-next-line @typescript-eslint/ban-types537// eslint-disable-next-line @typescript-eslint/ban-types
603export function definePlugin<538export function definePlugin<
@@ -614,15 +549,7 @@ export function definePlugin< · 10 unmodified lines
614>(549>(
615 authorFactory: (550 authorFactory: (
616 options?: TOptions,551 options?: TOptions,
617 ) => PluginSpec<552 ) => PluginSpec<TId, TExtension, TStore, TSchema, TExtensionService, THandlersLayer, TGroup>,
618 TId,
619 TExtension,
620 TStore,
621 TSchema,
622 TExtensionService,
623 THandlersLayer,
624 TGroup
625 >,
626): ConfiguredPlugin<553): ConfiguredPlugin<
627 TId,554 TId,
628 TExtension,555 TExtension,
@@ -643,9 +570,7 @@ export function definePlugin< · 14 unmodified lines
643 } = options ?? {};570 } = options ?? {};
644 571
645 const hasAuthorOptions = Object.keys(rest).length > 0;572 const hasAuthorOptions = Object.keys(rest).length > 0;
646 const spec = authorFactory(573 const spec = authorFactory(hasAuthorOptions ? (rest as TOptions) : undefined);
647 hasAuthorOptions ? (rest as TOptions) : undefined,
648 );
649 574
650 return {575 return {
651 ...spec,576 ...spec,
@@ -664,12 +589,7 @@ export function definePlugin< · 12 unmodified lines
664export type AnyPlugin = Plugin<string>;589export type AnyPlugin = Plugin<string>;
665 590
666export type PluginExtensions<TPlugins extends readonly AnyPlugin[]> = {591export type PluginExtensions<TPlugins extends readonly AnyPlugin[]> = {
667 readonly [P in TPlugins[number] as P["id"]]: P extends Plugin<592 readonly [P in TPlugins[number] as P["id"]]: P extends Plugin<string, infer TExt> ? TExt : never;
668 string,
669 infer TExt
670 >
671 ? TExt
672 : never;
673};593};
674 594
675/** Lightweight projection of a secret entry as returned by `ctx.secrets.list`. */595/** Lightweight projection of a secret entry as returned by `ctx.secrets.list`. */
packages/core/sdk/src/policies.test.ts+74−83
@@ -5,10 +5,7 @@ import { generateKeyBetween } from "fractional-indexing"; · 4 unmodified lines
5import type { ToolPolicyRow } from "./core-schema";5import type { ToolPolicyRow } from "./core-schema";
6import { PolicyId } from "./ids";6import { PolicyId } from "./ids";
7import { createExecutor } from "./executor";7import { createExecutor } from "./executor";
8import {8import { ElicitationResponse, type ElicitationHandler } from "./elicitation";
9 ElicitationResponse,
10 type ElicitationHandler,
11} from "./elicitation";
12import {9import {
13 effectivePolicyFromSorted,10 effectivePolicyFromSorted,
14 isValidPattern,11 isValidPattern,
@@ -149,13 +146,8 @@ describe("resolveToolPolicy", () => { · 134 unmodified lines
149 ROW("outer", "vercel.*", "block", "a0", "org"),146 ROW("outer", "vercel.*", "block", "a0", "org"),
150 ROW("inner", "vercel.dns.create", "approve", "a0", "user"),147 ROW("inner", "vercel.dns.create", "approve", "a0", "user"),
151 ];148 ];
152 const rank = (row: Pick<ToolPolicyRow, "scope_id">) =>149 const rank = (row: Pick<ToolPolicyRow, "scope_id">) => (row.scope_id === "user" ? 0 : 1);
153 row.scope_id === "user" ? 0 : 1;150 const result = resolveToolPolicy("vercel.dns.create", policies, rank);
154 const result = resolveToolPolicy(
155 "vercel.dns.create",
156 policies,
157 rank,
158 );
159 expect(result?.action).toBe("approve");151 expect(result?.action).toBe("approve");
160 expect(result?.policyId).toBe("inner");152 expect(result?.policyId).toBe("inner");
161 });153 });
@@ -167,18 +159,12 @@ describe("resolveToolPolicy", () => { · 5 unmodified lines
167 // refetch and reorder math sees colliding neighbor keys.159 // refetch and reorder math sees colliding neighbor keys.
168 const a = resolveToolPolicy(160 const a = resolveToolPolicy(
169 "vercel.dns.create",161 "vercel.dns.create",
170 [162 [ROW("z", "vercel.dns.*", "block", "a0"), ROW("a", "vercel.dns.*", "approve", "a0")],
171 ROW("z", "vercel.dns.*", "block", "a0"),
172 ROW("a", "vercel.dns.*", "approve", "a0"),
173 ],
174 flatRank,163 flatRank,
175 );164 );
176 const b = resolveToolPolicy(165 const b = resolveToolPolicy(
177 "vercel.dns.create",166 "vercel.dns.create",
178 [167 [ROW("a", "vercel.dns.*", "approve", "a0"), ROW("z", "vercel.dns.*", "block", "a0")],
179 ROW("a", "vercel.dns.*", "approve", "a0"),
180 ROW("z", "vercel.dns.*", "block", "a0"),
181 ],
182 flatRank,168 flatRank,
183 );169 );
184 // Same input rows in different order, same winner — id "a" sorts170 // Same input rows in different order, same winner — id "a" sorts
@@ -189,11 +175,11 @@ describe("resolveToolPolicy", () => { · 4 unmodified lines
189});175});
190 176
191describe("effectivePolicyFromSorted", () => {177describe("effectivePolicyFromSorted", () => {
192 const POL = (178 const POL = (id: string, pattern: string, action: "approve" | "require_approval" | "block") => ({
193 id: string,179 id: PolicyId.make(id),
194 pattern: string,180 pattern,
195 action: "approve" | "require_approval" | "block",181 action,
196 ) => ({ id: PolicyId.make(id), pattern, action });182 });
197 183
198 it("returns user policy when one matches", () => {184 it("returns user policy when one matches", () => {
199 const result = effectivePolicyFromSorted(185 const result = effectivePolicyFromSorted(
@@ -218,9 +204,10 @@ describe("effectivePolicyFromSorted", () => { · 18 unmodified lines
218 });204 });
219 205
220 it("falls back to plugin default approve when annotation is false/undefined", () => {206 it("falls back to plugin default approve when annotation is false/undefined", () => {
221 expect(207 expect(effectivePolicyFromSorted("vercel.dns.create", [], false)).toEqual({
222 effectivePolicyFromSorted("vercel.dns.create", [], false),208 action: "approve",
223 ).toEqual({ action: "approve", source: "plugin-default" });209 source: "plugin-default",
210 });
224 expect(effectivePolicyFromSorted("vercel.dns.create", [])).toEqual({211 expect(effectivePolicyFromSorted("vercel.dns.create", [])).toEqual({
225 action: "approve",212 action: "approve",
226 source: "plugin-default",213 source: "plugin-default",
@@ -342,10 +329,7 @@ describe("executor.policies", () => { · 115 unmodified lines
342 expect(second.position < first.position).toBe(true);329 expect(second.position < first.position).toBe(true);
343 330
344 const rules = yield* executor.policies.list();331 const rules = yield* executor.policies.list();
345 expect(rules.map((r) => r.pattern)).toEqual([332 expect(rules.map((r) => r.pattern)).toEqual(["vercel.delete", "vercel.*"]);
346 "vercel.delete",
347 "vercel.*",
348 ]);
349 }),333 }),
350 );334 );
351 335
@@ -425,34 +409,27 @@ describe("executor.policies", () => { · 73 unmodified lines
425 }),409 }),
426 );410 );
427 411
428 it.effect(412 it.effect("consecutive creates produce strictly increasing-precedence keys", () =>
429 "consecutive creates produce strictly increasing-precedence keys",413 Effect.gen(function* () {
430 () =>414 const executor = yield* setupExecutor();
431 Effect.gen(function* () {415 const created: string[] = [];
432 const executor = yield* setupExecutor();416 for (const pattern of ["a.*", "b.*", "c.*", "d.*"]) {
433 const created: string[] = [];417 const row = yield* executor.policies.create({
434 for (const pattern of ["a.*", "b.*", "c.*", "d.*"]) {418 scope: "test-scope",
435 const row = yield* executor.policies.create({419 pattern,
436 scope: "test-scope",420 action: "approve",
437 pattern,421 });
438 action: "approve",422 created.push(row.position);
439 });423 }
440 created.push(row.position);424 // Each new key sorts strictly above the previous (lower lex
441 }425 // order = higher precedence). No collisions.
442 // Each new key sorts strictly above the previous (lower lex426 for (let i = 1; i < created.length; i++) {
443 // order = higher precedence). No collisions.427 expect(created[i]! < created[i - 1]!).toBe(true);
444 for (let i = 1; i < created.length; i++) {428 }
445 expect(created[i]! < created[i - 1]!).toBe(true);429 // List order matches insertion-reverse.
446 }430 const rules = yield* executor.policies.list();
447 // List order matches insertion-reverse.431 expect(rules.map((r) => r.pattern)).toEqual(["d.*", "c.*", "b.*", "a.*"]);
448 const rules = yield* executor.policies.list();432 }),
449 expect(rules.map((r) => r.pattern)).toEqual([
450 "d.*",
451 "c.*",
452 "b.*",
453 "a.*",
454 ]);
455 }),
456 );433 );
457 434
458 it.effect("remove deletes the row", () =>435 it.effect("remove deletes the row", () =>
@@ -521,14 +498,10 @@ describe("blocked tools", () => { · 62 unmodified lines
521 pattern: "vercel.*",498 pattern: "vercel.*",
522 action: "block",499 action: "block",
523 });500 });
524 const result = yield* Effect.result(501 const result = yield* Effect.result(executor.tools.invoke("vercel.delete", {}));
525 executor.tools.invoke("vercel.delete", {}),
526 );
527 expect(Result.isFailure(result)).toBe(true);502 expect(Result.isFailure(result)).toBe(true);
528 if (!Result.isFailure(result)) return;503 if (!Result.isFailure(result)) return;
529 expect(Predicate.isTagged("ToolBlockedError")(result.failure)).toBe(504 expect(Predicate.isTagged("ToolBlockedError")(result.failure)).toBe(true);
530 true,
531 );
532 if (!Predicate.isTagged("ToolBlockedError")(result.failure)) return;505 if (!Predicate.isTagged("ToolBlockedError")(result.failure)) return;
533 expect(result.failure.pattern).toBe("vercel.*");506 expect(result.failure.pattern).toBe("vercel.*");
534 }),507 }),
@@ -545,9 +518,13 @@ describe("approve / require_approval interaction with annotations", () => { · 10 unmodified lines
545 action: "approve",518 action: "approve",
546 });519 });
547 const calls = { count: 0 };520 const calls = { count: 0 };
548 const result = yield* executor.tools.invoke("vercel.delete", {}, {521 const result = yield* executor.tools.invoke(
549 onElicitation: recordingHandler(calls),522 "vercel.delete",
550 });523 {},
524 {
525 onElicitation: recordingHandler(calls),
526 },
527 );
551 expect(calls.count).toBe(0);528 expect(calls.count).toBe(0);
552 expect(result).toEqual({ ran: "vercel.delete" });529 expect(result).toEqual({ ran: "vercel.delete" });
553 }),530 }),
@@ -562,9 +539,13 @@ describe("approve / require_approval interaction with annotations", () => { · 8 unmodified lines
562 action: "require_approval",539 action: "require_approval",
563 });540 });
564 const calls = { count: 0 };541 const calls = { count: 0 };
565 yield* executor.tools.invoke("vercel.deploy", {}, {542 yield* executor.tools.invoke(
566 onElicitation: recordingHandler(calls),543 "vercel.deploy",
567 });544 {},
545 {
546 onElicitation: recordingHandler(calls),
547 },
548 );
568 expect(calls.count).toBe(1);549 expect(calls.count).toBe(1);
569 }),550 }),
570 );551 );
@@ -578,15 +559,17 @@ describe("approve / require_approval interaction with annotations", () => { · 7 unmodified lines
578 action: "require_approval",559 action: "require_approval",
579 });560 });
580 const result = yield* Effect.result(561 const result = yield* Effect.result(
581 executor.tools.invoke("vercel.deploy", {}, {562 executor.tools.invoke(
582 onElicitation: decliningHandler,563 "vercel.deploy",
583 }),564 {},
565 {
566 onElicitation: decliningHandler,
567 },
568 ),
584 );569 );
585 expect(Result.isFailure(result)).toBe(true);570 expect(Result.isFailure(result)).toBe(true);
586 if (!Result.isFailure(result)) return;571 if (!Result.isFailure(result)) return;
587 expect(572 expect(Predicate.isTagged("ElicitationDeclinedError")(result.failure)).toBe(true);
588 Predicate.isTagged("ElicitationDeclinedError")(result.failure),
589 ).toBe(true);
590 }),573 }),
591 );574 );
592 575
@@ -597,16 +580,24 @@ describe("approve / require_approval interaction with annotations", () => { · 4 unmodified lines
597 // marks any tool whose name contains "delete" as requiring580 // marks any tool whose name contains "delete" as requiring
598 // approval, so the prompt should fire.581 // approval, so the prompt should fire.
599 const calls = { count: 0 };582 const calls = { count: 0 };
600 yield* executor.tools.invoke("vercel.delete", {}, {583 yield* executor.tools.invoke(
601 onElicitation: recordingHandler(calls),584 "vercel.delete",
602 });585 {},
586 {
587 onElicitation: recordingHandler(calls),
588 },
589 );
603 expect(calls.count).toBe(1);590 expect(calls.count).toBe(1);
604 591
605 // vercel.deploy has no plugin-required approval and no policy,592 // vercel.deploy has no plugin-required approval and no policy,
606 // so no prompt.593 // so no prompt.
607 yield* executor.tools.invoke("vercel.deploy", {}, {594 yield* executor.tools.invoke(
608 onElicitation: recordingHandler(calls),595 "vercel.deploy",
609 });596 {},
597 {
598 onElicitation: recordingHandler(calls),
599 },
600 );
610 expect(calls.count).toBe(1);601 expect(calls.count).toBe(1);
611 }),602 }),
612 );603 );
packages/core/sdk/src/policies.ts+2−8
@@ -184,9 +184,7 @@ export const resolveToolPolicy = ( · 183 unmodified lines
184// policies in evaluation order from the API.184// policies in evaluation order from the API.
185// ---------------------------------------------------------------------------185// ---------------------------------------------------------------------------
186 186
187const liftPlugin = (187const liftPlugin = (defaultRequiresApproval: boolean | undefined): EffectivePolicy =>
188 defaultRequiresApproval: boolean | undefined,
189): EffectivePolicy =>
190 defaultRequiresApproval188 defaultRequiresApproval
191 ? { action: "require_approval", source: "plugin-default" }189 ? { action: "require_approval", source: "plugin-default" }
192 : { action: "approve", source: "plugin-default" };190 : { action: "approve", source: "plugin-default" };
@@ -245,8 +243,4 @@ export const rowToToolPolicy = (row: ToolPolicyRow): ToolPolicy => ({ · 52 unmodified lines
245// inputs with effect/Schema.243// inputs with effect/Schema.
246// ---------------------------------------------------------------------------244// ---------------------------------------------------------------------------
247 245
248export const ToolPolicyActionSchema = Schema.Literals([246export const ToolPolicyActionSchema = Schema.Literals(["approve", "require_approval", "block"]);
249 "approve",
250 "require_approval",
251 "block",
252]);
packages/core/sdk/src/promise-executor.ts+22−33
@@ -40,25 +40,24 @@ import { Scope } from "./scope"; · 39 unmodified lines
40// off a returned ref still gets the branded type for use as an opaque token.40// off a returned ref still gets the branded type for use as an opaque token.
41// ---------------------------------------------------------------------------41// ---------------------------------------------------------------------------
42 42
43type Unbrand<T> = T extends Brand.Brand<string>43type Unbrand<T> =
44 ? string44 T extends Brand.Brand<string>
45 : T extends readonly (infer U)[]45 ? string
46 ? readonly Unbrand<U>[]46 : T extends readonly (infer U)[]
47 : T extends ReadonlyMap<infer K, infer V>47 ? readonly Unbrand<U>[]
48 ? ReadonlyMap<Unbrand<K>, Unbrand<V>>48 : T extends ReadonlyMap<infer K, infer V>
49 : T extends ReadonlySet<infer U>49 ? ReadonlyMap<Unbrand<K>, Unbrand<V>>
50 ? ReadonlySet<Unbrand<U>>50 : T extends ReadonlySet<infer U>
51 : T extends Date51 ? ReadonlySet<Unbrand<U>>
52 ? T52 : T extends Date
53 : T extends (...args: infer A) => infer R53 ? T
54 ? (...args: { [I in keyof A]: Unbrand<A[I]> }) => Unbrand<R>54 : T extends (...args: infer A) => infer R
55 : T extends object55 ? (...args: { [I in keyof A]: Unbrand<A[I]> }) => Unbrand<R>
56 ? { readonly [K in keyof T]: Unbrand<T[K]> }56 : T extends object
57 : T;57 ? { readonly [K in keyof T]: Unbrand<T[K]> }
58 58 : T;
59export type Promisified<T> = T extends (59
60 ...args: infer A60export type Promisified<T> = T extends (...args: infer A) => Effect.Effect<infer R, infer _E>
61) => Effect.Effect<infer R, infer _E>
62 ? (...args: { [I in keyof A]: Unbrand<A[I]> }) => Promise<R>61 ? (...args: { [I in keyof A]: Unbrand<A[I]> }) => Promise<R>
63 : T extends readonly unknown[]62 : T extends readonly unknown[]
64 ? T63 ? T
@@ -103,10 +102,7 @@ const isPlainObject = (v: unknown): v is Record<string | symbol, unknown> => · 38 unmodified lines
103const promisifyDeep = <T>(value: T): Promisified<T> => {102const promisifyDeep = <T>(value: T): Promisified<T> => {
104 if (typeof value === "function") {103 if (typeof value === "function") {
105 return ((...args: unknown[]) => {104 return ((...args: unknown[]) => {
106 const result = (value as (...a: unknown[]) => unknown).apply(105 const result = (value as (...a: unknown[]) => unknown).apply(undefined, args);
107 undefined,
108 args,
109 );
110 if (Effect.isEffect(result)) {106 if (Effect.isEffect(result)) {
111 return Effect.runPromise(result as Effect.Effect<unknown, unknown>);107 return Effect.runPromise(result as Effect.Effect<unknown, unknown>);
112 }108 }
@@ -121,10 +117,7 @@ const promisifyDeep = <T>(value: T): Promisified<T> => { · 8 unmodified lines
121 const v = Reflect.get(target, prop, receiver);117 const v = Reflect.get(target, prop, receiver);
122 if (typeof v === "function") {118 if (typeof v === "function") {
123 return (...args: unknown[]) => {119 return (...args: unknown[]) => {
124 const result = (v as (...a: unknown[]) => unknown).apply(120 const result = (v as (...a: unknown[]) => unknown).apply(target, args);
125 target,
126 args,
127 );
128 if (Effect.isEffect(result)) {121 if (Effect.isEffect(result)) {
129 return Effect.runPromise(result as Effect.Effect<unknown, unknown>);122 return Effect.runPromise(result as Effect.Effect<unknown, unknown>);
130 }123 }
@@ -143,9 +136,7 @@ const promisifyDeep = <T>(value: T): Promisified<T> => { · 12 unmodified lines
143// construct an executor with just `{ plugins: [...] }`.136// construct an executor with just `{ plugins: [...] }`.
144// ---------------------------------------------------------------------------137// ---------------------------------------------------------------------------
145 138
146export const createExecutor = async <139export const createExecutor = async <const TPlugins extends readonly AnyPlugin[] = []>(
147 const TPlugins extends readonly AnyPlugin[] = [],
148>(
149 config: ExecutorConfig<TPlugins>,140 config: ExecutorConfig<TPlugins>,
150): Promise<Executor<TPlugins>> => {141): Promise<Executor<TPlugins>> => {
151 const plugins = (config?.plugins ?? []) as TPlugins;142 const plugins = (config?.plugins ?? []) as TPlugins;
@@ -183,9 +174,7 @@ export const createExecutor = async < · 31 unmodified lines
183 // get the tagged error as the rejected value. See174 // get the tagged error as the rejected value. See
184 // notes/promise-sdk-typed-errors.md for the planned `runPromiseExit`175 // notes/promise-sdk-typed-errors.md for the planned `runPromiseExit`
185 // rewrite that exposes the full error union to consumers.176 // rewrite that exposes the full error union to consumers.
186 const effectExecutor = await Effect.runPromise(177 const effectExecutor = await Effect.runPromise(createEffectExecutor(effectConfig));
187 createEffectExecutor(effectConfig),
188 );
189 178
190 return promisifyDeep(effectExecutor) as Executor<TPlugins>;179 return promisifyDeep(effectExecutor) as Executor<TPlugins>;
191};180};
packages/core/sdk/src/promise.test.ts+2−4
@@ -33,8 +33,7 @@ const echoPlugin = definePlugin(() => ({ · 32 unmodified lines
33 },33 },
34 ],34 ],
35 extension: () => ({35 extension: () => ({
36 greet: (name: string) =>36 greet: (name: string) => Effect.succeed(`hello, ${name}`) as Effect.Effect<string, never>,
37 Effect.succeed(`hello, ${name}`) as Effect.Effect<string, never>,
38 }),37 }),
39}));38}));
40 39
@@ -110,8 +109,7 @@ describe("promise/createExecutor", () => { · 69 unmodified lines
110 "ap.ctl.go",109 "ap.ctl.go",
111 {},110 {},
112 {111 {
113 onElicitation: () =>112 onElicitation: () => Effect.succeed({ action: "decline" as const }) as any,
114 Effect.succeed({ action: "decline" as const }) as any,
115 },113 },
116 ),114 ),
117 ).rejects.toMatchObject({115 ).rejects.toMatchObject({
packages/core/sdk/src/promise.ts+1−5
@@ -51,11 +51,7 @@ export { · 50 unmodified lines
51 51
52// File-config helper for the CLI. Plain typed-object factory with no52// File-config helper for the CLI. Plain typed-object factory with no
53// Effect in its signature, so it's safe to live on the Promise surface.53// Effect in its signature, so it's safe to live on the Promise surface.
54export {54export { defineExecutorConfig, type ExecutorCliConfig, type ExecutorDialect } from "./config";
55 defineExecutorConfig,
56 type ExecutorCliConfig,
57 type ExecutorDialect,
58} from "./config";
59 55
60// Error tags — Promise callers handle these via .catch().56// Error tags — Promise callers handle these via .catch().
61export {57export {
packages/core/sdk/src/schema-types.ts+0−1
@@ -398,4 +398,3 @@ export const buildToolTypeScriptPreview = (input: { · 397 unmodified lines
398 : {}),398 : {}),
399 };399 };
400};400};
401
packages/core/sdk/src/scoped-adapter.test.ts+2−6
@@ -179,16 +179,12 @@ describe("scopeAdapter — read isolation", () => { · 178 unmodified lines
179 it.effect("caller-supplied scope_id filter is stripped (can't bypass isolation)", () =>179 it.effect("caller-supplied scope_id filter is stripped (can't bypass isolation)", () =>
180 Effect.gen(function* () {180 Effect.gen(function* () {
181 const inner = makeMemoryAdapter({ schema });181 const inner = makeMemoryAdapter({ schema });
182 yield* typedAdapter<typeof schema>(182 yield* typedAdapter<typeof schema>(scopeAdapter(inner, { scopes: ["c"] }, schema)).create({
183 scopeAdapter(inner, { scopes: ["c"] }, schema),
184 ).create({
185 model: "thing",183 model: "thing",
186 data: { id: "t-hidden", scope_id: "c", value: "secret" },184 data: { id: "t-hidden", scope_id: "c", value: "secret" },
187 });185 });
188 186
189 const reader = typedAdapter<typeof schema>(187 const reader = typedAdapter<typeof schema>(scopeAdapter(inner, { scopes: ["a"] }, schema));
190 scopeAdapter(inner, { scopes: ["a"] }, schema),
191 );
192 // Attempt to bypass by explicitly filtering for scope_id "c".188 // Attempt to bypass by explicitly filtering for scope_id "c".
193 const rows = yield* reader.findMany({189 const rows = yield* reader.findMany({
194 model: "thing",190 model: "thing",
packages/core/sdk/src/scoped-adapter.ts+7−34
@@ -49,10 +49,7 @@ const collectScopedModels = (schema: DBSchema): Set<string> => { · 48 unmodified lines
49 return out;49 return out;
50};50};
51 51
52const withScopeRead = (52const withScopeRead = (where: readonly Where[] | undefined, ctx: ScopeContext): Where[] => {
53 where: readonly Where[] | undefined,
54 ctx: ScopeContext,
55): Where[] => {
56 const base = (where ?? []).filter((w) => w.field !== SCOPE_FIELD);53 const base = (where ?? []).filter((w) => w.field !== SCOPE_FIELD);
57 const callerScope = (where ?? []).find((w) => w.field === SCOPE_FIELD);54 const callerScope = (where ?? []).find((w) => w.field === SCOPE_FIELD);
58 55
@@ -123,11 +120,7 @@ const wrapTxMethods = ( · 64 unmodified lines
123 create: (data) =>120 create: (data) =>
124 isScoped(data.model)121 isScoped(data.model)
125 ? Effect.flatMap(122 ? Effect.flatMap(
126 assertScopedWrite(123 assertScopedWrite(data.model, data.data as Record<string, unknown>, ctx),
127 data.model,
128 data.data as Record<string, unknown>,
129 ctx,
130 ),
131 () => inner.create(data),124 () => inner.create(data),
132 )125 )
133 : inner.create(data),126 : inner.create(data),
@@ -136,11 +129,7 @@ const wrapTxMethods = ( · 2 unmodified lines
136 ? Effect.flatMap(129 ? Effect.flatMap(
137 Effect.all(130 Effect.all(
138 data.data.map((row) =>131 data.data.map((row) =>
139 assertScopedWrite(132 assertScopedWrite(data.model, row as Record<string, unknown>, ctx),
140 data.model,
141 row as Record<string, unknown>,
142 ctx,
143 ),
144 ),133 ),
145 ),134 ),
146 () => inner.createMany(data),135 () => inner.createMany(data),
@@ -168,11 +157,7 @@ const wrapTxMethods = ( · 21 unmodified lines
168 // rows you can read. That's sufficient for isolation; we157 // rows you can read. That's sufficient for isolation; we
169 // don't need to force-stamp on update.158 // don't need to force-stamp on update.
170 (data.update as Record<string, unknown>)[SCOPE_FIELD] !== undefined159 (data.update as Record<string, unknown>)[SCOPE_FIELD] !== undefined
171 ? assertScopedWrite(160 ? assertScopedWrite(data.model, data.update as Record<string, unknown>, ctx)
172 data.model,
173 data.update as Record<string, unknown>,
174 ctx,
175 )
176 : Effect.void,161 : Effect.void,
177 () =>162 () =>
178 inner.update({163 inner.update({
@@ -185,11 +170,7 @@ const wrapTxMethods = ( · 6 unmodified lines
185 isScoped(data.model)170 isScoped(data.model)
186 ? Effect.flatMap(171 ? Effect.flatMap(
187 (data.update as Record<string, unknown>)[SCOPE_FIELD] !== undefined172 (data.update as Record<string, unknown>)[SCOPE_FIELD] !== undefined
188 ? assertScopedWrite(173 ? assertScopedWrite(data.model, data.update as Record<string, unknown>, ctx)
189 data.model,
190 data.update as Record<string, unknown>,
191 ctx,
192 )
193 : Effect.void,174 : Effect.void,
194 () =>175 () =>
195 inner.updateMany({176 inner.updateMany({
@@ -209,22 +190,14 @@ const wrapTxMethods = ( · 13 unmodified lines
209 };190 };
210};191};
211 192
212export const scopeAdapter = (193export const scopeAdapter = (inner: DBAdapter, ctx: ScopeContext, schema: DBSchema): DBAdapter => {
213 inner: DBAdapter,
214 ctx: ScopeContext,
215 schema: DBSchema,
216): DBAdapter => {
217 const scopedModels = collectScopedModels(schema);194 const scopedModels = collectScopedModels(schema);
218 const tx = wrapTxMethods(inner, ctx, scopedModels);195 const tx = wrapTxMethods(inner, ctx, scopedModels);
219 return {196 return {
220 ...tx,197 ...tx,
221 transaction: (callback) =>198 transaction: (callback) =>
222 inner.transaction((rawTrx) => {199 inner.transaction((rawTrx) => {
223 const scopedTrx: DBTransactionAdapter = wrapTxMethods(200 const scopedTrx: DBTransactionAdapter = wrapTxMethods(rawTrx, ctx, scopedModels);
224 rawTrx,
225 ctx,
226 scopedModels,
227 );
228 return callback(scopedTrx);201 return callback(scopedTrx);
229 }),202 }),
230 createSchema: inner.createSchema,203 createSchema: inner.createSchema,
packages/core/sdk/src/secrets.ts+5−20
@@ -30,31 +30,18 @@ export interface SecretProvider { · 29 unmodified lines
30 * failed, etc.) surface as `StorageFailure` — the executor treats30 * failed, etc.) surface as `StorageFailure` — the executor treats
31 * a provider call the same as a DB call; `StorageError` is captured31 * a provider call the same as a DB call; `StorageError` is captured
32 * at the HTTP edge to `InternalError`, `UniqueViolationError` dies. */32 * at the HTTP edge to `InternalError`, `UniqueViolationError` dies. */
33 readonly get: (33 readonly get: (id: string, scope: string) => Effect.Effect<string | null, StorageFailure>;
34 id: string,
35 scope: string,
36 ) => Effect.Effect<string | null, StorageFailure>;
37 /** Check whether a provider has a backing value without returning it.34 /** Check whether a provider has a backing value without returning it.
38 * Providers that can answer this cheaply should implement it so35 * Providers that can answer this cheaply should implement it so
39 * stale core routing rows don't appear as selectable secrets. */36 * stale core routing rows don't appear as selectable secrets. */
40 readonly has?: (37 readonly has?: (id: string, scope: string) => Effect.Effect<boolean, StorageFailure>;
41 id: string,
42 scope: string,
43 ) => Effect.Effect<boolean, StorageFailure>;
44 /** Set a secret value at a named scope. Only called on writable38 /** Set a secret value at a named scope. Only called on writable
45 * providers. Providers that partition by scope use this arg to39 * providers. Providers that partition by scope use this arg to
46 * decide where to write; flat providers ignore it. */40 * decide where to write; flat providers ignore it. */
47 readonly set?: (41 readonly set?: (id: string, value: string, scope: string) => Effect.Effect<void, StorageFailure>;
48 id: string,
49 value: string,
50 scope: string,
51 ) => Effect.Effect<void, StorageFailure>;
52 /** Delete a secret at a named scope. Only called on writable providers.42 /** Delete a secret at a named scope. Only called on writable providers.
53 * Returns true if something was deleted. */43 * Returns true if something was deleted. */
54 readonly delete?: (44 readonly delete?: (id: string, scope: string) => Effect.Effect<boolean, StorageFailure>;
55 id: string,
56 scope: string,
57 ) => Effect.Effect<boolean, StorageFailure>;
58 /** Enumerate known secret entries. Optional — not all backends can45 /** Enumerate known secret entries. Optional — not all backends can
59 * enumerate (env-backed providers, for example). */46 * enumerate (env-backed providers, for example). */
60 readonly list?: () => Effect.Effect<47 readonly list?: () => Effect.Effect<
@@ -90,9 +77,7 @@ export class SecretRef extends Schema.Class<SecretRef>("SecretRef")({ · 29 unmodified lines
90// OAuth token exchange writes to the innermost per-user scope.77// OAuth token exchange writes to the innermost per-user scope.
91// ---------------------------------------------------------------------------78// ---------------------------------------------------------------------------
92 79
93export class SetSecretInput extends Schema.Class<SetSecretInput>(80export class SetSecretInput extends Schema.Class<SetSecretInput>("SetSecretInput")({
94 "SetSecretInput",
95)({
96 id: SecretId,81 id: SecretId,
97 /** Scope id to own this secret. Must be one of the executor's82 /** Scope id to own this secret. Must be one of the executor's
98 * configured scopes. */83 * configured scopes. */
packages/core/sdk/src/testing.ts+8−11
@@ -16,21 +16,18 @@ import { Scope } from "./scope"; · 15 unmodified lines
16// need multi-scope behavior can pass `scopes` explicitly.16// need multi-scope behavior can pass `scopes` explicitly.
17// ---------------------------------------------------------------------------17// ---------------------------------------------------------------------------
18 18
19export const makeTestConfig = <19export const makeTestConfig = <const TPlugins extends readonly AnyPlugin[] = []>(options?: {
20 const TPlugins extends readonly AnyPlugin[] = [],
21>(options?: {
22 readonly scopeName?: string;20 readonly scopeName?: string;
23 readonly scopes?: readonly Scope[];21 readonly scopes?: readonly Scope[];
24 readonly plugins?: TPlugins;22 readonly plugins?: TPlugins;
25}): ExecutorConfig<TPlugins> => {23}): ExecutorConfig<TPlugins> => {
26 const scopes =24 const scopes = options?.scopes ?? [
27 options?.scopes ?? [25 new Scope({
28 new Scope({26 id: ScopeId.make("test-scope"),
29 id: ScopeId.make("test-scope"),27 name: options?.scopeName ?? "test",
30 name: options?.scopeName ?? "test",28 createdAt: new Date(),
31 createdAt: new Date(),29 }),
32 }),30 ];
33 ];
34 31
35 const schema = collectSchemas(options?.plugins ?? []);32 const schema = collectSchemas(options?.plugins ?? []);
36 33
packages/core/sdk/src/types.ts+1−3
@@ -65,9 +65,7 @@ export class ToolSchema extends Schema.Class<ToolSchema>("ToolSchema")({ · 64 unmodified lines
65 outputSchema: Schema.optional(Schema.Unknown),65 outputSchema: Schema.optional(Schema.Unknown),
66 inputTypeScript: Schema.optional(Schema.String),66 inputTypeScript: Schema.optional(Schema.String),
67 outputTypeScript: Schema.optional(Schema.String),67 outputTypeScript: Schema.optional(Schema.String),
68 typeScriptDefinitions: Schema.optional(68 typeScriptDefinitions: Schema.optional(Schema.Record(Schema.String, Schema.String)),
69 Schema.Record(Schema.String, Schema.String),
70 ),
71}) {}69}) {}
72 70
73// ---------------------------------------------------------------------------71// ---------------------------------------------------------------------------
packages/core/storage-core/LICENSE.md+21−20
@@ -6,26 +6,27 @@ Modifications were made to remove authentication-specific types and to · 5 unmodified lines
6adapt the interfaces to the executor project.6adapt the interfaces to the executor project.
7 7
8Vendored files:8Vendored files:
9 - src/adapter.ts — ported from packages/core/src/db/adapter/index.ts9
10 and types.ts (DBAdapter interface, Where DSL, CustomAdapter,10- src/adapter.ts — ported from packages/core/src/db/adapter/index.ts
11 DBAdapterFactoryConfig). Promise → Effect conversion, auth-specific11 and types.ts (DBAdapter interface, Where DSL, CustomAdapter,
12 model names stripped.12 DBAdapterFactoryConfig). Promise → Effect conversion, auth-specific
13 - src/schema.ts — ported from packages/core/src/db/type.ts13 model names stripped.
14 (DBSchema, DBFieldAttribute, InferDB*).14- src/schema.ts — ported from packages/core/src/db/type.ts
15 - src/factory.ts — ported from packages/core/src/db/adapter/factory.ts15 (DBSchema, DBFieldAttribute, InferDB\*).
16 (createAdapterFactory). Promise → Effect. Stripped auth-specific16- src/factory.ts — ported from packages/core/src/db/adapter/factory.ts
17 concerns: BetterAuthOptions generic, numeric serial ids, joins,17 (createAdapterFactory). Promise → Effect. Stripped auth-specific
18 telemetry spans, logger, plural model resolution. Matches our18 concerns: BetterAuthOptions generic, numeric serial ids, joins,
19 simpler CustomAdapter + DBAdapterFactoryConfig shape.19 telemetry spans, logger, plural model resolution. Matches our
20 - src/testing/memory.ts — ported from20 simpler CustomAdapter + DBAdapterFactoryConfig shape.
21 packages/memory-adapter/src/memory-adapter.ts (memoryAdapter).21- src/testing/memory.ts — ported from
22 Promise → Effect. Piped through our createAdapter.22 packages/memory-adapter/src/memory-adapter.ts (memoryAdapter).
23 - storage-drizzle/src/adapter.ts — ported from23 Promise → Effect. Piped through our createAdapter.
24 packages/drizzle-adapter/src/drizzle-adapter.ts (drizzleAdapter).24- storage-drizzle/src/adapter.ts — ported from
25 Promise → Effect. Takes an explicit tables map instead of reading25 packages/drizzle-adapter/src/drizzle-adapter.ts (drizzleAdapter).
26 db._.fullSchema. Piped through our createAdapter.26 Promise → Effect. Takes an explicit tables map instead of reading
27 27 db.\_.fullSchema. Piped through our createAdapter.
28-------------------------------------------------------------------------------28
29---
29 30
30The MIT License (MIT)31The MIT License (MIT)
31Copyright (c) 2024 - present, Bereket Engida32Copyright (c) 2024 - present, Bereket Engida
packages/core/storage-core/README.md+1−5
@@ -42,11 +42,7 @@ Or grab typed query helpers for an existing schema — purely a type-level · 41 unmodified lines
42view, no runtime cost:42view, no runtime cost:
43 43
44```ts44```ts
45import {45import { typedAdapter, type DBAdapter, type DBSchema } from "@executor-js/storage-core";
46 typedAdapter,
47 type DBAdapter,
48 type DBSchema,
49} from "@executor-js/storage-core";
50 46
51const schema = {47const schema = {
52 secrets: {48 secrets: {
packages/core/storage-core/package.json+6−6
@@ -53,6 +53,12 @@ · 52 unmodified lines
53 "@standard-schema/spec": "^1.0.0",53 "@standard-schema/spec": "^1.0.0",
54 "effect": "catalog:"54 "effect": "catalog:"
55 },55 },
56 "devDependencies": {
57 "@effect/vitest": "catalog:",
58 "tsup": "catalog:",
59 "typescript": "catalog:",
60 "vitest": "catalog:"
61 },
56 "peerDependencies": {62 "peerDependencies": {
57 "@effect/vitest": "catalog:",63 "@effect/vitest": "catalog:",
58 "vitest": "catalog:"64 "vitest": "catalog:"
@@ -64,11 +70,5 @@ · 5 unmodified lines
64 "vitest": {70 "vitest": {
65 "optional": true71 "optional": true
66 }72 }
67 },
68 "devDependencies": {
69 "@effect/vitest": "catalog:",
70 "tsup": "catalog:",
71 "typescript": "catalog:",
72 "vitest": "catalog:"
73 }73 }
74}74}
packages/core/storage-core/src/adapter.ts+2−7
@@ -192,9 +192,7 @@ export type DBAdapter = { · 191 unmodified lines
192 * "does my adapter support JSON?") without having to thread the config192 * "does my adapter support JSON?") without having to thread the config
193 * manually. Populated by `createAdapter`.193 * manually. Populated by `createAdapter`.
194 */194 */
195 options?:195 options?: ({ adapterConfig: DBAdapterFactoryConfig } & CustomAdapter["options"]) | undefined;
196 | ({ adapterConfig: DBAdapterFactoryConfig } & CustomAdapter["options"])
197 | undefined;
198};196};
199 197
200// ---------------------------------------------------------------------------198// ---------------------------------------------------------------------------
@@ -252,10 +250,7 @@ export interface CustomAdapter { · 51 unmodified lines
252 join?: JoinConfig | undefined;250 join?: JoinConfig | undefined;
253 }) => Effect.Effect<T[], StorageFailure>;251 }) => Effect.Effect<T[], StorageFailure>;
254 252
255 delete: (data: {253 delete: (data: { model: string; where: CleanedWhere[] }) => Effect.Effect<void, StorageFailure>;
256 model: string;
257 where: CleanedWhere[];
258 }) => Effect.Effect<void, StorageFailure>;
259 254
260 deleteMany: (data: {255 deleteMany: (data: {
261 model: string;256 model: string;
packages/core/storage-core/src/errors.ts+1−3
@@ -35,8 +35,6 @@ export class StorageError extends Data.TaggedError("StorageError")<{ · 34 unmodified lines
35 * driver message) stays internal because plugin code rarely needs it35 * driver message) stays internal because plugin code rarely needs it
36 * and surfacing it leaks backend specifics.36 * and surfacing it leaks backend specifics.
37 */37 */
38export class UniqueViolationError extends Data.TaggedError(38export class UniqueViolationError extends Data.TaggedError("UniqueViolationError")<{
39 "UniqueViolationError",
40)<{
41 readonly model?: string;39 readonly model?: string;
42}> {}40}> {}
packages/core/storage-core/src/memory.test.ts+1−4
@@ -11,10 +11,7 @@ import { Effect } from "effect"; · 10 unmodified lines
11 11
12import type { DBAdapter } from "./adapter";12import type { DBAdapter } from "./adapter";
13import { makeMemoryAdapter } from "./testing/memory";13import { makeMemoryAdapter } from "./testing/memory";
14import {14import { conformanceSchema, runAdapterConformance } from "./testing/conformance";
15 conformanceSchema,
16 runAdapterConformance,
17} from "./testing/conformance";
18 15
19const withAdapter = <A, E>(16const withAdapter = <A, E>(
20 fn: (adapter: DBAdapter) => Effect.Effect<A, E>,17 fn: (adapter: DBAdapter) => Effect.Effect<A, E>,
packages/core/storage-core/src/schema.ts+10−28
@@ -41,16 +41,13 @@ export type InferDBValueType<T extends DBFieldType> = T extends "string" · 40 unmodified lines
41 ? T[number]41 ? T[number]
42 : never;42 : never;
43 43
44export type InferDBFieldOutput<T extends DBFieldAttribute> =44export type InferDBFieldOutput<T extends DBFieldAttribute> = T["returned"] extends false
45 T["returned"] extends false45 ? never
46 ? never46 : T["required"] extends false
47 : T["required"] extends false47 ? InferDBValueType<T["type"]> | undefined | null
48 ? InferDBValueType<T["type"]> | undefined | null48 : InferDBValueType<T["type"]>;
49 : InferDBValueType<T["type"]>;
50 49
51export type InferDBFieldInput<T extends DBFieldAttribute> = InferDBValueType<50export type InferDBFieldInput<T extends DBFieldAttribute> = InferDBValueType<T["type"]>;
52 T["type"]
53>;
54 51
55export type InferDBFieldsInput<Field> =52export type InferDBFieldsInput<Field> =
56 Field extends Record<infer Key, DBFieldAttribute>53 Field extends Record<infer Key, DBFieldAttribute>
@@ -70,19 +67,13 @@ export type InferDBFieldsInput<Field> = · 13 unmodified lines
70 }67 }
71 : {};68 : {};
72 69
73export type InferDBFieldsOutput<70export type InferDBFieldsOutput<Fields extends Record<string, DBFieldAttribute>> =
74 Fields extends Record<string, DBFieldAttribute>,
75> =
76 Fields extends Record<infer Key, DBFieldAttribute>71 Fields extends Record<infer Key, DBFieldAttribute>
77 ? {72 ? {
78 [K in Key as Fields[K]["returned"] extends false73 [K in Key as Fields[K]["returned"] extends false
79 ? never74 ? never
80 : Fields[K]["required"] extends false75 : Fields[K]["required"] extends false
81 ? Fields[K]["defaultValue"] extends76 ? Fields[K]["defaultValue"] extends boolean | string | number | Date
82 | boolean
83 | string
84 | number
85 | Date
86 ? K77 ? K
87 : never78 : never
88 : K]: InferDBFieldOutput<Fields[K]>;79 : K]: InferDBFieldOutput<Fields[K]>;
@@ -90,11 +81,7 @@ export type InferDBFieldsOutput< · 1 unmodified line
90 [K in Key as Fields[K]["returned"] extends false81 [K in Key as Fields[K]["returned"] extends false
91 ? never82 ? never
92 : Fields[K]["required"] extends false83 : Fields[K]["required"] extends false
93 ? Fields[K]["defaultValue"] extends84 ? Fields[K]["defaultValue"] extends boolean | string | number | Date
94 | boolean
95 | string
96 | number
97 | Date
98 ? never85 ? never
99 : K86 : K
100 : never]?: InferDBFieldOutput<Fields[K]> | null;87 : never]?: InferDBFieldOutput<Fields[K]> | null;
@@ -131,12 +118,7 @@ export type DBFieldAttributeConfig = { · 30 unmodified lines
131 model: string;118 model: string;
132 field: string;119 field: string;
133 /** @default "cascade" */120 /** @default "cascade" */
134 onDelete?:121 onDelete?: "no action" | "restrict" | "cascade" | "set null" | "set default";
135 | "no action"
136 | "restrict"
137 | "cascade"
138 | "set null"
139 | "set default";
140 }122 }
141 | undefined;123 | undefined;
142 unique?: boolean | undefined;124 unique?: boolean | undefined;
packages/core/storage-core/src/typed.ts+4−14
@@ -18,20 +18,12 @@ · 17 unmodified lines
18import type { Effect } from "effect";18import type { Effect } from "effect";
19 19
20import type { DBAdapter, StorageFailure, Where } from "./adapter";20import type { DBAdapter, StorageFailure, Where } from "./adapter";
21import type {21import type { DBSchema, InferDBFieldsInput, InferDBFieldsOutput } from "./schema";
22 DBSchema,
23 InferDBFieldsInput,
24 InferDBFieldsOutput,
25} from "./schema";
26 22
27type RowInput<S extends DBSchema, M extends keyof S> = InferDBFieldsInput<23type RowInput<S extends DBSchema, M extends keyof S> = InferDBFieldsInput<S[M]["fields"]> &
28 S[M]["fields"]
29> &
30 Record<string, unknown>;24 Record<string, unknown>;
31 25
32type RowOutput<S extends DBSchema, M extends keyof S> = InferDBFieldsOutput<26type RowOutput<S extends DBSchema, M extends keyof S> = InferDBFieldsOutput<S[M]["fields"]> &
33 S[M]["fields"]
34> &
35 Record<string, unknown>;27 Record<string, unknown>;
36 28
37/**29/**
@@ -103,9 +95,7 @@ export interface TypedAdapter<S extends DBSchema, E = StorageFailure> { · 65 unmodified lines
103 * this is a typed re-export of the adapter's methods. Pass the schema as95 * this is a typed re-export of the adapter's methods. Pass the schema as
104 * a type parameter; the adapter argument is the normal untyped one.96 * a type parameter; the adapter argument is the normal untyped one.
105 */97 */
106export const typedAdapter = <S extends DBSchema>(98export const typedAdapter = <S extends DBSchema>(adapter: DBAdapter): TypedAdapter<S> => ({
107 adapter: DBAdapter,
108): TypedAdapter<S> => ({
109 raw: adapter,99 raw: adapter,
110 create: adapter.create as TypedAdapter<S>["create"],100 create: adapter.create as TypedAdapter<S>["create"],
111 createMany: adapter.createMany as TypedAdapter<S>["createMany"],101 createMany: adapter.createMany as TypedAdapter<S>["createMany"],
packages/core/storage-drizzle/src/adapter.test.ts+4−11
@@ -10,8 +10,7 @@ import { StorageError, UniqueViolationError } from "@executor-js/storage-core"; · 9 unmodified lines
10 10
11import { isTransientStorageError } from "./adapter";11import { isTransientStorageError } from "./adapter";
12 12
13const transientMsg =13const transientMsg = "[storage-drizzle] findOne select failed: Network connection lost.";
14 "[storage-drizzle] findOne select failed: Network connection lost.";
15 14
16describe("isTransientStorageError", () => {15describe("isTransientStorageError", () => {
17 it.for([16 it.for([
@@ -20,9 +19,7 @@ describe("isTransientStorageError", () => { · 2 unmodified lines
20 "[storage-drizzle] insert returning failed: ECONNRESET",19 "[storage-drizzle] insert returning failed: ECONNRESET",
21 "[storage-drizzle] findMany select failed: Connection terminated unexpectedly",20 "[storage-drizzle] findMany select failed: Connection terminated unexpectedly",
22 ])("returns true for %s", (message) => {21 ])("returns true for %s", (message) => {
23 expect(22 expect(isTransientStorageError(new StorageError({ message, cause: null }))).toBe(true);
24 isTransientStorageError(new StorageError({ message, cause: null })),
25 ).toBe(true);
26 });23 });
27 24
28 it("returns false for unique violation", () => {25 it("returns false for unique violation", () => {
@@ -52,9 +49,7 @@ describe("transient retry policy", () => { · 23 unmodified lines
52 const result = yield* Effect.suspend(() => {49 const result = yield* Effect.suspend(() => {
53 calls++;50 calls++;
54 if (calls < 3) {51 if (calls < 3) {
55 return Effect.fail(52 return Effect.fail(new StorageError({ message: transientMsg, cause: null }));
56 new StorageError({ message: transientMsg, cause: null }),
57 );
58 }53 }
59 return Effect.succeed("ok");54 return Effect.succeed("ok");
60 }).pipe(Effect.retry(retryPolicy));55 }).pipe(Effect.retry(retryPolicy));
@@ -70,9 +65,7 @@ describe("transient retry policy", () => { · 9 unmodified lines
70 const exit = yield* Effect.exit(65 const exit = yield* Effect.exit(
71 Effect.suspend(() => {66 Effect.suspend(() => {
72 calls++;67 calls++;
73 return Effect.fail(68 return Effect.fail(new StorageError({ message: transientMsg, cause: null }));
74 new StorageError({ message: transientMsg, cause: null }),
75 );
76 }).pipe(Effect.retry(retryPolicy)),69 }).pipe(Effect.retry(retryPolicy)),
77 );70 );
78 71
packages/core/storage-file/src/adapter.ts+2−6
@@ -28,16 +28,12 @@ export interface MakeSqliteAdapterOptions { · 27 unmodified lines
28 readonly generateId?: () => string;28 readonly generateId?: () => string;
29}29}
30 30
31export const makeSqliteAdapter = (31export const makeSqliteAdapter = (options: MakeSqliteAdapterOptions): DBAdapter =>
32 options: MakeSqliteAdapterOptions,
33): DBAdapter =>
34 drizzleAdapter({32 drizzleAdapter({
35 db: options.db,33 db: options.db,
36 schema: options.schema,34 schema: options.schema,
37 provider: "sqlite",35 provider: "sqlite",
38 adapterId: options.adapterId ?? "sqlite",36 adapterId: options.adapterId ?? "sqlite",
39 supportsTransaction: true,37 supportsTransaction: true,
40 customIdGenerator: options.generateId38 customIdGenerator: options.generateId ? () => options.generateId!() : undefined,
41 ? () => options.generateId!()
42 : undefined,
43 });39 });
packages/core/storage-file/src/blob-store.ts+5−27
@@ -41,21 +41,14 @@ const wrapErr = · 40 unmodified lines
41 cause,41 cause,
42 });42 });
43 43
44export const makeSqliteBlobStore = (44export const makeSqliteBlobStore = (options: MakeSqliteBlobStoreOptions): BlobStore => ({
45 options: MakeSqliteBlobStoreOptions,
46): BlobStore => ({
47 get: (namespace, key) =>45 get: (namespace, key) =>
48 Effect.try({46 Effect.try({
49 try: () =>47 try: () =>
50 options.db48 options.db
51 .select({ value: blobTable.value })49 .select({ value: blobTable.value })
52 .from(blobTable)50 .from(blobTable)
53 .where(51 .where(and(eq(blobTable.namespace, namespace), eq(blobTable.key, key)))
54 and(
55 eq(blobTable.namespace, namespace),
56 eq(blobTable.key, key),
57 ),
58 )
59 .limit(1)52 .limit(1)
60 .all() as ReadonlyArray<{ value: string }>,53 .all() as ReadonlyArray<{ value: string }>,
61 catch: wrapErr("get"),54 catch: wrapErr("get"),
@@ -72,12 +65,7 @@ export const makeSqliteBlobStore = ( · 10 unmodified lines
72 value: blobTable.value,65 value: blobTable.value,
73 })66 })
74 .from(blobTable)67 .from(blobTable)
75 .where(68 .where(and(inArray(blobTable.namespace, [...namespaces]), eq(blobTable.key, key)))
76 and(
77 inArray(blobTable.namespace, [...namespaces]),
78 eq(blobTable.key, key),
79 ),
80 )
81 .all() as ReadonlyArray<{ namespace: string; value: string }>,69 .all() as ReadonlyArray<{ namespace: string; value: string }>,
82 catch: wrapErr("getMany"),70 catch: wrapErr("getMany"),
83 }).pipe(71 }).pipe(
@@ -107,12 +95,7 @@ export const makeSqliteBlobStore = ( · 23 unmodified lines
107 try: () =>95 try: () =>
108 options.db96 options.db
109 .delete(blobTable)97 .delete(blobTable)
110 .where(98 .where(and(eq(blobTable.namespace, namespace), eq(blobTable.key, key)))
111 and(
112 eq(blobTable.namespace, namespace),
113 eq(blobTable.key, key),
114 ),
115 )
116 .run(),99 .run(),
117 catch: wrapErr("delete"),100 catch: wrapErr("delete"),
118 }).pipe(Effect.asVoid),101 }).pipe(Effect.asVoid),
@@ -123,12 +106,7 @@ export const makeSqliteBlobStore = ( · 4 unmodified lines
123 options.db106 options.db
124 .select({ one: drizzleSql<number>`1`.as("one") })107 .select({ one: drizzleSql<number>`1`.as("one") })
125 .from(blobTable)108 .from(blobTable)
126 .where(109 .where(and(eq(blobTable.namespace, namespace), eq(blobTable.key, key)))
127 and(
128 eq(blobTable.namespace, namespace),
129 eq(blobTable.key, key),
130 ),
131 )
132 .limit(1)110 .limit(1)
133 .all() as ReadonlyArray<{ one: number }>,111 .all() as ReadonlyArray<{ one: number }>,
134 catch: wrapErr("has"),112 catch: wrapErr("has"),
packages/core/storage-file/src/index.test.ts+3−16
@@ -2,19 +2,10 @@ import { Effect } from "effect"; · 1 unmodified line
2import Database from "better-sqlite3";2import Database from "better-sqlite3";
3import { drizzle } from "drizzle-orm/better-sqlite3";3import { drizzle } from "drizzle-orm/better-sqlite3";
4import { relations, sql } from "drizzle-orm";4import { relations, sql } from "drizzle-orm";
5import {5import { sqliteTable, text, real, integer, getTableConfig } from "drizzle-orm/sqlite-core";
6 sqliteTable,
7 text,
8 real,
9 integer,
10 getTableConfig,
11} from "drizzle-orm/sqlite-core";
12 6
13import type { DBAdapter } from "@executor-js/storage-core";7import type { DBAdapter } from "@executor-js/storage-core";
14import {8import { conformanceSchema, runAdapterConformance } from "@executor-js/storage-core/testing";
15 conformanceSchema,
16 runAdapterConformance,
17} from "@executor-js/storage-core/testing";
18 9
19import { makeSqliteAdapter } from "./index";10import { makeSqliteAdapter } from "./index";
20 11
@@ -91,11 +82,7 @@ const bootstrapTables = ( · 70 unmodified lines
91 if (col.isUnique) parts.push("UNIQUE");82 if (col.isUnique) parts.push("UNIQUE");
92 return parts.join(" ");83 return parts.join(" ");
93 });84 });
94 db.run(85 db.run(sql.raw(`CREATE TABLE IF NOT EXISTS "${config.name}" (${cols.join(", ")})`));
95 sql.raw(
96 `CREATE TABLE IF NOT EXISTS "${config.name}" (${cols.join(", ")})`,
97 ),
98 );
99 } catch {86 } catch {
100 // Not a table (e.g. relations) — skip87 // Not a table (e.g. relations) — skip
101 }88 }
packages/core/storage-postgres/src/adapter.ts+2−6
@@ -25,16 +25,12 @@ export interface MakePostgresAdapterOptions { · 24 unmodified lines
25 readonly generateId?: () => string;25 readonly generateId?: () => string;
26}26}
27 27
28export const makePostgresAdapter = (28export const makePostgresAdapter = (options: MakePostgresAdapterOptions): DBAdapter =>
29 options: MakePostgresAdapterOptions,
30): DBAdapter =>
31 drizzleAdapter({29 drizzleAdapter({
32 db: options.db,30 db: options.db,
33 schema: options.schema,31 schema: options.schema,
34 provider: "pg",32 provider: "pg",
35 adapterId: options.adapterId ?? "postgres",33 adapterId: options.adapterId ?? "postgres",
36 supportsTransaction: true,34 supportsTransaction: true,
37 customIdGenerator: options.generateId35 customIdGenerator: options.generateId ? () => options.generateId!() : undefined,
38 ? () => options.generateId!()
39 : undefined,
40 });36 });
packages/core/storage-postgres/src/blob-store.ts+5−27
@@ -42,21 +42,14 @@ const wrapErr = · 41 unmodified lines
42 cause,42 cause,
43 });43 });
44 44
45export const makePostgresBlobStore = (45export const makePostgresBlobStore = (options: MakePostgresBlobStoreOptions): BlobStore => ({
46 options: MakePostgresBlobStoreOptions,
47): BlobStore => ({
48 get: (namespace, key) =>46 get: (namespace, key) =>
49 Effect.tryPromise({47 Effect.tryPromise({
50 try: async () => {48 try: async () => {
51 const rows = await options.db49 const rows = await options.db
52 .select({ value: blobTable.value })50 .select({ value: blobTable.value })
53 .from(blobTable)51 .from(blobTable)
54 .where(52 .where(and(eq(blobTable.namespace, namespace), eq(blobTable.key, key)))
55 and(
56 eq(blobTable.namespace, namespace),
57 eq(blobTable.key, key),
58 ),
59 )
60 .limit(1);53 .limit(1);
61 return rows[0]?.value ?? null;54 return rows[0]?.value ?? null;
62 },55 },
@@ -73,12 +66,7 @@ export const makePostgresBlobStore = ( · 10 unmodified lines
73 value: blobTable.value,66 value: blobTable.value,
74 })67 })
75 .from(blobTable)68 .from(blobTable)
76 .where(69 .where(and(inArray(blobTable.namespace, [...namespaces]), eq(blobTable.key, key)));
77 and(
78 inArray(blobTable.namespace, [...namespaces]),
79 eq(blobTable.key, key),
80 ),
81 );
82 const out = new Map<string, string>();70 const out = new Map<string, string>();
83 for (const row of rows) out.set(row.namespace, row.value);71 for (const row of rows) out.set(row.namespace, row.value);
84 return out as ReadonlyMap<string, string>;72 return out as ReadonlyMap<string, string>;
@@ -103,12 +91,7 @@ export const makePostgresBlobStore = ( · 18 unmodified lines
103 try: async () => {91 try: async () => {
104 await options.db92 await options.db
105 .delete(blobTable)93 .delete(blobTable)
106 .where(94 .where(and(eq(blobTable.namespace, namespace), eq(blobTable.key, key)));
107 and(
108 eq(blobTable.namespace, namespace),
109 eq(blobTable.key, key),
110 ),
111 );
112 },95 },
113 catch: wrapErr("delete"),96 catch: wrapErr("delete"),
114 }),97 }),
@@ -118,12 +101,7 @@ export const makePostgresBlobStore = ( · 3 unmodified lines
118 const rows = await options.db101 const rows = await options.db
119 .select({ key: blobTable.key })102 .select({ key: blobTable.key })
120 .from(blobTable)103 .from(blobTable)
121 .where(104 .where(and(eq(blobTable.namespace, namespace), eq(blobTable.key, key)))
122 and(
123 eq(blobTable.namespace, namespace),
124 eq(blobTable.key, key),
125 ),
126 )
127 .limit(1);105 .limit(1);
128 return rows.length > 0;106 return rows.length > 0;
129 },107 },
packages/core/storage-postgres/src/index.test.ts+3−8
@@ -22,18 +22,13 @@ import { · 21 unmodified lines
22} from "drizzle-orm/pg-core";22} from "drizzle-orm/pg-core";
23 23
24import type { DBAdapter, StorageFailure } from "@executor-js/storage-core";24import type { DBAdapter, StorageFailure } from "@executor-js/storage-core";
25import {25import { conformanceSchema, runAdapterConformance } from "@executor-js/storage-core/testing";
26 conformanceSchema,
27 runAdapterConformance,
28} from "@executor-js/storage-core/testing";
29 26
30import { makePostgresAdapter } from "./index";27import { makePostgresAdapter } from "./index";
31 28
32const url = "postgresql://postgres:postgres@127.0.0.1:5435/postgres";29const url = "postgresql://postgres:postgres@127.0.0.1:5435/postgres";
33 30
34class PostgresTestDatabaseError extends Data.TaggedError(31class PostgresTestDatabaseError extends Data.TaggedError("PostgresTestDatabaseError")<{
35 "PostgresTestDatabaseError",
36)<{
37 readonly message: string;32 readonly message: string;
38 readonly cause: unknown;33 readonly cause: unknown;
39}> {}34}> {}
@@ -153,7 +148,7 @@ const resetTables = Effect.gen(function* () { · 113 unmodified lines
153 try: () =>148 try: () =>
154 sql`DROP TABLE IF EXISTS "source", "tag", "source_tag", "with_defaults", "blob" CASCADE`.then(149 sql`DROP TABLE IF EXISTS "source", "tag", "source_tag", "with_defaults", "blob" CASCADE`.then(
155 () => undefined,150 () => undefined,
156 ),151 ),
157 catch: (cause) =>152 catch: (cause) =>
158 new PostgresTestDatabaseError({153 new PostgresTestDatabaseError({
159 message: "failed to reset postgres conformance tables",154 message: "failed to reset postgres conformance tables",
packages/core/storage-postgres/src/index.ts+1−4
@@ -5,9 +5,6 @@ · 4 unmodified lines
5// wrapper around @executor-js/storage-drizzle.5// wrapper around @executor-js/storage-drizzle.
6// ---------------------------------------------------------------------------6// ---------------------------------------------------------------------------
7 7
8export {8export { makePostgresAdapter, type MakePostgresAdapterOptions } from "./adapter";
9 makePostgresAdapter,
10 type MakePostgresAdapterOptions,
11} from "./adapter";
12 9
13export { makePostgresBlobStore, blobTable } from "./blob-store";10export { makePostgresBlobStore, blobTable } from "./blob-store";
packages/core/vite-plugin/package.json+3−3
@@ -44,9 +44,6 @@ · 43 unmodified lines
44 "jiti": "^2.6.1",44 "jiti": "^2.6.1",
45 "jsonc-parser": "^3.3.1"45 "jsonc-parser": "^3.3.1"
46 },46 },
47 "peerDependencies": {
48 "vite": "catalog:"
49 },
50 "devDependencies": {47 "devDependencies": {
51 "@effect/vitest": "catalog:",48 "@effect/vitest": "catalog:",
52 "@types/node": "catalog:",49 "@types/node": "catalog:",
@@ -54,5 +51,8 @@ · 1 unmodified line
54 "typescript": "catalog:",51 "typescript": "catalog:",
55 "vite": "catalog:",52 "vite": "catalog:",
56 "vitest": "catalog:"53 "vitest": "catalog:"
54 },
55 "peerDependencies": {
56 "vite": "catalog:"
57 }57 }
58}58}
packages/core/vite-plugin/src/index.ts+8−23
@@ -56,10 +56,7 @@ const readJsoncPlugins = (path: string): readonly string[] => { · 55 unmodified lines
56 }56 }
57};57};
58 58
59const tryResolveClient = (59const tryResolveClient = (packageName: string, fromDir: string): string | null => {
60 packageName: string,
61 fromDir: string,
62): string | null => {
63 const require = createRequire(resolvePath(fromDir, "_anchor.js"));60 const require = createRequire(resolvePath(fromDir, "_anchor.js"));
64 try {61 try {
65 return require.resolve(`${packageName}/client`);62 return require.resolve(`${packageName}/client`);
@@ -83,9 +80,7 @@ interface ExecutorVitePluginOptions { · 17 unmodified lines
83 readonly jsoncPath?: string;80 readonly jsoncPath?: string;
84}81}
85 82
86export default function executorVitePlugin(83export default function executorVitePlugin(options: ExecutorVitePluginOptions = {}): Plugin {
87 options: ExecutorVitePluginOptions = {},
88): Plugin {
89 let projectRoot: string = process.cwd();84 let projectRoot: string = process.cwd();
90 let resolvedConfigPath: string | null = null;85 let resolvedConfigPath: string | null = null;
91 let resolvedJsoncPath: string | null = null;86 let resolvedJsoncPath: string | null = null;
@@ -93,13 +88,9 @@ export default function executorVitePlugin( · 1 unmodified line
93 88
94 const resolveConfigPath = (): string | null => {89 const resolveConfigPath = (): string | null => {
95 if (resolvedConfigPath) return resolvedConfigPath;90 if (resolvedConfigPath) return resolvedConfigPath;
96 const candidates = options.configPath91 const candidates = options.configPath ? [options.configPath] : DEFAULT_CONFIG_CANDIDATES;
97 ? [options.configPath]
98 : DEFAULT_CONFIG_CANDIDATES;
99 for (const candidate of candidates) {92 for (const candidate of candidates) {
100 const abs = isAbsolute(candidate)93 const abs = isAbsolute(candidate) ? candidate : resolvePath(projectRoot, candidate);
101 ? candidate
102 : resolvePath(projectRoot, candidate);
103 if (existsSync(abs)) {94 if (existsSync(abs)) {
104 resolvedConfigPath = abs;95 resolvedConfigPath = abs;
105 return abs;96 return abs;
@@ -110,13 +101,9 @@ export default function executorVitePlugin( · 4 unmodified lines
110 101
111 const resolveJsoncPath = (): string | null => {102 const resolveJsoncPath = (): string | null => {
112 if (resolvedJsoncPath) return resolvedJsoncPath;103 if (resolvedJsoncPath) return resolvedJsoncPath;
113 const candidates = options.jsoncPath104 const candidates = options.jsoncPath ? [options.jsoncPath] : DEFAULT_JSONC_CANDIDATES;
114 ? [options.jsoncPath]
115 : DEFAULT_JSONC_CANDIDATES;
116 for (const candidate of candidates) {105 for (const candidate of candidates) {
117 const abs = isAbsolute(candidate)106 const abs = isAbsolute(candidate) ? candidate : resolvePath(projectRoot, candidate);
118 ? candidate
119 : resolvePath(projectRoot, candidate);
120 if (existsSync(abs)) {107 if (existsSync(abs)) {
121 resolvedJsoncPath = abs;108 resolvedJsoncPath = abs;
122 return abs;109 return abs;
@@ -212,8 +199,7 @@ export default function executorVitePlugin( · 89 unmodified lines
212 }199 }
213 200
214 cachedSource =201 cachedSource =
215 `${lines.join("\n")}\n` +202 `${lines.join("\n")}\n` + `export const plugins = [${exportExpressions.join(", ")}];\n`;
216 `export const plugins = [${exportExpressions.join(", ")}];\n`;
217 return cachedSource;203 return cachedSource;
218 };204 };
219 205
@@ -235,8 +221,7 @@ export default function executorVitePlugin( · 15 unmodified lines
235 const configPath = resolveConfigPath();221 const configPath = resolveConfigPath();
236 const jsoncPath = resolveJsoncPath();222 const jsoncPath = resolveJsoncPath();
237 const isWatched =223 const isWatched =
238 (configPath && ctx.file === configPath) ||224 (configPath && ctx.file === configPath) || (jsoncPath && ctx.file === jsoncPath);
239 (jsoncPath && ctx.file === jsoncPath);
240 if (!isWatched) return undefined;225 if (!isWatched) return undefined;
241 cachedSource = null;226 cachedSource = null;
242 const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);227 const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);
packages/hosts/mcp/src/stdio-integration.test.ts+35−33
@@ -11,39 +11,41 @@ const cliEntry = resolve(repoRoot, "apps/cli/src/main.ts"); · 10 unmodified lines
11const testScope = resolve(repoRoot, "apps/local");11const testScope = resolve(repoRoot, "apps/local");
12 12
13describe("MCP stdio integration", () => {13describe("MCP stdio integration", () => {
14 it.effect("execute tool returns result over stdio transport", () =>14 it.effect(
15 Effect.gen(function* () {15 "execute tool returns result over stdio transport",
16 // Fresh temp dir so the test doesn't migrate against the developer's16 () =>
17 // real ~/.executor/data.db.17 Effect.gen(function* () {
18 const dataDir = mkdtempSync(join(tmpdir(), "executor-mcp-test-"));18 // Fresh temp dir so the test doesn't migrate against the developer's
19 19 // real ~/.executor/data.db.
20 const transport = new StdioClientTransport({20 const dataDir = mkdtempSync(join(tmpdir(), "executor-mcp-test-"));
21 command: "bun",21
22 args: ["run", cliEntry, "mcp", "--scope", testScope],22 const transport = new StdioClientTransport({
23 env: { ...process.env, EXECUTOR_DATA_DIR: dataDir },23 command: "bun",
24 });24 args: ["run", cliEntry, "mcp", "--scope", testScope],
25 25 env: { ...process.env, EXECUTOR_DATA_DIR: dataDir },
26 const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });26 });
27 27
28 yield* Effect.acquireRelease(28 const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
29 Effect.promise(() => client.connect(transport)),29
30 () => Effect.promise(() => transport.close()),30 yield* Effect.acquireRelease(
31 );31 Effect.promise(() => client.connect(transport)),
32 32 () => Effect.promise(() => transport.close()),
33 const { tools } = yield* Effect.promise(() => client.listTools());33 );
34 expect(tools.map((t) => t.name)).toContain("execute");34
35 35 const { tools } = yield* Effect.promise(() => client.listTools());
36 const result = yield* Effect.promise(() =>36 expect(tools.map((t) => t.name)).toContain("execute");
37 client.callTool({37
38 name: "execute",38 const result = yield* Effect.promise(() =>
39 arguments: { code: "return 2+2" },39 client.callTool({
40 }),40 name: "execute",
41 );41 arguments: { code: "return 2+2" },
42 42 }),
43 const text = (result.content as Array<{ type: string; text: string }>)[0]?.text;43 );
44 expect(text).toContain("4");44
45 expect(result.isError).toBeFalsy();45 const text = (result.content as Array<{ type: string; text: string }>)[0]?.text;
46 }).pipe(Effect.scoped),46 expect(text).toContain("4");
47 expect(result.isError).toBeFalsy();
48 }).pipe(Effect.scoped),
47 { timeout: 30_000 },49 { timeout: 30_000 },
48 );50 );
49});51});
packages/kernel/core/README.md+1−5
@@ -17,11 +17,7 @@ npm install @executor-js/codemode-core · 16 unmodified lines
17Implement a runtime that satisfies `CodeExecutor`:17Implement a runtime that satisfies `CodeExecutor`:
18 18
19```ts19```ts
20import type {20import type { CodeExecutor, ExecuteResult, SandboxToolInvoker } from "@executor-js/codemode-core";
21 CodeExecutor,
22 ExecuteResult,
23 SandboxToolInvoker,
24} from "@executor-js/codemode-core";
25import { Effect } from "effect";21import { Effect } from "effect";
26 22
27export const makeMyRuntime = (): CodeExecutor => ({23export const makeMyRuntime = (): CodeExecutor => ({
packages/kernel/core/src/code-recovery.test.ts+6−13
@@ -57,7 +57,9 @@ describe("recoverExecutionBody", () => { · 56 unmodified lines
57 });57 });
58 58
59 it("strips markdown code fences", () => {59 it("strips markdown code fences", () => {
60 expect(recoverExecutionBody("```js\nasync () => 42\n```")).toBe(wrapCallableBody("async () => 42"));60 expect(recoverExecutionBody("```js\nasync () => 42\n```")).toBe(
61 wrapCallableBody("async () => 42"),
62 );
61 });63 });
62 64
63 it("strips typescript fences", () => {65 it("strips typescript fences", () => {
@@ -121,13 +123,7 @@ describe("recoverExecutionBody", () => { · 57 unmodified lines
121 });123 });
122 124
123 it("extracts code from prose wrapped fences", () => {125 it("extracts code from prose wrapped fences", () => {
124 const code = [126 const code = ["Sure, here's the code:", "", "```ts", "async () => 42", "```"].join("\n");
125 "Sure, here's the code:",
126 "",
127 "```ts",
128 "async () => 42",
129 "```",
130 ].join("\n");
131 127
132 expect(recoverExecutionBody(code)).toBe(wrapCallableBody("async () => 42"));128 expect(recoverExecutionBody(code)).toBe(wrapCallableBody("async () => 42"));
133 });129 });
@@ -142,10 +138,7 @@ describe("recoverExecutionBody", () => { · 8 unmodified lines
142 });138 });
143 139
144 it("handles block comments before export default", () => {140 it("handles block comments before export default", () => {
145 const code = [141 const code = ["/* generated by the model */", "export default async () => 42"].join("\n");
146 "/* generated by the model */",
147 "export default async () => 42",
148 ].join("\n");
149 142
150 expect(recoverExecutionBody(code)).toBe(wrapCallableBody("async () => 42"));143 expect(recoverExecutionBody(code)).toBe(wrapCallableBody("async () => 42"));
151 });144 });
@@ -199,7 +192,7 @@ describe("recoverExecutionBody", () => { · 47 unmodified lines
199 " return { count: matches.length, top };",192 " return { count: matches.length, top };",
200 "} catch (error) {",193 "} catch (error) {",
201 " return {",194 " return {",
202 ' error: error instanceof Error ? error.message : String(error),',195 " error: error instanceof Error ? error.message : String(error),",
203 " query,",196 " query,",
204 " };",197 " };",
205 "}",198 "}",
packages/kernel/core/src/code-recovery.ts+14−10
@@ -1,8 +1,7 @@
1import { parse } from "@babel/parser";1import { parse } from "@babel/parser";
2 2
3const FENCED_CODE_BLOCK = /```(?:[^\n`]*)?\s*\n([\s\S]*?)```/i;3const FENCED_CODE_BLOCK = /```(?:[^\n`]*)?\s*\n([\s\S]*?)```/i;
4const FUNCTION_DECLARATION =4const FUNCTION_DECLARATION = /^(?:async\s+)?function(?:\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?\s*\(/;
5 /^(?:async\s+)?function(?:\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?\s*\(/;
6const CALLABLE_ERROR = "Code must evaluate to a function";5const CALLABLE_ERROR = "Code must evaluate to a function";
7 6
8const extractCandidateSource = (code: string): string => {7const extractCandidateSource = (code: string): string => {
@@ -47,7 +46,9 @@ const unwrapExpression = (expression: { type: string; expression?: unknown }): u · 38 unmodified lines
47 case "TSTypeAssertion":46 case "TSTypeAssertion":
48 case "TSNonNullExpression":47 case "TSNonNullExpression":
49 case "TSInstantiationExpression":48 case "TSInstantiationExpression":
50 return expression.expression ? unwrapExpression(expression.expression as { type: string }) : expression;49 return expression.expression
50 ? unwrapExpression(expression.expression as { type: string })
51 : expression;
51 default:52 default:
52 return expression;53 return expression;
53 }54 }
@@ -76,11 +77,11 @@ const renderExportDefaultBody = ( · 22 unmodified lines
76};77};
77 78
78type ExportDefaultDeclarationNode = {79type ExportDefaultDeclarationNode = {
79 type: string;80 type: string;
80 start?: number | null;81 start?: number | null;
81 end?: number | null;82 end?: number | null;
82 id?: { name?: string | null } | null;83 id?: { name?: string | null } | null;
83 expression?: unknown;84 expression?: unknown;
84};85};
85 86
86const renderParsedBody = (source: string): string => {87const renderParsedBody = (source: string): string => {
@@ -99,10 +100,13 @@ const renderParsedBody = (source: string): string => { · 12 unmodified lines
99 100
100 switch (statement.type) {101 switch (statement.type) {
101 case "ExpressionStatement": {102 case "ExpressionStatement": {
102 const expression = unwrapExpression(statement.expression as { type: string; expression?: unknown }) as {103 const expression = unwrapExpression(
104 statement.expression as { type: string; expression?: unknown },
105 ) as {
103 type?: string;106 type?: string;
104 };107 };
105 return expression?.type === "ArrowFunctionExpression" || expression?.type === "FunctionExpression"108 return expression?.type === "ArrowFunctionExpression" ||
109 expression?.type === "FunctionExpression"
106 ? wrapCallableBody(source)110 ? wrapCallableBody(source)
107 : source;111 : source;
108 }112 }
packages/kernel/core/src/strip-types.test.ts+4−2
@@ -6,7 +6,7 @@ describe("stripTypeScript", () => { · 5 unmodified lines
6 it("removes variable type annotations", () => {6 it("removes variable type annotations", () => {
7 const out = stripTypeScript('const x: string = "hello"; return x;');7 const out = stripTypeScript('const x: string = "hello"; return x;');
8 expect(out).not.toContain(": string");8 expect(out).not.toContain(": string");
9 expect(out).toContain('const x');9 expect(out).toContain("const x");
10 expect(out).toContain('"hello"');10 expect(out).toContain('"hello"');
11 });11 });
12 12
@@ -30,7 +30,9 @@ describe("stripTypeScript", () => { · 17 unmodified lines
30 });30 });
31 31
32 it("removes interface declarations", () => {32 it("removes interface declarations", () => {
33 const out = stripTypeScript("interface User { name: string; } const u = { name: 'a' }; return u;");33 const out = stripTypeScript(
34 "interface User { name: string; } const u = { name: 'a' }; return u;",
35 );
34 expect(out).not.toContain("interface User");36 expect(out).not.toContain("interface User");
35 expect(out).not.toContain(": string");37 expect(out).not.toContain(": string");
36 expect(() => new Function(out)).not.toThrow();38 expect(() => new Function(out)).not.toThrow();
packages/kernel/runtime-deno-subprocess/src/index.test.ts+19−17
@@ -167,23 +167,25 @@ describe.skipIf(!isDenoAvailable())("runtime-deno-subprocess", () => { · 166 unmodified lines
167 );167 );
168 168
169 // Skipped in CI and on Windows — outbound HTTPS may be blocked by firewall/policy169 // Skipped in CI and on Windows — outbound HTTPS may be blocked by firewall/policy
170 it.effect.skipIf(process.env["CI"] === "true" || process.platform === "win32")("network access can be allowed via permissions", () =>170 it.effect.skipIf(process.env["CI"] === "true" || process.platform === "win32")(
171 Effect.gen(function* () {171 "network access can be allowed via permissions",
172 const executor = makeDenoSubprocessExecutor({172 () =>
173 permissions: {173 Effect.gen(function* () {
174 allowNet: true,174 const executor = makeDenoSubprocessExecutor({
175 },175 permissions: {
176 });176 allowNet: true,
177 const toolInvoker = makeTestInvoker({});177 },
178 178 });
179 const output = yield* executor.execute(179 const toolInvoker = makeTestInvoker({});
180 ['const res = await fetch("https://example.com");', "return res.status;"].join("\n"),180
181 toolInvoker,181 const output = yield* executor.execute(
182 );182 ['const res = await fetch("https://example.com");', "return res.status;"].join("\n"),
183 183 toolInvoker,
184 expect(output.result).toBe(200);184 );
185 expect(output.error).toBeUndefined();185
186 }),186 expect(output.result).toBe(200);
187 expect(output.error).toBeUndefined();
188 }),
187 );189 );
188 190
189 it.effect("multiple sequential tool calls work correctly", () =>191 it.effect("multiple sequential tool calls work correctly", () =>
packages/kernel/runtime-deno-subprocess/src/index.ts+1−3
@@ -92,9 +92,7 @@ const defaultDenoExecutable = (): string => { · 91 unmodified lines
92 const isWindows = process.platform === "win32";92 const isWindows = process.platform === "win32";
93 const home = (process.env.HOME || process.env.USERPROFILE)?.trim();93 const home = (process.env.HOME || process.env.USERPROFILE)?.trim();
94 if (home) {94 if (home) {
95 const installedPath = isWindows95 const installedPath = isWindows ? `${home}\\.deno\\bin\\deno.exe` : `${home}/.deno/bin/deno`;
96 ? `${home}\\.deno\\bin\\deno.exe`
97 : `${home}/.deno/bin/deno`;
98 const result = spawnSync(installedPath, ["--version"], {96 const result = spawnSync(installedPath, ["--version"], {
99 stdio: "ignore",97 stdio: "ignore",
100 timeout: 5000,98 timeout: 5000,
packages/kernel/runtime-quickjs/README.md+5−5
@@ -33,11 +33,11 @@ const engine = createExecutionEngine({ · 32 unmodified lines
33 33
34### Options34### Options
35 35
36| Option | Default | Description |36| Option | Default | Description |
37| ------------------- | ---------- | -------------------------------------------- |37| ------------------- | ---------- | --------------------------------- |
38| `timeoutMs` | `5_000` | Max wall-clock time per execution |38| `timeoutMs` | `5_000` | Max wall-clock time per execution |
39| `memoryLimitBytes` | `64 * 1MB` | Max memory the VM can allocate |39| `memoryLimitBytes` | `64 * 1MB` | Max memory the VM can allocate |
40| `maxStackSizeBytes` | `1 * 1MB` | Max call-stack depth |40| `maxStackSizeBytes` | `1 * 1MB` | Max call-stack depth |
41 41
42### Swapping the QuickJS build42### Swapping the QuickJS build
43 43
packages/plugins/example/package.json+8−8
@@ -52,14 +52,6 @@ · 51 unmodified lines
52 "dependencies": {52 "dependencies": {
53 "@executor-js/sdk": "workspace:*"53 "@executor-js/sdk": "workspace:*"
54 },54 },
55 "peerDependencies": {
56 "react": "catalog:"
57 },
58 "peerDependenciesMeta": {
59 "react": {
60 "optional": true
61 }
62 },
63 "devDependencies": {55 "devDependencies": {
64 "@effect/vitest": "catalog:",56 "@effect/vitest": "catalog:",
65 "@types/node": "catalog:",57 "@types/node": "catalog:",
@@ -69,5 +61,13 @@ · 3 unmodified lines
69 "tsup": "catalog:",61 "tsup": "catalog:",
70 "typescript": "catalog:",62 "typescript": "catalog:",
71 "vitest": "catalog:"63 "vitest": "catalog:"
64 },
65 "peerDependencies": {
66 "react": "catalog:"
67 },
68 "peerDependenciesMeta": {
69 "react": {
70 "optional": true
71 }
72 }72 }
73}73}
packages/plugins/example/src/client.tsx+2−8
@@ -11,11 +11,7 @@ · 10 unmodified lines
11// ---------------------------------------------------------------------------11// ---------------------------------------------------------------------------
12 12
13import { useState } from "react";13import { useState } from "react";
14import {14import { defineClientPlugin, createPluginAtomClient, useAtomSet } from "@executor-js/sdk/client";
15 defineClientPlugin,
16 createPluginAtomClient,
17 useAtomSet,
18} from "@executor-js/sdk/client";
19 15
20import { ExampleApi } from "./shared";16import { ExampleApi } from "./shared";
21 17
@@ -68,9 +64,7 @@ function ExamplePage() { · 46 unmodified lines
68 </button>64 </button>
69 </div>65 </div>
70 {result && (66 {result && (
71 <pre style={{ marginTop: "1rem", padding: "0.5rem", background: "#f5f5f5" }}>67 <pre style={{ marginTop: "1rem", padding: "0.5rem", background: "#f5f5f5" }}>{result}</pre>
72 {result}
73 </pre>
74 )}68 )}
75 </div>69 </div>
76 );70 );
packages/plugins/example/src/server.ts+1−7
@@ -11,13 +11,7 @@ · 10 unmodified lines
11// React and other browser-only deps live in `./client` — never here.11// React and other browser-only deps live in `./client` — never here.
12// ---------------------------------------------------------------------------12// ---------------------------------------------------------------------------
13 13
14import {14import { Context, definePlugin, Effect, HttpApi, HttpApiBuilder } from "@executor-js/sdk";
15 Context,
16 definePlugin,
17 Effect,
18 HttpApi,
19 HttpApiBuilder,
20} from "@executor-js/sdk";
21 15
22import { ExampleApi } from "./shared";16import { ExampleApi } from "./shared";
23 17
packages/plugins/google-discovery/src/api/handlers.test.ts+20−26
@@ -53,9 +53,7 @@ const WebHandler = Effect.acquireRelease( · 52 unmodified lines
53 Layer.provide(observabilityMiddleware(Api)),53 Layer.provide(observabilityMiddleware(Api)),
54 Layer.provide(UnusedExecutor),54 Layer.provide(UnusedExecutor),
55 Layer.provide(UnusedExecutionEngine),55 Layer.provide(UnusedExecutionEngine),
56 Layer.provide(56 Layer.provide(Layer.succeed(GoogleDiscoveryExtensionService, failingExtension)),
57 Layer.succeed(GoogleDiscoveryExtensionService, failingExtension),
58 ),
59 Layer.provideMerge(HttpServer.layerServices),57 Layer.provideMerge(HttpServer.layerServices),
60 Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })),58 Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })),
61 ),59 ),
@@ -98,9 +96,7 @@ describe("GoogleDiscoveryHandlers", () => { · 36 unmodified lines
98 Layer.provide(observabilityMiddleware(Api)),96 Layer.provide(observabilityMiddleware(Api)),
99 Layer.provide(UnusedExecutor),97 Layer.provide(UnusedExecutor),
100 Layer.provide(UnusedExecutionEngine),98 Layer.provide(UnusedExecutionEngine),
101 Layer.provide(99 Layer.provide(Layer.succeed(GoogleDiscoveryExtensionService, extension)),
102 Layer.succeed(GoogleDiscoveryExtensionService, extension),
103 ),
104 Layer.provideMerge(HttpServer.layerServices),100 Layer.provideMerge(HttpServer.layerServices),
105 Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })),101 Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })),
106 ),102 ),
@@ -130,27 +126,25 @@ describe("GoogleDiscoveryHandlers", () => { · 23 unmodified lines
130 }),126 }),
131 );127 );
132 128
133 it.effect(129 it.effect("defect-returning methods produce an opaque InternalError, no leakage", () =>
134 "defect-returning methods produce an opaque InternalError, no leakage",130 Effect.gen(function* () {
135 () =>131 const web = yield* WebHandler;
136 Effect.gen(function* () {132 const response = yield* Effect.promise(() =>
137 const web = yield* WebHandler;133 web.handler(
138 const response = yield* Effect.promise(() =>134 new Request("http://localhost/scopes/scope_1/google-discovery/probe", {
139 web.handler(135 method: "POST",
140 new Request("http://localhost/scopes/scope_1/google-discovery/probe", {136 headers: { "content-type": "application/json" },
141 method: "POST",137 body: JSON.stringify({
142 headers: { "content-type": "application/json" },138 discoveryUrl: "https://example.googleapis.com/$discovery/rest?version=v1",
143 body: JSON.stringify({
144 discoveryUrl: "https://example.googleapis.com/$discovery/rest?version=v1",
145 }),
146 }),139 }),
147 HandlerContext,140 }),
148 ),141 HandlerContext,
149 );142 ),
143 );
150 144
151 expect(response.status).toBe(500);145 expect(response.status).toBe(500);
152 const body = yield* Effect.promise(() => response.text());146 const body = yield* Effect.promise(() => response.text());
153 expect(body).not.toContain("Not implemented");147 expect(body).not.toContain("Not implemented");
154 }),148 }),
155 );149 );
156});150});
packages/plugins/google-discovery/src/api/handlers.ts+4−5
@@ -2,10 +2,7 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"; · 1 unmodified line
2import { Context, Effect } from "effect";2import { Context, Effect } from "effect";
3 3
4import { addGroup, capture } from "@executor-js/api";4import { addGroup, capture } from "@executor-js/api";
5import type {5import type { GoogleDiscoveryAddSourceInput, GoogleDiscoveryPluginExtension } from "../sdk/plugin";
6 GoogleDiscoveryAddSourceInput,
7 GoogleDiscoveryPluginExtension,
8} from "../sdk/plugin";
9import { GoogleDiscoveryStoredSourceSchema } from "../sdk/stored-source";6import { GoogleDiscoveryStoredSourceSchema } from "../sdk/stored-source";
10import { GoogleDiscoveryGroup } from "./group";7import { GoogleDiscoveryGroup } from "./group";
11 8
@@ -20,7 +17,9 @@ import { GoogleDiscoveryGroup } from "./group"; · 8 unmodified lines
20// `.addError(InternalError)` on the group — no per-handler translation.17// `.addError(InternalError)` on the group — no per-handler translation.
21// ---------------------------------------------------------------------------18// ---------------------------------------------------------------------------
22 19
23export class GoogleDiscoveryExtensionService extends Context.Service<GoogleDiscoveryExtensionService, GoogleDiscoveryPluginExtension20export class GoogleDiscoveryExtensionService extends Context.Service<
21 GoogleDiscoveryExtensionService,
22 GoogleDiscoveryPluginExtension
24>()("GoogleDiscoveryExtensionService") {}23>()("GoogleDiscoveryExtensionService") {}
25 24
26// ---------------------------------------------------------------------------25// ---------------------------------------------------------------------------
packages/plugins/google-discovery/src/react/client.ts+3−4
@@ -2,7 +2,6 @@ import { createPluginAtomClient } from "@executor-js/sdk/client"; · 1 unmodified line
2import { getBaseUrl } from "@executor-js/react/api/base-url";2import { getBaseUrl } from "@executor-js/react/api/base-url";
3import { GoogleDiscoveryGroup } from "../api/group";3import { GoogleDiscoveryGroup } from "../api/group";
4 4
5export const GoogleDiscoveryClient = createPluginAtomClient(5export const GoogleDiscoveryClient = createPluginAtomClient(GoogleDiscoveryGroup, {
6 GoogleDiscoveryGroup,6 baseUrl: getBaseUrl(),
7 { baseUrl: getBaseUrl() },7});
8);
packages/plugins/google-discovery/src/react/index.ts+1−4
@@ -1,6 +1,3 @@
1export { googleDiscoverySourcePlugin } from "./source-plugin";1export { googleDiscoverySourcePlugin } from "./source-plugin";
2export { GoogleDiscoveryClient } from "./client";2export { GoogleDiscoveryClient } from "./client";
3export {3export { addGoogleDiscoverySource, probeGoogleDiscovery } from "./atoms";
4 addGoogleDiscoverySource,
5 probeGoogleDiscovery,
6} from "./atoms";
packages/plugins/google-discovery/src/sdk/document.ts+22−22
@@ -22,9 +22,7 @@ const LowercaseText = Schema.String.pipe( · 21 unmodified lines
22);22);
23const TextOption = Schema.OptionFromOptional(TrimmedString).pipe(23const TextOption = Schema.OptionFromOptional(TrimmedString).pipe(
24 Schema.decode({24 Schema.decode({
25 decode: SchemaGetter.transform((value) =>25 decode: SchemaGetter.transform((value) => Option.filter(value, (text) => text.length > 0)),
26 Option.filter(value, (text) => text.length > 0),
27 ),
28 encode: SchemaGetter.transform((value) => value),26 encode: SchemaGetter.transform((value) => value),
29 }),27 }),
30 Schema.withDecodingDefaultType(Effect.succeed(Option.none())),28 Schema.withDecodingDefaultType(Effect.succeed(Option.none())),
@@ -48,9 +46,7 @@ const DiscoveryParameterLocationInput = Schema.optional( · 17 unmodified lines
48const DiscoveryDefaultValue = Schema.Union([Schema.String, Schema.Number, Schema.Boolean]);46const DiscoveryDefaultValue = Schema.Union([Schema.String, Schema.Number, Schema.Boolean]);
49 47
50const DiscoverySchemaModel = Schema.Struct({48const DiscoverySchemaModel = Schema.Struct({
51 type: Schema.optional(49 type: Schema.optional(LowercaseText),
52 LowercaseText,
53 ),
54 description: TextOption,50 description: TextOption,
55 properties: UnknownRecordWithDefault,51 properties: UnknownRecordWithDefault,
56 items: Schema.optional(Schema.Unknown),52 items: Schema.optional(Schema.Unknown),
@@ -65,9 +61,7 @@ const DiscoverySchemaModel = Schema.Struct({ · 8 unmodified lines
65type DiscoverySchema = typeof DiscoverySchemaModel.Type;61type DiscoverySchema = typeof DiscoverySchemaModel.Type;
66 62
67const DiscoveryParameterModel = Schema.Struct({63const DiscoveryParameterModel = Schema.Struct({
68 type: Schema.optional(64 type: Schema.optional(LowercaseText),
69 LowercaseText,
70 ),
71 description: TextOption,65 description: TextOption,
72 properties: UnknownRecordWithDefault,66 properties: UnknownRecordWithDefault,
73 items: Schema.optional(Schema.Unknown),67 items: Schema.optional(Schema.Unknown),
@@ -446,7 +440,9 @@ const manifestMethodFromMethod = (input: { · 372 unmodified lines
446 hasBody: requestRef !== undefined,440 hasBody: requestRef !== undefined,
447 }),441 }),
448 inputSchema: Option.fromNullishOr(buildInputSchema({ parameters, requestRef })),442 inputSchema: Option.fromNullishOr(buildInputSchema({ parameters, requestRef })),
449 outputSchema: Option.fromNullishOr(responseRef ? { $ref: schemaRef(responseRef) } : undefined),443 outputSchema: Option.fromNullishOr(
444 responseRef ? { $ref: schemaRef(responseRef) } : undefined,
445 ),
450 scopes: method.scopes ?? [],446 scopes: method.scopes ?? [],
451 });447 });
452 });448 });
@@ -497,20 +493,24 @@ export const extractGoogleDiscoveryManifest = Effect.fn("GoogleDiscovery.extract · 44 unmodified lines
497 ),493 ),
498 );494 );
499 495
500 const topLevelMethods = yield* Effect.forEach(Object.values(document.methods ?? {}), (rawMethod) =>496 const topLevelMethods = yield* Effect.forEach(
501 manifestMethodFromMethod({497 Object.values(document.methods ?? {}),
502 service,498 (rawMethod) =>
503 rawMethod,499 manifestMethodFromMethod({
504 globalParameters: document.parameters ?? {},500 service,
505 }),501 rawMethod,
502 globalParameters: document.parameters ?? {},
503 }),
506 );504 );
507 505
508 const nestedMethods = yield* Effect.forEach(Object.values(document.resources ?? {}), (rawResource) =>506 const nestedMethods = yield* Effect.forEach(
509 collectMethods({507 Object.values(document.resources ?? {}),
510 service,508 (rawResource) =>
511 rawResource,509 collectMethods({
512 globalParameters: document.parameters ?? {},510 service,
513 }),511 rawResource,
512 globalParameters: document.parameters ?? {},
513 }),
514 );514 );
515 515
516 return new GoogleDiscoveryManifest({516 return new GoogleDiscoveryManifest({
packages/plugins/graphql/src/api/group.ts+6−13
@@ -3,10 +3,7 @@ import { Schema } from "effect"; · 2 unmodified lines
3import { ScopeId } from "@executor-js/sdk/core";3import { ScopeId } from "@executor-js/sdk/core";
4import { InternalError } from "@executor-js/api";4import { InternalError } from "@executor-js/api";
5 5
6import {6import { GraphqlIntrospectionError, GraphqlExtractionError } from "../sdk/errors";
7 GraphqlIntrospectionError,
8 GraphqlExtractionError,
9} from "../sdk/errors";
10import { GraphqlSourceAuth, HeaderValue } from "../sdk/types";7import { GraphqlSourceAuth, HeaderValue } from "../sdk/types";
11 8
12// StoredGraphqlSource shape as an HTTP response schema. Kept local to the9// StoredGraphqlSource shape as an HTTP response schema. Kept local to the
@@ -72,12 +69,8 @@ const AddSourceResponse = Schema.Struct({ · 59 unmodified lines
72// Errors with HTTP status69// Errors with HTTP status
73// ---------------------------------------------------------------------------70// ---------------------------------------------------------------------------
74 71
75const IntrospectionError = GraphqlIntrospectionError.annotate(72const IntrospectionError = GraphqlIntrospectionError.annotate({ httpApiStatus: 400 });
76 { httpApiStatus: 400 },73const ExtractionError = GraphqlExtractionError.annotate({ httpApiStatus: 400 });
77);
78const ExtractionError = GraphqlExtractionError.annotate(
79 { httpApiStatus: 400 },
80);
81 74
82// ---------------------------------------------------------------------------75// ---------------------------------------------------------------------------
83// Group76// Group
@@ -119,6 +112,6 @@ export const GraphqlGroup = HttpApiGroup.make("graphql") · 35 unmodified lines
119 success: UpdateSourceResponse,112 success: UpdateSourceResponse,
120 error: GraphqlErrors,113 error: GraphqlErrors,
121 }),114 }),
122 )115 );
123 // Plugin domain errors carry their own HTTP status (4xx);116// Plugin domain errors carry their own HTTP status (4xx);
124 // `InternalError` is the shared opaque 500 translated at the HTTP edge.117// `InternalError` is the shared opaque 500 translated at the HTTP edge.
packages/plugins/graphql/src/api/handlers.ts+50−62
@@ -2,11 +2,7 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"; · 1 unmodified line
2import { Context, Effect } from "effect";2import { Context, Effect } from "effect";
3 3
4import { addGroup, capture } from "@executor-js/api";4import { addGroup, capture } from "@executor-js/api";
5import type {5import type { GraphqlPluginExtension, HeaderValue, GraphqlUpdateSourceInput } from "../sdk/plugin";
6 GraphqlPluginExtension,
7 HeaderValue,
8 GraphqlUpdateSourceInput,
9} from "../sdk/plugin";
10import { GraphqlGroup } from "./group";6import { GraphqlGroup } from "./group";
11 7
12// ---------------------------------------------------------------------------8// ---------------------------------------------------------------------------
@@ -20,7 +16,10 @@ import { GraphqlGroup } from "./group"; · 7 unmodified lines
20// `.addError(InternalError)` on the group — no per-handler translation.16// `.addError(InternalError)` on the group — no per-handler translation.
21// ---------------------------------------------------------------------------17// ---------------------------------------------------------------------------
22 18
23export class GraphqlExtensionService extends Context.Service<GraphqlExtensionService, GraphqlPluginExtension>()("GraphqlExtensionService") {}19export class GraphqlExtensionService extends Context.Service<
20 GraphqlExtensionService,
21 GraphqlPluginExtension
22>()("GraphqlExtensionService") {}
24 23
25// ---------------------------------------------------------------------------24// ---------------------------------------------------------------------------
26// Composed API — core + graphql group25// Composed API — core + graphql group
@@ -38,61 +37,50 @@ const ExecutorApiWithGraphql = addGroup(GraphqlGroup); · 11 unmodified lines
38// by the API-level observability middleware.37// by the API-level observability middleware.
39// ---------------------------------------------------------------------------38// ---------------------------------------------------------------------------
40 39
41export const GraphqlHandlers = HttpApiBuilder.group(40export const GraphqlHandlers = HttpApiBuilder.group(ExecutorApiWithGraphql, "graphql", (handlers) =>
42 ExecutorApiWithGraphql,41 handlers
43 "graphql",42 .handle("addSource", ({ params: path, payload }) =>
44 (handlers) =>43 capture(
45 handlers44 Effect.gen(function* () {
46 .handle("addSource", ({ params: path, payload }) =>45 const ext = yield* GraphqlExtensionService;
47 capture(46 const result = yield* ext.addSource({
48 Effect.gen(function* () {47 endpoint: payload.endpoint,
49 const ext = yield* GraphqlExtensionService;48 scope: path.scopeId,
50 const result = yield* ext.addSource({49 name: payload.name,
51 endpoint: payload.endpoint,50 introspectionJson: payload.introspectionJson,
52 scope: path.scopeId,51 namespace: payload.namespace,
53 name: payload.name,52 headers: payload.headers as Record<string, HeaderValue> | undefined,
54 introspectionJson: payload.introspectionJson,53 queryParams: payload.queryParams as Record<string, HeaderValue> | undefined,
55 namespace: payload.namespace,54 auth: payload.auth,
56 headers: payload.headers as55 });
57 | Record<string, HeaderValue>56 return {
58 | undefined,57 toolCount: result.toolCount,
59 queryParams: payload.queryParams as58 namespace: result.namespace,
60 | Record<string, HeaderValue>59 };
61 | undefined,60 }),
62 auth: payload.auth,
63 });
64 return {
65 toolCount: result.toolCount,
66 namespace: result.namespace,
67 };
68 }),
69 ),
70 )
71 .handle("getSource", ({ params: path }) =>
72 capture(
73 Effect.gen(function* () {
74 const ext = yield* GraphqlExtensionService;
75 return yield* ext.getSource(path.namespace, path.scopeId);
76 }),
77 ),
78 )
79 .handle("updateSource", ({ params: path, payload }) =>
80 capture(
81 Effect.gen(function* () {
82 const ext = yield* GraphqlExtensionService;
83 yield* ext.updateSource(path.namespace, path.scopeId, {
84 name: payload.name,
85 endpoint: payload.endpoint,
86 headers: payload.headers as
87 | Record<string, HeaderValue>
88 | undefined,
89 queryParams: payload.queryParams as
90 | Record<string, HeaderValue>
91 | undefined,
92 auth: payload.auth,
93 } as GraphqlUpdateSourceInput);
94 return { updated: true };
95 }),
96 ),
97 ),61 ),
62 )
63 .handle("getSource", ({ params: path }) =>
64 capture(
65 Effect.gen(function* () {
66 const ext = yield* GraphqlExtensionService;
67 return yield* ext.getSource(path.namespace, path.scopeId);
68 }),
69 ),
70 )
71 .handle("updateSource", ({ params: path, payload }) =>
72 capture(
73 Effect.gen(function* () {
74 const ext = yield* GraphqlExtensionService;
75 yield* ext.updateSource(path.namespace, path.scopeId, {
76 name: payload.name,
77 endpoint: payload.endpoint,
78 headers: payload.headers as Record<string, HeaderValue> | undefined,
79 queryParams: payload.queryParams as Record<string, HeaderValue> | undefined,
80 auth: payload.auth,
81 } as GraphqlUpdateSourceInput);
82 return { updated: true };
83 }),
84 ),
85 ),
98);86);
packages/plugins/graphql/src/react/GraphqlSignInButton.tsx+2−1
@@ -25,7 +25,8 @@ export default function GraphqlSignInButton(props: { sourceId: string }) { · 24 unmodified lines
25 popupName: "graphql-oauth",25 popupName: "graphql-oauth",
26 });26 });
27 27
28 const source = AsyncResult.isSuccess(sourceResult) && sourceResult.value ? sourceResult.value : null;28 const source =
29 AsyncResult.isSuccess(sourceResult) && sourceResult.value ? sourceResult.value : null;
29 const oauth2 = source?.auth.kind === "oauth2" ? source.auth : null;30 const oauth2 = source?.auth.kind === "oauth2" ? source.auth : null;
30 const connections = AsyncResult.isSuccess(connectionsResult) ? connectionsResult.value : null;31 const connections = AsyncResult.isSuccess(connectionsResult) ? connectionsResult.value : null;
31 const isConnected =32 const isConnected =
packages/plugins/graphql/src/react/defaults.ts+1−2
@@ -3,5 +3,4 @@ import { · 2 unmodified lines
3 type HttpCredentialsState,3 type HttpCredentialsState,
4} from "@executor-js/react/plugins/http-credentials";4} from "@executor-js/react/plugins/http-credentials";
5 5
6export const initialGraphqlCredentials = (): HttpCredentialsState =>6export const initialGraphqlCredentials = (): HttpCredentialsState => emptyHttpCredentials();
7 emptyHttpCredentials();
packages/plugins/graphql/src/sdk/introspect.ts+1−3
@@ -226,9 +226,7 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* ( · 225 unmodified lines
226 }226 }
227 227
228 const raw = yield* response.json.pipe(228 const raw = yield* response.json.pipe(
229 Effect.tapCause((cause) =>229 Effect.tapCause((cause) => Effect.logError("graphql introspection JSON parse failed", cause)),
230 Effect.logError("graphql introspection JSON parse failed", cause),
231 ),
232 Effect.mapError(230 Effect.mapError(
233 () =>231 () =>
234 new GraphqlIntrospectionError({232 new GraphqlIntrospectionError({
packages/plugins/graphql/src/sdk/invoke.ts+1−2
@@ -20,8 +20,7 @@ export const resolveHeaders = ( · 19 unmodified lines
20 );20 );
21 return resolveSecretBackedMap({21 return resolveSecretBackedMap({
22 values: headers,22 values: headers,
23 getSecret: (secretId) =>23 getSecret: (secretId) => secrets.get(secretId).pipe(Effect.catch(() => Effect.succeed(null))),
24 secrets.get(secretId).pipe(Effect.catch(() => Effect.succeed(null))),
25 missing: "drop",24 missing: "drop",
26 onMissing: (name) =>25 onMissing: (name) =>
27 new GraphqlInvocationError({26 new GraphqlInvocationError({
packages/plugins/graphql/src/sdk/types.ts+7−9
@@ -64,15 +64,13 @@ export type QueryParamValue = typeof QueryParamValue.Type; · 63 unmodified lines
64// Source auth64// Source auth
65// ---------------------------------------------------------------------------65// ---------------------------------------------------------------------------
66 66
67export const GraphqlSourceAuth = Schema.Union(67export const GraphqlSourceAuth = Schema.Union([
68 [68 Schema.Struct({ kind: Schema.Literal("none") }),
69 Schema.Struct({ kind: Schema.Literal("none") }),69 Schema.Struct({
70 Schema.Struct({70 kind: Schema.Literal("oauth2"),
71 kind: Schema.Literal("oauth2"),71 connectionId: Schema.String,
72 connectionId: Schema.String,72 }),
73 }),73]);
74 ],
75);
76export type GraphqlSourceAuth = typeof GraphqlSourceAuth.Type;74export type GraphqlSourceAuth = typeof GraphqlSourceAuth.Type;
77 75
78export class InvocationConfig extends Schema.Class<InvocationConfig>("InvocationConfig")({76export class InvocationConfig extends Schema.Class<InvocationConfig>("InvocationConfig")({
packages/plugins/keychain/src/index.test.ts+3−1
@@ -62,7 +62,9 @@ describe("keychain plugin", () => { · 61 unmodified lines
62 const resolved = yield* executor.secrets.get(testId);62 const resolved = yield* executor.secrets.get(testId);
63 expect(resolved).toBe("keychain-test-value");63 expect(resolved).toBe("keychain-test-value");
64 }).pipe(64 }).pipe(
65 Effect.ensuring(executor.secrets.remove(testId).pipe(Effect.orElseSucceed(() => undefined))),65 Effect.ensuring(
66 executor.secrets.remove(testId).pipe(Effect.orElseSucceed(() => undefined)),
67 ),
66 );68 );
67 }),69 }),
68 );70 );
packages/plugins/keychain/src/index.ts+24−37
@@ -1,10 +1,6 @@
1import { Effect } from "effect";1import { Effect } from "effect";
2 2
3import {3import { definePlugin, type PluginCtx, type SecretProvider } from "@executor-js/sdk/core";
4 definePlugin,
5 type PluginCtx,
6 type SecretProvider,
7} from "@executor-js/sdk/core";
8 4
9import {5import {
10 deletePassword,6 deletePassword,
@@ -59,8 +55,7 @@ export type KeychainExtension = ReturnType<typeof makeKeychainExtension>; · 48 unmodified lines
59const scopedServiceName = (55const scopedServiceName = (
60 ctx: PluginCtx<unknown>,56 ctx: PluginCtx<unknown>,
61 options: KeychainPluginConfig | undefined,57 options: KeychainPluginConfig | undefined,
62): string =>58): string => `${resolveServiceName(options?.serviceName)}/${ctx.scopes[0]!.id}`;
63 `${resolveServiceName(options?.serviceName)}/${ctx.scopes[0]!.id}`;
64 59
65const makeKeychainExtension = (60const makeKeychainExtension = (
66 ctx: PluginCtx<unknown>,61 ctx: PluginCtx<unknown>,
@@ -83,34 +78,26 @@ const makeKeychainExtension = ( · 16 unmodified lines
83 };78 };
84};79};
85 80
86export const keychainPlugin = definePlugin(81export const keychainPlugin = definePlugin((options?: KeychainPluginConfig) => ({
87 (options?: KeychainPluginConfig) => ({82 id: "keychain" as const,
88 id: "keychain" as const,83 storage: () => ({}),
89 storage: () => ({}),84
90 85 extension: (ctx): KeychainExtension => makeKeychainExtension(ctx, options),
91 extension: (ctx): KeychainExtension => makeKeychainExtension(ctx, options),86
92 87 secretProviders: (ctx): Effect.Effect<readonly SecretProvider[]> =>
93 secretProviders: (ctx): Effect.Effect<readonly SecretProvider[]> =>88 Effect.gen(function* () {
94 Effect.gen(function* () {89 const serviceName = scopedServiceName(ctx, options);
95 const serviceName = scopedServiceName(ctx, options);90 const reachable = yield* setPassword(serviceName, PROBE_ACCOUNT, PROBE_VALUE).pipe(
96 const reachable = yield* setPassword(91 Effect.andThen(
97 serviceName,92 deletePassword(serviceName, PROBE_ACCOUNT).pipe(Effect.catch(() => Effect.void)),
98 PROBE_ACCOUNT,93 ),
99 PROBE_VALUE,94 Effect.as(true),
100 ).pipe(95 Effect.catch(() =>
101 Effect.andThen(96 Effect.logWarning("keychain unavailable, skipping provider registration").pipe(
102 deletePassword(serviceName, PROBE_ACCOUNT).pipe(97 Effect.as(false),
103 Effect.catch(() => Effect.void),
104 ),
105 ),
106 Effect.as(true),
107 Effect.catch(() =>
108 Effect.logWarning(
109 "keychain unavailable, skipping provider registration",
110 ).pipe(Effect.as(false)),
111 ),98 ),
112 );99 ),
113 return reachable ? [makeKeychainProvider(serviceName)] : [];100 );
114 }),101 return reachable ? [makeKeychainProvider(serviceName)] : [];
115 }),102 }),
116);103}));
packages/plugins/mcp/src/api/group.ts+10−11
@@ -179,14 +179,13 @@ export const McpGroup = HttpApiGroup.make("mcp") · 178 unmodified lines
179 success: UpdateSourceResponse,179 success: UpdateSourceResponse,
180 error: [InternalError, McpConnectionError, McpToolDiscoveryError],180 error: [InternalError, McpConnectionError, McpToolDiscoveryError],
181 }),181 }),
182 )182 );
183 // Errors declared once at the group level — every endpoint inherits.183// Errors declared once at the group level — every endpoint inherits.
184 // Plugin domain errors carry their own HttpApiSchema status (4xx);184// Plugin domain errors carry their own HttpApiSchema status (4xx);
185 // `InternalError` is the shared opaque 500 translated at the HTTP185// `InternalError` is the shared opaque 500 translated at the HTTP
186 // edge by `withCapture`. We only list errors an MCP *group*186// edge by `withCapture`. We only list errors an MCP *group*
187 // endpoint can surface: `McpInvocationError` is thrown inside187// endpoint can surface: `McpInvocationError` is thrown inside
188 // `invokeTool` which is reached via the core `tools.invoke`188// `invokeTool` which is reached via the core `tools.invoke`
189 // endpoint, not any MCP-group endpoint, so it doesn't belong here.189// endpoint, not any MCP-group endpoint, so it doesn't belong here.
190 // OAuth errors live on the shared `/oauth/*` group in `@executor-js/api`190// OAuth errors live on the shared `/oauth/*` group in `@executor-js/api`
191 // now — the MCP group only declares its own plugin-domain errors.191// now — the MCP group only declares its own plugin-domain errors.
192;
packages/plugins/mcp/src/api/handlers.test.ts+16−18
@@ -69,25 +69,23 @@ const McpConnectionErrorResponse = Schema.Struct({ · 68 unmodified lines
69});69});
70 70
71describe("McpHandlers", () => {71describe("McpHandlers", () => {
72 it.effect(72 it.effect("defect-returning methods produce an opaque InternalError, no leakage", () =>
73 "defect-returning methods produce an opaque InternalError, no leakage",73 Effect.gen(function* () {
74 () =>74 const web = yield* WebHandler;
75 Effect.gen(function* () {75 const response = yield* Effect.promise(() =>
76 const web = yield* WebHandler;76 (web.handler as (request: Request) => Promise<Response>)(
77 const response = yield* Effect.promise(() =>77 new Request("http://localhost/scopes/scope_1/mcp/probe", {
78 (web.handler as (request: Request) => Promise<Response>)(78 method: "POST",
79 new Request("http://localhost/scopes/scope_1/mcp/probe", {79 headers: { "content-type": "application/json" },
80 method: "POST",80 body: JSON.stringify({ endpoint: "https://example.com/mcp" }),
81 headers: { "content-type": "application/json" },81 }),
82 body: JSON.stringify({ endpoint: "https://example.com/mcp" }),82 ),
83 }),83 );
84 ),
85 );
86 84
87 expect(response.status).toBe(500);85 expect(response.status).toBe(500);
88 const body = yield* Effect.promise(() => response.text());86 const body = yield* Effect.promise(() => response.text());
89 expect(body).not.toContain("Not implemented");87 expect(body).not.toContain("Not implemented");
90 }),88 }),
91 );89 );
92 90
93 it.effect("domain MCP connection errors are encoded as 400 responses", () =>91 it.effect("domain MCP connection errors are encoded as 400 responses", () =>
packages/plugins/mcp/src/api/handlers.ts+3−2
@@ -20,8 +20,9 @@ import { McpGroup } from "./group"; · 19 unmodified lines
20// (no `Captured<>` inversion).20// (no `Captured<>` inversion).
21// ---------------------------------------------------------------------------21// ---------------------------------------------------------------------------
22 22
23export class McpExtensionService extends Context.Service<McpExtensionService, McpPluginExtension23export class McpExtensionService extends Context.Service<McpExtensionService, McpPluginExtension>()(
24>()("McpExtensionService") {}24 "McpExtensionService",
25) {}
25 26
26// ---------------------------------------------------------------------------27// ---------------------------------------------------------------------------
27// Composed API28// Composed API
packages/plugins/mcp/src/react/index.ts+1−6
@@ -1,9 +1,4 @@
1export { mcpSourcePlugin, createMcpSourcePlugin } from "./source-plugin";1export { mcpSourcePlugin, createMcpSourcePlugin } from "./source-plugin";
2export type { McpSourcePluginOptions } from "./source-plugin";2export type { McpSourcePluginOptions } from "./source-plugin";
3export { McpClient } from "./client";3export { McpClient } from "./client";
4export {4export { probeMcpEndpoint, addMcpSource, removeMcpSource, refreshMcpSource } from "./atoms";
5 probeMcpEndpoint,
6 addMcpSource,
7 removeMcpSource,
8 refreshMcpSource,
9} from "./atoms";
packages/plugins/mcp/src/react/source-plugin.tsx+1−3
@@ -23,9 +23,7 @@ export interface McpSourcePluginOptions { · 22 unmodified lines
23 readonly allowStdio?: boolean;23 readonly allowStdio?: boolean;
24}24}
25 25
26export const createMcpSourcePlugin = (26export const createMcpSourcePlugin = (options?: McpSourcePluginOptions): SourcePlugin => {
27 options?: McpSourcePluginOptions,
28): SourcePlugin => {
29 const allowStdio = options?.allowStdio ?? false;27 const allowStdio = options?.allowStdio ?? false;
30 28
31 const AddWithFlag: ComponentType<AddProps> = (props) => (29 const AddWithFlag: ComponentType<AddProps> = (props) => (
packages/plugins/mcp/src/sdk/connection-pool.test.ts+26−44
@@ -163,11 +163,7 @@ describe("MCP connection pooling (regression)", () => { · 162 unmodified lines
163 // First invoke is necessarily a cache miss — it cold-handshakes the163 // First invoke is necessarily a cache miss — it cold-handshakes the
164 // pooled connection. Every subsequent invoke must hit the cache and164 // pooled connection. Every subsequent invoke must hit the cache and
165 // produce zero new MCP server sessions.165 // produce zero new MCP server sessions.
166 yield* executor.tools.invoke(166 yield* executor.tools.invoke(echo.id, { value: "1" }, { onElicitation: "accept-all" });
167 echo.id,
168 { value: "1" },
169 { onElicitation: "accept-all" },
170 );
171 const sessionsAfterFirstInvoke = server.sessionCount();167 const sessionsAfterFirstInvoke = server.sessionCount();
172 expect(sessionsAfterFirstInvoke).toBe(sessionsAfterAddSource + 1);168 expect(sessionsAfterFirstInvoke).toBe(sessionsAfterAddSource + 1);
173 169
@@ -185,44 +181,30 @@ describe("MCP connection pooling (regression)", () => { · 11 unmodified lines
185 }),181 }),
186 );182 );
187 183
188 it.effect(184 it.effect("different tools on the same source share the cached connection", () =>
189 "different tools on the same source share the cached connection",185 Effect.gen(function* () {
190 () =>186 const server = yield* serveMcpServer;
191 Effect.gen(function* () {187 const executor = yield* makeTestExecutor(server.url);
192 const server = yield* serveMcpServer;188 const tools = yield* executor.tools.list();
193 const executor = yield* makeTestExecutor(server.url);189 const echo = tools.find((t) => t.name === "echo")!;
194 const tools = yield* executor.tools.list();190 const echo2 = tools.find((t) => t.name === "echo2")!;
195 const echo = tools.find((t) => t.name === "echo")!;191 expect(echo).toBeDefined();
196 const echo2 = tools.find((t) => t.name === "echo2")!;192 expect(echo2).toBeDefined();
197 expect(echo).toBeDefined();193
198 expect(echo2).toBeDefined();194 const sessionsAfterAddSource = server.sessionCount();
199 195
200 const sessionsAfterAddSource = server.sessionCount();196 // Cold handshake on first call.
201 197 yield* executor.tools.invoke(echo.id, { value: "a" }, { onElicitation: "accept-all" });
202 // Cold handshake on first call.198 const baseline = server.sessionCount();
203 yield* executor.tools.invoke(199 expect(baseline).toBe(sessionsAfterAddSource + 1);
204 echo.id,200
205 { value: "a" },201 // Different tool on the SAME source — same cache key, same conn.
206 { onElicitation: "accept-all" },202 yield* executor.tools.invoke(echo2.id, { value: "b" }, { onElicitation: "accept-all" });
207 );203 expect(server.sessionCount()).toBe(baseline);
208 const baseline = server.sessionCount();204
209 expect(baseline).toBe(sessionsAfterAddSource + 1);205 // Back to the first tool — still pooled.
210 206 yield* executor.tools.invoke(echo.id, { value: "c" }, { onElicitation: "accept-all" });
211 // Different tool on the SAME source — same cache key, same conn.207 expect(server.sessionCount()).toBe(baseline);
212 yield* executor.tools.invoke(208 }),
213 echo2.id,
214 { value: "b" },
215 { onElicitation: "accept-all" },
216 );
217 expect(server.sessionCount()).toBe(baseline);
218
219 // Back to the first tool — still pooled.
220 yield* executor.tools.invoke(
221 echo.id,
222 { value: "c" },
223 { onElicitation: "accept-all" },
224 );
225 expect(server.sessionCount()).toBe(baseline);
226 }),
227 );209 );
228});210});
packages/plugins/mcp/src/sdk/cross-user-isolation.test.ts+2−6
@@ -53,10 +53,7 @@ const makeSharedOrgExecutors = () => · 52 unmodified lines
53 53
54 const makeFor = (innerId: ScopeId) =>54 const makeFor = (innerId: ScopeId) =>
55 createExecutor({55 createExecutor({
56 scopes: [56 scopes: [new Scope({ id: innerId, name: "Personal", createdAt: new Date() }), orgScope],
57 new Scope({ id: innerId, name: "Personal", createdAt: new Date() }),
58 orgScope,
59 ],
60 adapter,57 adapter,
61 blobs,58 blobs,
62 plugins,59 plugins,
@@ -142,8 +139,7 @@ describe("MCP cross-user isolation within the same org", () => { · 79 unmodified lines
142 139
143 it.effect("A sees A's source, B sees org's, neither sees the other's inner", () =>140 it.effect("A sees A's source, B sees org's, neither sees the other's inner", () =>
144 Effect.gen(function* () {141 Effect.gen(function* () {
145 const { execA, execB, aInnerId, bInnerId, orgScopeId } =142 const { execA, execB, aInnerId, bInnerId, orgScopeId } = yield* makeSharedOrgExecutors();
146 yield* makeSharedOrgExecutors();
147 143
148 yield* seedSource(execA.mcp.addSource, {144 yield* seedSource(execA.mcp.addSource, {
149 scope: aInnerId,145 scope: aInnerId,
packages/plugins/mcp/src/sdk/elicitation.test.ts+1−2
@@ -145,8 +145,7 @@ describe("MCP elicitation (end-to-end)", () => { · 144 unmodified lines
145 gatedEcho.id,145 gatedEcho.id,
146 { value: "nope" },146 { value: "nope" },
147 {147 {
148 onElicitation: () =>148 onElicitation: () => Effect.succeed(new ElicitationResponse({ action: "decline" })),
149 Effect.succeed(new ElicitationResponse({ action: "decline" })),
150 },149 },
151 );150 );
152 151
packages/plugins/mcp/src/sdk/per-user-auth-isolation.test.ts+136−146
@@ -103,72 +103,69 @@ const failureError = <E>(exit: Exit.Exit<unknown, E>): E | undefined => · 102 unmodified lines
103const isToolInvocationError = (error: unknown): error is ToolInvocationError =>103const isToolInvocationError = (error: unknown): error is ToolInvocationError =>
104 Predicate.isTagged(error, "ToolInvocationError");104 Predicate.isTagged(error, "ToolInvocationError");
105 105
106const createAuthRecordingServer: Effect.Effect<TestServer, Error, never> =106const createAuthRecordingServer: Effect.Effect<TestServer, Error, never> = Effect.callback<
107 Effect.callback<TestServer, Error>((resume) => {107 TestServer,
108 const transports = new Map<string, StreamableHTTPServerTransport>();108 Error
109 const recorded: RecordedRequest[] = [];109>((resume) => {
110 110 const transports = new Map<string, StreamableHTTPServerTransport>();
111 const httpServer = http.createServer(async (req, res) => {111 const recorded: RecordedRequest[] = [];
112 recorded.push({112
113 authorization: req.headers["authorization"] as string | undefined,113 const httpServer = http.createServer(async (req, res) => {
114 });114 recorded.push({
115 authorization: req.headers["authorization"] as string | undefined,
116 });
115 117
116 const sessionId = req.headers["mcp-session-id"] as string | undefined;118 const sessionId = req.headers["mcp-session-id"] as string | undefined;
117 if (sessionId) {119 if (sessionId) {
118 const transport = transports.get(sessionId);120 const transport = transports.get(sessionId);
119 if (!transport) {121 if (!transport) {
120 res.writeHead(404);122 res.writeHead(404);
121 res.end("Session not found");123 res.end("Session not found");
122 return;
123 }
124 await transport.handleRequest(req, res);
125 return;124 return;
126 }125 }
127
128 const mcpServer = new McpServer(
129 { name: "iso-test", version: "1.0.0" },
130 { capabilities: {} },
131 );
132 mcpServer.registerTool(
133 "whoami",
134 {
135 description: "Echoes a marker — used to prove the invoke reached the server",
136 inputSchema: { marker: z.string() },
137 },
138 async ({ marker }: { marker: string }) => ({
139 content: [{ type: "text" as const, text: `ok:${marker}` }],
140 }),
141 );
142
143 const transport = new StreamableHTTPServerTransport({
144 sessionIdGenerator: () => crypto.randomUUID(),
145 onsessioninitialized: (sid) => {
146 transports.set(sid, transport);
147 },
148 });
149 await mcpServer.connect(transport);
150 await transport.handleRequest(req, res);126 await transport.handleRequest(req, res);
151 });127 return;
128 }
129
130 const mcpServer = new McpServer({ name: "iso-test", version: "1.0.0" }, { capabilities: {} });
131 mcpServer.registerTool(
132 "whoami",
133 {
134 description: "Echoes a marker — used to prove the invoke reached the server",
135 inputSchema: { marker: z.string() },
136 },
137 async ({ marker }: { marker: string }) => ({
138 content: [{ type: "text" as const, text: `ok:${marker}` }],
139 }),
140 );
152 141
153 httpServer.listen(0, () => {142 const transport = new StreamableHTTPServerTransport({
154 const addr = httpServer.address();143 sessionIdGenerator: () => crypto.randomUUID(),
155 const port = typeof addr === "object" && addr ? addr.port : 0;144 onsessioninitialized: (sid) => {
156 resume(145 transports.set(sid, transport);
157 Effect.succeed({146 },
158 url: `http://127.0.0.1:${port}`,
159 httpServer,
160 recorded: () => recorded,
161 }),
162 );
163 });147 });
148 await mcpServer.connect(transport);
149 await transport.handleRequest(req, res);
164 });150 });
165 151
166const serveMcpServer = Effect.acquireRelease(152 httpServer.listen(0, () => {
167 createAuthRecordingServer,153 const addr = httpServer.address();
168 ({ httpServer }) =>154 const port = typeof addr === "object" && addr ? addr.port : 0;
169 Effect.sync(() => {155 resume(
170 httpServer.close();156 Effect.succeed({
171 }),157 url: `http://127.0.0.1:${port}`,
158 httpServer,
159 recorded: () => recorded,
160 }),
161 );
162 });
163});
164
165const serveMcpServer = Effect.acquireRelease(createAuthRecordingServer, ({ httpServer }) =>
166 Effect.sync(() => {
167 httpServer.close();
168 }),
172);169);
173 170
174// ---------------------------------------------------------------------------171// ---------------------------------------------------------------------------
@@ -181,8 +178,7 @@ const USER_A = ScopeId.make("user-a"); · 6 unmodified lines
181const USER_B = ScopeId.make("user-b");178const USER_B = ScopeId.make("user-b");
182const ORG = ScopeId.make("org");179const ORG = ScopeId.make("org");
183 180
184const scope = (id: ScopeId, name: string): Scope =>181const scope = (id: ScopeId, name: string): Scope => new Scope({ id, name, createdAt: new Date() });
185 new Scope({ id, name, createdAt: new Date() });
186 182
187const makeLayeredMcpExecutors = () =>183const makeLayeredMcpExecutors = () =>
188 Effect.gen(function* () {184 Effect.gen(function* () {
@@ -290,9 +286,7 @@ describe("per-user MCP auth isolation", () => { · 101 unmodified lines
290 content: [{ type: "text", text: "ok:from-user-a" }],286 content: [{ type: "text", text: "ok:from-user-a" }],
291 });287 });
292 const userARequests = server.recorded().slice(recordedBeforeUserA);288 const userARequests = server.recorded().slice(recordedBeforeUserA);
293 expect(289 expect(userARequests.some((r) => r.authorization === "Bearer token-user-a")).toBe(true);
294 userARequests.some((r) => r.authorization === "Bearer token-user-a"),
295 ).toBe(true);
296 290
297 // Snapshot the recorded-auth slice we'll check for the user-B291 // Snapshot the recorded-auth slice we'll check for the user-B
298 // run — "token-user-a" must not appear in any request AFTER292 // run — "token-user-a" must not appear in any request AFTER
@@ -344,95 +338,91 @@ describe("per-user MCP auth isolation", () => { · 45 unmodified lines
344 // must fail rather than silently fall through to any other scope's338 // must fail rather than silently fall through to any other scope's
345 // value. This also locks the contract that the MCP plugin doesn't339 // value. This also locks the contract that the MCP plugin doesn't
346 // quietly downgrade a missing secret to "no auth" on the transport.340 // quietly downgrade a missing secret to "no auth" on the transport.
347 it.effect(341 it.effect("header source: unauthenticated user B cannot invoke via a per-user secret", () =>
348 "header source: unauthenticated user B cannot invoke via a per-user secret",342 Effect.gen(function* () {
349 () =>343 const server = yield* serveMcpServer;
350 Effect.gen(function* () {344 const { execUserA, execUserB } = yield* makeLayeredMcpExecutors();
351 const server = yield* serveMcpServer;345
352 const { execUserA, execUserB } = yield* makeLayeredMcpExecutors();346 const SECRET = SecretId.make("shared-mcp-token");
347
348 // User A plants their personal token at their per-user scope
349 // FIRST — so the subsequent addSource's discovery call can
350 // resolve it through user A's stack.
351 yield* execUserA.secrets.set(
352 new SetSecretInput({
353 id: SECRET,
354 scope: USER_A,
355 name: "User A MCP token",
356 value: "token-user-a-header",
357 }),
358 );
353 359
354 const SECRET = SecretId.make("shared-mcp-token");360 yield* execUserA.mcp.addSource({
361 transport: "remote",
362 scope: ORG,
363 name: "Shared MCP (header)",
364 endpoint: server.url,
365 namespace: "iso_header",
366 auth: {
367 kind: "header",
368 headerName: "Authorization",
369 secretId: SECRET,
370 prefix: "Bearer ",
371 },
372 });
355 373
356 // User A plants their personal token at their per-user scope374 // User A sanity invoke — server sees A's token. Asserting on
357 // FIRST — so the subsequent addSource's discovery call can375 // the recorded header here guards against false-positive for
358 // resolve it through user A's stack.376 // the negative check below (e.g. if the plugin silently dropped
359 yield* execUserA.secrets.set(377 // auth, both invocations would fail for unrelated reasons).
360 new SetSecretInput({378 const userATools = yield* execUserA.tools.list();
361 id: SECRET,379 const whoamiForA = userATools.find((t) => t.name === "whoami")!;
362 scope: USER_A,380
363 name: "User A MCP token",381 const recordedBeforeUserA = server.recorded().length;
364 value: "token-user-a-header",382 const userAResult = yield* execUserA.tools.invoke(
365 }),383 whoamiForA.id,
366 );384 { marker: "user-a-header" },
385 { onElicitation: "accept-all" },
386 );
387 expect(userAResult).toMatchObject({
388 content: [{ type: "text", text: "ok:user-a-header" }],
389 });
390 const userARequests = server.recorded().slice(recordedBeforeUserA);
391 expect(userARequests.some((r) => r.authorization === "Bearer token-user-a-header")).toBe(
392 true,
393 );
367 394
368 yield* execUserA.mcp.addSource({395 const recordedBeforeUserB = server.recorded().length;
369 transport: "remote",
370 scope: ORG,
371 name: "Shared MCP (header)",
372 endpoint: server.url,
373 namespace: "iso_header",
374 auth: {
375 kind: "header",
376 headerName: "Authorization",
377 secretId: SECRET,
378 prefix: "Bearer ",
379 },
380 });
381 396
382 // User A sanity invoke — server sees A's token. Asserting on397 // User B has no personal token. The fall-through lookup walks
383 // the recorded header here guards against false-positive for398 // [userB, org] — neither scope has the secret row, so the
384 // the negative check below (e.g. if the plugin silently dropped399 // resolver must return null and the invoke must fail.
385 // auth, both invocations would fail for unrelated reasons).400 const userBTools = yield* execUserB.tools.list();
386 const userATools = yield* execUserA.tools.list();401 const whoamiForB = userBTools.find((t) => t.name === "whoami")!;
387 const whoamiForA = userATools.find((t) => t.name === "whoami")!;
388 402
389 const recordedBeforeUserA = server.recorded().length;403 const userBResult = yield* Effect.exit(
390 const userAResult = yield* execUserA.tools.invoke(404 execUserB.tools.invoke(
391 whoamiForA.id,405 whoamiForB.id,
392 { marker: "user-a-header" },406 { marker: "user-b-header" },
393 { onElicitation: "accept-all" },407 { onElicitation: "accept-all" },
394 );408 ),
395 expect(userAResult).toMatchObject({409 );
396 content: [{ type: "text", text: "ok:user-a-header" }],
397 });
398 const userARequests = server.recorded().slice(recordedBeforeUserA);
399 expect(
400 userARequests.some(
401 (r) => r.authorization === "Bearer token-user-a-header",
402 ),
403 ).toBe(true);
404
405 const recordedBeforeUserB = server.recorded().length;
406
407 // User B has no personal token. The fall-through lookup walks
408 // [userB, org] — neither scope has the secret row, so the
409 // resolver must return null and the invoke must fail.
410 const userBTools = yield* execUserB.tools.list();
411 const whoamiForB = userBTools.find((t) => t.name === "whoami")!;
412
413 const userBResult = yield* Effect.exit(
414 execUserB.tools.invoke(
415 whoamiForB.id,
416 { marker: "user-b-header" },
417 { onElicitation: "accept-all" },
418 ),
419 );
420
421 expect(Exit.isFailure(userBResult)).toBe(true);
422 // tools.invoke wraps plugin failures in ToolInvocationError
423 // with the original error carried on `cause`. Pin the exact
424 // inner tag — a regression that swapped the "no connection
425 // found" check for a silent no-auth fallback would either
426 // succeed outright (leaking) or surface a different tag here.
427 const outer = failureError(userBResult);
428 expect(isToolInvocationError(outer)).toBe(true);
429 const inner = isToolInvocationError(outer) ? outer.cause : undefined;
430 expect(Predicate.isTagged(inner, "McpConnectionError")).toBe(true);
431 410
432 const afterUserB = server.recorded().slice(recordedBeforeUserB);411 expect(Exit.isFailure(userBResult)).toBe(true);
433 for (const req of afterUserB) {412 // tools.invoke wraps plugin failures in ToolInvocationError
434 expect(req.authorization).not.toBe("Bearer token-user-a-header");413 // with the original error carried on `cause`. Pin the exact
435 }414 // inner tag — a regression that swapped the "no connection
436 }),415 // found" check for a silent no-auth fallback would either
416 // succeed outright (leaking) or surface a different tag here.
417 const outer = failureError(userBResult);
418 expect(isToolInvocationError(outer)).toBe(true);
419 const inner = isToolInvocationError(outer) ? outer.cause : undefined;
420 expect(Predicate.isTagged(inner, "McpConnectionError")).toBe(true);
421
422 const afterUserB = server.recorded().slice(recordedBeforeUserB);
423 for (const req of afterUserB) {
424 expect(req.authorization).not.toBe("Bearer token-user-a-header");
425 }
426 }),
437 );427 );
438});428});
packages/plugins/mcp/src/sdk/probe-shape.test.ts+3−4
@@ -129,10 +129,9 @@ describe("probeMcpEndpointShape", () => { · 128 unmodified lines
129 }),129 }),
130 { status: 400, headers: { "content-type": "application/json" } },130 { status: 400, headers: { "content-type": "application/json" } },
131 );131 );
132 const result = yield* probeMcpEndpointShape(132 const result = yield* probeMcpEndpointShape("https://backboard.railway.com/graphql/v2", {
133 "https://backboard.railway.com/graphql/v2",133 fetch: stubFetch(response),
134 { fetch: stubFetch(response) },134 });
135 );
136 expect(result.kind).toBe("not-mcp");135 expect(result.kind).toBe("not-mcp");
137 }),136 }),
138 );137 );
packages/plugins/mcp/src/sdk/probe-shape.ts+1−2
@@ -133,8 +133,7 @@ export const probeMcpEndpointShape = ( · 132 unmodified lines
133 }133 }
134 return {134 return {
135 kind: "not-mcp",135 kind: "not-mcp",
136 reason:136 reason: "401 without Bearer WWW-Authenticate — not an MCP auth challenge",
137 "401 without Bearer WWW-Authenticate — not an MCP auth challenge",
138 } as const;137 } as const;
139 }138 }
140 139
packages/plugins/mcp/src/sdk/stdio-connector.ts+1−3
@@ -26,8 +26,6 @@ export const createStdioTransport = (config: StdioTransportConfig) => · 25 unmodified lines
26 new StdioClientTransport({26 new StdioClientTransport({
27 command: config.command,27 command: config.command,
28 args: config.args ? [...config.args] : undefined,28 args: config.args ? [...config.args] : undefined,
29 env: config.env29 env: config.env ? ({ ...process.env, ...config.env } as Record<string, string>) : undefined,
30 ? ({ ...process.env, ...config.env } as Record<string, string>)
31 : undefined,
32 cwd: config.cwd,30 cwd: config.cwd,
33 });31 });
packages/plugins/onepassword/src/api/handlers.ts+38−26
@@ -16,7 +16,9 @@ import { OnePasswordGroup } from "./group"; · 15 unmodified lines
16// `.addError(InternalError)` on the group — no per-handler translation.16// `.addError(InternalError)` on the group — no per-handler translation.
17// ---------------------------------------------------------------------------17// ---------------------------------------------------------------------------
18 18
19export class OnePasswordExtensionService extends Context.Service<OnePasswordExtensionService, OnePasswordExtension19export class OnePasswordExtensionService extends Context.Service<
20 OnePasswordExtensionService,
21 OnePasswordExtension
20>()("OnePasswordExtensionService") {}22>()("OnePasswordExtensionService") {}
21 23
22// ---------------------------------------------------------------------------24// ---------------------------------------------------------------------------
@@ -41,38 +43,48 @@ export const OnePasswordHandlers = HttpApiBuilder.group( · 18 unmodified lines
41 (handlers) =>43 (handlers) =>
42 handlers44 handlers
43 .handle("getConfig", () =>45 .handle("getConfig", () =>
44 capture(Effect.gen(function* () {46 capture(
45 const ext = yield* OnePasswordExtensionService;47 Effect.gen(function* () {
46 return yield* ext.getConfig();48 const ext = yield* OnePasswordExtensionService;
47 })),49 return yield* ext.getConfig();
50 }),
51 ),
48 )52 )
49 .handle("configure", ({ payload }) =>53 .handle("configure", ({ payload }) =>
50 capture(Effect.gen(function* () {54 capture(
51 const ext = yield* OnePasswordExtensionService;55 Effect.gen(function* () {
52 yield* ext.configure(payload);56 const ext = yield* OnePasswordExtensionService;
53 })),57 yield* ext.configure(payload);
58 }),
59 ),
54 )60 )
55 .handle("removeConfig", () =>61 .handle("removeConfig", () =>
56 capture(Effect.gen(function* () {62 capture(
57 const ext = yield* OnePasswordExtensionService;63 Effect.gen(function* () {
58 yield* ext.removeConfig();64 const ext = yield* OnePasswordExtensionService;
59 })),65 yield* ext.removeConfig();
66 }),
67 ),
60 )68 )
61 .handle("status", () =>69 .handle("status", () =>
62 capture(Effect.gen(function* () {70 capture(
63 const ext = yield* OnePasswordExtensionService;71 Effect.gen(function* () {
64 return yield* ext.status();72 const ext = yield* OnePasswordExtensionService;
65 })),73 return yield* ext.status();
74 }),
75 ),
66 )76 )
67 .handle("listVaults", ({ query: urlParams }) =>77 .handle("listVaults", ({ query: urlParams }) =>
68 capture(Effect.gen(function* () {78 capture(
69 const ext = yield* OnePasswordExtensionService;79 Effect.gen(function* () {
70 const auth =80 const ext = yield* OnePasswordExtensionService;
71 urlParams.authKind === "desktop-app"81 const auth =
72 ? { kind: "desktop-app" as const, accountName: urlParams.account }82 urlParams.authKind === "desktop-app"
73 : { kind: "service-account" as const, tokenSecretId: urlParams.account };83 ? { kind: "desktop-app" as const, accountName: urlParams.account }
74 const vaults = yield* ext.listVaults(auth);84 : { kind: "service-account" as const, tokenSecretId: urlParams.account };
75 return { vaults: [...vaults] };85 const vaults = yield* ext.listVaults(auth);
76 })),86 return { vaults: [...vaults] };
87 }),
88 ),
77 ),89 ),
78);90);
packages/plugins/openapi/src/api/group.ts+7−12
@@ -166,15 +166,10 @@ export const OpenApiGroup = HttpApiGroup.make("openapi") · 165 unmodified lines
166 }),166 }),
167 )167 )
168 .add(168 .add(
169 HttpApiEndpoint.post(169 HttpApiEndpoint.post("removeSourceBinding", "/scopes/:scopeId/openapi/source-bindings/remove", {
170 "removeSourceBinding",170 params: ScopeIdParam,
171 "/scopes/:scopeId/openapi/source-bindings/remove",171 payload: RemoveBindingPayload,
172 {172 success: Schema.Struct({ removed: Schema.Boolean }),
173 params: ScopeIdParam,173 error: DomainErrors,
174 payload: RemoveBindingPayload,174 }),
175 success: Schema.Struct({ removed: Schema.Boolean }),175 );
176 error: DomainErrors,
177 },
178 ),
179 )
180;
packages/plugins/openapi/src/api/handlers.ts+3−1
@@ -24,7 +24,9 @@ import { OpenApiGroup } from "./group"; · 23 unmodified lines
24// `.addError(InternalError)` on the group — no per-handler translation.24// `.addError(InternalError)` on the group — no per-handler translation.
25// ---------------------------------------------------------------------------25// ---------------------------------------------------------------------------
26 26
27export class OpenApiExtensionService extends Context.Service<OpenApiExtensionService, OpenApiPluginExtension27export class OpenApiExtensionService extends Context.Service<
28 OpenApiExtensionService,
29 OpenApiPluginExtension
28>()("OpenApiExtensionService") {}30>()("OpenApiExtensionService") {}
29 31
30// ---------------------------------------------------------------------------32// ---------------------------------------------------------------------------
packages/plugins/openapi/src/react/AddOpenApiSource.tsx+113−113
@@ -401,29 +401,29 @@ export default function AddOpenApiSource(props: { · 400 unmodified lines
401 const result = exit.value;401 const result = exit.value;
402 setPreview(result);402 setPreview(result);
403 403
404 const firstServer = result.servers[0];404 const firstServer = result.servers[0];
405 if (firstServer) {405 if (firstServer) {
406 setSelectedServerIndex(0);406 setSelectedServerIndex(0);
407 setVariableSelections(defaultSelectionsFor(firstServer));407 setVariableSelections(defaultSelectionsFor(firstServer));
408 setCustomBaseUrl("");408 setCustomBaseUrl("");
409 } else {409 } else {
410 setSelectedServerIndex(-1);410 setSelectedServerIndex(-1);
411 setVariableSelections({});411 setVariableSelections({});
412 setCustomBaseUrl("");412 setCustomBaseUrl("");
413 }413 }
414 414
415 const firstPreset = result.headerPresets[0];415 const firstPreset = result.headerPresets[0];
416 if (firstPreset) {416 if (firstPreset) {
417 setStrategy({ kind: "header", presetIndex: 0 });417 setStrategy({ kind: "header", presetIndex: 0 });
418 setCustomHeaders(entriesFromSpecPreset(firstPreset));418 setCustomHeaders(entriesFromSpecPreset(firstPreset));
419 } else {419 } else {
420 // No header presets — default to "custom" so the headers editor is420 // No header presets — default to "custom" so the headers editor is
421 // visible immediately. Specs with no `security` block (e.g. Microsoft421 // visible immediately. Specs with no `security` block (e.g. Microsoft
422 // Graph) would otherwise leave the user staring at just the422 // Graph) would otherwise leave the user staring at just the
423 // Authentication heading with no way to add headers.423 // Authentication heading with no way to add headers.
424 setStrategy({ kind: "custom" });424 setStrategy({ kind: "custom" });
425 setCustomHeaders([]);425 setCustomHeaders([]);
426 }426 }
427 setAnalyzing(false);427 setAnalyzing(false);
428 };428 };
429 429
@@ -490,126 +490,126 @@ export default function AddOpenApiSource(props: { · 60 unmodified lines
490 const tokenUrl = resolveOAuthUrl(selectedOAuth2Preset.tokenUrl, resolvedBaseUrl);490 const tokenUrl = resolveOAuthUrl(selectedOAuth2Preset.tokenUrl, resolvedBaseUrl);
491 491
492 if (selectedOAuth2Preset.flow === "clientCredentials") {492 if (selectedOAuth2Preset.flow === "clientCredentials") {
493 // RFC 6749 §4.4: no user-interactive consent step. The client_secret493 // RFC 6749 §4.4: no user-interactive consent step. The client_secret
494 // is mandatory; the backend exchanges tokens inline and returns a494 // is mandatory; the backend exchanges tokens inline and returns a
495 // completed OAuth2Auth we can attach to the source directly.495 // completed OAuth2Auth we can attach to the source directly.
496 if (!oauth2ClientSecretSecretId) {496 if (!oauth2ClientSecretSecretId) {
497 setOauth2Error("client_credentials requires a client secret");497 setOauth2Error("client_credentials requires a client secret");
498 return;498 return;
499 }499 }
500 setStartingOAuth(true);500 setStartingOAuth(true);
501 const connectionId = openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow);501 const connectionId = openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow);
502 const exit = await doStartOAuth({
503 params: { scopeId },
504 payload: {
505 endpoint: tokenUrl,
506 redirectUrl: tokenUrl,
507 connectionId,
508 tokenScope: scopeId,
509 strategy: {
510 kind: "client-credentials",
511 tokenEndpoint: tokenUrl,
512 clientIdSecretId: oauth2ClientIdSecretId,
513 clientSecretSecretId: oauth2ClientSecretSecretId,
514 scopes: [...oauth2SelectedScopes],
515 },
516 pluginId: "openapi",
517 identityLabel: `${displayName} OAuth`,
518 },
519 });
520 setStartingOAuth(false);
521 if (Exit.isFailure(exit)) {
522 setOauth2Error(errorMessageFromExit(exit, "Failed to start OAuth"));
523 return;
524 }
525 const response = exit.value;
526 if (!response.completedConnection) {
527 setOauth2Error("client_credentials flow did not mint a connection");
528 return;
529 }
530 setOauth2AuthState({
531 fingerprint: selectedOAuth2Fingerprint,
532 auth: new OAuth2Auth({
533 kind: "oauth2",
534 connectionId: response.completedConnection.connectionId,
535 securitySchemeName: selectedOAuth2Preset.securitySchemeName,
536 flow: "clientCredentials",
537 tokenUrl,
538 authorizationUrl: null,
539 clientIdSecretId: oauth2ClientIdSecretId,
540 clientSecretSecretId: oauth2ClientSecretSecretId,
541 scopes: [...oauth2SelectedScopes],
542 }),
543 });
544 setOauth2Error(null);
545 return;
546 }
547
548 const authorizationUrl = resolveOAuthUrl(
549 Option.getOrElse(selectedOAuth2Preset.authorizationUrl, () => ""),
550 resolvedBaseUrl,
551 );
552 const issuerUrl = inferOAuthIssuerUrl(authorizationUrl);
553
554 await oauth.openAuthorization({
555 run: async () => {
502 const exit = await doStartOAuth({556 const exit = await doStartOAuth({
503 params: { scopeId },557 params: { scopeId },
504 payload: {558 payload: {
505 endpoint: tokenUrl,559 endpoint: authorizationUrl,
506 redirectUrl: tokenUrl,560 connectionId: openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow),
507 connectionId,
508 tokenScope: scopeId,561 tokenScope: scopeId,
562 redirectUrl: oauth2RedirectUrl,
509 strategy: {563 strategy: {
510 kind: "client-credentials",564 kind: "authorization-code",
565 authorizationEndpoint: authorizationUrl,
511 tokenEndpoint: tokenUrl,566 tokenEndpoint: tokenUrl,
567 issuerUrl,
512 clientIdSecretId: oauth2ClientIdSecretId,568 clientIdSecretId: oauth2ClientIdSecretId,
513 clientSecretSecretId: oauth2ClientSecretSecretId,569 clientSecretSecretId: oauth2ClientSecretSecretId ?? null,
514 scopes: [...oauth2SelectedScopes],570 scopes: [...oauth2SelectedScopes],
515 },571 },
516 pluginId: "openapi",572 pluginId: "openapi",
517 identityLabel: `${displayName} OAuth`,573 identityLabel: `${displayName} OAuth`,
518 },574 },
519 });575 });
520 setStartingOAuth(false);
521 if (Exit.isFailure(exit)) {576 if (Exit.isFailure(exit)) {
522 setOauth2Error(errorMessageFromExit(exit, "Failed to start OAuth"));577 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: OAuth popup API represents start failure by rejecting run()
523 return;578 throw new Error(errorMessageFromExit(exit, "Failed to start OAuth"));
524 }579 }
525 const response = exit.value;580 const response = exit.value;
526 if (!response.completedConnection) {581 if (response.authorizationUrl === null) {
527 setOauth2Error("client_credentials flow did not mint a connection");582 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: OAuth popup API represents start failure by rejecting run()
528 return;583 throw new Error("Unexpected response flow from server");
529 }584 }
585 return {
586 sessionId: response.sessionId,
587 authorizationUrl: response.authorizationUrl,
588 };
589 },
590 onSuccess: (result) => {
530 setOauth2AuthState({591 setOauth2AuthState({
531 fingerprint: selectedOAuth2Fingerprint,592 fingerprint: selectedOAuth2Fingerprint,
532 auth: new OAuth2Auth({593 auth: new OAuth2Auth({
533 kind: "oauth2",594 kind: "oauth2",
534 connectionId: response.completedConnection.connectionId,595 connectionId: result.connectionId,
535 securitySchemeName: selectedOAuth2Preset.securitySchemeName,596 securitySchemeName: selectedOAuth2Preset.securitySchemeName,
536 flow: "clientCredentials",597 flow: "authorizationCode",
537 tokenUrl,598 tokenUrl,
538 authorizationUrl: null,599 authorizationUrl,
600 issuerUrl,
539 clientIdSecretId: oauth2ClientIdSecretId,601 clientIdSecretId: oauth2ClientIdSecretId,
540 clientSecretSecretId: oauth2ClientSecretSecretId,602 clientSecretSecretId: oauth2ClientSecretSecretId,
541 scopes: [...oauth2SelectedScopes],603 scopes: [...oauth2SelectedScopes],
542 }),604 }),
543 });605 });
544 setOauth2Error(null);606 setOauth2Error(null);
545 return;607 },
546 }608 onError: (message) => {
547 609 setStartingOAuth(false);
548 const authorizationUrl = resolveOAuthUrl(610 setOauth2Error(message);
549 Option.getOrElse(selectedOAuth2Preset.authorizationUrl, () => ""),611 },
550 resolvedBaseUrl,612 });
551 );
552 const issuerUrl = inferOAuthIssuerUrl(authorizationUrl);
553
554 await oauth.openAuthorization({
555 run: async () => {
556 const exit = await doStartOAuth({
557 params: { scopeId },
558 payload: {
559 endpoint: authorizationUrl,
560 connectionId: openApiOAuthConnectionId(resolvedSourceId, selectedOAuth2Preset.flow),
561 tokenScope: scopeId,
562 redirectUrl: oauth2RedirectUrl,
563 strategy: {
564 kind: "authorization-code",
565 authorizationEndpoint: authorizationUrl,
566 tokenEndpoint: tokenUrl,
567 issuerUrl,
568 clientIdSecretId: oauth2ClientIdSecretId,
569 clientSecretSecretId: oauth2ClientSecretSecretId ?? null,
570 scopes: [...oauth2SelectedScopes],
571 },
572 pluginId: "openapi",
573 identityLabel: `${displayName} OAuth`,
574 },
575 });
576 if (Exit.isFailure(exit)) {
577 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: OAuth popup API represents start failure by rejecting run()
578 throw new Error(errorMessageFromExit(exit, "Failed to start OAuth"));
579 }
580 const response = exit.value;
581 if (response.authorizationUrl === null) {
582 // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: OAuth popup API represents start failure by rejecting run()
583 throw new Error("Unexpected response flow from server");
584 }
585 return {
586 sessionId: response.sessionId,
587 authorizationUrl: response.authorizationUrl,
588 };
589 },
590 onSuccess: (result) => {
591 setOauth2AuthState({
592 fingerprint: selectedOAuth2Fingerprint,
593 auth: new OAuth2Auth({
594 kind: "oauth2",
595 connectionId: result.connectionId,
596 securitySchemeName: selectedOAuth2Preset.securitySchemeName,
597 flow: "authorizationCode",
598 tokenUrl,
599 authorizationUrl,
600 issuerUrl,
601 clientIdSecretId: oauth2ClientIdSecretId,
602 clientSecretSecretId: oauth2ClientSecretSecretId,
603 scopes: [...oauth2SelectedScopes],
604 }),
605 });
606 setOauth2Error(null);
607 },
608 onError: (message) => {
609 setStartingOAuth(false);
610 setOauth2Error(message);
611 },
612 });
613 }, [613 }, [
614 selectedOAuth2Preset,614 selectedOAuth2Preset,
615 oauth2ClientIdSecretId,615 oauth2ClientIdSecretId,
packages/plugins/openapi/src/react/OpenApiSourceSummary.tsx+1−3
@@ -87,9 +87,7 @@ export default function OpenApiSourceSummary(props: { · 86 unmodified lines
87 }87 }
88 const connections = AsyncResult.isSuccess(connectionsResult) ? connectionsResult.value : [];88 const connections = AsyncResult.isSuccess(connectionsResult) ? connectionsResult.value : [];
89 const liveConnectionIds = new Set(connections.map((connection) => connection.id));89 const liveConnectionIds = new Set(connections.map((connection) => connection.id));
90 const scopeRanks = new Map(90 const scopeRanks = new Map(scopeStack.map((scope, index) => [scope.id, index] as const));
91 scopeStack.map((scope, index) => [scope.id, index] as const),
92 );
93 const credentialTargetScope = userScope;91 const credentialTargetScope = userScope;
94 const missing = missingCredentialLabels(source, bindings, credentialTargetScope, scopeRanks, {92 const missing = missingCredentialLabels(source, bindings, credentialTargetScope, scopeRanks, {
95 liveConnectionIds,93 liveConnectionIds,
packages/plugins/openapi/src/sdk/client-credentials-oauth.test.ts+43−37
@@ -8,7 +8,13 @@ · 7 unmodified lines
8 8
9import { afterEach, expect, layer } from "@effect/vitest";9import { afterEach, expect, layer } from "@effect/vitest";
10import { Effect, Layer, Schema } from "effect";10import { Effect, Layer, Schema } from "effect";
11import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi";11import {
12 HttpApi,
13 HttpApiBuilder,
14 HttpApiEndpoint,
15 HttpApiGroup,
16 OpenApi,
17} from "effect/unstable/httpapi";
12import { FetchHttpClient, HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http";18import { FetchHttpClient, HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http";
13import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";19import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
14 20
@@ -91,36 +97,40 @@ const mockClientCredentialsFetch = (args: { · 76 unmodified lines
91 readonly expiresIn?: number;97 readonly expiresIn?: number;
92}) => {98}) => {
93 let callIndex = 0;99 let callIndex = 0;
94 globalThis.fetch = Object.assign(async (_input: RequestInfo | URL, init?: RequestInit) => {100 globalThis.fetch = Object.assign(
95 const url = typeof _input === "string" ? _input : _input.toString();101 async (_input: RequestInfo | URL, init?: RequestInit) => {
96 if (!url.includes("token.example.com")) {102 const url = typeof _input === "string" ? _input : _input.toString();
97 return originalFetch(_input, init);103 if (!url.includes("token.example.com")) {
98 }104 return originalFetch(_input, init);
99 const bodyText =105 }
100 init?.body instanceof URLSearchParams106 const bodyText =
101 ? init.body.toString()107 init?.body instanceof URLSearchParams
102 : typeof init?.body === "string"108 ? init.body.toString()
103 ? init.body109 : typeof init?.body === "string"
104 : "";110 ? init.body
105 const params = new URLSearchParams(bodyText);111 : "";
106 args.calls.push({112 const params = new URLSearchParams(bodyText);
107 grantType: params.get("grant_type"),113 args.calls.push({
108 clientId: params.get("client_id"),114 grantType: params.get("grant_type"),
109 clientSecret: params.get("client_secret"),115 clientId: params.get("client_id"),
110 scope: params.get("scope"),116 clientSecret: params.get("client_secret"),
111 });117 scope: params.get("scope"),
112 const token = args.accessTokens[Math.min(callIndex, args.accessTokens.length - 1)] ?? "unknown";118 });
113 callIndex += 1;119 const token =
114 const body: Record<string, unknown> = {120 args.accessTokens[Math.min(callIndex, args.accessTokens.length - 1)] ?? "unknown";
115 access_token: token,121 callIndex += 1;
116 token_type: "Bearer",122 const body: Record<string, unknown> = {
117 };123 access_token: token,
118 if (typeof args.expiresIn === "number") body.expires_in = args.expiresIn;124 token_type: "Bearer",
119 return new Response(JSON.stringify(body), {125 };
120 status: 200,126 if (typeof args.expiresIn === "number") body.expires_in = args.expiresIn;
121 headers: { "content-type": "application/json" },127 return new Response(JSON.stringify(body), {
122 });128 status: 200,
123 }, { preconnect: originalFetch.preconnect });129 headers: { "content-type": "application/json" },
130 });
131 },
132 { preconnect: originalFetch.preconnect },
133 );
124};134};
125 135
126afterEach(() => {136afterEach(() => {
@@ -306,17 +316,13 @@ layer(TestLayer)("OpenAPI client_credentials OAuth", (it) => { · 179 unmodified lines
306 316
307 // Access-token secret is owned by the connection and filtered317 // Access-token secret is owned by the connection and filtered
308 // out of the user-facing secret list.318 // out of the user-facing secret list.
309 const userSecretIds = new Set(319 const userSecretIds = new Set((yield* userExec.secrets.list()).map((s) => String(s.id)));
310 (yield* userExec.secrets.list()).map((s) => String(s.id)),
311 );
312 expect(userSecretIds).toContain("petstore_client_id");320 expect(userSecretIds).toContain("petstore_client_id");
313 expect(userSecretIds).toContain("petstore_client_secret");321 expect(userSecretIds).toContain("petstore_client_secret");
314 expect(userSecretIds).not.toContain(`${auth.connectionId}.access_token`);322 expect(userSecretIds).not.toContain(`${auth.connectionId}.access_token`);
315 323
316 // Admin scope sees neither alice's connection nor her token.324 // Admin scope sees neither alice's connection nor her token.
317 const adminSecretIds = new Set(325 const adminSecretIds = new Set((yield* adminExec.secrets.list()).map((s) => String(s.id)));
318 (yield* adminExec.secrets.list()).map((s) => String(s.id)),
319 );
320 expect(adminSecretIds).not.toContain(`${auth.connectionId}.access_token`);326 expect(adminSecretIds).not.toContain(`${auth.connectionId}.access_token`);
321 }),327 }),
322 );328 );
packages/plugins/openapi/src/sdk/credential-status.test.ts+1−6
@@ -84,12 +84,7 @@ describe("OpenAPI credential status", () => { · 83 unmodified lines
84 it("does not treat personal bindings as satisfying org-level credential status", () => {84 it("does not treat personal bindings as satisfying org-level credential status", () => {
85 expect(85 expect(
86 missingCredentialLabels(source, bindings(userScope, allSlots), orgScope, scopeRanks),86 missingCredentialLabels(source, bindings(userScope, allSlots), orgScope, scopeRanks),
87 ).toEqual([87 ).toEqual(["Authorization", "Client ID", "Client Secret", "OAuth client connection"]);
88 "Authorization",
89 "Client ID",
90 "Client Secret",
91 "OAuth client connection",
92 ]);
93 });88 });
94 89
95 it("prefers the personal binding over a shared org binding", () => {90 it("prefers the personal binding over a shared org binding", () => {
packages/plugins/openapi/src/sdk/credential-status.ts+5−21
@@ -33,15 +33,9 @@ export const effectiveBindingForScope = ( · 32 unmodified lines
33): BindingRowForCredentialStatus | null =>33): BindingRowForCredentialStatus | null =>
34 rows34 rows
35 .filter(35 .filter(
36 (row) =>36 (row) => row.slot === slot && scopeRank(ranks, row.scopeId) >= scopeRank(ranks, targetScope),
37 row.slot === slot &&
38 scopeRank(ranks, row.scopeId) >= scopeRank(ranks, targetScope),
39 )37 )
40 .sort(38 .sort((a, b) => scopeRank(ranks, a.scopeId) - scopeRank(ranks, b.scopeId))[0] ?? null;
41 (a, b) =>
42 scopeRank(ranks, a.scopeId) -
43 scopeRank(ranks, b.scopeId),
44 )[0] ?? null;
45 39
46const hasSecretBinding = (40const hasSecretBinding = (
47 rows: readonly BindingRowForCredentialStatus[],41 rows: readonly BindingRowForCredentialStatus[],
@@ -59,9 +53,7 @@ const hasConnectionBinding = ( · 11 unmodified lines
59) => {53) => {
60 const binding = effectiveBindingForScope(rows, slot, targetScope, ranks);54 const binding = effectiveBindingForScope(rows, slot, targetScope, ranks);
61 if (binding?.value.kind !== "connection") return false;55 if (binding?.value.kind !== "connection") return false;
62 return liveConnectionIds56 return liveConnectionIds ? liveConnectionIds.has(binding.value.connectionId) : true;
63 ? liveConnectionIds.has(binding.value.connectionId)
64 : true;
65};57};
66 58
67const effectiveClientSecretSlot = (oauth2: {59const effectiveClientSecretSlot = (oauth2: {
@@ -106,17 +98,9 @@ export function missingCredentialLabels( · 38 unmodified lines
106 }98 }
107 99
108 if (100 if (
109 !hasConnectionBinding(101 !hasConnectionBinding(bindings, oauth2.connectionSlot, targetScope, ranks, liveConnectionIds)
110 bindings,
111 oauth2.connectionSlot,
112 targetScope,
113 ranks,
114 liveConnectionIds,
115 )
116 ) {102 ) {
117 missing.push(103 missing.push(oauth2.flow === "clientCredentials" ? "OAuth client connection" : "OAuth sign-in");
118 oauth2.flow === "clientCredentials" ? "OAuth client connection" : "OAuth sign-in",
119 );
120 }104 }
121 105
122 return missing;106 return missing;
packages/plugins/openapi/src/sdk/extract.ts+2−6
@@ -124,9 +124,7 @@ const extractRequestBody = ( · 123 unmodified lines
124 contentType: mediaType,124 contentType: mediaType,
125 schema: Option.fromNullishOr(media.schema),125 schema: Option.fromNullishOr(media.schema),
126 encoding: Option.fromNullishOr(126 encoding: Option.fromNullishOr(
127 buildEncodingRecord(127 buildEncodingRecord((media as { encoding?: Record<string, unknown> }).encoding),
128 (media as { encoding?: Record<string, unknown> }).encoding,
129 ),
130 ),128 ),
131 }),129 }),
132 );130 );
@@ -246,9 +244,7 @@ const extractServers = (doc: ParsedDocument): ServerInfo[] => · 113 unmodified lines
246 new ServerVariable({244 new ServerVariable({
247 default: String(v.default),245 default: String(v.default),
248 enum:246 enum:
249 enumValues && enumValues.length > 0247 enumValues && enumValues.length > 0 ? Option.some(enumValues) : Option.none(),
250 ? Option.some(enumValues)
251 : Option.none(),
252 description: Option.fromNullishOr(v.description),248 description: Option.fromNullishOr(v.description),
253 }),249 }),
254 ],250 ],
packages/plugins/openapi/src/sdk/form-urlencoded-body.test.ts+52−58
@@ -36,14 +36,12 @@ const memoryProvider: SecretProvider = (() => { · 35 unmodified lines
36 return {36 return {
37 key: "memory",37 key: "memory",
38 writable: true,38 writable: true,
39 get: (id, scope) =>39 get: (id, scope) => Effect.sync(() => store.get(`${scope}\u0000${id}`) ?? null),
40 Effect.sync(() => store.get(`${scope}\u0000${id}`) ?? null),
41 set: (id, value, scope) =>40 set: (id, value, scope) =>
42 Effect.sync(() => {41 Effect.sync(() => {
43 store.set(`${scope}\u0000${id}`, value);42 store.set(`${scope}\u0000${id}`, value);
44 }),43 }),
45 delete: (id, scope) =>44 delete: (id, scope) => Effect.sync(() => store.delete(`${scope}\u0000${id}`)),
46 Effect.sync(() => store.delete(`${scope}\u0000${id}`)),
47 list: () => Effect.sync(() => []),45 list: () => Effect.sync(() => []),
48 };46 };
49})();47})();
@@ -61,31 +59,29 @@ type Captured = { · 11 unmodified lines
61 59
62const startEchoServer = () =>60const startEchoServer = () =>
63 Effect.acquireRelease(61 Effect.acquireRelease(
64 Effect.callback<{ baseUrl: string; captured: Captured; close: () => void }>(62 Effect.callback<{ baseUrl: string; captured: Captured; close: () => void }>((resume) => {
65 (resume) => {63 const captured: Captured = { contentType: "", body: "" };
66 const captured: Captured = { contentType: "", body: "" };64 const server = createServer((req, res) => {
67 const server = createServer((req, res) => {65 const chunks: Buffer[] = [];
68 const chunks: Buffer[] = [];66 req.on("data", (c: Buffer) => chunks.push(c));
69 req.on("data", (c: Buffer) => chunks.push(c));67 req.on("end", () => {
70 req.on("end", () => {68 captured.contentType = req.headers["content-type"] ?? "";
71 captured.contentType = req.headers["content-type"] ?? "";69 captured.body = Buffer.concat(chunks).toString("utf8");
72 captured.body = Buffer.concat(chunks).toString("utf8");70 res.writeHead(200, { "content-type": "application/json" });
73 res.writeHead(200, { "content-type": "application/json" });71 res.end(JSON.stringify({ ok: true }));
74 res.end(JSON.stringify({ ok: true }));
75 });
76 });72 });
77 server.listen(0, "127.0.0.1", () => {73 });
78 const port = (server.address() as AddressInfo).port;74 server.listen(0, "127.0.0.1", () => {
79 resume(75 const port = (server.address() as AddressInfo).port;
80 Effect.succeed({76 resume(
81 baseUrl: `http://127.0.0.1:${port}`,77 Effect.succeed({
82 captured,78 baseUrl: `http://127.0.0.1:${port}`,
83 close: () => server.close(),79 captured,
84 }),80 close: () => server.close(),
85 );81 }),
86 });82 );
87 },83 });
88 ),84 }),
89 (s) => Effect.sync(() => s.close()),85 (s) => Effect.sync(() => s.close()),
90 );86 );
91 87
@@ -130,40 +126,38 @@ const formSpec = JSON.stringify({ · 38 unmodified lines
130});126});
131 127
132describe("OpenAPI non-JSON request body serialization", () => {128describe("OpenAPI non-JSON request body serialization", () => {
133 it.effect(129 it.effect("form-urlencoded object body is properly encoded (no '[object Object]')", () =>
134 "form-urlencoded object body is properly encoded (no '[object Object]')",130 Effect.gen(function* () {
135 () =>131 const { baseUrl, captured } = yield* startEchoServer();
136 Effect.gen(function* () {
137 const { baseUrl, captured } = yield* startEchoServer();
138 132
139 const executor = yield* createExecutor(133 const executor = yield* createExecutor(
140 makeTestConfig({134 makeTestConfig({
141 plugins: [135 plugins: [
142 openApiPlugin({ httpClientLayer: FetchHttpClient.layer }),136 openApiPlugin({ httpClientLayer: FetchHttpClient.layer }),
143 memorySecretsPlugin(),137 memorySecretsPlugin(),
144 ] as const,138 ] as const,
145 }),139 }),
146 );140 );
147 141
148 yield* executor.openapi.addSpec({142 yield* executor.openapi.addSpec({
149 spec: formSpec,143 spec: formSpec,
150 scope: TEST_SCOPE,144 scope: TEST_SCOPE,
151 namespace: "form",145 namespace: "form",
152 baseUrl,146 baseUrl,
153 });147 });
154 148
155 yield* executor.tools.invoke(149 yield* executor.tools.invoke(
156 "form.forms.submit",150 "form.forms.submit",
157 { body: { name: "Acme", email: "a@b.com" } },151 { body: { name: "Acme", email: "a@b.com" } },
158 autoApprove,152 autoApprove,
159 );153 );
160 154
161 expect(captured.contentType).toBe("application/x-www-form-urlencoded");155 expect(captured.contentType).toBe("application/x-www-form-urlencoded");
162 expect(captured.body).not.toBe("[object Object]");156 expect(captured.body).not.toBe("[object Object]");
163 157
164 const parsed = new URLSearchParams(captured.body);158 const parsed = new URLSearchParams(captured.body);
165 expect(parsed.get("name")).toBe("Acme");159 expect(parsed.get("name")).toBe("Acme");
166 expect(parsed.get("email")).toBe("a@b.com");160 expect(parsed.get("email")).toBe("a@b.com");
167 }),161 }),
168 );162 );
169});163});
packages/plugins/openapi/src/sdk/index.test.ts+4−3
@@ -195,9 +195,10 @@ describe("OpenAPI plugin", () => { · 194 unmodified lines
195 195
196 const vars = Option.getOrThrow(server.variables);196 const vars = Option.getOrThrow(server.variables);
197 expect(vars.region!.default).toBe("us");197 expect(vars.region!.default).toBe("us");
198 expect(198 expect(Option.getOrElse(vars.region!.enum, () => [] as readonly string[])).toEqual([
199 Option.getOrElse(vars.region!.enum, () => [] as readonly string[]),199 "us",
200 ).toEqual(["us", "de"]);200 "de",
201 ]);
201 expect(Option.getOrElse(vars.region!.description, () => "")).toBe(202 expect(Option.getOrElse(vars.region!.description, () => "")).toBe(
202 "The data-storage-location for an organization",203 "The data-storage-location for an organization",
203 );204 );
packages/plugins/openapi/src/sdk/invoke.ts+7−20
@@ -191,9 +191,7 @@ const isXmlContentType = (ct: string | null | undefined): boolean => { · 190 unmodified lines
191 const normalized = normalizeContentType(ct);191 const normalized = normalizeContentType(ct);
192 if (!normalized) return false;192 if (!normalized) return false;
193 return (193 return (
194 normalized === "application/xml" ||194 normalized === "application/xml" || normalized === "text/xml" || normalized.endsWith("+xml")
195 normalized === "text/xml" ||
196 normalized.endsWith("+xml")
197 );195 );
198};196};
199 197
@@ -291,8 +289,7 @@ const serializeFormUrlEncoded = ( · 91 unmodified lines
291 parts.push(`${encKey}=${encodeFormValue(v, allowReserved)}`);289 parts.push(`${encKey}=${encodeFormValue(v, allowReserved)}`);
292 }290 }
293 } else {291 } else {
294 const sep =292 const sep = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ",";
295 style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ",";
296 parts.push(293 parts.push(
297 `${encKey}=${encodeFormValue(294 `${encKey}=${encodeFormValue(
298 raw.map((v) => (typeof v === "object" ? JSON.stringify(v) : String(v))).join(sep),295 raw.map((v) => (typeof v === "object" ? JSON.stringify(v) : String(v))).join(sep),
@@ -313,18 +310,13 @@ const serializeFormUrlEncoded = ( · 14 unmodified lines
313 // `%5B` / `%5D`. Matches swagger-client's behaviour and remains310 // `%5B` / `%5D`. Matches swagger-client's behaviour and remains
314 // accepted by common server-side parsers (qs, Rails, etc.).311 // accepted by common server-side parsers (qs, Rails, etc.).
315 parts.push(312 parts.push(
316 `${encodeURIComponent(`${key}[${subkey}]`)}=${encodeFormValue(313 `${encodeURIComponent(`${key}[${subkey}]`)}=${encodeFormValue(subval, allowReserved)}`,
317 subval,
318 allowReserved,
319 )}`,
320 );314 );
321 }315 }
322 } else if (explode) {316 } else if (explode) {
323 // form + explode=true on object: sub-keys become top-level fields.317 // form + explode=true on object: sub-keys become top-level fields.

40 more lines aren’t shown here.

packages/plugins/openapi/src/sdk/multi-scope-bearer.test.ts+406−424

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

packages/plugins/openapi/src/sdk/multi-scope-oauth.test.ts+71−57

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

packages/plugins/openapi/src/sdk/non-json-body.test.ts+29−53

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

packages/plugins/openapi/src/sdk/openapi-utils.ts+2−7

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

packages/plugins/openapi/src/sdk/plugin.test.ts+101−119

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

packages/plugins/openapi/src/sdk/preview-oauth2.test.ts+1−3

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

packages/plugins/openapi/src/sdk/preview.ts+4−10

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

packages/plugins/openapi/tsconfig.json+1−4

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

packages/plugins/workos-vault/src/react/WorkOSVaultSettings.tsx+2−2

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

packages/plugins/workos-vault/src/sdk/plugin.ts+26−27

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

packages/plugins/workos-vault/src/sdk/secret-store.test.ts+102−131

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

packages/plugins/workos-vault/src/sdk/secret-store.ts+26−49

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

packages/plugins/workos-vault/src/sdk/testing.ts+11−31

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

packages/react/src/api/reactivity-keys.tsx+2−8

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

packages/react/src/components/calendar.tsx+1−4

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

packages/react/src/components/chart.tsx+1−4

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

packages/react/src/components/code-block.tsx+1−3

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

packages/react/src/components/markdown.tsx+3−1

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

packages/react/src/components/schema-explorer.tsx+1−5

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

packages/react/src/components/tool-tree.tsx+5−9

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

packages/react/src/hooks/use-policy-actions.ts+7−6

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

packages/react/src/pages/secrets.tsx+3−13

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

packages/react/src/plugins/headers-list.tsx+1−3

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

packages/react/src/plugins/namespace.ts+1−3

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

packages/react/src/plugins/secret-id.test.ts+2−8

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

packages/react/src/plugins/secret-id.tsx+1−4

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

packages/react/src/plugins/source-identity.tsx+1−5

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

scripts/check-release-notes.ts+2−8

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

scripts/oxlint-plugin-executor/rules/no-redundant-primitive-cast.js+2−9

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

scripts/oxlint-plugin-executor/rules/no-unsupported-effect-api.js+1−2

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

scripts/oxlint-plugin-executor/utils.js+3−3

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

scripts/publish-packages.ts+2−6

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

tests/daemon-bootstrap.test.ts+30−23

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

tests/daemon-state.test.ts+2−1

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

tests/presets-reachable.test.ts+2−1

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

tests/release-bootstrap-smoke.test.ts+118−144

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

tests/source-identity.test.ts+1−4

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

tests/tools-cli.test.ts+10−6

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