* feat(secrets): expand secret target coverage and gateway tooling * docs(secrets): align gateway and CLI secret docs * chore(protocol): regenerate swift gateway models for secrets methods * fix(config): restore talk apiKey fallback and stabilize runner test * ci(windows): reduce test worker count for shard stability * ci(windows): raise node heap for test shard stability * test(feishu): make proxy env precedence assertion windows-safe * fix(gateway): resolve auth password SecretInput refs for clients * fix(gateway): resolve remote SecretInput credentials for clients * fix(secrets): skip inactive refs in command snapshot assignments * fix(secrets): scope gateway.remote refs to effective auth surfaces * fix(secrets): ignore memory defaults when enabled agents disable search * fix(secrets): honor Google Chat serviceAccountRef inheritance * fix(secrets): address tsgo errors in command and gateway collectors * fix(secrets): avoid auth-store load in providers-only configure * fix(gateway): defer local password ref resolution by precedence * fix(secrets): gate telegram webhook secret refs by webhook mode * fix(secrets): gate slack signing secret refs to http mode * fix(secrets): skip telegram botToken refs when tokenFile is set * fix(secrets): gate discord pluralkit refs by enabled flag * fix(secrets): gate discord voice tts refs by voice enabled * test(secrets): make runtime fixture modes explicit * fix(cli): resolve local qr password secret refs * fix(cli): fail when gateway leaves command refs unresolved * fix(gateway): fail when local password SecretRef is unresolved * fix(gateway): fail when required remote SecretRefs are unresolved * fix(gateway): resolve local password refs only when password can win * fix(cli): skip local password SecretRef resolution on qr token override * test(gateway): cast SecretRef fixtures to OpenClawConfig * test(secrets): activate mode-gated targets in runtime coverage fixture * fix(cron): support SecretInput webhook tokens safely * fix(bluebubbles): support SecretInput passwords across config paths * fix(msteams): make appPassword SecretInput-safe in onboarding/token paths * fix(bluebubbles): align SecretInput schema helper typing * fix(cli): clarify secrets.resolve version-skew errors * refactor(secrets): return structured inactive paths from secrets.resolve * refactor(gateway): type onboarding secret writes as SecretInput * chore(protocol): regenerate swift models for secrets.resolve * feat(secrets): expand extension credential secretref support * fix(secrets): gate web-search refs by active provider * fix(onboarding): detect SecretRef credentials in extension status * fix(onboarding): allow keeping existing ref in secret prompt * fix(onboarding): resolve gateway password SecretRefs for probe and tui * fix(onboarding): honor secret-input-mode for local gateway auth * fix(acp): resolve gateway SecretInput credentials * fix(secrets): gate gateway.remote refs to remote surfaces * test(secrets): cover pattern matching and inactive array refs * docs(secrets): clarify secrets.resolve and remote active surfaces * fix(bluebubbles): keep existing SecretRef during onboarding * fix(tests): resolve CI type errors in new SecretRef coverage * fix(extensions): replace raw fetch with SSRF-guarded fetch * test(secrets): mark gateway remote targets active in runtime coverage * test(infra): normalize home-prefix expectation across platforms * fix(cli): only resolve local qr password refs in password mode * test(cli): cover local qr token mode with unresolved password ref * docs(cli): clarify local qr password ref resolution behavior * refactor(extensions): reuse sdk SecretInput helpers * fix(wizard): resolve onboarding env-template secrets before plaintext * fix(cli): surface secrets.resolve diagnostics in memory and qr * test(secrets): repair post-rebase runtime and fixtures * fix(gateway): skip remote password ref resolution when token wins * fix(secrets): treat tailscale remote gateway refs as active * fix(gateway): allow remote password fallback when token ref is unresolved * fix(gateway): ignore stale local password refs for none and trusted-proxy * fix(gateway): skip remote secret ref resolution on local call paths * test(cli): cover qr remote tailscale secret ref resolution * fix(secrets): align gateway password active-surface with auth inference * fix(cli): resolve inferred local gateway password refs in qr * fix(gateway): prefer resolvable remote password over token ref pre-resolution * test(gateway): cover none and trusted-proxy stale password refs * docs(secrets): sync qr and gateway active-surface behavior * fix: restore stability blockers from pre-release audit * Secrets: fix collector/runtime precedence contradictions * docs: align secrets and web credential docs * fix(rebase): resolve integration regressions after main rebase * fix(node-host): resolve gateway secret refs for auth * fix(secrets): harden secretinput runtime readers * gateway: skip inactive auth secretref resolution * cli: avoid gateway preflight for inactive secret refs * extensions: allow unresolved refs in onboarding status * tests: fix qr-cli module mock hoist ordering * Security: align audit checks with SecretInput resolution * Gateway: resolve local-mode remote fallback secret refs * Node host: avoid resolving inactive password secret refs * Secrets runtime: mark Slack appToken inactive for HTTP mode * secrets: keep inactive gateway remote refs non-blocking * cli: include agent memory secret targets in runtime resolution * docs(secrets): sync docs with active-surface and web search behavior * fix(secrets): keep telegram top-level token refs active for blank account tokens * fix(daemon): resolve gateway password secret refs for probe auth * fix(secrets): skip IRC NickServ ref resolution when NickServ is disabled * fix(secrets): align token inheritance and exec timeout defaults * docs(secrets): clarify active-surface notes in cli docs * cli: require secrets.resolve gateway capability * gateway: log auth secret surface diagnostics * secrets: remove dead provider resolver module * fix(secrets): restore gateway auth precedence and fallback resolution * fix(tests): align plugin runtime mock typings --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
810 lines
24 KiB
TypeScript
810 lines
24 KiB
TypeScript
import { z } from "zod";
|
|
import { getBlockedNetworkModeReason } from "../agents/sandbox/network-mode.js";
|
|
import { parseDurationMs } from "../cli/parse-duration.js";
|
|
import { AgentModelSchema } from "./zod-schema.agent-model.js";
|
|
import {
|
|
GroupChatSchema,
|
|
HumanDelaySchema,
|
|
IdentitySchema,
|
|
SecretInputSchema,
|
|
ToolsLinksSchema,
|
|
ToolsMediaSchema,
|
|
} from "./zod-schema.core.js";
|
|
import { sensitive } from "./zod-schema.sensitive.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(),
|
|
directPolicy: z.union([z.literal("allow"), z.literal("block")]).optional(),
|
|
to: z.string().optional(),
|
|
accountId: z.string().optional(),
|
|
prompt: z.string().optional(),
|
|
ackMaxChars: z.number().int().nonnegative().optional(),
|
|
suppressToolErrorWarnings: z.boolean().optional(),
|
|
lightContext: z.boolean().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
|
|
.union([z.string(), z.array(z.string())])
|
|
.transform((value) => (Array.isArray(value) ? value.join("\n") : value))
|
|
.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(),
|
|
dangerouslyAllowReservedContainerTargets: z.boolean().optional(),
|
|
dangerouslyAllowExternalBindSources: z.boolean().optional(),
|
|
dangerouslyAllowContainerNamespaceJoin: z.boolean().optional(),
|
|
})
|
|
.strict()
|
|
.superRefine((data, ctx) => {
|
|
if (data.binds) {
|
|
for (let i = 0; i < data.binds.length; i += 1) {
|
|
const bind = data.binds[i]?.trim() ?? "";
|
|
if (!bind) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["binds", i],
|
|
message: "Sandbox security: bind mount entry must be a non-empty string.",
|
|
});
|
|
continue;
|
|
}
|
|
const firstColon = bind.indexOf(":");
|
|
const source = (firstColon <= 0 ? bind : bind.slice(0, firstColon)).trim();
|
|
if (!source.startsWith("/")) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["binds", i],
|
|
message:
|
|
`Sandbox security: bind mount "${bind}" uses a non-absolute source path "${source}". ` +
|
|
"Only absolute POSIX paths are supported for sandbox binds.",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const blockedNetworkReason = getBlockedNetworkModeReason({
|
|
network: data.network,
|
|
allowContainerNamespaceJoin: data.dangerouslyAllowContainerNamespaceJoin === true,
|
|
});
|
|
if (blockedNetworkReason === "host") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["network"],
|
|
message:
|
|
'Sandbox security: network mode "host" is blocked. Use "bridge" or "none" instead.',
|
|
});
|
|
}
|
|
if (blockedNetworkReason === "container_namespace_join") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["network"],
|
|
message:
|
|
'Sandbox security: network mode "container:*" is blocked by default. ' +
|
|
"Use a custom bridge network, or set dangerouslyAllowContainerNamespaceJoin=true only when you fully trust this runtime.",
|
|
});
|
|
}
|
|
if (data.seccompProfile?.trim().toLowerCase() === "unconfined") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["seccompProfile"],
|
|
message:
|
|
'Sandbox security: seccomp profile "unconfined" is blocked. ' +
|
|
"Use a custom seccomp profile file or omit this setting.",
|
|
});
|
|
}
|
|
if (data.apparmorProfile?.trim().toLowerCase() === "unconfined") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["apparmorProfile"],
|
|
message:
|
|
'Sandbox security: apparmor profile "unconfined" is blocked. ' +
|
|
"Use a named AppArmor profile or omit this setting.",
|
|
});
|
|
}
|
|
})
|
|
.optional();
|
|
|
|
export const SandboxBrowserSchema = z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
image: z.string().optional(),
|
|
containerPrefix: z.string().optional(),
|
|
network: z.string().optional(),
|
|
cdpPort: z.number().int().positive().optional(),
|
|
cdpSourceRange: z.string().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(),
|
|
autoStart: z.boolean().optional(),
|
|
autoStartTimeoutMs: z.number().int().positive().optional(),
|
|
binds: z.array(z.string()).optional(),
|
|
})
|
|
.superRefine((data, ctx) => {
|
|
if (data.network?.trim().toLowerCase() === "host") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["network"],
|
|
message:
|
|
'Sandbox security: browser network mode "host" is blocked. Use "bridge" or a custom bridge network instead.',
|
|
});
|
|
}
|
|
})
|
|
.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"),
|
|
z.literal("grok"),
|
|
z.literal("gemini"),
|
|
z.literal("kimi"),
|
|
])
|
|
.optional(),
|
|
apiKey: SecretInputSchema.optional().register(sensitive),
|
|
maxResults: z.number().int().positive().optional(),
|
|
timeoutSeconds: z.number().int().positive().optional(),
|
|
cacheTtlMinutes: z.number().nonnegative().optional(),
|
|
perplexity: z
|
|
.object({
|
|
apiKey: SecretInputSchema.optional().register(sensitive),
|
|
baseUrl: z.string().optional(),
|
|
model: z.string().optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
grok: z
|
|
.object({
|
|
apiKey: SecretInputSchema.optional().register(sensitive),
|
|
model: z.string().optional(),
|
|
inlineCitations: z.boolean().optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
gemini: z
|
|
.object({
|
|
apiKey: SecretInputSchema.optional().register(sensitive),
|
|
model: z.string().optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
kimi: z
|
|
.object({
|
|
apiKey: SecretInputSchema.optional().register(sensitive),
|
|
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(),
|
|
maxCharsCap: 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();
|
|
|
|
type AllowlistPolicy = {
|
|
allow?: string[];
|
|
alsoAllow?: string[];
|
|
};
|
|
|
|
function addAllowAlsoAllowConflictIssue(
|
|
value: AllowlistPolicy,
|
|
ctx: z.RefinementCtx,
|
|
message: string,
|
|
): void {
|
|
if (value.allow && value.allow.length > 0 && value.alsoAllow && value.alsoAllow.length > 0) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message,
|
|
});
|
|
}
|
|
}
|
|
|
|
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) => {
|
|
addAllowAlsoAllowConflictIssue(
|
|
value,
|
|
ctx,
|
|
"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();
|
|
|
|
const ToolExecApplyPatchSchema = z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
workspaceOnly: z.boolean().optional(),
|
|
allowModels: z.array(z.string()).optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
const ToolExecSafeBinProfileSchema = z
|
|
.object({
|
|
minPositional: z.number().int().nonnegative().optional(),
|
|
maxPositional: z.number().int().nonnegative().optional(),
|
|
allowedValueFlags: z.array(z.string()).optional(),
|
|
deniedFlags: z.array(z.string()).optional(),
|
|
})
|
|
.strict();
|
|
|
|
const ToolExecBaseShape = {
|
|
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(),
|
|
safeBinTrustedDirs: z.array(z.string()).optional(),
|
|
safeBinProfiles: z.record(z.string(), ToolExecSafeBinProfileSchema).optional(),
|
|
backgroundMs: z.number().int().positive().optional(),
|
|
timeoutSec: z.number().int().positive().optional(),
|
|
cleanupMs: z.number().int().positive().optional(),
|
|
notifyOnExit: z.boolean().optional(),
|
|
notifyOnExitEmptySuccess: z.boolean().optional(),
|
|
applyPatch: ToolExecApplyPatchSchema,
|
|
} as const;
|
|
|
|
const AgentToolExecSchema = z
|
|
.object({
|
|
...ToolExecBaseShape,
|
|
approvalRunningNoticeMs: z.number().int().nonnegative().optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
const ToolExecSchema = z.object(ToolExecBaseShape).strict().optional();
|
|
|
|
const ToolFsSchema = z
|
|
.object({
|
|
workspaceOnly: z.boolean().optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
const ToolLoopDetectionDetectorSchema = z
|
|
.object({
|
|
genericRepeat: z.boolean().optional(),
|
|
knownPollNoProgress: z.boolean().optional(),
|
|
pingPong: z.boolean().optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
|
|
const ToolLoopDetectionSchema = z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
historySize: z.number().int().positive().optional(),
|
|
warningThreshold: z.number().int().positive().optional(),
|
|
criticalThreshold: z.number().int().positive().optional(),
|
|
globalCircuitBreakerThreshold: z.number().int().positive().optional(),
|
|
detectors: ToolLoopDetectionDetectorSchema,
|
|
})
|
|
.strict()
|
|
.superRefine((value, ctx) => {
|
|
if (
|
|
value.warningThreshold !== undefined &&
|
|
value.criticalThreshold !== undefined &&
|
|
value.warningThreshold >= value.criticalThreshold
|
|
) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["criticalThreshold"],
|
|
message: "tools.loopDetection.warningThreshold must be lower than criticalThreshold.",
|
|
});
|
|
}
|
|
if (
|
|
value.criticalThreshold !== undefined &&
|
|
value.globalCircuitBreakerThreshold !== undefined &&
|
|
value.criticalThreshold >= value.globalCircuitBreakerThreshold
|
|
) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["globalCircuitBreakerThreshold"],
|
|
message:
|
|
"tools.loopDetection.criticalThreshold must be lower than globalCircuitBreakerThreshold.",
|
|
});
|
|
}
|
|
})
|
|
.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()
|
|
.superRefine((data, ctx) => {
|
|
const blockedBrowserNetworkReason = getBlockedNetworkModeReason({
|
|
network: data.browser?.network,
|
|
allowContainerNamespaceJoin: data.docker?.dangerouslyAllowContainerNamespaceJoin === true,
|
|
});
|
|
if (blockedBrowserNetworkReason === "container_namespace_join") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["browser", "network"],
|
|
message:
|
|
'Sandbox security: browser network mode "container:*" is blocked by default. ' +
|
|
"Set sandbox.docker.dangerouslyAllowContainerNamespaceJoin=true only when you fully trust this runtime.",
|
|
});
|
|
}
|
|
})
|
|
.optional();
|
|
|
|
const CommonToolPolicyFields = {
|
|
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(),
|
|
};
|
|
|
|
export const AgentToolsSchema = z
|
|
.object({
|
|
...CommonToolPolicyFields,
|
|
elevated: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
allowFrom: ElevatedAllowFromSchema,
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
exec: AgentToolExecSchema,
|
|
fs: ToolFsSchema,
|
|
loopDetection: ToolLoopDetectionSchema,
|
|
sandbox: z
|
|
.object({
|
|
tools: ToolPolicySchema,
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
})
|
|
.strict()
|
|
.superRefine((value, ctx) => {
|
|
addAllowAlsoAllowConflictIssue(
|
|
value,
|
|
ctx,
|
|
"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(),
|
|
extraPaths: z.array(z.string()).optional(),
|
|
experimental: z
|
|
.object({
|
|
sessionMemory: z.boolean().optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
provider: z
|
|
.union([
|
|
z.literal("openai"),
|
|
z.literal("local"),
|
|
z.literal("gemini"),
|
|
z.literal("voyage"),
|
|
z.literal("mistral"),
|
|
z.literal("ollama"),
|
|
])
|
|
.optional(),
|
|
remote: z
|
|
.object({
|
|
baseUrl: z.string().optional(),
|
|
apiKey: SecretInputSchema.optional().register(sensitive),
|
|
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("voyage"),
|
|
z.literal("mistral"),
|
|
z.literal("ollama"),
|
|
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(),
|
|
mmr: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
lambda: z.number().min(0).max(1).optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
temporalDecay: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
halfLifeDays: z.number().int().positive().optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
cache: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
maxEntries: z.number().int().positive().optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
})
|
|
.strict()
|
|
.optional();
|
|
export { AgentModelSchema };
|
|
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(),
|
|
skills: z.array(z.string()).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(),
|
|
thinking: z.string().optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
sandbox: AgentSandboxSchema,
|
|
tools: AgentToolsSchema,
|
|
})
|
|
.strict();
|
|
|
|
export const ToolsSchema = z
|
|
.object({
|
|
...CommonToolPolicyFields,
|
|
web: ToolsWebSchema,
|
|
media: ToolsMediaSchema,
|
|
links: ToolsLinksSchema,
|
|
sessions: z
|
|
.object({
|
|
visibility: z.enum(["self", "tree", "agent", "all"]).optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
loopDetection: ToolLoopDetectionSchema,
|
|
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: ToolExecSchema,
|
|
fs: ToolFsSchema,
|
|
subagents: z
|
|
.object({
|
|
tools: ToolPolicySchema,
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
sandbox: z
|
|
.object({
|
|
tools: ToolPolicySchema,
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
sessions_spawn: z
|
|
.object({
|
|
attachments: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
maxTotalBytes: z.number().optional(),
|
|
maxFiles: z.number().optional(),
|
|
maxFileBytes: z.number().optional(),
|
|
retainOnSessionKeep: z.boolean().optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
})
|
|
.strict()
|
|
.optional(),
|
|
})
|
|
.strict()
|
|
.superRefine((value, ctx) => {
|
|
addAllowAlsoAllowConflictIssue(
|
|
value,
|
|
ctx,
|
|
"tools cannot set both allow and alsoAllow in the same scope (merge alsoAllow into allow, or remove allow and use profile + alsoAllow)",
|
|
);
|
|
})
|
|
.optional();
|