Skip to content
Merged
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
35 changes: 21 additions & 14 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ and ARM64 separately.
app execution alias and declares the `OpenClaw.Gateway` MSIX identity.
- The package contains an expanded, read-only OpenClaw application tree.
`HostOptions` resolves `app\openclaw.mjs` directly from the package.
- `openclaw` resolves device-installed Node.js, confirms the packaged entry
point exists, and forwards every argument unchanged to `openclaw.mjs`.
- `clawctl setup` is a read-only readiness check for compatible Node.js and the
packaged entry point. Runtime launches do not hash or walk package files.
- `openclaw` resolves the Node.js executable extracted into package LocalState,
confirms the packaged entry point exists, and forwards every argument
unchanged to `openclaw.mjs`.
- `clawctl setup` validates and reuses or repairs the architecture-specific
bundled Node.js runtime in versioned package LocalState and verifies the
packaged entry point. Runtime launches do not hash or walk application files.
- `clawctl` parses its own arguments with System.CommandLine
(`ClawCtlCommandLine` builds the tree; `Program.RunControlAsync` invokes it).
Help, usage, version, and completion are library behavior; parse errors exit
Expand All @@ -70,16 +72,17 @@ and ARM64 separately.
- `GatewayLauncher` starts Node without a shell, uses `ArgumentList`, inherits
the console streams, and sets `OPENCLAW_SUPERVISOR_MODE=external` plus
`OPENCLAW_NO_AUTO_UPDATE=1`. The child process exit code is the launcher exit
code.
code. Only the child environment prepends the bundled runtime to `PATH`.
- Diagnostics are written to packaged LocalState (or
`%LOCALAPPDATA%\OpenClawGatewayMSIX` outside an MSIX context) with a named
mutex so concurrent processes append complete records.
- The GitHub workflow first builds and packs a pinned
`openclaw/openclaw` revision on Linux. Windows matrix jobs use
`Build-Payload.ps1` to produce x64/ARM64 expanded trees and build metadata,
then `Build-MSIX.ps1` to reject bundled Node.js, build the application
inventory, publish the NativeAOT host, validate package contents, and emit
MSIX metadata.
- The GitHub workflow first builds and packs a pinned `openclaw/openclaw`
revision on Linux using that revision's `setup-node-env` action. The resolved
Node.js version flows through `source.json` and `payload-metadata.json`;
Windows payload builds use the same version. `Build-MSIX.ps1` downloads its
matching official archive, rejects Node.js from the application payload,
builds the application inventory, publishes the NativeAOT host, validates
package contents, and emits MSIX metadata including the runtime hash.
- Unsigned artifacts are the normal PR/push output. Test signing uses a
temporary runner-local certificate. Official signing is gated to `main` and
the immutable upstream commit in `release-policy.json`; signing inputs are
Expand Down Expand Up @@ -111,15 +114,19 @@ and ARM64 separately.
- Treat launcher arguments as OpenClaw-owned. Do not add host-only switches,
consume `--`, rewrite arguments, or block upstream commands; tests explicitly
protect transparent forwarding.
- Preserve direct execution from the immutable package and the caller's
working directory. Do not add runtime extraction, copying, hashing, or
inventory walks.
- Preserve direct execution of `app\openclaw.mjs` from the immutable package
and the caller's working directory. Node.js extraction belongs only to
`clawctl setup` and targets versioned package LocalState; do not copy the
OpenClaw application payload.
- The build-time inventory is a release trust boundary. Keep safe unique paths,
lengths, and SHA-256 values synchronized across composition and signing
validation.
- Keep x64 and ARM64 behavior synchronized across the workflow matrix, scripts,
project runtime identifiers, manifest content, payload metadata, and signing
validation.
- Do not add a packaging-side Node.js version pin or support-range policy.
The selected upstream toolchain owns version selection; package composition
supplies `NodeRuntimeArchiveFileName`, and the host reads the archive name.
- Official releases combine the x64 and ARM64 packages into one signed
`.msixbundle` while retaining signed standalone packages for explicit
architecture-specific deployment. Compose the bundle before signing; bundle
Expand Down
30 changes: 14 additions & 16 deletions .github/workflows/gateway-msix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ permissions:
contents: read

env:
NODE_VERSION: 24.16.0
PNPM_VERSION: 11.15.1
OPENCLAW_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.openclaw_ref || '0965053fe6b9341776df147a6934b7485c60b5ca' }}
PACKAGING_ROOT: .

Expand Down Expand Up @@ -84,6 +82,10 @@ jobs:
run: >
.\scripts\Test-SigningInputs.Tests.ps1

- name: Test Node.js packaging inputs
shell: pwsh
run: .\scripts\Test-NodeRuntimeInputs.Tests.ps1

- name: Test signing workflow configuration
shell: pwsh
run: >
Expand All @@ -110,32 +112,26 @@ jobs:
outputs:
source_sha: ${{ steps.source.outputs.sha }}
package_version: ${{ steps.source.outputs.version }}
node_version: ${{ steps.source.outputs.node_version }}
steps:
- name: Check out OpenClaw source
uses: actions/checkout@v7
with:
repository: openclaw/openclaw
ref: ${{ env.OPENCLAW_REF }}
path: openclaw-source
persist-credentials: false
fetch-depth: 1

- name: Set up Node.js
uses: actions/setup-node@v6
- name: Set up upstream Node.js and pnpm
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}

- name: Enable pnpm
run: |
corepack enable
corepack prepare "pnpm@${PNPM_VERSION}" --activate
install-bun: "false"
install-deps: "false"

- name: Install dependencies
working-directory: openclaw-source
run: pnpm install --frozen-lockfile

- name: Build OpenClaw
working-directory: openclaw-source
env:
OPENCLAW_CONTROL_UI_RELEASE_BUILD: "1"
run: |
Expand All @@ -144,7 +140,6 @@ jobs:

- name: Pack npm package
id: source
working-directory: openclaw-source
run: |
set -euo pipefail
artifact_dir="${RUNNER_TEMP}/openclaw-package"
Expand All @@ -158,15 +153,18 @@ jobs:

source_sha="$(git rev-parse HEAD)"
package_version="$(node -p "require('./package.json').version")"
node_version="$(node -p 'process.versions.node')"
echo "sha=${source_sha}" >> "${GITHUB_OUTPUT}"
echo "version=${package_version}" >> "${GITHUB_OUTPUT}"
echo "node_version=${node_version}" >> "${GITHUB_OUTPUT}"

cat > "${artifact_dir}/source.json" <<EOF
{
"repository": "https://github.com/openclaw/openclaw",
"requestedRef": "${OPENCLAW_REF}",
"resolvedCommit": "${source_sha}",
"packageVersion": "${package_version}"
"packageVersion": "${package_version}",
"nodeVersion": "${node_version}"
}
EOF

Expand Down Expand Up @@ -197,7 +195,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
node-version: ${{ needs.build-package.outputs.node_version }}

- name: Download intermediate package
uses: actions/download-artifact@v8
Expand Down
11 changes: 8 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,14 @@ or package version logic:

```powershell
.\scripts\Test-SigningInputs.Tests.ps1
.\scripts\Test-NodeRuntimeInputs.Tests.ps1
.\scripts\Test-WorkflowPackageVersion.Tests.ps1
.\scripts\Test-GitHooks.Tests.ps1
```

The Node.js input suite requires Node.js and npm. It builds a dependency-free
local fixture; it does not download or build OpenClaw.

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:
Expand Down Expand Up @@ -167,9 +171,10 @@ bypassable, and required CI checks remain authoritative.
consume `--`, rewrite arguments, or block upstream commands. The
System.CommandLine tree covers `clawctl` only; the `openclaw` entrypoint must
keep forwarding its argument vector without parsing it.
- Preserve direct execution from the read-only MSIX package. `clawctl setup`
is a readiness check; do not add runtime extraction, copying, repair, or
launcher-managed package state under the user profile.
- Preserve direct execution of `app\openclaw.mjs` from the read-only MSIX
package. `clawctl setup` owns idempotent extraction of the bundled Node.js
archive into versioned package LocalState; do not copy the OpenClaw
application payload or use device-installed Node.js.
- Keep x64 and ARM64 behavior synchronized across the workflow matrix, scripts,
project runtime identifiers, manifest content, and signing validation.
- Metadata files are part of the release trust chain. Coordinate changes across
Expand Down
85 changes: 49 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ This repository builds a Windows MSIX package containing:
- one .NET 10 NativeAOT launcher exposed through the `openclaw` and `clawctl`
app execution aliases;
- a pinned, verified build of
[`openclaw/openclaw`](https://github.com/openclaw/openclaw).

Node.js is a device prerequisite and is never downloaded or included in the
MSIX.
[`openclaw/openclaw`](https://github.com/openclaw/openclaw);
- the official Node.js archive matching the upstream build's runtime version
and the package architecture.

The package is independent from the
[OpenClaw Windows Node and Companion](https://github.com/openclaw/openclaw-windows-node)
Expand All @@ -29,9 +28,12 @@ own package-management commands. Every argument, including an empty argument
list, is forwarded unchanged to `node openclaw.mjs`, and the launcher returns
the exact child exit code.

Before launching, the host discovers `node.exe` on `PATH` and verifies its
version and executable architecture. It never downloads, installs, or services
Node.js.
Before launching, the host resolves the bundled Node.js executable previously
prepared by `clawctl setup` and checks its PE product version and executable
architecture against the packaged archive without a separate Node.js process.
The runtime directory is prepended
to the child's `PATH` so Node.js, npm, and npx subprocesses use the bundled
tools without changing the user's environment.

The expanded OpenClaw application is installed read-only inside the MSIX.
After resolving Node.js, the launcher confirms that packaged
Expand All @@ -57,7 +59,7 @@ the read-only application directory the workspace.

| Command | Behavior |
|---|---|
| `clawctl setup` | Verify compatible Node.js is on `PATH` and confirm packaged `app\openclaw.mjs` exists. |
| `clawctl setup` | Extract the bundled Node.js runtime when needed and confirm packaged `app\openclaw.mjs` exists. |
| `clawctl --version` | Print the packaged launcher version. |

Bare `clawctl`, `clawctl -h`, and `clawctl --help` print help without changing
Expand All @@ -84,30 +86,29 @@ token reaches that CLI uninterpreted.
Commands such as `doctor`, `gateway`, and `uninstall` belong to the OpenClaw
CLI and must be invoked through `openclaw`.

`setup` requires a compatible device-installed Node.js runtime. Missing,
outdated, malformed, or architecture-incompatible runtimes produce an
actionable error rather than a later process-launch failure.

`clawctl setup` is read-only. It performs no extraction, hashing, inventory
walk, or state mutation.
`setup` extracts the architecture-specific runtime archive from the immutable
MSIX into the package's writable LocalState:
`%LOCALAPPDATA%\Packages\<package-family>\LocalState\OpenClaw\NodeJS\node-v<version>-win-<architecture>`.
Extraction is idempotent, versioned, and serialized across concurrent setup
processes, including different Windows sessions. Setup validates existing
runtimes before reuse, replaces invalid runtimes, and validates extraction
before publishing it.

The launcher places Node.js in a Windows job configured with
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`. The launcher remains alive while Node.js
runs; if the launcher exits or is terminated, Windows terminates Node.js and
its child processes when the job handle closes.

Install the current Node.js LTS release, open a new terminal, optionally check
readiness, then use `openclaw`:
Prepare the bundled runtime once, then use `openclaw`:

```powershell
winget install --id OpenJS.NodeJS.LTS --exact --source winget
clawctl setup
openclaw
```

The packaged OpenClaw revision accepts Node.js
`>=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0`. The launcher keeps this
requirement in one shared validator used by `clawctl` and `openclaw`.
When an MSIX update changes the bundled Node.js version, run `clawctl setup`
Comment thread
xlinush marked this conversation as resolved.
again before launching OpenClaw. Previously extracted versions are left in
place so an update does not remove a running process's runtime.

## Selecting the OpenClaw revision

Expand All @@ -123,6 +124,14 @@ For a one-time override, run **Build OpenClaw Gateway MSIX** manually and
provide a tag, branch, or preferably a full 40-character commit SHA in
`openclaw_ref`.

The source build uses that revision's `.github/actions/setup-node-env` action
to select Node.js and pnpm. Its resolved Node.js version is recorded in
`source.json`, reused for both Windows payload builds, and carried in
`payload-metadata.json`. Package composition downloads that exact version;
the launcher derives its runtime version and LocalState path from the bundled
archive name. There is no separate packaging-side Node.js version pin or
runtime-support policy.

The payload artifact records the requested ref and resolved upstream commit in
`payload-metadata.json`. That build-only file is not embedded in the MSIX.
`msix-metadata.json` records both the packaging repository commit and bundled
Expand All @@ -149,12 +158,14 @@ dotnet test .\OpenClaw.Gateway.MSIX.slnx `
```

`scripts\Build-Payload.ps1` npm-installs an OpenClaw package into an expanded,
architecture-specific application tree. `scripts\Build-MSIX.ps1` copies that
tree into package content, rejects any Node.js executable or runtime archive,
creates a per-file inventory, and then creates an unsigned NativeAOT MSIX.
architecture-specific application tree. `scripts\Build-MSIX.ps1` downloads
the official Node.js archive matching the payload's recorded build version
and architecture, copies both inputs into package content, rejects Node.js
inside the application payload, creates a per-file inventory, and then creates
an unsigned NativeAOT MSIX.
`scripts\Build-LocalMSIX.ps1` can reuse a successful workflow payload or a
local payload directory. The Node.js used by the payload build jobs is build
infrastructure only and is not copied into the MSIX.
local payload directory. `-NodeArchivePath` can supply an already-downloaded
archive, but its version and architecture must match the payload metadata.

Normal pull-request and push workflows publish unsigned packages for
validation. Manual runs support three signing modes:
Expand Down Expand Up @@ -232,6 +243,8 @@ is recorded in `release-policy.json`.
| Data | Default path |
|---|---|
| OpenClaw application files | Read-only MSIX package `app` directory |
| Bundled Node.js archive | Read-only MSIX package `runtime` directory |
| Extracted Node.js runtime | `%LOCALAPPDATA%\Packages\<package-family>\LocalState\OpenClaw\NodeJS\node-v<version>-win-<architecture>` |
| OpenClaw configuration and user state | `%USERPROFILE%\.openclaw` |
| Launcher diagnostics | `%LOCALAPPDATA%\Packages\<package-family>\LocalState\OpenClawGatewayMSIX\Logs\openclaw.log` |

Expand All @@ -245,18 +258,18 @@ removing the MSIX.
## Integrity and isolation boundary

The payload build emits an expanded npm-installed application tree.
`Build-MSIX.ps1` rejects bundled Node.js, copies the tree into package content,
`Build-MSIX.ps1` rejects Node.js from that tree, copies it into package content,
and records every application file's path, length, and SHA-256 in
`payload-files.json`. Package construction verifies that exact inventory
against the generated MSIX. Official signing authorization repeats the
inventory validation, including rejecting missing, changed, duplicate, unsafe,
or unlisted application entries, before requesting signing credentials.

At runtime, Windows' MSIX package integrity and read-only enforcement is the
trust boundary. `openclaw` and `clawctl setup` only check that
`app\openclaw.mjs` exists; neither performs file hashing or an inventory walk.
This avoids redundant startup overhead while keeping package mutation under
Windows servicing control.
`payload-files.json`. It separately validates and hashes the pinned Node.js
archive. Package construction verifies both inputs against the generated MSIX.
Official signing authorization repeats the application inventory and Node.js
archive validation before requesting signing credentials.

At runtime, Windows' MSIX package integrity and read-only enforcement remains
the trust boundary for the application and archive. `clawctl setup` extracts
the archive into versioned package LocalState; `openclaw` launches the packaged
`app\openclaw.mjs` directly with that extracted executable. Neither command
hashes or walks the expanded application inventory.

The longer-term design is to run the Gateway payload in a dedicated isolated
agent session rather than the interactive session where the human user is
Expand Down
7 changes: 7 additions & 0 deletions scripts/Build-LocalMSIX.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ param(

[long]$PayloadRunId,

[string]$NodeArchivePath,

[string]$PackageVersion,

[string]$OutputDirectory
Expand Down Expand Up @@ -55,6 +57,10 @@ if (Test-Path -LiteralPath $OutputDirectory) {
}
New-Item -Path $workDirectory -ItemType Directory -Force | Out-Null

if ($NodeArchivePath) {
$NodeArchivePath = (Resolve-Path -LiteralPath $NodeArchivePath).Path
}

if ($PayloadDirectory) {
$resolvedPayloadDirectory = (Resolve-Path -LiteralPath $PayloadDirectory).Path
}
Expand Down Expand Up @@ -135,6 +141,7 @@ try {
Write-Host "Building unsigned MSIX version $PackageVersion."
& .\scripts\Build-MSIX.ps1 `
-PayloadDirectory $resolvedPayloadDirectory `
-NodeArchivePath $NodeArchivePath `
-Architecture $Architecture `
-PackageVersion $PackageVersion `
-SourceCommit $sourceCommit `
Expand Down
Loading