Policies control whether an operation on a named resource is allowed. They may be authored in configuration files, but policy evaluation is 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:
providers describes endpoints, options, and model overrides.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.
enabled_providers and disabled_providers.plugin.load or mcp.connect.{
"experimental": {
"policies": [
{
"effect": "deny",
"action": "provider.use",
"resource": "openai",
},
],
},
}
interface PolicyInfo {
effect: "allow" | "deny"
action: string
resource: string
}
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.
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.
To evaluate an operation and resource:
allow.action and resource match the requested action and resource.effect.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.
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
Ordinary settings and policies have different precedence needs:
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 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.
{
"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:
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:
Policy.Service to evaluate provider.use for each provider ID.Whether denied providers are removed entirely or retained as disabled records for diagnostics remains an implementation decision.
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" },
],
},
}