diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index beb7ee3f..32996975 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,19 @@ +## 2026-05-18 | 資安供應鏈 S4.13:Owner Response Collection Order + +**背景**:S4.13 已能顯示 4 條 missing response lanes;本輪補上建議收件順序,讓 AwoooP 可以把下一個 owner response 工作排清楚,但仍不把收件順序變成 approval queue 或 execution queue。 + +**完成**: +- `source_control_owner_response_validation_rollup_v1` schema 新增 optional `owner_response_collection_order`。 +- `source-control-owner-response-validation-rollup.snapshot.json` 新增 4 步收件順序:S4.9 Gitea scope / canonical owner、S4.10 GitHub target owner / visibility / canonical、S4.11 refs truth、S4.12 workflow / secret name parity。 +- `source-control-owner-response-guard.py` 反查 collection order 與 validation lanes / missing lanes 必須一致,且每一步 `execution_authorized=false`。 +- 更新 S4.13 人讀文件、AwoooP checklist、status rollup、progress 與 handoff。 + +**仍禁止**: +- 不把 collection order 當成 approval queue 或 execution queue。 +- 不建立 GitHub repo、不修改 visibility、不寫 Gitea、不 sync/delete refs、不 force push、不切 GitHub primary。 +- 不修改 workflow / webhook / runner / deploy key / branch protection / CODEOWNERS / repository secret。 +- 不收 token value、secret value、private key、cookie、session 或未脫敏 payload。 + ## 2026-05-18 | 資安供應鏈 S4.13:Owner Response Missing Lanes **背景**:Owner response guard 已能確認四包 response 仍未收到;本輪補上 AwoooP 可直接顯示的 `missing_response_lanes`,讓 Operator Console 能把下一步缺口講清楚,而不是只顯示 guard pass。 diff --git a/docs/schemas/source_control_owner_response_validation_rollup_v1.schema.json b/docs/schemas/source_control_owner_response_validation_rollup_v1.schema.json index a6044c11..6c8025e5 100644 --- a/docs/schemas/source_control_owner_response_validation_rollup_v1.schema.json +++ b/docs/schemas/source_control_owner_response_validation_rollup_v1.schema.json @@ -247,6 +247,45 @@ }, "minItems": 1 }, + "owner_response_collection_order": { + "type": "array", + "description": "AwoooP 可顯示的 owner response 收件順序;此欄位只排序 review 工作,不授權任何 repo / refs / workflow / secret / runtime 動作。", + "items": { + "type": "object", + "required": [ + "order", + "lane_id", + "reason", + "required_packet", + "minimum_response", + "awooop_action", + "blocked_until_received", + "execution_authorized", + "still_forbidden" + ], + "properties": { + "order": {"type": "integer", "minimum": 1}, + "lane_id": {"type": "string"}, + "reason": {"type": "string"}, + "required_packet": {"type": "string"}, + "minimum_response": { + "type": "array", + "items": {"type": "string"}, + "minItems": 1 + }, + "awooop_action": {"type": "string", "enum": ["display_next_collection_item"]}, + "blocked_until_received": {"type": "boolean", "const": true}, + "execution_authorized": {"type": "boolean", "const": false}, + "still_forbidden": { + "type": "array", + "items": {"type": "string"}, + "minItems": 1 + } + }, + "additionalProperties": false + }, + "minItems": 1 + }, "allowed_outputs": { "type": "array", "items": {"type": "string"}, diff --git a/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md b/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md index 88d88a77..a7e6870b 100644 --- a/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md +++ b/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md @@ -42,7 +42,7 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | `security_mirror_quarantine_v1` | AwoooP 鏡像隔離契約 | Operator Console、Audit | mirror-only | 只隔離驗收失敗 payload、顯示 recovery request 與 retry gate;不作 runtime blocker | | `security_mirror_dry_run_v1` | AwoooP 鏡像 dry-run 報告契約 | Operator Console、Audit | mirror-only | 只回報接入演練結果,且必須包含 progress guard、owner response guard 與 latest local validation;不得轉成 production ingestion | | `security_mirror_status_rollup_v1` | AwoooP 鏡像狀態彙整契約 | Operator Console、Runtime State、Audit | mirror-only | 只顯示階段狀態、58% 進度估算、下一個 gate 與禁止事項;不得視為 runtime authorization | -| `source_control_owner_response_validation_rollup_v1` | S4.9 / S4.10 / S4.11 / S4.12 owner response validation rollup | Operator Console、Source-control review、Audit | mirror-only | 只顯示四包 response packets、22 個 templates、missing response lanes、10 個 cross-packet checks、quarantine rules 與 latest local validation;不得視為 approval 或 runtime gate | +| `source_control_owner_response_validation_rollup_v1` | S4.9 / S4.10 / S4.11 / S4.12 owner response validation rollup | Operator Console、Source-control review、Audit | mirror-only | 只顯示四包 response packets、22 個 templates、missing response lanes、owner response collection order、10 個 cross-packet checks、quarantine rules 與 latest local validation;不得視為 approval 或 runtime gate | | `coding_task_v1` | Code Review / Codex Security / manual review | Approval candidate、Channel Event、Audit | suggest-only | 不自動開 patch runner、不自動 merge | | `source_control_migration_event_v1` | Gitea/GitHub branch/tag/SHA diff | Supply-chain evidence、Approval candidate | mirror-only | 不觸發 deploy、不切換 primary | | `gitea_repo_inventory_v1` | Gitea org/user repo list 或管理匯出 | Supply-chain evidence、migration matrix | mirror-only | 顯示 public-only evidence、S4.5 authenticated/admin export request、S4.6 redacted import acceptance、S4.7 owner coverage attestation 與 S4.9 owner response 收件包;不保存 token value、不刪除或停用 Gitea repo | @@ -112,7 +112,7 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | `security_mirror_quarantine_v1.status=draft` | `observe` | 顯示 5 個 quarantine lanes、recovery request 與 retry gate;不得自動重試失敗 payload | | `security_mirror_dry_run_v1.dry_run_status=contract_defined_not_executed` | `observe` | 顯示 8 個 dry-run steps 與 `latest_local_validation.status=repo_snapshot_guard_pass`;`CHECK_PROGRESS_GUARD` 必須維持 58% 不是執行授權,`CHECK_OWNER_RESPONSE_GUARD` 必須維持 owner response received / accepted 皆為 0,不得視為 production ingestion 已啟用 | | `security_mirror_status_rollup_v1.rollup_status=framework_ready_waiting_approval` | `observe` | 顯示 S0-S4 階段、58% 進度估算、approval queue summary 與下一個 gate;不得新增 execution action | -| `source_control_owner_response_validation_rollup_v1.status=draft_waiting_owner_responses` | `observe` | 顯示四包 owner response packets、4 條 missing response lanes、22 個 templates、received / accepted / rejected 皆為 0,且 `latest_local_validation.result=SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`;不得當成 approval 或 execution authorization | +| `source_control_owner_response_validation_rollup_v1.status=draft_waiting_owner_responses` | `observe` | 顯示四包 owner response packets、4 條 missing response lanes、4 步收件順序、22 個 templates、received / accepted / rejected 皆為 0,且 `latest_local_validation.result=SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`;不得當成 approval 或 execution authorization | | `coding_task_v1.risk=LOW|MEDIUM` | `warn` | 可排入 Codex patch-only backlog | | `coding_task_v1.risk=HIGH|CRITICAL` | `approve_required` | 必須指定 `critic`、`vuln-verifier` | | `source_control_migration_event_v1.status=blocked` | `observe` | 顯示 blocking reason,不允許切 primary | diff --git a/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md b/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md index 840506f1..bf1385de 100644 --- a/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md +++ b/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md @@ -54,6 +54,8 @@ python3 scripts/security/source-control-owner-response-guard.py AwoooP 顯示 S4.13 時,應把 `missing_response_lanes` 當成 Operator Console 的主要缺口摘要:4 條 lane、22 個 response templates、目前 received / accepted 皆為 0。這只是在告訴 reviewer 下一步要補哪些 owner response,不代表可以建立 repo、sync refs、修改 workflow / secret、啟用 runner或切 GitHub primary。 +建議顯示 `owner_response_collection_order` 作為下一步收件順序:先 S4.9 Gitea scope / canonical owner,再 S4.10 GitHub target owner / visibility / canonical,再 S4.11 refs truth,最後 S4.12 workflow / secret name parity。這只是 review 順序,不是 approval queue 或 execution queue。 + ## 1. Session 分工 ### AwoooP 主線 Session diff --git a/docs/security/SECURITY-MIRROR-STATUS-ROLLUP.md b/docs/security/SECURITY-MIRROR-STATUS-ROLLUP.md index e555c4a5..fd895b59 100644 --- a/docs/security/SECURITY-MIRROR-STATUS-ROLLUP.md +++ b/docs/security/SECURITY-MIRROR-STATUS-ROLLUP.md @@ -32,7 +32,7 @@ | GitHub primary rollback ADR | S4.4 已建立;7 個 in-scope rollback drafts、0 個 owner approved、0 個 dry-run completed、0 個 active cutover | | Gitea inventory | S4.5 已補認證清冊匯出請求;S4.6 已補匯入驗收契約;S4.7 已補 owner coverage attestation;S4.8 已把既有 Gitea queue/gate/review packet/follow-up gate 對齊 attestation 先行;S4.9 已補 owner response 收件包;目前 status=`partial_waiting_authenticated_inventory`、未認證公開範圍 repos 2 個、本機可見 Gitea unique repos 4 個、匯出來源選項 2 類、匯入驗收 payload 0 筆、owner attestation items 5 個、收到 attestation 0 筆、owner response 0 筆、敏感 payload 必須隔離、允許收集 token value=false | | Workflow / secret name inventory | S4.1 已建立;S4.2 補 4 個 repos、31 個 workflow files、43 個 referenced secret names 的 local evidence;S4.3 補 7 個 repos、5 類 lanes 的 redacted export request;S4.12 補 5 個 owner response templates;0 個 inventory complete、禁止收集 secret value、禁止 write token | -| Owner response validation | S4.13 已建立;四包 owner response 目前 received/accepted 皆為 0;4 條 missing response lanes 可供 AwoooP 直接顯示;latest local validation 為 `SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`,不代表 owner response 已收到或任何執行授權 | +| Owner response validation | S4.13 已建立;四包 owner response 目前 received/accepted 皆為 0;4 條 missing response lanes 與 4 步 collection order 可供 AwoooP 直接顯示;latest local validation 為 `SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`,不代表 owner response 已收到或任何執行授權 | | Dry-run | `contract_defined_not_executed`;已納入 `CHECK_PROGRESS_GUARD` 與 `CHECK_OWNER_RESPONSE_GUARD`,latest local validation 為 `repo_snapshot_guard_pass`,仍不代表 production ingestion | | Runtime actions | `false` | | Payload ingestion | `false` | diff --git a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md index 9bbffe7c..ea8694f9 100644 --- a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md +++ b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md @@ -59,7 +59,7 @@ python3 scripts/security/security-mirror-progress-guard.py | S4.2 Workflow / Secret 名稱 local evidence | 完成草案 | 已建立 local read-only collector 與 snapshot;7 個 local repos visible、4 個 local evidence repos、31 個 workflow files、43 個 referenced secret names、secret value detected=false | 補 webhook / deploy key / branch protection / repository secret parity 的 redacted evidence;仍不可切 primary | | S4.3 Workflow / Secret 名稱 redacted export request | 完成草案 | 已建立 export request schema / snapshot / 人讀版;7 個 in-scope repos、5 類 export lanes:webhook、runner、deploy key、branch protection / CODEOWNERS、repository secret name parity;write token allowed=false | repo owner 或未來只讀 API 依 request 補 redacted export;仍不可收 secret value、不可修改 GitHub/Gitea | | S4.12 Workflow / Secret Name Owner Response 收件包 | 完成草案 | 已建立 owner response schema / snapshot / 人讀版;5 個 response templates、8 個 acceptance checks、10 個 rejection rules、candidate repos 8、in-scope repos 7、received response 0、accepted 0、execution authorized=false | owner 依模板回覆 webhook、runner、deploy key、branch protection / CODEOWNERS、repository secret name parity;response 通過只更新 read-only inventory / export request / readiness wording,不代表收 secret value、改 workflow、啟用 runner 或 primary approval | -| S4.13 Source Control Owner Response Validation Rollup | 完成草案 | 已建立 validation rollup schema / snapshot / 人讀版;彙整 S4.9 / S4.10 / S4.11 / S4.12 四包 response packets、4 條 missing response lanes、22 個 response templates、10 個 cross-packet checks、40 個 rejection rules、received / accepted / rejected response 皆為 0、execution authorized=false;latest local validation 為 `SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK` | AwoooP 可顯示四包 owner response 驗收總覽、缺口摘要與 quarantine rules;rollup 不代表 approval、runtime gate、repo / refs / workflow / secret / runner 執行授權或 primary approval | +| S4.13 Source Control Owner Response Validation Rollup | 完成草案 | 已建立 validation rollup schema / snapshot / 人讀版;彙整 S4.9 / S4.10 / S4.11 / S4.12 四包 response packets、4 條 missing response lanes、4 步 owner response collection order、22 個 response templates、10 個 cross-packet checks、40 個 rejection rules、received / accepted / rejected response 皆為 0、execution authorized=false;latest local validation 為 `SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK` | AwoooP 可顯示四包 owner response 驗收總覽、缺口摘要、建議收件順序與 quarantine rules;rollup 不代表 approval、runtime gate、repo / refs / workflow / secret / runner 執行授權或 primary approval | | S4.4 GitHub Primary rollback ADR | 完成草案 | 已建立 rollback ADR schema / snapshot / 人讀版;7 個 in-scope rollback drafts、0 owner approved、0 dry-run completed、0 active cutover | repo owner 審查 rollback owner、validation window 與 triggers;仍不可切 primary 或執行 rollback | | S4.5 Gitea 認證清冊匯出請求 | 完成草案 | 已建立匯出請求 schema / snapshot / 人讀版;目前未認證公開範圍 repo 2 個、本機可見 Gitea unique repo 4 個、覆蓋缺口 2 個、匯出來源選項 2 類;允許收集 token value=false | repo owner 依只讀 token API 或已脫敏管理匯出補私有 / 內部全量 repo list;仍不可保存 token、不可 write Gitea、不可 refs sync | | S4.6 Gitea 認證清冊匯入驗收契約 | 完成草案 | 已建立匯入驗收 schema / snapshot / 人讀版;目前 received payload 0、accepted 0、rejected 0;定義 10 個驗收檢查、10 個拒收規則與 4 個 quarantine lanes | owner 提供脫敏 payload 後先驗收 / 拒收 / 隔離;仍不可把驗收當 primary approval | diff --git a/docs/security/SOURCE-CONTROL-OWNER-RESPONSE-VALIDATION-ROLLUP.md b/docs/security/SOURCE-CONTROL-OWNER-RESPONSE-VALIDATION-ROLLUP.md index 7edf78a8..148fbcb4 100644 --- a/docs/security/SOURCE-CONTROL-OWNER-RESPONSE-VALIDATION-ROLLUP.md +++ b/docs/security/SOURCE-CONTROL-OWNER-RESPONSE-VALIDATION-ROLLUP.md @@ -69,6 +69,17 @@ S4.13 不新增第 36 個主 contract,不新增 approval item,不啟用 runt | S4.11 refs truth | 5 個 response templates 尚未收到 | Owner 回覆 refs truth、deprecated drift、release tags 與 GitHub-only refs disposition | 不 fetch / push / delete refs、不 force push、不切 primary | | S4.12 workflow / secret name | 5 個 response templates 尚未收到 | Owner 回覆 webhook、runner、deploy key、branch protection / CODEOWNERS、secret name parity 的脫敏狀態 | 不收 secret value、不改 workflow、不啟用 runner、不切 primary | +## 2.2 建議收件順序 + +| 順序 | Lane | 為什麼先後這樣排 | +|------|------|------------------| +| 1 | S4.9 Gitea owner attestation | 先確認 Gitea 覆蓋範圍與 canonical owner,避免後續 GitHub target / refs 判定建立在不完整 inventory 上 | +| 2 | S4.10 GitHub target decision | 再確認 GitHub target owner / visibility / canonical,避免 `not_found_or_private` 被誤解成可直接建立 repo | +| 3 | S4.11 refs truth | GitHub target owner / visibility 明確後,再判定 branch / tag 真相來源,避免 refs sync 或 delete 被提前誤用 | +| 4 | S4.12 workflow / secret name | 最後補 workflow / webhook / runner / deploy key / branch protection / CODEOWNERS / secret 名稱 parity,避免 secret 或 runner 變更早於 source truth | + +這個順序只讓 AwoooP 顯示下一個建議收件項目,不是 approval queue、不是 execution queue,也不授權任何 repo、refs、workflow、secret、runner 或 primary 動作。 + ## 3. Cross-Packet 驗收規則 1. 四個 source response packets 都必須可解析,且 summary 欄位存在。 diff --git a/docs/security/source-control-owner-response-validation-rollup.snapshot.json b/docs/security/source-control-owner-response-validation-rollup.snapshot.json index 2b42845f..fafceb88 100644 --- a/docs/security/source-control-owner-response-validation-rollup.snapshot.json +++ b/docs/security/source-control-owner-response-validation-rollup.snapshot.json @@ -376,6 +376,96 @@ ] } ], + "owner_response_collection_order": [ + { + "order": 1, + "lane_id": "s4_9_gitea_inventory_owner_attestation_response", + "reason": "先確認 Gitea 覆蓋範圍與 canonical owner,避免後續 GitHub target / refs 判定建立在不完整 inventory 上。", + "required_packet": "docs/security/GITEA-INVENTORY-OWNER-ATTESTATION-RESPONSE.md", + "minimum_response": [ + "public-only/local gap disposition", + "org/user endpoint disposition", + "110 adjacent source scope", + "canonical owner", + "legacy/inaccessible disposition" + ], + "awooop_action": "display_next_collection_item", + "blocked_until_received": true, + "execution_authorized": false, + "still_forbidden": [ + "store_token_value", + "write_gitea_repo", + "sync_refs", + "switch_github_primary" + ] + }, + { + "order": 2, + "lane_id": "s4_10_github_target_owner_decision_response", + "reason": "再確認 GitHub target owner / visibility / canonical,避免 not_found_or_private 被誤解成可直接建立 repo。", + "required_packet": "docs/security/GITHUB-TARGET-OWNER-DECISION-RESPONSE.md", + "minimum_response": [ + "repo owner", + "target visibility", + "canonical target disposition", + "not_found_or_private handling" + ], + "awooop_action": "display_next_collection_item", + "blocked_until_received": true, + "execution_authorized": false, + "still_forbidden": [ + "create_github_repo", + "change_repo_visibility", + "sync_refs", + "switch_github_primary" + ] + }, + { + "order": 3, + "lane_id": "s4_11_ref_truth_owner_response", + "reason": "GitHub target owner / visibility 明確後,再判定 branch / tag 真相來源,避免 refs sync 或 delete 被提前誤用。", + "required_packet": "docs/security/SOURCE-CONTROL-REF-TRUTH-OWNER-RESPONSE.md", + "minimum_response": [ + "main/dev truth disposition", + "deprecated drift disposition", + "release tag retention", + "GitHub-only refs disposition" + ], + "awooop_action": "display_next_collection_item", + "blocked_until_received": true, + "execution_authorized": false, + "still_forbidden": [ + "fetch_refs", + "push_refs", + "delete_refs", + "force_push", + "switch_github_primary" + ] + }, + { + "order": 4, + "lane_id": "s4_12_workflow_secret_name_owner_response", + "reason": "最後補 workflow / webhook / runner / deploy key / branch protection / CODEOWNERS / secret 名稱 parity,避免 secret 或 runner 變更早於 source truth。", + "required_packet": "docs/security/SOURCE-CONTROL-WORKFLOW-SECRET-NAME-OWNER-RESPONSE.md", + "minimum_response": [ + "webhook redacted state", + "runner label owner", + "deploy key redacted state", + "branch protection / CODEOWNERS state", + "repository secret name parity" + ], + "awooop_action": "display_next_collection_item", + "blocked_until_received": true, + "execution_authorized": false, + "still_forbidden": [ + "store_secret_value", + "modify_workflow", + "enable_runner", + "enable_github_hosted_runner", + "switch_github_primary" + ] + } + ], "latest_local_validation": { "status": "repo_snapshot_guard_pass", "date": "2026-05-18", diff --git a/scripts/security/source-control-owner-response-guard.py b/scripts/security/source-control-owner-response-guard.py index a9967383..bce015c6 100755 --- a/scripts/security/source-control-owner-response-guard.py +++ b/scripts/security/source-control-owner-response-guard.py @@ -135,15 +135,17 @@ def validate(root: Path) -> None: lane_by_id = {lane["lane_id"]: lane for lane in rollup["validation_lanes"]} missing_lane_by_id = {lane["lane_id"]: lane for lane in rollup["missing_response_lanes"]} + collection_order_by_id = {item["lane_id"]: item for item in rollup["owner_response_collection_order"]} total_templates = 0 total_acceptance_checks = 0 total_rejection_rules = 0 - for lane in LANES: + for index, lane in enumerate(LANES, start=1): snapshot = load_json(security_dir / lane["path"]) summary = snapshot["summary"] rollup_lane = lane_by_id[lane["lane_id"]] missing_lane = missing_lane_by_id[lane["lane_id"]] + collection_item = collection_order_by_id[lane["lane_id"]] assert_equal(f"{lane['lane_id']}.status", summary["owner_response_status"], "waiting_owner_response") assert_equal(f"{lane['lane_id']}.response_template_count", summary["response_template_count"], lane["expected_templates"]) @@ -171,6 +173,14 @@ def validate(root: Path) -> None: assert_equal(f"{lane['lane_id']}.missing_received_response_count", missing_lane["received_response_count"], 0) assert_equal(f"{lane['lane_id']}.missing_accepted_response_count", missing_lane["accepted_response_count"], 0) assert_equal(f"{lane['lane_id']}.missing_awooop_display_mode", missing_lane["awooop_display_mode"], "observe_missing_response") + assert_equal(f"{lane['lane_id']}.collection_order", collection_item["order"], index) + assert_equal( + f"{lane['lane_id']}.collection_awooop_action", + collection_item["awooop_action"], + "display_next_collection_item", + ) + assert_true(f"{lane['lane_id']}.collection_blocked_until_received", collection_item["blocked_until_received"]) + assert_false(f"{lane['lane_id']}.collection_execution_authorized", collection_item["execution_authorized"]) for flag in lane["false_flags"]: assert_false(f"{lane['lane_id']}.{flag}", summary[flag]) @@ -183,6 +193,7 @@ def validate(root: Path) -> None: assert_equal("source_packets.total_acceptance_checks", total_acceptance_checks, rollup_summary["total_acceptance_check_count"]) assert_equal("source_packets.total_rejection_rules", total_rejection_rules, rollup_summary["total_rejection_rule_count"]) assert_equal("missing_response_lanes.count", len(missing_lane_by_id), len(LANES)) + assert_equal("owner_response_collection_order.count", len(collection_order_by_id), len(LANES)) local_validation = rollup["latest_local_validation"] assert_equal("rollup.latest_local_validation.status", local_validation["status"], "repo_snapshot_guard_pass")