@clawops/cli 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/{apply-BTZFFGAD.js → apply-PZ5IIU7Z.js} +5 -4
- package/dist/{aws-D7Y6LCGK.js → aws-L4RR2LSE.js} +1 -1
- package/dist/{azure-JQAMNVHN.js → azure-7OXQENQL.js} +1 -1
- package/dist/{bootstrap-RY4VOSDJ.js → bootstrap-HL4DNUNG.js} +13 -4
- package/dist/bootstrap.sh.tmpl +204 -0
- package/dist/chunk-2YOP6N7X.js +126 -0
- package/dist/{chunk-GJEF6UQA.js → chunk-3XK66DKK.js} +100 -16
- package/dist/{chunk-ISFHLA4G.js → chunk-5SPESD5N.js} +8 -6
- package/dist/{chunk-YTH4L2GN.js → chunk-6X45DDGH.js} +5 -4
- package/dist/{chunk-XKT42M34.js → chunk-C5X375RO.js} +14 -1
- package/dist/{chunk-LCKD7L7X.js → chunk-DQDAWNGB.js} +1 -1
- package/dist/{chunk-CG3Y7Y2R.js → chunk-FSUWPL7Q.js} +9 -9
- package/dist/{chunk-ZFNPM2WG.js → chunk-O3F7OGH7.js} +40 -3
- package/dist/{chunk-3MFZ7E74.js → chunk-P5LKWRPL.js} +3 -3
- package/dist/chunk-PVUVI35Y.js +29 -0
- package/dist/{chunk-7KLLWDBA.js → chunk-SEABO2UU.js} +7 -4
- package/dist/cli.js +122 -68
- package/dist/{context-ALSJMTHE.js → context-5LCUG2QR.js} +1 -1
- package/dist/{gcp-6FT2A45S.js → gcp-UG6AWPIZ.js} +1 -1
- package/dist/{generate-ZTD4BVVI.js → generate-NIF6IBZC.js} +4 -3
- package/dist/{harden-BDVYW567.js → harden-URPXLSVF.js} +2 -2
- package/dist/{mcp-wire-ZWIYMLEW.js → mcp-wire-SLL4TLSC.js} +1 -1
- package/dist/{package-7WOZMHSO.js → package-TZ2HYF5C.js} +3 -2
- package/dist/{pool-JZGKPP6K.js → pool-QGFQRMGU.js} +2 -2
- package/dist/{remote-config-QF5TT7GU.js → remote-config-PFB66RPY.js} +3 -1
- package/dist/{server-EXBOOIEP.js → server-WM3AY4PJ.js} +19 -18
- package/dist/{ssh-E2AMBPZY.js → ssh-CNJNLM3E.js} +1 -1
- package/dist/{validate-T5M5EHSJ.js → validate-CFQ4FH2T.js} +2 -1
- package/dist/version-guard-MZAO4KHS.js +25 -0
- package/dist/versions-OEPAS2EL.js +24 -0
- package/package.json +3 -2
- package/spec/_gen-schemas.md +136 -0
- package/spec/deploy-plan.schema.json +193 -0
- package/spec/errors.yaml +175 -0
- package/spec/integrations.yaml +97 -0
- package/spec/invariants.yaml +81 -0
- package/spec/mcp-tools.yaml +532 -0
- package/spec/models.yaml +113 -0
- package/spec/openclaw-versions.yaml +67 -0
- package/spec/providers.schema.json +120 -0
package/README.md
CHANGED
|
@@ -12,6 +12,68 @@ and Cursor drive them through typed MCP tools with explicit safety controls.
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## What's new in v1.7.0
|
|
16
|
+
|
|
17
|
+
**`clawops harden`** — run an idempotent set of server-hardening modules against a deployed
|
|
18
|
+
stack over SSH. Each module has a read-only `check()` and an `apply()` step; `check()` runs first
|
|
19
|
+
and `apply()` is skipped when the box is already compliant. Sentinel files under
|
|
20
|
+
`/etc/clawops/hardening/` record what has been applied.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
clawops harden --list # show available modules
|
|
24
|
+
clawops harden --stack prod --dry-run # preview changes, apply nothing
|
|
25
|
+
clawops harden --stack prod # apply the default module set
|
|
26
|
+
clawops harden --stack prod --options ssh,ufw # apply a specific subset
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Common modules (all providers), ON by default:** `ssh` (hardens `sshd_config`, guards against
|
|
30
|
+
lockout by verifying `authorized_keys` is non-empty before restarting sshd), `ufw` (deny-all
|
|
31
|
+
incoming, allow SSH + gateway 18789), `fail2ban` (SSH jail: 5 failures → 10-minute ban),
|
|
32
|
+
`unattended-upgrades` (security-only auto updates), and `docker-socket` (verify
|
|
33
|
+
`/var/run/docker.sock` is `root:docker 660`). **Opt-in:** `auditd`, `lynis` (CIS Level 1 scan →
|
|
34
|
+
`~/.clawops/reports/`), `sysctl`.
|
|
35
|
+
|
|
36
|
+
**AWS modules (check-only, ON by default):** `aws-sg-audit` (warns on `0.0.0.0/0` ingress on
|
|
37
|
+
unexpected ports) and `aws-ssm-check` (verifies `AmazonSSMManagedInstanceCore` for emergency SSM
|
|
38
|
+
shell access). **Opt-in:** `aws-flow-logs`, `aws-guardduty`.
|
|
39
|
+
|
|
40
|
+
`clawops setup` now presents a multi-select hardening step after deploy (skippable with
|
|
41
|
+
`--no-harden`), and `clawops doctor --stack <name>` gains a Hardening section showing which
|
|
42
|
+
modules are applied, missing, or drifted.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## What's new in v1.6.0
|
|
47
|
+
|
|
48
|
+
**`clawops bug`** — open a pre-filled GitHub issue with system context (version, Node, OS,
|
|
49
|
+
provider, stack count, SSH key presence) populated automatically. `--json` emits the URL without
|
|
50
|
+
prompting or opening a browser, suitable for scripting. `clawops doctor` now prints a `clawops bug`
|
|
51
|
+
hint in its footer when it exits with an error.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
clawops bug # open a pre-filled issue in the browser
|
|
55
|
+
clawops bug --json # print the issue URL for scripting
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Cloud deploy bug fixes (AWS, GCP, Azure).** This release fixes 10 bugs found in a deploy audit,
|
|
59
|
+
several deploy-blocking:
|
|
60
|
+
|
|
61
|
+
- **Azure:** updated deprecated image reference `UbuntuServer/22.04-LTS` →
|
|
62
|
+
`0001-com-ubuntu-server-jammy/22_04-lts-gen2` — every Azure deployment was broken; and fixed a
|
|
63
|
+
malformed Key Vault `roleDefinitionId` that broke Key Vault RBAC entirely.
|
|
64
|
+
- **AWS:** Bedrock startup script now uses the IMDSv2 PUT→GET token flow (a plain `curl` to IMDS
|
|
65
|
+
returned 401, so the region always fell back to `us-east-1`); replaced
|
|
66
|
+
`AmazonBedrockFullAccess` with a least-privilege inline policy.
|
|
67
|
+
- **AWS (⚠️ migration impact):** migrated from inline Security Group ingress/egress arrays to
|
|
68
|
+
individual `SecurityGroupIngressRule`/`SecurityGroupEgressRule` resources so CIDR changes no
|
|
69
|
+
longer replace the whole Security Group. **Existing AWS stacks have their Security Group
|
|
70
|
+
replaced on the first `clawops up` after this upgrade** — see
|
|
71
|
+
`docs/decisions/0009-aws-sg-rule-resources.md` for the import-based mitigation.
|
|
72
|
+
- **All providers:** `accessMode=auto` egress-IP detection now returns a `Result` and throws a
|
|
73
|
+
clear error on failure instead of silently producing a VM with zero ingress rules.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
15
77
|
## What's new in v1.5.0
|
|
16
78
|
|
|
17
79
|
**`clawops mcp wire`** — wire the OpenClaw gateway's own AI as an MCP client of clawops.
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
applyPlan
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-YTH4L2GN.js";
|
|
4
|
+
} from "./chunk-5SPESD5N.js";
|
|
6
5
|
import "./chunk-6ZFIFDBJ.js";
|
|
7
6
|
import "./chunk-BOPSG2LI.js";
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-6X45DDGH.js";
|
|
8
|
+
import "./chunk-O3F7OGH7.js";
|
|
9
|
+
import "./chunk-P5LKWRPL.js";
|
|
10
|
+
import "./chunk-PVUVI35Y.js";
|
|
10
11
|
import "./chunk-A2I76FTA.js";
|
|
11
12
|
import "./chunk-CX5SL5HP.js";
|
|
12
13
|
import "./chunk-KGXPLI7W.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
acquireSession
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-DQDAWNGB.js";
|
|
5
|
+
import "./chunk-3XK66DKK.js";
|
|
6
6
|
import {
|
|
7
7
|
writeLocalState
|
|
8
8
|
} from "./chunk-A2I76FTA.js";
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "./chunk-KGXPLI7W.js";
|
|
13
13
|
|
|
14
14
|
// src/providers/local/bootstrap.ts
|
|
15
|
-
import { readFileSync } from "fs";
|
|
15
|
+
import { readFileSync, existsSync } from "fs";
|
|
16
16
|
import path from "path";
|
|
17
17
|
import { fileURLToPath } from "url";
|
|
18
18
|
var GATEWAY_PORT = 18789;
|
|
@@ -20,7 +20,16 @@ var HEALTH_POLL_INTERVAL_MS = 3e3;
|
|
|
20
20
|
var HEALTH_TIMEOUT_MS = 12e4;
|
|
21
21
|
function loadTemplate() {
|
|
22
22
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
|
|
23
|
+
const candidates = [
|
|
24
|
+
path.join(dir, "bootstrap.sh.tmpl"),
|
|
25
|
+
path.join(dir, "../src/providers/local/bootstrap.sh.tmpl")
|
|
26
|
+
];
|
|
27
|
+
for (const candidate of candidates) {
|
|
28
|
+
if (existsSync(candidate)) return readFileSync(candidate, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Cannot find bootstrap.sh.tmpl (looked in: ${candidates.join(", ")}). If this is an installed package, the template may be missing from the build output.`
|
|
32
|
+
);
|
|
24
33
|
}
|
|
25
34
|
function renderScript(openclawVersion) {
|
|
26
35
|
return loadTemplate().replace(/\{\{OPENCLAW_VERSION\}\}/g, openclawVersion);
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# clawops bootstrap script — idempotent; safe to re-run.
|
|
3
|
+
# Rendered by clawops and executed via SSH on the target host.
|
|
4
|
+
# Supports: Ubuntu/Debian, RHEL/Fedora/CentOS (dnf/yum), Alpine (apk), macOS.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
OPENCLAW_VERSION="{{OPENCLAW_VERSION}}"
|
|
8
|
+
OPENCLAW_PORT=18789
|
|
9
|
+
|
|
10
|
+
# ── OS detection ──────────────────────────────────────────────────────────────
|
|
11
|
+
OS="$(uname -s)"
|
|
12
|
+
|
|
13
|
+
case "$OS" in
|
|
14
|
+
Linux)
|
|
15
|
+
if command -v apt-get &>/dev/null; then
|
|
16
|
+
PKG_MANAGER=apt
|
|
17
|
+
elif command -v dnf &>/dev/null; then
|
|
18
|
+
PKG_MANAGER=dnf
|
|
19
|
+
elif command -v yum &>/dev/null; then
|
|
20
|
+
PKG_MANAGER=yum
|
|
21
|
+
elif command -v apk &>/dev/null; then
|
|
22
|
+
PKG_MANAGER=apk
|
|
23
|
+
else
|
|
24
|
+
echo "clawops: unsupported Linux distribution (no apt/dnf/yum/apk found)" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
OPENCLAW_USER=clawops
|
|
28
|
+
OPENCLAW_CONFIG=/home/${OPENCLAW_USER}/openclaw.json
|
|
29
|
+
;;
|
|
30
|
+
Darwin)
|
|
31
|
+
PKG_MANAGER=darwin
|
|
32
|
+
# When invoked via sudo the script runs as root but SUDO_USER is the real caller.
|
|
33
|
+
# Fall back to id -un for passwordless (non-sudo) invocations.
|
|
34
|
+
OPENCLAW_USER="${SUDO_USER:-$(id -un)}"
|
|
35
|
+
OPENCLAW_CONFIG="${HOME}/.config/openclaw/config.json"
|
|
36
|
+
mkdir -p "${HOME}/.config/openclaw"
|
|
37
|
+
# Fix ownership so subsequent non-sudo SSH writes (atomicWriteConfig) can overwrite.
|
|
38
|
+
chown "${OPENCLAW_USER}" "${HOME}/.config/openclaw" 2>/dev/null || true
|
|
39
|
+
# sudo resets PATH; add common Docker install locations so `command -v docker` works.
|
|
40
|
+
# Covers: Docker Desktop (Intel + AS), Homebrew (AS), Colima.
|
|
41
|
+
export PATH="/usr/local/bin:/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH"
|
|
42
|
+
;;
|
|
43
|
+
*)
|
|
44
|
+
echo "clawops: unsupported OS: $OS" >&2
|
|
45
|
+
exit 1
|
|
46
|
+
;;
|
|
47
|
+
esac
|
|
48
|
+
|
|
49
|
+
# ── Create clawops user (Linux only) ─────────────────────────────────────────
|
|
50
|
+
if [ "$OS" = "Linux" ]; then
|
|
51
|
+
id -u "${OPENCLAW_USER}" &>/dev/null || useradd -m -s /bin/bash "${OPENCLAW_USER}"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# ── Install Docker ─────────────────────────────────────────────────────────────
|
|
55
|
+
if ! command -v docker &>/dev/null; then
|
|
56
|
+
case "$PKG_MANAGER" in
|
|
57
|
+
darwin)
|
|
58
|
+
echo "clawops: Docker not found on macOS." >&2
|
|
59
|
+
echo "Install one of:" >&2
|
|
60
|
+
echo " Docker Desktop: https://www.docker.com/products/docker-desktop/" >&2
|
|
61
|
+
echo " Homebrew: brew install --cask docker" >&2
|
|
62
|
+
echo " Colima (FOSS): brew install colima docker && colima start" >&2
|
|
63
|
+
exit 1
|
|
64
|
+
;;
|
|
65
|
+
apt)
|
|
66
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
67
|
+
apt-get update -q
|
|
68
|
+
apt-get install -y -q ca-certificates curl gnupg lsb-release
|
|
69
|
+
install -m 0755 -d /etc/apt/keyrings
|
|
70
|
+
# Detect Ubuntu vs Debian; use the correct Docker APT repo for each.
|
|
71
|
+
# /etc/os-release ID is 'ubuntu' on Ubuntu and 'debian' on Debian.
|
|
72
|
+
# Other debian-based distros (Raspbian, Mint, etc.) fall back to the debian repo.
|
|
73
|
+
. /etc/os-release
|
|
74
|
+
case "${ID:-ubuntu}" in
|
|
75
|
+
ubuntu) DOCKER_DISTRO=ubuntu ;;
|
|
76
|
+
*) DOCKER_DISTRO=debian ;;
|
|
77
|
+
esac
|
|
78
|
+
curl -fsSL "https://download.docker.com/linux/${DOCKER_DISTRO}/gpg" \
|
|
79
|
+
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
|
80
|
+
chmod a+r /etc/apt/keyrings/docker.gpg
|
|
81
|
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
|
82
|
+
https://download.docker.com/linux/${DOCKER_DISTRO} $(lsb_release -cs) stable" \
|
|
83
|
+
> /etc/apt/sources.list.d/docker.list
|
|
84
|
+
apt-get update -q
|
|
85
|
+
apt-get install -y -q docker-ce docker-ce-cli containerd.io
|
|
86
|
+
systemctl enable --now docker
|
|
87
|
+
;;
|
|
88
|
+
dnf|yum)
|
|
89
|
+
$PKG_MANAGER install -y dnf-plugins-core 2>/dev/null || $PKG_MANAGER install -y yum-utils
|
|
90
|
+
$PKG_MANAGER config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
|
|
91
|
+
$PKG_MANAGER install -y docker-ce docker-ce-cli containerd.io
|
|
92
|
+
systemctl enable --now docker
|
|
93
|
+
;;
|
|
94
|
+
apk)
|
|
95
|
+
apk add --no-cache docker
|
|
96
|
+
rc-update add docker boot
|
|
97
|
+
service docker start
|
|
98
|
+
;;
|
|
99
|
+
esac
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# ── Verify Docker daemon is reachable ─────────────────────────────────────────
|
|
103
|
+
# Try docker version (faster than docker info) across all known socket paths.
|
|
104
|
+
docker_reachable() {
|
|
105
|
+
docker version >/dev/null 2>&1 && return 0
|
|
106
|
+
if [ "$OS" = "Darwin" ]; then
|
|
107
|
+
DOCKER_HOST="unix://${HOME}/.docker/run/docker.sock" docker version >/dev/null 2>&1 && return 0
|
|
108
|
+
DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" docker version >/dev/null 2>&1 && return 0
|
|
109
|
+
fi
|
|
110
|
+
return 1
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if ! docker_reachable; then
|
|
114
|
+
echo "clawops: Docker is installed but the daemon is not running." >&2
|
|
115
|
+
if [ "$PKG_MANAGER" = "darwin" ]; then
|
|
116
|
+
echo "Start Docker Desktop or run: colima start" >&2
|
|
117
|
+
else
|
|
118
|
+
echo "Try: systemctl start docker" >&2
|
|
119
|
+
fi
|
|
120
|
+
exit 1
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# ── Add clawops user to docker group (Linux only) ────────────────────────────
|
|
124
|
+
if [ "$OS" = "Linux" ]; then
|
|
125
|
+
usermod -aG docker "${OPENCLAW_USER}" || true
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# ── Pull OpenClaw image ────────────────────────────────────────────────────────
|
|
129
|
+
docker pull "ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}"
|
|
130
|
+
|
|
131
|
+
# ── Gateway auth token ────────────────────────────────────────────────────────
|
|
132
|
+
# OpenClaw refuses to bind a non-loopback listener without auth, and in a container
|
|
133
|
+
# it always binds 0.0.0.0 — so without a token the gateway exits 78 and systemd
|
|
134
|
+
# restart-loops. Generated once and reused on re-runs so restarts don't rotate it.
|
|
135
|
+
OPENCLAW_ENV_FILE=/home/clawops/openclaw.env
|
|
136
|
+
if [ ! -s "${OPENCLAW_ENV_FILE}" ]; then
|
|
137
|
+
OPENCLAW_TOKEN=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
|
138
|
+
printf 'OPENCLAW_GATEWAY_TOKEN=%s\n' "${OPENCLAW_TOKEN}" > "${OPENCLAW_ENV_FILE}"
|
|
139
|
+
chmod 600 "${OPENCLAW_ENV_FILE}"
|
|
140
|
+
chown "${OPENCLAW_USER}" "${OPENCLAW_ENV_FILE}" 2>/dev/null || true
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# ── Write default openclaw.json if absent ─────────────────────────────────────
|
|
144
|
+
# clawops up --config will overwrite this with the plan config overlay.
|
|
145
|
+
if [ ! -f "${OPENCLAW_CONFIG}" ]; then
|
|
146
|
+
cat > "${OPENCLAW_CONFIG}" <<'OPENCLAWJSON'
|
|
147
|
+
{"meta":{"lastTouchedVersion":"2026.4"},"gateway":{"port":18789,"auth":{"mode":"token"}},"models":{},"channels":{}}
|
|
148
|
+
OPENCLAWJSON
|
|
149
|
+
if [ "$OS" = "Linux" ]; then
|
|
150
|
+
chown "${OPENCLAW_USER}:${OPENCLAW_USER}" "${OPENCLAW_CONFIG}"
|
|
151
|
+
else
|
|
152
|
+
chown "${OPENCLAW_USER}" "${OPENCLAW_CONFIG}" 2>/dev/null || true
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# ── Start service ─────────────────────────────────────────────────────────────
|
|
157
|
+
if [ "$OS" = "Linux" ]; then
|
|
158
|
+
SERVICE_FILE=/etc/systemd/system/openclaw.service
|
|
159
|
+
cat > "${SERVICE_FILE}" <<UNIT
|
|
160
|
+
[Unit]
|
|
161
|
+
Description=OpenClaw gateway
|
|
162
|
+
After=docker.service
|
|
163
|
+
Requires=docker.service
|
|
164
|
+
|
|
165
|
+
[Service]
|
|
166
|
+
User=${OPENCLAW_USER}
|
|
167
|
+
Restart=always
|
|
168
|
+
RestartSec=5
|
|
169
|
+
ExecStartPre=-/usr/bin/docker stop openclaw
|
|
170
|
+
ExecStartPre=-/usr/bin/docker rm openclaw
|
|
171
|
+
ExecStart=/usr/bin/docker run --name openclaw --rm \
|
|
172
|
+
-p ${OPENCLAW_PORT}:${OPENCLAW_PORT} \
|
|
173
|
+
-e OPENCLAW_CONFIG_PATH=/app/config.json --add-host=host.docker.internal:host-gateway \
|
|
174
|
+
--env-file ${OPENCLAW_ENV_FILE} \
|
|
175
|
+
-v ${OPENCLAW_CONFIG}:/app/config.json:ro \
|
|
176
|
+
ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION} \
|
|
177
|
+
node openclaw.mjs gateway run --allow-unconfigured --port ${OPENCLAW_PORT}
|
|
178
|
+
ExecStop=/usr/bin/docker stop openclaw
|
|
179
|
+
|
|
180
|
+
[Install]
|
|
181
|
+
WantedBy=multi-user.target
|
|
182
|
+
UNIT
|
|
183
|
+
systemctl daemon-reload
|
|
184
|
+
systemctl enable openclaw
|
|
185
|
+
systemctl restart openclaw
|
|
186
|
+
|
|
187
|
+
elif [ "$OS" = "Darwin" ]; then
|
|
188
|
+
# macOS: no systemd; run as foreground-detached container.
|
|
189
|
+
# LaunchAgent support is a future enhancement.
|
|
190
|
+
docker stop openclaw 2>/dev/null || true
|
|
191
|
+
docker rm openclaw 2>/dev/null || true
|
|
192
|
+
docker run -d \
|
|
193
|
+
--name openclaw \
|
|
194
|
+
--restart unless-stopped \
|
|
195
|
+
-p "${OPENCLAW_PORT}:${OPENCLAW_PORT}" \
|
|
196
|
+
-e OPENCLAW_CONFIG_PATH=/app/config.json --add-host=host.docker.internal:host-gateway \
|
|
197
|
+
--env-file "${OPENCLAW_ENV_FILE}" \
|
|
198
|
+
--env-file ${OPENCLAW_ENV_FILE} \
|
|
199
|
+
-v "${OPENCLAW_CONFIG}":/app/config.json:ro \
|
|
200
|
+
"ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}" \
|
|
201
|
+
node openclaw.mjs gateway run --allow-unconfigured --port ${OPENCLAW_PORT}
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
echo "clawops: bootstrap complete"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolveSpecDir
|
|
4
|
+
} from "./chunk-PVUVI35Y.js";
|
|
5
|
+
|
|
6
|
+
// src/openclaw/versions.ts
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
// src/types/result.ts
|
|
11
|
+
function ok(value) {
|
|
12
|
+
return { ok: true, value };
|
|
13
|
+
}
|
|
14
|
+
function err(error) {
|
|
15
|
+
return { ok: false, error };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/openclaw/versions.ts
|
|
19
|
+
var MOVING_TAGS = ["latest", "stable", "dev", "main"];
|
|
20
|
+
function isMovingTag(v) {
|
|
21
|
+
return MOVING_TAGS.includes(v);
|
|
22
|
+
}
|
|
23
|
+
var _spec;
|
|
24
|
+
function loadVersionSpec(yaml) {
|
|
25
|
+
if (_spec) return _spec;
|
|
26
|
+
const specPath = join(resolveSpecDir(), "openclaw-versions.yaml");
|
|
27
|
+
try {
|
|
28
|
+
const raw = yaml.load(readFileSync(specPath, "utf-8"));
|
|
29
|
+
if (!raw?.support?.min) {
|
|
30
|
+
throw new Error("missing support.min");
|
|
31
|
+
}
|
|
32
|
+
_spec = {
|
|
33
|
+
support: {
|
|
34
|
+
min: raw.support.min,
|
|
35
|
+
max: raw.support.max ?? "",
|
|
36
|
+
recommended: raw.support.recommended ?? raw.support.min
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return _spec;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Cannot load OpenClaw version support matrix from ${specPath}: ${e.message}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function _resetVersionSpecCache() {
|
|
47
|
+
_spec = void 0;
|
|
48
|
+
}
|
|
49
|
+
function compareVersions(a, b) {
|
|
50
|
+
const parse = (v) => v.split(/[.-]/).map((seg) => Number.parseInt(seg, 10)).map((n) => Number.isNaN(n) ? 0 : n);
|
|
51
|
+
const pa = parse(a);
|
|
52
|
+
const pb = parse(b);
|
|
53
|
+
const len = Math.max(pa.length, pb.length);
|
|
54
|
+
for (let i = 0; i < len; i++) {
|
|
55
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
56
|
+
if (diff !== 0) return diff;
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
function checkVersion(version, support) {
|
|
61
|
+
if (isMovingTag(version)) {
|
|
62
|
+
return err({
|
|
63
|
+
requested: version,
|
|
64
|
+
resolved: version,
|
|
65
|
+
reason: "too-new",
|
|
66
|
+
supportedRange: describeRange(support),
|
|
67
|
+
message: `Cannot verify OpenClaw compatibility: "${version}" is a moving tag that was not resolved to a concrete release. Pin an explicit version (${support.recommended}) or re-run once tag resolution is available.`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (compareVersions(version, support.min) < 0) {
|
|
71
|
+
return err({
|
|
72
|
+
requested: version,
|
|
73
|
+
resolved: version,
|
|
74
|
+
reason: "too-old",
|
|
75
|
+
supportedRange: describeRange(support),
|
|
76
|
+
message: `OpenClaw ${version} is older than this clawops release supports (${describeRange(support)}). Upgrade OpenClaw, or pin ${support.recommended}.`
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (support.max && compareVersions(version, support.max) > 0) {
|
|
80
|
+
return err({
|
|
81
|
+
requested: version,
|
|
82
|
+
resolved: version,
|
|
83
|
+
reason: "too-new",
|
|
84
|
+
supportedRange: describeRange(support),
|
|
85
|
+
message: `OpenClaw ${version} is newer than this clawops release supports (${describeRange(support)}).
|
|
86
|
+
OpenClaw 2026.8.1+ changed the container runtime contract: state moved to SQLite, config moved to a writable path, and provider plugins became install-gated.
|
|
87
|
+
Use clawops 2.x for OpenClaw 2026.9.1 and later: npm install -g @clawops/cli@latest
|
|
88
|
+
To stay on this line, pin an OpenClaw version at or below ${support.max}.`
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return ok(version);
|
|
92
|
+
}
|
|
93
|
+
function describeRange(support) {
|
|
94
|
+
if (!support.max) return `>= ${support.min}`;
|
|
95
|
+
return `${support.min} \u2013 ${support.max}`;
|
|
96
|
+
}
|
|
97
|
+
async function resolveVersion(version, resolver) {
|
|
98
|
+
if (!isMovingTag(version)) return version;
|
|
99
|
+
if (!resolver) return version;
|
|
100
|
+
try {
|
|
101
|
+
return await resolver(version) ?? version;
|
|
102
|
+
} catch {
|
|
103
|
+
return version;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function assertSupportedVersion(version, yaml, resolver) {
|
|
107
|
+
const spec = loadVersionSpec(yaml);
|
|
108
|
+
const resolved = await resolveVersion(version, resolver);
|
|
109
|
+
const result = checkVersion(resolved, spec.support);
|
|
110
|
+
if (!result.ok && resolved !== version) {
|
|
111
|
+
return err({ ...result.error, requested: version });
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export {
|
|
117
|
+
MOVING_TAGS,
|
|
118
|
+
isMovingTag,
|
|
119
|
+
loadVersionSpec,
|
|
120
|
+
_resetVersionSpecCache,
|
|
121
|
+
compareVersions,
|
|
122
|
+
checkVersion,
|
|
123
|
+
describeRange,
|
|
124
|
+
resolveVersion,
|
|
125
|
+
assertSupportedVersion
|
|
126
|
+
};
|
|
@@ -8,6 +8,98 @@ import { readFileSync, appendFileSync, mkdirSync } from "fs";
|
|
|
8
8
|
import { createServer } from "net";
|
|
9
9
|
import path from "path";
|
|
10
10
|
import { Client } from "ssh2";
|
|
11
|
+
|
|
12
|
+
// src/transport/known-hosts.ts
|
|
13
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
14
|
+
function hostEntryFor(host, port) {
|
|
15
|
+
return port === 22 ? host : `[${host}]:${port}`;
|
|
16
|
+
}
|
|
17
|
+
function parseKnownHosts(content) {
|
|
18
|
+
const entries = [];
|
|
19
|
+
for (const raw of content.split("\n")) {
|
|
20
|
+
const line = raw.trim();
|
|
21
|
+
if (!line || line.startsWith("#")) continue;
|
|
22
|
+
let parts = line.split(/\s+/);
|
|
23
|
+
let marker;
|
|
24
|
+
if (parts[0]?.startsWith("@")) {
|
|
25
|
+
marker = parts[0];
|
|
26
|
+
parts = parts.slice(1);
|
|
27
|
+
}
|
|
28
|
+
const hostField = parts[0];
|
|
29
|
+
if (!hostField) continue;
|
|
30
|
+
const hosts = hostField.split(",");
|
|
31
|
+
if (parts.length >= 3) {
|
|
32
|
+
entries.push({ hosts, keyType: parts[1], base64Key: parts[2], ...marker ? { marker } : {} });
|
|
33
|
+
} else if (parts.length === 2 && /^[0-9a-f]+$/i.test(parts[1] ?? "")) {
|
|
34
|
+
entries.push({ hosts, hexKey: parts[1], ...marker ? { marker } : {} });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return entries;
|
|
38
|
+
}
|
|
39
|
+
function hashedHostMatches(token, hostEntry) {
|
|
40
|
+
const parts = token.split("|");
|
|
41
|
+
if (parts.length !== 4 || parts[1] !== "1") return false;
|
|
42
|
+
const salt = parts[2];
|
|
43
|
+
const expected = parts[3];
|
|
44
|
+
if (!salt || !expected) return false;
|
|
45
|
+
try {
|
|
46
|
+
const actual = createHmac("sha1", Buffer.from(salt, "base64")).update(hostEntry).digest("base64");
|
|
47
|
+
const a = Buffer.from(actual);
|
|
48
|
+
const b = Buffer.from(expected);
|
|
49
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function patternToRegExp(pattern) {
|
|
55
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
56
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
57
|
+
}
|
|
58
|
+
var isPattern = (host) => host.includes("*") || host.includes("?");
|
|
59
|
+
function entryMatchesHost(entry, hostEntry) {
|
|
60
|
+
let matched = false;
|
|
61
|
+
for (const host of entry.hosts) {
|
|
62
|
+
if (host.startsWith("|1|")) {
|
|
63
|
+
if (hashedHostMatches(host, hostEntry)) matched = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const negated = host.startsWith("!");
|
|
67
|
+
const pattern = negated ? host.slice(1) : host;
|
|
68
|
+
const hit = isPattern(pattern) ? patternToRegExp(pattern).test(hostEntry) : pattern === hostEntry;
|
|
69
|
+
if (hit) {
|
|
70
|
+
if (negated) return false;
|
|
71
|
+
matched = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return matched;
|
|
75
|
+
}
|
|
76
|
+
function verifyAgainstKnownHosts(content, host, port, key) {
|
|
77
|
+
const hostEntry = hostEntryFor(host, port);
|
|
78
|
+
const base64Key = key.toString("base64");
|
|
79
|
+
const hexKey = key.toString("hex");
|
|
80
|
+
let sawHost = false;
|
|
81
|
+
for (const entry of parseKnownHosts(content)) {
|
|
82
|
+
if (!entryMatchesHost(entry, hostEntry)) continue;
|
|
83
|
+
if (entry.marker === "@cert-authority") continue;
|
|
84
|
+
sawHost = true;
|
|
85
|
+
const matches = entry.base64Key ? entry.base64Key === base64Key : entry.hexKey?.toLowerCase() === hexKey.toLowerCase();
|
|
86
|
+
if (matches) return entry.marker === "@revoked" ? "mismatch" : "match";
|
|
87
|
+
}
|
|
88
|
+
return sawHost ? "mismatch" : "unknown";
|
|
89
|
+
}
|
|
90
|
+
function formatKnownHostsLine(host, port, keyType, key) {
|
|
91
|
+
return `${hostEntryFor(host, port)} ${keyType} ${key.toString("base64")}
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
function keyTypeFromBlob(key) {
|
|
95
|
+
if (key.length < 4) return void 0;
|
|
96
|
+
const len = key.readUInt32BE(0);
|
|
97
|
+
if (len === 0 || len > 64 || key.length < 4 + len) return void 0;
|
|
98
|
+
const type = key.subarray(4, 4 + len).toString("utf-8");
|
|
99
|
+
return /^[\x20-\x7e]+$/.test(type) ? type : void 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/transport/ssh.ts
|
|
11
103
|
var Ssh2Session = class {
|
|
12
104
|
constructor(client) {
|
|
13
105
|
this.client = client;
|
|
@@ -147,27 +239,19 @@ async function connect(opts) {
|
|
|
147
239
|
});
|
|
148
240
|
}
|
|
149
241
|
function verifyHostKey(host, port, keyHash, knownHostsPath) {
|
|
150
|
-
|
|
151
|
-
const hostEntry = port === 22 ? host : `[${host}]:${port}`;
|
|
152
|
-
let existing = null;
|
|
242
|
+
let content = "";
|
|
153
243
|
try {
|
|
154
|
-
|
|
155
|
-
for (const line of content.split("\n")) {
|
|
156
|
-
const parts = line.trim().split(/\s+/);
|
|
157
|
-
if (parts[0] === hostEntry && parts.length >= 2) {
|
|
158
|
-
existing = parts[1] ?? null;
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
244
|
+
content = readFileSync(knownHostsPath, "utf-8");
|
|
162
245
|
} catch {
|
|
163
246
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
247
|
+
const verdict = verifyAgainstKnownHosts(content, host, port, keyHash);
|
|
248
|
+
if (verdict === "match") return true;
|
|
249
|
+
if (verdict === "mismatch") return false;
|
|
250
|
+
const keyType = keyTypeFromBlob(keyHash);
|
|
251
|
+
if (!keyType) return false;
|
|
167
252
|
try {
|
|
168
253
|
mkdirSync(path.dirname(knownHostsPath), { recursive: true });
|
|
169
|
-
appendFileSync(knownHostsPath,
|
|
170
|
-
`, "utf-8");
|
|
254
|
+
appendFileSync(knownHostsPath, formatKnownHostsLine(host, port, keyType, keyHash), "utf-8");
|
|
171
255
|
} catch {
|
|
172
256
|
}
|
|
173
257
|
return true;
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
validatePlan
|
|
4
|
-
} from "./chunk-YTH4L2GN.js";
|
|
5
2
|
import {
|
|
6
3
|
saveOverlay
|
|
7
4
|
} from "./chunk-6ZFIFDBJ.js";
|
|
8
5
|
import {
|
|
9
6
|
resolveSecrets
|
|
10
7
|
} from "./chunk-BOPSG2LI.js";
|
|
8
|
+
import {
|
|
9
|
+
validatePlan
|
|
10
|
+
} from "./chunk-6X45DDGH.js";
|
|
11
11
|
import {
|
|
12
12
|
atomicWriteConfig,
|
|
13
13
|
deepMerge,
|
|
14
14
|
readRemoteConfig,
|
|
15
15
|
restartGateway
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-O3F7OGH7.js";
|
|
17
17
|
import {
|
|
18
18
|
buildContext
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-P5LKWRPL.js";
|
|
20
20
|
import {
|
|
21
21
|
UsageError
|
|
22
22
|
} from "./chunk-KGXPLI7W.js";
|
|
@@ -35,6 +35,8 @@ ${validation.errors.join("\n")}`
|
|
|
35
35
|
"plan/apply is not supported for the local provider. Use `clawops up` directly."
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
|
+
const { guardOpenclawVersion } = await import("./version-guard-MZAO4KHS.js");
|
|
39
|
+
await guardOpenclawVersion(plan.spec.openclaw.version);
|
|
38
40
|
const ctx = buildContext({
|
|
39
41
|
stack: plan.spec.stackName,
|
|
40
42
|
provider: plan.spec.provider
|
|
@@ -86,7 +88,7 @@ The diff you reviewed may no longer reflect what will be applied.
|
|
|
86
88
|
};
|
|
87
89
|
}
|
|
88
90
|
async function applyConfigOverlay(plan, outputs, ctx, signal) {
|
|
89
|
-
const { connect } = await import("./ssh-
|
|
91
|
+
const { connect } = await import("./ssh-CNJNLM3E.js");
|
|
90
92
|
const connInfo = ctx.adapter.getConnectionInfo(outputs);
|
|
91
93
|
const session = await connect({
|
|
92
94
|
host: connInfo.host,
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolveSpecDir
|
|
4
|
+
} from "./chunk-PVUVI35Y.js";
|
|
2
5
|
|
|
3
6
|
// src/plan/validate.ts
|
|
4
7
|
import Ajv from "ajv/dist/2020";
|
|
5
8
|
import addFormats from "ajv-formats";
|
|
6
9
|
import { readFileSync } from "fs";
|
|
7
|
-
import {
|
|
8
|
-
import { join, dirname } from "path";
|
|
9
|
-
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
import { join } from "path";
|
|
10
11
|
var _validate;
|
|
11
12
|
function getValidator() {
|
|
12
13
|
if (_validate) return _validate;
|
|
13
14
|
const ajv = new Ajv({ strict: false });
|
|
14
15
|
addFormats(ajv);
|
|
15
16
|
const schema = JSON.parse(
|
|
16
|
-
readFileSync(join(
|
|
17
|
+
readFileSync(join(resolveSpecDir(), "deploy-plan.schema.json"), "utf-8")
|
|
17
18
|
);
|
|
18
19
|
_validate = ajv.compile(schema);
|
|
19
20
|
return _validate;
|