From 73567a664ce9e21c85db7e62729a7b297683befd Mon Sep 17 00:00:00 2001
From: John Osumi <931193+sumitake@users.noreply.github.com>
Date: Sun, 12 Jul 2026 19:51:13 -0700
Subject: [PATCH 1/9] runtime: add zero-idle provider broker client
---
plugins/agent-collab/host_policy.py | 6 +-
plugins/agent-collab/runtime_client.py | 950 ++++++++++++++++++++++
plugins/agent-collab/runtime_setup.py | 28 +-
tests/test_agent_collab_migration.py | 42 +-
tests/test_agent_collab_runtime_client.py | 506 +++++++++++-
tests/test_runtime_setup.py | 34 +-
6 files changed, 1538 insertions(+), 28 deletions(-)
diff --git a/plugins/agent-collab/host_policy.py b/plugins/agent-collab/host_policy.py
index 0aa0ae7..bef6816 100644
--- a/plugins/agent-collab/host_policy.py
+++ b/plugins/agent-collab/host_policy.py
@@ -31,6 +31,7 @@
PLUGIN_ROOT = Path(__file__).resolve().parent
KNOWN_FAMILIES = frozenset({"anthropic", "google", "openai", "xai", "zhipu"})
+DEFAULT_OPENCODE_MODEL = "opencode/glm-5.2"
ROUTE_ACTIONS = frozenset(
{
("gemini", "advisory"),
@@ -503,10 +504,7 @@ def _resolve_opencode_model(
configured = str(values.get("opencode_model", "")).strip()
if configured:
return configured
- environment = os.environ.get("AGENT_COLLAB_OPENCODE_MODEL", "").strip()
- if environment:
- return environment
- return str(row.get("model", "")).strip()
+ return DEFAULT_OPENCODE_MODEL
def _validate_row(
diff --git a/plugins/agent-collab/runtime_client.py b/plugins/agent-collab/runtime_client.py
index 6e441e4..bbbefaf 100644
--- a/plugins/agent-collab/runtime_client.py
+++ b/plugins/agent-collab/runtime_client.py
@@ -19,11 +19,14 @@
import json
import os
import platform
+import plistlib
import re
import selectors
import shutil
import signal
+import socket
import stat
+import struct
import subprocess
import sys
import tempfile
@@ -47,6 +50,41 @@
MAX_RESPONSE_BYTES = 4 * 1024 * 1024
MAX_ARTIFACT_BYTES = 64 * 1024 * 1024
MAX_TIMEOUT_MS = 600_000
+BROKER_PROTOCOL_VERSION = 1
+BROKER_LABEL = "com.agent-collab.provider-broker"
+BROKER_SOCKET_NAME = "ProviderBroker"
+BROKER_FRAME_KEYS = frozenset(
+ {
+ "broker_protocol_version",
+ "runtime_protocol_version",
+ "artifact_sha256",
+ "manifest_sha256",
+ "client_pid",
+ "nonce",
+ "deadline_monotonic_ms",
+ "request",
+ }
+)
+BROKERED_ROUTES = frozenset({"opencode", "gemini"})
+BROKER_MAX_REQUEST_BYTES = MAX_REQUEST_BYTES
+BROKER_MAX_RESPONSE_BYTES = MAX_RESPONSE_BYTES
+BROKER_STATE_MAX_BYTES = 64 * 1024
+BROKER_SUN_PATH_MAX_BYTES = 103
+BROKER_SYSTEM_PATH = "/usr/bin:/bin:/usr/sbin:/sbin"
+BROKER_STATE_KEYS = frozenset(
+ {
+ "schema_version",
+ "artifact_sha256",
+ "manifest_sha256",
+ "runtime_path",
+ "manifest_path",
+ "plist_sha256",
+ "socket_path",
+ "label",
+ "previous",
+ }
+)
+_BROKER_HEADER = struct.Struct(">Q")
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
_TEAM_ID_RE = re.compile(r"^[A-Z0-9]{10}$")
_CODESIGN_FLAGS_RE = re.compile(r"\bflags=(0x[0-9a-f]+)(?:\([^)]*\))?", re.IGNORECASE)
@@ -156,6 +194,7 @@ class RuntimeResolution:
path: Path | None = None
contracts: frozenset[tuple[str, str]] = frozenset()
manifest_digest: str = ""
+ artifact_digest: str = ""
identity: FileIdentity | None = None
error: str = ""
@@ -578,6 +617,7 @@ def resolve_runtime() -> RuntimeResolution:
path=path,
contracts=entry["_contracts"],
manifest_digest=manifest_digest,
+ artifact_digest=digest,
identity=identity,
)
@@ -625,6 +665,909 @@ def _scrubbed_env(*, tmpdir: Path) -> dict[str, str]:
return env
+def _broker_root() -> Path:
+ value = _operator_home()
+ if value is None:
+ raise ValueError("operator home is unavailable")
+ return Path(value) / ".agent-collab" / "provider-broker"
+
+
+def _exact_mode(path: Path, *, expected_type: int, mode: int) -> os.stat_result | None:
+ try:
+ info = path.lstat()
+ except OSError:
+ return None
+ if (
+ stat.S_IFMT(info.st_mode) != expected_type
+ or stat.S_ISLNK(info.st_mode)
+ or info.st_uid != os.getuid()
+ or (expected_type != stat.S_IFDIR and info.st_nlink != 1)
+ or stat.S_IMODE(info.st_mode) != mode
+ ):
+ return None
+ return info
+
+
+def _broker_record_valid(document: object, root: Path, *, allow_previous: bool) -> bool:
+ if not isinstance(document, dict) or set(document) != BROKER_STATE_KEYS:
+ return False
+ if (
+ not _exact_int(document.get("schema_version"), 1)
+ or not isinstance(document.get("artifact_sha256"), str)
+ or not _SHA256_RE.fullmatch(document["artifact_sha256"])
+ or not isinstance(document.get("manifest_sha256"), str)
+ or not _SHA256_RE.fullmatch(document["manifest_sha256"])
+ or not isinstance(document.get("plist_sha256"), str)
+ or not _SHA256_RE.fullmatch(document["plist_sha256"])
+ or document.get("label") != BROKER_LABEL
+ ):
+ return False
+ digest = document["artifact_sha256"]
+ expected_version = root / "versions" / digest
+ expected_paths = {
+ "runtime_path": expected_version / "agent-collab-runtime",
+ "manifest_path": expected_version / MANIFEST_NAME,
+ "socket_path": root / "provider.sock",
+ }
+ for key, expected in expected_paths.items():
+ if not isinstance(document.get(key), str) or document[key] != str(expected):
+ return False
+ previous = document.get("previous")
+ if previous is not None and (
+ not allow_previous or not _broker_record_valid(previous, root, allow_previous=False)
+ ):
+ return False
+ return True
+
+
+def _load_broker_state(
+ *,
+ artifact_digest: str,
+ manifest_digest: str,
+ require_socket: bool,
+) -> tuple[dict[str, Any] | None, RuntimeResult | None]:
+ try:
+ root = _broker_root()
+ except ValueError:
+ return None, RuntimeResult(RuntimeStatus.CONFIG_ERROR, error="provider broker configuration is unavailable")
+ state_path = root / "state.json"
+ try:
+ root.lstat()
+ except FileNotFoundError:
+ return None, RuntimeResult(RuntimeStatus.UNAVAILABLE, error="provider broker is not installed")
+ if _exact_mode(root, expected_type=stat.S_IFDIR, mode=0o700) is None:
+ return None, RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker root identity is unsafe")
+ try:
+ state_path.lstat()
+ except FileNotFoundError:
+ return None, RuntimeResult(RuntimeStatus.UNAVAILABLE, error="provider broker is not installed")
+ state_identity = _exact_mode(state_path, expected_type=stat.S_IFREG, mode=0o600)
+ if state_identity is None or state_identity.st_size > BROKER_STATE_MAX_BYTES:
+ return None, RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker state identity is unsafe")
+ raw, opened_identity = _read_regular_nofollow(state_path, limit=BROKER_STATE_MAX_BYTES)
+ if raw is None or opened_identity is None:
+ return None, RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker state cannot be read safely")
+ try:
+ document = json.loads(raw.decode("utf-8"))
+ except (UnicodeError, ValueError, RecursionError):
+ return None, RuntimeResult(RuntimeStatus.CONFIG_ERROR, error="provider broker state is malformed")
+ if not _broker_record_valid(document, root, allow_previous=True):
+ return None, RuntimeResult(RuntimeStatus.CONFIG_ERROR, error="provider broker state contract mismatch")
+ if (
+ document["artifact_sha256"] != artifact_digest
+ or document["manifest_sha256"] != manifest_digest
+ ):
+ return None, RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker digest does not match the verified runtime")
+ try:
+ _verify_published_version(
+ root,
+ artifact_digest=artifact_digest,
+ manifest_digest=manifest_digest,
+ )
+ _verify_plist_against_state(root, document)
+ except (OSError, ValueError):
+ return None, RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker installed identity could not be proven")
+ if require_socket:
+ socket_path = Path(document["socket_path"])
+ if len(os.fsencode(socket_path)) > BROKER_SUN_PATH_MAX_BYTES:
+ return None, RuntimeResult(RuntimeStatus.CONFIG_ERROR, error="provider broker socket path is too long")
+ if _exact_mode(socket_path, expected_type=stat.S_IFSOCK, mode=0o600) is None:
+ return None, RuntimeResult(RuntimeStatus.UNAVAILABLE, error="provider broker socket is unavailable")
+ return dict(document), None
+
+
+def _broker_request_frame(
+ *,
+ request: Mapping[str, Any],
+ artifact_digest: str,
+ manifest_digest: str,
+ timeout_ms: int,
+) -> dict[str, Any]:
+ if (
+ not isinstance(request, dict)
+ or not _SHA256_RE.fullmatch(artifact_digest)
+ or not _SHA256_RE.fullmatch(manifest_digest)
+ or not _exact_int(timeout_ms)
+ or not 1 <= timeout_ms <= MAX_TIMEOUT_MS
+ or os.getpid() <= 1
+ ):
+ raise ValueError("invalid provider broker request")
+ nonce = base64.urlsafe_b64encode(os.urandom(32)).decode("ascii").rstrip("=")
+ return {
+ "broker_protocol_version": BROKER_PROTOCOL_VERSION,
+ "runtime_protocol_version": PROTOCOL_VERSION,
+ "artifact_sha256": artifact_digest,
+ "manifest_sha256": manifest_digest,
+ "client_pid": os.getpid(),
+ "nonce": nonce,
+ "deadline_monotonic_ms": int(time.monotonic() * 1000) + timeout_ms,
+ "request": dict(request),
+ }
+
+
+def _encode_broker_frame(document: object, *, max_bytes: int) -> bytes:
+ if not isinstance(document, dict) or not _exact_int(max_bytes) or max_bytes <= 0:
+ raise ValueError("invalid broker frame")
+ try:
+ payload = json.dumps(
+ document,
+ sort_keys=True,
+ separators=(",", ":"),
+ ensure_ascii=True,
+ allow_nan=False,
+ ).encode("utf-8")
+ except (TypeError, ValueError, UnicodeError, RecursionError) as exc:
+ raise ValueError("invalid broker frame") from exc
+ if len(payload) > max_bytes:
+ raise OverflowError("broker frame exceeds limit")
+ return _BROKER_HEADER.pack(len(payload)) + payload
+
+
+def _recv_broker_exact(peer: socket.socket, count: int, deadline: float) -> bytes:
+ chunks: list[bytes] = []
+ remaining = count
+ while remaining:
+ budget = deadline - time.monotonic()
+ if budget <= 0:
+ raise TimeoutError("provider broker deadline expired")
+ peer.settimeout(budget)
+ try:
+ chunk = peer.recv(remaining)
+ except socket.timeout as exc:
+ raise TimeoutError("provider broker deadline expired") from exc
+ if not chunk:
+ raise ValueError("truncated broker frame")
+ chunks.append(chunk)
+ remaining -= len(chunk)
+ return b"".join(chunks)
+
+
+def _read_broker_frame(
+ peer: socket.socket, *, max_bytes: int, deadline: float
+) -> dict[str, Any]:
+ if not _exact_int(max_bytes) or max_bytes <= 0 or not isinstance(deadline, (int, float)):
+ raise ValueError("invalid broker frame reader")
+ header = _recv_broker_exact(peer, _BROKER_HEADER.size, deadline)
+ size = _BROKER_HEADER.unpack(header)[0]
+ if size == 0:
+ raise ValueError("empty broker frame")
+ if size > max_bytes:
+ raise OverflowError("broker frame exceeds limit")
+ raw = _recv_broker_exact(peer, size, deadline)
+ try:
+ document = json.loads(
+ raw.decode("utf-8"),
+ parse_constant=lambda _value: (_ for _ in ()).throw(ValueError()),
+ )
+ except (UnicodeError, ValueError, RecursionError) as exc:
+ raise ValueError("invalid broker response") from exc
+ if not isinstance(document, dict):
+ raise ValueError("invalid broker response")
+ return document
+
+
+def _parse_broker_response(document: dict[str, Any], envelope: object) -> RuntimeResult:
+ status = document.get("status")
+ local_mapping = {
+ "config_error": RuntimeStatus.CONFIG_ERROR,
+ "integrity_error": RuntimeStatus.INTEGRITY_ERROR,
+ "peer_error": RuntimeStatus.INTEGRITY_ERROR,
+ "replay_error": RuntimeStatus.INTEGRITY_ERROR,
+ "protocol_error": RuntimeStatus.PROTOCOL_ERROR,
+ }
+ if status in local_mapping:
+ if (
+ set(document) != {"protocol_version", "request_id", "status", "error"}
+ or not _exact_int(document.get("protocol_version"), PROTOCOL_VERSION)
+ or document.get("request_id") != envelope.request_id
+ or not isinstance(document.get("error"), str)
+ or len(document["error"].encode("utf-8")) > 4096
+ ):
+ return RuntimeResult(RuntimeStatus.PROTOCOL_ERROR, error="provider broker failure contract mismatch")
+ return RuntimeResult(local_mapping[status], error=document["error"])
+ encoded = json.dumps(
+ document,
+ sort_keys=True,
+ separators=(",", ":"),
+ ensure_ascii=True,
+ allow_nan=False,
+ ).encode("utf-8")
+ return _parse_response(encoded, envelope, 0)
+
+
+def _launch_broker(
+ *,
+ resolution: RuntimeResolution,
+ payload: bytes,
+ timeout_ms: int,
+ envelope: object,
+) -> RuntimeResult:
+ if not resolution.artifact_digest or not resolution.manifest_digest:
+ return RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="verified runtime digests are unavailable")
+ try:
+ request = json.loads(payload.decode("utf-8"))
+ except (UnicodeError, ValueError, RecursionError):
+ return RuntimeResult(RuntimeStatus.PROTOCOL_ERROR, error="sealed broker request is invalid")
+ state, error = _load_broker_state(
+ artifact_digest=resolution.artifact_digest,
+ manifest_digest=resolution.manifest_digest,
+ require_socket=True,
+ )
+ if error is not None or state is None:
+ return error or RuntimeResult(RuntimeStatus.UNAVAILABLE, error="provider broker is unavailable")
+ try:
+ frame = _broker_request_frame(
+ request=request,
+ artifact_digest=resolution.artifact_digest,
+ manifest_digest=resolution.manifest_digest,
+ timeout_ms=timeout_ms,
+ )
+ encoded = _encode_broker_frame(frame, max_bytes=BROKER_MAX_REQUEST_BYTES)
+ deadline = time.monotonic() + timeout_ms / 1000
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as peer:
+ peer.settimeout(timeout_ms / 1000)
+ peer.connect(state["socket_path"])
+ peer.sendall(encoded)
+ response = _read_broker_frame(
+ peer,
+ max_bytes=BROKER_MAX_RESPONSE_BYTES,
+ deadline=deadline,
+ )
+ return _parse_broker_response(response, envelope)
+ except TimeoutError:
+ return RuntimeResult(RuntimeStatus.TIMEOUT, error="provider broker deadline expired")
+ except OverflowError:
+ return RuntimeResult(RuntimeStatus.OUTPUT_LIMIT, error="provider broker frame exceeded the fixed limit")
+ except PermissionError:
+ return RuntimeResult(RuntimeStatus.HOST_BLOCKED, error="host blocked provider broker connection")
+ except (ConnectionRefusedError, FileNotFoundError):
+ return RuntimeResult(RuntimeStatus.UNAVAILABLE, error="provider broker is unavailable")
+ except (OSError, TypeError, ValueError):
+ return RuntimeResult(RuntimeStatus.PROTOCOL_ERROR, error="provider broker exchange failed")
+
+
+def _broker_plist_document(
+ *, runtime_path: Path, socket_path: Path, tmpdir: Path, home: Path, uid: int
+) -> dict[str, Any]:
+ if (
+ not all(path.is_absolute() for path in (runtime_path, socket_path, tmpdir, home))
+ or not _exact_int(uid)
+ or uid < 0
+ or len(os.fsencode(socket_path)) > BROKER_SUN_PATH_MAX_BYTES
+ ):
+ raise ValueError("invalid provider broker plist input")
+ runtime = str(runtime_path)
+ return {
+ "Label": BROKER_LABEL,
+ "Program": runtime,
+ "ProgramArguments": [runtime, "broker", "--protocol", str(BROKER_PROTOCOL_VERSION)],
+ "EnvironmentVariables": {
+ "HOME": str(home),
+ "TMPDIR": str(tmpdir),
+ "PATH": BROKER_SYSTEM_PATH,
+ "LANG": "C.UTF-8",
+ "LC_ALL": "C.UTF-8",
+ },
+ "ProcessType": "Background",
+ "ThrottleInterval": 0,
+ "Sockets": {
+ BROKER_SOCKET_NAME: {
+ "SockFamily": "Unix",
+ "SockType": "stream",
+ "SockPathName": str(socket_path),
+ "SockPathOwner": uid,
+ "SockPathMode": 0o600,
+ }
+ },
+ }
+
+
+def _ensure_private_directory(path: Path, *, mode: int) -> None:
+ try:
+ path.mkdir(mode=mode)
+ except FileExistsError:
+ pass
+ if _exact_mode(path, expected_type=stat.S_IFDIR, mode=mode) is None:
+ raise ValueError("unsafe provider broker directory")
+
+
+def _ensure_broker_layout() -> Path:
+ root = _broker_root()
+ if len(os.fsencode(root / "provider.sock")) > BROKER_SUN_PATH_MAX_BYTES:
+ raise ValueError("provider broker socket path is too long")
+ parent = root.parent
+ if not parent.exists():
+ parent.mkdir(mode=0o700)
+ parent_info = parent.lstat()
+ if (
+ not stat.S_ISDIR(parent_info.st_mode)
+ or stat.S_ISLNK(parent_info.st_mode)
+ or parent_info.st_uid != os.getuid()
+ or stat.S_IMODE(parent_info.st_mode) & 0o022
+ ):
+ raise ValueError("unsafe provider broker parent")
+ _ensure_private_directory(root, mode=0o700)
+ for name in ("versions", "tmp", "replay"):
+ _ensure_private_directory(root / name, mode=0o700)
+ return root
+
+
+def _fsync_directory(path: Path) -> None:
+ descriptor = os.open(path, os.O_RDONLY)
+ try:
+ os.fsync(descriptor)
+ finally:
+ os.close(descriptor)
+
+
+def _write_private_atomic(path: Path, content: bytes, *, mode: int) -> None:
+ if path.parent != _broker_root() or path.name not in {"broker.plist", "state.json"}:
+ raise ValueError("unsafe provider broker write target")
+ temporary = path.parent / "tmp" / f".{path.name}.{os.urandom(16).hex()}"
+ try:
+ descriptor = os.open(
+ temporary,
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
+ mode,
+ )
+ try:
+ view = memoryview(content)
+ while view:
+ written = os.write(descriptor, view)
+ if written <= 0:
+ raise OSError("provider broker write made no progress")
+ view = view[written:]
+ os.fsync(descriptor)
+ os.fchmod(descriptor, mode)
+ finally:
+ os.close(descriptor)
+ os.replace(temporary, path)
+ os.chmod(path, mode, follow_symlinks=False)
+ _fsync_directory(path.parent)
+ except BaseException:
+ try:
+ temporary.unlink()
+ except OSError:
+ pass
+ raise
+
+
+def _copy_regular_nofollow(source: Path, target: Path, *, limit: int, mode: int) -> None:
+ raw, identity = _read_regular_nofollow(source, limit=limit)
+ if raw is None or identity is None:
+ raise ValueError("verified provider runtime source became unsafe")
+ descriptor = os.open(
+ target,
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
+ mode,
+ )
+ try:
+ view = memoryview(raw)
+ while view:
+ written = os.write(descriptor, view)
+ if written <= 0:
+ raise OSError("provider broker copy made no progress")
+ view = view[written:]
+ os.fsync(descriptor)
+ os.fchmod(descriptor, mode)
+ finally:
+ os.close(descriptor)
+
+
+def _verify_published_version(
+ root: Path, *, artifact_digest: str, manifest_digest: str
+) -> tuple[Path, Path]:
+ version = root / "versions" / artifact_digest
+ runtime = version / "agent-collab-runtime"
+ manifest = version / MANIFEST_NAME
+ if _exact_mode(version, expected_type=stat.S_IFDIR, mode=0o500) is None:
+ raise ValueError("provider broker version directory is unsafe")
+ runtime_identity = _exact_mode(runtime, expected_type=stat.S_IFREG, mode=0o500)
+ manifest_identity = _exact_mode(manifest, expected_type=stat.S_IFREG, mode=0o400)
+ if runtime_identity is None or manifest_identity is None:
+ raise ValueError("provider broker version files are unsafe")
+ observed_runtime = _sha256_regular(
+ runtime,
+ FileIdentity(
+ device=runtime_identity.st_dev,
+ inode=runtime_identity.st_ino,
+ size=runtime_identity.st_size,
+ mtime_ns=runtime_identity.st_mtime_ns,
+ mode=runtime_identity.st_mode,
+ uid=runtime_identity.st_uid,
+ links=runtime_identity.st_nlink,
+ ),
+ )
+ raw_manifest, _identity = _read_regular_nofollow(manifest, limit=1024 * 1024)
+ if (
+ observed_runtime != artifact_digest
+ or raw_manifest is None
+ or hashlib.sha256(raw_manifest).hexdigest() != manifest_digest
+ ):
+ raise ValueError("provider broker version digest mismatch")
+ return runtime, manifest
+
+
+def _publish_broker_version(
+ root: Path, *, resolution: RuntimeResolution
+) -> tuple[Path, Path]:
+ if resolution.path is None or resolution.identity is None:
+ raise ValueError("verified provider runtime is unavailable")
+ if _safe_file_identity(resolution.path, executable=True) != resolution.identity:
+ raise ValueError("verified provider runtime identity changed")
+ version = root / "versions" / resolution.artifact_digest
+ if version.exists():
+ return _verify_published_version(
+ root,
+ artifact_digest=resolution.artifact_digest,
+ manifest_digest=resolution.manifest_digest,
+ )
+ staging = root / "tmp" / f"version-{resolution.artifact_digest}-{os.urandom(8).hex()}"
+ staging.mkdir(mode=0o700)
+ try:
+ _copy_regular_nofollow(
+ resolution.path,
+ staging / "agent-collab-runtime",
+ limit=MAX_ARTIFACT_BYTES,
+ mode=0o500,
+ )
+ _copy_regular_nofollow(
+ PLUGIN_ROOT / MANIFEST_NAME,
+ staging / MANIFEST_NAME,
+ limit=1024 * 1024,
+ mode=0o400,
+ )
+ _fsync_directory(staging)
+ os.rename(staging, version)
+ os.chmod(version, 0o500)
+ _fsync_directory(root / "versions")
+ except BaseException:
+ if staging.exists():
+ os.chmod(staging, 0o700)
+ for child in staging.iterdir():
+ child.unlink()
+ staging.rmdir()
+ raise
+ return _verify_published_version(
+ root,
+ artifact_digest=resolution.artifact_digest,
+ manifest_digest=resolution.manifest_digest,
+ )
+
+
+def _plist_bytes(document: Mapping[str, Any]) -> bytes:
+ raw = plistlib.dumps(dict(document), fmt=plistlib.FMT_XML, sort_keys=True)
+ if plistlib.loads(raw) != dict(document):
+ raise ValueError("provider broker plist roundtrip failed")
+ return raw
+
+
+def _state_bytes(document: Mapping[str, Any]) -> bytes:
+ return (
+ json.dumps(
+ dict(document),
+ sort_keys=True,
+ separators=(",", ":"),
+ ensure_ascii=True,
+ allow_nan=False,
+ ).encode("utf-8")
+ + b"\n"
+ )
+
+
+def _read_current_broker_state(root: Path) -> dict[str, Any] | None:
+ if _exact_mode(root, expected_type=stat.S_IFDIR, mode=0o700) is None:
+ raise ValueError("provider broker root identity is unsafe")
+ state_path = root / "state.json"
+ if not state_path.exists():
+ return None
+ if _exact_mode(state_path, expected_type=stat.S_IFREG, mode=0o600) is None:
+ raise ValueError("provider broker state identity is unsafe")
+ raw, _identity = _read_regular_nofollow(state_path, limit=BROKER_STATE_MAX_BYTES)
+ if raw is None:
+ raise ValueError("provider broker state cannot be read safely")
+ try:
+ document = json.loads(raw.decode("utf-8"))
+ except (UnicodeError, ValueError, RecursionError) as exc:
+ raise ValueError("provider broker state is malformed") from exc
+ if not _broker_record_valid(document, root, allow_previous=True):
+ raise ValueError("provider broker state contract mismatch")
+ return dict(document)
+
+
+def _without_previous(document: Mapping[str, Any]) -> dict[str, Any]:
+ result = dict(document)
+ result["previous"] = None
+ return result
+
+
+def _launchctl(arguments: list[str], *, timeout: int = 20) -> subprocess.CompletedProcess[str]:
+ home = _operator_home()
+ if home is None:
+ raise ValueError("operator home is unavailable")
+ command = ["/bin/launchctl", *arguments]
+ process = subprocess.Popen(
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ cwd="/",
+ env={
+ "HOME": home,
+ "PATH": BROKER_SYSTEM_PATH,
+ "LANG": "C.UTF-8",
+ "LC_ALL": "C.UTF-8",
+ },
+ start_new_session=True,
+ close_fds=True,
+ )
+ out, err, collection_error = _collect_bounded_output(
+ process, timeout_ms=timeout * 1000
+ )
+ if collection_error is not None:
+ raise RuntimeError(collection_error.error)
+ return subprocess.CompletedProcess(
+ command,
+ process.returncode if process.returncode is not None else -1,
+ out.decode("utf-8", errors="replace"),
+ err.decode("utf-8", errors="replace"),
+ )
+
+
+def _bootout_broker(plist_path: Path) -> bool:
+ result = _launchctl(["bootout", f"gui/{os.getuid()}", str(plist_path)])
+ if result.returncode == 0:
+ return True
+ detail = (result.stdout + result.stderr).casefold()
+ return any(
+ marker in detail
+ for marker in ("could not find service", "no such process", "not found")
+ )
+
+
+def _bootstrap_broker(plist_path: Path) -> bool:
+ return _launchctl(
+ ["bootstrap", f"gui/{os.getuid()}", str(plist_path)]
+ ).returncode == 0
+
+
+def _broker_job_loaded() -> bool:
+ return _launchctl(
+ ["print", f"gui/{os.getuid()}/{BROKER_LABEL}"]
+ ).returncode == 0
+
+
+def _broker_ping(socket_path: Path) -> bool:
+ try:
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as peer:
+ peer.settimeout(5.0)
+ peer.connect(str(socket_path))
+ return _wait_for_broker_exit()
+ except OSError:
+ return False
+
+
+def _broker_process_idle() -> bool:
+ result = _launchctl(["print", f"gui/{os.getuid()}/{BROKER_LABEL}"])
+ if result.returncode != 0:
+ return False
+ states = [
+ line.split("=", 1)[1].strip().casefold()
+ for line in result.stdout.splitlines()
+ if line.strip().startswith("state =")
+ ]
+ has_live_pid = any(
+ line.strip().startswith("pid =")
+ and line.split("=", 1)[1].strip().isdigit()
+ and int(line.split("=", 1)[1].strip()) > 0
+ for line in result.stdout.splitlines()
+ )
+ return bool(states) and not has_live_pid and all(
+ state in {"not running", "exited"} for state in states
+ )
+
+
+def _wait_for_broker_exit() -> bool:
+ deadline = time.monotonic() + 5.0
+ while time.monotonic() < deadline:
+ if _broker_process_idle():
+ return True
+ time.sleep(0.05)
+ return False
+
+
+def _record_for(
+ *,
+ root: Path,
+ artifact_digest: str,
+ manifest_digest: str,
+ plist_digest: str,
+ previous: Mapping[str, Any] | None,
+) -> dict[str, Any]:
+ version = root / "versions" / artifact_digest
+ return {
+ "schema_version": 1,
+ "artifact_sha256": artifact_digest,
+ "manifest_sha256": manifest_digest,
+ "runtime_path": str(version / "agent-collab-runtime"),
+ "manifest_path": str(version / MANIFEST_NAME),
+ "plist_sha256": plist_digest,
+ "socket_path": str(root / "provider.sock"),
+ "label": BROKER_LABEL,
+ "previous": None if previous is None else _without_previous(previous),
+ }
+
+
+def _verify_plist_against_state(root: Path, state: Mapping[str, Any]) -> None:
+ plist_path = root / "broker.plist"
+ identity = _exact_mode(plist_path, expected_type=stat.S_IFREG, mode=0o600)
+ raw, _opened = _read_regular_nofollow(plist_path, limit=1024 * 1024)
+ if identity is None or raw is None or hashlib.sha256(raw).hexdigest() != state["plist_sha256"]:
+ raise ValueError("provider broker plist identity mismatch")
+ try:
+ document = plistlib.loads(raw)
+ except (plistlib.InvalidFileException, ValueError) as exc:
+ raise ValueError("provider broker plist is malformed") from exc
+ expected = _broker_plist_document(
+ runtime_path=Path(state["runtime_path"]),
+ socket_path=root / "provider.sock",
+ tmpdir=root / "tmp",
+ home=Path(_operator_home() or ""),
+ uid=os.getuid(),
+ )
+ if document != expected:
+ raise ValueError("provider broker plist contract mismatch")
+
+
+def _activate_broker_record(
+ root: Path,
+ *,
+ target_artifact: str,
+ target_manifest: str,
+ previous: Mapping[str, Any] | None,
+) -> RuntimeResult:
+ runtime, _manifest = _verify_published_version(
+ root,
+ artifact_digest=target_artifact,
+ manifest_digest=target_manifest,
+ )
+ home = _operator_home()
+ if home is None:
+ return RuntimeResult(RuntimeStatus.CONFIG_ERROR, error="operator home is unavailable")
+ plist_document = _broker_plist_document(
+ runtime_path=runtime,
+ socket_path=root / "provider.sock",
+ tmpdir=root / "tmp",
+ home=Path(home),
+ uid=os.getuid(),
+ )
+ plist_raw = _plist_bytes(plist_document)
+ record = _record_for(
+ root=root,
+ artifact_digest=target_artifact,
+ manifest_digest=target_manifest,
+ plist_digest=hashlib.sha256(plist_raw).hexdigest(),
+ previous=previous,
+ )
+ plist_path = root / "broker.plist"
+ old_record = None if previous is None else _without_previous(previous)
+ try:
+ if old_record is not None and not _bootout_broker(plist_path):
+ raise RuntimeError("existing provider broker could not be stopped")
+ if old_record is None and plist_path.exists():
+ raise RuntimeError("untracked provider broker plist exists")
+ _write_private_atomic(plist_path, plist_raw, mode=0o600)
+ _write_private_atomic(root / "state.json", _state_bytes(record), mode=0o600)
+ if not _bootstrap_broker(plist_path):
+ raise RuntimeError("provider broker could not be bootstrapped")
+ if not _broker_job_loaded():
+ raise RuntimeError("provider broker launchd identity was not observed")
+ socket_path = root / "provider.sock"
+ if _exact_mode(socket_path, expected_type=stat.S_IFSOCK, mode=0o600) is None:
+ raise RuntimeError("provider broker socket identity was not observed")
+ if not _broker_ping(socket_path):
+ raise RuntimeError("provider broker activation ping failed")
+ observed_state, observed_error = _load_broker_state(
+ artifact_digest=target_artifact,
+ manifest_digest=target_manifest,
+ require_socket=True,
+ )
+ if observed_error is not None or observed_state != record:
+ raise RuntimeError("provider broker activation state could not be read back")
+ return RuntimeResult(
+ RuntimeStatus.OK,
+ result={
+ "installed": True,
+ "artifact_sha256": target_artifact,
+ "manifest_sha256": target_manifest,
+ "socket_activated": True,
+ "persistent_process": False,
+ },
+ )
+ except (OSError, RuntimeError, subprocess.SubprocessError, ValueError):
+ restored = False
+ try:
+ _bootout_broker(plist_path)
+ if old_record is not None:
+ prior_runtime, _prior_manifest = _verify_published_version(
+ root,
+ artifact_digest=old_record["artifact_sha256"],
+ manifest_digest=old_record["manifest_sha256"],
+ )
+ prior_document = _broker_plist_document(
+ runtime_path=prior_runtime,
+ socket_path=root / "provider.sock",
+ tmpdir=root / "tmp",
+ home=Path(home),
+ uid=os.getuid(),
+ )
+ prior_raw = _plist_bytes(prior_document)
+ if hashlib.sha256(prior_raw).hexdigest() != old_record["plist_sha256"]:
+ raise ValueError("prior provider broker plist digest mismatch")
+ _write_private_atomic(plist_path, prior_raw, mode=0o600)
+ if not _bootstrap_broker(plist_path) or not _broker_job_loaded():
+ raise RuntimeError("prior provider broker could not be restored")
+ _write_private_atomic(
+ root / "state.json", _state_bytes(old_record), mode=0o600
+ )
+ restored = True
+ else:
+ for candidate in (root / "state.json", plist_path, root / "provider.sock"):
+ try:
+ candidate.unlink()
+ except FileNotFoundError:
+ pass
+ except (OSError, RuntimeError, subprocess.SubprocessError, ValueError):
+ restored = False
+ return RuntimeResult(
+ RuntimeStatus.PROVIDER_ERROR,
+ result={"restored_previous": restored},
+ error=(
+ "provider broker update failed; previous version restored"
+ if restored
+ else "provider broker update failed; no active version is proven"
+ ),
+ )
+
+
+def install_broker() -> RuntimeResult:
+ resolution = resolve_runtime()
+ if resolution.status is not RuntimeStatus.OK:
+ return RuntimeResult(resolution.status, error=resolution.error)
+ try:
+ root = _ensure_broker_layout()
+ current = _read_current_broker_state(root)
+ if current is not None:
+ _verify_plist_against_state(root, current)
+ _publish_broker_version(root, resolution=resolution)
+ return _activate_broker_record(
+ root,
+ target_artifact=resolution.artifact_digest,
+ target_manifest=resolution.manifest_digest,
+ previous=current,
+ )
+ except (OSError, subprocess.SubprocessError, ValueError):
+ return RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker installation preflight failed")
+
+
+def broker_status() -> RuntimeResult:
+ try:
+ root = _broker_root()
+ if not root.exists():
+ return RuntimeResult(RuntimeStatus.UNAVAILABLE, result={"installed": False}, error="provider broker is not installed")
+ if _exact_mode(root, expected_type=stat.S_IFDIR, mode=0o700) is None:
+ raise ValueError("unsafe provider broker root")
+ state = _read_current_broker_state(root)
+ if state is None:
+ return RuntimeResult(RuntimeStatus.UNAVAILABLE, result={"installed": False}, error="provider broker is not installed")
+ _verify_published_version(
+ root,
+ artifact_digest=state["artifact_sha256"],
+ manifest_digest=state["manifest_sha256"],
+ )
+ _verify_plist_against_state(root, state)
+ socket_valid = _exact_mode(
+ root / "provider.sock", expected_type=stat.S_IFSOCK, mode=0o600
+ ) is not None
+ loaded = _broker_job_loaded()
+ status = RuntimeStatus.OK if loaded and socket_valid else RuntimeStatus.UNAVAILABLE
+ return RuntimeResult(
+ status,
+ result={
+ "installed": True,
+ "active": loaded and socket_valid,
+ "launchd_job": loaded,
+ "socket": socket_valid,
+ "artifact_sha256": state["artifact_sha256"],
+ "manifest_sha256": state["manifest_sha256"],
+ "rollback_available": state["previous"] is not None,
+ "persistent_process": False,
+ },
+ error="" if status is RuntimeStatus.OK else "provider broker is installed but inactive",
+ )
+ except (OSError, subprocess.SubprocessError, ValueError):
+ return RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker status could not be proven")
+
+
+def rollback_broker() -> RuntimeResult:
+ try:
+ root = _broker_root()
+ state = _read_current_broker_state(root)
+ if state is None or state["previous"] is None:
+ return RuntimeResult(RuntimeStatus.UNAVAILABLE, error="provider broker rollback is unavailable")
+ _verify_plist_against_state(root, state)
+ previous = dict(state["previous"])
+ return _activate_broker_record(
+ root,
+ target_artifact=previous["artifact_sha256"],
+ target_manifest=previous["manifest_sha256"],
+ previous=_without_previous(state),
+ )
+ except (OSError, subprocess.SubprocessError, ValueError):
+ return RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker rollback preflight failed")
+
+
+def uninstall_broker() -> RuntimeResult:
+ try:
+ root = _broker_root()
+ state = _read_current_broker_state(root)
+ if state is None:
+ return RuntimeResult(RuntimeStatus.OK, result={"installed": False, "versions_retained": True})
+ _verify_plist_against_state(root, state)
+ plist_path = root / "broker.plist"
+ if not _bootout_broker(plist_path):
+ return RuntimeResult(RuntimeStatus.PROVIDER_ERROR, error="provider broker could not be stopped")
+ socket_path = root / "provider.sock"
+ try:
+ socket_path.lstat()
+ except FileNotFoundError:
+ pass
+ else:
+ if _exact_mode(
+ socket_path, expected_type=stat.S_IFSOCK, mode=0o600
+ ) is None:
+ raise ValueError("unsafe provider broker socket")
+ for candidate, expected_type, mode in (
+ (socket_path, stat.S_IFSOCK, 0o600),
+ (root / "state.json", stat.S_IFREG, 0o600),
+ (plist_path, stat.S_IFREG, 0o600),
+ ):
+ try:
+ candidate.lstat()
+ except FileNotFoundError:
+ continue
+ else:
+ if _exact_mode(candidate, expected_type=expected_type, mode=mode) is None:
+ raise ValueError("unsafe provider broker mutable state")
+ candidate.unlink()
+ return RuntimeResult(
+ RuntimeStatus.OK,
+ result={"installed": False, "versions_retained": True},
+ )
+ except (OSError, subprocess.SubprocessError, ValueError):
+ return RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker uninstall preflight failed")
+
+
class _RuntimeTempCleanupError(RuntimeError):
"""Raised when an invocation's isolated temporary directory cannot be removed."""
@@ -1127,6 +2070,13 @@ def invoke(*, envelope: object) -> RuntimeResult:
return RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="runtime manifest changed after policy selection")
if (envelope.route, envelope.action) not in resolution.contracts:
return RuntimeResult(RuntimeStatus.UNAVAILABLE, error="native runtime does not advertise the sealed route/action")
+ if envelope.route in BROKERED_ROUTES:
+ return _launch_broker(
+ resolution=resolution,
+ payload=payload,
+ timeout_ms=envelope.timeout_ms,
+ envelope=envelope,
+ )
return _launch_runtime(
resolution=resolution,
payload=payload,
diff --git a/plugins/agent-collab/runtime_setup.py b/plugins/agent-collab/runtime_setup.py
index 47b9d3b..13e273e 100644
--- a/plugins/agent-collab/runtime_setup.py
+++ b/plugins/agent-collab/runtime_setup.py
@@ -15,6 +15,12 @@
"prepare": ("prepare", 60_000),
"login-grok": ("grok_login", 600_000),
}
+_LIFECYCLE_COMMANDS = {
+ "install-broker": "install_broker",
+ "broker-status": "broker_status",
+ "rollback-broker": "rollback_broker",
+ "uninstall-broker": "uninstall_broker",
+}
def _emit(payload: dict[str, object]) -> None:
@@ -26,20 +32,26 @@ def _emit(payload: dict[str, object]) -> None:
def main(argv: list[str] | None = None) -> int:
args = list(sys.argv[1:] if argv is None else argv)
- if len(args) != 1 or args[0] not in _COMMANDS:
+ if len(args) != 1 or args[0] not in {*_COMMANDS, *_LIFECYCLE_COMMANDS}:
_emit(
{
"status": runtime_client.RuntimeStatus.CONFIG_ERROR.value,
- "error": "expected exactly one of: status, prepare, login-grok",
+ "error": (
+ "expected exactly one of: status, prepare, login-grok, "
+ "install-broker, broker-status, rollback-broker, uninstall-broker"
+ ),
}
)
return 2
- action, timeout_ms = _COMMANDS[args[0]]
- result = runtime_client.manage_runtime(
- action=action,
- request_id=f"setup-{uuid.uuid4().hex}",
- timeout_ms=timeout_ms,
- )
+ if args[0] in _LIFECYCLE_COMMANDS:
+ result = getattr(runtime_client, _LIFECYCLE_COMMANDS[args[0]])()
+ else:
+ action, timeout_ms = _COMMANDS[args[0]]
+ result = runtime_client.manage_runtime(
+ action=action,
+ request_id=f"setup-{uuid.uuid4().hex}",
+ timeout_ms=timeout_ms,
+ )
payload: dict[str, object] = {"status": result.status.value}
if result.result is not None:
payload["result"] = dict(result.result)
diff --git a/tests/test_agent_collab_migration.py b/tests/test_agent_collab_migration.py
index 63caccb..d7f1322 100644
--- a/tests/test_agent_collab_migration.py
+++ b/tests/test_agent_collab_migration.py
@@ -1229,7 +1229,7 @@ def test_policy_envelope_preserves_automatic_target_provenance(self) -> None:
self.assertFalse(outcome.envelope.explicit_target)
self.assertTrue(self.policy.verify_policy_envelope(outcome.envelope))
- def test_opencode_target_has_no_implicit_glm_default(self) -> None:
+ def test_opencode_target_uses_the_fixed_glm_preset_by_default(self) -> None:
with mock.patch.object(
self.policy,
"_runtime_contracts",
@@ -1250,8 +1250,44 @@ def test_opencode_target_has_no_implicit_glm_default(self) -> None:
},
row_config={"cwd": "/tmp/project"},
)
- self.assertEqual(outcome.status, self.policy.PreflightStatus.UNKNOWN_BLOCKED)
- self.assertIn("OpenCode model", outcome.warning)
+ self.assertEqual(outcome.status, self.policy.PreflightStatus.OK)
+ self.assertIsNotNone(outcome.envelope)
+ assert outcome.envelope is not None
+ row = json.loads(outcome.envelope.row_json)
+ self.assertEqual(row["model"], self.policy.DEFAULT_OPENCODE_MODEL)
+ self.assertEqual(outcome.envelope.target_author_family, "zhipu")
+
+ def test_opencode_ignores_ambient_and_row_model_fallbacks(self) -> None:
+ with mock.patch.object(
+ self.policy,
+ "_runtime_contracts",
+ return_value=(frozenset({("opencode", "plan")}), "digest-1"),
+ ), mock.patch.dict(
+ "os.environ",
+ {"AGENT_COLLAB_OPENCODE_MODEL": "google/gemini-ambient"},
+ clear=True,
+ ):
+ outcome = self.policy.issue_policy_envelope(
+ request_id="opencode-closed-precedence",
+ route="opencode",
+ action="plan",
+ governance=False,
+ prompt="plan",
+ timeout_ms=30_000,
+ explicit_config={
+ "primary_id": "claude",
+ "active_model": "anthropic/claude-opus",
+ "host_runtime": "claude-code",
+ "session_identifier": "c-1",
+ },
+ row_config={"model": "openai/row-model", "cwd": "/tmp/project"},
+ )
+ self.assertEqual(outcome.status, self.policy.PreflightStatus.OK)
+ assert outcome.envelope is not None
+ self.assertEqual(
+ json.loads(outcome.envelope.row_json)["model"],
+ self.policy.DEFAULT_OPENCODE_MODEL,
+ )
def test_opencode_worker_reobserves_explicit_selected_model_on_each_call(self) -> None:
cases = {
diff --git a/tests/test_agent_collab_runtime_client.py b/tests/test_agent_collab_runtime_client.py
index bf8bc19..d278643 100644
--- a/tests/test_agent_collab_runtime_client.py
+++ b/tests/test_agent_collab_runtime_client.py
@@ -8,12 +8,16 @@
import importlib.util
import json
import os
+import plistlib
import pwd
import signal
+import socket
import stat
+import struct
import subprocess
import sys
import tempfile
+import threading
import time
import unittest
from dataclasses import replace
@@ -235,6 +239,7 @@ def _envelope(
governance: bool = False,
artifact_model: str = "",
timeout_ms: int = 30_000,
+ opencode_model: str = "",
):
defaults: dict[tuple[str, str], dict[str, object]] = {
("gemini", "advisory"): {"model": "google/gemini-test", "effort": "high"},
@@ -269,6 +274,14 @@ def _envelope(
with mock.patch.object(
self.client, "_verify_macos_signature", return_value=(True, "")
):
+ explicit = {
+ "primary_id": "claude",
+ "active_model": "anthropic/claude-opus",
+ "host_runtime": "claude-code",
+ "session_identifier": "c-1",
+ }
+ if opencode_model:
+ explicit["opencode_model"] = opencode_model
decision = policy.issue_policy_envelope(
request_id=request_id,
route=route,
@@ -278,17 +291,22 @@ def _envelope(
timeout_ms=timeout_ms,
artifact_author_model=artifact_model,
artifact_content="artifact" if artifact_model else "",
- explicit_config={
- "primary_id": "claude",
- "active_model": "anthropic/claude-opus",
- "host_runtime": "claude-code",
- "session_identifier": "c-1",
- },
+ explicit_config=explicit,
row_config=row if row is not None else defaults[(route, action)],
)
self.assertIsNotNone(decision.envelope, decision.warning)
return decision.envelope
+ def _fixture_broker(self, **kwargs):
+ """Exercise native response parsing while substituting the test broker transport."""
+
+ return self.client._launch_runtime(
+ resolution=kwargs["resolution"],
+ payload=kwargs["payload"],
+ timeout_ms=kwargs["timeout_ms"],
+ envelope=kwargs["envelope"],
+ )
+
def test_source_manifest_never_contains_unsigned_placeholder(self) -> None:
plugin_root = ROOT / "plugins" / "agent-collab"
manifest = json.loads(
@@ -360,6 +378,440 @@ def test_valid_fixture_launches_fixed_protocol_with_scrubbed_env(self) -> None:
self.assertEqual(result.result["tmpdir_mode"], "0o700")
self.assertFalse(child_tmpdir.exists())
+ def test_resolution_carries_the_verified_artifact_digest(self) -> None:
+ binary = self._fixture()
+ expected = hashlib.sha256(binary.read_bytes()).hexdigest()
+ with mock.patch.object(
+ self.client, "_verify_macos_signature", return_value=(True, "")
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root):
+ result = self.client.resolve_runtime()
+ self.assertEqual(result.status, self.client.RuntimeStatus.OK)
+ self.assertEqual(result.artifact_digest, expected)
+
+ def test_broker_frame_is_exact_digest_bound_and_uses_canonical_nonce(self) -> None:
+ request = {
+ "protocol_version": 1,
+ "request_id": "broker-frame-1",
+ "operation": "execute",
+ }
+ now = 1_234.5
+ nonce = bytes(range(32))
+ with mock.patch.object(self.client.time, "monotonic", return_value=now), mock.patch.object(
+ self.client.os, "urandom", return_value=nonce
+ ):
+ frame = self.client._broker_request_frame(
+ request=request,
+ artifact_digest="a" * 64,
+ manifest_digest="b" * 64,
+ timeout_ms=30_000,
+ )
+ self.assertEqual(set(frame), self.client.BROKER_FRAME_KEYS)
+ self.assertEqual(frame["broker_protocol_version"], 1)
+ self.assertEqual(frame["runtime_protocol_version"], 1)
+ self.assertEqual(frame["artifact_sha256"], "a" * 64)
+ self.assertEqual(frame["manifest_sha256"], "b" * 64)
+ self.assertEqual(frame["client_pid"], os.getpid())
+ self.assertEqual(frame["deadline_monotonic_ms"], 1_264_500)
+ self.assertEqual(
+ frame["nonce"],
+ base64.urlsafe_b64encode(nonce).decode("ascii").rstrip("="),
+ )
+ self.assertEqual(frame["request"], request)
+
+ def test_broker_frame_codec_rejects_nan_bool_and_oversize(self) -> None:
+ with self.assertRaises(ValueError):
+ self.client._encode_broker_frame({"bad": float("nan")}, max_bytes=1024)
+ with self.assertRaises(ValueError):
+ self.client._encode_broker_frame({"bad": True}, max_bytes=True)
+ with self.assertRaises(OverflowError):
+ self.client._encode_broker_frame({"large": "x" * 50}, max_bytes=8)
+
+ def test_broker_frame_reader_rejects_truncated_and_oversize_payloads(self) -> None:
+ left, right = socket.socketpair()
+ self.addCleanup(left.close)
+ self.addCleanup(right.close)
+ right.sendall(struct.pack(">Q", 20) + b"{}")
+ right.close()
+ with self.assertRaises(ValueError):
+ self.client._read_broker_frame(left, max_bytes=1024, deadline=time.monotonic() + 1)
+
+ left, right = socket.socketpair()
+ self.addCleanup(left.close)
+ self.addCleanup(right.close)
+ right.sendall(struct.pack(">Q", 1025))
+ with self.assertRaises(OverflowError):
+ self.client._read_broker_frame(left, max_bytes=1024, deadline=time.monotonic() + 1)
+
+ left, right = socket.socketpair()
+ self.addCleanup(left.close)
+ self.addCleanup(right.close)
+ raw = b'{"value":NaN}'
+ right.sendall(struct.pack(">Q", len(raw)) + raw)
+ with self.assertRaises(ValueError):
+ self.client._read_broker_frame(left, max_bytes=1024, deadline=time.monotonic() + 1)
+
+ def test_opencode_and_gemini_use_broker_without_direct_fallback(self) -> None:
+ self._fixture()
+ for route, action in (("opencode", "plan"), ("gemini", "advisory")):
+ with self.subTest(route=route):
+ envelope = self._envelope(route=route, action=action)
+ expected = self.client.RuntimeResult(
+ self.client.RuntimeStatus.UNAVAILABLE,
+ error="provider broker is unavailable",
+ )
+ with mock.patch.object(
+ self.client, "_verify_macos_signature", return_value=(True, "")
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), mock.patch.object(
+ self.client, "_launch_broker", return_value=expected
+ ) as broker, mock.patch.object(self.client, "_launch_runtime") as direct:
+ result = self.client.invoke(envelope=envelope)
+ self.assertIs(result, expected)
+ broker.assert_called_once()
+ direct.assert_not_called()
+
+ def test_other_routes_retain_direct_runtime_path(self) -> None:
+ self._fixture()
+ for route, action in (
+ ("codex", "advisory"),
+ ("grok", "architecture"),
+ ("composer", "codegen"),
+ ):
+ with self.subTest(route=route):
+ envelope = self._envelope(route=route, action=action)
+ expected = self.client.RuntimeResult(
+ self.client.RuntimeStatus.PROVIDER_ERROR,
+ error="fixture direct result",
+ )
+ with mock.patch.object(
+ self.client, "_verify_macos_signature", return_value=(True, "")
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), mock.patch.object(
+ self.client, "_launch_runtime", return_value=expected
+ ) as direct, mock.patch.object(self.client, "_launch_broker") as broker:
+ result = self.client.invoke(envelope=envelope)
+ self.assertIs(result, expected)
+ direct.assert_called_once()
+ broker.assert_not_called()
+
+ def test_broker_exchange_roundtrips_a_sealed_gemini_response(self) -> None:
+ self._fixture()
+ socket_path = self.root / "exchange.sock"
+ listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ listener.bind(str(socket_path))
+ listener.listen(1)
+ socket_path.chmod(0o600)
+ self.addCleanup(listener.close)
+ envelope = self._envelope(
+ route="gemini", action="advisory", request_id="broker-exchange-1"
+ )
+ payload = self.client._native_document(envelope)
+ observed: dict[str, object] = {}
+
+ def server() -> None:
+ peer, _address = listener.accept()
+ with peer:
+ frame = self.client._read_broker_frame(
+ peer,
+ max_bytes=self.client.BROKER_MAX_REQUEST_BYTES,
+ deadline=time.monotonic() + 2,
+ )
+ observed.update(frame)
+ response = {
+ "protocol_version": 1,
+ "request_id": envelope.request_id,
+ "status": "ok",
+ "result": {"review": "ok"},
+ "provenance": {
+ "route": envelope.route,
+ "action": envelope.action,
+ "authority": envelope.authority,
+ "author_model": "google/gemini-test",
+ "author_family": "google",
+ "host_runtime": "fixture-broker",
+ "session_identifier": "fixture-broker-session",
+ "observation_sequence": 1,
+ },
+ }
+ peer.sendall(
+ self.client._encode_broker_frame(
+ response, max_bytes=self.client.BROKER_MAX_RESPONSE_BYTES
+ )
+ )
+
+ thread = threading.Thread(target=server)
+ thread.start()
+ resolution = self.client.RuntimeResolution(
+ self.client.RuntimeStatus.OK,
+ path=self.root / "runtime",
+ manifest_digest="b" * 64,
+ artifact_digest="a" * 64,
+ )
+ state = {"socket_path": str(socket_path)}
+ with mock.patch.object(
+ self.client, "_load_broker_state", return_value=(state, None)
+ ):
+ result = self.client._launch_broker(
+ resolution=resolution,
+ payload=payload,
+ timeout_ms=30_000,
+ envelope=envelope,
+ )
+ thread.join(timeout=2)
+ self.assertFalse(thread.is_alive())
+ self.assertEqual(result.status, self.client.RuntimeStatus.OK, result.error)
+ self.assertEqual(set(observed), self.client.BROKER_FRAME_KEYS)
+ self.assertEqual(observed["artifact_sha256"], "a" * 64)
+ self.assertEqual(observed["manifest_sha256"], "b" * 64)
+ self.assertEqual(observed["request"]["request_id"], envelope.request_id)
+
+ def test_broker_local_failures_remain_typed_without_internal_detail(self) -> None:
+ self._fixture()
+ envelope = self._envelope(route="opencode", action="plan")
+ expected = {
+ "config_error": self.client.RuntimeStatus.CONFIG_ERROR,
+ "integrity_error": self.client.RuntimeStatus.INTEGRITY_ERROR,
+ "peer_error": self.client.RuntimeStatus.INTEGRITY_ERROR,
+ "replay_error": self.client.RuntimeStatus.INTEGRITY_ERROR,
+ "protocol_error": self.client.RuntimeStatus.PROTOCOL_ERROR,
+ }
+ for status, runtime_status in expected.items():
+ with self.subTest(status=status):
+ result = self.client._parse_broker_response(
+ {
+ "protocol_version": 1,
+ "request_id": envelope.request_id,
+ "status": status,
+ "error": "provider broker request was rejected",
+ },
+ envelope,
+ )
+ self.assertEqual(result.status, runtime_status)
+ self.assertNotIn("/", result.error)
+
+ def test_broker_state_and_socket_are_exact_and_fail_closed(self) -> None:
+ root = self.root / "provider-broker"
+ root.mkdir(mode=0o700)
+ state = root / "state.json"
+ socket_path = root / "provider.sock"
+ document = {
+ "schema_version": 1,
+ "artifact_sha256": "a" * 64,
+ "manifest_sha256": "b" * 64,
+ "runtime_path": str(root / "versions" / ("a" * 64) / "agent-collab-runtime"),
+ "manifest_path": str(root / "versions" / ("a" * 64) / "runtime-manifest.json"),
+ "plist_sha256": "c" * 64,
+ "socket_path": str(socket_path),
+ "label": self.client.BROKER_LABEL,
+ "previous": None,
+ }
+ state.write_text(json.dumps(document), encoding="utf-8")
+ state.chmod(0o600)
+ with mock.patch.object(self.client, "_broker_root", return_value=root), mock.patch.object(
+ self.client, "_verify_published_version"
+ ), mock.patch.object(self.client, "_verify_plist_against_state"):
+ result, error = self.client._load_broker_state(
+ artifact_digest="a" * 64,
+ manifest_digest="b" * 64,
+ require_socket=False,
+ )
+ self.assertEqual(result, document)
+ self.assertIsNone(error)
+
+ state.chmod(0o644)
+ with mock.patch.object(self.client, "_broker_root", return_value=root), mock.patch.object(
+ self.client, "_verify_published_version"
+ ), mock.patch.object(self.client, "_verify_plist_against_state"):
+ result, error = self.client._load_broker_state(
+ artifact_digest="a" * 64,
+ manifest_digest="b" * 64,
+ require_socket=False,
+ )
+ self.assertIsNone(result)
+ self.assertEqual(error.status, self.client.RuntimeStatus.INTEGRITY_ERROR)
+
+ def test_closed_plist_has_socket_activation_and_no_persistence_keys(self) -> None:
+ runtime = self.root / "versions" / ("a" * 64) / "agent-collab-runtime"
+ socket_path = self.root / "provider.sock"
+ tmpdir = self.root / "tmp"
+ document = self.client._broker_plist_document(
+ runtime_path=runtime,
+ socket_path=socket_path,
+ tmpdir=tmpdir,
+ home=Path(pwd.getpwuid(os.getuid()).pw_dir),
+ uid=os.getuid(),
+ )
+ self.assertEqual(
+ set(document),
+ {
+ "Label",
+ "Program",
+ "ProgramArguments",
+ "EnvironmentVariables",
+ "ProcessType",
+ "ThrottleInterval",
+ "Sockets",
+ },
+ )
+ self.assertEqual(document["ProgramArguments"], [str(runtime), "broker", "--protocol", "1"])
+ self.assertEqual(document["ThrottleInterval"], 0)
+ self.assertNotIn("KeepAlive", document)
+ self.assertNotIn("RunAtLoad", document)
+ self.assertEqual(
+ document["Sockets"]["ProviderBroker"]["SockPathMode"], 384
+ )
+
+ def _broker_lifecycle_patches(self, root: Path):
+ socket_path = root / "provider.sock"
+
+ def bootstrap(_plist):
+ if not socket_path.exists():
+ peer = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ try:
+ peer.bind(str(socket_path))
+ finally:
+ peer.close()
+ socket_path.chmod(0o600)
+ return True
+
+ return (
+ mock.patch.object(self.client, "_broker_root", return_value=root),
+ mock.patch.object(self.client, "_bootout_broker", return_value=True),
+ mock.patch.object(self.client, "_bootstrap_broker", side_effect=bootstrap),
+ mock.patch.object(self.client, "_broker_job_loaded", return_value=True),
+ mock.patch.object(self.client, "_broker_ping", return_value=True),
+ )
+
+ def test_install_broker_publishes_exact_version_and_socket_activated_state(self) -> None:
+ binary = self._fixture()
+ root = self.root / "broker-state"
+ expected_artifact = hashlib.sha256(binary.read_bytes()).hexdigest()
+ patches = self._broker_lifecycle_patches(root)
+ with mock.patch.object(
+ self.client, "_verify_macos_signature", return_value=(True, "")
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), patches[0], patches[1], patches[2], patches[3], patches[4]:
+ result = self.client.install_broker()
+ self.assertEqual(result.status, self.client.RuntimeStatus.OK, result.error)
+ self.assertFalse(result.result["persistent_process"])
+ state = json.loads((root / "state.json").read_text(encoding="utf-8"))
+ self.assertEqual(state["artifact_sha256"], expected_artifact)
+ self.assertIsNone(state["previous"])
+ self.assertEqual(stat.S_IMODE((root / "broker.plist").stat().st_mode), 0o600)
+ version = root / "versions" / expected_artifact
+ self.assertEqual(stat.S_IMODE(version.stat().st_mode), 0o500)
+ self.assertEqual(
+ hashlib.sha256((version / "agent-collab-runtime").read_bytes()).hexdigest(),
+ expected_artifact,
+ )
+ plist = plistlib.loads((root / "broker.plist").read_bytes())
+ self.assertEqual(plist["Label"], self.client.BROKER_LABEL)
+ self.assertNotIn("KeepAlive", plist)
+ self.assertNotIn("RunAtLoad", plist)
+
+ def test_broker_update_records_one_verified_rollback_and_rollback_switches(self) -> None:
+ first = self._fixture(body="#!/bin/sh\nexit 0\n")
+ first_digest = hashlib.sha256(first.read_bytes()).hexdigest()
+ root = self.root / "broker-state"
+ patches = self._broker_lifecycle_patches(root)
+ with mock.patch.object(
+ self.client, "_verify_macos_signature", return_value=(True, "")
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), patches[0], patches[1], patches[2], patches[3], patches[4]:
+ self.assertEqual(self.client.install_broker().status, self.client.RuntimeStatus.OK)
+ second = self._fixture(body="#!/bin/sh\nexit 7\n")
+ second_digest = hashlib.sha256(second.read_bytes()).hexdigest()
+ updated = self.client.install_broker()
+ self.assertEqual(updated.status, self.client.RuntimeStatus.OK, updated.error)
+ state = json.loads((root / "state.json").read_text(encoding="utf-8"))
+ self.assertEqual(state["artifact_sha256"], second_digest)
+ self.assertEqual(state["previous"]["artifact_sha256"], first_digest)
+ rolled_back = self.client.rollback_broker()
+ self.assertEqual(rolled_back.status, self.client.RuntimeStatus.OK, rolled_back.error)
+ state = json.loads((root / "state.json").read_text(encoding="utf-8"))
+ self.assertEqual(state["artifact_sha256"], first_digest)
+ self.assertEqual(state["previous"]["artifact_sha256"], second_digest)
+
+ def test_failed_broker_update_restores_the_prior_exact_state(self) -> None:
+ first = self._fixture(body="#!/bin/sh\nexit 0\n")
+ first_digest = hashlib.sha256(first.read_bytes()).hexdigest()
+ root = self.root / "broker-state"
+ patches = self._broker_lifecycle_patches(root)
+ with mock.patch.object(
+ self.client, "_verify_macos_signature", return_value=(True, "")
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), patches[0], patches[1], patches[2], patches[3], patches[4]:
+ self.assertEqual(self.client.install_broker().status, self.client.RuntimeStatus.OK)
+ self._fixture(body="#!/bin/sh\nexit 9\n")
+ with mock.patch.object(
+ self.client, "_bootstrap_broker", side_effect=[False, True]
+ ):
+ failed = self.client.install_broker()
+ self.assertEqual(failed.status, self.client.RuntimeStatus.PROVIDER_ERROR)
+ self.assertTrue(failed.result["restored_previous"])
+ state = json.loads((root / "state.json").read_text(encoding="utf-8"))
+ self.assertEqual(state["artifact_sha256"], first_digest)
+
+ def test_uninstall_removes_mutable_state_but_retains_versions(self) -> None:
+ self._fixture()
+ root = self.root / "broker-state"
+ patches = self._broker_lifecycle_patches(root)
+ with mock.patch.object(
+ self.client, "_verify_macos_signature", return_value=(True, "")
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), patches[0], patches[1], patches[2], patches[3], patches[4]:
+ self.assertEqual(self.client.install_broker().status, self.client.RuntimeStatus.OK)
+ result = self.client.uninstall_broker()
+ self.assertEqual(result.status, self.client.RuntimeStatus.OK, result.error)
+ self.assertTrue(result.result["versions_retained"])
+ self.assertTrue(any((root / "versions").iterdir()))
+ self.assertFalse((root / "state.json").exists())
+ self.assertFalse((root / "broker.plist").exists())
+ self.assertFalse((root / "provider.sock").exists())
+
+ def test_foreign_or_malformed_broker_plist_blocks_update(self) -> None:
+ self._fixture()
+ root = self.root / "broker-state"
+ patches = self._broker_lifecycle_patches(root)
+ with mock.patch.object(
+ self.client, "_verify_macos_signature", return_value=(True, "")
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), patches[0], patches[1], patches[2], patches[3], patches[4]:
+ self.assertEqual(self.client.install_broker().status, self.client.RuntimeStatus.OK)
+ (root / "broker.plist").chmod(0o644)
+ result = self.client.install_broker()
+ self.assertEqual(result.status, self.client.RuntimeStatus.INTEGRITY_ERROR)
+
+ def test_launchctl_wrapper_uses_exact_binary_domain_and_scrubbed_environment(self) -> None:
+ process = mock.Mock(returncode=0)
+ with mock.patch.object(
+ self.client.subprocess, "Popen", return_value=process
+ ) as popen, mock.patch.object(
+ self.client, "_collect_bounded_output", return_value=(b"state = not running\n", b"", None)
+ ):
+ result = self.client._launchctl(
+ ["print", f"gui/{os.getuid()}/{self.client.BROKER_LABEL}"]
+ )
+ self.assertEqual(result.returncode, 0)
+ self.assertEqual(popen.call_args.args[0][0], "/bin/launchctl")
+ self.assertEqual(
+ popen.call_args.args[0][1:],
+ ["print", f"gui/{os.getuid()}/{self.client.BROKER_LABEL}"],
+ )
+ self.assertEqual(
+ set(popen.call_args.kwargs["env"]),
+ {"HOME", "PATH", "LANG", "LC_ALL"},
+ )
+
+ def test_broker_idle_requires_no_live_pid_and_an_explicit_stopped_state(self) -> None:
+ cases = (
+ ("state = not running\n", True),
+ ("state = exited\n", True),
+ ("state = running\npid = 123\n", False),
+ ("pid = 123\n", False),
+ ("", False),
+ )
+ for output, expected in cases:
+ with self.subTest(output=output), mock.patch.object(
+ self.client,
+ "_launchctl",
+ return_value=subprocess.CompletedProcess([], 0, output, ""),
+ ):
+ self.assertEqual(self.client._broker_process_idle(), expected)
+
def test_host_context_requires_the_exact_complete_codex_desktop_tuple(self) -> None:
keys = self.client.CODEX_DESKTOP_TUPLE
exact = dict(self.client.CODEX_DESKTOP_TUPLE.items())
@@ -820,7 +1272,9 @@ def test_gemini_advisory_is_read_only_native_execution(self) -> None:
with mock.patch.object(
self.client, "_verify_macos_signature", return_value=(True, "")
):
- with mock.patch.object(self.client, "PLUGIN_ROOT", self.root):
+ with mock.patch.object(self.client, "PLUGIN_ROOT", self.root), mock.patch.object(
+ self.client, "_launch_broker", side_effect=self._fixture_broker
+ ):
result = self.client.invoke(envelope=envelope)
self.assertEqual(result.status, self.client.RuntimeStatus.OK)
self.assertEqual(result.provenance["author_family"], "google")
@@ -886,7 +1340,9 @@ def test_response_family_is_derived_from_author_model(self) -> None:
envelope = self._envelope(route="opencode", action="plan")
with mock.patch.object(
self.client, "_verify_macos_signature", return_value=(True, "")
- ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root):
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), mock.patch.object(
+ self.client, "_launch_broker", side_effect=self._fixture_broker
+ ):
result = self.client.invoke(envelope=envelope)
self.assertEqual(result.status, self.client.RuntimeStatus.PROTOCOL_ERROR)
@@ -923,10 +1379,13 @@ def test_opencode_model_switch_updates_accepted_provenance(self) -> None:
route="opencode",
action="plan",
row={"model": model, "cwd": str(self.root)},
+ opencode_model=model,
)
with mock.patch.object(
self.client, "_verify_macos_signature", return_value=(True, "")
- ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root):
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), mock.patch.object(
+ self.client, "_launch_broker", side_effect=self._fixture_broker
+ ):
result = self.client.invoke(envelope=envelope)
self.assertEqual(result.status, self.client.RuntimeStatus.OK)
self.assertEqual(result.provenance["author_family"], family)
@@ -971,7 +1430,9 @@ def test_response_model_is_bound_to_exact_sealed_route_model(self) -> None:
envelope = self._envelope(route=route, action=action, row=row)
with mock.patch.object(
self.client, "_verify_macos_signature", return_value=(True, "")
- ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root):
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), mock.patch.object(
+ self.client, "_launch_broker", side_effect=self._fixture_broker
+ ):
result = self.client.invoke(envelope=envelope)
self.assertEqual(result.status, self.client.RuntimeStatus.PROTOCOL_ERROR)
@@ -1343,7 +1804,7 @@ def test_native_protocol_binds_exact_artifact_bytes_model_and_hash(self) -> None
with self.assertRaisesRegex(ValueError, "artifact hash"):
self.client._native_document(tampered)
- def test_row_escape_fields_and_anthropic_opencode_fail_before_runtime(self) -> None:
+ def test_row_escape_fields_fail_and_row_model_cannot_select_anthropic(self) -> None:
policy = self.client._load_host_policy()
with mock.patch.object(
policy,
@@ -1361,7 +1822,6 @@ def test_row_escape_fields_and_anthropic_opencode_fail_before_runtime(self) -> N
"cwd": "/tmp/project",
"tools": ["shell"],
},
- {"model": "anthropic/claude-sonnet", "cwd": "/tmp/project"},
):
with self.subTest(row=row):
decision = policy.issue_policy_envelope(
@@ -1381,6 +1841,28 @@ def test_row_escape_fields_and_anthropic_opencode_fail_before_runtime(self) -> N
)
self.assertIsNone(decision.envelope)
+ decision = policy.issue_policy_envelope(
+ request_id="ignore-row-model",
+ route="opencode",
+ action="plan",
+ governance=False,
+ prompt="plan",
+ timeout_ms=30_000,
+ explicit_config={
+ "primary_id": "custom",
+ "active_model": "custom/unknown",
+ "host_runtime": "custom",
+ "session_identifier": "s-1",
+ },
+ row_config={"model": "anthropic/claude-sonnet", "cwd": "/tmp/project"},
+ )
+ self.assertIsNotNone(decision.envelope)
+ assert decision.envelope is not None
+ self.assertEqual(
+ json.loads(decision.envelope.row_json)["model"],
+ policy.DEFAULT_OPENCODE_MODEL,
+ )
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_runtime_setup.py b/tests/test_runtime_setup.py
index f0e0397..58259dd 100644
--- a/tests/test_runtime_setup.py
+++ b/tests/test_runtime_setup.py
@@ -62,6 +62,33 @@ def test_each_public_command_maps_to_one_closed_management_action(self) -> None:
r"^setup-[0-9a-f]{32}$",
)
+ def test_each_broker_command_maps_to_one_closed_lifecycle_action(self) -> None:
+ mapping = {
+ "install-broker": "install_broker",
+ "broker-status": "broker_status",
+ "rollback-broker": "rollback_broker",
+ "uninstall-broker": "uninstall_broker",
+ }
+ for command, function_name in mapping.items():
+ with self.subTest(command=command):
+ result = self.client.RuntimeResult(
+ self.client.RuntimeStatus.OK,
+ result={"operation": command},
+ )
+ output = io.StringIO()
+ with mock.patch.object(
+ self.setup.runtime_client,
+ function_name,
+ return_value=result,
+ ) as lifecycle, mock.patch.object(
+ self.setup.runtime_client, "manage_runtime"
+ ) as managed, contextlib.redirect_stdout(output):
+ exit_code = self.setup.main([command])
+ self.assertEqual(exit_code, 0)
+ self.assertEqual(json.loads(output.getvalue())["status"], "ok")
+ lifecycle.assert_called_once_with()
+ managed.assert_not_called()
+
def test_cli_rejects_raw_provider_model_path_and_option_surfaces(self) -> None:
invalid = (
[],
@@ -71,17 +98,22 @@ def test_cli_rejects_raw_provider_model_path_and_option_surfaces(self) -> None:
["--path", "/tmp/runtime"],
["--env", "KEY=VALUE"],
["login"],
+ ["install-broker", "--path", "/tmp/runtime"],
+ ["broker-status", "--socket", "/tmp/provider.sock"],
)
for argv in invalid:
with self.subTest(argv=argv):
output = io.StringIO()
with mock.patch.object(
self.setup.runtime_client, "manage_runtime"
- ) as managed, contextlib.redirect_stdout(output):
+ ) as managed, mock.patch.object(
+ self.setup.runtime_client, "install_broker"
+ ) as install, contextlib.redirect_stdout(output):
exit_code = self.setup.main(list(argv))
self.assertEqual(exit_code, 2)
self.assertEqual(json.loads(output.getvalue())["status"], "config_error")
managed.assert_not_called()
+ install.assert_not_called()
def test_typed_management_failure_returns_nonzero_without_extra_fields(self) -> None:
result = self.client.RuntimeResult(
From fd77a6af0433229251cebd7ecd274c69a1de60cf Mon Sep 17 00:00:00 2001
From: John Osumi <931193+sumitake@users.noreply.github.com>
Date: Sun, 12 Jul 2026 19:51:20 -0700
Subject: [PATCH 2/9] release: prepare agent-collab 3.2.0 broker policy
---
.claude-plugin/marketplace.base.json | 2 +-
.claude-plugin/marketplace.json | 4 +-
README.md | 76 +++++++++++++++----
changelog.d/2026-07-13-provider-broker.md | 9 +++
.../agent-collab/.claude-plugin/plugin.json | 2 +-
.../agent-collab/.codex-plugin/plugin.json | 2 +-
plugins/agent-collab/README.md | 45 ++++++++++-
.../skills/agent-readiness/SKILL.md | 2 +-
.../skills/agent-runtime-status/SKILL.md | 2 +-
.../agent-collab/skills/architect/SKILL.md | 2 +-
.../skills/autonomy-readiness/SKILL.md | 2 +-
.../agent-collab/skills/brainstorm/SKILL.md | 2 +-
.../skills/chain-configurator/SKILL.md | 2 +-
plugins/agent-collab/skills/chain/SKILL.md | 2 +-
.../agent-collab/skills/code-review/SKILL.md | 2 +-
.../skills/compose-skills/SKILL.md | 2 +-
plugins/agent-collab/skills/debate/SKILL.md | 2 +-
plugins/agent-collab/skills/delegate/SKILL.md | 2 +-
.../agent-collab/skills/dev-delegate/SKILL.md | 2 +-
.../skills/governance-review/SKILL.md | 2 +-
.../agent-collab/skills/intent-check/SKILL.md | 2 +-
.../skills/knowledge-compile/SKILL.md | 2 +-
.../agent-collab/skills/logic-check/SKILL.md | 2 +-
.../agent-collab/skills/long-context/SKILL.md | 2 +-
.../skills/merge-resolve/SKILL.md | 2 +-
.../skills/migration-doctor/SKILL.md | 2 +-
.../agent-collab/skills/orchestrate/SKILL.md | 2 +-
.../agent-collab/skills/qa-verify/SKILL.md | 2 +-
plugins/agent-collab/skills/red-team/SKILL.md | 2 +-
plugins/agent-collab/skills/route/SKILL.md | 2 +-
.../skills/second-opinion/SKILL.md | 2 +-
.../skills/simulate-user/SKILL.md | 2 +-
plugins/agent-collab/skills/teamwork/SKILL.md | 2 +-
.../agent-collab/skills/ui-to-code/SKILL.md | 2 +-
.../skills/untrusted-audit/SKILL.md | 2 +-
.../skills/visual-review/SKILL.md | 2 +-
plugins/agent-collab/skills/worker/SKILL.md | 2 +-
scripts/skill-build-config.json | 2 +-
38 files changed, 150 insertions(+), 52 deletions(-)
create mode 100644 changelog.d/2026-07-13-provider-broker.md
diff --git a/.claude-plugin/marketplace.base.json b/.claude-plugin/marketplace.base.json
index 65ef956..4172ca1 100644
--- a/.claude-plugin/marketplace.base.json
+++ b/.claude-plugin/marketplace.base.json
@@ -9,7 +9,7 @@
"plugins": [],
"metadata": {
"description": "Agent collaboration plugin marketplace",
- "version": "3.1.0",
+ "version": "3.2.0",
"repository": "https://github.com/sumitake/agent-collab"
}
}
diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 40e4ed9..277eb17 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -10,7 +10,7 @@
{
"name": "agent-collab",
"description": "Unified dynamic-host collaboration package. Centralized skills and async coordination work without legacy packages; every model-execution route requires the verified signed plugin artifact.",
- "version": "3.1.0",
+ "version": "3.2.0",
"author": {
"name": "John Osumi"
},
@@ -32,7 +32,7 @@
],
"metadata": {
"description": "Agent collaboration plugin marketplace",
- "version": "3.1.0",
+ "version": "3.2.0",
"repository": "https://github.com/sumitake/agent-collab"
}
}
diff --git a/README.md b/README.md
index 9543688..39fa65a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# agent-collab
-This repository distributes one package: **agent-collab** (v3.1.0). It gives
+This repository distributes one package: **agent-collab** (v3.2.0). It gives
Claude, Codex, Antigravity, OpenCode, ZCode, and custom primary hosts the same
dynamic collaboration surface without publishing provider executors or
maintaining host-specific plugin copies.
@@ -23,9 +23,19 @@ Contributors need no access to the private build/sign system. See
| Package | Version | Role |
|---|---:|---|
-| `agent-collab` | 3.1.0 | Unified skills, dynamic host policy, migration preflight, and verified native-runtime client |
+| `agent-collab` | 3.2.0 | Unified skills, dynamic host policy, migration preflight, and verified native-runtime client |
-## What's new - v3.1.0
+## What's new - v3.2.0
+
+- Add explicit zero-idle launchd socket activation for managed Gemini and
+ OpenCode routes, with digest-bound state and no direct fallback.
+- Add closed install, status, rollback, and uninstall broker lifecycle commands
+ with transactional prior-version restoration and immutable version retention.
+- Resolve OpenCode models per request from live session observation, explicit
+ central configuration, or the fixed GLM preset; ignore ambient and row-level
+ model selection.
+
+The v3.1.0 licensing and distribution changes remain in force:
- Adopt the unmodified PolyForm Strict License 1.0.0. John Osumi retains
copyright, and commercial use requires separate explicit written approval
@@ -96,9 +106,12 @@ flowchart LR
P --> G["Governance and family-independence policy"]
G --> S["Sealed route/action preflight"]
S --> N["Observed non-model seam
host async-inbox readiness only"]
- S --> C["Verified plugin-relative native runtime"]
- C --> X["Managed Gemini, Codex, OpenCode,
Grok 4.5, and Composer roles"]
+ S --> C["Verified plugin-relative native runtime client"]
+ C --> B["Per-user launchd socket
zero idle process"]
+ B --> X1["Managed Gemini and OpenCode"]
+ C --> X2["Managed Codex, Grok 4.5,
and Composer"]
W["Private build/sign system"] -. "signed and notarized artifact" .-> C
+ W -. "same exact digest" .-> B
```
The plugin runtime client accepts no binary override. It selects only the
@@ -111,6 +124,19 @@ the client rejects unadvertised rows, mismatched route/authority combinations,
and author-family provenance drift. Missing, blocked, unsigned, mismatched, or
unsupported artifacts fail closed with typed status.
+Gemini and OpenCode use a digest-bound, per-user launchd Unix socket. Launchd
+owns the mode-`0600` socket and starts the exact signed runtime only when a
+request arrives. The broker accepts one bounded request, runs it through the
+managed backend, returns one bounded response, and exits; there is no
+`KeepAlive`, `RunAtLoad`, polling loop, interval, calendar trigger, or resident
+agent process. Codex, Grok, Composer, and runtime-management calls retain the
+fixed direct exact-artifact path. Missing, stale, or mismatched broker state is
+a typed failure and never falls back to direct Gemini or OpenCode execution.
+The current signed facade still returns typed `containment_error` for Gemini
+before Google provider setup because no completion-only Google transport is
+proven; socket activation establishes the safe transport boundary but does not
+silently activate an agentic CLI.
+
The expected Apple Developer ID Team ID is pinned in the public
`plugins/agent-collab/signing_policy.py` policy source, independently of the
runtime manifest. It is currently unconfigured; activation releases remain
@@ -157,9 +183,15 @@ until the signed runtime exposes the complete matrix, including Composer.
evidence distinguishes project-owned PolyForm material from the embedded
CPython, Nuitka, and incorporated third-party components.
7. Hosts update one package, run the migration doctor, restart, and verify the
- resolved profile plus eligible routes. Activation hosts then run the
- co-packaged `runtime_setup.py status` and `prepare` commands; managed Grok
- device login is exposed only as `runtime_setup.py login-grok`.
+ resolved profile plus eligible routes. Activation hosts run the co-packaged
+ `runtime_setup.py status` and `prepare` commands, then explicitly run
+ `install-broker` before enabling Gemini or OpenCode. Managed Grok device
+ login is exposed only as `runtime_setup.py login-grok`.
+8. Broker updates copy the verified artifact and manifest into an immutable
+ digest directory, atomically replace the closed launchd plist/state, verify
+ the exact job and socket, activate one protocol-only request, and prove the
+ broker process exits. Failed updates restore the prior verified digest or
+ report that no active version is proven.
Rollback uses policy-only safe mode. Set `AGENT_COLLAB_SAFE_MODE=1` in the
active host runtime environment and restart that host; all model-execution
@@ -203,6 +235,8 @@ the coordinator. Resolve the installed plugin root and run only:
```text
python3 "/runtime_setup.py" status
python3 "/runtime_setup.py" prepare
+python3 "/runtime_setup.py" install-broker
+python3 "/runtime_setup.py" broker-status
python3 "/runtime_setup.py" login-grok
```
@@ -215,6 +249,21 @@ external prerequisites and must be installed and authenticated through their
vendor-supported interfaces. No workspace checkout or provider-specific plugin
is required. Policy-only releases report the management surface unavailable.
+Broker lifecycle is always explicit—import, readiness, and route invocation do
+not install or modify launchd state. To switch to the one retained prior digest
+or to remove the job while keeping immutable rollback artifacts, run:
+
+```text
+python3 "/runtime_setup.py" rollback-broker
+python3 "/runtime_setup.py" uninstall-broker
+```
+
+`broker-status` is read-only and value-free: it reports only installation,
+job/socket state, artifact/manifest digests, rollback availability, and the
+fact that no persistent process is configured. Lifecycle commands accept no
+caller-selected path, label, socket, environment, provider, model, or raw
+argument.
+
Remove every old package reported by the doctor, then run the doctor again.
The doctor reads filesystem/registry state and Codex
`[plugins."name@marketplace"]` entries from `~/.codex/config.toml`, distinguishes
@@ -243,11 +292,12 @@ detection relies on strong session signals
(`CODEX_THREAD_ID`, `CLAUDE_CODE_SESSION_ID`/entrypoint,
`ANTIGRAVITY_SESSION_ID`, or `ZCODE_SESSION_ID`), never ambient installation
paths such as `CODEX_HOME` or `OPENCODE_CONFIG`.
-The current OpenCode preset is `opencode/glm-5.2` only when explicitly observed
-from host/session or central configuration. A missing observation is typed
-unavailable; a strong live OpenCode or ZCode model observation cannot be overridden
-by conflicting explicit family/config fields, and an Anthropic-selected
-OpenCode model is prohibited.
+The OpenCode model is selected on every request in this order: a strong live
+OpenCode or ZCode active-model observation, explicit central
+`primary.opencode_model` configuration, then the fixed current preset
+`opencode/glm-5.2`. Ambient environment values and row-level model fields are
+not selection fallbacks. A live session switch therefore changes provenance on
+the next request; an Anthropic or unknown selected model is prohibited.
`AGENT_COLLAB_ASYNC_INBOX=available` (or request field
`primary.async_inbox="available"`) is an availability observation, not a send
diff --git a/changelog.d/2026-07-13-provider-broker.md b/changelog.d/2026-07-13-provider-broker.md
new file mode 100644
index 0000000..c99ad78
--- /dev/null
+++ b/changelog.d/2026-07-13-provider-broker.md
@@ -0,0 +1,9 @@
+### agent-collab 3.2.0 — zero-idle provider broker
+
+- Route managed Gemini and OpenCode requests through an explicit, digest-bound
+ launchd socket broker that starts for one request and returns to zero idle
+ processes. Add closed install, status, rollback, and uninstall lifecycle
+ commands with transactional prior-version restoration and no direct fallback.
+- Resolve the OpenCode model per request from live OpenCode/ZCode observation,
+ explicit central configuration, or the fixed `opencode/glm-5.2` preset while
+ ignoring ambient and row-level model fallbacks.
diff --git a/plugins/agent-collab/.claude-plugin/plugin.json b/plugins/agent-collab/.claude-plugin/plugin.json
index 9bbb49d..6f5eae1 100644
--- a/plugins/agent-collab/.claude-plugin/plugin.json
+++ b/plugins/agent-collab/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "agent-collab",
- "version": "3.1.0",
+ "version": "3.2.0",
"description": "Unified dynamic-host collaboration package with centralized skills, migration preflight, and a verified plugin-relative native runtime boundary. The signed runtime artifact is intentionally absent until the private build/sign integration completes.",
"author": {
"name": "John Osumi"
diff --git a/plugins/agent-collab/.codex-plugin/plugin.json b/plugins/agent-collab/.codex-plugin/plugin.json
index 60c8e93..3a50934 100644
--- a/plugins/agent-collab/.codex-plugin/plugin.json
+++ b/plugins/agent-collab/.codex-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "agent-collab",
- "version": "3.1.0",
+ "version": "3.2.0",
"description": "Unified dynamic-host collaboration package with centralized skills, migration preflight, and a verified plugin-relative native runtime boundary. The signed runtime artifact is intentionally absent until the private build/sign integration completes.",
"author": {
"name": "John Osumi"
diff --git a/plugins/agent-collab/README.md b/plugins/agent-collab/README.md
index 620535c..79c7145 100644
--- a/plugins/agent-collab/README.md
+++ b/plugins/agent-collab/README.md
@@ -2,7 +2,7 @@
`agent-collab` is the single dynamic-host collaboration package.
-Current: **3.1.0**
+Current: **3.2.0**
It resolves `primary_id`, `primary_family`, `active_model`, `host_runtime`, and
`session_identifier` from the current host or explicit configuration. ZCode
@@ -46,7 +46,18 @@ inspects the executable as a thin arm64 Mach-O and requires exactly one macOS
`LC_BUILD_VERSION` with minimum macOS 14.0 instead of trusting those manifest
labels. It uses a fixed JSON protocol and scrubbed environment. The package
carries both `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json`; both
-identify this same 3.1.0 package.
+identify this same 3.2.0 package.
+
+Gemini and OpenCode are broker-only contracts. Their sealed requests cross a
+mode-`0600`, digest-bound per-user launchd Unix socket; launchd starts the exact
+signed runtime for one request, and the broker exits after its single bounded
+response. The plist has no keepalive, run-at-load, polling, interval, calendar,
+or resident-process trigger. A missing or stale broker is a typed failure—these
+routes never fall back to the direct artifact path. Codex, Grok, Composer, and
+runtime management retain fixed direct exact-artifact execution.
+The current Gemini facade remains typed `containment_error` before Google
+provider setup until a separately reviewed completion-only transport exists;
+the broker does not treat an agentic CLI mode as a read-only guarantee.
No signed artifact is present in this source tree yet. Native **Gemini
advisory/long-context**, **Codex advisory**, **OpenCode plan/build**, **Grok 4.5
@@ -169,6 +180,8 @@ use the closed management client only:
```text
python3 "/runtime_setup.py" status
python3 "/runtime_setup.py" prepare
+python3 "/runtime_setup.py" install-broker
+python3 "/runtime_setup.py" broker-status
python3 "/runtime_setup.py" login-grok
```
@@ -180,7 +193,25 @@ keeps the child environment scrubbed, reports only a closed host-context enum,
and never exposes private provider recipes. Codex and OpenCode still require
their exact supported external CLIs and standard authenticated host state;
install and authenticate those through their vendor-supported interfaces.
-Policy-only releases return typed `unavailable` for all three commands.
+Policy-only releases return typed `unavailable` for runtime-dependent commands.
+Broker installation is explicit; import, readiness, and invocation never
+install or mutate launchd state. `install-broker` publishes the verified
+artifact/manifest into an immutable digest directory, atomically activates a
+closed plist, proves the job/socket and one-request process exit, and retains
+one verified prior digest. `broker-status` is read-only and emits no prompt,
+credential, provider output, or private path.
+
+Use the closed rollback/removal actions only when needed:
+
+```text
+python3 "/runtime_setup.py" rollback-broker
+python3 "/runtime_setup.py" uninstall-broker
+```
+
+Rollback switches to the one complete prior verified record. Uninstall removes
+the exact job, socket, plist, and mutable state while retaining immutable
+version directories. No lifecycle command accepts a caller-selected path,
+label, socket, environment, provider, model, or raw argument.
## Standalone invocation and local threat limit
@@ -263,6 +294,14 @@ Exact row contracts are:
| `composer/codegen` | `{}` |
| `inbox/async` | `{"target_id":"claude|antigravity","target_family":"anthropic|google","target_session_identifier":"..."}`; readiness-only, non-governance, observed host transport |
+For OpenCode, a `model` field in the row is compatibility input only and never
+selects a backend. Selection is recomputed for every request from the strong
+live OpenCode or ZCode active-model observation, then explicit central
+`primary.opencode_model`, then the fixed `opencode/glm-5.2` preset. Ambient
+environment and row values are not fallbacks. The selected model must resolve
+to a known non-Anthropic family, and its family supplies artifact provenance
+and independence exclusion.
+
The inbox row is exact: `target_id=claude` requires
`target_family=anthropic`, while `target_id=antigravity` requires
`target_family=google`; the target session identifier must be nonblank and
diff --git a/plugins/agent-collab/skills/agent-readiness/SKILL.md b/plugins/agent-collab/skills/agent-readiness/SKILL.md
index 8732a12..c132525 100644
--- a/plugins/agent-collab/skills/agent-readiness/SKILL.md
+++ b/plugins/agent-collab/skills/agent-readiness/SKILL.md
@@ -1,6 +1,6 @@
---
name: agent-readiness
-version: 3.1.0
+version: 3.2.0
description: Evaluate whether an agent, model, CLI, plugin, or role is ready for a proposed responsibility. Use when the user says "agent readiness," "is this agent ready," "can Codex be primary," "can Grok handle this role," "promote this agent," "evaluate this worker," "review model readiness," or "/agent-collab:agent-readiness." Also offer this proactively before assigning a new primary, reviewer, worker, delegate, headless, release, or merge-related role to Claude, Codex, Antigravity/Gemini, Grok, or a future agent.
---
diff --git a/plugins/agent-collab/skills/agent-runtime-status/SKILL.md b/plugins/agent-collab/skills/agent-runtime-status/SKILL.md
index 55cfb48..5eeef26 100644
--- a/plugins/agent-collab/skills/agent-runtime-status/SKILL.md
+++ b/plugins/agent-collab/skills/agent-runtime-status/SKILL.md
@@ -1,6 +1,6 @@
---
name: agent-runtime-status
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Fast
effort: low
diff --git a/plugins/agent-collab/skills/architect/SKILL.md b/plugins/agent-collab/skills/architect/SKILL.md
index 12c57eb..0f13def 100644
--- a/plugins/agent-collab/skills/architect/SKILL.md
+++ b/plugins/agent-collab/skills/architect/SKILL.md
@@ -1,6 +1,6 @@
---
name: architect
-version: 3.1.0
+version: 3.2.0
description: Request read-only architecture consultation for codebase analysis, system design, implementation planning, decomposition, or long-horizon coding strategy. Use when the user says "ask the architect," "have Grok design this," "architecture consultation," "plan this implementation," "decompose this build," "analyze the system design," or "/agent-collab:architect." Also offer this proactively before a substantial multi-system or long-horizon implementation where an independent architecture pass can reduce rework. This role never edits files, runs shell commands or tests, mutates a worktree, opens PRs, merges, or deploys.
---
diff --git a/plugins/agent-collab/skills/autonomy-readiness/SKILL.md b/plugins/agent-collab/skills/autonomy-readiness/SKILL.md
index 0651dd9..d829b8b 100644
--- a/plugins/agent-collab/skills/autonomy-readiness/SKILL.md
+++ b/plugins/agent-collab/skills/autonomy-readiness/SKILL.md
@@ -1,6 +1,6 @@
---
name: autonomy-readiness
-version: 3.1.0
+version: 3.2.0
description: Evaluate whether an autonomous, always-on, scheduled, headless, or self-evolving workflow is ready to run safely. Use when the user says "autonomy readiness," "activation gate review," "is this workflow ready to run autonomously," "go/no-go autonomy," "always-on readiness," "headless operation review," or "/agent-collab:autonomy-readiness." Also offer this proactively before enabling background agents, recurring automations, auto-merge/self-evolution, external actions, unattended host runs, or any workflow that can continue without a human watching.
---
diff --git a/plugins/agent-collab/skills/brainstorm/SKILL.md b/plugins/agent-collab/skills/brainstorm/SKILL.md
index 03799ce..d90764e 100644
--- a/plugins/agent-collab/skills/brainstorm/SKILL.md
+++ b/plugins/agent-collab/skills/brainstorm/SKILL.md
@@ -1,6 +1,6 @@
---
name: brainstorm
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Fast
effort: low
diff --git a/plugins/agent-collab/skills/chain-configurator/SKILL.md b/plugins/agent-collab/skills/chain-configurator/SKILL.md
index a7ada5a..56b5cee 100644
--- a/plugins/agent-collab/skills/chain-configurator/SKILL.md
+++ b/plugins/agent-collab/skills/chain-configurator/SKILL.md
@@ -1,6 +1,6 @@
---
name: chain-configurator
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Standard
effort: medium
diff --git a/plugins/agent-collab/skills/chain/SKILL.md b/plugins/agent-collab/skills/chain/SKILL.md
index 3366303..8654f61 100644
--- a/plugins/agent-collab/skills/chain/SKILL.md
+++ b/plugins/agent-collab/skills/chain/SKILL.md
@@ -1,6 +1,6 @@
---
name: chain
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Standard
effort: medium
diff --git a/plugins/agent-collab/skills/code-review/SKILL.md b/plugins/agent-collab/skills/code-review/SKILL.md
index ef0fbbb..1a99a4e 100644
--- a/plugins/agent-collab/skills/code-review/SKILL.md
+++ b/plugins/agent-collab/skills/code-review/SKILL.md
@@ -1,6 +1,6 @@
---
name: code-review
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Advanced
effort: high
diff --git a/plugins/agent-collab/skills/compose-skills/SKILL.md b/plugins/agent-collab/skills/compose-skills/SKILL.md
index 714ba6c..33ab0c4 100644
--- a/plugins/agent-collab/skills/compose-skills/SKILL.md
+++ b/plugins/agent-collab/skills/compose-skills/SKILL.md
@@ -1,6 +1,6 @@
---
name: compose-skills
-version: 3.1.0
+version: 3.2.0
description: Select a bounded, token-aware combination of collaboration skills or task lenses before execution. Use when the user says "compose skills," "which skills should I use," "use skill composition," "select a recipe," "combine these skills," or "/agent-collab:compose-skills." Also offer this proactively when a task plausibly needs multiple lenses, reviewers, or agents and would benefit from progressive disclosure, explicit fan-out limits, and a smallest-useful-skill plan before routing or loading full skill bodies.
---
diff --git a/plugins/agent-collab/skills/debate/SKILL.md b/plugins/agent-collab/skills/debate/SKILL.md
index 2178f1c..43d3338 100644
--- a/plugins/agent-collab/skills/debate/SKILL.md
+++ b/plugins/agent-collab/skills/debate/SKILL.md
@@ -1,6 +1,6 @@
---
name: debate
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Advanced
effort: high
diff --git a/plugins/agent-collab/skills/delegate/SKILL.md b/plugins/agent-collab/skills/delegate/SKILL.md
index 601a144..d7fbaf2 100644
--- a/plugins/agent-collab/skills/delegate/SKILL.md
+++ b/plugins/agent-collab/skills/delegate/SKILL.md
@@ -1,6 +1,6 @@
---
name: delegate
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Fast
effort: low
diff --git a/plugins/agent-collab/skills/dev-delegate/SKILL.md b/plugins/agent-collab/skills/dev-delegate/SKILL.md
index d7566b0..c4c212d 100644
--- a/plugins/agent-collab/skills/dev-delegate/SKILL.md
+++ b/plugins/agent-collab/skills/dev-delegate/SKILL.md
@@ -1,6 +1,6 @@
---
name: dev-delegate
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Standard
effort: medium
diff --git a/plugins/agent-collab/skills/governance-review/SKILL.md b/plugins/agent-collab/skills/governance-review/SKILL.md
index 377ff07..badccfc 100644
--- a/plugins/agent-collab/skills/governance-review/SKILL.md
+++ b/plugins/agent-collab/skills/governance-review/SKILL.md
@@ -1,6 +1,6 @@
---
name: governance-review
-version: 3.1.0
+version: 3.2.0
description: Use when the operator says "governance review," "high-stakes review," "tiebreaker," or "second opinion." Also offer this proactively when reviewer-family independence must be enforced.
---
diff --git a/plugins/agent-collab/skills/intent-check/SKILL.md b/plugins/agent-collab/skills/intent-check/SKILL.md
index b075bcf..1916888 100644
--- a/plugins/agent-collab/skills/intent-check/SKILL.md
+++ b/plugins/agent-collab/skills/intent-check/SKILL.md
@@ -1,6 +1,6 @@
---
name: intent-check
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Advanced
effort: high
diff --git a/plugins/agent-collab/skills/knowledge-compile/SKILL.md b/plugins/agent-collab/skills/knowledge-compile/SKILL.md
index 6097919..e4aa4e2 100644
--- a/plugins/agent-collab/skills/knowledge-compile/SKILL.md
+++ b/plugins/agent-collab/skills/knowledge-compile/SKILL.md
@@ -1,6 +1,6 @@
---
name: knowledge-compile
-version: 3.1.0
+version: 3.2.0
description: Compile multiple sources into a durable, cited knowledge dossier without mixing claims, assumptions, and decisions. Use when the user says "compile knowledge," "build a dossier," "create a knowledge base," "synthesize these sources," "preserve research context," "make this reviewable later," or "/agent-collab:knowledge-compile." Also offer this proactively when a task spans several repos, PRs, papers, articles, logs, agent messages, or drafts and future agents need source-separated context for independent review.
---
diff --git a/plugins/agent-collab/skills/logic-check/SKILL.md b/plugins/agent-collab/skills/logic-check/SKILL.md
index 6d74d6f..c2d8341 100644
--- a/plugins/agent-collab/skills/logic-check/SKILL.md
+++ b/plugins/agent-collab/skills/logic-check/SKILL.md
@@ -1,6 +1,6 @@
---
name: logic-check
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Advanced
effort: xhigh
diff --git a/plugins/agent-collab/skills/long-context/SKILL.md b/plugins/agent-collab/skills/long-context/SKILL.md
index 5c67760..1bfe25a 100644
--- a/plugins/agent-collab/skills/long-context/SKILL.md
+++ b/plugins/agent-collab/skills/long-context/SKILL.md
@@ -1,6 +1,6 @@
---
name: long-context
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Advanced
effort: high
diff --git a/plugins/agent-collab/skills/merge-resolve/SKILL.md b/plugins/agent-collab/skills/merge-resolve/SKILL.md
index 223b80a..fed8a70 100644
--- a/plugins/agent-collab/skills/merge-resolve/SKILL.md
+++ b/plugins/agent-collab/skills/merge-resolve/SKILL.md
@@ -1,6 +1,6 @@
---
name: merge-resolve
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Advanced
effort: high
diff --git a/plugins/agent-collab/skills/migration-doctor/SKILL.md b/plugins/agent-collab/skills/migration-doctor/SKILL.md
index da570f5..934fe85 100644
--- a/plugins/agent-collab/skills/migration-doctor/SKILL.md
+++ b/plugins/agent-collab/skills/migration-doctor/SKILL.md
@@ -1,6 +1,6 @@
---
name: migration-doctor
-version: 3.1.0
+version: 3.2.0
description: Use when the user says "migration doctor," "check old collaboration plugins," "verify agent-collab migration," or "/agent-collab:migration-doctor." Also offer this proactively after installing or updating agent-collab, when provider routing is blocked, or when a retired package may still be selected from an installed plugin or cache.
---
diff --git a/plugins/agent-collab/skills/orchestrate/SKILL.md b/plugins/agent-collab/skills/orchestrate/SKILL.md
index c73451d..7871ffa 100644
--- a/plugins/agent-collab/skills/orchestrate/SKILL.md
+++ b/plugins/agent-collab/skills/orchestrate/SKILL.md
@@ -1,6 +1,6 @@
---
name: orchestrate
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Standard
effort: medium
diff --git a/plugins/agent-collab/skills/qa-verify/SKILL.md b/plugins/agent-collab/skills/qa-verify/SKILL.md
index 01ca57e..513db5c 100644
--- a/plugins/agent-collab/skills/qa-verify/SKILL.md
+++ b/plugins/agent-collab/skills/qa-verify/SKILL.md
@@ -1,6 +1,6 @@
---
name: qa-verify
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Fast
effort: low
diff --git a/plugins/agent-collab/skills/red-team/SKILL.md b/plugins/agent-collab/skills/red-team/SKILL.md
index a154d1c..35366cf 100644
--- a/plugins/agent-collab/skills/red-team/SKILL.md
+++ b/plugins/agent-collab/skills/red-team/SKILL.md
@@ -1,6 +1,6 @@
---
name: red-team
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Advanced
effort: high
diff --git a/plugins/agent-collab/skills/route/SKILL.md b/plugins/agent-collab/skills/route/SKILL.md
index 9e20cc5..d77bb4a 100644
--- a/plugins/agent-collab/skills/route/SKILL.md
+++ b/plugins/agent-collab/skills/route/SKILL.md
@@ -1,6 +1,6 @@
---
name: route
-version: 3.1.0
+version: 3.2.0
description: Use when the operator says "ask Codex," "target=gemini," "target=grok," "target=composer," or explicitly names a managed backend. Also offer this proactively when routing needs dynamic primary-family exclusion.
---
diff --git a/plugins/agent-collab/skills/second-opinion/SKILL.md b/plugins/agent-collab/skills/second-opinion/SKILL.md
index 520457e..0e200fa 100644
--- a/plugins/agent-collab/skills/second-opinion/SKILL.md
+++ b/plugins/agent-collab/skills/second-opinion/SKILL.md
@@ -1,6 +1,6 @@
---
name: second-opinion
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Advanced
effort: high
diff --git a/plugins/agent-collab/skills/simulate-user/SKILL.md b/plugins/agent-collab/skills/simulate-user/SKILL.md
index 1665010..dbaa357 100644
--- a/plugins/agent-collab/skills/simulate-user/SKILL.md
+++ b/plugins/agent-collab/skills/simulate-user/SKILL.md
@@ -1,6 +1,6 @@
---
name: simulate-user
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Fast
effort: low
diff --git a/plugins/agent-collab/skills/teamwork/SKILL.md b/plugins/agent-collab/skills/teamwork/SKILL.md
index d25b697..79af78e 100644
--- a/plugins/agent-collab/skills/teamwork/SKILL.md
+++ b/plugins/agent-collab/skills/teamwork/SKILL.md
@@ -1,6 +1,6 @@
---
name: teamwork
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Standard
effort: medium
diff --git a/plugins/agent-collab/skills/ui-to-code/SKILL.md b/plugins/agent-collab/skills/ui-to-code/SKILL.md
index b5a12b6..f71ec2c 100644
--- a/plugins/agent-collab/skills/ui-to-code/SKILL.md
+++ b/plugins/agent-collab/skills/ui-to-code/SKILL.md
@@ -1,6 +1,6 @@
---
name: ui-to-code
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Advanced
effort: high
diff --git a/plugins/agent-collab/skills/untrusted-audit/SKILL.md b/plugins/agent-collab/skills/untrusted-audit/SKILL.md
index e70bc6c..9432cfe 100644
--- a/plugins/agent-collab/skills/untrusted-audit/SKILL.md
+++ b/plugins/agent-collab/skills/untrusted-audit/SKILL.md
@@ -1,6 +1,6 @@
---
name: untrusted-audit
-version: 3.1.0
+version: 3.2.0
description: Audit an external or untrusted source before using it in code, skills, plugins, workflows, prompts, or operations. Use when the user says "audit this untrusted source," "can we use this repo," "review this gist," "prompt injection audit," "is this plugin safe," "evaluate this methodology," or "/agent-collab:untrusted-audit." Also offer this proactively when a task would incorporate third-party instructions, code, scripts, hooks, generated skills, package manifests, install steps, or auto-updated methodology into the workspace or agent environment.
---
diff --git a/plugins/agent-collab/skills/visual-review/SKILL.md b/plugins/agent-collab/skills/visual-review/SKILL.md
index ee99c90..7906cdc 100644
--- a/plugins/agent-collab/skills/visual-review/SKILL.md
+++ b/plugins/agent-collab/skills/visual-review/SKILL.md
@@ -1,6 +1,6 @@
---
name: visual-review
-version: 3.1.0
+version: 3.2.0
defaults:
tier: Advanced
effort: high
diff --git a/plugins/agent-collab/skills/worker/SKILL.md b/plugins/agent-collab/skills/worker/SKILL.md
index 009b793..c5a34c5 100644
--- a/plugins/agent-collab/skills/worker/SKILL.md
+++ b/plugins/agent-collab/skills/worker/SKILL.md
@@ -1,6 +1,6 @@
---
name: worker
-version: 3.1.0
+version: 3.2.0
description: Use when the operator says "delegate this implementation," "use Gemini for this corpus," "ask Codex to build," or "use Composer for codegen." Also offer this proactively when a bounded non-governance task benefits from a managed worker.
---
diff --git a/scripts/skill-build-config.json b/scripts/skill-build-config.json
index ffd6ff5..e7d88f8 100644
--- a/scripts/skill-build-config.json
+++ b/scripts/skill-build-config.json
@@ -20,7 +20,7 @@
"tier_flash_resolves_to_claude": "an asynchronous Anthropic inbox review; never a synchronous invocation",
"tier_pro_resolves_to_gemini": "an eligible managed Google-family reviewer at high effort",
"tier_flash_resolves_to_gemini": "an eligible managed Google-family reviewer at low effort",
- "skill_version": "3.1.0",
+ "skill_version": "3.2.0",
"agent_runtime_status_defaults_block": "defaults:\n tier: Fast\n effort: low\n",
"merge_resolve_defaults_block": "defaults:\n tier: Advanced\n effort: high\n",
"merge_resolve_call_params": "`effort='high'` in every eligible advisory row and no `tier` request field",
From 372de422e369c8782a0e7be66003e23794acaaf1 Mon Sep 17 00:00:00 2001
From: John Osumi <931193+sumitake@users.noreply.github.com>
Date: Sun, 12 Jul 2026 19:55:34 -0700
Subject: [PATCH 3/9] tests: derive release evidence version from manifest
---
tests/test_public_distribution_contract.py | 10 +++++-----
tests/test_release_evidence.py | 23 +++++++++++++++-------
2 files changed, 21 insertions(+), 12 deletions(-)
diff --git a/tests/test_public_distribution_contract.py b/tests/test_public_distribution_contract.py
index ec4488d..2641edb 100644
--- a/tests/test_public_distribution_contract.py
+++ b/tests/test_public_distribution_contract.py
@@ -80,17 +80,17 @@ def test_current_distributed_version_is_consistent(self) -> None:
)
readme = (ROOT / "README.md").read_text(encoding="utf-8")
- self.assertEqual(claude["version"], "3.1.0")
- self.assertEqual(codex["version"], "3.1.0")
- self.assertIn("**agent-collab** (v3.1.0)", readme)
- self.assertIn("## What's new - v3.1.0", readme)
+ version = claude["version"]
+ self.assertEqual(codex["version"], version)
+ self.assertIn(f"**agent-collab** (v{version})", readme)
+ self.assertIn(f"## What's new - v{version}", readme)
generated_skills = sorted(
(ROOT / "plugins" / "agent-collab" / "skills").glob("*/SKILL.md")
)
self.assertTrue(generated_skills)
for path in generated_skills:
- self.assertIn("\nversion: 3.1.0\n", path.read_text(encoding="utf-8"))
+ self.assertIn(f"\nversion: {version}\n", path.read_text(encoding="utf-8"))
def test_host_metadata_and_homelab_labels_are_not_distributed(self) -> None:
self.assertFalse((ROOT / ".claude-session-owner").exists())
diff --git a/tests/test_release_evidence.py b/tests/test_release_evidence.py
index bc4fb28..7a0bfd4 100644
--- a/tests/test_release_evidence.py
+++ b/tests/test_release_evidence.py
@@ -17,6 +17,15 @@
ROOT = Path(__file__).resolve().parents[1]
+PACKAGE_VERSION = json.loads(
+ (
+ ROOT
+ / "plugins"
+ / "agent-collab"
+ / ".claude-plugin"
+ / "plugin.json"
+ ).read_text(encoding="utf-8")
+)["version"]
ARCHIVE_SCRIPT = ROOT / "scripts" / "build_plugin_archive.py"
EVIDENCE_SCRIPT = ROOT / "scripts" / "build_release_evidence.py"
LEGAL_FILES = {"LICENSE", "NOTICE", "COMMERCIAL-LICENSING.md"}
@@ -49,9 +58,9 @@ class ReleaseEvidenceTests(unittest.TestCase):
def setUp(self) -> None:
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
- self.archive = self.root / "agent-collab v3.1.0.plugin"
- self.sbom = self.root / "agent-collab-v3.1.0.spdx.json"
- self.checksum = self.root / "agent-collab v3.1.0.plugin.sha256"
+ self.archive = self.root / f"agent-collab v{PACKAGE_VERSION}.plugin"
+ self.sbom = self.root / f"agent-collab-v{PACKAGE_VERSION}.spdx.json"
+ self.checksum = self.root / f"agent-collab v{PACKAGE_VERSION}.plugin.sha256"
def tearDown(self) -> None:
self.temp.cleanup()
@@ -69,7 +78,7 @@ def _build(self):
self.assertEqual(mode, "policy-only")
evidence_builder.build_evidence(
self.archive,
- version="3.1.0",
+ version=PACKAGE_VERSION,
created="2026-07-12T00:00:00Z",
sbom_output=self.sbom,
checksum_output=self.checksum,
@@ -141,7 +150,7 @@ def _build_activation(self):
self.assertEqual(mode, "activation")
evidence_builder.build_evidence(
self.archive,
- version="3.1.0",
+ version=PACKAGE_VERSION,
created="2026-07-12T00:00:00Z",
sbom_output=self.sbom,
checksum_output=self.checksum,
@@ -156,7 +165,7 @@ def test_spdx_inventory_contains_exact_legal_files_and_license(self) -> None:
self.assertEqual(sbom["dataLicense"], "CC0-1.0")
self.assertEqual(sbom["creationInfo"]["created"], "2026-07-12T00:00:00Z")
package = sbom["packages"][0]
- self.assertEqual(package["versionInfo"], "3.1.0")
+ self.assertEqual(package["versionInfo"], PACKAGE_VERSION)
self.assertEqual(
package["licenseDeclared"],
"LicenseRef-PolyForm-Strict-1.0.0",
@@ -433,7 +442,7 @@ def test_rejects_canonically_identical_output_paths_before_writing(
with self.assertRaisesRegex(ValueError, "different paths"):
evidence_builder.build_evidence(
self.archive,
- version="3.1.0",
+ version=PACKAGE_VERSION,
created="2026-07-12T00:00:00Z",
sbom_output=relative_sbom,
checksum_output=self.sbom,
From efc0724243f3f904bb30858ec7c36b00b22b6c7d Mon Sep 17 00:00:00 2001
From: John Osumi <931193+sumitake@users.noreply.github.com>
Date: Sun, 12 Jul 2026 20:08:17 -0700
Subject: [PATCH 4/9] runtime: reject duplicate broker JSON keys
---
changelog.d/2026-07-13-provider-broker.md | 1 +
plugins/agent-collab/runtime_client.py | 22 ++++++++++++++++++++--
tests/test_agent_collab_runtime_client.py | 8 ++++++++
3 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/changelog.d/2026-07-13-provider-broker.md b/changelog.d/2026-07-13-provider-broker.md
index c99ad78..66b6be0 100644
--- a/changelog.d/2026-07-13-provider-broker.md
+++ b/changelog.d/2026-07-13-provider-broker.md
@@ -7,3 +7,4 @@
- Resolve the OpenCode model per request from live OpenCode/ZCode observation,
explicit central configuration, or the fixed `opencode/glm-5.2` preset while
ignoring ambient and row-level model fallbacks.
+- Reject duplicate/non-finite broker responses and state before typed parsing.
diff --git a/plugins/agent-collab/runtime_client.py b/plugins/agent-collab/runtime_client.py
index bbbefaf..86da95f 100644
--- a/plugins/agent-collab/runtime_client.py
+++ b/plugins/agent-collab/runtime_client.py
@@ -748,7 +748,11 @@ def _load_broker_state(
if raw is None or opened_identity is None:
return None, RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker state cannot be read safely")
try:
- document = json.loads(raw.decode("utf-8"))
+ document = json.loads(
+ raw.decode("utf-8"),
+ object_pairs_hook=_unique_broker_json_object,
+ parse_constant=lambda _value: (_ for _ in ()).throw(ValueError()),
+ )
except (UnicodeError, ValueError, RecursionError):
return None, RuntimeResult(RuntimeStatus.CONFIG_ERROR, error="provider broker state is malformed")
if not _broker_record_valid(document, root, allow_previous=True):
@@ -842,6 +846,15 @@ def _recv_broker_exact(peer: socket.socket, count: int, deadline: float) -> byte
return b"".join(chunks)
+def _unique_broker_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
+ document: dict[str, Any] = {}
+ for key, value in pairs:
+ if key in document:
+ raise ValueError("duplicate provider broker JSON key")
+ document[key] = value
+ return document
+
+
def _read_broker_frame(
peer: socket.socket, *, max_bytes: int, deadline: float
) -> dict[str, Any]:
@@ -857,6 +870,7 @@ def _read_broker_frame(
try:
document = json.loads(
raw.decode("utf-8"),
+ object_pairs_hook=_unique_broker_json_object,
parse_constant=lambda _value: (_ for _ in ()).throw(ValueError()),
)
except (UnicodeError, ValueError, RecursionError) as exc:
@@ -1187,7 +1201,11 @@ def _read_current_broker_state(root: Path) -> dict[str, Any] | None:
if raw is None:
raise ValueError("provider broker state cannot be read safely")
try:
- document = json.loads(raw.decode("utf-8"))
+ document = json.loads(
+ raw.decode("utf-8"),
+ object_pairs_hook=_unique_broker_json_object,
+ parse_constant=lambda _value: (_ for _ in ()).throw(ValueError()),
+ )
except (UnicodeError, ValueError, RecursionError) as exc:
raise ValueError("provider broker state is malformed") from exc
if not _broker_record_valid(document, root, allow_previous=True):
diff --git a/tests/test_agent_collab_runtime_client.py b/tests/test_agent_collab_runtime_client.py
index d278643..0e3c2ab 100644
--- a/tests/test_agent_collab_runtime_client.py
+++ b/tests/test_agent_collab_runtime_client.py
@@ -450,6 +450,14 @@ def test_broker_frame_reader_rejects_truncated_and_oversize_payloads(self) -> No
with self.assertRaises(ValueError):
self.client._read_broker_frame(left, max_bytes=1024, deadline=time.monotonic() + 1)
+ left, right = socket.socketpair()
+ self.addCleanup(left.close)
+ self.addCleanup(right.close)
+ raw = b'{"value":1,"value":2}'
+ right.sendall(struct.pack(">Q", len(raw)) + raw)
+ with self.assertRaises(ValueError):
+ self.client._read_broker_frame(left, max_bytes=1024, deadline=time.monotonic() + 1)
+
def test_opencode_and_gemini_use_broker_without_direct_fallback(self) -> None:
self._fixture()
for route, action in (("opencode", "plan"), ("gemini", "advisory")):
From 3b97359cb7393fd3a3163a397a4de42f3f2a2e98 Mon Sep 17 00:00:00 2001
From: John Osumi <931193+sumitake@users.noreply.github.com>
Date: Sun, 12 Jul 2026 20:41:13 -0700
Subject: [PATCH 5/9] runtime: reject overflowed broker responses
---
plugins/agent-collab/runtime_client.py | 9 +++++++++
tests/test_agent_collab_runtime_client.py | 8 ++++++++
2 files changed, 17 insertions(+)
diff --git a/plugins/agent-collab/runtime_client.py b/plugins/agent-collab/runtime_client.py
index 86da95f..54b650e 100644
--- a/plugins/agent-collab/runtime_client.py
+++ b/plugins/agent-collab/runtime_client.py
@@ -17,6 +17,7 @@
import hashlib
import importlib.util
import json
+import math
import os
import platform
import plistlib
@@ -855,6 +856,13 @@ def _unique_broker_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
return document
+def _finite_broker_json_float(value: str) -> float:
+ parsed = float(value)
+ if not math.isfinite(parsed):
+ raise ValueError("non-finite provider broker JSON number")
+ return parsed
+
+
def _read_broker_frame(
peer: socket.socket, *, max_bytes: int, deadline: float
) -> dict[str, Any]:
@@ -871,6 +879,7 @@ def _read_broker_frame(
document = json.loads(
raw.decode("utf-8"),
object_pairs_hook=_unique_broker_json_object,
+ parse_float=_finite_broker_json_float,
parse_constant=lambda _value: (_ for _ in ()).throw(ValueError()),
)
except (UnicodeError, ValueError, RecursionError) as exc:
diff --git a/tests/test_agent_collab_runtime_client.py b/tests/test_agent_collab_runtime_client.py
index 0e3c2ab..b64a83b 100644
--- a/tests/test_agent_collab_runtime_client.py
+++ b/tests/test_agent_collab_runtime_client.py
@@ -450,6 +450,14 @@ def test_broker_frame_reader_rejects_truncated_and_oversize_payloads(self) -> No
with self.assertRaises(ValueError):
self.client._read_broker_frame(left, max_bytes=1024, deadline=time.monotonic() + 1)
+ left, right = socket.socketpair()
+ self.addCleanup(left.close)
+ self.addCleanup(right.close)
+ raw = b'{"value":1e309}'
+ right.sendall(struct.pack(">Q", len(raw)) + raw)
+ with self.assertRaises(ValueError):
+ self.client._read_broker_frame(left, max_bytes=1024, deadline=time.monotonic() + 1)
+
left, right = socket.socketpair()
self.addCleanup(left.close)
self.addCleanup(right.close)
From dce6c874323cd96bb5d52447f6e6a28272663eb5 Mon Sep 17 00:00:00 2001
From: John Osumi <931193+sumitake@users.noreply.github.com>
Date: Sun, 12 Jul 2026 22:00:48 -0700
Subject: [PATCH 6/9] runtime: route Grok and Composer through broker
---
README.md | 51 +++++++++++++++++++----
changelog.d/2026-07-13-provider-broker.md | 8 +++-
plugins/agent-collab/README.md | 28 +++++++++++--
plugins/agent-collab/runtime_client.py | 10 ++++-
tests/test_agent_collab_runtime_client.py | 13 ++++--
5 files changed, 92 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index 39fa65a..d1ea165 100644
--- a/README.md
+++ b/README.md
@@ -27,13 +27,17 @@ Contributors need no access to the private build/sign system. See
## What's new - v3.2.0
-- Add explicit zero-idle launchd socket activation for managed Gemini and
- OpenCode routes, with digest-bound state and no direct fallback.
+- Add explicit zero-idle launchd socket activation for managed Gemini,
+ OpenCode, Grok, and Composer routes, with digest-bound state and no direct
+ fallback.
- Add closed install, status, rollback, and uninstall broker lifecycle commands
with transactional prior-version restoration and immutable version retention.
- Resolve OpenCode models per request from live session observation, explicit
central configuration, or the fixed GLM preset; ignore ambient and row-level
model selection.
+- Require closed Grok terminal-state parsing, expose exact `cancelled` and
+ `input_limit` results, and permit only one same-route cancellation retry when
+ more than ten seconds remain under the original deadline.
The v3.1.0 licensing and distribution changes remain in force:
@@ -108,8 +112,8 @@ flowchart LR
S --> N["Observed non-model seam
host async-inbox readiness only"]
S --> C["Verified plugin-relative native runtime client"]
C --> B["Per-user launchd socket
zero idle process"]
- B --> X1["Managed Gemini and OpenCode"]
- C --> X2["Managed Codex, Grok 4.5,
and Composer"]
+ B --> X1["Managed Gemini, OpenCode,
Grok 4.5, and Composer"]
+ C --> X2["Managed Codex and
runtime management"]
W["Private build/sign system"] -. "signed and notarized artifact" .-> C
W -. "same exact digest" .-> B
```
@@ -124,19 +128,32 @@ the client rejects unadvertised rows, mismatched route/authority combinations,
and author-family provenance drift. Missing, blocked, unsigned, mismatched, or
unsupported artifacts fail closed with typed status.
-Gemini and OpenCode use a digest-bound, per-user launchd Unix socket. Launchd
+Gemini, OpenCode, Grok, and Composer use a digest-bound, per-user launchd Unix socket. Launchd
owns the mode-`0600` socket and starts the exact signed runtime only when a
request arrives. The broker accepts one bounded request, runs it through the
managed backend, returns one bounded response, and exits; there is no
`KeepAlive`, `RunAtLoad`, polling loop, interval, calendar trigger, or resident
-agent process. Codex, Grok, Composer, and runtime-management calls retain the
-fixed direct exact-artifact path. Missing, stale, or mismatched broker state is
-a typed failure and never falls back to direct Gemini or OpenCode execution.
+agent process. Codex and runtime-management calls retain the fixed direct
+exact-artifact path. Missing, stale, or mismatched broker state is a typed
+failure and never falls back to direct execution for any broker-only route.
+The broker removes the Codex Desktop outer-Seatbelt marker before backend
+dispatch because socket activation does not inherit the client's Seatbelt.
+Every brokered Grok and Composer attempt must therefore build and validate its
+own nested read-only sandbox. Provider children receive closed,
+backend-specific environment allowlists rooted in fresh private call
+directories rather than the broker's ambient environment. Broker frames cannot
+invoke local runtime-management actions.
The current signed facade still returns typed `containment_error` for Gemini
before Google provider setup because no completion-only Google transport is
proven; socket activation establishes the safe transport boundary but does not
silently activate an agentic CLI.
+The broker rejects cross-UID, stale/replayed, substituted-artifact, and
+connecting-process mismatches. It does not claim to protect provider
+credentials from arbitrary malicious code already running as the same operator
+UID, which can already read that user's auth state. The exact immutable runtime
+path and digest are revalidated for each request.
+
The expected Apple Developer ID Team ID is pinned in the public
`plugins/agent-collab/signing_policy.py` policy source, independently of the
runtime manifest. It is currently unconfigured; activation releases remain
@@ -185,7 +202,7 @@ until the signed runtime exposes the complete matrix, including Composer.
7. Hosts update one package, run the migration doctor, restart, and verify the
resolved profile plus eligible routes. Activation hosts run the co-packaged
`runtime_setup.py status` and `prepare` commands, then explicitly run
- `install-broker` before enabling Gemini or OpenCode. Managed Grok device
+ `install-broker` before enabling Gemini, OpenCode, Grok, or Composer. Managed Grok device
login is exposed only as `runtime_setup.py login-grok`.
8. Broker updates copy the verified artifact and manifest into an immutable
digest directory, atomically replace the closed launchd plist/state, verify
@@ -335,6 +352,12 @@ architecture, governance, and huge-context actions; `target=composer` is
constrained output-only code generation. Composer has no file, shell, test,
worktree, PR, or merge authority. Both remain deterministically temporarily
unavailable until the signed runtime advertises their exact route/action contracts.
+Grok/Composer requests are broker-only and never fall back to direct runtime
+execution. Grok prose accepts only an explicit `EndTurn` terminal; exact
+`Cancelled` is a named non-success with no retained assistant text and may be
+retried once only when more than ten seconds remain under the original
+deadline. Final UTF-8 input above the inclusive 1,048,576-byte managed ceiling
+returns `input_limit` before provider authentication or spawn.
Grok 4.5 is reachable only through sealed `architecture`, `governance`, or
`huge_context` actions; generic advisory, brainstorm, debate, QA, and fallback
requests cannot select it. OpenCode build is a distinct mutation-capable
@@ -345,6 +368,16 @@ and that family is excluded from selection alongside the active primary family.
unknown artifact-author family emits an independence warning for
non-governance work and fails governance closed.
+For non-trivial code generation, Composer must receive a comprehensive coding
+packet produced by the primary architect and reviewed by an eligible
+distinct-family synchronous architecture complement. The packet defines scope,
+invariants, authority boundaries, exact files and symbols, error taxonomy,
+lifecycle, tests, and acceptance criteria. Claude and Codex frontier primaries
+are the strongest default architecture seats; Grok 4.5 is the qualified
+near-peer/adversarial architecture complement. Composer implements the
+converged packet; it never plans the architecture. Asynchronous inbox review is
+a fallback only when no eligible synchronous complement is available.
+
## Clean public repository invariant
This repository, every reachable ref, and every release archive must remain
diff --git a/changelog.d/2026-07-13-provider-broker.md b/changelog.d/2026-07-13-provider-broker.md
index 66b6be0..c48b77d 100644
--- a/changelog.d/2026-07-13-provider-broker.md
+++ b/changelog.d/2026-07-13-provider-broker.md
@@ -1,6 +1,6 @@
### agent-collab 3.2.0 — zero-idle provider broker
-- Route managed Gemini and OpenCode requests through an explicit, digest-bound
+- Route managed Gemini, OpenCode, Grok, and Composer requests through an explicit, digest-bound
launchd socket broker that starts for one request and returns to zero idle
processes. Add closed install, status, rollback, and uninstall lifecycle
commands with transactional prior-version restoration and no direct fallback.
@@ -8,3 +8,9 @@
explicit central configuration, or the fixed `opencode/glm-5.2` preset while
ignoring ambient and row-level model fallbacks.
- Reject duplicate/non-finite broker responses and state before typed parsing.
+- Preserve typed `cancelled` and `input_limit` Grok/Composer failures, require
+ exact terminal-state validation, and document the one bounded cancellation
+ retry and comprehensive architecture-reviewed Composer coding-packet policy.
+- Strip the Codex Desktop Seatbelt marker from broker-dispatched work so every
+ Grok/Composer attempt validates its own nested read-only sandbox, preserve
+ closed provider-child environments, and keep runtime management direct-only.
diff --git a/plugins/agent-collab/README.md b/plugins/agent-collab/README.md
index 79c7145..f5ce94b 100644
--- a/plugins/agent-collab/README.md
+++ b/plugins/agent-collab/README.md
@@ -48,13 +48,19 @@ labels. It uses a fixed JSON protocol and scrubbed environment. The package
carries both `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json`; both
identify this same 3.2.0 package.
-Gemini and OpenCode are broker-only contracts. Their sealed requests cross a
+Gemini, OpenCode, Grok, and Composer are broker-only contracts. Their sealed requests cross a
mode-`0600`, digest-bound per-user launchd Unix socket; launchd starts the exact
signed runtime for one request, and the broker exits after its single bounded
response. The plist has no keepalive, run-at-load, polling, interval, calendar,
or resident-process trigger. A missing or stale broker is a typed failure—these
-routes never fall back to the direct artifact path. Codex, Grok, Composer, and
-runtime management retain fixed direct exact-artifact execution.
+routes never fall back to the direct artifact path. Codex and runtime
+management retain fixed direct exact-artifact execution.
+The broker strips the Codex Desktop outer-Seatbelt marker before dispatch:
+socket activation does not inherit that client sandbox, so every brokered Grok
+and Composer attempt independently validates its own nested read-only sandbox.
+Provider children receive closed backend-specific environment allowlists and
+fresh private call roots. Broker transport cannot invoke local `status`,
+`prepare`, or `grok_login` management authority.
The current Gemini facade remains typed `containment_error` before Google
provider setup until a separately reviewed completion-only transport exists;
the broker does not treat an agentic CLI mode as a read-only guarantee.
@@ -139,6 +145,22 @@ mutation-capable authority never promote or demote into one another.
| `target=grok` large-corpus extraction | Read-only | Grok 4.5 huge-context ingestion |
| `target=composer` constrained patch/code generation | Output-only | Composer output-only code/patch generation; trusted primary applies and verifies |
+Grok prose succeeds only on an explicit `EndTurn` terminal. Exact `Cancelled`
+is a typed non-success with no retained assistant text; the managed broker may
+retry it once only when more than ten seconds remain under the original
+deadline. Grok and Composer enforce an inclusive 1,048,576-byte final UTF-8
+input ceiling before authentication or spawn and return typed `input_limit`
+above it.
+
+Non-trivial Composer work starts from a comprehensive architectural coding
+packet: scope, invariants, authority boundaries, exact files and symbols, error
+taxonomy, lifecycle, tests, and acceptance criteria. The primary architect owns
+that packet, an eligible distinct-family synchronous frontier architect is the
+adversarial complement, and Composer is implementation-only. Claude and Codex
+frontier primaries are the strongest default architecture seats; Grok 4.5 is
+the qualified near-peer architecture/governance complement. Async inbox review
+is fallback-only when no eligible synchronous complement is available.
+
## Skills
All skills share the same coordinator, family-exclusion policy, sealed authority
diff --git a/plugins/agent-collab/runtime_client.py b/plugins/agent-collab/runtime_client.py
index 54b650e..b3d2e1b 100644
--- a/plugins/agent-collab/runtime_client.py
+++ b/plugins/agent-collab/runtime_client.py
@@ -66,7 +66,7 @@
"request",
}
)
-BROKERED_ROUTES = frozenset({"opencode", "gemini"})
+BROKERED_ROUTES = frozenset({"opencode", "gemini", "grok", "composer"})
BROKER_MAX_REQUEST_BYTES = MAX_REQUEST_BYTES
BROKER_MAX_RESPONSE_BYTES = MAX_RESPONSE_BYTES
BROKER_STATE_MAX_BYTES = 64 * 1024
@@ -149,6 +149,8 @@ def _load_signing_policy():
"auth_error",
"quota_error",
"containment_error",
+ "cancelled",
+ "input_limit",
"timeout",
"output_limit",
"teardown_error",
@@ -174,6 +176,8 @@ class RuntimeStatus(str, Enum):
AUTH_ERROR = "auth_error"
QUOTA_ERROR = "quota_error"
CONTAINMENT_ERROR = "containment_error"
+ CANCELLED = "cancelled"
+ INPUT_LIMIT = "input_limit"
TEARDOWN_ERROR = "teardown_error"
PROVIDER_ERROR = "provider_error"
@@ -1751,6 +1755,8 @@ def _failure_response_result(response: Mapping[str, Any]) -> RuntimeResult | Non
"auth_error": RuntimeStatus.AUTH_ERROR,
"quota_error": RuntimeStatus.QUOTA_ERROR,
"containment_error": RuntimeStatus.CONTAINMENT_ERROR,
+ "cancelled": RuntimeStatus.CANCELLED,
+ "input_limit": RuntimeStatus.INPUT_LIMIT,
"timeout": RuntimeStatus.TIMEOUT,
"output_limit": RuntimeStatus.OUTPUT_LIMIT,
"teardown_error": RuntimeStatus.TEARDOWN_ERROR,
@@ -1815,6 +1821,8 @@ def _parse_response(out: bytes, envelope: object, returncode: int) -> RuntimeRes
"auth_error": RuntimeStatus.AUTH_ERROR,
"quota_error": RuntimeStatus.QUOTA_ERROR,
"containment_error": RuntimeStatus.CONTAINMENT_ERROR,
+ "cancelled": RuntimeStatus.CANCELLED,
+ "input_limit": RuntimeStatus.INPUT_LIMIT,
"timeout": RuntimeStatus.TIMEOUT,
"output_limit": RuntimeStatus.OUTPUT_LIMIT,
"teardown_error": RuntimeStatus.TEARDOWN_ERROR,
diff --git a/tests/test_agent_collab_runtime_client.py b/tests/test_agent_collab_runtime_client.py
index b64a83b..a2d72f9 100644
--- a/tests/test_agent_collab_runtime_client.py
+++ b/tests/test_agent_collab_runtime_client.py
@@ -466,9 +466,14 @@ def test_broker_frame_reader_rejects_truncated_and_oversize_payloads(self) -> No
with self.assertRaises(ValueError):
self.client._read_broker_frame(left, max_bytes=1024, deadline=time.monotonic() + 1)
- def test_opencode_and_gemini_use_broker_without_direct_fallback(self) -> None:
+ def test_all_broker_only_routes_use_broker_without_direct_fallback(self) -> None:
self._fixture()
- for route, action in (("opencode", "plan"), ("gemini", "advisory")):
+ for route, action in (
+ ("opencode", "plan"),
+ ("gemini", "advisory"),
+ ("grok", "architecture"),
+ ("composer", "codegen"),
+ ):
with self.subTest(route=route):
envelope = self._envelope(route=route, action=action)
expected = self.client.RuntimeResult(
@@ -489,8 +494,6 @@ def test_other_routes_retain_direct_runtime_path(self) -> None:
self._fixture()
for route, action in (
("codex", "advisory"),
- ("grok", "architecture"),
- ("composer", "codegen"),
):
with self.subTest(route=route):
envelope = self._envelope(route=route, action=action)
@@ -1508,6 +1511,8 @@ def test_native_typed_failures_are_preserved(self) -> None:
"auth_error": self.client.RuntimeStatus.AUTH_ERROR,
"quota_error": self.client.RuntimeStatus.QUOTA_ERROR,
"containment_error": self.client.RuntimeStatus.CONTAINMENT_ERROR,
+ "cancelled": self.client.RuntimeStatus.CANCELLED,
+ "input_limit": self.client.RuntimeStatus.INPUT_LIMIT,
"timeout": self.client.RuntimeStatus.TIMEOUT,
"output_limit": self.client.RuntimeStatus.OUTPUT_LIMIT,
"teardown_error": self.client.RuntimeStatus.TEARDOWN_ERROR,
From e96ce56d41dd45d879d9d233744f50e3430c57fc Mon Sep 17 00:00:00 2001
From: John Osumi <931193+sumitake@users.noreply.github.com>
Date: Sun, 12 Jul 2026 22:47:00 -0700
Subject: [PATCH 7/9] docs: record broker cancellation lifecycle
---
README.md | 10 +++++++++-
changelog.d/2026-07-13-provider-broker.md | 6 ++++++
plugins/agent-collab/README.md | 4 ++++
3 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d1ea165..43b8331 100644
--- a/README.md
+++ b/README.md
@@ -143,6 +143,11 @@ own nested read-only sandbox. Provider children receive closed,
backend-specific environment allowlists rooted in fresh private call
directories rather than the broker's ambient environment. Broker frames cannot
invoke local runtime-management actions.
+If the client disconnects, the broker propagates cancellation through managed
+OpenCode, Grok, and Composer execution/readiness, reaps provider child groups,
+and discards partial output. Disconnect-driven cancellation is never retried;
+when too little deadline remains to establish the managed boundary, the route
+returns typed `timeout` before provider setup.
The current signed facade still returns typed `containment_error` for Gemini
before Google provider setup because no completion-only Google transport is
proven; socket activation establishes the safe transport boundary but does not
@@ -426,7 +431,10 @@ Security scanning is layered:
Branch protection requires current CI, CodeQL, secret-scan, and governance
results. Sensitive workflow, security, legal, and instruction surfaces have
-explicit CODEOWNERS coverage.
+explicit CODEOWNERS coverage. Repository settings allow squash merges only,
+delete merged branches, require signed commits and linear history, keep the
+default workflow token read-only, and require third-party Actions to be pinned
+to full commit SHAs.
## Development and release verification
diff --git a/changelog.d/2026-07-13-provider-broker.md b/changelog.d/2026-07-13-provider-broker.md
index c48b77d..cd4a60d 100644
--- a/changelog.d/2026-07-13-provider-broker.md
+++ b/changelog.d/2026-07-13-provider-broker.md
@@ -14,3 +14,9 @@
- Strip the Codex Desktop Seatbelt marker from broker-dispatched work so every
Grok/Composer attempt validates its own nested read-only sandbox, preserve
closed provider-child environments, and keep runtime management direct-only.
+- Propagate client disconnect through managed OpenCode, Grok, and Composer
+ calls, reap provider child groups, discard partial output, prohibit retry of
+ disconnect cancellation, and fail with typed `timeout` before setup when the
+ managed-boundary deadline reserve is exhausted.
+- Harden repository configuration with full-SHA Action pin enforcement while
+ retaining squash-only, signed, linear, review-thread-resolved releases.
diff --git a/plugins/agent-collab/README.md b/plugins/agent-collab/README.md
index f5ce94b..15e9233 100644
--- a/plugins/agent-collab/README.md
+++ b/plugins/agent-collab/README.md
@@ -61,6 +61,10 @@ and Composer attempt independently validates its own nested read-only sandbox.
Provider children receive closed backend-specific environment allowlists and
fresh private call roots. Broker transport cannot invoke local `status`,
`prepare`, or `grok_login` management authority.
+Client disconnect propagates cancellation through managed OpenCode, Grok, and
+Composer execution/readiness, reaps provider child groups, and discards partial
+output. Disconnect cancellation is never retried; a deadline below the managed
+setup reserve returns typed `timeout` before provider setup.
The current Gemini facade remains typed `containment_error` before Google
provider setup until a separately reviewed completion-only transport exists;
the broker does not treat an agentic CLI mode as a read-only guarantee.
From f32873dd0bf5d0b4a0d24cba107feea4247fb347 Mon Sep 17 00:00:00 2001
From: John Osumi <931193+sumitake@users.noreply.github.com>
Date: Sun, 12 Jul 2026 22:55:51 -0700
Subject: [PATCH 8/9] runtime: harden broker lifecycle state
---
README.md | 11 ++-
changelog.d/2026-07-13-provider-broker.md | 4 +
plugins/agent-collab/README.md | 16 ++--
plugins/agent-collab/host_policy.py | 19 ++---
plugins/agent-collab/runtime_client.py | 99 +++++++++++++++++++----
tests/test_agent_collab_migration.py | 26 +++++-
tests/test_agent_collab_runtime_client.py | 90 +++++++++++++++++++--
7 files changed, 219 insertions(+), 46 deletions(-)
diff --git a/README.md b/README.md
index 43b8331..90f9a6c 100644
--- a/README.md
+++ b/README.md
@@ -210,10 +210,13 @@ until the signed runtime exposes the complete matrix, including Composer.
`install-broker` before enabling Gemini, OpenCode, Grok, or Composer. Managed Grok device
login is exposed only as `runtime_setup.py login-grok`.
8. Broker updates copy the verified artifact and manifest into an immutable
- digest directory, atomically replace the closed launchd plist/state, verify
- the exact job and socket, activate one protocol-only request, and prove the
- broker process exits. Failed updates restore the prior verified digest or
- report that no active version is proven.
+ artifact-plus-manifest digest directory, atomically replace the closed
+ launchd plist/state, verify the exact job and socket, activate one
+ protocol-only request, and prove the broker process exits. Failed updates
+ restore the complete prior verified state, including its rollback target,
+ or report that no active version is proven. Same-version reactivation keeps
+ its existing rollback target; an unverified current version is never
+ advertised as rollback-safe.
Rollback uses policy-only safe mode. Set `AGENT_COLLAB_SAFE_MODE=1` in the
active host runtime environment and restart that host; all model-execution
diff --git a/changelog.d/2026-07-13-provider-broker.md b/changelog.d/2026-07-13-provider-broker.md
index cd4a60d..846ee29 100644
--- a/changelog.d/2026-07-13-provider-broker.md
+++ b/changelog.d/2026-07-13-provider-broker.md
@@ -20,3 +20,7 @@
managed-boundary deadline reserve is exhausted.
- Harden repository configuration with full-SHA Action pin enforcement while
retaining squash-only, signed, linear, review-thread-resolved releases.
+- Align OpenCode preflight with issuance model resolution; key immutable broker
+ versions by both artifact and manifest digests; preserve complete rollback
+ state across failures and no-op reactivation; reject unverified rollback
+ targets; and treat a never-installed root as typed uninstalled/unavailable.
diff --git a/plugins/agent-collab/README.md b/plugins/agent-collab/README.md
index 15e9233..67febca 100644
--- a/plugins/agent-collab/README.md
+++ b/plugins/agent-collab/README.md
@@ -222,10 +222,12 @@ install and authenticate those through their vendor-supported interfaces.
Policy-only releases return typed `unavailable` for runtime-dependent commands.
Broker installation is explicit; import, readiness, and invocation never
install or mutate launchd state. `install-broker` publishes the verified
-artifact/manifest into an immutable digest directory, atomically activates a
-closed plist, proves the job/socket and one-request process exit, and retains
-one verified prior digest. `broker-status` is read-only and emits no prompt,
-credential, provider output, or private path.
+artifact/manifest into an immutable artifact-plus-manifest digest directory,
+atomically activates a closed plist, proves the job/socket and one-request
+process exit, and retains one verified prior record. Failed updates restore the
+complete prior state; same-version reactivation preserves its rollback target,
+and an unverified version is never recorded as rollback-safe. `broker-status`
+is read-only and emits no prompt, credential, provider output, or private path.
Use the closed rollback/removal actions only when needed:
@@ -236,8 +238,10 @@ python3 "/runtime_setup.py" uninstall-broker
Rollback switches to the one complete prior verified record. Uninstall removes
the exact job, socket, plist, and mutable state while retaining immutable
-version directories. No lifecycle command accepts a caller-selected path,
-label, socket, environment, provider, model, or raw argument.
+version directories. On a machine where the broker was never installed,
+uninstall is an idempotent success and rollback is typed unavailable. No
+lifecycle command accepts a caller-selected path, label, socket, environment,
+provider, model, or raw argument.
## Standalone invocation and local threat limit
diff --git a/plugins/agent-collab/host_policy.py b/plugins/agent-collab/host_policy.py
index bef6816..fc7b72d 100644
--- a/plugins/agent-collab/host_policy.py
+++ b/plugins/agent-collab/host_policy.py
@@ -754,21 +754,12 @@ def startup_preflight(
excluded.add(artifact_family)
rows = contracts.intersection(GOVERNANCE_CONTRACTS) if governance else contracts
families: dict[str, str] = dict(ROUTE_FAMILIES)
- observed_opencode = ""
- if route_models is not None:
- observed_opencode = str(route_models.get("opencode", "")).strip()
- values = explicit_config or {}
- observed_opencode = (
- str(values.get("opencode_model", "")).strip()
- or (
- profile.active_model
- if profile.host_runtime == "opencode" and profile.active_model != "unknown"
- else ""
- )
- or os.environ.get("AGENT_COLLAB_OPENCODE_MODEL", "").strip()
- or observed_opencode
+ # Preflight and issuance must resolve the same selected OpenCode model.
+ # Ambient variables and caller-supplied route rows are not central policy
+ # and therefore cannot change either eligibility or artifact provenance.
+ families["opencode"] = resolve_model_family(
+ _resolve_opencode_model(profile, explicit_config, {})
)
- families["opencode"] = resolve_model_family(observed_opencode)
native_routes = tuple(
route
for route in ("gemini", "codex", "opencode", "grok", "composer")
diff --git a/plugins/agent-collab/runtime_client.py b/plugins/agent-collab/runtime_client.py
index b3d2e1b..53c438b 100644
--- a/plugins/agent-collab/runtime_client.py
+++ b/plugins/agent-collab/runtime_client.py
@@ -707,8 +707,11 @@ def _broker_record_valid(document: object, root: Path, *, allow_previous: bool)
or document.get("label") != BROKER_LABEL
):
return False
- digest = document["artifact_sha256"]
- expected_version = root / "versions" / digest
+ expected_version = _broker_version_path(
+ root,
+ artifact_digest=document["artifact_sha256"],
+ manifest_digest=document["manifest_sha256"],
+ )
expected_paths = {
"runtime_path": expected_version / "agent-collab-runtime",
"manifest_path": expected_version / MANIFEST_NAME,
@@ -1101,10 +1104,27 @@ def _copy_regular_nofollow(source: Path, target: Path, *, limit: int, mode: int)
os.close(descriptor)
+def _broker_version_path(
+ root: Path, *, artifact_digest: str, manifest_digest: str
+) -> Path:
+ if (
+ not isinstance(artifact_digest, str)
+ or not _SHA256_RE.fullmatch(artifact_digest)
+ or not isinstance(manifest_digest, str)
+ or not _SHA256_RE.fullmatch(manifest_digest)
+ ):
+ raise ValueError("provider broker version identity is invalid")
+ return root / "versions" / f"{artifact_digest}-{manifest_digest}"
+
+
def _verify_published_version(
root: Path, *, artifact_digest: str, manifest_digest: str
) -> tuple[Path, Path]:
- version = root / "versions" / artifact_digest
+ version = _broker_version_path(
+ root,
+ artifact_digest=artifact_digest,
+ manifest_digest=manifest_digest,
+ )
runtime = version / "agent-collab-runtime"
manifest = version / MANIFEST_NAME
if _exact_mode(version, expected_type=stat.S_IFDIR, mode=0o500) is None:
@@ -1142,7 +1162,11 @@ def _publish_broker_version(
raise ValueError("verified provider runtime is unavailable")
if _safe_file_identity(resolution.path, executable=True) != resolution.identity:
raise ValueError("verified provider runtime identity changed")
- version = root / "versions" / resolution.artifact_digest
+ version = _broker_version_path(
+ root,
+ artifact_digest=resolution.artifact_digest,
+ manifest_digest=resolution.manifest_digest,
+ )
if version.exists():
return _verify_published_version(
root,
@@ -1203,6 +1227,10 @@ def _state_bytes(document: Mapping[str, Any]) -> bytes:
def _read_current_broker_state(root: Path) -> dict[str, Any] | None:
+ try:
+ root.lstat()
+ except FileNotFoundError:
+ return None
if _exact_mode(root, expected_type=stat.S_IFDIR, mode=0o700) is None:
raise ValueError("provider broker root identity is unsafe")
state_path = root / "state.json"
@@ -1335,7 +1363,11 @@ def _record_for(
plist_digest: str,
previous: Mapping[str, Any] | None,
) -> dict[str, Any]:
- version = root / "versions" / artifact_digest
+ version = _broker_version_path(
+ root,
+ artifact_digest=artifact_digest,
+ manifest_digest=manifest_digest,
+ )
return {
"schema_version": 1,
"artifact_sha256": artifact_digest,
@@ -1375,7 +1407,9 @@ def _activate_broker_record(
*,
target_artifact: str,
target_manifest: str,
- previous: Mapping[str, Any] | None,
+ current_state: Mapping[str, Any] | None,
+ next_previous: Mapping[str, Any] | None,
+ restore_state: Mapping[str, Any] | None,
) -> RuntimeResult:
runtime, _manifest = _verify_published_version(
root,
@@ -1398,10 +1432,11 @@ def _activate_broker_record(
artifact_digest=target_artifact,
manifest_digest=target_manifest,
plist_digest=hashlib.sha256(plist_raw).hexdigest(),
- previous=previous,
+ previous=next_previous,
)
plist_path = root / "broker.plist"
- old_record = None if previous is None else _without_previous(previous)
+ old_record = None if current_state is None else dict(current_state)
+ restore_record = None if restore_state is None else dict(restore_state)
try:
if old_record is not None and not _bootout_broker(plist_path):
raise RuntimeError("existing provider broker could not be stopped")
@@ -1439,11 +1474,11 @@ def _activate_broker_record(
restored = False
try:
_bootout_broker(plist_path)
- if old_record is not None:
+ if restore_record is not None:
prior_runtime, _prior_manifest = _verify_published_version(
root,
- artifact_digest=old_record["artifact_sha256"],
- manifest_digest=old_record["manifest_sha256"],
+ artifact_digest=restore_record["artifact_sha256"],
+ manifest_digest=restore_record["manifest_sha256"],
)
prior_document = _broker_plist_document(
runtime_path=prior_runtime,
@@ -1453,13 +1488,13 @@ def _activate_broker_record(
uid=os.getuid(),
)
prior_raw = _plist_bytes(prior_document)
- if hashlib.sha256(prior_raw).hexdigest() != old_record["plist_sha256"]:
+ if hashlib.sha256(prior_raw).hexdigest() != restore_record["plist_sha256"]:
raise ValueError("prior provider broker plist digest mismatch")
_write_private_atomic(plist_path, prior_raw, mode=0o600)
if not _bootstrap_broker(plist_path) or not _broker_job_loaded():
raise RuntimeError("prior provider broker could not be restored")
_write_private_atomic(
- root / "state.json", _state_bytes(old_record), mode=0o600
+ root / "state.json", _state_bytes(restore_record), mode=0o600
)
restored = True
else:
@@ -1488,14 +1523,36 @@ def install_broker() -> RuntimeResult:
try:
root = _ensure_broker_layout()
current = _read_current_broker_state(root)
+ proven_current = None
if current is not None:
_verify_plist_against_state(root, current)
+ try:
+ _verify_published_version(
+ root,
+ artifact_digest=current["artifact_sha256"],
+ manifest_digest=current["manifest_sha256"],
+ )
+ except ValueError:
+ proven_current = None
+ else:
+ proven_current = current
_publish_broker_version(root, resolution=resolution)
+ same_version = bool(
+ current is not None
+ and current["artifact_sha256"] == resolution.artifact_digest
+ and current["manifest_sha256"] == resolution.manifest_digest
+ )
return _activate_broker_record(
root,
target_artifact=resolution.artifact_digest,
target_manifest=resolution.manifest_digest,
- previous=current,
+ current_state=current,
+ next_previous=(
+ current.get("previous")
+ if same_version and proven_current is not None
+ else proven_current
+ ),
+ restore_state=proven_current,
)
except (OSError, subprocess.SubprocessError, ValueError):
return RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker installation preflight failed")
@@ -1547,12 +1604,24 @@ def rollback_broker() -> RuntimeResult:
if state is None or state["previous"] is None:
return RuntimeResult(RuntimeStatus.UNAVAILABLE, error="provider broker rollback is unavailable")
_verify_plist_against_state(root, state)
+ try:
+ _verify_published_version(
+ root,
+ artifact_digest=state["artifact_sha256"],
+ manifest_digest=state["manifest_sha256"],
+ )
+ except ValueError:
+ proven_current = None
+ else:
+ proven_current = state
previous = dict(state["previous"])
return _activate_broker_record(
root,
target_artifact=previous["artifact_sha256"],
target_manifest=previous["manifest_sha256"],
- previous=_without_previous(state),
+ current_state=state,
+ next_previous=proven_current,
+ restore_state=proven_current,
)
except (OSError, subprocess.SubprocessError, ValueError):
return RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker rollback preflight failed")
diff --git a/tests/test_agent_collab_migration.py b/tests/test_agent_collab_migration.py
index d7f1322..20418b8 100644
--- a/tests/test_agent_collab_migration.py
+++ b/tests/test_agent_collab_migration.py
@@ -941,14 +941,38 @@ def test_open_code_claude_model_is_never_an_autonomous_route(self) -> None:
"active_model": "openai/gpt-5",
"host_runtime": "custom",
"session_identifier": "custom-1",
+ "opencode_model": "anthropic/claude-sonnet",
},
active_legacy_packages=(),
native_capabilities=NATIVE_CAPABILITIES,
safe_mode=False,
- route_models={"opencode": "anthropic/claude-sonnet"},
)
self.assertNotIn("opencode", outcome.eligible_routes)
+ def test_opencode_preflight_matches_issuance_default_and_ignores_ambient_rows(self) -> None:
+ with mock.patch.dict(
+ "os.environ",
+ {"AGENT_COLLAB_OPENCODE_MODEL": "anthropic/claude-ambient"},
+ clear=True,
+ ):
+ outcome = self._preflight(
+ governance=False,
+ explicit_config={
+ "primary_id": "custom",
+ "primary_family": "openai",
+ "active_model": "openai/gpt-5",
+ "host_runtime": "custom",
+ "session_identifier": "custom-default-1",
+ },
+ active_legacy_packages=(),
+ native_capabilities=NATIVE_CAPABILITIES,
+ safe_mode=False,
+ route_models={"opencode": "anthropic/claude-row"},
+ )
+
+ self.assertIn("opencode", outcome.eligible_routes)
+ self.assertNotIn("OpenCode model is not currently observed", outcome.warning)
+
def test_explicit_model_family_conflicts_are_never_governance_trusted(self) -> None:
cases = {
"anthropic/claude-opus": "anthropic",
diff --git a/tests/test_agent_collab_runtime_client.py b/tests/test_agent_collab_runtime_client.py
index a2d72f9..2dfa202 100644
--- a/tests/test_agent_collab_runtime_client.py
+++ b/tests/test_agent_collab_runtime_client.py
@@ -615,8 +615,12 @@ def test_broker_state_and_socket_are_exact_and_fail_closed(self) -> None:
"schema_version": 1,
"artifact_sha256": "a" * 64,
"manifest_sha256": "b" * 64,
- "runtime_path": str(root / "versions" / ("a" * 64) / "agent-collab-runtime"),
- "manifest_path": str(root / "versions" / ("a" * 64) / "runtime-manifest.json"),
+ "runtime_path": str(
+ root / "versions" / f"{'a' * 64}-{'b' * 64}" / "agent-collab-runtime"
+ ),
+ "manifest_path": str(
+ root / "versions" / f"{'a' * 64}-{'b' * 64}" / "runtime-manifest.json"
+ ),
"plist_sha256": "c" * 64,
"socket_path": str(socket_path),
"label": self.client.BROKER_LABEL,
@@ -714,7 +718,7 @@ def test_install_broker_publishes_exact_version_and_socket_activated_state(self)
self.assertEqual(state["artifact_sha256"], expected_artifact)
self.assertIsNone(state["previous"])
self.assertEqual(stat.S_IMODE((root / "broker.plist").stat().st_mode), 0o600)
- version = root / "versions" / expected_artifact
+ version = Path(state["runtime_path"]).parent
self.assertEqual(stat.S_IMODE(version.stat().st_mode), 0o500)
self.assertEqual(
hashlib.sha256((version / "agent-collab-runtime").read_bytes()).hexdigest(),
@@ -725,6 +729,19 @@ def test_install_broker_publishes_exact_version_and_socket_activated_state(self)
self.assertNotIn("KeepAlive", plist)
self.assertNotIn("RunAtLoad", plist)
+ def test_missing_broker_root_is_uninstalled_not_an_integrity_failure(self) -> None:
+ root = self.root / "never-installed"
+ with mock.patch.object(self.client, "_broker_root", return_value=root):
+ rolled_back = self.client.rollback_broker()
+ uninstalled = self.client.uninstall_broker()
+
+ self.assertEqual(rolled_back.status, self.client.RuntimeStatus.UNAVAILABLE)
+ self.assertEqual(uninstalled.status, self.client.RuntimeStatus.OK)
+ self.assertEqual(
+ uninstalled.result,
+ {"installed": False, "versions_retained": True},
+ )
+
def test_broker_update_records_one_verified_rollback_and_rollback_switches(self) -> None:
first = self._fixture(body="#!/bin/sh\nexit 0\n")
first_digest = hashlib.sha256(first.read_bytes()).hexdigest()
@@ -747,15 +764,18 @@ def test_broker_update_records_one_verified_rollback_and_rollback_switches(self)
self.assertEqual(state["artifact_sha256"], first_digest)
self.assertEqual(state["previous"]["artifact_sha256"], second_digest)
- def test_failed_broker_update_restores_the_prior_exact_state(self) -> None:
+ def test_failed_broker_update_restores_the_full_prior_rollback_state(self) -> None:
first = self._fixture(body="#!/bin/sh\nexit 0\n")
- first_digest = hashlib.sha256(first.read_bytes()).hexdigest()
root = self.root / "broker-state"
patches = self._broker_lifecycle_patches(root)
with mock.patch.object(
self.client, "_verify_macos_signature", return_value=(True, "")
), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), patches[0], patches[1], patches[2], patches[3], patches[4]:
self.assertEqual(self.client.install_broker().status, self.client.RuntimeStatus.OK)
+ self._fixture(body="#!/bin/sh\nexit 7\n")
+ self.assertEqual(self.client.install_broker().status, self.client.RuntimeStatus.OK)
+ prior = json.loads((root / "state.json").read_text(encoding="utf-8"))
+ self.assertIsNotNone(prior["previous"])
self._fixture(body="#!/bin/sh\nexit 9\n")
with mock.patch.object(
self.client, "_bootstrap_broker", side_effect=[False, True]
@@ -764,7 +784,65 @@ def test_failed_broker_update_restores_the_prior_exact_state(self) -> None:
self.assertEqual(failed.status, self.client.RuntimeStatus.PROVIDER_ERROR)
self.assertTrue(failed.result["restored_previous"])
state = json.loads((root / "state.json").read_text(encoding="utf-8"))
- self.assertEqual(state["artifact_sha256"], first_digest)
+ self.assertEqual(state, prior)
+
+ def test_noop_broker_install_preserves_existing_rollback_target(self) -> None:
+ root = self.root / "broker-state"
+ patches = self._broker_lifecycle_patches(root)
+ with mock.patch.object(
+ self.client, "_verify_macos_signature", return_value=(True, "")
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), patches[0], patches[1], patches[2], patches[3], patches[4]:
+ self._fixture(body="#!/bin/sh\nexit 0\n")
+ self.assertEqual(self.client.install_broker().status, self.client.RuntimeStatus.OK)
+ self._fixture(body="#!/bin/sh\nexit 7\n")
+ self.assertEqual(self.client.install_broker().status, self.client.RuntimeStatus.OK)
+ prior = json.loads((root / "state.json").read_text(encoding="utf-8"))
+ self.assertIsNotNone(prior["previous"])
+ repeated = self.client.install_broker()
+
+ self.assertEqual(repeated.status, self.client.RuntimeStatus.OK, repeated.error)
+ state = json.loads((root / "state.json").read_text(encoding="utf-8"))
+ self.assertEqual(state, prior)
+
+ def test_same_artifact_with_new_manifest_gets_a_distinct_immutable_version(self) -> None:
+ binary = self._fixture()
+ artifact_digest = hashlib.sha256(binary.read_bytes()).hexdigest()
+ root = self.root / "broker-state"
+ patches = self._broker_lifecycle_patches(root)
+ with mock.patch.object(
+ self.client, "_verify_macos_signature", return_value=(True, "")
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), patches[0], patches[1], patches[2], patches[3], patches[4]:
+ self.assertEqual(self.client.install_broker().status, self.client.RuntimeStatus.OK)
+ first = json.loads((root / "state.json").read_text(encoding="utf-8"))
+ manifest_path = self.root / "runtime-manifest.json"
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
+ manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
+ updated = self.client.install_broker()
+
+ self.assertEqual(updated.status, self.client.RuntimeStatus.OK, updated.error)
+ second = json.loads((root / "state.json").read_text(encoding="utf-8"))
+ self.assertEqual(second["artifact_sha256"], artifact_digest)
+ self.assertNotEqual(second["manifest_sha256"], first["manifest_sha256"])
+ self.assertNotEqual(second["runtime_path"], first["runtime_path"])
+ self.assertEqual(second["previous"]["manifest_sha256"], first["manifest_sha256"])
+ self.assertEqual(len(tuple((root / "versions").iterdir())), 2)
+
+ def test_unverified_current_version_is_not_recorded_as_rollback(self) -> None:
+ root = self.root / "broker-state"
+ patches = self._broker_lifecycle_patches(root)
+ with mock.patch.object(
+ self.client, "_verify_macos_signature", return_value=(True, "")
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), patches[0], patches[1], patches[2], patches[3], patches[4]:
+ self._fixture(body="#!/bin/sh\nexit 0\n")
+ self.assertEqual(self.client.install_broker().status, self.client.RuntimeStatus.OK)
+ current = json.loads((root / "state.json").read_text(encoding="utf-8"))
+ Path(current["manifest_path"]).chmod(0o600)
+ self._fixture(body="#!/bin/sh\nexit 7\n")
+ updated = self.client.install_broker()
+
+ self.assertEqual(updated.status, self.client.RuntimeStatus.OK, updated.error)
+ state = json.loads((root / "state.json").read_text(encoding="utf-8"))
+ self.assertIsNone(state["previous"])
def test_uninstall_removes_mutable_state_but_retains_versions(self) -> None:
self._fixture()
From 5f98eea5c25da1d918eaded9b2ee9b03b9194540 Mon Sep 17 00:00:00 2001
From: John Osumi <931193+sumitake@users.noreply.github.com>
Date: Sun, 12 Jul 2026 22:59:12 -0700
Subject: [PATCH 9/9] runtime: type launchctl lifecycle failures
---
README.md | 3 ++-
changelog.d/2026-07-13-provider-broker.md | 3 ++-
plugins/agent-collab/README.md | 3 ++-
plugins/agent-collab/runtime_client.py | 8 +++----
tests/test_agent_collab_runtime_client.py | 29 +++++++++++++++++++++++
5 files changed, 39 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 90f9a6c..9913de7 100644
--- a/README.md
+++ b/README.md
@@ -216,7 +216,8 @@ until the signed runtime exposes the complete matrix, including Composer.
restore the complete prior verified state, including its rollback target,
or report that no active version is proven. Same-version reactivation keeps
its existing rollback target; an unverified current version is never
- advertised as rollback-safe.
+ advertised as rollback-safe. Bounded `launchctl` timeout/output failures
+ remain typed lifecycle errors rather than escaping as host tracebacks.
Rollback uses policy-only safe mode. Set `AGENT_COLLAB_SAFE_MODE=1` in the
active host runtime environment and restart that host; all model-execution
diff --git a/changelog.d/2026-07-13-provider-broker.md b/changelog.d/2026-07-13-provider-broker.md
index 846ee29..1801bfc 100644
--- a/changelog.d/2026-07-13-provider-broker.md
+++ b/changelog.d/2026-07-13-provider-broker.md
@@ -23,4 +23,5 @@
- Align OpenCode preflight with issuance model resolution; key immutable broker
versions by both artifact and manifest digests; preserve complete rollback
state across failures and no-op reactivation; reject unverified rollback
- targets; and treat a never-installed root as typed uninstalled/unavailable.
+ targets; treat a never-installed root as typed uninstalled/unavailable; and
+ map bounded `launchctl` failures to closed lifecycle results.
diff --git a/plugins/agent-collab/README.md b/plugins/agent-collab/README.md
index 67febca..bc01a1a 100644
--- a/plugins/agent-collab/README.md
+++ b/plugins/agent-collab/README.md
@@ -227,7 +227,8 @@ atomically activates a closed plist, proves the job/socket and one-request
process exit, and retains one verified prior record. Failed updates restore the
complete prior state; same-version reactivation preserves its rollback target,
and an unverified version is never recorded as rollback-safe. `broker-status`
-is read-only and emits no prompt, credential, provider output, or private path.
+is read-only and emits no prompt, credential, provider output, or private path;
+bounded `launchctl` collection failures return a typed lifecycle error.
Use the closed rollback/removal actions only when needed:
diff --git a/plugins/agent-collab/runtime_client.py b/plugins/agent-collab/runtime_client.py
index 53c438b..acefda9 100644
--- a/plugins/agent-collab/runtime_client.py
+++ b/plugins/agent-collab/runtime_client.py
@@ -1554,7 +1554,7 @@ def install_broker() -> RuntimeResult:
),
restore_state=proven_current,
)
- except (OSError, subprocess.SubprocessError, ValueError):
+ except (OSError, RuntimeError, subprocess.SubprocessError, ValueError):
return RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker installation preflight failed")
@@ -1593,7 +1593,7 @@ def broker_status() -> RuntimeResult:
},
error="" if status is RuntimeStatus.OK else "provider broker is installed but inactive",
)
- except (OSError, subprocess.SubprocessError, ValueError):
+ except (OSError, RuntimeError, subprocess.SubprocessError, ValueError):
return RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker status could not be proven")
@@ -1623,7 +1623,7 @@ def rollback_broker() -> RuntimeResult:
next_previous=proven_current,
restore_state=proven_current,
)
- except (OSError, subprocess.SubprocessError, ValueError):
+ except (OSError, RuntimeError, subprocess.SubprocessError, ValueError):
return RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker rollback preflight failed")
@@ -1664,7 +1664,7 @@ def uninstall_broker() -> RuntimeResult:
RuntimeStatus.OK,
result={"installed": False, "versions_retained": True},
)
- except (OSError, subprocess.SubprocessError, ValueError):
+ except (OSError, RuntimeError, subprocess.SubprocessError, ValueError):
return RuntimeResult(RuntimeStatus.INTEGRITY_ERROR, error="provider broker uninstall preflight failed")
diff --git a/tests/test_agent_collab_runtime_client.py b/tests/test_agent_collab_runtime_client.py
index 2dfa202..c89e1f9 100644
--- a/tests/test_agent_collab_runtime_client.py
+++ b/tests/test_agent_collab_runtime_client.py
@@ -893,6 +893,35 @@ def test_launchctl_wrapper_uses_exact_binary_domain_and_scrubbed_environment(sel
{"HOME", "PATH", "LANG", "LC_ALL"},
)
+ def test_lifecycle_entrypoints_map_launchctl_runtime_errors_to_typed_failures(self) -> None:
+ root = self.root / "broker-state"
+ patches = self._broker_lifecycle_patches(root)
+ with mock.patch.object(
+ self.client, "_verify_macos_signature", return_value=(True, "")
+ ), mock.patch.object(self.client, "PLUGIN_ROOT", self.root), patches[0], patches[1], patches[2], patches[3], patches[4]:
+ self._fixture(body="#!/bin/sh\nexit 0\n")
+ self.assertEqual(self.client.install_broker().status, self.client.RuntimeStatus.OK)
+ self._fixture(body="#!/bin/sh\nexit 7\n")
+ self.assertEqual(self.client.install_broker().status, self.client.RuntimeStatus.OK)
+
+ with mock.patch.object(
+ self.client, "_broker_job_loaded", side_effect=RuntimeError("timeout")
+ ):
+ status = self.client.broker_status()
+ with mock.patch.object(
+ self.client, "_activate_broker_record", side_effect=RuntimeError("timeout")
+ ):
+ installed = self.client.install_broker()
+ rolled_back = self.client.rollback_broker()
+ with mock.patch.object(
+ self.client, "_bootout_broker", side_effect=RuntimeError("timeout")
+ ):
+ uninstalled = self.client.uninstall_broker()
+
+ for result in (status, installed, rolled_back, uninstalled):
+ with self.subTest(status=result.status):
+ self.assertEqual(result.status, self.client.RuntimeStatus.INTEGRITY_ERROR)
+
def test_broker_idle_requires_no_live_pid_and_an_explicit_stopped_state(self) -> None:
cases = (
("state = not running\n", True),