From 42d039998d73c47dec264377668ef1be00595bc2 Mon Sep 17 00:00:00 2001 From: Pocket Clawd Date: Mon, 26 Jan 2026 10:17:50 -0800 Subject: [PATCH] feat(config): forbid allow+alsoAllow in same scope; auto-merge --- src/config/legacy.migrations.part-3.ts | 85 ++++++++++++++++++++++++++ src/config/zod-schema.agent-runtime.ts | 43 +++++++++++-- 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/src/config/legacy.migrations.part-3.ts b/src/config/legacy.migrations.part-3.ts index 9db9e3ede..d4b75e871 100644 --- a/src/config/legacy.migrations.part-3.ts +++ b/src/config/legacy.migrations.part-3.ts @@ -9,6 +9,84 @@ import { resolveDefaultAgentIdFromRaw, } from "./legacy.shared.js"; +function mergeAlsoAllowIntoAllow(node: unknown): boolean { + if (!isRecord(node)) return false; + const allow = node.allow; + const alsoAllow = node.alsoAllow; + if (!Array.isArray(allow) || allow.length === 0) return false; + if (!Array.isArray(alsoAllow) || alsoAllow.length === 0) return false; + const merged = Array.from(new Set([...(allow as unknown[]), ...(alsoAllow as unknown[])])); + node.allow = merged; + delete node.alsoAllow; + return true; +} + +function migrateAlsoAllowInToolConfig(raw: Record, changes: string[]) { + let mutated = false; + + // Global tools + const tools = getRecord(raw.tools); + if (mergeAlsoAllowIntoAllow(tools)) { + mutated = true; + changes.push("Merged tools.alsoAllow into tools.allow (and removed tools.alsoAllow)."); + } + + // tools.byProvider.* + const byProvider = getRecord(tools?.byProvider); + if (byProvider) { + for (const [key, value] of Object.entries(byProvider)) { + if (mergeAlsoAllowIntoAllow(value)) { + mutated = true; + changes.push(`Merged tools.byProvider.${key}.alsoAllow into allow (and removed alsoAllow).`); + } + } + } + + // agents.list[].tools + const agentsList = getAgentsList(raw); + for (const agent of agentsList) { + const agentTools = getRecord(agent.tools); + if (mergeAlsoAllowIntoAllow(agentTools)) { + mutated = true; + const id = typeof agent.id === "string" ? agent.id : ""; + changes.push(`Merged agents.list[${id}].tools.alsoAllow into allow (and removed alsoAllow).`); + } + + const agentByProvider = getRecord(agentTools?.byProvider); + if (agentByProvider) { + for (const [key, value] of Object.entries(agentByProvider)) { + if (mergeAlsoAllowIntoAllow(value)) { + mutated = true; + const id = typeof agent.id === "string" ? agent.id : ""; + changes.push( + `Merged agents.list[${id}].tools.byProvider.${key}.alsoAllow into allow (and removed alsoAllow).`, + ); + } + } + } + } + + // Provider group tool policies: channels..groups.*.tools and similar nested tool policy objects. + const channels = getRecord(raw.channels); + if (channels) { + for (const [provider, providerCfg] of Object.entries(channels)) { + const groups = getRecord(getRecord(providerCfg)?.groups); + if (!groups) continue; + for (const [groupKey, groupCfg] of Object.entries(groups)) { + const toolsCfg = getRecord(getRecord(groupCfg)?.tools); + if (mergeAlsoAllowIntoAllow(toolsCfg)) { + mutated = true; + changes.push( + `Merged channels.${provider}.groups.${groupKey}.tools.alsoAllow into allow (and removed alsoAllow).`, + ); + } + } + } + } + + return mutated; +} + export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [ { id: "auth.anthropic-claude-cli-mode-oauth", @@ -24,6 +102,13 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [ changes.push('Updated auth.profiles["anthropic:claude-cli"].mode → "oauth".'); }, }, + { + id: "tools.alsoAllow-merge", + describe: "Merge tools.alsoAllow into allow when allow is present", + apply: (raw, changes) => { + migrateAlsoAllowInToolConfig(raw, changes); + }, + }, { id: "tools.bash->tools.exec", describe: "Move tools.bash to tools.exec", diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index e08f08d6e..99074c55e 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -147,14 +147,22 @@ export const SandboxPruneSchema = z .strict() .optional(); -export const ToolPolicySchema = z +const ToolPolicyBaseSchema = z .object({ allow: z.array(z.string()).optional(), alsoAllow: z.array(z.string()).optional(), deny: z.array(z.string()).optional(), }) - .strict() - .optional(); + .strict(); + +export const ToolPolicySchema = ToolPolicyBaseSchema.superRefine((value, ctx) => { + if (value.allow && value.allow.length > 0 && value.alsoAllow && value.alsoAllow.length > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "tools policy cannot set both allow and alsoAllow in the same scope (merge alsoAllow into allow, or remove allow and use profile + alsoAllow)", + }); + } +}).optional(); export const ToolsWebSearchSchema = z .object({ @@ -207,7 +215,16 @@ export const ToolPolicyWithProfileSchema = z deny: z.array(z.string()).optional(), profile: ToolProfileSchema, }) - .strict(); + .strict() + .superRefine((value, ctx) => { + if (value.allow && value.allow.length > 0 && value.alsoAllow && value.alsoAllow.length > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "tools.byProvider policy cannot set both allow and alsoAllow in the same scope (merge alsoAllow into allow, or remove allow and use profile + alsoAllow)", + }); + } + }); // Provider docking: allowlists keyed by provider id (no schema updates when adding providers). export const ElevatedAllowFromSchema = z @@ -274,6 +291,15 @@ export const AgentToolsSchema = z .optional(), }) .strict() + .superRefine((value, ctx) => { + if (value.allow && value.allow.length > 0 && value.alsoAllow && value.alsoAllow.length > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "agent tools cannot set both allow and alsoAllow in the same scope (merge alsoAllow into allow, or remove allow and use profile + alsoAllow)", + }); + } + }) .optional(); export const MemorySearchSchema = z @@ -511,4 +537,13 @@ export const ToolsSchema = z .optional(), }) .strict() + .superRefine((value, ctx) => { + if (value.allow && value.allow.length > 0 && value.alsoAllow && value.alsoAllow.length > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "tools cannot set both allow and alsoAllow in the same scope (merge alsoAllow into allow, or remove allow and use profile + alsoAllow)", + }); + } + }) .optional();