Config UI: tag filters and complete schema help/labels coverage (#23796)
* Config UI: add tag filters and complete schema help/labels * Config UI: finalize tags/help polish and unblock test suite * Protocol: regenerate Swift gateway models
This commit is contained in:
@@ -33,6 +33,7 @@ import type {
|
||||
export type ChannelConfigUiHint = {
|
||||
label?: string;
|
||||
help?: string;
|
||||
tags?: string[];
|
||||
advanced?: boolean;
|
||||
sensitive?: boolean;
|
||||
placeholder?: string;
|
||||
|
||||
763
src/config/schema.help.quality.test.ts
Normal file
763
src/config/schema.help.quality.test.ts
Normal file
@@ -0,0 +1,763 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { FIELD_HELP } from "./schema.help.js";
|
||||
import { FIELD_LABELS } from "./schema.labels.js";
|
||||
|
||||
const ROOT_SECTIONS = [
|
||||
"meta",
|
||||
"env",
|
||||
"wizard",
|
||||
"diagnostics",
|
||||
"logging",
|
||||
"update",
|
||||
"browser",
|
||||
"ui",
|
||||
"auth",
|
||||
"models",
|
||||
"nodeHost",
|
||||
"agents",
|
||||
"tools",
|
||||
"bindings",
|
||||
"broadcast",
|
||||
"audio",
|
||||
"media",
|
||||
"messages",
|
||||
"commands",
|
||||
"approvals",
|
||||
"session",
|
||||
"cron",
|
||||
"hooks",
|
||||
"web",
|
||||
"channels",
|
||||
"discovery",
|
||||
"canvasHost",
|
||||
"talk",
|
||||
"gateway",
|
||||
"memory",
|
||||
"plugins",
|
||||
] as const;
|
||||
|
||||
const TARGET_KEYS = [
|
||||
"memory.citations",
|
||||
"memory.backend",
|
||||
"memory.qmd.searchMode",
|
||||
"memory.qmd.scope",
|
||||
"memory.qmd.includeDefaultMemory",
|
||||
"memory.qmd.mcporter.enabled",
|
||||
"memory.qmd.mcporter.serverName",
|
||||
"memory.qmd.command",
|
||||
"memory.qmd.mcporter",
|
||||
"memory.qmd.mcporter.startDaemon",
|
||||
"memory.qmd.paths",
|
||||
"memory.qmd.paths.path",
|
||||
"memory.qmd.paths.pattern",
|
||||
"memory.qmd.paths.name",
|
||||
"memory.qmd.sessions.enabled",
|
||||
"memory.qmd.sessions.exportDir",
|
||||
"memory.qmd.sessions.retentionDays",
|
||||
"memory.qmd.update.interval",
|
||||
"memory.qmd.update.debounceMs",
|
||||
"memory.qmd.update.onBoot",
|
||||
"memory.qmd.update.waitForBootSync",
|
||||
"memory.qmd.update.embedInterval",
|
||||
"memory.qmd.update.commandTimeoutMs",
|
||||
"memory.qmd.update.updateTimeoutMs",
|
||||
"memory.qmd.update.embedTimeoutMs",
|
||||
"memory.qmd.limits.maxResults",
|
||||
"memory.qmd.limits.maxSnippetChars",
|
||||
"memory.qmd.limits.maxInjectedChars",
|
||||
"memory.qmd.limits.timeoutMs",
|
||||
"agents.defaults.memorySearch.provider",
|
||||
"agents.defaults.memorySearch.fallback",
|
||||
"agents.defaults.memorySearch.sources",
|
||||
"agents.defaults.memorySearch.extraPaths",
|
||||
"agents.defaults.memorySearch.experimental.sessionMemory",
|
||||
"agents.defaults.memorySearch.remote.baseUrl",
|
||||
"agents.defaults.memorySearch.remote.apiKey",
|
||||
"agents.defaults.memorySearch.remote.headers",
|
||||
"agents.defaults.memorySearch.remote.batch.enabled",
|
||||
"agents.defaults.memorySearch.remote.batch.wait",
|
||||
"agents.defaults.memorySearch.remote.batch.concurrency",
|
||||
"agents.defaults.memorySearch.remote.batch.pollIntervalMs",
|
||||
"agents.defaults.memorySearch.remote.batch.timeoutMinutes",
|
||||
"agents.defaults.memorySearch.local.modelPath",
|
||||
"agents.defaults.memorySearch.store.path",
|
||||
"agents.defaults.memorySearch.store.vector.enabled",
|
||||
"agents.defaults.memorySearch.store.vector.extensionPath",
|
||||
"agents.defaults.memorySearch.query.hybrid.enabled",
|
||||
"agents.defaults.memorySearch.query.hybrid.vectorWeight",
|
||||
"agents.defaults.memorySearch.query.hybrid.textWeight",
|
||||
"agents.defaults.memorySearch.query.hybrid.candidateMultiplier",
|
||||
"agents.defaults.memorySearch.query.hybrid.mmr.enabled",
|
||||
"agents.defaults.memorySearch.query.hybrid.mmr.lambda",
|
||||
"agents.defaults.memorySearch.query.hybrid.temporalDecay.enabled",
|
||||
"agents.defaults.memorySearch.query.hybrid.temporalDecay.halfLifeDays",
|
||||
"agents.defaults.memorySearch.cache.enabled",
|
||||
"agents.defaults.memorySearch.cache.maxEntries",
|
||||
"agents.defaults.memorySearch.sync.onSearch",
|
||||
"agents.defaults.memorySearch.sync.watch",
|
||||
"agents.defaults.memorySearch.sync.sessions.deltaBytes",
|
||||
"agents.defaults.memorySearch.sync.sessions.deltaMessages",
|
||||
"models.mode",
|
||||
"models.providers.*.auth",
|
||||
"models.providers.*.authHeader",
|
||||
"gateway.reload.mode",
|
||||
"gateway.controlUi.allowInsecureAuth",
|
||||
"gateway.controlUi.dangerouslyDisableDeviceAuth",
|
||||
"cron",
|
||||
"cron.enabled",
|
||||
"cron.store",
|
||||
"cron.maxConcurrentRuns",
|
||||
"cron.webhook",
|
||||
"cron.webhookToken",
|
||||
"cron.sessionRetention",
|
||||
"session",
|
||||
"session.scope",
|
||||
"session.dmScope",
|
||||
"session.identityLinks",
|
||||
"session.resetTriggers",
|
||||
"session.idleMinutes",
|
||||
"session.reset",
|
||||
"session.reset.mode",
|
||||
"session.reset.atHour",
|
||||
"session.reset.idleMinutes",
|
||||
"session.resetByType",
|
||||
"session.resetByType.direct",
|
||||
"session.resetByType.dm",
|
||||
"session.resetByType.group",
|
||||
"session.resetByType.thread",
|
||||
"session.resetByChannel",
|
||||
"session.store",
|
||||
"session.typingIntervalSeconds",
|
||||
"session.typingMode",
|
||||
"session.mainKey",
|
||||
"session.sendPolicy",
|
||||
"session.sendPolicy.default",
|
||||
"session.sendPolicy.rules",
|
||||
"session.sendPolicy.rules[].action",
|
||||
"session.sendPolicy.rules[].match",
|
||||
"session.sendPolicy.rules[].match.channel",
|
||||
"session.sendPolicy.rules[].match.chatType",
|
||||
"session.sendPolicy.rules[].match.keyPrefix",
|
||||
"session.sendPolicy.rules[].match.rawKeyPrefix",
|
||||
"session.agentToAgent",
|
||||
"session.agentToAgent.maxPingPongTurns",
|
||||
"session.threadBindings",
|
||||
"session.threadBindings.enabled",
|
||||
"session.threadBindings.ttlHours",
|
||||
"session.maintenance",
|
||||
"session.maintenance.mode",
|
||||
"session.maintenance.pruneAfter",
|
||||
"session.maintenance.pruneDays",
|
||||
"session.maintenance.maxEntries",
|
||||
"session.maintenance.rotateBytes",
|
||||
"approvals",
|
||||
"approvals.exec",
|
||||
"approvals.exec.enabled",
|
||||
"approvals.exec.mode",
|
||||
"approvals.exec.agentFilter",
|
||||
"approvals.exec.sessionFilter",
|
||||
"approvals.exec.targets",
|
||||
"approvals.exec.targets[].channel",
|
||||
"approvals.exec.targets[].to",
|
||||
"approvals.exec.targets[].accountId",
|
||||
"approvals.exec.targets[].threadId",
|
||||
"nodeHost",
|
||||
"nodeHost.browserProxy",
|
||||
"nodeHost.browserProxy.enabled",
|
||||
"nodeHost.browserProxy.allowProfiles",
|
||||
"media",
|
||||
"media.preserveFilenames",
|
||||
"audio",
|
||||
"audio.transcription",
|
||||
"audio.transcription.command",
|
||||
"audio.transcription.timeoutSeconds",
|
||||
"bindings",
|
||||
"bindings[].agentId",
|
||||
"bindings[].match",
|
||||
"bindings[].match.channel",
|
||||
"bindings[].match.accountId",
|
||||
"bindings[].match.peer",
|
||||
"bindings[].match.peer.kind",
|
||||
"bindings[].match.peer.id",
|
||||
"bindings[].match.guildId",
|
||||
"bindings[].match.teamId",
|
||||
"bindings[].match.roles",
|
||||
"broadcast",
|
||||
"broadcast.strategy",
|
||||
"broadcast.*",
|
||||
"commands",
|
||||
"commands.allowFrom",
|
||||
"hooks",
|
||||
"hooks.enabled",
|
||||
"hooks.path",
|
||||
"hooks.token",
|
||||
"hooks.defaultSessionKey",
|
||||
"hooks.allowRequestSessionKey",
|
||||
"hooks.allowedSessionKeyPrefixes",
|
||||
"hooks.allowedAgentIds",
|
||||
"hooks.maxBodyBytes",
|
||||
"hooks.transformsDir",
|
||||
"hooks.mappings",
|
||||
"hooks.mappings[].action",
|
||||
"hooks.mappings[].wakeMode",
|
||||
"hooks.mappings[].channel",
|
||||
"hooks.mappings[].transform.module",
|
||||
"hooks.gmail",
|
||||
"hooks.gmail.pushToken",
|
||||
"hooks.gmail.tailscale.mode",
|
||||
"hooks.gmail.thinking",
|
||||
"hooks.internal",
|
||||
"hooks.internal.handlers",
|
||||
"hooks.internal.handlers[].event",
|
||||
"hooks.internal.handlers[].module",
|
||||
"hooks.internal.load.extraDirs",
|
||||
"messages",
|
||||
"messages.messagePrefix",
|
||||
"messages.responsePrefix",
|
||||
"messages.groupChat",
|
||||
"messages.groupChat.mentionPatterns",
|
||||
"messages.groupChat.historyLimit",
|
||||
"messages.queue",
|
||||
"messages.queue.mode",
|
||||
"messages.queue.byChannel",
|
||||
"messages.queue.debounceMs",
|
||||
"messages.queue.debounceMsByChannel",
|
||||
"messages.queue.cap",
|
||||
"messages.queue.drop",
|
||||
"messages.inbound",
|
||||
"messages.inbound.byChannel",
|
||||
"messages.removeAckAfterReply",
|
||||
"messages.tts",
|
||||
"channels",
|
||||
"channels.defaults",
|
||||
"channels.defaults.groupPolicy",
|
||||
"channels.defaults.heartbeat",
|
||||
"channels.defaults.heartbeat.showOk",
|
||||
"channels.defaults.heartbeat.showAlerts",
|
||||
"channels.defaults.heartbeat.useIndicator",
|
||||
"gateway",
|
||||
"gateway.mode",
|
||||
"gateway.bind",
|
||||
"gateway.auth.mode",
|
||||
"gateway.tailscale.mode",
|
||||
"gateway.tools.allow",
|
||||
"gateway.tools.deny",
|
||||
"gateway.tls.enabled",
|
||||
"gateway.tls.autoGenerate",
|
||||
"gateway.http",
|
||||
"gateway.http.endpoints",
|
||||
"browser",
|
||||
"browser.enabled",
|
||||
"browser.cdpUrl",
|
||||
"browser.headless",
|
||||
"browser.noSandbox",
|
||||
"browser.profiles",
|
||||
"browser.profiles.*.driver",
|
||||
"tools",
|
||||
"tools.allow",
|
||||
"tools.deny",
|
||||
"tools.exec",
|
||||
"tools.exec.host",
|
||||
"tools.exec.security",
|
||||
"tools.exec.ask",
|
||||
"tools.exec.node",
|
||||
"tools.agentToAgent.enabled",
|
||||
"tools.elevated.enabled",
|
||||
"tools.elevated.allowFrom",
|
||||
"tools.subagents.tools",
|
||||
"tools.sandbox.tools",
|
||||
"web",
|
||||
"web.enabled",
|
||||
"web.heartbeatSeconds",
|
||||
"web.reconnect",
|
||||
"web.reconnect.initialMs",
|
||||
"web.reconnect.maxMs",
|
||||
"web.reconnect.factor",
|
||||
"web.reconnect.jitter",
|
||||
"web.reconnect.maxAttempts",
|
||||
"discovery",
|
||||
"discovery.wideArea.enabled",
|
||||
"discovery.mdns",
|
||||
"discovery.mdns.mode",
|
||||
"canvasHost",
|
||||
"canvasHost.enabled",
|
||||
"canvasHost.root",
|
||||
"canvasHost.port",
|
||||
"canvasHost.liveReload",
|
||||
"talk",
|
||||
"talk.voiceId",
|
||||
"talk.voiceAliases",
|
||||
"talk.modelId",
|
||||
"talk.outputFormat",
|
||||
"talk.interruptOnSpeech",
|
||||
"meta",
|
||||
"env",
|
||||
"env.shellEnv",
|
||||
"env.shellEnv.enabled",
|
||||
"env.shellEnv.timeoutMs",
|
||||
"env.vars",
|
||||
"wizard",
|
||||
"wizard.lastRunAt",
|
||||
"wizard.lastRunVersion",
|
||||
"wizard.lastRunCommit",
|
||||
"wizard.lastRunCommand",
|
||||
"wizard.lastRunMode",
|
||||
"diagnostics",
|
||||
"diagnostics.otel",
|
||||
"diagnostics.cacheTrace",
|
||||
"logging",
|
||||
"logging.level",
|
||||
"logging.file",
|
||||
"logging.consoleLevel",
|
||||
"logging.consoleStyle",
|
||||
"logging.redactSensitive",
|
||||
"logging.redactPatterns",
|
||||
"update",
|
||||
"ui",
|
||||
"ui.assistant",
|
||||
"plugins",
|
||||
"plugins.enabled",
|
||||
"plugins.allow",
|
||||
"plugins.deny",
|
||||
"plugins.load",
|
||||
"plugins.load.paths",
|
||||
"plugins.slots",
|
||||
"plugins.entries",
|
||||
"plugins.entries.*.enabled",
|
||||
"plugins.entries.*.apiKey",
|
||||
"plugins.entries.*.env",
|
||||
"plugins.entries.*.config",
|
||||
"plugins.installs",
|
||||
"auth",
|
||||
"auth.cooldowns",
|
||||
"models",
|
||||
"models.providers",
|
||||
"models.providers.*.baseUrl",
|
||||
"models.providers.*.apiKey",
|
||||
"models.providers.*.api",
|
||||
"models.providers.*.headers",
|
||||
"models.providers.*.models",
|
||||
"models.bedrockDiscovery",
|
||||
"models.bedrockDiscovery.enabled",
|
||||
"models.bedrockDiscovery.region",
|
||||
"models.bedrockDiscovery.providerFilter",
|
||||
"models.bedrockDiscovery.refreshInterval",
|
||||
"models.bedrockDiscovery.defaultContextWindow",
|
||||
"models.bedrockDiscovery.defaultMaxTokens",
|
||||
"agents",
|
||||
"agents.defaults",
|
||||
"agents.list",
|
||||
"agents.defaults.compaction",
|
||||
"agents.defaults.compaction.mode",
|
||||
"agents.defaults.compaction.reserveTokens",
|
||||
"agents.defaults.compaction.keepRecentTokens",
|
||||
"agents.defaults.compaction.reserveTokensFloor",
|
||||
"agents.defaults.compaction.maxHistoryShare",
|
||||
"agents.defaults.compaction.memoryFlush",
|
||||
"agents.defaults.compaction.memoryFlush.enabled",
|
||||
"agents.defaults.compaction.memoryFlush.softThresholdTokens",
|
||||
"agents.defaults.compaction.memoryFlush.prompt",
|
||||
"agents.defaults.compaction.memoryFlush.systemPrompt",
|
||||
] as const;
|
||||
|
||||
const ENUM_EXPECTATIONS: Record<string, string[]> = {
|
||||
"memory.citations": ['"auto"', '"on"', '"off"'],
|
||||
"memory.backend": ['"builtin"', '"qmd"'],
|
||||
"memory.qmd.searchMode": ['"query"', '"search"', '"vsearch"'],
|
||||
"models.mode": ['"merge"', '"replace"'],
|
||||
"models.providers.*.auth": ['"api-key"', '"token"', '"oauth"', '"aws-sdk"'],
|
||||
"gateway.reload.mode": ['"off"', '"restart"', '"hot"', '"hybrid"'],
|
||||
"approvals.exec.mode": ['"session"', '"targets"', '"both"'],
|
||||
"bindings[].match.peer.kind": ['"direct"', '"group"', '"channel"', '"dm"'],
|
||||
"broadcast.strategy": ['"parallel"', '"sequential"'],
|
||||
"hooks.mappings[].action": ['"wake"', '"agent"'],
|
||||
"hooks.mappings[].wakeMode": ['"now"', '"next-heartbeat"'],
|
||||
"hooks.gmail.tailscale.mode": ['"off"', '"serve"', '"funnel"'],
|
||||
"hooks.gmail.thinking": ['"off"', '"minimal"', '"low"', '"medium"', '"high"'],
|
||||
"messages.queue.mode": [
|
||||
'"steer"',
|
||||
'"followup"',
|
||||
'"collect"',
|
||||
'"steer-backlog"',
|
||||
'"steer+backlog"',
|
||||
'"queue"',
|
||||
'"interrupt"',
|
||||
],
|
||||
"messages.queue.drop": ['"old"', '"new"', '"summarize"'],
|
||||
"channels.defaults.groupPolicy": ['"open"', '"disabled"', '"allowlist"'],
|
||||
"gateway.mode": ['"local"', '"remote"'],
|
||||
"gateway.bind": ['"auto"', '"lan"', '"loopback"', '"custom"', '"tailnet"'],
|
||||
"gateway.auth.mode": ['"none"', '"token"', '"password"', '"trusted-proxy"'],
|
||||
"gateway.tailscale.mode": ['"off"', '"serve"', '"funnel"'],
|
||||
"browser.profiles.*.driver": ['"clawd"', '"extension"'],
|
||||
"discovery.mdns.mode": ['"off"', '"minimal"', '"full"'],
|
||||
"wizard.lastRunMode": ['"local"', '"remote"'],
|
||||
"diagnostics.otel.protocol": ['"http/protobuf"', '"grpc"'],
|
||||
"logging.level": ['"silent"', '"fatal"', '"error"', '"warn"', '"info"', '"debug"', '"trace"'],
|
||||
"logging.consoleLevel": [
|
||||
'"silent"',
|
||||
'"fatal"',
|
||||
'"error"',
|
||||
'"warn"',
|
||||
'"info"',
|
||||
'"debug"',
|
||||
'"trace"',
|
||||
],
|
||||
"logging.consoleStyle": ['"pretty"', '"compact"', '"json"'],
|
||||
"logging.redactSensitive": ['"off"', '"tools"'],
|
||||
"update.channel": ['"stable"', '"beta"', '"dev"'],
|
||||
"agents.defaults.compaction.mode": ['"default"', '"safeguard"'],
|
||||
};
|
||||
|
||||
const TOOLS_HOOKS_TARGET_KEYS = [
|
||||
"hooks.gmail.account",
|
||||
"hooks.gmail.allowUnsafeExternalContent",
|
||||
"hooks.gmail.hookUrl",
|
||||
"hooks.gmail.includeBody",
|
||||
"hooks.gmail.label",
|
||||
"hooks.gmail.model",
|
||||
"hooks.gmail.serve",
|
||||
"hooks.gmail.subscription",
|
||||
"hooks.gmail.tailscale",
|
||||
"hooks.gmail.topic",
|
||||
"hooks.internal.entries",
|
||||
"hooks.internal.installs",
|
||||
"hooks.internal.load",
|
||||
"hooks.mappings[].allowUnsafeExternalContent",
|
||||
"hooks.mappings[].deliver",
|
||||
"hooks.mappings[].id",
|
||||
"hooks.mappings[].match",
|
||||
"hooks.mappings[].messageTemplate",
|
||||
"hooks.mappings[].model",
|
||||
"hooks.mappings[].name",
|
||||
"hooks.mappings[].textTemplate",
|
||||
"hooks.mappings[].thinking",
|
||||
"hooks.mappings[].transform",
|
||||
"tools.alsoAllow",
|
||||
"tools.byProvider",
|
||||
"tools.exec.approvalRunningNoticeMs",
|
||||
"tools.links.enabled",
|
||||
"tools.links.maxLinks",
|
||||
"tools.links.models",
|
||||
"tools.links.scope",
|
||||
"tools.links.timeoutSeconds",
|
||||
"tools.media.audio.attachments",
|
||||
"tools.media.audio.enabled",
|
||||
"tools.media.audio.language",
|
||||
"tools.media.audio.maxBytes",
|
||||
"tools.media.audio.maxChars",
|
||||
"tools.media.audio.models",
|
||||
"tools.media.audio.prompt",
|
||||
"tools.media.audio.scope",
|
||||
"tools.media.audio.timeoutSeconds",
|
||||
"tools.media.concurrency",
|
||||
"tools.media.image.attachments",
|
||||
"tools.media.image.enabled",
|
||||
"tools.media.image.maxBytes",
|
||||
"tools.media.image.maxChars",
|
||||
"tools.media.image.models",
|
||||
"tools.media.image.prompt",
|
||||
"tools.media.image.scope",
|
||||
"tools.media.image.timeoutSeconds",
|
||||
"tools.media.models",
|
||||
"tools.media.video.attachments",
|
||||
"tools.media.video.enabled",
|
||||
"tools.media.video.maxBytes",
|
||||
"tools.media.video.maxChars",
|
||||
"tools.media.video.models",
|
||||
"tools.media.video.prompt",
|
||||
"tools.media.video.scope",
|
||||
"tools.media.video.timeoutSeconds",
|
||||
"tools.profile",
|
||||
] as const;
|
||||
|
||||
const CHANNELS_AGENTS_TARGET_KEYS = [
|
||||
"agents.defaults.memorySearch.chunking.overlap",
|
||||
"agents.defaults.memorySearch.chunking.tokens",
|
||||
"agents.defaults.memorySearch.enabled",
|
||||
"agents.defaults.memorySearch.model",
|
||||
"agents.defaults.memorySearch.query.maxResults",
|
||||
"agents.defaults.memorySearch.query.minScore",
|
||||
"agents.defaults.memorySearch.sync.onSessionStart",
|
||||
"agents.defaults.memorySearch.sync.watchDebounceMs",
|
||||
"agents.defaults.workspace",
|
||||
"agents.list[].tools.alsoAllow",
|
||||
"agents.list[].tools.byProvider",
|
||||
"agents.list[].tools.profile",
|
||||
"channels.bluebubbles",
|
||||
"channels.discord",
|
||||
"channels.discord.token",
|
||||
"channels.imessage",
|
||||
"channels.imessage.cliPath",
|
||||
"channels.irc",
|
||||
"channels.mattermost",
|
||||
"channels.msteams",
|
||||
"channels.signal",
|
||||
"channels.signal.account",
|
||||
"channels.slack",
|
||||
"channels.slack.appToken",
|
||||
"channels.slack.botToken",
|
||||
"channels.slack.userToken",
|
||||
"channels.slack.userTokenReadOnly",
|
||||
"channels.telegram",
|
||||
"channels.telegram.botToken",
|
||||
"channels.telegram.capabilities.inlineButtons",
|
||||
"channels.whatsapp",
|
||||
] as const;
|
||||
|
||||
const FINAL_BACKLOG_TARGET_KEYS = [
|
||||
"browser.evaluateEnabled",
|
||||
"browser.remoteCdpHandshakeTimeoutMs",
|
||||
"browser.remoteCdpTimeoutMs",
|
||||
"browser.snapshotDefaults",
|
||||
"browser.snapshotDefaults.mode",
|
||||
"browser.ssrfPolicy",
|
||||
"browser.ssrfPolicy.allowPrivateNetwork",
|
||||
"browser.ssrfPolicy.allowedHostnames",
|
||||
"browser.ssrfPolicy.hostnameAllowlist",
|
||||
"diagnostics.enabled",
|
||||
"diagnostics.otel.enabled",
|
||||
"diagnostics.otel.endpoint",
|
||||
"diagnostics.otel.flushIntervalMs",
|
||||
"diagnostics.otel.headers",
|
||||
"diagnostics.otel.logs",
|
||||
"diagnostics.otel.metrics",
|
||||
"diagnostics.otel.sampleRate",
|
||||
"diagnostics.otel.serviceName",
|
||||
"diagnostics.otel.traces",
|
||||
"gateway.remote.password",
|
||||
"gateway.remote.token",
|
||||
"skills.load.watch",
|
||||
"skills.load.watchDebounceMs",
|
||||
"talk.apiKey",
|
||||
"ui.assistant.avatar",
|
||||
"ui.assistant.name",
|
||||
"ui.seamColor",
|
||||
] as const;
|
||||
|
||||
describe("config help copy quality", () => {
|
||||
it("keeps root section labels and help complete", () => {
|
||||
for (const key of ROOT_SECTIONS) {
|
||||
expect(FIELD_LABELS[key], `missing root label for ${key}`).toBeDefined();
|
||||
expect(FIELD_HELP[key], `missing root help for ${key}`).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps labels in parity for all help keys", () => {
|
||||
for (const key of Object.keys(FIELD_HELP)) {
|
||||
expect(FIELD_LABELS[key], `missing label for help key ${key}`).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("covers the target confusing fields with non-trivial explanations", () => {
|
||||
for (const key of TARGET_KEYS) {
|
||||
const help = FIELD_HELP[key];
|
||||
expect(help, `missing help for ${key}`).toBeDefined();
|
||||
expect(help.length, `help too short for ${key}`).toBeGreaterThanOrEqual(80);
|
||||
expect(
|
||||
/(default|keep|use|enable|disable|controls|selects|sets|defines)/i.test(help),
|
||||
`help should include operational guidance for ${key}`,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("covers tools/hooks help keys with non-trivial operational guidance", () => {
|
||||
for (const key of TOOLS_HOOKS_TARGET_KEYS) {
|
||||
const help = FIELD_HELP[key];
|
||||
expect(help, `missing help for ${key}`).toBeDefined();
|
||||
expect(help.length, `help too short for ${key}`).toBeGreaterThanOrEqual(80);
|
||||
expect(
|
||||
/(default|keep|use|enable|disable|controls|set|sets|increase|lower|prefer|tune|avoid|choose|when)/i.test(
|
||||
help,
|
||||
),
|
||||
`help should include operational guidance for ${key}`,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("covers channels/agents help keys with non-trivial operational guidance", () => {
|
||||
for (const key of CHANNELS_AGENTS_TARGET_KEYS) {
|
||||
const help = FIELD_HELP[key];
|
||||
expect(help, `missing help for ${key}`).toBeDefined();
|
||||
expect(help.length, `help too short for ${key}`).toBeGreaterThanOrEqual(80);
|
||||
expect(
|
||||
/(default|keep|use|enable|disable|controls|set|sets|increase|lower|prefer|tune|avoid|choose|when)/i.test(
|
||||
help,
|
||||
),
|
||||
`help should include operational guidance for ${key}`,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("covers final backlog help keys with non-trivial operational guidance", () => {
|
||||
for (const key of FINAL_BACKLOG_TARGET_KEYS) {
|
||||
const help = FIELD_HELP[key];
|
||||
expect(help, `missing help for ${key}`).toBeDefined();
|
||||
expect(help.length, `help too short for ${key}`).toBeGreaterThanOrEqual(80);
|
||||
expect(
|
||||
/(default|keep|use|enable|disable|controls|set|sets|increase|lower|prefer|tune|avoid|choose|when)/i.test(
|
||||
help,
|
||||
),
|
||||
`help should include operational guidance for ${key}`,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("documents option behavior for enum-style fields", () => {
|
||||
for (const [key, options] of Object.entries(ENUM_EXPECTATIONS)) {
|
||||
const help = FIELD_HELP[key];
|
||||
expect(help, `missing help for enum key ${key}`).toBeDefined();
|
||||
for (const token of options) {
|
||||
expect(help.includes(token), `missing option ${token} in ${key}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("explains memory citations mode semantics", () => {
|
||||
const help = FIELD_HELP["memory.citations"];
|
||||
expect(help.includes('"auto"')).toBe(true);
|
||||
expect(help.includes('"on"')).toBe(true);
|
||||
expect(help.includes('"off"')).toBe(true);
|
||||
expect(/always|always shows/i.test(help)).toBe(true);
|
||||
expect(/hides|hide/i.test(help)).toBe(true);
|
||||
});
|
||||
|
||||
it("includes concrete examples on path and interval fields", () => {
|
||||
expect(FIELD_HELP["memory.qmd.paths.pattern"].includes("**/*.md")).toBe(true);
|
||||
expect(FIELD_HELP["memory.qmd.update.interval"].includes("5m")).toBe(true);
|
||||
expect(FIELD_HELP["memory.qmd.update.embedInterval"].includes("60m")).toBe(true);
|
||||
expect(FIELD_HELP["agents.defaults.memorySearch.store.path"]).toContain(
|
||||
"~/.openclaw/memory/{agentId}.sqlite",
|
||||
);
|
||||
});
|
||||
|
||||
it("documents cron deprecation, migration, and retention formats", () => {
|
||||
const legacy = FIELD_HELP["cron.webhook"];
|
||||
expect(/deprecated|legacy/i.test(legacy)).toBe(true);
|
||||
expect(legacy.includes('delivery.mode="webhook"')).toBe(true);
|
||||
expect(legacy.includes("delivery.to")).toBe(true);
|
||||
|
||||
const retention = FIELD_HELP["cron.sessionRetention"];
|
||||
expect(retention.includes("24h")).toBe(true);
|
||||
expect(retention.includes("7d")).toBe(true);
|
||||
expect(retention.includes("1h30m")).toBe(true);
|
||||
expect(/false/i.test(retention)).toBe(true);
|
||||
|
||||
const token = FIELD_HELP["cron.webhookToken"];
|
||||
expect(/token|bearer/i.test(token)).toBe(true);
|
||||
expect(/secret|env|rotate/i.test(token)).toBe(true);
|
||||
});
|
||||
|
||||
it("documents session send-policy examples and prefix semantics", () => {
|
||||
const rules = FIELD_HELP["session.sendPolicy.rules"];
|
||||
expect(rules.includes("{ action:")).toBe(true);
|
||||
expect(rules.includes('"deny"')).toBe(true);
|
||||
expect(rules.includes('"discord"')).toBe(true);
|
||||
|
||||
const keyPrefix = FIELD_HELP["session.sendPolicy.rules[].match.keyPrefix"];
|
||||
expect(/normalized/i.test(keyPrefix)).toBe(true);
|
||||
|
||||
const rawKeyPrefix = FIELD_HELP["session.sendPolicy.rules[].match.rawKeyPrefix"];
|
||||
expect(/raw|unnormalized/i.test(rawKeyPrefix)).toBe(true);
|
||||
});
|
||||
|
||||
it("documents session maintenance duration/size examples and deprecations", () => {
|
||||
const pruneAfter = FIELD_HELP["session.maintenance.pruneAfter"];
|
||||
expect(pruneAfter.includes("30d")).toBe(true);
|
||||
expect(pruneAfter.includes("12h")).toBe(true);
|
||||
|
||||
const rotate = FIELD_HELP["session.maintenance.rotateBytes"];
|
||||
expect(rotate.includes("10mb")).toBe(true);
|
||||
expect(rotate.includes("1gb")).toBe(true);
|
||||
|
||||
const deprecated = FIELD_HELP["session.maintenance.pruneDays"];
|
||||
expect(/deprecated/i.test(deprecated)).toBe(true);
|
||||
expect(deprecated.includes("session.maintenance.pruneAfter")).toBe(true);
|
||||
});
|
||||
|
||||
it("documents approvals filters and target semantics", () => {
|
||||
const sessionFilter = FIELD_HELP["approvals.exec.sessionFilter"];
|
||||
expect(/substring|regex/i.test(sessionFilter)).toBe(true);
|
||||
expect(sessionFilter.includes("discord:")).toBe(true);
|
||||
expect(sessionFilter.includes("^agent:ops:")).toBe(true);
|
||||
|
||||
const agentFilter = FIELD_HELP["approvals.exec.agentFilter"];
|
||||
expect(agentFilter.includes("primary")).toBe(true);
|
||||
expect(agentFilter.includes("ops-agent")).toBe(true);
|
||||
|
||||
const targetTo = FIELD_HELP["approvals.exec.targets[].to"];
|
||||
expect(/channel ID|user ID|thread root/i.test(targetTo)).toBe(true);
|
||||
expect(/differs|per provider/i.test(targetTo)).toBe(true);
|
||||
});
|
||||
|
||||
it("documents broadcast and audio command examples", () => {
|
||||
const audioCmd = FIELD_HELP["audio.transcription.command"];
|
||||
expect(audioCmd.includes("whisper-cli")).toBe(true);
|
||||
expect(audioCmd.includes("{input}")).toBe(true);
|
||||
|
||||
const broadcastMap = FIELD_HELP["broadcast.*"];
|
||||
expect(/source peer ID/i.test(broadcastMap)).toBe(true);
|
||||
expect(/destination peer IDs/i.test(broadcastMap)).toBe(true);
|
||||
});
|
||||
|
||||
it("documents hook transform safety and queue behavior options", () => {
|
||||
const transformModule = FIELD_HELP["hooks.mappings[].transform.module"];
|
||||
expect(/relative/i.test(transformModule)).toBe(true);
|
||||
expect(/path traversal|reviewed|controlled/i.test(transformModule)).toBe(true);
|
||||
|
||||
const queueMode = FIELD_HELP["messages.queue.mode"];
|
||||
expect(queueMode.includes('"interrupt"')).toBe(true);
|
||||
expect(queueMode.includes('"steer+backlog"')).toBe(true);
|
||||
});
|
||||
|
||||
it("documents gateway bind modes and web reconnect semantics", () => {
|
||||
const bind = FIELD_HELP["gateway.bind"];
|
||||
expect(bind.includes('"loopback"')).toBe(true);
|
||||
expect(bind.includes('"tailnet"')).toBe(true);
|
||||
|
||||
const reconnect = FIELD_HELP["web.reconnect.maxAttempts"];
|
||||
expect(/0 means no retries|no retries/i.test(reconnect)).toBe(true);
|
||||
expect(/failure sequence|retry/i.test(reconnect)).toBe(true);
|
||||
});
|
||||
|
||||
it("documents metadata/admin semantics for logging, wizard, and plugins", () => {
|
||||
const wizardMode = FIELD_HELP["wizard.lastRunMode"];
|
||||
expect(wizardMode.includes('"local"')).toBe(true);
|
||||
expect(wizardMode.includes('"remote"')).toBe(true);
|
||||
|
||||
const consoleStyle = FIELD_HELP["logging.consoleStyle"];
|
||||
expect(consoleStyle.includes('"pretty"')).toBe(true);
|
||||
expect(consoleStyle.includes('"compact"')).toBe(true);
|
||||
expect(consoleStyle.includes('"json"')).toBe(true);
|
||||
|
||||
const pluginApiKey = FIELD_HELP["plugins.entries.*.apiKey"];
|
||||
expect(/secret|env|credential/i.test(pluginApiKey)).toBe(true);
|
||||
|
||||
const pluginEnv = FIELD_HELP["plugins.entries.*.env"];
|
||||
expect(/scope|plugin|environment/i.test(pluginEnv)).toBe(true);
|
||||
});
|
||||
|
||||
it("documents auth/model root semantics and provider secret handling", () => {
|
||||
const providerKey = FIELD_HELP["models.providers.*.apiKey"];
|
||||
expect(/secret|env|credential/i.test(providerKey)).toBe(true);
|
||||
|
||||
const bedrockRefresh = FIELD_HELP["models.bedrockDiscovery.refreshInterval"];
|
||||
expect(/refresh|seconds|interval/i.test(bedrockRefresh)).toBe(true);
|
||||
expect(/cost|noise|api/i.test(bedrockRefresh)).toBe(true);
|
||||
|
||||
const authCooldowns = FIELD_HELP["auth.cooldowns"];
|
||||
expect(/cooldown|backoff|retry/i.test(authCooldowns)).toBe(true);
|
||||
});
|
||||
|
||||
it("documents agent compaction safeguards and memory flush behavior", () => {
|
||||
const mode = FIELD_HELP["agents.defaults.compaction.mode"];
|
||||
expect(mode.includes('"default"')).toBe(true);
|
||||
expect(mode.includes('"safeguard"')).toBe(true);
|
||||
|
||||
const historyShare = FIELD_HELP["agents.defaults.compaction.maxHistoryShare"];
|
||||
expect(/0\\.1-0\\.9|fraction|share/i.test(historyShare)).toBe(true);
|
||||
|
||||
const flush = FIELD_HELP["agents.defaults.compaction.memoryFlush.enabled"];
|
||||
expect(/pre-compaction|memory flush|token/i.test(flush)).toBe(true);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { FIELD_HELP } from "./schema.help.js";
|
||||
import { FIELD_LABELS } from "./schema.labels.js";
|
||||
import { applyDerivedTags } from "./schema.tags.js";
|
||||
import { sensitive } from "./zod-schema.sensitive.js";
|
||||
|
||||
const log = createSubsystemLogger("config/schema");
|
||||
@@ -9,6 +10,7 @@ const log = createSubsystemLogger("config/schema");
|
||||
export type ConfigUiHint = {
|
||||
label?: string;
|
||||
help?: string;
|
||||
tags?: string[];
|
||||
group?: string;
|
||||
order?: number;
|
||||
advanced?: boolean;
|
||||
@@ -143,7 +145,7 @@ export function buildBaseHints(): ConfigUiHints {
|
||||
const current = hints[path];
|
||||
hints[path] = current ? { ...current, placeholder } : { placeholder };
|
||||
}
|
||||
return hints;
|
||||
return applyDerivedTags(hints);
|
||||
}
|
||||
|
||||
export function applySensitiveHints(
|
||||
|
||||
@@ -1,8 +1,31 @@
|
||||
import { IRC_FIELD_LABELS } from "./schema.irc.js";
|
||||
|
||||
export const FIELD_LABELS: Record<string, string> = {
|
||||
meta: "Metadata",
|
||||
"meta.lastTouchedVersion": "Config Last Touched Version",
|
||||
"meta.lastTouchedAt": "Config Last Touched At",
|
||||
env: "Environment",
|
||||
"env.shellEnv": "Shell Environment Import",
|
||||
"env.shellEnv.enabled": "Shell Environment Import Enabled",
|
||||
"env.shellEnv.timeoutMs": "Shell Environment Import Timeout (ms)",
|
||||
"env.vars": "Environment Variable Overrides",
|
||||
wizard: "Setup Wizard State",
|
||||
"wizard.lastRunAt": "Wizard Last Run Timestamp",
|
||||
"wizard.lastRunVersion": "Wizard Last Run Version",
|
||||
"wizard.lastRunCommit": "Wizard Last Run Commit",
|
||||
"wizard.lastRunCommand": "Wizard Last Run Command",
|
||||
"wizard.lastRunMode": "Wizard Last Run Mode",
|
||||
diagnostics: "Diagnostics",
|
||||
"diagnostics.otel": "OpenTelemetry",
|
||||
"diagnostics.cacheTrace": "Cache Trace",
|
||||
logging: "Logging",
|
||||
"logging.level": "Log Level",
|
||||
"logging.file": "Log File Path",
|
||||
"logging.consoleLevel": "Console Log Level",
|
||||
"logging.consoleStyle": "Console Log Style",
|
||||
"logging.redactSensitive": "Sensitive Data Redaction Mode",
|
||||
"logging.redactPatterns": "Custom Redaction Patterns",
|
||||
update: "Updates",
|
||||
"update.channel": "Update Channel",
|
||||
"update.checkOnStart": "Update Check on Start",
|
||||
"update.auto.enabled": "Auto Update Enabled",
|
||||
@@ -28,6 +51,41 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"diagnostics.cacheTrace.includeSystem": "Cache Trace Include System",
|
||||
"agents.list.*.identity.avatar": "Identity Avatar",
|
||||
"agents.list.*.skills": "Agent Skill Filter",
|
||||
agents: "Agents",
|
||||
"agents.defaults": "Agent Defaults",
|
||||
"agents.list": "Agent List",
|
||||
gateway: "Gateway",
|
||||
"gateway.port": "Gateway Port",
|
||||
"gateway.mode": "Gateway Mode",
|
||||
"gateway.bind": "Gateway Bind Mode",
|
||||
"gateway.customBindHost": "Gateway Custom Bind Host",
|
||||
"gateway.controlUi": "Control UI",
|
||||
"gateway.controlUi.enabled": "Control UI Enabled",
|
||||
"gateway.auth": "Gateway Auth",
|
||||
"gateway.auth.mode": "Gateway Auth Mode",
|
||||
"gateway.auth.allowTailscale": "Gateway Auth Allow Tailscale Identity",
|
||||
"gateway.auth.rateLimit": "Gateway Auth Rate Limit",
|
||||
"gateway.auth.trustedProxy": "Gateway Trusted Proxy Auth",
|
||||
"gateway.trustedProxies": "Gateway Trusted Proxy CIDRs",
|
||||
"gateway.allowRealIpFallback": "Gateway Allow x-real-ip Fallback",
|
||||
"gateway.tools": "Gateway Tool Exposure Policy",
|
||||
"gateway.tools.allow": "Gateway Tool Allowlist",
|
||||
"gateway.tools.deny": "Gateway Tool Denylist",
|
||||
"gateway.channelHealthCheckMinutes": "Gateway Channel Health Check Interval (min)",
|
||||
"gateway.tailscale": "Gateway Tailscale",
|
||||
"gateway.tailscale.mode": "Gateway Tailscale Mode",
|
||||
"gateway.tailscale.resetOnExit": "Gateway Tailscale Reset on Exit",
|
||||
"gateway.remote": "Remote Gateway",
|
||||
"gateway.remote.transport": "Remote Gateway Transport",
|
||||
"gateway.reload": "Config Reload",
|
||||
"gateway.tls": "Gateway TLS",
|
||||
"gateway.tls.enabled": "Gateway TLS Enabled",
|
||||
"gateway.tls.autoGenerate": "Gateway TLS Auto-Generate Cert",
|
||||
"gateway.tls.certPath": "Gateway TLS Certificate Path",
|
||||
"gateway.tls.keyPath": "Gateway TLS Key Path",
|
||||
"gateway.tls.caPath": "Gateway TLS CA Path",
|
||||
"gateway.http": "Gateway HTTP API",
|
||||
"gateway.http.endpoints": "Gateway HTTP Endpoints",
|
||||
"gateway.remote.url": "Remote Gateway URL",
|
||||
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
|
||||
"gateway.remote.sshIdentity": "Remote Gateway SSH Identity",
|
||||
@@ -36,6 +94,25 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"gateway.remote.tlsFingerprint": "Remote Gateway TLS Fingerprint",
|
||||
"gateway.auth.token": "Gateway Token",
|
||||
"gateway.auth.password": "Gateway Password",
|
||||
browser: "Browser",
|
||||
"browser.enabled": "Browser Enabled",
|
||||
"browser.cdpUrl": "Browser CDP URL",
|
||||
"browser.color": "Browser Accent Color",
|
||||
"browser.executablePath": "Browser Executable Path",
|
||||
"browser.headless": "Browser Headless Mode",
|
||||
"browser.noSandbox": "Browser No-Sandbox Mode",
|
||||
"browser.attachOnly": "Browser Attach-only Mode",
|
||||
"browser.defaultProfile": "Browser Default Profile",
|
||||
"browser.profiles": "Browser Profiles",
|
||||
"browser.profiles.*.cdpPort": "Browser Profile CDP Port",
|
||||
"browser.profiles.*.cdpUrl": "Browser Profile CDP URL",
|
||||
"browser.profiles.*.driver": "Browser Profile Driver",
|
||||
"browser.profiles.*.color": "Browser Profile Accent Color",
|
||||
tools: "Tools",
|
||||
"tools.allow": "Tool Allowlist",
|
||||
"tools.deny": "Tool Denylist",
|
||||
"tools.web": "Web Tools",
|
||||
"tools.exec": "Exec Tool",
|
||||
"tools.media.image.enabled": "Enable Image Understanding",
|
||||
"tools.media.image.maxBytes": "Image Understanding Max Bytes",
|
||||
"tools.media.image.maxChars": "Image Understanding Max Chars",
|
||||
@@ -94,9 +171,30 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"tools.exec.security": "Exec Security",
|
||||
"tools.exec.ask": "Exec Ask",
|
||||
"tools.exec.node": "Exec Node Binding",
|
||||
"tools.agentToAgent": "Agent-to-Agent Tool Access",
|
||||
"tools.agentToAgent.enabled": "Enable Agent-to-Agent Tool",
|
||||
"tools.agentToAgent.allow": "Agent-to-Agent Target Allowlist",
|
||||
"tools.elevated": "Elevated Tool Access",
|
||||
"tools.elevated.enabled": "Enable Elevated Tool Access",
|
||||
"tools.elevated.allowFrom": "Elevated Tool Allow Rules",
|
||||
"tools.subagents": "Subagent Tool Policy",
|
||||
"tools.subagents.tools": "Subagent Tool Allow/Deny Policy",
|
||||
"tools.sandbox": "Sandbox Tool Policy",
|
||||
"tools.sandbox.tools": "Sandbox Tool Allow/Deny Policy",
|
||||
"tools.exec.pathPrepend": "Exec PATH Prepend",
|
||||
"tools.exec.safeBins": "Exec Safe Bins",
|
||||
"tools.exec.safeBinProfiles": "Exec Safe Bin Profiles",
|
||||
approvals: "Approvals",
|
||||
"approvals.exec": "Exec Approval Forwarding",
|
||||
"approvals.exec.enabled": "Forward Exec Approvals",
|
||||
"approvals.exec.mode": "Approval Forwarding Mode",
|
||||
"approvals.exec.agentFilter": "Approval Agent Filter",
|
||||
"approvals.exec.sessionFilter": "Approval Session Filter",
|
||||
"approvals.exec.targets": "Approval Forwarding Targets",
|
||||
"approvals.exec.targets[].channel": "Approval Target Channel",
|
||||
"approvals.exec.targets[].to": "Approval Target Destination",
|
||||
"approvals.exec.targets[].accountId": "Approval Target Account ID",
|
||||
"approvals.exec.targets[].threadId": "Approval Target Thread ID",
|
||||
"tools.message.allowCrossContextSend": "Allow Cross-Context Messaging",
|
||||
"tools.message.crossContext.allowWithinProvider": "Allow Cross-Context (Same Provider)",
|
||||
"tools.message.crossContext.allowAcrossProviders": "Allow Cross-Context (Across Providers)",
|
||||
@@ -110,12 +208,23 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"tools.web.search.maxResults": "Web Search Max Results",
|
||||
"tools.web.search.timeoutSeconds": "Web Search Timeout (sec)",
|
||||
"tools.web.search.cacheTtlMinutes": "Web Search Cache TTL (min)",
|
||||
"tools.web.search.perplexity.apiKey": "Perplexity API Key",
|
||||
"tools.web.search.perplexity.baseUrl": "Perplexity Base URL",
|
||||
"tools.web.search.perplexity.model": "Perplexity Model",
|
||||
"tools.web.fetch.enabled": "Enable Web Fetch Tool",
|
||||
"tools.web.fetch.maxChars": "Web Fetch Max Chars",
|
||||
"tools.web.fetch.maxCharsCap": "Web Fetch Hard Max Chars",
|
||||
"tools.web.fetch.timeoutSeconds": "Web Fetch Timeout (sec)",
|
||||
"tools.web.fetch.cacheTtlMinutes": "Web Fetch Cache TTL (min)",
|
||||
"tools.web.fetch.maxRedirects": "Web Fetch Max Redirects",
|
||||
"tools.web.fetch.userAgent": "Web Fetch User-Agent",
|
||||
"tools.web.fetch.readability": "Web Fetch Readability Extraction",
|
||||
"tools.web.fetch.firecrawl.enabled": "Enable Firecrawl Fallback",
|
||||
"tools.web.fetch.firecrawl.apiKey": "Firecrawl API Key",
|
||||
"tools.web.fetch.firecrawl.baseUrl": "Firecrawl Base URL",
|
||||
"tools.web.fetch.firecrawl.onlyMainContent": "Firecrawl Main Content Only",
|
||||
"tools.web.fetch.firecrawl.maxAgeMs": "Firecrawl Cache Max Age (ms)",
|
||||
"tools.web.fetch.firecrawl.timeoutSeconds": "Firecrawl Timeout (sec)",
|
||||
"gateway.controlUi.basePath": "Control UI Base Path",
|
||||
"gateway.controlUi.root": "Control UI Assets Root",
|
||||
"gateway.controlUi.allowedOrigins": "Control UI Allowed Origins",
|
||||
@@ -128,8 +237,30 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"gateway.nodes.browser.node": "Gateway Node Browser Pin",
|
||||
"gateway.nodes.allowCommands": "Gateway Node Allowlist (Extra Commands)",
|
||||
"gateway.nodes.denyCommands": "Gateway Node Denylist",
|
||||
nodeHost: "Node Host",
|
||||
"nodeHost.browserProxy": "Node Browser Proxy",
|
||||
"nodeHost.browserProxy.enabled": "Node Browser Proxy Enabled",
|
||||
"nodeHost.browserProxy.allowProfiles": "Node Browser Proxy Allowed Profiles",
|
||||
media: "Media",
|
||||
"media.preserveFilenames": "Preserve Media Filenames",
|
||||
audio: "Audio",
|
||||
"audio.transcription": "Audio Transcription",
|
||||
"audio.transcription.command": "Audio Transcription Command",
|
||||
"audio.transcription.timeoutSeconds": "Audio Transcription Timeout (sec)",
|
||||
bindings: "Bindings",
|
||||
"bindings[].agentId": "Binding Agent ID",
|
||||
"bindings[].match": "Binding Match Rule",
|
||||
"bindings[].match.channel": "Binding Channel",
|
||||
"bindings[].match.accountId": "Binding Account ID",
|
||||
"bindings[].match.peer": "Binding Peer Match",
|
||||
"bindings[].match.peer.kind": "Binding Peer Kind",
|
||||
"bindings[].match.peer.id": "Binding Peer ID",
|
||||
"bindings[].match.guildId": "Binding Guild ID",
|
||||
"bindings[].match.teamId": "Binding Team ID",
|
||||
"bindings[].match.roles": "Binding Roles",
|
||||
broadcast: "Broadcast",
|
||||
"broadcast.strategy": "Broadcast Strategy",
|
||||
"broadcast.*": "Broadcast Destination List",
|
||||
"skills.load.watch": "Watch Skills",
|
||||
"skills.load.watchDebounceMs": "Skills Watch Debounce (ms)",
|
||||
"agents.defaults.workspace": "Workspace",
|
||||
@@ -149,7 +280,11 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.defaults.memorySearch.remote.baseUrl": "Remote Embedding Base URL",
|
||||
"agents.defaults.memorySearch.remote.apiKey": "Remote Embedding API Key",
|
||||
"agents.defaults.memorySearch.remote.headers": "Remote Embedding Headers",
|
||||
"agents.defaults.memorySearch.remote.batch.enabled": "Remote Batch Embedding Enabled",
|
||||
"agents.defaults.memorySearch.remote.batch.wait": "Remote Batch Wait for Completion",
|
||||
"agents.defaults.memorySearch.remote.batch.concurrency": "Remote Batch Concurrency",
|
||||
"agents.defaults.memorySearch.remote.batch.pollIntervalMs": "Remote Batch Poll Interval (ms)",
|
||||
"agents.defaults.memorySearch.remote.batch.timeoutMinutes": "Remote Batch Timeout (min)",
|
||||
"agents.defaults.memorySearch.model": "Memory Search Model",
|
||||
"agents.defaults.memorySearch.fallback": "Memory Search Fallback",
|
||||
"agents.defaults.memorySearch.local.modelPath": "Local Embedding Model Path",
|
||||
@@ -182,6 +317,11 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"memory.backend": "Memory Backend",
|
||||
"memory.citations": "Memory Citations Mode",
|
||||
"memory.qmd.command": "QMD Binary",
|
||||
"memory.qmd.mcporter": "QMD MCPorter",
|
||||
"memory.qmd.mcporter.enabled": "QMD MCPorter Enabled",
|
||||
"memory.qmd.mcporter.serverName": "QMD MCPorter Server Name",
|
||||
"memory.qmd.mcporter.startDaemon": "QMD MCPorter Start Daemon",
|
||||
"memory.qmd.searchMode": "QMD Search Mode",
|
||||
"memory.qmd.includeDefaultMemory": "QMD Include Default Memory",
|
||||
"memory.qmd.paths": "QMD Extra Paths",
|
||||
"memory.qmd.paths.path": "QMD Path",
|
||||
@@ -203,8 +343,27 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"memory.qmd.limits.maxInjectedChars": "QMD Max Injected Chars",
|
||||
"memory.qmd.limits.timeoutMs": "QMD Search Timeout (ms)",
|
||||
"memory.qmd.scope": "QMD Surface Scope",
|
||||
auth: "Auth",
|
||||
"auth.profiles": "Auth Profiles",
|
||||
"auth.order": "Auth Profile Order",
|
||||
"auth.cooldowns": "Auth Cooldowns",
|
||||
models: "Models",
|
||||
"models.mode": "Model Catalog Mode",
|
||||
"models.providers": "Model Providers",
|
||||
"models.providers.*.baseUrl": "Model Provider Base URL",
|
||||
"models.providers.*.apiKey": "Model Provider API Key",
|
||||
"models.providers.*.auth": "Model Provider Auth Mode",
|
||||
"models.providers.*.api": "Model Provider API Adapter",
|
||||
"models.providers.*.headers": "Model Provider Headers",
|
||||
"models.providers.*.authHeader": "Model Provider Authorization Header",
|
||||
"models.providers.*.models": "Model Provider Model List",
|
||||
"models.bedrockDiscovery": "Bedrock Model Discovery",
|
||||
"models.bedrockDiscovery.enabled": "Bedrock Discovery Enabled",
|
||||
"models.bedrockDiscovery.region": "Bedrock Discovery Region",
|
||||
"models.bedrockDiscovery.providerFilter": "Bedrock Discovery Provider Filter",
|
||||
"models.bedrockDiscovery.refreshInterval": "Bedrock Discovery Refresh Interval (s)",
|
||||
"models.bedrockDiscovery.defaultContextWindow": "Bedrock Default Context Window",
|
||||
"models.bedrockDiscovery.defaultMaxTokens": "Bedrock Default Max Tokens",
|
||||
"auth.cooldowns.billingBackoffHours": "Billing Backoff (hours)",
|
||||
"auth.cooldowns.billingBackoffHoursByProvider": "Billing Backoff Overrides",
|
||||
"auth.cooldowns.billingMaxHours": "Billing Backoff Cap (hours)",
|
||||
@@ -219,6 +378,22 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.defaults.humanDelay.minMs": "Human Delay Min (ms)",
|
||||
"agents.defaults.humanDelay.maxMs": "Human Delay Max (ms)",
|
||||
"agents.defaults.cliBackends": "CLI Backends",
|
||||
"agents.defaults.compaction": "Compaction",
|
||||
"agents.defaults.compaction.mode": "Compaction Mode",
|
||||
"agents.defaults.compaction.reserveTokens": "Compaction Reserve Tokens",
|
||||
"agents.defaults.compaction.keepRecentTokens": "Compaction Keep Recent Tokens",
|
||||
"agents.defaults.compaction.reserveTokensFloor": "Compaction Reserve Token Floor",
|
||||
"agents.defaults.compaction.maxHistoryShare": "Compaction Max History Share",
|
||||
"agents.defaults.compaction.memoryFlush": "Compaction Memory Flush",
|
||||
"agents.defaults.compaction.memoryFlush.enabled": "Compaction Memory Flush Enabled",
|
||||
"agents.defaults.compaction.memoryFlush.softThresholdTokens":
|
||||
"Compaction Memory Flush Soft Threshold",
|
||||
"agents.defaults.compaction.memoryFlush.prompt": "Compaction Memory Flush Prompt",
|
||||
"agents.defaults.compaction.memoryFlush.systemPrompt": "Compaction Memory Flush System Prompt",
|
||||
"agents.defaults.heartbeat.suppressToolErrorWarnings": "Heartbeat Suppress Tool Error Warnings",
|
||||
"agents.defaults.sandbox.browser.network": "Sandbox Browser Network",
|
||||
"agents.defaults.sandbox.browser.cdpSourceRange": "Sandbox Browser CDP Source Port Range",
|
||||
commands: "Commands",
|
||||
"commands.native": "Native Commands",
|
||||
"commands.nativeSkills": "Native Skill Commands",
|
||||
"commands.text": "Text Commands",
|
||||
@@ -231,7 +406,10 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"commands.ownerAllowFrom": "Command Owners",
|
||||
"commands.ownerDisplay": "Owner ID Display",
|
||||
"commands.ownerDisplaySecret": "Owner ID Hash Secret",
|
||||
"commands.allowFrom": "Command Elevated Access Rules",
|
||||
ui: "UI",
|
||||
"ui.seamColor": "Accent Color",
|
||||
"ui.assistant": "Assistant Appearance",
|
||||
"ui.assistant.name": "Assistant Name",
|
||||
"ui.assistant.avatar": "Assistant Avatar",
|
||||
"browser.evaluateEnabled": "Browser Evaluate Enabled",
|
||||
@@ -243,19 +421,174 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"browser.ssrfPolicy.hostnameAllowlist": "Browser Hostname Allowlist",
|
||||
"browser.remoteCdpTimeoutMs": "Remote CDP Timeout (ms)",
|
||||
"browser.remoteCdpHandshakeTimeoutMs": "Remote CDP Handshake Timeout (ms)",
|
||||
session: "Session",
|
||||
"session.scope": "Session Scope",
|
||||
"session.dmScope": "DM Session Scope",
|
||||
"session.identityLinks": "Session Identity Links",
|
||||
"session.resetTriggers": "Session Reset Triggers",
|
||||
"session.idleMinutes": "Session Idle Minutes",
|
||||
"session.reset": "Session Reset Policy",
|
||||
"session.reset.mode": "Session Reset Mode",
|
||||
"session.reset.atHour": "Session Daily Reset Hour",
|
||||
"session.reset.idleMinutes": "Session Reset Idle Minutes",
|
||||
"session.resetByType": "Session Reset by Chat Type",
|
||||
"session.resetByType.direct": "Session Reset (Direct)",
|
||||
"session.resetByType.dm": "Session Reset (DM Deprecated Alias)",
|
||||
"session.resetByType.group": "Session Reset (Group)",
|
||||
"session.resetByType.thread": "Session Reset (Thread)",
|
||||
"session.resetByChannel": "Session Reset by Channel",
|
||||
"session.store": "Session Store Path",
|
||||
"session.typingIntervalSeconds": "Session Typing Interval (seconds)",
|
||||
"session.typingMode": "Session Typing Mode",
|
||||
"session.mainKey": "Session Main Key",
|
||||
"session.sendPolicy": "Session Send Policy",
|
||||
"session.sendPolicy.default": "Session Send Policy Default Action",
|
||||
"session.sendPolicy.rules": "Session Send Policy Rules",
|
||||
"session.sendPolicy.rules[].action": "Session Send Rule Action",
|
||||
"session.sendPolicy.rules[].match": "Session Send Rule Match",
|
||||
"session.sendPolicy.rules[].match.channel": "Session Send Rule Channel",
|
||||
"session.sendPolicy.rules[].match.chatType": "Session Send Rule Chat Type",
|
||||
"session.sendPolicy.rules[].match.keyPrefix": "Session Send Rule Key Prefix",
|
||||
"session.sendPolicy.rules[].match.rawKeyPrefix": "Session Send Rule Raw Key Prefix",
|
||||
"session.agentToAgent": "Session Agent-to-Agent",
|
||||
"session.agentToAgent.maxPingPongTurns": "Agent-to-Agent Ping-Pong Turns",
|
||||
"session.threadBindings": "Session Thread Bindings",
|
||||
"session.threadBindings.enabled": "Thread Binding Enabled",
|
||||
"session.threadBindings.ttlHours": "Thread Binding TTL (hours)",
|
||||
"session.agentToAgent.maxPingPongTurns": "Agent-to-Agent Ping-Pong Turns",
|
||||
"session.maintenance": "Session Maintenance",
|
||||
"session.maintenance.mode": "Session Maintenance Mode",
|
||||
"session.maintenance.pruneAfter": "Session Prune After",
|
||||
"session.maintenance.pruneDays": "Session Prune Days (Deprecated)",
|
||||
"session.maintenance.maxEntries": "Session Max Entries",
|
||||
"session.maintenance.rotateBytes": "Session Rotate Size",
|
||||
cron: "Cron",
|
||||
"cron.enabled": "Cron Enabled",
|
||||
"cron.store": "Cron Store Path",
|
||||
"cron.maxConcurrentRuns": "Cron Max Concurrent Runs",
|
||||
"cron.webhook": "Cron Legacy Webhook (Deprecated)",
|
||||
"cron.webhookToken": "Cron Webhook Bearer Token",
|
||||
"cron.sessionRetention": "Cron Session Retention",
|
||||
hooks: "Hooks",
|
||||
"hooks.enabled": "Hooks Enabled",
|
||||
"hooks.path": "Hooks Endpoint Path",
|
||||
"hooks.token": "Hooks Auth Token",
|
||||
"hooks.defaultSessionKey": "Hooks Default Session Key",
|
||||
"hooks.allowRequestSessionKey": "Hooks Allow Request Session Key",
|
||||
"hooks.allowedSessionKeyPrefixes": "Hooks Allowed Session Key Prefixes",
|
||||
"hooks.allowedAgentIds": "Hooks Allowed Agent IDs",
|
||||
"hooks.maxBodyBytes": "Hooks Max Body Bytes",
|
||||
"hooks.presets": "Hooks Presets",
|
||||
"hooks.transformsDir": "Hooks Transforms Directory",
|
||||
"hooks.mappings": "Hook Mappings",
|
||||
"hooks.mappings[].id": "Hook Mapping ID",
|
||||
"hooks.mappings[].match": "Hook Mapping Match",
|
||||
"hooks.mappings[].match.path": "Hook Mapping Match Path",
|
||||
"hooks.mappings[].match.source": "Hook Mapping Match Source",
|
||||
"hooks.mappings[].action": "Hook Mapping Action",
|
||||
"hooks.mappings[].wakeMode": "Hook Mapping Wake Mode",
|
||||
"hooks.mappings[].name": "Hook Mapping Name",
|
||||
"hooks.mappings[].agentId": "Hook Mapping Agent ID",
|
||||
"hooks.mappings[].sessionKey": "Hook Mapping Session Key",
|
||||
"hooks.mappings[].messageTemplate": "Hook Mapping Message Template",
|
||||
"hooks.mappings[].textTemplate": "Hook Mapping Text Template",
|
||||
"hooks.mappings[].deliver": "Hook Mapping Deliver Reply",
|
||||
"hooks.mappings[].allowUnsafeExternalContent": "Hook Mapping Allow Unsafe External Content",
|
||||
"hooks.mappings[].channel": "Hook Mapping Delivery Channel",
|
||||
"hooks.mappings[].to": "Hook Mapping Delivery Destination",
|
||||
"hooks.mappings[].model": "Hook Mapping Model Override",
|
||||
"hooks.mappings[].thinking": "Hook Mapping Thinking Override",
|
||||
"hooks.mappings[].timeoutSeconds": "Hook Mapping Timeout (sec)",
|
||||
"hooks.mappings[].transform": "Hook Mapping Transform",
|
||||
"hooks.mappings[].transform.module": "Hook Transform Module",
|
||||
"hooks.mappings[].transform.export": "Hook Transform Export",
|
||||
"hooks.gmail": "Gmail Hook",
|
||||
"hooks.gmail.account": "Gmail Hook Account",
|
||||
"hooks.gmail.label": "Gmail Hook Label",
|
||||
"hooks.gmail.topic": "Gmail Hook Pub/Sub Topic",
|
||||
"hooks.gmail.subscription": "Gmail Hook Subscription",
|
||||
"hooks.gmail.pushToken": "Gmail Hook Push Token",
|
||||
"hooks.gmail.hookUrl": "Gmail Hook Callback URL",
|
||||
"hooks.gmail.includeBody": "Gmail Hook Include Body",
|
||||
"hooks.gmail.maxBytes": "Gmail Hook Max Body Bytes",
|
||||
"hooks.gmail.renewEveryMinutes": "Gmail Hook Renew Interval (min)",
|
||||
"hooks.gmail.allowUnsafeExternalContent": "Gmail Hook Allow Unsafe External Content",
|
||||
"hooks.gmail.serve": "Gmail Hook Local Server",
|
||||
"hooks.gmail.serve.bind": "Gmail Hook Server Bind Address",
|
||||
"hooks.gmail.serve.port": "Gmail Hook Server Port",
|
||||
"hooks.gmail.serve.path": "Gmail Hook Server Path",
|
||||
"hooks.gmail.tailscale": "Gmail Hook Tailscale",
|
||||
"hooks.gmail.tailscale.mode": "Gmail Hook Tailscale Mode",
|
||||
"hooks.gmail.tailscale.path": "Gmail Hook Tailscale Path",
|
||||
"hooks.gmail.tailscale.target": "Gmail Hook Tailscale Target",
|
||||
"hooks.gmail.model": "Gmail Hook Model Override",
|
||||
"hooks.gmail.thinking": "Gmail Hook Thinking Override",
|
||||
"hooks.internal": "Internal Hooks",
|
||||
"hooks.internal.enabled": "Internal Hooks Enabled",
|
||||
"hooks.internal.handlers": "Internal Hook Handlers",
|
||||
"hooks.internal.handlers[].event": "Internal Hook Event",
|
||||
"hooks.internal.handlers[].module": "Internal Hook Module",
|
||||
"hooks.internal.handlers[].export": "Internal Hook Export",
|
||||
"hooks.internal.entries": "Internal Hook Entries",
|
||||
"hooks.internal.load": "Internal Hook Loader",
|
||||
"hooks.internal.load.extraDirs": "Internal Hook Extra Directories",
|
||||
"hooks.internal.installs": "Internal Hook Install Records",
|
||||
web: "Web Channel",
|
||||
"web.enabled": "Web Channel Enabled",
|
||||
"web.heartbeatSeconds": "Web Channel Heartbeat Interval (sec)",
|
||||
"web.reconnect": "Web Channel Reconnect Policy",
|
||||
"web.reconnect.initialMs": "Web Reconnect Initial Delay (ms)",
|
||||
"web.reconnect.maxMs": "Web Reconnect Max Delay (ms)",
|
||||
"web.reconnect.factor": "Web Reconnect Backoff Factor",
|
||||
"web.reconnect.jitter": "Web Reconnect Jitter",
|
||||
"web.reconnect.maxAttempts": "Web Reconnect Max Attempts",
|
||||
discovery: "Discovery",
|
||||
"discovery.wideArea": "Wide-area Discovery",
|
||||
"discovery.wideArea.enabled": "Wide-area Discovery Enabled",
|
||||
"discovery.mdns": "mDNS Discovery",
|
||||
canvasHost: "Canvas Host",
|
||||
"canvasHost.enabled": "Canvas Host Enabled",
|
||||
"canvasHost.root": "Canvas Host Root Directory",
|
||||
"canvasHost.port": "Canvas Host Port",
|
||||
"canvasHost.liveReload": "Canvas Host Live Reload",
|
||||
talk: "Talk",
|
||||
"talk.voiceId": "Talk Voice ID",
|
||||
"talk.voiceAliases": "Talk Voice Aliases",
|
||||
"talk.modelId": "Talk Model ID",
|
||||
"talk.outputFormat": "Talk Output Format",
|
||||
"talk.interruptOnSpeech": "Talk Interrupt on Speech",
|
||||
messages: "Messages",
|
||||
"messages.messagePrefix": "Inbound Message Prefix",
|
||||
"messages.responsePrefix": "Outbound Response Prefix",
|
||||
"messages.groupChat": "Group Chat Rules",
|
||||
"messages.groupChat.mentionPatterns": "Group Mention Patterns",
|
||||
"messages.groupChat.historyLimit": "Group History Limit",
|
||||
"messages.queue": "Inbound Queue",
|
||||
"messages.queue.mode": "Queue Mode",
|
||||
"messages.queue.byChannel": "Queue Mode by Channel",
|
||||
"messages.queue.debounceMs": "Queue Debounce (ms)",
|
||||
"messages.queue.debounceMsByChannel": "Queue Debounce by Channel (ms)",
|
||||
"messages.queue.cap": "Queue Capacity",
|
||||
"messages.queue.drop": "Queue Drop Strategy",
|
||||
"messages.inbound": "Inbound Debounce",
|
||||
"messages.suppressToolErrors": "Suppress Tool Error Warnings",
|
||||
"messages.ackReaction": "Ack Reaction Emoji",
|
||||
"messages.ackReactionScope": "Ack Reaction Scope",
|
||||
"messages.removeAckAfterReply": "Remove Ack Reaction After Reply",
|
||||
"messages.statusReactions": "Status Reactions",
|
||||
"messages.statusReactions.enabled": "Enable Status Reactions",
|
||||
"messages.statusReactions.emojis": "Status Reaction Emojis",
|
||||
"messages.statusReactions.timing": "Status Reaction Timing",
|
||||
"messages.inbound.debounceMs": "Inbound Message Debounce (ms)",
|
||||
"messages.inbound.byChannel": "Inbound Debounce by Channel (ms)",
|
||||
"messages.tts": "Message Text-to-Speech",
|
||||
"talk.apiKey": "Talk API Key",
|
||||
channels: "Channels",
|
||||
"channels.defaults": "Channel Defaults",
|
||||
"channels.defaults.groupPolicy": "Default Group Policy",
|
||||
"channels.defaults.heartbeat": "Default Heartbeat Visibility",
|
||||
"channels.defaults.heartbeat.showOk": "Heartbeat Show OK",
|
||||
"channels.defaults.heartbeat.showAlerts": "Heartbeat Show Alerts",
|
||||
"channels.defaults.heartbeat.useIndicator": "Heartbeat Use Indicator",
|
||||
"channels.whatsapp": "WhatsApp",
|
||||
"channels.telegram": "Telegram",
|
||||
"channels.telegram.customCommands": "Telegram Custom Commands",
|
||||
@@ -270,6 +603,9 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
...IRC_FIELD_LABELS,
|
||||
"channels.telegram.botToken": "Telegram Bot Token",
|
||||
"channels.telegram.dmPolicy": "Telegram DM Policy",
|
||||
"channels.telegram.configWrites": "Telegram Config Writes",
|
||||
"channels.telegram.commands.native": "Telegram Native Commands",
|
||||
"channels.telegram.commands.nativeSkills": "Telegram Native Skill Commands",
|
||||
"channels.telegram.streaming": "Telegram Streaming Mode",
|
||||
"channels.telegram.retry.attempts": "Telegram Retry Attempts",
|
||||
"channels.telegram.retry.minDelayMs": "Telegram Retry Min Delay (ms)",
|
||||
@@ -281,11 +617,20 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.whatsapp.dmPolicy": "WhatsApp DM Policy",
|
||||
"channels.whatsapp.selfChatMode": "WhatsApp Self-Phone Mode",
|
||||
"channels.whatsapp.debounceMs": "WhatsApp Message Debounce (ms)",
|
||||
"channels.whatsapp.configWrites": "WhatsApp Config Writes",
|
||||
"channels.signal.dmPolicy": "Signal DM Policy",
|
||||
"channels.signal.configWrites": "Signal Config Writes",
|
||||
"channels.imessage.dmPolicy": "iMessage DM Policy",
|
||||
"channels.imessage.configWrites": "iMessage Config Writes",
|
||||
"channels.bluebubbles.dmPolicy": "BlueBubbles DM Policy",
|
||||
"channels.msteams.configWrites": "MS Teams Config Writes",
|
||||
"channels.irc.configWrites": "IRC Config Writes",
|
||||
"channels.discord.dmPolicy": "Discord DM Policy",
|
||||
"channels.discord.dm.policy": "Discord DM Policy",
|
||||
"channels.discord.configWrites": "Discord Config Writes",
|
||||
"channels.discord.proxy": "Discord Proxy URL",
|
||||
"channels.discord.commands.native": "Discord Native Commands",
|
||||
"channels.discord.commands.nativeSkills": "Discord Native Skill Commands",
|
||||
"channels.discord.streaming": "Discord Streaming Mode",
|
||||
"channels.discord.streamMode": "Discord Stream Mode (Legacy)",
|
||||
"channels.discord.draftChunk.minChars": "Discord Draft Chunk Min Chars",
|
||||
@@ -304,6 +649,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.discord.intents.guildMembers": "Discord Guild Members Intent",
|
||||
"channels.discord.voice.enabled": "Discord Voice Enabled",
|
||||
"channels.discord.voice.autoJoin": "Discord Voice Auto-Join",
|
||||
"channels.discord.voice.tts": "Discord Voice Text-to-Speech",
|
||||
"channels.discord.pluralkit.enabled": "Discord PluralKit Enabled",
|
||||
"channels.discord.pluralkit.token": "Discord PluralKit Token",
|
||||
"channels.discord.activity": "Discord Presence Activity",
|
||||
@@ -312,6 +658,9 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.discord.activityUrl": "Discord Presence Activity URL",
|
||||
"channels.slack.dm.policy": "Slack DM Policy",
|
||||
"channels.slack.dmPolicy": "Slack DM Policy",
|
||||
"channels.slack.configWrites": "Slack Config Writes",
|
||||
"channels.slack.commands.native": "Slack Native Commands",
|
||||
"channels.slack.commands.nativeSkills": "Slack Native Skill Commands",
|
||||
"channels.slack.allowBots": "Slack Allow Bot Messages",
|
||||
"channels.discord.token": "Discord Bot Token",
|
||||
"channels.slack.botToken": "Slack Bot Token",
|
||||
@@ -326,6 +675,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.slack.thread.initialHistoryLimit": "Slack Thread Initial History Limit",
|
||||
"channels.mattermost.botToken": "Mattermost Bot Token",
|
||||
"channels.mattermost.baseUrl": "Mattermost Base URL",
|
||||
"channels.mattermost.configWrites": "Mattermost Config Writes",
|
||||
"channels.mattermost.chatmode": "Mattermost Chat Mode",
|
||||
"channels.mattermost.oncharPrefixes": "Mattermost Onchar Prefixes",
|
||||
"channels.mattermost.requireMention": "Mattermost Require Mention",
|
||||
@@ -333,15 +683,23 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.imessage.cliPath": "iMessage CLI Path",
|
||||
"agents.list[].skills": "Agent Skill Filter",
|
||||
"agents.list[].identity.avatar": "Agent Avatar",
|
||||
"agents.list[].heartbeat.suppressToolErrorWarnings":
|
||||
"Agent Heartbeat Suppress Tool Error Warnings",
|
||||
"agents.list[].sandbox.browser.network": "Agent Sandbox Browser Network",
|
||||
"agents.list[].sandbox.browser.cdpSourceRange": "Agent Sandbox Browser CDP Source Port Range",
|
||||
"discovery.mdns.mode": "mDNS Discovery Mode",
|
||||
plugins: "Plugins",
|
||||
"plugins.enabled": "Enable Plugins",
|
||||
"plugins.allow": "Plugin Allowlist",
|
||||
"plugins.deny": "Plugin Denylist",
|
||||
"plugins.load": "Plugin Loader",
|
||||
"plugins.load.paths": "Plugin Load Paths",
|
||||
"plugins.slots": "Plugin Slots",
|
||||
"plugins.slots.memory": "Memory Plugin",
|
||||
"plugins.entries": "Plugin Entries",
|
||||
"plugins.entries.*.enabled": "Plugin Enabled",
|
||||
"plugins.entries.*.apiKey": "Plugin API Key",
|
||||
"plugins.entries.*.env": "Plugin Environment Variables",
|
||||
"plugins.entries.*.config": "Plugin Config",
|
||||
"plugins.installs": "Plugin Install Records",
|
||||
"plugins.installs.*.source": "Plugin Install Source",
|
||||
|
||||
46
src/config/schema.tags.test.ts
Normal file
46
src/config/schema.tags.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildConfigSchema } from "./schema.js";
|
||||
import { applyDerivedTags, CONFIG_TAGS, deriveTagsForPath } from "./schema.tags.js";
|
||||
|
||||
describe("config schema tags", () => {
|
||||
it("derives security/auth tags for credential paths", () => {
|
||||
const tags = deriveTagsForPath("gateway.auth.token");
|
||||
expect(tags).toContain("security");
|
||||
expect(tags).toContain("auth");
|
||||
});
|
||||
|
||||
it("derives tools/performance tags for web fetch timeout paths", () => {
|
||||
const tags = deriveTagsForPath("tools.web.fetch.timeoutSeconds");
|
||||
expect(tags).toContain("tools");
|
||||
expect(tags).toContain("performance");
|
||||
});
|
||||
|
||||
it("keeps tags in the allowed taxonomy", () => {
|
||||
const withTags = applyDerivedTags({
|
||||
"gateway.auth.token": {},
|
||||
"tools.web.fetch.timeoutSeconds": {},
|
||||
"channels.slack.accounts.*.token": {},
|
||||
});
|
||||
const allowed = new Set<string>(CONFIG_TAGS);
|
||||
for (const hint of Object.values(withTags)) {
|
||||
for (const tag of hint.tags ?? []) {
|
||||
expect(allowed.has(tag)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("covers core/built-in config paths with tags", () => {
|
||||
const schema = buildConfigSchema();
|
||||
const allowed = new Set<string>(CONFIG_TAGS);
|
||||
for (const [key, hint] of Object.entries(schema.uiHints)) {
|
||||
if (!key.includes(".")) {
|
||||
continue;
|
||||
}
|
||||
const tags = hint.tags ?? [];
|
||||
expect(tags.length, `expected tags for ${key}`).toBeGreaterThan(0);
|
||||
for (const tag of tags) {
|
||||
expect(allowed.has(tag), `unexpected tag ${tag} on ${key}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
180
src/config/schema.tags.ts
Normal file
180
src/config/schema.tags.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js";
|
||||
|
||||
export const CONFIG_TAGS = [
|
||||
"security",
|
||||
"auth",
|
||||
"network",
|
||||
"access",
|
||||
"privacy",
|
||||
"observability",
|
||||
"performance",
|
||||
"reliability",
|
||||
"storage",
|
||||
"models",
|
||||
"media",
|
||||
"automation",
|
||||
"channels",
|
||||
"tools",
|
||||
"advanced",
|
||||
] as const;
|
||||
|
||||
export type ConfigTag = (typeof CONFIG_TAGS)[number];
|
||||
|
||||
const TAG_PRIORITY: Record<ConfigTag, number> = {
|
||||
security: 0,
|
||||
auth: 1,
|
||||
access: 2,
|
||||
network: 3,
|
||||
privacy: 4,
|
||||
observability: 5,
|
||||
reliability: 6,
|
||||
performance: 7,
|
||||
storage: 8,
|
||||
models: 9,
|
||||
media: 10,
|
||||
automation: 11,
|
||||
channels: 12,
|
||||
tools: 13,
|
||||
advanced: 14,
|
||||
};
|
||||
|
||||
const TAG_OVERRIDES: Record<string, ConfigTag[]> = {
|
||||
"gateway.auth.token": ["security", "auth", "access", "network"],
|
||||
"gateway.auth.password": ["security", "auth", "access", "network"],
|
||||
"gateway.controlUi.dangerouslyDisableDeviceAuth": ["security", "access", "network", "advanced"],
|
||||
"gateway.controlUi.allowInsecureAuth": ["security", "access", "network", "advanced"],
|
||||
"tools.exec.applyPatch.workspaceOnly": ["tools", "security", "access", "advanced"],
|
||||
};
|
||||
|
||||
const PREFIX_RULES: Array<{ prefix: string; tags: ConfigTag[] }> = [
|
||||
{ prefix: "channels.", tags: ["channels", "network"] },
|
||||
{ prefix: "tools.", tags: ["tools"] },
|
||||
{ prefix: "gateway.", tags: ["network"] },
|
||||
{ prefix: "nodehost.", tags: ["network"] },
|
||||
{ prefix: "discovery.", tags: ["network"] },
|
||||
{ prefix: "auth.", tags: ["auth", "access"] },
|
||||
{ prefix: "memory.", tags: ["storage"] },
|
||||
{ prefix: "models.", tags: ["models"] },
|
||||
{ prefix: "diagnostics.", tags: ["observability"] },
|
||||
{ prefix: "logging.", tags: ["observability"] },
|
||||
{ prefix: "cron.", tags: ["automation"] },
|
||||
{ prefix: "talk.", tags: ["media"] },
|
||||
{ prefix: "audio.", tags: ["media"] },
|
||||
];
|
||||
|
||||
const KEYWORD_RULES: Array<{ pattern: RegExp; tags: ConfigTag[] }> = [
|
||||
{ pattern: /(token|password|secret|api[_.-]?key|tlsfingerprint)/i, tags: ["security", "auth"] },
|
||||
{ pattern: /(allow|deny|owner|permission|policy|access)/i, tags: ["access"] },
|
||||
{ pattern: /(timeout|debounce|interval|concurrency|max|limit|cachettl)/i, tags: ["performance"] },
|
||||
{ pattern: /(retry|backoff|fallback|circuit|health|reload|probe)/i, tags: ["reliability"] },
|
||||
{ pattern: /(path|dir|file|store|db|session|cache)/i, tags: ["storage"] },
|
||||
{ pattern: /(telemetry|trace|metrics|logs|diagnostic)/i, tags: ["observability"] },
|
||||
{ pattern: /(experimental|dangerously|insecure)/i, tags: ["advanced", "security"] },
|
||||
{ pattern: /(privacy|redact|sanitize|anonym|pseudonym)/i, tags: ["privacy"] },
|
||||
];
|
||||
|
||||
const MODEL_PATH_PATTERN = /(^|\.)(model|models|modelid|imagemodel)(\.|$)/i;
|
||||
const MEDIA_PATH_PATTERN = /(tools\.media\.|^audio\.|^talk\.|image|video|stt|tts)/i;
|
||||
const AUTOMATION_PATH_PATTERN = /(cron|heartbeat|schedule|onstart|watchdebounce)/i;
|
||||
const AUTH_KEYWORD_PATTERN = /(token|password|secret|api[_.-]?key|credential|oauth)/i;
|
||||
|
||||
function normalizeTag(tag: string): ConfigTag | null {
|
||||
const normalized = tag.trim().toLowerCase() as ConfigTag;
|
||||
return CONFIG_TAGS.includes(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
function normalizeTags(tags: ReadonlyArray<string>): ConfigTag[] {
|
||||
const out = new Set<ConfigTag>();
|
||||
for (const tag of tags) {
|
||||
const normalized = normalizeTag(tag);
|
||||
if (normalized) {
|
||||
out.add(normalized);
|
||||
}
|
||||
}
|
||||
return [...out].toSorted((a, b) => TAG_PRIORITY[a] - TAG_PRIORITY[b]);
|
||||
}
|
||||
|
||||
function patternToRegExp(pattern: string): RegExp {
|
||||
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^.]+");
|
||||
return new RegExp(`^${escaped}$`, "i");
|
||||
}
|
||||
|
||||
function resolveOverride(path: string): ConfigTag[] | undefined {
|
||||
const direct = TAG_OVERRIDES[path];
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
for (const [pattern, tags] of Object.entries(TAG_OVERRIDES)) {
|
||||
if (!pattern.includes("*")) {
|
||||
continue;
|
||||
}
|
||||
if (patternToRegExp(pattern).test(path)) {
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function addTags(set: Set<ConfigTag>, tags: ReadonlyArray<ConfigTag>): void {
|
||||
for (const tag of tags) {
|
||||
set.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
export function deriveTagsForPath(path: string, hint?: ConfigUiHint): ConfigTag[] {
|
||||
const lowerPath = path.toLowerCase();
|
||||
const override = resolveOverride(path);
|
||||
if (override) {
|
||||
return normalizeTags(override);
|
||||
}
|
||||
|
||||
const tags = new Set<ConfigTag>();
|
||||
for (const rule of PREFIX_RULES) {
|
||||
if (lowerPath.startsWith(rule.prefix)) {
|
||||
addTags(tags, rule.tags);
|
||||
}
|
||||
}
|
||||
|
||||
for (const rule of KEYWORD_RULES) {
|
||||
if (rule.pattern.test(path)) {
|
||||
addTags(tags, rule.tags);
|
||||
}
|
||||
}
|
||||
|
||||
if (MODEL_PATH_PATTERN.test(path)) {
|
||||
tags.add("models");
|
||||
}
|
||||
if (MEDIA_PATH_PATTERN.test(path)) {
|
||||
tags.add("media");
|
||||
}
|
||||
if (AUTOMATION_PATH_PATTERN.test(path)) {
|
||||
tags.add("automation");
|
||||
}
|
||||
|
||||
if (hint?.sensitive) {
|
||||
tags.add("security");
|
||||
if (AUTH_KEYWORD_PATTERN.test(path)) {
|
||||
tags.add("auth");
|
||||
}
|
||||
}
|
||||
if (hint?.advanced) {
|
||||
tags.add("advanced");
|
||||
}
|
||||
|
||||
if (tags.size === 0) {
|
||||
tags.add("advanced");
|
||||
}
|
||||
|
||||
return normalizeTags([...tags]);
|
||||
}
|
||||
|
||||
export function applyDerivedTags(hints: ConfigUiHints): ConfigUiHints {
|
||||
const next: ConfigUiHints = {};
|
||||
for (const [path, hint] of Object.entries(hints)) {
|
||||
const existingTags = Array.isArray(hint?.tags) ? hint.tags : [];
|
||||
const derivedTags = deriveTagsForPath(path, hint);
|
||||
const tags = normalizeTags([...derivedTags, ...existingTags]);
|
||||
next[path] = { ...hint, tags };
|
||||
}
|
||||
return next;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { CHANNEL_IDS } from "../channels/registry.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js";
|
||||
import { applySensitiveHints, buildBaseHints, mapSensitivePaths } from "./schema.hints.js";
|
||||
import { applyDerivedTags } from "./schema.tags.js";
|
||||
import { OpenClawSchema } from "./zod-schema.js";
|
||||
|
||||
export type { ConfigUiHint, ConfigUiHints } from "./schema.hints.js";
|
||||
@@ -75,7 +76,7 @@ export type PluginUiMetadata = {
|
||||
description?: string;
|
||||
configUiHints?: Record<
|
||||
string,
|
||||
Pick<ConfigUiHint, "label" | "help" | "advanced" | "sensitive" | "placeholder">
|
||||
Pick<ConfigUiHint, "label" | "help" | "tags" | "advanced" | "sensitive" | "placeholder">
|
||||
>;
|
||||
configSchema?: JsonSchemaNode;
|
||||
};
|
||||
@@ -327,7 +328,7 @@ function buildBaseConfigSchema(): ConfigSchemaResponse {
|
||||
unrepresentable: "any",
|
||||
});
|
||||
schema.title = "OpenClawConfig";
|
||||
const hints = mapSensitivePaths(OpenClawSchema, "", buildBaseHints());
|
||||
const hints = applyDerivedTags(mapSensitivePaths(OpenClawSchema, "", buildBaseHints()));
|
||||
const next = {
|
||||
schema: stripChannelSchema(schema),
|
||||
uiHints: hints,
|
||||
@@ -357,7 +358,9 @@ export function buildConfigSchema(params?: {
|
||||
plugins,
|
||||
channels,
|
||||
);
|
||||
const mergedHints = applySensitiveHints(mergedWithoutSensitiveHints, extensionHintKeys);
|
||||
const mergedHints = applyDerivedTags(
|
||||
applySensitiveHints(mergedWithoutSensitiveHints, extensionHintKeys),
|
||||
);
|
||||
const mergedSchema = applyChannelSchemas(applyPluginSchemas(base.schema, plugins), channels);
|
||||
return {
|
||||
...base,
|
||||
|
||||
@@ -41,6 +41,7 @@ export const ConfigUiHintSchema = Type.Object(
|
||||
{
|
||||
label: Type.Optional(Type.String()),
|
||||
help: Type.Optional(Type.String()),
|
||||
tags: Type.Optional(Type.Array(Type.String())),
|
||||
group: Type.Optional(Type.String()),
|
||||
order: Type.Optional(Type.Integer()),
|
||||
advanced: Type.Optional(Type.Boolean()),
|
||||
|
||||
@@ -2,6 +2,8 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import type { AuthRateLimiter } from "../../auth-rate-limit.js";
|
||||
import { resolveConnectAuthDecision, type ConnectAuthState } from "./auth-context.js";
|
||||
|
||||
type VerifyDeviceTokenFn = Parameters<typeof resolveConnectAuthDecision>[0]["verifyDeviceToken"];
|
||||
|
||||
function createRateLimiter(params?: { allowed?: boolean; retryAfterMs?: number }): {
|
||||
limiter: AuthRateLimiter;
|
||||
reset: ReturnType<typeof vi.fn>;
|
||||
@@ -35,7 +37,7 @@ function createBaseState(overrides?: Partial<ConnectAuthState>): ConnectAuthStat
|
||||
}
|
||||
|
||||
async function resolveDeviceTokenDecision(params: {
|
||||
verifyDeviceToken: ReturnType<typeof vi.fn>;
|
||||
verifyDeviceToken: VerifyDeviceTokenFn;
|
||||
stateOverrides?: Partial<ConnectAuthState>;
|
||||
rateLimiter?: AuthRateLimiter;
|
||||
clientIp?: string;
|
||||
@@ -54,7 +56,7 @@ async function resolveDeviceTokenDecision(params: {
|
||||
|
||||
describe("resolveConnectAuthDecision", () => {
|
||||
it("keeps shared-secret mismatch when fallback device-token check fails", async () => {
|
||||
const verifyDeviceToken = vi.fn(async () => ({ ok: false }));
|
||||
const verifyDeviceToken = vi.fn<VerifyDeviceTokenFn>(async () => ({ ok: false }));
|
||||
const decision = await resolveConnectAuthDecision({
|
||||
state: createBaseState(),
|
||||
hasDeviceIdentity: true,
|
||||
@@ -69,7 +71,7 @@ describe("resolveConnectAuthDecision", () => {
|
||||
});
|
||||
|
||||
it("reports explicit device-token mismatches as device_token_mismatch", async () => {
|
||||
const verifyDeviceToken = vi.fn(async () => ({ ok: false }));
|
||||
const verifyDeviceToken = vi.fn<VerifyDeviceTokenFn>(async () => ({ ok: false }));
|
||||
const decision = await resolveConnectAuthDecision({
|
||||
state: createBaseState({
|
||||
deviceTokenCandidateSource: "explicit-device-token",
|
||||
@@ -86,7 +88,7 @@ describe("resolveConnectAuthDecision", () => {
|
||||
|
||||
it("accepts valid device tokens and marks auth method as device-token", async () => {
|
||||
const rateLimiter = createRateLimiter();
|
||||
const verifyDeviceToken = vi.fn(async () => ({ ok: true }));
|
||||
const verifyDeviceToken = vi.fn<VerifyDeviceTokenFn>(async () => ({ ok: true }));
|
||||
const decision = await resolveDeviceTokenDecision({
|
||||
verifyDeviceToken,
|
||||
rateLimiter: rateLimiter.limiter,
|
||||
@@ -100,7 +102,7 @@ describe("resolveConnectAuthDecision", () => {
|
||||
|
||||
it("returns rate-limited auth result without verifying device token", async () => {
|
||||
const rateLimiter = createRateLimiter({ allowed: false, retryAfterMs: 60_000 });
|
||||
const verifyDeviceToken = vi.fn(async () => ({ ok: true }));
|
||||
const verifyDeviceToken = vi.fn<VerifyDeviceTokenFn>(async () => ({ ok: true }));
|
||||
const decision = await resolveDeviceTokenDecision({
|
||||
verifyDeviceToken,
|
||||
rateLimiter: rateLimiter.limiter,
|
||||
@@ -113,7 +115,7 @@ describe("resolveConnectAuthDecision", () => {
|
||||
});
|
||||
|
||||
it("returns the original decision when device fallback does not apply", async () => {
|
||||
const verifyDeviceToken = vi.fn(async () => ({ ok: true }));
|
||||
const verifyDeviceToken = vi.fn<VerifyDeviceTokenFn>(async () => ({ ok: true }));
|
||||
const decision = await resolveConnectAuthDecision({
|
||||
state: createBaseState({
|
||||
authResult: { ok: true, method: "token" },
|
||||
|
||||
@@ -29,6 +29,7 @@ export type PluginLogger = {
|
||||
export type PluginConfigUiHint = {
|
||||
label?: string;
|
||||
help?: string;
|
||||
tags?: string[];
|
||||
advanced?: boolean;
|
||||
sensitive?: boolean;
|
||||
placeholder?: string;
|
||||
|
||||
@@ -72,12 +72,19 @@ export function resolveSlackThreadTs(params: {
|
||||
incomingThreadTs: string | undefined;
|
||||
messageTs: string | undefined;
|
||||
hasReplied: boolean;
|
||||
isThreadReply?: boolean;
|
||||
}): string | undefined {
|
||||
const isThreadReply =
|
||||
params.isThreadReply ??
|
||||
(typeof params.incomingThreadTs === "string" &&
|
||||
params.incomingThreadTs.length > 0 &&
|
||||
params.incomingThreadTs !== params.messageTs);
|
||||
const planner = createSlackReplyReferencePlanner({
|
||||
replyToMode: params.replyToMode,
|
||||
incomingThreadTs: params.incomingThreadTs,
|
||||
messageTs: params.messageTs,
|
||||
hasReplied: params.hasReplied,
|
||||
isThreadReply,
|
||||
});
|
||||
return planner.use();
|
||||
}
|
||||
|
||||
@@ -59,13 +59,13 @@ function createHarness(params?: {
|
||||
|
||||
describe("tui command handlers", () => {
|
||||
it("renders the sending indicator before chat.send resolves", async () => {
|
||||
let resolveSend: ((value: { runId: string }) => void) | null = null;
|
||||
const sendChat = vi.fn(
|
||||
() =>
|
||||
new Promise<{ runId: string }>((resolve) => {
|
||||
resolveSend = resolve;
|
||||
}),
|
||||
);
|
||||
let resolveSend: (value: { runId: string }) => void = () => {
|
||||
throw new Error("sendChat promise resolver was not initialized");
|
||||
};
|
||||
const sendPromise = new Promise<{ runId: string }>((resolve) => {
|
||||
resolveSend = (value) => resolve(value);
|
||||
});
|
||||
const sendChat = vi.fn(() => sendPromise);
|
||||
const setActivityStatus = vi.fn();
|
||||
|
||||
const { handleCommand, requestRender } = createHarness({
|
||||
@@ -81,10 +81,7 @@ describe("tui command handlers", () => {
|
||||
const renderOrders = requestRender.mock.invocationCallOrder;
|
||||
expect(renderOrders.some((order) => order > sendingOrder)).toBe(true);
|
||||
|
||||
if (typeof resolveSend !== "function") {
|
||||
throw new Error("expected sendChat to be pending");
|
||||
}
|
||||
(resolveSend as (value: { runId: string }) => void)({ runId: "r1" });
|
||||
resolveSend({ runId: "r1" });
|
||||
await pending;
|
||||
expect(setActivityStatus).toHaveBeenCalledWith("waiting");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user