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
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,20 +315,20 @@ Packaged installs declare camera, microphone, and location capabilities. Windows
# Speak text aloud on the Windows node (requires TTS enabled in Settings and tts.speak allowed on the gateway)
openclaw nodes invoke --node <id> --command tts.speak --params '{"text":"Hello from OpenClaw","provider":"windows"}'

# Execute a command on the Windows node
openclaw nodes invoke --node <id> --command system.run --params '{"command":"Get-Process | Select -First 5","shell":"powershell","timeoutMs":10000}'
# Execute a command on the Windows node (raw node.invoke requires canonical argv)
openclaw gateway call node.invoke --params '{"nodeId":"<id>","command":"system.run","params":{"command":["cmd.exe","/d","/s","/c","echo hello"],"rawCommand":"echo hello","timeoutMs":10000}}' --json

# View exec approval policy
openclaw nodes invoke --node <id> --command system.execApprovals.get

# Update exec approval policy (add custom rules)
openclaw nodes invoke --node <id> --command system.execApprovals.set --params '{"rules":[{"pattern":"echo *","action":"allow"},{"pattern":"*","action":"deny"}],"defaultAction":"deny"}'
# Update exec approvals using baseHash from the preceding get response
openclaw nodes invoke --node <id> --command system.execApprovals.set --params '{"baseHash":"<hash-from-get>","file":{"version":1,"defaults":{"security":"allowlist","ask":"on-miss","askFallback":"deny","autoAllowSkills":false},"agents":{"main":{"security":"allowlist","ask":"on-miss","askFallback":"deny","autoAllowSkills":false,"allowlist":[]}}}}'
```
> 📷 **Camera permission**: Desktop builds rely on Windows Privacy settings. Packaged MSIX builds will show the system consent prompt.

> 🔒 **Exec Policy**: `system.run` is gated by an approval policy on the Windows node at `%LOCALAPPDATA%\OpenClawTray\exec-policy.json` (schema: `{ "defaultAction": "...", "rules": [...] }`). This is separate from gateway-side `~/.openclaw/exec-approvals.json`.
> 🔒 **Exec approvals**: `system.run` is gated by the V2 approval coordinator and `%APPDATA%\OpenClawTray\exec-approvals.json`. The file uses the same `defaults`/`agents`/`allowlist` model as the macOS node host. `system.execApprovals.set` requires the current `baseHash` and rejects stale or unsafe remote updates.
>
> Rules are matched against the full command line. Known wrapper payloads such as `cmd /c ...`, `powershell -Command ...`, `pwsh -EncodedCommand ...`, and `bash -c ...` are also evaluated before execution. Dangerous environment overrides like `PATH`, `PATHEXT`, `NODE_OPTIONS`, `GIT_SSH_COMMAND`, `LD_*`, and `DYLD_*` are rejected.
> Allowlist rules match resolved executable paths, using path-aware wildcards such as `**/git.exe`. Script interpreters and command hosts cannot receive reusable grants. Non-empty custom environments are rejected until they can be identity-bound and displayed safely.

#### Command Center diagnostics

Expand All @@ -341,7 +341,7 @@ Open the status detail/Command Center from the tray menu or with `openclaw://com
- recent activity and node invoke results through the Activity Stream, storing command names/status/duration only (not payloads, screenshots, recordings, or secrets)
>
> ```bash
> openclaw nodes invoke --node <id> --command system.execApprovals.set --params '{"rules":[{"pattern":"powershell.exe","action":"allow"},{"pattern":"pwsh.exe","action":"allow"},{"pattern":"echo *","action":"allow"},{"pattern":"*","action":"deny"}],"defaultAction":"deny"}'
> openclaw nodes invoke --node <id> --command system.execApprovals.set --params '{"baseHash":"<hash-from-get>","file":{"version":1,"defaults":{"security":"allowlist","ask":"off","askFallback":"deny","autoAllowSkills":false},"agents":{"main":{"security":"allowlist","ask":"off","askFallback":"deny","autoAllowSkills":false,"allowlist":[]}}}}'
> ```

> 🔐 **Web Chat secure context**: Remote web chat requires `https://` (or localhost). If using a self-signed cert, trust it in Windows (Trusted Root Certification Authorities) or use an SSH tunnel to localhost.
Expand Down
4 changes: 2 additions & 2 deletions docs/MCP_MODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ MCP startup is reported from the actual listener state. `NodeService.McpStartupE

### Testing

The tray's most interesting code lives in capabilities`system.run` (LocalCommandRunner + ExecApprovalPolicy), `screen.snapshot` (Windows.Graphics.Capture + GraphicsCapturePicker), `canvas.*` (WebView2 with trusted origin enforcement), `camera.snap`/`camera.clip` (MediaCapture + consent prompt), `location.get` (Windows.Devices.Geolocation). All of that has nontrivial Windows-only behavior and almost none of it is currently exercised end-to-end without first standing up a gateway and authenticating.
The tray's most interesting code lives in capabilities: `system.run` (LocalCommandRunner + the V2 exec-approval coordinator), `screen.snapshot` (Windows.Graphics.Capture + GraphicsCapturePicker), `canvas.*` (WebView2 with trusted origin enforcement), `camera.snap`/`camera.clip` (MediaCapture + consent prompt), and `location.get` (Windows.Devices.Geolocation). All of that has nontrivial Windows-only behavior and almost none of it is currently exercised end-to-end without first standing up a gateway and authenticating.

Local MCP changes that. Concrete benefits:

Expand Down Expand Up @@ -260,7 +260,7 @@ The server is built on several defensive layers, not just one. Loopback alone is
Together these three checks force a malicious cross-origin browser fetch into a CORS preflight that we deliberately do not honor (no `Access-Control-Allow-*` is ever emitted), so the actual call is blocked before reaching capability code.
4. **Bearer token.** Every request must include the persistent local MCP bearer token (`Authorization: Bearer <token>`) once the server has created `%APPDATA%\OpenClawTray\mcp-token.txt`. This blocks drive-by local clients that know the port but cannot read the per-user token file.
5. **Concurrency cap.** A semaphore limits in-flight handlers to 8. A misbehaving local client cannot pin every threadpool thread on long-running screen/camera calls.
6. **Capability-level controls remain in force.** `SystemCapability.SetApprovalPolicy(...)` (the exec approval policy) still gates `system.run`. Camera and screen capture still go through Windows consent flows. MCP doesn't bypass any of those.
6. **Capability-level controls remain in force.** The V2 exec-approval coordinator still gates `system.run`. Camera and screen capture still go through Windows consent flows. MCP doesn't bypass any of those.

**Authentication is local bearer-token based.** The token is persistent, generated by the tray, stored in the current user's OpenClawTray data directory, and verified before MCP method dispatch. It is defense-in-depth rather than a hard sandbox boundary: a malicious process already running as the same user may still be able to read user-profile files or invoke native APIs directly. If we need stronger isolation for shared machines or low-trust local processes, the next step is scoped or per-call tokens issued by the tray, not URL ACLs or HTTPS — both add deployment pain without solving the same-user trust problem.

Expand Down
28 changes: 25 additions & 3 deletions docs/WINDOWS_NODE_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ Niche scenario. If the "server" must be Windows for some reason, this works but
| `camera.clip` | ✅ | ✅ | ✅ | ❌ | **✅** | MediaCapture + MediaEncoding |
| `camera.list` | ✅ | ✅ | ✅ | ❌ | **✅** | DeviceInformation.FindAllAsync |
| `screen.record` | ✅ CGWindowListCreateImage | ✅ ReplayKit | ✅ MediaProjection | ❌ | **✅** | Windows.Graphics.Capture |
| `system.run` | ✅ | ❌ | ❌ | ✅ | **✅** | Process.Start (cmd/pwsh) + ExecApprovalPolicy |
| `system.execApprovals` | ❌ | ❌ | ❌ | ❌ | **✅** | JSON policy file (exec-policy.json) |
| `system.run` | ✅ | ❌ | ❌ | ✅ | **✅** | Process.Start + V2 exec-approval coordinator |
| `system.execApprovals` | ❌ | ❌ | ❌ | ❌ | **✅** | V2 store (`exec-approvals.json`) with base-hash CAS |
| `system.notify` | ✅ NSUserNotification | ✅ UNUserNotification | ✅ NotificationManager | ❌ | **✅** | ToastNotificationManager |
| `location.get` | ✅ CLLocationManager | ✅ CLLocationManager | ✅ FusedLocation | ❌ | **✅** | Windows.Devices.Geolocation |
| `device.info/status` | ✅ shared schema | ✅ shared schema | ✅ shared schema | ❌ | **✅** | .NET runtime, storage, network |
Expand Down Expand Up @@ -453,7 +453,29 @@ string stderr = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
```

**Critical:** Exec approvals must be enforced locally, same as macOS/headless nodes. Store in `%APPDATA%\OpenClaw\exec-approvals.json`.
**Critical:** Exec approvals must be enforced locally, same as macOS/headless nodes. The default store is `%APPDATA%\OpenClawTray\exec-approvals.json`; `OPENCLAW_STATE_DIR` overrides that location.

#### Decision: retire Windows V1 exec policy without migration

Windows V1 `exec-policy.json` command-text globs are not evaluated or converted after the
V2 cutover. When no valid V2 policy exists, the node uses an empty allowlist with
prompt-on-miss and deny fallback.

**Consequences:**

- Existing V1 files remain untouched during normal runtime but no longer authorize or deny execution.
- Prior V1 allows require attended V2 reapproval; unattended calls deny.
- Prior V1 denies are not imported and may be explicitly superseded through an attended V2 prompt.
- Malformed or untrusted V2 state remains hard deny and is never replaced from V1 state.

This compatibility break is accepted because V1 matched shell command text while V2 binds
resolved executable identity and canonical argv. Mechanical conversion could widen a narrow
command rule into a reusable `cmd.exe` or PowerShell grant, while retaining both evaluators
would preserve parallel authorization paths and bypass drift.

**Rejected alternatives:** V1 runtime fallback, mechanical rule conversion, and file-triggered
migration UI. Revisit this decision only if measured support impact justifies a separate
compatibility feature.

### Location → Windows.Devices.Geolocation

Expand Down
49 changes: 49 additions & 0 deletions scripts/Uninstall-LocalGateway.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,50 @@ function Resolve-AppDataDir {
return Join-Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)) $DataDirectoryName
}

function Resolve-UsablePathValue {
param([string]$Value)
$trimmed = if ($null -eq $Value) { '' } else { $Value.Trim() }
if ([string]::IsNullOrEmpty($trimmed) -or $trimmed -in @('undefined', 'null')) {
return $null
}
return $trimmed
}

function Expand-ExecApprovalsHomePath {
param([string]$Path, [string]$Home)
if ($Path -eq '~') { return $Home }
if ($Path.StartsWith('~\') -or $Path.StartsWith('~/')) {
return Join-Path $Home $Path.Substring(2)
}
return $Path
}

function Resolve-ExecApprovalsPaths {
param([string]$DataDir)

$legacyPath = Join-Path $DataDir 'exec-approvals.json'
$stateDir = Resolve-UsablePathValue $env:OPENCLAW_STATE_DIR
if (-not $stateDir) { return @($legacyPath) }

$osHome = Resolve-UsablePathValue $env:HOME
if (-not $osHome) { $osHome = Resolve-UsablePathValue $env:USERPROFILE }
if (-not $osHome) {
$osHome = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)
}
if (-not $osHome) { $osHome = (Get-Location).Path }

$openClawHome = Resolve-UsablePathValue $env:OPENCLAW_HOME
$effectiveHome = if ($openClawHome) {
[IO.Path]::GetFullPath((Expand-ExecApprovalsHomePath $openClawHome $osHome))
} else {
[IO.Path]::GetFullPath($osHome)
}
$resolvedStateDir = [IO.Path]::GetFullPath(
(Expand-ExecApprovalsHomePath $stateDir $effectiveHome))
$activePath = Join-Path $resolvedStateDir 'exec-approvals.json'
return @($activePath, $legacyPath) | Select-Object -Unique
}

function Get-JsonPropertyValue {
param(
[object]$Object,
Expand Down Expand Up @@ -451,7 +495,12 @@ function Resolve-AppDataDir {
Remove-FileIfExists -Path (Join-Path $dataDir 'setup-state.json') -Label 'legacy setup-state.json'
Remove-FileIfExists -Path (Join-Path $localDataDir 'setup-state.json') -Label 'setup-state.json'
Remove-FileIfExists -Path (Join-Path $localDataDir 'run.marker') -Label 'run.marker'
foreach ($execApprovalsPath in (Resolve-ExecApprovalsPaths -DataDir $dataDir)) {
Remove-FileIfExists -Path $execApprovalsPath -Label 'exec-approvals.json'
}
Remove-FileIfExists -Path (Join-Path $localDataDir 'exec-approvals.json') -Label 'local legacy exec-approvals.json'
Remove-FileIfExists -Path (Join-Path $dataDir 'exec-policy.json') -Label 'exec-policy.json'
Remove-FileIfExists -Path (Join-Path $localDataDir 'exec-policy.json') -Label 'local exec-policy.json'
Remove-KeepaliveMarker -LocalDataDir $localDataDir

$registryCleanup = Remove-SetupManagedGatewayRecords -DataDir $dataDir
Expand Down
Loading
Loading