From 4cd69df7c80a9013a0b41ef014178adb65ca09fe Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 27 May 2026 22:16:46 -0400 Subject: [PATCH] feat(core): mark policies experimental --- packages/core/src/config/config.ts | 2 +- packages/core/src/config/schema.ts | 6 +- packages/core/test/config/config.test.ts | 8 +- packages/opencode/src/config/config.ts | 6 +- .../opencode/test/provider/provider.test.ts | 6 +- packages/web/src/content/docs/config.mdx | 18 ++-- packages/web/src/content/docs/policies.mdx | 82 +++++++++------- specs/v2/config.md | 30 +++--- specs/v2/provider-policy.md | 98 +++++++++++-------- 9 files changed, 146 insertions(+), 110 deletions(-) diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 273c24679..2afa9a91b 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -82,7 +82,7 @@ export const layer = Layer.effect( const configs = [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()] // Rules use the opposite order so a user-global rule can override a // repository rule. Statement order inside each file stays unchanged. - yield* policy.load(configs.toReversed().flatMap((config) => config.info.policies ?? [])) + yield* policy.load(configs.toReversed().flatMap((config) => config.info.experimental?.policies ?? [])) return Service.of({ directories: Effect.fn("Config.directories")(function* () { diff --git a/packages/core/src/config/schema.ts b/packages/core/src/config/schema.ts index b308bcabf..e69f6161b 100644 --- a/packages/core/src/config/schema.ts +++ b/packages/core/src/config/schema.ts @@ -14,6 +14,10 @@ export class Policy extends Schema.Class("ConfigV2.Policy")({ action: PolicyAction, }) {} +export class Experimental extends Schema.Class("ConfigV2.Experimental")({ + policies: Policy.pipe(Schema.Array, Schema.optional), +}) {} + export class Info extends Schema.Class("ConfigV2.Info")({ $schema: Schema.optional(Schema.String).annotate({ description: "JSON schema reference for configuration validation", @@ -21,7 +25,7 @@ export class Info extends Schema.Class("ConfigV2.Info")({ shell: Schema.String.pipe(Schema.optional).annotate({ description: "Default shell to use for terminal and shell tool execution", }), - policies: Policy.pipe(Schema.Array, Schema.optional), + experimental: Experimental.pipe(Schema.optional), providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional), }) {} diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 9c9063aa5..a376c7821 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -132,7 +132,7 @@ describe("Config", () => { const file = path.join(tmp.path, "opencode.json") const contents = JSON.stringify({ shell: "/bin/zsh", - policies: [{ effect: "deny", action: "provider.use", resource: "openai" }], + experimental: { policies: [{ effect: "deny", action: "provider.use", resource: "openai" }] }, providers: { local: provider }, }) yield* Effect.promise(() => fs.writeFile(file, contents)) @@ -143,7 +143,7 @@ describe("Config", () => { expect(documents[0]?.info.$schema).toBeUndefined() expect(documents[0]?.info.shell).toBe("/bin/zsh") - expect(documents[0]?.info.policies?.[0]).toEqual({ + expect(documents[0]?.info.experimental?.policies?.[0]).toEqual({ effect: "deny", action: "provider.use", resource: "openai", @@ -223,11 +223,11 @@ describe("Config", () => { await fs.mkdir(global, { recursive: true }) await fs.writeFile( path.join(global, "opencode.json"), - JSON.stringify({ policies: [{ effect: "deny", action: "provider.use", resource: "openai" }] }), + JSON.stringify({ experimental: { policies: [{ effect: "deny", action: "provider.use", resource: "openai" }] } }), ) await fs.writeFile( path.join(tmp.path, "opencode.json"), - JSON.stringify({ policies: [{ effect: "allow", action: "provider.use", resource: "openai" }] }), + JSON.stringify({ experimental: { policies: [{ effect: "allow", action: "provider.use", resource: "openai" }] } }), ) }) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index dd8d872c9..0bf23b3c6 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -178,9 +178,6 @@ export const Info = Schema.Struct({ 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", }), - policies: Schema.optional(Schema.mutable(Schema.Array(ConfigV2.Policy))).annotate({ - description: "Policy statements applied to supported resources, such as provider access", - }), model: Schema.optional(ConfigModelID).annotate({ description: "Model to use in the format of provider/model, eg anthropic/claude-2", }), @@ -305,6 +302,9 @@ 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({ + description: "Policy statements applied to supported resources, such as provider access", + }), }), ), }).annotate({ identifier: "Config" }) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 62c25bae9..60ab677b3 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -104,7 +104,9 @@ const alphaProviderConfig = { const denyAnthropicPolicyConfig = { provider: {}, - policies: [{ effect: "deny" as const, action: "provider.use" as const, resource: "anthropic" }], + experimental: { + policies: [{ effect: "deny" as const, action: "provider.use" as const, resource: "anthropic" }], + }, } it.instance("provider loaded from env variable", () => @@ -139,7 +141,7 @@ it.instance( ) it.instance( - "policies deny provider use", + "experimental policies deny provider use", Effect.gen(function* () { yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 4ce56576e..ff9e34867 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -395,18 +395,20 @@ You can also configure [local models](/docs/models#local). [Learn more](/docs/mo ### Policies -Use the `policies` option to allow or deny OpenCode actions on configured resources. Currently, policies can control which providers OpenCode may use. +Use the `experimental.policies` option to allow or deny OpenCode actions on configured resources. Currently, policies can control which providers OpenCode may use. ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", - "policies": [ - { - "effect": "deny", - "action": "provider.use", - "resource": "openai" - } - ] + "experimental": { + "policies": [ + { + "effect": "deny", + "action": "provider.use", + "resource": "openai" + } + ] + } } ``` diff --git a/packages/web/src/content/docs/policies.mdx b/packages/web/src/content/docs/policies.mdx index ff3144235..0f059562e 100644 --- a/packages/web/src/content/docs/policies.mdx +++ b/packages/web/src/content/docs/policies.mdx @@ -3,7 +3,7 @@ title: Policies description: Control which configured resources OpenCode may use. --- -Policies control whether OpenCode may perform an action on a named resource. They are configured with the `policies` array in `opencode.json`. +Policies control whether OpenCode may perform an action on a named resource. This feature is experimental and is configured with the `experimental.policies` array in `opencode.json`. Policies are separate from [permissions](/docs/permissions). Permissions control what tools can do during a session, while policies control whether OpenCode may use a resource such as an LLM provider. @@ -22,13 +22,15 @@ For example, deny use of the `openai` provider: ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", - "policies": [ - { - "effect": "deny", - "action": "provider.use", - "resource": "openai" - } - ] + "experimental": { + "policies": [ + { + "effect": "deny", + "action": "provider.use", + "resource": "openai" + } + ] + } } ``` @@ -55,13 +57,15 @@ The `resource` field supports wildcard matching. Use `*` to match zero or more c ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", - "policies": [ - { - "effect": "deny", - "action": "provider.use", - "resource": "company-*" - } - ] + "experimental": { + "policies": [ + { + "effect": "deny", + "action": "provider.use", + "resource": "company-*" + } + ] + } } ``` @@ -78,18 +82,20 @@ For example, allow only Anthropic: ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", - "policies": [ - { - "effect": "deny", - "action": "provider.use", - "resource": "*" - }, - { - "effect": "allow", - "action": "provider.use", - "resource": "anthropic" - } - ] + "experimental": { + "policies": [ + { + "effect": "deny", + "action": "provider.use", + "resource": "*" + }, + { + "effect": "allow", + "action": "provider.use", + "resource": "anthropic" + } + ] + } } ``` @@ -107,10 +113,12 @@ To replace `disabled_providers`: ```json title="opencode.json" { - "policies": [ - { "effect": "deny", "action": "provider.use", "resource": "openai" }, - { "effect": "deny", "action": "provider.use", "resource": "google" } - ] + "experimental": { + "policies": [ + { "effect": "deny", "action": "provider.use", "resource": "openai" }, + { "effect": "deny", "action": "provider.use", "resource": "google" } + ] + } } ``` @@ -118,10 +126,12 @@ To replace `enabled_providers`, deny all providers first and allow the selected ```json title="opencode.json" { - "policies": [ - { "effect": "deny", "action": "provider.use", "resource": "*" }, - { "effect": "allow", "action": "provider.use", "resource": "anthropic" }, - { "effect": "allow", "action": "provider.use", "resource": "openai" } - ] + "experimental": { + "policies": [ + { "effect": "deny", "action": "provider.use", "resource": "*" }, + { "effect": "allow", "action": "provider.use", "resource": "anthropic" }, + { "effect": "allow", "action": "provider.use", "resource": "openai" } + ] + } } ``` diff --git a/specs/v2/config.md b/specs/v2/config.md index 8db048463..74e896dbd 100644 --- a/specs/v2/config.md +++ b/specs/v2/config.md @@ -82,27 +82,29 @@ Provider catalog customization and model-choice configuration. The new core work | 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 `policies: [{ effect: "deny", action: "provider.use", resource: "..." }]`. | +| `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 | | -Provider selection rules belong in a plural `policies` array rather than provider entries or repeated top-level provider fields. Initial proposed shape: +Provider selection rules belong in `experimental.policies` rather than provider entries or repeated top-level provider fields. Initial proposed shape: ```jsonc { - "policies": [ - { - "effect": "deny", - "action": "provider.use", - "resource": "*", - }, - { - "effect": "allow", - "action": "provider.use", - "resource": "anthropic", - }, - ], + "experimental": { + "policies": [ + { + "effect": "deny", + "action": "provider.use", + "resource": "*", + }, + { + "effect": "allow", + "action": "provider.use", + "resource": "anthropic", + }, + ], + }, } ``` diff --git a/specs/v2/provider-policy.md b/specs/v2/provider-policy.md index afab09e07..6e3fc75ba 100644 --- a/specs/v2/provider-policy.md +++ b/specs/v2/provider-policy.md @@ -14,7 +14,7 @@ resource: provider ID, such as openai or company-ai Provider configuration and provider policy remain separate: - `providers` describes endpoints, options, and model overrides. -- `policies` determines whether an operation using a provider is allowed. +- `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. @@ -38,13 +38,15 @@ A provider can be correctly configured and have valid credentials while policy s ```jsonc { - "policies": [ - { - "effect": "deny", - "action": "provider.use", - "resource": "openai", - }, - ], + "experimental": { + "policies": [ + { + "effect": "deny", + "action": "provider.use", + "resource": "openai", + }, + ], + }, } ``` @@ -56,7 +58,7 @@ interface PolicyInfo { } ``` -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 `policies` union because config files are one place statements can be authored. +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 @@ -103,18 +105,20 @@ To deny all providers except Anthropic: ```jsonc { - "policies": [ - { - "effect": "deny", - "action": "provider.use", - "resource": "*", - }, - { - "effect": "allow", - "action": "provider.use", - "resource": "anthropic", - }, - ], + "experimental": { + "policies": [ + { + "effect": "deny", + "action": "provider.use", + "resource": "*", + }, + { + "effect": "allow", + "action": "provider.use", + "resource": "anthropic", + }, + ], + }, } ``` @@ -129,11 +133,13 @@ To allow internal providers except experimental ones: ```jsonc { - "policies": [ - { "effect": "deny", "action": "provider.use", "resource": "*" }, - { "effect": "allow", "action": "provider.use", "resource": "company-*" }, - { "effect": "deny", "action": "provider.use", "resource": "company-experimental-*" }, - ], + "experimental": { + "policies": [ + { "effect": "deny", "action": "provider.use", "resource": "*" }, + { "effect": "allow", "action": "provider.use", "resource": "company-*" }, + { "effect": "deny", "action": "provider.use", "resource": "company-experimental-*" }, + ], + }, } ``` @@ -159,7 +165,9 @@ Project config: ```jsonc { - "policies": [{ "effect": "allow", "action": "provider.use", "resource": "openai" }], + "experimental": { + "policies": [{ "effect": "allow", "action": "provider.use", "resource": "openai" }], + }, } ``` @@ -167,7 +175,9 @@ User-global config: ```jsonc { - "policies": [{ "effect": "deny", "action": "provider.use", "resource": "openai" }], + "experimental": { + "policies": [{ "effect": "deny", "action": "provider.use", "resource": "openai" }], + }, } ``` @@ -203,10 +213,12 @@ Provider policy is not a full sandbox for executable plugins. A denied provider }, }, }, - "policies": [ - { "effect": "deny", "action": "provider.use", "resource": "*" }, - { "effect": "allow", "action": "provider.use", "resource": "company-ai" }, - ], + "experimental": { + "policies": [ + { "effect": "deny", "action": "provider.use", "resource": "*" }, + { "effect": "allow", "action": "provider.use", "resource": "company-ai" }, + ], + }, } ``` @@ -247,10 +259,12 @@ Equivalent v2 policy: ```jsonc { - "policies": [ - { "effect": "deny", "action": "provider.use", "resource": "openai" }, - { "effect": "deny", "action": "provider.use", "resource": "google" }, - ], + "experimental": { + "policies": [ + { "effect": "deny", "action": "provider.use", "resource": "openai" }, + { "effect": "deny", "action": "provider.use", "resource": "google" }, + ], + }, } ``` @@ -266,10 +280,12 @@ Equivalent v2 policy: ```jsonc { - "policies": [ - { "effect": "deny", "action": "provider.use", "resource": "*" }, - { "effect": "allow", "action": "provider.use", "resource": "anthropic" }, - { "effect": "allow", "action": "provider.use", "resource": "openai" }, - ], + "experimental": { + "policies": [ + { "effect": "deny", "action": "provider.use", "resource": "*" }, + { "effect": "allow", "action": "provider.use", "resource": "anthropic" }, + { "effect": "allow", "action": "provider.use", "resource": "openai" }, + ], + }, } ```