This repository builds the OpenClaw Gateway MSIX package and the
openclaw.exe NativeAOT launcher it ships. It is Windows-focused and pins the
.NET SDK through global.json. Run every command below from the repository
root in PowerShell 7 (pwsh).
The contributor guides cover the workflows and architecture behind the command summary below:
- PowerShell 7
- The .NET SDK feature band pinned in
global.json - Node.js on
PATHfor the Companion configuration adapter tests. They run the production Node.js expression against fixture SDK modules, not an installed OpenClaw application or user profile. - Visual Studio Build Tools with the Desktop development with C++ workload and the Windows SDK, for NativeAOT publish and MSIX composition
The Copilot CLI configuration in .github\lsp.json enables JavaScript code
intelligence for plugins\gateway-isolation alongside the existing C# server.
Install its optional editor tooling once:
npm install --global typescript typescript-language-serverThe server uses npx --no-install, so it does not download dependencies when
Copilot starts it. In an existing Copilot session, run /lsp reload, then
/lsp test typescript-language-server to check startup.
dotnet restore .\OpenClaw.Gateway.MSIX.slnx
# Canonical quality gate: restore plus a Release rebuild with static analysis.
.\scripts\Test-DotNetQuality.ps1
dotnet test .\OpenClaw.Gateway.MSIX.slnx --configuration Release --no-restoreTest-DotNetQuality.ps1 is the single entry point used by local development,
continuous integration, and the optional pre-push hook, so all three report the
same result. It rebuilds with static analysis and then verifies whitespace and
code style. It always rebuilds: an up-to-date project is skipped and reports no
analyzer diagnostics at all. It never rewrites source.
Run one test by fully qualified name:
dotnet test .\tests\OpenClaw.Launcher.Tests\OpenClaw.Launcher.Tests.csproj `
--configuration Release `
--filter "FullyQualifiedName=OpenClaw.Launcher.Tests.HostOptionsTests.ParseForwardsAllArgumentsUnchanged"Run the PowerShell policy suites when you change the workflow, signing inputs, or package version logic:
.\scripts\Test-SigningInputs.Tests.ps1
.\scripts\Test-NodeRuntimeInputs.Tests.ps1
.\scripts\Test-PackagingRelevance.Tests.ps1
.\scripts\Test-OpenClawCacheKey.Tests.ps1
.\scripts\Test-OpenClawPackage.Tests.ps1
.\scripts\Test-Measure-Coverage.Tests.ps1
.\scripts\Test-MSIXReleaseIdentity.Tests.ps1
.\scripts\Test-OpenClawSource.Tests.ps1
.\scripts\Test-WorkflowPackageVersion.Tests.ps1
.\scripts\Test-GitHooks.Tests.ps1The CodeQL configuration excludes the root
tests tree from analysis. C# default setup uses build-mode: none, so tests
otherwise enter the database even when they are not compiled into the product.
The configuration leaves query selection and the configured threat model alone;
it does not declare profile paths or executable discovery safe.
Committing the file does not activate it. After merge, a maintainer must create
the organization's github-codeql-config-file repository property if necessary
and set this repository's value to .github/codeql/codeql-config.yml.
GitHub documents this default-setup customization.
Confirm the next analysis's augmented configuration contains the exclusion.
No repository or organization settings are changed by the local test.
When changing this configuration, use a CodeQL bundle with C# queries and its
executable on PATH, then run:
.\scripts\Test-CodeQLConfiguration.Tests.ps1Use -CodeQlPath to select a bundle outside PATH. The suite analyzes generated,
dependency-free C# fixtures in temporary directories without executing them. It
checks that test-only sinks and test-origin flows into production sinks disappear,
while attacker-controlled paths, canonicalized paths, executables, working
directories, and command arguments still report. Profile-folder controls must
also keep reporting: their ownership modeling is a separate unresolved issue.
Use coverage as a diagnostic to find gaps in meaningful behavior tests, not as a threshold or a reason by itself to add tests. From the repository root:
.\scripts\Measure-Coverage.ps1The command writes each local report to its own
artifacts\coverage\<timestamp>-<unique-id> directory, so runs started in
the same second do not overwrite each other's test results.
Use -Path 'src\OpenClaw.Launcher\*' to narrow reported files, -Uncovered
to show uncovered lines, -BaselinePath <coverage-file-or-directory> to
compare a prior result, and -Filter '<dotnet-test-filter>' to select tests.
To report an existing result without running tests, use
-CoberturaPath <coverage.cobertura.xml-or-directory>.
When the CI test-host job runs, it publishes its Release TRX test results and
available Debug coverage outputs (raw Cobertura data, TRX, and readable
summary.md) as the gateway-msix-test-results workflow artifact. Successful
coverage collection also adds the summary to the GitHub Actions run summary.
The workflow attempts the upload even when tests fail; coverage runs only
after the Release tests pass. Coverage, summary, or artifact-service failure
remains visible in its step logs and emits a warning, but does not fail the
required host-test job. An upload failure may leave no artifact; inspect the
upload step logs before relying on the results.
The source-selection tests use offline npm and GitHub fixtures.
The Node.js input suite requires Node.js and npm. It builds a dependency-free local fixture, including its install script, for the running Node.js architecture; it does not download or build OpenClaw. CI also runs this suite on the native x64 and ARM64 packaging runners. Payload installation requires Node.js to match the target architecture; npm's CPU flags alone do not change the architecture seen by dependency install scripts.
Run the NativeAOT publish when you change host JSON, reflection, interop, or
anything else that is trimming-sensitive. A JIT dotnet build does not
exercise that path:
$vsInstaller = Join-Path `
([Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFilesX86)) `
'Microsoft Visual Studio\Installer'
$env:Path = "$vsInstaller;$env:Path"
dotnet publish .\src\OpenClaw.Launcher\OpenClaw.Launcher.csproj `
--configuration Release --runtime win-x64 --self-containedRun the native clawctl gate when you change command-line parsing, help,
version output, or the host startup path. The xUnit suite runs under a JIT test
host, so it cannot see the entrypoint alias or root command name that the
launcher derives from native argv[0], and a successful publish is not
execution evidence. The script publishes the scenario driver in
tests\OpenClaw.Launcher.AotSmoke for win-x64 with NativeAOT into a temporary
directory it owns, runs it as clawctl.exe, repeats the run under a wrong
executable name to prove the alias check is real, and removes the directory
afterwards:
.\scripts\Test-NativeAotCli.Tests.ps1The driver calls the same Program.RunAsync that the shipped Main calls, so
startup diagnostics, argument routing, the error boundary, and disposal are all
covered. It must never call Main itself: Main resolves the diagnostic log
under the user's profile, so a gate built on it would append to your real
%LOCALAPPDATA%\OpenClawGatewayMSIX log. Add scenarios by injecting
fixture-owned collaborators through HostStartup — an explicit temporary
diagnostic path, in-memory writers, and Node/launch delegates that cannot start
a real process.
Formatting and analyzer severity are defined by the root .editorconfig. The
analyzer properties themselves live in Directory.Build.props, and the
repository uses only the analyzers that ship with the pinned SDK.
AnalysisMode=All enables the full analyzer set, and
EnforceCodeStyleInBuild runs IDE-style rules during command-line builds.
GenerateDocumentationFile is required for build-time IDE0005; the
resulting CS1591 diagnostic is deliberately suppressed rather than filled
with low-value XML comments. Rule severity belongs in .editorconfig, not in
individual project files.
Continuous integration never rewrites source. To apply formatting locally, review the resulting diff before committing:
dotnet format whitespace .\OpenClaw.Gateway.MSIX.slnx
dotnet format style .\OpenClaw.Gateway.MSIX.slnxTo check without writing files:
dotnet format whitespace .\OpenClaw.Gateway.MSIX.slnx --verify-no-changes
dotnet format style .\OpenClaw.Gateway.MSIX.slnx --verify-no-changes--verify-no-changes exits with code 2 when it finds violations.
The build is warning-free and TreatWarningsAsErrors is on, so any new warning
fails the build. That includes compiler diagnostics and analyzers this
repository has never seen, such as those introduced by an SDK upgrade. When one
appears, fix it or add a narrow suppression with a written rationale next to
the code it applies to. Do not commit a generated suppression baseline, and do
not relax a rule repository-wide to get past a single site.
NuGet audit advisories (NU1901-NU1904) are deliberately excluded from the
error gate. A newly published advisory can appear against an unchanged
dependency graph, and breaking main with no committed change and no in-build
fix helps nobody. They stay visible as warnings and are triaged as security
work.
.gitattributes normalizes tracked text to LF and .editorconfig sets
end_of_line = lf. Avoid whole-file rewrites through Set-Content or
Out-File, which can reintroduce CRLF. Verify with git ls-files --eol.
A checkout always produces LF, so this only bites on files you have just
created. Many Windows editors write CRLF by default, so a brand-new .cs file
can fail the whitespace check before its first commit with
error ENDOFLINE: Fix end of line marker. Run
dotnet format whitespace .\OpenClaw.Gateway.MSIX.slnx to normalize it, or
configure your editor to write LF for this repository.
You can have the quality gate run before every push instead of finding out from CI. The hook is opt in and local to your clone:
.\scripts\Install-GitHooks.ps1That writes the tracked hooks\pre-push into the hooks directory Git consults
for your working tree. When the hook does not skip a push as described below,
it runs Test-DotNetQuality.ps1 and nothing else, so it reports exactly what
CI reports. Rerun the installer after pulling a hook change to refresh it. To
remove it:
.\scripts\Install-GitHooks.ps1 -RemoveThe hook skips the quality script when every file the push changes is
documentation, using the same rule CI uses to skip packaging
(scripts\Get-PackagingRelevance.ps1: *.md, docs/, and LICENSE). The
changed files are the files each new commit changes against its first parent.
A new commit is one that no branch or tag on the destination contains; the
hook asks the destination for its current branches and tags with
git ls-remote during the push, which adds one round trip (about 0.5 s to
GitHub here). A push that only deletes remote refs, or whose new commits
change no files, checks nothing. A push that introduces a root commit, input
the hook does not recognize, a failed query of the destination, and any
failure to determine or classify the changed files run the quality script. The
hook prints one line saying whether it skipped or ran the quality script and
why.
The hook does not trust your local remote-tracking refs, so a ref left behind after its remote branch was deleted cannot hide a commit the destination does not have. Commits that exist on the destination only outside its branches and tags count as new, which can cause an extra run. The hook is a latency shortcut; CI remains authoritative.
A clone has one hooks directory, shared by every linked worktree
(git worktree add). Installing or removing from any worktree therefore
affects all of them, and each push runs the quality script from the worktree
you pushed. Other clones are unaffected.
Both operations are idempotent. Installation refuses to overwrite a pre-push
hook it did not write, removal only deletes a hook carrying its own marker, and
neither touches your global Git configuration or core.hooksPath.
Skip the hook for a single push with git push --no-verify. The hook is a
latency shortcut, not a policy boundary: it lives in one clone, it is
bypassable, and required CI checks remain authoritative.
- Ordinary builds and tests must leave
IncludePackagingContentunset. Packaging builds set it totrue, supply a runtime identifier and platform, and isolate MSIX intermediates underobj\packaging. - Treat launcher arguments as OpenClaw-owned. Do not add host-only switches,
consume
--, rewrite arguments, or block upstream commands. The System.CommandLine tree coversclawctlonly; theopenclawentrypoint must keep forwarding its argument vector without parsing it. - Preserve execution of the packaged
app\openclaw.mjsfrom the read-only MSIX package, but run it inside the isolated session rather than launching it directly.clawctl setupowns session provisioning and writes the setup marker thatopenclawrequires. Node.js is installed into the agent account's profile by the session host, not into package LocalState; do not copy the OpenClaw application payload or use device-installed Node.js. The one exception is narrow and deliberate: the isolated-session identity cannot map packaged files as executable images, soclawctl setupmirrors the dependency packages that carry native artifacts into agent LocalState and redirects resolution to them. That set is discovered by scanning, never hard-coded, and everything else keeps executing from the package. The agent account owns its ownPATHandNODE_OPTIONS: name the runtime directory in the launch request, place the preload on Node's argument vector, and let the preload append itself to the agent's options for child processes. Do not assign the invoking host's values over the agent's. Reclaim a staged root only when nothing is running from it; every launch holds its root for its lifetime, and a held root is left whole for a later setup. - Keep x64 and ARM64 behavior synchronized across the workflow matrix, scripts, project runtime identifiers, manifest content, and signing validation.
- Restore
src\OpenClaw.SessionHost\OpenClaw.SessionHost.csprojseparately with the target runtime andPublishAot=truebeforeBuild-MSIX.ps1publishes it with--no-restore; the ordinary solution restore is not sufficient. - Do not add a packaging-side Node.js version pin or support range. The
selected upstream toolchain owns the version; package composition supplies
NodeRuntimeArchiveFileName, and the host reads the archive name. - Metadata files are part of the release trust chain. Coordinate changes across payload creation, MSIX creation, signing validation, workflow artifacts, and tests.
- Keep official release identity derived from the run's resolved Gateway tag
and the policy's
msixRevision, usingscripts\Get-MSIXReleaseIdentity.ps1. The MSIX identity usesyear.month.VVPN.0: Gateway release sequenceVV, correction digitP, and rebuild digitN. Use revision0for the first MSIX of a Gateway tag and increment only for packaging-only rebuilds of that exact tag. Do not assign package versions or release tags by hand. - Treat published proof releases in
scripts/msix-upgrade-baselines.jsonas immutable transition fixtures. Keep their release asset names and SHA-256 digests pinned. The approved Partner Center identity reset must prove every legacy standalone and bundle identity, remove it, install the candidate with the reserved family name and isolated LocalState, and prove both candidate delivery formats install fresh. Once reserved-identity baselines exist, restore in-place upgrade and LocalState-retention proof. Run the harness only on an isolated clean Windows account; it refuses pre-existing OpenClaw Gateway registrations and cleans up only its own installation. - Use contract-specific source-generated
JsonSerializerContextmetadata. The launcher and session host are NativeAOT and must not introduce reflection-based serialization. - PowerShell scripts set
$ErrorActionPreference = 'Stop'and must also check$LASTEXITCODEafter invoking native tools. - Package versions have four numeric components that each fit in
UInt16. Package dependency versions belong inDirectory.Packages.props.
- A test earns its place by the realistic defect it would catch. Prefer functional and integration tests that drive the real path.
- Assert observable behavior: return values, emitted events, persisted state, exit codes, rendered output. Do not read a source file and assert on string markers of the implementation.
- Tests must never modify real user state. Use the isolated temporary
directories created by
TestDirectoryrather than touching a real OpenClaw profile, packaged LocalState, or an installed MSIX. - Tests must be deterministic: no sleep-based synchronization, hardcoded ports, or inter-test ordering dependencies.
README.md, CONTRIBUTING.md, the root and scoped AGENTS.md files, and
docs\*.md must agree with the source that owns each behavior claim. When
they disagree, source wins — verify the claim and correct the documents rather
than picking whichever reads best.
Check documentation references before you push:
.\scripts\Test-DocReferences.ps1It validates that repository paths named in markdown are actually tracked,
that relative links and their anchors resolve, and that markdown stays LF. It
resolves paths through git ls-files rather than the filesystem, because a
stale untracked build directory can make a renamed project path look valid.
Findings exit nonzero locally. Continuous integration runs the same script with
-Advisory, which annotates the pull request without failing the build, so a
documentation change is never blocked by it. When every changed path in a pull
request is Markdown, under docs\, or LICENSE, as classified by
scripts\Get-PackagingRelevance.ps1, CI skips the host test and packaging
jobs, so this advisory check is the pull request's only documentation
validation. Every other run executes the checker's own suite,
scripts\Test-DocReferences.Tests.ps1, in the host test job.
Three project skills in .github\skills\ support this work:
| Skill | Use it for |
|---|---|
technical-documentation |
Writing, reviewing, or auditing any documentation surface |
docs-refactor |
Rewriting, splitting, or reorganizing a page without losing behavior facts |
deslop |
Behavior-neutral cleanup of a branch diff before code review |
Use the pull request template. Title the PR
type: user-facing description, where type is one of feat, fix,
improve, refactor, docs, or chore. Describe the outcome rather than the
mechanism.
Lead with the problem and user or contributor impact in short, plain-language sentences, followed by a brief explanation and useful evidence. Keep technical details optional, but important risks and required actions visible. Name the exact head SHA your evidence came from and state which validation lanes you did not run.
Keep the description current when review feedback changes the implementation; the body is the durable explanation, not just the comment thread. Keep Allow edits from maintainers enabled so a maintainer can update the branch.
This repository keeps no changelog file. An official release calls
softprops/action-gh-release with generate_release_notes: true, so GitHub
composes the release notes from the titles of the pull requests merged since
the previous release tag. Your pull request title is the release note, which is
why the type: user-facing description form matters.
Larger work may land as a linear stack of dependent PRs, each with a single review boundary. When a change belongs to a stack:
- State
Layer N of M, the immediate parent, and the ordered stack in the PR body. - Validate each layer against its immediate parent, not only against the top of the stack.
- Merge bottom-up, and retarget the child PR to the parent's base before the parent merges.