Files
openclaw/src/gateway/tools-invoke-http.ts
Peter Steinberger 00d8d7ead0 refactor: extract normalization core package
Extract shared normalization/coercion helpers into private @openclaw/normalization-core workspace package while preserving existing plugin SDK helper subpaths.\n\nAlso keeps direct normalization-core imports internal, wires UI/build/loader resolution, and replaces the slow PR network CodeQL lane with a fast added-line boundary scan while retaining full CodeQL for scheduled/manual runs.\n\nVerification: local moved tests, plugin SDK boundary tests, extension loader tests, agents-support shard, UI build/test, build artifacts, lint, workflow guards, autoreview, and GitHub CI passed on PR head 963d893715.
2026-05-31 01:33:00 +01:00

95 lines
3.2 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from "node:http";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { normalizeMessageChannel } from "../utils/message-channel.js";
import type { AuthRateLimiter } from "./auth-rate-limit.js";
import type { ResolvedGatewayAuth } from "./auth.js";
import { readJsonBodyOrError, sendJson, sendMethodNotAllowed } from "./http-common.js";
import {
authorizeScopedGatewayHttpRequestOrReply,
getHeader,
resolveOpenAiCompatibleHttpOperatorScopes,
resolveOpenAiCompatibleHttpSenderIsOwner,
} from "./http-utils.js";
import { invokeGatewayTool, type ToolsInvokeInput } from "./tools-invoke-shared.js";
const DEFAULT_BODY_BYTES = 2 * 1024 * 1024;
export async function handleToolsInvokeHttpRequest(
req: IncomingMessage,
res: ServerResponse,
opts: {
auth: ResolvedGatewayAuth;
maxBodyBytes?: number;
trustedProxies?: string[];
allowRealIpFallback?: boolean;
rateLimiter?: AuthRateLimiter;
},
): Promise<boolean> {
let url: URL;
try {
url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
} catch {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "bad_request", message: "Invalid request URL" }));
return true;
}
if (url.pathname !== "/tools/invoke") {
return false;
}
if (req.method !== "POST") {
sendMethodNotAllowed(res, "POST");
return true;
}
// /tools/invoke intentionally uses the same shared-secret HTTP trust model as
// the OpenAI-compatible APIs: token/password bearer auth is full operator
// access for the gateway, not a narrower per-request scope boundary.
const authResult = await authorizeScopedGatewayHttpRequestOrReply({
req,
res,
auth: opts.auth,
trustedProxies: opts.trustedProxies,
allowRealIpFallback: opts.allowRealIpFallback,
rateLimiter: opts.rateLimiter,
operatorMethod: "agent",
resolveOperatorScopes: resolveOpenAiCompatibleHttpOperatorScopes,
});
if (!authResult) {
return true;
}
const { cfg, requestAuth } = authResult;
const bodyUnknown = await readJsonBodyOrError(req, res, opts.maxBodyBytes ?? DEFAULT_BODY_BYTES);
if (bodyUnknown === undefined) {
return true;
}
const body = (bodyUnknown ?? {}) as ToolsInvokeInput;
// Resolve message channel/account hints (optional headers) for policy inheritance.
const messageChannel = normalizeMessageChannel(
getHeader(req, "x-openclaw-message-channel") ?? "",
);
const accountId = normalizeOptionalString(getHeader(req, "x-openclaw-account-id"));
const agentTo = normalizeOptionalString(getHeader(req, "x-openclaw-message-to"));
const agentThreadId = normalizeOptionalString(getHeader(req, "x-openclaw-thread-id"));
const senderIsOwner = resolveOpenAiCompatibleHttpSenderIsOwner(req, requestAuth);
const outcome = await invokeGatewayTool({
cfg,
input: body,
messageChannel: messageChannel ?? undefined,
accountId,
agentTo,
agentThreadId,
senderIsOwner,
toolCallIdPrefix: "http",
});
if (outcome.ok) {
sendJson(res, outcome.status, { ok: true, result: outcome.result });
} else {
sendJson(res, outcome.status, { ok: false, error: outcome.error });
}
return true;
}