Skip to content

Commit 344aff3

Browse files
committed
fix(acpx): cap service timer timeouts
1 parent 56f46a2 commit 344aff3

2 files changed

Lines changed: 47 additions & 6 deletions

File tree

extensions/acpx/src/service.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "node:fs/promises";
22
import os from "node:os";
33
import path from "node:path";
4+
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
45
import { afterEach, describe, expect, it, vi } from "vitest";
56

67
const { runtimeRegistry } = vi.hoisted(() => ({
@@ -90,7 +91,7 @@ vi.mock("./process-reaper.js", () => ({
9091

9192
import { getAcpRuntimeBackend } from "../runtime-api.js";
9293
import type { OpenClawPluginServiceContext } from "../runtime-api.js";
93-
import { createAcpxRuntimeService } from "./service.js";
94+
import { createAcpxRuntimeService, resolveAcpxTimerTimeoutMs } from "./service.js";
9495

9596
const tempDirs: string[] = [];
9697
const previousEnv = {
@@ -196,6 +197,11 @@ function readFirstRuntimeFactoryInput(runtimeFactory: { mock: { calls: Array<Arr
196197
}
197198

198199
describe("createAcpxRuntimeService", () => {
200+
it("caps configured timeout seconds to timer-safe milliseconds", () => {
201+
expect(resolveAcpxTimerTimeoutMs(0.001)).toBe(1);
202+
expect(resolveAcpxTimerTimeoutMs(Number.MAX_SAFE_INTEGER)).toBe(MAX_TIMER_TIMEOUT_MS);
203+
});
204+
199205
it("registers and unregisters the embedded backend", async () => {
200206
const workspaceDir = await makeTempDir();
201207
const ctx = createServiceContext(workspaceDir);
@@ -580,6 +586,36 @@ describe("createAcpxRuntimeService", () => {
580586
await service.stop?.(ctx);
581587
});
582588

589+
it("caps oversized plugin timeouts before constructing the default acpx runtime", async () => {
590+
process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE = "0";
591+
const workspaceDir = await makeTempDir();
592+
const ctx = createServiceContext(workspaceDir);
593+
const service = createAcpxRuntimeService({
594+
pluginConfig: { timeoutSeconds: Number.MAX_SAFE_INTEGER },
595+
});
596+
597+
await service.start(ctx);
598+
599+
const backend = getAcpRuntimeBackend("acpx");
600+
if (!backend) {
601+
throw new Error("expected ACPX runtime backend");
602+
}
603+
const backendRuntime = backend.runtime as {
604+
ensureSession(input: { agent: string; mode: string; sessionKey: string }): Promise<unknown>;
605+
};
606+
607+
await backendRuntime.ensureSession({
608+
agent: "codex",
609+
mode: "oneshot",
610+
sessionKey: "agent:codex:acp:test",
611+
});
612+
613+
const [options] = acpxRuntimeConstructorMock.mock.calls[0] ?? [];
614+
expect(options).toHaveProperty("timeoutMs", MAX_TIMER_TIMEOUT_MS);
615+
616+
await service.stop?.(ctx);
617+
});
618+
583619
it("runs the embedded runtime probe at startup when explicitly enabled and reports health", async () => {
584620
process.env.OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE = "1";
585621
const workspaceDir = await makeTempDir();

extensions/acpx/src/service.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
33
import path from "node:path";
44
import { inspect } from "node:util";
55
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
6+
import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
67
import type {
78
AcpRuntime,
89
OpenClawPluginService,
@@ -60,6 +61,13 @@ function loadRuntimeModule(): Promise<AcpxRuntimeModule> {
6061
return runtimeModulePromise;
6162
}
6263

64+
export function resolveAcpxTimerTimeoutMs(timeoutSeconds: number | undefined): number | undefined {
65+
if (timeoutSeconds === undefined) {
66+
return undefined;
67+
}
68+
return finiteSecondsToTimerSafeMilliseconds(timeoutSeconds) ?? 1;
69+
}
70+
6371
function createLazyDefaultRuntime(params: AcpxRuntimeFactoryParams): AcpxRuntimeLike {
6472
let runtime: AcpxRuntimeLike | null = null;
6573
let runtimePromise: Promise<AcpxRuntimeLike> | null = null;
@@ -84,10 +92,7 @@ function createLazyDefaultRuntime(params: AcpxRuntimeFactoryParams): AcpxRuntime
8492
mcpServers: toAcpMcpServers(params.pluginConfig.mcpServers),
8593
permissionMode: params.pluginConfig.permissionMode,
8694
nonInteractivePermissions: params.pluginConfig.nonInteractivePermissions,
87-
timeoutMs:
88-
params.pluginConfig.timeoutSeconds != null
89-
? params.pluginConfig.timeoutSeconds * 1_000
90-
: undefined,
95+
timeoutMs: resolveAcpxTimerTimeoutMs(params.pluginConfig.timeoutSeconds),
9196
}) as AcpxRuntimeLike;
9297
return runtime;
9398
});
@@ -201,7 +206,7 @@ async function withStartupProbeTimeout<T>(params: {
201206
timeoutSeconds: number;
202207
}): Promise<T> {
203208
let timeout: ReturnType<typeof setTimeout> | undefined;
204-
const timeoutMs = Math.max(1, params.timeoutSeconds * 1_000);
209+
const timeoutMs = resolveAcpxTimerTimeoutMs(params.timeoutSeconds) ?? 1;
205210
try {
206211
return await Promise.race([
207212
params.promise,

0 commit comments

Comments
 (0)