fix permission config order (#24222)

This commit is contained in:
Dax
2026-04-25 09:18:42 -04:00
committed by GitHub
parent 9ff999cc2b
commit 66f93035b0
4 changed files with 40 additions and 70 deletions

View File

@@ -1,6 +1,7 @@
export * as ConfigPermission from "./permission"
import { Schema, SchemaGetter } from "effect"
import { zod } from "@/util/effect-zod"
import z from "zod"
import { ZodOverride, zod } from "@/util/effect-zod"
import { withStatics } from "@/util/schema"
export const Action = Schema.Literals(["ask", "allow", "deny"])
@@ -18,17 +19,9 @@ export const Rule = Schema.Union([Action, Object])
.pipe(withStatics((s) => ({ zod: zod(s) })))
export type Rule = Schema.Schema.Type<typeof Rule>
// Known permission keys get explicit types — most are full Rule (either a
// single Action or a per-pattern object), but a handful of tools take no
// sub-target patterns and are Action-only. Unknown keys fall through the
// Record rest signature as Rule.
//
// StructWithRest canonicalises key order on decode (known first, then rest),
// which used to require the `__originalKeys` preprocess hack because
// `Permission.fromConfig` depended on the user's insertion order. That
// dependency is gone — `fromConfig` now sorts top-level keys so wildcard
// permissions come before specifics, making the final precedence
// order-independent.
// Known permission keys get explicit types in the Effect schema for generated
// docs/types. Runtime config parsing uses `InfoZod` below so user key order is
// preserved for permission precedence.
const InputObject = Schema.StructWithRest(
Schema.Struct({
read: Schema.optional(Rule),
@@ -60,6 +53,18 @@ const InputSchema = Schema.Union([Action, InputObject])
const normalizeInput = (input: Schema.Schema.Type<typeof InputSchema>): Schema.Schema.Type<typeof InputObject> =>
typeof input === "string" ? { "*": input } : input
const ACTION_ONLY = new Set(["todowrite", "question", "webfetch", "websearch", "codesearch", "doom_loop"])
const InfoZod = z
.union([zod(Action), z.record(z.string(), z.union([zod(Action), z.record(z.string(), zod(Action))]))])
.transform(normalizeInput)
.superRefine((input, ctx) => {
for (const [key, value] of globalThis.Object.entries(input)) {
if (!ACTION_ONLY.has(key) || typeof value === "string") continue
ctx.addIssue({ code: "custom", message: `${key} must be a permission action`, path: [key] })
}
})
export const Info = InputSchema.pipe(
Schema.decodeTo(InputObject, {
decode: SchemaGetter.transform(normalizeInput),
@@ -70,6 +75,7 @@ export const Info = InputSchema.pipe(
}),
)
.annotate({ identifier: "PermissionConfig" })
.annotate({ [ZodOverride]: InfoZod })
.pipe(
// Walker already emits the decodeTo transform into the derived zod (see
// `encoded()` in effect-zod.ts), so just expose that directly.