mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
fix(subagents): explain browser tool profile filtering
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/STT: frame inbound voice-note transcripts as machine-generated,
|
||||
untrusted text in agent context while preserving raw transcript mention
|
||||
detection. Closes #33360. Thanks @smartchainark.
|
||||
- Subagents/browser: show an actionable `/tools` notice when browser automation is configured but filtered out by the active tool profile, and document that coding-profile agents should use `tools.alsoAllow: ["browser"]` rather than subagent allowlists alone.
|
||||
- Control UI/Quick Settings: persist the assistant avatar override to browser local storage (mirroring the user avatar) so uploaded image data URLs no longer fail config validation with "Too big: expected string to have <=200 characters". Also lift the gateway-side `ui.assistant.avatar` length cap to match the user avatar size budget for non-UI clients writing the field directly. Thanks @BunsDev.
|
||||
- Browser/CDP: make readiness diagnostics use the same discovery-first fallback as reachability for bare `ws://` Browserless and Browserbase CDP URLs. Fixes #69532.
|
||||
- ACP/OpenCode: update the bundled acpx runtime to 0.6.0 and cover the OpenCode ACP bind path in Docker live tests.
|
||||
|
||||
@@ -69,6 +69,24 @@ Browser config changes require a Gateway restart so the plugin can re-register i
|
||||
|
||||
## Agent guidance
|
||||
|
||||
Tool-profile note: `tools.profile: "coding"` includes `web_search` and
|
||||
`web_fetch`, but it does not include the full `browser` tool. If the agent or a
|
||||
spawned sub-agent should use browser automation, add browser at the profile
|
||||
stage:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
profile: "coding",
|
||||
alsoAllow: ["browser"],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
For a single agent, use `agents.list[].tools.alsoAllow: ["browser"]`.
|
||||
`tools.subagents.tools.allow: ["browser"]` alone is not enough because sub-agent
|
||||
policy is applied after profile filtering.
|
||||
|
||||
The browser plugin ships two levels of agent guidance:
|
||||
|
||||
- The `browser` tool description carries the compact always-on contract: pick
|
||||
|
||||
@@ -143,6 +143,12 @@ Per-agent override: `agents.list[].tools.profile`.
|
||||
| `messaging` | `group:messaging`, `sessions_list`, `sessions_history`, `sessions_send`, `session_status` |
|
||||
| `minimal` | `session_status` only |
|
||||
|
||||
`coding` includes lightweight web tools (`web_search`, `web_fetch`, `x_search`)
|
||||
but not the full browser-control tool. Browser automation can drive real
|
||||
sessions and logged-in profiles, so add it explicitly with
|
||||
`tools.alsoAllow: ["browser"]` or a per-agent
|
||||
`agents.list[].tools.alsoAllow: ["browser"]`.
|
||||
|
||||
The `coding` and `messaging` profiles also allow configured bundle MCP tools
|
||||
under the plugin key `bundle-mcp`. Add `tools.deny: ["bundle-mcp"]` when you
|
||||
want a profile to keep its normal built-ins but hide all configured MCP tools.
|
||||
|
||||
@@ -305,7 +305,11 @@ Announce payloads include a stats line at the end (even when wrapped):
|
||||
|
||||
## Tool Policy (sub-agent tools)
|
||||
|
||||
By default, sub-agents get **all tools except session tools** and system tools:
|
||||
Sub-agents use the same profile and tool-policy pipeline as the parent or target
|
||||
agent first. After that, OpenClaw applies the sub-agent restriction layer.
|
||||
|
||||
With no restrictive `tools.profile`, sub-agents get **all tools except session
|
||||
tools** and system tools:
|
||||
|
||||
- `sessions_list`
|
||||
- `sessions_history`
|
||||
@@ -341,6 +345,24 @@ Override via config:
|
||||
}
|
||||
```
|
||||
|
||||
`tools.subagents.tools.allow` is a final allow-only filter. It can narrow the
|
||||
already-resolved tool set, but it cannot add back a tool removed by
|
||||
`tools.profile`. For example, `tools.profile: "coding"` includes
|
||||
`web_search`/`web_fetch`, but not the `browser` tool. To let coding-profile
|
||||
sub-agents use browser automation, add browser at the profile stage:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
profile: "coding",
|
||||
alsoAllow: ["browser"],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use per-agent `agents.list[].tools.alsoAllow: ["browser"]` when only one agent
|
||||
should get browser automation.
|
||||
|
||||
## Concurrency
|
||||
|
||||
Sub-agents use a dedicated in-process queue lane:
|
||||
|
||||
@@ -353,6 +353,41 @@ describe("createOpenClawCodingTools", () => {
|
||||
expect(names.has("browser")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps browser out of coding-profile subagents unless profile-stage alsoAllow adds it", () => {
|
||||
const baseConfig = {
|
||||
browser: { enabled: true },
|
||||
plugins: { entries: { browser: { enabled: true } } },
|
||||
tools: { profile: "coding" },
|
||||
} as OpenClawConfig;
|
||||
const codingSubagent = createOpenClawCodingTools({
|
||||
sessionKey: "agent:main:subagent:test",
|
||||
config: baseConfig,
|
||||
});
|
||||
const codingNames = new Set(codingSubagent.map((tool) => tool.name));
|
||||
expect(codingNames.has("browser")).toBe(false);
|
||||
|
||||
const subagentAllowOnly = createOpenClawCodingTools({
|
||||
sessionKey: "agent:main:subagent:test",
|
||||
config: {
|
||||
...baseConfig,
|
||||
tools: {
|
||||
profile: "coding",
|
||||
subagents: { tools: { allow: ["browser"] } },
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
expect(subagentAllowOnly.some((tool) => tool.name === "browser")).toBe(false);
|
||||
|
||||
const profileStageAlsoAllow = createOpenClawCodingTools({
|
||||
sessionKey: "agent:main:subagent:test",
|
||||
config: {
|
||||
...baseConfig,
|
||||
tools: { profile: "coding", alsoAllow: ["browser"] },
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
expect(profileStageAlsoAllow.some((tool) => tool.name === "browser")).toBe(true);
|
||||
});
|
||||
|
||||
it("can keep message available when a cron route needs it under the coding profile", () => {
|
||||
const codingTools = createOpenClawCodingTools({
|
||||
config: { tools: { profile: "coding" } },
|
||||
|
||||
@@ -30,6 +30,7 @@ const coreTools = [
|
||||
stubActionTool("sessions_spawn", ["spawn", "handoff"]),
|
||||
stubActionTool("subagents", ["list", "show"]),
|
||||
stubActionTool("session_status", ["get", "show"]),
|
||||
stubActionTool("browser", ["status", "snapshot"]),
|
||||
stubTool("tts"),
|
||||
stubTool("image_generate"),
|
||||
stubTool("video_generate"),
|
||||
|
||||
@@ -13,6 +13,7 @@ describe("tool-catalog", () => {
|
||||
expect(policy!.allow).toContain("music_generate");
|
||||
expect(policy!.allow).toContain("video_generate");
|
||||
expect(policy!.allow).toContain("update_plan");
|
||||
expect(policy!.allow).not.toContain("browser");
|
||||
});
|
||||
|
||||
it("includes bundle MCP tools in coding and messaging profile policies", () => {
|
||||
|
||||
@@ -262,6 +262,50 @@ describe("resolveEffectiveToolInventory", () => {
|
||||
expect(result.profile).toBe("coding");
|
||||
});
|
||||
|
||||
it("adds an actionable notice when configured browser is filtered by the tool profile", async () => {
|
||||
const { resolveEffectiveToolInventory } = await loadHarness({
|
||||
tools: [
|
||||
mockTool({ name: "web_fetch", label: "Web Fetch", description: "Fetch web content" }),
|
||||
],
|
||||
effectivePolicy: { profile: "coding" },
|
||||
});
|
||||
|
||||
const result = resolveEffectiveToolInventory({
|
||||
cfg: {
|
||||
browser: { enabled: true },
|
||||
plugins: { entries: { browser: { enabled: true } } },
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(result.notices).toEqual([
|
||||
{
|
||||
id: "browser-filtered-by-profile",
|
||||
severity: "info",
|
||||
message:
|
||||
'Browser is configured, but the current tool profile does not include the browser tool. Add tools.alsoAllow: ["browser"] or agents.list[].tools.alsoAllow: ["browser"]; tools.subagents.tools.allow alone cannot add it back after profile filtering.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not add a browser profile notice when browser is already available", async () => {
|
||||
const { resolveEffectiveToolInventory } = await loadHarness({
|
||||
tools: [
|
||||
mockTool({ name: "browser", label: "Browser", description: "Control browser" }),
|
||||
mockTool({ name: "web_fetch", label: "Web Fetch", description: "Fetch web content" }),
|
||||
],
|
||||
effectivePolicy: { profile: "coding" },
|
||||
});
|
||||
|
||||
const result = resolveEffectiveToolInventory({
|
||||
cfg: {
|
||||
browser: { enabled: true },
|
||||
plugins: { entries: { browser: { enabled: true } } },
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(result.notices).toBeUndefined();
|
||||
});
|
||||
|
||||
it("passes resolved model compat into effective tool creation", async () => {
|
||||
const createToolsMock = vi.fn<typeof createOpenClawCodingTools>(() => [
|
||||
mockTool({ name: "exec", label: "Exec", description: "Run shell commands" }),
|
||||
|
||||
@@ -12,7 +12,9 @@ import { createOpenClawCodingTools } from "./pi-tools.js";
|
||||
import { resolveEffectiveToolPolicy } from "./pi-tools.policy.js";
|
||||
import { summarizeToolDescriptionText } from "./tool-description-summary.js";
|
||||
import { resolveToolDisplay } from "./tool-display.js";
|
||||
import { normalizeToolName } from "./tool-policy.js";
|
||||
import type {
|
||||
EffectiveToolInventoryNotice,
|
||||
EffectiveToolInventoryEntry,
|
||||
EffectiveToolInventoryGroup,
|
||||
EffectiveToolInventoryResult,
|
||||
@@ -70,6 +72,82 @@ function groupLabel(source: EffectiveToolSource): string {
|
||||
}
|
||||
}
|
||||
|
||||
function listIncludesTool(list: string[] | undefined, toolName: string): boolean {
|
||||
if (!Array.isArray(list)) {
|
||||
return false;
|
||||
}
|
||||
const normalizedToolName = normalizeToolName(toolName);
|
||||
return list.some((entry) => normalizeToolName(entry) === normalizedToolName);
|
||||
}
|
||||
|
||||
function policyDeniesTool(policy: { deny?: string[] } | undefined, toolName: string): boolean {
|
||||
return (
|
||||
listIncludesTool(policy?.deny, toolName) ||
|
||||
listIncludesTool(policy?.deny, "group:ui") ||
|
||||
listIncludesTool(policy?.deny, "group:openclaw")
|
||||
);
|
||||
}
|
||||
|
||||
function hasExplicitBrowserIntent(cfg: OpenClawConfig): boolean {
|
||||
return cfg.browser?.enabled !== false && Boolean(cfg.browser || cfg.plugins?.entries?.browser);
|
||||
}
|
||||
|
||||
function buildToolInventoryNotices(params: {
|
||||
cfg: OpenClawConfig;
|
||||
profile: string;
|
||||
entries: EffectiveToolInventoryEntry[];
|
||||
effectivePolicy: ReturnType<typeof resolveEffectiveToolPolicy>;
|
||||
}): EffectiveToolInventoryNotice[] | undefined {
|
||||
const hasBrowserTool = params.entries.some((entry) => normalizeToolName(entry.id) === "browser");
|
||||
if (hasBrowserTool || !hasExplicitBrowserIntent(params.cfg)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const browserDenied = [
|
||||
params.effectivePolicy.globalPolicy,
|
||||
params.effectivePolicy.globalProviderPolicy,
|
||||
params.effectivePolicy.agentPolicy,
|
||||
params.effectivePolicy.agentProviderPolicy,
|
||||
].some((policy) => policyDeniesTool(policy, "browser"));
|
||||
if (browserDenied) {
|
||||
return [
|
||||
{
|
||||
id: "browser-denied-by-policy",
|
||||
severity: "info",
|
||||
message:
|
||||
"Browser is configured, but this session does not expose the browser tool because tool policy denies it. Remove the browser deny entry to use browser automation.",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (params.profile !== "full") {
|
||||
return [
|
||||
{
|
||||
id: "browser-filtered-by-profile",
|
||||
severity: "info",
|
||||
message:
|
||||
'Browser is configured, but the current tool profile does not include the browser tool. Add tools.alsoAllow: ["browser"] or agents.list[].tools.alsoAllow: ["browser"]; tools.subagents.tools.allow alone cannot add it back after profile filtering.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(params.cfg.plugins?.allow) &&
|
||||
!listIncludesTool(params.cfg.plugins.allow, "browser")
|
||||
) {
|
||||
return [
|
||||
{
|
||||
id: "browser-plugin-not-allowed",
|
||||
severity: "warning",
|
||||
message:
|
||||
'Browser is configured, but plugins.allow does not include browser. Add "browser" to plugins.allow or remove the restrictive plugin allowlist.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function disambiguateLabels(entries: EffectiveToolInventoryEntry[]): EffectiveToolInventoryEntry[] {
|
||||
const counts = new Map<string, number>();
|
||||
for (const entry of entries) {
|
||||
@@ -170,6 +248,7 @@ export function resolveEffectiveToolInventory(
|
||||
})
|
||||
.toSorted((a, b) => a.label.localeCompare(b.label)),
|
||||
);
|
||||
const notices = buildToolInventoryNotices({ cfg: params.cfg, profile, entries, effectivePolicy });
|
||||
const groupsBySource = new Map<EffectiveToolSource, EffectiveToolInventoryEntry[]>();
|
||||
for (const entry of entries) {
|
||||
const tools = groupsBySource.get(entry.source) ?? [];
|
||||
@@ -192,5 +271,5 @@ export function resolveEffectiveToolInventory(
|
||||
})
|
||||
.filter((group): group is EffectiveToolInventoryGroup => group !== null);
|
||||
|
||||
return { agentId, profile, groups };
|
||||
return { agentId, profile, groups, ...(notices ? { notices } : {}) };
|
||||
}
|
||||
|
||||
@@ -19,10 +19,17 @@ export type EffectiveToolInventoryGroup = {
|
||||
tools: EffectiveToolInventoryEntry[];
|
||||
};
|
||||
|
||||
export type EffectiveToolInventoryNotice = {
|
||||
id: string;
|
||||
severity: "info" | "warning";
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type EffectiveToolInventoryResult = {
|
||||
agentId: string;
|
||||
profile: string;
|
||||
groups: EffectiveToolInventoryGroup[];
|
||||
notices?: EffectiveToolInventoryNotice[];
|
||||
};
|
||||
|
||||
export type ResolveEffectiveToolInventoryParams = {
|
||||
|
||||
@@ -72,6 +72,40 @@ describe("tools product copy", () => {
|
||||
expect(text).not.toContain("unavailable right now");
|
||||
});
|
||||
|
||||
it("renders effective tool inventory notices", () => {
|
||||
const text = buildToolsMessage({
|
||||
agentId: "main",
|
||||
profile: "coding",
|
||||
groups: [
|
||||
{
|
||||
id: "core",
|
||||
label: "Built-in tools",
|
||||
source: "core",
|
||||
tools: [
|
||||
{
|
||||
id: "web_fetch",
|
||||
label: "Web Fetch",
|
||||
description: "Fetch web content",
|
||||
rawDescription: "Fetch web content",
|
||||
source: "core",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
notices: [
|
||||
{
|
||||
id: "browser-filtered-by-profile",
|
||||
severity: "info",
|
||||
message:
|
||||
'Browser is configured, but the current tool profile does not include the browser tool. Add tools.alsoAllow: ["browser"].',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(text).toContain("Notes");
|
||||
expect(text).toContain('Add tools.alsoAllow: ["browser"].');
|
||||
});
|
||||
|
||||
it("keeps detailed descriptions in verbose mode", () => {
|
||||
const text = buildToolsMessage(
|
||||
{
|
||||
|
||||
@@ -97,5 +97,11 @@ export function buildToolsMessage(
|
||||
} else {
|
||||
lines.push("", "Use /tools verbose for descriptions.");
|
||||
}
|
||||
if (result.notices?.length) {
|
||||
lines.push("", "Notes");
|
||||
for (const notice of result.notices) {
|
||||
lines.push(` ${notice.message}`);
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user