Skip to content

Fresh aptl lab init + aptl lab start fails at the bind-mount preflight for every scenario (5.2.0 and dev) #951

Closed

Description

@Brad-Edwards

Finding

The documented quick start fails on a clean directory in about nine seconds, for the default scenario and for all four curated catalog scenarios, from the released 5.2.0 wheel on PyPI and from dev source (verified 2026-09-04):

pipx install aptl-labs && aptl lab init my-lab && cd my-lab && aptl lab start
[lab start] Checking bind-mount sources.
Bind-mount issue: Service 'misp': bind-mount source './config/soc_certs/misp/server.pem' does not exist. ...
Lab start failed.
  error: Bind-mount pre-flight failed:  (8 errors: misp, thehive, shuffle-frontend, cortex)

Every developer checkout that has ever booted a lab carries the gitignored config/soc_certs/ tree, so the failure is invisible there. CI runs aptl lab init but never aptl lab start. The released 5.2.0 wheel ships no scenarios/techvault-operational.sdl.yaml and carries the same code paths.

Root cause

Two defects, both introduced by the env-pack cutover (#908 / #875):

  1. _load_stateful_artifact_ownership in src/aptl/core/lab.py substitutes DEFAULT_RAES_SCENARIO (scenarios/techvault-operational.sdl.yaml, deleted in feat(scenario): declare TechVault content in the SDL via the env-pack (#875) #908) when no scenario is selected and returns silently when that file is missing, so ctx.stateful_artifact_ownership stays empty. _step_generate_soc_certs then skips host generation because scenario.source == "env-pack", and _step_check_bind_mounts fails. tests/test_lab.py::test_missing_scenario_leaves_legacy_ownership codifies the silent return. Calling admitted_stateful_artifact_ownership(..., scenario_path=None) in the same directory resolves 15 ownership entries, including the four SOC certificate consumers.
  2. Even with ownership loaded the preflight still fails. It validates docker-compose.yml bind destinations (/etc/nginx/certs/cert.pem, /etc/thehive/keystore.p12, /etc/lab-ca/lab-ca.pem) while the pack realizes those certificates at /opt/aptl/soc-certs/..., and _stateful_realization_owns_mount requires both source and destination to match (Remove silent sudo + root-owned cert bind mounts (generate certs user-owned) #677). The compose file no longer starts those services; the preflight checks a file the realization does not use. Verified by running _check_bind_mounts with the pack-declared ownership applied: still 8 errors.

Required work

  • Derive the bind-mount preflight from the admitted realization (the same plan the backend applies), or exempt by admitted generated source path while keeping the service match. No TechVault, service-name, or path special case.
  • Stop substituting a deleted default path. When scenario.source == "env-pack" and no scenario is selected, use the same pack resolution the start path uses.
  • Replace the test that codifies the silent return with one that runs the step sequence from a fresh aptl lab init directory with the env-pack default and no pre-existing generated state.
  • Add a CI job that builds the wheel, installs it into a clean venv, runs aptl lab init, boots techvault-observability-core, runs aptl lab stop -v --yes, and asserts zero project containers, networks, and volumes remain. Three containers; it fits ubuntu-latest.
  • Publish 5.2.1 once green.

Acceptance criteria

  • pipx install aptl-labs && aptl lab init d && cd d && aptl lab start passes the preflight for the default scenario and every catalog scenario from a clean directory.
  • No lab-start step validates docker-compose.yml mounts for services the admitted plan realizes from the pack, or the check is derived from the admitted plan.
  • CI boots and tears down the smallest scenario from the built wheel on every PR.
  • 5.2.1 is published with the fix.

Relationships

Activity

  1. added
    bugSomething isn't working
    scope:aptl-coreAPTL platform/range capability independent of one scenario.
    user-facingDirectly changes a user journey or user-visible outcome
    on Sep 4, 2026
  2. added
    in-progressAn agent is actively working this issue via /implement
    on Sep 4, 2026
  3. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    🛠️ Picked up by /implement - driver claude, branch 951-bind-mount-preflight, 2026-09-04T04:48:13.249Z.

  4. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    gc workflow phase recorded: preflight (issue #951). Posted by the MCP server to enforce ordering between workflow steps (issue #794 MVP-2). Do not edit or delete — used by downstream tools to gate phase prerequisites.

  5. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    gc workflow phase recorded: plan (issue #951). Posted by the MCP server to enforce ordering between workflow steps (issue #794 MVP-2). Do not edit or delete — used by downstream tools to gate phase prerequisites.

    Plan — fix the fresh-start bind-mount preflight (#951)

    Verified root cause

    Reproduced locally from dev source in a fresh aptl lab init directory (default
    scenario and --scenario techvault-observability-core and
    --scenario bounded-participant-agency-techvault): all three fail the preflight with the
    same 8 errors.

    Three defects, one shared cause: lab-start steps guess scenario facts from the config
    ceiling or a hardcoded path instead of reading the admitted realization.

    1. _load_stateful_artifact_ownership (src/aptl/core/lab.py:1163) substitutes
      DEFAULT_RAES_SCENARIO (scenarios/techvault-operational.sdl.yaml, deleted in feat(scenario): declare TechVault content in the SDL via the env-pack (#875) #908)
      when scenario_path is None, then returns silently because that file is missing. So on
      the default env-pack path nothing is ever admitted and
      ctx.stateful_artifact_ownership stays empty. Calling
      admitted_stateful_artifact_ownership(..., scenario_path=None) in the same directory
      resolves 15 entries.
    2. _step_check_bind_mounts validates project_dir/docker-compose.yml. Measured: for an
      env-pack bundle the pack ships no docker-compose.yml, so base_compose_file()
      generates the base model from the realization (24 services, zero relative binds);
      the stateful/content overrides bind absolute paths under
      DeploymentBackend.realization_root. The in-tree docker-compose.yml is not an input
      to the run at all
      . The preflight is parsing an unrelated file.
    3. _scenario_is_env_pack (lab.py:1203) reads config.scenario.source, so an explicit
      --scenario <catalog id> run — which resolve_scenario_bundle() resolves as
      project-tree — still skips host-side SOC cert generation. That is why the catalog
      scenarios fail too: certs are skipped and the static compose is checked.

    A fourth, compounding fact: the preflight filters services by
    config.containers.enabled_profiles() (the operator ceiling, soc: true by default)
    rather than the profiles the admitted plan selects. techvault-observability-core selects
    only observability, yet the preflight validates MISP/TheHive/Shuffle/Cortex.

    Design

    Binding guardrails are the Step 2.5 note
    docs/architecture/issue-951-fresh-env-pack-start-preflight.md. The forecast for this work
    already exists in docs/architecture/issue-550-soc-selected-profile-preflight.md:

    If a later issue scopes pre-start work such as certificate generation, image pulling,
    bind-mount checks, or host-port planning, resolve the selected surface once in an
    explicit pre-start boundary after config/backend initialization and reuse it.

    and raes_realization._load_profile_index already states the rule this fix restores:
    "the Compose model is a scenario-declared input, so it is read from the bundle, never the
    engine checkout."

    Admit once, then read the admitted facts. _step_load_config resolves the bundle,
    parses, plans, and interprets exactly once; _step_start_containers applies that same
    target/plan instead of re-planning. Today the bundle is staged and planned twice per lab
    start (I observed 5 staged pack copies accumulate under .aptl/staged-packs/, one per
    plan) at a measured 92 s each.

    • src/aptl/backends/raes.py — expose an admit_raes_scenario(...) seam returning the
      existing (bundle, target, execution_plan, realization) produced by _plan_scenario;
      start_raes_scenario(..., admitted=...) skips re-planning and goes straight to
      _apply_with_backend_retry. No new DTO for artifacts, no second parser, no new exception
      type — the guardrails forbid all three.
    • src/aptl/backends/_raes_scenario_queries.py — derive selected_profiles_for_scenario
      and admitted_stateful_artifact_ownership from one shared projection over an
      already-admitted realization, so query and apply cannot disagree.
    • src/aptl/core/lab.py
      • _load_stateful_artifact_ownership → _load_admitted_start_surface: passes
        ctx.scenario_path through unchanged (no DEFAULT_RAES_SCENARIO, no silent
        return), caches bundle root, ScenarioSourceKind, admitted selected profiles and
        artifact ownership on _LabStartContext, and fails closed on
        EnvPackError/OSError/ValueError/TypeError with a redacted LabResult.
      • _scenario_is_env_pack reads the resolved bundle's source_kind, not the config flag.
      • _step_check_bind_mounts passes the admitted bundle root and the admitted
        selected profiles
        to _check_bind_mounts. For an env-pack bundle the pack has no
        docker-compose.yml, so the existing compose_path.exists() guard makes it a logged
        no-op; source existence stays where it already belongs — the backend's post-realization
        effective_stateful_model_errors() / provider output checks, before compose up.

    _stateful_realization_owns_mount is deliberately left alone. Once the check reads the
    model that actually governs the run, the destination mismatch never arises, so #677's
    both-sides match stays intact. Weakening it is explicitly named as an anti-pattern.

    Security. No new endpoint, authz decision, or config key. Pack ingress keeps
    validate_pack()/validate_pack_content_manifest(); a missing or rejected pack fails
    closed as EnvPackError rather than degrading to a deleted path. Immutable input stays
    under ScenarioBundle.root, generated output under realization_root, operator secrets in
    project_dir/.env. Planning errors surface through redact()/LabResult — no staging
    roots, YAML context, or tracebacks. Failing open (skipping the guard on the env-pack
    path) is safe only because realization fails closed before compose up; the guard moves,
    it does not disappear.

    Maintainability. Reuses resolve_scenario_bundle, ScenarioSourceKind,
    select_backend_profiles, _plan_scenario, AptlProvisioner.realize_plan,
    base_compose_file, observe_project_runtime, project_scoped_volume_names. No scenario
    name, service allowlist, TechVault path, or certificate filename anywhere in the diff.

    Extensibility. The seam is the request-scoped ScenarioBundle plus one admitted
    ExecutionPlan. A new pack identity or acquisition mechanism changes the resolver input
    only; lab steps gain no branches.

    Whole-repo view. Touches lab orchestration, the RAES query/apply seam, CI, and tests.
    make policy (Vale) and mkdocs nav cover the new architecture note. Release Please owns
    the version — CHANGELOG.md/pyproject.toml are not edited.

    Work items and TDD paths

    # Item Path
    1 Admit-once seam in raes.py + shared projection in _raes_scenario_queries.py B
    2 _load_admitted_start_surface (no deleted-default substitution, fail closed) B
    3 _scenario_is_env_pack from the resolved bundle source kind B
    4 _step_check_bind_mounts uses admitted bundle root + admitted profiles B
    5 Replace test_missing_scenario_leaves_legacy_ownership; add fresh-init step-sequence test B
    6 Wheel → clean venv → lab init → boot → lab stop -v --yes → zero-residue CI job A
    7 Traceability: link the new gate to DEP-008 D

    Per the repo plan rules every Python change lands with pytest tests in tests/. No MCP,
    web, docker-compose.yml, or config/ change, so those rules do not bind.

    Tests

    Fast unit tests (default suite) pin the contract without paying the 92 s admission cost:

    • _load_admitted_start_surface calls the resolver with scenario_path unchanged and
      never references DEFAULT_RAES_SCENARIO; a resolver failure returns a failed, redacted
      LabResult instead of empty ownership.
    • _step_check_bind_mounts reads the compose model from the admitted bundle root
      (a bundle root without a compose file makes it a no-op even when project_dir has one),
      and filters by admitted selected profiles, not containers.enabled_profiles().
    • _scenario_is_env_pack is False for an explicit project-tree selection even when
      config.scenario.source == "env-pack".
    • Existing TestCheckBindMounts cases stay green — including the Remove silent sudo + root-owned cert bind mounts (generate certs user-owned) #677 both-sides cases.

    Integration (@pytest.mark.integration, named ..._integration so the repo's
    -k "not integration" fast run excludes it): from a fresh aptl lab init directory with
    the env-pack default and no pre-existing generated state, run the real step sequence
    through _step_check_bind_mounts and assert it passes. It asserts config/soc_certs/ and
    .aptl/ are absent at the start, per the guardrail against treating a developer's ignored
    generated state as a fixture.

    CI gate (DEP-008)

    New clean-install-lab-boot job in .github/workflows/checks.yml, ubuntu-latest,
    read-only permissions, SHA-pinned actions:

    1. Build the wheel with the existing locked, no-isolation toolchain.
    2. Fresh venv; install the locked runtime set, then the wheel with --no-deps. No
      pip install -e ., no source-checkout CLI, no local integration plugin.
    3. aptl lab init ci-lab; aptl lab start --scenario techvault-observability-core
      (three containers).
    4. aptl lab stop -v --yes in an always() step, then assert zero project containers
      (including stopped), networks, and volumes, scoped to the validated Compose project
      identity using the same label/namespace rules as observe_project_runtime() and
      _compose_volume_cleanup — never an aptl-* name prefix, never a daemon-wide prune.
      A failed start or a failed cleanup fails the job.

    This is the executable gate for DEP-008's own final clause — "pipx install aptl-labs
    followed by aptl lab init and aptl lab start shall produce a working lab without a git
    clone"
    — which #951 shows is currently false. Step 16 adds the job and the integration
    test to DEP-008's ## Traceability. DEP-008 is already ACTIVE, so no status transition.

    Local validation

    CI cannot be the only proof. I will boot and tear down a real lab on this machine from a
    fresh aptl lab init directory for the default env-pack scenario and for
    techvault-observability-core, then re-run the two catalog scenarios that reproduce today.

    Docs

    docs/architecture/issue-951-fresh-env-pack-start-preflight.md plus its index.md/
    mkdocs.yml entries (added by Step 2.5) ship with the diff. No ADR change: the note records
    that ADR-046/#874/#875/#677 already settle the boundary.

    Out of scope, flagged for your call

    With ownership now populated on the env-pack path, _step_generate_certs and
    _step_sync_credentials still run their legacy host-side generators, because the
    _WAZUH_*_OWNERSHIP constants use the static compose's dotted service keys
    (wazuh.manager) while an env-pack realization names services from its generated base
    (wazuh-manager). Behaviour is unchanged by this fix (ownership is empty today, so
    those generators already run), and the lab boots correctly — it is redundant work, not a
    failure. Retiring it is the "migrate all legacy project-tree producers" non-goal the
    preflight note excludes. Say the word and I will fix it here instead.

  6. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    gc_codex_review — cycle 1 of 1 (pre-push) on issue #951 (branch 951-bind-mount-preflight)
    Diff mode: inline — the complete diff was supplied in one prompt

    Core review

    Verdict: ship

    This is shaped correctly. Architecture-fit: it establishes a single admitted-execution value at the existing RAES boundary and derives bundle location, resolved source kind, profiles, and artifact ownership from that value, keeping preflight and apply aligned while reusing the canonical resolution, diagnostic, profile-selection, and runtime seams. Code-quality: the tests pin resolution precedence, fail-closed admission, propagation without replanning, bind-mount scoping, and the narrow Docker DNS baseline; the wheel-based boot-and-teardown job validates the released artifact. The seam can accommodate additional pre-start projections without introducing another planning pass or foreclosing scenario parameterization.

    No blocking findings.

    Security review

    Verdict: ship-with-fixes

    The change is broadly shaped correctly: it creates a single scenario-admission seam and threads its resolved bundle, profiles, artifact ownership, and execution plan through preflight and apply, preventing divergent re-planning across the startup lifecycle. The cross-cutting boundary spans scenario resolution, bind-mount validation, runtime observation, and clean-install CI without foreclosing alternate scenario sources. The exception is the runtime-listener baseline, where an address-wide exemption weakens the security gate beyond the Docker-owned socket it intends to recognize.

    Blocking findings (1):

    1. [one-off] Address-wide Docker DNS exemption lets workloads hide undeclared listeners — src/aptl/backends/_runtime_concern_excess.py:209
      Attacker model: code executing in a monitored container that wants to conceal an unauthorized backdoor or control listener from the SEM-218 runtime gate. Linux permits a workload to bind an unused TCP or UDP port on 127.0.0.11; Docker owns particular daemon-selected resolver sockets, not every socket on that address. Because this predicate discards every observed listener whose address equals 127.0.0.11 before comparing it with declarations, an attacker can run the declared service normally and add an arbitrary listener on that address without triggering excess detection. Subtract only sockets positively identified as Docker's embedded resolver—for example, the exact TCP/UDP sockets corroborated by the namespace's DNS redirect/runtime metadata—and continue evaluating any additional sockets on the address.
  7. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    gc_codex_review pre-push cycle 1 of 1 complete for issue #951 on branch '951-bind-mount-preflight'. Posted by the MCP server to enforce the pre-push hard-cap-1 contract (issues #796, #804, #906). Do not edit or delete — used by the next gc_codex_review (uncommitted) invocation to count cycles.

  8. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    Review decision record — codex cycle 1 (issue #951)

    Reviewer: codex
    Cycle: 1

    Architectural read:

    Core reviewer: This is shaped correctly. Architecture-fit: it establishes a single admitted-execution value at the existing RAES boundary and derives bundle location, resolved source kind, profiles, and artifact ownership from that value, keeping preflight and apply aligned while reusing the canonical resolution, diagnostic, profile-selection, and runtime seams. Code-quality: the tests pin resolution precedence, fail-closed admission, propagation without replanning, bind-mount scoping, and the narrow Docker DNS baseline; the wheel-based boot-and-teardown job validates the released artifact. The seam can accommodate additional pre-start projections without introducing another planning pass or foreclosing scenario parameterization.

    Security reviewer: The change is broadly shaped correctly: it creates a single scenario-admission seam and threads its resolved bundle, profiles, artifact ownership, and execution plan through preflight and apply, preventing divergent re-planning across the startup lifecycle. The cross-cutting boundary spans scenario resolution, bind-mount validation, runtime observation, and clean-install CI without foreclosing alternate scenario sources. The exception is the runtime-listener baseline, where an address-wide exemption weakens the security gate beyond the Docker-owned socket it intends to recognize.

    Blocking findings: 1

    Finding 1 — one-off

    • ID: F1
    • Title: [security] Address-wide Docker DNS exemption lets workloads hide undeclared listeners
    • Location: src/aptl/backends/_runtime_concern_excess.py:209
    • Decision: fix
    • Rationale: Attacker model: code executing in a monitored container that wants to conceal an unauthorized backdoor or control listener from the SEM-218 runtime gate. Linux permits a workload to bind an unused TCP or UDP port on 127.0.0.11; Docker owns…
  9. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    gc_test_quality_review cycle 1 of 1 — issue #951

    Reviewer: test-quality (claude-sonnet-5 via gc_test_quality_review)
    Branch: 951-bind-mount-preflight
    Cycle: 1 / 1
    Findings: 0 (clean run)

  10. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    gc_test_quality_review cycle 1 of 1 complete for issue #951 on branch '951-bind-mount-preflight'. Posted by the MCP server to enforce the gc_test_quality_review hard-cap-1 contract (issue #884 follow-up, default lowered in #906). Do not edit or delete — used by the next gc_test_quality_review invocation to count cycles.

  11. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    Review decision record — test-quality cycle 1 (issue #951)

    Reviewer: test-quality
    Cycle: 1

    Architectural read:

    This is a well-executed regression-closure change for issue #951 (fresh-install lab-start preflight). The production shape is correct: admission is centralized into admit_start_surface/AdmittedStartSurface and threaded once through _LabStartContext so pre-mutation decisions (bind-mount checks, SOC-cert generation, env-pack detection) and the later apply step read off the same admitted execution rather than re-planning (and re-staging the env-pack) multiple times — exactly the seam the accompanying architecture note calls for. The test suite mirrors this discipline: test_lab.py stubs admit_start_surface at the same boundary the orchestrator uses (never planning a real scenario in unit tests), then asserts concrete outcomes (error message content, ctx.admitted_start/ctx.admitted_surface/ctx.stateful_artifact_ownership identity, bind-mount error strings) rather than only mock-call presence. The new test_lab_fresh_start.py is the one test in this set that legitimately needs Docker (it admits the real bundled env-pack and queries backend image availability); it is correctly @pytest.mark.integration-marked per-test (avoiding the known module-level-marker coverage gotcha) and wired into a dedicated blocking fresh-start-preflight CI job separate from the fast suite, with the full artifact-level regression additionally proven by clean-install-lab-boot. The runtime-concern excess-detection tests (test_raes_runtime_observation.py) drive the real realization_disclosure gate end-to-end for both the DNS-resolver baseline carve-out and its adversarial edges (duplicate socket defeats attribution, ordinary loopback still rejected, presence-matching still enforced on the resolver address), so the carve-out cannot silently widen into a blanket exemption. I would ship this.

    Blocking findings: 0 (clean run)

  12. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    Pre-PR base synchronization

    • Source: refs/remotes/origin/dev at b6928e52fdf6f88d02a4422b3592f4f003f1e3c6
    • Outcome: already_current
    • Published feature head: affaf570a6fd14023a875f2b9726d8953a2f6b1c
    • Verified tree: 496a86829d97927059ce034054e3b2e6ae63d3ea
  13. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    Pre-PR base synchronization

    • Source: refs/remotes/origin/dev at b6928e52fdf6f88d02a4422b3592f4f003f1e3c6
    • Outcome: already_current
    • Published feature head: 848423a55c59890244c0cfc49cdb7a486ec857f0
    • Verified tree: db4c2e29648f3a41d06fa31a4c0309f47462d481
  14. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    Pre-PR base synchronization

    • Source: refs/remotes/origin/dev at b6928e52fdf6f88d02a4422b3592f4f003f1e3c6
    • Outcome: already_current
    • Published feature head: 1b283d693b8aa2fa1d9126f0669578ba0c892ce8
    • Verified tree: eca26d1bf69deb9c0da03aca966e86a8e1381317
  15. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    Ready for review — issue #951

    PR: #960
    Plan: #951 (comment)

    Outcome

    Installing APTL from PyPI and following the documented quick start now works. Before this, aptl lab start failed after about nine seconds with eight bind-mount errors, for the default scenario and every catalog scenario; it stayed hidden because developer machines carry leftover generated files and CI had never booted a lab. Lab start now reads what the scenario it is about to run actually declares. CI now builds the wheel, installs it clean, boots a real lab, tears it down, and proves nothing is left behind.

    Lab start admits the selected scenario exactly once and derives the bundle root, resolved source kind, selected profiles, and generated-artifact ownership from that single admission, which apply also consumes. That replaces three separate guesses: a deleted default scenario path, the operator's container ceiling, and the configured scenario source. The bind-mount pre-flight now validates the Compose model the run actually applies. Also fixes the SEM-218 excess-listener check, which counted Docker's embedded DNS resolver sockets as undeclared exposure and failed every node declaring a service listener.

    In-scope requirements

    Proposed state in this PR — authoritative only after merge.

    • DEP-008 (Self-Contained Distribution of Lab Assets from Installed Package) — ACTIVE (proposed) — Already ACTIVE; this PR adds the executable gate for its clean-install clause.
    • DSL-008 (APTL Realization of ACES Infrastructure Topology) — ACTIVE (proposed) — Already ACTIVE; adds traceability for the admitted-scenario seam and the realization-observation excess check.

    Files changed

    Added:

    • docs/architecture/issue-951-fresh-env-pack-start-preflight.md
    • scripts/ci/assert_project_teardown.py
    • tests/test_lab_fresh_start.py

    Modified:

    • .github/workflows/checks.yml
    • docs/architecture/index.md
    • docs/requirements/DEP-008/requirement.md
    • docs/requirements/DSL-008/requirement.md
    • mkdocs.yml
    • src/aptl/backends/_raes_scenario_queries.py
    • src/aptl/backends/_runtime_concern_excess.py
    • src/aptl/backends/raes.py
    • src/aptl/backends/raes_start_model.py
    • src/aptl/core/lab.py
    • src/aptl/validation/_live_gate_probes.py
    • src/aptl/validation/participant_agency_qualification.py
    • src/aptl/validation/participant_agency_readiness.py
    • src/aptl/validation/participant_qualification_boundary_environment.py
    • tests/test_bounded_participant_agency_scenario.py
    • tests/test_bounded_participant_runtime.py
    • tests/test_lab.py
    • tests/test_raes_backend.py
    • tests/test_raes_runtime_observation.py
    • tests/test_scenario_bundle_wiring.py

    Reviews

    • codex: Core: ship, no findings. Security: one blocking finding — the Docker DNS listener exemption was address-wide. Fixed to attribute only the resolver's own sockets, failing closed otherwise, with three regression tests.
    • test-quality: Clean, no findings.

    Traceability reconciliation

    • Pending — requirement status transition and IMPLEMENTS/TESTS reconciliation run in Phase E once the PR merges.

    Status

    • CI: ✅ green
    • SonarCloud: ✅ passed
    • PR ready for user review and merge. Ground Control reconciliation (requirement status + traceability) runs on merge (Phase E).
  16. Brad-Edwards commented on Sep 4, 2026

    @Brad-Edwards
    CollaboratorAuthor

    Final report — issue #951 complete

    PR: #960
    Plan: #951 (comment)

    Outcome

    Installing APTL from PyPI and following the documented quick start now works. Before this, aptl lab start failed after about nine seconds with eight bind-mount errors, for the default scenario and every catalog scenario; it stayed hidden because developer machines carry leftover generated files and CI had never booted a lab. Lab start now reads what the scenario it is about to run actually declares. CI now builds the wheel, installs it clean, boots a real lab, tears it down, and proves nothing is left behind.

    Lab start admits the selected scenario exactly once and derives the bundle root, resolved source kind, selected profiles, and generated-artifact ownership from that single admission, which apply also consumes. That replaces three separate guesses: a deleted default scenario path, the operator's container ceiling, and the configured scenario source. Also fixes the SEM-218 excess-listener check, which counted Docker's embedded DNS resolver sockets as undeclared exposure and failed every node declaring a service listener. Merged as 170593a; 5.2.1 follows from Release Please on the fix: title.

    Files changed

    Added:

    • docs/architecture/issue-951-fresh-env-pack-start-preflight.md
    • scripts/ci/assert_project_teardown.py
    • tests/test_lab_fresh_start.py

    Modified:

    • .github/workflows/checks.yml
    • docs/architecture/index.md
    • docs/requirements/DEP-008/requirement.md
    • docs/requirements/DSL-008/requirement.md
    • mkdocs.yml
    • src/aptl/backends/_raes_scenario_queries.py
    • src/aptl/backends/_runtime_concern_excess.py
    • src/aptl/backends/raes.py
    • src/aptl/backends/raes_start_model.py
    • src/aptl/core/lab.py
    • src/aptl/validation/_live_gate_probes.py
    • src/aptl/validation/participant_agency_qualification.py
    • src/aptl/validation/participant_agency_readiness.py
    • src/aptl/validation/participant_qualification_boundary_environment.py
    • tests/test_bounded_participant_agency_scenario.py
    • tests/test_bounded_participant_runtime.py
    • tests/test_lab.py
    • tests/test_raes_backend.py
    • tests/test_raes_runtime_observation.py
    • tests/test_scenario_bundle_wiring.py

    Reviews

    • codex: Core: ship, no findings. Security: one blocking finding — the Docker DNS listener exemption was address-wide. Fixed to attribute only the resolver's own sockets, failing closed otherwise, with regression tests.
    • test-quality: Clean, no findings.

    Traceability reconciliation

    • IMPLEMENTS / TESTS / DOCUMENTS added: 7
    • Links updated: 0
    • Stale links removed: 0

    Requirement-free issue: no UIDs are in scope, so no status transition applied. The two requirements above are traceability anchors for the new CI gate and the realization-observation change; both were already ACTIVE and verified so at merge revision 170593a.

    Status

    • CI: ✅ green
    • SonarCloud: ✅ passed
    • PR ready for user review and merge.
  17. removed
    in-progressAn agent is actively working this issue via /implement
    on Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingscope:aptl-coreAPTL platform/range capability independent of one scenario.user-facingDirectly changes a user journey or user-visible outcome

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions