From a5132d182979967bb51e04a690460a21aca5a88e Mon Sep 17 00:00:00 2001 From: devSejung Date: Thu, 22 Oct 2026 15:23:41 +0900 Subject: [PATCH] fix: sandbox package settings stay stale after image updates --- docker/platformclaw-runtime/README.md | 5 ++- .../platformclaw-runtime/platformclaw-deploy | 4 ++- docs/upstream/jammy-build.md | 36 +++++++++++++++++++ .../platformclaw-deploy-upgrade.test.ts | 27 ++++++++++---- .../platformclaw-runtime-docker.test.ts | 4 +-- 5 files changed, 66 insertions(+), 10 deletions(-) diff --git a/docker/platformclaw-runtime/README.md b/docker/platformclaw-runtime/README.md index 03ddfe1f8ec..b4c7267868e 100644 --- a/docker/platformclaw-runtime/README.md +++ b/docker/platformclaw-runtime/README.md @@ -397,7 +397,10 @@ first; preserve the existing `deployment.env` and data directories. The deployment helper loads it into rootful and rootless daemons, switches both refs, waits for health, recreates existing agent sandboxes with the new image, and restores the -prior environment on failure. Before startup it stops the stack, saves a full +prior environment on failure. Sandboxes are retired even when the image refs +are unchanged: an external build input such as `pip.conf` can change the loaded +image under the same source tag. New runs create sandboxes from that image. +Before startup it stops the stack, saves a full Gateway-state snapshot, and runs `openclaw doctor --fix --yes --non-interactive --repair-only` with the new image. This preserves repairs and migrations while deferring diagnostic-only checks. diff --git a/docker/platformclaw-runtime/platformclaw-deploy b/docker/platformclaw-runtime/platformclaw-deploy index 2a1cdc4ad0a..0018e10efef 100755 --- a/docker/platformclaw-runtime/platformclaw-deploy +++ b/docker/platformclaw-runtime/platformclaw-deploy @@ -1382,6 +1382,8 @@ apply_images() { # Doctor owns persisted-state upgrades. Keep a quiesced full state snapshot so # a failed health check can restore both the old image refs and its DB schema. + # External build inputs such as pip.conf can change under the same source tag. + # Always retire existing sandboxes so the next run uses the loaded image bytes. if "${compose[@]}" config --quiet && \ "${compose[@]}" down && \ create_gateway_state_backup && \ @@ -1389,7 +1391,7 @@ apply_images() { run_upgrade_doctor && \ bootstrap_skillhub true && \ "${compose[@]}" up -d --wait && \ - { [[ "$current_sandbox" == "$sandbox_image" ]] || recreate_sandboxes; }; then + recreate_sandboxes; then echo "PlatformClaw updated: $main_image / $sandbox_image" echo "Pre-update Gateway state retained for explicit rollback: $gateway_state_backup" # Read-only findings must never restore a pre-update snapshot after the diff --git a/docs/upstream/jammy-build.md b/docs/upstream/jammy-build.md index 79b440fe09d..10f497a2e8d 100644 --- a/docs/upstream/jammy-build.md +++ b/docs/upstream/jammy-build.md @@ -91,6 +91,42 @@ workspace virtual environment uses the approved indexes. Do not put passwords, tokens, or credential-bearing repository URLs in this file; image readers can extract its contents. Keep the file outside Git. +The build copies this file verbatim; the deployment bundle does not discover or +replace company mirror addresses. Replace obsolete indexes in the local file +before rebuilding. For a single approved HTTP mirror, use this shape: + +```ini +[global] +index-url = http:////simple +trusted-host = +``` + +Keep `trusted-host` in `[global]` so it also applies to `pip download` and +`pip index`. Remove old `extra-index-url` and `find-links` entries when the new +mirror replaces them. The pip config content hash invalidates the image build +cache when the file changes; a successful build proves the included bytes, not +connectivity to the company mirror from the deployment network. + +Transfer the newly built image archive together with its matching deployment +bundle, then run `platformclaw-deploy image update` with the archive and image +refs. Both `image set` and `image update` retire existing sandboxes even when +the refs are unchanged, so the next agent run uses the loaded image. Updating +the deployment bundle alone does not change an already baked `/etc/pip.conf`. + +After deployment, verify from an agent sandbox on the company network: + +```bash +python3 -m pip config debug +python3 -m venv /workspace/.venv +/workspace/.venv/bin/python -m pip install +``` + +Use a workspace virtual environment because the sandbox root filesystem is +read-only. `/etc/pip.conf` applies to all sandbox users and fresh virtual +environments; existing user/site config, `PIP_*` environment variables, and +command-line options can override it. Inspect those if the effective index +still differs from the approved mirror. + The sandbox image also preinstalls `python-is-python3` plus pinned `urllib3`, `Markdown`, `markdownify`, and `Pygments` packages. These are image dependencies, so every sandbox receives the same versions without installing them at runtime. diff --git a/test/scripts/platformclaw-deploy-upgrade.test.ts b/test/scripts/platformclaw-deploy-upgrade.test.ts index fce926c757e..5dd51d059b6 100644 --- a/test/scripts/platformclaw-deploy-upgrade.test.ts +++ b/test/scripts/platformclaw-deploy-upgrade.test.ts @@ -120,6 +120,7 @@ case "$*" in [[ -e "$probe_root/running" ]] || exit 93 if [[ "$*" == *"sandbox recreate --all --force"* ]]; then event sandboxes + [[ "$PLATFORMCLAW_TEST_MODE" != recreate-failure ]] || exit 19 elif [[ "$*" == *"doctor --lint --deferred --json"* ]]; then event diagnostics # Model an accepted request while diagnostics run. Rollback would discard it. @@ -139,9 +140,14 @@ esac } describe.skipIf(process.platform === "win32")("PlatformClaw image update lifecycle", () => { - it.each(["set", "update"])("opens services before diagnostics for image %s", (command) => { + it.each([ + ["set", newSha], + ["update", newSha], + ["set", oldSha], + ["update", oldSha], + ])("opens services and refreshes sandboxes for image %s targeting %s", (command, targetSha) => { const fixture = createUpgradeFixture("success"); - const result = runUpgrade(fixture, command); + const result = runUpgrade(fixture, command, targetSha); expect(result.status, result.stderr || result.error?.message).toBe(0); expect(readEvents(fixture)).toEqual([ ...(command === "update" ? ["load", "load"] : []), @@ -155,6 +161,9 @@ describe.skipIf(process.platform === "win32")("PlatformClaw image update lifecyc expect(readFileSync(path.join(fixture.state, "accepted-request"), "utf8")).toBe( "accepted-after-ready\n", ); + expect(readFileSync(fixture.envFile, "utf8")).toContain( + `PLATFORMCLAW_SANDBOX_IMAGE=platformclaw-sandbox:${targetSha}`, + ); }); it("retains new refs and accepted requests when online diagnostics report findings", () => { @@ -172,7 +181,7 @@ describe.skipIf(process.platform === "win32")("PlatformClaw image update lifecyc expect(existsSync(path.join(fixture.root, "running"))).toBe(true); }); - it.each(["repair-failure", "health-failure"])( + it.each(["repair-failure", "health-failure", "recreate-failure"])( "restores old state and images after %s", (mode) => { const fixture = createUpgradeFixture(mode); @@ -182,6 +191,7 @@ describe.skipIf(process.platform === "win32")("PlatformClaw image update lifecyc "down", "repair", ...(mode === "health-failure" ? ["health-failure"] : []), + ...(mode === "recreate-failure" ? ["ready", "sandboxes"] : []), "down", "ready", ]); @@ -193,6 +203,7 @@ describe.skipIf(process.platform === "win32")("PlatformClaw image update lifecyc ); expect(existsSync(path.join(fixture.state, "accepted-request"))).toBe(false); expect(existsSync(path.join(fixture.root, "running"))).toBe(true); + expect(result.stdout).not.toContain("PlatformClaw updated:"); }, ); }); @@ -201,7 +212,11 @@ function readEvents(fixture: ReturnType) { return readFileSync(path.join(fixture.root, "events"), "utf8").trim().split("\n"); } -function runUpgrade(fixture: ReturnType, command = "set") { +function runUpgrade( + fixture: ReturnType, + command = "set", + targetSha = newSha, +) { return spawnSync( "bash", [ @@ -209,8 +224,8 @@ function runUpgrade(fixture: ReturnType, command = "image", command, ...(command === "update" ? [fixture.archive] : []), - `platformclaw:${newSha}`, - `platformclaw-sandbox:${newSha}`, + `platformclaw:${targetSha}`, + `platformclaw-sandbox:${targetSha}`, ], { encoding: "utf8", diff --git a/test/scripts/platformclaw-runtime-docker.test.ts b/test/scripts/platformclaw-runtime-docker.test.ts index ecce60151f0..1295e13b254 100644 --- a/test/scripts/platformclaw-runtime-docker.test.ts +++ b/test/scripts/platformclaw-runtime-docker.test.ts @@ -2262,9 +2262,9 @@ grep -qx 'profile-secret-2' "$secret_root/guide-video-s3-secret-key" applyImages.indexOf('"${compose[@]}" down'), ); expect(deploy.indexOf('"${compose[@]}" up -d --wait &&')).toBeLessThan( - deploy.indexOf('[[ "$current_sandbox" == "$sandbox_image" ]] || recreate_sandboxes'), + deploy.indexOf(" recreate_sandboxes; then"), ); - expect(deploy.indexOf("recreate_sandboxes; }; then")).toBeLessThan( + expect(deploy.indexOf(" recreate_sandboxes; then")).toBeLessThan( deploy.indexOf('echo "PlatformClaw updated: $main_image / $sandbox_image"'), ); const upCommand = deploy.slice(deploy.indexOf(" up)"), deploy.indexOf("\n down)"));