mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(cli): keep offline gateway RPCs read-only (#125102)
Gateway health, suspend, and resume now preserve shared SQLite state when the target is unreachable. Related: #101290 Follow-up to #123674
This commit is contained in:
committed by
GitHub
parent
f77d2ec3a8
commit
a3eb8787ff
@@ -244,6 +244,13 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [
|
||||
route: { id: "gateway-status" },
|
||||
},
|
||||
{ commandPath: ["gateway", "call"], exact: true, policy: { networkProxy: "bypass" } },
|
||||
...["suspend", "resume"].map(
|
||||
(subcommand): CliCommandCatalogEntry => ({
|
||||
commandPath: ["gateway", subcommand],
|
||||
exact: true,
|
||||
policy: { configGuard: "validate", networkProxy: "bypass" },
|
||||
}),
|
||||
),
|
||||
{ commandPath: ["gateway", "diagnostics"], exact: true, policy: { networkProxy: "bypass" } },
|
||||
{ commandPath: ["gateway", "discover"], exact: true, policy: { networkProxy: "bypass" } },
|
||||
{
|
||||
|
||||
@@ -125,6 +125,15 @@ describe("command-path-policy", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps gateway suspension RPCs on non-observing config validation", () => {
|
||||
for (const subcommand of ["suspend", "resume"]) {
|
||||
expectResolvedPolicy(["gateway", subcommand], {
|
||||
configGuard: "validate",
|
||||
networkProxy: "bypass",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("applies exact overrides after broader channel plugin rules", () => {
|
||||
expectResolvedPolicy(["channels", "send"], {
|
||||
loadPlugins: "always",
|
||||
|
||||
@@ -81,6 +81,8 @@ describe("command-startup-policy", () => {
|
||||
["nodes", "remove"],
|
||||
["devices", "approve"],
|
||||
["devices", "remove"],
|
||||
["gateway", "suspend"],
|
||||
["gateway", "resume"],
|
||||
]) {
|
||||
expect(resolvePolicy({ commandPath })).toMatchObject({
|
||||
skipConfigGuard: false,
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
storeOriginDeviceToken,
|
||||
} from "../infra/device-auth-store.js";
|
||||
import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js";
|
||||
import { openNodeSqliteDatabase, resolveImmutableSqliteFileUri } from "../infra/node-sqlite.js";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import { getFreePort } from "../test-utils/ports.js";
|
||||
|
||||
@@ -31,6 +30,7 @@ const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
const execFileAsync = promisify(execFile);
|
||||
const activeChildren = new Set<ChildProcessWithoutNullStreams>();
|
||||
const activeServers = new Set<WebSocketServer>();
|
||||
const UNREACHABLE_GATEWAY_URL = "ws://127.0.0.1:9";
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
@@ -191,8 +191,6 @@ async function snapshotDirectoryContents(root: string): Promise<Record<string, s
|
||||
await visit(absolutePath);
|
||||
} else if (stat.isSymbolicLink()) {
|
||||
snapshot[relativePath] = `symlink:${await fs.readlink(absolutePath)}`;
|
||||
} else if (name === "openclaw.sqlite") {
|
||||
snapshot[relativePath] = "sqlite-database";
|
||||
} else {
|
||||
snapshot[relativePath] = `file:${createHash("sha256")
|
||||
.update(await fs.readFile(absolutePath))
|
||||
@@ -204,34 +202,80 @@ async function snapshotDirectoryContents(root: string): Promise<Record<string, s
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function snapshotSqliteTables(databasePath: string): Record<string, string[]> {
|
||||
const database = openNodeSqliteDatabase(resolveImmutableSqliteFileUri(databasePath), {
|
||||
readOnly: true,
|
||||
});
|
||||
async function snapshotSharedStateArtifacts(stateDir: string): Promise<Record<string, string>> {
|
||||
const sharedStateDir = path.join(stateDir, "state");
|
||||
try {
|
||||
const tables = database
|
||||
.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' ORDER BY name")
|
||||
.all() as Array<{ name: string }>;
|
||||
return Object.fromEntries(
|
||||
tables.map(({ name }) => {
|
||||
const quotedName = `"${name.replaceAll('"', '""')}"`;
|
||||
const rows = database
|
||||
.prepare(`SELECT * FROM ${quotedName}`)
|
||||
.all()
|
||||
.map((row) =>
|
||||
JSON.stringify(row, (_key, value: unknown) =>
|
||||
typeof value === "bigint" ? value.toString() : value,
|
||||
),
|
||||
)
|
||||
.toSorted();
|
||||
return [name, rows];
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
database.close();
|
||||
return await snapshotDirectoryContents(sharedStateDir);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareUnreachableGatewayCliFixture(params: {
|
||||
label: string;
|
||||
seeded: boolean;
|
||||
}): Promise<{ root: string; stateDir: string; configPath: string }> {
|
||||
const root = tempDirs.make(`openclaw-${params.label}-${params.seeded ? "seeded" : "absent"}-`);
|
||||
const stateDir = path.join(root, "state");
|
||||
const configPath = path.join(stateDir, "openclaw.json");
|
||||
await fs.mkdir(stateDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
auth: { mode: "none" },
|
||||
remote: { url: UNREACHABLE_GATEWAY_URL },
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (params.seeded) {
|
||||
const stateEnv = {
|
||||
...process.env,
|
||||
HOME: root,
|
||||
OPENCLAW_HOME: root,
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
};
|
||||
const identity = loadOrCreateDeviceIdentity({ env: stateEnv });
|
||||
storeOriginDeviceToken({
|
||||
gatewayScope: gatewayOriginScope(UNREACHABLE_GATEWAY_URL),
|
||||
deviceId: identity.deviceId,
|
||||
role: "operator",
|
||||
token: "stored-device-token",
|
||||
scopes: ["operator.admin"],
|
||||
env: stateEnv,
|
||||
});
|
||||
closeOpenClawStateDatabaseForTest();
|
||||
}
|
||||
return { root, stateDir, configPath };
|
||||
}
|
||||
|
||||
function expectUnreachableGatewayTransportFailure(
|
||||
result: Awaited<ReturnType<typeof runIsolatedGatewayCli>>,
|
||||
output: "json" | "text",
|
||||
): void {
|
||||
expect(result).toMatchObject({ code: 1, signal: null });
|
||||
if (output === "json") {
|
||||
expect(result.stderr).toBe("");
|
||||
expect(JSON.parse(result.stdout)).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
type: "gateway_transport_error",
|
||||
kind: "closed",
|
||||
message: expect.stringContaining("Gateway not reachable"),
|
||||
},
|
||||
gateway: { url: UNREACHABLE_GATEWAY_URL },
|
||||
});
|
||||
return;
|
||||
}
|
||||
expect(result.stderr).toContain("Gateway not reachable");
|
||||
expect(result.stderr).toContain(UNREACHABLE_GATEWAY_URL);
|
||||
expect(result.stderr).not.toContain("gateway timeout");
|
||||
}
|
||||
|
||||
async function runIsolatedGatewayCli(params: {
|
||||
args: string[];
|
||||
root: string;
|
||||
@@ -294,6 +338,55 @@ async function runIsolatedGatewayCli(params: {
|
||||
}
|
||||
|
||||
describe("gateway-backed CLI process exit", () => {
|
||||
it.each([
|
||||
{
|
||||
label: "root-health-json",
|
||||
args: ["health", "--json", "--timeout", "250"],
|
||||
output: "json" as const,
|
||||
},
|
||||
{
|
||||
label: "gateway-health-text",
|
||||
args: ["gateway", "health", "--timeout", "250"],
|
||||
output: "text" as const,
|
||||
},
|
||||
{
|
||||
label: "gateway-health-json",
|
||||
args: ["gateway", "health", "--json", "--timeout", "250"],
|
||||
output: "json" as const,
|
||||
},
|
||||
{
|
||||
label: "gateway-suspend-json",
|
||||
args: ["gateway", "suspend", "--json", "--timeout", "250"],
|
||||
output: "json" as const,
|
||||
},
|
||||
{
|
||||
label: "gateway-resume-json",
|
||||
args: ["gateway", "resume", "suspension-1", "--json", "--timeout", "250"],
|
||||
output: "json" as const,
|
||||
},
|
||||
])(
|
||||
"leaves shared state byte-identical after unreachable $label",
|
||||
async ({ label, args, output }) => {
|
||||
const absent = await prepareUnreachableGatewayCliFixture({ label, seeded: false });
|
||||
expect(await snapshotSharedStateArtifacts(absent.stateDir)).toEqual({});
|
||||
|
||||
const absentResult = await runIsolatedGatewayCli({ ...absent, args });
|
||||
|
||||
expectUnreachableGatewayTransportFailure(absentResult, output);
|
||||
expect(await snapshotSharedStateArtifacts(absent.stateDir)).toEqual({});
|
||||
|
||||
const seeded = await prepareUnreachableGatewayCliFixture({ label, seeded: true });
|
||||
const before = await snapshotSharedStateArtifacts(seeded.stateDir);
|
||||
expect(Object.keys(before)).toContain("openclaw.sqlite");
|
||||
|
||||
const seededResult = await runIsolatedGatewayCli({ ...seeded, args });
|
||||
|
||||
expectUnreachableGatewayTransportFailure(seededResult, output);
|
||||
expect(await snapshotSharedStateArtifacts(seeded.stateDir)).toEqual(before);
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
|
||||
it("dispatches node pairing mutations without opening the writable state database", async () => {
|
||||
const root = tempDirs.make("openclaw-node-pairing-cli-");
|
||||
const stateDir = path.join(root, "state");
|
||||
@@ -350,9 +443,7 @@ describe("gateway-backed CLI process exit", () => {
|
||||
env: stateEnv,
|
||||
});
|
||||
closeOpenClawStateDatabaseForTest();
|
||||
const databasePath = path.join(stateDir, "state", "openclaw.sqlite");
|
||||
const before = await snapshotDirectoryContents(stateDir);
|
||||
const beforeTables = snapshotSqliteTables(databasePath);
|
||||
|
||||
const result = await runIsolatedGatewayCli({
|
||||
args: ["nodes", "approve", "request-1", "--json"],
|
||||
@@ -365,7 +456,6 @@ describe("gateway-backed CLI process exit", () => {
|
||||
expect(JSON.parse(result.stdout)).toEqual({ approved: true });
|
||||
expect(gateway.calls).toEqual(["node.pair.list", "node.pair.approve"]);
|
||||
expect(await snapshotDirectoryContents(stateDir)).toEqual(before);
|
||||
expect(snapshotSqliteTables(databasePath)).toEqual(beforeTables);
|
||||
expect(
|
||||
loadOriginDeviceTokenReadOnly({
|
||||
gatewayScope: gatewayOriginScope(gateway.url),
|
||||
|
||||
@@ -16,7 +16,7 @@ describe("runGatewayHealthJsonRoute", () => {
|
||||
it("writes successful JSON without loading error-only dependencies", async () => {
|
||||
const runtime = createRuntime();
|
||||
const callGateway = vi.fn(async () => ({ ok: true, durationMs: 6 }));
|
||||
const readBestEffortConfig = vi.fn(async () => ({}));
|
||||
const readBestEffortHealthConfig = vi.fn(async () => ({}));
|
||||
const emitReachableGatewayAuthDiagnostic = vi.fn(async () => false);
|
||||
const formatGatewayAuthErrorJson = vi.fn();
|
||||
const formatGatewayClientRequestErrorJson = vi.fn();
|
||||
@@ -29,7 +29,7 @@ describe("runGatewayHealthJsonRoute", () => {
|
||||
runtime as never,
|
||||
{
|
||||
callGateway,
|
||||
readBestEffortConfig,
|
||||
readBestEffortHealthConfig,
|
||||
emitReachableGatewayAuthDiagnostic: emitReachableGatewayAuthDiagnostic as never,
|
||||
formatGatewayAuthErrorJson: formatGatewayAuthErrorJson as never,
|
||||
formatGatewayClientRequestErrorJson: formatGatewayClientRequestErrorJson as never,
|
||||
@@ -41,10 +41,10 @@ describe("runGatewayHealthJsonRoute", () => {
|
||||
"health",
|
||||
{ json: true, timeout: "10000" },
|
||||
undefined,
|
||||
{ defaultTimeoutMs: 10_000 },
|
||||
{ defaultTimeoutMs: 10_000, sharedStateMode: "read-only" },
|
||||
);
|
||||
expect(runtime.writeJson).toHaveBeenCalledWith({ ok: true, durationMs: 6 }, 2);
|
||||
expect(readBestEffortConfig).not.toHaveBeenCalled();
|
||||
expect(readBestEffortHealthConfig).not.toHaveBeenCalled();
|
||||
expect(emitReachableGatewayAuthDiagnostic).not.toHaveBeenCalled();
|
||||
expect(formatGatewayAuthErrorJson).not.toHaveBeenCalled();
|
||||
expect(formatGatewayClientRequestErrorJson).not.toHaveBeenCalled();
|
||||
@@ -54,7 +54,7 @@ describe("runGatewayHealthJsonRoute", () => {
|
||||
it("projects a local port into the routed config", async () => {
|
||||
const runtime = createRuntime();
|
||||
const callGateway = vi.fn(async () => ({ ok: true }));
|
||||
const readBestEffortConfig = vi.fn(async () => ({
|
||||
const readBestEffortHealthConfig = vi.fn(async () => ({
|
||||
gateway: { auth: { mode: "token" as const } },
|
||||
}));
|
||||
|
||||
@@ -64,7 +64,7 @@ describe("runGatewayHealthJsonRoute", () => {
|
||||
localPortOverride: 19083,
|
||||
},
|
||||
runtime as never,
|
||||
{ callGateway, readBestEffortConfig },
|
||||
{ callGateway, readBestEffortHealthConfig },
|
||||
);
|
||||
|
||||
expect(callGateway).toHaveBeenCalledWith(
|
||||
@@ -76,7 +76,7 @@ describe("runGatewayHealthJsonRoute", () => {
|
||||
},
|
||||
}),
|
||||
undefined,
|
||||
{ defaultTimeoutMs: 10_000 },
|
||||
{ defaultTimeoutMs: 10_000, sharedStateMode: "read-only" },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@ describe("runGatewayHealthJsonRoute", () => {
|
||||
runtime as never,
|
||||
{
|
||||
callGateway,
|
||||
readBestEffortConfig: vi.fn(async () => {
|
||||
readBestEffortHealthConfig: vi.fn(async () => {
|
||||
throw error;
|
||||
}),
|
||||
},
|
||||
@@ -115,7 +115,7 @@ describe("runGatewayHealthJsonRoute", () => {
|
||||
|
||||
await runGatewayHealthJsonRoute({ rpc: { json: true, timeout: "10000" } }, runtime as never, {
|
||||
callGateway,
|
||||
readBestEffortConfig: async () => ({}),
|
||||
readBestEffortHealthConfig: async () => ({}),
|
||||
emitReachableGatewayAuthDiagnostic: vi.fn(async () => false) as never,
|
||||
formatGatewayAuthErrorJson: vi.fn(() => null) as never,
|
||||
formatGatewayClientRequestErrorJson: vi.fn(() => null) as never,
|
||||
@@ -145,7 +145,7 @@ describe("runGatewayHealthJsonRoute", () => {
|
||||
|
||||
await runGatewayHealthJsonRoute({ rpc: { json: true, timeout: "10000" } }, runtime as never, {
|
||||
callGateway,
|
||||
readBestEffortConfig: async () => ({}),
|
||||
readBestEffortHealthConfig: async () => ({}),
|
||||
emitReachableGatewayAuthDiagnostic: vi.fn(async () => false) as never,
|
||||
formatGatewayAuthErrorJson: formatGatewayAuthErrorJson as never,
|
||||
formatGatewayClientRequestErrorJson: formatGatewayClientRequestErrorJson as never,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Route-first machine-readable Gateway health command.
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
|
||||
|
||||
@@ -14,7 +13,7 @@ type GatewayHealthJsonRouteArgs = {
|
||||
|
||||
type GatewayHealthRouteDependencies = {
|
||||
callGateway?: typeof import("../gateway-rpc.js").callGatewayFromCliWithTransport;
|
||||
readBestEffortConfig?: () => Promise<OpenClawConfig>;
|
||||
readBestEffortHealthConfig?: typeof import("../../commands/health.js").readBestEffortHealthConfig;
|
||||
emitReachableGatewayAuthDiagnostic?: typeof import("../../commands/health.js").emitReachableGatewayAuthDiagnostic;
|
||||
formatGatewayAuthErrorJson?: typeof import("../../gateway/call.js").formatGatewayAuthErrorJson;
|
||||
formatGatewayClientRequestErrorJson?: typeof import("../../gateway/call.js").formatGatewayClientRequestErrorJson;
|
||||
@@ -28,10 +27,10 @@ async function resolveRouteRpcOptions(
|
||||
if (args.localPortOverride === undefined) {
|
||||
return args.rpc;
|
||||
}
|
||||
const readBestEffortConfig =
|
||||
deps.readBestEffortConfig ??
|
||||
(await import("../../config/read-best-effort-config.runtime.js")).readBestEffortConfig;
|
||||
const config = await readBestEffortConfig();
|
||||
const readBestEffortHealthConfig =
|
||||
deps.readBestEffortHealthConfig ??
|
||||
(await import("../../commands/health.js")).readBestEffortHealthConfig;
|
||||
const config = await readBestEffortHealthConfig();
|
||||
return {
|
||||
...args.rpc,
|
||||
localPortOverride: args.localPortOverride,
|
||||
@@ -59,7 +58,10 @@ export async function runGatewayHealthJsonRoute(
|
||||
deps.callGateway ?? (await import("../gateway-rpc.js")).callGatewayFromCliWithTransport;
|
||||
writeRuntimeJson(
|
||||
runtime,
|
||||
await callGateway("health", rpc, undefined, { defaultTimeoutMs: 10_000 }),
|
||||
await callGateway("health", rpc, undefined, {
|
||||
defaultTimeoutMs: 10_000,
|
||||
sharedStateMode: "read-only",
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!rpc) {
|
||||
@@ -67,11 +69,10 @@ export async function runGatewayHealthJsonRoute(
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
const [healthModule, configModule, callModule] = await Promise.all([
|
||||
deps.emitReachableGatewayAuthDiagnostic ? undefined : import("../../commands/health.js"),
|
||||
deps.readBestEffortConfig
|
||||
const [healthModule, callModule] = await Promise.all([
|
||||
deps.emitReachableGatewayAuthDiagnostic && deps.readBestEffortHealthConfig
|
||||
? undefined
|
||||
: import("../../config/read-best-effort-config.runtime.js"),
|
||||
: import("../../commands/health.js"),
|
||||
deps.formatGatewayAuthErrorJson &&
|
||||
deps.formatGatewayClientRequestErrorJson &&
|
||||
deps.formatGatewayTransportErrorJson
|
||||
@@ -80,13 +81,14 @@ export async function runGatewayHealthJsonRoute(
|
||||
]);
|
||||
const emitReachableGatewayAuthDiagnostic =
|
||||
deps.emitReachableGatewayAuthDiagnostic ?? healthModule?.emitReachableGatewayAuthDiagnostic;
|
||||
const readBestEffortConfig = deps.readBestEffortConfig ?? configModule?.readBestEffortConfig;
|
||||
if (!emitReachableGatewayAuthDiagnostic || !readBestEffortConfig) {
|
||||
const readBestEffortHealthConfig =
|
||||
deps.readBestEffortHealthConfig ?? healthModule?.readBestEffortHealthConfig;
|
||||
if (!emitReachableGatewayAuthDiagnostic || !readBestEffortHealthConfig) {
|
||||
throw error;
|
||||
}
|
||||
const handled = await emitReachableGatewayAuthDiagnostic({
|
||||
error,
|
||||
config: rpc.config ?? (await readBestEffortConfig()),
|
||||
config: rpc.config ?? (await readBestEffortHealthConfig()),
|
||||
runtime,
|
||||
timeoutMs: Number(rpc.timeout ?? "10000"),
|
||||
token: rpc.token,
|
||||
|
||||
@@ -85,6 +85,7 @@ vi.mock("../../commands/health.js", () => ({
|
||||
emitReachableGatewayAuthDiagnostic: (params: unknown) =>
|
||||
mocks.emitReachableGatewayAuthDiagnostic(params),
|
||||
formatHealthChannelLines: () => mocks.formatHealthChannelLines(),
|
||||
readBestEffortHealthConfig: async () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("../../config/read-best-effort-config.runtime.js", () => ({
|
||||
|
||||
@@ -116,6 +116,7 @@ function gatewayCallOpts(cmd: Command, defaultTimeoutMs = DEFAULT_GATEWAY_RPC_TI
|
||||
async function callGatewayCli(method: string, opts: GatewayRpcOpts, params?: unknown) {
|
||||
return await callGatewayFromCliWithTransport(method, opts, params, {
|
||||
defaultTimeoutMs: DEFAULT_GATEWAY_RPC_TIMEOUT_MS,
|
||||
sharedStateMode: "read-only",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -696,14 +697,12 @@ export function registerGatewayCli(program: Command, deps: GatewayCliDependencie
|
||||
try {
|
||||
result = await callGatewayCli("health", rpcOpts);
|
||||
} catch (error) {
|
||||
const [{ emitReachableGatewayAuthDiagnostic }, { readBestEffortConfig }] =
|
||||
await Promise.all([
|
||||
(deps.loadGatewayHealthModule ?? loadGatewayHealthModule)(),
|
||||
loadConfigModule(),
|
||||
]);
|
||||
const { emitReachableGatewayAuthDiagnostic, readBestEffortHealthConfig } = await (
|
||||
deps.loadGatewayHealthModule ?? loadGatewayHealthModule
|
||||
)();
|
||||
const handled = await emitReachableGatewayAuthDiagnostic({
|
||||
error,
|
||||
config: rpcOpts.config ?? (await readBestEffortConfig()),
|
||||
config: rpcOpts.config ?? (await readBestEffortHealthConfig()),
|
||||
runtime: defaultRuntime,
|
||||
timeoutMs: parseGatewayRpcTimeoutOption(rpcOpts.timeout),
|
||||
token: rpcOpts.token,
|
||||
|
||||
@@ -436,6 +436,7 @@ describe("healthCommand", () => {
|
||||
expect(gatewayRequest.token).toBe("setup-token");
|
||||
expect(gatewayRequest.password).toBe("setup-password");
|
||||
expect(gatewayRequest.ignoreEnvUrlOverride).toBe(true);
|
||||
expect(gatewayRequest.sharedStateMode).toBe("read-only");
|
||||
});
|
||||
|
||||
it("outputs JSON for gateway transport failures in JSON mode", async () => {
|
||||
|
||||
@@ -267,6 +267,7 @@ export async function healthCommand(
|
||||
config: cfg,
|
||||
token: opts.token,
|
||||
password: opts.password,
|
||||
sharedStateMode: "read-only",
|
||||
ignoreEnvUrlOverride: opts.ignoreEnvUrlOverride,
|
||||
localPortOverride: opts.localPortOverride,
|
||||
}),
|
||||
@@ -562,7 +563,11 @@ export async function healthCommand(
|
||||
}
|
||||
}
|
||||
|
||||
async function readBestEffortHealthConfig(): Promise<OpenClawConfig> {
|
||||
const { readBestEffortConfig } = await loadConfigRuntime();
|
||||
return await readBestEffortConfig();
|
||||
export async function readBestEffortHealthConfig(): Promise<OpenClawConfig> {
|
||||
const { readConfigFileSnapshot } = await loadConfigRuntime();
|
||||
const snapshot = await readConfigFileSnapshot({
|
||||
observe: false,
|
||||
pluginValidation: "core-only",
|
||||
});
|
||||
return snapshot.runtimeConfig ?? snapshot.config;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user