refactor: extract web content core package (#88346)

Extract web-content shared runtime helpers into packages/web-content-core, move the focused tests with the new package, and split quiet CI shards so the node matrix no longer stalls past the no-output watchdog.\n\nVerification: node scripts/run-vitest.mjs test/scripts/ci-node-test-plan.test.ts test/scripts/run-vitest.test.ts src/infra/restart.test.ts src/infra/os-summary.test.ts src/infra/gateway-processes.test.ts src/infra/inline-option-token.test.ts src/infra/map-size.test.ts src/infra/machine-name.test.ts src/commands/doctor-whatsapp-responsiveness.test.ts; autoreview clean; manual CI https://github.com/openclaw/openclaw/actions/runs/26693962844; dependency guard https://github.com/openclaw/openclaw/actions/runs/26693959937. Admin merge used because optional Mantis Telegram Desktop proof was cancelled after blocking merge outside this PR's required proof.
This commit is contained in:
Peter Steinberger
2026-05-30 21:38:29 +01:00
committed by GitHub
parent c6b1fede5a
commit a20b2dc740
29 changed files with 1263 additions and 545 deletions

View File

@@ -0,0 +1,2 @@
import { WebProviderConfigSource, hasWebProviderEntryCredential, providerRequiresCredential, readWebProviderEnvValue, resolveWebProviderConfig, resolveWebProviderDefinition } from "./provider-runtime-shared.mjs";
export { WebProviderConfigSource, hasWebProviderEntryCredential, providerRequiresCredential, readWebProviderEnvValue, resolveWebProviderConfig, resolveWebProviderDefinition };

View File

@@ -0,0 +1,2 @@
import { hasWebProviderEntryCredential, providerRequiresCredential, readWebProviderEnvValue, resolveWebProviderConfig, resolveWebProviderDefinition } from "./provider-runtime-shared.mjs";
export { hasWebProviderEntryCredential, providerRequiresCredential, readWebProviderEnvValue, resolveWebProviderConfig, resolveWebProviderDefinition };

View File

@@ -0,0 +1,78 @@
//#region packages/web-content-core/src/provider-runtime-shared.d.ts
type WebProviderConfigSource = {
tools?: {
web?: {
search?: unknown;
fetch?: unknown;
};
};
};
type RuntimeWebProviderMetadata = {
providerConfigured?: string;
selectedProvider?: string;
};
type ProviderWithCredential = {
envVars: string[];
authProviderId?: string;
requiresCredential?: boolean;
};
type WebContentProcessEnv = Record<string, string | undefined>;
declare function resolveWebProviderConfig(cfg: WebProviderConfigSource | undefined, kind: "search" | "fetch"): Record<string, unknown> | undefined;
declare function readWebProviderEnvValue(envVars: string[], processEnv?: WebContentProcessEnv): string | undefined;
declare function providerRequiresCredential(provider: Pick<ProviderWithCredential, "requiresCredential">): boolean;
declare function hasWebProviderEntryCredential<TProvider extends ProviderWithCredential, TConfigSource extends WebProviderConfigSource, TConfig extends Record<string, unknown> | undefined>(params: {
provider: TProvider;
config: TConfigSource | undefined;
toolConfig: TConfig;
resolveRawValue: (params: {
provider: TProvider;
config: TConfigSource | undefined;
toolConfig: TConfig;
}) => unknown;
resolveFallbackRawValue?: (params: {
provider: TProvider;
config: TConfigSource | undefined;
toolConfig: TConfig;
}) => unknown;
resolveEnvValue: (params: {
provider: TProvider;
configuredEnvVarId?: string;
}) => string | undefined;
resolveProviderAuthValue?: (providerId: string) => boolean;
}): boolean;
declare function resolveWebProviderDefinition<TProvider extends {
id: string;
}, TConfigSource extends WebProviderConfigSource, TConfig extends Record<string, unknown> | undefined, TRuntimeMetadata extends RuntimeWebProviderMetadata, TDefinition>(params: {
config: TConfigSource | undefined;
toolConfig: TConfig;
runtimeMetadata: TRuntimeMetadata | undefined;
sandboxed?: boolean;
providerId?: string;
providers: TProvider[];
resolveEnabled: (params: {
toolConfig: TConfig;
sandboxed?: boolean;
}) => boolean;
resolveAutoProviderId: (params: {
config: TConfigSource | undefined;
toolConfig: TConfig;
providers: TProvider[];
}) => string;
resolveFallbackProviderId?: (params: {
config: TConfigSource | undefined;
toolConfig: TConfig;
providers: TProvider[];
providerId: string;
}) => string | undefined;
createTool: (params: {
provider: TProvider;
config: TConfigSource | undefined;
toolConfig: TConfig;
runtimeMetadata: TRuntimeMetadata | undefined;
}) => TDefinition | null;
}): {
provider: TProvider;
definition: TDefinition;
} | null;
//#endregion
export { WebProviderConfigSource, hasWebProviderEntryCredential, providerRequiresCredential, readWebProviderEnvValue, resolveWebProviderConfig, resolveWebProviderDefinition };

View File

@@ -0,0 +1,136 @@
//#region packages/web-content-core/src/provider-runtime-shared.ts
const DEFAULT_SECRET_PROVIDER_ALIAS = "default";
const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:";
const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:";
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
const ENV_SECRET_SHORTHAND_RE = /^\$([A-Z][A-Z0-9_]{0,127})$/;
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function normalizeSecretInputString(value) {
if (typeof value !== "string") return;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : void 0;
}
function normalizeSecretInput(value) {
if (typeof value !== "string") return "";
const collapsed = value.replace(/[\r\n\u2028\u2029]+/g, "");
let latin1Only = "";
for (const char of collapsed) {
const codePoint = char.codePointAt(0);
if (typeof codePoint === "number" && codePoint <= 255) latin1Only += char;
}
return latin1Only.trim();
}
function isSecretRef(value) {
if (!isRecord(value)) return false;
if (Object.keys(value).length !== 3) return false;
return (value.source === "env" || value.source === "file" || value.source === "exec") && typeof value.provider === "string" && value.provider.trim().length > 0 && typeof value.id === "string" && value.id.trim().length > 0;
}
function coerceSecretRef(value) {
if (isSecretRef(value)) return value;
if (typeof value === "string") {
const trimmed = value.trim();
const legacyPrefix = trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX) ? LEGACY_SECRETREF_ENV_MARKER_PREFIX : trimmed.startsWith(LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX) ? LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX : void 0;
if (legacyPrefix) {
const id = trimmed.slice(legacyPrefix.length);
return ENV_SECRET_REF_ID_RE.test(id) ? {
source: "env",
provider: DEFAULT_SECRET_PROVIDER_ALIAS,
id
} : null;
}
const match = ENV_SECRET_TEMPLATE_RE.exec(trimmed) ?? ENV_SECRET_SHORTHAND_RE.exec(trimmed);
return match ? {
source: "env",
provider: DEFAULT_SECRET_PROVIDER_ALIAS,
id: match[1]
} : null;
}
if (isRecord(value) && (value.source === "env" || value.source === "file" || value.source === "exec") && typeof value.id === "string" && value.id.trim().length > 0 && value.provider === void 0) return {
source: value.source,
provider: DEFAULT_SECRET_PROVIDER_ALIAS,
id: value.id
};
return null;
}
function resolveWebProviderConfig(cfg, kind) {
const webConfig = cfg?.tools?.web;
if (!webConfig || typeof webConfig !== "object") return;
const toolConfig = webConfig[kind];
if (!toolConfig || typeof toolConfig !== "object") return;
return toolConfig;
}
function readWebProviderEnvValue(envVars, processEnv = process.env) {
for (const envVar of envVars) {
const value = normalizeSecretInput(processEnv[envVar]);
if (value) return value;
}
}
function providerRequiresCredential(provider) {
return provider.requiresCredential !== false;
}
function hasWebProviderEntryCredential(params) {
if (!providerRequiresCredential(params.provider)) return true;
const rawValue = params.resolveRawValue({
provider: params.provider,
config: params.config,
toolConfig: params.toolConfig
});
const configuredRef = coerceSecretRef(rawValue);
if (configuredRef && configuredRef.source !== "env") return true;
if (normalizeSecretInput(normalizeSecretInputString(rawValue))) return true;
if (params.provider.authProviderId && params.resolveProviderAuthValue?.(params.provider.authProviderId)) return true;
if (params.resolveEnvValue({
provider: params.provider,
configuredEnvVarId: configuredRef?.source === "env" ? configuredRef.id : void 0
})) return true;
const fallbackRawValue = params.resolveFallbackRawValue?.({
provider: params.provider,
config: params.config,
toolConfig: params.toolConfig
});
const fallbackRef = coerceSecretRef(fallbackRawValue);
if (fallbackRef && fallbackRef.source !== "env") return true;
if (normalizeSecretInput(normalizeSecretInputString(fallbackRawValue))) return true;
return Boolean(fallbackRef?.source === "env" ? params.resolveEnvValue({
provider: params.provider,
configuredEnvVarId: fallbackRef.id
}) : void 0);
}
function resolveWebProviderDefinition(params) {
if (!params.resolveEnabled({
toolConfig: params.toolConfig,
sandboxed: params.sandboxed
})) return null;
const providers = params.providers.filter(Boolean);
if (providers.length === 0) return null;
const autoProviderId = params.resolveAutoProviderId({
config: params.config,
toolConfig: params.toolConfig,
providers
});
const providerId = params.providerId ?? params.runtimeMetadata?.selectedProvider ?? autoProviderId;
if (!providerId) return null;
const provider = providers.find((entry) => entry.id === providerId) ?? providers.find((entry) => entry.id === params.resolveFallbackProviderId?.({
config: params.config,
toolConfig: params.toolConfig,
providers,
providerId
}));
if (!provider) return null;
const definition = params.createTool({
provider,
config: params.config,
toolConfig: params.toolConfig,
runtimeMetadata: params.runtimeMetadata
});
if (!definition) return null;
return {
provider,
definition
};
}
//#endregion
export { hasWebProviderEntryCredential, providerRequiresCredential, readWebProviderEnvValue, resolveWebProviderConfig, resolveWebProviderDefinition };

View File

@@ -0,0 +1,26 @@
{
"name": "@openclaw/web-content-core",
"version": "0.0.0-private",
"private": true,
"files": [
"dist"
],
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
},
"./provider-runtime-shared": {
"types": "./dist/provider-runtime-shared.d.mts",
"import": "./dist/provider-runtime-shared.mjs",
"default": "./dist/provider-runtime-shared.mjs"
}
},
"scripts": {
"build": "tsdown src/index.ts src/provider-runtime-shared.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
}
}

View File

@@ -0,0 +1 @@
export * from "./provider-runtime-shared.js";

View File

@@ -0,0 +1,117 @@
import { describe, expect, it } from "vitest";
import {
hasWebProviderEntryCredential,
readWebProviderEnvValue,
resolveWebProviderConfig,
resolveWebProviderDefinition,
} from "./provider-runtime-shared.js";
describe("resolveWebProviderConfig", () => {
it("selects the requested web tool config", () => {
const search = { provider: "search-provider" };
expect(
resolveWebProviderConfig(
{
tools: {
web: {
search,
},
},
},
"search",
),
).toBe(search);
});
});
describe("readWebProviderEnvValue", () => {
it("normalizes env credentials before returning them", () => {
expect(readWebProviderEnvValue(["API_KEY"], { API_KEY: " key\r\nvalue🙂 " })).toBe("keyvalue");
});
});
describe("hasWebProviderEntryCredential", () => {
const provider = {
id: "custom",
envVars: ["CUSTOM_API_KEY"],
};
it("treats non-env secret refs as configured credentials", () => {
expect(
hasWebProviderEntryCredential({
provider,
config: {},
toolConfig: undefined,
resolveRawValue: () => ({
source: "file",
provider: "mounted-json",
id: "/custom/apiKey",
}),
resolveEnvValue: () => undefined,
}),
).toBe(true);
});
it("resolves env secret ref ids through the env resolver", () => {
expect(
hasWebProviderEntryCredential({
provider,
config: {},
toolConfig: undefined,
resolveRawValue: () => ({
source: "env",
provider: "default",
id: "CUSTOM_API_KEY",
}),
resolveEnvValue: ({ configuredEnvVarId }) =>
configuredEnvVarId === "CUSTOM_API_KEY" ? "secret" : undefined,
}),
).toBe(true);
});
it("falls back to provider auth before env probing", () => {
expect(
hasWebProviderEntryCredential({
provider: {
...provider,
authProviderId: "custom-auth",
},
config: {},
toolConfig: undefined,
resolveRawValue: () => undefined,
resolveEnvValue: () => undefined,
resolveProviderAuthValue: (providerId) => providerId === "custom-auth",
}),
).toBe(true);
});
});
describe("resolveWebProviderDefinition", () => {
it("falls back to auto-detect when runtime metadata has no selected provider", () => {
const resolved = resolveWebProviderDefinition({
config: {},
toolConfig: { enabled: true },
runtimeMetadata: {},
providers: [
{
id: "custom",
},
],
resolveEnabled: () => true,
resolveAutoProviderId: () => "custom",
createTool: ({ provider }) => ({
name: provider.id,
}),
});
expect(resolved).toEqual({
provider: {
id: "custom",
},
definition: {
name: "custom",
},
});
});
});

View File

@@ -0,0 +1,302 @@
export type WebProviderConfigSource = {
tools?: {
web?: {
search?: unknown;
fetch?: unknown;
};
};
};
type SecretRefSource = "env" | "file" | "exec";
type SecretRef = {
source: SecretRefSource;
provider: string;
id: string;
};
const DEFAULT_SECRET_PROVIDER_ALIAS = "default";
const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:";
const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:";
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
const ENV_SECRET_SHORTHAND_RE = /^\$([A-Z][A-Z0-9_]{0,127})$/;
type RuntimeWebProviderMetadata = {
providerConfigured?: string;
selectedProvider?: string;
};
type ProviderWithCredential = {
envVars: string[];
authProviderId?: string;
requiresCredential?: boolean;
};
type WebContentProcessEnv = Record<string, string | undefined>;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function normalizeSecretInputString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function normalizeSecretInput(value: unknown): string {
if (typeof value !== "string") {
return "";
}
const collapsed = value.replace(/[\r\n\u2028\u2029]+/g, "");
let latin1Only = "";
for (const char of collapsed) {
const codePoint = char.codePointAt(0);
if (typeof codePoint === "number" && codePoint <= 0xff) {
latin1Only += char;
}
}
return latin1Only.trim();
}
function isSecretRef(value: unknown): value is SecretRef {
if (!isRecord(value)) {
return false;
}
if (Object.keys(value).length !== 3) {
return false;
}
return (
(value.source === "env" || value.source === "file" || value.source === "exec") &&
typeof value.provider === "string" &&
value.provider.trim().length > 0 &&
typeof value.id === "string" &&
value.id.trim().length > 0
);
}
function coerceSecretRef(value: unknown): SecretRef | null {
if (isSecretRef(value)) {
return value;
}
if (typeof value === "string") {
const trimmed = value.trim();
const legacyPrefix = trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX)
? LEGACY_SECRETREF_ENV_MARKER_PREFIX
: trimmed.startsWith(LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX)
? LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX
: undefined;
if (legacyPrefix) {
const id = trimmed.slice(legacyPrefix.length);
return ENV_SECRET_REF_ID_RE.test(id)
? { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id }
: null;
}
const match = ENV_SECRET_TEMPLATE_RE.exec(trimmed) ?? ENV_SECRET_SHORTHAND_RE.exec(trimmed);
return match ? { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id: match[1] } : null;
}
if (
isRecord(value) &&
(value.source === "env" || value.source === "file" || value.source === "exec") &&
typeof value.id === "string" &&
value.id.trim().length > 0 &&
value.provider === undefined
) {
return {
source: value.source,
provider: DEFAULT_SECRET_PROVIDER_ALIAS,
id: value.id,
};
}
return null;
}
export function resolveWebProviderConfig(
cfg: WebProviderConfigSource | undefined,
kind: "search" | "fetch",
): Record<string, unknown> | undefined {
const webConfig = cfg?.tools?.web;
if (!webConfig || typeof webConfig !== "object") {
return undefined;
}
const toolConfig = webConfig[kind];
if (!toolConfig || typeof toolConfig !== "object") {
return undefined;
}
return toolConfig as Record<string, unknown>;
}
export function readWebProviderEnvValue(
envVars: string[],
processEnv: WebContentProcessEnv = process.env,
): string | undefined {
for (const envVar of envVars) {
const value = normalizeSecretInput(processEnv[envVar]);
if (value) {
return value;
}
}
return undefined;
}
export function providerRequiresCredential(
provider: Pick<ProviderWithCredential, "requiresCredential">,
): boolean {
return provider.requiresCredential !== false;
}
export function hasWebProviderEntryCredential<
TProvider extends ProviderWithCredential,
TConfigSource extends WebProviderConfigSource,
TConfig extends Record<string, unknown> | undefined,
>(params: {
provider: TProvider;
config: TConfigSource | undefined;
toolConfig: TConfig;
resolveRawValue: (params: {
provider: TProvider;
config: TConfigSource | undefined;
toolConfig: TConfig;
}) => unknown;
resolveFallbackRawValue?: (params: {
provider: TProvider;
config: TConfigSource | undefined;
toolConfig: TConfig;
}) => unknown;
resolveEnvValue: (params: {
provider: TProvider;
configuredEnvVarId?: string;
}) => string | undefined;
resolveProviderAuthValue?: (providerId: string) => boolean;
}): boolean {
if (!providerRequiresCredential(params.provider)) {
return true;
}
const rawValue = params.resolveRawValue({
provider: params.provider,
config: params.config,
toolConfig: params.toolConfig,
});
const configuredRef = coerceSecretRef(rawValue);
if (configuredRef && configuredRef.source !== "env") {
return true;
}
const fromConfig = normalizeSecretInput(normalizeSecretInputString(rawValue));
if (fromConfig) {
return true;
}
if (
params.provider.authProviderId &&
params.resolveProviderAuthValue?.(params.provider.authProviderId)
) {
return true;
}
if (
params.resolveEnvValue({
provider: params.provider,
configuredEnvVarId: configuredRef?.source === "env" ? configuredRef.id : undefined,
})
) {
return true;
}
const fallbackRawValue = params.resolveFallbackRawValue?.({
provider: params.provider,
config: params.config,
toolConfig: params.toolConfig,
});
const fallbackRef = coerceSecretRef(fallbackRawValue);
if (fallbackRef && fallbackRef.source !== "env") {
return true;
}
const fallbackConfig = normalizeSecretInput(normalizeSecretInputString(fallbackRawValue));
if (fallbackConfig) {
return true;
}
return Boolean(
fallbackRef?.source === "env"
? params.resolveEnvValue({
provider: params.provider,
configuredEnvVarId: fallbackRef.id,
})
: undefined,
);
}
export function resolveWebProviderDefinition<
TProvider extends { id: string },
TConfigSource extends WebProviderConfigSource,
TConfig extends Record<string, unknown> | undefined,
TRuntimeMetadata extends RuntimeWebProviderMetadata,
TDefinition,
>(params: {
config: TConfigSource | undefined;
toolConfig: TConfig;
runtimeMetadata: TRuntimeMetadata | undefined;
sandboxed?: boolean;
providerId?: string;
providers: TProvider[];
resolveEnabled: (params: { toolConfig: TConfig; sandboxed?: boolean }) => boolean;
resolveAutoProviderId: (params: {
config: TConfigSource | undefined;
toolConfig: TConfig;
providers: TProvider[];
}) => string;
resolveFallbackProviderId?: (params: {
config: TConfigSource | undefined;
toolConfig: TConfig;
providers: TProvider[];
providerId: string;
}) => string | undefined;
createTool: (params: {
provider: TProvider;
config: TConfigSource | undefined;
toolConfig: TConfig;
runtimeMetadata: TRuntimeMetadata | undefined;
}) => TDefinition | null;
}): { provider: TProvider; definition: TDefinition } | null {
if (!params.resolveEnabled({ toolConfig: params.toolConfig, sandboxed: params.sandboxed })) {
return null;
}
const providers = params.providers.filter(Boolean);
if (providers.length === 0) {
return null;
}
const autoProviderId = params.resolveAutoProviderId({
config: params.config,
toolConfig: params.toolConfig,
providers,
});
const providerId =
params.providerId ?? params.runtimeMetadata?.selectedProvider ?? autoProviderId;
if (!providerId) {
return null;
}
const provider =
providers.find((entry) => entry.id === providerId) ??
providers.find(
(entry) =>
entry.id ===
params.resolveFallbackProviderId?.({
config: params.config,
toolConfig: params.toolConfig,
providers,
providerId,
}),
);
if (!provider) {
return null;
}
const definition = params.createTool({
provider,
config: params.config,
toolConfig: params.toolConfig,
runtimeMetadata: params.runtimeMetadata,
});
if (!definition) {
return null;
}
return { provider, definition };
}

2
pnpm-lock.yaml generated
View File

@@ -1853,6 +1853,8 @@ importers:
specifier: 5.6.2
version: 5.6.2
packages/web-content-core: {}
ui:
dependencies:
'@create-markdown/preview':

View File

@@ -51,6 +51,7 @@ export const BUILD_ALL_STEPS = [
"packages/media-understanding-common/package.json",
"packages/terminal-core/package.json",
"packages/model-catalog-core/package.json",
"packages/web-content-core/package.json",
"packages/memory-host-sdk/package.json",
"tsconfig.json",
"tsconfig.plugin-sdk.dts.json",
@@ -62,6 +63,7 @@ export const BUILD_ALL_STEPS = [
"packages/media-generation-core/src",
"packages/media-understanding-common/src",
"packages/terminal-core/src",
"packages/web-content-core/src",
"src/types",
"src/video-generation/dashscope-compatible.ts",
"src/video-generation/types.ts",

View File

@@ -67,38 +67,55 @@ function resolveCommandShardName(file) {
if (name.startsWith("agent") || name.startsWith("channel") || name === "message.test.ts") {
return "agentic-commands-agent-channel";
}
if (name.startsWith("oauth-tls-preflight.doctor")) {
return "agentic-commands-doctor-auth";
}
if (name.startsWith("doctor")) {
if (name.startsWith("doctor/shared/") || name.startsWith("doctor/")) {
return "agentic-commands-doctor-shared";
}
if (name.startsWith("doctor-auth") || name.startsWith("doctor-claude")) {
if (name.startsWith("doctor-auth")) {
return "agentic-commands-doctor-auth";
}
if (name.startsWith("doctor-config") || name.startsWith("doctor-legacy-config")) {
return "agentic-commands-doctor-config";
}
if (name.startsWith("doctor-cron")) {
return "agentic-commands-doctor-cron";
if (
name.startsWith("doctor-config") ||
name.startsWith("doctor-legacy-config") ||
name.startsWith("doctor-state")
) {
return "agentic-commands-doctor-config-state";
}
if (
name.startsWith("doctor-gateway") ||
name.startsWith("doctor-cron") ||
name.startsWith("doctor-heartbeat") ||
name.startsWith("doctor-memory") ||
name.startsWith("doctor-plugin") ||
name.startsWith("doctor-session") ||
name.startsWith("doctor-state") ||
name.startsWith("doctor-workspace")
name.startsWith("doctor-session")
) {
return "agentic-commands-doctor-runtime";
return "agentic-commands-doctor-sessions-cron";
}
if (name.startsWith("doctor-gateway")) {
return "agentic-commands-doctor-gateway";
}
if (name.startsWith("doctor-device")) {
return "agentic-commands-doctor-device";
}
if (name.startsWith("doctor-platform")) {
return "agentic-commands-doctor-platform";
}
if (name.startsWith("doctor-whatsapp")) {
return "agentic-commands-doctor-whatsapp";
}
if (name.startsWith("doctor-workspace")) {
return "agentic-commands-doctor-workspace";
}
if (
name.startsWith("doctor-platform-notes") ||
name.startsWith("doctor-sandbox") ||
name.startsWith("doctor-whatsapp")
name.startsWith("doctor-browser") ||
name.startsWith("doctor-plugin") ||
name.startsWith("doctor-skill") ||
name.startsWith("doctor-memory") ||
name.startsWith("doctor-claude")
) {
return "agentic-commands-doctor-misc-platform";
return "agentic-commands-doctor-plugins-tools";
}
return "agentic-commands-doctor-misc-core";
return "agentic-commands-doctor";
}
if (
name.startsWith("auth-choice") ||
@@ -131,13 +148,17 @@ function createAgenticCommandSplitShards() {
return [
"agentic-commands-agent-channel",
"agentic-commands-doctor",
"agentic-commands-doctor-auth",
"agentic-commands-doctor-config",
"agentic-commands-doctor-cron",
"agentic-commands-doctor-misc-core",
"agentic-commands-doctor-misc-platform",
"agentic-commands-doctor-runtime",
"agentic-commands-doctor-config-state",
"agentic-commands-doctor-device",
"agentic-commands-doctor-gateway",
"agentic-commands-doctor-platform",
"agentic-commands-doctor-plugins-tools",
"agentic-commands-doctor-sessions-cron",
"agentic-commands-doctor-shared",
"agentic-commands-doctor-whatsapp",
"agentic-commands-doctor-workspace",
"agentic-commands-models",
"agentic-commands-onboard-config",
"agentic-commands-status-tools",
@@ -151,117 +172,6 @@ function createAgenticCommandSplitShards() {
.filter((shard) => shard.includePatterns.length > 0);
}
function resolveInfraStateShardName(file) {
const name = relative("src/infra", file).replaceAll("\\", "/");
if (name.startsWith("approval")) {
return "core-runtime-infra-approval";
}
if (name.startsWith("exec") || name.startsWith("system-run")) {
return "core-runtime-infra-exec";
}
if (name.startsWith("heartbeat")) {
return "core-runtime-infra-heartbeat";
}
if (name.startsWith("outbound/")) {
return "core-runtime-infra-outbound";
}
if (name.startsWith("net/") || name.startsWith("fetch")) {
return "core-runtime-infra-network";
}
if (
name.startsWith("device") ||
name.startsWith("node-pairing") ||
name.startsWith("pairing") ||
name.startsWith("push")
) {
return "core-runtime-infra-device-push";
}
if (
name.startsWith("install") ||
name.startsWith("npm") ||
name.startsWith("package") ||
name.startsWith("provider-usage") ||
name.startsWith("update")
) {
return "core-runtime-infra-package-provider";
}
if (name.startsWith("session") || name.startsWith("state-migrations")) {
return "core-runtime-infra-session-state";
}
if (name < "g") {
return "core-runtime-infra-misc-a-f";
}
if (name.startsWith("gateway")) {
return "core-runtime-infra-misc-gateway";
}
if (name < "m") {
return "core-runtime-infra-misc-g-l";
}
if (name < "p") {
return "core-runtime-infra-misc-m-o";
}
if (
name.startsWith("parse") ||
name.startsWith("path") ||
name.startsWith("plain") ||
name.startsWith("plugin") ||
name.startsWith("ports") ||
name.startsWith("prototype")
) {
return "core-runtime-infra-misc-path-ports";
}
if (
name.startsWith("process") ||
name.startsWith("replace") ||
name.startsWith("resolve") ||
name.startsWith("restart") ||
name.startsWith("retry") ||
name.startsWith("run") ||
name.startsWith("runtime")
) {
return "core-runtime-infra-misc-process-restart";
}
if (name < "t") {
return "core-runtime-infra-misc-s-system";
}
return "core-runtime-infra-misc-t-z";
}
function createInfraStateSplitShards() {
const groups = new Map();
for (const file of listTestFiles("src/infra")) {
const shardName = resolveInfraStateShardName(file);
groups.set(shardName, [...(groups.get(shardName) ?? []), file]);
}
return [
"core-runtime-infra-approval",
"core-runtime-infra-device-push",
"core-runtime-infra-exec",
"core-runtime-infra-heartbeat",
"core-runtime-infra-misc-a-f",
"core-runtime-infra-misc-g-l",
"core-runtime-infra-misc-gateway",
"core-runtime-infra-misc-m-o",
"core-runtime-infra-misc-path-ports",
"core-runtime-infra-misc-process-restart",
"core-runtime-infra-misc-s-system",
"core-runtime-infra-misc-t-z",
"core-runtime-infra-network",
"core-runtime-infra-outbound",
"core-runtime-infra-package-provider",
"core-runtime-infra-session-state",
]
.map((shardName) => ({
configs: ["test/vitest/vitest.infra.config.ts"],
includePatterns: groups.get(shardName) ?? [],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName,
}))
.filter((shard) => shard.includePatterns.length > 0);
}
const GATEWAY_SERVER_BACKED_HTTP_TESTS = new Set([
"src/gateway/embeddings-http.test.ts",
"src/gateway/models-http.test.ts",
@@ -387,6 +297,250 @@ function createCronSplitShards() {
.filter((shard) => shard.includePatterns.length > 0);
}
function resolveInfraShardName(file) {
const name = relative("src/infra", file).replaceAll("\\", "/");
if (name.startsWith("approval") || name.startsWith("exec")) {
return "core-runtime-infra-approval-exec";
}
if (name.startsWith("heartbeat-runner")) {
return "core-runtime-infra-heartbeat-runner";
}
if (name.startsWith("heartbeat")) {
return "core-runtime-infra-heartbeat-core";
}
if (name.startsWith("outbound/message-action")) {
return "core-runtime-infra-outbound-actions";
}
if (name.startsWith("outbound/")) {
return "core-runtime-infra-outbound-core";
}
if (
name.startsWith("net/") ||
name.startsWith("install") ||
name.startsWith("npm") ||
name.startsWith("brew") ||
name.startsWith("binaries")
) {
return "core-runtime-infra-net-install";
}
if (name.startsWith("device")) {
return "core-runtime-infra-device";
}
if (name.startsWith("gateway-lock") || name.startsWith("gateway-process-argv")) {
return "core-runtime-infra-gateway-lock-argv";
}
if (name.startsWith("gateway-processes")) {
return "core-runtime-infra-gateway-processes";
}
if (name.startsWith("gateway-watch")) {
return "core-runtime-infra-gateway-watch";
}
if (name.startsWith("node") || name.startsWith("bonjour") || name.startsWith("network")) {
return "core-runtime-infra-network-node";
}
if (
name.startsWith("archive") ||
name.startsWith("backup") ||
name.startsWith("diagnostic") ||
name.startsWith("diagnostics")
) {
return "core-runtime-infra-diagnostics-state";
}
if (
name.startsWith("command-analysis/") ||
name.startsWith("command-explainer/") ||
name.startsWith("file-") ||
name.startsWith("fs-") ||
name.startsWith("json") ||
name.startsWith("path") ||
name.startsWith("shell") ||
name.startsWith("tmp-openclaw-dir")
) {
return "core-runtime-infra-files-commands";
}
if (name.startsWith("provider-usage") || name.startsWith("push-")) {
return "core-runtime-infra-provider-push";
}
if (
name.startsWith("kysely") ||
name.startsWith("session") ||
name.startsWith("sqlite") ||
name.startsWith("stale-lock") ||
name.startsWith("state-migrations")
) {
return "core-runtime-infra-storage-state";
}
if (
name.startsWith("channel") ||
name.startsWith("plugin") ||
name.startsWith("pairing") ||
name.startsWith("voicewake")
) {
return "core-runtime-infra-channel-plugin";
}
if (
name.startsWith("package") ||
name.startsWith("ports") ||
name.startsWith("process") ||
name.startsWith("restart") ||
name.startsWith("runtime") ||
name.startsWith("run-node") ||
name.startsWith("system") ||
name.startsWith("update")
) {
return "core-runtime-infra-system-runtime";
}
if (
name.startsWith("dotenv") ||
name.startsWith("env") ||
name.startsWith("gemini-auth") ||
name.startsWith("google-api") ||
name.startsWith("home-dir") ||
name.startsWith("host-env") ||
name.startsWith("openclaw-exec-env") ||
name.startsWith("secret") ||
name.startsWith("secure-random")
) {
return "core-runtime-infra-env-auth";
}
if (
name.startsWith("build-stamp") ||
name.startsWith("changelog") ||
name.startsWith("clawhub") ||
name.startsWith("detect-package-manager") ||
name.startsWith("git-") ||
name.startsWith("openclaw-root") ||
name.startsWith("tsdown") ||
name.startsWith("vitest")
) {
return "core-runtime-infra-repo-tooling";
}
if (
name.startsWith("scp") ||
name.startsWith("ssh") ||
name.startsWith("tailnet") ||
name.startsWith("tailscale") ||
name.startsWith("tcp") ||
name.startsWith("tls/") ||
name.startsWith("transport") ||
name.startsWith("widearea") ||
name.startsWith("windows") ||
name.startsWith("ws") ||
name.startsWith("wsl")
) {
return "core-runtime-infra-network-platform";
}
if (
name.startsWith("abort") ||
name.startsWith("backoff") ||
name.startsWith("errors") ||
name.startsWith("fatal-error") ||
name.startsWith("fetch") ||
name.startsWith("fixed-window") ||
name.startsWith("format-time/") ||
name.startsWith("http-body") ||
name.startsWith("parse-finite-number") ||
name.startsWith("plain-object") ||
name.startsWith("prototype-keys") ||
name.startsWith("retry") ||
name.startsWith("warning-filter")
) {
return "core-runtime-infra-core-utils";
}
if (
name.startsWith("browser") ||
name.startsWith("cli-") ||
name.startsWith("clipboard") ||
name.startsWith("control-ui") ||
name.startsWith("embedded") ||
name.startsWith("is-main")
) {
return "core-runtime-infra-cli-ui";
}
if (
name.startsWith("agent-events") ||
name.startsWith("event-session") ||
name.startsWith("infra-") ||
name.startsWith("non-fatal") ||
name.startsWith("supervisor") ||
name.startsWith("unhandled")
) {
return "core-runtime-infra-events-runtime";
}
if (
name.startsWith("boundary") ||
name.startsWith("hardlink") ||
name.startsWith("replace-file") ||
name.startsWith("resolve-system-bin") ||
name.startsWith("safe-package-install") ||
name.startsWith("stable-node-path") ||
name.startsWith("watch-node")
) {
return "core-runtime-infra-file-safety";
}
if (name.startsWith("dedupe") || name.startsWith("disk-space")) {
return "core-runtime-infra-misc-dedupe-disk";
}
if (
name.startsWith("inline-option-token") ||
name.startsWith("map-size") ||
name.startsWith("machine-name")
) {
return "core-runtime-infra-misc-values";
}
if (name.startsWith("os-summary")) {
return "core-runtime-infra-misc-os";
}
return "core-runtime-infra-misc";
}
function createInfraSplitShards() {
const groups = new Map();
for (const file of listTestFiles("src/infra")) {
const shardName = resolveInfraShardName(file);
groups.set(shardName, [...(groups.get(shardName) ?? []), file]);
}
return [
"core-runtime-infra-approval-exec",
"core-runtime-infra-channel-plugin",
"core-runtime-infra-cli-ui",
"core-runtime-infra-device",
"core-runtime-infra-diagnostics-state",
"core-runtime-infra-core-utils",
"core-runtime-infra-env-auth",
"core-runtime-infra-events-runtime",
"core-runtime-infra-file-safety",
"core-runtime-infra-files-commands",
"core-runtime-infra-gateway-lock-argv",
"core-runtime-infra-gateway-processes",
"core-runtime-infra-gateway-watch",
"core-runtime-infra-heartbeat-core",
"core-runtime-infra-heartbeat-runner",
"core-runtime-infra-misc",
"core-runtime-infra-misc-dedupe-disk",
"core-runtime-infra-misc-os",
"core-runtime-infra-misc-values",
"core-runtime-infra-net-install",
"core-runtime-infra-network-node",
"core-runtime-infra-network-platform",
"core-runtime-infra-outbound-actions",
"core-runtime-infra-outbound-core",
"core-runtime-infra-provider-push",
"core-runtime-infra-repo-tooling",
"core-runtime-infra-storage-state",
"core-runtime-infra-system-runtime",
]
.map((shardName) => ({
configs: ["test/vitest/vitest.infra.config.ts"],
includePatterns: groups.get(shardName) ?? [],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName,
}))
.filter((shard) => shard.includePatterns.length > 0);
}
const SPLIT_NODE_SHARDS = new Map([
[
"core-unit-fast",
@@ -429,13 +583,13 @@ const SPLIT_NODE_SHARDS = new Map([
[
"core-runtime",
[
...createInfraStateSplitShards(),
{
shardName: "core-runtime-hooks",
configs: ["test/vitest/vitest.hooks.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
},
...createInfraSplitShards(),
{
shardName: "core-runtime-secrets",
configs: ["test/vitest/vitest.secrets.config.ts"],

View File

@@ -14,6 +14,7 @@ const RUN_NODE_PACKAGE_SOURCE_ROOTS = [
"packages/media-generation-core/src",
"packages/media-understanding-common/src",
"packages/terminal-core/src",
"packages/web-content-core/src",
"packages/net-policy/src",
];

View File

@@ -17,8 +17,8 @@ const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
const ANSI_CSI_PREFIX = `${String.fromCharCode(27)}[`;
const ANSI_CSI_SUFFIX_RE = /^[0-?]*[ -/]*[@-~]/u;
const SUPPRESSED_VITEST_STDERR_PATTERNS = ["[PLUGIN_TIMINGS]"];
export const DEFAULT_VITEST_NO_OUTPUT_TIMEOUT_MS = 300_000;
export const DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS = 120_000;
export const DEFAULT_VITEST_NO_OUTPUT_TIMEOUT_MS = 120_000;
export const DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS = 60_000;
const VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS";
const VITEST_NO_OUTPUT_HEARTBEAT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS";
const UI_VITEST_CONFIG = "test/vitest/vitest.ui.config.ts";

View File

@@ -2357,6 +2357,9 @@ export function applyDefaultVitestNoOutputTimeout(specs, params = {}) {
export function shouldRetryVitestNoOutputTimeout(env = process.env) {
const value = env[VITEST_NO_OUTPUT_RETRY_ENV_KEY]?.trim().toLowerCase();
if (value === undefined && isCiLikeEnv(env)) {
return false;
}
return !["0", "false", "no", "off"].includes(value ?? "");
}

View File

@@ -89,7 +89,7 @@ describe("image resize utility", () => {
maxHeight: 2_000,
maxWidth: 2_000,
},
maxBase64Bytes: 4_000,
maxBytes: 3_000,
opaque: { format: "jpeg", quality: 70 },
search: {
compressionLevel: [6, 9],

View File

@@ -32,6 +32,10 @@ const DEFAULT_OPTIONS: Required<ImageResizeOptions> = {
jpegQuality: 80,
};
function maxBinaryBytesForBase64Budget(maxBase64Bytes: number): number {
return Math.floor(maxBase64Bytes / 4) * 3;
}
interface EncodedCandidate {
data: string;
encodedSize: number;
@@ -107,7 +111,7 @@ export async function resizeImage(
maxWidth: opts.maxWidth,
maxHeight: opts.maxHeight,
},
maxBase64Bytes: opts.maxBytes,
maxBytes: maxBinaryBytesForBase64Budget(opts.maxBytes),
opaque: { format: "jpeg", quality: opts.jpegQuality },
transparent: { format: "png" },
search: {

View File

@@ -400,6 +400,22 @@ function storeHasUnsafeUntouchedHydratedSkillPrompts(
if (!ref || !isSessionSkillPromptBlobReadable(storePath, ref)) {
return true;
}
if (serializedPromptRefs?.has(key)) {
const projected = projectSessionStoreForPersistence({ storePath, store: { [key]: entry } });
for (const blob of projected.promptBlobs.values()) {
if (!blob.path) {
continue;
}
try {
const stat = fs.statSync(blob.path);
if (!stat.isFile() || stat.size !== blob.ref.bytes) {
return true;
}
} catch {
return true;
}
}
}
}
return false;
}

View File

@@ -8,23 +8,16 @@ const parseProcCmdlineMock = vi.hoisted(() => vi.fn());
const isGatewayArgvMock = vi.hoisted(() => vi.fn());
const findGatewayPidsOnPortSyncMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", async (importOriginal) => ({
...(await importOriginal<typeof import("node:child_process")>()),
vi.mock("node:child_process", () => ({
spawnSync: spawnSyncMock,
}));
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
const actualWithDefault = actual as typeof actual & { default?: typeof actual };
return {
...actual,
default: {
...(actualWithDefault.default ?? actual),
readFileSync: (...args: unknown[]) => readFileSyncMock(...args),
},
vi.mock("node:fs", () => ({
default: {
readFileSync: (...args: unknown[]) => readFileSyncMock(...args),
};
});
},
readFileSync: (...args: unknown[]) => readFileSyncMock(...args),
}));
vi.mock("../daemon/cmd-argv.js", () => ({
parseCmdScriptCommandLine: (...args: unknown[]) => parseCmdScriptCommandLineMock(...args),

View File

@@ -7,7 +7,8 @@ const execFileMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", () => ({
execFile: Object.assign(execFileMock, {
[Symbol.for("nodejs.util.promisify.custom")]: vi.fn(),
}),
__promisify__: vi.fn(),
}) as typeof import("node:child_process").execFile,
}));
const originalVitest = process.env.VITEST;

View File

@@ -3,8 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
const spawnSyncMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", async (importOriginal) => ({
...(await importOriginal<typeof import("node:child_process")>()),
vi.mock("node:child_process", () => ({
spawnSync: spawnSyncMock,
}));

View File

@@ -3,28 +3,28 @@ import { captureFullEnv } from "../test-utils/env.js";
import { mockProcessPlatform } from "../test-utils/vitest-spies.js";
const spawnSyncMock = vi.hoisted(() => vi.fn());
const execFileMock = vi.hoisted(() =>
Object.assign(vi.fn(), {
[Symbol.for("nodejs.util.promisify.custom")]: vi.fn(),
}),
);
const resolveLsofCommandSyncMock = vi.hoisted(() => vi.fn());
const resolveGatewayPortMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
return {
...actual,
spawnSync: (...args: unknown[]) => spawnSyncMock(...args),
};
});
vi.mock("node:child_process", () => ({
execFile: execFileMock,
spawnSync: (...args: unknown[]) => spawnSyncMock(...args),
}));
vi.mock("./ports-lsof.js", () => ({
resolveLsofCommandSync: (...args: unknown[]) => resolveLsofCommandSyncMock(...args),
}));
vi.mock("../config/paths.js", async () => {
const actual = await vi.importActual<typeof import("../config/paths.js")>("../config/paths.js");
return {
...actual,
resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args),
};
});
vi.mock("../config/paths.js", () => ({
resolveGatewayPort: (...args: unknown[]) => resolveGatewayPortMock(...args),
resolveStateDir: (env: NodeJS.ProcessEnv = process.env) =>
env.OPENCLAW_STATE_DIR ?? "/tmp/openclaw-state",
}));
const { testing, cleanStaleGatewayProcessesSync, findGatewayPidsOnPortSync } =
await import("./restart-stale-pids.js");
@@ -34,6 +34,7 @@ let currentTimeMs = 0;
const envSnapshot = captureFullEnv();
beforeEach(() => {
execFileMock.mockReset();
spawnSyncMock.mockReset();
resolveLsofCommandSyncMock.mockReset();
resolveGatewayPortMock.mockReset();

View File

@@ -512,7 +512,10 @@ export const PLUGIN_COMPAT_RECORDS = [
docsPath: "/plugins/sdk-agent-harness",
surfaces: ["manifest/catalog execution policy", "runtime selection"],
diagnostics: ["agent runtime compatibility warning"],
tests: ["src/plugins/provider-runtime.test.ts", "src/web/provider-runtime-shared.test.ts"],
tests: [
"src/plugins/provider-runtime.test.ts",
"packages/web-content-core/src/provider-runtime-shared.test.ts",
],
},
{
code: "generated-bundled-channel-config-fallback",

View File

@@ -1,31 +0,0 @@
import { describe, expect, it } from "vitest";
import { resolveWebProviderDefinition } from "./provider-runtime-shared.js";
describe("resolveWebProviderDefinition", () => {
it("falls back to auto-detect when runtime metadata has no selected provider", () => {
const resolved = resolveWebProviderDefinition({
config: {},
toolConfig: { enabled: true },
runtimeMetadata: {},
providers: [
{
id: "custom",
},
],
resolveEnabled: () => true,
resolveAutoProviderId: () => "custom",
createTool: ({ provider }) => ({
name: provider.id,
}),
});
expect(resolved).toEqual({
provider: {
id: "custom",
},
definition: {
name: "custom",
},
});
});
});

View File

@@ -1,201 +1 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeSecretInputString, resolveSecretInputRef } from "../config/types.secrets.js";
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
type RuntimeWebProviderMetadata = {
providerConfigured?: string;
selectedProvider?: string;
};
type ProviderWithCredential = {
envVars: string[];
authProviderId?: string;
requiresCredential?: boolean;
};
export function resolveWebProviderConfig(
cfg: OpenClawConfig | undefined,
kind: "search" | "fetch",
): Record<string, unknown> | undefined {
const webConfig = cfg?.tools?.web;
if (!webConfig || typeof webConfig !== "object") {
return undefined;
}
const toolConfig = webConfig[kind];
if (!toolConfig || typeof toolConfig !== "object") {
return undefined;
}
return toolConfig as Record<string, unknown>;
}
export function readWebProviderEnvValue(
envVars: string[],
processEnv: NodeJS.ProcessEnv = process.env,
): string | undefined {
for (const envVar of envVars) {
const value = normalizeSecretInput(processEnv[envVar]);
if (value) {
return value;
}
}
return undefined;
}
export function providerRequiresCredential(
provider: Pick<ProviderWithCredential, "requiresCredential">,
): boolean {
return provider.requiresCredential !== false;
}
export function hasWebProviderEntryCredential<
TProvider extends ProviderWithCredential,
TConfig extends Record<string, unknown> | undefined,
>(params: {
provider: TProvider;
config: OpenClawConfig | undefined;
toolConfig: TConfig;
resolveRawValue: (params: {
provider: TProvider;
config: OpenClawConfig | undefined;
toolConfig: TConfig;
}) => unknown;
resolveFallbackRawValue?: (params: {
provider: TProvider;
config: OpenClawConfig | undefined;
toolConfig: TConfig;
}) => unknown;
resolveEnvValue: (params: {
provider: TProvider;
configuredEnvVarId?: string;
}) => string | undefined;
resolveProviderAuthValue?: (providerId: string) => boolean;
}): boolean {
if (!providerRequiresCredential(params.provider)) {
return true;
}
const rawValue = params.resolveRawValue({
provider: params.provider,
config: params.config,
toolConfig: params.toolConfig,
});
const configuredRef = resolveSecretInputRef({
value: rawValue,
}).ref;
if (configuredRef && configuredRef.source !== "env") {
return true;
}
const fromConfig = normalizeSecretInput(normalizeSecretInputString(rawValue));
if (fromConfig) {
return true;
}
if (
params.provider.authProviderId &&
params.resolveProviderAuthValue?.(params.provider.authProviderId)
) {
return true;
}
if (
params.resolveEnvValue({
provider: params.provider,
configuredEnvVarId: configuredRef?.source === "env" ? configuredRef.id : undefined,
})
) {
return true;
}
const fallbackRawValue = params.resolveFallbackRawValue?.({
provider: params.provider,
config: params.config,
toolConfig: params.toolConfig,
});
const fallbackRef = resolveSecretInputRef({ value: fallbackRawValue }).ref;
if (fallbackRef && fallbackRef.source !== "env") {
return true;
}
const fallbackConfig = normalizeSecretInput(normalizeSecretInputString(fallbackRawValue));
if (fallbackConfig) {
return true;
}
return Boolean(
fallbackRef?.source === "env"
? params.resolveEnvValue({
provider: params.provider,
configuredEnvVarId: fallbackRef.id,
})
: undefined,
);
}
export function resolveWebProviderDefinition<
TProvider extends { id: string },
TConfig extends Record<string, unknown> | undefined,
TRuntimeMetadata extends RuntimeWebProviderMetadata,
TDefinition,
>(params: {
config: OpenClawConfig | undefined;
toolConfig: TConfig;
runtimeMetadata: TRuntimeMetadata | undefined;
sandboxed?: boolean;
providerId?: string;
providers: TProvider[];
resolveEnabled: (params: { toolConfig: TConfig; sandboxed?: boolean }) => boolean;
resolveAutoProviderId: (params: {
config: OpenClawConfig | undefined;
toolConfig: TConfig;
providers: TProvider[];
}) => string;
resolveFallbackProviderId?: (params: {
config: OpenClawConfig | undefined;
toolConfig: TConfig;
providers: TProvider[];
providerId: string;
}) => string | undefined;
createTool: (params: {
provider: TProvider;
config: OpenClawConfig | undefined;
toolConfig: TConfig;
runtimeMetadata: TRuntimeMetadata | undefined;
}) => TDefinition | null;
}): { provider: TProvider; definition: TDefinition } | null {
if (!params.resolveEnabled({ toolConfig: params.toolConfig, sandboxed: params.sandboxed })) {
return null;
}
const providers = params.providers.filter(Boolean);
if (providers.length === 0) {
return null;
}
const autoProviderId = params.resolveAutoProviderId({
config: params.config,
toolConfig: params.toolConfig,
providers,
});
const providerId =
params.providerId ?? params.runtimeMetadata?.selectedProvider ?? autoProviderId;
if (!providerId) {
return null;
}
const provider =
providers.find((entry) => entry.id === providerId) ??
providers.find(
(entry) =>
entry.id ===
params.resolveFallbackProviderId?.({
config: params.config,
toolConfig: params.toolConfig,
providers,
providerId,
}),
);
if (!provider) {
return null;
}
const definition = params.createTool({
provider,
config: params.config,
toolConfig: params.toolConfig,
runtimeMetadata: params.runtimeMetadata,
});
if (!definition) {
return null;
}
return { provider, definition };
}
export * from "../../packages/web-content-core/src/provider-runtime-shared.js";

View File

@@ -194,108 +194,174 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => {
}));
expect(runtimeShards).toEqual([
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-approval",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-device-push",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-exec",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-heartbeat",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-misc-a-f",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-misc-g-l",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-misc-gateway",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-misc-m-o",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-misc-path-ports",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-misc-process-restart",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-misc-s-system",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-misc-t-z",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-network",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-outbound",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-package-provider",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-session-state",
},
{
configs: ["test/vitest/vitest.hooks.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-hooks",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-approval-exec",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-channel-plugin",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-cli-ui",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-device",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-diagnostics-state",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-core-utils",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-env-auth",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-events-runtime",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-file-safety",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-files-commands",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-gateway-lock-argv",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-gateway-processes",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-gateway-watch",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-heartbeat-core",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-heartbeat-runner",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-misc-dedupe-disk",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-misc-os",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-misc-values",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-net-install",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-network-node",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-network-platform",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-outbound-actions",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-outbound-core",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-provider-push",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-repo-tooling",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-storage-state",
},
{
configs: ["test/vitest/vitest.infra.config.ts"],
requiresDist: false,
runner: "blacksmith-4vcpu-ubuntu-2404",
shardName: "core-runtime-infra-system-runtime",
},
{
configs: ["test/vitest/vitest.secrets.config.ts"],
requiresDist: false,
@@ -356,6 +422,48 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => {
]);
});
it("covers every infra test exactly once across core runtime infra shards", () => {
const infraShards = createNodeTestShards().filter((shard) =>
shard.shardName.startsWith("core-runtime-infra-"),
);
const actual = infraShards
.flatMap((shard) => shard.includePatterns ?? [])
.toSorted((a, b) => a.localeCompare(b));
expect(infraShards.map((shard) => shard.shardName)).toEqual([
"core-runtime-infra-approval-exec",
"core-runtime-infra-channel-plugin",
"core-runtime-infra-cli-ui",
"core-runtime-infra-device",
"core-runtime-infra-diagnostics-state",
"core-runtime-infra-core-utils",
"core-runtime-infra-env-auth",
"core-runtime-infra-events-runtime",
"core-runtime-infra-file-safety",
"core-runtime-infra-files-commands",
"core-runtime-infra-gateway-lock-argv",
"core-runtime-infra-gateway-processes",
"core-runtime-infra-gateway-watch",
"core-runtime-infra-heartbeat-core",
"core-runtime-infra-heartbeat-runner",
"core-runtime-infra-misc-dedupe-disk",
"core-runtime-infra-misc-os",
"core-runtime-infra-misc-values",
"core-runtime-infra-net-install",
"core-runtime-infra-network-node",
"core-runtime-infra-network-platform",
"core-runtime-infra-outbound-actions",
"core-runtime-infra-outbound-core",
"core-runtime-infra-provider-push",
"core-runtime-infra-repo-tooling",
"core-runtime-infra-storage-state",
"core-runtime-infra-system-runtime",
"core-runtime-infra-process",
]);
expect(actual).toEqual(listTestFiles("src/infra"));
expect(new Set(actual).size).toBe(actual.length);
});
it("covers every cron test exactly once across core runtime cron shards", () => {
const cronShards = createNodeTestShards().filter((shard) =>
shard.shardName.startsWith("core-runtime-cron-"),
@@ -373,36 +481,6 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => {
expect(new Set(actual).size).toBe(actual.length);
});
it("covers every infra test exactly once across core runtime infra shards", () => {
const infraShards = createNodeTestShards().filter((shard) =>
shard.configs.includes("test/vitest/vitest.infra.config.ts"),
);
const actual = infraShards
.flatMap((shard) => shard.includePatterns ?? [])
.toSorted((a, b) => a.localeCompare(b));
expect(infraShards.map((shard) => shard.shardName)).toEqual([
"core-runtime-infra-approval",
"core-runtime-infra-device-push",
"core-runtime-infra-exec",
"core-runtime-infra-heartbeat",
"core-runtime-infra-misc-a-f",
"core-runtime-infra-misc-g-l",
"core-runtime-infra-misc-gateway",
"core-runtime-infra-misc-m-o",
"core-runtime-infra-misc-path-ports",
"core-runtime-infra-misc-process-restart",
"core-runtime-infra-misc-s-system",
"core-runtime-infra-misc-t-z",
"core-runtime-infra-network",
"core-runtime-infra-outbound",
"core-runtime-infra-package-provider",
"core-runtime-infra-session-state",
]);
expect(actual).toEqual(listTestFiles("src/infra"));
expect(new Set(actual).size).toBe(actual.length);
});
it("splits the agentic lane into control-plane, command, agent, gateway, SDK, and plugin shards", () => {
const shards = createNodeTestShards();
const controlPlaneShards = shards.filter((shard) =>
@@ -466,13 +544,17 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => {
});
expect(commandShards.map((shard) => shard.shardName)).toEqual([
"agentic-commands-agent-channel",
"agentic-commands-doctor",
"agentic-commands-doctor-auth",
"agentic-commands-doctor-config",
"agentic-commands-doctor-cron",
"agentic-commands-doctor-misc-core",
"agentic-commands-doctor-misc-platform",
"agentic-commands-doctor-runtime",
"agentic-commands-doctor-config-state",
"agentic-commands-doctor-device",
"agentic-commands-doctor-gateway",
"agentic-commands-doctor-platform",
"agentic-commands-doctor-plugins-tools",
"agentic-commands-doctor-sessions-cron",
"agentic-commands-doctor-shared",
"agentic-commands-doctor-whatsapp",
"agentic-commands-doctor-workspace",
"agentic-commands-models",
"agentic-commands-onboard-config",
"agentic-commands-status-tools",
@@ -487,6 +569,10 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => {
shardName: shard.shardName,
})),
);
expect(
commandShards.find((shard) => shard.shardName === "agentic-commands-doctor-auth")
?.includePatterns,
).toContain("src/commands/oauth-tls-preflight.doctor.test.ts");
const commandShardFiles = commandShards
.flatMap((shard) => shard.includePatterns ?? [])
.toSorted((a, b) => a.localeCompare(b));

View File

@@ -302,35 +302,35 @@ describe("scripts/run-vitest", () => {
it("defaults direct non-watch runs to the stall watchdog", () => {
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["run"])).toEqual({
PATH: "/usr/bin",
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "120000",
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000",
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "60000",
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "120000",
});
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["run", "-t", "watch"])).toEqual({
PATH: "/usr/bin",
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "120000",
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000",
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "60000",
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "120000",
});
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["--watch=false"])).toEqual({
PATH: "/usr/bin",
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "120000",
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000",
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "60000",
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "120000",
});
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["--watch", "false"])).toEqual({
PATH: "/usr/bin",
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "120000",
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000",
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "60000",
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "120000",
});
expect(resolveRunVitestSpawnEnv({ PATH: "/usr/bin" }, ["--no-watch"])).toEqual({
PATH: "/usr/bin",
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "120000",
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000",
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "60000",
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "120000",
});
expect(resolveRunVitestSpawnEnv({ CI: "true", PATH: "/usr/bin" }, ["src/foo.test.ts"])).toEqual(
{
CI: "true",
PATH: "/usr/bin",
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "120000",
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000",
OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS: "60000",
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "120000",
},
);
expect(

View File

@@ -2293,6 +2293,8 @@ describe("scripts/test-projects Vitest stall watchdog", () => {
it("allows changed checks to disable automatic silent-run retries", () => {
expect(shouldRetryVitestNoOutputTimeout({})).toBe(true);
expect(shouldRetryVitestNoOutputTimeout({ CI: "true" })).toBe(false);
expect(shouldRetryVitestNoOutputTimeout({ GITHUB_ACTIONS: "true" })).toBe(false);
expect(shouldRetryVitestNoOutputTimeout({ OPENCLAW_VITEST_NO_OUTPUT_RETRY: "1" })).toBe(true);
expect(shouldRetryVitestNoOutputTimeout({ OPENCLAW_VITEST_NO_OUTPUT_RETRY: "0" })).toBe(false);
expect(shouldRetryVitestNoOutputTimeout({ OPENCLAW_VITEST_NO_OUTPUT_RETRY: "false" })).toBe(

View File

@@ -149,6 +149,11 @@
],
"@openclaw/net-policy/url-userinfo": ["./packages/net-policy/src/url-userinfo.ts"],
"@openclaw/net-policy/*": ["./packages/net-policy/src/*"],
"@openclaw/web-content-core": ["./packages/web-content-core/src/index.ts"],
"@openclaw/web-content-core/provider-runtime-shared": [
"./packages/web-content-core/src/provider-runtime-shared.ts"
],
"@openclaw/web-content-core/*": ["./packages/web-content-core/src/*"],
"@openclaw/speech-core": ["./packages/speech-core/runtime-api.ts"],
"@openclaw/speech-core/api": ["./packages/speech-core/api.ts"],
"@openclaw/speech-core/runtime-api": ["./packages/speech-core/runtime-api.ts"],

View File

@@ -448,6 +448,13 @@ function buildTerminalCoreDistEntries(): Record<string, string> {
};
}
function buildWebContentCoreDistEntries(): Record<string, string> {
return {
index: "packages/web-content-core/src/index.ts",
"provider-runtime-shared": "packages/web-content-core/src/provider-runtime-shared.ts",
};
}
function buildSpeechCoreDistEntries(): Record<string, string> {
return {
api: "packages/speech-core/api.ts",
@@ -647,6 +654,12 @@ export default defineConfig([
neverBundle: shouldExternalizeTerminalCoreDependency,
},
}),
nodeWorkspacePackageBuildConfig({
clean: true,
dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined,
entry: buildWebContentCoreDistEntries(),
outDir: "packages/web-content-core/dist",
}),
nodeWorkspacePackageBuildConfig({
clean: true,
dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined,