Unified · Split
| @@ -1,10 +1,6 @@ | ||
| 1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | |
| 3 | −import { | |
| 4 | − protectedResourceMetadataUrlFor, | |
| 5 | − resourceUrlFor, | |
| 6 | − toolkitSlugFromRequest, | |
| 7 | −} from "./auth"; | |
| 3 | +import { protectedResourceMetadataUrlFor, resourceUrlFor, toolkitSlugFromRequest } from "./auth"; | |
| 8 | 4 | import { classifyMcpPath, prepareMcpOrgScope } from "./mount"; |
| 9 | 5 | |
| 10 | 6 | describe("cloud MCP toolkit route normalization", () => { |
| @@ -50,9 +46,7 @@ describe("cloud MCP toolkit route normalization", () => { · 39 unmodified lines | ||
| 50 | 46 | |
| 51 | 47 | it("builds toolkit-specific resource and metadata URLs", () => { |
| 52 | 48 | expect(resourceUrlFor(null, "deploy")).toBe("https://executor.sh/mcp/toolkits/deploy"); |
| 53 | − expect(resourceUrlFor("acme", "deploy")).toBe( | |
| 54 | − "https://executor.sh/acme/mcp/toolkits/deploy", | |
| 55 | − ); | |
| 49 | + expect(resourceUrlFor("acme", "deploy")).toBe("https://executor.sh/acme/mcp/toolkits/deploy"); | |
| 56 | 50 | expect(protectedResourceMetadataUrlFor(null, "deploy")).toBe( |
| 57 | 51 | "https://executor.sh/.well-known/oauth-protected-resource/mcp/toolkits/deploy", |
| 58 | 52 | ); |
| @@ -78,11 +78,7 @@ const matchMcpSuffix = (segments: readonly string[]): MatchedMcpSuffix | undefin · 77 unmodified lines | ||
| 78 | 78 | const organizationId = orgSelectorSegment(segments[0]); |
| 79 | 79 | return organizationId ? { organizationId } : undefined; |
| 80 | 80 | } |
| 81 | − if ( | |
| 82 | − segments.length === 4 && | |
| 83 | − segments[1] === "mcp" && | |
| 84 | − segments[2] === "toolkits" | |
| 85 | − ) { | |
| 81 | + if (segments.length === 4 && segments[1] === "mcp" && segments[2] === "toolkits") { | |
| 86 | 82 | const organizationId = orgSelectorSegment(segments[0]); |
| 87 | 83 | const toolkitSlug = segments[3]; |
| 88 | 84 | return organizationId && toolkitSlug ? { organizationId, toolkitSlug } : undefined; |
| @@ -114,9 +110,7 @@ export const classifyMcpPath = (pathname: string): McpRoute => { · 25 unmodified lines | ||
| 114 | 110 | const prmPrefix = "/.well-known/oauth-protected-resource"; |
| 115 | 111 | if (pathname.startsWith(`${prmPrefix}/`)) { |
| 116 | 112 | const matched = matchMcpSuffix(segments.slice(2)); |
| 117 | − return matched === undefined | |
| 118 | − ? null | |
| 119 | − : { kind: "oauth-protected-resource", ...matched }; | |
| 113 | + return matched === undefined ? null : { kind: "oauth-protected-resource", ...matched }; | |
| 120 | 114 | } |
| 121 | 115 | |
| 122 | 116 | // MCP transport: `/mcp` or `/<org>/mcp`. |
| @@ -46,8 +46,7 @@ import { BetterAuth } from "../auth/better-auth"; · 45 unmodified lines | ||
| 46 | 46 | // --------------------------------------------------------------------------- |
| 47 | 47 | |
| 48 | 48 | const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource"; |
| 49 | −const TOOLKIT_PROTECTED_RESOURCE_METADATA_PATH = | |
| 50 | − `${PROTECTED_RESOURCE_METADATA_PATH}/mcp/toolkits/:toolkitSlug`; | |
| 49 | +const TOOLKIT_PROTECTED_RESOURCE_METADATA_PATH = `${PROTECTED_RESOURCE_METADATA_PATH}/mcp/toolkits/:toolkitSlug`; | |
| 51 | 50 | const AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server"; |
| 52 | 51 | const TOOLKIT_MCP_SEGMENT = "/mcp/toolkits/"; |
| 53 | 52 | |
| @@ -37,8 +37,7 @@ export default defineExecutorConfig({ · 36 unmodified lines | ||
| 37 | 37 | onepasswordHttpPlugin(), |
| 38 | 38 | desktopSettingsPlugin({ |
| 39 | 39 | webBaseUrl: |
| 40 | − process.env.EXECUTOR_WEB_BASE_URL ?? | |
| 41 | − `http://localhost:${process.env.PORT ?? "4788"}`, | |
| 40 | + process.env.EXECUTOR_WEB_BASE_URL ?? `http://localhost:${process.env.PORT ?? "4788"}`, | |
| 42 | 41 | }), |
| 43 | 42 | ] as const, |
| 44 | 43 | }); |
| @@ -49,8 +49,7 @@ const makeTenantId = (cwd: string): string => { · 48 unmodified lines | ||
| 49 | 49 | return `${folder}-${hash}`; |
| 50 | 50 | }; |
| 51 | 51 | |
| 52 | −const resolvePluginConfigPath = (scopeDir: string): string => | |
| 53 | − join(scopeDir, "executor.jsonc"); | |
| 52 | +const resolvePluginConfigPath = (scopeDir: string): string => join(scopeDir, "executor.jsonc"); | |
| 54 | 53 | |
| 55 | 54 | // Plugins reach the host through two doors that compose: |
| 56 | 55 | // - `executor.config.ts`'s static tuple |
| @@ -69,14 +68,11 @@ const loadLocalPlugins = (options: LocalExecutorOptions = {}) => · 12 unmodified lines | ||
| 69 | 68 | activeToolkitSlug: options.activeToolkitSlug, |
| 70 | 69 | }); |
| 71 | 70 | const dynamicPlugins = |
| 72 | − (yield* Effect.promise(() => | |
| 73 | − loadPluginsFromJsonc({ path: resolvePluginConfigPath(cwd) }), | |
| 74 | − )) ?? []; | |
| 71 | + (yield* Effect.promise(() => loadPluginsFromJsonc({ path: resolvePluginConfigPath(cwd) }))) ?? | |
| 72 | + []; | |
| 75 | 73 | |
| 76 | 74 | const staticPackageNames = new Set( |
| 77 | − staticPlugins | |
| 78 | − .map((plugin) => plugin.packageName) | |
| 79 | − .filter((name): name is string => !!name), | |
| 75 | + staticPlugins.map((plugin) => plugin.packageName).filter((name): name is string => !!name), | |
| 80 | 76 | ); |
| 81 | 77 | const dedupedDynamic = dynamicPlugins.filter((plugin) => { |
| 82 | 78 | if (plugin.packageName && staticPackageNames.has(plugin.packageName)) { |
| @@ -101,23 +97,18 @@ interface LocalExecutorBundle { · 18 unmodified lines | ||
| 101 | 97 | readonly plugins: LocalPlugins; |
| 102 | 98 | } |
| 103 | 99 | |
| 104 | −class LocalExecutorTag extends Context.Service< | |
| 105 | − LocalExecutorTag, | |
| 106 | − LocalExecutorBundle | |
| 107 | −>()("@executor-js/local/Executor") {} | |
| 100 | +class LocalExecutorTag extends Context.Service<LocalExecutorTag, LocalExecutorBundle>()( | |
| 101 | + "@executor-js/local/Executor", | |
| 102 | +) {} | |
| 108 | 103 | |
| 109 | 104 | export type LocalExecutor = LocalExecutorBundle["executor"]; |
| 110 | 105 | |
| 111 | −class LocalExecutorCreateError extends Data.TaggedError( | |
| 112 | − "LocalExecutorCreateError", | |
| 113 | −)<{ | |
| 106 | +class LocalExecutorCreateError extends Data.TaggedError("LocalExecutorCreateError")<{ | |
| 114 | 107 | readonly message: string; |
| 115 | 108 | readonly cause: unknown; |
| 116 | 109 | }> {} |
| 117 | 110 | |
| 118 | −class LocalExecutorDisposeError extends Data.TaggedError( | |
| 119 | − "LocalExecutorDisposeError", | |
| 120 | −)<{ | |
| 111 | +class LocalExecutorDisposeError extends Data.TaggedError("LocalExecutorDisposeError")<{ | |
| 121 | 112 | readonly operation: "createHandle" | "disposeExecutor" | "disposeRuntime"; |
| 122 | 113 | readonly cause: unknown; |
| 123 | 114 | }> {} |
| @@ -142,13 +133,10 @@ const handleOrNull = (promise: ReturnType<typeof createExecutorHandle>) => · 18 unmodified lines | ||
| 142 | 133 | Effect.runPromise( |
| 143 | 134 | Effect.tryPromise({ |
| 144 | 135 | try: () => promise, |
| 145 | − catch: (cause) => | |
| 146 | − new LocalExecutorDisposeError({ operation: "createHandle", cause }), | |
| 136 | + catch: (cause) => new LocalExecutorDisposeError({ operation: "createHandle", cause }), | |
| 147 | 137 | }).pipe( |
| 148 | 138 | Effect.catch(() => |
| 149 | − Effect.succeed<Awaited<ReturnType<typeof createExecutorHandle>> | null>( | |
| 150 | − null, | |
| 151 | − ), | |
| 139 | + Effect.succeed<Awaited<ReturnType<typeof createExecutorHandle>> | null>(null), | |
| 152 | 140 | ), |
| 153 | 141 | ), |
| 154 | 142 | ); |
| @@ -212,8 +200,7 @@ const createLocalExecutorLayer = (options: LocalExecutorOptions = {}) => { · 57 unmodified lines | ||
| 212 | 200 | // resolution so a custom $PORT flows through. EXECUTOR_WEB_BASE_URL |
| 213 | 201 | // overrides entirely for deployments where the UI is on a different host. |
| 214 | 202 | const webBaseUrl = |
| 215 | − process.env.EXECUTOR_WEB_BASE_URL ?? | |
| 216 | − `http://localhost:${process.env.PORT ?? "4788"}`; | |
| 203 | + process.env.EXECUTOR_WEB_BASE_URL ?? `http://localhost:${process.env.PORT ?? "4788"}`; | |
| 217 | 204 | |
| 218 | 205 | const executor = yield* createExecutor({ |
| 219 | 206 | tenant: Tenant.make(tenantId), |
| @@ -247,9 +234,7 @@ const createLocalExecutorLayer = (options: LocalExecutorOptions = {}) => { · 27 unmodified lines | ||
| 247 | 234 | ); |
| 248 | 235 | }; |
| 249 | 236 | |
| 250 | −export const createExecutorHandle = async ( | |
| 251 | − options: LocalExecutorOptions = {}, | |
| 252 | −) => { | |
| 237 | +export const createExecutorHandle = async (options: LocalExecutorOptions = {}) => { | |
| 253 | 238 | const layer = createLocalExecutorLayer(options); |
| 254 | 239 | const runtime = ManagedRuntime.make(layer); |
| 255 | 240 | const bundle = await runtime.runPromise(LocalExecutorTag.asEffect()); |
| @@ -275,17 +260,14 @@ const loadSharedHandle = () => { · 19 unmodified lines | ||
| 275 | 260 | return sharedHandlePromise; |
| 276 | 261 | }; |
| 277 | 262 | |
| 278 | −export const getExecutor = () => | |
| 279 | − loadSharedHandle().then((handle) => handle.executor); | |
| 263 | +export const getExecutor = () => loadSharedHandle().then((handle) => handle.executor); | |
| 280 | 264 | export const getExecutorBundle = () => loadSharedHandle(); |
| 281 | 265 | |
| 282 | 266 | export const disposeExecutor = async (): Promise<void> => { |
| 283 | 267 | const currentHandlePromise = sharedHandlePromise; |
| 284 | 268 | sharedHandlePromise = null; |
| 285 | 269 | |
| 286 | − const handle = currentHandlePromise | |
| 287 | − ? await handleOrNull(currentHandlePromise) | |
| 288 | − : null; | |
| 270 | + const handle = currentHandlePromise ? await handleOrNull(currentHandlePromise) : null; | |
| 289 | 271 | if (handle) { |
| 290 | 272 | await ignorePromiseFailure("disposeExecutor", () => handle.dispose()); |
| 291 | 273 | } |
| @@ -53,9 +53,7 @@ const closeServerHandlers = async (handlers: ServerHandlers): Promise<void> => { · 52 unmodified lines | ||
| 53 | 53 | ); |
| 54 | 54 | }; |
| 55 | 55 | |
| 56 | −export const createServerHandlers = async ( | |
| 57 | − token: string, | |
| 58 | −): Promise<ServerHandlers> => { | |
| 56 | +export const createServerHandlers = async (token: string): Promise<ServerHandlers> => { | |
| 59 | 57 | // The typed `/api` web-handler comes from `ExecutorApp.make` (./app.ts). The |
| 60 | 58 | // boot bearer token is the authoritative `/api` gate (see `identity.ts`). |
| 61 | 59 | const apiHandler: ServerHandlers["api"] = await makeLocalApiHandler(token); |
| @@ -90,18 +88,15 @@ export const createServerHandlers = async ( · 28 unmodified lines | ||
| 90 | 88 | return { api: apiHandler, mcp }; |
| 91 | 89 | }; |
| 92 | 90 | |
| 93 | −export class ServerHandlersService extends Context.Service< | |
| 94 | − ServerHandlersService, | |
| 95 | − ServerHandlers | |
| 96 | −>()("@executor-js/local/ServerHandlersService") {} | |
| 91 | +export class ServerHandlersService extends Context.Service<ServerHandlersService, ServerHandlers>()( | |
| 92 | + "@executor-js/local/ServerHandlersService", | |
| 93 | +) {} | |
| 97 | 94 | |
| 98 | 95 | // The handlers are built once per process and memoized. The boot token is |
| 99 | 96 | // captured on the first call (serve.ts / the vite dev middleware both pass the |
| 100 | 97 | // SAME token loaded from `auth.json`), so memoization on first-call is correct. |
| 101 | −let serverHandlersRuntime: ManagedRuntime.ManagedRuntime< | |
| 102 | − ServerHandlersService, | |
| 103 | − never | |
| 104 | −> | null = null; | |
| 98 | +let serverHandlersRuntime: ManagedRuntime.ManagedRuntime<ServerHandlersService, never> | null = | |
| 99 | + null; | |
| 105 | 100 | |
| 106 | 101 | const getServerHandlersRuntime = ( |
| 107 | 102 | token: string, |
| @@ -67,9 +67,7 @@ const formatBoundaryError = (error: unknown): unknown => { · 66 unmodified lines | ||
| 67 | 67 | return error; |
| 68 | 68 | }; |
| 69 | 69 | |
| 70 | −const ignoreClose = ( | |
| 71 | − close: (() => Promise<void>) | undefined, | |
| 72 | −): Promise<void> => | |
| 70 | +const ignoreClose = (close: (() => Promise<void>) | undefined): Promise<void> => | |
| 73 | 71 | close |
| 74 | 72 | ? Effect.runPromise( |
| 75 | 73 | Effect.ignore( |
| @@ -81,10 +79,8 @@ const ignoreClose = ( · 5 unmodified lines | ||
| 81 | 79 | ) |
| 82 | 80 | : Promise.resolve(); |
| 83 | 81 | |
| 84 | −const pausedRequestPattern = | |
| 85 | − /^\/api\/mcp-sessions\/([^/?#]+)\/executions\/([^/?#]+)$/; | |
| 86 | −const approvalRequestPattern = | |
| 87 | − /^\/api\/mcp-sessions\/([^/?#]+)\/executions\/([^/?#]+)\/resume$/; | |
| 82 | +const pausedRequestPattern = /^\/api\/mcp-sessions\/([^/?#]+)\/executions\/([^/?#]+)$/; | |
| 83 | +const approvalRequestPattern = /^\/api\/mcp-sessions\/([^/?#]+)\/executions\/([^/?#]+)\/resume$/; | |
| 88 | 84 | |
| 89 | 85 | const json = (value: unknown, status = 200): Response => |
| 90 | 86 | new Response(JSON.stringify(value), { |
| @@ -97,15 +93,10 @@ const readResumeResponse = (request: Request): Promise<ResumeResponse | null> => · 6 unmodified lines | ||
| 97 | 93 | Effect.tryPromise({ |
| 98 | 94 | try: () => request.json(), |
| 99 | 95 | catch: () => null, |
| 100 | − }).pipe( | |
| 101 | − Effect.map((raw) => (raw === null ? null : decodeResumeResponse(raw))), | |
| 102 | − ), | |
| 96 | + }).pipe(Effect.map((raw) => (raw === null ? null : decodeResumeResponse(raw)))), | |
| 103 | 97 | ); |
| 104 | 98 | |
| 105 | −const resumeApprovalResult = ( | |
| 106 | − executionId: string, | |
| 107 | − response: ResumeResponse, | |
| 108 | −) => ({ | |
| 99 | +const resumeApprovalResult = (executionId: string, response: ResumeResponse) => ({ | |
| 109 | 100 | status: "completed", |
| 110 | 101 | ...formatResumeAcknowledgement(executionId, response), |
| 111 | 102 | isError: false, |
| @@ -121,23 +112,18 @@ const resourceFromRequest = (request: Request): McpResource | null => { · 9 unmodified lines | ||
| 121 | 112 | return { kind: "toolkit", slug: decodeURIComponent(match[1]) }; |
| 122 | 113 | }; |
| 123 | 114 | |
| 124 | −const engineFromConfig = ( | |
| 125 | − config: ExecutorMcpServerConfig, | |
| 126 | −): AnyExecutionEngine | null => ("engine" in config ? config.engine : null); | |
| 115 | +const engineFromConfig = (config: ExecutorMcpServerConfig): AnyExecutionEngine | null => | |
| 116 | + "engine" in config ? config.engine : null; | |
| 127 | 117 | |
| 128 | 118 | const normalizeHandlerConfig = ( |
| 129 | 119 | input: ExecutorMcpServerConfig | LocalMcpRequestHandlerConfig, |
| 130 | −): LocalMcpRequestHandlerConfig => | |
| 131 | − "defaultConfig" in input ? input : { defaultConfig: input }; | |
| 120 | +): LocalMcpRequestHandlerConfig => ("defaultConfig" in input ? input : { defaultConfig: input }); | |
| 132 | 121 | |
| 133 | 122 | export const createMcpRequestHandler = ( |
| 134 | 123 | input: ExecutorMcpServerConfig | LocalMcpRequestHandlerConfig, |
| 135 | 124 | ): McpRequestHandler => { |
| 136 | 125 | const handlerConfig = normalizeHandlerConfig(input); |
| 137 | − const transports = new Map< | |
| 138 | − string, | |
| 139 | − WebStandardStreamableHTTPServerTransport | |
| 140 | − >(); | |
| 126 | + const transports = new Map<string, WebStandardStreamableHTTPServerTransport>(); | |
| 141 | 127 | const servers = new Map<string, McpServer>(); |
| 142 | 128 | const resources = new Map<string, McpResource>(); |
| 143 | 129 | const sessionEngines = new Map<string, AnyExecutionEngine>(); |
| @@ -151,29 +137,19 @@ export const createMcpRequestHandler = ( · 7 unmodified lines | ||
| 151 | 137 | ): Promise<ReturnType<typeof formatPausedExecution> | null> => |
| 152 | 138 | (sessionEngines.get(sessionId) ?? defaultEngine) |
| 153 | 139 | ? Effect.runPromise( |
| 154 | − (sessionEngines.get(sessionId) ?? defaultEngine)! | |
| 155 | − .getPausedExecution(executionId) | |
| 156 | − .pipe( | |
| 157 | − Effect.map((paused) => | |
| 158 | − paused ? formatPausedExecution(paused) : null, | |
| 159 | − ), | |
| 160 | − Effect.orElseSucceed(() => null), | |
| 161 | − ), | |
| 140 | + (sessionEngines.get(sessionId) ?? defaultEngine)!.getPausedExecution(executionId).pipe( | |
| 141 | + Effect.map((paused) => (paused ? formatPausedExecution(paused) : null)), | |
| 142 | + Effect.orElseSucceed(() => null), | |
| 143 | + ), | |
| 162 | 144 | ) |
| 163 | 145 | : Promise.resolve(null); |
| 164 | 146 | |
| 165 | − const configForResource = async ( | |
| 166 | − resource: McpResource, | |
| 167 | − ): Promise<LocalMcpServerConfig> => { | |
| 168 | − if (!handlerConfig.createConfigForResource) | |
| 169 | − return { config: handlerConfig.defaultConfig }; | |
| 147 | + const configForResource = async (resource: McpResource): Promise<LocalMcpServerConfig> => { | |
| 148 | + if (!handlerConfig.createConfigForResource) return { config: handlerConfig.defaultConfig }; | |
| 170 | 149 | return handlerConfig.createConfigForResource(resource); |
| 171 | 150 | }; |
| 172 | 151 | |
| 173 | − const dispose = async ( | |
| 174 | − id: string, | |
| 175 | − opts: { transport?: boolean; server?: boolean } = {}, | |
| 176 | − ) => { | |
| 152 | + const dispose = async (id: string, opts: { transport?: boolean; server?: boolean } = {}) => { | |
| 177 | 153 | const t = transports.get(id); |
| 178 | 154 | const s = servers.get(id); |
| 179 | 155 | const close = sessionClosers.get(id); |
| @@ -197,15 +173,8 @@ export const createMcpRequestHandler = ( · 17 unmodified lines | ||
| 197 | 173 | const transport = transports.get(sessionId); |
| 198 | 174 | if (!transport) return jsonError(404, -32001, "Session not found"); |
| 199 | 175 | const sessionResource = resources.get(sessionId); |
| 200 | − if ( | |
| 201 | − !sessionResource || | |
| 202 | − mcpResourceKey(sessionResource) !== mcpResourceKey(resource) | |
| 203 | − ) { | |
| 204 | − return jsonError( | |
| 205 | − 403, | |
| 206 | − -32003, | |
| 207 | − "Session belongs to a different MCP resource", | |
| 208 | − ); | |
| 176 | + if (!sessionResource || mcpResourceKey(sessionResource) !== mcpResourceKey(resource)) { | |
| 177 | + return jsonError(403, -32003, "Session belongs to a different MCP resource"); | |
| 209 | 178 | } |
| 210 | 179 | return transport.handleRequest(request); |
| 211 | 180 | } |
| @@ -221,12 +190,9 @@ export const createMcpRequestHandler = ( · 9 unmodified lines | ||
| 221 | 190 | transports.set(sid, transport); |
| 222 | 191 | if (created) servers.set(sid, created); |
| 223 | 192 | resources.set(sid, resource); |
| 224 | − const engine = resourceConfig | |
| 225 | − ? engineFromConfig(resourceConfig.config) | |
| 226 | − : null; | |
| 193 | + const engine = resourceConfig ? engineFromConfig(resourceConfig.config) : null; | |
| 227 | 194 | if (engine) sessionEngines.set(sid, engine); |
| 228 | − if (resourceConfig?.close) | |
| 229 | − sessionClosers.set(sid, resourceConfig.close); | |
| 195 | + if (resourceConfig?.close) sessionClosers.set(sid, resourceConfig.close); | |
| 230 | 196 | }, |
| 231 | 197 | onsessionclosed: (sid) => void dispose(sid, { server: true }), |
| 232 | 198 | }); |
| @@ -249,11 +215,7 @@ export const createMcpRequestHandler = ( · 16 unmodified lines | ||
| 249 | 215 | ? { |
| 250 | 216 | mode: "browser" as const, |
| 251 | 217 | approvalUrl: (executionId) => |
| 252 | − approvalUrlForRequest( | |
| 253 | − request, | |
| 254 | − executionId, | |
| 255 | − createdSessionId, | |
| 256 | − ), | |
| 218 | + approvalUrlForRequest(request, executionId, createdSessionId), | |
| 257 | 219 | } |
| 258 | 220 | : { mode: elicitationMode }, |
| 259 | 221 | }), |
| @@ -283,13 +245,9 @@ export const createMcpRequestHandler = ( · 23 unmodified lines | ||
| 283 | 245 | handlePausedRequest: async (request) => { |
| 284 | 246 | const match = pausedRequestPattern.exec(new URL(request.url).pathname); |
| 285 | 247 | if (!match) return json({ error: "Not found" }, 404); |
| 286 | − if (request.method !== "GET") | |
| 287 | − return json({ error: "Method not allowed" }, 405); | |
| 248 | + if (request.method !== "GET") return json({ error: "Method not allowed" }, 405); | |
| 288 | 249 | |
| 289 | − const paused = await pausedDetail( | |
| 290 | − decodeURIComponent(match[1]), | |
| 291 | − decodeURIComponent(match[2]), | |
| 292 | − ); | |
| 250 | + const paused = await pausedDetail(decodeURIComponent(match[1]), decodeURIComponent(match[2])); | |
| 293 | 251 | if (!paused) return json({ error: "Paused execution not found" }, 404); |
| 294 | 252 | return json({ text: paused.text, structured: paused.structured }); |
| 295 | 253 | }, |
| @@ -297,8 +255,7 @@ export const createMcpRequestHandler = ( · 1 unmodified line | ||
| 297 | 255 | handleApprovalRequest: async (request) => { |
| 298 | 256 | const match = approvalRequestPattern.exec(new URL(request.url).pathname); |
| 299 | 257 | if (!match) return json({ error: "Not found" }, 404); |
| 300 | − if (request.method !== "POST") | |
| 301 | − return json({ error: "Method not allowed" }, 405); | |
| 258 | + if (request.method !== "POST") return json({ error: "Method not allowed" }, 405); | |
| 302 | 259 | |
| 303 | 260 | const sessionId = decodeURIComponent(match[1]); |
| 304 | 261 | const executionId = decodeURIComponent(match[2]); |
| @@ -316,9 +273,7 @@ export const createMcpRequestHandler = ( · 11 unmodified lines | ||
| 316 | 273 | |
| 317 | 274 | close: async () => { |
| 318 | 275 | const ids = new Set([...transports.keys(), ...servers.keys()]); |
| 319 | − await Promise.all( | |
| 320 | − [...ids].map((id) => dispose(id, { transport: true, server: true })), | |
| 321 | − ); | |
| 276 | + await Promise.all([...ids].map((id) => dispose(id, { transport: true, server: true }))); | |
| 322 | 277 | }, |
| 323 | 278 | }; |
| 324 | 279 | }; |
| @@ -327,9 +282,7 @@ export const createMcpRequestHandler = ( · 2 unmodified lines | ||
| 327 | 282 | // Stdio transport |
| 328 | 283 | // --------------------------------------------------------------------------- |
| 329 | 284 | |
| 330 | −export const runMcpStdioServer = async ( | |
| 331 | − config: ExecutorMcpServerConfig, | |
| 332 | −): Promise<void> => { | |
| 285 | +export const runMcpStdioServer = async (config: ExecutorMcpServerConfig): Promise<void> => { | |
| 333 | 286 | startIntegrationsRefresh(); |
| 334 | 287 | |
| 335 | 288 | const server = await Effect.runPromise(createExecutorMcpServer(config)); |
| @@ -35,10 +35,7 @@ const htmlResponse = (file: Bun.BunFile): Response => · 34 unmodified lines | ||
| 35 | 35 | headers: { "content-type": "text/html", "cache-control": "no-store" }, |
| 36 | 36 | }); |
| 37 | 37 | |
| 38 | −function collectStaticRoutes( | |
| 39 | − dir: string, | |
| 40 | − prefix = "", | |
| 41 | −): Record<string, StaticHandler> { | |
| 38 | +function collectStaticRoutes(dir: string, prefix = ""): Record<string, StaticHandler> { | |
| 42 | 39 | const routes: Record<string, StaticHandler> = {}; |
| 43 | 40 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: filesystem route discovery is best-effort for optional built assets |
| 44 | 41 | try { |
| @@ -67,9 +64,7 @@ function collectStaticRoutes( · 22 unmodified lines | ||
| 67 | 64 | * Convert an embedded web UI map (path → bunfs path) into Bun.serve routes. |
| 68 | 65 | * The embedded map is generated by the CLI build step using `with { type: "file" }`. |
| 69 | 66 | */ |
| 70 | −function embeddedToStaticRoutes( | |
| 71 | − embedded: Record<string, string>, | |
| 72 | −): Record<string, StaticHandler> { | |
| 67 | +function embeddedToStaticRoutes(embedded: Record<string, string>): Record<string, StaticHandler> { | |
| 73 | 68 | const routes: Record<string, StaticHandler> = {}; |
| 74 | 69 | for (const [key, bunfsPath] of Object.entries(embedded)) { |
| 75 | 70 | const file = Bun.file(bunfsPath); |
| @@ -165,9 +160,7 @@ async function startViteChild(): Promise<ViteChild> { · 89 unmodified lines | ||
| 165 | 160 | } |
| 166 | 161 | if (child.exitCode !== null) { |
| 167 | 162 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: child process aborted before becoming ready |
| 168 | − throw new Error( | |
| 169 | − `vite dev exited with code ${child.exitCode} before becoming ready`, | |
| 170 | − ); | |
| 163 | + throw new Error(`vite dev exited with code ${child.exitCode} before becoming ready`); | |
| 171 | 164 | } |
| 172 | 165 | await Bun.sleep(150); |
| 173 | 166 | } |
| @@ -256,8 +249,7 @@ const withCorsHeaders = ( · 82 unmodified lines | ||
| 256 | 249 | if (!origin || !isAllowedOrigin(origin, allowedHosts)) return response; |
| 257 | 250 | const headers = new Headers(response.headers); |
| 258 | 251 | headers.set("access-control-allow-origin", origin); |
| 259 | − for (const [key, value] of Object.entries(corsHeaders)) | |
| 260 | − headers.set(key, value); | |
| 252 | + for (const [key, value] of Object.entries(corsHeaders)) headers.set(key, value); | |
| 261 | 253 | headers.set( |
| 262 | 254 | "access-control-allow-headers", |
| 263 | 255 | req.headers.get("access-control-request-headers") ?? |
| @@ -271,23 +263,17 @@ const withCorsHeaders = ( · 7 unmodified lines | ||
| 271 | 263 | }); |
| 272 | 264 | }; |
| 273 | 265 | |
| 274 | −const corsPreflightResponse = ( | |
| 275 | − req: Request, | |
| 276 | − allowedHosts: ReadonlySet<string>, | |
| 277 | −): Response => | |
| 266 | +const corsPreflightResponse = (req: Request, allowedHosts: ReadonlySet<string>): Response => | |
| 278 | 267 | withCorsHeaders(req, new Response(null, { status: 204 }), allowedHosts); |
| 279 | 268 | |
| 280 | −export async function startServer( | |
| 281 | − opts: StartServerOptions = {}, | |
| 282 | −): Promise<ServerInstance> { | |
| 269 | +export async function startServer(opts: StartServerOptions = {}): Promise<ServerInstance> { | |
| 283 | 270 | const port = opts.port ?? parseInt(process.env.PORT ?? "4788", 10); |
| 284 | 271 | const hostname = opts.hostname ?? "127.0.0.1"; |
| 285 | 272 | // ONE credential, always present: an explicit override or the stable token |
| 286 | 273 | // from auth.json (minted on first run). Auth is unconditionally on — loopback |
| 287 | 274 | // is no longer a free pass, since Executor runs arbitrary code that any local |
| 288 | 275 | // process could otherwise drive. |
| 289 | − const authToken = | |
| 290 | − normalizeCredential(opts.authToken) ?? loadOrMintLocalAuthToken(); | |
| 276 | + const authToken = normalizeCredential(opts.authToken) ?? loadOrMintLocalAuthToken(); | |
| 291 | 277 | const isAuthorized = makeIsAuthorized(authToken); |
| 292 | 278 | // CORS-only origin allowlist (no Host gate — the bearer is the boundary). |
| 293 | 279 | const corsAllowedHosts = new Set<string>([ |
| @@ -317,26 +303,20 @@ export async function startServer( · 23 unmodified lines | ||
| 317 | 303 | |
| 318 | 304 | const devMode = process.env.EXECUTOR_DEV === "1" && !opts.embeddedWebUI; |
| 319 | 305 | if (devMode) { |
| 320 | − console.log( | |
| 321 | − "[executor] EXECUTOR_DEV=1 — spawning vite dev child for live UI", | |
| 322 | − ); | |
| 306 | + console.log("[executor] EXECUTOR_DEV=1 — spawning vite dev child for live UI"); | |
| 323 | 307 | viteChild = await startViteChild(); |
| 324 | 308 | // Diagnostic only — this is the internal vite port the daemon proxies to. |
| 325 | 309 | // It must NOT read as a destination: the URL to open is the `Open:` line the |
| 326 | 310 | // CLI prints (the daemon port, with ?_token). Hitting the vite port directly |
| 327 | 311 | // skips that bootstrap and lands on the auth gate. |
| 328 | − console.log( | |
| 329 | − `[executor] (internal) vite dev child at ${viteChild.url} — don't open this`, | |
| 330 | − ); | |
| 312 | + console.log(`[executor] (internal) vite dev child at ${viteChild.url} — don't open this`); | |
| 331 | 313 | serveIndex = () => |
| 332 | 314 | // Unused when viteChild is non-null; defined so the type checker |
| 333 | 315 | // can keep `serveIndex` non-nullable. |
| 334 | 316 | new Response("vite not ready", { status: 503 }); |
| 335 | 317 | } else if (opts.embeddedWebUI) { |
| 336 | 318 | staticRoutes = embeddedToStaticRoutes(opts.embeddedWebUI); |
| 337 | − const indexFile = Bun.file( | |
| 338 | − opts.embeddedWebUI["index.html"] ?? join(clientDir, "index.html"), | |
| 339 | − ); | |
| 319 | + const indexFile = Bun.file(opts.embeddedWebUI["index.html"] ?? join(clientDir, "index.html")); | |
| 340 | 320 | serveIndex = () => htmlResponse(indexFile); |
| 341 | 321 | } else { |
| 342 | 322 | staticRoutes = collectStaticRoutes(clientDir); |
| @@ -364,9 +344,7 @@ export async function startServer( · 21 unmodified lines | ||
| 364 | 344 | // Unauthenticated liveness probe — carries no data, used by the CLI |
| 365 | 345 | // reachability check (which therefore never forwards a credential). |
| 366 | 346 | if (url.pathname === "/api/health" && req.method === "GET") { |
| 367 | − return withCors( | |
| 368 | − new Response("ok", { headers: { "content-type": "text/plain" } }), | |
| 369 | − ); | |
| 347 | + return withCors(new Response("ok", { headers: { "content-type": "text/plain" } })); | |
| 370 | 348 | } |
| 371 | 349 | |
| 372 | 350 | // OAuth provider callbacks are hit by the user's external browser and |
| @@ -374,8 +352,7 @@ export async function startServer( · 1 unmodified line | ||
| 374 | 352 | // gate — see isUnauthenticatedOAuthCallbackPath. Everything else under |
| 375 | 353 | // /api and /mcp requires the bearer. |
| 376 | 354 | const skipAuth = isUnauthenticatedOAuthCallbackPath(url.pathname); |
| 377 | − const isMcpPath = | |
| 378 | − url.pathname === "/mcp" || url.pathname.startsWith("/mcp/"); | |
| 355 | + const isMcpPath = url.pathname === "/mcp" || url.pathname.startsWith("/mcp/"); | |
| 379 | 356 | const isGatedSurface = url.pathname.startsWith("/api") || isMcpPath; |
| 380 | 357 | |
| 381 | 358 | if (isGatedSurface && !skipAuth && !isAuthorized(req)) { |
| @@ -457,7 +434,5 @@ export async function startServer( · 75 unmodified lines | ||
| 457 | 434 | |
| 458 | 435 | if (import.meta.main) { |
| 459 | 436 | const server = await startServer(); |
| 460 | − console.log( | |
| 461 | − `Executor listening on http://localhost:${server.port}/?_token=${server.authToken}`, | |
| 462 | − ); | |
| 437 | + console.log(`Executor listening on http://localhost:${server.port}/?_token=${server.authToken}`); | |
| 463 | 438 | } |
| @@ -6,10 +6,7 @@ import { defineConfig, type Plugin } from "vite"; · 5 unmodified lines | ||
| 6 | 6 | import appPlugin from "@executor-js/app/vite"; |
| 7 | 7 | import { loadOrMintLocalAuthToken } from "./src/auth"; |
| 8 | 8 | import { consumeOAuthResult } from "./src/oauth-result-store"; |
| 9 | −import { | |
| 10 | − isUnauthenticatedOAuthCallbackPath, | |
| 11 | − makeIsAuthorized, | |
| 12 | −} from "./src/serve-shared"; | |
| 9 | +import { isUnauthenticatedOAuthCallbackPath, makeIsAuthorized } from "./src/serve-shared"; | |
| 13 | 10 | |
| 14 | 11 | // oxlint-disable-next-line executor/no-json-parse -- boundary: Vite config reads package metadata from package.json |
| 15 | 12 | const rootPackage = JSON.parse( |
| @@ -26,9 +23,7 @@ const cliPackage = JSON.parse( · 10 unmodified lines | ||
| 26 | 23 | ) as { version?: string }; |
| 27 | 24 | |
| 28 | 25 | const repositoryUrl = |
| 29 | − typeof rootPackage.repository === "string" | |
| 30 | − ? rootPackage.repository | |
| 31 | − : rootPackage.repository?.url; | |
| 26 | + typeof rootPackage.repository === "string" ? rootPackage.repository : rootPackage.repository?.url; | |
| 32 | 27 | |
| 33 | 28 | const EXECUTOR_VERSION = cliPackage.version ?? rootPackage.version; |
| 34 | 29 | const EXECUTOR_GITHUB_URL = ( |
| @@ -67,9 +62,7 @@ function executorApiPlugin(): Plugin { · 32 unmodified lines | ||
| 67 | 62 | server.httpServer?.once("listening", () => { |
| 68 | 63 | const address = server.httpServer?.address(); |
| 69 | 64 | const port = |
| 70 | − typeof address === "object" && address | |
| 71 | − ? address.port | |
| 72 | − : server.config.server.port; | |
| 65 | + typeof address === "object" && address ? address.port : server.config.server.port; | |
| 73 | 66 | server.config.logger.info( |
| 74 | 67 | `\n Open with auth: http://127.0.0.1:${port}/?_token=${devToken}\n`, |
| 75 | 68 | ); |
| @@ -77,10 +70,7 @@ function executorApiPlugin(): Plugin { · 1 unmodified line | ||
| 77 | 70 | } |
| 78 | 71 | |
| 79 | 72 | server.watcher.on("change", (path) => { |
| 80 | − if ( | |
| 81 | − path.includes("/apps/local/src/") || | |
| 82 | − path.endsWith("/executor.config.ts") | |
| 83 | − ) { | |
| 73 | + if (path.includes("/apps/local/src/") || path.endsWith("/executor.config.ts")) { | |
| 84 | 74 | handlers = null; |
| 85 | 75 | } |
| 86 | 76 | }); |
| @@ -100,8 +90,7 @@ function executorApiPlugin(): Plugin { · 13 unmodified lines | ||
| 100 | 90 | // bootstrap, so the UI is unaffected; external MCP clients use the |
| 101 | 91 | // daemon port. |
| 102 | 92 | const authExempt = |
| 103 | − pathOnly === "/api/health" || | |
| 104 | − isUnauthenticatedOAuthCallbackPath(pathOnly); | |
| 93 | + pathOnly === "/api/health" || isUnauthenticatedOAuthCallbackPath(pathOnly); | |
| 105 | 94 | if (!authExempt) { |
| 106 | 95 | const presented = req.headers.authorization; |
| 107 | 96 | const authValue = Array.isArray(presented) ? presented[0] : presented; |
| @@ -109,9 +98,7 @@ function executorApiPlugin(): Plugin { · 1 unmodified line | ||
| 109 | 98 | "http://localhost/", |
| 110 | 99 | authValue ? { headers: { authorization: authValue } } : undefined, |
| 111 | 100 | ); |
| 112 | − if ( | |
| 113 | − !makeIsAuthorized(devToken ?? loadOrMintLocalAuthToken())(probe) | |
| 114 | − ) { | |
| 101 | + if (!makeIsAuthorized(devToken ?? loadOrMintLocalAuthToken())(probe)) { | |
| 115 | 102 | res.statusCode = 401; |
| 116 | 103 | res.setHeader("www-authenticate", 'Bearer realm="executor"'); |
| 117 | 104 | res.end("Unauthorized"); |
| @@ -123,16 +110,13 @@ function executorApiPlugin(): Plugin { · 5 unmodified lines | ||
| 123 | 110 | try { |
| 124 | 111 | if (!handlers) { |
| 125 | 112 | const { getServerHandlers } = await import("./src/main"); |
| 126 | − handlers = await getServerHandlers( | |
| 127 | − devToken ?? loadOrMintLocalAuthToken(), | |
| 128 | − ); | |
| 113 | + handlers = await getServerHandlers(devToken ?? loadOrMintLocalAuthToken()); | |
| 129 | 114 | } |
| 130 | 115 | |
| 131 | 116 | const origin = `http://${req.headers.host ?? "localhost"}`; |
| 132 | 117 | const headers = new Headers(); |
| 133 | 118 | for (const [key, value] of Object.entries(req.headers)) { |
| 134 | − if (value) | |
| 135 | − headers.set(key, Array.isArray(value) ? value.join(", ") : value); | |
| 119 | + if (value) headers.set(key, Array.isArray(value) ? value.join(", ") : value); | |
| 136 | 120 | } |
| 137 | 121 | |
| 138 | 122 | const hasBody = req.method !== "GET" && req.method !== "HEAD"; |
| @@ -158,21 +142,14 @@ function executorApiPlugin(): Plugin { · 19 unmodified lines | ||
| 158 | 142 | : handlers.mcp.handleApprovalRequest; |
| 159 | 143 | response = await handler(webRequest(rawUrl)); |
| 160 | 144 | } else { |
| 161 | − const awaitMatch = /^\/api\/oauth\/await\/([^/?#]+)$/.exec( | |
| 162 | − pathOnly, | |
| 163 | − ); | |
| 145 | + const awaitMatch = /^\/api\/oauth\/await\/([^/?#]+)$/.exec(pathOnly); | |
| 164 | 146 | if (awaitMatch && req.method === "GET") { |
| 165 | − response = new Response( | |
| 166 | − JSON.stringify(consumeOAuthResult(awaitMatch[1]!)), | |
| 167 | − { | |
| 168 | − headers: { "content-type": "application/json" }, | |
| 169 | − }, | |
| 170 | − ); | |
| 147 | + response = new Response(JSON.stringify(consumeOAuthResult(awaitMatch[1]!)), { | |
| 148 | + headers: { "content-type": "application/json" }, | |
| 149 | + }); | |
| 171 | 150 | } else { |
| 172 | 151 | // Strip /api prefix for Effect handlers. |
| 173 | − response = await handlers.api.handler( | |
| 174 | − webRequest(rawUrl.slice("/api".length) || "/"), | |
| 175 | − ); | |
| 152 | + response = await handlers.api.handler(webRequest(rawUrl.slice("/api".length) || "/")); | |
| 176 | 153 | } |
| 177 | 154 | } |
| 178 | 155 | |
| @@ -211,9 +188,7 @@ export default defineConfig({ · 32 unmodified lines | ||
| 211 | 188 | "import.meta.env.VITE_APP_VERSION": JSON.stringify(EXECUTOR_VERSION), |
| 212 | 189 | "import.meta.env.VITE_GITHUB_URL": JSON.stringify(EXECUTOR_GITHUB_URL), |
| 213 | 190 | "import.meta.env.VITE_EXECUTOR_DEV_CLI_CWD": JSON.stringify(REPO_ROOT), |
| 214 | − "process.env.NODE_ENV": JSON.stringify( | |
| 215 | − process.env.NODE_ENV ?? "development", | |
| 216 | − ), | |
| 191 | + "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "development"), | |
| 217 | 192 | }, |
| 218 | 193 | resolve: { |
| 219 | 194 | tsconfigPaths: true, |
| @@ -67,8 +67,8 @@ const pingSpec = (baseUrl: string): string => · 66 unmodified lines | ||
| 67 | 67 | }); |
| 68 | 68 | |
| 69 | 69 | const closeServer = (server: Server): Promise<void> => |
| 70 | − new Promise((resolve, reject) => { | |
| 71 | − server.close((error) => (error ? reject(error) : resolve())); | |
| 70 | + new Promise((resolve) => { | |
| 71 | + server.close(() => resolve()); | |
| 72 | 72 | }); |
| 73 | 73 | |
| 74 | 74 | const servePingApi = Effect.acquireRelease( |
| @@ -56,8 +56,8 @@ const pingSpec = (baseUrl: string): string => · 55 unmodified lines | ||
| 56 | 56 | }); |
| 57 | 57 | |
| 58 | 58 | const closeServer = (server: Server): Promise<void> => |
| 59 | − new Promise((resolve, reject) => { | |
| 60 | − server.close((error) => (error ? reject(error) : resolve())); | |
| 59 | + new Promise((resolve) => { | |
| 60 | + server.close(() => resolve()); | |
| 61 | 61 | }); |
| 62 | 62 | |
| 63 | 63 | const servePingApi = Effect.acquireRelease( |
| @@ -156,13 +156,19 @@ scenario( · 155 unmodified lines | ||
| 156 | 156 | }); |
| 157 | 157 | |
| 158 | 158 | await step("Remove the connection from the toolkit tools list", async () => { |
| 159 | − await page.getByRole("button", { name: /^Remove connection / }).first().click(); | |
| 159 | + await page | |
| 160 | + .getByRole("button", { name: /^Remove connection / }) | |
| 161 | + .first() | |
| 162 | + .click(); | |
| 160 | 163 | await page.getByText("No connections added").waitFor(); |
| 161 | 164 | await page.getByRole("button", { name: "Add connection to toolkit" }).click(); |
| 162 | 165 | const dialog = page.getByRole("dialog", { name: "Add connection" }); |
| 163 | 166 | await dialog.waitFor(); |
| 164 | 167 | await dialog.getByLabel("Search connections and tools").fill("policies.list"); |
| 165 | − await dialog.getByRole("button", { name: /^Add connection / }).first().click(); | |
| 168 | + await dialog | |
| 169 | + .getByRole("button", { name: /^Add connection / }) | |
| 170 | + .first() | |
| 171 | + .click(); | |
| 166 | 172 | await dialog.waitFor({ state: "hidden" }); |
| 167 | 173 | const toolkitTools = page.getByRole("region", { name: "Toolkit tools" }); |
| 168 | 174 | await toolkitTools.getByLabel("Filter tools").fill("policies.list"); |
| @@ -49,17 +49,7 @@ const insertIntegrationRawConfig = ( · 48 unmodified lines | ||
| 49 | 49 | sql: `INSERT INTO integration |
| 50 | 50 | (row_id, tenant, slug, plugin_id, name, description, config, can_remove, can_refresh, created_at, updated_at) |
| 51 | 51 | VALUES (?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?)`, |
| 52 | − args: [ | |
| 53 | − row.rowId, | |
| 54 | − row.tenant, | |
| 55 | − row.slug, | |
| 56 | − row.pluginId, | |
| 57 | − row.slug, | |
| 58 | − row.slug, | |
| 59 | − row.config, | |
| 60 | − now, | |
| 61 | − now, | |
| 62 | − ], | |
| 52 | + args: [row.rowId, row.tenant, row.slug, row.pluginId, row.slug, row.slug, row.config, now, now], | |
| 63 | 53 | }); |
| 64 | 54 | |
| 65 | 55 | const insertBlob = ( |
| @@ -121,14 +111,7 @@ const insertTool = ( · 55 unmodified lines | ||
| 121 | 111 | sql: `INSERT INTO tool |
| 122 | 112 | (tenant, owner, subject, integration, connection, plugin_id, name, description, input_schema, output_schema, annotations, created_at, updated_at, row_id) |
| 123 | 113 | VALUES (?, 'org', '', ?, 'default', ?, 'items.list', 'List items', NULL, NULL, NULL, ?, ?, ?)`, |
| 124 | − args: [ | |
| 125 | − row.tenant, | |
| 126 | − row.integration, | |
| 127 | − row.pluginId, | |
| 128 | − now, | |
| 129 | − now, | |
| 130 | − `tool-${row.integration}`, | |
| 131 | − ], | |
| 114 | + args: [row.tenant, row.integration, row.pluginId, now, now, `tool-${row.integration}`], | |
| 132 | 115 | }); |
| 133 | 116 | |
| 134 | 117 | const insertDefinition = ( |
| @@ -143,21 +126,13 @@ const insertDefinition = ( · 8 unmodified lines | ||
| 143 | 126 | sql: `INSERT INTO definition |
| 144 | 127 | (tenant, owner, subject, integration, connection, plugin_id, name, schema, created_at, row_id) |
| 145 | 128 | VALUES (?, 'org', '', ?, 'default', ?, 'Item', '{}', ?, ?)`, |
| 146 | − args: [ | |
| 147 | − row.tenant, | |
| 148 | − row.integration, | |
| 149 | − row.pluginId, | |
| 150 | − now, | |
| 151 | − `definition-${row.integration}`, | |
| 152 | − ], | |
| 129 | + args: [row.tenant, row.integration, row.pluginId, now, `definition-${row.integration}`], | |
| 153 | 130 | }); |
| 154 | 131 | |
| 155 | 132 | describe("runSqliteGoogleOpenApiOwnershipMigration", () => { |
| 156 | 133 | it.effect("skips malformed legacy integration config rows", () => |
| 157 | 134 | Effect.gen(function* () { |
| 158 | − const db = yield* Effect.promise(() => | |
| 159 | − createSqliteTestFumaDb({ tables: collectTables() }), | |
| 160 | − ); | |
| 135 | + const db = yield* Effect.promise(() => createSqliteTestFumaDb({ tables: collectTables() })); | |
| 161 | 136 | |
| 162 | 137 | yield* Effect.promise(() => |
| 163 | 138 | insertIntegrationRawConfig(db.client, { |
| @@ -169,16 +144,12 @@ describe("runSqliteGoogleOpenApiOwnershipMigration", () => { · 5 unmodified lines | ||
| 169 | 144 | }), |
| 170 | 145 | ); |
| 171 | 146 | |
| 172 | − expect(yield* runSqliteGoogleOpenApiOwnershipMigration(db.client)).toBe( | |
| 173 | − 0, | |
| 174 | − ); | |
| 147 | + expect(yield* runSqliteGoogleOpenApiOwnershipMigration(db.client)).toBe(0); | |
| 175 | 148 | |
| 176 | 149 | const integrations = yield* Effect.promise(() => |
| 177 | 150 | db.client.execute("SELECT slug, plugin_id, config FROM integration"), |
| 178 | 151 | ); |
| 179 | − expect(integrations.rows).toEqual([ | |
| 180 | − { slug: "broken", plugin_id: "openapi", config: "" }, | |
| 181 | − ]); | |
| 152 | + expect(integrations.rows).toEqual([{ slug: "broken", plugin_id: "openapi", config: "" }]); | |
| 182 | 153 | |
| 183 | 154 | yield* Effect.promise(() => db.close()); |
| 184 | 155 | }), |
| @@ -186,9 +157,7 @@ describe("runSqliteGoogleOpenApiOwnershipMigration", () => { · 1 unmodified line | ||
| 186 | 157 | |
| 187 | 158 | it.effect("moves OpenAPI-owned Google bundle rows to the Google plugin", () => |
| 188 | 159 | Effect.gen(function* () { |
| 189 | − const db = yield* Effect.promise(() => | |
| 190 | − createSqliteTestFumaDb({ tables: collectTables() }), | |
| 191 | − ); | |
| 160 | + const db = yield* Effect.promise(() => createSqliteTestFumaDb({ tables: collectTables() })); | |
| 192 | 161 | const client = db.client; |
| 193 | 162 | |
| 194 | 163 | yield* Effect.promise(() => |
| @@ -199,9 +168,7 @@ describe("runSqliteGoogleOpenApiOwnershipMigration", () => { · 4 unmodified lines | ||
| 199 | 168 | pluginId: "openapi", |
| 200 | 169 | config: { |
| 201 | 170 | specHash: "googlehash", |
| 202 | − googleDiscoveryUrls: [ | |
| 203 | − "https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest", | |
| 204 | − ], | |
| 171 | + googleDiscoveryUrls: ["https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest"], | |
| 205 | 172 | }, |
| 206 | 173 | }), |
| 207 | 174 | ); |
| @@ -277,14 +244,9 @@ describe("runSqliteGoogleOpenApiOwnershipMigration", () => { · 69 unmodified lines | ||
| 277 | 244 | expect(googleBlob.rows).toEqual([{ value: "google spec" }]); |
| 278 | 245 | |
| 279 | 246 | const openApiBlobs = yield* Effect.promise(() => |
| 280 | − client.execute( | |
| 281 | − "SELECT key FROM blob WHERE namespace = 'o:org_1/openapi' ORDER BY key", | |
| 282 | − ), | |
| 247 | + client.execute("SELECT key FROM blob WHERE namespace = 'o:org_1/openapi' ORDER BY key"), | |
| 283 | 248 | ); |
| 284 | − expect(openApiBlobs.rows).toEqual([ | |
| 285 | − { key: "spec/googlehash" }, | |
| 286 | − { key: "spec/stripehash" }, | |
| 287 | − ]); | |
| 249 | + expect(openApiBlobs.rows).toEqual([{ key: "spec/googlehash" }, { key: "spec/stripehash" }]); | |
| 288 | 250 | |
| 289 | 251 | const storage = yield* Effect.promise(() => |
| 290 | 252 | client.execute( |
| @@ -297,9 +259,7 @@ describe("runSqliteGoogleOpenApiOwnershipMigration", () => { · 6 unmodified lines | ||
| 297 | 259 | ]); |
| 298 | 260 | |
| 299 | 261 | const tools = yield* Effect.promise(() => |
| 300 | − client.execute( | |
| 301 | − "SELECT integration, plugin_id FROM tool ORDER BY integration", | |
| 302 | − ), | |
| 262 | + client.execute("SELECT integration, plugin_id FROM tool ORDER BY integration"), | |
| 303 | 263 | ); |
| 304 | 264 | expect(tools.rows).toEqual([ |
| 305 | 265 | { integration: "google", plugin_id: "google" }, |
| @@ -307,9 +267,7 @@ describe("runSqliteGoogleOpenApiOwnershipMigration", () => { · 1 unmodified line | ||
| 307 | 267 | ]); |
| 308 | 268 | |
| 309 | 269 | const definitions = yield* Effect.promise(() => |
| 310 | − client.execute( | |
| 311 | − "SELECT integration, plugin_id FROM definition ORDER BY integration", | |
| 312 | − ), | |
| 270 | + client.execute("SELECT integration, plugin_id FROM definition ORDER BY integration"), | |
| 313 | 271 | ); |
| 314 | 272 | expect(definitions.rows).toEqual([ |
| 315 | 273 | { integration: "google", plugin_id: "google" }, |
| @@ -1,8 +1,5 @@ | ||
| 1 | 1 | import { Effect } from "effect"; |
| 2 | −import { | |
| 3 | − DataMigrationError, | |
| 4 | − type SqliteDataMigrationClient, | |
| 5 | −} from "@executor-js/sdk/core"; | |
| 2 | +import { DataMigrationError, type SqliteDataMigrationClient } from "@executor-js/sdk/core"; | |
| 6 | 3 | |
| 7 | 4 | const MIGRATION_NAME = "2026-06-20-google-openapi-ownership"; |
| 8 | 5 | const googleOpenApiCandidate = (alias?: string): string => { |
| @@ -16,8 +13,7 @@ const execute = ( · 7 unmodified lines | ||
| 16 | 13 | ) => |
| 17 | 14 | Effect.tryPromise({ |
| 18 | 15 | try: () => client.execute(stmt), |
| 19 | − catch: (cause) => | |
| 20 | − new DataMigrationError({ migration: MIGRATION_NAME, cause }), | |
| 16 | + catch: (cause) => new DataMigrationError({ migration: MIGRATION_NAME, cause }), | |
| 21 | 17 | }); |
| 22 | 18 | |
| 23 | 19 | export const runSqliteGoogleOpenApiOwnershipMigration = ( |
| @@ -0,0 +1 @@ | ||
| 1 | +# @executor-js/plugin-toolkits | |
| @@ -530,11 +530,12 @@ function CreateToolkitDialog(props: { · 529 unmodified lines | ||
| 530 | 530 | function AddToolkitCard(props: { owner: Owner; onClick: () => void }) { |
| 531 | 531 | const scopeLabel = addToolkitScopeLabel(props.owner); |
| 532 | 532 | return ( |
| 533 | − <button | |
| 533 | + <Button | |
| 534 | 534 | type="button" |
| 535 | + variant="ghost" | |
| 535 | 536 | onClick={props.onClick} |
| 536 | 537 | aria-label={`Add ${scopeLabel} toolkit`} |
| 537 | − className="group flex min-h-36 min-w-0 self-start items-center justify-center rounded-md border border-dashed border-border/75 bg-card/40 text-muted-foreground transition-[border-color,background-color,box-shadow,color] hover:border-foreground/25 hover:bg-muted/20 hover:text-foreground hover:shadow-sm focus-visible:ring-[3px] focus-visible:ring-ring/30 focus-visible:outline-none" | |
| 538 | + className="group flex h-auto min-h-36 min-w-0 self-start items-center justify-center rounded-md border border-dashed border-border/75 bg-card/40 p-0 text-muted-foreground transition-[border-color,background-color,box-shadow,color] hover:border-foreground/25 hover:bg-muted/20 hover:text-foreground hover:shadow-sm focus-visible:ring-[3px] focus-visible:ring-ring/30 focus-visible:outline-none" | |
| 538 | 539 | style={toolkitCardStyle} |
| 539 | 540 | > |
| 540 | 541 | <span |
| @@ -546,7 +547,7 @@ function AddToolkitCard(props: { owner: Owner; onClick: () => void }) { · 5 unmodified lines | ||
| 546 | 547 | > |
| 547 | 548 | <PlusIcon className="size-6" /> |
| 548 | 549 | </span> |
| 549 | − </button> | |
| 550 | + </Button> | |
| 550 | 551 | ); |
| 551 | 552 | } |
| 552 | 553 | |
| @@ -1047,11 +1048,7 @@ function ToolkitWorkspace(props: { · 494 unmodified lines | ||
| 1047 | 1048 | ); |
| 1048 | 1049 | const exposedToolIds = useMemo( |
| 1049 | 1050 | () => |
| 1050 | − new Set( | |
| 1051 | − toolkitTools | |
| 1052 | − .filter((tool) => tool.policy.action !== "block") | |
| 1053 | − .map((tool) => tool.id), | |
| 1054 | − ), | |
| 1051 | + new Set(toolkitTools.filter((tool) => tool.policy.action !== "block").map((tool) => tool.id)), | |
| 1055 | 1052 | [toolkitTools], |
| 1056 | 1053 | ); |
| 1057 | 1054 | const selectedTool = selectedToolId |
| @@ -1231,9 +1228,7 @@ export function ToolkitsPage(props: PluginPageProps) { · 173 unmodified lines | ||
| 1231 | 1228 | |
| 1232 | 1229 | const toolkitRows = AsyncResult.isSuccess(toolkits) ? toolkits.value.toolkits : []; |
| 1233 | 1230 | const selectedToolkit = |
| 1234 | − selectedToolkitSlug === null | |
| 1235 | − ? null | |
| 1236 | − : toolkitByRouteSlug(toolkitRows, selectedToolkitSlug); | |
| 1231 | + selectedToolkitSlug === null ? null : toolkitByRouteSlug(toolkitRows, selectedToolkitSlug); | |
| 1237 | 1232 | |
| 1238 | 1233 | const toolRows = AsyncResult.isSuccess(tools) ? (tools.value as readonly ToolRow[]) : []; |
| 1239 | 1234 | const integrationRows = AsyncResult.isSuccess(integrations) |
| @@ -249,9 +249,13 @@ const makeToolkitsExtension = (ctx: PluginCtx<ToolkitStorage>) => { · 248 unmodified lines | ||
| 249 | 249 | const getEntry = (toolkitId: string) => storage.toolkits.get({ key: toolkitId }); |
| 250 | 250 | |
| 251 | 251 | const getBySlugEntry = (slug: string) => |
| 252 | − storage.toolkits.query({ where: { slug } }).pipe( | |
| 253 | − Effect.map((entries) => entries.find((entry) => entry.owner === "org") ?? entries[0] ?? null), | |
| 254 | − ); | |
| 252 | + storage.toolkits | |
| 253 | + .query({ where: { slug } }) | |
| 254 | + .pipe( | |
| 255 | + Effect.map( | |
| 256 | + (entries) => entries.find((entry) => entry.owner === "org") ?? entries[0] ?? null, | |
| 257 | + ), | |
| 258 | + ); | |
| 255 | 259 | |
| 256 | 260 | const requireToolkit = (toolkitId: string) => |
| 257 | 261 | getEntry(toolkitId).pipe( |