Skip to content

Commit e0290ce

Browse files
committed
fix(doctor): polish sandbox MCP warnings
1 parent 92cb62b commit e0290ce

4 files changed

Lines changed: 81 additions & 8 deletions

File tree

docs/gateway/config-tools.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ Configured MCP servers are exposed as plugin-owned tools under the `bundle-mcp`
5454
- `group:plugins` for all loaded plugin-owned tools
5555
- exact MCP server globs such as `outlook__*` when you only want one server
5656

57+
Server globs use the provider-safe MCP server prefix, not necessarily the raw `mcp.servers` key. Non-`[A-Za-z0-9_-]` characters become `-`, names that do not start with a letter get an `mcp-` prefix, and long or duplicate prefixes may be truncated or suffixed; for example, `mcp.servers["Outlook Graph"]` uses a glob like `outlook-graph__*`.
58+
5759
```json5
5860
{
5961
agents: { defaults: { sandbox: { mode: "all" } } },
@@ -72,7 +74,7 @@ Configured MCP servers are exposed as plugin-owned tools under the `bundle-mcp`
7274
}
7375
```
7476

75-
Without that sandbox-layer entry, the MCP server can still load successfully while its tools are filtered before the provider request. Use `openclaw doctor` to catch this shape.
77+
Without that sandbox-layer entry, the MCP server can still load successfully while its tools are filtered before the provider request. Use `openclaw doctor` to catch this shape for OpenClaw-managed servers in `mcp.servers`. MCP servers loaded from bundled plugin manifests or Claude `.mcp.json` use the same sandbox gate, but this diagnostic does not enumerate those sources yet; use the same allowlist entries if their tools disappear in sandboxed turns.
7678

7779
### `tools.allow` / `tools.deny`
7880

docs/gateway/sandbox-vs-tool-policy-vs-elevated.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ Available groups:
102102
- `group:openclaw`: all built-in OpenClaw tools (excludes provider plugins)
103103
- `group:plugins`: all loaded plugin-owned tools, including configured MCP servers exposed through `bundle-mcp`
104104

105-
For sandboxed MCP servers, the sandbox tool policy is a second allow gate. If `mcp.servers` is configured but sandboxed turns only show built-in tools, add `bundle-mcp`, `group:plugins`, or a server glob such as `outlook__*` to `tools.sandbox.tools.alsoAllow`, then restart/reload the gateway and recapture the tool list.
105+
For sandboxed MCP servers, the sandbox tool policy is a second allow gate. If `mcp.servers` is configured but sandboxed turns only show built-in tools, add `bundle-mcp`, `group:plugins`, or a server glob such as `outlook__*` to `tools.sandbox.tools.alsoAllow`, then restart/reload the gateway and recapture the tool list. Server globs use the provider-safe MCP server prefix: non-`[A-Za-z0-9_-]` characters become `-`, names that do not start with a letter get an `mcp-` prefix, and long or duplicate prefixes may be truncated or suffixed.
106+
107+
`openclaw doctor` currently checks this shape for OpenClaw-managed servers in `mcp.servers`. MCP servers loaded from bundled plugin manifests or Claude `.mcp.json` use the same sandbox gate, but this diagnostic does not enumerate those sources yet; use the same allowlist entries if their tools disappear in sandboxed turns.
106108

107109
## Elevated: exec-only "run on host"
108110

src/commands/doctor/shared/plugin-tool-allowlist-warnings.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,43 @@ describe("collectPluginToolAllowlistWarnings", () => {
115115
]);
116116
});
117117

118+
it("uses a config-path source label when sandbox allowlist is unset", () => {
119+
const warnings = collectPluginToolAllowlistWarnings({
120+
cfg: {
121+
agents: { defaults: { sandbox: { mode: "all" } } },
122+
mcp: { servers: { outlook: { command: "node", args: ["outlook-server.js"] } } },
123+
},
124+
manifestRegistry,
125+
});
126+
127+
expect(warnings).toEqual([
128+
'- mcp.servers defines 1 MCP server ("outlook"), but tools.sandbox.tools.alsoAllow (unset) does not include "bundle-mcp", "group:plugins", or a matching "<server>__*" MCP tool pattern. Sandboxed agents will filter bundled MCP tools before provider requests. Add "bundle-mcp" to tools.sandbox.tools.alsoAllow (or use "group:plugins" / server globs) if those MCP tools should be visible; use tools.sandbox.tools.allow: [] only when you intentionally want no sandbox allow gate.',
129+
]);
130+
});
131+
132+
it("uses plural grammar when multiple sandbox allow sources hide MCP servers", () => {
133+
const warnings = collectPluginToolAllowlistWarnings({
134+
cfg: {
135+
agents: {
136+
defaults: { sandbox: { mode: "all" } },
137+
list: [
138+
{
139+
id: "worker",
140+
tools: { sandbox: { tools: { alsoAllow: ["web_fetch"] } } },
141+
},
142+
],
143+
},
144+
mcp: { servers: { outlook: { command: "node", args: ["outlook-server.js"] } } },
145+
tools: { sandbox: { tools: { alsoAllow: ["web_search"] } } },
146+
},
147+
manifestRegistry,
148+
});
149+
150+
expect(warnings).toEqual([
151+
'- mcp.servers defines 1 MCP server ("outlook"), but agents.list[0].tools.sandbox.tools.alsoAllow, tools.sandbox.tools.alsoAllow do not include "bundle-mcp", "group:plugins", or a matching "<server>__*" MCP tool pattern. Sandboxed agents will filter bundled MCP tools before provider requests. Add "bundle-mcp" to tools.sandbox.tools.alsoAllow (or use "group:plugins" / server globs) if those MCP tools should be visible; use tools.sandbox.tools.allow: [] only when you intentionally want no sandbox allow gate.',
152+
]);
153+
});
154+
118155
it("does not warn for sandboxed MCP servers when bundle-mcp is explicitly allowed", () => {
119156
const warnings = collectPluginToolAllowlistWarnings({
120157
cfg: {
@@ -198,6 +235,19 @@ describe("collectPluginToolAllowlistWarnings", () => {
198235
expect(warnings).toStrictEqual([]);
199236
});
200237

238+
it("does not warn when a server glob matches the sanitized MCP server name", () => {
239+
const warnings = collectPluginToolAllowlistWarnings({
240+
cfg: {
241+
agents: { defaults: { sandbox: { mode: "all" } } },
242+
mcp: { servers: { "Outlook Graph": { command: "node", args: ["outlook-server.js"] } } },
243+
tools: { sandbox: { tools: { alsoAllow: ["outlook-graph__*"] } } },
244+
},
245+
manifestRegistry,
246+
});
247+
248+
expect(warnings).toStrictEqual([]);
249+
});
250+
201251
it("does not warn for sandboxed MCP servers when sandbox allow is explicitly allow-all", () => {
202252
const warnings = collectPluginToolAllowlistWarnings({
203253
cfg: {

src/commands/doctor/shared/plugin-tool-allowlist-warnings.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,29 @@ function collectToolAllowlistSources(cfg: OpenClawConfig): ToolAllowlistSource[]
7979
return sources;
8080
}
8181

82-
function formatSourceLabels(labels: Iterable<string>): string {
83-
const sorted = [...new Set(labels)].toSorted((left, right) => left.localeCompare(right));
82+
function collectSortedSourceLabels(labels: Iterable<string>): string[] {
83+
return [...new Set(labels)].toSorted((left, right) => left.localeCompare(right));
84+
}
85+
86+
function formatSortedSourceLabels(sorted: readonly string[]): string {
8487
if (sorted.length <= 3) {
8588
return sorted.join(", ");
8689
}
8790
return `${sorted.slice(0, 3).join(", ")} (+${sorted.length - 3} more)`;
8891
}
8992

93+
function formatSourceLabels(labels: Iterable<string>): string {
94+
return formatSortedSourceLabels(collectSortedSourceLabels(labels));
95+
}
96+
97+
function formatSourceLabelSubject(labels: Iterable<string>): { text: string; verb: "does" | "do" } {
98+
const sorted = collectSortedSourceLabels(labels);
99+
return {
100+
text: formatSortedSourceLabels(sorted),
101+
verb: sorted.length === 1 ? "does" : "do",
102+
};
103+
}
104+
90105
function collectToolOwners(registry: PluginManifestRegistry): Map<string, string[]> {
91106
const owners = new Map<string, string[]>();
92107
for (const plugin of registry.plugins) {
@@ -202,7 +217,7 @@ function buildEffectiveSandboxToolPolicy(params: {
202217
const allowLabels = [allow.label, alsoAllow.label].filter((label): label is string =>
203218
Boolean(label),
204219
);
205-
const labels = allowLabels.length > 0 ? allowLabels : ["default sandbox tool allowlist"];
220+
const labels = allowLabels.length > 0 ? allowLabels : ["tools.sandbox.tools.alsoAllow (unset)"];
206221
const dedupeLabels = Array.from(new Set([...labels, deny.label].filter(Boolean)));
207222

208223
return {
@@ -233,13 +248,16 @@ function collectActiveSandboxToolPolicies(cfg: OpenClawConfig): ActiveSandboxToo
233248
if (!hasRecord(agent)) {
234249
return;
235250
}
236-
const explicitMode = agent.sandbox?.mode;
251+
const agentSandbox = hasRecord(agent.sandbox) ? agent.sandbox : undefined;
252+
const explicitMode = agentSandbox?.mode;
237253
const agentSandboxActive =
238254
explicitMode === undefined ? defaultSandboxActive : isSandboxModeActive(explicitMode);
239255
if (!agentSandboxActive) {
240256
return;
241257
}
242-
const agentPolicy = hasRecord(agent.tools?.sandbox) ? agent.tools.sandbox.tools : undefined;
258+
const agentTools = hasRecord(agent.tools) ? agent.tools : undefined;
259+
const agentToolsSandbox = hasRecord(agentTools?.sandbox) ? agentTools.sandbox : undefined;
260+
const agentPolicy = hasRecord(agentToolsSandbox?.tools) ? agentToolsSandbox.tools : undefined;
243261
addPolicy(
244262
buildEffectiveSandboxToolPolicy({
245263
agentPolicy,
@@ -327,8 +345,9 @@ function collectSandboxMcpAllowlistWarnings(cfg: OpenClawConfig): string[] {
327345
if (issueSources.length === 0) {
328346
return [];
329347
}
348+
const sourceSubject = formatSourceLabelSubject(issueSources);
330349
return [
331-
`- mcp.servers defines ${formatMcpServerSummary(serverNames)}, but ${formatSourceLabels(issueSources)} does not include "bundle-mcp", "group:plugins", or a matching "<server>${TOOL_NAME_SEPARATOR}*" MCP tool pattern. Sandboxed agents will filter bundled MCP tools before provider requests. Add "bundle-mcp" to tools.sandbox.tools.alsoAllow (or use "group:plugins" / server globs) if those MCP tools should be visible; use tools.sandbox.tools.allow: [] only when you intentionally want no sandbox allow gate.`,
350+
`- mcp.servers defines ${formatMcpServerSummary(serverNames)}, but ${sourceSubject.text} ${sourceSubject.verb} not include "bundle-mcp", "group:plugins", or a matching "<server>${TOOL_NAME_SEPARATOR}*" MCP tool pattern. Sandboxed agents will filter bundled MCP tools before provider requests. Add "bundle-mcp" to tools.sandbox.tools.alsoAllow (or use "group:plugins" / server globs) if those MCP tools should be visible; use tools.sandbox.tools.allow: [] only when you intentionally want no sandbox allow gate.`,
332351
];
333352
}
334353

0 commit comments

Comments
 (0)