550 lines
17 KiB
TypeScript
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();
|