Compare commits

...

15 Commits

Author SHA1 Message Date
Your Name
c2b19ea019 Merge remote-tracking branch 'refs/remotes/gitea-ssh/main' into codex/110-runner-pressure-merge-main-20260627
Some checks failed
Code Review / ai-code-review (push) Has been cancelled
Ansible / Reboot Recovery Contract / validate (push) Failing after 1m10s
# Conflicts:
#	docs/LOGBOOK.md
2026-06-27 21:48:26 +08:00
Your Name
b5bf42bf0a docs(iwooos): record wazuh reviewer post-enable readback [skip ci] 2026-06-27 21:45:13 +08:00
Your Name
a68d9e40a7 feat(github): preflight owner response intake
Some checks failed
CD Pipeline / tests (push) Successful in 1m48s
Code Review / ai-code-review (push) Successful in 9s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
2026-06-27 21:44:54 +08:00
Your Name
6f228e7f8a Merge remote-tracking branch 'refs/remotes/gitea-ssh/main' into codex/110-runner-pressure-merge-main-20260627
# Conflicts:
#	docs/LOGBOOK.md
2026-06-27 21:39:35 +08:00
AWOOOI CD
1a6f8f4275 chore(cd): deploy 1a8613c [skip ci] 2026-06-27 13:36:35 +00:00
Your Name
1a8613c9e6 fix(governance): stabilize automation tab deep link
All checks were successful
CD Pipeline / tests (push) Successful in 1m44s
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / build-and-deploy (push) Successful in 4m52s
CD Pipeline / post-deploy-checks (push) Successful in 1m35s
2026-06-27 21:30:40 +08:00
Your Name
c73ce995e2 feat(iwooos): mark wazuh reviewer post-enable readback
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
Code Review / ai-code-review (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
2026-06-27 21:28:37 +08:00
Your Name
b6c2271f64 docs(github): record owner response intake readback [skip ci] 2026-06-27 21:28:11 +08:00
Your Name
319208f1da Merge remote-tracking branch 'refs/remotes/gitea-ssh/main' into codex/110-runner-pressure-merge-main-20260627
# Conflicts:
#	docs/LOGBOOK.md
2026-06-27 21:24:44 +08:00
Your Name
df498e55b1 test(recovery): cover momo source arrival gate 2026-06-27 21:20:34 +08:00
Your Name
18fa182bce fix(recovery): surface momo source arrival gate in quick check 2026-06-27 21:14:42 +08:00
Your Name
2c08a151ca Merge remote-tracking branch 'gitea-ssh/main' into codex/110-runner-pressure-merge-main-20260627
# Conflicts:
#	docs/LOGBOOK.md
2026-06-27 21:11:20 +08:00
Your Name
03f39d3c58 chore(recovery): add momo source arrival gate 2026-06-27 21:02:10 +08:00
Your Name
073141abcb docs(ops): record 110 runner pressure closeout 2026-06-27 20:17:14 +08:00
Your Name
4c951b2996 fix(ci): keep 110 runner inactive until pressure clears 2026-06-27 20:15:01 +08:00
25 changed files with 1208 additions and 85 deletions

View File

@@ -26,7 +26,7 @@ on:
jobs:
validate:
runs-on: self-hosted
runs-on: awoooi-ubuntu
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

View File

@@ -1245,6 +1245,12 @@ jobs:
- uses: actions/checkout@v4
- name: Wait for Host Web Build Pressure
# 2026-06-27 Codex: post-deploy Playwright smoke is browser-heavy too.
# Refuse to add another smoke run while 110 already has CI/build/smoke
# pressure; this gate is read-only and never kills other repo work.
run: bash scripts/ci/wait-host-web-build-pressure.sh
- name: Get Commit Info
id: commit
run: |

View File

@@ -38,15 +38,6 @@ from src.core.sse import get_publisher
from src.services.agent_market_governance_snapshot import (
load_latest_agent_market_governance_snapshot,
)
from src.services.ai_agent_market_radar_readback import (
load_latest_ai_agent_market_radar_readback,
)
from src.services.ai_technology_radar_readback import (
load_latest_ai_technology_radar_readback,
)
from src.services.ai_technology_report_cadence_readback import (
load_latest_ai_technology_report_cadence_readback,
)
from src.services.agent_service import (
AgentService,
TaskState,
@@ -88,12 +79,6 @@ from src.services.ai_agent_critic_reviewer_result_capture import (
from src.services.ai_agent_deployment_layout import (
load_latest_ai_agent_deployment_layout,
)
from src.services.awoooi_status_cleanup_dashboard import (
load_latest_awoooi_status_cleanup_dashboard,
)
from src.services.github_target_private_backup_evidence_gate import (
load_latest_github_target_private_backup_evidence_gate,
)
from src.services.ai_agent_failure_receipt_no_send_replay import (
load_latest_ai_agent_failure_receipt_no_send_replay,
)
@@ -118,6 +103,9 @@ from src.services.ai_agent_live_read_model_gate import (
from src.services.ai_agent_low_medium_risk_whitelist import (
load_latest_ai_agent_low_medium_risk_whitelist,
)
from src.services.ai_agent_market_radar_readback import (
load_latest_ai_agent_market_radar_readback,
)
from src.services.ai_agent_matched_playbook_learning_gap import (
load_latest_ai_agent_matched_playbook_learning_gap,
)
@@ -307,6 +295,15 @@ from src.services.ai_agent_version_lifecycle_update_proposal import (
from src.services.ai_provider_route_matrix import (
load_latest_ai_provider_route_matrix,
)
from src.services.ai_technology_radar_readback import (
load_latest_ai_technology_radar_readback,
)
from src.services.ai_technology_report_cadence_readback import (
load_latest_ai_technology_report_cadence_readback,
)
from src.services.awoooi_status_cleanup_dashboard import (
load_latest_awoooi_status_cleanup_dashboard,
)
from src.services.backup_dr_readiness_matrix import (
load_latest_backup_dr_readiness_matrix,
)
@@ -319,6 +316,9 @@ from src.services.backup_notification_policy import (
from src.services.backup_restore_drill_approval_package_template import (
load_latest_backup_restore_drill_approval_package_template,
)
from src.services.delivery_closure_workbench import (
load_delivery_closure_workbench,
)
from src.services.dependency_drift_check_plan import (
load_latest_dependency_drift_check_plan,
)
@@ -331,15 +331,16 @@ from src.services.dependency_supply_chain_drift_monitor import (
from src.services.dependency_upgrade_approval_package_template import (
load_latest_dependency_upgrade_approval_package_template,
)
from src.services.delivery_closure_workbench import (
load_delivery_closure_workbench,
)
from src.services.docker_build_surface_inventory import (
load_latest_docker_build_surface_inventory,
)
from src.services.gitea_workflow_runner_health import (
load_latest_gitea_workflow_runner_health,
)
from src.services.github_target_private_backup_evidence_gate import (
load_latest_github_target_private_backup_evidence_gate,
preflight_github_target_owner_response_submission,
)
from src.services.host_runaway_aiops_loop_readiness import (
load_latest_host_runaway_aiops_loop_readiness,
)
@@ -990,6 +991,42 @@ async def get_github_target_private_backup_evidence_gate() -> dict[str, Any]:
) from exc
@router.post(
"/github-target-owner-response-intake-preflight",
response_model=dict[str, Any],
summary="預檢 GitHub target owner response candidate",
description=(
"只驗證一份 GitHub target owner response candidate 是否符合 read-only intake 規則;"
"此端點不持久化 submission、不呼叫 GitHub live API、不建立 repo、不改 visibility、不同步 refs、"
"不觸發 workflow、不收 private clone URL credential 或任何 secret value。"
),
)
async def preflight_github_target_owner_response_intake(
submission: dict[str, Any],
) -> dict[str, Any]:
"""Validate a GitHub target owner response candidate without persisting it."""
try:
payload = await asyncio.to_thread(
preflight_github_target_owner_response_submission,
submission,
)
return redact_public_lan_topology(payload)
except FileNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(exc),
) from exc
except (json.JSONDecodeError, ValueError) as exc:
logger.error(
"github_target_owner_response_intake_preflight_invalid",
error=str(exc),
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="GitHub target owner response intake preflight 無效",
) from exc
@router.get(
"/agent-12-agent-war-room",
response_model=dict[str, Any],

View File

@@ -8,6 +8,7 @@ because AWOOOI policy requires GitHub backup targets to be private.
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any
@@ -22,6 +23,39 @@ _APPROVAL_PACKAGE_FILE = "github-target-repo-approval-package.snapshot.json"
_PROBE_FILE = "github-target-probe.snapshot.json"
_CONNECTOR_READBACK_FILE = "github-target-connector-readback.snapshot.json"
_MISSING_SOURCE_READINESS_FILE = "github-target-missing-source-readiness.snapshot.json"
_PREFLIGHT_SCHEMA_VERSION = "github_target_owner_response_intake_preflight_v1"
_PREFLIGHT_MODE = "validate_owner_response_only_no_persist_no_github_write"
_SUBMISSION_METADATA_FIELDS = {
"github_repo",
"response_id",
"submission_mode",
"template_id",
}
_FORBIDDEN_KEY_FRAGMENTS = {
"api_request_body",
"authorization_header",
"cookie",
"credential",
"db_dump",
"deploy_key",
"git_object_pack",
"password",
"private_clone_url",
"private_key",
"repo_archive",
"repo_creation_command",
"secret_value",
"session",
"token_value",
"visibility_change_command",
}
_SENSITIVE_VALUE_PATTERNS = (
("github_token", re.compile(r"\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b")),
("github_fine_grained_token", re.compile(r"\bgithub_pat_[A-Za-z0-9_]{20,}\b")),
("private_key_block", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")),
("credentialed_url", re.compile(r"[a-z][a-z0-9+.-]*://[^/\s:@]+:[^@\s]+@")),
("raw_internal_lan_address", re.compile(r"\b192\.168\.0\.\d+\b")),
)
def load_latest_github_target_private_backup_evidence_gate(
@@ -56,6 +90,127 @@ def load_latest_github_target_private_backup_evidence_gate(
)
def preflight_github_target_owner_response_submission(
submission: dict[str, Any],
security_dir: Path | None = None,
) -> dict[str, Any]:
"""Validate an owner response candidate without storing or executing it."""
gate = load_latest_github_target_private_backup_evidence_gate(security_dir)
intake = _dict(gate.get("owner_response_intake_readiness"))
target_by_template = {
str(target.get("owner_response_template_id")): _dict(target)
for target in _list(gate.get("targets"))
if target.get("owner_response_template_id")
}
payload = _dict(submission)
allowed_modes = set(_strings(intake.get("allowed_submission_modes")))
allowed_fields = set(_strings(intake.get("allowed_response_fields")))
forbidden_payloads = set(_strings(intake.get("forbidden_payloads")))
still_forbidden = set(_strings(intake.get("still_forbidden")))
submission_mode = str(payload.get("submission_mode") or "")
candidate_responses = [_dict(row) for row in _list(payload.get("responses"))]
mode_allowed = submission_mode in allowed_modes
global_scan_payload = {
key: value for key, value in payload.items() if key != "responses"
}
global_hits = _forbidden_payload_hits(
global_scan_payload,
forbidden_payloads=forbidden_payloads | still_forbidden,
)
response_results = [
_preflight_owner_response_item(
response=response,
submission_mode=submission_mode,
mode_allowed=mode_allowed,
target_by_template=target_by_template,
allowed_fields=allowed_fields,
forbidden_payloads=forbidden_payloads | still_forbidden,
)
for response in candidate_responses
]
passed_count = sum(
1 for row in response_results if row["accepted_for_read_only_intake"] is True
)
blocked_count = len(response_results) - passed_count
global_blockers: list[str] = []
if not mode_allowed:
global_blockers.append("submission_mode_not_allowed")
if not candidate_responses:
global_blockers.append("response_items_missing")
if global_hits:
global_blockers.append("forbidden_payload_detected")
preflight_passed = (
not global_blockers and candidate_responses and blocked_count == 0
)
return {
"schema_version": _PREFLIGHT_SCHEMA_VERSION,
"generated_at": gate.get("generated_at", ""),
"status": "ready_for_read_only_owner_response_intake"
if preflight_passed
else "blocked_owner_response_intake_preflight",
"mode": _PREFLIGHT_MODE,
"summary": {
"candidate_response_item_count": len(candidate_responses),
"preflight_passed_response_item_count": passed_count,
"preflight_blocked_response_item_count": blocked_count,
"forbidden_payload_hit_count": len(global_hits)
+ sum(len(row["forbidden_hits"]) for row in response_results),
"unsupported_field_count": sum(
len(row["unsupported_fields"]) for row in response_results
),
"missing_required_field_count": sum(
len(row["missing_required_fields"]) for row in response_results
),
"owner_response_received_count": 0,
"owner_response_accepted_count": 0,
"safe_credential_accepted_evidence_count": 0,
"github_missing_target_create_private_repo_ready_count": gate["summary"][
"github_missing_target_create_private_repo_ready_count"
],
"github_missing_target_refs_sync_ready_count": gate["summary"][
"github_missing_target_refs_sync_ready_count"
],
"execution_authorized": False,
"write_performed": False,
"github_api_write_allowed": False,
"repo_creation_authorized": False,
"visibility_change_authorized": False,
"refs_sync_authorized": False,
"secret_value_collection_allowed": False,
},
"submission_mode": submission_mode,
"submission_mode_allowed": mode_allowed,
"allowed_submission_modes": sorted(allowed_modes),
"global_blockers": global_blockers,
"global_forbidden_hits": global_hits,
"responses": response_results,
"operation_boundaries": {
"preflight_only": True,
"persist_submission_allowed": False,
"read_only_markdown_response_allowed": True,
"redacted_metadata_pointer_allowed": True,
"github_api_write_allowed": False,
"repo_creation_allowed": False,
"visibility_change_allowed": False,
"refs_sync_allowed": False,
"workflow_trigger_allowed": False,
"private_clone_url_collection_allowed": False,
"secret_value_collection_allowed": False,
},
"authorization_flags": {
"owner_response_execution_authorized": False,
"repo_creation_authorized": False,
"visibility_change_authorized": False,
"refs_sync_authorized": False,
"workflow_trigger_authorized": False,
"secret_value_collection_allowed": False,
"private_clone_url_collection_allowed": False,
},
}
def build_github_target_private_backup_evidence_gate(
*,
decision: dict[str, Any],
@@ -293,6 +448,89 @@ def build_github_target_private_backup_evidence_gate(
}
def _preflight_owner_response_item(
*,
response: dict[str, Any],
submission_mode: str,
mode_allowed: bool,
target_by_template: dict[str, dict[str, Any]],
allowed_fields: set[str],
forbidden_payloads: set[str],
) -> dict[str, Any]:
template_id = str(response.get("template_id") or "")
target = target_by_template.get(template_id, {})
required_fields = set(_strings(target.get("owner_response_required_fields")))
acceptable_decisions = set(
_strings(target.get("owner_response_acceptable_decisions"))
)
response_fields = set(response)
supported_fields = allowed_fields | _SUBMISSION_METADATA_FIELDS
unsupported_fields = sorted(response_fields - supported_fields)
missing_required_fields = sorted(
field
for field in required_fields
if not _has_response_value(response.get(field))
)
evidence_refs = sorted(
set(_response_strings(response.get("redacted_evidence_refs")))
| set(_response_strings(response.get("evidence_refs")))
)
decision = str(response.get("decision") or "")
blockers: list[str] = []
if not mode_allowed:
blockers.append("submission_mode_not_allowed")
if not target:
blockers.append("unknown_or_unrequested_template_id")
if unsupported_fields:
blockers.append("unsupported_response_fields")
if missing_required_fields:
blockers.append("required_fields_missing")
if acceptable_decisions and decision not in acceptable_decisions:
blockers.append("decision_not_allowed_for_template")
if not evidence_refs:
blockers.append("redacted_evidence_refs_missing")
forbidden_hits = _forbidden_payload_hits(
response,
forbidden_payloads=forbidden_payloads,
)
evidence_ref_hits = _evidence_ref_hits(evidence_refs)
if forbidden_hits:
blockers.append("forbidden_payload_detected")
if evidence_ref_hits:
blockers.append("unsafe_evidence_ref_detected")
accepted = not blockers
return {
"template_id": template_id,
"github_repo": str(
response.get("github_repo") or target.get("github_repo") or ""
),
"submission_mode": submission_mode,
"status": "preflight_passed_read_only_intake_candidate"
if accepted
else "blocked_owner_response_candidate",
"accepted_for_read_only_intake": accepted,
"decision": decision,
"allowed_decision_count": len(acceptable_decisions),
"required_field_count": len(required_fields),
"missing_required_fields": missing_required_fields,
"unsupported_fields": unsupported_fields,
"redacted_evidence_ref_count": len(evidence_refs),
"redacted_evidence_refs": evidence_refs,
"forbidden_hits": forbidden_hits,
"evidence_ref_hits": evidence_ref_hits,
"blockers": sorted(set(blockers)),
"owner_response_received": False,
"owner_response_accepted": False,
"safe_credential_evidence_accepted": False,
"execution_authorized": False,
"repo_creation_authorized": False,
"visibility_change_authorized": False,
"refs_sync_authorized": False,
}
def _build_target(
*,
decision: dict[str, Any],
@@ -873,6 +1111,113 @@ def _rejection_rules(owner_response: dict[str, Any]) -> list[str]:
]
def _has_response_value(value: Any) -> bool:
if isinstance(value, str):
return bool(value.strip())
if isinstance(value, list):
return any(_has_response_value(item) for item in value)
return value is not None
def _response_strings(value: Any) -> list[str]:
if isinstance(value, str):
return [value] if value.strip() else []
if isinstance(value, list):
return [str(item) for item in value if str(item).strip()]
return []
def _forbidden_payload_hits(
value: Any,
*,
forbidden_payloads: set[str],
path: str = "$",
) -> list[dict[str, str]]:
hits: list[dict[str, str]] = []
if isinstance(value, dict):
for key, child in value.items():
key_text = str(key)
key_lower = key_text.lower()
if (
key_lower in forbidden_payloads
or any(fragment in key_lower for fragment in _FORBIDDEN_KEY_FRAGMENTS)
):
hits.append(
{
"path": f"{path}.{key_text}",
"kind": "forbidden_field",
"match": key_text,
}
)
hits.extend(
_forbidden_payload_hits(
child,
forbidden_payloads=forbidden_payloads,
path=f"{path}.{key_text}",
)
)
return hits
if isinstance(value, list):
for index, child in enumerate(value):
hits.extend(
_forbidden_payload_hits(
child,
forbidden_payloads=forbidden_payloads,
path=f"{path}[{index}]",
)
)
return hits
if isinstance(value, str):
lowered = value.lower()
for forbidden in sorted(forbidden_payloads):
if forbidden and forbidden.lower() in lowered:
hits.append(
{
"path": path,
"kind": "forbidden_payload_label",
"match": forbidden,
}
)
for label, pattern in _SENSITIVE_VALUE_PATTERNS:
if pattern.search(value):
hits.append(
{
"path": path,
"kind": label,
"match": label,
}
)
return hits
def _evidence_ref_hits(evidence_refs: list[str]) -> list[dict[str, str]]:
hits: list[dict[str, str]] = []
for index, evidence_ref in enumerate(evidence_refs):
if not (
evidence_ref.startswith("docs/")
or evidence_ref.startswith("reports/")
or evidence_ref.startswith("owner-metadata:")
or evidence_ref.startswith("redacted:")
):
hits.append(
{
"path": f"evidence_refs[{index}]",
"kind": "unsupported_evidence_ref_scheme",
"match": evidence_ref,
}
)
for label, pattern in _SENSITIVE_VALUE_PATTERNS:
if pattern.search(evidence_ref):
hits.append(
{
"path": f"evidence_refs[{index}]",
"kind": label,
"match": label,
}
)
return hits
def _dict(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
@@ -888,6 +1233,6 @@ def _strings(value: Any) -> list[str]:
def _int(value: Any) -> int:
if isinstance(value, bool):
return int(value)
if isinstance(value, (int, float)):
if isinstance(value, int | float):
return int(value)
return 0

View File

@@ -284,7 +284,6 @@ def _require_boundaries(payload: dict[str, Any]) -> None:
summary = _summary(payload)
for key in (
"manager_registry_accepted_count",
"post_enable_readback_passed_count",
"runtime_gate_count",
"host_write_authorized_count",
"active_response_authorized_count",
@@ -299,7 +298,8 @@ def _require_boundaries(payload: dict[str, Any]) -> None:
passed = _int(summary.get("reviewer_validation_passed_count"))
failed = _int(summary.get("reviewer_validation_failed_count"))
quarantined = _int(summary.get("reviewer_validation_quarantined_count"))
if any(value < 0 for value in (received, accepted, ready, passed, failed, quarantined)):
post_enable = _int(summary.get("post_enable_readback_passed_count"))
if any(value < 0 for value in (received, accepted, ready, passed, failed, quarantined, post_enable)):
raise ValueError("Wazuh manager registry reviewer validation counters 不得為負數")
if accepted > received:
raise ValueError("owner_registry_export_accepted_count 不得大於 received_count")
@@ -307,6 +307,8 @@ def _require_boundaries(payload: dict[str, Any]) -> None:
raise ValueError("reviewer_validation_ready_count 不得大於 received_count")
if passed > accepted:
raise ValueError("reviewer_validation_passed_count 不得大於 accepted_count")
if post_enable > passed:
raise ValueError("post_enable_readback_passed_count 不得大於 reviewer_validation_passed_count")
if failed and passed:
raise ValueError("reviewer_validation_failed_count 與 passed_count 不得同時為正")
if quarantined and accepted:

View File

@@ -8,6 +8,7 @@ import pytest
from src.services.github_target_private_backup_evidence_gate import (
load_latest_github_target_private_backup_evidence_gate,
preflight_github_target_owner_response_submission,
)
from src.services.snapshot_paths import default_security_dir
@@ -208,6 +209,62 @@ def test_github_target_private_backup_gate_rejects_missing_source_write_flags(tm
load_latest_github_target_private_backup_evidence_gate(tmp_path)
def test_github_target_owner_response_preflight_accepts_redacted_evidence_refs():
preflight = preflight_github_target_owner_response_submission(
_valid_owner_response_submission()
)
assert (
preflight["schema_version"]
== "github_target_owner_response_intake_preflight_v1"
)
assert preflight["status"] == "ready_for_read_only_owner_response_intake"
assert preflight["mode"] == "validate_owner_response_only_no_persist_no_github_write"
assert preflight["summary"]["candidate_response_item_count"] == 1
assert preflight["summary"]["preflight_passed_response_item_count"] == 1
assert preflight["summary"]["preflight_blocked_response_item_count"] == 0
assert preflight["summary"]["owner_response_received_count"] == 0
assert preflight["summary"]["owner_response_accepted_count"] == 0
assert preflight["summary"]["safe_credential_accepted_evidence_count"] == 0
assert preflight["summary"]["github_api_write_allowed"] is False
assert preflight["summary"]["repo_creation_authorized"] is False
assert preflight["summary"]["refs_sync_authorized"] is False
assert preflight["operation_boundaries"]["persist_submission_allowed"] is False
assert preflight["operation_boundaries"]["github_api_write_allowed"] is False
assert preflight["operation_boundaries"]["private_clone_url_collection_allowed"] is False
assert preflight["authorization_flags"]["owner_response_execution_authorized"] is False
assert preflight["responses"][0]["accepted_for_read_only_intake"] is True
assert preflight["responses"][0]["owner_response_received"] is False
assert preflight["responses"][0]["owner_response_accepted"] is False
def test_github_target_owner_response_preflight_blocks_credentials_and_commands():
submission = _valid_owner_response_submission()
submission["responses"][0]["private_clone_url_credential"] = (
"https://owner:ghp_1234567890abcdefghijklmnopqrstu@github.com/owenhytsai/awoooi.git"
)
submission["responses"][0]["repo_creation_command"] = (
"gh repo create owenhytsai/awoooi --private"
)
preflight = preflight_github_target_owner_response_submission(submission)
assert preflight["status"] == "blocked_owner_response_intake_preflight"
assert preflight["summary"]["candidate_response_item_count"] == 1
assert preflight["summary"]["preflight_passed_response_item_count"] == 0
assert preflight["summary"]["preflight_blocked_response_item_count"] == 1
assert preflight["summary"]["forbidden_payload_hit_count"] >= 3
assert preflight["summary"]["owner_response_received_count"] == 0
assert preflight["summary"]["owner_response_accepted_count"] == 0
assert preflight["summary"]["github_api_write_allowed"] is False
response = preflight["responses"][0]
assert response["accepted_for_read_only_intake"] is False
assert "forbidden_payload_detected" in response["blockers"]
assert "unsupported_response_fields" in response["blockers"]
assert response["execution_authorized"] is False
assert response["repo_creation_authorized"] is False
def _copy_security_snapshots(tmp_path: Path) -> None:
source_dir = default_security_dir(Path(__file__))
for filename in (
@@ -219,3 +276,34 @@ def _copy_security_snapshots(tmp_path: Path) -> None:
"github-target-missing-source-readiness.snapshot.json",
):
shutil.copy(source_dir / filename, tmp_path / filename)
def _valid_owner_response_submission() -> dict[str, object]:
return {
"submission_mode": "read_only_markdown_response",
"responses": [
{
"template_id": "target-awoooi-refs-blocked",
"github_repo": "owenhytsai/awoooi",
"owner_role_or_team": "platform-owner",
"decision": "hold_pending_refs_truth",
"decision_reason": "Need refs truth review before any sync action.",
"affected_scope": "awoooi github backup target",
"redacted_evidence_refs": [
"docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md",
"docs/security/source-control-ref-detail-diff.snapshot.json",
],
"evidence_refs": [
"docs/security/source-control-workflow-secret-name-inventory.snapshot.json"
],
"followup_owner": "platform-owner",
"rollback_owner": "platform-owner",
"maintenance_window": "not_authorized",
"validation_plan": "read-only refs truth review only",
"canonical_source": "gitea_main",
"github_target_disposition": "existing_private_candidate",
"visibility_review_owner": "platform-owner",
"refs_truth_review_owner": "platform-owner",
}
],
}

View File

@@ -59,3 +59,60 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g
assert intake["not_approval"] is True
assert data["targets"][0]["owner_response_execution_authorized"] is False
assert "192.168.0." not in response.text
def test_github_target_owner_response_intake_preflight_endpoint_blocks_secrets():
app = FastAPI()
app.include_router(router, prefix="/api/v1")
client = TestClient(app)
response = client.post(
"/api/v1/agents/github-target-owner-response-intake-preflight",
json={
"submission_mode": "read_only_markdown_response",
"responses": [
{
"template_id": "target-awoooi-refs-blocked",
"github_repo": "owenhytsai/awoooi",
"owner_role_or_team": "platform-owner",
"decision": "hold_pending_refs_truth",
"decision_reason": "Need refs truth review before sync.",
"affected_scope": "awoooi github backup target",
"redacted_evidence_refs": [
"docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md"
],
"evidence_refs": [
"docs/security/source-control-ref-detail-diff.snapshot.json"
],
"followup_owner": "platform-owner",
"rollback_owner": "platform-owner",
"maintenance_window": "not_authorized",
"validation_plan": "read-only refs truth review only",
"canonical_source": "gitea_main",
"github_target_disposition": "existing_private_candidate",
"visibility_review_owner": "platform-owner",
"refs_truth_review_owner": "platform-owner",
"private_clone_url_credential": (
"https://owner:ghp_1234567890abcdefghijklmnopqrstu@github.com/owenhytsai/awoooi.git"
),
}
],
},
)
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "github_target_owner_response_intake_preflight_v1"
assert data["status"] == "blocked_owner_response_intake_preflight"
assert data["summary"]["preflight_passed_response_item_count"] == 0
assert data["summary"]["preflight_blocked_response_item_count"] == 1
assert data["summary"]["owner_response_received_count"] == 0
assert data["summary"]["owner_response_accepted_count"] == 0
assert data["summary"]["safe_credential_accepted_evidence_count"] == 0
assert data["operation_boundaries"]["persist_submission_allowed"] is False
assert data["operation_boundaries"]["github_api_write_allowed"] is False
assert data["operation_boundaries"]["private_clone_url_collection_allowed"] is False
assert data["authorization_flags"]["owner_response_execution_authorized"] is False
assert data["responses"][0]["accepted_for_read_only_intake"] is False
assert "forbidden_payload_detected" in data["responses"][0]["blockers"]
assert "192.168.0." not in response.text

View File

@@ -83,19 +83,20 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_contract_has_passed_r
assert payload["schema_version"] == "iwooos_wazuh_manager_registry_reviewer_validation_readback_v1"
assert payload["source_schema_version"] == "wazuh_manager_registry_reviewer_validation_v1"
assert payload["status"] == "accepted_for_readonly_posture_only"
assert payload["mode"] == "committed_validation_passed_readback_no_runtime_no_secret_collection"
assert payload["status"] == "post_enable_iwooos_readback_passed_no_runtime_no_secret_collection"
assert payload["mode"] == "committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection"
assert payload["summary"]["expected_scope_alias_count"] == 6
assert payload["summary"]["required_owner_field_count"] == 28
assert payload["summary"]["per_host_required_field_count"] == 9
assert payload["summary"]["reviewer_validation_check_count"] == 10
assert payload["summary"]["outcome_lane_count"] == 13
assert payload["summary"]["outcome_lane_count"] == 14
assert payload["summary"]["evidence_slot_count"] == 6
assert payload["summary"]["forbidden_payload_count"] == 27
assert payload["summary"]["owner_registry_export_received_count"] == 1
assert payload["summary"]["owner_registry_export_accepted_count"] == 1
assert payload["summary"]["reviewer_validation_ready_count"] == 1
assert payload["summary"]["reviewer_validation_passed_count"] == 1
assert payload["summary"]["post_enable_readback_passed_count"] == 1
assert payload["summary"]["reviewer_validation_quarantined_count"] == 0
assert payload["summary"]["manager_registry_accepted_count"] == 0
assert payload["summary"]["runtime_gate_count"] == 0
@@ -115,7 +116,7 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_evidence_slots_are_ac
assert all(item["received"] is True for item in payload["evidence_slots"])
assert all(item["accepted"] is True for item in payload["evidence_slots"])
assert all(item["quarantined"] is False for item in payload["evidence_slots"])
assert all(item["next_gate"] == "post_enable_iwooos_readback" for item in payload["evidence_slots"])
assert all(item["next_gate"] == "manager_registry_acceptance_evidence_review" for item in payload["evidence_slots"])
assert "managed_core_node_a" in payload["expected_scope_aliases"]
assert "manager_registry_agent_counts" in [item["slot_id"] for item in payload["evidence_slots"]]
@@ -129,6 +130,7 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe()
assert data["summary"]["owner_registry_export_received_count"] == 1
assert data["summary"]["owner_registry_export_accepted_count"] == 1
assert data["summary"]["reviewer_validation_passed_count"] == 1
assert data["summary"]["post_enable_readback_passed_count"] == 1
assert data["summary"]["manager_registry_accepted_count"] == 0
assert data["summary"]["runtime_gate_count"] == 0
assert len(data["reviewer_validation_checks"]) == 10
@@ -145,6 +147,10 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe()
marker == "wazuh_manager_registry_reviewer_validation_passed_count=1"
for marker in data["boundary_markers"]
)
assert any(
marker == "wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count=1"
for marker in data["boundary_markers"]
)
assert any(
marker == "wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0"
for marker in data["boundary_markers"]
@@ -202,6 +208,7 @@ def test_iwooos_wazuh_manager_registry_owner_export_validation_api_does_not_pers
assert readback["summary"]["owner_registry_export_received_count"] == 1
assert readback["summary"]["owner_registry_export_accepted_count"] == 1
assert readback["summary"]["reviewer_validation_passed_count"] == 1
assert readback["summary"]["post_enable_readback_passed_count"] == 1
assert readback["summary"]["manager_registry_accepted_count"] == 0
assert readback["summary"]["runtime_gate_count"] == 0

View File

@@ -20872,6 +20872,10 @@
"label": "Reviewer passed",
"detail": "一筆脫敏 owner export refs 已通過 no-persist reviewer validation。"
},
"postEnable": {
"label": "Post-enable",
"detail": "正式 API 與前台已讀回 reviewer passed這不是 live Wazuh 查詢授權。"
},
"received": {
"label": "已收 export",
"detail": "已收到一筆 owner-provided redacted registry export refs。"

View File

@@ -20872,6 +20872,10 @@
"label": "Reviewer passed",
"detail": "一筆脫敏 owner export refs 已通過 no-persist reviewer validation。"
},
"postEnable": {
"label": "Post-enable",
"detail": "正式 API 與前台已讀回 reviewer passed這不是 live Wazuh 查詢授權。"
},
"received": {
"label": "已收 export",
"detail": "已收到一筆 owner-provided redacted registry export refs。"

View File

@@ -32,6 +32,21 @@ import { QueueTab } from './tabs/queue-tab'
import { AgentMarketTab } from './tabs/agent-market-tab'
import { AutomationInventoryTab } from './tabs/automation-inventory-tab'
const GOVERNANCE_SECTION_IDS = [
'slo',
'events',
'queue',
'agent-market',
'automation-inventory',
] as const
function normalizeGovernanceSectionId(value: string | null): string | undefined {
if (!value) return undefined
return GOVERNANCE_SECTION_IDS.includes(value as (typeof GOVERNANCE_SECTION_IDS)[number])
? value
: undefined
}
export default function GovernancePage({
params,
}: {
@@ -50,16 +65,32 @@ export default function GovernancePage({
const activeSection = governanceSections.find(section => section.id === requestedTab) ?? governanceSections[0]
useEffect(() => {
const tab = new URLSearchParams(window.location.search).get('tab') ?? undefined
const hashTab = window.location.hash ? window.location.hash.slice(1) : null
const tab = normalizeGovernanceSectionId(
new URLSearchParams(window.location.search).get('tab') ?? hashTab
)
setRequestedTab(tab)
if (!tab) return
const section = document.getElementById(tab)
section?.scrollIntoView({ block: 'start' })
let cancelled = false
const scrollToRequestedSection = () => {
if (cancelled) return
document.getElementById(tab)?.scrollIntoView({ block: 'start' })
}
const frame = window.requestAnimationFrame(scrollToRequestedSection)
const timers = [150, 600, 1500, 3000].map(delay =>
window.setTimeout(scrollToRequestedSection, delay)
)
return () => {
cancelled = true
window.cancelAnimationFrame(frame)
timers.forEach(timer => window.clearTimeout(timer))
}
}, [])
return (
<AppLayout locale={params.locale}>
<div className="min-w-0 overflow-x-hidden">
{/* ComplianceBadge 橫幅 — PR 3 接 /governance/compliance-score API */}
<GlassCard variant="subtle" padding="sm" className="mb-3">
<div className="flex items-center gap-2">
@@ -79,7 +110,7 @@ export default function GovernancePage({
return (
<Link
key={section.id}
href={`/${params.locale}/governance#${section.id}`}
href={`/${params.locale}/governance?tab=${section.id}#${section.id}`}
className="group min-w-0 border bg-white p-3 text-[#141413] no-underline transition hover:border-[#d97757] hover:bg-[#fffaf7]"
style={{
borderColor: selected ? '#d97757' : '#e0ddd4',
@@ -125,6 +156,7 @@ export default function GovernancePage({
)
})}
</div>
</div>
</AppLayout>
)
}

View File

@@ -2479,7 +2479,7 @@ const wazuhManagerRegistryReviewerValidationBoundaries = [
'wazuh_manager_registry_reviewer_validation_required_owner_field_count=28',
'wazuh_manager_registry_reviewer_validation_per_host_required_field_count=9',
'wazuh_manager_registry_reviewer_validation_check_count=10',
'wazuh_manager_registry_reviewer_validation_outcome_lane_count=13',
'wazuh_manager_registry_reviewer_validation_outcome_lane_count=14',
'wazuh_manager_registry_reviewer_validation_evidence_slot_count=6',
'wazuh_manager_registry_reviewer_validation_forbidden_payload_count=27',
'wazuh_manager_registry_reviewer_validation_owner_registry_export_received_count=1',
@@ -2487,7 +2487,7 @@ const wazuhManagerRegistryReviewerValidationBoundaries = [
'wazuh_manager_registry_reviewer_validation_passed_count=1',
'wazuh_manager_registry_reviewer_validation_quarantined_count=0',
'wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0',
'wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count=0',
'wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count=1',
'wazuh_manager_registry_reviewer_validation_runtime_gate_count=0',
'wazuh_api_live_query_authorized=false',
'wazuh_agent_reenroll_authorized=false',
@@ -9822,6 +9822,12 @@ function IwoooSWazuhManagerRegistryReviewerValidationBoard() {
icon: ClipboardCheck,
tone: 'steady',
},
{
key: 'postEnable',
value: summary ? String(summary.post_enable_readback_passed_count) : loading ? '...' : '1',
icon: SearchCheck,
tone: summary?.post_enable_readback_passed_count ? 'steady' : 'locked',
},
{
key: 'received',
value: summary ? String(summary.owner_registry_export_received_count) : loading ? '...' : '1',

View File

@@ -1,3 +1,134 @@
## 2026-06-27MOMO daily-sales source absence readback 與 cold-start blocker
**背景**110 runner / StockPlatform smoke 壓力已止血後,重新跑全主機 cold-start scorecard 與資料 freshness。AWOOOI / IwoooS / Stock / 188 主要 public routes 可用,但整體 cold-start 仍不能宣告 full green目前主要業務資料 blocker 是 188 MOMO daily sales freshness。
**執行邊界**
- 本輪只做 read-only preflight、log readback、檔名 / mtime / size 層級來源搜尋與 scorecard 彙整。
- 未做 DB write / truncate / restore / manual import未移動 Drive 檔案,未重啟 Docker / Nginx / K3s / scheduler未讀 token value、raw session、SQLite、`.env` 或 secret。
**cold-start / scorecard 結果**
- `scripts/reboot-recovery/post-reboot-readiness-summary.sh` artifact`/tmp/awoooi-post-reboot-readiness-20260627-codex-rerun/summary.txt`
- `POST_START_RESULT=BLOCKED``POST_START_PASS=37``POST_START_WARN=3``POST_START_BLOCKED=2``SERVICE_GREEN=0`
- `PRODUCT_DATA_GREEN=1`、Stock freshness `ok`latest trading date `2026-06-26``STOCK_BLOCKERS=none`
- `BACKUP_CORE_GREEN=1`,但 `DR_ESCROW_BLOCKED=1``ESCROW_MISSING_COUNT=5`
- Wazuh route `200`,但 `WAZUH_MANAGER_REGISTRY_ACCEPTED=0``WAZUH_RUNTIME_GATE=0``RUNTIME_ACTION_AUTHORIZED=0`
- 直接 cold-start rerun`PASS=88``WARN=0``BLOCKED=1`;唯一 blocker 是 `188 momo daily sales data stale beyond 3 days`
- 20:48 next-gate dispatch 使用同一份 summary 回傳 `DISPATCH_RC=2``SERVICE_GREEN=0``NEXT_REQUIRED_GATES=credential_escrow_evidence,wazuh_manager_registry_export``DISPATCH_AUTHORIZED=0``REQUEST_SENT_COUNT=0``HOST_WRITE_AUTHORIZED=0``SECRET_VALUE_COLLECTION_ALLOWED=0`,並停在 `NEXT_STEP=restore_service_before_boundary_dispatch`;因此目前不可把 escrow / Wazuh gates 當成已可送出的 owner packet。
**MOMO readback 結果**
- `scripts/reboot-recovery/momo-drive-token-source-recovery-preflight.sh` 結果:`PASS=20``WARN=3``BLOCKED=2`
- MOMO healthlocal / public health 皆 `200`runtime version `V10.725`app health `healthy`
- DB daily range`109061|2025-07-01|2026-06-24`freshness `3|2026-06-24`
- current monthly 與 sync snapshot parity`15383|15383|2026-06-01|2026-06-24|2026-06-01|2026-06-24`
- latest import job `57``completed|即時業績_當日.xlsx|15383|15383|0`,表示 2026-06-25 13:16-13:18 的已匯入來源處理乾淨,但資料仍只到 `2026-06-24`
- Drive pending intake`LOCAL_EXACT_DAILY_SOURCE_COUNT=0`archive / global latest evidence 仍停在 `2026-06-25T04:21:47.000Z`
- `momo-scheduler` 36h log 顯示 Google Drive 連線成功、定期檢查 `當日業績匯入`,但多次回報找不到 Excelscheduler 是 healthy / registered不是目前 freshness blocker 的主因。
- 188 與本機安全範圍檔名搜尋只找到舊 `即時業績_當日_20260112.xlsx` 候選,未找到可用於 2026-06-26 / 2026-06-27 的合法 daily-sales source。
- 20:54 二次 preflight 仍為 `DRIVE_INTAKE_COUNT=0`、archive / global latest `2026-06-25T04:21:47.000Z`、DB daily freshness `3|2026-06-24`、latest import job `57 completed``momo-pro-system` / `momo-scheduler` containers 仍 healthy且最近 scheduler log 只有排程註冊與一般 warning沒有新 Excel 入站或成功匯入證據。
**DR / Wazuh gate readback**
- 110 `/backup/scripts/offsite-escrow-evidence-report.sh --no-color` 顯示 rclone offsite configured、full offsite marker fresh、local backup repos checkable但 5 個 credential escrow marker 全缺:`restic_repository_password``offsite_provider_credentials``break_glass_admin_credentials``dns_registrar_recovery``oauth_ai_provider_recovery`
- `scripts/security/wazuh-manager-registry-reviewer-validation.py` 通過 repo contract validation但 snapshot 仍是 `received=0 accepted=0 runtime_gate=0`route / transport / index pattern 不能替代 manager registry accepted。
- 本輪沒有寫 escrow marker沒有產生 owner response沒有查 Wazuh live API / secret也沒有 Wazuh active response、agent re-enroll、restart、host write 或 Kali active scan。
**2026-06-27 21:00 gate 補強**
- 新增 `scripts/reboot-recovery/momo-source-arrival-gate.py`,只解析 `momo-drive-token-source-recovery-preflight.sh` 產出的 log 或 stdin不連線、不查 token、不 import、不移動 Drive、不寫 DB。
- 真實 20:54 preflight log 驗證:`MOMO_SOURCE_ARRIVAL_GATE status=blocked_source_absent_fail_closed source_intake=0 freshness=3|2026-06-24 safe_import_preflight_allowed=0 runtime_write_authorized=0 db_write_authorized=0 drive_move_authorized=0 next_step=wait_for_legitimate_daily_sales_source_then_rerun_gate`exit code `2`
- 合成 source-arrived case 驗證Drive intake count `1` 且 freshness stale 時,只回 `source_arrived_ready_for_safe_import_preflight``safe_import_preflight_allowed=1`,仍固定 `runtime_write_authorized=0``db_write_authorized=0``drive_move_authorized=0`
- 合成 freshness-green case 驗證freshness `1|2026-06-26` 時回 `freshness_already_green_recheck_cold_start`,下一步仍是重跑 post-reboot summary不得直接宣告 full green。
**結論**
- 目前狀態是 `SERVICE_BLOCKED_MOMO_SOURCE_ABSENCE` / `SOURCE_ABSENT_FAIL_CLOSED`,不是 runner、Docker、Nginx、K3s 或 scheduler 事故。
- 禁止用舊 archive、舊 sample、本機舊檔、手寫 DB、truncate / restore 或 manual Drive movement 製造 freshness 假綠。
- 解除 blocker 需要新的合法 `即時業績_當日` source 出現在 `當日業績匯入`,或 owner-approved safe source evidence ref之後才可在 maintenance-safe path 執行匯入,並要求 `sync_success=true`、source 只在成功後移動、daily snapshot / realtime monthly bounds 一致、freshness `<=2`,再重跑 cold-start scorecard。
**下一步**
- 保持 fail-closed等待合法來源到位後做 read-only preflight recheck。
- 若有 owner-approved source evidence ref另開 maintenance window 走安全匯入路徑;仍不得在沒有來源證據時宣告 all-green。
## 2026-06-27110 Gitea runner 降壓防回彈與 workflow label 收斂
**背景**110 CPU 事故已確認主因是 Gitea runner 反覆拉起 StockPlatform headless Chrome smoke前一輪已停止 `gitea-act-runner-host.service`、清掉 Actions / smoke並把 live runner labels 收斂為 `awoooi-ubuntu` / `awoooi-host`。本輪目標是防止 cold-start / startup 流程把 runner 又自動拉起,並補齊 AWOOI workflow label 與 post-deploy pressure gate。
**完成內容**
- `.gitea/workflows/cd.yaml``post-deploy-checks` 在 checkout 後新增 `Wait for Host Web Build Pressure`,避免 Alert Chain / Source Link / Monitoring / Playwright smoke 疊到 110 既有 build / smoke / load 壓力。
- `.gitea/workflows/ansible-lint.yml``self-hosted` 收斂為 `awoooi-ubuntu`AWOOI workflows 目前只剩 `awoooi-ubuntu` / `awoooi-host` 兩類 label。
- `scripts/reboot-recovery/awoooi-startup-110.sh` 改成預設不自動啟動 Gitea host runner只有明確設定 `AWOOOI_START_GITEA_RUNNER_ON_BOOT=1` 才允許 startup 拉起 runner。
- live `/usr/local/bin/awoooi-startup-110.sh` 已安裝新版,舊檔備份為 `/usr/local/bin/awoooi-startup-110.sh.bak-20260627-runner-inactive`;本輪沒有執行 startup script也沒有重啟 runner。
- closeout 時發現 Docker-wrapped `gitea-runner` 短暫回彈為 running確認 active task containers `0` 後,已只針對 `gitea-runner` 執行 `docker update --restart=no``docker stop -t 60`,恢復 `Restart=no Status=exited Running=false`
- `ops/runner/audit-workflow-labels.py` 修正 local fallback沒有 Gitea auth 但指定 `--local-repo` 時不再輸出假空白。
- `ops/runner/check-runner-isolation-readiness.sh` 認得 `awoooi-ubuntu`,避免把新 label 誤判成 unknown / mixed owner。
- `ops/runner/README.md` 更新 2026-06-27 runner 降壓狀態、hard-fail pressure gate、startup 開關與 workflow label 邊界。
**驗證結果**
- `bash -n scripts/reboot-recovery/awoooi-startup-110.sh scripts/ci/wait-host-web-build-pressure.sh ops/runner/check-runner-isolation-readiness.sh ops/runner/audit-runner-pool.sh`:通過。
- `python3 -m py_compile ops/runner/audit-workflow-labels.py scripts/ops/host-runaway-process-exporter.py`:通過。
- Gitea workflow YAML parse10 個 workflow 全部通過。
- `rg "runs-on: (ubuntu-latest|self-hosted|ubuntu-22.04|ubuntu-24.04)" .gitea/workflows`:無命中。
- `ops/runner/audit-workflow-labels.py --repo wooo/awoooi --local-repo wooo/awoooi=/Users/ogt/awoooi`labels 只剩 `awoooi-host` / `awoooi-ubuntu`
- 110 readback`gitea-act-runner-host.service=inactive`、Actions containers `0`、active CI groups `0`、StockPlatform orphan groups `0`
- Docker-wrapped `gitea-runner``Restart=no Status=exited Running=false`
- 110 readinessprimary labels `awoooi-ubuntu` / `awoooi-host` 均為 `awoooi_dedicated``mixed_owner_classes=0`active action containers `none`
- 110 pressure gate 目前 `GATE_RC=1`,原因是 `load5/core 0.886667 > 0.85`top process 顯示主要是 `restic` 6h backup不是 Gitea Actions / Chrome smoke 事故復燃。
- 110 local Gitea / Sentry / Alertmanager / Grafana health readback`200 / 302 / 200 / 200`
**邊界與下一步**
- runner inactive 是刻意降壓;未完成限流 / 搬遷前不可直接重開。
- 本輪未重啟 Docker / Nginx / firewall / K3s未 kill process未讀 raw sessions / SQLite / auth / secret。
- 下一個 P0把 StockPlatform smoke 改成排程限流或搬到非 110 runner再做全主機 cold-start scorecard 與資料 freshness readback。
## 2026-06-27IwoooS Wazuh owner export reviewer validation passed 本地完成
## 2026-06-27 — 21:45 IwoooS Wazuh reviewer post-enable readback 正式讀回完成
**時間與來源**
- 2026-06-27 21:24-21:45 Asia/Taipei。
- 來源feature branch `codex/iwooos-post-enable-readback-20260627`、Gitea main、Gitea Actions public HTML、production API / `/zh-TW/iwooos` desktop / mobile smoke。
**完成內容**
- `GET /api/v1/iwooos/wazuh-manager-registry-reviewer-validation` 已正式讀回 `post_enable_iwooos_readback_passed_no_runtime_no_secret_collection`
- API service 放寬 `post_enable_readback_passed_count` 從 0 更新為 1但仍強制 `manager_registry_accepted_count``runtime_gate_count``host_write_authorized_count``active_response_authorized_count``secret_value_collection_allowed_count` 維持 0。
- 前台 `/zh-TW/iwooos` Wazuh manager registry reviewer validation 卡片新增 `Post-enable = 1`,文案明確標示這不是 live Wazuh 查詢授權。
- security guard 與 contract tests 已同步 lane count `13 -> 14`,新增 `post_enable_iwooos_readback_passed``manager_registry_acceptance_evidence_review` 下一關。
**Gitea / deploy 狀態**
- code commit`c73ce995e feat(iwooos): mark wazuh reviewer post-enable readback`
- 後續 main commit`1a8613c9e fix(governance): stabilize automation tab deep link`,包含 `c73ce995e`
- deploy marker`1a6f8f427 chore(cd): deploy 1a8613c [skip ci]`
- `c73ce995e` 單獨 `code-review.yaml #3693` / `cd.yaml #3692` 因後續 main push 被 concurrency 取消;最新 main `code-review.yaml #3695` 成功,`cd.yaml #3694` 成功。
**本地驗證結果**
- `DATABASE_URL=sqlite:///test.db python3.11 -m pytest apps/api/tests/test_iwooos_wazuh_manager_registry_reviewer_validation.py apps/api/tests/test_iwooos_runtime_security_readback.py apps/api/tests/test_iwooos_wazuh_managed_host_coverage.py apps/api/tests/test_iwooos_wazuh_api.py -q``23 passed`
- `python3 scripts/security/wazuh-manager-registry-reviewer-validation.py --root .``post_enable=1 runtime_gate=0`
- `python3 scripts/security/iwooos-frontend-display-redaction-guard.py --root .`:通過。
- `python3 scripts/security/security-mirror-progress-guard.py --root .`:通過。
- `python3 -m py_compile ...`、JSON parse、`git diff --check``pnpm --dir apps/web typecheck`:通過。
**production API readback**
- `/api/v1/health?_v=1a6f8f427`HTTP `200``status=healthy``environment=prod``mock_mode=false`
- `GET /api/v1/iwooos/wazuh-manager-registry-reviewer-validation?_v=1a6f8f427-final`HTTP `200`
- schema`iwooos_wazuh_manager_registry_reviewer_validation_readback_v1`
- status`post_enable_iwooos_readback_passed_no_runtime_no_secret_collection`
- mode`committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection`
- summary`outcome_lane_count=14``owner_registry_export_received_count=1``owner_registry_export_accepted_count=1``reviewer_validation_passed_count=1``post_enable_readback_passed_count=1`
- valid redacted sample POST`accepted_for_readonly_posture_only``mode=no_persist_validation_no_runtime_no_secret_collection`POST-local `post_enable_readback_passed_count=0`POST 後 GET global 仍維持 `post_enable_readback_passed_count=1`,沒有累加或保存 payload。
**production browser smoke**
- Desktop `1360x900``/zh-TW/iwooos?_v=1a6f8f427-desktop-dom` HTTP `200`、console error `0`、水平溢出 `0`、forbidden hits `0`
- Mobile `384x900``/zh-TW/iwooos?_v=1a6f8f427-mobile-dom` HTTP `200`、console error `0`、水平溢出 `0`、forbidden hits `0`
- 前台可見片段:`Post-enable` / `1` / `正式 API 與前台已讀回 reviewer passed這不是 live Wazuh 查詢授權。`
**仍維持 0 / false**
- `manager_registry_accepted_count=0``runtime_gate_count=0``host_write_authorized_count=0``active_response_authorized_count=0``secret_value_collection_allowed_count=0`
- `wazuh_api_live_query_authorized=false``wazuh_agent_reenroll_authorized=false``wazuh_agent_restart_authorized=false``wazuh_active_response_authorized=false``raw_wazuh_payload_storage_allowed=false``not_authorization=true`
**未做**
- 沒有 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作;沒有讀 secret 明文;沒有重新註冊 agent沒有 Wazuh restart沒有 Wazuh active response沒有 Kali active scan沒有 force push。
**完成度 / 下一步**
- Wazuh reviewer post-enable readback`85% -> 100%`
- IwoooS 整體:保守 `70% -> 72%`。此段只完成 production API / 前台 readback不代表 Wazuh 全主機納管或 manager registry accepted 已完成。
- 下一個 P0`manager_registry_acceptance_evidence_review`,必須以 manager registry accepted evidence 與 host/product/agent scope 對帳推進Dashboard 200、前台可見或 API 200 仍不可當成全主機納管完成。
## 2026-06-27 — 21:24 GitHub backup owner response intake readiness 正式讀回完成
**時間與來源**
@@ -27,6 +158,7 @@
- `owner_response_allowed_response_field_count=25``owner_response_forbidden_payload_count=15``owner_response_collection_check_count=6``owner_response_intake_preflight_check_count=6`
- `owner_response_request_execution_authorized=false``owner_response_received_count=0``owner_response_accepted_count=0``safe_credential_accepted_evidence_count=0``execution_ready_count=0``blocked_target_count=9`
- `owner_response_intake_readiness.status=ready_to_collect_read_only_owner_response_not_authorization``request_ready=true``execution_authorized=false``not_approval=true`
- forbidden / redaction proof`private_clone_url_credential` 仍在 forbidden payloads`read_only_markdown_response` 是允許 submission mode`create_github_repo` 仍在 still forbidden正式 API 回應未命中 `192.168.0.`
**Delivery Workbench readback**
- `GET /api/v1/agents/delivery-closure-workbench?_v=9f5097f66-github-owner-intake`HTTP `200`
@@ -117,7 +249,6 @@
- 部署後讀回 `GET /api/v1/agents/github-target-private-backup-evidence-gate`,目標確認 `owner_response_request_ready=true`、requested templates `9`、forbidden payloads `15`、collection / preflight checks `6/6`,且 create/private ready 與 refs sync ready 仍維持 `0`
## 2026-06-27IwoooS Wazuh owner export reviewer validation passed 正式讀回完成
**時間與來源**
- 2026-06-27 20:42-21:22 Asia/Taipei。
- 來源:`docs/security/wazuh-manager-registry-reviewer-validation.snapshot.json``scripts/security/wazuh-manager-registry-reviewer-validation.py`、API service / tests、Gitea Actions、production API、`/zh-TW/iwooos` desktop / mobile browser smoke。

View File

@@ -296,6 +296,8 @@ NO-GO: truncate, whole-DB restore, manual Drive movement, or manual import witho
UNBLOCK: new legitimate PChome daily-sales source appears in 當日業績匯入 or an owner-approved safe import path; import job succeeds with sync_success=true; source file moves only after success; daily_sales_snapshot and realtime_sales_monthly bounds match; MOMO_DAILY_FRESHNESS <= 2.
```
2026-06-27 起,若已有 `momo-drive-token-source-recovery-preflight.sh` log先跑 `python3 scripts/reboot-recovery/momo-source-arrival-gate.py --preflight-log <log>` 做機器判讀:`blocked_source_absent_fail_closed` 代表繼續等合法來源;`source_arrived_ready_for_safe_import_preflight` 只代表可進另一個 safe import preflight不代表 DB write、Drive move、manual import 或 runtime write 已授權;`freshness_already_green_recheck_cold_start` 仍必須重跑同一 evidence chain 的 post-reboot summary 後才能更新恢復宣告。
所有回報必須使用這組詞,避免把「服務面可用」誤報成「整體 DR 完成」。
### 0.3 Codex 工作站交接判定

View File

@@ -2,7 +2,7 @@
"evidence_slots": [
{
"accepted": true,
"next_gate": "post_enable_iwooos_readback",
"next_gate": "manager_registry_acceptance_evidence_review",
"quarantined": false,
"received": true,
"required_fields": [
@@ -17,7 +17,7 @@
},
{
"accepted": true,
"next_gate": "post_enable_iwooos_readback",
"next_gate": "manager_registry_acceptance_evidence_review",
"quarantined": false,
"received": true,
"required_fields": [
@@ -30,7 +30,7 @@
},
{
"accepted": true,
"next_gate": "post_enable_iwooos_readback",
"next_gate": "manager_registry_acceptance_evidence_review",
"quarantined": false,
"received": true,
"required_fields": [
@@ -45,7 +45,7 @@
},
{
"accepted": true,
"next_gate": "post_enable_iwooos_readback",
"next_gate": "manager_registry_acceptance_evidence_review",
"quarantined": false,
"received": true,
"required_fields": [
@@ -58,7 +58,7 @@
},
{
"accepted": true,
"next_gate": "post_enable_iwooos_readback",
"next_gate": "manager_registry_acceptance_evidence_review",
"quarantined": false,
"received": true,
"required_fields": [
@@ -74,7 +74,7 @@
},
{
"accepted": true,
"next_gate": "post_enable_iwooos_readback",
"next_gate": "manager_registry_acceptance_evidence_review",
"quarantined": false,
"received": true,
"required_fields": [
@@ -150,10 +150,11 @@
"firewall_change",
"nginx_reload"
],
"generated_at": "2026-06-27T20:42:31+08:00",
"mode": "committed_validation_passed_readback_no_runtime_no_secret_collection",
"generated_at": "2026-06-27T21:45:00+08:00",
"mode": "committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection",
"no_false_green_rules": [
"reviewer validation passed 只代表脫敏 owner export refs 通過 no-persist 驗證。",
"post-enable IwoooS readback passed 只代表 production API / 前台已讀回 reviewer passed不代表 live Wazuh 查詢或 runtime action。",
"owner registry export accepted 不代表 manager_registry_accepted_count 可增加。",
"Dashboard 可見、index pattern 三綠勾、HTTP 200 或 transport observed 不可替代 manager registry counts。",
"reviewer accepted 只可更新只讀 postureactive response、agent restart、reenroll、host write、secret rotation 或掃描仍需獨立 runtime gate。"
@@ -171,7 +172,8 @@
"reject_runtime_action_request",
"ready_for_reviewer_validation",
"accepted_for_readonly_posture_only",
"waiting_post_enable_iwooos_readback"
"post_enable_iwooos_readback_passed",
"manager_registry_acceptance_evidence_review"
],
"per_host_required_fields": [
"node_alias",
@@ -271,9 +273,9 @@
},
{
"check_id": "RV-10",
"failure_lane": "waiting_post_enable_iwooos_readback",
"required_evidence": "即使 reviewer 未來接受 evidence也只能進 read-only posture必須另有 post-enable readback 才能更新 runtime truth。",
"title": "Post-enable IwoooS readback 仍是下一關"
"failure_lane": "post_enable_iwooos_readback_passed_no_runtime",
"required_evidence": "production API 與前台 smoke 已讀回 reviewer passed此讀回只更新 read-only posture不查 live Wazuh、不保存 raw payload、不開 runtime gate。",
"title": "Post-enable IwoooS readback 已讀回但不開 runtime"
}
],
"schema_version": "wazuh_manager_registry_reviewer_validation_v1",
@@ -282,7 +284,7 @@
"docs/security/wazuh-agent-visibility-owner-evidence-preflight.snapshot.json",
"docs/security/wazuh-managed-host-coverage-gate.snapshot.json"
],
"status": "accepted_for_readonly_posture_only",
"status": "post_enable_iwooos_readback_passed_no_runtime_no_secret_collection",
"summary": {
"active_response_authorized_count": 0,
"evidence_slot_count": 6,
@@ -291,11 +293,11 @@
"forbidden_payload_count": 27,
"host_write_authorized_count": 0,
"manager_registry_accepted_count": 0,
"outcome_lane_count": 13,
"outcome_lane_count": 14,
"owner_registry_export_accepted_count": 1,
"owner_registry_export_received_count": 1,
"per_host_required_field_count": 9,
"post_enable_readback_passed_count": 0,
"post_enable_readback_passed_count": 1,
"required_owner_field_count": 28,
"reviewer_validation_check_count": 10,
"reviewer_validation_failed_count": 0,

View File

@@ -41,7 +41,7 @@ resources:
images:
- name: 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER
newName: 192.168.0.110:5000/awoooi/api
newTag: 9f5097f664ef62721c7ea6afa60cb4910da568ef
newTag: 1a8613c9e6641f5d07ab3ff3dad4f8550237d04f
- name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER
newName: 192.168.0.110:5000/awoooi/web
newTag: 9f5097f664ef62721c7ea6afa60cb4910da568ef
newTag: 1a8613c9e6641f5d07ab3ff3dad4f8550237d04f

View File

@@ -132,9 +132,9 @@ runner:
| Job | runner label | 用途 |
|-----|--------------|------|
| `tests` | `ubuntu-latest` | API unit + B5 integration tests跑在 ci-runner container |
| `tests` | `awoooi-host` | API unit + B5 integration tests直接跑在 110 host runner |
| `build-and-deploy` | `awoooi-host` | Harbor login、API/Web image build/push、GitOps deploy直接跑在 110 host |
| `post-deploy-checks` | `ubuntu-latest` | Alert chain、monitoring coverage、Playwright smoke |
| `post-deploy-checks` | `awoooi-host` | Alert chain、monitoring coverage、Playwright smoke |
110 只保留 host-level `act_runner` daemon並在同一份 config 宣告兩類 label
@@ -143,9 +143,7 @@ runner:
capacity: 1
shutdown_timeout: 1h
labels:
- "ubuntu-latest:docker://192.168.0.110:5000/awoooi/ci-runner:act-22.04"
- "ubuntu-22.04:docker://192.168.0.110:5000/awoooi/ci-runner:act-22.04"
- "ubuntu-24.04:docker://192.168.0.110:5000/awoooi/ci-runner:act-22.04"
- "awoooi-ubuntu:docker://192.168.0.110:5000/awoooi/ci-runner:act-22.04"
- "awoooi-host:host"
```
@@ -208,15 +206,27 @@ AWOOI 的 Docker lock會和 AWOOI Web image 內的 Next production build 疊
- 只讀取 `ps`,不 kill / renice / reset 任何外部 process。
- 排除 AWOOI 自身 checkout、local worktree 與 Web Docker build 內的
`/app/apps/web` process避免誤判自己的部署。
- 預設最多等待 60 次、每次 10 秒;若仍有外部 build,先以 warning 放行
避免 CD 永久卡住
- 可用 `HOST_WEB_BUILD_PRESSURE_WARN_ONLY=0` 改成 hard fail但必須先確認
runner 隔離與其他 repo build 排程已收斂,避免把 shared runner 壓力轉成
部署中斷。
- 預設最多等待 60 次、每次 10 秒;若仍有外部 build / smoke / CI 壓力
hard fail避免繼續把新的 browser smoke 疊到 production host
- 只有明確設定 `HOST_WEB_BUILD_PRESSURE_WARN_ONLY=1` 才 warning 放行;這只能
用在已確認壓力來源可接受的受控補跑。
長期方向仍是 runner 隔離或 build offload此 gate 是在 shared runner 尚未
拆分前,降低重型前端 build 互相踩踏的保守保護層。
### 第四層補充: startup 不自動重開 Gitea runner
2026-06-27 110 CPU 事故止血後,`gitea-act-runner-host.service` 維持 inactive 是
刻意降壓狀態。`scripts/reboot-recovery/awoooi-startup-110.sh` 仍可修正 runner
`shutdown_timeout` 與 labels也會停用 legacy Docker runner但預設不會啟動
host runner。只有明確設定下列開關時才允許 startup 拉起 runner
```bash
AWOOOI_START_GITEA_RUNNER_ON_BOOT=1 /usr/local/bin/awoooi-startup-110.sh
```
未完成 runner 限流 / 搬遷前,不要把這個開關加入 systemd environment。
### 第五層修復: legacy Docker runner drain
2026-05-21 再次確認 110 同時存在兩個 runner
@@ -370,6 +380,12 @@ runner registration / service
三個 split runner smoke 都通過後,才 drain primary runner 並移除混合 labels。
2026-06-27 live update110 的 `gitea-act-runner-host.service` 已刻意停在
`inactive``/home/wooo/act-runner/config.yaml` labels 已收斂為
`awoooi-ubuntu``awoooi-host`capacity 仍為 `1`。這是降壓與 label isolation
狀態AWOOI workflows 也應只使用 `awoooi-ubuntu``awoooi-host`,不可再使用
`ubuntu-latest` / `self-hosted` 這類泛用 label。這不代表 runner 搬遷完成,也不代表可以直接重開 runner。
---
版本: v2.0 | 更新: 2026-03-29 | 作者: Claude Code
變更: v1.0→v2.0 序列建構取代 Job Concurrency Groups

View File

@@ -179,7 +179,7 @@ def fetch_local_labels(repo: str, branch: str, repo_path: Path) -> tuple[list[Wo
def label_owner(label: str) -> str:
value = label.strip().strip("'\"")
if value == "awoooi-host":
if value in {"awoooi-host", "awoooi-ubuntu"}:
return "awoooi_dedicated"
if value == "ewoooc-host":
return "foreign_dedicated"
@@ -234,7 +234,13 @@ def main() -> int:
error: str | None = None
if auth is not None:
repo_labels, error = fetch_gitea_labels(repo, args.branch, auth)
elif repo not in local_paths:
elif repo in local_paths:
repo_labels, local_error = fetch_local_labels(repo, args.branch, local_paths[repo])
if local_error:
errors.append(f"{repo}: {local_error}")
labels.extend(repo_labels)
continue
else:
error = "gitea_auth_unavailable"
if error and repo in local_paths:

View File

@@ -70,7 +70,7 @@ label_owner() {
local label="$1"
local label_name="${label%%:*}"
case "$label_name" in
awoooi-host)
awoooi-host|awoooi-ubuntu|awoooi-*)
printf 'awoooi_dedicated'
;;
ewoooc-host)

View File

@@ -184,15 +184,18 @@ fi
# ──────────────────────────────────────────────
# STEP 6: Gitea Act RunnerCI/CD 核心)
# 2026-04-05 Claude Code: 加入 — 解決重開機後 Gitea runner 離線、CD 失效
# 重要:必須在 Gitea server 啟動後才能啟動 runner
# 2026-06-27 Codex: 110 是 production / registry / observability 主機;
# runner 預設維持停用降壓,未完成限流 / 搬遷前不可在 startup 自動拉起。
# ──────────────────────────────────────────────
log "[6/6] 啟動 Gitea Act Runner..."
log "[6/6] 檢查 Gitea Act Runner(預設不自動啟動)..."
RUNNER_DIR="/home/wooo/act-runner"
RUNNER_SERVICE="gitea-act-runner-host.service"
START_GITEA_RUNNER_ON_BOOT="${AWOOOI_START_GITEA_RUNNER_ON_BOOT:-0}"
if [ -x "$RUNNER_DIR/act_runner" ] && [ -f "$RUNNER_DIR/config.yaml" ]; then
# 若舊的 .runner 配置指向過期 hostname先清除讓 runner 重新註冊
# 若舊的 .runner 配置指向過期 hostname只有在明確允許啟動 runner
# 時才清除重新註冊;預設降壓模式不得碰 registration 狀態。
RUNNER_FILE="$RUNNER_DIR/data/.runner"
if [ -f "$RUNNER_FILE" ]; then
if [ "$START_GITEA_RUNNER_ON_BOOT" = "1" ] && [ -f "$RUNNER_FILE" ]; then
OLD_URL=$(python3 -c "import json; d=json.load(open('$RUNNER_FILE')); print(d.get('address',''))" 2>/dev/null || echo "")
if [ "$OLD_URL" != "http://192.168.0.110:3001" ]; then
log "⚠️ runner 配置過期 ($OLD_URL),清除重新註冊..."
@@ -248,10 +251,14 @@ while idx < len(lines):
path.write_text("\n".join(output) + "\n")
PY
if systemctl list-unit-files "$RUNNER_SERVICE" >/dev/null 2>&1; then
systemctl enable --now "$RUNNER_SERVICE" >/dev/null 2>&1 || true
elif ! pgrep -f "$RUNNER_DIR/act_runner daemon" >/dev/null; then
nohup "$RUNNER_DIR/run-host-runner.sh" >> "$RUNNER_DIR/host-runner.log" 2>&1 &
if [ "$START_GITEA_RUNNER_ON_BOOT" = "1" ]; then
if systemctl list-unit-files "$RUNNER_SERVICE" >/dev/null 2>&1; then
systemctl enable --now "$RUNNER_SERVICE" >/dev/null 2>&1 || true
elif ! pgrep -f "$RUNNER_DIR/act_runner daemon" >/dev/null; then
nohup "$RUNNER_DIR/run-host-runner.sh" >> "$RUNNER_DIR/host-runner.log" 2>&1 &
fi
else
log "⏸️ Gitea host runner 維持停用;設定 AWOOOI_START_GITEA_RUNNER_ON_BOOT=1 才允許 startup 啟動"
fi
# 已停用 Docker-wrapped runner避免它搶走 host label job。
@@ -269,9 +276,11 @@ PY
# 驗證 runner 已連線 Gitea
if pgrep -f "$RUNNER_DIR/act_runner daemon" >/dev/null; then
log " Gitea host act_runner 已啟動"
else
log "⚠️ Gitea host act_runner 目前正在執行;請確認是否為受控限流 / 搬遷後狀態"
elif [ "$START_GITEA_RUNNER_ON_BOOT" = "1" ]; then
log "⚠️ Gitea host act_runner 可能尚未啟動,查看: $RUNNER_DIR/host-runner.log"
else
log "✅ Gitea host act_runner 維持 inactive 降壓狀態"
fi
else
log "⚠️ 找不到 act-runner binary/config: $RUNNER_DIR"

View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""Classify MOMO daily-sales source arrival from a read-only preflight log.
This parser never connects to MOMO, never imports files, never moves Drive
artifacts, and never authorizes DB / host / Drive writes. It turns the existing
`momo-drive-token-source-recovery-preflight.sh` evidence into a compact gate so
operators can tell whether they should keep waiting for a legitimate source or
start a separate safe-import preflight.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any
EXPECTED_IMPORT_CONFIG = "當日業績匯入|即時業績_當日"
SUMMARY_RE = re.compile(
r"^MOMO_DRIVE_TOKEN_SOURCE_PREFLIGHT "
r"PASS=(?P<pass>\d+) WARN=(?P<warn>\d+) BLOCKED=(?P<blocked>\d+) "
r"HOST=(?P<host>\S+) FRESHNESS_MAX_DAYS=(?P<freshness_max_days>\d+)"
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Classify MOMO source-arrival readiness from preflight output.",
)
parser.add_argument(
"--preflight-log",
required=True,
help="Path to momo-drive-token-source-recovery-preflight output, or '-' for stdin.",
)
parser.add_argument("--json", action="store_true", help="Print JSON result.")
return parser.parse_args()
def load_text(source: str) -> str:
if source == "-":
return sys.stdin.read()
return Path(source).read_text(encoding="utf-8")
def parse_int(value: Any, default: int | None = None) -> int | None:
try:
return int(str(value).strip())
except (TypeError, ValueError):
return default
def parse_pipe(value: str, expected_parts: int) -> list[str]:
parts = str(value or "").split("|")
if len(parts) < expected_parts:
parts.extend([""] * (expected_parts - len(parts)))
return parts[:expected_parts]
def parse_preflight(text: str) -> dict[str, Any]:
values: dict[str, str] = {}
messages = {"ok": [], "warn": [], "blocked": []}
summary: dict[str, Any] = {}
for raw_line in text.splitlines():
line = raw_line.strip()
if not line:
continue
summary_match = SUMMARY_RE.match(line)
if summary_match:
summary = {
key: parse_int(value) if key != "host" else value
for key, value in summary_match.groupdict().items()
}
continue
if line.startswith("OK: "):
messages["ok"].append(line[4:])
continue
if line.startswith("WARN: "):
messages["warn"].append(line[6:])
continue
if line.startswith("BLOCKED: "):
messages["blocked"].append(line[9:])
continue
if re.match(r"^[A-Z][A-Z0-9_]+(?:\s|$)", line):
key, _, value = line.partition(" ")
values[key] = value.strip()
return {"values": values, "messages": messages, "summary": summary}
def monthly_sync_ok(value: str) -> bool:
snapshot_count, monthly_count, dmin, dmax, mmin, mmax = parse_pipe(value, 6)
snapshot_n = parse_int(snapshot_count, 0) or 0
return (
snapshot_n > 0
and snapshot_count == monthly_count
and bool(dmin)
and bool(dmax)
and dmin == mmin
and dmax == mmax
)
def latest_import_clean(value: str) -> bool:
job_id, status, _file_name, _created, _completed, total, success, errors = parse_pipe(
value, 8
)
return (
parse_int(job_id) is not None
and status == "completed"
and parse_int(total, -1) == parse_int(success, -2)
and parse_int(errors, -1) == 0
)
def classify(parsed: dict[str, Any]) -> dict[str, Any]:
values = parsed["values"]
summary = parsed["summary"]
messages = parsed["messages"]
freshness_days_text, latest_daily_date = parse_pipe(values.get("DB_DAILY_FRESHNESS", ""), 2)
freshness_days = parse_int(freshness_days_text)
freshness_max_days = parse_int(summary.get("freshness_max_days"), 2) or 2
drive_intake_count = parse_int(values.get("DRIVE_INTAKE_COUNT"), 0) or 0
drive_failed_count = parse_int(values.get("DRIVE_FAILED_COUNT"), 0) or 0
drive_archive_latest = values.get("DRIVE_ARCHIVE_LATEST_MODIFIED", "none") or "none"
drive_global_latest = values.get("DRIVE_GLOBAL_LATEST_MODIFIED", "none") or "none"
service_ready = (
values.get("MOMO_PUBLIC_HEALTH_CODE") == "200"
and values.get("MOMO_HEALTH_CODE") == "200"
and values.get("MOMO_APP_HEALTH") == "healthy"
and values.get("SCHEDULER_RUNNING") == "true"
and values.get("SCHEDULER_HEALTH") == "healthy"
)
import_config_ok = EXPECTED_IMPORT_CONFIG in values.get("IMPORT_CONFIG", "")
sync_ok = monthly_sync_ok(values.get("DB_MONTHLY_SYNC", ""))
clean_import = latest_import_clean(values.get("DB_LATEST_DAILY_IMPORT_JOB", ""))
freshness_green = (
freshness_days is not None and 0 <= freshness_days <= freshness_max_days
)
freshness_stale = freshness_days is not None and freshness_days > freshness_max_days
blockers: list[str] = []
warnings: list[str] = []
status = "blocked_preflight_evidence_incomplete"
next_step = "rerun_momo_drive_token_source_recovery_preflight"
safe_import_preflight_allowed = False
exit_code = 2
if not summary:
blockers.append("preflight_summary_missing")
if not service_ready:
blockers.append("momo_service_or_scheduler_not_ready")
if not import_config_ok:
blockers.append("drive_import_config_not_expected_intake")
if not sync_ok:
blockers.append("current_month_snapshot_realtime_sync_not_proven")
if drive_failed_count > 0:
warnings.append("drive_failed_folder_has_matching_candidates")
if blockers:
status = "blocked_service_or_evidence_not_ready"
next_step = "repair_readonly_preflight_evidence_before_source_or_import_decision"
elif freshness_green:
status = "freshness_already_green_recheck_cold_start"
next_step = "rerun_post_reboot_readiness_summary_with_same_evidence_chain"
exit_code = 0
elif drive_intake_count > 0 and freshness_stale:
status = "source_arrived_ready_for_safe_import_preflight"
next_step = "run_owner_approved_safe_import_preflight_no_db_or_drive_write_yet"
safe_import_preflight_allowed = True
exit_code = 0
elif drive_intake_count > 0:
status = "source_arrived_freshness_unknown_recheck_before_import"
next_step = "rerun_momo_preflight_and_validate_freshness_before_import"
safe_import_preflight_allowed = True
exit_code = 1
elif freshness_stale:
status = "blocked_source_absent_fail_closed"
next_step = "wait_for_legitimate_daily_sales_source_then_rerun_gate"
else:
status = "blocked_freshness_unknown_fail_closed"
next_step = "rerun_preflight_or_repair_readonly_freshness_readback"
if not clean_import:
warnings.append("latest_daily_import_job_not_clean_completed")
return {
"schema_version": "momo_source_arrival_gate_v1",
"status": status,
"exit_code": exit_code,
"next_step": next_step,
"safe_import_preflight_allowed": safe_import_preflight_allowed,
"runtime_write_authorized": False,
"db_write_authorized": False,
"drive_move_authorized": False,
"manual_import_authorized": False,
"secret_value_collection_allowed": False,
"service_ready": service_ready,
"import_config_ok": import_config_ok,
"current_month_sync_ok": sync_ok,
"latest_import_clean": clean_import,
"freshness_days": freshness_days,
"freshness_latest_date": latest_daily_date or "unknown",
"freshness_max_days": freshness_max_days,
"drive_intake_count": drive_intake_count,
"drive_archive_latest_modified": drive_archive_latest,
"drive_global_latest_modified": drive_global_latest,
"drive_failed_count": drive_failed_count,
"preflight_pass": summary.get("pass", 0),
"preflight_warn": summary.get("warn", len(messages["warn"])),
"preflight_blocked": summary.get("blocked", len(messages["blocked"])),
"blockers": blockers,
"warnings": warnings,
"no_false_green_rules": [
"source_arrived_does_not_authorize_import",
"safe_import_preflight_allowed_does_not_authorize_db_write",
"freshness_green_requires_post_reboot_summary_recheck",
"archive_or_local_old_file_does_not_count_as_new_source",
],
}
def print_human(result: dict[str, Any]) -> None:
print(
"MOMO_SOURCE_ARRIVAL_GATE "
f"status={result['status']} "
f"source_intake={result['drive_intake_count']} "
f"freshness={result['freshness_days']}|{result['freshness_latest_date']} "
f"safe_import_preflight_allowed={int(result['safe_import_preflight_allowed'])} "
"runtime_write_authorized=0 "
"db_write_authorized=0 "
"drive_move_authorized=0 "
f"next_step={result['next_step']}"
)
def main() -> int:
args = parse_args()
result = classify(parse_preflight(load_text(args.preflight_log)))
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
else:
print_human(result)
return int(result["exit_code"])
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -287,6 +287,20 @@ if [[ "$RUN_MOMO" -eq 1 ]]; then
;;
esac
grep -E 'MOMO_DRIVE_TOKEN_SOURCE_PREFLIGHT|MOMO_HEALTH_VERSION|DB_MONTHLY_SYNC|DB_DAILY_FRESHNESS|DB_LATEST_DAILY_IMPORT_JOB' "$momo_tmp" || true
source_gate_output="$("$ROOT_DIR/scripts/reboot-recovery/momo-source-arrival-gate.py" --preflight-log "$momo_tmp" 2>&1)"
source_gate_rc=$?
printf '%s\n' "$source_gate_output"
if grep -q 'status=blocked_source_absent_fail_closed' <<<"$source_gate_output"; then
evidence_warn "MOMO source-arrival gate confirms source absent fail-closed"
elif grep -q 'status=source_arrived_ready_for_safe_import_preflight' <<<"$source_gate_output"; then
boundary_warn "MOMO source arrived; safe import preflight only, DB/Drive/runtime writes remain unauthorized"
elif grep -q 'status=freshness_already_green_recheck_cold_start' <<<"$source_gate_output"; then
ok "MOMO source-arrival gate reports freshness green; rerun post-reboot summary before updating declaration"
elif [[ "$source_gate_rc" -ne 0 ]]; then
service_warn "MOMO source-arrival gate did not produce a known state rc=$source_gate_rc"
else
evidence_warn "MOMO source-arrival gate produced a non-terminal state"
fi
rm -f "$momo_tmp"
fi

View File

@@ -0,0 +1,89 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
SCRIPT = ROOT / "scripts" / "reboot-recovery" / "momo-source-arrival-gate.py"
def load_module():
spec = importlib.util.spec_from_file_location("momo_source_arrival_gate", SCRIPT)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
BASE_PREFLIGHT = """
HOST ollama
MOMO_HEALTH_CODE 200
MOMO_PUBLIC_HEALTH_CODE 200
MOMO_APP_HEALTH healthy
SCHEDULER_RUNNING true
SCHEDULER_HEALTH healthy
DRIVE_ARCHIVE_LATEST_MODIFIED 2026-06-25T04:21:47.000Z
DRIVE_GLOBAL_LATEST_MODIFIED 2026-06-25T04:21:47.000Z
DRIVE_FAILED_COUNT 0
DB_MONTHLY_SYNC 15383|15383|2026-06-01|2026-06-24|2026-06-01|2026-06-24
DB_LATEST_DAILY_IMPORT_JOB 57|completed|即時業績_當日.xlsx|2026-06-25T13:16:47.359958|2026-06-25T13:18:02.964985|15383|15383|0
IMPORT_CONFIG 當日業績匯入|即時業績_當日
MOMO_DRIVE_TOKEN_SOURCE_PREFLIGHT PASS=20 WARN=3 BLOCKED=2 HOST=ollama@192.168.0.188 FRESHNESS_MAX_DAYS=2
"""
def classify(extra_lines: str):
module = load_module()
return module.classify(module.parse_preflight(BASE_PREFLIGHT + extra_lines))
def assert_no_write_authorization(result):
assert result["runtime_write_authorized"] is False
assert result["db_write_authorized"] is False
assert result["drive_move_authorized"] is False
assert result["manual_import_authorized"] is False
assert result["secret_value_collection_allowed"] is False
def test_source_absent_fail_closed():
result = classify(
"""
DRIVE_INTAKE_COUNT 0
DB_DAILY_FRESHNESS 3|2026-06-24
"""
)
assert result["status"] == "blocked_source_absent_fail_closed"
assert result["exit_code"] == 2
assert result["safe_import_preflight_allowed"] is False
assert_no_write_authorization(result)
def test_source_arrived_allows_only_safe_import_preflight():
result = classify(
"""
DRIVE_INTAKE_COUNT 1
DB_DAILY_FRESHNESS 3|2026-06-24
"""
)
assert result["status"] == "source_arrived_ready_for_safe_import_preflight"
assert result["exit_code"] == 0
assert result["safe_import_preflight_allowed"] is True
assert_no_write_authorization(result)
def test_freshness_green_requires_cold_start_recheck():
result = classify(
"""
DRIVE_INTAKE_COUNT 0
DB_DAILY_FRESHNESS 1|2026-06-26
"""
)
assert result["status"] == "freshness_already_green_recheck_cold_start"
assert result["next_step"] == "rerun_post_reboot_readiness_summary_with_same_evidence_chain"
assert result["exit_code"] == 0
assert result["safe_import_preflight_allowed"] is False
assert_no_write_authorization(result)

View File

@@ -29577,6 +29577,7 @@ def validate(root: Path) -> None:
"wazuh_manager_registry_reviewer_validation_owner_registry_export_received_count=1",
"wazuh_manager_registry_reviewer_validation_owner_registry_export_accepted_count=1",
"wazuh_manager_registry_reviewer_validation_passed_count=1",
"wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count=1",
"wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0",
"wazuh_manager_registry_reviewer_validation_runtime_gate_count=0",
]:
@@ -29606,6 +29607,7 @@ def validate(root: Path) -> None:
"wazuh_manager_registry_reviewer_validation_owner_registry_export_received_count=1",
"wazuh_manager_registry_reviewer_validation_owner_registry_export_accepted_count=1",
"wazuh_manager_registry_reviewer_validation_passed_count=1",
"wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count=1",
"wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0",
"wazuh_manager_registry_reviewer_validation_runtime_gate_count=0",
]:

View File

@@ -128,9 +128,9 @@ REVIEWER_VALIDATION_CHECKS = [
},
{
"check_id": "RV-10",
"title": "Post-enable IwoooS readback 仍是下一關",
"required_evidence": "即使 reviewer 未來接受 evidence也只能進 read-only posture必須另有 post-enable readback 才能更新 runtime truth",
"failure_lane": "waiting_post_enable_iwooos_readback",
"title": "Post-enable IwoooS readback 已讀回但不開 runtime",
"required_evidence": "production API 與前台 smoke 已讀回 reviewer passed此讀回只更新 read-only posture不查 live Wazuh、不保存 raw payload、不開 runtime gate",
"failure_lane": "post_enable_iwooos_readback_passed_no_runtime",
},
]
@@ -147,7 +147,8 @@ OUTCOME_LANES = [
"reject_runtime_action_request",
"ready_for_reviewer_validation",
"accepted_for_readonly_posture_only",
"waiting_post_enable_iwooos_readback",
"post_enable_iwooos_readback_passed",
"manager_registry_acceptance_evidence_review",
]
EVIDENCE_SLOTS = [
@@ -292,8 +293,8 @@ def build_snapshot(generated_at: str) -> dict[str, Any]:
return {
"schema_version": SCHEMA_VERSION,
"generated_at": generated_at,
"status": "accepted_for_readonly_posture_only",
"mode": "committed_validation_passed_readback_no_runtime_no_secret_collection",
"status": "post_enable_iwooos_readback_passed_no_runtime_no_secret_collection",
"mode": "committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection",
"scope": "wazuh_manager_registry_owner_export_reviewer_validation",
"source_refs": [
"docs/security/wazuh-agent-visibility-owner-evidence-preflight.snapshot.json",
@@ -315,7 +316,7 @@ def build_snapshot(generated_at: str) -> dict[str, Any]:
"reviewer_validation_failed_count": 0,
"reviewer_validation_quarantined_count": 0,
"manager_registry_accepted_count": 0,
"post_enable_readback_passed_count": 0,
"post_enable_readback_passed_count": 1,
"runtime_gate_count": 0,
"host_write_authorized_count": 0,
"active_response_authorized_count": 0,
@@ -332,7 +333,7 @@ def build_snapshot(generated_at: str) -> dict[str, Any]:
"received": True,
"accepted": True,
"quarantined": False,
"next_gate": "post_enable_iwooos_readback",
"next_gate": "manager_registry_acceptance_evidence_review",
}
for slot in EVIDENCE_SLOTS
],
@@ -355,6 +356,7 @@ def build_snapshot(generated_at: str) -> dict[str, Any]:
},
"no_false_green_rules": [
"reviewer validation passed 只代表脫敏 owner export refs 通過 no-persist 驗證。",
"post-enable IwoooS readback passed 只代表 production API / 前台已讀回 reviewer passed不代表 live Wazuh 查詢或 runtime action。",
"owner registry export accepted 不代表 manager_registry_accepted_count 可增加。",
"Dashboard 可見、index pattern 三綠勾、HTTP 200 或 transport observed 不可替代 manager registry counts。",
"reviewer accepted 只可更新只讀 postureactive response、agent restart、reenroll、host write、secret rotation 或掃描仍需獨立 runtime gate。",
@@ -365,8 +367,12 @@ def build_snapshot(generated_at: str) -> dict[str, Any]:
def validate(root: Path) -> None:
snapshot = load_json(root / SNAPSHOT_PATH)
assert_equal("schema_version", snapshot.get("schema_version"), SCHEMA_VERSION)
assert_equal("status", snapshot.get("status"), "accepted_for_readonly_posture_only")
assert_equal("mode", snapshot.get("mode"), "committed_validation_passed_readback_no_runtime_no_secret_collection")
assert_equal("status", snapshot.get("status"), "post_enable_iwooos_readback_passed_no_runtime_no_secret_collection")
assert_equal(
"mode",
snapshot.get("mode"),
"committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection",
)
assert_equal("scope", snapshot.get("scope"), "wazuh_manager_registry_owner_export_reviewer_validation")
assert_equal("expected_scope_aliases", snapshot.get("expected_scope_aliases"), EXPECTED_SCOPE_ALIASES)
assert_equal("required_owner_fields", snapshot.get("required_owner_fields"), REQUIRED_OWNER_FIELDS)
@@ -394,13 +400,13 @@ def validate(root: Path) -> None:
"owner_registry_export_accepted_count",
"reviewer_validation_ready_count",
"reviewer_validation_passed_count",
"post_enable_readback_passed_count",
]:
assert_equal(f"summary.{key}", summary.get(key), 1)
for key in [
"reviewer_validation_failed_count",
"reviewer_validation_quarantined_count",
"manager_registry_accepted_count",
"post_enable_readback_passed_count",
"runtime_gate_count",
"host_write_authorized_count",
"active_response_authorized_count",
@@ -415,7 +421,11 @@ def validate(root: Path) -> None:
assert_equal(f"evidence_slots.{slot.get('slot_id')}.received", slot.get("received"), True)
assert_equal(f"evidence_slots.{slot.get('slot_id')}.accepted", slot.get("accepted"), True)
assert_false(f"evidence_slots.{slot.get('slot_id')}.quarantined", slot.get("quarantined"))
assert_equal(f"evidence_slots.{slot.get('slot_id')}.next_gate", slot.get("next_gate"), "post_enable_iwooos_readback")
assert_equal(
f"evidence_slots.{slot.get('slot_id')}.next_gate",
slot.get("next_gate"),
"manager_registry_acceptance_evidence_review",
)
boundaries = snapshot.get("execution_boundaries", {})
for key, value in boundaries.items():
@@ -454,6 +464,7 @@ def main() -> None:
f"slots={summary['evidence_slot_count']} "
f"received={summary['owner_registry_export_received_count']} "
f"accepted={summary['owner_registry_export_accepted_count']} "
f"post_enable={summary['post_enable_readback_passed_count']} "
f"runtime_gate={summary['runtime_gate_count']}"
)