Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 100 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,10 @@ jobs:
-TrxFileName OpenClaw.SetupEngine.Tests.trx
-Runtime win-x64

- name: Test cleanup helper signing boundary
shell: pwsh
run: .\scripts\test-cleanup-host-signing.ps1

- name: Run Dev containment settings tests
run: >
dotnet test tests/OpenClaw.Tray.Tests
Expand Down Expand Up @@ -996,13 +1000,78 @@ jobs:
TestResults/E2E/
if-no-files-found: warn

sign-cleanup-host:
name: Sign embedded cleanup host before compilation
needs: [change-classification, metadata]
if: (startsWith(github.ref, 'refs/tags/v') || inputs.promotion_alpha != '') && needs.metadata.result == 'success'
runs-on: windows-latest
environment: release-signing
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ inputs.promotion_source_sha || github.sha }}
persist-credentials: false

- *promotion-version-tag

- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json

- name: Build AnyCPU cleanup host
run: dotnet build src/OpenClaw.GatewayCleanupHost -c Release -o signing-cleanup-host

- name: Disable NuGet source mapping for cleanup signing
shell: pwsh
run: |
if (Test-Path NuGet.Config) { Rename-Item NuGet.Config NuGet.Config.signing.bak }

- name: Azure Login for Cleanup Signing
uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Sign cleanup executable before embedding
uses: azure/artifact-signing-action@v2
with:
endpoint: https://eus.codesigning.azure.net/
signing-account-name: openclaw
certificate-profile-name: openclaw
files-folder: signing-cleanup-host
files-folder-filter: exe
files-folder-depth: 1
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256

- name: Verify signed cleanup input
shell: pwsh
run: >
.\scripts\Test-CleanupHostSignature.ps1
-HostPath signing-cleanup-host\OpenClaw.GatewayCleanupHost.exe -RequireSigned
-SignerSubject "CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US"

- uses: actions/upload-artifact@v7
with:
name: openclaw-signed-cleanup-host
path: signing-cleanup-host/OpenClaw.GatewayCleanupHost.exe
if-no-files-found: error

build-x64:
name: x64 release publish smoke
needs: [change-classification, metadata]
if: ${{ !cancelled() && needs.change-classification.result == 'success' && needs.metadata.result == 'success' && needs.change-classification.outputs.x64_release == 'true' }}
needs: [change-classification, metadata, sign-cleanup-host]
if: ${{ !cancelled() && needs.change-classification.result == 'success' && needs.metadata.result == 'success' && (needs.sign-cleanup-host.result == 'success' || needs.sign-cleanup-host.result == 'skipped') && needs.change-classification.outputs.x64_release == 'true' }}
runs-on: windows-latest
env:
OPENCLAW_BUILD_VERSION: ${{ needs.metadata.outputs.semVer }}
RequireSignedCleanupHost: ${{ startsWith(github.ref, 'refs/tags/v') || inputs.promotion_alpha != '' }}
OpenClawSignedCleanupHostPath: ${{ needs.sign-cleanup-host.result == 'success' && format('{0}\signed-cleanup-host\OpenClaw.GatewayCleanupHost.exe', github.workspace) || '' }}

steps:
- uses: actions/checkout@v7
Expand All @@ -1029,6 +1098,13 @@ jobs:
- name: Restore WinUI Tray App
run: dotnet restore src/OpenClaw.Tray.WinUI -r win-x64

- name: Download signed cleanup host
if: needs.sign-cleanup-host.result == 'success'
uses: actions/download-artifact@v8
with:
name: openclaw-signed-cleanup-host
path: signed-cleanup-host

- name: Publish WinUI Tray App
run: dotnet publish src/OpenClaw.Tray.WinUI -c Release -r win-x64 --self-contained --no-restore -o publish -p:Version=$env:OPENCLAW_BUILD_VERSION -p:InformationalVersion=$env:OPENCLAW_BUILD_VERSION

Expand Down Expand Up @@ -1058,11 +1134,13 @@ jobs:

build-arm64:
name: ARM64 release publish
needs: [change-classification, metadata]
if: ${{ !cancelled() && needs.change-classification.result == 'success' && needs.metadata.result == 'success' && needs.change-classification.outputs.arm64_release == 'true' }}
needs: [change-classification, metadata, sign-cleanup-host]
if: ${{ !cancelled() && needs.change-classification.result == 'success' && needs.metadata.result == 'success' && (needs.sign-cleanup-host.result == 'success' || needs.sign-cleanup-host.result == 'skipped') && needs.change-classification.outputs.arm64_release == 'true' }}
runs-on: windows-11-arm
env:
OPENCLAW_BUILD_VERSION: ${{ needs.metadata.outputs.semVer }}
RequireSignedCleanupHost: ${{ startsWith(github.ref, 'refs/tags/v') || inputs.promotion_alpha != '' }}
OpenClawSignedCleanupHostPath: ${{ needs.sign-cleanup-host.result == 'success' && format('{0}\signed-cleanup-host\OpenClaw.GatewayCleanupHost.exe', github.workspace) || '' }}

steps:
- uses: actions/checkout@v7
Expand All @@ -1089,6 +1167,13 @@ jobs:
- name: Restore WinUI Tray App
run: dotnet restore src/OpenClaw.Tray.WinUI -r win-arm64

- name: Download signed cleanup host
if: needs.sign-cleanup-host.result == 'success'
uses: actions/download-artifact@v8
with:
name: openclaw-signed-cleanup-host
path: signed-cleanup-host

- name: Publish WinUI Tray App
run: dotnet publish src/OpenClaw.Tray.WinUI -c Release -r win-arm64 --self-contained --no-restore -o publish -p:Version=$env:OPENCLAW_BUILD_VERSION -p:InformationalVersion=$env:OPENCLAW_BUILD_VERSION

Expand Down Expand Up @@ -1120,12 +1205,14 @@ jobs:

build-msix:
name: MSIX artifacts (${{ matrix.architecture }})
needs: [change-classification, metadata, reserve-msix-version]
if: ${{ !cancelled() && needs.change-classification.result == 'success' && needs.metadata.result == 'success' && (needs.reserve-msix-version.result == 'success' || needs.reserve-msix-version.result == 'skipped') && (needs.change-classification.outputs.x64_release == 'true' || needs.change-classification.outputs.arm64_release == 'true') }}
needs: [change-classification, metadata, reserve-msix-version, sign-cleanup-host]
if: ${{ !cancelled() && needs.change-classification.result == 'success' && needs.metadata.result == 'success' && (needs.sign-cleanup-host.result == 'success' || needs.sign-cleanup-host.result == 'skipped') && (needs.reserve-msix-version.result == 'success' || needs.reserve-msix-version.result == 'skipped') && (needs.change-classification.outputs.x64_release == 'true' || needs.change-classification.outputs.arm64_release == 'true') }}
# Dev-signed packages publish on tag releases. Unsigned Store packages stay workflow-only.
runs-on: ${{ matrix.architecture == 'arm64' && 'windows-11-arm' || 'windows-latest' }}
env:
OPENCLAW_BUILD_VERSION: ${{ needs.metadata.outputs.semVer }}
RequireSignedCleanupHost: ${{ startsWith(github.ref, 'refs/tags/v') || inputs.promotion_alpha != '' }}
OpenClawSignedCleanupHostPath: ${{ needs.sign-cleanup-host.result == 'success' && format('{0}\signed-cleanup-host\OpenClaw.GatewayCleanupHost.exe', github.workspace) || '' }}
DEV_MSIX_REVISION: ${{ github.run_number }}
MSIX_VERSION_INFO: ${{ needs.reserve-msix-version.outputs.versionInfo || needs.metadata.outputs.msixVersionInfo }}
MSIX_SOURCE_VERSION: ${{ needs.reserve-msix-version.outputs.sourceVersion || needs.metadata.outputs.msixSourceVersion }}
Expand Down Expand Up @@ -1168,6 +1255,13 @@ jobs:
$info | ConvertTo-Json -Depth 4 |
Set-Content -LiteralPath "$env:RUNNER_TEMP\openclaw-msix-version.json" -Encoding utf8

- name: Download signed cleanup host
if: needs.sign-cleanup-host.result == 'success'
uses: actions/download-artifact@v8
with:
name: openclaw-signed-cleanup-host
path: signed-cleanup-host

- name: Build and validate unsigned Store MSIX
shell: pwsh
run: >
Expand Down
1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ new attempt. This boundary does not change release signing or Windows policy.
| Exactly-once ordered app shutdown sequencing | `AppShutdownCoordinator` | authoritative |
| App composition-root startup sequencing | `AppBootstrapper` (planned) | planned |
| Packaged uninstall cleanup, preservation recovery and removed-target reconciliation | `GatewayPackageCleanup`; App only invokes its startup gate and presents recovery. Legacy name/path markers never grant unattended deletion authority; malformed receipts/settings retain pending evidence and fail closed | authoritative |
| Windowless scheduled cleanup launch after Companion removal | `OpenClaw.GatewayCleanupHost` (GUI-subsystem .NET Framework helper), embedded and deployed by `GatewayPackageCleanup`; `GatewayPackageCleanup.Task.ps1` registers its durable versioned path. PowerShell starts with `CreateNoWindow`; worker exit codes reach Task Scheduler | authoritative |
| Inno migration records, preparation, and pre-start completion guard | `MigrationRecordCodec`, `MigrationPreparation`, `MigrationInventory`, `InnoMigrationStartupGuard` | authoritative |
| Cross-session migration exclusion and source activity inspection | `MigrationOperationLock`, `InnoSourceActivityVerifier` (App only retains the runtime handle through process exit) | authoritative |
| Store migration startup admission and finalization | `InnoInstallationDetector`, `InnoSourceRemovalVerifier`, `MigrationStartupRecordReader`, `MigrationInventoryCapture`, `StoreMigrationStartupCoordinator`, `StoreMigrationFinalizationCoordinator`, `StoreMigrationStartupGuard` | authoritative |
Expand Down
24 changes: 24 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ OpenClaw-owned binaries:
- `OpenClaw.SetupEngine.dll`
- `OpenClaw.Shared.dll`
- `OpenClawTray.FunctionalUI.dll`
- `OpenClaw.GatewayCleanupHost.exe` embedded inside `OpenClaw.SetupEngine.dll`

Third-party/runtime executables that must not be OpenClaw-signed:

Expand Down Expand Up @@ -923,6 +924,28 @@ registration should have a federated credential for:

## How CI signs payload binaries

On release tags and alpha promotion builds, `sign-cleanup-host` first builds the
AnyCPU .NET Framework cleanup executable and signs it through the existing
protected `release-signing` environment. The x64, ARM64 and Store/Dev MSIX build
jobs download that same signed artifact before compiling SetupEngine.
`OpenClawSignedCleanupHostPath` selects those bytes instead of rebuilding the
helper through its project reference. Non-dev Release builds default to
`RequireSignedCleanupHost=true` and reject a missing signing input; any supplied
signing input must have a valid Authenticode signature. Debug and Dev builds
retain the unsigned project-reference path. Non-published PR release smoke and
MSIX fixture builds explicitly set `RequireSignedCleanupHost=false`; the same
override is available for intentional unsigned local Release testing, not
release publication.

SetupEngine verifies the input before embedding and checks the actual compiled
resource afterward for a matching hash and valid signature. The release payload
verifier also extracts that resource and requires the expected OpenClaw signer.
Signing only the outer DLL or MSIX does not sign an embedded executable. Do not
post-sign a loose helper and assume the previously embedded bytes changed.
`scripts\test-cleanup-host-signing.ps1` exercises the real build with an existing
OS-signed fixture, unsigned development output, missing/unsigned signing inputs
and mismatched resource bytes, without creating certificates or changing trust.

The release workflow does not recursively sign every `.exe`. Instead it creates
temporary signing input directories with hardlinks to only the OpenClaw-owned
executables and DLLs from the x64 and ARM64 payloads, then runs Azure Artifact
Expand All @@ -939,6 +962,7 @@ For release tags, the **Build and Test** workflow should run:

- `change-classification` with the `full` result
- `fast-validation`
- `sign-cleanup-host` before release payload compilation
- `test`
- `e2etests` shards: `setup-connect`, `revocation-recovery`, and `network-recovery`
- `build` matrix entries shown by GitHub as `build (win-x64)` and `build (win-arm64)`
Expand Down
4 changes: 4 additions & 0 deletions docs/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ Go to **Settings → Apps → Installed apps**, find **OpenClaw Companion**, and

Your settings file at `%APPDATA%\OpenClawTray\settings.json` is not removed automatically, and device identity files for gateways unrelated to the one you removed (including per-gateway keys at `%APPDATA%\OpenClawTray\gateways\<gateway-id>\device-key-ed25519.json`) are preserved. Choosing **Yes** also removes the removed gateway's own identity directory. Root operator and node tokens are preserved only while an external gateway record remains; otherwise they are cleared, even if another local or loopback gateway record remains. Delete `%APPDATA%\OpenClawTray\` manually if you want a fully clean uninstall.

For the **Companion MSIX**, managed Gateway cleanup uses a current-user interactive scheduled task that survives Companion removal. Its worker, registration script and versioned `cleanup-host-<hash>.exe` are copied to `%USERPROFILE%\.openclaw-companion\cleanup\<Companion-package-family>\`. The task runs at logon and every minute through a GUI-subsystem helper that starts Windows PowerShell with `CreateNoWindow`, preventing a desktop console from being created. The helper targets the Windows-provided .NET Framework 4.8 and forwards the worker's exit code, recording failures in `cleanup-host-error.txt` beside the worker. It does not depend on VBScript, the Companion's .NET 10 runtime, or an executable inside the removed package. Versioned helper paths allow an updated Companion to refresh the task without overwriting a running helper.

The worker checks Companion package identity, ownership sentinels and removal events, then removes only the recorded owned Gateway resources after confirmed Companion removal. Polling also retries pending cleanup and provides a second absence check at least 30 seconds after the first when removal-event evidence is unavailable. The exact one-minute interval is a cleanup-latency choice, not an AppX requirement; increasing it would only slow cleanup, not prevent a visible launch. Opening an updated packaged Companion refreshes an existing owned cleanup task with the new action. A Gateway-only package update cannot refresh this Companion-owned task.

For the headless dev/WSL CLI uninstall (`--uninstall --dry-run`, then `--confirm-destructive`), see the [uninstall skill](../.agents/skills/uninstall/SKILL.md). For a **clean retest of native Gateway/MXC, Companion, and managed llama.cpp**, use its [hard-clean procedure](../.agents/skills/uninstall/HARD-CLEAN.md). This inventories exact targets and requires confirmation before teardown, package removal, or residual-state deletion. Shared model caches, unrelated profiles, and WSL are preserved unless explicitly included.

On a demo/test device without Copilot, copy [scripts/clean-uninstall.ps1](../scripts/clean-uninstall.ps1) and run it with Windows PowerShell 5.1. Running without arguments previews the scope; `-ConfirmDestructive` applies it. Use `-All` to include dev state, receipt-backed cached models, and owned release/dev WSL distros; add `-ExcludeCachedModels` to preserve external shared cached weights. It still requires `-ConfirmDestructive` to apply and does not discover custom profile paths. Read the [standalone options and preservation rules](../.agents/skills/uninstall/HARD-CLEAN.md#standalone-script-no-copilot-required) before expanding the scope or removing shared model weights.
Expand Down
22 changes: 22 additions & 0 deletions docs/TEST_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ shared thread-pool scheduling latency.

### Additional projects

Gateway cleanup task registration, refresh, quoting and failure propagation run
in the ordinary SetupEngine suite. The registration fixture replaces only the
persistence boundary and cannot register a real scheduled task. The two desktop
visibility tests use the existing integration opt-in because their positive
control deliberately displays a classic console. Run them on an interactive
Windows desktop without concurrent desktop automation:

```powershell
$env:OPENCLAW_REPO_ROOT = (Get-Location).Path
$env:OPENCLAW_RUN_INTEGRATION = '1'
dotnet test .\tests\OpenClaw.SetupEngine.Tests\OpenClaw.SetupEngine.Tests.csproj --no-restore --filter 'FullyQualifiedName~GatewayCleanupTaskTests'
```

The visibility contract is no on-screen visible window or taskbar entry, not
absence of an HWND. WinEvent observations and periodic window sampling cover
startup through worker exit; native UI Automation checks the shell taskbar,
including visible custom-shell surfaces when present. The visible-console positive control
must be detected by both window observers and actual taskbar enumeration.
Unavailable shell/provider access fails the selected proof instead of treating
a window-style heuristic as taskbar evidence. Sampling gaps remain a limit of
the measurement, not proof that an arbitrarily short transient is impossible.

The chat composer's real keyboard regression is opt-in because it uses Windows
`SendInput` on a visible, focused proof window. Run it only on an isolated
interactive desktop with no concurrent computer-use or human input, Caps Lock
Expand Down
1 change: 1 addition & 0 deletions openclaw-windows-node.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
</Project>
<Project Path="src/OpenClawTray.FunctionalUI/OpenClawTray.FunctionalUI.csproj" />
<Project Path="src/OpenClaw.SetupEngine/OpenClaw.SetupEngine.csproj" />
<Project Path="src/OpenClaw.GatewayCleanupHost/OpenClaw.GatewayCleanupHost.csproj" />
<Project Path="src/OpenClaw.SetupEngine.UI/OpenClaw.SetupEngine.UI.csproj">
<Platform Solution="*|Any CPU" Project="x64" />
<Platform Solution="*|x64" Project="x64" />
Expand Down
Loading
Loading