"""Config documents for the fleet: shape, layer selection, hashing.
A document is a list of layers, each with a name, a ``config`` merge-patch and
an optional ``match`` selector::
{"layers": [
{"name": "global", "config": {"settings": {"gnauth_client_id": "..."}}},
{"name": "lab", "match": {"tags": ["lab"]},
"config": {"settings": {"llm_complete_timeout": 120}}},
{"name": "yuki", "match": {"names": ["yuki"]},
"config": {"profiles": {"assistant": {"temperature": 0.2}}}}
]}
Layers apply in document order over the instance's local config; a layer with
no ``match`` (or an empty one) applies to everyone. ``enabled: false`` keeps a
layer in the history but out of the fleet.
The hash is computed over the **layers an instance actually receives**, not
over the whole document, and with exactly the same canonicalisation the
instance uses (``navi/overlay/util.py``). Publishing a layer that targets one
machine therefore leaves every other machine's hash untouched — otherwise each
publish would make the whole fleet re-ack identical content.
"""
from __future__ import annotations
import hashlib
import json
_MATCH_KEYS = {"names", "instance_ids", "tags"}
class ConfigError(ValueError):
"""A published document is not usable. Reported as HTTP 400."""
def canonical_json(value: object) -> str:
# Keep in lockstep with navi/overlay/util.py — the two hashes are compared.
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def content_hash(value: object) -> str:
digest = hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
return f"sha256:{digest}"
def _string_list(value: object, where: str) -> list[str]:
if value is None:
return []
if not isinstance(value, list) or not all(isinstance(v, str) and v for v in value):
raise ConfigError(f"{where} must be a list of non-empty strings")
return list(value)
def validate_layers(raw: object) -> list[dict]:
"""Normalise a published document; raise ``ConfigError`` on bad shape."""
if not isinstance(raw, list):
raise ConfigError("'layers' must be a list")
# An empty list is a real publish: it clears the fleet overlay and every
# instance falls back to its local config.
seen: set[str] = set()
layers: list[dict] = []
for index, layer in enumerate(raw):
if not isinstance(layer, dict):
raise ConfigError(f"layer #{index} is not an object")
name = layer.get("name")
if not isinstance(name, str) or not name.strip():
raise ConfigError(f"layer #{index} needs a non-empty 'name'")
if name in seen:
raise ConfigError(f"duplicate layer name {name!r}")
seen.add(name)
config = layer.get("config")
if not isinstance(config, dict):
raise ConfigError(f"layer {name!r}: 'config' must be an object")
match_raw = layer.get("match") or {}
if not isinstance(match_raw, dict):
raise ConfigError(f"layer {name!r}: 'match' must be an object")
unknown = set(match_raw) - _MATCH_KEYS
if unknown:
raise ConfigError(
f"layer {name!r}: unknown match keys {sorted(unknown)} "
f"(known: {sorted(_MATCH_KEYS)})"
)
match = {
key: _string_list(match_raw.get(key), f"layer {name!r}: match.{key}")
for key in _MATCH_KEYS
if match_raw.get(key) is not None
}
if not any(match.values()):
match = {}
enabled = layer.get("enabled", True)
if not isinstance(enabled, bool):
raise ConfigError(f"layer {name!r}: 'enabled' must be true or false")
layers.append(
{"name": name, "config": config, "match": match, "enabled": enabled}
)
return layers
def _matches(layer: dict, name: str, instance_id: str, tags: list[str]) -> bool:
match = layer.get("match") or {}
if not match: # a global layer
return True
if name and name in match.get("names", []):
return True
if instance_id and instance_id in match.get("instance_ids", []):
return True
return bool(set(tags) & set(match.get("tags", [])))
def effective_layers(
layers: list[dict],
*,
name: str = "",
instance_id: str = "",
tags: list[str] | None = None,
) -> list[dict]:
"""The layers this instance receives, in document order, sans metadata."""
tags = tags or []
return [
{"name": layer["name"], "config": layer["config"]}
for layer in layers
if layer.get("enabled", True) and _matches(layer, name, instance_id, tags)
]
def effective_hash(
layers: list[dict],
*,
name: str = "",
instance_id: str = "",
tags: list[str] | None = None,
) -> str:
return content_hash(
effective_layers(layers, name=name, instance_id=instance_id, tags=tags)
)