mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-30 05:27:36 +02:00
fix(agents): expose configured MCP tools in Pi profiles
This commit is contained in:
@@ -33,6 +33,9 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/Amazon Bedrock Mantle: refresh IAM-backed bearer tokens at runtime instead of baking discovery-time tokens into provider config, so long-lived Mantle sessions keep working after the initial token ages out. Thanks @wirjo.
|
||||
- Config/includes: write through single-file top-level includes for isolated OpenClaw-owned mutations, so `plugins install` and `plugins update` update an included `plugins.json5` file instead of flattening modular `$include` configs. Fixes #41050 and #66048.
|
||||
- Config/reload: plan gateway reloads from source-authored config instead of runtime-materialized snapshots, so plugin update writes no longer trigger false restarts from derived provider/plugin config paths. Fixes #68732.
|
||||
- Agents/MCP: keep `mcp.servers` and bundle MCP tools available in Pi embedded
|
||||
`coding` and `messaging` sessions while preserving `minimal` profile and
|
||||
`tools.deny: ["bundle-mcp"]` opt-out behavior. Fixes #68875 and #68818.
|
||||
- Codex harness: rotate the shared app-server websocket client when the configured bearer token changes, so auth-token refreshes reconnect with the new `Authorization` header instead of reusing a stale socket. (#70328) Thanks @Lucenx9.
|
||||
- Telegram/sandbox: keep Telegram bot DMs on per-account sender session keys even when `session.dmScope=main`, so sandbox/tool policy can distinguish Telegram-originated direct chats from the agent main session.
|
||||
- Config/models: merge provider-scoped model allowlist updates and protect model/provider map writes from accidental full replacement, adding `config set --merge` for additive updates and `--replace` for intentional clobbers. Fixes #65920, #68392, and #68653.
|
||||
|
||||
@@ -369,6 +369,9 @@ Important behavior:
|
||||
reachable right now
|
||||
- runtime adapters decide which transport shapes they actually support at
|
||||
execution time
|
||||
- embedded Pi exposes configured MCP tools in normal `coding` and `messaging`
|
||||
tool profiles; `minimal` still hides them, and `tools.deny: ["bundle-mcp"]`
|
||||
disables them explicitly
|
||||
|
||||
## Saved MCP server definitions
|
||||
|
||||
|
||||
@@ -882,7 +882,7 @@ These Docker runners split into two buckets:
|
||||
`OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=90000`. Override those env vars when you
|
||||
explicitly want the larger exhaustive scan.
|
||||
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, then reuses it for the two live Docker lanes.
|
||||
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:gateway-network`, `test:docker:mcp-channels`, and `test:docker:plugins` boot one or more real containers and verify higher-level integration paths.
|
||||
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:gateway-network`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, and `test:docker:plugins` boot one or more real containers and verify higher-level integration paths.
|
||||
|
||||
The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:
|
||||
|
||||
@@ -895,6 +895,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
|
||||
- Onboarding wizard (TTY, full scaffolding): `pnpm test:docker:onboard` (script: `scripts/e2e/onboard-docker.sh`)
|
||||
- Gateway networking (two containers, WS auth + health): `pnpm test:docker:gateway-network` (script: `scripts/e2e/gateway-network-docker.sh`)
|
||||
- MCP channel bridge (seeded Gateway + stdio bridge + raw Claude notification-frame smoke): `pnpm test:docker:mcp-channels` (script: `scripts/e2e/mcp-channels-docker.sh`)
|
||||
- Pi bundle MCP tools (real stdio MCP server + embedded Pi profile allow/deny smoke): `pnpm test:docker:pi-bundle-mcp-tools` (script: `scripts/e2e/pi-bundle-mcp-tools-docker.sh`)
|
||||
- Plugins (install smoke + `/plugin` alias + Claude-bundle restart semantics): `pnpm test:docker:plugins` (script: `scripts/e2e/plugins-docker.sh`)
|
||||
- Bundled plugin runtime deps: `pnpm test:docker:bundled-channel-deps` builds a small Docker runner image by default, builds and packs OpenClaw once on the host, then mounts that tarball into each Linux install scenario. Reuse the image with `OPENCLAW_SKIP_DOCKER_BUILD=1`, skip the host rebuild after a fresh local build with `OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0`, or point at an existing tarball with `OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ=/path/to/openclaw-*.tgz`.
|
||||
- Narrow bundled plugin runtime deps while iterating by disabling unrelated scenarios, for example:
|
||||
@@ -931,6 +932,11 @@ live event queue behavior, outbound send routing, and Claude-style channel +
|
||||
permission notifications over the real stdio MCP bridge. The notification check
|
||||
inspects the raw stdio MCP frames directly so the smoke validates what the
|
||||
bridge actually emits, not just what a specific client SDK happens to surface.
|
||||
`test:docker:pi-bundle-mcp-tools` is deterministic and does not need a live
|
||||
model key. It builds the repo Docker image, starts a real stdio MCP probe server
|
||||
inside the container, materializes that server through the embedded Pi bundle
|
||||
MCP runtime, executes the tool, then verifies `coding` and `messaging` keep
|
||||
`bundle-mcp` tools while `minimal` and `tools.deny: ["bundle-mcp"]` filter them.
|
||||
|
||||
Manual ACP plain-language thread smoke (not CI):
|
||||
|
||||
|
||||
@@ -104,6 +104,8 @@ loader. Cursor command markdown works through the same path.
|
||||
`mcpServers`
|
||||
- OpenClaw exposes supported bundle MCP tools during embedded Pi agent turns by
|
||||
launching stdio servers or connecting to HTTP servers
|
||||
- the `coding` and `messaging` tool profiles include bundle MCP tools by
|
||||
default; use `tools.deny: ["bundle-mcp"]` to opt out for an agent or gateway
|
||||
- project-local Pi settings still apply after bundle defaults, so workspace
|
||||
settings can override bundle MCP entries when needed
|
||||
- bundle MCP tool catalogs are sorted deterministically before registration, so
|
||||
@@ -170,6 +172,9 @@ OpenClaw registers bundle MCP tools with provider-safe names in the form
|
||||
- colliding sanitized names are disambiguated with numeric suffixes
|
||||
- final exposed tool order is deterministic by safe name to keep repeated Pi
|
||||
turns cache-stable
|
||||
- profile filtering treats all tools from one bundle MCP server as plugin-owned
|
||||
by `bundle-mcp`, so profile allowlists and deny lists can include either
|
||||
individual exposed tool names or the `bundle-mcp` plugin key
|
||||
|
||||
#### Embedded Pi settings
|
||||
|
||||
|
||||
@@ -139,6 +139,11 @@ Per-agent override: `agents.list[].tools.profile`.
|
||||
| `messaging` | `group:messaging`, `sessions_list`, `sessions_history`, `sessions_send`, `session_status` |
|
||||
| `minimal` | `session_status` only |
|
||||
|
||||
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.
|
||||
The `minimal` profile does not include bundle MCP tools.
|
||||
|
||||
### Tool groups
|
||||
|
||||
Use `group:*` shorthands in allow/deny lists:
|
||||
|
||||
@@ -1412,7 +1412,7 @@
|
||||
"test:contracts:plugins": "node scripts/run-vitest.mjs run --config test/vitest/vitest.contracts-plugin.config.ts --maxWorkers=1",
|
||||
"test:coverage": "node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --coverage",
|
||||
"test:coverage:changed": "node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts --coverage --changed origin/main",
|
||||
"test:docker:all": "pnpm test:docker:live-build && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-models && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-gateway && pnpm test:docker:openwebui && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:mcp-channels && pnpm test:docker:cron-mcp-cleanup && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:bundled-channel-deps && pnpm test:docker:cleanup",
|
||||
"test:docker:all": "pnpm test:docker:live-build && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-models && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-gateway && pnpm test:docker:openwebui && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:mcp-channels && pnpm test:docker:pi-bundle-mcp-tools && pnpm test:docker:cron-mcp-cleanup && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:bundled-channel-deps && pnpm test:docker:cleanup",
|
||||
"test:docker:bundled-channel-deps": "bash scripts/e2e/bundled-channel-runtime-deps-docker.sh",
|
||||
"test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
|
||||
"test:docker:cron-mcp-cleanup": "bash scripts/e2e/cron-mcp-cleanup-docker.sh",
|
||||
@@ -1440,6 +1440,7 @@
|
||||
"test:docker:mcp-channels": "bash scripts/e2e/mcp-channels-docker.sh",
|
||||
"test:docker:onboard": "bash scripts/e2e/onboard-docker.sh",
|
||||
"test:docker:openwebui": "bash scripts/e2e/openwebui-docker.sh",
|
||||
"test:docker:pi-bundle-mcp-tools": "bash scripts/e2e/pi-bundle-mcp-tools-docker.sh",
|
||||
"test:docker:plugins": "bash scripts/e2e/plugins-docker.sh",
|
||||
"test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
|
||||
"test:e2e": "node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts",
|
||||
|
||||
157
scripts/e2e/pi-bundle-mcp-tools-docker-client.ts
Normal file
157
scripts/e2e/pi-bundle-mcp-tools-docker-client.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { materializeBundleMcpToolsForRun } from "../../src/agents/pi-bundle-mcp-materialize.ts";
|
||||
import {
|
||||
disposeAllSessionMcpRuntimes,
|
||||
getOrCreateSessionMcpRuntime,
|
||||
} from "../../src/agents/pi-bundle-mcp-runtime.ts";
|
||||
import { applyFinalEffectiveToolPolicy } from "../../src/agents/pi-embedded-runner/effective-tool-policy.ts";
|
||||
import type { OpenClawConfig } from "../../src/config/types.openclaw.ts";
|
||||
import { getPluginToolMeta } from "../../src/plugins/tools.ts";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function writeProbeServer(serverPath: string) {
|
||||
const sdkMcpServerPath = require.resolve("@modelcontextprotocol/sdk/server/mcp.js");
|
||||
const sdkStdioServerPath = require.resolve("@modelcontextprotocol/sdk/server/stdio.js");
|
||||
await fs.writeFile(
|
||||
serverPath,
|
||||
`#!/usr/bin/env node
|
||||
import { McpServer } from ${JSON.stringify(sdkMcpServerPath)};
|
||||
import { StdioServerTransport } from ${JSON.stringify(sdkStdioServerPath)};
|
||||
|
||||
const server = new McpServer({ name: "pi-bundle-mcp-tools-probe", version: "1.0.0" });
|
||||
server.tool("docker_probe", "Docker Pi MCP tool availability probe", async () => ({
|
||||
content: [{ type: "text", text: "pi-bundle-mcp-tools-ok" }],
|
||||
}));
|
||||
|
||||
await server.connect(new StdioServerTransport());
|
||||
`,
|
||||
{ encoding: "utf-8", mode: 0o755 },
|
||||
);
|
||||
}
|
||||
|
||||
function applyPolicy(params: {
|
||||
tools: Awaited<ReturnType<typeof materializeBundleMcpToolsForRun>>["tools"];
|
||||
config: OpenClawConfig;
|
||||
}) {
|
||||
const warnings: string[] = [];
|
||||
return {
|
||||
tools: applyFinalEffectiveToolPolicy({
|
||||
bundledTools: params.tools,
|
||||
config: params.config,
|
||||
sessionKey: "agent:main:docker-pi-bundle-mcp",
|
||||
agentId: "main",
|
||||
senderIsOwner: true,
|
||||
warn: (message) => {
|
||||
warnings.push(message);
|
||||
},
|
||||
}),
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const stateDir =
|
||||
process.env.OPENCLAW_STATE_DIR?.trim() ||
|
||||
path.join(os.tmpdir(), `openclaw-pi-bundle-mcp-${process.pid}`);
|
||||
const probeDir = path.join(stateDir, "pi-bundle-mcp-tools");
|
||||
const serverPath = path.join(probeDir, "probe-server.mjs");
|
||||
await fs.mkdir(probeDir, { recursive: true });
|
||||
await writeProbeServer(serverPath);
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
tools: {
|
||||
profile: "coding",
|
||||
},
|
||||
mcp: {
|
||||
servers: {
|
||||
dockerProbe: {
|
||||
command: "node",
|
||||
args: [serverPath],
|
||||
cwd: probeDir,
|
||||
connectionTimeoutMs: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const runtime = await getOrCreateSessionMcpRuntime({
|
||||
sessionId: `docker-pi-bundle-mcp-${randomUUID()}`,
|
||||
sessionKey: "agent:main:docker-pi-bundle-mcp",
|
||||
workspaceDir: probeDir,
|
||||
cfg,
|
||||
});
|
||||
const materialized = await materializeBundleMcpToolsForRun({ runtime });
|
||||
const probeTool = materialized.tools.find((tool) => tool.name === "dockerProbe__docker_probe");
|
||||
assert(probeTool, "expected dockerProbe__docker_probe to materialize");
|
||||
assert(
|
||||
getPluginToolMeta(probeTool)?.pluginId === "bundle-mcp",
|
||||
"expected materialized MCP tool to be tagged as bundle-mcp",
|
||||
);
|
||||
|
||||
const result = await probeTool.execute("docker-mcp-probe", {}, undefined, undefined);
|
||||
assert(
|
||||
result.content.some((item) => item.type === "text" && item.text === "pi-bundle-mcp-tools-ok"),
|
||||
"expected materialized MCP tool execution result",
|
||||
);
|
||||
|
||||
const coding = applyPolicy({ tools: materialized.tools, config: cfg });
|
||||
assert(
|
||||
coding.tools.some((tool) => tool.name === probeTool.name),
|
||||
"expected coding profile to keep bundle MCP tools",
|
||||
);
|
||||
|
||||
const messaging = applyPolicy({
|
||||
tools: materialized.tools,
|
||||
config: { ...cfg, tools: { profile: "messaging" } },
|
||||
});
|
||||
assert(
|
||||
messaging.tools.some((tool) => tool.name === probeTool.name),
|
||||
"expected messaging profile to keep bundle MCP tools",
|
||||
);
|
||||
|
||||
const minimal = applyPolicy({
|
||||
tools: materialized.tools,
|
||||
config: { ...cfg, tools: { profile: "minimal" } },
|
||||
});
|
||||
assert(minimal.tools.length === 0, "expected minimal profile to filter bundle MCP tools");
|
||||
|
||||
const denied = applyPolicy({
|
||||
tools: materialized.tools,
|
||||
config: { ...cfg, tools: { profile: "coding", deny: ["bundle-mcp"] } },
|
||||
});
|
||||
assert(denied.tools.length === 0, "expected tools.deny bundle-mcp to filter MCP tools");
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
tool: probeTool.name,
|
||||
profileCounts: {
|
||||
coding: coding.tools.length,
|
||||
messaging: messaging.tools.length,
|
||||
minimal: minimal.tools.length,
|
||||
denied: denied.tools.length,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
} finally {
|
||||
await disposeAllSessionMcpRuntimes();
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
40
scripts/e2e/pi-bundle-mcp-tools-docker.sh
Executable file
40
scripts/e2e/pi-bundle-mcp-tools-docker.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh"
|
||||
IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw-pi-bundle-mcp-tools-e2e}"
|
||||
CONTAINER_NAME="openclaw-pi-bundle-mcp-tools-e2e-$$"
|
||||
RUN_LOG="$(mktemp -t openclaw-pi-bundle-mcp-tools-log.XXXXXX)"
|
||||
|
||||
cleanup() {
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
rm -f "$RUN_LOG"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [ "${OPENCLAW_SKIP_DOCKER_BUILD:-0}" != "1" ]; then
|
||||
echo "Building Docker image..."
|
||||
run_logged pi-bundle-mcp-tools-build docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR"
|
||||
fi
|
||||
|
||||
echo "Running in-container Pi bundle MCP tool availability smoke..."
|
||||
set +e
|
||||
docker run --rm \
|
||||
--name "$CONTAINER_NAME" \
|
||||
-e "OPENCLAW_STATE_DIR=/tmp/openclaw-state" \
|
||||
"$IMAGE_NAME" \
|
||||
bash -lc "set -euo pipefail
|
||||
node --import tsx scripts/e2e/pi-bundle-mcp-tools-docker-client.ts
|
||||
" >"$RUN_LOG" 2>&1
|
||||
status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "Docker Pi bundle MCP tool availability smoke failed"
|
||||
cat "$RUN_LOG"
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
cat "$RUN_LOG"
|
||||
echo "OK"
|
||||
@@ -2,6 +2,7 @@ import { spawn, type ChildProcess } from "node:child_process";
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { logDebug, logWarn } from "../logger.js";
|
||||
import { setPluginToolMeta } from "../plugins/tools.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import { loadEmbeddedPiLspConfig } from "./embedded-pi-lsp.js";
|
||||
import {
|
||||
@@ -368,6 +369,10 @@ export async function createBundleLspToolRuntime(params: {
|
||||
continue;
|
||||
}
|
||||
reservedNames.add(normalizedName);
|
||||
setPluginToolMeta(tool, {
|
||||
pluginId: "bundle-lsp",
|
||||
optional: false,
|
||||
});
|
||||
tools.push(tool);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { logWarn } from "../logger.js";
|
||||
import { setPluginToolMeta } from "../plugins/tools.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import {
|
||||
buildSafeToolName,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
TOOL_NAME_SEPARATOR,
|
||||
} from "./pi-bundle-mcp-names.js";
|
||||
import type { BundleMcpToolRuntime, SessionMcpRuntime } from "./pi-bundle-mcp-types.js";
|
||||
import type { AnyAgentTool } from "./tools/common.js";
|
||||
|
||||
function toAgentToolResult(params: {
|
||||
serverName: string;
|
||||
@@ -96,7 +98,7 @@ export async function materializeBundleMcpToolsForRun(params: {
|
||||
);
|
||||
}
|
||||
reservedNames.add(normalizeLowercaseStringOrEmpty(safeToolName));
|
||||
tools.push({
|
||||
const agentTool: AnyAgentTool = {
|
||||
name: safeToolName,
|
||||
label: tool.title ?? tool.toolName,
|
||||
description: tool.description || tool.fallbackDescription,
|
||||
@@ -109,7 +111,12 @@ export async function materializeBundleMcpToolsForRun(params: {
|
||||
result,
|
||||
});
|
||||
},
|
||||
};
|
||||
setPluginToolMeta(agentTool, {
|
||||
pluginId: "bundle-mcp",
|
||||
optional: false,
|
||||
});
|
||||
tools.push(agentTool);
|
||||
}
|
||||
|
||||
// Sort tools deterministically by name so the tools block in API requests is stable across
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getPluginToolMeta } from "../plugins/tools.js";
|
||||
import {
|
||||
createBundleMcpToolRuntime,
|
||||
materializeBundleMcpToolsForRun,
|
||||
@@ -58,6 +59,7 @@ describe("createBundleMcpToolRuntime", () => {
|
||||
});
|
||||
|
||||
expect(runtime.tools.map((tool) => tool.name)).toEqual(["bundleProbe__bundle_probe"]);
|
||||
expect(getPluginToolMeta(runtime.tools[0])?.pluginId).toBe("bundle-mcp");
|
||||
const result = await runtime.tools[0].execute("call-bundle-probe", {}, undefined, undefined);
|
||||
expect(result.content[0]).toMatchObject({
|
||||
type: "text",
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import "./test-helpers/fast-coding-tools.js";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { setPluginToolMeta } from "../plugins/tools.js";
|
||||
import {
|
||||
cleanupEmbeddedPiRunnerTestWorkspace,
|
||||
createEmbeddedPiRunnerOpenAiConfig,
|
||||
@@ -53,24 +54,26 @@ vi.mock("./pi-bundle-mcp-tools.js", () => ({
|
||||
}),
|
||||
dispose: async () => {},
|
||||
}),
|
||||
materializeBundleMcpToolsForRun: async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: "bundleProbe__bundle_probe",
|
||||
label: "bundle_probe",
|
||||
description: "Bundle MCP probe",
|
||||
parameters: { type: "object", properties: {} },
|
||||
execute: async () => ({
|
||||
content: [{ type: "text", text: "FROM-BUNDLE" }],
|
||||
details: {
|
||||
mcpServer: "bundleProbe",
|
||||
mcpTool: "bundle_probe",
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
dispose: async () => {},
|
||||
}),
|
||||
materializeBundleMcpToolsForRun: async () => {
|
||||
const tool = {
|
||||
name: "bundleProbe__bundle_probe",
|
||||
label: "bundle_probe",
|
||||
description: "Bundle MCP probe",
|
||||
parameters: { type: "object", properties: {} },
|
||||
execute: async () => ({
|
||||
content: [{ type: "text", text: "FROM-BUNDLE" }],
|
||||
details: {
|
||||
mcpServer: "bundleProbe",
|
||||
mcpTool: "bundle_probe",
|
||||
},
|
||||
}),
|
||||
};
|
||||
setPluginToolMeta(tool as any, { pluginId: "bundle-mcp", optional: false });
|
||||
return {
|
||||
tools: [tool],
|
||||
dispose: async () => {},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@mariozechner/pi-ai", async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { setPluginToolMeta } from "../../plugins/tools.js";
|
||||
import type { AnyAgentTool } from "../tools/common.js";
|
||||
import { applyFinalEffectiveToolPolicy } from "./effective-tool-policy.js";
|
||||
|
||||
@@ -117,4 +118,30 @@ describe("applyFinalEffectiveToolPolicy", () => {
|
||||
|
||||
expect(warnings.some((w) => w.includes("totally-made-up-tool"))).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps bundle MCP tools in the coding profile via plugin metadata", () => {
|
||||
const mcpTool = makeTool("bundleProbe__bundle_probe");
|
||||
setPluginToolMeta(mcpTool, { pluginId: "bundle-mcp", optional: false });
|
||||
|
||||
const filtered = applyFinalEffectiveToolPolicy({
|
||||
bundledTools: [mcpTool],
|
||||
config: { tools: { profile: "coding" } },
|
||||
warn: () => {},
|
||||
});
|
||||
|
||||
expect(filtered.map((tool) => tool.name)).toEqual(["bundleProbe__bundle_probe"]);
|
||||
});
|
||||
|
||||
it("lets explicit deny entries override the profile bundle MCP allowlist", () => {
|
||||
const mcpTool = makeTool("bundleProbe__bundle_probe");
|
||||
setPluginToolMeta(mcpTool, { pluginId: "bundle-mcp", optional: false });
|
||||
|
||||
const filtered = applyFinalEffectiveToolPolicy({
|
||||
bundledTools: [mcpTool],
|
||||
config: { tools: { profile: "coding", deny: ["bundle-mcp"] } },
|
||||
warn: () => {},
|
||||
});
|
||||
|
||||
expect(filtered).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,4 +14,10 @@ describe("tool-catalog", () => {
|
||||
expect(policy!.allow).toContain("video_generate");
|
||||
expect(policy!.allow).toContain("update_plan");
|
||||
});
|
||||
|
||||
it("includes bundle MCP tools in coding and messaging profile policies", () => {
|
||||
expect(resolveCoreToolProfilePolicy("coding")?.allow).toContain("bundle-mcp");
|
||||
expect(resolveCoreToolProfilePolicy("messaging")?.allow).toContain("bundle-mcp");
|
||||
expect(resolveCoreToolProfilePolicy("minimal")?.allow).not.toContain("bundle-mcp");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -318,10 +318,10 @@ const CORE_TOOL_PROFILES: Record<ToolProfileId, ToolProfilePolicy> = {
|
||||
allow: listCoreToolIdsForProfile("minimal"),
|
||||
},
|
||||
coding: {
|
||||
allow: listCoreToolIdsForProfile("coding"),
|
||||
allow: [...listCoreToolIdsForProfile("coding"), "bundle-mcp"],
|
||||
},
|
||||
messaging: {
|
||||
allow: listCoreToolIdsForProfile("messaging"),
|
||||
allow: [...listCoreToolIdsForProfile("messaging"), "bundle-mcp"],
|
||||
},
|
||||
full: {},
|
||||
};
|
||||
|
||||
@@ -128,7 +128,9 @@ export function applyToolPolicyPipeline(params: {
|
||||
const warnableGatedCoreEntries = step.suppressUnavailableCoreToolWarning
|
||||
? []
|
||||
: gatedCoreEntries.filter((entry) => !unavailableCoreWarningAllowlist.has(entry));
|
||||
const otherEntries = resolved.unknownAllowlist.filter((entry) => !isKnownCoreToolId(entry));
|
||||
const otherEntries = resolved.unknownAllowlist.filter(
|
||||
(entry) => !isKnownCoreToolId(entry) && !unavailableCoreWarningAllowlist.has(entry),
|
||||
);
|
||||
const warningEntries = [...warnableGatedCoreEntries, ...otherEntries];
|
||||
if (
|
||||
shouldWarnAboutUnknownAllowlist({
|
||||
|
||||
@@ -13,13 +13,17 @@ import {
|
||||
} from "./runtime/load-context.js";
|
||||
import type { OpenClawPluginToolContext } from "./types.js";
|
||||
|
||||
type PluginToolMeta = {
|
||||
export type PluginToolMeta = {
|
||||
pluginId: string;
|
||||
optional: boolean;
|
||||
};
|
||||
|
||||
const pluginToolMeta = new WeakMap<AnyAgentTool, PluginToolMeta>();
|
||||
|
||||
export function setPluginToolMeta(tool: AnyAgentTool, meta: PluginToolMeta): void {
|
||||
pluginToolMeta.set(tool, meta);
|
||||
}
|
||||
|
||||
export function getPluginToolMeta(tool: AnyAgentTool): PluginToolMeta | undefined {
|
||||
return pluginToolMeta.get(tool);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user