123 lines
4.6 KiB
Python
123 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import os
|
|
import stat
|
|
from typing import Any
|
|
|
|
from .errors import DispatchError
|
|
from .securefs import (
|
|
atomic_write_bytes,
|
|
atomic_write_bytes_shared,
|
|
ensure_secure_directory,
|
|
ensure_shared_directory,
|
|
read_secure_bytes,
|
|
)
|
|
|
|
|
|
SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
|
|
|
|
|
|
class DispatchSink:
|
|
"""Reliable file-based dispatch adapter for existing OpenClaw/Codex watchers."""
|
|
|
|
def __init__(self, outbox_dir: Path, command_queue_dir: Path, contract_commit: str):
|
|
self.outbox_dir = Path(outbox_dir)
|
|
self.command_queue_dir = Path(command_queue_dir)
|
|
self.contract_commit = contract_commit
|
|
ensure_secure_directory(self.outbox_dir)
|
|
ensure_shared_directory(self.command_queue_dir)
|
|
|
|
def dispatch(self, record: dict[str, Any]) -> tuple[Path, Path]:
|
|
work_unit_id = record.get("work_unit_id")
|
|
event_id = record.get("event_id")
|
|
agent = record.get("assigned_agent")
|
|
payload_json = record.get("payload_json")
|
|
if (
|
|
not isinstance(work_unit_id, str)
|
|
or not SAFE_ID.fullmatch(work_unit_id)
|
|
or not isinstance(event_id, str)
|
|
or not SAFE_ID.fullmatch(event_id)
|
|
or agent not in {"openclaw", "codex"}
|
|
or not isinstance(payload_json, str)
|
|
):
|
|
raise DispatchError("invalid_dispatch_record")
|
|
try:
|
|
work_unit = json.loads(payload_json)
|
|
except json.JSONDecodeError as exc:
|
|
raise DispatchError("invalid_dispatch_payload") from exc
|
|
|
|
project_key = record.get("project_key")
|
|
state_version = record.get("state_version")
|
|
|
|
envelope = {
|
|
"schema_version": 1,
|
|
"contract_commit": self.contract_commit,
|
|
"event_id": event_id,
|
|
"assigned_agent": agent,
|
|
"work_unit": work_unit,
|
|
"callback": {
|
|
"event_endpoint": "/v1/orchestrator/events",
|
|
"start_event_type": "work_unit_started",
|
|
"end_event_type": "work_unit_completed",
|
|
},
|
|
}
|
|
outbox_path = self.outbox_dir / "dispatched" / f"{work_unit_id}.json"
|
|
if agent == "codex":
|
|
if (
|
|
not isinstance(project_key, str)
|
|
or not SAFE_ID.fullmatch(project_key)
|
|
or not isinstance(state_version, int)
|
|
or state_version < 0
|
|
):
|
|
raise DispatchError("invalid_dispatch_context")
|
|
required_checks = work_unit.get("required_checks", [])
|
|
execution_kind = (
|
|
"acceptance_probe"
|
|
if isinstance(required_checks, list) and "acceptance_probe" in required_checks
|
|
else "codex"
|
|
)
|
|
queue_value = {
|
|
"schema_version": 1,
|
|
"command_id": work_unit_id,
|
|
"project_key": project_key,
|
|
"work_unit_id": work_unit_id,
|
|
"contract_commit": self.contract_commit,
|
|
"state_version": state_version,
|
|
"execution": {
|
|
"kind": execution_kind,
|
|
"input": work_unit.get("instruction", ""),
|
|
},
|
|
}
|
|
# The Codex watcher polls the configured active directory itself.
|
|
queue_path = self.command_queue_dir / f"{work_unit_id}.json"
|
|
else:
|
|
queue_value = envelope
|
|
queue_path = self.command_queue_dir / agent / "pending" / f"{work_unit_id}.json"
|
|
ensure_secure_directory(outbox_path.parent)
|
|
ensure_shared_directory(queue_path.parent)
|
|
|
|
# Idempotent immutable payload: existing identical file is accepted.
|
|
self._write_idempotent_json(outbox_path, envelope, shared=False)
|
|
self._write_idempotent_json(queue_path, queue_value, shared=True)
|
|
return outbox_path, queue_path
|
|
|
|
@staticmethod
|
|
def _write_idempotent_json(path: Path, value: dict[str, Any], *, shared: bool) -> None:
|
|
payload = (json.dumps(value, indent=2, sort_keys=True) + "\n").encode()
|
|
try:
|
|
st = os.lstat(path)
|
|
except FileNotFoundError:
|
|
if shared:
|
|
atomic_write_bytes_shared(path, payload, mode=0o640)
|
|
else:
|
|
atomic_write_bytes(path, payload, mode=0o600)
|
|
return
|
|
if not stat.S_ISREG(st.st_mode):
|
|
raise DispatchError(f"dispatch_target_not_regular:{path.name}")
|
|
existing = read_secure_bytes(path, expected_uid=None, max_bytes=1_048_576)
|
|
if existing != payload:
|
|
raise DispatchError(f"dispatch_idempotency_conflict:{path.name}")
|