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

This commit is contained in:
Your Name
2026-06-27 21:44:54 +08:00
parent 1a6f8f4275
commit a68d9e40a7
4 changed files with 546 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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