Unified · Split
| @@ -37,7 +37,8 @@ · 36 unmodified lines | |||
| 37 | "graphql": "^16.12.0", | 37 | "graphql": "^16.12.0", |
| 38 | "jsonc-parser": "3.3.1", | 38 | "jsonc-parser": "3.3.1", |
| 39 | "postgres": "^3.4.7", | 39 | "postgres": "^3.4.7", |
| 40 | "prettier": "^3.8.1" | 40 | "prettier": "^3.8.1", |
| 41 | "typescript": "^5.9.3" | ||
| 41 | }, | 42 | }, |
| 42 | "devDependencies": { | 43 | "devDependencies": { |
| 43 | "@effect/vitest": "catalog:", | 44 | "@effect/vitest": "catalog:", |
| @@ -4,9 +4,7 @@ import * as Exit from "effect/Exit"; · 3 unmodified lines | |||
| 4 | import * as Layer from "effect/Layer"; | 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Scope from "effect/Scope"; | 5 | import * as Scope from "effect/Scope"; |
| 6 | 6 | ||
| 7 | import { | 7 | import { type ControlPlaneApiRuntimeContext } from "#api"; |
| 8 | type ControlPlaneApiRuntimeContext, | ||
| 9 | } from "#api"; | ||
| 10 | import { | 8 | import { |
| 11 | SqlControlPlanePersistenceLive, | 9 | SqlControlPlanePersistenceLive, |
| 12 | SqlControlPlanePersistenceService, | 10 | SqlControlPlanePersistenceService, |
| @@ -18,9 +16,7 @@ import { · 5 unmodified lines | |||
| 18 | } from "#persistence"; | 16 | } from "#persistence"; |
| 19 | import type { LocalInstallation } from "#schema"; | 17 | import type { LocalInstallation } from "#schema"; |
| 20 | 18 | ||
| 21 | import { | 19 | import { type ResolveExecutionEnvironment } from "./execution-state"; |
| 22 | type ResolveExecutionEnvironment, | ||
| 23 | } from "./execution-state"; | ||
| 24 | import { | 20 | import { |
| 25 | createLiveExecutionManager, | 21 | createLiveExecutionManager, |
| 26 | LiveExecutionManagerLive, | 22 | LiveExecutionManagerLive, |
| @@ -33,10 +29,7 @@ import { · 6 unmodified lines | |||
| 33 | } from "./local-config"; | 29 | } from "./local-config"; |
| 34 | import { RuntimeLocalWorkspaceService } from "./local-runtime-context"; | 30 | import { RuntimeLocalWorkspaceService } from "./local-runtime-context"; |
| 35 | import { synchronizeLocalWorkspaceState } from "./local-workspace-sync"; | 31 | import { synchronizeLocalWorkspaceState } from "./local-workspace-sync"; |
| 36 | import { | 32 | import { ControlPlaneStore, ControlPlaneStoreLive } from "./store"; |
| 37 | ControlPlaneStore, | ||
| 38 | ControlPlaneStoreLive, | ||
| 39 | } from "./store"; | ||
| 40 | import { | 33 | import { |
| 41 | createRuntimeSourceAuthService, | 34 | createRuntimeSourceAuthService, |
| 42 | RuntimeSourceAuthServiceLive, | 35 | RuntimeSourceAuthServiceLive, |
| @@ -56,6 +49,7 @@ export * from "./live-execution"; · 13 unmodified lines | |||
| 56 | export * from "./local-config"; | 49 | export * from "./local-config"; |
| 57 | export * from "./local-installation"; | 50 | export * from "./local-installation"; |
| 58 | export * from "./local-source-artifacts"; | 51 | export * from "./local-source-artifacts"; |
| 52 | export * from "./local-tools"; | ||
| 59 | export * from "./schema-type-signature"; | 53 | export * from "./schema-type-signature"; |
| 60 | export * from "./source-auth-service"; | 54 | export * from "./source-auth-service"; |
| 61 | export * from "./secret-material-providers"; | 55 | export * from "./secret-material-providers"; |
| @@ -106,7 +100,7 @@ const runtimeContextTags = [ · 44 unmodified lines | |||
| 106 | 100 | ||
| 107 | const createRuntimeLayerFromContext = ( | 101 | const createRuntimeLayerFromContext = ( |
| 108 | context: Context.Context< | 102 | context: Context.Context< |
| 109 | ControlPlaneStore | 103 | | ControlPlaneStore |
| 110 | | LiveExecutionManagerService | 104 | | LiveExecutionManagerService |
| 111 | | RuntimeLocalWorkspaceService | 105 | | RuntimeLocalWorkspaceService |
| 112 | | RuntimeSourceAuthServiceTag | 106 | | RuntimeSourceAuthServiceTag |
| @@ -116,7 +110,7 @@ const createRuntimeLayerFromContext = ( · 3 unmodified lines | |||
| 116 | const runtimeContext = context.pipe( | 110 | const runtimeContext = context.pipe( |
| 117 | Context.pick(...runtimeContextTags), | 111 | Context.pick(...runtimeContextTags), |
| 118 | ) as Context.Context< | 112 | ) as Context.Context< |
| 119 | ControlPlaneStore | 113 | | ControlPlaneStore |
| 120 | | LiveExecutionManagerService | 114 | | LiveExecutionManagerService |
| 121 | | RuntimeLocalWorkspaceService | 115 | | RuntimeLocalWorkspaceService |
| 122 | | RuntimeSourceAuthServiceTag | 116 | | RuntimeSourceAuthServiceTag |
| @@ -132,15 +126,11 @@ export const createRuntimeControlPlaneLayer = ( · 9 unmodified lines | |||
| 132 | const liveExecutionManagerLayer = LiveExecutionManagerLive; | 126 | const liveExecutionManagerLayer = LiveExecutionManagerLive; |
| 133 | const sourceAuthLayer = RuntimeSourceAuthServiceLive({ | 127 | const sourceAuthLayer = RuntimeSourceAuthServiceLive({ |
| 134 | getLocalServerBaseUrl: options.getLocalServerBaseUrl, | 128 | getLocalServerBaseUrl: options.getLocalServerBaseUrl, |
| 135 | }).pipe( | 129 | }).pipe(Layer.provide(liveExecutionManagerLayer)); |
| 136 | Layer.provide(liveExecutionManagerLayer), | ||
| 137 | ); | ||
| 138 | const executionResolverLayer = RuntimeExecutionResolverLive({ | 130 | const executionResolverLayer = RuntimeExecutionResolverLive({ |
| 139 | executionResolver: options.executionResolver, | 131 | executionResolver: options.executionResolver, |
| 140 | resolveSecretMaterial: options.resolveSecretMaterial, | 132 | resolveSecretMaterial: options.resolveSecretMaterial, |
| 141 | }).pipe( | 133 | }).pipe(Layer.provide(sourceAuthLayer)); |
| 142 | Layer.provide(sourceAuthLayer), | ||
| 143 | ); | ||
| 144 | 134 | ||
| 145 | return Layer.mergeAll( | 135 | return Layer.mergeAll( |
| 146 | ControlPlaneStoreLive, | 136 | ControlPlaneStoreLive, |
| @@ -157,8 +147,8 @@ export type ControlPlaneRuntime = { · 10 unmodified lines | |||
| 157 | close: () => Promise<void>; | 147 | close: () => Promise<void>; |
| 158 | }; | 148 | }; |
| 159 | 149 | ||
| 160 | export type CreateControlPlaneRuntimeOptions = CreateSqlRuntimeOptions | 150 | export type CreateControlPlaneRuntimeOptions = CreateSqlRuntimeOptions & |
| 161 | & RuntimeControlPlaneOptions; | 151 | RuntimeControlPlaneOptions; |
| 162 | 152 | ||
| 163 | export const createControlPlaneRuntime = ( | 153 | export const createControlPlaneRuntime = ( |
| 164 | options: CreateControlPlaneRuntimeOptions, | 154 | options: CreateControlPlaneRuntimeOptions, |
| @@ -168,14 +158,19 @@ export const createControlPlaneRuntime = ( · 3 unmodified lines | |||
| 168 | const persistenceAndRowsLayer = SqlControlPlaneRowsLive.pipe( | 158 | const persistenceAndRowsLayer = SqlControlPlaneRowsLive.pipe( |
| 169 | Layer.provideMerge(SqlControlPlanePersistenceLive(options)), | 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 | Effect.catchAll((error) => | 165 | Effect.catchAll((error) => |
| 173 | closeScope(scope).pipe( | 166 | closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), |
| 174 | Effect.zipRight(Effect.fail(error)), | 167 | ), |
| 175 | )), | ||
| 176 | ); | 168 | ); |
| 177 | 169 | ||
| 178 | const persistence = Context.get(baseContext, SqlControlPlanePersistenceService); | 170 | const persistence = Context.get( |
| 171 | baseContext, | ||
| 172 | SqlControlPlanePersistenceService, | ||
| 173 | ); | ||
| 179 | const rows = Context.get(baseContext, SqlControlPlaneRowsService); | 174 | const rows = Context.get(baseContext, SqlControlPlaneRowsService); |
| 180 | 175 | ||
| 181 | const localWorkspaceContext = yield* resolveLocalWorkspaceContext({ | 176 | const localWorkspaceContext = yield* resolveLocalWorkspaceContext({ |
| @@ -183,9 +178,8 @@ export const createControlPlaneRuntime = ( · 1 unmodified line | |||
| 183 | }).pipe( | 178 | }).pipe( |
| 184 | Effect.mapError(toLocalRuntimeBootstrapError), | 179 | Effect.mapError(toLocalRuntimeBootstrapError), |
| 185 | Effect.catchAll((error) => | 180 | Effect.catchAll((error) => |
| 186 | closeScope(scope).pipe( | 181 | closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), |
| 187 | Effect.zipRight(Effect.fail(error)), | 182 | ), |
| 188 | )), | ||
| 189 | ); | 183 | ); |
| 190 | 184 | ||
| 191 | const localInstallation = yield* getOrProvisionLocalInstallation({ | 185 | const localInstallation = yield* getOrProvisionLocalInstallation({ |
| @@ -193,17 +187,17 @@ export const createControlPlaneRuntime = ( · 1 unmodified line | |||
| 193 | }).pipe( | 187 | }).pipe( |
| 194 | Effect.mapError(toLocalRuntimeBootstrapError), | 188 | Effect.mapError(toLocalRuntimeBootstrapError), |
| 195 | Effect.catchAll((error) => | 189 | Effect.catchAll((error) => |
| 196 | closeScope(scope).pipe( | 190 | closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), |
| 197 | Effect.zipRight(Effect.fail(error)), | 191 | ), |
| 198 | )), | ||
| 199 | ); | 192 | ); |
| 200 | 193 | ||
| 201 | const loadedLocalConfig = yield* loadLocalExecutorConfig(localWorkspaceContext).pipe( | 194 | const loadedLocalConfig = yield* loadLocalExecutorConfig( |
| 195 | localWorkspaceContext, | ||
| 196 | ).pipe( | ||
| 202 | Effect.mapError(toLocalRuntimeBootstrapError), | 197 | Effect.mapError(toLocalRuntimeBootstrapError), |
| 203 | Effect.catchAll((error) => | 198 | Effect.catchAll((error) => |
| 204 | closeScope(scope).pipe( | 199 | closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), |
| 205 | Effect.zipRight(Effect.fail(error)), | 200 | ), |
| 206 | )), | ||
| 207 | ); | 201 | ); |
| 208 | 202 | ||
| 209 | const effectiveLocalConfig = yield* synchronizeLocalWorkspaceState({ | 203 | const effectiveLocalConfig = yield* synchronizeLocalWorkspaceState({ |
| @@ -212,14 +206,13 @@ export const createControlPlaneRuntime = ( · 2 unmodified lines | |||
| 212 | }).pipe( | 206 | }).pipe( |
| 213 | Effect.mapError(toLocalRuntimeBootstrapError), | 207 | Effect.mapError(toLocalRuntimeBootstrapError), |
| 214 | Effect.catchAll((error) => | 208 | Effect.catchAll((error) => |
| 215 | closeScope(scope).pipe( | 209 | closeScope(scope).pipe(Effect.zipRight(Effect.fail(error))), |
| 216 | Effect.zipRight(Effect.fail(error)), | 210 | ), |
| 217 | )), | ||
| 218 | ); | 211 | ); |
| 219 | 212 | ||
| 220 | const resolveSecretMaterial = | 213 | const resolveSecretMaterial = |
| 221 | options.resolveSecretMaterial | 214 | options.resolveSecretMaterial ?? |
| 222 | ?? createDefaultSecretMaterialResolver({ | 215 | createDefaultSecretMaterialResolver({ |
| 223 | rows, | 216 | rows, |
| 224 | localConfig: effectiveLocalConfig, | 217 | localConfig: effectiveLocalConfig, |
| 225 | workspaceRoot: localWorkspaceContext.workspaceRoot, | 218 | workspaceRoot: localWorkspaceContext.workspaceRoot, |
| @@ -245,8 +238,8 @@ export const createControlPlaneRuntime = ( · 19 unmodified lines | |||
| 245 | }, | 238 | }, |
| 246 | }); | 239 | }); |
| 247 | const executionResolver = | 240 | const executionResolver = |
| 248 | options.executionResolver | 241 | options.executionResolver ?? |
| 249 | ?? createWorkspaceExecutionEnvironmentResolver({ | 242 | createWorkspaceExecutionEnvironmentResolver({ |
| 250 | rows, | 243 | rows, |
| 251 | sourceAuthService, | 244 | sourceAuthService, |
| 252 | resolveSecretMaterial, | 245 | resolveSecretMaterial, |
| @@ -275,6 +268,7 @@ export const createControlPlaneRuntime = ( · 22 unmodified lines | |||
| 275 | persistence, | 268 | persistence, |
| 276 | localInstallation, | 269 | localInstallation, |
| 277 | runtimeLayer: concreteRuntimeLayer, | 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 | export const unknownLocalErrorDetails = (cause: unknown): string => | 3 | export const unknownLocalErrorDetails = (cause: unknown): string => |
| 4 | cause instanceof Error ? cause.message : String(cause); | 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 | readonly message: string; | 9 | readonly message: string; |
| 8 | readonly action: string; | 10 | readonly action: string; |
| 9 | readonly path: string; | 11 | readonly path: string; |
| 10 | readonly details: string; | 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 | readonly message: string; | 18 | readonly message: string; |
| 15 | readonly path: string; | 19 | readonly path: string; |
| 16 | readonly details: string; | 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 | readonly message: string; | 26 | readonly message: string; |
| 21 | readonly path: string; | 27 | readonly path: string; |
| 22 | readonly details: string; | 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 | readonly message: string; | 34 | readonly message: string; |
| 27 | readonly path: string; | 35 | readonly path: string; |
| 28 | readonly details: string; | 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 | readonly message: string; | 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 | readonly message: string; | 81 | readonly message: string; |
| 37 | readonly requestedWorkspaceId: string; | 82 | readonly requestedWorkspaceId: string; |
| 38 | readonly activeWorkspaceId: string; | 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 | readonly message: string; | 89 | readonly message: string; |
| 43 | readonly sourceId: string; | 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 | readonly message: string; | 96 | readonly message: string; |
| 48 | readonly sourceId: string; | 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 | readonly message: string; | 103 | readonly message: string; |
| 53 | readonly kind: string; | 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 | import { randomUUID } from "node:crypto"; | 1 | import { randomUUID } from "node:crypto"; |
| 2 | import { mkdtempSync } from "node:fs"; | ||
| 3 | import { createServer } from "node:http"; | 2 | import { createServer } from "node:http"; |
| 4 | import { tmpdir } from "node:os"; | 3 | import { dirname, join } from "node:path"; |
| 5 | import { join } from "node:path"; | ||
| 6 | 4 | ||
| 7 | import { | 5 | import { |
| 6 | FileSystem, | ||
| 8 | HttpApi, | 7 | HttpApi, |
| 9 | HttpApiBuilder, | 8 | HttpApiBuilder, |
| 10 | HttpApiEndpoint, | 9 | HttpApiEndpoint, |
| @@ -12,6 +11,7 @@ import { · 1 unmodified line | |||
| 12 | HttpApiSchema, | 11 | HttpApiSchema, |
| 13 | HttpServer, | 12 | HttpServer, |
| 14 | } from "@effect/platform"; | 13 | } from "@effect/platform"; |
| 14 | import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"; | ||
| 15 | import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; | 15 | import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; |
| 16 | import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; | 16 | import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; |
| 17 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | 17 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
| @@ -80,9 +80,7 @@ import { · 62 unmodified lines | |||
| 80 | loadLocalWorkspaceState, | 80 | loadLocalWorkspaceState, |
| 81 | writeLocalWorkspaceState, | 81 | writeLocalWorkspaceState, |
| 82 | } from "./local-workspace-state"; | 82 | } from "./local-workspace-state"; |
| 83 | import { | 83 | import { materializationFromMcpManifestEntries } from "./source-adapters/mcp"; |
| 84 | materializationFromMcpManifestEntries, | ||
| 85 | } from "./source-adapters/mcp"; | ||
| 86 | import { createWorkspaceExecutionEnvironmentResolver } from "./workspace-execution-environment"; | 84 | import { createWorkspaceExecutionEnvironmentResolver } from "./workspace-execution-environment"; |
| 87 | 85 | ||
| 88 | type CountedMcpServer = { | 86 | type CountedMcpServer = { |
| @@ -116,6 +114,15 @@ const normalizeSearchText = ( · 27 unmodified lines | |||
| 116 | .join(" ") | 114 | .join(" ") |
| 117 | .toLowerCase(); | 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 | const GraphqlUserType = new GraphQLObjectType({ | 126 | const GraphqlUserType = new GraphQLObjectType({ |
| 120 | name: "User", | 127 | name: "User", |
| 121 | fields: { | 128 | fields: { |
| @@ -941,98 +948,98 @@ const persistConnectedGithubOpenApiSource = (input: { · 819 unmodified lines | |||
| 941 | context: input.runtimeLocalWorkspace.context, | 948 | context: input.runtimeLocalWorkspace.context, |
| 942 | sourceId: source.id, | 949 | sourceId: source.id, |
| 943 | artifact: buildLocalSourceArtifact({ | 950 | artifact: buildLocalSourceArtifact({ |
| 944 | source: { | 951 | source: { |
| 945 | ...source, | 952 | ...source, |
| 946 | sourceHash: manifest.sourceHash, | 953 | sourceHash: manifest.sourceHash, |
| 947 | }, | 954 | }, |
| 948 | materialization: { | 955 | materialization: { |
| 949 | manifestJson: JSON.stringify(manifest), | 956 | manifestJson: JSON.stringify(manifest), |
| 950 | manifestHash: manifest.sourceHash, | 957 | manifestHash: manifest.sourceHash, |
| 951 | sourceHash: manifest.sourceHash, | 958 | sourceHash: manifest.sourceHash, |
| 952 | documents: [ | 959 | documents: [ |
| 953 | { | 960 | { |
| 954 | id: `src_recipe_doc_${randomUUID()}`, | 961 | id: `src_recipe_doc_${randomUUID()}`, |
| 955 | recipeRevisionId: "src_recipe_rev_materialization" as never, | 962 | recipeRevisionId: "src_recipe_rev_materialization" as never, |
| 956 | documentKind: "openapi", | 963 | documentKind: "openapi", |
| 957 | documentKey: | 964 | documentKey: |
| 958 | typeof source.binding.specUrl === "string" | 965 | typeof source.binding.specUrl === "string" |
| 959 | ? source.binding.specUrl | 966 | ? source.binding.specUrl |
| 960 | : source.endpoint, | 967 | : source.endpoint, |
| 961 | contentText: openApiDocumentText, | 968 | contentText: openApiDocumentText, |
| 962 | contentHash: manifest.sourceHash, | 969 | contentHash: manifest.sourceHash, |
| 963 | fetchedAt: now, | 970 | fetchedAt: now, |
| 964 | createdAt: now, | 971 | createdAt: now, |
| 965 | updatedAt: now, | 972 | 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 | }), | ||
| 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 | yield* writeLocalWorkspaceState({ | 1044 | yield* writeLocalWorkspaceState({ |
| 1038 | context: input.runtimeLocalWorkspace.context, | 1045 | context: input.runtimeLocalWorkspace.context, |
| @@ -1054,62 +1061,104 @@ const persistConnectedGithubOpenApiSource = (input: { · 15 unmodified lines | |||
| 1054 | return; | 1061 | return; |
| 1055 | }) as Effect.Effect<void, Error, never>; | 1062 | }) as Effect.Effect<void, Error, never>; |
| 1056 | 1063 | ||
| 1057 | const makeRuntimeLocalWorkspaceState = (workspaceId: WorkspaceId): Effect.Effect<RuntimeLocalWorkspaceState, Error, never> => | 1064 | const makeRuntimeLocalWorkspaceState = ( |
| 1058 | Effect.gen(function* () { | 1065 | workspaceId: WorkspaceId, |
| 1059 | const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-workspace-env-")); | 1066 | ): Effect.Effect<RuntimeLocalWorkspaceState, Error, Scope.Scope> => |
| 1060 | const context = yield* resolveLocalWorkspaceContext({ workspaceRoot }); | 1067 | provideNodeFileSystem( |
| 1061 | const loadedConfig = yield* loadLocalExecutorConfig(context); | 1068 | Effect.gen(function* () { |
| 1062 | return { | 1069 | const fs = yield* FileSystem.FileSystem; |
| 1063 | context, | 1070 | const workspaceRoot = yield* fs.makeTempDirectoryScoped({ |
| 1064 | installation: { | 1071 | prefix: "executor-workspace-env-", |
| 1065 | workspaceId, | 1072 | }); |
| 1066 | accountId: AccountIdSchema.make("acc_local_test"), | 1073 | const context = yield* resolveLocalWorkspaceContext({ workspaceRoot }); |
| 1067 | }, | 1074 | const loadedConfig = yield* loadLocalExecutorConfig(context); |
| 1068 | loadedConfig, | 1075 | return { |
| 1069 | } satisfies RuntimeLocalWorkspaceState; | 1076 | context, |
| 1070 | }); | 1077 | installation: { |
| 1071 | 1078 | workspaceId, | |
| 1072 | const makeResolver = ( | 1079 | accountId: AccountIdSchema.make("acc_local_test"), |
| 1073 | persistence: SqlControlPlanePersistence, | 1080 | }, |
| 1074 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState, | 1081 | loadedConfig, |
| 1075 | ) => | 1082 | } satisfies RuntimeLocalWorkspaceState; |
| 1076 | ((input: Parameters<ReturnType<typeof createWorkspaceExecutionEnvironmentResolver>>[0]) => | 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 | createWorkspaceExecutionEnvironmentResolver({ | 1114 | createWorkspaceExecutionEnvironmentResolver({ |
| 1078 | rows: persistence.rows, | 1115 | rows: persistence.rows, |
| 1079 | sourceAuthService: { | 1116 | sourceAuthService: { |
| 1080 | getLocalServerBaseUrl: () => null, | 1117 | getLocalServerBaseUrl: () => null, |
| 1081 | storeSecretMaterial: () => | 1118 | storeSecretMaterial: () => |
| 1082 | Effect.fail(new Error("not implemented in test")), | 1119 | Effect.fail(new Error("not implemented in test")), |
| 1083 | getSourceById: () => Effect.fail(new Error("not implemented in test")), | 1120 | getSourceById: () => Effect.fail(new Error("not implemented in test")), |
| 1084 | addExecutorSource: () => | 1121 | addExecutorSource: () => |
| 1085 | Effect.fail(new Error("not implemented in test")), | 1122 | Effect.fail(new Error("not implemented in test")), |
| 1086 | connectMcpSource: () => Effect.fail(new Error("not implemented in test")), | 1123 | connectMcpSource: () => |
| 1087 | startSourceOAuthSession: () => | 1124 | Effect.fail(new Error("not implemented in test")), |
| 1088 | Effect.fail(new Error("not implemented in test")), | 1125 | startSourceOAuthSession: () => |
| 1089 | completeSourceOAuthSession: () => | 1126 | Effect.fail(new Error("not implemented in test")), |
| 1090 | Effect.fail(new Error("not implemented in test")), | 1127 | completeSourceOAuthSession: () => |
| 1091 | completeSourceCredentialSetup: () => | 1128 | Effect.fail(new Error("not implemented in test")), |
| 1092 | Effect.fail(new Error("not implemented in test")), | 1129 | completeSourceCredentialSetup: () => |
| 1093 | } as RuntimeSourceAuthService, | 1130 | Effect.fail(new Error("not implemented in test")), |
| 1094 | })(input).pipe( | 1131 | } as RuntimeSourceAuthService, |
| 1095 | Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace), | 1132 | })(input).pipe( |
| 1096 | )); | 1133 | Effect.provideService( |
| 1097 | 1134 | RuntimeLocalWorkspaceService, | |
| 1098 | const makeResolverWithLiveSourceAuth = ( | 1135 | runtimeLocalWorkspace, |
| 1099 | persistence: SqlControlPlanePersistence, | 1136 | ), |
| 1100 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState, | 1137 | ); |
| 1101 | ) => | 1138 | |
| 1102 | ((input: Parameters<ReturnType<typeof createWorkspaceExecutionEnvironmentResolver>>[0]) => | 1139 | const makeResolverWithLiveSourceAuth = |
| 1140 | ( | ||
| 1141 | persistence: SqlControlPlanePersistence, | ||
| 1142 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState, | ||
| 1143 | ) => | ||
| 1144 | ( | ||
| 1145 | input: Parameters< | ||
| 1146 | ReturnType<typeof createWorkspaceExecutionEnvironmentResolver> | ||
| 1147 | >[0], | ||
| 1148 | ) => | ||
| 1103 | createWorkspaceExecutionEnvironmentResolver({ | 1149 | createWorkspaceExecutionEnvironmentResolver({ |
| 1104 | rows: persistence.rows, | ||
| 1105 | sourceAuthService: createRuntimeSourceAuthService({ | ||
| 1106 | rows: persistence.rows, | 1150 | rows: persistence.rows, |
| 1107 | liveExecutionManager: createLiveExecutionManager(), | 1151 | sourceAuthService: createRuntimeSourceAuthService({ |
| 1108 | getLocalServerBaseUrl: () => "http://127.0.0.1:8788", | 1152 | rows: persistence.rows, |
| 1109 | }), | 1153 | liveExecutionManager: createLiveExecutionManager(), |
| 1110 | })(input).pipe( | 1154 | getLocalServerBaseUrl: () => "http://127.0.0.1:8788", |
| 1111 | Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace), | 1155 | }), |
| 1112 | )); | 1156 | })(input).pipe( |
| 1157 | Effect.provideService( | ||
| 1158 | RuntimeLocalWorkspaceService, | ||
| 1159 | runtimeLocalWorkspace, | ||
| 1160 | ), | ||
| 1161 | ); | ||
| 1113 | 1162 | ||
| 1114 | describe("workspace-execution-environment", () => { | 1163 | describe("workspace-execution-environment", () => { |
| 1115 | it.scoped("discovers persisted tools without live MCP listing", () => | 1164 | it.scoped("discovers persisted tools without live MCP listing", () => |
| @@ -1118,7 +1167,8 @@ describe("workspace-execution-environment", () => { · 2 unmodified lines | |||
| 1118 | const persistence = yield* makePersistence; | 1167 | const persistence = yield* makePersistence; |
| 1119 | const workspaceId = WorkspaceIdSchema.make("ws_catalog"); | 1168 | const workspaceId = WorkspaceIdSchema.make("ws_catalog"); |
| 1120 | const sourceId = SourceIdSchema.make("src_catalog"); | 1169 | const sourceId = SourceIdSchema.make("src_catalog"); |
| 1121 | const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | 1170 | const runtimeLocalWorkspace = |
| 1171 | yield* makeRuntimeLocalWorkspaceState(workspaceId); | ||
| 1122 | yield* persistConnectedEchoTool({ | 1172 | yield* persistConnectedEchoTool({ |
| 1123 | runtimeLocalWorkspace, | 1173 | runtimeLocalWorkspace, |
| 1124 | endpoint: server.endpoint, | 1174 | endpoint: server.endpoint, |
| @@ -1132,7 +1182,10 @@ describe("workspace-execution-environment", () => { · 7 unmodified lines | |||
| 1132 | cause instanceof Error ? cause : new Error(String(cause)), | 1182 | cause instanceof Error ? cause : new Error(String(cause)), |
| 1133 | }).pipe(Effect.orDie); | 1183 | }).pipe(Effect.orDie); |
| 1134 | 1184 | ||
| 1135 | const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | 1185 | const resolveEnvironment = makeResolver( |
| 1186 | persistence, | ||
| 1187 | runtimeLocalWorkspace, | ||
| 1188 | ); | ||
| 1136 | const environment = yield* resolveEnvironment({ | 1189 | const environment = yield* resolveEnvironment({ |
| 1137 | workspaceId, | 1190 | workspaceId, |
| 1138 | accountId: AccountIdSchema.make("acc_catalog"), | 1191 | accountId: AccountIdSchema.make("acc_catalog"), |
| @@ -1165,14 +1218,18 @@ describe("workspace-execution-environment", () => { · 26 unmodified lines | |||
| 1165 | const persistence = yield* makePersistence; | 1218 | const persistence = yield* makePersistence; |
| 1166 | const workspaceId = WorkspaceIdSchema.make("ws_github_discovery"); | 1219 | const workspaceId = WorkspaceIdSchema.make("ws_github_discovery"); |
| 1167 | const sourceId = SourceIdSchema.make("src_github_discovery"); | 1220 | const sourceId = SourceIdSchema.make("src_github_discovery"); |
| 1168 | const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | 1221 | const runtimeLocalWorkspace = |
| 1222 | yield* makeRuntimeLocalWorkspaceState(workspaceId); | ||
| 1169 | yield* persistConnectedGithubOpenApiSource({ | 1223 | yield* persistConnectedGithubOpenApiSource({ |
| 1170 | runtimeLocalWorkspace, | 1224 | runtimeLocalWorkspace, |
| 1171 | workspaceId, | 1225 | workspaceId, |
| 1172 | sourceId, | 1226 | sourceId, |
| 1173 | }); | 1227 | }); |
| 1174 | 1228 | ||
| 1175 | const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | 1229 | const resolveEnvironment = makeResolver( |
| 1230 | persistence, | ||
| 1231 | runtimeLocalWorkspace, | ||
| 1232 | ); | ||
| 1176 | const environment = yield* resolveEnvironment({ | 1233 | const environment = yield* resolveEnvironment({ |
| 1177 | workspaceId, | 1234 | workspaceId, |
| 1178 | accountId: AccountIdSchema.make("acc_github_discovery"), | 1235 | accountId: AccountIdSchema.make("acc_github_discovery"), |
| @@ -1219,7 +1276,8 @@ describe("workspace-execution-environment", () => { · 40 unmodified lines | |||
| 1219 | const persistence = yield* makePersistence; | 1276 | const persistence = yield* makePersistence; |
| 1220 | const workspaceId = WorkspaceIdSchema.make("ws_invoke"); | 1277 | const workspaceId = WorkspaceIdSchema.make("ws_invoke"); |
| 1221 | const sourceId = SourceIdSchema.make("src_invoke"); | 1278 | const sourceId = SourceIdSchema.make("src_invoke"); |
| 1222 | const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | 1279 | const runtimeLocalWorkspace = |
| 1280 | yield* makeRuntimeLocalWorkspaceState(workspaceId); | ||
| 1223 | yield* persistConnectedEchoTool({ | 1281 | yield* persistConnectedEchoTool({ |
| 1224 | runtimeLocalWorkspace, | 1282 | runtimeLocalWorkspace, |
| 1225 | endpoint: server.endpoint, | 1283 | endpoint: server.endpoint, |
| @@ -1227,7 +1285,10 @@ describe("workspace-execution-environment", () => { · 1 unmodified line | |||
| 1227 | sourceId, | 1285 | sourceId, |
| 1228 | }); | 1286 | }); |
| 1229 | 1287 | ||
| 1230 | const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | 1288 | const resolveEnvironment = makeResolver( |
| 1289 | persistence, | ||
| 1290 | runtimeLocalWorkspace, | ||
| 1291 | ); | ||
| 1231 | const environment = yield* resolveEnvironment({ | 1292 | const environment = yield* resolveEnvironment({ |
| 1232 | workspaceId, | 1293 | workspaceId, |
| 1233 | accountId: AccountIdSchema.make("acc_invoke"), | 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 | it.scoped( | 1433 | it.scoped( |
| 1253 | "adds an OpenAPI source through executor.sources.add with elicited credentials", | 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 | const specServer = yield* makeOpenApiSpecServer; | 1437 | const specServer = yield* makeOpenApiSpecServer; |
| 1257 | const persistence = yield* makePersistence; | 1438 | const persistence = yield* makePersistence; |
| 1258 | const workspaceId = WorkspaceIdSchema.make("ws_add_openapi"); | 1439 | const workspaceId = WorkspaceIdSchema.make("ws_add_openapi"); |
| 1259 | const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | 1440 | const runtimeLocalWorkspace = |
| 1260 | const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence, runtimeLocalWorkspace); | 1441 | yield* makeRuntimeLocalWorkspaceState(workspaceId); |
| 1442 | const resolveEnvironment = makeResolverWithLiveSourceAuth( | ||
| 1443 | persistence, | ||
| 1444 | runtimeLocalWorkspace, | ||
| 1445 | ); | ||
| 1261 | const now = Date.now(); | 1446 | const now = Date.now(); |
| 1262 | const tokenSecretMaterialId = SecretMaterialIdSchema.make( | 1447 | const tokenSecretMaterialId = SecretMaterialIdSchema.make( |
| 1263 | "sec_test_openapi_bearer", | 1448 | "sec_test_openapi_bearer", |
| @@ -1338,7 +1523,9 @@ describe("workspace-execution-environment", () => { · 74 unmodified lines | |||
| 1338 | encodeURIComponent("exec_add_openapi:executor.sources.add:"), | 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 | expect(loadedConfig.config?.sources?.[added.id]).toMatchObject({ | 1529 | expect(loadedConfig.config?.sources?.[added.id]).toMatchObject({ |
| 1343 | kind: "openapi", | 1530 | kind: "openapi", |
| 1344 | connection: { | 1531 | connection: { |
| @@ -1352,7 +1539,9 @@ describe("workspace-execution-environment", () => { · 7 unmodified lines | |||
| 1352 | expect(artifact?.documents[0]?.contentText).toContain( | 1539 | expect(artifact?.documents[0]?.contentText).toContain( |
| 1353 | '"operationId":"repos.getRepo"', | 1540 | '"operationId":"repos.getRepo"', |
| 1354 | ); | 1541 | ); |
| 1355 | const workspaceState = yield* loadLocalWorkspaceState(runtimeLocalWorkspace.context); | 1542 | const workspaceState = yield* loadLocalWorkspaceState( |
| 1543 | runtimeLocalWorkspace.context, | ||
| 1544 | ); | ||
| 1356 | expect(workspaceState.sources[added.id]?.status).toBe("connected"); | 1545 | expect(workspaceState.sources[added.id]?.status).toBe("connected"); |
| 1357 | 1546 | ||
| 1358 | const freshEnvironment = yield* resolveEnvironment({ | 1547 | const freshEnvironment = yield* resolveEnvironment({ |
| @@ -1399,8 +1588,12 @@ describe("workspace-execution-environment", () => { · 40 unmodified lines | |||
| 1399 | const graphqlServer = yield* makeGraphqlServer; | 1588 | const graphqlServer = yield* makeGraphqlServer; |
| 1400 | const persistence = yield* makePersistence; | 1589 | const persistence = yield* makePersistence; |
| 1401 | const workspaceId = WorkspaceIdSchema.make("ws_add_graphql"); | 1590 | const workspaceId = WorkspaceIdSchema.make("ws_add_graphql"); |
| 1402 | const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | 1591 | const runtimeLocalWorkspace = |
| 1403 | const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence, runtimeLocalWorkspace); | 1592 | yield* makeRuntimeLocalWorkspaceState(workspaceId); |
| 1593 | const resolveEnvironment = makeResolverWithLiveSourceAuth( | ||
| 1594 | persistence, | ||
| 1595 | runtimeLocalWorkspace, | ||
| 1596 | ); | ||
| 1404 | const now = Date.now(); | 1597 | const now = Date.now(); |
| 1405 | const tokenSecretMaterialId = SecretMaterialIdSchema.make( | 1598 | const tokenSecretMaterialId = SecretMaterialIdSchema.make( |
| 1406 | "sec_test_graphql_bearer", | 1599 | "sec_test_graphql_bearer", |
| @@ -1630,7 +1823,8 @@ describe("workspace-execution-environment", () => { · 223 unmodified lines | |||
| 1630 | const persistence = yield* makePersistence; | 1823 | const persistence = yield* makePersistence; |
| 1631 | const workspaceId = WorkspaceIdSchema.make("ws_params_auth"); | 1824 | const workspaceId = WorkspaceIdSchema.make("ws_params_auth"); |
| 1632 | const sourceId = SourceIdSchema.make("src_params_auth"); | 1825 | const sourceId = SourceIdSchema.make("src_params_auth"); |
| 1633 | const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | 1826 | const runtimeLocalWorkspace = |
| 1827 | yield* makeRuntimeLocalWorkspaceState(workspaceId); | ||
| 1634 | 1828 | ||
| 1635 | yield* persistConnectedGithubOpenApiSource({ | 1829 | yield* persistConnectedGithubOpenApiSource({ |
| 1636 | runtimeLocalWorkspace, | 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 | const environment = yield* resolveEnvironment({ | 1844 | const environment = yield* resolveEnvironment({ |
| 1648 | workspaceId, | 1845 | workspaceId, |
| 1649 | accountId: AccountIdSchema.make("acc_params_auth"), | 1846 | accountId: AccountIdSchema.make("acc_params_auth"), |
| @@ -1683,7 +1880,8 @@ describe("workspace-execution-environment", () => { · 33 unmodified lines | |||
| 1683 | "ws_openapi_describe_bundle", | 1880 | "ws_openapi_describe_bundle", |
| 1684 | ); | 1881 | ); |
| 1685 | const sourceId = SourceIdSchema.make("src_openapi_describe_bundle"); | 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 | yield* persistConnectedGithubOpenApiSource({ | 1886 | yield* persistConnectedGithubOpenApiSource({ |
| 1689 | runtimeLocalWorkspace, | 1887 | runtimeLocalWorkspace, |
| @@ -1691,7 +1889,10 @@ describe("workspace-execution-environment", () => { · 1 unmodified line | |||
| 1691 | sourceId, | 1889 | sourceId, |
| 1692 | }); | 1890 | }); |
| 1693 | 1891 | ||
| 1694 | const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | 1892 | const resolveEnvironment = makeResolver( |
| 1893 | persistence, | ||
| 1894 | runtimeLocalWorkspace, | ||
| 1895 | ); | ||
| 1695 | const environment = yield* resolveEnvironment({ | 1896 | const environment = yield* resolveEnvironment({ |
| 1696 | workspaceId, | 1897 | workspaceId, |
| 1697 | accountId: AccountIdSchema.make("acc_openapi_describe_bundle"), | 1898 | accountId: AccountIdSchema.make("acc_openapi_describe_bundle"), |
| @@ -1739,8 +1940,12 @@ describe("workspace-execution-environment", () => { · 41 unmodified lines | |||
| 1739 | Effect.gen(function* () { | 1940 | Effect.gen(function* () { |
| 1740 | const persistence = yield* makePersistence; | 1941 | const persistence = yield* makePersistence; |
| 1741 | const workspaceId = WorkspaceIdSchema.make("ws_describe_add_source"); | 1942 | const workspaceId = WorkspaceIdSchema.make("ws_describe_add_source"); |
| 1742 | const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | 1943 | const runtimeLocalWorkspace = |
| 1743 | const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence, runtimeLocalWorkspace); | 1944 | yield* makeRuntimeLocalWorkspaceState(workspaceId); |
| 1945 | const resolveEnvironment = makeResolverWithLiveSourceAuth( | ||
| 1946 | persistence, | ||
| 1947 | runtimeLocalWorkspace, | ||
| 1948 | ); | ||
| 1744 | 1949 | ||
| 1745 | const environment = yield* resolveEnvironment({ | 1950 | const environment = yield* resolveEnvironment({ |
| 1746 | workspaceId, | 1951 | workspaceId, |
| @@ -2,8 +2,10 @@ import { · 1 unmodified line | |||
| 2 | createSystemToolMap, | 2 | createSystemToolMap, |
| 3 | createToolCatalogFromTools, | 3 | createToolCatalogFromTools, |
| 4 | makeToolInvokerFromTools, | 4 | makeToolInvokerFromTools, |
| 5 | mergeToolMaps, | ||
| 5 | type SearchHit, | 6 | type SearchHit, |
| 6 | type ToolCatalog, | 7 | type ToolCatalog, |
| 8 | type ToolDescriptor, | ||
| 7 | type ToolInvoker, | 9 | type ToolInvoker, |
| 8 | type ToolNamespace, | 10 | type ToolNamespace, |
| 9 | type ToolPath, | 11 | type ToolPath, |
| @@ -51,14 +53,13 @@ import { · 41 unmodified lines | |||
| 51 | evaluateInvocationPolicy, | 53 | evaluateInvocationPolicy, |
| 52 | type InvocationDescriptor, | 54 | type InvocationDescriptor, |
| 53 | } from "./invocation-policy-engine"; | 55 | } from "./invocation-policy-engine"; |
| 54 | import { | 56 | import { loadRuntimeLocalWorkspacePolicies } from "./policies-operations"; |
| 55 | loadRuntimeLocalWorkspacePolicies, | ||
| 56 | } from "./policies-operations"; | ||
| 57 | import { | 57 | import { |
| 58 | getRuntimeLocalWorkspaceOption, | 58 | getRuntimeLocalWorkspaceOption, |
| 59 | RuntimeLocalWorkspaceService, | 59 | RuntimeLocalWorkspaceService, |
| 60 | type RuntimeLocalWorkspaceState, | 60 | type RuntimeLocalWorkspaceState, |
| 61 | } from "./local-runtime-context"; | 61 | } from "./local-runtime-context"; |
| 62 | import { loadLocalToolRuntime, type LocalToolRuntime } from "./local-tools"; | ||
| 62 | 63 | ||
| 63 | const asToolPath = (value: string): ToolPath => value as ToolPath; | 64 | const asToolPath = (value: string): ToolPath => value as ToolPath; |
| 64 | 65 | ||
| @@ -205,10 +206,19 @@ const loadWorkspaceSchemaBundle = (input: { · 140 unmodified lines | |||
| 205 | : null; | 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 | if (Option.isSome(bundle)) { | 212 | if (Option.isSome(bundle)) { |
| 210 | const sourceRecords = yield* input.rows.sources.listByWorkspaceId(input.workspaceId); | 213 | const sourceRecords = yield* input.rows.sources.listByWorkspaceId( |
| 211 | if (sourceRecords.some((sourceRecord) => sourceRecord.recipeRevisionId === bundle.value.recipeRevisionId)) { | 214 | input.workspaceId, |
| 215 | ); | ||
| 216 | if ( | ||
| 217 | sourceRecords.some( | ||
| 218 | (sourceRecord) => | ||
| 219 | sourceRecord.recipeRevisionId === bundle.value.recipeRevisionId, | ||
| 220 | ) | ||
| 221 | ) { | ||
| 212 | return { | 222 | return { |
| 213 | id: bundle.value.id, | 223 | id: bundle.value.id, |
| 214 | kind: bundle.value.bundleKind, | 224 | kind: bundle.value.bundleKind, |
| @@ -386,7 +396,9 @@ const authorizePersistedToolInvocation = (input: { · 171 unmodified lines | |||
| 386 | >[0]["onElicitation"]; | 396 | >[0]["onElicitation"]; |
| 387 | }): Effect.Effect<void, Error, never> => | 397 | }): Effect.Effect<void, Error, never> => |
| 388 | Effect.gen(function* () { | 398 | Effect.gen(function* () { |
| 389 | const localWorkspacePolicies = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | 399 | const localWorkspacePolicies = yield* loadRuntimeLocalWorkspacePolicies( |
| 400 | input.workspaceId, | ||
| 401 | ).pipe( | ||
| 390 | Effect.mapError((cause) => | 402 | Effect.mapError((cause) => |
| 391 | cause instanceof Error ? cause : new Error(String(cause)), | 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 | const createWorkspaceToolCatalog = (input: { | 525 | const createWorkspaceToolCatalog = (input: { |
| 478 | workspaceId: Source["workspaceId"]; | 526 | workspaceId: Source["workspaceId"]; |
| 479 | accountId: AccountId; | 527 | accountId: AccountId; |
| 480 | rows: SqlControlPlaneRows; | 528 | rows: SqlControlPlaneRows; |
| 481 | executorCatalog: ToolCatalog; | 529 | authoredCatalog: ToolCatalog; |
| 482 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null; | 530 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null; |
| 483 | }): ToolCatalog => ({ | 531 | }): ToolCatalog => ({ |
| 484 | listNamespaces: ({ limit }) => | 532 | listNamespaces: ({ limit }) => |
| 485 | provideRuntimeLocalWorkspace(Effect.gen(function* () { | 533 | provideRuntimeLocalWorkspace( |
| 486 | const [recipeTools, executor] = yield* Effect.all([ | 534 | Effect.gen(function* () { |
| 487 | loadWorkspaceRecipeTools({ | 535 | const [recipeTools, authored] = yield* Effect.all([ |
| 488 | rows: input.rows, | 536 | loadWorkspaceRecipeTools({ |
| 489 | workspaceId: input.workspaceId, | 537 | rows: input.rows, |
| 490 | accountId: input.accountId, | 538 | workspaceId: input.workspaceId, |
| 491 | includeSchemas: false, | 539 | accountId: input.accountId, |
| 492 | }), | 540 | includeSchemas: false, |
| 493 | input.executorCatalog.listNamespaces({ limit }), | 541 | }), |
| 494 | ]); | 542 | input.authoredCatalog.listNamespaces({ limit }), |
| 495 | 543 | ]); | |
| 496 | const merged = new Map<string, ToolNamespace>(); | 544 | |
| 497 | for (const tool of recipeTools) { | 545 | const merged = new Map<string, ToolNamespace>(); |
| 498 | const existing = merged.get(tool.searchNamespace); | 546 | for (const tool of recipeTools) { |
| 499 | merged.set(tool.searchNamespace, { | 547 | const existing = merged.get(tool.searchNamespace); |
| 500 | namespace: tool.searchNamespace, | 548 | merged.set(tool.searchNamespace, { |
| 501 | toolCount: (existing?.toolCount ?? 0) + 1, | 549 | namespace: tool.searchNamespace, |
| 502 | }); | 550 | toolCount: (existing?.toolCount ?? 0) + 1, |
| 503 | } | 551 | }); |
| 504 | for (const namespace of executor) { | 552 | } |
| 505 | const existing = merged.get(namespace.namespace); | 553 | for (const namespace of authored) { |
| 506 | merged.set(namespace.namespace, { | 554 | const existing = merged.get(namespace.namespace); |
| 507 | namespace: namespace.namespace, | 555 | merged.set(namespace.namespace, { |
| 508 | displayName: namespace.displayName ?? existing?.displayName, | 556 | namespace: namespace.namespace, |
| 509 | toolCount: | 557 | displayName: namespace.displayName ?? existing?.displayName, |
| 510 | namespace.toolCount !== undefined || | 558 | toolCount: |
| 511 | existing?.toolCount === undefined | 559 | namespace.toolCount !== undefined || |
| 512 | ? namespace.toolCount | 560 | existing?.toolCount === undefined |
| 513 | : existing.toolCount, | 561 | ? namespace.toolCount |
| 514 | }); | 562 | : existing.toolCount, |
| 515 | } | 563 | }); |
| 564 | } | ||
| 516 | 565 | ||
| 517 | return [...merged.values()] | 566 | return [...merged.values()] |
| 518 | .sort((left, right) => left.namespace.localeCompare(right.namespace)) | 567 | .sort((left, right) => left.namespace.localeCompare(right.namespace)) |
| 519 | .slice(0, limit); | 568 | .slice(0, limit); |
| 520 | }), input.runtimeLocalWorkspace), | 569 | }), |
| 570 | input.runtimeLocalWorkspace, | ||
| 571 | ), | ||
| 521 | 572 | ||
| 522 | listTools: ({ namespace, query, limit, includeSchemas = false }) => | 573 | listTools: ({ namespace, query, limit, includeSchemas = false }) => |
| 523 | provideRuntimeLocalWorkspace(Effect.gen(function* () { | 574 | provideRuntimeLocalWorkspace( |
| 524 | const [recipeTools, executor] = yield* Effect.all([ | 575 | Effect.gen(function* () { |
| 525 | loadWorkspaceRecipeTools({ | 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 | rows: input.rows, | 627 | rows: input.rows, |
| 527 | workspaceId: input.workspaceId, | 628 | workspaceId: input.workspaceId, |
| 528 | accountId: input.accountId, | 629 | accountId: input.accountId, |
| 630 | path, | ||
| 529 | includeSchemas, | 631 | includeSchemas, |
| 530 | }).pipe( | 632 | }); |
| 531 | Effect.map((tools) => | 633 | return recipeTool?.descriptor ?? null; |
| 532 | tools.filter((tool) => { | 634 | }), |
| 533 | if (namespace && tool.searchNamespace !== namespace) { | 635 | input.runtimeLocalWorkspace, |
| 534 | return false; | 636 | ), |
| 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 | } | ||
| 567 | 637 | ||
| 568 | const recipeTool = yield* loadWorkspaceRecipeToolByPath({ | 638 | getSchemaBundle: ({ id }) => |
| 639 | provideRuntimeLocalWorkspace( | ||
| 640 | loadWorkspaceSchemaBundle({ | ||
| 569 | rows: input.rows, | 641 | rows: input.rows, |
| 570 | workspaceId: input.workspaceId, | 642 | workspaceId: input.workspaceId, |
| 571 | accountId: input.accountId, | 643 | id: SourceRecipeSchemaBundleIdSchema.make(id), |
| 572 | path, | 644 | }), |
| 573 | includeSchemas, | 645 | input.runtimeLocalWorkspace, |
| 574 | }); | 646 | ), |
| 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), | ||
| 584 | 647 | ||
| 585 | searchTools: ({ query, namespace, limit }) => | 648 | searchTools: ({ query, namespace, limit }) => |
| 586 | provideRuntimeLocalWorkspace(Effect.gen(function* () { | 649 | provideRuntimeLocalWorkspace( |
| 587 | const queryTokens = tokenize(query); | 650 | Effect.gen(function* () { |
| 588 | const [recipeTools, executor] = yield* Effect.all([ | 651 | const queryTokens = tokenize(query); |
| 589 | loadWorkspaceRecipeTools({ | 652 | const [recipeTools, authored] = yield* Effect.all([ |
| 590 | rows: input.rows, | 653 | loadWorkspaceRecipeTools({ |
| 591 | workspaceId: input.workspaceId, | 654 | rows: input.rows, |
| 592 | accountId: input.accountId, | 655 | workspaceId: input.workspaceId, |
| 593 | includeSchemas: false, | 656 | accountId: input.accountId, |
| 594 | }).pipe( | 657 | includeSchemas: false, |
| 595 | Effect.map((tools) => | 658 | }).pipe( |
| 596 | tools.filter( | 659 | Effect.map((tools) => |
| 597 | (tool) => !namespace || tool.searchNamespace === namespace, | 660 | tools.filter( |
| 661 | (tool) => !namespace || tool.searchNamespace === namespace, | ||
| 662 | ), | ||
| 598 | ), | 663 | ), |
| 599 | ), | 664 | ), |
| 600 | ), | 665 | input.authoredCatalog.searchTools({ |
| 601 | input.executorCatalog.searchTools({ | 666 | query, |
| 602 | query, | 667 | ...(namespace !== undefined ? { namespace } : {}), |
| 603 | ...(namespace !== undefined ? { namespace } : {}), | 668 | limit, |
| 604 | limit, | 669 | }), |
| 605 | }), | 670 | ]); |
| 606 | ]); | 671 | |
| 607 | 672 | const recipeHits: SearchHit[] = recipeTools | |
| 608 | const recipeHits: SearchHit[] = recipeTools | 673 | .map((tool) => ({ |
| 609 | .map((tool) => ({ | 674 | path: asToolPath(tool.path), |
| 610 | path: asToolPath(tool.path), | 675 | score: scoreRecipeTool(queryTokens, tool), |
| 611 | score: scoreRecipeTool(queryTokens, tool), | 676 | })) |
| 612 | })) | 677 | .filter((hit) => hit.score > 0); |
| 613 | .filter((hit) => hit.score > 0); | 678 | |
| 614 | 679 | return dedupeSearchHits(authored, recipeHits) | |
| 615 | return [...recipeHits, ...executor] | 680 | .sort( |
| 616 | .sort( | 681 | (left, right) => |
| 617 | (left, right) => | 682 | right.score - left.score || left.path.localeCompare(right.path), |
| 618 | right.score - left.score || left.path.localeCompare(right.path), | 683 | ) |
| 619 | ) | 684 | .slice(0, limit); |
| 620 | .slice(0, limit); | 685 | }), |
| 621 | }), input.runtimeLocalWorkspace), | 686 | input.runtimeLocalWorkspace, |
| 687 | ), | ||
| 622 | }); | 688 | }); |
| 623 | 689 | ||
| 624 | const createWorkspaceToolInvoker = (input: { | 690 | const createWorkspaceToolInvoker = (input: { |
| @@ -628,6 +694,7 @@ const createWorkspaceToolInvoker = (input: { · 3 unmodified lines | |||
| 628 | resolveSecretMaterial: ResolveSecretMaterial; | 694 | resolveSecretMaterial: ResolveSecretMaterial; |
| 629 | sourceAuthService: RuntimeSourceAuthService; | 695 | sourceAuthService: RuntimeSourceAuthService; |
| 630 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null; | 696 | runtimeLocalWorkspace: RuntimeLocalWorkspaceState | null; |
| 697 | localToolRuntime: LocalToolRuntime; | ||
| 631 | onElicitation?: Parameters< | 698 | onElicitation?: Parameters< |
| 632 | typeof makeToolInvokerFromTools | 699 | typeof makeToolInvokerFromTools |
| 633 | >[0]["onElicitation"]; | 700 | >[0]["onElicitation"]; |
| @@ -641,25 +708,34 @@ const createWorkspaceToolInvoker = (input: { · 7 unmodified lines | |||
| 641 | sourceAuthService: input.sourceAuthService, | 708 | sourceAuthService: input.sourceAuthService, |
| 642 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | 709 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, |
| 643 | }); | 710 | }); |
| 644 | const executorCatalog = createToolCatalogFromTools({ | 711 | const authoredTools = mergeToolMaps([ |
| 645 | tools: executorTools, | 712 | executorTools, |
| 713 | input.localToolRuntime.tools, | ||
| 714 | ]); | ||
| 715 | const authoredCatalog = createToolCatalogFromTools({ | ||
| 716 | tools: authoredTools, | ||
| 646 | }); | 717 | }); |
| 647 | const catalog = createWorkspaceToolCatalog({ | 718 | const catalog = createWorkspaceToolCatalog({ |
| 648 | workspaceId: input.workspaceId, | 719 | workspaceId: input.workspaceId, |
| 649 | accountId: input.accountId, | 720 | accountId: input.accountId, |
| 650 | rows: input.rows, | 721 | rows: input.rows, |
| 651 | executorCatalog, | 722 | authoredCatalog, |
| 652 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, | 723 | runtimeLocalWorkspace: input.runtimeLocalWorkspace, |
| 653 | }); | 724 | }); |
| 654 | const systemTools = createSystemToolMap({ catalog }); | 725 | const systemTools = createSystemToolMap({ catalog }); |
| 655 | const systemToolPaths = new Set(Object.keys(systemTools)); | 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 | const systemInvoker = makeToolInvokerFromTools({ | 733 | const systemInvoker = makeToolInvokerFromTools({ |
| 658 | tools: systemTools, | 734 | tools: systemTools, |
| 659 | onElicitation: input.onElicitation, | 735 | onElicitation: input.onElicitation, |
| 660 | }); | 736 | }); |
| 661 | const executorInvoker = makeToolInvokerFromTools({ | 737 | const authoredInvoker = makeToolInvokerFromTools({ |
| 662 | tools: executorTools, | 738 | tools: authoredTools, |
| 663 | onElicitation: input.onElicitation, | 739 | onElicitation: input.onElicitation, |
| 664 | }); | 740 | }); |
| 665 | 741 | ||
| @@ -668,54 +744,60 @@ const createWorkspaceToolInvoker = (input: { · 2 unmodified lines | |||
| 668 | args: unknown; | 744 | args: unknown; |
| 669 | context?: Record<string, unknown>; | 745 | context?: Record<string, unknown>; |
| 670 | }) => | 746 | }) => |
| 671 | provideRuntimeLocalWorkspace(Effect.gen(function* () { | 747 | provideRuntimeLocalWorkspace( |
| 672 | const recipeTool = yield* loadWorkspaceRecipeToolByPath({ | 748 | Effect.gen(function* () { |
| 673 | rows: input.rows, | 749 | const recipeTool = yield* loadWorkspaceRecipeToolByPath({ |
| 674 | workspaceId: input.workspaceId, | 750 | rows: input.rows, |
| 675 | accountId: input.accountId, | 751 | workspaceId: input.workspaceId, |
| 676 | path: invocation.path, | 752 | accountId: input.accountId, |
| 677 | includeSchemas: false, | 753 | path: invocation.path, |
| 678 | }); | 754 | includeSchemas: false, |
| 679 | if (!recipeTool) { | 755 | }); |
| 680 | return yield* Effect.fail( | 756 | if (!recipeTool) { |
| 681 | new Error(`Unknown tool path: ${invocation.path}`), | 757 | return yield* Effect.fail( |
| 682 | ); | 758 | new Error(`Unknown tool path: ${invocation.path}`), |
| 683 | } | 759 | ); |
| 760 | } | ||
| 684 | 761 | ||
| 685 | yield* authorizePersistedToolInvocation({ | 762 | yield* authorizePersistedToolInvocation({ |
| 686 | rows: input.rows, | 763 | rows: input.rows, |
| 687 | workspaceId: input.workspaceId, | 764 | workspaceId: input.workspaceId, |
| 688 | accountId: input.accountId, | 765 | accountId: input.accountId, |
| 689 | descriptor: toInvocationDescriptorFromRecipeTool({ tool: recipeTool }), | 766 | descriptor: toInvocationDescriptorFromRecipeTool({ |
| 690 | args: invocation.args, | 767 | tool: recipeTool, |
| 691 | source: recipeTool.source, | 768 | }), |
| 692 | context: invocation.context, | 769 | args: invocation.args, |
| 693 | onElicitation: input.onElicitation, | 770 | source: recipeTool.source, |
| 694 | }); | 771 | context: invocation.context, |
| 772 | onElicitation: input.onElicitation, | ||
| 773 | }); | ||
| 695 | 774 | ||
| 696 | const auth = yield* resolveSourceAuthMaterial({ | 775 | const auth = yield* resolveSourceAuthMaterial({ |
| 697 | rows: input.rows, | 776 | rows: input.rows, |
| 698 | source: recipeTool.source, | 777 | source: recipeTool.source, |
| 699 | actorAccountId: input.accountId, | 778 | actorAccountId: input.accountId, |
| 700 | resolveSecretMaterial: input.resolveSecretMaterial, | 779 | resolveSecretMaterial: input.resolveSecretMaterial, |
| 701 | context: toSecretResolutionContext(invocation.context), | 780 | context: toSecretResolutionContext(invocation.context), |
| 702 | }); | 781 | }); |
| 703 | const schemaBundle = recipeTool.schemaBundleId | 782 | const schemaBundle = recipeTool.schemaBundleId |
| 704 | ? yield* loadWorkspaceSchemaBundle({ | 783 | ? yield* loadWorkspaceSchemaBundle({ |
| 705 | rows: input.rows, | 784 | rows: input.rows, |
| 706 | workspaceId: input.workspaceId, | 785 | workspaceId: input.workspaceId, |
| 707 | id: SourceRecipeSchemaBundleIdSchema.make(recipeTool.schemaBundleId), | 786 | id: SourceRecipeSchemaBundleIdSchema.make( |
| 708 | }) | 787 | recipeTool.schemaBundleId, |
| 709 | : null; | 788 | ), |
| 710 | const recipe = yield* loadSourceWithRecipe({ | 789 | }) |
| 711 | rows: input.rows, | 790 | : null; |
| 712 | workspaceId: input.workspaceId, | 791 | const recipe = yield* loadSourceWithRecipe({ |
| 713 | sourceId: recipeTool.source.id, | 792 | rows: input.rows, |
| 714 | actorAccountId: input.accountId, | 793 | workspaceId: input.workspaceId, |
| 715 | }); | 794 | sourceId: recipeTool.source.id, |
| 795 | actorAccountId: input.accountId, | ||
| 796 | }); | ||
| 716 | 797 | ||
| 717 | return yield* getSourceAdapterForOperation(recipeTool.operation) | 798 | return yield* getSourceAdapterForOperation( |
| 718 | .invokePersistedTool({ | 799 | recipeTool.operation, |
| 800 | ).invokePersistedTool({ | ||
| 719 | rows: input.rows, | 801 | rows: input.rows, |
| 720 | workspaceId: input.workspaceId, | 802 | workspaceId: input.workspaceId, |
| 721 | accountId: input.accountId, | 803 | accountId: input.accountId, |
| @@ -729,7 +811,9 @@ const createWorkspaceToolInvoker = (input: { · 7 unmodified lines | |||
| 729 | context: invocation.context, | 811 | context: invocation.context, |
| 730 | onElicitation: input.onElicitation, | 812 | onElicitation: input.onElicitation, |
| 731 | }); | 813 | }); |
| 732 | }), input.runtimeLocalWorkspace); | 814 | }), |
| 815 | input.runtimeLocalWorkspace, | ||
| 816 | ); | ||
| 733 | 817 | ||
| 734 | return { | 818 | return { |
| 735 | catalog, | 819 | catalog, |
| @@ -738,8 +822,8 @@ const createWorkspaceToolInvoker = (input: { · 2 unmodified lines | |||
| 738 | provideRuntimeLocalWorkspace( | 822 | provideRuntimeLocalWorkspace( |
| 739 | systemToolPaths.has(path) | 823 | systemToolPaths.has(path) |
| 740 | ? systemInvoker.invoke({ path, args, context }) | 824 | ? systemInvoker.invoke({ path, args, context }) |
| 741 | : executorToolPaths.has(path) | 825 | : authoredToolPaths.has(path) |
| 742 | ? executorInvoker.invoke({ path, args, context }) | 826 | ? authoredInvoker.invoke({ path, args, context }) |
| 743 | : invokePersistedTool({ path, args, context }), | 827 | : invokePersistedTool({ path, args, context }), |
| 744 | input.runtimeLocalWorkspace, | 828 | input.runtimeLocalWorkspace, |
| 745 | ), | 829 | ), |
| @@ -761,6 +845,15 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 15 unmodified lines | |||
| 761 | return ({ workspaceId, accountId, onElicitation }) => | 845 | return ({ workspaceId, accountId, onElicitation }) => |
| 762 | Effect.gen(function* () { | 846 | Effect.gen(function* () { |
| 763 | const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | 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 | const { catalog, toolInvoker } = createWorkspaceToolInvoker({ | 857 | const { catalog, toolInvoker } = createWorkspaceToolInvoker({ |
| 765 | workspaceId, | 858 | workspaceId, |
| 766 | accountId, | 859 | accountId, |
| @@ -768,6 +861,7 @@ export const createWorkspaceExecutionEnvironmentResolver = (input: { · 1 unmodified line | |||
| 768 | resolveSecretMaterial, | 861 | resolveSecretMaterial, |
| 769 | sourceAuthService: input.sourceAuthService, | 862 | sourceAuthService: input.sourceAuthService, |
| 770 | runtimeLocalWorkspace, | 863 | runtimeLocalWorkspace, |
| 864 | localToolRuntime, | ||
| 771 | onElicitation, | 865 | onElicitation, |
| 772 | }); | 866 | }); |
| 773 | 867 | ||