From dc978cb8892d076c40c263a7c1e28f80e297ffb1 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 9 May 2026 00:20:28 -0400 Subject: [PATCH] fix(server): validate permission and question ids (#26456) --- packages/opencode/src/permission/schema.ts | 2 +- packages/opencode/src/question/schema.ts | 2 +- .../test/server/httpapi-instance.test.ts | 31 ++++++++++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/permission/schema.ts b/packages/opencode/src/permission/schema.ts index 4eddc6a47a..e8bdb2ea20 100644 --- a/packages/opencode/src/permission/schema.ts +++ b/packages/opencode/src/permission/schema.ts @@ -6,7 +6,7 @@ import { Newtype } from "@/util/schema" export class PermissionID extends Newtype()( "PermissionID", - Schema.String.annotate({ [ZodOverride]: Identifier.schema("permission") }), + Schema.String.check(Schema.isStartsWith("per")).annotate({ [ZodOverride]: Identifier.schema("permission") }), ) { static ascending(id?: string): PermissionID { return this.make(Identifier.ascending("permission", id)) diff --git a/packages/opencode/src/question/schema.ts b/packages/opencode/src/question/schema.ts index f7a0e096a3..7dade9cdfa 100644 --- a/packages/opencode/src/question/schema.ts +++ b/packages/opencode/src/question/schema.ts @@ -6,7 +6,7 @@ import { Newtype } from "@/util/schema" export class QuestionID extends Newtype()( "QuestionID", - Schema.String.annotate({ [ZodOverride]: Identifier.schema("question") }), + Schema.String.check(Schema.isStartsWith("que")).annotate({ [ZodOverride]: Identifier.schema("question") }), ) { static ascending(id?: string): QuestionID { return this.make(Identifier.ascending("question", id)) diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts index da8f8fb56d..90eb6538c7 100644 --- a/packages/opencode/test/server/httpapi-instance.test.ts +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -1,7 +1,7 @@ import { NodeHttpServer, NodeServices } from "@effect/platform-node" import { Flag } from "@opencode-ai/core/flag/flag" import { describe, expect } from "bun:test" -import { Config, Effect, FileSystem, Layer, Path } from "effect" +import { Config, Context, Effect, FileSystem, Layer, Path } from "effect" import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http" import * as Socket from "effect/unstable/socket/Socket" import { WorkspaceID } from "../../src/control-plane/schema" @@ -53,6 +53,7 @@ const httpApiServerLayer = servedRoutes.pipe( ) const it = testEffect(Layer.mergeAll(testStateLayer, httpApiServerLayer)) +const handlerContext = Context.empty() as Context.Context const directoryHeader = (dir: string) => HttpClientRequest.setHeader("x-opencode-directory", dir) @@ -121,6 +122,34 @@ describe("instance HttpApi", () => { }), ) + it.live("rejects malformed permission and question request ids", () => + Effect.gen(function* () { + const dir = yield* tmpdirScoped({ git: true }) + const request = (path: string, init?: RequestInit) => + Effect.promise(() => + ExperimentalHttpApiServer.webHandler().handler( + new Request(`http://localhost${path}`, { + ...init, + headers: { "x-opencode-directory": dir, "content-type": "application/json", ...init?.headers }, + }), + handlerContext, + ), + ) + const [permission, questionReply, questionReject] = yield* Effect.all( + [ + request("/permission/invalid-permission-id/reply", { method: "POST", body: JSON.stringify({ reply: "once" }) }), + request("/question/invalid-question-id/reply", { method: "POST", body: JSON.stringify({ answers: [["Yes"]] }) }), + request("/question/invalid-question-id/reject", { method: "POST" }), + ], + { concurrency: "unbounded" }, + ) + + expect(permission.status).toBe(400) + expect(questionReply.status).toBe(400) + expect(questionReject.status).toBe(400) + }), + ) + it.live("serves path and VCS read endpoints", () => Effect.gen(function* () { const dir = yield* tmpdirScoped({ git: true })