summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-17 23:05:06 -0400
committerGitHub <[email protected]>2026-04-18 03:05:06 +0000
commita6a4350d1032da8b1ae1ae1a51f11b7a0844fcff (patch)
treec85ace3550d6d4bab3e19afc20778db5cd11d490
parent471b9f4dc443869795e0732c668e366150951d36 (diff)
downloadopencode-a6a4350d1032da8b1ae1ae1a51f11b7a0844fcff.tar.gz
opencode-a6a4350d1032da8b1ae1ae1a51f11b7a0844fcff.zip
refactor(config): migrate permission.ts Info to Effect Schema (#23231)
-rw-r--r--packages/opencode/src/config/permission.ts85
1 files changed, 46 insertions, 39 deletions
diff --git a/packages/opencode/src/config/permission.ts b/packages/opencode/src/config/permission.ts
index 7cfbaec01..eb3ac412b 100644
--- a/packages/opencode/src/config/permission.ts
+++ b/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>