Showing the changes to apps/cloud/src/engine/execution-stack.ts (1 of 4 files). Show all
Unified · Split
| @@ -41,6 +41,7 @@ import { · 40 unmodified lines | |||
| 41 | HostConfig, | 41 | HostConfig, |
| 42 | PluginsProvider, | 42 | PluginsProvider, |
| 43 | collectTables, | 43 | collectTables, |
| 44 | type HostConfigShape, | ||
| 44 | } from "@executor-js/api/server"; | 45 | } from "@executor-js/api/server"; |
| 45 | import { makeDynamicWorkerExecutor } from "@executor-js/runtime-dynamic-worker"; | 46 | import { makeDynamicWorkerExecutor } from "@executor-js/runtime-dynamic-worker"; |
| 46 | import type { AnyPlugin } from "@executor-js/sdk"; | 47 | import type { AnyPlugin } from "@executor-js/sdk"; |
| @@ -88,18 +89,69 @@ export const CloudPluginsProvider: Layer.Layer<PluginsProvider> = Layer.succeed( · 41 unmodified lines | |||
| 88 | */ | 89 | */ |
| 89 | export const CLOUD_MOUNT_PREFIX = "/api" as const; | 90 | export const CLOUD_MOUNT_PREFIX = "/api" as const; |
| 90 | 91 | ||
| 91 | export const CloudHostConfig: Layer.Layer<HostConfig> = Layer.sync(HostConfig, () => ({ | 92 | /** |
| 92 | // SSRF / private-network egress guard. Config-driven, NOT a test flag: | 93 | * Cloud's host config, parameterized by the ONE seam its two execution planes |
| 93 | // production leaves `ALLOW_LOCAL_NETWORK` unset so the guard stays ON (`false`); | 94 | * genuinely disagree about: where a deferred tool-catalog refresh runs. |
| 94 | // the e2e dev-server env opts in with `"true"` so in-scenario fixture | 95 | * |
| 95 | // servers on localhost are reachable. See `hosted-http-client.ts`. | 96 | * The MCP session Durable Object holds its database handle for the life of the |
| 96 | allowLocalNetwork: env.ALLOW_LOCAL_NETWORK === "true", | 97 | * session, so it can hand the batch straight to `ctx.waitUntil` and forget it. |
| 97 | webBaseUrl: env.VITE_PUBLIC_SITE_URL ?? "https://executor.sh", | 98 | * The HTTP plane's handle dies with its request and gets no seam at all (see |
| 98 | oauthCallbackPath: `${CLOUD_MOUNT_PREFIX}/oauth/callback`, | 99 | * {@link CloudHostConfig}). Nothing else about the config differs, which is why |
| 99 | // WorkOS Vault is cloud's credential storage implementation detail, not a | 100 | * this is a parameter rather than a second config module. |
| 100 | // user-selectable provider surface. | 101 | */ |
| 101 | exposeCredentialProviders: false, | 102 | export const makeCloudHostConfig = ( |
| 102 | })); | 103 | deferToolSync: HostConfigShape["deferToolSync"], |
| 104 | ): Layer.Layer<HostConfig> => | ||
| 105 | Layer.sync(HostConfig, () => ({ | ||
| 106 | // SSRF / private-network egress guard. Config-driven, NOT a test flag: | ||
| 107 | // production leaves `ALLOW_LOCAL_NETWORK` unset so the guard stays ON (`false`); | ||
| 108 | // the e2e dev-server env opts in with `"true"` so in-scenario fixture | ||
| 109 | // servers on localhost are reachable. See `hosted-http-client.ts`. | ||
| 110 | allowLocalNetwork: env.ALLOW_LOCAL_NETWORK === "true", | ||
| 111 | webBaseUrl: env.VITE_PUBLIC_SITE_URL ?? "https://executor.sh", | ||
| 112 | oauthCallbackPath: `${CLOUD_MOUNT_PREFIX}/oauth/callback`, | ||
| 113 | // WorkOS Vault is cloud's credential storage implementation detail, not a | ||
| 114 | // user-selectable provider surface. | ||
| 115 | exposeCredentialProviders: false, | ||
| 116 | deferToolSync, | ||
| 117 | })); | ||
| 118 | |||
| 119 | /** | ||
| 120 | * The HTTP plane's config, and the one seam it CANNOT fill: `deferToolSync` is | ||
| 121 | * absent, so a tools read over `/api/*` still refreshes an expired catalog | ||
| 122 | * inline. | ||
| 123 | * | ||
| 124 | * Not an omission. The postgres pool a request's executor closes over is | ||
| 125 | * acquired and released inside `requestScopedMiddleware`'s `Effect.scoped`, | ||
| 126 | * which wraps the ROUTE HANDLER — so the pool is torn down while the handler's | ||
| 127 | * `HttpServerResponse` is still a value, strictly before `HttpEffect.toHandled` | ||
| 128 | * writes it. There is therefore no window on this plane that is both after the | ||
| 129 | * response and before the socket closes: work scheduled with `waitUntil` finds | ||
| 130 | * a dead pool, and work drained inside the scope is simply inline work with | ||
| 131 | * extra steps. `api.request-scope.node.test.ts` pins that ordering, so a future | ||
| 132 | * change to the request lifecycle will say so rather than making this comment | ||
| 133 | * quietly wrong. | ||
| 134 | * | ||
| 135 | * Holding the pool open past the response instead — deferring the scope close | ||
| 136 | * into `waitUntil` — is the one thing that would make the window exist, and it | ||
| 137 | * is the thing `closePostgres` was written to prevent. That finalizer awaits | ||
| 138 | * the teardown precisely so live-plus-closing sockets stay bounded by what is | ||
| 139 | * in flight; unbounded, the backlog is the sustained-load cascade recorded in | ||
| 140 | * `db/db.ts`. Trading a socket leak for a background refresh is not a trade | ||
| 141 | * this plane should make, and it would make it on EVERY request, not just the | ||
| 142 | * ones that deferred. | ||
| 143 | * | ||
| 144 | * The alternative — giving the batch a short-lived pool of its own via | ||
| 145 | * `makeDbLayer()` — does not fit either: the task the sdk hands over is already | ||
| 146 | * bound to the executor that built it, so re-targeting it means rebuilding a | ||
| 147 | * whole second executor in the background. That is a background scheduler, a | ||
| 148 | * different feature with a different failure surface, not a wiring detail. | ||
| 149 | * | ||
| 150 | * Cloud's high-volume tools read is `tools/list` over MCP, and that plane DOES | ||
| 151 | * defer: the session Durable Object holds its database handle for the life of | ||
| 152 | * the session (see `mcp/session-durable-object.ts`). | ||
| 153 | */ | ||
| 154 | export const CloudHostConfig: Layer.Layer<HostConfig> = makeCloudHostConfig(undefined); | ||
| 103 | 155 | ||
| 104 | export const CloudCodeExecutorProvider: Layer.Layer<CodeExecutorProvider> = Layer.sync( | 156 | export const CloudCodeExecutorProvider: Layer.Layer<CodeExecutorProvider> = Layer.sync( |
| 105 | CodeExecutorProvider, | 157 | CodeExecutorProvider, |
| @@ -114,13 +166,16 @@ export const CloudCodeExecutorProvider: Layer.Layer<CodeExecutorProvider> = Laye · 8 unmodified lines | |||
| 114 | * exported so that overlay builds over the SAME four seams. There is no neutral | 166 | * exported so that overlay builds over the SAME four seams. There is no neutral |
| 115 | * no-op-decorator variant anymore: every cloud execution meters. | 167 | * no-op-decorator variant anymore: every cloud execution meters. |
| 116 | */ | 168 | */ |
| 169 | export const makeCloudExecutionSeamsLayer = ( | ||
| 170 | hostConfig: Layer.Layer<HostConfig>, | ||
| 171 | ): Layer.Layer< | ||
| 172 | DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider, | ||
| 173 | never, | ||
| 174 | DbService | ||
| 175 | > => Layer.mergeAll(CloudDbProvider, CloudPluginsProvider, hostConfig, CloudCodeExecutorProvider); | ||
| 176 | |||
| 117 | export const CloudExecutionSeamsLayer: Layer.Layer< | 177 | export const CloudExecutionSeamsLayer: Layer.Layer< |
| 118 | DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider, | 178 | DbProvider | PluginsProvider | HostConfig | CodeExecutorProvider, |
| 119 | never, | 179 | never, |
| 120 | DbService | 180 | DbService |
| 121 | > = Layer.mergeAll( | 181 | > = makeCloudExecutionSeamsLayer(CloudHostConfig); |
| 122 | CloudDbProvider, | ||
| 123 | CloudPluginsProvider, | ||
| 124 | CloudHostConfig, | ||
| 125 | CloudCodeExecutorProvider, | ||
| 126 | ); | ||