Files
aios/scripts/task-plane/create_shadow_tasks.py
T

181 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""Create one authenticated, no-side-effect Task Plane Shadow task.
The client deliberately has no worker, Git, deployment, or credential-writing
capabilities. Its token is accepted only from a protected file reference or
stdin so that it cannot enter the process command line.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any
MINIMUM_CLIENT_CAPABILITIES = (
"task.creator",
"task.read",
"task.events.read",
"task.evidence.read",
)
class ShadowClientError(RuntimeError):
"""HTTP failure that intentionally carries no response body."""
def __init__(self, status: int):
super().__init__(f"shadow task request failed with HTTP {status}")
self.status = status
def _request_json(
base_url: str,
path: str,
token: str,
*,
method: str = "GET",
payload: dict[str, Any] | None = None,
idempotency_key: str | None = None,
) -> tuple[int, dict[str, Any]]:
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
}
data = None
if payload is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
request = urllib.request.Request(
base_url.rstrip("/") + path,
data=data,
headers=headers,
method=method,
)
try:
with urllib.request.urlopen(request, timeout=10) as response:
body = response.read()
parsed = json.loads(body.decode("utf-8")) if body else {}
return response.status, parsed
except urllib.error.HTTPError as error:
# Never read or print an error body: it could contain sensitive data.
raise ShadowClientError(error.code) from None
except urllib.error.URLError as error:
raise ShadowClientError(503) from error
def run_shadow_client(
*,
base_url: str,
token: str,
task_id: str,
idempotency_key: str,
) -> dict[str, Any]:
"""Create and read one deterministic Shadow task without executing it."""
create_status, _ = _request_json(
base_url,
"/tasks",
token,
method="POST",
idempotency_key=idempotency_key,
payload={
"task_id": task_id,
"task_type": "platform.shadow",
"risk": "low",
"approval_policy": "patch-only",
"metadata": {"production_side_effects": False},
},
)
task_status, task = _request_json(base_url, f"/tasks/{task_id}", token)
events_status, events = _request_json(base_url, f"/tasks/{task_id}/events", token)
evidence_status, evidence = _request_json(base_url, f"/tasks/{task_id}/evidence", token)
return {
"task_id": str(task.get("task_id", task_id)),
"status": str(task.get("status", "UNKNOWN")),
"event_count": len(events if isinstance(events, list) else events.get("events", [])),
"evidence_count": len(evidence if isinstance(evidence, list) else evidence.get("evidence", [])),
"http_statuses": {
"create": create_status,
"task": task_status,
"events": events_status,
"evidence": evidence_status,
},
"production_side_effects": False,
}
def _reject_plaintext_token_argument(argv: list[str]) -> None:
if any(argument == "--token" or argument.startswith("--token=") for argument in argv):
raise SystemExit("plaintext token arguments are forbidden")
def _read_token(args: argparse.Namespace) -> str:
if args.token_stdin:
token = sys.stdin.buffer.read().decode("utf-8").strip()
else:
token_file = args.token_file or os.environ.get("AIOS_TASK_PLANE_TOKEN_FILE")
if not token_file:
raise SystemExit("runtime credential reference is required")
try:
token = Path(token_file).read_text(encoding="utf-8").strip()
except OSError:
raise SystemExit("runtime credential reference is unavailable") from None
if not token:
raise SystemExit("runtime credential reference is unavailable")
return token
def _write_report(path: Path, summary: dict[str, Any]) -> str:
encoded = (json.dumps(summary, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(encoded)
return hashlib.sha256(encoded).hexdigest()
def main(argv: list[str] | None = None) -> int:
argv = list(sys.argv[1:] if argv is None else argv)
_reject_plaintext_token_argument(argv)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-url", default="http://127.0.0.1:18922")
token_group = parser.add_mutually_exclusive_group()
token_group.add_argument("--token-file", help="protected runtime credential reference")
token_group.add_argument("--token-stdin", action="store_true")
parser.add_argument("--task-id", default="AIOS-TASK-PLANE-SHADOW-CLIENT")
parser.add_argument("--idempotency-key", default="task-plane-shadow-client-v1")
parser.add_argument("--report", required=True, type=Path)
args = parser.parse_args(argv)
token = _read_token(args)
try:
summary = run_shadow_client(
base_url=args.base_url,
token=token,
task_id=args.task_id,
idempotency_key=args.idempotency_key,
)
except ShadowClientError as error:
print(f"http_status={error.status}", file=sys.stderr)
return error.status
report_sha256 = _write_report(args.report, summary)
print(f"task_id={summary['task_id']}")
print(f"status={summary['status']}")
print(f"event_count={summary['event_count']}")
print(f"evidence_count={summary['evidence_count']}")
print("http_statuses=" + json.dumps(summary["http_statuses"], sort_keys=True))
print(f"report_sha256={report_sha256}")
return 0
if __name__ == "__main__":
raise SystemExit(main())