Files
Moltbot/src/gateway/tools-invoke-http.ts
2026-01-26 10:05:31 -08:00

270 lines
9.3 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from "node:http";
import { createClawdbotTools } from "../agents/clawdbot-tools.js";
import {
filterToolsByPolicy,
resolveEffectiveToolPolicy,
resolveGroupToolPolicy,
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";
import { getBearerToken, getHeader } from "./http-utils.js";
import {
readJsonBodyOrError,
sendInvalidRequest,
sendJson,
sendMethodNotAllowed,
sendUnauthorized,
} from "./http-common.js";
const DEFAULT_BODY_BYTES = 2 * 1024 * 1024;
type ToolsInvokeBody = {
tool?: unknown;
action?: unknown;
args?: unknown;
sessionKey?: unknown;
dryRun?: unknown;
};
function resolveSessionKeyFromBody(body: ToolsInvokeBody): string | undefined {
if (typeof body.sessionKey === "string" && body.sessionKey.trim()) return body.sessionKey.trim();
return undefined;
}
function mergeActionIntoArgsIfSupported(params: {
toolSchema: unknown;
action: string | undefined;
args: Record<string, unknown>;
}): Record<string, unknown> {
const { toolSchema, action, args } = params;
if (!action) return args;
if (args.action !== undefined) return args;
// TypeBox schemas are plain objects; many tools define an `action` property.
const schemaObj = toolSchema as { properties?: Record<string, unknown> } | null;
const hasAction = Boolean(
schemaObj &&
typeof schemaObj === "object" &&
schemaObj.properties &&
"action" in schemaObj.properties,
);
if (!hasAction) return args;
return { ...args, action };
}
export async function handleToolsInvokeHttpRequest(
req: IncomingMessage,
res: ServerResponse,
opts: { auth: ResolvedGatewayAuth; maxBodyBytes?: number; trustedProxies?: string[] },
): Promise<boolean> {
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
if (url.pathname !== "/tools/invoke") return false;
if (req.method !== "POST") {
sendMethodNotAllowed(res, "POST");
return true;
}
const cfg = loadConfig();
const token = getBearerToken(req);
const authResult = await authorizeGatewayConnect({
auth: opts.auth,
connectAuth: token ? { token, password: token } : null,
req,
trustedProxies: opts.trustedProxies ?? cfg.gateway?.trustedProxies,
});
if (!authResult.ok) {
sendUnauthorized(res);
return true;
}
const bodyUnknown = await readJsonBodyOrError(req, res, opts.maxBodyBytes ?? DEFAULT_BODY_BYTES);
if (bodyUnknown === undefined) return true;
const body = (bodyUnknown ?? {}) as ToolsInvokeBody;
const toolName = typeof body.tool === "string" ? body.tool.trim() : "";
if (!toolName) {
sendInvalidRequest(res, "tools.invoke requires body.tool");
return true;
}
const action = typeof body.action === "string" ? body.action.trim() : undefined;
const argsRaw = body.args;
const args = (
argsRaw && typeof argsRaw === "object" && !Array.isArray(argsRaw)
? (argsRaw as Record<string, unknown>)
: {}
) as Record<string, unknown>;
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(
getHeader(req, "x-clawdbot-message-channel") ?? "",
);
const accountId = getHeader(req, "x-clawdbot-account-id")?.trim() || undefined;
const {
agentId,
globalPolicy,
globalProviderPolicy,
agentPolicy,
agentProviderPolicy,
profile,
providerProfile,
profileAlsoAllow,
providerProfileAlsoAllow,
} = resolveEffectiveToolPolicy({ config: cfg, sessionKey });
const profilePolicy = resolveToolProfilePolicy(profile);
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({
config: cfg,
sessionKey,
messageProvider: messageChannel ?? undefined,
accountId: accountId ?? null,
});
const subagentPolicy = isSubagentSessionKey(sessionKey)
? resolveSubagentToolPolicy(cfg)
: undefined;
// 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 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. Use tools.alsoAllow for additive plugin tool enablement."
: "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(
profilePolicyWithAlsoAllow,
profile ? `tools.profile (${profile})` : "tools.profile",
);
const providerProfileExpanded = resolvePolicy(
providerProfilePolicyWithAlsoAllow,
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 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,
error: { type: "not_found", message: `Tool not available: ${toolName}` },
});
return true;
}
try {
const toolArgs = mergeActionIntoArgsIfSupported({
toolSchema: (tool as any).parameters,
action,
args,
});
const result = await (tool as any).execute?.(`http-${Date.now()}`, toolArgs);
sendJson(res, 200, { ok: true, result });
} catch (err) {
sendJson(res, 400, {
ok: false,
error: { type: "tool_error", message: err instanceof Error ? err.message : String(err) },
});
}
return true;
}