Преглед изворни кода

feat(id): brand PermissionID, PtyID, QuestionID, and ToolID (#17042)

Kit Langton пре 6 месеци
родитељ
комит
2a4dedc210

+ 2 - 2
packages/opencode/src/account/schema.ts

@@ -3,13 +3,13 @@ import { Schema } from "effect"
 import { withStatics } from "@/util/schema"
 
 export const AccountID = Schema.String.pipe(
-  Schema.brand("AccountId"),
+  Schema.brand("AccountID"),
   withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
 )
 export type AccountID = Schema.Schema.Type<typeof AccountID>
 
 export const OrgID = Schema.String.pipe(
-  Schema.brand("OrgId"),
+  Schema.brand("OrgID"),
   withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
 )
 export type OrgID = Schema.Schema.Type<typeof OrgID>

+ 2 - 2
packages/opencode/src/control-plane/schema.ts

@@ -4,7 +4,7 @@ import z from "zod"
 import { withStatics } from "@/util/schema"
 import { Identifier } from "@/id/id"
 
-const workspaceIdSchema = Schema.String.pipe(Schema.brand("WorkspaceId"))
+const workspaceIdSchema = Schema.String.pipe(Schema.brand("WorkspaceID"))
 
 export type WorkspaceID = typeof workspaceIdSchema.Type
 
@@ -12,6 +12,6 @@ export const WorkspaceID = workspaceIdSchema.pipe(
   withStatics((schema: typeof workspaceIdSchema) => ({
     make: (id: string) => schema.makeUnsafe(id),
     ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("workspace", id)),
-    zod: z.string().startsWith("wrk").pipe(z.custom<WorkspaceID>()),
+    zod: Identifier.schema("workspace").pipe(z.custom<WorkspaceID>()),
   })),
 )

+ 4 - 4
packages/opencode/src/permission/index.ts

@@ -3,10 +3,10 @@ import { Bus } from "@/bus"
 import { SessionID, MessageID } from "@/session/schema"
 import z from "zod"
 import { Log } from "../util/log"
-import { Identifier } from "../id/id"
 import { Plugin } from "../plugin"
 import { Instance } from "../project/instance"
 import { Wildcard } from "../util/wildcard"
+import { PermissionID } from "./schema"
 
 export namespace Permission {
   const log = Log.create({ service: "permission" })
@@ -22,7 +22,7 @@ export namespace Permission {
 
   export const Info = z
     .object({
-      id: z.string(),
+      id: PermissionID.zod,
       type: z.string(),
       pattern: z.union([z.string(), z.array(z.string())]).optional(),
       sessionID: SessionID.zod,
@@ -45,7 +45,7 @@ export namespace Permission {
       "permission.replied",
       z.object({
         sessionID: SessionID.zod,
-        permissionID: z.string(),
+        permissionID: PermissionID.zod,
         response: z.string(),
       }),
     ),
@@ -118,7 +118,7 @@ export namespace Permission {
     const keys = toKeys(input.pattern, input.type)
     if (covered(keys, approvedForSession)) return
     const info: Info = {
-      id: Identifier.ascending("permission"),
+      id: PermissionID.ascending(),
       type: input.type,
       pattern: input.pattern,
       sessionID: input.sessionID,

+ 5 - 5
packages/opencode/src/permission/next.ts

@@ -1,8 +1,8 @@
 import { Bus } from "@/bus"
 import { BusEvent } from "@/bus/bus-event"
 import { Config } from "@/config/config"
-import { Identifier } from "@/id/id"
 import { SessionID, MessageID } from "@/session/schema"
+import { PermissionID } from "./schema"
 import { Instance } from "@/project/instance"
 import { Database, eq } from "@/storage/db"
 import { PermissionTable } from "@/session/session.sql"
@@ -69,7 +69,7 @@ export namespace PermissionNext {
 
   export const Request = z
     .object({
-      id: Identifier.schema("permission"),
+      id: PermissionID.zod,
       sessionID: SessionID.zod,
       permission: z.string(),
       patterns: z.string().array(),
@@ -102,7 +102,7 @@ export namespace PermissionNext {
       "permission.replied",
       z.object({
         sessionID: SessionID.zod,
-        requestID: z.string(),
+        requestID: PermissionID.zod,
         reply: Reply,
       }),
     ),
@@ -143,7 +143,7 @@ export namespace PermissionNext {
         if (rule.action === "deny")
           throw new DeniedError(ruleset.filter((r) => Wildcard.match(request.permission, r.permission)))
         if (rule.action === "ask") {
-          const id = input.id ?? Identifier.ascending("permission")
+          const id = input.id ?? PermissionID.ascending()
           return new Promise<void>((resolve, reject) => {
             const info: Request = {
               id,
@@ -164,7 +164,7 @@ export namespace PermissionNext {
 
   export const reply = fn(
     z.object({
-      requestID: Identifier.schema("permission"),
+      requestID: PermissionID.zod,
       reply: Reply,
       message: z.string().optional(),
     }),

+ 17 - 0
packages/opencode/src/permission/schema.ts

@@ -0,0 +1,17 @@
+import { Schema } from "effect"
+import z from "zod"
+
+import { Identifier } from "@/id/id"
+import { withStatics } from "@/util/schema"
+
+const permissionIdSchema = Schema.String.pipe(Schema.brand("PermissionID"))
+
+export type PermissionID = typeof permissionIdSchema.Type
+
+export const PermissionID = permissionIdSchema.pipe(
+  withStatics((schema: typeof permissionIdSchema) => ({
+    make: (id: string) => schema.makeUnsafe(id),
+    ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("permission", id)),
+    zod: Identifier.schema("permission").pipe(z.custom<PermissionID>()),
+  })),
+)

+ 1 - 1
packages/opencode/src/project/schema.ts

@@ -3,7 +3,7 @@ import z from "zod"
 
 import { withStatics } from "@/util/schema"
 
-const projectIdSchema = Schema.String.pipe(Schema.brand("ProjectId"))
+const projectIdSchema = Schema.String.pipe(Schema.brand("ProjectID"))
 
 export type ProjectID = typeof projectIdSchema.Type
 

+ 6 - 6
packages/opencode/src/pty/index.ts

@@ -2,12 +2,12 @@ import { BusEvent } from "@/bus/bus-event"
 import { Bus } from "@/bus"
 import { type IPty } from "bun-pty"
 import z from "zod"
-import { Identifier } from "../id/id"
 import { Log } from "../util/log"
 import { Instance } from "../project/instance"
 import { lazy } from "@opencode-ai/util/lazy"
 import { Shell } from "@/shell/shell"
 import { Plugin } from "@/plugin"
+import { PtyID } from "./schema"
 
 export namespace Pty {
   const log = Log.create({ service: "pty" })
@@ -40,7 +40,7 @@ export namespace Pty {
 
   export const Info = z
     .object({
-      id: Identifier.schema("pty"),
+      id: PtyID.zod,
       title: z.string(),
       command: z.string(),
       args: z.array(z.string()),
@@ -77,8 +77,8 @@ export namespace Pty {
   export const Event = {
     Created: BusEvent.define("pty.created", z.object({ info: Info })),
     Updated: BusEvent.define("pty.updated", z.object({ info: Info })),
-    Exited: BusEvent.define("pty.exited", z.object({ id: Identifier.schema("pty"), exitCode: z.number() })),
-    Deleted: BusEvent.define("pty.deleted", z.object({ id: Identifier.schema("pty") })),
+    Exited: BusEvent.define("pty.exited", z.object({ id: PtyID.zod, exitCode: z.number() })),
+    Deleted: BusEvent.define("pty.deleted", z.object({ id: PtyID.zod })),
   }
 
   interface ActiveSession {
@@ -118,7 +118,7 @@ export namespace Pty {
   }
 
   export async function create(input: CreateInput) {
-    const id = Identifier.create("pty", false)
+    const id = PtyID.ascending()
     const command = input.command || Shell.preferred()
     const args = input.args || []
     if (command.endsWith("sh")) {
@@ -234,7 +234,7 @@ export namespace Pty {
       }
     }
     session.subscribers.clear()
-    Bus.publish(Event.Deleted, { id })
+    Bus.publish(Event.Deleted, { id: session.info.id })
   }
 
   export function resize(id: string, cols: number, rows: number) {

+ 17 - 0
packages/opencode/src/pty/schema.ts

@@ -0,0 +1,17 @@
+import { Schema } from "effect"
+import z from "zod"
+
+import { Identifier } from "@/id/id"
+import { withStatics } from "@/util/schema"
+
+const ptyIdSchema = Schema.String.pipe(Schema.brand("PtyID"))
+
+export type PtyID = typeof ptyIdSchema.Type
+
+export const PtyID = ptyIdSchema.pipe(
+  withStatics((schema: typeof ptyIdSchema) => ({
+    make: (id: string) => schema.makeUnsafe(id),
+    ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("pty", id)),
+    zod: Identifier.schema("pty").pipe(z.custom<PtyID>()),
+  })),
+)

+ 5 - 5
packages/opencode/src/question/index.ts

@@ -1,10 +1,10 @@
 import { Bus } from "@/bus"
 import { BusEvent } from "@/bus/bus-event"
-import { Identifier } from "@/id/id"
 import { SessionID, MessageID } from "@/session/schema"
 import { Instance } from "@/project/instance"
 import { Log } from "@/util/log"
 import z from "zod"
+import { QuestionID } from "./schema"
 
 export namespace Question {
   const log = Log.create({ service: "question" })
@@ -34,7 +34,7 @@ export namespace Question {
 
   export const Request = z
     .object({
-      id: Identifier.schema("question"),
+      id: QuestionID.zod,
       sessionID: SessionID.zod,
       questions: z.array(Info).describe("Questions to ask"),
       tool: z
@@ -67,7 +67,7 @@ export namespace Question {
       "question.replied",
       z.object({
         sessionID: SessionID.zod,
-        requestID: z.string(),
+        requestID: QuestionID.zod,
         answers: z.array(Answer),
       }),
     ),
@@ -75,7 +75,7 @@ export namespace Question {
       "question.rejected",
       z.object({
         sessionID: SessionID.zod,
-        requestID: z.string(),
+        requestID: QuestionID.zod,
       }),
     ),
   }
@@ -101,7 +101,7 @@ export namespace Question {
     tool?: { messageID: MessageID; callID: string }
   }): Promise<Answer[]> {
     const s = await state()
-    const id = Identifier.ascending("question")
+    const id = QuestionID.ascending()
 
     log.info("asking", { id, questions: input.questions.length })
 

+ 17 - 0
packages/opencode/src/question/schema.ts

@@ -0,0 +1,17 @@
+import { Schema } from "effect"
+import z from "zod"
+
+import { Identifier } from "@/id/id"
+import { withStatics } from "@/util/schema"
+
+const questionIdSchema = Schema.String.pipe(Schema.brand("QuestionID"))
+
+export type QuestionID = typeof questionIdSchema.Type
+
+export const QuestionID = questionIdSchema.pipe(
+  withStatics((schema: typeof questionIdSchema) => ({
+    make: (id: string) => schema.makeUnsafe(id),
+    ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("question", id)),
+    zod: Identifier.schema("question").pipe(z.custom<QuestionID>()),
+  })),
+)

+ 2 - 1
packages/opencode/src/server/routes/permission.ts

@@ -2,6 +2,7 @@ import { Hono } from "hono"
 import { describeRoute, validator, resolver } from "hono-openapi"
 import z from "zod"
 import { PermissionNext } from "@/permission/next"
+import { PermissionID } from "@/permission/schema"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"
 
@@ -28,7 +29,7 @@ export const PermissionRoutes = lazy(() =>
       validator(
         "param",
         z.object({
-          requestID: z.string(),
+          requestID: PermissionID.zod,
         }),
       ),
       validator("json", z.object({ reply: PermissionNext.Reply, message: z.string().optional() })),

+ 6 - 5
packages/opencode/src/server/routes/pty.ts

@@ -3,6 +3,7 @@ import { describeRoute, validator, resolver } from "hono-openapi"
 import { upgradeWebSocket } from "hono/bun"
 import z from "zod"
 import { Pty } from "@/pty"
+import { PtyID } from "@/pty/schema"
 import { NotFoundError } from "../../storage/db"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"
@@ -72,7 +73,7 @@ export const PtyRoutes = lazy(() =>
           ...errors(404),
         },
       }),
-      validator("param", z.object({ ptyID: z.string() })),
+      validator("param", z.object({ ptyID: PtyID.zod })),
       async (c) => {
         const info = Pty.get(c.req.valid("param").ptyID)
         if (!info) {
@@ -99,7 +100,7 @@ export const PtyRoutes = lazy(() =>
           ...errors(400),
         },
       }),
-      validator("param", z.object({ ptyID: z.string() })),
+      validator("param", z.object({ ptyID: PtyID.zod })),
       validator("json", Pty.UpdateInput),
       async (c) => {
         const info = await Pty.update(c.req.valid("param").ptyID, c.req.valid("json"))
@@ -124,7 +125,7 @@ export const PtyRoutes = lazy(() =>
           ...errors(404),
         },
       }),
-      validator("param", z.object({ ptyID: z.string() })),
+      validator("param", z.object({ ptyID: PtyID.zod })),
       async (c) => {
         await Pty.remove(c.req.valid("param").ptyID)
         return c.json(true)
@@ -148,9 +149,9 @@ export const PtyRoutes = lazy(() =>
           ...errors(404),
         },
       }),
-      validator("param", z.object({ ptyID: z.string() })),
+      validator("param", z.object({ ptyID: PtyID.zod })),
       upgradeWebSocket((c) => {
-        const id = c.req.param("ptyID")
+        const id = PtyID.zod.parse(c.req.param("ptyID"))
         const cursor = (() => {
           const value = c.req.query("cursor")
           if (!value) return

+ 3 - 2
packages/opencode/src/server/routes/question.ts

@@ -1,6 +1,7 @@
 import { Hono } from "hono"
 import { describeRoute, validator } from "hono-openapi"
 import { resolver } from "hono-openapi"
+import { QuestionID } from "@/question/schema"
 import { Question } from "../../question"
 import z from "zod"
 import { errors } from "../error"
@@ -51,7 +52,7 @@ export const QuestionRoutes = lazy(() =>
       validator(
         "param",
         z.object({
-          requestID: z.string(),
+          requestID: QuestionID.zod,
         }),
       ),
       validator("json", Question.Reply),
@@ -86,7 +87,7 @@ export const QuestionRoutes = lazy(() =>
       validator(
         "param",
         z.object({
-          requestID: z.string(),
+          requestID: QuestionID.zod,
         }),
       ),
       async (c) => {

+ 2 - 1
packages/opencode/src/server/routes/session.ts

@@ -15,6 +15,7 @@ import { Agent } from "../../agent/agent"
 import { Snapshot } from "@/snapshot"
 import { Log } from "../../util/log"
 import { PermissionNext } from "@/permission/next"
+import { PermissionID } from "@/permission/schema"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"
 
@@ -957,7 +958,7 @@ export const SessionRoutes = lazy(() =>
         "param",
         z.object({
           sessionID: SessionID.zod,
-          permissionID: z.string(),
+          permissionID: PermissionID.zod,
         }),
       ),
       validator("json", z.object({ response: PermissionNext.Reply })),

+ 6 - 6
packages/opencode/src/session/schema.ts

@@ -4,7 +4,7 @@ import z from "zod"
 import { withStatics } from "@/util/schema"
 import { Identifier } from "@/id/id"
 
-const sessionIdSchema = Schema.String.pipe(Schema.brand("SessionId"))
+const sessionIdSchema = Schema.String.pipe(Schema.brand("SessionID"))
 
 export type SessionID = typeof sessionIdSchema.Type
 
@@ -12,11 +12,11 @@ export const SessionID = sessionIdSchema.pipe(
   withStatics((schema: typeof sessionIdSchema) => ({
     make: (id: string) => schema.makeUnsafe(id),
     descending: (id?: string) => schema.makeUnsafe(Identifier.descending("session", id)),
-    zod: z.string().startsWith("ses").pipe(z.custom<SessionID>()),
+    zod: Identifier.schema("session").pipe(z.custom<SessionID>()),
   })),
 )
 
-const messageIdSchema = Schema.String.pipe(Schema.brand("MessageId"))
+const messageIdSchema = Schema.String.pipe(Schema.brand("MessageID"))
 
 export type MessageID = typeof messageIdSchema.Type
 
@@ -24,11 +24,11 @@ export const MessageID = messageIdSchema.pipe(
   withStatics((schema: typeof messageIdSchema) => ({
     make: (id: string) => schema.makeUnsafe(id),
     ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("message", id)),
-    zod: z.string().startsWith("msg").pipe(z.custom<MessageID>()),
+    zod: Identifier.schema("message").pipe(z.custom<MessageID>()),
   })),
 )
 
-const partIdSchema = Schema.String.pipe(Schema.brand("PartId"))
+const partIdSchema = Schema.String.pipe(Schema.brand("PartID"))
 
 export type PartID = typeof partIdSchema.Type
 
@@ -36,6 +36,6 @@ export const PartID = partIdSchema.pipe(
   withStatics((schema: typeof partIdSchema) => ({
     make: (id: string) => schema.makeUnsafe(id),
     ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("part", id)),
-    zod: z.string().startsWith("prt").pipe(z.custom<PartID>()),
+    zod: Identifier.schema("part").pipe(z.custom<PartID>()),
   })),
 )

+ 17 - 0
packages/opencode/src/tool/schema.ts

@@ -0,0 +1,17 @@
+import { Schema } from "effect"
+import z from "zod"
+
+import { Identifier } from "@/id/id"
+import { withStatics } from "@/util/schema"
+
+const toolIdSchema = Schema.String.pipe(Schema.brand("ToolID"))
+
+export type ToolID = typeof toolIdSchema.Type
+
+export const ToolID = toolIdSchema.pipe(
+  withStatics((schema: typeof toolIdSchema) => ({
+    make: (id: string) => schema.makeUnsafe(id),
+    ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("tool", id)),
+    zod: Identifier.schema("tool").pipe(z.custom<ToolID>()),
+  })),
+)

+ 2 - 1
packages/opencode/src/tool/truncation.ts

@@ -7,6 +7,7 @@ import type { Agent } from "../agent/agent"
 import { Scheduler } from "../scheduler"
 import { Filesystem } from "../util/filesystem"
 import { Glob } from "../util/glob"
+import { ToolID } from "./schema"
 
 export namespace Truncate {
   export const MAX_LINES = 2000
@@ -90,7 +91,7 @@ export namespace Truncate {
     const unit = hitBytes ? "bytes" : "lines"
     const preview = out.join("\n")
 
-    const id = Identifier.ascending("tool")
+    const id = ToolID.ascending()
     const filepath = path.join(DIR, id)
     await Filesystem.write(filepath, text)
 

+ 10 - 9
packages/opencode/test/permission/next.test.ts

@@ -1,6 +1,7 @@
 import { test, expect } from "bun:test"
 import os from "os"
 import { PermissionNext } from "../../src/permission/next"
+import { PermissionID } from "../../src/permission/schema"
 import { Instance } from "../../src/project/instance"
 import { tmpdir } from "../fixture/fixture"
 import { SessionID } from "../../src/session/schema"
@@ -522,7 +523,7 @@ test("reply - once resolves the pending ask", async () => {
     directory: tmp.path,
     fn: async () => {
       const askPromise = PermissionNext.ask({
-        id: "permission_test1",
+        id: PermissionID.make("per_test1"),
         sessionID: SessionID.make("session_test"),
         permission: "bash",
         patterns: ["ls"],
@@ -532,7 +533,7 @@ test("reply - once resolves the pending ask", async () => {
       })
 
       await PermissionNext.reply({
-        requestID: "permission_test1",
+        requestID: PermissionID.make("per_test1"),
         reply: "once",
       })
 
@@ -547,7 +548,7 @@ test("reply - reject throws RejectedError", async () => {
     directory: tmp.path,
     fn: async () => {
       const askPromise = PermissionNext.ask({
-        id: "permission_test2",
+        id: PermissionID.make("per_test2"),
         sessionID: SessionID.make("session_test"),
         permission: "bash",
         patterns: ["ls"],
@@ -557,7 +558,7 @@ test("reply - reject throws RejectedError", async () => {
       })
 
       await PermissionNext.reply({
-        requestID: "permission_test2",
+        requestID: PermissionID.make("per_test2"),
         reply: "reject",
       })
 
@@ -572,7 +573,7 @@ test("reply - always persists approval and resolves", async () => {
     directory: tmp.path,
     fn: async () => {
       const askPromise = PermissionNext.ask({
-        id: "permission_test3",
+        id: PermissionID.make("per_test3"),
         sessionID: SessionID.make("session_test"),
         permission: "bash",
         patterns: ["ls"],
@@ -582,7 +583,7 @@ test("reply - always persists approval and resolves", async () => {
       })
 
       await PermissionNext.reply({
-        requestID: "permission_test3",
+        requestID: PermissionID.make("per_test3"),
         reply: "always",
       })
 
@@ -613,7 +614,7 @@ test("reply - reject cancels all pending for same session", async () => {
     directory: tmp.path,
     fn: async () => {
       const askPromise1 = PermissionNext.ask({
-        id: "permission_test4a",
+        id: PermissionID.make("per_test4a"),
         sessionID: SessionID.make("session_same"),
         permission: "bash",
         patterns: ["ls"],
@@ -623,7 +624,7 @@ test("reply - reject cancels all pending for same session", async () => {
       })
 
       const askPromise2 = PermissionNext.ask({
-        id: "permission_test4b",
+        id: PermissionID.make("per_test4b"),
         sessionID: SessionID.make("session_same"),
         permission: "edit",
         patterns: ["foo.ts"],
@@ -638,7 +639,7 @@ test("reply - reject cancels all pending for same session", async () => {
 
       // Reject the first one
       await PermissionNext.reply({
-        requestID: "permission_test4a",
+        requestID: PermissionID.make("per_test4a"),
         reply: "reject",
       })
 

+ 3 - 2
packages/opencode/test/question/question.test.ts

@@ -1,6 +1,7 @@
 import { test, expect } from "bun:test"
 import { Question } from "../../src/question"
 import { Instance } from "../../src/project/instance"
+import { QuestionID } from "../../src/question/schema"
 import { tmpdir } from "../fixture/fixture"
 import { SessionID } from "../../src/session/schema"
 
@@ -131,7 +132,7 @@ test("reply - does nothing for unknown requestID", async () => {
     directory: tmp.path,
     fn: async () => {
       await Question.reply({
-        requestID: "que_unknown",
+        requestID: QuestionID.make("que_unknown"),
         answers: [["Option 1"]],
       })
       // Should not throw
@@ -204,7 +205,7 @@ test("reject - does nothing for unknown requestID", async () => {
   await Instance.provide({
     directory: tmp.path,
     fn: async () => {
-      await Question.reject("que_unknown")
+      await Question.reject(QuestionID.make("que_unknown"))
       // Should not throw
     },
   })