Skip to content

refactor(core): make Config.Info canonical Effect Schema#23716

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

refactor(core): make Config.Info canonical Effect Schema#23716
kitlangton merged 1 commit intodevfrom
kit/config-effect-canonical

Conversation

@kitlangton
Copy link
Copy Markdown
Contributor

Summary

Step 2 of the incremental Config.Info migration (spec: packages/opencode/specs/effect/http-api.md). This PR flips the canonical direction — the Effect Schema is now the source of truth and the zod surface is derived from it.

Stacks on #23712 (now merged into dev).

What changes

  • src/config/config.ts: the internal InfoSchema is renamed to InfoStruct (private). Info is now exported as the Effect Schema with .annotate({ identifier: "Config" }), and .zod is derived via withStatics(s => ({ zod: (zod(s).strict().meta({ ref: "Config" })) })). type Info preserved (DeepMutable<...> & { plugin_origins? }).
  • Hono boundary updates use .zod:
    • src/server/routes/instance/config.tsresolver(Config.Info.zod), validator("json", Config.Info.zod)
    • src/server/routes/global.ts — same
  • ConfigParse.schema(...) call sites switched to Config.Info.zod:
    • src/config/config.ts (loadConfig, updateGlobal)
    • test/config/config.test.ts (5 sites)
    • test/session/compaction.test.ts (Config.Info.parseConfig.Info.zod.parse)
  • script/schema.ts uses Config.Info.zod for JSON Schema generation
  • HttpApi endpoint in httpapi/config.ts now references Config.Info directly (was Config.InfoSchema)

Rationale

Spec ordering rule (http-api.md):

  1. move exported Info / Input / Output route DTOs to Effect Schema

Prior state had Config.Info as the canonical zod and InfoSchema as a hidden Effect Schema. This is the "parallel Zod and Effect definitions" the spec warns against. After this PR the Effect Schema is canonical and .zod is a single derived compatibility surface — meeting the same pattern used elsewhere (e.g. Provider.Info, ConfigProvider.Info).

Verification

  • bun typecheck clean (one pre-existing unrelated error in cli/cmd/tui/app.tsx)
  • ./packages/sdk/js/script/build.ts regenerated; packages/sdk/js/src/v2/gen/types.gen.ts byte-identical to dev (5493 lines, zero diff)
  • End-to-end smoke test:
    • With OPENCODE_EXPERIMENTAL_HTTPAPI=true: GET /config → 200, GET /config/providers → 200 (HttpApi path)
    • Without the flag: GET /config → 200, GET /config/providers → 200 (Hono path)

Follow-ups (tracked in spec)

  • Step 3: migrate Config.Service parsing to use Schema.decodeUnknown(Config.Info) directly instead of the .zod round-trip. This removes the zod surface from the service layer.
  • Step 4: drop the .zod surface as HttpApi takes over each remaining Hono route group.

Also note: this PR keeps DeepMutable local to config.ts (as pre-existing) because Types.DeepMutable from effect-smol collapses unknown to {} at the fallback branch, which would widen Record<string, unknown> fields like ConfigPlugin.Options. A future consolidation may lift this helper once the effect-smol behavior is addressed.


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).

Step 2 of the incremental Config migration (specs/effect/http-api.md).
Flips the canonical direction so the Effect Schema is the source of truth
and the zod surface is derived from it, eliminating parallel Zod/Effect
definitions of the same type.

- src/config/config.ts: rename internal InfoSchema -> InfoStruct; export
  Info as the Effect Schema (with identifier 'Config') and derive .zod via
  withStatics. Preserve existing DeepMutable + plugin_origins type shape.
- Hono validators and resolvers updated to use Config.Info.zod in
  server/routes/instance/config.ts and server/routes/global.ts.
- ConfigParse.schema() call sites (config.ts, test/config/config.test.ts,
  test/session/compaction.test.ts) and script/schema.ts use Config.Info.zod.
- HttpApi endpoint (httpapi/config.ts) now references Config.Info directly
  since it is the Effect Schema.
- Regenerated SDK: types.gen.ts byte-identical to dev (5493 lines, zero diff).
@kitlangton kitlangton force-pushed the kit/config-effect-canonical branch from 4c12daa to 2137598 Compare April 21, 2026 18:02
@kitlangton kitlangton merged commit ecc06a3 into dev Apr 21, 2026
8 of 9 checks passed
@kitlangton kitlangton deleted the kit/config-effect-canonical branch April 21, 2026 18:06
kitlangton added a commit that referenced this pull request Apr 21, 2026
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.
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