mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-21 14:11:26 +00:00
fix(ci): align skills api and trim status startup
This commit is contained in:
@@ -56,7 +56,7 @@ describe("compaction retry integration", () => {
|
||||
} as unknown as NonNullable<ExtensionContext["model"]>;
|
||||
|
||||
const invokeGenerateSummary = (signal = new AbortController().signal) =>
|
||||
mockGenerateSummary(testMessages, testModel, 1000, "test-api-key", signal);
|
||||
mockGenerateSummary(testMessages, testModel, 1000, "test-api-key", undefined, signal);
|
||||
|
||||
const runSummaryRetry = (options: Parameters<typeof retryAsync>[1]) =>
|
||||
retryAsync(() => invokeGenerateSummary(), options);
|
||||
|
||||
@@ -257,6 +257,7 @@ async function summarizeChunks(params: {
|
||||
model,
|
||||
params.reserveTokens,
|
||||
params.apiKey,
|
||||
undefined,
|
||||
params.signal,
|
||||
effectiveInstructions,
|
||||
summary,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createSyntheticSourceInfo } from "@mariozechner/pi-coding-agent";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { installDownloadSpec } from "./skills-install-download.js";
|
||||
import { setTempStateDir } from "./skills-install.download-test-utils.js";
|
||||
@@ -60,7 +61,11 @@ function buildEntry(name: string): SkillEntry {
|
||||
description: `${name} test skill`,
|
||||
filePath: path.join(skillDir, "SKILL.md"),
|
||||
baseDir: skillDir,
|
||||
source: "openclaw-workspace",
|
||||
sourceInfo: createSyntheticSourceInfo(path.join(skillDir, "SKILL.md"), {
|
||||
source: "openclaw-workspace",
|
||||
scope: "project",
|
||||
baseDir: skillDir,
|
||||
}),
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createSyntheticSourceInfo } from "@mariozechner/pi-coding-agent";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildWorkspaceSkillStatus } from "./skills-status.js";
|
||||
import type { SkillEntry } from "./skills/types.js";
|
||||
@@ -17,7 +18,10 @@ describe("buildWorkspaceSkillStatus", () => {
|
||||
description: "test",
|
||||
filePath: "/tmp/os-scoped",
|
||||
baseDir: "/tmp",
|
||||
source: "test",
|
||||
sourceInfo: createSyntheticSourceInfo("/tmp/os-scoped", {
|
||||
source: "test",
|
||||
baseDir: "/tmp",
|
||||
}),
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createSyntheticSourceInfo } from "@mariozechner/pi-coding-agent";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withEnv } from "../test-utils/env.js";
|
||||
import { buildWorkspaceSkillStatus } from "./skills-status.js";
|
||||
@@ -24,7 +25,11 @@ function makeEntry(params: {
|
||||
description: `desc:${params.name}`,
|
||||
filePath: `/tmp/${params.name}/SKILL.md`,
|
||||
baseDir: `/tmp/${params.name}`,
|
||||
source: params.source ?? "openclaw-workspace",
|
||||
sourceInfo: createSyntheticSourceInfo(`/tmp/${params.name}/SKILL.md`, {
|
||||
source: params.source ?? "openclaw-workspace",
|
||||
scope: "project",
|
||||
baseDir: `/tmp/${params.name}`,
|
||||
}),
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createSyntheticSourceInfo } from "@mariozechner/pi-coding-agent";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSkillsPromptForRun } from "./skills.js";
|
||||
import type { SkillEntry } from "./skills/types.js";
|
||||
@@ -17,7 +18,11 @@ describe("resolveSkillsPromptForRun", () => {
|
||||
description: "Demo",
|
||||
filePath: "/app/skills/demo-skill/SKILL.md",
|
||||
baseDir: "/app/skills/demo-skill",
|
||||
source: "openclaw-bundled",
|
||||
sourceInfo: createSyntheticSourceInfo("/app/skills/demo-skill/SKILL.md", {
|
||||
source: "openclaw-bundled",
|
||||
scope: "project",
|
||||
baseDir: "/app/skills/demo-skill",
|
||||
}),
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import os from "node:os";
|
||||
import { formatSkillsForPrompt, type Skill } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
createSyntheticSourceInfo,
|
||||
formatSkillsForPrompt,
|
||||
type Skill,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { SkillEntry } from "./types.js";
|
||||
@@ -15,7 +19,11 @@ function makeSkill(name: string, desc = "A skill", filePath = `/skills/${name}/S
|
||||
description: desc,
|
||||
filePath,
|
||||
baseDir: `/skills/${name}`,
|
||||
source: "workspace",
|
||||
sourceInfo: createSyntheticSourceInfo(filePath, {
|
||||
source: "workspace",
|
||||
scope: "project",
|
||||
baseDir: `/skills/${name}`,
|
||||
}),
|
||||
disableModelInvocation: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Skill } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
export function resolveSkillSource(skill: Skill): string {
|
||||
return skill.source ?? "unknown";
|
||||
return skill.sourceInfo.source ?? "unknown";
|
||||
}
|
||||
|
||||
@@ -66,15 +66,10 @@ describe("tryRouteCli", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("passes suppressDoctorStdout=true for routed --json commands", async () => {
|
||||
it("skips config guard for routed status --json commands", async () => {
|
||||
await expect(tryRouteCli(["node", "openclaw", "status", "--json"])).resolves.toBe(true);
|
||||
|
||||
expect(ensureConfigReadyMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
commandPath: ["status"],
|
||||
suppressDoctorStdout: true,
|
||||
}),
|
||||
);
|
||||
expect(ensureConfigReadyMock).not.toHaveBeenCalled();
|
||||
expect(ensurePluginRegistryLoadedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { isTruthyEnvValue } from "../infra/env.js";
|
||||
import { loggingState } from "../logging/state.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { getCommandPathWithRootOptions, hasFlag, hasHelpOrVersion } from "./argv.js";
|
||||
import { emitCliBanner } from "./banner.js";
|
||||
import { findRoutedCommand } from "./program/routes.js";
|
||||
|
||||
async function prepareRoutedCommand(params: {
|
||||
@@ -12,13 +10,22 @@ async function prepareRoutedCommand(params: {
|
||||
loadPlugins?: boolean | ((argv: string[]) => boolean);
|
||||
}) {
|
||||
const suppressDoctorStdout = hasFlag(params.argv, "--json");
|
||||
emitCliBanner(VERSION, { argv: params.argv });
|
||||
const { ensureConfigReady } = await import("./program/config-guard.js");
|
||||
await ensureConfigReady({
|
||||
runtime: defaultRuntime,
|
||||
commandPath: params.commandPath,
|
||||
...(suppressDoctorStdout ? { suppressDoctorStdout: true } : {}),
|
||||
});
|
||||
const skipConfigGuard = params.commandPath[0] === "status" && suppressDoctorStdout;
|
||||
if (!suppressDoctorStdout && process.stdout.isTTY) {
|
||||
const [{ emitCliBanner }, { VERSION }] = await Promise.all([
|
||||
import("./banner.js"),
|
||||
import("../version.js"),
|
||||
]);
|
||||
emitCliBanner(VERSION, { argv: params.argv });
|
||||
}
|
||||
if (!skipConfigGuard) {
|
||||
const { ensureConfigReady } = await import("./program/config-guard.js");
|
||||
await ensureConfigReady({
|
||||
runtime: defaultRuntime,
|
||||
commandPath: params.commandPath,
|
||||
...(suppressDoctorStdout ? { suppressDoctorStdout: true } : {}),
|
||||
});
|
||||
}
|
||||
const shouldLoadPlugins =
|
||||
typeof params.loadPlugins === "function" ? params.loadPlugins(params.argv) : params.loadPlugins;
|
||||
if (shouldLoadPlugins) {
|
||||
|
||||
@@ -7,6 +7,7 @@ const normalizeEnvMock = vi.hoisted(() => vi.fn());
|
||||
const ensurePathMock = vi.hoisted(() => vi.fn());
|
||||
const assertRuntimeMock = vi.hoisted(() => vi.fn());
|
||||
const closeActiveMemorySearchManagersMock = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const hasMemoryRuntimeMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const outputRootHelpMock = vi.hoisted(() => vi.fn());
|
||||
const buildProgramMock = vi.hoisted(() => vi.fn());
|
||||
const maybeRunCliInContainerMock = vi.hoisted(() =>
|
||||
@@ -44,6 +45,10 @@ vi.mock("../plugins/memory-runtime.js", () => ({
|
||||
closeActiveMemorySearchManagers: closeActiveMemorySearchManagersMock,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/memory-state.js", () => ({
|
||||
hasMemoryRuntime: hasMemoryRuntimeMock,
|
||||
}));
|
||||
|
||||
vi.mock("./program/root-help.js", () => ({
|
||||
outputRootHelp: outputRootHelpMock,
|
||||
}));
|
||||
@@ -57,6 +62,7 @@ const { runCli } = await import("./run-main.js");
|
||||
describe("runCli exit behavior", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
hasMemoryRuntimeMock.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("does not force process.exit after successful routed command", async () => {
|
||||
@@ -69,7 +75,7 @@ describe("runCli exit behavior", () => {
|
||||
|
||||
expect(maybeRunCliInContainerMock).toHaveBeenCalledWith(["node", "openclaw", "status"]);
|
||||
expect(tryRouteCliMock).toHaveBeenCalledWith(["node", "openclaw", "status"]);
|
||||
expect(closeActiveMemorySearchManagersMock).toHaveBeenCalledTimes(1);
|
||||
expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled();
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
@@ -85,11 +91,20 @@ describe("runCli exit behavior", () => {
|
||||
expect(tryRouteCliMock).not.toHaveBeenCalled();
|
||||
expect(outputRootHelpMock).toHaveBeenCalledTimes(1);
|
||||
expect(buildProgramMock).not.toHaveBeenCalled();
|
||||
expect(closeActiveMemorySearchManagersMock).toHaveBeenCalledTimes(1);
|
||||
expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled();
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("closes memory managers when a runtime was registered", async () => {
|
||||
tryRouteCliMock.mockResolvedValueOnce(true);
|
||||
hasMemoryRuntimeMock.mockReturnValue(true);
|
||||
|
||||
await runCli(["node", "openclaw", "status"]);
|
||||
|
||||
expect(closeActiveMemorySearchManagersMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns after a handled container-target invocation", async () => {
|
||||
maybeRunCliInContainerMock.mockReturnValueOnce({ handled: true, exitCode: 0 });
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { normalizeEnv } from "../infra/env.js";
|
||||
import { formatUncaughtError } from "../infra/errors.js";
|
||||
import { isMainModule } from "../infra/is-main.js";
|
||||
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
|
||||
import { assertSupportedRuntime } from "../infra/runtime-guard.js";
|
||||
import { enableConsoleCapture } from "../logging.js";
|
||||
import { hasMemoryRuntime } from "../plugins/memory-state.js";
|
||||
import {
|
||||
getCommandPathWithRootOptions,
|
||||
getPrimaryCommand,
|
||||
@@ -13,12 +17,14 @@ import {
|
||||
isRootHelpInvocation,
|
||||
} from "./argv.js";
|
||||
import { maybeRunCliInContainer, parseCliContainerArgs } from "./container-target.js";
|
||||
import { loadCliDotEnv } from "./dotenv.js";
|
||||
import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js";
|
||||
import { tryRouteCli } from "./route.js";
|
||||
import { normalizeWindowsArgv } from "./windows-argv.js";
|
||||
|
||||
async function closeCliMemoryManagers(): Promise<void> {
|
||||
if (!hasMemoryRuntime()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { closeActiveMemorySearchManagers } = await import("../plugins/memory-runtime.js");
|
||||
await closeActiveMemorySearchManagers();
|
||||
@@ -80,6 +86,13 @@ export function shouldUseRootHelpFastPath(argv: string[]): boolean {
|
||||
return isRootHelpInvocation(argv);
|
||||
}
|
||||
|
||||
function shouldLoadCliDotEnv(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
if (existsSync(path.join(process.cwd(), ".env"))) {
|
||||
return true;
|
||||
}
|
||||
return existsSync(path.join(resolveStateDir(env), ".env"));
|
||||
}
|
||||
|
||||
export async function runCli(argv: string[] = process.argv) {
|
||||
const originalArgv = normalizeWindowsArgv(argv);
|
||||
const parsedContainer = parseCliContainerArgs(originalArgv);
|
||||
@@ -108,7 +121,10 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
}
|
||||
let normalizedArgv = parsedProfile.argv;
|
||||
|
||||
loadCliDotEnv({ quiet: true });
|
||||
if (shouldLoadCliDotEnv()) {
|
||||
const { loadCliDotEnv } = await import("./dotenv.js");
|
||||
loadCliDotEnv({ quiet: true });
|
||||
}
|
||||
normalizeEnv();
|
||||
if (shouldEnsureCliPath(normalizedArgv)) {
|
||||
ensureOpenClawCliOnPath();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createSyntheticSourceInfo } from "@mariozechner/pi-coding-agent";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
||||
import type { SkillEntry } from "../agents/skills.js";
|
||||
@@ -38,7 +39,11 @@ describe("skills-cli (e2e)", () => {
|
||||
description: "Capture UI screenshots",
|
||||
filePath: path.join(baseDir, "SKILL.md"),
|
||||
baseDir,
|
||||
source: "openclaw-bundled",
|
||||
sourceInfo: createSyntheticSourceInfo(path.join(baseDir, "SKILL.md"), {
|
||||
source: "openclaw-bundled",
|
||||
scope: "project",
|
||||
baseDir,
|
||||
}),
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
||||
31
src/commands/status.command.text-runtime.ts
Normal file
31
src/commands/status.command.text-runtime.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export { formatCliCommand } from "../cli/command-format.js";
|
||||
export { resolveGatewayPort } from "../config/config.js";
|
||||
export { info } from "../globals.js";
|
||||
export { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
export { formatGitInstallLabel } from "../infra/update-check.js";
|
||||
export {
|
||||
resolveMemoryCacheSummary,
|
||||
resolveMemoryFtsState,
|
||||
resolveMemoryVectorState,
|
||||
} from "../plugin-sdk/memory-core-host-status.js";
|
||||
export {
|
||||
formatPluginCompatibilityNotice,
|
||||
summarizePluginCompatibility,
|
||||
} from "../plugins/status.js";
|
||||
export { getTerminalTableWidth, renderTable } from "../terminal/table.js";
|
||||
export { theme } from "../terminal/theme.js";
|
||||
export { formatHealthChannelLines } from "./health.js";
|
||||
export { resolveControlUiLinks } from "./onboard-helpers.js";
|
||||
export { groupChannelIssuesByChannel } from "./status-all/channel-issues.js";
|
||||
export { formatGatewayAuthUsed } from "./status-all/format.js";
|
||||
export {
|
||||
formatDuration,
|
||||
formatKTokens,
|
||||
formatTokensCompact,
|
||||
shortenText,
|
||||
} from "./status.format.js";
|
||||
export {
|
||||
formatUpdateAvailableHint,
|
||||
formatUpdateOneLiner,
|
||||
resolveUpdateAvailability,
|
||||
} from "./status.update.js";
|
||||
@@ -1,46 +1,22 @@
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { withProgress } from "../cli/progress.js";
|
||||
import { resolveGatewayPort } from "../config/config.js";
|
||||
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
||||
import { info } from "../globals.js";
|
||||
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
import type { HeartbeatEventPayload } from "../infra/heartbeat-events.js";
|
||||
import { normalizeUpdateChannel, resolveUpdateChannelDisplay } from "../infra/update-channels.js";
|
||||
import { formatGitInstallLabel } from "../infra/update-check.js";
|
||||
import {
|
||||
resolveMemoryCacheSummary,
|
||||
resolveMemoryFtsState,
|
||||
resolveMemoryVectorState,
|
||||
type Tone,
|
||||
} from "../plugin-sdk/memory-core-host-status.js";
|
||||
import {
|
||||
formatPluginCompatibilityNotice,
|
||||
summarizePluginCompatibility,
|
||||
} from "../plugins/status.js";
|
||||
import type { Tone } from "../plugin-sdk/memory-core-host-status.js";
|
||||
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
|
||||
import { getTerminalTableWidth, renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { formatHealthChannelLines, type HealthSummary } from "./health.js";
|
||||
import { resolveControlUiLinks } from "./onboard-helpers.js";
|
||||
import { statusAllCommand } from "./status-all.js";
|
||||
import { groupChannelIssuesByChannel } from "./status-all/channel-issues.js";
|
||||
import { formatGatewayAuthUsed } from "./status-all/format.js";
|
||||
import type { HealthSummary } from "./health.js";
|
||||
import { getDaemonStatusSummary, getNodeDaemonStatusSummary } from "./status.daemon.js";
|
||||
import {
|
||||
formatDuration,
|
||||
formatKTokens,
|
||||
formatTokensCompact,
|
||||
shortenText,
|
||||
} from "./status.format.js";
|
||||
import { scanStatus } from "./status.scan.js";
|
||||
import {
|
||||
formatUpdateAvailableHint,
|
||||
formatUpdateOneLiner,
|
||||
resolveUpdateAvailability,
|
||||
} from "./status.update.js";
|
||||
|
||||
let providerUsagePromise: Promise<typeof import("../infra/provider-usage.js")> | undefined;
|
||||
let securityAuditModulePromise: Promise<typeof import("../security/audit.runtime.js")> | undefined;
|
||||
let gatewayCallModulePromise: Promise<typeof import("../gateway/call.js")> | undefined;
|
||||
let statusScanModulePromise: Promise<typeof import("./status.scan.js")> | undefined;
|
||||
let statusScanFastJsonModulePromise:
|
||||
| Promise<typeof import("./status.scan.fast-json.js")>
|
||||
| undefined;
|
||||
let statusAllModulePromise: Promise<typeof import("./status-all.js")> | undefined;
|
||||
let statusCommandTextRuntimePromise:
|
||||
| Promise<typeof import("./status.command.text-runtime.js")>
|
||||
| undefined;
|
||||
|
||||
function loadProviderUsage() {
|
||||
providerUsagePromise ??= import("../infra/provider-usage.js");
|
||||
@@ -52,6 +28,31 @@ function loadSecurityAuditModule() {
|
||||
return securityAuditModulePromise;
|
||||
}
|
||||
|
||||
function loadGatewayCallModule() {
|
||||
gatewayCallModulePromise ??= import("../gateway/call.js");
|
||||
return gatewayCallModulePromise;
|
||||
}
|
||||
|
||||
function loadStatusScanModule() {
|
||||
statusScanModulePromise ??= import("./status.scan.js");
|
||||
return statusScanModulePromise;
|
||||
}
|
||||
|
||||
function loadStatusScanFastJsonModule() {
|
||||
statusScanFastJsonModulePromise ??= import("./status.scan.fast-json.js");
|
||||
return statusScanFastJsonModulePromise;
|
||||
}
|
||||
|
||||
function loadStatusAllModule() {
|
||||
statusAllModulePromise ??= import("./status-all.js");
|
||||
return statusAllModulePromise;
|
||||
}
|
||||
|
||||
function loadStatusCommandTextRuntime() {
|
||||
statusCommandTextRuntimePromise ??= import("./status.command.text-runtime.js");
|
||||
return statusCommandTextRuntimePromise;
|
||||
}
|
||||
|
||||
function resolvePairingRecoveryContext(params: {
|
||||
error?: string | null;
|
||||
closeReason?: string | null;
|
||||
@@ -91,14 +92,19 @@ export async function statusCommand(
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
if (opts.all && !opts.json) {
|
||||
await statusAllCommand(runtime, { timeoutMs: opts.timeoutMs });
|
||||
await loadStatusAllModule().then(({ statusAllCommand }) =>
|
||||
statusAllCommand(runtime, { timeoutMs: opts.timeoutMs }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const scan = await scanStatus(
|
||||
{ json: opts.json, timeoutMs: opts.timeoutMs, all: opts.all },
|
||||
runtime,
|
||||
);
|
||||
const scan = opts.json
|
||||
? await loadStatusScanFastJsonModule().then(({ scanStatusJsonFast }) =>
|
||||
scanStatusJsonFast({ timeoutMs: opts.timeoutMs, all: opts.all }, runtime),
|
||||
)
|
||||
: await loadStatusScanModule().then(({ scanStatus }) =>
|
||||
scanStatus({ json: false, timeoutMs: opts.timeoutMs, all: opts.all }, runtime),
|
||||
);
|
||||
const runSecurityAudit = async () =>
|
||||
await loadSecurityAuditModule().then(({ runSecurityAudit }) =>
|
||||
runSecurityAudit({
|
||||
@@ -164,23 +170,29 @@ export async function statusCommand(
|
||||
indeterminate: true,
|
||||
enabled: opts.json !== true,
|
||||
},
|
||||
async () =>
|
||||
await callGateway<HealthSummary>({
|
||||
async () => {
|
||||
const { callGateway } = await loadGatewayCallModule();
|
||||
return await callGateway<HealthSummary>({
|
||||
method: "health",
|
||||
params: { probe: true },
|
||||
timeoutMs: opts.timeoutMs,
|
||||
config: scan.cfg,
|
||||
}),
|
||||
});
|
||||
},
|
||||
)
|
||||
: undefined;
|
||||
const lastHeartbeat =
|
||||
opts.deep && gatewayReachable
|
||||
? await callGateway<HeartbeatEventPayload | null>({
|
||||
method: "last-heartbeat",
|
||||
params: {},
|
||||
timeoutMs: opts.timeoutMs,
|
||||
config: scan.cfg,
|
||||
}).catch(() => null)
|
||||
? await loadGatewayCallModule()
|
||||
.then(({ callGateway }) =>
|
||||
callGateway<HeartbeatEventPayload | null>({
|
||||
method: "last-heartbeat",
|
||||
params: {},
|
||||
timeoutMs: opts.timeoutMs,
|
||||
config: scan.cfg,
|
||||
}),
|
||||
)
|
||||
.catch(() => null)
|
||||
: null;
|
||||
|
||||
const configChannel = normalizeUpdateChannel(cfg.update?.channel);
|
||||
@@ -230,11 +242,38 @@ export async function statusCommand(
|
||||
}
|
||||
|
||||
const rich = true;
|
||||
const {
|
||||
formatCliCommand,
|
||||
formatDuration,
|
||||
formatGatewayAuthUsed,
|
||||
formatGitInstallLabel,
|
||||
formatHealthChannelLines,
|
||||
formatKTokens,
|
||||
formatPluginCompatibilityNotice,
|
||||
formatTimeAgo,
|
||||
formatTokensCompact,
|
||||
formatUpdateAvailableHint,
|
||||
formatUpdateOneLiner,
|
||||
getTerminalTableWidth,
|
||||
groupChannelIssuesByChannel,
|
||||
info,
|
||||
renderTable,
|
||||
resolveControlUiLinks,
|
||||
resolveGatewayPort,
|
||||
resolveMemoryCacheSummary,
|
||||
resolveMemoryFtsState,
|
||||
resolveMemoryVectorState,
|
||||
resolveUpdateAvailability,
|
||||
shortenText,
|
||||
summarizePluginCompatibility,
|
||||
theme,
|
||||
} = await loadStatusCommandTextRuntime();
|
||||
const muted = (value: string) => (rich ? theme.muted(value) : value);
|
||||
const ok = (value: string) => (rich ? theme.success(value) : value);
|
||||
const warn = (value: string) => (rich ? theme.warn(value) : value);
|
||||
|
||||
if (opts.verbose) {
|
||||
const { buildGatewayConnectionDetails } = await loadGatewayCallModule();
|
||||
const details = buildGatewayConnectionDetails({ config: scan.cfg });
|
||||
runtime.log(info("Gateway connection:"));
|
||||
for (const line of details.message.split("\n")) {
|
||||
@@ -253,19 +292,15 @@ export async function statusCommand(
|
||||
runtime.log("");
|
||||
}
|
||||
|
||||
const dashboard = (() => {
|
||||
const controlUiEnabled = cfg.gateway?.controlUi?.enabled ?? true;
|
||||
if (!controlUiEnabled) {
|
||||
return "disabled";
|
||||
}
|
||||
const links = resolveControlUiLinks({
|
||||
port: resolveGatewayPort(cfg),
|
||||
bind: cfg.gateway?.bind,
|
||||
customBindHost: cfg.gateway?.customBindHost,
|
||||
basePath: cfg.gateway?.controlUi?.basePath,
|
||||
});
|
||||
return links.httpUrl;
|
||||
})();
|
||||
const dashboard =
|
||||
(cfg.gateway?.controlUi?.enabled ?? true)
|
||||
? resolveControlUiLinks({
|
||||
port: resolveGatewayPort(cfg),
|
||||
bind: cfg.gateway?.bind,
|
||||
customBindHost: cfg.gateway?.customBindHost,
|
||||
basePath: cfg.gateway?.controlUi?.basePath,
|
||||
}).httpUrl
|
||||
: "disabled";
|
||||
|
||||
const gatewayValue = (() => {
|
||||
const target = remoteUrlMissing
|
||||
|
||||
@@ -6,7 +6,7 @@ import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { resolveOsSummary } from "../infra/os-summary.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { getAgentLocalStatuses } from "./status.agent-local.js";
|
||||
import type { getAgentLocalStatuses as getAgentLocalStatusesFn } from "./status.agent-local.js";
|
||||
import type { StatusScanResult } from "./status.scan.js";
|
||||
import { scanStatusJsonCore } from "./status.scan.json-core.js";
|
||||
import {
|
||||
@@ -69,7 +69,7 @@ function resolveDefaultMemoryStorePath(agentId: string): string {
|
||||
|
||||
async function resolveMemoryStatusSnapshot(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentStatus: Awaited<ReturnType<typeof getAgentLocalStatuses>>;
|
||||
agentStatus: Awaited<ReturnType<typeof getAgentLocalStatusesFn>>;
|
||||
memoryPlugin: MemoryPluginStatus;
|
||||
}): Promise<MemoryStatusSnapshot | null> {
|
||||
const { resolveMemorySearchConfig } = await loadMemorySearchModule();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import type { UpdateCheckResult } from "../infra/update-check.js";
|
||||
import { loggingState } from "../logging/state.js";
|
||||
import { runExec } from "../process/exec.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { getAgentLocalStatuses } from "./status.agent-local.js";
|
||||
import type { getAgentLocalStatuses as getAgentLocalStatusesFn } from "./status.agent-local.js";
|
||||
import type { StatusScanResult } from "./status.scan.js";
|
||||
import {
|
||||
buildTailscaleHttpsUrl,
|
||||
@@ -10,13 +11,15 @@ import {
|
||||
resolveGatewayProbeSnapshot,
|
||||
resolveMemoryPluginStatus,
|
||||
} from "./status.scan.shared.js";
|
||||
import { getStatusSummary } from "./status.summary.js";
|
||||
import { getUpdateCheckResult } from "./status.update.js";
|
||||
import type { getStatusSummary as getStatusSummaryFn } from "./status.summary.js";
|
||||
|
||||
let pluginRegistryModulePromise: Promise<typeof import("../cli/plugin-registry.js")> | undefined;
|
||||
let statusScanDepsRuntimeModulePromise:
|
||||
| Promise<typeof import("./status.scan.deps.runtime.js")>
|
||||
| undefined;
|
||||
let statusAgentLocalModulePromise: Promise<typeof import("./status.agent-local.js")> | undefined;
|
||||
let statusSummaryModulePromise: Promise<typeof import("./status.summary.js")> | undefined;
|
||||
let statusUpdateModulePromise: Promise<typeof import("./status.update.js")> | undefined;
|
||||
|
||||
function loadPluginRegistryModule() {
|
||||
pluginRegistryModulePromise ??= import("../cli/plugin-registry.js");
|
||||
@@ -28,7 +31,22 @@ function loadStatusScanDepsRuntimeModule() {
|
||||
return statusScanDepsRuntimeModulePromise;
|
||||
}
|
||||
|
||||
export function buildColdStartUpdateResult(): Awaited<ReturnType<typeof getUpdateCheckResult>> {
|
||||
function loadStatusAgentLocalModule() {
|
||||
statusAgentLocalModulePromise ??= import("./status.agent-local.js");
|
||||
return statusAgentLocalModulePromise;
|
||||
}
|
||||
|
||||
function loadStatusSummaryModule() {
|
||||
statusSummaryModulePromise ??= import("./status.summary.js");
|
||||
return statusSummaryModulePromise;
|
||||
}
|
||||
|
||||
function loadStatusUpdateModule() {
|
||||
statusUpdateModulePromise ??= import("./status.update.js");
|
||||
return statusUpdateModulePromise;
|
||||
}
|
||||
|
||||
export function buildColdStartUpdateResult(): UpdateCheckResult {
|
||||
return {
|
||||
root: null,
|
||||
installKind: "unknown",
|
||||
@@ -36,6 +54,34 @@ export function buildColdStartUpdateResult(): Awaited<ReturnType<typeof getUpdat
|
||||
};
|
||||
}
|
||||
|
||||
function buildColdStartAgentLocalStatuses(): Awaited<ReturnType<typeof getAgentLocalStatusesFn>> {
|
||||
return {
|
||||
defaultId: "main",
|
||||
agents: [],
|
||||
totalSessions: 0,
|
||||
bootstrapPendingCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function buildColdStartStatusSummary(): Awaited<ReturnType<typeof getStatusSummaryFn>> {
|
||||
return {
|
||||
runtimeVersion: null,
|
||||
heartbeat: {
|
||||
defaultAgentId: "main",
|
||||
agents: [],
|
||||
},
|
||||
channelSummary: [],
|
||||
queuedSystemEvents: [],
|
||||
sessions: {
|
||||
paths: [],
|
||||
count: 0,
|
||||
defaults: { model: null, contextTokens: null },
|
||||
recent: [],
|
||||
byAgent: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function scanStatusJsonCore(params: {
|
||||
coldStart: boolean;
|
||||
cfg: OpenClawConfig;
|
||||
@@ -46,7 +92,7 @@ export async function scanStatusJsonCore(params: {
|
||||
resolveOsSummary: () => StatusScanResult["osSummary"];
|
||||
resolveMemory: (args: {
|
||||
cfg: OpenClawConfig;
|
||||
agentStatus: Awaited<ReturnType<typeof getAgentLocalStatuses>>;
|
||||
agentStatus: Awaited<ReturnType<typeof getAgentLocalStatusesFn>>;
|
||||
memoryPlugin: StatusScanResult["memoryPlugin"];
|
||||
runtime: RuntimeEnv;
|
||||
}) => Promise<StatusScanResult["memory"]>;
|
||||
@@ -72,13 +118,21 @@ export async function scanStatusJsonCore(params: {
|
||||
params.coldStart && !hasConfiguredChannels && opts.all !== true;
|
||||
const updatePromise = skipColdStartNetworkChecks
|
||||
? Promise.resolve(buildColdStartUpdateResult())
|
||||
: getUpdateCheckResult({
|
||||
timeoutMs: updateTimeoutMs,
|
||||
fetchGit: true,
|
||||
includeRegistry: true,
|
||||
});
|
||||
const agentStatusPromise = getAgentLocalStatuses(cfg);
|
||||
const summaryPromise = getStatusSummary({ config: cfg, sourceConfig });
|
||||
: loadStatusUpdateModule().then(({ getUpdateCheckResult }) =>
|
||||
getUpdateCheckResult({
|
||||
timeoutMs: updateTimeoutMs,
|
||||
fetchGit: true,
|
||||
includeRegistry: true,
|
||||
}),
|
||||
);
|
||||
const agentStatusPromise = skipColdStartNetworkChecks
|
||||
? Promise.resolve(buildColdStartAgentLocalStatuses())
|
||||
: loadStatusAgentLocalModule().then(({ getAgentLocalStatuses }) => getAgentLocalStatuses(cfg));
|
||||
const summaryPromise = skipColdStartNetworkChecks
|
||||
? Promise.resolve(buildColdStartStatusSummary())
|
||||
: loadStatusSummaryModule().then(({ getStatusSummary }) =>
|
||||
getStatusSummary({ config: cfg, sourceConfig }),
|
||||
);
|
||||
const tailscaleDnsPromise =
|
||||
tailscaleMode === "off"
|
||||
? Promise.resolve<string | null>(null)
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
||||
import { buildGatewayConnectionDetailsWithResolvers } from "../gateway/connection-details.js";
|
||||
import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js";
|
||||
import { probeGateway } from "../gateway/probe.js";
|
||||
import type { MemoryProviderStatus } from "../plugin-sdk/memory-core-host-engine-storage.js";
|
||||
import {
|
||||
pickGatewaySelfPresence,
|
||||
resolveGatewayProbeAuthResolution,
|
||||
} from "./status.gateway-probe.js";
|
||||
export { pickGatewaySelfPresence } from "./gateway-presence.js";
|
||||
|
||||
let gatewayProbeModulePromise: Promise<typeof import("./status.gateway-probe.js")> | undefined;
|
||||
|
||||
function loadGatewayProbeModule() {
|
||||
gatewayProbeModulePromise ??= import("./status.gateway-probe.js");
|
||||
return gatewayProbeModulePromise;
|
||||
}
|
||||
|
||||
export type MemoryStatusSnapshot = MemoryProviderStatus & {
|
||||
agentId: string;
|
||||
@@ -20,7 +24,7 @@ export type MemoryPluginStatus = {
|
||||
};
|
||||
|
||||
export type GatewayProbeSnapshot = {
|
||||
gatewayConnection: ReturnType<typeof buildGatewayConnectionDetails>;
|
||||
gatewayConnection: ReturnType<typeof buildGatewayConnectionDetailsWithResolvers>;
|
||||
remoteUrlMissing: boolean;
|
||||
gatewayMode: "local" | "remote";
|
||||
gatewayProbeAuth: {
|
||||
@@ -60,24 +64,31 @@ export async function resolveGatewayProbeSnapshot(params: {
|
||||
cfg: OpenClawConfig;
|
||||
opts: { timeoutMs?: number; all?: boolean; skipProbe?: boolean };
|
||||
}): Promise<GatewayProbeSnapshot> {
|
||||
const gatewayConnection = buildGatewayConnectionDetails({ config: params.cfg });
|
||||
const gatewayConnection = buildGatewayConnectionDetailsWithResolvers({ config: params.cfg });
|
||||
const isRemoteMode = params.cfg.gateway?.mode === "remote";
|
||||
const remoteUrlRaw =
|
||||
typeof params.cfg.gateway?.remote?.url === "string" ? params.cfg.gateway.remote.url : "";
|
||||
const remoteUrlMissing = isRemoteMode && !remoteUrlRaw.trim();
|
||||
const gatewayMode = isRemoteMode ? "remote" : "local";
|
||||
if (remoteUrlMissing || params.opts.skipProbe) {
|
||||
return {
|
||||
gatewayConnection,
|
||||
remoteUrlMissing,
|
||||
gatewayMode,
|
||||
gatewayProbeAuth: {},
|
||||
gatewayProbeAuthWarning: undefined,
|
||||
gatewayProbe: null,
|
||||
};
|
||||
}
|
||||
const { resolveGatewayProbeAuthResolution } = await loadGatewayProbeModule();
|
||||
const gatewayProbeAuthResolution = await resolveGatewayProbeAuthResolution(params.cfg);
|
||||
let gatewayProbeAuthWarning = gatewayProbeAuthResolution.warning;
|
||||
const gatewayProbe = remoteUrlMissing
|
||||
? null
|
||||
: params.opts.skipProbe
|
||||
? null
|
||||
: await probeGateway({
|
||||
url: gatewayConnection.url,
|
||||
auth: gatewayProbeAuthResolution.auth,
|
||||
timeoutMs: Math.min(params.opts.all ? 5000 : 2500, params.opts.timeoutMs ?? 10_000),
|
||||
detailLevel: "presence",
|
||||
}).catch(() => null);
|
||||
const gatewayProbe = await probeGateway({
|
||||
url: gatewayConnection.url,
|
||||
auth: gatewayProbeAuthResolution.auth,
|
||||
timeoutMs: Math.min(params.opts.all ? 5000 : 2500, params.opts.timeoutMs ?? 10_000),
|
||||
detailLevel: "presence",
|
||||
}).catch(() => null);
|
||||
if (gatewayProbeAuthWarning && gatewayProbe?.ok === false) {
|
||||
gatewayProbe.error = gatewayProbe.error
|
||||
? `${gatewayProbe.error}; ${gatewayProbeAuthWarning}`
|
||||
@@ -155,5 +166,3 @@ export async function resolveSharedMemoryStatusSnapshot(params: {
|
||||
await manager.close?.().catch(() => {});
|
||||
return { agentId, ...status };
|
||||
}
|
||||
|
||||
export { pickGatewaySelfPresence };
|
||||
|
||||
@@ -182,6 +182,10 @@ export async function loadStatusScanModuleForTest(
|
||||
probeGateway: mocks.probeGateway,
|
||||
}));
|
||||
vi.doMock("./status.gateway-probe.js", () => createStatusGatewayProbeModuleMock(mocks));
|
||||
vi.doMock("../gateway/connection-details.js", () => ({
|
||||
buildGatewayConnectionDetails: mocks.buildGatewayConnectionDetails,
|
||||
buildGatewayConnectionDetailsWithResolvers: mocks.buildGatewayConnectionDetails,
|
||||
}));
|
||||
vi.doMock("../process/exec.js", () => createStatusExecModuleMock());
|
||||
vi.doMock("../cli/plugin-registry.js", () => createStatusPluginRegistryModuleMock(mocks));
|
||||
vi.doMock("../plugins/status.js", () => createStatusPluginStatusModuleMock(mocks));
|
||||
|
||||
@@ -7,9 +7,9 @@ import { withProgress } from "../cli/progress.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { readBestEffortConfig } from "../config/config.js";
|
||||
import { resolveConfigPath } from "../config/paths.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import type { collectChannelStatusIssues as collectChannelStatusIssuesFn } from "../infra/channels-status-issues.js";
|
||||
import { resolveOsSummary } from "../infra/os-summary.js";
|
||||
import type { UpdateCheckResult } from "../infra/update-check.js";
|
||||
import {
|
||||
buildPluginCompatibilityNotices,
|
||||
type PluginCompatibilityNotice,
|
||||
@@ -18,7 +18,7 @@ import { runExec } from "../process/exec.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { createLazyRuntimeSurface } from "../shared/lazy-runtime.js";
|
||||
import type { buildChannelsTable as buildChannelsTableFn } from "./status-all/channels.js";
|
||||
import { getAgentLocalStatuses } from "./status.agent-local.js";
|
||||
import type { getAgentLocalStatuses as getAgentLocalStatusesFn } from "./status.agent-local.js";
|
||||
import { buildColdStartUpdateResult, scanStatusJsonCore } from "./status.scan.json-core.js";
|
||||
import {
|
||||
buildTailscaleHttpsUrl,
|
||||
@@ -30,14 +30,17 @@ import {
|
||||
type MemoryPluginStatus,
|
||||
type MemoryStatusSnapshot,
|
||||
} from "./status.scan.shared.js";
|
||||
import { getStatusSummary } from "./status.summary.js";
|
||||
import { getUpdateCheckResult } from "./status.update.js";
|
||||
import type { getStatusSummary as getStatusSummaryFn } from "./status.summary.js";
|
||||
|
||||
type DeferredResult<T> = { ok: true; value: T } | { ok: false; error: unknown };
|
||||
|
||||
let statusScanDepsRuntimeModulePromise:
|
||||
| Promise<typeof import("./status.scan.deps.runtime.js")>
|
||||
| undefined;
|
||||
let statusAgentLocalModulePromise: Promise<typeof import("./status.agent-local.js")> | undefined;
|
||||
let statusSummaryModulePromise: Promise<typeof import("./status.summary.js")> | undefined;
|
||||
let statusUpdateModulePromise: Promise<typeof import("./status.update.js")> | undefined;
|
||||
let gatewayCallModulePromise: Promise<typeof import("../gateway/call.js")> | undefined;
|
||||
|
||||
const loadStatusScanRuntimeModule = createLazyRuntimeSurface(
|
||||
() => import("./status.scan.runtime.js"),
|
||||
@@ -49,6 +52,26 @@ function loadStatusScanDepsRuntimeModule() {
|
||||
return statusScanDepsRuntimeModulePromise;
|
||||
}
|
||||
|
||||
function loadStatusAgentLocalModule() {
|
||||
statusAgentLocalModulePromise ??= import("./status.agent-local.js");
|
||||
return statusAgentLocalModulePromise;
|
||||
}
|
||||
|
||||
function loadStatusSummaryModule() {
|
||||
statusSummaryModulePromise ??= import("./status.summary.js");
|
||||
return statusSummaryModulePromise;
|
||||
}
|
||||
|
||||
function loadStatusUpdateModule() {
|
||||
statusUpdateModulePromise ??= import("./status.update.js");
|
||||
return statusUpdateModulePromise;
|
||||
}
|
||||
|
||||
function loadGatewayCallModule() {
|
||||
gatewayCallModulePromise ??= import("../gateway/call.js");
|
||||
return gatewayCallModulePromise;
|
||||
}
|
||||
|
||||
function deferResult<T>(promise: Promise<T>): Promise<DeferredResult<T>> {
|
||||
return promise.then(
|
||||
(value) => ({ ok: true, value }),
|
||||
@@ -75,6 +98,7 @@ async function resolveChannelsStatus(params: {
|
||||
if (!params.gatewayReachable) {
|
||||
return null;
|
||||
}
|
||||
const { callGateway } = await loadGatewayCallModule();
|
||||
return await callGateway({
|
||||
config: params.cfg,
|
||||
method: "channels.status",
|
||||
@@ -94,7 +118,7 @@ export type StatusScanResult = {
|
||||
tailscaleMode: string;
|
||||
tailscaleDns: string | null;
|
||||
tailscaleHttpsUrl: string | null;
|
||||
update: Awaited<ReturnType<typeof getUpdateCheckResult>>;
|
||||
update: UpdateCheckResult;
|
||||
gatewayConnection: GatewayProbeSnapshot["gatewayConnection"];
|
||||
remoteUrlMissing: boolean;
|
||||
gatewayMode: "local" | "remote";
|
||||
@@ -107,9 +131,9 @@ export type StatusScanResult = {
|
||||
gatewayReachable: boolean;
|
||||
gatewaySelf: ReturnType<typeof pickGatewaySelfPresence>;
|
||||
channelIssues: ReturnType<typeof collectChannelStatusIssuesFn>;
|
||||
agentStatus: Awaited<ReturnType<typeof getAgentLocalStatuses>>;
|
||||
agentStatus: Awaited<ReturnType<typeof getAgentLocalStatusesFn>>;
|
||||
channels: Awaited<ReturnType<typeof buildChannelsTableFn>>;
|
||||
summary: Awaited<ReturnType<typeof getStatusSummary>>;
|
||||
summary: Awaited<ReturnType<typeof getStatusSummaryFn>>;
|
||||
memory: MemoryStatusSnapshot | null;
|
||||
memoryPlugin: MemoryPluginStatus;
|
||||
pluginCompatibility: PluginCompatibilityNotice[];
|
||||
@@ -117,7 +141,7 @@ export type StatusScanResult = {
|
||||
|
||||
async function resolveMemoryStatusSnapshot(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentStatus: Awaited<ReturnType<typeof getAgentLocalStatuses>>;
|
||||
agentStatus: Awaited<ReturnType<typeof getAgentLocalStatusesFn>>;
|
||||
memoryPlugin: MemoryPluginStatus;
|
||||
}): Promise<MemoryStatusSnapshot | null> {
|
||||
const { getMemorySearchManager } = await loadStatusScanDepsRuntimeModule();
|
||||
@@ -130,6 +154,34 @@ async function resolveMemoryStatusSnapshot(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function buildColdStartAgentLocalStatuses(): Awaited<ReturnType<typeof getAgentLocalStatusesFn>> {
|
||||
return {
|
||||
defaultId: "main",
|
||||
agents: [],
|
||||
totalSessions: 0,
|
||||
bootstrapPendingCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function buildColdStartStatusSummary(): Awaited<ReturnType<typeof getStatusSummaryFn>> {
|
||||
return {
|
||||
runtimeVersion: null,
|
||||
heartbeat: {
|
||||
defaultAgentId: "main",
|
||||
agents: [],
|
||||
},
|
||||
channelSummary: [],
|
||||
queuedSystemEvents: [],
|
||||
sessions: {
|
||||
paths: [],
|
||||
count: 0,
|
||||
defaults: { model: null, contextTokens: null },
|
||||
recent: [],
|
||||
byAgent: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function scanStatusJsonFast(opts: {
|
||||
timeoutMs?: number;
|
||||
all?: boolean;
|
||||
@@ -208,15 +260,27 @@ export async function scanStatus(
|
||||
const updatePromise = deferResult(
|
||||
skipColdStartNetworkChecks
|
||||
? Promise.resolve(buildColdStartUpdateResult())
|
||||
: getUpdateCheckResult({
|
||||
timeoutMs: updateTimeoutMs,
|
||||
fetchGit: true,
|
||||
includeRegistry: true,
|
||||
}),
|
||||
: loadStatusUpdateModule().then(({ getUpdateCheckResult }) =>
|
||||
getUpdateCheckResult({
|
||||
timeoutMs: updateTimeoutMs,
|
||||
fetchGit: true,
|
||||
includeRegistry: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const agentStatusPromise = deferResult(
|
||||
skipColdStartNetworkChecks
|
||||
? Promise.resolve(buildColdStartAgentLocalStatuses())
|
||||
: loadStatusAgentLocalModule().then(({ getAgentLocalStatuses }) =>
|
||||
getAgentLocalStatuses(cfg),
|
||||
),
|
||||
);
|
||||
const agentStatusPromise = deferResult(getAgentLocalStatuses(cfg));
|
||||
const summaryPromise = deferResult(
|
||||
getStatusSummary({ config: cfg, sourceConfig: loadedRaw }),
|
||||
skipColdStartNetworkChecks
|
||||
? Promise.resolve(buildColdStartStatusSummary())
|
||||
: loadStatusSummaryModule().then(({ getStatusSummary }) =>
|
||||
getStatusSummary({ config: cfg, sourceConfig: loadedRaw }),
|
||||
),
|
||||
);
|
||||
progress.tick();
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ import {
|
||||
} from "../utils/message-channel.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import { GatewayClient, type GatewayClientOptions } from "./client.js";
|
||||
import {
|
||||
buildGatewayConnectionDetailsWithResolvers,
|
||||
type GatewayConnectionDetails,
|
||||
} from "./connection-details.js";
|
||||
import {
|
||||
GatewaySecretRefUnavailableError,
|
||||
resolveGatewayCredentialsFromConfig,
|
||||
@@ -37,8 +41,8 @@ import {
|
||||
resolveLeastPrivilegeOperatorScopesForMethod,
|
||||
type OperatorScope,
|
||||
} from "./method-scopes.js";
|
||||
import { isSecureWebSocketUrl } from "./net.js";
|
||||
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
||||
export type { GatewayConnectionDetails };
|
||||
|
||||
type CallGatewayBaseOptions = {
|
||||
url?: string;
|
||||
@@ -78,14 +82,6 @@ export type CallGatewayOptions = CallGatewayBaseOptions & {
|
||||
scopes?: OperatorScope[];
|
||||
};
|
||||
|
||||
export type GatewayConnectionDetails = {
|
||||
url: string;
|
||||
urlSource: string;
|
||||
bindDetail?: string;
|
||||
remoteFallbackNote?: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
const defaultCreateGatewayClient = (opts: GatewayClientOptions) => new GatewayClient(opts);
|
||||
const defaultGatewayCallDeps = {
|
||||
createGatewayClient: defaultCreateGatewayClient,
|
||||
@@ -124,6 +120,21 @@ function resolveGatewayPortValue(config?: OpenClawConfig, env?: NodeJS.ProcessEn
|
||||
return resolveGatewayPortFn(config, env);
|
||||
}
|
||||
|
||||
export function buildGatewayConnectionDetails(
|
||||
options: {
|
||||
config?: OpenClawConfig;
|
||||
url?: string;
|
||||
configPath?: string;
|
||||
urlSource?: "cli" | "env";
|
||||
} = {},
|
||||
): GatewayConnectionDetails {
|
||||
return buildGatewayConnectionDetailsWithResolvers(options, {
|
||||
loadConfig: () => gatewayCallDeps.loadConfig(),
|
||||
resolveConfigPath: (env) => resolveGatewayConfigPath(env),
|
||||
resolveGatewayPort: (config, env) => resolveGatewayPortValue(config, env),
|
||||
});
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
setDepsForTests(deps: Partial<typeof defaultGatewayCallDeps> | undefined): void {
|
||||
gatewayCallDeps.createGatewayClient =
|
||||
@@ -223,95 +234,6 @@ export function ensureExplicitGatewayAuth(params: {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
export function buildGatewayConnectionDetails(
|
||||
options: {
|
||||
config?: OpenClawConfig;
|
||||
url?: string;
|
||||
configPath?: string;
|
||||
urlSource?: "cli" | "env";
|
||||
} = {},
|
||||
): GatewayConnectionDetails {
|
||||
const config = options.config ?? gatewayCallDeps.loadConfig();
|
||||
const configPath = options.configPath ?? resolveGatewayConfigPath(process.env);
|
||||
const isRemoteMode = config.gateway?.mode === "remote";
|
||||
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
||||
const tlsEnabled = config.gateway?.tls?.enabled === true;
|
||||
const localPort = resolveGatewayPortValue(config);
|
||||
const bindMode = config.gateway?.bind ?? "loopback";
|
||||
const scheme = tlsEnabled ? "wss" : "ws";
|
||||
// Self-connections should always target loopback; bind mode only controls listener exposure.
|
||||
const localUrl = `${scheme}://127.0.0.1:${localPort}`;
|
||||
const cliUrlOverride =
|
||||
typeof options.url === "string" && options.url.trim().length > 0
|
||||
? options.url.trim()
|
||||
: undefined;
|
||||
const envUrlOverride = cliUrlOverride
|
||||
? undefined
|
||||
: trimToUndefined(process.env.OPENCLAW_GATEWAY_URL);
|
||||
const urlOverride = cliUrlOverride ?? envUrlOverride;
|
||||
const remoteUrl =
|
||||
typeof remote?.url === "string" && remote.url.trim().length > 0 ? remote.url.trim() : undefined;
|
||||
const remoteMisconfigured = isRemoteMode && !urlOverride && !remoteUrl;
|
||||
const urlSourceHint =
|
||||
options.urlSource ?? (cliUrlOverride ? "cli" : envUrlOverride ? "env" : undefined);
|
||||
const url = urlOverride || remoteUrl || localUrl;
|
||||
const urlSource = urlOverride
|
||||
? urlSourceHint === "env"
|
||||
? "env OPENCLAW_GATEWAY_URL"
|
||||
: "cli --url"
|
||||
: remoteUrl
|
||||
? "config gateway.remote.url"
|
||||
: remoteMisconfigured
|
||||
? "missing gateway.remote.url (fallback local)"
|
||||
: "local loopback";
|
||||
const bindDetail = !urlOverride && !remoteUrl ? `Bind: ${bindMode}` : undefined;
|
||||
const remoteFallbackNote = remoteMisconfigured
|
||||
? "Warn: gateway.mode=remote but gateway.remote.url is missing; set gateway.remote.url or switch gateway.mode=local."
|
||||
: undefined;
|
||||
|
||||
const allowPrivateWs = process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS === "1";
|
||||
// Security check: block ALL insecure ws:// to non-loopback addresses (CWE-319, CVSS 9.8)
|
||||
// This applies to the FINAL resolved URL, regardless of source (config, CLI override, etc).
|
||||
// Both credentials and chat/conversation data must not be transmitted over plaintext to remote hosts.
|
||||
if (!isSecureWebSocketUrl(url, { allowPrivateWs })) {
|
||||
throw new Error(
|
||||
[
|
||||
`SECURITY ERROR: Gateway URL "${url}" uses plaintext ws:// to a non-loopback address.`,
|
||||
"Both credentials and chat data would be exposed to network interception.",
|
||||
`Source: ${urlSource}`,
|
||||
`Config: ${configPath}`,
|
||||
"Fix: Use wss:// for remote gateway URLs.",
|
||||
"Safe remote access defaults:",
|
||||
"- keep gateway.bind=loopback and use an SSH tunnel (ssh -N -L 18789:127.0.0.1:18789 user@gateway-host)",
|
||||
"- or use Tailscale Serve/Funnel for HTTPS remote access",
|
||||
allowPrivateWs
|
||||
? undefined
|
||||
: "Break-glass (trusted private networks only): set OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1",
|
||||
"Doctor: openclaw doctor --fix",
|
||||
"Docs: https://docs.openclaw.ai/gateway/remote",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
const message = [
|
||||
`Gateway target: ${url}`,
|
||||
`Source: ${urlSource}`,
|
||||
`Config: ${configPath}`,
|
||||
bindDetail,
|
||||
remoteFallbackNote,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
return {
|
||||
url,
|
||||
urlSource,
|
||||
bindDetail,
|
||||
remoteFallbackNote,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
type GatewayRemoteSettings = {
|
||||
url?: string;
|
||||
token?: string;
|
||||
|
||||
112
src/gateway/connection-details.ts
Normal file
112
src/gateway/connection-details.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { resolveConfigPath, resolveGatewayPort } from "../config/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { isSecureWebSocketUrl } from "./net.js";
|
||||
|
||||
export type GatewayConnectionDetails = {
|
||||
url: string;
|
||||
urlSource: string;
|
||||
bindDetail?: string;
|
||||
remoteFallbackNote?: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type GatewayConnectionDetailResolvers = {
|
||||
loadConfig?: () => OpenClawConfig;
|
||||
resolveConfigPath?: (env: NodeJS.ProcessEnv) => string;
|
||||
resolveGatewayPort?: (cfg?: OpenClawConfig, env?: NodeJS.ProcessEnv) => number;
|
||||
};
|
||||
|
||||
function trimToUndefined(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
export function buildGatewayConnectionDetailsWithResolvers(
|
||||
options: {
|
||||
config?: OpenClawConfig;
|
||||
url?: string;
|
||||
configPath?: string;
|
||||
urlSource?: "cli" | "env";
|
||||
} = {},
|
||||
resolvers: GatewayConnectionDetailResolvers = {},
|
||||
): GatewayConnectionDetails {
|
||||
const config = options.config ?? resolvers.loadConfig?.() ?? {};
|
||||
const configPath =
|
||||
options.configPath ??
|
||||
resolvers.resolveConfigPath?.(process.env) ??
|
||||
resolveConfigPath(process.env);
|
||||
const isRemoteMode = config.gateway?.mode === "remote";
|
||||
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
||||
const tlsEnabled = config.gateway?.tls?.enabled === true;
|
||||
const localPort =
|
||||
resolvers.resolveGatewayPort?.(config, process.env) ?? resolveGatewayPort(config);
|
||||
const bindMode = config.gateway?.bind ?? "loopback";
|
||||
const scheme = tlsEnabled ? "wss" : "ws";
|
||||
const localUrl = `${scheme}://127.0.0.1:${localPort}`;
|
||||
const cliUrlOverride =
|
||||
typeof options.url === "string" && options.url.trim().length > 0
|
||||
? options.url.trim()
|
||||
: undefined;
|
||||
const envUrlOverride = cliUrlOverride
|
||||
? undefined
|
||||
: trimToUndefined(process.env.OPENCLAW_GATEWAY_URL);
|
||||
const urlOverride = cliUrlOverride ?? envUrlOverride;
|
||||
const remoteUrl =
|
||||
typeof remote?.url === "string" && remote.url.trim().length > 0 ? remote.url.trim() : undefined;
|
||||
const remoteMisconfigured = isRemoteMode && !urlOverride && !remoteUrl;
|
||||
const urlSourceHint =
|
||||
options.urlSource ?? (cliUrlOverride ? "cli" : envUrlOverride ? "env" : undefined);
|
||||
const url = urlOverride || remoteUrl || localUrl;
|
||||
const urlSource = urlOverride
|
||||
? urlSourceHint === "env"
|
||||
? "env OPENCLAW_GATEWAY_URL"
|
||||
: "cli --url"
|
||||
: remoteUrl
|
||||
? "config gateway.remote.url"
|
||||
: remoteMisconfigured
|
||||
? "missing gateway.remote.url (fallback local)"
|
||||
: "local loopback";
|
||||
const bindDetail = !urlOverride && !remoteUrl ? `Bind: ${bindMode}` : undefined;
|
||||
const remoteFallbackNote = remoteMisconfigured
|
||||
? "Warn: gateway.mode=remote but gateway.remote.url is missing; set gateway.remote.url or switch gateway.mode=local."
|
||||
: undefined;
|
||||
|
||||
const allowPrivateWs = process.env.OPENCLAW_ALLOW_INSECURE_PRIVATE_WS === "1";
|
||||
if (!isSecureWebSocketUrl(url, { allowPrivateWs })) {
|
||||
throw new Error(
|
||||
[
|
||||
`SECURITY ERROR: Gateway URL "${url}" uses plaintext ws:// to a non-loopback address.`,
|
||||
"Both credentials and chat data would be exposed to network interception.",
|
||||
`Source: ${urlSource}`,
|
||||
`Config: ${configPath}`,
|
||||
"Fix: Use wss:// for remote gateway URLs.",
|
||||
"Safe remote access defaults:",
|
||||
"- keep gateway.bind=loopback and use an SSH tunnel (ssh -N -L 18789:127.0.0.1:18789 user@gateway-host)",
|
||||
"- or use Tailscale Serve/Funnel for HTTPS remote access",
|
||||
allowPrivateWs
|
||||
? undefined
|
||||
: "Break-glass (trusted private networks only): set OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1",
|
||||
"Doctor: openclaw doctor --fix",
|
||||
"Docs: https://docs.openclaw.ai/gateway/remote",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
const message = [
|
||||
`Gateway target: ${url}`,
|
||||
`Source: ${urlSource}`,
|
||||
`Config: ${configPath}`,
|
||||
bindDetail,
|
||||
remoteFallbackNote,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
return {
|
||||
url,
|
||||
urlSource,
|
||||
bindDetail,
|
||||
remoteFallbackNote,
|
||||
message,
|
||||
};
|
||||
}
|
||||
@@ -105,6 +105,10 @@ export function getMemoryRuntime(): MemoryPluginRuntime | undefined {
|
||||
return memoryPluginState.runtime;
|
||||
}
|
||||
|
||||
export function hasMemoryRuntime(): boolean {
|
||||
return memoryPluginState.runtime !== undefined;
|
||||
}
|
||||
|
||||
export function restoreMemoryPluginState(state: MemoryPluginState): void {
|
||||
memoryPluginState.promptBuilder = state.promptBuilder;
|
||||
memoryPluginState.flushPlanResolver = state.flushPlanResolver;
|
||||
|
||||
Reference in New Issue
Block a user