mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
feat(core): expand v2 config schema
This commit is contained in:
@@ -3,18 +3,74 @@ export * as Config from "./config"
|
||||
import path from "path"
|
||||
import { type ParseError, parse } from "jsonc-parser"
|
||||
import { Context, Effect, Layer, Option, Schema } from "effect"
|
||||
import { AppFileSystem } from "../filesystem"
|
||||
import { Global } from "../global"
|
||||
import { Location } from "../location"
|
||||
import { Policy } from "../policy"
|
||||
import { AbsolutePath } from "../schema"
|
||||
import { ConfigV2 } from "./schema"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { Global } from "./global"
|
||||
import { Location } from "./location"
|
||||
import { Policy } from "./policy"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { ConfigExperimental } from "./config/experimental"
|
||||
import { ConfigPlugin } from "./config/plugin"
|
||||
import { ConfigProvider } from "./config/provider"
|
||||
import { ConfigReference } from "./config/reference"
|
||||
import { ConfigWatcher } from "./config/watcher"
|
||||
|
||||
export class Info extends Schema.Class<Info>("Config.Info")({
|
||||
$schema: Schema.optional(Schema.String).annotate({
|
||||
description: "JSON schema reference for configuration validation",
|
||||
}),
|
||||
shell: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Default shell to use for terminal and shell tool execution",
|
||||
}),
|
||||
model: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Default model to use when no session or agent model is selected",
|
||||
}),
|
||||
autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")]).pipe(Schema.optional).annotate({
|
||||
description: "Automatically update or notify when a new version is available",
|
||||
}),
|
||||
snapshots: Schema.Boolean.pipe(Schema.optional).annotate({
|
||||
description: "Enable snapshots used for undo and revert behavior",
|
||||
}),
|
||||
watcher: ConfigWatcher.Info.pipe(Schema.optional).annotate({
|
||||
description: "Filesystem watcher configuration",
|
||||
}),
|
||||
skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({
|
||||
description: "Additional paths or URLs to discover skills from",
|
||||
}),
|
||||
instructions: Schema.String.pipe(Schema.Array, Schema.optional).annotate({
|
||||
description: "Additional paths or URLs supplying ambient instructions",
|
||||
}),
|
||||
references: ConfigReference.Info.pipe(Schema.optional).annotate({
|
||||
description: "Named local directories or Git repositories available as external context",
|
||||
}),
|
||||
plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({
|
||||
description: "Ordered external plugin packages to load",
|
||||
}),
|
||||
experimental: ConfigExperimental.Experimental.pipe(Schema.optional),
|
||||
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class FileSource extends Schema.Class<FileSource>("Config.FileSource")({
|
||||
type: Schema.Literal("file"),
|
||||
path: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class MemorySource extends Schema.Class<MemorySource>("Config.MemorySource")({
|
||||
type: Schema.Literal("memory"),
|
||||
}) {}
|
||||
|
||||
export const Source = Schema.Union([FileSource, MemorySource]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Source = typeof Source.Type
|
||||
|
||||
export class Loaded extends Schema.Class<Loaded>("Config.Loaded")({
|
||||
source: Source,
|
||||
info: Info,
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
/** Returns supplemental config directories from lowest to highest priority. */
|
||||
readonly directories: () => Effect.Effect<AbsolutePath[]>
|
||||
/** Loads location config files from lowest to highest priority. */
|
||||
readonly get: () => Effect.Effect<ConfigV2.Loaded[]>
|
||||
readonly get: () => Effect.Effect<Loaded[]>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Config") {}
|
||||
@@ -39,15 +95,15 @@ export const layer = Layer.effect(
|
||||
// Accept legacy fields while v2 is migrated incrementally; recognized
|
||||
// fields still have to satisfy the v2 schema.
|
||||
const info = Option.getOrUndefined(
|
||||
Schema.decodeUnknownOption(ConfigV2.Info)(input, { errors: "all", onExcessProperty: "ignore" }),
|
||||
Schema.decodeUnknownOption(Info)(input, { errors: "all", onExcessProperty: "ignore" }),
|
||||
)
|
||||
if (!info) return
|
||||
return new ConfigV2.Loaded({ source: new ConfigV2.FileSource({ type: "file", path: filepath }), info })
|
||||
return new Loaded({ source: new FileSource({ type: "file", path: filepath }), info })
|
||||
})
|
||||
|
||||
const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) {
|
||||
return yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe(
|
||||
Effect.map((configs) => configs.filter((config): config is ConfigV2.Loaded => config !== undefined)),
|
||||
Effect.map((configs) => configs.filter((config): config is Loaded => config !== undefined)),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -74,7 +130,7 @@ export const layer = Layer.effect(
|
||||
.pipe(Effect.orDie)).toReversed()
|
||||
const direct = yield* Effect.forEach(directPaths, loadFile).pipe(
|
||||
Effect.orDie,
|
||||
Effect.map((configs) => configs.filter((config): config is ConfigV2.Loaded => config !== undefined)),
|
||||
Effect.map((configs) => configs.filter((config): config is Loaded => config !== undefined)),
|
||||
)
|
||||
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
|
||||
// Apply general settings first and more specific settings last:
|
||||
@@ -0,0 +1,18 @@
|
||||
export * as ConfigExperimental from "./experimental"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Policy as PolicyV2 } from "../policy"
|
||||
|
||||
// Each core domain exports the policy actions it supports. Adding an action to
|
||||
// this union makes it valid in authored config while keeping Policy generic.
|
||||
export const PolicyAction = Schema.Union([Catalog.PolicyActions])
|
||||
|
||||
export class Policy extends Schema.Class<Policy>("Config.Experimental.Policy")({
|
||||
...PolicyV2.Info.fields,
|
||||
action: PolicyAction,
|
||||
}) {}
|
||||
|
||||
export class Experimental extends Schema.Class<Experimental>("Config.Experimental")({
|
||||
policies: Policy.pipe(Schema.Array, Schema.optional),
|
||||
}) {}
|
||||
@@ -0,0 +1,13 @@
|
||||
export * as ConfigPlugin from "./plugin"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class Entry extends Schema.Class<Entry>("Config.Plugin.Entry")({
|
||||
package: Schema.String,
|
||||
options: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const Plugin = Schema.Union([Schema.String, Entry])
|
||||
export type Plugin = typeof Plugin.Type
|
||||
|
||||
export const Plugins = Plugin.pipe(Schema.Array)
|
||||
@@ -2,7 +2,7 @@ export * as ConfigProvider from "./provider"
|
||||
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Config } from "./config"
|
||||
import { Config } from "../config"
|
||||
import { ProviderV2 } from "../provider"
|
||||
import { ModelV2 } from "../model"
|
||||
import { PluginV2 } from "../plugin"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export * as ConfigReference from "./reference"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class Git extends Schema.Class<Git>("Config.Reference.Git")({
|
||||
repository: Schema.String,
|
||||
branch: Schema.String.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Local extends Schema.Class<Local>("Config.Reference.Local")({
|
||||
path: Schema.String,
|
||||
}) {}
|
||||
|
||||
export const Entry = Schema.Union([Schema.String, Git, Local])
|
||||
export type Entry = typeof Entry.Type
|
||||
|
||||
export const Info = Schema.Record(Schema.String, Entry)
|
||||
@@ -1,47 +0,0 @@
|
||||
export * as ConfigV2 from "./schema"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Policy as PolicyV2 } from "../policy"
|
||||
import { ConfigProvider } from "./provider"
|
||||
|
||||
// Each core domain exports the policy actions it supports. Adding an action to
|
||||
// this union makes it valid in authored config while keeping Policy generic.
|
||||
export const PolicyAction = Schema.Union([Catalog.PolicyActions])
|
||||
|
||||
export class Policy extends Schema.Class<Policy>("ConfigV2.Policy")({
|
||||
...PolicyV2.Info.fields,
|
||||
action: PolicyAction,
|
||||
}) {}
|
||||
|
||||
export class Experimental extends Schema.Class<Experimental>("ConfigV2.Experimental")({
|
||||
policies: Policy.pipe(Schema.Array, Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigV2.Info")({
|
||||
$schema: Schema.optional(Schema.String).annotate({
|
||||
description: "JSON schema reference for configuration validation",
|
||||
}),
|
||||
shell: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Default shell to use for terminal and shell tool execution",
|
||||
}),
|
||||
experimental: Experimental.pipe(Schema.optional),
|
||||
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class FileSource extends Schema.Class<FileSource>("ConfigV2.FileSource")({
|
||||
type: Schema.Literal("file"),
|
||||
path: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class MemorySource extends Schema.Class<MemorySource>("ConfigV2.MemorySource")({
|
||||
type: Schema.Literal("memory"),
|
||||
}) {}
|
||||
|
||||
export const Source = Schema.Union([FileSource, MemorySource]).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Source = typeof Source.Type
|
||||
|
||||
export class Loaded extends Schema.Class<Loaded>("ConfigV2.Loaded")({
|
||||
source: Source,
|
||||
info: Info,
|
||||
}) {}
|
||||
@@ -0,0 +1,7 @@
|
||||
export * as ConfigWatcher from "./watcher"
|
||||
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class Info extends Schema.Class<Info>("Config.Watcher")({
|
||||
ignore: Schema.String.pipe(Schema.Array, Schema.optional),
|
||||
}) {}
|
||||
@@ -3,7 +3,7 @@ import { Location } from "./location"
|
||||
import { Catalog } from "./catalog"
|
||||
import { PluginBoot } from "./plugin/boot"
|
||||
import { Policy } from "./policy"
|
||||
import { Config } from "./config/config"
|
||||
import { Config } from "./config"
|
||||
|
||||
export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("@opencode/example/LocationServiceMap", {
|
||||
lookup: (ref: Location.Ref) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ export * as PluginBoot from "./boot"
|
||||
import { Context, Deferred, Effect, Layer } from "effect"
|
||||
import { AccountV2 } from "../account"
|
||||
import { Catalog } from "../catalog"
|
||||
import { Config } from "../config/config"
|
||||
import { Config } from "../config"
|
||||
import { ConfigProvider } from "../config/provider"
|
||||
import { EventV2 } from "../event"
|
||||
import { Npm } from "../npm"
|
||||
|
||||
@@ -2,9 +2,8 @@ import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Config } from "@opencode-ai/core/config/config"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { ConfigProvider } from "@opencode-ai/core/config/provider"
|
||||
import { ConfigV2 } from "@opencode-ai/core/config/schema"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
@@ -105,8 +104,8 @@ describe("Config", () => {
|
||||
expect(documents).toHaveLength(3)
|
||||
expect(documents.map((document) => document.source.type)).toEqual(["file", "file", "file"])
|
||||
expect(documents.map((document) => document.info.$schema)).toEqual(["base", "middle", "last"])
|
||||
expect(documents[0]).toBeInstanceOf(ConfigV2.Loaded)
|
||||
expect(documents[0]?.source).toBeInstanceOf(ConfigV2.FileSource)
|
||||
expect(documents[0]).toBeInstanceOf(Config.Loaded)
|
||||
expect(documents[0]?.source).toBeInstanceOf(Config.FileSource)
|
||||
expect(documents[0]?.source.type === "file" ? documents[0].source.path : undefined).toBe(
|
||||
path.join(tmp.path, "config.json"),
|
||||
)
|
||||
@@ -155,7 +154,7 @@ describe("Config", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live("loads recognized v2 fields from config files that still contain legacy fields", () =>
|
||||
it.live("loads supported scalar and resource configuration", () =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||
@@ -168,8 +167,20 @@ describe("Config", () => {
|
||||
JSON.stringify({
|
||||
shell: "/bin/bash",
|
||||
model: "anthropic/claude",
|
||||
disabled_providers: ["openai"],
|
||||
server: { port: 4096 },
|
||||
autoupdate: "notify",
|
||||
snapshots: false,
|
||||
watcher: { ignore: ["node_modules/**", "dist/**", ".git"] },
|
||||
skills: ["./skills", "~/shared-skills", "https://example.com/.well-known/skills/"],
|
||||
instructions: ["CONTRIBUTING.md", ".cursor/rules/*.md", "https://example.com/shared-rules.md"],
|
||||
references: {
|
||||
local: { path: "../library" },
|
||||
sdk: { repository: "github.com/example/sdk", branch: "main" },
|
||||
shorthand: "github.com/example/docs",
|
||||
},
|
||||
plugins: [
|
||||
"opencode-helicone-session",
|
||||
{ package: "@my-org/audit-plugin", options: { endpoint: "https://audit.example.com" } },
|
||||
],
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -180,6 +191,29 @@ describe("Config", () => {
|
||||
|
||||
expect(documents).toHaveLength(1)
|
||||
expect(documents[0]?.info.shell).toBe("/bin/bash")
|
||||
expect(documents[0]?.info.model).toBe("anthropic/claude")
|
||||
expect(documents[0]?.info.autoupdate).toBe("notify")
|
||||
expect(documents[0]?.info.snapshots).toBe(false)
|
||||
expect(documents[0]?.info.watcher).toEqual({ ignore: ["node_modules/**", "dist/**", ".git"] })
|
||||
expect(documents[0]?.info.skills).toEqual([
|
||||
"./skills",
|
||||
"~/shared-skills",
|
||||
"https://example.com/.well-known/skills/",
|
||||
])
|
||||
expect(documents[0]?.info.instructions).toEqual([
|
||||
"CONTRIBUTING.md",
|
||||
".cursor/rules/*.md",
|
||||
"https://example.com/shared-rules.md",
|
||||
])
|
||||
expect(documents[0]?.info.references).toEqual({
|
||||
local: { path: "../library" },
|
||||
sdk: { repository: "github.com/example/sdk", branch: "main" },
|
||||
shorthand: "github.com/example/docs",
|
||||
})
|
||||
expect(documents[0]?.info.plugins).toEqual([
|
||||
"opencode-helicone-session",
|
||||
{ package: "@my-org/audit-plugin", options: { endpoint: "https://audit.example.com" } },
|
||||
])
|
||||
}).pipe(Effect.provide(testLayer(tmp.path)))
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { Config } from "@opencode-ai/core/config/config"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { ConfigProvider } from "@opencode-ai/core/config/provider"
|
||||
import { ConfigV2 } from "@opencode-ai/core/config/schema"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
@@ -21,7 +20,7 @@ function options(headers: Record<string, string>, variant?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const decode = Schema.decodeUnknownSync(ConfigV2.Info)
|
||||
const decode = Schema.decodeUnknownSync(Config.Info)
|
||||
|
||||
describe("ConfigProvider.Plugin", () => {
|
||||
it.effect("loads configured providers and applies later model overrides", () =>
|
||||
@@ -34,8 +33,8 @@ describe("ConfigProvider.Plugin", () => {
|
||||
directories: () => Effect.succeed([]),
|
||||
get: () =>
|
||||
Effect.succeed([
|
||||
new ConfigV2.Loaded({
|
||||
source: new ConfigV2.MemorySource({ type: "memory" }),
|
||||
new Config.Loaded({
|
||||
source: new Config.MemorySource({ type: "memory" }),
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
@@ -63,8 +62,8 @@ describe("ConfigProvider.Plugin", () => {
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new ConfigV2.Loaded({
|
||||
source: new ConfigV2.MemorySource({ type: "memory" }),
|
||||
new Config.Loaded({
|
||||
source: new Config.MemorySource({ type: "memory" }),
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: {
|
||||
@@ -95,8 +94,8 @@ describe("ConfigProvider.Plugin", () => {
|
||||
},
|
||||
}),
|
||||
}),
|
||||
new ConfigV2.Loaded({
|
||||
source: new ConfigV2.MemorySource({ type: "memory" }),
|
||||
new Config.Loaded({
|
||||
source: new Config.MemorySource({ type: "memory" }),
|
||||
info: decode({
|
||||
providers: {
|
||||
custom: { name: "Renamed" },
|
||||
|
||||
@@ -43,7 +43,7 @@ import { ConfigSkills } from "./skills"
|
||||
import { ConfigVariable } from "./variable"
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||
import { ConfigV2 } from "@opencode-ai/core/config/schema"
|
||||
import { ConfigExperimental } from "@opencode-ai/core/config/experimental"
|
||||
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
@@ -302,7 +302,7 @@ export const Info = Schema.Struct({
|
||||
mcp_timeout: Schema.optional(PositiveInt).annotate({
|
||||
description: "Timeout in milliseconds for model context protocol (MCP) requests",
|
||||
}),
|
||||
policies: Schema.optional(Schema.mutable(Schema.Array(ConfigV2.Policy))).annotate({
|
||||
policies: Schema.optional(Schema.mutable(Schema.Array(ConfigExperimental.Policy))).annotate({
|
||||
description: "Policy statements applied to supported resources, such as provider access",
|
||||
}),
|
||||
}),
|
||||
|
||||
+75
-18
@@ -36,20 +36,71 @@ Settings that affect process startup, shell execution, or network serving. Revie
|
||||
|
||||
Configuration that introduces location-scoped project resources or discoverable content.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| -------------- | --------------------------------------- | ------- | ----- |
|
||||
| `command` | User-defined commands | pending | |
|
||||
| `skills` | Additional skill locations | pending | |
|
||||
| `reference` | Named git or local directory references | pending | |
|
||||
| `instructions` | Additional instruction file patterns | pending | |
|
||||
| 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 | pending | Existing loader records origin and global/local scope. |
|
||||
| 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
|
||||
|
||||
@@ -57,8 +108,8 @@ Settings controlling local file observation, snapshots, language tooling, and to
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------- | --------------------------------------- | ------- | ----- |
|
||||
| `watcher` | Ignore patterns for filesystem watching | pending | |
|
||||
| `snapshot` | Enable filesystem snapshot tracking | pending | |
|
||||
| `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 | pending | |
|
||||
| `lsp` | Configure language servers | pending | |
|
||||
| `attachment` | Configure attachment/image processing | pending | |
|
||||
@@ -79,13 +130,13 @@ Settings affecting sharing behavior or user/account identity rather than model e
|
||||
|
||||
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 | pending | New core schema currently uses `providers`; decide public key compatibility. |
|
||||
| `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 | pending | |
|
||||
| `small_model` | Small/utility model selection | pending | |
|
||||
| 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:
|
||||
|
||||
@@ -112,6 +163,12 @@ See [provider-policy.md](./provider-policy.md) for the provider policy semantics
|
||||
|
||||
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.
|
||||
|
||||
## Group 8: Agents And Permissions
|
||||
|
||||
Agent behavior and tool-access policy. Review together because agent configuration can contain permissions and model choices.
|
||||
|
||||
Reference in New Issue
Block a user