Unified · Split
| @@ -37,7 +37,8 @@ · 36 unmodified lines | ||
| 37 | 37 | "graphql": "^16.12.0", |
| 38 | 38 | "jsonc-parser": "3.3.1", |
| 39 | 39 | "postgres": "^3.4.7", |
| 40 | − "prettier": "^3.8.1" | |
| 40 | + "prettier": "^3.8.1", | |
| 41 | + "typescript": "^5.9.3" | |
| 41 | 42 | }, |
| 42 | 43 | "devDependencies": { |
| 43 | 44 | "@effect/vitest": "catalog:", |
| @@ -4,9 +4,7 @@ import * as Exit from "effect/Exit"; · 3 unmodified lines | ||
| 4 | 4 | import * as Layer from "effect/Layer"; |
| 5 | 5 | import * as Scope from "effect/Scope"; |
| 6 | 6 | |
| 7 | −import { | |
| 8 | − type ControlPlaneApiRuntimeContext, | |
| 9 | −} from "#api"; | |
| 7 | +import { type ControlPlaneApiRuntimeContext } from "#api"; | |
| 10 | 8 | import { |
| 11 | 9 | SqlControlPlanePersistenceLive, |
| 12 | 10 | SqlControlPlanePersistenceService, |
| @@ -18,9 +16,7 @@ import { · 5 unmodified lines | ||
| 18 | 16 | } from "#persistence"; |
| 19 | 17 | import type { LocalInstallation } from "#schema"; |
| 20 | 18 | |
| 21 | −import { | |
| 22 | − type ResolveExecutionEnvironment, | |
| 23 | −} from "./execution-state"; | |
| 19 | +import { type ResolveExecutionEnvironment } from "./execution-state"; | |
| 24 | 20 | import { |
| 25 | 21 | createLiveExecutionManager, |
| 26 | 22 | LiveExecutionManagerLive, |
| @@ -33,10 +29,7 @@ import { · 6 unmodified lines | ||
| 33 | 29 | } from "./local-config"; |
| 34 | 30 | import { RuntimeLocalWorkspaceService } from "./local-runtime-context"; |
| 35 | 31 | import { synchronizeLocalWorkspaceState } from "./local-workspace-sync"; |
| 36 | −import { | |
| 37 | − ControlPlaneStore, | |
| 38 | − ControlPlaneStoreLive, | |
| 39 | −} from "./store"; | |
| 32 | +import { ControlPlaneStore, ControlPlaneStoreLive } from "./store"; | |
| 40 | 33 | import { |
| 41 | 34 | createRuntimeSourceAuthService, |
| 42 | 35 | RuntimeSourceAuthServiceLive, |
| @@ -56,6 +49,7 @@ export * from "./live-execution"; · 13 unmodified lines | ||
| 56 | 49 | export * from "./local-config"; |
| 57 | 50 | export * from "./local-installation"; |
| 58 | 51 | export * from "./local-source-artifacts"; |
| 52 | +export * from "./local-tools"; | |
| 59 | 53 | export * from "./schema-type-signature"; |
| 60 | 54 | export * from "./source-auth-service"; |
| 61 | 55 | export * from "./secret-material-providers"; |
| @@ -106,7 +100,7 @@ const runtimeContextTags = [ · 44 unmodified lines | ||
| 106 | 100 | |
| 107 | 101 | const createRuntimeLayerFromContext = ( |
| 108 | 102 | context: Context.Context< |
| 109 | − ControlPlaneStore | |
| 103 | + | ControlPlaneStore | |
| 110 | 104 | | LiveExecutionManagerService |
| 111 | 105 | | RuntimeLocalWorkspaceService |
| 112 | 106 | | RuntimeSourceAuthServiceTag |
| @@ -116,7 +110,7 @@ const createRuntimeLayerFromContext = ( · 3 unmodified lines | ||
| 116 | 110 | const runtimeContext = context.pipe( |
| 117 | 111 | Context.pick(...runtimeContextTags), |
| 118 | 112 | ) as Context.Context< |
| 119 | − ControlPlaneStore | |
| 113 | + | ControlPlaneStore | |
| 120 | 114 | | LiveExecutionManagerService |
| 121 | 115 | | RuntimeLocalWorkspaceService |
| 122 | 116 | | RuntimeSourceAuthServiceTag |
| @@ -132,15 +126,11 @@ export const createRuntimeControlPlaneLayer = ( · 9 unmodified lines | ||
| 132 | 126 | const liveExecutionManagerLayer = LiveExecutionManagerLive; |
| 133 | 127 | const sourceAuthLayer = RuntimeSourceAuthServiceLive({ |
| 134 | 128 | getLocalServerBaseUrl: options.getLocalServerBaseUrl, |
| 135 | − }).pipe( | |
| 136 | − Layer.provide(liveExecutionManagerLayer), | |
| 137 | − ); | |
| 129 | + }).pipe(Layer.provide(liveExecutionManagerLayer)); | |
| 138 | 130 | const executionResolverLayer = RuntimeExecutionResolverLive({ |
| 139 | 131 | executionResolver: options.executionResolver, |
| 140 | 132 | resolveSecretMaterial: options.resolveSecretMaterial, |
| 141 | − }).pipe( | |
| 142 | − Layer.provide(sourceAuthLayer), | |
| 143 | − ); | |
| 133 | + }).pipe(Layer.provide(sourceAuthLayer)); | |
| 144 | 134 | |
| 145 | 135 | return Layer.mergeAll( |
| 146 | 136 | ControlPlaneStoreLive, |
| @@ -157,8 +147,8 @@ export type ControlPlaneRuntime = { · 10 unmodified lines | ||
| 157 | 147 | close: () => Promise<void>; |
| 158 | 148 | }; |
| 159 | 149 | |
| 160 | −export type CreateControlPlaneRuntimeOptions = CreateSqlRuntimeOptions | |
| 161 | − & RuntimeControlPlaneOptions; | |
| 150 | +export type CreateControlPlaneRuntimeOptions = CreateSqlRuntimeOptions & | |
| 151 | + RuntimeControlPlaneOptions; | |
| 162 | 152 | |
| 163 | 153 | export const createControlPlaneRuntime = ( |
| 164 | 154 | options: CreateControlPlaneRuntimeOptions, |
| @@ -168,14 +158,19 @@ export const createControlPlaneRuntime = ( · 3 unmodified lines | ||
| 168 | 158 | const persistenceAndRowsLayer = SqlControlPlaneRowsLive.pipe( |
| 169 | 159 | Layer.provideMerge(SqlControlPlanePersistenceLive(options)), |
| 170 | 160 | ); |
| 171 | − const baseContext = yield* Layer.buildWithScope(persistenceAndRowsLayer, scope).pipe( | |
| 161 | + const baseContext = yield* Layer.buildWithScope( | |
| 162 | + persistenceAndRowsLayer, | |
| 163 | + scope, | |
| 164 | + ).pipe( | |
| 172 | 165 | Effect.catchAll((error) => |
| 173 | − closeScope(scope).pipe( | |
| 174 | − Effect.zipRight(Effect.fail(error)), | |
| 175 | − )), | |
| 166 | + closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), | |
| 167 | + ), | |
| 176 | 168 | ); |
| 177 | 169 | |
| 178 | − const persistence = Context.get(baseContext, SqlControlPlanePersistenceService); | |
| 170 | + const persistence = Context.get( | |
| 171 | + baseContext, | |
| 172 | + SqlControlPlanePersistenceService, | |
| 173 | + ); | |
| 179 | 174 | const rows = Context.get(baseContext, SqlControlPlaneRowsService); |
| 180 | 175 | |
| 181 | 176 | const localWorkspaceContext = yield* resolveLocalWorkspaceContext({ |
| @@ -183,9 +178,8 @@ export const createControlPlaneRuntime = ( · 1 unmodified line | ||
| 183 | 178 | }).pipe( |
| 184 | 179 | Effect.mapError(toLocalRuntimeBootstrapError), |
| 185 | 180 | Effect.catchAll((error) => |
| 186 | − closeScope(scope).pipe( | |
| 187 | − Effect.zipRight(Effect.fail(error)), | |
| 188 | − )), | |
| 181 | + closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), | |
| 182 | + ), | |
| 189 | 183 | ); |
| 190 | 184 | |
| 191 | 185 | const localInstallation = yield* getOrProvisionLocalInstallation({ |
| @@ -193,17 +187,17 @@ export const createControlPlaneRuntime = ( · 1 unmodified line | ||
| 193 | 187 | }).pipe( |
| 194 | 188 | Effect.mapError(toLocalRuntimeBootstrapError), |
| 195 | 189 | Effect.catchAll((error) => |
| 196 | − closeScope(scope).pipe( | |
| 197 | − Effect.zipRight(Effect.fail(error)), | |
| 198 | − )), | |
| 190 | + closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), | |
| 191 | + ), | |
| 199 | 192 | ); |
| 200 | 193 | |
| 201 | − const loadedLocalConfig = yield* loadLocalExecutorConfig(localWorkspaceContext).pipe( | |
| 194 | + const loadedLocalConfig = yield* loadLocalExecutorConfig( | |
| 195 | + localWorkspaceContext, | |
| 196 | + ).pipe( | |
| 202 | 197 | Effect.mapError(toLocalRuntimeBootstrapError), |
| 203 | 198 | Effect.catchAll((error) => |
| 204 | − closeScope(scope).pipe( | |
| 205 | − Effect.zipRight(Effect.fail(error)), | |
| 206 | − )), | |
| 199 | + closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), | |
| 200 | + ), | |
| 207 | 201 | ); |
| 208 | 202 | |
| 209 | 203 | const effectiveLocalConfig = yield* synchronizeLocalWorkspaceState({ |
| @@ -212,14 +206,13 @@ export const createControlPlaneRuntime = ( · 2 unmodified lines | ||
| 212 | 206 | }).pipe( |
| 213 | 207 | Effect.mapError(toLocalRuntimeBootstrapError), |
| 214 | 208 | Effect.catchAll((error) => |
| 215 | − closeScope(scope).pipe( | |
| 216 | − Effect.zipRight(Effect.fail(error)), | |
| 217 | − )), | |
| 209 | + closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), | |
| 210 | + ), | |
| 218 | 211 | ); |
| 219 | 212 | |
| 220 | 213 | const resolveSecretMaterial = |
| 221 | − options.resolveSecretMaterial | |
| 222 | − ?? createDefaultSecretMaterialResolver({ | |
| 214 | + options.resolveSecretMaterial ?? | |
| 215 | + createDefaultSecretMaterialResolver({ | |
| 223 | 216 | rows, |
| 224 | 217 | localConfig: effectiveLocalConfig, |
| 225 | 218 | workspaceRoot: localWorkspaceContext.workspaceRoot, |
| @@ -245,8 +238,8 @@ export const createControlPlaneRuntime = ( · 19 unmodified lines | ||
| 245 | 238 | }, |
| 246 | 239 | }); |
| 247 | 240 | const executionResolver = |
| 248 | − options.executionResolver | |
| 249 | − ?? createWorkspaceExecutionEnvironmentResolver({ | |
| 241 | + options.executionResolver ?? | |
| 242 | + createWorkspaceExecutionEnvironmentResolver({ | |
| 250 | 243 | rows, |
| 251 | 244 | sourceAuthService, |
| 252 | 245 | resolveSecretMaterial, |
| @@ -275,6 +268,7 @@ export const createControlPlaneRuntime = ( · 22 unmodified lines | ||
| 275 | 268 | persistence, |
| 276 | 269 | localInstallation, |
| 277 | 270 | runtimeLayer: concreteRuntimeLayer, |
| 278 | − close: () => Effect.runPromise(Scope.close(scope, Exit.void)).catch(() => {}), | |
| 271 | + close: () => | |
| 272 | + Effect.runPromise(Scope.close(scope, Exit.void)).catch(() => {}), | |
| 279 | 273 | }; |
| 280 | 274 | }); |
| @@ -3,52 +3,103 @@ import * as Data from "effect/Data"; · 2 unmodified lines | ||
| 3 | 3 | export const unknownLocalErrorDetails = (cause: unknown): string => |
| 4 | 4 | cause instanceof Error ? cause.message : String(cause); |
| 5 | 5 | |
| 6 | −export class LocalFileSystemError extends Data.TaggedError("LocalFileSystemError")<{ | |
| 6 | +export class LocalFileSystemError extends Data.TaggedError( | |
| 7 | + "LocalFileSystemError", | |
| 8 | +)<{ | |
| 7 | 9 | readonly message: string; |
| 8 | 10 | readonly action: string; |
| 9 | 11 | readonly path: string; |
| 10 | 12 | readonly details: string; |
| 11 | 13 | }> {} |
| 12 | 14 | |
| 13 | −export class LocalExecutorConfigDecodeError extends Data.TaggedError("LocalExecutorConfigDecodeError")<{ | |
| 15 | +export class LocalExecutorConfigDecodeError extends Data.TaggedError( | |
| 16 | + "LocalExecutorConfigDecodeError", | |
| 17 | +)<{ | |
| 14 | 18 | readonly message: string; |
| 15 | 19 | readonly path: string; |
| 16 | 20 | readonly details: string; |
| 17 | 21 | }> {} |
| 18 | 22 | |
| 19 | −export class LocalWorkspaceStateDecodeError extends Data.TaggedError("LocalWorkspaceStateDecodeError")<{ | |
| 23 | +export class LocalWorkspaceStateDecodeError extends Data.TaggedError( | |
| 24 | + "LocalWorkspaceStateDecodeError", | |
| 25 | +)<{ | |
| 20 | 26 | readonly message: string; |
| 21 | 27 | readonly path: string; |
| 22 | 28 | readonly details: string; |
| 23 | 29 | }> {} |
| 24 | 30 | |
| 25 | −export class LocalSourceArtifactDecodeError extends Data.TaggedError("LocalSourceArtifactDecodeError")<{ | |
| 31 | +export class LocalSourceArtifactDecodeError extends Data.TaggedError( | |
| 32 | + "LocalSourceArtifactDecodeError", | |
| 33 | +)<{ | |
| 26 | 34 | readonly message: string; |
| 27 | 35 | readonly path: string; |
| 28 | 36 | readonly details: string; |
| 29 | 37 | }> {} |
| 30 | 38 | |
| 31 | −export class RuntimeLocalWorkspaceUnavailableError extends Data.TaggedError("RuntimeLocalWorkspaceUnavailableError")<{ | |
| 39 | +export class LocalToolTranspileError extends Data.TaggedError( | |
| 40 | + "LocalToolTranspileError", | |
| 41 | +)<{ | |
| 42 | + readonly message: string; | |
| 43 | + readonly path: string; | |
| 44 | + readonly details: string; | |
| 45 | +}> {} | |
| 46 | + | |
| 47 | +export class LocalToolImportError extends Data.TaggedError( | |
| 48 | + "LocalToolImportError", | |
| 49 | +)<{ | |
| 50 | + readonly message: string; | |
| 51 | + readonly path: string; | |
| 52 | + readonly details: string; | |
| 53 | +}> {} | |
| 54 | + | |
| 55 | +export class LocalToolDefinitionError extends Data.TaggedError( | |
| 56 | + "LocalToolDefinitionError", | |
| 57 | +)<{ | |
| 58 | + readonly message: string; | |
| 59 | + readonly path: string; | |
| 60 | + readonly details: string; | |
| 61 | +}> {} | |
| 62 | + | |
| 63 | +export class LocalToolPathConflictError extends Data.TaggedError( | |
| 64 | + "LocalToolPathConflictError", | |
| 65 | +)<{ | |
| 66 | + readonly message: string; | |
| 67 | + readonly path: string; | |
| 68 | + readonly otherPath: string; | |
| 69 | + readonly toolPath: string; | |
| 70 | +}> {} | |
| 71 | + | |
| 72 | +export class RuntimeLocalWorkspaceUnavailableError extends Data.TaggedError( | |
| 73 | + "RuntimeLocalWorkspaceUnavailableError", | |
| 74 | +)<{ | |
| 32 | 75 | readonly message: string; |
| 33 | 76 | }> {} |
| 34 | 77 | |
| 35 | −export class RuntimeLocalWorkspaceMismatchError extends Data.TaggedError("RuntimeLocalWorkspaceMismatchError")<{ | |
| 78 | +export class RuntimeLocalWorkspaceMismatchError extends Data.TaggedError( | |
| 79 | + "RuntimeLocalWorkspaceMismatchError", | |
| 80 | +)<{ | |
| 36 | 81 | readonly message: string; |
| 37 | 82 | readonly requestedWorkspaceId: string; |
| 38 | 83 | readonly activeWorkspaceId: string; |
| 39 | 84 | }> {} |
| 40 | 85 | |
| 41 | −export class LocalConfiguredSourceNotFoundError extends Data.TaggedError("LocalConfiguredSourceNotFoundError")<{ | |
| 86 | +export class LocalConfiguredSourceNotFoundError extends Data.TaggedError( | |
| 87 | + "LocalConfiguredSourceNotFoundError", | |
| 88 | +)<{ | |
| 42 | 89 | readonly message: string; |
| 43 | 90 | readonly sourceId: string; |
| 44 | 91 | }> {} |
| 45 | 92 | |
| 46 | −export class LocalSourceArtifactMissingError extends Data.TaggedError("LocalSourceArtifactMissingError")<{ | |
| 93 | +export class LocalSourceArtifactMissingError extends Data.TaggedError( | |
| 94 | + "LocalSourceArtifactMissingError", | |
| 95 | +)<{ | |
| 47 | 96 | readonly message: string; |
| 48 | 97 | readonly sourceId: string; |
| 49 | 98 | }> {} |
| 50 | 99 | |
| 51 | −export class LocalUnsupportedSourceKindError extends Data.TaggedError("LocalUnsupportedSourceKindError")<{ | |
| 100 | +export class LocalUnsupportedSourceKindError extends Data.TaggedError( | |
| 101 | + "LocalUnsupportedSourceKindError", | |
| 102 | +)<{ | |
| 52 | 103 | readonly message: string; |
| 53 | 104 | readonly kind: string; |
| 54 | 105 | }> {} |
| @@ -0,0 +1,654 @@ | ||
| 1 | +import { createHash } from "node:crypto"; | |
| 2 | +import { existsSync } from "node:fs"; | |
| 3 | +import { dirname, extname, join, relative, resolve, sep } from "node:path"; | |
| 4 | +import { fileURLToPath, pathToFileURL } from "node:url"; | |
| 5 | +import { FileSystem } from "@effect/platform"; | |
| 6 | +import { NodeFileSystem } from "@effect/platform-node"; | |
| 7 | +import { | |
| 8 | + createToolCatalogFromTools, | |
| 9 | + makeToolInvokerFromTools, | |
| 10 | + type ExecutableTool, | |
| 11 | + type StandardSchema, | |
| 12 | + type ToolCatalog, | |
| 13 | + type ToolDefinition, | |
| 14 | + type ToolInput, | |
| 15 | + type ToolInvoker, | |
| 16 | + type ToolMap, | |
| 17 | + type ToolMetadata, | |
| 18 | + type ToolPath, | |
| 19 | +} from "@executor/codemode-core"; | |
| 20 | +import * as Effect from "effect/Effect"; | |
| 21 | +import * as ts from "typescript"; | |
| 22 | + | |
| 23 | +import type { ResolvedLocalWorkspaceContext } from "./local-config"; | |
| 24 | +import { | |
| 25 | + LocalFileSystemError, | |
| 26 | + LocalToolDefinitionError, | |
| 27 | + LocalToolImportError, | |
| 28 | + LocalToolPathConflictError, | |
| 29 | + LocalToolTranspileError, | |
| 30 | + unknownLocalErrorDetails, | |
| 31 | +} from "./local-errors"; | |
| 32 | + | |
| 33 | +const SUPPORTED_LOCAL_TOOL_EXTENSIONS = new Set([".ts", ".js", ".mjs"]); | |
| 34 | +const LOCAL_TOOLS_DIRECTORY = "tools"; | |
| 35 | +const LOCAL_TOOLS_ARTIFACT_DIRECTORY = "local-tools"; | |
| 36 | +const LOCAL_TOOLS_SOURCE_KEY_PREFIX = "local.tool"; | |
| 37 | +const LOCAL_TOOL_SEGMENT_PATTERN = /^[A-Za-z0-9_-]+$/; | |
| 38 | + | |
| 39 | +type Awaitable<T> = T | Promise<T>; | |
| 40 | + | |
| 41 | +type ParsedStandardSchema<TSchema extends StandardSchema<any, any>> = | |
| 42 | + TSchema extends StandardSchema<any, infer Parsed> ? Parsed : never; | |
| 43 | + | |
| 44 | +export type LocalTool< | |
| 45 | + TInputSchema extends StandardSchema<any, any> = StandardSchema<any, any>, | |
| 46 | + TOutputSchema extends StandardSchema<any, any> | undefined = | |
| 47 | + | StandardSchema<any, any> | |
| 48 | + | undefined, | |
| 49 | +> = { | |
| 50 | + description?: string; | |
| 51 | + inputSchema: TInputSchema; | |
| 52 | + outputSchema?: TOutputSchema; | |
| 53 | + parameters?: TInputSchema; | |
| 54 | + execute: ( | |
| 55 | + args: ParsedStandardSchema<TInputSchema>, | |
| 56 | + ) => Awaitable< | |
| 57 | + TOutputSchema extends StandardSchema<any, any> | |
| 58 | + ? ParsedStandardSchema<TOutputSchema> | |
| 59 | + : unknown | |
| 60 | + >; | |
| 61 | +}; | |
| 62 | + | |
| 63 | +export type LocalToolWithMetadata< | |
| 64 | + TInputSchema extends StandardSchema<any, any> = StandardSchema<any, any>, | |
| 65 | + TOutputSchema extends StandardSchema<any, any> | undefined = | |
| 66 | + | StandardSchema<any, any> | |
| 67 | + | undefined, | |
| 68 | +> = { | |
| 69 | + tool: LocalTool<TInputSchema, TOutputSchema>; | |
| 70 | + metadata?: Omit<ToolMetadata, "sourceKey">; | |
| 71 | +}; | |
| 72 | + | |
| 73 | +export const defineLocalTool = < | |
| 74 | + TInputSchema extends StandardSchema<any, any>, | |
| 75 | + TOutputSchema extends StandardSchema<any, any> | undefined = undefined, | |
| 76 | +>( | |
| 77 | + input: | |
| 78 | + | LocalTool<TInputSchema, TOutputSchema> | |
| 79 | + | LocalToolWithMetadata<TInputSchema, TOutputSchema>, | |
| 80 | +): typeof input => input; | |
| 81 | + | |
| 82 | +export type LocalToolRuntime = { | |
| 83 | + tools: ToolMap; | |
| 84 | + catalog: ToolCatalog; | |
| 85 | + toolInvoker: ToolInvoker; | |
| 86 | + toolPaths: Set<string>; | |
| 87 | +}; | |
| 88 | + | |
| 89 | +const emptyLocalToolRuntime = (): LocalToolRuntime => { | |
| 90 | + const tools: ToolMap = {}; | |
| 91 | + return { | |
| 92 | + tools, | |
| 93 | + catalog: createToolCatalogFromTools({ tools }), | |
| 94 | + toolInvoker: makeToolInvokerFromTools({ tools }), | |
| 95 | + toolPaths: new Set(), | |
| 96 | + }; | |
| 97 | +}; | |
| 98 | + | |
| 99 | +const provideNodeFileSystem = <A, E, R>( | |
| 100 | + effect: Effect.Effect<A, E, R | FileSystem.FileSystem>, | |
| 101 | +): Effect.Effect<A, E, Exclude<R, FileSystem.FileSystem>> => | |
| 102 | + effect.pipe(Effect.provide(NodeFileSystem.layer)) as Effect.Effect< | |
| 103 | + A, | |
| 104 | + E, | |
| 105 | + Exclude<R, FileSystem.FileSystem> | |
| 106 | + >; | |
| 107 | + | |
| 108 | +const mapFileSystemError = (path: string, action: string) => (cause: unknown) => | |
| 109 | + new LocalFileSystemError({ | |
| 110 | + message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`, | |
| 111 | + action, | |
| 112 | + path, | |
| 113 | + details: unknownLocalErrorDetails(cause), | |
| 114 | + }); | |
| 115 | + | |
| 116 | +const localToolsDirectory = (context: ResolvedLocalWorkspaceContext): string => | |
| 117 | + join(context.configDirectory, LOCAL_TOOLS_DIRECTORY); | |
| 118 | + | |
| 119 | +const localToolsArtifactDirectory = ( | |
| 120 | + context: ResolvedLocalWorkspaceContext, | |
| 121 | +): string => join(context.artifactsDirectory, LOCAL_TOOLS_ARTIFACT_DIRECTORY); | |
| 122 | + | |
| 123 | +const isSupportedLocalToolFile = (path: string): boolean => | |
| 124 | + SUPPORTED_LOCAL_TOOL_EXTENSIONS.has(extname(path)); | |
| 125 | + | |
| 126 | +const isIgnoredToolSegment = (segment: string): boolean => | |
| 127 | + segment.startsWith(".") || segment.startsWith("_"); | |
| 128 | + | |
| 129 | +const toArtifactRelativePath = (relativePath: string): string => { | |
| 130 | + const extension = extname(relativePath); | |
| 131 | + return `${relativePath.slice(0, -extension.length)}.js`; | |
| 132 | +}; | |
| 133 | + | |
| 134 | +const toToolPath = ( | |
| 135 | + relativePath: string, | |
| 136 | +): Effect.Effect<ToolPath, LocalToolDefinitionError> => | |
| 137 | + Effect.gen(function* () { | |
| 138 | + const withoutExtension = relativePath.slice( | |
| 139 | + 0, | |
| 140 | + -extname(relativePath).length, | |
| 141 | + ); | |
| 142 | + const rawSegments = withoutExtension.split(sep).filter(Boolean); | |
| 143 | + const segments = | |
| 144 | + rawSegments.at(-1) === "index" ? rawSegments.slice(0, -1) : rawSegments; | |
| 145 | + | |
| 146 | + if (segments.length === 0) { | |
| 147 | + return yield* Effect.fail( | |
| 148 | + new LocalToolDefinitionError({ | |
| 149 | + message: `Invalid local tool path ${relativePath}: root index files are not supported`, | |
| 150 | + path: relativePath, | |
| 151 | + details: | |
| 152 | + "Root index files are not supported. Use a named file such as hello.ts or a nested index.ts.", | |
| 153 | + }), | |
| 154 | + ); | |
| 155 | + } | |
| 156 | + | |
| 157 | + for (const segment of segments) { | |
| 158 | + if (!LOCAL_TOOL_SEGMENT_PATTERN.test(segment)) { | |
| 159 | + return yield* Effect.fail( | |
| 160 | + new LocalToolDefinitionError({ | |
| 161 | + message: `Invalid local tool path ${relativePath}: segment ${segment} contains unsupported characters`, | |
| 162 | + path: relativePath, | |
| 163 | + details: `Tool path segments may only contain letters, numbers, underscores, and hyphens. Invalid segment: ${segment}`, | |
| 164 | + }), | |
| 165 | + ); | |
| 166 | + } | |
| 167 | + } | |
| 168 | + | |
| 169 | + return segments.join(".") as ToolPath; | |
| 170 | + }); | |
| 171 | + | |
| 172 | +const readDirectoryEntries = (path: string) => | |
| 173 | + Effect.gen(function* () { | |
| 174 | + const fs = yield* FileSystem.FileSystem; | |
| 175 | + const entries = yield* fs | |
| 176 | + .readDirectory(path) | |
| 177 | + .pipe( | |
| 178 | + Effect.mapError(mapFileSystemError(path, "read local tools directory")), | |
| 179 | + ); | |
| 180 | + return entries.sort((left, right) => left.localeCompare(right)); | |
| 181 | + }); | |
| 182 | + | |
| 183 | +const collectLocalToolSourceFiles = ( | |
| 184 | + root: string, | |
| 185 | +): Effect.Effect<string[], LocalFileSystemError, FileSystem.FileSystem> => | |
| 186 | + Effect.gen(function* () { | |
| 187 | + const fs = yield* FileSystem.FileSystem; | |
| 188 | + const exists = yield* fs | |
| 189 | + .exists(root) | |
| 190 | + .pipe( | |
| 191 | + Effect.mapError( | |
| 192 | + mapFileSystemError(root, "check local tools directory"), | |
| 193 | + ), | |
| 194 | + ); | |
| 195 | + if (!exists) { | |
| 196 | + return []; | |
| 197 | + } | |
| 198 | + | |
| 199 | + const walk = ( | |
| 200 | + directory: string, | |
| 201 | + ): Effect.Effect<string[], LocalFileSystemError, FileSystem.FileSystem> => | |
| 202 | + Effect.gen(function* () { | |
| 203 | + const entries = yield* readDirectoryEntries(directory); | |
| 204 | + const files: string[] = []; | |
| 205 | + | |
| 206 | + for (const entry of entries) { | |
| 207 | + const fullPath = join(directory, entry); | |
| 208 | + const info = yield* fs | |
| 209 | + .stat(fullPath) | |
| 210 | + .pipe( | |
| 211 | + Effect.mapError( | |
| 212 | + mapFileSystemError(fullPath, "stat local tool path"), | |
| 213 | + ), | |
| 214 | + ); | |
| 215 | + | |
| 216 | + if (info.type === "Directory") { | |
| 217 | + files.push(...(yield* walk(fullPath))); | |
| 218 | + continue; | |
| 219 | + } | |
| 220 | + | |
| 221 | + if (info.type === "File" && isSupportedLocalToolFile(fullPath)) { | |
| 222 | + files.push(fullPath); | |
| 223 | + } | |
| 224 | + } | |
| 225 | + | |
| 226 | + return files; | |
| 227 | + }); | |
| 228 | + | |
| 229 | + return yield* walk(root); | |
| 230 | + }); | |
| 231 | + | |
| 232 | +const toolCandidateFiles = (root: string, files: readonly string[]): string[] => | |
| 233 | + files.filter((path) => | |
| 234 | + relative(root, path) | |
| 235 | + .split(sep) | |
| 236 | + .every((segment) => !isIgnoredToolSegment(segment)), | |
| 237 | + ); | |
| 238 | + | |
| 239 | +const resolveCompilerDiagnostics = ( | |
| 240 | + sourcePath: string, | |
| 241 | + diagnostics: readonly ts.Diagnostic[] | undefined, | |
| 242 | +): LocalToolTranspileError | null => { | |
| 243 | + const issues = (diagnostics ?? []) | |
| 244 | + .filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error) | |
| 245 | + .map((diagnostic) => { | |
| 246 | + const message = ts.flattenDiagnosticMessageText( | |
| 247 | + diagnostic.messageText, | |
| 248 | + "\n", | |
| 249 | + ); | |
| 250 | + if (!diagnostic.file || diagnostic.start === undefined) { | |
| 251 | + return message; | |
| 252 | + } | |
| 253 | + const position = diagnostic.file.getLineAndCharacterOfPosition( | |
| 254 | + diagnostic.start, | |
| 255 | + ); | |
| 256 | + return `${position.line + 1}:${position.character + 1} ${message}`; | |
| 257 | + }); | |
| 258 | + | |
| 259 | + if (issues.length === 0) { | |
| 260 | + return null; | |
| 261 | + } | |
| 262 | + | |
| 263 | + return new LocalToolTranspileError({ | |
| 264 | + message: `Failed to transpile local tool ${sourcePath}`, | |
| 265 | + path: sourcePath, | |
| 266 | + details: issues.join("\n"), | |
| 267 | + }); | |
| 268 | +}; | |
| 269 | + | |
| 270 | +const normalizeRelativeImportSpecifier = (specifier: string): string => { | |
| 271 | + if (!specifier.startsWith("./") && !specifier.startsWith("../")) { | |
| 272 | + return specifier; | |
| 273 | + } | |
| 274 | + | |
| 275 | + const match = specifier.match(/^(.*?)(\?.*|#.*)?$/); | |
| 276 | + const pathname = match?.[1] ?? specifier; | |
| 277 | + const suffix = match?.[2] ?? ""; | |
| 278 | + const extension = extname(pathname); | |
| 279 | + | |
| 280 | + if (extension === ".js") { | |
| 281 | + return specifier; | |
| 282 | + } | |
| 283 | + | |
| 284 | + if ([".ts", ".mjs"].includes(extension)) { | |
| 285 | + return `${pathname.slice(0, -extension.length)}.js${suffix}`; | |
| 286 | + } | |
| 287 | + | |
| 288 | + if (extension.length > 0) { | |
| 289 | + return specifier; | |
| 290 | + } | |
| 291 | + | |
| 292 | + return `${pathname}.js${suffix}`; | |
| 293 | +}; | |
| 294 | + | |
| 295 | +const rewriteRelativeImportSpecifiers = (code: string): string => | |
| 296 | + code | |
| 297 | + .replace( | |
| 298 | + /(from\s+["'])(\.{1,2}\/[^"']+)(["'])/g, | |
| 299 | + (_match, prefix: string, specifier: string, suffix: string) => | |
| 300 | + `${prefix}${normalizeRelativeImportSpecifier(specifier)}${suffix}`, | |
| 301 | + ) | |
| 302 | + .replace( | |
| 303 | + /(import\s+["'])(\.{1,2}\/[^"']+)(["'])/g, | |
| 304 | + (_match, prefix: string, specifier: string, suffix: string) => | |
| 305 | + `${prefix}${normalizeRelativeImportSpecifier(specifier)}${suffix}`, | |
| 306 | + ) | |
| 307 | + .replace( | |
| 308 | + /(import\(\s*["'])(\.{1,2}\/[^"']+)(["']\s*\))/g, | |
| 309 | + (_match, prefix: string, specifier: string, suffix: string) => | |
| 310 | + `${prefix}${normalizeRelativeImportSpecifier(specifier)}${suffix}`, | |
| 311 | + ); | |
| 312 | + | |
| 313 | +const transpileSourceFile = (input: { | |
| 314 | + sourcePath: string; | |
| 315 | + content: string; | |
| 316 | +}): Effect.Effect<string, LocalToolTranspileError> => | |
| 317 | + Effect.gen(function* () { | |
| 318 | + if (extname(input.sourcePath) !== ".ts") { | |
| 319 | + return rewriteRelativeImportSpecifiers(input.content); | |
| 320 | + } | |
| 321 | + | |
| 322 | + const transpiled = ts.transpileModule(input.content, { | |
| 323 | + fileName: input.sourcePath, | |
| 324 | + compilerOptions: { | |
| 325 | + module: ts.ModuleKind.ESNext, | |
| 326 | + moduleResolution: ts.ModuleResolutionKind.Bundler, | |
| 327 | + target: ts.ScriptTarget.ES2022, | |
| 328 | + sourceMap: false, | |
| 329 | + inlineSourceMap: false, | |
| 330 | + inlineSources: false, | |
| 331 | + }, | |
| 332 | + reportDiagnostics: true, | |
| 333 | + }); | |
| 334 | + | |
| 335 | + const diagnosticError = resolveCompilerDiagnostics( | |
| 336 | + input.sourcePath, | |
| 337 | + transpiled.diagnostics, | |
| 338 | + ); | |
| 339 | + if (diagnosticError) { | |
| 340 | + return yield* Effect.fail(diagnosticError); | |
| 341 | + } | |
| 342 | + | |
| 343 | + return rewriteRelativeImportSpecifiers(transpiled.outputText); | |
| 344 | + }); | |
| 345 | + | |
| 346 | +const resolveExecutorNodeModulesDirectory = (): string => { | |
| 347 | + let current = dirname(fileURLToPath(import.meta.url)); | |
| 348 | + | |
| 349 | + while (true) { | |
| 350 | + const candidate = join(current, "node_modules"); | |
| 351 | + if (existsSync(candidate)) { | |
| 352 | + return candidate; | |
| 353 | + } | |
| 354 | + | |
| 355 | + const parent = dirname(current); | |
| 356 | + if (parent === current) { | |
| 357 | + throw new Error( | |
| 358 | + "Unable to locate Executor node_modules directory for local tools", | |
| 359 | + ); | |
| 360 | + } | |
| 361 | + current = parent; | |
| 362 | + } | |
| 363 | +}; | |
| 364 | + | |
| 365 | +const EXECUTOR_NODE_MODULES_DIRECTORY = resolveExecutorNodeModulesDirectory(); | |
| 366 | + | |
| 367 | +const ensureArtifactNodeModulesLink = (artifactRoot: string) => | |
| 368 | + Effect.gen(function* () { | |
| 369 | + const fs = yield* FileSystem.FileSystem; | |
| 370 | + const linkPath = join(artifactRoot, "node_modules"); | |
| 371 | + const exists = yield* fs | |
| 372 | + .exists(linkPath) | |
| 373 | + .pipe( | |
| 374 | + Effect.mapError( | |
| 375 | + mapFileSystemError(linkPath, "check local tool node_modules link"), | |
| 376 | + ), | |
| 377 | + ); | |
| 378 | + if (exists) { | |
| 379 | + return; | |
| 380 | + } | |
| 381 | + | |
| 382 | + yield* fs | |
| 383 | + .symlink(EXECUTOR_NODE_MODULES_DIRECTORY, linkPath) | |
| 384 | + .pipe( | |
| 385 | + Effect.mapError( | |
| 386 | + mapFileSystemError(linkPath, "create local tool node_modules link"), | |
| 387 | + ), | |
| 388 | + ); | |
| 389 | + }); | |
| 390 | + | |
| 391 | +const buildLocalToolArtifacts = (input: { | |
| 392 | + context: ResolvedLocalWorkspaceContext; | |
| 393 | + sourceDirectory: string; | |
| 394 | + sourceFiles: readonly string[]; | |
| 395 | +}): Effect.Effect< | |
| 396 | + Map<string, string>, | |
| 397 | + LocalFileSystemError | LocalToolTranspileError, | |
| 398 | + FileSystem.FileSystem | |
| 399 | +> => | |
| 400 | + Effect.gen(function* () { | |
| 401 | + const fs = yield* FileSystem.FileSystem; | |
| 402 | + const sources = yield* Effect.forEach(input.sourceFiles, (sourcePath) => | |
| 403 | + Effect.gen(function* () { | |
| 404 | + const content = yield* fs | |
| 405 | + .readFileString(sourcePath, "utf8") | |
| 406 | + .pipe( | |
| 407 | + Effect.mapError( | |
| 408 | + mapFileSystemError(sourcePath, "read local tool source"), | |
| 409 | + ), | |
| 410 | + ); | |
| 411 | + const relativePath = relative(input.sourceDirectory, sourcePath); | |
| 412 | + const contentHash = createHash("sha256") | |
| 413 | + .update(relativePath) | |
| 414 | + .update("\0") | |
| 415 | + .update(content) | |
| 416 | + .digest("hex") | |
| 417 | + .slice(0, 12); | |
| 418 | + return { | |
| 419 | + sourcePath, | |
| 420 | + relativePath, | |
| 421 | + content, | |
| 422 | + contentHash, | |
| 423 | + }; | |
| 424 | + }), | |
| 425 | + ); | |
| 426 | + | |
| 427 | + const buildHash = createHash("sha256") | |
| 428 | + .update( | |
| 429 | + JSON.stringify( | |
| 430 | + sources.map((source) => [source.relativePath, source.contentHash]), | |
| 431 | + ), | |
| 432 | + ) | |
| 433 | + .digest("hex") | |
| 434 | + .slice(0, 16); | |
| 435 | + const artifactRoot = join( | |
| 436 | + localToolsArtifactDirectory(input.context), | |
| 437 | + buildHash, | |
| 438 | + ); | |
| 439 | + | |
| 440 | + yield* fs | |
| 441 | + .makeDirectory(artifactRoot, { recursive: true }) | |
| 442 | + .pipe( | |
| 443 | + Effect.mapError( | |
| 444 | + mapFileSystemError( | |
| 445 | + artifactRoot, | |
| 446 | + "create local tool artifact directory", | |
| 447 | + ), | |
| 448 | + ), | |
| 449 | + ); | |
| 450 | + yield* ensureArtifactNodeModulesLink(artifactRoot); | |
| 451 | + | |
| 452 | + const output = new Map<string, string>(); | |
| 453 | + | |
| 454 | + for (const source of sources) { | |
| 455 | + const artifactRelativePath = toArtifactRelativePath(source.relativePath); | |
| 456 | + const artifactPath = join(artifactRoot, artifactRelativePath); | |
| 457 | + const artifactDirectory = dirname(artifactPath); | |
| 458 | + const transpiled = yield* transpileSourceFile({ | |
| 459 | + sourcePath: source.sourcePath, | |
| 460 | + content: source.content, | |
| 461 | + }); | |
| 462 | + | |
| 463 | + yield* fs | |
| 464 | + .makeDirectory(artifactDirectory, { recursive: true }) | |
| 465 | + .pipe( | |
| 466 | + Effect.mapError( | |
| 467 | + mapFileSystemError( | |
| 468 | + artifactDirectory, | |
| 469 | + "create local tool artifact parent directory", | |
| 470 | + ), | |
| 471 | + ), | |
| 472 | + ); | |
| 473 | + yield* fs | |
| 474 | + .writeFileString(artifactPath, transpiled) | |
| 475 | + .pipe( | |
| 476 | + Effect.mapError( | |
| 477 | + mapFileSystemError(artifactPath, "write local tool artifact"), | |
| 478 | + ), | |
| 479 | + ); | |
| 480 | + | |
| 481 | + output.set(source.sourcePath, artifactPath); | |
| 482 | + } | |
| 483 | + | |
| 484 | + return output; | |
| 485 | + }); | |
| 486 | + | |
| 487 | +const isExecutableTool = (value: unknown): value is ExecutableTool => | |
| 488 | + typeof value === "object" && | |
| 489 | + value !== null && | |
| 490 | + "inputSchema" in value && | |
| 491 | + typeof (value as { execute?: unknown }).execute === "function"; | |
| 492 | + | |
| 493 | +const isToolDefinition = (value: unknown): value is ToolDefinition => | |
| 494 | + typeof value === "object" && | |
| 495 | + value !== null && | |
| 496 | + "tool" in value && | |
| 497 | + isExecutableTool((value as { tool?: unknown }).tool); | |
| 498 | + | |
| 499 | +const normalizeToolExport = (input: { | |
| 500 | + toolPath: ToolPath; | |
| 501 | + sourcePath: string; | |
| 502 | + exported: unknown; | |
| 503 | +}): Effect.Effect<ToolInput, LocalToolDefinitionError> => { | |
| 504 | + const sourceKey = `${LOCAL_TOOLS_SOURCE_KEY_PREFIX}.${input.toolPath}`; | |
| 505 | + | |
| 506 | + if (isToolDefinition(input.exported)) { | |
| 507 | + const definition = input.exported; | |
| 508 | + return Effect.succeed({ | |
| 509 | + ...definition, | |
| 510 | + metadata: { | |
| 511 | + ...(definition.metadata ?? {}), | |
| 512 | + sourceKey, | |
| 513 | + }, | |
| 514 | + }); | |
| 515 | + } | |
| 516 | + | |
| 517 | + if (isExecutableTool(input.exported)) { | |
| 518 | + const tool = input.exported; | |
| 519 | + return Effect.succeed({ | |
| 520 | + tool, | |
| 521 | + metadata: { | |
| 522 | + sourceKey, | |
| 523 | + }, | |
| 524 | + }); | |
| 525 | + } | |
| 526 | + | |
| 527 | + return Effect.fail( | |
| 528 | + new LocalToolDefinitionError({ | |
| 529 | + message: `Invalid local tool export in ${input.sourcePath}`, | |
| 530 | + path: input.sourcePath, | |
| 531 | + details: | |
| 532 | + "Local tool files must export a default value or named `tool` export containing either an executable tool or a `{ tool, metadata? }` definition.", | |
| 533 | + }), | |
| 534 | + ); | |
| 535 | +}; | |
| 536 | + | |
| 537 | +const importLocalToolModule = (input: { | |
| 538 | + sourcePath: string; | |
| 539 | + artifactPath: string; | |
| 540 | + toolPath: ToolPath; | |
| 541 | +}): Effect.Effect<ToolInput, LocalToolImportError | LocalToolDefinitionError> => | |
| 542 | + Effect.tryPromise({ | |
| 543 | + try: () => import(pathToFileURL(resolve(input.artifactPath)).href), | |
| 544 | + catch: (cause) => | |
| 545 | + new LocalToolImportError({ | |
| 546 | + message: `Failed to import local tool ${input.sourcePath}`, | |
| 547 | + path: input.sourcePath, | |
| 548 | + details: unknownLocalErrorDetails(cause), | |
| 549 | + }), | |
| 550 | + }).pipe( | |
| 551 | + Effect.flatMap((mod) => { | |
| 552 | + const hasDefault = mod.default !== undefined; | |
| 553 | + const hasNamedTool = mod.tool !== undefined; | |
| 554 | + | |
| 555 | + if (hasDefault && hasNamedTool) { | |
| 556 | + return Effect.fail( | |
| 557 | + new LocalToolDefinitionError({ | |
| 558 | + message: `Invalid local tool export in ${input.sourcePath}`, | |
| 559 | + path: input.sourcePath, | |
| 560 | + details: | |
| 561 | + "Export either a default tool or a named `tool` export, but not both.", | |
| 562 | + }), | |
| 563 | + ); | |
| 564 | + } | |
| 565 | + | |
| 566 | + if (!hasDefault && !hasNamedTool) { | |
| 567 | + return Effect.fail( | |
| 568 | + new LocalToolDefinitionError({ | |
| 569 | + message: `Missing local tool export in ${input.sourcePath}`, | |
| 570 | + path: input.sourcePath, | |
| 571 | + details: "Expected a default export or named `tool` export.", | |
| 572 | + }), | |
| 573 | + ); | |
| 574 | + } | |
| 575 | + | |
| 576 | + return normalizeToolExport({ | |
| 577 | + toolPath: input.toolPath, | |
| 578 | + sourcePath: input.sourcePath, | |
| 579 | + exported: hasDefault ? mod.default : mod.tool, | |
| 580 | + }); | |
| 581 | + }), | |
| 582 | + ); | |
| 583 | + | |
| 584 | +export const loadLocalToolRuntime = ( | |
| 585 | + context: ResolvedLocalWorkspaceContext, | |
| 586 | +): Effect.Effect< | |
| 587 | + LocalToolRuntime, | |
| 588 | + | LocalFileSystemError | |
| 589 | + | LocalToolDefinitionError | |
| 590 | + | LocalToolImportError | |
| 591 | + | LocalToolPathConflictError | |
| 592 | + | LocalToolTranspileError | |
| 593 | +> => | |
| 594 | + provideNodeFileSystem( | |
| 595 | + Effect.gen(function* () { | |
| 596 | + const sourceDirectory = localToolsDirectory(context); | |
| 597 | + const sourceFiles = yield* collectLocalToolSourceFiles(sourceDirectory); | |
| 598 | + | |
| 599 | + if (sourceFiles.length === 0) { | |
| 600 | + return emptyLocalToolRuntime(); | |
| 601 | + } | |
| 602 | + | |
| 603 | + const artifactPaths = yield* buildLocalToolArtifacts({ | |
| 604 | + context, | |
| 605 | + sourceDirectory, | |
| 606 | + sourceFiles, | |
| 607 | + }); | |
| 608 | + const candidateFiles = toolCandidateFiles(sourceDirectory, sourceFiles); | |
| 609 | + const tools: ToolMap = {}; | |
| 610 | + const seenToolPaths = new Map<string, string>(); | |
| 611 | + | |
| 612 | + for (const sourcePath of candidateFiles) { | |
| 613 | + const relativePath = relative(sourceDirectory, sourcePath); | |
| 614 | + const toolPath = yield* toToolPath(relativePath); | |
| 615 | + const existing = seenToolPaths.get(toolPath); | |
| 616 | + if (existing) { | |
| 617 | + return yield* Effect.fail( | |
| 618 | + new LocalToolPathConflictError({ | |
| 619 | + message: `Local tool path conflict for ${toolPath}`, | |
| 620 | + path: sourcePath, | |
| 621 | + otherPath: existing, | |
| 622 | + toolPath, | |
| 623 | + }), | |
| 624 | + ); | |
| 625 | + } | |
| 626 | + | |
| 627 | + const artifactPath = artifactPaths.get(sourcePath); | |
| 628 | + if (!artifactPath) { | |
| 629 | + return yield* Effect.fail( | |
| 630 | + new LocalToolImportError({ | |
| 631 | + message: `Missing compiled artifact for local tool ${sourcePath}`, | |
| 632 | + path: sourcePath, | |
| 633 | + details: | |
| 634 | + "Expected a compiled local tool artifact, but none was produced.", | |
| 635 | + }), | |
| 636 | + ); | |
| 637 | + } | |
| 638 | + | |
| 639 | + tools[toolPath] = yield* importLocalToolModule({ | |
| 640 | + sourcePath, | |
| 641 | + artifactPath, | |
| 642 | + toolPath, | |
| 643 | + }); | |
| 644 | + seenToolPaths.set(toolPath, sourcePath); | |
| 645 | + } | |
| 646 | + | |
| 647 | + return { | |
| 648 | + tools, | |
| 649 | + catalog: createToolCatalogFromTools({ tools }), | |
| 650 | + toolInvoker: makeToolInvokerFromTools({ tools }), | |
| 651 | + toolPaths: new Set(Object.keys(tools)), | |
| 652 | + } satisfies LocalToolRuntime; | |
| 653 | + }), | |
| 654 | + ); | |
| @@ -1,10 +1,9 @@ | ||
| 1 | 1 | import { randomUUID } from "node:crypto"; |
| 2 | −import { mkdtempSync } from "node:fs"; | |
| 3 | 2 | import { createServer } from "node:http"; |
| 4 | −import { tmpdir } from "node:os"; | |
| 5 | −import { join } from "node:path"; | |
| 3 | +import { dirname, join } from "node:path"; | |
| 6 | 4 | |
| 7 | 5 | import { |
| 6 | + FileSystem, | |
| 8 | 7 | HttpApi, |
| 9 | 8 | HttpApiBuilder, |
| 10 | 9 | HttpApiEndpoint, |
| @@ -12,6 +11,7 @@ import { · 1 unmodified line | ||
| 12 | 11 | HttpApiSchema, |
| 13 | 12 | HttpServer, |
| 14 | 13 | } from "@effect/platform"; |
| 14 | +import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"; | |
| 15 | 15 | import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; |
| 16 | 16 | import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; |
| 17 | 17 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
| @@ -80,9 +80,7 @@ import { · 62 unmodified lines | ||
| 80 | 80 | loadLocalWorkspaceState, |
| 81 | 81 | writeLocalWorkspaceState, |
| 82 | 82 | } from "./local-workspace-state"; |
| 83 | −import { | |
| 84 | − materializationFromMcpManifestEntries, | |
| 85 | −} from "./source-adapters/mcp"; | |
| 83 | +import { materializationFromMcpManifestEntries } from "./source-adapters/mcp"; | |
| 86 | 84 | import { createWorkspaceExecutionEnvironmentResolver } from "./workspace-execution-environment"; |
| 87 | 85 | |
| 88 | 86 | type CountedMcpServer = { |
| @@ -116,6 +114,15 @@ const normalizeSearchText = ( · 27 unmodified lines | ||
| 116 | 114 | .join(" ") |
| 117 | 115 | .toLowerCase(); |
| 118 | 116 | |
| 117 | +const provideNodeFileSystem = <A, E, R>( | |
| 118 | + effect: Effect.Effect<A, E, R | FileSystem.FileSystem>, | |
| 119 | +): Effect.Effect<A, E, Exclude<R, FileSystem.FileSystem>> => | |
| 120 | + effect.pipe(Effect.provide(NodeFileSystem.layer)) as Effect.Effect< | |
| 121 | + A, | |
| 122 | + E, | |
| 123 | + Exclude<R, FileSystem.FileSystem> | |
| 124 | + >; | |
| 125 | + | |
| 119 | 126 | const GraphqlUserType = new GraphQLObjectType({ |
| 120 | 127 | name: "User", |
| 121 | 128 | fields: { |
| @@ -941,98 +948,98 @@ const persistConnectedGithubOpenApiSource = (input: { · 819 unmodified lines | ||
| 941 | 948 | context: input.runtimeLocalWorkspace.context, |
| 942 | 949 | sourceId: source.id, |
| 943 | 950 | artifact: buildLocalSourceArtifact({ |
| 944 | − source: { | |
| 945 | − ...source, | |
| 946 | − sourceHash: manifest.sourceHash, | |
| 947 | − }, | |
| 948 | − materialization: { | |
| 949 | − manifestJson: JSON.stringify(manifest), | |
| 950 | − manifestHash: manifest.sourceHash, | |
| 951 | − sourceHash: manifest.sourceHash, | |
| 952 | − documents: [ | |
| 953 | − { | |
| 954 | − id: `src_recipe_doc_${randomUUID()}`, | |
| 955 | − recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 956 | − documentKind: "openapi", | |
| 957 | − documentKey: | |
| 958 | − typeof source.binding.specUrl === "string" | |
| 959 | − ? source.binding.specUrl | |
| 960 | − : source.endpoint, | |
| 961 | − contentText: openApiDocumentText, | |
| 962 | − contentHash: manifest.sourceHash, | |
| 963 | − fetchedAt: now, | |
| 964 | − createdAt: now, | |
| 965 | − updatedAt: now, | |
| 966 | − }, | |
| 967 | − ], | |
| 968 | − schemaBundles: | |
| 969 | − refEntries.length > 0 | |
| 970 | − ? [ | |
| 971 | − { | |
| 972 | − id: SourceRecipeSchemaBundleIdSchema.make( | |
| 973 | − `src_recipe_bundle_${randomUUID()}`, | |
| 974 | − ), | |
| 975 | − recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 976 | − bundleKind: "json_schema_ref_map", | |
| 977 | − refsJson: JSON.stringify( | |
| 978 | − Object.fromEntries( | |
| 979 | − refEntries.map(([ref, rawValue]) => { | |
| 980 | − try { | |
| 981 | − return [ref, JSON.parse(rawValue) as unknown]; | |
| 982 | − } catch { | |
| 983 | − return [ref, rawValue]; | |
| 984 | − } | |
| 985 | − }), | |
| 986 | − ), | |
| 987 | − ), | |
| 988 | − contentHash: manifest.sourceHash, | |
| 989 | − createdAt: now, | |
| 990 | − updatedAt: now, | |
| 991 | − }, | |
| 992 | − ] | |
| 993 | − : [], | |
| 994 | − operations: definitions.map((definition) => { | |
| 995 | − const presentation = buildOpenApiToolPresentation({ | |
| 996 | − definition, | |
| 997 | − }); | |
| 998 | − const method = definition.method.toUpperCase(); | |
| 999 | − | |
| 1000 | − return { | |
| 1001 | − id: `src_recipe_op_${randomUUID()}`, | |
| 1002 | − recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 1003 | − operationKey: definition.toolId, | |
| 1004 | − transportKind: "http", | |
| 1005 | − toolId: definition.toolId, | |
| 1006 | − title: definition.name, | |
| 1007 | − description: definition.description, | |
| 1008 | − operationKind: | |
| 1009 | − method === "GET" || method === "HEAD" | |
| 1010 | − ? "read" | |
| 1011 | − : method === "DELETE" | |
| 1012 | − ? "delete" | |
| 1013 | − : "write", | |
| 1014 | − searchText: normalizeSearchText( | |
| 1015 | − definition.toolId, | |
| 1016 | − definition.name, | |
| 1017 | − definition.description, | |
| 1018 | − definition.rawToolId, | |
| 1019 | − definition.operationId ?? undefined, | |
| 1020 | − definition.method, | |
| 1021 | − definition.path, | |
| 1022 | − definition.group, | |
| 1023 | − definition.leaf, | |
| 1024 | − definition.tags.join(" "), | |
| 1025 | − ), | |
| 1026 | − inputSchemaJson: presentation.inputSchemaJson ?? null, | |
| 1027 | − outputSchemaJson: presentation.outputSchemaJson ?? null, | |
| 1028 | − providerKind: "openapi", | |
| 1029 | − providerDataJson: presentation.providerDataJson, | |
| 1030 | − createdAt: now, | |
| 1031 | − updatedAt: now, | |
| 1032 | − }; | |
| 1033 | − }), | |
| 951 | + source: { | |
| 952 | + ...source, | |
| 953 | + sourceHash: manifest.sourceHash, | |
| 954 | + }, | |
| 955 | + materialization: { | |
| 956 | + manifestJson: JSON.stringify(manifest), | |
| 957 | + manifestHash: manifest.sourceHash, | |
| 958 | + sourceHash: manifest.sourceHash, | |
| 959 | + documents: [ | |
| 960 | + { | |
| 961 | + id: `src_recipe_doc_${randomUUID()}`, | |
| 962 | + recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 963 | + documentKind: "openapi", | |
| 964 | + documentKey: | |
| 965 | + typeof source.binding.specUrl === "string" | |
| 966 | + ? source.binding.specUrl | |
| 967 | + : source.endpoint, | |
| 968 | + contentText: openApiDocumentText, | |
| 969 | + contentHash: manifest.sourceHash, | |
| 970 | + fetchedAt: now, | |
| 971 | + createdAt: now, | |
| 972 | + updatedAt: now, | |
| 1034 | 973 | }, |
| 974 | + ], | |
| 975 | + schemaBundles: | |
| 976 | + refEntries.length > 0 | |
| 977 | + ? [ | |
| 978 | + { | |
| 979 | + id: SourceRecipeSchemaBundleIdSchema.make( | |
| 980 | + `src_recipe_bundle_${randomUUID()}`, | |
| 981 | + ), | |
| 982 | + recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 983 | + bundleKind: "json_schema_ref_map", | |
| 984 | + refsJson: JSON.stringify( | |
| 985 | + Object.fromEntries( | |
| 986 | + refEntries.map(([ref, rawValue]) => { | |
| 987 | + try { | |
| 988 | + return [ref, JSON.parse(rawValue) as unknown]; | |
| 989 | + } catch { | |
| 990 | + return [ref, rawValue]; | |
| 991 | + } | |
| 992 | + }), | |
| 993 | + ), | |
| 994 | + ), | |
| 995 | + contentHash: manifest.sourceHash, | |
| 996 | + createdAt: now, | |
| 997 | + updatedAt: now, | |
| 998 | + }, | |
| 999 | + ] | |
| 1000 | + : [], | |
| 1001 | + operations: definitions.map((definition) => { | |
| 1002 | + const presentation = buildOpenApiToolPresentation({ | |
| 1003 | + definition, | |
| 1004 | + }); | |
| 1005 | + const method = definition.method.toUpperCase(); | |
| 1006 | + | |
| 1007 | + return { | |
| 1008 | + id: `src_recipe_op_${randomUUID()}`, | |
| 1009 | + recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 1010 | + operationKey: definition.toolId, | |
| 1011 | + transportKind: "http", | |
| 1012 | + toolId: definition.toolId, | |
| 1013 | + title: definition.name, | |
| 1014 | + description: definition.description, | |
| 1015 | + operationKind: | |
| 1016 | + method === "GET" || method === "HEAD" | |
| 1017 | + ? "read" | |
| 1018 | + : method === "DELETE" | |
| 1019 | + ? "delete" | |
| 1020 | + : "write", | |
| 1021 | + searchText: normalizeSearchText( | |
| 1022 | + definition.toolId, | |
| 1023 | + definition.name, | |
| 1024 | + definition.description, | |
| 1025 | + definition.rawToolId, | |
| 1026 | + definition.operationId ?? undefined, | |
| 1027 | + definition.method, | |
| 1028 | + definition.path, | |
| 1029 | + definition.group, | |
| 1030 | + definition.leaf, | |
| 1031 | + definition.tags.join(" "), | |
| 1032 | + ), | |
| 1033 | + inputSchemaJson: presentation.inputSchemaJson ?? null, | |
| 1034 | + outputSchemaJson: presentation.outputSchemaJson ?? null, | |
| 1035 | + providerKind: "openapi", | |
| 1036 | + providerDataJson: presentation.providerDataJson, | |
| 1037 | + createdAt: now, | |
| 1038 | + updatedAt: now, | |
| 1039 | + }; | |
| 1035 | 1040 | }), |
| 1041 | + }, | |
| 1042 | + }), | |
| 1036 | 1043 | }); |
| 1037 | 1044 | yield* writeLocalWorkspaceState({ |
| 1038 | 1045 | context: input.runtimeLocalWorkspace.context, |
| @@ -1054,62 +1061,104 @@ const persistConnectedGithubOpenApiSource = (input: { · 15 unmodified lines | ||
| 1054 | 1061 | return; |
| 1055 | 1062 | }) as Effect.Effect<void, Error, never>; |
| 1056 | 1063 | |
| 1057 | −const makeRuntimeLocalWorkspaceState = (workspaceId: WorkspaceId): Effect.Effect<RuntimeLocalWorkspaceState, Error, never> => | |
| 1058 | − Effect.gen(function* () { | |
| 1059 | − const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-workspace-env-")); | |
| 1060 | − const context = yield* resolveLocalWorkspaceContext({ workspaceRoot }); | |
| 1061 | − const loadedConfig = yield* loadLocalExecutorConfig(context); | |
| 1062 | − return { | |
| 1063 | − context, | |
| 1064 | − installation: { | |
| 1065 | − workspaceId, | |
| 1066 | − accountId: AccountIdSchema.make("acc_local_test"), | |
| 1067 | − }, | |
| 1068 | − loadedConfig, | |
| 1069 | − } satisfies RuntimeLocalWorkspaceState; | |
| 1070 | − }); | |
| 1071 | − | |
| 1072 | −const makeResolver = ( | |
| 1073 | − persistence: SqlControlPlanePersistence, | |
| 1074 | − runtimeLocalWorkspace: RuntimeLocalWorkspaceState, | |
| 1075 | −) => | |
| 1076 | − ((input: Parameters<ReturnType<typeof createWorkspaceExecutionEnvironmentResolver>>[0]) => | |
| 1064 | +const makeRuntimeLocalWorkspaceState = ( | |
| 1065 | + workspaceId: WorkspaceId, | |
| 1066 | +): Effect.Effect<RuntimeLocalWorkspaceState, Error, Scope.Scope> => | |
| 1067 | + provideNodeFileSystem( | |
| 1068 | + Effect.gen(function* () { | |
| 1069 | + const fs = yield* FileSystem.FileSystem; | |
| 1070 | + const workspaceRoot = yield* fs.makeTempDirectoryScoped({ | |
| 1071 | + prefix: "executor-workspace-env-", | |
| 1072 | + }); | |
| 1073 | + const context = yield* resolveLocalWorkspaceContext({ workspaceRoot }); | |
| 1074 | + const loadedConfig = yield* loadLocalExecutorConfig(context); | |
| 1075 | + return { | |
| 1076 | + context, | |
| 1077 | + installation: { | |
| 1078 | + workspaceId, | |
| 1079 | + accountId: AccountIdSchema.make("acc_local_test"), | |
| 1080 | + }, | |
| 1081 | + loadedConfig, | |
| 1082 | + } satisfies RuntimeLocalWorkspaceState; | |
| 1083 | + }), | |
| 1084 | + ); | |
| 1085 | + | |
| 1086 | +const writeLocalToolSource = (input: { | |
| 1087 | + runtimeLocalWorkspace: RuntimeLocalWorkspaceState; | |
| 1088 | + relativePath: string; | |
| 1089 | + content: string; | |
| 1090 | +}): Effect.Effect<void, Error, never> => | |
| 1091 | + provideNodeFileSystem( | |
| 1092 | + Effect.gen(function* () { | |
| 1093 | + const fs = yield* FileSystem.FileSystem; | |
| 1094 | + const toolPath = join( | |
| 1095 | + input.runtimeLocalWorkspace.context.configDirectory, | |
| 1096 | + "tools", | |
| 1097 | + input.relativePath, | |
| 1098 | + ); | |
| 1099 | + yield* fs.makeDirectory(dirname(toolPath), { recursive: true }); | |
| 1100 | + yield* fs.writeFileString(toolPath, input.content); | |
| 1101 | + }), | |
| 1102 | + ); | |
| 1103 | + | |
| 1104 | +const makeResolver = | |
| 1105 | + ( | |
| 1106 | + persistence: SqlControlPlanePersistence, | |
| 1107 | + runtimeLocalWorkspace: RuntimeLocalWorkspaceState, | |
| 1108 | + ) => | |
| 1109 | + ( | |
| 1110 | + input: Parameters< | |
| 1111 | + ReturnType<typeof createWorkspaceExecutionEnvironmentResolver> | |
| 1112 | + >[0], | |
| 1113 | + ) => | |
| 1077 | 1114 | createWorkspaceExecutionEnvironmentResolver({ |
| 1078 | − rows: persistence.rows, | |
| 1079 | − sourceAuthService: { | |
| 1080 | − getLocalServerBaseUrl: () => null, | |
| 1081 | − storeSecretMaterial: () => | |
| 1082 | − Effect.fail(new Error("not implemented in test")), | |
| 1083 | − getSourceById: () => Effect.fail(new Error("not implemented in test")), | |
| 1084 | − addExecutorSource: () => | |
| 1085 | − Effect.fail(new Error("not implemented in test")), | |
| 1086 | − connectMcpSource: () => Effect.fail(new Error("not implemented in test")), | |
| 1087 | − startSourceOAuthSession: () => | |
| 1088 | − Effect.fail(new Error("not implemented in test")), | |
| 1089 | − completeSourceOAuthSession: () => | |
| 1090 | − Effect.fail(new Error("not implemented in test")), | |
| 1091 | − completeSourceCredentialSetup: () => | |
| 1092 | − Effect.fail(new Error("not implemented in test")), | |
| 1093 | − } as RuntimeSourceAuthService, | |
| 1094 | − })(input).pipe( | |
| 1095 | − Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace), | |
| 1096 | − )); | |
| 1097 | − | |
| 1098 | −const makeResolverWithLiveSourceAuth = ( | |
| 1099 | − persistence: SqlControlPlanePersistence, | |
| 1100 | − runtimeLocalWorkspace: RuntimeLocalWorkspaceState, | |
| 1101 | −) => | |
| 1102 | − ((input: Parameters<ReturnType<typeof createWorkspaceExecutionEnvironmentResolver>>[0]) => | |
| 1115 | + rows: persistence.rows, | |
| 1116 | + sourceAuthService: { | |
| 1117 | + getLocalServerBaseUrl: () => null, | |
| 1118 | + storeSecretMaterial: () => | |
| 1119 | + Effect.fail(new Error("not implemented in test")), | |
| 1120 | + getSourceById: () => Effect.fail(new Error("not implemented in test")), | |
| 1121 | + addExecutorSource: () => | |
| 1122 | + Effect.fail(new Error("not implemented in test")), | |
| 1123 | + connectMcpSource: () => | |
| 1124 | + Effect.fail(new Error("not implemented in test")), | |
| 1125 | + startSourceOAuthSession: () => | |
| 1126 | + Effect.fail(new Error("not implemented in test")), | |
| 1127 | + completeSourceOAuthSession: () => | |
| 1128 | + Effect.fail(new Error("not implemented in test")), | |
| 1129 | + completeSourceCredentialSetup: () => | |
| 1130 | + Effect.fail(new Error("not implemented in test")), | |
| 1131 | + } as RuntimeSourceAuthService, | |
| 1132 | + })(input).pipe( | |
| 1133 | + Effect.provideService( | |
| 1134 | + RuntimeLocalWorkspaceService, | |
| 1135 | + runtimeLocalWorkspace, | |
| 1136 | + ), | |
| 1137 | + ); | |
| 1138 | + | |
| 1139 | +const makeResolverWithLiveSourceAuth = | |
| 1140 | + ( | |
| 1141 | + persistence: SqlControlPlanePersistence, | |
| 1142 | + runtimeLocalWorkspace: RuntimeLocalWorkspaceState, | |
| 1143 | + ) => | |
| 1144 | + ( | |
| 1145 | + input: Parameters< | |
| 1146 | + ReturnType<typeof createWorkspaceExecutionEnvironmentResolver> | |
| 1147 | + >[0], | |
| 1148 | + ) => | |
| 1103 | 1149 | createWorkspaceExecutionEnvironmentResolver({ |
| 1104 | − rows: persistence.rows, | |
| 1105 | − sourceAuthService: createRuntimeSourceAuthService({ | |
| 1106 | 1150 | rows: persistence.rows, |
| 1107 | − liveExecutionManager: createLiveExecutionManager(), | |
| 1108 | − getLocalServerBaseUrl: () => "http://127.0.0.1:8788", | |
| 1109 | − }), | |
| 1110 | − })(input).pipe( | |
| 1111 | − Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace), | |
| 1112 | − )); | |
| 1151 | + sourceAuthService: createRuntimeSourceAuthService({ | |
| 1152 | + rows: persistence.rows, | |
| 1153 | + liveExecutionManager: createLiveExecutionManager(), | |
| 1154 | + getLocalServerBaseUrl: () => "http://127.0.0.1:8788", | |
| 1155 | + }), | |
| 1156 | + })(input).pipe( | |
| 1157 | + Effect.provideService( | |
| 1158 | + RuntimeLocalWorkspaceService, | |
| 1159 | + runtimeLocalWorkspace, | |
| 1160 | + ), | |
| 1161 | + ); | |
| 1113 | 1162 | |
| 1114 | 1163 | describe("workspace-execution-environment", () => { |
| 1115 | 1164 | it.scoped("discovers persisted tools without live MCP listing", () => |
| @@ -1118,7 +1167,8 @@ describe("workspace-execution-environment", () => { · 2 unmodified lines | ||
| 1118 | 1167 | const persistence = yield* makePersistence; |
| 1119 | 1168 | const workspaceId = WorkspaceIdSchema.make("ws_catalog"); |
| 1120 | 1169 | const sourceId = SourceIdSchema.make("src_catalog"); |
| 1121 | − const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1170 | + const runtimeLocalWorkspace = | |
| 1171 | + yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1122 | 1172 | yield* persistConnectedEchoTool({ |
| 1123 | 1173 | runtimeLocalWorkspace, |
| 1124 | 1174 | endpoint: server.endpoint, |
| @@ -1132,7 +1182,10 @@ describe("workspace-execution-environment", () => { · 7 unmodified lines | ||
| 1132 | 1182 | cause instanceof Error ? cause : new Error(String(cause)), |
| 1133 | 1183 | }).pipe(Effect.orDie); |
| 1134 | 1184 | |
| 1135 | − const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | |
| 1185 | + const resolveEnvironment = makeResolver( | |
| 1186 | + persistence, | |
| 1187 | + runtimeLocalWorkspace, | |
| 1188 | + ); | |
| 1136 | 1189 | const environment = yield* resolveEnvironment({ |
| 1137 | 1190 | workspaceId, |
| 1138 | 1191 | accountId: AccountIdSchema.make("acc_catalog"), |
| @@ -1165,14 +1218,18 @@ describe("workspace-execution-environment", () => { · 26 unmodified lines | ||
| 1165 | 1218 | const persistence = yield* makePersistence; |
| 1166 | 1219 | const workspaceId = WorkspaceIdSchema.make("ws_github_discovery"); |
| 1167 | 1220 | const sourceId = SourceIdSchema.make("src_github_discovery"); |
| 1168 | − const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1221 | + const runtimeLocalWorkspace = | |
| 1222 | + yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1169 | 1223 | yield* persistConnectedGithubOpenApiSource({ |
| 1170 | 1224 | runtimeLocalWorkspace, |
| 1171 | 1225 | workspaceId, |
| 1172 | 1226 | sourceId, |
| 1173 | 1227 | }); |
| 1174 | 1228 | |
| 1175 | − const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | |
| 1229 | + const resolveEnvironment = makeResolver( | |
| 1230 | + persistence, | |
| 1231 | + runtimeLocalWorkspace, | |
| 1232 | + ); | |
| 1176 | 1233 | const environment = yield* resolveEnvironment({ |
| 1177 | 1234 | workspaceId, |
| 1178 | 1235 | accountId: AccountIdSchema.make("acc_github_discovery"), |
| @@ -1219,7 +1276,8 @@ describe("workspace-execution-environment", () => { · 40 unmodified lines | ||
| 1219 | 1276 | const persistence = yield* makePersistence; |
| 1220 | 1277 | const workspaceId = WorkspaceIdSchema.make("ws_invoke"); |
| 1221 | 1278 | const sourceId = SourceIdSchema.make("src_invoke"); |
| 1222 | − const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1279 | + const runtimeLocalWorkspace = | |
| 1280 | + yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1223 | 1281 | yield* persistConnectedEchoTool({ |
| 1224 | 1282 | runtimeLocalWorkspace, |
| 1225 | 1283 | endpoint: server.endpoint, |
| @@ -1227,7 +1285,10 @@ describe("workspace-execution-environment", () => { · 1 unmodified line | ||
| 1227 | 1285 | sourceId, |
| 1228 | 1286 | }); |
| 1229 | 1287 | |
| 1230 | − const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | |
| 1288 | + const resolveEnvironment = makeResolver( | |
| 1289 | + persistence, | |
| 1290 | + runtimeLocalWorkspace, | |
| 1291 | + ); | |
| 1231 | 1292 | const environment = yield* resolveEnvironment({ |
| 1232 | 1293 | workspaceId, |
| 1233 | 1294 | accountId: AccountIdSchema.make("acc_invoke"), |
| @@ -1249,6 +1310,126 @@ describe("workspace-execution-environment", () => { · 15 unmodified lines | ||
| 1249 | 1310 | }), |
| 1250 | 1311 | ); |
| 1251 | 1312 | |
| 1313 | + it.scoped( | |
| 1314 | + "discovers and invokes local TypeScript tools from grouped directories", | |
| 1315 | + () => | |
| 1316 | + Effect.gen(function* () { | |
| 1317 | + const persistence = yield* makePersistence; | |
| 1318 | + const workspaceId = WorkspaceIdSchema.make("ws_local_tools"); | |
| 1319 | + const runtimeLocalWorkspace = | |
| 1320 | + yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1321 | + yield* writeLocalToolSource({ | |
| 1322 | + runtimeLocalWorkspace, | |
| 1323 | + relativePath: join("math", "arithmetic", "add.ts"), | |
| 1324 | + content: ` | |
| 1325 | +import { Schema } from "effect" | |
| 1326 | + | |
| 1327 | +const inputSchema = Schema.standardSchemaV1( | |
| 1328 | + Schema.Struct({ | |
| 1329 | + a: Schema.Number, | |
| 1330 | + b: Schema.Number, | |
| 1331 | + }), | |
| 1332 | +) | |
| 1333 | + | |
| 1334 | +const outputSchema = Schema.standardSchemaV1( | |
| 1335 | + Schema.Struct({ | |
| 1336 | + sum: Schema.Number, | |
| 1337 | + }), | |
| 1338 | +) | |
| 1339 | + | |
| 1340 | +export default { | |
| 1341 | + description: "Add two numbers together", | |
| 1342 | + inputSchema, | |
| 1343 | + outputSchema, | |
| 1344 | + async execute({ a, b }: { a: number; b: number }) { | |
| 1345 | + await Promise.resolve() | |
| 1346 | + return { sum: a + b } | |
| 1347 | + }, | |
| 1348 | +} | |
| 1349 | +`.trimStart(), | |
| 1350 | + }); | |
| 1351 | + yield* writeLocalToolSource({ | |
| 1352 | + runtimeLocalWorkspace, | |
| 1353 | + relativePath: join("text", "case", "upper.ts"), | |
| 1354 | + content: ` | |
| 1355 | +import { defineLocalTool } from "@executor/control-plane" | |
| 1356 | +import * as z from "zod/v4" | |
| 1357 | + | |
| 1358 | +const inputSchema = z.object({ | |
| 1359 | + value: z.string(), | |
| 1360 | +}) | |
| 1361 | + | |
| 1362 | +const outputSchema = z.object({ | |
| 1363 | + value: z.string(), | |
| 1364 | +}) | |
| 1365 | + | |
| 1366 | +export const tool = defineLocalTool({ | |
| 1367 | + description: "Convert text to uppercase", | |
| 1368 | + inputSchema, | |
| 1369 | + outputSchema, | |
| 1370 | + async execute({ value }) { | |
| 1371 | + await Promise.resolve() | |
| 1372 | + return { value: value.toUpperCase() } | |
| 1373 | + }, | |
| 1374 | +}) | |
| 1375 | +`.trimStart(), | |
| 1376 | + }); | |
| 1377 | + | |
| 1378 | + const resolveEnvironment = makeResolver( | |
| 1379 | + persistence, | |
| 1380 | + runtimeLocalWorkspace, | |
| 1381 | + ); | |
| 1382 | + const environment = yield* resolveEnvironment({ | |
| 1383 | + workspaceId, | |
| 1384 | + accountId: AccountIdSchema.make("acc_local_tools"), | |
| 1385 | + executionId: ExecutionIdSchema.make("exec_local_tools"), | |
| 1386 | + }); | |
| 1387 | + | |
| 1388 | + const discovered = (yield* environment.toolInvoker.invoke({ | |
| 1389 | + path: "discover", | |
| 1390 | + args: { | |
| 1391 | + query: "add two numbers arithmetic", | |
| 1392 | + limit: 5, | |
| 1393 | + }, | |
| 1394 | + })) as { | |
| 1395 | + bestPath: string | null; | |
| 1396 | + results: Array<{ path: string }>; | |
| 1397 | + }; | |
| 1398 | + | |
| 1399 | + expect(discovered.bestPath).toBe("math.arithmetic.add"); | |
| 1400 | + expect(discovered.results[0]?.path).toBe("math.arithmetic.add"); | |
| 1401 | + | |
| 1402 | + const listed = yield* environment.catalog!.listTools({ | |
| 1403 | + namespace: "math.arithmetic", | |
| 1404 | + limit: 5, | |
| 1405 | + includeSchemas: false, | |
| 1406 | + }); | |
| 1407 | + | |
| 1408 | + expect(listed).toHaveLength(1); | |
| 1409 | + expect(listed[0]?.path).toBe("math.arithmetic.add"); | |
| 1410 | + expect(listed[0]?.sourceKey).toBe("local.tool.math.arithmetic.add"); | |
| 1411 | + | |
| 1412 | + const result = (yield* environment.toolInvoker.invoke({ | |
| 1413 | + path: "math.arithmetic.add", | |
| 1414 | + args: { | |
| 1415 | + a: 2, | |
| 1416 | + b: 3, | |
| 1417 | + }, | |
| 1418 | + })) as { sum: number }; | |
| 1419 | + | |
| 1420 | + expect(result.sum).toBe(5); | |
| 1421 | + | |
| 1422 | + const upper = (yield* environment.toolInvoker.invoke({ | |
| 1423 | + path: "text.case.upper", | |
| 1424 | + args: { | |
| 1425 | + value: "hello", | |
| 1426 | + }, | |
| 1427 | + })) as { value: string }; | |
| 1428 | + | |
| 1429 | + expect(upper.value).toBe("HELLO"); | |
| 1430 | + }), | |
| 1431 | + ); | |
| 1432 | + | |
| 1252 | 1433 | it.scoped( |
| 1253 | 1434 | "adds an OpenAPI source through executor.sources.add with elicited credentials", |
| 1254 | 1435 | () => |
| @@ -1256,8 +1437,12 @@ describe("workspace-execution-environment", () => { · 1 unmodified line | ||
| 1256 | 1437 | const specServer = yield* makeOpenApiSpecServer; |
| 1257 | 1438 | const persistence = yield* makePersistence; |
| 1258 | 1439 | const workspaceId = WorkspaceIdSchema.make("ws_add_openapi"); |
| 1259 | − const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1260 | − const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence, runtimeLocalWorkspace); | |
| 1440 | + const runtimeLocalWorkspace = | |
| 1441 | + yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1442 | + const resolveEnvironment = makeResolverWithLiveSourceAuth( | |
| 1443 | + persistence, | |
| 1444 | + runtimeLocalWorkspace, | |
| 1445 | + ); | |
| 1261 | 1446 | const now = Date.now(); |
| 1262 | 1447 | const tokenSecretMaterialId = SecretMaterialIdSchema.make( |
| 1263 | 1448 | "sec_test_openapi_bearer", |
| @@ -1338,7 +1523,9 @@ describe("workspace-execution-environment", () => { · 74 unmodified lines | ||
| 1338 | 1523 | encodeURIComponent("exec_add_openapi:executor.sources.add:"), |
| 1339 | 1524 | ); |
| 1340 | 1525 | |
| 1341 | − const loadedConfig = yield* loadLocalExecutorConfig(runtimeLocalWorkspace.context); | |
| 1526 | + const loadedConfig = yield* loadLocalExecutorConfig( | |
| 1527 | + runtimeLocalWorkspace.context, | |
| 1528 | + ); | |
| 1342 | 1529 | expect(loadedConfig.config?.sources?.[added.id]).toMatchObject({ |
| 1343 | 1530 | kind: "openapi", |
| 1344 | 1531 | connection: { |
| @@ -1352,7 +1539,9 @@ describe("workspace-execution-environment", () => { · 7 unmodified lines | ||
| 1352 | 1539 | expect(artifact?.documents[0]?.contentText).toContain( |
| 1353 | 1540 | '"operationId":"repos.getRepo"', |
| 1354 | 1541 | ); |
| 1355 | − const workspaceState = yield* loadLocalWorkspaceState(runtimeLocalWorkspace.context); | |
| 1542 | + const workspaceState = yield* loadLocalWorkspaceState( | |
| 1543 | + runtimeLocalWorkspace.context, | |
| 1544 | + ); | |
| 1356 | 1545 | expect(workspaceState.sources[added.id]?.status).toBe("connected"); |
| 1357 | 1546 | |
| 1358 | 1547 | const freshEnvironment = yield* resolveEnvironment({ |
| @@ -1399,8 +1588,12 @@ describe("workspace-execution-environment", () => { · 40 unmodified lines | ||
| 1399 | 1588 | const graphqlServer = yield* makeGraphqlServer; |
| 1400 | 1589 | const persistence = yield* makePersistence; |
| 1401 | 1590 | const workspaceId = WorkspaceIdSchema.make("ws_add_graphql"); |
| 1402 | − const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1403 | − const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence, runtimeLocalWorkspace); | |
| 1591 | + const runtimeLocalWorkspace = | |
| 1592 | + yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1593 | + const resolveEnvironment = makeResolverWithLiveSourceAuth( | |
| 1594 | + persistence, | |
| 1595 | + runtimeLocalWorkspace, | |
| 1596 | + ); | |
| 1404 | 1597 | const now = Date.now(); |
| 1405 | 1598 | const tokenSecretMaterialId = SecretMaterialIdSchema.make( |
| 1406 | 1599 | "sec_test_graphql_bearer", |
| @@ -1630,7 +1823,8 @@ describe("workspace-execution-environment", () => { · 223 unmodified lines | ||
| 1630 | 1823 | const persistence = yield* makePersistence; |
| 1631 | 1824 | const workspaceId = WorkspaceIdSchema.make("ws_params_auth"); |
| 1632 | 1825 | const sourceId = SourceIdSchema.make("src_params_auth"); |
| 1633 | − const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1826 | + const runtimeLocalWorkspace = | |
| 1827 | + yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1634 | 1828 | |
| 1635 | 1829 | yield* persistConnectedGithubOpenApiSource({ |
| 1636 | 1830 | runtimeLocalWorkspace, |
| @@ -1643,7 +1837,10 @@ describe("workspace-execution-environment", () => { · 6 unmodified lines | ||
| 1643 | 1837 | }, |
| 1644 | 1838 | }); |
| 1645 | 1839 | |
| 1646 | − const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | |
| 1840 | + const resolveEnvironment = makeResolver( | |
| 1841 | + persistence, | |
| 1842 | + runtimeLocalWorkspace, | |
| 1843 | + ); | |
| 1647 | 1844 | const environment = yield* resolveEnvironment({ |
| 1648 | 1845 | workspaceId, |
| 1649 | 1846 | accountId: AccountIdSchema.make("acc_params_auth"), |
| @@ -1683,7 +1880,8 @@ describe("workspace-execution-environment", () => { · 33 unmodified lines | ||
| 1683 | 1880 | "ws_openapi_describe_bundle", |
| 1684 | 1881 | ); |
| 1685 | 1882 | const sourceId = SourceIdSchema.make("src_openapi_describe_bundle"); |
| 1686 | − const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1883 | + const runtimeLocalWorkspace = | |
| 1884 | + yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1687 | 1885 | |
| 1688 | 1886 | yield* persistConnectedGithubOpenApiSource({ |
| 1689 | 1887 | runtimeLocalWorkspace, |
| @@ -1691,7 +1889,10 @@ describe("workspace-execution-environment", () => { · 1 unmodified line | ||
| 1691 | 1889 | sourceId, |
| 1692 | 1890 | }); |
| 1693 | 1891 | |
| 1694 | − const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | |
| 1892 | + const resolveEnvironment = makeResolver( | |
| 1893 | + persistence, | |
| 1894 | + runtimeLocalWorkspace, | |
| 1895 | + ); | |
| 1695 | 1896 | const environment = yield* resolveEnvironment({ |
| 1696 | 1897 | workspaceId, |
| 1697 | 1898 | accountId: AccountIdSchema.make("acc_openapi_describe_bundle"), |
| @@ -1739,8 +1940,12 @@ describe("workspace-execution-environment", () => { · 41 unmodified lines | ||
| 1739 | 1940 | Effect.gen(function* () { |
| 1740 | 1941 | const persistence = yield* makePersistence; |
| 1741 | 1942 | const workspaceId = WorkspaceIdSchema.make("ws_describe_add_source"); |
| 1742 | − const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1743 | − const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence, runtimeLocalWorkspace); | |
| 1943 | + const runtimeLocalWorkspace = | |
| 1944 | + yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1945 | + const resolveEnvironment = makeResolverWithLiveSourceAuth( | |
| 1946 | + persistence, | |
| 1947 | + runtimeLocalWorkspace, | |
| 1948 | + ); | |
| 1744 | 1949 | |
| 1745 | 1950 | const environment = yield* resolveEnvironment({ |
| 1746 | 1951 | workspaceId, |
| @@ -2,8 +2,10 @@ import { · 1 unmodified line | ||
| 2 | 2 | createSystemToolMap, |
| 3 | 3 | createToolCatalogFromTools, |
| 4 | 4 | makeToolInvokerFromTools, |
| 5 | + mergeToolMaps, | |
| 5 | 6 | type SearchHit, |
| 6 | 7 | type ToolCatalog, |
| 8 | + type ToolDescriptor, | |
| 7 | 9 | type ToolInvoker, |
| 8 | 10 | type ToolNamespace, |
| 9 | 11 | type ToolPath, |
| @@ -51,14 +53,13 @@ import { · 41 unmodified lines | ||
| 51 | 53 | evaluateInvocationPolicy, |
| 52 | 54 | type InvocationDescriptor, |
| 53 | 55 | } from "./invocation-policy-engine"; |
| 54 | −import { | |
| 55 | − loadRuntimeLocalWorkspacePolicies, | |
| 56 | −} from "./policies-operations"; | |
| 56 | +import { loadRuntimeLocalWorkspacePolicies } from "./policies-operations"; | |
| 57 | 57 | import { |
| 58 | 58 | getRuntimeLocalWorkspaceOption, |
| 59 | 59 | RuntimeLocalWorkspaceService, |
| 60 | 60 | type RuntimeLocalWorkspaceState, |
| 61 | 61 | } from "./local-runtime-context"; |
| 62 | +import { loadLocalToolRuntime, type LocalToolRuntime } from "./local-tools"; | |
| 62 | 63 | |
| 63 | 64 | const asToolPath = (value: string): ToolPath => value as ToolPath; |
| 64 | 65 | |
| @@ -205,10 +206,19 @@ const loadWorkspaceSchemaBundle = (input: { · 140 unmodified lines | ||
| 205 | 206 | : null; |
| 206 | 207 | } |
| 207 | 208 | |
| 208 | − const bundle = yield* input.rows.sourceRecipeSchemaBundles.getById(input.id); | |
| 209 | + const bundle = yield* input.rows.sourceRecipeSchemaBundles.getById( | |
| 210 | + input.id, | |
| 211 | + ); | |
| 209 | 212 | if (Option.isSome(bundle)) { |
| 210 | − const sourceRecords = yield* input.rows.sources.listByWorkspaceId(input.workspaceId); | |
| 211 | − if (sourceRecords.some((sourceRecord) => sourceRecord.recipeRevisionId === bundle.value.recipeRevisionId)) { | |
| 213 | + const sourceRecords = yield* input.rows.sources.listByWorkspaceId( | |
| 214 | + input.workspaceId, | |
| 215 | + ); | |
| 216 | + if ( | |
| 217 | + sourceRecords.some( | |
| 218 | + (sourceRecord) => | |
| 219 | + sourceRecord.recipeRevisionId === bundle.value.recipeRevisionId, | |
| 220 | + ) | |
| 221 | + ) { | |
| 212 | 222 | return { |
| 213 | 223 | id: bundle.value.id, |
| 214 | 224 | kind: bundle.value.bundleKind, |
| @@ -386,7 +396,9 @@ const authorizePersistedToolInvocation = (input: { · 171 unmodified lines | ||
| 386 | 396 | >[0]["onElicitation"]; |
| 387 | 397 | }): Effect.Effect<void, Error, never> => |
| 388 | 398 | Effect.gen(function* () { |
| 389 | − const localWorkspacePolicies = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | |
| 399 | + const localWorkspacePolicies = yield* loadRuntimeLocalWorkspacePolicies( | |
| 400 | + input.workspaceId, | |
| 401 | + ).pipe( | |
| 390 | 402 | Effect.mapError((cause) => |
| 391 | 403 | cause instanceof Error ? cause : new Error(String(cause)), |
| 392 | 404 | ), |
| @@ -474,151 +486,205 @@ const provideRuntimeLocalWorkspace = <A, E, R>( · 81 unmodified lines | ||
| 474 | 486 | ), |
| 475 | 487 | ); |
| 476 | 488 | |
| 489 | +const dedupeToolDescriptors = ( | |
| 490 | + preferred: readonly ToolDescriptor[], | |
| 491 | + fallback: readonly ToolDescriptor[], | |
| 492 | +): ToolDescriptor[] => { | |
| 493 | + const merged = new Map<string, ToolDescriptor>(); | |
| 494 | + | |
| 495 | + for (const descriptor of preferred) { | |
| 496 | + merged.set(descriptor.path, descriptor); | |
| 497 | + } | |
| 498 | + for (const descriptor of fallback) { | |
| 499 | + if (!merged.has(descriptor.path)) { | |
| 500 | + merged.set(descriptor.path, descriptor); | |
| 501 | + } | |
| 502 | + } | |
| 503 | + | |
| 504 | + return [...merged.values()]; | |
| 505 | +}; | |
| 506 | + | |
| 507 | +const dedupeSearchHits = ( | |
| 508 | + preferred: readonly SearchHit[], | |
| 509 | + fallback: readonly SearchHit[], | |
| 510 | +): SearchHit[] => { | |
| 511 | + const merged = new Map<string, SearchHit>(); | |
| 512 | + | |
| 513 | + for (const hit of preferred) { | |
| 514 | + merged.set(hit.path, hit); | |
| 515 | + } | |
| 516 | + for (const hit of fallback) { | |
| 517 | + if (!merged.has(hit.path)) { | |
| 518 | + merged.set(hit.path, hit); | |
| 519 | + } | |
| 520 | + } | |
| 521 | + | |
| 522 | + return [...merged.values()]; | |
| 523 | +}; | |
| 524 | + | |
| 477 | 525 | const createWorkspaceToolCatalog = (input: { |
| 478 | 526 | workspaceId: Source["workspaceId"]; |
| 479 | 527 | accountId: AccountId; |
| 480 | 528 | rows: SqlControlPlaneRows; |
| 481 | − executorCatalog: ToolCatalog; | |
| 529 | + authoredCatalog: ToolCatalog; | |
| 482 | 530 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null; |
| 483 | 531 | }): ToolCatalog => ({ |
| 484 | 532 | listNamespaces: ({ limit }) => |
| 485 | − provideRuntimeLocalWorkspace(Effect.gen(function* () { | |
| 486 | − const [recipeTools, executor] = yield* Effect.all([ | |
| 487 | − loadWorkspaceRecipeTools({ | |
| 488 | − rows: input.rows, | |
| 489 | − workspaceId: input.workspaceId, | |
| 490 | − accountId: input.accountId, | |
| 491 | − includeSchemas: false, | |
| 492 | − }), | |
| 493 | − input.executorCatalog.listNamespaces({ limit }), | |
| 494 | − ]); | |
| 495 | − | |
| 496 | − const merged = new Map<string, ToolNamespace>(); | |
| 497 | − for (const tool of recipeTools) { | |
| 498 | − const existing = merged.get(tool.searchNamespace); | |
| 499 | − merged.set(tool.searchNamespace, { | |
| 500 | − namespace: tool.searchNamespace, | |
| 501 | − toolCount: (existing?.toolCount ?? 0) + 1, | |
| 502 | − }); | |
| 503 | − } | |
| 504 | − for (const namespace of executor) { | |
| 505 | − const existing = merged.get(namespace.namespace); | |
| 506 | − merged.set(namespace.namespace, { | |
| 507 | − namespace: namespace.namespace, | |
| 508 | − displayName: namespace.displayName ?? existing?.displayName, | |
| 509 | − toolCount: | |
| 510 | − namespace.toolCount !== undefined || | |
| 511 | − existing?.toolCount === undefined | |
| 512 | − ? namespace.toolCount | |
| 513 | − : existing.toolCount, | |
| 514 | − }); | |
| 515 | − } | |
| 533 | + provideRuntimeLocalWorkspace( | |
| 534 | + Effect.gen(function* () { | |
| 535 | + const [recipeTools, authored] = yield* Effect.all([ | |
| 536 | + loadWorkspaceRecipeTools({ | |
| 537 | + rows: input.rows, | |
| 538 | + workspaceId: input.workspaceId, | |
| 539 | + accountId: input.accountId, | |
| 540 | + includeSchemas: false, | |
| 541 | + }), | |
| 542 | + input.authoredCatalog.listNamespaces({ limit }), | |
| 543 | + ]); | |
| 544 | + | |
| 545 | + const merged = new Map<string, ToolNamespace>(); | |
| 546 | + for (const tool of recipeTools) { | |
| 547 | + const existing = merged.get(tool.searchNamespace); | |
| 548 | + merged.set(tool.searchNamespace, { | |
| 549 | + namespace: tool.searchNamespace, | |
| 550 | + toolCount: (existing?.toolCount ?? 0) + 1, | |
| 551 | + }); | |
| 552 | + } | |
| 553 | + for (const namespace of authored) { | |
| 554 | + const existing = merged.get(namespace.namespace); | |
| 555 | + merged.set(namespace.namespace, { | |
| 556 | + namespace: namespace.namespace, | |
| 557 | + displayName: namespace.displayName ?? existing?.displayName, | |
| 558 | + toolCount: | |
| 559 | + namespace.toolCount !== undefined || | |
| 560 | + existing?.toolCount === undefined | |
| 561 | + ? namespace.toolCount | |
| 562 | + : existing.toolCount, | |
| 563 | + }); | |
| 564 | + } | |
| 516 | 565 | |
| 517 | − return [...merged.values()] | |
| 518 | − .sort((left, right) => left.namespace.localeCompare(right.namespace)) | |
| 519 | − .slice(0, limit); | |
| 520 | − }), input.runtimeLocalWorkspace), | |
| 566 | + return [...merged.values()] | |
| 567 | + .sort((left, right) => left.namespace.localeCompare(right.namespace)) | |
| 568 | + .slice(0, limit); | |
| 569 | + }), | |
| 570 | + input.runtimeLocalWorkspace, | |
| 571 | + ), | |
| 521 | 572 | |
| 522 | 573 | listTools: ({ namespace, query, limit, includeSchemas = false }) => |
| 523 | − provideRuntimeLocalWorkspace(Effect.gen(function* () { | |
| 524 | − const [recipeTools, executor] = yield* Effect.all([ | |
| 525 | − loadWorkspaceRecipeTools({ | |
| 574 | + provideRuntimeLocalWorkspace( | |
| 575 | + Effect.gen(function* () { | |
| 576 | + const [recipeTools, authored] = yield* Effect.all([ | |
| 577 | + loadWorkspaceRecipeTools({ | |
| 578 | + rows: input.rows, | |
| 579 | + workspaceId: input.workspaceId, | |
| 580 | + accountId: input.accountId, | |
| 581 | + includeSchemas, | |
| 582 | + }).pipe( | |
| 583 | + Effect.map((tools) => | |
| 584 | + tools.filter((tool) => { | |
| 585 | + if (namespace && tool.searchNamespace !== namespace) { | |
| 586 | + return false; | |
| 587 | + } | |
| 588 | + if (!query) { | |
| 589 | + return true; | |
| 590 | + } | |
| 591 | + return tokenize(query).every((token) => | |
| 592 | + tool.searchText.includes(token), | |
| 593 | + ); | |
| 594 | + }), | |
| 595 | + ), | |
| 596 | + ), | |
| 597 | + input.authoredCatalog.listTools({ | |
| 598 | + ...(namespace !== undefined ? { namespace } : {}), | |
| 599 | + ...(query !== undefined ? { query } : {}), | |
| 600 | + limit, | |
| 601 | + includeSchemas, | |
| 602 | + }), | |
| 603 | + ]); | |
| 604 | + | |
| 605 | + return dedupeToolDescriptors( | |
| 606 | + authored, | |
| 607 | + recipeTools.map((tool) => tool.descriptor), | |
| 608 | + ) | |
| 609 | + .sort((left, right) => left.path.localeCompare(right.path)) | |
| 610 | + .slice(0, limit); | |
| 611 | + }), | |
| 612 | + input.runtimeLocalWorkspace, | |
| 613 | + ), | |
| 614 | + | |
| 615 | + getToolByPath: ({ path, includeSchemas }) => | |
| 616 | + provideRuntimeLocalWorkspace( | |
| 617 | + Effect.gen(function* () { | |
| 618 | + const authored = yield* input.authoredCatalog.getToolByPath({ | |
| 619 | + path, | |
| 620 | + includeSchemas, | |
| 621 | + }); | |
| 622 | + if (authored) { | |
| 623 | + return authored; | |
| 624 | + } | |
| 625 | + | |
| 626 | + const recipeTool = yield* loadWorkspaceRecipeToolByPath({ | |
| 526 | 627 | rows: input.rows, |
| 527 | 628 | workspaceId: input.workspaceId, |
| 528 | 629 | accountId: input.accountId, |
| 630 | + path, | |
| 529 | 631 | includeSchemas, |
| 530 | − }).pipe( | |
| 531 | − Effect.map((tools) => | |
| 532 | − tools.filter((tool) => { | |
| 533 | − if (namespace && tool.searchNamespace !== namespace) { | |
| 534 | − return false; | |
| 535 | − } | |
| 536 | − if (!query) { | |
| 537 | − return true; | |
| 538 | − } | |
| 539 | − return tokenize(query).every((token) => | |
| 540 | − tool.searchText.includes(token), | |
| 541 | − ); | |
| 542 | − }), | |
| 543 | − ), | |
| 544 | − ), | |
| 545 | − input.executorCatalog.listTools({ | |
| 546 | − ...(namespace !== undefined ? { namespace } : {}), | |
| 547 | − ...(query !== undefined ? { query } : {}), | |
| 548 | − limit, | |
| 549 | − includeSchemas, | |
| 550 | − }), | |
| 551 | − ]); | |
| 552 | − | |
| 553 | − return [...recipeTools.map((tool) => tool.descriptor), ...executor] | |
| 554 | − .sort((left, right) => left.path.localeCompare(right.path)) | |
| 555 | − .slice(0, limit); | |
| 556 | − }), input.runtimeLocalWorkspace), | |
| 557 | − | |
| 558 | − getToolByPath: ({ path, includeSchemas }) => | |
| 559 | − provideRuntimeLocalWorkspace(Effect.gen(function* () { | |
| 560 | − const executor = yield* input.executorCatalog.getToolByPath({ | |
| 561 | − path, | |
| 562 | − includeSchemas, | |
| 563 | − }); | |
| 564 | − if (executor) { | |
| 565 | − return executor; | |
| 566 | − } | |
| 632 | + }); | |
| 633 | + return recipeTool?.descriptor ?? null; | |
| 634 | + }), | |
| 635 | + input.runtimeLocalWorkspace, | |
| 636 | + ), | |
| 567 | 637 | |
| 568 | − const recipeTool = yield* loadWorkspaceRecipeToolByPath({ | |
| 638 | + getSchemaBundle: ({ id }) => | |
| 639 | + provideRuntimeLocalWorkspace( | |
| 640 | + loadWorkspaceSchemaBundle({ | |
| 569 | 641 | rows: input.rows, |
| 570 | 642 | workspaceId: input.workspaceId, |
| 571 | − accountId: input.accountId, | |
| 572 | − path, | |
| 573 | − includeSchemas, | |
| 574 | − }); | |
| 575 | − return recipeTool?.descriptor ?? null; | |
| 576 | − }), input.runtimeLocalWorkspace), | |
| 577 | − | |
| 578 | − getSchemaBundle: ({ id }) => | |
| 579 | − provideRuntimeLocalWorkspace(loadWorkspaceSchemaBundle({ | |
| 580 | − rows: input.rows, | |
| 581 | − workspaceId: input.workspaceId, | |
| 582 | − id: SourceRecipeSchemaBundleIdSchema.make(id), | |
| 583 | − }), input.runtimeLocalWorkspace), | |
| 643 | + id: SourceRecipeSchemaBundleIdSchema.make(id), | |
| 644 | + }), | |
| 645 | + input.runtimeLocalWorkspace, | |
| 646 | + ), | |
| 584 | 647 | |
| 585 | 648 | searchTools: ({ query, namespace, limit }) => |
| 586 | − provideRuntimeLocalWorkspace(Effect.gen(function* () { | |
| 587 | − const queryTokens = tokenize(query); | |
| 588 | − const [recipeTools, executor] = yield* Effect.all([ | |
| 589 | − loadWorkspaceRecipeTools({ | |
| 590 | − rows: input.rows, | |
| 591 | − workspaceId: input.workspaceId, | |
| 592 | − accountId: input.accountId, | |
| 593 | − includeSchemas: false, | |
| 594 | − }).pipe( | |
| 595 | − Effect.map((tools) => | |
| 596 | − tools.filter( | |
| 597 | − (tool) => !namespace || tool.searchNamespace === namespace, | |
| 649 | + provideRuntimeLocalWorkspace( | |
| 650 | + Effect.gen(function* () { | |
| 651 | + const queryTokens = tokenize(query); | |
| 652 | + const [recipeTools, authored] = yield* Effect.all([ | |
| 653 | + loadWorkspaceRecipeTools({ | |
| 654 | + rows: input.rows, | |
| 655 | + workspaceId: input.workspaceId, | |
| 656 | + accountId: input.accountId, | |
| 657 | + includeSchemas: false, | |
| 658 | + }).pipe( | |
| 659 | + Effect.map((tools) => | |
| 660 | + tools.filter( | |
| 661 | + (tool) => !namespace || tool.searchNamespace === namespace, | |
| 662 | + ), | |
| 598 | 663 | ), |
| 599 | 664 | ), |
| 600 | − ), | |
| 601 | − input.executorCatalog.searchTools({ | |
| 602 | − query, | |
| 603 | − ...(namespace !== undefined ? { namespace } : {}), | |
| 604 | − limit, | |
| 605 | − }), | |
| 606 | − ]); | |
| 607 | − | |
| 608 | − const recipeHits: SearchHit[] = recipeTools | |
| 609 | − .map((tool) => ({ | |
| 610 | − path: asToolPath(tool.path), | |
| 611 | − score: scoreRecipeTool(queryTokens, tool), | |
| 612 | − })) | |
| 613 | − .filter((hit) => hit.score > 0); | |
| 614 | − | |
| 615 | − return [...recipeHits, ...executor] | |
| 616 | − .sort( | |
| 617 | − (left, right) => | |
| 618 | − right.score - left.score || left.path.localeCompare(right.path), | |
| 619 | − ) | |
| 620 | − .slice(0, limit); | |
| 621 | − }), input.runtimeLocalWorkspace), | |
| 665 | + input.authoredCatalog.searchTools({ | |
| 666 | + query, | |
| 667 | + ...(namespace !== undefined ? { namespace } : {}), | |
| 668 | + limit, | |
| 669 | + }), | |
| 670 | + ]); | |
| 671 | + | |
| 672 | + const recipeHits: SearchHit[] = recipeTools | |
| 673 | + .map((tool) => ({ | |
| 674 | + path: asToolPath(tool.path), | |
| 675 | + score: scoreRecipeTool(queryTokens, tool), | |
| 676 | + })) | |
| 677 | + .filter((hit) => hit.score > 0); | |
| 678 | + | |
| 679 | + return dedupeSearchHits(authored, recipeHits) | |
| 680 | + .sort( | |
| 681 | + (left, right) => | |
| 682 | + right.score - left.score || left.path.localeCompare(right.path), | |
| 683 | + ) | |
| 684 | + .slice(0, limit); | |
| 685 | + }), | |
| 686 | + input.runtimeLocalWorkspace, | |
| 687 | + ), | |
| 622 | 688 | }); |
| 623 | 689 | |
| 624 | 690 | const createWorkspaceToolInvoker = (input: { |
| @@ -628,6 +694,7 @@ const createWorkspaceToolInvoker = (input: { · 3 unmodified lines | ||
| 628 | 694 | resolveSecretMaterial: ResolveSecretMaterial; |
| 629 | 695 | sourceAuthService: RuntimeSourceAuthService; |
| 630 | 696 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null; |
| 697 | + localToolRuntime: LocalToolRuntime; | |
| 631 | 698 | onElicitation?: Parameters< |
| 632 | 699 | typeof makeToolInvokerFromTools |
| 633 | 700 | >[0]["onElicitation"]; |
| @@ -641,25 +708,34 @@ const createWorkspaceToolInvoker = (input: { · 7 unmodified lines | ||
| 641 | 708 | sourceAuthService: input.sourceAuthService, |
| 642 | 709 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, |
| 643 | 710 | }); |
| 644 | − const executorCatalog = createToolCatalogFromTools({ | |
| 645 | − tools: executorTools, | |
| 711 | + const authoredTools = mergeToolMaps([ | |
| 712 | + executorTools, | |
| 713 | + input.localToolRuntime.tools, | |
| 714 | + ]); | |
| 715 | + const authoredCatalog = createToolCatalogFromTools({ | |
| 716 | + tools: authoredTools, | |
| 646 | 717 | }); |
| 647 | 718 | const catalog = createWorkspaceToolCatalog({ |
| 648 | 719 | workspaceId: input.workspaceId, |
| 649 | 720 | accountId: input.accountId, |
| 650 | 721 | rows: input.rows, |
| 651 | − executorCatalog, | |
| 722 | + authoredCatalog, | |
| 652 | 723 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, |
| 653 | 724 | }); |
| 654 | 725 | const systemTools = createSystemToolMap({ catalog }); |
| 655 | 726 | const systemToolPaths = new Set(Object.keys(systemTools)); |
| 656 | − const executorToolPaths = new Set(Object.keys(executorTools)); | |
| 727 | + const authoredToolPaths = new Set(Object.keys(authoredTools)); | |
| 728 | + for (const systemToolPath of systemToolPaths) { | |
| 729 | + if (authoredToolPaths.has(systemToolPath)) { | |
| 730 | + throw new Error(`Tool path conflict: ${systemToolPath}`); | |
| 731 | + } | |
| 732 | + } | |
| 657 | 733 | const systemInvoker = makeToolInvokerFromTools({ |
| 658 | 734 | tools: systemTools, |
| 659 | 735 | onElicitation: input.onElicitation, |
| 660 | 736 | }); |
| 661 | − const executorInvoker = makeToolInvokerFromTools({ | |
| 662 | − tools: executorTools, | |
| 737 | + const authoredInvoker = makeToolInvokerFromTools({ | |
| 738 | + tools: authoredTools, | |
| 663 | 739 | onElicitation: input.onElicitation, |
| 664 | 740 | }); |
| 665 | 741 | |
| @@ -668,54 +744,60 @@ const createWorkspaceToolInvoker = (input: { · 2 unmodified lines | ||
| 668 | 744 | args: unknown; |
| 669 | 745 | context?: Record<string, unknown>; |
| 670 | 746 | }) => |
| 671 | − provideRuntimeLocalWorkspace(Effect.gen(function* () { | |
| 672 | − const recipeTool = yield* loadWorkspaceRecipeToolByPath({ | |
| 673 | − rows: input.rows, | |
| 674 | − workspaceId: input.workspaceId, | |
| 675 | − accountId: input.accountId, | |
| 676 | − path: invocation.path, | |
| 677 | − includeSchemas: false, | |
| 678 | − }); | |
| 679 | − if (!recipeTool) { | |
| 680 | − return yield* Effect.fail( | |
| 681 | − new Error(`Unknown tool path: ${invocation.path}`), | |
| 682 | − ); | |
| 683 | − } | |
| 747 | + provideRuntimeLocalWorkspace( | |
| 748 | + Effect.gen(function* () { | |
| 749 | + const recipeTool = yield* loadWorkspaceRecipeToolByPath({ | |
| 750 | + rows: input.rows, | |
| 751 | + workspaceId: input.workspaceId, | |
| 752 | + accountId: input.accountId, | |
| 753 | + path: invocation.path, | |
| 754 | + includeSchemas: false, | |
| 755 | + }); | |
| 756 | + if (!recipeTool) { | |
| 757 | + return yield* Effect.fail( | |
| 758 | + new Error(`Unknown tool path: ${invocation.path}`), | |
| 759 | + ); | |
| 760 | + } | |
| 684 | 761 | |
| 685 | − yield* authorizePersistedToolInvocation({ | |
| 686 | − rows: input.rows, | |
| 687 | − workspaceId: input.workspaceId, | |
| 688 | − accountId: input.accountId, | |
| 689 | − descriptor: toInvocationDescriptorFromRecipeTool({ tool: recipeTool }), | |
| 690 | − args: invocation.args, | |
| 691 | − source: recipeTool.source, | |
| 692 | − context: invocation.context, | |
| 693 | − onElicitation: input.onElicitation, | |
| 694 | − }); | |
| 762 | + yield* authorizePersistedToolInvocation({ | |
| 763 | + rows: input.rows, | |
| 764 | + workspaceId: input.workspaceId, | |
| 765 | + accountId: input.accountId, | |
| 766 | + descriptor: toInvocationDescriptorFromRecipeTool({ | |
| 767 | + tool: recipeTool, | |
| 768 | + }), | |
| 769 | + args: invocation.args, | |
| 770 | + source: recipeTool.source, | |
| 771 | + context: invocation.context, | |
| 772 | + onElicitation: input.onElicitation, | |
| 773 | + }); | |
| 695 | 774 | |
| 696 | − const auth = yield* resolveSourceAuthMaterial({ | |
| 697 | − rows: input.rows, | |
| 698 | − source: recipeTool.source, | |
| 699 | − actorAccountId: input.accountId, | |
| 700 | − resolveSecretMaterial: input.resolveSecretMaterial, | |
| 701 | − context: toSecretResolutionContext(invocation.context), | |
| 702 | − }); | |
| 703 | − const schemaBundle = recipeTool.schemaBundleId | |
| 704 | − ? yield* loadWorkspaceSchemaBundle({ | |
| 705 | − rows: input.rows, | |
| 706 | − workspaceId: input.workspaceId, | |
| 707 | − id: SourceRecipeSchemaBundleIdSchema.make(recipeTool.schemaBundleId), | |
| 708 | − }) | |
| 709 | − : null; | |
| 710 | − const recipe = yield* loadSourceWithRecipe({ | |
| 711 | − rows: input.rows, | |
| 712 | − workspaceId: input.workspaceId, | |
| 713 | − sourceId: recipeTool.source.id, | |
| 714 | − actorAccountId: input.accountId, | |
| 715 | − }); | |
| 775 | + const auth = yield* resolveSourceAuthMaterial({ | |
| 776 | + rows: input.rows, | |
| 777 | + source: recipeTool.source, | |
| 778 | + actorAccountId: input.accountId, | |
| 779 | + resolveSecretMaterial: input.resolveSecretMaterial, | |
| 780 | + context: toSecretResolutionContext(invocation.context), | |
| 781 | + }); | |
| 782 | + const schemaBundle = recipeTool.schemaBundleId | |
| 783 | + ? yield* loadWorkspaceSchemaBundle({ | |
| 784 | + rows: input.rows, | |
| 785 | + workspaceId: input.workspaceId, | |
| 786 | + id: SourceRecipeSchemaBundleIdSchema.make( | |
| 787 | + recipeTool.schemaBundleId, | |
| 788 | + ), | |
| 789 | + }) | |
| 790 | + : null; | |
| 791 | + const recipe = yield* loadSourceWithRecipe({ | |
| 792 | + rows: input.rows, | |
| 793 | + workspaceId: input.workspaceId, | |
| 794 | + sourceId: recipeTool.source.id, | |
| 795 | + actorAccountId: input.accountId, | |
| 796 | + }); | |
| 716 | 797 | |
| 717 | − return yield* getSourceAdapterForOperation(recipeTool.operation) | |
| 718 | − .invokePersistedTool({ | |
| 798 | + return yield* getSourceAdapterForOperation( | |
| 799 | + recipeTool.operation, | |
| 800 | + ).invokePersistedTool({ | |
| 719 | 801 | rows: input.rows, |
| 720 | 802 | workspaceId: input.workspaceId, |
| 721 | 803 | accountId: input.accountId, |
| @@ -729,7 +811,9 @@ const createWorkspaceToolInvoker = (input: { · 7 unmodified lines | ||
| 729 | 811 | context: invocation.context, |
| 730 | 812 | onElicitation: input.onElicitation, |
| 731 | 813 | }); |
| 732 | − }), input.runtimeLocalWorkspace); | |
| 814 | + }), | |
| 815 | + input.runtimeLocalWorkspace, | |
| 816 | + ); | |
| 733 | 817 | |
| 734 | 818 | return { |
| 735 | 819 | catalog, |
| @@ -738,8 +822,8 @@ const createWorkspaceToolInvoker = (input: { · 2 unmodified lines | ||
| 738 | 822 | provideRuntimeLocalWorkspace( |
| 739 | 823 | systemToolPaths.has(path) |
| 740 | 824 | ? systemInvoker.invoke({ path, args, context }) |
| 741 | − : executorToolPaths.has(path) | |
| 742 | − ? executorInvoker.invoke({ path, args, context }) | |
| 825 | + : authoredToolPaths.has(path) | |
| 826 | + ? authoredInvoker.invoke({ path, args, context }) | |
| 743 | 827 | : invokePersistedTool({ path, args, context }), |
| 744 | 828 | input.runtimeLocalWorkspace, |
| 745 | 829 | ), |
| @@ -761,6 +845,15 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 15 unmodified lines | ||
| 761 | 845 | return ({ workspaceId, accountId, onElicitation }) => |
| 762 | 846 | Effect.gen(function* () { |
| 763 | 847 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); |
| 848 | + const localToolRuntime = | |
| 849 | + runtimeLocalWorkspace === null | |
| 850 | + ? { | |
| 851 | + tools: {}, | |
| 852 | + catalog: createToolCatalogFromTools({ tools: {} }), | |
| 853 | + toolInvoker: makeToolInvokerFromTools({ tools: {} }), | |
| 854 | + toolPaths: new Set<string>(), | |
| 855 | + } | |
| 856 | + : yield* loadLocalToolRuntime(runtimeLocalWorkspace.context); | |
| 764 | 857 | const { catalog, toolInvoker } = createWorkspaceToolInvoker({ |
| 765 | 858 | workspaceId, |
| 766 | 859 | accountId, |
| @@ -768,6 +861,7 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 1 unmodified line | ||
| 768 | 861 | resolveSecretMaterial, |
| 769 | 862 | sourceAuthService: input.sourceAuthService, |
| 770 | 863 | runtimeLocalWorkspace, |
| 864 | + localToolRuntime, | |
| 771 | 865 | onElicitation, |
| 772 | 866 | }); |
| 773 | 867 | |