mirror of https://github.com/openclaw/openclaw.git
fix: stabilize macos daemon onboarding
This commit is contained in:
parent
0a3b9a9a09
commit
80e7da92ce
|
|
@ -3,7 +3,6 @@
|
||||||
Docs: https://docs.openclaw.ai
|
Docs: https://docs.openclaw.ai
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
- Android/chat settings: redesign the chat settings sheet with grouped device and media sections, refresh the Connect and Voice tabs, and tighten the chat composer/session header for a denser mobile layout. (#44894) Thanks @obviyus.
|
- Android/chat settings: redesign the chat settings sheet with grouped device and media sections, refresh the Connect and Voice tabs, and tighten the chat composer/session header for a denser mobile layout. (#44894) Thanks @obviyus.
|
||||||
|
|
@ -28,7 +27,7 @@ Docs: https://docs.openclaw.ai
|
||||||
- Security/exec approvals: unwrap more `pnpm` runtime forms during approval binding, including `pnpm --reporter ... exec` and direct `pnpm node` file runs, with matching regression coverage and docs updates.
|
- Security/exec approvals: unwrap more `pnpm` runtime forms during approval binding, including `pnpm --reporter ... exec` and direct `pnpm node` file runs, with matching regression coverage and docs updates.
|
||||||
- Security/exec approvals: fail closed for Perl `-M` and `-I` approval flows so preload and load-path module resolution stays outside approval-backed runtime execution unless the operator uses a broader explicit trust path.
|
- Security/exec approvals: fail closed for Perl `-M` and `-I` approval flows so preload and load-path module resolution stays outside approval-backed runtime execution unless the operator uses a broader explicit trust path.
|
||||||
- Control UI/insecure auth: preserve explicit shared token and password auth on plain-HTTP Control UI connects so LAN and reverse-proxy sessions no longer drop shared auth before the first WebSocket handshake. (#45088) Thanks @velvet-shark.
|
- Control UI/insecure auth: preserve explicit shared token and password auth on plain-HTTP Control UI connects so LAN and reverse-proxy sessions no longer drop shared auth before the first WebSocket handshake. (#45088) Thanks @velvet-shark.
|
||||||
|
- macOS/onboarding: avoid self-restarting freshly bootstrapped launchd gateways and give new daemon installs longer to become healthy, so `openclaw onboard --install-daemon` no longer false-fails on slower Macs and fresh VM snapshots.
|
||||||
## 2026.3.12
|
## 2026.3.12
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,9 @@ const gatewayClientCalls: Array<{
|
||||||
onClose?: (code: number, reason: string) => void;
|
onClose?: (code: number, reason: string) => void;
|
||||||
}> = [];
|
}> = [];
|
||||||
const ensureWorkspaceAndSessionsMock = vi.fn(async (..._args: unknown[]) => {});
|
const ensureWorkspaceAndSessionsMock = vi.fn(async (..._args: unknown[]) => {});
|
||||||
|
const installGatewayDaemonNonInteractiveMock = vi.hoisted(() => vi.fn(async () => {}));
|
||||||
let waitForGatewayReachableMock:
|
let waitForGatewayReachableMock:
|
||||||
| ((params: { url: string; token?: string; password?: string }) => Promise<{
|
| ((params: { url: string; token?: string; password?: string; deadlineMs?: number }) => Promise<{
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
detail?: string;
|
detail?: string;
|
||||||
}>)
|
}>)
|
||||||
|
|
@ -59,6 +60,10 @@ vi.mock("./onboard-helpers.js", async (importOriginal) => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("./onboard-non-interactive/local/daemon-install.js", () => ({
|
||||||
|
installGatewayDaemonNonInteractive: installGatewayDaemonNonInteractiveMock,
|
||||||
|
}));
|
||||||
|
|
||||||
const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
|
const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
|
||||||
const { resolveConfigPath: resolveStateConfigPath } = await import("../config/paths.js");
|
const { resolveConfigPath: resolveStateConfigPath } = await import("../config/paths.js");
|
||||||
const { resolveConfigPath } = await import("../config/config.js");
|
const { resolveConfigPath } = await import("../config/config.js");
|
||||||
|
|
@ -128,6 +133,7 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
waitForGatewayReachableMock = undefined;
|
waitForGatewayReachableMock = undefined;
|
||||||
|
installGatewayDaemonNonInteractiveMock.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("writes gateway token auth into config", async () => {
|
it("writes gateway token auth into config", async () => {
|
||||||
|
|
@ -343,6 +349,33 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
|
||||||
});
|
});
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
|
|
||||||
|
it("uses a longer health deadline when daemon install was requested", async () => {
|
||||||
|
await withStateDir("state-local-daemon-health-", async (stateDir) => {
|
||||||
|
let capturedDeadlineMs: number | undefined;
|
||||||
|
waitForGatewayReachableMock = vi.fn(async (params: { deadlineMs?: number }) => {
|
||||||
|
capturedDeadlineMs = params.deadlineMs;
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
await runNonInteractiveOnboarding(
|
||||||
|
{
|
||||||
|
nonInteractive: true,
|
||||||
|
mode: "local",
|
||||||
|
workspace: path.join(stateDir, "openclaw"),
|
||||||
|
authChoice: "skip",
|
||||||
|
skipSkills: true,
|
||||||
|
skipHealth: false,
|
||||||
|
installDaemon: true,
|
||||||
|
gatewayBind: "loopback",
|
||||||
|
},
|
||||||
|
runtime,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(installGatewayDaemonNonInteractiveMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(capturedDeadlineMs).toBe(45_000);
|
||||||
|
});
|
||||||
|
}, 60_000);
|
||||||
|
|
||||||
it("auto-generates token auth when binding LAN and persists the token", async () => {
|
it("auto-generates token auth when binding LAN and persists the token", async () => {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
// Windows runner occasionally drops the temp config write in this flow; skip to keep CI green.
|
// Windows runner occasionally drops the temp config write in this flow; skip to keep CI green.
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ import { logNonInteractiveOnboardingJson } from "./local/output.js";
|
||||||
import { applyNonInteractiveSkillsConfig } from "./local/skills-config.js";
|
import { applyNonInteractiveSkillsConfig } from "./local/skills-config.js";
|
||||||
import { resolveNonInteractiveWorkspaceDir } from "./local/workspace.js";
|
import { resolveNonInteractiveWorkspaceDir } from "./local/workspace.js";
|
||||||
|
|
||||||
|
const INSTALL_DAEMON_HEALTH_DEADLINE_MS = 45_000;
|
||||||
|
const ATTACH_EXISTING_GATEWAY_HEALTH_DEADLINE_MS = 15_000;
|
||||||
|
|
||||||
export async function runNonInteractiveOnboardingLocal(params: {
|
export async function runNonInteractiveOnboardingLocal(params: {
|
||||||
opts: OnboardOptions;
|
opts: OnboardOptions;
|
||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
|
|
@ -107,7 +110,9 @@ export async function runNonInteractiveOnboardingLocal(params: {
|
||||||
const probe = await waitForGatewayReachable({
|
const probe = await waitForGatewayReachable({
|
||||||
url: links.wsUrl,
|
url: links.wsUrl,
|
||||||
token: gatewayResult.gatewayToken,
|
token: gatewayResult.gatewayToken,
|
||||||
deadlineMs: 15_000,
|
deadlineMs: opts.installDaemon
|
||||||
|
? INSTALL_DAEMON_HEALTH_DEADLINE_MS
|
||||||
|
: ATTACH_EXISTING_GATEWAY_HEALTH_DEADLINE_MS,
|
||||||
});
|
});
|
||||||
if (!probe.ok) {
|
if (!probe.ok) {
|
||||||
const message = [
|
const message = [
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,7 @@ describe("launchd install", () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
it("enables service before bootstrap (clears persisted disabled state)", async () => {
|
it("enables service before bootstrap without self-restarting the fresh agent", async () => {
|
||||||
const env = createDefaultLaunchdEnv();
|
const env = createDefaultLaunchdEnv();
|
||||||
await installLaunchAgent({
|
await installLaunchAgent({
|
||||||
env,
|
env,
|
||||||
|
|
@ -269,9 +269,13 @@ describe("launchd install", () => {
|
||||||
const bootstrapIndex = state.launchctlCalls.findIndex(
|
const bootstrapIndex = state.launchctlCalls.findIndex(
|
||||||
(c) => c[0] === "bootstrap" && c[1] === domain && c[2] === plistPath,
|
(c) => c[0] === "bootstrap" && c[1] === domain && c[2] === plistPath,
|
||||||
);
|
);
|
||||||
|
const installKickstartIndex = state.launchctlCalls.findIndex(
|
||||||
|
(c) => c[0] === "kickstart" && c[2] === serviceId,
|
||||||
|
);
|
||||||
expect(enableIndex).toBeGreaterThanOrEqual(0);
|
expect(enableIndex).toBeGreaterThanOrEqual(0);
|
||||||
expect(bootstrapIndex).toBeGreaterThanOrEqual(0);
|
expect(bootstrapIndex).toBeGreaterThanOrEqual(0);
|
||||||
expect(enableIndex).toBeLessThan(bootstrapIndex);
|
expect(enableIndex).toBeLessThan(bootstrapIndex);
|
||||||
|
expect(installKickstartIndex).toBe(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("writes TMPDIR to LaunchAgent environment when provided", async () => {
|
it("writes TMPDIR to LaunchAgent environment when provided", async () => {
|
||||||
|
|
|
||||||
|
|
@ -431,7 +431,9 @@ export async function installLaunchAgent({
|
||||||
}
|
}
|
||||||
throw new Error(`launchctl bootstrap failed: ${detail}`);
|
throw new Error(`launchctl bootstrap failed: ${detail}`);
|
||||||
}
|
}
|
||||||
await execLaunchctl(["kickstart", "-k", `${domain}/${label}`]);
|
// `bootstrap` already loads RunAtLoad agents. Avoid `kickstart -k` here:
|
||||||
|
// on slow macOS guests it SIGTERMs the freshly booted gateway and pushes the
|
||||||
|
// real listener startup past onboarding's health deadline.
|
||||||
|
|
||||||
// Ensure we don't end up writing to a clack spinner line (wizards show progress without a newline).
|
// Ensure we don't end up writing to a clack spinner line (wizards show progress without a newline).
|
||||||
writeFormattedLines(
|
writeFormattedLines(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue