@@ -11,6 +11,7 @@ import {
1111 runSqliteDataMigrations ,
1212 type AnyPlugin ,
1313 type Executor ,
14+ type FumaDb ,
1415} from "@executor-js/sdk" ;
1516import { collectTables } from "@executor-js/api/server" ;
1617import { loadPluginsFromJsonc } from "@executor-js/config" ;
@@ -92,6 +93,7 @@ const loadLocalPlugins = (options: LocalExecutorOptions = {}) =>
9293interface LocalExecutorBundle {
9394 readonly executor : Executor < LocalPlugins > ;
9495 readonly plugins : LocalPlugins ;
96+ readonly db : FumaDb ;
9597 /** Where this daemon's web UI is reachable, resolved once at boot. Surfaced
9698 * so callers building user-facing links (MCP artifact deep links) use the
9799 * same origin the executor itself was configured with. */
@@ -142,6 +144,42 @@ const handleOrNull = (promise: ReturnType<typeof createExecutorHandle>) =>
142144 ) ,
143145 ) ;
144146
147+ const createExecutorBundleForDb = ( db : FumaDb , cwd : string , plugins : LocalPlugins ) =>
148+ Effect . gen ( function * ( ) {
149+ const tenantId = makeTenantId ( cwd ) ;
150+ // webBaseUrl is where the executor's web UI listens - same port as the
151+ // daemon API since the daemon serves both. Mirrors serve.ts's port
152+ // resolution so a custom $PORT flows through. EXECUTOR_WEB_BASE_URL
153+ // overrides entirely for deployments where the UI is on a different host.
154+ const webBaseUrl =
155+ process . env . EXECUTOR_WEB_BASE_URL ?? `http://localhost:${ process . env . PORT ?? "4788" } ` ;
156+
157+ const executor = yield * createExecutor ( {
158+ tenant : Tenant . make ( tenantId ) ,
159+ subject : Subject . make ( LOCAL_SUBJECT ) ,
160+ db,
161+ plugins,
162+ onIntegrationChange : ( event ) =>
163+ localAnalytics . record (
164+ event . kind === "added" ? "integration_added" : "integration_removed" ,
165+ { plugin_key : event . pluginKey } ,
166+ ) ,
167+ onElicitation : "accept-all" ,
168+ oauthEndpointUrlPolicy : { allowHttp : true } ,
169+ // EXPLICIT OAuth callback - the daemon serves the v2 `/api/oauth/callback`
170+ // route on the same origin as the web UI. Derived from `webBaseUrl`
171+ // (loopback localhost is correct + intended for the local CLI, but it
172+ // is wired explicitly here rather than relying on a hidden default).
173+ redirectUri : new URL ( "/api/oauth/callback" , webBaseUrl ) . toString ( ) ,
174+ // Built-in agent-facing tools (integrations / connections / policies).
175+ coreTools : {
176+ webBaseUrl,
177+ } ,
178+ } ) ;
179+
180+ return { executor, plugins, db, webBaseUrl } ;
181+ } ) ;
182+
145183const createLocalExecutorLayer = ( options : LocalExecutorOptions = { } ) => {
146184 const storage = resolveStorage ( ) ;
147185
@@ -184,35 +222,8 @@ const createLocalExecutorLayer = (options: LocalExecutorOptions = {}) => {
184222 ) ,
185223 ) ;
186224
187- // webBaseUrl is where the executor's web UI listens — same port as the
188- // daemon API since the daemon serves both. Mirrors serve.ts's port
189- // resolution so a custom $PORT flows through. EXECUTOR_WEB_BASE_URL
190- // overrides entirely for deployments where the UI is on a different host.
191- const webBaseUrl =
192- process . env . EXECUTOR_WEB_BASE_URL ?? `http://localhost:${ process . env . PORT ?? "4788" } ` ;
193-
194- const executor = yield * createExecutor ( {
195- tenant : Tenant . make ( tenantId ) ,
196- subject : Subject . make ( LOCAL_SUBJECT ) ,
197- db : sqlite . db ,
198- plugins,
199- onIntegrationChange : ( event ) =>
200- localAnalytics . record (
201- event . kind === "added" ? "integration_added" : "integration_removed" ,
202- { plugin_key : event . pluginKey } ,
203- ) ,
204- onElicitation : "accept-all" ,
205- oauthEndpointUrlPolicy : { allowHttp : true } ,
206- // EXPLICIT OAuth callback — the daemon serves the v2 `/api/oauth/callback`
207- // route on the same origin as the web UI. Derived from `webBaseUrl`
208- // (loopback localhost is correct + intended for the local CLI, but it
209- // is wired explicitly here rather than relying on a hidden default).
210- redirectUri : new URL ( "/api/oauth/callback" , webBaseUrl ) . toString ( ) ,
211- // Built-in agent-facing tools (integrations / connections / policies).
212- coreTools : {
213- webBaseUrl,
214- } ,
215- } ) ;
225+ const bundle = yield * createExecutorBundleForDb ( sqlite . db , cwd , plugins ) ;
226+ const executor = bundle . executor ;
216227
217228 if ( migration . migrated ) {
218229 console . warn (
@@ -243,7 +254,7 @@ const createLocalExecutorLayer = (options: LocalExecutorOptions = {}) => {
243254 ) ;
244255 }
245256
246- return { executor , plugins , webBaseUrl } ;
257+ return bundle ;
247258 } ) ,
248259 ) ;
249260} ;
@@ -257,6 +268,7 @@ export const createExecutorHandle = async (options: LocalExecutorOptions = {}) =
257268 executor : bundle . executor ,
258269 plugins : bundle . plugins ,
259270 webBaseUrl : bundle . webBaseUrl ,
271+ db : bundle . db ,
260272 dispose : async ( ) => {
261273 await Effect . runPromise ( Effect . ignore ( bundle . executor . close ( ) ) ) ;
262274 await ignorePromiseFailure ( "disposeRuntime" , ( ) => runtime . dispose ( ) ) ;
@@ -310,6 +322,20 @@ const loadSharedHandle = (): Promise<ExecutorHandle> => {
310322export const getExecutor = ( ) => loadSharedHandle ( ) . then ( ( handle ) => handle . executor ) ;
311323export const getExecutorBundle = ( ) => loadSharedHandle ( ) ;
312324
325+ export const createScopedExecutorHandle = async ( options : LocalExecutorOptions = { } ) => {
326+ const shared = await getExecutorBundle ( ) ;
327+ const { cwd, plugins } = await Effect . runPromise ( loadLocalPlugins ( options ) ) ;
328+ const bundle = await Effect . runPromise ( createExecutorBundleForDb ( shared . db , cwd , plugins ) ) ;
329+
330+ return {
331+ executor : bundle . executor ,
332+ plugins : bundle . plugins ,
333+ dispose : async ( ) => {
334+ await Effect . runPromise ( Effect . ignore ( bundle . executor . close ( ) ) ) ;
335+ } ,
336+ } ;
337+ } ;
338+
313339export const disposeExecutor = async ( ) : Promise < void > => {
314340 const currentHandlePromise = sharedHandlePromise ;
315341 sharedHandlePromise = null ;
0 commit comments