summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2026-01-05 14:06:38 -0500
committerDax Raad <[email protected]>2026-01-05 14:09:37 -0500
commit0276885181da3a7f32491069e36836fbb4ae0dbc (patch)
tree12a8a1f31d7b5c6466eccdab131e79273860cba6
parent5a38a6f248555e03d91b35c446ea0f9f4753b325 (diff)
downloadopencode-0276885181da3a7f32491069e36836fbb4ae0dbc.tar.gz
opencode-0276885181da3a7f32491069e36836fbb4ae0dbc.zip
core: preserve permission config key order to maintain user-defined permission precedence
-rw-r--r--packages/opencode/src/config/config.ts65
-rw-r--r--packages/opencode/test/config/config.test.ts45
2 files changed, 90 insertions, 20 deletions
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 8eafb92e8..a91c91cf0 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -395,27 +395,52 @@ export namespace Config {
})
export type PermissionRule = z.infer<typeof PermissionRule>
+ // Capture original key order before zod reorders, then rebuild in original order
+ const permissionPreprocess = (val: unknown) => {
+ if (typeof val === "object" && val !== null && !Array.isArray(val)) {
+ return { __originalKeys: Object.keys(val), ...val }
+ }
+ return val
+ }
+
+ const permissionTransform = (x: unknown): Record<string, PermissionRule> => {
+ if (typeof x === "string") return { "*": x as PermissionAction }
+ const obj = x as { __originalKeys?: string[] } & Record<string, unknown>
+ const { __originalKeys, ...rest } = obj
+ if (!__originalKeys) return rest as Record<string, PermissionRule>
+ const result: Record<string, PermissionRule> = {}
+ for (const key of __originalKeys) {
+ if (key in rest) result[key] = rest[key] as PermissionRule
+ }
+ return result
+ }
+
export const Permission = z
- .object({
- read: PermissionRule.optional(),
- edit: PermissionRule.optional(),
- glob: PermissionRule.optional(),
- grep: PermissionRule.optional(),
- list: PermissionRule.optional(),
- bash: PermissionRule.optional(),
- task: PermissionRule.optional(),
- external_directory: PermissionRule.optional(),
- todowrite: PermissionAction.optional(),
- todoread: PermissionAction.optional(),
- webfetch: PermissionAction.optional(),
- websearch: PermissionAction.optional(),
- codesearch: PermissionAction.optional(),
- lsp: PermissionRule.optional(),
- doom_loop: PermissionAction.optional(),
- })
- .catchall(PermissionRule)
- .or(PermissionAction)
- .transform((x) => (typeof x === "string" ? { "*": x } : x))
+ .preprocess(
+ permissionPreprocess,
+ z
+ .object({
+ __originalKeys: z.string().array().optional(),
+ read: PermissionRule.optional(),
+ edit: PermissionRule.optional(),
+ glob: PermissionRule.optional(),
+ grep: PermissionRule.optional(),
+ list: PermissionRule.optional(),
+ bash: PermissionRule.optional(),
+ task: PermissionRule.optional(),
+ external_directory: PermissionRule.optional(),
+ todowrite: PermissionAction.optional(),
+ todoread: PermissionAction.optional(),
+ webfetch: PermissionAction.optional(),
+ websearch: PermissionAction.optional(),
+ codesearch: PermissionAction.optional(),
+ lsp: PermissionRule.optional(),
+ doom_loop: PermissionAction.optional(),
+ })
+ .catchall(PermissionRule)
+ .or(PermissionAction),
+ )
+ .transform(permissionTransform)
.meta({
ref: "PermissionConfig",
})
diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts
index c35a391f8..4efc4b742 100644
--- a/packages/opencode/test/config/config.test.ts
+++ b/packages/opencode/test/config/config.test.ts
@@ -868,3 +868,48 @@ test("merges legacy tools with existing permission config", async () => {
},
})
})
+
+test("permission config preserves key order", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ await Bun.write(
+ path.join(dir, "opencode.json"),
+ JSON.stringify({
+ $schema: "https://opencode.ai/config.json",
+ permission: {
+ "*": "deny",
+ edit: "ask",
+ write: "ask",
+ external_directory: "ask",
+ read: "allow",
+ todowrite: "allow",
+ todoread: "allow",
+ "thoughts_*": "allow",
+ "reasoning_model_*": "allow",
+ "tools_*": "allow",
+ "pr_comments_*": "allow",
+ },
+ }),
+ )
+ },
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const config = await Config.get()
+ expect(Object.keys(config.permission!)).toEqual([
+ "*",
+ "edit",
+ "write",
+ "external_directory",
+ "read",
+ "todowrite",
+ "todoread",
+ "thoughts_*",
+ "reasoning_model_*",
+ "tools_*",
+ "pr_comments_*",
+ ])
+ },
+ })
+})