Split control-plane and Convex handlers into domain-focused modules, keep v1-only transport routes, and remove obsolete rpc/state-convex scaffolding so local and remote share the same Effect HttpApi architecture.
Unified · Split
| @@ -64,7 +64,7 @@ Control is optional capability, not required for execute-only environments. · 63 unmodified lines | ||
| 64 | 64 | How clients talk to executor. |
| 65 | 65 | |
| 66 | 66 | - MCP server/gateway |
| 67 | −- OpenAPI/RPC control API | |
| 67 | +- OpenAPI control API | |
| 68 | 68 | - AI SDK tool bridge |
| 69 | 69 | |
| 70 | 70 | Transport does not own core execution semantics. |
| @@ -167,7 +167,7 @@ This is mostly transport/API behavior; core runtime pipeline stays the same. · 96 unmodified lines | ||
| 167 | 167 | |
| 168 | 168 | --- |
| 169 | 169 | |
| 170 | −## 8) MCP, RPC, and AI SDK strategy | |
| 170 | +## 8) MCP, OpenAPI, and AI SDK strategy | |
| 171 | 171 | |
| 172 | 172 | ### MCP |
| 173 | 173 | |
| @@ -179,7 +179,7 @@ Top-level model-facing tools stay minimal: · 5 unmodified lines | ||
| 179 | 179 | |
| 180 | 180 | ### Control API |
| 181 | 181 | |
| 182 | −Control is exposed through Executor OpenAPI/RPC contracts. | |
| 182 | +Control is exposed through Executor OpenAPI contracts. | |
| 183 | 183 | |
| 184 | 184 | - Host/app control calls should use control API directly. |
| 185 | 185 | - Model control calls can use `tools.executor.*` inside `execute` code. |
| @@ -287,16 +287,15 @@ v2/ · 101 unmodified lines | ||
| 287 | 287 | schema/ |
| 288 | 288 | state-contracts/ |
| 289 | 289 | state-local-file/ |
| 290 | − state-convex/ | |
| 291 | 290 | domain/ |
| 292 | 291 | approvals/ |
| 293 | 292 | source-manager/ |
| 293 | + control-plane/ | |
| 294 | 294 | engine/ |
| 295 | 295 | mcp-gateway/ |
| 296 | 296 | oauth/ |
| 297 | 297 | secrets/ |
| 298 | 298 | sync/ |
| 299 | − rpc/ | |
| 300 | 299 | sdk/ |
| 301 | 300 | ai-sdk-adapter/ |
| 302 | 301 | ``` |
| @@ -313,15 +312,15 @@ Planned follow-up split (after behavior stabilizes): · 10 unmodified lines | ||
| 313 | 312 | confect -> schema -> state-contracts |
| 314 | 313 | |
| 315 | 314 | state-contracts -> state-local-file |
| 316 | −state-contracts -> state-convex | |
| 317 | 315 | |
| 318 | 316 | schema + state-contracts + oauth + secrets -> domain |
| 319 | 317 | domain -> approvals/source-manager/engine |
| 318 | +source-manager + schema -> control-plane | |
| 320 | 319 | approvals/source-manager/engine -> mcp-gateway |
| 321 | −domain/features -> rpc -> sdk -> ai-sdk-adapter | |
| 320 | +domain/features -> sdk -> ai-sdk-adapter | |
| 322 | 321 | |
| 323 | −apps/pm -> mcp-gateway + domain + state-local-file + rpc | |
| 324 | −apps/convex -> mcp-gateway + domain + state-convex + rpc | |
| 322 | +apps/pm -> mcp-gateway + domain + state-local-file + control-plane | |
| 323 | +apps/convex -> mcp-gateway + domain + control-plane | |
| 325 | 324 | apps/cli/web -> sdk (and/or ai-sdk bridge) |
| 326 | 325 | ``` |
| 327 | 326 | Rules: |
| @@ -367,7 +366,7 @@ Rules: · 39 unmodified lines | ||
| 367 | 366 | ### Phase 5: transport polish |
| 368 | 367 | |
| 369 | 368 | - MCP surface stabilization |
| 370 | −- OpenAPI/RPC control surface stabilization | |
| 369 | +- OpenAPI control surface stabilization | |
| 371 | 370 | - AI SDK bridge (`toAiSdkTools`) and docs/examples |
| 372 | 371 | |
| 373 | 372 | --- |
| @@ -414,7 +413,7 @@ Still missing: · 40 unmodified lines | ||
| 414 | 413 | - shared tool invocation service wired with persisted approval lifecycle in v2 path |
| 415 | 414 | - cloudflare-worker-loader runtime adapter implementation in v2 runtime path |
| 416 | 415 | - parity conformance tests for runtime/provider/pending-approval/cancel flows |
| 417 | −- full rpc/sdk/ai-sdk adapter wiring and usage examples | |
| 416 | +- full openapi/sdk/ai-sdk adapter wiring and usage examples | |
| 418 | 417 | |
| 419 | 418 | --- |
| 420 | 419 | |
| @@ -3,7 +3,9 @@ · 2 unmodified lines | ||
| 3 | 3 | Convex remote server app scaffold for Executor v2. |
| 4 | 4 | |
| 5 | 5 | Current scaffold includes: |
| 6 | − Convex schema wiring at `convex/schema.ts`, sourced from `@executor-v2/persistence-convex` | |
| 7 | − MCP HTTP endpoint at `convex/http.ts` and `convex/mcp.ts`, wired through a run client | |
| 8 | − runtime callback endpoints at `POST /runtime/tool-call` and `POST /v1/runtime/tool-call` | |
| 9 | − action-level callback handler scaffold in `convex/runtimeCallbacks.ts` | |
| 6 | + Convex schema wiring lives directly in `convex/schema.ts` | |
| 7 | + MCP HTTP endpoint at `GET/POST/DELETE /v1/mcp` via `convex/http.ts` and `convex/mcp.ts` | |
| 8 | + runtime callback endpoint at `POST /v1/runtime/tool-call` | |
| 9 | + control-plane source endpoints at `GET/POST /v1/workspaces/:workspaceId/sources` | |
| 10 | + control-plane source removal endpoint at `DELETE /v1/workspaces/:workspaceId/sources/:sourceId` | |
| 11 | + generated OpenAPI spec endpoint at `GET /v1/openapi.json` | |
| @@ -8,10 +8,14 @@ · 7 unmodified lines | ||
| 8 | 8 | * @module |
| 9 | 9 | */ |
| 10 | 10 | |
| 11 | +import type * as controlPlane from "../controlPlane.js"; | |
| 12 | +import type * as control_plane_errors from "../control_plane/errors.js"; | |
| 13 | +import type * as control_plane_http from "../control_plane/http.js"; | |
| 14 | +import type * as control_plane_service from "../control_plane/service.js"; | |
| 15 | +import type * as control_plane_sources from "../control_plane/sources.js"; | |
| 11 | 16 | import type * as executor from "../executor.js"; |
| 12 | 17 | import type * as http from "../http.js"; |
| 13 | 18 | import type * as mcp from "../mcp.js"; |
| 14 | −import type * as rpc_exit from "../rpc_exit.js"; | |
| 15 | 19 | import type * as run_executor from "../run_executor.js"; |
| 16 | 20 | import type * as runtimeCallbacks from "../runtimeCallbacks.js"; |
| 17 | 21 | import type * as runtime_adapter from "../runtime_adapter.js"; |
| @@ -23,10 +27,14 @@ import type { · 5 unmodified lines | ||
| 23 | 27 | } from "convex/server"; |
| 24 | 28 | |
| 25 | 29 | declare const fullApi: ApiFromModules<{ |
| 30 | + controlPlane: typeof controlPlane; | |
| 31 | + "control_plane/errors": typeof control_plane_errors; | |
| 32 | + "control_plane/http": typeof control_plane_http; | |
| 33 | + "control_plane/service": typeof control_plane_service; | |
| 34 | + "control_plane/sources": typeof control_plane_sources; | |
| 26 | 35 | executor: typeof executor; |
| 27 | 36 | http: typeof http; |
| 28 | 37 | mcp: typeof mcp; |
| 29 | − rpc_exit: typeof rpc_exit; | |
| 30 | 38 | run_executor: typeof run_executor; |
| 31 | 39 | runtimeCallbacks: typeof runtimeCallbacks; |
| 32 | 40 | runtime_adapter: typeof runtime_adapter; |
| @@ -0,0 +1,12 @@ | ||
| 1 | +import { | |
| 2 | + listSources as listSourcesImpl, | |
| 3 | + removeSource as removeSourceImpl, | |
| 4 | + upsertSource as upsertSourceImpl, | |
| 5 | +} from "./control_plane/sources"; | |
| 6 | +import { controlPlaneHttpHandler as controlPlaneHttpHandlerImpl } from "./control_plane/http"; | |
| 7 | + | |
| 8 | +export const listSources = listSourcesImpl; | |
| 9 | +export const upsertSource = upsertSourceImpl; | |
| 10 | +export const removeSource = removeSourceImpl; | |
| 11 | + | |
| 12 | +export const controlPlaneHttpHandler = controlPlaneHttpHandlerImpl; | |
| @@ -0,0 +1,75 @@ | ||
| 1 | +import { SourceStoreError } from "@executor-v2/persistence-ports"; | |
| 2 | + | |
| 3 | +type ControlPlaneErrorCode = | |
| 4 | + | "bad_request" | |
| 5 | + | "not_found" | |
| 6 | + | "method_not_allowed" | |
| 7 | + | "internal"; | |
| 8 | + | |
| 9 | +type ControlPlaneErrorResponse = { | |
| 10 | + ok: false; | |
| 11 | + error: { | |
| 12 | + code: ControlPlaneErrorCode; | |
| 13 | + message: string; | |
| 14 | + details: string | null; | |
| 15 | + }; | |
| 16 | +}; | |
| 17 | + | |
| 18 | +const renderCause = (cause: unknown): string => String(cause); | |
| 19 | + | |
| 20 | +export const toSourceStoreError = ( | |
| 21 | + operation: string, | |
| 22 | + cause: unknown, | |
| 23 | +): SourceStoreError => | |
| 24 | + new SourceStoreError({ | |
| 25 | + operation, | |
| 26 | + filePath: "convex://control-plane", | |
| 27 | + message: "Convex control plane call failed", | |
| 28 | + reason: null, | |
| 29 | + details: renderCause(cause), | |
| 30 | + }); | |
| 31 | + | |
| 32 | +const toErrorCode = (status: number): ControlPlaneErrorCode => { | |
| 33 | + switch (status) { | |
| 34 | + case 400: | |
| 35 | + return "bad_request"; | |
| 36 | + case 404: | |
| 37 | + return "not_found"; | |
| 38 | + case 405: | |
| 39 | + return "method_not_allowed"; | |
| 40 | + default: | |
| 41 | + return "internal"; | |
| 42 | + } | |
| 43 | +}; | |
| 44 | + | |
| 45 | +export const controlPlaneErrorResponse = ( | |
| 46 | + status: number, | |
| 47 | + message: string, | |
| 48 | + details: string | null, | |
| 49 | +): Response => | |
| 50 | + Response.json( | |
| 51 | + { | |
| 52 | + ok: false, | |
| 53 | + error: { | |
| 54 | + code: toErrorCode(status), | |
| 55 | + message, | |
| 56 | + details, | |
| 57 | + }, | |
| 58 | + } satisfies ControlPlaneErrorResponse, | |
| 59 | + { status }, | |
| 60 | + ); | |
| 61 | + | |
| 62 | +const isJsonResponse = (response: Response): boolean => | |
| 63 | + response.headers.get("content-type")?.toLowerCase().includes("application/json") ?? false; | |
| 64 | + | |
| 65 | +export const normalizeControlPlaneErrorResponse = (response: Response): Response => { | |
| 66 | + if (response.ok || isJsonResponse(response)) { | |
| 67 | + return response; | |
| 68 | + } | |
| 69 | + | |
| 70 | + return controlPlaneErrorResponse( | |
| 71 | + response.status, | |
| 72 | + "Control plane request failed", | |
| 73 | + response.statusText || null, | |
| 74 | + ); | |
| 75 | +}; | |
| @@ -0,0 +1,65 @@ | ||
| 1 | +import { | |
| 2 | + ControlPlaneService, | |
| 3 | + controlPlaneOpenApiSpec, | |
| 4 | + makeControlPlaneWebHandler, | |
| 5 | +} from "@executor-v2/control-plane"; | |
| 6 | +import * as Effect from "effect/Effect"; | |
| 7 | +import * as Layer from "effect/Layer"; | |
| 8 | + | |
| 9 | +import { httpAction, type ActionCtx } from "../_generated/server"; | |
| 10 | +import { | |
| 11 | + controlPlaneErrorResponse, | |
| 12 | + normalizeControlPlaneErrorResponse, | |
| 13 | + toSourceStoreError, | |
| 14 | +} from "./errors"; | |
| 15 | +import { makeConvexControlPlaneService } from "./service"; | |
| 16 | + | |
| 17 | +const isOpenApiRequest = (request: Request): boolean => | |
| 18 | + new URL(request.url).pathname === "/v1/openapi.json"; | |
| 19 | + | |
| 20 | +const handleControlPlaneRequest = ( | |
| 21 | + ctx: ActionCtx, | |
| 22 | + request: Request, | |
| 23 | +): Effect.Effect<Response, never> => | |
| 24 | + Effect.scoped( | |
| 25 | + Effect.gen(function* () { | |
| 26 | + if (isOpenApiRequest(request)) { | |
| 27 | + return Response.json(controlPlaneOpenApiSpec, { status: 200 }); | |
| 28 | + } | |
| 29 | + | |
| 30 | + const webHandler = makeControlPlaneWebHandler( | |
| 31 | + Layer.succeed(ControlPlaneService, makeConvexControlPlaneService(ctx)), | |
| 32 | + ); | |
| 33 | + | |
| 34 | + yield* Effect.addFinalizer(() => | |
| 35 | + Effect.tryPromise({ | |
| 36 | + try: () => webHandler.dispose(), | |
| 37 | + catch: (cause) => toSourceStoreError("controlPlane.dispose", cause), | |
| 38 | + }).pipe( | |
| 39 | + Effect.asVoid, | |
| 40 | + Effect.catchAll(() => Effect.void), | |
| 41 | + ), | |
| 42 | + ); | |
| 43 | + | |
| 44 | + const response = yield* Effect.tryPromise({ | |
| 45 | + try: () => webHandler.handler(request), | |
| 46 | + catch: (cause) => toSourceStoreError("controlPlane.http", cause), | |
| 47 | + }); | |
| 48 | + | |
| 49 | + return normalizeControlPlaneErrorResponse(response); | |
| 50 | + }), | |
| 51 | + ).pipe( | |
| 52 | + Effect.catchAll((error) => | |
| 53 | + Effect.succeed( | |
| 54 | + controlPlaneErrorResponse( | |
| 55 | + 500, | |
| 56 | + "Control plane request failed", | |
| 57 | + error.details, | |
| 58 | + ), | |
| 59 | + ), | |
| 60 | + ), | |
| 61 | + ); | |
| 62 | + | |
| 63 | +export const controlPlaneHttpHandler = httpAction((ctx, request) => | |
| 64 | + Effect.runPromise(handleControlPlaneRequest(ctx, request)), | |
| 65 | +); | |
| @@ -0,0 +1,41 @@ | ||
| 1 | +import { | |
| 2 | + ControlPlaneService, | |
| 3 | + type ControlPlaneServiceShape, | |
| 4 | +} from "@executor-v2/control-plane"; | |
| 5 | +import * as Effect from "effect/Effect"; | |
| 6 | + | |
| 7 | +import { api } from "../_generated/api"; | |
| 8 | +import type { ActionCtx } from "../_generated/server"; | |
| 9 | +import { toSourceStoreError } from "./errors"; | |
| 10 | + | |
| 11 | +export const makeConvexControlPlaneService = ( | |
| 12 | + ctx: ActionCtx, | |
| 13 | +): ControlPlaneServiceShape => | |
| 14 | + ControlPlaneService.of({ | |
| 15 | + listSources: (workspaceId) => | |
| 16 | + Effect.tryPromise({ | |
| 17 | + try: () => | |
| 18 | + ctx.runQuery(api.controlPlane.listSources, { | |
| 19 | + workspaceId, | |
| 20 | + }), | |
| 21 | + catch: (cause) => toSourceStoreError("controlPlane.listSources", cause), | |
| 22 | + }), | |
| 23 | + upsertSource: (input) => | |
| 24 | + Effect.tryPromise({ | |
| 25 | + try: () => | |
| 26 | + ctx.runMutation(api.controlPlane.upsertSource, { | |
| 27 | + workspaceId: input.workspaceId, | |
| 28 | + payload: input.payload, | |
| 29 | + }), | |
| 30 | + catch: (cause) => toSourceStoreError("controlPlane.upsertSource", cause), | |
| 31 | + }), | |
| 32 | + removeSource: (input) => | |
| 33 | + Effect.tryPromise({ | |
| 34 | + try: () => | |
| 35 | + ctx.runMutation(api.controlPlane.removeSource, { | |
| 36 | + workspaceId: input.workspaceId, | |
| 37 | + sourceId: input.sourceId, | |
| 38 | + }), | |
| 39 | + catch: (cause) => toSourceStoreError("controlPlane.removeSource", cause), | |
| 40 | + }), | |
| 41 | + }); | |
| @@ -0,0 +1,140 @@ | ||
| 1 | +import { type UpsertSourcePayload } from "@executor-v2/control-plane"; | |
| 2 | +import { SourceSchema, type Source } from "@executor-v2/schema"; | |
| 3 | +import { v } from "convex/values"; | |
| 4 | +import * as Schema from "effect/Schema"; | |
| 5 | + | |
| 6 | +import { mutation, query } from "../_generated/server"; | |
| 7 | + | |
| 8 | +const decodeSource = Schema.decodeUnknownSync(SourceSchema); | |
| 9 | + | |
| 10 | +const sourceStoreKey = (source: Source): string => `${source.workspaceId}:${source.id}`; | |
| 11 | + | |
| 12 | +const sortSources = (sources: ReadonlyArray<Source>): Array<Source> => | |
| 13 | + [...sources].sort((left, right) => { | |
| 14 | + const leftName = left.name.toLowerCase(); | |
| 15 | + const rightName = right.name.toLowerCase(); | |
| 16 | + | |
| 17 | + if (leftName === rightName) { | |
| 18 | + return sourceStoreKey(left).localeCompare(sourceStoreKey(right)); | |
| 19 | + } | |
| 20 | + | |
| 21 | + return leftName.localeCompare(rightName); | |
| 22 | + }); | |
| 23 | + | |
| 24 | +const toSource = (document: Record<string, unknown>): Source => { | |
| 25 | + const { _id: _ignoredId, _creationTime: _ignoredCreationTime, ...source } = document; | |
| 26 | + return decodeSource(source); | |
| 27 | +}; | |
| 28 | + | |
| 29 | +const sourceKindValidator = v.union( | |
| 30 | + v.literal("mcp"), | |
| 31 | + v.literal("openapi"), | |
| 32 | + v.literal("graphql"), | |
| 33 | + v.literal("internal"), | |
| 34 | +); | |
| 35 | + | |
| 36 | +const sourceStatusValidator = v.union( | |
| 37 | + v.literal("draft"), | |
| 38 | + v.literal("probing"), | |
| 39 | + v.literal("auth_required"), | |
| 40 | + v.literal("connected"), | |
| 41 | + v.literal("error"), | |
| 42 | +); | |
| 43 | + | |
| 44 | +export const listSources = query({ | |
| 45 | + args: { | |
| 46 | + workspaceId: v.string(), | |
| 47 | + }, | |
| 48 | + handler: async (ctx, args): Promise<Array<Source>> => { | |
| 49 | + const documents = await ctx.db | |
| 50 | + .query("sources") | |
| 51 | + .withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId)) | |
| 52 | + .collect(); | |
| 53 | + | |
| 54 | + return sortSources( | |
| 55 | + documents.map((document) => | |
| 56 | + toSource(document as unknown as Record<string, unknown>), | |
| 57 | + ), | |
| 58 | + ); | |
| 59 | + }, | |
| 60 | +}); | |
| 61 | + | |
| 62 | +export const upsertSource = mutation({ | |
| 63 | + args: { | |
| 64 | + workspaceId: v.string(), | |
| 65 | + payload: v.object({ | |
| 66 | + id: v.optional(v.string()), | |
| 67 | + name: v.string(), | |
| 68 | + kind: sourceKindValidator, | |
| 69 | + endpoint: v.string(), | |
| 70 | + status: v.optional(sourceStatusValidator), | |
| 71 | + enabled: v.optional(v.boolean()), | |
| 72 | + configJson: v.optional(v.string()), | |
| 73 | + sourceHash: v.optional(v.union(v.string(), v.null())), | |
| 74 | + lastError: v.optional(v.union(v.string(), v.null())), | |
| 75 | + }), | |
| 76 | + }, | |
| 77 | + handler: async (ctx, args): Promise<Source> => { | |
| 78 | + const now = Date.now(); | |
| 79 | + const sourceId = args.payload.id ?? `src_${crypto.randomUUID()}`; | |
| 80 | + | |
| 81 | + const existing = await ctx.db | |
| 82 | + .query("sources") | |
| 83 | + .withIndex("by_domainId", (q) => q.eq("id", sourceId)) | |
| 84 | + .unique(); | |
| 85 | + | |
| 86 | + const existingInWorkspace = | |
| 87 | + existing && existing.workspaceId === args.workspaceId ? existing : null; | |
| 88 | + | |
| 89 | + const payload = args.payload as UpsertSourcePayload; | |
| 90 | + | |
| 91 | + const source = decodeSource({ | |
| 92 | + id: sourceId, | |
| 93 | + workspaceId: args.workspaceId, | |
| 94 | + name: payload.name, | |
| 95 | + kind: payload.kind, | |
| 96 | + endpoint: payload.endpoint, | |
| 97 | + status: payload.status ?? "draft", | |
| 98 | + enabled: payload.enabled ?? true, | |
| 99 | + configJson: payload.configJson ?? "{}", | |
| 100 | + sourceHash: payload.sourceHash ?? null, | |
| 101 | + lastError: payload.lastError ?? null, | |
| 102 | + createdAt: existingInWorkspace?.createdAt ?? now, | |
| 103 | + updatedAt: now, | |
| 104 | + }); | |
| 105 | + | |
| 106 | + if (existingInWorkspace) { | |
| 107 | + await ctx.db.patch(existingInWorkspace._id, source); | |
| 108 | + } else { | |
| 109 | + await ctx.db.insert("sources", source); | |
| 110 | + } | |
| 111 | + | |
| 112 | + return source; | |
| 113 | + }, | |
| 114 | +}); | |
| 115 | + | |
| 116 | +export const removeSource = mutation({ | |
| 117 | + args: { | |
| 118 | + workspaceId: v.string(), | |
| 119 | + sourceId: v.string(), | |
| 120 | + }, | |
| 121 | + handler: async ( | |
| 122 | + ctx, | |
| 123 | + args, | |
| 124 | + ): Promise<{ | |
| 125 | + removed: boolean; | |
| 126 | + }> => { | |
| 127 | + const existing = await ctx.db | |
| 128 | + .query("sources") | |
| 129 | + .withIndex("by_domainId", (q) => q.eq("id", args.sourceId)) | |
| 130 | + .unique(); | |
| 131 | + | |
| 132 | + if (!existing || existing.workspaceId !== args.workspaceId) { | |
| 133 | + return { removed: false }; | |
| 134 | + } | |
| 135 | + | |
| 136 | + await ctx.db.delete(existing._id); | |
| 137 | + | |
| 138 | + return { removed: true }; | |
| 139 | + }, | |
| 140 | +}); | |
| @@ -3,7 +3,7 @@ import { convexTest } from "convex-test"; · 2 unmodified lines | ||
| 3 | 3 | import * as Effect from "effect/Effect"; |
| 4 | 4 | |
| 5 | 5 | import { api } from "./_generated/api"; |
| 6 | −import { unwrapRpcSuccess } from "./rpc_exit"; | |
| 6 | +import { executeRunImpl } from "./executor"; | |
| 7 | 7 | import schema from "./schema"; |
| 8 | 8 | |
| 9 | 9 | const setup = () => |
| @@ -12,54 +12,80 @@ const setup = () => · 2 unmodified lines | ||
| 12 | 12 | "./mcp.ts": () => import("./mcp"), |
| 13 | 13 | "./executor.ts": () => import("./executor"), |
| 14 | 14 | "./runtimeCallbacks.ts": () => import("./runtimeCallbacks"), |
| 15 | + "./controlPlane.ts": () => import("./controlPlane"), | |
| 15 | 16 | "./_generated/api.js": () => import("./_generated/api.js"), |
| 16 | 17 | }); |
| 17 | 18 | |
| 18 | −describe("Convex executor actions", () => { | |
| 19 | − it.effect("executes code via executeRun action", () => | |
| 19 | +describe("Convex executor and control-plane", () => { | |
| 20 | + it.effect("executes code via executeRunImpl", () => | |
| 20 | 21 | Effect.gen(function* () { |
| 21 | − const t = setup(); | |
| 22 | − | |
| 23 | − const result = unwrapRpcSuccess<{ | |
| 24 | − status: string; | |
| 25 | − result?: unknown; | |
| 26 | − }>( | |
| 27 | − yield* Effect.tryPromise(() => | |
| 28 | − t.action(api.executor.executeRun, { | |
| 29 | − code: "return 6 * 7;", | |
| 30 | − }), | |
| 31 | − ), | |
| 32 | − "executor.executeRun", | |
| 33 | − ); | |
| 22 | + const result = yield* executeRunImpl({ | |
| 23 | + code: "return 6 * 7;", | |
| 24 | + }); | |
| 34 | 25 | |
| 35 | 26 | expect(result.status).toBe("completed"); |
| 36 | 27 | expect(result.result).toBe(42); |
| 37 | 28 | }), |
| 38 | 29 | ); |
| 39 | 30 | |
| 40 | − it.effect("returns failed callback result from runtime callback action", () => | |
| 31 | + it.effect("upserts, lists, and removes sources", () => | |
| 41 | 32 | Effect.gen(function* () { |
| 42 | 33 | const t = setup(); |
| 43 | 34 | |
| 44 | − const result = unwrapRpcSuccess<{ | |
| 45 | − ok: boolean; | |
| 46 | − kind?: string; | |
| 47 | − error?: string; | |
| 48 | − }>( | |
| 49 | − yield* Effect.tryPromise(() => | |
| 50 | − t.action(api.runtimeCallbacks.handleToolCall, { | |
| 51 | − runId: "run_1", | |
| 52 | − callId: "call_1", | |
| 53 | − toolPath: "tools.example.lookup", | |
| 54 | − input: { query: "ping" }, | |
| 55 | − }), | |
| 56 | − ), | |
| 57 | − "runtimeCallbacks.handleToolCall", | |
| 58 | − ); | |
| 35 | + const added = (yield* Effect.tryPromise(() => | |
| 36 | + t.mutation(api.controlPlane.upsertSource, { | |
| 37 | + workspaceId: "ws_1", | |
| 38 | + payload: { | |
| 39 | + id: "src_1", | |
| 40 | + name: "Weather", | |
| 41 | + kind: "openapi", | |
| 42 | + endpoint: "https://example.com/openapi.json", | |
| 43 | + enabled: true, | |
| 44 | + configJson: "{}", | |
| 45 | + status: "draft", | |
| 46 | + sourceHash: null, | |
| 47 | + lastError: null, | |
| 48 | + }, | |
| 49 | + }), | |
| 50 | + )) as { | |
| 51 | + id: string; | |
| 52 | + workspaceId: string; | |
| 53 | + name: string; | |
| 54 | + }; | |
| 55 | + | |
| 56 | + expect(added.id).toBe("src_1"); | |
| 57 | + expect(added.workspaceId).toBe("ws_1"); | |
| 58 | + expect(added.name).toBe("Weather"); | |
| 59 | + | |
| 60 | + const listed = (yield* Effect.tryPromise(() => | |
| 61 | + t.query(api.controlPlane.listSources, { | |
| 62 | + workspaceId: "ws_1", | |
| 63 | + }), | |
| 64 | + )) as Array<{ | |
| 65 | + id: string; | |
| 66 | + }>; | |
| 67 | + | |
| 68 | + expect(listed).toHaveLength(1); | |
| 69 | + expect(listed[0]?.id).toBe("src_1"); | |
| 70 | + | |
| 71 | + const removed = (yield* Effect.tryPromise(() => | |
| 72 | + t.mutation(api.controlPlane.removeSource, { | |
| 73 | + workspaceId: "ws_1", | |
| 74 | + sourceId: "src_1", | |
| 75 | + }), | |
| 76 | + )) as { | |
| 77 | + removed: boolean; | |
| 78 | + }; | |
| 79 | + | |
| 80 | + expect(removed.removed).toBe(true); | |
| 81 | + | |
| 82 | + const listedAfterRemove = (yield* Effect.tryPromise(() => | |
| 83 | + t.query(api.controlPlane.listSources, { | |
| 84 | + workspaceId: "ws_1", | |
| 85 | + }), | |
| 86 | + )) as Array<unknown>; | |
| 59 | 87 | |
| 60 | − expect(result.ok).toBe(false); | |
| 61 | − expect(result.kind).toBe("failed"); | |
| 62 | − expect(result.error).toContain("tools.example.lookup"); | |
| 88 | + expect(listedAfterRemove).toHaveLength(0); | |
| 63 | 89 | }), |
| 64 | 90 | ); |
| 65 | 91 | }); |
| @@ -1,9 +1,6 @@ | ||
| 1 | −import { createRpcFactory, makeRpcModule } from "@executor-v2/confect/rpc"; | |
| 2 | −import { executorConfectSchema } from "@executor-v2/persistence-convex"; | |
| 3 | −import type { ExecuteRunInput } from "@executor-v2/sdk"; | |
| 1 | +import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk"; | |
| 4 | 2 | import * as Effect from "effect/Effect"; |
| 5 | 3 | import * as Layer from "effect/Layer"; |
| 6 | −import * as Schema from "effect/Schema"; | |
| 7 | 4 | |
| 8 | 5 | import { |
| 9 | 6 | ConvexRunExecutor, |
| @@ -11,24 +8,6 @@ import { · 1 unmodified line | ||
| 11 | 8 | ConvexToolProviderRegistryLive, |
| 12 | 9 | } from "./run_executor"; |
| 13 | 10 | |
| 14 | −const executeRunResultSchema = Schema.Struct({ | |
| 15 | − runId: Schema.String, | |
| 16 | − status: Schema.Union( | |
| 17 | − Schema.Literal("completed"), | |
| 18 | − Schema.Literal("failed"), | |
| 19 | − Schema.Literal("timed_out"), | |
| 20 | − Schema.Literal("denied"), | |
| 21 | − ), | |
| 22 | − result: Schema.optional(Schema.Unknown), | |
| 23 | − error: Schema.optional(Schema.String), | |
| 24 | − exitCode: Schema.optional(Schema.Number), | |
| 25 | − durationMs: Schema.optional(Schema.Number), | |
| 26 | −}); | |
| 27 | − | |
| 28 | −const factory = createRpcFactory({ | |
| 29 | − schema: executorConfectSchema, | |
| 30 | −}); | |
| 31 | − | |
| 32 | 11 | const ConvexExecuteDependenciesLive = Layer.merge( |
| 33 | 12 | ConvexRunExecutorLive, |
| 34 | 13 | ConvexToolProviderRegistryLive, |
| @@ -36,29 +15,8 @@ const ConvexExecuteDependenciesLive = Layer.merge( · 1 unmodified line | ||
| 36 | 15 | |
| 37 | 16 | export const executeRunImpl = ( |
| 38 | 17 | input: ExecuteRunInput, |
| 39 | −): Effect.Effect<typeof executeRunResultSchema.Type> => | |
| 18 | +): Effect.Effect<ExecuteRunResult> => | |
| 40 | 19 | Effect.gen(function* () { |
| 41 | 20 | const runExecutor = yield* ConvexRunExecutor; |
| 42 | 21 | return yield* runExecutor.executeRun(input); |
| 43 | 22 | }).pipe(Effect.provide(ConvexExecuteDependenciesLive)); |
| 44 | − | |
| 45 | −const executeRunEndpoint = factory.action({ | |
| 46 | − payload: { | |
| 47 | − code: Schema.String, | |
| 48 | − timeoutMs: Schema.optional(Schema.Number), | |
| 49 | − }, | |
| 50 | − success: executeRunResultSchema, | |
| 51 | −}); | |
| 52 | − | |
| 53 | −executeRunEndpoint.implement((payload) => | |
| 54 | − executeRunImpl({ | |
| 55 | − code: payload.code, | |
| 56 | − timeoutMs: payload.timeoutMs, | |
| 57 | − }), | |
| 58 | −); | |
| 59 | − | |
| 60 | −const executorRpc = makeRpcModule({ | |
| 61 | − executeRun: executeRunEndpoint, | |
| 62 | −}); | |
| 63 | − | |
| 64 | −export const executeRun = executorRpc.handlers.executeRun; | |
| @@ -1,24 +1,48 @@ | ||
| 1 | 1 | import { httpRouter } from "convex/server"; |
| 2 | + | |
| 3 | +import { controlPlaneHttpHandler } from "./controlPlane"; | |
| 2 | 4 | import { mcpHandler } from "./mcp"; |
| 3 | 5 | import { handleToolCallHttp } from "./runtimeCallbacks"; |
| 4 | 6 | |
| 5 | 7 | const http = httpRouter(); |
| 6 | 8 | |
| 7 | −http.route({ path: "/mcp", method: "POST", handler: mcpHandler }); | |
| 8 | −http.route({ path: "/mcp", method: "GET", handler: mcpHandler }); | |
| 9 | −http.route({ path: "/mcp", method: "DELETE", handler: mcpHandler }); | |
| 10 | 9 | http.route({ path: "/v1/mcp", method: "POST", handler: mcpHandler }); |
| 11 | 10 | http.route({ path: "/v1/mcp", method: "GET", handler: mcpHandler }); |
| 12 | 11 | http.route({ path: "/v1/mcp", method: "DELETE", handler: mcpHandler }); |
| 13 | 12 | http.route({ |
| 14 | − path: "/runtime/tool-call", | |
| 13 | + path: "/v1/runtime/tool-call", | |
| 15 | 14 | method: "POST", |
| 16 | 15 | handler: handleToolCallHttp, |
| 17 | 16 | }); |
| 18 | 17 | http.route({ |
| 19 | − path: "/v1/runtime/tool-call", | |
| 18 | + pathPrefix: "/v1/", | |
| 19 | + method: "GET", | |
| 20 | + handler: controlPlaneHttpHandler, | |
| 21 | +}); | |
| 22 | +http.route({ | |
| 23 | + pathPrefix: "/v1/", | |
| 20 | 24 | method: "POST", |
| 21 | − handler: handleToolCallHttp, | |
| 25 | + handler: controlPlaneHttpHandler, | |
| 26 | +}); | |
| 27 | +http.route({ | |
| 28 | + pathPrefix: "/v1/", | |
| 29 | + method: "PUT", | |
| 30 | + handler: controlPlaneHttpHandler, | |
| 31 | +}); | |
| 32 | +http.route({ | |
| 33 | + pathPrefix: "/v1/", | |
| 34 | + method: "PATCH", | |
| 35 | + handler: controlPlaneHttpHandler, | |
| 36 | +}); | |
| 37 | +http.route({ | |
| 38 | + pathPrefix: "/v1/", | |
| 39 | + method: "DELETE", | |
| 40 | + handler: controlPlaneHttpHandler, | |
| 41 | +}); | |
| 42 | +http.route({ | |
| 43 | + pathPrefix: "/v1/", | |
| 44 | + method: "OPTIONS", | |
| 45 | + handler: controlPlaneHttpHandler, | |
| 22 | 46 | }); |
| 23 | 47 | |
| 24 | 48 | export default http; |
| @@ -1,13 +1,13 @@ | ||
| 1 | 1 | import { handleMcpHttpRequest } from "@executor-v2/mcp-gateway"; |
| 2 | 2 | import { createExecutorRunClient } from "@executor-v2/sdk"; |
| 3 | −import { api } from "./_generated/api"; | |
| 3 | +import * as Effect from "effect/Effect"; | |
| 4 | + | |
| 4 | 5 | import { httpAction } from "./_generated/server"; |
| 5 | −import { unwrapRpcSuccess } from "./rpc_exit"; | |
| 6 | −export const mcpHandler = httpAction(async (ctx, request) => { | |
| 6 | +import { executeRunImpl } from "./executor"; | |
| 7 | + | |
| 8 | +export const mcpHandler = httpAction(async (_ctx, request) => { | |
| 7 | 9 | const runClient = createExecutorRunClient((input) => |
| 8 | − ctx | |
| 9 | − .runAction(api.executor.executeRun, input) | |
| 10 | − .then((result) => unwrapRpcSuccess(result, "executor.executeRun")), | |
| 10 | + Effect.runPromise(executeRunImpl(input)), | |
| 11 | 11 | ); |
| 12 | 12 | |
| 13 | 13 | return handleMcpHttpRequest(request, { |
| @@ -1,50 +0,0 @@ | ||
| 1 | −type RpcExitSuccess<T> = { | |
| 2 | − _tag: "Success"; | |
| 3 | − value: T; | |
| 4 | −}; | |
| 5 | − | |
| 6 | −type RpcExitFailure = { | |
| 7 | − _tag: "Failure"; | |
| 8 | − cause: unknown; | |
| 9 | −}; | |
| 10 | − | |
| 11 | −const isRpcExitSuccess = <T>(value: unknown): value is RpcExitSuccess<T> => | |
| 12 | − typeof value === "object" && | |
| 13 | − value !== null && | |
| 14 | − "_tag" in value && | |
| 15 | − value._tag === "Success" && | |
| 16 | − "value" in value; | |
| 17 | − | |
| 18 | −const isRpcExitFailure = (value: unknown): value is RpcExitFailure => | |
| 19 | − typeof value === "object" && | |
| 20 | − value !== null && | |
| 21 | − "_tag" in value && | |
| 22 | − value._tag === "Failure"; | |
| 23 | − | |
| 24 | −const renderUnknown = (value: unknown): string => { | |
| 25 | − if (value instanceof Error) { | |
| 26 | − return value.message; | |
| 27 | − } | |
| 28 | − | |
| 29 | − try { | |
| 30 | − return JSON.stringify(value); | |
| 31 | − } catch { | |
| 32 | − return String(value); | |
| 33 | − } | |
| 34 | −}; | |
| 35 | − | |
| 36 | −export const unwrapRpcSuccess = <T>(value: unknown, endpoint: string): T => { | |
| 37 | − if (isRpcExitSuccess<T>(value)) { | |
| 38 | − return value.value; | |
| 39 | − } | |
| 40 | − | |
| 41 | − if (isRpcExitFailure(value)) { | |
| 42 | − throw new Error( | |
| 43 | − `RPC endpoint '${endpoint}' returned failure exit: ${renderUnknown(value.cause)}`, | |
| 44 | − ); | |
| 45 | − } | |
| 46 | − | |
| 47 | − throw new Error( | |
| 48 | − `RPC endpoint '${endpoint}' returned invalid exit payload: ${renderUnknown(value)}`, | |
| 49 | − ); | |
| 50 | −}; | |
| @@ -1,14 +1,14 @@ | ||
| 1 | 1 | import { |
| 2 | + makeLocalInProcessRuntimeAdapter, | |
| 2 | 3 | makeToolProviderRegistry, |
| 3 | 4 | ToolProviderRegistryService, |
| 4 | −} from "@executor-v2/engine/tool-providers"; | |
| 5 | + type RuntimeExecuteError, | |
| 6 | +} from "@executor-v2/engine"; | |
| 5 | 7 | import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk"; |
| 6 | 8 | import * as Context from "effect/Context"; |
| 7 | 9 | import * as Effect from "effect/Effect"; |
| 8 | 10 | import * as Layer from "effect/Layer"; |
| 9 | 11 | |
| 10 | −import { makeLocalInProcessRuntimeAdapter } from "./runtime_adapter"; | |
| 11 | − | |
| 12 | 12 | export type ConvexRunExecutorService = { |
| 13 | 13 | executeRun: ( |
| 14 | 14 | input: ExecuteRunInput, |
| @@ -21,21 +21,16 @@ export class ConvexRunExecutor extends Context.Tag( · 6 unmodified lines | ||
| 21 | 21 | |
| 22 | 22 | const runtimeAdapter = makeLocalInProcessRuntimeAdapter(); |
| 23 | 23 | |
| 24 | −const errorToText = (error: unknown): string => { | |
| 25 | − if (error instanceof Error) { | |
| 26 | − return error.message; | |
| 27 | − } | |
| 28 | − | |
| 29 | − if ( | |
| 30 | − typeof error === "object" && | |
| 31 | − error !== null && | |
| 32 | − "message" in error && | |
| 33 | − typeof error.message === "string" | |
| 34 | − ) { | |
| 35 | − return error.message; | |
| 24 | +const formatRuntimeExecuteError = (error: RuntimeExecuteError): string => { | |
| 25 | + switch (error._tag) { | |
| 26 | + case "RuntimeAdapterError": | |
| 27 | + case "LocalCodeRunnerError": | |
| 28 | + case "DenoSubprocessRunnerError": | |
| 29 | + case "ToolProviderError": | |
| 30 | + return error.details ? `${error.message}: ${error.details}` : error.message; | |
| 31 | + case "ToolProviderRegistryError": | |
| 32 | + return error.message; | |
| 36 | 33 | } |
| 37 | − | |
| 38 | − return String(error); | |
| 39 | 34 | }; |
| 40 | 35 | |
| 41 | 36 | const executeRun = Effect.fn( |
| @@ -59,21 +54,21 @@ const executeRun = Effect.fn( · 17 unmodified lines | ||
| 59 | 54 | tools: [], |
| 60 | 55 | }) |
| 61 | 56 | .pipe( |
| 62 | − Effect.map( | |
| 63 | − (result): ExecuteRunResult => ({ | |
| 64 | − runId, | |
| 65 | − status: "completed", | |
| 66 | − result, | |
| 67 | − }), | |
| 68 | − ), | |
| 69 | − Effect.catchAll((error) => | |
| 70 | − Effect.succeed({ | |
| 71 | − runId, | |
| 72 | − status: "failed", | |
| 73 | − error: errorToText(error), | |
| 74 | − } satisfies ExecuteRunResult), | |
| 75 | − ), | |
| 76 | − ); | |
| 57 | + Effect.map( | |
| 58 | + (result): ExecuteRunResult => ({ | |
| 59 | + runId, | |
| 60 | + status: "completed", | |
| 61 | + result, | |
| 62 | + }), | |
| 63 | + ), | |
| 64 | + Effect.catchAll((error) => | |
| 65 | + Effect.succeed({ | |
| 66 | + runId, | |
| 67 | + status: "failed", | |
| 68 | + error: formatRuntimeExecuteError(error), | |
| 69 | + } satisfies ExecuteRunResult), | |
| 70 | + ), | |
| 71 | + ); | |
| 77 | 72 | }); |
| 78 | 73 | |
| 79 | 74 | export const ConvexRunExecutorLive = Layer.succeed( |
| @@ -1,27 +1,13 @@ | ||
| 1 | −import { createRpcFactory, makeRpcModule } from "@executor-v2/confect/rpc"; | |
| 2 | −import { executorConfectSchema } from "@executor-v2/persistence-convex"; | |
| 3 | 1 | import type { |
| 4 | 2 | RuntimeToolCallRequest, |
| 5 | 3 | RuntimeToolCallResult, |
| 6 | 4 | } from "@executor-v2/sdk"; |
| 5 | +import * as Data from "effect/Data"; | |
| 7 | 6 | import * as Effect from "effect/Effect"; |
| 7 | +import * as ParseResult from "effect/ParseResult"; | |
| 8 | 8 | import * as Schema from "effect/Schema"; |
| 9 | −import { api } from "./_generated/api"; | |
| 10 | −import { httpAction } from "./_generated/server"; | |
| 11 | −import { unwrapRpcSuccess } from "./rpc_exit"; | |
| 12 | − | |
| 13 | −const runtimeToolCallResultSchema = Schema.Struct({ | |
| 14 | − ok: Schema.Boolean, | |
| 15 | − kind: Schema.optional(Schema.String), | |
| 16 | − error: Schema.optional(Schema.String), | |
| 17 | − value: Schema.optional(Schema.Unknown), | |
| 18 | − approvalId: Schema.optional(Schema.String), | |
| 19 | − retryAfterMs: Schema.optional(Schema.Number), | |
| 20 | −}); | |
| 21 | 9 | |
| 22 | −const factory = createRpcFactory({ | |
| 23 | − schema: executorConfectSchema, | |
| 24 | −}); | |
| 10 | +import { httpAction } from "./_generated/server"; | |
| 25 | 11 | |
| 26 | 12 | export const handleToolCallImpl = ( |
| 27 | 13 | input: RuntimeToolCallRequest, |
| @@ -32,35 +18,26 @@ export const handleToolCallImpl = ( · 4 unmodified lines | ||
| 32 | 18 | error: `Convex runtime callback received tool '${input.toolPath}', but callback invocation is not wired yet.`, |
| 33 | 19 | }); |
| 34 | 20 | |
| 35 | −const handleToolCallEndpoint = factory.action({ | |
| 36 | − payload: { | |
| 37 | − runId: Schema.String, | |
| 38 | − callId: Schema.String, | |
| 39 | − toolPath: Schema.String, | |
| 40 | − input: Schema.optional( | |
| 41 | − Schema.Record({ | |
| 42 | − key: Schema.String, | |
| 43 | − value: Schema.Unknown, | |
| 44 | − }), | |
| 45 | − ), | |
| 46 | − }, | |
| 47 | − success: runtimeToolCallResultSchema, | |
| 48 | −}); | |
| 49 | − | |
| 50 | −handleToolCallEndpoint.implement((payload) => | |
| 51 | − handleToolCallImpl({ | |
| 52 | − runId: payload.runId, | |
| 53 | − callId: payload.callId, | |
| 54 | − toolPath: payload.toolPath, | |
| 55 | − input: payload.input, | |
| 56 | − }), | |
| 57 | −); | |
| 21 | +class RuntimeToolCallBadRequestError extends Data.TaggedError( | |
| 22 | + "RuntimeToolCallBadRequestError", | |
| 23 | +)<{ | |
| 24 | + message: string; | |
| 25 | + details: string; | |
| 26 | +}> {} | |
| 58 | 27 | |
| 59 | −const runtimeCallbacksRpc = makeRpcModule({ | |
| 60 | − handleToolCall: handleToolCallEndpoint, | |
| 28 | +const RuntimeToolCallRequestSchema = Schema.Struct({ | |
| 29 | + runId: Schema.String, | |
| 30 | + callId: Schema.String, | |
| 31 | + toolPath: Schema.String, | |
| 32 | + input: Schema.optional( | |
| 33 | + Schema.Record({ | |
| 34 | + key: Schema.String, | |
| 35 | + value: Schema.Unknown, | |
| 36 | + }), | |
| 37 | + ), | |
| 61 | 38 | }); |
| 62 | 39 | |
| 63 | −export const handleToolCall = runtimeCallbacksRpc.handlers.handleToolCall; | |
| 40 | +const decodeRuntimeToolCallRequest = Schema.decodeUnknown(RuntimeToolCallRequestSchema); | |
| 64 | 41 | |
| 65 | 42 | const badRequest = (message: string): Response => |
| 66 | 43 | Response.json( |
| @@ -72,48 +49,42 @@ const badRequest = (message: string): Response => · 5 unmodified lines | ||
| 72 | 49 | { status: 400 }, |
| 73 | 50 | ); |
| 74 | 51 | |
| 75 | −export const handleToolCallHttp = httpAction(async (ctx, request) => { | |
| 76 | − let body: unknown; | |
| 77 | − try { | |
| 78 | − body = await request.json(); | |
| 79 | − } catch (cause) { | |
| 80 | − return badRequest( | |
| 81 | − `Invalid runtime callback request body: ${ | |
| 82 | − cause instanceof Error ? cause.message : String(cause) | |
| 83 | − }`, | |
| 84 | − ); | |
| 85 | − } | |
| 86 | − | |
| 87 | − if (typeof body !== "object" || body === null) { | |
| 88 | − return badRequest("Runtime callback request body must be an object"); | |
| 89 | − } | |
| 52 | +const formatBadRequestMessage = (error: RuntimeToolCallBadRequestError): string => | |
| 53 | + error.details.length > 0 ? `${error.message}: ${error.details}` : error.message; | |
| 90 | 54 | |
| 91 | − const payload = body as Partial<RuntimeToolCallRequest>; | |
| 55 | +const formatUnknownDetails = (cause: unknown): string => String(cause); | |
| 92 | 56 | |
| 93 | − if ( | |
| 94 | − typeof payload.runId !== "string" || | |
| 95 | − typeof payload.callId !== "string" || | |
| 96 | − typeof payload.toolPath !== "string" | |
| 97 | − ) { | |
| 98 | − return badRequest("Runtime callback request body is missing required fields"); | |
| 99 | − } | |
| 57 | +const handleToolCallHttpEffect = ( | |
| 58 | + request: Request, | |
| 59 | +): Effect.Effect<Response, never> => | |
| 60 | + Effect.gen(function* () { | |
| 61 | + const body = yield* Effect.tryPromise({ | |
| 62 | + try: () => request.json(), | |
| 63 | + catch: (cause) => | |
| 64 | + new RuntimeToolCallBadRequestError({ | |
| 65 | + message: "Invalid runtime callback request body", | |
| 66 | + details: formatUnknownDetails(cause), | |
| 67 | + }), | |
| 68 | + }); | |
| 100 | 69 | |
| 101 | − let result: RuntimeToolCallResult; | |
| 102 | − try { | |
| 103 | − result = unwrapRpcSuccess( | |
| 104 | − await ctx.runAction(api.runtimeCallbacks.handleToolCall, { | |
| 105 | − runId: payload.runId, | |
| 106 | − callId: payload.callId, | |
| 107 | − toolPath: payload.toolPath, | |
| 108 | − input: | |
| 109 | − payload.input && typeof payload.input === "object" | |
| 110 | − ? (payload.input as Record<string, unknown>) | |
| 111 | − : undefined, | |
| 112 | − }), | |
| 113 | − "runtimeCallbacks.handleToolCall", | |
| 70 | + const input = yield* decodeRuntimeToolCallRequest(body).pipe( | |
| 71 | + Effect.mapError( | |
| 72 | + (cause) => | |
| 73 | + new RuntimeToolCallBadRequestError({ | |
| 74 | + message: "Runtime callback request body is invalid", | |
| 75 | + details: ParseResult.TreeFormatter.formatErrorSync(cause), | |
| 76 | + }), | |
| 77 | + ), | |
| 114 | 78 | ); |
| 115 | − } catch (cause) { | |
| 116 | − return badRequest(cause instanceof Error ? cause.message : String(cause)); | |
| 117 | − } | |
| 118 | − return Response.json(result, { status: 200 }); | |
| 119 | −}); | |
| 79 | + | |
| 80 | + const result = yield* handleToolCallImpl(input); | |
| 81 | + return Response.json(result, { status: 200 }); | |
| 82 | + }).pipe( | |
| 83 | + Effect.catchTag("RuntimeToolCallBadRequestError", (error) => | |
| 84 | + Effect.succeed(badRequest(formatBadRequestMessage(error))), | |
| 85 | + ), | |
| 86 | + ); | |
| 87 | + | |
| 88 | +export const handleToolCallHttp = httpAction((_ctx, request) => | |
| 89 | + Effect.runPromise(handleToolCallHttpEffect(request)), | |
| 90 | +); | |
| @@ -1,3 +1,58 @@ | ||
| 1 | −import { executorConfectSchema } from "@executor-v2/persistence-convex"; | |
| 1 | +import { defineSchema, defineTable, type TablesFromSchemaDefinition } from "@executor-v2/confect"; | |
| 2 | +import { | |
| 3 | + ApprovalSchema, | |
| 4 | + CredentialRefSchema, | |
| 5 | + EventEnvelopeSchema, | |
| 6 | + OAuthTokenSchema, | |
| 7 | + PolicySchema, | |
| 8 | + ProfileSchema, | |
| 9 | + SourceSchema, | |
| 10 | + SyncStateSchema, | |
| 11 | + TaskRunSchema, | |
| 12 | + ToolArtifactSchema, | |
| 13 | + WorkspaceSchema, | |
| 14 | +} from "@executor-v2/schema"; | |
| 15 | + | |
| 16 | +export const executorConfectSchema = defineSchema({ | |
| 17 | + profiles: defineTable(ProfileSchema).index("by_domainId", ["id"]), | |
| 18 | + workspaces: defineTable(WorkspaceSchema) | |
| 19 | + .index("by_domainId", ["id"]) | |
| 20 | + .index("by_profileId", ["profileId"]), | |
| 21 | + sources: defineTable(SourceSchema) | |
| 22 | + .index("by_domainId", ["id"]) | |
| 23 | + .index("by_workspaceId", ["workspaceId"]), | |
| 24 | + toolArtifacts: defineTable(ToolArtifactSchema) | |
| 25 | + .index("by_domainId", ["id"]) | |
| 26 | + .index("by_workspaceId", ["workspaceId"]) | |
| 27 | + .index("by_sourceId", ["sourceId"]), | |
| 28 | + credentialRefs: defineTable(CredentialRefSchema) | |
| 29 | + .index("by_domainId", ["id"]) | |
| 30 | + .index("by_workspaceId", ["workspaceId"]) | |
| 31 | + .index("by_sourceId", ["sourceId"]), | |
| 32 | + oauthTokens: defineTable(OAuthTokenSchema) | |
| 33 | + .index("by_domainId", ["id"]) | |
| 34 | + .index("by_workspaceId", ["workspaceId"]) | |
| 35 | + .index("by_sourceId", ["sourceId"]), | |
| 36 | + policies: defineTable(PolicySchema) | |
| 37 | + .index("by_domainId", ["id"]) | |
| 38 | + .index("by_workspaceId", ["workspaceId"]), | |
| 39 | + approvals: defineTable(ApprovalSchema) | |
| 40 | + .index("by_domainId", ["id"]) | |
| 41 | + .index("by_workspaceId", ["workspaceId"]) | |
| 42 | + .index("by_taskRunId", ["taskRunId"]), | |
| 43 | + taskRuns: defineTable(TaskRunSchema) | |
| 44 | + .index("by_domainId", ["id"]) | |
| 45 | + .index("by_workspaceId", ["workspaceId"]) | |
| 46 | + .index("by_sessionId", ["sessionId"]), | |
| 47 | + syncStates: defineTable(SyncStateSchema) | |
| 48 | + .index("by_domainId", ["id"]) | |
| 49 | + .index("by_workspaceId", ["workspaceId"]), | |
| 50 | + events: defineTable(EventEnvelopeSchema) | |
| 51 | + .index("by_domainId", ["id"]) | |
| 52 | + .index("by_workspaceId", ["workspaceId"]) | |
| 53 | + .index("by_workspaceId_sequence", ["workspaceId", "sequence"]), | |
| 54 | +}); | |
| 55 | + | |
| 56 | +export type ExecutorConfectTables = TablesFromSchemaDefinition<typeof executorConfectSchema>; | |
| 2 | 57 | |
| 3 | 58 | export default executorConfectSchema.convexSchemaDefinition; |
| @@ -10,9 +10,10 @@ · 9 unmodified lines | ||
| 10 | 10 | }, |
| 11 | 11 | "dependencies": { |
| 12 | 12 | "@executor-v2/confect": "workspace:*", |
| 13 | + "@executor-v2/control-plane": "workspace:*", | |
| 13 | 14 | "@executor-v2/engine": "workspace:*", |
| 14 | 15 | "@executor-v2/mcp-gateway": "workspace:*", |
| 15 | − "@executor-v2/persistence-convex": "workspace:*", | |
| 16 | + "@executor-v2/schema": "workspace:*", | |
| 16 | 17 | "@executor-v2/sdk": "workspace:*", |
| 17 | 18 | "convex": "catalog:", |
| 18 | 19 | "effect": "catalog:" |
| @@ -3,11 +3,14 @@ · 2 unmodified lines | ||
| 3 | 3 | Local Process Manager app scaffold for Executor v2. |
| 4 | 4 | |
| 5 | 5 | Current scaffold includes: |
| 6 | −- local MCP endpoint at `POST /mcp` | |
| 6 | +- local MCP endpoint at `GET/POST/DELETE /v1/mcp` | |
| 7 | 7 | - health endpoint at `GET /healthz` |
| 8 | 8 | - MCP tool routing via `@executor-v2/mcp-gateway` |
| 9 | 9 | - local runtime adapter injection (`makeLocalInProcessRuntimeAdapter()` in entry wiring) |
| 10 | − runtime callback endpoint at `POST /runtime/tool-call` | |
| 10 | + runtime callback endpoint at `POST /v1/runtime/tool-call` | |
| 11 | +- control-plane source endpoints at `GET/POST /v1/workspaces/:workspaceId/sources` | |
| 12 | +- control-plane source removal endpoint at `DELETE /v1/workspaces/:workspaceId/sources/:sourceId` | |
| 13 | +- generated OpenAPI spec endpoint at `GET /v1/openapi.json` | |
| 11 | 14 | |
| 12 | 15 | App wiring is now split by responsibility: |
| 13 | 16 | - `src/config.ts`: Effect config service (`PORT`) |
| @@ -11,9 +11,11 @@ · 10 unmodified lines | ||
| 11 | 11 | "dependencies": { |
| 12 | 12 | "@effect/platform": "catalog:", |
| 13 | 13 | "@effect/platform-bun": "^0.86.0", |
| 14 | + "@executor-v2/control-plane": "workspace:*", | |
| 14 | 15 | "@executor-v2/engine": "workspace:*", |
| 15 | 16 | "@executor-v2/mcp-gateway": "workspace:*", |
| 16 | 17 | "@executor-v2/sdk": "workspace:*", |
| 18 | + "@executor-v2/persistence-local": "workspace:*", | |
| 17 | 19 | "effect": "catalog:" |
| 18 | 20 | }, |
| 19 | 21 | "devDependencies": { |
| @@ -109,7 +109,7 @@ const withMcpClient = (port: number) => · 108 unmodified lines | ||
| 109 | 109 | Effect.tryPromise({ |
| 110 | 110 | try: async () => { |
| 111 | 111 | const transport = new StreamableHTTPClientTransport( |
| 112 | − new URL(`http://127.0.0.1:${port}/mcp`), | |
| 112 | + new URL(`http://127.0.0.1:${port}/v1/mcp`), | |
| 113 | 113 | ); |
| 114 | 114 | const client = new Client( |
| 115 | 115 | { name: "executor-v2-pm-e2e", version: "0.0.0" }, |
| @@ -5,6 +5,11 @@ import { · 4 unmodified lines | ||
| 5 | 5 | HttpServerResponse, |
| 6 | 6 | } from "@effect/platform"; |
| 7 | 7 | import { BunHttpServer, BunHttpServerRequest } from "@effect/platform-bun"; |
| 8 | +import { | |
| 9 | + ControlPlaneService, | |
| 10 | + controlPlaneOpenApiSpec, | |
| 11 | + makeControlPlaneWebHandler, | |
| 12 | +} from "@executor-v2/control-plane"; | |
| 8 | 13 | import * as Effect from "effect/Effect"; |
| 9 | 14 | import * as Layer from "effect/Layer"; |
| 10 | 15 | |
| @@ -25,17 +30,33 @@ const fromWebHandler = (handler: (request: Request) => Promise<Response>) => · 14 unmodified lines | ||
| 25 | 30 | export const startPmHttpServer = Effect.fn("@executor-v2/app-pm/http.start")(function* () { |
| 26 | 31 | const { port } = yield* PmConfig; |
| 27 | 32 | const { handleMcp } = yield* PmMcpHandler; |
| 33 | + const controlPlaneService = yield* ControlPlaneService; | |
| 34 | + const controlPlaneWebHandler = yield* Effect.sync(() => | |
| 35 | + makeControlPlaneWebHandler( | |
| 36 | + Layer.succeed(ControlPlaneService, controlPlaneService), | |
| 37 | + ), | |
| 38 | + ); | |
| 39 | + | |
| 40 | + yield* Effect.addFinalizer(() => | |
| 41 | + Effect.promise(() => controlPlaneWebHandler.dispose()), | |
| 42 | + ); | |
| 28 | 43 | |
| 29 | 44 | const httpLive = HttpRouter.empty.pipe( |
| 30 | 45 | HttpRouter.get("/healthz", HttpServerResponse.json({ ok: true, service: "pm" })), |
| 31 | − HttpRouter.get("/mcp", fromWebHandler(handleMcp)), | |
| 32 | − HttpRouter.post("/mcp", fromWebHandler(handleMcp)), | |
| 33 | − HttpRouter.del("/mcp", fromWebHandler(handleMcp)), | |
| 34 | 46 | HttpRouter.get("/v1/mcp", fromWebHandler(handleMcp)), |
| 35 | 47 | HttpRouter.post("/v1/mcp", fromWebHandler(handleMcp)), |
| 36 | 48 | HttpRouter.del("/v1/mcp", fromWebHandler(handleMcp)), |
| 37 | − HttpRouter.post("/runtime/tool-call", handleToolCallHttp), | |
| 38 | 49 | HttpRouter.post("/v1/runtime/tool-call", handleToolCallHttp), |
| 50 | + HttpRouter.get("/v1/workspaces/:workspaceId/sources", fromWebHandler(controlPlaneWebHandler.handler)), | |
| 51 | + HttpRouter.post("/v1/workspaces/:workspaceId/sources", fromWebHandler(controlPlaneWebHandler.handler)), | |
| 52 | + HttpRouter.del( | |
| 53 | + "/v1/workspaces/:workspaceId/sources/:sourceId", | |
| 54 | + fromWebHandler(controlPlaneWebHandler.handler), | |
| 55 | + ), | |
| 56 | + HttpRouter.get( | |
| 57 | + "/v1/openapi.json", | |
| 58 | + HttpServerResponse.unsafeJson(controlPlaneOpenApiSpec), | |
| 59 | + ), | |
| 39 | 60 | HttpServer.serve(), |
| 40 | 61 | HttpServer.withLogAddress, |
| 41 | 62 | Layer.provide(BunHttpServer.layer({ port })), |
| @@ -1,4 +1,7 @@ | ||
| 1 | +import * as BunContext from "@effect/platform-bun/BunContext"; | |
| 2 | +import { ControlPlaneServiceLive } from "@executor-v2/control-plane"; | |
| 1 | 3 | import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/engine"; |
| 4 | +import { LocalSourceStoreLive } from "@executor-v2/persistence-local"; | |
| 2 | 5 | |
| 3 | 6 | import * as Effect from "effect/Effect"; |
| 4 | 7 | import * as Layer from "effect/Layer"; |
| @@ -18,12 +21,21 @@ const PmMcpDependenciesLive = Layer.merge( · 13 unmodified lines | ||
| 18 | 21 | PmToolProviderRegistryLive, |
| 19 | 22 | ); |
| 20 | 23 | |
| 24 | +const PmControlPlaneDependenciesLive = ControlPlaneServiceLive.pipe( | |
| 25 | + Layer.provide( | |
| 26 | + LocalSourceStoreLive({ | |
| 27 | + rootDir: process.env.PM_STATE_ROOT_DIR ?? ".executor-v2/pm-state", | |
| 28 | + }).pipe(Layer.provide(BunContext.layer)), | |
| 29 | + ), | |
| 30 | +); | |
| 31 | + | |
| 21 | 32 | const PmAppLive = Layer.mergeAll( |
| 22 | 33 | PmConfigLive, |
| 23 | 34 | PmMcpHandlerLive.pipe(Layer.provide(PmMcpDependenciesLive)), |
| 24 | 35 | PmToolCallHandlerLive, |
| 36 | + PmControlPlaneDependenciesLive, | |
| 25 | 37 | ); |
| 26 | 38 | |
| 27 | −const program = startPmHttpServer().pipe(Effect.provide(PmAppLive)); | |
| 39 | +const program = Effect.scoped(startPmHttpServer()).pipe(Effect.provide(PmAppLive)); | |
| 28 | 40 | |
| 29 | 41 | await Effect.runPromise(program); |
| @@ -2,6 +2,7 @@ import { · 1 unmodified line | ||
| 2 | 2 | makeToolProviderRegistry, |
| 3 | 3 | ToolProviderRegistryService, |
| 4 | 4 | type RuntimeAdapter, |
| 5 | + type RuntimeExecuteError, | |
| 5 | 6 | } from "@executor-v2/engine"; |
| 6 | 7 | import type { ExecuteRunInput, ExecuteRunResult } from "@executor-v2/sdk"; |
| 7 | 8 | import * as Context from "effect/Context"; |
| @@ -19,21 +20,16 @@ export class PmRunExecutor extends Context.Tag("@executor-v2/app-pm/PmRunExecuto · 11 unmodified lines | ||
| 19 | 20 | PmRunExecutorService |
| 20 | 21 | >() {} |
| 21 | 22 | |
| 22 | −const errorToText = (error: unknown): string => { | |
| 23 | − if (error instanceof Error) { | |
| 24 | − return error.message; | |
| 23 | +const formatRuntimeExecuteError = (error: RuntimeExecuteError): string => { | |
| 24 | + switch (error._tag) { | |
| 25 | + case "RuntimeAdapterError": | |
| 26 | + case "LocalCodeRunnerError": | |
| 27 | + case "DenoSubprocessRunnerError": | |
| 28 | + case "ToolProviderError": | |
| 29 | + return error.details ? `${error.message}: ${error.details}` : error.message; | |
| 30 | + case "ToolProviderRegistryError": | |
| 31 | + return error.message; | |
| 25 | 32 | } |
| 26 | − | |
| 27 | − if ( | |
| 28 | − typeof error === "object" && | |
| 29 | − error !== null && | |
| 30 | − "message" in error && | |
| 31 | − typeof error.message === "string" | |
| 32 | − ) { | |
| 33 | − return error.message; | |
| 34 | − } | |
| 35 | − | |
| 36 | − return String(error); | |
| 37 | 33 | }; |
| 38 | 34 | |
| 39 | 35 | const makeExecuteRun = (runtimeAdapter: RuntimeAdapter) => |
| @@ -69,7 +65,7 @@ const makeExecuteRun = (runtimeAdapter: RuntimeAdapter) => · 29 unmodified lines | ||
| 69 | 65 | Effect.succeed({ |
| 70 | 66 | runId, |
| 71 | 67 | status: "failed", |
| 72 | − error: errorToText(error), | |
| 68 | + error: formatRuntimeExecuteError(error), | |
| 73 | 69 | } satisfies ExecuteRunResult), |
| 74 | 70 | ), |
| 75 | 71 | ); |
| @@ -7,6 +7,7 @@ import * as Context from "effect/Context"; · 6 unmodified lines | ||
| 7 | 7 | import * as Data from "effect/Data"; |
| 8 | 8 | import * as Effect from "effect/Effect"; |
| 9 | 9 | import * as Layer from "effect/Layer"; |
| 10 | +import * as ParseResult from "effect/ParseResult"; | |
| 10 | 11 | import * as Schema from "effect/Schema"; |
| 11 | 12 | |
| 12 | 13 | export type PmToolCallHandlerService = { |
| @@ -37,29 +38,25 @@ const RuntimeToolCallRequestSchema = Schema.Struct({ · 24 unmodified lines | ||
| 37 | 38 | |
| 38 | 39 | const decodeRuntimeToolCallRequest = Schema.decodeUnknown(RuntimeToolCallRequestSchema); |
| 39 | 40 | |
| 40 | −const errorToText = (error: unknown): string => { | |
| 41 | − if (error instanceof Error) { | |
| 42 | − return error.message; | |
| 43 | − } | |
| 44 | − | |
| 45 | − if ( | |
| 46 | − typeof error === "object" && | |
| 47 | − error !== null && | |
| 48 | − "message" in error && | |
| 49 | − typeof error.message === "string" | |
| 50 | − ) { | |
| 51 | − return error.message; | |
| 52 | − } | |
| 53 | − | |
| 54 | − return String(error); | |
| 55 | −}; | |
| 56 | − | |
| 57 | 41 | const decodeRequestBodyError = (cause: unknown): PmToolCallHttpRequestError => |
| 58 | 42 | new PmToolCallHttpRequestError({ |
| 59 | 43 | message: "Invalid runtime callback request body", |
| 60 | − details: cause instanceof Error ? cause.message : String(cause), | |
| 44 | + details: String(cause), | |
| 61 | 45 | }); |
| 62 | 46 | |
| 47 | +const decodeRequestPayloadError = (cause: unknown): PmToolCallHttpRequestError => | |
| 48 | + new PmToolCallHttpRequestError({ | |
| 49 | + message: "Runtime callback request body is invalid", | |
| 50 | + details: ParseResult.isParseError(cause) | |
| 51 | + ? ParseResult.TreeFormatter.formatErrorSync(cause) | |
| 52 | + : String(cause), | |
| 53 | + }); | |
| 54 | + | |
| 55 | +const formatHttpRequestError = (error: PmToolCallHttpRequestError): string => | |
| 56 | + error.details && error.details.length > 0 | |
| 57 | + ? `${error.message}: ${error.details}` | |
| 58 | + : error.message; | |
| 59 | + | |
| 63 | 60 | const handleToolCall = Effect.fn("@executor-v2/app-pm/tool-call.handle")(function* ( |
| 64 | 61 | input: RuntimeToolCallRequest, |
| 65 | 62 | ) { |
| @@ -82,24 +79,26 @@ export const handleToolCallBody = Effect.fn( · 16 unmodified lines | ||
| 82 | 79 | )(function* (body: unknown) { |
| 83 | 80 | const handler = yield* PmToolCallHandler; |
| 84 | 81 | const input = yield* decodeRuntimeToolCallRequest(body).pipe( |
| 85 | − Effect.mapError(decodeRequestBodyError), | |
| 82 | + Effect.mapError(decodeRequestPayloadError), | |
| 86 | 83 | ); |
| 87 | 84 | |
| 88 | 85 | return yield* handler.handleToolCall(input); |
| 89 | 86 | }); |
| 90 | 87 | |
| 91 | 88 | export const handleToolCallHttp = Effect.gen(function* () { |
| 92 | − const body = yield* HttpServerRequest.schemaBodyJson(Schema.Unknown); | |
| 89 | + const body = yield* HttpServerRequest.schemaBodyJson(Schema.Unknown).pipe( | |
| 90 | + Effect.mapError(decodeRequestBodyError), | |
| 91 | + ); | |
| 93 | 92 | const result = yield* handleToolCallBody(body); |
| 94 | 93 | |
| 95 | 94 | return yield* HttpServerResponse.json(result, { status: 200 }); |
| 96 | 95 | }).pipe( |
| 97 | − Effect.catchAll((error) => | |
| 96 | + Effect.catchTag("PmToolCallHttpRequestError", (error) => | |
| 98 | 97 | HttpServerResponse.json( |
| 99 | 98 | { |
| 100 | 99 | ok: false, |
| 101 | 100 | kind: "failed", |
| 102 | − error: errorToText(error), | |
| 101 | + error: formatHttpRequestError(error), | |
| 103 | 102 | } satisfies RuntimeToolCallResult, |
| 104 | 103 | { status: 400 }, |
| 105 | 104 | ), |
| @@ -21,9 +21,10 @@ · 20 unmodified lines | ||
| 21 | 21 | "name": "@executor-v2/app-convex", |
| 22 | 22 | "dependencies": { |
| 23 | 23 | "@executor-v2/confect": "workspace:*", |
| 24 | + "@executor-v2/control-plane": "workspace:*", | |
| 24 | 25 | "@executor-v2/engine": "workspace:*", |
| 25 | 26 | "@executor-v2/mcp-gateway": "workspace:*", |
| 26 | − "@executor-v2/persistence-convex": "workspace:*", | |
| 27 | + "@executor-v2/schema": "workspace:*", | |
| 27 | 28 | "@executor-v2/sdk": "workspace:*", |
| 28 | 29 | "convex": "catalog:", |
| 29 | 30 | "effect": "catalog:", |
| @@ -39,8 +40,10 @@ · 9 unmodified lines | ||
| 39 | 40 | "dependencies": { |
| 40 | 41 | "@effect/platform": "catalog:", |
| 41 | 42 | "@effect/platform-bun": "^0.86.0", |
| 43 | + "@executor-v2/control-plane": "workspace:*", | |
| 42 | 44 | "@executor-v2/engine": "workspace:*", |
| 43 | 45 | "@executor-v2/mcp-gateway": "workspace:*", |
| 46 | + "@executor-v2/persistence-local": "workspace:*", | |
| 44 | 47 | "@executor-v2/sdk": "workspace:*", |
| 45 | 48 | "effect": "catalog:", |
| 46 | 49 | }, |
| @@ -87,6 +90,17 @@ · 40 unmodified lines | ||
| 87 | 90 | "vitest": "catalog:", |
| 88 | 91 | }, |
| 89 | 92 | }, |
| 93 | + "packages/control-plane": { | |
| 94 | + "name": "@executor-v2/control-plane", | |
| 95 | + "dependencies": { | |
| 96 | + "@effect-atom/atom": "^0.4.11", | |
| 97 | + "@effect/platform": "catalog:", | |
| 98 | + "@executor-v2/persistence-ports": "workspace:*", | |
| 99 | + "@executor-v2/schema": "workspace:*", | |
| 100 | + "@executor-v2/source-manager": "workspace:*", | |
| 101 | + "effect": "catalog:", | |
| 102 | + }, | |
| 103 | + }, | |
| 90 | 104 | "packages/domain": { |
| 91 | 105 | "name": "@executor-v2/domain", |
| 92 | 106 | }, |
| @@ -114,9 +128,6 @@ · 21 unmodified lines | ||
| 114 | 128 | "packages/oauth": { |
| 115 | 129 | "name": "@executor-v2/oauth", |
| 116 | 130 | }, |
| 117 | − "packages/rpc": { | |
| 118 | − "name": "@executor-v2/rpc", | |
| 119 | − }, | |
| 120 | 131 | "packages/schema": { |
| 121 | 132 | "name": "@executor-v2/schema", |
| 122 | 133 | "dependencies": { |
| @@ -148,15 +159,6 @@ · 25 unmodified lines | ||
| 148 | 159 | "effect": "catalog:", |
| 149 | 160 | }, |
| 150 | 161 | }, |
| 151 | − "packages/state-convex": { | |
| 152 | − "name": "@executor-v2/persistence-convex", | |
| 153 | − "dependencies": { | |
| 154 | − "@executor-v2/confect": "workspace:*", | |
| 155 | − "@executor-v2/persistence-ports": "workspace:*", | |
| 156 | − "@executor-v2/schema": "workspace:*", | |
| 157 | − "effect": "catalog:", | |
| 158 | − }, | |
| 159 | − }, | |
| 160 | 162 | "packages/state-local-file": { |
| 161 | 163 | "name": "@executor-v2/persistence-local", |
| 162 | 164 | "dependencies": { |
| @@ -300,6 +302,8 @@ · 137 unmodified lines | ||
| 300 | 302 | |
| 301 | 303 | "@executor-v2/confect": ["@executor-v2/confect@workspace:packages/confect"], |
| 302 | 304 | |
| 305 | + "@executor-v2/control-plane": ["@executor-v2/control-plane@workspace:packages/control-plane"], | |
| 306 | + | |
| 303 | 307 | "@executor-v2/domain": ["@executor-v2/domain@workspace:packages/domain"], |
| 304 | 308 | |
| 305 | 309 | "@executor-v2/engine": ["@executor-v2/engine@workspace:packages/engine"], |
| @@ -308,14 +312,10 @@ · 2 unmodified lines | ||
| 308 | 312 | |
| 309 | 313 | "@executor-v2/oauth": ["@executor-v2/oauth@workspace:packages/oauth"], |
| 310 | 314 | |
| 311 | − "@executor-v2/persistence-convex": ["@executor-v2/persistence-convex@workspace:packages/state-convex"], | |
| 312 | − | |
| 313 | 315 | "@executor-v2/persistence-local": ["@executor-v2/persistence-local@workspace:packages/state-local-file"], |
| 314 | 316 | |
| 315 | 317 | "@executor-v2/persistence-ports": ["@executor-v2/persistence-ports@workspace:packages/state-contracts"], |
| 316 | 318 | |
| 317 | − "@executor-v2/rpc": ["@executor-v2/rpc@workspace:packages/rpc"], | |
| 318 | − | |
| 319 | 319 | "@executor-v2/schema": ["@executor-v2/schema@workspace:packages/schema"], |
| 320 | 320 | |
| 321 | 321 | "@executor-v2/sdk": ["@executor-v2/sdk@workspace:packages/sdk"], |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | { |
| 2 | − "name": "@executor-v2/persistence-convex", | |
| 2 | + "name": "@executor-v2/control-plane", | |
| 3 | 3 | "private": true, |
| 4 | 4 | "type": "module", |
| 5 | 5 | "exports": { |
| @@ -9,9 +9,11 @@ · 3 unmodified lines | ||
| 9 | 9 | "typecheck": "bunx tsc --noEmit -p tsconfig.json" |
| 10 | 10 | }, |
| 11 | 11 | "dependencies": { |
| 12 | − "@executor-v2/confect": "workspace:*", | |
| 13 | − "@executor-v2/schema": "workspace:*", | |
| 12 | + "@effect-atom/atom": "^0.4.11", | |
| 13 | + "@effect/platform": "catalog:", | |
| 14 | 14 | "@executor-v2/persistence-ports": "workspace:*", |
| 15 | + "@executor-v2/schema": "workspace:*", | |
| 16 | + "@executor-v2/source-manager": "workspace:*", | |
| 15 | 17 | "effect": "catalog:" |
| 16 | 18 | } |
| 17 | −} | |
| 19 | +} | |
| @@ -0,0 +1,26 @@ | ||
| 1 | +import { HttpApi, OpenApi } from "@effect/platform"; | |
| 2 | + | |
| 3 | +import { SourcesApi } from "./sources/api"; | |
| 4 | + | |
| 5 | +export { | |
| 6 | + ControlPlaneBadRequestError, | |
| 7 | + ControlPlaneStorageError, | |
| 8 | +} from "./errors"; | |
| 9 | + | |
| 10 | +export { | |
| 11 | + RemoveSourceResultSchema, | |
| 12 | + UpsertSourcePayloadSchema, | |
| 13 | + type RemoveSourceResult, | |
| 14 | + type UpsertSourcePayload, | |
| 15 | +} from "./sources/api"; | |
| 16 | + | |
| 17 | +export class ControlPlaneApi extends HttpApi.make("controlPlane") | |
| 18 | + .add(SourcesApi) | |
| 19 | + .annotateContext( | |
| 20 | + OpenApi.annotations({ | |
| 21 | + title: "Executor v2 Control Plane API", | |
| 22 | + description: "Backend-agnostic control plane API", | |
| 23 | + }), | |
| 24 | + ) {} | |
| 25 | + | |
| 26 | +export const controlPlaneOpenApiSpec = OpenApi.fromApi(ControlPlaneApi); | |
| @@ -0,0 +1,159 @@ | ||
| 1 | +import { Atom, Result } from "@effect-atom/atom"; | |
| 2 | +import { | |
| 3 | + FetchHttpClient, | |
| 4 | + HttpApiClient, | |
| 5 | + HttpApiError, | |
| 6 | + HttpClientError, | |
| 7 | +} from "@effect/platform"; | |
| 8 | +import type { | |
| 9 | + Source, | |
| 10 | + SourceId, | |
| 11 | + WorkspaceId, | |
| 12 | +} from "@executor-v2/schema"; | |
| 13 | +import * as Effect from "effect/Effect"; | |
| 14 | +import * as ParseResult from "effect/ParseResult"; | |
| 15 | + | |
| 16 | +import { | |
| 17 | + ControlPlaneApi, | |
| 18 | + type ControlPlaneBadRequestError, | |
| 19 | + type ControlPlaneStorageError, | |
| 20 | + type RemoveSourceResult, | |
| 21 | + type UpsertSourcePayload, | |
| 22 | +} from "./api"; | |
| 23 | + | |
| 24 | +export type ControlPlaneClientOptions = { | |
| 25 | + baseUrl: string; | |
| 26 | +}; | |
| 27 | + | |
| 28 | +export type ControlPlaneClientError = | |
| 29 | + | ControlPlaneBadRequestError | |
| 30 | + | ControlPlaneStorageError | |
| 31 | + | HttpApiError.HttpApiDecodeError | |
| 32 | + | HttpClientError.HttpClientError | |
| 33 | + | ParseResult.ParseError; | |
| 34 | + | |
| 35 | +export type ListSourcesRequest = { | |
| 36 | + workspaceId: WorkspaceId; | |
| 37 | +}; | |
| 38 | + | |
| 39 | +export type UpsertSourceRequest = { | |
| 40 | + workspaceId: WorkspaceId; | |
| 41 | + payload: UpsertSourcePayload; | |
| 42 | +}; | |
| 43 | + | |
| 44 | +export type RemoveSourceRequest = { | |
| 45 | + workspaceId: WorkspaceId; | |
| 46 | + sourceId: SourceId; | |
| 47 | +}; | |
| 48 | + | |
| 49 | +export type ControlPlaneReadOptions = { | |
| 50 | + enabled?: boolean; | |
| 51 | +}; | |
| 52 | + | |
| 53 | +export const makeControlPlaneClient = (options: ControlPlaneClientOptions) => | |
| 54 | + HttpApiClient.make(ControlPlaneApi, { | |
| 55 | + baseUrl: options.baseUrl, | |
| 56 | + }); | |
| 57 | + | |
| 58 | +const isReadEnabled = (options?: ControlPlaneReadOptions): boolean => | |
| 59 | + options?.enabled ?? true; | |
| 60 | + | |
| 61 | +const makeDisabledResultAtom = <Success, Error>() => | |
| 62 | + Atom.make(Result.initial<Success, Error>()); | |
| 63 | + | |
| 64 | +export const createControlPlaneAtomClient = ( | |
| 65 | + options: ControlPlaneClientOptions, | |
| 66 | +) => { | |
| 67 | + const baseLayer = FetchHttpClient.layer; | |
| 68 | + const runtime = Atom.runtime(baseLayer); | |
| 69 | + | |
| 70 | + const listSourcesEffect = ( | |
| 71 | + input: ListSourcesRequest, | |
| 72 | + ): Effect.Effect<ReadonlyArray<Source>, ControlPlaneClientError, never> => | |
| 73 | + Effect.gen(function* () { | |
| 74 | + const client = yield* makeControlPlaneClient(options); | |
| 75 | + return yield* client.sources.list({ | |
| 76 | + path: { | |
| 77 | + workspaceId: input.workspaceId, | |
| 78 | + }, | |
| 79 | + }); | |
| 80 | + }).pipe(Effect.provide(baseLayer)); | |
| 81 | + | |
| 82 | + const upsertSourceEffect = ( | |
| 83 | + input: UpsertSourceRequest, | |
| 84 | + ): Effect.Effect<Source, ControlPlaneClientError, never> => | |
| 85 | + Effect.gen(function* () { | |
| 86 | + const client = yield* makeControlPlaneClient(options); | |
| 87 | + return yield* client.sources.upsert({ | |
| 88 | + path: { | |
| 89 | + workspaceId: input.workspaceId, | |
| 90 | + }, | |
| 91 | + payload: input.payload, | |
| 92 | + }); | |
| 93 | + }).pipe(Effect.provide(baseLayer)); | |
| 94 | + | |
| 95 | + const removeSourceEffect = ( | |
| 96 | + input: RemoveSourceRequest, | |
| 97 | + ): Effect.Effect<RemoveSourceResult, ControlPlaneClientError, never> => | |
| 98 | + Effect.gen(function* () { | |
| 99 | + const client = yield* makeControlPlaneClient(options); | |
| 100 | + return yield* client.sources.remove({ | |
| 101 | + path: { | |
| 102 | + workspaceId: input.workspaceId, | |
| 103 | + sourceId: input.sourceId, | |
| 104 | + }, | |
| 105 | + }); | |
| 106 | + }).pipe(Effect.provide(baseLayer)); | |
| 107 | + | |
| 108 | + const listSourcesFamily = Atom.family((workspaceId: WorkspaceId) => | |
| 109 | + runtime.atom( | |
| 110 | + listSourcesEffect({ | |
| 111 | + workspaceId, | |
| 112 | + }), | |
| 113 | + ), | |
| 114 | + ); | |
| 115 | + | |
| 116 | + const listDisabledAtom = makeDisabledResultAtom< | |
| 117 | + ReadonlyArray<Source>, | |
| 118 | + ControlPlaneClientError | |
| 119 | + >(); | |
| 120 | + | |
| 121 | + const upsertSourceMutate = runtime.fn<UpsertSourceRequest>()( | |
| 122 | + Effect.fnUntraced(function* (input) { | |
| 123 | + return yield* upsertSourceEffect(input); | |
| 124 | + }), | |
| 125 | + ); | |
| 126 | + | |
| 127 | + const removeSourceMutate = runtime.fn<RemoveSourceRequest>()( | |
| 128 | + Effect.fnUntraced(function* (input) { | |
| 129 | + return yield* removeSourceEffect(input); | |
| 130 | + }), | |
| 131 | + ); | |
| 132 | + | |
| 133 | + return { | |
| 134 | + runtime, | |
| 135 | + sources: { | |
| 136 | + list: { | |
| 137 | + query: (input: ListSourcesRequest, readOptions?: ControlPlaneReadOptions) => | |
| 138 | + isReadEnabled(readOptions) | |
| 139 | + ? listSourcesFamily(input.workspaceId) | |
| 140 | + : listDisabledAtom, | |
| 141 | + queryEffect: listSourcesEffect, | |
| 142 | + queryPromise: (input: ListSourcesRequest) => | |
| 143 | + Effect.runPromise(listSourcesEffect(input)), | |
| 144 | + }, | |
| 145 | + upsert: { | |
| 146 | + mutate: upsertSourceMutate, | |
| 147 | + mutateEffect: upsertSourceEffect, | |
| 148 | + mutatePromise: (input: UpsertSourceRequest) => | |
| 149 | + Effect.runPromise(upsertSourceEffect(input)), | |
| 150 | + }, | |
| 151 | + remove: { | |
| 152 | + mutate: removeSourceMutate, | |
| 153 | + mutateEffect: removeSourceEffect, | |
| 154 | + mutatePromise: (input: RemoveSourceRequest) => | |
| 155 | + Effect.runPromise(removeSourceEffect(input)), | |
| 156 | + }, | |
| 157 | + }, | |
| 158 | + }; | |
| 159 | +}; | |
| @@ -0,0 +1,22 @@ | ||
| 1 | +import { HttpApiSchema } from "@effect/platform"; | |
| 2 | +import * as Schema from "effect/Schema"; | |
| 3 | + | |
| 4 | +export class ControlPlaneBadRequestError extends Schema.TaggedError<ControlPlaneBadRequestError>()( | |
| 5 | + "ControlPlaneBadRequestError", | |
| 6 | + { | |
| 7 | + operation: Schema.String, | |
| 8 | + message: Schema.String, | |
| 9 | + details: Schema.String, | |
| 10 | + }, | |
| 11 | + HttpApiSchema.annotations({ status: 400 }), | |
| 12 | +) {} | |
| 13 | + | |
| 14 | +export class ControlPlaneStorageError extends Schema.TaggedError<ControlPlaneStorageError>()( | |
| 15 | + "ControlPlaneStorageError", | |
| 16 | + { | |
| 17 | + operation: Schema.String, | |
| 18 | + message: Schema.String, | |
| 19 | + details: Schema.String, | |
| 20 | + }, | |
| 21 | + HttpApiSchema.annotations({ status: 500 }), | |
| 22 | +) {} | |
| @@ -0,0 +1,20 @@ | ||
| 1 | +import { HttpApiBuilder, HttpServer } from "@effect/platform"; | |
| 2 | +import * as Layer from "effect/Layer"; | |
| 3 | + | |
| 4 | +import { ControlPlaneApi } from "./api"; | |
| 5 | +import { ControlPlaneService } from "./service"; | |
| 6 | +import { ControlPlaneSourcesLive } from "./sources/http"; | |
| 7 | + | |
| 8 | +export const ControlPlaneApiLive = HttpApiBuilder.api(ControlPlaneApi).pipe( | |
| 9 | + Layer.provide(ControlPlaneSourcesLive), | |
| 10 | +); | |
| 11 | + | |
| 12 | +export const makeControlPlaneWebHandler = <E>( | |
| 13 | + serviceLayer: Layer.Layer<ControlPlaneService, E, never>, | |
| 14 | +) => | |
| 15 | + HttpApiBuilder.toWebHandler( | |
| 16 | + Layer.mergeAll( | |
| 17 | + ControlPlaneApiLive.pipe(Layer.provide(serviceLayer)), | |
| 18 | + HttpServer.layerContext, | |
| 19 | + ), | |
| 20 | + ); | |
| @@ -0,0 +1,44 @@ | ||
| 1 | +export { | |
| 2 | + ControlPlaneApi, | |
| 3 | + RemoveSourceResultSchema, | |
| 4 | + UpsertSourcePayloadSchema, | |
| 5 | + controlPlaneOpenApiSpec, | |
| 6 | + type RemoveSourceResult, | |
| 7 | + type UpsertSourcePayload, | |
| 8 | +} from "./api"; | |
| 9 | + | |
| 10 | +export { | |
| 11 | + ControlPlaneBadRequestError, | |
| 12 | + ControlPlaneStorageError, | |
| 13 | +} from "./errors"; | |
| 14 | + | |
| 15 | +export { | |
| 16 | + ControlPlaneService, | |
| 17 | + ControlPlaneServiceLive, | |
| 18 | + makeControlPlaneService, | |
| 19 | + type ControlPlaneServiceShape, | |
| 20 | +} from "./service"; | |
| 21 | + | |
| 22 | +export { | |
| 23 | + ControlPlaneApiLive, | |
| 24 | + makeControlPlaneWebHandler, | |
| 25 | +} from "./http"; | |
| 26 | + | |
| 27 | +export { | |
| 28 | + ControlPlaneSourcesLive, | |
| 29 | + makeControlPlaneSourcesService, | |
| 30 | + type ControlPlaneSourcesServiceShape, | |
| 31 | + type RemoveSourceInput, | |
| 32 | + type UpsertSourceInput, | |
| 33 | +} from "./sources"; | |
| 34 | + | |
| 35 | +export { | |
| 36 | + createControlPlaneAtomClient, | |
| 37 | + makeControlPlaneClient, | |
| 38 | + type ControlPlaneClientError, | |
| 39 | + type ControlPlaneClientOptions, | |
| 40 | + type ControlPlaneReadOptions, | |
| 41 | + type ListSourcesRequest, | |
| 42 | + type RemoveSourceRequest, | |
| 43 | + type UpsertSourceRequest, | |
| 44 | +} from "./client"; | |
| @@ -0,0 +1,37 @@ | ||
| 1 | +import { SourceCatalog, SourceCatalogLive } from "@executor-v2/source-manager/source-catalog"; | |
| 2 | +import * as Context from "effect/Context"; | |
| 3 | +import * as Effect from "effect/Effect"; | |
| 4 | +import * as Layer from "effect/Layer"; | |
| 5 | + | |
| 6 | +import { | |
| 7 | + makeControlPlaneSourcesService, | |
| 8 | + type ControlPlaneSourcesServiceShape, | |
| 9 | +} from "./sources/service"; | |
| 10 | + | |
| 11 | +export type ControlPlaneServiceShape = ControlPlaneSourcesServiceShape; | |
| 12 | + | |
| 13 | +export class ControlPlaneService extends Context.Tag("@executor-v2/control-plane/ControlPlaneService")< | |
| 14 | + ControlPlaneService, | |
| 15 | + ControlPlaneServiceShape | |
| 16 | +>() {} | |
| 17 | + | |
| 18 | +export const makeControlPlaneService = ( | |
| 19 | + services: { | |
| 20 | + sources: ControlPlaneSourcesServiceShape; | |
| 21 | + }, | |
| 22 | +): ControlPlaneServiceShape => ({ | |
| 23 | + ...services.sources, | |
| 24 | +}); | |
| 25 | + | |
| 26 | +export const ControlPlaneServiceLive = Layer.effect( | |
| 27 | + ControlPlaneService, | |
| 28 | + Effect.gen(function* () { | |
| 29 | + const sourceCatalog = yield* SourceCatalog; | |
| 30 | + | |
| 31 | + return ControlPlaneService.of( | |
| 32 | + makeControlPlaneService({ | |
| 33 | + sources: makeControlPlaneSourcesService(sourceCatalog), | |
| 34 | + }), | |
| 35 | + ); | |
| 36 | + }), | |
| 37 | +).pipe(Layer.provide(SourceCatalogLive)); | |
| @@ -0,0 +1,51 @@ | ||
| 1 | +import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | |
| 2 | +import { SourceIdSchema, SourceSchema, WorkspaceIdSchema } from "@executor-v2/schema"; | |
| 3 | +import * as Schema from "effect/Schema"; | |
| 4 | + | |
| 5 | +import { ControlPlaneBadRequestError, ControlPlaneStorageError } from "../errors"; | |
| 6 | + | |
| 7 | +const RequiredUpsertSourcePayloadSchema = SourceSchema.pipe( | |
| 8 | + Schema.pick("name", "kind", "endpoint"), | |
| 9 | +); | |
| 10 | + | |
| 11 | +const OptionalUpsertSourcePayloadSchema = SourceSchema.pipe( | |
| 12 | + Schema.pick("id", "status", "enabled", "configJson", "sourceHash", "lastError"), | |
| 13 | + Schema.partialWith({ exact: true }), | |
| 14 | +); | |
| 15 | + | |
| 16 | +export const UpsertSourcePayloadSchema = RequiredUpsertSourcePayloadSchema.pipe( | |
| 17 | + Schema.extend(OptionalUpsertSourcePayloadSchema), | |
| 18 | +); | |
| 19 | + | |
| 20 | +export type UpsertSourcePayload = typeof UpsertSourcePayloadSchema.Type; | |
| 21 | + | |
| 22 | +export const RemoveSourceResultSchema = Schema.Struct({ | |
| 23 | + removed: Schema.Boolean, | |
| 24 | +}); | |
| 25 | + | |
| 26 | +export type RemoveSourceResult = typeof RemoveSourceResultSchema.Type; | |
| 27 | + | |
| 28 | +const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); | |
| 29 | +const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema); | |
| 30 | + | |
| 31 | +export class SourcesApi extends HttpApiGroup.make("sources") | |
| 32 | + .add( | |
| 33 | + HttpApiEndpoint.get("list")`/workspaces/${workspaceIdParam}/sources` | |
| 34 | + .addSuccess(Schema.Array(SourceSchema)) | |
| 35 | + .addError(ControlPlaneBadRequestError) | |
| 36 | + .addError(ControlPlaneStorageError), | |
| 37 | + ) | |
| 38 | + .add( | |
| 39 | + HttpApiEndpoint.post("upsert")`/workspaces/${workspaceIdParam}/sources` | |
| 40 | + .setPayload(UpsertSourcePayloadSchema) | |
| 41 | + .addSuccess(SourceSchema) | |
| 42 | + .addError(ControlPlaneBadRequestError) | |
| 43 | + .addError(ControlPlaneStorageError), | |
| 44 | + ) | |
| 45 | + .add( | |
| 46 | + HttpApiEndpoint.del("remove")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}` | |
| 47 | + .addSuccess(RemoveSourceResultSchema) | |
| 48 | + .addError(ControlPlaneBadRequestError) | |
| 49 | + .addError(ControlPlaneStorageError), | |
| 50 | + ) | |
| 51 | + .prefix("/v1") {} | |
| @@ -0,0 +1,67 @@ | ||
| 1 | +import { HttpApiBuilder } from "@effect/platform"; | |
| 2 | +import { type SourceStoreError } from "@executor-v2/persistence-ports"; | |
| 3 | +import * as Effect from "effect/Effect"; | |
| 4 | + | |
| 5 | +import { ControlPlaneBadRequestError, ControlPlaneStorageError } from "../errors"; | |
| 6 | +import { ControlPlaneService } from "../service"; | |
| 7 | +import { ControlPlaneApi } from "../api"; | |
| 8 | + | |
| 9 | +const toStorageError = ( | |
| 10 | + operation: string, | |
| 11 | + cause: SourceStoreError, | |
| 12 | +): ControlPlaneStorageError => | |
| 13 | + new ControlPlaneStorageError({ | |
| 14 | + operation, | |
| 15 | + message: "Control plane operation failed", | |
| 16 | + details: cause.details ?? cause.message, | |
| 17 | + }); | |
| 18 | + | |
| 19 | +export const ControlPlaneSourcesLive = HttpApiBuilder.group( | |
| 20 | + ControlPlaneApi, | |
| 21 | + "sources", | |
| 22 | + (handlers) => | |
| 23 | + handlers | |
| 24 | + .handle("list", ({ path }) => | |
| 25 | + Effect.gen(function* () { | |
| 26 | + const service = yield* ControlPlaneService; | |
| 27 | + return yield* service.listSources(path.workspaceId); | |
| 28 | + }).pipe( | |
| 29 | + Effect.catchTag("SourceStoreError", (cause) => | |
| 30 | + toStorageError("sources.list", cause), | |
| 31 | + ), | |
| 32 | + ), | |
| 33 | + ) | |
| 34 | + .handle("upsert", ({ path, payload }) => | |
| 35 | + Effect.gen(function* () { | |
| 36 | + const service = yield* ControlPlaneService; | |
| 37 | + return yield* service.upsertSource({ | |
| 38 | + workspaceId: path.workspaceId, | |
| 39 | + payload, | |
| 40 | + }); | |
| 41 | + }).pipe( | |
| 42 | + Effect.catchTag("SourceCatalogValidationError", (error) => | |
| 43 | + new ControlPlaneBadRequestError({ | |
| 44 | + operation: error.operation, | |
| 45 | + message: error.message, | |
| 46 | + details: error.details, | |
| 47 | + }), | |
| 48 | + ), | |
| 49 | + Effect.catchTag("SourceStoreError", (cause) => | |
| 50 | + toStorageError("sources.upsert", cause), | |
| 51 | + ), | |
| 52 | + ), | |
| 53 | + ) | |
| 54 | + .handle("remove", ({ path }) => | |
| 55 | + Effect.gen(function* () { | |
| 56 | + const service = yield* ControlPlaneService; | |
| 57 | + return yield* service.removeSource({ | |
| 58 | + workspaceId: path.workspaceId, | |
| 59 | + sourceId: path.sourceId, | |
| 60 | + }); | |
| 61 | + }).pipe( | |
| 62 | + Effect.catchTag("SourceStoreError", (cause) => | |
| 63 | + toStorageError("sources.remove", cause), | |
| 64 | + ), | |
| 65 | + ), | |
| 66 | + ), | |
| 67 | +); | |
| @@ -0,0 +1,16 @@ | ||
| 1 | +export { | |
| 2 | + RemoveSourceResultSchema, | |
| 3 | + SourcesApi, | |
| 4 | + UpsertSourcePayloadSchema, | |
| 5 | + type RemoveSourceResult, | |
| 6 | + type UpsertSourcePayload, | |
| 7 | +} from "./api"; | |
| 8 | + | |
| 9 | +export { | |
| 10 | + makeControlPlaneSourcesService, | |
| 11 | + type ControlPlaneSourcesServiceShape, | |
| 12 | + type RemoveSourceInput, | |
| 13 | + type UpsertSourceInput, | |
| 14 | +} from "./service"; | |
| 15 | + | |
| 16 | +export { ControlPlaneSourcesLive } from "./http"; | |
| @@ -0,0 +1,45 @@ | ||
| 1 | +import { type SourceStoreError } from "@executor-v2/persistence-ports"; | |
| 2 | +import { | |
| 3 | + SourceCatalogValidationError, | |
| 4 | + type SourceCatalogService, | |
| 5 | +} from "@executor-v2/source-manager/source-catalog"; | |
| 6 | +import { type Source, type SourceId, type WorkspaceId } from "@executor-v2/schema"; | |
| 7 | +import * as Effect from "effect/Effect"; | |
| 8 | + | |
| 9 | +import type { RemoveSourceResult, UpsertSourcePayload } from "./api"; | |
| 10 | + | |
| 11 | +export type UpsertSourceInput = { | |
| 12 | + workspaceId: WorkspaceId; | |
| 13 | + payload: UpsertSourcePayload; | |
| 14 | +}; | |
| 15 | + | |
| 16 | +export type RemoveSourceInput = { | |
| 17 | + workspaceId: WorkspaceId; | |
| 18 | + sourceId: SourceId; | |
| 19 | +}; | |
| 20 | + | |
| 21 | +export type ControlPlaneSourcesServiceShape = { | |
| 22 | + listSources: ( | |
| 23 | + workspaceId: WorkspaceId, | |
| 24 | + ) => Effect.Effect<ReadonlyArray<Source>, SourceStoreError>; | |
| 25 | + upsertSource: ( | |
| 26 | + input: UpsertSourceInput, | |
| 27 | + ) => Effect.Effect<Source, SourceStoreError | SourceCatalogValidationError>; | |
| 28 | + removeSource: (input: RemoveSourceInput) => Effect.Effect<RemoveSourceResult, SourceStoreError>; | |
| 29 | +}; | |
| 30 | + | |
| 31 | +export const makeControlPlaneSourcesService = ( | |
| 32 | + sourceCatalog: SourceCatalogService, | |
| 33 | +): ControlPlaneSourcesServiceShape => ({ | |
| 34 | + listSources: (workspaceId) => sourceCatalog.listSources(workspaceId), | |
| 35 | + upsertSource: (input) => | |
| 36 | + sourceCatalog.upsertSource({ | |
| 37 | + workspaceId: input.workspaceId, | |
| 38 | + payload: input.payload, | |
| 39 | + }), | |
| 40 | + removeSource: (input) => | |
| 41 | + sourceCatalog.removeSource({ | |
| 42 | + workspaceId: input.workspaceId, | |
| 43 | + sourceId: input.sourceId, | |
| 44 | + }), | |
| 45 | +}); | |
| @@ -1,4 +1,7 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "extends": "../../tsconfig.json", |
| 3 | + "compilerOptions": { | |
| 4 | + "types": ["node", "bun"] | |
| 5 | + }, | |
| 3 | 6 | "include": ["src"] |
| 4 | −} | |
| 7 | +} | |
| @@ -9,7 +9,11 @@ import { · 8 unmodified lines | ||
| 9 | 9 | type ToolProvider, |
| 10 | 10 | type ToolProviderRegistry, |
| 11 | 11 | } from "./tool-providers"; |
| 12 | −import type { RuntimeAdapter, RuntimeRunnableTool } from "./runtime-adapters"; | |
| 12 | +import type { | |
| 13 | + RuntimeAdapter, | |
| 14 | + RuntimeExecuteError, | |
| 15 | + RuntimeRunnableTool, | |
| 16 | +} from "./runtime-adapters"; | |
| 13 | 17 | |
| 14 | 18 | export type RuntimeRunClientExecuteInput = { |
| 15 | 19 | code: string; |
| @@ -57,21 +61,16 @@ export type CreateInMemoryRuntimeRunClientOptions = { · 41 unmodified lines | ||
| 57 | 61 | makeRunId?: () => string; |
| 58 | 62 | }; |
| 59 | 63 | |
| 60 | −const errorToText = (error: unknown): string => { | |
| 61 | − if (error instanceof Error) { | |
| 62 | − return error.message; | |
| 64 | +const formatRuntimeExecuteError = (error: RuntimeExecuteError): string => { | |
| 65 | + switch (error._tag) { | |
| 66 | + case "RuntimeAdapterError": | |
| 67 | + case "LocalCodeRunnerError": | |
| 68 | + case "DenoSubprocessRunnerError": | |
| 69 | + case "ToolProviderError": | |
| 70 | + return error.details ? `${error.message}: ${error.details}` : error.message; | |
| 71 | + case "ToolProviderRegistryError": | |
| 72 | + return error.message; | |
| 63 | 73 | } |
| 64 | − | |
| 65 | − if ( | |
| 66 | − typeof error === "object" && | |
| 67 | − error !== null && | |
| 68 | − "message" in error && | |
| 69 | − typeof error.message === "string" | |
| 70 | − ) { | |
| 71 | − return error.message; | |
| 72 | − } | |
| 73 | − | |
| 74 | − return String(error); | |
| 75 | 74 | }; |
| 76 | 75 | |
| 77 | 76 | const toInMemoryDescriptor = ( |
| @@ -127,7 +126,7 @@ const makeInMemoryToolProvider = ( · 49 unmodified lines | ||
| 127 | 126 | operation: "invoke", |
| 128 | 127 | providerKind: "in_memory", |
| 129 | 128 | message: `In-memory tool invocation failed: ${input.tool.toolId}`, |
| 130 | − details: cause instanceof Error ? cause.message : String(cause), | |
| 129 | + details: String(cause), | |
| 131 | 130 | }), |
| 132 | 131 | }); |
| 133 | 132 | }, |
| @@ -152,7 +151,7 @@ export const createRuntimeRunClient = ( · 18 unmodified lines | ||
| 152 | 151 | return { |
| 153 | 152 | runId, |
| 154 | 153 | status: "failed", |
| 155 | − error: errorToText(availabilityResult.left), | |
| 154 | + error: "Runtime availability check failed", | |
| 156 | 155 | }; |
| 157 | 156 | } |
| 158 | 157 | |
| @@ -185,7 +184,7 @@ export const createRuntimeRunClient = ( · 26 unmodified lines | ||
| 185 | 184 | return { |
| 186 | 185 | runId, |
| 187 | 186 | status: "failed", |
| 188 | − error: errorToText(executionResult.left), | |
| 187 | + error: formatRuntimeExecuteError(executionResult.left), | |
| 189 | 188 | }; |
| 190 | 189 | } |
| 191 | 190 | |
| @@ -1,3 +0,0 @@ | ||
| 1 | −# rpc | |
| 2 | − | |
| 3 | −Effect RPC contract scaffold for Executor v2. | |
| @@ -1,8 +0,0 @@ | ||
| 1 | −{ | |
| 2 | − "name": "@executor-v2/rpc", | |
| 3 | − "private": true, | |
| 4 | − "type": "module", | |
| 5 | − "scripts": { | |
| 6 | − "typecheck": "bunx tsc --noEmit -p tsconfig.json" | |
| 7 | − } | |
| 8 | −} | |
| @@ -1 +0,0 @@ | ||
| 1 | −export {}; | |
| @@ -3,7 +3,8 @@ · 2 unmodified lines | ||
| 3 | 3 | "private": true, |
| 4 | 4 | "type": "module", |
| 5 | 5 | "exports": { |
| 6 | − ".": "./src/index.ts" | |
| 6 | + ".": "./src/index.ts", | |
| 7 | + "./source-catalog": "./src/source-catalog.ts" | |
| 7 | 8 | }, |
| 8 | 9 | "scripts": { |
| 9 | 10 | "test": "bunx vitest run --no-watch src/*.test.ts", |
| @@ -5,3 +5,15 @@ export { · 4 unmodified lines | ||
| 5 | 5 | type RefreshOpenApiArtifactRequest, |
| 6 | 6 | type SourceManagerService, |
| 7 | 7 | } from "./service"; |
| 8 | + | |
| 9 | +export { | |
| 10 | + SourceCatalog, | |
| 11 | + SourceCatalogLive, | |
| 12 | + SourceCatalogValidationError, | |
| 13 | + makeSourceCatalogService, | |
| 14 | + type RemoveSourceRequest, | |
| 15 | + type RemoveSourceResult, | |
| 16 | + type SourceCatalogService, | |
| 17 | + type UpsertSourcePayload, | |
| 18 | + type UpsertSourceRequest, | |
| 19 | +} from "./source-catalog"; | |
| @@ -0,0 +1,158 @@ | ||
| 1 | +import { | |
| 2 | + SourceStoreError, | |
| 3 | + SourceStoreService, | |
| 4 | + type SourceStore, | |
| 5 | +} from "@executor-v2/persistence-ports"; | |
| 6 | +import { | |
| 7 | + SourceIdSchema, | |
| 8 | + SourceSchema, | |
| 9 | + type Source, | |
| 10 | + type SourceId, | |
| 11 | + type SourceKind, | |
| 12 | + type SourceStatus, | |
| 13 | + type WorkspaceId, | |
| 14 | +} from "@executor-v2/schema"; | |
| 15 | +import * as Context from "effect/Context"; | |
| 16 | +import * as Data from "effect/Data"; | |
| 17 | +import * as Effect from "effect/Effect"; | |
| 18 | +import * as Layer from "effect/Layer"; | |
| 19 | +import * as Option from "effect/Option"; | |
| 20 | +import * as ParseResult from "effect/ParseResult"; | |
| 21 | +import * as Schema from "effect/Schema"; | |
| 22 | + | |
| 23 | +export class SourceCatalogValidationError extends Data.TaggedError( | |
| 24 | + "SourceCatalogValidationError", | |
| 25 | +)<{ | |
| 26 | + operation: string; | |
| 27 | + message: string; | |
| 28 | + details: string; | |
| 29 | +}> {} | |
| 30 | + | |
| 31 | +export type UpsertSourcePayload = { | |
| 32 | + id?: SourceId; | |
| 33 | + name: string; | |
| 34 | + kind: SourceKind; | |
| 35 | + endpoint: string; | |
| 36 | + status?: SourceStatus; | |
| 37 | + enabled?: boolean; | |
| 38 | + configJson?: string; | |
| 39 | + sourceHash?: string | null; | |
| 40 | + lastError?: string | null; | |
| 41 | +}; | |
| 42 | + | |
| 43 | +export type UpsertSourceRequest = { | |
| 44 | + workspaceId: WorkspaceId; | |
| 45 | + payload: UpsertSourcePayload; | |
| 46 | + now?: () => number; | |
| 47 | +}; | |
| 48 | + | |
| 49 | +export type RemoveSourceRequest = { | |
| 50 | + workspaceId: WorkspaceId; | |
| 51 | + sourceId: SourceId; | |
| 52 | +}; | |
| 53 | + | |
| 54 | +export type RemoveSourceResult = { | |
| 55 | + removed: boolean; | |
| 56 | +}; | |
| 57 | + | |
| 58 | +export type SourceCatalogService = { | |
| 59 | + listSources: ( | |
| 60 | + workspaceId: WorkspaceId, | |
| 61 | + ) => Effect.Effect<ReadonlyArray<Source>, SourceStoreError>; | |
| 62 | + upsertSource: ( | |
| 63 | + input: UpsertSourceRequest, | |
| 64 | + ) => Effect.Effect<Source, SourceStoreError | SourceCatalogValidationError>; | |
| 65 | + removeSource: ( | |
| 66 | + input: RemoveSourceRequest, | |
| 67 | + ) => Effect.Effect<RemoveSourceResult, SourceStoreError>; | |
| 68 | +}; | |
| 69 | + | |
| 70 | +export class SourceCatalog extends Context.Tag("@executor-v2/source-manager/SourceCatalog")< | |
| 71 | + SourceCatalog, | |
| 72 | + SourceCatalogService | |
| 73 | +>() {} | |
| 74 | + | |
| 75 | +const decodeSourceId = Schema.decodeUnknown(SourceIdSchema); | |
| 76 | +const decodeSource = Schema.decodeUnknown(SourceSchema); | |
| 77 | + | |
| 78 | +const sourceStoreKey = (source: Source): string => `${source.workspaceId}:${source.id}`; | |
| 79 | + | |
| 80 | +const sortSources = (sources: ReadonlyArray<Source>): Array<Source> => | |
| 81 | + [...sources].sort((left, right) => { | |
| 82 | + const leftName = left.name.toLowerCase(); | |
| 83 | + const rightName = right.name.toLowerCase(); | |
| 84 | + | |
| 85 | + if (leftName === rightName) { | |
| 86 | + return sourceStoreKey(left).localeCompare(sourceStoreKey(right)); | |
| 87 | + } | |
| 88 | + | |
| 89 | + return leftName.localeCompare(rightName); | |
| 90 | + }); | |
| 91 | + | |
| 92 | +const toSourceCatalogValidationError = ( | |
| 93 | + operation: string, | |
| 94 | + cause: unknown, | |
| 95 | +): SourceCatalogValidationError => | |
| 96 | + new SourceCatalogValidationError({ | |
| 97 | + operation, | |
| 98 | + message: "Invalid source payload", | |
| 99 | + details: ParseResult.isParseError(cause) | |
| 100 | + ? ParseResult.TreeFormatter.formatErrorSync(cause) | |
| 101 | + : String(cause), | |
| 102 | + }); | |
| 103 | + | |
| 104 | +const createSourceId = (): Effect.Effect<SourceId, SourceCatalogValidationError> => | |
| 105 | + decodeSourceId(`src_${crypto.randomUUID()}`).pipe( | |
| 106 | + Effect.mapError((cause) => toSourceCatalogValidationError("sources.upsert.id", cause)), | |
| 107 | + ); | |
| 108 | + | |
| 109 | +const timestamp = (clock?: () => number): number => clock?.() ?? Date.now(); | |
| 110 | + | |
| 111 | +export const makeSourceCatalogService = ( | |
| 112 | + sourceStore: SourceStore, | |
| 113 | +): SourceCatalogService => ({ | |
| 114 | + listSources: (workspaceId) => | |
| 115 | + sourceStore.listByWorkspace(workspaceId).pipe(Effect.map(sortSources)), | |
| 116 | + upsertSource: (input) => | |
| 117 | + Effect.gen(function* () { | |
| 118 | + const sourceId = input.payload.id ?? (yield* createSourceId()); | |
| 119 | + const now = timestamp(input.now); | |
| 120 | + const existingOption = yield* sourceStore.getById(input.workspaceId, sourceId); | |
| 121 | + const existing = Option.getOrUndefined(existingOption); | |
| 122 | + | |
| 123 | + const source = yield* decodeSource({ | |
| 124 | + id: sourceId, | |
| 125 | + workspaceId: input.workspaceId, | |
| 126 | + name: input.payload.name, | |
| 127 | + kind: input.payload.kind, | |
| 128 | + endpoint: input.payload.endpoint, | |
| 129 | + status: input.payload.status ?? "draft", | |
| 130 | + enabled: input.payload.enabled ?? true, | |
| 131 | + configJson: input.payload.configJson ?? "{}", | |
| 132 | + sourceHash: input.payload.sourceHash ?? null, | |
| 133 | + lastError: input.payload.lastError ?? null, | |
| 134 | + createdAt: existing?.createdAt ?? now, | |
| 135 | + updatedAt: now, | |
| 136 | + }).pipe( | |
| 137 | + Effect.mapError((cause) => | |
| 138 | + toSourceCatalogValidationError("sources.upsert.payload", cause), | |
| 139 | + ), | |
| 140 | + ); | |
| 141 | + | |
| 142 | + yield* sourceStore.upsert(source); | |
| 143 | + return source; | |
| 144 | + }), | |
| 145 | + removeSource: (input) => | |
| 146 | + sourceStore.removeById(input.workspaceId, input.sourceId).pipe( | |
| 147 | + Effect.map((removed): RemoveSourceResult => ({ removed })), | |
| 148 | + ), | |
| 149 | +}); | |
| 150 | + | |
| 151 | +export const SourceCatalogLive = Layer.effect( | |
| 152 | + SourceCatalog, | |
| 153 | + Effect.gen(function* () { | |
| 154 | + const sourceStore = yield* SourceStoreService; | |
| 155 | + | |
| 156 | + return SourceCatalog.of(makeSourceCatalogService(sourceStore)); | |
| 157 | + }), | |
| 158 | +); | |
| @@ -1,8 +0,0 @@ | ||
| 1 | −# persistence-convex | |
| 2 | − | |
| 3 | −Convex-backed persistence adapter scaffold for Executor v2. | |
| 4 | − | |
| 5 | −Current scaffold includes: | |
| 6 | −- Convex table-typed ID schemas in `src/convex-ids.ts` | |
| 7 | −- shared Convex schema projection in `src/schema.ts` | |
| 8 | −- first adapter implementation for `ProfileStore` in `src/profiles.ts` | |
| @@ -1,25 +0,0 @@ | ||
| 1 | −import { Id } from "@executor-v2/confect"; | |
| 2 | − | |
| 3 | −export const ProfileDocIdSchema = Id("profiles"); | |
| 4 | −export const WorkspaceDocIdSchema = Id("workspaces"); | |
| 5 | −export const SourceDocIdSchema = Id("sources"); | |
| 6 | −export const ToolArtifactDocIdSchema = Id("toolArtifacts"); | |
| 7 | −export const CredentialRefDocIdSchema = Id("credentialRefs"); | |
| 8 | −export const OAuthTokenDocIdSchema = Id("oauthTokens"); | |
| 9 | −export const PolicyDocIdSchema = Id("policies"); | |
| 10 | −export const ApprovalDocIdSchema = Id("approvals"); | |
| 11 | −export const TaskRunDocIdSchema = Id("taskRuns"); | |
| 12 | −export const SyncStateDocIdSchema = Id("syncStates"); | |
| 13 | −export const EventDocIdSchema = Id("events"); | |
| 14 | − | |
| 15 | −export type ProfileDocId = typeof ProfileDocIdSchema.Type; | |
| 16 | −export type WorkspaceDocId = typeof WorkspaceDocIdSchema.Type; | |
| 17 | −export type SourceDocId = typeof SourceDocIdSchema.Type; | |
| 18 | −export type ToolArtifactDocId = typeof ToolArtifactDocIdSchema.Type; | |
| 19 | −export type CredentialRefDocId = typeof CredentialRefDocIdSchema.Type; | |
| 20 | −export type OAuthTokenDocId = typeof OAuthTokenDocIdSchema.Type; | |
| 21 | −export type PolicyDocId = typeof PolicyDocIdSchema.Type; | |
| 22 | −export type ApprovalDocId = typeof ApprovalDocIdSchema.Type; | |
| 23 | −export type TaskRunDocId = typeof TaskRunDocIdSchema.Type; | |
| 24 | −export type SyncStateDocId = typeof SyncStateDocIdSchema.Type; | |
| 25 | −export type EventDocId = typeof EventDocIdSchema.Type; | |
| @@ -1,26 +0,0 @@ | ||
| 1 | −export { | |
| 2 | − ApprovalDocIdSchema, | |
| 3 | − CredentialRefDocIdSchema, | |
| 4 | − EventDocIdSchema, | |
| 5 | − OAuthTokenDocIdSchema, | |
| 6 | − PolicyDocIdSchema, | |
| 7 | − ProfileDocIdSchema, | |
| 8 | − SourceDocIdSchema, | |
| 9 | − SyncStateDocIdSchema, | |
| 10 | − TaskRunDocIdSchema, | |
| 11 | − ToolArtifactDocIdSchema, | |
| 12 | − WorkspaceDocIdSchema, | |
| 13 | − type ApprovalDocId, | |
| 14 | − type CredentialRefDocId, | |
| 15 | − type EventDocId, | |
| 16 | − type OAuthTokenDocId, | |
| 17 | − type PolicyDocId, | |
| 18 | − type ProfileDocId, | |
| 19 | − type SourceDocId, | |
| 20 | − type SyncStateDocId, | |
| 21 | − type TaskRunDocId, | |
| 22 | − type ToolArtifactDocId, | |
| 23 | − type WorkspaceDocId, | |
| 24 | −} from "./convex-ids"; | |
| 25 | −export { makeConvexProfileStore } from "./profiles"; | |
| 26 | −export { executorConfectSchema, type ExecutorConfectTables } from "./schema"; | |
| @@ -1,33 +0,0 @@ | ||
| 1 | −import type { | |
| 2 | − ConfectQueryCtx, | |
| 3 | − DocumentFromTable, | |
| 4 | −} from "@executor-v2/confect"; | |
| 5 | −import { ProfileSchema, type Profile, type ProfileId } from "@executor-v2/schema"; | |
| 6 | −import type { ProfileStore } from "@executor-v2/persistence-ports"; | |
| 7 | −import * as Effect from "effect/Effect"; | |
| 8 | −import { pipe } from "effect/Function"; | |
| 9 | −import * as Option from "effect/Option"; | |
| 10 | −import * as Schema from "effect/Schema"; | |
| 11 | − | |
| 12 | −import type { ExecutorConfectTables } from "./schema"; | |
| 13 | − | |
| 14 | −type ProfileDocument = DocumentFromTable<ExecutorConfectTables, "profiles">; | |
| 15 | − | |
| 16 | −const toProfile = (document: ProfileDocument): Profile => { | |
| 17 | − const { _id: _ignoredId, _creationTime: _ignoredCreationTime, ...profile } = document; | |
| 18 | − return Schema.decodeUnknownSync(ProfileSchema)(profile); | |
| 19 | −}; | |
| 20 | − | |
| 21 | −export const makeConvexProfileStore = ( | |
| 22 | − ctx: ConfectQueryCtx<ExecutorConfectTables>, | |
| 23 | −): ProfileStore => ({ | |
| 24 | − getById: (id: ProfileId) => | |
| 25 | − pipe( | |
| 26 | − ctx.db | |
| 27 | − .query("profiles") | |
| 28 | − .withIndex("by_domainId", (q) => q.eq("id", id)) | |
| 29 | − .unique(), | |
| 30 | − Effect.orDie, | |
| 31 | − Effect.map(Option.map(toProfile)), | |
| 32 | − ), | |
| 33 | −}); | |
| @@ -1,56 +0,0 @@ | ||
| 1 | −import { defineSchema, defineTable, type TablesFromSchemaDefinition } from "@executor-v2/confect"; | |
| 2 | −import { | |
| 3 | − ApprovalSchema, | |
| 4 | − CredentialRefSchema, | |
| 5 | − EventEnvelopeSchema, | |
| 6 | − OAuthTokenSchema, | |
| 7 | − PolicySchema, | |
| 8 | − ProfileSchema, | |
| 9 | − SourceSchema, | |
| 10 | − SyncStateSchema, | |
| 11 | − TaskRunSchema, | |
| 12 | − ToolArtifactSchema, | |
| 13 | − WorkspaceSchema, | |
| 14 | −} from "@executor-v2/schema"; | |
| 15 | − | |
| 16 | −export const executorConfectSchema = defineSchema({ | |
| 17 | − profiles: defineTable(ProfileSchema).index("by_domainId", ["id"]), | |
| 18 | − workspaces: defineTable(WorkspaceSchema) | |
| 19 | − .index("by_domainId", ["id"]) | |
| 20 | − .index("by_profileId", ["profileId"]), | |
| 21 | − sources: defineTable(SourceSchema) | |
| 22 | − .index("by_domainId", ["id"]) | |
| 23 | − .index("by_workspaceId", ["workspaceId"]), | |
| 24 | − toolArtifacts: defineTable(ToolArtifactSchema) | |
| 25 | − .index("by_domainId", ["id"]) | |
| 26 | − .index("by_workspaceId", ["workspaceId"]) | |
| 27 | − .index("by_sourceId", ["sourceId"]), | |
| 28 | − credentialRefs: defineTable(CredentialRefSchema) | |
| 29 | − .index("by_domainId", ["id"]) | |
| 30 | − .index("by_workspaceId", ["workspaceId"]) | |
| 31 | − .index("by_sourceId", ["sourceId"]), | |
| 32 | − oauthTokens: defineTable(OAuthTokenSchema) | |
| 33 | − .index("by_domainId", ["id"]) | |
| 34 | − .index("by_workspaceId", ["workspaceId"]) | |
| 35 | − .index("by_sourceId", ["sourceId"]), | |
| 36 | − policies: defineTable(PolicySchema) | |
| 37 | − .index("by_domainId", ["id"]) | |
| 38 | − .index("by_workspaceId", ["workspaceId"]), | |
| 39 | − approvals: defineTable(ApprovalSchema) | |
| 40 | − .index("by_domainId", ["id"]) | |
| 41 | − .index("by_workspaceId", ["workspaceId"]) | |
| 42 | − .index("by_taskRunId", ["taskRunId"]), | |
| 43 | − taskRuns: defineTable(TaskRunSchema) | |
| 44 | − .index("by_domainId", ["id"]) | |
| 45 | − .index("by_workspaceId", ["workspaceId"]) | |
| 46 | − .index("by_sessionId", ["sessionId"]), | |
| 47 | − syncStates: defineTable(SyncStateSchema) | |
| 48 | − .index("by_domainId", ["id"]) | |
| 49 | − .index("by_workspaceId", ["workspaceId"]), | |
| 50 | − events: defineTable(EventEnvelopeSchema) | |
| 51 | − .index("by_domainId", ["id"]) | |
| 52 | − .index("by_workspaceId", ["workspaceId"]) | |
| 53 | − .index("by_workspaceId_sequence", ["workspaceId", "sequence"]), | |
| 54 | −}); | |
| 55 | − | |
| 56 | −export type ExecutorConfectTables = TablesFromSchemaDefinition<typeof executorConfectSchema>; | |
| @@ -1,4 +0,0 @@ | ||
| 1 | −{ | |
| 2 | − "extends": "../../tsconfig.json", | |
| 3 | − "include": ["src"] | |
| 4 | −} | |