refactor(gateway): centralize parameter validation and shared helpers (#114353)

* refactor(gateway): centralize parameter validation

* refactor(outbound): centralize runtime scaffolding stripping

* refactor(usage): remove session cost forwarding layers

* refactor(gateway): share attachment failure logging

* refactor(gateway): centralize startup tracing helpers

* fix(usage): keep refresh result type internal

* fix(outbound): preserve visible whitespace around runtime context
This commit is contained in:
Peter Steinberger
2026-07-27 05:34:12 -04:00
committed by GitHub
parent 0680dfc6a9
commit 84a4ff30d5
46 changed files with 321 additions and 882 deletions
@@ -109,15 +109,6 @@ describe("discordOutbound", () => {
});
});
it("sanitizes internal runtime scaffolding before Discord delivery", () => {
expect(
discordOutbound.sanitizeText?.({
text: "<previous_response>null</previous_response>visible",
payload: { text: "<previous_response>null</previous_response>visible" },
}),
).toBe("visible");
});
it("uses allowFrom to disambiguate bare numeric DM delivery targets", () => {
expect(
discordOutbound.resolveTarget?.({
@@ -130,17 +121,6 @@ describe("discordOutbound", () => {
});
});
it("preserves Discord-native angle markup while stripping internal scaffolding", () => {
expect(
discordOutbound.sanitizeText?.({
text: "soon <t:1710000000:R> run </deploy:123> <previous_response>null</previous_response>",
payload: {
text: "soon <t:1710000000:R> run </deploy:123> <previous_response>null</previous_response>",
},
}),
).toBe("soon <t:1710000000:R> run </deploy:123> ");
});
it("forwards explicit formatting options to Discord text sends", async () => {
await discordOutbound.sendText?.({
cfg: {},
@@ -37,20 +37,6 @@ import {
import { resolveDiscordReplyReference } from "./reply-reference.js";
export const DISCORD_TEXT_CHUNK_LIMIT = 2000;
const DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_BLOCK_RE =
/<\s*(system-reminder|previous_response)\b[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi;
const DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_SELF_CLOSING_RE =
/<\s*(?:system-reminder|previous_response)\b[^>]*\/\s*>/gi;
const DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_TAG_RE =
/<\s*\/?\s*(?:system-reminder|previous_response)\b[^>]*>/gi;
function stripDiscordInternalRuntimeScaffolding(text: string): string {
return text
.replace(DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_BLOCK_RE, "")
.replace(DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_SELF_CLOSING_RE, "")
.replace(DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_TAG_RE, "");
}
const loadDiscordThreadBindings = createLazyRuntimeModule(
() => import("./monitor/thread-bindings.js"),
);
@@ -119,7 +105,6 @@ export const discordOutbound: ChannelOutboundAdapter = {
maxLines: ctx?.formatting?.maxLinesPerMessage,
}),
textChunkLimit: DISCORD_TEXT_CHUNK_LIMIT,
sanitizeText: ({ text }) => stripDiscordInternalRuntimeScaffolding(text),
pollMaxOptions: 10,
normalizePayload: ({ payload }) => normalizeDiscordApprovalPayload(payload),
presentationCapabilities: DISCORD_PRESENTATION_CAPABILITIES,
+54 -13
View File
@@ -39,21 +39,37 @@ export function escapeInternalRuntimeContextDelimiters(value: string): string {
.replaceAll(INTERNAL_RUNTIME_CONTEXT_END, ESCAPED_INTERNAL_RUNTIME_CONTEXT_END);
}
function delimitedTokenLinePattern(token: string): string {
return `(?:^|\\r?\\n)[ \\t]*${escapeRegExp(token)}[ \\t]*(?=\\r?\\n|$)`;
}
function findDelimitedTokenIndex(text: string, token: string, from: number): number {
const tokenRe = new RegExp(`(?:^|\\r?\\n)${escapeRegExp(token)}(?=\\r?\\n|$)`, "g");
const tokenRe = new RegExp(delimitedTokenLinePattern(token), "g");
tokenRe.lastIndex = Math.max(0, from);
const match = tokenRe.exec(text);
if (!match) {
return -1;
}
const prefixLength = match[0].length - token.length;
return match.index + prefixLength;
return match.index + match[0].indexOf(token);
}
function stripStandaloneDelimitedTokenLines(text: string, token: string): string {
return text.replace(new RegExp(delimitedTokenLinePattern(token), "g"), "");
}
function findDelimitedTokenLinePrefixStart(text: string, tokenIndex: number): number {
const lineStart = text.lastIndexOf("\n", tokenIndex - 1) + 1;
if (lineStart === 0) {
return 0;
}
return text[lineStart - 2] === "\r" ? lineStart - 2 : lineStart - 1;
}
function extractDelimitedBlocks(
text: string,
begin: string,
end: string,
options: { preserveSurroundingWhitespace?: boolean; separator?: string } = {},
): { text: string; blocks: string[] } {
let next = text;
const blocks: string[] = [];
@@ -82,19 +98,37 @@ function extractDelimitedBlocks(
cursor = nextEnd + end.length;
}
const before = next.slice(0, start).trimEnd();
const blockStart = options.preserveSurroundingWhitespace
? findDelimitedTokenLinePrefixStart(next, start)
: start;
const before = options.preserveSurroundingWhitespace
? next.slice(0, blockStart)
: next.slice(0, start).trimEnd();
if (finish === -1 || depth !== 0) {
return { text: before, blocks };
}
const blockEnd = finish + end.length;
let blockEnd = finish + end.length;
while (next[blockEnd] === " " || next[blockEnd] === "\t") {
blockEnd += 1;
}
blocks.push(next.slice(start, blockEnd).trim());
const after = next.slice(blockEnd).trimStart();
next = before && after ? `${before}\n\n${after}` : `${before}${after}`;
const after = options.preserveSurroundingWhitespace
? next.slice(blockEnd)
: next.slice(blockEnd).trimStart();
next =
!options.preserveSurroundingWhitespace && before && after
? `${before}${options.separator ?? "\n\n"}${after}`
: `${before}${after}`;
}
}
function stripDelimitedBlock(text: string, begin: string, end: string): string {
return extractDelimitedBlocks(text, begin, end).text;
function stripDelimitedBlock(
text: string,
begin: string,
end: string,
options?: { preserveSurroundingWhitespace?: boolean; separator?: string },
): string {
return extractDelimitedBlocks(text, begin, end, options).text;
}
function findLegacyInternalEventEnd(text: string, start: number): number | null {
@@ -215,13 +249,20 @@ function stripRuntimeContextPromptPreface(text: string): string {
}
/** Remove protected and legacy runtime-context blocks from text. */
export function stripInternalRuntimeContext(text: string): string {
export function stripInternalRuntimeContext(
text: string,
options: { preserveSurroundingWhitespace?: boolean; separator?: string } = {},
): string {
if (!text) {
return text;
}
const withoutDelimitedBlocks = stripDelimitedBlock(
text,
INTERNAL_RUNTIME_CONTEXT_BEGIN,
const withoutDelimitedBlocks = stripStandaloneDelimitedTokenLines(
stripDelimitedBlock(
text,
INTERNAL_RUNTIME_CONTEXT_BEGIN,
INTERNAL_RUNTIME_CONTEXT_END,
options,
),
INTERNAL_RUNTIME_CONTEXT_END,
);
return stripRuntimeContextPromptPreface(
+17 -1
View File
@@ -6,11 +6,13 @@ import { extensionForMime, mimeTypeFromFilePath } from "@openclaw/media-core/mim
import { expectDefined } from "@openclaw/normalization-core";
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { formatErrorMessage } from "../infra/errors.js";
import { formatErrorMessage, formatUncaughtError } from "../infra/errors.js";
import type { SubsystemLogger } from "../logging/subsystem.js";
import type { MediaFact } from "../media/media-facts.js";
import type { PromptImageOrderEntry } from "../media/prompt-image-order.js";
import { sniffMimeFromBase64 } from "../media/sniff-mime-from-base64.js";
import { deleteMediaBuffer, saveMediaBuffer, type SavedMedia } from "../media/store.js";
import { formatForLog } from "./ws-log.js";
export type ChatAttachment = {
type?: string;
@@ -63,6 +65,20 @@ const TEXT_ONLY_OFFLOAD_LIMIT = 10;
const DEFAULT_CHAT_ATTACHMENT_MAX_MB = 20;
export function logAttachmentFailure(
log: Pick<SubsystemLogger, "error">,
label: string,
err: unknown,
): void {
const primary = formatUncaughtError(err);
const cause = err instanceof Error ? err.cause : undefined;
const causeText = cause === undefined ? "" : formatUncaughtError(cause);
log.error(label, {
error: !causeText || causeText === primary ? primary : `${primary}\nCaused by: ${causeText}`,
consoleMessage: `${label}: ${formatForLog(err)}`,
});
}
export function stripImageMediaMarkers(message: string, refs: readonly OffloadedRef[]): string {
return refs.reduce((projected, ref) => {
const marker = ref.mimeType.startsWith("image/") ? `\n[media attached: ${ref.mediaRef}]` : "";
@@ -9,7 +9,6 @@ import {
resolveExplicitAgentSessionKey,
} from "../../config/sessions.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { formatUncaughtError } from "../../infra/errors.js";
import {
loadVoiceWakeRoutingConfig,
resolveVoiceWakeRouteByTrigger,
@@ -32,6 +31,7 @@ import {
} from "../../utils/message-channel.js";
import {
MediaOffloadError,
logAttachmentFailure,
parseMessageWithAttachments,
resolveChatAttachmentMaxBytes,
type ChatAttachment,
@@ -66,27 +66,6 @@ type AgentContentPhaseResult = {
to: string;
};
function formatAttachmentFailureForLog(err: unknown): string {
const primary = formatUncaughtError(err);
const cause = err instanceof Error ? err.cause : undefined;
if (cause === undefined) {
return primary;
}
const causeText = formatUncaughtError(cause);
return !causeText || causeText === primary ? primary : `${primary}\nCaused by: ${causeText}`;
}
function logAttachmentFailure(
logGateway: Pick<GatewayRequestHandlerOptions["context"]["logGateway"], "error">,
label: string,
err: unknown,
): void {
logGateway.error(label, {
error: formatAttachmentFailureForLog(err),
consoleMessage: `${label}: ${formatForLog(err)}`,
});
}
export async function prepareAgentContentPhase(params: {
request: AgentRunRequest;
cfg: OpenClawConfig;
+2 -12
View File
@@ -2,7 +2,6 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateAgentIdentityParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { resolvePublicAgentAvatarSource } from "../../agents/identity-avatar.js";
@@ -11,23 +10,14 @@ import { classifySessionKeyShape, normalizeAgentId } from "../../routing/session
import { resolveGatewayAssistantAvatar } from "../assistant-avatar.js";
import { resolveAssistantIdentity } from "../assistant-identity.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
export const agentIdentityGetHandler: GatewayRequestHandlers["agent.identity.get"] = ({
params,
respond,
context,
}) => {
if (!validateAgentIdentityParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid agent.identity.get params: ${formatValidationErrors(
validateAgentIdentityParams.errors,
)}`,
),
);
if (!assertValidParams(params, validateAgentIdentityParams, "agent.identity.get", respond)) {
return;
}
const agentIdRaw = normalizeOptionalString(params.agentId) ?? "";
@@ -3,7 +3,6 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateAgentParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { resolveDefaultAgentId } from "../../agents/agent-scope.js";
@@ -40,6 +39,7 @@ import {
} from "./agent-handler-helpers.js";
import type { AgentRunRequest } from "./agent-request-types.js";
import type { GatewayRequestHandlerOptions } from "./types.js";
import { assertValidParams } from "./validation.js";
type AgentRequestPreflight = {
request: AgentRunRequest;
@@ -69,15 +69,7 @@ type AgentRequestPreflight = {
export function prepareAgentRequestPreflight(
params: Pick<GatewayRequestHandlerOptions, "params" | "respond" | "context" | "client">,
): AgentRequestPreflight | undefined {
if (!validateAgentParams(params.params)) {
params.respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid agent params: ${formatValidationErrors(validateAgentParams.errors)}`,
),
);
if (!assertValidParams(params.params, validateAgentParams, "agent", params.respond)) {
return undefined;
}
const request = params.params as AgentRunRequest;
+3 -15
View File
@@ -1,26 +1,14 @@
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateAgentWaitParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { validateAgentWaitParams } from "../../../packages/gateway-protocol/src/index.js";
import { waitForAgentJob } from "./agent-job.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
export const agentWaitHandler: GatewayRequestHandlers["agent.wait"] = async ({
params,
respond,
context,
}) => {
if (!validateAgentWaitParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid agent.wait params: ${formatValidationErrors(validateAgentWaitParams.errors)}`,
),
);
if (!assertValidParams(params, validateAgentWaitParams, "agent.wait", respond)) {
return;
}
const runId = (params.runId ?? "").trim();
+3 -14
View File
@@ -1,11 +1,7 @@
// Approval shared helpers normalize pending exec/plugin approval lookups,
// decision payloads, turn-source routing, and gateway error responses.
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import {
ErrorCodes,
errorShape,
formatValidationErrors,
} from "../../../packages/gateway-protocol/src/index.js";
import { ErrorCodes, errorShape } from "../../../packages/gateway-protocol/src/index.js";
import type { ValidationError } from "../../../packages/gateway-protocol/src/index.js";
import { hasApprovalTurnSourceRoute } from "../../infra/approval-turn-source.js";
import type { ExecApprovalDecision } from "../../infra/exec-approvals.js";
@@ -17,6 +13,7 @@ import type {
import { ADMIN_SCOPE, APPROVALS_SCOPE } from "../method-scopes.js";
import { buildWaitResponse, type WaitReasonResolver } from "./approval-wait-response.js";
import type { GatewayClient, GatewayRequestContext, RespondFn } from "./types.js";
import { assertValidParams } from "./validation.js";
const APPROVAL_NOT_FOUND_DETAILS = {
reason: ErrorCodes.APPROVAL_NOT_FOUND,
@@ -271,15 +268,7 @@ export function resolveApprovalDecisionParams<TParams extends ApprovalResolvePar
respond: RespondFn;
}): { inputId: string; decision: ExecApprovalDecision } | null {
const rawParams = params.rawParams;
if (!params.validate(rawParams)) {
params.respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid ${params.methodName} params: ${formatValidationErrors(params.validate.errors)}`,
),
);
if (!assertValidParams(rawParams, params.validate, params.methodName, params.respond)) {
return null;
}
if (!isApprovalDecision(rawParams.decision)) {
+5 -21
View File
@@ -2,7 +2,6 @@
import {
ErrorCodes,
errorShape,
formatValidationErrors,
type AuditActivityEventV1,
type AuditEvent,
validateAuditActivityListParams,
@@ -15,6 +14,7 @@ import type {
ToolActionAuditEventRecord,
} from "../../audit/audit-event-types.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
const DEFAULT_AUDIT_LIST_LIMIT = 100;
const MAX_AUDIT_LIST_LIMIT = 500;
@@ -78,15 +78,7 @@ function invalidRangeOrCursor(params: { cursor?: string; after?: number; before?
export const auditHandlers: GatewayRequestHandlers = {
"audit.list": ({ params, respond }) => {
if (!validateAuditListParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid audit.list params: ${formatValidationErrors(validateAuditListParams.errors)}`,
),
);
if (!assertValidParams(params, validateAuditListParams, "audit.list", respond)) {
return;
}
const parsed = invalidRangeOrCursor(params);
@@ -122,17 +114,9 @@ export const auditHandlers: GatewayRequestHandlers = {
});
},
"audit.activity.list": ({ params, respond }) => {
if (!validateAuditActivityListParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid audit.activity.list params: ${formatValidationErrors(
validateAuditActivityListParams.errors,
)}`,
),
);
if (
!assertValidParams(params, validateAuditActivityListParams, "audit.activity.list", respond)
) {
return;
}
const parsed = invalidRangeOrCursor(params);
+1 -10
View File
@@ -3,7 +3,6 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateChannelsStartParams,
validateChannelsStopParams,
validateChannelsLogoutParams,
@@ -335,15 +334,7 @@ async function stopChannelAccount(params: {
/** Gateway request handlers for channel list, status, start, stop, and logout. */
export const channelsHandlers: GatewayRequestHandlers = {
"channels.status": async ({ params, respond, context }) => {
if (!validateChannelsStatusParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid channels.status params: ${formatValidationErrors(validateChannelsStatusParams.errors)}`,
),
);
if (!assertValidParams(params, validateChannelsStatusParams, "channels.status", respond)) {
return;
}
const probe = (params as { probe?: boolean }).probe === true;
@@ -2,7 +2,6 @@
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateChatAbortParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { resolveDefaultAgentId } from "../../agents/agent-scope.js";
@@ -37,6 +36,7 @@ import {
normalizeUnknownChatText as normalizeUnknownText,
} from "./chat-text-normalization.js";
import type { GatewayRequestContext, GatewayRequestHandlerOptions } from "./types.js";
import { assertValidParams } from "./validation.js";
type ChatAbortLifecycle = {
onAuthorizedAfterQueuedAbort?: () => boolean;
@@ -46,15 +46,7 @@ export async function handleChatAbortRequestWithLifecycle(
{ params, respond, context, client }: GatewayRequestHandlerOptions,
lifecycle: ChatAbortLifecycle = {},
): Promise<void> {
if (!validateChatAbortParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid chat.abort params: ${formatValidationErrors(validateChatAbortParams.errors)}`,
),
);
if (!assertValidParams(params, validateChatAbortParams, "chat.abort", respond)) {
return;
}
const {
@@ -6,7 +6,6 @@ import {
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateChatHistoryParams,
validateChatMetadataParams,
} from "../../../packages/gateway-protocol/src/index.js";
@@ -70,6 +69,7 @@ import type {
GatewayRequestHandlerOptions,
GatewayRequestHandlers,
} from "./types.js";
import { assertValidParams } from "./validation.js";
type ChatHistoryMethod = "chat.history" | "chat.startup";
@@ -95,15 +95,7 @@ async function handleChatMetadataRequest({
respond,
context,
}: GatewayRequestHandlerOptions): Promise<void> {
if (!validateChatMetadataParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid chat.metadata params: ${formatValidationErrors(validateChatMetadataParams.errors)}`,
),
);
if (!assertValidParams(params, validateChatMetadataParams, "chat.metadata", respond)) {
return;
}
const metadataParams = params;
@@ -333,15 +325,7 @@ async function handleChatHistoryRequest({
includeAgentsList?: boolean;
includeMetadata?: boolean;
}) {
if (!validateChatHistoryParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid ${method} params: ${formatValidationErrors(validateChatHistoryParams.errors)}`,
),
);
if (!assertValidParams(params, validateChatHistoryParams, method, respond)) {
return;
}
const {
@@ -2,7 +2,6 @@
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateChatMessageGetParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
@@ -22,6 +21,7 @@ import { readChatHistoryMessageId } from "./chat-history-pages.js";
import { resolveRequestedChatAgentId, validateChatSelectedAgent } from "./chat-origin-routing.js";
import { normalizeOptionalChatText as normalizeOptionalText } from "./chat-text-normalization.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
async function isChatMessageIdVisibleAfterHistoryFilters(params: {
sessionId: string;
@@ -57,15 +57,7 @@ async function isChatMessageIdVisibleAfterHistoryFilters(params: {
export const chatMessageGetHandlers: GatewayRequestHandlers = {
"chat.message.get": async ({ params, respond, context }) => {
if (!validateChatMessageGetParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid chat.message.get params: ${formatValidationErrors(validateChatMessageGetParams.errors)}`,
),
);
if (!assertValidParams(params, validateChatMessageGetParams, "chat.message.get", respond)) {
return;
}
const { sessionKey, messageId, maxChars } = params as {
@@ -11,19 +11,19 @@ import type { MsgContext, TemplateContext } from "../../auto-reply/templating.js
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { clearAgentRunContext } from "../../infra/agent-events.js";
import { measureDiagnosticsTimelineSpan } from "../../infra/diagnostics-timeline.js";
import { formatErrorMessage, formatUncaughtError } from "../../infra/errors.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { parseInboundMediaUri } from "../../media/media-reference.js";
import { deleteMediaBuffer, MEDIA_MAX_BYTES } from "../../media/store.js";
import {
MediaOffloadError,
type OffloadedRef,
logAttachmentFailure,
parseMessageWithAttachments,
resolveChatAttachmentMaxBytes,
stripImageMediaMarkers,
UnsupportedAttachmentError,
} from "../chat-attachments.js";
import { resolveGatewayModelSupportsImages } from "../session-utils.js";
import { formatForLog } from "../ws-log.js";
import {
explicitOriginTargetsAcpSession,
explicitOriginTargetsPluginBinding,
@@ -34,30 +34,6 @@ import type { PreparedChatSendSession } from "./chat-send-session.js";
import { roundedChatSendTimingMs } from "./chat-server-timing.js";
import type { GatewayRequestHandlerOptions } from "./types.js";
function formatAttachmentFailureForLog(err: unknown): string {
const primary = formatUncaughtError(err);
const cause = err instanceof Error ? err.cause : undefined;
if (cause === undefined) {
return primary;
}
const causeText = formatUncaughtError(cause);
if (!causeText || causeText === primary) {
return primary;
}
return `${primary}\nCaused by: ${causeText}`;
}
function logAttachmentFailure(
logGateway: Pick<GatewayRequestHandlerOptions["context"]["logGateway"], "error">,
label: string,
err: unknown,
): void {
logGateway.error(label, {
error: formatAttachmentFailureForLog(err),
consoleMessage: `${label}: ${formatForLog(err)}`,
});
}
function isPdfOffloadedRef(ref: OffloadedRef): boolean {
const mime = ref.mimeType.trim().toLowerCase();
if (mime === "application/pdf" || mime.endsWith("+pdf")) {
+3 -19
View File
@@ -2,7 +2,6 @@
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateChatInjectParams,
validateChatToolTitlesParams,
} from "../../../packages/gateway-protocol/src/index.js";
@@ -29,6 +28,7 @@ import { handleChatSend } from "./chat-send-handler.js";
import { normalizeOptionalChatText as normalizeOptionalText } from "./chat-text-normalization.js";
import { appendAssistantTranscriptMessage } from "./chat-transcript-persistence.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
export {
augmentChatHistoryWithCanvasBlocks,
@@ -49,15 +49,7 @@ export const chatHandlers: GatewayRequestHandlers = {
...chatHistoryHandlers,
...chatMessageGetHandlers,
"chat.toolTitles": async ({ params, respond, context }) => {
if (!validateChatToolTitlesParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid chat.toolTitles params: ${formatValidationErrors(validateChatToolTitlesParams.errors)}`,
),
);
if (!assertValidParams(params, validateChatToolTitlesParams, "chat.toolTitles", respond)) {
return;
}
const cfg = context.getRuntimeConfig();
@@ -115,15 +107,7 @@ export const chatHandlers: GatewayRequestHandlers = {
"chat.abort": handleChatAbortRequest,
"chat.send": handleChatSend,
"chat.inject": async ({ params, respond, context }) => {
if (!validateChatInjectParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid chat.inject params: ${formatValidationErrors(validateChatInjectParams.errors)}`,
),
);
if (!assertValidParams(params, validateChatInjectParams, "chat.inject", respond)) {
return;
}
const p = params as {
+3 -15
View File
@@ -1,29 +1,17 @@
// Commands gateway methods expose validated command listing for a resolved
// agent, provider, scope, and argument-detail request.
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateCommandsListParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { validateCommandsListParams } from "../../../packages/gateway-protocol/src/index.js";
import { resolveAgentIdOrRespondError } from "./agent-id-shared.js";
import { buildCommandsListResult } from "./commands-list-result.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
export { buildCommandsListResult };
/** Gateway handler for enumerating available chat/native commands. */
export const commandsHandlers: GatewayRequestHandlers = {
"commands.list": ({ params, respond, context }) => {
if (!validateCommandsListParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid commands.list params: ${formatValidationErrors(validateCommandsListParams.errors)}`,
),
);
if (!assertValidParams(params, validateCommandsListParams, "commands.list", respond)) {
return;
}
const resolved = resolveAgentIdOrRespondError({
+18 -37
View File
@@ -2,7 +2,6 @@ import { createHash } from "node:crypto";
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateConversationListParams,
validateConversationSendParams,
validateConversationTurnCancelParams,
@@ -35,6 +34,7 @@ import type {
GatewayRequestHandlers,
RespondFn,
} from "./types.js";
import { assertValidParams } from "./validation.js";
type ConversationHandlerDeps = {
cancelConversationTurn: typeof cancelPendingConversationTurn;
@@ -201,15 +201,9 @@ export function createConversationHandlers(
const deps = { ...defaultConversationHandlerDeps, ...overrides };
return {
"conversations.list": async ({ params, respond, context }) => {
if (!validateConversationListParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid conversations.list params: ${formatValidationErrors(validateConversationListParams.errors)}`,
),
);
if (
!assertValidParams(params, validateConversationListParams, "conversations.list", respond)
) {
return;
}
const request = params as ConversationListParams;
@@ -237,15 +231,9 @@ export function createConversationHandlers(
}
},
"conversations.send": async ({ params, respond, context, client }) => {
if (!validateConversationSendParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid conversations.send params: ${formatValidationErrors(validateConversationSendParams.errors)}`,
),
);
if (
!assertValidParams(params, validateConversationSendParams, "conversations.send", respond)
) {
return;
}
const request = params as ConversationSendParams;
@@ -291,15 +279,14 @@ export function createConversationHandlers(
});
},
"conversations.turn.cancel": ({ params, respond }) => {
if (!validateConversationTurnCancelParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid conversations.turn.cancel params: ${formatValidationErrors(validateConversationTurnCancelParams.errors)}`,
),
);
if (
!assertValidParams(
params,
validateConversationTurnCancelParams,
"conversations.turn.cancel",
respond,
)
) {
return;
}
const request = params as ConversationTurnCancelParams;
@@ -315,15 +302,9 @@ export function createConversationHandlers(
);
},
"conversations.turn": async ({ params, respond, context, client }) => {
if (!validateConversationTurnParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid conversations.turn params: ${formatValidationErrors(validateConversationTurnParams.errors)}`,
),
);
if (
!assertValidParams(params, validateConversationTurnParams, "conversations.turn", respond)
) {
return;
}
const request = params as ConversationTurnParams;
+12 -82
View File
@@ -3,7 +3,6 @@ import { parseBoolean } from "@openclaw/normalization-core/boolean-coercion";
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateCronAddParams,
validateCronGetParams,
validateCronListParams,
@@ -67,6 +66,7 @@ import { isCronInvalidRequestError } from "./cron-error-classification.js";
import { listCronPageForCallerScope } from "./cron-list-caller-scope.js";
import { cronRunLogPageFilters, filterCronRunLogJobsByAgent } from "./cron-run-log-filters.js";
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
import { assertValidParams } from "./validation.js";
type CronJobIdParams = { id?: string; jobId?: string };
@@ -336,15 +336,7 @@ function respondMissingCronJobId(respond: RespondFn, method: string): void {
/** Gateway request handlers for cron jobs and cron run-log access. */
export const cronHandlers: GatewayRequestHandlers = {
wake: ({ params, respond, context, client }) => {
if (!validateWakeParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid wake params: ${formatValidationErrors(validateWakeParams.errors)}`,
),
);
if (!assertValidParams(params, validateWakeParams, "wake", respond)) {
return;
}
// Caller-supplied sessionKey / agentId thread through to `cron.wake` so
@@ -429,15 +421,7 @@ export const cronHandlers: GatewayRequestHandlers = {
respond(true, result, undefined);
},
"cron.list": async ({ params, respond, context, client }) => {
if (!validateCronListParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid cron.list params: ${formatValidationErrors(validateCronListParams.errors)}`,
),
);
if (!assertValidParams(params, validateCronListParams, "cron.list", respond)) {
return;
}
const p = params as {
@@ -490,27 +474,14 @@ export const cronHandlers: GatewayRequestHandlers = {
respond(true, { ...page, jobs: page.jobs.map(cronJobReadView), deliveryPreviews }, undefined);
},
"cron.status": async ({ params, respond, context }) => {
if (!validateCronStatusParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid cron.status params: ${formatValidationErrors(validateCronStatusParams.errors)}`,
),
);
if (!assertValidParams(params, validateCronStatusParams, "cron.status", respond)) {
return;
}
const status = await context.cron.status();
respond(true, status, undefined);
},
"cron.get": async ({ params, respond, context, client }) => {
if (!validateCronGetParams(params)) {
respondInvalidCronParams(
respond,
"cron.get",
formatValidationErrors(validateCronGetParams.errors),
);
if (!assertValidParams(params, validateCronGetParams, "cron.get", respond)) {
return;
}
const jobId = resolveCronJobId(params as CronJobIdParams);
@@ -539,12 +510,7 @@ export const cronHandlers: GatewayRequestHandlers = {
respond(true, cronJobReadView(job), undefined);
},
"cron.scratch.get": async ({ params, respond, context, client }) => {
if (!validateCronScratchGetParams(params)) {
respondInvalidCronParams(
respond,
"cron.scratch.get",
formatValidationErrors(validateCronScratchGetParams.errors),
);
if (!assertValidParams(params, validateCronScratchGetParams, "cron.scratch.get", respond)) {
return;
}
const jobId = resolveCronJobId(params as CronJobIdParams);
@@ -577,12 +543,7 @@ export const cronHandlers: GatewayRequestHandlers = {
);
},
"cron.scratch.set": async ({ params, respond, context, client }) => {
if (!validateCronScratchSetParams(params)) {
respondInvalidCronParams(
respond,
"cron.scratch.set",
formatValidationErrors(validateCronScratchSetParams.errors),
);
if (!assertValidParams(params, validateCronScratchSetParams, "cron.scratch.set", respond)) {
return;
}
const p = params as CronJobIdParams & {
@@ -677,15 +638,7 @@ export const cronHandlers: GatewayRequestHandlers = {
return;
}
const candidate = normalized;
if (!validateCronAddParams(candidate)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid cron.add params: ${formatValidationErrors(validateCronAddParams.errors)}`,
),
);
if (!assertValidParams(candidate, validateCronAddParams, "cron.add", respond)) {
return;
}
const callerScope = readCronCallerScope(client);
@@ -820,15 +773,7 @@ export const cronHandlers: GatewayRequestHandlers = {
normalizedPatch && typeof params === "object" && params !== null
? { ...params, patch: normalizedPatch }
: params;
if (!validateCronUpdateParams(candidate)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid cron.update params: ${formatValidationErrors(validateCronUpdateParams.errors)}`,
),
);
if (!assertValidParams(candidate, validateCronUpdateParams, "cron.update", respond)) {
return;
}
const p = candidate as {
@@ -987,12 +932,7 @@ export const cronHandlers: GatewayRequestHandlers = {
respond(true, cronJobReadView(job), undefined);
},
"cron.remove": async ({ params, respond, context, client }) => {
if (!validateCronRemoveParams(params)) {
respondInvalidCronParams(
respond,
"cron.remove",
formatValidationErrors(validateCronRemoveParams.errors),
);
if (!assertValidParams(params, validateCronRemoveParams, "cron.remove", respond)) {
return;
}
const jobId = resolveCronJobId(params as CronJobIdParams);
@@ -1031,12 +971,7 @@ export const cronHandlers: GatewayRequestHandlers = {
respond(true, result, undefined);
},
"cron.run": async ({ params, respond, context, client }) => {
if (!validateCronRunParams(params)) {
respondInvalidCronParams(
respond,
"cron.run",
formatValidationErrors(validateCronRunParams.errors),
);
if (!assertValidParams(params, validateCronRunParams, "cron.run", respond)) {
return;
}
const p = params as CronJobIdParams & {
@@ -1085,12 +1020,7 @@ export const cronHandlers: GatewayRequestHandlers = {
respond(true, { ...result, processInstanceId: getGatewayProcessInstanceId() }, undefined);
},
"cron.runs": async ({ params, respond, context, client }) => {
if (!validateCronRunsParams(params)) {
respondInvalidCronParams(
respond,
"cron.runs",
formatValidationErrors(validateCronRunsParams.errors),
);
if (!assertValidParams(params, validateCronRunsParams, "cron.runs", respond)) {
return;
}
const p = params as CronRunsRequestParams;
+14 -78
View File
@@ -2,7 +2,6 @@
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateDevicePairApproveParams,
validateDevicePairListParams,
validateDevicePairRemoveParams,
@@ -42,6 +41,7 @@ import {
import type { DeviceManagementAuthz } from "./device-management-authz.js";
import { emitDeviceManagementSecurityEvent } from "./device-management-security.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
const DEVICE_TOKEN_ROTATION_DENIED_MESSAGE = "device token rotation denied";
const DEVICE_TOKEN_REVOCATION_DENIED_MESSAGE = "device token revocation denied";
@@ -214,17 +214,7 @@ function emitDeviceTokenLifecycleSecurityEvent(params: {
/** Gateway request handlers for device pair approval, removal, token rotation, and revocation. */
export const deviceHandlers: GatewayRequestHandlers = {
"device.pair.list": async ({ params, respond, context, client }) => {
if (!validateDevicePairListParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid device.pair.list params: ${formatValidationErrors(
validateDevicePairListParams.errors,
)}`,
),
);
if (!assertValidParams(params, validateDevicePairListParams, "device.pair.list", respond)) {
return;
}
const list = await listDevicePairing();
@@ -254,17 +244,9 @@ export const deviceHandlers: GatewayRequestHandlers = {
);
},
"device.pair.approve": async ({ params, respond, context, client }) => {
if (!validateDevicePairApproveParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid device.pair.approve params: ${formatValidationErrors(
validateDevicePairApproveParams.errors,
)}`,
),
);
if (
!assertValidParams(params, validateDevicePairApproveParams, "device.pair.approve", respond)
) {
return;
}
const { requestId } = params as { requestId: string };
@@ -417,17 +399,7 @@ export const deviceHandlers: GatewayRequestHandlers = {
}
},
"device.pair.reject": async ({ params, respond, context, client }) => {
if (!validateDevicePairRejectParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid device.pair.reject params: ${formatValidationErrors(
validateDevicePairRejectParams.errors,
)}`,
),
);
if (!assertValidParams(params, validateDevicePairRejectParams, "device.pair.reject", respond)) {
return;
}
const { requestId } = params as { requestId: string };
@@ -493,17 +465,7 @@ export const deviceHandlers: GatewayRequestHandlers = {
respond(true, rejected, undefined);
},
"device.pair.remove": async ({ params, respond, context, client }) => {
if (!validateDevicePairRemoveParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid device.pair.remove params: ${formatValidationErrors(
validateDevicePairRemoveParams.errors,
)}`,
),
);
if (!assertValidParams(params, validateDevicePairRemoveParams, "device.pair.remove", respond)) {
return;
}
const { deviceId } = params as { deviceId: string };
@@ -568,17 +530,7 @@ export const deviceHandlers: GatewayRequestHandlers = {
});
},
"device.pair.rename": async ({ params, respond, context, client }) => {
if (!validateDevicePairRenameParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid device.pair.rename params: ${formatValidationErrors(
validateDevicePairRenameParams.errors,
)}`,
),
);
if (!assertValidParams(params, validateDevicePairRenameParams, "device.pair.rename", respond)) {
return;
}
const { deviceId, label } = params as {
@@ -644,17 +596,9 @@ export const deviceHandlers: GatewayRequestHandlers = {
respond(true, { deviceId, label: trimmed }, undefined);
},
"device.token.rotate": async ({ params, respond, context, client }) => {
if (!validateDeviceTokenRotateParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid device.token.rotate params: ${formatValidationErrors(
validateDeviceTokenRotateParams.errors,
)}`,
),
);
if (
!assertValidParams(params, validateDeviceTokenRotateParams, "device.token.rotate", respond)
) {
return;
}
const { deviceId, role, scopes } = params as {
@@ -777,17 +721,9 @@ export const deviceHandlers: GatewayRequestHandlers = {
});
},
"device.token.revoke": async ({ params, respond, context, client }) => {
if (!validateDeviceTokenRevokeParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid device.token.revoke params: ${formatValidationErrors(
validateDeviceTokenRevokeParams.errors,
)}`,
),
);
if (
!assertValidParams(params, validateDeviceTokenRevokeParams, "device.token.revoke", respond)
) {
return;
}
const { deviceId, role } = params as { deviceId: string; role: string };
+10 -23
View File
@@ -4,7 +4,6 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateExecApprovalGetParams,
validateExecApprovalRequestParams,
validateExecApprovalResolveParams,
@@ -47,6 +46,7 @@ import {
resolvePendingApprovalRecord,
} from "./approval-shared.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
const APPROVAL_ALLOW_ALWAYS_UNAVAILABLE_DETAILS = {
reason: "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE",
@@ -99,17 +99,7 @@ export function createExecApprovalHandlers(
): GatewayRequestHandlers {
return {
"exec.approval.get": async ({ params, respond, client }) => {
if (!validateExecApprovalGetParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid exec.approval.get params: ${formatValidationErrors(
validateExecApprovalGetParams.errors,
)}`,
),
);
if (!assertValidParams(params, validateExecApprovalGetParams, "exec.approval.get", respond)) {
return;
}
const p = params as { id: string };
@@ -145,17 +135,14 @@ export function createExecApprovalHandlers(
respond(true, listVisiblePendingApprovalRequests({ manager, client }), undefined);
},
"exec.approval.request": async ({ params, respond, context, client }) => {
if (!validateExecApprovalRequestParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid exec.approval.request params: ${formatValidationErrors(
validateExecApprovalRequestParams.errors,
)}`,
),
);
if (
!assertValidParams(
params,
validateExecApprovalRequestParams,
"exec.approval.request",
respond,
)
) {
return;
}
const p = params as {
+2 -10
View File
@@ -2,24 +2,16 @@
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateLogsTailParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { readConfiguredLogTail } from "../../logging/log-tail.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
/** Gateway handler for bounded reads from the configured gateway log. */
export const logsHandlers: GatewayRequestHandlers = {
"logs.tail": async ({ params, respond }) => {
if (!validateLogsTailParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid logs.tail params: ${formatValidationErrors(validateLogsTailParams.errors)}`,
),
);
if (!assertValidParams(params, validateLogsTailParams, "logs.tail", respond)) {
return;
}
+2 -10
View File
@@ -3,7 +3,6 @@ import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import {
ErrorCodes,
errorShape,
formatValidationErrors,
type ModelsProbeParams,
type ModelsProbeResult,
validateModelsProbeParams,
@@ -20,6 +19,7 @@ import {
unknownModelAuthAgentIdError,
} from "./model-auth-agent-scope.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
const DEFAULT_TIMEOUT_MS = 20_000;
const MIN_TIMEOUT_MS = 5_000;
@@ -96,15 +96,7 @@ function mapProbeResult(provider: string, results: AuthProbeResult[]): ModelsPro
export const modelsProbeHandlers: GatewayRequestHandlers = {
"models.probe": async ({ params, respond, context }) => {
if (!validateModelsProbeParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid models.probe params: ${formatValidationErrors(validateModelsProbeParams.errors)}`,
),
);
if (!assertValidParams(params, validateModelsProbeParams, "models.probe", respond)) {
return;
}
const request = params as ModelsProbeParams;
+2 -10
View File
@@ -3,11 +3,11 @@
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateModelsListParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { buildModelsListResult } from "./models-list-result.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
export { buildModelsListResult };
@@ -16,15 +16,7 @@ export { buildModelsListResult };
// extra runtime discovery on each request.
export const modelsHandlers: GatewayRequestHandlers = {
"models.list": async ({ params, respond, context }) => {
if (!validateModelsListParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid models.list params: ${formatValidationErrors(validateModelsListParams.errors)}`,
),
);
if (!assertValidParams(params, validateModelsListParams, "models.list", respond)) {
return;
}
try {
+9 -14
View File
@@ -2,9 +2,6 @@
import { randomUUID } from "node:crypto";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validatePluginApprovalRequestParams,
validatePluginApprovalResolveParams,
} from "../../../packages/gateway-protocol/src/index.js";
@@ -30,6 +27,7 @@ import {
resolveApprovalDecisionParams,
} from "./approval-shared.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
type PluginApprovalIosPushDelivery = {
handleRequested?: (
@@ -52,17 +50,14 @@ export function createPluginApprovalHandlers(
respond(true, listVisiblePendingApprovalRequests({ manager, client }), undefined);
},
"plugin.approval.request": async ({ params, client, respond, context }) => {
if (!validatePluginApprovalRequestParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid plugin.approval.request params: ${formatValidationErrors(
validatePluginApprovalRequestParams.errors,
)}`,
),
);
if (
!assertValidParams(
params,
validatePluginApprovalRequestParams,
"plugin.approval.request",
respond,
)
) {
return;
}
const p = params as {
+17 -18
View File
@@ -23,6 +23,7 @@ import {
} from "../../plugins/schema-validator.js";
import { ADMIN_SCOPE, READ_SCOPE, WRITE_SCOPE } from "../operator-scopes.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
const log = createSubsystemLogger("gateway/plugin-host-hooks");
@@ -45,15 +46,14 @@ function validatePluginSessionActionJsonFields(
/** Gateway handlers for plugin-declared Control UI descriptors and session actions. */
export const pluginHostHookHandlers: GatewayRequestHandlers = {
"plugins.uiDescriptors": ({ params, respond }) => {
if (!validatePluginsUiDescriptorsParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid plugins.uiDescriptors params: ${formatValidationErrors(validatePluginsUiDescriptorsParams.errors)}`,
),
);
if (
!assertValidParams(
params,
validatePluginsUiDescriptorsParams,
"plugins.uiDescriptors",
respond,
)
) {
return;
}
const descriptors = (getActivePluginRegistry()?.controlUiDescriptors ?? []).map((entry) => {
@@ -96,15 +96,14 @@ export const pluginHostHookHandlers: GatewayRequestHandlers = {
respond(true, result, undefined);
},
"plugins.sessionAction": async ({ params, client, respond }) => {
if (!validatePluginsSessionActionParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid plugins.sessionAction params: ${formatValidationErrors(validatePluginsSessionActionParams.errors)}`,
),
);
if (
!assertValidParams(
params,
validatePluginsSessionActionParams,
"plugins.sessionAction",
respond,
)
) {
return;
}
const pluginId = normalizeOptionalString(params.pluginId);
+4 -28
View File
@@ -8,7 +8,6 @@ import {
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateMessageActionParams,
validatePollParams,
validateSendParams,
@@ -76,6 +75,7 @@ import {
type GatewayInflightResult as InflightResult,
} from "./inflight.js";
import type { GatewayRequestContext, GatewayRequestHandlers, RespondFn } from "./types.js";
import { assertValidParams } from "./validation.js";
type MessageActionToolContext = Omit<ChannelThreadingToolContext, "currentChatType">;
@@ -465,15 +465,7 @@ function scheduleDeliveredSourceReplyTranscriptMirror(params: {
export const sendHandlers: GatewayRequestHandlers = {
"message.action": async ({ params, respond, context, client }) => {
const p = params;
if (!validateMessageActionParams(p)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid message.action params: ${formatValidationErrors(validateMessageActionParams.errors)}`,
),
);
if (!assertValidParams(p, validateMessageActionParams, "message.action", respond)) {
return;
}
const request = p as {
@@ -639,15 +631,7 @@ export const sendHandlers: GatewayRequestHandlers = {
},
send: async ({ params, respond, context, client }) => {
const p = params;
if (!validateSendParams(p)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid send params: ${formatValidationErrors(validateSendParams.errors)}`,
),
);
if (!assertValidParams(p, validateSendParams, "send", respond)) {
return;
}
const request = p as {
@@ -900,15 +884,7 @@ export const sendHandlers: GatewayRequestHandlers = {
},
poll: async ({ params, respond, context, client }) => {
const p = params;
if (!validatePollParams(p)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid poll params: ${formatValidationErrors(validatePollParams.errors)}`,
),
);
if (!assertValidParams(p, validatePollParams, "poll", respond)) {
return;
}
const request = p as {
+15 -46
View File
@@ -7,7 +7,6 @@ import {
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateTalkClientCloseParams,
validateTalkClientCreateParams,
validateTalkClientSteerParams,
@@ -65,6 +64,7 @@ import {
resolveTalkRealtimeProviderInstructions,
} from "./talk-shared.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
const LEGACY_VOICE_BINDING_TTL_MS = 6 * 60 * 60_000;
const REALTIME_VOICE_CONTEXT_MAX_ITEMS = 16;
@@ -127,15 +127,7 @@ function resolveTalkClientAgentId(
*/
export const talkClientHandlers: GatewayRequestHandlers = {
"talk.client.create": async ({ params, respond, context, client }) => {
if (!validateTalkClientCreateParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid talk.client.create params: ${formatValidationErrors(validateTalkClientCreateParams.errors)}`,
),
);
if (!assertValidParams(params, validateTalkClientCreateParams, "talk.client.create", respond)) {
return;
}
const typedParams = params as {
@@ -406,15 +398,9 @@ export const talkClientHandlers: GatewayRequestHandlers = {
},
"talk.client.toolCall": async (request) => {
const { params, respond } = request;
if (!validateTalkClientToolCallParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid talk.client.toolCall params: ${formatValidationErrors(validateTalkClientToolCallParams.errors)}`,
),
);
if (
!assertValidParams(params, validateTalkClientToolCallParams, "talk.client.toolCall", respond)
) {
return;
}
if (params.name !== REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
@@ -540,15 +526,14 @@ export const talkClientHandlers: GatewayRequestHandlers = {
);
},
"talk.client.transcript": async ({ params, respond, context }) => {
if (!validateTalkClientTranscriptParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid talk.client.transcript params: ${formatValidationErrors(validateTalkClientTranscriptParams.errors)}`,
),
);
if (
!assertValidParams(
params,
validateTalkClientTranscriptParams,
"talk.client.transcript",
respond,
)
) {
return;
}
try {
@@ -569,15 +554,7 @@ export const talkClientHandlers: GatewayRequestHandlers = {
}
},
"talk.client.close": async ({ params, respond, context, client }) => {
if (!validateTalkClientCloseParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid talk.client.close params: ${formatValidationErrors(validateTalkClientCloseParams.errors)}`,
),
);
if (!assertValidParams(params, validateTalkClientCloseParams, "talk.client.close", respond)) {
return;
}
try {
@@ -610,15 +587,7 @@ export const talkClientHandlers: GatewayRequestHandlers = {
}
},
"talk.client.steer": async ({ params, respond, client, context }) => {
if (!validateTalkClientSteerParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid talk.client.steer params: ${formatValidationErrors(validateTalkClientSteerParams.errors)}`,
),
);
if (!assertValidParams(params, validateTalkClientSteerParams, "talk.client.steer", respond)) {
return;
}
if (
+5 -37
View File
@@ -8,7 +8,6 @@ import {
import {
ErrorCodes,
errorShape,
formatValidationErrors,
missingScopeErrorShape,
type TalkSpeakParams,
validateTalkCatalogParams,
@@ -74,6 +73,7 @@ import {
resolveConfiguredRealtimeTranscriptionProvider,
} from "./talk-shared.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
type TalkSpeakReason =
| "talk_unconfigured"
@@ -713,15 +713,7 @@ export const talkHandlers: GatewayRequestHandlers = {
...talkClientHandlers,
"talk.catalog": async ({ params, respond, context }) => {
const catalogParams = params ?? {};
if (!validateTalkCatalogParams(catalogParams)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid talk.catalog params: ${formatValidationErrors(validateTalkCatalogParams.errors)}`,
),
);
if (!assertValidParams(catalogParams, validateTalkCatalogParams, "talk.catalog", respond)) {
return;
}
@@ -732,15 +724,7 @@ export const talkHandlers: GatewayRequestHandlers = {
}
},
"talk.config": async ({ params, respond, client, context }) => {
if (!validateTalkConfigParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid talk.config params: ${formatValidationErrors(validateTalkConfigParams.errors)}`,
),
);
if (!assertValidParams(params, validateTalkConfigParams, "talk.config", respond)) {
return;
}
@@ -783,15 +767,7 @@ export const talkHandlers: GatewayRequestHandlers = {
respond(true, { config: configPayload }, undefined);
},
"talk.speak": async ({ params, respond, context }) => {
if (!validateTalkSpeakParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid talk.speak params: ${formatValidationErrors(validateTalkSpeakParams.errors)}`,
),
);
if (!assertValidParams(params, validateTalkSpeakParams, "talk.speak", respond)) {
return;
}
@@ -889,15 +865,7 @@ export const talkHandlers: GatewayRequestHandlers = {
);
return;
}
if (!validateTalkModeParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid talk.mode params: ${formatValidationErrors(validateTalkModeParams.errors)}`,
),
);
if (!assertValidParams(params, validateTalkModeParams, "talk.mode", respond)) {
return;
}
const payload = {
+4 -28
View File
@@ -4,7 +4,6 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
import {
ErrorCodes,
errorShape,
formatValidationErrors,
type TaskSummary,
type TasksListParams,
validateTasksCancelParams,
@@ -19,6 +18,7 @@ import { cancelDetachedTaskRunById } from "../../tasks/task-executor.js";
import type { TaskRecord, TaskStatus } from "../../tasks/task-registry.types.js";
import { mapTaskSummary, taskUpdatedAt } from "./task-summary.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
const DEFAULT_TASKS_LIST_LIMIT = 100;
const MAX_TASKS_LIST_LIMIT = 500;
@@ -89,15 +89,7 @@ function parseCursor(cursor: string | undefined): number | null {
// above keep runtime registry details out of the wire result.
export const tasksHandlers: GatewayRequestHandlers = {
"tasks.list": ({ params, respond, context }) => {
if (!validateTasksListParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid tasks.list params: ${formatValidationErrors(validateTasksListParams.errors)}`,
),
);
if (!assertValidParams(params, validateTasksListParams, "tasks.list", respond)) {
return;
}
const cursor = parseCursor(params.cursor);
@@ -150,15 +142,7 @@ export const tasksHandlers: GatewayRequestHandlers = {
});
},
"tasks.get": ({ params, respond }) => {
if (!validateTasksGetParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid tasks.get params: ${formatValidationErrors(validateTasksGetParams.errors)}`,
),
);
if (!assertValidParams(params, validateTasksGetParams, "tasks.get", respond)) {
return;
}
const taskId = params.taskId;
@@ -176,15 +160,7 @@ export const tasksHandlers: GatewayRequestHandlers = {
respond(true, { task: mapTaskSummary(task, { includePrompt: true }) });
},
"tasks.cancel": async ({ params, respond, context }) => {
if (!validateTasksCancelParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid tasks.cancel params: ${formatValidationErrors(validateTasksCancelParams.errors)}`,
),
);
if (!assertValidParams(params, validateTasksCancelParams, "tasks.cancel", respond)) {
return;
}
const taskId = params.taskId;
@@ -1,12 +1,12 @@
import {
ErrorCodes,
errorShape,
formatValidationErrors,
type TerminalUploadParams,
validateTerminalUploadParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { isCanonicalTerminalUploadBase64 } from "../../../packages/gateway-protocol/src/schema/terminal-constants.js";
import type { GatewayRequestHandlerOptions, GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
function invalid(respond: GatewayRequestHandlerOptions["respond"], detail: string): void {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, detail));
@@ -15,11 +15,7 @@ function invalid(respond: GatewayRequestHandlerOptions["respond"], detail: strin
export const terminalUploadHandlers: GatewayRequestHandlers = {
"terminal.upload": async (opts) => {
const { params, respond, context } = opts;
if (!validateTerminalUploadParams(params)) {
invalid(
respond,
`invalid terminal.upload params: ${formatValidationErrors(validateTerminalUploadParams.errors)}`,
);
if (!assertValidParams(params, validateTerminalUploadParams, "terminal.upload", respond)) {
return;
}
const connId = opts.client?.connId;
+7 -31
View File
@@ -9,7 +9,6 @@ import {
import {
ErrorCodes,
errorShape,
formatValidationErrors,
type TerminalOpenParams,
type TerminalUploadResult,
validateTerminalAttachParams,
@@ -40,6 +39,7 @@ import {
} from "./terminal-open-plan.js";
import { terminalUploadHandlers } from "./terminal-upload.js";
import type { GatewayRequestHandlerOptions, GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
function invalid(respond: GatewayRequestHandlerOptions["respond"], detail: string): void {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, detail));
@@ -136,11 +136,7 @@ export const terminalHandlers: GatewayRequestHandlers = {
...terminalUploadHandlers,
"terminal.open": async (opts) => {
const { params, respond, context } = opts;
if (!validateTerminalOpenParams(params)) {
invalid(
respond,
`invalid terminal.open params: ${formatValidationErrors(validateTerminalOpenParams.errors)}`,
);
if (!assertValidParams(params, validateTerminalOpenParams, "terminal.open", respond)) {
return;
}
const connId = requireConnId(opts);
@@ -370,11 +366,7 @@ export const terminalHandlers: GatewayRequestHandlers = {
"terminal.input": async (opts) => {
const { params, respond, context } = opts;
if (!validateTerminalInputParams(params)) {
invalid(
respond,
`invalid terminal.input params: ${formatValidationErrors(validateTerminalInputParams.errors)}`,
);
if (!assertValidParams(params, validateTerminalInputParams, "terminal.input", respond)) {
return;
}
const connId = requireConnId(opts);
@@ -396,11 +388,7 @@ export const terminalHandlers: GatewayRequestHandlers = {
"terminal.resize": async (opts) => {
const { params, respond, context } = opts;
if (!validateTerminalResizeParams(params)) {
invalid(
respond,
`invalid terminal.resize params: ${formatValidationErrors(validateTerminalResizeParams.errors)}`,
);
if (!assertValidParams(params, validateTerminalResizeParams, "terminal.resize", respond)) {
return;
}
const connId = requireConnId(opts);
@@ -419,11 +407,7 @@ export const terminalHandlers: GatewayRequestHandlers = {
"terminal.close": async (opts) => {
const { params, respond, context } = opts;
if (!validateTerminalCloseParams(params)) {
invalid(
respond,
`invalid terminal.close params: ${formatValidationErrors(validateTerminalCloseParams.errors)}`,
);
if (!assertValidParams(params, validateTerminalCloseParams, "terminal.close", respond)) {
return;
}
const connId = requireConnId(opts);
@@ -437,11 +421,7 @@ export const terminalHandlers: GatewayRequestHandlers = {
"terminal.attach": async (opts) => {
const { params, respond, context } = opts;
if (!validateTerminalAttachParams(params)) {
invalid(
respond,
`invalid terminal.attach params: ${formatValidationErrors(validateTerminalAttachParams.errors)}`,
);
if (!assertValidParams(params, validateTerminalAttachParams, "terminal.attach", respond)) {
return;
}
const connId = requireConnId(opts);
@@ -509,11 +489,7 @@ export const terminalHandlers: GatewayRequestHandlers = {
"terminal.text": async (opts) => {
const { params, respond, context } = opts;
if (!validateTerminalTextParams(params)) {
invalid(
respond,
`invalid terminal.text params: ${formatValidationErrors(validateTerminalTextParams.errors)}`,
);
if (!assertValidParams(params, validateTerminalTextParams, "terminal.text", respond)) {
return;
}
const connId = requireConnId(opts);
+2 -12
View File
@@ -1,9 +1,6 @@
// Gateway RPC handler for the tool catalog shown by clients and Control UI.
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import {
ErrorCodes,
errorShape,
formatValidationErrors,
type ToolsCatalogResult,
validateToolsCatalogParams,
} from "../../../packages/gateway-protocol/src/index.js";
@@ -30,6 +27,7 @@ import {
} from "../../plugins/tools.js";
import { resolveAgentIdOrRespondError } from "./agent-id-shared.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
type ToolCatalogEntry = {
id: string;
@@ -226,15 +224,7 @@ function buildToolsCatalogResult(params: {
/** Gateway request handlers for tool catalog queries. */
export const toolsCatalogHandlers: GatewayRequestHandlers = {
"tools.catalog": ({ params, respond, context }) => {
if (!validateToolsCatalogParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid tools.catalog params: ${formatValidationErrors(validateToolsCatalogParams.errors)}`,
),
);
if (!assertValidParams(params, validateToolsCatalogParams, "tools.catalog", respond)) {
return;
}
const resolved = resolveAgentIdOrRespondError({
+9 -10
View File
@@ -4,7 +4,6 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateToolsEffectiveParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { resolveConversationCapabilityProfile } from "../../agents/conversation-capability-profile.js";
@@ -40,6 +39,7 @@ import {
resolveSessionModelRef,
} from "./tools-effective.runtime.js";
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
import { assertValidParams } from "./validation.js";
const TOOLS_EFFECTIVE_FRESH_TTL_MS = 10_000;
const TOOLS_EFFECTIVE_STALE_TTL_MS = 120_000;
@@ -554,15 +554,14 @@ async function handleToolsEffectiveRequest(params: {
respond: RespondFn;
context: Parameters<GatewayRequestHandlers[string]>[0]["context"];
}) {
if (!validateToolsEffectiveParams(params.rawParams)) {
params.respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid tools.effective params: ${formatValidationErrors(validateToolsEffectiveParams.errors)}`,
),
);
if (
!assertValidParams(
params.rawParams,
validateToolsEffectiveParams,
"tools.effective",
params.respond,
)
) {
return;
}
const cfg = params.context.getRuntimeConfig();
+2 -10
View File
@@ -4,13 +4,13 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateToolsInvokeParams,
type ToolsInvokeResult,
} from "../../../packages/gateway-protocol/src/index.js";
import { resolveGatewayConversationReadOrigin } from "../conversation-read-origin.js";
import { invokeGatewayTool } from "../tools-invoke-shared.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
/**
* RPC adapter for invoking gateway-visible tools from connected clients.
@@ -38,15 +38,7 @@ function resolveRpcErrorCode(params: {
/** Handles `tools.invoke` with protocol-shaped success and failure payloads. */
export const toolsInvokeHandlers: GatewayRequestHandlers = {
"tools.invoke": async ({ params, respond, context, client }) => {
if (!validateToolsInvokeParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid tools.invoke params: ${formatValidationErrors(validateToolsInvokeParams.errors)}`,
),
);
if (!assertValidParams(params, validateToolsInvokeParams, "tools.invoke", respond)) {
return;
}
const requestedToolName = normalizeOptionalString(params.name);
+2 -10
View File
@@ -3,7 +3,6 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateTtsSpeakParams,
} from "../../../packages/gateway-protocol/src/index.js";
import {
@@ -35,6 +34,7 @@ import {
import { formatForLog } from "../ws-log.js";
import { inferSpeechMimeType } from "./speech-mime.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
/** Gateway request handlers for TTS status, preference mutation, and synthesis. */
export const ttsHandlers: GatewayRequestHandlers = {
@@ -156,15 +156,7 @@ export const ttsHandlers: GatewayRequestHandlers = {
// Unlike tts.convert (gateway-local audioPath) this returns the clip inline,
// so remote clients (mobile apps) can play it without filesystem access.
"tts.speak": async ({ params, respond, context }) => {
if (!validateTtsSpeakParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid tts.speak params: ${formatValidationErrors(validateTtsSpeakParams.errors)}`,
),
);
if (!assertValidParams(params, validateTtsSpeakParams, "tts.speak", respond)) {
return;
}
const text = normalizeOptionalString(params.text);
+2 -10
View File
@@ -6,24 +6,16 @@ import {
import {
ErrorCodes,
errorShape,
formatValidationErrors,
type UiCommandParams,
validateUiCommandParams,
} from "../../../packages/gateway-protocol/src/index.js";
import type { GatewayRequestContextWithClientLookup } from "../server-request-context.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
export const uiCommandHandlers: GatewayRequestHandlers = {
"ui.command": ({ params, respond, context }) => {
if (!validateUiCommandParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid ui.command params: ${formatValidationErrors(validateUiCommandParams.errors)}`,
),
);
if (!assertValidParams(params, validateUiCommandParams, "ui.command", respond)) {
return;
}
+2 -10
View File
@@ -6,7 +6,6 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
import {
ErrorCodes,
errorShape,
formatValidationErrors,
validateSessionsUsageParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { listAgentIds, resolveDefaultAgentId } from "../../agents/agent-scope.js";
@@ -69,6 +68,7 @@ import {
} from "../session-store-key.js";
import { loadCombinedSessionStoreForGateway, loadSessionEntryReadOnly } from "../session-utils.js";
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
import { assertValidParams } from "./validation.js";
const COST_USAGE_CACHE_TTL_MS = 30_000;
const COST_USAGE_CACHE_MAX = 256;
@@ -1121,15 +1121,7 @@ export const usageHandlers: GatewayRequestHandlers = {
respond(true, summary, undefined);
},
"sessions.usage": async ({ respond, params, context }) => {
if (!validateSessionsUsageParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid sessions.usage params: ${formatValidationErrors(validateSessionsUsageParams.errors)}`,
),
);
if (!assertValidParams(params, validateSessionsUsageParams, "sessions.usage", respond)) {
return;
}
+1 -15
View File
@@ -3,12 +3,7 @@
import type { GatewayTailscaleMode } from "../config/types.gateway.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginRegistry } from "../plugins/registry-types.js";
type Awaitable<T> = T | Promise<T>;
type GatewayStartupTrace = {
measure: <T>(name: string, run: () => Awaitable<T>) => Promise<T>;
};
import { measureStartup, type GatewayStartupTrace } from "./server-startup-trace.js";
type StartGatewayMaintenanceTimers =
typeof import("./server-maintenance.js").startGatewayMaintenanceTimers;
@@ -16,15 +11,6 @@ type GatewayMaintenanceParams = Parameters<StartGatewayMaintenanceTimers>[0];
const loadRemoteSkillsRuntimeModule = async () => await import("../skills/runtime/remote.js");
/** Measure an early-startup step when tracing is enabled, otherwise run it directly. */
async function measureStartup<T>(
startupTrace: GatewayStartupTrace | undefined,
name: string,
run: () => Awaitable<T>,
): Promise<T> {
return startupTrace ? startupTrace.measure(name, run) : await run();
}
/** Start plugin discovery and return the Bonjour shutdown callback when discovery is active. */
export async function startGatewayPluginDiscovery(params: {
minimalTestGateway: boolean;
+1 -14
View File
@@ -34,6 +34,7 @@ import {
formatGatewayStartupOutcomes,
type GatewayStartupOutcomeRecorder,
} from "./server-startup-outcomes.js";
import { measureStartup, type GatewayStartupTrace } from "./server-startup-trace.js";
import type { startGatewayTailscaleExposure } from "./server-tailscale.js";
import { warmMacOSSystemCaOffMainThread } from "./system-ca-warmup.js";
const ACP_BACKEND_READY_TIMEOUT_MS = 5_000;
@@ -45,11 +46,6 @@ const DEFERRED_SIDECAR_START_DELAY_MS = 100;
const SESSION_LOCK_CLEANUP_CONCURRENCY = 4;
const SKIP_STARTUP_MODEL_PREWARM_ENV = "OPENCLAW_SKIP_STARTUP_MODEL_PREWARM";
type Awaitable<T> = T | Promise<T>;
type GatewayStartupTrace = {
detail: (name: string, metrics: ReadonlyArray<readonly [string, number | string]>) => void;
mark: (name: string) => void;
measure: <T>(name: string, run: () => Awaitable<T>) => Promise<T>;
};
type GatewayMemoryStartupPolicy =
| { mode: "off" }
@@ -87,15 +83,6 @@ export function stopPostReadySidecarsAfterCloseStarted(params: {
}
}
/** Measure a post-attach startup step when tracing is active. */
async function measureStartup<T>(
startupTrace: GatewayStartupTrace | undefined,
name: string,
run: () => Awaitable<T>,
): Promise<T> {
return startupTrace ? startupTrace.measure(name, run) : await run();
}
/** Measure provider-auth warming without letting event-loop stalls hide in wall time. */
async function measureProviderAuthWarm(run: () => Promise<void>): Promise<{
elapsedMs: number;
+16
View File
@@ -10,6 +10,22 @@ import type { createSubsystemLogger } from "../logging/subsystem.js";
import { recordGatewayRestartTraceDetail, recordGatewayRestartTraceSpan } from "./restart-trace.js";
type GatewayLogger = ReturnType<typeof createSubsystemLogger>;
type Awaitable<T> = T | Promise<T>;
export type GatewayStartupTrace = {
detail: (name: string, metrics: ReadonlyArray<readonly [string, number | string]>) => void;
mark: (name: string) => void;
measure: <T>(name: string, run: () => Awaitable<T>) => Promise<T>;
};
/** Measure a startup step when tracing is active, otherwise run it directly. */
export async function measureStartup<T>(
startupTrace: GatewayStartupTrace | undefined,
name: string,
run: () => Awaitable<T>,
): Promise<T> {
return startupTrace ? startupTrace.measure(name, run) : await run();
}
export function createGatewayStartupTrace(log: GatewayLogger) {
const logEnabled = isTruthyEnvValue(process.env.OPENCLAW_GATEWAY_STARTUP_TRACE);
+8 -24
View File
@@ -1,4 +1,5 @@
import { stripPlainTextToolCallBlocks } from "../../../packages/tool-call-repair/src/index.js";
import { stripInternalRuntimeContext } from "../../agents/internal-runtime-context.js";
import { escapeRegExp } from "../../shared/regexp.js";
const INTERNAL_RUNTIME_SCAFFOLDING_TAGS = ["system-reminder", "previous_response"] as const;
@@ -15,9 +16,6 @@ const INTERNAL_RUNTIME_SCAFFOLDING_TAG_RE = new RegExp(
`<\\s*\\/?\\s*(?:${INTERNAL_RUNTIME_SCAFFOLDING_TAG_PATTERN})\\b[^>]*>`,
"gi",
);
const INTERNAL_RUNTIME_DELIMITED_BLOCKS = [
["<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>", "<<<END_OPENCLAW_INTERNAL_CONTEXT>>>"],
] as const;
const INTERNAL_RUNTIME_MARKER_LINES = [
"<<<BEGIN_UNTRUSTED_CHILD_RESULT>>>",
"<<<END_UNTRUSTED_CHILD_RESULT>>>",
@@ -28,20 +26,6 @@ function standaloneLinePattern(token: string): string {
return `(?:^|\\r?\\n)[ \\t]*${escapeRegExp(token)}[ \\t]*(?=\\r?\\n|$)`;
}
function stripDelimitedRuntimeBlock(text: string, begin: string, end: string): string {
const closedBlockRe = new RegExp(
`${standaloneLinePattern(begin)}[\\s\\S]*?${standaloneLinePattern(end)}`,
"g",
);
// If the closing delimiter is missing, drop the rest rather than leaking
// internal runtime context to user-visible outbound text.
const unmatchedBeginRe = new RegExp(`${standaloneLinePattern(begin)}[\\s\\S]*$`, "g");
return stripStandaloneMarkerLine(
text.replace(closedBlockRe, "").replace(unmatchedBeginRe, ""),
end,
);
}
function stripStandaloneMarkerLine(text: string, marker: string): string {
return text.replace(new RegExp(standaloneLinePattern(marker), "g"), "");
}
@@ -78,13 +62,13 @@ function unwrapPromptDataWrapperLines(text: string): string {
}
export function stripInternalRuntimeScaffolding(text: string): string {
let stripped = unwrapPromptDataWrapperLines(text)
.replace(INTERNAL_RUNTIME_SCAFFOLDING_BLOCK_RE, "")
.replace(INTERNAL_RUNTIME_SCAFFOLDING_SELF_CLOSING_RE, "")
.replace(INTERNAL_RUNTIME_SCAFFOLDING_TAG_RE, "");
for (const [begin, end] of INTERNAL_RUNTIME_DELIMITED_BLOCKS) {
stripped = stripDelimitedRuntimeBlock(stripped, begin, end);
}
let stripped = stripInternalRuntimeContext(
unwrapPromptDataWrapperLines(text)
.replace(INTERNAL_RUNTIME_SCAFFOLDING_BLOCK_RE, "")
.replace(INTERNAL_RUNTIME_SCAFFOLDING_SELF_CLOSING_RE, "")
.replace(INTERNAL_RUNTIME_SCAFFOLDING_TAG_RE, ""),
{ preserveSurroundingWhitespace: true },
);
for (const marker of INTERNAL_RUNTIME_MARKER_LINES) {
stripped = stripStandaloneMarkerLine(stripped, marker);
}
+28
View File
@@ -171,6 +171,34 @@ describe("stripInternalRuntimeScaffolding", () => {
).toBe("before\nafter");
});
it("removes indented runtime context delimiters without leaving marker fragments", () => {
expect(
stripInternalRuntimeScaffolding(
[
"before",
" <<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>",
"internal",
"\t<<<END_OPENCLAW_INTERNAL_CONTEXT>>> ",
"after",
].join("\n"),
),
).toBe("before\nafter");
});
it("preserves visible whitespace around removed runtime context", () => {
expect(
stripInternalRuntimeScaffolding(
[
"before ",
"<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>",
"internal",
"<<<END_OPENCLAW_INTERNAL_CONTEXT>>>",
" indented code",
].join("\n"),
),
).toBe("before \n indented code");
});
it("unwraps standalone untrusted child-result marker lines", () => {
expect(
stripInternalRuntimeScaffolding(
+1 -1
View File
@@ -84,7 +84,7 @@ export type UsageCostStoredRollup = {
valueJson: string;
};
export type UsageCostRefreshResult = "refreshed" | "busy";
type UsageCostRefreshResult = "refreshed" | "busy";
export function resolveUsageCostCacheDatabasePath(agentId: string): string {
return resolveOpenClawAgentSqlitePath({ agentId: normalizeAgentId(agentId) });
+1 -13
View File
@@ -13,7 +13,6 @@ import {
resolveUsageCostAgentDir,
resolveUsageCostCacheDatabasePath,
resolveUsageCostPricingFingerprint,
type UsageCostRefreshResult,
} from "./session-cost-usage-aggregation.js";
import { isSessionCostUsageRefreshRunning } from "./session-cost-usage-cache.sqlite.js";
import {
@@ -87,17 +86,6 @@ export async function loadCostUsageSummary(params: {
});
}
async function refreshCostUsageCache(params: {
config?: OpenClawConfig;
agentId: string;
agentDir?: string;
maxFiles?: number;
sessionFiles?: string[];
startMs?: number;
}): Promise<UsageCostRefreshResult> {
return await refreshCostUsageCacheForAgent(params);
}
export async function loadCostUsageSummaryFromCache(params: {
startMs: number;
endMs: number;
@@ -116,7 +104,7 @@ export async function loadCostUsageSummaryFromCache(params: {
if (params.requestRefresh !== false && staleFiles.length > 0) {
const cachedFiles = countUsableUsageCostRollups({ rollups, files });
if (params.refreshMode === "sync-when-empty" && cachedFiles === 0) {
const result = await refreshCostUsageCache({
const result = await refreshCostUsageCacheForAgent({
config: params.config,
agentId: params.agentId,
agentDir,
+20 -43
View File
@@ -37,7 +37,7 @@ import {
parseUsageCostTranscriptEntry,
type UsageCostResolver,
} from "./session-cost-usage-pricing.js";
import type { ParsedTranscriptEntry, ParsedUsageEntry } from "./session-cost-usage.types.js";
import type { ParsedUsageEntry } from "./session-cost-usage.types.js";
export const USAGE_COST_TRANSCRIPT_STAT_CONCURRENCY = 32;
@@ -337,28 +337,6 @@ export async function* readTranscriptRecordsBestEffort(
}
}
async function scanTranscriptFile(params: {
filePath: string;
config?: OpenClawConfig;
resolveCost?: UsageCostResolver;
startOffset?: number;
endOffset?: number;
onEntry: (entry: ParsedTranscriptEntry) => void;
}): Promise<void> {
const resolveCost = params.resolveCost ?? createUsageCostResolver({ config: params.config });
for await (const parsed of readTranscriptRecords(
params.filePath,
params.startOffset,
params.endOffset,
)) {
const entry = parseUsageCostTranscriptEntry(parsed, resolveCost);
if (!entry) {
continue;
}
params.onEntry(entry);
}
}
export async function scanUsageFile(params: {
filePath: string;
config?: OpenClawConfig;
@@ -367,26 +345,25 @@ export async function scanUsageFile(params: {
endOffset?: number;
onEntry: (entry: ParsedUsageEntry) => void;
}): Promise<void> {
await scanTranscriptFile({
filePath: params.filePath,
config: params.config,
resolveCost: params.resolveCost,
startOffset: params.startOffset,
endOffset: params.endOffset,
onEntry: (entry) => {
if (!entry.usage) {
return;
}
params.onEntry({
usage: entry.usage,
costTotal: entry.costTotal,
costBreakdown: entry.costBreakdown,
provider: entry.provider,
model: entry.model,
timestamp: entry.timestamp,
});
},
});
const resolveCost = params.resolveCost ?? createUsageCostResolver({ config: params.config });
for await (const parsed of readTranscriptRecords(
params.filePath,
params.startOffset,
params.endOffset,
)) {
const entry = parseUsageCostTranscriptEntry(parsed, resolveCost);
if (!entry?.usage) {
continue;
}
params.onEntry({
usage: entry.usage,
costTotal: entry.costTotal,
costBreakdown: entry.costBreakdown,
provider: entry.provider,
model: entry.model,
timestamp: entry.timestamp,
});
}
}
export function resolveExistingUsageSessionFile(params: {