diff --git a/deploy/decnet-attribution.service.j2 b/deploy/decnet-attribution.service.j2 deleted file mode 100644 index 22afbd97..00000000 --- a/deploy/decnet-attribution.service.j2 +++ /dev/null @@ -1,45 +0,0 @@ -[Unit] -Description=DECNET Attribution Engine v0 (per-(identity, primitive) state machine) -Documentation=https://git.resacachile.cl/anti/DECNET/wiki/Workers#attribution -After=network-online.target decnet-bus.service -Wants=network-online.target decnet-bus.service - -[Service] -Type=simple -User={{ user }} -Group={{ group }} -WorkingDirectory={{ install_dir }} -EnvironmentFile=-{{ install_dir }}/.env.local -Environment=DECNET_SYSTEM_LOGS=/var/log/decnet/decnet.attribution.log -# Subscribes to attacker.observation.> and, for each event, ensures a -# stub AttackerIdentity row, runs the per-ValueKind merger over the -# full identity-keyed observation series, upserts the derived state in -# attribution_state, and publishes attribution.profile.state_changed -# only on transition. Periodic tick (default 60s) fires -# attribution.profile.multi_actor_suspected when >= 2 primitives flag -# the same identity. Closes DEBT-051. -ExecStart={{ venv_dir }}/bin/decnet attribution -StandardOutput=append:/var/log/decnet/decnet.attribution.log -StandardError=append:/var/log/decnet/decnet.attribution.log - -CapabilityBoundingSet= -AmbientCapabilities= - -# Security Hardening -NoNewPrivileges=yes -ProtectSystem=full -ProtectHome=read-only -PrivateTmp=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -RestrictSUIDSGID=yes -LockPersonality=yes -ReadWritePaths={{ install_dir }} /var/log/decnet - -Restart=on-failure -RestartSec=5 -TimeoutStopSec=15 - -[Install] -WantedBy=multi-user.target diff --git a/deploy/decnet-campaign-clusterer.service.j2 b/deploy/decnet-campaign-clusterer.service.j2 deleted file mode 100644 index 8bc128d8..00000000 --- a/deploy/decnet-campaign-clusterer.service.j2 +++ /dev/null @@ -1,52 +0,0 @@ -[Unit] -Description=DECNET Campaign Clusterer (identities → campaigns / operations) -Documentation=https://git.resacachile.cl/anti/DECNET/wiki/Workers#campaign-clusterer -After=network-online.target decnet-bus.service decnet-clusterer.service -Wants=network-online.target decnet-bus.service decnet-clusterer.service - -[Service] -Type=simple -User={{ user }} -Group={{ group }} -WorkingDirectory={{ install_dir }} -EnvironmentFile=-{{ install_dir }}/.env.local -Environment=DECNET_SYSTEM_LOGS=/var/log/decnet/decnet.campaign-clusterer.log -# Subscribes to identity.>; falls back to a 60s slow-tick poll when -# the bus is idle or unavailable. Reads AttackerIdentity rows, -# projects them into the campaign-level similarity graph -# (phase-handoff / shared-infra / temporal overlap / cohort), runs -# union-find, writes campaigns rows + sets -# attacker_identities.campaign_id, and publishes campaign.formed / -# campaign.identity.assigned / campaign.merged / campaign.unmerged -# plus the cross-family identity.campaign.assigned for identity-side -# subscribers. -# -# Master-only: gated via MASTER_ONLY_COMMANDS in decnet/cli/gating.py. -# Sits one layer above decnet-clusterer (the After=/Wants= ensures the -# identity layer is up first; the campaign clusterer then wakes on -# identity.> events fired by it). -ExecStart={{ venv_dir }}/bin/decnet campaign-clusterer -StandardOutput=append:/var/log/decnet/decnet.campaign-clusterer.log -StandardError=append:/var/log/decnet/decnet.campaign-clusterer.log - -CapabilityBoundingSet= -AmbientCapabilities= - -# Security Hardening -NoNewPrivileges=yes -ProtectSystem=full -ProtectHome=read-only -PrivateTmp=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -RestrictSUIDSGID=yes -LockPersonality=yes -ReadWritePaths={{ install_dir }} /var/log/decnet - -Restart=on-failure -RestartSec=5 -TimeoutStopSec=15 - -[Install] -WantedBy=multi-user.target diff --git a/deploy/decnet-clusterer.service.j2 b/deploy/decnet-clusterer.service.j2 deleted file mode 100644 index 22e8d990..00000000 --- a/deploy/decnet-clusterer.service.j2 +++ /dev/null @@ -1,47 +0,0 @@ -[Unit] -Description=DECNET Identity Clusterer (per-IP observations → identities) -Documentation=https://git.resacachile.cl/anti/DECNET/wiki/Workers#identity-clusterer -After=network-online.target decnet-bus.service -Wants=network-online.target decnet-bus.service - -[Service] -Type=simple -User={{ user }} -Group={{ group }} -WorkingDirectory={{ install_dir }} -EnvironmentFile=-{{ install_dir }}/.env.local -Environment=DECNET_SYSTEM_LOGS=/var/log/decnet/decnet.clusterer.log -# Subscribes to attacker.observed and attacker.scored; falls back to a -# 60s slow-tick poll when the bus is idle or unavailable. Reads -# Attacker rows, projects per-IP observations into the similarity -# graph (JA3 / HASSH / payload-hash / C2-endpoint), runs union-find, -# writes attacker_identities rows + sets attackers.identity_id, and -# publishes identity.formed / identity.observation.linked / -# identity.merged / identity.unmerged. -# -# Master-only: gated via MASTER_ONLY_COMMANDS in decnet/cli/gating.py. -ExecStart={{ venv_dir }}/bin/decnet clusterer -StandardOutput=append:/var/log/decnet/decnet.clusterer.log -StandardError=append:/var/log/decnet/decnet.clusterer.log - -CapabilityBoundingSet= -AmbientCapabilities= - -# Security Hardening -NoNewPrivileges=yes -ProtectSystem=full -ProtectHome=read-only -PrivateTmp=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -RestrictSUIDSGID=yes -LockPersonality=yes -ReadWritePaths={{ install_dir }} /var/log/decnet - -Restart=on-failure -RestartSec=5 -TimeoutStopSec=15 - -[Install] -WantedBy=multi-user.target diff --git a/deploy/decnet-enrich.service.j2 b/deploy/decnet-enrich.service.j2 deleted file mode 100644 index 344c1e4c..00000000 --- a/deploy/decnet-enrich.service.j2 +++ /dev/null @@ -1,47 +0,0 @@ -[Unit] -Description=DECNET Threat-Intel Enrichment (GreyNoise + AbuseIPDB + abuse.ch) -Documentation=https://git.resacachile.cl/anti/DECNET/wiki/Workers#intel-enrichment -After=network-online.target decnet-bus.service -Wants=network-online.target decnet-bus.service - -[Service] -Type=simple -User={{ user }} -Group={{ group }} -WorkingDirectory={{ install_dir }} -EnvironmentFile=-{{ install_dir }}/.env.local -Environment=DECNET_SYSTEM_LOGS=/var/log/decnet/decnet.enrich.log -# Subscribes to attacker.observed and attacker.scored; falls back to a 60s -# slow-tick poll when the bus is idle or unavailable. Per attacker IP fans -# out across the configured intel providers, writes the aggregate verdict -# to attacker_intel, and publishes attacker.intel.enriched. -# -# Free-tier API keys are read from .env.local: -# DECNET_GREYNOISE_API_KEY= (optional, lifts rate limit) -# DECNET_ABUSEIPDB_API_KEY= (required for AbuseIPDB lookups) -# DECNET_THREATFOX_API_KEY= (optional, lifts rate limit) -ExecStart={{ venv_dir }}/bin/decnet enrich -StandardOutput=append:/var/log/decnet/decnet.enrich.log -StandardError=append:/var/log/decnet/decnet.enrich.log - -CapabilityBoundingSet= -AmbientCapabilities= - -# Security Hardening -NoNewPrivileges=yes -ProtectSystem=full -ProtectHome=read-only -PrivateTmp=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -RestrictSUIDSGID=yes -LockPersonality=yes -ReadWritePaths={{ install_dir }} /var/log/decnet - -Restart=on-failure -RestartSec=5 -TimeoutStopSec=15 - -[Install] -WantedBy=multi-user.target diff --git a/deploy/decnet-mutator.service.j2 b/deploy/decnet-mutator.service.j2 deleted file mode 100644 index 8353cee8..00000000 --- a/deploy/decnet-mutator.service.j2 +++ /dev/null @@ -1,41 +0,0 @@ -[Unit] -Description=DECNET Mutator (runtime fleet mutation watch loop) -Documentation=https://git.resacachile.cl/anti/DECNET/wiki/Workers#mutator -After=network-online.target docker.service decnet-bus.service -Wants=network-online.target decnet-bus.service -Requires=docker.service - -[Service] -Type=simple -User={{ user }} -Group={{ group }} -# Mutator recomposes decky services via docker compose. -SupplementaryGroups=docker -WorkingDirectory={{ install_dir }} -EnvironmentFile=-{{ install_dir }}/.env.local -Environment=DECNET_SYSTEM_LOGS=/var/log/decnet/decnet.mutator.log -ExecStart={{ venv_dir }}/bin/decnet mutate --watch -StandardOutput=append:/var/log/decnet/decnet.mutator.log -StandardError=append:/var/log/decnet/decnet.mutator.log - -CapabilityBoundingSet= -AmbientCapabilities= - -# Security Hardening -NoNewPrivileges=yes -ProtectSystem=full -ProtectHome=read-only -PrivateTmp=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -RestrictSUIDSGID=yes -LockPersonality=yes -ReadWritePaths={{ install_dir }} /var/log/decnet - -Restart=on-failure -RestartSec=5 -TimeoutStopSec=15 - -[Install] -WantedBy=multi-user.target diff --git a/deploy/decnet-orchestrator.service.j2 b/deploy/decnet-orchestrator.service.j2 deleted file mode 100644 index 4553bf07..00000000 --- a/deploy/decnet-orchestrator.service.j2 +++ /dev/null @@ -1,50 +0,0 @@ -[Unit] -Description=DECNET Orchestrator (synthetic life-injection — inter-decky traffic, file plants, email drops) -Documentation=https://git.resacachile.cl/anti/DECNET/wiki/Workers#orchestrator -After=network-online.target decnet-bus.service -Wants=network-online.target decnet-bus.service - -[Service] -Type=simple -User={{ user }} -Group={{ group }} -WorkingDirectory={{ install_dir }} -EnvironmentFile=-{{ install_dir }}/.env.local -Environment=DECNET_SYSTEM_LOGS=/var/log/decnet/decnet.orchestrator.log -# Realism content engine — LLM + persona-pool config used by the -# email + (post-stage-6) file-class enrichment paths. See -# decnet/realism/llm/factory.py and decnet/realism/personas_pool.py. -Environment=DECNET_REALISM_LLM=ollama -Environment=DECNET_REALISM_MODEL=llama3.1 -Environment=DECNET_REALISM_TIMEOUT=60 -Environment=DECNET_REALISM_PERSONAS=/etc/decnet/email_personas.json -ExecStart={{ venv_dir }}/bin/decnet orchestrate -StandardOutput=append:/var/log/decnet/decnet.orchestrator.log -StandardError=append:/var/log/decnet/decnet.orchestrator.log - -# The orchestrator drives `docker exec` against decky containers, so it -# needs membership in the docker group. It does NOT bind to the network, -# launch new containers, or write outside its own logs and install dir. -SupplementaryGroups=docker - -CapabilityBoundingSet= -AmbientCapabilities= - -# Security Hardening -NoNewPrivileges=yes -ProtectSystem=full -ProtectHome=read-only -PrivateTmp=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -RestrictSUIDSGID=yes -LockPersonality=yes -ReadWritePaths={{ install_dir }} /var/log/decnet - -Restart=on-failure -RestartSec=5 -TimeoutStopSec=15 - -[Install] -WantedBy=multi-user.target diff --git a/deploy/decnet-profiler.service.j2 b/deploy/decnet-profiler.service.j2 deleted file mode 100644 index 3f521627..00000000 --- a/deploy/decnet-profiler.service.j2 +++ /dev/null @@ -1,38 +0,0 @@ -[Unit] -Description=DECNET Profiler (attacker profiling and scoring) -Documentation=https://git.resacachile.cl/anti/DECNET/wiki/Workers#profiler -After=network-online.target decnet-bus.service -Wants=network-online.target decnet-bus.service - -[Service] -Type=simple -User={{ user }} -Group={{ group }} -WorkingDirectory={{ install_dir }} -EnvironmentFile=-{{ install_dir }}/.env.local -Environment=DECNET_SYSTEM_LOGS=/var/log/decnet/decnet.profiler.log -ExecStart={{ venv_dir }}/bin/decnet profiler -StandardOutput=append:/var/log/decnet/decnet.profiler.log -StandardError=append:/var/log/decnet/decnet.profiler.log - -CapabilityBoundingSet= -AmbientCapabilities= - -# Security Hardening -NoNewPrivileges=yes -ProtectSystem=full -ProtectHome=read-only -PrivateTmp=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -RestrictSUIDSGID=yes -LockPersonality=yes -ReadWritePaths={{ install_dir }} /var/log/decnet - -Restart=on-failure -RestartSec=5 -TimeoutStopSec=15 - -[Install] -WantedBy=multi-user.target diff --git a/deploy/decnet-reconciler.service.j2 b/deploy/decnet-reconciler.service.j2 deleted file mode 100644 index 47663183..00000000 --- a/deploy/decnet-reconciler.service.j2 +++ /dev/null @@ -1,47 +0,0 @@ -[Unit] -Description=DECNET Fleet Reconciler (converges decnet-state.json ↔ fleet_deckies DB ↔ docker) -Documentation=https://git.resacachile.cl/anti/DECNET/wiki/Workers#reconciler -After=network-online.target decnet-bus.service -Wants=network-online.target decnet-bus.service - -[Service] -Type=simple -User={{ user }} -Group={{ group }} -WorkingDirectory={{ install_dir }} -EnvironmentFile=-{{ install_dir }}/.env.local -Environment=DECNET_SYSTEM_LOGS=/var/log/decnet/decnet.reconciler.log -ExecStart={{ venv_dir }}/bin/decnet reconcile -StandardOutput=append:/var/log/decnet/decnet.reconciler.log -StandardError=append:/var/log/decnet/decnet.reconciler.log - -# The reconciler queries the docker daemon (via `docker.from_env()`) to -# observe per-container state. Membership in the docker group lets it -# read /var/run/docker.sock without root. It does NOT exec into -# containers, bind to the network, or spawn new containers. -SupplementaryGroups=docker - -CapabilityBoundingSet= -AmbientCapabilities= - -# Security Hardening -NoNewPrivileges=yes -ProtectSystem=full -ProtectHome=read-only -PrivateTmp=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -RestrictSUIDSGID=yes -LockPersonality=yes -# Read-only access to /var/lib/decnet so we can read decnet-state.json. -# Read-write access only to install_dir + log dir. -ReadOnlyPaths=/var/lib/decnet -ReadWritePaths={{ install_dir }} /var/log/decnet - -Restart=on-failure -RestartSec=5 -TimeoutStopSec=15 - -[Install] -WantedBy=multi-user.target diff --git a/deploy/decnet-reuse-correlator.service.j2 b/deploy/decnet-reuse-correlator.service.j2 deleted file mode 100644 index 23f78988..00000000 --- a/deploy/decnet-reuse-correlator.service.j2 +++ /dev/null @@ -1,41 +0,0 @@ -[Unit] -Description=DECNET Credential-Reuse Correlator (cross-target secret-reuse detection) -Documentation=https://git.resacachile.cl/anti/DECNET/wiki/Workers#reuse-correlator -After=network-online.target decnet-bus.service -Wants=network-online.target decnet-bus.service - -[Service] -Type=simple -User={{ user }} -Group={{ group }} -WorkingDirectory={{ install_dir }} -EnvironmentFile=-{{ install_dir }}/.env.local -Environment=DECNET_SYSTEM_LOGS=/var/log/decnet/decnet.reuse-correlator.log -# Subscribes to credential.captured and attacker.observed; falls back to -# a 60s slow-tick poll when the bus is idle or unavailable. Publishes -# credential.reuse.detected once per new/grown finding. -ExecStart={{ venv_dir }}/bin/decnet reuse-correlate -StandardOutput=append:/var/log/decnet/decnet.reuse-correlator.log -StandardError=append:/var/log/decnet/decnet.reuse-correlator.log - -CapabilityBoundingSet= -AmbientCapabilities= - -# Security Hardening -NoNewPrivileges=yes -ProtectSystem=full -ProtectHome=read-only -PrivateTmp=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -RestrictSUIDSGID=yes -LockPersonality=yes -ReadWritePaths={{ install_dir }} /var/log/decnet - -Restart=on-failure -RestartSec=5 -TimeoutStopSec=15 - -[Install] -WantedBy=multi-user.target diff --git a/deploy/decnet-ttp.service.j2 b/deploy/decnet-ttp.service.j2 deleted file mode 100644 index e466084c..00000000 --- a/deploy/decnet-ttp.service.j2 +++ /dev/null @@ -1,57 +0,0 @@ -[Unit] -Description=DECNET TTP Tagger (MITRE ATT&CK technique tagging) -Documentation=https://git.resacachile.cl/anti/DECNET/wiki/Workers#ttp-tagger -After=network-online.target decnet-bus.service decnet-clusterer.service decnet-enrich.service decnet-reuse-correlator.service -Wants=network-online.target decnet-bus.service - -[Service] -Type=simple -User={{ user }} -Group={{ group }} -WorkingDirectory={{ install_dir }} -EnvironmentFile=-{{ install_dir }}/.env.local -Environment=DECNET_SYSTEM_LOGS=/var/log/decnet/decnet.ttp.log -# Subscribes to attacker.session.ended (primary), attacker.observed, -# attacker.intel.enriched, identity.formed, identity.merged, -# credential.reuse.detected, email.received, and canary.> ; falls back -# to a 60s slow-tick poll when the bus is idle or unavailable. Each -# event is dispatched through the CompositeTagger (RuleEngine + -# Behavioral / Intel / Email / CanaryFingerprint / Identity / -# Credential lifters), persisted via the idempotent INSERT OR IGNORE -# repo write, and ttp.tagged + ttp.rule.fired. are -# published only when the insert returned a non-zero rowcount -# (loop-prevention invariant — see TTP_TAGGING.md §"Bus topics"). -# -# Master-only: gated via MASTER_ONLY_COMMANDS in decnet/cli/gating.py. -# Sits one layer above the identity / intel / reuse-correlator -# workers — the After= dependencies ensure their bus topics are live -# before the TTP worker subscribes. -ExecStart={{ venv_dir }}/bin/decnet ttp -StandardOutput=append:/var/log/decnet/decnet.ttp.log -StandardError=append:/var/log/decnet/decnet.ttp.log - -CapabilityBoundingSet= -AmbientCapabilities= - -# Security Hardening -NoNewPrivileges=yes -ProtectSystem=full -# Dev installs under /home need ProtectHome=read-only (the worker -# reads ./rules/ttp/ from the project root, which lives under /home -# on dev boxes — read-only suffices because the FilesystemRuleStore -# only reads YAMLs, never writes). -ProtectHome=read-only -PrivateTmp=yes -ProtectKernelTunables=yes -ProtectKernelModules=yes -ProtectControlGroups=yes -RestrictSUIDSGID=yes -LockPersonality=yes -ReadWritePaths={{ install_dir }} /var/log/decnet - -Restart=on-failure -RestartSec=5 -TimeoutStopSec=15 - -[Install] -WantedBy=multi-user.target diff --git a/deploy/decnet.target b/deploy/decnet.target index 71f0b25f..960fdd59 100644 --- a/deploy/decnet.target +++ b/deploy/decnet.target @@ -5,23 +5,21 @@ Documentation=https://git.resacachile.cl/anti/DECNET/wiki/Workers # heartbeats to it), then the API + data-plane workers. systemd resolves the # actual ordering via each unit's own After=/Wants= on decnet-bus.service — # this target is a convenience grouping, not an ordering primitive. +# Consolidated since 1.1/1.2: the batch (reconcile/enrich/orchestrate/mutate) +# and cpu (clusterer/campaign-clusterer/attribution/reuse-correlate) supervisor +# groups and the heavy (profiler/ttp) prefork fleet replace their per-worker +# units. The standalone workers below kept their own units. Wants=decnet-bus.service \ decnet-api.service \ decnet-web.service \ decnet-collector.service \ - decnet-profiler.service \ decnet-sniffer.service \ decnet-prober.service \ - decnet-mutator.service \ - decnet-reconciler.service \ - decnet-reuse-correlator.service \ - decnet-enrich.service \ - decnet-clusterer.service \ - decnet-campaign-clusterer.service \ - decnet-ttp.service \ decnet-webhook.service \ decnet-canary.service \ - decnet-orchestrator.service + decnet-supervise-batch.service \ + decnet-supervise-cpu.service \ + decnet-fleet-heavy.service After=decnet-bus.service [Install] diff --git a/tests/deploy/test_orchestrator_unit.py b/tests/deploy/test_orchestrator_unit.py deleted file mode 100644 index 63446bd3..00000000 --- a/tests/deploy/test_orchestrator_unit.py +++ /dev/null @@ -1,152 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -"""Smoke tests for the orchestrator systemd unit + decnet.target wiring. - -These don't exercise systemd (the test host wouldn't have it); they -just assert the static contents of ``deploy/decnet-orchestrator.service.j2`` -and ``deploy/decnet.target`` match what ``decnet init`` will install. - -Anti-regressions for two specific failure modes: - -1. After the realism migration (stage 5), ``decnet-emailgen.service`` - is gone — the orchestrator covers the email branch. A regression - that re-introduces the emailgen unit file or the ``decnet.target`` - entry would only surface on a fresh host install; cheap to catch - here. -2. The orchestrator unit must ship the ``DECNET_REALISM_*`` env block - so the LLM enrichment + persona-pool path are configurable per - host without editing the .j2. -""" -from __future__ import annotations - -from pathlib import Path - -import pytest - - -REPO = Path(__file__).resolve().parent.parent.parent -DEPLOY = REPO / "deploy" - - -@pytest.fixture -def unit_text() -> str: - return (DEPLOY / "decnet-orchestrator.service.j2").read_text() - - -@pytest.fixture -def target_text() -> str: - return (DEPLOY / "decnet.target").read_text() - - -# ── orchestrator unit ──────────────────────────────────────────────────────── - - -def test_orchestrator_unit_exists(): - assert (DEPLOY / "decnet-orchestrator.service.j2").exists() - - -def test_orchestrator_unit_uses_orchestrate_subcommand(unit_text): - assert "decnet orchestrate" in unit_text - - -def test_orchestrator_unit_has_docker_supplementary_group(unit_text): - """SSHDriver shells `docker exec` against decky containers — without - this group the worker can't reach the docker socket.""" - assert "SupplementaryGroups=docker" in unit_text - - -def test_orchestrator_unit_orders_after_bus(unit_text): - """Bus must be up first so heartbeats publish from the start.""" - assert "After=network-online.target decnet-bus.service" in unit_text - assert "Wants=network-online.target decnet-bus.service" in unit_text - - -def test_orchestrator_unit_has_security_hardening(unit_text): - for directive in ( - "NoNewPrivileges=yes", - "ProtectSystem=full", - "ProtectHome=read-only", - "PrivateTmp=yes", - "ProtectKernelTunables=yes", - "ProtectKernelModules=yes", - "ProtectControlGroups=yes", - "RestrictSUIDSGID=yes", - "LockPersonality=yes", - ): - assert directive in unit_text, f"missing {directive}" - - -def test_orchestrator_unit_writes_to_log_dir(unit_text): - assert "/var/log/decnet/decnet.orchestrator.log" in unit_text - assert "ReadWritePaths={{ install_dir }} /var/log/decnet" in unit_text - - -def test_orchestrator_unit_restart_on_failure(unit_text): - assert "Restart=on-failure" in unit_text - - -def test_orchestrator_unit_carries_realism_env_block(unit_text): - """Stage 5 + 6 contract: the orchestrator's LLM enrichment and - persona-pool path are configured per host via DECNET_REALISM_* - env vars. Shipping them in the .j2 means an operator who never - drops a .env.local still gets sane defaults.""" - for var in ( - "DECNET_REALISM_LLM", - "DECNET_REALISM_MODEL", - "DECNET_REALISM_TIMEOUT", - "DECNET_REALISM_PERSONAS", - ): - assert var in unit_text, f"missing {var} in unit" - - -def test_orchestrator_unit_does_not_carry_legacy_emailgen_envs(unit_text): - """Pre-v1 clean break per the realism migration: the - DECNET_EMAILGEN_* env vars are no longer read. Carrying them in - the unit would mislead operators into thinking they still apply.""" - for legacy in ( - "DECNET_EMAILGEN_LLM", - "DECNET_EMAILGEN_MODEL", - "DECNET_EMAILGEN_TIMEOUT", - "DECNET_EMAILGEN_PERSONAS", - ): - assert legacy not in unit_text, ( - f"legacy env {legacy} still referenced; clean-break broken" - ) - - -# ── decnet.target ──────────────────────────────────────────────────────────── - - -def test_target_wants_orchestrator(target_text): - assert "decnet-orchestrator.service" in target_text - - -def test_target_does_not_want_emailgen(target_text): - """Stage 5 of the realism migration deleted decnet-emailgen.service. - A fresh `decnet init` against a target file that still mentions it - fails systemctl start with `Unit decnet-emailgen.service could not - be found`, blocking the whole target. Anti-regression.""" - assert "decnet-emailgen.service" not in target_text - - -def test_target_wants_canary(target_text): - """Canary worker is a peer of orchestrator; both are part of the - realism + callback story. Bundle check.""" - assert "decnet-canary.service" in target_text - - -def test_target_orders_after_bus(target_text): - """Whole target depends on the bus being up.""" - assert "After=decnet-bus.service" in target_text - - -# ── unit file no longer exists ─────────────────────────────────────────────── - - -def test_emailgen_unit_template_is_gone(): - """The pre-collapse ``deploy/decnet-emailgen.service.j2`` must stay - deleted. A future commit that re-creates it (e.g. by accident - during a partial revert) would break the realism migration's - service-collapse contract.""" - assert not (DEPLOY / "decnet-emailgen.service.j2").exists(), ( - "decnet-emailgen.service.j2 reappeared — service collapse undone?" - )