mirror of
https://github.com/anomalyco/opencode.git
synced 2026-06-02 06:16:48 +02:00
feat(core): add location-scoped config loading (#29625)
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
# Catalog / Config / Plugin Lifecycle Options
|
||||
|
||||
We need to choose where provider/model inputs live and how visible catalog state changes after boot. The designs below compare config, models.dev, auth, plugin activation/disablement, config edits, and policy changes under each option.
|
||||
|
||||
## Scenarios
|
||||
|
||||
- Initial load: a location opens, built-in/configured plugins activate, and the first visible catalog is constructed.
|
||||
- Config: authored provider/model definitions and overrides.
|
||||
- models.dev: remote provider/model data refreshed on a timer.
|
||||
- Auth: active credentials enable/configure providers and can later disappear.
|
||||
- Plugin activation: a plugin starts contributing while the location is open.
|
||||
- Plugin disablement: a plugin stops contributing and its influence must disappear.
|
||||
- Config edit: authored configuration changes while the location is open.
|
||||
- Policy: allowed/denied provider selection changes after providers exist.
|
||||
|
||||
## A. Config Transforms, Service Reload
|
||||
|
||||
`Config` merges its ordered documents and then runs ordered, replayable plugin transforms. Each transform is a callback receiving `Draft<Config.Info>` and may mutate any config field.
|
||||
|
||||
```ts
|
||||
type ConfigTransform = (config: Draft<Config.Info>) => void
|
||||
|
||||
const transform = yield * Config.transform()
|
||||
|
||||
yield *
|
||||
transform((config) => {
|
||||
config.providers ??= {}
|
||||
config.providers.acme = {
|
||||
/* ... */
|
||||
}
|
||||
config.model = "acme/code"
|
||||
config.permissions = [
|
||||
/* ... */
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
Because a transform can mutate any part of config, a transform change cannot safely trigger only `Catalog.reload()` or any other granular subset. Every service derived from config must reload in place from the newly transformed config.
|
||||
|
||||
```ts
|
||||
const transform = yield* Config.transform()
|
||||
yield* transform((draft) => mutateAnyConfigField(draft))
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ Agent.reload()
|
||||
→ MCP.reload()
|
||||
→ other config-consuming services reload
|
||||
```
|
||||
|
||||
### Initial Load
|
||||
|
||||
Configured plugin installation/updates should not block location readiness. Build an initial snapshot from authored config and fast built-ins, then activate slow plugins in the background and coalesce their resulting reload requests.
|
||||
|
||||
```ts
|
||||
LocationServiceMap.get(ref)
|
||||
→ build location layer
|
||||
→ Config.layer reads authored documents
|
||||
→ merge authored documents
|
||||
→ run currently active Config transforms
|
||||
→ Policy.layer reads transformed Config
|
||||
→ Catalog.layer reads transformed Config
|
||||
→ materialize baseline provider/model catalog
|
||||
→ PluginBoot baseline ready
|
||||
→ Frontend.fetchCatalog()
|
||||
|
||||
PluginBoot background fiber
|
||||
→ install/update plugin packages concurrently
|
||||
→ activate completed plugins
|
||||
→ Config.transform()
|
||||
→ transform(updateConfig)
|
||||
→ ReloadScheduler.request()
|
||||
→ debounce short burst of completed activations
|
||||
→ Reload.all()
|
||||
→ Config.get()
|
||||
→ run newly active Config transforms
|
||||
→ Catalog.reload()
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
The initial layer build is not a reload. `Reload.all()` only runs after the live location changes, such as a background plugin becoming active or a config source changing. Debouncing reduces repeated full-service reloads when multiple plugins complete near each other; each batch still reloads every config-consuming service because a config transform may mutate any field.
|
||||
|
||||
### Config
|
||||
|
||||
```ts
|
||||
config file loaded
|
||||
→ config source/watch trigger records new documents
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
### models.dev
|
||||
|
||||
```ts
|
||||
timer fires
|
||||
→ ModelsDevPlugin.refresh()
|
||||
→ ModelsDev.get()
|
||||
→ transform(applyModelsDevToConfig)
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
`Catalog` does not know about `ModelsDev`; the plugin transforms config before catalog reads it.
|
||||
|
||||
### Auth
|
||||
|
||||
```ts
|
||||
Account.switched(providerID)
|
||||
→ AuthPlugin.refresh(providerID)
|
||||
→ Account.active(providerID)
|
||||
→ transform(applyAuthToConfig)
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
### Plugin Activation
|
||||
|
||||
```ts
|
||||
Plugin.activate("acme-models")
|
||||
→ Config.transform()
|
||||
→ transform(applyAcmeConfig)
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
### Plugin Disablement
|
||||
|
||||
```ts
|
||||
Plugin.disable("company-naming")
|
||||
→ close plugin scope
|
||||
→ Config internally unregisters transform in finalizer
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ sonnet.name = "Sonnet"
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
### Config Edit
|
||||
|
||||
```ts
|
||||
file watcher sees edit
|
||||
→ config source/watch trigger records updated documents
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
### Policy
|
||||
|
||||
```ts
|
||||
policy config changes
|
||||
→ config source/watch trigger records updated documents
|
||||
→ Reload.all()
|
||||
→ Policy.reload()
|
||||
→ Catalog.reload()
|
||||
→ apply updated policy
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- A plugin receives `Draft<Config.Info>`, can inspect preceding config state, and can mutate arbitrary config fields through a replayable transform.
|
||||
- Plugin disablement removes its config transform and lets services rematerialize without manual undo.
|
||||
- models.dev and auth become config transforms rather than catalog dependencies.
|
||||
- `Config` owns merge/order semantics for fields visible to transforms.
|
||||
- Granular service reload is not safe because a config transform can mutate anything; every config-consuming service reloads after any transform change.
|
||||
- `Catalog` depends on provider/model config semantics and is part of that full service reload.
|
||||
- One reload produces at most one `Catalog.Event.Updated` notification.
|
||||
- Deferred plugin activation avoids blocking readiness, but plugin completions may cause repeated full-service reload batches during startup.
|
||||
|
||||
## B. Catalog Transforms
|
||||
|
||||
Plugins register replayable catalog transforms. Each transform receives a `Catalog.Editor` whose helper methods mutate a private catalog draft; `Catalog` rematerializes visible records from its active transforms.
|
||||
|
||||
```ts
|
||||
interface Catalog {
|
||||
transform(): Effect.Effect<
|
||||
(update: (catalog: Catalog.Editor) => void) => Effect.Effect<void>,
|
||||
never,
|
||||
Scope.Scope
|
||||
>
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
const transform = yield* Catalog.transform()
|
||||
yield* transform(update)
|
||||
→ replace this transform callback
|
||||
→ apply active transforms in registration order
|
||||
→ apply policy
|
||||
→ commit diff
|
||||
→ Event.publish(Catalog.Event.Updated)
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
### Initial Load
|
||||
|
||||
Configured plugin installation/updates should not block location readiness. Build an initial catalog from immediately available sources, then activate slow plugins in the background and coalesce refresh requests.
|
||||
|
||||
```ts
|
||||
LocationServiceMap.get(ref)
|
||||
→ build location layer
|
||||
→ Catalog.layer creates empty catalog state
|
||||
→ PluginBoot.layer activates immediately available plugins
|
||||
→ ConfigProviderPlugin installs Catalog.transform()
|
||||
→ ModelsDevPlugin installs Catalog.transform()
|
||||
→ AuthPlugin installs Catalog.transform()
|
||||
→ Catalog.layer applies active transforms during boot
|
||||
→ apply policy
|
||||
→ materialize baseline provider/model catalog
|
||||
→ PluginBoot baseline ready
|
||||
→ Frontend.fetchCatalog()
|
||||
|
||||
PluginBoot background fiber
|
||||
→ install/update plugin packages concurrently
|
||||
→ activate completed plugins
|
||||
→ Catalog.transform()
|
||||
→ transform(updateCatalog)
|
||||
→ Catalog internally rebuilds
|
||||
→ Catalog.Event.Updated
|
||||
→ Frontend.refetchCatalog()
|
||||
```
|
||||
|
||||
Each completed plugin activation rebuilds catalog when it calls its transform. Debouncing plugin completions would require adding an explicit batch/suspend-rebuild mechanism; it does not arise from the transform interface itself.
|
||||
|
||||
### Config
|
||||
|
||||
```ts
|
||||
config file loaded
|
||||
→ ConfigProviderAdapter.load()
|
||||
→ transform(applyConfigToCatalog)
|
||||
→ Catalog internally rebuilds
|
||||
```
|
||||
|
||||
### models.dev
|
||||
|
||||
```ts
|
||||
timer fires
|
||||
→ ModelsDevPlugin.refresh()
|
||||
→ ModelsDev.get()
|
||||
→ transform(applyModelsDevToCatalog)
|
||||
→ Catalog internally rebuilds
|
||||
→ commit diff
|
||||
```
|
||||
|
||||
### Auth
|
||||
|
||||
```ts
|
||||
Account.switched(providerID)
|
||||
→ AuthPlugin.refresh()
|
||||
→ transform(applyAuthToCatalog)
|
||||
→ Catalog internally rebuilds
|
||||
→ replay active transforms including current auth
|
||||
→ apply policy
|
||||
→ commit diff
|
||||
```
|
||||
|
||||
### Plugin Activation
|
||||
|
||||
```ts
|
||||
Plugin.activate("acme-models")
|
||||
→ Catalog.transform()
|
||||
→ transform(applyAcmeToCatalog)
|
||||
→ Catalog internally rebuilds
|
||||
→ commit diff
|
||||
```
|
||||
|
||||
### Plugin Disablement
|
||||
|
||||
```ts
|
||||
Plugin.disable("company-naming")
|
||||
→ close plugin scope
|
||||
→ Catalog internally unregisters transform in finalizer
|
||||
→ Catalog internally rebuilds
|
||||
→ sonnet.name = "Sonnet"
|
||||
→ commit diff
|
||||
```
|
||||
|
||||
### Config Edit
|
||||
|
||||
```ts
|
||||
file watcher sees edit
|
||||
→ ConfigProviderAdapter.load()
|
||||
→ transform(applyUpdatedConfigToCatalog)
|
||||
→ Catalog internally rebuilds
|
||||
```
|
||||
|
||||
### Policy
|
||||
|
||||
```ts
|
||||
policy changes
|
||||
→ Catalog rebuild trigger
|
||||
→ replay all active transforms
|
||||
→ apply updated policy last
|
||||
→ commit diff
|
||||
```
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- Disablement, source refresh, and policy re-evaluation are transform replay operations.
|
||||
- Auth does not need to be represented as config.
|
||||
- Config remains one catalog source rather than a catalog dependency.
|
||||
- The API shape matches A, but the mutable draft is catalog state instead of configuration state.
|
||||
- Catalog needs transform ordering and internal rebuild behavior in addition to reads.
|
||||
- Recompute ordering, serialization, and diff events must be specified.
|
||||
- One internal rebuild produces at most one `Catalog.Event.Updated` notification.
|
||||
- Deferred plugin activation avoids blocking readiness and only rebuilds catalog for catalog transform changes.
|
||||
- Debouncing those rebuilds needs an additional batching interface or an activation coordinator that installs multiple transforms before exposing updates.
|
||||
@@ -0,0 +1,394 @@
|
||||
# V2 Config Review
|
||||
|
||||
This document breaks the legacy configuration schema into small review groups. Work through one group at a time and decide whether each field should be ported as-is, removed, or redesigned for v2.
|
||||
|
||||
## Status Labels
|
||||
|
||||
- `pending`: not discussed yet
|
||||
- `keep`: port with substantially the existing meaning
|
||||
- `remove`: do not carry forward
|
||||
- `redesign`: keep the capability with a different shape, scope, or owning module
|
||||
|
||||
## Schema Scope
|
||||
|
||||
Use one v2 config schema for now. Some fields, such as `autoupdate`, are intended for global/user configuration, but there is not yet enough benefit to enforce that with separate global and location schemas. Revisit this if more scope-sensitive fields survive the review.
|
||||
|
||||
## Group 1: File Metadata
|
||||
|
||||
Small fields describing the config file itself rather than application behavior.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| --------- | ---------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------- |
|
||||
| `$schema` | JSON schema reference for editor validation and completion | keep | Keep as read-only metadata; loading config must not insert it or create files for it. |
|
||||
|
||||
## Group 2: Process And Server Settings
|
||||
|
||||
Settings that affect process startup, shell execution, or network serving. Review global-only versus location-specific scope carefully.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------ | --------------------------------------------------- | ------ | ------------------------------------------------------------------------------ |
|
||||
| `shell` | Default shell for terminal and shell tool execution | keep | Port as effective config; shared shell choice is used throughout opencode. |
|
||||
| `logLevel` | Intended logging level configuration | remove | Do not port: no config consumer exists and logging initializes from CLI input. |
|
||||
| `server` | Hostname, port, mDNS, and CORS settings | remove | Do not port: location config is loaded after the server is already running. |
|
||||
| `autoupdate` | Automatic update or notification behavior | keep | Global-only user preference; keep `true`, `false`, and `"notify"`. |
|
||||
|
||||
## Group 3: Commands And Project Resources
|
||||
|
||||
Configuration that introduces location-scoped project resources or discoverable content.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| -------------- | --------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `command` | User-defined commands | remove | Do not port as v2 config; named reusable user workflows belong to skills. |
|
||||
| `skills` | Additional skill locations | redesign | Replace `{ paths?, urls? }` with a single array of local path or remote URL discovery sources. |
|
||||
| `reference` | Named git or local directory references | redesign | Rename to plural `references`; retain named local path and Git repository external-context entries. |
|
||||
| `instructions` | Additional ambient instruction sources | keep | Keep as one array of local paths, glob patterns, or remote URLs supplying automatically included context. |
|
||||
|
||||
V2 does not expose separate user-authored command configuration. Skills should cover named reusable prompt workflows, whether invoked directly by the user or loaded by an agent. Internal command routing and built-in commands may remain runtime concerns without creating a `command` or `commands` config field.
|
||||
|
||||
This intentionally does not port legacy command-only behavior such as per-command `model`, `agent`, `subtask`, prompt shell expansion, or positional/template substitution. If a related capability is needed in v2, it should be designed in the owning domain rather than preserved through a second workflow definition system.
|
||||
|
||||
Keep `skills` as discovery-source configuration rather than inline workflow definitions. Skill content remains owned by `SKILL.md`; each `skills` entry is either a local search root or a remote discovery URL. Direct invocation behavior can be designed separately without expanding the config shape.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"skills": ["./team-skills", "~/shared-skills", "https://example.com/.well-known/skills/"],
|
||||
}
|
||||
```
|
||||
|
||||
Keep ambient instructions separate from skills. Instructions are automatically included as model context, while skills are loaded or invoked intentionally. Each source is unambiguously either a local path/glob or a URL, so v2 keeps the simple array shape:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"instructions": ["CONTRIBUTING.md", "docs/guidelines.md", ".cursor/rules/*.md", "https://example.com/shared-rules.md"],
|
||||
}
|
||||
```
|
||||
|
||||
Keep named external context references as a v2 configuration capability, renamed to plural `references` because it is a collection keyed by alias. References declare local directories or Git repositories that can later be addressed as `@alias` or `@alias/path` when the v2 runtime implements this behavior.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"references": {
|
||||
"design-system": { "path": "../ui-library" },
|
||||
"sdk": { "repository": "github.com/example/sdk", "branch": "main" },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Retain the compact string entry form as well: values starting with `.`, `/`, or `~` represent local paths, and other strings represent Git repositories.
|
||||
|
||||
## Group 4: Plugins
|
||||
|
||||
Plugin loading has source-path and scope-sensitive behavior, so it should be reviewed separately from other project resources.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| -------- | ----------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `plugin` | User-specified plugin modules | redesign | Rename to plural `plugins`; retain ordered loading with package strings or `{ package, options? }` entries. |
|
||||
|
||||
Plugin order remains part of the v2 configuration contract because hook registration and execution can depend on load order. Replace legacy option tuples with readable object entries:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"plugins": [
|
||||
"opencode-helicone-session",
|
||||
{
|
||||
"package": "@my-org/audit-plugin",
|
||||
"options": {
|
||||
"endpoint": "https://audit.example.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
The configured `plugins` list represents package-loaded plugins only. Local plugin code remains discovered from plugin directories such as `.opencode/plugins/`; v2 does not port arbitrary configured local paths or file URLs into this field.
|
||||
|
||||
## Group 5: Filesystem And Tool Runtime
|
||||
|
||||
Settings controlling local file observation, snapshots, language tooling, and tool output behavior.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------- | --------------------------------------- | ------- | ----- |
|
||||
| `watcher` | Ignore patterns for filesystem watching | keep | Keep `{ ignore?: string[] }`; this configures the filesystem watcher subsystem. |
|
||||
| `snapshot` | Enable filesystem snapshot tracking | redesign | Rename to plural `snapshots`; controls creation of snapshots used for undo and revert behavior. |
|
||||
| `formatter` | Configure formatters | keep | Keep singular `boolean \| Record<string, entry>` shape; it configures built-in enablement and named formatter overrides. |
|
||||
| `lsp` | Configure language servers | keep | Keep singular `boolean \| Record<string, entry>` shape; custom servers need commands and file extensions. |
|
||||
| `attachment` | Configure attachment/image processing | redesign | Rename to plural `attachments`; retain `{ image?: { auto_resize?, max_width?, max_height?, max_base64_bytes? } }` for input normalization limits. |
|
||||
| `tool_output` | Configure tool output truncation limits | keep | Keep `{ max_lines?, max_bytes? }`; both positive thresholds apply to saved-preview truncation behavior. |
|
||||
|
||||
`formatter` and `lsp` configure one project tooling subsystem each, so their singular names remain appropriate. `true` enables the built-in registrations, `false` disables them, and a keyed object enables built-ins while applying named overrides or custom registrations. Custom language servers must declare `extensions` so runtime file attachment is deterministic; validation of known built-in server IDs belongs with the eventual v2 LSP integration rather than the aggregate core config schema.
|
||||
|
||||
Rename legacy `attachment` to `attachments` in v2. This setting controls processing for the attachment domain and may expand beyond image handling, while singular `attachment` is already used as a model capability flag indicating whether one model accepts attachments.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"formatter": {
|
||||
"prettier": { "disabled": true },
|
||||
"project": { "command": ["./scripts/format", "$FILE"], "extensions": [".foo"] },
|
||||
},
|
||||
"lsp": {
|
||||
"typescript": { "disabled": true },
|
||||
"project": { "command": ["project-language-server", "--stdio"], "extensions": [".foo"] },
|
||||
},
|
||||
"attachments": {
|
||||
"image": { "auto_resize": true, "max_width": 2000, "max_height": 2000 },
|
||||
},
|
||||
"tool_output": { "max_lines": 2000, "max_bytes": 51200 },
|
||||
}
|
||||
```
|
||||
|
||||
## Group 6: Sharing And Identity
|
||||
|
||||
Settings affecting sharing behavior or user/account identity rather than model execution.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------ | ----------------------------------------------- | ------- | ------------------------------- |
|
||||
| `share` | Session sharing behavior | keep | Keep `"manual" \| "auto" \| "disabled"`; it controls manual sharing permission and automatic sharing of new sessions. |
|
||||
| `autoshare` | Legacy automatic sharing flag | remove | Do not port deprecated alias; use `share: "auto"`. |
|
||||
| `enterprise` | Enterprise URL configuration | keep | Keep `{ url?: string }`; currently selects the legacy sharing service endpoint when no organization account is active. |
|
||||
| `username` | Display username in conversations and telemetry | keep | Keep string identity override; runtime may otherwise resolve an operating-system username. |
|
||||
|
||||
Retain `share` as the single session-sharing setting. `"manual"` permits explicit sharing, `"auto"` shares newly created top-level sessions, and `"disabled"` prevents sharing. Legacy `autoshare: true` is only an alias for `share: "auto"`, so v2 does not expose it.
|
||||
|
||||
Retain `enterprise.url` for legacy enterprise share hosting selection and `username` as a user-facing identity override. These remain separate from server authentication credentials; `username` identifies the user in conversation and telemetry behavior rather than HTTP basic-auth configuration.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"share": "disabled",
|
||||
"enterprise": { "url": "https://share.example.com" },
|
||||
"username": "developer",
|
||||
}
|
||||
```
|
||||
|
||||
## Group 7: Providers And Model Selection
|
||||
|
||||
Provider catalog customization and model-choice configuration. The new core work has started here.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| -------------------- | ------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `provider` | Custom provider configuration and model overrides | redesign | Rename to plural `providers` in v2; do not preserve the legacy singular key. Review nested provider/model fields separately. |
|
||||
| `disabled_providers` | Disable automatically loaded providers | redesign | Replace with `experimental.policies: [{ effect: "deny", action: "provider.use", resource: "..." }]`. |
|
||||
| `enabled_providers` | Restrict enabled providers to an allowlist | redesign | Replace with ordered `provider.use` allow/deny statements and wildcard resources. |
|
||||
| `model` | Default model selection | keep | Keep as the fallback model when an active session or agent does not specify a model. |
|
||||
| `small_model` | Small/utility model selection | remove | Do not port; its only runtime consumer is title generation, which can use an explicit `title` agent model override. |
|
||||
|
||||
Provider selection rules belong in `experimental.policies` rather than provider entries or repeated top-level provider fields. Initial proposed shape:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"experimental": {
|
||||
"policies": [
|
||||
{
|
||||
"effect": "deny",
|
||||
"action": "provider.use",
|
||||
"resource": "*",
|
||||
},
|
||||
{
|
||||
"effect": "allow",
|
||||
"action": "provider.use",
|
||||
"resource": "anthropic",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
See [provider-policy.md](./provider-policy.md) for the provider policy semantics and precedence rules.
|
||||
|
||||
Policy evaluation will consume authored config documents in reverse order while preserving statement order inside each document. The precedence of `.opencode` policy sources remains open until `.opencode` configuration is reviewed.
|
||||
|
||||
Provider configuration uses the plural `providers` key in v2. This intentionally differs from the legacy singular `provider` key; v2 does not add a compatibility alias while its configuration surface is still being defined.
|
||||
|
||||
Keep `model` as the default model fallback. It is application-wide behavior used when an active session or agent has no explicit model selection, so it does not belong inside any individual provider configuration.
|
||||
|
||||
Do not port `small_model`. In the current runtime it is only consulted while generating a session title: the `title` agent model wins first, then `small_model`, then automatic/current-model fallback. In v2, users who need a specific title model should configure the `title` agent directly rather than use a separate top-level model setting.
|
||||
|
||||
Provider, model, variant, and provisional agent `options` are authored as partial patches rather than fully materialized runtime option records. Users should be able to set only the override they need, such as a header or an AI SDK request option; catalog state supplies empty defaults and merges patches in configuration order.
|
||||
|
||||
Keep provider `env` as an authored list of recognized credential environment variable names. Built-in catalog providers already carry this metadata for automatic environment-backed availability, and configured providers may need to declare the same source. For a configured provider this is additive metadata, not a requirement that one of the variables exists: the provider may instead be usable through configured options, a stored account, or an endpoint that needs no credential.
|
||||
|
||||
Within configured models, rename legacy upstream model identifier `id` to `api_id` rather than exposing camelCase runtime `apiID`. Model `limit` is an authored patch, so an override may change only `context`, `input`, or `output`. Model `cost` accepts one simple pricing object or an array of tiered pricing entries; omitted cache prices default to zero.
|
||||
|
||||
Do not port legacy provider model `reasoning`, `temperature`, or `interleaved` flags as first-class config fields; provider/request behavior belongs in structured `options` or model variants. Do not port `release_date`, `status`, `experimental`, `whitelist`, or `blacklist` in this v2 surface.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"providers": {
|
||||
"internal": {
|
||||
"env": ["INTERNAL_LLM_API_KEY"],
|
||||
"options": { "headers": { "Authorization": "Bearer {env:API_KEY}" } },
|
||||
"models": {
|
||||
"chat": {
|
||||
"api_id": "upstream-chat-model",
|
||||
"limit": { "output": 32768 },
|
||||
"cost": { "input": 1.25, "output": 10 },
|
||||
"variants": [
|
||||
{ "id": "high", "aisdk": { "request": { "reasoningEffort": "high" } } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Group 8: Agents And Permissions
|
||||
|
||||
Agent behavior and tool-access policy. Review together because agent configuration can contain permissions and model choices.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| --------------- | --------------------------------------------------- | ------- | ------------------------------------------- |
|
||||
| `default_agent` | Choose default primary agent | remove | Do not retain a separate top-level selector; default choice should be designed with the v2 agent configuration model. |
|
||||
| `mode` | Legacy agent configuration alias | remove | Do not port deprecated alias; configure agents through the v2 agent surface only. |
|
||||
| `agent` | Configure primary, subagent, and specialized agents | redesign | Rename to plural `agents`; retain a named map of built-in overrides and custom agent definitions. |
|
||||
| `permission` | Tool permission rules | redesign | Rename to plural `permissions`; replace legacy map shorthand with an ordered array of `{ permission, pattern, action }` rules. |
|
||||
| `tools` | Legacy tool enable/disable map | remove | Do not port boolean enable/disable alias; express tool access through permissions. |
|
||||
|
||||
Do not port `default_agent` ahead of the v2 agent design. The legacy runtime uses it to choose a visible, non-subagent fallback instead of `build`, but exposing that selection as an isolated top-level field would pre-commit v2 to the legacy agent model before agents and their policy surface are defined together.
|
||||
|
||||
Do not port `mode`. The legacy loader already merges this deprecated alias into `agent`, and v2 should expose only one authoring surface for agent definitions.
|
||||
|
||||
Rename legacy `agent` to `agents` because the setting is a collection keyed by agent name. It should continue to support overriding built-in agents such as `build`, `plan`, and `title`, as well as declaring named custom agents. The nested entry schema remains open until agent-local `permission` and deprecated `tools` behavior are decided.
|
||||
|
||||
Keep nested `agents.<name>.mode` with values `"primary"`, `"subagent"`, or `"all"`. This identifies an agent's runtime role and is separate from the removed top-level legacy `mode` alias, which was an alternate container for agent definitions.
|
||||
|
||||
For named configurable entries across v2, use `disabled?: boolean` consistently when an entry should remain configured but inactive. Agent definitions should therefore redesign legacy `disable` as `disabled`; this matches formatters, language servers, future MCP server definitions, and configured model overrides. Runtime catalog state may still track active availability as `enabled`; that is not user-authored config.
|
||||
|
||||
Keep separate `model` and `variant` fields on agent definitions. A model reference uses `provider/model-id`, but model IDs may themselves contain slash-delimited segments, such as `openrouter/openai/gpt-5`; appending a variant to that string would be ambiguous.
|
||||
|
||||
Keep `color` on agent definitions. Agents are user-visible selectable entities, so a user-authored display color is appropriate metadata for the agent rather than an unrelated application presentation setting. Retain hex colors and named theme colors supported by the existing configuration.
|
||||
|
||||
Keep agent-local `options` provisionally using the same structured provider options shape available on configured providers and models: headers, body, and AI SDK provider/request overrides. Its long-term ownership remains open for team review because reusable provider-specific presets can instead be modeled as variants. Do not retain dedicated agent `temperature` or `top_p` fields.
|
||||
|
||||
Retain `description`, `hidden`, and `steps`; they define an agent's discoverability, visibility, and iteration budget rather than model request parameters. Rename legacy agent `prompt` to `system`, making clear that it supplies persistent system-level agent content without colliding with top-level ambient `instructions`. Remove deprecated `maxSteps` in favor of `steps`.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"agents": {
|
||||
"reviewer": {
|
||||
"model": "openrouter/openai/gpt-5",
|
||||
"variant": "high",
|
||||
"options": {
|
||||
"headers": { "x-agent": "reviewer" },
|
||||
"body": {},
|
||||
"aisdk": { "provider": {}, "request": { "reasoningEffort": "high" } },
|
||||
},
|
||||
"description": "Review changes for correctness",
|
||||
"system": "Find regressions and missing tests.",
|
||||
"mode": "subagent",
|
||||
"color": "warning",
|
||||
"steps": 12,
|
||||
"disabled": false,
|
||||
"permissions": [
|
||||
{ "permission": "edit", "pattern": "*", "action": "deny" },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Do not port `tools`, either as a top-level setting or as an agent-entry alias. The legacy loader already converts tool booleans into permission rules, including collapsing write-adjacent tool names into `edit`; v2 should avoid carrying that lossy compatibility input forward.
|
||||
|
||||
Rename legacy `permission` to `permissions` and expose the normalized ordered ruleset already modeled by `PermissionV2.Ruleset`. Rules retain the interactive `"ask"` action in addition to `"allow"` and `"deny"`; this is distinct from `experimental.policies`, whose provider enforcement currently needs only allow/deny decisions. The same `permissions` ruleset shape should be used inside future `agents` entries.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"permissions": [
|
||||
{ "permission": "bash", "pattern": "*", "action": "ask" },
|
||||
{ "permission": "bash", "pattern": "git status", "action": "allow" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
## Group 9: Integrations
|
||||
|
||||
External protocol and server integration configuration.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ----- | ------------------------------------- | ------- | ----- |
|
||||
| `mcp` | MCP server definitions and enablement | redesign | Keep opencode's explicit local/remote server entry format, nested under `mcp.servers`; use `disabled` for inactive entries and move timeout here. |
|
||||
|
||||
Keep the opencode MCP server entry format instead of adopting the common `mcpServers` copy/paste shape. Local servers remain explicit `type: "local"` entries with command arrays and `environment`; remote servers remain explicit `type: "remote"` entries with `url`, `headers`, and optional `oauth`. Nest the server map under `mcp.servers` so protocol-wide settings such as default timeout can live under the same subsystem.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"mcp": {
|
||||
"timeout": 5000,
|
||||
"servers": {
|
||||
"github": {
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "@github/github-mcp-server"],
|
||||
"environment": { "GITHUB_TOKEN": "{env:GITHUB_TOKEN}" },
|
||||
"disabled": false,
|
||||
"timeout": 10000,
|
||||
},
|
||||
"docs": {
|
||||
"type": "remote",
|
||||
"url": "https://docs.example.com/mcp",
|
||||
"headers": { "Authorization": "Bearer {env:DOCS_TOKEN}" },
|
||||
"oauth": {
|
||||
"client_id": "{env:MCP_CLIENT_ID}",
|
||||
"client_secret": "{env:MCP_CLIENT_SECRET}",
|
||||
"scope": "read write",
|
||||
"callback_port": 19876,
|
||||
"redirect_uri": "http://127.0.0.1:19876/mcp/oauth/callback",
|
||||
},
|
||||
"disabled": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Group 10: Conversation Lifecycle
|
||||
|
||||
Behavior affecting long-running conversations and context management.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------ | ----------------------------------------------------------- | ------- | ----- |
|
||||
| `compaction` | Automatic compaction, pruning, and context reserve settings | redesign | Group retained verbatim history under `keep` and rename context headroom to `buffer`. |
|
||||
|
||||
Retain the compaction capability but redesign the less clear limits. `keep.turns` is the maximum number of recent user turns to preserve verbatim after compaction, and `keep.tokens` is the token budget for those retained turns. `buffer` is the token headroom reserved so automatic compaction triggers before the input window is exhausted.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"compaction": {
|
||||
"auto": true,
|
||||
"prune": true,
|
||||
"keep": {
|
||||
"turns": 2,
|
||||
"tokens": 2000,
|
||||
},
|
||||
"buffer": 10000,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Group 11: Deprecated And Experimental Settings
|
||||
|
||||
Fields that should not be ported by inertia; each needs an explicit justification.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------------------------------ | --------------------------------------- | ------- | ------------------------------------------------------------------- |
|
||||
| `layout` | Legacy layout selection | remove | Do not port deprecated option; stretch layout is always used. |
|
||||
| `experimental.disable_paste_summary` | Disable pasted-content summary behavior | remove | Do not port; pasted-input presentation behavior belongs to the client/UI surface. |
|
||||
| `experimental.batch_tool` | Enable batch tool | remove | Do not port; batch tool is no longer a supported feature. |
|
||||
| `experimental.openTelemetry` | Enable AI SDK telemetry spans | remove | Do not port; observability is process-level and should use standard OpenTelemetry environment or declarative configuration. |
|
||||
| `experimental.primary_tools` | Restrict tools to primary agents | remove | Do not port obsolete gating; agent tool access is configured through permissions. |
|
||||
| `experimental.continue_loop_on_deny` | Continue loop after denied tool call | remove | Do not port legacy denied-tool loop behavior. |
|
||||
| `experimental.mcp_timeout` | MCP request timeout | redesign | Move to `mcp.timeout` for the default and `mcp.servers.<name>.timeout` for per-server overrides. |
|
||||
|
||||
## Review Order
|
||||
|
||||
Work through the groups in this order unless a dependency between decisions becomes clear:
|
||||
|
||||
1. File Metadata
|
||||
2. Process And Server Settings
|
||||
3. Providers And Model Selection
|
||||
4. Commands And Project Resources
|
||||
5. Plugins
|
||||
6. Filesystem And Tool Runtime
|
||||
7. Sharing And Identity
|
||||
8. Agents And Permissions
|
||||
9. Integrations
|
||||
10. Conversation Lifecycle
|
||||
11. Deprecated And Experimental Settings
|
||||
@@ -0,0 +1,291 @@
|
||||
# Policy
|
||||
|
||||
## Purpose
|
||||
|
||||
Policies control whether an operation on a named resource is allowed. They may be authored in configuration files, but policy evaluation is its own runtime concern.
|
||||
|
||||
The first policy consumer is provider availability:
|
||||
|
||||
```text
|
||||
action: provider.use
|
||||
resource: provider ID, such as openai or company-ai
|
||||
```
|
||||
|
||||
Provider configuration and provider policy remain separate:
|
||||
|
||||
- `providers` describes endpoints, options, and model overrides.
|
||||
- `experimental.policies` determines whether an operation using a provider is allowed.
|
||||
|
||||
A provider can be correctly configured and have valid credentials while policy still denies its use.
|
||||
|
||||
## Goals
|
||||
|
||||
- Replace legacy `enabled_providers` and `disabled_providers`.
|
||||
- Keep the default experience unchanged when users specify no policy.
|
||||
- Support wildcard matching for actions and resources.
|
||||
- Provide one small policy vocabulary that can later cover operations such as `plugin.load` or `mcp.connect`.
|
||||
- Let user policy override repository policy, and later allow organization-managed policy to override both.
|
||||
- Keep evaluation simple: matching statements are applied in order and the last match wins.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Policies do not configure endpoints, credentials, models, or provider options.
|
||||
- Policies do not make unusable resources usable.
|
||||
- Policies do not currently provide conditions, principals, approval prompts, or enforced configuration values.
|
||||
- This spec does not define how organization-managed policies are delivered.
|
||||
|
||||
## Statement Shape
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"experimental": {
|
||||
"policies": [
|
||||
{
|
||||
"effect": "deny",
|
||||
"action": "provider.use",
|
||||
"resource": "openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
interface PolicyInfo {
|
||||
effect: "allow" | "deny"
|
||||
action: string
|
||||
resource: string
|
||||
}
|
||||
```
|
||||
|
||||
The `Policy` module owns the shared `Policy.Info` interface, `Policy.Effect` type, and evaluator. Domains define their supported typed statement schemas; for example, `Catalog.ProviderPolicy` fixes `action` to `"provider.use"`. The config schema gathers those domain-defined statement schemas into the accepted `experimental.policies` union because config files are one place statements can be authored while the capability is experimental.
|
||||
|
||||
## Matching
|
||||
|
||||
Both `action` and `resource` use opencode's existing wildcard matching behavior.
|
||||
|
||||
Examples:
|
||||
|
||||
| Action | Resource | Matches |
|
||||
| -------------- | ----------- | ---------------------------------------------------------------------------- |
|
||||
| `provider.use` | `openai` | Only use of provider ID `openai` |
|
||||
| `provider.use` | `company-*` | Use of provider IDs such as `company-us` and `company-eu` |
|
||||
| `provider.*` | `*` | Any provider operation on any provider, if more actions are introduced later |
|
||||
|
||||
No pattern-specific precedence exists. A specific resource does not automatically beat a wildcard resource. Written/evaluation order controls the result.
|
||||
|
||||
## Evaluation
|
||||
|
||||
To evaluate an operation and resource:
|
||||
|
||||
1. Start with `allow`.
|
||||
2. Consider every statement whose `action` and `resource` match the requested action and resource.
|
||||
3. Each matching statement replaces the current decision with its `effect`.
|
||||
4. The last matching statement determines the result.
|
||||
|
||||
Conceptually:
|
||||
|
||||
```ts
|
||||
function evaluate(action: string, resource: string, fallback: Policy.Effect, statements: Policy.Info[]) {
|
||||
return (
|
||||
statements.findLast(
|
||||
(statement) => Wildcard.match(action, statement.action) && Wildcard.match(resource, statement.resource),
|
||||
)?.effect ?? fallback
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Each caller supplies the default effect appropriate for its operation. Catalog provider use supplies `"allow"`, so no provider policy statements means normal behavior continues: otherwise usable providers are allowed.
|
||||
|
||||
## Ordering Within One Config Document
|
||||
|
||||
Statements remain in the order written by the user.
|
||||
|
||||
To deny all providers except Anthropic:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"experimental": {
|
||||
"policies": [
|
||||
{
|
||||
"effect": "deny",
|
||||
"action": "provider.use",
|
||||
"resource": "*",
|
||||
},
|
||||
{
|
||||
"effect": "allow",
|
||||
"action": "provider.use",
|
||||
"resource": "anthropic",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```text
|
||||
provider.use / anthropic -> allow
|
||||
provider.use / openai -> deny
|
||||
```
|
||||
|
||||
To allow internal providers except experimental ones:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"experimental": {
|
||||
"policies": [
|
||||
{ "effect": "deny", "action": "provider.use", "resource": "*" },
|
||||
{ "effect": "allow", "action": "provider.use", "resource": "company-*" },
|
||||
{ "effect": "deny", "action": "provider.use", "resource": "company-experimental-*" },
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```text
|
||||
company-stable: allowed
|
||||
company-experimental-fast: denied
|
||||
openai: denied
|
||||
```
|
||||
|
||||
## Ordering Across Authored Config Documents
|
||||
|
||||
Ordinary settings and policies have different precedence needs:
|
||||
|
||||
- Ordinary settings are read forward, so location-specific settings override user-global settings.
|
||||
- Policies are read by reversing authored config documents, so user-global policy can override repository policy.
|
||||
- Statements inside each document keep their written order.
|
||||
|
||||
At minimum, this means a repository cannot silently re-enable something the user denied globally.
|
||||
|
||||
Project config:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"experimental": {
|
||||
"policies": [{ "effect": "allow", "action": "provider.use", "resource": "openai" }],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
User-global config:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"experimental": {
|
||||
"policies": [{ "effect": "deny", "action": "provider.use", "resource": "openai" }],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```text
|
||||
provider.use / openai -> deny
|
||||
```
|
||||
|
||||
The relative policy precedence of direct project files and `.opencode` files is intentionally deferred until `.opencode` configuration is reviewed.
|
||||
|
||||
## Organization-Managed Policy
|
||||
|
||||
Organization-managed policy is not ordinary authored config. When implemented, managed statements must be appended after the reversed authored statements so they have final authority.
|
||||
|
||||
```text
|
||||
repository policy -> user-global policy -> organization-managed policy
|
||||
```
|
||||
|
||||
Plugins must not be allowed to add, remove, or override policy statements. Plugins can contribute functionality or configured providers; policy determines whether opencode permits an operation through its managed execution paths.
|
||||
|
||||
Provider policy is not a full sandbox for executable plugins. A denied provider must not be usable through the normal provider/model path, but arbitrary plugin code requires separate governance if that becomes a compliance requirement.
|
||||
|
||||
## Interaction With Provider Configuration
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"providers": {
|
||||
"company-ai": {
|
||||
"endpoint": {
|
||||
"type": "openai/responses",
|
||||
"url": "https://ai.company.example/v1/responses",
|
||||
},
|
||||
},
|
||||
},
|
||||
"experimental": {
|
||||
"policies": [
|
||||
{ "effect": "deny", "action": "provider.use", "resource": "*" },
|
||||
{ "effect": "allow", "action": "provider.use", "resource": "company-ai" },
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The provider entry configures `company-ai`; the policy statements make it the only provider permitted for use.
|
||||
|
||||
Provider policy applies regardless of how a provider becomes known or usable, including:
|
||||
|
||||
- models.dev catalog data
|
||||
- environment credentials
|
||||
- saved accounts
|
||||
- built-in provider plugins
|
||||
- explicit provider configuration
|
||||
|
||||
## Applying Provider Policy
|
||||
|
||||
Provider records and model overrides should be assembled before checking provider policy. Otherwise later provider loading could recreate a provider that was already filtered.
|
||||
|
||||
Intended flow:
|
||||
|
||||
1. Build provider/model catalog entries.
|
||||
2. Apply configured provider and model overrides.
|
||||
3. Ask `Policy.Service` to evaluate `provider.use` for each provider ID.
|
||||
4. Prevent denied providers from being selectable or used.
|
||||
|
||||
Whether denied providers are removed entirely or retained as disabled records for diagnostics remains an implementation decision.
|
||||
|
||||
## Legacy Migration
|
||||
|
||||
Legacy deny list:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"disabled_providers": ["openai", "google"],
|
||||
}
|
||||
```
|
||||
|
||||
Equivalent v2 policy:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"experimental": {
|
||||
"policies": [
|
||||
{ "effect": "deny", "action": "provider.use", "resource": "openai" },
|
||||
{ "effect": "deny", "action": "provider.use", "resource": "google" },
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Legacy allowlist:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"enabled_providers": ["anthropic", "openai"],
|
||||
}
|
||||
```
|
||||
|
||||
Equivalent v2 policy:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"experimental": {
|
||||
"policies": [
|
||||
{ "effect": "deny", "action": "provider.use", "resource": "*" },
|
||||
{ "effect": "allow", "action": "provider.use", "resource": "anthropic" },
|
||||
{ "effect": "allow", "action": "provider.use", "resource": "openai" },
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user