diff --git a/extensions/arcee/onboard.ts b/extensions/arcee/onboard.ts index 43babe3e193..b45d7943fe9 100644 --- a/extensions/arcee/onboard.ts +++ b/extensions/arcee/onboard.ts @@ -2,7 +2,7 @@ import { createModelCatalogPresetAppliers, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; -import { ARCEE_BASE_URL } from "./api.js"; +import { ARCEE_BASE_URL } from "./models.js"; import { buildArceeCatalogModels, buildArceeOpenRouterCatalogModels, diff --git a/extensions/arcee/provider-catalog.ts b/extensions/arcee/provider-catalog.ts index 2827ee58730..3b2b5729bd7 100644 --- a/extensions/arcee/provider-catalog.ts +++ b/extensions/arcee/provider-catalog.ts @@ -1,5 +1,5 @@ import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; -import { buildArceeModelDefinition, ARCEE_BASE_URL, ARCEE_MODEL_CATALOG } from "./api.js"; +import { buildArceeModelDefinition, ARCEE_BASE_URL, ARCEE_MODEL_CATALOG } from "./models.js"; export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"; diff --git a/extensions/browser/src/browser/cdp.helpers.ts b/extensions/browser/src/browser/cdp.helpers.ts index c39f982f819..e77a399906f 100644 --- a/extensions/browser/src/browser/cdp.helpers.ts +++ b/extensions/browser/src/browser/cdp.helpers.ts @@ -6,7 +6,7 @@ import { rawDataToString } from "../infra/ws.js"; import { redactSensitiveText } from "../logging/redact.js"; import { getDirectAgentForCdp, withNoProxyForCdpUrl } from "./cdp-proxy-bypass.js"; import { CDP_HTTP_REQUEST_TIMEOUT_MS, CDP_WS_HANDSHAKE_TIMEOUT_MS } from "./cdp-timeouts.js"; -import { resolveBrowserRateLimitMessage } from "./client-fetch.js"; +import { resolveBrowserRateLimitMessage } from "./rate-limit-message.js"; export { isLoopbackHost }; diff --git a/extensions/browser/src/browser/client-fetch.ts b/extensions/browser/src/browser/client-fetch.ts index ee90a053dde..e9a924f00f0 100644 --- a/extensions/browser/src/browser/client-fetch.ts +++ b/extensions/browser/src/browser/client-fetch.ts @@ -9,6 +9,7 @@ import { createBrowserControlContext, startBrowserControlServiceFromConfig, } from "./control-service.js"; +import { resolveBrowserRateLimitMessage } from "./rate-limit-message.js"; import { createBrowserRouteDispatcher } from "./routes/dispatcher.js"; // Application-level error from the browser control service (service is reachable @@ -104,36 +105,10 @@ const BROWSER_TOOL_MODEL_HINT = "Do NOT retry the browser tool — it will keep failing. " + "Use an alternative approach or inform the user that the browser is currently unavailable."; -const BROWSER_SERVICE_RATE_LIMIT_MESSAGE = - "Browser service rate limit reached. " + - "Wait for the current session to complete, or retry later."; - -const BROWSERBASE_RATE_LIMIT_MESSAGE = - "Browserbase rate limit reached (max concurrent sessions). " + - "Wait for the current session to complete, or upgrade your plan."; - function isRateLimitStatus(status: number): boolean { return status === 429; } -function isBrowserbaseUrl(url: string): boolean { - if (!isAbsoluteHttp(url)) { - return false; - } - try { - const host = normalizeLowercaseStringOrEmpty(new URL(url).hostname); - return host === "browserbase.com" || host.endsWith(".browserbase.com"); - } catch { - return false; - } -} - -export function resolveBrowserRateLimitMessage(url: string): string { - return isBrowserbaseUrl(url) - ? BROWSERBASE_RATE_LIMIT_MESSAGE - : BROWSER_SERVICE_RATE_LIMIT_MESSAGE; -} - function resolveBrowserFetchOperatorHint(url: string): string { const isLocal = !isAbsoluteHttp(url); return isLocal diff --git a/extensions/browser/src/browser/rate-limit-message.ts b/extensions/browser/src/browser/rate-limit-message.ts new file mode 100644 index 00000000000..4ad9886797a --- /dev/null +++ b/extensions/browser/src/browser/rate-limit-message.ts @@ -0,0 +1,31 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; + +const BROWSER_SERVICE_RATE_LIMIT_MESSAGE = + "Browser service rate limit reached. " + + "Wait for the current session to complete, or retry later."; + +const BROWSERBASE_RATE_LIMIT_MESSAGE = + "Browserbase rate limit reached (max concurrent sessions). " + + "Wait for the current session to complete, or upgrade your plan."; + +function isAbsoluteHttp(url: string): boolean { + return /^https?:\/\//i.test(url.trim()); +} + +function isBrowserbaseUrl(url: string): boolean { + if (!isAbsoluteHttp(url)) { + return false; + } + try { + const host = normalizeLowercaseStringOrEmpty(new URL(url).hostname); + return host === "browserbase.com" || host.endsWith(".browserbase.com"); + } catch { + return false; + } +} + +export function resolveBrowserRateLimitMessage(url: string): string { + return isBrowserbaseUrl(url) + ? BROWSERBASE_RATE_LIMIT_MESSAGE + : BROWSER_SERVICE_RATE_LIMIT_MESSAGE; +} diff --git a/extensions/browser/src/node-host/invoke-browser.ts b/extensions/browser/src/node-host/invoke-browser.ts index b403fc03c50..b7f05084c37 100644 --- a/extensions/browser/src/node-host/invoke-browser.ts +++ b/extensions/browser/src/node-host/invoke-browser.ts @@ -1,17 +1,19 @@ import fsPromises from "node:fs/promises"; +import { loadConfig } from "openclaw/plugin-sdk/browser-config-runtime"; +import { withTimeout } from "openclaw/plugin-sdk/browser-node-runtime"; +import { detectMime } from "openclaw/plugin-sdk/browser-setup-tools"; +import { redactCdpUrl } from "../browser/cdp.helpers.js"; +import { resolveBrowserConfig } from "../browser/config.js"; +import { + isPersistentBrowserProfileMutation, + normalizeBrowserRequestPath, + resolveRequestedBrowserProfile, +} from "../browser/request-policy.js"; +import { createBrowserRouteDispatcher } from "../browser/routes/dispatcher.js"; import { createBrowserControlContext, - createBrowserRouteDispatcher, - detectMime, - isPersistentBrowserProfileMutation, - loadConfig, - normalizeBrowserRequestPath, - redactCdpUrl, - resolveBrowserConfig, - resolveRequestedBrowserProfile, startBrowserControlServiceFromConfig, - withTimeout, -} from "../core-api.js"; +} from "../control-service.js"; type BrowserProxyParams = { method?: string; diff --git a/extensions/byteplus/provider-catalog.ts b/extensions/byteplus/provider-catalog.ts index 0e4b8f51a27..396fc2ca8ed 100644 --- a/extensions/byteplus/provider-catalog.ts +++ b/extensions/byteplus/provider-catalog.ts @@ -5,7 +5,7 @@ import { BYTEPLUS_CODING_BASE_URL, BYTEPLUS_CODING_MODEL_CATALOG, BYTEPLUS_MODEL_CATALOG, -} from "./api.js"; +} from "./models.js"; export function buildBytePlusProvider(): ModelProviderConfig { return { diff --git a/extensions/chutes/onboard.ts b/extensions/chutes/onboard.ts index 070ea471aa8..659437aa69d 100644 --- a/extensions/chutes/onboard.ts +++ b/extensions/chutes/onboard.ts @@ -8,7 +8,7 @@ import { CHUTES_DEFAULT_MODEL_REF, CHUTES_MODEL_CATALOG, buildChutesModelDefinition, -} from "./api.js"; +} from "./models.js"; export { CHUTES_DEFAULT_MODEL_REF }; diff --git a/extensions/chutes/provider-catalog.ts b/extensions/chutes/provider-catalog.ts index cea9bda63b5..5148037509d 100644 --- a/extensions/chutes/provider-catalog.ts +++ b/extensions/chutes/provider-catalog.ts @@ -4,7 +4,7 @@ import { CHUTES_MODEL_CATALOG, buildChutesModelDefinition, discoverChutesModels, -} from "./api.js"; +} from "./models.js"; /** * Build the Chutes provider with dynamic model discovery. diff --git a/extensions/deepseek/provider-catalog.ts b/extensions/deepseek/provider-catalog.ts index d3e3f6b3b07..2a0b7313bb1 100644 --- a/extensions/deepseek/provider-catalog.ts +++ b/extensions/deepseek/provider-catalog.ts @@ -1,5 +1,9 @@ import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; -import { buildDeepSeekModelDefinition, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL_CATALOG } from "./api.js"; +import { + buildDeepSeekModelDefinition, + DEEPSEEK_BASE_URL, + DEEPSEEK_MODEL_CATALOG, +} from "./models.js"; export function buildDeepSeekProvider(): ModelProviderConfig { return { diff --git a/extensions/discord/src/monitor.gateway.ts b/extensions/discord/src/monitor.gateway.ts index 3ecf1a2dda5..1ea48af3a7f 100644 --- a/extensions/discord/src/monitor.gateway.ts +++ b/extensions/discord/src/monitor.gateway.ts @@ -1,4 +1,3 @@ -import type { EventEmitter } from "node:events"; import type { DiscordGatewayHandle } from "./monitor/gateway-handle.js"; import { DiscordGatewayEvent, @@ -6,6 +5,8 @@ import { DiscordGatewaySupervisor, } from "./monitor/gateway-supervisor.js"; +export { getDiscordGatewayEmitter } from "./monitor/gateway-supervisor.js"; + export type WaitForDiscordGatewayStopParams = { gateway?: DiscordGatewayHandle; abortSignal?: AbortSignal; @@ -14,10 +15,6 @@ export type WaitForDiscordGatewayStopParams = { registerForceStop?: (forceStop: (err: unknown) => void) => void; }; -export function getDiscordGatewayEmitter(gateway?: unknown): EventEmitter | undefined { - return (gateway as { emitter?: EventEmitter } | undefined)?.emitter; -} - export async function waitForDiscordGatewayStop( params: WaitForDiscordGatewayStopParams, ): Promise { diff --git a/extensions/discord/src/monitor/gateway-supervisor.ts b/extensions/discord/src/monitor/gateway-supervisor.ts index 71f7f5874ae..99929c3731d 100644 --- a/extensions/discord/src/monitor/gateway-supervisor.ts +++ b/extensions/discord/src/monitor/gateway-supervisor.ts @@ -2,7 +2,6 @@ import type { EventEmitter } from "node:events"; import { danger } from "openclaw/plugin-sdk/runtime-env"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime"; -import { getDiscordGatewayEmitter } from "../monitor.gateway.js"; export type DiscordGatewayEventType = | "disallowed-intents" @@ -29,6 +28,10 @@ export class DiscordGatewayLifecycleError extends Error { } } +export function getDiscordGatewayEmitter(gateway?: unknown): EventEmitter | undefined { + return (gateway as { emitter?: EventEmitter } | undefined)?.emitter; +} + export type DiscordGatewaySupervisor = { emitter?: EventEmitter; attachLifecycle: (handler: (event: DiscordGatewayEvent) => void) => void; diff --git a/extensions/mattermost/index.ts b/extensions/mattermost/index.ts index 987c2ffe6e6..63c6a11c2b3 100644 --- a/extensions/mattermost/index.ts +++ b/extensions/mattermost/index.ts @@ -6,7 +6,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/channel-entry-contra function registerSlashCommandRoute(api: OpenClawPluginApi): void { const register = loadBundledEntryExportSync<(api: OpenClawPluginApi) => void>(import.meta.url, { - specifier: "./runtime-api.js", + specifier: "./slash-route-api.js", exportName: "registerSlashCommandRoute", }); register(api); diff --git a/extensions/mattermost/runtime-api.ts b/extensions/mattermost/runtime-api.ts index d232769bd1c..0bac0c5f34a 100644 --- a/extensions/mattermost/runtime-api.ts +++ b/extensions/mattermost/runtime-api.ts @@ -86,5 +86,4 @@ export { resolveChannelMediaMaxBytes, } from "openclaw/plugin-sdk/media-runtime"; export { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared"; -export { registerSlashCommandRoute } from "./src/mattermost/slash-state.js"; export { setMattermostRuntime } from "./src/runtime.js"; diff --git a/extensions/mattermost/slash-route-api.ts b/extensions/mattermost/slash-route-api.ts new file mode 100644 index 00000000000..1eb801d16ca --- /dev/null +++ b/extensions/mattermost/slash-route-api.ts @@ -0,0 +1 @@ +export { registerSlashCommandRoute } from "./src/mattermost/slash-state.js"; diff --git a/extensions/nostr/src/nostr-profile-http.ts b/extensions/nostr/src/nostr-profile-http.ts index a6b2d366ec4..e20b1f7d747 100644 --- a/extensions/nostr/src/nostr-profile-http.ts +++ b/extensions/nostr/src/nostr-profile-http.ts @@ -16,13 +16,13 @@ import { import { z } from "openclaw/plugin-sdk/zod"; import { createFixedWindowRateLimiter, - isBlockedHostnameOrIp, readJsonBodyWithLimit, requestBodyErrorToText, -} from "../api.js"; +} from "../runtime-api.js"; import { publishNostrProfile, getNostrProfileState } from "./channel.js"; import { NostrProfileSchema, type NostrProfile } from "./config-schema.js"; import { importProfileFromRelays, mergeProfiles } from "./nostr-profile-import.js"; +import { validateUrlSafety } from "./nostr-profile-url-safety.js"; // ============================================================================ // Types @@ -103,30 +103,6 @@ async function withPublishLock(accountId: string, fn: () => Promise): Prom } } -// ============================================================================ -// SSRF Protection -// ============================================================================ - -function validateUrlSafety(urlStr: string): { ok: true } | { ok: false; error: string } { - try { - const url = new URL(urlStr); - - if (url.protocol !== "https:") { - return { ok: false, error: "URL must use https:// protocol" }; - } - - const hostname = normalizeLowercaseStringOrEmpty(url.hostname); - - if (isBlockedHostnameOrIp(hostname)) { - return { ok: false, error: "URL must not point to private/internal addresses" }; - } - - return { ok: true }; - } catch { - return { ok: false, error: "Invalid URL format" }; - } -} - // Export for use in import validation export { validateUrlSafety }; diff --git a/extensions/nostr/src/nostr-profile-import.ts b/extensions/nostr/src/nostr-profile-import.ts index a2ea80019d3..db1da623971 100644 --- a/extensions/nostr/src/nostr-profile-import.ts +++ b/extensions/nostr/src/nostr-profile-import.ts @@ -7,7 +7,7 @@ import { SimplePool, verifyEvent, type Event } from "nostr-tools"; import type { NostrProfile } from "./config-schema.js"; -import { validateUrlSafety } from "./nostr-profile-http.js"; +import { validateUrlSafety } from "./nostr-profile-url-safety.js"; import { contentToProfile, type ProfileContent } from "./nostr-profile.js"; // ============================================================================ diff --git a/extensions/nostr/src/nostr-profile-url-safety.ts b/extensions/nostr/src/nostr-profile-url-safety.ts new file mode 100644 index 00000000000..bbee86101b0 --- /dev/null +++ b/extensions/nostr/src/nostr-profile-url-safety.ts @@ -0,0 +1,22 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; +import { isBlockedHostnameOrIp } from "../runtime-api.js"; + +export function validateUrlSafety(urlStr: string): { ok: true } | { ok: false; error: string } { + try { + const url = new URL(urlStr); + + if (url.protocol !== "https:") { + return { ok: false, error: "URL must use https:// protocol" }; + } + + const hostname = normalizeLowercaseStringOrEmpty(url.hostname); + + if (isBlockedHostnameOrIp(hostname)) { + return { ok: false, error: "URL must not point to private/internal addresses" }; + } + + return { ok: true }; + } catch { + return { ok: false, error: "Invalid URL format" }; + } +} diff --git a/extensions/qa-lab/api.ts b/extensions/qa-lab/api.ts index e807386e1c1..140ea5d1190 100644 --- a/extensions/qa-lab/api.ts +++ b/extensions/qa-lab/api.ts @@ -15,5 +15,6 @@ export * from "./src/scenario.js"; export * from "./src/scenario-catalog.js"; export * from "./src/self-check-scenario.js"; export * from "./src/self-check.js"; +export * from "./src/self-check-runner.js"; export * from "./src/gateway-child.js"; export * from "./src/suite.js"; diff --git a/extensions/qa-lab/src/self-check-runner.ts b/extensions/qa-lab/src/self-check-runner.ts new file mode 100644 index 00000000000..669ce769ee4 --- /dev/null +++ b/extensions/qa-lab/src/self-check-runner.ts @@ -0,0 +1,15 @@ +import { startQaLabServer } from "./lab-server.js"; + +export async function runQaLabSelfCheck(params?: { repoRoot?: string; outputPath?: string }) { + const server = await startQaLabServer({ + repoRoot: params?.repoRoot, + outputPath: params?.outputPath, + }); + try { + return await server.runSelfCheck(); + } finally { + await server.stop(); + } +} + +export const runQaE2eSelfCheck = runQaLabSelfCheck; diff --git a/extensions/qa-lab/src/self-check.ts b/extensions/qa-lab/src/self-check.ts index 9980030c462..848de9e0857 100644 --- a/extensions/qa-lab/src/self-check.ts +++ b/extensions/qa-lab/src/self-check.ts @@ -2,7 +2,6 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { QaBusState } from "./bus-state.js"; -import { startQaLabServer } from "./lab-server.js"; import { renderQaMarkdownReport } from "./report.js"; import { runQaScenario, type QaScenarioResult } from "./scenario.js"; import { createQaSelfCheckScenario } from "./self-check-scenario.js"; @@ -87,17 +86,3 @@ export async function runQaSelfCheckAgainstState(params: { scenarioResult, }; } - -export async function runQaLabSelfCheck(params?: { repoRoot?: string; outputPath?: string }) { - const server = await startQaLabServer({ - repoRoot: params?.repoRoot, - outputPath: params?.outputPath, - }); - try { - return await server.runSelfCheck(); - } finally { - await server.stop(); - } -} - -export const runQaE2eSelfCheck = runQaLabSelfCheck; diff --git a/extensions/together/onboard.ts b/extensions/together/onboard.ts index 3f298355d87..f9ca510a413 100644 --- a/extensions/together/onboard.ts +++ b/extensions/together/onboard.ts @@ -2,7 +2,11 @@ import { createModelCatalogPresetAppliers, type OpenClawConfig, } from "openclaw/plugin-sdk/provider-onboard"; -import { buildTogetherModelDefinition, TOGETHER_BASE_URL, TOGETHER_MODEL_CATALOG } from "./api.js"; +import { + buildTogetherModelDefinition, + TOGETHER_BASE_URL, + TOGETHER_MODEL_CATALOG, +} from "./models.js"; export const TOGETHER_DEFAULT_MODEL_REF = "together/moonshotai/Kimi-K2.5"; diff --git a/extensions/together/provider-catalog.ts b/extensions/together/provider-catalog.ts index dbfaafdb93a..e30cc678958 100644 --- a/extensions/together/provider-catalog.ts +++ b/extensions/together/provider-catalog.ts @@ -1,5 +1,9 @@ import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; -import { buildTogetherModelDefinition, TOGETHER_BASE_URL, TOGETHER_MODEL_CATALOG } from "./api.js"; +import { + buildTogetherModelDefinition, + TOGETHER_BASE_URL, + TOGETHER_MODEL_CATALOG, +} from "./models.js"; export function buildTogetherProvider(): ModelProviderConfig { return { diff --git a/extensions/twitch/src/plugin.ts b/extensions/twitch/src/plugin.ts index 0650b66715e..265c853a83c 100644 --- a/extensions/twitch/src/plugin.ts +++ b/extensions/twitch/src/plugin.ts @@ -6,6 +6,7 @@ */ import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers"; +import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema"; import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core"; import { createLoggedPairingApprovalNotifier, @@ -17,7 +18,6 @@ import { createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers"; import type { OpenClawConfig } from "../api.js"; -import { buildChannelConfigSchema } from "../api.js"; import { twitchMessageActions } from "./actions.js"; import { removeClientManager } from "./client-manager-registry.js"; import { TwitchConfigSchema } from "./config-schema.js"; diff --git a/extensions/venice/provider-catalog.ts b/extensions/venice/provider-catalog.ts index 1bd7fec2f20..37a7eb7f047 100644 --- a/extensions/venice/provider-catalog.ts +++ b/extensions/venice/provider-catalog.ts @@ -1,5 +1,5 @@ import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; -import { discoverVeniceModels, VENICE_BASE_URL } from "./api.js"; +import { discoverVeniceModels, VENICE_BASE_URL } from "./models.js"; export async function buildVeniceProvider(): Promise { const models = await discoverVeniceModels(); diff --git a/extensions/vercel-ai-gateway/provider-catalog.ts b/extensions/vercel-ai-gateway/provider-catalog.ts index deb7c571174..e2390784f81 100644 --- a/extensions/vercel-ai-gateway/provider-catalog.ts +++ b/extensions/vercel-ai-gateway/provider-catalog.ts @@ -1,5 +1,5 @@ import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; -import { discoverVercelAiGatewayModels, VERCEL_AI_GATEWAY_BASE_URL } from "./api.js"; +import { discoverVercelAiGatewayModels, VERCEL_AI_GATEWAY_BASE_URL } from "./models.js"; export async function buildVercelAiGatewayProvider(): Promise { return { diff --git a/extensions/volcengine/provider-catalog.ts b/extensions/volcengine/provider-catalog.ts index 33825c0b192..1e58d52fcec 100644 --- a/extensions/volcengine/provider-catalog.ts +++ b/extensions/volcengine/provider-catalog.ts @@ -5,7 +5,7 @@ import { DOUBAO_CODING_BASE_URL, DOUBAO_CODING_MODEL_CATALOG, DOUBAO_MODEL_CATALOG, -} from "./api.js"; +} from "./models.js"; export function buildDoubaoProvider(): ModelProviderConfig { return { diff --git a/extensions/xai/provider-models.ts b/extensions/xai/provider-models.ts index d56be1c43a1..8c02f4886a7 100644 --- a/extensions/xai/provider-models.ts +++ b/extensions/xai/provider-models.ts @@ -3,8 +3,8 @@ import type { ProviderRuntimeModel, } from "openclaw/plugin-sdk/plugin-entry"; import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-model-shared"; +import { applyXaiModelCompat } from "openclaw/plugin-sdk/provider-tools"; import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; -import { applyXaiModelCompat } from "./api.js"; import { resolveXaiCatalogEntry, XAI_BASE_URL } from "./model-definitions.js"; const XAI_MODERN_MODEL_PREFIXES = ["grok-3", "grok-4", "grok-code-fast"] as const; diff --git a/src/agents/pi-embedded-runner/transcript-rewrite.ts b/src/agents/pi-embedded-runner/transcript-rewrite.ts index 2f173980579..42eafc2b0af 100644 --- a/src/agents/pi-embedded-runner/transcript-rewrite.ts +++ b/src/agents/pi-embedded-runner/transcript-rewrite.ts @@ -7,7 +7,7 @@ import type { } from "../../context-engine/types.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js"; -import { getRawSessionAppendMessage } from "../session-tool-result-guard.js"; +import { getRawSessionAppendMessage } from "../session-raw-append-message.js"; import { acquireSessionWriteLock } from "../session-write-lock.js"; import { log } from "./logger.js"; diff --git a/src/agents/session-raw-append-message.ts b/src/agents/session-raw-append-message.ts new file mode 100644 index 00000000000..6aef2d3ac68 --- /dev/null +++ b/src/agents/session-raw-append-message.ts @@ -0,0 +1,24 @@ +import type { SessionManager } from "@mariozechner/pi-coding-agent"; + +const RAW_APPEND_MESSAGE = Symbol("openclaw.session.rawAppendMessage"); + +export type SessionManagerWithRawAppend = SessionManager & { + [RAW_APPEND_MESSAGE]?: SessionManager["appendMessage"]; +}; + +/** + * Return the unguarded appendMessage implementation for a session manager. + */ +export function getRawSessionAppendMessage( + sessionManager: SessionManager, +): SessionManager["appendMessage"] { + const rawAppend = (sessionManager as SessionManagerWithRawAppend)[RAW_APPEND_MESSAGE]; + return rawAppend ?? sessionManager.appendMessage.bind(sessionManager); +} + +export function setRawSessionAppendMessage( + sessionManager: SessionManager, + appendMessage: SessionManager["appendMessage"], +): void { + (sessionManager as SessionManagerWithRawAppend)[RAW_APPEND_MESSAGE] = appendMessage; +} diff --git a/src/agents/session-tool-result-guard.ts b/src/agents/session-tool-result-guard.ts index 8118d089261..ecefb2dd6da 100644 --- a/src/agents/session-tool-result-guard.ts +++ b/src/agents/session-tool-result-guard.ts @@ -11,14 +11,13 @@ import { DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS, truncateToolResultMessage, } from "./pi-embedded-runner/tool-result-truncation.js"; +import { + getRawSessionAppendMessage, + setRawSessionAppendMessage, +} from "./session-raw-append-message.js"; import { createPendingToolCallState } from "./session-tool-result-state.js"; import { makeMissingToolResult, sanitizeToolCallInputs } from "./session-transcript-repair.js"; import { extractToolCallsFromAssistant, extractToolResultId } from "./tool-call-id.js"; -const RAW_APPEND_MESSAGE = Symbol("openclaw.session.rawAppendMessage"); - -type SessionManagerWithRawAppend = SessionManager & { - [RAW_APPEND_MESSAGE]?: SessionManager["appendMessage"]; -}; /** * Truncate oversized text content blocks in a tool result message. @@ -63,15 +62,7 @@ function normalizePersistedToolResultName( return toolResult; } -/** - * Return the unguarded appendMessage implementation for a session manager. - */ -export function getRawSessionAppendMessage( - sessionManager: SessionManager, -): SessionManager["appendMessage"] { - const rawAppend = (sessionManager as SessionManagerWithRawAppend)[RAW_APPEND_MESSAGE]; - return rawAppend ?? sessionManager.appendMessage.bind(sessionManager); -} +export { getRawSessionAppendMessage }; export function installSessionToolResultGuard( sessionManager: SessionManager, @@ -115,7 +106,7 @@ export function installSessionToolResultGuard( getPendingIds: () => string[]; } { const originalAppend = getRawSessionAppendMessage(sessionManager); - (sessionManager as SessionManagerWithRawAppend)[RAW_APPEND_MESSAGE] = originalAppend; + setRawSessionAppendMessage(sessionManager, originalAppend); const pendingState = createPendingToolCallState(); const persistMessage = (message: AgentMessage) => { const transformer = opts?.transformMessageForPersistence; diff --git a/src/agents/subagent-announce-delivery.runtime.ts b/src/agents/subagent-announce-delivery.runtime.ts index e94dfa6c775..fc4773c76eb 100644 --- a/src/agents/subagent-announce-delivery.runtime.ts +++ b/src/agents/subagent-announce-delivery.runtime.ts @@ -11,4 +11,4 @@ export { resolveExternalBestEffortDeliveryTarget } from "../infra/outbound/best- export { createBoundDeliveryRouter } from "../infra/outbound/bound-delivery-router.js"; export { resolveConversationIdFromTargets } from "../infra/outbound/conversation-id.js"; export { getGlobalHookRunner } from "../plugins/hook-runner-global.js"; -export { isEmbeddedPiRunActive, queueEmbeddedPiMessage } from "./pi-embedded.js"; +export { isEmbeddedPiRunActive, queueEmbeddedPiMessage } from "./pi-embedded-runner/runs.js"; diff --git a/src/agents/subagent-announce.runtime.ts b/src/agents/subagent-announce.runtime.ts index ec73e311584..250147c40bd 100644 --- a/src/agents/subagent-announce.runtime.ts +++ b/src/agents/subagent-announce.runtime.ts @@ -10,4 +10,4 @@ export { isEmbeddedPiRunActive, queueEmbeddedPiMessage, waitForEmbeddedPiRunEnd, -} from "./pi-embedded.js"; +} from "./pi-embedded-runner/runs.js"; diff --git a/src/agents/subagent-control.ts b/src/agents/subagent-control.ts index 44bb4cd5521..eeaed507010 100644 --- a/src/agents/subagent-control.ts +++ b/src/agents/subagent-control.ts @@ -15,7 +15,7 @@ import { formatErrorMessage } from "../infra/errors.js"; import { isSubagentSessionKey, parseAgentSessionKey } from "../routing/session-key.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js"; import { AGENT_LANE_SUBAGENT } from "./lanes.js"; -import { abortEmbeddedPiRun } from "./pi-embedded.js"; +import { abortEmbeddedPiRun } from "./pi-embedded-runner/runs.js"; import { readLatestAssistantReplySnapshot, waitForAgentRunAndReadUpdatedAssistantReply, diff --git a/src/config/defaults.test.ts b/src/config/defaults.test.ts index 343b58d0bb7..acd698e2139 100644 --- a/src/config/defaults.test.ts +++ b/src/config/defaults.test.ts @@ -2,13 +2,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js"; const mocks = vi.hoisted(() => ({ - applyProviderConfigDefaultsWithPlugin: vi.fn(), + applyProviderConfigDefaultsForConfig: vi.fn(), })); -vi.mock("../plugins/provider-runtime.js", () => ({ - applyProviderConfigDefaultsWithPlugin: ( - ...args: Parameters - ) => mocks.applyProviderConfigDefaultsWithPlugin(...args), +vi.mock("./provider-policy.js", () => ({ + applyProviderConfigDefaultsForConfig: ( + ...args: Parameters + ) => mocks.applyProviderConfigDefaultsForConfig(...args), + normalizeProviderConfigForConfigDefaults: (_params: { providerConfig: unknown }) => + _params.providerConfig, })); let applyContextPruningDefaults: typeof import("./defaults.js").applyContextPruningDefaults; @@ -20,7 +22,7 @@ describe("config defaults", () => { vi.resetModules(); ({ applyAgentDefaults, applyContextPruningDefaults, applyMessageDefaults } = await import("./defaults.js")); - mocks.applyProviderConfigDefaultsWithPlugin.mockReset(); + mocks.applyProviderConfigDefaultsForConfig.mockReset(); }); it("skips provider defaults when agent defaults are absent", () => { @@ -35,7 +37,7 @@ describe("config defaults", () => { }; expect(applyContextPruningDefaults(cfg as never)).toBe(cfg); - expect(mocks.applyProviderConfigDefaultsWithPlugin).not.toHaveBeenCalled(); + expect(mocks.applyProviderConfigDefaultsForConfig).not.toHaveBeenCalled(); }); it("uses anthropic provider defaults when agent defaults exist", () => { @@ -53,10 +55,10 @@ describe("config defaults", () => { }, }, }; - mocks.applyProviderConfigDefaultsWithPlugin.mockReturnValue(nextCfg); + mocks.applyProviderConfigDefaultsForConfig.mockReturnValue(nextCfg); expect(applyContextPruningDefaults(cfg as never)).toBe(nextCfg); - expect(mocks.applyProviderConfigDefaultsWithPlugin).toHaveBeenCalledTimes(1); + expect(mocks.applyProviderConfigDefaultsForConfig).toHaveBeenCalledTimes(1); }); it("defaults ackReactionScope without deriving other message fields", () => { diff --git a/src/config/defaults.ts b/src/config/defaults.ts index e19fc67ffbf..90c7c330f09 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -1,8 +1,10 @@ import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js"; -import { normalizeProviderSpecificConfig } from "../agents/models-config.providers.policy.js"; import { normalizeProviderId } from "../agents/provider-id.js"; -import { applyProviderConfigDefaultsWithPlugin } from "../plugins/provider-runtime.js"; import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js"; +import { + applyProviderConfigDefaultsForConfig, + normalizeProviderConfigForConfigDefaults, +} from "./provider-policy.js"; import { normalizeTalkConfig } from "./talk.js"; import type { OpenClawConfig } from "./types.js"; import type { ModelDefinitionConfig } from "./types.models.js"; @@ -139,7 +141,10 @@ export function applyModelDefaults(cfg: OpenClawConfig): OpenClawConfig { if (providerConfig) { const nextProviders = { ...providerConfig }; for (const [providerId, provider] of Object.entries(providerConfig)) { - const normalizedProvider = normalizeProviderSpecificConfig(providerId, provider); + const normalizedProvider = normalizeProviderConfigForConfigDefaults({ + provider: providerId, + providerConfig: provider, + }); const models = normalizedProvider.models; if (!Array.isArray(models) || models.length === 0) { if (normalizedProvider !== provider) { @@ -339,13 +344,10 @@ export function applyContextPruningDefaults(cfg: OpenClawConfig): OpenClawConfig return cfg; } return ( - applyProviderConfigDefaultsWithPlugin({ + applyProviderConfigDefaultsForConfig({ provider: "anthropic", - context: { - provider: "anthropic", - config: cfg, - env: process.env, - }, + config: cfg, + env: process.env, }) ?? cfg ); } diff --git a/src/config/model-alias-defaults.test.ts b/src/config/model-alias-defaults.test.ts index df26f496e5f..666720a8638 100644 --- a/src/config/model-alias-defaults.test.ts +++ b/src/config/model-alias-defaults.test.ts @@ -1,21 +1,8 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js"; import { applyModelDefaults } from "./defaults.js"; import type { OpenClawConfig } from "./types.js"; -const { normalizeProviderSpecificConfigMock } = vi.hoisted(() => ({ - normalizeProviderSpecificConfigMock: vi.fn((providerKey: string, provider: unknown) => { - if (providerKey !== "anthropic" || !provider || typeof provider !== "object") { - return provider; - } - return { ...(provider as Record), api: "anthropic-messages" }; - }), -})); - -vi.mock("../agents/models-config.providers.policy.js", () => ({ - normalizeProviderSpecificConfig: normalizeProviderSpecificConfigMock, -})); - describe("applyModelDefaults", () => { function buildProxyProviderConfig(overrides?: { contextWindow?: number; maxTokens?: number }) { return { diff --git a/src/config/provider-policy.ts b/src/config/provider-policy.ts new file mode 100644 index 00000000000..1ff87e00381 --- /dev/null +++ b/src/config/provider-policy.ts @@ -0,0 +1,27 @@ +import { resolveBundledProviderPolicySurface } from "../plugins/provider-public-artifacts.js"; +import type { ModelProviderConfig, OpenClawConfig } from "./types.js"; + +export function normalizeProviderConfigForConfigDefaults(params: { + provider: string; + providerConfig: ModelProviderConfig; +}): ModelProviderConfig { + const normalized = resolveBundledProviderPolicySurface(params.provider)?.normalizeConfig?.({ + provider: params.provider, + providerConfig: params.providerConfig, + }); + return normalized && normalized !== params.providerConfig ? normalized : params.providerConfig; +} + +export function applyProviderConfigDefaultsForConfig(params: { + provider: string; + config: OpenClawConfig; + env: NodeJS.ProcessEnv; +}): OpenClawConfig { + return ( + resolveBundledProviderPolicySurface(params.provider)?.applyConfigDefaults?.({ + provider: params.provider, + config: params.config, + env: params.env, + }) ?? params.config + ); +} diff --git a/src/tasks/task-registry.audit.ts b/src/tasks/task-registry.audit.ts index b309922093f..63ead4fafa0 100644 --- a/src/tasks/task-registry.audit.ts +++ b/src/tasks/task-registry.audit.ts @@ -5,7 +5,6 @@ import { type TaskAuditSeverity, type TaskAuditSummary, } from "./task-registry.audit.shared.js"; -import { reconcileInspectableTasks } from "./task-registry.reconcile.js"; import type { TaskRecord } from "./task-registry.types.js"; export type TaskAuditOptions = { @@ -20,6 +19,12 @@ const DEFAULT_STALE_RUNNING_MS = 30 * 60_000; export { createEmptyTaskAuditSummary }; export type { TaskAuditCode, TaskAuditFinding, TaskAuditSeverity, TaskAuditSummary }; +let taskAuditTaskProvider: () => TaskRecord[] = () => []; + +export function configureTaskAuditTaskProvider(provider: () => TaskRecord[]): void { + taskAuditTaskProvider = provider; +} + function createFinding(params: { severity: TaskAuditSeverity; code: TaskAuditCode; @@ -83,7 +88,7 @@ function compareFindings(left: TaskAuditFinding, right: TaskAuditFinding): numbe } export function listTaskAuditFindings(options: TaskAuditOptions = {}): TaskAuditFinding[] { - const tasks = options.tasks ?? reconcileInspectableTasks(); + const tasks = options.tasks ?? taskAuditTaskProvider(); const now = options.now ?? Date.now(); const staleQueuedMs = options.staleQueuedMs ?? DEFAULT_STALE_QUEUED_MS; const staleRunningMs = options.staleRunningMs ?? DEFAULT_STALE_RUNNING_MS; diff --git a/src/tasks/task-registry.maintenance.ts b/src/tasks/task-registry.maintenance.ts index b104bef2167..02bb518c841 100644 --- a/src/tasks/task-registry.maintenance.ts +++ b/src/tasks/task-registry.maintenance.ts @@ -15,7 +15,11 @@ import { resolveTaskForLookupToken, setTaskCleanupAfterById, } from "./runtime-internal.js"; -import { listTaskAuditFindings, summarizeTaskAuditFindings } from "./task-registry.audit.js"; +import { + configureTaskAuditTaskProvider, + listTaskAuditFindings, + summarizeTaskAuditFindings, +} from "./task-registry.audit.js"; import type { TaskAuditSummary } from "./task-registry.audit.js"; import { summarizeTaskRecords } from "./task-registry.summary.js"; import type { TaskRecord, TaskRegistrySummary } from "./task-registry.types.js"; @@ -229,6 +233,8 @@ export function reconcileInspectableTasks(): TaskRecord[] { .map((task) => reconcileTaskRecordForOperatorInspection(task)); } +configureTaskAuditTaskProvider(reconcileInspectableTasks); + export function getInspectableTaskRegistrySummary(): TaskRegistrySummary { return summarizeTaskRecords(reconcileInspectableTasks()); }