Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docker/platformclaw-runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion docker/platformclaw-runtime/platformclaw-deploy
Original file line number Diff line number Diff line change
Expand Up @@ -1382,14 +1382,16 @@ 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 && \
create_skillhub_state_backup && \
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
Expand Down
36 changes: 36 additions & 0 deletions docs/upstream/jammy-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://<approved-package-host>/<approved-simple-index-path>/simple
trusted-host = <approved-package-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 <approved-package>
```

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.
Expand Down
27 changes: 21 additions & 6 deletions test/scripts/platformclaw-deploy-upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"] : []),
Expand All @@ -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", () => {
Expand All @@ -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);
Expand All @@ -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",
]);
Expand All @@ -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:");
},
);
});
Expand All @@ -201,16 +212,20 @@ function readEvents(fixture: ReturnType<typeof createUpgradeFixture>) {
return readFileSync(path.join(fixture.root, "events"), "utf8").trim().split("\n");
}

function runUpgrade(fixture: ReturnType<typeof createUpgradeFixture>, command = "set") {
function runUpgrade(
fixture: ReturnType<typeof createUpgradeFixture>,
command = "set",
targetSha = newSha,
) {
return spawnSync(
"bash",
[
fixture.deploy,
"image",
command,
...(command === "update" ? [fixture.archive] : []),
`platformclaw:${newSha}`,
`platformclaw-sandbox:${newSha}`,
`platformclaw:${targetSha}`,
`platformclaw-sandbox:${targetSha}`,
],
{
encoding: "utf8",
Expand Down
4 changes: 2 additions & 2 deletions test/scripts/platformclaw-runtime-docker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)"));
Expand Down