feat(config): surface dynamic tool policy suggestions in schema

This commit is contained in:
Sebastian
2026-02-10 08:34:50 -05:00
parent 671ac4d924
commit 60554db11e
2 changed files with 64 additions and 12 deletions

View File

@@ -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<string, unknown> };
const toolsNode = schema.properties?.tools as
| { properties?: Record<string, unknown> }
| undefined;
const allowNode = toolsNode?.properties?.allow as
| { items?: Record<string, unknown> }
| 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<Record<string, unknown>>;
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);
});
});

View File

@@ -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,