Skip to content

Commit 5880f50

Browse files
authored
Merge branch 'main' into fix-custom-scheme-origin-allowlist
2 parents 6511317 + 9d04064 commit 5880f50

37 files changed

Lines changed: 984 additions & 339 deletions

docs/install/docker.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,29 @@ Docker is **optional**. Use it only if you want a containerized gateway or to va
4646

4747
</Step>
4848

49+
<Step title="Airgapped rerun">
50+
On offline hosts, transfer and load the image first:
51+
52+
```bash
53+
docker load -i openclaw-image.tar
54+
export OPENCLAW_IMAGE="ghcr.io/openclaw/openclaw:latest"
55+
./scripts/docker/setup.sh --offline
56+
```
57+
58+
`--offline` verifies that `OPENCLAW_IMAGE` already exists locally, disables
59+
implicit Compose pulls and builds, then runs the normal setup flow such as
60+
`.env` synchronization, permission fixes, onboarding, gateway config sync,
61+
and Compose startup.
62+
63+
If `OPENCLAW_SANDBOX=1`, offline setup also checks the configured default
64+
and active per-agent sandbox images on the daemon behind
65+
`OPENCLAW_DOCKER_SOCKET`. Docker-backed browser images must also carry the
66+
current OpenClaw browser contract label. When a required image is missing or
67+
incompatible, setup exits without changing sandbox configuration instead of
68+
reporting success with an unusable sandbox.
69+
70+
</Step>
71+
4972
<Step title="Complete onboarding">
5073
The setup script runs onboarding automatically. It will:
5174

extensions/qa-lab/src/docker-up.runtime.test.ts

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,12 @@
11
// Qa Lab tests cover docker up plugin behavior.
22
import { mkdtemp, readFile, rm } from "node:fs/promises";
3-
import { createServer } from "node:net";
43
import os from "node:os";
54
import path from "node:path";
65
import { describe, expect, it, vi } from "vitest";
76
import { runQaDockerUp } from "./docker-up.runtime.js";
87

98
type QaDockerUpDeps = NonNullable<Parameters<typeof runQaDockerUp>[1]>;
109

11-
async function occupyPortOrAcceptExisting(port: number): Promise<{ close: () => Promise<void> }> {
12-
const server = createServer();
13-
const listening = await new Promise<boolean>((resolve, reject) => {
14-
server.once("error", (error: NodeJS.ErrnoException) => {
15-
if (error.code === "EADDRINUSE") {
16-
resolve(false);
17-
return;
18-
}
19-
reject(error);
20-
});
21-
server.listen(port, "127.0.0.1", () => resolve(true));
22-
});
23-
24-
return {
25-
close: async () => {
26-
if (!listening) {
27-
return;
28-
}
29-
await new Promise<void>((resolve, reject) => {
30-
server.close((error) => (error ? reject(error) : resolve()));
31-
});
32-
},
33-
};
34-
}
35-
3610
function createHealthyDockerDeps(calls: string[]): QaDockerUpDeps {
3711
return {
3812
async runCommand(command, args, cwd) {
@@ -163,7 +137,8 @@ describe("runQaDockerUp", () => {
163137
const outputDir = await mkdtemp(path.join(os.tmpdir(), "qa-docker-up-"));
164138
const gatewayPort = 18789;
165139
const qaLabPort = 43124;
166-
const resolveHostPort = vi.fn(async (preferredPort: number) => {
140+
const resolveHostPort = vi.fn(async (preferredPort: number, pinned: boolean) => {
141+
expect(pinned).toBe(false);
167142
if (preferredPort === gatewayPort) {
168143
return 28001;
169144
}
@@ -172,16 +147,12 @@ describe("runQaDockerUp", () => {
172147
}
173148
return preferredPort;
174149
});
175-
const gatewayPortReservation = await occupyPortOrAcceptExisting(18789);
176-
const qaLabPortReservation = await occupyPortOrAcceptExisting(43124);
177150

178151
try {
179152
const result = await runQaDockerUp(
180153
{
181154
repoRoot: "/repo/openclaw",
182155
outputDir,
183-
gatewayPort,
184-
qaLabPort,
185156
skipUiBuild: true,
186157
usePrebuiltImage: true,
187158
},
@@ -202,9 +173,9 @@ describe("runQaDockerUp", () => {
202173
expect(result.qaLabUrl).not.toBe(`http://127.0.0.1:${qaLabPort}`);
203174
expect(result.gatewayUrl).toBe("http://127.0.0.1:28001/");
204175
expect(result.qaLabUrl).toBe("http://127.0.0.1:28002");
176+
expect(resolveHostPort).toHaveBeenCalledWith(gatewayPort, false);
177+
expect(resolveHostPort).toHaveBeenCalledWith(qaLabPort, false);
205178
} finally {
206-
await gatewayPortReservation.close();
207-
await qaLabPortReservation.close();
208179
await rm(outputDir, { recursive: true, force: true });
209180
}
210181
});

packages/gateway-client/src/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,10 +500,11 @@ function formatGatewayClientErrorForLog(err: unknown): string {
500500
export function resolveGatewayClientConnectChallengeTimeoutMs(
501501
opts: Pick<
502502
GatewayClientOptions,
503-
"connectChallengeTimeoutMs" | "connectDelayMs" | "preauthHandshakeTimeoutMs"
503+
"connectChallengeTimeoutMs" | "connectDelayMs" | "env" | "preauthHandshakeTimeoutMs"
504504
>,
505505
): number {
506506
return resolveConnectChallengeTimeoutMs(readConnectChallengeTimeoutOverride(opts), {
507+
env: opts.env,
507508
configuredTimeoutMs: opts.preauthHandshakeTimeoutMs,
508509
});
509510
}

packages/gateway-client/src/client.watchdog.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ describe("GatewayClient", () => {
125125
preauthHandshakeTimeoutMs: 30_000,
126126
}),
127127
).toBe(30_000);
128+
expect(
129+
resolveGatewayClientConnectChallengeTimeoutMs({
130+
env: { OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS: "6000" },
131+
}),
132+
).toBe(6_000);
128133
});
129134

130135
test("closes on missing ticks", async () => {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Gateway Client tests cover readiness behavior.
2+
import { describe, expect, it, vi } from "vitest";
3+
import { startGatewayClientWithReadinessWait } from "./readiness.js";
4+
5+
describe("startGatewayClientWithReadinessWait", () => {
6+
it("uses the injected client env when resolving the readiness timeout", async () => {
7+
const waitForReady = vi.fn(async () => ({
8+
ready: true,
9+
aborted: false,
10+
elapsedMs: 0,
11+
checks: 1,
12+
maxDriftMs: 0,
13+
}));
14+
const client = { start: vi.fn() };
15+
16+
await startGatewayClientWithReadinessWait(waitForReady, client, {
17+
clientOptions: {
18+
env: { OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS: "6000" },
19+
},
20+
});
21+
22+
expect(waitForReady).toHaveBeenCalledWith({
23+
maxWaitMs: 6_000,
24+
signal: undefined,
25+
});
26+
expect(client.start).toHaveBeenCalledTimes(1);
27+
});
28+
});

packages/gateway-client/src/readiness.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export type GatewayClientStartReadinessOptions = {
2121
timeoutMs?: number;
2222
clientOptions?: Pick<
2323
GatewayClientOptions,
24-
"connectChallengeTimeoutMs" | "connectDelayMs" | "preauthHandshakeTimeoutMs"
24+
"connectChallengeTimeoutMs" | "connectDelayMs" | "env" | "preauthHandshakeTimeoutMs"
2525
>;
2626
signal?: AbortSignal;
2727
};
@@ -42,6 +42,7 @@ function resolveGatewayClientStartReadinessTimeoutMs(
4242
? clientOptions.connectDelayMs
4343
: undefined;
4444
return resolveConnectChallengeTimeoutMs(timeoutOverride, {
45+
env: clientOptions.env,
4546
configuredTimeoutMs: clientOptions.preauthHandshakeTimeoutMs,
4647
});
4748
}

scripts/check-plugin-gateway-gauntlet.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,28 @@ Options:
209209
--cpu-core-warn <ratio> Hot CPU threshold (default: 0.9)
210210
--hot-wall-warn-ms <ms> Minimum wall time for hot CPU observations (default: 30000)
211211
--max-rss-warn-mb <mb> Maximum RSS warning threshold (default: 1536)
212+
--wall-anomaly-multiplier <n> Wall-time anomaly multiplier (default: 3)
213+
--rss-anomaly-multiplier <n> RSS anomaly multiplier (default: 2.5)
214+
--qa-cpu-regression-multiplier <n> QA baseline CPU regression multiplier (default: 2)
215+
--qa-wall-regression-multiplier <n> QA baseline wall regression multiplier (default: 2)
216+
--command-timeout-ms <ms> Lifecycle/slash command timeout (default: 120000)
217+
--build-timeout-ms <ms> Prebuild command timeout (default: 600000)
218+
--qa-timeout-ms <ms> QA chunk timeout (default: 900000)
212219
--skip-prebuild Skip the upfront build used to avoid per-command rebuild noise
213220
--skip-lifecycle Skip plugin install/inspect/disable/enable/doctor/uninstall
214221
--skip-qa Skip QA Lab RPC conversation runs
215222
--skip-slash-help Skip CLI help probes for plugin-declared command aliases
216223
--allow-empty Allow zero-command runs when every active phase is skipped
217224
--fail-on-observation Treat RSS/CPU/wall observation rows as guard failures
218225
--keep-run-root Preserve isolated HOME/state/log temp root after success
226+
227+
Environment:
228+
OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_IDS Comma-separated plugin ids to include
229+
OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_TOTAL Total plugin shards
230+
OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_INDEX Zero-based shard index
231+
OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_FAIL_ON_OBSERVATION=1
232+
OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_KEEP_RUN_ROOT=1
233+
OPENCLAW_PLUGIN_GATEWAY_GAUNTLET_QA_SUMMARY_MAX_BYTES QA summary read ceiling
219234
`);
220235
}
221236

scripts/check-workflows.mjs

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
// Uses installed tools when present, otherwise falls back to pinned hooks where
44
// possible, then runs repo-specific workflow guards.
55
import { spawnSync } from "node:child_process";
6-
import { readdirSync } from "node:fs";
6+
import { mkdtempSync, readdirSync, rmSync } from "node:fs";
7+
import { tmpdir } from "node:os";
78
import { join } from "node:path";
89

910
const ACTIONLINT_VERSION = "1.7.11";
11+
const PRE_COMMIT_VERSION = "4.2.0";
1012
const WORKFLOW_DIR = ".github/workflows";
1113

1214
function commandExists(command, args = ["--version"]) {
@@ -25,6 +27,59 @@ function run(command, args) {
2527
}
2628
}
2729

30+
function runChecked(command, args) {
31+
const result = spawnSync(command, args, { stdio: "inherit" });
32+
if (result.error) {
33+
return {
34+
message: `[check-workflows] failed to run ${command}: ${result.error.message}`,
35+
status: 1,
36+
};
37+
}
38+
if (result.status !== 0) {
39+
return {
40+
message: null,
41+
status: result.status ?? 1,
42+
};
43+
}
44+
return null;
45+
}
46+
47+
function runPreCommitFromTempVenv(hook, hookArgs) {
48+
if (!commandExists("python3", ["--version"])) {
49+
return false;
50+
}
51+
const venvDir = mkdtempSync(join(tmpdir(), "openclaw-check-workflows-pre-commit-"));
52+
const python = join(venvDir, process.platform === "win32" ? "Scripts/python.exe" : "bin/python");
53+
let failure;
54+
try {
55+
failure = runChecked("python3", ["-m", "venv", venvDir]);
56+
if (!failure) {
57+
failure = runChecked(python, [
58+
"-m",
59+
"pip",
60+
"install",
61+
"--disable-pip-version-check",
62+
`pre-commit==${PRE_COMMIT_VERSION}`,
63+
]);
64+
}
65+
if (!failure) {
66+
failure = runChecked(python, ["-m", "pre_commit", ...hookArgs]);
67+
}
68+
if (failure) {
69+
return false;
70+
}
71+
return true;
72+
} finally {
73+
rmSync(venvDir, { force: true, recursive: true });
74+
if (failure) {
75+
if (failure.message) {
76+
console.error(failure.message);
77+
}
78+
process.exit(failure.status);
79+
}
80+
}
81+
}
82+
2883
function workflowFiles() {
2984
return readdirSync(WORKFLOW_DIR)
3085
.filter((file) => file.endsWith(".yml") || file.endsWith(".yaml"))
@@ -42,9 +97,12 @@ function runPreCommitHook(hook, files) {
4297
run("python3", ["-m", "pre_commit", ...hookArgs]);
4398
return;
4499
}
100+
if (runPreCommitFromTempVenv(hook, hookArgs)) {
101+
return;
102+
}
45103

46104
console.error(
47-
`[check-workflows] missing pre-commit runtime for ${hook}: install pre-commit or python3 pre_commit.`,
105+
`[check-workflows] missing pre-commit runtime for ${hook}: install pre-commit or Python venv support for pre-commit ${PRE_COMMIT_VERSION}.`,
48106
);
49107
process.exit(1);
50108
}
@@ -57,7 +115,8 @@ if (commandExists("actionlint")) {
57115
run("go", ["run", `github.com/rhysd/actionlint/cmd/actionlint@v${ACTIONLINT_VERSION}`]);
58116
} else if (
59117
commandExists("pre-commit") ||
60-
commandExists("python3", ["-m", "pre_commit", "--version"])
118+
commandExists("python3", ["-m", "pre_commit", "--version"]) ||
119+
commandExists("python3", ["--version"])
61120
) {
62121
runPreCommitHook("actionlint", workflows);
63122
} else {

0 commit comments

Comments
 (0)