From 60554db11e5d67a13dbbb1fd4113adcb9d95f005 Mon Sep 17 00:00:00 2001 From: Sebastian <19554889+sebslight@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:34:50 -0500 Subject: [PATCH] feat(config): surface dynamic tool policy suggestions in schema --- src/config/schema.test.ts | 32 +++++++++++++++++++ src/config/zod-schema.agent-runtime.ts | 44 +++++++++++++++++++------- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index f7f90f37e4..1b1579fe85 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -101,4 +101,36 @@ describe("config schema", () => { expect(defaultsHint?.help).toContain("last"); expect(listHint?.help).toContain("bluebubbles"); }); + + it("includes tool policy suggestions in schema while still allowing free-form strings", () => { + const res = buildConfigSchema(); + const schema = res.schema as { properties?: Record }; + + const toolsNode = schema.properties?.tools as + | { properties?: Record } + | undefined; + const allowNode = toolsNode?.properties?.allow as + | { items?: Record } + | undefined; + const itemNode = allowNode?.items; + const variants = [ + ...(Array.isArray(itemNode?.anyOf) ? itemNode.anyOf : []), + ...(Array.isArray(itemNode?.oneOf) ? itemNode.oneOf : []), + ...(Array.isArray(itemNode?.allOf) ? itemNode.allOf : []), + ] as Array>; + + const hasSuggestedEntry = variants.some((entry) => { + const values = Array.isArray(entry.enum) ? entry.enum : []; + return values.includes("exec") || values.includes("group:fs"); + }); + + const hasOpenString = variants.some((entry) => { + const type = entry.type; + const allowsString = type === "string" || (Array.isArray(type) && type.includes("string")); + return allowsString && !Array.isArray(entry.enum) && entry.const === undefined; + }); + + expect(hasSuggestedEntry).toBe(true); + expect(hasOpenString).toBe(true); + }); }); diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index 035c3b23b1..2239f672a3 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { TOOL_GROUPS } from "../agents/tool-policy.js"; import { parseDurationMs } from "../cli/parse-duration.js"; import { GroupChatSchema, @@ -150,11 +151,30 @@ export const SandboxPruneSchema = z .strict() .optional(); +const TOOL_POLICY_SUGGESTED_ENTRIES = Array.from( + new Set([ + ...Object.keys(TOOL_GROUPS), + ...Object.values(TOOL_GROUPS).flat(), + // Expanded later against runtime plugin registrations. + "group:plugins", + ]), +).toSorted((a, b) => a.localeCompare(b)); + +const ToolPolicySuggestedEntrySchema = + TOOL_POLICY_SUGGESTED_ENTRIES.length > 0 + ? z.enum(TOOL_POLICY_SUGGESTED_ENTRIES as [string, ...string[]]) + : z.never(); + +const ToolPolicyEntrySchema = + TOOL_POLICY_SUGGESTED_ENTRIES.length > 0 + ? z.union([ToolPolicySuggestedEntrySchema, z.string()]) + : z.string(); + const ToolPolicyBaseSchema = z .object({ - allow: z.array(z.string()).optional(), - alsoAllow: z.array(z.string()).optional(), - deny: z.array(z.string()).optional(), + allow: z.array(ToolPolicyEntrySchema).optional(), + alsoAllow: z.array(ToolPolicyEntrySchema).optional(), + deny: z.array(ToolPolicyEntrySchema).optional(), }) .strict(); @@ -223,9 +243,9 @@ export const ToolProfileSchema = z export const ToolPolicyWithProfileSchema = z .object({ - allow: z.array(z.string()).optional(), - alsoAllow: z.array(z.string()).optional(), - deny: z.array(z.string()).optional(), + allow: z.array(ToolPolicyEntrySchema).optional(), + alsoAllow: z.array(ToolPolicyEntrySchema).optional(), + deny: z.array(ToolPolicyEntrySchema).optional(), profile: ToolProfileSchema, }) .strict() @@ -262,9 +282,9 @@ export const AgentSandboxSchema = z export const AgentToolsSchema = z .object({ profile: ToolProfileSchema, - allow: z.array(z.string()).optional(), - alsoAllow: z.array(z.string()).optional(), - deny: z.array(z.string()).optional(), + allow: z.array(ToolPolicyEntrySchema).optional(), + alsoAllow: z.array(ToolPolicyEntrySchema).optional(), + deny: z.array(ToolPolicyEntrySchema).optional(), byProvider: z.record(z.string(), ToolPolicyWithProfileSchema).optional(), elevated: z .object({ @@ -477,9 +497,9 @@ export const AgentEntrySchema = z export const ToolsSchema = z .object({ profile: ToolProfileSchema, - allow: z.array(z.string()).optional(), - alsoAllow: z.array(z.string()).optional(), - deny: z.array(z.string()).optional(), + allow: z.array(ToolPolicyEntrySchema).optional(), + alsoAllow: z.array(ToolPolicyEntrySchema).optional(), + deny: z.array(ToolPolicyEntrySchema).optional(), byProvider: z.record(z.string(), ToolPolicyWithProfileSchema).optional(), web: ToolsWebSchema, media: ToolsMediaSchema,