diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index f609f7f1748c..9873b1770f4e 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -1484,6 +1484,47 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); + it.effect("rejects WebSocket-only turn bootstrap at the HTTP schema", () => + Effect.gen(function* () { + yield* buildAppUnderTest(); + + const createdAt = "2026-01-01T00:00:00.000Z"; + const response = yield* HttpClient.post("/api/orchestration/dispatch", { + headers: { + cookie: yield* getAuthenticatedSessionCookieHeader(), + }, + body: yield* HttpBody.json({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-http-bootstrap-turn-start"), + threadId: ThreadId.make("thread-http-bootstrap"), + message: { + messageId: MessageId.make("msg-http-bootstrap"), + role: "user", + text: "hello", + attachments: [], + }, + modelSelection: defaultModelSelection, + runtimeMode: "full-access", + interactionMode: "default", + bootstrap: { + createThread: { + projectId: defaultProjectId, + title: "Bootstrap Thread", + modelSelection: defaultModelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: "main", + worktreePath: null, + createdAt, + }, + }, + createdAt, + }), + }); + assert.equal(response.status, 400); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + it.effect("serves static index content for GET / when staticDir is configured", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; diff --git a/packages/contracts/src/environmentHttp.ts b/packages/contracts/src/environmentHttp.ts index a895697e36b0..00c16fe0ee27 100644 --- a/packages/contracts/src/environmentHttp.ts +++ b/packages/contracts/src/environmentHttp.ts @@ -32,8 +32,8 @@ import { } from "./baseSchemas.ts"; import { ExecutionEnvironmentDescriptor } from "./environment.ts"; import { - ClientOrchestrationCommand, DispatchResult, + HttpOrchestrationCommand, OrchestrationReadModel, OrchestrationShellSnapshot, OrchestrationThreadDetailSnapshot, @@ -531,7 +531,7 @@ export class EnvironmentOrchestrationHttpApi extends HttpApiGroup.make("orchestr .add( HttpApiEndpoint.post("dispatch", "/api/orchestration/dispatch", { headers: OptionalBearerHeaders, - payload: ClientOrchestrationCommand, + payload: HttpOrchestrationCommand, success: DispatchResult, error: EnvironmentOrchestrationDispatchErrors, }).middleware(EnvironmentAuthenticatedAuth), diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 7ca7175ad175..ace57ec3411d 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -912,7 +912,7 @@ export const ThreadTurnStartCommand = Schema.Struct({ createdAt: IsoDateTime, }); -const ClientThreadTurnStartCommand = Schema.Struct({ +const ClientThreadTurnStartCommandFields = { type: Schema.Literal("thread.turn.start"), commandId: CommandId, threadId: ThreadId, @@ -926,9 +926,17 @@ const ClientThreadTurnStartCommand = Schema.Struct({ titleSeed: Schema.optional(TrimmedNonEmptyString), runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode, - bootstrap: Schema.optional(ThreadTurnStartBootstrap), sourceProposedPlan: Schema.optional(SourceProposedPlanReference), createdAt: IsoDateTime, +}; + +const ClientThreadTurnStartCommand = Schema.Struct({ + ...ClientThreadTurnStartCommandFields, + bootstrap: Schema.optional(ThreadTurnStartBootstrap), +}); + +const HttpThreadTurnStartCommand = Schema.Struct(ClientThreadTurnStartCommandFields).annotate({ + parseOptions: { onExcessProperty: "error" }, }); const ThreadTurnInterruptCommand = Schema.Struct({ @@ -1033,6 +1041,33 @@ export const ClientOrchestrationCommand = Schema.Union([ ]); export type ClientOrchestrationCommand = typeof ClientOrchestrationCommand.Type; +export const HttpOrchestrationCommand = Schema.Union([ + ProjectCreateCommand, + ProjectMetaUpdateCommand, + ProjectDeleteCommand, + ThreadCreateCommand, + ThreadDeleteCommand, + ThreadArchiveCommand, + ThreadUnarchiveCommand, + ThreadSettleCommand, + ThreadUnsettleCommand, + ThreadSnoozeCommand, + ThreadUnsnoozeCommand, + ThreadPinCommand, + ThreadUnpinCommand, + ThreadPinReorderCommand, + ThreadMetaUpdateCommand, + ThreadRuntimeModeSetCommand, + ThreadInteractionModeSetCommand, + HttpThreadTurnStartCommand, + ThreadTurnInterruptCommand, + ThreadApprovalRespondCommand, + ThreadUserInputRespondCommand, + ThreadCheckpointRevertCommand, + ThreadSessionStopCommand, +]); +export type HttpOrchestrationCommand = typeof HttpOrchestrationCommand.Type; + const ThreadSessionSetCommand = Schema.Struct({ type: Schema.Literal("thread.session.set"), commandId: CommandId,