292 lines
8.5 KiB
Markdown
292 lines
8.5 KiB
Markdown
# Policy
|
|
|
|
## Purpose
|
|
|
|
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:
|
|
|
|
```text
|
|
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.
|
|
|
|
## Goals
|
|
|
|
- Replace legacy `enabled_providers` and `disabled_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.load` or `mcp.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
|
|
|
|
```jsonc
|
|
{
|
|
"experimental": {
|
|
"policies": [
|
|
{
|
|
"effect": "deny",
|
|
"action": "provider.use",
|
|
"resource": "openai",
|
|
},
|
|
],
|
|
},
|
|
}
|
|
```
|
|
|
|
```ts
|
|
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.
|
|
|
|
## 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:
|
|
|
|
1. Start with `allow`.
|
|
2. Consider every statement whose `action` and `resource` match the requested action and resource.
|
|
3. Each matching statement replaces the current decision with its `effect`.
|
|
4. The last matching statement determines the result.
|
|
|
|
Conceptually:
|
|
|
|
```ts
|
|
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:
|
|
|
|
```jsonc
|
|
{
|
|
"experimental": {
|
|
"policies": [
|
|
{
|
|
"effect": "deny",
|
|
"action": "provider.use",
|
|
"resource": "*",
|
|
},
|
|
{
|
|
"effect": "allow",
|
|
"action": "provider.use",
|
|
"resource": "anthropic",
|
|
},
|
|
],
|
|
},
|
|
}
|
|
```
|
|
|
|
Result:
|
|
|
|
```text
|
|
provider.use / anthropic -> allow
|
|
provider.use / openai -> deny
|
|
```
|
|
|
|
To allow internal providers except experimental ones:
|
|
|
|
```jsonc
|
|
{
|
|
"experimental": {
|
|
"policies": [
|
|
{ "effect": "deny", "action": "provider.use", "resource": "*" },
|
|
{ "effect": "allow", "action": "provider.use", "resource": "company-*" },
|
|
{ "effect": "deny", "action": "provider.use", "resource": "company-experimental-*" },
|
|
],
|
|
},
|
|
}
|
|
```
|
|
|
|
Result:
|
|
|
|
```text
|
|
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:
|
|
|
|
```jsonc
|
|
{
|
|
"experimental": {
|
|
"policies": [{ "effect": "allow", "action": "provider.use", "resource": "openai" }],
|
|
},
|
|
}
|
|
```
|
|
|
|
User-global config:
|
|
|
|
```jsonc
|
|
{
|
|
"experimental": {
|
|
"policies": [{ "effect": "deny", "action": "provider.use", "resource": "openai" }],
|
|
},
|
|
}
|
|
```
|
|
|
|
Result:
|
|
|
|
```text
|
|
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.
|
|
|
|
```text
|
|
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
|
|
|
|
```jsonc
|
|
{
|
|
"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:
|
|
|
|
1. Build provider/model catalog entries.
|
|
2. Apply configured provider and model overrides.
|
|
3. Ask `Policy.Service` to evaluate `provider.use` for each provider ID.
|
|
4. 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:
|
|
|
|
```jsonc
|
|
{
|
|
"disabled_providers": ["openai", "google"],
|
|
}
|
|
```
|
|
|
|
Equivalent v2 policy:
|
|
|
|
```jsonc
|
|
{
|
|
"experimental": {
|
|
"policies": [
|
|
{ "effect": "deny", "action": "provider.use", "resource": "openai" },
|
|
{ "effect": "deny", "action": "provider.use", "resource": "google" },
|
|
],
|
|
},
|
|
}
|
|
```
|
|
|
|
Legacy allowlist:
|
|
|
|
```jsonc
|
|
{
|
|
"enabled_providers": ["anthropic", "openai"],
|
|
}
|
|
```
|
|
|
|
Equivalent v2 policy:
|
|
|
|
```jsonc
|
|
{
|
|
"experimental": {
|
|
"policies": [
|
|
{ "effect": "deny", "action": "provider.use", "resource": "*" },
|
|
{ "effect": "allow", "action": "provider.use", "resource": "anthropic" },
|
|
{ "effect": "allow", "action": "provider.use", "resource": "openai" },
|
|
],
|
|
},
|
|
}
|
|
```
|