Files
okxlin 203a53bb01 feat: update DSH and publish OpenClaw Docker image
Squash merge of the verified DSH Alpha.2, OpenClaw Docker Hub publication, CI reliability, and security-policy fixes.
2026-09-19 12:01:15 +08:00

182 lines
11 KiB
Python

#!/usr/bin/env python3
"""Verify gateway auth and real sandbox operations against a disposable daemon."""
import argparse
import json
from pathlib import Path
import secrets
import subprocess
import sys
import time
import uuid
ROOT = Path(__file__).resolve().parents[1]
def docker(*args, check=True, timeout=180, **kwargs):
result = subprocess.run(["docker", *args], capture_output=True, text=True, timeout=timeout, **kwargs)
if check and result.returncode:
raise RuntimeError(result.stdout + result.stderr)
return result
def inner_docker(daemon, *args, check=True, timeout=180, **kwargs):
return docker("exec", daemon, "docker", "-H", "unix:///docker/docker.sock", *args,
check=check, timeout=timeout, **kwargs)
def load_image_into_daemon(daemon, image):
with subprocess.Popen(["docker", "save", image], stdout=subprocess.PIPE) as saved:
loaded = subprocess.run(
["docker", "exec", "-i", daemon, "docker", "-H", "unix:///docker/docker.sock", "load"],
stdin=saved.stdout,
capture_output=True,
text=True,
timeout=600,
)
saved.stdout.close()
saved_status = saved.wait(timeout=30)
if saved_status or loaded.returncode:
details = (loaded.stdout + loaded.stderr).strip()
raise RuntimeError(
f"could not load image {image} into the isolated daemon "
f"(save_exit={saved_status}, load_exit={loaded.returncode}): {details}"
)
SANDBOX_PROBE = r"""
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import {execFileSync} from 'node:child_process';
import {resolveSandboxContext} from '/app/dist/plugin-sdk/agent-harness-runtime.js';
const config = {agents: {defaults: {
workspace: '/workspace/agent', skipBootstrap: true,
sandbox: {mode: 'all', scope: 'session', workspaceAccess: 'rw', workspaceRoot: '/workspace/sandboxes',
docker: {image: process.env.SMOKE_SANDBOX_IMAGE, containerPrefix: 'rf-inner-', network: 'none',
readOnlyRoot: true, user: '1000:1000', capDrop: ['ALL']},
browser: {enabled: false}, prune: {idleHours: 0, maxAgeDays: 0}}
}}};
await fs.mkdir('/workspace/agent', {recursive: true});
await fs.writeFile('/home/node/.openclaw/openclaw.json', JSON.stringify(config));
const params = {config, agentId: 'main', sessionKey: 'agent:main:rf-smoke', workspaceDir: '/workspace/agent'};
let sandbox = await resolveSandboxContext(params);
assert.equal(sandbox.backendId, 'docker');
const info = JSON.parse(execFileSync('docker', ['inspect', sandbox.containerName], {encoding: 'utf8'}))[0];
assert.equal(info.HostConfig.NetworkMode, 'none');
assert.equal(info.HostConfig.ReadonlyRootfs, true);
assert.equal(info.HostConfig.Privileged, false);
assert.deepEqual(info.HostConfig.CapDrop, ['ALL']);
assert.ok(info.Mounts.every(m => m.Type === 'tmpfs' ? ['/tmp', '/var/tmp', '/run'].includes(m.Destination)
: m.Type === 'bind' && !m.Source.includes('docker.sock') && (m.Source.startsWith('/workspace/')
|| !m.RW && m.Source.startsWith('/home/node/.openclaw/sandbox/skills-workspaces/'))), JSON.stringify(info.Mounts));
const run = await sandbox.backend.runShellCommand({script: 'printf "sandbox works" > /workspace/probe.txt; cat /workspace/probe.txt; id -u'});
assert.equal(run.code, 0);
assert.match(run.stdout.toString(), /sandbox works1000/);
assert.equal(await fs.readFile('/workspace/agent/probe.txt', 'utf8'), 'sandbox works');
const denied = await sandbox.backend.runShellCommand({script: 'touch /must-not-write', allowFailure: true});
assert.notEqual(denied.code, 0, 'sandbox root must remain read only');
const list = execFileSync('node', ['/app/openclaw.mjs', 'sandbox', 'list', '--json'], {encoding: 'utf8'});
const listed = JSON.parse(list).containers.find(item => item.containerName === sandbox.containerName);
assert.ok(listed?.sessionKey, 'sandbox CLI did not report the real container');
// Upstream qualifies the CLI scope key with the workspace hash.
execFileSync('node', ['/app/openclaw.mjs', 'sandbox', 'recreate', '--session', listed.sessionKey, '--force']);
assert.throws(() => execFileSync('docker', ['inspect', sandbox.containerName], {stdio: 'pipe'}));
sandbox = await resolveSandboxContext(params);
const after = await sandbox.backend.runShellCommand({script: 'cat /workspace/probe.txt'});
assert.equal(after.stdout.toString(), 'sandbox works');
console.log('PASS: OpenClaw provisions, executes, lists and recreates its sandbox; workspace persists, root is read only, network/capabilities/socket are isolated');
"""
def main(image):
components = json.loads((ROOT / "openclaw-builder/configs/components.json").read_text())
daemon_image, sandbox_image = components["test_daemon_image"], components["test_sandbox_image"]
for dependency in (daemon_image, sandbox_image):
if docker("image", "inspect", dependency, check=False).returncode:
docker("pull", "--quiet", dependency, timeout=300)
owner = "rf-openclaw-" + uuid.uuid4().hex[:12]
daemon, gateway = owner + "-daemon", owner + "-gateway"
volumes = [owner + suffix for suffix in ("-socket", "-data", "-workspace", "-state")]
sandbox_tag = "release-factory-test/sandbox:" + owner
gateway_tag = "release-factory-test/gateway:" + owner
token = secrets.token_hex(24)
try:
for volume in volumes:
docker("volume", "create", "--label", f"io.release-factory.test={owner}", volume)
# The gateway and its sandboxes run inside this isolated daemon. It
# receives only the daemon socket and its four test bind roots, never
# the host Docker socket or a host directory outside those roots.
docker("run", "-d", "--name", daemon, "--network", "none", "--privileged",
"--label", f"io.release-factory.test={owner}", "--entrypoint", "dockerd",
"-v", volumes[0] + ":/docker", "-v", volumes[1] + ":/var/lib/docker",
"-v", volumes[2] + ":/workspace", "-v", volumes[3] + ":/home/node/.openclaw", daemon_image,
"--host=unix:///docker/docker.sock", "--bridge=none", "--iptables=false",
"--ip6tables=false", "--ip-forward=false", "--storage-driver=overlay2")
for _ in range(60):
if docker("exec", daemon, "docker", "-H", "unix:///docker/docker.sock", "info", check=False).returncode == 0:
break
time.sleep(1)
else:
raise RuntimeError("isolated Docker daemon did not become ready")
docker("exec", daemon, "sh", "-c", "chown 1000:1000 /docker/docker.sock /workspace /home/node/.openclaw; chmod 660 /docker/docker.sock")
docker("image", "tag", sandbox_image, sandbox_tag)
docker("image", "tag", image, gateway_tag)
load_image_into_daemon(daemon, sandbox_tag)
load_image_into_daemon(daemon, gateway_tag)
inner_docker(daemon, "run", "-d", "--name", gateway, "--network", "none",
"--label", f"io.release-factory.test={owner}",
"-e", "OPENCLAW_GATEWAY_TOKEN=" + token, "-e", "DOCKER_HOST=unix:///docker/docker.sock",
"-e", "SMOKE_SANDBOX_IMAGE=" + sandbox_tag, "-v", "/docker:/docker",
"-v", "/workspace:/workspace", "-v", "/home/node/.openclaw:/home/node/.openclaw",
"--entrypoint", "node", gateway_tag, "/app/openclaw.mjs",
"gateway", "run", "--allow-unconfigured", "--auth", "token", "--bind", "loopback")
for _ in range(90):
probe = inner_docker(daemon, "exec", gateway, "node", "-e",
"fetch('http://127.0.0.1:18789/healthz').then(r=>process.exit(r.status===200?0:1)).catch(()=>process.exit(1))", check=False)
if probe.returncode == 0:
break
if not json.loads(inner_docker(daemon, "inspect", gateway).stdout)[0]["State"]["Running"]:
raise RuntimeError("OpenClaw gateway exited during startup")
time.sleep(2)
else:
raise RuntimeError("OpenClaw gateway did not become healthy")
rejected = inner_docker(daemon, "exec", gateway, "node", "/app/openclaw.mjs", "gateway", "health",
"--url", "ws://127.0.0.1:18789", "--token", "invalid-test-token", check=False)
assert rejected.returncode and any(s in (rejected.stdout + rejected.stderr).lower()
for s in ("unauthorized", "token mismatch", "token_mismatch")), "gateway must reject invalid auth"
healthy = inner_docker(daemon, "exec", gateway, "node", "/app/openclaw.mjs", "gateway", "health", "--json")
assert '"ok": true' in healthy.stdout, "authenticated deep health did not succeed"
print("PASS: gateway liveness, invalid-token rejection and authenticated deep health", flush=True)
result = inner_docker(daemon, "exec", gateway, "node", "--input-type=module", "-e", SANDBOX_PROBE, timeout=300)
print(result.stdout, end="", flush=True)
version = inner_docker(daemon, "exec", gateway, "docker", "compose", "version", "--short")
assert version.stdout.strip(), "bundled Compose must execute"
except BaseException:
for name in (gateway,):
logs = inner_docker(daemon, "logs", "--tail", "40", name, check=False)
print((logs.stdout + logs.stderr).replace(token, "[test token]")[-12000:], file=sys.stderr)
logs = docker("logs", "--tail", "40", daemon, check=False)
print((logs.stdout + logs.stderr).replace(token, "[test token]")[-12000:], file=sys.stderr)
raise
finally:
daemon_label = docker("inspect", "--format", '{{index .Config.Labels "io.release-factory.test"}}', daemon, check=False)
if daemon_label.stdout.strip() == owner:
gateway_label = inner_docker(daemon, "inspect", "--format", '{{index .Config.Labels "io.release-factory.test"}}', gateway, check=False)
if gateway_label.stdout.strip() == owner:
inner_docker(daemon, "rm", "-f", "-v", gateway, check=False)
docker("rm", "-f", "-v", daemon)
for volume in volumes:
label = docker("volume", "inspect", "--format", '{{index .Labels "io.release-factory.test"}}', volume, check=False)
if label.stdout.strip() == owner:
docker("volume", "rm", volume)
for tag in (sandbox_tag, gateway_tag):
original = docker("image", "inspect", "--format", "{{.Id}}", sandbox_image if tag == sandbox_tag else image, check=False)
tagged = docker("image", "inspect", "--format", "{{.Id}}", tag, check=False)
if tagged.returncode == 0 and tagged.stdout == original.stdout:
docker("image", "rm", tag)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("image")
main(parser.parse_args().image)