DEP-003 Ephemeral Lifecycle Policy Preflight¶
This note is the architecture preflight for DEP-003 / issue #467. It is
guidance, not an implementation plan. It extends
RNG-001 Ephemeral Environments:
RNG-001 owns the destructive clean-boot seam; DEP-003 adds lifecycle policy
decisions around when provisioning and teardown happen.
Existing ADRs remain binding: ADR-013 and ADR-023 own deployment backends, ADR-025 owns first-party config shape, ADR-028 owns runtime-rendered config, ADR-029 owns secret handling, ADR-030 owns lab-result envelopes, ADR-031 owns orchestration contract guards, ADR-037 owns Docker Compose backend cohesion, ADR-039 owns web API auth, and ADR-044 owns RAES/run reproducibility records.
Architecture Decisions¶
- Treat lifecycle policy as a control-plane decision layer above lab lifecycle. TTL expiry, idle detection, and schedules decide when to invoke existing lifecycle operations; they do not become new Docker, Compose, RAES, or web lifecycle implementations.
- Provisioning must reuse the public lab start path:
orchestrate_lab_start()for normal starts andclean_boot_lab()when the policy requires clean state. Teardown must reusestop_lab()throughDeploymentBackend; complete teardown meansremove_volumes=Trueunless a typed policy explicitly says otherwise. - A "range instance" is the configured deployment project, not an individual
container, scenario file, folder name,
lab.name, GitHub issue, or run archive. The current concrete identity isDeploymentConfig.project_nameplus the project directory/backend. Any future concurrent instances must parameterize that identity and state root explicitly instead of relying on Docker name prefixes. - Lifecycle policy state is data:
started_at,expires_at,last_activity_at,next_provision_at, policy name, action, result, and narrow error labels. It must not carry.envvalues, bearer tokens, raw Docker stderr, terminal content, generated config, or command lines. - Automated policy execution must have one owner per project/backend. Do not hide timers in FastAPI request handlers, SSE loops, or web clients where process restarts, multiple workers, or disconnected browsers can double-run or miss destructive actions.
- Lifecycle actions must be serialized per project/backend. A scheduled start,
TTL teardown, idle teardown, manual
aptl lab start --clean, and API start/stop must not overlap against the same Compose project.
Cross-Cutting Concerns To Reuse¶
- Lab lifecycle:
clean_boot_lab(),orchestrate_lab_start(),stop_lab(remove_volumes=True),_LabStartContext,_LAB_START_STEPS,LabResult,StartupOutcome, andStartupDiagnostic. - Deployment boundary:
DeploymentBackend,DockerComposeBackend,SSHComposeBackend,DeploymentConfig.project_name, compose-project label filters, SSH transport semantics, backend timeouts,BackendTimeoutError, andBackendSeedError. - Config and env:
AptlConfig,DeploymentConfig,ContainerSettings.enabled_profiles(),RunStorageConfig,load_config(),load_dotenv(),EnvVars,env_vars_from_dict(), andfind_placeholder_env_values(). - Scenario selection and RAES handoff:
resolve_scenario_selection(),start_raes_scenario(),selected_profiles_for_scenario(), RAES parser / runtime manager gates, and the REP-001 run-record threading already in lab start. - API and web surfaces:
verify_token,WebAuthSettings,LabActionResponse,StartupDiagnosticModel,StartupOutcomeLiteral,web/src/lib/types.ts, and existing API projection style. - Observability and persistence:
get_logger(),redact(),RangeSnapshot.to_dict(),LocalRunStore.write_json(),write_jsonl(),append_jsonl(), and the run archive/reference pattern from ADR-044. - Live proof point:
src/aptl/validation/_live_gate_probes.pyalready consumesclean_boot_lab()instead of open-coding destructive stop/start.
Security And Validation Layers¶
- Auth surface: any API trigger for provisioning, teardown, policy updates, or policy status must remain behind ADR-039 bearer-token auth. Do not pass API tokens, destructive intent, or policy payloads in query strings, EventSource URLs, WebSocket subprotocol data beyond the existing terminal token shape, logs, or redirects.
- Policy input shape: durable lifecycle knobs belong in strict Pydantic
config models under
AptlConfigonly when they have a runtime consumer. Transient CLI/API options may use boundary DTOs, but they must be typed, range-checked, andextra="forbid"in the same style. Avoid unchecked dicts, free-form shell commands, stringly typed cleanup modes, and duplicated profile/scenario schemas. - Time and schedule parsing: use timezone-aware UTC instants for persisted timestamps and simple bounded integer seconds for TTL/idle intervals. If a cron-like schedule is required later, add one validated parser boundary; do not interpret arbitrary schedule strings in multiple call sites.
- Environment binding: lifecycle policy must not parse
.envor service config itself. Lab start continues throughload_dotenv(),env_vars_from_dict(), placeholder checks, generated config renderers, SOC cert generation, Suricata seeding, and bind-mount validation. - Deployment boundary: policy actions call lifecycle functions that call
DeploymentBackend. Do not calldocker compose,docker ps,docker volume rm,ssh, or backend-private_run()methods directly from policy, API, web, validation, or tests. - Idle detection: track narrow activity metadata only. Acceptable signals are control-plane lifecycle actions, terminal/session activity timestamps, scenario/orchestrator actions, runstore appends, or explicit heartbeat-style markers routed through one activity boundary. Do not record terminal bytes, command text, SOC payloads, packet contents, raw Docker logs, CPU load, or network counters as the canonical idle signal.
- OS/process exposure: schedulers and API workers must not put bearer
tokens, passwords, API keys, private keys, raw
.env, rendered config, or credential-bearing curl commands in argv or shell strings. Preserve argv-list subprocess construction and existingcurl_safehandling for SOC HTTP. - Error envelopes: lifecycle policy failures use existing
LabResult/LabActionResponseshapes. A failed TTL/idle teardown is not a new exception hierarchy or readiness category. Redact backend stderr and unexpected exception text before it reaches CLI, API, logs, telemetry, web, run archives, or policy state. - Persistence boundary: policy events and action receipts that are part of
a run must use
LocalRunStoreredacting JSON/JSONL writes. Scheduler checkpoint state, if needed, belongs under ignored.aptl/state with the same ID/path validation and redaction discipline, not in checked-in config or as mutable run-archive truth.
Extensibility Seam¶
The seam belongs at a small lifecycle-policy boundary that evaluates typed policy data and emits one of the existing lifecycle actions: start, clean boot, or teardown. The first policy variants should be parameterized by:
- instance identity: project directory, deployment backend, and
deployment.project_name; - lifecycle action: normal start, clean boot, or teardown;
- cleanup policy: remove Compose-managed volumes or preserve them;
- selected scenario: catalog id or explicit scenario path resolved by the existing scenario-selection helper;
- timing policy: TTL seconds, idle-timeout seconds, and scheduled UTC windows or intervals;
- activity source: a single last-activity marker that future terminal, orchestrator, API, or MCP activity producers can update without re-editing every policy evaluator.
Future cloud, Kubernetes, classroom pool, quota, or queue semantics should
extend this policy boundary and DeploymentBackend implementations. They
should not duplicate lab-start ordering, Docker Compose command construction,
config parsing, API schemas, or run-record assembly.
Whole-Repo Surface¶
aptl.json,.env,.env.example,AptlConfig,DeploymentConfig.project_name, andRunStorageConfig.docker-compose.yml, Compose profiles, top-level volumes, networks, generated bind mounts, and compose-project labels..aptl/state,.aptl/config/...,.aptl/runs/...,config/soc_certs/...,config/suricata/...,keys/,.mcp.json, and run archives.src/aptl/core/lab.py,src/aptl/core/lab_types.py,src/aptl/core/deployment/,src/aptl/core/config.py,src/aptl/core/env.py,src/aptl/core/runstore.py,src/aptl/core/snapshot.py, andsrc/aptl/backends/raes*.py.src/aptl/cli/lab.py,src/aptl/api/routers/lab.py,src/aptl/api/schemas.py,src/aptl/api/deps.py,web/src/lib/api.ts, andweb/src/lib/types.ts.- Host/runtime layers: Docker daemon, SSH Docker transport, local process argv, local filesystem permissions, API worker lifetime, scheduler lifetime, and browser/web client disconnects.
- Repo gates:
.gc/plan-rules.md,pytest, andpre-commit run --all-files.
Gotchas And Anti-Patterns¶
- Adding a second
EphemeralInstance,RangeController, Docker script, API schema, exception hierarchy, or validation stack when existing lifecycle results and deployment backends already own the behavior. - Treating TTL/idle/schedule policy as deployment-backend behavior. Backends perform typed lifecycle operations; policy decides when to call them.
- Treating clean state as a container restart while preserving service volumes, SOC databases, generated rule volumes, stale API-key sync state, or in-container credentials.
- Inferring instance identity from container names,
aptl-*prefixes, scenario names, folder names,lab.name, GitHub issue ids, or RAES metadata. - Deleting
.env,.mcp.json,keys/, checked-inconfig/, run archives, or RAES inventory evidence as part of the default teardown guarantee. - Running timers in web clients, SSE loops, or request-local
asynciotasks and calling that reliable automated teardown. - Scraping logs, terminal transcripts, Docker stats, packet captures, or SOC payloads to decide idle state.
- Returning raw Docker stderr, raw
icontractmessages, rendered config,.envvalues, terminal content, or command lines in policy errors. - Weakening SSH-remote safety by assuming locally generated artifacts are visible to a remote Docker daemon.
Non-Goals¶
- Do not implement DEP-003 in this preflight.
- Do not redesign Docker Compose, RAES SDL, deployment backends, run archive layout, SOC seeding, Suricata rule semantics, terminal capture, web auth, or startup readiness classification.
- Do not add Kubernetes, Podman, Nomad, cloud account provisioning, quota management, classroom pool scheduling, or multi-tenant billing semantics as part of the first lifecycle policy boundary.
- Do not guarantee byte-identical regenerated credentials, logs, timestamps, Docker object IDs, network IDs, endpoint IDs, or image cache contents across provisioned instances.
- Do not make destructive TTL/idle teardown part of default fast CI or pre-commit.