@openclawbrain/cli 0.4.11 → 0.4.13
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/local-learner.d.ts +1 -0
- package/dist/src/local-learner.js +8 -8
- package/dist/src/proof-command.js +654 -0
- package/extension/runtime-guard.ts +45 -0
- package/package.json +3 -2
|
@@ -3670,7 +3670,11 @@ export function buildGraphLocalActionSet(nodeBlockId, neighborBlockIds, graph, v
|
|
|
3670
3670
|
// TrajectoryStepV1, TrajectoryV1 defined below in PG5 section
|
|
3671
3671
|
export const STOP_ACTION_ID = "__STOP__";
|
|
3672
3672
|
const STOP_ACTION = STOP_ACTION_ID;
|
|
3673
|
+
const STOP_ACTION_UPDATE_SEPARATOR = "::";
|
|
3673
3674
|
const DEFAULT_STOP_BIAS = 0.0;
|
|
3675
|
+
export function buildStopActionUpdateBlockId(nodeBlockId) {
|
|
3676
|
+
return `${nodeBlockId}${STOP_ACTION_UPDATE_SEPARATOR}${STOP_ACTION_ID}`;
|
|
3677
|
+
}
|
|
3674
3678
|
const DEFAULT_TAU = 1.0;
|
|
3675
3679
|
const DEFAULT_BASELINE_ALPHA = 0.05;
|
|
3676
3680
|
const MAX_TRAJECTORY_LENGTH = 20;
|
|
@@ -3741,15 +3745,12 @@ export function computeTrajectoryPolicyGradient(trajectory, adjacency, graph, ve
|
|
|
3741
3745
|
grad = (1 / tau) * (-prob);
|
|
3742
3746
|
}
|
|
3743
3747
|
}
|
|
3744
|
-
|
|
3748
|
+
const gradientKey = j === STOP_ACTION_ID ? buildStopActionUpdateBlockId(step.nodeBlockId) : j;
|
|
3749
|
+
tailGradient.set(gradientKey, (tailGradient.get(gradientKey) ?? 0) + grad);
|
|
3745
3750
|
}
|
|
3746
3751
|
}
|
|
3747
3752
|
// Weight by advantage and pgScale, accumulate into updates
|
|
3748
3753
|
for (const [blockId, grad] of tailGradient) {
|
|
3749
|
-
// Skip STOP action — it has no learnable weight
|
|
3750
|
-
if (blockId === STOP_ACTION_ID) {
|
|
3751
|
-
continue;
|
|
3752
|
-
}
|
|
3753
3754
|
const scaledDelta = advantage * grad * pgScale;
|
|
3754
3755
|
const current = updates.get(blockId);
|
|
3755
3756
|
if (current === undefined) {
|
|
@@ -4587,13 +4588,12 @@ function computeTrajectoryPolicyGradientV2(trajectory, adjacency, graph, vectors
|
|
|
4587
4588
|
const grad = neighborId === actionKey
|
|
4588
4589
|
? (1 / tau) * (1 - prob)
|
|
4589
4590
|
: (1 / tau) * (-prob);
|
|
4590
|
-
|
|
4591
|
+
const gradientKey = neighborId === STOP_ACTION ? buildStopActionUpdateBlockId(step.nodeBlockId) : neighborId;
|
|
4592
|
+
tailGradient.set(gradientKey, (tailGradient.get(gradientKey) ?? 0) + grad);
|
|
4591
4593
|
}
|
|
4592
4594
|
}
|
|
4593
4595
|
// Weight by advantage and pgScale
|
|
4594
4596
|
for (const [blockId, grad] of tailGradient) {
|
|
4595
|
-
if (blockId === STOP_ACTION)
|
|
4596
|
-
continue; // don't update virtual STOP
|
|
4597
4597
|
const delta = roundPolicyGradientValue(advantage * grad * pgScale);
|
|
4598
4598
|
if (delta === 0)
|
|
4599
4599
|
continue;
|
|
@@ -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
|
+
}
|