Files
awoooi/scripts/security/agent-bounty-owner-request-draft.py

436 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
IwoooS agent-bounty-protocol owner request draft 產生器。
本工具讀取 agent-bounty-protocol onboarding handoff snapshot將 repo / refs、
deployment、產品 surface、資料分級、MCP / A2A / treasury 邊界轉成人工送件前
request draft。它不讀 agent-bounty-protocol 工作樹、不讀 .env、不部署、不啟動
cron / daemon、不 claim / submit、不 payout / withdraw。
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
TAIPEI = timezone(timedelta(hours=8))
REQUEST_FIELDS = [
"request_id",
"scope_id",
"label",
"request_kind",
"priority",
"source_handoff_ref",
"source_evidence_refs",
"routes",
"boundary",
"data_classes",
"owner_role_fields",
"decision",
"decision_reason",
"affected_scope",
"redacted_evidence_refs",
"canonical_repo_ref",
"repo_dirty_disposition",
"deployment_boundary_ref",
"auth_abuse_boundary_ref",
"external_agent_boundary_ref",
"settlement_treasury_boundary_ref",
"maintenance_window",
"rollback_owner",
"validation_plan",
"followup_owner",
"not_approval",
]
REQUIRED_OWNER_FIELDS = [
"product_owner_role_or_team",
"security_owner_role_or_team",
"source_control_owner_role_or_team",
"deployment_owner_role_or_team",
"data_classification_owner_role_or_team",
"external_agent_boundary_owner_role_or_team",
"settlement_or_treasury_owner_role_or_team",
"notification_owner_role_or_team",
"surface_scope",
"decision",
"decision_reason",
"redacted_evidence_refs",
"canonical_repo_ref",
"repo_dirty_disposition",
"deployment_boundary_ref",
"auth_abuse_boundary_ref",
"external_agent_boundary_ref",
"settlement_treasury_boundary_ref",
"maintenance_window",
"rollback_owner",
"validation_plan",
"followup_owner",
]
CONTROL_REQUESTS = [
{
"scope_id": "repo_refs_boundary",
"label": "Repo / refs / workflow boundary",
"request_kind": "source_control_boundary",
"priority": "P0",
"routes": [],
"boundary": "確認 canonical repo、dirty workspace disposition、workflow / runner / secret name owner不得 push、sync refs 或建立 repo。",
"data_classes": [],
"write_capable": True,
"treasury_related": False,
"mcp_a2a_related": False,
},
{
"scope_id": "deployment_boundary",
"label": "Production / compose / domain boundary",
"request_kind": "deployment_boundary",
"priority": "P0",
"routes": ["https://agent.wooo.work", "/api/v1/health"],
"boundary": "確認 production host、compose directory、domain / TLS、health smoke 與 rollback owner不得 deploy、restart 或 migration。",
"data_classes": [],
"write_capable": True,
"treasury_related": False,
"mcp_a2a_related": False,
},
{
"scope_id": "data_classification_boundary",
"label": "Data classification / evidence intake boundary",
"request_kind": "data_classification_boundary",
"priority": "P0",
"routes": [],
"boundary": "確認 task、agent、webhook、traffic、treasury、admin、cron 資料分級;只收 metadata不收 raw payload 或 secret。",
"data_classes": "all",
"write_capable": False,
"treasury_related": True,
"mcp_a2a_related": True,
},
{
"scope_id": "external_agent_treasury_boundary",
"label": "MCP / A2A / external agent / treasury boundary",
"request_kind": "external_agent_treasury_boundary",
"priority": "P0",
"routes": ["/api/mcp/[tool]", "/api/a2a/*", "/admin/treasury", "/api/admin/withdraw"],
"boundary": "確認外部 agent 自主行為、settlement、staking、payout、withdrawal 與 notification owner不得 claim、submit、daemon、send 或 payout。",
"data_classes": ["MCP tool calls / A2A protocol events", "settlement / staking / treasury / Stripe"],
"write_capable": True,
"treasury_related": True,
"mcp_a2a_related": True,
},
]
FALSE_COUNTER_KEYS = [
"request_sent_count",
"recipient_confirmed_count",
"owner_response_received_count",
"owner_response_accepted_count",
"live_evidence_received_count",
"repo_refs_truth_accepted_count",
"data_classification_accepted_count",
"deployment_boundary_accepted_count",
"external_agent_boundary_accepted_count",
"settlement_treasury_accepted_count",
"auth_abuse_boundary_accepted_count",
"runtime_gate_count",
"runtime_execution_authorized_count",
"production_deploy_authorized_count",
"repo_creation_authorized_count",
"refs_sync_authorized_count",
"workflow_modification_authorized_count",
"secret_value_collection_allowed_count",
"env_file_read_authorized_count",
"active_scan_authorized_count",
"credentialed_scan_authorized_count",
"deploy_authorized_count",
"compose_restart_authorized_count",
"db_migration_authorized_count",
"daemon_start_authorized_count",
"cron_enable_authorized_count",
"auto_claim_authorized_count",
"auto_submit_authorized_count",
"external_agent_message_authorized_count",
"telegram_send_authorized_count",
"discord_send_authorized_count",
"github_comment_authorized_count",
"payout_authorized_count",
"withdrawal_authorized_count",
"staking_action_authorized_count",
"webhook_secret_change_authorized_count",
"shared_database_authorized_count",
"shared_session_authorized_count",
"shared_rbac_authorized_count",
"host_write_authorized_count",
"production_write_authorized_count",
"action_button_count",
]
FALSE_DRAFT_KEYS = [
"request_sent",
"recipient_confirmed",
"owner_response_received",
"owner_response_accepted",
"live_evidence_received",
"repo_refs_truth_accepted",
"data_classification_accepted",
"deployment_boundary_accepted",
"external_agent_boundary_accepted",
"settlement_treasury_accepted",
"auth_abuse_boundary_accepted",
"runtime_gate",
"runtime_execution_authorized",
"production_deploy_authorized",
"repo_creation_authorized",
"refs_sync_authorized",
"workflow_modification_authorized",
"secret_value_collection_allowed",
"env_file_read_authorized",
"active_scan_authorized",
"credentialed_scan_authorized",
"deploy_authorized",
"compose_restart_authorized",
"db_migration_authorized",
"daemon_start_authorized",
"cron_enable_authorized",
"auto_claim_authorized",
"auto_submit_authorized",
"external_agent_message_authorized",
"telegram_send_authorized",
"discord_send_authorized",
"github_comment_authorized",
"payout_authorized",
"withdrawal_authorized",
"staking_action_authorized",
"webhook_secret_change_authorized",
"shared_database_authorized",
"shared_session_authorized",
"shared_rbac_authorized",
"host_write_authorized",
"production_write_authorized",
"action_buttons_allowed",
]
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 request_draft(
*,
scope_id: str,
label: str,
request_kind: str,
priority: str,
routes: list[str],
boundary: str,
data_classes: list[str],
write_capable: bool,
treasury_related: bool,
mcp_a2a_related: bool,
handoff: dict[str, Any],
) -> dict[str, Any]:
draft = {
"request_id": f"agent_bounty_owner_request:{scope_id}",
"status": "draft_not_dispatched",
"scope_id": scope_id,
"label": label,
"request_kind": request_kind,
"priority": priority,
"source_handoff_ref": "docs/security/agent-bounty-iwooos-onboarding-handoff.snapshot.json",
"source_evidence_refs": handoff["source_evidence_refs"],
"routes": routes,
"boundary": boundary,
"data_classes": data_classes,
"write_capable_scope": write_capable,
"treasury_related_scope": treasury_related,
"mcp_a2a_related_scope": mcp_a2a_related,
"requires_live_evidence": True,
"request_fields": REQUEST_FIELDS,
"owner_role_fields": handoff["owner_response_handoff"]["required_response_fields"],
"required_owner_fields": REQUIRED_OWNER_FIELDS,
"forbidden_inputs": handoff["owner_response_handoff"]["forbidden_inputs"],
"blocked_actions": handoff["forbidden_actions"],
"decision": "pending_owner_decision",
"decision_reason": "pending_decision_reason",
"affected_scope": "pending_affected_scope",
"redacted_evidence_refs": [],
"canonical_repo_ref": None,
"repo_dirty_disposition": "pending_repo_dirty_disposition",
"deployment_boundary_ref": None,
"auth_abuse_boundary_ref": None,
"external_agent_boundary_ref": None,
"settlement_treasury_boundary_ref": None,
"maintenance_window": "pending_maintenance_window",
"rollback_owner": "pending_rollback_owner",
"validation_plan": "pending_validation_plan",
"followup_owner": "pending_followup_owner",
"not_approval": True,
}
for key in FALSE_DRAFT_KEYS:
draft[key] = False
return draft
def build_report(root: Path, handoff: dict[str, Any], generated_at: str | None) -> dict[str, Any]:
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
data_types = [item["data_type"] for item in handoff["data_classification_intake"]]
drafts: list[dict[str, Any]] = []
for control in CONTROL_REQUESTS:
data_classes = data_types if control["data_classes"] == "all" else list(control["data_classes"])
drafts.append(
request_draft(
scope_id=control["scope_id"],
label=control["label"],
request_kind=control["request_kind"],
priority=control["priority"],
routes=list(control["routes"]),
boundary=control["boundary"],
data_classes=data_classes,
write_capable=bool(control["write_capable"]),
treasury_related=bool(control["treasury_related"]),
mcp_a2a_related=bool(control["mcp_a2a_related"]),
handoff=handoff,
)
)
for surface in handoff["product_surfaces"]:
surface_id = surface["surface_id"]
treasury_related = surface_id in {"a2a-agent-protocol", "admin-and-treasury"}
mcp_a2a_related = surface_id in {"mcp-and-open-task-api", "a2a-agent-protocol", "automation-and-cron"}
write_capable = surface_id in {
"mcp-and-open-task-api",
"a2a-agent-protocol",
"automation-and-cron",
"admin-and-treasury",
"webhooks-and-traffic",
}
drafts.append(
request_draft(
scope_id=surface_id,
label=surface_id.replace("-", " ").title(),
request_kind="product_surface_boundary",
priority="P0",
routes=surface["routes"],
boundary=surface["boundary"],
data_classes=[],
write_capable=write_capable,
treasury_related=treasury_related,
mcp_a2a_related=mcp_a2a_related,
handoff=handoff,
)
)
write_capable_drafts = [item for item in drafts if item["write_capable_scope"]]
treasury_related_drafts = [item for item in drafts if item["treasury_related_scope"]]
mcp_a2a_related_drafts = [item for item in drafts if item["mcp_a2a_related_scope"]]
summary = {
"request_draft_count": len(drafts),
"control_boundary_request_count": len(CONTROL_REQUESTS),
"product_surface_request_count": len(handoff["product_surfaces"]),
"write_capable_request_draft_count": len(write_capable_drafts),
"treasury_related_request_draft_count": len(treasury_related_drafts),
"mcp_a2a_related_request_draft_count": len(mcp_a2a_related_drafts),
"live_evidence_required_request_count": len(drafts),
"request_field_count": len(REQUEST_FIELDS),
"required_owner_field_count": len(REQUIRED_OWNER_FIELDS),
"owner_role_field_count": len(handoff["owner_response_handoff"]["required_response_fields"]),
"forbidden_input_count": len(handoff["owner_response_handoff"]["forbidden_inputs"]),
"blocked_action_count": len(handoff["forbidden_actions"]),
}
for key in FALSE_COUNTER_KEYS:
summary[key] = 0
execution_boundaries = {
("action_buttons_allowed" if key == "action_button_count" else key.replace("_count", "")): False
for key in FALSE_COUNTER_KEYS
}
execution_boundaries["not_authorization"] = True
return {
"schema_version": "agent_bounty_owner_request_draft_v1",
"generated_at": report_time,
"git_commit": git_short_sha(root),
"source_handoff_schema_version": handoff["schema_version"],
"source_handoff_status": handoff["status"],
"status": "owner_request_draft_ready_not_dispatched",
"product_name": handoff["summary"]["product_name"],
"summary": summary,
"execution_boundaries": execution_boundaries,
"request_fields": REQUEST_FIELDS,
"required_owner_fields": REQUIRED_OWNER_FIELDS,
"owner_role_fields": handoff["owner_response_handoff"]["required_response_fields"],
"forbidden_inputs": handoff["owner_response_handoff"]["forbidden_inputs"],
"blocked_actions": handoff["forbidden_actions"],
"request_drafts": drafts,
"next_steps": [
"人工送件前確認 product / security / source-control / deployment / external-agent / treasury owner role。",
"owner 只能提供脫敏 repo refs、dirty workspace disposition、deployment boundary、auth / abuse boundary、MCP / A2A boundary、treasury boundary 與 validation plan。",
"收到回覆後先做欄位完整性、敏感 payload 隔離、source-control / runtime / financial action 拒收與 reviewer checklist不得直接 deploy、claim、submit、daemon、payout 或 send notification。",
],
}
def main() -> int:
parser = argparse.ArgumentParser(description="IwoooS agent-bounty owner request draft 產生器")
parser.add_argument("--root", default=".", help="repo root")
parser.add_argument(
"--handoff-report",
default="docs/security/agent-bounty-iwooos-onboarding-handoff.snapshot.json",
help="agent-bounty-iwooos-onboarding handoff JSON",
)
parser.add_argument("--output", help="寫出 JSON 報告")
parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用")
args = parser.parse_args()
root = Path(args.root).resolve()
handoff = load_json(root / args.handoff_report)
report = build_report(root, handoff, args.generated_at)
payload = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)
if args.output:
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(payload + "\n", encoding="utf-8")
else:
print(payload)
summary = report["summary"]
print(
"AGENT_BOUNTY_OWNER_REQUEST_DRAFT_OK "
f"drafts={summary['request_draft_count']} "
f"write_capable={summary['write_capable_request_draft_count']} "
f"treasury={summary['treasury_related_request_draft_count']} "
f"fields={summary['required_owner_field_count']} "
f"sent={summary['request_sent_count']} "
f"runtime_gate={summary['runtime_gate_count']}",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
sys.exit(main())