import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"; import { Schema } from "effect"; import { InternalError } from "@executor-js/sdk/shared"; // --------------------------------------------------------------------------- // Schemas // --------------------------------------------------------------------------- const ExecuteRequest = Schema.Struct({ code: Schema.String, // When true the caller is the human approver: approval-gated tools run to // completion instead of pausing. Set by the operator-facing Run/Test panel, // where clicking Run is itself the approval. `block` policies still apply. autoApprove: Schema.optional(Schema.Boolean), /** * Set when the caller is a rendered artifact rather than the console's own * code surface — the artifact page has no MCP client, so the shell reaches * the server here instead of through `execute-action`. * * It narrows the request in both directions: the code must be one * proxy-shaped tool call, and the integration role it names is resolved * through that artifact's stored connection bindings. Absent, this is * ordinary codemode and nothing changes. */ artifactId: Schema.optional(Schema.String), }); const CompletedResult = Schema.Struct({ status: Schema.Literal("completed"), text: Schema.String, structured: Schema.Unknown, isError: Schema.Boolean, }); const PausedResult = Schema.Struct({ status: Schema.Literal("paused"), text: Schema.String, structured: Schema.Unknown, }); const ExecuteResponse = Schema.Union([CompletedResult, PausedResult]); const ResumeRequest = Schema.Struct({ action: Schema.Literals(["accept", "decline", "cancel"]), content: Schema.optional(Schema.Unknown), /** How long an accepted approval lasts, when the paused interaction's * terms offer a choice (`interaction.meta.persist` lists the scopes). * Omitted, the approval is for this call only. */ persist: Schema.optional(Schema.String), }); const ResumeResponse = Schema.Union([CompletedResult, PausedResult]); const PausedExecutionInfo = Schema.Struct({ text: Schema.String, structured: Schema.Unknown, }); const ExecutionNotFoundError = Schema.TaggedStruct("ExecutionNotFoundError", { executionId: Schema.String, }).annotate({ httpApiStatus: 404 }); /** * The approval window closed before the human answered. * * Separate from `ExecutionNotFoundError` because the two mean different things * to the person clicking Approve: an unknown id is a client bug, while an * expired approval is an ordinary outcome with a clear next step — nothing ran, * so triggering the action again is safe. The shell renders this as its * expired-approval state rather than an error. */ const ApprovalExpiredError = Schema.TaggedStruct("ApprovalExpiredError", { executionId: Schema.String, }) .annotate({ httpApiStatus: 410 }) .annotate({ description: "The approval window closed before the action was approved. Nothing ran; trigger the action again.", }); /** * An artifact-originated execution that could not be turned into a call: the * code was not the shell proxy's emission, the artifact is not this caller's, * or a role in it has no connection bound. * * `role` and `integration` accompany `binding_unresolved` so a client can offer * to rebind rather than only report the failure. */ const ArtifactActionError = Schema.TaggedStruct("ArtifactActionError", { error: Schema.Literals(["invalid_action_code", "artifact_unavailable", "binding_unresolved"]), reason: Schema.String, role: Schema.optional(Schema.String), integration: Schema.optional(Schema.String), }) .annotate({ httpApiStatus: 400 }) .annotate({ description: "A rendered artifact's tool call could not be resolved: bad shape, unknown artifact, or an unbound integration role.", }); // --------------------------------------------------------------------------- // Params // --------------------------------------------------------------------------- const ExecutionParams = { executionId: Schema.String }; // --------------------------------------------------------------------------- // Group // --------------------------------------------------------------------------- export const ExecutionsApi = HttpApiGroup.make("executions") .add( HttpApiEndpoint.get("getPaused", "/executions/:executionId", { params: ExecutionParams, success: PausedExecutionInfo, error: [InternalError, ExecutionNotFoundError], }), ) .add( HttpApiEndpoint.post("execute", "/executions", { payload: ExecuteRequest, success: ExecuteResponse, error: [InternalError, ArtifactActionError], }), ) .add( HttpApiEndpoint.post("resume", "/executions/:executionId/resume", { params: ExecutionParams, payload: ResumeRequest, success: ResumeResponse, error: [InternalError, ExecutionNotFoundError, ApprovalExpiredError], }), );