docs(ops): add blocked product response intake preflight [skip ci]
This commit is contained in:
@@ -1,3 +1,26 @@
|
||||
## 2026-06-24|Blocked products owner response intake preflight
|
||||
|
||||
**背景**:blocked products 已有 owner decision packages `8/8`、response templates `8/8`、acceptance ledgers `8/8`,但仍缺真正 owner response。為避免下一輪人工判斷混亂,本輪新增可執行 intake preflight,讓未來收件先走機器檢查與 lanes 分流。
|
||||
|
||||
**新增 / 更新**:
|
||||
- `scripts/security/blocked-products-owner-response-intake-preflight.py`
|
||||
- `docs/operations/blocked-product-owner-responses/README.md`
|
||||
- `docs/operations/codex-gitea-blocked-products-owner-response-intake-preflight.snapshot.json`
|
||||
- `docs/operations/CODEX-GITEA-BLOCKED-PRODUCTS-OWNER-RESPONSE-INTAKE-PREFLIGHT-2026-06-24.md`
|
||||
|
||||
**固定口徑**:
|
||||
- blocked products:`8`
|
||||
- response files:`0`
|
||||
- intake lanes:`6`
|
||||
- required owner response fields:`14`
|
||||
- acceptance checks:`16`
|
||||
- rejection guards:`15`
|
||||
- owner response received / accepted / rejected:`0 / 0 / 0`
|
||||
- review branch ready / remote dev ready / remote dev created:`0 / 0 / 0`
|
||||
- product repo write / runtime write / secret collection:`0`
|
||||
|
||||
**邊界**:這是 repo-only intake preflight,不是 owner response、review branch、remote `dev` branch、Gitea repo write、runtime write、secret collection、raw `.git` sync 或 raw conversation sync。
|
||||
|
||||
## 2026-06-24|Codex Start Here acceptance ledger sync readback
|
||||
|
||||
**背景**:blocked products owner response acceptance ledger 已推上 Gitea 後,Mac Mini 本機 `~/.codex/CODEX-START-HERE.md` 與 `~/.codex/codex-workstation-sync-dashboard.snapshot.json` 需要再次同步到 MacBook Pro,避免外出開新 Codex 視窗時讀不到 acceptance ledger gate。
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Codex Gitea Blocked Products Owner Response Intake Preflight
|
||||
|
||||
- generated_at: `2026-06-24T15:05:00+08:00`
|
||||
- blocked_products: `8`
|
||||
- response_files: `0`
|
||||
- owner_response_received: `0 / 8`
|
||||
- owner_response_accepted: `0 / 8`
|
||||
- remote_dev_ready: `0 / 8`
|
||||
- runtime_write_authorized: `0`
|
||||
|
||||
## 目的
|
||||
|
||||
本階段把 blocked products 的 owner response 從「文件模板」推進成「可執行的 intake preflight」。未來如果收到產品級、已脫敏的 owner response JSON,先由 `scripts/security/blocked-products-owner-response-intake-preflight.py` 掃描、分類、拒收或轉 reviewer checklist。
|
||||
|
||||
目前尚未收到任何 owner response,所以所有 accepted / remote dev / runtime gate 仍維持 `0`。
|
||||
|
||||
## 收件位置
|
||||
|
||||
只允許放置 metadata-only JSON:
|
||||
|
||||
- `docs/operations/blocked-product-owner-responses/`
|
||||
|
||||
目前此目錄只有 README,沒有 response JSON。
|
||||
|
||||
## Intake Lanes
|
||||
|
||||
| Lane | 用途 | Gate effect |
|
||||
|------|------|-------------|
|
||||
| `keep_waiting_owner_response` | 沒有產品級回覆或只有一般批准語句 | received / accepted / remote dev 仍為 0 |
|
||||
| `quarantine_sensitive_payload` | 疑似 secret / raw payload / `.env` / raw `.git` | 不保存 raw payload,不進 review |
|
||||
| `reject_execution_request` | 夾帶 repo write / deploy / restart / runtime 操作 | 拒收,不建立 action button |
|
||||
| `request_supplement` | 欄位不足或 evidence refs 不足 | 補件,不增加 accepted |
|
||||
| `ready_for_acceptance_review` | 欄位完整且脫敏 | 只進 reviewer checklist |
|
||||
| `metadata_accepted_waiting_branch_confirmation` | metadata accepted 後仍需分支最終確認 | runtime write 仍 false |
|
||||
|
||||
## 驗證指令
|
||||
|
||||
```bash
|
||||
python3 scripts/security/blocked-products-owner-response-intake-preflight.py \
|
||||
--root . \
|
||||
--generated-at 2026-06-24T15:05:00+08:00
|
||||
```
|
||||
|
||||
預期 readback:
|
||||
|
||||
```text
|
||||
BLOCKED_PRODUCTS_OWNER_RESPONSE_INTAKE_PREFLIGHT_OK products=8 responses=0 accepted=0 remote_dev_ready=0 runtime_write=0
|
||||
```
|
||||
|
||||
## 邊界
|
||||
|
||||
- 沒有讀 secret、`.env`、raw conversation、raw `.git` 或 runtime volume。
|
||||
- 沒有修改任何產品 repo。
|
||||
- 沒有建立 review branch、remote `dev` branch 或 Gitea repo。
|
||||
- 沒有部署、restart、reload、DB / K8s / host / firewall / Nginx runtime write。
|
||||
- 一般「批准繼續」仍不等於產品級 owner response。
|
||||
14
docs/operations/blocked-product-owner-responses/README.md
Normal file
14
docs/operations/blocked-product-owner-responses/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Blocked Product Owner Responses
|
||||
|
||||
本目錄只允許放「已脫敏、metadata-only」owner response JSON。
|
||||
|
||||
禁止放入:
|
||||
|
||||
- secret value、token、password、private key、cookie、session、authorization header
|
||||
- `.env` 內容、hash、partial token、credential derivative
|
||||
- raw Codex / ChatGPT conversation
|
||||
- raw `.git` directory 或 git bundle
|
||||
- runtime volume payload
|
||||
- 未脫敏 log、backup archive、browser profile、database dump
|
||||
|
||||
目前尚未收到任何可驗收的 owner response,所以此目錄只保留本 README。
|
||||
@@ -0,0 +1,267 @@
|
||||
{
|
||||
"schema_version": "codex_gitea_blocked_products_owner_response_intake_preflight_v1",
|
||||
"generated_at": "2026-06-24T15:05:00+08:00",
|
||||
"git_commit": "413a0dc8",
|
||||
"source_ledger_schema_version": "codex_gitea_blocked_products_owner_response_acceptance_v1",
|
||||
"status": "waiting_owner_response",
|
||||
"source_ledger": "docs/operations/codex-gitea-blocked-products-owner-response-acceptance.snapshot.json",
|
||||
"response_directory": "docs/operations/blocked-product-owner-responses",
|
||||
"summary": {
|
||||
"blocked_product_count": 8,
|
||||
"intake_candidate_count": 8,
|
||||
"response_file_count": 0,
|
||||
"parsed_response_file_count": 0,
|
||||
"quarantined_response_file_count": 0,
|
||||
"valid_redacted_response_file_count": 0,
|
||||
"required_owner_response_field_count": 14,
|
||||
"acceptance_check_count": 16,
|
||||
"rejection_guard_count": 15,
|
||||
"intake_lane_count": 6,
|
||||
"owner_response_received_count": 0,
|
||||
"owner_response_accepted_count": 0,
|
||||
"owner_response_rejected_count": 0,
|
||||
"supplement_requested_count": 0,
|
||||
"review_branch_ready_count": 0,
|
||||
"remote_dev_branch_ready_count": 0,
|
||||
"remote_dev_branch_created_count": 0,
|
||||
"product_repo_write_authorized_count": 0,
|
||||
"runtime_write_authorized_count": 0,
|
||||
"secret_values_collected_count": 0,
|
||||
"action_button_count": 0
|
||||
},
|
||||
"intake_lanes": [
|
||||
{
|
||||
"lane_id": "keep_waiting_owner_response",
|
||||
"instruction": "沒有產品級 redacted owner response,或只有一般批准語句時維持等待。",
|
||||
"gate_effect": "received / accepted / remote_dev_ready 全部維持 0。"
|
||||
},
|
||||
{
|
||||
"lane_id": "quarantine_sensitive_payload",
|
||||
"instruction": "疑似包含 secret、.env、raw conversation、raw .git、runtime volume 或未脫敏 payload 時隔離。",
|
||||
"gate_effect": "不得保存 raw payload,不得渲染到前端,不得進 acceptance review。"
|
||||
},
|
||||
{
|
||||
"lane_id": "reject_execution_request",
|
||||
"instruction": "夾帶 repo write、remote dev branch、deploy、restart、DB、K8s、host 或 runtime 執行要求時拒收。",
|
||||
"gate_effect": "不得建立 action button,不得轉成執行批准。"
|
||||
},
|
||||
{
|
||||
"lane_id": "request_supplement",
|
||||
"instruction": "欄位不足、scope 不清、include / exclude 模糊或 evidence refs 不足時補件。",
|
||||
"gate_effect": "不得增加 accepted 或 remote_dev_ready。"
|
||||
},
|
||||
{
|
||||
"lane_id": "ready_for_acceptance_review",
|
||||
"instruction": "欄位完整、證據脫敏、無敏感 payload 且無執行要求時才可進 reviewer checklist。",
|
||||
"gate_effect": "仍不是 accepted,也不是 remote dev 授權。"
|
||||
},
|
||||
{
|
||||
"lane_id": "metadata_accepted_waiting_branch_confirmation",
|
||||
"instruction": "即使 metadata accepted,也還需要逐產品 branch / baseline final confirmation。",
|
||||
"gate_effect": "runtime_write 仍必須是 false;remote dev 仍需獨立最終確認。"
|
||||
}
|
||||
],
|
||||
"required_owner_response_fields": [
|
||||
"owner_role_or_team",
|
||||
"decision",
|
||||
"decision_reason",
|
||||
"accepted_baseline_source",
|
||||
"include_groups",
|
||||
"exclude_groups",
|
||||
"quarantined_paths_ack",
|
||||
"env_secret_policy_ack",
|
||||
"generated_artifact_policy_ack",
|
||||
"review_branch_allowed",
|
||||
"remote_dev_branch_allowed",
|
||||
"runtime_write_allowed",
|
||||
"followup_owner",
|
||||
"evidence_refs"
|
||||
],
|
||||
"acceptance_checks": [
|
||||
"owner_role_or_team_is_present",
|
||||
"decision_is_product_specific",
|
||||
"decision_reason_is_present",
|
||||
"baseline_source_is_explicit",
|
||||
"include_groups_are_specific",
|
||||
"exclude_groups_are_specific",
|
||||
"quarantined_paths_ack_is_true",
|
||||
"env_secret_policy_ack_is_true",
|
||||
"generated_artifact_policy_ack_is_true",
|
||||
"review_branch_allowed_is_explicit",
|
||||
"remote_dev_branch_allowed_is_explicit",
|
||||
"runtime_write_allowed_is_false",
|
||||
"evidence_refs_are_redacted",
|
||||
"followup_owner_is_present",
|
||||
"no_secret_value_or_partial_secret_present",
|
||||
"no_raw_conversation_or_raw_git_sync_requested"
|
||||
],
|
||||
"rejection_guards": [
|
||||
"generic_approval_phrase_only",
|
||||
"missing_owner_role_or_team",
|
||||
"missing_decision_reason",
|
||||
"missing_baseline_source",
|
||||
"ambiguous_include_or_exclude_groups",
|
||||
"requests_secret_value_or_env_content",
|
||||
"requests_raw_git_directory_sync",
|
||||
"requests_raw_codex_or_chatgpt_history_sync",
|
||||
"requests_runtime_volume_sync",
|
||||
"requests_runtime_write",
|
||||
"requests_product_repo_write_without_review_branch",
|
||||
"requests_remote_dev_branch_without_explicit_product_decision",
|
||||
"includes_generated_outputs_without_sanitized_policy",
|
||||
"includes_logs_or_backup_archives_without_policy",
|
||||
"missing_redacted_evidence_refs"
|
||||
],
|
||||
"products": [
|
||||
{
|
||||
"product_id": "clawbot-openclaw",
|
||||
"status": "waiting_owner_response",
|
||||
"decision_package": "docs/operations/CLAWBOT-OPENCLAW-DEV-BASELINE-OWNER-DECISION-2026-06-24.md",
|
||||
"response_template_section": "P1-1 ClawBot / OpenClaw",
|
||||
"default_blockers": ["owner_response_missing", "two_file_drift_not_accepted"],
|
||||
"intake_lane": "keep_waiting_owner_response",
|
||||
"owner_response_received": false,
|
||||
"owner_response_accepted": false,
|
||||
"owner_response_rejected": false,
|
||||
"supplement_requested": false,
|
||||
"quarantined": false,
|
||||
"review_branch_ready": false,
|
||||
"remote_dev_branch_ready": false,
|
||||
"runtime_write_authorized": false,
|
||||
"secret_values_collected": false
|
||||
},
|
||||
{
|
||||
"product_id": "tsenyang-website",
|
||||
"status": "waiting_owner_response",
|
||||
"decision_package": "docs/operations/TSENYANG-WEBSITE-DEV-BASELINE-OWNER-DECISION-2026-06-24.md",
|
||||
"response_template_section": "P1-2 Tsenyang Website",
|
||||
"default_blockers": ["owner_response_missing", "presentation_output_policy_missing"],
|
||||
"intake_lane": "keep_waiting_owner_response",
|
||||
"owner_response_received": false,
|
||||
"owner_response_accepted": false,
|
||||
"owner_response_rejected": false,
|
||||
"supplement_requested": false,
|
||||
"quarantined": false,
|
||||
"review_branch_ready": false,
|
||||
"remote_dev_branch_ready": false,
|
||||
"runtime_write_authorized": false,
|
||||
"secret_values_collected": false
|
||||
},
|
||||
{
|
||||
"product_id": "agent-bounty-protocol",
|
||||
"status": "waiting_owner_response",
|
||||
"decision_package": "docs/operations/AGENT-BOUNTY-DEV-BASELINE-OWNER-DECISION-2026-06-24.md",
|
||||
"response_template_section": "P1-3 Agent Bounty",
|
||||
"default_blockers": ["owner_response_missing", "a2a_treasury_scope_not_accepted", "backup_archive_policy_missing"],
|
||||
"intake_lane": "keep_waiting_owner_response",
|
||||
"owner_response_received": false,
|
||||
"owner_response_accepted": false,
|
||||
"owner_response_rejected": false,
|
||||
"supplement_requested": false,
|
||||
"quarantined": false,
|
||||
"review_branch_ready": false,
|
||||
"remote_dev_branch_ready": false,
|
||||
"runtime_write_authorized": false,
|
||||
"secret_values_collected": false
|
||||
},
|
||||
{
|
||||
"product_id": "2026fifa",
|
||||
"status": "waiting_owner_response",
|
||||
"decision_package": "docs/operations/2026FIFA-DEV-BASELINE-OWNER-DECISION-2026-06-24.md",
|
||||
"response_template_section": "P1-4 2026FIFA",
|
||||
"default_blockers": ["owner_response_missing", "narrow_scanner_not_completed"],
|
||||
"intake_lane": "keep_waiting_owner_response",
|
||||
"owner_response_received": false,
|
||||
"owner_response_accepted": false,
|
||||
"owner_response_rejected": false,
|
||||
"supplement_requested": false,
|
||||
"quarantined": false,
|
||||
"review_branch_ready": false,
|
||||
"remote_dev_branch_ready": false,
|
||||
"runtime_write_authorized": false,
|
||||
"secret_values_collected": false
|
||||
},
|
||||
{
|
||||
"product_id": "vibework",
|
||||
"status": "waiting_owner_response",
|
||||
"decision_package": "docs/operations/VIBEWORK-DEV-BASELINE-OWNER-DECISION-2026-06-24.md",
|
||||
"response_template_section": "P1-5 VibeWork",
|
||||
"default_blockers": ["owner_response_missing", "release_scope_not_split_or_accepted", "diff_check_debt_unresolved"],
|
||||
"intake_lane": "keep_waiting_owner_response",
|
||||
"owner_response_received": false,
|
||||
"owner_response_accepted": false,
|
||||
"owner_response_rejected": false,
|
||||
"supplement_requested": false,
|
||||
"quarantined": false,
|
||||
"review_branch_ready": false,
|
||||
"remote_dev_branch_ready": false,
|
||||
"runtime_write_authorized": false,
|
||||
"secret_values_collected": false
|
||||
},
|
||||
{
|
||||
"product_id": "stockplatform-v2",
|
||||
"status": "waiting_owner_response",
|
||||
"decision_package": "docs/operations/STOCKPLATFORM-V2-DEV-BASELINE-OWNER-DECISION-2026-06-24.md",
|
||||
"response_template_section": "P1-6 StockPlatform v2",
|
||||
"default_blockers": ["owner_response_missing", "tmp_generated_outputs_not_excluded", "source_candidate_policy_missing"],
|
||||
"intake_lane": "keep_waiting_owner_response",
|
||||
"owner_response_received": false,
|
||||
"owner_response_accepted": false,
|
||||
"owner_response_rejected": false,
|
||||
"supplement_requested": false,
|
||||
"quarantined": false,
|
||||
"review_branch_ready": false,
|
||||
"remote_dev_branch_ready": false,
|
||||
"runtime_write_authorized": false,
|
||||
"secret_values_collected": false
|
||||
},
|
||||
{
|
||||
"product_id": "bitan-pharmacy",
|
||||
"status": "waiting_owner_response",
|
||||
"decision_package": "docs/operations/BITAN-PHARMACY-DEV-BASELINE-OWNER-DECISION-2026-06-24.md",
|
||||
"response_template_section": "P1-7 Bitan Pharmacy",
|
||||
"default_blockers": ["owner_response_missing", "internal_inventory_missing", "public_content_cleanliness_evidence_policy_missing"],
|
||||
"intake_lane": "keep_waiting_owner_response",
|
||||
"owner_response_received": false,
|
||||
"owner_response_accepted": false,
|
||||
"owner_response_rejected": false,
|
||||
"supplement_requested": false,
|
||||
"quarantined": false,
|
||||
"review_branch_ready": false,
|
||||
"remote_dev_branch_ready": false,
|
||||
"runtime_write_authorized": false,
|
||||
"secret_values_collected": false
|
||||
},
|
||||
{
|
||||
"product_id": "vtuber",
|
||||
"status": "waiting_owner_response",
|
||||
"decision_package": "docs/operations/VTUBER-DEV-BASELINE-OWNER-DECISION-2026-06-24.md",
|
||||
"response_template_section": "P1-8 VTuber",
|
||||
"default_blockers": ["owner_response_missing", "repository_identity_unresolved", "remote_repo_missing"],
|
||||
"intake_lane": "keep_waiting_owner_response",
|
||||
"owner_response_received": false,
|
||||
"owner_response_accepted": false,
|
||||
"owner_response_rejected": false,
|
||||
"supplement_requested": false,
|
||||
"quarantined": false,
|
||||
"review_branch_ready": false,
|
||||
"remote_dev_branch_ready": false,
|
||||
"runtime_write_authorized": false,
|
||||
"secret_values_collected": false
|
||||
}
|
||||
],
|
||||
"inspected_response_files": [],
|
||||
"hard_gates": [
|
||||
"Only redacted owner response metadata JSON files may be inspected.",
|
||||
"Generic approval text is not a product-specific owner response.",
|
||||
"Accepted count stays 0 until reviewer acceptance is recorded.",
|
||||
"Remote dev branch readiness stays 0 until product-specific final confirmation.",
|
||||
"Runtime write, product repo write, secret collection, raw .git sync, raw conversation sync, .env read, and runtime volume sync remain forbidden."
|
||||
],
|
||||
"secret_values_collected": false,
|
||||
"env_file_content_read": false,
|
||||
"raw_git_sync_allowed": false,
|
||||
"raw_conversation_sync_allowed": false,
|
||||
"runtime_write_authorized": false,
|
||||
"product_repo_write_authorized": false
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Codex / Gitea blocked products owner response intake preflight.
|
||||
|
||||
This is a repo-only metadata gate. It reads the committed acceptance ledger and
|
||||
optionally scans redacted owner response metadata files. It must never read
|
||||
secret values, .env contents, raw Codex conversations, raw .git directories, or
|
||||
runtime volumes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
TAIPEI = timezone(timedelta(hours=8))
|
||||
|
||||
DEFAULT_LEDGER = Path("docs/operations/codex-gitea-blocked-products-owner-response-acceptance.snapshot.json")
|
||||
DEFAULT_RESPONSE_DIR = Path("docs/operations/blocked-product-owner-responses")
|
||||
|
||||
INTAKE_LANES = [
|
||||
{
|
||||
"lane_id": "keep_waiting_owner_response",
|
||||
"instruction": "沒有產品級 redacted owner response,或只有一般批准語句時維持等待。",
|
||||
"gate_effect": "received / accepted / remote_dev_ready 全部維持 0。",
|
||||
},
|
||||
{
|
||||
"lane_id": "quarantine_sensitive_payload",
|
||||
"instruction": "疑似包含 secret、.env、raw conversation、raw .git、runtime volume 或未脫敏 payload 時隔離。",
|
||||
"gate_effect": "不得保存 raw payload,不得渲染到前端,不得進 acceptance review。",
|
||||
},
|
||||
{
|
||||
"lane_id": "reject_execution_request",
|
||||
"instruction": "夾帶 repo write、remote dev branch、deploy、restart、DB、K8s、host 或 runtime 執行要求時拒收。",
|
||||
"gate_effect": "不得建立 action button,不得轉成執行批准。",
|
||||
},
|
||||
{
|
||||
"lane_id": "request_supplement",
|
||||
"instruction": "欄位不足、scope 不清、include / exclude 模糊或 evidence refs 不足時補件。",
|
||||
"gate_effect": "不得增加 accepted 或 remote_dev_ready。",
|
||||
},
|
||||
{
|
||||
"lane_id": "ready_for_acceptance_review",
|
||||
"instruction": "欄位完整、證據脫敏、無敏感 payload 且無執行要求時才可進 reviewer checklist。",
|
||||
"gate_effect": "仍不是 accepted,也不是 remote dev 授權。",
|
||||
},
|
||||
{
|
||||
"lane_id": "metadata_accepted_waiting_branch_confirmation",
|
||||
"instruction": "即使 metadata accepted,也還需要逐產品 branch / baseline final confirmation。",
|
||||
"gate_effect": "runtime_write 仍必須是 false;remote dev 仍需獨立最終確認。",
|
||||
},
|
||||
]
|
||||
|
||||
FORBIDDEN_KEYS = [
|
||||
"secret",
|
||||
"token",
|
||||
"password",
|
||||
"private_key",
|
||||
"cookie",
|
||||
"session",
|
||||
"authorization",
|
||||
"env_content",
|
||||
"raw_git",
|
||||
"raw_conversation",
|
||||
"runtime_volume",
|
||||
]
|
||||
|
||||
|
||||
def git_short_sha(root: Path) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--short", "HEAD"],
|
||||
cwd=root,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def response_files(response_dir: Path) -> list[Path]:
|
||||
if not response_dir.exists():
|
||||
return []
|
||||
return sorted(path for path in response_dir.glob("*.json") if path.is_file())
|
||||
|
||||
|
||||
def has_forbidden_key(payload: Any) -> bool:
|
||||
if isinstance(payload, dict):
|
||||
for key, value in payload.items():
|
||||
normalized = str(key).lower()
|
||||
if any(forbidden in normalized for forbidden in FORBIDDEN_KEYS):
|
||||
return True
|
||||
if has_forbidden_key(value):
|
||||
return True
|
||||
elif isinstance(payload, list):
|
||||
return any(has_forbidden_key(item) for item in payload)
|
||||
return False
|
||||
|
||||
|
||||
def inspect_responses(response_paths: list[Path]) -> list[dict[str, Any]]:
|
||||
inspected: list[dict[str, Any]] = []
|
||||
for path in response_paths:
|
||||
try:
|
||||
payload = load_json(path)
|
||||
parse_ok = True
|
||||
parse_error = ""
|
||||
except Exception as exc:
|
||||
payload = {}
|
||||
parse_ok = False
|
||||
parse_error = str(exc)
|
||||
product_id = payload.get("product_id", path.stem) if isinstance(payload, dict) else path.stem
|
||||
inspected.append(
|
||||
{
|
||||
"path": str(path),
|
||||
"product_id": product_id,
|
||||
"parse_ok": parse_ok,
|
||||
"parse_error": parse_error,
|
||||
"contains_forbidden_key": has_forbidden_key(payload),
|
||||
"received": parse_ok,
|
||||
"accepted": False,
|
||||
"review_branch_ready": False,
|
||||
"remote_dev_branch_ready": False,
|
||||
"runtime_write_authorized": False,
|
||||
}
|
||||
)
|
||||
return inspected
|
||||
|
||||
|
||||
def repo_relative(root: Path, path: Path) -> str:
|
||||
try:
|
||||
return path.relative_to(root).as_posix()
|
||||
except ValueError:
|
||||
return str(path)
|
||||
|
||||
|
||||
def candidate_from_product(product: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"product_id": product["product_id"],
|
||||
"status": "waiting_owner_response",
|
||||
"decision_package": product["decision_package"],
|
||||
"response_template_section": product["response_template_section"],
|
||||
"default_blockers": product["default_blockers"],
|
||||
"intake_lane": "keep_waiting_owner_response",
|
||||
"owner_response_received": False,
|
||||
"owner_response_accepted": False,
|
||||
"owner_response_rejected": False,
|
||||
"supplement_requested": False,
|
||||
"quarantined": False,
|
||||
"review_branch_ready": False,
|
||||
"remote_dev_branch_ready": False,
|
||||
"runtime_write_authorized": False,
|
||||
"secret_values_collected": False,
|
||||
}
|
||||
|
||||
|
||||
def build_report(root: Path, ledger: dict[str, Any], response_dir: Path, generated_at: str | None) -> dict[str, Any]:
|
||||
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
|
||||
products = ledger.get("products", [])
|
||||
response_paths = response_files(response_dir)
|
||||
inspected = inspect_responses(response_paths)
|
||||
parsed_received = [item for item in inspected if item["received"] and not item["contains_forbidden_key"]]
|
||||
quarantined = [item for item in inspected if item["contains_forbidden_key"] or not item["parse_ok"]]
|
||||
|
||||
return {
|
||||
"schema_version": "codex_gitea_blocked_products_owner_response_intake_preflight_v1",
|
||||
"generated_at": report_time,
|
||||
"git_commit": git_short_sha(root),
|
||||
"source_ledger_schema_version": ledger.get("schema_version"),
|
||||
"status": "waiting_owner_response",
|
||||
"source_ledger": str(DEFAULT_LEDGER),
|
||||
"response_directory": repo_relative(root, response_dir),
|
||||
"summary": {
|
||||
"blocked_product_count": len(products),
|
||||
"intake_candidate_count": len(products),
|
||||
"response_file_count": len(response_paths),
|
||||
"parsed_response_file_count": sum(1 for item in inspected if item["parse_ok"]),
|
||||
"quarantined_response_file_count": len(quarantined),
|
||||
"valid_redacted_response_file_count": len(parsed_received),
|
||||
"required_owner_response_field_count": len(ledger.get("required_owner_response_fields", [])),
|
||||
"acceptance_check_count": len(ledger.get("acceptance_checks", [])),
|
||||
"rejection_guard_count": len(ledger.get("rejection_guards", [])),
|
||||
"intake_lane_count": len(INTAKE_LANES),
|
||||
"owner_response_received_count": 0,
|
||||
"owner_response_accepted_count": 0,
|
||||
"owner_response_rejected_count": 0,
|
||||
"supplement_requested_count": 0,
|
||||
"review_branch_ready_count": 0,
|
||||
"remote_dev_branch_ready_count": 0,
|
||||
"remote_dev_branch_created_count": 0,
|
||||
"product_repo_write_authorized_count": 0,
|
||||
"runtime_write_authorized_count": 0,
|
||||
"secret_values_collected_count": 0,
|
||||
"action_button_count": 0,
|
||||
},
|
||||
"intake_lanes": INTAKE_LANES,
|
||||
"required_owner_response_fields": ledger.get("required_owner_response_fields", []),
|
||||
"acceptance_checks": ledger.get("acceptance_checks", []),
|
||||
"rejection_guards": ledger.get("rejection_guards", []),
|
||||
"products": [candidate_from_product(product) for product in products],
|
||||
"inspected_response_files": inspected,
|
||||
"hard_gates": [
|
||||
"Only redacted owner response metadata JSON files may be inspected.",
|
||||
"Generic approval text is not a product-specific owner response.",
|
||||
"Accepted count stays 0 until reviewer acceptance is recorded.",
|
||||
"Remote dev branch readiness stays 0 until product-specific final confirmation.",
|
||||
"Runtime write, product repo write, secret collection, raw .git sync, raw conversation sync, .env read, and runtime volume sync remain forbidden.",
|
||||
],
|
||||
"secret_values_collected": False,
|
||||
"env_file_content_read": False,
|
||||
"raw_git_sync_allowed": False,
|
||||
"raw_conversation_sync_allowed": False,
|
||||
"runtime_write_authorized": False,
|
||||
"product_repo_write_authorized": False,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", default=".", help="Repository root.")
|
||||
parser.add_argument("--ledger", default=str(DEFAULT_LEDGER), help="Acceptance ledger snapshot path.")
|
||||
parser.add_argument("--response-dir", default=str(DEFAULT_RESPONSE_DIR), help="Redacted response metadata directory.")
|
||||
parser.add_argument("--generated-at", default=None, help="Override generated_at timestamp.")
|
||||
parser.add_argument("--output", default=None, help="Optional output JSON path.")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.root).resolve()
|
||||
ledger_path = (root / args.ledger).resolve()
|
||||
response_dir = (root / args.response_dir).resolve()
|
||||
|
||||
ledger = load_json(ledger_path)
|
||||
report = build_report(root, ledger, response_dir, args.generated_at)
|
||||
text = json.dumps(report, ensure_ascii=False, indent=2) + "\n"
|
||||
|
||||
if args.output:
|
||||
Path(args.output).write_text(text, encoding="utf-8")
|
||||
else:
|
||||
print(text, end="")
|
||||
|
||||
summary = report["summary"]
|
||||
print(
|
||||
"BLOCKED_PRODUCTS_OWNER_RESPONSE_INTAKE_PREFLIGHT_OK "
|
||||
f"products={summary['blocked_product_count']} "
|
||||
f"responses={summary['response_file_count']} "
|
||||
f"accepted={summary['owner_response_accepted_count']} "
|
||||
f"remote_dev_ready={summary['remote_dev_branch_ready_count']} "
|
||||
f"runtime_write={summary['runtime_write_authorized_count']}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user