History

feat: API contract, server handler, and frontend with effect-atom

- 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

Rhys Sullivan committed 7 months ago

Commit 8babea81 parent 00219a926 files · +1022 −32Browse files

Showing the changes to packages (8 of 26 files). Show all

Unified · Split

8 files
  1. packages/core/api/package.json +22 −0
  2. packages/core/api/src/api.ts +14 −0
  3. packages/core/api/src/errors.ts +26 −0
  4. packages/core/api/src/index.ts +3 −0
  5. packages/core/api/src/secrets/api.ts +78 −0
  6. packages/core/api/src/tools/api.ts +87 −0
  7. packages/core/api/tsconfig.json +21 −0
  8. packages/plugins/openapi/package.json +2 −2
packages/core/api/package.json+22−0
@@ -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}
packages/core/api/src/api.ts+14−0
@@ -0,0 +1,14 @@
1import { HttpApi, OpenApi } from "@effect/platform";
2
3import { ToolsApi } from "./tools/api";
4import { SecretsApi } from "./secrets/api";
5
6export 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 );
packages/core/api/src/errors.ts+26−0
@@ -0,0 +1,26 @@
1import { HttpApiSchema } from "@effect/platform";
2import { Schema } from "effect";
3
4export class ApiNotFoundError extends Schema.TaggedError<ApiNotFoundError>()(
5 "ApiNotFoundError",
6 {
7 message: Schema.String,
8 },
9 HttpApiSchema.annotations({ status: 404 }),
10) {}
11
12export class ApiBadRequestError extends Schema.TaggedError<ApiBadRequestError>()(
13 "ApiBadRequestError",
14 {
15 message: Schema.String,
16 },
17 HttpApiSchema.annotations({ status: 400 }),
18) {}
19
20export class ApiInternalError extends Schema.TaggedError<ApiInternalError>()(
21 "ApiInternalError",
22 {
23 message: Schema.String,
24 },
25 HttpApiSchema.annotations({ status: 500 }),
26) {}
packages/core/api/src/index.ts+3−0
@@ -0,0 +1,3 @@
1export { ExecutorApi } from "./api";
2export { ToolsApi } from "./tools/api";
3export { SecretsApi } from "./secrets/api";
packages/core/api/src/secrets/api.ts+78−0
@@ -0,0 +1,78 @@
1import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
2import { Schema } from "effect";
3import {
4 ScopeId,
5 SecretId,
6 SecretNotFoundError,
7 SecretResolutionError,
8} from "@executor/sdk";
9
10// ---------------------------------------------------------------------------
11// Params
12// ---------------------------------------------------------------------------
13
14const scopeIdParam = HttpApiSchema.param("scopeId", ScopeId);
15const secretIdParam = HttpApiSchema.param("secretId", SecretId);
16
17// ---------------------------------------------------------------------------
18// Response / payload schemas
19// ---------------------------------------------------------------------------
20
21const 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
30const SecretStatusResponse = Schema.Struct({
31 secretId: SecretId,
32 status: Schema.Literal("resolved", "missing"),
33});
34
35const 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
47const SecretNotFound = SecretNotFoundError.annotations(
48 HttpApiSchema.annotations({ status: 404 }),
49);
50const SecretResolution = SecretResolutionError.annotations(
51 HttpApiSchema.annotations({ status: 500 }),
52);
53
54// ---------------------------------------------------------------------------
55// Group
56// ---------------------------------------------------------------------------
57
58export 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") {}
packages/core/api/src/tools/api.ts+87−0
@@ -0,0 +1,87 @@
1import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
2import { Schema } from "effect";
3import {
4 ScopeId,
5 ToolId,
6 ToolNotFoundError,
7 ToolInvocationError,
8 PolicyDeniedError,
9 ElicitationDeclinedError,
10} from "@executor/sdk";
11
12// ---------------------------------------------------------------------------
13// Params
14// ---------------------------------------------------------------------------
15
16const scopeIdParam = HttpApiSchema.param("scopeId", ScopeId);
17const toolIdParam = HttpApiSchema.param("toolId", ToolId);
18
19// ---------------------------------------------------------------------------
20// Response schemas
21// ---------------------------------------------------------------------------
22
23const 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
31const ToolSchemaResponse = Schema.Struct({
32 id: ToolId,
33 inputSchema: Schema.optional(Schema.Unknown),
34 outputSchema: Schema.optional(Schema.Unknown),
35});
36
37const ToolInvokePayload = Schema.Struct({
38 args: Schema.Unknown,
39});
40
41const 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
51const ToolNotFound = ToolNotFoundError.annotations(
52 HttpApiSchema.annotations({ status: 404 }),
53);
54const ToolInvocation = ToolInvocationError.annotations(
55 HttpApiSchema.annotations({ status: 500 }),
56);
57const PolicyDenied = PolicyDeniedError.annotations(
58 HttpApiSchema.annotations({ status: 403 }),
59);
60const ElicitationDeclined = ElicitationDeclinedError.annotations(
61 HttpApiSchema.annotations({ status: 422 }),
62);
63
64// ---------------------------------------------------------------------------
65// Group
66// ---------------------------------------------------------------------------
67
68export 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") {}
packages/core/api/tsconfig.json+21−0
@@ -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}
packages/plugins/openapi/package.json+2−2
@@ -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",

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t