436 lines
16 KiB
Python
436 lines
16 KiB
Python
#!/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())
|