ADR-045: Ephemeral Lifecycle Policy Enforcement¶
Status¶
accepted
Date¶
2026-06-28
Context¶
DEP-003 asks the platform to support "automated provisioning and teardown of complete range instances on demand, with defined lifecycle policies (TTL-based auto-teardown, idle detection, scheduled provisioning)."
The on-demand half already exists. RNG-001 owns the destructive clean-boot seam
through orchestrate_lab_start(), stop_lab(), and clean_boot_lab() in
src/aptl/core/lab.py. What DEP-003 adds is a decision layer that chooses when
to call those operations: tear a range down once it has lived past its TTL or
gone idle, and provision a range on a schedule.
The DEP-003 preflight note sets the binding guardrails: policy is a control-plane layer above lab operations, a "range instance" is the configured deployment project, lifecycle state is narrow data, and automated execution needs one owner per project so concurrent or restarted processes never double-run a destructive action.
The open question the preflight leaves to this decision is the process model that drives enforcement. The pure policy evaluation (compare timestamps against a TTL, an idle timeout, and a schedule) is identical regardless of how it runs; only the surrounding process differs:
- A long-running daemon that owns a poll loop and a lockfile as its primary artifact.
- A background task inside the API server lifespan, which enforces only while that server runs.
- A single-shot tick that the operator schedules with an external timer.
Decision¶
APTL enforces lifecycle policy with a single-shot, idempotent tick:
aptl lab enforce performs exactly one evaluate-and-act cycle and exits. The
operator owns cadence through a systemd timer or a cron entry. aptl lab
monitor is a thin loop over the same tick for hosts without an external
scheduler.
The decision has four parts:
- No daemon and no API coupling. Option 1 makes a long-lived process and its lock the main artifact for what is a few timestamp comparisons. Option 2 stops enforcing the moment the API server stops, which silently defeats TTL teardown—the property operators most depend on for cost control. A single-shot tick keeps the always-on concern where the host already solves it (the timer) and keeps the codebase to pure evaluation plus a thin shell.
- The lab start path is unchanged. Enforcement imports
lab_status(),stop_lab(), andclean_boot_lab(); it adds no step to_LAB_START_STEPS. Each tick reconciles provisioning time against the observed running state, so a range started by hand and a range started by a schedule age the same way. The cost is that TTL counts from the first tick that observed the range running rather than the exact start instant—bounded by one tick interval and acceptable for teardown decisions measured in minutes to hours. - Idle uses capture recency. The idle signal is the most recent evidence
written under the active run directory (
resolve_active_run_dir), falling back to provisioning time when no scenario is active. Capture recency is a narrow control-plane activity marker that needs no new heartbeat wiring across the MCP servers. - One owner, narrow state. A
flockon.aptl/lifecycle/.lockserializes ticks, so a manualenforceand a runningmonitorcannot act at once. State lives in.aptl/lifecycle/state.jsonat mode0600and holds only timestamps, the last action and result, a redacted error label, and the per-day fired-schedule markers. Every value passes throughredact()at the persist boundary (ADR-029).
Policy is authored in aptl.json as a strict Pydantic model
(LabLifecyclePolicyConfig, ADR-025): bounded positive ttl_minutes and
idle_timeout_minutes, a teardown_remove_volumes cleanup flag, and a
schedule list of HH:MM UTC times with an optional weekday filter and
scenario id. aptl lab policy show renders the resolved policy and current
state.
Consequences¶
Positive
- The always-on concern is one line of operator configuration, not a process to supervise. The tick is trivially testable: the pure evaluators take timestamps and return decisions with no clock, Docker, or filesystem.
- TTL teardown keeps working whether or not the API server or web UI runs.
- Reusing the RNG-001 lifecycle functions and the ADR-030 result envelope means no second teardown path and no new error hierarchy.
Negative / risks
- Enforcement only happens when the operator's timer fires. A host with no timer
and no
monitorrunning enforces nothing—the feature is opt-in by design, but operators must wire the cadence. - TTL and idle resolve at tick granularity, so a short interval is needed for tight timeouts.
Out of scope
- REST policy endpoints, multiple concurrent named ranges, and cron-grammar schedules (daily plus weekday only). A future concurrent-instance design must parameterize the range identity and state root, as the preflight notes.