@clawops/cli 1.7.9 → 2.0.1
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 +504 -76
- package/dist/apply-KYFPWI5U.js +27 -0
- package/dist/automation-RMAIBNVC.js +44 -0
- package/dist/bedrock-XVASJX2K.js +61 -0
- package/dist/{bootstrap-HL4DNUNG.js → bootstrap-HNGKY6WW.js} +31 -19
- package/dist/bootstrap.sh.tmpl +51 -31
- package/dist/channels-KBUR42IU.js +53 -0
- package/dist/chunk-2I7KCMXZ.js +142 -0
- package/dist/{chunk-DQDAWNGB.js → chunk-2Z7J3ERQ.js} +1 -1
- package/dist/chunk-3OLUJPCB.js +36 -0
- package/dist/chunk-5XT5IAJK.js +69 -0
- package/dist/chunk-AEEMRFZV.js +24 -0
- package/dist/chunk-BSLIYB5Q.js +106 -0
- package/dist/{chunk-3XK66DKK.js → chunk-E4NJSJ54.js} +57 -93
- package/dist/chunk-FEK7VJAY.js +16 -0
- package/dist/{chunk-UKTP332Z.js → chunk-GAZPPSKU.js} +6 -7
- package/dist/chunk-GIEVAGEK.js +126 -0
- package/dist/chunk-HSPHXG6W.js +67 -0
- package/dist/chunk-IVWP4QUJ.js +901 -0
- package/dist/{chunk-PVUVI35Y.js → chunk-JW5A7E3X.js} +5 -1
- package/dist/{chunk-GZN7PKP4.js → chunk-KC3GC3PE.js} +328 -121
- package/dist/chunk-LBE6COC4.js +244 -0
- package/dist/chunk-LEVIW2PV.js +890 -0
- package/dist/chunk-LT4XMO5D.js +50 -0
- package/dist/chunk-MC5ATCXS.js +108 -0
- package/dist/chunk-N4WZLWJ4.js +16 -0
- package/dist/chunk-NUDIHZEH.js +55 -0
- package/dist/chunk-NUL5F6YV.js +56 -0
- package/dist/chunk-RHOWYIWT.js +52 -0
- package/dist/chunk-STLRHLLV.js +193 -0
- package/dist/chunk-V25QRZRB.js +45 -0
- package/dist/chunk-VOEJYRIK.js +33 -0
- package/dist/{providers-2OABPW2E.js → chunk-VUFMZEIM.js} +3 -2
- package/dist/chunk-W3OLNNJW.js +51 -0
- package/dist/cli-WLD3EBEM.js +11 -0
- package/dist/cli-auth-GD5SK7JU.js +13 -0
- package/dist/cli.js +1296 -695
- package/dist/config-validate-K2N6XLDX.js +76 -0
- package/dist/context-7EVC6W45.js +20 -0
- package/dist/docker-3PDO4TS7.js +12 -0
- package/dist/{firewall-XQGO7HWK.js → firewall-5VKZ4WBH.js} +32 -1
- package/dist/generate-I6YQ5V2T.js +29 -0
- package/dist/{harden-URPXLSVF.js → harden-72JMRT4E.js} +51 -11
- package/dist/health-5CCOXYGG.js +11 -0
- package/dist/index.d.ts +56 -0
- package/dist/known-hosts-file-I3Q3NBYQ.js +22 -0
- package/dist/logs-5A7YYA53.js +15 -0
- package/dist/{mcp-wire-7E5JQY47.js → mcp-wire-3SWBM7D6.js} +32 -16
- package/dist/models-AGTQWYDL.js +32 -0
- package/dist/network-args-PYJRJBYO.js +64 -0
- package/dist/{package-5AR4ZPK4.js → package-5BCMEBTE.js} +11 -4
- package/dist/passphrase-MJBE45N7.js +15 -0
- package/dist/plugins-DTST3OER.js +49 -0
- package/dist/{pool-QGFQRMGU.js → pool-PIIKJ4L2.js} +3 -2
- package/dist/preflight-EF52S3WW.js +162 -0
- package/dist/preflight-MWWM5JT3.js +181 -0
- package/dist/preflight-TFD4N5ZU.js +141 -0
- package/dist/privileged-JFA6LLTV.js +17 -0
- package/dist/providers-FW6H6OPF.js +11 -0
- package/dist/ready-KDNRBNSU.js +95 -0
- package/dist/{remote-config-QDLN4UMG.js → remote-config-3HZLUWEA.js} +4 -3
- package/dist/runtime-X3KW5TCP.js +44 -0
- package/dist/server-GBA7BJGK.js +46 -0
- package/dist/spec-path-KQOOIQCA.js +9 -0
- package/dist/ssh-VAI5DU5M.js +11 -0
- package/dist/ssh-key-DUO6OKOI.js +7 -0
- package/dist/stack-config-R4SPSUQZ.js +12 -0
- package/dist/upgrade-ULD4KFYC.js +88 -0
- package/dist/validate-YVI3BJ55.js +14 -0
- package/dist/{version-guard-VUCNZXQS.js → version-guard-XVYW5WFJ.js} +2 -2
- package/dist/{versions-2UVAT6PQ.js → versions-W5LRQCAJ.js} +2 -2
- package/dist/wait-WO72TRRQ.js +79 -0
- package/package.json +11 -4
- package/spec/deploy-plan.schema.json +114 -25
- package/spec/integrations.yaml +135 -28
- package/spec/mcp-tools.yaml +89 -37
- package/spec/models.yaml +23 -3
- package/spec/openclaw-2.0.config.schema.json +1 -0
- package/spec/openclaw-versions.yaml +116 -27
- package/spec/providers.schema.json +163 -28
- package/dist/apply-F3KY2HTB.js +0 -17
- package/dist/automation-RULUSJBW.js +0 -30
- package/dist/aws-L4RR2LSE.js +0 -254
- package/dist/azure-7OXQENQL.js +0 -262
- package/dist/chunk-35WVQY3X.js +0 -122
- package/dist/chunk-6X45DDGH.js +0 -42
- package/dist/chunk-A4QR4TXW.js +0 -120
- package/dist/chunk-C5X375RO.js +0 -85
- package/dist/chunk-FNS56UQY.js +0 -741
- package/dist/chunk-P5LKWRPL.js +0 -114
- package/dist/chunk-SEZMVVAS.js +0 -156
- package/dist/context-5LCUG2QR.js +0 -10
- package/dist/gcp-UG6AWPIZ.js +0 -193
- package/dist/generate-OAGYT3VH.js +0 -15
- package/dist/local-DXBEVZ5C.js +0 -47
- package/dist/server-FIZCOLHL.js +0 -32
- package/dist/ssh-CNJNLM3E.js +0 -8
- package/dist/validate-CFQ4FH2T.js +0 -10
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
readLocalState
|
|
4
|
+
} from "./chunk-A2I76FTA.js";
|
|
5
|
+
import {
|
|
6
|
+
getConfigDir,
|
|
7
|
+
requireConfig
|
|
8
|
+
} from "./chunk-CX5SL5HP.js";
|
|
9
|
+
import {
|
|
10
|
+
UsageError
|
|
11
|
+
} from "./chunk-KGXPLI7W.js";
|
|
12
|
+
import {
|
|
13
|
+
getProvider
|
|
14
|
+
} from "./chunk-VUFMZEIM.js";
|
|
15
|
+
|
|
16
|
+
// src/cli/context.ts
|
|
17
|
+
function buildContext(args) {
|
|
18
|
+
const config = requireConfig();
|
|
19
|
+
const stackName = typeof args.stack === "string" ? args.stack : config.defaults.stack;
|
|
20
|
+
const providerName = typeof args.provider === "string" ? args.provider : config.stacks[stackName]?.provider ?? config.defaults.provider;
|
|
21
|
+
const adapter = loadProvider(providerName);
|
|
22
|
+
const localState = providerName === "local" ? readLocalState(stackName) : void 0;
|
|
23
|
+
let stackCache = null;
|
|
24
|
+
return {
|
|
25
|
+
config,
|
|
26
|
+
adapter,
|
|
27
|
+
stackName,
|
|
28
|
+
localState,
|
|
29
|
+
async getStack() {
|
|
30
|
+
if (providerName === "local") {
|
|
31
|
+
throw new UsageError(
|
|
32
|
+
"The local provider does not use Pulumi stacks. Use `clawops up` to bootstrap the host instead."
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (stackCache) return stackCache;
|
|
36
|
+
const stackConfig = config.stacks[stackName];
|
|
37
|
+
if (!stackConfig) {
|
|
38
|
+
throw new UsageError(
|
|
39
|
+
`Stack "${stackName}" not found in config. Run \`clawops init\` or use \`--stack\` to specify a different stack.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const { getOrCreateStack } = await import("./automation-RMAIBNVC.js");
|
|
43
|
+
stackCache = await getOrCreateStack({
|
|
44
|
+
stack: stackName,
|
|
45
|
+
stateUrl: stackConfig.stateUrl,
|
|
46
|
+
program: adapter.program,
|
|
47
|
+
configDir: getConfigDir()
|
|
48
|
+
});
|
|
49
|
+
return stackCache;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function loadProvider(name) {
|
|
54
|
+
try {
|
|
55
|
+
return getProvider(name);
|
|
56
|
+
} catch {
|
|
57
|
+
throw new UsageError(
|
|
58
|
+
`Provider "${name}" is not yet supported. Supported providers: gcp, aws, azure, local`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function loadAdapterModule(name) {
|
|
63
|
+
return loadProvider(name);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
buildContext,
|
|
68
|
+
loadAdapterModule
|
|
69
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/plan/ssh-key.ts
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
|
+
import ssh2 from "ssh2";
|
|
6
|
+
function resolvePublicKey(privateKeyPath) {
|
|
7
|
+
const pub = `${privateKeyPath}.pub`;
|
|
8
|
+
if (existsSync(pub)) {
|
|
9
|
+
const contents = readFileSync(pub, "utf-8").trim();
|
|
10
|
+
if (contents !== "") return contents;
|
|
11
|
+
}
|
|
12
|
+
if (!existsSync(privateKeyPath)) return void 0;
|
|
13
|
+
try {
|
|
14
|
+
const parsed = ssh2.utils.parseKey(readFileSync(privateKeyPath));
|
|
15
|
+
if (parsed instanceof Error || !("getPublicSSH" in parsed)) return void 0;
|
|
16
|
+
return `${parsed.type} ${parsed.getPublicSSH().toString("base64")} clawops`;
|
|
17
|
+
} catch {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
resolvePublicKey
|
|
24
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolveSpecDir
|
|
4
|
+
} from "./chunk-JW5A7E3X.js";
|
|
5
|
+
|
|
6
|
+
// src/plan/validate.ts
|
|
7
|
+
import Ajv from "ajv/dist/2020";
|
|
8
|
+
import addFormats from "ajv-formats";
|
|
9
|
+
import { readFileSync } from "fs";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
var _validate;
|
|
12
|
+
function getValidator() {
|
|
13
|
+
if (_validate) return _validate;
|
|
14
|
+
const ajv = new Ajv({ strict: false });
|
|
15
|
+
addFormats(ajv);
|
|
16
|
+
const schema = JSON.parse(
|
|
17
|
+
readFileSync(join(resolveSpecDir(), "deploy-plan.schema.json"), "utf-8")
|
|
18
|
+
);
|
|
19
|
+
_validate = ajv.compile(schema);
|
|
20
|
+
return _validate;
|
|
21
|
+
}
|
|
22
|
+
function validatePlan(plan) {
|
|
23
|
+
const validate = getValidator();
|
|
24
|
+
const ok = validate(plan);
|
|
25
|
+
if (ok) return { ok: true, errors: [] };
|
|
26
|
+
const errors = (validate.errors ?? []).map(
|
|
27
|
+
(e) => `${e.instancePath} ${e.message ?? ""}`.trim()
|
|
28
|
+
);
|
|
29
|
+
return { ok: false, errors };
|
|
30
|
+
}
|
|
31
|
+
function assertValidPlan(plan) {
|
|
32
|
+
const result = validatePlan(plan);
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
throw new Error(`Invalid deploy plan:
|
|
35
|
+
${result.errors.join("\n")}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function validatePlanConfig(plan) {
|
|
39
|
+
const overlay = plan.spec?.openclaw?.config;
|
|
40
|
+
if (overlay === void 0 || overlay === null) return { ok: true, errors: [], warnings: [] };
|
|
41
|
+
const [{ validateConfig }, yaml] = await Promise.all([
|
|
42
|
+
import("./config-validate-K2N6XLDX.js"),
|
|
43
|
+
import("./js-yaml-PTEEG4FO.js")
|
|
44
|
+
]);
|
|
45
|
+
const { loadVersionSpec } = await import("./versions-W5LRQCAJ.js");
|
|
46
|
+
const spec = loadVersionSpec(yaml);
|
|
47
|
+
const { deepMerge } = await import("./remote-config-3HZLUWEA.js");
|
|
48
|
+
const merged = deepMerge(
|
|
49
|
+
{ gateway: { mode: "local" } },
|
|
50
|
+
overlay
|
|
51
|
+
);
|
|
52
|
+
const { errors, warnings } = await validateConfig(merged, {
|
|
53
|
+
openclawVersion: plan.spec?.openclaw?.version,
|
|
54
|
+
schemaCapturedFrom: spec.runtime?.configSchemaCapturedFrom
|
|
55
|
+
});
|
|
56
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
57
|
+
}
|
|
58
|
+
function validatePlanNetwork(plan) {
|
|
59
|
+
const net = plan.spec?.network;
|
|
60
|
+
if (!net) return { ok: true, errors: [], warnings: [] };
|
|
61
|
+
const errors = [];
|
|
62
|
+
const warnings = [];
|
|
63
|
+
const publish = net.publishGateway ?? "loopback";
|
|
64
|
+
const gatewayCidrs = net.allowedGatewayCidrs ?? [];
|
|
65
|
+
if (publish === "loopback" && gatewayCidrs.length > 0) {
|
|
66
|
+
errors.push(
|
|
67
|
+
`network.allowedGatewayCidrs lists ${gatewayCidrs.length} CIDR(s) but network.publishGateway is "loopback", so the gateway binds 127.0.0.1 on the host and nothing is listening on a routable interface. Those rules would admit traffic to a closed port. Either drop them and reach the gateway with \`clawops tunnel\`, or set publishGateway to "all" \u2014 which serves plaintext HTTP, so put TLS in front of it.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if ((net.allowedSshCidrs ?? []).length === 0) {
|
|
71
|
+
warnings.push(
|
|
72
|
+
"network.allowedSshCidrs is empty, so this deployment will accept no SSH connections at all \u2014 `clawops ssh`, `logs`, `tunnel` and `harden` will not be able to reach it. Pass `--ssh-cidr auto` (this machine) or an explicit CIDR when generating the plan."
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
for (const [field, cidrs] of [
|
|
76
|
+
["allowedSshCidrs", net.allowedSshCidrs ?? []],
|
|
77
|
+
["allowedGatewayCidrs", gatewayCidrs]
|
|
78
|
+
]) {
|
|
79
|
+
if (cidrs.some((c) => c === "0.0.0.0/0" || c === "::/0")) {
|
|
80
|
+
warnings.push(
|
|
81
|
+
`network.${field} admits the whole internet. clawops will create the rule you asked for, but \`clawops harden\` reports it as a finding.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const configured = gatewayPortFromConfig(plan.spec?.openclaw?.config);
|
|
86
|
+
if (net.gatewayPort !== void 0 && configured !== void 0 && configured !== net.gatewayPort) {
|
|
87
|
+
errors.push(
|
|
88
|
+
`network.gatewayPort is ${net.gatewayPort} but the config overlay sets gateway.port to ${configured}. The container would publish one port while the gateway listened on the other. Set one of them, not both.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
92
|
+
}
|
|
93
|
+
function gatewayPortFromConfig(config) {
|
|
94
|
+
if (config === null || typeof config !== "object") return void 0;
|
|
95
|
+
const gateway = config["gateway"];
|
|
96
|
+
if (gateway === null || typeof gateway !== "object") return void 0;
|
|
97
|
+
const port = gateway["port"];
|
|
98
|
+
return typeof port === "number" ? port : void 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
validatePlan,
|
|
103
|
+
assertValidPlan,
|
|
104
|
+
validatePlanConfig,
|
|
105
|
+
validatePlanNetwork
|
|
106
|
+
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
formatKnownHostsLine,
|
|
4
|
+
keyTypeFromBlob,
|
|
5
|
+
verifyAgainstKnownHosts
|
|
6
|
+
} from "./chunk-MC5ATCXS.js";
|
|
2
7
|
import {
|
|
3
8
|
NetworkError
|
|
4
9
|
} from "./chunk-KGXPLI7W.js";
|
|
@@ -8,98 +13,6 @@ import { readFileSync, appendFileSync, mkdirSync } from "fs";
|
|
|
8
13
|
import { createServer } from "net";
|
|
9
14
|
import path from "path";
|
|
10
15
|
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
|
|
103
16
|
var Ssh2Session = class {
|
|
104
17
|
constructor(client) {
|
|
105
18
|
this.client = client;
|
|
@@ -140,6 +53,47 @@ var Ssh2Session = class {
|
|
|
140
53
|
});
|
|
141
54
|
});
|
|
142
55
|
}
|
|
56
|
+
execWithInput(command, input, signal) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
if (signal?.aborted) {
|
|
59
|
+
reject(new NetworkError("Operation aborted"));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.client.exec(command, (err, channel) => {
|
|
63
|
+
if (err) {
|
|
64
|
+
reject(new NetworkError(`SSH exec failed: ${err.message}`));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let stdout = "";
|
|
68
|
+
let stderr = "";
|
|
69
|
+
const onAbort = () => {
|
|
70
|
+
channel.destroy();
|
|
71
|
+
reject(new NetworkError("Operation aborted"));
|
|
72
|
+
};
|
|
73
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
74
|
+
channel.on("data", (data) => {
|
|
75
|
+
stdout += data.toString("utf-8");
|
|
76
|
+
});
|
|
77
|
+
channel.stderr.on("data", (data) => {
|
|
78
|
+
stderr += data.toString("utf-8");
|
|
79
|
+
});
|
|
80
|
+
channel.on("close", (code) => {
|
|
81
|
+
signal?.removeEventListener("abort", onAbort);
|
|
82
|
+
resolve({ stdout, stderr, code: code ?? 0 });
|
|
83
|
+
});
|
|
84
|
+
channel.on("error", (chanErr) => {
|
|
85
|
+
signal?.removeEventListener("abort", onAbort);
|
|
86
|
+
reject(new NetworkError(`SSH channel error: ${chanErr.message}`));
|
|
87
|
+
});
|
|
88
|
+
input.on("error", (readErr) => {
|
|
89
|
+
channel.destroy();
|
|
90
|
+
signal?.removeEventListener("abort", onAbort);
|
|
91
|
+
reject(new NetworkError(`Failed reading input: ${readErr.message}`));
|
|
92
|
+
});
|
|
93
|
+
input.pipe(channel);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
143
97
|
stream(command, signal) {
|
|
144
98
|
return new Promise((resolve, reject) => {
|
|
145
99
|
if (signal?.aborted) {
|
|
@@ -199,6 +153,15 @@ var Ssh2Session = class {
|
|
|
199
153
|
this.client.end();
|
|
200
154
|
}
|
|
201
155
|
};
|
|
156
|
+
function describeConnectError(message, opts) {
|
|
157
|
+
if (!/host.*(denied|verification)/i.test(message)) {
|
|
158
|
+
return `SSH connection failed: ${message}`;
|
|
159
|
+
}
|
|
160
|
+
const entry = opts.port === 22 ? opts.host : `[${opts.host}]:${opts.port}`;
|
|
161
|
+
return `the host key for ${opts.host}:${opts.port} does not match the one recorded in ${opts.knownHostsPath}. If your cloud reassigned this address \u2014 the usual cause \u2014 drop the stale entry and retry:
|
|
162
|
+
ssh-keygen -R ${entry} -f ${opts.knownHostsPath}
|
|
163
|
+
If you did not expect this address to change hands, do not connect.`;
|
|
164
|
+
}
|
|
202
165
|
async function connect(opts) {
|
|
203
166
|
let privateKey;
|
|
204
167
|
try {
|
|
@@ -225,7 +188,7 @@ async function connect(opts) {
|
|
|
225
188
|
});
|
|
226
189
|
client.on("error", (err) => {
|
|
227
190
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
228
|
-
reject(new NetworkError(
|
|
191
|
+
reject(new NetworkError(describeConnectError(err.message, opts)));
|
|
229
192
|
});
|
|
230
193
|
const config = {
|
|
231
194
|
host: opts.host,
|
|
@@ -258,5 +221,6 @@ function verifyHostKey(host, port, keyHash, knownHostsPath) {
|
|
|
258
221
|
}
|
|
259
222
|
|
|
260
223
|
export {
|
|
224
|
+
describeConnectError,
|
|
261
225
|
connect
|
|
262
226
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/providers/azure/sizes.ts
|
|
4
|
+
var INSTANCE_TYPE_MAP = {
|
|
5
|
+
micro: "Standard_B1s",
|
|
6
|
+
small: "Standard_B2s",
|
|
7
|
+
medium: "Standard_B4ms",
|
|
8
|
+
large: "Standard_B8ms",
|
|
9
|
+
gpu: "Standard_NC6s_v3"
|
|
10
|
+
};
|
|
11
|
+
var DEFAULT_ALIAS = "small";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
INSTANCE_TYPE_MAP,
|
|
15
|
+
DEFAULT_ALIAS
|
|
16
|
+
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolveSpecDir
|
|
4
|
+
} from "./chunk-JW5A7E3X.js";
|
|
2
5
|
import {
|
|
3
6
|
err,
|
|
4
7
|
ok
|
|
5
8
|
} from "./chunk-EI7CQQ7D.js";
|
|
6
|
-
import {
|
|
7
|
-
resolveSpecDir
|
|
8
|
-
} from "./chunk-PVUVI35Y.js";
|
|
9
9
|
|
|
10
10
|
// src/openclaw/versions.ts
|
|
11
11
|
import { readFileSync } from "fs";
|
|
@@ -28,7 +28,8 @@ function loadVersionSpec(yaml) {
|
|
|
28
28
|
min: raw.support.min,
|
|
29
29
|
max: raw.support.max ?? "",
|
|
30
30
|
recommended: raw.support.recommended ?? raw.support.min
|
|
31
|
-
}
|
|
31
|
+
},
|
|
32
|
+
...raw.runtime ? { runtime: raw.runtime } : {}
|
|
32
33
|
};
|
|
33
34
|
return _spec;
|
|
34
35
|
} catch (e) {
|
|
@@ -77,9 +78,7 @@ function checkVersion(version, support) {
|
|
|
77
78
|
reason: "too-new",
|
|
78
79
|
supportedRange: describeRange(support),
|
|
79
80
|
message: `OpenClaw ${version} is newer than this clawops release supports (${describeRange(support)}).
|
|
80
|
-
|
|
81
|
-
Use clawops 2.x for OpenClaw 2026.9.1 and later: npm install -g @clawops/cli@latest
|
|
82
|
-
To stay on this line, pin an OpenClaw version at or below ${support.max}.`
|
|
81
|
+
Pin an OpenClaw version at or below ${support.max}, or upgrade clawops.`
|
|
83
82
|
});
|
|
84
83
|
}
|
|
85
84
|
return ok(version);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
GATEWAY_PORT
|
|
4
|
+
} from "./chunk-VOEJYRIK.js";
|
|
5
|
+
|
|
6
|
+
// src/openclaw/runtime.ts
|
|
7
|
+
var STATE_DIR_CONTAINER = "/home/node/.openclaw";
|
|
8
|
+
var STATE_DIR_HOST_LINUX = "/var/lib/clawops/openclaw";
|
|
9
|
+
var STATE_DIR_HOST_MACOS = "${HOME}/.clawops/openclaw";
|
|
10
|
+
var CONFIG_FILENAME = "openclaw.json";
|
|
11
|
+
var CONTAINER_UID = 1e3;
|
|
12
|
+
var CONFIG_MOUNT_PATH = `${STATE_DIR_CONTAINER}/${CONFIG_FILENAME}`;
|
|
13
|
+
function stateDirForOS(os) {
|
|
14
|
+
return os === "Darwin" ? STATE_DIR_HOST_MACOS : STATE_DIR_HOST_LINUX;
|
|
15
|
+
}
|
|
16
|
+
function configPathForOS(os) {
|
|
17
|
+
return `${stateDirForOS(os)}/${CONFIG_FILENAME}`;
|
|
18
|
+
}
|
|
19
|
+
var ENV_FILE_PATH = "/home/clawops/openclaw.env";
|
|
20
|
+
var SECURITY_FLAGS = [
|
|
21
|
+
"--cap-drop=ALL",
|
|
22
|
+
"--security-opt no-new-privileges",
|
|
23
|
+
"--init",
|
|
24
|
+
"--pids-limit 512"
|
|
25
|
+
].join(" ");
|
|
26
|
+
var SUPERVISOR_ENV = "-e OPENCLAW_SUPERVISOR_MODE=external";
|
|
27
|
+
var ADD_HOST_FLAG = "--add-host=host.docker.internal:host-gateway";
|
|
28
|
+
function publishFlag(port, scope) {
|
|
29
|
+
return scope === "loopback" ? `-p 127.0.0.1:${port}:${port}` : `-p ${port}:${port}`;
|
|
30
|
+
}
|
|
31
|
+
function gatewayRunArgs(spec) {
|
|
32
|
+
const {
|
|
33
|
+
image,
|
|
34
|
+
stateDir,
|
|
35
|
+
port = GATEWAY_PORT,
|
|
36
|
+
envFilePath = ENV_FILE_PATH,
|
|
37
|
+
supervisor = "docker",
|
|
38
|
+
publish = "loopback",
|
|
39
|
+
limits
|
|
40
|
+
} = spec;
|
|
41
|
+
const envFileArg = `$([ -s ${envFilePath} ] && echo --env-file ${envFilePath})`;
|
|
42
|
+
const lifecycle = supervisor === "systemd" ? "--rm" : "-d --restart unless-stopped";
|
|
43
|
+
const capacity = [
|
|
44
|
+
limits?.memory ? `--memory ${limits.memory}` : "",
|
|
45
|
+
limits?.cpus ? `--cpus ${limits.cpus}` : ""
|
|
46
|
+
].filter(Boolean).join(" ");
|
|
47
|
+
return [
|
|
48
|
+
`docker run ${lifecycle} --name openclaw`,
|
|
49
|
+
publishFlag(port, publish),
|
|
50
|
+
SECURITY_FLAGS,
|
|
51
|
+
capacity,
|
|
52
|
+
// No OPENCLAW_CONFIG_PATH: STATE_DIR_CONTAINER is OpenClaw's own default, so mounting
|
|
53
|
+
// it there *is* the configuration. One fewer thing to keep in sync.
|
|
54
|
+
ADD_HOST_FLAG,
|
|
55
|
+
SUPERVISOR_ENV,
|
|
56
|
+
envFileArg,
|
|
57
|
+
// The directory, writable. Never the file, and never :ro — see GatewayRunSpec.stateDir.
|
|
58
|
+
`-v ${stateDir}:${STATE_DIR_CONTAINER}`,
|
|
59
|
+
spec.extraArgs?.trim() ?? "",
|
|
60
|
+
image,
|
|
61
|
+
// No --allow-unconfigured. That flag bypasses a check upstream describes as detecting
|
|
62
|
+
// "suspicious or clobbered config", and clawops passed it permanently — meaning a
|
|
63
|
+
// clobbered config would have started silently on defaults instead of failing.
|
|
64
|
+
// Provisioning writes `gateway.mode: "local"`, which is what the check actually wants.
|
|
65
|
+
// Measured on 2026.9.2: with the mode present the gateway starts without the flag; a
|
|
66
|
+
// config lacking it exits 78, which is the signal we want rather than one we suppress.
|
|
67
|
+
`node openclaw.mjs gateway run --port ${port}`
|
|
68
|
+
].filter(Boolean).join(" ");
|
|
69
|
+
}
|
|
70
|
+
function gatewayRunCommand(spec) {
|
|
71
|
+
const { pathPrefix = "" } = spec;
|
|
72
|
+
return pathPrefix + [
|
|
73
|
+
"docker stop openclaw 2>/dev/null || true",
|
|
74
|
+
"docker rm openclaw 2>/dev/null || true",
|
|
75
|
+
gatewayRunArgs(spec)
|
|
76
|
+
].join(" && ");
|
|
77
|
+
}
|
|
78
|
+
var PUBLISH_INSPECT_CMD = `docker inspect openclaw --format '{{json .HostConfig.PortBindings}}'`;
|
|
79
|
+
function publishedGatewayPort(inspectStdout) {
|
|
80
|
+
try {
|
|
81
|
+
const bindings = JSON.parse(inspectStdout.trim());
|
|
82
|
+
if (!bindings) return void 0;
|
|
83
|
+
for (const hostPorts of Object.values(bindings)) {
|
|
84
|
+
for (const binding of hostPorts ?? []) {
|
|
85
|
+
const port = Number(binding.HostPort);
|
|
86
|
+
if (Number.isInteger(port) && port > 0 && port < 65536) return port;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
function publishForRestart(inspectStdout) {
|
|
94
|
+
try {
|
|
95
|
+
const bindings = JSON.parse(inspectStdout.trim());
|
|
96
|
+
if (!bindings) return "loopback";
|
|
97
|
+
for (const hostPorts of Object.values(bindings)) {
|
|
98
|
+
for (const binding of hostPorts ?? []) {
|
|
99
|
+
const ip = binding.HostIp ?? "";
|
|
100
|
+
if (ip === "" || ip === "0.0.0.0" || ip === "::") return "all";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
return "loopback";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
STATE_DIR_CONTAINER,
|
|
110
|
+
STATE_DIR_HOST_LINUX,
|
|
111
|
+
STATE_DIR_HOST_MACOS,
|
|
112
|
+
CONFIG_FILENAME,
|
|
113
|
+
CONTAINER_UID,
|
|
114
|
+
CONFIG_MOUNT_PATH,
|
|
115
|
+
stateDirForOS,
|
|
116
|
+
configPathForOS,
|
|
117
|
+
ENV_FILE_PATH,
|
|
118
|
+
SECURITY_FLAGS,
|
|
119
|
+
SUPERVISOR_ENV,
|
|
120
|
+
ADD_HOST_FLAG,
|
|
121
|
+
gatewayRunArgs,
|
|
122
|
+
gatewayRunCommand,
|
|
123
|
+
PUBLISH_INSPECT_CMD,
|
|
124
|
+
publishedGatewayPort,
|
|
125
|
+
publishForRestart
|
|
126
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolvePublicKey
|
|
4
|
+
} from "./chunk-AEEMRFZV.js";
|
|
5
|
+
import {
|
|
6
|
+
GATEWAY_PORT
|
|
7
|
+
} from "./chunk-VOEJYRIK.js";
|
|
8
|
+
import {
|
|
9
|
+
getConfig
|
|
10
|
+
} from "./chunk-CX5SL5HP.js";
|
|
11
|
+
import {
|
|
12
|
+
UsageError
|
|
13
|
+
} from "./chunk-KGXPLI7W.js";
|
|
14
|
+
|
|
15
|
+
// src/plan/stack-config.ts
|
|
16
|
+
import process from "process";
|
|
17
|
+
async function writeStackConfig(stack, plan) {
|
|
18
|
+
const sshPublicKey = plan.spec.ssh?.publicKey ?? fallbackPublicKey();
|
|
19
|
+
if (!sshPublicKey) {
|
|
20
|
+
throw new UsageError(
|
|
21
|
+
"This plan carries no SSH public key, and none could be resolved from `ssh.keyPath` in ~/.clawops/config.json. The instance would have no way to admit anyone. Run `clawops doctor` to see why the key is unusable, then regenerate the plan."
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
await stack.setConfig("sshPublicKey", { value: sshPublicKey });
|
|
25
|
+
await stack.setConfig("instanceType", { value: plan.spec.instanceType });
|
|
26
|
+
if (plan.spec.region) {
|
|
27
|
+
await stack.setConfig("region", { value: plan.spec.region });
|
|
28
|
+
}
|
|
29
|
+
await stack.setConfig("openclawVersion", { value: plan.spec.openclaw.version });
|
|
30
|
+
await stack.setConfig("publishGateway", {
|
|
31
|
+
value: plan.spec.network?.publishGateway ?? "loopback"
|
|
32
|
+
});
|
|
33
|
+
await stack.setConfig("gatewayPort", {
|
|
34
|
+
value: String(plan.spec.network?.gatewayPort ?? GATEWAY_PORT)
|
|
35
|
+
});
|
|
36
|
+
if (plan.spec.provider === "gcp") {
|
|
37
|
+
const { resolveProjectId } = await import("./preflight-EF52S3WW.js");
|
|
38
|
+
const project = resolveProjectId();
|
|
39
|
+
if (project) await stack.setConfig("gcp:project", { value: project });
|
|
40
|
+
}
|
|
41
|
+
if (plan.spec.provider === "azure") {
|
|
42
|
+
const { resolveSubscriptionId } = await import("./cli-auth-GD5SK7JU.js");
|
|
43
|
+
const subscription = resolveSubscriptionId();
|
|
44
|
+
if (subscription) await stack.setConfig("azure-native:subscriptionId", { value: subscription });
|
|
45
|
+
}
|
|
46
|
+
await stack.setConfig("accessMode", { value: "restricted" });
|
|
47
|
+
await stack.setConfig("sshCidrs", {
|
|
48
|
+
value: (plan.spec.network?.allowedSshCidrs ?? []).join(",")
|
|
49
|
+
});
|
|
50
|
+
await stack.setConfig("gatewayCidrs", {
|
|
51
|
+
value: (plan.spec.network?.allowedGatewayCidrs ?? []).join(",")
|
|
52
|
+
});
|
|
53
|
+
const modelProvider = plan.spec.openclaw.config?.["models"]?.["provider"];
|
|
54
|
+
if (modelProvider === "bedrock") {
|
|
55
|
+
await stack.setConfig("bedrockEnabled", { value: "true" });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function fallbackPublicKey() {
|
|
59
|
+
const config = getConfig();
|
|
60
|
+
if (!config) return void 0;
|
|
61
|
+
const keyPath = config.ssh.keyPath.replace(/^~/, process.env["HOME"] ?? "~");
|
|
62
|
+
return resolvePublicKey(keyPath);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
writeStackConfig
|
|
67
|
+
};
|