mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 12:41:37 +03:00
fix(core): give managed configuration highest precedence (#55267)
This commit is contained in:
committed by
GitHub
Unverified
parent
6a7cedcbf1
commit
6dc4d4e758
@@ -18,6 +18,7 @@ import { ConfigNormalize } from "./config/normalize.js"
|
||||
import { ConfigDiscovery } from "./config/discovery.js"
|
||||
import { ConfigWatch } from "./config/watch.js"
|
||||
import { WellKnown } from "./wellknown.js"
|
||||
import { ConfigExperimental } from "@opencode/schema/config/experimental"
|
||||
import { AppProcess } from "@opencode/util/process"
|
||||
import { ChildProcess } from "effect/process"
|
||||
|
||||
@@ -241,16 +242,41 @@ export const layer = (options?: Options) =>
|
||||
)
|
||||
: []
|
||||
|
||||
// System entries sit at lowest priority so users and projects can
|
||||
// override ordinary defaults while reversed policy evaluation gives
|
||||
// system policies highest file-based authority. Global entries sit
|
||||
// below explicit and direct files; project directories rank above them.
|
||||
// MDM preferences rank below the system directory so their policies
|
||||
// carry the most file-based authority, matching V1 where they won.
|
||||
const systemSupplementary = [
|
||||
...(sources.preferences ? yield* loadPreferences(sources.preferences).pipe(Effect.orDie) : []),
|
||||
// Managed settings load last so they override everything, and MDM
|
||||
// preferences outrank the system directory as in V1. Policies evaluate
|
||||
// in reverse load order, so managed policies split off and load first
|
||||
// to keep final file-based authority.
|
||||
const managed = [
|
||||
...(sources.system?.present ? yield* loadDirectory(sources.system.path).pipe(Effect.orDie) : []),
|
||||
...(sources.preferences ? yield* loadPreferences(sources.preferences).pipe(Effect.orDie) : []),
|
||||
]
|
||||
const managedPolicies = managed
|
||||
.flatMap((entry) =>
|
||||
entry.type === "document" && entry.info.experimental?.policies
|
||||
? [
|
||||
new Document({
|
||||
type: "document",
|
||||
path: entry.path,
|
||||
info: new Info({
|
||||
experimental: new ConfigExperimental.Info({ policies: entry.info.experimental.policies }),
|
||||
}),
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
)
|
||||
.toReversed()
|
||||
const managedSettings = managed.map((entry) =>
|
||||
entry.type === "document" && entry.info.experimental?.policies
|
||||
? new Document({
|
||||
...entry,
|
||||
info: new Info({
|
||||
...entry.info,
|
||||
experimental: new ConfigExperimental.Info({ ...entry.info.experimental, policies: undefined }),
|
||||
}),
|
||||
})
|
||||
: entry,
|
||||
)
|
||||
// Global entries sit below explicit and direct files; project directories rank above them.
|
||||
const globalSupplementary = sources.global ? yield* loadDirectory(sources.global).pipe(Effect.orDie) : []
|
||||
const projectSupplementary = yield* Effect.forEach(
|
||||
sources.project.filter((root) => root.present),
|
||||
@@ -260,13 +286,14 @@ export const layer = (options?: Options) =>
|
||||
Effect.map((entries) => entries.flat()),
|
||||
)
|
||||
return [
|
||||
...systemSupplementary,
|
||||
...managedPolicies,
|
||||
...(yield* loadWellknown().pipe(Effect.orDie)),
|
||||
...globalSupplementary,
|
||||
...explicit,
|
||||
...direct,
|
||||
...projectSupplementary,
|
||||
...content,
|
||||
...managedSettings,
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Provider } from "@opencode/core/provider"
|
||||
import { AbsolutePath } from "@opencode/core/schema"
|
||||
import { WellKnown } from "@opencode/core/wellknown"
|
||||
import { KV } from "@opencode/core/kv"
|
||||
import { ManagedPolicy } from "@opencode/core/managed-policy"
|
||||
import { Integration } from "@opencode/schema/integration"
|
||||
import { emptyCredentialNode, emptyWellknownNode } from "../fixture/config-nodes"
|
||||
import { location } from "../fixture/location"
|
||||
@@ -77,6 +78,8 @@ function testLayer(
|
||||
return Layer.mergeAll(built, watcher)
|
||||
}
|
||||
|
||||
const tool = (effect: "allow" | "deny") => ({ action: "tool.use", resource: "shell:*", effect })
|
||||
|
||||
const provider = {
|
||||
package: "native",
|
||||
settings: {},
|
||||
@@ -202,7 +205,7 @@ describe("Config", () => {
|
||||
),
|
||||
)
|
||||
|
||||
it.live("loads system configuration at lowest priority", () =>
|
||||
it.live("loads system configuration at highest priority", () =>
|
||||
Effect.acquireDisposable(Effect.promise(() => tmpdir())).pipe(
|
||||
Effect.flatMap((tmp) => {
|
||||
const system = path.join(tmp.path, "system")
|
||||
@@ -214,10 +217,13 @@ describe("Config", () => {
|
||||
await fs.mkdir(project, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(system, "opencode.json"),
|
||||
JSON.stringify({ shell: "system", username: "system-user" }),
|
||||
JSON.stringify({ shell: "system", username: "system-user", experimental: { policies: [tool("deny")] } }),
|
||||
)
|
||||
await fs.writeFile(path.join(global, "opencode.json"), JSON.stringify({ shell: "global" }))
|
||||
await fs.writeFile(path.join(project, "opencode.json"), JSON.stringify({ shell: "project" }))
|
||||
await fs.writeFile(
|
||||
path.join(project, "opencode.json"),
|
||||
JSON.stringify({ shell: "project", experimental: { policies: [tool("allow")] } }),
|
||||
)
|
||||
}).pipe(
|
||||
Effect.andThen(
|
||||
Effect.gen(function* () {
|
||||
@@ -225,13 +231,16 @@ describe("Config", () => {
|
||||
const entries = yield* config.entries()
|
||||
expect(
|
||||
entries.flatMap((entry) => (entry.type === "document" && entry.info.shell ? [entry.info.shell] : [])),
|
||||
).toEqual(["system", "global", "project"])
|
||||
).toEqual(["global", "project", "system"])
|
||||
expect(entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))).toEqual([
|
||||
AbsolutePath.make(system),
|
||||
AbsolutePath.make(global),
|
||||
AbsolutePath.make(system),
|
||||
])
|
||||
expect(Config.latest(entries, "shell")).toBe("project")
|
||||
expect(Config.latest(entries, "shell")).toBe("system")
|
||||
expect(Config.latest(entries, "username")).toBe("system-user")
|
||||
expect(
|
||||
ManagedPolicy.decision(ManagedPolicy.statements(entries, { statements: [] }), "tool.use", "shell:ls"),
|
||||
).toBe("deny")
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
testLayer(project, global, project, undefined, undefined, emptyCredentialNode, emptyWellknownNode, {
|
||||
|
||||
@@ -27,7 +27,7 @@ OpenCode supports JSON and JSONC. Use JSONC when you want comments or trailing c
|
||||
|
||||
## Locations
|
||||
|
||||
Put machine-wide defaults in the system configuration directory:
|
||||
Put managed, machine-wide settings in the system configuration directory:
|
||||
|
||||
| Platform | Directory |
|
||||
| -------- | --------------------------------------- |
|
||||
@@ -56,9 +56,11 @@ machine-wide one:
|
||||
/Library/Managed Preferences/ai.opencode.managed.plist
|
||||
```
|
||||
|
||||
Managed preferences load just below the system directory, so their
|
||||
[policies](/policies#precedence) have the most authority of any file. OpenCode
|
||||
reads them at startup and whenever another config change triggers a reload.
|
||||
Managed settings have the highest precedence, so users and projects cannot
|
||||
override the values they set, and managed preferences override the system
|
||||
directory. Settings alone cannot stop users from adding providers, MCP servers,
|
||||
agents, or plugins; use [policies](/policies#precedence) in managed
|
||||
configuration to enforce restrictions.
|
||||
|
||||
Put user settings for every project in the global configuration:
|
||||
|
||||
@@ -95,15 +97,16 @@ For example, start OpenCode from `/home/user/projects/acme/packages/web`:
|
||||
|
||||
OpenCode applies these files from lowest to highest precedence:
|
||||
|
||||
1. `/etc/opencode/opencode.json`
|
||||
2. `~/.config/opencode/opencode.json`
|
||||
3. `/home/user/projects/acme/opencode.json`
|
||||
4. `/home/user/projects/acme/packages/web/opencode.json`
|
||||
1. `~/.config/opencode/opencode.json`
|
||||
2. `/home/user/projects/acme/opencode.json`
|
||||
3. `/home/user/projects/acme/packages/web/opencode.json`
|
||||
4. `/etc/opencode/opencode.json`
|
||||
|
||||
The package config overrides matching settings from the repository config,
|
||||
which overrides the global and system configs. Settings that do not conflict
|
||||
are preserved, and [policies](/policies#precedence) evaluate in the reverse
|
||||
order so system policies outrank global and project policies.
|
||||
which overrides the global config. The system config overrides all of them.
|
||||
Settings that do not conflict are preserved. [Policies](/policies#precedence)
|
||||
from global and project files evaluate in the reverse order, and system
|
||||
policies outrank all of them.
|
||||
|
||||
## Schema
|
||||
|
||||
|
||||
@@ -65,11 +65,11 @@ V2 reads these same locations. It normalizes supported V1 and native V2 fields i
|
||||
file. Existing supported V1 configuration is intended to keep working, so you do not need to convert it to try or adopt
|
||||
V2.
|
||||
|
||||
In V2, the system configuration directory loads at the lowest precedence for ordinary settings so it provides
|
||||
machine-wide defaults, while [policies](/policies#precedence) evaluate in the reverse order so system
|
||||
`experimental.policies` (including migrated `enabled_providers` and `disabled_providers`) cannot be overridden by global
|
||||
or project files. Express admin-enforced restrictions as `experimental.policies` rather than ordinary settings such as
|
||||
`permissions`.
|
||||
As in V1, system configuration and macOS managed preferences have the highest precedence, so their settings override
|
||||
global and project files. Their [policies](/policies#precedence), including migrated `enabled_providers` and
|
||||
`disabled_providers`, also outrank global and project policies. Overriding a setting cannot stop users from adding
|
||||
providers, MCP servers, agents, or plugins, so express admin-enforced restrictions as `experimental.policies` rather
|
||||
than ordinary settings such as `permissions`.
|
||||
|
||||
### Ask OpenCode to migrate
|
||||
|
||||
|
||||
@@ -181,7 +181,8 @@ it was configured.
|
||||
## Precedence
|
||||
|
||||
Ordinary settings let the nearest configuration win. Policies reverse that: statements
|
||||
from broader configuration are evaluated later, so they override narrower ones.
|
||||
from broader configuration are evaluated later, so they override narrower ones. Managed
|
||||
configuration and the connected Console always have the final say.
|
||||
|
||||
| Authority | Source |
|
||||
| ----------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
Reference in New Issue
Block a user