openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
970 lines (969 loc) • 37.1 kB
JavaScript
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { O as tryResolveSoleAgentId, i as listAgentEntries, m as resolveAgentWorkspaceDir, o as listAgentIds, u as resolveAgentDir } from "./agent-scope-config-DcbEhP0R.js";
import "./session-key-BnWWjqNc.js";
import { o as redactSensitiveUrlLikeString } from "./redact-sensitive-url-BN1NZvXG.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { n as sanitizeTerminalText } from "./safe-text-BGBqp1a4.js";
import { c as normalizeToolPolicyName } from "./tool-policy-shared-DIyS0iQC.js";
import { t as assignSafeServerNames } from "./agent-bundle-mcp-names-CP3ugHLh.js";
import { i as collectExplicitAllowlist } from "./tool-policy-Wi0C45cX.js";
import { x as findModelInCatalog } from "./model-selection-shared-BlLyx1r2.js";
import "./defaults-CdX9UGcX.js";
import "./agent-scope-DbtJyKUL.js";
import { t as resolveDefaultModelForAgent } from "./model-selection-config-BrdmmqKD.js";
import { n as isGatewaySecretRefUnavailableError } from "./credentials-CXbS4PS0.js";
import { a as readGatewayServiceState, o as resolveGatewayService } from "./service-Cc6NX6Jm.js";
import { n as getSystemdCgroupHygieneSummary } from "./service-runtime-C1Las5eF.js";
import { a as buildGatewayProbeConnectionDetails, o as callGateway, p as isGatewayCredentialsRequiredError } from "./call-BWR5pPgw.js";
import { t as loadSessionMcpConfig } from "./agent-bundle-mcp-runtime-config-MN40lQtN.js";
import { i as partitionMcpServersByConnectionScope } from "./mcp-connection-resolver-FyG5JEPz.js";
import { n as resolveMcpAuthProfileId } from "./mcp-auth-profile--_BtN_eA.js";
import "./model-catalog-DOgUhUHe.js";
import { a as loadPreparedModelCatalog } from "./prepared-model-catalog-Bt4dYhnY.js";
import "./model-selection-di2kjKCB.js";
import { a as setPluginToolMeta, r as getPluginToolMeta } from "./tool-metadata-B5aqo73s.js";
import { n as resolveEffectiveToolPolicy } from "./agent-tools.policy-NF_9Y4S4.js";
import { t as resolveConversationCapabilityProfile } from "./conversation-capability-profile-B6PkMXFD.js";
import { r as inspectRuntimeToolInputSchemas } from "./tool-schema-projection-a_T2Sr4Q.js";
import { n as normalizeAgentRuntimeTools } from "./tools-CAIhAR2v.js";
import { t as applyFinalEffectiveToolPolicy } from "./effective-tool-policy-Bco8DCTH.js";
import { a as shouldCreateBundleMcpRuntimeForAttempt } from "./attempt-tool-construction-plan-CRC43c_k.js";
import { n as supportsModelTools } from "./model-tool-support-DIQSEumC.js";
import { t as buildWorkspaceSkillStatus } from "./status-BDd5nSbi.js";
import { n as formatLocalAudioSelection, r as inspectLocalAudioSelection } from "./local-audio-YEyS3BND.js";
import { t as collectUnavailableAgentSkills } from "./doctor-skills-core-DZQVCoNO.js";
import { t as hasActiveGatewayExecCredential } from "./doctor-gateway-exec-credential-jPkNF-y8.js";
import { t as scrubDoctorErrorMessage } from "./doctor-error-message-PVjhuNzO.js";
import { c as gatewayConnectErrorWasRateLimited, r as GATEWAY_HEALTH_RATE_LIMITED_MESSAGE } from "./gateway-health-auth-diagnostic-6ZBqs-mo.js";
import { s as shouldManageGatewayService } from "./doctor-service-repair-policy-S9bPaCiq.js";
import { t as projectDoctorSecretRuntimeDegradations } from "./doctor-secret-runtime-degradation-auwIRP9K.js";
//#region src/flows/doctor-core-checks.runtime.ts
const PROVIDER_CATALOG_ORDERS = [
"simple",
"profile",
"paired",
"late"
];
const PROVIDER_CATALOG_ORDER_SET = new Set(PROVIDER_CATALOG_ORDERS);
function formatGatewayHealthDiagnostic(value) {
const raw = value instanceof Error ? value.message : String(value);
return scrubDoctorErrorMessage(sanitizeTerminalText(redactSensitiveUrlLikeString(raw)));
}
function detectUnavailableSkills(cfg, workspaceDir) {
const report = buildWorkspaceSkillStatus(workspaceDir, {
config: cfg,
agentId: tryResolveSoleAgentId(cfg)
});
return collectUnavailableAgentSkills(report);
}
async function collectLocalAudioAccelerationFindings() {
const selection = await inspectLocalAudioSelection();
const available = selection.candidates.filter((candidate) => candidate.available);
if (available.length === 0) return [];
const summary = formatLocalAudioSelection(selection);
if (summary) return [{
checkId: "core/doctor/local-audio-acceleration",
severity: "info",
message: `Local STT auto-selection: ${summary}.`,
path: "tools.media.models"
}];
return [{
checkId: "core/doctor/local-audio-acceleration",
severity: "info",
message: `Local STT commands were found but none are ready for auto-selection: ${available.map((candidate) => `${candidate.command}: ${candidate.reason}`).join("; ")}.`,
path: "tools.media.models",
fixHint: "Install the matching local model/runtime, or configure an audio-capable tools.media.models CLI entry."
}];
}
async function collectGatewayHealthFindings(ctx) {
const mode = ctx.cfg.gateway?.mode === "remote" ? "remote" : "local";
const gatewayPath = mode === "remote" ? "gateway.remote.url" : "gateway.mode";
let probeDetails;
const warning = (message, fixHint) => ({
checkId: "core/doctor/gateway-health",
severity: "warning",
message,
path: probeDetails || mode === "remote" ? gatewayPath : "gateway",
...probeDetails ? { target: formatGatewayHealthDiagnostic(probeDetails.url) } : {},
fixHint
});
try {
probeDetails = await buildGatewayProbeConnectionDetails({
config: ctx.cfg,
configPath: ctx.configPath
});
if (ctx.allowExecSecretRefs !== true && await hasActiveGatewayExecCredential({
cfg: ctx.cfg,
env: ctx.env,
targetUrl: probeDetails.url
})) return [warning("Authenticated Gateway health inspection was intentionally skipped because an active credential uses an exec SecretRef.", "Rerun `openclaw doctor --lint --only core/doctor/gateway-health --allow-exec` to permit configured secret execution.")];
const status = await callGateway({
method: "status",
params: { includeChannelSummary: false },
timeoutMs: 3e3,
sharedStateMode: "read-only",
config: ctx.cfg,
configPath: ctx.configPath,
tlsFingerprint: probeDetails.tlsFingerprint,
preauthHandshakeTimeoutMs: probeDetails.preauthHandshakeTimeoutMs
});
return projectDoctorSecretRuntimeDegradations(status).map((owner) => ({
checkId: "core/doctor/gateway-health",
severity: "warning",
message: `Secret runtime degradation: ${owner.message}`,
path: owner.path,
target: owner.target,
fixHint: `Retry: ${owner.retryHint}`
}));
} catch (error) {
if (!probeDetails) return [warning(`Gateway health inspection could not be prepared: ${formatGatewayHealthDiagnostic(error)}`, "Fix Gateway connection configuration, then rerun `openclaw doctor --lint --only core/doctor/gateway-health`.")];
const diagnostic = gatewayConnectErrorWasRateLimited(error) ? {
message: GATEWAY_HEALTH_RATE_LIMITED_MESSAGE,
fixHint: "Wait for the temporary authentication lockout to expire, then rerun doctor."
} : isGatewayCredentialsRequiredError(error) || isGatewaySecretRefUnavailableError(error) ? {
message: "Gateway status could not be inspected because this CLI has no usable token/password or paired device token for read-scope RPCs.",
fixHint: "Configure the Gateway token/password or pair this device, then rerun the selected health check."
} : {
message: `Gateway status could not be inspected: ${formatGatewayHealthDiagnostic(error)}`,
fixHint: mode === "remote" ? "Verify the remote Gateway URL, network path, TLS settings, and credentials." : "Inspect the service with `openclaw gateway status --deep`, or run `openclaw doctor` for guided checks."
};
return [warning(diagnostic.message, diagnostic.fixHint)];
}
}
function gatewayRuntimeStatus(runtime) {
return runtime?.status ?? runtime?.state ?? runtime?.subState;
}
async function collectGatewayDaemonFindings(ctx) {
if (ctx.cfg.gateway?.mode === "remote" || !await shouldManageGatewayService()) return [];
const service = resolveGatewayService();
const state = await readGatewayServiceState(service, { env: process.env });
const findings = [];
if (state.loadState.status === "unknown") {
findings.push({
checkId: "core/doctor/gateway-daemon",
severity: "warning",
message: `Gateway service status could not be determined: ${state.loadState.detail}`,
path: state.command?.sourcePath,
target: service.label,
fixHint: "Run `openclaw gateway status --deep`, restore service-manager access, and retry."
});
return findings;
}
if (!state.installed) {
findings.push({
checkId: "core/doctor/gateway-daemon",
severity: "warning",
message: "Gateway service is not installed.",
path: "gateway.mode",
target: service.label,
fixHint: "Run `openclaw gateway install` to install the service."
});
return findings;
}
if (state.loadState.status === "not-loaded") findings.push({
checkId: "core/doctor/gateway-daemon",
severity: "warning",
message: "Gateway service is installed but not loaded.",
path: state.command?.sourcePath,
target: service.label,
fixHint: "Start the installed service with `openclaw gateway start`."
});
const status = gatewayRuntimeStatus(state.runtime);
if (state.loadState.status === "loaded" && !state.running) findings.push({
checkId: "core/doctor/gateway-daemon",
severity: "warning",
message: status ? `Gateway service runtime is ${status}, not running.` : "Gateway service is loaded but runtime status could not confirm it is running.",
path: state.command?.sourcePath,
target: service.label,
fixHint: "Run `openclaw gateway status --deep` to inspect the service before choosing a recovery action."
});
if (state.runtime?.missingGuiSession) findings.push({
checkId: "core/doctor/gateway-daemon",
severity: "warning",
message: "Gateway service cannot attach to the user GUI session.",
path: state.command?.sourcePath,
target: service.label,
fixHint: state.runtime.detail ?? "Log into a GUI session, then rerun doctor."
});
if (state.runtime?.missingSupervision || state.runtime?.missingUnit) findings.push({
checkId: "core/doctor/gateway-daemon",
severity: "warning",
message: "Gateway service supervision metadata is missing.",
path: state.command?.sourcePath,
target: service.label,
fixHint: state.runtime.detail ?? "Reinstall or reload the Gateway service."
});
const hygiene = getSystemdCgroupHygieneSummary(state.runtime?.systemd);
if (hygiene) findings.push({
checkId: "core/doctor/gateway-daemon",
severity: "warning",
message: `Gateway systemd service has risky ${hygiene}.`,
path: state.command?.sourcePath,
target: service.label,
fixHint: "Repair the systemd unit so stale child processes are cleaned up reliably."
});
return findings;
}
function providerCatalogPath(pluginId) {
return pluginId ? `plugins.entries.${pluginId}` : void 0;
}
function providerCatalogProjectionFinding(params) {
const path = providerCatalogPath(params.pluginId);
return {
checkId: "core/doctor/provider-catalog-projection",
severity: "error",
message: params.message,
...path ? { path } : {},
target: params.providerId,
requirement: formatErrorMessage(params.error),
fixHint: "Fix the plugin provider catalog hook or disable the plugin, then rerun doctor before relying on model discovery."
};
}
function isReadableRecord(value) {
return value !== null && typeof value === "object";
}
function isTrimmedNonEmptyString(value) {
return typeof value === "string" && value.trim() === value && value.length > 0;
}
function hasProviderCatalogKey(params) {
try {
return {
ok: true,
present: params.key in params.value
};
} catch (error) {
return {
ok: false,
finding: providerCatalogProjectionFinding({
providerId: params.providerId,
pluginId: params.pluginId,
message: `Provider catalog ${params.providerId} result keys cannot be checked during doctor validation.`,
error
})
};
}
}
function readProviderCatalogValue(params) {
if (!isReadableRecord(params.value)) return {
ok: true,
value: void 0
};
try {
return {
ok: true,
value: params.value[params.key]
};
} catch (error) {
return {
ok: false,
finding: providerCatalogProjectionFinding({
providerId: params.providerId,
pluginId: params.pluginId,
message: `Provider catalog ${params.providerId} entry cannot be read during doctor validation.`,
error
})
};
}
}
function collectProviderCatalogModelFindings(params) {
const findings = [];
let models;
try {
if (!Array.isArray(params.models)) return [providerCatalogProjectionFinding({
providerId: params.providerId,
pluginId: params.pluginId,
message: `Provider catalog ${params.providerId} models value is invalid during doctor validation.`,
error: /* @__PURE__ */ new Error("models must be an array")
})];
models = params.models;
} catch (error) {
return [providerCatalogProjectionFinding({
providerId: params.providerId,
pluginId: params.pluginId,
message: `Provider catalog ${params.providerId} models value cannot be checked during doctor validation.`,
error
})];
}
let modelEntries;
try {
modelEntries = [];
let index = 0;
for (const model of models) {
modelEntries.push([index, model]);
index += 1;
}
} catch (error) {
return [providerCatalogProjectionFinding({
providerId: params.providerId,
pluginId: params.pluginId,
message: `Provider catalog ${params.providerId} model rows cannot be enumerated during doctor validation.`,
error
})];
}
for (const [index, model] of modelEntries) {
const modelId = readProviderCatalogValue({
value: model,
key: "id",
providerId: params.providerId,
pluginId: params.pluginId
});
if (!modelId.ok) {
findings.push(modelId.finding);
continue;
}
if (!isTrimmedNonEmptyString(modelId.value)) findings.push(providerCatalogProjectionFinding({
providerId: params.providerId,
pluginId: params.pluginId,
message: `Provider catalog ${params.providerId} model row ${index} has an invalid model id.`,
error: /* @__PURE__ */ new Error("model id must be a non-empty trimmed string")
}));
const modelName = readProviderCatalogValue({
value: model,
key: "name",
providerId: params.providerId,
pluginId: params.pluginId
});
if (!modelName.ok) {
findings.push(modelName.finding);
continue;
}
if (modelName.value !== void 0 && typeof modelName.value !== "string") findings.push(providerCatalogProjectionFinding({
providerId: params.providerId,
pluginId: params.pluginId,
message: `Provider catalog ${params.providerId} model row ${index} has an invalid model name.`,
error: /* @__PURE__ */ new Error("model name must be a string when present")
}));
}
return findings;
}
function collectProviderCatalogResultFindings(params) {
if (params.result == null) return [];
if (!isReadableRecord(params.result)) return [providerCatalogProjectionFinding({
providerId: params.providerId,
pluginId: params.pluginId,
message: `Provider catalog ${params.providerId} result is invalid during doctor validation.`,
error: /* @__PURE__ */ new Error("result must be an object")
})];
const hasProvider = hasProviderCatalogKey({
value: params.result,
key: "provider",
providerId: params.providerId,
pluginId: params.pluginId
});
if (!hasProvider.ok) return [hasProvider.finding];
const provider = readProviderCatalogValue({
value: params.result,
key: "provider",
providerId: params.providerId,
pluginId: params.pluginId
});
if (!provider.ok) return [provider.finding];
if (hasProvider.present && !isReadableRecord(provider.value)) return [providerCatalogProjectionFinding({
providerId: params.providerId,
pluginId: params.pluginId,
message: `Provider catalog ${params.providerId} provider value is invalid during doctor validation.`,
error: /* @__PURE__ */ new Error("provider must be an object")
})];
if (isReadableRecord(provider.value)) {
const models = readProviderCatalogValue({
value: provider.value,
key: "models",
providerId: params.providerId,
pluginId: params.pluginId
});
return models.ok ? collectProviderCatalogModelFindings({
...params,
models: models.value
}) : [models.finding];
}
const providers = readProviderCatalogValue({
value: params.result,
key: "providers",
providerId: params.providerId,
pluginId: params.pluginId
});
if (!providers.ok) return [providers.finding];
if (!isReadableRecord(providers.value)) return [providerCatalogProjectionFinding({
providerId: params.providerId,
pluginId: params.pluginId,
message: `Provider catalog ${params.providerId} result is invalid during doctor validation.`,
error: /* @__PURE__ */ new Error("result must include provider or providers object")
})];
let providerIds;
try {
providerIds = Object.keys(providers.value);
} catch (error) {
return [providerCatalogProjectionFinding({
providerId: params.providerId,
pluginId: params.pluginId,
message: `Provider catalog ${params.providerId} provider entries cannot be enumerated during doctor validation.`,
error
})];
}
const findings = [];
for (const providerId of providerIds) {
if (!isTrimmedNonEmptyString(providerId)) {
findings.push(providerCatalogProjectionFinding({
providerId: params.providerId,
pluginId: params.pluginId,
message: `Provider catalog ${params.providerId} provider key is invalid during doctor validation.`,
error: /* @__PURE__ */ new Error("provider key must be a non-empty trimmed string")
}));
continue;
}
const providerConfig = readProviderCatalogValue({
value: providers.value,
key: providerId,
providerId,
pluginId: params.pluginId
});
if (!providerConfig.ok) {
findings.push(providerConfig.finding);
continue;
}
if (!isReadableRecord(providerConfig.value)) {
findings.push(providerCatalogProjectionFinding({
providerId,
pluginId: params.pluginId,
message: `Provider catalog ${providerId} provider entry is invalid during doctor validation.`,
error: /* @__PURE__ */ new Error("provider entry must be an object")
}));
continue;
}
const models = readProviderCatalogValue({
value: providerConfig.value,
key: "models",
providerId,
pluginId: params.pluginId
});
findings.push(...models.ok ? collectProviderCatalogModelFindings({
providerId,
pluginId: params.pluginId,
models: models.value
}) : [models.finding]);
}
return findings;
}
function readProviderCatalogOrder(provider) {
let order;
try {
order = provider.staticCatalog?.order ?? "late";
} catch (error) {
return {
ok: false,
finding: providerCatalogProjectionFinding({
providerId: provider.id,
pluginId: provider.pluginId,
message: `Provider catalog ${provider.id} order cannot be read during doctor validation.`,
error
})
};
}
if (PROVIDER_CATALOG_ORDER_SET.has(order)) return {
ok: true,
order
};
return {
ok: false,
finding: providerCatalogProjectionFinding({
providerId: provider.id,
pluginId: provider.pluginId,
message: `Provider catalog ${provider.id} order is invalid during doctor validation.`,
error: /* @__PURE__ */ new Error("order must be simple, profile, paired, or late")
})
};
}
function groupProviderCatalogsForDoctor(providers) {
const findings = [];
const byOrder = {
simple: [],
profile: [],
paired: [],
late: []
};
for (const provider of providers) {
const order = readProviderCatalogOrder(provider);
if (!order.ok) {
findings.push(order.finding);
byOrder.late.push(provider);
continue;
}
byOrder[order.order].push(provider);
}
for (const order of PROVIDER_CATALOG_ORDERS) byOrder[order].sort((a, b) => a.label.localeCompare(b.label));
return {
findings,
byOrder
};
}
async function collectProviderCatalogProjectionFindings(cfg, workspaceDir) {
const { runProviderStaticCatalog } = await import("./provider-discovery--QYRxR2h.js");
const { resolvePluginProvidersCore } = await import("./providers.runtime.js");
const env = process.env;
let providers;
try {
providers = resolvePluginProvidersCore({
config: cfg,
workspaceDir,
env,
includeUntrustedWorkspacePlugins: false
});
} catch (error) {
return [{
checkId: "core/doctor/provider-catalog-projection",
severity: "error",
message: "Provider catalog hooks could not be loaded for doctor validation.",
requirement: formatErrorMessage(error),
fixHint: "Fix plugin provider discovery loading, then rerun doctor."
}];
}
const findings = [];
const grouped = groupProviderCatalogsForDoctor(providers);
findings.push(...grouped.findings);
for (const order of PROVIDER_CATALOG_ORDERS) for (const provider of grouped.byOrder[order]) {
let staticCatalog;
let staticCatalogRun;
try {
staticCatalog = provider.staticCatalog;
staticCatalogRun = isReadableRecord(staticCatalog) ? staticCatalog.run : void 0;
} catch (error) {
findings.push(providerCatalogProjectionFinding({
providerId: provider.id,
pluginId: provider.pluginId,
message: `Provider catalog ${provider.id} static catalog hook cannot be read during doctor validation.`,
error
}));
continue;
}
if (staticCatalog === void 0) continue;
if (typeof staticCatalogRun !== "function") {
findings.push(providerCatalogProjectionFinding({
providerId: provider.id,
pluginId: provider.pluginId,
message: `Provider catalog ${provider.id} static catalog hook is invalid during doctor validation.`,
error: /* @__PURE__ */ new Error("static catalog run must be a function")
}));
continue;
}
let result;
try {
result = await runProviderStaticCatalog({ provider });
} catch (error) {
findings.push(providerCatalogProjectionFinding({
providerId: provider.id,
pluginId: provider.pluginId,
message: `Provider catalog ${provider.id} failed during doctor validation.`,
error
}));
continue;
}
findings.push(...collectProviderCatalogResultFindings({
providerId: provider.id,
pluginId: provider.pluginId,
result
}));
}
return findings;
}
function buildDoctorRuntimeModel(params) {
const provider = params.provider || "openai";
const id = params.modelId || "gpt-5.6-sol";
const api = params.entry?.api ?? (provider === "openai" ? "openai-responses" : void 0);
const baseUrl = params.entry?.baseUrl ?? (api === "openai-chatgpt-responses" ? "https://chatgpt.com/backend-api" : provider === "openai" ? "https://api.openai.com/v1" : void 0);
return {
...params.entry,
provider,
id,
name: params.entry?.name ?? id,
...api ? { api } : {},
...baseUrl ? { baseUrl } : {}
};
}
function toolSchemaDiagnosticToFinding(params) {
let tool;
try {
tool = params.tools[params.diagnostic.toolIndex];
} catch {
tool = void 0;
}
const pluginId = tool ? getPluginToolMeta(tool)?.pluginId : void 0;
const owner = pluginId ? ` from plugin ${pluginId}` : "";
const agent = `Agent ${params.agentId} `;
const path = pluginId === "bundle-mcp" ? "mcp.servers" : pluginId ? `plugins.entries.${pluginId}` : `tools.${params.diagnostic.toolName}`;
const fixHint = pluginId === "bundle-mcp" ? "Disable or update the offending MCP server/tool so its parameters are a JSON object schema, then rerun doctor." : "Disable or update the offending plugin/tool so its parameters are a JSON object schema, then rerun doctor.";
return {
checkId: "core/doctor/runtime-tool-schemas",
severity: "error",
message: `${agent}tool ${params.diagnostic.toolName}${owner} has an unsupported input schema for runtime projection.`,
path,
target: params.diagnostic.toolName,
requirement: params.diagnostic.violations.join(", "),
fixHint
};
}
function collectToolSchemaFindings(params) {
return inspectRuntimeToolInputSchemas(params.tools).map((diagnostic) => toolSchemaDiagnosticToFinding({
agentId: params.agentId,
tools: params.tools,
diagnostic
}));
}
function collectNormalizedToolSchemaFindings(params) {
const preNormalizationFindings = [];
let normalizedTools;
try {
normalizedTools = normalizeAgentRuntimeTools({
tools: params.tools,
provider: params.modelRef.provider,
config: params.cfg,
workspaceDir: params.workspaceDir,
env: process.env,
modelId: params.modelRef.model,
modelApi: params.model.api,
model: params.model,
onPreNormalizationSchemaDiagnostics: (diagnostics, sourceTools) => {
preNormalizationFindings.push(...diagnostics.map((diagnostic) => toolSchemaDiagnosticToFinding({
agentId: params.agentId,
tools: sourceTools,
diagnostic
})));
}
});
} catch (error) {
return [...preNormalizationFindings, params.normalizationFailureFinding(error)];
}
return [...preNormalizationFindings, ...collectToolSchemaFindings({
agentId: params.agentId,
tools: normalizedTools
})];
}
function collectBundleMcpRuntimeToolSchemaFindings(params) {
const activeBundleTools = applyFinalEffectiveToolPolicy({
bundledTools: params.bundleRuntime.tools,
config: params.cfg,
conversationCapabilityProfile: resolveConversationCapabilityProfile({
config: params.cfg,
agentId: params.agentId,
modelProvider: params.modelRef.provider,
modelId: params.modelRef.model
}),
warn: () => {}
});
return collectNormalizedToolSchemaFindings({
agentId: params.agentId,
tools: activeBundleTools,
cfg: params.cfg,
workspaceDir: params.workspaceDir,
modelRef: params.modelRef,
model: params.model,
normalizationFailureFinding: bundleMcpRuntimeNormalizationFailureFinding
});
}
function agentRuntimeToolLoadFailureFinding(params) {
return {
checkId: "core/doctor/runtime-tool-schemas",
severity: "error",
message: `Agent ${params.agentId} runtime tool schema validation could not load the runtime tool set.`,
path: `agents.${params.agentId}.tools`,
requirement: formatErrorMessage(params.error),
fixHint: "Fix provider/plugin tool loading errors, then rerun doctor before relying on assistant tool startup."
};
}
function agentRuntimeToolNormalizationFailureFinding(params) {
return {
checkId: "core/doctor/runtime-tool-schemas",
severity: "error",
message: `Agent ${params.agentId} runtime tool schema validation could not normalize the runtime tool set.`,
path: `agents.${params.agentId}.tools`,
requirement: formatErrorMessage(params.error),
fixHint: "Fix provider/plugin schema normalization errors, then rerun doctor before relying on assistant tool startup."
};
}
async function collectAgentRuntimeToolSchemaFindings(params) {
let tools;
try {
const { createOpenClawCodingTools } = await import("./agent-tools-Nk45faPF.js");
tools = createOpenClawCodingTools({
agentId: params.agentId,
workspaceDir: params.workspaceDir,
config: params.cfg,
modelProvider: params.modelRef.provider,
modelId: params.modelRef.model,
modelApi: params.model.api,
modelCompat: params.model.compat,
modelContextWindowTokens: params.model.contextWindow,
allowGatewaySubagentBinding: true,
emitBeforeToolCallDiagnostics: false
});
} catch (error) {
return [agentRuntimeToolLoadFailureFinding({
agentId: params.agentId,
error
})];
}
return collectNormalizedToolSchemaFindings({
agentId: params.agentId,
tools,
cfg: params.cfg,
workspaceDir: params.workspaceDir,
modelRef: params.modelRef,
model: params.model,
normalizationFailureFinding: (error) => agentRuntimeToolNormalizationFailureFinding({
agentId: params.agentId,
error
})
});
}
function bundleMcpRuntimeNormalizationFailureFinding(error) {
return {
checkId: "core/doctor/runtime-tool-schemas",
severity: "error",
message: "Configured MCP tool schema validation could not normalize the runtime tool set.",
path: "mcp.servers",
requirement: formatErrorMessage(error),
fixHint: "Fix provider/plugin schema normalization errors, then rerun doctor before relying on assistant tool startup."
};
}
function bundleMcpRuntimeLoadFailureFinding(error) {
return {
checkId: "core/doctor/runtime-tool-schemas",
severity: "error",
message: "Configured MCP tool schema validation could not load the runtime tool set.",
path: "mcp.servers",
requirement: formatErrorMessage(error),
fixHint: "Fix or disable the offending MCP server, then rerun doctor before relying on assistant tool startup."
};
}
function bundleMcpRuntimeDiagnosticFinding(diagnostic) {
return {
checkId: "core/doctor/runtime-tool-schemas",
severity: "error",
message: `Configured MCP server "${diagnostic.serverName}" could not expose runtime tools for schema validation.`,
path: `mcp.servers.${diagnostic.serverName}`,
requirement: diagnostic.message,
fixHint: "Fix or disable the offending MCP server, then rerun doctor before relying on assistant tool startup."
};
}
function bundleMcpRequesterInspectionFinding(serverName) {
return {
checkId: "core/doctor/runtime-tool-schemas",
severity: "info",
message: `Configured requester-scoped MCP server "${serverName}" was not probed without an authenticated requester.`,
path: `mcp.servers.${serverName}`,
requirement: "authenticated requester context",
fixHint: "Verify this server from an authenticated agent turn."
};
}
function makeBundleMcpDiagnosticSentinel(name) {
const sentinel = {
name,
label: "Bundle MCP diagnostic",
description: "Internal doctor sentinel for bundle MCP schema diagnostics.",
parameters: {
type: "object",
properties: {}
},
execute: async () => ({
content: [],
details: {}
})
};
setPluginToolMeta(sentinel, {
pluginId: "bundle-mcp",
optional: false
});
return sentinel;
}
function synthesizeBundleMcpAllowlistSentinelName(params) {
const normalized = normalizeToolPolicyName(params.allowlistEntry);
const serverPrefix = normalizeToolPolicyName(`${params.safeServerName}__`);
if (normalized.startsWith(serverPrefix)) return normalized;
const separatorIndex = normalized.lastIndexOf("__");
if (separatorIndex < 0) return;
const toolPattern = normalized.slice(separatorIndex + 2);
if (!toolPattern) return;
const concreteToolName = toolPattern.replace(/\*/g, "diagnostic").replace(/\?/g, "x");
return `${params.safeServerName}__${concreteToolName}`;
}
function collectBundleMcpDiagnosticSentinels(params) {
const sentinels = [makeBundleMcpDiagnosticSentinel(`${params.diagnostic.safeServerName}__runtime_schema`)];
const effectivePolicy = resolveEffectiveToolPolicy({
config: params.cfg,
agentId: params.agentId,
modelProvider: params.modelRef.provider,
modelId: params.modelRef.model
});
const explicitAllowlist = collectExplicitAllowlist([
effectivePolicy.globalPolicy,
effectivePolicy.globalProviderPolicy,
effectivePolicy.agentPolicy,
effectivePolicy.agentProviderPolicy,
effectivePolicy.profileAlsoAllow ? { allow: effectivePolicy.profileAlsoAllow } : void 0,
effectivePolicy.providerProfileAlsoAllow ? { allow: effectivePolicy.providerProfileAlsoAllow } : void 0
]);
if (explicitAllowlist.length === 0) return sentinels;
for (const entry of explicitAllowlist) {
const sentinelName = synthesizeBundleMcpAllowlistSentinelName({
safeServerName: params.diagnostic.safeServerName,
allowlistEntry: entry
});
if (sentinelName) sentinels.push(makeBundleMcpDiagnosticSentinel(sentinelName));
}
return sentinels;
}
function shouldReportBundleMcpRuntimeDiagnostic(params) {
return applyFinalEffectiveToolPolicy({
bundledTools: collectBundleMcpDiagnosticSentinels(params),
config: params.cfg,
conversationCapabilityProfile: resolveConversationCapabilityProfile({
config: params.cfg,
agentId: params.agentId,
modelProvider: params.modelRef.provider,
modelId: params.modelRef.model
}),
warn: () => {}
}).length > 0;
}
function filterPolicyActiveBundleMcpDiagnostics(params) {
return params.diagnostics.filter((diagnostic) => shouldReportBundleMcpRuntimeDiagnostic({
cfg: params.cfg,
agentId: params.agentId,
modelRef: params.modelRef,
diagnostic
}));
}
function isAcpRuntimeAgent(cfg, agentId) {
return listAgentEntries(cfg).find((candidate) => normalizeAgentId(candidate.id) === agentId)?.runtime?.type === "acp";
}
async function collectRuntimeToolSchemaFindings(cfg, options) {
const findings = [];
const bundleRuntimeByContext = /* @__PURE__ */ new Map();
const bundleRuntimeLoadErrorsByContext = /* @__PURE__ */ new Map();
const reportedBundleRuntimeDiagnostics = /* @__PURE__ */ new Set();
const reportedBundleRuntimeLoadErrors = /* @__PURE__ */ new Set();
const reportedRequesterScopedServers = /* @__PURE__ */ new Set();
try {
for (const agentId of listAgentIds(cfg)) {
if (isAcpRuntimeAgent(cfg, agentId)) continue;
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
const collectForAgent = async () => {
const agentDir = resolveAgentDir(cfg, agentId);
const catalog = await loadPreparedModelCatalog({
config: cfg,
agentId,
agentDir,
readOnly: true,
providerDiscoveryProviderIds: []
});
const modelRef = resolveDefaultModelForAgent({
cfg,
agentId,
allowPluginNormalization: true
});
const model = buildDoctorRuntimeModel({
entry: findModelInCatalog(catalog, modelRef.provider, modelRef.model),
provider: modelRef.provider,
modelId: modelRef.model
});
if (!supportsModelTools(model)) return;
findings.push(...await collectAgentRuntimeToolSchemaFindings({
cfg,
agentId,
workspaceDir,
modelRef,
model
}));
if (!shouldCreateBundleMcpRuntimeForAttempt({ toolsEnabled: true })) return;
const fullMcpConfig = loadSessionMcpConfig({
workspaceDir,
cfg,
logDiagnostics: false
});
const safeServerNamesByServer = assignSafeServerNames(Object.keys(fullMcpConfig.loaded.mcpServers));
const { requesterScopedServerNames } = partitionMcpServersByConnectionScope(fullMcpConfig.loaded.mcpServers);
for (const serverName of requesterScopedServerNames) {
if (reportedRequesterScopedServers.has(serverName)) continue;
const diagnostic = {
serverName,
safeServerName: safeServerNamesByServer.get(serverName) ?? serverName,
launchSummary: "requester-scoped connection",
message: "authenticated requester context required"
};
if (shouldReportBundleMcpRuntimeDiagnostic({
cfg,
agentId,
modelRef,
diagnostic
})) {
findings.push(bundleMcpRequesterInspectionFinding(serverName));
reportedRequesterScopedServers.add(serverName);
}
}
const excludeServerNames = new Set(requesterScopedServerNames);
const staticMcpConfig = loadSessionMcpConfig({
workspaceDir,
cfg,
logDiagnostics: false,
excludeServerNames,
safeServerNamesByServer
});
const credentialContext = Object.values(staticMcpConfig.loaded.mcpServers).some(resolveMcpAuthProfileId) ? agentDir : "shared";
const runtimeContext = `${staticMcpConfig.fingerprint}\0${credentialContext}`;
if (!bundleRuntimeByContext.has(runtimeContext) && !bundleRuntimeLoadErrorsByContext.has(runtimeContext)) try {
const { createBundleMcpToolRuntime } = await import("./agent-bundle-mcp-tools-FqLm3YjH.js");
bundleRuntimeByContext.set(runtimeContext, await createBundleMcpToolRuntime({
workspaceDir,
agentDir,
cfg,
excludeServerNames,
safeServerNamesByServer
}));
} catch (error) {
bundleRuntimeLoadErrorsByContext.set(runtimeContext, bundleMcpRuntimeLoadFailureFinding(error));
}
const bundleRuntimeLoadError = bundleRuntimeLoadErrorsByContext.get(runtimeContext);
if (bundleRuntimeLoadError) {
if (!reportedBundleRuntimeLoadErrors.has(runtimeContext)) {
findings.push(bundleRuntimeLoadError);
reportedBundleRuntimeLoadErrors.add(runtimeContext);
}
return;
}
const bundleRuntime = bundleRuntimeByContext.get(runtimeContext);
if (bundleRuntime) {
if (bundleRuntime.diagnostics && bundleRuntime.diagnostics.length > 0) {
const policyActiveDiagnostics = filterPolicyActiveBundleMcpDiagnostics({
diagnostics: bundleRuntime.diagnostics,
cfg,
agentId,
modelRef
});
for (const diagnostic of policyActiveDiagnostics) {
if (reportedBundleRuntimeDiagnostics.has(diagnostic.serverName)) continue;
findings.push(bundleMcpRuntimeDiagnosticFinding(diagnostic));
reportedBundleRuntimeDiagnostics.add(diagnostic.serverName);
}
}
findings.push(...collectBundleMcpRuntimeToolSchemaFindings({
bundleRuntime,
cfg,
agentId,
workspaceDir,
modelRef,
model
}));
}
};
if (options?.runWithPluginMetadataSnapshot) await options.runWithPluginMetadataSnapshot({
config: cfg,
workspaceDir
}, collectForAgent);
else await collectForAgent();
}
} finally {
await Promise.all([...bundleRuntimeByContext.values()].map((runtime) => runtime.dispose()));
}
return findings;
}
//#endregion
export { collectGatewayDaemonFindings, collectGatewayHealthFindings, collectLocalAudioAccelerationFindings, collectProviderCatalogProjectionFindings, collectRuntimeToolSchemaFindings, detectUnavailableSkills };