Skip to content

fix(chat): preserve history cache correlation - #7

Closed
bkudiess wants to merge 203 commits into
mainfrom
bkudiess-finalize-history-cache-correlation
Closed

fix(chat): preserve history cache correlation#7
bkudiess wants to merge 203 commits into
mainfrom
bkudiess-finalize-history-cache-correlation

Conversation

@bkudiess

@bkudiess bkudiess commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Related to openclaw#894

Scope

  • Combine session and thread tool metadata caches in stable chronological order using cloned entries.
  • Deduplicate only stable (RunId, ToolCallId, LegacyTurn) identities while preserving reused call IDs across distinct runs and legacy turns.
  • Merge richer current-canonicalized identity and safe display arguments into the earliest chronological owner.
  • Give each flattened history row a synthetic history-tool-* identity without propagating unverified positional cached RunId or ToolCallId values.
  • Preserve verified call IDs for structured history.
  • Revalidate cached names, labels, and display arguments through current sanitization on read clones without mutating persisted cache entries.
  • Keep bounded, bidi-safe display behavior and prevent reducer row collapse.

This is a surgical three-file change based on current main at 77141d27ee69728da6c3509fb116de63bd176da4. hideFromChannelProgress is intentionally out of scope.

Validation

  • git diff --check
  • .\build.ps1
  • dotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csproj --no-restore: 3,631 passed, 0 failed, 32 skipped
  • dotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csproj --no-restore: 2,252 passed, 0 failed
  • Focused cache/history tests: 28 passed, 0 failed
  • Deterministic cache ordering and non-collapse repetition: 25 of 25 iterations, 150 of 150 aggregate tests passed
  • NativeToolIdentityScreenshotProofTests and ReactorToolActivityProofTests: 2 passed, 0 failed

Real behavior proof

  • Cross-key duplicate cache records retain the earliest chronological row while merging richer canonical identity, safe labels, and allowlisted arguments.
  • Reused tool call IDs remain distinct across separate run IDs and legacy turns.
  • Flattened history rows receive distinct synthetic identities and do not inherit unverified positional cached correlation IDs, preventing reducer collapse.
  • Mixed structured and flattened history retains the structured call's verified ID while isolating the flattened row under a synthetic ID.
  • Read-time canonicalization, bidi removal, and bounds enforcement operate on clones; regression coverage confirms source cache entries remain unchanged.
  • Real WinUI proof classes passed for trusted identity, safe input rendering, truthful fallback, compact activity grouping, and accessible expansion.

Review

A fresh GPT-5.6 Sol/high independent review found one medium issue: stale persisted IdentityStrength values were compared before current canonicalization. The implementation now canonicalizes cloned entries before merge, with a regression proving an invalid stale explicit identity cannot suppress a later valid specific identity. A fresh GPT-5.6 Sol/high rubber-duck pass after that fix found no remaining issues and confirmed the scope remains surgical.

PR openclaw#1111 interaction

PR openclaw#1111 is complementary and remains unmodified. Its current head is be50098bae12e6291ad60b2196ca50996837f94e, rebased/merged with base snapshot 9f9a8eda6340c7ec6f0acbc410de4f6b7d24a3ce, and it is now marked ready rather than draft. Its semantic patch still owns persisted call ID aliases, message-level errors, string-encoded safe arguments, and failed-tool grouping.

The only nearby provider change in openclaw#1111 replaces structured-history argument extraction with ExtractSafeToolDisplayArgsValue and removes the old converter. This PR changes the separate flattened-history branches and cache combination/read-validation seam. There is no direct changed-line overlap in the source or tests. The changes are complementary: openclaw#1111 improves verified structured payload projection; this PR corrects positional cache correlation and flattened row identity.

Release boundary

openclaw#894 must remain open after this PR lands. Closeout requires a release that includes openclaw#1078, openclaw#1114, and this follow-up.

bkudiess and others added 30 commits June 24, 2026 12:41
Adds app-managed WSL gateway recovery actions to the setup wizard, moves shared WSL gateway helpers into OpenClaw.Connection, and documents the recovery behavior.
Fixes the Settings hotkey to open the Settings page and refreshes the MXC helper restore so stale helper binaries are not reused while keeping npm restore incremental.
Ports the MXC probe/SDK 0.7.0 work and inbound pairing approval dialog from the mistaken master branch target onto main.

Maintainer repair included:
- Preserving non-actionable handling for ambiguous legacy pairing fallback IDs in the new approval queue.
- Adding regression coverage for device and node fallback ambiguity.
- Version-stamping MXC SDK restore so stale node_modules cannot satisfy the 0.7.0 port.

Validation:
- Local .\build.ps1 passed.
- Local Shared tests passed: 2411 passed / 29 skipped.
- Local Tray tests passed: 1127 passed.
- Local Connection tests passed: 373 passed.
- GitHub CI passed on repaired head 7be0688: test, E2E setup-connect, E2E revocation-recovery, E2E network-recovery, build win-x64, build win-arm64, repo-hygiene.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Routes local system.run approval prompts into native chat when an interactive chat surface is available, while preserving the native approval dialog fallback.

Maintainer updates:
- Rebased onto current main after the MXC/pairing port landed.
- Kept the deleted Chat Explorations fake provider removed during rebase.
- Removed a duplicate ChatWindow.Show() call introduced during the branch hardening pass.

Validation:
- Local .\build.ps1 passed.
- Local Shared tests passed: 2417 passed / 29 skipped.
- Local Tray tests passed: 1154 passed.
- Independent code review found only the duplicate Show() issue, now fixed.
- Independent security review found no concrete security vulnerabilities.
- GitHub CI passed on repaired head ae301c0: repo-hygiene, test, E2E setup-connect, E2E revocation-recovery, E2E network-recovery, build win-x64, build win-arm64, Socket Security.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merged after daily triage: focused scope, green CI, and low-risk validation evidence.
Merged after daily triage: focused scope, green CI, and low-risk validation evidence.
Merged after daily triage: focused scope, green CI, and low-risk validation evidence.
Merged after rerunning the failed setup-connect shard successfully; follow-on win-x64 and win-arm64 builds also passed.
Adds a static gateway protocol snapshot guard covering sessions, files, commands, and compaction, with maintainer repair so no-parameter used request methods are also checked for payload drift.
Improves Cron History so users can inspect full sanitized run responses, preserves expanded/running state while refreshing, adds response-aware cron.run feedback, and includes compatibility coverage for legacy cron.run payload fallback.
Route selected background issues into the in-app Notifications page, including connection, sandbox, channel, pairing, and cron failures. Partially addresses openclaw#820; it does not close the broader notification-routing issue.
* Make A2UI dataModelUpdate.valueArray first-class

The v0.8 `dataModelUpdate.valueArray` typed value was silently dropped by the
WinUI parser: `DataModelEntry` had no `ValueArray` field and `ToJsonNode()`
returned null for it. Only `valueString/Number/Boolean/valueMap` were handled.
This broke seeding an array into a surface's data model (e.g. a multi-select
`MultipleChoice` bound to a path), even though `valueArray` is part of the v0.8
protocol (docs/a2ui/protocol.md §2.2 and data-and-actions.md).

Changes:
- Parser (A2UIProtocol.cs): add `DataModelEntry.ValueArray`; `ToJsonNode()`
  emits a `JsonArray`; new `ParseValueArray`/`ParseArrayElement` handle
  value-typed object elements, bare primitives (`["a",1,true]`), nested
  maps/arrays, and preserve JSON null as a stable index slot.
- DoS guard (DataModelStore.cs): the 32-deep depth bound now recurses
  `valueArray` as well as `valueMap`.
- Security (SecretRedactor.cs): the array branch now redacts registered/
  denylisted element paths instead of only recursing, so a secret seeded into
  an array (e.g. an obscured field bound to /codes/0) no longer leaks via
  canvas.a2ui.dump.
- Docs (SKILL.md): correct the stale "arrays are not first-class" note.

Tests: parser coverage for valueArray (strings, mixed scalars, maps, nested
arrays, bare primitives, empty, null slots) plus valueMap/scalar regressions;
secret-redaction regressions for secrets inside arrays; store-level base-path
landing and depth-guard rejection; and an end-to-end MultipleChoice surface
seeded via valueArray that asserts both the snapshot and the rendered
preselection.

Validation: build.ps1 (all projects), Shared.Tests, Tray.Tests, and A2UI
UITests all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix valueArray redaction and notifications

Reject non-canonical array pointer indices for data-model array access, notify descendant subscribers when container values are replaced, and redact registered secret descendants when action contexts or snapshots include parent paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
* Fix canvas navigation URL handling

* Fix canvas navigate dispatcher timeout

Route canvas.navigate through the same dispatcher timeout guard used by canvas.eval and canvas.snapshot so stalled UI dispatch cannot hang MCP tool calls indefinitely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Scott Hanselman <scott@hanselman.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add TTS provider fallback and tts.status readiness command

When the requested/configured TTS provider isn't usable (no ElevenLabs key,
Piper voice not downloaded), gracefully fall back to Windows TTS instead of
throwing, and report the effective vs requested provider plus a fellBack flag.
A stale configured Windows voice now degrades to the system default voice so
the fallback always speaks.

Add a tts.status node command that reports per-provider readiness
(ready/needs-api-key/needs-voice/voice-not-downloaded/unavailable) plus the
configured/effective provider and willFallBack. PII-free. Wired across
DangerousCommands gating, MCP descriptions, skill.md, and the setup allowlist
export. Fix a stale pre-GA SHA-256 TODO docstring in PiperVoiceManager
(verification is already implemented and enforced by AssetHashPinningTests).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Preserve configured TTS fallback for chat playback

Leave chat playback provider unset so TextToSpeechService can distinguish configured/default playback from explicit provider requests and apply the new Windows fallback when the configured provider is unavailable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
Validate screen.snapshot image formats before invoking the capture backend, normalize jpg to jpeg, and derive the response data URI MIME type from the validated format instead of the backend echo.

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…w#824)

* Wizard protocol resilience: progress/non-interactive steps, auth prompts, restart hardening

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Harden wizard progress polling budget

Allow a single long-running gateway progress step to use the full bounded progress budget instead of failing after the previous shorter per-step cap. Share the progress constants between the setup runner and UI and cover the intended budget in tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
Measure the tray popup against its final client width and apply pixel-exact window sizing so wrapped content determines height deterministically.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Normalize the Sandbox page toggle off when MXC is definitively unavailable, reject turning it back on in the host-fallback mode, and preserve strict fallback blocking so users who opted into command denial remain protected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Squashed from PR openclaw#800 after maintainer review and local fixes.\n\nLocal validation:\n- .\\build.ps1\n- dotnet test .\\tests\\OpenClaw.Shared.Tests\\OpenClaw.Shared.Tests.csproj --no-restore\n- dotnet test .\\tests\\OpenClaw.Tray.Tests\\OpenClaw.Tray.Tests.csproj --no-restore\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: add Gateway MXC runtime E2E proof

* test: probe MXC E2E gate with discovered wxc-exec

* test: extend Gateway MXC E2E timeout budgets

* test: avoid blocked redirection in Gateway MXC deny proof

* test: prove MXC denied copy reaches destination

* test: document serialized MXC E2E collection gate

* test: raise MXC E2E sandbox timeout cap

* test: approve MXC denied-write copy proof step

* test: sync MXC approval proof diagnostics

* test: narrow MXC exec approval proof policy

* ci: extend setup-connect E2E timeout

Give only the setup-connect E2E shard extra time for the combined setup and MXC runtime proof fixtures while leaving the other E2E shards on the existing budget.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: omit unsupported MXC denied paths

Windows wxc-exec rejects filesystem.deniedPaths before launching the sandboxed command. Keep denied paths as the local allow-list filter, but omit the unsupported backend field so AppContainer default-deny can run the command while still stripping parent/exact grants over sensitive paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Vitor Cepeda Lopes <7040636+TheAngryPit@users.noreply.github.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
)

* Improve remote gateway setup and connection error recovery

Add RemoteGatewayClassifier + GatewayErrorClassifier in OpenClaw.Shared and
wire ConnectionPage remote setup advice, scope/token-drift/TLS recovery, and
localized strings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clear stale SSH fields for setup-code flow

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
* Node mode UI: surface MCP-only/connecting states and repair gateway-node gating

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix MCP-only node status visibility

Surface the local MCP-only node card even when no gateway/operator session exists, and make the reconnect-backoff test wait for server-side accept publication.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Keep MCP-only mode from joining gateways

Gate the post-operator local NodeService auto-connect on EnableNodeMode so local MCP-only serving does not create gateway node pairing requests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Request node reconnect in shared-token setup E2E

The shared-token setup path can now remain MCP-only after operator approval, so the E2E needs to request node reconnect explicitly before waiting for a node credential.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
)

A connection/auth failure rendered two error bars: the global top-window
InfoBar plus an in-page "Connection Error" InfoBar on the Connection page.
The auth pipeline also published two global notifications for one failure,
which forced the banner action to degrade to "Show more".

Remove the in-page AuthErrorBar and consolidate to one top banner that:
- carries the gateway/node error states the in-page bar covered
- always routes the user to the Connection page via an "Open Connection"
  action (connection notifications are prioritized as the visible banner,
  preferring an actionable one over an action-less transient)
- renders as a single thin line: bold headline + " - detail", with the
  action as a right-aligned hyperlink (Windows-aligned InfoBar)

Other changes:
- Stop publishing the duplicate connection:authentication-failed banner;
  the snapshot-driven connection:issue notification is the single source.
- Re-home transient errors: WSL host-action failures use the inline card
  status (or a top notification when the card is hidden); connect/switch
  failures publish via ShowTransientConnectionError on the same banner id.
- Remove now-dead resw strings (ConnectionPage_AuthGuidance*,
  ConnectionPage_ConnectFailed) across all five locales.

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add tray live dashboard glance

Add a compact status summary to the tray flyout that surfaces gateway health, refresh freshness, session and usage metrics, and the current session preview before the existing action rows.

Wire a Diagnostics action into the tray menu and keep the summary computation in a testable, render-free builder shared by existing usage/session rows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove message previews from tray glance

Keep the tray dashboard focused on health, freshness, session metadata, and usage while avoiding conversation message text in the top-level flyout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Avoid activity snippets in tray dashboard glance

Keep the top-level tray dashboard limited to stable metadata so command, query, path, or URL activity text stays out of the glance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
Move notifications from the footer rail into a title-bar bell flyout and replace the compact connection status dots with a status pill plus detail flyout. Keep the status presentation aligned with the Connection page by deriving node state from shared presenter logic and the node-mode/capability settings.

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port PR openclaw#562 onto current main and harden diagnostics redaction so JSONL remains parseable while sensitive values, metadata keys, paths, emails, headers, and compound JSON values are sanitized before export.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
karkarl and others added 29 commits August 4, 2026 23:21
Migrate chat and Connection presentation to WinUI theme resources, preserve system-colored Hub navigation in High Contrast, and add runtime coverage for isolated tool and compaction styles.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use stable native anchoring for streamed chat growth and validate immutable queued tail requests against current identity, bounds, item count, and generation before native navigation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* feat(sessions): consume flattened gateway classification

* test(sessions): fix flattened facts test syntax

* fix(sessions): preserve legacy display fallback

* fix(sessions): reject raw key display names

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: bakudies@microsoft.com <bakudies@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* docs: clean up README to match main repo polish

- Remove outdated screenshots (will add fresh ones separately)
- Add badge row (CI, .NET, license, Discord)
- Condense intro to one punchy sentence
- Add quick-links row and install table
- Replace verbose feature lists with concise grid
- Move Mac parity table, Quick Send troubleshooting,
  node capabilities reference, deep links table, and
  Command Center diagnostics to linked docs
- Keep essential dev workflow: setup, build, run, test
- Add Documentation links table
- Consistent structure: Install > Features > Projects > Dev > Config > Docs > License

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 049e0978-76d3-4105-82c7-898a2fd7f4fd

* docs: tighten README based on critical review

- Title: 'OpenClaw Windows Hub' (matches product name)
- Intro: one sentence what-it-is, one what-it-does (no feature-list sentence)
- Install: mention 'Set up locally' happy path
- Step 3: concrete CLI example instead of vague config reference
- 'Also included' -> 'Features' (proper header)
- Docs table: split contributor-facing (table) from user-facing (inline links)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 049e0978-76d3-4105-82c7-898a2fd7f4fd

* docs: restore build/run flags that contributors actually need

Bring back setup-dev.ps1 flags, build.ps1 -Project, dotnet build
with RID (WebView2 gotcha), run-app-local.ps1 -NoBuild/-Dev/-Isolated,
and MSIX build command. Organized into Prerequisites/Build/Run/Test
subsections instead of one dense block.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 049e0978-76d3-4105-82c7-898a2fd7f4fd

* docs: restore user-facing setup detail that people actually need

- Node Mode: expand to 4 steps with actual allowCommands JSON config
- Add privacy warning (no wildcards, screen.record/camera are opt-in)
- Add verify step with test commands from gateway side
- Add Deep Links table (key links, not all 30+)
- Add File Paths table (settings, logs, gateways, exec-approvals)
- Link MCP Mode doc from features list

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 049e0978-76d3-4105-82c7-898a2fd7f4fd

* docs: add OpenClaw bot icon to README title

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 049e0978-76d3-4105-82c7-898a2fd7f4fd

* docs: make Windows README app-first

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: bakudies@microsoft.com <bakudies@microsoft.com>
Copilot-Session: 049e0978-76d3-4105-82c7-898a2fd7f4fd
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* feat: simplify session status presentation

* test: preserve session presentation priority

* test: pin active session run field

* test: align chat threads with session run state

* test: seed session status UI fixture

* test: add background session to UI fixture

* fix: retain session run state

* fix: update session fixture for flattened fields

* fix: preserve localized resource nodes

* fix: align chat activity with run liveness
…claw#1092)

* feat(chat): compact consecutive tool activity

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(chat): compact expanded tool rows

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(chat): preserve tool input details

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(chat): space expanded tool details

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(chat): preserve selected tool detail text

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(chat): preserve ordered tool history

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test(chat): inspect Reactor tool output text

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test(chat): reconcile tool activity off-tree

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test(chat): expand compact native tool proof

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(chat): correlate string tool results

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(chat): preserve history tool correlation

Prefer semantic tool-result call references and consume aborted assistant state on the first replayed part, including tool-first messages.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
openclaw#1079)

Bumps [github/gh-aw-actions/setup-cli](https://github.com/github/gh-aw-actions) from 0.83.3 to 0.84.1.
- [Release notes](https://github.com/github/gh-aw-actions/releases)
- [Changelog](https://github.com/github/gh-aw-actions/blob/main/CHANGELOG.md)
- [Commits](github/gh-aw-actions@6f8e8ef...0292041)

---
updated-dependencies:
- dependency-name: github/gh-aw-actions/setup-cli
  dependency-version: 0.84.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/stale](https://github.com/actions/stale) from 10 to 11.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](actions/stale@v10...v11)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '11'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 27dc9d2f-ee94-48f1-bf06-8f3d10414fc9
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 27dc9d2f-ee94-48f1-bf06-8f3d10414fc9
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 27dc9d2f-ee94-48f1-bf06-8f3d10414fc9
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 27dc9d2f-ee94-48f1-bf06-8f3d10414fc9
Preserve native tool identity and safe output convergence while correctly handling successful aborts, failed-abort recovery, and prior-turn legacy replay correlations.
)

* fix: prove relayless managed gateway ownership

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix: close relayless provenance races

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test(e2e): lease mirrored-WSL-safe gateway ports

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 184fa070-4567-45dc-ac2d-c81c2840af14

* test(e2e): prove selected gateway port

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 184fa070-4567-45dc-ac2d-c81c2840af14

* test(e2e): bound Windows port inspection

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 184fa070-4567-45dc-ac2d-c81c2840af14

* test(e2e): harden WSL port proof

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 184fa070-4567-45dc-ac2d-c81c2840af14

* test(e2e): parse localized port exclusions

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 184fa070-4567-45dc-ac2d-c81c2840af14

* test(e2e): tolerate unrelated port exclusions

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Fix gateway reload mode default

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(setup): recover gateway 2026.7.1 restart

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 15b5584f-677b-453f-8aa4-4f33fe60bc50

* test(shared): cover gateway 1012 close frame

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 15b5584f-677b-453f-8aa4-4f33fe60bc50

* test(shared): target managed 1012 loopback

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 15b5584f-677b-453f-8aa4-4f33fe60bc50

* test(shared): verify restart clears handshake

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 15b5584f-677b-453f-8aa4-4f33fe60bc50

* test(shared): place restart handshake setup

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 15b5584f-677b-453f-8aa4-4f33fe60bc50

* fix(setup): harden gateway restart recovery

Classify the Gateway 2026.7.1 terminal restart from stable model-check context, and bound managed-local NoListener retries without weakening endpoint ownership checks.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(setup): bound gateway startup races

Keep retryable startup close 1013 and typed listener snapshot changes inside the existing managed-local restart window while preserving immediate failure for every other unknown owner.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(connection): retry bootstrap cleanup after save failure

Keep gateway registry memory aligned with persisted state when bootstrap credential cleanup cannot be saved, so later connection lifecycle events can retry safely.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 73908040-d4b7-493b-b024-b0c9ebcfc98d

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 184fa070-4567-45dc-ac2d-c81c2840af14
Copilot-Session: 15b5584f-677b-453f-8aa4-4f33fe60bc50
Copilot-Session: 73908040-d4b7-493b-b024-b0c9ebcfc98d
* fix: align exec approval snapshot schema

Omit the redundant baseHash response field so file-backed V2 snapshots match the gateway protocol. Keep hash as the compare-and-swap value supplied to system.execApprovals.set.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix: bind reusable Windows exec commands

Bind simple canonical CMD carriers to one resolved executable identity for allowlist matching, persistence, metadata, and direct execution. Keep shell syntax and ambiguous command forms on the one-time approval path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix: harden reusable exec host checks

Keep argument-selected Windows code hosts on the one-time approval path, document the maintained host catalog, and enforce the reusable-command prerequisite for durable fallback execution.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix: correct carrier recognition and tighten reusable exec binding

Review follow-ups for the reusable exec command binder.

Carrier shape (blocker): the binder only accepted a cmd carrier with
exactly 5 argv elements, but the gateway sends the command tail already
tokenized (upstream fixture: cmd.exe /d /s /c echo "SAFE&&whoami"), so
real invocations never bound and allowlists stayed broken. Carrier
recognition now lives in a single owner, CanonicalCmdCarrier, which
accepts multi-element tails and identifies cmd by basename so the
absolute-path form works. MxcConfigBuilder now shares that owner instead
of keeping its own divergent copy, which also removes a latent
NotSupportedException on carriers with more than 5 elements.

Bindable executables (high): PATHEXT resolution can return .vbs, .js,
.msc, .com and similar, so a denylist of .bat/.cmd was insufficient for
durable rules. Replaced with an .exe-only allowlist, applied in both the
binder and the coordinator.

Prompt resolved path (high): ExecApprovalEvaluation.Resolution is derived
from the allowlist resolutions, which are now empty for unbindable
commands, so the approval dialog lost the resolved path on exactly the
riskiest path. Restored a fallback to the carrier resolution.

Indirect command hosts: corrected the inaccurate claim that this catalog
matches the macOS binding model (macOS binds an argPattern instead) and
expanded it with 20 argument-selected code hosts.

Also documents why baseHash must not be reintroduced into the exec
approvals snapshot, records the new ownership in the architecture ledger
with guard tests, and documents the interpreter-rule behavior change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix: only look through a trusted system cmd carrier

Rubber-duck review caught an approval/execution identity mismatch that the
previous commit introduced.

Unifying carrier recognition on a basename match let any image named
cmd.exe be looked through, including a copy in a writable directory. For
"C:\Writable\cmd.exe /d /s /c hostname.exe" the binder would bind against
the inner hostname.exe, so the approval dialog showed that trusted path,
but an allow-once still launches the outer image, which is free to ignore
its arguments and run something else entirely.

The two owners need different predicates. Serialization must stay
permissive: cmd parses its own raw command line rather than going through
CommandLineToArgvW, so anything cmd receives has to use the cmd-aware
serializer regardless of where the image lives, and being permissive there
only ever yields correct quoting. Trust must be strict, because looking
through a carrier is what makes the shown identity differ from the
executed image.

IsCmdExecutable stays as the serialization predicate and keeps its
MxcConfigBuilder caller. IsTrustedCarrierExecutable is new and accepts
only the bare gateway name or a fully-qualified path in the Windows system
directories; the binder now uses TryGetTrustedCanonicalPayload. An
unrecognized carrier falls through to the existing indirect-host
rejection, so it stays unbindable instead of binding to the wrong
identity.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix: verify resolved cmd carrier identity

Require durable binding to resolve the outer carrier to system cmd.exe, restore native .com executables, preserve .com command-host classification, and clarify the live gateway carrier shape.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(exec-approvals): bind durable approvals to arguments, not executable names

Durable approval identity is now executable path plus an argument pattern,
matching the gateway protocol and the macOS node. Approving one invocation of a
program no longer authorizes every other invocation of it.

Upstream ground truth is openclaw/openclaw at db90dff1396fecbf7029e9e9ea19d6c6ca3e644e:
packages/gateway-protocol/src/schema/exec-approvals.ts, src/infra/exec-approvals-allowlist.ts,
src/infra/exec-command-resolution.ts, src/infra/system-run-command.ts.

Argument binding (D1)

ExecArgPattern owns the wire format. A generated pattern is an anchored regex
over the NUL-separated argument subject, matching buildArgPatternFromArgv on
Windows. The macOS hashed "sha256:argv:" form is recognized so an entry created
there matches here. Patterns are remote-influenced input, so matching is time
bounded and a malformed pattern fails closed.

ExecAllowlistMatcher now mirrors matchAllowlist: an entry with an argument
pattern must match the arguments and wins over a path-only match; a generated
entry with no argument pattern never matches, because such an entry predates
argument binding and would authorize argv the operator never saw.

The name-keyed interpreter catalog in ExecCommandToken is deleted. A name list
is not a boundary: renaming an image defeats it and the list can never be
complete. Argument binding constrains a renamed interpreter exactly as tightly.

rawCommand consistency (D2)

ValidatedRunRequest carries rawCommand and the validator rejects a mismatch
against command, mirroring the gateway's RAW_COMMAND_MISMATCH. Both accepted
forms are honored: the formatted argv and the inline cmd payload. rawCommand is
never promoted to executable input.

Network executables (D3)

A durable rule names a path, not the bytes at that path. UNC and mapped network
paths are refused for durable binding because whoever controls the share
controls what a past approval authorizes later.

Carrier diagnostics (D4)

An unbindable carrier now reports why (non-canonical-cmd-carrier,
carrier-payload-not-static, untrusted-cmd-carrier-image, and so on) instead of
silently producing an allowlist that appears to be ignored.

Identity separated from transport (D5)

A strictly recognized canonical cmd carrier is authorized by the executable it
actually runs, but is executed exactly as received. Substituting the bound
direct argv would drop the carrier's in-band PATH and TEMP setup, which is the
only environment contract the sandbox currently accepts. An invariant asserts
the executed transport still reconstructs the approved request. Images merely
named cmd.exe are refused: looking through one would describe the trusted inner
program while running the untrusted outer image.

Also fixes a real widening bug: rewriting the approvals file dropped source and
argPattern, silently converting a narrow generated rule into a path-only one.

Behavior change worth calling out: a hand-written path-only rule such as
**/wsl.exe now authorizes that executable instead of hard-failing with
persistent-approval-not-permitted-for-command-host. That matches upstream and is
the operator's explicit choice. Generated rules still pin their arguments.

Validation: build.ps1, Shared 3502, Tray 2116, WinNode.Cli 126.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix(exec-approvals): execute trusted carriers via resolved System32 path

The D5 carrier-preservation path kept the request's original argv[0], which is
typically a bare `cmd.exe`. LocalCommandRunner.ValidateDirectExecutable requires a
fully-qualified executable, so every preserved carrier failed at launch, and a
relative argv[0] would also let Windows re-resolve the image against PATH/cwd at
spawn time - the exact hijack the resolved-path rule exists to prevent.

CanonicalCmdCarrier.ResolveTrustedCarrierPath now resolves a trusted carrier token
to the absolute System32/SysWOW64 cmd.exe, and the binder pins executionArgv[0] to
it. The anti-drift invariant in CarrierTransportMatchesRequest permits exactly that
one substitution (argv[0], compared OrdinalIgnoreCase); argv[1..] must still be
Ordinal-identical, so no metacharacter drift can be introduced. A carrier that
cannot be resolved to a trusted image is rejected with UntrustedCarrierImage.

E2E: adds a multi-element-tail allowlist proof matching the gateway's real wire
shape, plus a child-spawn diagnostic. Both approval proofs now assert the approval
decision unconditionally and tolerate only STATUS_DLL_INIT_FAILED for the exit
code, since some hosts cannot spawn any child executable under MXC. Any other
nonzero exit still fails.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* docs+test: document argPattern identity model and prove CAS stale-hash rejection

WINDOWS_NODE_ARCHITECTURE still described the removed indirect-command-host
catalog as the durable security boundary. Replaced with the implemented model:
durable identity is executable + argPattern, the platform-specific argPattern wire
form, upstream matchAllowlist authorization consequences, the trusted-carrier
identity/transport split and its two anti-drift invariants, and the deliberate
path-only policy loosening.

Adds an architecture ledger row for exec-carrier-transport-identity so a future
change cannot quietly substitute the bound direct argv for the carrier while MXC
still needs the in-band PATH/TEMP bootstrap.

Adds SystemExecApprovals_SetRejectsStaleBaseHash over the real MCP HTTP server:
the GET snapshot carries no baseHash, a stale baseHash on .set is rejected, and
the rejected write does not mutate the file.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix: close three authorization holes in exec approval binding

Three ways a durable approval could authorize one thing and permit another.

Current-directory hijack. The trusted cmd carrier is executed verbatim, so
cmd.exe re-resolves the payload executable at launch and it searches the
current directory before PATH. Our resolver searches PATH only. A file named
like the payload sitting in the working directory therefore meant the binder
authorized the PATH copy while cmd.exe launched the local one, and a replay
re-resolved to the same PATH copy so no prompt appeared. The binder now
refuses to bind when the current directory shadows a bare payload name, so
the two resolvers can never disagree. Explicitly relative payloads are
unaffected: both resolve them the same way.

Remote binding strip. A remote .set was allowed to retain any entry whose
pattern matched an existing one. Keeping the path while dropping argPattern
and source converted a narrow generated rule into a path-only grant for that
executable, which is privilege escalation without adding an entry. Retention
now compares the whole authorization identity.

NUL in arguments. NUL separates arguments inside a persisted argPattern, so
"a\0b" rendered identically to the two arguments "a","b" and a stored rule
could match a differently segmented argv. NUL is not representable in a
Windows command line anyway, so arguments containing one are rejected before
anything is bound or written.

Also stamps allowlist usage against the matched entry id and argument binding
rather than the pattern alone, so a path-only rule no longer absorbs the
usage metadata of a bound sibling, and logs the breadth of each grant.

Validation: build.ps1; Shared 3508 passed; Tray 2116 passed;
WinNode.Cli 126 passed; MCP exec-approvals integration 4 passed against a
live tray subprocess.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix: harden the exec approval authorization checks against bypass

Follow-up review found three ways around the checks added in the previous
commit.

Identity forgery. The retained-entry identity joined pattern, argPattern and
source with NUL, but argPattern legitimately contains NUL. A caller could
shift the delimiter between fields to forge a match while leaving a broader,
unanchored regex behind: (argPattern "^--version\0$", source "allow-always")
had the same joined form as (argPattern "^--version", source "$\0allow-always").
The fields are now compared structurally, so no redistribution can collide.

Omitted working directory. The current-directory shadow check treated a null
cwd as "no current directory", but a child process inherits this one's and
cmd.exe searches it before PATH just the same. It now falls back to the
inherited directory instead of skipping the check.

Carrier payload NUL. The NUL rejection sat in the direct-binding path only.
The carrier tokenizes a payload string into a fresh argv, so a NUL carried
inside the payload was checked but the outer command was not. The check now
runs at entry, before any carrier parsing, and again after tokenization.

Validation: build.ps1 (WinNodeCli hit the known transient GitVersion mutex
error; rebuilt clean on retry); Shared 3510 passed; Tray 2116 passed;
WinNode.Cli 126 passed; MCP exec-approvals integration 5 passed against a
live tray subprocess rebuilt from this head.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* docs: record the carrier payload resolution rule and its residual risk

Documents the working-directory ambiguity refusal, the NUL argument rule, and
the structural retained-entry identity comparison. Also states plainly that the
ambiguity check is an approval-time snapshot, so a writable working directory
remains in scope for payload substitution until the transport question is
settled.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix: hash retained-entry identities to keep the remote set check bounded

The structural comparison introduced in the previous commit replaced a hash set
with a linear scan. The submitted allowlist is remote-controlled, so a caller
could pass the retention constraint with many duplicate entries and force a full
scan of the local allowlist for each one. Restores hashed lookup with a
structural tuple comparer, so collision safety does not cost bounded lookup.

Source-only commit. Not validated locally: coordinator holds the host.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix: quarantine legacy command-host rules and pin the trusted cmd carrier

D6, targeted quarantine. An allowlist entry with neither `source` nor
`argPattern` stays a valid path-only rule for an ordinary executable, but goes
inert when its resolved target is a wrapper, interpreter, or code host the
previous model refused to approve durably (python, cmd, powershell, pwsh, wsl
and the rest of the pre-existing catalog, including versioned interpreters).
Those commands prompt instead of matching. The entry is not deleted and is not
auto-migrated on first observation. Only an explicit Allow always writes a
source-marked, argument-bound sibling, which then matches normally. This keeps
the move from a name catalog to argument binding from silently loosening a case
that used to be denied outright. The catalog is a legacy-compatibility
predicate, not the security boundary; structural wrapper classification plus
argument binding remains the model.

D7, constrained Option A. The recognized canonical trusted cmd carrier is still
executed (its in-band PATH and TEMP bootstrap is required under MXC 0.7, which
rejects a non-empty process.env), but it is reconstructed with the payload's
executable token pinned to the binder-resolved absolute path, in addition to the
already-pinned argv[0]. This closes the working-directory substitution race:
without the pin, the binder authorizes using a PATH-only resolver while cmd
resolves the same token at launch searching the working directory first, so
anything able to write there between approval and launch picked the winner.

New `CmdPayloadTokenizer` is the single owner of payload parsing and exposes
token spans, so the rewrite is built from the parsed structure and never from a
string replacement. An argument that repeats the executable's text survives
untouched. Pinning is refused rather than approximated: the pinned path must be
readable back by cmd byte for byte, so whitespace, quotes, control characters,
`% ! ^ & | < > ( )`, the command-name delimiters `, ; =`, and a trailing
backslash all fail closed to prompt-only. Whitespace is refused rather than
quoted because under `/s` cmd strips the payload's first and last quote and uses
the remainder verbatim, which would reintroduce the ambiguity the pin exists to
remove. Reconstruction is validated by round trip: same argument count, the
executable equal to the pinned path, every other argument ordinal-identical,
tail arity preserved, and the whole carrier still recognized as canonical. The
same equivalence is re-checked at execution time. The approval-time
working-directory ambiguity check is demoted to a diagnostic and the residual
race is removed from the docs.

Reconciles remote contributor head b9be5f4:
- Adopted: `.com` is a directly-executable image, so it is bindable and is
  stripped by NormalizedBasename (so powershell.com classifies like
  powershell.exe).
- Adopted: the doc clarification that the live gateway sends one pre-joined tail
  element and a tokenized tail is a low-level/fixture shape.
- Superseded: `IsTrustedSystemCmdPath` detected a bare cmd.exe resolving outside
  System32 and refused to bind. Pinning is strictly stronger: argv[0] is set to
  the system image, so a rogue cmd.exe first on PATH is not merely detected, it
  is never what runs. The weaker predicate must not be resurrected. The test
  that protected the invariant is preserved in strengthened form.

NOT VALIDATED. No build, test, or tool run was performed for this commit: the
coordinator holds an exclusive local host lock. Source only, pending the
`1109 LOCAL WINDOW START` window.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* test: harden pinned-carrier coverage and reconcile .com classification

Adds focused CanonicalCmdCarrier coverage for the D7 reconstruction: the
request's own switch spelling is preserved, a relative carrier or payload is
refused, the pinned carrier matches the request it was built from, any other
difference (different program, different argument, injected metacharacter,
unqualified payload, changed switch) is refused at execution time, and the
PATHEXT extension a bare name gains when resolved is accepted.

Refuses pinning a path containing cmd's command-name delimiters , ; = which our
tokenizer deliberately does not model, so such a path cannot be split by cmd
into a different program plus an argument. Adds the three cases to the
representation-safety theory.

Avoids nullable/variance-sensitive assertion overloads and collection
expressions in generic Assert.Equal calls, since tests build with
TreatWarningsAsErrors.

NOT VALIDATED. Source only, local host hold still in force.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix: keep durable binding .exe-only and verify pinned payloads by reconstruction

Two coordinator-required corrections to the D6/D7 work. All other D6/D7
behavior is unchanged.

1. Revert the .com durable bindability expansion.

   b9be5f4 proposed treating .com as bindable alongside .exe, and that was
   adopted in 4f8252c. It is reverted here. Widening the durable authorization
   allowlist to another executable format is a separate decision with its own
   review, and it is out of scope for a change about carrier binding.
   IsBindableExecutable is .exe only again, and .com rejoins the
   non-bindable PATHEXT set.

   NormalizedBasename still strips .com, for one narrow reason that is now
   documented at the call site: IsLegacyQuarantinedHost has to recognize a
   provenance-less legacy entry naming powershell.com exactly as it recognizes
   powershell.exe, otherwise the D6 change in model would silently upgrade a
   case the old catalog refused outright. That is quarantine classification, not
   bindability. Both halves now have focused tests
   (NativeComExtensionTarget_DoesNotBind for the contract,
   ComExtension_QuarantinesAsTheSameCommandHostAsExe and
   LegacyPathOnlyEntryForACommandHostWithComExtension_IsInert for the
   transitional behavior).

2. Verify the pinned payload by reconstruction, not token comparison.

   PinnedCarrierMatchesRequest documented that all interior spacing must be
   byte-identical, but it only compared token values. A payload whose tokens
   matched while its spacing was rewritten passed. It now reconstructs the one
   payload the request is permitted to have become, by applying the executable
   token span edit to the request's own payload, and requires ordinal equality
   with the payload actually being executed. That single check subsumes argument
   values, argument count, and every byte of interior spacing.

   PinnedCarrier_RejectsInteriorSpacingChangesThatPreserveTokens covers a
   doubled space, a tab, a leading space, and a trailing space. Each asserts the
   tokens are still identical before asserting the match fails, so the rejection
   is attributable to spacing alone.

Docs and the architecture ledger are updated to say .exe only.

No local validation, rebase, push, or PR mutation was performed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix(exec-approvals): restore the full pre-binding catalog for D6 quarantine

D6 asks a purely historical question about an allowlist entry that is already
on disk and carries neither provenance nor an argument binding: would this
exact entry have been refused durable approval when it was written? The answer
has to come from the catalog as it actually stood immediately before argument
binding replaced it, not from a fresh judgement about which programs look
dangerous today.

The previous revision restored that catalog from cdcb74f, which predates the
indirect-host expansion, so it silently dropped 28 names: mshta, regsvr32,
rundll32, msbuild, csc, vbc, dnx, rcsi, installutil, regasm, regsvcs,
mavinject, msiexec, certutil, bitsadmin, wmic, forfiles, scriptrunner, pcalua,
cmstp, odbcconf, msdt, ieexec, presentationhost, winrs, hh, msxsl and xwizard.
Every one of those was refused durable approval in the shipped release, so a
provenance-less entry naming one could only have been written under a model
that denied it. Leaving them out would have converted a denial into an
unconditional allow on upgrade, which is exactly the regression D6 exists to
prevent.

The catalog and IsVersionedInterpreter are now a verbatim copy of e4ff61e,
the last revision before binding replaced them (69 names, byte-identical and
in the same order). The accompanying comment states that this list must not be
curated, pruned or extended, because anything added would claim to have been
denied in a past release when it was not.

This does not reopen D4. The catalog is not the security boundary and cannot
become one: IsLegacyQuarantinedHost has exactly one call site
(ExecAllowlistMatcher.MatchInternal), reached only when an entry has no
ArgPattern and no Source. Every rule this node generates carries an argument
binding, so new rules never take that path. WindowsCodeHost_BindsOnlyToThe
ApprovedPayload still binds mshta, regsvr32 and rundll32 durably through
argument binding, unchanged.

Tests:
- LegacyPathOnlyEntryForACommandHost_IsInert gains mshta, rundll32, regsvr32,
  msbuild, certutil and wmic.
- LegacyQuarantineCoversTheFullPreBindingCatalog enumerates all 69 names and
  reports any that were dropped by name, so this class of truncation fails
  loudly rather than silently widening an allow.
- LegacyQuarantineCoversVersionedInterpreters covers python3, python3.12,
  pythonw3.11 and pypy3.10.
- LegacyQuarantineDoesNotCoverLookalikeNames pins that pythonish,
  python-wrapper and pypycache stay ordinary executables.
- LegacyPathOnlyEntryForAnOrdinaryExecutable_StillMatches is the unchanged
  control for hostname.exe, git.exe and rg.exe.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix(exec-approvals): keep NormalizedBasename .exe-only and drop the .com quarantine claim

Exact archaeology: e4ff61e's NormalizedBasename stripped `.exe` and nothing
else. A `.com` spelling therefore never normalized to a catalog name, was never
classified as a command host, and was never refused durable approval. The
previous revision of this branch taught NormalizedBasename to strip `.com` as
well so that a provenance-less entry naming `powershell.com` would go inert.
That inverted the very rule D6 exists to enforce: it invents a denial that
never happened, which is the same class of error as dropping one that did.

It was also far wider than intended. NormalizedBasename is shared by IsEnv, the
shell-wrapper normalizer, and the PowerShell and builtin classifiers, so the
extra strip silently changed what all of them recognize, well outside the
transitional quarantine it was meant to serve. A separate `.com` stripper for
the quarantine alone would have been no better, because the historical
behavior it must reproduce had no such concept at all.

NormalizedBasename is now byte-identical to e4ff61e (verified mechanically),
and a legacy entry naming `python.com` keeps working exactly as it did before.

Unchanged and re-verified:
- IsBindableExecutable is still `.exe` only, so a real Windows `.com` image is
  still refused durable binding. Bindability and legacy classification are
  independent questions and stay that way.
- The pre-binding catalog is still the complete verbatim 69-name copy from
  e4ff61e, with the exact versioned-interpreter logic.
- PinnedCarrierMatchesRequest still verifies by reconstruction, so interior
  spacing drift is still rejected.

Tests:
- ComExtension_QuarantinesAsTheSameCommandHostAsExe becomes
  ComExtension_IsNotLegacyQuarantined, asserting the opposite for the same
  three tokens.
- LegacyPathOnlyEntryForACommandHostWithComExtension_IsInert becomes
  ..._StillMatches.
- NativeComExtensionTarget_DoesNotBind is untouched: it is the bindability
  contract and is unaffected by classification.

Docs: the WINDOWS_NODE_ARCHITECTURE quarantine section drops the `.com`
spelling claim, names the indirect execution hosts now in the catalog, and
states the `.exe`-only matching rule with its rationale.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* docs: drop the .com discussion from the legacy quarantine section

The quarantine section explained at length why a `.com` spelling is not
quarantined. The explanation was correct but it kept `.com` in front of the
reader in a section about which names were historically refused, which is
exactly the confusion to avoid. The rule is stated positively instead: the
quarantine compares a basename with only a `.exe` suffix stripped, exactly as
the original catalog did.

Versioned interpreters (`python3.12`) and the indirect execution hosts are
retained in the refused-outright list, since those were genuinely in the
historical catalog.

No behavior change. The `.com` reference remaining in this document is the
pre-existing non-executable extension list, which is about resolution rather
than quarantine.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* refactor: delete the superseded working-directory ambiguity helper

ExecCommandResolver.HasCurrentDirectoryCandidate had zero callers once a trusted
carrier's payload executable token became pinned to its resolved absolute path.
Keeping it compiled "as a diagnostic" was the wrong disposition for a routine
that used to be an authorization boundary: a dead security check is an
invitation to wire it back up, and its own doc comment had to argue against
that. Deleting it removes the invitation.

The check was never sound as a boundary. It could only observe the working
directory as it stood when approval was granted, and anything able to write
there before launch simply wins after the check has passed. Pinning closes the
window properly: cmd performs no second resolution, so there is nothing left to
shadow.

TryGetProcessCurrentDirectory went with it; it had no other caller.

A comment in its place records what was removed and why, so the next reader
does not rediscover the idea and reintroduce it.

Docs:
- ARCHITECTURE ledger row `exec-carrier-cwd-ambiguity-check` no longer carries
  the helper as still-allowed, and its closure note is discharged. The invariant
  now states that ExecCommandResolver exposes no such method, so the ledger
  asserts the absence rather than tolerating the presence.
- WINDOWS_NODE_ARCHITECTURE no longer says the check is retained as a
  diagnostic.

No new test: no existing test referenced the helper, so there is no contract to
preserve or update.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* test: make carrier binding tests independent of the developer's PATH

Fourteen carrier tests failed on a machine with coreutils installed, and the
binder was right every time.

These tests bind a bare `hostname.exe` payload, which resolves through PATH.
The assumption was that PATH yields `C:\Windows\System32\hostname.exe`. With
coreutils installed it yields `C:\Program Files\coreutils\bin\hostname.exe`
instead, and that path contains a space. Under `cmd /s` the first and last quote
of the payload are stripped and the rest is used verbatim, so a spaced path
cannot be pinned without changing what cmd executes. The binder refuses and
returns CarrierPayloadNotPinnable, which is the accepted D7 fail-closed
behavior. The tests were asserting on ambient machine state, not on the code.

Confirmed by probe against the real binder:

    Resolve(hostname.exe).ResolvedPath: [C:\Program Files\coreutils\bin\hostname.exe]
    TryBuildPinnedCarrier: False
    TryBind -> bound=<null> failure=CarrierPayloadNotPinnable

Two fixes, chosen by what each layer can express:

Binder-level tests pin PATH to the system directory through the existing `env`
argument, so a bare name still has to be resolved through PATH and the behavior
under test is unchanged. New ExecTestPath helper holds that, with the reasoning,
so the next person does not reintroduce a process-PATH dependency.
BareCmdResolvingOutsideSystemDirectory_ExecutesTheSystemImage keeps its rogue
directory first but now also has the system directory on PATH, since otherwise
the payload cannot resolve at all and the test fails before reaching its claim.

Coordinator-level tests cannot do that. ExecApprovalV2InputValidator denies a
non-empty env with custom-env-not-supported, so a request cannot carry a PATH
and resolution always uses the process PATH. Those three tests name the payload
absolutely instead. They are about the coordinator's authorize-and-execute
wiring, which an absolute payload exercises identically, and bare-name
resolution stays covered at the binder level where env can be injected.

This also means the spaced-path refusal now has real-world evidence rather than
only a synthetic test: an ordinary developer machine produced exactly the case
D7 fails closed on.

Validation on this commit:
  Shared    3618 total, 3586 passed, 0 failed, 32 skipped
  Tray      2165 total, 2165 passed, 0 failed
  WinNode   126 total, 126 passed, 0 failed

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix(exec-approvals): close two adversarial-review gaps in carrier pinning and currency

Adversarial review of the exec-approval binding work surfaced two real
defects that focused tests now cover.

1. Versioned executables could bind and then hard-fail at execution.
   PinnedCarrierMatchesRequest required the request name to have no
   extension before allowing the pinned name to carry one. But
   Path.HasExtension("python3.11") is true (it sees ".11"), so a
   versioned tool bound successfully at authorization time and then
   failed at execution time with InternalError(unresolved-executable-on-allow).
   The rule is now simply: the pinned name equals the request name, or
   the request name plus exactly one appended extension. Stem identity is
   still required, so a pinned name with a different stem is rejected.

2. The mid-approval policy guard could miss a tightened grant.
   ExecApprovalsCurrency fingerprinted allowlist entries by Pattern only,
   so adding an argPattern, narrowing an existing argPattern, or marking a
   path-only entry as generated was invisible to IsStillCurrent. The
   fingerprint is now the (Pattern, ArgPattern, Source) triple. Pattern
   keeps case-insensitive comparison for Windows paths while ArgPattern
   and Source compare ordinally, using a comparer rather than case folding
   so the ToUpperInvariant/ToLowerInvariant round-trip pitfall cannot
   collapse distinct identities.

Neither change relaxes an authorization decision: both make a previously
silent acceptance fail closed or bind correctly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix(exec-approvals): fail closed on any provenance marker; make MXC proof hermetic

Second adversarial reviewer findings.

Accepted: source gating in ExecAllowlistMatcher was fail-open.
A path-only entry was treated as generated (and therefore inert) only when
its source was exactly "allow-always" under an ordinal comparison. Any other
non-empty value - a different case, a padded string, a marker written by
another producer, or a corrupted one - fell through to the path-only branch.
That was fail-open twice over: the entry gained path-only reach, and because
its source was non-empty it also skipped the legacy quarantine check. The
rule is now simply that any non-empty source means a generator produced the
entry, so a generated entry that lost its argPattern is inert regardless of
spelling. Provenance is absent only when source is empty or whitespace, and
those entries keep hand-written path-only semantics behind the quarantine
check exactly as before. No entry that used to be denied becomes allowed.

Rejected: persisted command text as a secret-disclosure regression.
The reviewer proposed redacting CommandText/LastUsedCommand and filtering
them out of system.execApprovals.get. That does not mitigate the stated risk
and would break two contracts. ArgPattern already encodes the exact argv and
is load-bearing for authorization, so arguments are on disk either way;
redaction would only stop an operator from auditing what they approved. The
get payload is returned verbatim and its hash is the concurrency token for a
matching set, so filtering the bytes would break read-modify-write. Returning
the file from execApprovals.get also predates this branch. The invariant is
recorded on the contract instead.

Also makes the MXC exec-approval proof hermetic. It used hostname.exe, which
is commonly shadowed on a developer PATH by a copy in a directory the sandbox
does not grant, so the proof could fail for reasons that have nothing to do
with approvals. It now uses whoami.exe, which has no common shadow.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* test(mxc-e2e): recognize the second host child-spawn incapacity signature

The MXC exec-approval proofs already tolerated one shape of "this host cannot
spawn a child executable inside the AppContainer": exit code 0xC0000142,
STATUS_DLL_INIT_FAILED. The same incapacity also appears as CreateProcess being
refused outright, which cmd reports as exit code 1 with "Access is denied." on
stderr. On a host expressing the second shape the proofs failed as if approvals
were broken, which is the wrong report.

The evidence that this is a host property and not an approvals or binding
problem is the child-spawn diagnostic, which runs with grant=none. No allowlist
entry participates, nothing is bound, and the carrier executes exactly as the
gateway sent it, with a bare executable name and no pinned path. It still
fails, while the echo builtin in the same payload and the same container
succeeds. It also reproduces under security=full. So it is not caused by
argument binding, by pinning a payload to a resolved absolute path, or by any
V2 decision.

The tolerance is deliberately narrow: exit code 1 together with exactly that
stderr message. It does not weaken what these tests prove. Each caller
separately asserts the approval decision and the MXC request shape from the
tray log, and a real approvals regression surfaces as decision=deny or a node
error rather than this signature. Any other nonzero exit code still fails.

Documents the limitation and its validation consequences in the gateway node
exec FAQ.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* test(mxc-e2e): report an incomplete child-spawn probe as host capability, not failure

On a host that cannot spawn a child executable inside the MXC AppContainer, the
sandbox run does not always come back with a clean nonzero exit. Observed here:
wxc-exec starts the container, the node logs decision=allow, and the run then
never returns a result at all, so the gateway CLI call gives up and the probe
fails before it can read any payload. That surfaced as an approvals proof
failing at AssertCommandSucceeded, which blames exec approvals for a
sandbox-runtime limitation of the host.

The child-spawn diagnostic now reports an incomplete probe as what it is: host
capability could not be established. This test asserts no approvals behavior,
so nothing is being swallowed. The approval decision and the MXC request shape
are asserted by the two bound-rule proofs, which still fail normally, and every
other nonzero exit code is still a failure everywhere else.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix: pin the canonical cmd carrier for one-time allows too

Adversarial review found that the two launch-time pins were applied only when
execution went through the reusable transport, which was selected for durable
allows and match-dependent fallback allows but not for a one-time Allow Once.

An Allow Once therefore executed the request's own argv: argv[0] was the
request's PATH-resolved cmd.exe, and a bare payload name stayed bare for cmd to
resolve again at launch, searching the working directory before PATH. The prompt
meanwhile names the inner executable the binder resolved through a trusted
system cmd.exe, so the operator approved one image while a different one could
run. With a writable directory ahead of the system directory on the node's PATH,
approving a command once could execute attacker-controlled code.

Approval identity, durability, and execution transport are three separate
things. Route a recognized canonical carrier through its pinned transport
whenever one exists, independent of durability. Durability is still gated on
requiresReusableExecution, so a one-time allow persists nothing.

Regression test asserts the pinned payload token; it fails without this change
with the token still bare. The test discovers a bindable payload from a
candidate list because a coordinator request cannot carry a custom env and a
shadowed system name can resolve under a spaced path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

* fix: pin execution transport on every approved exec path

Follow-up to ce0a3cb from the same adversarial review, which found two more
paths that still executed an image other than the one the operator approved.

1. The pre-approved branch chose the reusable transport only for an allowlist
   hit, so a security=full/ask=off run reached process launch with the carrier
   unpinned: argv[0] resolved through PATH and a bare payload token stayed bare
   for cmd to resolve again at launch. Full policy already permits arbitrary
   execution, so this is not an authorization escalation, but it left the
   "every approved run is pinned" invariant untrue and the two allow branches
   inconsistent.

2. A direct command is resolved twice, once by the normalizer for the execution
   identity and once by the binder for the identity that is displayed and
   stored. Execution used the normalizer's, so the prompt could name one image
   while a separate lookup of the same name selected another.

Both collapse into one rule, so transport selection now lives in a single
helper used by both decision points: whenever the binder produced a reusable
command, that command's argv is what runs. This is safe for direct commands
because BindDirect already applies the same env-wrapper unwrapping and modifier
rejection as BuildApprovedExecution and adds strictly more gates, so only the
source of the resolved executable changes.

Durability is unchanged and still gated separately, so no allow persists
anything it did not persist before.

PreApprovedFullPolicy_CanonicalCarrier_ExecutesThePinnedCarrier fails without
this change with the payload token still bare.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
Copilot-Session: 1223ff2c-11b6-48d9-937b-a56408e99b93
…aw#1113)

* fix(chat): preserve image paste after rerender

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(chat): restore reliable composer context menu

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(exec): clarify executable allowlist rules

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(exec): align allowlist copy with runtime policy

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Trust only the optional Gateway-stamped top-level sessionKey for node invocation attribution while keeping omitted or malformed attribution backward-compatible and untrusted nested values ignored.

Preserve the correlated handshake authorization added by openclaw#1076 and cover both invocation transports plus approval planning.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…penclaw#1104)

* ci: add Windows node release candidate E2E

* ci: test Windows node release artifacts

* ci: exclude MXC from hosted release candidate E2E

* ci: bind Windows candidate E2E to release provenance

* ci: harden release candidate E2E gate

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* ci: fetch full history for release E2E

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* ci: restore WinUI runtime assets for release E2E

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* ci: restore release E2E runtime assets

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test: assert E2E restore ordering

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test: bind E2E restore to RID builds

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test: order E2E restore before its build

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* ci: bind Windows release asset tuple

* fix(setup): clean staged package on cancellation

* ci: restore exact release candidate validation

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* ci: bind candidate artifact source run

Download the exact caller artifact from its immutable workflow run instead of assuming the reusable workflow shares the producer run.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 84bc73e4-939a-4e25-ba88-26af9f4fc8b5

* fix: support gateway command config migration

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 84bc73e4-939a-4e25-ba88-26af9f4fc8b5

* fix(setup): replay normalized gateway restarts

Recognize the Gateway 2026.7.2 wizard restart error so setup uses the existing bounded, ownership-verified replay path instead of failing before reconnect.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 84bc73e4-939a-4e25-ba88-26af9f4fc8b5

* fix(setup): own gateway lifecycle during wizard

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 84bc73e4-939a-4e25-ba88-26af9f4fc8b5

---------

Co-authored-by: Scott Hanselman <scott@hanselman.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 84bc73e4-939a-4e25-ba88-26af9f4fc8b5
* fix(setup): retry wizard migration lease restoration

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(setup): use full restoration retry budget

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@bkudiess bkudiess closed this Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.