Skip to content

feat(core): enforce Console-managed policies - #49729

Merged
adamdotdevin merged 1 commit into
v2from
managed-policy
Sep 21, 2026
Merged

adamdotdevin merged 1 commit into
v2from
managed-policy

Conversation

@adamdotdevin

@adamdotdevin adamdotdevin commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Summary

OpenCode Console compiles per-member Providers and Tools policies into experimental.policies statements and returns them from GET /api/v2/config. The client fetched that response but decoded only providers and websearch, so the statements were dropped and nothing was enforced. This PR makes V2 read them and evaluate them with organization authority.

Changes

Decode and hold the statements

  • RemoteResponse in packages/core/src/plugin/provider/opencode.ts now decodes experimental.policies (ConfigPolicy.Info[]). Omitted experimental and an empty array are equivalent; unknown fields are ignored.
  • New process-global ManagedPolicy service (packages/core/src/managed-policy.ts) holding { statements, organization }. The Console plugin writes it on every snapshot apply; the policy plugin reads it synchronously. Registered in PluginInternal services/requirements.

Evaluate with final authority

  • opencode.config.policy builds one list: reversed authored documents, then organization statements. Same findLast, both actions. Org deny * cannot be lifted by a repo/user allow; org allow lifts a lower-authority deny.
  • Denials from an organization statement read Blocked by <org>'s policy (or Blocked by your organization's policy); authored statements keep Blocked by configuration policy.

Failure semantics (extends #49873 — please confirm)

  • fix(core): retain console config after refresh errors #49873 already retains the last config when the /api/v2/config fetch fails for the same connection. This PR routes the OAuth token refresh failure through the same rule: a Console outage that happens to coincide with a near-expiry access token otherwise still clears the snapshot, which fails closed for managed providers but open for policy. Disconnect, credential switch, and 404 still replace the snapshot. No durable offline cache.
  • load() is restructured so both failure points share one Effect.catch; the upstream retention test passes unchanged through it.

Protect the enforcement path (decision — please confirm)

  • PluginSupervisor.resolve ignores remove operations matching opencode.config.policy or opencode.provider.opencode, whatever the selector (-*, -opencode.*, exact). Otherwise a repository "plugins": ["-opencode.config.policy"] switches off enforcement. The IDs live in PluginInternal.guarded. This narrows -plugin semantics for exactly these two internal IDs.

Housekeeping owed by the permission action

  • The action enum in packages/protocol/openapi.json and the www copies was regenerated upstream by refactor(schema): separate provider and model settings #49850 (client types by chore(client): regenerate policy types #49613), so no generated-file changes remain in this PR.
  • specs/v2/provider-policy.md: permission action, Console delivery contract, authority order, failure rule, supervisor guard.
  • Docs: new services/www/src/docs/content/policies.mdx; linked from config.mdx, permissions.mdx, plugins.mdx, migrate-v1.mdx, and console/index.mdx; added to sidebar navigation.

Tests

  • test/config/policy.test.ts: permission deny/allow/ask semantics, action:resource matching incl. trailing * and compound resources, bare *, org statements evaluated last (deny-all + allow, allow restoring a user deny, org deny beating a project allow with the org-named message), catalog rebuild after replacing statements.
  • test/plugin/provider-opencode.test.ts: end-to-end through the plugin host (Console deny * + allow opencode removes a local anthropic, keeps opencode, unknown fields ignored); statements bound to the connection across poll, unchanged skip, 503 retention, credential switch to another org, 404 clear, and disconnect clear.
  • test/plugin/supervisor.test.ts: -opencode.config.policy/-opencode.provider.opencode/-opencode.config.agent and -* leave the guarded plugins active while other removals still apply.
  • bun run check passes. test/config + test/plugin pass except 7 pre-existing failures in supervisor-reload.test.ts and module.test.ts that fail identically on a clean origin/v2 checkout in this environment (module watcher timeouts).

Verification

Rebased onto origin/v2 at 5848ee0d24 (only conflict: load() in opencode.ts against #49873, resolved as described above). The manual check against a dev Console running the Console repo's policy-types branch (§6 of the handoff) has not been performed in this PR — I did not have a dev Console available. Before merging, someone with one should confirm: a denied local provider disappears from the model list, sudo ls is refused with the organization message while ls still works, a project with "plugins": ["-opencode.config.policy"] changes nothing, and removing the Console policy restores both within a minute.

The Console returns experimental.policies from /api/v2/config, but the
client decoded only providers and websearch, so nothing was enforced.

- Decode the statements and hold them in a process-global ManagedPolicy
  service the Console plugin writes and the policy plugin reads.
- Evaluate organization statements after every reversed authored
  document so they have final authority; name the organization in the
  denial message.
- Keep the last config for the same connection when a fetch or token
  refresh fails, so an outage cannot lift organization policy while
  personal credentials keep working. Switch, disconnect, and 404 still
  replace it.
- Ignore plugin removals for opencode.config.policy and
  opencode.provider.opencode so a repository cannot disable enforcement.
- Cover the permission action in tests and the spec, regenerate the
  OpenAPI enum, and add the V2 policies docs page.
@adamdotdevin
adamdotdevin merged commit 4b9a3d8 into v2 Sep 21, 2026
10 checks passed
@adamdotdevin
adamdotdevin deleted the managed-policy branch September 21, 2026 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant