8.6 KiB
Provider Policy Proposal
Status: Proposed and unimplemented. This document is design input, not current configuration or runtime behavior.
Purpose
This proposal would let policies control whether an operation on a named resource is allowed. Statements could be authored in configuration files while policy evaluation remained its own runtime concern.
The first policy consumer is provider availability:
action: provider.use
resource: provider ID, such as openai or company-ai
Provider configuration and provider policy remain separate:
providersdescribes endpoints, options, and model overrides.experimental.policiesdetermines whether an operation using a provider is allowed.
A provider can be correctly configured and have valid credentials while policy still denies its use.
Goals
- Replace legacy
enabled_providersanddisabled_providers. - Keep the default experience unchanged when users specify no policy.
- Support wildcard matching for actions and resources.
- Provide one small policy vocabulary that can later cover operations such as
plugin.loadormcp.connect. - Let user policy override repository policy, and later allow organization-managed policy to override both.
- Keep evaluation simple: matching statements are applied in order and the last match wins.
Non-Goals
- Policies do not configure endpoints, credentials, models, or provider options.
- Policies do not make unusable resources usable.
- Policies do not currently provide conditions, principals, approval prompts, or enforced configuration values.
- This spec does not define how organization-managed policies are delivered.
Statement Shape
{
"experimental": {
"policies": [
{
"effect": "deny",
"action": "provider.use",
"resource": "openai",
},
],
},
}
interface PolicyInfo {
effect: "allow" | "deny"
action: string
resource: string
}
The proposed Policy module would own the shared Policy.Info interface, Policy.Effect type, and evaluator. Domains would define their supported typed statement schemas; for example, Catalog.ProviderPolicy would fix action to "provider.use". The config schema could gather those domain-defined statement schemas into an experimental.policies union.
Matching
Both action and resource use opencode's existing wildcard matching behavior.
Examples:
| Action | Resource | Matches |
|---|---|---|
provider.use |
openai |
Only use of provider ID openai |
provider.use |
company-* |
Use of provider IDs such as company-us and company-eu |
provider.* |
* |
Any provider operation on any provider, if more actions are introduced later |
No pattern-specific precedence exists. A specific resource does not automatically beat a wildcard resource. Written/evaluation order controls the result.
Evaluation
To evaluate an operation and resource:
- Start with
allow. - Consider every statement whose
actionandresourcematch the requested action and resource. - Each matching statement replaces the current decision with its
effect. - The last matching statement determines the result.
Conceptually:
function evaluate(action: string, resource: string, fallback: Policy.Effect, statements: Policy.Info[]) {
return (
statements.findLast(
(statement) => Wildcard.match(action, statement.action) && Wildcard.match(resource, statement.resource),
)?.effect ?? fallback
)
}
Each caller supplies the default effect appropriate for its operation. Catalog provider use supplies "allow", so no provider policy statements means normal behavior continues: otherwise usable providers are allowed.
Ordering Within One Config Document
Statements remain in the order written by the user.
To deny all providers except Anthropic:
{
"experimental": {
"policies": [
{
"effect": "deny",
"action": "provider.use",
"resource": "*",
},
{
"effect": "allow",
"action": "provider.use",
"resource": "anthropic",
},
],
},
}
Result:
provider.use / anthropic -> allow
provider.use / openai -> deny
To allow internal providers except experimental ones:
{
"experimental": {
"policies": [
{ "effect": "deny", "action": "provider.use", "resource": "*" },
{ "effect": "allow", "action": "provider.use", "resource": "company-*" },
{ "effect": "deny", "action": "provider.use", "resource": "company-experimental-*" },
],
},
}
Result:
company-stable: allowed
company-experimental-fast: denied
openai: denied
Ordering Across Authored Config Documents
Ordinary settings and policies have different precedence needs:
- Ordinary settings are read forward, so location-specific settings override user-global settings.
- Policies are read by reversing authored config documents, so user-global policy can override repository policy.
- Statements inside each document keep their written order.
At minimum, this means a repository cannot silently re-enable something the user denied globally.
Project config:
{
"experimental": {
"policies": [{ "effect": "allow", "action": "provider.use", "resource": "openai" }],
},
}
User-global config:
{
"experimental": {
"policies": [{ "effect": "deny", "action": "provider.use", "resource": "openai" }],
},
}
Result:
provider.use / openai -> deny
The relative policy precedence of direct project files and .opencode files is intentionally deferred until .opencode configuration is reviewed.
Organization-Managed Policy
Organization-managed policy is not ordinary authored config. When implemented, managed statements must be appended after the reversed authored statements so they have final authority.
repository policy -> user-global policy -> organization-managed policy
Plugins must not be allowed to add, remove, or override policy statements. Plugins can contribute functionality or configured providers; policy determines whether opencode permits an operation through its managed execution paths.
Provider policy is not a full sandbox for executable plugins. A denied provider must not be usable through the normal provider/model path, but arbitrary plugin code requires separate governance if that becomes a compliance requirement.
Interaction With Provider Configuration
{
"providers": {
"company-ai": {
"endpoint": {
"type": "openai/responses",
"url": "https://ai.company.example/v1/responses",
},
},
},
"experimental": {
"policies": [
{ "effect": "deny", "action": "provider.use", "resource": "*" },
{ "effect": "allow", "action": "provider.use", "resource": "company-ai" },
],
},
}
The provider entry configures company-ai; the policy statements make it the only provider permitted for use.
Provider policy applies regardless of how a provider becomes known or usable, including:
- models.dev catalog data
- environment credentials
- saved accounts
- built-in provider plugins
- explicit provider configuration
Applying Provider Policy
Provider records and model overrides should be assembled before checking provider policy. Otherwise later provider loading could recreate a provider that was already filtered.
Intended flow:
- Build provider/model catalog entries.
- Apply configured provider and model overrides.
- Ask
Policy.Serviceto evaluateprovider.usefor each provider ID. - Prevent denied providers from being selectable or used.
Whether denied providers are removed entirely or retained as disabled records for diagnostics remains an implementation decision.
Legacy Migration
Legacy deny list:
{
"disabled_providers": ["openai", "google"],
}
Equivalent v2 policy:
{
"experimental": {
"policies": [
{ "effect": "deny", "action": "provider.use", "resource": "openai" },
{ "effect": "deny", "action": "provider.use", "resource": "google" },
],
},
}
Legacy allowlist:
{
"enabled_providers": ["anthropic", "openai"],
}
Equivalent v2 policy:
{
"experimental": {
"policies": [
{ "effect": "deny", "action": "provider.use", "resource": "*" },
{ "effect": "allow", "action": "provider.use", "resource": "anthropic" },
{ "effect": "allow", "action": "provider.use", "resource": "openai" },
],
},
}