ソースを参照

refactor(config): migrate permission.ts Info to Effect Schema (#23231)

Kit Langton 3 日 前
コミット
a6a4350d10
1 ファイル変更46 行追加39 行削除
  1. 46 39
      packages/opencode/src/config/permission.ts

+ 46 - 39
packages/opencode/src/config/permission.ts

@@ -1,16 +1,8 @@
 export * as ConfigPermission from "./permission"
 import { Schema } from "effect"
-import z from "zod"
-import { zod } from "@/util/effect-zod"
+import { zod, ZodPreprocess } from "@/util/effect-zod"
 import { withStatics } from "@/util/schema"
 
-const permissionPreprocess = (val: unknown) => {
-  if (typeof val === "object" && val !== null && !Array.isArray(val)) {
-    return { __originalKeys: globalThis.Object.keys(val), ...val }
-  }
-  return val
-}
-
 export const Action = Schema.Literals(["ask", "allow", "deny"])
   .annotate({ identifier: "PermissionActionConfig" })
   .pipe(withStatics((s) => ({ zod: zod(s) })))
@@ -26,6 +18,48 @@ export const Rule = Schema.Union([Action, Object])
   .pipe(withStatics((s) => ({ zod: zod(s) })))
 export type Rule = Schema.Schema.Type<typeof Rule>
 
+// Captures the user's original property insertion order before Schema.Struct
+// canonicalises the object.  See the `ZodPreprocess` comment in
+// `util/effect-zod.ts` for the full rationale — in short: rule precedence is
+// encoded in JSON key order (`evaluate.ts` uses `findLast`, so later keys win)
+// and `Schema.StructWithRest` would otherwise drop that order.
+const permissionPreprocess = (val: unknown) => {
+  if (typeof val === "object" && val !== null && !Array.isArray(val)) {
+    return { __originalKeys: globalThis.Object.keys(val), ...val }
+  }
+  return val
+}
+
+const ObjectShape = Schema.StructWithRest(
+  Schema.Struct({
+    __originalKeys: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
+    read: Schema.optional(Rule),
+    edit: Schema.optional(Rule),
+    glob: Schema.optional(Rule),
+    grep: Schema.optional(Rule),
+    list: Schema.optional(Rule),
+    bash: Schema.optional(Rule),
+    task: Schema.optional(Rule),
+    external_directory: Schema.optional(Rule),
+    todowrite: Schema.optional(Action),
+    question: Schema.optional(Action),
+    webfetch: Schema.optional(Action),
+    websearch: Schema.optional(Action),
+    codesearch: Schema.optional(Action),
+    lsp: Schema.optional(Rule),
+    doom_loop: Schema.optional(Action),
+    skill: Schema.optional(Rule),
+  }),
+  [Schema.Record(Schema.String, Rule)],
+)
+
+const InnerSchema = Schema.Union([ObjectShape, Action]).annotate({
+  [ZodPreprocess]: permissionPreprocess,
+})
+
+// Post-parse: drop the __originalKeys metadata and rebuild the rule map in the
+// user's original insertion order.  A plain string input (the Action branch of
+// the union) becomes `{ "*": action }`.
 const transform = (x: unknown): Record<string, Rule> => {
   if (typeof x === "string") return { "*": x as Action }
   const obj = x as { __originalKeys?: string[] } & Record<string, unknown>
@@ -38,34 +72,7 @@ const transform = (x: unknown): Record<string, Rule> => {
   return result
 }
 
-export const Info = z
-  .preprocess(
-    permissionPreprocess,
-    z
-      .object({
-        __originalKeys: z.string().array().optional(),
-        read: Rule.zod.optional(),
-        edit: Rule.zod.optional(),
-        glob: Rule.zod.optional(),
-        grep: Rule.zod.optional(),
-        list: Rule.zod.optional(),
-        bash: Rule.zod.optional(),
-        task: Rule.zod.optional(),
-        external_directory: Rule.zod.optional(),
-        todowrite: Action.zod.optional(),
-        question: Action.zod.optional(),
-        webfetch: Action.zod.optional(),
-        websearch: Action.zod.optional(),
-        codesearch: Action.zod.optional(),
-        lsp: Rule.zod.optional(),
-        doom_loop: Action.zod.optional(),
-        skill: Rule.zod.optional(),
-      })
-      .catchall(Rule.zod)
-      .or(Action.zod),
-  )
+export const Info = zod(InnerSchema)
   .transform(transform)
-  .meta({
-    ref: "PermissionConfig",
-  })
-export type Info = z.infer<typeof Info>
+  .meta({ ref: "PermissionConfig" })
+export type Info = Record<string, Rule>