Plugin Sandbox Threat Model (STRIDE)

Audience: Security Guild, Plugin/Release-Orchestrator engineers, operators hardening plugin hosts. Scope: A STRIDE analysis of the host-side plugin execution boundary across the BuiltIn, Trusted, and Untrusted sandbox modes, with verified controls and tracked residual risk.

Sprint 20260501-054 PLG-OS-CONF-06. Companion to the implementation in Plugin module dossier and the operator runbook Plugin sandbox on Linux. Covers BuiltIn (InProcessHardened), Trusted (OutOfProcessOverlay), and Untrusted (OutOfProcessConfined) execution modes shipped by sprints 20260501-003 / 051 / 052 / 053 / 054.

The host-side implementation lives in src/Plugin/ — chiefly StellaOps.Plugin.Abstractions (PluginTrustLevel), StellaOps.Plugin.Host (AssemblyPluginLoader, SandboxedPluginProxy, PluginTrustDecisionMaker), and StellaOps.Plugin.Sandbox (SandboxModeMatrix, ProcessSandbox, DefaultPluginSandboxWorkspaceFactory, LinuxAppArmorAttacher, WindowsResourceLimiter, NetworkPolicyEnforcer, LinuxStaleMountSweeper, and the PluginSandboxEvent* diagnostics surface). The ReleaseOrchestrator module consumes this runtime; it does not own the sandbox code.

1. Scope & Method

2. Trust Levels & Sandbox Modes

Trust levelSandbox modeProcess boundaryFilesystemOS confinementDefault for
BuiltInInProcessHardenedNone — runs in host ALCHost filesystem (SDK policy seam only)NoneFirst-party packs shipped with the platform
TrustedOutOfProcessOverlayProcessSandbox (gRPC bridge)Overlay workspace (kernel overlayfs / fuse-overlayfs / Windows junction)None by defaultVerified/signed third-party plugins from a trusted issuer
UntrustedOutOfProcessConfinedProcessSandbox (gRPC bridge)Overlay workspace + lower-immutability proofLinux: AppArmor (or SELinux where supported); Windows: Job Object with memory + active-process limitsPlugins lacking strong signature trust

Trust level is derived from signature verification in PluginTrustDecisionMaker.DecideAsync: the signature must verify (IPluginSignatureValidator.ValidateAsync), then the verified leaf thumbprint is mapped to BuiltIn / Trusted / Untrusted against the PluginHostOptions.BuiltInSignerThumbprints / TrustedSignerThumbprints signer allowlists; a verifying signature whose thumbprint is on neither list resolves to Untrusted. A signature that does not verify is rejected outright (PluginSignatureRejectedException). Plugin identity allowlists are not a trust input — only the cryptographic signer thumbprint is. SandboxModeMatrix.FromTrust then maps the trust verdict to the runtime sandbox mode, and AssemblyPluginLoader consumes that mode (it rejects in-process Trusted loads outright; see TB2). A dev-only escape hatch (AllowUnsignedPluginsInDevelopment, honoured only when IHostEnvironment.IsDevelopment()) loads unsigned plugins as Untrusted with a structured warning; it is ignored in Production.

3. Trust Boundaries

BoundaryRationaleControls
TB1 — Plugin install dir ↔️ Sandbox lowerThe verified plugin contents must remain immutable for the lifetime of any sandbox.Read-only attribute on Windows install files; overlayfs lower immutability on Linux; workspace factory rejects overlap and reparse escapes.
TB2 — Host process ↔️ Plugin processTrusted/Untrusted plugins run in a separate process; host code must never load Trusted plugin types in-process.AssemblyPluginLoader rejects direct Trusted in-process loads; SandboxedPluginProxy routes operation invocation through IPluginOperationInvoker over the gRPC bridge.
TB3 — Plugin process ↔️ Host kernelPlugin process inherits the service account’s effective capabilities. For Untrusted, OS confinement narrows them.AppArmor profile, SELinux sample policy, Windows Job Object with kill-on-close + memory + active-process limits + no breakaway.
TB4 — Sandbox workspace ↔️ Host filesystemPlugin-visible writes must land in upper, never above the sandbox root.Workspace factory absolute-path + non-traversal + non-overlap validation; cleanup constrained to the configured sandbox root.
TB5 — gRPC bridge ↔️ Other host servicesThe plugin bridge is the only sanctioned IPC channel; named pipes / Unix sockets are local-only.Per-sandbox correlation IDs, JSON payload bounds, deterministic error mapping (plugin.sandbox.* / plugin.operation.*).
TB6 — Plugin process ↔️ Other pluginsTwo plugins must not share a workspace, mount point, or Job Object.Per-sandbox unique workspace under <sandbox-root>/<sandbox-id>/; per-process Windows Job Object handle; per-sandbox AppArmor profile attach.
TB7 — Plugin process ↔️ Network egressA confined plugin must not exfiltrate context or reach lateral hosts/services beyond its declared egress policy.NetworkPolicyEnforcer.ApplyPolicyAsync installs per-sandbox firewall rules (Linux iptables chain STELLAOPS_<id>; Windows netsh advfirewall rules). Untrusted default (SandboxConfiguration.Default/trust-level config) sets AllowAllHosts=false with an empty allowlist → block-all-outbound, plus a blocked-port set (e.g. 22/3389/5432/27017/6379). Trusted (SandboxConfiguration.Trusted) sets AllowAllHosts=true. Rules are torn down on stop (RemovePolicyAsync).

4. Data Flow Diagram (Untrusted on Linux)

flowchart LR
    subgraph Host
        H[Plugin host runtime
StellaOps.Plugin.Host] WSF[IPluginSandboxWorkspaceFactory] AA[AppArmor profile
stellaops-plugin-sandbox] NPE[INetworkPolicyEnforcer
iptables chain] BR[gRPC bridge
IGrpcPluginBridge] end subgraph SandboxRoot["sandbox-root/<id>"] L[lower
verified install] U[upper
scratch] W[work
overlay state] M[merged
plugin CWD] end subgraph Plugin["Plugin process (confined)"] P[plugin.dll loaded by host] end H -->|prepare| WSF WSF -->|mount overlay| M L -.read.-> M U -.write.-> M W -.work.-> M H -->|fork+exec| Plugin AA -.attach via aa_change_onexec.-> Plugin NPE -.egress filter (block-all-outbound).-> Plugin P <-->|gRPC over UDS| BR BR <--> H P -.reads.-> M P -.writes.-> M

5. STRIDE Analysis

Two distinct identifier families. Start/operation error codes carried by PluginSandboxException/PluginOperationException use the plugin.sandbox.* and plugin.operation.* / plugin.workspace.* prefixes (e.g. plugin.sandbox.os_confinement_failed, plugin.sandbox.process_exited, plugin.workspace.cleanup_failed). The structured diagnostic event names emitted through IPluginSandboxEventEmitter are a separate, namespaced set of stable constants in PluginSandboxEventName (plugin.sandbox.confinement.*, plugin.sandbox.workspace.*, plugin.sandbox.resource_limiter.*, plugin.sandbox.lifecycle.start_denied). They overlap conceptually but are not interchangeable strings.

#ThreatSTRIDEBoundaryRisk (L×I)Existing controlsResidual / follow-up
T01ALC reflection escape (legacy Trusted) — Trusted plugin uses Type.GetType / private reflection to call host internals or Authority APIs from inside the host ALC.ETB2Med×HighSprint 051 routes Trusted lifecycle through ProcessSandbox. AssemblyPluginLoader rejects direct in-process Trusted loads. Regression test fails if Trusted operation execution instantiates plugin types in the host process.None for new releases. Operators must complete the migration off the legacy ALC path; release notes call out the deprecation.
T02Process boundary bypass — Trusted/Untrusted plugin escapes the gRPC bridge by spawning a sibling process that exits the sandbox tree before kill.TTB2 / TB3Low×High (Untrusted), Med×Med (Trusted)Untrusted: Job Object on Windows with LIMIT_ACTIVE_PROCESS=1 + KILL_ON_JOB_CLOSE + no breakaway; AppArmor deny ptrace, deny mount, deny mknod. Trusted: process-tree kill on graceful-stop fallback.Trusted process-tree kill is best-effort — a fast-forking grandchild may escape briefly. Acceptable for Trusted; not acceptable for Untrusted, where LIMIT_ACTIVE_PROCESS=1 is the contract. Linux ptrace-deny is enforced by AppArmor only when the profile attaches; if attach fails, sandbox start fails closed.
T03Kernel overlayfs lower mutation from host — A privileged host actor (operator, sibling service, badly written init script) writes into lower. The kernel overlay does not detect host-level lower writes; subsequent reads through merged will see the mutation, breaking install-dir immutability.TTB1Low×HighWorkspace layout documents lower as immutable for sandbox lifetime. Operator runbook forbids lower mutation. Plugin install signature is verified before lower is exposed.Residual. No filesystem-level lock prevents a privileged sibling from writing to lower. Mitigation: keep the install directory on a read-only bind mount (operator runbook); the runtime cannot enforce this without taking ownership of host filesystem layout.
T04fuse-overlayfs FUSE-layer escape — A bug or non-default mount option in fuse-overlayfs exposes upper writes to other users on the host, or leaks host paths through xattr passthrough.ITB4Low×MedStrategy is opt-in only when kernel overlay is unavailable. Pin minimum version fuse-overlayfs ≥ 1.10 for xattr passthrough fixes. Mount options come from a typed builder, never shell concatenation. fuse module access is the host’s responsibility.Residual. FUSE userspace driver bugs are upstream risk. Mitigation: prefer kernel overlay where possible; treat fuse-overlayfs as the rootless/CI fallback.
T05Stale mount cleanup escapes the sandbox root — The stale-mount sweep (PLG-LINUX-OVL-04) walks above <sandbox-root> and unmounts host paths it should not touch.TTB4Low×HighImplemented: LinuxStaleMountSweeper.FindStaleSandboxMounts filters parsed /proc/self/mountinfo to overlay / fuse-overlayfs mounts that are strictly contained under the configured <sandbox-root> (IsStrictlyContainedIn, with a separator-boundary check so …/stellaops-runner-evil cannot match …/stellaops-runner, plus .. normalization). ILinuxMountActionExecutor.TryRemoveScratchAsync re-enforces the same strict-containment as a last gate. Per-entry failures are accumulated, never thrown, and surfaced as plugin.sandbox.workspace.cleanup_failed events.Residual closed for the prefix-matching threat. Covered by LinuxStaleMountSweeperTests against a fixture mount table that asserts <sandbox-root>-prefix-only matching (/home/operator/sandbox/merged is excluded). Symlink-target escape is handled by the action executor at sweep time, not parse time. The live unmount/scratch-removal path (SweepAsync) is Linux-only and runner-gated.
T06AppArmor false positives — Production plugin denied a legitimate path because the profile is too narrow. Operator removes the profile, regressing to no-confinement.DoS / ETB3Med×MedProfile authored from the workspace contract (read lower+merged, write upper; deny ptrace/deny mount/deny mknod and a denied capability bag in devops/linux/apparmor/stellaops-plugin-sandbox.profile). When AppArmorOptions.EnableAuditLogScraping is on, ApparmorAuditLogScraper tails the kernel audit sink (default /var/log/audit/audit.log) and converts apparmor="DENIED" AVC lines into structured plugin.sandbox.confinement.profile_denied events (Sprint 20260504-002 KENF-AA-02). SELinux sample policy mirrors the same intent.Residual. Profile false-positive risk is paid forward to denial-event monitoring. The scraper is opt-in and no-ops (with one startup warning, never throwing) on hosts without a readable audit log, so denial visibility is not guaranteed on every host. Operators must investigate denials before relaxing the profile; the threat model recommends a “complain mode” rollout window before “enforce” in production.
T07AppArmor / SELinux attach failure silently downgrades — Profile install or aa_change_onexec fails and the sandbox starts without confinement.ETB3Med×HighUntrusted attach is mandatory and is verified in ProcessSandbox.PreflightAppArmorLinuxAsync before the plugin process is ever spawned (step 3a, ahead of IPluginProcessManager.StartAsync). Any of profile-not-loadable / required-but-unavailable / aa_change_onexec failure raises SandboxStartupRefusedException, surfaced as the start-error code plugin.sandbox.os_confinement_failed (and the structured plugin.sandbox.confinement.fail_closed / …profile_missing events). Untrusted forces AppArmorOptions.RequireAppArmor=true on Linux. Trusted does not require attach (it runs the best-effort warn-and-degrade PreflightAppArmorTrustedWarnLinuxAsync path).Residual closed for Untrusted. Trusted intentionally has no OS confinement attach by default; operators who enable Trusted profile attachment opt in to the same fail-closed semantics.
T08SELinux sample-only limitation — Released SELinux .te is advisory; running on RHEL with enforcing mode but without the profile loaded means Untrusted runs without SELinux enforcement.ETB3Med×High on SELinux-first distrosSprint 054 explicitly marks SELinux sample-only until a validation runner exists; release notes call out that operators on SELinux-first hosts must load the policy or run with AppArmor.Residual. Until PLG-OS-CONF-03 has a runner, SELinux remains “operator-loaded”; the threat model treats SELinux Untrusted hosts as “AppArmor-equivalent intent, operator-verified”.
T09Windows junction reparse-point escape — A plugin abuses merged (junction target) to traverse out of the sandbox root via ..\ segments, or uses NTFS extended reparse semantics to redirect a path lookup.E / TTB1 / TB4Low×HighWorkspace factory uses Win32 reparse-point APIs (not cmd/mklink), validates absolute paths, rejects parent traversal, rejects source/destination overlap, rejects existing reparse points at the destination. Existing install files are marked read-only while the workspace is active.Residual. Junctions follow target on traversal; a plugin reading merged\..\..\Windows\System32\config\SAM is constrained only by NTFS ACLs of the host service account. Mitigation: run the plugin host service under a least-privilege account; full ACL enforcement awaits future driver-level COW work (PSB follow-up).
T10Policy-level COW residual on WindowsOverlayFilesystemPolicy redirects writes via SDK/policy interposition, not a filesystem driver. A native plugin that bypasses the SDK and calls CreateFileW directly on a lower path can observe the read-only ACL but not a write redirection.T / ITB1Med×High for native pluginsWindows install files are read-only while the workspace is active; native writes hit ERROR_ACCESS_DENIED. ALC-managed plugins are rerouted by SDK.Residual. Native plugins on Windows must opt in to writing through merged/upper paths; the threat model marks this as a known posture and tracks driver-level COW (WinFsp) as a future hardening sprint.
T11Job Object breakaway / child-process fan-out — Untrusted Windows plugin spawns child processes to exhaust active-process limits or escape the job.DoS / ETB3Low×HighSprint 054 PLG-OS-CONF-04: WindowsResourceLimiter.ApplyLimitsAsync sets the Untrusted Job Object to JOB_OBJECT_LIMIT_ACTIVE_PROCESS with ActiveProcessLimit=1 (from ResourceLimits.MaxProcesses, overridable only by explicit operator config), KILL_ON_JOB_CLOSE, and no breakaway flags. VerifyAppliedLimits re-reads the applied limits via QueryInformationJobObject and asserts IsProcessInJob, kill-on-close, no breakaway, and the configured memory/process limits — at step 5, before the gRPC bridge connects (step 6). Successful attach emits plugin.sandbox.confinement.job_object_attached; attach failure throws and emits plugin.sandbox.confinement.fail_closed.Residual. A child-process spawn past the active-process limit is rejected by the OS Job Object (CreateProcess fails); the rejection is an OS-level enforcement, not a per-attempt Stella event. Memory-pressure termination is verified locally but awaits dedicated Windows runner evidence before release sign-off.
T12Job Object memory-limit bypass — Untrusted plugin exhausts host memory below the per-job limit but above shared host headroom (e.g., balloons file-cache pages).DoSTB3Low×HighWindowsResourceLimiter configures LIMIT_PROCESS_MEMORY and LIMIT_JOB_MEMORY from ResourceLimits.MaxMemoryMb. The kernel terminates the plugin at or above the limit.Residual. Page-cache pressure outside the job is a host-capacity concern, not a confinement bypass. Operators sizing hosts must budget plugin memory ceilings against total host RAM.
T13gRPC bridge correlation collision — A misbehaving plugin reuses correlation IDs to confuse host operation routing.T / SpoofingTB5Low×LowGrpcPluginBridge.InvokeAsync / InvokeStreamingAsync generate a fresh per-call Guid.NewGuid().ToString("N") CorrelationId on the request; the plugin never supplies it. Operation routing is request/response correlated by the gRPC call itself (one outstanding InvokeAsync per call), bounded by the per-operation timeout.Residual closed. Correlation IDs are host-generated and opaque to plugins; the host does not re-read/validate the plugin’s echo of the ID, so the ID is a diagnostic correlation key rather than an integrity check — response binding relies on the synchronous gRPC call, not on echo matching.
T14gRPC payload exhaustion — Plugin returns an enormous JSON payload to OOM the host bridge serializer.DoSTB5Low×MedThe host bound is the gRPC channel’s default maximum receive message size (Grpc.Net.Client default ~4 MB); GrpcChannelOptions in GrpcPluginBridge.ConnectAsync does not raise it, so oversized responses are rejected at the channel before the JSON deserializer (JsonSerializer.Deserialize) runs. Operation timeouts terminate stalled invocations.Residual. The cap is the implicit gRPC channel default, not an explicit, operator-configurable MaxReceiveMessageSize; making it an explicit setting (and adding a JsonSerializerOptions.MaxDepth guard) is tracked as future hardening.
T15BuiltIn residual riskBuiltIn runs in the host ALC with full host privileges. A compromised first-party pack has full host process access.ETB2Low×CriticalBuiltIn is reserved for first-party packs shipped with the platform; the trust derivation cannot promote a third-party plugin to BuiltIn. SBOM/attestation governs the supply chain of first-party packs.Residual. This is the inherent cost of running first-party packs in-process; the threat model documents it as “supply chain governs first-party safety”.
T16STELLAOPS_SANDBOX_WORKSPACE_* env-var spoofing — A plugin reads its own workspace env vars and trusts them as ground truth, even though the plugin process can mutate its own environment.ITB5Low×LowEnv vars are diagnostics, not a security control. The host confirms workspace state through workspace metadata returned from IPluginSandboxWorkspaceFactory.Residual closed. Documented in dossier — env vars are diagnostics only.
T17Stale workspace from crash leaks signed install bytes — After a host crash, leftover upper/merged may contain partial copies of lower (materialized-copy strategy) accessible by other host users.ITB1Low×MedSandbox root is created with restrictive permissions (0700-equivalent) at host start. On Linux, the LinuxStaleMountSweeper (PLG-LINUX-OVL-04, now shipped) unmounts and best-effort-removes orphaned sandbox scratch under <sandbox-root> at host start.Residual. The sweeper’s live SweepAsync is Linux/runner-gated, and there is no equivalent automated crash-recovery sweep on Windows yet, so operators still rely on the runbook for crash cleanup on hosts where the sweeper does not run.
T18Plugin signature replay across trust transitions — Operator downgrades a plugin from Untrusted to Trusted by re-issuing a signature with a different issuer, retaining cached state from the Untrusted run.ETB2Low×MedSandbox state is per-invocation; upper/work/merged are deleted on stop. Signature trust is recomputed on every load; cached ALC instances are not reused across trust transitions.Residual closed. Operator policy decisions on issuer trust are governed separately.
T19Confinement event log forging — A plugin writes log lines that mimic confinement audit events to confuse SOC.Spoofing / RepudiationTB5Low×MedPlugin stdout/stderr is captured by the host; structured confinement events are emitted host-side only, through IPluginSandboxEventEmitter (default LoggerPluginSandboxEventEmitter) as typed PluginSandboxEvent records. Event names are stable constants in PluginSandboxEventName (validated by PluginSandboxEvent.Create); a plugin cannot inject a record into this channel because it never holds the emitter.Residual closed. The typed event subsystem (PLG-OS-CONF-05) is implemented: PluginSandboxEvent, PluginSandboxEventName, PluginSandboxEventReason/Severity/StrategyName, and the IPluginSandboxEventEmitter seam. Audit consumers match on the host-emitted event-name constants and ignore plugin stdout/stderr by category.
T20Cleanup-failure DoS — A buggy plugin holds a workspace mount across stop; cleanup fails; subsequent sandbox starts with the same id are blocked.DoSTB4Low×MedNew sandbox starts use a fresh per-invocation id, so a stale workspace cannot block new starts directly. Operator runbook covers manual cleanup.Residual. The blocked path is operator-visible (workspace exception code plugin.workspace.cleanup_failed / event plugin.sandbox.workspace.cleanup_failed); operator action is documented.
T21Plugin network egress / data exfiltration — A confined plugin opens outbound connections to exfiltrate host context or reach lateral services.I / ETB7Low×High (Untrusted), Med×Med (Trusted)NetworkPolicyEnforcer.ApplyPolicyAsync runs before ExecuteAsync and installs per-sandbox egress rules: Linux iptables custom chain STELLAOPS_<id> jumped from OUTPUT; Windows netsh advfirewall out rules. Untrusted defaults to block-all-outbound (AllowAllHosts=false, empty allowlist) plus a blocked-port set; allowlisted hosts are resolved to IPs and ACCEPT-ed before a trailing DROP. Rules are removed on stop.Residual. Enforcement depends on the host firewall stack (iptables/Windows Firewall) being present and the service account holding the privilege to program it; on hosts where the command fails the failure is logged (Linux apply rethrows; Windows logs) but rule installation is not independently re-verified the way Job Object / AppArmor attachment is. Trusted runs with AllowAllHosts=true by design. Egress controls are not part of the in-process BuiltIn posture.
T22Untrusted in-process compatibility ALCAssemblyPluginLoader still has a collectible-ALC path for Untrusted loads (used until the confined-process runner fully owns that route), so an Untrusted plugin loaded through the loader (rather than ProcessSandbox) runs in-process behind ALC isolation only.ETB2Low×HighThe loader hard-rejects in-process Trusted loads (SandboxMode.OutOfProcessOverlayPluginLoadException). For Untrusted it creates a collectible PluginAssemblyLoadContext (EnableAssemblyIsolation), not an OS-confined process; OS confinement (AppArmor/Job Object) only applies on the ProcessSandbox route.Residual. ALC isolation is escapable by reflection (the same class of weakness as legacy Trusted in T01). The intended runtime route for Untrusted is ProcessSandbox (SandboxMode.OutOfProcessConfined); the loader’s Untrusted ALC branch is a documented transitional compatibility path. Operators must drive Untrusted plugins through the process sandbox, not the in-process loader.

6. Open Items

ItemSprintStatusOwner
Kernel overlayfs and fuse-overlayfs workspace paths052 PLG-LINUX-OVL-02/03DONE in code (DefaultPluginSandboxWorkspaceFactory + LinuxOverlayMountCommand; sprint archived). Live kernel-enforcement still runner-gated — LinuxKernelOverlayIntegrationTests / LinuxFuseOverlayIntegrationTests skip without a Linux runner.Plugin + DevOps
Stale-mount sweep with <sandbox-root>-prefix constraint052 PLG-LINUX-OVL-04DONE (LinuxStaleMountSweeper + LinuxStaleMountSweeperTests fixture-table proof; sprint archived). Live SweepAsync is Linux/runner-gated.Plugin
AppArmor profile + attach strategy054 PLG-OS-CONF-02DONE (2026-05-02; devops/linux/apparmor/stellaops-plugin-sandbox.profile + libapparmor aa_change_onexec attach + WSL2 unit/integration tests shipped)Plugin + DevOps
AppArmor kernel-enforcement runner provisioning054 PLG-OS-CONF-02 residualOPEN (need a Linux runner with apparmor LSM compiled into kernel; WSL2 lacks it; multipass-on-Hyper-V networking blocked in current Windows 11 environment)Plugin + DevOps
SELinux sample policy + manual validation054 PLG-OS-CONF-03DONE (2026-05-02; sample devops/linux/selinux/stellaops-plugin-sandbox.te committed, checkmodule + semodule_package syntax-check green; rollout posture sample-only/advisory)DevOps + Docs
Windows Job Object memory-pressure runner evidence054 PLG-OS-CONF-04OPEN — Job Object attach/limit code (WindowsResourceLimiter) is DONE and unit-tested; live memory-pressure termination still needs a pinned Windows runner (sprint archived)Plugin
Structured Plugin sandbox confinement event subsystem054 PLG-OS-CONF-05DONE (PluginSandboxEvent + PluginSandboxEventName/Reason/Severity/StrategyName + IPluginSandboxEventEmitter; contract in docs/modules/plugin/sandbox-confinement-events.md; sprint archived)Plugin + Docs
Driver-level Windows COW (WinFsp follow-up)post-053Future hardeningPlugin
Explicit, operator-configurable gRPC MaxReceiveMessageSize (+ JsonSerializerOptions.MaxDepth)futureBacklogPlugin

7. Release Notes (sprint 054 confinement posture)

The following items are added to docs/releases/RELEASE_PROCESS.md checklist “Plugin sandbox posture” and should appear in release notes for any release that ships the sprint 054 changes:

8. Review Cadence