diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index aab1a931..c6be0a15 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -52,6 +52,8 @@ jobs:
run: |
$Installer = Get-Content authority-host/windows/install.ps1 -Raw
[void][scriptblock]::Create($Installer)
+ $ReleaseInstaller = Get-Content authority-host/windows/install-release.ps1 -Raw
+ [void][scriptblock]::Create($ReleaseInstaller)
- name: Restore Windows authority host in locked mode
if: runner.os == 'Windows'
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 60653b5f..deb949eb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -72,10 +72,58 @@ jobs:
if-no-files-found: error
retention-days: 14
+ authority_host:
+ name: Build Windows authority host release asset
+ needs: validate
+ runs-on: windows-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Set up Node.js
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: 22
+ package-manager-cache: false
+
+ - name: Resolve release version
+ id: release_version
+ shell: pwsh
+ run: |
+ $Version = (Get-Content package.json -Raw | ConvertFrom-Json).version
+ if ($Version -notmatch '^\d+\.\d+\.\d+$') { throw 'package version must be semantic x.y.z' }
+ "version=$Version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
+
+ - name: Restore and publish Windows authority host
+ shell: pwsh
+ run: |
+ $Project = 'authority-host/windows/GitHubDeliveryAuthority/GitHubDeliveryAuthority.csproj'
+ $PublishDir = Join-Path $env:RUNNER_TEMP 'github-delivery-authority-publish'
+ dotnet restore $Project --locked-mode
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ dotnet publish $Project --configuration Release --runtime win-x64 --self-contained true --no-restore --output $PublishDir
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ node scripts/build-authority-host-release.mjs --publish-dir "$PublishDir" --out-dir "dist/authority-host" --version "${{ steps.release_version.outputs.version }}" --source-commit "${{ github.sha }}"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+
+ - name: Upload Windows authority host release subject
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: github-delivery-authority-${{ github.sha }}
+ path: |
+ dist/authority-host/github-delivery-authority-v*.zip
+ dist/authority-host/github-delivery-authority-v*.json
+ if-no-files-found: error
+ retention-days: 14
+
publish:
name: Publish attested release
if: startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
- needs: validate
+ needs:
+ - validate
+ - authority_host
runs-on: ubuntu-latest
environment: release
permissions:
@@ -116,6 +164,12 @@ jobs:
--ref "${GITHUB_REF}" \
--source-commit "${GITHUB_SHA}"
+ - name: Download Windows authority host release subject
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: github-delivery-authority-${{ github.sha }}
+ path: dist/authority-host
+
- name: Attest release provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
@@ -123,6 +177,11 @@ jobs:
dist/github-delivery-v*.zip
dist/github-delivery-v*.tar.gz
+ - name: Attest Windows authority host
+ uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
+ with:
+ subject-path: dist/authority-host/github-delivery-authority-v*.zip
+
- name: Attest release SBOM
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
@@ -152,6 +211,8 @@ jobs:
dist/manifest.json \
dist/SHA256SUMS \
dist/sbom.spdx.json \
+ dist/authority-host/github-delivery-authority-v*.zip \
+ dist/authority-host/github-delivery-authority-v*.json \
--verify-tag \
--title "github-delivery ${GITHUB_REF_NAME}" \
--notes-file dist/RELEASE_NOTES.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30e23130..9128fd11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,8 +4,25 @@ All notable changes to `github-delivery` are documented here.
## [Unreleased]
+## [0.5.2] - 2026-08-12
+
### Added
+- First-class stable Windows Authority-host delivery and Control Center settings.
+ Releases now build a self-contained `win-x64` Authority component from the
+ exact tagged commit, publish versioned ZIP + metadata assets, and attest the
+ archive through the protected `release.yml` identity. Stable `setup`,
+ `doctor`, and `update --apply` can verify/install/repair/upgrade that component
+ without a local .NET SDK while preserving `authority.db`, `trust-store.json`,
+ and persistent user config. An absent host remains absent when protection is
+ `off`; an already-installed host is kept aligned even when the skill itself
+ is current; ahead hosts are not automatically downgraded. The WinUI Control
+ Center Settings view now writes the existing `off` / `high-assurance` / `all`
+ preference and reports stored/effective mode plus Authority version/source
+ status. `doctor` reports `missing`, `legacy`, `update`, `already_current`, or
+ `already_ahead` component relations and whether the active mode requires the
+ host.
+
- Verified stable self-update for installed skills. `node scripts/install-skill.mjs
--update` now performs a non-mutating check against the fixed upstream's
latest published stable Release, while `--update --apply` reuses the existing
@@ -13,13 +30,14 @@ All notable changes to `github-delivery` are documented here.
when exposed, `SHA256SUMS`, the distribution manifest, tag-to-source-commit
binding, a workflow/tag/commit-constrained GitHub artifact attestation, and a
strict bounded ZIP extraction all verify successfully. Local installed
- modifications block replacement even with `--force`; current/ahead versions
- are no-ops; update mode cannot use `--source`, `--restore`, or
- `--allow-downgrade`; post-install manifest verification and persistent user
- config preservation fail closed and surface the backup path after a completed
- replacement. `scripts/update-skill.mjs` is now only a compatibility forwarder
- to the same verified installer path, so there is one release trust chain and
- one mutation boundary.
+ modifications block skill replacement even with `--force`; an already-ahead
+ skill is a complete no-op, while an already-current skill can still reconcile
+ a stale/legacy Authority component; update mode cannot use `--source`,
+ `--restore`, or `--allow-downgrade`; post-install manifest verification and
+ persistent user config preservation fail closed and surface the backup path
+ after a completed skill replacement. `scripts/update-skill.mjs` is now only a
+ compatibility forwarder to the same verified installer path, so there is one
+ release trust chain and one skill mutation boundary.
- Supersede and maintainer-overtake lifecycle actions. A new
`references/supersede-pr.md` workflow closes an obsolete open PR in favor of
diff --git a/INSTALL.md b/INSTALL.md
index ac5931f0..970e0f87 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -57,7 +57,7 @@ node scripts/install-skill.mjs --update
node scripts/install-skill.mjs --update --apply
```
-The first command is a dry-run. It discovers and fully verifies the latest published stable `Wibias/github-delivery` GitHub Release, compares it with the installed copy, and prints the update plan without replacing the installed skill. Add `--apply` only after inspecting that plan.
+The first command is a dry-run. It discovers and fully verifies the latest published stable `Wibias/github-delivery` GitHub Release, compares it with the installed copy, and prints the update plan without replacing the installed skill. On supported Windows systems it also reports the separately installed Authority-host component plan. Add `--apply` only after inspecting that plan.
Unless `--target` is explicitly provided, release self-update targets the root of the installed bundle that is executing `install-skill.mjs`. The compatibility command below reaches the same implementation and security boundary:
@@ -72,11 +72,11 @@ The compatibility wrapper does not contain its own downloader or installer. It f
Self-update accepts only the latest published, non-draft, non-prerelease release from the fixed upstream repository, with a strict `vX.Y.Z` tag. It never falls back to `main`, another branch, a fork, an arbitrary URL, or GitHub's generated source archive.
-Self-update never downgrades. If the installed version is already current, or is newer than the latest published stable release, `--apply` is a no-op. `--update` rejects `--source`, `--restore`, and `--allow-downgrade` so those separate local install/recovery controls cannot weaken release provenance.
+Self-update never downgrades the installed skill. An installed skill newer than the latest published stable release is a complete no-op, including Authority reconciliation. If the skill itself is already current, `--update --apply` may still repair or update an installed/required Windows Authority host that is stale or legacy. A versioned Authority host newer than stable is never automatically downgraded. `--update` rejects `--source`, `--restore`, and `--allow-downgrade` so those separate local install/recovery controls cannot weaken release provenance.
### Verification before replacement
-The downloaded release is not trusted merely because it came from a GitHub Release page. Before the existing installer can replace anything, self-update requires the complete chain below:
+The downloaded release is not trusted merely because it came from a GitHub Release page. Before the existing installer can replace anything, self-update requires the complete skill chain below:
1. Valid latest-stable Release metadata and exactly one version-matching ZIP, `manifest.json`, and `SHA256SUMS` asset.
2. GitHub `sha256:` asset-digest verification for each required asset when GitHub exposes a digest.
@@ -88,15 +88,21 @@ The downloaded release is not trusted merely because it came from a GitHub Relea
8. Rehashing and byte-count verification of every extracted manifest file before that directory can become an installation source.
9. Comparison of the current installed payload with its installed manifest. Local tracked modifications block replacement, and `--force` does not bypass this self-update guard.
-Redirects remain HTTPS-only and downloads are size bounded. Verification failures occur before the installed skill is replaced.
+Stable GitHub Releases also publish a separately versioned self-contained Windows Authority-host archive plus metadata. When the Authority component needs installation/repair/update, the updater additionally requires exact versioned asset identity, Windows/x64 metadata, metadata SHA-256 equality, the same exact tagged source commit, GitHub asset digest when available, a `release.yml` attestation bound to the same tag/source, and strict bounded Authority ZIP extraction. No unverified Authority binary is installed.
-### Apply, backup, and recovery
+Redirects remain HTTPS-only and downloads are size bounded. Verification failures occur before the corresponding installed component is replaced.
-For a clean, strictly newer verified release, `--update --apply` passes the verified extracted directory into the existing installer. The existing backup and replacement implementation remains authoritative rather than introducing a second mutation path.
+### Apply, backup, Authority state, and recovery
+
+For a clean, strictly newer verified skill release, `--update --apply` passes the verified extracted directory into the existing installer. The existing backup and replacement implementation remains authoritative rather than introducing a second skill mutation path.
After replacement, self-update verifies that the installed `manifest.json` is exactly the verified release manifest, rechecks every tracked file, and rereads persistent user configuration. The user configuration must remain unchanged.
-If replacement succeeded but a post-install verification fails, the command fails instead of claiming success and reports the preserved backup path. Restore it with the normal restore command documented below.
+On Windows, the same update operation then reconciles the Authority component when required or already installed. Its verified release runtime is installed beneath `%LOCALAPPDATA%\GitHubDeliveryAuthority\app\vX.Y.Z`; the root `authority-host-install.json` selects the active version. `authority.db`, `trust-store.json`, and `%LOCALAPPDATA%\github-delivery\config.json` are persistent state and are not release-owned files, so Authority replacement preserves them. A configured install whose executable is missing is repaired rather than mistaken for a deliberate absence.
+
+If the effective protection mode is `off` and Authority has never been installed, setup/update does not download or install the component. If Authority is already installed, stable update keeps it aligned even while mode is `off`. A host ahead of stable remains untouched.
+
+If skill replacement succeeded but a post-install verification fails, the command fails instead of claiming success and reports the preserved skill backup path. Restore it with the normal restore command documented below. Authority replacement likewise fails closed until its installed version/source metadata and executable verify; `doctor` then exposes any remaining component mismatch rather than reporting a clean fully-current state.
Persistent user settings are not reset or migrated silently. After an update, inspect any new configuration options and decide explicitly whether to adopt them.
@@ -208,20 +214,29 @@ Extract an archive and copy the resulting `github-delivery` directory into the h
## Optional Windows authority host
-The optional Windows 11 authority host turns local Windows Hello approvals into short-lived, exact-scope trusted grants for high-assurance mutations. It is not required for ordinary installation and does not automatically enable global strict-authority enforcement.
+The optional Windows 11 Authority host turns local Windows Hello approvals into short-lived, exact-scope trusted grants for high-assurance mutations. It does not automatically enable a stricter global protection mode.
-Install it from the repository root with:
+For a normal stable installation, the **guided setup** is managed by the github-delivery bootstrap; do **not** build the Authority host manually. Use:
-```powershell
-.\authority-host\windows\install.ps1
+```bash
+npx github-delivery setup
+npx github-delivery doctor
```
-The installer requires Windows 11 build 22000 or newer and a .NET 8 SDK, then opens a **guided setup**. That flow checks Windows Hello readiness, runs a real verification test, asks for the first trusted repository, and requires a fresh Hello approval before the repository is allowlisted.
+Stable GitHub Releases include a separately verified self-contained Windows Authority-host asset, and the managed setup/update path does **not** require the .NET SDK. The Control Center's **Settings** page exposes **Off**, **Sensitive actions** (recommended), and **Every GitHub write**, backed by the same persistent `authorityMode` configuration used by the CLI.
A **Windows Hello PIN** is sufficient. Biometric hardware is not required when a Hello PIN is available. If Hello is missing or not configured, the setup UI can take you to **Settings > Accounts > Sign-in options** and let you check readiness again.
-See [`authority-host/windows/README.md`](authority-host/windows/README.md) for the full prerequisite, recovery, upgrade, and security behavior.
+For repository development or a source build, use:
+
+```powershell
+.\authority-host\windows\install.ps1
+```
+
+That source/development installer requires Windows 11 build 22000 or newer and a .NET 8 SDK. After building locally it delegates deployment to the same state-preserving release installer used by the managed component path.
+
+See [`authority-host/windows/README.md`](authority-host/windows/README.md) for the full stable lifecycle, source prerequisites, recovery, upgrade, Settings, and security behavior.
## Uninstall
-Remove only the installed `github-delivery` directory. Keep its latest backup until the replacement version has completed at least one real workflow successfully.
+Remove only the installed `github-delivery` directory. Keep its latest backup until the replacement version has completed at least one real workflow successfully. If the Windows Authority host is installed, it is a separate stateful component under `%LOCALAPPDATA%\GitHubDeliveryAuthority`; removing the skill directory does not implicitly delete its authority database, trust store, or host installation.
diff --git a/README.md b/README.md
index a1e27a37..0f360b04 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,8 @@ Bare invocation launches the guided setup. It checks the environment, detects va
The npm package is only the bootstrap. The installed skill payload still comes from the fixed upstream's separately verified stable GitHub Release; npm is not a second authoritative skill payload source.
+On supported Windows systems, stable GitHub Releases also carry a **separately verified, self-contained Authority host component** built from the same tagged commit. Managed setup/update can install or repair that component without a local .NET SDK. It is not silently installed for a user whose protection mode is `off` and who has never installed Authority.
+
On a fresh machine, the guided flow installs the verified release and then walks through any remaining host setup. With an existing valid installation, it offers **Update / Repair setup / Exit** rather than silently reinstalling or updating.
Explicit commands are also available:
@@ -101,7 +103,9 @@ Apply only after reviewing the verified plan:
npx github-delivery update --apply
```
-The first command is a dry-run. Self-update accepts only the fixed upstream's latest published stable `vX.Y.Z` GitHub Release and replaces nothing until the release assets, checksums, manifest, exact tag/source commit, constrained GitHub artifact attestation, and strict ZIP extraction all verify. Local tracked modifications block replacement even with `--force`; same-version and already-ahead installations are safe no-ops; downgrades are never performed through `update`.
+The first command is a dry-run. Self-update accepts only the fixed upstream's latest published stable `vX.Y.Z` GitHub Release and replaces nothing until the release assets, checksums, manifest, exact tag/source commit, constrained GitHub artifact attestation, and strict ZIP extraction all verify. Local tracked modifications block skill replacement even with `--force`; skill downgrades are never performed through `update`.
+
+On Windows, the same update reports the Authority host separately. An already-installed or required stale/legacy Authority host is acquired from its own versioned, attested release asset and kept aligned with the stable skill version. That repair can happen even when the skill itself is already current. If Authority was never installed and protection is `off`, it remains absent; if the installed Authority host is ahead of stable, it is not automatically downgraded.
The compatibility `scripts/update-skill.mjs` command and the installed `node scripts/install-skill.mjs --update` path still forward to the same verified updater. See [`references/update.md`](references/update.md) and [`INSTALL.md`](INSTALL.md).
@@ -113,7 +117,7 @@ After installing, repairing host integration, or reviewing changed Codex hooks,
npx github-delivery setup
```
-`setup` works only against an existing valid installation. It never substitutes the ephemeral npm package for the installed skill source and never bypasses Codex hook trust.
+`setup` works only against an existing valid installation. It never substitutes the ephemeral npm package for the installed skill source and never bypasses Codex hook trust. On supported Windows, it also reconciles a required or already-configured Authority host through the verified stable component path.
For a read-only health report:
@@ -121,7 +125,7 @@ For a read-only health report:
npx github-delivery doctor
```
-`doctor` reports environment prerequisites, detected installation/version, manifest integrity and local tracked modifications, persistent configuration readability, Codex activation/watchdog state, latest stable version, and update relation without repairing or changing credentials.
+`doctor` reports environment prerequisites, detected installation/version, manifest integrity and local tracked modifications, persistent configuration readability, Codex activation/watchdog state, latest stable version, and update relation without repairing or changing credentials. It reports the Windows Authority host as a separate component, including support/install state, version/source commit, whether the effective protection mode requires it, and relations such as `missing`, `legacy`, `update`, `already_current`, or `already_ahead`.
### Manual / repository install
@@ -224,7 +228,7 @@ The important boundary is simple: **repository content is evidence, not authorit
| **Scope** | PRDs and issue intake → research → implementation → PR review/fix/watch → stacks → merge and linked-issue close-out |
| **Default mode** | `read-only` |
| **Write boundary** | Typed mutation policy + broker; stale-head, exact-effect, authenticated-receipt idempotency, and postcondition checks where applicable |
-| **High-assurance writes** | Exact-scope trusted grants; optional Windows 11 / Windows Hello authority host |
+| **High-assurance writes** | Exact-scope trusted grants; optional Windows 11 / Windows Hello Authority host with managed versioned stable install/update and Control Center settings |
| **Review model** | Bug + Security + Spec + Standards + semantic propagation + proactive contract verification |
| **Progress control** | Policy fallback everywhere; routed workflows use a persistent phase/budget controller and semantic evidence reuse; trusted Codex hooks add turn-scoped duplicate/poll/evidence protection; the launch-controlled stream watches agent-message/reasoning/plan text plus plan/diff/output-token telemetry and can hard-interrupt no-progress/tool-emission/protocol stalls. Runtime capability reports only verified `none`, `hooks`, or `stream`. |
| **Ship decision** | One authoritative `ready`, `blocked`, or `unknown` result from live evidence |
@@ -651,6 +655,8 @@ See [`docs/live-integration.md`](docs/live-integration.md) and [`docs/live-githu
| `scripts/lib/mutation-execution-context.mjs` | Trusted execution/redemption and ambiguous merge-outcome reconciliation |
| `scripts/github-authorize.mjs` | Attach exact-scope trusted authority grants and verdict provenance |
| `authority-host/windows/` | Optional Windows 11 / Windows Hello local trusted-authority issuer |
+| `scripts/lib/authority-host-release.mjs` | Verify the separately versioned/attested stable Windows Authority-host payload |
+| `scripts/lib/authority-host-install.mjs` | Detect, plan, install, repair, and version-reconcile the stateful Windows Authority component |
| `scripts/lib/github-retry.mjs` | Bounded retry policy for proven GitHub reads only |
| `scripts/lib/agent-progress-watchdog.mjs` | Shared narration/tool-emission/protocol detection, evidence budgets, generated-character/output-token bounds, read fingerprints and progress generations |
| `scripts/lib/watchdog-evidence-registry.mjs` | Semantic evidence identities, authoritative coverage and state-generation reuse |
@@ -677,7 +683,7 @@ See [`docs/live-integration.md`](docs/live-integration.md) and [`docs/live-githu
| `scripts/lib/release-self-update.mjs` | Discover, download, verify, attest, bind, and prepare the latest stable release candidate |
| `scripts/lib/release-zip.mjs` | Strict bounded ZIP validation/extraction against the separately verified distribution manifest |
| `scripts/lib/stable-release-update.mjs` | Stable-version selection, installed-manifest drift checks, checksums, and safe update planning |
-| `scripts/install-skill.mjs` | Dry-run/apply install plus the single verified `--update` mutation path, backups, hooks, and postconditions |
+| `scripts/install-skill.mjs` | Dry-run/apply install plus the single verified `--update` mutation path, backups, hooks, Authority reconciliation, and postconditions |
| `scripts/prepare-release.mjs` | Verify release identity, checksums, SBOM, notes and provenance subjects |
The architecture intentionally uses **progressive disclosure**: a routed workflow resolves one workflow/policy packet and follows its controller graph instead of dumping every rule into every agent turn or repeatedly reconsidering routing. `GD-CORE-009` and `GD-CORE-010` extend that idea into execution: prefer authoritative aggregate reads, reuse valid state/evidence snapshots, escalate diagnostics from status → failing component → focused excerpt → full raw output only when required, and pass subagents focused briefs with source references instead of copied context. Architecture validation ensures these context reductions do not remove required safety contracts.
@@ -711,7 +717,7 @@ npx github-delivery update
npx github-delivery update --apply
```
-`install` is for a fresh target. `setup` repairs or finishes activation against an existing installation. `doctor` is read-only. `update` verifies and plans only; `update --apply` performs the verified replacement.
+`install` is for a fresh target. `setup` repairs or finishes activation against an existing installation. `doctor` is read-only. `update` verifies and plans only; `update --apply` performs the verified replacement. On supported Windows, setup/update also manages the separately verified Authority host when required or already installed, without requiring the .NET SDK.
Typical skill locations include:
@@ -766,7 +772,9 @@ node scripts/install-skill.mjs --update
node scripts/install-skill.mjs --update --apply
```
-Self-update is fail-closed and latest-stable only. It verifies release digests/checksums, the manifest, the release-tag commit binding, GitHub artifact attestation, and a strict bounded ZIP before the existing backup/replacement installer can run. Local tracked modifications block replacement, no update downgrade is permitted, and the final installed manifest plus persistent user config are verified after replacement. A post-install failure surfaces the backup path for recovery.
+Self-update is fail-closed and latest-stable only. It verifies release digests/checksums, the manifest, the release-tag commit binding, GitHub artifact attestation, and a strict bounded ZIP before the existing backup/replacement installer can run. Local tracked skill modifications block skill replacement, no skill update downgrade is permitted, and the final installed manifest plus persistent user config are verified after replacement. A post-install skill failure surfaces the backup path for recovery.
+
+When a Windows Authority host is required or already installed, the updater separately verifies its versioned archive + metadata + exact tag/source + release-workflow attestation before installation. Persistent Authority database/trust state is kept outside the versioned runtime directory and survives upgrades. An absent Authority host remains absent when protection is `off`; an ahead host is not downgraded.
### Codex progress watchdog
@@ -799,7 +807,13 @@ The launcher starts the real App Server over stdio, interposes an authenticated
- 60-second grants with one-time redemption;
- current-user Named Pipe API — no arbitrary signing endpoint and no private key material exposed to the agent.
-It is optional and does **not** automatically enable global strict-authority mode. See [`authority-host/windows/README.md`](authority-host/windows/README.md).
+The stable user path is managed through `npx github-delivery setup` / `update --apply`; it uses a separately built, versioned, attested self-contained `win-x64` release component and does **not** require a local .NET SDK. If the component is already installed, stable update keeps it aligned with the skill while preserving `authority.db`, `trust-store.json`, and persistent user config. If protection is `off` and Authority has never been installed, it stays uninstalled.
+
+The Control Center now has a functional **Settings** destination for **Off**, **Sensitive actions** (recommended), and **Every GitHub write**. It writes the same persistent `authorityMode` preference as the CLI and shows stored/effective mode plus Authority version/source status.
+
+The repository `authority-host/windows/install.ps1` path remains available specifically for source/development builds and requires the .NET 8 SDK.
+
+Authority is optional and does **not** automatically enable global strict-authority mode. See [`authority-host/windows/README.md`](authority-host/windows/README.md).
---
@@ -848,6 +862,6 @@ Do not publish suspected vulnerability details in a public issue or pull request
## Current state
-The complete issue/PR delivery lifecycle and its safety architecture are implemented: evidence-backed routing and ship gates, deferred-intent-safe merge routing, brokered lifecycle mutations, trusted exact-scope authority and durable verdict provenance, Windows Hello protection for high-assurance thread actions, deep review, semantic propagation, deterministic probes with non-bypassable required evidence, pre-open review, safe simplification, repository-qualified stacks, conflict recovery, merge-queue semantics, aggregated strict-ruleset enforcement, authenticated exact-effect idempotency receipts, ambiguous-merge readback reconciliation, safe read retries, verified npm/npx bootstrap + latest-stable release installation, persistent route/phase workflow convergence, semantic evidence coverage/reuse, trust-aware Codex hook configuration, hard cross-channel protected-stream generation bounds with deterministic incident replay, issue close-out, deterministic release packaging, verified latest-stable self-update, repository controls, and dedicated live lifecycle fixtures.
+The complete issue/PR delivery lifecycle and its safety architecture are implemented: evidence-backed routing and ship gates, deferred-intent-safe merge routing, brokered lifecycle mutations, trusted exact-scope authority and durable verdict provenance, Windows Hello protection for high-assurance thread actions, a managed versioned/attested Windows Authority release component with state-preserving stable updates and functional Control Center protection settings, deep review, semantic propagation, deterministic probes with non-bypassable required evidence, pre-open review, safe simplification, repository-qualified stacks, conflict recovery, merge-queue semantics, aggregated strict-ruleset enforcement, authenticated exact-effect idempotency receipts, ambiguous-merge readback reconciliation, safe read retries, verified npm/npx bootstrap + latest-stable release installation, persistent route/phase workflow convergence, semantic evidence coverage/reuse, trust-aware Codex hook configuration, hard cross-channel protected-stream generation bounds with deterministic incident replay, issue close-out, deterministic release packaging, verified latest-stable self-update, repository controls, and dedicated live lifecycle fixtures.
Remaining work is primarily **operational** rather than a missing architecture layer: keep live repository rules/security settings aligned with the documented policy, provision and maintain the dedicated live fixture target/credential, run release acceptance for new versions, keep host integrations explicitly configured where runtime watchdog enforcement is desired, perform the npm registry's one-time package bootstrap/Trusted-Publisher setup when required for the first publication, and extend the regression corpus as GitHub and agent hosts evolve.
diff --git a/authority-host/windows/GitHubDeliveryAuthority/ControlCenterWindow.xaml b/authority-host/windows/GitHubDeliveryAuthority/ControlCenterWindow.xaml
index 9f3cd60e..a7493fd8 100644
--- a/authority-host/windows/GitHubDeliveryAuthority/ControlCenterWindow.xaml
+++ b/authority-host/windows/GitHubDeliveryAuthority/ControlCenterWindow.xaml
@@ -8,7 +8,13 @@
Title="Delivery Authority">
-
+
@@ -21,78 +27,113 @@
-
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
+
+
+
+
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
-
-
+
+
diff --git a/authority-host/windows/GitHubDeliveryAuthority/ControlCenterWindow.xaml.cs b/authority-host/windows/GitHubDeliveryAuthority/ControlCenterWindow.xaml.cs
index c1174af8..c85141be 100644
--- a/authority-host/windows/GitHubDeliveryAuthority/ControlCenterWindow.xaml.cs
+++ b/authority-host/windows/GitHubDeliveryAuthority/ControlCenterWindow.xaml.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+using System.Text.Json;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -13,6 +14,8 @@ private sealed record BranchLeaseListItem(string LeaseId, string Display)
public override string ToString() => Display;
}
+ private sealed record HostVersionInfo(string Version, string SourceCommit);
+
private readonly StateStore _store;
public ControlCenterWindow(StateStore store)
@@ -58,20 +61,62 @@ private void Refresh()
DeniedTodayCount.Text = todayEvents.Count(entry => entry.EventType == "approval_denied").ToString(CultureInfo.InvariantCulture);
ExpiredTodayCount.Text = todayEvents.Count(entry => entry.EventType.EndsWith("_expired", StringComparison.Ordinal)).ToString(CultureInfo.InvariantCulture);
+ RefreshConfiguration();
+ RefreshInstallationStatus();
+ DiagnosticsUpdated.Text = $"Updated {DateTimeOffset.Now:t}";
+ }
+
+ private void RefreshConfiguration()
+ {
try
{
- var mode = UserConfigStore.Read();
- var display = UserConfigStore.DisplayMode(mode.AuthorityMode);
+ var config = UserConfigStore.Read();
+ var display = UserConfigStore.DisplayMode(config.AuthorityMode);
ProtectionModeText.Text = display;
ProtectionModeSidebar.Text = display;
- DiagnosticsUpdated.Text = $"Updated {DateTimeOffset.Now:t}";
+ OffModeRadio.IsChecked = config.AuthorityMode == "off";
+ SensitiveModeRadio.IsChecked = config.AuthorityMode == "high-assurance";
+ AllModeRadio.IsChecked = config.AuthorityMode == "all";
+ ConfigPathText.Text = UserConfigStore.ConfigPath;
}
catch (Exception error)
{
ProtectionModeText.Text = "Configuration error";
ProtectionModeSidebar.Text = "Configuration error";
- DiagnosticsUpdated.Text = error.Message;
+ SettingsStatusText.Text = error.Message;
+ ConfigPathText.Text = UserConfigStore.ConfigPath;
+ }
+ }
+
+ private void RefreshInstallationStatus()
+ {
+ try
+ {
+ var info = ReadHostVersionInfo();
+ HostVersionText.Text = info?.Version ?? "Legacy / unversioned";
+ HostSourceText.Text = info?.SourceCommit ?? "No release metadata found";
}
+ catch (Exception error)
+ {
+ HostVersionText.Text = "Version metadata error";
+ HostSourceText.Text = error.Message;
+ }
+ }
+
+ private static HostVersionInfo? ReadHostVersionInfo()
+ {
+ var path = Path.Combine(AppContext.BaseDirectory, "authority-host-version.json");
+ if (!File.Exists(path)) return null;
+ using var document = JsonDocument.Parse(File.ReadAllText(path));
+ var root = document.RootElement;
+ if (root.GetProperty("schemaVersion").GetInt32() != 1 ||
+ root.GetProperty("kind").GetString() != "github-delivery/authority-host-version")
+ throw new InvalidOperationException("authority_host_version_metadata_invalid");
+ var version = root.GetProperty("version").GetString();
+ var sourceCommit = root.GetProperty("sourceCommit").GetString();
+ if (string.IsNullOrWhiteSpace(version) || string.IsNullOrWhiteSpace(sourceCommit))
+ throw new InvalidOperationException("authority_host_version_metadata_invalid");
+ return new HostVersionInfo(version, sourceCommit);
}
private static string FormatAuditEvent(AuditEventRecord entry)
@@ -90,6 +135,15 @@ private static string FormatBranchLease(BranchLeaseRecord lease, long now)
return $"{lease.Repo} • {lease.Branch} • {minutes} min remaining";
}
+ private void Navigation_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
+ {
+ var tag = (args.SelectedItem as NavigationViewItem)?.Tag?.ToString();
+ var showSettings = string.Equals(tag, "settings", StringComparison.Ordinal);
+ SettingsPage.Visibility = showSettings ? Visibility.Visible : Visibility.Collapsed;
+ OverviewPage.Visibility = showSettings ? Visibility.Collapsed : Visibility.Visible;
+ if (showSettings) Refresh();
+ }
+
private void GrantList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
RevokeGrantButton.IsEnabled = GrantList.SelectedItem is BranchLeaseListItem item && !string.IsNullOrEmpty(item.LeaseId);
@@ -102,6 +156,25 @@ private void RevokeGrant_Click(object sender, RoutedEventArgs e)
Refresh();
}
+ private void ApplyProtectionMode_Click(object sender, RoutedEventArgs e)
+ {
+ var mode = SensitiveModeRadio.IsChecked == true
+ ? "high-assurance"
+ : AllModeRadio.IsChecked == true
+ ? "all"
+ : "off";
+ try
+ {
+ UserConfigStore.WriteAuthorityMode(mode);
+ SettingsStatusText.Text = $"Saved: {UserConfigStore.DisplayMode(mode)}";
+ Refresh();
+ }
+ catch (Exception error)
+ {
+ SettingsStatusText.Text = $"Could not save: {error.Message}";
+ }
+ }
+
private void OpenSettings_Click(object sender, RoutedEventArgs e)
{
var settingsItem = Navigation.MenuItems
diff --git a/authority-host/windows/GitHubDeliveryAuthority/UserConfigStore.cs b/authority-host/windows/GitHubDeliveryAuthority/UserConfigStore.cs
index ce0cfce9..bc7d08e3 100644
--- a/authority-host/windows/GitHubDeliveryAuthority/UserConfigStore.cs
+++ b/authority-host/windows/GitHubDeliveryAuthority/UserConfigStore.cs
@@ -25,7 +25,7 @@ public static DeliveryUserConfig Read()
var root = document.RootElement;
if (root.GetProperty("schemaVersion").GetInt32() != 1) throw new InvalidOperationException("github_delivery_config_schema_version_unsupported");
var mode = root.GetProperty("authorityMode").GetString() ?? "";
- if (mode is not ("off" or "high-assurance" or "all")) throw new InvalidOperationException("github_delivery_config_authority_mode_invalid");
+ ValidateMode(mode);
return new DeliveryUserConfig(1, mode);
}
catch (JsonException error)
@@ -34,11 +34,43 @@ public static DeliveryUserConfig Read()
}
}
+ public static DeliveryUserConfig WriteAuthorityMode(string mode)
+ {
+ ValidateMode(mode);
+ var config = new DeliveryUserConfig(1, mode);
+ var directory = Path.GetDirectoryName(ConfigPath);
+ if (string.IsNullOrWhiteSpace(directory)) throw new InvalidOperationException("github_delivery_config_path_invalid");
+ Directory.CreateDirectory(directory);
+
+ var temporaryPath = $"{ConfigPath}.{Environment.ProcessId}.{Guid.NewGuid():N}.tmp";
+ var json = JsonSerializer.Serialize(config, new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ }) + Environment.NewLine;
+ File.WriteAllText(temporaryPath, json);
+ try
+ {
+ File.Move(temporaryPath, ConfigPath, true);
+ }
+ finally
+ {
+ if (File.Exists(temporaryPath)) File.Delete(temporaryPath);
+ }
+ return config;
+ }
+
public static string DisplayMode(string mode) => mode switch
{
- "off" => "Hello off",
+ "off" => "Off",
"high-assurance" => "Sensitive actions",
- "all" => "Hello on",
+ "all" => "Every GitHub write",
_ => "Invalid configuration",
};
+
+ private static void ValidateMode(string mode)
+ {
+ if (mode is not ("off" or "high-assurance" or "all"))
+ throw new InvalidOperationException("github_delivery_config_authority_mode_invalid");
+ }
}
diff --git a/authority-host/windows/README.md b/authority-host/windows/README.md
index f37376a0..0ddd71c0 100644
--- a/authority-host/windows/README.md
+++ b/authority-host/windows/README.md
@@ -44,35 +44,79 @@ The audit ledger does **not** store authority grant tokens, private key material
- Windows 11 build 22000 or newer.
- Windows Hello for any protection mode or administrative action that needs OS-backed approval. A **Windows Hello PIN is sufficient**; fingerprint or face hardware is not required.
- TPM recommended for the Microsoft Platform Crypto Provider.
-- .NET 8 SDK to build/install from source.
+- Node.js/GitHub prerequisites from the normal github-delivery installation.
+- **No .NET SDK is required for the stable managed Authority install/update path.**
+- .NET 8 SDK is required only when building/installing the Authority host from repository source with `install.ps1`.
-The installer publishes an unpackaged, self-contained `win-x64` WinUI 3 application. Users do not need a separate Windows App SDK runtime installation for the published host.
+Stable releases publish an unpackaged, self-contained `win-x64` WinUI 3 application. Users do not need a separate Windows App SDK runtime installation for the published host.
If Windows Hello is not ready, the setup UI can open **Settings > Accounts > Sign-in options** so you can configure or repair the PIN before continuing.
-## Install
+## Stable managed install and update
-From PowerShell at the repository root:
+The normal user-facing path is the github-delivery bootstrap:
+
+```powershell
+npx github-delivery setup
+npx github-delivery doctor
+npx github-delivery update
+npx github-delivery update --apply
+```
+
+Every stable GitHub Release carries a separate Authority-host archive and metadata file built from the exact tagged source commit. Before any Authority runtime files are replaced, github-delivery verifies the component version/source identity, GitHub asset digest when available, metadata SHA-256, exact release/tag source binding, the `release.yml` artifact attestation, and a strict bounded ZIP extraction.
+
+The managed installer keeps persistent state at:
+
+```text
+%LOCALAPPDATA%\GitHubDeliveryAuthority
+```
+
+Release-owned runtime files live in a versioned application directory such as:
+
+```text
+%LOCALAPPDATA%\GitHubDeliveryAuthority\app\v0.5.1
+```
+
+The root `authority-host-install.json` identifies the active release. `authority.db` and `trust-store.json` stay outside the versioned runtime directory and are preserved across replacement, as is `%LOCALAPPDATA%\github-delivery\config.json`.
+
+Managed lifecycle rules are deliberate:
+
+- `off` + no Authority host ever installed: do not download or install the component;
+- `high-assurance` / `all` + missing host: `setup` installs the verified stable component on supported Windows x64;
+- an already-installed or legacy host can be repaired/upgraded even if the current mode is `off`;
+- a versioned host already equal to stable is unchanged;
+- a host ahead of the stable release is reported as `already_ahead` and is never automatically downgraded;
+- a configured install record whose executable is missing is treated as a broken install and repaired rather than mistaken for an intentional absence.
+
+`doctor` is read-only and reports the skill and Authority host separately, including support/install state, version/source commit, whether the current mode requires Authority, and relations such as `missing`, `legacy`, `update`, `already_current`, or `already_ahead`.
+
+### Control Center Settings
+
+Open **Control Center > Settings** to choose the same persistent protection preference used by the Node CLI:
+
+- **Off** -> `off`
+- **Sensitive actions** -> `high-assurance` (**Recommended**)
+- **Every GitHub write** -> `all`
+
+The page shows the stored/effective protection mode plus installed Authority version/source metadata. If an environment override changes the effective mode, the UI warns that **Apply** changes the stored preference but does not override the active environment policy.
+
+## Repository / source installation
+
+For repository development, use PowerShell from the repository root:
```powershell
.\authority-host\windows\install.ps1
```
-The installer fails early unless it finds:
+This is **not** the normal stable binary-distribution path. The source installer fails early unless it finds:
1. Windows 11 build 22000 or newer;
2. the `dotnet` command;
3. at least one installed .NET 8 SDK.
-It then:
+It builds the self-contained host into a temporary publish directory, stamps local version/source metadata, and then delegates deployment to `install-release.ps1` so source and stable installs share one state-preserving process/shortcut/environment replacement boundary.
-1. publishes the self-contained host under `%LOCALAPPDATA%\GitHubDeliveryAuthority`;
-2. stops only a running `GitHubDeliveryAuthority` process whose executable is inside that install directory;
-3. copies the new files and creates a per-user Startup shortcut;
-4. sets `GITHUB_DELIVERY_AUTHORITY_TRUST_STORE` to the generated public-key trust store;
-5. sets `GITHUB_DELIVERY_AUTHORITY_PIPE=github-delivery-authority-v1`;
-6. starts the host with `--setup`;
-7. leaves the user-selected github-delivery protection policy unchanged.
+`install-release.ps1` does not call `dotnet`. It stops only a running `GitHubDeliveryAuthority` process whose resolved executable path is inside the selected install root, deploys a versioned runtime, recreates the per-user Startup shortcut, sets `GITHUB_DELIVERY_AUTHORITY_TRUST_STORE` and `GITHUB_DELIVERY_AUTHORITY_PIPE=github-delivery-authority-v1` at User scope, verifies the installed metadata, and starts the host.
### First-run setup
@@ -103,9 +147,11 @@ For `DeviceNotPresent` and `NotConfiguredForUser`, the UI can open `ms-settings:
## Upgrade behavior
-Running `install.ps1` again performs the same prerequisite checks, publishes the replacement self-contained build, stops only the installed authority-host instance under the selected install directory, replaces the application files, and starts the new host with `--setup`.
+For stable users, `npx github-delivery update` reports the skill plan and Authority-host plan independently. `npx github-delivery update --apply` keeps an already-installed Authority host aligned with the verified stable release; this remains true when the skill itself is already current, so a stale/legacy host can be the only component changed by an update.
+
+The release installer preserves `authority.db`, `trust-store.json`, persistent github-delivery config, and unrelated user state. It does not claim success until the active install record and executable match the intended release version/source commit.
-Existing SQLite authority state remains in the configured install directory. Global github-delivery protection configuration is stored outside the skill installation and is not silently changed by an authority-host upgrade.
+For source development, rerunning `install.ps1` rebuilds locally and delegates to the same release installer boundary.
## Use from github-delivery
@@ -117,7 +163,7 @@ node scripts/github-authorize.mjs --request batch.json --out authorized.json
The output contains the same broker requests with one `authorityGrant` attached to each operation. Execute individual requests through `scripts/github-mutate.mjs` as usual. Grants that declare `redemption: required` are atomically consumed by the host immediately before the GitHub mutation.
-The preferred user-facing configuration path is the prompt-driven setup/settings workflow documented by the repository. The legacy environment switch remains a compatibility override that forces the equivalent of `all`:
+The preferred user-facing configuration path is **Control Center > Settings** or the prompt-driven setup/configuration workflow documented by the repository. The legacy environment switch remains a compatibility override that forces the equivalent of `all`:
```powershell
$env:GITHUB_DELIVERY_REQUIRE_TRUSTED_AUTHORITY = '1'
diff --git a/authority-host/windows/install-release.ps1 b/authority-host/windows/install-release.ps1
new file mode 100644
index 00000000..9d572da6
--- /dev/null
+++ b/authority-host/windows/install-release.ps1
@@ -0,0 +1,121 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$SourceDir,
+ [Parameter(Mandatory = $true)]
+ [string]$ExpectedVersion,
+ [Parameter(Mandatory = $true)]
+ [string]$ExpectedSourceCommit,
+ [string]$InstallDir = (Join-Path $env:LOCALAPPDATA 'GitHubDeliveryAuthority'),
+ [string]$PipeName = 'github-delivery-authority-v1'
+)
+
+$ErrorActionPreference = 'Stop'
+$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
+
+if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
+ throw 'GitHub Delivery Authority can only be installed on Windows.'
+}
+if ([Environment]::OSVersion.Version.Build -lt 22000) {
+ throw 'Windows 11 build 22000 or newer is required.'
+}
+if ($ExpectedVersion -notmatch '^\d+\.\d+\.\d+$') {
+ throw 'ExpectedVersion must be semantic x.y.z.'
+}
+if ($ExpectedSourceCommit -notmatch '^[0-9a-fA-F]{40}$') {
+ throw 'ExpectedSourceCommit must be a 40-character commit SHA.'
+}
+
+$SourceDir = [IO.Path]::GetFullPath($SourceDir)
+$InstallDir = [IO.Path]::GetFullPath($InstallDir)
+$versionPath = Join-Path $SourceDir 'authority-host-version.json'
+$exe = Join-Path $SourceDir 'GitHubDeliveryAuthority.exe'
+if (-not (Test-Path $SourceDir -PathType Container)) { throw 'Verified Authority source directory is missing.' }
+if (-not (Test-Path $versionPath -PathType Leaf)) { throw 'Verified Authority version metadata is missing.' }
+if (-not (Test-Path $exe -PathType Leaf)) { throw 'Verified Authority executable is missing.' }
+
+$versionInfo = Get-Content $versionPath -Raw | ConvertFrom-Json
+if ($versionInfo.schemaVersion -ne 1 -or $versionInfo.kind -ne 'github-delivery/authority-host-version') {
+ throw 'Authority version metadata is invalid.'
+}
+if ($versionInfo.version -ne $ExpectedVersion) { throw 'Authority version metadata does not match the expected version.' }
+if ($versionInfo.sourceCommit.ToLowerInvariant() -ne $ExpectedSourceCommit.ToLowerInvariant()) {
+ throw 'Authority source commit does not match the expected commit.'
+}
+if ($versionInfo.platform -ne 'win32' -or $versionInfo.arch -ne 'x64') {
+ throw 'Authority platform metadata is invalid.'
+}
+
+New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
+$appRoot = Join-Path $InstallDir 'app'
+New-Item -ItemType Directory -Force -Path $appRoot | Out-Null
+$targetDir = Join-Path $appRoot ('v' + $ExpectedVersion)
+$stagingDir = Join-Path $appRoot ('.staging-' + [guid]::NewGuid().ToString('N'))
+
+$installRootPrefix = $InstallDir.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
+$installedProcesses = Get-Process -Name 'GitHubDeliveryAuthority' -ErrorAction SilentlyContinue | Where-Object {
+ try {
+ $processPath = $_.Path
+ if (-not $processPath) { return $false }
+ [IO.Path]::GetFullPath($processPath).StartsWith($installRootPrefix, [StringComparison]::OrdinalIgnoreCase)
+ }
+ catch { $false }
+}
+foreach ($process in $installedProcesses) {
+ Stop-Process -Id $process.Id -Force
+ Wait-Process -Id $process.Id -ErrorAction SilentlyContinue
+}
+
+try {
+ New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null
+ Copy-Item (Join-Path $SourceDir '*') $stagingDir -Recurse -Force
+ $stagedExe = Join-Path $stagingDir 'GitHubDeliveryAuthority.exe'
+ if (-not (Test-Path $stagedExe -PathType Leaf)) { throw 'Staged Authority executable is missing.' }
+
+ if (Test-Path $targetDir) {
+ Remove-Item $targetDir -Recurse -Force
+ }
+ Move-Item $stagingDir $targetDir
+
+ $installedExe = Join-Path $targetDir 'GitHubDeliveryAuthority.exe'
+ $startup = [Environment]::GetFolderPath('Startup')
+ $shortcutPath = Join-Path $startup 'GitHub Delivery Authority.lnk'
+ $shell = New-Object -ComObject WScript.Shell
+ $shortcut = $shell.CreateShortcut($shortcutPath)
+ $shortcut.TargetPath = $installedExe
+ $shortcut.WorkingDirectory = $targetDir
+ $shortcut.Save()
+
+ [Environment]::SetEnvironmentVariable(
+ 'GITHUB_DELIVERY_AUTHORITY_TRUST_STORE',
+ (Join-Path $InstallDir 'trust-store.json'),
+ 'User')
+ [Environment]::SetEnvironmentVariable('GITHUB_DELIVERY_AUTHORITY_PIPE', $PipeName, 'User')
+ $env:GITHUB_DELIVERY_AUTHORITY_TRUST_STORE = Join-Path $InstallDir 'trust-store.json'
+ $env:GITHUB_DELIVERY_AUTHORITY_PIPE = $PipeName
+
+ $installRecord = [ordered]@{
+ schemaVersion = 1
+ kind = 'github-delivery/authority-host-install'
+ version = $ExpectedVersion
+ sourceCommit = $ExpectedSourceCommit.ToLowerInvariant()
+ appDir = ('app/v' + $ExpectedVersion)
+ installedAt = [DateTimeOffset]::UtcNow.ToString('o')
+ }
+ $recordPath = Join-Path $InstallDir 'authority-host-install.json'
+ $recordTemp = $recordPath + '.' + [guid]::NewGuid().ToString('N') + '.tmp'
+ $recordJson = ($installRecord | ConvertTo-Json) + [Environment]::NewLine
+ [IO.File]::WriteAllText($recordTemp, $recordJson, $utf8NoBom)
+ Move-Item -Force $recordTemp $recordPath
+
+ # Remove only the obsolete root-level launcher from the legacy layout. State
+ # (`authority.db`, trust-store.json) and unknown user files are deliberately preserved.
+ $legacyExe = Join-Path $InstallDir 'GitHubDeliveryAuthority.exe'
+ if (Test-Path $legacyExe -PathType Leaf) { Remove-Item $legacyExe -Force }
+
+ Start-Process $installedExe
+ Write-Host "Installed GitHub Delivery Authority $ExpectedVersion to $targetDir"
+}
+finally {
+ if (Test-Path $stagingDir) { Remove-Item $stagingDir -Recurse -Force -ErrorAction SilentlyContinue }
+}
diff --git a/authority-host/windows/install.ps1 b/authority-host/windows/install.ps1
index cacd3744..eb65bef9 100644
--- a/authority-host/windows/install.ps1
+++ b/authority-host/windows/install.ps1
@@ -5,8 +5,11 @@ param(
)
$ErrorActionPreference = 'Stop'
+$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
$project = Join-Path $PSScriptRoot 'GitHubDeliveryAuthority\GitHubDeliveryAuthority.csproj'
$publish = Join-Path $env:TEMP ('github-delivery-authority-publish-' + [guid]::NewGuid().ToString('N'))
+$releaseInstaller = Join-Path $PSScriptRoot 'install-release.ps1'
+$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..'))
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
throw 'GitHub Delivery Authority can only be installed on Windows 11.'
@@ -19,12 +22,12 @@ if ($windowsBuild -lt 22000) {
$dotnet = Get-Command dotnet -ErrorAction SilentlyContinue
if (-not $dotnet) {
- throw 'The .NET 8 SDK is required. Install the .NET 8 SDK, then run this installer again.'
+ throw 'The .NET 8 SDK is required only when installing the Authority host from source. Stable npx install/update uses the prebuilt verified release component.'
}
$installedSdks = @(& $dotnet.Source --list-sdks)
if (-not ($installedSdks | Where-Object { $_ -match '^8\.' })) {
- throw 'The .NET 8 SDK is required. Install an 8.x SDK, then run this installer again.'
+ throw 'The .NET 8 SDK is required when installing the Authority host from source. Install an 8.x SDK, then run this installer again.'
}
try {
@@ -33,49 +36,37 @@ try {
throw "dotnet publish failed with exit code $LASTEXITCODE."
}
- New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
- $installRoot = [IO.Path]::GetFullPath($InstallDir).TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
+ $package = Get-Content (Join-Path $repoRoot 'package.json') -Raw | ConvertFrom-Json
+ $version = [string]$package.version
+ if ($version -notmatch '^\d+\.\d+\.\d+$') { throw 'Repository package version is invalid.' }
- $installedProcesses = Get-Process -Name 'GitHubDeliveryAuthority' -ErrorAction SilentlyContinue | Where-Object {
- try {
- $processPath = $_.Path
- if (-not $processPath) { return $false }
- [IO.Path]::GetFullPath($processPath).StartsWith($installRoot, [StringComparison]::OrdinalIgnoreCase)
- }
- catch {
- $false
- }
+ $sourceCommit = $null
+ $git = Get-Command git -ErrorAction SilentlyContinue
+ if ($git) {
+ $candidate = (& $git.Source -C $repoRoot rev-parse HEAD 2>$null | Select-Object -First 1)
+ if ($candidate -match '^[0-9a-fA-F]{40}$') { $sourceCommit = $candidate.ToLowerInvariant() }
}
-
- foreach ($process in $installedProcesses) {
- Stop-Process -Id $process.Id -Force
- Wait-Process -Id $process.Id -ErrorAction SilentlyContinue
+ if (-not $sourceCommit) {
+ throw 'A git source commit is required to install the Authority host from source.'
}
- Copy-Item (Join-Path $publish '*') $InstallDir -Recurse -Force
-
- $exe = Join-Path $InstallDir 'GitHubDeliveryAuthority.exe'
- $startup = [Environment]::GetFolderPath('Startup')
- $shortcutPath = Join-Path $startup 'GitHub Delivery Authority.lnk'
- $shell = New-Object -ComObject WScript.Shell
- $shortcut = $shell.CreateShortcut($shortcutPath)
- $shortcut.TargetPath = $exe
- $shortcut.WorkingDirectory = $InstallDir
- $shortcut.Save()
+ $versionInfo = [ordered]@{
+ schemaVersion = 1
+ kind = 'github-delivery/authority-host-version'
+ version = $version
+ sourceCommit = $sourceCommit
+ platform = 'win32'
+ arch = 'x64'
+ }
+ $versionJson = ($versionInfo | ConvertTo-Json) + [Environment]::NewLine
+ [IO.File]::WriteAllText((Join-Path $publish 'authority-host-version.json'), $versionJson, $utf8NoBom)
- [Environment]::SetEnvironmentVariable(
- 'GITHUB_DELIVERY_AUTHORITY_TRUST_STORE',
- (Join-Path $InstallDir 'trust-store.json'),
- 'User')
- [Environment]::SetEnvironmentVariable('GITHUB_DELIVERY_AUTHORITY_PIPE', $PipeName, 'User')
- $env:GITHUB_DELIVERY_AUTHORITY_TRUST_STORE = Join-Path $InstallDir 'trust-store.json'
- $env:GITHUB_DELIVERY_AUTHORITY_PIPE = $PipeName
+ & $releaseInstaller -SourceDir $publish -ExpectedVersion $version -ExpectedSourceCommit $sourceCommit -InstallDir $InstallDir -PipeName $PipeName
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- Start-Process $exe -ArgumentList '--setup'
- Write-Host "Installed GitHub Delivery Authority to $InstallDir"
- Write-Host 'The setup window will check Windows Hello, test it, and guide you through the first repository.'
+ Write-Host 'The Authority host was built from source and installed through the same state-preserving deployment boundary used by verified releases.'
Write-Host 'A Windows Hello PIN is sufficient; fingerprint or face hardware is not required.'
- Write-Host 'Strict trusted-authority mode was NOT enabled automatically.'
+ Write-Host 'The user-selected github-delivery protection mode was NOT changed.'
}
finally {
Remove-Item $publish -Recurse -Force -ErrorAction SilentlyContinue
diff --git a/docs/superpowers/plans/2026-08-12-authority-host-release-settings.md b/docs/superpowers/plans/2026-08-12-authority-host-release-settings.md
new file mode 100644
index 00000000..15bd4486
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-12-authority-host-release-settings.md
@@ -0,0 +1,817 @@
+# Authority Host Release Integration + Settings Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Make the Windows Delivery Authority host a verified, versioned GitHub Release component that stable install/update can install or upgrade without a local .NET SDK, and add a functional Control Center Settings page for the three existing authority modes.
+
+**Architecture:** The tagged release builds the WinUI host on `windows-latest`, packages it as a separate self-contained `win-x64` asset, and attests it from the protected release workflow. Node-side release verification reuses the existing GitHub release/tag/attestation trust chain, while a focused Windows component installer stages and replaces only release-owned host files while preserving authority state. The Control Center reads/writes the same persistent `authorityMode` config as Node and exposes host version/status from installed metadata.
+
+**Tech Stack:** Node.js 22/24 ESM, GitHub Actions, .NET 8, WinUI 3 / Windows App SDK, PowerShell, Node `node:test`.
+
+## Global Constraints
+
+- The npm package stays a thin bootstrap; do not bundle the Windows Authority binary into npm.
+- Stable Authority-host installation must not require a local .NET SDK.
+- The stable source of truth remains the fixed `Wibias/github-delivery` GitHub Release.
+- Keep `authorityMode` values exactly `off`, `high-assurance`, and `all`.
+- Keep persistent default `authorityMode = off`.
+- Keep named pipe `github-delivery-authority-v1`.
+- Preserve `%LOCALAPPDATA%\GitHubDeliveryAuthority\authority.db` and `trust-store.json` across host upgrades.
+- Preserve `%LOCALAPPDATA%\github-delivery\config.json` across all skill/host upgrades.
+- Do not install the Authority host for a Windows user with `off` when no host is already installed.
+- If the host is already installed, stable update may upgrade it even when the current mode is `off`.
+- Never automatically downgrade an Authority host that is ahead of the stable release.
+- All downloaded Authority binaries must be version/source/digest/attestation verified before mutation.
+- Non-Windows stable install/update behavior must remain unchanged.
+
+---
+
+### Task 1: Build a deterministic Authority-host release package
+
+**Files:**
+- Modify: `scripts/lib/distribution.mjs`
+- Create: `scripts/build-authority-host-release.mjs`
+- Create: `tests/unit/authority-host-release-package.test.mjs`
+
+**Interfaces:**
+- Consumes: an already-published self-contained directory from `dotnet publish` plus `version` and tagged `sourceCommit`.
+- Produces: `buildAuthorityHostRelease({ publishDir, outDir, version, sourceCommit }) -> { archivePath, metadataPath, metadata }`.
+- Produces release metadata kind `github-delivery/authority-host-release` and archive name `github-delivery-authority-v-win-x64.zip`.
+
+- [ ] **Step 1: Write failing packaging tests**
+
+Create `tests/unit/authority-host-release-package.test.mjs` with a temporary fake publish directory and assert exact asset identity and metadata:
+
+```js
+import assert from "node:assert/strict";
+import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import test from "node:test";
+
+import { buildAuthorityHostRelease } from "../../scripts/build-authority-host-release.mjs";
+
+const sourceCommit = "a".repeat(40);
+
+test("packages a version-bound win-x64 authority host asset", () => {
+ const root = mkdtempSync(join(tmpdir(), "gd-authority-package-"));
+ const publishDir = join(root, "publish");
+ const outDir = join(root, "out");
+ mkdirSync(publishDir, { recursive: true });
+ writeFileSync(join(publishDir, "GitHubDeliveryAuthority.exe"), "exe");
+ writeFileSync(join(publishDir, "Microsoft.WindowsAppRuntime.dll"), "dll");
+
+ const result = buildAuthorityHostRelease({
+ publishDir,
+ outDir,
+ version: "0.5.2",
+ sourceCommit,
+ });
+
+ assert.equal(result.metadata.version, "0.5.2");
+ assert.equal(result.metadata.sourceCommit, sourceCommit);
+ assert.equal(result.metadata.platform, "win32");
+ assert.equal(result.metadata.arch, "x64");
+ assert.equal(result.metadata.archive, "github-delivery-authority-v0.5.2-win-x64.zip");
+ assert.match(result.metadata.sha256, /^[0-9a-f]{64}$/);
+ assert.deepEqual(
+ JSON.parse(readFileSync(result.metadataPath, "utf8")),
+ result.metadata,
+ );
+});
+```
+
+Also assert invalid versions/source SHAs fail and that the generated archive contains `GitHubDeliveryAuthority/authority-host-version.json`.
+
+- [ ] **Step 2: Run the new test and verify it fails**
+
+Run:
+
+```bash
+node --test tests/unit/authority-host-release-package.test.mjs
+```
+
+Expected: FAIL because `scripts/build-authority-host-release.mjs` does not exist.
+
+- [ ] **Step 3: Export the existing stored ZIP writer instead of duplicating ZIP code**
+
+In `scripts/lib/distribution.mjs`, rename/export the current private `zipArchive(entries)` as:
+
+```js
+export function createStoredZipArchive(entries) {
+ // existing zipArchive implementation, unchanged
+}
+```
+
+Update the existing skill distribution caller to use `createStoredZipArchive(entries)` so normal distribution bytes stay identical.
+
+- [ ] **Step 4: Implement the Authority release packager**
+
+Create `scripts/build-authority-host-release.mjs` with validation and a callable export. The core shape must be:
+
+```js
+export function buildAuthorityHostRelease({ publishDir, outDir, version, sourceCommit }) {
+ validateVersion(version);
+ validateCommit(sourceCommit);
+ const archiveName = `github-delivery-authority-v${version}-win-x64.zip`;
+ const metadataName = `github-delivery-authority-v${version}-win-x64.json`;
+ const versionInfo = {
+ schemaVersion: 1,
+ kind: "github-delivery/authority-host-version",
+ version,
+ sourceCommit: sourceCommit.toLowerCase(),
+ platform: "win32",
+ arch: "x64",
+ };
+ // collect regular files from publishDir, reject links, inject
+ // GitHubDeliveryAuthority/authority-host-version.json, build stored ZIP,
+ // hash archive, write release metadata.
+}
+```
+
+The release metadata must exactly match the design document. Reject symlinks/reparse-point-like unexpected entries where detectable, duplicate relative paths, absolute paths, `..`, empty publish directories, and missing `GitHubDeliveryAuthority.exe`.
+
+- [ ] **Step 5: Run packaging tests**
+
+Run:
+
+```bash
+node --test tests/unit/authority-host-release-package.test.mjs tests/unit/distribution*.test.mjs
+```
+
+Expected: PASS and existing distribution tests remain byte-compatible.
+
+- [ ] **Step 6: Commit the packaging unit**
+
+Commit message:
+
+```text
+build: package versioned authority host asset
+```
+
+---
+
+### Task 2: Publish and attest the Authority host from `release.yml`
+
+**Files:**
+- Modify: `.github/workflows/release.yml`
+- Modify: `tests/unit/windows-authority-winui.test.mjs`
+- Create: `tests/unit/release-authority-asset.test.mjs`
+
+**Interfaces:**
+- Consumes: Task 1 `scripts/build-authority-host-release.mjs`.
+- Produces workflow artifact `github-delivery-authority-${{ github.sha }}` containing the `.zip` and `.json`.
+- Publishes both files as GitHub Release assets and attests the `.zip` from `release.yml`.
+
+- [ ] **Step 1: Write failing workflow contract tests**
+
+Create `tests/unit/release-authority-asset.test.mjs` that reads `.github/workflows/release.yml` and asserts:
+
+```js
+assert.match(workflow, /authority_host:/);
+assert.match(workflow, /runs-on: windows-latest/);
+assert.match(workflow, /dotnet publish[\s\S]*--runtime win-x64[\s\S]*--self-contained true/);
+assert.match(workflow, /build-authority-host-release\.mjs/);
+assert.match(workflow, /actions\/upload-artifact@/);
+assert.match(workflow, /actions\/download-artifact@/);
+assert.match(workflow, /github-delivery-authority-v\*\.zip/);
+assert.match(workflow, /Attest Windows authority host/);
+assert.match(workflow, /gh release create[\s\S]*github-delivery-authority-v\*\.json/);
+```
+
+Extend `windows-authority-winui.test.mjs` so CI still proves the host is self-contained but does not treat the source installer as the stable binary distribution mechanism.
+
+- [ ] **Step 2: Run the workflow tests and verify they fail**
+
+Run:
+
+```bash
+node --test tests/unit/release-authority-asset.test.mjs tests/unit/windows-authority-winui.test.mjs
+```
+
+Expected: FAIL on missing release job/assets.
+
+- [ ] **Step 3: Add the Windows release build job**
+
+Add a job after `validate`:
+
+```yaml
+authority_host:
+ name: Build Windows authority host release asset
+ needs: validate
+ runs-on: windows-latest
+ permissions:
+ contents: read
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - name: Set up Node.js
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: 22
+ package-manager-cache: false
+ - name: Restore and publish Windows authority host
+ shell: pwsh
+ run: |
+ $Project = 'authority-host/windows/GitHubDeliveryAuthority/GitHubDeliveryAuthority.csproj'
+ $PublishDir = Join-Path $env:RUNNER_TEMP 'github-delivery-authority-publish'
+ dotnet restore $Project --locked-mode
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ dotnet publish $Project --configuration Release --runtime win-x64 --self-contained true --no-restore --output $PublishDir /p:Version=${{ steps.release_version.outputs.version }}
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+```
+
+Resolve the semantic version from `package.json` in a preceding step and pass it to both `dotnet publish` and `scripts/build-authority-host-release.mjs` together with `${{ github.sha }}`.
+
+- [ ] **Step 4: Upload the Windows asset from the build job**
+
+Use the already-pinned `actions/upload-artifact` version used by the repo and upload only:
+
+```text
+/github-delivery-authority-v*.zip
+/github-delivery-authority-v*.json
+```
+
+with `if-no-files-found: error`.
+
+- [ ] **Step 5: Make protected publish depend on the Windows artifact**
+
+Change:
+
+```yaml
+needs: validate
+```
+
+to:
+
+```yaml
+needs:
+ - validate
+ - authority_host
+```
+
+Download the artifact before attest/publish, attest the Authority ZIP with `actions/attest`, and include both Authority files in `gh release create`.
+
+- [ ] **Step 6: Run workflow/security checks**
+
+Run:
+
+```bash
+node --test tests/unit/release-authority-asset.test.mjs tests/unit/windows-authority-winui.test.mjs
+npm run security:repo
+```
+
+Expected: PASS.
+
+- [ ] **Step 7: Commit the release workflow unit**
+
+Commit message:
+
+```text
+release: publish attested authority host asset
+```
+
+---
+
+### Task 3: Verify and extract the Authority-host release asset
+
+**Files:**
+- Create: `scripts/lib/authority-host-release.mjs`
+- Modify: `scripts/lib/release-self-update.mjs`
+- Create: `tests/unit/authority-host-release.test.mjs`
+
+**Interfaces:**
+- Consumes: `createGitHubReleaseClient()`, `verifyGitHubAssetDigest()`, and `verifyReleaseAttestation()` from the existing stable-release trust chain.
+- Produces: `acquireVerifiedAuthorityHostPayload({ release, workspace, client, sourceCommit, attestationRunner })`.
+- Produces: `{ verified: true, source, metadata, archivePath, release: { tag, version, sourceCommit } }`.
+
+- [ ] **Step 1: Write failing metadata and verification tests**
+
+Cover exact success plus rejection of wrong version, platform, arch, source commit, archive name, SHA-256, duplicate assets, malformed GitHub asset digest, and attestation failure.
+
+Use a fake release with:
+
+```js
+const release = {
+ tag_name: "v0.5.2",
+ draft: false,
+ prerelease: false,
+ assets: [
+ { name: "github-delivery-authority-v0.5.2-win-x64.zip", browser_download_url: "https://example.test/host.zip" },
+ { name: "github-delivery-authority-v0.5.2-win-x64.json", browser_download_url: "https://example.test/host.json" },
+ ],
+};
+```
+
+Include malicious ZIP fixtures for `../`, absolute paths, duplicate entries, unsupported compression/data descriptors, and unbounded file/total sizes.
+
+- [ ] **Step 2: Run the test and verify it fails**
+
+```bash
+node --test tests/unit/authority-host-release.test.mjs
+```
+
+Expected: FAIL because the module does not exist.
+
+- [ ] **Step 3: Export the reusable stable-release helpers needed by the component verifier**
+
+From `scripts/lib/release-self-update.mjs`, export the existing `verifyGitHubAssetDigest` and keep `verifyReleaseAttestation` public. Do not weaken their existing constraints.
+
+- [ ] **Step 4: Implement strict Authority metadata validation**
+
+Create:
+
+```js
+export function validateAuthorityHostReleaseMetadata(value, { version, sourceCommit } = {}) {
+ if (
+ value?.schemaVersion !== 1 ||
+ value?.kind !== "github-delivery/authority-host-release" ||
+ value?.version !== version ||
+ value?.sourceCommit?.toLowerCase() !== sourceCommit.toLowerCase() ||
+ value?.platform !== "win32" ||
+ value?.arch !== "x64" ||
+ value?.archive !== `github-delivery-authority-v${version}-win-x64.zip` ||
+ !/^[0-9a-f]{64}$/i.test(value?.sha256 || "")
+ ) throw new Error("authority_host_release_metadata_invalid");
+ return { ...value, sourceCommit: value.sourceCommit.toLowerCase(), sha256: value.sha256.toLowerCase() };
+}
+```
+
+- [ ] **Step 5: Implement bounded extraction under one fixed root**
+
+The Authority ZIP must contain only regular files beneath:
+
+```text
+GitHubDeliveryAuthority/
+```
+
+Use the same central-directory/local-header validation rules as `release-zip.mjs`, but Authority-specific limits large enough for a self-contained WinUI app, for example:
+
+```js
+const AUTHORITY_LIMITS = Object.freeze({
+ maxArchiveBytes: 256 * 1024 * 1024,
+ maxFileBytes: 128 * 1024 * 1024,
+ maxTotalBytes: 512 * 1024 * 1024,
+ maxFiles: 4096,
+});
+```
+
+Do not shell out to an unconstrained archive extractor.
+
+- [ ] **Step 6: Implement acquisition and attestation verification**
+
+`acquireVerifiedAuthorityHostPayload` must:
+
+1. derive version from `release.tag_name`;
+2. require exactly one versioned `.zip` and `.json` asset;
+3. download with explicit size limits;
+4. verify GitHub asset digests if present;
+5. parse metadata;
+6. compare archive SHA-256 to metadata;
+7. compare tag source SHA to metadata source SHA;
+8. persist the archive to a private workspace;
+9. call `verifyReleaseAttestation({ archivePath, tag, sourceCommit })`;
+10. strictly extract and return the staged root.
+
+- [ ] **Step 7: Run verification tests**
+
+```bash
+node --test tests/unit/authority-host-release.test.mjs tests/unit/release-self-update.test.mjs tests/unit/release-zip.test.mjs
+```
+
+Expected: PASS.
+
+- [ ] **Step 8: Commit the verification unit**
+
+Commit message:
+
+```text
+security: verify authority host release assets
+```
+
+---
+
+### Task 4: Install and upgrade the verified Windows component without .NET SDK
+
+**Files:**
+- Create: `scripts/lib/authority-host-install.mjs`
+- Create: `authority-host/windows/install-release.ps1`
+- Modify: `authority-host/windows/install.ps1`
+- Create: `tests/unit/authority-host-install.test.mjs`
+- Modify: `tests/unit/windows-authority-winui.test.mjs`
+
+**Interfaces:**
+- Produces: `readInstalledAuthorityHost({ env, platform, arch })`.
+- Produces: `planAuthorityHostUpdate({ installed, releaseVersion, mode, platform, arch })`.
+- Produces: `applyVerifiedAuthorityHost({ source, metadata, env, runner })`.
+- PowerShell installer accepts `-SourceDir`, optional `-InstallDir`, optional `-PipeName`, and `-Setup`.
+
+- [ ] **Step 1: Write the decision-matrix tests**
+
+Add exact cases:
+
+```js
+assert.equal(planAuthorityHostUpdate({ installed: null, releaseVersion: "0.5.2", mode: "off", platform: "win32", arch: "x64" }).action, "skip");
+assert.equal(planAuthorityHostUpdate({ installed: null, releaseVersion: "0.5.2", mode: "high-assurance", platform: "win32", arch: "x64" }).action, "install");
+assert.equal(planAuthorityHostUpdate({ installed: { version: "0.5.1" }, releaseVersion: "0.5.2", mode: "off", platform: "win32", arch: "x64" }).action, "update");
+assert.equal(planAuthorityHostUpdate({ installed: { version: "0.5.2" }, releaseVersion: "0.5.2", mode: "all", platform: "win32", arch: "x64" }).action, "already_current");
+assert.equal(planAuthorityHostUpdate({ installed: { version: "0.6.0" }, releaseVersion: "0.5.2", mode: "all", platform: "win32", arch: "x64" }).action, "already_ahead");
+```
+
+Also test a legacy install: executable exists but `authority-host-version.json` does not -> `installed.legacy === true` and action `update` when stable update/setup is allowed.
+
+- [ ] **Step 2: Run and verify failure**
+
+```bash
+node --test tests/unit/authority-host-install.test.mjs
+```
+
+Expected: FAIL because the module is missing.
+
+- [ ] **Step 3: Implement installed-host detection**
+
+The canonical path is:
+
+```js
+join(env.LOCALAPPDATA, "GitHubDeliveryAuthority")
+```
+
+Require `GitHubDeliveryAuthority.exe` plus valid `authority-host-version.json` for a versioned install. Parse only schema version 1, semantic version, 40-char source SHA, `win32`, `x64`. If the executable exists without valid metadata, return a legacy/unversioned result rather than pretending it is current.
+
+- [ ] **Step 4: Implement the pure update planner**
+
+Return only these actions:
+
+```text
+skip
+unsupported
+install
+update
+already_current
+already_ahead
+```
+
+`off + missing` => `skip`; installed host participates regardless of `off`; `high-assurance|all + missing` => `install` on Windows x64.
+
+- [ ] **Step 5: Write the release installer PowerShell script**
+
+`authority-host/windows/install-release.ps1` must not call `dotnet`. It receives an already verified staged publish directory and:
+
+```powershell
+$StateFiles = @('authority.db', 'trust-store.json')
+```
+
+It must stop only `GitHubDeliveryAuthority` processes whose resolved executable path is inside `$InstallDir`, preserve the state files, replace runtime files from `$SourceDir`, recreate the Startup shortcut, set `GITHUB_DELIVERY_AUTHORITY_TRUST_STORE` and `GITHUB_DELIVERY_AUTHORITY_PIPE` at User scope, then start the executable. Use `--setup` only for a fresh install or explicit `-Setup` request; upgrading an existing stateful install must not force repository setup.
+
+Before success, require:
+
+```powershell
+Test-Path (Join-Path $InstallDir 'GitHubDeliveryAuthority.exe')
+Test-Path (Join-Path $InstallDir 'authority-host-version.json')
+```
+
+- [ ] **Step 6: Make source `install.ps1` delegate deployment semantics**
+
+Keep its .NET prerequisite/build steps for development, but after `dotnet publish` invoke `install-release.ps1 -SourceDir $publish ...` rather than maintaining a second independent copy/shortcut/process-stop implementation.
+
+This creates one deployment boundary for both source and stable release installation.
+
+- [ ] **Step 7: Implement the Node apply wrapper**
+
+`applyVerifiedAuthorityHost` must invoke PowerShell with argument arrays and `shell: false`, then re-read installed metadata and require exact version/source equality before returning success.
+
+- [ ] **Step 8: Add preservation and no-.NET regression tests**
+
+Tests must assert the release installer text contains no `dotnet publish`/SDK requirement, preserves both state files, restricts process stopping by executable path, and verifies post-install metadata.
+
+- [ ] **Step 9: Run install tests**
+
+```bash
+node --test tests/unit/authority-host-install.test.mjs tests/unit/windows-authority-winui.test.mjs
+```
+
+Expected: PASS.
+
+- [ ] **Step 10: Commit the component installer**
+
+Commit message:
+
+```text
+feat: safely install verified authority host builds
+```
+
+---
+
+### Task 5: Integrate Authority-host planning into setup, update, and doctor
+
+**Files:**
+- Modify: `scripts/lib/bootstrap-maintenance.mjs`
+- Modify: `scripts/install-skill.mjs`
+- Modify: `scripts/lib/bootstrap-cli.mjs` if output/help text needs component status
+- Modify: `tests/unit/bootstrap-maintenance.test.mjs`
+- Modify: `tests/unit/stable-release-update.test.mjs`
+- Create: `tests/unit/authority-host-bootstrap.test.mjs`
+
+**Interfaces:**
+- Consumes Tasks 3-4 verifier/planner/apply functions.
+- Extends setup/update/doctor results with `authorityHost`.
+
+- [ ] **Step 1: Write failing bootstrap integration tests**
+
+Cover these exact scenarios:
+
+1. setup + `off` + missing host -> no network/install action;
+2. setup + `high-assurance` + missing host -> verified install;
+3. setup + `all` + stale host -> verified update;
+4. update dry-run + skill current + host stale -> reports host `update` rather than global no-op;
+5. update `--apply` + skill current + host stale -> upgrades host;
+6. update `--apply` + skill update + host stale -> both components update;
+7. host ahead -> no downgrade;
+8. Linux/macOS -> no host install attempt;
+9. doctor reports `missing`, `legacy`, `update`, `already_current`, and `already_ahead` relations.
+
+- [ ] **Step 2: Run and verify failure**
+
+```bash
+node --test tests/unit/authority-host-bootstrap.test.mjs tests/unit/bootstrap-maintenance.test.mjs
+```
+
+Expected: FAIL on absent `authorityHost` integration.
+
+- [ ] **Step 3: Add a component-status helper**
+
+In `bootstrap-maintenance.mjs`, add a focused helper that reads user config, resolves effective authority mode, reads the installed host, and computes the host plan against the same stable release version used for the skill.
+
+Do not duplicate authority-mode parsing; use `resolveAuthorityMode` from `user-config.mjs`.
+
+- [ ] **Step 4: Integrate setup**
+
+Before returning `status: "ready"`, setup must ensure a required (`high-assurance`/`all`) Windows host is installed/current. If host acquisition or install fails, return/throw an explicit authority-host setup error instead of reporting ready.
+
+If the host is missing and mode is `off`, setup must not download the host asset.
+
+- [ ] **Step 5: Integrate stable update without losing the already-current repair path**
+
+Refactor the current early return:
+
+```js
+if (candidate.plan.action === "already_current" || candidate.plan.action === "already_ahead") {
+ return ...;
+}
+```
+
+so an already-current skill can still repair/update an installed stale Authority host. The result should expose:
+
+```js
+{
+ action: "update",
+ updated: skillUpdated || authorityHostUpdated,
+ skill: { ... },
+ authorityHost: { action, updated, version, previousVersion, error: null },
+}
+```
+
+Do not report `updated: false` when a stale host was actually replaced.
+
+- [ ] **Step 6: Integrate doctor**
+
+Add:
+
+```js
+authorityHost: {
+ supported,
+ installed,
+ legacy,
+ version,
+ sourceCommit,
+ relation,
+ requiredByMode,
+ error,
+}
+```
+
+`requiredByMode` is true only for effective `high-assurance` or `all`.
+
+- [ ] **Step 7: Run bootstrap/update tests**
+
+```bash
+node --test tests/unit/authority-host-bootstrap.test.mjs tests/unit/bootstrap-maintenance.test.mjs tests/unit/stable-release-update.test.mjs
+```
+
+Expected: PASS.
+
+- [ ] **Step 8: Commit bootstrap integration**
+
+Commit message:
+
+```text
+feat: keep authority host aligned with stable updates
+```
+
+---
+
+### Task 6: Implement the functional WinUI Settings page and version status
+
+**Files:**
+- Modify: `authority-host/windows/GitHubDeliveryAuthority/UserConfigStore.cs`
+- Create: `authority-host/windows/GitHubDeliveryAuthority/AuthorityVersionInfo.cs`
+- Create: `authority-host/windows/GitHubDeliveryAuthority/SettingsView.xaml`
+- Create: `authority-host/windows/GitHubDeliveryAuthority/SettingsView.xaml.cs`
+- Modify: `authority-host/windows/GitHubDeliveryAuthority/ControlCenterWindow.xaml`
+- Modify: `authority-host/windows/GitHubDeliveryAuthority/ControlCenterWindow.xaml.cs`
+- Modify: `tests/unit/windows-authority-winui.test.mjs`
+- Modify: `tests/unit/user-config.test.mjs` only if cross-language schema contract coverage needs extension
+
+**Interfaces:**
+- `UserConfigStore.WriteAuthorityMode(string mode) -> DeliveryUserConfig`.
+- `UserConfigStore.ResolveEffectiveMode(DeliveryUserConfig stored) -> (string Mode, string? OverrideSource)` or equivalent focused record.
+- `AuthorityVersionInfo.Read() -> AuthorityVersionInfo?` from `AppPaths.RootDirectory/authority-host-version.json`.
+- `SettingsView.Refresh()` reloads current config/version status.
+
+- [ ] **Step 1: Write failing GUI contract tests**
+
+Extend `windows-authority-winui.test.mjs` to assert:
+
+```js
+const control = read(`${root}/ControlCenterWindow.xaml`);
+const settings = read(`${root}/SettingsView.xaml`);
+const configStore = read(`${root}/UserConfigStore.cs`);
+
+assert.match(control, /SelectionChanged="Navigation_SelectionChanged"/);
+assert.match(control, /SettingsView/);
+for (const text of ["Off", "Sensitive actions", "Every GitHub write", "Recommended", "Apply"]) {
+ assert.match(settings, new RegExp(text));
+}
+assert.match(configStore, /WriteAuthorityMode/);
+assert.match(configStore, /File\.Move\([\s\S]*overwrite:\s*true/);
+```
+
+Also assert Settings displays Authority version and effective override warning copy.
+
+- [ ] **Step 2: Run and verify failure**
+
+```bash
+node --test tests/unit/windows-authority-winui.test.mjs
+```
+
+Expected: FAIL because `SettingsView` and write support do not exist.
+
+- [ ] **Step 3: Add atomic C# config writes with the exact Node schema**
+
+Implement validation:
+
+```csharp
+private static bool IsValidMode(string mode)
+ => mode is "off" or "high-assurance" or "all";
+```
+
+Write JSON:
+
+```json
+{
+ "schemaVersion": 1,
+ "authorityMode": "high-assurance"
+}
+```
+
+Create the parent directory, write to a uniquely named temp file in the same directory, then atomically replace/move onto `ConfigPath`. Do not write when simply opening Settings.
+
+Mirror environment override precedence:
+
+1. `GITHUB_DELIVERY_REQUIRE_TRUSTED_AUTHORITY=1` => `all`;
+2. `GITHUB_DELIVERY_AUTHORITY_MODE` if valid;
+3. stored config.
+
+- [ ] **Step 4: Add version metadata reader**
+
+`AuthorityVersionInfo.cs` validates schema/kind/version/source/platform/arch before display. Invalid metadata must yield an explicit unavailable/invalid status rather than crashing the Control Center.
+
+- [ ] **Step 5: Build the Settings view**
+
+Use three radio buttons/cards mapped exactly:
+
+```text
+Off -> off
+Sensitive actions -> high-assurance
+Every GitHub write -> all
+```
+
+`Sensitive actions` carries a `Recommended` label. Add concise descriptions preserving the policy wording from `references/configuration.md`.
+
+The view shows stored mode, effective mode, host version, and source commit. If an environment override changes effective mode, show a visible warning that Apply changes the stored preference but not the active override.
+
+- [ ] **Step 6: Wire real navigation**
+
+Add `SelectionChanged="Navigation_SelectionChanged"` to the `NavigationView`. Wrap the existing dashboard content and `SettingsView` in named containers. Handler behavior:
+
+```csharp
+var tag = (Navigation.SelectedItem as NavigationViewItem)?.Tag?.ToString();
+DashboardContent.Visibility = tag == "settings" ? Visibility.Collapsed : Visibility.Visible;
+SettingsContent.Visibility = tag == "settings" ? Visibility.Visible : Visibility.Collapsed;
+if (tag == "settings") SettingsContent.Refresh();
+```
+
+The existing `OpenSettings_Click` continues selecting the Settings item, which now causes real content navigation.
+
+- [ ] **Step 7: Build and self-test the WinUI host**
+
+On Windows CI/local Windows:
+
+```powershell
+dotnet restore .\authority-host\windows\GitHubDeliveryAuthority\GitHubDeliveryAuthority.csproj --locked-mode
+dotnet build .\authority-host\windows\GitHubDeliveryAuthority\GitHubDeliveryAuthority.csproj -c Release --no-restore
+dotnet run --project .\authority-host\windows\GitHubDeliveryAuthority\GitHubDeliveryAuthority.csproj -c Release --no-build -- --self-test
+```
+
+Expected: all succeed.
+
+- [ ] **Step 8: Run Node GUI/config regressions**
+
+```bash
+node --test tests/unit/windows-authority-winui.test.mjs tests/unit/user-config.test.mjs tests/unit/authority-mode-enforcement.test.mjs
+```
+
+Expected: PASS and no authority-mode semantic changes.
+
+- [ ] **Step 9: Commit the GUI unit**
+
+Commit message:
+
+```text
+feat: add authority protection settings UI
+```
+
+---
+
+### Task 7: Document the unified component lifecycle and run the full gate
+
+**Files:**
+- Modify: `README.md`
+- Modify: `INSTALL.md`
+- Modify: `authority-host/windows/README.md`
+- Modify: `references/configuration.md`
+- Modify: `CHANGELOG.md`
+- Modify tests only if documentation contracts require exact copy updates
+
+**Interfaces:**
+- User-facing commands remain `npx github-delivery`, `setup`, `doctor`, `update`, and `update --apply`.
+- Source-development Authority install remains `authority-host/windows/install.ps1`.
+
+- [ ] **Step 1: Update docs with the new stable lifecycle**
+
+Document these user-visible facts exactly:
+
+```text
+- Stable GitHub Releases include a separately verified self-contained Windows Authority host asset.
+- Users do not need the .NET SDK for stable Authority host install/update.
+- If Authority is already installed, stable update keeps it aligned with the skill.
+- If protection is Off and Authority was never installed, update does not install it.
+- Control Center -> Settings contains Off / Sensitive actions / Every GitHub write.
+- doctor reports skill and Authority-host version/status separately.
+```
+
+Keep the source `install.ps1` instructions explicitly labeled as repository/development installation.
+
+- [ ] **Step 2: Add an Unreleased changelog entry**
+
+Describe both root-cause fixes: stale separately-installed authority binaries after skill upgrades, and the previously non-functional Settings destination.
+
+- [ ] **Step 3: Run focused release/component tests**
+
+```bash
+node --test \
+ tests/unit/authority-host-release-package.test.mjs \
+ tests/unit/release-authority-asset.test.mjs \
+ tests/unit/authority-host-release.test.mjs \
+ tests/unit/authority-host-install.test.mjs \
+ tests/unit/authority-host-bootstrap.test.mjs \
+ tests/unit/windows-authority-winui.test.mjs
+```
+
+Expected: PASS.
+
+- [ ] **Step 4: Run the repository full gate**
+
+```bash
+npm run check
+```
+
+Expected: PASS on Node 22/24 supported environments. Windows CI additionally must build/publish/self-test the Authority host.
+
+- [ ] **Step 5: Inspect the final diff for scope**
+
+Verify the PR changes only release packaging, Authority component install/update/doctor integration, WinUI Settings/version status, associated tests, and docs. Do not include unrelated mutation-policy or watchdog changes.
+
+- [ ] **Step 6: Commit docs/final integration**
+
+Commit message:
+
+```text
+docs: document authority host component lifecycle
+```
+
+- [ ] **Step 7: Mark the PR ready only after CI is green and implementation review passes**
+
+Keep the PR draft while any implementation task remains incomplete or while the Windows Authority build/release tests are not green.
diff --git a/docs/superpowers/specs/2026-08-12-authority-host-release-settings-design.md b/docs/superpowers/specs/2026-08-12-authority-host-release-settings-design.md
new file mode 100644
index 00000000..440551ee
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-12-authority-host-release-settings-design.md
@@ -0,0 +1,220 @@
+# Authority Host Release Integration + Settings Design
+
+## Goal
+
+Ship the Windows Delivery Authority host as a first-class, versioned release component and make its three protection modes configurable from the Control Center GUI, so a normal github-delivery upgrade cannot leave the skill on a new version while the authority executable remains on an old GUI/build.
+
+## Problem
+
+The stable github-delivery release payload already contains `authority-host/windows` source, but `npx github-delivery update --apply` only replaces the installed skill directory. The separately installed executable under `%LOCALAPPDATA%\GitHubDeliveryAuthority` is not upgraded, because the source installer `authority-host/windows/install.ps1` is never invoked by the stable update path.
+
+That creates a split installation:
+
+- skill version can be current;
+- Windows Authority host can remain an older binary indefinitely;
+- the user can therefore still see the old approval GUI after upgrading github-delivery.
+
+The new WinUI Control Center also advertises a `Settings` destination and shows the current protection mode, but there is no functional settings content that lets the user change `authorityMode`.
+
+## Chosen approach
+
+Treat the Windows Authority host as a separately built, separately verified asset of the same GitHub Release.
+
+A release publishes both:
+
+- the existing portable github-delivery skill archives;
+- a self-contained Windows x64 Authority host archive built from the exact same tagged commit.
+
+The npm package remains a thin bootstrap. It does not contain the Authority host binary.
+
+The Authority host asset is installed or upgraded only on supported Windows systems, using the verified stable GitHub Release as the source of truth.
+
+## Release artifact contract
+
+For release version `X.Y.Z`, publish:
+
+- `github-delivery-authority-vX.Y.Z-win-x64.zip`
+- `github-delivery-authority-vX.Y.Z-win-x64.json`
+
+The JSON metadata has this shape:
+
+```json
+{
+ "schemaVersion": 1,
+ "kind": "github-delivery/authority-host-release",
+ "version": "X.Y.Z",
+ "sourceCommit": "<40-char tagged commit sha>",
+ "platform": "win32",
+ "arch": "x64",
+ "archive": "github-delivery-authority-vX.Y.Z-win-x64.zip",
+ "sha256": "<64-char sha256>"
+}
+```
+
+The archive contains the self-contained WinUI publish output and an installed metadata file named `authority-host-version.json` with the same version/source/platform identity.
+
+The release workflow must attest the Authority host archive with the same `release.yml` workflow identity, tag ref, and source commit used for the skill release. The updater must verify:
+
+1. stable non-draft/non-prerelease release;
+2. exact versioned asset names;
+3. GitHub asset digest when exposed;
+4. metadata schema/version/platform/arch;
+5. archive SHA-256 against metadata;
+6. tag -> source commit equality;
+7. metadata source commit equality;
+8. GitHub artifact attestation constrained to `Wibias/github-delivery/.github/workflows/release.yml`, the exact tag, and the exact source commit;
+9. bounded ZIP extraction with no traversal/symlink/path confusion.
+
+No unverified Authority binary may be installed.
+
+## Build and release architecture
+
+`release.yml` gains a Windows job that checks out the exact tagged source, installs .NET 8, reads the version from `package.json`, and publishes `GitHubDeliveryAuthority` self-contained for `win-x64`.
+
+The build stamps the host with the same semantic version as `package.json` and produces the archive + metadata above. The result is uploaded as a workflow artifact.
+
+The protected `publish` job depends on both the existing release validation and the Authority-host build. It downloads the Authority artifact, attests the Authority archive, publishes npm as today, and attaches the Authority assets alongside the existing skill assets to the GitHub Release.
+
+The source-level `authority-host/windows/install.ps1` remains supported for repository development, but stable user install/update must not require a local .NET SDK.
+
+## Stable install/update behavior
+
+Add a Node-side Authority host component manager used by `npx github-delivery setup`, `doctor`, and stable `update --apply`.
+
+### Detection
+
+The canonical installed host directory remains:
+
+```text
+%LOCALAPPDATA%\GitHubDeliveryAuthority
+```
+
+A host is considered installed only when the executable and `authority-host-version.json` are both present and valid. Legacy installs without metadata are detected as `legacy/unversioned` and are eligible for replacement.
+
+### Install decision
+
+- `authorityMode = off` and no Authority host installed: do not install it.
+- `authorityMode = high-assurance` or `all` and host missing: install the verified Authority asset during setup.
+- Host already installed, regardless of current mode: stable `update --apply` upgrades it to the same stable version as the skill when an update is being applied.
+- Host version already equal to the stable release: no-op.
+- Host version ahead of the stable release: do not downgrade automatically; report `already_ahead`.
+- Unsupported OS/architecture: do not try to install; report an explicit diagnostic when protection requires the host.
+
+### Replacement safety
+
+The release installer stages the verified archive before touching the live host.
+
+When replacing an existing host:
+
+1. preserve `authority.db` and `trust-store.json` as authority state, not release files;
+2. stop only a `GitHubDeliveryAuthority` process whose executable path is inside the canonical install directory;
+3. replace release-owned files from the staged verified archive;
+4. recreate/repair the per-user Startup shortcut and user environment variables;
+5. restart the host without forcing first-run repository setup when preserved state is already valid;
+6. verify the installed `authority-host-version.json` matches the intended release;
+7. surface a hard failure if replacement cannot be verified.
+
+The host update must never delete the authority database, trust store, private-key backing state, or the persistent github-delivery user config.
+
+## Update transaction semantics
+
+The stable updater verifies both the skill payload and, when needed, the Authority host payload before beginning mutation.
+
+If only the skill needs updating, behavior stays unchanged.
+
+If the installed Authority host also needs updating, the updater performs the skill replacement and Authority replacement as one requested update operation and returns component-specific results. Failure of the Authority replacement is not reported as a clean fully-updated state; `doctor` must surface the mismatch explicitly.
+
+The existing skill backup remains available for skill rollback. The Authority installer stages replacement and preserves local state; it must not claim success until the installed metadata verifies.
+
+## Control Center Settings
+
+The Control Center gets a real Settings view reachable from the left navigation and the existing `Open settings` button.
+
+The page presents exactly three protection choices:
+
+- **Off** (`off`) — No Windows Hello prompts. Normal github-delivery mutation policy still applies.
+- **Sensitive actions** (`high-assurance`) — Recommended. Require Windows Hello for intrinsically high-assurance and autonomous execution, including protected pushes/merges.
+- **Every GitHub write** (`all`) — Require Windows Hello for every executed GitHub mutation.
+
+The currently stored mode is selected on load. `Apply` writes the same `%LOCALAPPDATA%\github-delivery\config.json` used by the Node CLI. There is one source of truth.
+
+C# `UserConfigStore` gains validated atomic write support matching the Node config schema. Invalid modes are rejected. The GUI must not silently rewrite configuration on view/open; only explicit Apply changes it.
+
+Changing the mode in the GUI preserves the current policy semantics: it is an explicit local user configuration action, and this PR does not add a new Windows Hello gate around config changes because the existing CLI config path does not require one either.
+
+## Version/status reporting
+
+Control Center Settings/Diagnostics shows:
+
+- github-delivery Authority host version;
+- source commit (short form is fine in the UI);
+- current stored protection mode;
+- effective mode warning when an environment-variable override changes the effective value;
+- Authority host readiness / legacy-unversioned state when applicable.
+
+`npx github-delivery doctor` reports at least:
+
+```json
+{
+ "authorityHost": {
+ "supported": true,
+ "installed": true,
+ "version": "X.Y.Z",
+ "sourceCommit": "...",
+ "relation": "already_current",
+ "requiredByMode": true,
+ "error": null
+ }
+}
+```
+
+`relation` uses `missing`, `legacy`, `update`, `already_current`, or `already_ahead` where applicable.
+
+## GUI navigation
+
+`NavigationView.SelectionChanged` switches the content between the existing dashboard and the Settings view. The existing `Open settings` button selects Settings and therefore renders the Settings content. This PR does not need to invent separate new screens for Activity/Allowlist/Temporary grants/Diagnostics beyond preserving their current dashboard behavior; the required functional navigation addition is Settings.
+
+## Backward compatibility
+
+- Existing `authorityMode` values and environment overrides keep their semantics.
+- Existing authority DB/trust-store locations remain unchanged.
+- Existing named pipe remains `github-delivery-authority-v1`.
+- Existing direct source installer remains available to developers.
+- Stable install/update on non-Windows systems remains unaffected.
+- `off` remains the persistent default.
+- A legacy Authority host installation can be upgraded without resetting its allowlist or key material.
+
+## Failure handling
+
+Fail closed for verification failures, malformed component metadata, unexpected archive content, source/tag mismatch, attestation failure, or post-install version mismatch.
+
+Do not silently install an Authority host on a user who has `off` selected and has never installed the component.
+
+Do not report the system as fully current when the skill and an already-installed/required Authority host are on different stable versions.
+
+## Tests
+
+Add/extend tests for:
+
+- release workflow publishes and attests the Authority asset;
+- Authority metadata validation and exact release/source binding;
+- download limits and malicious ZIP/path rejection;
+- host detection including legacy installs;
+- install decision matrix for `off`, `high-assurance`, `all`;
+- no downgrade and same-version no-op;
+- state-file preservation during replacement;
+- `doctor` component status;
+- Settings navigation actually renders settings content;
+- all three GUI mode choices map to the existing internal values;
+- C# config writes are validated and atomic;
+- old WinForms approval classes remain excluded from the compiled WinUI app;
+- existing mutation/authority semantics remain unchanged.
+
+## Out of scope
+
+- macOS/Linux authority host implementations;
+- changing the three authority-mode semantics;
+- changing repository allowlist rules;
+- changing branch-lease semantics;
+- changing mutation policy or which actions are intrinsically high-assurance;
+- bundling the Windows binary into the npm package.
diff --git a/package.json b/package.json
index 3640b291..03e24f0e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "github-delivery",
- "version": "0.5.1",
+ "version": "0.5.2",
"type": "module",
"engines": {
"node": "^22 || ^24"
@@ -12,6 +12,8 @@
"scripts/github-delivery-cli.mjs",
"scripts/install-codex-watchdog-hooks.mjs",
"scripts/install-skill.mjs",
+ "scripts/lib/authority-host-install.mjs",
+ "scripts/lib/authority-host-release.mjs",
"scripts/lib/bootstrap-cli.mjs",
"scripts/lib/bootstrap-command.mjs",
"scripts/lib/bootstrap-install.mjs",
diff --git a/references/configuration.md b/references/configuration.md
index 7fffe2e4..43520aa3 100644
--- a/references/configuration.md
+++ b/references/configuration.md
@@ -26,13 +26,28 @@ node scripts/github-delivery-config.mjs --authority-mode high-assurance
node scripts/github-delivery-config.mjs --authority-mode all
```
+On Windows, the same stored preference is available in **Control Center > Settings** as **Off**, **Sensitive actions**, and **Every GitHub write**. Both paths write the same persistent user configuration; environment-variable overrides can still make the effective mode stricter than the stored preference.
+
The config is global for the user's github-delivery installation and lives outside the installed skill directory.
## Windows Authority host
-If the selected mode is `high-assurance` or `all`, install or verify the Windows Authority host on supported Windows systems using `authority-host/windows/install.ps1`, then run its readiness/self-test path. A Windows Hello PIN is sufficient; biometric hardware is not required.
+For normal stable installations, use the managed lifecycle:
+
+```bash
+npx github-delivery setup
+npx github-delivery doctor
+npx github-delivery update
+npx github-delivery update --apply
+```
+
+Stable GitHub Releases contain a separately verified, self-contained `win-x64` Authority-host asset. Stable setup/update verifies the component's version, tagged source commit, archive digest, bounded extraction, and release-workflow attestation before installation, so users do **not** need the .NET SDK for the managed Authority install/update path.
+
+If the effective mode is `high-assurance` or `all`, `setup` installs or repairs the verified Authority host on supported Windows systems. If the selected mode is `off` and the host has never been installed, setup/update does not install it. Once the host is already installed, stable update keeps it aligned with the skill even when the current mode is `off`; a host newer than stable is never automatically downgraded.
-If the selected mode is `off`, do not make the authority host a prerequisite. An already installed host may remain installed for later use.
+`authority-host/windows/install.ps1` remains the repository/development source-install path. It requires Windows 11 plus the .NET 8 SDK and delegates deployment to the same state-preserving release installer semantics after building locally.
+
+A Windows Hello PIN is sufficient; biometric hardware is not required.
## Completion
@@ -41,7 +56,10 @@ Show:
- installed skill version/path;
- config path;
- stored and effective protection mode;
-- authority-host readiness when applicable;
+- Authority-host version/status and stable relation when applicable;
+- whether the effective mode requires Authority;
- any unresolved prerequisites or diagnostics.
+`doctor` is read-only and reports the skill and Authority host separately. Authority relations distinguish `missing`, `legacy`, `update`, `already_current`, and `already_ahead` where applicable.
+
Do not perform unrelated GitHub mutations as part of setup/configuration.
diff --git a/scripts/build-authority-host-release.mjs b/scripts/build-authority-host-release.mjs
new file mode 100644
index 00000000..0f502cc5
--- /dev/null
+++ b/scripts/build-authority-host-release.mjs
@@ -0,0 +1,206 @@
+#!/usr/bin/env node
+import { createHash } from "node:crypto";
+import {
+ existsSync,
+ lstatSync,
+ mkdirSync,
+ readFileSync,
+ readdirSync,
+ writeFileSync,
+} from "node:fs";
+import { join, resolve, sep } from "node:path";
+import { pathToFileURL } from "node:url";
+
+const FIXED_ZIP_DATE = 33;
+const FIXED_ZIP_TIME = 0;
+const ROOT = "GitHubDeliveryAuthority/";
+
+function fail(code, detail = "") {
+ throw new Error(detail ? `${code}:${detail}` : code);
+}
+
+function sha256(value) {
+ return createHash("sha256").update(value).digest("hex");
+}
+
+function crc32(buffer) {
+ let crc = 0xffffffff;
+ for (const byte of buffer) {
+ crc ^= byte;
+ for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
+ }
+ return (crc ^ 0xffffffff) >>> 0;
+}
+
+function toPosix(path) {
+ return path.split(sep).join("/");
+}
+
+function validateVersion(version) {
+ if (!/^\d+\.\d+\.\d+$/.test(String(version || ""))) fail("authority_host_release_version_invalid");
+ return String(version);
+}
+
+function validateCommit(sourceCommit) {
+ if (!/^[0-9a-f]{40}$/i.test(String(sourceCommit || ""))) fail("authority_host_release_source_commit_invalid");
+ return String(sourceCommit).toLowerCase();
+}
+
+function walkRegularFiles(root, current, output) {
+ const absolute = join(root, current);
+ const info = lstatSync(absolute);
+ if (info.isSymbolicLink()) fail("authority_host_release_symlink_forbidden", current);
+ if (info.isFile()) {
+ output.push(toPosix(current));
+ return;
+ }
+ if (!info.isDirectory()) fail("authority_host_release_file_type_invalid", current);
+ for (const entry of readdirSync(absolute).sort()) walkRegularFiles(root, join(current, entry), output);
+}
+
+function collectFiles(publishDir) {
+ if (!existsSync(publishDir)) fail("authority_host_release_publish_dir_missing");
+ const files = [];
+ for (const entry of readdirSync(publishDir).sort()) walkRegularFiles(publishDir, entry, files);
+ if (files.length === 0) fail("authority_host_release_publish_dir_empty");
+ if (!files.includes("GitHubDeliveryAuthority.exe")) fail("authority_host_release_executable_missing");
+ if (files.includes("authority-host-version.json")) fail("authority_host_release_reserved_file_present");
+ return files;
+}
+
+function storedZip(entries) {
+ const locals = [];
+ const centrals = [];
+ let offset = 0;
+ for (const entry of entries) {
+ const name = Buffer.from(entry.path, "utf8");
+ const data = entry.content;
+ const checksum = crc32(data);
+ const local = Buffer.alloc(30);
+ local.writeUInt32LE(0x04034b50, 0);
+ local.writeUInt16LE(20, 4);
+ local.writeUInt16LE(0x0800, 6);
+ local.writeUInt16LE(0, 8);
+ local.writeUInt16LE(FIXED_ZIP_TIME, 10);
+ local.writeUInt16LE(FIXED_ZIP_DATE, 12);
+ local.writeUInt32LE(checksum, 14);
+ local.writeUInt32LE(data.length, 18);
+ local.writeUInt32LE(data.length, 22);
+ local.writeUInt16LE(name.length, 26);
+ local.writeUInt16LE(0, 28);
+ locals.push(local, name, data);
+
+ const central = Buffer.alloc(46);
+ central.writeUInt32LE(0x02014b50, 0);
+ central.writeUInt16LE(0x0314, 4);
+ central.writeUInt16LE(20, 6);
+ central.writeUInt16LE(0x0800, 8);
+ central.writeUInt16LE(0, 10);
+ central.writeUInt16LE(FIXED_ZIP_TIME, 12);
+ central.writeUInt16LE(FIXED_ZIP_DATE, 14);
+ central.writeUInt32LE(checksum, 16);
+ central.writeUInt32LE(data.length, 20);
+ central.writeUInt32LE(data.length, 24);
+ central.writeUInt16LE(name.length, 28);
+ central.writeUInt16LE(0, 30);
+ central.writeUInt16LE(0, 32);
+ central.writeUInt16LE(0, 34);
+ central.writeUInt16LE(0, 36);
+ central.writeUInt32LE((0o644 << 16) >>> 0, 38);
+ central.writeUInt32LE(offset, 42);
+ centrals.push(central, name);
+ offset += local.length + name.length + data.length;
+ }
+
+ const centralBuffer = Buffer.concat(centrals);
+ const end = Buffer.alloc(22);
+ end.writeUInt32LE(0x06054b50, 0);
+ end.writeUInt16LE(0, 4);
+ end.writeUInt16LE(0, 6);
+ end.writeUInt16LE(entries.length, 8);
+ end.writeUInt16LE(entries.length, 10);
+ end.writeUInt32LE(centralBuffer.length, 12);
+ end.writeUInt32LE(offset, 16);
+ end.writeUInt16LE(0, 20);
+ return Buffer.concat([...locals, centralBuffer, end]);
+}
+
+export function buildAuthorityHostRelease({ publishDir, outDir, version, sourceCommit } = {}) {
+ publishDir = resolve(String(publishDir || ""));
+ outDir = resolve(String(outDir || ""));
+ version = validateVersion(version);
+ sourceCommit = validateCommit(sourceCommit);
+ const files = collectFiles(publishDir);
+
+ const versionInfo = {
+ schemaVersion: 1,
+ kind: "github-delivery/authority-host-version",
+ version,
+ sourceCommit,
+ platform: "win32",
+ arch: "x64",
+ };
+ const versionBytes = Buffer.from(`${JSON.stringify(versionInfo, null, 2)}\n`, "utf8");
+ const payload = files.map((path) => ({
+ path,
+ content: readFileSync(join(publishDir, ...path.split("/"))),
+ }));
+ payload.push({ path: "authority-host-version.json", content: versionBytes });
+ payload.sort((left, right) => left.path.localeCompare(right.path));
+
+ const entries = payload.map((entry) => ({
+ path: `${ROOT}${entry.path}`,
+ content: entry.content,
+ }));
+ const archive = storedZip(entries);
+ const archiveName = `github-delivery-authority-v${version}-win-x64.zip`;
+ const metadataName = `github-delivery-authority-v${version}-win-x64.json`;
+ const metadata = {
+ schemaVersion: 1,
+ kind: "github-delivery/authority-host-release",
+ version,
+ sourceCommit,
+ platform: "win32",
+ arch: "x64",
+ archive: archiveName,
+ sha256: sha256(archive),
+ files: payload.map((entry) => ({
+ path: entry.path,
+ bytes: entry.content.length,
+ sha256: sha256(entry.content),
+ })),
+ };
+
+ mkdirSync(outDir, { recursive: true });
+ const archivePath = join(outDir, archiveName);
+ const metadataPath = join(outDir, metadataName);
+ writeFileSync(archivePath, archive);
+ writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
+ return { archivePath, metadataPath, metadata };
+}
+
+function parseArgs(argv) {
+ const options = {};
+ for (let index = 0; index < argv.length; index += 1) {
+ const arg = argv[index];
+ if (arg === "--publish-dir") options.publishDir = argv[++index];
+ else if (arg === "--out-dir") options.outDir = argv[++index];
+ else if (arg === "--version") options.version = argv[++index];
+ else if (arg === "--source-commit") options.sourceCommit = argv[++index];
+ else fail("authority_host_release_argument_unknown", arg);
+ }
+ if (!options.publishDir || !options.outDir || !options.version || !options.sourceCommit) {
+ fail("authority_host_release_arguments_required");
+ }
+ return options;
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ try {
+ const result = buildAuthorityHostRelease(parseArgs(process.argv.slice(2)));
+ process.stdout.write(`${JSON.stringify(result.metadata, null, 2)}\n`);
+ } catch (error) {
+ process.stderr.write(`${JSON.stringify({ error: String(error?.message || error) })}\n`);
+ process.exitCode = 1;
+ }
+}
diff --git a/scripts/install-skill.mjs b/scripts/install-skill.mjs
index b4ead7b5..ef11c69b 100755
--- a/scripts/install-skill.mjs
+++ b/scripts/install-skill.mjs
@@ -7,12 +7,17 @@ import { pathToFileURL } from "node:url";
import { installCodexWatchdogHooks } from "./install-codex-watchdog-hooks.mjs";
import { applyInstallation, planInstallation, restoreBackup } from "./lib/distribution.mjs";
+import {
+ planAuthorityHostUpdate,
+ readInstalledAuthorityHost,
+ reconcileStableAuthorityHost,
+} from "./lib/authority-host-install.mjs";
import { prepareVerifiedReleaseCandidate } from "./lib/release-self-update.mjs";
import {
compareInstalledManifest,
readInstalledManifest,
} from "./lib/stable-release-update.mjs";
-import { readUserConfig } from "./lib/user-config.mjs";
+import { readUserConfig, resolveAuthorityMode } from "./lib/user-config.mjs";
import {
selectWatchdogMode,
writeActivationReceipt,
@@ -221,6 +226,27 @@ function sameUserConfig(before, after) {
return isDeepStrictEqual(before?.config, after?.config);
}
+function planAuthorityForRelease(candidate, dependencies = {}) {
+ const readConfig = dependencies.readUserConfig || readUserConfig;
+ const readAuthority = dependencies.readInstalledAuthorityHost || readInstalledAuthorityHost;
+ const planAuthority = dependencies.planAuthorityHostUpdate || planAuthorityHostUpdate;
+ const installed = readAuthority();
+ if (!installed?.supported) {
+ return planAuthority({
+ mode: "off",
+ targetVersion: candidate.release.version,
+ installed,
+ });
+ }
+ const config = readConfig();
+ const mode = resolveAuthorityMode({ config: config.config, env: process.env });
+ return planAuthority({
+ mode,
+ targetVersion: candidate.release.version,
+ installed,
+ });
+}
+
export async function runInstallCommand(options, dependencies = {}) {
const install = dependencies.installSkill || installSkill;
if (!options.update) return install(options);
@@ -230,6 +256,7 @@ export async function runInstallCommand(options, dependencies = {}) {
const removeWorkspace = dependencies.removeWorkspace || removeReleaseUpdateWorkspace;
const readConfig = dependencies.readUserConfig || readUserConfig;
const verifyRelease = dependencies.verifyInstalledRelease || verifyInstalledRelease;
+ const reconcileAuthority = dependencies.reconcileStableAuthorityHost || reconcileStableAuthorityHost;
const workspace = makeWorkspace();
let installation = null;
@@ -242,6 +269,18 @@ export async function runInstallCommand(options, dependencies = {}) {
throw new Error("stable_release_candidate_invalid");
}
+ if (candidate.plan.action === "already_ahead") {
+ return {
+ ...candidate.plan,
+ apply: Boolean(options.apply),
+ updated: false,
+ verified: true,
+ release: candidate.release,
+ authorityHost: { action: "skipped_skill_ahead", changed: false },
+ };
+ }
+
+ const authorityPlan = planAuthorityForRelease(candidate, dependencies);
if (!options.apply) {
return {
...candidate.plan,
@@ -249,16 +288,24 @@ export async function runInstallCommand(options, dependencies = {}) {
updated: false,
verified: true,
release: candidate.release,
+ authorityHost: authorityPlan,
};
}
- if (candidate.plan.action === "already_current" || candidate.plan.action === "already_ahead") {
+ if (candidate.plan.action === "already_current") {
+ const authorityHost = await reconcileAuthority({
+ expectedRelease: candidate.release,
+ scriptPath: join(options.target, "authority-host", "windows", "install-release.ps1"),
+ });
+ const authorityUpdated = authorityHost?.changed === true;
return {
...candidate.plan,
+ action: authorityUpdated ? "update" : candidate.plan.action,
apply: true,
- updated: false,
+ updated: authorityUpdated,
verified: true,
release: candidate.release,
+ authorityHost,
};
}
if (candidate.plan.action !== "update" || candidate.plan.safeToReplace !== true) {
@@ -281,6 +328,11 @@ export async function runInstallCommand(options, dependencies = {}) {
throw new Error("stable_update_user_config_changed_unexpectedly");
}
+ const authorityHost = await reconcileAuthority({
+ expectedRelease: candidate.release,
+ scriptPath: join(options.target, "authority-host", "windows", "install-release.ps1"),
+ });
+
return {
action: "update",
apply: true,
@@ -292,6 +344,7 @@ export async function runInstallCommand(options, dependencies = {}) {
backupPath: installation?.backupPath || null,
release: candidate.release,
watchdog: installation?.watchdog || null,
+ authorityHost,
};
} catch (error) {
if (installation?.backupPath && error && typeof error === "object") {
diff --git a/scripts/lib/authority-host-install.mjs b/scripts/lib/authority-host-install.mjs
new file mode 100644
index 00000000..bf60499c
--- /dev/null
+++ b/scripts/lib/authority-host-install.mjs
@@ -0,0 +1,237 @@
+import { spawnSync } from "node:child_process";
+import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
+import { homedir, tmpdir } from "node:os";
+import { dirname, join, resolve, win32 as win32Path } from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { acquireVerifiedAuthorityHostPayload } from "./authority-host-release.mjs";
+import { createGitHubReleaseClient } from "./release-self-update.mjs";
+import { compareStableVersions } from "./stable-release-update.mjs";
+import { readUserConfig, resolveAuthorityMode } from "./user-config.mjs";
+
+function fail(code, detail = "") {
+ throw new Error(detail ? `${code}:${detail}` : code);
+}
+
+function validExpectedRelease(value) {
+ return Boolean(
+ value &&
+ /^v\d+\.\d+\.\d+$/.test(String(value.tag || "")) &&
+ /^\d+\.\d+\.\d+$/.test(String(value.version || "")) &&
+ value.tag === `v${value.version}` &&
+ /^[0-9a-f]{40}$/i.test(String(value.sourceCommit || "")),
+ );
+}
+
+export function authorityHostInstallRoot({
+ platform = process.platform,
+ env = process.env,
+ home = homedir(),
+} = {}) {
+ if (platform !== "win32") return null;
+ const local = env.LOCALAPPDATA || win32Path.join(home, "AppData", "Local");
+ return win32Path.join(local, "GitHubDeliveryAuthority");
+}
+
+function normalizeInstallRecord(value) {
+ if (
+ !value || typeof value !== "object" || Array.isArray(value) ||
+ value.schemaVersion !== 1 || value.kind !== "github-delivery/authority-host-install" ||
+ !/^\d+\.\d+\.\d+$/.test(String(value.version || "")) ||
+ !/^[0-9a-f]{40}$/i.test(String(value.sourceCommit || "")) ||
+ typeof value.appDir !== "string" || !/^app\/v\d+\.\d+\.\d+$/.test(value.appDir)
+ ) fail("authority_host_install_record_invalid");
+ return {
+ schemaVersion: 1,
+ kind: value.kind,
+ version: value.version,
+ sourceCommit: value.sourceCommit.toLowerCase(),
+ appDir: value.appDir,
+ installedAt: typeof value.installedAt === "string" ? value.installedAt : null,
+ };
+}
+
+export function readInstalledAuthorityHost({
+ platform = process.platform,
+ env = process.env,
+ home = homedir(),
+ exists = existsSync,
+ readFile = readFileSync,
+} = {}) {
+ const root = authorityHostInstallRoot({ platform, env, home });
+ if (!root) return { supported: false, configured: false, installed: false, legacy: false, root: null, version: null, sourceCommit: null };
+ const recordPath = win32Path.join(root, "authority-host-install.json");
+ if (exists(recordPath)) {
+ let record;
+ try { record = normalizeInstallRecord(JSON.parse(readFile(recordPath, "utf8"))); }
+ catch (error) {
+ if (String(error?.message || "").startsWith("authority_host_install_record_invalid")) throw error;
+ fail("authority_host_install_record_invalid");
+ }
+ const exePath = win32Path.join(root, ...record.appDir.split("/"), "GitHubDeliveryAuthority.exe");
+ return {
+ supported: true,
+ configured: true,
+ installed: exists(exePath),
+ legacy: false,
+ root,
+ recordPath,
+ exePath,
+ version: record.version,
+ sourceCommit: record.sourceCommit,
+ record,
+ };
+ }
+
+ const legacyExe = win32Path.join(root, "GitHubDeliveryAuthority.exe");
+ if (exists(legacyExe)) {
+ return {
+ supported: true,
+ configured: true,
+ installed: true,
+ legacy: true,
+ root,
+ recordPath,
+ exePath: legacyExe,
+ version: null,
+ sourceCommit: null,
+ record: null,
+ };
+ }
+ return { supported: true, configured: false, installed: false, legacy: false, root, recordPath, exePath: null, version: null, sourceCommit: null, record: null };
+}
+
+export function planAuthorityHostUpdate({ mode, targetVersion, installed } = {}) {
+ if (!installed?.supported) return { action: "unsupported", required: false, currentVersion: null, targetVersion: targetVersion || null };
+ if (!/^\d+\.\d+\.\d+$/.test(String(targetVersion || ""))) fail("authority_host_target_version_invalid");
+ if (!installed.installed && installed.configured) {
+ return { action: "repair", required: true, currentVersion: installed.version || null, targetVersion };
+ }
+ if (!installed.installed && mode === "off") {
+ return { action: "disabled", required: false, currentVersion: null, targetVersion };
+ }
+ if (!installed.installed) return { action: "install", required: true, currentVersion: null, targetVersion };
+ if (installed.legacy || !installed.version) return { action: "upgrade_legacy", required: true, currentVersion: null, targetVersion };
+ const comparison = compareStableVersions(installed.version, targetVersion);
+ if (comparison > 0) return { action: "already_ahead", required: false, currentVersion: installed.version, targetVersion };
+ if (comparison === 0) return { action: "already_current", required: false, currentVersion: installed.version, targetVersion };
+ return { action: "update", required: true, currentVersion: installed.version, targetVersion };
+}
+
+function defaultInstallScript() {
+ return resolve(dirname(fileURLToPath(import.meta.url)), "../../authority-host/windows/install-release.ps1");
+}
+
+export function installVerifiedAuthorityHost({
+ payload,
+ scriptPath = defaultInstallScript(),
+ runner = spawnSync,
+} = {}) {
+ if (!payload?.verified || payload?.kind !== "github-delivery/verified-authority-host-payload") fail("authority_host_verified_payload_required");
+ const result = runner(
+ "powershell.exe",
+ [
+ "-NoLogo",
+ "-NoProfile",
+ "-NonInteractive",
+ "-ExecutionPolicy", "Bypass",
+ "-File", scriptPath,
+ "-SourceDir", payload.source,
+ "-ExpectedVersion", payload.metadata.version,
+ "-ExpectedSourceCommit", payload.metadata.sourceCommit,
+ ],
+ {
+ encoding: "utf8",
+ windowsHide: true,
+ stdio: "pipe",
+ shell: false,
+ },
+ );
+ if (!result || result.error || result.status !== 0) {
+ fail("authority_host_install_failed", result?.stderr || result?.error?.message || "install-release.ps1 failed");
+ }
+ return { installed: true, version: payload.metadata.version, sourceCommit: payload.metadata.sourceCommit, stdout: result.stdout || "" };
+}
+
+function makeWorkspace() {
+ return mkdtempSync(join(tmpdir(), "github-delivery-authority-update-"));
+}
+
+export async function resolveLatestAuthorityExpectedRelease(client = createGitHubReleaseClient()) {
+ const release = await client.latestRelease();
+ const tag = String(release?.tag_name || "");
+ const match = /^v(\d+\.\d+\.\d+)$/.exec(tag);
+ if (!match) fail("authority_host_release_tag_invalid");
+ const sourceCommit = await client.resolveTagCommit(tag);
+ if (!/^[0-9a-f]{40}$/i.test(String(sourceCommit || ""))) fail("authority_host_release_source_commit_invalid");
+ return {
+ expectedRelease: { tag, version: match[1], sourceCommit: sourceCommit.toLowerCase() },
+ release,
+ };
+}
+
+export async function reconcileStableAuthorityHost({
+ expectedRelease = null,
+ platform = process.platform,
+ env = process.env,
+ home = homedir(),
+ client = createGitHubReleaseClient(),
+ attestationRunner = undefined,
+ installRunner = spawnSync,
+ scriptPath = defaultInstallScript(),
+ dependencies = {},
+} = {}) {
+ const readInstalled = dependencies.readInstalledAuthorityHost || readInstalledAuthorityHost;
+ const readConfig = dependencies.readUserConfig || readUserConfig;
+ const install = dependencies.installVerifiedAuthorityHost || installVerifiedAuthorityHost;
+ const installed = readInstalled({ platform, env, home });
+ const config = readConfig({ platform, env, home });
+ const mode = resolveAuthorityMode({ config: config.config, env });
+ if (!installed.supported) {
+ return {
+ action: "unsupported",
+ required: mode === "high-assurance" || mode === "all",
+ changed: false,
+ installed,
+ mode,
+ };
+ }
+
+ if (!installed.installed && !installed.configured && mode === "off") {
+ return { action: "disabled", required: false, changed: false, installed, mode, currentVersion: null, targetVersion: expectedRelease?.version || null };
+ }
+
+ let releaseMetadata = null;
+ if (expectedRelease !== null && !validExpectedRelease(expectedRelease)) fail("authority_host_expected_release_invalid");
+ if (expectedRelease === null) {
+ const resolved = await (dependencies.resolveLatestAuthorityExpectedRelease || resolveLatestAuthorityExpectedRelease)(client);
+ expectedRelease = resolved.expectedRelease;
+ releaseMetadata = resolved.release;
+ }
+
+ const plan = planAuthorityHostUpdate({ mode, targetVersion: expectedRelease.version, installed });
+ if (!plan.required) return { ...plan, changed: false, installed, mode };
+
+ if (!releaseMetadata) releaseMetadata = await client.latestRelease();
+ if (releaseMetadata?.tag_name !== expectedRelease.tag) fail("authority_host_release_changed_during_update");
+ const workspace = (dependencies.makeWorkspace || makeWorkspace)();
+ try {
+ const acquire = dependencies.acquireVerifiedAuthorityHostPayload || acquireVerifiedAuthorityHostPayload;
+ const payload = await acquire({
+ release: releaseMetadata,
+ workspace,
+ client,
+ expectedVersion: expectedRelease.version,
+ expectedSourceCommit: expectedRelease.sourceCommit,
+ attestationRunner,
+ });
+ const result = install({ payload, runner: installRunner, scriptPath });
+ const after = readInstalled({ platform, env, home });
+ if (!after.installed || after.version !== expectedRelease.version || after.sourceCommit !== expectedRelease.sourceCommit.toLowerCase()) {
+ fail("authority_host_postinstall_verification_failed");
+ }
+ return { ...plan, changed: true, installed: after, mode, result };
+ } finally {
+ (dependencies.removeWorkspace || ((path) => rmSync(path, { recursive: true, force: true })))(workspace);
+ }
+}
diff --git a/scripts/lib/authority-host-release.mjs b/scripts/lib/authority-host-release.mjs
new file mode 100644
index 00000000..3965045e
--- /dev/null
+++ b/scripts/lib/authority-host-release.mjs
@@ -0,0 +1,321 @@
+import { createHash, timingSafeEqual } from "node:crypto";
+import { existsSync, lstatSync, mkdirSync, writeFileSync } from "node:fs";
+import { join, resolve } from "node:path";
+import { TextDecoder } from "node:util";
+
+import {
+ verifyGitHubAssetDigest,
+ verifyReleaseAttestation,
+} from "./release-self-update.mjs";
+
+const ROOT = "GitHubDeliveryAuthority/";
+const UTF8_FLAG = 0x0800;
+const ENCRYPTED_FLAG = 0x0001;
+const DATA_DESCRIPTOR_FLAG = 0x0008;
+const STORED_METHOD = 0;
+const EOCD_SIGNATURE = 0x06054b50;
+const CENTRAL_SIGNATURE = 0x02014b50;
+const LOCAL_SIGNATURE = 0x04034b50;
+const decoder = new TextDecoder("utf-8", { fatal: true });
+const LIMITS = Object.freeze({
+ archive: 384 * 1024 * 1024,
+ metadata: 4 * 1024 * 1024,
+ file: 192 * 1024 * 1024,
+ total: 768 * 1024 * 1024,
+ files: 4096,
+});
+
+function fail(code, detail = "") {
+ throw new Error(detail ? `${code}:${detail}` : code);
+}
+
+function sha256(value) {
+ return createHash("sha256").update(value).digest("hex");
+}
+
+function crc32(buffer) {
+ let crc = 0xffffffff;
+ for (const byte of buffer) {
+ crc ^= byte;
+ for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
+ }
+ return (crc ^ 0xffffffff) >>> 0;
+}
+
+function isCommit(value) {
+ return typeof value === "string" && /^[0-9a-f]{40}$/i.test(value);
+}
+
+function isVersion(value) {
+ return typeof value === "string" && /^\d+\.\d+\.\d+$/.test(value);
+}
+
+function assetName(version, extension) {
+ return `github-delivery-authority-v${version}-win-x64.${extension}`;
+}
+
+function uniqueAsset(release, name) {
+ const matches = (release?.assets || []).filter((entry) => entry?.name === name);
+ if (matches.length === 0) fail("authority_host_release_asset_missing", name);
+ if (matches.length > 1) fail("authority_host_release_asset_duplicate", name);
+ return matches[0];
+}
+
+function safeRelativePath(path) {
+ if (
+ typeof path !== "string" ||
+ path.length === 0 ||
+ path.includes("\0") ||
+ path.includes("\\") ||
+ path.startsWith("/") ||
+ /^[A-Za-z]:/.test(path)
+ ) fail("authority_host_release_path_invalid", String(path));
+ const segments = path.split("/");
+ if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
+ fail("authority_host_release_path_invalid", path);
+ }
+ return path;
+}
+
+export function validateAuthorityHostReleaseMetadata(value, { version, sourceCommit } = {}) {
+ if (!isVersion(version) || !isCommit(sourceCommit)) fail("authority_host_release_expected_identity_invalid");
+ if (
+ !value || typeof value !== "object" || Array.isArray(value) ||
+ value.schemaVersion !== 1 ||
+ value.kind !== "github-delivery/authority-host-release" ||
+ value.version !== version ||
+ String(value.sourceCommit || "").toLowerCase() !== sourceCommit.toLowerCase() ||
+ value.platform !== "win32" ||
+ value.arch !== "x64" ||
+ value.archive !== assetName(version, "zip") ||
+ !/^[0-9a-f]{64}$/i.test(String(value.sha256 || "")) ||
+ !Array.isArray(value.files) || value.files.length === 0 || value.files.length > LIMITS.files
+ ) fail("authority_host_release_metadata_invalid");
+
+ const seen = new Set();
+ let total = 0;
+ const files = value.files.map((entry) => {
+ const path = safeRelativePath(entry?.path);
+ if (seen.has(path)) fail("authority_host_release_metadata_duplicate_path", path);
+ seen.add(path);
+ if (!Number.isSafeInteger(entry?.bytes) || entry.bytes < 0 || entry.bytes > LIMITS.file) {
+ fail("authority_host_release_metadata_invalid", path);
+ }
+ if (!/^[0-9a-f]{64}$/i.test(String(entry?.sha256 || ""))) fail("authority_host_release_metadata_invalid", path);
+ total += entry.bytes;
+ if (!Number.isSafeInteger(total) || total > LIMITS.total) fail("authority_host_release_total_limit_exceeded");
+ return { path, bytes: entry.bytes, sha256: entry.sha256.toLowerCase() };
+ });
+ if (!seen.has("GitHubDeliveryAuthority.exe") || !seen.has("authority-host-version.json")) {
+ fail("authority_host_release_required_file_missing");
+ }
+ return {
+ schemaVersion: 1,
+ kind: value.kind,
+ version,
+ sourceCommit: sourceCommit.toLowerCase(),
+ platform: "win32",
+ arch: "x64",
+ archive: value.archive,
+ sha256: value.sha256.toLowerCase(),
+ files,
+ };
+}
+
+function decodeName(bytes) {
+ try { return decoder.decode(bytes); }
+ catch { fail("authority_host_release_zip_name_invalid"); }
+}
+
+function findEocd(archive) {
+ const minimum = Math.max(0, archive.length - 65_557);
+ for (let offset = archive.length - 22; offset >= minimum; offset -= 1) {
+ if (archive.readUInt32LE(offset) !== EOCD_SIGNATURE) continue;
+ const commentLength = archive.readUInt16LE(offset + 20);
+ if (offset + 22 + commentLength === archive.length) return offset;
+ }
+ fail("authority_host_release_zip_eocd_missing");
+}
+
+function parseZip(archive) {
+ if (!Buffer.isBuffer(archive) || archive.length < 22 || archive.length > LIMITS.archive) {
+ fail("authority_host_release_zip_invalid");
+ }
+ const eocd = findEocd(archive);
+ const disk = archive.readUInt16LE(eocd + 4);
+ const centralDisk = archive.readUInt16LE(eocd + 6);
+ const entriesOnDisk = archive.readUInt16LE(eocd + 8);
+ const entryCount = archive.readUInt16LE(eocd + 10);
+ const centralSize = archive.readUInt32LE(eocd + 12);
+ const centralOffset = archive.readUInt32LE(eocd + 16);
+ if (disk !== 0 || centralDisk !== 0 || entriesOnDisk !== entryCount || entryCount === 0 || entryCount > LIMITS.files) {
+ fail("authority_host_release_zip_structure_invalid");
+ }
+ if (entryCount === 0xffff || centralSize === 0xffffffff || centralOffset === 0xffffffff || centralOffset + centralSize !== eocd) {
+ fail("authority_host_release_zip_structure_invalid");
+ }
+
+ const entries = [];
+ const seen = new Set();
+ let cursor = centralOffset;
+ let total = 0;
+ for (let index = 0; index < entryCount; index += 1) {
+ if (cursor + 46 > eocd || archive.readUInt32LE(cursor) !== CENTRAL_SIGNATURE) fail("authority_host_release_zip_central_invalid");
+ const flags = archive.readUInt16LE(cursor + 8);
+ const method = archive.readUInt16LE(cursor + 10);
+ const checksum = archive.readUInt32LE(cursor + 16);
+ const compressedBytes = archive.readUInt32LE(cursor + 20);
+ const bytes = archive.readUInt32LE(cursor + 24);
+ const nameLength = archive.readUInt16LE(cursor + 28);
+ const extraLength = archive.readUInt16LE(cursor + 30);
+ const commentLength = archive.readUInt16LE(cursor + 32);
+ const diskStart = archive.readUInt16LE(cursor + 34);
+ const localOffset = archive.readUInt32LE(cursor + 42);
+ const end = cursor + 46 + nameLength + extraLength + commentLength;
+ if (end > eocd || diskStart !== 0 || (flags & ENCRYPTED_FLAG) !== 0 || (flags & DATA_DESCRIPTOR_FLAG) !== 0 || (flags & UTF8_FLAG) === 0) {
+ fail("authority_host_release_zip_entry_invalid");
+ }
+ if (method !== STORED_METHOD || compressedBytes !== bytes || extraLength !== 0 || commentLength !== 0 || bytes > LIMITS.file) {
+ fail("authority_host_release_zip_entry_invalid");
+ }
+ total += bytes;
+ if (!Number.isSafeInteger(total) || total > LIMITS.total) fail("authority_host_release_total_limit_exceeded");
+
+ const nameBytes = archive.subarray(cursor + 46, cursor + 46 + nameLength);
+ const fullPath = decodeName(nameBytes);
+ if (!fullPath.startsWith(ROOT) || fullPath === ROOT.slice(0, -1)) fail("authority_host_release_zip_path_invalid", fullPath);
+ const relativePath = safeRelativePath(fullPath.slice(ROOT.length));
+ if (seen.has(relativePath)) fail("authority_host_release_zip_duplicate_path", relativePath);
+ seen.add(relativePath);
+
+ if (localOffset + 30 > centralOffset || archive.readUInt32LE(localOffset) !== LOCAL_SIGNATURE) fail("authority_host_release_zip_local_invalid", relativePath);
+ const localFlags = archive.readUInt16LE(localOffset + 6);
+ const localMethod = archive.readUInt16LE(localOffset + 8);
+ const localChecksum = archive.readUInt32LE(localOffset + 14);
+ const localCompressed = archive.readUInt32LE(localOffset + 18);
+ const localBytes = archive.readUInt32LE(localOffset + 22);
+ const localNameLength = archive.readUInt16LE(localOffset + 26);
+ const localExtraLength = archive.readUInt16LE(localOffset + 28);
+ if (localFlags !== flags || localMethod !== method || localChecksum !== checksum || localCompressed !== compressedBytes || localBytes !== bytes || localExtraLength !== 0) {
+ fail("authority_host_release_zip_local_mismatch", relativePath);
+ }
+ const localNameStart = localOffset + 30;
+ const localNameEnd = localNameStart + localNameLength;
+ if (localNameEnd > centralOffset || !archive.subarray(localNameStart, localNameEnd).equals(nameBytes)) {
+ fail("authority_host_release_zip_local_mismatch", relativePath);
+ }
+ const dataOffset = localNameEnd;
+ const dataEnd = dataOffset + bytes;
+ if (dataEnd > centralOffset) fail("authority_host_release_zip_bounds_invalid", relativePath);
+ const content = archive.subarray(dataOffset, dataEnd);
+ if (crc32(content) !== checksum) fail("authority_host_release_zip_crc_mismatch", relativePath);
+ entries.push({ relativePath, content });
+ cursor = end;
+ }
+ if (cursor !== eocd) fail("authority_host_release_zip_central_invalid");
+ return entries;
+}
+
+export function extractVerifiedAuthorityHostZip({ archive, metadata, destination } = {}) {
+ const entries = parseZip(archive);
+ const expected = new Map(metadata.files.map((entry) => [entry.path, entry]));
+ if (entries.length !== expected.size) fail("authority_host_release_zip_file_set_mismatch");
+ for (const entry of entries) {
+ const declared = expected.get(entry.relativePath);
+ if (!declared || entry.content.length !== declared.bytes || sha256(entry.content) !== declared.sha256) {
+ fail("authority_host_release_zip_file_mismatch", entry.relativePath);
+ }
+ }
+
+ destination = resolve(String(destination || ""));
+ if (existsSync(destination) && lstatSync(destination).isSymbolicLink()) fail("authority_host_release_destination_unsafe");
+ const root = join(destination, "GitHubDeliveryAuthority");
+ if (existsSync(root)) fail("authority_host_release_destination_exists");
+ mkdirSync(root, { recursive: true, mode: 0o700 });
+ for (const entry of entries) {
+ const segments = entry.relativePath.split("/");
+ const output = join(root, ...segments);
+ if (segments.length > 1) mkdirSync(join(root, ...segments.slice(0, -1)), { recursive: true, mode: 0o700 });
+ writeFileSync(output, entry.content, { mode: 0o600, flag: "wx" });
+ }
+
+ const embedded = JSON.parse(entries.find((entry) => entry.relativePath === "authority-host-version.json").content.toString("utf8"));
+ if (
+ embedded?.schemaVersion !== 1 || embedded?.kind !== "github-delivery/authority-host-version" ||
+ embedded?.version !== metadata.version || String(embedded?.sourceCommit || "").toLowerCase() !== metadata.sourceCommit ||
+ embedded?.platform !== "win32" || embedded?.arch !== "x64"
+ ) fail("authority_host_release_embedded_version_invalid");
+ return { root, files: entries.map((entry) => entry.relativePath).sort() };
+}
+
+function parseMetadata(bytes, expected) {
+ let parsed;
+ try { parsed = JSON.parse(bytes.toString("utf8")); }
+ catch { fail("authority_host_release_metadata_invalid_json"); }
+ return validateAuthorityHostReleaseMetadata(parsed, expected);
+}
+
+function equalHash(leftHex, rightHex) {
+ const left = Buffer.from(leftHex, "hex");
+ const right = Buffer.from(rightHex, "hex");
+ return left.length === right.length && timingSafeEqual(left, right);
+}
+
+export async function acquireVerifiedAuthorityHostPayload({
+ release,
+ workspace,
+ client,
+ expectedVersion,
+ expectedSourceCommit,
+ attestationRunner = undefined,
+} = {}) {
+ if (!client || typeof client.downloadAsset !== "function" || typeof client.resolveTagCommit !== "function") fail("authority_host_release_client_invalid");
+ if (!release || release.draft !== false || release.prerelease !== false || release.tag_name !== `v${expectedVersion}`) fail("authority_host_release_identity_invalid");
+ if (!isVersion(expectedVersion) || !isCommit(expectedSourceCommit)) fail("authority_host_release_expected_identity_invalid");
+
+ const metadataAsset = uniqueAsset(release, assetName(expectedVersion, "json"));
+ const archiveAsset = uniqueAsset(release, assetName(expectedVersion, "zip"));
+ const metadataBytes = await client.downloadAsset(metadataAsset, LIMITS.metadata);
+ if (!Buffer.isBuffer(metadataBytes)) fail("authority_host_release_download_invalid");
+ verifyGitHubAssetDigest(metadataAsset, metadataBytes);
+
+ const resolvedCommit = await client.resolveTagCommit(release.tag_name);
+ if (resolvedCommit.toLowerCase() !== expectedSourceCommit.toLowerCase()) fail("authority_host_release_source_commit_mismatch");
+ const metadata = parseMetadata(metadataBytes, { version: expectedVersion, sourceCommit: expectedSourceCommit });
+
+ const archive = await client.downloadAsset(archiveAsset, LIMITS.archive);
+ if (!Buffer.isBuffer(archive)) fail("authority_host_release_download_invalid");
+ verifyGitHubAssetDigest(archiveAsset, archive);
+ const actualHash = sha256(archive);
+ if (!equalHash(actualHash, metadata.sha256)) fail("authority_host_release_archive_digest_mismatch");
+
+ const root = resolve(String(workspace || ""));
+ const downloads = join(root, "authority-downloads");
+ const extraction = join(root, "authority-extracted");
+ mkdirSync(downloads, { recursive: true, mode: 0o700 });
+ mkdirSync(extraction, { recursive: true, mode: 0o700 });
+ const archivePath = join(downloads, metadata.archive);
+ writeFileSync(archivePath, archive, { mode: 0o600, flag: "wx" });
+ verifyReleaseAttestation({
+ archivePath,
+ tag: release.tag_name,
+ sourceCommit: expectedSourceCommit,
+ ...(attestationRunner ? { runner: attestationRunner } : {}),
+ });
+ const extracted = extractVerifiedAuthorityHostZip({ archive, metadata, destination: extraction });
+ return {
+ schemaVersion: 1,
+ kind: "github-delivery/verified-authority-host-payload",
+ verified: true,
+ source: extracted.root,
+ archivePath,
+ metadata,
+ release: {
+ tag: release.tag_name,
+ version: expectedVersion,
+ sourceCommit: expectedSourceCommit.toLowerCase(),
+ },
+ };
+}
+
+export const authorityHostReleaseDefaults = Object.freeze({ root: ROOT, limits: LIMITS });
diff --git a/scripts/lib/bootstrap-install.mjs b/scripts/lib/bootstrap-install.mjs
index 3310b899..db1f961e 100644
--- a/scripts/lib/bootstrap-install.mjs
+++ b/scripts/lib/bootstrap-install.mjs
@@ -5,6 +5,7 @@ import { isDeepStrictEqual } from "node:util";
import { createInterface } from "node:readline/promises";
import { installSkill, parseInstallArgs } from "../install-skill.mjs";
+import { reconcileStableAuthorityHost } from "./authority-host-install.mjs";
import { acquireVerifiedReleasePayload } from "./release-self-update.mjs";
import {
compareInstalledManifest,
@@ -54,7 +55,9 @@ function validPayload(payload) {
&& payload.source.length > 0
&& payload.manifest?.kind === "github-delivery/distribution-manifest"
&& payload.manifest?.name === "github-delivery"
- && typeof payload.release?.version === "string",
+ && typeof payload.release?.version === "string"
+ && typeof payload.release?.tag === "string"
+ && typeof payload.release?.sourceCommit === "string",
);
}
@@ -113,6 +116,7 @@ export async function runGuidedInstall({
const readConfig = dependencies.readUserConfig || readUserConfig;
const verify = dependencies.verifyInstalledRelease || verifyInstalledRelease;
const confirm = dependencies.confirmApply || confirmApply;
+ const reconcileAuthority = dependencies.reconcileStableAuthorityHost || reconcileStableAuthorityHost;
const workspace = make();
let installation = null;
@@ -165,6 +169,11 @@ export async function runGuidedInstall({
fail("stable_install_user_config_changed_unexpectedly");
}
+ const authorityHost = await reconcileAuthority({
+ expectedRelease: payload.release,
+ scriptPath: join(target, "authority-host", "windows", "install-release.ps1"),
+ });
+
return {
action: "install",
apply: true,
@@ -174,6 +183,7 @@ export async function runGuidedInstall({
target,
backupPath: installation?.backupPath || null,
watchdog: installation?.watchdog || null,
+ authorityHost,
};
} catch (error) {
if (installation?.backupPath && error && typeof error === "object") {
diff --git a/scripts/lib/bootstrap-maintenance.mjs b/scripts/lib/bootstrap-maintenance.mjs
index 7f345987..80ca3100 100644
--- a/scripts/lib/bootstrap-maintenance.mjs
+++ b/scripts/lib/bootstrap-maintenance.mjs
@@ -4,6 +4,10 @@ import { pathToFileURL } from "node:url";
import { installCodexWatchdogHooks } from "../install-codex-watchdog-hooks.mjs";
import { parseInstallArgs, runInstallCommand } from "../install-skill.mjs";
+import {
+ readInstalledAuthorityHost,
+ reconcileStableAuthorityHost,
+} from "./authority-host-install.mjs";
import { confirmApply } from "./bootstrap-install.mjs";
import {
checkBootstrapEnvironment,
@@ -15,7 +19,7 @@ import {
compareStableVersions,
readInstalledManifest,
} from "./stable-release-update.mjs";
-import { readUserConfig } from "./user-config.mjs";
+import { readUserConfig, resolveAuthorityMode } from "./user-config.mjs";
import { readActivationReceipt } from "./watchdog-activation.mjs";
function fail(code) {
@@ -39,6 +43,10 @@ function readyReceipt(receipt) {
return receipt.mode === "hooks" && receipt.hookTrustVerified === true;
}
+function modeRequiresAuthority(mode) {
+ return mode === "high-assurance" || mode === "all";
+}
+
function trustGuidance(changed) {
return changed
? "The installed GitHub Delivery hook definition changed. Open /hooks in Codex, review the exact new definition, and trust it before running setup again."
@@ -76,6 +84,13 @@ export async function runBootstrapSetup({
const discover = dependencies.discoverInstallations || discoverInstallations;
requireValidInstallation(target, discover);
+ const reconcileAuthority = dependencies.reconcileStableAuthorityHost || reconcileStableAuthorityHost;
+ const authorityHost = await reconcileAuthority({
+ scriptPath: join(target, "authority-host", "windows", "install-release.ps1"),
+ });
+ if (authorityHost?.action === "unsupported" && authorityHost?.required === true) {
+ fail("bootstrap_setup_authority_host_unsupported");
+ }
const readReceipt = dependencies.readActivationReceipt || readActivationReceipt;
const receipt = readReceipt({ codexHome });
@@ -85,7 +100,8 @@ export async function runBootstrapSetup({
status: "ready",
target,
watchdog: receipt.mode,
- changed: false,
+ changed: authorityHost?.changed === true,
+ authorityHost,
};
}
@@ -101,7 +117,8 @@ export async function runBootstrapSetup({
status: "hook_trust_required",
target,
watchdog: receipt?.mode || "none",
- changed: false,
+ changed: authorityHost?.changed === true,
+ authorityHost,
hookDefinitionChanged: true,
guidance: trustGuidance(true),
};
@@ -118,7 +135,8 @@ export async function runBootstrapSetup({
status: "hook_trust_required",
target,
watchdog: receipt?.mode || "none",
- changed: false,
+ changed: authorityHost?.changed === true,
+ authorityHost,
hookDefinitionChanged: false,
guidance: trustGuidance(false),
};
@@ -146,7 +164,8 @@ export async function runBootstrapSetup({
status: watchdog === "hooks" || watchdog === "stream" ? "ready" : "hook_trust_required",
target,
watchdog,
- changed: result?.watchdog?.receiptChanged === true,
+ changed: result?.watchdog?.receiptChanged === true || authorityHost?.changed === true,
+ authorityHost,
guidance: watchdog === "hooks" || watchdog === "stream" ? null : trustGuidance(false),
result,
};
@@ -169,6 +188,7 @@ export async function runBootstrapDoctor({
const readManifest = dependencies.readInstalledManifest || readInstalledManifest;
const compareManifest = dependencies.compareInstalledManifest || compareInstalledManifest;
const readConfig = dependencies.readUserConfig || readUserConfig;
+ const readAuthority = dependencies.readInstalledAuthorityHost || readInstalledAuthorityHost;
const readReceipt = dependencies.readActivationReceipt || readActivationReceipt;
const latestRelease = dependencies.latestRelease || (() => createGitHubReleaseClient().latestRelease());
@@ -186,7 +206,18 @@ export async function runBootstrapDoctor({
installations: found,
installed: { ok: Boolean(selected), version: selected?.version || null },
integrity: { ok: false, clean: null, modifications: [], error: null },
- config: { ok: false, source: null, error: null },
+ config: { ok: false, source: null, effectiveAuthorityMode: null, error: null },
+ authorityHost: {
+ ok: false,
+ supported: process.platform === "win32",
+ installed: false,
+ legacy: false,
+ version: null,
+ sourceCommit: null,
+ relation: null,
+ requiredByMode: false,
+ error: null,
+ },
activation: readReceipt({ codexHome: resolve(codexHome) }),
latest: { version: null, relation: null, error: null },
};
@@ -209,11 +240,39 @@ export async function runBootstrapDoctor({
try {
const config = readConfig();
- report.config = { ok: true, source: config?.source || null, error: null };
+ const effectiveAuthorityMode = resolveAuthorityMode({ config: config.config, env: process.env });
+ report.config = {
+ ok: true,
+ source: config?.source || null,
+ effectiveAuthorityMode,
+ error: null,
+ };
+ report.authorityHost.requiredByMode = modeRequiresAuthority(effectiveAuthorityMode);
} catch (error) {
report.config.error = String(error?.message || error);
}
+ try {
+ const authority = readAuthority();
+ const requiredByMode = report.authorityHost.requiredByMode;
+ const authorityRelation = authority.supported
+ ? (!authority.installed ? "missing" : (authority.legacy || !authority.version ? "legacy" : null))
+ : null;
+ report.authorityHost = {
+ ok: true,
+ supported: authority.supported,
+ installed: authority.installed,
+ legacy: authority.legacy,
+ version: authority.version,
+ sourceCommit: authority.sourceCommit,
+ relation: authorityRelation,
+ requiredByMode,
+ error: !authority.supported && requiredByMode ? "authority_host_required_unsupported" : null,
+ };
+ } catch (error) {
+ report.authorityHost.error = String(error?.message || error);
+ }
+
try {
const release = await latestRelease();
const tag = String(release?.tag_name || "");
@@ -221,6 +280,15 @@ export async function runBootstrapDoctor({
if (!match) fail("stable_release_tag_invalid");
report.latest.version = match[1];
if (manifest?.version) report.latest.relation = relation(manifest.version, match[1]);
+ if (
+ report.authorityHost.ok &&
+ report.authorityHost.supported &&
+ report.authorityHost.installed &&
+ !report.authorityHost.legacy &&
+ report.authorityHost.version
+ ) {
+ report.authorityHost.relation = relation(report.authorityHost.version, match[1]);
+ }
} catch (error) {
report.latest.error = String(error?.message || error);
}
diff --git a/scripts/validate-npm-package.mjs b/scripts/validate-npm-package.mjs
index a38d5477..3b395e04 100644
--- a/scripts/validate-npm-package.mjs
+++ b/scripts/validate-npm-package.mjs
@@ -14,6 +14,8 @@ const RUNTIME_FILES = [
"scripts/github-delivery-cli.mjs",
"scripts/install-codex-watchdog-hooks.mjs",
"scripts/install-skill.mjs",
+ "scripts/lib/authority-host-install.mjs",
+ "scripts/lib/authority-host-release.mjs",
"scripts/lib/bootstrap-cli.mjs",
"scripts/lib/bootstrap-command.mjs",
"scripts/lib/bootstrap-install.mjs",
diff --git a/tests/unit/authority-host-bootstrap.test.mjs b/tests/unit/authority-host-bootstrap.test.mjs
new file mode 100644
index 00000000..6343a5e7
--- /dev/null
+++ b/tests/unit/authority-host-bootstrap.test.mjs
@@ -0,0 +1,89 @@
+import assert from "node:assert/strict";
+import { resolve } from "node:path";
+import test from "node:test";
+
+import { runBootstrapDoctor } from "../../scripts/lib/bootstrap-maintenance.mjs";
+
+const TARGET = resolve("/tmp/github-delivery-authority-doctor");
+
+function baseDependencies({ mode = "off", authority } = {}) {
+ return {
+ checkBootstrapEnvironment: () => ({ ok: true }),
+ discoverInstallations: () => [{ target: TARGET, valid: true, version: "0.5.1", reason: null }],
+ readInstalledManifest: () => ({
+ schemaVersion: 1,
+ kind: "github-delivery/distribution-manifest",
+ name: "github-delivery",
+ version: "0.5.1",
+ sourceCommit: "a".repeat(40),
+ files: [],
+ }),
+ compareInstalledManifest: () => ({ clean: true, modifications: [] }),
+ readUserConfig: () => ({ source: "file", config: { schemaVersion: 1, authorityMode: mode } }),
+ readInstalledAuthorityHost: () => authority,
+ readActivationReceipt: () => null,
+ latestRelease: async () => ({ tag_name: "v0.5.1", draft: false, prerelease: false, assets: [] }),
+ };
+}
+
+test("doctor reports a required missing Authority host explicitly", async () => {
+ const report = await runBootstrapDoctor({
+ target: TARGET,
+ dependencies: baseDependencies({
+ mode: "high-assurance",
+ authority: {
+ supported: true,
+ configured: false,
+ installed: false,
+ legacy: false,
+ version: null,
+ sourceCommit: null,
+ },
+ }),
+ });
+
+ assert.equal(report.authorityHost.requiredByMode, true);
+ assert.equal(report.authorityHost.relation, "missing");
+ assert.equal(report.authorityHost.error, null);
+});
+
+test("doctor distinguishes a legacy Authority host from a version update", async () => {
+ const report = await runBootstrapDoctor({
+ target: TARGET,
+ dependencies: baseDependencies({
+ mode: "off",
+ authority: {
+ supported: true,
+ configured: true,
+ installed: true,
+ legacy: true,
+ version: null,
+ sourceCommit: null,
+ },
+ }),
+ });
+
+ assert.equal(report.authorityHost.requiredByMode, false);
+ assert.equal(report.authorityHost.relation, "legacy");
+});
+
+test("doctor surfaces an unsupported required Authority host", async () => {
+ const report = await runBootstrapDoctor({
+ target: TARGET,
+ dependencies: baseDependencies({
+ mode: "all",
+ authority: {
+ supported: false,
+ configured: false,
+ installed: false,
+ legacy: false,
+ version: null,
+ sourceCommit: null,
+ },
+ }),
+ });
+
+ assert.equal(report.authorityHost.requiredByMode, true);
+ assert.equal(report.authorityHost.relation, null);
+ assert.equal(report.authorityHost.error, "authority_host_required_unsupported");
+});
diff --git a/tests/unit/authority-host-install.test.mjs b/tests/unit/authority-host-install.test.mjs
new file mode 100644
index 00000000..1b682d89
--- /dev/null
+++ b/tests/unit/authority-host-install.test.mjs
@@ -0,0 +1,133 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+ planAuthorityHostUpdate,
+ readInstalledAuthorityHost,
+ reconcileStableAuthorityHost,
+} from "../../scripts/lib/authority-host-install.mjs";
+
+const winEnv = { LOCALAPPDATA: "C:\\Users\\me\\AppData\\Local" };
+
+function installed(overrides = {}) {
+ return {
+ supported: true,
+ installed: true,
+ legacy: false,
+ version: "0.5.1",
+ sourceCommit: "a".repeat(40),
+ ...overrides,
+ };
+}
+
+test("authority host planning preserves off semantics but upgrades existing legacy hosts", () => {
+ assert.deepEqual(
+ planAuthorityHostUpdate({
+ mode: "off",
+ targetVersion: "0.5.2",
+ installed: { supported: true, installed: false, legacy: false, version: null },
+ }),
+ { action: "disabled", required: false, currentVersion: null, targetVersion: "0.5.2" },
+ );
+ assert.equal(
+ planAuthorityHostUpdate({
+ mode: "off",
+ targetVersion: "0.5.2",
+ installed: installed({ legacy: true, version: null }),
+ }).action,
+ "upgrade_legacy",
+ );
+ assert.equal(
+ planAuthorityHostUpdate({
+ mode: "high-assurance",
+ targetVersion: "0.5.2",
+ installed: { supported: true, installed: false, legacy: false, version: null },
+ }).action,
+ "install",
+ );
+});
+
+test("authority host planning never downgrades an ahead install", () => {
+ const plan = planAuthorityHostUpdate({
+ mode: "all",
+ targetVersion: "0.5.2",
+ installed: installed({ version: "0.6.0" }),
+ });
+ assert.equal(plan.action, "already_ahead");
+ assert.equal(plan.required, false);
+});
+
+test("installed authority host detection distinguishes versioned and legacy layouts", () => {
+ const versionedRecord = JSON.stringify({
+ schemaVersion: 1,
+ kind: "github-delivery/authority-host-install",
+ version: "0.5.2",
+ sourceCommit: "b".repeat(40),
+ appDir: "app/v0.5.2",
+ installedAt: "2026-08-12T00:00:00Z",
+ });
+ const recordPath = "C:\\Users\\me\\AppData\\Local\\GitHubDeliveryAuthority\\authority-host-install.json";
+ const exePath = "C:\\Users\\me\\AppData\\Local\\GitHubDeliveryAuthority\\app\\v0.5.2\\GitHubDeliveryAuthority.exe";
+ const versioned = readInstalledAuthorityHost({
+ platform: "win32",
+ env: winEnv,
+ home: "C:\\Users\\me",
+ exists: (path) => path === recordPath || path === exePath,
+ readFile: () => versionedRecord,
+ });
+ assert.equal(versioned.installed, true);
+ assert.equal(versioned.legacy, false);
+ assert.equal(versioned.version, "0.5.2");
+ assert.equal(versioned.sourceCommit, "b".repeat(40));
+
+ const legacyExe = "C:\\Users\\me\\AppData\\Local\\GitHubDeliveryAuthority\\GitHubDeliveryAuthority.exe";
+ const legacy = readInstalledAuthorityHost({
+ platform: "win32",
+ env: winEnv,
+ home: "C:\\Users\\me",
+ exists: (path) => path === legacyExe,
+ readFile: () => { throw new Error("not used"); },
+ });
+ assert.equal(legacy.installed, true);
+ assert.equal(legacy.legacy, true);
+ assert.equal(legacy.version, null);
+});
+
+test("unsupported systems report when the configured mode requires Authority", async () => {
+ const result = await reconcileStableAuthorityHost({
+ platform: "linux",
+ dependencies: {
+ readInstalledAuthorityHost: () => ({
+ supported: false,
+ configured: false,
+ installed: false,
+ legacy: false,
+ version: null,
+ sourceCommit: null,
+ }),
+ readUserConfig: () => ({
+ source: "file",
+ config: { schemaVersion: 1, authorityMode: "high-assurance" },
+ }),
+ },
+ client: {
+ latestRelease() { throw new Error("unsupported host must not fetch release assets"); },
+ resolveTagCommit() { throw new Error("unsupported host must not resolve tags"); },
+ },
+ });
+
+ assert.deepEqual(result, {
+ action: "unsupported",
+ required: true,
+ changed: false,
+ installed: {
+ supported: false,
+ configured: false,
+ installed: false,
+ legacy: false,
+ version: null,
+ sourceCommit: null,
+ },
+ mode: "high-assurance",
+ });
+});
diff --git a/tests/unit/authority-host-release-package.test.mjs b/tests/unit/authority-host-release-package.test.mjs
new file mode 100644
index 00000000..b308f263
--- /dev/null
+++ b/tests/unit/authority-host-release-package.test.mjs
@@ -0,0 +1,66 @@
+import assert from "node:assert/strict";
+import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import test from "node:test";
+
+import { buildAuthorityHostRelease } from "../../scripts/build-authority-host-release.mjs";
+
+const sourceCommit = "a".repeat(40);
+
+function fixture() {
+ const root = mkdtempSync(join(tmpdir(), "gd-authority-package-"));
+ const publishDir = join(root, "publish");
+ mkdirSync(join(publishDir, "runtimes", "win-x64", "native"), { recursive: true });
+ writeFileSync(join(publishDir, "GitHubDeliveryAuthority.exe"), "exe");
+ writeFileSync(join(publishDir, "GitHubDeliveryAuthority.dll"), "dll");
+ writeFileSync(join(publishDir, "runtimes", "win-x64", "native", "e_sqlite3.dll"), "sqlite");
+ return { root, publishDir };
+}
+
+test("packages deterministic version-bound authority host release assets", () => {
+ const { root, publishDir } = fixture();
+ try {
+ const first = buildAuthorityHostRelease({
+ publishDir,
+ outDir: join(root, "first"),
+ version: "0.5.2",
+ sourceCommit,
+ });
+ const second = buildAuthorityHostRelease({
+ publishDir,
+ outDir: join(root, "second"),
+ version: "0.5.2",
+ sourceCommit,
+ });
+
+ assert.equal(first.metadata.kind, "github-delivery/authority-host-release");
+ assert.equal(first.metadata.version, "0.5.2");
+ assert.equal(first.metadata.sourceCommit, sourceCommit);
+ assert.equal(first.metadata.platform, "win32");
+ assert.equal(first.metadata.arch, "x64");
+ assert.equal(first.metadata.archive, "github-delivery-authority-v0.5.2-win-x64.zip");
+ assert.match(first.metadata.sha256, /^[0-9a-f]{64}$/);
+ assert(first.metadata.files.some((entry) => entry.path === "GitHubDeliveryAuthority.exe"));
+ assert(first.metadata.files.some((entry) => entry.path === "authority-host-version.json"));
+ assert.deepEqual(readFileSync(first.archivePath), readFileSync(second.archivePath));
+ assert.deepEqual(
+ JSON.parse(readFileSync(first.metadataPath, "utf8")),
+ first.metadata,
+ );
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+});
+
+test("rejects invalid release identity and missing executable", () => {
+ const { root, publishDir } = fixture();
+ try {
+ assert.throws(() => buildAuthorityHostRelease({ publishDir, outDir: join(root, "bad"), version: "dev", sourceCommit }), /authority_host_release_version_invalid/);
+ assert.throws(() => buildAuthorityHostRelease({ publishDir, outDir: join(root, "bad2"), version: "0.5.2", sourceCommit: "bad" }), /authority_host_release_source_commit_invalid/);
+ rmSync(join(publishDir, "GitHubDeliveryAuthority.exe"));
+ assert.throws(() => buildAuthorityHostRelease({ publishDir, outDir: join(root, "bad3"), version: "0.5.2", sourceCommit }), /authority_host_release_executable_missing/);
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+});
diff --git a/tests/unit/authority-host-release.test.mjs b/tests/unit/authority-host-release.test.mjs
new file mode 100644
index 00000000..0bb566da
--- /dev/null
+++ b/tests/unit/authority-host-release.test.mjs
@@ -0,0 +1,169 @@
+import assert from "node:assert/strict";
+import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import test from "node:test";
+
+import { buildAuthorityHostRelease } from "../../scripts/build-authority-host-release.mjs";
+import {
+ acquireVerifiedAuthorityHostPayload,
+ extractVerifiedAuthorityHostZip,
+ validateAuthorityHostReleaseMetadata,
+} from "../../scripts/lib/authority-host-release.mjs";
+
+const VERSION = "0.5.2";
+const SOURCE = "b".repeat(40);
+
+function makePackage() {
+ const root = mkdtempSync(join(tmpdir(), "gd-authority-verify-"));
+ const publishDir = join(root, "publish");
+ const outDir = join(root, "out");
+ mkdirSync(publishDir, { recursive: true });
+ writeFileSync(join(publishDir, "GitHubDeliveryAuthority.exe"), "exe");
+ writeFileSync(join(publishDir, "GitHubDeliveryAuthority.dll"), "dll");
+ const built = buildAuthorityHostRelease({ publishDir, outDir, version: VERSION, sourceCommit: SOURCE });
+ return {
+ root,
+ built,
+ archive: readFileSync(built.archivePath),
+ metadataBytes: readFileSync(built.metadataPath),
+ };
+}
+
+function releaseFor(built) {
+ return {
+ tag_name: `v${VERSION}`,
+ draft: false,
+ prerelease: false,
+ assets: [
+ { name: built.metadata.archive, browser_download_url: "https://example.test/authority.zip" },
+ { name: built.metadata.archive.replace(/\.zip$/, ".json"), browser_download_url: "https://example.test/authority.json" },
+ ],
+ };
+}
+
+function clientFor({ built, archive, metadataBytes }) {
+ return {
+ async latestRelease() { return releaseFor(built); },
+ async resolveTagCommit(tag) {
+ assert.equal(tag, `v${VERSION}`);
+ return SOURCE;
+ },
+ async downloadAsset(asset) {
+ return asset.name.endsWith(".json") ? metadataBytes : archive;
+ },
+ };
+}
+
+function successfulAttestationRunner() {
+ return { status: 0, stdout: "verified", stderr: "", error: null };
+}
+
+test("validates exact authority release identity and file manifest", () => {
+ const fixture = makePackage();
+ try {
+ const validated = validateAuthorityHostReleaseMetadata(fixture.built.metadata, {
+ version: VERSION,
+ sourceCommit: SOURCE,
+ });
+ assert.equal(validated.version, VERSION);
+ assert.equal(validated.sourceCommit, SOURCE);
+ assert(validated.files.some((entry) => entry.path === "GitHubDeliveryAuthority.exe"));
+ assert(validated.files.some((entry) => entry.path === "authority-host-version.json"));
+
+ assert.throws(
+ () => validateAuthorityHostReleaseMetadata({ ...fixture.built.metadata, platform: "linux" }, { version: VERSION, sourceCommit: SOURCE }),
+ /authority_host_release_metadata_invalid/,
+ );
+ assert.throws(
+ () => validateAuthorityHostReleaseMetadata({ ...fixture.built.metadata, sourceCommit: "c".repeat(40) }, { version: VERSION, sourceCommit: SOURCE }),
+ /authority_host_release_metadata_invalid/,
+ );
+ const duplicate = structuredClone(fixture.built.metadata);
+ duplicate.files.push({ ...duplicate.files[0] });
+ assert.throws(
+ () => validateAuthorityHostReleaseMetadata(duplicate, { version: VERSION, sourceCommit: SOURCE }),
+ /authority_host_release_metadata_duplicate_path/,
+ );
+ } finally {
+ rmSync(fixture.root, { recursive: true, force: true });
+ }
+});
+
+test("strictly extracts a verified authority archive under the fixed root", () => {
+ const fixture = makePackage();
+ try {
+ const destination = join(fixture.root, "extract");
+ const result = extractVerifiedAuthorityHostZip({
+ archive: fixture.archive,
+ metadata: fixture.built.metadata,
+ destination,
+ });
+ assert.equal(readFileSync(join(result.root, "GitHubDeliveryAuthority.exe"), "utf8"), "exe");
+ const versionInfo = JSON.parse(readFileSync(join(result.root, "authority-host-version.json"), "utf8"));
+ assert.equal(versionInfo.version, VERSION);
+ assert.equal(versionInfo.sourceCommit, SOURCE);
+ } finally {
+ rmSync(fixture.root, { recursive: true, force: true });
+ }
+});
+
+test("acquires only a digest- and attestation-verified authority payload", async () => {
+ const fixture = makePackage();
+ try {
+ const client = clientFor(fixture);
+ const payload = await acquireVerifiedAuthorityHostPayload({
+ release: releaseFor(fixture.built),
+ workspace: join(fixture.root, "workspace"),
+ client,
+ expectedVersion: VERSION,
+ expectedSourceCommit: SOURCE,
+ attestationRunner: successfulAttestationRunner,
+ });
+ assert.equal(payload.verified, true);
+ assert.equal(payload.metadata.version, VERSION);
+ assert.equal(readFileSync(join(payload.source, "GitHubDeliveryAuthority.exe"), "utf8"), "exe");
+ } finally {
+ rmSync(fixture.root, { recursive: true, force: true });
+ }
+});
+
+test("rejects archive tampering before extraction", async () => {
+ const fixture = makePackage();
+ try {
+ const tampered = Buffer.from(fixture.archive);
+ tampered[tampered.length - 1] ^= 0x01;
+ await assert.rejects(
+ acquireVerifiedAuthorityHostPayload({
+ release: releaseFor(fixture.built),
+ workspace: join(fixture.root, "workspace"),
+ client: clientFor({ ...fixture, archive: tampered }),
+ expectedVersion: VERSION,
+ expectedSourceCommit: SOURCE,
+ attestationRunner: successfulAttestationRunner,
+ }),
+ /authority_host_release_archive_digest_mismatch/,
+ );
+ } finally {
+ rmSync(fixture.root, { recursive: true, force: true });
+ }
+});
+
+test("rejects authority payloads whose GitHub attestation does not verify", async () => {
+ const fixture = makePackage();
+ try {
+ await assert.rejects(
+ acquireVerifiedAuthorityHostPayload({
+ release: releaseFor(fixture.built),
+ workspace: join(fixture.root, "workspace"),
+ client: clientFor(fixture),
+ expectedVersion: VERSION,
+ expectedSourceCommit: SOURCE,
+ attestationRunner: () => ({ status: 1, stdout: "", stderr: "no attestation", error: null }),
+ }),
+ /stable_release_attestation_failed/,
+ );
+ } finally {
+ rmSync(fixture.root, { recursive: true, force: true });
+ }
+});
diff --git a/tests/unit/bootstrap-install.test.mjs b/tests/unit/bootstrap-install.test.mjs
index 10435e47..55f4529c 100644
--- a/tests/unit/bootstrap-install.test.mjs
+++ b/tests/unit/bootstrap-install.test.mjs
@@ -25,6 +25,12 @@ function verifiedPayload(workspace, version = "0.5.0") {
};
}
+const AUTHORITY_NOOP = Object.freeze({
+ action: "unsupported",
+ changed: false,
+ installed: { supported: false, installed: false },
+});
+
function dependencies(overrides = {}) {
return {
makeWorkspace: () => "/tmp/github-delivery-bootstrap-test",
@@ -44,8 +50,9 @@ function dependencies(overrides = {}) {
watchdog: { mode: "none", hookTrustRequired: false },
};
},
- readUserConfig: () => ({ config: { strictAuthority: false } }),
+ readUserConfig: () => ({ config: { schemaVersion: 1, authorityMode: "off" } }),
verifyInstalledRelease: () => ({ clean: true, modifications: [] }),
+ reconcileStableAuthorityHost: async () => ({ ...AUTHORITY_NOOP }),
confirmApply: async () => true,
...overrides,
};
@@ -60,7 +67,7 @@ test("confirmApply defaults to no for blank, EOF, and anything except an explici
}
});
-test("guided install verifies a release and performs a dry-run before asking to apply", async () => {
+test("guided install verifies a release, performs a dry-run, then reconciles Authority after apply", async () => {
const events = [];
const target = resolve("/tmp/skills/github-delivery");
const result = await runGuidedInstall({
@@ -88,6 +95,12 @@ test("guided install verifies a release and performs a dry-run before asking to
events.push("verify-installed");
return { clean: true };
},
+ async reconcileStableAuthorityHost(options) {
+ events.push("reconcile-authority");
+ assert.deepEqual(options.expectedRelease, verifiedPayload("/tmp").release);
+ assert.equal(options.scriptPath, join(target, "authority-host", "windows", "install-release.ps1"));
+ return { ...AUTHORITY_NOOP };
+ },
}),
});
@@ -97,13 +110,15 @@ test("guided install verifies a release and performs a dry-run before asking to
"confirm:true",
"install:true",
"verify-installed",
+ "reconcile-authority",
]);
assert.equal(result.action, "install");
assert.equal(result.apply, true);
assert.equal(result.verified, true);
+ assert.equal(result.authorityHost.action, "unsupported");
});
-test("declining the shown dry-run cleans up and never calls installer apply", async () => {
+test("declining the shown dry-run cleans up and never calls installer apply or Authority reconciliation", async () => {
const events = [];
const target = resolve("/tmp/skills/github-delivery");
const deps = dependencies({
@@ -133,6 +148,9 @@ test("declining the shown dry-run cleans up and never calls installer apply", as
verifyInstalledRelease() {
throw new Error("cancelled install must not verify a non-existent install");
},
+ reconcileStableAuthorityHost() {
+ throw new Error("cancelled install must not reconcile Authority");
+ },
});
const result = await runGuidedInstall({ target, dependencies: deps });
@@ -153,16 +171,17 @@ test("declining the shown dry-run cleans up and never calls installer apply", as
]);
});
-test("accepted install requires post-install manifest verification and unchanged user config", async () => {
+test("accepted install requires post-install manifest verification, unchanged user config, and Authority reconciliation", async () => {
let configReads = 0;
let verified = 0;
+ let authorityCalls = 0;
const target = resolve("/tmp/skills/github-delivery");
const result = await runGuidedInstall({
target,
dependencies: dependencies({
readUserConfig() {
configReads += 1;
- return { config: { strictAuthority: false } };
+ return { config: { schemaVersion: 1, authorityMode: "off" } };
},
installSkill(options) {
return {
@@ -179,15 +198,21 @@ test("accepted install requires post-install manifest verification and unchanged
assert.equal(options.manifest.version, "0.5.0");
return { clean: true };
},
+ async reconcileStableAuthorityHost() {
+ authorityCalls += 1;
+ return { ...AUTHORITY_NOOP };
+ },
}),
});
assert.equal(configReads, 2);
assert.equal(verified, 1);
+ assert.equal(authorityCalls, 1);
assert.equal(result.backupPath, "/tmp/backup");
});
-test("post-install config drift fails closed and keeps the installer backup path on the error", async () => {
+test("post-install config drift fails closed before Authority reconciliation and keeps backup path", async () => {
let configReads = 0;
+ let authorityCalls = 0;
const target = resolve("/tmp/skills/github-delivery");
await assert.rejects(
runGuidedInstall({
@@ -195,7 +220,7 @@ test("post-install config drift fails closed and keeps the installer backup path
dependencies: dependencies({
readUserConfig() {
configReads += 1;
- return { config: { strictAuthority: configReads > 1 } };
+ return { config: { schemaVersion: 1, authorityMode: configReads > 1 ? "all" : "off" } };
},
installSkill(options) {
return {
@@ -206,6 +231,10 @@ test("post-install config drift fails closed and keeps the installer backup path
watchdog: null,
};
},
+ reconcileStableAuthorityHost() {
+ authorityCalls += 1;
+ return { ...AUTHORITY_NOOP };
+ },
}),
}),
(error) => {
@@ -214,6 +243,7 @@ test("post-install config drift fails closed and keeps the installer backup path
return true;
},
);
+ assert.equal(authorityCalls, 0);
});
test("explicit install refuses to silently reinstall an already valid installation", async () => {
@@ -227,4 +257,4 @@ test("explicit install refuses to silently reinstall an already valid installati
}),
/bootstrap_install_existing/,
);
-});
\ No newline at end of file
+});
diff --git a/tests/unit/bootstrap-maintenance.test.mjs b/tests/unit/bootstrap-maintenance.test.mjs
index bd6f3997..c8f407d1 100644
--- a/tests/unit/bootstrap-maintenance.test.mjs
+++ b/tests/unit/bootstrap-maintenance.test.mjs
@@ -11,11 +11,23 @@ import { runBootstrap } from "../../scripts/lib/bootstrap-command.mjs";
const TARGET = resolve("/tmp/github-delivery-installed");
const CODEX_HOME = resolve("/tmp/codex-home");
+const AUTHORITY_NOOP = Object.freeze({
+ action: "unsupported",
+ changed: false,
+ installed: { supported: false, installed: false },
+});
function validInstallation(version = "0.4.0") {
return [{ target: TARGET, valid: true, version, reason: null }];
}
+function authorityNoopDependencies(extra = {}) {
+ return {
+ reconcileStableAuthorityHost: async () => ({ ...AUTHORITY_NOOP }),
+ ...extra,
+ };
+}
+
test("update always delegates through the installed target explicitly", async () => {
const seen = [];
const result = await runBootstrapUpdate({
@@ -60,13 +72,20 @@ test("setup fails clearly when no valid installed skill exists", async () => {
);
});
-test("setup leaves a healthy activation untouched", async () => {
+test("setup leaves a healthy activation untouched after authority reconciliation", async () => {
let mutations = 0;
+ let authorityCalls = 0;
+ const authorityHost = { action: "already_current", changed: false, installed: { supported: true, installed: true, version: "0.5.1" } };
const result = await runBootstrapSetup({
target: TARGET,
codexHome: CODEX_HOME,
dependencies: {
discoverInstallations: () => validInstallation(),
+ async reconcileStableAuthorityHost(options) {
+ authorityCalls += 1;
+ assert.equal(options.scriptPath, join(TARGET, "authority-host", "windows", "install-release.ps1"));
+ return authorityHost;
+ },
readActivationReceipt: () => ({
schemaVersion: 1,
mode: "hooks",
@@ -85,6 +104,7 @@ test("setup leaves a healthy activation untouched", async () => {
},
});
+ assert.equal(authorityCalls, 1);
assert.equal(mutations, 0);
assert.deepEqual(result, {
action: "setup",
@@ -92,7 +112,28 @@ test("setup leaves a healthy activation untouched", async () => {
target: TARGET,
watchdog: "hooks",
changed: false,
+ authorityHost,
+ });
+});
+
+test("setup surfaces an authority-host repair even when watchdog activation is already healthy", async () => {
+ const authorityHost = {
+ action: "upgrade_legacy",
+ changed: true,
+ installed: { supported: true, installed: true, legacy: false, version: "0.5.1" },
+ };
+ const result = await runBootstrapSetup({
+ target: TARGET,
+ codexHome: CODEX_HOME,
+ dependencies: {
+ discoverInstallations: () => validInstallation("0.5.1"),
+ reconcileStableAuthorityHost: async () => authorityHost,
+ readActivationReceipt: () => ({ mode: "hooks", hooksConfigured: true, hookTrustVerified: true }),
+ },
});
+ assert.equal(result.status, "ready");
+ assert.equal(result.changed, true);
+ assert.deepEqual(result.authorityHost, authorityHost);
});
test("setup never applies a trust assertion when the installed hook definition would change", async () => {
@@ -101,7 +142,7 @@ test("setup never applies a trust assertion when the installed hook definition w
const result = await runBootstrapSetup({
target: TARGET,
codexHome: CODEX_HOME,
- dependencies: {
+ dependencies: authorityNoopDependencies({
discoverInstallations: () => validInstallation(),
readActivationReceipt: () => ({
schemaVersion: 1,
@@ -124,13 +165,14 @@ test("setup never applies a trust assertion when the installed hook definition w
installerLoads += 1;
throw new Error("changed hooks must not reach activation apply");
},
- },
+ }),
});
assert.equal(confirmed, 0);
assert.equal(installerLoads, 0);
assert.equal(result.status, "hook_trust_required");
assert.equal(result.hookDefinitionChanged, true);
+ assert.deepEqual(result.authorityHost, AUTHORITY_NOOP);
assert.match(result.guidance, /\/hooks/);
});
@@ -139,7 +181,7 @@ test("setup refreshes activation only through the installer inside the installed
const result = await runBootstrapSetup({
target: TARGET,
codexHome: CODEX_HOME,
- dependencies: {
+ dependencies: authorityNoopDependencies({
discoverInstallations: () => validInstallation(),
readActivationReceipt: () => ({
schemaVersion: 1,
@@ -180,7 +222,7 @@ test("setup refreshes activation only through the installer inside the installed
},
};
},
- },
+ }),
});
assert.deepEqual(events.slice(0, 2), ["inspect", "confirm"]);
@@ -188,9 +230,10 @@ test("setup refreshes activation only through the installer inside the installed
assert.equal(events.at(-1), "run");
assert.equal(result.status, "ready");
assert.equal(result.watchdog, "hooks");
+ assert.deepEqual(result.authorityHost, AUTHORITY_NOOP);
});
-test("doctor is read-only and reports integrity, activation, config, and update relation", async () => {
+test("doctor is read-only and reports integrity, activation, config, authority host, and update relation", async () => {
const mutations = [];
const manifest = {
schemaVersion: 1,
@@ -209,6 +252,7 @@ test("doctor is read-only and reports integrity, activation, config, and update
readInstalledManifest: () => manifest,
compareInstalledManifest: () => ({ clean: false, modifications: [{ path: "SKILL.md", reason: "changed" }] }),
readUserConfig: () => ({ source: "default", config: { schemaVersion: 1, authorityMode: "off" } }),
+ readInstalledAuthorityHost: () => ({ supported: true, installed: true, legacy: true, version: null, sourceCommit: null }),
readActivationReceipt: () => ({ mode: "none", degradationReason: "hook_trust_required", hooksConfigured: true, hookTrustVerified: false }),
async latestRelease() {
return { tag_name: "v0.5.0", draft: false, prerelease: false, assets: [] };
@@ -225,7 +269,12 @@ test("doctor is read-only and reports integrity, activation, config, and update
assert.equal(report.installed.version, "0.4.0");
assert.equal(report.integrity.clean, false);
assert.equal(report.config.ok, true);
+ assert.equal(report.config.effectiveAuthorityMode, "off");
assert.equal(report.activation.degradationReason, "hook_trust_required");
+ assert.equal(report.authorityHost.ok, true);
+ assert.equal(report.authorityHost.legacy, true);
+ assert.equal(report.authorityHost.relation, "legacy");
+ assert.equal(report.authorityHost.requiredByMode, false);
assert.deepEqual(report.latest, { version: "0.5.0", relation: "update", error: null });
});
@@ -255,4 +304,4 @@ test("bare bootstrap never silently updates an existing installation", async ()
});
assert.equal(updates, 0);
assert.deepEqual(result, { action: "exit", target: TARGET });
-});
\ No newline at end of file
+});
diff --git a/tests/unit/npm-package.test.mjs b/tests/unit/npm-package.test.mjs
index 541d7347..0bec7c54 100644
--- a/tests/unit/npm-package.test.mjs
+++ b/tests/unit/npm-package.test.mjs
@@ -14,6 +14,8 @@ const REQUIRED_RUNTIME = new Set([
"scripts/github-delivery-cli.mjs",
"scripts/install-codex-watchdog-hooks.mjs",
"scripts/install-skill.mjs",
+ "scripts/lib/authority-host-install.mjs",
+ "scripts/lib/authority-host-release.mjs",
"scripts/lib/bootstrap-cli.mjs",
"scripts/lib/bootstrap-command.mjs",
"scripts/lib/bootstrap-install.mjs",
diff --git a/tests/unit/release-authority-asset.test.mjs b/tests/unit/release-authority-asset.test.mjs
new file mode 100644
index 00000000..54481396
--- /dev/null
+++ b/tests/unit/release-authority-asset.test.mjs
@@ -0,0 +1,21 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import test from "node:test";
+
+const workflow = readFileSync(new URL("../../.github/workflows/release.yml", import.meta.url), "utf8");
+
+test("release workflow builds, attests, and publishes the Windows authority host asset", () => {
+ assert.match(workflow, /authority_host:/);
+ assert.match(workflow, /runs-on: windows-latest/);
+ assert.match(workflow, /dotnet publish[\s\S]*--runtime win-x64[\s\S]*--self-contained true/);
+ assert.match(workflow, /build-authority-host-release\.mjs/);
+ assert.match(workflow, /github-delivery-authority-\$\{\{ github\.sha \}\}/);
+ assert.match(workflow, /actions\/download-artifact@[0-9a-f]{40}/);
+ assert.match(workflow, /name: Attest Windows authority host/);
+ assert.match(workflow, /subject-path: dist\/authority-host\/github-delivery-authority-v\*\.zip/);
+ assert.match(workflow, /gh release create[\s\S]*dist\/authority-host\/github-delivery-authority-v\*\.zip[\s\S]*dist\/authority-host\/github-delivery-authority-v\*\.json/);
+});
+
+test("protected publish waits for both validation and authority host build", () => {
+ assert.match(workflow, /publish:[\s\S]*needs:\s*\n\s*- validate\s*\n\s*- authority_host/);
+});
diff --git a/tests/unit/release-self-update-integration.test.mjs b/tests/unit/release-self-update-integration.test.mjs
index 9c2413f3..6a6aad5f 100644
--- a/tests/unit/release-self-update-integration.test.mjs
+++ b/tests/unit/release-self-update-integration.test.mjs
@@ -46,10 +46,31 @@ function verifiedCandidate(root, target) {
};
}
+const UNSUPPORTED_AUTHORITY = Object.freeze({
+ supported: false,
+ installed: false,
+ legacy: false,
+ version: null,
+ sourceCommit: null,
+});
+const UNSUPPORTED_AUTHORITY_RESULT = Object.freeze({
+ action: "unsupported",
+ changed: false,
+ installed: UNSUPPORTED_AUTHORITY,
+});
+
+function authorityTestDependencies() {
+ return {
+ readInstalledAuthorityHost: () => ({ ...UNSUPPORTED_AUTHORITY }),
+ reconcileStableAuthorityHost: async () => ({ ...UNSUPPORTED_AUTHORITY_RESULT }),
+ };
+}
+
function workspaceDependencies(root) {
return {
makeWorkspace: () => join(root, "workspace"),
removeWorkspace: () => {},
+ ...authorityTestDependencies(),
};
}
@@ -63,6 +84,7 @@ test("release self-update dry-run never mutates the installed target", async ()
}, {
makeWorkspace: () => join(root, "workspace"),
removeWorkspace: (workspace) => { removedWorkspace = workspace; },
+ ...authorityTestDependencies(),
prepareVerifiedReleaseCandidate: async ({ target: candidateTarget, workspace }) => {
assert.equal(candidateTarget, target);
assert.equal(workspace, join(root, "workspace"));
@@ -84,31 +106,59 @@ test("release self-update dry-run never mutates the installed target", async ()
assert.equal(result.apply, false);
assert.equal(result.updated, false);
assert.equal(result.release.sourceCommit, "a".repeat(40));
+ assert.equal(result.authorityHost.action, "unsupported");
assert.equal(removedWorkspace, join(root, "workspace"));
}));
-test("already-current and already-ahead releases are no-ops even with apply", async () => {
- for (const action of ["already_current", "already_ahead"]) {
- await withFixture(async ({ root, target }) => {
- const candidate = verifiedCandidate(root, target);
- candidate.plan.action = action;
- candidate.plan.safeToReplace = false;
- let installCalls = 0;
- const result = await runInstallCommand({ update: true, apply: true, target }, {
- ...workspaceDependencies(root),
- prepareVerifiedReleaseCandidate: async () => candidate,
- installSkill: () => { installCalls += 1; },
- readUserConfig: () => { throw new Error("no-op must not read config"); },
- verifyInstalledRelease: () => { throw new Error("no-op must not verify post-install state"); },
- });
- assert.equal(installCalls, 0);
- assert.equal(result.action, action);
- assert.equal(result.apply, true);
- assert.equal(result.updated, false);
- assert.equal(readFileSync(join(target, "marker.txt"), "utf8"), "old\n");
- });
- }
-});
+test("already-current release reports an Authority-only repair as an applied update", async () => withFixture(async ({ root, target }) => {
+ const candidate = verifiedCandidate(root, target);
+ candidate.plan.action = "already_current";
+ candidate.plan.safeToReplace = false;
+ let installCalls = 0;
+ let authorityCalls = 0;
+ const authorityHost = { action: "upgrade_legacy", changed: true, installed: { supported: true, installed: true, version: "0.5.0" } };
+ const result = await runInstallCommand({ update: true, apply: true, target }, {
+ ...workspaceDependencies(root),
+ prepareVerifiedReleaseCandidate: async () => candidate,
+ installSkill: () => { installCalls += 1; },
+ readUserConfig: () => { throw new Error("unsupported authority planning must not read config"); },
+ verifyInstalledRelease: () => { throw new Error("already-current skill must not verify post-install state"); },
+ async reconcileStableAuthorityHost(options) {
+ authorityCalls += 1;
+ assert.equal(options.expectedRelease, candidate.release);
+ assert.equal(options.scriptPath, join(target, "authority-host", "windows", "install-release.ps1"));
+ return authorityHost;
+ },
+ });
+ assert.equal(installCalls, 0);
+ assert.equal(authorityCalls, 1);
+ assert.equal(result.action, "update");
+ assert.equal(result.updated, true);
+ assert.deepEqual(result.authorityHost, authorityHost);
+ assert.equal(readFileSync(join(target, "marker.txt"), "utf8"), "old\n");
+}));
+
+test("already-ahead release remains a complete no-op including Authority", async () => withFixture(async ({ root, target }) => {
+ const candidate = verifiedCandidate(root, target);
+ candidate.plan.action = "already_ahead";
+ candidate.plan.safeToReplace = false;
+ let installCalls = 0;
+ let authorityCalls = 0;
+ const result = await runInstallCommand({ update: true, apply: true, target }, {
+ ...workspaceDependencies(root),
+ prepareVerifiedReleaseCandidate: async () => candidate,
+ installSkill: () => { installCalls += 1; },
+ readUserConfig: () => { throw new Error("ahead no-op must not read config"); },
+ reconcileStableAuthorityHost: async () => { authorityCalls += 1; throw new Error("ahead no-op must not reconcile authority"); },
+ verifyInstalledRelease: () => { throw new Error("ahead no-op must not verify post-install state"); },
+ });
+ assert.equal(installCalls, 0);
+ assert.equal(authorityCalls, 0);
+ assert.equal(result.action, "already_ahead");
+ assert.equal(result.updated, false);
+ assert.equal(result.authorityHost.action, "skipped_skill_ahead");
+ assert.equal(readFileSync(join(target, "marker.txt"), "utf8"), "old\n");
+}));
test("local modifications block replacement and force cannot bypass the update plan", async () => withFixture(async ({ root, target }) => {
const candidate = verifiedCandidate(root, target);
@@ -153,6 +203,7 @@ test("release self-update apply installs only the verified candidate then verifi
};
let configReads = 0;
let verifyCalls = 0;
+ let authorityCalls = 0;
let removedWorkspace = null;
const result = await runInstallCommand({
@@ -164,6 +215,7 @@ test("release self-update apply installs only the verified candidate then verifi
}, {
makeWorkspace: () => join(root, "workspace"),
removeWorkspace: (workspace) => { removedWorkspace = workspace; },
+ ...authorityTestDependencies(),
prepareVerifiedReleaseCandidate: async () => candidate,
readUserConfig: () => {
configReads += 1;
@@ -192,11 +244,18 @@ test("release self-update apply installs only the verified candidate then verifi
assert.deepEqual(manifest, candidate.manifest);
return { clean: true };
},
+ async reconcileStableAuthorityHost(options) {
+ authorityCalls += 1;
+ assert.equal(options.expectedRelease, candidate.release);
+ assert.equal(options.scriptPath, join(target, "authority-host", "windows", "install-release.ps1"));
+ return { ...UNSUPPORTED_AUTHORITY_RESULT };
+ },
});
assert.equal(readFileSync(join(target, "marker.txt"), "utf8"), "new\n");
assert.equal(configReads, 2);
assert.equal(verifyCalls, 1);
+ assert.equal(authorityCalls, 1);
assert.equal(result.action, "update");
assert.equal(result.apply, true);
assert.equal(result.updated, true);
@@ -205,6 +264,7 @@ test("release self-update apply installs only the verified candidate then verifi
assert.equal(result.backupPath, backupPath);
assert.equal(result.release.sourceCommit, "a".repeat(40));
assert.equal(result.watchdog.hookTrustRequired, true);
+ assert.equal(result.authorityHost.action, "unsupported");
assert.equal(removedWorkspace, join(root, "workspace"));
}));
diff --git a/tests/unit/windows-authority-onboarding.test.mjs b/tests/unit/windows-authority-onboarding.test.mjs
index 106fe116..94c73cab 100644
--- a/tests/unit/windows-authority-onboarding.test.mjs
+++ b/tests/unit/windows-authority-onboarding.test.mjs
@@ -6,17 +6,24 @@ function read(path) {
return readFileSync(new URL(`../../${path}`, import.meta.url), "utf8");
}
-test("Windows authority installer enforces supported platform and guided setup", () => {
+test("Windows authority installers enforce supported platform and guided setup", () => {
const installer = read("authority-host/windows/install.ps1");
+ const releaseInstaller = read("authority-host/windows/install-release.ps1");
const program = read("authority-host/windows/GitHubDeliveryAuthority/Program.cs");
+ const appHost = read("authority-host/windows/GitHubDeliveryAuthority/AuthorityAppHost.cs");
assert.match(installer, /22000/);
assert.match(installer, /--list-sdks/);
assert.match(installer, /8\./);
- assert.match(installer, /Get-Process/);
- assert.match(installer, /--setup/);
+ assert.match(installer, /install-release\.ps1/);
assert.match(installer, /PIN/i);
+
+ assert.match(releaseInstaller, /Get-Process/);
+ assert.match(releaseInstaller, /Start-Process \$installedExe/);
+
assert.match(program, /args\.Contains\("--setup",\s*StringComparer\.Ordinal\)/);
+ assert.match(appHost, /ShouldShowSetup\(_forceSetup,\s*_store\.ListAllowedRepositories\(\)\.Count\)/);
+ assert.match(appHost, /forceSetup\s*\|\|\s*allowedRepositoryCount\s*==\s*0/);
});
test("Windows authority documentation explains Hello setup and recovery", () => {
diff --git a/tests/unit/windows-authority-winui.test.mjs b/tests/unit/windows-authority-winui.test.mjs
index b846ff02..6f2e9ce5 100644
--- a/tests/unit/windows-authority-winui.test.mjs
+++ b/tests/unit/windows-authority-winui.test.mjs
@@ -18,11 +18,20 @@ test("authority host is unpackaged self-contained WinUI 3, not WinForms", () =>
assert.doesNotMatch(project, /UseWindowsForms/);
});
-test("installer preserves the self-contained deployment contract and CI publishes it", () => {
+test("source installer preserves self-contained deployment and delegates to the release installer", () => {
const installer = read("authority-host/windows/install.ps1");
+ const releaseInstaller = read("authority-host/windows/install-release.ps1");
const workflow = read(".github/workflows/ci.yml");
assert.match(installer, /dotnet\.Source publish[\s\S]*--self-contained true/);
+ assert.match(installer, /install-release\.ps1/);
assert.doesNotMatch(installer, /--self-contained false/);
+ assert.match(releaseInstaller, /authority-host-version\.json/);
+ assert.match(releaseInstaller, /authority-host-install\.json/);
+ assert.match(releaseInstaller, /Join-Path \$InstallDir 'app'/);
+ assert.match(releaseInstaller, /Join-Path \$appRoot \('v' \+ \$ExpectedVersion\)/);
+ assert.match(releaseInstaller, /authority\.db/);
+ assert.match(releaseInstaller, /trust-store\.json/);
+ assert.doesNotMatch(releaseInstaller, /dotnet publish|dotnet\.Source publish/);
assert.match(workflow, /Publish Windows authority host/);
assert.match(workflow, /dotnet publish[\s\S]*--self-contained true/);
});
@@ -46,6 +55,27 @@ test("control center implements the selected activity-first audit design in ligh
}
});
+test("settings page exposes and persists exactly the three authority protection modes", () => {
+ const window = read(`${root}/ControlCenterWindow.xaml`);
+ const code = read(`${root}/ControlCenterWindow.xaml.cs`);
+ const store = read(`${root}/UserConfigStore.cs`);
+
+ for (const phrase of [
+ "Sensitive actions (Recommended)",
+ "Every GitHub write",
+ "No Windows Hello prompts.",
+ "Delivery Authority",
+ "Source commit",
+ "Config file",
+ ]) assert.match(window, new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
+ assert.match(window, /SelectionChanged="Navigation_SelectionChanged"/);
+ assert.match(window, /Click="ApplyProtectionMode_Click"/);
+ assert.match(code, /UserConfigStore\.WriteAuthorityMode\(mode\)/);
+ assert.match(code, /authority-host-version\.json/);
+ assert.match(store, /WriteAuthorityMode\(string mode\)/);
+ for (const mode of ["off", "high-assurance", "all"]) assert.match(store, new RegExp(`\\"${mode}\\"`));
+});
+
test("approval UI uses the refined design without GitHub or Windows brand logos", () => {
const window = read(`${root}/ApprovalWindow.xaml`);
assert.match(window, /Approve GitHub mutation/);