Issue #951 Fresh Env-Pack Start Preflight¶
This note fixes the architecture boundary for the clean-install startup regression in issue #951. It is design guidance, not an implementation plan. No new ADR is needed: the accepted authorities are already the scenario-bundle root boundary, the env-pack admission boundary, the certificate producer boundary, and the lifecycle cleanup boundary.
Architecture Decisions¶
Resolve and admit the selected bundle once¶
Scenario selection has one precedence rule: an explicit catalog/path selection
is a project-tree bundle; otherwise ScenarioSourceConfig selects the configured
env-pack or project-tree source. config.scenario.source therefore does not, by
itself, describe the source of the scenario being started. Code that skips a
legacy producer because the config default says env-pack is wrong when the
operator supplied a catalog or explicit path.
The public start must reuse resolve_scenario_bundle(). A missing default
project-tree filename must not be substituted before that resolver runs, and a
missing file must not silently mean “no ownership.” In particular,
DEFAULT_RAES_SCENARIO is not the default env-pack's location and must not be
used by lab-start preflight when scenario_path is None.
Resolve the ScenarioBundle, parse it, create the RuntimeTarget, and produce
the RAES ExecutionPlan once. The same target, plan, and
AptlProvisioner.realize_plan() result must drive pre-mutation decisions and
the later RuntimeManager.apply(). Reuse the provisioner's existing cached
AptlRealization; do not create a second ownership DTO, reparse/replan in
_load_stateful_artifact_ownership(), or stage the same env-pack twice.
Request-scoped orchestration may carry these existing objects through
_LabStartContext; they are not a new persisted or public schema.
Validate the model that will actually be applied¶
The bind-source preflight has one narrow purpose: stop Docker from fabricating a missing host source as a root-owned directory. It is not an alternate authority for scenario membership, generated-artifact consumers, or mount destinations.
For a project-tree bundle, the static docker-compose.yml remains the model and
its bind check is a compatibility guard. It must be limited to the profiles
selected by the admitted realization, not the broader AptlConfig.containers
ceiling. This is what keeps a reduced catalog scenario from validating SOC
services it does not start.
For an env-pack bundle, the project directory's docker-compose.yml is not an
input to realization. The backend generates the base model and its stateful and
content overrides from DeploymentRealizationSpec. Lab orchestration must not
scan the unrelated static file. Bind-source existence belongs in the existing
fully merged, --no-interpolate effective-model validation after generated
artifacts and content have been materialized and before compose up.
effective_stateful_model_errors(), provider output checks, and content
placement/readback remain the semantic validators; do not reproduce them in
core.lab.
Mount targets are still checked against the admitted consumer contract in the
backend's effective-model validation. Do not compare a target from the static
Compose compatibility model with a target from the env-pack realization. If a
narrow compatibility exemption is retained during migration, its existence
claim may match only the exact admitted service plus canonical generated source
under DeploymentBackend.realization_root; destination matching remains the
effective model's job. No scenario name, service allowlist, TechVault path, or
certificate filename may select the exemption.
Generated output paths for an env-pack are rooted at the backend's writable
realization_root (project_dir for local Compose), not the pristine staged
ScenarioBundle.root. The pack root remains the authority for immutable input
bytes. Confusing these roots recreates the same defect with a different path.
Prove the distributed artifact and cleanup contract¶
The regression gate must exercise the product users install, not an editable
checkout. Build the wheel with the existing locked, no-isolation toolchain;
install locked runtime dependencies plus that wheel into a clean virtual
environment; do not install the project editable or copy ignored generated
state. Run the public aptl lab init, start
techvault-defensive-min, and run aptl lab stop -v --yes.
Teardown verification is project-scoped. Reuse the identities and query rules
from observe_project_runtime() and _compose_volume_cleanup: both Compose and
direct-materializer container labels, the Compose project network label, and
the validated <project_name>_ volume namespace. Assert zero containers,
networks, and volumes. Never use names such as aptl-* as the sole authority,
and never use a daemon-wide prune. Cleanup must run on failure as well as
success, while a failed start or failed cleanup still fails the job.
The 5.2.1 version and changelog are release-please outputs. The fix PR needs a
fix: Conventional Commit title; implementation must not hand-edit
pyproject.toml, src/aptl/__init__.py, CHANGELOG.md, or the release manifest
to publish the patch.
Canonical Incumbents To Reuse¶
| Concern | Canonical owner and required reuse |
|---|---|
| Scenario selection and source | ScenarioCatalog, resolve_scenario_selection(), ScenarioSourceConfig, resolve_scenario_bundle(), ScenarioBundle, and ScenarioSourceKind. Explicit selection precedence stays in the resolver. |
| Pack ingress | env_pack_bundle(), validate_pack(), validate_pack_content_manifest(), per-invocation contained staging, and aptl.utils.pathsafe. No local pack schema or fallback search. |
| Admission and interpretation | _plan_scenario(), RAES parser/compiler/planner diagnostics, RuntimeTarget, ExecutionPlan, AptlProvisioner.realize_plan(), AptlRealization, and DeploymentRealizationSpec. One admitted execution, not a query plan plus an apply plan. |
| Generated artifacts | raes_stateful_realization.py, artifact_source_path(), ComposeStatefulRealizationMixin, stateful_realization_errors(), provider-specific output validation, and DeploymentBackend.realization_root. |
| Compose model | base_compose_file(), _realization_compose_files(), _validate_realization_compose_model(), effective_stateful_model_errors(), and _build_command(). Validate the merged model with no interpolation; do not add another Compose parser. |
| Config and secrets | Strict AptlConfig models, hydrate_dotenv(), load_dotenv(), EnvVars, placeholder rejection, owner-only generated files, ADR-029 redaction, and explicit project_dir/.env Compose binding. |
| Results and observability | RAES Diagnostic, render_raes_diagnostics(), LabResult, StartupOutcome, StartupDiagnostic, get_logger(), and redact(). No new exception hierarchy or API envelope. |
| Distribution | _asset_manifest.py, hatch_build.py, assets.materialize(), .gitignore, wheel-content tests, locked requirements, and the existing release workflow. Generated certs, keys, .env, and .aptl/ remain absent from artifacts. |
| Lifecycle cleanup | lifecycle_mutation_lock(), stop_lab(), stop_compose_lab(), observe_project_runtime(), project-label cleanup, and project_scoped_volume_names(). |
Security And Cross-Cutting Passage¶
| Layer | Required behavior |
|---|---|
| Auth surface | This issue adds no endpoint or authorization decision. Existing API token, Host, CSRF, session, and CLI boundaries remain unchanged; the CI job uses only the local CLI. |
| Config shape | aptl.json continues through Pydantic models with extra="forbid"; scenario identity/source and validated deployment.project_name remain the only durable selectors. Add no bind-preflight toggle, service list, or path map. |
| Catalog/path gate | Catalog entries and explicit paths pass the existing id schema, no-follow project containment, and RAES parse validation. An explicit path is never reclassified from the config default. |
| Pack shape and bytes | Env-pack acquisition passes both public env-packs validators, exact inventory/digest checks, no-symlink/hardlink/special-file rules, and contained immutable staging. A missing/rejected pack fails closed as EnvPackError; it never falls back to a deleted project path. |
| RAES shape and policy | Generated outputs, consumers, service bindings, access modes, sensitivity, dependencies, and destinations pass RAES validation and APTL's existing typed lowering/stateful graph checks before mutation. |
| Filesystem and secret handling | Immutable pack input uses ScenarioBundle.root; generated output uses realization_root; operator secrets use project_dir/.env. Preserve canonical contained paths, no-follow checks, atomic writes, restrictive modes, declared-output completeness, and package exclusions. No PEM, private-key, token, or env bytes enter mount metadata or evidence. |
| Compose/effective model | Use the existing argv-list runner, explicit project directory/env file, bounded timeouts, --no-interpolate, and normalized effective model. Do not serialize, log, or persist the rendered model because it can carry sensitive configuration. |
| OS/process exposure | Only non-secret paths and fixed Docker/Compose options may appear in argv. Do not add shell=True, command strings, credential-bearing URLs, env dumps, or raw Docker output. Local/SSH visibility continues through supports_local_artifacts and the existing remote-artifact refusal/probe boundaries. |
| Error envelope | Pack/parser/planner errors become bounded, redacted diagnostics; producer/backend failures stay unsuccessful LabResult/RAES apply results. Do not expose raw YAML parser context, absolute staging roots, full commands, Compose stderr, tracebacks, or environment mappings. |
| Logging/persistence | Log bounded phase, source kind, stable diagnostic code, service/address, and counts through get_logger()/redact(). ScenarioBundle.details() continues to omit roots; run records retain identities/digests and outcomes, never generated bytes or operational paths. |
| CI authority | Keep workflow permissions read-only, pin actions by SHA, use hashed dependency exports, install the wheel with --no-deps after the locked runtime set, and scope every Docker assertion to the validated Compose project identity. |
Extensibility Seam¶
The seam is the already-selected, request-scoped ScenarioBundle plus the one
admitted ExecutionPlan and cached AptlRealization. The next pack identity,
catalog scenario, or immutable acquisition mechanism changes the resolver input;
it does not add branches to lab steps or teach the static Compose checker about
new services.
The bind-validation parameter is the actual model source (project-tree static
model or generated effective model) and its backend realization_root. A future
remote-materialization implementation can change how that backend proves source
existence without changing RAES, the catalog, or lab orchestration. The CI
scenario id and project name are explicit inputs; resource assertions derive
from the project identity rather than copied container/network lists.
Whole-Repository Surface In Scope¶
- Selection/admission:
src/aptl/core/{config,scenario_catalog,scenario_bundle,lab}.pyandsrc/aptl/backends/{raes,_raes_scenario_resolution,_raes_scenario_queries,raes_provisioner,raes_realization}.py. - Realization:
src/aptl/core/deployment/{backend,docker_compose,realization,_compose_realization,_compose_model_realization,_compose_stateful_*,_compose_content_mounts}.py. - Secrets/errors:
src/aptl/core/{env,credentials,soc_ca,lab_types}.py,src/aptl/utils/{pathsafe,redaction,logging}.py, and CLI/API result projections. - Distribution/workflow:
src/aptl/_asset_manifest.py,hatch_build.py,pyproject.toml, locked requirements,.gitignore,.github/workflows/checks.yml,.github/workflows/release-please.yml, anddocs/releasing.md. - Cleanup: lifecycle lock/orchestration plus
_compose_stop,_compose_runtime_inventory,_compose_project_cleanup, and_compose_volume_cleanup. - Verification: lab, scenario-bundle/catalog, env-pack, stateful/effective-model, asset/wheel, lifecycle/cleanup, and current-catalog scenario tests. The public catalog is data; coverage must not copy a fixed count of four entries now that the repository contains another catalog scenario.
Gotchas And Anti-Patterns¶
- Do not infer actual source kind from
config.scenario.sourceafter an explicit scenario path has won selection. - Do not replace
NonewithDEFAULT_RAES_SCENARIObefore env-pack resolution, and do not silently return when that deleted file is absent. - Do not resolve/stage the bundle once for display, again for ownership, and a third time for apply. Distinct staging paths are not proof of one admitted execution.
- Do not compare static-Compose destinations with generated-model destinations or weaken destination validation globally to make two unrelated models agree.
- Do not compute generated source paths from the pristine pack root when the backend writes them under its realization root.
- Do not key behavior on
techvault,misp,thehive,shuffle,cortex, a certificate filename, or/opt/aptl/soc-certs. - Do not add another tuple/DTO mirroring generated artifacts, another YAML or Compose schema, another validation pass, or another exception hierarchy.
- Do not treat a developer's ignored generated state as a fixture. Start tests
from
assets.materialize()/the public init path and assert the relevant generated roots are initially absent. - Do not let the wheel CI use
pip install -e ., the source checkout's CLI, or a separately installed local integration plugin; those bypass the released artifact contract under test. - Do not make teardown best-effort in CI, assert only running containers, or use broad name prefixes/global prune. Include stopped containers, networks, and volumes, and fail when absence cannot be proved.
- Do not hand-edit release versions or changelog entries for
5.2.1.
Non-Goals And Boundaries¶
This issue does not redesign RAES or env-packs schemas, change scenario content, add a pack-specific provider, migrate all legacy project-tree producers, add a deployment backend, weaken TLS/credential policy, alter participant behavior or readiness, or make remote Docker consume controller-local generated files.
It does not make docker compose up a supported replacement for aptl lab
start, ship generated credentials/certificates in the wheel, rename services,
or revise the intended topology. The implementation boundary is correct
scenario selection, one admitted realization, validation of the model actually
applied, clean wheel-installed startup, project-scoped teardown proof, and the
normal automated patch-release workflow.