Skip to content
Closed
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
10 changes: 10 additions & 0 deletions .github/workflows/gateway-msix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ jobs:
cache: true
cache-dependency-path: Directory.Packages.props

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

- name: Restore
run: dotnet restore .\OpenClaw.Gateway.MSIX.slnx

Expand All @@ -67,6 +72,11 @@ jobs:
run: >
.\scripts\Test-WorkflowPackageVersion.Tests.ps1

- name: Test Gateway isolation plugin
shell: pwsh
run: >
.\scripts\Test-GatewayIsolationPlugin.Tests.ps1

build-package:
name: Build OpenClaw npm package
runs-on: ubuntu-latest
Expand Down
35 changes: 24 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,18 @@ copy, repair, or otherwise change package files at runtime.
Every OpenClaw child process runs with
`OPENCLAW_SUPERVISOR_MODE=external`,
`OPENCLAW_SERVICE_REPAIR_POLICY=external`, and
`OPENCLAW_NO_AUTO_UPDATE=1`. These declare external lifecycle ownership,
prevent doctor-owned service repair, and disable configured background
auto-updates. The pinned OpenClaw `v2026.8.2` release honors external supervisor
mode by refusing native service mutation and OpenClaw self-update with guidance
to use the external supervisor's workflow. This behavior belongs to upstream
OpenClaw; the launcher does not reserve, reject, or rewrite upstream command
arguments.
`OPENCLAW_NO_AUTO_UPDATE=1`. It also reports the selected Windows Gateway
session mode through the process-stable
`CLAWCTL_GATEWAY_ISOLATION=enabled|disabled` environment variable. The current
interactive-session launch path reports `disabled`; the future isolated-session
launch path will select `enabled` when that session switch is implemented.
These values declare external lifecycle ownership, prevent doctor-owned service
repair, disable configured background auto-updates, and expose diagnostic
isolation status without claiming independent attestation. The pinned OpenClaw
`v2026.8.2` release honors external supervisor mode by refusing native service
mutation and OpenClaw self-update with guidance to use the external supervisor's
workflow. This behavior belongs to upstream OpenClaw; the launcher does not
reserve, reject, or rewrite upstream command arguments.
OpenClaw inherits the terminal's working directory; the launcher does not make
the read-only application directory the workspace.

Expand Down Expand Up @@ -100,7 +105,10 @@ both:
Changing only the workflow-dispatch default does not change automatic builds.
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`.
`openclaw_ref`. Payload composition validates that the selected OpenClaw
runtime can discover and load the packaging-owned Gateway Isolation plugin
with its required read-only route shape; incompatible older refs fail instead
of producing a package without status UI.

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.
Expand All @@ -125,9 +133,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 and provisions the packaging-owned,
enabled-by-default Gateway Isolation plugin into OpenClaw's bundled plugin
directory. The plugin adds a read-only **Gateway Isolation** tab to the Control
group and serves it through an authenticated, sandboxed plugin route. It reads
only the launch-time `CLAWCTL_GATEWAY_ISOLATION` value and registers no mutation
RPC or process control. `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.
`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.
Expand Down
116 changes: 116 additions & 0 deletions docs/validation/pr-28/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Gateway Isolation validation

## Why is this change being made?

PR #28 adds launcher-reported Gateway isolation status, a read-only authenticated
Control UI tab, and payload provisioning. This evidence covers those implemented
contracts against production source `9aa1df286c2b6fd59b4c101201ddae0ae6000324`.

## What changed?

Added reusable validation harnesses, expanded regression tests, named test results,
HTTP/browser assertions, runtime registration output, and authentic screenshots.
Launcher and plugin production code are unchanged.

## How was the change tested?

### Evidence matrix

| Implemented contract | Executed check and result | Proof |
|---|---|---|
| Interactive launcher selects `disabled` | Real NativeAOT `openclaw.exe` started the Gateway with inherited `CLAWCTL_GATEWAY_ISOLATION=enabled`. The child Gateway and authenticated Control UI reported **Disabled**, proving the launcher overrides inherited input | [Runtime matrix: launcher-disabled](runtime-matrix.json), [screenshot](launcher-disabled.png) |
| Both exact values are supported | Real Gateway processes with explicit `enabled` and `disabled` launcher-input fixtures rendered the correct state, green/warning tone, and inverse CLI command | [Runtime matrix](runtime-matrix.json), [Enabled](fixture-enabled.png), [Disabled](fixture-disabled.png) |
| Invalid or absent input fails closed | Missing, `invalid`, `ENABLED`, empty, and whitespace-padded input each returned **HTTP 503** and rendered the invalid-launcher-mode diagnostic, with no status badge, CLI command, or Copy control | [Runtime matrix](runtime-matrix.json), [missing](fixture-missing.png), [invalid](fixture-invalid.png), [uppercase](fixture-uppercase.png), [empty](fixture-empty.png), [whitespace](fixture-whitespace.png) |
| Route authentication | Each of eight Gateway runs checked GET and HEAD with no token, a wrong token, and the correct token. Unauthorized requests returned **401**; authorized requests returned **200** or **503** according to input. HEAD had an empty body | [80 HTTP case results](runtime-matrix.json) |
| Read-only route | Authenticated POST, PUT, PATCH, and DELETE returned the same read-only page bytes as GET. A subsequent GET remained identical. This handler serves status for these methods; it does not implement a mutation action | [Runtime matrix](runtime-matrix.json), [command output](runtime-checks.txt) |
| Response hardening | Successful and fail-closed responses had `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, and CSP restricting frame ancestors to self | [Runtime assertions](../../../scripts/validation/gateway-isolation-runtime.mjs) |
| Full authenticated Control UI integration | All eight runs completed normal `hello-ok` handshakes with the matching bundled build, exposed the sidebar tab, and rendered the plugin in `sandbox="allow-scripts"` without `allow-same-origin` | [Runtime matrix](runtime-matrix.json), screenshots below |
| Copy behavior | Clicked Copy for the real launcher and both valid input fixtures. The button reported **Copied**, and browser clipboard readback equaled the exact inverse CLI command | [Runtime matrix](runtime-matrix.json) |
| Clipboard capability branches | Six browser capability fixtures exercised Clipboard API success, API denial with legacy-copy success, and both methods unavailable, for both valid states. Manual fallback kept the exact command selected and showed the manual-copy instruction | [Fixture results](copy-fixtures.json), [output](copy-checks.txt), [harness](../../../scripts/validation/gateway-isolation-copy.mjs) |
| Process-stable reporting | Seven initial inputs were each read exactly once. After plugin construction, repeated changes to the input source did not change any response, including fail-closed responses | [21 passing Node tests](plugin-tests.tap), [test source](../../../plugins/gateway-isolation/index.test.js) |
| Typed launcher selection | Enabled/Disabled mappings and invalid enum rejection passed, alongside argument forwarding, working-directory, process, and entrypoint regressions | [57 named passing .NET results](launcher-tests.json), [test source](../../../tests/OpenClaw.Launcher.Tests/GatewayLauncherTests.cs) |
| Minimal runtime surface | Real launcher-driven runtime inspection reported bundled, enabled, activated, loaded, imported; **one HTTP route**, **zero Gateway methods, tools, services, diagnostics, hooks, commands, discovery services, MCP servers, and LSP servers** | [Sanitized runtime inspection](runtime-registration.json) |
| Control descriptor | Node tests verify the exact label, Control group, order, icon, route, `operator.read` scope, gateway authentication, and exact path matching | [Node results](plugin-tests.tap) |
| Payload acceptance and rejection | **66 passed:** 33 per architecture. Six accepted cases verified exact three-file shipping content, matching hashes, metadata, excluded tests, and environment restoration. Sixty cases rejected missing/conflicting plugin directories, failed runtime inspection, and invalid/missing runtime shapes | [Payload matrix](payload-matrix.json), [transcript](payload-matrix.txt), [reusable harness](../../../scripts/validation/Test-GatewayIsolationPayloadMatrix.ps1) |
| Signing and workflow regressions | Existing signing-input and workflow package-version checks passed | [Command output](policy-checks.txt) |

### Screenshots

**Real NativeAOT launcher, inherited input `enabled`, actual reported state Disabled:**

![Real launcher reports Disabled and copies the enable command](launcher-disabled.png)

**Real Gateway with explicit `enabled` launcher-input fixture:**

![Enabled input fixture displays Enabled and copies the disable command](fixture-enabled.png)

**Real Gateway with missing launcher-input fixture:**

![Missing input fails closed in the authenticated Control UI](fixture-missing.png)

**Real Gateway with invalid launcher-input fixture:**

![Invalid input fails closed in the authenticated Control UI](fixture-invalid.png)

The valid-state fixtures set the process input directly while running the real
Gateway, plugin, authentication, and Control UI. The launcher case runs the actual
published executable. All screenshots are captured after clicking Gateway
Isolation through the normal Control UI. The same browser context is reused
across the matrix, including valid-to-invalid transitions.

### Test environment and provenance

Windows x64 build `10.0.26687.0`, .NET SDK `10.0.401`, Node.js `24.16.0`, and
Microsoft Edge `152.0.4191.66` through Playwright. The application uses an expanded
package layout: the NativeAOT launcher beside `app\openclaw.mjs`.

The pinned OpenClaw `2026.8.2` runtime, commit
`0965053fe6b9341776df147a6934b7485c60b5ca`, came from
[workflow run 34546297901](https://github.com/openclaw/openclaw-windows-packaging/actions/runs/34546297901),
artifact `openclaw-gateway-payload-x64` (`10179486438`). That artifact's packaging
commit was `52f2a53fb62b13499ba2692d81870413fcaf943a`. The three PR-owned plugin
files were provisioned into `app\dist\extensions\gateway-isolation`.

The bundled Control UI was built from unmodified source at the same pinned
OpenClaw commit, with its frozen lockfile and declared pnpm `12.1.0`.
The supported build inputs used the Gateway's canonical build timestamp:

```powershell
$env:GIT_COMMIT = '0965053fe6b9341776df147a6934b7485c60b5ca'
$env:OPENCLAW_BUILD_TIMESTAMP = '2026-09-11T00:24:39.157Z'
$env:OPENCLAW_CONTROL_UI_RELEASE_BUILD = '1'
pnpm install --frozen-lockfile --ignore-scripts
pnpm --dir ui build
```

Gateway and UI reported
`2026.8.2-release-0965053fe6b9-2026-09-11T00-24-39.157Z`.
Served JavaScript matched the rebuilt output hash. Authentication and bundled
build-admission checks ran normally. [SHA-256 fingerprints](hashes.json) identify
the exact launcher, Node runtime, plugin, runtime entrypoint, build metadata,
and UI assets. The runtime matrix also records the tested executable hashes.

### Reproduction

From the repository root, with a published launcher/application layout and the
compatible Node executable available:

```powershell
dotnet test .\OpenClaw.Gateway.MSIX.slnx --configuration Release --no-restore
node --test --test-reporter=tap .\plugins\gateway-isolation\index.test.js
node .\scripts\validation\gateway-isolation-runtime.mjs `
<layout-directory> <node.exe> <evidence-directory>
node .\scripts\validation\gateway-isolation-copy.mjs `
<layout-directory> <evidence-directory>
pwsh -NoProfile -File .\scripts\validation\Test-GatewayIsolationPayloadMatrix.ps1 `
-TestRoot .\.validation-payload-matrix `
-EvidenceDirectory .\docs\validation\pr-28 `
-ProductionSourceCommit 9aa1df286c2b6fd59b4c101201ddae0ae6000324
.\scripts\Test-SigningInputs.Tests.ps1
.\scripts\Test-WorkflowPackageVersion.Tests.ps1
```

The runtime harness creates isolated profiles and random test credentials, checks
that its loopback port is free before starting, and stops only the process trees
it started. After each case it verifies the port is free. Public results contain
assertions, versions, and hashes rather than tokens or machine-local paths.
6 changes: 6 additions & 0 deletions docs/validation/pr-28/copy-checks.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
PASS enabled/clipboard-api: copy success
PASS enabled/legacy-fallback: copy success
PASS enabled/manual-selection: manual text remains selected
PASS disabled/clipboard-api: copy success
PASS disabled/legacy-fallback: copy success
PASS disabled/manual-selection: manual text remains selected
104 changes: 104 additions & 0 deletions docs/validation/pr-28/copy-fixtures.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
{
"kind": "Browser capability fixtures against the actual rendered plugin page",
"completedAt": "2026-09-11T03:55:15.070Z",
"cases": [
{
"mode": "enabled",
"capabilityFixture": "clipboard-api",
"command": "clawctl gateway-isolation disable",
"label": "Copied",
"calls": [
{
"method": "clipboard.writeText",
"value": "clawctl gateway-isolation disable"
}
],
"passed": true
},
{
"mode": "enabled",
"capabilityFixture": "legacy-fallback",
"command": "clawctl gateway-isolation disable",
"label": "Copied",
"calls": [
{
"method": "clipboard.writeText",
"value": "clawctl gateway-isolation disable"
},
{
"method": "execCommand",
"command": "copy",
"selection": "clawctl gateway-isolation disable"
}
],
"passed": true
},
{
"mode": "enabled",
"capabilityFixture": "manual-selection",
"command": "clawctl gateway-isolation disable",
"label": "Selected",
"calls": [
{
"method": "clipboard.writeText",
"value": "clawctl gateway-isolation disable"
},
{
"method": "execCommand",
"command": "copy",
"selection": "clawctl gateway-isolation disable"
}
],
"passed": true
},
{
"mode": "disabled",
"capabilityFixture": "clipboard-api",
"command": "clawctl gateway-isolation enable",
"label": "Copied",
"calls": [
{
"method": "clipboard.writeText",
"value": "clawctl gateway-isolation enable"
}
],
"passed": true
},
{
"mode": "disabled",
"capabilityFixture": "legacy-fallback",
"command": "clawctl gateway-isolation enable",
"label": "Copied",
"calls": [
{
"method": "clipboard.writeText",
"value": "clawctl gateway-isolation enable"
},
{
"method": "execCommand",
"command": "copy",
"selection": "clawctl gateway-isolation enable"
}
],
"passed": true
},
{
"mode": "disabled",
"capabilityFixture": "manual-selection",
"command": "clawctl gateway-isolation enable",
"label": "Selected",
"calls": [
{
"method": "clipboard.writeText",
"value": "clawctl gateway-isolation enable"
},
{
"method": "execCommand",
"command": "copy",
"selection": "clawctl gateway-isolation enable"
}
],
"passed": true
}
]
}
33 changes: 33 additions & 0 deletions docs/validation/pr-28/e2e-results.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"timestamp": "2026-09-11T02:45:20.860Z",
"browser": "152.0.4191.66",
"browserEngine": "Microsoft Edge (headless Chromium)",
"launcher": "NativeAOT win-x64, unpackaged app layout",
"admissionBypasses": false,
"events": [
{
"event": "route-auth-check",
"anonymous": 401,
"authenticated": 200,
"cacheControl": "no-store",
"csp": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; frame-ancestors 'self'"
},
{
"event": "authenticated-hello-ok",
"version": "2026.8.2",
"buildId": "2026.8.2-release-0965053fe6b9-2026-09-11T00-24-39.157Z",
"controlUiBuildSource": "bundled"
},
{
"event": "control-ui-plugin-tab",
"state": "Disabled",
"sandbox": "allow-scripts",
"command": "clawctl gateway-isolation enable",
"realLauncherMode": true
},
{
"event": "copy-affordance",
"result": "Copied"
}
]
}
Binary file added docs/validation/pr-28/fixture-disabled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/validation/pr-28/fixture-empty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/validation/pr-28/fixture-enabled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/validation/pr-28/fixture-invalid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/validation/pr-28/fixture-missing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/validation/pr-28/fixture-uppercase.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/validation/pr-28/fixture-whitespace.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/validation/pr-28/gateway-isolation-e2e.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions docs/validation/pr-28/hashes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"algorithm": "SHA-256",
"packagingSource": "9aa1df286c2b6fd59b4c101201ddae0ae6000324",
"upstreamSource": "0965053fe6b9341776df147a6934b7485c60b5ca",
"files": {
"launcher": "a85063b9caedf346444db4da72fa3ab62a0bf913b6a4b6056b8d0b6d4a476def",
"node": "b3094d0b49f9ad602262a9921551737bb97637c05dd357a06ae98188d7290aa3",
"upstreamNpmArtifact": "d0d5b0a0a2ace6c0a957b785929af6c0c0d69fe5dc5fb47e995db25ec700548f",
"runtimeEntry": "4f4d29770da4f86dbd0e07cbd4d46deab785905dd89ac719033fcfd866fb5d17",
"gatewayBuildInfo": "b5bbc98a27a81d41261ca7984a36126ccad982e3fb1ea46e67234cc007038648",
"plugin": "7f98aed21bf7773f78a25c94c47d4f0f4efd6998c314201869307b798f25d1cd",
"controlUiIndex": "73ac0e6f38896c9d491b45a6c3d23b4990dae139d8ff3cb93da7a33e640bb1e7",
"controlUiEntry": "1430574e4d89d8c43b303057d918fdba41a4a940d436d7feaf8e0122a533e4f6",
"screenshot": "1dd7153742614febf8d00aa08c50f3c3465cdd8a7a3b4cf87e8fa585177a0142"
},
"evidenceFiles": {
"fixture-disabled.png": "9445d0a982b921b77113fa3889278c129b31ba2de50ea591caf2e5af40cd13b4",
"fixture-empty.png": "5b64cdc293b6fa1e6be4d04d5c37c7d0403293e9e8aaaf1576c3b55dcfc2a940",
"fixture-enabled.png": "6912ea4e768ae4a2f8fceff4a64c4f371eb44184b99b061ec6a9fe5599aa4fea",
"fixture-invalid.png": "5b64cdc293b6fa1e6be4d04d5c37c7d0403293e9e8aaaf1576c3b55dcfc2a940",
"fixture-missing.png": "5b64cdc293b6fa1e6be4d04d5c37c7d0403293e9e8aaaf1576c3b55dcfc2a940",
"fixture-uppercase.png": "5b64cdc293b6fa1e6be4d04d5c37c7d0403293e9e8aaaf1576c3b55dcfc2a940",
"fixture-whitespace.png": "5b64cdc293b6fa1e6be4d04d5c37c7d0403293e9e8aaaf1576c3b55dcfc2a940",
"gateway-isolation-e2e.png": "1dd7153742614febf8d00aa08c50f3c3465cdd8a7a3b4cf87e8fa585177a0142",
"launcher-disabled.png": "9445d0a982b921b77113fa3889278c129b31ba2de50ea591caf2e5af40cd13b4"
}
}
Binary file added docs/validation/pr-28/launcher-disabled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading