Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
61690ba
Prepare versioned Linux package ownership and rollback gate
spenceralbrecht Oct 26, 2026
a442c66
Bound real Debian package phases with whole-runner quarantine
spenceralbrecht Oct 26, 2026
69f4a57
Prepare Windows Chocolatey ownership upgrade qualification
spenceralbrecht Oct 26, 2026
7397bb9
Bind Windows ownership qualification to packed payloads
spenceralbrecht Oct 26, 2026
443399a
Integrate bounded native package ownership qualification
spenceralbrecht Oct 26, 2026
18d6aef
Reconcile owned Windows shim and preserve primary failures
spenceralbrecht Oct 26, 2026
0416667
Reconcile verified legacy shim in Windows product hooks
spenceralbrecht Oct 27, 2026
0e9dc09
Keep Windows package finalization within service rollback
spenceralbrecht Oct 27, 2026
9fb4a60
Retain fixed Windows package failure diagnostics
spenceralbrecht Oct 27, 2026
683692f
Bound native path fixture cleanup and retain both failures
spenceralbrecht Oct 27, 2026
e6b320a
Reap owned fixture process trees before runtime cleanup
spenceralbrecht Oct 27, 2026
8909294
Preserve unqualified global shims during service migration
spenceralbrecht Oct 27, 2026
bff04c0
Isolate Linux native qualification in an owned systemd slice
spenceralbrecht Oct 27, 2026
8e8358a
Preserve qualifier failures and verify fixture unit ownership
spenceralbrecht Oct 27, 2026
23882ba
Check existing fixture units without loading implicit slices
spenceralbrecht Oct 27, 2026
4c539e6
Remove redundant unit-file query and label admission failures
spenceralbrecht Oct 27, 2026
6f22828
Bound fixture identities and fix Windows timeout executable
spenceralbrecht Oct 27, 2026
47c5d8a
Model the already loaded fixture helper directly
spenceralbrecht Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions .github/workflows/native-qualification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Agent native qualification

on:
push:
branches: [master, security/agent-node24-delivery-20261025]
branches: [master, security/agent-node24-delivery-20261025, security/agent-package-ownership-20261026]
pull_request:
branches: [master]

Expand Down Expand Up @@ -31,23 +31,26 @@ jobs:
npm run build
python3 scripts/test-stage-distribution.py
python3 scripts/test-install-linux-bundle.py
python3 scripts/test-linux-deb.py
python3 scripts/test-linux-slice.py
python3 scripts/test-native-preparation.py
python3 scripts/test-legacy-package-inputs.py
node scripts/test-native-report-fixture.cjs
node scripts/test-native-fixture-paths.cjs
- name: Stage pinned official runtime and labeled fixtures
run: python3 scripts/prepare-native-inputs.py --output "$RUNNER_TEMP/vmpower-native"
- name: Real native bundle CLI
run: '"$RUNNER_TEMP/vmpower-native/bundle/runtime/node" scripts/native-bundle-smoke.cjs'
- name: Stage exact retained public Debian package families before quarantine
run: python3 scripts/prepare-legacy-package-inputs.py --output "$RUNNER_TEMP/vmpower-native"
- name: Isolated systemd lifecycle and progress
run: |
sudo --preserve-env=GITHUB_ACTIONS,RUNNER_ENVIRONMENT,GITHUB_RUN_ID,ImageOS,ImageVersion,RUNNER_TEMP \
python3 scripts/qualify-native-linux.py --output "$RUNNER_TEMP/vmpower-native"
python3 scripts/linux-package-quarantine.py --output "$RUNNER_TEMP/vmpower-native" --legacy-dpkg
- name: Always reconcile owned fixture cleanup
if: always()
run: |
if [ -f scripts/qualify-native-linux.py ]; then
sudo --preserve-env=GITHUB_ACTIONS,RUNNER_ENVIRONMENT,GITHUB_RUN_ID,ImageOS,ImageVersion,RUNNER_TEMP \
python3 scripts/qualify-native-linux.py --cleanup --output "$RUNNER_TEMP/vmpower-native"
python3 scripts/linux-package-quarantine.py --cleanup --output "$RUNNER_TEMP/vmpower-native"
fi
- name: Sanitized qualification receipts only
if: always()
Expand All @@ -73,6 +76,18 @@ jobs:
with:
node-version: 24.22.1
registry-url: https://registry.npmjs.org
- name: Parse PowerShell and verify admitted existing Chocolatey
run: |
$ErrorActionPreference='Stop'
foreach ($file in (Get-ChildItem scripts,packages/win32/node24/tools -Filter '*.ps1' -Recurse -File)) {
$tokens=$null;$parseErrors=$null
$null=[System.Management.Automation.Language.Parser]::ParseFile($file.FullName,[ref]$tokens,[ref]$parseErrors)
if ($parseErrors.Count -ne 0) { throw ('PowerShell parse failed: '+$file.Name) }
}
# Official runner-images Windows2025 image 20261021.299.1, read 2026-10-26.
$chocolateyVersion=& (Join-Path $env:ChocolateyInstall 'bin\choco.exe') --version
if ($LASTEXITCODE -ne 0 -or ([string]$chocolateyVersion).Trim() -cne '2.7.4') { throw 'Existing Chocolatey differs from admitted 2.7.4; no installation or update allowed' }
& scripts/test-windows-package-cleanup.ps1
- name: Install exact public lock and compile
run: |
npm ci --legacy-peer-deps --ignore-scripts --no-audit --no-fund --registry=https://registry.npmjs.org
Expand All @@ -83,6 +98,10 @@ jobs:
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python scripts/test-native-preparation.py
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python scripts/test-build-windows-package.py
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python scripts/test-legacy-package-inputs.py
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
node scripts/test-native-report-fixture.cjs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
node scripts/test-native-fixture-paths.cjs
Expand All @@ -92,6 +111,8 @@ jobs:
& scripts/test-windows-bootstrap.ps1
python scripts/prepare-native-inputs.py --output "$env:RUNNER_TEMP\vmpower-native"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python scripts/prepare-legacy-package-inputs.py --output "$env:RUNNER_TEMP\vmpower-native"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Real native bundle CLI and installer argv ACL controls
run: |
$root=Join-Path $env:RUNNER_TEMP 'vmpower-native'
Expand All @@ -100,7 +121,7 @@ jobs:
$prepared=Get-Content -Raw "$root\evidence\prepared.json" | ConvertFrom-Json
& scripts/test-install-windows-bundle.ps1 -Bundle "$root\bundle" -ExpectedManifestSha256 $prepared.bundles.bundle -NssmExe "$root\nssm.exe"
- name: Isolated NSSM lifecycle and progress
run: '& scripts/qualify-native-windows.ps1 -Output "$env:RUNNER_TEMP\vmpower-native"'
run: '& scripts/qualify-native-windows.ps1 -Output "$env:RUNNER_TEMP\vmpower-native" -LegacyPackages "$env:RUNNER_TEMP\vmpower-native\legacy-packages" -ChocolateyVersion 2.7.4'
- name: Always reconcile owned fixture cleanup
if: always()
run: |
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ network. A real transport negative control verifies that namespace before
testing. Tests additionally deny Node transports and machine agent-config
reads; notification pipes, processes and provider calls remain synthetic.

Linux native fixtures check existing manager units, unit lookup paths and cgroups
before creating their owned slice. Do not use a named `systemctl show` query to
prove slice absence: systemd can load an implicit slice during that query.
Sanitized supervisor receipts distinguish the primary refusal from later cleanup;
a successful workflow step alone does not establish resource cleanup.

Drone's ordinary branch pushes, pull requests and `master` merges run only the
modern source-validation step. Every legacy packaging and Azure publishing
step requires both the `tag` event and a `refs/tags/v*` reference. A release tag
Expand Down
138 changes: 138 additions & 0 deletions distribution/linux-debian-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Linux Debian ownership and rollback candidate

This source prepares one package builder and the actual maintainer hooks for a
future release. It is not published or customer-qualified. The optional native
gate below has not run. The current public binaries remain unchanged.

The two cached public Linux packages are both `vmpower-agent` version `0.1`, with
different executable bytes. They own `/usr/bin/vmpower`,
`/etc/systemd/system/vmpower.service`, and `/etc/vmpower-template.conf`. Replacing
the first two files with the standalone bundle installer leaves their old dpkg
ownership intact. The hosted negative controls must demonstrate both old removal
and old reinstall replacing those modern entrypoints, for **both** exact packages:

| Input | Bytes | SHA256 |
| --- | ---: | --- |
| dashboard-linux.deb | 10794560 | 06bf0d767e4625db0477f3e23e30b4e36ac6e7a7d50882b3c655c472f45875b1 |
| docs-linux.deb | 11090764 | e8c0028ea78322e5937becba766654f178e4dfbe1c9817af33e024d93b588311 |

`scripts/build-linux-deb.py` uses a verified `linux-x64` bundle and an explicit
package version. The supported version series is `0.2.0`, optionally with a
prerelease suffix such as `0.2.0~rc1`; the hosted gate compares versions with real
dpkg. Application version `0.0.2` is deliberately not used as a Debian upgrade
over `0.1`. Choosing the final published version remains a release decision.

The resulting amd64 package owns the launcher, service unit, packaged bundle and
installer helpers. The launcher/unit bytes come from the same rendering functions
used by the existing Linux installer. The packaged bundle is installed by that
qualified installer into retained, immutable `/opt/vmpower-agent/releases` paths.
These retained rollback releases, `/etc/vmpower.conf`, the dedicated service
account and root-only transaction snapshots are not removed by package removal
or purge. They remain explicitly managed state; this is an intentional retention
policy, not a claim that dpkg owns every retained file.

The actual preinst accepts both pinned legacy binary families and fails closed
on an unknown legacy executable. It journals the recognized unit/launcher and service
state before dpkg unpacks replacement files. Existing JSON configuration bytes
are preserved. First installation retains the previous `API_KEY`, `API_SECRET`,
`VM_ID` environment contract and creates configuration exclusively as 0600.
Postinst restores the pre-unpack service files and calls the existing verified
installer, which creates/validates a dedicated nonroot service account, assigns
root:vmpower 0640 configuration access, preserves stopped/enabled state, checks a
stable child, and restores runtime state on activation failure. Prerm stops and
disables the service on removal; postrm reloads systemd. Upgrade hooks retain the
predecessor until the installer's transaction replaces it.

If postinst fails, a running predecessor is restored, but dpkg correctly reports
the candidate as **half-configured**. Do not call that a complete rollback.
Reinstall the retained, verified previous deb with `dpkg --install PREVIOUS.deb`
to restore the installed version and package database ownership. No hook runs a
nested dpkg operation under its lock. The gate tests both the immediate runtime
restoration and this explicit package rollback. Retain the prior exact deb and
its hash before a real upgrade; do not delete the snapshots or old public URLs.

## Bounded hosted gate proposal — not dispatched

Use only an explicitly admitted disposable Ubuntu 24 x64 runner with systemd and
the existing 15-minute Linux native ceiling. The integrated existing workflow
enables this extension on the explicit combined candidate branch and retains its
events, permissions and timeout. Root must admit the exact source and invocation
before any push; this source preparation has not dispatched CI.

1. Retain the existing source tests and native input preparation. Run
`python3 scripts/test-linux-deb.py` and the existing Linux installer tests.
Stage the same pinned Node24 bundle/upgrade/crashing inputs with the unchanged
preparation helper. `prepare-legacy-package-inputs.py` stages the two exact
archived public families above from their fixed HTTPS origins into
`$RUNNER_TEMP/vmpower-native/legacy-debs/` without installing or executing them.
It reuses the existing no-redirect downloader and verifies hashes and sizes;
the qualifier independently verifies both byte counts and SHA256 values.
2. Run the existing guarded native qualifier with its optional flag:
`sudo --preserve-env=GITHUB_ACTIONS,RUNNER_ENVIRONMENT,GITHUB_RUN_ID,ImageOS,ImageVersion,RUNNER_TEMP python3 scripts/qualify-native-linux.py --output "$RUNNER_TEMP/vmpower-native" --legacy-dpkg`.
It retains the original nine native phases and their network/cleanup controls.
It rejects pre-existing package registration, account/config/service or owned
package paths before reading any customer configuration.
3. Keep the existing service-user IPv4/IPv6 rules. Immediately before any actual
package install, `linux-package-quarantine.py` adds two journaled whole-runner
OUTPUT rejects for every nonloopback IPv4/IPv6 protocol and identity. Root and
service-user socket probes must reach owned loopback and receive explicit
external denial; the IPv4 reject counter must cover both probes. IPv6 rule
readback plus an explicit denial is retained even on a host without an IPv6
route. No package download or dependency resolution occurs under quarantine.
This temporarily disconnects the GitHub runner's control connection as well.
A separate-session watchdog must acknowledge readiness before rules are added.
After 420 seconds it terminates the qualifier's private process group (including
dpkg/maintainer descendants) before removing only the two whole-runner rules.
Existing service-UID rules still deny any systemd child until ordinary cleanup.
Deadline expiry is a failed run, never acceptance or an automatic retry.
Under this quarantine, install each exact
legacy deb with real dpkg and pre-existing synthetic config. Only its known
postinst executes; **no legacy Node10 executable starts**. Run both collision
controls. Build and install real `0.2.0~rc1`, `rc2`, `rc3` packages with the same
builder/hooks intended for release; use rc3's existing crashing Node24 fixture
for rollback. Verify actual dpkg ownership/version/checksums, unchanged config
bytes, root:vmpower 0640 access, service-user readability and ordinary-user
denial, nonroot stable startup and two reports, actual hook-driven upgrade,
half-configured failure plus explicit prior-deb rollback, and actual package
remove/reinstall/purge retention. Do not substitute mocked package operations.
4. Always invoke the existing `--cleanup` entrypoint. Its journal now includes
the exact fixture-owned package registration and additional fixed paths when
this option is used; the cleanup command needs no extra flag. It purges only
that registration **before dropping quarantine**, verifies absence, stops the
exact watchdog, removes owned state, stops all owned
processes/receivers, removes firewall rules and restores the original `/opt`
permissions. Upload sanitized lifecycle/cleanup JSON only; no packages,
configuration bytes, raw environments or snapshots.

Expected incremental gate work is two package-family matrices within the existing
15-minute Linux job. The two-report waits alone need about 60 seconds per family;
package compression, installation and rollback add time. This is an unmeasured
bound proposal; timeout or incomplete cleanup is failure, not permission to
increase resources or rerun automatically.

The prior candidate incorrectly treated service-UID denial as sufficient for
root package hooks. The package entrypoint now enforces whole-runner quarantine;
the watchdog is necessary because a blocked runner cannot receive its next
cleanup action. Offline tests cover the exact rule scope, process-group identity,
deadline kill-before-restore ordering and reused-process refusal. Hosted proof of
watchdog recovery, root/service denial, runner reconnection and package cleanup is
still required. Do not bypass this gate based on the old postinst's current
no-op behavior with existing synthetic configuration.

## Remaining release limits

Only modern runtime execution on the admitted Ubuntu 24 host can be established
by this gate. Active legacy Node10 runtime compatibility, other distributions,
older customer kernels/libc/systemd/Python, customer architectures, actual fleet
versions and custom unit/account configurations remain unqualified. The package
requires Python 3.9+, systemd and compatible native libraries; those dependencies
do not prove any untested host is supported. Unknown units and unsafe ownership
fail closed. Exact old binary families are retained; Windows packaging is outside
this change. Existing HTTPS/source/hash provenance is preserved; no new signing
or publisher-authentication claim is introduced. Root owns execution admission,
final release version, immutable package publication, rollback inventory and
consumer readback after the live-usage audit.

References: [Debian maintainer-script upgrade ordering](https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html),
[configuration handling](https://www.debian.org/doc/debian-policy/ap-pkg-conffiles.html),
and [dpkg version/ownership/verification behavior](https://manpages.debian.org/unstable/dpkg/dpkg.1.en.html).
Loading
Loading