Execution Plugins

Execution plugins are the Release Orchestrator extension point for build, scan, deploy, verify, and custom execution steps. They replace host-specific hardcoded deployment capability registration with a single signed and isolated contract built on the existing Stella Ops Plugin SDK.

Status

Accepted by ADR-006 and implemented under docs/implplan/SPRINT_20260525_012_ReleaseOrchestrator_execution_plugin_framework.md.

Runtime bundle composition now follows the production-shaped root devops/plugins/execution/<profile>/<plugin-id>/ mounted read-only to /app/plugins/execution/<profile>/<plugin-id>/. The lab producer devops/scripts/build-lab-plugin-bundles.ps1 defaults to the harness profile, and docker-compose.lab-plugin-bundles.yml now mounts /app/plugins/execution/harness plus the execution trust root under /app/trust-roots/plugins/execution.

Sprint 20260604_007 adds the production-shaped first-party bundle producer devops/build/package-execution-plugins.ps1. The default profile emits build.docker, deploy.compose, deploy.exec, deploy.artifact-extract, and deploy.docker-plugin into the ignored runtime drop devops/plugins/execution/default/; the optional-build-script profile emits only build.script into devops/plugins/execution/optional-build-script/. The producer writes stellaops.execution.bundle-inventory.json beside each profile and uses the execution trust root under devops/etc/certificates/trust-roots/plugins/execution/. Local development uses the repo’s lab signer; release engineering must replace that signing material for production/offline-kit publication.

First-party execution plugin manifests must carry a signed Pattern Z requires block for the host-owned services their implementations consume. Agent Core admission fails closed for the known first-party execution plugin IDs when those service declarations are missing, weakened to optional for required services, scoped outside perPlugin, or versioned outside ^1.0.

The default compose overlay is devops/compose/docker-compose.plugins.execution.yml; the optional overlay is devops/compose/docker-compose.plugins.execution-optional.yml. Render them through devops/compose/scripts/compose-cli.ps1 with COMPOSE_PROFILES=agents so the profile-gated agent-core service is included. Full compose output can contain local environment values; persist only sanitized plugin-related env and volume summaries in QA evidence.

Composability inventory

Capability / pathSourceProfilePackaging and runtime note
build.dockersrc/ReleaseOrchestrator/__Agents/StellaOps.Agent.BuildDocker/plugin.manifest.jsondefaultSigned mounted bundle; requires declares IBuildxProcessRunner plus optional attestation/reachability producer and result-emitter contracts.
build.scriptsrc/ReleaseOrchestrator/__Agents/StellaOps.Agent.BuildScript/plugin.manifest.jsonoptional-build-scriptSigned mounted bundle, opt-in only because it runs operator-supplied containerized code; requires declares ContainerExecutionPluginBackend.
deploy.compose / deploy.compose.upsrc/ReleaseOrchestrator/__Agents/StellaOps.Agent.DeployCompose/plugin.manifest.jsondefaultSigned mounted bundle wrapping ComposeCapability; requires declares the host-owned IAgentCapability bridge and legacy compose.* aliases remain compatible.
deploy.execsrc/ReleaseOrchestrator/__Agents/StellaOps.Agent.DeployNative/plugin.manifest.exec.jsondefaultSigned mounted native deploy bundle; requires declares IDeployProcessRunner.
deploy.artifact-extractsrc/ReleaseOrchestrator/__Agents/StellaOps.Agent.DeployNative/plugin.manifest.artifact-extract.jsondefaultSigned mounted native artifact extraction bundle; requires declares IDeployProcessRunner and IArtifactFileSystem.
deploy.docker-pluginsrc/ReleaseOrchestrator/__Agents/StellaOps.Agent.DeployNative/plugin.manifest.docker-plugin.jsondefaultSigned mounted plugin-container deploy bundle; requires declares IDeployProcessRunner and IArtifactFileSystem; digest validation remains part of the task/payload path.
ansible / deploy.ansiblesrc/ReleaseOrchestrator/__Agents/StellaOps.Agent.Ansibleoptional capabilityHost-owned optional capability today; not part of the default bundle producer because it depends on operator playbooks and runtime obligations.
Execution diagnostics/probe reportStellaOps.Agent.Core.Capability.ExecutionPluginProbeReporterhost-ownedReports catalog and enabled capability state to the execution plugin scratch root.
Legacy compatibility pathsAgentCapabilityExecutionPlugin and ExecutionPluginAgentCapabilityhost-owned shimsPreserve existing task types, heartbeat capability reporting, and compose.* aliases while payloads move to signed bundles.
Cloud/provider executorsStellaOps.Agent.Ecs, StellaOps.Agent.Nomadexplicit opt-in onlyNot mounted or enabled by the default execution overlay.

Goals

Contract

The neutral contract lives in src/Plugin/StellaOps.Plugin.Abstractions/Execution/IExecutionPlugin.cs. This keeps the execution contract next to IPlugin and avoids a dependency cycle: Release Orchestrator and later execution plugin assemblies can reference the Plugin SDK abstractions, while src/Plugin/** does not reference ReleaseOrchestrator internals.

An execution plugin implements IExecutionPlugin and also participates in the Plugin SDK IPlugin lifecycle. IExecutionPlugin.Capabilities intentionally hides the broad IPlugin.Capabilities flag property with execution capability strings; implementations that need both surfaces implement the SDK flag property explicitly.

public interface IExecutionPlugin : IPlugin
{
    ExecutionKind Kind { get; }

    new IReadOnlyList<string> Capabilities { get; }

    Task<ExecutionResult> ExecuteAsync(
        ExecutionContext context,
        ExecutionInputs inputs,
        CancellationToken cancellationToken);
}

ExecutionKind is one of Build, Scan, Deploy, Verify, or Custom. Capabilities are stable wire names such as deploy.compose, deploy.ssh, deploy.ansible, build.docker, or scan.sbom.

ExecutionContext carries host-controlled identity, tenant, target, release, correlation, timeout, and evidence context. ExecutionInputs carries scoped configuration, files, mounts, and secrets already resolved by the host or orchestrator. Plugins must not read ambient credentials. ExecutionResult returns status, structured outputs, emitted events, and raw-output proof where available.

Manifest Extensions

Execution plugins use the existing Plugin SDK manifest. Any execution-specific fields are carried under the optional execution object and are additive and backward compatible:

Existing integration plugin manifests remain valid.

Example:

{
  "execution": {
    "kind": "Deploy",
    "capabilities": [ "compose.up", "compose.rollback" ],
    "inputSchema": { "type": "object" },
    "outputSchema": { "type": "object" }
  }
}

Hosts

In-process host

Built-in .NET capabilities run in-process with BuiltIn trust. The agent host registers them as execution plugins, then exposes them through the existing capability registry. The migration keeps the current Docker, Compose, SSH, WinRM, ECS, and Nomad task types stable.

The first implementation keeps the existing IAgentCapability behavior behind two shims in StellaOps.Agent.Core:

StellaOps.Agent.Host no longer switches on capability names. It registers built-in execution plugins in ExecutionPluginCatalog and resolves EnabledCapabilities names or aliases from that catalog.

External in-process execution plugins are loaded through the Plugin SDK host from Plugins:PluginPaths. The agent enables strict SDK admission for this path:

External plugins are loaded into ExecutionPluginCatalog under their plugin ID. Each declared execution capability is also accepted as an EnabledCapabilities alias, so operators can enable either com.example.execution.ansible or deploy.ansible. If no external plugin is explicitly enabled, it remains loaded but absent from the task CapabilityRegistry.

Container host

Trusted and untrusted multi-language plugins run through the generalized container execution backend derived from the release script executor. The host controls mounts, environment variables, scoped secrets, network policy, timeouts, resource limits, and raw-output proof capture.

The compiled backend lives in src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Scripts/ExecutionPlugins/. ContainerExecutionPlugin implements IExecutionPlugin for an image-packaged plugin descriptor, while ContainerExecutionPluginBackend prepares the container spec through IExecutionPluginRuntimeImageManager and IExecutionPluginContainerPoolManager. This keeps the release-script evidence path (ScriptRawOutputProof) as the proof format for multi-language execution plugins without forcing the older incomplete script Docker implementation into the build.

Container descriptors declare plugin identity, kind, execution capabilities, image reference, trust level, signature validation result, command, args, non-secret environment, mounts, network policy, and resource limits. For Trusted and Untrusted plugins, the backend fails closed unless the Plugin SDK signature result is accepted before any container is acquired.

Invocation inputs are projected into scoped container inputs only:

The backend does not copy ambient process environment variables into the container spec, and descriptor environment keys prefixed with STELLA_ are rejected so a plugin cannot spoof host-scoped variables or secrets. Network is disabled by default and must be declared explicitly through the descriptor network policy.

Current verification uses a deterministic accepted-signature container fixture with a fake container pool. It proves backend loading, scoped input projection, timeout/mount/network/resource propagation, fail-closed signature handling, and ScriptRawOutputProof capture. A live signed directory-loaded plugin and real container execution remain gated by external plugin loading/signature integration and local container runtime availability.

Authoring Execution Plugins

In-process SDK plugins

Use in-process plugins for built-in or tightly controlled .NET execution code that can run inside the target-agent process. A new in-process plugin must:

Built-in target-agent capability migration uses the lower-risk shim path: AgentCapabilityExecutionPlugin adapts an existing IAgentCapability to IExecutionPlugin, then ExecutionPluginAgentCapability exposes the plugin back through the current capability registry. This is the required path for docker, compose, ssh, winrm, ecs, and nomad until their payload builders move to a native plugin surface.

ECS and Nomad are optional built-in plugins. Operators enable them through Agent:EnabledCapabilities or the legacy CSV Agent:Capabilities value. ECS uses the AWS SDK credential chain and optional Agent:EcsRegion; Nomad uses Agent:NomadAddress, then NOMAD_ADDR, then http://127.0.0.1:4646.

Container-host plugins

Use container-host plugins for multi-language execution steps or untrusted operator extensions. A container-host plugin is described by a ContainerExecutionPluginDescriptor and wrapped by ContainerExecutionPlugin. Authors must package the runtime as an offline-available image reference, preferably pinned by digest, and declare:

At invocation time, the host projects scoped inputs into the container: STELLA_EXECUTION_ID, STELLA_CAPABILITY, STELLA_PAYLOAD, optional context IDs, STELLA_VAR_* variables, STELLA_SECRET_* secrets, and STELLA_FILE_* file references. The process environment is not copied into the container. Secrets must not be written to stdout, stderr, or output proof.

Container plugins report structured outputs by emitting stdout lines in the form STELLA_OUTPUT: key=value. The backend captures stdout, stderr, exit code, duration, timeout state, image reference, digest, and signature metadata as ScriptRawOutputProof.

The non-live authoring contract is complete. Phase 3 still must prove a signed directory-loaded container plugin running on a real local/CI container runtime before EPF-004 can move from BLOCKED to DONE.

Trust And Security

Capability Mapping

Deployment dispatch resolves task types to plugin capabilities through the registry instead of a hardcoded switch. The first migration preserves these existing task strings:

Existing task typeExecution capability
docker.rundeploy.docker
compose.updeploy.compose
ssh.executedeploy.ssh
winrm.executedeploy.winrm
ecs.deploydeploy.ecs
nomad.deploydeploy.nomad
deploy.ansibledeploy.ansible
docker_plugin_deploydeploy.docker-plugin

New capabilities, such as deploy.ansible, deploy.docker-plugin, build.docker, and build.script, are added by registering execution plugins rather than editing the agent host switch.

Built-In Ansible Plugin

deploy.ansible is the built-in Ansible deploy plugin. It is intentionally a thin launcher for operator-supplied automation:

For the API setup, secret-boundary rules, and live acceptance procedure, use the Ansible delivery environment onboarding runbook.

The plugin runs ansible-playbook with ProcessStartInfo.ArgumentList and does not invoke a shell. It does not generate, rewrite, template, or vendor playbooks or roles. The operator provides the playbook tree through the configured path or mount, and the plugin runs the selected playbook as-is. The standalone agent-core image carries ansible-core; the Ansible capability health check verifies ansible-playbook --version when the capability is enabled.

The BUSL-1.1 wrapper and GPLv3 Ansible runtime remain separate programs: Stella Ops invokes ansible-playbook as a separate process and does not link to or vendor Ansible code. The license posture is recorded in NOTICE.md and docs/legal/THIRD-PARTY-DEPENDENCIES.md.

Built-In Build Plugins (Sprint 017)

build.docker

Build-time co-production (Sprint 017, ADR-024)

When Stella both builds and pushes the image itself, it holds the source and the push rights at the same instant — the moment to co-produce SBOM + reachability referrers bundled with the pushed digest. This is opt-in and gated entirely on payload flags; every flag defaults false/null, so existing manifests load unchanged and the pre-sprint behaviour is preserved byte-for-byte.

2026-06-12 source-contract update: once buildx reports the pushed digest, BuildDockerExecutionPlugin composes a digest-pinned subject reference as imageName@sha256:<digest> and forwards it through BuildAttestationEvidence, HttpBuildAttestationEvidenceEmitter, BuildCoProductionRequest, BuildCoProductionSignal, and ComponentVersionScmInfoAvailable. The Scanner bridge consumes the event field as image.reference, so source-backed scans have the OCI coordinate needed for subject pulls and referrer publication even when a raw digest alone would not identify the registry repository.

Stella-built vs. externally-built boundary
Image built + pushed by Stella (build.docker)Image built elsewhere, ingested post-hoc
Triggerbuild.docker --push success pathsync-versions discovers a digest; manual PATCH .../versions/{id} adds SCM
SBOM referrercentral scan (Phase 1)post-hoc registry scan
Reachability sourcebuild-context (Phase 2) or scm-fetch (Phase 1)scm-fetch (SCM available) or rootfs (rootfs-only)
Sinkdual-write coordinator (internal-first; opt-in external)same dual-write coordinator

The shape, sink, and attachment-state machine are identical across both columns — only the reachability source annotation differs. There is no separate code path for Stella-built images downstream of the event; both feed the same ScmInfoAvailableScanTrigger and the same reachability-referrer serializer.

Graceful degradation ladder (a build NEVER fails on a co-production miss)
  1. Neither emitSbom nor emitReachability set → nothing published (opt-in).
  2. No pushed digest → nothing published (the image was not pushed; nothing to attach to).
  3. No operator-pinned componentId/versionId AND the pushed digest does not resolve to a component-version (registry sync hasn’t recorded it yet, no registry, or registry outage) → SBOM-only (SkippedNoComponentForDigest): no fabricated correlation; the post-push scan still attaches an SBOM by digest. (An operator-pinned explicit pair bypasses this step entirely — it is used verbatim, so a freshly-built digest still engages co-production.)
  4. No complete SCM info (commit + sourceRepositoryUrl) → SBOM-only: the source-derived event is not published, but the post-push scan still produces an SBOM referrer by digest.
  5. Phase 2 producer absent/disabled → degrade to Phase 1 (source=scm-fetch).
  6. Any referrer/reachability/transport error is swallowed + logged; the build result is unaffected.

Build reachability producer (Phase 2, BLD-003)

2026-06-12 packaging update: Agent.Host can now publish a reachability-capable artifact with /p:IncludeBuildDockerReachability=true. At runtime, Agent:BuildReachabilityProducerEnabled=true loads StellaOps.Agent.BuildDocker.Reachability.dll and calls AddBuildDockerReachabilityProducer(...) to replace the no-op producer. If the runtime flag is enabled without that packaged DLL, startup fails with an explicit configuration error instead of silently falling back to the no-op. The live build -> push -> registry -> scan proof remains the open BLD-003 criterion. The local image helpers expose this as AGENT_CORE_INCLUDE_BUILD_REACHABILITY=true; pair it with AGENT_CORE_BUILD_COPRODUCTION_ENABLED=true and AGENT_CORE_BUILD_REACHABILITY_PRODUCER_ENABLED=true only for the live build-time co-production proof lane.

build.script

Native Deploy Plugins (Sprint 017)

These plugins are the “do not require an external deploy tool” path. Each is default-off until selected via a release template; the legacy compose.* capability strings continue to resolve through deploy.compose for backward compatibility.

deploy.compose

deploy.exec

deploy.artifact-extract

deploy.docker-plugin

The Ansible-retirement parity matrix and the go/no-go decision tied to these native plugins are recorded in docs/implplan/SPRINT_20260525_017_ReleaseOrchestrator_build_native_deploy_plugins.md (NDP-003).

Registry Diagnostics

IExecutionPluginRegistryDiagnostics lists loaded execution plugins for agent diagnostics and the FE plugin-list work. Each descriptor includes:

Built-in adapters report signature status NotRequired. External plugins must report Verified; rejected unsigned or malformed plugins never enter the catalog.

Agent Core also writes the worker-style runtime-composition report at startup. ExecutionPluginProbeReporter reads the execution catalog and active CapabilityRegistry, probes enabled plugins through IExecutionPlugin health checks, and writes canonical PluginProbeReport JSON to /var/lib/stellaops/plugin-scratch/execution/probe-report.json by default. The scratch root is configurable with StellaOps:Plugins:ScratchRoot, matching the compose StellaOps__Plugins__ScratchRoot environment key. If the shared old-host bridge is adopted for Agent Core later, the bridge call point is the single write path in ExecutionPluginProbeReportWriter.

Verification Requirements

The framework is not complete until these gates pass: