Skip to content

Commit f2f69fb

Browse files
authored
refactor: phase 3 foundation for node-based checks (#154)
1 parent a569cd1 commit f2f69fb

6 files changed

Lines changed: 262 additions & 52 deletions

File tree

bin/checks/integrity-status.mjs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env node
2+
3+
import fs from "node:fs";
4+
5+
function asString(value, fallback) {
6+
return typeof value === "string" && value.length > 0 ? value : fallback;
7+
}
8+
9+
function asCountString(value) {
10+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
11+
return String(Math.trunc(value));
12+
}
13+
if (typeof value === "string" && /^\d+$/.test(value)) {
14+
return value;
15+
}
16+
return "0";
17+
}
18+
19+
const statusPath = process.argv[2] || "";
20+
21+
if (!statusPath) {
22+
process.stdout.write(
23+
JSON.stringify({
24+
ok: "0",
25+
exists: "0",
26+
status: "unknown",
27+
checked_at: "unknown",
28+
missing_files: "0",
29+
hash_mismatches: "0",
30+
error: "missing_path_argument",
31+
}),
32+
);
33+
process.exit(0);
34+
}
35+
36+
if (!fs.existsSync(statusPath)) {
37+
process.stdout.write(
38+
JSON.stringify({
39+
ok: "1",
40+
exists: "0",
41+
status: "missing",
42+
checked_at: "unknown",
43+
missing_files: "0",
44+
hash_mismatches: "0",
45+
error: "",
46+
}),
47+
);
48+
process.exit(0);
49+
}
50+
51+
try {
52+
const raw = fs.readFileSync(statusPath, "utf8");
53+
const parsed = JSON.parse(raw);
54+
55+
process.stdout.write(
56+
JSON.stringify({
57+
ok: "1",
58+
exists: "1",
59+
status: asString(parsed?.status, "unknown"),
60+
checked_at: asString(parsed?.checked_at, "unknown"),
61+
missing_files: asCountString(parsed?.missing_files),
62+
hash_mismatches: asCountString(parsed?.hash_mismatches),
63+
error: "",
64+
}),
65+
);
66+
} catch {
67+
process.stdout.write(
68+
JSON.stringify({
69+
ok: "0",
70+
exists: "1",
71+
status: "unknown",
72+
checked_at: "unknown",
73+
missing_files: "0",
74+
hash_mismatches: "0",
75+
error: "parse_error",
76+
}),
77+
);
78+
}

bin/doctor.sh

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -301,31 +301,62 @@ else
301301
fi
302302

303303
INTEGRITY_STATUS_FILE="$BAUDBOT_INTEGRITY_STATUS_FILE"
304-
if [ -f "$INTEGRITY_STATUS_FILE" ]; then
305-
integrity_status="$(jq -r '.status // "unknown"' "$INTEGRITY_STATUS_FILE" 2>/dev/null || echo "unknown")"
306-
integrity_checked_at="$(jq -r '.checked_at // "unknown"' "$INTEGRITY_STATUS_FILE" 2>/dev/null || echo "unknown")"
307-
integrity_missing="$(jq -r '.missing_files // 0' "$INTEGRITY_STATUS_FILE" 2>/dev/null || echo "0")"
308-
integrity_mismatches="$(jq -r '.hash_mismatches // 0' "$INTEGRITY_STATUS_FILE" 2>/dev/null || echo "0")"
309-
310-
case "$integrity_status" in
311-
pass)
312-
pass "startup manifest integrity passed ($integrity_checked_at)"
313-
;;
314-
warn)
315-
warn "startup manifest integrity reported issues at $integrity_checked_at ($integrity_missing missing, $integrity_mismatches mismatched)"
316-
;;
317-
fail)
318-
fail "startup manifest integrity failed at $integrity_checked_at ($integrity_missing missing, $integrity_mismatches mismatched)"
319-
;;
320-
skipped)
321-
warn "startup manifest integrity check is disabled/skipped"
322-
;;
323-
*)
324-
warn "startup manifest integrity status unknown (file: $INTEGRITY_STATUS_FILE)"
325-
;;
326-
esac
304+
INTEGRITY_CHECK_SCRIPT="$BAUDBOT_ROOT/bin/checks/integrity-status.mjs"
305+
INTEGRITY_CHECK_NODE_BIN=""
306+
if [ -n "${NODE_BIN:-}" ] && [ -x "${NODE_BIN:-}" ]; then
307+
INTEGRITY_CHECK_NODE_BIN="$NODE_BIN"
308+
elif command -v node >/dev/null 2>&1; then
309+
INTEGRITY_CHECK_NODE_BIN="$(command -v node)"
310+
fi
311+
312+
if [ -n "$INTEGRITY_CHECK_NODE_BIN" ] && [ -f "$INTEGRITY_CHECK_SCRIPT" ]; then
313+
integrity_payload="$($INTEGRITY_CHECK_NODE_BIN "$INTEGRITY_CHECK_SCRIPT" "$INTEGRITY_STATUS_FILE" 2>/dev/null || true)"
314+
else
315+
integrity_payload=""
316+
fi
317+
318+
if [ -n "$integrity_payload" ]; then
319+
integrity_exists="$(printf '%s' "$integrity_payload" | json_get_string_stdin "exists" 2>/dev/null || true)"
320+
integrity_status="$(printf '%s' "$integrity_payload" | json_get_string_stdin "status" 2>/dev/null || true)"
321+
integrity_checked_at="$(printf '%s' "$integrity_payload" | json_get_string_stdin "checked_at" 2>/dev/null || true)"
322+
integrity_missing="$(printf '%s' "$integrity_payload" | json_get_string_stdin "missing_files" 2>/dev/null || true)"
323+
integrity_mismatches="$(printf '%s' "$integrity_payload" | json_get_string_stdin "hash_mismatches" 2>/dev/null || true)"
324+
325+
[ -n "$integrity_exists" ] || integrity_exists="0"
326+
[ -n "$integrity_status" ] || integrity_status="unknown"
327+
[ -n "$integrity_checked_at" ] || integrity_checked_at="unknown"
328+
[ -n "$integrity_missing" ] || integrity_missing="0"
329+
[ -n "$integrity_mismatches" ] || integrity_mismatches="0"
330+
331+
if [ "$integrity_exists" != "1" ]; then
332+
if [ "$IS_ROOT" -ne 1 ] && [ -d "$BAUDBOT_HOME/.pi/agent" ]; then
333+
warn "cannot verify startup manifest integrity status as non-root (run: sudo baudbot doctor)"
334+
else
335+
warn "startup manifest integrity status file missing ($INTEGRITY_STATUS_FILE)"
336+
fi
337+
else
338+
case "$integrity_status" in
339+
pass)
340+
pass "startup manifest integrity passed ($integrity_checked_at)"
341+
;;
342+
warn)
343+
warn "startup manifest integrity reported issues at $integrity_checked_at ($integrity_missing missing, $integrity_mismatches mismatched)"
344+
;;
345+
fail)
346+
fail "startup manifest integrity failed at $integrity_checked_at ($integrity_missing missing, $integrity_mismatches mismatched)"
347+
;;
348+
skipped)
349+
warn "startup manifest integrity check is disabled/skipped"
350+
;;
351+
*)
352+
warn "startup manifest integrity status unknown (file: $INTEGRITY_STATUS_FILE)"
353+
;;
354+
esac
355+
fi
327356
else
328-
if [ "$IS_ROOT" -ne 1 ] && [ -d "$BAUDBOT_HOME/.pi/agent" ]; then
357+
if [ -f "$INTEGRITY_STATUS_FILE" ]; then
358+
warn "startup manifest integrity status unreadable (file: $INTEGRITY_STATUS_FILE)"
359+
elif [ "$IS_ROOT" -ne 1 ] && [ -d "$BAUDBOT_HOME/.pi/agent" ]; then
329360
warn "cannot verify startup manifest integrity status as non-root (run: sudo baudbot doctor)"
330361
else
331362
warn "startup manifest integrity status file missing ($INTEGRITY_STATUS_FILE)"

bin/security-audit.sh

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -289,34 +289,62 @@ else
289289
"Run deploy.sh to generate"
290290
fi
291291

292-
if [ -f "$BAUDBOT_INTEGRITY_STATUS_FILE" ]; then
293-
status_value="$(jq -r '.status // "unknown"' "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || echo "unknown")"
294-
status_checked_at="$(jq -r '.checked_at // "unknown"' "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || echo "unknown")"
295-
status_missing="$(jq -r '.missing_files // 0' "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || echo "0")"
296-
status_mismatches="$(jq -r '.hash_mismatches // 0' "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || echo "0")"
297-
298-
case "$status_value" in
299-
pass)
300-
ok "Last startup integrity check passed ($status_checked_at)"
301-
;;
302-
warn)
303-
finding "WARN" "Last startup integrity check reported issues" \
304-
"$status_checked_at — missing: $status_missing, mismatched: $status_mismatches"
305-
;;
306-
fail)
307-
finding "CRITICAL" "Last startup integrity check failed" \
308-
"$status_checked_at — missing: $status_missing, mismatched: $status_mismatches"
309-
;;
310-
skipped)
311-
finding "WARN" "Last startup integrity check was skipped/disabled" "$status_checked_at"
312-
;;
313-
*)
314-
finding "INFO" "Startup integrity status unknown" "$BAUDBOT_INTEGRITY_STATUS_FILE"
315-
;;
316-
esac
292+
INTEGRITY_CHECK_SCRIPT="$BAUDBOT_SRC/bin/checks/integrity-status.mjs"
293+
INTEGRITY_CHECK_NODE_BIN="$(bb_resolve_runtime_node_bin "$BAUDBOT_HOME" 2>/dev/null || true)"
294+
if [ -z "$INTEGRITY_CHECK_NODE_BIN" ] && command -v node >/dev/null 2>&1; then
295+
INTEGRITY_CHECK_NODE_BIN="$(command -v node)"
296+
fi
297+
298+
if [ -n "$INTEGRITY_CHECK_NODE_BIN" ] && [ -f "$INTEGRITY_CHECK_SCRIPT" ]; then
299+
integrity_payload="$($INTEGRITY_CHECK_NODE_BIN "$INTEGRITY_CHECK_SCRIPT" "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || true)"
300+
else
301+
integrity_payload=""
302+
fi
303+
304+
if [ -n "$integrity_payload" ]; then
305+
status_exists="$(printf '%s' "$integrity_payload" | json_get_string_stdin "exists" 2>/dev/null || true)"
306+
status_value="$(printf '%s' "$integrity_payload" | json_get_string_stdin "status" 2>/dev/null || true)"
307+
status_checked_at="$(printf '%s' "$integrity_payload" | json_get_string_stdin "checked_at" 2>/dev/null || true)"
308+
status_missing="$(printf '%s' "$integrity_payload" | json_get_string_stdin "missing_files" 2>/dev/null || true)"
309+
status_mismatches="$(printf '%s' "$integrity_payload" | json_get_string_stdin "hash_mismatches" 2>/dev/null || true)"
310+
311+
[ -n "$status_exists" ] || status_exists="0"
312+
[ -n "$status_value" ] || status_value="unknown"
313+
[ -n "$status_checked_at" ] || status_checked_at="unknown"
314+
[ -n "$status_missing" ] || status_missing="0"
315+
[ -n "$status_mismatches" ] || status_mismatches="0"
316+
317+
if [ "$status_exists" != "1" ]; then
318+
finding "WARN" "No startup integrity status found" \
319+
"Expected: $BAUDBOT_INTEGRITY_STATUS_FILE (restart agent after deploy)"
320+
else
321+
case "$status_value" in
322+
pass)
323+
ok "Last startup integrity check passed ($status_checked_at)"
324+
;;
325+
warn)
326+
finding "WARN" "Last startup integrity check reported issues" \
327+
"$status_checked_at — missing: $status_missing, mismatched: $status_mismatches"
328+
;;
329+
fail)
330+
finding "CRITICAL" "Last startup integrity check failed" \
331+
"$status_checked_at — missing: $status_missing, mismatched: $status_mismatches"
332+
;;
333+
skipped)
334+
finding "WARN" "Last startup integrity check was skipped/disabled" "$status_checked_at"
335+
;;
336+
*)
337+
finding "INFO" "Startup integrity status unknown" "$BAUDBOT_INTEGRITY_STATUS_FILE"
338+
;;
339+
esac
340+
fi
317341
else
318-
finding "WARN" "No startup integrity status found" \
319-
"Expected: $BAUDBOT_INTEGRITY_STATUS_FILE (restart agent after deploy)"
342+
if [ -f "$BAUDBOT_INTEGRITY_STATUS_FILE" ]; then
343+
finding "INFO" "Startup integrity status unreadable" "$BAUDBOT_INTEGRITY_STATUS_FILE"
344+
else
345+
finding "WARN" "No startup integrity status found" \
346+
"Expected: $BAUDBOT_INTEGRITY_STATUS_FILE (restart agent after deploy)"
347+
fi
320348
fi
321349
echo ""
322350

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"scripts": {
66
"test": "vitest run --config vitest.config.mjs",
7-
"test:js": "vitest run --config vitest.config.mjs pi/extensions/heartbeat.test.mjs pi/extensions/memory.test.mjs test/legacy-node-tests.test.mjs test/broker-bridge.integration.test.mjs",
7+
"test:js": "vitest run --config vitest.config.mjs pi/extensions/heartbeat.test.mjs pi/extensions/memory.test.mjs test/legacy-node-tests.test.mjs test/broker-bridge.integration.test.mjs test/integrity-status-check.test.mjs",
88
"test:shell": "vitest run --config vitest.config.mjs test/shell-scripts.test.mjs test/security-audit.test.mjs",
99
"test:coverage": "vitest run --config vitest.config.mjs --coverage pi/extensions/heartbeat.test.mjs pi/extensions/memory.test.mjs test/legacy-node-tests.test.mjs",
1010
"lint": "npm run lint:js && npm run lint:shell",
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { spawnSync } from "node:child_process";
2+
import fs from "node:fs";
3+
import os from "node:os";
4+
import path from "node:path";
5+
import { afterEach, describe, expect, it } from "vitest";
6+
7+
const scriptPath = path.resolve("bin/checks/integrity-status.mjs");
8+
9+
const tmpDirs = [];
10+
11+
function runCheck(statusPath) {
12+
const result = spawnSync("node", [scriptPath, statusPath], {
13+
cwd: path.resolve("."),
14+
encoding: "utf8",
15+
});
16+
17+
expect(result.status).toBe(0);
18+
return JSON.parse(result.stdout);
19+
}
20+
21+
afterEach(() => {
22+
for (const dir of tmpDirs.splice(0)) {
23+
fs.rmSync(dir, { recursive: true, force: true });
24+
}
25+
});
26+
27+
describe("bin/checks/integrity-status.mjs", () => {
28+
it("reports missing status file", () => {
29+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "baudbot-integrity-check-"));
30+
tmpDirs.push(tmpDir);
31+
32+
const payload = runCheck(path.join(tmpDir, "missing.json"));
33+
expect(payload.exists).toBe("0");
34+
expect(payload.status).toBe("missing");
35+
});
36+
37+
it("normalizes valid status payload", () => {
38+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "baudbot-integrity-check-"));
39+
tmpDirs.push(tmpDir);
40+
41+
const statusPath = path.join(tmpDir, "status.json");
42+
fs.writeFileSync(
43+
statusPath,
44+
JSON.stringify({
45+
status: "warn",
46+
checked_at: "2026-02-24T00:00:00Z",
47+
missing_files: 2,
48+
hash_mismatches: 1,
49+
}),
50+
);
51+
52+
const payload = runCheck(statusPath);
53+
expect(payload.exists).toBe("1");
54+
expect(payload.status).toBe("warn");
55+
expect(payload.checked_at).toBe("2026-02-24T00:00:00Z");
56+
expect(payload.missing_files).toBe("2");
57+
expect(payload.hash_mismatches).toBe("1");
58+
});
59+
60+
it("handles invalid JSON safely", () => {
61+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "baudbot-integrity-check-"));
62+
tmpDirs.push(tmpDir);
63+
64+
const statusPath = path.join(tmpDir, "status.json");
65+
fs.writeFileSync(statusPath, "{not-json");
66+
67+
const payload = runCheck(statusPath);
68+
expect(payload.exists).toBe("1");
69+
expect(payload.ok).toBe("0");
70+
expect(payload.error).toBe("parse_error");
71+
});
72+
});

vitest.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default defineConfig({
77
"pi/extensions/memory.test.mjs",
88
"test/legacy-node-tests.test.mjs",
99
"test/broker-bridge.integration.test.mjs",
10+
"test/integrity-status-check.test.mjs",
1011
"test/shell-scripts.test.mjs",
1112
"test/security-audit.test.mjs",
1213
],

0 commit comments

Comments
 (0)