diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 755e99f4..92ecc076 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -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], diff --git a/apps/api/src/services/github_target_private_backup_evidence_gate.py b/apps/api/src/services/github_target_private_backup_evidence_gate.py index 0e034119..f8b6f92e 100644 --- a/apps/api/src/services/github_target_private_backup_evidence_gate.py +++ b/apps/api/src/services/github_target_private_backup_evidence_gate.py @@ -8,6 +8,7 @@ because AWOOOI policy requires GitHub backup targets to be private. from __future__ import annotations import json +import re from pathlib import Path from typing import Any @@ -22,6 +23,39 @@ _APPROVAL_PACKAGE_FILE = "github-target-repo-approval-package.snapshot.json" _PROBE_FILE = "github-target-probe.snapshot.json" _CONNECTOR_READBACK_FILE = "github-target-connector-readback.snapshot.json" _MISSING_SOURCE_READINESS_FILE = "github-target-missing-source-readiness.snapshot.json" +_PREFLIGHT_SCHEMA_VERSION = "github_target_owner_response_intake_preflight_v1" +_PREFLIGHT_MODE = "validate_owner_response_only_no_persist_no_github_write" +_SUBMISSION_METADATA_FIELDS = { + "github_repo", + "response_id", + "submission_mode", + "template_id", +} +_FORBIDDEN_KEY_FRAGMENTS = { + "api_request_body", + "authorization_header", + "cookie", + "credential", + "db_dump", + "deploy_key", + "git_object_pack", + "password", + "private_clone_url", + "private_key", + "repo_archive", + "repo_creation_command", + "secret_value", + "session", + "token_value", + "visibility_change_command", +} +_SENSITIVE_VALUE_PATTERNS = ( + ("github_token", re.compile(r"\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b")), + ("github_fine_grained_token", re.compile(r"\bgithub_pat_[A-Za-z0-9_]{20,}\b")), + ("private_key_block", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")), + ("credentialed_url", re.compile(r"[a-z][a-z0-9+.-]*://[^/\s:@]+:[^@\s]+@")), + ("raw_internal_lan_address", re.compile(r"\b192\.168\.0\.\d+\b")), +) def load_latest_github_target_private_backup_evidence_gate( @@ -56,6 +90,127 @@ def load_latest_github_target_private_backup_evidence_gate( ) +def preflight_github_target_owner_response_submission( + submission: dict[str, Any], + security_dir: Path | None = None, +) -> dict[str, Any]: + """Validate an owner response candidate without storing or executing it.""" + gate = load_latest_github_target_private_backup_evidence_gate(security_dir) + intake = _dict(gate.get("owner_response_intake_readiness")) + target_by_template = { + str(target.get("owner_response_template_id")): _dict(target) + for target in _list(gate.get("targets")) + if target.get("owner_response_template_id") + } + payload = _dict(submission) + allowed_modes = set(_strings(intake.get("allowed_submission_modes"))) + allowed_fields = set(_strings(intake.get("allowed_response_fields"))) + forbidden_payloads = set(_strings(intake.get("forbidden_payloads"))) + still_forbidden = set(_strings(intake.get("still_forbidden"))) + + submission_mode = str(payload.get("submission_mode") or "") + candidate_responses = [_dict(row) for row in _list(payload.get("responses"))] + mode_allowed = submission_mode in allowed_modes + global_scan_payload = { + key: value for key, value in payload.items() if key != "responses" + } + global_hits = _forbidden_payload_hits( + global_scan_payload, + forbidden_payloads=forbidden_payloads | still_forbidden, + ) + response_results = [ + _preflight_owner_response_item( + response=response, + submission_mode=submission_mode, + mode_allowed=mode_allowed, + target_by_template=target_by_template, + allowed_fields=allowed_fields, + forbidden_payloads=forbidden_payloads | still_forbidden, + ) + for response in candidate_responses + ] + passed_count = sum( + 1 for row in response_results if row["accepted_for_read_only_intake"] is True + ) + blocked_count = len(response_results) - passed_count + global_blockers: list[str] = [] + if not mode_allowed: + global_blockers.append("submission_mode_not_allowed") + if not candidate_responses: + global_blockers.append("response_items_missing") + if global_hits: + global_blockers.append("forbidden_payload_detected") + + preflight_passed = ( + not global_blockers and candidate_responses and blocked_count == 0 + ) + return { + "schema_version": _PREFLIGHT_SCHEMA_VERSION, + "generated_at": gate.get("generated_at", ""), + "status": "ready_for_read_only_owner_response_intake" + if preflight_passed + else "blocked_owner_response_intake_preflight", + "mode": _PREFLIGHT_MODE, + "summary": { + "candidate_response_item_count": len(candidate_responses), + "preflight_passed_response_item_count": passed_count, + "preflight_blocked_response_item_count": blocked_count, + "forbidden_payload_hit_count": len(global_hits) + + sum(len(row["forbidden_hits"]) for row in response_results), + "unsupported_field_count": sum( + len(row["unsupported_fields"]) for row in response_results + ), + "missing_required_field_count": sum( + len(row["missing_required_fields"]) for row in response_results + ), + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "safe_credential_accepted_evidence_count": 0, + "github_missing_target_create_private_repo_ready_count": gate["summary"][ + "github_missing_target_create_private_repo_ready_count" + ], + "github_missing_target_refs_sync_ready_count": gate["summary"][ + "github_missing_target_refs_sync_ready_count" + ], + "execution_authorized": False, + "write_performed": False, + "github_api_write_allowed": False, + "repo_creation_authorized": False, + "visibility_change_authorized": False, + "refs_sync_authorized": False, + "secret_value_collection_allowed": False, + }, + "submission_mode": submission_mode, + "submission_mode_allowed": mode_allowed, + "allowed_submission_modes": sorted(allowed_modes), + "global_blockers": global_blockers, + "global_forbidden_hits": global_hits, + "responses": response_results, + "operation_boundaries": { + "preflight_only": True, + "persist_submission_allowed": False, + "read_only_markdown_response_allowed": True, + "redacted_metadata_pointer_allowed": True, + "github_api_write_allowed": False, + "repo_creation_allowed": False, + "visibility_change_allowed": False, + "refs_sync_allowed": False, + "workflow_trigger_allowed": False, + "private_clone_url_collection_allowed": False, + "secret_value_collection_allowed": False, + }, + "authorization_flags": { + "owner_response_execution_authorized": False, + "repo_creation_authorized": False, + "visibility_change_authorized": False, + "refs_sync_authorized": False, + "workflow_trigger_authorized": False, + "secret_value_collection_allowed": False, + "private_clone_url_collection_allowed": False, + }, + } + + def build_github_target_private_backup_evidence_gate( *, decision: dict[str, Any], @@ -293,6 +448,89 @@ def build_github_target_private_backup_evidence_gate( } +def _preflight_owner_response_item( + *, + response: dict[str, Any], + submission_mode: str, + mode_allowed: bool, + target_by_template: dict[str, dict[str, Any]], + allowed_fields: set[str], + forbidden_payloads: set[str], +) -> dict[str, Any]: + template_id = str(response.get("template_id") or "") + target = target_by_template.get(template_id, {}) + required_fields = set(_strings(target.get("owner_response_required_fields"))) + acceptable_decisions = set( + _strings(target.get("owner_response_acceptable_decisions")) + ) + response_fields = set(response) + supported_fields = allowed_fields | _SUBMISSION_METADATA_FIELDS + unsupported_fields = sorted(response_fields - supported_fields) + missing_required_fields = sorted( + field + for field in required_fields + if not _has_response_value(response.get(field)) + ) + evidence_refs = sorted( + set(_response_strings(response.get("redacted_evidence_refs"))) + | set(_response_strings(response.get("evidence_refs"))) + ) + decision = str(response.get("decision") or "") + blockers: list[str] = [] + if not mode_allowed: + blockers.append("submission_mode_not_allowed") + if not target: + blockers.append("unknown_or_unrequested_template_id") + if unsupported_fields: + blockers.append("unsupported_response_fields") + if missing_required_fields: + blockers.append("required_fields_missing") + if acceptable_decisions and decision not in acceptable_decisions: + blockers.append("decision_not_allowed_for_template") + if not evidence_refs: + blockers.append("redacted_evidence_refs_missing") + + forbidden_hits = _forbidden_payload_hits( + response, + forbidden_payloads=forbidden_payloads, + ) + evidence_ref_hits = _evidence_ref_hits(evidence_refs) + if forbidden_hits: + blockers.append("forbidden_payload_detected") + if evidence_ref_hits: + blockers.append("unsafe_evidence_ref_detected") + + accepted = not blockers + return { + "template_id": template_id, + "github_repo": str( + response.get("github_repo") or target.get("github_repo") or "" + ), + "submission_mode": submission_mode, + "status": "preflight_passed_read_only_intake_candidate" + if accepted + else "blocked_owner_response_candidate", + "accepted_for_read_only_intake": accepted, + "decision": decision, + "allowed_decision_count": len(acceptable_decisions), + "required_field_count": len(required_fields), + "missing_required_fields": missing_required_fields, + "unsupported_fields": unsupported_fields, + "redacted_evidence_ref_count": len(evidence_refs), + "redacted_evidence_refs": evidence_refs, + "forbidden_hits": forbidden_hits, + "evidence_ref_hits": evidence_ref_hits, + "blockers": sorted(set(blockers)), + "owner_response_received": False, + "owner_response_accepted": False, + "safe_credential_evidence_accepted": False, + "execution_authorized": False, + "repo_creation_authorized": False, + "visibility_change_authorized": False, + "refs_sync_authorized": False, + } + + def _build_target( *, decision: dict[str, Any], @@ -873,6 +1111,113 @@ def _rejection_rules(owner_response: dict[str, Any]) -> list[str]: ] +def _has_response_value(value: Any) -> bool: + if isinstance(value, str): + return bool(value.strip()) + if isinstance(value, list): + return any(_has_response_value(item) for item in value) + return value is not None + + +def _response_strings(value: Any) -> list[str]: + if isinstance(value, str): + return [value] if value.strip() else [] + if isinstance(value, list): + return [str(item) for item in value if str(item).strip()] + return [] + + +def _forbidden_payload_hits( + value: Any, + *, + forbidden_payloads: set[str], + path: str = "$", +) -> list[dict[str, str]]: + hits: list[dict[str, str]] = [] + if isinstance(value, dict): + for key, child in value.items(): + key_text = str(key) + key_lower = key_text.lower() + if ( + key_lower in forbidden_payloads + or any(fragment in key_lower for fragment in _FORBIDDEN_KEY_FRAGMENTS) + ): + hits.append( + { + "path": f"{path}.{key_text}", + "kind": "forbidden_field", + "match": key_text, + } + ) + hits.extend( + _forbidden_payload_hits( + child, + forbidden_payloads=forbidden_payloads, + path=f"{path}.{key_text}", + ) + ) + return hits + if isinstance(value, list): + for index, child in enumerate(value): + hits.extend( + _forbidden_payload_hits( + child, + forbidden_payloads=forbidden_payloads, + path=f"{path}[{index}]", + ) + ) + return hits + if isinstance(value, str): + lowered = value.lower() + for forbidden in sorted(forbidden_payloads): + if forbidden and forbidden.lower() in lowered: + hits.append( + { + "path": path, + "kind": "forbidden_payload_label", + "match": forbidden, + } + ) + for label, pattern in _SENSITIVE_VALUE_PATTERNS: + if pattern.search(value): + hits.append( + { + "path": path, + "kind": label, + "match": label, + } + ) + return hits + + +def _evidence_ref_hits(evidence_refs: list[str]) -> list[dict[str, str]]: + hits: list[dict[str, str]] = [] + for index, evidence_ref in enumerate(evidence_refs): + if not ( + evidence_ref.startswith("docs/") + or evidence_ref.startswith("reports/") + or evidence_ref.startswith("owner-metadata:") + or evidence_ref.startswith("redacted:") + ): + hits.append( + { + "path": f"evidence_refs[{index}]", + "kind": "unsupported_evidence_ref_scheme", + "match": evidence_ref, + } + ) + for label, pattern in _SENSITIVE_VALUE_PATTERNS: + if pattern.search(evidence_ref): + hits.append( + { + "path": f"evidence_refs[{index}]", + "kind": label, + "match": label, + } + ) + return hits + + def _dict(value: Any) -> dict[str, Any]: return value if isinstance(value, dict) else {} @@ -888,6 +1233,6 @@ def _strings(value: Any) -> list[str]: def _int(value: Any) -> int: if isinstance(value, bool): return int(value) - if isinstance(value, (int, float)): + if isinstance(value, int | float): return int(value) return 0 diff --git a/apps/api/tests/test_github_target_private_backup_evidence_gate.py b/apps/api/tests/test_github_target_private_backup_evidence_gate.py index 7c0f81f0..e4ecbecc 100644 --- a/apps/api/tests/test_github_target_private_backup_evidence_gate.py +++ b/apps/api/tests/test_github_target_private_backup_evidence_gate.py @@ -8,6 +8,7 @@ import pytest from src.services.github_target_private_backup_evidence_gate import ( load_latest_github_target_private_backup_evidence_gate, + preflight_github_target_owner_response_submission, ) from src.services.snapshot_paths import default_security_dir @@ -208,6 +209,62 @@ def test_github_target_private_backup_gate_rejects_missing_source_write_flags(tm load_latest_github_target_private_backup_evidence_gate(tmp_path) +def test_github_target_owner_response_preflight_accepts_redacted_evidence_refs(): + preflight = preflight_github_target_owner_response_submission( + _valid_owner_response_submission() + ) + + assert ( + preflight["schema_version"] + == "github_target_owner_response_intake_preflight_v1" + ) + assert preflight["status"] == "ready_for_read_only_owner_response_intake" + assert preflight["mode"] == "validate_owner_response_only_no_persist_no_github_write" + assert preflight["summary"]["candidate_response_item_count"] == 1 + assert preflight["summary"]["preflight_passed_response_item_count"] == 1 + assert preflight["summary"]["preflight_blocked_response_item_count"] == 0 + assert preflight["summary"]["owner_response_received_count"] == 0 + assert preflight["summary"]["owner_response_accepted_count"] == 0 + assert preflight["summary"]["safe_credential_accepted_evidence_count"] == 0 + assert preflight["summary"]["github_api_write_allowed"] is False + assert preflight["summary"]["repo_creation_authorized"] is False + assert preflight["summary"]["refs_sync_authorized"] is False + assert preflight["operation_boundaries"]["persist_submission_allowed"] is False + assert preflight["operation_boundaries"]["github_api_write_allowed"] is False + assert preflight["operation_boundaries"]["private_clone_url_collection_allowed"] is False + assert preflight["authorization_flags"]["owner_response_execution_authorized"] is False + assert preflight["responses"][0]["accepted_for_read_only_intake"] is True + assert preflight["responses"][0]["owner_response_received"] is False + assert preflight["responses"][0]["owner_response_accepted"] is False + + +def test_github_target_owner_response_preflight_blocks_credentials_and_commands(): + submission = _valid_owner_response_submission() + submission["responses"][0]["private_clone_url_credential"] = ( + "https://owner:ghp_1234567890abcdefghijklmnopqrstu@github.com/owenhytsai/awoooi.git" + ) + submission["responses"][0]["repo_creation_command"] = ( + "gh repo create owenhytsai/awoooi --private" + ) + + preflight = preflight_github_target_owner_response_submission(submission) + + assert preflight["status"] == "blocked_owner_response_intake_preflight" + assert preflight["summary"]["candidate_response_item_count"] == 1 + assert preflight["summary"]["preflight_passed_response_item_count"] == 0 + assert preflight["summary"]["preflight_blocked_response_item_count"] == 1 + assert preflight["summary"]["forbidden_payload_hit_count"] >= 3 + assert preflight["summary"]["owner_response_received_count"] == 0 + assert preflight["summary"]["owner_response_accepted_count"] == 0 + assert preflight["summary"]["github_api_write_allowed"] is False + response = preflight["responses"][0] + assert response["accepted_for_read_only_intake"] is False + assert "forbidden_payload_detected" in response["blockers"] + assert "unsupported_response_fields" in response["blockers"] + assert response["execution_authorized"] is False + assert response["repo_creation_authorized"] is False + + def _copy_security_snapshots(tmp_path: Path) -> None: source_dir = default_security_dir(Path(__file__)) for filename in ( @@ -219,3 +276,34 @@ def _copy_security_snapshots(tmp_path: Path) -> None: "github-target-missing-source-readiness.snapshot.json", ): shutil.copy(source_dir / filename, tmp_path / filename) + + +def _valid_owner_response_submission() -> dict[str, object]: + return { + "submission_mode": "read_only_markdown_response", + "responses": [ + { + "template_id": "target-awoooi-refs-blocked", + "github_repo": "owenhytsai/awoooi", + "owner_role_or_team": "platform-owner", + "decision": "hold_pending_refs_truth", + "decision_reason": "Need refs truth review before any sync action.", + "affected_scope": "awoooi github backup target", + "redacted_evidence_refs": [ + "docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md", + "docs/security/source-control-ref-detail-diff.snapshot.json", + ], + "evidence_refs": [ + "docs/security/source-control-workflow-secret-name-inventory.snapshot.json" + ], + "followup_owner": "platform-owner", + "rollback_owner": "platform-owner", + "maintenance_window": "not_authorized", + "validation_plan": "read-only refs truth review only", + "canonical_source": "gitea_main", + "github_target_disposition": "existing_private_candidate", + "visibility_review_owner": "platform-owner", + "refs_truth_review_owner": "platform-owner", + } + ], + } diff --git a/apps/api/tests/test_github_target_private_backup_evidence_gate_api.py b/apps/api/tests/test_github_target_private_backup_evidence_gate_api.py index 9fdeb536..a4e8e851 100644 --- a/apps/api/tests/test_github_target_private_backup_evidence_gate_api.py +++ b/apps/api/tests/test_github_target_private_backup_evidence_gate_api.py @@ -59,3 +59,60 @@ def test_github_target_private_backup_evidence_gate_endpoint_returns_read_only_g assert intake["not_approval"] is True assert data["targets"][0]["owner_response_execution_authorized"] is False assert "192.168.0." not in response.text + + +def test_github_target_owner_response_intake_preflight_endpoint_blocks_secrets(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.post( + "/api/v1/agents/github-target-owner-response-intake-preflight", + json={ + "submission_mode": "read_only_markdown_response", + "responses": [ + { + "template_id": "target-awoooi-refs-blocked", + "github_repo": "owenhytsai/awoooi", + "owner_role_or_team": "platform-owner", + "decision": "hold_pending_refs_truth", + "decision_reason": "Need refs truth review before sync.", + "affected_scope": "awoooi github backup target", + "redacted_evidence_refs": [ + "docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md" + ], + "evidence_refs": [ + "docs/security/source-control-ref-detail-diff.snapshot.json" + ], + "followup_owner": "platform-owner", + "rollback_owner": "platform-owner", + "maintenance_window": "not_authorized", + "validation_plan": "read-only refs truth review only", + "canonical_source": "gitea_main", + "github_target_disposition": "existing_private_candidate", + "visibility_review_owner": "platform-owner", + "refs_truth_review_owner": "platform-owner", + "private_clone_url_credential": ( + "https://owner:ghp_1234567890abcdefghijklmnopqrstu@github.com/owenhytsai/awoooi.git" + ), + } + ], + }, + ) + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "github_target_owner_response_intake_preflight_v1" + assert data["status"] == "blocked_owner_response_intake_preflight" + assert data["summary"]["preflight_passed_response_item_count"] == 0 + assert data["summary"]["preflight_blocked_response_item_count"] == 1 + assert data["summary"]["owner_response_received_count"] == 0 + assert data["summary"]["owner_response_accepted_count"] == 0 + assert data["summary"]["safe_credential_accepted_evidence_count"] == 0 + assert data["operation_boundaries"]["persist_submission_allowed"] is False + assert data["operation_boundaries"]["github_api_write_allowed"] is False + assert data["operation_boundaries"]["private_clone_url_collection_allowed"] is False + assert data["authorization_flags"]["owner_response_execution_authorized"] is False + assert data["responses"][0]["accepted_for_read_only_intake"] is False + assert "forbidden_payload_detected" in data["responses"][0]["blockers"] + assert "192.168.0." not in response.text