feat(github): preflight owner response intake
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user