diff --git a/packages/opencode/src/server/routes/instance/httpapi/api.ts b/packages/opencode/src/server/routes/instance/httpapi/api.ts index bdd917e4e8..4c6e46a455 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/api.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/api.ts @@ -21,6 +21,7 @@ import { TuiApi } from "./groups/tui" import { WorkspaceApi } from "./groups/workspace" import { V2Api } from "./groups/v2" import { Authorization } from "./middleware/authorization" +import { SchemaErrorMiddleware } from "./middleware/schema-error" // SSE event schemas built from the BusEvent/SyncEvent registries. const EventSchema = Schema.Union(BusEvent.effectPayloads()).annotate({ identifier: "Event" }) @@ -29,6 +30,7 @@ const SyncEventSchemas = SyncEvent.effectPayloads() export const RootHttpApi = HttpApi.make("opencode-root") .addHttpApi(ControlApi) .addHttpApi(GlobalApi) + .middleware(SchemaErrorMiddleware) .middleware(Authorization) export const InstanceHttpApi = HttpApi.make("opencode-instance") @@ -47,6 +49,7 @@ export const InstanceHttpApi = HttpApi.make("opencode-instance") .addHttpApi(V2Api) .addHttpApi(TuiApi) .addHttpApi(WorkspaceApi) + .middleware(SchemaErrorMiddleware) export const OpenCodeHttpApi = HttpApi.make("opencode") .addHttpApi(RootHttpApi) diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/schema-error.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/schema-error.ts new file mode 100644 index 0000000000..2a8b54f322 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/schema-error.ts @@ -0,0 +1,36 @@ +import { Effect } from "effect" +import { HttpServerResponse } from "effect/unstable/http" +import { HttpApiMiddleware } from "effect/unstable/httpapi" +import * as Log from "@opencode-ai/core/util/log" + +const log = Log.create({ service: "server" }) + +// Effect's Issue formatter recursively dumps the rejected `actual` value with +// no truncation, so a 5KB invalid array produces a ~360KB string. Cap to keep +// 4xx responses small and avoid mirroring entire request payloads (which may +// contain secrets) into the response body and log file. +const REASON_LIMIT = 1024 +function truncateReason(reason: string) { + if (reason.length <= REASON_LIMIT) return reason + return reason.slice(0, REASON_LIMIT) + `… (${reason.length - REASON_LIMIT} more chars)` +} + +// Default Respondable returns an empty 400 body. Match the NamedError shape +// used by other 4xx/5xx so the SDK's `wrapClientError` extracts `.data.message`. +export class SchemaErrorMiddleware extends HttpApiMiddleware.Service()( + "@opencode/HttpApiSchemaError", +) {} + +export const schemaErrorLayer = HttpApiMiddleware.layerSchemaErrorTransform( + SchemaErrorMiddleware, + (error) => { + const reason = truncateReason(error.cause.message) + log.warn("schema rejection", { kind: error.kind, reason }) + return Effect.succeed( + HttpServerResponse.jsonUnsafe( + { name: "BadRequest", data: { message: reason, kind: error.kind } }, + { status: 400 }, + ), + ) + }, +) diff --git a/packages/opencode/src/server/routes/instance/httpapi/public.ts b/packages/opencode/src/server/routes/instance/httpapi/public.ts index 4286e6f6cc..460a2be7a5 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/public.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/public.ts @@ -178,17 +178,20 @@ function addLegacyErrorSchemas(spec: OpenApiSpec) { if (!spec.components?.schemas) return spec.components.schemas.BadRequestError = { type: "object", - required: ["data", "errors", "success"], + required: ["name", "data"], properties: { - data: {}, - errors: { - type: "array", - items: { - type: "object", - additionalProperties: {}, + name: { type: "string", enum: ["BadRequest"] }, + data: { + type: "object", + required: ["message"], + properties: { + message: { type: "string" }, + kind: { + type: "string", + enum: ["Params", "Headers", "Query", "Body", "Payload"], + }, }, }, - success: { type: "boolean", enum: [false] }, }, } spec.components.schemas.NotFoundError = { diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 495497ecb4..7ce21dfadb 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -84,6 +84,7 @@ import { compressionLayer } from "./middleware/compression" import { corsVaryFix } from "./middleware/cors-vary" import { errorLayer } from "./middleware/error" import { fenceLayer } from "./middleware/fence" +import { schemaErrorLayer } from "./middleware/schema-error" export const context = Context.makeUnsafe(new Map()) @@ -114,6 +115,7 @@ const authOnlyRouterLayer = authorizationRouterMiddleware.layer.pipe(Layer.provi const httpApiAuthLayer = authorizationLayer.pipe(Layer.provide(ServerAuth.Config.defaultLayer)) const rootApiRoutes = HttpApiBuilder.layer(RootHttpApi).pipe( Layer.provide([controlHandlers, globalHandlers]), + Layer.provide(schemaErrorLayer), Layer.provide(httpApiAuthLayer), ) const instanceRouterLayer = authorizationRouterMiddleware @@ -150,6 +152,7 @@ const instanceRoutes = Layer.mergeAll(rawInstanceRoutes, instanceApiRoutes).pipe httpApiAuthLayer, workspaceRoutingLayer.pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal)), instanceContextLayer, + schemaErrorLayer, ]), ) diff --git a/packages/opencode/test/server/httpapi-schema-error-body.test.ts b/packages/opencode/test/server/httpapi-schema-error-body.test.ts new file mode 100644 index 0000000000..fe6a1caad0 --- /dev/null +++ b/packages/opencode/test/server/httpapi-schema-error-body.test.ts @@ -0,0 +1,162 @@ +import { afterEach, describe, expect } from "bun:test" +import { Effect } from "effect" +import { eq } from "drizzle-orm" +import * as Database from "@/storage/db" +import { ModelID, ProviderID } from "../../src/provider/schema" +import { WithInstance } from "../../src/project/with-instance" +import { Server } from "../../src/server/server" +import { Session } from "@/session/session" +import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session" +import { SyncPaths } from "../../src/server/routes/instance/httpapi/groups/sync" +import { MessageID, PartID } from "../../src/session/schema" +import { PartTable } from "@/session/session.sql" +import { resetDatabase } from "../fixture/db" +import { disposeAllInstances, tmpdir } from "../fixture/fixture" +import { it } from "../lib/effect" + +afterEach(async () => { + await disposeAllInstances() + await resetDatabase() +}) + +const withTmp = ( + options: Parameters[0], + fn: (tmp: Awaited>) => Effect.Effect, +) => + Effect.acquireRelease( + Effect.promise(() => tmpdir(options)), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe(Effect.flatMap(fn)) + +async function seedCorruptStepFinishPart(directory: string) { + return WithInstance.provide({ + directory, + fn: () => + Effect.runPromise( + Effect.gen(function* () { + const session = yield* Session.Service + const info = yield* session.create({}) + const message = yield* session.updateMessage({ + id: MessageID.ascending(), + role: "user", + sessionID: info.id, + agent: "build", + model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") }, + time: { created: Date.now() }, + }) + const partID = PartID.ascending() + yield* session.updatePart({ + id: partID, + sessionID: info.id, + messageID: message.id, + type: "step-finish", + reason: "stop", + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + }) + // Schema.Finite still rejects NaN at encode — exact mirror of the + // corrupt row that broke the user's session in the OMO/Windows bug. + Database.use((db) => + db + .update(PartTable) + .set({ + data: { + type: "step-finish", + reason: "stop", + cost: 0, + tokens: { input: 0, output: NaN, reasoning: 0, cache: { read: 0, write: 0 } }, + } as never, // drizzle's .set() can't narrow the discriminated union + }) + .where(eq(PartTable.id, partID)) + .run(), + ) + return info.id + }).pipe(Effect.provide(Session.defaultLayer)), + ), + }) +} + +describe("schema-rejection wire shape", () => { + it.live( + "Payload schema rejection returns NamedError-shaped JSON, not empty", + withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) => + Effect.gen(function* () { + const res = yield* Effect.promise(async () => + Server.Default().app.request(SyncPaths.history, { + method: "POST", + headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" }, + body: JSON.stringify({ aggregate: -1 }), + }), + ) + const body = yield* Effect.promise(async () => res.text()) + expect(res.status).toBe(400) + expect(res.headers.get("content-type") ?? "").toContain("application/json") + const parsed = JSON.parse(body) + expect(parsed).toMatchObject({ + name: "BadRequest", + data: { kind: expect.stringMatching(/^(Body|Payload)$/) }, + }) + expect(parsed.data.message).toEqual(expect.any(String)) + expect(parsed.data.message.length).toBeGreaterThan(0) + }), + ), + ) + + it.live( + "Query schema rejection returns NamedError-shaped JSON", + withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) => + Effect.gen(function* () { + // /find/file?limit=999999 violates the limit constraint check. + const url = `/find/file?query=foo&limit=999999&directory=${encodeURIComponent(tmp.path)}` + const res = yield* Effect.promise(async () => Server.Default().app.request(url)) + const body = yield* Effect.promise(async () => res.text()) + expect(res.status).toBe(400) + const parsed = JSON.parse(body) + expect(parsed).toMatchObject({ name: "BadRequest", data: { kind: "Query" } }) + }), + ), + ) + + it.live( + "rejected request body never echoes back unbounded — message is capped", + // Defense against DoS-amplification + secret-echo: Effect's Issue formatter + // dumps the rejected `actual` verbatim. A multi-MB invalid array would + // become a multi-MB 400 response and log line. Cap kicks in around 1KB. + withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) => + Effect.gen(function* () { + const huge = "X".repeat(50_000) + const res = yield* Effect.promise(async () => + Server.Default().app.request(SyncPaths.history, { + method: "POST", + headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" }, + body: JSON.stringify({ aggregate: huge }), + }), + ) + const body = yield* Effect.promise(async () => res.text()) + expect(res.status).toBe(400) + // 1 KB cap + small JSON envelope ≈ <2 KB — never tens of KB. + expect(body.length).toBeLessThan(2 * 1024) + const parsed = JSON.parse(body) + expect(parsed.data.message).not.toContain(huge) + }), + ), + ) + + it.live( + "response-encode failure: corrupted stored row returns NamedError-shaped JSON with field path", + withTmp({ config: { formatter: false, lsp: false } }, (tmp) => + Effect.gen(function* () { + const sessionID = yield* Effect.promise(() => seedCorruptStepFinishPart(tmp.path)) + const url = `${SessionPaths.messages.replace(":sessionID", sessionID)}?limit=80&directory=${encodeURIComponent(tmp.path)}` + const res = yield* Effect.promise(async () => Server.Default().app.request(url)) + const body = yield* Effect.promise(async () => res.text()) + expect(res.status).toBe(400) + expect(res.headers.get("content-type") ?? "").toContain("application/json") + const parsed = JSON.parse(body) + expect(parsed).toMatchObject({ name: "BadRequest", data: { kind: "Body" } }) + // Field path in data.message — what made this PR worth shipping. + expect(parsed.data.message).toMatch(/output/) + }), + ), + ) +}) diff --git a/packages/opencode/test/server/sdk-error-shape.test.ts b/packages/opencode/test/server/sdk-error-shape.test.ts index 30eedc9adb..f41fe2cb30 100644 --- a/packages/opencode/test/server/sdk-error-shape.test.ts +++ b/packages/opencode/test/server/sdk-error-shape.test.ts @@ -52,23 +52,33 @@ describe("v2 SDK error shape", () => { }) }) - test("400 with empty body throws a real Error naming the status", async () => { + test("400 schema rejection: SDK extracts the field-level reason from the NamedError body", async () => { + // Canary for the #26631 wire shape. Asserts the contract end-to-end: + // server emits {name:"BadRequest", data:{message, kind}}, SDK's + // wrapClientError extracts .data.message into Error.message. If either + // side regresses (#26457 reverted because both layers were missing), + // this test fails before users see (empty response body). await using tmp = await tmpdir({ config: { formatter: false, lsp: false } }) const sdk = client(tmp.path) let caught: unknown try { - // POST /sync/history with `aggregate: -1` triggers schema validation - // that returns an empty 400 body (verified via plan-mode probe). - await sdk.sync.history.list({ aggregate: -1 } as any, { throwOnError: true }) + await sdk.sync.history.list({ body: { aggregate: -1 } as any }, { throwOnError: true }) } catch (e) { caught = e } expect(caught).toBeInstanceOf(Error) const err = caught as Error - const cause = err.cause as { status?: number } - expect(err.message.length).toBeGreaterThan(0) + const cause = err.cause as { body?: any; status?: number } expect(cause.status).toBe(400) + expect(cause.body).toMatchObject({ + name: "BadRequest", + data: { kind: expect.stringMatching(/^(Body|Payload)$/) }, + }) + expect(typeof cause.body.data.message).toBe("string") + expect(cause.body.data.message.length).toBeGreaterThan(0) + // Whatever the server put in data.message must be what the user sees. + expect(err.message).toBe(cause.body.data.message) }) }) diff --git a/packages/opencode/test/server/sdk-v1-smoke.test.ts b/packages/opencode/test/server/sdk-v1-smoke.test.ts new file mode 100644 index 0000000000..2b09e0c872 --- /dev/null +++ b/packages/opencode/test/server/sdk-v1-smoke.test.ts @@ -0,0 +1,60 @@ +// Smoke test: v1 SDK (the plugin contract) can actually reach core endpoints +// against the current server. v1 generation has been frozen since #5216 +// (2025-12-07) so types may be stale, but runtime calls should still work +// for endpoints the v1 SDK was generated against. +import { afterEach, describe, expect, test } from "bun:test" +import { createOpencodeClient } from "@opencode-ai/sdk" +import { Server } from "../../src/server/server" +import { tmpdir, disposeAllInstances } from "../fixture/fixture" +import { resetDatabase } from "../fixture/db" +import * as Log from "@opencode-ai/core/util/log" + +void Log.init({ print: false }) + +afterEach(async () => { + await disposeAllInstances() + await resetDatabase() +}) + +function client(directory: string) { + return createOpencodeClient({ + baseUrl: "http://test", + directory, + fetch: ((req: Request) => Server.Default().app.fetch(req)) as unknown as typeof fetch, + }) +} + +describe("v1 SDK runtime smoke", () => { + test("session.list reaches the server and returns 200", async () => { + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + const sdk = client(tmp.path) + const result = await sdk.session.list() + expect(result.error).toBeUndefined() + expect(Array.isArray(result.data)).toBe(true) + }) + + test("path.get reaches the server and returns 200", async () => { + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + const sdk = client(tmp.path) + const result = await sdk.path.get() + expect(result.error).toBeUndefined() + expect(result.data).toBeDefined() + }) + + test("config.get reaches the server and returns 200", async () => { + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + const sdk = client(tmp.path) + const result = await sdk.config.get() + expect(result.error).toBeUndefined() + expect(result.data).toBeDefined() + }) + + test("session 404: result-tuple path returns the error body", async () => { + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + const sdk = client(tmp.path) + const result = await sdk.session.get({ path: { id: "ses_no_such" } as never }) + expect(result.error).toBeDefined() + // wire body for 404 is NamedError-shaped + expect(result.error).toMatchObject({ name: "NotFoundError" }) + }) +}) diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 62e1b8fe8d..8fd2a02b92 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -752,11 +752,11 @@ export type Project = { } export type BadRequestError = { - data: unknown - errors: Array<{ - [key: string]: unknown - }> - success: false + name: "BadRequest" + data: { + message: string + kind?: "Params" | "Headers" | "Query" | "Body" | "Payload" + } } export type NotFoundError = { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index ae7e9767ce..1c8bb5ac7c 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -5,6 +5,12 @@ export type ClientOptions = { } export type Event = + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow1 + | EventTuiSessionSelect + | EventServerConnected + | EventGlobalDisposed | EventServerInstanceDisposed | EventFileEdited | EventFileWatcherUpdated @@ -24,10 +30,6 @@ export type Event = | EventSessionStatus | EventSessionIdle | EventSessionCompacted - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow1 - | EventTuiSessionSelect | EventMcpToolsChanged | EventMcpBrowserOpenFailed | EventCommandExecuted @@ -75,8 +77,6 @@ export type Event = | EventSessionNextCompactionStarted | EventSessionNextCompactionDelta | EventSessionNextCompactionEnded - | EventServerConnected - | EventGlobalDisposed export type OAuth = { type: "oauth" @@ -103,6 +103,61 @@ export type WellKnownAuth = { export type Auth = OAuth | ApiAuth | WellKnownAuth +export type EventTuiPromptAppend = { + id: string + type: "tui.prompt.append" + properties: { + text: string + } +} + +export type EventTuiCommandExecute = { + id: string + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.line.up" + | "session.line.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } +} + +export type EventTuiToastShow = { + id: string + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + duration?: number + } +} + +export type EventTuiSessionSelect = { + id: string + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + export type PermissionRequest = { id: string sessionID: string @@ -280,61 +335,6 @@ export type SessionStatus = type: "busy" } -export type EventTuiPromptAppend = { - id: string - type: "tui.prompt.append" - properties: { - text: string - } -} - -export type EventTuiCommandExecute = { - id: string - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.line.up" - | "session.line.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type EventTuiToastShow = { - id: string - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - duration?: number - } -} - -export type EventTuiSessionSelect = { - id: string - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - export type Project = { id: string worktree: string @@ -778,6 +778,12 @@ export type GlobalEvent = { project?: string workspace?: string payload: + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventServerConnected + | EventGlobalDisposed | EventServerInstanceDisposed | EventFileEdited | EventFileWatcherUpdated @@ -797,10 +803,6 @@ export type GlobalEvent = { | EventSessionStatus | EventSessionIdle | EventSessionCompacted - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect | EventMcpToolsChanged | EventMcpBrowserOpenFailed | EventCommandExecuted @@ -848,8 +850,6 @@ export type GlobalEvent = { | EventSessionNextCompactionStarted | EventSessionNextCompactionDelta | EventSessionNextCompactionEnded - | EventServerConnected - | EventGlobalDisposed | SyncEventMessageUpdated | SyncEventMessageRemoved | SyncEventMessagePartUpdated @@ -2330,6 +2330,22 @@ export type SyncEventSessionNextCompactionEnded = { } } +export type EventServerConnected = { + id: string + type: "server.connected" + properties: { + [key: string]: unknown + } +} + +export type EventGlobalDisposed = { + id: string + type: "global.disposed" + properties: { + [key: string]: unknown + } +} + export type EventServerInstanceDisposed = { id: string type: "server.instance.disposed" @@ -3044,22 +3060,6 @@ export type EventSessionNextCompactionEnded = { } } -export type EventServerConnected = { - id: string - type: "server.connected" - properties: { - [key: string]: unknown - } -} - -export type EventGlobalDisposed = { - id: string - type: "global.disposed" - properties: { - [key: string]: unknown - } -} - export type SessionInfo = { id: string parentID?: string @@ -3296,11 +3296,11 @@ export type EventTuiToastShow1 = { } export type BadRequestError = { - data: unknown - errors: Array<{ - [key: string]: unknown - }> - success: false + name: "BadRequest" + data: { + message: string + kind?: "Params" | "Headers" | "Query" | "Body" | "Payload" + } } export type AuthRemoveData = { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 8ed22e3101..73cb5c3318 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8868,6 +8868,24 @@ "schemas": { "Event": { "anyOf": [ + { + "$ref": "#/components/schemas/Event.tui.prompt.append" + }, + { + "$ref": "#/components/schemas/Event.tui.command.execute" + }, + { + "$ref": "#/components/schemas/EventTuiToastShow1" + }, + { + "$ref": "#/components/schemas/Event.tui.session.select" + }, + { + "$ref": "#/components/schemas/EventServerConnected" + }, + { + "$ref": "#/components/schemas/EventGlobalDisposed" + }, { "$ref": "#/components/schemas/EventServerInstanceDisposed" }, @@ -8925,18 +8943,6 @@ { "$ref": "#/components/schemas/EventSessionCompacted" }, - { - "$ref": "#/components/schemas/Event.tui.prompt.append" - }, - { - "$ref": "#/components/schemas/Event.tui.command.execute" - }, - { - "$ref": "#/components/schemas/EventTuiToastShow1" - }, - { - "$ref": "#/components/schemas/Event.tui.session.select" - }, { "$ref": "#/components/schemas/EventMcpToolsChanged" }, @@ -9077,12 +9083,6 @@ }, { "$ref": "#/components/schemas/EventSessionNextCompactionEnded" - }, - { - "$ref": "#/components/schemas/EventServerConnected" - }, - { - "$ref": "#/components/schemas/EventGlobalDisposed" } ] }, @@ -9163,6 +9163,140 @@ } ] }, + "Event.tui.prompt.append": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["tui.prompt.append"] + }, + "properties": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "Event.tui.command.execute": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["tui.command.execute"] + }, + "properties": { + "type": "object", + "properties": { + "command": { + "anyOf": [ + { + "type": "string", + "enum": [ + "session.list", + "session.new", + "session.share", + "session.interrupt", + "session.compact", + "session.page.up", + "session.page.down", + "session.line.up", + "session.line.down", + "session.half.page.up", + "session.half.page.down", + "session.first", + "session.last", + "prompt.clear", + "prompt.submit", + "agent.cycle" + ] + }, + { + "type": "string" + } + ] + } + }, + "required": ["command"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "Event.tui.toast.show": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["tui.toast.show"] + }, + "properties": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "variant": { + "type": "string", + "enum": ["info", "success", "warning", "error"] + }, + "duration": { + "type": "integer", + "exclusiveMinimum": 0 + } + }, + "required": ["message", "variant"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "Event.tui.session.select": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["tui.session.select"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses", + "description": "Session ID to navigate to" + } + }, + "required": ["sessionID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "PermissionRequest": { "type": "object", "properties": { @@ -9622,140 +9756,6 @@ } ] }, - "Event.tui.prompt.append": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["tui.prompt.append"] - }, - "properties": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.command.execute": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["tui.command.execute"] - }, - "properties": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] - } - }, - "required": ["command"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.toast.show": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["tui.toast.show"] - }, - "properties": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": ["info", "success", "warning", "error"] - }, - "duration": { - "type": "integer", - "exclusiveMinimum": 0 - } - }, - "required": ["message", "variant"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "Event.tui.session.select": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["tui.session.select"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses", - "description": "Session ID to navigate to" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "Project": { "type": "object", "properties": { @@ -11129,6 +11129,24 @@ }, "payload": { "anyOf": [ + { + "$ref": "#/components/schemas/Event.tui.prompt.append" + }, + { + "$ref": "#/components/schemas/Event.tui.command.execute" + }, + { + "$ref": "#/components/schemas/Event.tui.toast.show" + }, + { + "$ref": "#/components/schemas/Event.tui.session.select" + }, + { + "$ref": "#/components/schemas/EventServerConnected" + }, + { + "$ref": "#/components/schemas/EventGlobalDisposed" + }, { "$ref": "#/components/schemas/EventServerInstanceDisposed" }, @@ -11186,18 +11204,6 @@ { "$ref": "#/components/schemas/EventSessionCompacted" }, - { - "$ref": "#/components/schemas/Event.tui.prompt.append" - }, - { - "$ref": "#/components/schemas/Event.tui.command.execute" - }, - { - "$ref": "#/components/schemas/Event.tui.toast.show" - }, - { - "$ref": "#/components/schemas/Event.tui.session.select" - }, { "$ref": "#/components/schemas/EventMcpToolsChanged" }, @@ -11339,12 +11345,6 @@ { "$ref": "#/components/schemas/EventSessionNextCompactionEnded" }, - { - "$ref": "#/components/schemas/EventServerConnected" - }, - { - "$ref": "#/components/schemas/EventGlobalDisposed" - }, { "$ref": "#/components/schemas/SyncEventMessageUpdated" }, @@ -15843,6 +15843,42 @@ "required": ["type", "name", "id", "seq", "aggregateID", "data"], "additionalProperties": false }, + "EventServerConnected": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["server.connected"] + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventGlobalDisposed": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["global.disposed"] + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventServerInstanceDisposed": { "type": "object", "properties": { @@ -18011,42 +18047,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventServerConnected": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["server.connected"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventGlobalDisposed": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["global.disposed"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "SessionInfo": { "type": "object", "properties": { @@ -18725,19 +18725,24 @@ }, "BadRequestError": { "type": "object", - "required": ["data", "errors", "success"], + "required": ["name", "data"], "properties": { - "data": {}, - "errors": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": {} - } + "name": { + "type": "string", + "enum": ["BadRequest"] }, - "success": { - "type": "boolean", - "enum": [false] + "data": { + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": ["Params", "Headers", "Query", "Body", "Payload"] + } + } } } }