Skip to content

Commit dc978cb

Browse files
authored
fix(server): validate permission and question ids (anomalyco#26456)
1 parent 8cbc43f commit dc978cb

3 files changed

Lines changed: 32 additions & 3 deletions

File tree

packages/opencode/src/permission/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Newtype } from "@/util/schema"
66

77
export class PermissionID extends Newtype<PermissionID>()(
88
"PermissionID",
9-
Schema.String.annotate({ [ZodOverride]: Identifier.schema("permission") }),
9+
Schema.String.check(Schema.isStartsWith("per")).annotate({ [ZodOverride]: Identifier.schema("permission") }),
1010
) {
1111
static ascending(id?: string): PermissionID {
1212
return this.make(Identifier.ascending("permission", id))

packages/opencode/src/question/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Newtype } from "@/util/schema"
66

77
export class QuestionID extends Newtype<QuestionID>()(
88
"QuestionID",
9-
Schema.String.annotate({ [ZodOverride]: Identifier.schema("question") }),
9+
Schema.String.check(Schema.isStartsWith("que")).annotate({ [ZodOverride]: Identifier.schema("question") }),
1010
) {
1111
static ascending(id?: string): QuestionID {
1212
return this.make(Identifier.ascending("question", id))

packages/opencode/test/server/httpapi-instance.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
22
import { Flag } from "@opencode-ai/core/flag/flag"
33
import { describe, expect } from "bun:test"
4-
import { Config, Effect, FileSystem, Layer, Path } from "effect"
4+
import { Config, Context, Effect, FileSystem, Layer, Path } from "effect"
55
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
66
import * as Socket from "effect/unstable/socket/Socket"
77
import { WorkspaceID } from "../../src/control-plane/schema"
@@ -53,6 +53,7 @@ const httpApiServerLayer = servedRoutes.pipe(
5353
)
5454

5555
const it = testEffect(Layer.mergeAll(testStateLayer, httpApiServerLayer))
56+
const handlerContext = Context.empty() as Context.Context<unknown>
5657

5758
const directoryHeader = (dir: string) => HttpClientRequest.setHeader("x-opencode-directory", dir)
5859

@@ -121,6 +122,34 @@ describe("instance HttpApi", () => {
121122
}),
122123
)
123124

125+
it.live("rejects malformed permission and question request ids", () =>
126+
Effect.gen(function* () {
127+
const dir = yield* tmpdirScoped({ git: true })
128+
const request = (path: string, init?: RequestInit) =>
129+
Effect.promise(() =>
130+
ExperimentalHttpApiServer.webHandler().handler(
131+
new Request(`http://localhost${path}`, {
132+
...init,
133+
headers: { "x-opencode-directory": dir, "content-type": "application/json", ...init?.headers },
134+
}),
135+
handlerContext,
136+
),
137+
)
138+
const [permission, questionReply, questionReject] = yield* Effect.all(
139+
[
140+
request("/permission/invalid-permission-id/reply", { method: "POST", body: JSON.stringify({ reply: "once" }) }),
141+
request("/question/invalid-question-id/reply", { method: "POST", body: JSON.stringify({ answers: [["Yes"]] }) }),
142+
request("/question/invalid-question-id/reject", { method: "POST" }),
143+
],
144+
{ concurrency: "unbounded" },
145+
)
146+
147+
expect(permission.status).toBe(400)
148+
expect(questionReply.status).toBe(400)
149+
expect(questionReject.status).toBe(400)
150+
}),
151+
)
152+
124153
it.live("serves path and VCS read endpoints", () =>
125154
Effect.gen(function* () {
126155
const dir = yield* tmpdirScoped({ git: true })

0 commit comments

Comments
 (0)