| author | |
| committer | |
| log | a938184751633f3e50941c60bbc8ef3566475802 |
| tree | f38708c3047a0c1eefb2bfcdbd8ff668d1f51f4c |
| parent | e4ffc9282a8149a9b8a2cacee98111ddddd8d95c |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover) |
14 files changed, 455 insertions(+), 1 deletions(-)
service/forward-auth/service.pkl+1-1| ... | @@ -14,7 +14,7 @@ requirements { | ... | @@ -14,7 +14,7 @@ requirements { |
| 14 | name = module.meta.name | 14 | name = module.meta.name |
| 15 | redirectUris { | 15 | redirectUris { |
| 16 | when (isPreview) { "https://\(module.id).\(site.domain)/snow.oauth2/callback" } | 16 | when (isPreview) { "https://\(module.id).\(site.domain)/snow.oauth2/callback" } |
| 17 | when (!isPreview) { for (host in new Listing<String> { "music"; "seedbox"; "ddns"; "redis"; "pg"; "snr"; "rdr"; "logs"; "metrics"; "traces"; "jkt" }) { | 17 | when (!isPreview) { for (host in new Listing<String> { "music"; "seedbox"; "ddns"; "redis"; "pg"; "snr"; "rdr"; "logs"; "metrics"; "traces"; "jkt"; "openclaw" }) { |
| 18 | "https://\(host).\(site.domain)/snow.oauth2/callback" | 18 | "https://\(host).\(site.domain)/snow.oauth2/callback" |
| 19 | } } | 19 | } } |
| 20 | } | 20 | } |
service/openclaw/Dockerfile.guest created+4| ... | @@ -0,0 +1,4 @@ | ||
| 1 | FROM ghcr.io/openclaw/openclaw@sha256:032e25ae4a15aa2e6d2653699833e0965e99f580e679024e31e3ddda375e9bcd | ||
| 2 | USER root | ||
| 3 | RUN apt-get update && apt-get install -y --no-install-recommends python3 python3-venv python3-pip git ripgrep curl build-essential && rm -rf /var/lib/apt/lists/* | ||
| 4 | USER node | ||
service/openclaw/install.sh created+22| ... | @@ -0,0 +1,22 @@ | ||
| 1 | #!/bin/bash | ||
| 2 | set -e | ||
| 3 | export http_proxy=http://10.0.2.100:3128 https_proxy=http://10.0.2.100:3128 | ||
| 4 | export no_proxy=localhost,127.0.0.1,10.0.2.101 | ||
| 5 | systemctl disable --now systemd-resolved | ||
| 6 | rm -f /etc/resolv.conf | ||
| 7 | install -m 644 /opt/openclaw/resolv.conf /etc/resolv.conf | ||
| 8 | if ! command -v docker >/dev/null; then | ||
| 9 | apt-get -o Acquire::Retries=10 update | ||
| 10 | DEBIAN_FRONTEND=noninteractive apt-get install -y docker.io | ||
| 11 | fi | ||
| 12 | systemctl daemon-reload | ||
| 13 | systemctl restart docker | ||
| 14 | mkdir -p /opt/openclaw/state/workspace | ||
| 15 | if ! test -f /opt/openclaw/state/openclaw.json; then | ||
| 16 | cp /opt/openclaw/openclaw.json /opt/openclaw/state/openclaw.json | ||
| 17 | fi | ||
| 18 | chown -R 1000:1000 /opt/openclaw/state | ||
| 19 | chmod 700 /opt/openclaw/state | ||
| 20 | docker build --build-arg http_proxy --build-arg https_proxy -t openclaw-trial - < /opt/openclaw/Dockerfile | ||
| 21 | systemctl enable openclaw | ||
| 22 | systemctl restart openclaw | ||
service/openclaw/machine-openclaw.slice created+8| ... | @@ -0,0 +1,8 @@ | ||
| 1 | [Unit] | ||
| 2 | Description=OpenClaw virtual machine | ||
| 3 | Before=libvirtd.service | ||
| 4 | |||
| 5 | [Slice] | ||
| 6 | TasksMax=512 | ||
| 7 | MemoryMax=8G | ||
| 8 | CPUQuota=400% | ||
service/openclaw/nginx.conf created+47| ... | @@ -0,0 +1,47 @@ | ||
| 1 | worker_processes 1; | ||
| 2 | pid /tmp/nginx.pid; | ||
| 3 | error_log /dev/stderr; | ||
| 4 | events { worker_connections 256; } | ||
| 5 | http { | ||
| 6 | access_log off; | ||
| 7 | client_body_temp_path /tmp/client; | ||
| 8 | proxy_temp_path /tmp/proxy; | ||
| 9 | fastcgi_temp_path /tmp/fastcgi; | ||
| 10 | uwsgi_temp_path /tmp/uwsgi; | ||
| 11 | scgi_temp_path /tmp/scgi; | ||
| 12 | map $http_upgrade $connection_upgrade { default upgrade; '' close; } | ||
| 13 | server { | ||
| 14 | listen 127.0.0.1:18889; | ||
| 15 | client_max_body_size 32m; | ||
| 16 | location = /healthz { proxy_pass http://127.0.0.1:18890/healthz; } | ||
| 17 | location / { | ||
| 18 | if ($http_x_openclaw_user = '') { return 403; } | ||
| 19 | proxy_pass http://127.0.0.1:18890; | ||
| 20 | proxy_http_version 1.1; | ||
| 21 | proxy_set_header Upgrade $http_upgrade; | ||
| 22 | proxy_set_header Connection $connection_upgrade; | ||
| 23 | proxy_set_header Host openclaw.paperclover.net; | ||
| 24 | proxy_set_header X-Forwarded-Host openclaw.paperclover.net; | ||
| 25 | proxy_set_header X-Forwarded-Proto https; | ||
| 26 | proxy_set_header X-Forwarded-For $http_x_forwarded_for; | ||
| 27 | proxy_set_header X-OpenClaw-User $http_x_openclaw_user; | ||
| 28 | proxy_buffering off; | ||
| 29 | proxy_read_timeout 28800s; | ||
| 30 | } | ||
| 31 | } | ||
| 32 | server { | ||
| 33 | listen 127.0.0.1:18881; | ||
| 34 | location /v1/ { | ||
| 35 | proxy_pass https://ai.paperclover.net; | ||
| 36 | proxy_ssl_server_name on; | ||
| 37 | proxy_ssl_verify on; | ||
| 38 | proxy_ssl_verify_depth 5; | ||
| 39 | proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; | ||
| 40 | proxy_set_header Host ai.paperclover.net; | ||
| 41 | proxy_set_header Authorization "Bearer MODEL_API_KEY"; | ||
| 42 | proxy_buffering off; | ||
| 43 | proxy_read_timeout 28800s; | ||
| 44 | } | ||
| 45 | location / { return 404; } | ||
| 46 | } | ||
| 47 | } | ||
service/openclaw/openclaw-install.service created+14| ... | @@ -0,0 +1,14 @@ | ||
| 1 | [Unit] | ||
| 2 | Description=Prepare OpenClaw trial | ||
| 3 | After=network-online.target | ||
| 4 | StartLimitIntervalSec=0 | ||
| 5 | |||
| 6 | [Service] | ||
| 7 | Type=oneshot | ||
| 8 | RemainAfterExit=yes | ||
| 9 | Restart=on-failure | ||
| 10 | RestartSec=15 | ||
| 11 | ExecStart=/usr/local/sbin/openclaw-install | ||
| 12 | |||
| 13 | [Install] | ||
| 14 | WantedBy=multi-user.target | ||
service/openclaw/openclaw.json created+58| ... | @@ -0,0 +1,58 @@ | ||
| 1 | { | ||
| 2 | "gateway": { | ||
| 3 | "mode": "local", | ||
| 4 | "bind": "lan", | ||
| 5 | "port": 18789, | ||
| 6 | "publicOrigin": "https://openclaw.paperclover.net", | ||
| 7 | "trustedProxies": ["10.0.2.2"], | ||
| 8 | "auth": { | ||
| 9 | "mode": "trusted-proxy", | ||
| 10 | "trustedProxy": { | ||
| 11 | "userHeader": "x-openclaw-user", | ||
| 12 | "requiredHeaders": ["x-forwarded-proto", "x-forwarded-host"], | ||
| 13 | "allowUsers": ["snow", "ai"], | ||
| 14 | "deviceAutoApprove": {"enabled": true} | ||
| 15 | }, | ||
| 16 | "identityScopes": {"snow": ["operator.admin"], "ai": ["operator.admin"]} | ||
| 17 | }, | ||
| 18 | "controlUi": {"allowedOrigins": ["https://openclaw.paperclover.net"]} | ||
| 19 | }, | ||
| 20 | "agents": { | ||
| 21 | "defaults": { | ||
| 22 | "workspace": "/home/node/.openclaw/workspace", | ||
| 23 | "model": {"primary": ""}, | ||
| 24 | "thinkingDefault": "medium", | ||
| 25 | "heartbeat": {"every": "0m"}, | ||
| 26 | "maxConcurrent": 1, | ||
| 27 | "sandbox": {"mode": "off"} | ||
| 28 | } | ||
| 29 | }, | ||
| 30 | "models": { | ||
| 31 | "mode": "replace", | ||
| 32 | "providers": { | ||
| 33 | "snowglobe": { | ||
| 34 | "baseUrl": "http://10.0.2.101:8080/v1", | ||
| 35 | "apiKey": "vm-inference-proxy", | ||
| 36 | "api": "openai-completions", | ||
| 37 | "models": [{ | ||
| 38 | "id": "", "name": "", "contextWindow": 0, | ||
| 39 | "reasoning": true, "input": ["text", "image"], "maxTokens": 16384, | ||
| 40 | "compat": {"toolSchemaProfile": "llamacpp", "supportsReasoningEffort": true} | ||
| 41 | }] | ||
| 42 | } | ||
| 43 | } | ||
| 44 | }, | ||
| 45 | "tools": { | ||
| 46 | "profile": "full", | ||
| 47 | "elevated": {"enabled": false}, | ||
| 48 | "exec": {"host": "gateway", "security": "full", "ask": "off"} | ||
| 49 | }, | ||
| 50 | "browser": { | ||
| 51 | "enabled": true, "headless": true, "noSandbox": true, | ||
| 52 | "ssrfPolicy": {"dangerouslyAllowPrivateNetwork": true}, | ||
| 53 | "defaultProfile": "openclaw", | ||
| 54 | "extraArgs": ["--proxy-server=http://10.0.2.100:3128", "--disable-gpu"] | ||
| 55 | }, | ||
| 56 | "discovery": {"mdns": {"mode": "off"}}, | ||
| 57 | "update": {"checkOnStart": false} | ||
| 58 | } | ||
service/openclaw/openclaw.service created+14| ... | @@ -0,0 +1,14 @@ | ||
| 1 | [Unit] | ||
| 2 | Description=OpenClaw trial | ||
| 3 | Requires=docker.service | ||
| 4 | After=docker.service network-online.target | ||
| 5 | |||
| 6 | [Service] | ||
| 7 | Restart=always | ||
| 8 | RestartSec=5 | ||
| 9 | ExecStartPre=-/usr/bin/docker rm -f openclaw | ||
| 10 | ExecStart=/usr/bin/docker run --name openclaw --network host --memory 4g --cpus 3 --pids-limit 512 --cap-drop ALL --security-opt no-new-privileges --shm-size 512m -e HTTP_PROXY=http://10.0.2.100:3128 -e HTTPS_PROXY=http://10.0.2.100:3128 -e http_proxy=http://10.0.2.100:3128 -e https_proxy=http://10.0.2.100:3128 -e NO_PROXY=localhost,127.0.0.1,10.0.2.101 -e no_proxy=localhost,127.0.0.1,10.0.2.101 -e NODE_USE_ENV_PROXY=1 -e TZ=America/Los_Angeles -v /opt/openclaw/state:/home/node/.openclaw openclaw-trial | ||
| 11 | ExecStop=/usr/bin/docker stop -t 15 openclaw | ||
| 12 | |||
| 13 | [Install] | ||
| 14 | WantedBy=multi-user.target | ||
service/openclaw/prepare.py created+130| ... | @@ -0,0 +1,130 @@ | ||
| 1 | #!/usr/bin/env python3 | ||
| 2 | import glob | ||
| 3 | import hashlib | ||
| 4 | import json | ||
| 5 | import os | ||
| 6 | from pathlib import Path | ||
| 7 | import secrets | ||
| 8 | import shutil | ||
| 9 | import subprocess | ||
| 10 | import sys | ||
| 11 | import urllib.request | ||
| 12 | import xml.etree.ElementTree as ET | ||
| 13 | |||
| 14 | |||
| 15 | data = json.load(sys.stdin) | ||
| 16 | root = Path(data["hostRoot"]) | ||
| 17 | if root.name != "openclaw": | ||
| 18 | raise ValueError("OpenClaw's dedicated VM is production-only") | ||
| 19 | source = Path(__file__).parent | ||
| 20 | vm = root / "vm" | ||
| 21 | vm.mkdir(mode=0o700, exist_ok=True) | ||
| 22 | cfg = root / "cfg" | ||
| 23 | cfg.mkdir(mode=0o700, exist_ok=True) | ||
| 24 | key = json.loads(subprocess.check_output( | ||
| 25 | ["nomad", "var", "get", "-out=json", "nomad/jobs/local-ai"], | ||
| 26 | text=True, stderr=subprocess.DEVNULL, | ||
| 27 | ))["Items"]["api_key"] | ||
| 28 | if not key.isalnum(): | ||
| 29 | raise ValueError("Invalid model API credential") | ||
| 30 | nginx = source.joinpath("nginx.conf").read_text().replace("MODEL_API_KEY", key) | ||
| 31 | pending = cfg / ".nginx.conf.pending" | ||
| 32 | pending.write_text(nginx) | ||
| 33 | pending.chmod(0o600) | ||
| 34 | os.chown(pending, data["uid"], data["uid"]) | ||
| 35 | pending.replace(cfg / "nginx.conf") | ||
| 36 | os.chown(cfg, data["uid"], data["uid"]) | ||
| 37 | |||
| 38 | if not (vm / "ssh_key").exists(): | ||
| 39 | subprocess.run(["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(vm / "ssh_key")], check=True) | ||
| 40 | |||
| 41 | catalog = json.loads(source.parent.joinpath("local-ai/catalog.json").read_text())["models"][0] | ||
| 42 | config = json.loads(source.joinpath("openclaw.json").read_text()) | ||
| 43 | # Browser control needs its own password under trusted-proxy auth. | ||
| 44 | previous = json.loads((vm / "openclaw.json").read_text()) if (vm / "openclaw.json").exists() else {} | ||
| 45 | config["gateway"]["auth"]["password"] = previous.get("gateway", {}).get("auth", {}).get("password") or secrets.token_hex(24) | ||
| 46 | model = config["models"]["providers"]["snowglobe"]["models"][0] | ||
| 47 | model.update(id=catalog["slug"], name=catalog["display_name"], contextWindow=catalog["context_window"]) | ||
| 48 | config["agents"]["defaults"]["model"]["primary"] = "snowglobe/" + catalog["slug"] | ||
| 49 | (vm / "openclaw.json").write_text(json.dumps(config, indent=2) + "\n") | ||
| 50 | |||
| 51 | if not (vm / "disk.qcow2").exists(): | ||
| 52 | image = vm / "ubuntu.img" | ||
| 53 | expected = "6a81c37564db9b1ee84e141922625e1d7c5b389b99bb3c572e0243607d5bb4d2" | ||
| 54 | if not image.exists(): | ||
| 55 | urllib.request.urlretrieve( | ||
| 56 | "https://cloud-images.ubuntu.com/noble/20260926/noble-server-cloudimg-amd64.img", image, | ||
| 57 | ) | ||
| 58 | with image.open("rb") as stream: | ||
| 59 | if hashlib.file_digest(stream, "sha256").hexdigest() != expected: | ||
| 60 | raise ValueError("Ubuntu image checksum mismatch") | ||
| 61 | subprocess.run(["qemu-img", "convert", "-O", "qcow2", str(image), str(vm / ".disk.pending")], check=True) | ||
| 62 | subprocess.run(["qemu-img", "resize", str(vm / ".disk.pending"), "32G"], check=True) | ||
| 63 | |||
| 64 | writes = [ | ||
| 65 | {"path": "/etc/apt/apt.conf.d/90proxy", "content": 'Acquire::http::Proxy "http://10.0.2.100:3128";\nAcquire::https::Proxy "http://10.0.2.100:3128";\n'}, | ||
| 66 | {"path": "/etc/systemd/system/docker.service.d/proxy.conf", "content": "[Service]\nEnvironment=HTTP_PROXY=http://10.0.2.100:3128 HTTPS_PROXY=http://10.0.2.100:3128 NO_PROXY=localhost,127.0.0.1,10.0.2.101\n"}, | ||
| 67 | {"path": "/opt/openclaw/openclaw.json", "content": json.dumps(config, indent=2), "permissions": "0600"}, | ||
| 68 | {"path": "/opt/openclaw/Dockerfile", "content": source.joinpath("Dockerfile.guest").read_text()}, | ||
| 69 | {"path": "/opt/openclaw/resolv.conf", "content": source.joinpath("resolv.conf").read_text()}, | ||
| 70 | {"path": "/etc/systemd/system/openclaw.service", "content": source.joinpath("openclaw.service").read_text()}, | ||
| 71 | {"path": "/usr/local/sbin/openclaw-install", "content": source.joinpath("install.sh").read_text(), "permissions": "0755"}, | ||
| 72 | {"path": "/etc/systemd/system/openclaw-install.service", "content": source.joinpath("openclaw-install.service").read_text()}, | ||
| 73 | ] | ||
| 74 | cloud = { | ||
| 75 | "hostname": "openclaw", "manage_etc_hosts": True, "ssh_pwauth": False, | ||
| 76 | "disable_root": True, "users": [{"name": "ocadmin", "lock_passwd": True, | ||
| 77 | "shell": "/bin/bash", "sudo": "ALL=(ALL) NOPASSWD:ALL", | ||
| 78 | "ssh_authorized_keys": [(vm / "ssh_key.pub").read_text().strip()]}], | ||
| 79 | "write_files": writes, | ||
| 80 | "bootcmd": [["systemctl", "mask", "--now", "snapd.service", "snapd.socket", "snapd.seeded.service", "snapd.autoimport.service"]], | ||
| 81 | "runcmd": [["systemctl", "daemon-reload"], ["systemctl", "enable", "--now", "openclaw-install"]], | ||
| 82 | } | ||
| 83 | (vm / "user-data").write_text("#cloud-config\n" + json.dumps(cloud)) | ||
| 84 | (vm / "meta-data").write_text("instance-id: openclaw-trial\nlocal-hostname: openclaw\n") | ||
| 85 | iso = shutil.which("genisoimage") or sorted(glob.glob("/nix/store/*-cdrkit-*/bin/genisoimage"))[0] | ||
| 86 | subprocess.run([iso, "-quiet", "-output", str(vm / "seed.iso"), "-volid", "cidata", "-joliet", "-rock", str(vm / "user-data"), str(vm / "meta-data")], check=True) | ||
| 87 | (vm / ".disk.pending").replace(vm / "disk.qcow2") | ||
| 88 | image.unlink() | ||
| 89 | |||
| 90 | qemu = "http://libvirt.org/schemas/domain/qemu/1.0" | ||
| 91 | units = Path("/etc/systemd/system.control") | ||
| 92 | units.mkdir(exist_ok=True) | ||
| 93 | shutil.copyfile(source / "machine-openclaw.slice", units / "machine-openclaw.slice") | ||
| 94 | dependency = units / "libvirtd.service.d" | ||
| 95 | dependency.mkdir(exist_ok=True) | ||
| 96 | (dependency / "openclaw.conf").write_text("[Unit]\nRequires=machine-openclaw.slice\nAfter=machine-openclaw.slice\n") | ||
| 97 | subprocess.run(["systemctl", "daemon-reload"], check=True) | ||
| 98 | subprocess.run(["systemctl", "start", "machine-openclaw.slice"], check=True) | ||
| 99 | ET.register_namespace("qemu", qemu) | ||
| 100 | domain = ET.fromstring(f"""<domain type="kvm"> | ||
| 101 | <name>openclaw</name><memory unit="MiB">6144</memory><vcpu>4</vcpu> | ||
| 102 | <resource><partition>/machine/openclaw</partition></resource> | ||
| 103 | <os><type arch="x86_64" machine="q35">hvm</type><boot dev="hd"/></os> | ||
| 104 | <features><acpi/><apic/></features><cpu mode="host-passthrough"/> | ||
| 105 | <on_poweroff>destroy</on_poweroff><on_reboot>restart</on_reboot><on_crash>restart</on_crash> | ||
| 106 | <devices> | ||
| 107 | <disk type="file" device="disk"><driver name="qemu" type="qcow2"/><source file="{vm}/disk.qcow2"/><target dev="vda" bus="virtio"/></disk> | ||
| 108 | <disk type="file" device="cdrom"><driver name="qemu" type="raw"/><source file="{vm}/seed.iso"/><target dev="sda" bus="sata"/><readonly/></disk> | ||
| 109 | <serial type="pty"><target port="0"/></serial> | ||
| 110 | <memballoon model="virtio"/><rng model="virtio"><backend model="random">/dev/urandom</backend></rng> | ||
| 111 | </devices> | ||
| 112 | </domain>""") | ||
| 113 | existing = subprocess.run(["virsh", "domuuid", "openclaw"], capture_output=True, text=True) | ||
| 114 | if existing.returncode == 0: | ||
| 115 | ET.SubElement(domain, "uuid").text = existing.stdout.strip() | ||
| 116 | args = ET.SubElement(domain, "{" + qemu + "}commandline") | ||
| 117 | capabilities = ET.SubElement(domain, "{" + qemu + "}capabilities") | ||
| 118 | # Libvirt's spawn/setsid filters block the fixed per-connection proxy commands. | ||
| 119 | ET.SubElement(capabilities, "{" + qemu + "}del", capability="seccomp-sandbox") | ||
| 120 | for value in [ | ||
| 121 | "-sandbox", "on,obsolete=deny,elevateprivileges=allow,spawn=allow,resourcecontrol=deny", | ||
| 122 | "-netdev", "user,id=ocnet,restrict=on,ipv6=off,hostfwd=tcp:127.0.0.1:18890-:18789,hostfwd=tcp:127.0.0.1:18892-:22,guestfwd=tcp:10.0.2.100:3128-cmd:/run/current-system/sw/bin/nc 127.0.0.1 18882,guestfwd=tcp:10.0.2.101:8080-cmd:/run/current-system/sw/bin/nc 127.0.0.1 18881,guestfwd=tcp:10.0.2.102:53-cmd:/run/current-system/sw/bin/nc 1.1.1.1 53", | ||
| 123 | "-device", "virtio-net-pci,netdev=ocnet,bus=pcie.0,addr=0x10", | ||
| 124 | ]: | ||
| 125 | ET.SubElement(args, "{" + qemu + "}arg", value=value) | ||
| 126 | (vm / "domain.xml").write_text(ET.tostring(domain, encoding="unicode")) | ||
| 127 | subprocess.run(["virsh", "define", str(vm / "domain.xml")], check=True) | ||
| 128 | subprocess.run(["virsh", "autostart", "openclaw"], check=True) | ||
| 129 | if subprocess.check_output(["virsh", "domstate", "openclaw"], text=True).strip() != "running": | ||
| 130 | subprocess.run(["virsh", "start", "openclaw"], check=True) | ||
service/openclaw/resolv.conf created+2| ... | @@ -0,0 +1,2 @@ | ||
| 1 | nameserver 10.0.2.102 | ||
| 2 | options use-vc timeout:3 attempts:2 | ||
service/openclaw/service.pkl created+42| ... | @@ -0,0 +1,42 @@ | ||
| 1 | amends "../../config/Service.pkl" | ||
| 2 | |||
| 3 | meta { name = "OpenClaw"; access = "infra-admin" } | ||
| 4 | enabled = (read?("env:STUDIO_LOCAL_AI") ?? "false") == "true" | ||
| 5 | stageIsolation = "fresh" | ||
| 6 | healthyDeadline = "30m" | ||
| 7 | healthRestartGrace = "30m" | ||
| 8 | prepare = "prepare.py" | ||
| 9 | setup = "setup.py" | ||
| 10 | |||
| 11 | containers { | ||
| 12 | ["gateway"] { | ||
| 13 | image = "docker.io/library/nginx@sha256:17ae566734b63632e543c907ba74757e0c1a25d812ab9f10a07a6bed98dd199c" | ||
| 14 | hostNetwork = true | ||
| 15 | entrypoint = "/usr/sbin/nginx" | ||
| 16 | args { "-c"; "/cfg/nginx.conf"; "-g"; "daemon off;" } | ||
| 17 | cpu = 200 | ||
| 18 | memory = 256 | ||
| 19 | http { | ||
| 20 | containerPort = 18889 | ||
| 21 | hostPort = 18889 | ||
| 22 | loopback = true | ||
| 23 | subdomain = "openclaw" | ||
| 24 | authRole = "infra-admin" | ||
| 25 | userHeader = "X-OpenClaw-User" | ||
| 26 | checkPath = "/healthz" | ||
| 27 | } | ||
| 28 | volumes { | ||
| 29 | ["/cfg"] {} | ||
| 30 | ["/etc/ssl/certs/ca-certificates.crt"] { src = "/var/lib/studio/ca-bundle.crt"; readOnly = true } | ||
| 31 | } | ||
| 32 | } | ||
| 33 | ["internet"] { | ||
| 34 | image = "docker.io/ubuntu/squid@sha256:8fafd41d6ddceb295d26eea9938321d825ac5351c7e46cf6a8aa5d093b8ed1ce" | ||
| 35 | hostNetwork = true | ||
| 36 | entrypoint = "/usr/sbin/squid" | ||
| 37 | args { "-N"; "-f"; "/config/squid.conf" } | ||
| 38 | cpu = 200 | ||
| 39 | memory = 256 | ||
| 40 | volumes { ["/config/squid.conf"] { config = "squid.conf" } } | ||
| 41 | } | ||
| 42 | } | ||
service/openclaw/setup.py created+33| ... | @@ -0,0 +1,33 @@ | ||
| 1 | #!/usr/bin/env python3 | ||
| 2 | import json | ||
| 3 | from pathlib import Path | ||
| 4 | import subprocess | ||
| 5 | import sys | ||
| 6 | |||
| 7 | |||
| 8 | data = json.load(sys.stdin) | ||
| 9 | vm = Path(data["hostRoot"]) / "vm" | ||
| 10 | source = Path(__file__).parent | ||
| 11 | options = ["-i", str(vm / "ssh_key"), "-o", "BatchMode=yes", "-o", "IdentitiesOnly=yes", | ||
| 12 | "-o", "ForwardAgent=no", "-o", "StrictHostKeyChecking=accept-new", | ||
| 13 | "-o", "UserKnownHostsFile=" + str(vm / "known_hosts")] | ||
| 14 | for file, destination in [(vm / "openclaw.json", "/tmp/openclaw.json"), | ||
| 15 | (source / "Dockerfile.guest", "/tmp/Dockerfile.guest"), | ||
| 16 | (source / "openclaw.service", "/tmp/openclaw.service"), | ||
| 17 | (source / "install.sh", "/tmp/openclaw-install"), | ||
| 18 | (source / "openclaw-install.service", "/tmp/openclaw-install.service"), | ||
| 19 | (source / "resolv.conf", "/tmp/resolv.conf")]: | ||
| 20 | subprocess.run(["scp", "-P", "18892", *options, str(file), "ocadmin@127.0.0.1:" + destination], check=True) | ||
| 21 | subprocess.run(["ssh", "-p", "18892", *options, "ocadmin@127.0.0.1", "sudo bash -se"], check=True, input=""" | ||
| 22 | set -e | ||
| 23 | install -m 755 /tmp/openclaw-install /usr/local/sbin/openclaw-install | ||
| 24 | install /tmp/openclaw-install.service /etc/systemd/system/openclaw-install.service | ||
| 25 | install /tmp/resolv.conf /opt/openclaw/resolv.conf | ||
| 26 | install /tmp/Dockerfile.guest /opt/openclaw/Dockerfile | ||
| 27 | install -o 1000 -g 1000 -m 600 /tmp/openclaw.json /opt/openclaw/state/openclaw.json | ||
| 28 | install /tmp/openclaw.service /etc/systemd/system/openclaw.service | ||
| 29 | systemctl daemon-reload | ||
| 30 | systemctl enable openclaw-install | ||
| 31 | systemctl restart openclaw-install | ||
| 32 | rm /tmp/openclaw.json /tmp/Dockerfile.guest /tmp/openclaw.service /tmp/openclaw-install /tmp/openclaw-install.service /tmp/resolv.conf | ||
| 33 | """, text=True) | ||
service/openclaw/squid.conf created+17| ... | @@ -0,0 +1,17 @@ | ||
| 1 | http_port 127.0.0.1:18882 | ||
| 2 | acl local src 127.0.0.1/32 | ||
| 3 | acl private dst 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4 | ||
| 4 | acl private dst ::/128 ::1/128 ::ffff:0:0/96 64:ff9b::/96 64:ff9b:1::/48 100::/64 fc00::/7 fe80::/10 ff00::/8 | ||
| 5 | http_access deny private | ||
| 6 | http_access allow local | ||
| 7 | http_access deny all | ||
| 8 | cache deny all | ||
| 9 | cache_mem 8 MB | ||
| 10 | maximum_object_size 0 KB | ||
| 11 | access_log none | ||
| 12 | cache_log /dev/stderr | ||
| 13 | cache_store_log none | ||
| 14 | pid_filename /tmp/squid.pid | ||
| 15 | coredump_dir /tmp | ||
| 16 | netdb_filename none | ||
| 17 | shutdown_lifetime 1 seconds | ||
tools/openclaw-verify.py created+63| ... | @@ -0,0 +1,63 @@ | ||
| 1 | #!/usr/bin/env python3 | ||
| 2 | import argparse | ||
| 3 | import shlex | ||
| 4 | import subprocess | ||
| 5 | |||
| 6 | |||
| 7 | parser = argparse.ArgumentParser() | ||
| 8 | parser.add_argument("--host", default="root@zenith") | ||
| 9 | task = parser.add_mutually_exclusive_group() | ||
| 10 | task.add_argument("--prepare-task", action="store_true") | ||
| 11 | task.add_argument("--check-task", action="store_true") | ||
| 12 | args = parser.parse_args() | ||
| 13 | guest = ["ssh", "-i", "/srv/prod/openclaw/vm/ssh_key", "-p", "18892", | ||
| 14 | "-o", "BatchMode=yes", "-o", "IdentitiesOnly=yes", "-o", "ForwardAgent=no", | ||
| 15 | "-o", "StrictHostKeyChecking=yes", "-o", "UserKnownHostsFile=/srv/prod/openclaw/vm/known_hosts", | ||
| 16 | "ocadmin@127.0.0.1", "sudo docker exec -i openclaw python3 -"] | ||
| 17 | script = ''' | ||
| 18 | import json, os, pathlib, socket, subprocess, urllib.error, urllib.request | ||
| 19 | for name in ["NO_PROXY", "no_proxy"]: | ||
| 20 | os.environ.pop(name, None) | ||
| 21 | proxy = urllib.request.build_opener(urllib.request.ProxyHandler({"http": "http://10.0.2.100:3128", "https": "http://10.0.2.100:3128"})) | ||
| 22 | with proxy.open("https://example.com", timeout=20) as response: | ||
| 23 | assert response.status == 200 | ||
| 24 | for target in ["127.0.0.1", "10.0.0.1", "169.254.169.254", "192.168.1.1", "[::1]"]: | ||
| 25 | try: | ||
| 26 | proxy.open("http://" + target, timeout=5) | ||
| 27 | raise AssertionError("Private proxy target accepted: " + target) | ||
| 28 | except urllib.error.HTTPError as error: | ||
| 29 | assert error.code == 403, (target, error.code) | ||
| 30 | for target, port in [("10.0.2.2", 4646), ("10.0.0.1", 22), ("1.1.1.1", 443)]: | ||
| 31 | try: | ||
| 32 | connection = socket.create_connection((target, port), timeout=2) | ||
| 33 | except OSError: | ||
| 34 | continue | ||
| 35 | connection.close() | ||
| 36 | raise AssertionError("Direct egress accepted: " + target) | ||
| 37 | direct = urllib.request.build_opener(urllib.request.ProxyHandler({})) | ||
| 38 | with direct.open("http://10.0.2.101:8080/v1/models", timeout=20) as response: | ||
| 39 | assert json.load(response)["data"] | ||
| 40 | assert socket.getaddrinfo("example.com", 443) | ||
| 41 | assert not pathlib.Path("/var/run/docker.sock").exists() | ||
| 42 | assert not pathlib.Path("/srv/clover").exists() | ||
| 43 | print(json.dumps({"public_internet": True, "private_proxy_denied": 5, "direct_egress_denied": 3, "inference": True, "dns": True, "host_mounts": False}), flush=True) | ||
| 44 | ''' | ||
| 45 | if args.prepare_task or args.check_task: | ||
| 46 | script += r''' | ||
| 47 | directory = pathlib.Path("/home/node/.openclaw/workspace/openclaw-trial-check") | ||
| 48 | tests = "from median import median\nassert median([3,1,2]) == 2\nassert median([7,1,4,2]) == 3\na=[3,1,2]; median(a); assert a == [3,1,2]\nprint('MEDIAN_OK')\n" | ||
| 49 | ''' | ||
| 50 | if args.prepare_task: | ||
| 51 | script += r''' | ||
| 52 | directory.mkdir(exist_ok=True) | ||
| 53 | (directory / "median.py").write_text("def median(items):\n items.sort()\n return items[len(items) // 2]\n") | ||
| 54 | (directory / "check.py").write_text(tests) | ||
| 55 | print("Prepared coding fixture: " + str(directory)) | ||
| 56 | ''' | ||
| 57 | else: | ||
| 58 | script += """ | ||
| 59 | assert (directory / "check.py").read_text() == tests | ||
| 60 | subprocess.run(["python3", "check.py"], cwd=directory, check=True) | ||
| 61 | """ | ||
| 62 | subprocess.run(["ssh", "-o", "BatchMode=yes", args.host, shlex.join(guest)], | ||
| 63 | input=script, text=True, check=True) | ||