refactor(core): move v1 schemas into core (#30473)

This commit is contained in:
Dax
2026-06-03 02:42:13 +00:00
committed by GitHub
parent 0543fd29c8
commit 83452558f7
129 changed files with 1578 additions and 1227 deletions
+6 -98
View File
@@ -1,110 +1,18 @@
export * as ConfigAgent from "./agent"
import path from "path"
import { Exit, Schema, SchemaGetter } from "effect"
import { PositiveInt } from "@opencode-ai/core/schema"
import { Exit, Schema } from "effect"
import * as Log from "@opencode-ai/core/util/log"
import { Glob } from "@opencode-ai/core/util/glob"
import { ConfigAgentV1 } from "@opencode-ai/core/v1/config/agent"
import { configEntryNameFromPath } from "./entry-name"
import * as ConfigMarkdown from "./markdown"
import { ConfigModelID } from "./model-id"
import { ConfigParse } from "./parse"
import { ConfigPermission } from "./permission"
const log = Log.create({ service: "config" })
const Color = Schema.Union([
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
])
const AgentSchema = Schema.StructWithRest(
Schema.Struct({
model: Schema.optional(ConfigModelID),
variant: Schema.optional(Schema.String).annotate({
description: "Default model variant for this agent (applies only when using the agent's configured model).",
}),
temperature: Schema.optional(Schema.Finite),
top_p: Schema.optional(Schema.Finite),
prompt: Schema.optional(Schema.String),
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({
description: "@deprecated Use 'permission' field instead",
}),
disable: Schema.optional(Schema.Boolean),
description: Schema.optional(Schema.String).annotate({ description: "Description of when to use the agent" }),
mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])),
hidden: Schema.optional(Schema.Boolean).annotate({
description: "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)",
}),
options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
color: Schema.optional(Color).annotate({
description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)",
}),
steps: Schema.optional(PositiveInt).annotate({
description: "Maximum number of agentic iterations before forcing text-only response",
}),
maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }),
permission: Schema.optional(ConfigPermission.Info),
}),
[Schema.Record(Schema.String, Schema.Any)],
)
const KNOWN_KEYS = new Set([
"name",
"model",
"variant",
"prompt",
"description",
"temperature",
"top_p",
"mode",
"hidden",
"color",
"steps",
"maxSteps",
"options",
"permission",
"disable",
"tools",
])
// Post-parse normalisation:
// - Promote any unknown-but-present keys into `options` so they survive the
// round-trip in a well-known field.
// - Translate the deprecated `tools: { name: boolean }` map into the new
// `permission` shape (write-adjacent tools collapse into `permission.edit`).
// - Coalesce `steps ?? maxSteps` so downstream can ignore the deprecated alias.
const normalize = (agent: Schema.Schema.Type<typeof AgentSchema>): Schema.Schema.Type<typeof AgentSchema> => {
const options: Record<string, unknown> = { ...agent.options }
for (const [key, value] of Object.entries(agent)) {
if (!KNOWN_KEYS.has(key)) options[key] = value
}
const permission: ConfigPermission.Info = {}
for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
const action = enabled ? "allow" : "deny"
if (tool === "write" || tool === "edit" || tool === "patch") {
permission.edit = action
continue
}
permission[tool] = action
}
globalThis.Object.assign(permission, agent.permission)
const steps = agent.steps ?? agent.maxSteps
return { ...agent, options, permission, ...(steps !== undefined ? { steps } : {}) }
}
export const Info = AgentSchema.pipe(
Schema.decodeTo(AgentSchema, {
decode: SchemaGetter.transform(normalize),
encode: SchemaGetter.passthrough({ strict: false }),
}),
).annotate({ identifier: "AgentConfig" })
export type Info = Schema.Schema.Type<typeof Info>
export async function load(dir: string) {
const result: Record<string, Info> = {}
const result: Record<string, ConfigAgentV1.Info> = {}
for (const item of await Glob.scan("{agent,agents}/**/*.md", {
cwd: dir,
absolute: true,
@@ -124,13 +32,13 @@ export async function load(dir: string) {
...md.data,
prompt: md.content.trim(),
}
result[config.name] = ConfigParse.schema(Info, config, item)
result[config.name] = ConfigParse.schema(ConfigAgentV1.Info, config, item)
}
return result
}
export async function loadMode(dir: string) {
const result: Record<string, Info> = {}
const result: Record<string, ConfigAgentV1.Info> = {}
for (const item of await Glob.scan("{mode,modes}/*.md", {
cwd: dir,
absolute: true,
@@ -148,7 +56,7 @@ export async function loadMode(dir: string) {
...md.data,
prompt: md.content.trim(),
}
const parsed = Schema.decodeUnknownExit(Info)(config, { errors: "all", propertyOrder: "original" })
const parsed = Schema.decodeUnknownExit(ConfigAgentV1.Info)(config, { errors: "all", propertyOrder: "original" })
if (Exit.isSuccess(parsed)) {
result[config.name] = {
...parsed.value,
@@ -1,25 +0,0 @@
export * as ConfigAttachment from "./attachment"
import { Schema } from "effect"
import { PositiveInt } from "@opencode-ai/core/schema"
export const Image = Schema.Struct({
auto_resize: Schema.optional(Schema.Boolean).annotate({
description: "Resize images before sending them to the model when they exceed configured limits (default: true)",
}),
max_width: Schema.optional(PositiveInt).annotate({
description: "Maximum image width before resizing or rejecting the attachment (default: 2000)",
}),
max_height: Schema.optional(PositiveInt).annotate({
description: "Maximum image height before resizing or rejecting the attachment (default: 2000)",
}),
max_base64_bytes: Schema.optional(PositiveInt).annotate({
description: "Maximum base64 payload bytes for an image attachment (default: 5242880)",
}),
}).annotate({ identifier: "ImageAttachmentConfig" })
export type Image = Schema.Schema.Type<typeof Image>
export const Info = Schema.Struct({
image: Schema.optional(Image).annotate({ description: "Image attachment configuration" }),
}).annotate({ identifier: "AttachmentConfig" })
export type Info = Schema.Schema.Type<typeof Info>
+4 -14
View File
@@ -4,27 +4,17 @@ import path from "path"
import * as Log from "@opencode-ai/core/util/log"
import { Cause, Exit, Schema } from "effect"
import { Glob } from "@opencode-ai/core/util/glob"
import { ConfigCommandV1 } from "@opencode-ai/core/v1/config/command"
import { configEntryNameFromPath } from "./entry-name"
import { InvalidError } from "./error"
import { InvalidError } from "@opencode-ai/core/v1/config/error"
import * as ConfigMarkdown from "./markdown"
import { ConfigModelID } from "./model-id"
const log = Log.create({ service: "config" })
export const Info = Schema.Struct({
template: Schema.String,
description: Schema.optional(Schema.String),
agent: Schema.optional(Schema.String),
model: Schema.optional(ConfigModelID),
subtask: Schema.optional(Schema.Boolean),
})
export type Info = Schema.Schema.Type<typeof Info>
const decodeInfo = Schema.decodeUnknownExit(Info)
const decodeInfo = Schema.decodeUnknownExit(ConfigCommandV1.Info)
export async function load(dir: string) {
const result: Record<string, Info> = {}
const result: Record<string, ConfigCommandV1.Info> = {}
for (const item of await Glob.scan("{command,commands}/**/*.md", {
cwd: dir,
absolute: true,
+13 -221
View File
@@ -6,7 +6,6 @@ import os from "os"
import { mergeDeep } from "remeda"
import { Global } from "@opencode-ai/core/global"
import fsNode from "fs/promises"
import { NamedError } from "@opencode-ai/core/util/error"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Auth } from "../auth"
import { Env } from "../env"
@@ -15,35 +14,25 @@ import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/instal
import { existsSync } from "fs"
import { Account } from "@/account/account"
import { isRecord } from "@/util/record"
import type { ConsoleState } from "./console-state"
import type { ConsoleState } from "@opencode-ai/core/v1/config/console-state"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { InstanceState } from "@/effect/instance-state"
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { containsPath, type InstanceContext } from "../project/instance-context"
import { NonNegativeInt, PositiveInt, type DeepMutable } from "@opencode-ai/core/schema"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { ConfigPermissionV1 } from "@opencode-ai/core/v1/config/permission"
import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
import { ConfigAgent } from "./agent"
import { ConfigAttachment } from "./attachment"
import { ConfigCommand } from "./command"
import { ConfigFormatter } from "./formatter"
import { ConfigLayout } from "./layout"
import { ConfigLSP } from "./lsp"
import { ConfigManaged } from "./managed"
import { ConfigMCP } from "./mcp"
import { ConfigModelID } from "./model-id"
import { ConfigParse } from "./parse"
import { ConfigPaths } from "./paths"
import { ConfigPermission } from "./permission"
import { ConfigPlugin } from "./plugin"
import { ConfigProvider } from "./provider"
import { ConfigReference } from "./reference"
import { ConfigServer } from "./server"
import { ConfigSkills } from "./skills"
import { ConfigVariable } from "./variable"
import { Npm } from "@opencode-ai/core/npm"
import { withTransientReadRetry } from "@/util/effect-http-client"
import { ConfigExperimental } from "@opencode-ai/core/config/experimental"
const log = Log.create({ service: "config" })
@@ -110,12 +99,7 @@ async function substituteWellKnownRemoteConfig(input: {
return { url, headers }
}
const WellKnownConfig = Schema.Struct({
config: Schema.optional(Schema.Json),
remote_config: Schema.optional(Schema.Json),
})
async function resolveLoadedPlugins<T extends { plugin?: ConfigPlugin.Spec[] }>(config: T, filepath: string) {
async function resolveLoadedPlugins<T extends { plugin?: ConfigPluginV1.Spec[] }>(config: T, filepath: string) {
if (!config.plugin) return config
for (let i = 0; i < config.plugin.length; i++) {
// Normalize path-like plugin specs while we still know which config file declared them.
@@ -125,193 +109,7 @@ async function resolveLoadedPlugins<T extends { plugin?: ConfigPlugin.Spec[] }>(
return config
}
export type Layout = ConfigLayout.Layout
const LogLevelRef = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({
identifier: "LogLevel",
description: "Log level",
})
export const Info = Schema.Struct({
$schema: Schema.optional(Schema.String).annotate({
description: "JSON schema reference for configuration validation",
}),
shell: Schema.optional(Schema.String).annotate({
description: "Default shell to use for terminal and bash tool",
}),
logLevel: Schema.optional(LogLevelRef).annotate({ description: "Log level" }),
server: Schema.optional(ConfigServer.Server).annotate({
description: "Server configuration for opencode serve and web commands",
}),
command: Schema.optional(Schema.Record(Schema.String, ConfigCommand.Info)).annotate({
description: "Command configuration, see https://opencode.ai/docs/commands",
}),
skills: Schema.optional(ConfigSkills.Info).annotate({ description: "Additional skill folder paths" }),
reference: Schema.optional(ConfigReference.Info).annotate({
description: "Named git or local directory references that can be mentioned as @alias or @alias/path",
}),
watcher: Schema.optional(
Schema.Struct({
ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
}),
),
snapshot: Schema.optional(Schema.Boolean).annotate({
description:
"Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
}),
// User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged.
plugin: Schema.optional(Schema.mutable(Schema.Array(ConfigPlugin.Spec))),
share: Schema.optional(Schema.Literals(["manual", "auto", "disabled"])).annotate({
description:
"Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
}),
autoshare: Schema.optional(Schema.Boolean).annotate({
description: "@deprecated Use 'share' field instead. Share newly created sessions automatically",
}),
autoupdate: Schema.optional(Schema.Union([Schema.Boolean, Schema.Literal("notify")])).annotate({
description:
"Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
}),
disabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "Disable providers that are loaded automatically",
}),
enabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "When set, ONLY these providers will be enabled. All other providers will be ignored",
}),
model: Schema.optional(ConfigModelID).annotate({
description: "Model to use in the format of provider/model, eg anthropic/claude-2",
}),
small_model: Schema.optional(ConfigModelID).annotate({
description: "Small model to use for tasks like title generation in the format of provider/model",
}),
default_agent: Schema.optional(Schema.String).annotate({
description:
"Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
}),
username: Schema.optional(Schema.String).annotate({
description: "Custom username to display in conversations instead of system username",
}),
mode: Schema.optional(
Schema.StructWithRest(
Schema.Struct({
build: Schema.optional(ConfigAgent.Info),
plan: Schema.optional(ConfigAgent.Info),
}),
[Schema.Record(Schema.String, ConfigAgent.Info)],
),
).annotate({ description: "@deprecated Use `agent` field instead." }),
agent: Schema.optional(
Schema.StructWithRest(
Schema.Struct({
// primary
plan: Schema.optional(ConfigAgent.Info),
build: Schema.optional(ConfigAgent.Info),
// subagent
general: Schema.optional(ConfigAgent.Info),
explore: Schema.optional(ConfigAgent.Info),
// specialized
title: Schema.optional(ConfigAgent.Info),
summary: Schema.optional(ConfigAgent.Info),
compaction: Schema.optional(ConfigAgent.Info),
}),
[Schema.Record(Schema.String, ConfigAgent.Info)],
),
).annotate({ description: "Agent configuration, see https://opencode.ai/docs/agents" }),
provider: Schema.optional(Schema.Record(Schema.String, ConfigProvider.Info)).annotate({
description: "Custom provider configurations and model overrides",
}),
mcp: Schema.optional(
Schema.Record(
Schema.String,
Schema.Union([
ConfigMCP.Info,
// Matches the legacy `{ enabled: false }` form used to disable a server.
Schema.Struct({ enabled: Schema.Boolean }),
]),
),
).annotate({ description: "MCP (Model Context Protocol) server configurations" }),
formatter: Schema.optional(ConfigFormatter.Info).annotate({
description:
"Enable or configure formatters. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides.",
}),
lsp: Schema.optional(ConfigLSP.Info).annotate({
description:
"Enable or configure LSP servers. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides.",
}),
instructions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "Additional instruction files or patterns to include",
}),
layout: Schema.optional(ConfigLayout.Layout).annotate({ description: "@deprecated Always uses stretch layout." }),
permission: Schema.optional(ConfigPermission.Info),
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
attachment: Schema.optional(ConfigAttachment.Info).annotate({
description: "Attachment processing configuration, including image size limits and resizing behavior",
}),
enterprise: Schema.optional(
Schema.Struct({
url: Schema.optional(Schema.String).annotate({ description: "Enterprise URL" }),
}),
),
tool_output: Schema.optional(
Schema.Struct({
max_lines: Schema.optional(PositiveInt).annotate({
description: "Maximum lines of tool output before it is truncated and saved to disk (default: 2000)",
}),
max_bytes: Schema.optional(PositiveInt).annotate({
description: "Maximum bytes of tool output before it is truncated and saved to disk (default: 51200)",
}),
}),
).annotate({
description:
"Thresholds for truncating tool output. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned.",
}),
compaction: Schema.optional(
Schema.Struct({
auto: Schema.optional(Schema.Boolean).annotate({
description: "Enable automatic compaction when context is full (default: true)",
}),
prune: Schema.optional(Schema.Boolean).annotate({
description: "Enable pruning of old tool outputs (default: true)",
}),
tail_turns: Schema.optional(NonNegativeInt).annotate({
description:
"Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2)",
}),
preserve_recent_tokens: Schema.optional(NonNegativeInt).annotate({
description: "Maximum number of tokens from recent turns to preserve verbatim after compaction",
}),
reserved: Schema.optional(NonNegativeInt).annotate({
description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.",
}),
}),
),
experimental: Schema.optional(
Schema.Struct({
disable_paste_summary: Schema.optional(Schema.Boolean),
batch_tool: Schema.optional(Schema.Boolean).annotate({ description: "Enable the batch tool" }),
openTelemetry: Schema.optional(Schema.Boolean).annotate({
description: "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)",
}),
primary_tools: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "Tools that should only be available to primary agents.",
}),
continue_loop_on_deny: Schema.optional(Schema.Boolean).annotate({
description: "Continue the agent loop when a tool call is denied",
}),
mcp_timeout: Schema.optional(PositiveInt).annotate({
description: "Timeout in milliseconds for model context protocol (MCP) requests",
}),
policies: Schema.optional(Schema.mutable(Schema.Array(ConfigExperimental.Policy))).annotate({
description: "Policy statements applied to supported resources, such as provider access",
}),
}),
),
}).annotate({ identifier: "Config" })
// Uses the shared `DeepMutable` from `@opencode-ai/core/schema`. See the definition
// there for why the local variant is needed over `Types.DeepMutable` from
// effect-smol (the upstream version collapses `unknown` to `{}`).
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
type Info = ConfigV1.Info & {
// plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
// with the file and scope it came from so later runtime code can make location-sensitive decisions.
plugin_origins?: ConfigPlugin.Origin[]
@@ -375,12 +173,6 @@ function writableGlobal(info: Info) {
return next
}
export const ConfigDirectoryTypoError = NamedError.create("ConfigDirectoryTypoError", {
path: Schema.String,
dir: Schema.String,
suggestion: Schema.String,
})
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
@@ -424,7 +216,7 @@ export const layer = Layer.effect(
),
)
const parsed = ConfigParse.jsonc(expanded, source)
const data = ConfigParse.schema(Info, normalizeLoadedConfig(parsed, source), source)
const data = ConfigParse.schema(ConfigV1.Info, normalizeLoadedConfig(parsed, source), source)
if (!("path" in options)) return data
yield* Effect.promise(() => resolveLoadedPlugins(data, options.path))
@@ -530,7 +322,7 @@ export const layer = Layer.effect(
source: string,
// mergePluginOrigins receives raw Specs from one config source, before provenance for this merge step
// is attached.
list: ConfigPlugin.Spec[] | undefined,
list: ConfigPluginV1.Spec[] | undefined,
// Scope can be inferred from the source path, but some callers already know whether the config should
// behave as global or local and can pass that explicitly.
kind?: ConfigPlugin.Scope,
@@ -558,7 +350,7 @@ export const layer = Layer.effect(
authEnv[value.key] = value.token
const wellknownURL = `${url}/.well-known/opencode`
log.debug("fetching remote config", { url: wellknownURL })
const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, WellKnownConfig)
const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, ConfigV1.WellKnown)
const remote = yield* Effect.promise(() =>
substituteWellKnownRemoteConfig({
value: wellknown.remote_config,
@@ -753,9 +545,9 @@ export const layer = Layer.effect(
}
if (result.tools) {
const perms: Record<string, ConfigPermission.Action> = {}
const perms: Record<string, ConfigPermissionV1.Action> = {}
for (const [tool, enabled] of Object.entries(result.tools)) {
const action: ConfigPermission.Action = enabled ? "allow" : "deny"
const action: ConfigPermissionV1.Action = enabled ? "allow" : "deny"
if (tool === "write" || tool === "edit" || tool === "patch") {
perms.edit = action
continue
@@ -844,7 +636,7 @@ export const layer = Layer.effect(
let next: Info
let changed: boolean
if (!file.endsWith(".jsonc")) {
const existing = ConfigParse.schema(Info, ConfigParse.jsonc(before, file), file)
const existing = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(before, file), file)
const merged = mergeDeep(writable(existing), patch)
const serialized = JSON.stringify(merged, null, 2)
changed = serialized !== before
@@ -852,7 +644,7 @@ export const layer = Layer.effect(
next = merged
} else {
const updated = patchJsonc(before, patch)
next = ConfigParse.schema(Info, ConfigParse.jsonc(updated, file), file)
next = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(updated, file), file)
changed = updated !== before
if (changed) yield* fs.writeFileString(file, updated).pipe(Effect.orDie)
}
@@ -1,14 +0,0 @@
import { Schema } from "effect"
import { NonNegativeInt } from "@opencode-ai/core/schema"
export class ConsoleState extends Schema.Class<ConsoleState>("ConsoleState")({
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
activeOrgName: Schema.optional(Schema.String),
switchableOrgCount: NonNegativeInt,
}) {}
export const emptyConsoleState: ConsoleState = ConsoleState.make({
consoleManagedProviders: [],
activeOrgName: undefined,
switchableOrgCount: 0,
})
-23
View File
@@ -1,23 +0,0 @@
export * as ConfigError from "./error"
import { NamedError } from "@opencode-ai/core/util/error"
import { Schema } from "effect"
const Issue = Schema.StructWithRest(
Schema.Struct({
message: Schema.String,
path: Schema.Array(Schema.String),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
export const JsonError = NamedError.create("ConfigJsonError", {
path: Schema.String,
message: Schema.optional(Schema.String),
})
export const InvalidError = NamedError.create("ConfigInvalidError", {
path: Schema.String,
issues: Schema.optional(Schema.Array(Issue)),
message: Schema.optional(Schema.String),
})
-13
View File
@@ -1,13 +0,0 @@
export * as ConfigFormatter from "./formatter"
import { Schema } from "effect"
export const Entry = Schema.Struct({
disabled: Schema.optional(Schema.Boolean),
command: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
environment: Schema.optional(Schema.Record(Schema.String, Schema.String)),
extensions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
})
export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)])
export type Info = Schema.Schema.Type<typeof Info>
-6
View File
@@ -1,6 +0,0 @@
import { Schema } from "effect"
export const Layout = Schema.Literals(["auto", "stretch"]).annotate({ identifier: "LayoutConfig" })
export type Layout = Schema.Schema.Type<typeof Layout>
export * as ConfigLayout from "./layout"
-43
View File
@@ -1,43 +0,0 @@
export * as ConfigLSP from "./lsp"
import { Schema } from "effect"
import * as LSPServer from "../lsp/server"
export const Disabled = Schema.Struct({
disabled: Schema.Literal(true),
}).pipe((schema) => schema)
export const Entry = Schema.Union([
Disabled,
Schema.Struct({
command: Schema.mutable(Schema.Array(Schema.String)),
extensions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
disabled: Schema.optional(Schema.Boolean),
env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
initialization: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}),
]).pipe((schema) => schema)
/**
* For custom (non-builtin) LSP server entries, `extensions` is required so the
* client knows which files the server should attach to. Builtin server IDs and
* explicitly disabled entries are exempt.
*/
export const requiresExtensionsForCustomServers = Schema.makeFilter<
boolean | Record<string, Schema.Schema.Type<typeof Entry>>
>((data) => {
if (typeof data === "boolean") return undefined
const serverIds = new Set(Object.values(LSPServer).map((server) => server.id))
const ok = Object.entries(data).every(([id, config]) => {
if ("disabled" in config && config.disabled) return true
if (serverIds.has(id)) return true
return "extensions" in config && Boolean(config.extensions)
})
return ok ? undefined : "For custom LSP servers, 'extensions' array is required."
})
export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)])
.check(requiresExtensionsForCustomServers)
.pipe((schema) => schema)
export type Info = Schema.Schema.Type<typeof Info>
+1 -7
View File
@@ -1,7 +1,6 @@
import { NamedError } from "@opencode-ai/core/util/error"
import matter from "gray-matter"
import { Schema } from "effect"
import { Filesystem } from "@/util/filesystem"
import { FrontmatterError } from "@opencode-ai/core/v1/config/error"
export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g
export const SHELL_REGEX = /!`([^`]+)`/g
@@ -88,9 +87,4 @@ export async function parse(filePath: string) {
}
}
export const FrontmatterError = NamedError.create("ConfigFrontmatterError", {
path: Schema.String,
message: Schema.String,
})
export * as ConfigMarkdown from "./markdown"
-60
View File
@@ -1,60 +0,0 @@
import { Schema } from "effect"
import { PositiveInt } from "@opencode-ai/core/schema"
export const Local = Schema.Struct({
type: Schema.Literal("local").annotate({ description: "Type of MCP server connection" }),
command: Schema.mutable(Schema.Array(Schema.String)).annotate({
description: "Command and arguments to run the MCP server",
}),
environment: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({
description: "Environment variables to set when running the MCP server",
}),
enabled: Schema.optional(Schema.Boolean).annotate({
description: "Enable or disable the MCP server on startup",
}),
timeout: Schema.optional(PositiveInt).annotate({
description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
}),
}).annotate({ identifier: "McpLocalConfig" })
export type Local = Schema.Schema.Type<typeof Local>
export const OAuth = Schema.Struct({
clientId: Schema.optional(Schema.String).annotate({
description: "OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted.",
}),
clientSecret: Schema.optional(Schema.String).annotate({
description: "OAuth client secret (if required by the authorization server)",
}),
scope: Schema.optional(Schema.String).annotate({ description: "OAuth scopes to request during authorization" }),
callbackPort: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))).annotate({
description:
"Port for the local OAuth callback server (default: 19876). Shorthand for redirectUri when only the port needs changing. Ignored if redirectUri is set.",
}),
redirectUri: Schema.optional(Schema.String).annotate({
description: "OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback).",
}),
}).annotate({ identifier: "McpOAuthConfig" })
export type OAuth = Schema.Schema.Type<typeof OAuth>
export const Remote = Schema.Struct({
type: Schema.Literal("remote").annotate({ description: "Type of MCP server connection" }),
url: Schema.String.annotate({ description: "URL of the remote MCP server" }),
enabled: Schema.optional(Schema.Boolean).annotate({
description: "Enable or disable the MCP server on startup",
}),
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({
description: "Headers to send with the request",
}),
oauth: Schema.optional(Schema.Union([OAuth, Schema.Literal(false)])).annotate({
description: "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.",
}),
timeout: Schema.optional(PositiveInt).annotate({
description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
}),
}).annotate({ identifier: "McpRemoteConfig" })
export type Remote = Schema.Schema.Type<typeof Remote>
export const Info = Schema.Union([Local, Remote]).annotate({ discriminator: "type" })
export type Info = Schema.Schema.Type<typeof Info>
export * as ConfigMCP from "./mcp"
-5
View File
@@ -1,5 +0,0 @@
import { Schema } from "effect"
export const ConfigModelID = Schema.String
export type ConfigModelID = Schema.Schema.Type<typeof ConfigModelID>
+1 -1
View File
@@ -3,7 +3,7 @@ export * as ConfigParse from "./parse"
import { type ParseError as JsoncParseError, parse as parseJsoncImpl, printParseErrorCode } from "jsonc-parser"
import { Cause, Exit, Schema as EffectSchema, SchemaIssue } from "effect"
import type { DeepMutable } from "@opencode-ai/core/schema"
import { InvalidError, JsonError } from "./error"
import { InvalidError, JsonError } from "@opencode-ai/core/v1/config/error"
export function jsonc(text: string, filepath: string): unknown {
const errors: JsoncParseError[] = []
@@ -1,56 +0,0 @@
export * as ConfigPermission from "./permission"
import { Schema, SchemaGetter } from "effect"
export const Action = Schema.Literals(["ask", "allow", "deny"]).annotate({ identifier: "PermissionActionConfig" })
export type Action = Schema.Schema.Type<typeof Action>
export const Object = Schema.Record(Schema.String, Action).annotate({ identifier: "PermissionObjectConfig" })
export type Object = Schema.Schema.Type<typeof Object>
export const Rule = Schema.Union([Action, Object]).annotate({ identifier: "PermissionRuleConfig" })
export type Rule = Schema.Schema.Type<typeof Rule>
// Known permission keys get explicit types in the Effect schema for generated
// docs/types. Runtime config parsing uses Effect's `propertyOrder: "original"`
// parse option so user key order is preserved for permission precedence.
const InputObject = Schema.StructWithRest(
Schema.Struct({
read: Schema.optional(Rule),
edit: Schema.optional(Rule),
glob: Schema.optional(Rule),
grep: Schema.optional(Rule),
list: Schema.optional(Rule),
bash: Schema.optional(Rule),
task: Schema.optional(Rule),
external_directory: Schema.optional(Rule),
todowrite: Schema.optional(Action),
question: Schema.optional(Action),
webfetch: Schema.optional(Action),
websearch: Schema.optional(Action),
lsp: Schema.optional(Rule),
doom_loop: Schema.optional(Action),
skill: Schema.optional(Rule),
}),
[Schema.Record(Schema.String, Rule)],
)
// Input the user writes in config: either a single Action (shorthand for "*")
// or an object of per-target rules.
const InputSchema = Schema.Union([Action, InputObject])
// Normalise the Action shorthand into `{ "*": action }`. Object inputs pass
// through untouched.
const normalizeInput = (input: Schema.Schema.Type<typeof InputSchema>): Schema.Schema.Type<typeof InputObject> =>
typeof input === "string" ? { "*": input } : input
export const Info = InputSchema.pipe(
Schema.decodeTo(InputObject, {
decode: SchemaGetter.transform(normalizeInput),
// Not perfectly invertible (we lose whether the user originally typed an
// Action shorthand), but the object form is always a valid representation
// of the same rules.
encode: SchemaGetter.passthrough({ strict: false }),
}),
).annotate({ identifier: "PermissionConfig" })
type _Info = Schema.Schema.Type<typeof InputObject>
export type Info = { -readonly [K in keyof _Info]: _Info[K] }
+6 -14
View File
@@ -1,30 +1,22 @@
import { Glob } from "@opencode-ai/core/util/glob"
import { Schema } from "effect"
import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
import { pathToFileURL } from "url"
import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
import path from "path"
export const Options = Schema.Record(Schema.String, Schema.Unknown)
export type Options = Schema.Schema.Type<typeof Options>
// Spec is the user-config value: either just a plugin identifier, or the identifier plus inline options.
// It answers "what should we load?" but says nothing about where that value came from.
export const Spec = Schema.Union([Schema.String, Schema.mutable(Schema.Tuple([Schema.String, Options]))])
export type Spec = Schema.Schema.Type<typeof Spec>
export type Scope = "global" | "local"
// Origin keeps the original config provenance attached to a spec.
// After multiple config files are merged, callers still need to know which file declared the plugin
// and whether it should behave like a global or project-local plugin.
export type Origin = {
spec: Spec
spec: ConfigPluginV1.Spec
source: string
scope: Scope
}
export async function load(dir: string) {
const plugins: Spec[] = []
const plugins: ConfigPluginV1.Spec[] = []
for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", {
cwd: dir,
@@ -37,17 +29,17 @@ export async function load(dir: string) {
return plugins
}
export function pluginSpecifier(plugin: Spec): string {
export function pluginSpecifier(plugin: ConfigPluginV1.Spec): string {
return Array.isArray(plugin) ? plugin[0] : plugin
}
export function pluginOptions(plugin: Spec): Options | undefined {
export function pluginOptions(plugin: ConfigPluginV1.Spec): ConfigPluginV1.Options | undefined {
return Array.isArray(plugin) ? plugin[1] : undefined
}
// Path-like specs are resolved relative to the config file that declared them so merges later on do not
// accidentally reinterpret `./plugin.ts` relative to some other directory.
export async function resolvePluginSpec(plugin: Spec, configFilepath: string): Promise<Spec> {
export async function resolvePluginSpec(plugin: ConfigPluginV1.Spec, configFilepath: string): Promise<ConfigPluginV1.Spec> {
const spec = pluginSpecifier(plugin)
if (!isPathPluginSpec(spec)) return plugin
-120
View File
@@ -1,120 +0,0 @@
import { Schema } from "effect"
import { PositiveInt } from "@opencode-ai/core/schema"
import { ModelStatus } from "@/provider/model-status"
export const Model = Schema.Struct({
id: Schema.optional(Schema.String),
name: Schema.optional(Schema.String),
family: Schema.optional(Schema.String),
release_date: Schema.optional(Schema.String),
attachment: Schema.optional(Schema.Boolean),
reasoning: Schema.optional(Schema.Boolean),
temperature: Schema.optional(Schema.Boolean),
tool_call: Schema.optional(Schema.Boolean),
interleaved: Schema.optional(
Schema.Union([
Schema.Literal(true),
Schema.Struct({
field: Schema.Literals(["reasoning_content", "reasoning_details"]),
}),
]),
),
cost: Schema.optional(
Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
cache_read: Schema.optional(Schema.Finite),
cache_write: Schema.optional(Schema.Finite),
context_over_200k: Schema.optional(
Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
cache_read: Schema.optional(Schema.Finite),
cache_write: Schema.optional(Schema.Finite),
}),
),
}),
),
limit: Schema.optional(
Schema.Struct({
context: Schema.Finite,
input: Schema.optional(Schema.Finite),
output: Schema.Finite,
}),
),
modalities: Schema.optional(
Schema.Struct({
input: Schema.optional(Schema.mutable(Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])))),
output: Schema.optional(
Schema.mutable(Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"]))),
),
}),
),
experimental: Schema.optional(Schema.Boolean),
status: Schema.optional(ModelStatus),
provider: Schema.optional(
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
),
options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
variants: Schema.optional(
Schema.Record(
Schema.String,
Schema.StructWithRest(
Schema.Struct({
disabled: Schema.optional(Schema.Boolean).annotate({ description: "Disable this variant for the model" }),
}),
[Schema.Record(Schema.String, Schema.Any)],
),
).annotate({ description: "Variant-specific configuration" }),
),
})
export const Info = Schema.Struct({
api: Schema.optional(Schema.String),
name: Schema.optional(Schema.String),
env: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
id: Schema.optional(Schema.String),
npm: Schema.optional(Schema.String),
whitelist: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
blacklist: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
options: Schema.optional(
Schema.StructWithRest(
Schema.Struct({
apiKey: Schema.optional(Schema.String),
baseURL: Schema.optional(Schema.String),
enterpriseUrl: Schema.optional(Schema.String).annotate({
description: "GitHub Enterprise URL for copilot authentication",
}),
setCacheKey: Schema.optional(Schema.Boolean).annotate({
description: "Enable promptCacheKey for this provider (default false)",
}),
timeout: Schema.optional(
Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({
description: "Timeout in milliseconds for full requests to this provider. Set to false to disable timeout.",
}),
).annotate({
description: "Timeout in milliseconds for full requests to this provider. Set to false to disable timeout.",
}),
headerTimeout: Schema.optional(
Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({
description:
"Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.",
}),
).annotate({
description:
"Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.",
}),
chunkTimeout: Schema.optional(PositiveInt).annotate({
description:
"Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
}),
}),
[Schema.Record(Schema.String, Schema.Any)],
),
),
models: Schema.optional(Schema.Record(Schema.String, Model)),
}).annotate({ identifier: "ProviderConfig" })
export type Info = Schema.Schema.Type<typeof Info>
export * as ConfigProvider from "./provider"
+3 -24
View File
@@ -1,27 +1,6 @@
export * as ConfigReference from "./reference"
import { Schema } from "effect"
const Git = Schema.Struct({
repository: Schema.String.annotate({
description: "Git repository URL, host/path reference, or GitHub owner/repo shorthand",
}),
branch: Schema.optional(Schema.String).annotate({
description: "Branch or ref to clone and inspect",
}),
})
const Local = Schema.Struct({
path: Schema.String.annotate({
description: "Absolute path, ~/ path, or workspace-relative path to a local reference directory",
}),
})
export const Entry = Schema.Union([Schema.String, Git, Local]).annotate({ identifier: "ReferenceConfigEntry" })
export type Entry = Schema.Schema.Type<typeof Entry>
export const Info = Schema.Record(Schema.String, Entry).annotate({ identifier: "ReferenceConfig" })
export type Info = Schema.Schema.Type<typeof Info>
import { ConfigReferenceV1 } from "@opencode-ai/core/v1/config/reference"
export type NormalizedEntry =
| {
@@ -47,7 +26,7 @@ export function validateAlias(name: string) {
}
}
export function normalizeEntry(entry: Entry): NormalizedEntry {
export function normalizeEntry(entry: ConfigReferenceV1.Entry): NormalizedEntry {
if (typeof entry === "string") {
if (entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~")) {
return { kind: "local", path: entry }
@@ -59,7 +38,7 @@ export function normalizeEntry(entry: Entry): NormalizedEntry {
return { kind: "git", repository: entry.repository, branch: entry.branch }
}
export function normalize(info: Info): NormalizedInfo {
export function normalize(info: ConfigReferenceV1.Info): NormalizedInfo {
return Object.fromEntries(
Object.entries(info).map(([name, entry]) => {
const aliasError = validateAlias(name)
-19
View File
@@ -1,19 +0,0 @@
import { Schema } from "effect"
import { PositiveInt } from "@opencode-ai/core/schema"
export const Server = Schema.Struct({
port: Schema.optional(PositiveInt).annotate({
description: "Port to listen on",
}),
hostname: Schema.optional(Schema.String).annotate({ description: "Hostname to listen on" }),
mdns: Schema.optional(Schema.Boolean).annotate({ description: "Enable mDNS service discovery" }),
mdnsDomain: Schema.optional(Schema.String).annotate({
description: "Custom domain name for mDNS service (default: opencode.local)",
}),
cors: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "Additional domains to allow for CORS",
}),
}).annotate({ identifier: "ServerConfig" })
export type Server = Schema.Schema.Type<typeof Server>
export * as ConfigServer from "./server"
-14
View File
@@ -1,14 +0,0 @@
import { Schema } from "effect"
export const Info = Schema.Struct({
paths: Schema.optional(Schema.Array(Schema.String)).annotate({
description: "Additional paths to skill folders",
}),
urls: Schema.optional(Schema.Array(Schema.String)).annotate({
description: "URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)",
}),
})
export type Info = Schema.Schema.Type<typeof Info>
export * as ConfigSkills from "./skills"
+1 -1
View File
@@ -3,7 +3,7 @@ export * as ConfigVariable from "./variable"
import path from "path"
import os from "os"
import { Filesystem } from "@/util/filesystem"
import { InvalidError } from "./error"
import { InvalidError } from "@opencode-ai/core/v1/config/error"
type ParseSource =
| {