From 970e0163d0d1e808a61bf463e8480e945a139e8e Mon Sep 17 00:00:00 2001 From: adityagarud Date: Sat, 29 Aug 2026 06:25:56 +0000 Subject: [PATCH 1/2] fix(server): reject HTTP turn bootstrap clearly --- apps/server/src/orchestration/http.ts | 3 ++ apps/server/src/server.test.ts | 52 +++++++++++++++++++++++ packages/contracts/src/environmentHttp.ts | 1 + 3 files changed, 56 insertions(+) diff --git a/apps/server/src/orchestration/http.ts b/apps/server/src/orchestration/http.ts index f7147106c7a9..c43500a949d6 100644 --- a/apps/server/src/orchestration/http.ts +++ b/apps/server/src/orchestration/http.ts @@ -93,6 +93,9 @@ export const orchestrationHttpApiLayer = HttpApiBuilder.group( Effect.fn("environment.orchestration.dispatch")(function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); yield* requireEnvironmentScope(AuthOrchestrationOperateScope); + if (args.payload.type === "thread.turn.start" && args.payload.bootstrap !== undefined) { + return yield* failEnvironmentInvalidRequest("http_bootstrap_not_supported"); + } const normalizedCommand = yield* normalizeDispatchCommand(args.payload).pipe( Effect.catch(() => failEnvironmentInvalidRequest("invalid_command")), ); diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index f609f7f1748c..6f12ee0caea9 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -1484,6 +1484,58 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); + it.effect("rejects WebSocket-only turn bootstrap over HTTP with a typed error", () => + 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, + }), + }); + const body = (yield* response.json) as { + readonly _tag: string; + readonly code: string; + readonly reason: string; + readonly traceId: string; + }; + + assert.equal(response.status, 400); + assert.equal(body._tag, "EnvironmentRequestInvalidError"); + assert.equal(body.code, "invalid_request"); + assert.equal(body.reason, "http_bootstrap_not_supported"); + assert.equal(typeof body.traceId, "string"); + }).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..60c4bb769e85 100644 --- a/packages/contracts/src/environmentHttp.ts +++ b/packages/contracts/src/environmentHttp.ts @@ -67,6 +67,7 @@ export const EnvironmentRequestInvalidReason = Schema.Literals([ "invalid_scope", "scope_not_granted", "invalid_command", + "http_bootstrap_not_supported", ]); export type EnvironmentRequestInvalidReason = typeof EnvironmentRequestInvalidReason.Type; From 3237dfa4ef9613625ca8bc9d6c38427e6d1b1e11 Mon Sep 17 00:00:00 2001 From: adityagarud Date: Sat, 29 Aug 2026 06:49:10 +0000 Subject: [PATCH 2/2] fix(server): model HTTP dispatch support in contracts --- apps/server/src/orchestration/http.ts | 3 -- apps/server/src/server.test.ts | 13 +------- packages/contracts/src/environmentHttp.ts | 5 ++- packages/contracts/src/orchestration.ts | 39 +++++++++++++++++++++-- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/apps/server/src/orchestration/http.ts b/apps/server/src/orchestration/http.ts index c43500a949d6..f7147106c7a9 100644 --- a/apps/server/src/orchestration/http.ts +++ b/apps/server/src/orchestration/http.ts @@ -93,9 +93,6 @@ export const orchestrationHttpApiLayer = HttpApiBuilder.group( Effect.fn("environment.orchestration.dispatch")(function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); yield* requireEnvironmentScope(AuthOrchestrationOperateScope); - if (args.payload.type === "thread.turn.start" && args.payload.bootstrap !== undefined) { - return yield* failEnvironmentInvalidRequest("http_bootstrap_not_supported"); - } const normalizedCommand = yield* normalizeDispatchCommand(args.payload).pipe( Effect.catch(() => failEnvironmentInvalidRequest("invalid_command")), ); diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 6f12ee0caea9..9873b1770f4e 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -1484,7 +1484,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); - it.effect("rejects WebSocket-only turn bootstrap over HTTP with a typed error", () => + it.effect("rejects WebSocket-only turn bootstrap at the HTTP schema", () => Effect.gen(function* () { yield* buildAppUnderTest(); @@ -1521,18 +1521,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => { createdAt, }), }); - const body = (yield* response.json) as { - readonly _tag: string; - readonly code: string; - readonly reason: string; - readonly traceId: string; - }; - assert.equal(response.status, 400); - assert.equal(body._tag, "EnvironmentRequestInvalidError"); - assert.equal(body.code, "invalid_request"); - assert.equal(body.reason, "http_bootstrap_not_supported"); - assert.equal(typeof body.traceId, "string"); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); diff --git a/packages/contracts/src/environmentHttp.ts b/packages/contracts/src/environmentHttp.ts index 60c4bb769e85..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, @@ -67,7 +67,6 @@ export const EnvironmentRequestInvalidReason = Schema.Literals([ "invalid_scope", "scope_not_granted", "invalid_command", - "http_bootstrap_not_supported", ]); export type EnvironmentRequestInvalidReason = typeof EnvironmentRequestInvalidReason.Type; @@ -532,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,