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/core/api (7 of 26 files). Show all

Unified · Split

7 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
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 @@
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+ );
packages/core/api/src/errors.ts+26−0
@@ -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+) {}
packages/core/api/src/index.ts+3−0
@@ -0,0 +1,3 @@
1+export { ExecutorApi } from "./api";
2+export { ToolsApi } from "./tools/api";
3+export { SecretsApi } from "./secrets/api";
packages/core/api/src/secrets/api.ts+78−0
@@ -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") {}
packages/core/api/src/tools/api.ts+87−0
@@ -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") {}
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+}

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