mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
feat(core): add agents and permissions config schema
This commit is contained in:
@@ -6,8 +6,10 @@ import { Context, Effect, Layer, Option, Schema } from "effect"
|
||||
import { AppFileSystem } from "./filesystem"
|
||||
import { Global } from "./global"
|
||||
import { Location } from "./location"
|
||||
import { PermissionV2 } from "./permission"
|
||||
import { Policy } from "./policy"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { ConfigAgent } from "./config/agent"
|
||||
import { ConfigAttachments } from "./config/attachments"
|
||||
import { ConfigExperimental } from "./config/experimental"
|
||||
import { ConfigFormatter } from "./config/formatter"
|
||||
@@ -42,6 +44,12 @@ export class Info extends Schema.Class<Info>("Config.Info")({
|
||||
username: Schema.String.pipe(Schema.optional).annotate({
|
||||
description: "Username displayed in conversations and used for telemetry identity",
|
||||
}),
|
||||
permissions: PermissionV2.Ruleset.pipe(Schema.optional).annotate({
|
||||
description: "Ordered tool permission rules applied to agent tool use",
|
||||
}),
|
||||
agents: Schema.Record(Schema.String, ConfigAgent.Info).pipe(Schema.optional).annotate({
|
||||
description: "Named built-in agent overrides and custom agent definitions",
|
||||
}),
|
||||
snapshots: Schema.Boolean.pipe(Schema.optional).annotate({
|
||||
description: "Enable snapshots used for undo and revert behavior",
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export * as ConfigAgent from "./agent"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { ProviderV2 } from "../provider"
|
||||
import { PositiveInt } from "../schema"
|
||||
|
||||
export const Color = Schema.Union([
|
||||
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
|
||||
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
|
||||
])
|
||||
|
||||
export class Info extends Schema.Class<Info>("ConfigV2.Agent")({
|
||||
model: Schema.String.pipe(Schema.optional),
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
options: ProviderV2.Options.pipe(Schema.optional),
|
||||
system: Schema.String.pipe(Schema.optional),
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
mode: Schema.Literals(["subagent", "primary", "all"]).pipe(Schema.optional),
|
||||
hidden: Schema.Boolean.pipe(Schema.optional),
|
||||
color: Color.pipe(Schema.optional),
|
||||
steps: PositiveInt.pipe(Schema.optional),
|
||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||
permissions: PermissionV2.Ruleset.pipe(Schema.optional),
|
||||
}) {}
|
||||
@@ -22,7 +22,7 @@ class Model extends Schema.Class<Model>("ConfigV2.Model")({
|
||||
...ProviderV2.Options.fields,
|
||||
}).pipe(Schema.Array, Schema.optional),
|
||||
cost: ModelV2.Cost.pipe(Schema.Array).pipe(Schema.optional),
|
||||
enabled: Schema.Boolean.pipe(Schema.optional),
|
||||
disabled: Schema.Boolean.pipe(Schema.optional),
|
||||
limit: Schema.Struct({
|
||||
context: Schema.Int,
|
||||
input: Schema.Int.pipe(Schema.optional),
|
||||
@@ -110,7 +110,7 @@ export const Plugin = PluginV2.define({
|
||||
cache: { ...cost.cache },
|
||||
}))
|
||||
}
|
||||
if (config.enabled !== undefined) model.enabled = config.enabled
|
||||
if (config.disabled !== undefined) model.enabled = !config.disabled
|
||||
if (config.limit !== undefined) model.limit = { ...config.limit }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -171,6 +171,29 @@ describe("Config", () => {
|
||||
share: "disabled",
|
||||
enterprise: { url: "https://share.example.com" },
|
||||
username: "test-user",
|
||||
permissions: [
|
||||
{ permission: "bash", pattern: "*", action: "ask" },
|
||||
{ permission: "bash", pattern: "git status", action: "allow" },
|
||||
],
|
||||
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.",
|
||||
mode: "subagent",
|
||||
hidden: false,
|
||||
color: "warning",
|
||||
steps: 12,
|
||||
disabled: false,
|
||||
permissions: [{ permission: "edit", pattern: "*", action: "deny" }],
|
||||
},
|
||||
},
|
||||
snapshots: false,
|
||||
watcher: { ignore: ["node_modules/**", "dist/**", ".git"] },
|
||||
formatter: { prettier: { disabled: true }, custom: { command: ["custom-fmt", "$FILE"], extensions: [".foo"] } },
|
||||
@@ -203,6 +226,27 @@ describe("Config", () => {
|
||||
expect(documents[0]?.info.share).toBe("disabled")
|
||||
expect(documents[0]?.info.enterprise).toEqual({ url: "https://share.example.com" })
|
||||
expect(documents[0]?.info.username).toBe("test-user")
|
||||
expect(documents[0]?.info.permissions).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "ask" },
|
||||
{ permission: "bash", pattern: "git status", action: "allow" },
|
||||
])
|
||||
expect(documents[0]?.info.agents?.reviewer).toEqual({
|
||||
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.",
|
||||
mode: "subagent",
|
||||
hidden: false,
|
||||
color: "warning",
|
||||
steps: 12,
|
||||
disabled: false,
|
||||
permissions: [{ permission: "edit", pattern: "*", action: "deny" }],
|
||||
})
|
||||
expect(documents[0]?.info.snapshots).toBe(false)
|
||||
expect(documents[0]?.info.watcher).toEqual({ ignore: ["node_modules/**", "dist/**", ".git"] })
|
||||
expect(documents[0]?.info.formatter).toEqual({
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("ConfigProvider.Plugin", () => {
|
||||
chat: {
|
||||
name: "First",
|
||||
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
||||
enabled: false,
|
||||
disabled: true,
|
||||
limit: { context: 100, output: 50 },
|
||||
options: options({ first: "first", shared: "first" }, "retained"),
|
||||
variants: [
|
||||
|
||||
+61
-5
@@ -208,11 +208,67 @@ Agent behavior and tool-access policy. Review together because agent configurati
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| --------------- | --------------------------------------------------- | ------- | ------------------------------------------- |
|
||||
| `default_agent` | Choose default primary agent | pending | |
|
||||
| `mode` | Legacy agent configuration alias | pending | Deprecated in favor of `agent`. |
|
||||
| `agent` | Configure primary, subagent, and specialized agents | pending | |
|
||||
| `permission` | Tool permission rules | pending | |
|
||||
| `tools` | Legacy tool enable/disable map | pending | Converted to permissions by current loader. |
|
||||
| `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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user