#!/usr/bin/env python3 """Submit an OpenClaw/Codex task lifecycle event over HTTP or a polled inbox.""" from __future__ import annotations import argparse import json import os from pathlib import Path import sys import urllib.error import urllib.request REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT)) from services.aios_orchestrator_gateway.models import Event from services.aios_orchestrator_gateway.securefs import atomic_write_bytes_shared, read_secure_json, read_secure_text def parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser() p.add_argument("--project-key", required=True) p.add_argument("--contract-commit", required=True) p.add_argument("--event-id", required=True) p.add_argument("--source-agent", required=True, choices=["openclaw", "codex", "gitea", "system"]) p.add_argument("--event-type", required=True) p.add_argument("--work-unit-id", required=True) p.add_argument( "--status", required=True, choices=["READY", "RUNNING", "PASS", "BLOCKED", "FAIL", "TIMEOUT", "NOT_EXECUTED"], ) p.add_argument("--state-version", type=int, required=True) p.add_argument("--implementation-commit") p.add_argument("--result-commit") p.add_argument("--blocker") p.add_argument("--evidence-ref", action="append", default=[]) p.add_argument("--payload-file", type=Path) p.add_argument("--endpoint") p.add_argument("--token-file", type=Path) p.add_argument("--inbox-dir", type=Path) p.add_argument("--timeout-seconds", type=int, default=15) return p def main() -> int: args = parser().parse_args() if not args.endpoint and not args.inbox_dir: raise SystemExit("either --endpoint or --inbox-dir is required") if args.endpoint and not args.token_file: raise SystemExit("--token-file is required with --endpoint") payload = {} if args.payload_file: if not args.payload_file.is_absolute(): raise SystemExit("--payload-file must be absolute") payload = read_secure_json( args.payload_file, expected_uid=os.getuid(), max_bytes=262_144 ) if not isinstance(payload, dict): raise SystemExit("payload root must be an object") event = Event.from_dict( { "schema_version": 1, "project_key": args.project_key, "contract_commit": args.contract_commit, "event_id": args.event_id, "source_agent": args.source_agent, "event_type": args.event_type, "work_unit_id": args.work_unit_id, "status": args.status, "state_version": args.state_version, "implementation_commit": args.implementation_commit, "result_commit": args.result_commit, "blocker": args.blocker, "evidence_refs": args.evidence_ref, "payload": payload, } ) encoded = event.canonical_json().encode() http_error: Exception | None = None if args.endpoint: token = read_secure_text( args.token_file, expected_uid=None, max_bytes=64_000, ) request = urllib.request.Request( args.endpoint.rstrip("/") + "/v1/orchestrator/events", data=encoded, method="POST", headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", "Idempotency-Key": event.event_id, "User-Agent": "aios-agent-event-reporter/0.2", }, ) try: with urllib.request.urlopen(request, timeout=args.timeout_seconds) as response: response.read(65_536) if response.status not in {200, 202}: raise RuntimeError(f"unexpected HTTP {response.status}") print(f"EVENT_SUBMIT=PASS event_id={event.event_id} channel=http") return 0 except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError) as exc: http_error = exc if args.inbox_dir: pending = args.inbox_dir / "pending" path = pending / f"{event.event_id}.json" atomic_write_bytes_shared( path, (json.dumps(event.to_dict(), indent=2, sort_keys=True) + "\n").encode(), mode=0o640, group_writable_directory=True, ) print(f"EVENT_SUBMIT=PASS event_id={event.event_id} channel=file_inbox") return 0 print( f"EVENT_SUBMIT=FAIL event_id={event.event_id} reason={type(http_error).__name__}", file=sys.stderr, ) return 2 if __name__ == "__main__": raise SystemExit(main())