generated from coulomb/repo-seed
feat(directive): implement BRIDGE-WP-0004 AccessManagementDirective alignment
- ActorType enum (adm/agt/atm) replaces actor_class string; config validates naming convention (adm-*/agt-*/atm-*) with hard ConfigError on mismatch; legacy 'human'/'automation' values accepted with DeprecationWarning - cert_command: pluggable shell string run before each SSH launch; cert written to state dir; -i cert appended to SSH command alongside -i key - TTL-aware cert refresh: parses Valid-to via ssh-keygen -L; pre-emptive restart 5 min before expiry (no backoff, no attempt increment); CERT_EXPIRING logged - CertAcquisitionError: cert failures trigger normal backoff/retry loop - cert_identity: Key ID parsed from cert and recorded in BRIDGE_CONNECTED event - bridge cert-status: new CLI command; exit 1 on expired cert; --json flag - 233 tests passing, ruff clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import warnings
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
from bridge.models import ActorInfo, HealthCheckConfig, ReconnectPolicy, TunnelConfig
|
||||
from bridge.models import ActorInfo, ActorType, HealthCheckConfig, ReconnectPolicy, TunnelConfig
|
||||
|
||||
|
||||
class ConfigError(Exception):
|
||||
@@ -91,6 +92,10 @@ def _parse_tunnel(name: str, data: dict) -> TunnelConfig:
|
||||
if direction not in ("reverse", "local"):
|
||||
raise ConfigError(f"Tunnel '{name}' direction must be 'reverse' or 'local', got: {direction!r}")
|
||||
|
||||
cert_command = data.get("cert_command") or None
|
||||
if cert_command is not None:
|
||||
cert_command = str(cert_command)
|
||||
|
||||
return TunnelConfig(
|
||||
name=name,
|
||||
host=str(data["host"]),
|
||||
@@ -102,9 +107,40 @@ def _parse_tunnel(name: str, data: dict) -> TunnelConfig:
|
||||
reconnect=reconnect,
|
||||
health_check=health_check,
|
||||
direction=direction,
|
||||
cert_command=cert_command,
|
||||
)
|
||||
|
||||
|
||||
_LEGACY_CLASS_MAP = {
|
||||
"human": ActorType.ADM,
|
||||
"automation": ActorType.ATM,
|
||||
}
|
||||
|
||||
_ACTOR_TYPE_PREFIXES = {
|
||||
ActorType.ADM: "adm-",
|
||||
ActorType.AGT: "agt-",
|
||||
ActorType.ATM: "atm-",
|
||||
}
|
||||
|
||||
|
||||
def _parse_actor_type(name: str, raw_class: str) -> ActorType:
|
||||
if raw_class in _LEGACY_CLASS_MAP:
|
||||
warnings.warn(
|
||||
f"Actor '{name}': class '{raw_class}' is deprecated; "
|
||||
f"use '{_LEGACY_CLASS_MAP[raw_class].value}' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
return _LEGACY_CLASS_MAP[raw_class]
|
||||
try:
|
||||
return ActorType(raw_class)
|
||||
except ValueError:
|
||||
raise ConfigError(
|
||||
f"Actor '{name}' has unknown class '{raw_class}'; "
|
||||
f"must be one of: adm, agt, atm (or legacy: human, automation)"
|
||||
)
|
||||
|
||||
|
||||
def _parse_actors(raw: dict) -> Dict[str, ActorInfo]:
|
||||
actors = {}
|
||||
for name, data in raw.items():
|
||||
@@ -112,9 +148,16 @@ def _parse_actors(raw: dict) -> Dict[str, ActorInfo]:
|
||||
raise ConfigError(f"Actor '{name}' must be a mapping")
|
||||
if "class" not in data:
|
||||
raise ConfigError(f"Actor '{name}' missing required field: class")
|
||||
actor_type = _parse_actor_type(name, str(data["class"]))
|
||||
required_prefix = _ACTOR_TYPE_PREFIXES[actor_type]
|
||||
if not name.startswith(required_prefix):
|
||||
raise ConfigError(
|
||||
f"Actor '{name}' has type '{actor_type.value}' but name must start "
|
||||
f"with '{required_prefix}' (got '{name}')"
|
||||
)
|
||||
actors[name] = ActorInfo(
|
||||
name=name,
|
||||
actor_class=str(data["class"]),
|
||||
actor_type=actor_type,
|
||||
description=str(data.get("description", "")),
|
||||
)
|
||||
return actors
|
||||
|
||||
Reference in New Issue
Block a user