feat: add provider-specific tool policies
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
- Sandbox: preserve configured PATH for `docker exec` so custom tools remain available. (#873) — thanks @akonyer.
|
||||
- Slack: respect `channels.slack.requireMention` default when resolving channel mention gating. (#850) — thanks @evalexpr.
|
||||
- Telegram: aggregate split inbound messages into one prompt (reduces “one reply per fragment”).
|
||||
- Auto-reply: treat trailing `NO_REPLY` tokens as silent replies.
|
||||
|
||||
## 2026.1.14
|
||||
|
||||
@@ -78,6 +79,7 @@
|
||||
- Memory: new `clawdbot memory` CLI plus `memory_search`/`memory_get` tools with snippets + line ranges; index stored under `~/.clawdbot/memory/{agentId}.sqlite` with watch-on-by-default.
|
||||
- Agents: strengthen memory recall guidance; make workspace bootstrap truncation configurable (default 20k) with warnings; add default sub-agent model config.
|
||||
- Tools/Sandbox: add tool profiles + group shorthands; support tool-policy groups in `tools.sandbox.tools`; drop legacy `memory` shorthand; allow Docker bind mounts via `docker.binds`. (#790) — thanks @akonyer.
|
||||
- Tools: add provider/model-specific tool policy overrides (`tools.byProvider`) to trim tool exposure per provider.
|
||||
- Tools: add browser `scrollintoview` action; allow Claude/Gemini tool param aliases; allow thinking `xhigh` for GPT-5.2/Codex with safe downgrades. (#793) — thanks @hsrvc; (#444) — thanks @grp06.
|
||||
- Gateway/CLI: add Tailscale binary discovery, custom bind mode, and probe auth retry; add `clawdbot dashboard` auto-open flow; default native slash commands to `"auto"` with per-provider overrides. (#740) — thanks @jeffersonwarrior.
|
||||
- Auth/Onboarding: add Chutes OAuth (PKCE + refresh + onboarding choice); normalize API key inputs; default TUI onboarding to `deliver: false`. (#726) — thanks @FrieSei; (#791) — thanks @roshanasingh4.
|
||||
@@ -90,6 +92,7 @@
|
||||
|
||||
### Fixes
|
||||
- Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds.
|
||||
- Tools: apply global tool allow/deny even when agent-specific tool policy is set.
|
||||
- Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles. (#822) — thanks @sebslight; (#705) — thanks @TAGOOZ.
|
||||
- Auth: drop invalid auth profiles from ordering so environment keys can still be used for providers like MiniMax.
|
||||
- Gemini: normalize Gemini 3 ids to preview variants; strip Gemini CLI tool call/response ids; downgrade missing `thought_signature`; strip Claude `msg_*` thought_signature fields to avoid base64 decode errors. (#795) — thanks @thewilloftheshadow; (#783) — thanks @ananth-vardhan-cn; (#793) — thanks @hsrvc; (#805) — thanks @marcmarg.
|
||||
|
||||
@@ -41,24 +41,92 @@ export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolP
|
||||
return tools.filter((tool) => isToolAllowedByPolicyName(tool.name, policy));
|
||||
}
|
||||
|
||||
type ToolPolicyConfig = {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
profile?: string;
|
||||
};
|
||||
|
||||
function pickToolPolicy(config?: ToolPolicyConfig): SandboxToolPolicy | undefined {
|
||||
if (!config) return undefined;
|
||||
const allow = Array.isArray(config.allow) ? config.allow : undefined;
|
||||
const deny = Array.isArray(config.deny) ? config.deny : undefined;
|
||||
if (!allow && !deny) return undefined;
|
||||
return { allow, deny };
|
||||
}
|
||||
|
||||
function normalizeProviderKey(value: string): string {
|
||||
return value.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function resolveProviderToolPolicy(params: {
|
||||
byProvider?: Record<string, ToolPolicyConfig>;
|
||||
modelProvider?: string;
|
||||
modelId?: string;
|
||||
}): ToolPolicyConfig | undefined {
|
||||
const provider = params.modelProvider?.trim();
|
||||
if (!provider || !params.byProvider) return undefined;
|
||||
|
||||
const entries = Object.entries(params.byProvider);
|
||||
if (entries.length === 0) return undefined;
|
||||
|
||||
const lookup = new Map<string, ToolPolicyConfig>();
|
||||
for (const [key, value] of entries) {
|
||||
const normalized = normalizeProviderKey(key);
|
||||
if (!normalized) continue;
|
||||
lookup.set(normalized, value);
|
||||
}
|
||||
|
||||
const normalizedProvider = normalizeProviderKey(provider);
|
||||
const rawModelId = params.modelId?.trim().toLowerCase();
|
||||
const fullModelId =
|
||||
rawModelId && !rawModelId.includes("/")
|
||||
? `${normalizedProvider}/${rawModelId}`
|
||||
: rawModelId;
|
||||
|
||||
const candidates = [
|
||||
...(fullModelId ? [fullModelId] : []),
|
||||
normalizedProvider,
|
||||
];
|
||||
|
||||
for (const key of candidates) {
|
||||
const match = lookup.get(key);
|
||||
if (match) return match;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveEffectiveToolPolicy(params: {
|
||||
config?: ClawdbotConfig;
|
||||
sessionKey?: string;
|
||||
modelProvider?: string;
|
||||
modelId?: string;
|
||||
}) {
|
||||
const agentId = params.sessionKey ? resolveAgentIdFromSessionKey(params.sessionKey) : undefined;
|
||||
const agentConfig =
|
||||
params.config && agentId ? resolveAgentConfig(params.config, agentId) : undefined;
|
||||
const agentTools = agentConfig?.tools;
|
||||
const hasAgentToolPolicy =
|
||||
Array.isArray(agentTools?.allow) ||
|
||||
Array.isArray(agentTools?.deny) ||
|
||||
typeof agentTools?.profile === "string";
|
||||
const globalTools = params.config?.tools;
|
||||
|
||||
const profile = agentTools?.profile ?? globalTools?.profile;
|
||||
const providerPolicy = resolveProviderToolPolicy({
|
||||
byProvider: globalTools?.byProvider,
|
||||
modelProvider: params.modelProvider,
|
||||
modelId: params.modelId,
|
||||
});
|
||||
const agentProviderPolicy = resolveProviderToolPolicy({
|
||||
byProvider: agentTools?.byProvider,
|
||||
modelProvider: params.modelProvider,
|
||||
modelId: params.modelId,
|
||||
});
|
||||
return {
|
||||
agentId,
|
||||
policy: hasAgentToolPolicy ? agentTools : globalTools,
|
||||
globalPolicy: pickToolPolicy(globalTools),
|
||||
globalProviderPolicy: pickToolPolicy(providerPolicy),
|
||||
agentPolicy: pickToolPolicy(agentTools),
|
||||
agentProviderPolicy: pickToolPolicy(agentProviderPolicy),
|
||||
profile,
|
||||
providerProfile: agentProviderPolicy?.profile ?? providerPolicy?.profile,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -111,21 +111,33 @@ export function createClawdbotCodingTools(options?: {
|
||||
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
||||
const {
|
||||
agentId,
|
||||
policy: effectiveToolsPolicy,
|
||||
globalPolicy,
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
profile,
|
||||
providerProfile,
|
||||
} = resolveEffectiveToolPolicy({
|
||||
config: options?.config,
|
||||
sessionKey: options?.sessionKey,
|
||||
modelProvider: options?.modelProvider,
|
||||
modelId: options?.modelId,
|
||||
});
|
||||
const profilePolicy = resolveToolProfilePolicy(profile);
|
||||
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
||||
const scopeKey =
|
||||
options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||
const subagentPolicy =
|
||||
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
||||
? resolveSubagentToolPolicy(options.config)
|
||||
: undefined;
|
||||
const allowBackground = isToolAllowedByPolicies("process", [
|
||||
profilePolicy,
|
||||
effectiveToolsPolicy,
|
||||
providerProfilePolicy,
|
||||
globalPolicy,
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
sandbox?.tools,
|
||||
subagentPolicy,
|
||||
]);
|
||||
@@ -228,11 +240,27 @@ export function createClawdbotCodingTools(options?: {
|
||||
hasRepliedRef: options?.hasRepliedRef,
|
||||
}),
|
||||
];
|
||||
const toolsFiltered = profilePolicy ? filterToolsByPolicy(tools, profilePolicy) : tools;
|
||||
const policyFiltered = effectiveToolsPolicy
|
||||
? filterToolsByPolicy(toolsFiltered, effectiveToolsPolicy)
|
||||
const toolsFiltered = profilePolicy
|
||||
? filterToolsByPolicy(tools, profilePolicy)
|
||||
: tools;
|
||||
const providerProfileFiltered = providerProfilePolicy
|
||||
? filterToolsByPolicy(toolsFiltered, providerProfilePolicy)
|
||||
: toolsFiltered;
|
||||
const sandboxed = sandbox ? filterToolsByPolicy(policyFiltered, sandbox.tools) : policyFiltered;
|
||||
const globalFiltered = globalPolicy
|
||||
? filterToolsByPolicy(providerProfileFiltered, globalPolicy)
|
||||
: providerProfileFiltered;
|
||||
const globalProviderFiltered = globalProviderPolicy
|
||||
? filterToolsByPolicy(globalFiltered, globalProviderPolicy)
|
||||
: globalFiltered;
|
||||
const agentFiltered = agentPolicy
|
||||
? filterToolsByPolicy(globalProviderFiltered, agentPolicy)
|
||||
: globalProviderFiltered;
|
||||
const agentProviderFiltered = agentProviderPolicy
|
||||
? filterToolsByPolicy(agentFiltered, agentProviderPolicy)
|
||||
: agentFiltered;
|
||||
const sandboxed = sandbox
|
||||
? filterToolsByPolicy(agentProviderFiltered, sandbox.tools)
|
||||
: agentProviderFiltered;
|
||||
const subagentFiltered = subagentPolicy
|
||||
? filterToolsByPolicy(sandboxed, subagentPolicy)
|
||||
: sandboxed;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This module scrubs/normalizes tool schemas to keep Gemini happy.
|
||||
|
||||
// Keywords that Cloud Code Assist API rejects (not compliant with their JSON Schema subset)
|
||||
const UNSUPPORTED_SCHEMA_KEYWORDS = new Set([
|
||||
export const GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS = new Set([
|
||||
"patternProperties",
|
||||
"additionalProperties",
|
||||
"$schema",
|
||||
@@ -254,7 +254,7 @@ function cleanSchemaForGeminiWithDefs(
|
||||
const cleaned: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (UNSUPPORTED_SCHEMA_KEYWORDS.has(key)) continue;
|
||||
if (GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS.has(key)) continue;
|
||||
|
||||
if (key === "const") {
|
||||
cleaned.enum = [value];
|
||||
|
||||
@@ -11,6 +11,7 @@ describe("createReplyDispatcher", () => {
|
||||
expect(dispatcher.sendFinalReply({ text: " " })).toBe(false);
|
||||
expect(dispatcher.sendFinalReply({ text: SILENT_REPLY_TOKEN })).toBe(false);
|
||||
expect(dispatcher.sendFinalReply({ text: `${SILENT_REPLY_TOKEN} -- nope` })).toBe(false);
|
||||
expect(dispatcher.sendFinalReply({ text: `interject.${SILENT_REPLY_TOKEN}` })).toBe(false);
|
||||
|
||||
await dispatcher.waitForIdle();
|
||||
expect(deliver).not.toHaveBeenCalled();
|
||||
|
||||
@@ -10,6 +10,9 @@ export function isSilentReplyText(
|
||||
token: string = SILENT_REPLY_TOKEN,
|
||||
): boolean {
|
||||
if (!text) return false;
|
||||
const re = new RegExp(`^\\s*${escapeRegExp(token)}(?=$|\\W)`);
|
||||
return re.test(text);
|
||||
const escaped = escapeRegExp(token);
|
||||
const prefix = new RegExp(`^\\s*${escaped}(?=$|\\W)`);
|
||||
if (prefix.test(text)) return true;
|
||||
const suffix = new RegExp(`\\b${escaped}\\b\\W*$`);
|
||||
return suffix.test(text);
|
||||
}
|
||||
|
||||
@@ -101,6 +101,8 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"tools.audio.transcription.timeoutSeconds": "Audio Transcription Timeout (sec)",
|
||||
"tools.profile": "Tool Profile",
|
||||
"agents.list[].tools.profile": "Agent Tool Profile",
|
||||
"tools.byProvider": "Tool Policy by Provider",
|
||||
"agents.list[].tools.byProvider": "Agent Tool Policy by Provider",
|
||||
"tools.exec.applyPatch.enabled": "Enable apply_patch",
|
||||
"tools.exec.applyPatch.allowModels": "apply_patch Model Allowlist",
|
||||
"gateway.controlUi.basePath": "Control UI Base Path",
|
||||
|
||||
Reference in New Issue
Block a user