mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(crabbox): surface invalid cloud worker profiles (#125991)
This commit is contained in:
committed by
GitHub
parent
f97a133254
commit
99acca309c
@@ -1,5 +1,4 @@
|
||||
import path from "node:path";
|
||||
import type { HealthCheck } from "openclaw/plugin-sdk/health";
|
||||
import type { WorkerProfile, WorkerProvider } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import * as processRuntime from "openclaw/plugin-sdk/process-runtime";
|
||||
import type { SpawnResult } from "openclaw/plugin-sdk/process-runtime";
|
||||
@@ -11,10 +10,6 @@ import {
|
||||
resolveCrabboxBinary,
|
||||
} from "./crabbox-worker-profile.js";
|
||||
import { createCrabboxWorkerProvider, resolveOpenClawRoot } from "./crabbox-worker-provider.js";
|
||||
import {
|
||||
CRABBOX_CLOUD_WORKER_PROFILE_CHECK_ID,
|
||||
registerCrabboxWorkerProviderDoctorChecks,
|
||||
} from "./doctor.js";
|
||||
|
||||
const OPERATION_ID = `provision:v2:${"0".repeat(64)}`;
|
||||
const LEASE_ID = "cbx_6071fc2062a6";
|
||||
@@ -893,6 +888,9 @@ describe("Crabbox worker provider", () => {
|
||||
expect(String(calls[2]?.options.input)).toContain(
|
||||
'npx --yes --package "$package_spec" -- openclaw',
|
||||
);
|
||||
expect(String(calls[2]?.options.input)).toContain(
|
||||
"OpenClaw worker bootstrap could not install Gateway version 2026.8.1",
|
||||
);
|
||||
expect(String(calls[2]?.options.input)).toContain(
|
||||
'connect --target-file "$setup_code_file" --ephemeral',
|
||||
);
|
||||
@@ -1715,113 +1713,6 @@ describe("Crabbox binary resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function captureCrabboxDoctorCheck(): HealthCheck {
|
||||
let check: HealthCheck | undefined;
|
||||
registerCrabboxWorkerProviderDoctorChecks({
|
||||
openclawRoot: OPENCLAW_ROOT,
|
||||
registerHealthCheck(value) {
|
||||
check = value;
|
||||
},
|
||||
});
|
||||
if (!check) {
|
||||
throw new Error("Crabbox doctor check was not registered");
|
||||
}
|
||||
return check;
|
||||
}
|
||||
|
||||
describe("Crabbox worker doctor", () => {
|
||||
it("reports a configured non-executable binary with a profile-specific repair", async () => {
|
||||
const probe = vi.spyOn(doctorRuntime, "probeCrabboxVersion");
|
||||
const binary = path.resolve(path.sep, "nonexistent", "crabbox");
|
||||
try {
|
||||
await expect(
|
||||
captureCrabboxDoctorCheck().detect({
|
||||
cfg: {
|
||||
cloudWorkers: {
|
||||
profiles: {
|
||||
aws: { provider: "crabbox", settings: { binary } },
|
||||
},
|
||||
},
|
||||
},
|
||||
env: { PATH: "" },
|
||||
} as never),
|
||||
).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: CRABBOX_CLOUD_WORKER_PROFILE_CHECK_ID,
|
||||
severity: "warning",
|
||||
message: expect.stringContaining('profile "aws"'),
|
||||
path: binary,
|
||||
target: "aws",
|
||||
fixHint: expect.stringContaining("cloudWorkers.profiles.aws.settings.binary"),
|
||||
}),
|
||||
]);
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
probe.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("emits no finding for a supported configured binary", async () => {
|
||||
const probe = vi
|
||||
.spyOn(doctorRuntime, "probeCrabboxVersion")
|
||||
.mockResolvedValue({ status: "supported", version: "0.41.6" });
|
||||
try {
|
||||
await expect(
|
||||
captureCrabboxDoctorCheck().detect({
|
||||
cfg: {
|
||||
cloudWorkers: {
|
||||
profiles: {
|
||||
aws: { provider: "crabbox", settings: { binary: process.execPath } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never),
|
||||
).resolves.toEqual([]);
|
||||
expect(probe).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
probe.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("reports an indeterminate version probe without asserting failure", async () => {
|
||||
const probe = vi.spyOn(doctorRuntime, "probeCrabboxVersion").mockResolvedValue({
|
||||
status: "indeterminate",
|
||||
reason: "version command timed out after 2000 ms",
|
||||
});
|
||||
try {
|
||||
await expect(
|
||||
captureCrabboxDoctorCheck().detect({
|
||||
cfg: {
|
||||
cloudWorkers: {
|
||||
profiles: {
|
||||
aws: { provider: "crabbox", settings: { binary: process.execPath } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never),
|
||||
).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
severity: "info",
|
||||
message: expect.stringContaining("could not determine its version"),
|
||||
fixHint: expect.stringContaining(`${process.execPath} --version`),
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
probe.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not probe when no Crabbox cloud worker profile is configured", async () => {
|
||||
const probe = vi.spyOn(doctorRuntime, "probeCrabboxVersion");
|
||||
try {
|
||||
await expect(captureCrabboxDoctorCheck().detect({ cfg: {} } as never)).resolves.toEqual([]);
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
probe.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Crabbox version probe", () => {
|
||||
it.each([
|
||||
{ output: "0.41.1\n", expected: { status: "supported", version: "0.41.1" } },
|
||||
|
||||
@@ -289,7 +289,12 @@ function nodeEnrollmentSetupCommand(params: {
|
||||
" fi",
|
||||
" done",
|
||||
"fi",
|
||||
'test -s "$package_spec_file"',
|
||||
'if [ ! -s "$package_spec_file" ]; then',
|
||||
` printf "%s\\n" ${shellQuote(
|
||||
`OpenClaw worker bootstrap could not install Gateway version ${enrollment.openclawVersion}; for an unreleased Gateway build, cloudWorkers profile setup must install that exact version globally before enrollment.`,
|
||||
)} >&2`,
|
||||
" exit 1",
|
||||
"fi",
|
||||
'package_spec="$(cat "$package_spec_file")"',
|
||||
'if [ "$package_spec" = "@global" ]; then',
|
||||
` setsid -f sh -c 'printf "%s\\n" "$$" >"$1"; shift; exec "$@"' sh "$pid_file" env OPENCLAW_STATE_DIR="$state_dir" openclaw ${launch} >"$state_dir/node.log" 2>&1 </dev/null`,
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import path from "node:path";
|
||||
import type { HealthCheck } from "openclaw/plugin-sdk/health";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import * as doctorRuntime from "./crabbox-worker-doctor-runtime.js";
|
||||
import {
|
||||
CRABBOX_CLOUD_WORKER_PROFILE_CHECK_ID,
|
||||
registerCrabboxWorkerProviderDoctorChecks,
|
||||
} from "./doctor.js";
|
||||
|
||||
const OPENCLAW_ROOT = path.resolve(path.sep, "workspace", "openclaw");
|
||||
|
||||
function captureCrabboxDoctorCheck(): HealthCheck {
|
||||
let check: HealthCheck | undefined;
|
||||
registerCrabboxWorkerProviderDoctorChecks({
|
||||
openclawRoot: OPENCLAW_ROOT,
|
||||
registerHealthCheck(value) {
|
||||
check = value;
|
||||
},
|
||||
});
|
||||
if (!check) {
|
||||
throw new Error("Crabbox doctor check was not registered");
|
||||
}
|
||||
return check;
|
||||
}
|
||||
|
||||
describe("Crabbox worker doctor", () => {
|
||||
it("reports a configured non-executable binary with a profile-specific repair", async () => {
|
||||
const probe = vi.spyOn(doctorRuntime, "probeCrabboxVersion");
|
||||
const binary = path.resolve(path.sep, "nonexistent", "crabbox");
|
||||
try {
|
||||
await expect(
|
||||
captureCrabboxDoctorCheck().detect({
|
||||
cfg: {
|
||||
cloudWorkers: {
|
||||
profiles: {
|
||||
aws: { provider: "crabbox", settings: { binary } },
|
||||
},
|
||||
},
|
||||
},
|
||||
env: { PATH: "" },
|
||||
} as never),
|
||||
).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: CRABBOX_CLOUD_WORKER_PROFILE_CHECK_ID,
|
||||
severity: "warning",
|
||||
message: expect.stringContaining('profile "aws"'),
|
||||
path: binary,
|
||||
target: "aws",
|
||||
fixHint: expect.stringContaining("cloudWorkers.profiles.aws.settings.binary"),
|
||||
}),
|
||||
]);
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
probe.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("emits no finding for a supported configured binary", async () => {
|
||||
const probe = vi
|
||||
.spyOn(doctorRuntime, "probeCrabboxVersion")
|
||||
.mockResolvedValue({ status: "supported", version: "0.41.6" });
|
||||
try {
|
||||
await expect(
|
||||
captureCrabboxDoctorCheck().detect({
|
||||
cfg: {
|
||||
cloudWorkers: {
|
||||
profiles: {
|
||||
aws: { provider: "crabbox", settings: { binary: process.execPath } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never),
|
||||
).resolves.toEqual([]);
|
||||
expect(probe).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
probe.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("reports an indeterminate version probe without asserting failure", async () => {
|
||||
const probe = vi.spyOn(doctorRuntime, "probeCrabboxVersion").mockResolvedValue({
|
||||
status: "indeterminate",
|
||||
reason: "version command timed out after 2000 ms",
|
||||
});
|
||||
try {
|
||||
await expect(
|
||||
captureCrabboxDoctorCheck().detect({
|
||||
cfg: {
|
||||
cloudWorkers: {
|
||||
profiles: {
|
||||
aws: { provider: "crabbox", settings: { binary: process.execPath } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never),
|
||||
).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
severity: "info",
|
||||
message: expect.stringContaining("could not determine its version"),
|
||||
fixHint: expect.stringContaining(`${process.execPath} --version`),
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
probe.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("detects and removes only stale Crabbox desktop settings", async () => {
|
||||
const probe = vi
|
||||
.spyOn(doctorRuntime, "probeCrabboxVersion")
|
||||
.mockResolvedValue({ status: "supported", version: "0.41.6" });
|
||||
const cfg = {
|
||||
cloudWorkers: {
|
||||
desktop: true,
|
||||
profiles: {
|
||||
aws: {
|
||||
provider: "crabbox",
|
||||
install: "npm",
|
||||
settings: { binary: process.execPath, class: "fast", desktop: true, ttl: "12h" },
|
||||
},
|
||||
secondary: {
|
||||
provider: " CRABBOX ",
|
||||
settings: { binary: process.execPath, desktop: true, idleTimeout: "30m" },
|
||||
},
|
||||
disabled: {
|
||||
provider: "crabbox",
|
||||
settings: { binary: process.execPath, desktop: false },
|
||||
},
|
||||
absent: { provider: "crabbox", settings: { binary: process.execPath } },
|
||||
malformed: {
|
||||
provider: "crabbox",
|
||||
settings: { binary: process.execPath, desktop: "true" },
|
||||
},
|
||||
other: { provider: "device", settings: { desktop: true } },
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
const before = structuredClone(cfg);
|
||||
const check = captureCrabboxDoctorCheck();
|
||||
try {
|
||||
const findings = await check.detect({ cfg } as never);
|
||||
expect(findings).toEqual([
|
||||
expect.objectContaining({
|
||||
checkId: CRABBOX_CLOUD_WORKER_PROFILE_CHECK_ID,
|
||||
severity: "warning",
|
||||
message: expect.stringContaining("node transport no longer supports desktop profiles"),
|
||||
ocPath: "cloudWorkers.profiles.aws.settings.desktop",
|
||||
target: "aws",
|
||||
fixHint: expect.stringContaining("openclaw doctor --fix"),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ocPath: "cloudWorkers.profiles.secondary.settings.desktop",
|
||||
target: "secondary",
|
||||
}),
|
||||
]);
|
||||
|
||||
const repaired = await check.repair!({ cfg, dryRun: true } as never, findings);
|
||||
expect(cfg).toEqual(before);
|
||||
expect(repaired.config).toEqual({
|
||||
cloudWorkers: {
|
||||
desktop: true,
|
||||
profiles: {
|
||||
aws: {
|
||||
provider: "crabbox",
|
||||
install: "npm",
|
||||
settings: { binary: process.execPath, class: "fast", ttl: "12h" },
|
||||
},
|
||||
secondary: {
|
||||
provider: " CRABBOX ",
|
||||
settings: { binary: process.execPath, idleTimeout: "30m" },
|
||||
},
|
||||
disabled: {
|
||||
provider: "crabbox",
|
||||
settings: { binary: process.execPath, desktop: false },
|
||||
},
|
||||
absent: { provider: "crabbox", settings: { binary: process.execPath } },
|
||||
malformed: {
|
||||
provider: "crabbox",
|
||||
settings: { binary: process.execPath, desktop: "true" },
|
||||
},
|
||||
other: { provider: "device", settings: { desktop: true } },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(repaired.changes).toHaveLength(2);
|
||||
|
||||
await expect(check.repair!({ cfg: repaired.config } as never, findings)).resolves.toEqual({
|
||||
config: repaired.config,
|
||||
changes: [],
|
||||
});
|
||||
} finally {
|
||||
probe.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not probe when no Crabbox cloud worker profile is configured", async () => {
|
||||
const probe = vi.spyOn(doctorRuntime, "probeCrabboxVersion");
|
||||
try {
|
||||
await expect(captureCrabboxDoctorCheck().detect({ cfg: {} } as never)).resolves.toEqual([]);
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
probe.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -44,7 +44,7 @@ function createCrabboxCloudWorkerProfileCheck(openclawRoot: string): HealthCheck
|
||||
return {
|
||||
id: CRABBOX_CLOUD_WORKER_PROFILE_CHECK_ID,
|
||||
kind: "plugin",
|
||||
description: "Verify configured Crabbox cloud worker binaries before dispatch.",
|
||||
description: "Verify configured Crabbox cloud worker profiles before dispatch.",
|
||||
source: "crabbox",
|
||||
async detect(ctx) {
|
||||
const profiles = Object.entries(ctx.cfg.cloudWorkers?.profiles ?? {}).filter(
|
||||
@@ -56,7 +56,21 @@ function createCrabboxCloudWorkerProfileCheck(openclawRoot: string): HealthCheck
|
||||
const probes = new Map<string, ReturnType<typeof doctorRuntime.probeCrabboxVersion>>();
|
||||
const findings: HealthFinding[] = [];
|
||||
for (const [profileId, profile] of profiles) {
|
||||
const explicitBinary = nonEmptyString(readRecord(profile.settings)?.binary);
|
||||
const settings = readRecord(profile.settings);
|
||||
if (settings?.desktop === true) {
|
||||
const configPath = `cloudWorkers.profiles.${profileId}.settings.desktop`;
|
||||
findings.push({
|
||||
checkId: CRABBOX_CLOUD_WORKER_PROFILE_CHECK_ID,
|
||||
severity: "warning",
|
||||
source: "crabbox",
|
||||
message: `Cloud worker profile "${profileId}" requests a desktop, but Crabbox node transport no longer supports desktop profiles.`,
|
||||
ocPath: configPath,
|
||||
target: profileId,
|
||||
requirement: "a Crabbox cloud worker profile without desktop mode",
|
||||
fixHint: `Run \`openclaw doctor --fix\` to remove the stale ${configPath} setting.`,
|
||||
});
|
||||
}
|
||||
const explicitBinary = nonEmptyString(settings?.binary);
|
||||
const binary = findCrabboxBinary({
|
||||
...(explicitBinary ? { explicit: explicitBinary } : {}),
|
||||
openclawRoot,
|
||||
@@ -104,6 +118,43 @@ function createCrabboxCloudWorkerProfileCheck(openclawRoot: string): HealthCheck
|
||||
}
|
||||
return findings;
|
||||
},
|
||||
async repair(ctx, findings) {
|
||||
const profileIds = new Set(
|
||||
findings.flatMap((entry) => {
|
||||
const profileId = entry.target;
|
||||
return entry.checkId === CRABBOX_CLOUD_WORKER_PROFILE_CHECK_ID &&
|
||||
profileId &&
|
||||
entry.ocPath === `cloudWorkers.profiles.${profileId}.settings.desktop`
|
||||
? [profileId]
|
||||
: [];
|
||||
}),
|
||||
);
|
||||
const staleProfileIds = [...profileIds].filter((profileId) => {
|
||||
const profile = ctx.cfg.cloudWorkers?.profiles?.[profileId];
|
||||
return (
|
||||
profile?.provider.trim().toLowerCase() === CRABBOX_WORKER_PROVIDER_ID &&
|
||||
readRecord(profile.settings)?.desktop === true
|
||||
);
|
||||
});
|
||||
if (staleProfileIds.length === 0) {
|
||||
return { config: ctx.cfg, changes: [] };
|
||||
}
|
||||
|
||||
const config = structuredClone(ctx.cfg);
|
||||
for (const profileId of staleProfileIds) {
|
||||
const settings = readRecord(config.cloudWorkers?.profiles?.[profileId]?.settings);
|
||||
if (settings) {
|
||||
delete settings.desktop;
|
||||
}
|
||||
}
|
||||
return {
|
||||
config,
|
||||
changes: staleProfileIds.map(
|
||||
(profileId) =>
|
||||
`Removed stale cloudWorkers.profiles.${profileId}.settings.desktop from Crabbox profile "${profileId}".`,
|
||||
),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user