mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-10 16:13:41 +00:00
163 lines
7.3 KiB
Markdown
163 lines
7.3 KiB
Markdown
# Mixed V1/V2 Config Normalization Plan
|
|
|
|
Status: **Implemented and verified**
|
|
|
|
## Goal
|
|
|
|
Replace whole-document V1/V2 detection with one config-domain compatibility pipeline. Supported V1 fields, native V2 fields, and practical mixtures of both should load without an unrelated legacy key changing how the rest of the document is decoded.
|
|
|
|
## Decision
|
|
|
|
Normalize recognized fields independently into the encoded side of the V2 `Config.Info` schema, then perform one final complete-document V2 decode:
|
|
|
|
```text
|
|
JSON/JSONC encoded input
|
|
-> parse and retain source-property presence
|
|
-> validate each recognized field or collection entry
|
|
-> migrate supported V1 candidates to V2 encoded values
|
|
-> decode and re-encode native V2 candidates
|
|
-> merge with native V2 precedence
|
|
-> decode Config.Info once
|
|
-> log redacted diagnostics
|
|
```
|
|
|
|
There is no whole-document version classification and no independent whole-document V1 and V2 decode.
|
|
|
|
The encoded boundary matters because schemas such as warming durations transform strings into runtime values. Decoded values must not be fed back into the encoded side of `Config.Info`.
|
|
|
|
## Behavior
|
|
|
|
| Situation | Result |
|
|
| --- | --- |
|
|
| Supported V1-only field | Migrate it to its canonical V2 destination. |
|
|
| Native V2 field | Preserve it after schema decode and encode. |
|
|
| Disjoint V1 and V2 map entries | Preserve both. |
|
|
| Same canonical scalar, map entry, or nested leaf | Valid native V2 wins regardless of JSON key order. |
|
|
| Malformed native value with valid legacy fallback | Skip native value, log it, and retain legacy value. |
|
|
| Malformed collection entry | Skip only the explicitly supported recovery unit. |
|
|
| Unsupported accepted V1 setting | Omit it and log a redacted warning. |
|
|
| Unknown field | Continue ignoring it for forward compatibility. |
|
|
|
|
Valid supported V1 syntax does not warn merely because it is legacy.
|
|
|
|
## Field Precedence
|
|
|
|
| Destination | Lowest to highest precedence |
|
|
| --- | --- |
|
|
| `snapshots` | `snapshot` < `snapshots` |
|
|
| `share` | `autoshare` < `share` |
|
|
| `references[name]` | `reference[name]` < `references[name]` |
|
|
| `agents[name]` | `agent[name]` < `mode[name]` < `agents[name]` |
|
|
| `commands[name]` | `command[name]` < `commands[name]` |
|
|
| `providers[name]` | `provider[name]` < `providers[name]` |
|
|
| `permissions` | `tools` rules < `permission` rules < native `permissions` |
|
|
| `plugins` | migrated `plugin` items < native `plugins` items |
|
|
| `media` | `attachment` < `media` |
|
|
| `experimental.policies` | enabled-provider policies < disabled-provider policies < native policies |
|
|
| `mcp.servers[name]` | direct legacy server < native `servers[name]` |
|
|
| `mcp.timeout.*` | `experimental.mcp_timeout` < native timeout leaf |
|
|
| `compaction.keep.tokens` | `preserve_recent_tokens` < `keep.tokens` |
|
|
| `compaction.buffer` | `reserved` < `buffer` |
|
|
|
|
Ordered rules and plugin directives retain both forms, with migrated V1 entries first and native V2 entries last.
|
|
|
|
## Shared Shapes
|
|
|
|
### Skills
|
|
|
|
- A V2 array retains each valid string item.
|
|
- A V1 object combines valid `paths` followed by valid `urls`.
|
|
- Empty and unknown-only V1 objects normalize to an empty array under permissive excess-property handling.
|
|
|
|
### MCP
|
|
|
|
- Direct entries under `mcp` are V1 servers.
|
|
- Entries under `mcp.servers` are native V2 servers.
|
|
- Both sets are merged by server name, with a complete native server replacing a duplicate legacy server.
|
|
- A malformed native duplicate is skipped so a valid legacy server remains.
|
|
- Native global timeout leaves override only matching values migrated from `experimental.mcp_timeout`.
|
|
- Raw `type` and `enabled` discriminators preserve legacy servers that happen to be named `servers` or `timeout`.
|
|
|
|
### Compaction
|
|
|
|
- `preserve_recent_tokens` becomes `keep.tokens`.
|
|
- `reserved` becomes `buffer`.
|
|
- Native leaves win conflicts.
|
|
- `tail_turns` and `prune` remain unsupported and produce warnings.
|
|
|
|
### Experimental
|
|
|
|
- `subagent_depth` is shared.
|
|
- Legacy provider lists generate ordered canonical policies.
|
|
- Native policies follow generated policies.
|
|
- An explicit empty `enabled_providers` keeps deny-all behavior.
|
|
- A non-empty list with no valid items contributes no policy, avoiding accidental deny-all from malformed input.
|
|
|
|
## Recovery Units
|
|
|
|
Named commands, agents, providers, MCP servers, formatters, language servers, and references recover independently. Plugin, permission, skill, instruction, provider-ID, and policy arrays recover by item. Top-level legacy permissions recover by action/resource rule. Complex interiors of one agent, provider, command, or MCP server remain atomic rather than being recursively salvaged.
|
|
|
|
Every decoder preserves `propertyOrder: "original"` because V1 permission precedence depends on user order. Excess properties remain ignored except for the explicit unsupported inventory.
|
|
|
|
## Provider IDs
|
|
|
|
Provider ID compatibility remains a config migration concern only. Existing V1 agent, command, provider, and provider-policy adapters continue using the migration helper's retired-ID mapping.
|
|
|
|
The shared top-level `model` field remains exact because its string and object forms are valid native V2 syntax and provider declarations may come from a different config layer. It is never reinterpreted based on unrelated legacy fields.
|
|
|
|
This change does not add runtime provider aliases or modify provider policy evaluation, catalog state, model resolution, Sessions, plugins, Server behavior, or generation.
|
|
|
|
## Diagnostics
|
|
|
|
Diagnostics contain only source, JSON path, category, and action. They never include raw values because config may contain credentials after substitution.
|
|
|
|
Malformed JSON, empty content, and valid non-object roots reject one document with a source-aware warning. Malformed recognized fields and entries are skipped at their recovery boundary while unrelated valid configuration continues loading.
|
|
|
|
## Implementation
|
|
|
|
- Add a pure `ConfigNormalize.normalize` module under `packages/core/src/config/`.
|
|
- Reuse field migration primitives from `packages/core/src/v1/config/migrate.ts`.
|
|
- Replace `ConfigMigrateV1.isV1` in `packages/core/src/config.ts` with normalization and one final V2 decode.
|
|
- Log diagnostics uniformly for files, `OPENCODE_CONFIG_CONTENT`, and well-known virtual config.
|
|
- Add property and table-driven config normalization tests.
|
|
- Update migration and compaction documentation.
|
|
|
|
## Verification
|
|
|
|
The implementation must establish:
|
|
|
|
1. Valid native V2 config preserves decoded meaning after encoded normalization.
|
|
2. Supported V1 fields preserve existing behavior.
|
|
3. Adding a legacy field cannot change unrelated native field interpretation.
|
|
4. Native V2 wins canonical conflicts independent of key order.
|
|
5. One malformed entry does not remove valid siblings.
|
|
6. Mixed MCP, compaction, and experimental values normalize deterministically.
|
|
7. Diagnostics are precise and value-redacted.
|
|
8. False, zero, empty, and absent values retain distinct presence semantics.
|
|
|
|
Run from `packages/core`:
|
|
|
|
```sh
|
|
bun test test/config
|
|
bun typecheck
|
|
```
|
|
|
|
Run from `packages/www` after documentation changes:
|
|
|
|
```sh
|
|
bun typecheck
|
|
bun validate
|
|
bun run build
|
|
```
|
|
|
|
## Non-Goals
|
|
|
|
- Runtime provider alias resolution.
|
|
- Provider policy or catalog changes.
|
|
- Model resolver or Session changes.
|
|
- Plugin API changes.
|
|
- Server or Protocol changes.
|
|
- Generation lifecycle changes.
|
|
- Recursive V1/V2 inference inside one agent, provider, command, or model.
|
|
- Restoring removed V1 functionality.
|
|
- Rewriting user files on disk.
|