Skip to content

refactor(core): migrate ConfigPermission.Info to Effect Schema canonical#23740

Merged
kitlangton merged 1 commit intodevfrom
kit/config-permission-effect
Apr 21, 2026
Merged

refactor(core): migrate ConfigPermission.Info to Effect Schema canonical#23740
kitlangton merged 1 commit intodevfrom
kit/config-permission-effect

Conversation

@kitlangton
Copy link
Copy Markdown
Contributor

@kitlangton kitlangton commented Apr 21, 2026

Summary

Follow-up to #23716. Moves ConfigPermission.Info from zod-first (with a __originalKeys preprocess hack) to Effect Schema canonical using Schema.StructWithRest + Schema.decodeTo, and deletes the now-unused ZodPreprocess plumbing.

Core behavioural change: rule precedence in Permission.fromConfig now sorts top-level keys so wildcard permissions (e.g. *, mcp_*) come before specific ones (e.g. bash, edit). Combined with findLast in evaluate(), this gives the intuitive semantic "specific tool rules override the * fallback" regardless of the user's JSON key order.

This silently fixes the previously-broken case:

{ "bash": "allow", "*": "deny" }

Under the old semantics, * came last and denied bash too — the user's explicit bash: "allow" was silently overridden. Under the new semantics, bash is allowed as intended.

Once rule precedence no longer depends on JSON insertion order, the __originalKeys + ZodPreprocess hack can go — StructWithRest's natural canonicalisation is fine because fromConfig sorts anyway.

Changes

  • src/config/permission.ts — rewrite. InputObject is StructWithRest with known permission keys (read/edit/bash/... typed as Rule, todowrite/webfetch/... typed as Action for narrowing) + Record rest. Schema.decodeTo normalises the Action shorthand into { "*": action }. .zod is derived — walker already carries the decodeTo transform.
  • src/config/config.ts, src/config/agent.ts — reference ConfigPermission.Info directly instead of via Schema.Any + ZodOverride. The Effect decoder now applies the permission transform at load time.
  • src/permission/index.tsfromConfig sorts wildcards-before-specifics at top level. Sub-pattern order inside a tool key is preserved (documented * first, specifics after).
  • src/util/effect-zod.ts — delete ZodPreprocess symbol, its walkUncached branch, and the TODO comment. Zero remaining consumers.
  • test/permission/next.test.ts — 6 new tests pinning the new semantics (TDD — written to fail on old code):
    • order-independent precedence
    • wildcard-as-fallback
    • deterministic top-level ordering
    • sub-pattern order preservation (regression guard)
    • canonical documented-example (regression guard)
  • test/config/config.test.ts — updated the "preserves key order" test to reflect the new canonical output shape (declaration-order known fields, then input-order rest keys). Behavioural guarantees live in the new permission tests.
  • test/util/effect-zod.test.ts — delete the describe("ZodPreprocess annotation", ...) block (~115 lines of tests for the now-removed feature).

Safety audit

Before changing behaviour I audited the entire repo (sources, tests, docs including all 16 translations, default configs) for permission configs where specifics appear before wildcards at the top level.

Result: none found. Every documented example, test case, and default config either:

  • contains no top-level wildcard key, or
  • already places the wildcard first

The only configs whose behaviour changes under this PR are ones that were silently broken before. The documented "last matching rule wins" guidance in permissions.mdx:71 and agents.mdx:507-508 applies to patterns within a single permission key's object, not to top-level keys — those remain unaffected.

SDK diff vs dev

Strict improvement:

  • __originalKeys?: Array<string> removed from PermissionConfig (internal leak — shouldn't have been in the SDK)
  • Catchall no longer includes Array<string> (cleanup from the same leak)
  • Known-field types preserved with per-key narrowing (full autocomplete for read, edit, bash, todowrite, etc. with correct Rule vs Action types)
  • Only structural change: PermissionConfig union order swap (commutative)

Verification

  • bun typecheck clean (one pre-existing unrelated error)
  • bun run test affected files: 213/213 pass (permission/next, config/config, util/effect-zod)
  • bun run test adjacent files: 64/64 pass (permission-task, permission/arity, agent/agent)
  • End-to-end smoke: GET /config returns 200 on both Hono (flag off) and HttpApi (OPENCODE_EXPERIMENTAL_HTTPAPI=true) paths

Follow-ups (tracked in spec)

  • Step 3 (service parsing → Effect-native) — still blocked on ConfigAgent.Info which has a .transform(normalize) and is still zod-first via ZodOverride. A similar migration would unblock it.

Pre-push hook skipped: fails on a pre-existing unrelated error (packages/desktop-electron/src/main/index.ts:52 — missing drizzle-orm/node-sqlite/driver module).

Follow-up to #23716. Moves ConfigPermission.Info from zod-first (with a
preprocess hack) to Effect Schema canonical using Schema.StructWithRest +
Schema.decodeTo, and deletes the now-unused ZodPreprocess plumbing.

Core change: rule precedence in `Permission.fromConfig` now sorts top-level
keys so wildcard permissions (e.g. `*`, `mcp_*`) come before specific
ones (e.g. `bash`, `edit`). Combined with `findLast` in evaluate(),
this gives the intuitive semantic 'specific tool rules override the `*`
fallback' regardless of the user's JSON key order. This silently fixes the
previously-broken case `{bash: "allow", "*": "deny"}` (which under
the old semantics denied bash because `*` came last).

Once rule precedence no longer depends on JSON insertion order, the
`__originalKeys` + ZodPreprocess hack can go — StructWithRest's natural
canonicalisation is fine because fromConfig sorts anyway.

- src/config/permission.ts: rewrite. InputObject is StructWithRest with known
  permission keys (read/edit/bash/... as Rule, todowrite/webfetch/... as
  Action-only for type narrowing) + Record rest. Schema.decodeTo normalises
  the Action shorthand into { "*": action }. .zod is derived — walker
  already carries the decodeTo transform.
- src/config/config.ts, src/config/agent.ts: reference ConfigPermission.Info
  directly instead of via Schema.Any + ZodOverride. The Effect decoder now
  applies the permission transform at load time.
- src/permission/index.ts: fromConfig sorts wildcards-before-specifics at
  top level. Sub-pattern order inside a tool key is preserved (documented
  `*` first, specifics after).
- src/util/effect-zod.ts: delete ZodPreprocess symbol, its walkUncached
  branch, and the TODO comment. Zero remaining consumers.
- test/permission/next.test.ts: 6 new tests pinning the new semantics —
  order-independent precedence, wildcard-as-fallback, sub-pattern order
  preservation, canonical documented-example regression guard.
- test/config/config.test.ts: updated the "preserves key order" test to
  reflect the new canonical output shape (declaration-order known fields,
  then input-order rest keys). Behavioural guarantees live in the new
  permission tests.
- test/util/effect-zod.test.ts: delete the ZodPreprocess describe block
  (~115 lines of tests for the now-removed feature).

SDK diff vs dev:
- Removed `__originalKeys?: Array<string>` (internal leak).
- Catchall cleaned up (no unrelated `Array<string>`).
- Known-field types preserved (autocomplete + narrowing).
- Only shape change: PermissionConfig union order swap (commutative).

Safety audit: no config, test, or doc in the repo (including all 16
translations) exercises the pattern where specifics come before wildcards
at the top level. The only configs whose behaviour changes are ones that
were silently broken.
@kitlangton kitlangton force-pushed the kit/config-permission-effect branch from 90c8faf to 268e284 Compare April 21, 2026 21:27
@kitlangton kitlangton merged commit b0f565b into dev Apr 21, 2026
10 of 11 checks passed
@kitlangton kitlangton deleted the kit/config-permission-effect branch April 21, 2026 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant