Skip to content

Commit ca236d0

Browse files
committed
fix: harden gateway launchd and configure sections
1 parent 524185a commit ca236d0

22 files changed

Lines changed: 515 additions & 119 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ Docs: https://docs.openclaw.ai
4242
- Agents/auth: include the checked credential source in missing API key errors, so users can see which env var, profile, or config path to fix. Fixes #82785. Thanks @loeclos.
4343
- Providers/GitHub Copilot: hash Responses replay item ids with sha256 instead of a weak 32-bit hash and build same-provider Copilot tool-call ids distinctly, so concurrent tool-call replays no longer collide and reject follow-up turns.
4444
- Agents/replay: normalize malformed assistant replay content before transport conversion while preserving empty-stop replay repair, so bad provider history no longer crashes with non-iterable content. Fixes #43795. (#82748) Thanks @IWhatsskill.
45+
- Gateway/macOS: write LaunchAgent stdout under `~/Library/Logs/openclaw`, suppress stderr, and attach stdin to `/dev/null` so launchd startup avoids symlinked state-dir log failures and silent module-evaluation hangs. Fixes #40207 and #46153. Thanks @dhruvkelawala and @frankr.
46+
- CLI/configure: let model-only section setup enter provider auth directly instead of first asking where the Gateway runs, unblocking OAuth/token setup in terminals where that unrelated prompt is unresponsive. Fixes #39223. Thanks @LevityLeads.
4547
- Providers/Anthropic-messages: extract `reasoning_content` from `thinking` blocks during assistant replay so proxy providers that route through the Anthropic-messages transport preserve reasoning context across tool-call follow-up turns. Thanks @Sunnyone2three.
4648
- Agents/GitHub Copilot: normalize replayed Responses tool-call IDs before dispatch so resumed sessions with historical overlong tool IDs continue instead of failing Copilot schema validation. (#82750) Thanks @galiniliev.
4749
- CLI/web: resolve provider-scoped web search/fetch SecretRefs for `infer web ... --provider ...` while leaving unrelated plugin secrets untouched. Fixes #82621. Thanks @leno23.

docs/cli/configure.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Available sections:
5555

5656
Notes:
5757

58-
- Choosing where the Gateway runs always updates `gateway.mode`. You can select "Continue" without other sections if that is all you need.
58+
- The full wizard and gateway-related sections ask where the Gateway runs and update `gateway.mode`. Section filters that do not include `gateway`, `daemon`, or `health` go directly to the requested setup.
5959
- After local config writes, configure installs selected downloadable plugins when the chosen setup path requires them. Remote gateway config does not install local plugin packages.
6060
- Channel-oriented services (Slack/Discord/Matrix/Microsoft Teams) prompt for channel/room allowlists during setup. You can enter names or IDs; the wizard resolves names to IDs when possible.
6161
- If you run the daemon install step, token auth requires a token, and `gateway.auth.token` is SecretRef-managed, configure validates the SecretRef but does not persist resolved plaintext token values into supervisor service environment metadata.

docs/help/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1560,7 +1560,7 @@ lives on the [Models FAQ](/help/faq-models).
15601560

15611561
Service/supervisor logs (when the gateway runs via launchd/systemd):
15621562

1563-
- macOS: `$OPENCLAW_STATE_DIR/logs/gateway.log` and `gateway.err.log` (default: `~/.openclaw/logs/...`; profiles use `~/.openclaw-<profile>/logs/...`)
1563+
- macOS launchd stdout: `~/Library/Logs/openclaw/gateway.log` (profiles use `gateway-<profile>.log`; stderr is suppressed)
15641564
- Linux: `journalctl --user -u openclaw-gateway[-<profile>].service -n 200 --no-pager`
15651565
- Windows: `schtasks /Query /TN "OpenClaw Gateway (<profile>)" /V /FO LIST`
15661566

docs/platforms/mac/bundled-gateway.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ Behavior:
4949

5050
Logging:
5151

52-
- launchd stdout/err: `/tmp/openclaw/openclaw-gateway.log`
52+
- launchd stdout: `~/Library/Logs/openclaw/gateway.log` (profiles use `gateway-<profile>.log`)
53+
- launchd stderr: suppressed
5354

5455
## Version compatibility
5556

src/cli/daemon-cli/status.print.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ vi.mock("../../daemon/restart-logs.js", () => ({
3737
stdoutPath: "/tmp/gateway.out.log",
3838
stderrPath: "/tmp/gateway.err.log",
3939
}),
40+
resolveGatewaySupervisorLogPaths: () => ({
41+
logDir: "/Users/test/Library/Logs/openclaw",
42+
stdoutPath: "/Users/test/Library/Logs/openclaw/gateway.log",
43+
stderrPath: "/Users/test/Library/Logs/openclaw/gateway.err.log",
44+
}),
4045
resolveGatewayRestartLogPath: () => "/tmp/gateway-restart.log",
4146
}));
4247

@@ -160,6 +165,46 @@ describe("printDaemonStatus", () => {
160165
expectMockLineContains(runtime.error, formatCliCommand("openclaw gateway restart"));
161166
});
162167

168+
it("prints macOS launchd stdout and suppressed stderr when gateway is not listening", () => {
169+
const originalPlatform = process.platform;
170+
Object.defineProperty(process, "platform", { value: "darwin" });
171+
try {
172+
printDaemonStatus(
173+
{
174+
service: {
175+
label: "LaunchAgent",
176+
loaded: true,
177+
loadedText: "loaded",
178+
notLoadedText: "not loaded",
179+
runtime: { status: "running", pid: 8000 },
180+
command: { programArguments: [], environment: { HOME: "/Users/test" } },
181+
},
182+
gateway: {
183+
bindMode: "loopback",
184+
bindHost: "127.0.0.1",
185+
port: 18789,
186+
portSource: "env/config",
187+
probeUrl: "ws://127.0.0.1:18789",
188+
},
189+
port: {
190+
port: 18789,
191+
status: "free",
192+
listeners: [],
193+
hints: [],
194+
},
195+
extraServices: [],
196+
},
197+
{ json: false },
198+
);
199+
} finally {
200+
Object.defineProperty(process, "platform", { value: originalPlatform });
201+
}
202+
203+
expectMockLineContains(runtime.error, "Gateway port 18789 is not listening");
204+
expectMockLineContains(runtime.error, "/Users/test/Library/Logs/openclaw/gateway.log");
205+
expectMockLineContains(runtime.error, "Errors: suppressed");
206+
});
207+
163208
it("prints probe kind and capability separately", () => {
164209
printDaemonStatus(
165210
{

src/cli/daemon-cli/status.print.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import {
44
resolveGatewaySystemdServiceName,
55
} from "../../daemon/constants.js";
66
import { renderGatewayServiceCleanupHints } from "../../daemon/inspect.js";
7-
import { resolveGatewayLogPaths, resolveGatewayRestartLogPath } from "../../daemon/restart-logs.js";
7+
import {
8+
resolveGatewayRestartLogPath,
9+
resolveGatewaySupervisorLogPaths,
10+
} from "../../daemon/restart-logs.js";
811
import {
912
isSystemdUnavailableDetail,
1013
renderSystemdUnavailableHints,
@@ -393,9 +396,9 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
393396
errorText(`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`),
394397
);
395398
} else if (process.platform === "darwin") {
396-
const logs = resolveGatewayLogPaths(serviceEnv);
399+
const logs = resolveGatewaySupervisorLogPaths(serviceEnv, { platform: "darwin" });
397400
defaultRuntime.error(`${errorText("Logs:")} ${shortenHomePath(logs.stdoutPath)}`);
398-
defaultRuntime.error(`${errorText("Errors:")} ${shortenHomePath(logs.stderrPath)}`);
401+
defaultRuntime.error(`${errorText("Errors:")} suppressed`);
399402
}
400403
defaultRuntime.error(
401404
`${errorText("Restart log:")} ${shortenHomePath(resolveGatewayRestartLogPath(serviceEnv))}`,

src/commands/configure.wizard.test.ts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const mocks = vi.hoisted(() => {
2626
waitForGatewayReachable: vi.fn(),
2727
resolveControlUiLinks: vi.fn(),
2828
summarizeExistingConfig: vi.fn(),
29+
promptAuthConfig: vi.fn(),
30+
promptGatewayConfig: vi.fn(),
2931
promptRemoteGatewayConfig: vi.fn(async (cfg: OpenClawConfig) => ({
3032
...cfg,
3133
gateway: { mode: "remote", remote: { url: "wss://gateway.example.test" } },
@@ -86,11 +88,11 @@ vi.mock("./health-format.js", () => ({
8688
}));
8789

8890
vi.mock("./configure.gateway.js", () => ({
89-
promptGatewayConfig: vi.fn(),
91+
promptGatewayConfig: mocks.promptGatewayConfig,
9092
}));
9193

9294
vi.mock("./configure.gateway-auth.js", () => ({
93-
promptAuthConfig: vi.fn(),
95+
promptAuthConfig: mocks.promptAuthConfig,
9496
}));
9597

9698
vi.mock("./configure.channels.js", () => ({
@@ -276,6 +278,13 @@ describe("runConfigureWizard", () => {
276278
]);
277279
mocks.setupSearch.mockReset();
278280
mocks.setupSearch.mockImplementation(async (cfg: OpenClawConfig) => cfg);
281+
mocks.promptAuthConfig.mockReset();
282+
mocks.promptAuthConfig.mockImplementation(async (cfg: OpenClawConfig) => cfg);
283+
mocks.promptGatewayConfig.mockReset();
284+
mocks.promptGatewayConfig.mockImplementation(async (cfg: OpenClawConfig) => ({
285+
config: cfg,
286+
port: 18789,
287+
}));
279288
});
280289

281290
it("persists gateway.mode=local when only the run mode is selected", async () => {
@@ -328,6 +337,46 @@ describe("runConfigureWizard", () => {
328337
expect(runtime.exit).toHaveBeenCalledWith(1);
329338
});
330339

340+
it("does not gate model-only configure behind Gateway run-mode selection", async () => {
341+
setupBaseWizardState();
342+
343+
await runConfigureWizard({ command: "configure", sections: ["model"] }, createRuntime());
344+
345+
expect(mocks.promptAuthConfig).toHaveBeenCalledOnce();
346+
expect(mocks.clackSelect).not.toHaveBeenCalledWith(
347+
expect.objectContaining({ message: "Where will the Gateway run?" }),
348+
);
349+
expect(mocks.probeGatewayReachable).not.toHaveBeenCalledWith(
350+
expect.objectContaining({ timeoutMs: 300 }),
351+
);
352+
expect(mocks.ensureControlUiAssetsBuilt).not.toHaveBeenCalled();
353+
expect(mocks.resolveControlUiLinks).not.toHaveBeenCalled();
354+
expect(requireWriteConfig().gateway).toBeUndefined();
355+
});
356+
357+
it("runs model-only configure for existing remote Gateway configs", async () => {
358+
setupBaseWizardState({
359+
gateway: { mode: "remote", remote: { url: "wss://gateway.example.test" } },
360+
});
361+
362+
await runConfigureWizard({ command: "configure", sections: ["model"] }, createRuntime());
363+
364+
expect(mocks.promptAuthConfig).toHaveBeenCalledOnce();
365+
expect(mocks.promptRemoteGatewayConfig).not.toHaveBeenCalled();
366+
expect(getGateway(requireWriteConfig()).mode).toBe("remote");
367+
expect(mocks.ensureControlUiAssetsBuilt).not.toHaveBeenCalled();
368+
expect(mocks.resolveControlUiLinks).not.toHaveBeenCalled();
369+
expect(mocks.probeGatewayReachable).not.toHaveBeenCalled();
370+
expect(mocks.note).toHaveBeenCalledWith(
371+
[
372+
"Remote Gateway:",
373+
"wss://gateway.example.test",
374+
"Docs: https://docs.openclaw.ai/gateway/remote",
375+
].join("\n"),
376+
"Gateway",
377+
);
378+
});
379+
331380
it("persists provider-owned web search config changes returned by setupSearch", async () => {
332381
setupBaseWizardState();
333382
mocks.setupSearch.mockImplementation(async (cfg: OpenClawConfig) =>
@@ -337,7 +386,7 @@ describe("runConfigureWizard", () => {
337386
})(cfg),
338387
);
339388
queueWizardPrompts({
340-
select: ["local"],
389+
select: [],
341390
confirm: [true, false],
342391
});
343392

@@ -347,7 +396,7 @@ describe("runConfigureWizard", () => {
347396
mockCallArg(mocks.setupSearch, "setupSearch"),
348397
"setupSearch config",
349398
);
350-
expect(getGateway(setupConfig).mode).toBe("local");
399+
expect(setupConfig.gateway).toBeUndefined();
351400
const written = requireWriteConfig();
352401
const search = getWebSearch(written);
353402
expect(search.provider).toBe("firecrawl");
@@ -365,7 +414,7 @@ describe("runConfigureWizard", () => {
365414
setupBaseWizardState();
366415
mocks.resolveSearchProviderOptions.mockReturnValue([]);
367416
queueWizardPrompts({
368-
select: ["local"],
417+
select: [],
369418
confirm: [true, false],
370419
});
371420

@@ -385,7 +434,7 @@ describe("runConfigureWizard", () => {
385434
it("does not load managed search provider options when web search is disabled", async () => {
386435
setupBaseWizardState();
387436
queueWizardPrompts({
388-
select: ["local"],
437+
select: [],
389438
confirm: [false, true],
390439
});
391440

@@ -404,15 +453,15 @@ describe("runConfigureWizard", () => {
404453
it("defers channel status checks until a channel is selected", async () => {
405454
setupBaseWizardState();
406455
queueWizardPrompts({
407-
select: ["local", "configure"],
456+
select: ["configure"],
408457
confirm: [],
409458
});
410459

411460
await runConfigureWizard({ command: "configure", sections: ["channels"] }, createRuntime());
412461

413462
const setupChannelsCall = mocks.setupChannels.mock.calls[0] as Array<unknown> | undefined;
414463
const setupChannelsConfig = requireRecord(setupChannelsCall?.[0], "setupChannels config");
415-
expect(getGateway(setupChannelsConfig).mode).toBe("local");
464+
expect(setupChannelsConfig.gateway).toBeUndefined();
416465
const setupChannelsOptions = requireRecord(setupChannelsCall?.[3], "setupChannels options");
417466
expect(setupChannelsOptions.deferStatusUntilSelection).toBe(true);
418467
expect(setupChannelsOptions.skipStatusNote).toBe(true);
@@ -439,7 +488,7 @@ describe("runConfigureWizard", () => {
439488
})(cfg),
440489
);
441490
queueWizardPrompts({
442-
select: ["local"],
491+
select: [],
443492
confirm: [true, false],
444493
});
445494

@@ -461,7 +510,7 @@ describe("runConfigureWizard", () => {
461510
},
462511
});
463512
queueWizardPrompts({
464-
select: ["local", "cached"],
513+
select: ["cached"],
465514
confirm: [true, true, false, true],
466515
});
467516

@@ -527,7 +576,7 @@ describe("runConfigureWizard", () => {
527576
};
528577
setupBaseWizardState(baseConfig);
529578
queueWizardPrompts({
530-
select: ["local"],
579+
select: [],
531580
confirm: [],
532581
});
533582

0 commit comments

Comments
 (0)