@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,28 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "./chunk-
|
|
3
|
+
interpretProbe,
|
|
4
|
+
probeCommand
|
|
5
|
+
} from "./chunk-LT4XMO5D.js";
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
restartGateway
|
|
10
|
-
} from "./chunk-SEZMVVAS.js";
|
|
11
|
-
import {
|
|
12
|
-
buildContext
|
|
13
|
-
} from "./chunk-P5LKWRPL.js";
|
|
7
|
+
execPrivileged
|
|
8
|
+
} from "./chunk-NUDIHZEH.js";
|
|
14
9
|
import {
|
|
15
10
|
chalk,
|
|
16
11
|
failure
|
|
17
12
|
} from "./chunk-Q7NQY5HV.js";
|
|
18
13
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
GATEWAY_PORT,
|
|
15
|
+
PUBLISH_INSPECT_CMD,
|
|
16
|
+
STATE_DIR_HOST_LINUX,
|
|
17
|
+
configPathForOS,
|
|
18
|
+
publishForRestart
|
|
19
|
+
} from "./chunk-6TIYVHPX.js";
|
|
21
20
|
|
|
22
21
|
// src/cli/commands/monitor.ts
|
|
23
22
|
import { defineCommand } from "citty";
|
|
24
23
|
import process from "process";
|
|
25
|
-
var GATEWAY_PORT = 18789;
|
|
26
24
|
function formatUptime(startedAt) {
|
|
27
25
|
if (!startedAt) return "\u2014";
|
|
28
26
|
const ms = Date.now() - new Date(startedAt).getTime();
|
|
@@ -38,27 +36,29 @@ function formatUptime(startedAt) {
|
|
|
38
36
|
async function gatherSnapshot(session, signal, tailLines = 10) {
|
|
39
37
|
const DOCKER = "PATH=/usr/local/bin:/opt/homebrew/bin:$PATH docker";
|
|
40
38
|
const [inspectRaw, statsRaw, healthRaw, configRaw, diskRaw, logsRaw] = await Promise.all([
|
|
41
|
-
|
|
39
|
+
execPrivileged(
|
|
40
|
+
session,
|
|
42
41
|
`${DOCKER} inspect openclaw --format '{{.State.Status}}|{{.Config.Image}}|{{.State.StartedAt}}|{{.RestartCount}}' 2>/dev/null || echo 'not found|||0'`,
|
|
43
42
|
signal
|
|
44
43
|
),
|
|
45
|
-
|
|
44
|
+
execPrivileged(
|
|
45
|
+
session,
|
|
46
46
|
`${DOCKER} stats openclaw --no-stream --format '{{.MemUsage}}|{{.CPUPerc}}' 2>/dev/null || echo '\u2014|\u2014'`,
|
|
47
47
|
signal
|
|
48
48
|
),
|
|
49
|
+
session.exec(probeCommand("live", GATEWAY_PORT), signal),
|
|
49
50
|
session.exec(
|
|
50
|
-
`
|
|
51
|
+
`cat ${configPathForOS("Linux")} 2>/dev/null || echo '{}'`,
|
|
51
52
|
signal
|
|
52
53
|
),
|
|
53
54
|
session.exec(
|
|
54
|
-
|
|
55
|
+
// The state directory, not the service user's home: 2.0 keeps the SQLite database
|
|
56
|
+
// there, so that is the filesystem that fills up.
|
|
57
|
+
`df -h ${STATE_DIR_HOST_LINUX} 2>/dev/null | awk 'NR==2{print $5" used ("$3" of "$2")"}'`,
|
|
55
58
|
signal
|
|
56
59
|
),
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
signal
|
|
60
|
-
),
|
|
61
|
-
session.exec(
|
|
60
|
+
execPrivileged(
|
|
61
|
+
session,
|
|
62
62
|
`${DOCKER} logs openclaw -n ${tailLines} 2>&1 || echo '(no logs)'`,
|
|
63
63
|
signal
|
|
64
64
|
)
|
|
@@ -82,7 +82,9 @@ async function gatherSnapshot(session, signal, tailLines = 10) {
|
|
|
82
82
|
return {
|
|
83
83
|
container: { status, image, startedAt, restartCount, memUsage, cpuPct },
|
|
84
84
|
gateway: {
|
|
85
|
-
|
|
85
|
+
// Judged on the payload: a status code cannot distinguish a healthy gateway from
|
|
86
|
+
// a path that fell through to the Control UI. See src/openclaw/health.ts.
|
|
87
|
+
reachable: interpretProbe("live", healthRaw.stdout).ok,
|
|
86
88
|
version,
|
|
87
89
|
authMode
|
|
88
90
|
},
|
|
@@ -131,7 +133,7 @@ function renderSnapshot(snap, opts) {
|
|
|
131
133
|
return lines.join("\n");
|
|
132
134
|
}
|
|
133
135
|
async function probeEntries() {
|
|
134
|
-
const { buildContext
|
|
136
|
+
const { buildContext } = await import("./context-KHKWJJMC.js");
|
|
135
137
|
const { getConfig } = await import("./store-SDUR52Z5.js");
|
|
136
138
|
const config = getConfig();
|
|
137
139
|
if (!config) return [];
|
|
@@ -144,7 +146,7 @@ async function probeEntries() {
|
|
|
144
146
|
deployed: false
|
|
145
147
|
};
|
|
146
148
|
try {
|
|
147
|
-
const ctx =
|
|
149
|
+
const ctx = buildContext({ stack: name });
|
|
148
150
|
if (ctx.adapter.name === "local") {
|
|
149
151
|
return { ...base, deployed: !!ctx.localState };
|
|
150
152
|
}
|
|
@@ -358,13 +360,13 @@ var monitor_default = defineCommand({
|
|
|
358
360
|
const tailLines = Math.max(1, parseInt(String(args.tail ?? "10"), 10) || 10);
|
|
359
361
|
const isTTY = Boolean(process.stdout.isTTY);
|
|
360
362
|
const noColor = Boolean(args["no-color"]) || !isTTY;
|
|
361
|
-
const { buildContext
|
|
362
|
-
const { acquireSession
|
|
363
|
+
const { buildContext } = await import("./context-KHKWJJMC.js");
|
|
364
|
+
const { acquireSession, drainPool } = await import("./pool-EX4ZTA5P.js");
|
|
363
365
|
const ac = new AbortController();
|
|
364
366
|
process.on("SIGINT", () => ac.abort());
|
|
365
367
|
process.on("SIGTERM", () => ac.abort());
|
|
366
368
|
if (args.stack) {
|
|
367
|
-
const ctx =
|
|
369
|
+
const ctx = buildContext(args);
|
|
368
370
|
let conn;
|
|
369
371
|
if (ctx.adapter.name === "local") {
|
|
370
372
|
if (!ctx.localState) {
|
|
@@ -392,7 +394,7 @@ var monitor_default = defineCommand({
|
|
|
392
394
|
});
|
|
393
395
|
}
|
|
394
396
|
if (!isTTY) {
|
|
395
|
-
const { session: session2, release: release2 } = await
|
|
397
|
+
const { session: session2, release: release2 } = await acquireSession({ ...conn, signal: ac.signal });
|
|
396
398
|
try {
|
|
397
399
|
const snap = await gatherSnapshot(session2, ac.signal, tailLines);
|
|
398
400
|
process.stdout.write(
|
|
@@ -400,11 +402,11 @@ var monitor_default = defineCommand({
|
|
|
400
402
|
);
|
|
401
403
|
} finally {
|
|
402
404
|
release2();
|
|
403
|
-
|
|
405
|
+
drainPool();
|
|
404
406
|
}
|
|
405
407
|
return;
|
|
406
408
|
}
|
|
407
|
-
const { session, release } = await
|
|
409
|
+
const { session, release } = await acquireSession({ ...conn, signal: ac.signal });
|
|
408
410
|
process.stdin.setRawMode(true);
|
|
409
411
|
process.stdin.resume();
|
|
410
412
|
process.stdin.setEncoding("utf8");
|
|
@@ -421,7 +423,7 @@ var monitor_default = defineCommand({
|
|
|
421
423
|
process.stdin.pause();
|
|
422
424
|
process.stdout.write("\x1B[?25h\n");
|
|
423
425
|
release();
|
|
424
|
-
|
|
426
|
+
drainPool();
|
|
425
427
|
}
|
|
426
428
|
return;
|
|
427
429
|
}
|
|
@@ -442,7 +444,7 @@ var monitor_default = defineCommand({
|
|
|
442
444
|
keyHandler = fn;
|
|
443
445
|
}, noColor);
|
|
444
446
|
if (!stackName || ac.signal.aborted) break;
|
|
445
|
-
const ctx =
|
|
447
|
+
const ctx = buildContext({ stack: stackName });
|
|
446
448
|
let conn = null;
|
|
447
449
|
if (ctx.adapter.name === "local") {
|
|
448
450
|
if (!ctx.localState) continue;
|
|
@@ -468,7 +470,7 @@ var monitor_default = defineCommand({
|
|
|
468
470
|
}
|
|
469
471
|
}
|
|
470
472
|
if (!conn) continue;
|
|
471
|
-
const { session, release } = await
|
|
473
|
+
const { session, release } = await acquireSession({ ...conn, signal: ac.signal });
|
|
472
474
|
try {
|
|
473
475
|
const result = await runDashboard(session, stackName, { intervalSec, tailLines, noColor }, ac, (fn) => {
|
|
474
476
|
keyHandler = fn;
|
|
@@ -476,7 +478,7 @@ var monitor_default = defineCommand({
|
|
|
476
478
|
if (result === "quit") break;
|
|
477
479
|
} finally {
|
|
478
480
|
release();
|
|
479
|
-
|
|
481
|
+
drainPool();
|
|
480
482
|
}
|
|
481
483
|
}
|
|
482
484
|
} finally {
|
|
@@ -487,254 +489,319 @@ var monitor_default = defineCommand({
|
|
|
487
489
|
}
|
|
488
490
|
});
|
|
489
491
|
|
|
490
|
-
// src/
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const stack = await ctx.getStack();
|
|
505
|
-
const outputMap = await stack.outputs();
|
|
506
|
-
const outputs = Object.fromEntries(
|
|
507
|
-
Object.entries(outputMap).map(([k, v]) => [k, v.value])
|
|
508
|
-
);
|
|
509
|
-
if (!outputs["publicIp"]) {
|
|
510
|
-
throw new StateError("Stack has no outputs \u2014 run `clawops up` first.");
|
|
511
|
-
}
|
|
512
|
-
const base = extractBaseOutputs(outputs);
|
|
513
|
-
return ctx.adapter.getConnectionInfo({
|
|
514
|
-
...base,
|
|
515
|
-
privateKeyPath: ctx.config.ssh.keyPath,
|
|
516
|
-
knownHostsPath: ctx.config.ssh.knownHostsPath
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
function errText(message) {
|
|
520
|
-
return { content: [{ type: "text", text: message }], isError: true };
|
|
521
|
-
}
|
|
522
|
-
function okText(t) {
|
|
523
|
-
return { content: [{ type: "text", text: t }] };
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// src/mcp/tools/cli/config.ts
|
|
527
|
-
var VALID_AUTH_MODES = /* @__PURE__ */ new Set(["none", "token", "password", "trusted-proxy"]);
|
|
528
|
-
async function handleConfigGet(input, _server) {
|
|
529
|
-
const ac = new AbortController();
|
|
530
|
-
const ctx = buildContext({ stack: input.stackName });
|
|
531
|
-
const conn = await resolveConn(ctx);
|
|
532
|
-
const { session, release } = await acquireSession(conn);
|
|
533
|
-
try {
|
|
534
|
-
const result = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
|
|
535
|
-
let cfg;
|
|
536
|
-
try {
|
|
537
|
-
cfg = JSON.parse(result.stdout);
|
|
538
|
-
} catch {
|
|
539
|
-
return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${result.stderr || result.stdout}`);
|
|
540
|
-
}
|
|
541
|
-
const value = input.key ? getPath(cfg, input.key) : cfg;
|
|
542
|
-
return okText(JSON.stringify(value, null, 2));
|
|
543
|
-
} finally {
|
|
544
|
-
release();
|
|
545
|
-
drainPool();
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
async function handleConfigSet(input, server) {
|
|
549
|
-
const elicit = await server.server.elicitInput({
|
|
550
|
-
message: `Set ${input.key} = ${input.value} on stack "${input.stackName ?? "default"}"?`,
|
|
551
|
-
requestedSchema: {
|
|
552
|
-
type: "object",
|
|
553
|
-
properties: { confirmed: { type: "boolean", title: "Confirm config change" } },
|
|
554
|
-
required: ["confirmed"]
|
|
555
|
-
}
|
|
492
|
+
// src/diagnostics/index.ts
|
|
493
|
+
import process2 from "process";
|
|
494
|
+
import { accessSync, mkdirSync, constants } from "fs";
|
|
495
|
+
import path from "path";
|
|
496
|
+
async function runDiagnostics(opts = {}, deps = {}) {
|
|
497
|
+
const { getConfig, getConfigDir } = await import("./store-SDUR52Z5.js");
|
|
498
|
+
const config = getConfig();
|
|
499
|
+
const sections = [];
|
|
500
|
+
sections.push({ title: "Runtime", checks: runtimeChecks(getConfigDir()) });
|
|
501
|
+
sections.push({
|
|
502
|
+
title: "Config",
|
|
503
|
+
checks: [
|
|
504
|
+
config ? { name: "Config file", status: "pass", detail: path.join(getConfigDir(), "config.json") } : { name: "Config file", status: "warn", detail: "not found", remedy: "run `clawops init`" }
|
|
505
|
+
]
|
|
556
506
|
});
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const { session, release } = await acquireSession(conn);
|
|
564
|
-
try {
|
|
565
|
-
const readResult = await session.exec(`cat ${OPENCLAW_CONFIG}`, ac.signal);
|
|
566
|
-
let cfg;
|
|
567
|
-
try {
|
|
568
|
-
cfg = JSON.parse(readResult.stdout);
|
|
569
|
-
} catch {
|
|
570
|
-
return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${readResult.stderr}`);
|
|
571
|
-
}
|
|
572
|
-
let parsedValue = input.value;
|
|
507
|
+
sections.push({ title: "SSH", checks: sshChecks(config) });
|
|
508
|
+
sections.push({ title: "Credentials", checks: await credentialChecks(config) });
|
|
509
|
+
sections.push({ title: "OpenClaw", checks: await versionChecks() });
|
|
510
|
+
if (opts.stack) {
|
|
511
|
+
const open = deps.openSession ?? defaultOpenSession;
|
|
512
|
+
let handle;
|
|
573
513
|
try {
|
|
574
|
-
|
|
575
|
-
} catch {
|
|
576
|
-
}
|
|
577
|
-
setPath(cfg, input.key, parsedValue);
|
|
578
|
-
try {
|
|
579
|
-
await atomicWriteConfig(session, cfg, ac.signal);
|
|
514
|
+
handle = await open(opts.stack, opts.signal);
|
|
580
515
|
} catch (err) {
|
|
581
|
-
|
|
516
|
+
sections.push({
|
|
517
|
+
title: "Remote health",
|
|
518
|
+
checks: [{ name: "Connection", status: "fail", detail: messageOf(err) }]
|
|
519
|
+
});
|
|
582
520
|
}
|
|
583
|
-
|
|
584
|
-
if (input.restart) {
|
|
521
|
+
if (handle) {
|
|
585
522
|
try {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
523
|
+
sections.push({
|
|
524
|
+
title: "Remote health",
|
|
525
|
+
checks: await remoteChecks(handle.session, opts.signal)
|
|
526
|
+
});
|
|
527
|
+
sections.push({
|
|
528
|
+
title: "Hardening",
|
|
529
|
+
checks: await hardeningChecks(handle.conn, opts.stack, opts.signal)
|
|
530
|
+
});
|
|
531
|
+
} finally {
|
|
532
|
+
handle.release();
|
|
533
|
+
const { drainPool } = await import("./pool-EX4ZTA5P.js");
|
|
534
|
+
drainPool();
|
|
590
535
|
}
|
|
591
536
|
}
|
|
592
|
-
return okText(`Config set: ${input.key}${note}`);
|
|
593
|
-
} finally {
|
|
594
|
-
release();
|
|
595
|
-
drainPool();
|
|
596
537
|
}
|
|
538
|
+
return summarise(sections);
|
|
539
|
+
}
|
|
540
|
+
function summarise(sections) {
|
|
541
|
+
const counts = { pass: 0, fail: 0, warn: 0, info: 0 };
|
|
542
|
+
for (const s of sections) for (const c of s.checks) counts[c.status]++;
|
|
543
|
+
return { sections, ok: counts.fail === 0, counts };
|
|
597
544
|
}
|
|
598
|
-
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
545
|
+
function runtimeChecks(configDir) {
|
|
546
|
+
const checks = [];
|
|
547
|
+
const nodeMajor = parseInt(process2.version.slice(1).split(".")[0] ?? "0", 10);
|
|
548
|
+
checks.push(
|
|
549
|
+
nodeMajor >= 22 ? { name: "Node.js", status: "pass", detail: process2.version } : {
|
|
550
|
+
name: "Node.js",
|
|
551
|
+
status: "fail",
|
|
552
|
+
detail: `${process2.version} (requires >=22)`,
|
|
553
|
+
remedy: "install Node.js 22 or later"
|
|
605
554
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
555
|
+
);
|
|
556
|
+
const pulumiHome = path.join(configDir, ".pulumi");
|
|
557
|
+
try {
|
|
558
|
+
mkdirSync(pulumiHome, { recursive: true });
|
|
559
|
+
checks.push({ name: "Pulumi home", status: "pass", detail: pulumiHome });
|
|
560
|
+
} catch {
|
|
561
|
+
checks.push({ name: "Pulumi home", status: "fail", detail: `${pulumiHome} (not writable)` });
|
|
562
|
+
}
|
|
563
|
+
return checks;
|
|
564
|
+
}
|
|
565
|
+
function expandHome(p) {
|
|
566
|
+
return p.replace(/^~/, process2.env["HOME"] ?? "~");
|
|
567
|
+
}
|
|
568
|
+
function sshChecks(config) {
|
|
569
|
+
if (!config) return [{ name: "SSH", status: "info", detail: "skipped \u2014 no config" }];
|
|
570
|
+
const checks = [];
|
|
571
|
+
const keyPath = expandHome(config.ssh.keyPath);
|
|
572
|
+
try {
|
|
573
|
+
accessSync(keyPath, constants.R_OK);
|
|
574
|
+
checks.push({ name: "SSH key", status: "pass", detail: keyPath });
|
|
575
|
+
} catch {
|
|
576
|
+
checks.push({
|
|
577
|
+
name: "SSH key",
|
|
578
|
+
status: "fail",
|
|
579
|
+
detail: `${keyPath} (not found or not readable)`
|
|
580
|
+
});
|
|
609
581
|
}
|
|
610
|
-
const
|
|
611
|
-
const ctx = buildContext({ stack: input.stackName });
|
|
612
|
-
const conn = await resolveConn(ctx);
|
|
613
|
-
const { session, release } = await acquireSession(conn);
|
|
582
|
+
const knownHostsPath = expandHome(config.ssh.knownHostsPath);
|
|
614
583
|
try {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
584
|
+
accessSync(knownHostsPath, constants.F_OK);
|
|
585
|
+
checks.push({ name: "known_hosts", status: "pass", detail: knownHostsPath });
|
|
586
|
+
} catch {
|
|
587
|
+
checks.push({
|
|
588
|
+
name: "known_hosts",
|
|
589
|
+
status: "warn",
|
|
590
|
+
detail: `${knownHostsPath} (does not exist \u2014 will be created on first connect)`
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return checks;
|
|
594
|
+
}
|
|
595
|
+
async function credentialChecks(config) {
|
|
596
|
+
if (!config) return [{ name: "Credentials", status: "info", detail: "skipped \u2014 no config" }];
|
|
597
|
+
const { getProvider } = await import("./providers-2OABPW2E.js");
|
|
598
|
+
await Promise.all([
|
|
599
|
+
import("./aws-AMON7DA7.js"),
|
|
600
|
+
import("./gcp-2JBTD7ZM.js"),
|
|
601
|
+
import("./azure-T4G6OBJQ.js"),
|
|
602
|
+
import("./local-DXBEVZ5C.js")
|
|
603
|
+
]);
|
|
604
|
+
const checks = [];
|
|
605
|
+
const seen = /* @__PURE__ */ new Set();
|
|
606
|
+
for (const [stackName, stackCfg] of Object.entries(config.stacks)) {
|
|
607
|
+
const providerName = stackCfg.provider;
|
|
608
|
+
if (seen.has(providerName)) continue;
|
|
609
|
+
seen.add(providerName);
|
|
610
|
+
if (providerName === "local") {
|
|
611
|
+
checks.push({
|
|
612
|
+
name: providerName,
|
|
613
|
+
status: "pass",
|
|
614
|
+
detail: `stack "${stackName}" (SSH-only, no cloud credentials required)`
|
|
615
|
+
});
|
|
616
|
+
continue;
|
|
621
617
|
}
|
|
622
|
-
deletePath(cfg, input.key);
|
|
623
618
|
try {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
note = " (gateway restarted)";
|
|
633
|
-
} catch (err) {
|
|
634
|
-
return errText(`Gateway restart failed: ${err.message}`);
|
|
619
|
+
const adapter = getProvider(providerName);
|
|
620
|
+
const result = await adapter.validateConfig();
|
|
621
|
+
if (result.ok) {
|
|
622
|
+
checks.push({ name: providerName, status: "pass", detail: `stack "${stackName}"` });
|
|
623
|
+
} else {
|
|
624
|
+
for (const err of result.errors) {
|
|
625
|
+
checks.push({ name: providerName, status: "fail", detail: `stack "${stackName}" \u2014 ${err}` });
|
|
626
|
+
}
|
|
635
627
|
}
|
|
628
|
+
} catch (err) {
|
|
629
|
+
checks.push({
|
|
630
|
+
name: providerName,
|
|
631
|
+
status: "fail",
|
|
632
|
+
detail: `stack "${stackName}" \u2014 ${messageOf(err)}`
|
|
633
|
+
});
|
|
636
634
|
}
|
|
637
|
-
return okText(`Config key removed: ${input.key}${note}`);
|
|
638
|
-
} finally {
|
|
639
|
-
release();
|
|
640
|
-
drainPool();
|
|
641
635
|
}
|
|
636
|
+
if (seen.size === 0) {
|
|
637
|
+
checks.push({ name: "Stacks", status: "warn", detail: "none configured", remedy: "run `clawops init`" });
|
|
638
|
+
}
|
|
639
|
+
return checks;
|
|
642
640
|
}
|
|
643
|
-
async function
|
|
644
|
-
const ac = new AbortController();
|
|
645
|
-
const ctx = buildContext({ stack: input.stackName });
|
|
646
|
-
const conn = await resolveConn(ctx);
|
|
647
|
-
const { session, release } = await acquireSession(conn);
|
|
641
|
+
async function versionChecks() {
|
|
648
642
|
try {
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
643
|
+
const yaml = await import("./js-yaml-PTEEG4FO.js");
|
|
644
|
+
const { loadVersionSpec, describeRange } = await import("./versions-W5LRQCAJ.js");
|
|
645
|
+
const support = loadVersionSpec(yaml).support;
|
|
646
|
+
const checks = [
|
|
647
|
+
{
|
|
648
|
+
name: "Supported range",
|
|
649
|
+
status: "pass",
|
|
650
|
+
detail: `${describeRange(support)} (recommended ${support.recommended})`
|
|
651
|
+
}
|
|
652
|
+
];
|
|
653
|
+
if (!support.max) {
|
|
654
|
+
checks.push({
|
|
655
|
+
name: "Upper bound",
|
|
656
|
+
status: "warn",
|
|
657
|
+
detail: "none declared \u2014 this line would accept any OpenClaw release"
|
|
658
|
+
});
|
|
655
659
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
release();
|
|
660
|
-
drainPool();
|
|
660
|
+
return checks;
|
|
661
|
+
} catch (e) {
|
|
662
|
+
return [{ name: "Version matrix", status: "fail", detail: messageOf(e) }];
|
|
661
663
|
}
|
|
662
664
|
}
|
|
663
|
-
function
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
665
|
+
async function remoteChecks(session, signal) {
|
|
666
|
+
const checks = [];
|
|
667
|
+
const containerResult = await execPrivileged(
|
|
668
|
+
session,
|
|
669
|
+
`docker inspect openclaw --format '{{.State.Status}}' 2>/dev/null || echo 'not found'`,
|
|
670
|
+
signal
|
|
671
|
+
);
|
|
672
|
+
const containerStatus = containerResult.stdout.trim();
|
|
673
|
+
checks.push(
|
|
674
|
+
containerStatus === "running" ? { name: "Container", status: "pass", detail: "running" } : { name: "Container", status: "fail", detail: containerStatus || "unknown" }
|
|
675
|
+
);
|
|
676
|
+
const imageResult = await execPrivileged(
|
|
677
|
+
session,
|
|
678
|
+
`docker inspect openclaw --format '{{.Config.Image}}' 2>/dev/null || echo ''`,
|
|
679
|
+
signal
|
|
680
|
+
);
|
|
681
|
+
const image = imageResult.stdout.trim();
|
|
682
|
+
const deployedVersion = image.includes(":") ? image.slice(image.lastIndexOf(":") + 1) : "";
|
|
683
|
+
if (deployedVersion) {
|
|
684
|
+
checks.push(await deployedVersionCheck(image, deployedVersion));
|
|
676
685
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
+
const probe = await session.exec(probeCommand("started", GATEWAY_PORT), signal);
|
|
687
|
+
const verdict = interpretProbe("started", probe.stdout);
|
|
688
|
+
checks.push(
|
|
689
|
+
verdict.ok ? { name: "Gateway", status: "pass", detail: `answering on 127.0.0.1:${GATEWAY_PORT}` } : { name: "Gateway", status: "fail", detail: verdict.reason ?? "not answering" }
|
|
690
|
+
);
|
|
691
|
+
const pubResult = await execPrivileged(session, PUBLISH_INSPECT_CMD, signal);
|
|
692
|
+
checks.push(
|
|
693
|
+
publishForRestart(pubResult.stdout) === "all" ? {
|
|
694
|
+
name: "Published",
|
|
695
|
+
status: "warn",
|
|
696
|
+
detail: "0.0.0.0 \u2014 the gateway port is open on every interface, serving plaintext HTTP",
|
|
697
|
+
remedy: 'front it with TLS, or set network.publishGateway to "loopback" and use `clawops tunnel`'
|
|
698
|
+
} : { name: "Published", status: "pass", detail: "127.0.0.1 only (reach it with `clawops tunnel`)" }
|
|
699
|
+
);
|
|
700
|
+
const diskResult = await session.exec(
|
|
701
|
+
// The state directory: 2.0's SQLite lives there, not in the user's home.
|
|
702
|
+
`df -h ${STATE_DIR_HOST_LINUX} 2>/dev/null | awk 'NR==2{print $5" used ("$3" of "$2")"}'`,
|
|
703
|
+
signal
|
|
704
|
+
);
|
|
705
|
+
const diskUsage = diskResult.stdout.trim();
|
|
706
|
+
if (diskUsage) {
|
|
707
|
+
const pct = parseInt(diskUsage.match(/^(\d+)%/)?.[1] ?? "0", 10);
|
|
708
|
+
checks.push({
|
|
709
|
+
name: "Disk",
|
|
710
|
+
status: pct >= 90 ? "fail" : pct >= 75 ? "warn" : "pass",
|
|
711
|
+
detail: diskUsage
|
|
712
|
+
});
|
|
713
|
+
} else {
|
|
714
|
+
checks.push({ name: "Disk", status: "warn", detail: "unable to determine disk usage" });
|
|
686
715
|
}
|
|
687
|
-
|
|
716
|
+
const logrotateResult = await session.exec(
|
|
717
|
+
`test -f /etc/logrotate.d/openclaw && echo 'configured' || echo 'not configured'`,
|
|
718
|
+
signal
|
|
719
|
+
);
|
|
720
|
+
checks.push(
|
|
721
|
+
logrotateResult.stdout.trim() === "configured" ? { name: "Log rotation", status: "pass", detail: "configured" } : { name: "Log rotation", status: "warn", detail: "not configured \u2014 logs may grow unbounded" }
|
|
722
|
+
);
|
|
723
|
+
return checks;
|
|
688
724
|
}
|
|
689
|
-
function
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
const meta = cfg["meta"];
|
|
702
|
-
if (meta !== void 0 && (typeof meta !== "object" || Array.isArray(meta) || meta === null)) {
|
|
703
|
-
issues.push("'meta' must be an object.");
|
|
704
|
-
} else if (meta && typeof meta === "object") {
|
|
705
|
-
const ltv = meta["lastTouchedVersion"];
|
|
706
|
-
if (ltv !== void 0 && typeof ltv !== "string") {
|
|
707
|
-
issues.push("'meta.lastTouchedVersion' must be a string.");
|
|
708
|
-
}
|
|
725
|
+
async function deployedVersionCheck(image, deployedVersion) {
|
|
726
|
+
const yaml = await import("./js-yaml-PTEEG4FO.js");
|
|
727
|
+
const { loadVersionSpec, checkVersion, isMovingTag } = await import("./versions-W5LRQCAJ.js");
|
|
728
|
+
const support = loadVersionSpec(yaml).support;
|
|
729
|
+
if (isMovingTag(deployedVersion)) {
|
|
730
|
+
return {
|
|
731
|
+
name: "Deployed",
|
|
732
|
+
status: "warn",
|
|
733
|
+
detail: `${image} (moving tag)`,
|
|
734
|
+
remedy: "pin an explicit version \u2014 a moving tag resolves to whatever is newest"
|
|
735
|
+
};
|
|
709
736
|
}
|
|
710
|
-
const
|
|
711
|
-
if (
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
737
|
+
const check = checkVersion(deployedVersion, support);
|
|
738
|
+
if (check.ok) return { name: "Deployed", status: "pass", detail: `OpenClaw ${deployedVersion}` };
|
|
739
|
+
return {
|
|
740
|
+
name: "Deployed",
|
|
741
|
+
status: "fail",
|
|
742
|
+
detail: `OpenClaw ${deployedVersion} is not supported by this clawops line`,
|
|
743
|
+
remedy: check.error.reason === "too-old" ? "this clawops line requires OpenClaw 2.0 or later \u2014 run `clawops migrate` to move an existing 1.x deployment across, which preserves its state and device identity" : "upgrade clawops, or pin an OpenClaw version this line supports"
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
async function hardeningChecks(conn, stack, signal) {
|
|
747
|
+
try {
|
|
748
|
+
const { MODULE_CATALOG, resolveModules, withRemoteExec } = await import("./harden-PCDK3UQ5.js");
|
|
749
|
+
const { buildContext } = await import("./context-KHKWJJMC.js");
|
|
750
|
+
const provider = buildContext({ stack }).adapter.name;
|
|
751
|
+
const modules = resolveModules(MODULE_CATALOG, void 0, provider);
|
|
752
|
+
const checks = [];
|
|
753
|
+
await withRemoteExec(conn, signal, async (exec) => {
|
|
754
|
+
for (const mod of modules) {
|
|
755
|
+
const result = await mod.check(exec);
|
|
756
|
+
if (result.status === "applied") {
|
|
757
|
+
checks.push({ name: mod.label, status: "pass", detail: "applied" });
|
|
758
|
+
} else if (result.status === "drifted") {
|
|
759
|
+
checks.push({ name: mod.label, status: "warn", detail: `drifted \u2014 ${result.detail}` });
|
|
760
|
+
} else if (result.status === "missing") {
|
|
761
|
+
checks.push({ name: mod.label, status: "info", detail: "not applied", remedy: "run `clawops harden`" });
|
|
762
|
+
}
|
|
723
763
|
}
|
|
724
|
-
}
|
|
764
|
+
});
|
|
765
|
+
return checks;
|
|
766
|
+
} catch (err) {
|
|
767
|
+
return [{ name: "Hardening checks", status: "fail", detail: messageOf(err) }];
|
|
725
768
|
}
|
|
726
|
-
|
|
769
|
+
}
|
|
770
|
+
async function defaultOpenSession(stack, signal) {
|
|
771
|
+
const { getConfig } = await import("./store-SDUR52Z5.js");
|
|
772
|
+
const { buildContext } = await import("./context-KHKWJJMC.js");
|
|
773
|
+
const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
|
|
774
|
+
const { acquireSession } = await import("./pool-EX4ZTA5P.js");
|
|
775
|
+
const config = getConfig();
|
|
776
|
+
if (!config) throw new Error("no config \u2014 run `clawops init`");
|
|
777
|
+
const ctx = buildContext({ stack });
|
|
778
|
+
const stackObj = await ctx.getStack();
|
|
779
|
+
const outputMap = await stackObj.outputs();
|
|
780
|
+
const outputs = Object.fromEntries(
|
|
781
|
+
Object.entries(outputMap).map(([k, v]) => [k, v.value])
|
|
782
|
+
);
|
|
783
|
+
const base = extractBaseOutputs(outputs);
|
|
784
|
+
const conn = ctx.adapter.getConnectionInfo({
|
|
785
|
+
...base,
|
|
786
|
+
privateKeyPath: config.ssh.keyPath,
|
|
787
|
+
knownHostsPath: config.ssh.knownHostsPath
|
|
788
|
+
});
|
|
789
|
+
const { session, release } = await acquireSession({
|
|
790
|
+
host: conn.host,
|
|
791
|
+
port: conn.port,
|
|
792
|
+
user: conn.user,
|
|
793
|
+
privateKeyPath: conn.privateKeyPath,
|
|
794
|
+
knownHostsPath: conn.knownHostsPath,
|
|
795
|
+
signal
|
|
796
|
+
});
|
|
797
|
+
return { session, release, conn };
|
|
798
|
+
}
|
|
799
|
+
function messageOf(err) {
|
|
800
|
+
return err instanceof Error ? err.message : String(err);
|
|
727
801
|
}
|
|
728
802
|
|
|
729
803
|
export {
|
|
730
|
-
|
|
731
|
-
errText,
|
|
732
|
-
okText,
|
|
733
|
-
handleConfigGet,
|
|
734
|
-
handleConfigSet,
|
|
735
|
-
handleConfigUnset,
|
|
736
|
-
handleConfigValidate,
|
|
737
|
-
validateOpenclawConfig,
|
|
804
|
+
runDiagnostics,
|
|
738
805
|
formatUptime,
|
|
739
806
|
gatherSnapshot,
|
|
740
807
|
monitor_default
|