feat: add provider-specific tool policies

This commit is contained in:
Peter Steinberger
2026-01-13 09:59:21 +00:00
parent 512dbedee3
commit fa8d9b9189
7 changed files with 121 additions and 16 deletions

View File

@@ -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,
};
}

View File

@@ -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;

View File

@@ -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];