@openclawbrain/cli 0.4.11 → 0.4.12
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 +3 -2
- package/dist/src/cli.js +45 -8
- package/dist/src/daemon.d.ts +6 -1
- package/dist/src/daemon.js +229 -41
- package/dist/src/proof-command.js +654 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Operator CLI for OpenClawBrain. Use it with `@openclawbrain/openclaw`.
|
|
4
4
|
|
|
5
|
-
The public install story is three commands to install or update,
|
|
5
|
+
The public install story is three commands to install or update, one command to verify, and one official proof command when you need a durable operator bundle.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
openclaw plugins install @openclawbrain/openclaw
|
|
9
9
|
npx @openclawbrain/cli install --openclaw-home ~/.openclaw
|
|
10
10
|
openclaw gateway restart
|
|
11
11
|
npx @openclawbrain/cli status --openclaw-home ~/.openclaw --detailed
|
|
12
|
+
npx @openclawbrain/cli proof --openclaw-home ~/.openclaw --skip-install --skip-restart
|
|
12
13
|
```
|
|
13
14
|
|
|
14
|
-
The first three commands install or update OpenClawBrain.
|
|
15
|
+
The first three commands install or update OpenClawBrain. `status --detailed` is the quick verify surface. `proof` writes `summary.md`, `steps.json`, `verdict.json`, raw step logs, and proof pointers under one bundle directory.
|
|
15
16
|
|
|
16
17
|
## Common commands
|
|
17
18
|
|
package/dist/src/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ import { buildTracedLearningBridgePayloadFromRuntime, buildTracedLearningStatusS
|
|
|
24
24
|
import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
|
|
25
25
|
import { readOpenClawBrainProviderDefaults, readOpenClawBrainProviderConfig, readOpenClawBrainProviderConfigFromSources, resolveOpenClawBrainProviderDefaultsPath } from "./provider-config.js";
|
|
26
26
|
import { formatOperatorLearningPathSummary } from "./status-learning-path.js";
|
|
27
|
+
import { buildProofCommandForOpenClawHome, buildProofCommandHelpSection, captureOperatorProofBundle, formatOperatorProofResult, parseProofCliArgs } from "./proof-command.js";
|
|
27
28
|
const OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV = "OPENCLAWBRAIN_EMBEDDER_BASE_URL";
|
|
28
29
|
const OPENCLAWBRAIN_EMBEDDER_PROVIDER_ENV = "OPENCLAWBRAIN_EMBEDDER_PROVIDER";
|
|
29
30
|
const OPENCLAWBRAIN_EMBEDDER_MODEL_ENV = "OPENCLAWBRAIN_EMBEDDER_MODEL";
|
|
@@ -466,6 +467,7 @@ function buildSetupDeletedMessage() {
|
|
|
466
467
|
].join(" ");
|
|
467
468
|
}
|
|
468
469
|
function operatorCliHelp() {
|
|
470
|
+
const proofHelp = buildProofCommandHelpSection();
|
|
469
471
|
return [
|
|
470
472
|
"Usage:",
|
|
471
473
|
" openclawbrain install [--openclaw-home <path>] [options]",
|
|
@@ -480,6 +482,7 @@ function operatorCliHelp() {
|
|
|
480
482
|
" openclawbrain scan --session <trace.json> --root <path> [options]",
|
|
481
483
|
" openclawbrain scan --live <event-export-path> --workspace <workspace.json> [options]",
|
|
482
484
|
" openclawbrain learn [--activation-root <path>|--openclaw-home <path>] [--json]",
|
|
485
|
+
proofHelp.usage,
|
|
483
486
|
" openclawbrain-ops <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options] # compatibility alias",
|
|
484
487
|
" openclawbrain-ops scan --session <trace.json> --root <path> [options] # compatibility alias",
|
|
485
488
|
"",
|
|
@@ -508,6 +511,7 @@ function operatorCliHelp() {
|
|
|
508
511
|
" --limit <N> Maximum number of history entries to show (default: 20, history only).",
|
|
509
512
|
" --scan-root <path> Event-export scan root for watch mode (defaults to <activation-root>/event-exports).",
|
|
510
513
|
" --interval <seconds> Polling interval for watch mode (default: 30).",
|
|
514
|
+
...proofHelp.optionLines,
|
|
511
515
|
" --json Emit machine-readable JSON instead of text.",
|
|
512
516
|
" --help Show this help.",
|
|
513
517
|
"",
|
|
@@ -516,17 +520,19 @@ function operatorCliHelp() {
|
|
|
516
520
|
" 2. attach openclawbrain attach --openclaw-home <path> [--activation-root <path>] — explicit reattach/manual hook path for known brain data; use install first",
|
|
517
521
|
" 3. status openclawbrain status --activation-root <path> — answer \"How's the brain?\" for that boundary",
|
|
518
522
|
" 4. status --detailed openclawbrain status --activation-root <path> --detailed — explain serve path, freshness, backlog, and failure mode",
|
|
519
|
-
|
|
520
|
-
" 6.
|
|
521
|
-
" 7. daemon
|
|
522
|
-
" 8.
|
|
523
|
-
" 9.
|
|
523
|
+
proofHelp.lifecycle,
|
|
524
|
+
" 6. watch openclawbrain watch --activation-root <path> — run the foreground learning/watch loop",
|
|
525
|
+
" 7. daemon start openclawbrain daemon start --activation-root <path> — keep watch running in the background on macOS",
|
|
526
|
+
" 8. daemon status openclawbrain daemon status --activation-root <path> — inspect the background watch state",
|
|
527
|
+
" 9. detach openclawbrain detach --openclaw-home <path> — remove the profile hookup only and keep brain data",
|
|
528
|
+
" 10. uninstall openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data — remove the hookup and choose the data outcome explicitly",
|
|
524
529
|
"",
|
|
525
530
|
"Advanced/operator surfaces:",
|
|
526
531
|
" context preview the brain context that would be injected for a message",
|
|
527
532
|
" rollback preview or apply active <- previous, active -> candidate pointer movement",
|
|
528
533
|
" scan inspect one recorded session or live event export without claiming a daemon is running",
|
|
529
534
|
" learn one-shot local-session learning pass against the resolved activation root",
|
|
535
|
+
proofHelp.advanced,
|
|
530
536
|
" status --teacher-snapshot keeps the current live-first / principal-priority / passive-backfill learner order visible when that snapshot exists",
|
|
531
537
|
" native package installs still need the openclawbrain CLI available because install/attach pin the activation root for that package copy",
|
|
532
538
|
" watch/daemon persist their operator snapshot at <activation-root>/watch/teacher-snapshot.json; --teacher-snapshot overrides the default path",
|
|
@@ -539,7 +545,8 @@ function operatorCliHelp() {
|
|
|
539
545
|
" attach: 0 on successful profile hookup/bootstrap, 1 on input/read failure.",
|
|
540
546
|
" detach: 0 on successful unhook, 1 on input/read failure.",
|
|
541
547
|
" uninstall: 0 on successful unhook/cleanup, 1 on input/read failure.",
|
|
542
|
-
" scan: 0 on successful replay/scan, 1 on input/read failure."
|
|
548
|
+
" scan: 0 on successful replay/scan, 1 on input/read failure.",
|
|
549
|
+
" proof: 0 on successful bundle capture, 1 on input/read failure."
|
|
543
550
|
].join("\n");
|
|
544
551
|
}
|
|
545
552
|
function yesNo(value) {
|
|
@@ -1880,7 +1887,7 @@ function buildStatusNextStep(status, report, options) {
|
|
|
1880
1887
|
return "Repair the runtime-load proof file before trusting attach truth again; status now knows the exact file that broke.";
|
|
1881
1888
|
}
|
|
1882
1889
|
if (options.installHook.state === "installed" && status.brainStatus.serveState === "serving_active_pack") {
|
|
1883
|
-
return
|
|
1890
|
+
return `Capture a durable proof bundle: \`${buildProofCommandForOpenClawHome(options.openclawHome)}\`.`;
|
|
1884
1891
|
}
|
|
1885
1892
|
if (report.learning.warningStates.includes("principal_live_backlog") ||
|
|
1886
1893
|
report.learning.warningStates.includes("active_pack_behind_latest_principal")) {
|
|
@@ -2007,9 +2014,12 @@ export function parseOperatorCliArgs(argv) {
|
|
|
2007
2014
|
args.shift();
|
|
2008
2015
|
return parseDaemonArgs(args);
|
|
2009
2016
|
}
|
|
2010
|
-
if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "install" || args[0] === "detach" || args[0] === "uninstall" || args[0] === "context" || args[0] === "history" || args[0] === "learn" || args[0] === "watch" || args[0] === "export" || args[0] === "import" || args[0] === "reset") {
|
|
2017
|
+
if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "install" || args[0] === "detach" || args[0] === "uninstall" || args[0] === "context" || args[0] === "history" || args[0] === "learn" || args[0] === "watch" || args[0] === "export" || args[0] === "import" || args[0] === "reset" || args[0] === "proof") {
|
|
2011
2018
|
command = args.shift();
|
|
2012
2019
|
}
|
|
2020
|
+
if (command === "proof") {
|
|
2021
|
+
return parseProofCliArgs(args);
|
|
2022
|
+
}
|
|
2013
2023
|
if (command === "learn") {
|
|
2014
2024
|
for (let index = 0; index < args.length; index += 1) {
|
|
2015
2025
|
const arg = args[index];
|
|
@@ -3389,6 +3399,7 @@ function runProfileHookAttachCommand(parsed) {
|
|
|
3389
3399
|
? null
|
|
3390
3400
|
: `Confirm gateway after restart: ${brainFeedback.restart.gatewayStatusCommand}`,
|
|
3391
3401
|
`Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`,
|
|
3402
|
+
`Capture proof: ${buildProofCommandForOpenClawHome(parsed.openclawHome)}`,
|
|
3392
3403
|
`Check learner service: ${buildLearnerServiceStatusCommand(parsed.activationRoot)}`,
|
|
3393
3404
|
embedderProvision !== null && embedderProvision.state === "skipped"
|
|
3394
3405
|
? `Provision default embedder later: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`
|
|
@@ -3537,6 +3548,7 @@ function runProfileHookAttachCommand(parsed) {
|
|
|
3537
3548
|
console.log(`Gateway: Confirm OpenClaw after restart: ${brainFeedback.restart.gatewayStatusCommand}`);
|
|
3538
3549
|
}
|
|
3539
3550
|
console.log(`Check: ${buildInstallStatusCommand(parsed.activationRoot)}`);
|
|
3551
|
+
console.log(`Proof: ${buildProofCommandForOpenClawHome(parsed.openclawHome)}`);
|
|
3540
3552
|
console.log(`Learner: ${buildLearnerServiceStatusCommand(parsed.activationRoot)}`);
|
|
3541
3553
|
if (embedderProvision !== null && embedderProvision.state === "skipped") {
|
|
3542
3554
|
console.log(`Embedder: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`);
|
|
@@ -5548,6 +5560,28 @@ function runResetCommand(parsed) {
|
|
|
5548
5560
|
}
|
|
5549
5561
|
return 0;
|
|
5550
5562
|
}
|
|
5563
|
+
function runProofCommand(parsed) {
|
|
5564
|
+
if (parsed.help) {
|
|
5565
|
+
console.log(operatorCliHelp());
|
|
5566
|
+
return 0;
|
|
5567
|
+
}
|
|
5568
|
+
const result = captureOperatorProofBundle({
|
|
5569
|
+
openclawHome: parsed.openclawHome,
|
|
5570
|
+
activationRoot: parsed.activationRoot,
|
|
5571
|
+
outputDir: parsed.outputDir,
|
|
5572
|
+
skipInstall: parsed.skipInstall,
|
|
5573
|
+
skipRestart: parsed.skipRestart,
|
|
5574
|
+
pluginId: parsed.pluginId,
|
|
5575
|
+
timeoutMs: parsed.timeoutMs,
|
|
5576
|
+
});
|
|
5577
|
+
if (parsed.json) {
|
|
5578
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5579
|
+
}
|
|
5580
|
+
else {
|
|
5581
|
+
console.log(formatOperatorProofResult(result));
|
|
5582
|
+
}
|
|
5583
|
+
return 0;
|
|
5584
|
+
}
|
|
5551
5585
|
export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
5552
5586
|
const parsed = parseOperatorCliArgs(argv);
|
|
5553
5587
|
if (parsed.command === "context") {
|
|
@@ -5556,6 +5590,9 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
5556
5590
|
if (parsed.command === "reset") {
|
|
5557
5591
|
return runResetCommand(parsed);
|
|
5558
5592
|
}
|
|
5593
|
+
if (parsed.command === "proof") {
|
|
5594
|
+
return runProofCommand(parsed);
|
|
5595
|
+
}
|
|
5559
5596
|
if (parsed.help) {
|
|
5560
5597
|
console.log(operatorCliHelp());
|
|
5561
5598
|
return 0;
|
package/dist/src/daemon.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Commands:
|
|
9
9
|
* daemon start — generate and load a launchd plist
|
|
10
10
|
* daemon stop — unload the plist
|
|
11
|
-
* daemon status — show running/stopped + PID + last log lines
|
|
11
|
+
* daemon status — show running/stopped + PID + launch command + last log lines
|
|
12
12
|
* daemon logs — tail the daemon log file
|
|
13
13
|
*/
|
|
14
14
|
type DaemonCommandRunner = (command: string) => string;
|
|
@@ -42,6 +42,11 @@ export interface ManagedLearnerServiceInspection {
|
|
|
42
42
|
running: boolean;
|
|
43
43
|
pid: number | null;
|
|
44
44
|
configuredActivationRoot: string | null;
|
|
45
|
+
configuredProgramArguments: string[] | null;
|
|
46
|
+
configuredCommand: string | null;
|
|
47
|
+
configuredRuntimePath: string | null;
|
|
48
|
+
configuredRuntimePackageSpec: string | null;
|
|
49
|
+
configuredRuntimeLooksEphemeral: boolean | null;
|
|
45
50
|
matchesRequestedActivationRoot: boolean | null;
|
|
46
51
|
launchctlAvailable: boolean;
|
|
47
52
|
}
|
package/dist/src/daemon.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Commands:
|
|
9
9
|
* daemon start — generate and load a launchd plist
|
|
10
10
|
* daemon stop — unload the plist
|
|
11
|
-
* daemon status — show running/stopped + PID + last log lines
|
|
11
|
+
* daemon status — show running/stopped + PID + launch command + last log lines
|
|
12
12
|
* daemon logs — tail the daemon log file
|
|
13
13
|
*/
|
|
14
14
|
import { execSync } from "node:child_process";
|
|
@@ -22,6 +22,8 @@ const LOG_ROOT_DIRNAME = "daemon";
|
|
|
22
22
|
const DEFAULT_SCAN_ROOT_DIRNAME = "event-exports";
|
|
23
23
|
const BASELINE_STATE_BASENAME = "baseline-state.json";
|
|
24
24
|
const SCANNER_CHECKPOINT_BASENAME = ".openclawbrain-scanner-checkpoint.json";
|
|
25
|
+
const CLI_PACKAGE_NAME = "@openclawbrain/cli";
|
|
26
|
+
const CLI_BIN_NAME = "openclawbrain";
|
|
25
27
|
const DEFAULT_DAEMON_COMMAND_RUNNER = (command) => execSync(command, {
|
|
26
28
|
encoding: "utf8",
|
|
27
29
|
stdio: "pipe",
|
|
@@ -62,15 +64,81 @@ export function buildDaemonServiceIdentity(activationRoot) {
|
|
|
62
64
|
export function setDaemonCommandRunnerForTesting(runner) {
|
|
63
65
|
daemonCommandRunner = runner ?? DEFAULT_DAEMON_COMMAND_RUNNER;
|
|
64
66
|
}
|
|
65
|
-
function
|
|
67
|
+
function readPackageMetadata(packageRoot) {
|
|
68
|
+
if (packageRoot === null) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
72
|
+
if (!existsSync(packageJsonPath)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
66
75
|
try {
|
|
67
|
-
const
|
|
68
|
-
|
|
76
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
77
|
+
const name = typeof packageJson.name === "string" && packageJson.name.trim().length > 0
|
|
78
|
+
? packageJson.name.trim()
|
|
79
|
+
: null;
|
|
80
|
+
const version = typeof packageJson.version === "string" && packageJson.version.trim().length > 0
|
|
81
|
+
? packageJson.version.trim()
|
|
82
|
+
: null;
|
|
83
|
+
if (name === null) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return { name, version };
|
|
69
87
|
}
|
|
70
88
|
catch {
|
|
71
89
|
return null;
|
|
72
90
|
}
|
|
73
91
|
}
|
|
92
|
+
function isCliScriptPath(filePath) {
|
|
93
|
+
const basename = path.basename(filePath);
|
|
94
|
+
return basename === "cli.js" || basename === "cli.cjs" || basename === "cli.mjs";
|
|
95
|
+
}
|
|
96
|
+
function isNodeExecutablePath(filePath) {
|
|
97
|
+
return /^node(?:\.exe)?$/i.test(path.basename(filePath));
|
|
98
|
+
}
|
|
99
|
+
function isNpxCachePath(filePath) {
|
|
100
|
+
const resolvedPath = safeRealpath(path.resolve(filePath));
|
|
101
|
+
return resolvedPath.split(path.sep).includes("_npx");
|
|
102
|
+
}
|
|
103
|
+
function formatCommandArgument(value) {
|
|
104
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : JSON.stringify(value);
|
|
105
|
+
}
|
|
106
|
+
function formatCommand(programArguments) {
|
|
107
|
+
return programArguments.map((argument) => formatCommandArgument(argument)).join(" ");
|
|
108
|
+
}
|
|
109
|
+
function escapePlistString(value) {
|
|
110
|
+
return value
|
|
111
|
+
.replace(/&/g, "&")
|
|
112
|
+
.replace(/</g, "<")
|
|
113
|
+
.replace(/>/g, ">")
|
|
114
|
+
.replace(/"/g, """)
|
|
115
|
+
.replace(/'/g, "'");
|
|
116
|
+
}
|
|
117
|
+
function unescapePlistString(value) {
|
|
118
|
+
return value
|
|
119
|
+
.replace(/'/g, "'")
|
|
120
|
+
.replace(/"/g, "\"")
|
|
121
|
+
.replace(/>/g, ">")
|
|
122
|
+
.replace(/</g, "<")
|
|
123
|
+
.replace(/&/g, "&");
|
|
124
|
+
}
|
|
125
|
+
function getCommandPaths(commandName) {
|
|
126
|
+
try {
|
|
127
|
+
return daemonCommandRunner(`which -a ${commandName}`)
|
|
128
|
+
.split("\n")
|
|
129
|
+
.map((entry) => entry.trim())
|
|
130
|
+
.filter((entry) => entry.length > 0);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
try {
|
|
134
|
+
const resolved = daemonCommandRunner(`command -v ${commandName}`).trim();
|
|
135
|
+
return resolved.length > 0 ? [resolved] : [];
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
74
142
|
function safeRealpath(filePath) {
|
|
75
143
|
try {
|
|
76
144
|
return realpathSync(filePath);
|
|
@@ -110,62 +178,129 @@ function resolveCliScriptCandidate(candidatePath) {
|
|
|
110
178
|
return null;
|
|
111
179
|
}
|
|
112
180
|
const resolvedCandidate = safeRealpath(absoluteCandidate);
|
|
113
|
-
|
|
114
|
-
if (basename !== "cli.js" && basename !== "cli.cjs" && basename !== "cli.mjs") {
|
|
181
|
+
if (!isCliScriptPath(resolvedCandidate)) {
|
|
115
182
|
return null;
|
|
116
183
|
}
|
|
117
184
|
return resolvedCandidate;
|
|
118
185
|
}
|
|
119
|
-
function
|
|
186
|
+
function resolveCliPackageRoot(startDir) {
|
|
187
|
+
const packageRoot = resolvePackageRoot(startDir);
|
|
188
|
+
const packageMetadata = readPackageMetadata(packageRoot);
|
|
189
|
+
if (packageMetadata?.name === CLI_PACKAGE_NAME) {
|
|
190
|
+
return packageRoot;
|
|
191
|
+
}
|
|
192
|
+
if (packageRoot !== null) {
|
|
193
|
+
const siblingCliRoot = path.join(path.dirname(packageRoot), "cli");
|
|
194
|
+
const siblingMetadata = readPackageMetadata(siblingCliRoot);
|
|
195
|
+
if (siblingMetadata?.name === CLI_PACKAGE_NAME) {
|
|
196
|
+
return siblingCliRoot;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function resolveDaemonPackageManagerLaunchSpec(moduleDir) {
|
|
202
|
+
const cliPackageRoot = resolveCliPackageRoot(moduleDir);
|
|
203
|
+
const cliPackageMetadata = readPackageMetadata(cliPackageRoot);
|
|
204
|
+
if (cliPackageMetadata === null) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const npmPath = getCommandPaths("npm").find((candidate) => !isNpxCachePath(candidate)) ?? null;
|
|
208
|
+
if (npmPath === null) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const packageSpec = cliPackageMetadata.version === null
|
|
212
|
+
? CLI_PACKAGE_NAME
|
|
213
|
+
: `${CLI_PACKAGE_NAME}@${cliPackageMetadata.version}`;
|
|
214
|
+
return {
|
|
215
|
+
programArguments: [npmPath, "exec", "--yes", `--package=${packageSpec}`, "--", CLI_BIN_NAME],
|
|
216
|
+
runtimePath: npmPath,
|
|
217
|
+
runtimePackageSpec: packageSpec,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function getOpenclawbrainCliScriptPathCandidates() {
|
|
120
221
|
const moduleFilePath = fileURLToPath(import.meta.url);
|
|
121
222
|
const moduleDir = path.dirname(moduleFilePath);
|
|
122
223
|
const packageRoot = resolvePackageRoot(moduleDir);
|
|
123
|
-
|
|
224
|
+
return [
|
|
124
225
|
process.argv[1],
|
|
125
226
|
path.join(moduleDir, "cli.js"),
|
|
126
227
|
packageRoot === null ? null : path.join(packageRoot, "dist", "src", "cli.js")
|
|
127
228
|
];
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
229
|
+
}
|
|
230
|
+
function buildDaemonLaunchProgramArguments(serviceIdentity, programArguments) {
|
|
231
|
+
return [...programArguments, "watch", "--activation-root", serviceIdentity.requestedActivationRoot];
|
|
232
|
+
}
|
|
233
|
+
function describeDaemonProgramArguments(programArguments) {
|
|
234
|
+
if (programArguments === null || programArguments.length === 0) {
|
|
235
|
+
return {
|
|
236
|
+
configuredProgramArguments: null,
|
|
237
|
+
configuredCommand: null,
|
|
238
|
+
configuredRuntimePath: null,
|
|
239
|
+
configuredRuntimePackageSpec: null,
|
|
240
|
+
configuredRuntimeLooksEphemeral: null
|
|
241
|
+
};
|
|
133
242
|
}
|
|
134
|
-
|
|
243
|
+
const runtimePath = programArguments.length >= 2 && isNodeExecutablePath(programArguments[0]) && isCliScriptPath(programArguments[1])
|
|
244
|
+
? programArguments[1]
|
|
245
|
+
: programArguments[0];
|
|
246
|
+
const runtimePackageSpec = programArguments.find((argument) => argument.startsWith("--package="))?.slice("--package=".length) ?? null;
|
|
247
|
+
return {
|
|
248
|
+
configuredProgramArguments: programArguments,
|
|
249
|
+
configuredCommand: formatCommand(programArguments),
|
|
250
|
+
configuredRuntimePath: runtimePath,
|
|
251
|
+
configuredRuntimePackageSpec: runtimePackageSpec,
|
|
252
|
+
configuredRuntimeLooksEphemeral: runtimePath === null ? null : isNpxCachePath(runtimePath)
|
|
253
|
+
};
|
|
135
254
|
}
|
|
136
255
|
function resolveDaemonProgramArguments() {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
256
|
+
for (const candidate of getOpenclawbrainCliScriptPathCandidates()) {
|
|
257
|
+
const cliScriptPath = resolveCliScriptCandidate(candidate);
|
|
258
|
+
if (cliScriptPath !== null && !isNpxCachePath(cliScriptPath)) {
|
|
259
|
+
return {
|
|
260
|
+
programArguments: [process.execPath, cliScriptPath],
|
|
261
|
+
runtimePath: cliScriptPath,
|
|
262
|
+
runtimePackageSpec: null,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
140
265
|
}
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
143
|
-
return
|
|
266
|
+
const durableBinPath = getCommandPaths(CLI_BIN_NAME).find((candidate) => !isNpxCachePath(candidate)) ?? null;
|
|
267
|
+
if (durableBinPath !== null) {
|
|
268
|
+
return {
|
|
269
|
+
programArguments: [durableBinPath],
|
|
270
|
+
runtimePath: durableBinPath,
|
|
271
|
+
runtimePackageSpec: null,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const moduleFilePath = fileURLToPath(import.meta.url);
|
|
275
|
+
const moduleDir = path.dirname(moduleFilePath);
|
|
276
|
+
const packageManagerLaunchSpec = resolveDaemonPackageManagerLaunchSpec(moduleDir);
|
|
277
|
+
if (packageManagerLaunchSpec !== null) {
|
|
278
|
+
return packageManagerLaunchSpec;
|
|
144
279
|
}
|
|
145
280
|
return null;
|
|
146
281
|
}
|
|
147
282
|
function buildPlistXml(serviceIdentity, programArguments) {
|
|
148
283
|
const logPath = serviceIdentity.logPath;
|
|
149
284
|
const homeDir = getHomeDir();
|
|
150
|
-
const daemonProgramArguments =
|
|
151
|
-
.map((argument) => ` <string>${argument}</string>`)
|
|
285
|
+
const daemonProgramArguments = buildDaemonLaunchProgramArguments(serviceIdentity, programArguments)
|
|
286
|
+
.map((argument) => ` <string>${escapePlistString(argument)}</string>`)
|
|
152
287
|
.join("\n");
|
|
153
288
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
154
289
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
155
290
|
<plist version="1.0">
|
|
156
291
|
<dict>
|
|
157
292
|
<key>Label</key>
|
|
158
|
-
<string>${serviceIdentity.label}</string>
|
|
293
|
+
<string>${escapePlistString(serviceIdentity.label)}</string>
|
|
159
294
|
<key>ProgramArguments</key>
|
|
160
295
|
<array>
|
|
161
296
|
${daemonProgramArguments}
|
|
162
297
|
</array>
|
|
163
298
|
<key>WorkingDirectory</key>
|
|
164
|
-
<string>${serviceIdentity.requestedActivationRoot}</string>
|
|
299
|
+
<string>${escapePlistString(serviceIdentity.requestedActivationRoot)}</string>
|
|
165
300
|
<key>StandardOutPath</key>
|
|
166
|
-
<string>${logPath}</string>
|
|
301
|
+
<string>${escapePlistString(logPath)}</string>
|
|
167
302
|
<key>StandardErrorPath</key>
|
|
168
|
-
<string>${logPath}</string>
|
|
303
|
+
<string>${escapePlistString(logPath)}</string>
|
|
169
304
|
<key>KeepAlive</key>
|
|
170
305
|
<true/>
|
|
171
306
|
<key>RunAtLoad</key>
|
|
@@ -173,9 +308,9 @@ ${daemonProgramArguments}
|
|
|
173
308
|
<key>EnvironmentVariables</key>
|
|
174
309
|
<dict>
|
|
175
310
|
<key>HOME</key>
|
|
176
|
-
<string>${homeDir}</string>
|
|
311
|
+
<string>${escapePlistString(homeDir)}</string>
|
|
177
312
|
<key>PATH</key>
|
|
178
|
-
<string
|
|
313
|
+
<string>${escapePlistString("/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin")}</string>
|
|
179
314
|
</dict>
|
|
180
315
|
</dict>
|
|
181
316
|
</plist>
|
|
@@ -235,6 +370,7 @@ function getLaunchctlInfo(label) {
|
|
|
235
370
|
function inspectManagedLearnerServiceInternal(activationRoot) {
|
|
236
371
|
const serviceIdentity = buildDaemonServiceIdentity(activationRoot);
|
|
237
372
|
const configuredActivationRoot = readDaemonActivationRoot(serviceIdentity.plistPath);
|
|
373
|
+
const configuredProgramArguments = readDaemonProgramArguments(serviceIdentity.plistPath);
|
|
238
374
|
const info = getLaunchctlInfo(serviceIdentity.label);
|
|
239
375
|
return {
|
|
240
376
|
requestedActivationRoot: serviceIdentity.requestedActivationRoot,
|
|
@@ -246,6 +382,7 @@ function inspectManagedLearnerServiceInternal(activationRoot) {
|
|
|
246
382
|
running: info.running,
|
|
247
383
|
pid: info.pid,
|
|
248
384
|
configuredActivationRoot,
|
|
385
|
+
...describeDaemonProgramArguments(configuredProgramArguments),
|
|
249
386
|
matchesRequestedActivationRoot: configuredActivationRoot === null
|
|
250
387
|
? null
|
|
251
388
|
: canonicalizeActivationRoot(configuredActivationRoot) === serviceIdentity.canonicalActivationRoot,
|
|
@@ -262,11 +399,11 @@ function startManagedLearnerService(activationRoot) {
|
|
|
262
399
|
inspection: inspectionBeforeStart
|
|
263
400
|
};
|
|
264
401
|
}
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
402
|
+
const launchSpec = resolveDaemonProgramArguments();
|
|
403
|
+
if (launchSpec === null) {
|
|
267
404
|
return {
|
|
268
405
|
ok: false,
|
|
269
|
-
message: "Failed to resolve an OpenClawBrain CLI launch command.",
|
|
406
|
+
message: "Failed to resolve an OpenClawBrain CLI launch command without pinning an npx cache path.",
|
|
270
407
|
inspection: inspectionBeforeStart
|
|
271
408
|
};
|
|
272
409
|
}
|
|
@@ -275,7 +412,7 @@ function startManagedLearnerService(activationRoot) {
|
|
|
275
412
|
mkdirSync(launchAgentsDir, { recursive: true });
|
|
276
413
|
}
|
|
277
414
|
ensureLogDir(serviceIdentity.logPath);
|
|
278
|
-
const plistContent = buildPlistXml(serviceIdentity, programArguments);
|
|
415
|
+
const plistContent = buildPlistXml(serviceIdentity, launchSpec.programArguments);
|
|
279
416
|
writeFileSync(inspectionBeforeStart.plistPath, plistContent, "utf8");
|
|
280
417
|
const result = launchctlLoad(inspectionBeforeStart.plistPath);
|
|
281
418
|
if (!result.ok && !inspectionBeforeStart.installed) {
|
|
@@ -347,7 +484,7 @@ export function ensureManagedLearnerServiceForActivationRoot(activationRoot) {
|
|
|
347
484
|
}
|
|
348
485
|
const reason = !inspection.launchctlAvailable
|
|
349
486
|
? "launchctl_unavailable"
|
|
350
|
-
: startResult.message === "Failed to resolve an OpenClawBrain CLI launch command."
|
|
487
|
+
: startResult.message === "Failed to resolve an OpenClawBrain CLI launch command without pinning an npx cache path."
|
|
351
488
|
? "launch_command_unavailable"
|
|
352
489
|
: "launch_failed";
|
|
353
490
|
return {
|
|
@@ -419,19 +556,39 @@ function readOptionalJsonFile(filePath) {
|
|
|
419
556
|
return null;
|
|
420
557
|
}
|
|
421
558
|
}
|
|
422
|
-
function
|
|
559
|
+
function readDaemonProgramArguments(plistPath) {
|
|
423
560
|
if (!existsSync(plistPath)) {
|
|
424
561
|
return null;
|
|
425
562
|
}
|
|
426
563
|
try {
|
|
427
564
|
const plist = readFileSync(plistPath, "utf8");
|
|
428
|
-
const
|
|
429
|
-
|
|
565
|
+
const sectionMatch = plist.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
|
|
566
|
+
if (sectionMatch === null) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
const programArguments = [];
|
|
570
|
+
const stringPattern = /<string>([\s\S]*?)<\/string>/g;
|
|
571
|
+
let match = stringPattern.exec(sectionMatch[1]);
|
|
572
|
+
while (match !== null) {
|
|
573
|
+
programArguments.push(unescapePlistString(match[1]));
|
|
574
|
+
match = stringPattern.exec(sectionMatch[1]);
|
|
575
|
+
}
|
|
576
|
+
return programArguments.length > 0 ? programArguments : null;
|
|
430
577
|
}
|
|
431
578
|
catch {
|
|
432
579
|
return null;
|
|
433
580
|
}
|
|
434
581
|
}
|
|
582
|
+
function readDaemonActivationRoot(plistPath) {
|
|
583
|
+
const programArguments = readDaemonProgramArguments(plistPath);
|
|
584
|
+
if (programArguments !== null) {
|
|
585
|
+
const activationRootIndex = programArguments.indexOf("--activation-root");
|
|
586
|
+
if (activationRootIndex !== -1) {
|
|
587
|
+
return programArguments[activationRootIndex + 1] ?? null;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
435
592
|
function getWatchStatePaths(activationRoot) {
|
|
436
593
|
if (activationRoot === null) {
|
|
437
594
|
return {
|
|
@@ -611,9 +768,9 @@ export function daemonStart(activationRoot, json) {
|
|
|
611
768
|
const serviceIdentity = buildDaemonServiceIdentity(activationRoot);
|
|
612
769
|
const plistPath = serviceIdentity.plistPath;
|
|
613
770
|
const logPath = serviceIdentity.logPath;
|
|
614
|
-
const
|
|
615
|
-
if (
|
|
616
|
-
const message = "Failed to resolve an OpenClawBrain CLI launch command. Install/build
|
|
771
|
+
const launchSpec = resolveDaemonProgramArguments();
|
|
772
|
+
if (launchSpec === null) {
|
|
773
|
+
const message = "Failed to resolve an OpenClawBrain CLI launch command without pinning an npx cache path. Install/build @openclawbrain/cli or use a durable repo/runtime checkout.";
|
|
617
774
|
if (json) {
|
|
618
775
|
console.log(JSON.stringify({
|
|
619
776
|
command: "daemon start",
|
|
@@ -630,6 +787,8 @@ export function daemonStart(activationRoot, json) {
|
|
|
630
787
|
}
|
|
631
788
|
return 1;
|
|
632
789
|
}
|
|
790
|
+
const daemonProgramArguments = buildDaemonLaunchProgramArguments(serviceIdentity, launchSpec.programArguments);
|
|
791
|
+
const daemonLaunchDescription = describeDaemonProgramArguments(daemonProgramArguments);
|
|
633
792
|
// Ensure LaunchAgents dir exists
|
|
634
793
|
const launchAgentsDir = path.dirname(plistPath);
|
|
635
794
|
if (!existsSync(launchAgentsDir)) {
|
|
@@ -637,7 +796,7 @@ export function daemonStart(activationRoot, json) {
|
|
|
637
796
|
}
|
|
638
797
|
ensureLogDir(logPath);
|
|
639
798
|
// Write the plist
|
|
640
|
-
const plistContent = buildPlistXml(serviceIdentity, programArguments);
|
|
799
|
+
const plistContent = buildPlistXml(serviceIdentity, launchSpec.programArguments);
|
|
641
800
|
writeFileSync(plistPath, plistContent, "utf8");
|
|
642
801
|
// Load it
|
|
643
802
|
const result = launchctlLoad(plistPath);
|
|
@@ -649,6 +808,7 @@ export function daemonStart(activationRoot, json) {
|
|
|
649
808
|
logPath,
|
|
650
809
|
activationRoot: serviceIdentity.requestedActivationRoot,
|
|
651
810
|
serviceLabel: serviceIdentity.label,
|
|
811
|
+
...daemonLaunchDescription,
|
|
652
812
|
message: result.message,
|
|
653
813
|
}, null, 2));
|
|
654
814
|
}
|
|
@@ -659,6 +819,15 @@ export function daemonStart(activationRoot, json) {
|
|
|
659
819
|
console.log(` Plist: ${plistPath}`);
|
|
660
820
|
console.log(` Log: ${logPath}`);
|
|
661
821
|
console.log(` Root: ${serviceIdentity.requestedActivationRoot}`);
|
|
822
|
+
if (daemonLaunchDescription.configuredRuntimePath !== null) {
|
|
823
|
+
const runtimePackageSuffix = daemonLaunchDescription.configuredRuntimePackageSpec === null
|
|
824
|
+
? ""
|
|
825
|
+
: ` (${daemonLaunchDescription.configuredRuntimePackageSpec})`;
|
|
826
|
+
console.log(` Runtime: ${daemonLaunchDescription.configuredRuntimePath}${runtimePackageSuffix}`);
|
|
827
|
+
}
|
|
828
|
+
if (daemonLaunchDescription.configuredCommand !== null) {
|
|
829
|
+
console.log(` Command: ${daemonLaunchDescription.configuredCommand}`);
|
|
830
|
+
}
|
|
662
831
|
}
|
|
663
832
|
else {
|
|
664
833
|
console.error(`✗ ${result.message}`);
|
|
@@ -725,12 +894,14 @@ export function daemonStatus(activationRoot, json) {
|
|
|
725
894
|
const info = getLaunchctlInfo(serviceIdentity.label);
|
|
726
895
|
const lastLogLines = readLastLines(logPath, 5);
|
|
727
896
|
const configuredActivationRoot = readDaemonActivationRoot(plistPath);
|
|
897
|
+
const configuredProgramArguments = readDaemonProgramArguments(plistPath);
|
|
728
898
|
const requestedActivationRoot = serviceIdentity.requestedActivationRoot;
|
|
729
899
|
const watchStatePaths = getWatchStatePaths(requestedActivationRoot);
|
|
730
900
|
const watchState = readWatchStateSummary(requestedActivationRoot);
|
|
731
901
|
const matchesRequestedActivationRoot = configuredActivationRoot === null
|
|
732
902
|
? null
|
|
733
903
|
: canonicalizeActivationRoot(configuredActivationRoot) === serviceIdentity.canonicalActivationRoot;
|
|
904
|
+
const daemonLaunchDescription = describeDaemonProgramArguments(configuredProgramArguments);
|
|
734
905
|
if (json) {
|
|
735
906
|
console.log(JSON.stringify({
|
|
736
907
|
command: "daemon status",
|
|
@@ -742,6 +913,7 @@ export function daemonStatus(activationRoot, json) {
|
|
|
742
913
|
logPath,
|
|
743
914
|
activationRoot: requestedActivationRoot,
|
|
744
915
|
configuredActivationRoot,
|
|
916
|
+
...daemonLaunchDescription,
|
|
745
917
|
matchesRequestedActivationRoot,
|
|
746
918
|
...watchStatePaths,
|
|
747
919
|
watchState,
|
|
@@ -766,6 +938,22 @@ export function daemonStatus(activationRoot, json) {
|
|
|
766
938
|
if (matchesRequestedActivationRoot === false) {
|
|
767
939
|
console.log(" Requested root does not match the installed daemon plist.");
|
|
768
940
|
}
|
|
941
|
+
if (daemonLaunchDescription.configuredRuntimePath !== null) {
|
|
942
|
+
const runtimePackageSuffix = daemonLaunchDescription.configuredRuntimePackageSpec === null
|
|
943
|
+
? ""
|
|
944
|
+
: ` (${daemonLaunchDescription.configuredRuntimePackageSpec})`;
|
|
945
|
+
const runtimeWarning = daemonLaunchDescription.configuredRuntimeLooksEphemeral ? " [ephemeral]" : "";
|
|
946
|
+
console.log(` Runtime: ${daemonLaunchDescription.configuredRuntimePath}${runtimePackageSuffix}${runtimeWarning}`);
|
|
947
|
+
}
|
|
948
|
+
if (configuredProgramArguments !== null && configuredProgramArguments.length > 0) {
|
|
949
|
+
console.log(` Program: ${configuredProgramArguments[0]}`);
|
|
950
|
+
if (configuredProgramArguments.length > 1) {
|
|
951
|
+
console.log(` Args: ${configuredProgramArguments.slice(1).map((argument) => formatCommandArgument(argument)).join(" ")}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (daemonLaunchDescription.configuredCommand !== null) {
|
|
955
|
+
console.log(` Command: ${daemonLaunchDescription.configuredCommand}`);
|
|
956
|
+
}
|
|
769
957
|
console.log(` Log: ${logPath}`);
|
|
770
958
|
if (watchState.scanRoot !== null) {
|
|
771
959
|
console.log(` Scan root: ${watchState.scanRoot}`);
|
|
@@ -904,7 +1092,7 @@ export function daemonHelp() {
|
|
|
904
1092
|
"Subcommands:",
|
|
905
1093
|
" start Generate a macOS launchd plist and start the daemon (runs openclawbrain watch).",
|
|
906
1094
|
" stop Stop the daemon and remove the launchd plist.",
|
|
907
|
-
" status Show whether the daemon is running, its PID, and recent log lines.",
|
|
1095
|
+
" status Show whether the daemon is running, its PID, configured launch command, and recent log lines.",
|
|
908
1096
|
" logs Show the last 50 lines of the per-activation-root daemon log under ~/.openclawbrain/daemon/.",
|
|
909
1097
|
"",
|
|
910
1098
|
"Options:",
|
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_OPERATOR_PROOF_PLUGIN_ID = "openclawbrain";
|
|
8
|
+
export const DEFAULT_OPERATOR_PROOF_TIMEOUT_MS = 120_000;
|
|
9
|
+
|
|
10
|
+
function quoteShellArg(value) {
|
|
11
|
+
return `'${value.replace(/'/g, `"'"'`)}'`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeOptionalCliString(value) {
|
|
15
|
+
if (typeof value !== "string") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function canonicalizeExistingProofPath(filePath) {
|
|
23
|
+
const resolvedPath = path.resolve(filePath);
|
|
24
|
+
try {
|
|
25
|
+
return realpathSync(resolvedPath);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return resolvedPath;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shellJoin(parts) {
|
|
33
|
+
return parts
|
|
34
|
+
.map((part) => {
|
|
35
|
+
if (/^[A-Za-z0-9_./:@=-]+$/.test(part)) {
|
|
36
|
+
return part;
|
|
37
|
+
}
|
|
38
|
+
return JSON.stringify(part);
|
|
39
|
+
})
|
|
40
|
+
.join(" ");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function timestampToken(date = new Date()) {
|
|
44
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z").replace("T", "-");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveProofOutputDir(options) {
|
|
48
|
+
if (options.outputDir !== null) {
|
|
49
|
+
return path.resolve(options.outputDir);
|
|
50
|
+
}
|
|
51
|
+
return path.resolve(options.cwd ?? process.cwd(), "artifacts", `operator-proof-${timestampToken()}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeText(filePath, text) {
|
|
55
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
56
|
+
writeFileSync(filePath, text, "utf8");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeJson(filePath, value) {
|
|
60
|
+
writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildCurrentCliInvocation(cliEntryPath = process.argv[1]) {
|
|
64
|
+
const normalizedEntryPath = normalizeOptionalCliString(cliEntryPath);
|
|
65
|
+
if (normalizedEntryPath === null) {
|
|
66
|
+
return {
|
|
67
|
+
command: "openclawbrain",
|
|
68
|
+
args: []
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
command: process.execPath,
|
|
73
|
+
args: [path.resolve(normalizedEntryPath)]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function defaultRunCapture(command, args, options = {}) {
|
|
78
|
+
const startedAt = new Date();
|
|
79
|
+
const result = spawnSync(command, args, {
|
|
80
|
+
cwd: options.cwd ?? process.cwd(),
|
|
81
|
+
env: options.env ?? process.env,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
timeout: options.timeoutMs,
|
|
84
|
+
});
|
|
85
|
+
const endedAt = new Date();
|
|
86
|
+
return {
|
|
87
|
+
label: options.label ?? command,
|
|
88
|
+
command,
|
|
89
|
+
argv: args,
|
|
90
|
+
shellCommand: shellJoin([command, ...args]),
|
|
91
|
+
startedAt: startedAt.toISOString(),
|
|
92
|
+
endedAt: endedAt.toISOString(),
|
|
93
|
+
durationMs: endedAt.getTime() - startedAt.getTime(),
|
|
94
|
+
exitCode: typeof result.status === "number" ? result.status : null,
|
|
95
|
+
signal: result.signal ?? null,
|
|
96
|
+
stdout: result.stdout ?? "",
|
|
97
|
+
stderr: result.stderr ?? "",
|
|
98
|
+
error: result.error ? String(result.error) : null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function summarizeCapture(step) {
|
|
103
|
+
let resultClass = "unknown";
|
|
104
|
+
if (step.exitCode === 0 && !step.error) {
|
|
105
|
+
resultClass = "success";
|
|
106
|
+
}
|
|
107
|
+
else if (step.signal === "SIGTERM" || step.signal === "SIGKILL") {
|
|
108
|
+
resultClass = "interrupted";
|
|
109
|
+
}
|
|
110
|
+
else if (step.error && /timed out/i.test(step.error)) {
|
|
111
|
+
resultClass = "timed_out";
|
|
112
|
+
}
|
|
113
|
+
else if (step.exitCode !== 0 || step.error) {
|
|
114
|
+
resultClass = "command_failed";
|
|
115
|
+
}
|
|
116
|
+
const captureState = step.exitCode === null && !step.stdout && !step.stderr
|
|
117
|
+
? "missing"
|
|
118
|
+
: step.exitCode === null
|
|
119
|
+
? "partial"
|
|
120
|
+
: "complete";
|
|
121
|
+
return { resultClass, captureState };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeStepBundle(bundleDir, stepId, capture) {
|
|
125
|
+
const stdoutName = `${stepId}.stdout.log`;
|
|
126
|
+
const stderrName = `${stepId}.stderr.log`;
|
|
127
|
+
writeText(path.join(bundleDir, stdoutName), capture.stdout);
|
|
128
|
+
writeText(path.join(bundleDir, stderrName), capture.stderr);
|
|
129
|
+
return { stdoutName, stderrName };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractGatewayLogPath(text) {
|
|
133
|
+
const match = text.match(/^File logs:\s+(.+)$/m);
|
|
134
|
+
return match ? match[1].trim() : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function extractActivationRoot(statusText, override) {
|
|
138
|
+
if (override !== null) {
|
|
139
|
+
return path.resolve(override);
|
|
140
|
+
}
|
|
141
|
+
const targetMatch = statusText.match(/^target\s+activation=([^\s]+)\s+/m);
|
|
142
|
+
if (targetMatch) {
|
|
143
|
+
return targetMatch[1].trim();
|
|
144
|
+
}
|
|
145
|
+
const hostMatch = statusText.match(/^host\s+runtime=[^\s]+\s+activation=([^\s]+)$/m);
|
|
146
|
+
if (hostMatch) {
|
|
147
|
+
return hostMatch[1].trim();
|
|
148
|
+
}
|
|
149
|
+
return path.join(homedir(), ".openclawbrain", "activation");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readTextIfExists(filePath) {
|
|
153
|
+
if (filePath === null || !existsSync(filePath)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return readFileSync(filePath, "utf8");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function readJsonSnapshot(filePath) {
|
|
160
|
+
if (filePath === null) {
|
|
161
|
+
return {
|
|
162
|
+
path: null,
|
|
163
|
+
exists: false,
|
|
164
|
+
error: "proof path unresolved",
|
|
165
|
+
value: null
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (!existsSync(filePath)) {
|
|
169
|
+
return {
|
|
170
|
+
path: filePath,
|
|
171
|
+
exists: false,
|
|
172
|
+
error: null,
|
|
173
|
+
value: null
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
return {
|
|
178
|
+
path: filePath,
|
|
179
|
+
exists: true,
|
|
180
|
+
error: null,
|
|
181
|
+
value: JSON.parse(readFileSync(filePath, "utf8"))
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
return {
|
|
186
|
+
path: filePath,
|
|
187
|
+
exists: true,
|
|
188
|
+
error: error instanceof Error ? error.message : String(error),
|
|
189
|
+
value: null
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function extractStartupBreadcrumbs(logText, bundleStartedAtIso) {
|
|
195
|
+
if (!logText) {
|
|
196
|
+
return { all: [], afterBundleStart: [] };
|
|
197
|
+
}
|
|
198
|
+
const bundleStartMs = Date.parse(bundleStartedAtIso);
|
|
199
|
+
const out = [];
|
|
200
|
+
for (const line of logText.split(/\r?\n/)) {
|
|
201
|
+
if (!line.includes("[openclawbrain] BRAIN LOADED") && !line.includes("[openclawbrain] BRAIN NOT YET LOADED")) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
let parsed = null;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(line);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
parsed = null;
|
|
210
|
+
}
|
|
211
|
+
const timestamp = parsed?._meta?.date ?? parsed?.time ?? null;
|
|
212
|
+
out.push({
|
|
213
|
+
line,
|
|
214
|
+
timestamp,
|
|
215
|
+
kind: line.includes("BRAIN LOADED") ? "loaded" : "not_yet_loaded",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
all: out,
|
|
220
|
+
afterBundleStart: out.filter((entry) => entry.timestamp && Date.parse(entry.timestamp) >= bundleStartMs),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function extractStatusSignals(statusText) {
|
|
225
|
+
return {
|
|
226
|
+
statusOk: /^STATUS ok$/m.test(statusText),
|
|
227
|
+
loadProofReady: /loadProof=status_probe_ready/.test(statusText),
|
|
228
|
+
runtimeProven: /attachTruth .*runtime=proven/.test(statusText),
|
|
229
|
+
pluginInstalled: /hook\s+install=installed/.test(statusText),
|
|
230
|
+
serveActivePack: /serve\s+state=serving_active_pack/.test(statusText),
|
|
231
|
+
routeFnAvailable: /routeFn\s+available=yes/.test(statusText),
|
|
232
|
+
proofPath: statusText.match(/proofPath=([^\s]+)/)?.[1] ?? null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function hasPackagedHookSource(pluginInspectText) {
|
|
237
|
+
return /Source:\s+.*(?:@openclawbrain[\\/]+openclaw|openclawbrain)[\\/]+dist[\\/]+extension[\\/]+index\.js/m.test(pluginInspectText);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, breadcrumbs, runtimeLoadProofSnapshot, openclawHome }) {
|
|
241
|
+
const failedStep = steps.find((step) => step.resultClass !== "success" && step.skipped !== true);
|
|
242
|
+
if (failedStep) {
|
|
243
|
+
return {
|
|
244
|
+
verdict: "command_failed",
|
|
245
|
+
severity: "blocking",
|
|
246
|
+
why: `${failedStep.stepId} exited as ${failedStep.resultClass}`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const gatewayHealthy = /Runtime:\s+running/m.test(gatewayStatus) && /RPC probe:\s+ok/m.test(gatewayStatus);
|
|
250
|
+
const pluginLoaded = /Status:\s+loaded/m.test(pluginInspect);
|
|
251
|
+
const packagedHookPath = hasPackagedHookSource(pluginInspect);
|
|
252
|
+
const breadcrumbLoaded = breadcrumbs.afterBundleStart.some((entry) => entry.kind === "loaded");
|
|
253
|
+
const runtimeProofMatched = Array.isArray(runtimeLoadProofSnapshot?.value?.profiles)
|
|
254
|
+
&& runtimeLoadProofSnapshot.value.profiles.some((profile) => canonicalizeExistingProofPath(profile?.openclawHome ?? "") === canonicalizeExistingProofPath(openclawHome));
|
|
255
|
+
const missingProofs = [];
|
|
256
|
+
if (!gatewayHealthy)
|
|
257
|
+
missingProofs.push("gateway_health");
|
|
258
|
+
if (!pluginLoaded)
|
|
259
|
+
missingProofs.push("plugin_loaded");
|
|
260
|
+
if (!packagedHookPath)
|
|
261
|
+
missingProofs.push("packaged_hook_path");
|
|
262
|
+
if (!statusSignals.statusOk)
|
|
263
|
+
missingProofs.push("status_ok");
|
|
264
|
+
if (!statusSignals.loadProofReady)
|
|
265
|
+
missingProofs.push("load_proof");
|
|
266
|
+
if (!statusSignals.runtimeProven)
|
|
267
|
+
missingProofs.push("runtime_proven");
|
|
268
|
+
if (!statusSignals.serveActivePack)
|
|
269
|
+
missingProofs.push("serve_active_pack");
|
|
270
|
+
if (!statusSignals.routeFnAvailable)
|
|
271
|
+
missingProofs.push("route_fn");
|
|
272
|
+
if (!breadcrumbLoaded)
|
|
273
|
+
missingProofs.push("startup_breadcrumb");
|
|
274
|
+
if (!runtimeProofMatched)
|
|
275
|
+
missingProofs.push("runtime_load_proof_record");
|
|
276
|
+
if (missingProofs.length === 0) {
|
|
277
|
+
return {
|
|
278
|
+
verdict: "success_and_proven",
|
|
279
|
+
severity: "none",
|
|
280
|
+
why: "install, restart, gateway health, plugin load, startup breadcrumb, runtime-load-proof record, and detailed status all aligned",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const blocking = missingProofs.some((item) => [
|
|
284
|
+
"gateway_health",
|
|
285
|
+
"plugin_loaded",
|
|
286
|
+
"packaged_hook_path",
|
|
287
|
+
"status_ok",
|
|
288
|
+
"load_proof",
|
|
289
|
+
"runtime_proven",
|
|
290
|
+
"serve_active_pack",
|
|
291
|
+
"route_fn",
|
|
292
|
+
].includes(item));
|
|
293
|
+
return {
|
|
294
|
+
verdict: blocking ? "degraded_or_failed_proof" : "success_but_proof_incomplete",
|
|
295
|
+
severity: blocking ? "blocking" : "degraded",
|
|
296
|
+
why: `missing or conflicting proofs: ${missingProofs.join(", ")}`,
|
|
297
|
+
missingProofs,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildSummary({ options, steps, verdict, gatewayStatusText, pluginInspectText, statusSignals, breadcrumbs, runtimeLoadProofSnapshot }) {
|
|
302
|
+
const passed = [];
|
|
303
|
+
const missing = [];
|
|
304
|
+
if (steps.find((step) => step.stepId === "01-install")?.resultClass === "success") {
|
|
305
|
+
passed.push("install command succeeded");
|
|
306
|
+
}
|
|
307
|
+
if (steps.find((step) => step.stepId === "02-restart")?.skipped === true || steps.find((step) => step.stepId === "02-restart")?.resultClass === "success") {
|
|
308
|
+
passed.push("restart step completed or was intentionally skipped");
|
|
309
|
+
}
|
|
310
|
+
if (/Runtime:\s+running/m.test(gatewayStatusText) && /RPC probe:\s+ok/m.test(gatewayStatusText)) {
|
|
311
|
+
passed.push("gateway status showed runtime running and RPC probe ok");
|
|
312
|
+
}
|
|
313
|
+
if (/Status:\s+loaded/m.test(pluginInspectText)) {
|
|
314
|
+
passed.push("plugin inspect showed OpenClawBrain loaded");
|
|
315
|
+
}
|
|
316
|
+
if (statusSignals.statusOk) {
|
|
317
|
+
passed.push("detailed status returned STATUS ok");
|
|
318
|
+
}
|
|
319
|
+
if (statusSignals.loadProofReady) {
|
|
320
|
+
passed.push("detailed status reported loadProof=status_probe_ready");
|
|
321
|
+
}
|
|
322
|
+
if (statusSignals.serveActivePack) {
|
|
323
|
+
passed.push("detailed status reported serve state=serving_active_pack");
|
|
324
|
+
}
|
|
325
|
+
if (statusSignals.routeFnAvailable) {
|
|
326
|
+
passed.push("detailed status reported routeFn available=yes");
|
|
327
|
+
}
|
|
328
|
+
if (breadcrumbs.afterBundleStart.some((entry) => entry.kind === "loaded")) {
|
|
329
|
+
passed.push("startup log contained a post-bundle [openclawbrain] BRAIN LOADED breadcrumb");
|
|
330
|
+
}
|
|
331
|
+
if (!statusSignals.loadProofReady)
|
|
332
|
+
missing.push("detailed status did not prove hook load");
|
|
333
|
+
if (!breadcrumbs.afterBundleStart.some((entry) => entry.kind === "loaded"))
|
|
334
|
+
missing.push("no post-bundle startup breadcrumb was found");
|
|
335
|
+
if (runtimeLoadProofSnapshot.path === null)
|
|
336
|
+
missing.push("runtime-load-proof path could not be resolved");
|
|
337
|
+
if (runtimeLoadProofSnapshot.error !== null)
|
|
338
|
+
missing.push(`runtime-load-proof snapshot was unreadable: ${runtimeLoadProofSnapshot.error}`);
|
|
339
|
+
const lines = [
|
|
340
|
+
"# OpenClawBrain operator proof summary",
|
|
341
|
+
"",
|
|
342
|
+
`- openclaw home: \`${options.openclawHome}\``,
|
|
343
|
+
`- bundle verdict: **${verdict.verdict}**`,
|
|
344
|
+
`- severity: **${verdict.severity}**`,
|
|
345
|
+
`- why: ${verdict.why}`,
|
|
346
|
+
"",
|
|
347
|
+
"## Passed",
|
|
348
|
+
...passed.map((item) => `- ${item}`),
|
|
349
|
+
"",
|
|
350
|
+
"## Missing / incomplete",
|
|
351
|
+
...(missing.length === 0 ? ["- none"] : missing.map((item) => `- ${item}`)),
|
|
352
|
+
"",
|
|
353
|
+
"## Step ledger",
|
|
354
|
+
...steps.map((step) => `- ${step.stepId}: ${step.skipped ? "skipped" : `${step.resultClass} (${step.captureState})`} - ${step.summary}`),
|
|
355
|
+
];
|
|
356
|
+
if (runtimeLoadProofSnapshot.path !== null) {
|
|
357
|
+
lines.push("", "## Runtime proof file", `- ${runtimeLoadProofSnapshot.path}`);
|
|
358
|
+
}
|
|
359
|
+
return `${lines.join("\n")}\n`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function readOpenClawProfileName(openclawHome) {
|
|
363
|
+
try {
|
|
364
|
+
const openclawJsonPath = path.join(openclawHome, "openclaw.json");
|
|
365
|
+
if (!existsSync(openclawJsonPath)) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const parsed = JSON.parse(readFileSync(openclawJsonPath, "utf8"));
|
|
369
|
+
return normalizeOptionalCliString(parsed?.profile);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function buildGatewayArgs(action, profileName) {
|
|
377
|
+
return profileName === null
|
|
378
|
+
? ["gateway", action]
|
|
379
|
+
: ["gateway", action, "--profile", profileName];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function buildProofCommandForOpenClawHome(openclawHome) {
|
|
383
|
+
return `openclawbrain proof --openclaw-home ${quoteShellArg(path.resolve(openclawHome))}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function buildProofCommandHelpSection() {
|
|
387
|
+
return {
|
|
388
|
+
usage: " openclawbrain proof --openclaw-home <path> [options]",
|
|
389
|
+
optionLines: [
|
|
390
|
+
" --output-dir <path> Bundle directory for proof artifacts (proof only). Defaults to ./artifacts/operator-proof-<timestamp>.",
|
|
391
|
+
" --skip-install Capture proof without rerunning install first (proof only).",
|
|
392
|
+
" --skip-restart Capture proof without restarting OpenClaw first (proof only).",
|
|
393
|
+
` --plugin-id <id> Plugin id for \`openclaw plugins inspect\` (proof only; default: ${DEFAULT_OPERATOR_PROOF_PLUGIN_ID}).`,
|
|
394
|
+
` --timeout-ms <ms> Per-step timeout in ms for proof capture (proof only; default: ${DEFAULT_OPERATOR_PROOF_TIMEOUT_MS}).`,
|
|
395
|
+
],
|
|
396
|
+
lifecycle: " 5. proof openclawbrain proof --openclaw-home <path> - capture one durable operator proof bundle after install/restart/status",
|
|
397
|
+
advanced: " proof capture one durable operator proof bundle with step logs, startup breadcrumbs, and a runtime-load-proof snapshot",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function parseProofCliArgs(argv, options = {}) {
|
|
402
|
+
const existsSyncImpl = options.existsSyncImpl ?? existsSync;
|
|
403
|
+
let openclawHome = null;
|
|
404
|
+
let activationRoot = null;
|
|
405
|
+
let outputDir = null;
|
|
406
|
+
let skipInstall = false;
|
|
407
|
+
let skipRestart = false;
|
|
408
|
+
let pluginId = DEFAULT_OPERATOR_PROOF_PLUGIN_ID;
|
|
409
|
+
let timeoutMs = DEFAULT_OPERATOR_PROOF_TIMEOUT_MS;
|
|
410
|
+
let json = false;
|
|
411
|
+
let help = false;
|
|
412
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
413
|
+
const arg = argv[index];
|
|
414
|
+
if (arg === "--help" || arg === "-h") {
|
|
415
|
+
help = true;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (arg === "--json") {
|
|
419
|
+
json = true;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (arg === "--skip-install") {
|
|
423
|
+
skipInstall = true;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (arg === "--skip-restart") {
|
|
427
|
+
skipRestart = true;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (arg === "--openclaw-home") {
|
|
431
|
+
const next = argv[index + 1];
|
|
432
|
+
if (next === undefined) {
|
|
433
|
+
throw new Error("--openclaw-home requires a value");
|
|
434
|
+
}
|
|
435
|
+
openclawHome = next;
|
|
436
|
+
index += 1;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (arg === "--activation-root") {
|
|
440
|
+
const next = argv[index + 1];
|
|
441
|
+
if (next === undefined) {
|
|
442
|
+
throw new Error("--activation-root requires a value");
|
|
443
|
+
}
|
|
444
|
+
activationRoot = next;
|
|
445
|
+
index += 1;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (arg === "--output-dir") {
|
|
449
|
+
const next = argv[index + 1];
|
|
450
|
+
if (next === undefined) {
|
|
451
|
+
throw new Error("--output-dir requires a value");
|
|
452
|
+
}
|
|
453
|
+
outputDir = next;
|
|
454
|
+
index += 1;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (arg === "--plugin-id") {
|
|
458
|
+
const next = argv[index + 1];
|
|
459
|
+
if (next === undefined) {
|
|
460
|
+
throw new Error("--plugin-id requires a value");
|
|
461
|
+
}
|
|
462
|
+
pluginId = next;
|
|
463
|
+
index += 1;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (arg === "--timeout-ms") {
|
|
467
|
+
const next = argv[index + 1];
|
|
468
|
+
if (next === undefined) {
|
|
469
|
+
throw new Error("--timeout-ms requires a value");
|
|
470
|
+
}
|
|
471
|
+
const parsed = Number.parseInt(next, 10);
|
|
472
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
473
|
+
throw new Error("--timeout-ms must be a positive integer");
|
|
474
|
+
}
|
|
475
|
+
timeoutMs = parsed;
|
|
476
|
+
index += 1;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
throw new Error(`unknown argument for proof: ${arg}`);
|
|
480
|
+
}
|
|
481
|
+
if (help) {
|
|
482
|
+
return {
|
|
483
|
+
command: "proof",
|
|
484
|
+
openclawHome: "",
|
|
485
|
+
activationRoot: null,
|
|
486
|
+
outputDir: null,
|
|
487
|
+
skipInstall,
|
|
488
|
+
skipRestart,
|
|
489
|
+
pluginId,
|
|
490
|
+
timeoutMs,
|
|
491
|
+
json,
|
|
492
|
+
help
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
if (openclawHome === null) {
|
|
496
|
+
throw new Error("proof requires --openclaw-home <path>");
|
|
497
|
+
}
|
|
498
|
+
const resolvedOpenClawHome = path.resolve(openclawHome);
|
|
499
|
+
if (!existsSyncImpl(resolvedOpenClawHome)) {
|
|
500
|
+
throw new Error(`--openclaw-home directory does not exist: ${resolvedOpenClawHome}`);
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
command: "proof",
|
|
504
|
+
openclawHome: resolvedOpenClawHome,
|
|
505
|
+
activationRoot: activationRoot === null ? null : path.resolve(activationRoot),
|
|
506
|
+
outputDir: outputDir === null ? null : path.resolve(outputDir),
|
|
507
|
+
skipInstall,
|
|
508
|
+
skipRestart,
|
|
509
|
+
pluginId,
|
|
510
|
+
timeoutMs,
|
|
511
|
+
json,
|
|
512
|
+
help
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function captureOperatorProofBundle(options) {
|
|
517
|
+
const cliInvocation = options.cliInvocation ?? buildCurrentCliInvocation();
|
|
518
|
+
const runCapture = options.runCapture ?? defaultRunCapture;
|
|
519
|
+
const bundleStartedAt = new Date().toISOString();
|
|
520
|
+
const bundleDir = resolveProofOutputDir(options);
|
|
521
|
+
mkdirSync(bundleDir, { recursive: true });
|
|
522
|
+
const steps = [];
|
|
523
|
+
const gatewayProfile = readOpenClawProfileName(options.openclawHome);
|
|
524
|
+
function addStep(stepId, label, command, args, { skipped = false } = {}) {
|
|
525
|
+
if (skipped) {
|
|
526
|
+
steps.push({
|
|
527
|
+
stepId,
|
|
528
|
+
label,
|
|
529
|
+
shellCommand: shellJoin([command, ...args]),
|
|
530
|
+
skipped: true,
|
|
531
|
+
captureState: "complete",
|
|
532
|
+
resultClass: "success",
|
|
533
|
+
summary: "step intentionally skipped",
|
|
534
|
+
stdoutPath: null,
|
|
535
|
+
stderrPath: null,
|
|
536
|
+
});
|
|
537
|
+
return { stdout: "", stderr: "", exitCode: 0, signal: null, error: null };
|
|
538
|
+
}
|
|
539
|
+
const capture = runCapture(command, args, {
|
|
540
|
+
label,
|
|
541
|
+
cwd: options.cwd ?? process.cwd(),
|
|
542
|
+
env: options.env ?? process.env,
|
|
543
|
+
timeoutMs: options.timeoutMs,
|
|
544
|
+
});
|
|
545
|
+
const { stdoutName, stderrName } = writeStepBundle(bundleDir, stepId, capture);
|
|
546
|
+
const summary = summarizeCapture(capture);
|
|
547
|
+
steps.push({
|
|
548
|
+
stepId,
|
|
549
|
+
label,
|
|
550
|
+
shellCommand: capture.shellCommand ?? shellJoin([command, ...args]),
|
|
551
|
+
startedAt: capture.startedAt ?? null,
|
|
552
|
+
endedAt: capture.endedAt ?? null,
|
|
553
|
+
durationMs: capture.durationMs ?? null,
|
|
554
|
+
exitCode: capture.exitCode ?? null,
|
|
555
|
+
signal: capture.signal ?? null,
|
|
556
|
+
resultClass: summary.resultClass,
|
|
557
|
+
captureState: summary.captureState,
|
|
558
|
+
summary: summary.resultClass === "success"
|
|
559
|
+
? `${label} completed successfully`
|
|
560
|
+
: `${label} ended as ${summary.resultClass}`,
|
|
561
|
+
stdoutPath: stdoutName,
|
|
562
|
+
stderrPath: stderrName,
|
|
563
|
+
});
|
|
564
|
+
return capture;
|
|
565
|
+
}
|
|
566
|
+
addStep("01-install", "install", cliInvocation.command, [...cliInvocation.args, "install", "--openclaw-home", options.openclawHome], { skipped: options.skipInstall === true });
|
|
567
|
+
addStep("02-restart", "gateway restart", "openclaw", buildGatewayArgs("restart", gatewayProfile), { skipped: options.skipRestart === true });
|
|
568
|
+
const gatewayStatusCapture = addStep("03-gateway-status", "gateway status", "openclaw", buildGatewayArgs("status", gatewayProfile));
|
|
569
|
+
const pluginInspectCapture = addStep("04-plugin-inspect", "plugin inspect", "openclaw", ["plugins", "inspect", options.pluginId]);
|
|
570
|
+
const statusCapture = addStep("05-detailed-status", "detailed status", cliInvocation.command, [...cliInvocation.args, "status", "--openclaw-home", options.openclawHome, "--detailed"]);
|
|
571
|
+
const gatewayLogPath = extractGatewayLogPath(gatewayStatusCapture.stdout);
|
|
572
|
+
const activationRoot = extractActivationRoot(statusCapture.stdout, options.activationRoot ?? null);
|
|
573
|
+
const runtimeLoadProofPath = path.join(activationRoot, "attachment-truth", "runtime-load-proofs.json");
|
|
574
|
+
const runtimeLoadProofSnapshot = readJsonSnapshot(runtimeLoadProofPath);
|
|
575
|
+
const gatewayLogText = readTextIfExists(gatewayLogPath);
|
|
576
|
+
const breadcrumbs = extractStartupBreadcrumbs(gatewayLogText, bundleStartedAt);
|
|
577
|
+
const statusSignals = extractStatusSignals(statusCapture.stdout);
|
|
578
|
+
writeText(path.join(bundleDir, "extracted-startup-breadcrumbs.log"), breadcrumbs.all.length === 0
|
|
579
|
+
? "<no matching breadcrumbs found>\n"
|
|
580
|
+
: `${breadcrumbs.all.map((entry) => entry.line).join("\n")}\n`);
|
|
581
|
+
writeJson(path.join(bundleDir, "runtime-load-proof.json"), runtimeLoadProofSnapshot);
|
|
582
|
+
const verdict = buildVerdict({
|
|
583
|
+
steps,
|
|
584
|
+
gatewayStatus: gatewayStatusCapture.stdout,
|
|
585
|
+
pluginInspect: pluginInspectCapture.stdout,
|
|
586
|
+
statusSignals,
|
|
587
|
+
breadcrumbs,
|
|
588
|
+
runtimeLoadProofSnapshot,
|
|
589
|
+
openclawHome: options.openclawHome,
|
|
590
|
+
});
|
|
591
|
+
writeJson(path.join(bundleDir, "steps.json"), {
|
|
592
|
+
bundleStartedAt,
|
|
593
|
+
openclawHome: canonicalizeExistingProofPath(options.openclawHome),
|
|
594
|
+
activationRoot,
|
|
595
|
+
gatewayProfile,
|
|
596
|
+
gatewayLogPath,
|
|
597
|
+
steps,
|
|
598
|
+
});
|
|
599
|
+
writeJson(path.join(bundleDir, "verdict.json"), {
|
|
600
|
+
bundleStartedAt,
|
|
601
|
+
verdict,
|
|
602
|
+
statusSignals,
|
|
603
|
+
breadcrumbs: {
|
|
604
|
+
allCount: breadcrumbs.all.length,
|
|
605
|
+
postBundleCount: breadcrumbs.afterBundleStart.length,
|
|
606
|
+
postBundleKinds: breadcrumbs.afterBundleStart.map((entry) => entry.kind),
|
|
607
|
+
},
|
|
608
|
+
runtimeLoadProofPath,
|
|
609
|
+
runtimeLoadProofError: runtimeLoadProofSnapshot.error,
|
|
610
|
+
});
|
|
611
|
+
writeText(path.join(bundleDir, "summary.md"), buildSummary({
|
|
612
|
+
options,
|
|
613
|
+
steps,
|
|
614
|
+
verdict,
|
|
615
|
+
gatewayStatusText: gatewayStatusCapture.stdout,
|
|
616
|
+
pluginInspectText: pluginInspectCapture.stdout,
|
|
617
|
+
statusSignals,
|
|
618
|
+
breadcrumbs,
|
|
619
|
+
runtimeLoadProofSnapshot,
|
|
620
|
+
}));
|
|
621
|
+
return {
|
|
622
|
+
ok: true,
|
|
623
|
+
bundleDir,
|
|
624
|
+
bundleStartedAt,
|
|
625
|
+
activationRoot,
|
|
626
|
+
gatewayProfile,
|
|
627
|
+
gatewayLogPath,
|
|
628
|
+
runtimeLoadProofPath,
|
|
629
|
+
runtimeLoadProofSnapshot,
|
|
630
|
+
verdict,
|
|
631
|
+
statusSignals,
|
|
632
|
+
steps,
|
|
633
|
+
summaryPath: path.join(bundleDir, "summary.md"),
|
|
634
|
+
stepsPath: path.join(bundleDir, "steps.json"),
|
|
635
|
+
verdictPath: path.join(bundleDir, "verdict.json"),
|
|
636
|
+
breadcrumbPath: path.join(bundleDir, "extracted-startup-breadcrumbs.log"),
|
|
637
|
+
runtimeLoadProofSnapshotPath: path.join(bundleDir, "runtime-load-proof.json"),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function formatOperatorProofResult(result) {
|
|
642
|
+
const lines = [
|
|
643
|
+
`PROOF ${result.verdict.verdict}`,
|
|
644
|
+
` Severity: ${result.verdict.severity}`,
|
|
645
|
+
` Why: ${result.verdict.why}`,
|
|
646
|
+
` Bundle: ${result.bundleDir}`,
|
|
647
|
+
` Summary: ${result.summaryPath}`,
|
|
648
|
+
` Steps: ${result.stepsPath}`,
|
|
649
|
+
` Verdict: ${result.verdictPath}`,
|
|
650
|
+
` Breadcrumbs: ${result.breadcrumbPath}`,
|
|
651
|
+
` Runtime proof: ${result.runtimeLoadProofSnapshotPath}`,
|
|
652
|
+
];
|
|
653
|
+
return lines.join("\n");
|
|
654
|
+
}
|
package/package.json
CHANGED