Files
company-haness/.claude/hooks/spawn_bindings.py
T

116 lines
3.8 KiB
Python

"""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