Files
adaptive-multi-protocol-gat…/ampg/state_contract.py
T
2026-07-06 11:06:11 -04:00

390 lines
10 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from .config import GatewayConfig, ProtocolConfig, SiteConfig
@dataclass(frozen=True)
class StatePathContract:
site_id: str
protocol: str
role: str
path: Path
owner: str
required: bool
sensitive: bool
description: str
def gateway_state_root(config: GatewayConfig) -> Path:
if config.paths:
return config.paths.state_dir
return config.config_path.parent / ".ampg/state"
def protocol_state_dir(
config: GatewayConfig,
site: SiteConfig,
protocol: ProtocolConfig,
) -> Path:
return gateway_state_root(config) / site.id / protocol.name
def state_contract(config: GatewayConfig) -> list[StatePathContract]:
contracts: list[StatePathContract] = []
for site in config.sites:
for protocol in site.protocols.values():
if not protocol.enabled:
continue
contracts.extend(protocol_state_contract(config, site, protocol))
return contracts
def protocol_state_contract(
config: GatewayConfig,
site: SiteConfig,
protocol: ProtocolConfig,
) -> list[StatePathContract]:
state_dir = protocol_state_dir(config, site, protocol)
contracts = [
_contract(
site,
protocol,
role="state-dir",
path=state_dir,
owner="ampg",
required=True,
sensitive=False,
description="AMPG-owned protocol state directory.",
)
]
if protocol.name == "tor":
contracts.extend(_tor_contract(site, protocol, state_dir))
elif protocol.name == "i2p":
contracts.extend(_i2p_contract(site, protocol, state_dir))
elif protocol.name == "gemini":
contracts.extend(_gemini_contract(site, protocol, state_dir))
elif protocol.name == "clearnet":
contracts.extend(_clearnet_contract(site, protocol, state_dir))
else:
contracts.extend(_generic_contract(site, protocol, state_dir))
return contracts
def address_capture_candidates(
config: GatewayConfig,
site: SiteConfig,
protocol: ProtocolConfig,
) -> tuple[Path, ...]:
candidates: list[Path] = []
configured_file = protocol.options.get("address_file")
if configured_file:
path = Path(str(configured_file)).expanduser()
if not path.is_absolute():
path = config.config_path.parent / path
candidates.append(path.resolve())
candidates.extend(
contract.path
for contract in protocol_state_contract(config, site, protocol)
if contract.role == "address-file"
)
return tuple(candidates)
def daemon_config_path(
config: GatewayConfig,
site: SiteConfig,
protocol: ProtocolConfig,
) -> Path:
state_dir = protocol_state_dir(config, site, protocol)
if protocol.name == "tor":
return state_dir / "torrc"
if protocol.name == "i2p":
return state_dir / "i2pd-tunnels.conf"
if protocol.name == "clearnet":
return state_dir / "nginx-server.conf"
if protocol.name == "gemini":
return state_dir / "agate-plan.txt"
return state_dir / f"{protocol.daemon}-plan.txt"
def _tor_contract(
site: SiteConfig,
protocol: ProtocolConfig,
state_dir: Path,
) -> list[StatePathContract]:
return [
_contract(
site,
protocol,
role="daemon-config",
path=state_dir / "torrc",
owner="ampg",
required=True,
sensitive=False,
description="Reviewed Tor config copied into managed state before start.",
),
_contract(
site,
protocol,
role="daemon-data",
path=state_dir / "data",
owner="daemon",
required=True,
sensitive=False,
description="Tor DataDirectory.",
),
_contract(
site,
protocol,
role="identity-dir",
path=state_dir / "hidden-service",
owner="daemon",
required=True,
sensitive=True,
description="Tor onion-service key material and hostname.",
),
_contract(
site,
protocol,
role="address-file",
path=state_dir / "hidden-service/hostname",
owner="daemon",
required=True,
sensitive=False,
description="Onion hostname captured after Tor creates the hidden service.",
),
_contract(
site,
protocol,
role="address-file",
path=state_dir / "hostname",
owner="operator",
required=False,
sensitive=False,
description="Optional adopted-daemon hostname export.",
),
_contract(
site,
protocol,
role="log-file",
path=state_dir / "tor.log",
owner="daemon",
required=False,
sensitive=False,
description="Managed Tor log file.",
),
]
def _i2p_contract(
site: SiteConfig,
protocol: ProtocolConfig,
state_dir: Path,
) -> list[StatePathContract]:
key_file = str(protocol.options.get("keys_file", f"{site.id}-web.dat"))
return [
_contract(
site,
protocol,
role="daemon-config",
path=state_dir / "i2pd-tunnels.conf",
owner="ampg",
required=True,
sensitive=False,
description="Reviewed i2pd tunnel config copied into managed state before start.",
),
_contract(
site,
protocol,
role="daemon-data",
path=state_dir / "data",
owner="daemon",
required=True,
sensitive=False,
description="Managed i2pd data directory.",
),
_contract(
site,
protocol,
role="identity-key",
path=state_dir / key_file,
owner="daemon",
required=True,
sensitive=True,
description="I2P destination key file for the web tunnel.",
),
_contract(
site,
protocol,
role="address-file",
path=state_dir / "hostname.txt",
owner="daemon",
required=True,
sensitive=False,
description="Preferred I2P hostname or b32 export captured after tunnel creation.",
),
_contract(
site,
protocol,
role="address-file",
path=state_dir / "b32.txt",
owner="daemon",
required=False,
sensitive=False,
description="Alternate I2P b32 export captured after tunnel creation.",
),
_contract(
site,
protocol,
role="address-file",
path=state_dir / "address.txt",
owner="operator",
required=False,
sensitive=False,
description="Optional adopted-daemon address export.",
),
]
def _gemini_contract(
site: SiteConfig,
protocol: ProtocolConfig,
state_dir: Path,
) -> list[StatePathContract]:
return [
_contract(
site,
protocol,
role="daemon-config",
path=state_dir / "agate-plan.txt",
owner="ampg",
required=True,
sensitive=False,
description="Reviewed Agate launch values copied into managed state before start.",
),
_contract(
site,
protocol,
role="certificate",
path=state_dir / f"{site.id}.crt",
owner="ampg",
required=True,
sensitive=False,
description="Gemini certificate.",
),
_contract(
site,
protocol,
role="identity-key",
path=state_dir / f"{site.id}.key",
owner="ampg",
required=True,
sensitive=True,
description="Gemini private key.",
),
]
def _clearnet_contract(
site: SiteConfig,
protocol: ProtocolConfig,
state_dir: Path,
) -> list[StatePathContract]:
return [
_contract(
site,
protocol,
role="daemon-config",
path=state_dir / "nginx-server.conf",
owner="ampg",
required=True,
sensitive=False,
description="Reviewed nginx config copied into managed state before start.",
),
_contract(
site,
protocol,
role="pid-file",
path=state_dir / "nginx.pid",
owner="daemon",
required=False,
sensitive=False,
description="Managed nginx pid file.",
),
_contract(
site,
protocol,
role="log-file",
path=state_dir / "nginx-error.log",
owner="daemon",
required=False,
sensitive=False,
description="Managed nginx error log.",
),
]
def _generic_contract(
site: SiteConfig,
protocol: ProtocolConfig,
state_dir: Path,
) -> list[StatePathContract]:
return [
_contract(
site,
protocol,
role="daemon-config",
path=state_dir / f"{protocol.daemon}-plan.txt",
owner="ampg",
required=False,
sensitive=False,
description="Adapter-specific reviewed daemon config.",
),
_contract(
site,
protocol,
role="address-file",
path=state_dir / "address.txt",
owner="daemon",
required=False,
sensitive=False,
description="Adapter-specific public address export.",
),
_contract(
site,
protocol,
role="address-file",
path=state_dir / "hostname",
owner="daemon",
required=False,
sensitive=False,
description="Adapter-specific public hostname export.",
),
]
def _contract(
site: SiteConfig,
protocol: ProtocolConfig,
*,
role: str,
path: Path,
owner: str,
required: bool,
sensitive: bool,
description: str,
) -> StatePathContract:
return StatePathContract(
site_id=site.id,
protocol=protocol.name,
role=role,
path=path,
owner=owner,
required=required,
sensitive=sensitive,
description=description,
)