feat(core): add provider policy enforcement
This commit is contained in:
parent
9e556b0f6c
commit
e24b589da1
22 changed files with 946 additions and 22 deletions
169
specs/v2/config.md
Normal file
169
specs/v2/config.md
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# V2 Config Review
|
||||
|
||||
This document breaks the legacy configuration schema into small review groups. Work through one group at a time and decide whether each field should be ported as-is, removed, or redesigned for v2.
|
||||
|
||||
## Status Labels
|
||||
|
||||
- `pending`: not discussed yet
|
||||
- `keep`: port with substantially the existing meaning
|
||||
- `remove`: do not carry forward
|
||||
- `redesign`: keep the capability with a different shape, scope, or owning module
|
||||
|
||||
## Schema Scope
|
||||
|
||||
Use one v2 config schema for now. Some fields, such as `autoupdate`, are intended for global/user configuration, but there is not yet enough benefit to enforce that with separate global and location schemas. Revisit this if more scope-sensitive fields survive the review.
|
||||
|
||||
## Group 1: File Metadata
|
||||
|
||||
Small fields describing the config file itself rather than application behavior.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| --------- | ---------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------- |
|
||||
| `$schema` | JSON schema reference for editor validation and completion | keep | Keep as read-only metadata; loading config must not insert it or create files for it. |
|
||||
|
||||
## Group 2: Process And Server Settings
|
||||
|
||||
Settings that affect process startup, shell execution, or network serving. Review global-only versus location-specific scope carefully.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------ | --------------------------------------------------- | ------ | ------------------------------------------------------------------------------ |
|
||||
| `shell` | Default shell for terminal and shell tool execution | keep | Port as effective config; shared shell choice is used throughout opencode. |
|
||||
| `logLevel` | Intended logging level configuration | remove | Do not port: no config consumer exists and logging initializes from CLI input. |
|
||||
| `server` | Hostname, port, mDNS, and CORS settings | remove | Do not port: location config is loaded after the server is already running. |
|
||||
| `autoupdate` | Automatic update or notification behavior | keep | Global-only user preference; keep `true`, `false`, and `"notify"`. |
|
||||
|
||||
## Group 3: Commands And Project Resources
|
||||
|
||||
Configuration that introduces location-scoped project resources or discoverable content.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| -------------- | --------------------------------------- | ------- | ----- |
|
||||
| `command` | User-defined commands | pending | |
|
||||
| `skills` | Additional skill locations | pending | |
|
||||
| `reference` | Named git or local directory references | pending | |
|
||||
| `instructions` | Additional instruction file patterns | pending | |
|
||||
|
||||
## Group 4: Plugins
|
||||
|
||||
Plugin loading has source-path and scope-sensitive behavior, so it should be reviewed separately from other project resources.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| -------- | ----------------------------- | ------- | ------------------------------------------------------ |
|
||||
| `plugin` | User-specified plugin modules | pending | Existing loader records origin and global/local scope. |
|
||||
|
||||
## Group 5: Filesystem And Tool Runtime
|
||||
|
||||
Settings controlling local file observation, snapshots, language tooling, and tool output behavior.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------- | --------------------------------------- | ------- | ----- |
|
||||
| `watcher` | Ignore patterns for filesystem watching | pending | |
|
||||
| `snapshot` | Enable filesystem snapshot tracking | pending | |
|
||||
| `formatter` | Configure formatters | pending | |
|
||||
| `lsp` | Configure language servers | pending | |
|
||||
| `attachment` | Configure attachment/image processing | pending | |
|
||||
| `tool_output` | Configure tool output truncation limits | pending | |
|
||||
|
||||
## Group 6: Sharing And Identity
|
||||
|
||||
Settings affecting sharing behavior or user/account identity rather than model execution.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------ | ----------------------------------------------- | ------- | ------------------------------- |
|
||||
| `share` | Session sharing behavior | pending | |
|
||||
| `autoshare` | Legacy automatic sharing flag | pending | Deprecated in favor of `share`. |
|
||||
| `enterprise` | Enterprise URL configuration | pending | |
|
||||
| `username` | Display username in conversations and telemetry | pending | |
|
||||
|
||||
## Group 7: Providers And Model Selection
|
||||
|
||||
Provider catalog customization and model-choice configuration. The new core work has started here.
|
||||
|
||||
| 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: "..." }]`. |
|
||||
| `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:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"policies": [
|
||||
{
|
||||
"effect": "deny",
|
||||
"action": "provider.use",
|
||||
"resource": "*",
|
||||
},
|
||||
{
|
||||
"effect": "allow",
|
||||
"action": "provider.use",
|
||||
"resource": "anthropic",
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
See [provider-policy.md](./provider-policy.md) for the provider policy semantics and precedence rules.
|
||||
|
||||
Policy evaluation will consume authored config documents in reverse order while preserving statement order inside each document. The precedence of `.opencode` policy sources remains open until `.opencode` configuration is reviewed.
|
||||
|
||||
## Group 8: Agents And Permissions
|
||||
|
||||
Agent behavior and tool-access policy. Review together because agent configuration can contain permissions and model choices.
|
||||
|
||||
| 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. |
|
||||
|
||||
## Group 9: Integrations
|
||||
|
||||
External protocol and server integration configuration.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ----- | ------------------------------------- | ------- | ----- |
|
||||
| `mcp` | MCP server definitions and enablement | pending | |
|
||||
|
||||
## Group 10: Conversation Lifecycle
|
||||
|
||||
Behavior affecting long-running conversations and context management.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------ | ----------------------------------------------------------- | ------- | ----- |
|
||||
| `compaction` | Automatic compaction, pruning, and context reserve settings | pending | |
|
||||
|
||||
## Group 11: Deprecated And Experimental Settings
|
||||
|
||||
Fields that should not be ported by inertia; each needs an explicit justification.
|
||||
|
||||
| Field | Current Purpose | Status | Notes |
|
||||
| ------------------------------------ | --------------------------------------- | ------- | ------------------------------------------------------------------- |
|
||||
| `layout` | Legacy layout selection | pending | Deprecated; current description says stretch layout is always used. |
|
||||
| `experimental.disable_paste_summary` | Disable pasted-content summary behavior | pending | |
|
||||
| `experimental.batch_tool` | Enable batch tool | pending | |
|
||||
| `experimental.openTelemetry` | Enable AI SDK telemetry spans | pending | |
|
||||
| `experimental.primary_tools` | Restrict tools to primary agents | pending | |
|
||||
| `experimental.continue_loop_on_deny` | Continue loop after denied tool call | pending | |
|
||||
| `experimental.mcp_timeout` | MCP request timeout | pending | May belong with MCP rather than experiments. |
|
||||
|
||||
## Review Order
|
||||
|
||||
Work through the groups in this order unless a dependency between decisions becomes clear:
|
||||
|
||||
1. File Metadata
|
||||
2. Process And Server Settings
|
||||
3. Providers And Model Selection
|
||||
4. Commands And Project Resources
|
||||
5. Plugins
|
||||
6. Filesystem And Tool Runtime
|
||||
7. Sharing And Identity
|
||||
8. Agents And Permissions
|
||||
9. Integrations
|
||||
10. Conversation Lifecycle
|
||||
11. Deprecated And Experimental Settings
|
||||
275
specs/v2/provider-policy.md
Normal file
275
specs/v2/provider-policy.md
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
# 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.
|
||||
- `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
|
||||
{
|
||||
"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 `policies` union because config files are one place statements can be authored.
|
||||
|
||||
## 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
|
||||
{
|
||||
"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
|
||||
{
|
||||
"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
|
||||
{
|
||||
"policies": [{ "effect": "allow", "action": "provider.use", "resource": "openai" }],
|
||||
}
|
||||
```
|
||||
|
||||
User-global config:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"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",
|
||||
},
|
||||
},
|
||||
},
|
||||
"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
|
||||
{
|
||||
"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
|
||||
{
|
||||
"policies": [
|
||||
{ "effect": "deny", "action": "provider.use", "resource": "*" },
|
||||
{ "effect": "allow", "action": "provider.use", "resource": "anthropic" },
|
||||
{ "effect": "allow", "action": "provider.use", "resource": "openai" },
|
||||
],
|
||||
}
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue