Skip to content

Commit 78fbcd3

Browse files
authored
fix(apple): preserve localized static labels
1 parent 3cdef51 commit 78fbcd3

29 files changed

Lines changed: 766 additions & 36 deletions

Dockerfile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
# Build stages use full bookworm; the runtime image is always bookworm-slim.
1010
ARG OPENCLAW_EXTENSIONS=""
1111
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
12+
ARG OPENCLAW_DOCKER_BUILD_NODE_OPTIONS="--max-old-space-size=8192"
13+
ARG OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=""
14+
ARG OPENCLAW_DOCKER_BUILD_SKIP_DTS=1
1215
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="docker.io/library/node:24-bookworm@sha256:8530f76a96d88820d288761f022e318970dda93d01536919fbc16076b7983e63"
1316
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="docker.io/library/node:24-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
1417
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
@@ -49,6 +52,9 @@ FROM ${OPENCLAW_BUN_IMAGE} AS bun-binary
4952
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS build
5053
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
5154
ARG OPENCLAW_EXTENSIONS
55+
ARG OPENCLAW_DOCKER_BUILD_NODE_OPTIONS
56+
ARG OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB
57+
ARG OPENCLAW_DOCKER_BUILD_SKIP_DTS
5258

5359
# Copy pinned Bun binary from the official image instead of fetching via curl.
5460
COPY --from=bun-binary /usr/local/bin/bun /usr/local/bin/bun
@@ -119,7 +125,7 @@ RUN pnpm_config_verify_deps_before_run=false pnpm canvas:a2ui:bundle || \
119125
RUN if printf '%s\n' "$OPENCLAW_EXTENSIONS" | tr ',' ' ' | tr ' ' '\n' | grep -qx 'qa-lab'; then \
120126
export OPENCLAW_BUILD_PRIVATE_QA=1 OPENCLAW_ENABLE_PRIVATE_QA_CLI=1; \
121127
fi && \
122-
NODE_OPTIONS=--max-old-space-size=8192 pnpm_config_verify_deps_before_run=false pnpm build:docker
128+
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD="$OPENCLAW_DOCKER_BUILD_SKIP_DTS" OPENCLAW_TSDOWN_MAX_OLD_SPACE_MB="$OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB" NODE_OPTIONS="$OPENCLAW_DOCKER_BUILD_NODE_OPTIONS" pnpm_config_verify_deps_before_run=false pnpm build:docker
123129
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
124130
ENV OPENCLAW_PREFER_PNPM=1
125131
RUN pnpm_config_verify_deps_before_run=false pnpm ui:build

docs/cli/node.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Options:
5858

5959
- `--host <host>`: Gateway WebSocket host (default: `127.0.0.1`)
6060
- `--port <port>`: Gateway WebSocket port (default: `18789`)
61+
- `--context-path <path>`: Gateway WebSocket context path (e.g. `/openclaw-gw`). Appended to the WebSocket URL.
6162
- `--tls`: Use TLS for the gateway connection
6263
- `--tls-fingerprint <sha256>`: Expected TLS certificate fingerprint (sha256)
6364
- `--node-id <id>`: Override node id (clears pairing token)
@@ -95,6 +96,7 @@ Options:
9596

9697
- `--host <host>`: Gateway WebSocket host (default: `127.0.0.1`)
9798
- `--port <port>`: Gateway WebSocket port (default: `18789`)
99+
- `--context-path <path>`: Gateway WebSocket context path (e.g. `/openclaw-gw`). Appended to the WebSocket URL.
98100
- `--tls`: Use TLS for the gateway connection
99101
- `--tls-fingerprint <sha256>`: Expected TLS certificate fingerprint (sha256)
100102
- `--node-id <id>`: Override node id (clears pairing token)

docs/install/docker.md

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -161,25 +161,28 @@ and setup-time config writes through `openclaw-gateway` with
161161

162162
The setup script accepts these optional environment variables:
163163

164-
| Variable | Purpose |
165-
| ------------------------------------------ | --------------------------------------------------------------------- |
166-
| `OPENCLAW_IMAGE` | Use a remote image instead of building locally |
167-
| `OPENCLAW_IMAGE_APT_PACKAGES` | Install extra apt packages during build (space-separated) |
168-
| `OPENCLAW_IMAGE_PIP_PACKAGES` | Install extra Python packages during build (space-separated) |
169-
| `OPENCLAW_EXTENSIONS` | Pre-install plugin dependencies at build time (space-separated names) |
170-
| `OPENCLAW_EXTRA_MOUNTS` | Extra host bind mounts (comma-separated `source:target[:opts]`) |
171-
| `OPENCLAW_HOME_VOLUME` | Persist `/home/node` in a named Docker volume |
172-
| `OPENCLAW_SANDBOX` | Opt in to sandbox bootstrap (`1`, `true`, `yes`, `on`) |
173-
| `OPENCLAW_SKIP_ONBOARDING` | Skip the interactive onboarding step (`1`, `true`, `yes`, `on`) |
174-
| `OPENCLAW_DOCKER_SOCKET` | Override Docker socket path |
175-
| `OPENCLAW_DISABLE_BONJOUR` | Disable Bonjour/mDNS advertising (defaults to `1` for Docker) |
176-
| `OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS` | Disable bundled plugin source bind-mount overlays |
177-
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Shared OTLP/HTTP collector endpoint for OpenTelemetry export |
178-
| `OTEL_EXPORTER_OTLP_*_ENDPOINT` | Signal-specific OTLP endpoints for traces, metrics, or logs |
179-
| `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP protocol override. Only `http/protobuf` is supported today |
180-
| `OTEL_SERVICE_NAME` | Service name used for OpenTelemetry resources |
181-
| `OTEL_SEMCONV_STABILITY_OPT_IN` | Opt in to latest experimental GenAI semantic attributes |
182-
| `OPENCLAW_OTEL_PRELOADED` | Skip starting a second OpenTelemetry SDK when one is preloaded |
164+
| Variable | Purpose |
165+
| ----------------------------------------------- | --------------------------------------------------------------------- |
166+
| `OPENCLAW_IMAGE` | Use a remote image instead of building locally |
167+
| `OPENCLAW_IMAGE_APT_PACKAGES` | Install extra apt packages during build (space-separated) |
168+
| `OPENCLAW_IMAGE_PIP_PACKAGES` | Install extra Python packages during build (space-separated) |
169+
| `OPENCLAW_EXTENSIONS` | Pre-install plugin dependencies at build time (space-separated names) |
170+
| `OPENCLAW_DOCKER_BUILD_NODE_OPTIONS` | Override the local source-build Node options |
171+
| `OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB` | Override the local source-build tsdown heap in MB |
172+
| `OPENCLAW_DOCKER_BUILD_SKIP_DTS` | Skip declaration output during runtime-only local image builds |
173+
| `OPENCLAW_EXTRA_MOUNTS` | Extra host bind mounts (comma-separated `source:target[:opts]`) |
174+
| `OPENCLAW_HOME_VOLUME` | Persist `/home/node` in a named Docker volume |
175+
| `OPENCLAW_SANDBOX` | Opt in to sandbox bootstrap (`1`, `true`, `yes`, `on`) |
176+
| `OPENCLAW_SKIP_ONBOARDING` | Skip the interactive onboarding step (`1`, `true`, `yes`, `on`) |
177+
| `OPENCLAW_DOCKER_SOCKET` | Override Docker socket path |
178+
| `OPENCLAW_DISABLE_BONJOUR` | Disable Bonjour/mDNS advertising (defaults to `1` for Docker) |
179+
| `OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS` | Disable bundled plugin source bind-mount overlays |
180+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Shared OTLP/HTTP collector endpoint for OpenTelemetry export |
181+
| `OTEL_EXPORTER_OTLP_*_ENDPOINT` | Signal-specific OTLP endpoints for traces, metrics, or logs |
182+
| `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP protocol override. Only `http/protobuf` is supported today |
183+
| `OTEL_SERVICE_NAME` | Service name used for OpenTelemetry resources |
184+
| `OTEL_SEMCONV_STABILITY_OPT_IN` | Opt in to latest experimental GenAI semantic attributes |
185+
| `OPENCLAW_OTEL_PRELOADED` | Skip starting a second OpenTelemetry SDK when one is preloaded |
183186

184187
The official Docker image does not ship Homebrew. During onboarding, OpenClaw
185188
hides brew-only skill dependency installers when it is running in a Linux
@@ -190,6 +193,15 @@ or installed manually. For dependencies available from Debian packages, use
190193
For Python dependencies, use `OPENCLAW_IMAGE_PIP_PACKAGES`. This runs
191194
`python3 -m pip install --break-system-packages` during the image build, so pin
192195
package versions and use only package indexes you trust.
196+
Source builds default `OPENCLAW_DOCKER_BUILD_NODE_OPTIONS` to
197+
`--max-old-space-size=8192` and leave
198+
`OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB` unset so the tsdown wrapper can
199+
respect container memory limits. They also default
200+
`OPENCLAW_DOCKER_BUILD_SKIP_DTS=1` because runtime images prune declaration
201+
files after build. If Docker reports `ResourceExhausted`, `cannot allocate
202+
memory`, or aborts during `tsdown`, increase the Docker builder memory limit or
203+
retry with smaller explicit heaps, for example
204+
`OPENCLAW_DOCKER_BUILD_NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=4096`.
193205

194206
Maintainers can test bundled plugin source against a packaged image by mounting
195207
one plugin source directory over its packaged source path, for example

packages/media-core/src/inbound-path-policy.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ describe("inbound-path-policy", () => {
4747
expectInboundPathAllowedCase(filePath, expected);
4848
});
4949

50+
it("matches Windows drive roots case-insensitively", () => {
51+
expect(
52+
isInboundPathAllowed({
53+
filePath: "C:\\Users\\Alice\\Library\\Messages\\Attachments\\12\\34\\ABCDEF\\IMG_0001.jpeg",
54+
roots: ["c:/users/*/library/messages/attachments"],
55+
}),
56+
).toBe(true);
57+
});
58+
5059
it.each([
5160
{
5261
name: "normalizes and de-duplicates merged roots",

packages/media-core/src/inbound-path-policy.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ function normalizePosixAbsolutePath(value: string): string | undefined {
2121
if (WINDOWS_DRIVE_ROOT_RE.test(withoutTrailingSlash)) {
2222
return undefined;
2323
}
24-
return withoutTrailingSlash;
24+
return WINDOWS_DRIVE_ABS_RE.test(withoutTrailingSlash)
25+
? withoutTrailingSlash.toLowerCase()
26+
: withoutTrailingSlash;
2527
}
2628

2729
function splitPathSegments(value: string): string[] {

scripts/docker/setup.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,9 @@ export OPENCLAW_IMAGE="$IMAGE_NAME"
521521
export OPENCLAW_IMAGE_APT_PACKAGES="${OPENCLAW_IMAGE_APT_PACKAGES-${OPENCLAW_DOCKER_APT_PACKAGES:-}}"
522522
export OPENCLAW_IMAGE_PIP_PACKAGES="${OPENCLAW_IMAGE_PIP_PACKAGES:-}"
523523
export OPENCLAW_EXTENSIONS="${OPENCLAW_EXTENSIONS:-}"
524+
export OPENCLAW_DOCKER_BUILD_NODE_OPTIONS="${OPENCLAW_DOCKER_BUILD_NODE_OPTIONS---max-old-space-size=8192}"
525+
export OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB="${OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB:-}"
526+
export OPENCLAW_DOCKER_BUILD_SKIP_DTS="${OPENCLAW_DOCKER_BUILD_SKIP_DTS:-1}"
524527
export OPENCLAW_INSTALL_BROWSER="${OPENCLAW_INSTALL_BROWSER:-}"
525528
export OPENCLAW_EXTRA_MOUNTS="$EXTRA_MOUNTS"
526529
export OPENCLAW_HOME_VOLUME="$HOME_VOLUME_NAME"
@@ -726,6 +729,9 @@ upsert_env "$ENV_FILE" \
726729
OPENCLAW_IMAGE_APT_PACKAGES \
727730
OPENCLAW_IMAGE_PIP_PACKAGES \
728731
OPENCLAW_EXTENSIONS \
732+
OPENCLAW_DOCKER_BUILD_NODE_OPTIONS \
733+
OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB \
734+
OPENCLAW_DOCKER_BUILD_SKIP_DTS \
729735
OPENCLAW_INSTALL_BROWSER \
730736
OPENCLAW_SANDBOX \
731737
OPENCLAW_DOCKER_SOCKET \
@@ -752,6 +758,9 @@ elif [[ "$IMAGE_NAME" == "openclaw:local" ]]; then
752758
--build-arg "OPENCLAW_IMAGE_APT_PACKAGES=${OPENCLAW_IMAGE_APT_PACKAGES}" \
753759
--build-arg "OPENCLAW_IMAGE_PIP_PACKAGES=${OPENCLAW_IMAGE_PIP_PACKAGES}" \
754760
--build-arg "OPENCLAW_EXTENSIONS=${OPENCLAW_EXTENSIONS}" \
761+
--build-arg "OPENCLAW_DOCKER_BUILD_NODE_OPTIONS=${OPENCLAW_DOCKER_BUILD_NODE_OPTIONS}" \
762+
--build-arg "OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=${OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB}" \
763+
--build-arg "OPENCLAW_DOCKER_BUILD_SKIP_DTS=${OPENCLAW_DOCKER_BUILD_SKIP_DTS}" \
755764
--build-arg "OPENCLAW_INSTALL_BROWSER=${OPENCLAW_INSTALL_BROWSER}" \
756765
--build-arg "OPENCLAW_INSTALL_DOCKER_CLI=${OPENCLAW_INSTALL_DOCKER_CLI:-}" \
757766
-t "$IMAGE_NAME" \

scripts/lib/docker-build.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,19 @@ docker_build_transient_failure() {
5555
"$log_file"
5656
}
5757

58+
docker_build_resource_exhausted_failure() {
59+
local log_file="$1"
60+
grep -Eqi 'ResourceExhausted|cannot allocate memory|out of memory|exit code: 137|signal: killed|Killed' "$log_file"
61+
}
62+
63+
docker_build_print_resource_exhausted_hint() {
64+
cat >&2 <<'EOF'
65+
Docker build failed because the builder ran out of memory.
66+
Try increasing the Docker/BuildKit memory limit, closing other memory-heavy processes, or rebuilding with a smaller OpenClaw build heap, for example:
67+
OPENCLAW_DOCKER_BUILD_NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB=4096 ./scripts/docker/setup.sh
68+
EOF
69+
}
70+
5871
docker_build_retry_count() {
5972
local configured="${OPENCLAW_DOCKER_BUILD_RETRIES:-2}"
6073
if [[ "$configured" =~ ^[0-9]+$ ]]; then
@@ -235,6 +248,9 @@ docker_build_with_retries() {
235248

236249
if [ "$attempt" -ge "$max_attempts" ] || ! docker_build_transient_failure "$log_file"; then
237250
docker_e2e_print_log "$log_file"
251+
if docker_build_resource_exhausted_failure "$log_file"; then
252+
docker_build_print_resource_exhausted_hint
253+
fi
238254
rm -f "$log_file"
239255
return 1
240256
fi

src/agents/embedded-agent-error-observation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const RAW_ERROR_CONSOLE_SUPPRESSED_FAILURE_KINDS = new Set<ProviderRuntimeFailur
2727
"auth_html",
2828
"auth_refresh",
2929
"auth_scope",
30+
"upstream_html",
3031
]);
3132

3233
function resolveConfiguredRedactPatterns(): string[] {

src/agents/embedded-agent-helpers/errors.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,8 @@ const AUTH_INVALID_TOKEN_HINT_RE =
453453
/\bunauthorized\b|\b(?:invalid|incorrect|expired|stale)[_\s-]?api[_\s-]?key\b|\b(?:invalid|incorrect|expired|stale)\s+(?:token|jwt|credential|api[_\s-]?key)\b|\b(?:token|jwt|credential|api[_\s-]?key)\s+(?:is\s+)?(?:invalid|incorrect|expired|stale)\b/i;
454454
const HTML_BODY_RE = /^\s*(?:<!doctype\s+html\b|<html\b)/i;
455455
const HTML_CLOSE_RE = /<\/html>/i;
456+
const CLOUDFLARE_CHALLENGE_RE =
457+
/Enable\s+JavaScript\s+and\s+cookies\s+to\s+continue|cf-browser-verification|__cf_challenge|cdn-cgi\/challenge-platform|challenge-error-text/i;
456458
const PROXY_ERROR_RE =
457459
/\bproxyconnect\b|\bhttps?_proxy\b|\b407\b|\bproxy authentication required\b|\btunnel connection failed\b|\bconnect tunnel\b|\bsocks proxy\b|\bproxy error\b/i;
458460
const DNS_ERROR_RE = /\benotfound\b|\beai_again\b|\bgetaddrinfo\b|\bno such host\b|\bdns\b/i;
@@ -520,6 +522,10 @@ function isHtmlErrorResponse(raw: string, status?: number): boolean {
520522
return HTML_BODY_RE.test(rest) && HTML_CLOSE_RE.test(rest);
521523
}
522524

525+
function isCloudflareChallengeResponse(message: string): boolean {
526+
return CLOUDFLARE_CHALLENGE_RE.test(message);
527+
}
528+
523529
function isTransportHtmlErrorStatus(status: number | undefined): boolean {
524530
return (
525531
status === 408 ||
@@ -1265,6 +1271,13 @@ export function classifyProviderRuntimeFailureKind(
12651271
return "proxy";
12661272
}
12671273
if (message && isHtmlErrorResponse(message, status)) {
1274+
// Cloudflare challenge pages block programmatic requests at the CDN layer.
1275+
// These are upstream gateway blocks, not authentication failures — surface
1276+
// the more accurate "upstream_html" message, which already mentions
1277+
// "CDN or gateway (e.g. Cloudflare) blocked the request".
1278+
if (status === 403 && isCloudflareChallengeResponse(message)) {
1279+
return "upstream_html";
1280+
}
12681281
return status === 401 || status === 403 ? "auth_html" : "upstream_html";
12691282
}
12701283
const failoverClassification = classifyFailoverSignal({

src/agents/embedded-agent-helpers/provider-error-patterns.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,18 @@ describe("Cloudflare / CDN HTML error page classification (#67517)", () => {
172172
const cloudflareHtml503 =
173173
"<!doctype html><html><head><title>503</title></head>" +
174174
"<body><h1>Service Unavailable</h1><p>Please try again. Rate limit exceeded.</p></body></html>";
175+
const cloudflareChallengeHtml =
176+
"<!doctype html><html><head><title>403 Forbidden</title></head>" +
177+
"<body>Enable JavaScript and cookies to continue." +
178+
"<p>Please stand by, while we are checking your browser...</p></body></html>";
179+
const cloudflareChallengeCdnCgiHtml =
180+
"<!doctype html><html><head><title>403 Forbidden</title></head>" +
181+
'<body><script src="/cdn-cgi/challenge-platform/h/g/orchestrate/chl_page"></script>' +
182+
"<p>Checking your browser...</p></body></html>";
183+
const cloudflareChallengeErrorTextHtml =
184+
"<!doctype html><html><head><title>403 Forbidden</title></head>" +
185+
'<body><span id="challenge-error-text">Enable JavaScript and cookies to continue</span>' +
186+
"<p>Please stand by...</p></body></html>";
175187
const html401 =
176188
"<!doctype html><html><head><title>401 Unauthorized</title></head>" +
177189
"<body><h1>Unauthorized</h1></body></html>";
@@ -226,7 +238,30 @@ describe("Cloudflare / CDN HTML error page classification (#67517)", () => {
226238
);
227239
});
228240

229-
it("classifies 403 HTML runtime failures as auth_html", () => {
241+
it("classifies Cloudflare challenge 403 as upstream_html", () => {
242+
// Cloudflare browser-challenge pages are CDN blocks, not auth failures.
243+
expect(
244+
classifyProviderRuntimeFailureKind({ status: 403, message: cloudflareChallengeHtml }),
245+
).toBe("upstream_html");
246+
});
247+
it("classifies Cloudflare challenge 403 with cdn-cgi/challenge-platform as upstream_html", () => {
248+
// Challenge pages with the challenge platform script path are also CDN blocks.
249+
expect(
250+
classifyProviderRuntimeFailureKind({ status: 403, message: cloudflareChallengeCdnCgiHtml }),
251+
).toBe("upstream_html");
252+
});
253+
254+
it("classifies Cloudflare challenge 403 with challenge-error-text as upstream_html", () => {
255+
// Challenge pages with the challenge-error-text element are also CDN blocks.
256+
expect(
257+
classifyProviderRuntimeFailureKind({
258+
status: 403,
259+
message: cloudflareChallengeErrorTextHtml,
260+
}),
261+
).toBe("upstream_html");
262+
});
263+
264+
it("classifies generic 403 HTML runtime failures as auth_html", () => {
230265
expect(classifyProviderRuntimeFailureKind({ status: 403, message: html403 })).toBe("auth_html");
231266
});
232267

0 commit comments

Comments
 (0)