refactor(core): make Config.Info canonical Effect Schema#23716
Merged
kitlangton merged 1 commit intodevfrom Apr 21, 2026
Merged
refactor(core): make Config.Info canonical Effect Schema#23716kitlangton merged 1 commit intodevfrom
kitlangton merged 1 commit intodevfrom
Conversation
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).
4c12daa to
2137598
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Step 2 of the incremental
Config.Infomigration (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 internalInfoSchemais renamed toInfoStruct(private).Infois now exported as the Effect Schema with.annotate({ identifier: "Config" }), and.zodis derived viawithStatics(s => ({ zod: (zod(s).strict().meta({ ref: "Config" })) })).type Infopreserved (DeepMutable<...> & { plugin_origins? })..zod:src/server/routes/instance/config.ts—resolver(Config.Info.zod),validator("json", Config.Info.zod)src/server/routes/global.ts— sameConfigParse.schema(...)call sites switched toConfig.Info.zod:src/config/config.ts(loadConfig, updateGlobal)test/config/config.test.ts(5 sites)test/session/compaction.test.ts(Config.Info.parse→Config.Info.zod.parse)script/schema.tsusesConfig.Info.zodfor JSON Schema generationhttpapi/config.tsnow referencesConfig.Infodirectly (wasConfig.InfoSchema)Rationale
Spec ordering rule (
http-api.md):Prior state had
Config.Infoas the canonical zod andInfoSchemaas 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.zodis a single derived compatibility surface — meeting the same pattern used elsewhere (e.g.Provider.Info,ConfigProvider.Info).Verification
bun typecheckclean (one pre-existing unrelated error incli/cmd/tui/app.tsx)./packages/sdk/js/script/build.tsregenerated;packages/sdk/js/src/v2/gen/types.gen.tsbyte-identical to dev (5493 lines, zero diff)OPENCODE_EXPERIMENTAL_HTTPAPI=true:GET /config→ 200,GET /config/providers→ 200 (HttpApi path)GET /config→ 200,GET /config/providers→ 200 (Hono path)Follow-ups (tracked in spec)
Config.Serviceparsing to useSchema.decodeUnknown(Config.Info)directly instead of the.zodround-trip. This removes the zod surface from the service layer..zodsurface as HttpApi takes over each remaining Hono route group.Also note: this PR keeps
DeepMutablelocal toconfig.ts(as pre-existing) becauseTypes.DeepMutablefrom effect-smol collapsesunknownto{}at the fallback branch, which would widenRecord<string, unknown>fields likeConfigPlugin.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— missingdrizzle-orm/node-sqlite/drivermodule).