From 5b744be46c51bb16001dd2ff7d492647efb5e994 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 09:36:16 +0000 Subject: [PATCH 01/13] fix(sandbox): cron preflight inference.local uses trusted env-proxy mode Signed-off-by: Tinson Lai --- Dockerfile | 38 ++++ ...test-cron-preflight-inference-local-e2e.sh | 186 ++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100755 test/e2e/test-cron-preflight-inference-local-e2e.sh diff --git a/Dockerfile b/Dockerfile index d7646fb1c9..3c07340f33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -382,6 +382,44 @@ RUN set -eu; \ patch_fail "Patch 4 cannot safely skip"; \ fi; \ fi; \ + # --- Patch 5: cron model-provider preflight opts into trusted env-proxy mode --- \ + # Reviewed against openclaw@2026.5.27 dist: the cron isolated-agent preflight \ + # (`probeLocalProviderEndpoint`) calls `fetchWithSsrFGuard` with \ + # `auditContext: "cron-model-provider-preflight"` and a narrow hostname-allowlist \ + # SsrFPolicy, but does not pass a `mode`. Default STRICT mode pins DNS for the \ + # managed inference hostname (`inference.local`), which is intentionally only \ + # resolvable through the OpenShell L7 proxy — pinned `dns.lookup` therefore \ + # fails with EAI_AGAIN and the scheduler permanently skips every cron run. \ + # Inject `mode: "trusted_env_proxy"` so the call uses the env proxy dispatcher; \ + # SSRF protection is retained through the existing hostname allowlist and the \ + # proxy's own ACLs. Scoped to the exact audit-context string; other \ + # `fetchWithSsrFGuard` callers are untouched. \ + preflight_files="$(grep -RIlF --include='*.js' 'cron-model-provider-preflight' "$OC_DIST" || true)"; \ + if [ -n "$preflight_files" ]; then \ + patched_preflight=0; \ + for f in $preflight_files; do \ + if grep -Fq 'mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"' "$f"; then \ + echo "INFO: Patch 5 already present in $f"; \ + else \ + sed -i -E 's|auditContext: "cron-model-provider-preflight"|mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"|g' "$f"; \ + grep -Fq 'mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"' "$f" \ + || patch_fail "Patch 5 verification failed for $f"; \ + patched_preflight=1; \ + fi; \ + done; \ + if [ "$patched_preflight" = "1" ]; then \ + echo "INFO: Patch 5 applied to OpenClaw ${OC_VERSION} cron preflight trusted env-proxy"; \ + fi; \ + else \ + preflight_refs="$(grep -RIlE --include='*.js' 'preflightCronModelProvider|probeLocalProviderEndpoint' "$OC_DIST" || true)"; \ + if [ -z "$preflight_refs" ]; then \ + echo "INFO: OpenClaw ${OC_VERSION} has no cron model-provider preflight; Patch 5 not needed"; \ + else \ + echo "ERROR: Patch 5 target missing but cron preflight references remain:" >&2; \ + printf '%s\n' "$preflight_refs" | head -n 5 >&2; \ + patch_fail "Patch 5 cannot safely skip"; \ + fi; \ + fi; \ # --- Patch 3: follow symlinks in plugin-install path checks (#2203) --- \ # OpenClaw's install-safe-path and install-package-dir reject symlinked \ # directories via lstat. Changing lstat → stat in these two modules lets \ diff --git a/test/e2e/test-cron-preflight-inference-local-e2e.sh b/test/e2e/test-cron-preflight-inference-local-e2e.sh new file mode 100755 index 0000000000..469280f892 --- /dev/null +++ b/test/e2e/test-cron-preflight-inference-local-e2e.sh @@ -0,0 +1,186 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Cron preflight inference.local E2E. +# +# Onboards a fresh sandbox against the managed cloud provider (whose base URL +# resolves through `inference.local`), schedules an isolated agentTurn cron +# job, force-triggers it via `openclaw cron run --wait`, and asserts the +# provider preflight does not skip the run with `EAI_AGAIN` or the +# "local provider endpoint is not reachable" message. +# +# Prerequisites: +# - Docker running +# - NVIDIA_API_KEY set (real key, starts with nvapi-) +# - NEMOCLAW_NON_INTERACTIVE=1, NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 +# +# Environment: +# NEMOCLAW_SANDBOX_NAME — sandbox name (default: e2e-cron-preflight) +# NEMOCLAW_RECREATE_SANDBOX=1 — destroy + recreate if exists +# NEMOCLAW_CRON_PREFLIGHT_MODEL — cloud model (default: nvidia/nemotron-3-super-120b-a12b) +# NEMOCLAW_CRON_PREFLIGHT_WAIT — --wait-timeout for cron run (default: 90s) +# NEMOCLAW_CRON_PREFLIGHT_KEEP=1 — keep the sandbox after the test for inspection +# +# Usage: +# NEMOCLAW_NON_INTERACTIVE=1 NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ +# NVIDIA_API_KEY=nvapi-... bash test/e2e/test-cron-preflight-inference-local-e2e.sh + +set -uo pipefail + +PASS=0 +FAIL=0 +SKIP=0 +TOTAL=0 + +pass() { + PASS=$((PASS + 1)) + TOTAL=$((TOTAL + 1)) + printf '\033[32m PASS: %s\033[0m\n' "$1" +} +fail() { + FAIL=$((FAIL + 1)) + TOTAL=$((TOTAL + 1)) + printf '\033[31m FAIL: %s\033[0m\n' "$1" +} +skip() { + SKIP=$((SKIP + 1)) + TOTAL=$((TOTAL + 1)) + printf '\033[33m SKIP: %s\033[0m\n' "$1" +} +section() { + echo "" + printf '\033[1;36m=== %s ===\033[0m\n' "$1" +} +info() { printf '\033[1;34m [info]\033[0m %s\n' "$1"; } + +# ── Repo root ── +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_candidate="$(cd "${_script_dir}/../.." && pwd)" +if [ -d /workspace ] && [ -f /workspace/package.json ] && [ -d /workspace/test/e2e ]; then + REPO="/workspace" +elif [ -f "${_candidate}/package.json" ] && [ -d "${_candidate}/test/e2e" ]; then + REPO="${_candidate}" +else + echo "ERROR: Cannot find repo root." + exit 1 +fi +cd "$REPO" || { + echo "ERROR: Cannot cd into repo root '$REPO'." + exit 1 +} + +SANDBOX="${NEMOCLAW_SANDBOX_NAME:-e2e-cron-preflight}" +MODEL="${NEMOCLAW_CRON_PREFLIGHT_MODEL:-nvidia/nemotron-3-super-120b-a12b}" +WAIT_TIMEOUT="${NEMOCLAW_CRON_PREFLIGHT_WAIT:-90s}" + +# ── Prereqs ── +section "Prerequisites" +if ! command -v docker >/dev/null 2>&1; then + skip "docker not installed" + echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" + exit 0 +fi +if ! command -v jq >/dev/null 2>&1; then + skip "jq not installed" + echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" + exit 0 +fi +if [ -z "${NVIDIA_API_KEY:-}" ]; then + skip "NVIDIA_API_KEY not set" + echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" + exit 0 +fi +if [ "${NVIDIA_API_KEY:0:6}" != "nvapi-" ]; then + skip "NVIDIA_API_KEY does not start with nvapi-" + echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" + exit 0 +fi +pass "prerequisites satisfied" + +# shellcheck disable=SC2317 # invoked via trap +cleanup() { + if [ "${NEMOCLAW_CRON_PREFLIGHT_KEEP:-0}" = "1" ]; then + info "NEMOCLAW_CRON_PREFLIGHT_KEEP=1 set; leaving sandbox $SANDBOX in place" + return + fi + info "destroying sandbox $SANDBOX" + nemoclaw "$SANDBOX" destroy --yes >/dev/null 2>&1 || true +} +trap cleanup EXIT + +# ── Onboard ── +section "Onboard sandbox '$SANDBOX'" +if [ "${NEMOCLAW_RECREATE_SANDBOX:-0}" = "1" ]; then + info "NEMOCLAW_RECREATE_SANDBOX=1 set; destroying existing sandbox first" + nemoclaw "$SANDBOX" destroy --yes >/dev/null 2>&1 || true +fi + +NEMOCLAW_SANDBOX_NAME="$SANDBOX" nemoclaw onboard \ + --provider build \ + --model "$MODEL" 2>&1 | sed 's/^/ /' +ONBOARD_RC=${PIPESTATUS[0]} +if [ "$ONBOARD_RC" -ne 0 ]; then + fail "onboard exited $ONBOARD_RC" + echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" + exit 1 +fi +pass "onboard completed" + +# ── Schedule cron job ── +section "Schedule isolated agentTurn cron job" +JOB_NAME="preflight-$(date +%s)" +ADD_OUT="$(nemoclaw "$SANDBOX" exec -- openclaw cron add \ + --name "$JOB_NAME" \ + --agent main \ + --session isolated \ + --every 12h \ + --message "Reply with the single word: ok." \ + --keep-after-run \ + --json 2>&1)" +ADD_RC=$? +if [ "$ADD_RC" -ne 0 ]; then + fail "cron add exited $ADD_RC" + printf '%s\n' "$ADD_OUT" | sed 's/^/ /' + echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" + exit 1 +fi + +JOB_ID="$(printf '%s' "$ADD_OUT" | jq -r '.id // empty' 2>/dev/null || true)" +if [ -z "$JOB_ID" ]; then + fail "cron add returned no id" + printf '%s\n' "$ADD_OUT" | sed 's/^/ /' + echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" + exit 1 +fi +pass "scheduled job $JOB_NAME ($JOB_ID)" + +# ── Force-trigger + wait ── +section "Force-trigger and wait" +RUN_OUT="$(nemoclaw "$SANDBOX" exec -- openclaw cron run "$JOB_ID" \ + --wait --wait-timeout "$WAIT_TIMEOUT" --json 2>&1)" +RUN_RC=$? +info "raw cron run output (rc=$RUN_RC):" +printf '%s\n' "$RUN_OUT" | sed 's/^/ /' + +STATUS="$(printf '%s' "$RUN_OUT" | jq -r '.run.status // .status // empty' 2>/dev/null || true)" +REASON="$(printf '%s' "$RUN_OUT" | jq -r '.run.reason // .reason // ""' 2>/dev/null || true)" + +# ── Assertions ── +section "Assertions" +if printf '%s' "$REASON" | grep -qi "EAI_AGAIN"; then + fail "preflight raised EAI_AGAIN; reason='$REASON'" +elif printf '%s' "$REASON" | grep -qi "local provider endpoint is not reachable"; then + fail "preflight reported endpoint unreachable; reason='$REASON'" +elif [ "$STATUS" = "skipped" ]; then + fail "cron run reported status=skipped; reason='$REASON'" +elif [ "$STATUS" = "ok" ]; then + pass "cron run status=ok" +else + fail "unexpected cron run status='$STATUS' rc=$RUN_RC reason='$REASON'" +fi + +section "Summary" +echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" +[ "$FAIL" -eq 0 ] || exit 1 +exit 0 From 6c9027d1b3cb0caf8311b541d97718596eb3e599 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 09:42:55 +0000 Subject: [PATCH 02/13] ci(nightly-e2e): wire cron preflight inference.local script into nightly matrix Signed-off-by: Tinson Lai --- .github/workflows/nightly-e2e.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml index 66615e41e6..9a0637d7d9 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -119,6 +119,7 @@ on: description: >- Comma-separated job names to run (empty = all). Valid: cloud-e2e, cloud-onboard-e2e, cloud-inference-e2e, + cron-preflight-inference-local-e2e, agent-turn-latency-e2e, skill-agent-e2e, openclaw-skill-cli-e2e, docs-validation-e2e, messaging-providers-e2e, openclaw-slack-pairing-e2e, openclaw-tui-chat-correlation-e2e, issue-4434-tui-unreachable-inference-e2e, @@ -239,6 +240,21 @@ jobs: env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-cloud-inference"}' nvidia_api_key: true secrets: *nightly-e2e-default-secrets + cron-preflight-inference-local-e2e: + if: >- + github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || + inputs.jobs == '' || + contains(format(',{0},', inputs.jobs), ',cron-preflight-inference-local-e2e,')) + uses: ./.github/workflows/e2e-script.yaml + with: + ref: ${{ inputs.target_ref || github.ref }} + script: test/e2e/test-cron-preflight-inference-local-e2e.sh + timeout_minutes: 30 + artifact_name: "install-log-cron-preflight-inference-local" + artifact_path: "/tmp/nemoclaw-e2e-cron-preflight-install.log" + env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-cron-preflight"}' + nvidia_api_key: true + secrets: *nightly-e2e-default-secrets agent-turn-latency-e2e: if: >- github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || From 50a18ab3bc0a69a414859ba9c8d38bbb7500b551 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 10:06:12 +0000 Subject: [PATCH 03/13] fix(review): address cron preflight patch review findings Signed-off-by: Tinson Lai --- .github/workflows/nightly-e2e.yaml | 3 ++ Dockerfile | 40 ++++++++++++------- .../migration/legacy-inventory.json | 11 +++++ ...test-cron-preflight-inference-local-e2e.sh | 19 +++++++-- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml index 9a0637d7d9..7ce45c6c8f 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -2245,6 +2245,7 @@ jobs: cloud-e2e, cloud-onboard-e2e, cloud-inference-e2e, + cron-preflight-inference-local-e2e, agent-turn-latency-e2e, skill-agent-e2e, openclaw-skill-cli-e2e, @@ -2362,6 +2363,7 @@ jobs: cloud-e2e, cloud-onboard-e2e, cloud-inference-e2e, + cron-preflight-inference-local-e2e, agent-turn-latency-e2e, skill-agent-e2e, openclaw-skill-cli-e2e, @@ -2538,6 +2540,7 @@ jobs: cloud-e2e, cloud-onboard-e2e, cloud-inference-e2e, + cron-preflight-inference-local-e2e, agent-turn-latency-e2e, skill-agent-e2e, openclaw-skill-cli-e2e, diff --git a/Dockerfile b/Dockerfile index 3c07340f33..a720911110 100644 --- a/Dockerfile +++ b/Dockerfile @@ -382,42 +382,52 @@ RUN set -eu; \ patch_fail "Patch 4 cannot safely skip"; \ fi; \ fi; \ - # --- Patch 5: cron model-provider preflight opts into trusted env-proxy mode --- \ + # --- Patch 6: cron model-provider preflight opts into trusted env-proxy mode --- \ # Reviewed against openclaw@2026.5.27 dist: the cron isolated-agent preflight \ # (`probeLocalProviderEndpoint`) calls `fetchWithSsrFGuard` with \ # `auditContext: "cron-model-provider-preflight"` and a narrow hostname-allowlist \ - # SsrFPolicy, but does not pass a `mode`. Default STRICT mode pins DNS for the \ - # managed inference hostname (`inference.local`), which is intentionally only \ - # resolvable through the OpenShell L7 proxy — pinned `dns.lookup` therefore \ - # fails with EAI_AGAIN and the scheduler permanently skips every cron run. \ - # Inject `mode: "trusted_env_proxy"` so the call uses the env proxy dispatcher; \ - # SSRF protection is retained through the existing hostname allowlist and the \ - # proxy's own ACLs. Scoped to the exact audit-context string; other \ - # `fetchWithSsrFGuard` callers are untouched. \ + # SsrFPolicy from `buildLocalProviderSsrFPolicy`, but does not pass a `mode`. \ + # Default STRICT mode pins DNS for the managed inference hostname \ + # (`inference.local`), which is intentionally only resolvable through the \ + # OpenShell L7 proxy — pinned `dns.lookup` therefore fails with EAI_AGAIN and \ + # the scheduler permanently skips every cron run. Inject \ + # `mode: "trusted_env_proxy"` so the call uses the env proxy dispatcher; SSRF \ + # protection is retained through the existing hostname allowlist and the \ + # proxy's own ACLs. \ + # \ + # The patch keys on three co-located markers in the same file — the audit \ + # context literal, the `fetchWithSsrFGuard(` call, and the \ + # `buildLocalProviderSsrFPolicy` helper that builds the surrounding policy — \ + # to pin the rewrite to the preflight call and fail closed if upstream drift \ + # ever reuses the audit-context string outside this shape. \ preflight_files="$(grep -RIlF --include='*.js' 'cron-model-provider-preflight' "$OC_DIST" || true)"; \ if [ -n "$preflight_files" ]; then \ patched_preflight=0; \ for f in $preflight_files; do \ + grep -Fq 'fetchWithSsrFGuard(' "$f" \ + || patch_fail "Patch 6 shape gate: $f has cron-model-provider-preflight but no fetchWithSsrFGuard call"; \ + grep -Fq 'buildLocalProviderSsrFPolicy' "$f" \ + || patch_fail "Patch 6 shape gate: $f has cron-model-provider-preflight but no buildLocalProviderSsrFPolicy"; \ if grep -Fq 'mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"' "$f"; then \ - echo "INFO: Patch 5 already present in $f"; \ + echo "INFO: Patch 6 already present in $f"; \ else \ sed -i -E 's|auditContext: "cron-model-provider-preflight"|mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"|g' "$f"; \ grep -Fq 'mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"' "$f" \ - || patch_fail "Patch 5 verification failed for $f"; \ + || patch_fail "Patch 6 verification failed for $f"; \ patched_preflight=1; \ fi; \ done; \ if [ "$patched_preflight" = "1" ]; then \ - echo "INFO: Patch 5 applied to OpenClaw ${OC_VERSION} cron preflight trusted env-proxy"; \ + echo "INFO: Patch 6 applied to OpenClaw ${OC_VERSION} cron preflight trusted env-proxy"; \ fi; \ else \ preflight_refs="$(grep -RIlE --include='*.js' 'preflightCronModelProvider|probeLocalProviderEndpoint' "$OC_DIST" || true)"; \ if [ -z "$preflight_refs" ]; then \ - echo "INFO: OpenClaw ${OC_VERSION} has no cron model-provider preflight; Patch 5 not needed"; \ + echo "INFO: OpenClaw ${OC_VERSION} has no cron model-provider preflight; Patch 6 not needed"; \ else \ - echo "ERROR: Patch 5 target missing but cron preflight references remain:" >&2; \ + echo "ERROR: Patch 6 target missing but cron preflight references remain:" >&2; \ printf '%s\n' "$preflight_refs" | head -n 5 >&2; \ - patch_fail "Patch 5 cannot safely skip"; \ + patch_fail "Patch 6 cannot safely skip"; \ fi; \ fi; \ # --- Patch 3: follow symlinks in plugin-install path checks (#2203) --- \ diff --git a/test/e2e-scenario/migration/legacy-inventory.json b/test/e2e-scenario/migration/legacy-inventory.json index e66981d726..68ac006c8a 100644 --- a/test/e2e-scenario/migration/legacy-inventory.json +++ b/test/e2e-scenario/migration/legacy-inventory.json @@ -200,6 +200,17 @@ "deletionReady": false, "notes": "Initial completeness row; classify detailed coverage and deletion evidence in the owning migration issue before deleting." }, + { + "legacyScript": "test/e2e/test-cron-preflight-inference-local-e2e.sh", + "domain": "inference", + "ownerIssue": "#4349", + "status": "not-migrated", + "targetVitestScenarios": [], + "bridgeProbes": [], + "retiredReason": "", + "deletionReady": false, + "notes": "Covers OpenClaw cron isolated-agent provider preflight against the managed inference hostname; classify detailed coverage and deletion evidence in the owning migration issue before deleting." + }, { "legacyScript": "test/e2e/test-common-egress-agent-e2e.sh", "domain": "security-policy", diff --git a/test/e2e/test-cron-preflight-inference-local-e2e.sh b/test/e2e/test-cron-preflight-inference-local-e2e.sh index 469280f892..6b77b6e930 100755 --- a/test/e2e/test-cron-preflight-inference-local-e2e.sh +++ b/test/e2e/test-cron-preflight-inference-local-e2e.sh @@ -98,12 +98,18 @@ if [ "${NVIDIA_API_KEY:0:6}" != "nvapi-" ]; then fi pass "prerequisites satisfied" -# shellcheck disable=SC2317 # invoked via trap +CREATED_SANDBOX=0 + +# shellcheck disable=SC2317,SC2329 # invoked via trap cleanup() { if [ "${NEMOCLAW_CRON_PREFLIGHT_KEEP:-0}" = "1" ]; then info "NEMOCLAW_CRON_PREFLIGHT_KEEP=1 set; leaving sandbox $SANDBOX in place" return fi + if [ "$CREATED_SANDBOX" != "1" ]; then + info "sandbox $SANDBOX was pre-existing and not recreated; leaving it alone" + return + fi info "destroying sandbox $SANDBOX" nemoclaw "$SANDBOX" destroy --yes >/dev/null 2>&1 || true } @@ -114,17 +120,22 @@ section "Onboard sandbox '$SANDBOX'" if [ "${NEMOCLAW_RECREATE_SANDBOX:-0}" = "1" ]; then info "NEMOCLAW_RECREATE_SANDBOX=1 set; destroying existing sandbox first" nemoclaw "$SANDBOX" destroy --yes >/dev/null 2>&1 || true + CREATED_SANDBOX=1 fi -NEMOCLAW_SANDBOX_NAME="$SANDBOX" nemoclaw onboard \ - --provider build \ - --model "$MODEL" 2>&1 | sed 's/^/ /' +NEMOCLAW_SANDBOX_NAME="$SANDBOX" \ + NEMOCLAW_PROVIDER=build \ + NEMOCLAW_MODEL="$MODEL" \ + nemoclaw onboard \ + --non-interactive \ + --yes-i-accept-third-party-software 2>&1 | sed 's/^/ /' ONBOARD_RC=${PIPESTATUS[0]} if [ "$ONBOARD_RC" -ne 0 ]; then fail "onboard exited $ONBOARD_RC" echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" exit 1 fi +CREATED_SANDBOX=1 pass "onboard completed" # ── Schedule cron job ── From 496377ac03e3d9184e1ed80cfca64b2c6f2c0613 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 10:18:23 +0000 Subject: [PATCH 04/13] fix(ci): add coderabbit script entry + onboard env prereqs + shfmt Signed-off-by: Tinson Lai --- .coderabbit.yaml | 22 +++++++++++++++++++ ...test-cron-preflight-inference-local-e2e.sh | 14 ++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 1812f62438..7ade1b138f 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -619,6 +619,28 @@ reviews: gh workflow run nightly-e2e.yaml --ref -f jobs=cloud-inference-e2e ``` + - path: "test/e2e/test-cron-preflight-inference-local-e2e.sh" + instructions: | + This script onboards a sandbox against the managed cloud + inference provider, schedules an isolated agentTurn cron job, + and force-triggers it via `openclaw cron run --wait`, asserting + the provider preflight does not skip the run with EAI_AGAIN or + the "local provider endpoint is not reachable" message. + + Exercises the Dockerfile fetch-guard patch that opts the cron + model-provider preflight into trusted env-proxy mode so the + managed inference hostname (`inference.local`) routes through + the OpenShell L7 proxy instead of failing pinned DNS lookup. + + **E2E test recommendation:** + - `cron-preflight-inference-local-e2e` — managed inference cron + preflight via OpenShell L7 proxy + + To run selectively: + ``` + gh workflow run nightly-e2e.yaml --ref -f jobs=cron-preflight-inference-local-e2e + ``` + - path: "test/e2e/test-openclaw-inference-switch.sh" instructions: | This script validates OpenClaw model/provider switching with diff --git a/test/e2e/test-cron-preflight-inference-local-e2e.sh b/test/e2e/test-cron-preflight-inference-local-e2e.sh index 6b77b6e930..cb75cc6830 100755 --- a/test/e2e/test-cron-preflight-inference-local-e2e.sh +++ b/test/e2e/test-cron-preflight-inference-local-e2e.sh @@ -96,6 +96,16 @@ if [ "${NVIDIA_API_KEY:0:6}" != "nvapi-" ]; then echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" exit 0 fi +if [ "${NEMOCLAW_NON_INTERACTIVE:-}" != "1" ]; then + skip "NEMOCLAW_NON_INTERACTIVE must be 1; refusing to risk an interactive onboard prompt" + echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" + exit 0 +fi +if [ "${NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE:-}" != "1" ]; then + skip "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE must be 1; refusing to risk an interactive onboard prompt" + echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" + exit 0 +fi pass "prerequisites satisfied" CREATED_SANDBOX=0 @@ -127,8 +137,8 @@ NEMOCLAW_SANDBOX_NAME="$SANDBOX" \ NEMOCLAW_PROVIDER=build \ NEMOCLAW_MODEL="$MODEL" \ nemoclaw onboard \ - --non-interactive \ - --yes-i-accept-third-party-software 2>&1 | sed 's/^/ /' + --non-interactive \ + --yes-i-accept-third-party-software 2>&1 | sed 's/^/ /' ONBOARD_RC=${PIPESTATUS[0]} if [ "$ONBOARD_RC" -ne 0 ]; then fail "onboard exited $ONBOARD_RC" From 5e95adc9c0b837fae35a45593ab2254e38550b1f Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 10:35:00 +0000 Subject: [PATCH 05/13] fix(e2e): cron preflight script must install nemoclaw via install.sh Signed-off-by: Tinson Lai --- ...test-cron-preflight-inference-local-e2e.sh | 77 ++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/test/e2e/test-cron-preflight-inference-local-e2e.sh b/test/e2e/test-cron-preflight-inference-local-e2e.sh index cb75cc6830..412688c1e6 100755 --- a/test/e2e/test-cron-preflight-inference-local-e2e.sh +++ b/test/e2e/test-cron-preflight-inference-local-e2e.sh @@ -65,14 +65,22 @@ else echo "ERROR: Cannot find repo root." exit 1 fi +unset _script_dir _candidate cd "$REPO" || { echo "ERROR: Cannot cd into repo root '$REPO'." exit 1 } +E2E_DIR="${REPO}/test/e2e" SANDBOX="${NEMOCLAW_SANDBOX_NAME:-e2e-cron-preflight}" MODEL="${NEMOCLAW_CRON_PREFLIGHT_MODEL:-nvidia/nemotron-3-super-120b-a12b}" WAIT_TIMEOUT="${NEMOCLAW_CRON_PREFLIGHT_WAIT:-90s}" +INSTALL_LOG="/tmp/nemoclaw-e2e-cron-preflight-install.log" + +# shellcheck source=test/e2e/lib/sandbox-teardown.sh +. "${E2E_DIR}/lib/sandbox-teardown.sh" +# shellcheck source=test/e2e/lib/install-path-refresh.sh +. "${E2E_DIR}/lib/install-path-refresh.sh" # ── Prereqs ── section "Prerequisites" @@ -108,45 +116,42 @@ if [ "${NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE:-}" != "1" ]; then fi pass "prerequisites satisfied" -CREATED_SANDBOX=0 - -# shellcheck disable=SC2317,SC2329 # invoked via trap -cleanup() { - if [ "${NEMOCLAW_CRON_PREFLIGHT_KEEP:-0}" = "1" ]; then - info "NEMOCLAW_CRON_PREFLIGHT_KEEP=1 set; leaving sandbox $SANDBOX in place" - return - fi - if [ "$CREATED_SANDBOX" != "1" ]; then - info "sandbox $SANDBOX was pre-existing and not recreated; leaving it alone" - return - fi - info "destroying sandbox $SANDBOX" - nemoclaw "$SANDBOX" destroy --yes >/dev/null 2>&1 || true -} -trap cleanup EXIT - -# ── Onboard ── -section "Onboard sandbox '$SANDBOX'" -if [ "${NEMOCLAW_RECREATE_SANDBOX:-0}" = "1" ]; then - info "NEMOCLAW_RECREATE_SANDBOX=1 set; destroying existing sandbox first" - nemoclaw "$SANDBOX" destroy --yes >/dev/null 2>&1 || true - CREATED_SANDBOX=1 +# ── Install NemoClaw + onboard sandbox ── +section "Install NemoClaw + onboard sandbox '$SANDBOX'" +export NEMOCLAW_SANDBOX_NAME="$SANDBOX" +export NEMOCLAW_RECREATE_SANDBOX="${NEMOCLAW_RECREATE_SANDBOX:-1}" +export NEMOCLAW_PROVIDER="${NEMOCLAW_PROVIDER:-build}" +export NEMOCLAW_MODEL="${NEMOCLAW_MODEL:-$MODEL}" + +info "Installing NemoClaw via install.sh --non-interactive..." +bash install.sh --non-interactive --yes-i-accept-third-party-software >"$INSTALL_LOG" 2>&1 & +install_pid=$! +tail -f "$INSTALL_LOG" --pid=$install_pid 2>/dev/null & +tail_pid=$! +wait "$install_pid" +install_exit=$? +kill "$tail_pid" 2>/dev/null || true +wait "$tail_pid" 2>/dev/null || true + +nemoclaw_refresh_install_env +export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" +# shellcheck source=/dev/null +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" +nemoclaw_ensure_local_bin_on_path + +if [ "$install_exit" -ne 0 ]; then + fail "install.sh failed (exit $install_exit)" + tail -30 "$INSTALL_LOG" + exit 1 fi +pass "NemoClaw installed + sandbox onboarded" -NEMOCLAW_SANDBOX_NAME="$SANDBOX" \ - NEMOCLAW_PROVIDER=build \ - NEMOCLAW_MODEL="$MODEL" \ - nemoclaw onboard \ - --non-interactive \ - --yes-i-accept-third-party-software 2>&1 | sed 's/^/ /' -ONBOARD_RC=${PIPESTATUS[0]} -if [ "$ONBOARD_RC" -ne 0 ]; then - fail "onboard exited $ONBOARD_RC" - echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" +command -v nemoclaw >/dev/null 2>&1 || { + fail "nemoclaw not on PATH after install" exit 1 -fi -CREATED_SANDBOX=1 -pass "onboard completed" +} + +register_sandbox_for_teardown "$SANDBOX" # ── Schedule cron job ── section "Schedule isolated agentTurn cron job" From afd2ee526070b18363f563b489e76b2f4072dafa Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 11:32:37 +0000 Subject: [PATCH 06/13] fix(e2e): source proxy env so openclaw cron picks up admin gateway token Signed-off-by: Tinson Lai --- .../test-cron-preflight-inference-local-e2e.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/e2e/test-cron-preflight-inference-local-e2e.sh b/test/e2e/test-cron-preflight-inference-local-e2e.sh index 412688c1e6..f4ca62aff1 100755 --- a/test/e2e/test-cron-preflight-inference-local-e2e.sh +++ b/test/e2e/test-cron-preflight-inference-local-e2e.sh @@ -153,10 +153,23 @@ command -v nemoclaw >/dev/null 2>&1 || { register_sandbox_for_teardown "$SANDBOX" +# Run an openclaw command inside the sandbox with the runtime shell env +# (/tmp/nemoclaw-proxy-env.sh) sourced first. The non-interactive `exec` +# path bypasses the system-wide shell hook that sources this file on login, +# leaving OPENCLAW_GATEWAY_TOKEN unset and forcing the call into a fresh +# baseline-scope device pair — so privileged operations like `cron add` +# trip a scope-upgrade approval that never completes in CI. Sourcing it +# explicitly hands the call the admin token (mirrors connect-shell). +sandbox_openclaw() { + nemoclaw "$SANDBOX" exec -- \ + sh -c '. /tmp/nemoclaw-proxy-env.sh && exec openclaw "$@"' \ + nemoclaw-openclaw "$@" +} + # ── Schedule cron job ── section "Schedule isolated agentTurn cron job" JOB_NAME="preflight-$(date +%s)" -ADD_OUT="$(nemoclaw "$SANDBOX" exec -- openclaw cron add \ +ADD_OUT="$(sandbox_openclaw cron add \ --name "$JOB_NAME" \ --agent main \ --session isolated \ @@ -183,7 +196,7 @@ pass "scheduled job $JOB_NAME ($JOB_ID)" # ── Force-trigger + wait ── section "Force-trigger and wait" -RUN_OUT="$(nemoclaw "$SANDBOX" exec -- openclaw cron run "$JOB_ID" \ +RUN_OUT="$(sandbox_openclaw cron run "$JOB_ID" \ --wait --wait-timeout "$WAIT_TIMEOUT" --json 2>&1)" RUN_RC=$? info "raw cron run output (rc=$RUN_RC):" From 15659bd0efae7a08439adc751c14c45671af70d1 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 11:52:01 +0000 Subject: [PATCH 07/13] fix(e2e): probe cron preflight runtime directly instead of via cron CLI Signed-off-by: Tinson Lai --- ...test-cron-preflight-inference-local-e2e.sh | 159 ++++++++++++------ 1 file changed, 104 insertions(+), 55 deletions(-) diff --git a/test/e2e/test-cron-preflight-inference-local-e2e.sh b/test/e2e/test-cron-preflight-inference-local-e2e.sh index f4ca62aff1..b43fa8426b 100755 --- a/test/e2e/test-cron-preflight-inference-local-e2e.sh +++ b/test/e2e/test-cron-preflight-inference-local-e2e.sh @@ -19,7 +19,6 @@ # NEMOCLAW_SANDBOX_NAME — sandbox name (default: e2e-cron-preflight) # NEMOCLAW_RECREATE_SANDBOX=1 — destroy + recreate if exists # NEMOCLAW_CRON_PREFLIGHT_MODEL — cloud model (default: nvidia/nemotron-3-super-120b-a12b) -# NEMOCLAW_CRON_PREFLIGHT_WAIT — --wait-timeout for cron run (default: 90s) # NEMOCLAW_CRON_PREFLIGHT_KEEP=1 — keep the sandbox after the test for inspection # # Usage: @@ -74,7 +73,6 @@ cd "$REPO" || { E2E_DIR="${REPO}/test/e2e" SANDBOX="${NEMOCLAW_SANDBOX_NAME:-e2e-cron-preflight}" MODEL="${NEMOCLAW_CRON_PREFLIGHT_MODEL:-nvidia/nemotron-3-super-120b-a12b}" -WAIT_TIMEOUT="${NEMOCLAW_CRON_PREFLIGHT_WAIT:-90s}" INSTALL_LOG="/tmp/nemoclaw-e2e-cron-preflight-install.log" # shellcheck source=test/e2e/lib/sandbox-teardown.sh @@ -153,70 +151,121 @@ command -v nemoclaw >/dev/null 2>&1 || { register_sandbox_for_teardown "$SANDBOX" -# Run an openclaw command inside the sandbox with the runtime shell env -# (/tmp/nemoclaw-proxy-env.sh) sourced first. The non-interactive `exec` -# path bypasses the system-wide shell hook that sources this file on login, -# leaving OPENCLAW_GATEWAY_TOKEN unset and forcing the call into a fresh -# baseline-scope device pair — so privileged operations like `cron add` -# trip a scope-upgrade approval that never completes in CI. Sourcing it -# explicitly hands the call the admin token (mirrors connect-shell). -sandbox_openclaw() { - nemoclaw "$SANDBOX" exec -- \ - sh -c '. /tmp/nemoclaw-proxy-env.sh && exec openclaw "$@"' \ - nemoclaw-openclaw "$@" +# ── Probe the cron preflight directly ── +# +# The cron CLI surfaces (`openclaw cron add` / `openclaw cron run`) require +# `operator.admin` scope, which the in-sandbox auto-pair approval sweep +# deliberately excludes from its allowlist. There is no declarative way for +# an external CLI to call those RPCs without an interactive scope-upgrade +# approval, which is plumbing noise for what this test is actually checking. +# +# Patch 6 only changes the `fetchWithSsrFGuard` call inside +# `probeLocalProviderEndpoint`. Invoke that function directly via a node +# script loaded from the in-sandbox OpenClaw dist instead: the probe asserts +# the same behaviour (managed inference base URL reachable from cron +# preflight) without any gateway, scheduler, or device pairing involvement. +section "Probe cron preflight against managed inference base URL" + +PROBE_SRC=$( + cat <<'PROBE_JS' +const fs = require("node:fs"); +const path = require("node:path"); + +function isManagedLocalProvider(provider) { + if (!provider || typeof provider.baseUrl !== "string") return false; + try { + const host = new URL(provider.baseUrl).hostname.toLowerCase(); + return host.endsWith(".local") || host === "localhost" || host === "127.0.0.1"; + } catch { + return false; + } } -# ── Schedule cron job ── -section "Schedule isolated agentTurn cron job" -JOB_NAME="preflight-$(date +%s)" -ADD_OUT="$(sandbox_openclaw cron add \ - --name "$JOB_NAME" \ - --agent main \ - --session isolated \ - --every 12h \ - --message "Reply with the single word: ok." \ - --keep-after-run \ - --json 2>&1)" -ADD_RC=$? -if [ "$ADD_RC" -ne 0 ]; then - fail "cron add exited $ADD_RC" - printf '%s\n' "$ADD_OUT" | sed 's/^/ /' - echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" - exit 1 -fi +(async () => { + const distRoot = "/usr/local/lib/node_modules/openclaw/dist"; + const preflightModule = path.join( + distRoot, + "cron", + "isolated-agent", + "model-preflight.runtime.js", + ); + if (!fs.existsSync(preflightModule)) { + console.error(JSON.stringify({ error: "preflight-module-missing", path: preflightModule })); + process.exit(3); + } + const { preflightCronModelProvider } = require(preflightModule); + if (typeof preflightCronModelProvider !== "function") { + console.error(JSON.stringify({ error: "preflight-export-missing" })); + process.exit(3); + } -JOB_ID="$(printf '%s' "$ADD_OUT" | jq -r '.id // empty' 2>/dev/null || true)" -if [ -z "$JOB_ID" ]; then - fail "cron add returned no id" - printf '%s\n' "$ADD_OUT" | sed 's/^/ /' - echo " Total: $TOTAL Pass: $PASS Fail: $FAIL Skip: $SKIP" - exit 1 -fi -pass "scheduled job $JOB_NAME ($JOB_ID)" + const configPath = process.env.OPENCLAW_CONFIG_PATH || "/sandbox/.openclaw/openclaw.json"; + let cfg; + try { + cfg = JSON.parse(fs.readFileSync(configPath, "utf8")); + } catch (err) { + console.error(JSON.stringify({ error: "config-read-failed", configPath, message: String(err) })); + process.exit(3); + } + + const providers = (cfg.models && cfg.models.providers) || {}; + const providerKey = Object.keys(providers).find((key) => isManagedLocalProvider(providers[key])); + if (!providerKey) { + console.error( + JSON.stringify({ error: "no-managed-local-provider", providers: Object.keys(providers) }), + ); + process.exit(3); + } + const providerCfg = providers[providerKey]; + const modelKey = + providerCfg.defaultModel || + (Array.isArray(providerCfg.models) ? providerCfg.models[0] : undefined) || + "ping"; + + try { + const result = await preflightCronModelProvider({ + cfg, + provider: providerKey, + model: modelKey, + }); + console.log(JSON.stringify({ providerKey, modelKey, baseUrl: providerCfg.baseUrl, result })); + process.exit(result && result.status === "available" ? 0 : 1); + } catch (err) { + console.error(JSON.stringify({ error: "preflight-threw", message: String(err && err.stack ? err.stack : err) })); + process.exit(2); + } +})(); +PROBE_JS +) +PROBE_B64="$(printf '%s' "$PROBE_SRC" | base64 -w 0)" -# ── Force-trigger + wait ── -section "Force-trigger and wait" -RUN_OUT="$(sandbox_openclaw cron run "$JOB_ID" \ - --wait --wait-timeout "$WAIT_TIMEOUT" --json 2>&1)" -RUN_RC=$? -info "raw cron run output (rc=$RUN_RC):" -printf '%s\n' "$RUN_OUT" | sed 's/^/ /' +PROBE_OUT="$(nemoclaw "$SANDBOX" exec -- sh -c " +. /tmp/nemoclaw-proxy-env.sh +__probe=\"\$(mktemp /tmp/nemoclaw-preflight-probe.XXXXXX.cjs)\" +printf %s '$PROBE_B64' | base64 -d > \"\$__probe\" +node \"\$__probe\" +__rc=\$? +rm -f \"\$__probe\" +exit \"\$__rc\" +" 2>&1)" +PROBE_RC=$? +info "preflight probe output (rc=$PROBE_RC):" +printf '%s\n' "$PROBE_OUT" | sed 's/^/ /' -STATUS="$(printf '%s' "$RUN_OUT" | jq -r '.run.status // .status // empty' 2>/dev/null || true)" -REASON="$(printf '%s' "$RUN_OUT" | jq -r '.run.reason // .reason // ""' 2>/dev/null || true)" +STATUS="$(printf '%s' "$PROBE_OUT" | jq -r 'select(.result) | .result.status // empty' 2>/dev/null || true)" +REASON="$(printf '%s' "$PROBE_OUT" | jq -r 'select(.result) | .result.reason // ""' 2>/dev/null || true)" -# ── Assertions ── section "Assertions" -if printf '%s' "$REASON" | grep -qi "EAI_AGAIN"; then +if [ "$PROBE_RC" -ge 2 ]; then + fail "probe harness failed (rc=$PROBE_RC); preflight did not run" +elif printf '%s' "$REASON" | grep -qi "EAI_AGAIN"; then fail "preflight raised EAI_AGAIN; reason='$REASON'" elif printf '%s' "$REASON" | grep -qi "local provider endpoint is not reachable"; then fail "preflight reported endpoint unreachable; reason='$REASON'" -elif [ "$STATUS" = "skipped" ]; then - fail "cron run reported status=skipped; reason='$REASON'" -elif [ "$STATUS" = "ok" ]; then - pass "cron run status=ok" +elif [ "$STATUS" = "available" ]; then + pass "preflight status=available" else - fail "unexpected cron run status='$STATUS' rc=$RUN_RC reason='$REASON'" + fail "unexpected probe status='$STATUS' rc=$PROBE_RC reason='$REASON'" fi section "Summary" From ff80aed497e1549f24da22a9e991dfd8b1ae7fc5 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 12:00:11 +0000 Subject: [PATCH 08/13] fix(e2e): collapse probe sh -c payload to single line for openshell exec Signed-off-by: Tinson Lai --- .../test-cron-preflight-inference-local-e2e.sh | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/e2e/test-cron-preflight-inference-local-e2e.sh b/test/e2e/test-cron-preflight-inference-local-e2e.sh index b43fa8426b..6c3f6995fb 100755 --- a/test/e2e/test-cron-preflight-inference-local-e2e.sh +++ b/test/e2e/test-cron-preflight-inference-local-e2e.sh @@ -239,15 +239,13 @@ PROBE_JS ) PROBE_B64="$(printf '%s' "$PROBE_SRC" | base64 -w 0)" -PROBE_OUT="$(nemoclaw "$SANDBOX" exec -- sh -c " -. /tmp/nemoclaw-proxy-env.sh -__probe=\"\$(mktemp /tmp/nemoclaw-preflight-probe.XXXXXX.cjs)\" -printf %s '$PROBE_B64' | base64 -d > \"\$__probe\" -node \"\$__probe\" -__rc=\$? -rm -f \"\$__probe\" -exit \"\$__rc\" -" 2>&1)" +# openshell sandbox exec rejects any command argument that contains a newline +# or carriage return ("command argument N contains newline or carriage return +# characters"), so the inner `sh -c` payload must be a single physical line. +# Chain with `&&` for success-only steps and `;` for the cleanup tail so the +# probe exit code is preserved end-to-end. +PROBE_SHELL=". /tmp/nemoclaw-proxy-env.sh && __probe=\"\$(mktemp /tmp/nemoclaw-preflight-probe.XXXXXX.cjs)\" && printf %s '$PROBE_B64' | base64 -d > \"\$__probe\" && node \"\$__probe\"; __rc=\$?; rm -f \"\$__probe\"; exit \"\$__rc\"" +PROBE_OUT="$(nemoclaw "$SANDBOX" exec -- sh -c "$PROBE_SHELL" 2>&1)" PROBE_RC=$? info "preflight probe output (rc=$PROBE_RC):" printf '%s\n' "$PROBE_OUT" | sed 's/^/ /' From d128b1adc01d07967c08026a3055c78725beba9f Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 12:10:43 +0000 Subject: [PATCH 09/13] fix(e2e): discover preflight module dynamically + dynamic-import ESM Signed-off-by: Tinson Lai --- ...test-cron-preflight-inference-local-e2e.sh | 98 ++++++++++++++++--- 1 file changed, 84 insertions(+), 14 deletions(-) diff --git a/test/e2e/test-cron-preflight-inference-local-e2e.sh b/test/e2e/test-cron-preflight-inference-local-e2e.sh index 6c3f6995fb..2b27b6f81d 100755 --- a/test/e2e/test-cron-preflight-inference-local-e2e.sh +++ b/test/e2e/test-cron-preflight-inference-local-e2e.sh @@ -170,6 +170,14 @@ PROBE_SRC=$( cat <<'PROBE_JS' const fs = require("node:fs"); const path = require("node:path"); +const url = require("node:url"); + +const AUDIT_CONTEXT = "cron-model-provider-preflight"; +const EXPORT_NAME = "preflightCronModelProvider"; +const DIST_ROOTS = [ + "/usr/local/lib/node_modules/openclaw/dist", + "/usr/lib/node_modules/openclaw/dist", +]; function isManagedLocalProvider(provider) { if (!provider || typeof provider.baseUrl !== "string") return false; @@ -181,21 +189,74 @@ function isManagedLocalProvider(provider) { } } +function findPreflightModule(root) { + const stack = [root]; + while (stack.length > 0) { + const dir = stack.pop(); + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + stack.push(full); + continue; + } + if (!entry.isFile()) continue; + if (!(full.endsWith(".js") || full.endsWith(".mjs") || full.endsWith(".cjs"))) continue; + let body; + try { + body = fs.readFileSync(full, "utf8"); + } catch { + continue; + } + if (body.includes(AUDIT_CONTEXT) && body.includes(EXPORT_NAME)) { + return full; + } + } + } + return null; +} + (async () => { - const distRoot = "/usr/local/lib/node_modules/openclaw/dist"; - const preflightModule = path.join( - distRoot, - "cron", - "isolated-agent", - "model-preflight.runtime.js", - ); - if (!fs.existsSync(preflightModule)) { - console.error(JSON.stringify({ error: "preflight-module-missing", path: preflightModule })); + let target = null; + const scanned = []; + for (const root of DIST_ROOTS) { + if (!fs.existsSync(root)) continue; + scanned.push(root); + target = findPreflightModule(root); + if (target) break; + } + if (!target) { + console.error(JSON.stringify({ error: "preflight-source-not-found", scanned })); process.exit(3); } - const { preflightCronModelProvider } = require(preflightModule); + + let mod; + try { + mod = await import(url.pathToFileURL(target).href); + } catch (err) { + console.error( + JSON.stringify({ + error: "preflight-import-threw", + target, + message: String(err && err.stack ? err.stack : err), + }), + ); + process.exit(3); + } + const preflightCronModelProvider = mod[EXPORT_NAME]; if (typeof preflightCronModelProvider !== "function") { - console.error(JSON.stringify({ error: "preflight-export-missing" })); + console.error( + JSON.stringify({ + error: "preflight-export-missing", + target, + exports: Object.keys(mod), + }), + ); process.exit(3); } @@ -204,7 +265,9 @@ function isManagedLocalProvider(provider) { try { cfg = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch (err) { - console.error(JSON.stringify({ error: "config-read-failed", configPath, message: String(err) })); + console.error( + JSON.stringify({ error: "config-read-failed", configPath, message: String(err) }), + ); process.exit(3); } @@ -228,10 +291,17 @@ function isManagedLocalProvider(provider) { provider: providerKey, model: modelKey, }); - console.log(JSON.stringify({ providerKey, modelKey, baseUrl: providerCfg.baseUrl, result })); + console.log( + JSON.stringify({ providerKey, modelKey, baseUrl: providerCfg.baseUrl, target, result }), + ); process.exit(result && result.status === "available" ? 0 : 1); } catch (err) { - console.error(JSON.stringify({ error: "preflight-threw", message: String(err && err.stack ? err.stack : err) })); + console.error( + JSON.stringify({ + error: "preflight-threw", + message: String(err && err.stack ? err.stack : err), + }), + ); process.exit(2); } })(); From 1fd12ca87f33c773648c50d0dcd4fa40a778925a Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 12:18:36 +0000 Subject: [PATCH 10/13] fix(e2e): filter probe stdout to JSON line before jq parses status Signed-off-by: Tinson Lai --- test/e2e/test-cron-preflight-inference-local-e2e.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/e2e/test-cron-preflight-inference-local-e2e.sh b/test/e2e/test-cron-preflight-inference-local-e2e.sh index 2b27b6f81d..d63e3a5c4a 100755 --- a/test/e2e/test-cron-preflight-inference-local-e2e.sh +++ b/test/e2e/test-cron-preflight-inference-local-e2e.sh @@ -320,8 +320,12 @@ PROBE_RC=$? info "preflight probe output (rc=$PROBE_RC):" printf '%s\n' "$PROBE_OUT" | sed 's/^/ /' -STATUS="$(printf '%s' "$PROBE_OUT" | jq -r 'select(.result) | .result.status // empty' 2>/dev/null || true)" -REASON="$(printf '%s' "$PROBE_OUT" | jq -r 'select(.result) | .result.reason // ""' 2>/dev/null || true)" +# Probe stdout/stderr are interleaved (captured via 2>&1). Pick the structured +# JSON result line (the only line that starts with `{"providerKey"`) before +# parsing, so undici experimental-feature warnings on stderr do not break jq. +PROBE_JSON="$(printf '%s\n' "$PROBE_OUT" | grep -E '^\s*\{"providerKey"' | tail -n 1)" +STATUS="$(printf '%s' "$PROBE_JSON" | jq -r '.result.status // empty' 2>/dev/null || true)" +REASON="$(printf '%s' "$PROBE_JSON" | jq -r '.result.reason // ""' 2>/dev/null || true)" section "Assertions" if [ "$PROBE_RC" -ge 2 ]; then From 8b18578cba08cc1769bfbe3f2d8e465b057f0910 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 12:53:01 +0000 Subject: [PATCH 11/13] fix(sandbox): tighten Patch 6 to single-callsite proof + add regression cases Signed-off-by: Tinson Lai --- Dockerfile | 35 +++- test/fetch-guard-patch-regression.test.ts | 211 ++++++++++++++++++++++ 2 files changed, 237 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index a720911110..a4053fa806 100644 --- a/Dockerfile +++ b/Dockerfile @@ -395,26 +395,43 @@ RUN set -eu; \ # protection is retained through the existing hostname allowlist and the \ # proxy's own ACLs. \ # \ - # The patch keys on three co-located markers in the same file — the audit \ - # context literal, the `fetchWithSsrFGuard(` call, and the \ - # `buildLocalProviderSsrFPolicy` helper that builds the surrounding policy — \ - # to pin the rewrite to the preflight call and fail closed if upstream drift \ - # ever reuses the audit-context string outside this shape. \ + # The patch keys on the co-located shape of the reviewed preflight call: in \ + # any file that mentions the audit context literal, both the \ + # `fetchWithSsrFGuard(` helper and the `buildLocalProviderSsrFPolicy` policy \ + # builder must appear; the audit literal itself must appear exactly once; and \ + # after patching exactly one patched literal must remain. Any ambiguous \ + # multi-callsite or mixed patched/unpatched layout fails the image build \ + # rather than silently widening the rewrite. \ + # \ + # Removal condition: drop this block (and any related `OC_VERSION` floor bump) \ + # once an OpenClaw release sets `mode: "trusted_env_proxy"` directly at the \ + # preflight call site or otherwise routes the managed inference base URL \ + # through the env-proxy dispatcher by default. The reviewed shape lives at \ + # `src/cron/isolated-agent/model-preflight.runtime.ts` in the openclaw repo. \ preflight_files="$(grep -RIlF --include='*.js' 'cron-model-provider-preflight' "$OC_DIST" || true)"; \ if [ -n "$preflight_files" ]; then \ patched_preflight=0; \ for f in $preflight_files; do \ + audit_count="$(grep -Fc 'auditContext: "cron-model-provider-preflight"' "$f" || true)"; \ + [ "${audit_count:-0}" -ge 1 ] \ + || patch_fail "Patch 6 shape gate: $f mentions cron-model-provider-preflight but has no auditContext literal"; \ + [ "${audit_count:-0}" -eq 1 ] \ + || patch_fail "Patch 6 shape gate: $f has ${audit_count} auditContext literals (expected exactly 1); refusing ambiguous multi-callsite rewrite"; \ grep -Fq 'fetchWithSsrFGuard(' "$f" \ || patch_fail "Patch 6 shape gate: $f has cron-model-provider-preflight but no fetchWithSsrFGuard call"; \ grep -Fq 'buildLocalProviderSsrFPolicy' "$f" \ || patch_fail "Patch 6 shape gate: $f has cron-model-provider-preflight but no buildLocalProviderSsrFPolicy"; \ - if grep -Fq 'mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"' "$f"; then \ + patched_count="$(grep -Fc 'mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"' "$f" || true)"; \ + if [ "${patched_count:-0}" -eq 1 ]; then \ echo "INFO: Patch 6 already present in $f"; \ - else \ + elif [ "${patched_count:-0}" -eq 0 ]; then \ sed -i -E 's|auditContext: "cron-model-provider-preflight"|mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"|g' "$f"; \ - grep -Fq 'mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"' "$f" \ - || patch_fail "Patch 6 verification failed for $f"; \ + new_patched_count="$(grep -Fc 'mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"' "$f" || true)"; \ + [ "${new_patched_count:-0}" -eq 1 ] \ + || patch_fail "Patch 6 verification: expected exactly one patched literal in $f, found ${new_patched_count}"; \ patched_preflight=1; \ + else \ + patch_fail "Patch 6 shape gate: $f has ${patched_count} already-patched literals (expected 0 or 1); refusing mixed-state rewrite"; \ fi; \ done; \ if [ "$patched_preflight" = "1" ]; then \ diff --git a/test/fetch-guard-patch-regression.test.ts b/test/fetch-guard-patch-regression.test.ts index df1bf51c52..b8b871d400 100644 --- a/test/fetch-guard-patch-regression.test.ts +++ b/test/fetch-guard-patch-regression.test.ts @@ -1210,4 +1210,215 @@ if (!blocked) throw new Error('private IP literal was not blocked');`, fs.rmSync(tmp, { recursive: true, force: true }); } }); + + function reviewedCronPreflightFixture({ + auditOccurrences = 1, + includeFetchWithSsrFGuard = true, + includeBuildLocalProviderSsrFPolicy = true, + patchedOccurrences = 0, + }: { + auditOccurrences?: number; + includeFetchWithSsrFGuard?: boolean; + includeBuildLocalProviderSsrFPolicy?: boolean; + patchedOccurrences?: number; + } = {}): string { + const lines: string[] = [ + "const PREFLIGHT_TIMEOUT_MS = 2500;", + "function buildProbeUrl(api, baseUrl) { return baseUrl + (api === 'ollama' ? '/api/tags' : '/models'); }", + ]; + const policyHelper = includeBuildLocalProviderSsrFPolicy + ? "buildLocalProviderSsrFPolicy" + : "buildDriftedSsrFPolicy"; + if (includeBuildLocalProviderSsrFPolicy) { + lines.push( + "function buildLocalProviderSsrFPolicy(baseUrl) {", + " const parsed = new URL(baseUrl);", + " return { hostnameAllowlist: [parsed.hostname], allowPrivateNetwork: true };", + "}", + ); + } else { + lines.push( + "function buildDriftedSsrFPolicy(baseUrl) {", + " const parsed = new URL(baseUrl);", + " return { hostnameAllowlist: [parsed.hostname] };", + "}", + ); + } + lines.push("async function probeLocalProviderEndpoint(params) {"); + for (let index = 0; index < patchedOccurrences; index += 1) { + lines.push( + ` const ${index === 0 ? "patched" : `patched_${index}`} = await ${ + includeFetchWithSsrFGuard ? "fetchWithSsrFGuard" : "callPatchedFetch" + }({`, + ` url: buildProbeUrl(params.api, params.baseUrl),`, + ` policy: ${policyHelper}(params.baseUrl),`, + ` timeoutMs: PREFLIGHT_TIMEOUT_MS,`, + ` mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight",`, + " });", + ); + } + for (let index = 0; index < auditOccurrences - patchedOccurrences; index += 1) { + lines.push( + ` const ${index === 0 ? "result" : `result_${index}`} = await ${ + includeFetchWithSsrFGuard ? "fetchWithSsrFGuard" : "callUnpatchedFetch" + }({`, + ` url: buildProbeUrl(params.api, params.baseUrl),`, + ` policy: ${policyHelper}(params.baseUrl),`, + ` timeoutMs: PREFLIGHT_TIMEOUT_MS,`, + ` auditContext: "cron-model-provider-preflight",`, + " });", + ); + } + lines.push( + " return null;", + "}", + "export { probeLocalProviderEndpoint, preflightCronModelProvider };", + "function preflightCronModelProvider() {}", + "", + ); + return lines.join("\n"); + } + + function writeNeighbouringFetchGuardFixtures(dist: string): void { + // Earlier patches in the same RUN block (1, 2, 2b, 4) only need the dist to + // navigate their "not needed" branches; mirror the shape proven by the + // "skips the strict export patch when strict fetch mode is absent" test so + // execution reaches Patch 6 without classifying the dist as unknown. + fs.writeFileSync( + path.join(dist, "media-runtime.js"), + "export { readRemoteMediaBuffer, saveRemoteMedia, fetchRemoteMedia };\n", + ); + fs.writeFileSync( + path.join(dist, "fetch-guard-neighbour.js"), + [ + "const withTrustedEnvProxyGuardedFetchMode = Symbol('trusted');", + "async function fetchGuardedMediaResponse() {", + " return fetchWithSsrFGuard(withTrustedEnvProxyGuardedFetchMode({}));", + "}", + "export { withTrustedEnvProxyGuardedFetchMode as a };", + "", + ].join("\n"), + ); + } + + it("applies Patch 6 to a reviewed single-callsite cron preflight fixture", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-patch6-happy-")); + const dist = path.join(tmp, "dist"); + fs.mkdirSync(dist, { recursive: true }); + writeNeighbouringFetchGuardFixtures(dist); + const preflightPath = path.join(dist, "model-preflight.runtime.js"); + fs.writeFileSync(preflightPath, reviewedCronPreflightFixture()); + try { + const patch = runFetchGuardPatchBlock(dist, tmp); + expect(patch.status, `${patch.stdout}${patch.stderr}`).toBe(0); + expect(patch.stdout).toContain( + "Patch 6 applied to OpenClaw 2026.5.27 cron preflight trusted env-proxy", + ); + const patched = fs.readFileSync(preflightPath, "utf-8"); + expect( + patched.match( + /mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"/g, + )?.length, + ).toBe(1); + expect(patched).not.toMatch( + /(? { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-patch6-idempotent-")); + const dist = path.join(tmp, "dist"); + fs.mkdirSync(dist, { recursive: true }); + writeNeighbouringFetchGuardFixtures(dist); + const preflightPath = path.join(dist, "model-preflight.runtime.js"); + const source = reviewedCronPreflightFixture({ auditOccurrences: 1, patchedOccurrences: 1 }); + fs.writeFileSync(preflightPath, source); + try { + const patch = runFetchGuardPatchBlock(dist, tmp); + expect(patch.status, `${patch.stdout}${patch.stderr}`).toBe(0); + expect(patch.stdout).toContain("Patch 6 already present in"); + expect(patch.stdout).not.toContain("Patch 6 applied to OpenClaw"); + expect(fs.readFileSync(preflightPath, "utf-8")).toBe(source); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); + + it("skips Patch 6 when the dist has no cron preflight references", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-patch6-absent-")); + const dist = path.join(tmp, "dist"); + fs.mkdirSync(dist, { recursive: true }); + writeNeighbouringFetchGuardFixtures(dist); + try { + const patch = runFetchGuardPatchBlock(dist, tmp); + expect(patch.status, `${patch.stdout}${patch.stderr}`).toBe(0); + expect(patch.stdout).toContain( + "OpenClaw 2026.5.27 has no cron model-provider preflight; Patch 6 not needed", + ); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); + + it("fails Patch 6 closed when the fetchWithSsrFGuard helper is missing", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-patch6-no-fetch-")); + const dist = path.join(tmp, "dist"); + fs.mkdirSync(dist, { recursive: true }); + writeNeighbouringFetchGuardFixtures(dist); + fs.writeFileSync( + path.join(dist, "model-preflight.runtime.js"), + reviewedCronPreflightFixture({ includeFetchWithSsrFGuard: false }), + ); + try { + const patch = runFetchGuardPatchBlock(dist, tmp); + expect(patch.status).toBe(1); + expect(patch.stderr).toContain( + "Patch 6 shape gate: ", + ); + expect(patch.stderr).toContain("no fetchWithSsrFGuard call"); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); + + it("fails Patch 6 closed when the SsrF policy helper is missing", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-patch6-no-policy-")); + const dist = path.join(tmp, "dist"); + fs.mkdirSync(dist, { recursive: true }); + writeNeighbouringFetchGuardFixtures(dist); + fs.writeFileSync( + path.join(dist, "model-preflight.runtime.js"), + reviewedCronPreflightFixture({ includeBuildLocalProviderSsrFPolicy: false }), + ); + try { + const patch = runFetchGuardPatchBlock(dist, tmp); + expect(patch.status).toBe(1); + expect(patch.stderr).toContain("Patch 6 shape gate: "); + expect(patch.stderr).toContain("no buildLocalProviderSsrFPolicy"); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); + + it("fails Patch 6 closed when the audit context literal is ambiguous (multi-callsite)", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-patch6-ambiguous-")); + const dist = path.join(tmp, "dist"); + fs.mkdirSync(dist, { recursive: true }); + writeNeighbouringFetchGuardFixtures(dist); + fs.writeFileSync( + path.join(dist, "model-preflight.runtime.js"), + reviewedCronPreflightFixture({ auditOccurrences: 2 }), + ); + try { + const patch = runFetchGuardPatchBlock(dist, tmp); + expect(patch.status).toBe(1); + expect(patch.stderr).toContain("Patch 6 shape gate: "); + expect(patch.stderr).toContain("refusing ambiguous multi-callsite rewrite"); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); }); From 393b8d47238a747ae4f0a58e4f10c20b055379db Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 12:56:03 +0000 Subject: [PATCH 12/13] fix(e2e): pin probe to inference.local, honor keep flag, match probe wording Signed-off-by: Tinson Lai --- .coderabbit.yaml | 17 +++++---- ...test-cron-preflight-inference-local-e2e.sh | 36 ++++++++++++++----- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 7ade1b138f..367914d319 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -622,19 +622,24 @@ reviews: - path: "test/e2e/test-cron-preflight-inference-local-e2e.sh" instructions: | This script onboards a sandbox against the managed cloud - inference provider, schedules an isolated agentTurn cron job, - and force-triggers it via `openclaw cron run --wait`, asserting - the provider preflight does not skip the run with EAI_AGAIN or - the "local provider endpoint is not reachable" message. + inference provider, then loads OpenClaw's cron isolated-agent + preflight runtime directly from the in-sandbox dist and calls + `preflightCronModelProvider` against the onboarded + `inference.local` provider/model, asserting the result reports + `status: "available"` and never `EAI_AGAIN` or the + "local provider endpoint is not reachable" message. Exercises the Dockerfile fetch-guard patch that opts the cron model-provider preflight into trusted env-proxy mode so the managed inference hostname (`inference.local`) routes through the OpenShell L7 proxy instead of failing pinned DNS lookup. + The scheduler boundary is intentionally bypassed: the cron CLI + surfaces require `operator.admin` scope, which the in-sandbox + auto-pair approval sweep deliberately omits. **E2E test recommendation:** - - `cron-preflight-inference-local-e2e` — managed inference cron - preflight via OpenShell L7 proxy + - `cron-preflight-inference-local-e2e` — direct cron preflight + runtime probe via OpenShell L7 proxy To run selectively: ``` diff --git a/test/e2e/test-cron-preflight-inference-local-e2e.sh b/test/e2e/test-cron-preflight-inference-local-e2e.sh index d63e3a5c4a..21ad66db32 100755 --- a/test/e2e/test-cron-preflight-inference-local-e2e.sh +++ b/test/e2e/test-cron-preflight-inference-local-e2e.sh @@ -5,11 +5,18 @@ # Cron preflight inference.local E2E. # # Onboards a fresh sandbox against the managed cloud provider (whose base URL -# resolves through `inference.local`), schedules an isolated agentTurn cron -# job, force-triggers it via `openclaw cron run --wait`, and asserts the -# provider preflight does not skip the run with `EAI_AGAIN` or the +# resolves through `inference.local`), then loads OpenClaw's cron isolated-agent +# preflight runtime directly from the in-sandbox dist and invokes +# `preflightCronModelProvider` against the onboarded provider/model. Asserts +# the call returns `status: "available"` and never reports `EAI_AGAIN` or the # "local provider endpoint is not reachable" message. # +# This probes the exact runtime path Patch 6 modifies — the cron CLI surfaces +# (`openclaw cron add` / `openclaw cron run`) need `operator.admin` scope, which +# the in-sandbox auto-pair approval sweep deliberately omits from its allowlist, +# so the scheduler boundary is intentionally bypassed in favour of a direct +# runtime probe. +# # Prerequisites: # - Docker running # - NVIDIA_API_KEY set (real key, starts with nvapi-) @@ -149,6 +156,10 @@ command -v nemoclaw >/dev/null 2>&1 || { exit 1 } +# Wire the documented `NEMOCLAW_CRON_PREFLIGHT_KEEP` flag through to the shared +# teardown helper (which honours only `NEMOCLAW_E2E_KEEP_SANDBOX`) so the +# documented escape hatch actually preserves the sandbox for inspection. +export NEMOCLAW_E2E_KEEP_SANDBOX="${NEMOCLAW_E2E_KEEP_SANDBOX:-${NEMOCLAW_CRON_PREFLIGHT_KEEP:-}}" register_sandbox_for_teardown "$SANDBOX" # ── Probe the cron preflight directly ── @@ -174,16 +185,16 @@ const url = require("node:url"); const AUDIT_CONTEXT = "cron-model-provider-preflight"; const EXPORT_NAME = "preflightCronModelProvider"; +const EXPECTED_HOSTNAME = "inference.local"; const DIST_ROOTS = [ "/usr/local/lib/node_modules/openclaw/dist", "/usr/lib/node_modules/openclaw/dist", ]; -function isManagedLocalProvider(provider) { +function isExpectedManagedProvider(provider) { if (!provider || typeof provider.baseUrl !== "string") return false; try { - const host = new URL(provider.baseUrl).hostname.toLowerCase(); - return host.endsWith(".local") || host === "localhost" || host === "127.0.0.1"; + return new URL(provider.baseUrl).hostname.toLowerCase() === EXPECTED_HOSTNAME; } catch { return false; } @@ -272,10 +283,19 @@ function findPreflightModule(root) { } const providers = (cfg.models && cfg.models.providers) || {}; - const providerKey = Object.keys(providers).find((key) => isManagedLocalProvider(providers[key])); + const providerKey = Object.keys(providers).find((key) => + isExpectedManagedProvider(providers[key]), + ); if (!providerKey) { console.error( - JSON.stringify({ error: "no-managed-local-provider", providers: Object.keys(providers) }), + JSON.stringify({ + error: "no-managed-inference-local-provider", + expectedHost: EXPECTED_HOSTNAME, + providers: Object.entries(providers).map(([key, value]) => ({ + key, + baseUrl: value && typeof value.baseUrl === "string" ? value.baseUrl : null, + })), + }), ); process.exit(3); } From f734ccc4354e7df4cc64f2025c57cd810d682ddb Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 10 Jun 2026 13:15:20 +0000 Subject: [PATCH 13/13] style(test): apply biome format to fetch-guard-patch-regression patches Signed-off-by: Tinson Lai --- test/fetch-guard-patch-regression.test.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/test/fetch-guard-patch-regression.test.ts b/test/fetch-guard-patch-regression.test.ts index b8b871d400..c310df38e3 100644 --- a/test/fetch-guard-patch-regression.test.ts +++ b/test/fetch-guard-patch-regression.test.ts @@ -1316,13 +1316,10 @@ if (!blocked) throw new Error('private IP literal was not blocked');`, ); const patched = fs.readFileSync(preflightPath, "utf-8"); expect( - patched.match( - /mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"/g, - )?.length, + patched.match(/mode: "trusted_env_proxy", auditContext: "cron-model-provider-preflight"/g) + ?.length, ).toBe(1); - expect(patched).not.toMatch( - /(?