Files
clawdbot/src/config/zod-schema.agent-runtime.ts

550 lines
17 KiB
TypeScript

import { z } from "zod";
import { parseDurationMs } from "../cli/parse-duration.js";
import {
GroupChatSchema,
HumanDelaySchema,
IdentitySchema,
ToolsLinksSchema,
ToolsMediaSchema,
} from "./zod-schema.core.js";
export const HeartbeatSchema = z
.object({
every: z.string().optional(),
activeHours: z
.object({
start: z.string().optional(),
end: z.string().optional(),
timezone: z.string().optional(),
})
.strict()
.optional(),
model: z.string().optional(),
session: z.string().optional(),
includeReasoning: z.boolean().optional(),
target: z.string().optional(),
to: z.string().optional(),
prompt: z.string().optional(),
ackMaxChars: z.number().int().nonnegative().optional(),
})
.strict()
.superRefine((val, ctx) => {
if (!val.every) return;
try {
parseDurationMs(val.every, { defaultUnit: "m" });
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["every"],
message: "invalid duration (use ms, s, m, h)",
});
}
const active = val.activeHours;
if (!active) return;
const timePattern = /^([01]\d|2[0-3]|24):([0-5]\d)$/;
const validateTime = (raw: string | undefined, opts: { allow24: boolean }, path: string) => {
if (!raw) return;
if (!timePattern.test(raw)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["activeHours", path],
message: 'invalid time (use "HH:MM" 24h format)',
});
return;
}
const [hourStr, minuteStr] = raw.split(":");
const hour = Number(hourStr);
const minute = Number(minuteStr);
if (hour === 24 && minute !== 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["activeHours", path],
message: "invalid time (24:00 is the only allowed 24:xx value)",
});
return;
}
if (hour === 24 && !opts.allow24) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["activeHours", path],
message: "invalid time (start cannot be 24:00)",
});
}
};
validateTime(active.start, { allow24: false }, "start");
validateTime(active.end, { allow24: true }, "end");
})
.optional();
export const SandboxDockerSchema = z
.object({
image: z.string().optional(),
containerPrefix: z.string().optional(),
workdir: z.string().optional(),
readOnlyRoot: z.boolean().optional(),
tmpfs: z.array(z.string()).optional(),
network: z.string().optional(),
user: z.string().optional(),
capDrop: z.array(z.string()).optional(),
env: z.record(z.string(), z.string()).optional(),
setupCommand: z.string().optional(),
pidsLimit: z.number().int().positive().optional(),
memory: z.union([z.string(), z.number()]).optional(),
memorySwap: z.union([z.string(), z.number()]).optional(),
cpus: z.number().positive().optional(),
ulimits: z
.record(
z.string(),
z.union([
z.string(),
z.number(),
z
.object({
soft: z.number().int().nonnegative().optional(),
hard: z.number().int().nonnegative().optional(),
})
.strict(),
]),
)
.optional(),
seccompProfile: z.string().optional(),
apparmorProfile: z.string().optional(),
dns: z.array(z.string()).optional(),
extraHosts: z.array(z.string()).optional(),
binds: z.array(z.string()).optional(),
})
.strict()
.optional();
export const SandboxBrowserSchema = z
.object({
enabled: z.boolean().optional(),
image: z.string().optional(),
containerPrefix: z.string().optional(),
cdpPort: z.number().int().positive().optional(),
vncPort: z.number().int().positive().optional(),
noVncPort: z.number().int().positive().optional(),
headless: z.boolean().optional(),
enableNoVnc: z.boolean().optional(),
allowHostControl: z.boolean().optional(),
allowedControlUrls: z.array(z.string()).optional(),
allowedControlHosts: z.array(z.string()).optional(),
allowedControlPorts: z.array(z.number().int().positive()).optional(),
autoStart: z.boolean().optional(),
autoStartTimeoutMs: z.number().int().positive().optional(),
})
.strict()
.optional();
export const SandboxPruneSchema = z
.object({
idleHours: z.number().int().nonnegative().optional(),
maxAgeDays: z.number().int().nonnegative().optional(),
})
.strict()
.optional();
const ToolPolicyBaseSchema = z
.object({
allow: z.array(z.string()).optional(),
alsoAllow: z.array(z.string()).optional(),
deny: z.array(z.string()).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({
enabled: z.boolean().optional(),
provider: z.union([z.literal("brave"), z.literal("perplexity")]).optional(),
apiKey: z.string().optional(),
maxResults: z.number().int().positive().optional(),
timeoutSeconds: z.number().int().positive().optional(),
cacheTtlMinutes: z.number().nonnegative().optional(),
perplexity: z
.object({
apiKey: z.string().optional(),
baseUrl: z.string().optional(),
model: z.string().optional(),
})
.strict()
.optional(),
})
.strict()
.optional();
export const ToolsWebFetchSchema = z
.object({
enabled: z.boolean().optional(),
maxChars: z.number().int().positive().optional(),
timeoutSeconds: z.number().int().positive().optional(),
cacheTtlMinutes: z.number().nonnegative().optional(),
maxRedirects: z.number().int().nonnegative().optional(),
userAgent: z.string().optional(),
})
.strict()
.optional();
export const ToolsWebSchema = z
.object({
search: ToolsWebSearchSchema,
fetch: ToolsWebFetchSchema,
})
.strict()
.optional();
export const ToolProfileSchema = z
.union([z.literal("minimal"), z.literal("coding"), z.literal("messaging"), z.literal("full")])
.optional();
export const ToolPolicyWithProfileSchema = z
.object({
allow: z.array(z.string()).optional(),
alsoAllow: z.array(z.string()).optional(),
deny: z.array(z.string()).optional(),
profile: ToolProfileSchema,
})
.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
.record(z.string(), z.array(z.union([z.string(), z.number()])))
.optional();
export const AgentSandboxSchema = z
.object({
mode: z.union([z.literal("off"), z.literal("non-main"), z.literal("all")]).optional(),
workspaceAccess: z.union([z.literal("none"), z.literal("ro"), z.literal("rw")]).optional(),
sessionToolsVisibility: z.union([z.literal("spawned"), z.literal("all")]).optional(),
scope: z.union([z.literal("session"), z.literal("agent"), z.literal("shared")]).optional(),
perSession: z.boolean().optional(),
workspaceRoot: z.string().optional(),
docker: SandboxDockerSchema,
browser: SandboxBrowserSchema,
prune: SandboxPruneSchema,
})
.strict()
.optional();
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(),
byProvider: z.record(z.string(), ToolPolicyWithProfileSchema).optional(),
elevated: z
.object({
enabled: z.boolean().optional(),
allowFrom: ElevatedAllowFromSchema,
})
.strict()
.optional(),
exec: z
.object({
host: z.enum(["sandbox", "gateway", "node"]).optional(),
security: z.enum(["deny", "allowlist", "full"]).optional(),
ask: z.enum(["off", "on-miss", "always"]).optional(),
node: z.string().optional(),
pathPrepend: z.array(z.string()).optional(),
safeBins: z.array(z.string()).optional(),
backgroundMs: z.number().int().positive().optional(),
timeoutSec: z.number().int().positive().optional(),
approvalRunningNoticeMs: z.number().int().nonnegative().optional(),
cleanupMs: z.number().int().positive().optional(),
notifyOnExit: z.boolean().optional(),
applyPatch: z
.object({
enabled: z.boolean().optional(),
allowModels: z.array(z.string()).optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
sandbox: z
.object({
tools: ToolPolicySchema,
})
.strict()
.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
.object({
enabled: z.boolean().optional(),
sources: z.array(z.union([z.literal("memory"), z.literal("sessions")])).optional(),
experimental: z
.object({
sessionMemory: z.boolean().optional(),
})
.strict()
.optional(),
provider: z.union([z.literal("openai"), z.literal("local"), z.literal("gemini")]).optional(),
remote: z
.object({
baseUrl: z.string().optional(),
apiKey: z.string().optional(),
headers: z.record(z.string(), z.string()).optional(),
batch: z
.object({
enabled: z.boolean().optional(),
wait: z.boolean().optional(),
concurrency: z.number().int().positive().optional(),
pollIntervalMs: z.number().int().nonnegative().optional(),
timeoutMinutes: z.number().int().positive().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
fallback: z
.union([z.literal("openai"), z.literal("gemini"), z.literal("local"), z.literal("none")])
.optional(),
model: z.string().optional(),
local: z
.object({
modelPath: z.string().optional(),
modelCacheDir: z.string().optional(),
})
.strict()
.optional(),
store: z
.object({
driver: z.literal("sqlite").optional(),
path: z.string().optional(),
vector: z
.object({
enabled: z.boolean().optional(),
extensionPath: z.string().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
chunking: z
.object({
tokens: z.number().int().positive().optional(),
overlap: z.number().int().nonnegative().optional(),
})
.strict()
.optional(),
sync: z
.object({
onSessionStart: z.boolean().optional(),
onSearch: z.boolean().optional(),
watch: z.boolean().optional(),
watchDebounceMs: z.number().int().nonnegative().optional(),
intervalMinutes: z.number().int().nonnegative().optional(),
sessions: z
.object({
deltaBytes: z.number().int().nonnegative().optional(),
deltaMessages: z.number().int().nonnegative().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
query: z
.object({
maxResults: z.number().int().positive().optional(),
minScore: z.number().min(0).max(1).optional(),
hybrid: z
.object({
enabled: z.boolean().optional(),
vectorWeight: z.number().min(0).max(1).optional(),
textWeight: z.number().min(0).max(1).optional(),
candidateMultiplier: z.number().int().positive().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
cache: z
.object({
enabled: z.boolean().optional(),
maxEntries: z.number().int().positive().optional(),
})
.strict()
.optional(),
})
.strict()
.optional();
export const AgentModelSchema = z.union([
z.string(),
z
.object({
primary: z.string().optional(),
fallbacks: z.array(z.string()).optional(),
})
.strict(),
]);
export const AgentEntrySchema = z
.object({
id: z.string(),
default: z.boolean().optional(),
name: z.string().optional(),
workspace: z.string().optional(),
agentDir: z.string().optional(),
model: AgentModelSchema.optional(),
memorySearch: MemorySearchSchema,
humanDelay: HumanDelaySchema.optional(),
heartbeat: HeartbeatSchema,
identity: IdentitySchema,
groupChat: GroupChatSchema,
subagents: z
.object({
allowAgents: z.array(z.string()).optional(),
model: z
.union([
z.string(),
z
.object({
primary: z.string().optional(),
fallbacks: z.array(z.string()).optional(),
})
.strict(),
])
.optional(),
})
.strict()
.optional(),
sandbox: AgentSandboxSchema,
tools: AgentToolsSchema,
})
.strict();
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(),
byProvider: z.record(z.string(), ToolPolicyWithProfileSchema).optional(),
web: ToolsWebSchema,
media: ToolsMediaSchema,
links: ToolsLinksSchema,
message: z
.object({
allowCrossContextSend: z.boolean().optional(),
crossContext: z
.object({
allowWithinProvider: z.boolean().optional(),
allowAcrossProviders: z.boolean().optional(),
marker: z
.object({
enabled: z.boolean().optional(),
prefix: z.string().optional(),
suffix: z.string().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
broadcast: z
.object({
enabled: z.boolean().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
agentToAgent: z
.object({
enabled: z.boolean().optional(),
allow: z.array(z.string()).optional(),
})
.strict()
.optional(),
elevated: z
.object({
enabled: z.boolean().optional(),
allowFrom: ElevatedAllowFromSchema,
})
.strict()
.optional(),
exec: z
.object({
host: z.enum(["sandbox", "gateway", "node"]).optional(),
security: z.enum(["deny", "allowlist", "full"]).optional(),
ask: z.enum(["off", "on-miss", "always"]).optional(),
node: z.string().optional(),
pathPrepend: z.array(z.string()).optional(),
safeBins: z.array(z.string()).optional(),
backgroundMs: z.number().int().positive().optional(),
timeoutSec: z.number().int().positive().optional(),
cleanupMs: z.number().int().positive().optional(),
notifyOnExit: z.boolean().optional(),
applyPatch: z
.object({
enabled: z.boolean().optional(),
allowModels: z.array(z.string()).optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),
subagents: z
.object({
tools: ToolPolicySchema,
})
.strict()
.optional(),
sandbox: z
.object({
tools: ToolPolicySchema,
})
.strict()
.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();