Issue #980 Pack Adapter Install Seam Preflight¶
This note is the architecture preflight for issue #980. It is guidance, not an
implementation plan. At this preflight's baseline (3ce89ad5), APTL already
selects installed scenario startup, runtime-parameter, pack/backend interaction,
verification, and participant-smoke providers through entry points. Capture,
several product-specific lifecycle choices, operator-group vocabulary, and a
release compatibility shim still bypass that boundary.
The change must complete those seams without creating a general plugin object, a second capture model, or a second lifecycle controller. ADR-047 and EXP-010 remain authoritative for capture admission and evidence persistence; ADR-053 remains authoritative for data-only pack/backend serving; the issue #970 preflight remains authoritative for lifecycle coordination. Issue #980 narrowly changes ADR-053's current assumption that operator-group labels come from APTL's static configuration fields: the admitted scenario/adapter mapping is now the source of that bounded vocabulary.
Architecture Decisions And Guardrails¶
Select one exact adapter set once¶
All issue #980 hooks are selected from the already admitted PackIdentity, not
from a default pack name, an SDL path, a registration ID, or the set of every
installed provider. The selection key is the exact pack ID plus backend profile;
the provider contract also validates pack version and set digest. Discovery
filters an entry-point name before importing it, admits exactly one compatible
provider, and records host-observed distribution name/version and entry-point
name. Duplicate, malformed, incompatible, or load-failed candidates fail closed
with stable bounded diagnostics.
Reuse aptl.backends.pack_interaction_discovery as the strongest incumbent for
that protocol. scenario_startup, scenario_runtime_parameters, and
scenario_verification_discovery supply the adjacent patterns. Do not create a
repository-wide plugin registry or make provider discovery a mutable global.
Installed Python is trusted executable code, not a sandbox boundary; APTL does
not auto-install a provider merely because an authored pack asks for one.
The normalized selections are request-scoped and travel with the admitted scenario start. They are reused through planning, realization, capture, verification, retry, and teardown. Restart-safe state pins the same pack and host-observed provider provenance so a later process does not silently bind a different installed release.
Keep purpose-specific contracts separate¶
The new capture entry-point group is aptl.scenario_capture; its selector is
<pack_id>.<backend_profile>. Its provider exposes a typed, immutable capture
contribution. Keep these parts distinct inside that contribution:
- declaration data: a tuple of core
CollectorRegistrationvalues; - executable source construction/finalization behind a narrow typed runtime context; and
- evidence interpretation metadata needed to corroborate admitted propositions.
CollectorRegistration remains declaration-only. It must not acquire factories,
callbacks, import strings, commands, paths, credentials, or backend handles.
Core owns its schema and validation; aptl_techvault instantiates it. This keeps
registration admission deterministic and keeps executable authority out of the
capture plan.
Pack-release planning compatibility uses a separate typed installed-provider
contract, selected with the same exact identity. Use the entry-point group
aptl.scenario_planning_compatibility, also keyed by
<pack_id>.<backend_profile>. It must not be folded into capture, verification,
runtime parameters, serving interaction, or a catch-all scenario adapter.
The existing aptl.scenario_startup contract remains the home for fixed
lifecycle hook slots. The existing aptl.pack_backend_interactions contract
remains data-only. The existing aptl.scenario_verifiers contract remains the
semantic verdict boundary. Different authority and failure semantics are the
reason these seams stay separate even when one distribution implements all of
them.
Capture admission is one immutable path¶
The selected capture contribution constructs the CollectorRegistry used by
the whole admitted run. A scenario-backed run must use that same registry for:
- the RAES backend manifest's aggregate observation projection;
compile_scenario_capture_demands()matching andCaptureBindingcreation;- capture-plan digesting and create-once persistence;
- runtime source/collector construction; and
- transcript parsing, finalization, readback, and proposition corroboration.
Do not aggregate registrations from all installed packs. Do not retain
import-time or default-argument references to DEFAULT_COLLECTOR_REGISTRY on
the admitted-scenario path. A core-only scenario has an honest empty registry
unless a substrate-generic core registration is independently justified; it
must not receive TechVault registrations by fallback.
build_collectors may bind a generic WindowedQueryCollector for any ID in the
admitted registry/bindings. It must still reject a source key that is absent
from that admitted set. “Any admitted registration” does not mean “any caller
supplied string.” Preserve the coordinator's independent check that a
collector's registration ID equals the pinned CaptureBinding ID.
Reuse validate_registration_id, CollectorRegistry, CaptureBinding,
CaptureLimits, CaptureVisibility, CapturePlan, and the existing canonical
digest. Registration IDs remain non-executable labels. Duplicate IDs fail at
provider admission; limits, media types, contract versions, sensitivity,
redaction, retention, and loss disclosure remain core-validated.
The current NATIVE_EVIDENCE_CAPABILITIES map is a second TechVault schema for
facts already related by evidence requirement, capture offer/binding, and
proposition metadata. A second pack must not require another map under
src/aptl. Derive corroboration from the exact admitted binding and normalized
provider interpretation metadata, validating every authored semantic axis.
Never infer proposition truth from a registration ID alone or treat receipt of
a record as proof of its claim.
If capture-plan or run-record projection gains provider provenance, evolve the existing versioned schema and canonical digest. Do not create a parallel scenario-capture plan or repository. Persisted active-transcript authority must pin enough exact adapter provenance to finalize safely after process restart; current configuration or “whichever provider is installed now” is not durable authority.
Move behavior, not only filenames¶
All twelve core/evidence/adapters/techvault*.py modules move under
aptl_techvault. The boundary is transitive: no module under src/aptl may
import their types or functions. In particular, transcript parsing, native
evidence acquisition, proposition truth, and volume-reset cleanup currently
contain indirect TechVault dependencies that must consume the selected typed
contract instead.
Core continues to own the collector protocol, coordinator, typed outcomes, redaction, quotas, persistence, and backend effects. The adapter owns product API queries, product response parsing, readiness probes, transcript semantics, and product-specific reset work. Teardown invokes only the adapter selected for the persisted run; it must not broadcast reset callbacks to all installed providers.
The current transitive leaks are part of the boundary, not incidental cleanup:
backends/_raes_transcript_parsing.pyimports TechVault transcript/source types;backends/raes_evidence_acquisition.pyexports a fixed native-registration set and TechVault owner;backends/_raes_native_evidence_acquisition.pyconstructs that owner;core/lab.pyprojects credentials and activates capture from the fixed set;core/deployment/_compose_stop.pyimports TechVault baseline cleanup; andbackends/_raes_native_proposition_truth.pydecides claims from a TechVault table.
Replacing the twelve module imports while leaving any of these identity-gated handoffs in core would not satisfy the second-pack acceptance case.
Startup hooks express work, not product nouns¶
StartupCapability retains only substrate-generic lab mechanics. WAZUH,
SOC, and WAZUH_REPAIR are not renamed to vague generic flags; they disappear
from core. Their behavior is supplied by the selected startup provider through
fixed typed hook slots such as preparation, bounded retry repair, and reset.
Core retains phase order, deadlines, cancellation, diagnostics, retry count,
and the rule that retry uses the same admitted plan.
Hooks may not add an arbitrary stage graph, invoke the planner, change the
backend, mutate admitted resources, select services by core configuration field
name, or return shell fragments. Product credential requirements are explicit
validated declarations. They do not justify passing the whole .env,
os.environ, AptlConfig, run store, or project filesystem to provider code.
Removing enum members is incomplete if equivalent branches remain. Audit the
admitted route for named wazuh, soc, techvault, container, registration,
and SDL-path conditions, including retry, certificate generation, capture
activation, stop/reset, and recovery. Unrelated repository-wide identifier
enforcement remains #1121.
Operator groups come from admitted component mappings¶
The serving provider still returns a total, exact mapping for the admitted
component addresses; it cannot invent or omit components. Normalize that
mapping first, then derive the run's operator-group vocabulary from its group
values. ContainerSettings.model_fields is not an adapter vocabulary and must
not be passed to the provider as one.
Each non-null group is a bounded safe Compose-profile token: non-empty, unique
after normalization, length-limited, and free of path, control, whitespace,
shell, and option-prefix forms. Core passes it to DeploymentBackend as a
separate argv value. The legacy enabled-profile fields do not filter
pack-defined groups; they have no vocabulary for an independently installed
pack. Any future operator policy for those groups must itself be admitted
against the selected mapping rather than inferred from ContainerSettings. A
pack may map every component to no group.
Stop, kill, and recovery must use the selected/persisted run's groups and
backend ownership evidence, not ALL_KNOWN_PROFILES, the current
ContainerSettings fields, or the default adapter. This change does not make
ContainerSettings an arbitrary dictionary and does not weaken its strict
Pydantic validation.
Compatibility hooks return bounded data¶
Core remains the only RAES compiler/planner/apply authority. It compiles once, invokes the exact selected compatibility hook once, validates the returned typed decision, and plans once. A compatibility provider cannot call RAES planning, suppress diagnostics, change addresses/resources/operations/capture or evaluation demands, or mutate module globals.
The existing TechVault shim's allowed transformations form the maximum initial
authority: release-scoped changes to the named runtime requirement axes and a
bounded runtime node-limit decision. Core owns the RLock, temporary limit
application, and finally restoration. The provider supplies data; it never
sets RAES globals itself. Unsupported releases get no transformation and fail
normally rather than falling through an identity heuristic.
Cross-Cutting Layers The Design Must Pass¶
| Layer | Canonical incumbent and required behavior |
|---|---|
| Web/API authentication | verify_token, WebAuthSettings, BFF Host/Origin/CSRF/session enforcement remain unchanged. This issue adds no route and passes no bearer/session material to an adapter. |
| First-party configuration | AptlConfig and nested Pydantic models keep extra="forbid"; ScenarioSourceConfig, deployment config, and project-name validation remain canonical. No entry-point name, Python import, command, or filesystem path becomes user-selectable configuration. |
| Pack admission | resolve_scenario_bundle, env_pack_bundle, environment-pack validate_pack, content manifests, and no-follow staging establish exact PackIdentity before provider loading. An adapter cannot replace or weaken pack validation. |
| RAES shape and policy | Public RAES parser, instantiation, compiler, planner, manifest validation, capture-demand compiler, and runtime disclosure remain authoritative. Provider output is revalidated and cannot waive a RAES diagnostic. |
| Installed extension admission | Reuse exact pre-load selector filtering, API/pack version and digest checks, backend compatibility, unique-match admission, host distribution provenance, bounded immutable results, and stable errors from the existing discovery modules. |
| Capture validation | Reuse validate_registration_id, duplicate detection, governed vocabulary projection, exact demand matching, config digests, immutable bindings/plans, coordinator identity/media/deadline/quota checks, and typed outcomes. |
| Secrets and environment | Reuse hydrate_dotenv, load_dotenv, EnvVars, declared key/alias validation, and redaction. Pass only explicitly required values. Secrets never appear in entry-point metadata, argv, URLs, logs, exception messages, adapter results, capture plans, or run evidence. Existing header/stdin/private-file transports remain canonical. |
| Filesystem and persistence | Reuse ScenarioBundle.read_asset, path-safe helpers, LocalRunStore, create-once plan persistence, content-store checksums, and secure atomic local state. Providers do not choose archive paths or write an alternate run database. |
| OS/backend exposure | DeploymentBackend remains the only Docker/Compose/SSH effect boundary. Use argv arrays without a shell, validate operator groups before use, bound subprocess time/output, and keep registration IDs and provider selectors non-executable. |
| Error envelopes and observability | Reuse RAES Diagnostic, StartupDiagnostic, CollectorStatus, AcquisitionDisposition, LabResult, get_logger, and redact. Log stable code, selector/provenance, counts, duration, and exception class; do not expose raw response bodies, stderr, paths, environment values, or exception text. |
| Packaging and supply chain | pyproject.toml owns entry points and wheel package inclusion; Hatch's bundled src copy, uv.lock, hashed requirements, asset-manifest generation, and pre-commit secret checks remain in scope. A clean install must work without network discovery or dynamic package installation. |
The auth layer is deliberately a no-op passage for this work, not an omitted security concern. The executable trust boundary is installation of Python code; contract validation limits accidental or confused authority but does not sandbox a malicious installed distribution.
Canonical Incumbents To Reuse¶
- Capture declaration/admission:
core.experiment.capture_registry,capture_plan,raes_evidence, andraes_manifest. - Acquisition:
core.evidence.protocol,sources,coordinator,outcomes, redaction,LocalRunStore, and the content store. - Pack identity/discovery:
scenario_bundle,scenario_startup,scenario_runtime_parameters,pack_interaction_discovery, andscenario_verification_discovery. - Lifecycle and effects:
orchestrate_lab_start,AdmittedScenarioStart,AdmittedStartSurface,DeploymentBackend, lifecycle locks, and existing startup diagnostics/results. - Serving:
PackBackendInteraction, its total-address validation and copied immutable projection. Its schema should evolve rather than gain a parallel mapping DTO. - Planning: the public RAES runtime model, compiler/planner, and the current
lock/restore discipline in
raes_planning_compat; only provider selection and bounded decision data move. - Persistence/provenance: existing capture plan, run record/backend evidence, active transcript authority, and canonical digests. Do not add a repository.
Whole-Repository Surfaces In Scope¶
The implementation must inspect and keep consistent at least these surfaces:
pyproject.toml, wheel inclusion, bundled lab-data source copy, and packaging artifact tests;src/aptl_techvaultproviders and all five existing entry-point contracts;- capture registry/plan/manifest/admission and every import-time default registry
reference under
src/aptl; - evidence wiring, native acquisition, transcript authority/parsing, proposition truth, reset, and stop/finalization paths;
- startup capability normalization,
.envhydration, retry/repair, certificate preparation, capture activation, stop, kill, and recovery in the lab route; - pack/backend component mapping, enabled-profile policy, Compose argv lowering,
persisted ownership, and the
ALL_KNOWN_PROFILESfallback; - planning compatibility in both plan and apply routes; and
- verifier discovery/execution and run-record provenance used by the acceptance path.
The obvious next extension is another pack release or backend profile with a
different registration set, operator-group labels, startup preparation, and no
planning workaround. The required parameter is therefore the one admitted
PackIdentity plus backend profile, carried through all purpose-specific
selections and durable authorities. Adding that variation must require only a
new installed distribution and authored environment pack, not a new core enum,
configuration field, registration table, proposition map, or conditional.
Acceptance And Regression Guardrails¶
The second-pack proof uses a real separately built and installed adapter
distribution with actual entry-point metadata in a clean subprocess/virtual
environment. It exercises pack admission, startup selection, capture
registration and acquisition, and verifier selection end to end. Monkeypatched
entry_points(), fake _EntryPoint objects, or direct provider injection are
useful unit tests but do not satisfy the issue.
Build and install from repository-local fixtures without network access. Assert
the fixture pack's token does not appear under src/aptl. Also prove duplicate,
missing, malformed, wrong-version/digest, wrong-backend, provider-exception,
unknown-source-ID, restart/provenance-drift, unsafe operator-group, and redacted
error paths fail closed. Preserve a core-only minimal scenario test and the real
TechVault wheel path.
Because the wheel bundles the tracked source tree beneath aptl/_labdata,
boundary tests must inspect both importable modules and bundled copies. Merely
moving Python packages while leaving old tracked modules in the bundled source
does not complete the boundary.
Gotchas And Anti-Patterns¶
- Do not merge every installed registration into a process-global registry.
- Do not cache selection without pack version, set digest, backend, and host distribution provenance.
- Do not bind an adapter from a registration ID, SDL filename, current config, or default TechVault identity.
- Do not let default arguments retain the old global registry after admission.
- Do not put executable factories into
CollectorRegistrationor serialize callables/import paths into plans. - Do not accept arbitrary collector source keys merely because wiring is no longer a fixed-ID allowlist.
- Do not duplicate RAES, capture, serving, error, or persistence schemas in the adapter package.
- Do not equate a successful API response or evidence record with proposition truth without exact semantic validation.
- Do not pass raw environment/config/store/path/backend authority when a narrow validated DTO suffices.
- Do not rename Wazuh/SOC branches to generic-sounding branches while retaining the same product test in core.
- Do not replace fixed lifecycle hook slots with an adapter-defined stage graph.
- Do not derive teardown profiles from static config or invoke all installed adapters during cleanup.
- Do not allow a compatibility provider to plan, apply, suppress diagnostics, or mutate RAES globals.
- Do not leak secret-bearing provider errors, response bodies, subprocess output, or paths through diagnostics or logs.
Non-Goals And Implementation Boundaries¶
- This preflight does not implement issue #980 or prescribe task sequencing.
- Repository-wide identifier enforcement, the ownership ledger, and removal of the default pack identity remain issue #1121.
- Broad decomposition of
lab.pyremains issue #970; #980 changes only the adapter-facing ownership needed to remove product branches from the admitted route. - This issue does not add dynamic package installation, untrusted-code sandboxing, arbitrary plugins, a generalized event bus, or user-authored lifecycle stages.
- It does not redesign RAES contracts, relax strict first-party configuration,
add new HTTP/MCP auth surfaces, or turn
ContainerSettingsinto a plugin schema. - It does not move generic coordination, validation, redaction, persistence, diagnostics, retry policy, backend effects, or verification orchestration out of core.
- Unrelated historical TechVault identifiers may remain until #1121, but no new issue #980 path may depend on them and the real second-pack path may not reach an identity-gated fallback.