- packages/core/api: HttpApi contract with ToolsApi + SecretsApi groups SDK tagged errors pass through directly with status annotations (ToolNotFoundError→404, PolicyDeniedError→403, etc.) - apps/server: API handler implementation via HttpApiBuilder Exports createApiHandler() for reuse (no baked-in server runtime) Handlers are pure pass-through — no manual error catching - apps/web: Vite + React frontend served on same port as API Vite plugin forwards /v1/*, /docs, /openapi.json to Effect handler effect-atom integration: AtomHttpApi.Tag typed client, query/mutation atoms RegistryProvider + useAtomValue with Result pattern matching - Catalog pins @effect/platform, platform-bun, platform-node, cluster - All 9 packages typecheck clean
Showing the changes to packages (8 of 26 files). Show all
Unified · Split
| @@ -0,0 +1,22 @@ | |||
| 1 | { | ||
| 2 | "name": "@executor/api", | ||
| 3 | "version": "0.0.0", | ||
| 4 | "private": true, | ||
| 5 | "type": "module", | ||
| 6 | "exports": { | ||
| 7 | ".": "./src/index.ts" | ||
| 8 | }, | ||
| 9 | "dependencies": { | ||
| 10 | "@effect/platform": "catalog:", | ||
| 11 | "@executor/sdk": "workspace:*", | ||
| 12 | "effect": "catalog:" | ||
| 13 | }, | ||
| 14 | "devDependencies": { | ||
| 15 | "@types/node": "catalog:", | ||
| 16 | "bun-types": "catalog:", | ||
| 17 | "typescript": "^5.9.3" | ||
| 18 | }, | ||
| 19 | "scripts": { | ||
| 20 | "typecheck": "tsc --noEmit" | ||
| 21 | } | ||
| 22 | } | ||
| @@ -0,0 +1,14 @@ | |||
| 1 | import { HttpApi, OpenApi } from "@effect/platform"; | ||
| 2 | |||
| 3 | import { ToolsApi } from "./tools/api"; | ||
| 4 | import { SecretsApi } from "./secrets/api"; | ||
| 5 | |||
| 6 | export const ExecutorApi = HttpApi.make("executor") | ||
| 7 | .add(ToolsApi) | ||
| 8 | .add(SecretsApi) | ||
| 9 | .annotateContext( | ||
| 10 | OpenApi.annotations({ | ||
| 11 | title: "Executor API", | ||
| 12 | description: "Tool execution platform API", | ||
| 13 | }), | ||
| 14 | ); | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | import { HttpApiSchema } from "@effect/platform"; | ||
| 2 | import { Schema } from "effect"; | ||
| 3 | |||
| 4 | export class ApiNotFoundError extends Schema.TaggedError<ApiNotFoundError>()( | ||
| 5 | "ApiNotFoundError", | ||
| 6 | { | ||
| 7 | message: Schema.String, | ||
| 8 | }, | ||
| 9 | HttpApiSchema.annotations({ status: 404 }), | ||
| 10 | ) {} | ||
| 11 | |||
| 12 | export class ApiBadRequestError extends Schema.TaggedError<ApiBadRequestError>()( | ||
| 13 | "ApiBadRequestError", | ||
| 14 | { | ||
| 15 | message: Schema.String, | ||
| 16 | }, | ||
| 17 | HttpApiSchema.annotations({ status: 400 }), | ||
| 18 | ) {} | ||
| 19 | |||
| 20 | export class ApiInternalError extends Schema.TaggedError<ApiInternalError>()( | ||
| 21 | "ApiInternalError", | ||
| 22 | { | ||
| 23 | message: Schema.String, | ||
| 24 | }, | ||
| 25 | HttpApiSchema.annotations({ status: 500 }), | ||
| 26 | ) {} | ||
| @@ -0,0 +1,3 @@ | |||
| 1 | export { ExecutorApi } from "./api"; | ||
| 2 | export { ToolsApi } from "./tools/api"; | ||
| 3 | export { SecretsApi } from "./secrets/api"; | ||
| @@ -0,0 +1,78 @@ | |||
| 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | ||
| 2 | import { Schema } from "effect"; | ||
| 3 | import { | ||
| 4 | ScopeId, | ||
| 5 | SecretId, | ||
| 6 | SecretNotFoundError, | ||
| 7 | SecretResolutionError, | ||
| 8 | } from "@executor/sdk"; | ||
| 9 | |||
| 10 | // --------------------------------------------------------------------------- | ||
| 11 | // Params | ||
| 12 | // --------------------------------------------------------------------------- | ||
| 13 | |||
| 14 | const scopeIdParam = HttpApiSchema.param("scopeId", ScopeId); | ||
| 15 | const secretIdParam = HttpApiSchema.param("secretId", SecretId); | ||
| 16 | |||
| 17 | // --------------------------------------------------------------------------- | ||
| 18 | // Response / payload schemas | ||
| 19 | // --------------------------------------------------------------------------- | ||
| 20 | |||
| 21 | const SecretRefResponse = Schema.Struct({ | ||
| 22 | id: SecretId, | ||
| 23 | scopeId: ScopeId, | ||
| 24 | name: Schema.String, | ||
| 25 | provider: Schema.optional(Schema.String), | ||
| 26 | purpose: Schema.optional(Schema.String), | ||
| 27 | createdAt: Schema.Number, | ||
| 28 | }); | ||
| 29 | |||
| 30 | const SecretStatusResponse = Schema.Struct({ | ||
| 31 | secretId: SecretId, | ||
| 32 | status: Schema.Literal("resolved", "missing"), | ||
| 33 | }); | ||
| 34 | |||
| 35 | const SetSecretPayload = Schema.Struct({ | ||
| 36 | id: SecretId, | ||
| 37 | name: Schema.String, | ||
| 38 | value: Schema.String, | ||
| 39 | purpose: Schema.optional(Schema.String), | ||
| 40 | provider: Schema.optional(Schema.String), | ||
| 41 | }); | ||
| 42 | |||
| 43 | // --------------------------------------------------------------------------- | ||
| 44 | // Error schemas with HTTP status annotations | ||
| 45 | // --------------------------------------------------------------------------- | ||
| 46 | |||
| 47 | const SecretNotFound = SecretNotFoundError.annotations( | ||
| 48 | HttpApiSchema.annotations({ status: 404 }), | ||
| 49 | ); | ||
| 50 | const SecretResolution = SecretResolutionError.annotations( | ||
| 51 | HttpApiSchema.annotations({ status: 500 }), | ||
| 52 | ); | ||
| 53 | |||
| 54 | // --------------------------------------------------------------------------- | ||
| 55 | // Group | ||
| 56 | // --------------------------------------------------------------------------- | ||
| 57 | |||
| 58 | export class SecretsApi extends HttpApiGroup.make("secrets") | ||
| 59 | .add( | ||
| 60 | HttpApiEndpoint.get("list")`/scopes/${scopeIdParam}/secrets` | ||
| 61 | .addSuccess(Schema.Array(SecretRefResponse)), | ||
| 62 | ) | ||
| 63 | .add( | ||
| 64 | HttpApiEndpoint.get("status")`/scopes/${scopeIdParam}/secrets/${secretIdParam}/status` | ||
| 65 | .addSuccess(SecretStatusResponse), | ||
| 66 | ) | ||
| 67 | .add( | ||
| 68 | HttpApiEndpoint.post("set")`/scopes/${scopeIdParam}/secrets` | ||
| 69 | .setPayload(SetSecretPayload) | ||
| 70 | .addSuccess(SecretRefResponse) | ||
| 71 | .addError(SecretResolution), | ||
| 72 | ) | ||
| 73 | .add( | ||
| 74 | HttpApiEndpoint.del("remove")`/scopes/${scopeIdParam}/secrets/${secretIdParam}` | ||
| 75 | .addSuccess(Schema.Struct({ removed: Schema.Boolean })) | ||
| 76 | .addError(SecretNotFound), | ||
| 77 | ) | ||
| 78 | .prefix("/v1") {} | ||
| @@ -0,0 +1,87 @@ | |||
| 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | ||
| 2 | import { Schema } from "effect"; | ||
| 3 | import { | ||
| 4 | ScopeId, | ||
| 5 | ToolId, | ||
| 6 | ToolNotFoundError, | ||
| 7 | ToolInvocationError, | ||
| 8 | PolicyDeniedError, | ||
| 9 | ElicitationDeclinedError, | ||
| 10 | } from "@executor/sdk"; | ||
| 11 | |||
| 12 | // --------------------------------------------------------------------------- | ||
| 13 | // Params | ||
| 14 | // --------------------------------------------------------------------------- | ||
| 15 | |||
| 16 | const scopeIdParam = HttpApiSchema.param("scopeId", ScopeId); | ||
| 17 | const toolIdParam = HttpApiSchema.param("toolId", ToolId); | ||
| 18 | |||
| 19 | // --------------------------------------------------------------------------- | ||
| 20 | // Response schemas | ||
| 21 | // --------------------------------------------------------------------------- | ||
| 22 | |||
| 23 | const ToolMetadataResponse = Schema.Struct({ | ||
| 24 | id: ToolId, | ||
| 25 | name: Schema.String, | ||
| 26 | description: Schema.optional(Schema.String), | ||
| 27 | tags: Schema.Array(Schema.String), | ||
| 28 | mayElicit: Schema.optional(Schema.Boolean), | ||
| 29 | }); | ||
| 30 | |||
| 31 | const ToolSchemaResponse = Schema.Struct({ | ||
| 32 | id: ToolId, | ||
| 33 | inputSchema: Schema.optional(Schema.Unknown), | ||
| 34 | outputSchema: Schema.optional(Schema.Unknown), | ||
| 35 | }); | ||
| 36 | |||
| 37 | const ToolInvokePayload = Schema.Struct({ | ||
| 38 | args: Schema.Unknown, | ||
| 39 | }); | ||
| 40 | |||
| 41 | const ToolInvokeResponse = Schema.Struct({ | ||
| 42 | data: Schema.Unknown, | ||
| 43 | error: Schema.NullOr(Schema.Unknown), | ||
| 44 | status: Schema.optional(Schema.Number), | ||
| 45 | }); | ||
| 46 | |||
| 47 | // --------------------------------------------------------------------------- | ||
| 48 | // Error schemas with HTTP status annotations | ||
| 49 | // --------------------------------------------------------------------------- | ||
| 50 | |||
| 51 | const ToolNotFound = ToolNotFoundError.annotations( | ||
| 52 | HttpApiSchema.annotations({ status: 404 }), | ||
| 53 | ); | ||
| 54 | const ToolInvocation = ToolInvocationError.annotations( | ||
| 55 | HttpApiSchema.annotations({ status: 500 }), | ||
| 56 | ); | ||
| 57 | const PolicyDenied = PolicyDeniedError.annotations( | ||
| 58 | HttpApiSchema.annotations({ status: 403 }), | ||
| 59 | ); | ||
| 60 | const ElicitationDeclined = ElicitationDeclinedError.annotations( | ||
| 61 | HttpApiSchema.annotations({ status: 422 }), | ||
| 62 | ); | ||
| 63 | |||
| 64 | // --------------------------------------------------------------------------- | ||
| 65 | // Group | ||
| 66 | // --------------------------------------------------------------------------- | ||
| 67 | |||
| 68 | export class ToolsApi extends HttpApiGroup.make("tools") | ||
| 69 | .add( | ||
| 70 | HttpApiEndpoint.get("list")`/scopes/${scopeIdParam}/tools` | ||
| 71 | .addSuccess(Schema.Array(ToolMetadataResponse)), | ||
| 72 | ) | ||
| 73 | .add( | ||
| 74 | HttpApiEndpoint.get("schema")`/scopes/${scopeIdParam}/tools/${toolIdParam}/schema` | ||
| 75 | .addSuccess(ToolSchemaResponse) | ||
| 76 | .addError(ToolNotFound), | ||
| 77 | ) | ||
| 78 | .add( | ||
| 79 | HttpApiEndpoint.post("invoke")`/scopes/${scopeIdParam}/tools/${toolIdParam}/invoke` | ||
| 80 | .setPayload(ToolInvokePayload) | ||
| 81 | .addSuccess(ToolInvokeResponse) | ||
| 82 | .addError(ToolNotFound) | ||
| 83 | .addError(ToolInvocation) | ||
| 84 | .addError(PolicyDenied) | ||
| 85 | .addError(ElicitationDeclined), | ||
| 86 | ) | ||
| 87 | .prefix("/v1") {} | ||
| @@ -0,0 +1,21 @@ | |||
| 1 | { | ||
| 2 | "compilerOptions": { | ||
| 3 | "target": "ESNext", | ||
| 4 | "module": "ESNext", | ||
| 5 | "moduleResolution": "bundler", | ||
| 6 | "strict": true, | ||
| 7 | "esModuleInterop": true, | ||
| 8 | "skipLibCheck": true, | ||
| 9 | "outDir": "dist", | ||
| 10 | "rootDir": "src", | ||
| 11 | "declaration": true, | ||
| 12 | "declarationMap": true, | ||
| 13 | "sourceMap": true, | ||
| 14 | "plugins": [ | ||
| 15 | { | ||
| 16 | "name": "@effect/language-service" | ||
| 17 | } | ||
| 18 | ] | ||
| 19 | }, | ||
| 20 | "include": ["src"] | ||
| 21 | } | ||
| @@ -13,8 +13,8 @@ · 12 unmodified lines | |||
| 13 | }, | 13 | }, |
| 14 | "dependencies": { | 14 | "dependencies": { |
| 15 | "@apidevtools/swagger-parser": "^12.1.0", | 15 | "@apidevtools/swagger-parser": "^12.1.0", |
| 16 | "@effect/platform": "^0.96.0", | 16 | "@effect/platform": "catalog:", |
| 17 | "@effect/platform-node": "^0.106.0", | 17 | "@effect/platform-node": "catalog:", |
| 18 | "@executor/sdk": "workspace:*", | 18 | "@executor/sdk": "workspace:*", |
| 19 | "effect": "catalog:", | 19 | "effect": "catalog:", |
| 20 | "openapi-types": "^12.1.3", | 20 | "openapi-types": "^12.1.3", |