diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index c96835dac..fa7ff2252 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -1159,6 +1159,14 @@ const s49OwnerResponseIntakeBoundaries = [ 's4_9_owner_response_intake_accepted_count=0', 's4_9_owner_response_intake_rejected_count=0', 's4_9_owner_response_intake_runtime_gate_count=0', + 's4_9_owner_response_intake_source_bound=true', + 's4_9_owner_response_intake_handoff_queue_count=5', + 's4_9_owner_response_intake_handoff_queue_ready_count=0', + 's4_9_owner_response_intake_handoff_queue_received_count=0', + 's4_9_owner_response_intake_handoff_queue_accepted_count=0', + 's4_9_owner_response_intake_handoff_queue_runtime_gate_count=0', + 's4_9_owner_response_intake_handoff_queue_raw_payload_allowed=false', + 's4_9_owner_response_intake_handoff_queue_action_buttons_allowed=false', 's4_9_owner_response_intake_runtime_gate_opened=false', 's4_9_owner_response_intake_action_buttons_allowed=false', 'runtime_execution_authorized=false', diff --git a/docs/schemas/gitea_inventory_owner_attestation_response_v1.schema.json b/docs/schemas/gitea_inventory_owner_attestation_response_v1.schema.json index ec956952e..cddcbd859 100644 --- a/docs/schemas/gitea_inventory_owner_attestation_response_v1.schema.json +++ b/docs/schemas/gitea_inventory_owner_attestation_response_v1.schema.json @@ -15,6 +15,7 @@ "source_indexes", "summary", "response_templates", + "owner_response_intake_handoff_queue", "acceptance_checks", "rejection_rules", "allowed_outputs", @@ -67,6 +68,13 @@ "owner_response_display_section_count", "owner_response_request_packet_count", "owner_response_collection_check_count", + "owner_response_intake_handoff_queue_count", + "owner_response_intake_handoff_queue_ready_count", + "owner_response_intake_handoff_queue_received_count", + "owner_response_intake_handoff_queue_accepted_count", + "owner_response_intake_handoff_queue_runtime_gate_count", + "owner_response_intake_handoff_queue_raw_payload_allowed", + "owner_response_intake_handoff_queue_action_buttons_allowed", "intake_preflight_check_count", "intake_outcome_lane_count", "acceptance_check_count", @@ -94,6 +102,19 @@ "owner_response_display_section_count": {"type": "integer", "minimum": 0}, "owner_response_request_packet_count": {"type": "integer", "minimum": 0}, "owner_response_collection_check_count": {"type": "integer", "minimum": 0}, + "owner_response_intake_handoff_queue_count": {"type": "integer", "minimum": 0}, + "owner_response_intake_handoff_queue_ready_count": {"type": "integer", "minimum": 0}, + "owner_response_intake_handoff_queue_received_count": {"type": "integer", "minimum": 0}, + "owner_response_intake_handoff_queue_accepted_count": {"type": "integer", "minimum": 0}, + "owner_response_intake_handoff_queue_runtime_gate_count": {"type": "integer", "minimum": 0}, + "owner_response_intake_handoff_queue_raw_payload_allowed": { + "type": "boolean", + "const": false + }, + "owner_response_intake_handoff_queue_action_buttons_allowed": { + "type": "boolean", + "const": false + }, "intake_preflight_check_count": {"type": "integer", "minimum": 0}, "intake_outcome_lane_count": {"type": "integer", "minimum": 0}, "acceptance_check_count": {"type": "integer", "minimum": 0}, @@ -355,6 +376,55 @@ }, "minItems": 1 }, + "owner_response_intake_handoff_queue": { + "type": "array", + "description": "S4.9 owner response 收件前 handoff queue;只供 AwoooP / IwoooS 顯示下一個 owner 要補的脫敏中繼資料,不代表 request sent、received、accepted 或 runtime gate。", + "items": { + "type": "object", + "required": [ + "handoff_lane_id", + "display_order", + "source_template_id", + "template_label", + "handoff_status", + "required_owner_field_count", + "required_owner_fields", + "current_blocker", + "next_safe_step", + "received_response_count", + "accepted_response_count", + "runtime_gate_count", + "raw_payload_allowed", + "secret_plaintext_allowed", + "action_buttons_allowed", + "execution_authorized", + "not_approval", + "still_forbidden" + ], + "properties": { + "handoff_lane_id": {"type": "string"}, + "display_order": {"type": "integer", "minimum": 1}, + "source_template_id": {"type": "string"}, + "template_label": {"type": "string"}, + "handoff_status": {"type": "string", "enum": ["waiting_owner_response_metadata"]}, + "required_owner_field_count": {"type": "integer", "minimum": 1}, + "required_owner_fields": {"type": "array", "items": {"type": "string"}, "minItems": 1}, + "current_blocker": {"type": "string"}, + "next_safe_step": {"type": "string"}, + "received_response_count": {"type": "integer", "const": 0}, + "accepted_response_count": {"type": "integer", "const": 0}, + "runtime_gate_count": {"type": "integer", "const": 0}, + "raw_payload_allowed": {"type": "boolean", "const": false}, + "secret_plaintext_allowed": {"type": "boolean", "const": false}, + "action_buttons_allowed": {"type": "boolean", "const": false}, + "execution_authorized": {"type": "boolean", "const": false}, + "not_approval": {"type": "boolean", "const": true}, + "still_forbidden": {"type": "array", "items": {"type": "string"}, "minItems": 1} + }, + "additionalProperties": false + }, + "minItems": 1 + }, "owner_response_request_packet": { "type": "object", "description": "AwoooP 可直接顯示給 owner 的 S4.9 回覆請求;只說明要填什麼與不得貼什麼,不授權任何執行。", diff --git a/docs/schemas/iwooos_posture_projection_v1.schema.json b/docs/schemas/iwooos_posture_projection_v1.schema.json index 8c3303d41..a08999b0f 100644 --- a/docs/schemas/iwooos_posture_projection_v1.schema.json +++ b/docs/schemas/iwooos_posture_projection_v1.schema.json @@ -182,7 +182,15 @@ "action_buttons_allowed", "owner_response_next_action_focus_item_count", "s4_9_owner_response_preflight_check_count", - "s4_9_owner_response_request_template_count" + "s4_9_owner_response_request_template_count", + "s4_9_owner_response_intake_source_bound", + "s4_9_owner_response_intake_handoff_queue_count", + "s4_9_owner_response_intake_handoff_queue_ready_count", + "s4_9_owner_response_intake_handoff_queue_received_count", + "s4_9_owner_response_intake_handoff_queue_accepted_count", + "s4_9_owner_response_intake_handoff_queue_runtime_gate_count", + "s4_9_owner_response_intake_handoff_queue_raw_payload_allowed", + "s4_9_owner_response_intake_handoff_queue_action_buttons_allowed" ], "properties": { "route_path": { @@ -499,6 +507,38 @@ "s4_9_owner_response_request_template_count": { "type": "integer", "const": 5 + }, + "s4_9_owner_response_intake_source_bound": { + "type": "boolean", + "const": true + }, + "s4_9_owner_response_intake_handoff_queue_count": { + "type": "integer", + "const": 5 + }, + "s4_9_owner_response_intake_handoff_queue_ready_count": { + "type": "integer", + "const": 0 + }, + "s4_9_owner_response_intake_handoff_queue_received_count": { + "type": "integer", + "const": 0 + }, + "s4_9_owner_response_intake_handoff_queue_accepted_count": { + "type": "integer", + "const": 0 + }, + "s4_9_owner_response_intake_handoff_queue_runtime_gate_count": { + "type": "integer", + "const": 0 + }, + "s4_9_owner_response_intake_handoff_queue_raw_payload_allowed": { + "type": "boolean", + "const": false + }, + "s4_9_owner_response_intake_handoff_queue_action_buttons_allowed": { + "type": "boolean", + "const": false } }, "additionalProperties": false diff --git a/docs/security/gitea-inventory-owner-attestation-response.snapshot.json b/docs/security/gitea-inventory-owner-attestation-response.snapshot.json index 3b96ca0b0..70db09b43 100644 --- a/docs/security/gitea-inventory-owner-attestation-response.snapshot.json +++ b/docs/security/gitea-inventory-owner-attestation-response.snapshot.json @@ -28,6 +28,13 @@ "owner_response_display_section_count": 8, "owner_response_request_packet_count": 1, "owner_response_collection_check_count": 6, + "owner_response_intake_handoff_queue_count": 5, + "owner_response_intake_handoff_queue_ready_count": 0, + "owner_response_intake_handoff_queue_received_count": 0, + "owner_response_intake_handoff_queue_accepted_count": 0, + "owner_response_intake_handoff_queue_runtime_gate_count": 0, + "owner_response_intake_handoff_queue_raw_payload_allowed": false, + "owner_response_intake_handoff_queue_action_buttons_allowed": false, "intake_preflight_check_count": 6, "intake_outcome_lane_count": 5, "acceptance_check_count": 8, @@ -363,6 +370,181 @@ ] } ], + "owner_response_intake_handoff_queue": [ + { + "handoff_lane_id": "publicGap", + "display_order": 1, + "source_template_id": "response-public-only-vs-local-gitea-gap", + "template_label": "D1", + "handoff_status": "waiting_owner_response_metadata", + "required_owner_field_count": 6, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_repos", + "evidence_refs", + "followup_owner" + ], + "current_blocker": "缺少 public-only / local gap 的逐 repo owner 判定。", + "next_safe_step": "只等待負責人提供脫敏 metadata pointer;不得收 raw response 或啟動 repo / refs 動作。", + "received_response_count": 0, + "accepted_response_count": 0, + "runtime_gate_count": 0, + "raw_payload_allowed": false, + "secret_plaintext_allowed": false, + "action_buttons_allowed": false, + "execution_authorized": false, + "not_approval": true, + "still_forbidden": [ + "mark_received_from_handoff_queue", + "mark_accepted_from_handoff_queue", + "store_owner_response_raw_body", + "create_github_repo", + "sync_refs", + "switch_github_primary" + ] + }, + { + "handoff_lane_id": "namespaceIdentity", + "display_order": 2, + "source_template_id": "response-org-user-endpoint-identity", + "template_label": "D2", + "handoff_status": "waiting_owner_response_metadata", + "required_owner_field_count": 6, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "canonical_namespace", + "evidence_refs", + "followup_owner" + ], + "current_blocker": "缺少 Gitea `wooo` user / org canonical namespace 判定。", + "next_safe_step": "只等待負責人提供 canonical namespace 與脫敏 evidence ref;不得使用 write/admin token。", + "received_response_count": 0, + "accepted_response_count": 0, + "runtime_gate_count": 0, + "raw_payload_allowed": false, + "secret_plaintext_allowed": false, + "action_buttons_allowed": false, + "execution_authorized": false, + "not_approval": true, + "still_forbidden": [ + "mark_received_from_handoff_queue", + "mark_accepted_from_handoff_queue", + "store_owner_response_raw_body", + "store_token_value", + "write_gitea_repo", + "switch_github_primary" + ] + }, + { + "handoff_lane_id": "adjacentScope", + "display_order": 3, + "source_template_id": "response-internal-110-adjacent-scope", + "template_label": "D3", + "handoff_status": "waiting_owner_response_metadata", + "required_owner_field_count": 6, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_sources", + "evidence_refs", + "followup_owner" + ], + "current_blocker": "缺少 110 相鄰來源逐項 scope 判定。", + "next_safe_step": "只等待 owner 標示來源是否 in-scope、legacy、external 或需補證;不得 fetch、push 或刪 refs。", + "received_response_count": 0, + "accepted_response_count": 0, + "runtime_gate_count": 0, + "raw_payload_allowed": false, + "secret_plaintext_allowed": false, + "action_buttons_allowed": false, + "execution_authorized": false, + "not_approval": true, + "still_forbidden": [ + "mark_received_from_handoff_queue", + "mark_accepted_from_handoff_queue", + "store_owner_response_raw_body", + "fetch_refs", + "delete_refs", + "switch_github_primary" + ] + }, + { + "handoff_lane_id": "canonicalOwner", + "display_order": 4, + "source_template_id": "response-repo-owner-canonical-scope", + "template_label": "D4", + "handoff_status": "waiting_owner_response_metadata", + "required_owner_field_count": 8, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "repo_owner", + "canonical_source", + "github_target_candidate", + "visibility_review_owner", + "evidence_refs" + ], + "current_blocker": "缺少 repo owner、canonical source、GitHub target candidate 與 visibility review owner。", + "next_safe_step": "只等待負責人補齊 owner / canonical / visibility metadata;不得建立 GitHub repo 或改 visibility。", + "received_response_count": 0, + "accepted_response_count": 0, + "runtime_gate_count": 0, + "raw_payload_allowed": false, + "secret_plaintext_allowed": false, + "action_buttons_allowed": false, + "execution_authorized": false, + "not_approval": true, + "still_forbidden": [ + "mark_received_from_handoff_queue", + "mark_accepted_from_handoff_queue", + "store_owner_response_raw_body", + "create_github_repo", + "change_repo_visibility", + "switch_github_primary" + ] + }, + { + "handoff_lane_id": "legacyDisposition", + "display_order": 5, + "source_template_id": "response-legacy-or-inaccessible-disposition", + "template_label": "D5", + "handoff_status": "waiting_owner_response_metadata", + "required_owner_field_count": 7, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_repos_or_sources", + "disposition", + "evidence_refs", + "followup_owner" + ], + "current_blocker": "缺少 legacy、inaccessible 或 external repo disposition 與後續 owner。", + "next_safe_step": "只等待負責人補 disposition metadata;不得 archive、delete 或停用 Gitea repo。", + "received_response_count": 0, + "accepted_response_count": 0, + "runtime_gate_count": 0, + "raw_payload_allowed": false, + "secret_plaintext_allowed": false, + "action_buttons_allowed": false, + "execution_authorized": false, + "not_approval": true, + "still_forbidden": [ + "mark_received_from_handoff_queue", + "mark_accepted_from_handoff_queue", + "store_owner_response_raw_body", + "archive_repo", + "delete_repo", + "disable_gitea_repo" + ] + } + ], "owner_response_audit_event_templates": [ { "event_template_id": "audit-owner-response-request-shown", diff --git a/docs/security/iwooos-posture-projection.snapshot.json b/docs/security/iwooos-posture-projection.snapshot.json index b1ff9b537..46f19ee7f 100644 --- a/docs/security/iwooos-posture-projection.snapshot.json +++ b/docs/security/iwooos-posture-projection.snapshot.json @@ -9,6 +9,8 @@ "docs/security/security-mirror-status-rollup.snapshot.json", "docs/security/security-rollout-policy.snapshot.json", "docs/security/source-control-owner-response-validation-rollup.snapshot.json", + "docs/security/gitea-inventory-owner-attestation-response.snapshot.json", + "docs/schemas/gitea_inventory_owner_attestation_response_v1.schema.json", "docs/security/source-control-primary-readiness-gate.snapshot.json", "docs/security/GITEA-INVENTORY-OWNER-ATTESTATION-REQUEST-DRAFT.md", "docs/security/gitea-inventory-owner-attestation-request-draft.snapshot.json", @@ -111,6 +113,14 @@ "s4_9_owner_response_intake_accepted_count": 0, "s4_9_owner_response_intake_rejected_count": 0, "s4_9_owner_response_intake_runtime_gate_count": 0, + "s4_9_owner_response_intake_source_bound": true, + "s4_9_owner_response_intake_handoff_queue_count": 5, + "s4_9_owner_response_intake_handoff_queue_ready_count": 0, + "s4_9_owner_response_intake_handoff_queue_received_count": 0, + "s4_9_owner_response_intake_handoff_queue_accepted_count": 0, + "s4_9_owner_response_intake_handoff_queue_runtime_gate_count": 0, + "s4_9_owner_response_intake_handoff_queue_raw_payload_allowed": false, + "s4_9_owner_response_intake_handoff_queue_action_buttons_allowed": false, "visual_command_dashboard_widget_count": 14, "visual_command_dashboard_first_layer": true, "visual_command_dashboard_default_visible": false, diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 6960135c9..2222ab7e9 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -1928,6 +1928,34 @@ def validate(root: Path) -> None: iwooos_projection["summary"][count_key], 0, ) + assert_true( + "iwooos_projection.summary.s4_9_owner_response_intake_source_bound", + iwooos_projection["summary"]["s4_9_owner_response_intake_source_bound"], + ) + assert_equal( + "iwooos_projection.summary.s4_9_owner_response_intake_handoff_queue_count", + iwooos_projection["summary"]["s4_9_owner_response_intake_handoff_queue_count"], + 5, + ) + for count_key in [ + "s4_9_owner_response_intake_handoff_queue_ready_count", + "s4_9_owner_response_intake_handoff_queue_received_count", + "s4_9_owner_response_intake_handoff_queue_accepted_count", + "s4_9_owner_response_intake_handoff_queue_runtime_gate_count", + ]: + assert_equal( + f"iwooos_projection.summary.{count_key}", + iwooos_projection["summary"][count_key], + 0, + ) + assert_false( + "iwooos_projection.summary.s4_9_owner_response_intake_handoff_queue_raw_payload_allowed", + iwooos_projection["summary"]["s4_9_owner_response_intake_handoff_queue_raw_payload_allowed"], + ) + assert_false( + "iwooos_projection.summary.s4_9_owner_response_intake_handoff_queue_action_buttons_allowed", + iwooos_projection["summary"]["s4_9_owner_response_intake_handoff_queue_action_buttons_allowed"], + ) assert_equal( "iwooos_projection.summary.all_product_coverage_snapshot_default_summary_mode", iwooos_projection["summary"]["all_product_coverage_snapshot_default_summary_mode"], @@ -14485,6 +14513,14 @@ def validate(root: Path) -> None: "s4_9_owner_response_intake_accepted_count=0", "s4_9_owner_response_intake_rejected_count=0", "s4_9_owner_response_intake_runtime_gate_count=0", + "s4_9_owner_response_intake_source_bound=true", + "s4_9_owner_response_intake_handoff_queue_count=5", + "s4_9_owner_response_intake_handoff_queue_ready_count=0", + "s4_9_owner_response_intake_handoff_queue_received_count=0", + "s4_9_owner_response_intake_handoff_queue_accepted_count=0", + "s4_9_owner_response_intake_handoff_queue_runtime_gate_count=0", + "s4_9_owner_response_intake_handoff_queue_raw_payload_allowed=false", + "s4_9_owner_response_intake_handoff_queue_action_buttons_allowed=false", "s4_9_owner_response_intake_runtime_gate_opened=false", "s4_9_owner_response_intake_action_buttons_allowed=false", "runtime_execution_authorized=false", diff --git a/scripts/security/source-control-owner-response-guard.py b/scripts/security/source-control-owner-response-guard.py index 9e0fb96c1..d954b3a7d 100755 --- a/scripts/security/source-control-owner-response-guard.py +++ b/scripts/security/source-control-owner-response-guard.py @@ -86,6 +86,21 @@ LANES = [ "display-preflight-and-outcome-lanes", "display-acceptance-and-rejection-rules", ], + "expected_handoff_queue_ids": [ + "publicGap", + "namespaceIdentity", + "adjacentScope", + "canonicalOwner", + "legacyDisposition", + ], + "expected_handoff_queue_template_ids": [ + "response-public-only-vs-local-gitea-gap", + "response-org-user-endpoint-identity", + "response-internal-110-adjacent-scope", + "response-repo-owner-canonical-scope", + "response-legacy-or-inaccessible-disposition", + ], + "expected_handoff_queue_required_field_counts": [6, 6, 6, 8, 7], }, { "lane_id": "s4_10_github_target_owner_decision_response", @@ -742,6 +757,95 @@ def validate(root: Path) -> None: item["execution_authorized"], ) assert_true(f"{lane['lane_id']}.{item['section_id']}.not_approval", item["not_approval"]) + expected_handoff_queue_ids = lane.get("expected_handoff_queue_ids") + if expected_handoff_queue_ids is not None: + handoff_queue = snapshot["owner_response_intake_handoff_queue"] + expected_handoff_queue_template_ids = lane["expected_handoff_queue_template_ids"] + expected_handoff_queue_required_field_counts = lane["expected_handoff_queue_required_field_counts"] + assert_equal( + f"{lane['lane_id']}.owner_response_intake_handoff_queue_count", + summary["owner_response_intake_handoff_queue_count"], + len(expected_handoff_queue_ids), + ) + for count_key in [ + "owner_response_intake_handoff_queue_ready_count", + "owner_response_intake_handoff_queue_received_count", + "owner_response_intake_handoff_queue_accepted_count", + "owner_response_intake_handoff_queue_runtime_gate_count", + ]: + assert_equal(f"{lane['lane_id']}.{count_key}", summary[count_key], 0) + assert_false( + f"{lane['lane_id']}.owner_response_intake_handoff_queue_raw_payload_allowed", + summary["owner_response_intake_handoff_queue_raw_payload_allowed"], + ) + assert_false( + f"{lane['lane_id']}.owner_response_intake_handoff_queue_action_buttons_allowed", + summary["owner_response_intake_handoff_queue_action_buttons_allowed"], + ) + assert_equal( + f"{lane['lane_id']}.owner_response_intake_handoff_queue_ids", + [item["handoff_lane_id"] for item in handoff_queue], + expected_handoff_queue_ids, + ) + assert_equal( + f"{lane['lane_id']}.owner_response_intake_handoff_queue_display_order", + [item["display_order"] for item in handoff_queue], + list(range(1, len(expected_handoff_queue_ids) + 1)), + ) + assert_equal( + f"{lane['lane_id']}.owner_response_intake_handoff_queue_source_template_ids", + [item["source_template_id"] for item in handoff_queue], + expected_handoff_queue_template_ids, + ) + assert_equal( + f"{lane['lane_id']}.owner_response_intake_handoff_queue_template_labels", + [item["template_label"] for item in handoff_queue], + [f"D{index}" for index in range(1, len(expected_handoff_queue_ids) + 1)], + ) + assert_equal( + f"{lane['lane_id']}.owner_response_intake_handoff_queue_required_field_counts", + [item["required_owner_field_count"] for item in handoff_queue], + expected_handoff_queue_required_field_counts, + ) + for item in handoff_queue: + assert_equal( + f"{lane['lane_id']}.{item['handoff_lane_id']}.handoff_status", + item["handoff_status"], + "waiting_owner_response_metadata", + ) + for count_key in [ + "received_response_count", + "accepted_response_count", + "runtime_gate_count", + ]: + assert_equal( + f"{lane['lane_id']}.{item['handoff_lane_id']}.{count_key}", + item[count_key], + 0, + ) + for false_key in [ + "raw_payload_allowed", + "secret_plaintext_allowed", + "action_buttons_allowed", + "execution_authorized", + ]: + assert_false( + f"{lane['lane_id']}.{item['handoff_lane_id']}.{false_key}", + item[false_key], + ) + assert_true( + f"{lane['lane_id']}.{item['handoff_lane_id']}.not_approval", + item["not_approval"], + ) + for forbidden_action in [ + "mark_received_from_handoff_queue", + "mark_accepted_from_handoff_queue", + "store_owner_response_raw_body", + ]: + assert_true( + f"{lane['lane_id']}.{item['handoff_lane_id']}.still_forbidden.{forbidden_action}", + forbidden_action in item["still_forbidden"], + ) expected_request_packet_id = lane.get("expected_request_packet_id") if expected_request_packet_id is not None: request_packet = snapshot["owner_response_request_packet"]