refactor(core): migrate ConfigPermission.Info to Effect Schema canonical (#23740)
This commit is contained in:
@@ -22,12 +22,6 @@ const Color = Schema.Union([
|
||||
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||
])
|
||||
|
||||
// ConfigPermission.Info is a zod schema (its `.preprocess(...).transform(...)`
|
||||
// shape lives outside the Effect Schema type system), so the walker reaches it
|
||||
// via ZodOverride rather than a pure Schema reference. This preserves the
|
||||
// `$ref: PermissionConfig` emitted in openapi.json.
|
||||
const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info })
|
||||
|
||||
const AgentSchema = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
model: Schema.optional(ConfigModelID),
|
||||
@@ -54,7 +48,7 @@ const AgentSchema = Schema.StructWithRest(
|
||||
description: "Maximum number of agentic iterations before forcing text-only response",
|
||||
}),
|
||||
maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }),
|
||||
permission: Schema.optional(PermissionRef),
|
||||
permission: Schema.optional(ConfigPermission.Info),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Any)],
|
||||
)
|
||||
|
||||
@@ -86,7 +86,6 @@ export type Layout = ConfigLayout.Layout
|
||||
// ZodOverride-annotated Schema.Any. Walker sees the annotation and emits the
|
||||
// exact zod directly, preserving component $refs.
|
||||
const AgentRef = Schema.Any.annotate({ [ZodOverride]: ConfigAgent.Info })
|
||||
const PermissionRef = Schema.Any.annotate({ [ZodOverride]: ConfigPermission.Info })
|
||||
const LogLevelRef = Schema.Any.annotate({ [ZodOverride]: Log.Level })
|
||||
|
||||
const PositiveInt = Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(0))
|
||||
@@ -198,7 +197,7 @@ export const Info = Schema.Struct({
|
||||
description: "Additional instruction files or patterns to include",
|
||||
}),
|
||||
layout: Schema.optional(ConfigLayout.Layout).annotate({ description: "@deprecated Always uses stretch layout." }),
|
||||
permission: Schema.optional(PermissionRef),
|
||||
permission: Schema.optional(ConfigPermission.Info),
|
||||
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
|
||||
enterprise: Schema.optional(
|
||||
Schema.Struct({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as ConfigPermission from "./permission"
|
||||
import { Schema } from "effect"
|
||||
import { zod, ZodPreprocess } from "@/util/effect-zod"
|
||||
import { Schema, SchemaGetter } from "effect"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
export const Action = Schema.Literals(["ask", "allow", "deny"])
|
||||
@@ -18,21 +18,19 @@ 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(
|
||||
// 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.
|
||||
const InputObject = 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),
|
||||
@@ -53,24 +51,29 @@ const ObjectShape = Schema.StructWithRest(
|
||||
[Schema.Record(Schema.String, Rule)],
|
||||
)
|
||||
|
||||
const InnerSchema = Schema.Union([ObjectShape, Action]).annotate({
|
||||
[ZodPreprocess]: permissionPreprocess,
|
||||
})
|
||||
// Input the user writes in config: either a single Action (shorthand for "*")
|
||||
// or an object of per-target rules.
|
||||
const InputSchema = Schema.Union([Action, InputObject])
|
||||
|
||||
// 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>
|
||||
const { __originalKeys, ...rest } = obj
|
||||
if (!__originalKeys) return rest as Record<string, Rule>
|
||||
const result: Record<string, Rule> = {}
|
||||
for (const key of __originalKeys) {
|
||||
if (key in rest) result[key] = rest[key] as Rule
|
||||
}
|
||||
return result
|
||||
}
|
||||
// Normalise the Action shorthand into `{ "*": action }`. Object inputs pass
|
||||
// through untouched.
|
||||
const normalizeInput = (input: Schema.Schema.Type<typeof InputSchema>): Schema.Schema.Type<typeof InputObject> =>
|
||||
typeof input === "string" ? { "*": input } : input
|
||||
|
||||
export const Info = zod(InnerSchema).transform(transform).meta({ ref: "PermissionConfig" })
|
||||
export type Info = Record<string, Rule>
|
||||
export const Info = InputSchema.pipe(
|
||||
Schema.decodeTo(InputObject, {
|
||||
decode: SchemaGetter.transform(normalizeInput),
|
||||
// Not perfectly invertible (we lose whether the user originally typed an
|
||||
// Action shorthand), but the object form is always a valid representation
|
||||
// of the same rules.
|
||||
encode: SchemaGetter.passthrough({ strict: false }),
|
||||
}),
|
||||
)
|
||||
.annotate({ identifier: "PermissionConfig" })
|
||||
.pipe(
|
||||
// Walker already emits the decodeTo transform into the derived zod (see
|
||||
// `encoded()` in effect-zod.ts), so just expose that directly.
|
||||
withStatics((s) => ({ zod: zod(s) })),
|
||||
)
|
||||
type _Info = Schema.Schema.Type<typeof InputObject>
|
||||
export type Info = { -readonly [K in keyof _Info]: _Info[K] }
|
||||
|
||||
Reference in New Issue
Block a user