Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07b5b-ea58-7ad2-bdbb-0b1c995cfc35
168 lines
5.4 KiB
Python
168 lines
5.4 KiB
Python
import hashlib
|
|
import json
|
|
import math
|
|
import os
|
|
from dataclasses import asdict, dataclass, field
|
|
from pathlib import Path
|
|
from urllib.parse import urlsplit
|
|
|
|
import tomllib
|
|
|
|
|
|
@dataclass
|
|
class Config:
|
|
state_dir: Path = field(
|
|
default_factory=lambda: Path(
|
|
os.environ.get(
|
|
"COORDINATION_STATE_DIR",
|
|
str(
|
|
Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local/state"))
|
|
/ "coordination-engine"
|
|
),
|
|
)
|
|
)
|
|
)
|
|
socket: Path = field(
|
|
default_factory=lambda: Path(
|
|
os.environ.get(
|
|
"COORDINATION_SOCKET",
|
|
str(
|
|
Path(
|
|
os.environ.get(
|
|
"XDG_RUNTIME_DIR", f"/tmp/coordination-{os.getuid()}"
|
|
)
|
|
)
|
|
/ "coordination-engine.sock"
|
|
),
|
|
)
|
|
)
|
|
)
|
|
tamq_socket: Path = field(
|
|
default_factory=lambda: Path(
|
|
os.environ.get(
|
|
"TAMQ_SOCKET",
|
|
str(Path(os.environ.get("XDG_RUNTIME_DIR", "/tmp")) / "tamq.sock"),
|
|
)
|
|
)
|
|
)
|
|
api_base: str = field(
|
|
default_factory=lambda: os.environ.get(
|
|
"STATEHUB_API_BASE", "http://127.0.0.1:8000"
|
|
)
|
|
)
|
|
repos: list[str] = field(default_factory=list)
|
|
endpoints: dict[str, str] = field(default_factory=dict)
|
|
poll_interval: float = 15
|
|
lease_seconds: float = 30
|
|
renew_interval: float = 10
|
|
timeout: float = 5
|
|
busy_timeout: float = 5
|
|
max_attempts: int = 4
|
|
retry_backoff: list[float] = field(default_factory=lambda: [5, 15, 60, 300])
|
|
policy_profile: str = "default"
|
|
# Only these classes may be requested automatically. Unknown classes stop.
|
|
allow: list[str] = field(
|
|
default_factory=lambda: [
|
|
"repo_inspect",
|
|
"repo_edit",
|
|
"local_checks",
|
|
"state_updates",
|
|
"tmux_wake",
|
|
]
|
|
)
|
|
|
|
def validate(self):
|
|
for key in (
|
|
"poll_interval",
|
|
"lease_seconds",
|
|
"renew_interval",
|
|
"timeout",
|
|
"busy_timeout",
|
|
):
|
|
value = getattr(self, key)
|
|
if (
|
|
isinstance(value, bool)
|
|
or not isinstance(value, (float, int))
|
|
or not math.isfinite(value)
|
|
or value <= 0
|
|
):
|
|
raise ValueError(f"invalid {key}")
|
|
if self.renew_interval >= self.lease_seconds:
|
|
raise ValueError("renew_interval must be shorter than lease_seconds")
|
|
if type(self.max_attempts) is not int or not 0 <= self.max_attempts <= 9:
|
|
raise ValueError("max_attempts must be 0..9")
|
|
if not self.retry_backoff or any(
|
|
type(v) not in (int, float) or not math.isfinite(v) or v <= 0
|
|
for v in self.retry_backoff
|
|
):
|
|
raise ValueError("retry_backoff must contain positive finite delays")
|
|
if self.policy_profile != "default" or not set(self.allow) <= {
|
|
"repo_inspect",
|
|
"repo_edit",
|
|
"local_checks",
|
|
"state_updates",
|
|
"tmux_wake",
|
|
}:
|
|
raise ValueError("unsupported policy profile or action grant")
|
|
if not isinstance(self.repos, list) or any(
|
|
not isinstance(r, str)
|
|
or not r
|
|
or any(
|
|
c
|
|
not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._"
|
|
for c in r
|
|
)
|
|
for r in self.repos
|
|
):
|
|
raise ValueError("repos must contain exact repository slugs")
|
|
if not isinstance(self.endpoints, dict) or any(
|
|
k not in self.repos or not isinstance(v, str) or not v
|
|
for k, v in self.endpoints.items()
|
|
):
|
|
raise ValueError("endpoints must map configured repos to endpoint IDs")
|
|
url = urlsplit(self.api_base)
|
|
if (
|
|
url.scheme not in {"http", "https"}
|
|
or not url.hostname
|
|
or url.username
|
|
or url.password
|
|
or url.query
|
|
or url.fragment
|
|
):
|
|
raise ValueError("invalid State Hub URL; credentials are not configuration")
|
|
for name in ("state_dir", "socket", "tamq_socket"):
|
|
setattr(self, name, Path(getattr(self, name)).expanduser().absolute())
|
|
return self
|
|
|
|
@property
|
|
def version(self):
|
|
return hashlib.sha256(
|
|
json.dumps(asdict(self), default=str, sort_keys=True).encode()
|
|
).hexdigest()[:16]
|
|
|
|
@classmethod
|
|
def load(cls, path=None):
|
|
path = Path(
|
|
path
|
|
or os.environ.get(
|
|
"COORDINATION_CONFIG",
|
|
str(
|
|
Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
/ "coordination-engine/config.toml"
|
|
),
|
|
)
|
|
)
|
|
data = (
|
|
tomllib.loads(path.read_text()).get("coordination", {})
|
|
if path.exists()
|
|
else {}
|
|
)
|
|
for env, key in [
|
|
("COORDINATION_STATE_DIR", "state_dir"),
|
|
("COORDINATION_SOCKET", "socket"),
|
|
("TAMQ_SOCKET", "tamq_socket"),
|
|
("STATEHUB_API_BASE", "api_base"),
|
|
]:
|
|
if env in os.environ:
|
|
data[key] = os.environ[env]
|
|
return cls(**data).validate()
|