@clawops/cli 1.7.1 → 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.
Files changed (40) hide show
  1. package/dist/{apply-BTZFFGAD.js → apply-PZ5IIU7Z.js} +5 -4
  2. package/dist/{aws-D7Y6LCGK.js → aws-L4RR2LSE.js} +1 -1
  3. package/dist/{azure-JQAMNVHN.js → azure-7OXQENQL.js} +1 -1
  4. package/dist/{bootstrap-RY4VOSDJ.js → bootstrap-HL4DNUNG.js} +13 -4
  5. package/dist/bootstrap.sh.tmpl +204 -0
  6. package/dist/chunk-2YOP6N7X.js +126 -0
  7. package/dist/{chunk-GJEF6UQA.js → chunk-3XK66DKK.js} +100 -16
  8. package/dist/{chunk-ISFHLA4G.js → chunk-5SPESD5N.js} +8 -6
  9. package/dist/{chunk-YTH4L2GN.js → chunk-6X45DDGH.js} +5 -4
  10. package/dist/{chunk-XKT42M34.js → chunk-C5X375RO.js} +14 -1
  11. package/dist/{chunk-LCKD7L7X.js → chunk-DQDAWNGB.js} +1 -1
  12. package/dist/{chunk-CG3Y7Y2R.js → chunk-FSUWPL7Q.js} +9 -9
  13. package/dist/{chunk-ZFNPM2WG.js → chunk-O3F7OGH7.js} +40 -3
  14. package/dist/{chunk-3MFZ7E74.js → chunk-P5LKWRPL.js} +3 -3
  15. package/dist/chunk-PVUVI35Y.js +29 -0
  16. package/dist/{chunk-FQ4JXUCR.js → chunk-SEABO2UU.js} +7 -4
  17. package/dist/cli.js +122 -68
  18. package/dist/{context-ALSJMTHE.js → context-5LCUG2QR.js} +1 -1
  19. package/dist/{gcp-6FT2A45S.js → gcp-UG6AWPIZ.js} +1 -1
  20. package/dist/{generate-SPT7PJKR.js → generate-NIF6IBZC.js} +4 -3
  21. package/dist/{harden-BDVYW567.js → harden-URPXLSVF.js} +2 -2
  22. package/dist/{mcp-wire-ZWIYMLEW.js → mcp-wire-SLL4TLSC.js} +1 -1
  23. package/dist/{package-OMPI6RMV.js → package-TZ2HYF5C.js} +3 -2
  24. package/dist/{pool-JZGKPP6K.js → pool-QGFQRMGU.js} +2 -2
  25. package/dist/{remote-config-QF5TT7GU.js → remote-config-PFB66RPY.js} +3 -1
  26. package/dist/{server-RGLKKCCU.js → server-WM3AY4PJ.js} +19 -18
  27. package/dist/{ssh-E2AMBPZY.js → ssh-CNJNLM3E.js} +1 -1
  28. package/dist/{validate-T5M5EHSJ.js → validate-CFQ4FH2T.js} +2 -1
  29. package/dist/version-guard-MZAO4KHS.js +25 -0
  30. package/dist/versions-OEPAS2EL.js +24 -0
  31. package/package.json +3 -2
  32. package/spec/_gen-schemas.md +136 -0
  33. package/spec/deploy-plan.schema.json +193 -0
  34. package/spec/errors.yaml +175 -0
  35. package/spec/integrations.yaml +97 -0
  36. package/spec/invariants.yaml +81 -0
  37. package/spec/mcp-tools.yaml +532 -0
  38. package/spec/models.yaml +113 -0
  39. package/spec/openclaw-versions.yaml +67 -0
  40. package/spec/providers.schema.json +120 -0
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  applyPlan
4
- } from "./chunk-ISFHLA4G.js";
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-ZFNPM2WG.js";
9
- import "./chunk-3MFZ7E74.js";
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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  makeStartupScript
4
- } from "./chunk-XKT42M34.js";
4
+ } from "./chunk-C5X375RO.js";
5
5
 
6
6
  // src/providers/aws/index.ts
7
7
  import process2 from "process";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  makeStartupScript
4
- } from "./chunk-XKT42M34.js";
4
+ } from "./chunk-C5X375RO.js";
5
5
 
6
6
  // src/providers/azure/index.ts
7
7
  import process2 from "process";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  acquireSession
4
- } from "./chunk-LCKD7L7X.js";
5
- import "./chunk-GJEF6UQA.js";
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
- return readFileSync(path.join(dir, "bootstrap.sh.tmpl"), "utf-8");
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
- const keyHex = keyHash.toString("hex");
151
- const hostEntry = port === 22 ? host : `[${host}]:${port}`;
152
- let existing = null;
242
+ let content = "";
153
243
  try {
154
- const content = readFileSync(knownHostsPath, "utf-8");
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
- if (existing !== null) {
165
- return existing === keyHex;
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, `${hostEntry} ${keyHex}
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-ZFNPM2WG.js";
16
+ } from "./chunk-O3F7OGH7.js";
17
17
  import {
18
18
  buildContext
19
- } from "./chunk-3MFZ7E74.js";
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-E2AMBPZY.js");
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 { fileURLToPath } from "url";
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(__dirname, "../../spec/deploy-plan.schema.json"), "utf-8")
17
+ readFileSync(join(resolveSpecDir(), "deploy-plan.schema.json"), "utf-8")
17
18
  );
18
19
  _validate = ajv.compile(schema);
19
20
  return _validate;
@@ -42,6 +42,17 @@ usermod -aG docker clawops
42
42
  OPENCLAW_VERSION="${openclawVersion}"
43
43
  docker pull ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION}
44
44
 
45
+ # \u2500\u2500 Gateway auth token \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
46
+ # OpenClaw refuses a non-loopback bind without auth, and in a container it always
47
+ # binds 0.0.0.0. Without a token the gateway exits 78 and restart-loops.
48
+ OPENCLAW_ENV_FILE=/home/clawops/openclaw.env
49
+ if [ ! -s "\${OPENCLAW_ENV_FILE}" ]; then
50
+ OPENCLAW_TOKEN=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \\n')
51
+ printf 'OPENCLAW_GATEWAY_TOKEN=%s\\n' "\${OPENCLAW_TOKEN}" > "\${OPENCLAW_ENV_FILE}"
52
+ chmod 600 "\${OPENCLAW_ENV_FILE}"
53
+ chown clawops:clawops "\${OPENCLAW_ENV_FILE}"
54
+ fi
55
+
45
56
  # \u2500\u2500 Default config (apply.ts will overwrite with plan overlay) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
46
57
  OPENCLAW_CONFIG=/home/clawops/openclaw.json
47
58
  if [ ! -f "\${OPENCLAW_CONFIG}" ]; then
@@ -58,9 +69,11 @@ docker run -d \\
58
69
  --name openclaw \\
59
70
  --restart unless-stopped \\
60
71
  -p 18789:18789 \\
72
+ -e OPENCLAW_CONFIG_PATH=/app/config.json --add-host=host.docker.internal:host-gateway \\
73
+ --env-file "\${OPENCLAW_ENV_FILE}" \\
61
74
  -v "\${OPENCLAW_CONFIG}":/app/config.json:ro \\
62
75
  ${bedrockEnvBlock} ghcr.io/openclaw/openclaw:\${OPENCLAW_VERSION} \\
63
- node openclaw.mjs gateway run --allow-unconfigured
76
+ node openclaw.mjs gateway run --allow-unconfigured --port 18789
64
77
  `;
65
78
  }
66
79
  function makeBedrockEnvBlock() {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  connect
4
- } from "./chunk-GJEF6UQA.js";
4
+ } from "./chunk-3XK66DKK.js";
5
5
  import {
6
6
  NetworkError
7
7
  } from "./chunk-KGXPLI7W.js";