From 4f927c40698335f88f0a1e25a544f19978ef4955 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:28:54 -0700 Subject: [PATCH] feat(observability): name HTTP handlers with Effect.fn spans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert all 68 HttpApiBuilder handlers across core/api, the source plugins (mcp/graphql/openapi/onepassword), host-selfhost, and cloud from inline `(args) => Effect.gen(...)` to named `Effect.fn("group.endpoint")(function* (ctx) {...})`. Each endpoint now opens a named trace span + fiber. Input types don't infer through Effect.fn, so each handler's `ctx` is annotated from the endpoint contract; a few contract body/param schemas gained `export` to support `typeof X.Type`. Pure span-naming refactor — no behaviour change. Verified: typecheck (all 38 packages), lint (0/0, require-effect-fn-name), format, and api/cloud/plugin/host-selfhost test suites. --- apps/cloud/src/auth/api.ts | 8 +- apps/cloud/src/auth/handlers.ts | 85 +++-- apps/cloud/src/org/handlers.ts | 19 +- apps/host-selfhost/src/admin/handlers.ts | 24 +- apps/host-selfhost/src/system/handlers.ts | 10 +- packages/core/api/src/executions/api.ts | 4 +- packages/core/api/src/handlers/connections.ts | 131 +++++--- packages/core/api/src/handlers/executions.ts | 129 ++++---- packages/core/api/src/handlers/oauth.ts | 230 +++++++------ packages/core/api/src/handlers/policies.ts | 110 ++++--- packages/core/api/src/handlers/scope.ts | 45 +-- packages/core/api/src/handlers/secrets.ts | 152 +++++---- packages/core/api/src/handlers/sources.ts | 303 +++++++++++------- packages/core/api/src/handlers/tools.ts | 72 +++-- packages/core/api/src/oauth/api.ts | 10 +- packages/core/api/src/policies/api.ts | 4 +- packages/core/api/src/secrets/api.ts | 2 +- packages/plugins/graphql/src/api/group.ts | 2 +- packages/plugins/graphql/src/api/handlers.ts | 71 ++-- packages/plugins/mcp/src/api/group.ts | 6 +- packages/plugins/mcp/src/api/handlers.ts | 130 +++++--- .../plugins/onepassword/src/api/handlers.ts | 104 +++--- packages/plugins/openapi/src/api/group.ts | 6 +- packages/plugins/openapi/src/api/handlers.ts | 165 ++++++---- 24 files changed, 1077 insertions(+), 745 deletions(-) diff --git a/apps/cloud/src/auth/api.ts b/apps/cloud/src/auth/api.ts index 973ade561..bc09c95cc 100644 --- a/apps/cloud/src/auth/api.ts +++ b/apps/cloud/src/auth/api.ts @@ -31,11 +31,11 @@ const AuthOrganizationsResponse = Schema.Struct({ activeOrganizationId: Schema.NullOr(Schema.String), }); -const SwitchOrganizationBody = Schema.Struct({ +export const SwitchOrganizationBody = Schema.Struct({ organizationId: Schema.String, }); -const CreateOrganizationBody = Schema.Struct({ +export const CreateOrganizationBody = Schema.Struct({ name: Schema.String, }); @@ -69,7 +69,7 @@ const PendingInvitationsResponse = Schema.Struct({ invitations: Schema.Array(PendingInvitation), }); -const AcceptInvitationBody = Schema.Struct({ +export const AcceptInvitationBody = Schema.Struct({ invitationId: Schema.String, }); @@ -83,7 +83,7 @@ const McpSessionExecutionParams = { executionId: Schema.String, }; -const ResumeMcpExecutionBody = Schema.Struct({ +export const ResumeMcpExecutionBody = Schema.Struct({ action: Schema.Literals(["accept", "decline", "cancel"]), content: Schema.optional(Schema.Unknown), }); diff --git a/apps/cloud/src/auth/handlers.ts b/apps/cloud/src/auth/handlers.ts index 5b9e12f0c..c8b4bf3b2 100644 --- a/apps/cloud/src/auth/handlers.ts +++ b/apps/cloud/src/auth/handlers.ts @@ -4,10 +4,14 @@ import { Duration, Effect, Predicate } from "effect"; import { AUTH_PATHS, + type AcceptInvitationBody, CloudAuthApi, CloudAuthPublicApi, + type CreateOrganizationBody, McpExecutionNotFoundError, McpSessionForbiddenError, + type ResumeMcpExecutionBody, + type SwitchOrganizationBody, } from "./api"; import { NoOrganization } from "@executor-js/api/server"; import { SessionContext, SessionCookies } from "./middleware"; @@ -233,8 +237,9 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( "cloudAuth", (handlers) => handlers - .handle("me", () => - Effect.gen(function* () { + .handle( + "me", + Effect.fn("cloudAuth.me")(function* () { const session = yield* SessionContext; const org = session.organizationId ? yield* authorizeOrganization(session.accountId, session.organizationId) @@ -256,8 +261,9 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( deleteResponseCookie(HttpServerResponse.redirect("/", { status: 302 }), "wos-session"), ), ) - .handle("organizations", () => - Effect.gen(function* () { + .handle( + "organizations", + Effect.fn("cloudAuth.organizations")(function* () { const workos = yield* WorkOSClient; const session = yield* SessionContext; @@ -278,28 +284,34 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( }; }), ) - .handle("switchOrganization", ({ payload }) => - Effect.gen(function* () { + .handle( + "switchOrganization", + Effect.fn("cloudAuth.switchOrganization")(function* (ctx: { + payload: typeof SwitchOrganizationBody.Type; + }) { const workos = yield* WorkOSClient; const session = yield* SessionContext; const refreshed = yield* workos.refreshSession( session.sealedSession, - payload.organizationId, + ctx.payload.organizationId, ); if (refreshed) { (yield* SessionCookies).set("wos-session", refreshed, RESPONSE_COOKIE_OPTIONS); } }), ) - .handle("createOrganization", ({ payload }) => - Effect.gen(function* () { + .handle( + "createOrganization", + Effect.fn("cloudAuth.createOrganization")(function* (ctx: { + payload: typeof CreateOrganizationBody.Type; + }) { const workos = yield* WorkOSClient; const users = yield* UserStoreService; const session = yield* SessionContext; const autumn = yield* AutumnService; - const name = payload.name.trim(); + const name = ctx.payload.name.trim(); const memberships = yield* workos.listUserMemberships(session.accountId); const activeMemberships = memberships.data.filter( (membership) => membership.status === "active", @@ -363,8 +375,9 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( return { id: org.id, name: org.name }; }), ) - .handle("pendingInvitations", () => - Effect.gen(function* () { + .handle( + "pendingInvitations", + Effect.fn("cloudAuth.pendingInvitations")(function* () { const workos = yield* WorkOSClient; const session = yield* SessionContext; @@ -412,19 +425,22 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( }; }), ) - .handle("acceptInvitation", ({ payload }) => - Effect.gen(function* () { + .handle( + "acceptInvitation", + Effect.fn("cloudAuth.acceptInvitation")(function* (ctx: { + payload: typeof AcceptInvitationBody.Type; + }) { const workos = yield* WorkOSClient; const users = yield* UserStoreService; const session = yield* SessionContext; - const invitation = yield* workos.acceptInvitation(payload.invitationId); + const invitation = yield* workos.acceptInvitation(ctx.payload.invitationId); // Defensive: invitations created without an org shouldn't reach // this UI, but the SDK type allows null so guard anyway. if (!invitation.organizationId) { yield* Effect.logWarning("acceptInvitation: invitation has no organizationId", { - invitationId: payload.invitationId, + invitationId: ctx.payload.invitationId, }); return yield* new WorkOSError(); } @@ -456,20 +472,26 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( return { id: org.id, name: org.name }; }), ) - .handle("getMcpPaused", ({ params }) => - Effect.gen(function* () { + .handle( + "getMcpPaused", + Effect.fn("cloudAuth.getMcpPaused")(function* (ctx: { + params: { mcpSessionId: string; executionId: string }; + }) { const owner = yield* requireSessionOrganizationId; - const stub = yield* requireMcpSessionStub(params.mcpSessionId, params.executionId); + const stub = yield* requireMcpSessionStub( + ctx.params.mcpSessionId, + ctx.params.executionId, + ); const result = yield* Effect.promise( () => - stub.getPausedExecutionForApproval(params.executionId, { + stub.getPausedExecutionForApproval(ctx.params.executionId, { accountId: owner.accountId, organizationId: owner.organizationId, }) as Promise, ); if (result.status !== "ok") { - return yield* failMcpApprovalResult(result, params); + return yield* failMcpApprovalResult(result, ctx.params); } return { @@ -478,27 +500,34 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( }; }), ) - .handle("resumeMcpExecution", ({ params, payload }) => - Effect.gen(function* () { + .handle( + "resumeMcpExecution", + Effect.fn("cloudAuth.resumeMcpExecution")(function* (ctx: { + params: { mcpSessionId: string; executionId: string }; + payload: typeof ResumeMcpExecutionBody.Type; + }) { const owner = yield* requireSessionOrganizationId; - const stub = yield* requireMcpSessionStub(params.mcpSessionId, params.executionId); + const stub = yield* requireMcpSessionStub( + ctx.params.mcpSessionId, + ctx.params.executionId, + ); const result = yield* Effect.promise( () => stub.resumeExecutionForApproval( - params.executionId, + ctx.params.executionId, { accountId: owner.accountId, organizationId: owner.organizationId, }, { - action: payload.action, - content: payload.content as Record | undefined, + action: ctx.payload.action, + content: ctx.payload.content as Record | undefined, }, ) as Promise, ); if (result.status !== "ok") { - return yield* failMcpApprovalResult(result, params); + return yield* failMcpApprovalResult(result, ctx.params); } if (result.executionStatus === "paused") { diff --git a/apps/cloud/src/org/handlers.ts b/apps/cloud/src/org/handlers.ts index a12811cec..f4b287fee 100644 --- a/apps/cloud/src/org/handlers.ts +++ b/apps/cloud/src/org/handlers.ts @@ -45,8 +45,9 @@ const assertDomainInSessionOrg = (domainId: string) => export const OrgHandlers = HttpApiBuilder.group(OrgHttpApi, "org", (handlers) => handlers - .handle("listDomains", () => - Effect.gen(function* () { + .handle( + "listDomains", + Effect.fn("org.listDomains")(function* () { const auth = yield* AuthContext; const workos = yield* WorkOSClient; const org = yield* workos.getOrganization(auth.organizationId); @@ -70,8 +71,9 @@ export const OrgHandlers = HttpApiBuilder.group(OrgHttpApi, "org", (handlers) => return { domains }; }), ) - .handle("getDomainVerificationLink", () => - Effect.gen(function* () { + .handle( + "getDomainVerificationLink", + Effect.fn("org.getDomainVerificationLink")(function* () { yield* requireAdmin; const auth = yield* AuthContext; @@ -97,12 +99,13 @@ export const OrgHandlers = HttpApiBuilder.group(OrgHttpApi, "org", (handlers) => return { link }; }), ) - .handle("deleteDomain", ({ params }) => - Effect.gen(function* () { + .handle( + "deleteDomain", + Effect.fn("org.deleteDomain")(function* (ctx: { params: { domainId: string } }) { yield* requireAdmin; - yield* assertDomainInSessionOrg(params.domainId); + yield* assertDomainInSessionOrg(ctx.params.domainId); const workos = yield* WorkOSClient; - yield* workos.deleteOrganizationDomain(params.domainId); + yield* workos.deleteOrganizationDomain(ctx.params.domainId); return { success: true }; }), ), diff --git a/apps/host-selfhost/src/admin/handlers.ts b/apps/host-selfhost/src/admin/handlers.ts index 1f070c583..b9fa51081 100644 --- a/apps/host-selfhost/src/admin/handlers.ts +++ b/apps/host-selfhost/src/admin/handlers.ts @@ -7,6 +7,7 @@ import { AdminForbidden, AdminHttpApi, AdminUnauthorized, + type CreateInviteBody, type InviteCode as InviteCodeSchema, } from "./api"; import { BetterAuth, type BetterAuthHandle } from "../auth/better-auth"; @@ -62,8 +63,9 @@ const toWire = (row: InviteCodeRow): typeof InviteCodeSchema.Type => ({ export const AdminHandlers = HttpApiBuilder.group(AdminHttpApi, "admin", (handlers) => handlers - .handle("listInvites", () => - Effect.gen(function* () { + .handle( + "listInvites", + Effect.fn("admin.listInvites")(function* () { yield* requireAdmin(yield* requestHeaders); const { client } = yield* SelfHostDb; const rows = yield* Effect.tryPromise({ @@ -73,19 +75,20 @@ export const AdminHandlers = HttpApiBuilder.group(AdminHttpApi, "admin", (handle return { invites: rows.map(toWire) }; }), ) - .handle("createInvite", ({ payload }) => - Effect.gen(function* () { + .handle( + "createInvite", + Effect.fn("admin.createInvite")(function* (ctx: { payload: typeof CreateInviteBody.Type }) { const member = yield* requireAdmin(yield* requestHeaders); const { client } = yield* SelfHostDb; - const days = payload.expiresInDays ?? null; + const days = ctx.payload.expiresInDays ?? null; const expiresAt = days && days > 0 ? new Date(Date.now() + days * 86_400_000).toISOString() : null; const row = yield* Effect.tryPromise({ try: () => createInviteCode(client, { createdBy: member.userId, - role: narrowRole(payload.role), - label: payload.label?.trim() ? payload.label.trim() : null, + role: narrowRole(ctx.payload.role), + label: ctx.payload.label?.trim() ? ctx.payload.label.trim() : null, expiresAt, }), catch: () => new AdminError({ message: "Failed to create invite" }), @@ -93,12 +96,13 @@ export const AdminHandlers = HttpApiBuilder.group(AdminHttpApi, "admin", (handle return toWire(row); }), ) - .handle("revokeInvite", ({ params }) => - Effect.gen(function* () { + .handle( + "revokeInvite", + Effect.fn("admin.revokeInvite")(function* (ctx: { params: { inviteId: string } }) { yield* requireAdmin(yield* requestHeaders); const { client } = yield* SelfHostDb; yield* Effect.tryPromise({ - try: () => revokeInviteCode(client, params.inviteId), + try: () => revokeInviteCode(client, ctx.params.inviteId), catch: () => new AdminError({ message: "Failed to revoke invite" }), }); return { success: true }; diff --git a/apps/host-selfhost/src/system/handlers.ts b/apps/host-selfhost/src/system/handlers.ts index 29816beda..045e0dda5 100644 --- a/apps/host-selfhost/src/system/handlers.ts +++ b/apps/host-selfhost/src/system/handlers.ts @@ -14,8 +14,9 @@ import { SelfHostDb, type SelfHostDbHandle } from "../db/self-host-db"; export const SystemHandlers = HttpApiBuilder.group(SystemHttpApi, "system", (handlers) => handlers - .handle("health", () => - Effect.gen(function* () { + .handle( + "health", + Effect.fn("system.health")(function* () { const { client } = yield* SelfHostDb; const status = yield* Effect.tryPromise({ try: () => client.execute("SELECT 1"), @@ -27,8 +28,9 @@ export const SystemHandlers = HttpApiBuilder.group(SystemHttpApi, "system", (han return { status }; }), ) - .handle("setupStatus", () => - Effect.gen(function* () { + .handle( + "setupStatus", + Effect.fn("system.setupStatus")(function* () { const { auth, organizationId } = yield* BetterAuth; // Count via Better Auth's adapter (see countOrgMembers) so this read is // consistent with how memberships are written. diff --git a/packages/core/api/src/executions/api.ts b/packages/core/api/src/executions/api.ts index ad6eb4067..a568c51e9 100644 --- a/packages/core/api/src/executions/api.ts +++ b/packages/core/api/src/executions/api.ts @@ -7,7 +7,7 @@ import { InternalError } from "@executor-js/sdk/shared"; // Schemas // --------------------------------------------------------------------------- -const ExecuteRequest = Schema.Struct({ +export const ExecuteRequest = Schema.Struct({ code: Schema.String, }); @@ -26,7 +26,7 @@ const PausedResult = Schema.Struct({ const ExecuteResponse = Schema.Union([CompletedResult, PausedResult]); -const ResumeRequest = Schema.Struct({ +export const ResumeRequest = Schema.Struct({ action: Schema.Literals(["accept", "decline", "cancel"]), content: Schema.optional(Schema.Unknown), }); diff --git a/packages/core/api/src/handlers/connections.ts b/packages/core/api/src/handlers/connections.ts index 24c7d9c5a..5efa1479d 100644 --- a/packages/core/api/src/handlers/connections.ts +++ b/packages/core/api/src/handlers/connections.ts @@ -5,7 +5,10 @@ import { capture } from "@executor-js/api"; import { RemoveConnectionInput, UpdateConnectionIdentityInput, + type ConnectionId, + type ConnectionIdentityOverride, type ConnectionRef, + type ScopeId, } from "@executor-js/sdk"; import { ExecutorApi } from "../api"; @@ -26,62 +29,84 @@ const refToResponse = (ref: ConnectionRef) => ({ export const ConnectionsHandlers = HttpApiBuilder.group(ExecutorApi, "connections", (handlers) => handlers - .handle("list", () => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const refs = yield* executor.connections.list(); - return refs.map(refToResponse); - }), - ), + .handle( + "list", + Effect.fn("connections.list")(function* () { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const refs = yield* executor.connections.list(); + return refs.map(refToResponse); + }), + ); + }), ) - .handle("remove", ({ params: path }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - yield* executor.connections.remove( - RemoveConnectionInput.make({ - id: path.connectionId, - targetScope: path.scopeId, - }), - ); - return { removed: true }; - }), - ), + .handle( + "remove", + Effect.fn("connections.remove")(function* (ctx: { + params: { scopeId: ScopeId; connectionId: ConnectionId }; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + yield* executor.connections.remove( + RemoveConnectionInput.make({ + id: ctx.params.connectionId, + targetScope: ctx.params.scopeId, + }), + ); + return { removed: true }; + }), + ); + }), ) - .handle("usages", ({ params: path }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - return yield* executor.connections.usages(path.connectionId); - }), - ), + .handle( + "usages", + Effect.fn("connections.usages")(function* (ctx: { params: { connectionId: ConnectionId } }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + return yield* executor.connections.usages(ctx.params.connectionId); + }), + ); + }), ) - .handle("identity", ({ params: path }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - return yield* readConnectionIdentity({ - executor, - scopeId: path.scopeId, - connectionId: path.connectionId, - }); - }), - ), + .handle( + "identity", + Effect.fn("connections.identity")(function* (ctx: { + params: { scopeId: ScopeId; connectionId: ConnectionId }; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + return yield* readConnectionIdentity({ + executor, + scopeId: ctx.params.scopeId, + connectionId: ctx.params.connectionId, + }); + }), + ); + }), ) - .handle("updateIdentity", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const ref = yield* executor.connections.setIdentityOverride( - UpdateConnectionIdentityInput.make({ - id: path.connectionId, - targetScope: path.scopeId, - identityOverride: payload.identityOverride, - }), - ); - return refToResponse(ref); - }), - ), + .handle( + "updateIdentity", + Effect.fn("connections.updateIdentity")(function* (ctx: { + params: { scopeId: ScopeId; connectionId: ConnectionId }; + payload: { identityOverride: ConnectionIdentityOverride | null }; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const ref = yield* executor.connections.setIdentityOverride( + UpdateConnectionIdentityInput.make({ + id: ctx.params.connectionId, + targetScope: ctx.params.scopeId, + identityOverride: ctx.payload.identityOverride, + }), + ); + return refToResponse(ref); + }), + ); + }), ), ); diff --git a/packages/core/api/src/handlers/executions.ts b/packages/core/api/src/handlers/executions.ts index 86e1c17ac..fe455e0e5 100644 --- a/packages/core/api/src/handlers/executions.ts +++ b/packages/core/api/src/handlers/executions.ts @@ -3,6 +3,7 @@ import { Effect } from "effect"; import { Schema } from "effect"; import { ExecutorApi } from "../api"; +import { type ExecuteRequest, type ResumeRequest } from "../executions/api"; import { formatExecuteResult, formatPausedExecution } from "@executor-js/execution"; import { ExecutionEngineService } from "../services"; import { capture, captureEngineError } from "@executor-js/api"; @@ -16,77 +17,91 @@ class ExecutionNotFoundError extends Schema.TaggedErrorClass handlers - .handle("getPaused", ({ params: path }) => - capture( - Effect.gen(function* () { - const engine = yield* ExecutionEngineService; - const paused = yield* captureEngineError(engine.getPausedExecution(path.executionId)); + .handle( + "getPaused", + Effect.fn("executions.getPaused")(function* (ctx: { params: { executionId: string } }) { + return yield* capture( + Effect.gen(function* () { + const engine = yield* ExecutionEngineService; + const paused = yield* captureEngineError( + engine.getPausedExecution(ctx.params.executionId), + ); - if (!paused) { - return yield* new ExecutionNotFoundError({ executionId: path.executionId }); - } + if (!paused) { + return yield* new ExecutionNotFoundError({ executionId: ctx.params.executionId }); + } - return formatPausedExecution(paused); - }), - ), + return formatPausedExecution(paused); + }), + ); + }), ) - .handle("execute", ({ payload }) => - capture( - Effect.gen(function* () { - const engine = yield* ExecutionEngineService; - const outcome = yield* captureEngineError(engine.executeWithPause(payload.code)); + .handle( + "execute", + Effect.fn("executions.execute")(function* (ctx: { payload: typeof ExecuteRequest.Type }) { + return yield* capture( + Effect.gen(function* () { + const engine = yield* ExecutionEngineService; + const outcome = yield* captureEngineError(engine.executeWithPause(ctx.payload.code)); - if (outcome.status === "completed") { - const formatted = formatExecuteResult(outcome.result); + if (outcome.status === "completed") { + const formatted = formatExecuteResult(outcome.result); + return { + status: "completed" as const, + text: formatted.text, + structured: formatted.structured, + isError: formatted.isError, + }; + } + + const formatted = formatPausedExecution(outcome.execution); return { - status: "completed" as const, + status: "paused" as const, text: formatted.text, structured: formatted.structured, - isError: formatted.isError, }; - } - - const formatted = formatPausedExecution(outcome.execution); - return { - status: "paused" as const, - text: formatted.text, - structured: formatted.structured, - }; - }), - ), + }), + ); + }), ) - .handle("resume", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - const engine = yield* ExecutionEngineService; - const result = yield* captureEngineError( - engine.resume(path.executionId, { - action: payload.action, - content: payload.content as Record | undefined, - }), - ); + .handle( + "resume", + Effect.fn("executions.resume")(function* (ctx: { + params: { executionId: string }; + payload: typeof ResumeRequest.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const engine = yield* ExecutionEngineService; + const result = yield* captureEngineError( + engine.resume(ctx.params.executionId, { + action: ctx.payload.action, + content: ctx.payload.content as Record | undefined, + }), + ); - if (!result) { - return yield* new ExecutionNotFoundError({ executionId: path.executionId }); - } + if (!result) { + return yield* new ExecutionNotFoundError({ executionId: ctx.params.executionId }); + } - if (result.status === "completed") { - const formatted = formatExecuteResult(result.result); + if (result.status === "completed") { + const formatted = formatExecuteResult(result.result); + return { + status: "completed" as const, + text: formatted.text, + structured: formatted.structured, + isError: formatted.isError, + }; + } + + const formatted = formatPausedExecution(result.execution); return { - status: "completed" as const, + status: "paused" as const, text: formatted.text, structured: formatted.structured, - isError: formatted.isError, }; - } - - const formatted = formatPausedExecution(result.execution); - return { - status: "paused" as const, - text: formatted.text, - structured: formatted.structured, - }; - }), - ), + }), + ); + }), ), ); diff --git a/packages/core/api/src/handlers/oauth.ts b/packages/core/api/src/handlers/oauth.ts index 6013512c4..ccd886565 100644 --- a/packages/core/api/src/handlers/oauth.ts +++ b/packages/core/api/src/handlers/oauth.ts @@ -20,8 +20,16 @@ import { type OAuthStrategy, type SecretBackedValue, } from "@executor-js/sdk"; +import { type ScopeId } from "@executor-js/sdk/shared"; import { ExecutorApi } from "../api"; +import { + type CallbackUrlParams, + type CancelPayload, + type CompletePayload, + type ProbePayload, + type StartPayload, +} from "../oauth/api"; import { capture } from "../observability"; import { ExecutorService } from "../services"; @@ -96,109 +104,133 @@ const requireMatchingTokenScope = ( export const OAuthHandlers = HttpApiBuilder.group(ExecutorApi, "oauth", (handlers) => handlers - .handle("probe", ({ payload }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const headers = yield* resolveOAuthSecretBackedMap( - executor, - payload.headers, - (message) => new OAuthProbeError({ message }), - ); - const queryParams = yield* resolveOAuthSecretBackedMap( - executor, - payload.queryParams, - (message) => new OAuthProbeError({ message }), - ); - return yield* executor.oauth.probe({ - endpoint: payload.endpoint, - headers, - queryParams, - }); - }), - ), - ) - .handle("start", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - yield* requireMatchingTokenScope(path.scopeId, payload.tokenScope); - const executor = yield* ExecutorService; - const headers = yield* resolveOAuthSecretBackedMap( - executor, - payload.headers, - (message) => new OAuthStartError({ message }), - ); - const queryParams = yield* resolveOAuthSecretBackedMap( - executor, - payload.queryParams, - (message) => new OAuthStartError({ message }), - ); - return yield* executor.oauth.start({ - endpoint: payload.endpoint, - headers, - queryParams, - redirectUrl: payload.redirectUrl, - connectionId: payload.connectionId, - tokenScope: payload.tokenScope, - strategy: payload.strategy as OAuthStrategy, - pluginId: payload.pluginId, - identityLabel: payload.identityLabel, - }); - }), - ), + .handle( + "probe", + Effect.fn("oauth.probe")(function* (ctx: { payload: typeof ProbePayload.Type }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const headers = yield* resolveOAuthSecretBackedMap( + executor, + ctx.payload.headers, + (message) => new OAuthProbeError({ message }), + ); + const queryParams = yield* resolveOAuthSecretBackedMap( + executor, + ctx.payload.queryParams, + (message) => new OAuthProbeError({ message }), + ); + return yield* executor.oauth.probe({ + endpoint: ctx.payload.endpoint, + headers, + queryParams, + }); + }), + ); + }), ) - .handle("complete", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - return yield* executor.oauth.complete({ - state: payload.state, - tokenScope: path.scopeId, - code: payload.code, - error: payload.error, - }); - }), - ), + .handle( + "start", + Effect.fn("oauth.start")(function* (ctx: { + params: { scopeId: typeof ScopeId.Type }; + payload: typeof StartPayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + yield* requireMatchingTokenScope(ctx.params.scopeId, ctx.payload.tokenScope); + const executor = yield* ExecutorService; + const headers = yield* resolveOAuthSecretBackedMap( + executor, + ctx.payload.headers, + (message) => new OAuthStartError({ message }), + ); + const queryParams = yield* resolveOAuthSecretBackedMap( + executor, + ctx.payload.queryParams, + (message) => new OAuthStartError({ message }), + ); + return yield* executor.oauth.start({ + endpoint: ctx.payload.endpoint, + headers, + queryParams, + redirectUrl: ctx.payload.redirectUrl, + connectionId: ctx.payload.connectionId, + tokenScope: ctx.payload.tokenScope, + strategy: ctx.payload.strategy as OAuthStrategy, + pluginId: ctx.payload.pluginId, + identityLabel: ctx.payload.identityLabel, + }); + }), + ); + }), ) - .handle("cancel", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - if (path.scopeId !== payload.tokenScope) { - return yield* new OAuthSessionNotFoundError({ - sessionId: payload.sessionId, + .handle( + "complete", + Effect.fn("oauth.complete")(function* (ctx: { + params: { scopeId: typeof ScopeId.Type }; + payload: typeof CompletePayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + return yield* executor.oauth.complete({ + state: ctx.payload.state, + tokenScope: ctx.params.scopeId, + code: ctx.payload.code, + error: ctx.payload.error, }); - } - const executor = yield* ExecutorService; - yield* executor.oauth.cancel(payload.sessionId, payload.tokenScope); - return { cancelled: true }; - }), - ), + }), + ); + }), + ) + .handle( + "cancel", + Effect.fn("oauth.cancel")(function* (ctx: { + params: { scopeId: typeof ScopeId.Type }; + payload: typeof CancelPayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + if (ctx.params.scopeId !== ctx.payload.tokenScope) { + return yield* new OAuthSessionNotFoundError({ + sessionId: ctx.payload.sessionId, + }); + } + const executor = yield* ExecutorService; + yield* executor.oauth.cancel(ctx.payload.sessionId, ctx.payload.tokenScope); + return { cancelled: true }; + }), + ); + }), ) - .handle("callback", ({ query: urlParams }) => - // The callback always renders HTML, even on failure — the popup - // shows the error + messages it back to the opener. - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const html = yield* runOAuthCallback({ - complete: ({ state, code, error }) => - executor.oauth - .complete({ - state, - code: code ?? undefined, - error: error ?? undefined, - }) - .pipe( - Effect.tapError((cause) => - Effect.logError("OAuth callback completion failed", cause), + .handle( + "callback", + Effect.fn("oauth.callback")(function* (ctx: { query: typeof CallbackUrlParams.Type }) { + // The callback always renders HTML, even on failure — the popup + // shows the error + messages it back to the opener. + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const html = yield* runOAuthCallback({ + complete: ({ state, code, error }) => + executor.oauth + .complete({ + state, + code: code ?? undefined, + error: error ?? undefined, + }) + .pipe( + Effect.tapError((cause) => + Effect.logError("OAuth callback completion failed", cause), + ), ), - ), - urlParams, - toErrorMessage: toPopupErrorMessage, - channelName: OAUTH_POPUP_CHANNEL, - }); - return HttpServerResponse.html(html); - }), - ), + urlParams: ctx.query, + toErrorMessage: toPopupErrorMessage, + channelName: OAUTH_POPUP_CHANNEL, + }); + return HttpServerResponse.html(html); + }), + ); + }), ), ); diff --git a/packages/core/api/src/handlers/policies.ts b/packages/core/api/src/handlers/policies.ts index 40d16c7b0..88605118c 100644 --- a/packages/core/api/src/handlers/policies.ts +++ b/packages/core/api/src/handlers/policies.ts @@ -1,9 +1,11 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"; import { Effect } from "effect"; import type { ToolPolicy } from "@executor-js/sdk"; +import type { PolicyId, ScopeId } from "@executor-js/sdk/shared"; import { ExecutorApi } from "../api"; import { ExecutorService } from "../services"; +import { type CreateToolPolicyPayload, type UpdateToolPolicyPayload } from "../policies/api"; import { capture } from "@executor-js/api"; const policyToResponse = (p: ToolPolicy) => ({ @@ -18,51 +20,73 @@ const policyToResponse = (p: ToolPolicy) => ({ export const PoliciesHandlers = HttpApiBuilder.group(ExecutorApi, "policies", (handlers) => handlers - .handle("list", () => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const policies = yield* executor.policies.list(); - return policies.map(policyToResponse); - }), - ), + .handle( + "list", + Effect.fn("policies.list")(function* () { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const policies = yield* executor.policies.list(); + return policies.map(policyToResponse); + }), + ); + }), ) - .handle("create", ({ payload }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const created = yield* executor.policies.create({ - targetScope: payload.targetScope, - pattern: payload.pattern, - action: payload.action, - position: payload.position, - }); - return policyToResponse(created); - }), - ), + .handle( + "create", + Effect.fn("policies.create")(function* (ctx: { + payload: typeof CreateToolPolicyPayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const created = yield* executor.policies.create({ + targetScope: ctx.payload.targetScope, + pattern: ctx.payload.pattern, + action: ctx.payload.action, + position: ctx.payload.position, + }); + return policyToResponse(created); + }), + ); + }), ) - .handle("update", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const updated = yield* executor.policies.update({ - id: path.policyId, - targetScope: payload.targetScope, - pattern: payload.pattern, - action: payload.action, - position: payload.position, - }); - return policyToResponse(updated); - }), - ), + .handle( + "update", + Effect.fn("policies.update")(function* (ctx: { + params: { policyId: typeof PolicyId.Type }; + payload: typeof UpdateToolPolicyPayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const updated = yield* executor.policies.update({ + id: ctx.params.policyId, + targetScope: ctx.payload.targetScope, + pattern: ctx.payload.pattern, + action: ctx.payload.action, + position: ctx.payload.position, + }); + return policyToResponse(updated); + }), + ); + }), ) - .handle("remove", ({ params: path }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - yield* executor.policies.remove({ id: path.policyId, targetScope: path.scopeId }); - return { removed: true }; - }), - ), + .handle( + "remove", + Effect.fn("policies.remove")(function* (ctx: { + params: { policyId: typeof PolicyId.Type; scopeId: typeof ScopeId.Type }; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + yield* executor.policies.remove({ + id: ctx.params.policyId, + targetScope: ctx.params.scopeId, + }); + return { removed: true }; + }), + ); + }), ), ); diff --git a/packages/core/api/src/handlers/scope.ts b/packages/core/api/src/handlers/scope.ts index fab1194f8..6ddfb4c82 100644 --- a/packages/core/api/src/handlers/scope.ts +++ b/packages/core/api/src/handlers/scope.ts @@ -6,26 +6,29 @@ import { ExecutorService } from "../services"; import { capture } from "@executor-js/api"; export const ScopeHandlers = HttpApiBuilder.group(ExecutorApi, "scope", (handlers) => - handlers.handle("info", () => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - // `id` / `name` / `dir` continue to point at the outermost scope so - // existing clients keep their source writes org/workspace-scoped. - // `stack` exposes the full innermost-first scope stack so the UI can - // deliberately target per-user secret writes when binding credentials. - const scope = executor.scopes.at(-1)!; - return { - id: scope.id, - name: scope.name, - dir: scope.name, - stack: executor.scopes.map((entry) => ({ - id: entry.id, - name: entry.name, - dir: entry.name, - })), - }; - }), - ), + handlers.handle( + "info", + Effect.fn("scope.info")(function* () { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + // `id` / `name` / `dir` continue to point at the outermost scope so + // existing clients keep their source writes org/workspace-scoped. + // `stack` exposes the full innermost-first scope stack so the UI can + // deliberately target per-user secret writes when binding credentials. + const scope = executor.scopes.at(-1)!; + return { + id: scope.id, + name: scope.name, + dir: scope.name, + stack: executor.scopes.map((entry) => ({ + id: entry.id, + name: entry.name, + dir: entry.name, + })), + }; + }), + ); + }), ), ); diff --git a/packages/core/api/src/handlers/secrets.ts b/packages/core/api/src/handlers/secrets.ts index 75ffa0dcb..631dadda8 100644 --- a/packages/core/api/src/handlers/secrets.ts +++ b/packages/core/api/src/handlers/secrets.ts @@ -1,10 +1,17 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"; import { Effect } from "effect"; -import { RemoveSecretInput, SetSecretInput, type SecretRef } from "@executor-js/sdk"; +import { + RemoveSecretInput, + SetSecretInput, + type ScopeId, + type SecretId, + type SecretRef, +} from "@executor-js/sdk"; import { ExecutorApi } from "../api"; import { ExecutorService } from "../services"; import { capture } from "@executor-js/api"; +import { type SetSecretPayload } from "../secrets/api"; const refToResponse = (ref: SecretRef) => ({ id: ref.id, @@ -16,70 +23,93 @@ const refToResponse = (ref: SecretRef) => ({ export const SecretsHandlers = HttpApiBuilder.group(ExecutorApi, "secrets", (handlers) => handlers - .handle("list", () => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const refs = yield* executor.secrets.list(); - return refs.map(refToResponse); - }), - ), + .handle( + "list", + Effect.fn("secrets.list")(function* () { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const refs = yield* executor.secrets.list(); + return refs.map(refToResponse); + }), + ); + }), ) - .handle("listAll", () => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const refs = yield* executor.secrets.listAll(); - return refs.map(refToResponse); - }), - ), + .handle( + "listAll", + Effect.fn("secrets.listAll")(function* () { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const refs = yield* executor.secrets.listAll(); + return refs.map(refToResponse); + }), + ); + }), ) - .handle("status", ({ params: path }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const status = yield* executor.secrets.status(path.secretId); - return { secretId: path.secretId, status }; - }), - ), + .handle( + "status", + Effect.fn("secrets.status")(function* (ctx: { params: { secretId: SecretId } }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const status = yield* executor.secrets.status(ctx.params.secretId); + return { secretId: ctx.params.secretId, status }; + }), + ); + }), ) - .handle("set", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const ref = yield* executor.secrets.set( - SetSecretInput.make({ - id: payload.id, - scope: path.scopeId, - name: payload.name, - value: payload.value, - provider: payload.provider, - }), - ); - return refToResponse(ref); - }), - ), + .handle( + "set", + Effect.fn("secrets.set")(function* (ctx: { + params: { scopeId: ScopeId }; + payload: typeof SetSecretPayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const ref = yield* executor.secrets.set( + SetSecretInput.make({ + id: ctx.payload.id, + scope: ctx.params.scopeId, + name: ctx.payload.name, + value: ctx.payload.value, + provider: ctx.payload.provider, + }), + ); + return refToResponse(ref); + }), + ); + }), ) - .handle("remove", ({ params: path }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - yield* executor.secrets.remove( - RemoveSecretInput.make({ - id: path.secretId, - targetScope: path.scopeId, - }), - ); - return { removed: true }; - }), - ), + .handle( + "remove", + Effect.fn("secrets.remove")(function* (ctx: { + params: { scopeId: ScopeId; secretId: SecretId }; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + yield* executor.secrets.remove( + RemoveSecretInput.make({ + id: ctx.params.secretId, + targetScope: ctx.params.scopeId, + }), + ); + return { removed: true }; + }), + ); + }), ) - .handle("usages", ({ params: path }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - return yield* executor.secrets.usages(path.secretId); - }), - ), + .handle( + "usages", + Effect.fn("secrets.usages")(function* (ctx: { params: { secretId: SecretId } }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + return yield* executor.secrets.usages(ctx.params.secretId); + }), + ); + }), ), ); diff --git a/packages/core/api/src/handlers/sources.ts b/packages/core/api/src/handlers/sources.ts index e2f781da8..c4e0b2364 100644 --- a/packages/core/api/src/handlers/sources.ts +++ b/packages/core/api/src/handlers/sources.ts @@ -1,6 +1,11 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"; import { Effect } from "effect"; import { ScopeId, ToolId } from "@executor-js/sdk"; +import type { + RemoveSourceCredentialBindingInput, + ReplaceSourceCredentialBindingsInput, + SetSourceCredentialBindingInput, +} from "@executor-js/sdk/shared"; import { ExecutorApi } from "../api"; import { ExecutorService } from "../services"; @@ -8,135 +13,191 @@ import { capture } from "@executor-js/api"; export const SourcesHandlers = HttpApiBuilder.group(ExecutorApi, "sources", (handlers) => handlers - .handle("list", () => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const sources = yield* executor.sources.list(); - return sources.map((s) => ({ - id: s.id, - scopeId: s.scopeId ? ScopeId.make(s.scopeId) : undefined, - name: s.name, - kind: s.kind, - url: s.url, - runtime: s.runtime, - canRemove: s.canRemove, - canRefresh: s.canRefresh, - canEdit: s.canEdit, - connectionIds: s.connectionIds, - })); - }), - ), + .handle( + "list", + Effect.fn("sources.list")(function* () { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const sources = yield* executor.sources.list(); + return sources.map((s) => ({ + id: s.id, + scopeId: s.scopeId ? ScopeId.make(s.scopeId) : undefined, + name: s.name, + kind: s.kind, + url: s.url, + runtime: s.runtime, + canRemove: s.canRemove, + canRefresh: s.canRefresh, + canEdit: s.canEdit, + connectionIds: s.connectionIds, + })); + }), + ); + }), ) - .handle("remove", ({ params: path }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - yield* executor.sources.remove({ id: path.sourceId, targetScope: path.scopeId }); - return { removed: true }; - }), - ), + .handle( + "remove", + Effect.fn("sources.remove")(function* (ctx: { + params: { scopeId: ScopeId; sourceId: string }; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + yield* executor.sources.remove({ + id: ctx.params.sourceId, + targetScope: ctx.params.scopeId, + }); + return { removed: true }; + }), + ); + }), ) - .handle("refresh", ({ params: path }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - yield* executor.sources.refresh({ id: path.sourceId, targetScope: path.scopeId }); - return { refreshed: true }; - }), - ), + .handle( + "refresh", + Effect.fn("sources.refresh")(function* (ctx: { + params: { scopeId: ScopeId; sourceId: string }; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + yield* executor.sources.refresh({ + id: ctx.params.sourceId, + targetScope: ctx.params.scopeId, + }); + return { refreshed: true }; + }), + ); + }), ) - .handle("tools", ({ params: path }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - // Source detail is a management view — include policy-blocked - // tools so users can see and unblock them from the same place - // they review the source's other tools. Annotations are loaded - // so the UI can show the plugin's default approval state for - // tools that have no user policy override. - const tools = yield* executor.tools.list({ - sourceId: path.sourceId, - includeAnnotations: true, - includeBlocked: true, - }); - return tools.map((t) => ({ - id: ToolId.make(t.id), - pluginId: t.pluginId, - sourceId: t.sourceId, - name: t.name, - description: t.description, - mayElicit: t.annotations?.mayElicit, - requiresApproval: t.annotations?.requiresApproval, - approvalDescription: t.annotations?.approvalDescription, - })); - }), - ), + .handle( + "tools", + Effect.fn("sources.tools")(function* (ctx: { params: { sourceId: string } }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + // Source detail is a management view — include policy-blocked + // tools so users can see and unblock them from the same place + // they review the source's other tools. Annotations are loaded + // so the UI can show the plugin's default approval state for + // tools that have no user policy override. + const tools = yield* executor.tools.list({ + sourceId: ctx.params.sourceId, + includeAnnotations: true, + includeBlocked: true, + }); + return tools.map((t) => ({ + id: ToolId.make(t.id), + pluginId: t.pluginId, + sourceId: t.sourceId, + name: t.name, + description: t.description, + mayElicit: t.annotations?.mayElicit, + requiresApproval: t.annotations?.requiresApproval, + approvalDescription: t.annotations?.approvalDescription, + })); + }), + ); + }), ) - .handle("detect", ({ payload }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const results = yield* executor.sources.detect(payload.url.trim()); - return results.map((r) => ({ - kind: r.kind, - confidence: r.confidence, - endpoint: r.endpoint, - name: r.name, - namespace: r.namespace, - })); - }), - ), + .handle( + "detect", + Effect.fn("sources.detect")(function* (ctx: { payload: { url: string } }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const results = yield* executor.sources.detect(ctx.payload.url.trim()); + return results.map((r) => ({ + kind: r.kind, + confidence: r.confidence, + endpoint: r.endpoint, + name: r.name, + namespace: r.namespace, + })); + }), + ); + }), ) - .handle("configure", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - return yield* executor.sources.configure({ - source: payload.source, - scope: payload.scope ?? path.scopeId, - type: payload.type, - config: payload.config, - }); - }), - ), + .handle( + "configure", + Effect.fn("sources.configure")(function* (ctx: { + params: { scopeId: ScopeId }; + payload: { + source: { id: string; scope: ScopeId }; + scope?: ScopeId; + type?: string; + config: unknown; + }; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + return yield* executor.sources.configure({ + source: ctx.payload.source, + scope: ctx.payload.scope ?? ctx.params.scopeId, + type: ctx.payload.type, + config: ctx.payload.config, + }); + }), + ); + }), ) - .handle("listBindings", ({ params: path }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - return yield* executor.sources.listBindings({ - source: { - id: path.sourceId, - scope: path.sourceScopeId, - }, - }); - }), - ), + .handle( + "listBindings", + Effect.fn("sources.listBindings")(function* (ctx: { + params: { sourceId: string; sourceScopeId: ScopeId }; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + return yield* executor.sources.listBindings({ + source: { + id: ctx.params.sourceId, + scope: ctx.params.sourceScopeId, + }, + }); + }), + ); + }), ) - .handle("setBinding", ({ payload }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - return yield* executor.sources.setBinding(payload); - }), - ), + .handle( + "setBinding", + Effect.fn("sources.setBinding")(function* (ctx: { + payload: SetSourceCredentialBindingInput; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + return yield* executor.sources.setBinding(ctx.payload); + }), + ); + }), ) - .handle("removeBinding", ({ payload }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - yield* executor.sources.removeBinding(payload); - return { removed: true }; - }), - ), + .handle( + "removeBinding", + Effect.fn("sources.removeBinding")(function* (ctx: { + payload: RemoveSourceCredentialBindingInput; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + yield* executor.sources.removeBinding(ctx.payload); + return { removed: true }; + }), + ); + }), ) - .handle("replaceBindings", ({ payload }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - return yield* executor.sources.replaceBindings(payload); - }), - ), + .handle( + "replaceBindings", + Effect.fn("sources.replaceBindings")(function* (ctx: { + payload: ReplaceSourceCredentialBindingsInput; + }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + return yield* executor.sources.replaceBindings(ctx.payload); + }), + ); + }), ), ); diff --git a/packages/core/api/src/handlers/tools.ts b/packages/core/api/src/handlers/tools.ts index f4b7540b9..34ec277bd 100644 --- a/packages/core/api/src/handlers/tools.ts +++ b/packages/core/api/src/handlers/tools.ts @@ -8,39 +8,45 @@ import { capture } from "@executor-js/api"; export const ToolsHandlers = HttpApiBuilder.group(ExecutorApi, "tools", (handlers) => handlers - .handle("list", () => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - // Keep the all-tools view bounded to metadata already available - // from discovery. Per-source detail loads annotations for the - // smaller source-local management view. - const tools = yield* executor.tools.list({ - includeAnnotations: false, - includeBlocked: true, - }); - return tools.map((t) => ({ - id: ToolId.make(t.id), - pluginId: t.pluginId, - sourceId: t.sourceId, - name: t.name, - description: t.description, - mayElicit: t.annotations?.mayElicit, - requiresApproval: t.annotations?.requiresApproval, - })); - }), - ), + .handle( + "list", + Effect.fn("tools.list")(function* () { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + // Keep the all-tools view bounded to metadata already available + // from discovery. Per-source detail loads annotations for the + // smaller source-local management view. + const tools = yield* executor.tools.list({ + includeAnnotations: false, + includeBlocked: true, + }); + return tools.map((t) => ({ + id: ToolId.make(t.id), + pluginId: t.pluginId, + sourceId: t.sourceId, + name: t.name, + description: t.description, + mayElicit: t.annotations?.mayElicit, + requiresApproval: t.annotations?.requiresApproval, + })); + }), + ); + }), ) - .handle("schema", ({ params: path }) => - capture( - Effect.gen(function* () { - const executor = yield* ExecutorService; - const schema = yield* executor.tools.schema(path.toolId); - if (schema === null) { - return yield* new ToolNotFoundError({ toolId: path.toolId }); - } - return schema; - }), - ), + .handle( + "schema", + Effect.fn("tools.schema")(function* (ctx: { params: { toolId: typeof ToolId.Type } }) { + return yield* capture( + Effect.gen(function* () { + const executor = yield* ExecutorService; + const schema = yield* executor.tools.schema(ctx.params.toolId); + if (schema === null) { + return yield* new ToolNotFoundError({ toolId: ctx.params.toolId }); + } + return schema; + }), + ); + }), ), ); diff --git a/packages/core/api/src/oauth/api.ts b/packages/core/api/src/oauth/api.ts index a7af770b1..7d854cd29 100644 --- a/packages/core/api/src/oauth/api.ts +++ b/packages/core/api/src/oauth/api.ts @@ -26,7 +26,7 @@ const ScopeParams = { scopeId: ScopeId }; // Probe — decide between dynamic-DCR and paste-your-credentials flows // --------------------------------------------------------------------------- -const ProbePayload = Schema.Struct({ +export const ProbePayload = Schema.Struct({ endpoint: Schema.String, headers: Schema.optional(SecretBackedMap), queryParams: Schema.optional(SecretBackedMap), @@ -48,7 +48,7 @@ const ProbeResponse = Schema.Struct({ // Connection inline and returns it under `completedConnection`. // --------------------------------------------------------------------------- -const StartPayload = Schema.Struct({ +export const StartPayload = Schema.Struct({ /** Resource URL — used by probe/display, not by the start flow for * static strategies. */ endpoint: Schema.String, @@ -85,7 +85,7 @@ const StartResponse = Schema.Struct({ // Complete — exchange the code, mint the Connection, drop the session. // --------------------------------------------------------------------------- -const CompletePayload = Schema.Struct({ +export const CompletePayload = Schema.Struct({ state: Schema.String, code: Schema.optional(Schema.String), error: Schema.optional(Schema.String), @@ -101,7 +101,7 @@ const CompleteResponse = Schema.Struct({ // Cancel — drop an in-flight session without exchanging. // --------------------------------------------------------------------------- -const CancelPayload = Schema.Struct({ +export const CancelPayload = Schema.Struct({ sessionId: Schema.String, /** Scope that owns the pending OAuth session. Must match start.tokenScope. */ tokenScope: Schema.String, @@ -117,7 +117,7 @@ const CancelResponse = Schema.Struct({ // result back to the opener via `postMessage` / `BroadcastChannel`. // --------------------------------------------------------------------------- -const CallbackUrlParams = Schema.Struct({ +export const CallbackUrlParams = Schema.Struct({ state: Schema.String, code: Schema.optional(Schema.String), error: Schema.optional(Schema.String), diff --git a/packages/core/api/src/policies/api.ts b/packages/core/api/src/policies/api.ts index 21fdad29e..3e169fe76 100644 --- a/packages/core/api/src/policies/api.ts +++ b/packages/core/api/src/policies/api.ts @@ -23,14 +23,14 @@ const ToolPolicyResponse = Schema.Struct({ updatedAt: Schema.Number, }); -const CreateToolPolicyPayload = Schema.Struct({ +export const CreateToolPolicyPayload = Schema.Struct({ targetScope: ScopeId, pattern: Schema.String, action: ToolPolicyActionSchema, position: Schema.optional(Schema.String), }); -const UpdateToolPolicyPayload = Schema.Struct({ +export const UpdateToolPolicyPayload = Schema.Struct({ targetScope: ScopeId, pattern: Schema.optional(Schema.String), action: Schema.optional(ToolPolicyActionSchema), diff --git a/packages/core/api/src/secrets/api.ts b/packages/core/api/src/secrets/api.ts index a204ab32e..7b0da4085 100644 --- a/packages/core/api/src/secrets/api.ts +++ b/packages/core/api/src/secrets/api.ts @@ -35,7 +35,7 @@ const SecretStatusResponse = Schema.Struct({ status: Schema.Literals(["resolved", "missing"]), }); -const SetSecretPayload = Schema.Struct({ +export const SetSecretPayload = Schema.Struct({ id: SecretId, name: Schema.String, value: Schema.String, diff --git a/packages/plugins/graphql/src/api/group.ts b/packages/plugins/graphql/src/api/group.ts index 90798858a..251503fc5 100644 --- a/packages/plugins/graphql/src/api/group.ts +++ b/packages/plugins/graphql/src/api/group.ts @@ -41,7 +41,7 @@ const SourceParams = { // Payloads // --------------------------------------------------------------------------- -const AddSourcePayload = Schema.Struct({ +export const AddSourcePayload = Schema.Struct({ endpoint: Schema.String, name: Schema.String, introspectionJson: Schema.optional(Schema.String), diff --git a/packages/plugins/graphql/src/api/handlers.ts b/packages/plugins/graphql/src/api/handlers.ts index 38c1a4cfb..4616ce1c8 100644 --- a/packages/plugins/graphql/src/api/handlers.ts +++ b/packages/plugins/graphql/src/api/handlers.ts @@ -4,7 +4,7 @@ import { Context, Effect } from "effect"; import { addGroup, capture } from "@executor-js/api"; import { ScopeId } from "@executor-js/sdk/core"; import type { GraphqlPluginExtension } from "../sdk/plugin"; -import { GraphqlGroup } from "./group"; +import { type AddSourcePayload, GraphqlGroup } from "./group"; // --------------------------------------------------------------------------- // Service tag @@ -40,35 +40,46 @@ const ExecutorApiWithGraphql = addGroup(GraphqlGroup); export const GraphqlHandlers = HttpApiBuilder.group(ExecutorApiWithGraphql, "graphql", (handlers) => handlers - .handle("addSource", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - const ext = yield* GraphqlExtensionService; - const result = yield* ext.addSource({ - endpoint: payload.endpoint, - scope: path.scopeId, - name: payload.name, - introspectionJson: payload.introspectionJson, - namespace: payload.namespace, - headers: payload.headers, - queryParams: payload.queryParams, - oauth2: payload.oauth2, - credentials: payload.credentials, - }); - return { - toolCount: result.toolCount, - namespace: result.namespace, - }; - }), - ), + .handle( + "addSource", + Effect.fn("graphql.addSource")(function* (ctx: { + params: { scopeId: typeof ScopeId.Type }; + payload: typeof AddSourcePayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* GraphqlExtensionService; + const result = yield* ext.addSource({ + endpoint: ctx.payload.endpoint, + scope: ctx.params.scopeId, + name: ctx.payload.name, + introspectionJson: ctx.payload.introspectionJson, + namespace: ctx.payload.namespace, + headers: ctx.payload.headers, + queryParams: ctx.payload.queryParams, + oauth2: ctx.payload.oauth2, + credentials: ctx.payload.credentials, + }); + return { + toolCount: result.toolCount, + namespace: result.namespace, + }; + }), + ); + }), ) - .handle("getSource", ({ params: path }) => - capture( - Effect.gen(function* () { - const ext = yield* GraphqlExtensionService; - const source = yield* ext.getSource(path.namespace, path.scopeId); - return source ? { ...source, scope: ScopeId.make(source.scope) } : null; - }), - ), + .handle( + "getSource", + Effect.fn("graphql.getSource")(function* (ctx: { + params: { scopeId: typeof ScopeId.Type; namespace: string }; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* GraphqlExtensionService; + const source = yield* ext.getSource(ctx.params.namespace, ctx.params.scopeId); + return source ? { ...source, scope: ScopeId.make(source.scope) } : null; + }), + ); + }), ), ); diff --git a/packages/plugins/mcp/src/api/group.ts b/packages/plugins/mcp/src/api/group.ts index 8c03efe9b..7cd41b4e4 100644 --- a/packages/plugins/mcp/src/api/group.ts +++ b/packages/plugins/mcp/src/api/group.ts @@ -48,9 +48,9 @@ const AddStdioSourcePayload = Schema.Struct({ namespace: Schema.optional(Schema.String), }); -const AddSourcePayload = Schema.Union([AddRemoteSourcePayload, AddStdioSourcePayload]); +export const AddSourcePayload = Schema.Union([AddRemoteSourcePayload, AddStdioSourcePayload]); -const ProbeEndpointPayload = Schema.Struct({ +export const ProbeEndpointPayload = Schema.Struct({ endpoint: Schema.String, headers: Schema.optional(SecretBackedMap), queryParams: Schema.optional(SecretBackedMap), @@ -66,7 +66,7 @@ const ProbeEndpointResponse = Schema.Struct({ serverName: Schema.NullOr(Schema.String), }); -const NamespacePayload = Schema.Struct({ +export const NamespacePayload = Schema.Struct({ namespace: Schema.String, }); diff --git a/packages/plugins/mcp/src/api/handlers.ts b/packages/plugins/mcp/src/api/handlers.ts index b6cea24e0..55222db57 100644 --- a/packages/plugins/mcp/src/api/handlers.ts +++ b/packages/plugins/mcp/src/api/handlers.ts @@ -7,7 +7,12 @@ import type { OAuth2SourceConfigType } from "@executor-js/sdk/http-source"; import type { McpPluginExtension, McpProbeEndpointInput, McpSourceConfig } from "../sdk/plugin"; import type { McpConfiguredValueInput } from "../sdk/types"; import { McpStoredSourceSchema } from "../sdk/stored-source"; -import { McpGroup } from "./group"; +import { + McpGroup, + type AddSourcePayload, + type NamespacePayload, + type ProbeEndpointPayload, +} from "./group"; // --------------------------------------------------------------------------- // Service tag — holds the raw extension shape the executor produces. @@ -98,55 +103,86 @@ const toSourceConfig = ( export const McpHandlers = HttpApiBuilder.group(ExecutorApiWithMcp, "mcp", (handlers) => handlers - .handle("probeEndpoint", ({ payload }) => - capture( - Effect.gen(function* () { - const ext = yield* McpExtensionService; - return yield* ext.probeEndpoint(payload as McpProbeEndpointInput); - }), - ), + .handle( + "probeEndpoint", + Effect.fn("mcp.probeEndpoint")(function* (ctx: { + payload: typeof ProbeEndpointPayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* McpExtensionService; + return yield* ext.probeEndpoint(ctx.payload as McpProbeEndpointInput); + }), + ); + }), ) - .handle("addSource", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - const ext = yield* McpExtensionService; - return yield* ext.addSource( - toSourceConfig(payload as Parameters[0], path.scopeId), - ); - }), - ), + .handle( + "addSource", + Effect.fn("mcp.addSource")(function* (ctx: { + params: { scopeId: ScopeId }; + payload: typeof AddSourcePayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* McpExtensionService; + return yield* ext.addSource( + toSourceConfig( + ctx.payload as Parameters[0], + ctx.params.scopeId, + ), + ); + }), + ); + }), ) - .handle("removeSource", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - const ext = yield* McpExtensionService; - yield* ext.removeSource(payload.namespace, path.scopeId); - return { removed: true }; - }), - ), + .handle( + "removeSource", + Effect.fn("mcp.removeSource")(function* (ctx: { + params: { scopeId: ScopeId }; + payload: typeof NamespacePayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* McpExtensionService; + yield* ext.removeSource(ctx.payload.namespace, ctx.params.scopeId); + return { removed: true }; + }), + ); + }), ) - .handle("refreshSource", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - const ext = yield* McpExtensionService; - return yield* ext.refreshSource(payload.namespace, path.scopeId); - }), - ), + .handle( + "refreshSource", + Effect.fn("mcp.refreshSource")(function* (ctx: { + params: { scopeId: ScopeId }; + payload: typeof NamespacePayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* McpExtensionService; + return yield* ext.refreshSource(ctx.payload.namespace, ctx.params.scopeId); + }), + ); + }), ) - .handle("getSource", ({ params: path }) => - capture( - Effect.gen(function* () { - const ext = yield* McpExtensionService; - const source = yield* ext.getSource(path.namespace, path.scopeId); - return source - ? McpStoredSourceSchema.make({ - namespace: source.namespace, - scope: ScopeId.make(source.scope), - name: source.name, - config: source.config, - }) - : null; - }), - ), + .handle( + "getSource", + Effect.fn("mcp.getSource")(function* (ctx: { + params: { scopeId: ScopeId; namespace: string }; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* McpExtensionService; + const source = yield* ext.getSource(ctx.params.namespace, ctx.params.scopeId); + return source + ? McpStoredSourceSchema.make({ + namespace: source.namespace, + scope: ScopeId.make(source.scope), + name: source.name, + config: source.config, + }) + : null; + }), + ); + }), ), ); diff --git a/packages/plugins/onepassword/src/api/handlers.ts b/packages/plugins/onepassword/src/api/handlers.ts index b24989073..2fd07db8f 100644 --- a/packages/plugins/onepassword/src/api/handlers.ts +++ b/packages/plugins/onepassword/src/api/handlers.ts @@ -1,8 +1,10 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"; import { Context, Effect } from "effect"; +import { type ScopeId } from "@executor-js/sdk/shared"; import { addGroup, capture } from "@executor-js/api"; import type { OnePasswordExtension } from "../sdk/plugin"; +import { type OnePasswordConfig } from "../sdk/types"; import { OnePasswordGroup } from "./group"; // --------------------------------------------------------------------------- @@ -42,49 +44,71 @@ export const OnePasswordHandlers = HttpApiBuilder.group( "onepassword", (handlers) => handlers - .handle("getConfig", () => - capture( - Effect.gen(function* () { - const ext = yield* OnePasswordExtensionService; - return yield* ext.getConfig(); - }), - ), + .handle( + "getConfig", + Effect.fn("onepassword.getConfig")(function* () { + return yield* capture( + Effect.gen(function* () { + const ext = yield* OnePasswordExtensionService; + return yield* ext.getConfig(); + }), + ); + }), ) - .handle("configure", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - const ext = yield* OnePasswordExtensionService; - yield* ext.configure(payload, path.scopeId); - }), - ), + .handle( + "configure", + Effect.fn("onepassword.configure")(function* (ctx: { + params: { scopeId: typeof ScopeId.Type }; + payload: typeof OnePasswordConfig.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* OnePasswordExtensionService; + yield* ext.configure(ctx.payload, ctx.params.scopeId); + }), + ); + }), ) - .handle("removeConfig", ({ params: path }) => - capture( - Effect.gen(function* () { - const ext = yield* OnePasswordExtensionService; - yield* ext.removeConfig(path.scopeId); - }), - ), + .handle( + "removeConfig", + Effect.fn("onepassword.removeConfig")(function* (ctx: { + params: { scopeId: typeof ScopeId.Type }; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* OnePasswordExtensionService; + yield* ext.removeConfig(ctx.params.scopeId); + }), + ); + }), ) - .handle("status", () => - capture( - Effect.gen(function* () { - const ext = yield* OnePasswordExtensionService; - return yield* ext.status(); - }), - ), + .handle( + "status", + Effect.fn("onepassword.status")(function* () { + return yield* capture( + Effect.gen(function* () { + const ext = yield* OnePasswordExtensionService; + return yield* ext.status(); + }), + ); + }), ) - .handle("listVaults", ({ query: urlParams }) => - capture( - Effect.gen(function* () { - const ext = yield* OnePasswordExtensionService; - const auth = - urlParams.authKind === "desktop-app" - ? { kind: "desktop-app" as const, accountName: urlParams.account } - : { kind: "service-account" as const, tokenSecretId: urlParams.account }; - const vaults = yield* ext.listVaults(auth); - return { vaults: [...vaults] }; - }), - ), + .handle( + "listVaults", + Effect.fn("onepassword.listVaults")(function* (ctx: { + query: { authKind: "desktop-app" | "service-account"; account: string }; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* OnePasswordExtensionService; + const auth = + ctx.query.authKind === "desktop-app" + ? { kind: "desktop-app" as const, accountName: ctx.query.account } + : { kind: "service-account" as const, tokenSecretId: ctx.query.account }; + const vaults = yield* ext.listVaults(auth); + return { vaults: [...vaults] }; + }), + ); + }), ), ); diff --git a/packages/plugins/openapi/src/api/group.ts b/packages/plugins/openapi/src/api/group.ts index d30fc1b83..0f9758e8f 100644 --- a/packages/plugins/openapi/src/api/group.ts +++ b/packages/plugins/openapi/src/api/group.ts @@ -64,7 +64,7 @@ const SpecFetchCredentialsPayload = Schema.Struct({ // Payloads // --------------------------------------------------------------------------- -const AddSpecPayload = Schema.Struct({ +export const AddSpecPayload = Schema.Struct({ spec: OpenApiSpecInputPayload, specFetchCredentials: Schema.optional(SpecFetchCredentialsPayload), name: Schema.String, @@ -75,7 +75,7 @@ const AddSpecPayload = Schema.Struct({ oauth2: Schema.optional(OAuth2SourceConfig), }); -const PreviewSpecPayload = Schema.Struct({ +export const PreviewSpecPayload = Schema.Struct({ spec: Schema.String, specFetchCredentials: Schema.optional(PreviewSpecFetchCredentialsPayload), }); @@ -101,7 +101,7 @@ const ConfigureCredentialPayload = Schema.Union([ const ConfigureCredentialMapPayload = Schema.Record(Schema.String, ConfigureCredentialPayload); -const ConfigurePayload = Schema.Struct({ +export const ConfigurePayload = Schema.Struct({ source: Schema.Struct({ id: Schema.String, scope: ScopeId, diff --git a/packages/plugins/openapi/src/api/handlers.ts b/packages/plugins/openapi/src/api/handlers.ts index ac50dcd88..4d6887065 100644 --- a/packages/plugins/openapi/src/api/handlers.ts +++ b/packages/plugins/openapi/src/api/handlers.ts @@ -2,6 +2,7 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"; import { Context, Effect } from "effect"; import { addGroup, capture } from "@executor-js/api"; +import type { ScopeId } from "@executor-js/sdk/shared"; import type { OpenApiConfiguredValueInput, OpenApiConfigureInput, @@ -10,7 +11,12 @@ import type { OpenApiSpecFetchCredentialsInput, } from "../sdk/plugin"; import { StoredSourceSchema } from "../sdk/store"; -import { OpenApiGroup } from "./group"; +import { + OpenApiGroup, + type AddSpecPayload, + type ConfigurePayload, + type PreviewSpecPayload, +} from "./group"; // --------------------------------------------------------------------------- // Service tag @@ -46,76 +52,97 @@ const ExecutorApiWithOpenApi = addGroup(OpenApiGroup); export const OpenApiHandlers = HttpApiBuilder.group(ExecutorApiWithOpenApi, "openapi", (handlers) => handlers - .handle("previewSpec", ({ payload }) => - capture( - Effect.gen(function* () { - const ext = yield* OpenApiExtensionService; - return yield* ext.previewSpec({ - spec: payload.spec, - specFetchCredentials: payload.specFetchCredentials as - | OpenApiPreviewSpecFetchCredentialsInput - | undefined, - }); - }), - ), + .handle( + "previewSpec", + Effect.fn("openapi.previewSpec")(function* (ctx: { + payload: typeof PreviewSpecPayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* OpenApiExtensionService; + return yield* ext.previewSpec({ + spec: ctx.payload.spec, + specFetchCredentials: ctx.payload.specFetchCredentials as + | OpenApiPreviewSpecFetchCredentialsInput + | undefined, + }); + }), + ); + }), ) - .handle("addSpec", ({ params: path, payload }) => - capture( - Effect.gen(function* () { - const ext = yield* OpenApiExtensionService; - const result = yield* ext.addSpec({ - spec: payload.spec, - specFetchCredentials: payload.specFetchCredentials as - | OpenApiSpecFetchCredentialsInput - | undefined, - scope: path.scopeId, - name: payload.name, - baseUrl: payload.baseUrl, - namespace: payload.namespace, - headers: payload.headers as Record | undefined, - queryParams: payload.queryParams as - | Record - | undefined, - oauth2: payload.oauth2, - }); - return { - toolCount: result.toolCount, - namespace: result.sourceId, - }; - }), - ), + .handle( + "addSpec", + Effect.fn("openapi.addSpec")(function* (ctx: { + params: { scopeId: ScopeId }; + payload: typeof AddSpecPayload.Type; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* OpenApiExtensionService; + const result = yield* ext.addSpec({ + spec: ctx.payload.spec, + specFetchCredentials: ctx.payload.specFetchCredentials as + | OpenApiSpecFetchCredentialsInput + | undefined, + scope: ctx.params.scopeId, + name: ctx.payload.name, + baseUrl: ctx.payload.baseUrl, + namespace: ctx.payload.namespace, + headers: ctx.payload.headers as + | Record + | undefined, + queryParams: ctx.payload.queryParams as + | Record + | undefined, + oauth2: ctx.payload.oauth2, + }); + return { + toolCount: result.toolCount, + namespace: result.sourceId, + }; + }), + ); + }), ) - .handle("getSource", ({ params: path }) => - capture( - Effect.gen(function* () { - const ext = yield* OpenApiExtensionService; - const source = yield* ext.getSource(path.namespace, path.scopeId); - return source - ? StoredSourceSchema.make({ - namespace: source.namespace, - scope: source.scope, - name: source.name, - config: { - sourceUrl: source.config.sourceUrl, - googleDiscoveryUrls: source.config.googleDiscoveryUrls, - baseUrl: source.config.baseUrl, - namespace: source.config.namespace, - headers: source.config.headers, - queryParams: source.config.queryParams, - specFetchCredentials: source.config.specFetchCredentials, - oauth2: source.config.oauth2, - }, - }) - : null; - }), - ), + .handle( + "getSource", + Effect.fn("openapi.getSource")(function* (ctx: { + params: { scopeId: ScopeId; namespace: string }; + }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* OpenApiExtensionService; + const source = yield* ext.getSource(ctx.params.namespace, ctx.params.scopeId); + return source + ? StoredSourceSchema.make({ + namespace: source.namespace, + scope: source.scope, + name: source.name, + config: { + sourceUrl: source.config.sourceUrl, + googleDiscoveryUrls: source.config.googleDiscoveryUrls, + baseUrl: source.config.baseUrl, + namespace: source.config.namespace, + headers: source.config.headers, + queryParams: source.config.queryParams, + specFetchCredentials: source.config.specFetchCredentials, + oauth2: source.config.oauth2, + }, + }) + : null; + }), + ); + }), ) - .handle("configure", ({ payload }) => - capture( - Effect.gen(function* () { - const ext = yield* OpenApiExtensionService; - return yield* ext.configure(payload.source, payload as OpenApiConfigureInput); - }), - ), + .handle( + "configure", + Effect.fn("openapi.configure")(function* (ctx: { payload: typeof ConfigurePayload.Type }) { + return yield* capture( + Effect.gen(function* () { + const ext = yield* OpenApiExtensionService; + return yield* ext.configure(ctx.payload.source, ctx.payload as OpenApiConfigureInput); + }), + ); + }), ), );