@clawops/cli 1.7.8 → 2.0.0
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 +412 -70
- package/dist/{apply-F3KY2HTB.js → apply-AM5HERE7.js} +8 -6
- package/dist/{aws-L4RR2LSE.js → aws-AMON7DA7.js} +20 -9
- package/dist/{azure-7OXQENQL.js → azure-T4G6OBJQ.js} +19 -8
- package/dist/{bootstrap-HL4DNUNG.js → bootstrap-DMRPWJGN.js} +28 -19
- package/dist/bootstrap.sh.tmpl +51 -31
- package/dist/channels-KBUR42IU.js +53 -0
- package/dist/{chunk-DQDAWNGB.js → chunk-3WYA2QDM.js} +1 -1
- package/dist/chunk-6TIYVHPX.js +152 -0
- package/dist/{chunk-3XK66DKK.js → chunk-AHAAEXWC.js} +41 -0
- package/dist/{chunk-QQNKBTYZ.js → chunk-AX74VXTV.js} +292 -94
- package/dist/{chunk-C5X375RO.js → chunk-CKSPROD5.js} +59 -15
- package/dist/{chunk-UKTP332Z.js → chunk-GAZPPSKU.js} +6 -7
- package/dist/{chunk-FNS56UQY.js → chunk-GYV4KNW7.js} +319 -252
- package/dist/{chunk-PVUVI35Y.js → chunk-JW5A7E3X.js} +5 -1
- package/dist/chunk-LT4XMO5D.js +50 -0
- package/dist/{chunk-P5LKWRPL.js → chunk-LU2V3LXA.js} +3 -3
- package/dist/chunk-N44K3LHE.js +140 -0
- package/dist/chunk-NUDIHZEH.js +55 -0
- package/dist/chunk-SBLQH2TJ.js +56 -0
- package/dist/chunk-UFRO4H5W.js +228 -0
- package/dist/{chunk-RAAONHW3.js → chunk-WHJMKDV7.js} +24 -4
- package/dist/chunk-YR3O2I3S.js +101 -0
- package/dist/cli.js +866 -577
- package/dist/config-validate-K2N6XLDX.js +76 -0
- package/dist/{context-5LCUG2QR.js → context-KHKWJJMC.js} +1 -1
- package/dist/{firewall-XQGO7HWK.js → firewall-T3NTPZJD.js} +10 -0
- package/dist/{gcp-UG6AWPIZ.js → gcp-2JBTD7ZM.js} +19 -8
- package/dist/{generate-ELD7QF5Z.js → generate-2XV2GSP2.js} +4 -4
- package/dist/{harden-URPXLSVF.js → harden-PCDK3UQ5.js} +48 -11
- package/dist/health-5CCOXYGG.js +11 -0
- package/dist/logs-P7YCKVF4.js +15 -0
- package/dist/{mcp-wire-ZMIVFAUC.js → mcp-wire-BFEY2Y6H.js} +23 -17
- package/dist/{package-QYZFN3FP.js → package-3HUW57XH.js} +3 -3
- package/dist/plugins-DTST3OER.js +49 -0
- package/dist/{pool-QGFQRMGU.js → pool-EX4ZTA5P.js} +2 -2
- package/dist/privileged-JFA6LLTV.js +17 -0
- package/dist/{remote-config-QDLN4UMG.js → remote-config-Z6IIZR3D.js} +3 -3
- package/dist/runtime-LZ7NA53V.js +42 -0
- package/dist/{server-FWTOSSZP.js → server-A5AB67GQ.js} +16 -12
- package/dist/spec-path-KQOOIQCA.js +9 -0
- package/dist/{ssh-CNJNLM3E.js → ssh-B3NZAU4I.js} +1 -1
- package/dist/upgrade-ULD4KFYC.js +88 -0
- package/dist/validate-BOQRWX4F.js +14 -0
- package/dist/{version-guard-VUCNZXQS.js → version-guard-GPSTCPT7.js} +3 -3
- package/dist/{versions-2UVAT6PQ.js → versions-W5LRQCAJ.js} +2 -2
- package/package.json +3 -3
- package/spec/deploy-plan.schema.json +114 -25
- package/spec/integrations.yaml +135 -28
- package/spec/mcp-tools.yaml +68 -35
- package/spec/models.yaml +9 -0
- package/spec/openclaw-2.0.config.schema.json +1 -0
- package/spec/openclaw-versions.yaml +116 -27
- package/dist/chunk-35WVQY3X.js +0 -122
- package/dist/chunk-6X45DDGH.js +0 -42
- package/dist/chunk-SEZMVVAS.js +0 -156
- package/dist/validate-CFQ4FH2T.js +0 -10
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
makeStartupScript
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-CKSPROD5.js";
|
|
5
|
+
import {
|
|
6
|
+
GATEWAY_PORT
|
|
7
|
+
} from "./chunk-6TIYVHPX.js";
|
|
8
|
+
import "./chunk-EI7CQQ7D.js";
|
|
5
9
|
|
|
6
10
|
// src/providers/aws/index.ts
|
|
7
11
|
import process2 from "process";
|
|
8
12
|
|
|
9
13
|
// src/providers/aws/program.ts
|
|
10
|
-
var GATEWAY_PORT = 18789;
|
|
11
14
|
var SSH_PORT = 22;
|
|
12
15
|
var awsProgram = async () => {
|
|
13
|
-
const [pulumi, aws, { resolveIngressCidrs, detectEgressIp }] = await Promise.all([
|
|
16
|
+
const [pulumi, aws, { resolveIngressCidrs, resolveGatewayIngressCidrs, resolveGatewayPort, detectEgressIp }] = await Promise.all([
|
|
14
17
|
import("@pulumi/pulumi"),
|
|
15
18
|
import("@pulumi/aws"),
|
|
16
|
-
import("./firewall-
|
|
19
|
+
import("./firewall-T3NTPZJD.js")
|
|
17
20
|
]);
|
|
18
21
|
const cfg = new pulumi.Config();
|
|
19
22
|
const instanceType = cfg.get("instanceType") ?? "t3.small";
|
|
20
23
|
const region = cfg.get("region") ?? "us-east-1";
|
|
21
24
|
const openclawVersion = cfg.get("openclawVersion") ?? "latest";
|
|
25
|
+
const publishGateway = cfg.get("publishGateway") === "all" ? "all" : "loopback";
|
|
26
|
+
const gatewayPort = resolveGatewayPort(cfg.get("gatewayPort"), GATEWAY_PORT);
|
|
22
27
|
const accessMode = cfg.get("accessMode") ?? "restricted";
|
|
23
28
|
const allowedCidrs = cfg.get("allowedCidrs") ?? "";
|
|
24
29
|
const sshCidrs = cfg.get("sshCidrs") ?? "";
|
|
@@ -38,7 +43,13 @@ var awsProgram = async () => {
|
|
|
38
43
|
);
|
|
39
44
|
}
|
|
40
45
|
const sshIngressCidrs = resolveIngressCidrs(accessMode, allowedCidrs, sshCidrs, egressResult);
|
|
41
|
-
const gatewayIngressCidrs =
|
|
46
|
+
const gatewayIngressCidrs = resolveGatewayIngressCidrs(
|
|
47
|
+
publishGateway,
|
|
48
|
+
accessMode,
|
|
49
|
+
allowedCidrs,
|
|
50
|
+
gatewayCidrs,
|
|
51
|
+
egressResult
|
|
52
|
+
);
|
|
42
53
|
const vpc = new aws.ec2.Vpc("clawops-vpc", {
|
|
43
54
|
cidrBlock: "10.0.0.0/16",
|
|
44
55
|
enableDnsHostnames: true,
|
|
@@ -91,8 +102,8 @@ var awsProgram = async () => {
|
|
|
91
102
|
new aws.vpc.SecurityGroupIngressRule(`clawops-sg-gw-${i}`, {
|
|
92
103
|
securityGroupId: sg.id,
|
|
93
104
|
ipProtocol: "tcp",
|
|
94
|
-
fromPort:
|
|
95
|
-
toPort:
|
|
105
|
+
fromPort: gatewayPort,
|
|
106
|
+
toPort: gatewayPort,
|
|
96
107
|
cidrIpv4: cidr,
|
|
97
108
|
tags: { Name: `clawops-gateway-${i}` }
|
|
98
109
|
});
|
|
@@ -159,7 +170,7 @@ var awsProgram = async () => {
|
|
|
159
170
|
vpcSecurityGroupIds: [sg.id],
|
|
160
171
|
iamInstanceProfile: instanceProfile.name,
|
|
161
172
|
keyName: keyPair.keyName,
|
|
162
|
-
userData: makeStartupScript({ openclawVersion, os: "ubuntu", bedrockEnabled }),
|
|
173
|
+
userData: makeStartupScript({ openclawVersion, os: "ubuntu", bedrockEnabled, publishGateway, gatewayPort }),
|
|
163
174
|
// IMDSv2 with hopLimit=2 so Docker containers on this host can reach IMDS
|
|
164
175
|
// and obtain the instance role credentials (required for Bedrock access).
|
|
165
176
|
metadataOptions: {
|
|
@@ -179,7 +190,7 @@ var awsProgram = async () => {
|
|
|
179
190
|
return {
|
|
180
191
|
instanceId: instance.id,
|
|
181
192
|
publicIp: eip.publicIp,
|
|
182
|
-
gatewayUrl: pulumi.interpolate`https://${eip.publicIp}:${
|
|
193
|
+
gatewayUrl: pulumi.interpolate`https://${eip.publicIp}:${gatewayPort}`,
|
|
183
194
|
sshHost: eip.publicIp,
|
|
184
195
|
sshPort: SSH_PORT,
|
|
185
196
|
sshUser: "ubuntu",
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
makeStartupScript
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-CKSPROD5.js";
|
|
5
|
+
import {
|
|
6
|
+
GATEWAY_PORT
|
|
7
|
+
} from "./chunk-6TIYVHPX.js";
|
|
8
|
+
import "./chunk-EI7CQQ7D.js";
|
|
5
9
|
|
|
6
10
|
// src/providers/azure/index.ts
|
|
7
11
|
import process2 from "process";
|
|
8
12
|
|
|
9
13
|
// src/providers/azure/program.ts
|
|
10
14
|
import { createHash } from "crypto";
|
|
11
|
-
var GATEWAY_PORT = 18789;
|
|
12
15
|
var SSH_PORT = 22;
|
|
13
16
|
var azureProgram = async () => {
|
|
14
|
-
const [pulumi, azure, random, { resolveIngressCidrs, detectEgressIp }] = await Promise.all([
|
|
17
|
+
const [pulumi, azure, random, { resolveIngressCidrs, resolveGatewayIngressCidrs, resolveGatewayPort, detectEgressIp }] = await Promise.all([
|
|
15
18
|
import("@pulumi/pulumi"),
|
|
16
19
|
import("@pulumi/azure-native"),
|
|
17
20
|
import("@pulumi/random"),
|
|
18
|
-
import("./firewall-
|
|
21
|
+
import("./firewall-T3NTPZJD.js")
|
|
19
22
|
]);
|
|
20
23
|
const cfg = new pulumi.Config();
|
|
21
24
|
const stackName = pulumi.getStack();
|
|
22
25
|
const instanceType = cfg.get("instanceType") ?? "Standard_B2s";
|
|
23
26
|
const location = cfg.get("region") ?? "eastus";
|
|
24
27
|
const openclawVersion = cfg.get("openclawVersion") ?? "latest";
|
|
28
|
+
const publishGateway = cfg.get("publishGateway") === "all" ? "all" : "loopback";
|
|
29
|
+
const gatewayPort = resolveGatewayPort(cfg.get("gatewayPort"), GATEWAY_PORT);
|
|
25
30
|
const accessMode = cfg.get("accessMode") ?? "restricted";
|
|
26
31
|
const allowedCidrs = cfg.get("allowedCidrs") ?? "";
|
|
27
32
|
const sshCidrs = cfg.get("sshCidrs") ?? "";
|
|
@@ -42,7 +47,13 @@ var azureProgram = async () => {
|
|
|
42
47
|
);
|
|
43
48
|
}
|
|
44
49
|
const sshIngressCidrs = resolveIngressCidrs(accessMode, allowedCidrs, sshCidrs, egressResult);
|
|
45
|
-
const gatewayIngressCidrs =
|
|
50
|
+
const gatewayIngressCidrs = resolveGatewayIngressCidrs(
|
|
51
|
+
publishGateway,
|
|
52
|
+
accessMode,
|
|
53
|
+
allowedCidrs,
|
|
54
|
+
gatewayCidrs,
|
|
55
|
+
egressResult
|
|
56
|
+
);
|
|
46
57
|
const rg = new azure.resources.ResourceGroup("clawops-rg", {
|
|
47
58
|
resourceGroupName,
|
|
48
59
|
location
|
|
@@ -82,7 +93,7 @@ var azureProgram = async () => {
|
|
|
82
93
|
sourceAddressPrefix: cidr,
|
|
83
94
|
sourcePortRange: "*",
|
|
84
95
|
destinationAddressPrefix: "*",
|
|
85
|
-
destinationPortRange: String(
|
|
96
|
+
destinationPortRange: String(gatewayPort)
|
|
86
97
|
});
|
|
87
98
|
}
|
|
88
99
|
const nsg = new azure.network.NetworkSecurityGroup("clawops-nsg", {
|
|
@@ -115,7 +126,7 @@ var azureProgram = async () => {
|
|
|
115
126
|
osProfile: {
|
|
116
127
|
adminUsername: "clawops",
|
|
117
128
|
computerName: "clawops",
|
|
118
|
-
customData: Buffer.from(makeStartupScript({ openclawVersion, os: "ubuntu" })).toString("base64"),
|
|
129
|
+
customData: Buffer.from(makeStartupScript({ openclawVersion, os: "ubuntu", publishGateway, gatewayPort })).toString("base64"),
|
|
119
130
|
linuxConfiguration: {
|
|
120
131
|
disablePasswordAuthentication: true,
|
|
121
132
|
ssh: {
|
|
@@ -181,7 +192,7 @@ var azureProgram = async () => {
|
|
|
181
192
|
return {
|
|
182
193
|
instanceId: vm.id,
|
|
183
194
|
publicIp: resolvedIp,
|
|
184
|
-
gatewayUrl: pulumi.interpolate`https://${resolvedIp}:${
|
|
195
|
+
gatewayUrl: pulumi.interpolate`https://${resolvedIp}:${gatewayPort}`,
|
|
185
196
|
sshHost: resolvedIp,
|
|
186
197
|
sshPort: SSH_PORT,
|
|
187
198
|
sshUser: "clawops",
|
|
@@ -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-3WYA2QDM.js";
|
|
5
|
+
import "./chunk-AHAAEXWC.js";
|
|
6
6
|
import {
|
|
7
7
|
writeLocalState
|
|
8
8
|
} from "./chunk-A2I76FTA.js";
|
|
@@ -10,12 +10,17 @@ import "./chunk-CX5SL5HP.js";
|
|
|
10
10
|
import {
|
|
11
11
|
ProviderError
|
|
12
12
|
} from "./chunk-KGXPLI7W.js";
|
|
13
|
+
import {
|
|
14
|
+
GATEWAY_PORT,
|
|
15
|
+
gatewayRunArgs,
|
|
16
|
+
gatewayRunCommand
|
|
17
|
+
} from "./chunk-6TIYVHPX.js";
|
|
18
|
+
import "./chunk-EI7CQQ7D.js";
|
|
13
19
|
|
|
14
20
|
// src/providers/local/bootstrap.ts
|
|
15
21
|
import { readFileSync, existsSync } from "fs";
|
|
16
22
|
import path from "path";
|
|
17
23
|
import { fileURLToPath } from "url";
|
|
18
|
-
var GATEWAY_PORT = 18789;
|
|
19
24
|
var HEALTH_POLL_INTERVAL_MS = 3e3;
|
|
20
25
|
var HEALTH_TIMEOUT_MS = 12e4;
|
|
21
26
|
function loadTemplate() {
|
|
@@ -31,11 +36,18 @@ function loadTemplate() {
|
|
|
31
36
|
`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
37
|
);
|
|
33
38
|
}
|
|
34
|
-
function renderScript(openclawVersion) {
|
|
35
|
-
|
|
39
|
+
function renderScript(openclawVersion, gatewayPort = GATEWAY_PORT) {
|
|
40
|
+
const shared = {
|
|
41
|
+
image: "ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}",
|
|
42
|
+
stateDir: '"${OPENCLAW_STATE_DIR}"',
|
|
43
|
+
envFilePath: '"${OPENCLAW_ENV_FILE}"',
|
|
44
|
+
port: "${OPENCLAW_PORT}"
|
|
45
|
+
};
|
|
46
|
+
return loadTemplate().replace(/\{\{GATEWAY_RUN_SYSTEMD\}\}/g, gatewayRunArgs({ ...shared, supervisor: "systemd" })).replace(/\{\{GATEWAY_RUN_DARWIN\}\}/g, gatewayRunCommand(shared)).replace(/\{\{OPENCLAW_VERSION\}\}/g, openclawVersion).replace(/\{\{OPENCLAW_PORT\}\}/g, String(gatewayPort));
|
|
36
47
|
}
|
|
37
48
|
async function localBootstrap(opts) {
|
|
38
|
-
const
|
|
49
|
+
const gatewayPort = opts.gatewayPort ?? GATEWAY_PORT;
|
|
50
|
+
const script = renderScript(opts.openclawVersion, gatewayPort);
|
|
39
51
|
const b64 = Buffer.from(script, "utf-8").toString("base64");
|
|
40
52
|
const command = opts.sudoPassword ? `printf '%s
|
|
41
53
|
' ${shellQuote(opts.sudoPassword)} | sudo -S -p '' bash -c "echo '${b64}' | base64 -d | bash"` : `echo '${b64}' | base64 -d | sudo bash`;
|
|
@@ -59,13 +71,14 @@ ${result.stderr || result.stdout}`
|
|
|
59
71
|
);
|
|
60
72
|
}
|
|
61
73
|
}
|
|
74
|
+
if (!opts.noWait) await waitForGateway(session, gatewayPort, opts.signal);
|
|
62
75
|
} finally {
|
|
63
76
|
release();
|
|
64
77
|
}
|
|
65
78
|
const state = {
|
|
66
79
|
instanceId: `local:${opts.host}`,
|
|
67
80
|
publicIp: opts.host,
|
|
68
|
-
gatewayUrl: `http://${opts.host}:${
|
|
81
|
+
gatewayUrl: `http://${opts.host}:${gatewayPort}`,
|
|
69
82
|
sshHost: opts.host,
|
|
70
83
|
sshPort: opts.port,
|
|
71
84
|
sshUser: opts.user,
|
|
@@ -75,27 +88,22 @@ ${result.stderr || result.stdout}`
|
|
|
75
88
|
knownHostsPath: opts.knownHostsPath
|
|
76
89
|
};
|
|
77
90
|
writeLocalState(opts.stackName, state);
|
|
78
|
-
if (!opts.noWait) {
|
|
79
|
-
await waitForGateway(opts.host, GATEWAY_PORT, opts.signal);
|
|
80
|
-
}
|
|
81
91
|
return state;
|
|
82
92
|
}
|
|
83
|
-
async function waitForGateway(
|
|
84
|
-
const
|
|
93
|
+
async function waitForGateway(session, gatewayPort, signal) {
|
|
94
|
+
const { probeCommand, interpretProbe } = await import("./health-5CCOXYGG.js");
|
|
95
|
+
const probe = probeCommand("started", gatewayPort);
|
|
85
96
|
const deadline = Date.now() + HEALTH_TIMEOUT_MS;
|
|
86
97
|
while (Date.now() < deadline) {
|
|
87
98
|
if (signal?.aborted) {
|
|
88
99
|
throw new ProviderError("Bootstrap aborted while waiting for gateway");
|
|
89
100
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (res.ok) return;
|
|
93
|
-
} catch {
|
|
94
|
-
}
|
|
101
|
+
const result = await session.exec(probe, signal);
|
|
102
|
+
if (interpretProbe("started", result.stdout).ok) return;
|
|
95
103
|
await sleep(HEALTH_POLL_INTERVAL_MS);
|
|
96
104
|
}
|
|
97
105
|
throw new ProviderError(
|
|
98
|
-
`Gateway
|
|
106
|
+
`Gateway did not finish starting within ${HEALTH_TIMEOUT_MS / 1e3}s. Probed /startupz on the host's loopback; check \`clawops logs\`.`
|
|
99
107
|
);
|
|
100
108
|
}
|
|
101
109
|
function sleep(ms) {
|
|
@@ -132,5 +140,6 @@ ${stderr}`);
|
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
export {
|
|
135
|
-
localBootstrap
|
|
143
|
+
localBootstrap,
|
|
144
|
+
renderScript
|
|
136
145
|
};
|
package/dist/bootstrap.sh.tmpl
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
set -euo pipefail
|
|
6
6
|
|
|
7
7
|
OPENCLAW_VERSION="{{OPENCLAW_VERSION}}"
|
|
8
|
-
OPENCLAW_PORT=
|
|
8
|
+
OPENCLAW_PORT={{OPENCLAW_PORT}}
|
|
9
9
|
|
|
10
10
|
# ── OS detection ──────────────────────────────────────────────────────────────
|
|
11
11
|
OS="$(uname -s)"
|
|
@@ -25,17 +25,20 @@ case "$OS" in
|
|
|
25
25
|
exit 1
|
|
26
26
|
fi
|
|
27
27
|
OPENCLAW_USER=clawops
|
|
28
|
-
|
|
28
|
+
OPENCLAW_STATE_DIR=/var/lib/clawops/openclaw
|
|
29
|
+
OPENCLAW_CONFIG="${OPENCLAW_STATE_DIR}/openclaw.json"
|
|
30
|
+
OPENCLAW_LEGACY_CONFIG=/home/${OPENCLAW_USER}/openclaw.json
|
|
29
31
|
;;
|
|
30
32
|
Darwin)
|
|
31
33
|
PKG_MANAGER=darwin
|
|
32
34
|
# When invoked via sudo the script runs as root but SUDO_USER is the real caller.
|
|
33
35
|
# Fall back to id -un for passwordless (non-sudo) invocations.
|
|
34
36
|
OPENCLAW_USER="${SUDO_USER:-$(id -un)}"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
OPENCLAW_STATE_DIR="${HOME}/.clawops/openclaw"
|
|
38
|
+
OPENCLAW_CONFIG="${OPENCLAW_STATE_DIR}/openclaw.json"
|
|
39
|
+
OPENCLAW_LEGACY_CONFIG="${HOME}/.config/openclaw/config.json"
|
|
37
40
|
# Fix ownership so subsequent non-sudo SSH writes (atomicWriteConfig) can overwrite.
|
|
38
|
-
chown "${OPENCLAW_USER}" "${
|
|
41
|
+
chown "${OPENCLAW_USER}" "${OPENCLAW_STATE_DIR}" 2>/dev/null || true
|
|
39
42
|
# sudo resets PATH; add common Docker install locations so `command -v docker` works.
|
|
40
43
|
# Covers: Docker Desktop (Intel + AS), Homebrew (AS), Colima.
|
|
41
44
|
export PATH="/usr/local/bin:/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH"
|
|
@@ -140,17 +143,51 @@ if [ ! -s "${OPENCLAW_ENV_FILE}" ]; then
|
|
|
140
143
|
chown "${OPENCLAW_USER}" "${OPENCLAW_ENV_FILE}" 2>/dev/null || true
|
|
141
144
|
fi
|
|
142
145
|
|
|
146
|
+
# ── State directory ───────────────────────────────────────────────────────────
|
|
147
|
+
# One directory holds config, the SQLite state and any plugins installed into it, so a
|
|
148
|
+
# container replacement no longer destroys sessions and credentials.
|
|
149
|
+
mkdir -p "${OPENCLAW_STATE_DIR}"
|
|
150
|
+
|
|
151
|
+
# Migrate a pre-2.0 deployment. The config used to be a bare file elsewhere; without this
|
|
152
|
+
# an in-place upgrade comes up with no configuration at all. Guarded on the target being
|
|
153
|
+
# absent, so re-running the bootstrap never clobbers a later edit.
|
|
154
|
+
if [ -f "${OPENCLAW_LEGACY_CONFIG}" ] && [ ! -f "${OPENCLAW_CONFIG}" ]; then
|
|
155
|
+
cp -p "${OPENCLAW_LEGACY_CONFIG}" "${OPENCLAW_CONFIG}"
|
|
156
|
+
mv "${OPENCLAW_LEGACY_CONFIG}" "${OPENCLAW_LEGACY_CONFIG}.migrated"
|
|
157
|
+
echo "clawops: migrated config from ${OPENCLAW_LEGACY_CONFIG}"
|
|
158
|
+
OPENCLAW_MIGRATED=1
|
|
159
|
+
fi
|
|
160
|
+
|
|
143
161
|
# ── Write default openclaw.json if absent ─────────────────────────────────────
|
|
144
162
|
# clawops up --config will overwrite this with the plan config overlay.
|
|
145
163
|
if [ ! -f "${OPENCLAW_CONFIG}" ]; then
|
|
146
|
-
|
|
147
|
-
|
|
164
|
+
# Unquoted heredoc, so ${OPENCLAW_PORT} expands. gateway.port and the -p mapping have to
|
|
165
|
+
# be the same number: a container that publishes one port while the gateway listens on
|
|
166
|
+
# another starts, looks healthy to Docker, and answers nothing.
|
|
167
|
+
cat > "${OPENCLAW_CONFIG}" <<OPENCLAWJSON
|
|
168
|
+
{"meta":{"lastTouchedVersion":"2026.9"},"gateway":{"mode":"local","port":${OPENCLAW_PORT},"auth":{"mode":"token"}},"models":{},"channels":{}}
|
|
148
169
|
OPENCLAWJSON
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# Ownership last, so it covers the config and anything migrated above.
|
|
173
|
+
if [ "$OS" = "Linux" ]; then
|
|
174
|
+
# NUMERIC, never clawops:clawops. useradd gets uid 1001 on Ubuntu 24.04 because the
|
|
175
|
+
# ubuntu user already holds 1000, and the container runs as 1000 — a 1001-owned state
|
|
176
|
+
# dir makes the gateway exit 1 with EACCES on its own SQLite WAL.
|
|
177
|
+
chown -R 1000:1000 "${OPENCLAW_STATE_DIR}"
|
|
178
|
+
else
|
|
179
|
+
chown -R "${OPENCLAW_USER}" "${OPENCLAW_STATE_DIR}" 2>/dev/null || true
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# A 1.x config has no gateway.mode, and clawops no longer passes --allow-unconfigured, so a
|
|
183
|
+
# migrated deployment would exit 78. Normalised with OpenClaw's own tooling — schema-aware,
|
|
184
|
+
# atomic, and it applies OpenClaw's internal config migrations too. After the chown, so the
|
|
185
|
+
# container can write.
|
|
186
|
+
if [ "${OPENCLAW_MIGRATED:-0}" = "1" ]; then
|
|
187
|
+
docker run --rm -v "${OPENCLAW_STATE_DIR}":/home/node/.openclaw \
|
|
188
|
+
"ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}" \
|
|
189
|
+
openclaw config set gateway.mode local || \
|
|
190
|
+
echo "clawops: WARNING could not set gateway.mode; gateway may refuse to start" >&2
|
|
154
191
|
fi
|
|
155
192
|
|
|
156
193
|
# ── Start service ─────────────────────────────────────────────────────────────
|
|
@@ -168,13 +205,7 @@ Restart=always
|
|
|
168
205
|
RestartSec=5
|
|
169
206
|
ExecStartPre=-/usr/bin/docker stop openclaw
|
|
170
207
|
ExecStartPre=-/usr/bin/docker rm openclaw
|
|
171
|
-
ExecStart=/usr/bin/
|
|
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}
|
|
208
|
+
ExecStart=/usr/bin/{{GATEWAY_RUN_SYSTEMD}}
|
|
178
209
|
ExecStop=/usr/bin/docker stop openclaw
|
|
179
210
|
|
|
180
211
|
[Install]
|
|
@@ -187,18 +218,7 @@ UNIT
|
|
|
187
218
|
elif [ "$OS" = "Darwin" ]; then
|
|
188
219
|
# macOS: no systemd; run as foreground-detached container.
|
|
189
220
|
# LaunchAgent support is a future enhancement.
|
|
190
|
-
|
|
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}
|
|
221
|
+
{{GATEWAY_RUN_DARWIN}}
|
|
202
222
|
fi
|
|
203
223
|
|
|
204
224
|
echo "clawops: bootstrap complete"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/openclaw/channels.ts
|
|
4
|
+
function requiredChannelPlugins(cfg, catalog) {
|
|
5
|
+
const configured = configuredChannelKeys(cfg);
|
|
6
|
+
const out = [];
|
|
7
|
+
for (const entry of catalog.integrations) {
|
|
8
|
+
const plugin = entry.plugin;
|
|
9
|
+
if (!plugin || plugin.source === "bundled" || !plugin.package) continue;
|
|
10
|
+
if (configured.has(entry.channelKey)) {
|
|
11
|
+
out.push({
|
|
12
|
+
channelKey: entry.channelKey,
|
|
13
|
+
package: plugin.package,
|
|
14
|
+
version: plugin.version ?? ""
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function channelInstallCommand(plugin, image, stateDir) {
|
|
21
|
+
const spec = plugin.version ? `${plugin.package}@${plugin.version}` : plugin.package;
|
|
22
|
+
return `docker run --rm -v ${stateDir}:/home/node/.openclaw ${image} openclaw plugins install '${spec}' --accept-capabilities`;
|
|
23
|
+
}
|
|
24
|
+
var CHANNELS_LIST_CMD = "docker exec openclaw openclaw channels list --all --json";
|
|
25
|
+
function missingChannels(cfg, channelsListJson, catalog) {
|
|
26
|
+
let installed;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(channelsListJson);
|
|
29
|
+
const chat = parsed.chat ?? {};
|
|
30
|
+
installed = new Set(
|
|
31
|
+
Object.entries(chat).filter(([, v]) => v.installed === true).map(([k]) => k)
|
|
32
|
+
);
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const bundled = new Set(
|
|
37
|
+
catalog.integrations.filter((i) => i.plugin?.source === "bundled").map((i) => i.channelKey)
|
|
38
|
+
);
|
|
39
|
+
return [...configuredChannelKeys(cfg)].filter((k) => !installed.has(k) && !bundled.has(k));
|
|
40
|
+
}
|
|
41
|
+
function configuredChannelKeys(cfg) {
|
|
42
|
+
if (cfg === null || typeof cfg !== "object") return /* @__PURE__ */ new Set();
|
|
43
|
+
const channels = cfg["channels"];
|
|
44
|
+
if (channels === null || typeof channels !== "object") return /* @__PURE__ */ new Set();
|
|
45
|
+
const notAChannel = /* @__PURE__ */ new Set(["defaults", "modelByChannel"]);
|
|
46
|
+
return new Set(Object.keys(channels).filter((k) => !notAChannel.has(k)));
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
CHANNELS_LIST_CMD,
|
|
50
|
+
channelInstallCommand,
|
|
51
|
+
missingChannels,
|
|
52
|
+
requiredChannelPlugins
|
|
53
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
err,
|
|
4
|
+
ok
|
|
5
|
+
} from "./chunk-EI7CQQ7D.js";
|
|
6
|
+
|
|
7
|
+
// src/openclaw/run-flags.ts
|
|
8
|
+
var CONFIG_MOUNT_PATH = "/app/config.json";
|
|
9
|
+
var GATEWAY_PORT = 18789;
|
|
10
|
+
var CONFIG_PATH_ENV = `-e OPENCLAW_CONFIG_PATH=${CONFIG_MOUNT_PATH}`;
|
|
11
|
+
var ADD_HOST_FLAG = "--add-host=host.docker.internal:host-gateway";
|
|
12
|
+
var PORT_PIN = `--port ${GATEWAY_PORT}`;
|
|
13
|
+
var COMMON_RUN_FLAGS = `${CONFIG_PATH_ENV} ${ADD_HOST_FLAG}`;
|
|
14
|
+
var IMAGE_INSPECT_CMD = `docker inspect openclaw --format '{{.Config.Image}}'`;
|
|
15
|
+
function imageForRestart(inspectStdout) {
|
|
16
|
+
const image = inspectStdout.trim();
|
|
17
|
+
if (!image || image.includes("Error") || !image.includes(":")) {
|
|
18
|
+
return err(
|
|
19
|
+
"No running OpenClaw container found, so there is no image version to reuse. Deploy one with `clawops up --openclaw-version <version>`. clawops does not fall back to `latest` or `stable` here: both now point at OpenClaw 2.0, which this release line does not support."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return ok(image);
|
|
23
|
+
}
|
|
24
|
+
function versionOf(image) {
|
|
25
|
+
return image.split(":")[1] ?? "unknown";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/openclaw/runtime.ts
|
|
29
|
+
var STATE_DIR_CONTAINER = "/home/node/.openclaw";
|
|
30
|
+
var STATE_DIR_HOST_LINUX = "/var/lib/clawops/openclaw";
|
|
31
|
+
var STATE_DIR_HOST_MACOS = "${HOME}/.clawops/openclaw";
|
|
32
|
+
var CONFIG_FILENAME = "openclaw.json";
|
|
33
|
+
var CONTAINER_UID = 1e3;
|
|
34
|
+
var CONFIG_MOUNT_PATH2 = `${STATE_DIR_CONTAINER}/${CONFIG_FILENAME}`;
|
|
35
|
+
function stateDirForOS(os) {
|
|
36
|
+
return os === "Darwin" ? STATE_DIR_HOST_MACOS : STATE_DIR_HOST_LINUX;
|
|
37
|
+
}
|
|
38
|
+
function configPathForOS(os) {
|
|
39
|
+
return `${stateDirForOS(os)}/${CONFIG_FILENAME}`;
|
|
40
|
+
}
|
|
41
|
+
var ENV_FILE_PATH = "/home/clawops/openclaw.env";
|
|
42
|
+
var SECURITY_FLAGS = [
|
|
43
|
+
"--cap-drop=ALL",
|
|
44
|
+
"--security-opt no-new-privileges",
|
|
45
|
+
"--init",
|
|
46
|
+
"--pids-limit 512"
|
|
47
|
+
].join(" ");
|
|
48
|
+
var SUPERVISOR_ENV = "-e OPENCLAW_SUPERVISOR_MODE=external";
|
|
49
|
+
var ADD_HOST_FLAG2 = "--add-host=host.docker.internal:host-gateway";
|
|
50
|
+
function publishFlag(port, scope) {
|
|
51
|
+
return scope === "loopback" ? `-p 127.0.0.1:${port}:${port}` : `-p ${port}:${port}`;
|
|
52
|
+
}
|
|
53
|
+
function gatewayRunArgs(spec) {
|
|
54
|
+
const {
|
|
55
|
+
image,
|
|
56
|
+
stateDir,
|
|
57
|
+
port = GATEWAY_PORT,
|
|
58
|
+
envFilePath = ENV_FILE_PATH,
|
|
59
|
+
supervisor = "docker",
|
|
60
|
+
publish = "loopback",
|
|
61
|
+
limits
|
|
62
|
+
} = spec;
|
|
63
|
+
const envFileArg = `$([ -s ${envFilePath} ] && echo --env-file ${envFilePath})`;
|
|
64
|
+
const lifecycle = supervisor === "systemd" ? "--rm" : "-d --restart unless-stopped";
|
|
65
|
+
const capacity = [
|
|
66
|
+
limits?.memory ? `--memory ${limits.memory}` : "",
|
|
67
|
+
limits?.cpus ? `--cpus ${limits.cpus}` : ""
|
|
68
|
+
].filter(Boolean).join(" ");
|
|
69
|
+
return [
|
|
70
|
+
`docker run ${lifecycle} --name openclaw`,
|
|
71
|
+
publishFlag(port, publish),
|
|
72
|
+
SECURITY_FLAGS,
|
|
73
|
+
capacity,
|
|
74
|
+
// No OPENCLAW_CONFIG_PATH: STATE_DIR_CONTAINER is OpenClaw's own default, so mounting
|
|
75
|
+
// it there *is* the configuration. One fewer thing to keep in sync.
|
|
76
|
+
ADD_HOST_FLAG2,
|
|
77
|
+
SUPERVISOR_ENV,
|
|
78
|
+
envFileArg,
|
|
79
|
+
// The directory, writable. Never the file, and never :ro — see GatewayRunSpec.stateDir.
|
|
80
|
+
`-v ${stateDir}:${STATE_DIR_CONTAINER}`,
|
|
81
|
+
spec.extraArgs?.trim() ?? "",
|
|
82
|
+
image,
|
|
83
|
+
// No --allow-unconfigured. That flag bypasses a check upstream describes as detecting
|
|
84
|
+
// "suspicious or clobbered config", and clawops passed it permanently — meaning a
|
|
85
|
+
// clobbered config would have started silently on defaults instead of failing.
|
|
86
|
+
// Provisioning writes `gateway.mode: "local"`, which is what the check actually wants.
|
|
87
|
+
// Measured on 2026.9.2: with the mode present the gateway starts without the flag; a
|
|
88
|
+
// config lacking it exits 78, which is the signal we want rather than one we suppress.
|
|
89
|
+
`node openclaw.mjs gateway run --port ${port}`
|
|
90
|
+
].filter(Boolean).join(" ");
|
|
91
|
+
}
|
|
92
|
+
function gatewayRunCommand(spec) {
|
|
93
|
+
const { pathPrefix = "" } = spec;
|
|
94
|
+
return pathPrefix + [
|
|
95
|
+
"docker stop openclaw 2>/dev/null || true",
|
|
96
|
+
"docker rm openclaw 2>/dev/null || true",
|
|
97
|
+
gatewayRunArgs(spec)
|
|
98
|
+
].join(" && ");
|
|
99
|
+
}
|
|
100
|
+
var PUBLISH_INSPECT_CMD = `docker inspect openclaw --format '{{json .HostConfig.PortBindings}}'`;
|
|
101
|
+
function publishedGatewayPort(inspectStdout) {
|
|
102
|
+
try {
|
|
103
|
+
const bindings = JSON.parse(inspectStdout.trim());
|
|
104
|
+
if (!bindings) return void 0;
|
|
105
|
+
for (const hostPorts of Object.values(bindings)) {
|
|
106
|
+
for (const binding of hostPorts ?? []) {
|
|
107
|
+
const port = Number(binding.HostPort);
|
|
108
|
+
if (Number.isInteger(port) && port > 0 && port < 65536) return port;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
function publishForRestart(inspectStdout) {
|
|
116
|
+
try {
|
|
117
|
+
const bindings = JSON.parse(inspectStdout.trim());
|
|
118
|
+
if (!bindings) return "loopback";
|
|
119
|
+
for (const hostPorts of Object.values(bindings)) {
|
|
120
|
+
for (const binding of hostPorts ?? []) {
|
|
121
|
+
const ip = binding.HostIp ?? "";
|
|
122
|
+
if (ip === "" || ip === "0.0.0.0" || ip === "::") return "all";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
return "loopback";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
GATEWAY_PORT,
|
|
132
|
+
IMAGE_INSPECT_CMD,
|
|
133
|
+
imageForRestart,
|
|
134
|
+
versionOf,
|
|
135
|
+
STATE_DIR_CONTAINER,
|
|
136
|
+
STATE_DIR_HOST_LINUX,
|
|
137
|
+
STATE_DIR_HOST_MACOS,
|
|
138
|
+
CONFIG_FILENAME,
|
|
139
|
+
CONTAINER_UID,
|
|
140
|
+
CONFIG_MOUNT_PATH2 as CONFIG_MOUNT_PATH,
|
|
141
|
+
stateDirForOS,
|
|
142
|
+
configPathForOS,
|
|
143
|
+
ENV_FILE_PATH,
|
|
144
|
+
SECURITY_FLAGS,
|
|
145
|
+
SUPERVISOR_ENV,
|
|
146
|
+
ADD_HOST_FLAG2 as ADD_HOST_FLAG,
|
|
147
|
+
gatewayRunArgs,
|
|
148
|
+
gatewayRunCommand,
|
|
149
|
+
PUBLISH_INSPECT_CMD,
|
|
150
|
+
publishedGatewayPort,
|
|
151
|
+
publishForRestart
|
|
152
|
+
};
|
|
@@ -140,6 +140,47 @@ var Ssh2Session = class {
|
|
|
140
140
|
});
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
|
+
execWithInput(command, input, signal) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
if (signal?.aborted) {
|
|
146
|
+
reject(new NetworkError("Operation aborted"));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.client.exec(command, (err, channel) => {
|
|
150
|
+
if (err) {
|
|
151
|
+
reject(new NetworkError(`SSH exec failed: ${err.message}`));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let stdout = "";
|
|
155
|
+
let stderr = "";
|
|
156
|
+
const onAbort = () => {
|
|
157
|
+
channel.destroy();
|
|
158
|
+
reject(new NetworkError("Operation aborted"));
|
|
159
|
+
};
|
|
160
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
161
|
+
channel.on("data", (data) => {
|
|
162
|
+
stdout += data.toString("utf-8");
|
|
163
|
+
});
|
|
164
|
+
channel.stderr.on("data", (data) => {
|
|
165
|
+
stderr += data.toString("utf-8");
|
|
166
|
+
});
|
|
167
|
+
channel.on("close", (code) => {
|
|
168
|
+
signal?.removeEventListener("abort", onAbort);
|
|
169
|
+
resolve({ stdout, stderr, code: code ?? 0 });
|
|
170
|
+
});
|
|
171
|
+
channel.on("error", (chanErr) => {
|
|
172
|
+
signal?.removeEventListener("abort", onAbort);
|
|
173
|
+
reject(new NetworkError(`SSH channel error: ${chanErr.message}`));
|
|
174
|
+
});
|
|
175
|
+
input.on("error", (readErr) => {
|
|
176
|
+
channel.destroy();
|
|
177
|
+
signal?.removeEventListener("abort", onAbort);
|
|
178
|
+
reject(new NetworkError(`Failed reading input: ${readErr.message}`));
|
|
179
|
+
});
|
|
180
|
+
input.pipe(channel);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
143
184
|
stream(command, signal) {
|
|
144
185
|
return new Promise((resolve, reject) => {
|
|
145
186
|
if (signal?.aborted) {
|