Compare commits

..

32 Commits

Author SHA1 Message Date
Your Name
62309d3990 docs(github): record safe credential intake readback [skip ci] 2026-06-27 22:37:12 +08:00
AWOOOI CD
257544d097 chore(cd): deploy 30af0fb [skip ci] 2026-06-27 22:32:25 +08:00
Your Name
ca29cbd5af docs(awooop): record manual gate payload cleanup [skip ci] 2026-06-27 22:29:38 +08:00
Your Name
30af0fb420 Merge remote-tracking branch 'gitea/main' into codex/github-safe-credential-intake-20260627
Some checks failed
Ansible / Reboot Recovery Contract / validate (push) Failing after 1m9s
CD Pipeline / tests (push) Successful in 1m45s
Code Review / ai-code-review (push) Successful in 15s
CD Pipeline / build-and-deploy (push) Successful in 3m48s
CD Pipeline / post-deploy-checks (push) Successful in 1m46s
2026-06-27 22:23:38 +08:00
AWOOOI CD
f98aaa8ee9 chore(cd): deploy a2733fd [skip ci] 2026-06-27 22:21:55 +08:00
Your Name
527e9762af Merge remote-tracking branch 'gitea/main' into codex/github-safe-credential-intake-20260627 2026-06-27 22:16:00 +08:00
Your Name
a2733fd431 test(awooop): lock controlled apply receipt km snapshot
All checks were successful
CD Pipeline / tests (push) Successful in 1m46s
Code Review / ai-code-review (push) Successful in 22s
CD Pipeline / build-and-deploy (push) Successful in 4m40s
CD Pipeline / post-deploy-checks (push) Successful in 1m43s
2026-06-27 22:15:07 +08:00
Your Name
f219463f20 Merge remote-tracking branch 'gitea/main' into codex/github-safe-credential-intake-20260627 2026-06-27 22:14:25 +08:00
Your Name
b2458b9330 fix(awooop): remove manual gate payload residue
Some checks failed
CD Pipeline / tests (push) Waiting to run
Code Review / ai-code-review (push) Successful in 16s
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
2026-06-27 22:01:33 +08:00
Your Name
fdc703811d docs(github): record owner response preflight readback [skip ci] 2026-06-27 21:57:07 +08:00
Your Name
0624be08df fix(awooop): persist controlled apply receipt snapshots
Some checks failed
CD Pipeline / build-and-deploy (push) Blocked by required conditions
CD Pipeline / tests (push) Successful in 1m46s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / post-deploy-checks (push) Has been cancelled
2026-06-27 21:52:06 +08:00
AWOOOI CD
5b7bd55a90 chore(cd): deploy a68d9e4 [skip ci] 2026-06-27 13:51:37 +00:00
Your Name
3a2b3b3e6f Merge remote-tracking branch 'gitea/main' into codex/github-safe-credential-intake-20260627 2026-06-27 21:49:34 +08:00
Your Name
f917ea41c2 feat(github): add safe credential intake readiness 2026-06-27 21:48:33 +08:00
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
d2d1446594 docs(github): record owner response intake readback [skip ci] 2026-06-27 21:25:47 +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
7b2b3db458 docs(awooop): record controlled automation readback [skip ci] 2026-06-27 21:20:40 +08:00
Your Name
df498e55b1 test(recovery): cover momo source arrival gate 2026-06-27 21:20:34 +08:00
AWOOOI CD
e49c6190ec chore(cd): deploy 9f5097f [skip ci] 2026-06-27 13:20:14 +00: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
30 changed files with 1889 additions and 151 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

@@ -435,6 +435,9 @@ def _truth_status(
approval_suppressed = _approval_suppresses_repair_execution(approvals)
effective_ops = [] if approval_suppressed else _effective_execution_ops(automation_ops)
has_execution_records = bool(effective_ops or repair_rows)
latest_verification = str(
_latest_verification_result(incident, evidence_rows) or ""
).lower()
stage = "received"
stage_status = incident_status.lower()
if incident_status in {"RESOLVED", "CLOSED"}:
@@ -504,7 +507,8 @@ def _truth_status(
if incident_status == "INVESTIGATING" and approvals:
if execution_succeeded:
blockers.append("incident_open_after_successful_execution")
needs_human = True
if latest_verification != "success":
needs_human = True
elif not has_execution_records:
blockers.append("incident_still_investigating_after_approval")

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],
@@ -93,6 +248,14 @@ def build_github_target_private_backup_evidence_gate(
}
owner_summary = _dict(owner_response.get("summary"))
owner_response_intake_readiness = _owner_response_intake_readiness(owner_response)
safe_credential_evidence_intake_readiness = (
_safe_credential_evidence_intake_readiness(
owner_response=owner_response,
required_target_count=len(
[row for row in decisions if row.get("approval_required") is True]
),
)
)
owner_template_by_repo = {
str(row.get("github_repo")): _dict(row)
for row in _list(owner_response.get("response_templates"))
@@ -214,6 +377,25 @@ def build_github_target_private_backup_evidence_gate(
- private_backup_verified_count,
"safe_credential_required_count": len(approval_required_targets),
"safe_credential_accepted_evidence_count": 0,
"safe_credential_evidence_intake_ready": safe_credential_evidence_intake_readiness[
"intake_ready"
],
"safe_credential_required_redacted_evidence_ref_count": safe_credential_evidence_intake_readiness[
"required_redacted_evidence_ref_count"
],
"safe_credential_evidence_ref_rule_count": safe_credential_evidence_intake_readiness[
"evidence_ref_rule_count"
],
"safe_credential_redaction_example_count": safe_credential_evidence_intake_readiness[
"redaction_example_count"
],
"safe_credential_forbidden_payload_count": safe_credential_evidence_intake_readiness[
"forbidden_payload_count"
],
"safe_credential_quarantine_lane_count": safe_credential_evidence_intake_readiness[
"quarantine_lane_count"
],
"safe_credential_raw_payload_storage_allowed": False,
"owner_response_received_count": _int(
owner_summary.get("received_response_count")
),
@@ -264,6 +446,7 @@ def build_github_target_private_backup_evidence_gate(
"public_repo_allowed": False,
},
"owner_response_intake_readiness": owner_response_intake_readiness,
"safe_credential_evidence_intake_readiness": safe_credential_evidence_intake_readiness,
"targets": targets,
"acceptance_requirements": _acceptance_requirements(owner_response),
"rejection_rules": _rejection_rules(owner_response),
@@ -293,6 +476,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],
@@ -367,6 +633,21 @@ def _build_target(
"private_visibility_owner_evidence_ref": connector_readback.get("evidence_ref"),
"safe_credential_evidence_status": "not_collected",
"safe_credential_evidence_ref": None,
"safe_credential_evidence_intake_ready": approval_required,
"safe_credential_evidence_submission_status": "waiting_redacted_evidence_ref"
if approval_required
else "not_required",
"safe_credential_required_redacted_evidence_ref": approval_required,
"safe_credential_allowed_evidence_ref_types": [
"repo_path",
"snapshot_path",
"redacted_metadata_pointer",
]
if approval_required
else [],
"safe_credential_raw_payload_storage_allowed": False,
"safe_credential_private_clone_url_collection_allowed": False,
"safe_credential_secret_value_collection_allowed": False,
"owner_response_template_id": owner_response_template.get("template_id"),
"owner_response_submission_status": "waiting_owner_response"
if approval_required
@@ -687,6 +968,9 @@ def _require_owner_response_intake_consistency(
_dict(row) for row in _list(payload.get("intake_preflight_checks"))
]
acceptance_checks = [_dict(row) for row in _list(payload.get("acceptance_checks"))]
redaction_examples = [
_dict(row) for row in _list(payload.get("owner_response_redaction_examples"))
]
summary = _dict(payload.get("summary"))
if (
@@ -712,6 +996,10 @@ def _require_owner_response_intake_consistency(
raise ValueError(f"{label}: intake preflight check count must match checks")
if _int(summary.get("acceptance_check_count")) != len(acceptance_checks):
raise ValueError(f"{label}: acceptance check count must match checks")
if _int(summary.get("owner_response_redaction_example_count")) != len(
redaction_examples
):
raise ValueError(f"{label}: redaction example count must match examples")
template_ids = {
str(row.get("template_id")) for row in templates if row.get("template_id")
@@ -723,12 +1011,18 @@ def _require_owner_response_intake_consistency(
)
executable_rows = [
str(row.get("template_id") or row.get("check_id") or "unknown")
str(
row.get("template_id")
or row.get("check_id")
or row.get("example_id")
or "unknown"
)
for row in [
*templates,
*collection_checks,
*preflight_checks,
*acceptance_checks,
*redaction_examples,
]
if row.get("execution_authorized") is not False
]
@@ -737,6 +1031,31 @@ def _require_owner_response_intake_consistency(
f"{label}: owner response intake rows must remain non-executing: {executable_rows}"
)
raw_payload_examples = [
str(row.get("example_id") or "unknown")
for row in redaction_examples
if row.get("stored_raw_payload_allowed") is not False
]
if raw_payload_examples:
raise ValueError(
f"{label}: redaction examples must not allow raw payload storage: {raw_payload_examples}"
)
required_forbidden_payloads = {
"token_value",
"secret_value",
"private_clone_url_credential",
"repo_archive",
"git_object_pack",
}
missing_forbidden_payloads = sorted(
required_forbidden_payloads - set(_strings(packet.get("forbidden_payloads")))
)
if missing_forbidden_payloads:
raise ValueError(
f"{label}: forbidden payloads missing safe credential blockers: {missing_forbidden_payloads}"
)
def _owner_response_intake_readiness(owner_response: dict[str, Any]) -> dict[str, Any]:
summary = _dict(owner_response.get("summary"))
@@ -805,6 +1124,104 @@ def _owner_response_intake_readiness(owner_response: dict[str, Any]) -> dict[str
}
def _safe_credential_evidence_intake_readiness(
*,
owner_response: dict[str, Any],
required_target_count: int,
) -> dict[str, Any]:
summary = _dict(owner_response.get("summary"))
packet = _dict(owner_response.get("owner_response_request_packet"))
evidence_ref_rules = _strings(packet.get("evidence_ref_rules"))
redaction_examples = [
_redaction_example_summary(row)
for row in _list(owner_response.get("owner_response_redaction_examples"))
]
forbidden_payloads = _strings(packet.get("forbidden_payloads"))
checks = [
*[
_dict(row)
for row in _list(owner_response.get("owner_response_collection_checks"))
],
*[_dict(row) for row in _list(owner_response.get("intake_preflight_checks"))],
*[_dict(row) for row in _list(owner_response.get("acceptance_checks"))],
]
quarantine_lanes = sorted(
{
str(row.get("failure_lane"))
for row in checks
if str(row.get("failure_lane") or "").startswith("quarantine")
}
)
accepted_evidence_count = 0
required_redacted_evidence_ref_count = (
required_target_count - accepted_evidence_count
)
required_forbidden_payloads = {
"token_value",
"secret_value",
"private_clone_url_credential",
"repo_archive",
"git_object_pack",
}
intake_ready = (
packet.get("execution_authorized") is False
and packet.get("not_approval") is True
and bool(evidence_ref_rules)
and bool(redaction_examples)
and _int(summary.get("owner_response_redaction_example_count"))
== len(redaction_examples)
and required_forbidden_payloads.issubset(set(forbidden_payloads))
)
return {
"status": "ready_to_collect_redacted_evidence_refs_not_credentials"
if intake_ready
else "blocked_safe_credential_evidence_intake_contract_incomplete",
"intake_ready": intake_ready,
"required_target_count": required_target_count,
"accepted_evidence_count": accepted_evidence_count,
"required_redacted_evidence_ref_count": required_redacted_evidence_ref_count,
"allowed_evidence_ref_types": [
"repo_path",
"snapshot_path",
"redacted_metadata_pointer",
],
"evidence_ref_rule_count": len(evidence_ref_rules),
"evidence_ref_rules": evidence_ref_rules,
"redaction_example_count": len(redaction_examples),
"redaction_examples": redaction_examples,
"forbidden_payload_count": len(forbidden_payloads),
"forbidden_payloads": forbidden_payloads,
"quarantine_lane_count": len(quarantine_lanes),
"quarantine_lanes": quarantine_lanes,
"allowed_submission_modes": _strings(packet.get("allowed_submission_modes")),
"stored_raw_payload_allowed": False,
"secret_value_collection_allowed": False,
"private_clone_url_collection_allowed": False,
"credential_value_collection_allowed": False,
"target_owner_request_dispatch_authorized": False,
"action_buttons_allowed": False,
"execution_authorized": False,
"not_approval": True,
}
def _redaction_example_summary(value: Any) -> dict[str, Any]:
row = _dict(value)
return {
"example_id": str(row.get("example_id") or ""),
"display_order": _int(row.get("display_order")),
"example_status": str(row.get("example_status") or ""),
"category": str(row.get("category") or ""),
"safe_response_shape": _strings(row.get("safe_response_shape")),
"required_redactions": _strings(row.get("required_redactions")),
"forbidden_raw_values": _strings(row.get("forbidden_raw_values")),
"stored_raw_payload_allowed": False,
"execution_authorized": False,
"not_approval": row.get("not_approval") is True,
}
def _owner_response_template_summary(value: Any) -> dict[str, Any]:
row = _dict(value)
return {
@@ -873,6 +1290,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 +1412,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

@@ -9237,6 +9237,10 @@ class TelegramGateway:
incident_id=incident_id,
truth_chain=truth_chain,
)
km_completion_summary = await _fetch_km_stale_completion_summary_for_incident(
incident_id=incident_id,
project_id=project_id or "awoooi",
)
source_extra = _callback_reply_source_envelope_extra(
incident_id=incident_id,
failure_context="controlled_apply_result",
@@ -9245,6 +9249,7 @@ class TelegramGateway:
chunk_count=1,
callback_action="controlled_apply_result",
parse_mode="HTML",
km_stale_completion_summary=km_completion_summary,
awooop_status_chain=status_snapshot,
)
lines = [

View File

@@ -232,6 +232,30 @@ def test_truth_status_marks_open_incident_after_successful_execution() -> None:
assert "incident_open_after_successful_execution" in status["blockers"]
def test_truth_status_keeps_verified_controlled_apply_autonomous_when_incident_open() -> None:
status = _truth_status(
incident={
"incident_id": "INC-OPEN-VERIFIED",
"status": "INVESTIGATING",
"verification_result": "success",
},
approvals=[{"status": "EXECUTION_SUCCESS", "action": "ansible controlled apply"}],
evidence_rows=[{"sensors_attempted": 8, "sensors_succeeded": 6}],
automation_ops=[],
drift=None,
drift_repeat_count=0,
gateway_mcp_total=3,
legacy_mcp_total=2,
outbound_visible_total=1,
auto_repair_executions=[{"success": True}],
)
assert status["current_stage"] == "execution_succeeded"
assert status["stage_status"] == "success"
assert status["needs_human"] is False
assert "incident_open_after_successful_execution" in status["blockers"]
def test_truth_status_marks_repeated_pending_drift_as_human_needed() -> None:
status = _truth_status(
incident=None,

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
@@ -55,6 +56,17 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot
assert snapshot["summary"]["github_missing_target_refs_sync_ready_count"] == 0
assert snapshot["summary"]["private_backup_verified_count"] == 4
assert snapshot["summary"]["private_visibility_verified_count"] == 4
assert snapshot["summary"]["safe_credential_required_count"] == 9
assert snapshot["summary"]["safe_credential_accepted_evidence_count"] == 0
assert snapshot["summary"]["safe_credential_evidence_intake_ready"] is True
assert (
snapshot["summary"]["safe_credential_required_redacted_evidence_ref_count"] == 9
)
assert snapshot["summary"]["safe_credential_evidence_ref_rule_count"] == 5
assert snapshot["summary"]["safe_credential_redaction_example_count"] == 5
assert snapshot["summary"]["safe_credential_forbidden_payload_count"] == 15
assert snapshot["summary"]["safe_credential_quarantine_lane_count"] == 1
assert snapshot["summary"]["safe_credential_raw_payload_storage_allowed"] is False
assert snapshot["summary"]["owner_response_request_ready"] is True
assert snapshot["summary"]["owner_response_required_response_item_count"] == 9
assert snapshot["summary"]["owner_response_requested_template_count"] == 9
@@ -97,6 +109,39 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot
assert "private_clone_url_credential" in intake["forbidden_payloads"]
assert "read_only_markdown_response" in intake["allowed_submission_modes"]
assert "create_github_repo" in intake["still_forbidden"]
safe_credential_intake = snapshot["safe_credential_evidence_intake_readiness"]
assert (
safe_credential_intake["status"]
== "ready_to_collect_redacted_evidence_refs_not_credentials"
)
assert safe_credential_intake["intake_ready"] is True
assert safe_credential_intake["required_target_count"] == 9
assert safe_credential_intake["accepted_evidence_count"] == 0
assert safe_credential_intake["required_redacted_evidence_ref_count"] == 9
assert safe_credential_intake["evidence_ref_rule_count"] == 5
assert safe_credential_intake["redaction_example_count"] == 5
assert safe_credential_intake["forbidden_payload_count"] == 15
assert safe_credential_intake["quarantine_lane_count"] == 1
assert "quarantine_sensitive_payload" in safe_credential_intake["quarantine_lanes"]
assert safe_credential_intake["stored_raw_payload_allowed"] is False
assert safe_credential_intake["secret_value_collection_allowed"] is False
assert safe_credential_intake["private_clone_url_collection_allowed"] is False
assert safe_credential_intake["credential_value_collection_allowed"] is False
assert safe_credential_intake["execution_authorized"] is False
assert safe_credential_intake["not_approval"] is True
assert (
"private_clone_url_credential" in safe_credential_intake["forbidden_payloads"]
)
assert "repo_archive" in safe_credential_intake["forbidden_payloads"]
assert "git_object_pack" in safe_credential_intake["forbidden_payloads"]
assert (
"redacted_metadata_pointer"
in safe_credential_intake["allowed_evidence_ref_types"]
)
first_redaction_example = safe_credential_intake["redaction_examples"][0]
assert first_redaction_example["example_id"] == "redaction-github-target-doc-ref"
assert first_redaction_example["stored_raw_payload_allowed"] is False
assert first_redaction_example["execution_authorized"] is False
targets = {target["github_repo"]: target for target in snapshot["targets"]}
assert targets["owenhytsai/awoooi"]["visibility_evidence_status"] == (
@@ -112,6 +157,28 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot
targets["owenhytsai/awoooi"]["owner_response_submission_status"]
== "waiting_owner_response"
)
assert (
targets["owenhytsai/awoooi"]["safe_credential_evidence_submission_status"]
== "waiting_redacted_evidence_ref"
)
assert (
"redacted_metadata_pointer"
in targets["owenhytsai/awoooi"]["safe_credential_allowed_evidence_ref_types"]
)
assert (
targets["owenhytsai/awoooi"]["safe_credential_raw_payload_storage_allowed"]
is False
)
assert (
targets["owenhytsai/awoooi"][
"safe_credential_private_clone_url_collection_allowed"
]
is False
)
assert (
targets["owenhytsai/awoooi"]["safe_credential_secret_value_collection_allowed"]
is False
)
assert targets["owenhytsai/awoooi"]["owner_response_execution_authorized"] is False
assert (
"canonical_source"
@@ -152,6 +219,14 @@ def test_load_github_target_private_backup_evidence_gate_from_committed_snapshot
targets["nexu-io/open-design"]["owner_response_submission_status"]
== "not_required"
)
assert (
targets["nexu-io/open-design"]["safe_credential_evidence_submission_status"]
== "not_required"
)
assert (
targets["nexu-io/open-design"]["safe_credential_allowed_evidence_ref_types"]
== []
)
def test_github_target_private_backup_gate_rejects_runtime_authorization(tmp_path):
@@ -182,6 +257,45 @@ def test_github_target_private_backup_gate_rejects_owner_response_execution_pack
load_latest_github_target_private_backup_evidence_gate(tmp_path)
def test_github_target_private_backup_gate_rejects_raw_payload_redaction_example(
tmp_path,
):
_copy_security_snapshots(tmp_path)
owner_response_path = (
tmp_path / "github-target-owner-decision-response.snapshot.json"
)
owner_response = json.loads(owner_response_path.read_text(encoding="utf-8"))
owner_response["owner_response_redaction_examples"][0][
"stored_raw_payload_allowed"
] = True
owner_response_path.write_text(json.dumps(owner_response), encoding="utf-8")
with pytest.raises(ValueError, match="redaction examples"):
load_latest_github_target_private_backup_evidence_gate(tmp_path)
def test_github_target_private_backup_gate_requires_safe_credential_blockers(
tmp_path,
):
_copy_security_snapshots(tmp_path)
owner_response_path = (
tmp_path / "github-target-owner-decision-response.snapshot.json"
)
owner_response = json.loads(owner_response_path.read_text(encoding="utf-8"))
forbidden_payloads = owner_response["owner_response_request_packet"][
"forbidden_payloads"
]
owner_response["owner_response_request_packet"]["forbidden_payloads"] = [
payload
for payload in forbidden_payloads
if payload != "private_clone_url_credential"
]
owner_response_path.write_text(json.dumps(owner_response), encoding="utf-8")
with pytest.raises(ValueError, match="forbidden payloads"):
load_latest_github_target_private_backup_evidence_gate(tmp_path)
def test_github_target_private_backup_gate_requires_decision_rollup_consistency(
tmp_path,
):
@@ -208,6 +322,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 +389,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

@@ -27,6 +27,13 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g
assert data["summary"]["github_missing_target_refs_sync_ready_count"] == 0
assert data["summary"]["private_backup_verified_count"] == 4
assert data["summary"]["private_visibility_verified_count"] == 4
assert data["summary"]["safe_credential_evidence_intake_ready"] is True
assert data["summary"]["safe_credential_required_redacted_evidence_ref_count"] == 9
assert data["summary"]["safe_credential_evidence_ref_rule_count"] == 5
assert data["summary"]["safe_credential_redaction_example_count"] == 5
assert data["summary"]["safe_credential_forbidden_payload_count"] == 15
assert data["summary"]["safe_credential_quarantine_lane_count"] == 1
assert data["summary"]["safe_credential_raw_payload_storage_allowed"] is False
assert data["summary"]["owner_response_request_ready"] is True
assert data["summary"]["owner_response_required_response_item_count"] == 9
assert data["summary"]["owner_response_requested_template_count"] == 9
@@ -57,5 +64,88 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g
assert intake["forbidden_payload_count"] == 15
assert intake["execution_authorized"] is False
assert intake["not_approval"] is True
safe_credential_intake = data["safe_credential_evidence_intake_readiness"]
assert (
safe_credential_intake["status"]
== "ready_to_collect_redacted_evidence_refs_not_credentials"
)
assert safe_credential_intake["intake_ready"] is True
assert safe_credential_intake["required_target_count"] == 9
assert safe_credential_intake["required_redacted_evidence_ref_count"] == 9
assert safe_credential_intake["evidence_ref_rule_count"] == 5
assert safe_credential_intake["redaction_example_count"] == 5
assert safe_credential_intake["forbidden_payload_count"] == 15
assert safe_credential_intake["quarantine_lane_count"] == 1
assert safe_credential_intake["stored_raw_payload_allowed"] is False
assert safe_credential_intake["private_clone_url_collection_allowed"] is False
assert safe_credential_intake["secret_value_collection_allowed"] is False
assert safe_credential_intake["execution_authorized"] is False
assert safe_credential_intake["not_approval"] is True
assert data["targets"][0]["owner_response_execution_authorized"] is False
assert (
data["targets"][0]["safe_credential_evidence_submission_status"]
== "waiting_redacted_evidence_ref"
)
assert data["targets"][0]["safe_credential_raw_payload_storage_allowed"] is False
assert (
data["targets"][0]["safe_credential_private_clone_url_collection_allowed"]
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

@@ -1092,12 +1092,45 @@ async def test_controlled_apply_result_receipt_marks_callback_reply_evidence(mon
},
}
async def fake_fetch_km_completion_summary(*, incident_id, project_id):
assert incident_id == "INC-20260627-64472B"
assert project_id == "awoooi"
return {
"schema_version": "km_stale_owner_review_completion_telegram_summary_v1",
"status": "no_related_owner_review",
"project_id": "awoooi",
"incident_id": "INC-20260627-64472B",
"missing_reason": "no_matching_completion_item",
"ready_count": 10,
"blocked_count": 0,
"completed_count": 1,
"failed_count": 0,
"related_total": 0,
"writes_on_read": False,
"batch_writes_allowed": False,
"manual_review_required": True,
"triage": {
"schema_version": "km_stale_callback_owner_review_triage_v1",
"flow_stage": "callback_observed_owner_review_link_missing",
"ai_lead_agent": "Hermes",
"supporting_agents": ["OpenClaw"],
"automation_state": "manual_owner_review_required",
"safe_to_auto_repair": False,
"blocking_reason": "no_matching_completion_item",
"matching_strategy": "related_incident_id_exact_match",
},
}
monkeypatch.setattr(TelegramGateway, "alert_chat_id", property(lambda _self: "chat"))
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
monkeypatch.setattr(
"src.services.awooop_truth_chain_service.fetch_truth_chain",
fake_fetch_truth_chain,
)
monkeypatch.setattr(
"src.services.telegram_gateway._fetch_km_stale_completion_summary_for_incident",
fake_fetch_km_completion_summary,
)
result = await gateway.send_controlled_apply_result_receipt(
incident_id="INC-20260627-64472B",
@@ -1123,6 +1156,14 @@ async def test_controlled_apply_result_receipt_marks_callback_reply_evidence(mon
assert source_extra["callback_reply"]["action"] == "controlled_apply_result"
assert source_extra["callback_reply"]["status"] == "callback_reply_sent"
assert source_extra["source_refs"]["incident_ids"] == ["INC-20260627-64472B"]
km_snapshot = source_extra["km_stale_completion_summary"]
assert (
km_snapshot["source_schema_version"]
== "km_stale_owner_review_completion_telegram_summary_v1"
)
assert km_snapshot["status"] == "no_related_owner_review"
assert km_snapshot["ready_count"] == 10
assert km_snapshot["triage"]["ai_lead_agent"] == "Hermes"
snapshot = source_extra["awooop_status_chain"]
assert snapshot["repair_state"] == "auto_repaired_verified"
assert snapshot["operator_outcome"]["state"] == "completed_verified"

View File

@@ -13226,7 +13226,7 @@
},
"evidenceTimeline": {
"title": "證據時序",
"signal": "監控、AwoooP 真相鏈與人工閘門已接成證據時序。",
"signal": "監控、AwoooP 真相鏈與受控授權閘門已接成證據時序。",
"interpretation": "目前停在 S4.9 負責人證據等待狀態,避免將可視化誤判為已推進。",
"next": "補審查者接受紀錄與負責人回覆。",
"ring": "時序已接線;等待 S4.9 證據。"
@@ -13259,7 +13259,7 @@
"body": "監控、MCP、Ansible、KM與告警資料先進入證據線讓 IwoooS 能解釋訊號來源與新鮮度。"
},
"awooopTruth": {
"body": "AwoooP 是跨工作線工作狀態與人工閘門的真相鏈IwoooS只讀消費它不把顯示狀態當批准。"
"body": "AwoooP 是跨工作線工作狀態與受控授權閘門的真相鏈IwoooS只讀消費它不把顯示狀態當批准。"
},
"runtimeGate": {
"body": "所有掃描、修復、部署、主機更新與版本來源變更仍集中在執行閘 0沒有正式決策前不產生執行按鈕。"
@@ -13279,7 +13279,7 @@
"title": "主機拓樸",
"mapTitle": "資安觀測節點 / 開發主機群 A / 開發主機群 B只讀觀測主機網",
"detail": "將 資安觀測節點、開發主機群 A / 開發主機群 B、監控工具與AwoooP 真相鏈整合為單一拓樸視角。",
"evidence": "目前只呈現觀測窗口、證據位置與人工閘門,沒有執行 SSH、掃描或主機設定變更。",
"evidence": "目前只呈現觀測窗口、證據位置與受控授權閘門,沒有執行 SSH、掃描或主機設定變更。",
"next": "等執行期閘門與掃描範圍批准後,才把只讀證據轉入受控探測。",
"locked": "host_change_authorized=falsescan_authorized=false。"
},
@@ -13293,7 +13293,7 @@
},
"evidenceFlow": {
"title": "證據流",
"mapTitle": "監控 → AwoooP 真相鏈 → 人工閘門",
"mapTitle": "監控 → AwoooP 真相鏈 → 受控授權閘門",
"detail": "用證據線表示資料如何被收集、脫敏、審查與回寫;這比單純列文件更接近 SOC / XDR的操作體驗。",
"evidence": "AwoooP 跨工作線狀態已接線IwoooS只讀鏡像與進度守門已有證據。",
"next": "補審查者接受紀錄與負責人決策紀錄,才可能進入下一個執行期閘門。",
@@ -13436,7 +13436,7 @@
},
"awooop": {
"title": "AwoooP 真相鏈",
"body": "同步跨工作線工作狀態與人工閘門。"
"body": "同步跨工作線工作狀態與受控授權閘門。"
},
"vibework": {
"title": "任務媒合產品 納管",
@@ -13807,7 +13807,7 @@
},
"allProductCoverageSnapshot": {
"title": "全產品只讀套用快照",
"subtitle": "先回答最常被問的問題:所有專案產品都套用 IwoooS 資安框架,但目前只套可視化、證據欄位與人工閘門,不套掃描、修復、部署或阻擋控制。",
"subtitle": "先回答最常被問的問題:所有專案產品都套用 IwoooS 資安框架,但目前只套可視化、證據欄位與受控授權閘門,不套掃描、修復、部署或阻擋控制。",
"boundaryTitle": "快照邊界",
"boundaryIntro": "以下鍵值固定:預設區只顯示簡版快照;完整三軸明細與分階段套用台帳保留在進階區,執行期就緒仍是 0。",
"summary": {
@@ -13821,7 +13821,7 @@
},
"runtimeReady": {
"label": "執行期就緒",
"detail": "目前 0未經人工閘門不進執行期。"
"detail": "目前 0未經受控授權閘門不進執行期。"
},
"nextGate": {
"label": "下一閘門",
@@ -13856,11 +13856,11 @@
},
"toolsMonitoring": {
"title": "監控、工具與自動化",
"body": "只顯示 僅中繼資料證據、阻塞解除條件與人工閘門。"
"body": "只顯示 僅中繼資料證據、阻塞解除條件與受控授權閘門。"
},
"vibeWork": {
"title": "任務媒合產品 新專案",
"body": "已納入 IwoooS只讀資安框架、繁中可視化、人工閘門與執行期=false 旗標。"
"body": "已納入 IwoooS只讀資安框架、繁中可視化、受控授權閘門與執行期=false 旗標。"
},
"futureProducts": {
"title": "未來新增產品",
@@ -13875,13 +13875,13 @@
"globalSecurityMeshMatrix": {
"eyebrow": "全域資安納管矩陣",
"title": "所有產品、主機、工具放在同一張表",
"subtitle": "將 AwoooI、AwoooP、IwoooS、公開網站群、任務媒合產品、代理賞金協議、資安觀測節點、開發主機與GitHub / Gitea 版本來源放到同一個只讀矩陣,先呈現納管範圍,再決定哪一段要收證或開人工閘門。",
"subtitle": "將 AwoooI、AwoooP、IwoooS、公開網站群、任務媒合產品、代理賞金協議、資安觀測節點、開發主機與GitHub / Gitea 版本來源放到同一個只讀矩陣,先呈現納管範圍,再決定哪一段要收證或開受控授權閘門。",
"coverageLabel": "覆蓋",
"evidenceLabel": "證據",
"runtimeLabel": "執行",
"nextLabel": "下一步",
"boundaryTitle": "全域納管邊界",
"boundaryIntro": "以下鍵值固定:這張矩陣只呈現可視化、證據狀態與人工閘門,不提供掃描、修復、主機變更、部署或版本來源變更。",
"boundaryIntro": "以下鍵值固定:這張矩陣只呈現可視化、證據狀態與受控授權閘門,不提供掃描、修復、主機變更、部署或版本來源變更。",
"summary": {
"assets": {
"label": "資產列",
@@ -16568,7 +16568,7 @@
"followupRuntimeGatePointer": {
"title": "後續執行期指標",
"body": "若決策牽涉掃描、修復、部署、主要來源切換或阻擋升級,草稿只能留下後續閘門指標。",
"draft": "只標記後續要走哪個人工閘門,不建立執行命令。",
"draft": "只標記後續要走哪個受控授權閘門,不建立執行命令。",
"guard": "不切主要來源、不停用 Gitea、不建立執行期閘門。"
}
}
@@ -16638,7 +16638,7 @@
"sourceControlSeparation": {
"title": "主要來源分離",
"body": "GitHub 主要來源切換、Gitea 停用、工作流程 / 機密設定或參照 動作不能由候選直接推進。",
"preflight": "只標記需要另開主要來源或版本控制人工閘門。",
"preflight": "只標記需要另開主要來源或版本控制受控授權閘門。",
"guard": "不切 GitHub 主要來源、不停用 Gitea、不改 工作流程 / 機密設定。"
}
}
@@ -16713,7 +16713,7 @@
},
"runtimeOrCutoverGateRequired": {
"title": "另開執行或切換閘門",
"body": "若候選需要掃描、修復、主機更新、主要來源切換或Gitea 停用,必須另開人工閘門。",
"body": "若候選需要掃描、修復、主機更新、主要來源切換或Gitea 停用,必須另開受控授權閘門。",
"result": "只標記需要哪一種後續閘門。",
"guard": "不呼叫 Kali、不開 SSH、不切 GitHub 主要來源、不停用 Gitea。"
}
@@ -16783,7 +16783,7 @@
},
"handoffSourceControlPointer": {
"title": "主要來源指標包",
"body": "GitHub 主要來源切換、Gitea 停用、參照或工作流程 / 機密設定 動作只能作為後續人工閘門指標。",
"body": "GitHub 主要來源切換、Gitea 停用、參照或工作流程 / 機密設定 動作只能作為後續受控授權閘門指標。",
"handoff": "只標記版本控制與主要來源相關待決事項。",
"guard": "不建立專案庫、不改 參照、不改 工作流程 / 機密設定、不停用 Gitea。"
}
@@ -16853,7 +16853,7 @@
},
"runtimeCutoverSeparation": {
"title": "執行與切換分離",
"body": "檢查掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用是否被留在獨立人工閘門。",
"body": "檢查掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用是否被留在獨立受控授權閘門。",
"review": "只標記後續需要哪一類獨立閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -16929,7 +16929,7 @@
},
"runtimeOrCutoverGateRequired": {
"title": "另開執行或切換閘門",
"body": "若結果需要掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用,必須另開人工閘門。",
"body": "若結果需要掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用,必須另開受控授權閘門。",
"result": "只標記需要哪一種後續閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -17005,7 +17005,7 @@
},
"runtimeCutoverPointer": {
"title": "執行切換指標包",
"body": "整理掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用是否需要另開人工閘門。",
"body": "整理掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用是否需要另開受控授權閘門。",
"prepare": "只標記後續可能需要哪一種獨立閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -17081,7 +17081,7 @@
},
"runtimeCutoverSeparationCheck": {
"title": "執行切換分離檢查",
"body": "確認掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用是否留在獨立人工閘門。",
"body": "確認掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用是否留在獨立受控授權閘門。",
"review": "只標記後續是否需要獨立閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -17157,7 +17157,7 @@
},
"runtimeOrPrimaryGateRequired": {
"title": "另開執行或主要來源閘門",
"body": "若後續需要掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用,必須另開人工閘門。",
"body": "若後續需要掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用,必須另開受控授權閘門。",
"result": "只標記後續需要哪一種獨立閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -17310,7 +17310,7 @@
"runtimePrimarySeparated": {
"title": "執行與主要來源分離檢查",
"body": "確認掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用都留在獨立閘門。",
"confirmation": "只標記後續是否需要獨立人工閘門。",
"confirmation": "只標記後續是否需要獨立受控授權閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
}
@@ -17385,7 +17385,7 @@
},
"runtimeOrPrimaryGateRequired": {
"title": "另開執行或主要來源閘門",
"body": "若後續需要 Kali 掃描、SSH、主機更新、修復、GitHub 主要來源切換或Gitea 停用,必須另開獨立人工閘門。",
"body": "若後續需要 Kali 掃描、SSH、主機更新、修復、GitHub 主要來源切換或Gitea 停用,必須另開獨立受控授權閘門。",
"result": "只標記需要哪一種後續閘門,不在本看板執行。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -17461,7 +17461,7 @@
},
"runtimePrimaryGatePacket": {
"title": "執行與主要來源閘門包",
"body": "整理後續若要 Kali、SSH、主機更新、修復、GitHub 主要來源切換或Gitea 停用時需要另開的人工閘門。",
"body": "整理後續若要 Kali、SSH、主機更新、修復、GitHub 主要來源切換或Gitea 停用時需要另開的受控授權閘門。",
"requirement": "只標記後續閘門類型,不在本看板執行。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -17538,7 +17538,7 @@
"runtimePrimarySeparated": {
"title": "執行與主要來源分離檢查",
"body": "確認 Kali、SSH、主機更新、修復、GitHub 主要來源切換或Gitea 停用都留在獨立閘門。",
"confirmation": "只標記後續是否需要獨立人工閘門。",
"confirmation": "只標記後續是否需要獨立受控授權閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
}
@@ -17629,7 +17629,7 @@
"title": "後續節奏改成批次推進",
"body": "已完成多層清單;後續應優先收斂 P0 負責人回覆與AwoooP 落地,避免持續拆分低影響核對項。",
"unlock": "相同類型的 framework detail 之後合併成一批回報,只有高層閘門 變動才調整 重點。",
"guard": "節奏加快不等於放寬安全;執行期與來源-control cutover仍需人工閘門。"
"guard": "節奏加快不等於放寬安全;執行期與來源-control cutover仍需受控授權閘門。"
}
}
},
@@ -18114,7 +18114,7 @@
},
"concreteSecurityBlockerResolution": {
"title": "目前阻塞與解除條件",
"subtitle": "將 64% 無法前進的原因拆成六個阻塞點。每個阻塞點都標明為什麼卡住,以及要用哪種脫敏證據或人工閘門 才能解除;這仍是只讀狀態,不是批准或執行入口。",
"subtitle": "將 64% 無法前進的原因拆成六個阻塞點。每個阻塞點都標明為什麼卡住,以及要用哪種脫敏證據或受控授權閘門 才能解除;這仍是只讀狀態,不是批准或執行入口。",
"blockerLabel": "阻塞",
"whyLabel": "卡住原因",
"unlockLabel": "解除條件",
@@ -18225,13 +18225,13 @@
},
"toolsMonitoring": {
"title": "監控、工具與自動化流程",
"current": "可套用只讀狀態、阻塞解除條件、證據參照與人工閘門 顯示。",
"current": "可套用只讀狀態、阻塞解除條件、證據參照與受控授權閘門 顯示。",
"next": "先讓工具輸出 僅中繼資料證據,再接審查者與執行期閘門。",
"boundary": "不得讓工具自動觸發修復、部署、機密 收集或外部付費變更。"
},
"vibeWork": {
"title": "任務媒合產品 新專案",
"current": "已納入全產品三軸進度、IwoooS可視化、只讀 governance與人工閘門 口徑。",
"current": "已納入全產品三軸進度、IwoooS可視化、只讀 governance與受控授權閘門 口徑。",
"next": "先補產品 負責人、資料分級、版本來源與部署邊界,再評估後續分階段收嚴。",
"boundary": "不得因新專案納管就自動掃描、部署、改 專案庫、改 工作流程或套 阻擋 enforcement。"
},
@@ -19417,7 +19417,7 @@
"boundary": {
"title": "執行邊界",
"state": "閘門 0",
"detail": "目前 IwoooS 是低摩擦框架與只讀證據網;掃描、修復、部署、主機更新與版本來源變更都要獨立人工閘門。",
"detail": "目前 IwoooS 是低摩擦框架與只讀證據網;掃描、修復、部署、主機更新與版本來源變更都要獨立受控授權閘門。",
"evidence": "守門、快照、LOGBOOK 與邊界鍵都已鎖住。",
"next": "等 負責人證據或人工決策後,再另開 follow-up 執行期閘門。",
"locked": "runtime 授權仍維持關閉;執行期閘門維持 0。"
@@ -20005,7 +20005,7 @@
"mutationRequestRejected": {
"title": "變更要求拒收檢查",
"body": "確認回覆封套沒有夾帶建立專案庫、改可見性、同步分支、修改工作流程或收集機密值的要求。",
"failure": "夾帶變更要求時只能拒收或拆到獨立人工閘門。",
"failure": "夾帶變更要求時只能拒收或拆到獨立受控授權閘門。",
"guard": "不從封套觸發 GitHub、Gitea、分支 / 標籤參照、工作流程、機密設定或執行器動作。"
},
"followupOwnerTrace": {
@@ -20075,7 +20075,7 @@
"rejectMutationRequest": {
"title": "拒收變更要求",
"body": "封套夾帶建立專案庫、改可見性、同步分支、修改工作流程或收集機密值時,直接分流為拒收。",
"next": "只記為需另開人工閘門的變更要求,不在封套內處理。",
"next": "只記為需另開受控授權閘門的變更要求,不在封套內處理。",
"guard": "不從封套觸發 GitHub、Gitea、分支 / 標籤參照、工作流程、機密設定或執行器動作。"
},
"keepFollowupOwnerWaiting": {
@@ -20259,7 +20259,7 @@
},
"allProducts": {
"title": "所有產品先套只讀框架",
"body": "核心產品、公開網站、版本來源、主機、監控工具、任務媒合產品與未來產品七類都已套用 IwoooS可視化與人工閘門 口徑。"
"body": "核心產品、公開網站、版本來源、主機、監控工具、任務媒合產品與未來產品七類都已套用 IwoooS可視化與受控授權閘門 口徑。"
},
"runtime": {
"title": "執行期仍保持關閉",
@@ -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

@@ -13226,7 +13226,7 @@
},
"evidenceTimeline": {
"title": "證據時序",
"signal": "監控、AwoooP 真相鏈與人工閘門已接成證據時序。",
"signal": "監控、AwoooP 真相鏈與受控授權閘門已接成證據時序。",
"interpretation": "目前停在 S4.9 負責人證據等待狀態,避免將可視化誤判為已推進。",
"next": "補審查者接受紀錄與負責人回覆。",
"ring": "時序已接線;等待 S4.9 證據。"
@@ -13259,7 +13259,7 @@
"body": "監控、MCP、Ansible、KM與告警資料先進入證據線讓 IwoooS 能解釋訊號來源與新鮮度。"
},
"awooopTruth": {
"body": "AwoooP 是跨工作線工作狀態與人工閘門的真相鏈IwoooS只讀消費它不把顯示狀態當批准。"
"body": "AwoooP 是跨工作線工作狀態與受控授權閘門的真相鏈IwoooS只讀消費它不把顯示狀態當批准。"
},
"runtimeGate": {
"body": "所有掃描、修復、部署、主機更新與版本來源變更仍集中在執行閘 0沒有正式決策前不產生執行按鈕。"
@@ -13279,7 +13279,7 @@
"title": "主機拓樸",
"mapTitle": "資安觀測節點 / 開發主機群 A / 開發主機群 B只讀觀測主機網",
"detail": "將 資安觀測節點、開發主機群 A / 開發主機群 B、監控工具與AwoooP 真相鏈整合為單一拓樸視角。",
"evidence": "目前只呈現觀測窗口、證據位置與人工閘門,沒有執行 SSH、掃描或主機設定變更。",
"evidence": "目前只呈現觀測窗口、證據位置與受控授權閘門,沒有執行 SSH、掃描或主機設定變更。",
"next": "等執行期閘門與掃描範圍批准後,才把只讀證據轉入受控探測。",
"locked": "host_change_authorized=falsescan_authorized=false。"
},
@@ -13293,7 +13293,7 @@
},
"evidenceFlow": {
"title": "證據流",
"mapTitle": "監控 → AwoooP 真相鏈 → 人工閘門",
"mapTitle": "監控 → AwoooP 真相鏈 → 受控授權閘門",
"detail": "用證據線表示資料如何被收集、脫敏、審查與回寫;這比單純列文件更接近 SOC / XDR的操作體驗。",
"evidence": "AwoooP 跨工作線狀態已接線IwoooS只讀鏡像與進度守門已有證據。",
"next": "補審查者接受紀錄與負責人決策紀錄,才可能進入下一個執行期閘門。",
@@ -13436,7 +13436,7 @@
},
"awooop": {
"title": "AwoooP 真相鏈",
"body": "同步跨工作線工作狀態與人工閘門。"
"body": "同步跨工作線工作狀態與受控授權閘門。"
},
"vibework": {
"title": "任務媒合產品 納管",
@@ -13807,7 +13807,7 @@
},
"allProductCoverageSnapshot": {
"title": "全產品只讀套用快照",
"subtitle": "先回答最常被問的問題:所有專案產品都套用 IwoooS 資安框架,但目前只套可視化、證據欄位與人工閘門,不套掃描、修復、部署或阻擋控制。",
"subtitle": "先回答最常被問的問題:所有專案產品都套用 IwoooS 資安框架,但目前只套可視化、證據欄位與受控授權閘門,不套掃描、修復、部署或阻擋控制。",
"boundaryTitle": "快照邊界",
"boundaryIntro": "以下鍵值固定:預設區只顯示簡版快照;完整三軸明細與分階段套用台帳保留在進階區,執行期就緒仍是 0。",
"summary": {
@@ -13821,7 +13821,7 @@
},
"runtimeReady": {
"label": "執行期就緒",
"detail": "目前 0未經人工閘門不進執行期。"
"detail": "目前 0未經受控授權閘門不進執行期。"
},
"nextGate": {
"label": "下一閘門",
@@ -13856,11 +13856,11 @@
},
"toolsMonitoring": {
"title": "監控、工具與自動化",
"body": "只顯示 僅中繼資料證據、阻塞解除條件與人工閘門。"
"body": "只顯示 僅中繼資料證據、阻塞解除條件與受控授權閘門。"
},
"vibeWork": {
"title": "任務媒合產品 新專案",
"body": "已納入 IwoooS只讀資安框架、繁中可視化、人工閘門與執行期=false 旗標。"
"body": "已納入 IwoooS只讀資安框架、繁中可視化、受控授權閘門與執行期=false 旗標。"
},
"futureProducts": {
"title": "未來新增產品",
@@ -13875,13 +13875,13 @@
"globalSecurityMeshMatrix": {
"eyebrow": "全域資安納管矩陣",
"title": "所有產品、主機、工具放在同一張表",
"subtitle": "將 AwoooI、AwoooP、IwoooS、公開網站群、任務媒合產品、代理賞金協議、資安觀測節點、開發主機與GitHub / Gitea 版本來源放到同一個只讀矩陣,先呈現納管範圍,再決定哪一段要收證或開人工閘門。",
"subtitle": "將 AwoooI、AwoooP、IwoooS、公開網站群、任務媒合產品、代理賞金協議、資安觀測節點、開發主機與GitHub / Gitea 版本來源放到同一個只讀矩陣,先呈現納管範圍,再決定哪一段要收證或開受控授權閘門。",
"coverageLabel": "覆蓋",
"evidenceLabel": "證據",
"runtimeLabel": "執行",
"nextLabel": "下一步",
"boundaryTitle": "全域納管邊界",
"boundaryIntro": "以下鍵值固定:這張矩陣只呈現可視化、證據狀態與人工閘門,不提供掃描、修復、主機變更、部署或版本來源變更。",
"boundaryIntro": "以下鍵值固定:這張矩陣只呈現可視化、證據狀態與受控授權閘門,不提供掃描、修復、主機變更、部署或版本來源變更。",
"summary": {
"assets": {
"label": "資產列",
@@ -16568,7 +16568,7 @@
"followupRuntimeGatePointer": {
"title": "後續執行期指標",
"body": "若決策牽涉掃描、修復、部署、主要來源切換或阻擋升級,草稿只能留下後續閘門指標。",
"draft": "只標記後續要走哪個人工閘門,不建立執行命令。",
"draft": "只標記後續要走哪個受控授權閘門,不建立執行命令。",
"guard": "不切主要來源、不停用 Gitea、不建立執行期閘門。"
}
}
@@ -16638,7 +16638,7 @@
"sourceControlSeparation": {
"title": "主要來源分離",
"body": "GitHub 主要來源切換、Gitea 停用、工作流程 / 機密設定或參照 動作不能由候選直接推進。",
"preflight": "只標記需要另開主要來源或版本控制人工閘門。",
"preflight": "只標記需要另開主要來源或版本控制受控授權閘門。",
"guard": "不切 GitHub 主要來源、不停用 Gitea、不改 工作流程 / 機密設定。"
}
}
@@ -16713,7 +16713,7 @@
},
"runtimeOrCutoverGateRequired": {
"title": "另開執行或切換閘門",
"body": "若候選需要掃描、修復、主機更新、主要來源切換或Gitea 停用,必須另開人工閘門。",
"body": "若候選需要掃描、修復、主機更新、主要來源切換或Gitea 停用,必須另開受控授權閘門。",
"result": "只標記需要哪一種後續閘門。",
"guard": "不呼叫 Kali、不開 SSH、不切 GitHub 主要來源、不停用 Gitea。"
}
@@ -16783,7 +16783,7 @@
},
"handoffSourceControlPointer": {
"title": "主要來源指標包",
"body": "GitHub 主要來源切換、Gitea 停用、參照或工作流程 / 機密設定 動作只能作為後續人工閘門指標。",
"body": "GitHub 主要來源切換、Gitea 停用、參照或工作流程 / 機密設定 動作只能作為後續受控授權閘門指標。",
"handoff": "只標記版本控制與主要來源相關待決事項。",
"guard": "不建立專案庫、不改 參照、不改 工作流程 / 機密設定、不停用 Gitea。"
}
@@ -16853,7 +16853,7 @@
},
"runtimeCutoverSeparation": {
"title": "執行與切換分離",
"body": "檢查掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用是否被留在獨立人工閘門。",
"body": "檢查掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用是否被留在獨立受控授權閘門。",
"review": "只標記後續需要哪一類獨立閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -16929,7 +16929,7 @@
},
"runtimeOrCutoverGateRequired": {
"title": "另開執行或切換閘門",
"body": "若結果需要掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用,必須另開人工閘門。",
"body": "若結果需要掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用,必須另開受控授權閘門。",
"result": "只標記需要哪一種後續閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -17005,7 +17005,7 @@
},
"runtimeCutoverPointer": {
"title": "執行切換指標包",
"body": "整理掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用是否需要另開人工閘門。",
"body": "整理掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用是否需要另開受控授權閘門。",
"prepare": "只標記後續可能需要哪一種獨立閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -17081,7 +17081,7 @@
},
"runtimeCutoverSeparationCheck": {
"title": "執行切換分離檢查",
"body": "確認掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用是否留在獨立人工閘門。",
"body": "確認掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用是否留在獨立受控授權閘門。",
"review": "只標記後續是否需要獨立閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -17157,7 +17157,7 @@
},
"runtimeOrPrimaryGateRequired": {
"title": "另開執行或主要來源閘門",
"body": "若後續需要掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用,必須另開人工閘門。",
"body": "若後續需要掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用,必須另開受控授權閘門。",
"result": "只標記後續需要哪一種獨立閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -17310,7 +17310,7 @@
"runtimePrimarySeparated": {
"title": "執行與主要來源分離檢查",
"body": "確認掃描、修復、主機更新、GitHub 主要來源切換或Gitea 停用都留在獨立閘門。",
"confirmation": "只標記後續是否需要獨立人工閘門。",
"confirmation": "只標記後續是否需要獨立受控授權閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
}
@@ -17385,7 +17385,7 @@
},
"runtimeOrPrimaryGateRequired": {
"title": "另開執行或主要來源閘門",
"body": "若後續需要 Kali 掃描、SSH、主機更新、修復、GitHub 主要來源切換或Gitea 停用,必須另開獨立人工閘門。",
"body": "若後續需要 Kali 掃描、SSH、主機更新、修復、GitHub 主要來源切換或Gitea 停用,必須另開獨立受控授權閘門。",
"result": "只標記需要哪一種後續閘門,不在本看板執行。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -17461,7 +17461,7 @@
},
"runtimePrimaryGatePacket": {
"title": "執行與主要來源閘門包",
"body": "整理後續若要 Kali、SSH、主機更新、修復、GitHub 主要來源切換或Gitea 停用時需要另開的人工閘門。",
"body": "整理後續若要 Kali、SSH、主機更新、修復、GitHub 主要來源切換或Gitea 停用時需要另開的受控授權閘門。",
"requirement": "只標記後續閘門類型,不在本看板執行。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
@@ -17538,7 +17538,7 @@
"runtimePrimarySeparated": {
"title": "執行與主要來源分離檢查",
"body": "確認 Kali、SSH、主機更新、修復、GitHub 主要來源切換或Gitea 停用都留在獨立閘門。",
"confirmation": "只標記後續是否需要獨立人工閘門。",
"confirmation": "只標記後續是否需要獨立受控授權閘門。",
"guard": "不呼叫 Kali、不開 SSH、不更新主機、不切主要來源、不停用 Gitea。"
}
}
@@ -17629,7 +17629,7 @@
"title": "後續節奏改成批次推進",
"body": "已完成多層清單;後續應優先收斂 P0 負責人回覆與AwoooP 落地,避免持續拆分低影響核對項。",
"unlock": "相同類型的 framework detail 之後合併成一批回報,只有高層閘門 變動才調整 重點。",
"guard": "節奏加快不等於放寬安全;執行期與來源-control cutover仍需人工閘門。"
"guard": "節奏加快不等於放寬安全;執行期與來源-control cutover仍需受控授權閘門。"
}
}
},
@@ -18114,7 +18114,7 @@
},
"concreteSecurityBlockerResolution": {
"title": "目前阻塞與解除條件",
"subtitle": "將 64% 無法前進的原因拆成六個阻塞點。每個阻塞點都標明為什麼卡住,以及要用哪種脫敏證據或人工閘門 才能解除;這仍是只讀狀態,不是批准或執行入口。",
"subtitle": "將 64% 無法前進的原因拆成六個阻塞點。每個阻塞點都標明為什麼卡住,以及要用哪種脫敏證據或受控授權閘門 才能解除;這仍是只讀狀態,不是批准或執行入口。",
"blockerLabel": "阻塞",
"whyLabel": "卡住原因",
"unlockLabel": "解除條件",
@@ -18225,13 +18225,13 @@
},
"toolsMonitoring": {
"title": "監控、工具與自動化流程",
"current": "可套用只讀狀態、阻塞解除條件、證據參照與人工閘門 顯示。",
"current": "可套用只讀狀態、阻塞解除條件、證據參照與受控授權閘門 顯示。",
"next": "先讓工具輸出 僅中繼資料證據,再接審查者與執行期閘門。",
"boundary": "不得讓工具自動觸發修復、部署、機密 收集或外部付費變更。"
},
"vibeWork": {
"title": "任務媒合產品 新專案",
"current": "已納入全產品三軸進度、IwoooS可視化、只讀 governance與人工閘門 口徑。",
"current": "已納入全產品三軸進度、IwoooS可視化、只讀 governance與受控授權閘門 口徑。",
"next": "先補產品 負責人、資料分級、版本來源與部署邊界,再評估後續分階段收嚴。",
"boundary": "不得因新專案納管就自動掃描、部署、改 專案庫、改 工作流程或套 阻擋 enforcement。"
},
@@ -19417,7 +19417,7 @@
"boundary": {
"title": "執行邊界",
"state": "閘門 0",
"detail": "目前 IwoooS 是低摩擦框架與只讀證據網;掃描、修復、部署、主機更新與版本來源變更都要獨立人工閘門。",
"detail": "目前 IwoooS 是低摩擦框架與只讀證據網;掃描、修復、部署、主機更新與版本來源變更都要獨立受控授權閘門。",
"evidence": "守門、快照、LOGBOOK 與邊界鍵都已鎖住。",
"next": "等 負責人證據或人工決策後,再另開 follow-up 執行期閘門。",
"locked": "runtime 授權仍維持關閉;執行期閘門維持 0。"
@@ -20005,7 +20005,7 @@
"mutationRequestRejected": {
"title": "變更要求拒收檢查",
"body": "確認回覆封套沒有夾帶建立專案庫、改可見性、同步分支、修改工作流程或收集機密值的要求。",
"failure": "夾帶變更要求時只能拒收或拆到獨立人工閘門。",
"failure": "夾帶變更要求時只能拒收或拆到獨立受控授權閘門。",
"guard": "不從封套觸發 GitHub、Gitea、分支 / 標籤參照、工作流程、機密設定或執行器動作。"
},
"followupOwnerTrace": {
@@ -20075,7 +20075,7 @@
"rejectMutationRequest": {
"title": "拒收變更要求",
"body": "封套夾帶建立專案庫、改可見性、同步分支、修改工作流程或收集機密值時,直接分流為拒收。",
"next": "只記為需另開人工閘門的變更要求,不在封套內處理。",
"next": "只記為需另開受控授權閘門的變更要求,不在封套內處理。",
"guard": "不從封套觸發 GitHub、Gitea、分支 / 標籤參照、工作流程、機密設定或執行器動作。"
},
"keepFollowupOwnerWaiting": {
@@ -20259,7 +20259,7 @@
},
"allProducts": {
"title": "所有產品先套只讀框架",
"body": "核心產品、公開網站、版本來源、主機、監控工具、任務媒合產品與未來產品七類都已套用 IwoooS可視化與人工閘門 口徑。"
"body": "核心產品、公開網站、版本來源、主機、監控工具、任務媒合產品與未來產品七類都已套用 IwoooS可視化與受控授權閘門 口徑。"
},
"runtime": {
"title": "執行期仍保持關閉",
@@ -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

@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function AwoooPAlertsPage({ params }: { params: { locale: string } }) {
redirect(`/${params.locale}/awooop/runs#ai-alert-card-delivery-readback`);
}

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,261 @@
## 2026-06-27 — 22:28 AwoooP manual gate payload residue 正式清零
**背景**:正式 `/zh-TW/awooop/approvals`、Runs、Work Items 的 HTML payload 仍因 Next / i18n namespace 序列化帶出 IwoooS 舊文案 `人工閘門`,每頁各命中 32 次;`/zh-TW/awooop/alerts` 也回 404。這會讓 AwoooP 看起來仍把人工 gate 當作 low / medium / high 的預設終局。
**完成內容**
- `apps/web/messages/zh-TW.json``apps/web/messages/en.json` 將 32 組 IwoooS payload 字串從 `人工閘門` 改為 `受控授權閘門`,保留 hard blocker / no-runtime-write 邊界,但不再使用人工 gate 作為產品預設語意。
- 新增 `apps/web/src/app/[locale]/awooop/alerts/page.tsx`,將 `/awooop/alerts` 穩定導向 Runs 的 `#ai-alert-card-delivery-readback` 區段,避免 Alerts smoke 變成 404。
**本地驗證結果**
- JSON parse`apps/web/messages/zh-TW.json``apps/web/messages/en.json` 通過。
- i18n mirrorzh-TW / en key count `14468 / 14468`missing `0 / 0`
- source 搜尋AwoooP source 與 messages 內 `待人工決策``阻塞與人工閘門``人工接手``人工閘門``人工升級``owner review``owner packet` 命中 `0`
- `pnpm --filter @awoooi/web typecheck`:通過。
- `git diff --check`:通過。
**Gitea / deploy 狀態**
- fix commit`b2458b933 fix(awooop): remove manual gate payload residue`
- 後續整合 head`a2733fd43 test(awooop): lock controlled apply receipt km snapshot`,包含 `b2458b933`
- deploy marker`f98aaa8ee chore(cd): deploy a2733fd [skip ci]`
- Gitea `code-review.yaml #3705` 成功Gitea `cd.yaml #3704` 成功並產出 deploy marker。
**production readback**
- `/api/v1/health?_v=f98aaa8ee-manual-residue-clean`HTTP `200``status=healthy``environment=prod``mock_mode=false`
- HTML readbackApprovals / Runs / Work Items / Alerts 皆 HTTP `200`,舊詞 hit count 全部 `0`
- `/zh-TW/awooop/alerts` final URL 讀回 `/zh-TW/awooop/runs`,不再 404。
- 新語意可讀:`待 AI 受控決策``阻塞與 AI 受控隊列``AI 處置包與工作項``受控執行邊界``受控授權閘門``controlled gate``controlled review`
**desktop / mobile smoke**
- Chrome headless desktop `1366x900` 與 mobile `390x844`Approvals / Runs / Work Items / Alerts 皆 HTTP `200`
- visible 舊詞 count `0`、HTML 舊詞 count `0``horizontalOverflow=false`
- Alerts final URL`/zh-TW/awooop/runs#ai-alert-card-delivery-readback`
- Approvals 仍有既有未登入資源 `401` console error本輪未新增且不影響 HTML / visible text / layout smoke。
**未做與邊界**
- 沒有把 critical / secret / destructive / active scan / force push / repo visibility / refs mutation 放寬成自動執行。
- 沒有 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作;沒有讀 secret沒有 force push。
**下一步**
- 若後續再清 IwoooS / security 歷史 payload應只改產品顯示與 controlled / break-glass 語意,不把 `0 / false` 計數假性上修。
## 2026-06-27 — 21:56 GitHub owner response preflight API 正式讀回完成
**背景**:上一段已把 GitHub target owner response request packet / templates / forbidden payloads 暴露成 read-only readiness本段不是文件補充而是新增正式 POST preflight API讓 owner-provided redacted evidence refs 可以先被驗證,同時拒收 credential URL、repo creation command、secret-like value 與未允許欄位。
**完成內容**
- 新增 `POST /api/v1/agents/github-target-owner-response-intake-preflight`
- API 只做 candidate owner response validation`mode=validate_owner_response_only_no_persist_no_github_write`
- `apps/api/src/services/github_target_private_backup_evidence_gate.py` 新增 `preflight_github_target_owner_response_submission(...)`,依 committed templates / allowed fields / acceptable decisions / evidence refs / forbidden payloads 驗證。
- 合法 `read_only_markdown_response` 可回 `ready_for_read_only_owner_response_intake`;含 private clone credential、GitHub token-like string、repo creation command、unsupported field 或 secret-like value 會回 `blocked_owner_response_intake_preflight`
- 回應固定經過 public LAN topology redaction正式回應未命中 `192.168.0.`
**本地驗證結果**
- `python3 -m py_compile apps/api/src/services/github_target_private_backup_evidence_gate.py apps/api/src/api/v1/agents.py`:通過。
- `DATABASE_URL=sqlite:///test.db PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_github_target_private_backup_evidence_gate.py apps/api/tests/test_github_target_private_backup_evidence_gate_api.py apps/api/tests/test_delivery_closure_workbench_api.py -q``10 passed`
- `python3.11 -m ruff check apps/api/src/services/github_target_private_backup_evidence_gate.py apps/api/src/api/v1/agents.py apps/api/tests/test_github_target_private_backup_evidence_gate.py apps/api/tests/test_github_target_private_backup_evidence_gate_api.py`:通過。
- `git diff --check`:通過。
**Gitea / deploy 狀態**
- code commit`a68d9e40a feat(github): preflight owner response intake`
- deploy marker`5b7bd55a9 chore(cd): deploy a68d9e4 [skip ci]`
- `code-review.yaml #3697` 成功;原 `cd.yaml #3696` 後續被更新的 main push supersede / cancel但已產出 deploy marker。
- 最新 `gitea-ssh/main=0624be08d` 已包含 `a68d9e40a`;目前尚未看到比 `5b7bd55a9` 更新且指向 `0624be08d` 的 deploy marker。
**production API readback**
- `/api/v1/health?_v=a68d9e40-owner-preflight-final`HTTP `200``status=healthy``environment=prod``mock_mode=false`
- valid POSTHTTP `200`、schema `github_target_owner_response_intake_preflight_v1`、status `ready_for_read_only_owner_response_intake``candidate_response_item_count=1``preflight_passed_response_item_count=1``preflight_blocked_response_item_count=0``accepted_for_read_only_intake=true`
- dangerous POSTHTTP `200`、status `blocked_owner_response_intake_preflight``preflight_passed_response_item_count=0``preflight_blocked_response_item_count=1``forbidden_payload_hit_count=4`、blockers 包含 `forbidden_payload_detected``unsupported_response_fields`
**仍維持 0 / false**
- valid / dangerous POST 皆維持 `owner_response_received_count=0``owner_response_accepted_count=0``safe_credential_accepted_evidence_count=0`
- `github_api_write_allowed=false``repo_creation_authorized=false``refs_sync_authorized=false``persist_submission_allowed=false``private_clone_url_collection_allowed=false`
**未做**
- 沒有 GitHub repo creation、visibility change、refs sync、workflow trigger、private clone URL collection、secret value collection 或 persistence。
- 沒有 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作;沒有 force push。
**完成度 / 下一步**
- 本段「owner response intake preflight API / production readback」`0% -> 100%`
- GitHub backup mirror governance 仍為 blocked下一個 P0 是設計 no-secret persistence contract 或 workbench UI intake但只能保存 redacted evidence refs 與 validation receipt不能保存 private clone credential、secret value 或執行 GitHub write。
## 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 正式讀回完成
**時間與來源**
- 2026-06-27 20:49-21:24 Asia/Taipei。
- 來源feature branch `codex/github-backup-missing-targets-20260627`、Gitea main、Gitea Actions、production API readback。
**完成內容**
- `GET /api/v1/agents/github-target-private-backup-evidence-gate` 已正式讀回 `owner_response_intake_readiness`
- GitHub target owner response request packet、9 個 response templates、allowed fields、forbidden payloads、collection checks、intake preflight checks 與 acceptance checks 已能由 production API 讀出。
- 每個 target 已能讀回 owner response template mapping 與 `owner_response_execution_authorized=false`
- Validator 已防止 owner response request packet、templates 或 checks 夾帶 execution authorization。
**Gitea / deploy 狀態**
- code commit`80138e985 feat(api): expose github owner response intake readiness`
- merge / main push commit`9f5097f66 Merge remote-tracking branch 'gitea/main' into codex/github-backup-missing-targets-20260627`
- deploy marker`e49c6190e chore(cd): deploy 9f5097f [skip ci]`
- code-review`#3690` 成功。
- CD`#3689` 成功tests、build-and-deploy、post-deploy-checks 均完成。
- 最新 main 後續 docs marker`7b2b3db45 docs(awooop): record controlled automation readback [skip ci]`,不改本段 production code 基準。
**production API readback**
- `/api/v1/health?_v=9f5097f66-github-owner-intake`HTTP `200``status=healthy``environment=prod``mock_mode=false`
- `GET /api/v1/agents/github-target-private-backup-evidence-gate?_v=9f5097f66-github-owner-intake`HTTP `200`
- `approval_required_target_count=9``private_backup_verified_count=4``private_visibility_verified_count=4`
- `github_missing_target_resolution_count=5``github_missing_target_create_private_repo_ready_count=0``github_missing_target_refs_sync_ready_count=0`
- `owner_response_request_ready=true``owner_response_required_response_item_count=9``owner_response_requested_template_count=9``owner_response_template_count=9`
- `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`
- schema`delivery_closure_workbench_v1`
- GitHub lane`status=blocked_private_visibility_and_safe_credential_evidence_required`、metric `private_backup_verified=4/9``completion_percent=44``blocker_count=9`
**仍維持 0 / false**
- `github_missing_target_create_private_repo_ready_count=0``github_missing_target_refs_sync_ready_count=0``owner_response_received_count=0``owner_response_accepted_count=0``safe_credential_accepted_evidence_count=0``execution_ready_count=0`
- `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`
**未做**
- 沒有 GitHub repo creation、visibility change、refs sync、workflow trigger、private clone URL collection、secret value collection沒有 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作;沒有 force push。
**完成度 / 下一步**
- 本段「GitHub owner response intake readiness API / production readback」`85% -> 100%`
- GitHub backup mirror governance 仍為 blocked下一個 P0 是 owner-provided safe credential evidence / redacted evidence refs intake但不得收 private clone URL credential 或 secret value。
## 2026-06-27AwoooP Approvals controlled automation 文案正式讀回完成
**背景**P2-416 D1N 已把 AI Agent 舊 manual gate 規範改為 controlled automationlow / medium / high 風險應走 allowlist、check-mode、controlled apply、verifier、rollback 與 KM / PlayBook writebackcritical 才 break-glass。正式 `/zh-TW/awooop/approvals` 仍因 Next HTML payload 序列化其他 namespace殘留 `待人工決策``阻塞與人工閘門``人工接手``owner review``owner packet` 等舊語意,容易讓 Approvals / Runs / Work Items / Alerts 看起來把人工當預設終局。
@@ -71,7 +329,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。
@@ -47597,3 +47854,88 @@ production browser smoke:
**下一個 P0**
- 開新工作視窗處理 owner-provided redacted Wazuh manager registry export 收件與 reviewer validation passed只有 evidence 完整、算術一致、6 alias scope parity、Dashboard API repair readback 與 owner / rollback owner 齊全時,才可推進 manager registry accepted仍不得把 Dashboard 200 或前台可見當成全主機納管完成。
## 2026-06-27 — 21:47 GitHub safe credential evidence intake readiness 本地完成
**時間與來源**
- 2026-06-27 21:47 Asia/Taipei。
- 來源feature branch `codex/github-safe-credential-intake-20260627``gitea/main=1a6f8f427`
**完成內容**
- `apps/api/src/services/github_target_private_backup_evidence_gate.py` 新增 `safe_credential_evidence_intake_readiness` readback將 GitHub target owner response snapshot 內的 `evidence_ref_rules``owner_response_redaction_examples``forbidden_payloads`、quarantine lanes 與 submission modes 投影到 API。
- summary 新增 safe credential intake counters`safe_credential_evidence_intake_ready=true``safe_credential_required_redacted_evidence_ref_count=9``safe_credential_evidence_ref_rule_count=5``safe_credential_redaction_example_count=5``safe_credential_forbidden_payload_count=15``safe_credential_quarantine_lane_count=1``safe_credential_raw_payload_storage_allowed=false`
- per-target 新增安全收件狀態approval-required targets 顯示 `waiting_redacted_evidence_ref`external target 顯示 `not_required`allowed ref types 只允許 `repo_path``snapshot_path``redacted_metadata_pointer`
- 一致性檢查新增 fail-closed guardredaction example 不得允許 raw payload storageforbidden payloads 必須包含 `token_value``secret_value``private_clone_url_credential``repo_archive``git_object_pack`
**本地驗證結果**
- `python3.11 -m ruff format apps/api/src/services/github_target_private_backup_evidence_gate.py apps/api/tests/test_github_target_private_backup_evidence_gate.py apps/api/tests/test_github_target_private_backup_evidence_gate_api.py`:通過,`3 files left unchanged`
- `python3.11 -m ruff check apps/api/src/services/github_target_private_backup_evidence_gate.py apps/api/tests/test_github_target_private_backup_evidence_gate.py apps/api/tests/test_github_target_private_backup_evidence_gate_api.py`:通過。
- `python3 -m py_compile apps/api/src/services/github_target_private_backup_evidence_gate.py`:通過。
- `DATABASE_URL=sqlite:///test.db PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_github_target_private_backup_evidence_gate.py apps/api/tests/test_github_target_private_backup_evidence_gate_api.py apps/api/tests/test_delivery_closure_workbench_api.py -q``9 passed`
- `git diff --check`:通過。
- 本地 readback snippet`safe_credential_evidence_intake_ready=True`、required redacted evidence refs `9`、evidence rules `5`、redaction examples `5`、forbidden payloads `15`、quarantine lanes `1`、raw payload storage allowed `False``private_backup_verified_count=4``execution_ready_count=0``blocked_target_count=9`
**完成度與同步狀態**
- 本段「GitHub safe credential evidence intake readiness source/API」本地`0% -> 85%`
- 尚未 merge/push main、尚未 CD、尚未 production API readbackproduction 仍以前一版為準直到 Gitea CD 成功與 readback 完成。
**仍維持 0 / false**
- `safe_credential_accepted_evidence_count=0``owner_response_received_count=0``owner_response_accepted_count=0``github_missing_target_create_private_repo_ready_count=0``github_missing_target_refs_sync_ready_count=0``execution_ready_count=0``blocked_target_count=9`
- `secret_value_collection_allowed=false``private_clone_url_collection_allowed=false``credential_value_collection_allowed=false``stored_raw_payload_allowed=false``repo_creation_authorized=false``visibility_change_authorized=false``refs_sync_authorized=false``workflow_trigger_authorized=false`
**做過的命令類型**
- 寫入repo service / test / LOGBOOK。
- 只讀git fetch / merge check、本地 API readback snippet、本地 focused tests。
- 未做:沒有 GitHub repo creation、visibility change、refs sync、workflow trigger、private clone URL collection、secret value collection沒有 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作;沒有 force push。
**下一個 P0**
- commit feature正常 push 到 Gitea確認 CD idle/success 後 normal push `HEAD:main`,再做 production readback目標為 `safe_credential_evidence_intake_ready=true`、required refs `9`、rules `5`、redaction examples `5`、forbidden payloads `15`、quarantine lanes `1`,同時 create/private ready `0`、refs sync ready `0`、execution ready `0` 維持不變。
## 2026-06-27 — 22:35 GitHub safe credential evidence intake readiness production 讀回完成
**時間與來源**
- 2026-06-27 22:18-22:35 Asia/Taipei。
- 來源feature branch `codex/github-safe-credential-intake-20260627`、main release `30af0fb420`、deploy marker `257544d09`、Gitea Actions 與 production API readback。
**完成內容**
- GitHub private backup evidence gate 已正式暴露 `safe_credential_evidence_intake_readiness`,可讀出脫敏 evidence ref 收件狀態、redaction examples、forbidden payloads 與 quarantine lanes。
- 本段只建立 safe credential evidence 的 read-only intake contract不代表 safe credential evidence 已 accepted也不代表 repo creation、visibility change、refs sync 或 GitHub primary switch 已授權。
**runs 與驗證**
- feature latest`30af0fb420 Merge remote-tracking branch 'gitea/main' into codex/github-safe-credential-intake-20260627`
- deploy marker`257544d09 chore(cd): deploy 30af0fb [skip ci]`
- Gitea CD run `3707`Success。
- Gitea code-review run `3708`Success。
- 本地驗證:`python3.11 -m ruff check ...` 通過、`python3 -m py_compile apps/api/src/services/github_target_private_backup_evidence_gate.py` 通過、focused pytest `12 passed``pnpm --dir apps/web typecheck` 通過、`git diff --check` 通過。
**production API readback**
- `GET https://awoooi.wooo.work/api/v1/health``200``status=healthy``environment=prod``mock_mode=false`
- `GET https://awoooi.wooo.work/api/v1/agents/github-target-private-backup-evidence-gate``200`
- `safe_credential_evidence_intake_ready=True`
- `safe_credential_required_redacted_evidence_ref_count=9``safe_credential_evidence_ref_rule_count=5``safe_credential_redaction_example_count=5``safe_credential_forbidden_payload_count=15``safe_credential_quarantine_lane_count=1`
- `safe_credential_raw_payload_storage_allowed=False``private_clone_url_collection_allowed=False``secret_value_collection_allowed=False`
- top-level status`ready_to_collect_redacted_evidence_refs_not_credentials``intake_ready=True``execution_authorized=False``not_approval=True`
- first target `owenhytsai/awoooi``safe_credential_evidence_submission_status=waiting_redacted_evidence_ref``safe_credential_raw_payload_storage_allowed=False`
**production delivery readback**
- `GET https://awoooi.wooo.work/api/v1/agents/delivery-closure-workbench``200``schema_version=delivery_closure_workbench_v1`
- GitHub lane`lane_id=github``status=blocked_private_visibility_and_safe_credential_evidence_required``completion_percent=44``blocker_count=9`
**完成度與同步狀態**
- 本段「GitHub safe credential evidence intake readiness API / production 讀回」:`85% -> 100%`
- GitHub backup mirror governance仍為 blocked本段只把安全收件契約正式上線不代表 private backup gate 已開。
**仍維持 0 / false**
- `safe_credential_accepted_evidence_count=0``owner_response_received_count=0``owner_response_accepted_count=0`
- `github_missing_target_create_private_repo_ready_count=0``github_missing_target_refs_sync_ready_count=0``execution_ready_count=0``blocked_target_count=9``private_backup_verified_count=4`
- `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``stored_raw_payload_allowed=false`
**做過的命令類型**
- 寫入repo service / test / LOGBOOK以及正常 Gitea feature / main push。
- 只讀Gitea Actions UI readback、production health、production GitHub gate API、Delivery Workbench API。
- 未做:沒有 GitHub repo creation、visibility change、refs sync、workflow trigger、private clone URL collection、secret value collection沒有 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作;沒有 force push。
**下一個 P0**
- `P0-01` GitHub private backup evidence acceptance逐 target 收 owner-provided redacted evidence refs`safe_credential_accepted_evidence_count``0/9` 往上推;仍不得收 private clone URL、secret value、repo archive 或 git object pack。
- `P0-02` missing target canonical source owner decision釐清 `ewoooc``bitan-pharmacy``tsenyang-website``VibeWork``agent-bounty-protocol` 的 canonical source before repo creation。
- `P0-03` refs sync readinesscanonical source 與 owner decision 未成立前,`refs_sync_ready_count` 必須維持 `0`

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: f47ee7d966da266db5980190758fd4aabece1a18
newTag: 30af0fb4202073719f8c8011fe773f806576ede0
- name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER
newName: 192.168.0.110:5000/awoooi/web
newTag: f47ee7d966da266db5980190758fd4aabece1a18
newTag: 30af0fb4202073719f8c8011fe773f806576ede0

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']}"
)