"""Trusted bridge from Agent PreToolUse package validation to native SubagentStart. Native SubagentStart payloads may omit the spawning prompt. PreToolUse therefore appends a pending exact package binding; SubagentStart claims the oldest unclaimed binding for the same concrete agent type. The log is append-only and protected by guard_tools. """ from __future__ import annotations import json import os import uuid from datetime import datetime, timezone import _workspace as W def _path() -> str: return os.path.join(W.state_dir(), "spawn-bindings.jsonl") def _rows() -> list[dict]: path = _path() if not os.path.exists(path): return [] rows = [] for line in open(path, encoding="utf-8"): try: value = json.loads(line) if isinstance(value, dict): rows.append(value) except Exception: continue return rows def _append(record: dict) -> None: path = _path() os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "a", encoding="utf-8") as handle: try: import fcntl fcntl.flock(handle.fileno(), fcntl.LOCK_EX) except Exception: pass handle.write(json.dumps(record, ensure_ascii=False) + "\n") handle.flush() os.fsync(handle.fileno()) def record_pending( agent_type: str, package_path: str, package_sha256: str, session_id: str | None = None, ) -> str | None: try: binding_id = "spb-" + uuid.uuid4().hex _append({ "event-type": "spawn-binding-pending", "binding-id": binding_id, "agent-type": str(agent_type).lower(), "context-package": package_path, "context-package-sha256": package_sha256, "session-id": session_id, "at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), }) return binding_id except Exception: return None def claim_pending(agent_type: str, agent_id: str, session_id: str | None = None) -> dict | None: try: path = _path() os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "a+", encoding="utf-8") as handle: try: import fcntl fcntl.flock(handle.fileno(), fcntl.LOCK_EX) except Exception: pass handle.seek(0) rows = [] for line in handle: try: value = json.loads(line) if isinstance(value, dict): rows.append(value) except Exception: continue consumed = {row.get("binding-id") for row in rows if row.get("event-type") == "spawn-binding-claimed"} pending = [row for row in rows if row.get("event-type") == "spawn-binding-pending" and row.get("agent-type") == str(agent_type).lower() and row.get("binding-id") not in consumed and (not session_id or not row.get("session-id") or row.get("session-id") == session_id)] if not pending: return None record = pending[0] claimed = { "event-type": "spawn-binding-claimed", "binding-id": record["binding-id"], "agent-id": agent_id, "session-id": session_id, "at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), } handle.seek(0, os.SEEK_END) handle.write(json.dumps(claimed, ensure_ascii=False) + "\n") handle.flush() os.fsync(handle.fileno()) return record except Exception: return None