feat(config): add tools.alsoAllow additive allowlist
This commit is contained in:
committed by
Pocket Clawd
parent
b9098f3401
commit
2ad3508a33
@@ -96,13 +96,22 @@ export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolP
|
|||||||
|
|
||||||
type ToolPolicyConfig = {
|
type ToolPolicyConfig = {
|
||||||
allow?: string[];
|
allow?: string[];
|
||||||
|
alsoAllow?: string[];
|
||||||
deny?: string[];
|
deny?: string[];
|
||||||
profile?: string;
|
profile?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function unionAllow(base?: string[], extra?: string[]) {
|
||||||
|
if (!Array.isArray(extra) || extra.length === 0) return base;
|
||||||
|
if (!Array.isArray(base) || base.length === 0) return base;
|
||||||
|
return Array.from(new Set([...base, ...extra]));
|
||||||
|
}
|
||||||
|
|
||||||
function pickToolPolicy(config?: ToolPolicyConfig): SandboxToolPolicy | undefined {
|
function pickToolPolicy(config?: ToolPolicyConfig): SandboxToolPolicy | undefined {
|
||||||
if (!config) return undefined;
|
if (!config) return undefined;
|
||||||
const allow = Array.isArray(config.allow) ? config.allow : undefined;
|
const allow = Array.isArray(config.allow)
|
||||||
|
? unionAllow(config.allow, config.alsoAllow)
|
||||||
|
: undefined;
|
||||||
const deny = Array.isArray(config.deny) ? config.deny : undefined;
|
const deny = Array.isArray(config.deny) ? config.deny : undefined;
|
||||||
if (!allow && !deny) return undefined;
|
if (!allow && !deny) return undefined;
|
||||||
return { allow, deny };
|
return { allow, deny };
|
||||||
@@ -195,6 +204,17 @@ export function resolveEffectiveToolPolicy(params: {
|
|||||||
agentProviderPolicy: pickToolPolicy(agentProviderPolicy),
|
agentProviderPolicy: pickToolPolicy(agentProviderPolicy),
|
||||||
profile,
|
profile,
|
||||||
providerProfile: agentProviderPolicy?.profile ?? providerPolicy?.profile,
|
providerProfile: agentProviderPolicy?.profile ?? providerPolicy?.profile,
|
||||||
|
// alsoAllow is applied at the profile stage (to avoid being filtered out early).
|
||||||
|
profileAlsoAllow: Array.isArray(agentTools?.alsoAllow)
|
||||||
|
? agentTools?.alsoAllow
|
||||||
|
: Array.isArray(globalTools?.alsoAllow)
|
||||||
|
? globalTools?.alsoAllow
|
||||||
|
: undefined,
|
||||||
|
providerProfileAlsoAllow: Array.isArray(agentProviderPolicy?.alsoAllow)
|
||||||
|
? agentProviderPolicy?.alsoAllow
|
||||||
|
: Array.isArray(providerPolicy?.alsoAllow)
|
||||||
|
? providerPolicy?.alsoAllow
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
agentProviderPolicy,
|
agentProviderPolicy,
|
||||||
profile,
|
profile,
|
||||||
providerProfile,
|
providerProfile,
|
||||||
|
profileAlsoAllow,
|
||||||
|
providerProfileAlsoAllow,
|
||||||
} = resolveEffectiveToolPolicy({
|
} = resolveEffectiveToolPolicy({
|
||||||
config: options?.config,
|
config: options?.config,
|
||||||
sessionKey: options?.sessionKey,
|
sessionKey: options?.sessionKey,
|
||||||
@@ -175,14 +177,25 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
});
|
});
|
||||||
const profilePolicy = resolveToolProfilePolicy(profile);
|
const profilePolicy = resolveToolProfilePolicy(profile);
|
||||||
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
||||||
|
|
||||||
|
const mergeAlsoAllow = (policy: typeof profilePolicy, alsoAllow?: string[]) => {
|
||||||
|
if (!policy?.allow || !Array.isArray(alsoAllow) || alsoAllow.length === 0) return policy;
|
||||||
|
return { ...policy, allow: Array.from(new Set([...policy.allow, ...alsoAllow])) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const profilePolicyWithAlsoAllow = mergeAlsoAllow(profilePolicy, profileAlsoAllow);
|
||||||
|
const providerProfilePolicyWithAlsoAllow = mergeAlsoAllow(
|
||||||
|
providerProfilePolicy,
|
||||||
|
providerProfileAlsoAllow,
|
||||||
|
);
|
||||||
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||||
const subagentPolicy =
|
const subagentPolicy =
|
||||||
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
||||||
? resolveSubagentToolPolicy(options.config)
|
? resolveSubagentToolPolicy(options.config)
|
||||||
: undefined;
|
: undefined;
|
||||||
const allowBackground = isToolAllowedByPolicies("process", [
|
const allowBackground = isToolAllowedByPolicies("process", [
|
||||||
profilePolicy,
|
profilePolicyWithAlsoAllow,
|
||||||
providerProfilePolicy,
|
providerProfilePolicyWithAlsoAllow,
|
||||||
globalPolicy,
|
globalPolicy,
|
||||||
globalProviderPolicy,
|
globalProviderPolicy,
|
||||||
agentPolicy,
|
agentPolicy,
|
||||||
@@ -340,11 +353,11 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
return expandPolicyWithPluginGroups(resolved.policy, pluginGroups);
|
return expandPolicyWithPluginGroups(resolved.policy, pluginGroups);
|
||||||
};
|
};
|
||||||
const profilePolicyExpanded = resolvePolicy(
|
const profilePolicyExpanded = resolvePolicy(
|
||||||
profilePolicy,
|
profilePolicyWithAlsoAllow,
|
||||||
profile ? `tools.profile (${profile})` : "tools.profile",
|
profile ? `tools.profile (${profile})` : "tools.profile",
|
||||||
);
|
);
|
||||||
const providerProfileExpanded = resolvePolicy(
|
const providerProfileExpanded = resolvePolicy(
|
||||||
providerProfilePolicy,
|
providerProfilePolicyWithAlsoAllow,
|
||||||
providerProfile ? `tools.byProvider.profile (${providerProfile})` : "tools.byProvider.profile",
|
providerProfile ? `tools.byProvider.profile (${providerProfile})` : "tools.byProvider.profile",
|
||||||
);
|
);
|
||||||
const globalPolicyExpanded = resolvePolicy(globalPolicy, "tools.allow");
|
const globalPolicyExpanded = resolvePolicy(globalPolicy, "tools.allow");
|
||||||
|
|||||||
@@ -165,7 +165,9 @@ const FIELD_LABELS: Record<string, string> = {
|
|||||||
"tools.links.models": "Link Understanding Models",
|
"tools.links.models": "Link Understanding Models",
|
||||||
"tools.links.scope": "Link Understanding Scope",
|
"tools.links.scope": "Link Understanding Scope",
|
||||||
"tools.profile": "Tool Profile",
|
"tools.profile": "Tool Profile",
|
||||||
|
"tools.alsoAllow": "Tool Allowlist Additions",
|
||||||
"agents.list[].tools.profile": "Agent Tool Profile",
|
"agents.list[].tools.profile": "Agent Tool Profile",
|
||||||
|
"agents.list[].tools.alsoAllow": "Agent Tool Allowlist Additions",
|
||||||
"tools.byProvider": "Tool Policy by Provider",
|
"tools.byProvider": "Tool Policy by Provider",
|
||||||
"agents.list[].tools.byProvider": "Agent Tool Policy by Provider",
|
"agents.list[].tools.byProvider": "Agent Tool Policy by Provider",
|
||||||
"tools.exec.applyPatch.enabled": "Enable apply_patch",
|
"tools.exec.applyPatch.enabled": "Enable apply_patch",
|
||||||
|
|||||||
@@ -140,12 +140,21 @@ export type ToolProfileId = "minimal" | "coding" | "messaging" | "full";
|
|||||||
|
|
||||||
export type ToolPolicyConfig = {
|
export type ToolPolicyConfig = {
|
||||||
allow?: string[];
|
allow?: string[];
|
||||||
|
/**
|
||||||
|
* Additional allowlist entries merged into the effective allowlist.
|
||||||
|
*
|
||||||
|
* Intended for additive configuration (e.g., "also allow lobster") without forcing
|
||||||
|
* users to replace/duplicate an existing allowlist or profile.
|
||||||
|
*/
|
||||||
|
alsoAllow?: string[];
|
||||||
deny?: string[];
|
deny?: string[];
|
||||||
profile?: ToolProfileId;
|
profile?: ToolProfileId;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GroupToolPolicyConfig = {
|
export type GroupToolPolicyConfig = {
|
||||||
allow?: string[];
|
allow?: string[];
|
||||||
|
/** Additional allowlist entries merged into allow. */
|
||||||
|
alsoAllow?: string[];
|
||||||
deny?: string[];
|
deny?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -188,6 +197,8 @@ export type AgentToolsConfig = {
|
|||||||
/** Base tool profile applied before allow/deny lists. */
|
/** Base tool profile applied before allow/deny lists. */
|
||||||
profile?: ToolProfileId;
|
profile?: ToolProfileId;
|
||||||
allow?: string[];
|
allow?: string[];
|
||||||
|
/** Additional allowlist entries merged into allow and/or profile allowlist. */
|
||||||
|
alsoAllow?: string[];
|
||||||
deny?: string[];
|
deny?: string[];
|
||||||
/** Optional tool policy overrides keyed by provider id or "provider/model". */
|
/** Optional tool policy overrides keyed by provider id or "provider/model". */
|
||||||
byProvider?: Record<string, ToolPolicyConfig>;
|
byProvider?: Record<string, ToolPolicyConfig>;
|
||||||
@@ -312,6 +323,8 @@ export type ToolsConfig = {
|
|||||||
/** Base tool profile applied before allow/deny lists. */
|
/** Base tool profile applied before allow/deny lists. */
|
||||||
profile?: ToolProfileId;
|
profile?: ToolProfileId;
|
||||||
allow?: string[];
|
allow?: string[];
|
||||||
|
/** Additional allowlist entries merged into allow and/or profile allowlist. */
|
||||||
|
alsoAllow?: string[];
|
||||||
deny?: string[];
|
deny?: string[];
|
||||||
/** Optional tool policy overrides keyed by provider id or "provider/model". */
|
/** Optional tool policy overrides keyed by provider id or "provider/model". */
|
||||||
byProvider?: Record<string, ToolPolicyConfig>;
|
byProvider?: Record<string, ToolPolicyConfig>;
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export const SandboxPruneSchema = z
|
|||||||
export const ToolPolicySchema = z
|
export const ToolPolicySchema = z
|
||||||
.object({
|
.object({
|
||||||
allow: z.array(z.string()).optional(),
|
allow: z.array(z.string()).optional(),
|
||||||
|
alsoAllow: z.array(z.string()).optional(),
|
||||||
deny: z.array(z.string()).optional(),
|
deny: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
@@ -202,6 +203,7 @@ export const ToolProfileSchema = z
|
|||||||
export const ToolPolicyWithProfileSchema = z
|
export const ToolPolicyWithProfileSchema = z
|
||||||
.object({
|
.object({
|
||||||
allow: z.array(z.string()).optional(),
|
allow: z.array(z.string()).optional(),
|
||||||
|
alsoAllow: z.array(z.string()).optional(),
|
||||||
deny: z.array(z.string()).optional(),
|
deny: z.array(z.string()).optional(),
|
||||||
profile: ToolProfileSchema,
|
profile: ToolProfileSchema,
|
||||||
})
|
})
|
||||||
@@ -231,6 +233,7 @@ export const AgentToolsSchema = z
|
|||||||
.object({
|
.object({
|
||||||
profile: ToolProfileSchema,
|
profile: ToolProfileSchema,
|
||||||
allow: z.array(z.string()).optional(),
|
allow: z.array(z.string()).optional(),
|
||||||
|
alsoAllow: z.array(z.string()).optional(),
|
||||||
deny: z.array(z.string()).optional(),
|
deny: z.array(z.string()).optional(),
|
||||||
byProvider: z.record(z.string(), ToolPolicyWithProfileSchema).optional(),
|
byProvider: z.record(z.string(), ToolPolicyWithProfileSchema).optional(),
|
||||||
elevated: z
|
elevated: z
|
||||||
@@ -425,6 +428,7 @@ export const ToolsSchema = z
|
|||||||
.object({
|
.object({
|
||||||
profile: ToolProfileSchema,
|
profile: ToolProfileSchema,
|
||||||
allow: z.array(z.string()).optional(),
|
allow: z.array(z.string()).optional(),
|
||||||
|
alsoAllow: z.array(z.string()).optional(),
|
||||||
deny: z.array(z.string()).optional(),
|
deny: z.array(z.string()).optional(),
|
||||||
byProvider: z.record(z.string(), ToolPolicyWithProfileSchema).optional(),
|
byProvider: z.record(z.string(), ToolPolicyWithProfileSchema).optional(),
|
||||||
web: ToolsWebSchema,
|
web: ToolsWebSchema,
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
|
|
||||||
import { installGatewayTestHooks, getFreePort, startGatewayServer } from "./test-helpers.server.js";
|
import { installGatewayTestHooks, getFreePort, startGatewayServer } from "./test-helpers.server.js";
|
||||||
import { resetTestPluginRegistry, setTestPluginRegistry, testState } from "./test-helpers.mocks.js";
|
import { resetTestPluginRegistry, setTestPluginRegistry, testState } from "./test-helpers.mocks.js";
|
||||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||||
|
|
||||||
installGatewayTestHooks({ scope: "suite" });
|
installGatewayTestHooks({ scope: "suite" });
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Ensure these tests are not affected by host env vars.
|
||||||
|
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||||
|
delete process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
||||||
|
});
|
||||||
|
|
||||||
const resolveGatewayToken = (): string => {
|
const resolveGatewayToken = (): string => {
|
||||||
const token = (testState.gatewayAuth as { token?: string } | undefined)?.token;
|
const token = (testState.gatewayAuth as { token?: string } | undefined)?.token;
|
||||||
if (!token) throw new Error("test gateway token missing");
|
if (!token) throw new Error("test gateway token missing");
|
||||||
@@ -47,6 +54,35 @@ describe("POST /tools/invoke", () => {
|
|||||||
await server.close();
|
await server.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("supports tools.alsoAllow as additive allowlist (profile stage)", async () => {
|
||||||
|
// No explicit tool allowlist; rely on profile + alsoAllow.
|
||||||
|
testState.agentsConfig = {
|
||||||
|
list: [{ id: "main" }],
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// minimal profile does NOT include sessions_list, but alsoAllow should.
|
||||||
|
const { writeConfigFile } = await import("../config/config.js");
|
||||||
|
await writeConfigFile({
|
||||||
|
tools: { profile: "minimal", alsoAllow: ["sessions_list"] },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||||
|
const token = resolveGatewayToken();
|
||||||
|
|
||||||
|
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||||
|
body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.ok).toBe(true);
|
||||||
|
|
||||||
|
await server.close();
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts password auth when bearer token matches", async () => {
|
it("accepts password auth when bearer token matches", async () => {
|
||||||
testState.agentsConfig = {
|
testState.agentsConfig = {
|
||||||
list: [
|
list: [
|
||||||
|
|||||||
@@ -130,9 +130,22 @@ export async function handleToolsInvokeHttpRequest(
|
|||||||
agentProviderPolicy,
|
agentProviderPolicy,
|
||||||
profile,
|
profile,
|
||||||
providerProfile,
|
providerProfile,
|
||||||
|
profileAlsoAllow,
|
||||||
|
providerProfileAlsoAllow,
|
||||||
} = resolveEffectiveToolPolicy({ config: cfg, sessionKey });
|
} = resolveEffectiveToolPolicy({ config: cfg, sessionKey });
|
||||||
const profilePolicy = resolveToolProfilePolicy(profile);
|
const profilePolicy = resolveToolProfilePolicy(profile);
|
||||||
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
||||||
|
|
||||||
|
const mergeAlsoAllow = (policy: typeof profilePolicy, alsoAllow?: string[]) => {
|
||||||
|
if (!policy?.allow || !Array.isArray(alsoAllow) || alsoAllow.length === 0) return policy;
|
||||||
|
return { ...policy, allow: Array.from(new Set([...policy.allow, ...alsoAllow])) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const profilePolicyWithAlsoAllow = mergeAlsoAllow(profilePolicy, profileAlsoAllow);
|
||||||
|
const providerProfilePolicyWithAlsoAllow = mergeAlsoAllow(
|
||||||
|
providerProfilePolicy,
|
||||||
|
providerProfileAlsoAllow,
|
||||||
|
);
|
||||||
const groupPolicy = resolveGroupToolPolicy({
|
const groupPolicy = resolveGroupToolPolicy({
|
||||||
config: cfg,
|
config: cfg,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
@@ -183,11 +196,11 @@ export async function handleToolsInvokeHttpRequest(
|
|||||||
return expandPolicyWithPluginGroups(resolved.policy, pluginGroups);
|
return expandPolicyWithPluginGroups(resolved.policy, pluginGroups);
|
||||||
};
|
};
|
||||||
const profilePolicyExpanded = resolvePolicy(
|
const profilePolicyExpanded = resolvePolicy(
|
||||||
profilePolicy,
|
profilePolicyWithAlsoAllow,
|
||||||
profile ? `tools.profile (${profile})` : "tools.profile",
|
profile ? `tools.profile (${profile})` : "tools.profile",
|
||||||
);
|
);
|
||||||
const providerProfileExpanded = resolvePolicy(
|
const providerProfileExpanded = resolvePolicy(
|
||||||
providerProfilePolicy,
|
providerProfilePolicyWithAlsoAllow,
|
||||||
providerProfile ? `tools.byProvider.profile (${providerProfile})` : "tools.byProvider.profile",
|
providerProfile ? `tools.byProvider.profile (${providerProfile})` : "tools.byProvider.profile",
|
||||||
);
|
);
|
||||||
const globalPolicyExpanded = resolvePolicy(globalPolicy, "tools.allow");
|
const globalPolicyExpanded = resolvePolicy(globalPolicy, "tools.allow");
|
||||||
|
|||||||
Reference in New Issue
Block a user