fix: document tools invoke + honor main session key (#1575) (thanks @vignesh07)
This commit is contained in:
@@ -1,15 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { installGatewayTestHooks, getFreePort } from "./test-helpers.server.js";
|
||||
import { startGatewayServer } from "./server.js";
|
||||
import { installGatewayTestHooks, getFreePort, startGatewayServer } from "./test-helpers.server.js";
|
||||
import { testState } from "./test-helpers.mocks.js";
|
||||
|
||||
installGatewayTestHooks({ scope: "suite" });
|
||||
|
||||
describe("POST /tools/invoke", () => {
|
||||
it("invokes a tool and returns {ok:true,result}", async () => {
|
||||
testState.gatewayAuth = { mode: "none" } as any;
|
||||
|
||||
// Allow the sessions_list tool for main agent.
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
@@ -41,8 +38,7 @@ describe("POST /tools/invoke", () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it("rejects unauthorized when auth mode is token and header is missing", async () => {
|
||||
testState.gatewayAuth = { mode: "token", token: "t" } as any;
|
||||
it("accepts password auth when bearer token matches", async () => {
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{
|
||||
@@ -55,7 +51,42 @@ describe("POST /tools/invoke", () => {
|
||||
} as any;
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
const server = await startGatewayServer(port, {
|
||||
bind: "loopback",
|
||||
auth: { mode: "password", password: "secret" },
|
||||
});
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
authorization: "Bearer secret",
|
||||
},
|
||||
body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it("rejects unauthorized when auth mode is token and header is missing", async () => {
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
tools: {
|
||||
allow: ["sessions_list"],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, {
|
||||
bind: "loopback",
|
||||
auth: { mode: "token", token: "t" },
|
||||
});
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
@@ -69,7 +100,6 @@ describe("POST /tools/invoke", () => {
|
||||
});
|
||||
|
||||
it("returns 404 when tool is not allowlisted", async () => {
|
||||
testState.gatewayAuth = { mode: "none" } as any;
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{
|
||||
@@ -94,4 +124,77 @@ describe("POST /tools/invoke", () => {
|
||||
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it("respects tools.profile allowlist", async () => {
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
tools: {
|
||||
allow: ["sessions_list"],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
const { writeConfigFile } = await import("../config/config.js");
|
||||
await writeConfigFile({
|
||||
tools: { profile: "minimal" },
|
||||
} as any);
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it("uses the configured main session key when sessionKey is missing or main", async () => {
|
||||
testState.agentsConfig = {
|
||||
list: [
|
||||
{
|
||||
id: "main",
|
||||
tools: {
|
||||
deny: ["sessions_list"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "ops",
|
||||
default: true,
|
||||
tools: {
|
||||
allow: ["sessions_list"],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
testState.sessionConfig = { mainKey: "primary" };
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
|
||||
const payload = { tool: "sessions_list", action: "json", args: {} };
|
||||
|
||||
const resDefault = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
expect(resDefault.status).toBe(200);
|
||||
|
||||
const resMain = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ...payload, sessionKey: "main" }),
|
||||
});
|
||||
expect(resMain.status).toBe(200);
|
||||
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { resolveAgentIdFromSessionKey } from "../agents/agent-scope.js";
|
||||
import { createClawdbotTools } from "../agents/clawdbot-tools.js";
|
||||
import {
|
||||
filterToolsByPolicy,
|
||||
resolveEffectiveToolPolicy,
|
||||
resolveGroupToolPolicy,
|
||||
isToolAllowedByPolicies,
|
||||
resolveSubagentToolPolicy,
|
||||
} from "../agents/pi-tools.policy.js";
|
||||
import {
|
||||
buildPluginToolGroups,
|
||||
collectExplicitAllowlist,
|
||||
expandPolicyWithPluginGroups,
|
||||
normalizeToolName,
|
||||
resolveToolProfilePolicy,
|
||||
stripPluginOnlyAllowlist,
|
||||
} from "../agents/tool-policy.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { resolveMainSessionKey } from "../config/sessions.js";
|
||||
import { logWarn } from "../logger.js";
|
||||
import { getPluginToolMeta } from "../plugins/tools.js";
|
||||
import { isSubagentSessionKey } from "../routing/session-key.js";
|
||||
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
||||
|
||||
import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js";
|
||||
@@ -71,7 +83,7 @@ export async function handleToolsInvokeHttpRequest(
|
||||
const token = getBearerToken(req);
|
||||
const authResult = await authorizeGatewayConnect({
|
||||
auth: opts.auth,
|
||||
connectAuth: token ? { token } : null,
|
||||
connectAuth: token ? { token, password: token } : null,
|
||||
req,
|
||||
});
|
||||
if (!authResult.ok) {
|
||||
@@ -98,9 +110,10 @@ export async function handleToolsInvokeHttpRequest(
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
|
||||
const sessionKey = resolveSessionKeyFromBody(body) ?? "main";
|
||||
const cfg = loadConfig();
|
||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||
const rawSessionKey = resolveSessionKeyFromBody(body);
|
||||
const sessionKey =
|
||||
!rawSessionKey || rawSessionKey === "main" ? resolveMainSessionKey(cfg) : rawSessionKey;
|
||||
|
||||
// Resolve message channel/account hints (optional headers) for policy inheritance.
|
||||
const messageChannel = normalizeMessageChannel(
|
||||
@@ -108,34 +121,113 @@ export async function handleToolsInvokeHttpRequest(
|
||||
);
|
||||
const accountId = getHeader(req, "x-clawdbot-account-id")?.trim() || undefined;
|
||||
|
||||
// Build tool list (core + plugin tools).
|
||||
const allTools = createClawdbotTools({
|
||||
agentSessionKey: sessionKey,
|
||||
agentChannel: messageChannel ?? undefined,
|
||||
agentAccountId: accountId,
|
||||
config: cfg,
|
||||
});
|
||||
|
||||
const policy = resolveEffectiveToolPolicy({ config: cfg, sessionKey });
|
||||
const {
|
||||
agentId,
|
||||
globalPolicy,
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
profile,
|
||||
providerProfile,
|
||||
} = resolveEffectiveToolPolicy({ config: cfg, sessionKey });
|
||||
const profilePolicy = resolveToolProfilePolicy(profile);
|
||||
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
||||
const groupPolicy = resolveGroupToolPolicy({
|
||||
config: cfg,
|
||||
sessionKey,
|
||||
messageProvider: messageChannel ?? undefined,
|
||||
accountId: accountId ?? null,
|
||||
});
|
||||
const subagentPolicy = isSubagentSessionKey(sessionKey)
|
||||
? resolveSubagentToolPolicy(cfg)
|
||||
: undefined;
|
||||
|
||||
const allowed = (name: string) =>
|
||||
isToolAllowedByPolicies(name, [
|
||||
policy.globalPolicy,
|
||||
policy.agentPolicy,
|
||||
policy.globalProviderPolicy,
|
||||
policy.agentProviderPolicy,
|
||||
// Build tool list (core + plugin tools).
|
||||
const allTools = createClawdbotTools({
|
||||
agentSessionKey: sessionKey,
|
||||
agentChannel: messageChannel ?? undefined,
|
||||
agentAccountId: accountId,
|
||||
config: cfg,
|
||||
pluginToolAllowlist: collectExplicitAllowlist([
|
||||
profilePolicy,
|
||||
providerProfilePolicy,
|
||||
globalPolicy,
|
||||
globalProviderPolicy,
|
||||
agentPolicy,
|
||||
agentProviderPolicy,
|
||||
groupPolicy,
|
||||
]);
|
||||
subagentPolicy,
|
||||
]),
|
||||
});
|
||||
|
||||
const tools = (allTools as any[]).filter((t) => allowed(t.name));
|
||||
const coreToolNames = new Set(
|
||||
allTools
|
||||
.filter((tool) => !getPluginToolMeta(tool as any))
|
||||
.map((tool) => normalizeToolName(tool.name))
|
||||
.filter(Boolean),
|
||||
);
|
||||
const pluginGroups = buildPluginToolGroups({
|
||||
tools: allTools,
|
||||
toolMeta: (tool) => getPluginToolMeta(tool as any),
|
||||
});
|
||||
const resolvePolicy = (policy: typeof profilePolicy, label: string) => {
|
||||
const resolved = stripPluginOnlyAllowlist(policy, pluginGroups, coreToolNames);
|
||||
if (resolved.unknownAllowlist.length > 0) {
|
||||
const entries = resolved.unknownAllowlist.join(", ");
|
||||
const suffix = resolved.strippedAllowlist
|
||||
? "Ignoring allowlist so core tools remain available."
|
||||
: "These entries won't match any tool unless the plugin is enabled.";
|
||||
logWarn(`tools: ${label} allowlist contains unknown entries (${entries}). ${suffix}`);
|
||||
}
|
||||
return expandPolicyWithPluginGroups(resolved.policy, pluginGroups);
|
||||
};
|
||||
const profilePolicyExpanded = resolvePolicy(
|
||||
profilePolicy,
|
||||
profile ? `tools.profile (${profile})` : "tools.profile",
|
||||
);
|
||||
const providerProfileExpanded = resolvePolicy(
|
||||
providerProfilePolicy,
|
||||
providerProfile ? `tools.byProvider.profile (${providerProfile})` : "tools.byProvider.profile",
|
||||
);
|
||||
const globalPolicyExpanded = resolvePolicy(globalPolicy, "tools.allow");
|
||||
const globalProviderExpanded = resolvePolicy(globalProviderPolicy, "tools.byProvider.allow");
|
||||
const agentPolicyExpanded = resolvePolicy(
|
||||
agentPolicy,
|
||||
agentId ? `agents.${agentId}.tools.allow` : "agent tools.allow",
|
||||
);
|
||||
const agentProviderExpanded = resolvePolicy(
|
||||
agentProviderPolicy,
|
||||
agentId ? `agents.${agentId}.tools.byProvider.allow` : "agent tools.byProvider.allow",
|
||||
);
|
||||
const groupPolicyExpanded = resolvePolicy(groupPolicy, "group tools.allow");
|
||||
const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups);
|
||||
|
||||
const tool = tools.find((t) => t.name === toolName);
|
||||
const toolsFiltered = profilePolicyExpanded
|
||||
? filterToolsByPolicy(allTools, profilePolicyExpanded)
|
||||
: allTools;
|
||||
const providerProfileFiltered = providerProfileExpanded
|
||||
? filterToolsByPolicy(toolsFiltered, providerProfileExpanded)
|
||||
: toolsFiltered;
|
||||
const globalFiltered = globalPolicyExpanded
|
||||
? filterToolsByPolicy(providerProfileFiltered, globalPolicyExpanded)
|
||||
: providerProfileFiltered;
|
||||
const globalProviderFiltered = globalProviderExpanded
|
||||
? filterToolsByPolicy(globalFiltered, globalProviderExpanded)
|
||||
: globalFiltered;
|
||||
const agentFiltered = agentPolicyExpanded
|
||||
? filterToolsByPolicy(globalProviderFiltered, agentPolicyExpanded)
|
||||
: globalProviderFiltered;
|
||||
const agentProviderFiltered = agentProviderExpanded
|
||||
? filterToolsByPolicy(agentFiltered, agentProviderExpanded)
|
||||
: agentFiltered;
|
||||
const groupFiltered = groupPolicyExpanded
|
||||
? filterToolsByPolicy(agentProviderFiltered, groupPolicyExpanded)
|
||||
: agentProviderFiltered;
|
||||
const subagentFiltered = subagentPolicyExpanded
|
||||
? filterToolsByPolicy(groupFiltered, subagentPolicyExpanded)
|
||||
: groupFiltered;
|
||||
|
||||
const tool = subagentFiltered.find((t) => t.name === toolName);
|
||||
if (!tool) {
|
||||
sendJson(res, 404, {
|
||||
ok: false,
|
||||
|
||||
Reference in New Issue
Block a user