diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index f34cc7d03..5df82c918 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,29 @@ +## 2026-06-29 — 09:13 post-reboot verifier false-positive 收斂與 service green readback + +**背景**: +- 09:05 machine-readable summary 因 `POST_START_SERVICE_WARNINGS=1` 回 `SERVICE_GREEN=0` / `OVERALL_DECLARATION=SERVICE_BLOCKED`,但 live evidence 顯示 route / K3s / backup / Stock freshness / 110 runner pressure 都已通過。 +- 追查後確認 warning 來自 CPU process classifier 誤把 Gitea `git log` 參數中的 `.playwright-mcp` 當成 browser / smoke process;host runaway exporter 同時讀回 orphan browser group `0`、active Actions container/process `0`、host pressure gate `0`。 + +**完成內容**: +- 新增 `scripts/reboot-recovery/post-start-smoke-process-classifier.awk`,只把真 `chrome` / `chromium` process 或明確 smoke / Playwright command 算成 browser/smoke candidate,排除 `git` / `gitea` / shell helper 自我匹配。 +- `scripts/reboot-recovery/post-start-quick-check.sh` 改用此 classifier,並把 cold-start `WARN>0 BLOCKED=0` 從 service warning 改為 evidence warning;MOMO、Stock、backup、routes、CPU、runner 專門 gate 仍負責判斷 service impact。 +- 新增 `scripts/reboot-recovery/tests/test_post_start_smoke_process_classifier.py`,覆蓋 `.playwright-mcp` false positive、真 chromium 與 StockPlatform smoke 命中。 + +**驗證結果**: +- `bash -n scripts/reboot-recovery/post-start-quick-check.sh`:通過。 +- `python3.11 -m pytest scripts/reboot-recovery/tests/test_post_start_smoke_process_classifier.py scripts/reboot-recovery/tests/test_momo_source_arrival_gate.py -q`:`6 passed`。 +- `git diff --check`:通過。 +- `post-reboot-readiness-summary.sh --no-color` artifact `/tmp/awoooi-post-reboot-readiness-20260629-091041-after-classifier/summary.txt`:`POST_START_PASS=43`、`POST_START_WARN=5`、`POST_START_BLOCKED=0`、`POST_START_SERVICE_WARNINGS=0`、`POST_START_BOUNDARY_WARNINGS=1`、`POST_START_EVIDENCE_WARNINGS=4`、`SERVICE_GREEN=1`、`PRODUCT_DATA_GREEN=1`、`STOCK_FRESHNESS_STATUS=ok`、`STOCK_LATEST_TRADING_DATE=2026-06-26`、`BACKUP_CORE_GREEN=1`、`HOST_188_HYGIENE_BLOCKED=0`、`WAZUH_MANAGER_REGISTRY_ACCEPTED=6`、`RUNTIME_ACTION_AUTHORIZED=0`、`OVERALL_DECLARATION=FULL_STACK_GREEN_DR_ESCROW_BLOCKED`、`NEXT_REQUIRED_GATES=credential_escrow_evidence`。 +- `post-reboot-declaration-guard.py --summary-file /tmp/awoooi-post-reboot-readiness-20260629-091041-after-classifier/summary.txt`:`POST_REBOOT_DECLARATION_GUARD_OK status=allowed_with_boundary_blockers allowed=5 forbidden=4 next_gates=1`。 +- `post-reboot-next-gate-dispatch.sh --summary-file ...`:`NEXT_GATE_COUNT=1`,唯一 gate 為 `credential_escrow_evidence`,要求 non-secret evidence id,禁止 password / token / secret value / hash / prefix / suffix / raw credential。 +- owner packet artifact `/tmp/awoooi-post-reboot-owner-packets-20260629-0913.json` 通過 contract guard:`POST_REBOOT_OWNER_PACKET_CONTRACT_GUARD_OK gates=1 request_sent=0 accepted=0 runtime_gate=0`。 +- owner response preflight 正確 fail-closed:`POST_REBOOT_OWNER_RESPONSE_PREFLIGHT_BLOCKED status=blocked_waiting_owner_response_file expected_gates=1 received=0 accepted=0 runtime_gate=0`。 + +**仍維持**: +- `ESCROW_MISSING_COUNT=5`,不得宣稱 `DR_COMPLETE`。 +- `MOMO_SOURCE_ARRIVAL_GATE status=blocked_source_absent_fail_closed`、`source_intake=0`、`runtime_write_authorized=0`、`db_write_authorized=0`、`drive_move_authorized=0`;MOMO legitimate source 到達前不做 DB/Drive runtime write。 +- non-110 runner registration token 仍是 secret 硬邊界;本段沒有讀、複製、貼上或外送 token / `.runner` 內容,也沒有啟動 188 runner service、重開 110 runner、使用 GitHub、force push、重啟 Docker / Nginx / firewall / K3s / DB。 + ## 2026-06-29 — 09:00 Public Gitea Actions queue readback verifier **完成內容**: diff --git a/scripts/reboot-recovery/post-start-quick-check.sh b/scripts/reboot-recovery/post-start-quick-check.sh index 7439f442c..66522913a 100755 --- a/scripts/reboot-recovery/post-start-quick-check.sh +++ b/scripts/reboot-recovery/post-start-quick-check.sh @@ -18,6 +18,7 @@ RUN_BACKUP=1 RUN_ROUTES=1 RUN_CPU=1 NO_COLOR_FLAG=0 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" COLD_START_PENDING_BLOCKERS=0 COLD_START_BLOCKED_SUMMARY="" COLD_START_BLOCKED_LINES="" @@ -248,7 +249,7 @@ if [[ "$RUN_COLD_START" -eq 1 ]]; then COLD_START_BLOCKED_LINES="$(grep -E '^BLOCKED ' "$cold_tmp" || true)" evidence_warn "cold-start blockers pending wrapper retry classification: $cold_summary" elif [[ "$cold_warn" -gt 0 ]]; then - service_warn "cold-start is warning-only, not blocked: $cold_summary" + evidence_warn "cold-start is warning-only; dedicated gates below classify service impact: $cold_summary" elif [[ "$cold_rc" -eq 0 ]]; then ok "cold-start command exited 0" else @@ -526,7 +527,9 @@ if [[ "$RUN_CPU" -eq 1 ]]; then evidence_warn "110 CPU/process readback failed" fi cat "$cpu_tmp" - if grep -Eiq 'chrome|chromium|playwright' "$cpu_tmp"; then + smoke_candidates="$(awk -f "$SCRIPT_DIR/post-start-smoke-process-classifier.awk" "$cpu_tmp" || true)" + if [[ -n "$smoke_candidates" ]]; then + printf '%s\n' "$smoke_candidates" evidence_warn "browser/smoke process is visible; classify orphan vs active parent before action" fi if grep -Eiq 'gitea|actions|runner|npm|pnpm|pytest|pip-audit' "$cpu_tmp"; then diff --git a/scripts/reboot-recovery/post-start-smoke-process-classifier.awk b/scripts/reboot-recovery/post-start-smoke-process-classifier.awk new file mode 100644 index 000000000..a3843029c --- /dev/null +++ b/scripts/reboot-recovery/post-start-smoke-process-classifier.awk @@ -0,0 +1,21 @@ +BEGIN { + IGNORECASE = 1 +} + +NR == 1 { + next +} + +{ + comm = $7 + if (comm ~ /^(git|gitea|grep|awk|bash|sh)$/) { + next + } + if (comm ~ /^(chrome|chromium|chromium-browser)$/) { + print + next + } + if ($0 ~ /(stockplatform.*smoke|review-bulk|product-ux|(^|[[:space:]])playwright([[:space:]]|$)|npx[[:space:]].*playwright|node[[:space:]].*playwright)/) { + print + } +} diff --git a/scripts/reboot-recovery/tests/test_post_start_smoke_process_classifier.py b/scripts/reboot-recovery/tests/test_post_start_smoke_process_classifier.py new file mode 100644 index 000000000..57fb5154d --- /dev/null +++ b/scripts/reboot-recovery/tests/test_post_start_smoke_process_classifier.py @@ -0,0 +1,44 @@ +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +CLASSIFIER = ROOT / "post-start-smoke-process-classifier.awk" + + +def classify(sample: str) -> str: + result = subprocess.run( + ["awk", "-f", str(CLASSIFIER)], + input=sample, + text=True, + capture_output=True, + check=True, + ) + return result.stdout + + +def test_ignores_gitea_git_log_with_playwright_path() -> None: + sample = """\ +PID PPID PGID STAT %CPU %MEM COMMAND COMMAND +3686370 1249344 3686370 R 48.0 0.0 git /usr/bin/git log -- .playwright-mcp docs +1249344 1249342 1249344 Ssl 60.6 1.1 gitea /usr/local/bin/gitea web +""" + assert classify(sample) == "" + + +def test_detects_real_chromium_process() -> None: + sample = """\ +PID PPID PGID STAT %CPU %MEM COMMAND COMMAND +123 1 123 Sl 95.0 1.0 chromium /usr/bin/chromium --headless +""" + output = classify(sample) + assert "chromium" in output + + +def test_detects_stockplatform_smoke_process() -> None: + sample = """\ +PID PPID PGID STAT %CPU %MEM COMMAND COMMAND +456 1 456 Sl 88.0 0.8 node node scripts/ops/stockplatform-review-bulk-ux-smoke.mjs +""" + output = classify(sample) + assert "stockplatform-review-bulk-ux-smoke" in output