Skip to content

MXC: replace Node bridge with direct wxc-exec.exe spawn#487

Merged
shanselman merged 57 commits into
openclaw:masterfrom
bkudiess:bkudiess/mxc-drop-node-bridge
May 22, 2026
Merged

MXC: replace Node bridge with direct wxc-exec.exe spawn#487
shanselman merged 57 commits into
openclaw:masterfrom
bkudiess:bkudiess/mxc-drop-node-bridge

Conversation

@bkudiess
Copy link
Copy Markdown
Contributor

@bkudiess bkudiess commented May 21, 2026

Replaces the Node.js bridge for MXC AppContainer sandboxing with a direct C# wxc-exec.exe pipeline. node.exe and tools/mxc/run-command.cjs are no longer required at runtime.

This keeps the original #487 direction and adds maintainer follow-ups from local MXC debugging: cwd grants, shell startup compatibility, ReFS diagnostics, local test gating, and Windows 10 fallback behavior.

Fixes #494

What changed

  • New direct path: MxcConfigBuilder, DirectAppContainerExecutor, and MxcExecutor build and launch wxc-exec.exe configs from C#.
  • Build output: WinUI copies wxc-exec.exe from @microsoft/mxc-sdk into OpenClaw.Tray.WinUI\bin\...\tools\mxc\<arch>\ and validates it is present.
  • Least privilege cwd: ungranted cwd is auto-granted read-only only; explicit readwrite grants remain readwrite.
  • Temporary shell startup shim: read-only drive-root grants are added because cmd.exe currently stats the drive root during startup.
  • ReFS diagnostics: non-NTFS grant roots log warnings; MXC filesystem grants require NTFS-backed paths.
  • Windows 10 fallback: when MXC is unavailable, system.run falls back to host execution with warning UI instead of blocking all commands.
  • Tests/docs: local MXC integration tests skip on GitHub Actions and on ReFS-backed checkout/output/grant paths; docs describe local NTFS testing.

Validation

  • ./build.ps1
  • dotnet test ./tests/OpenClaw.Shared.Tests/OpenClaw.Shared.Tests.csproj --no-restore — 1898 passed / 29 skipped
  • dotnet test ./tests/OpenClaw.Tray.Tests/OpenClaw.Tray.Tests.csproj --no-restore — 1191 passed
  • Simulated GitHub Actions MXC integration skip with GITHUB_ACTIONS=true and OPENCLAW_RUN_INTEGRATION=1

github-actions Bot and others added 30 commits May 21, 2026 01:32
…ssion key

When the gateway handshake completes but does not advertise a
mainSessionKey (or the legacy mainKey alias), the composer was showing
"Connected" with a disabled-but-unlabelled send box. The user had no
indication that the gateway needed updating.

Changes:
- OpenClawChatDataProvider: emit "Incompatible gateway" as
  ConnectionStatus when HasHandshakeSnapshot=true but MainSessionKey is
  null/empty and the socket is Connected, instead of plain "Connected".
- OpenClawChatRoot: map the "Incompatible" prefix to the new
  "incompatible-gateway" connState token.
- OpenClawComposer: handle "incompatible-gateway" → disable inputs +
  show Chat_Composer_Placeholder_IncompatibleGateway placeholder.
- Resources.resw (all 5 locales): add
  Chat_Composer_Placeholder_IncompatibleGateway string.
- OpenClawChatDataProviderTests: add two focused tests for the
  incompatible-handshake path (snapshot ConnectionStatus and
  ComposeTarget.IsReady=false).

Closes openclaw#459

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

Visual cleanup pass on the chat surface (no functional change):

- OpenClawComposer: right-edge padding 8 -> 14 in both ThreeRow and
  InlinePill branches so the action icons (attach / mic / settings / send)
  no longer jam against the window edge.
- OpenClawComposer: dropdowns row ColumnGap 4 -> 6 for clearer separation
  between Channel / Model / Reasoning pickers.
- OpenClawChatTimeline: cap tool-burst cards (CardOf + TaskList listCard)
  at MaxWidth=720 with HAlign.Left so a single 'exec' row no longer
  stretches across the full viewport with the Done pill floating at the
  far right edge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Symmetrize user/assistant/tool burst outer margins (16px both sides)

- Use bubbleRadius for tool CardOf/listCard and conditional header buttons

- Make tool card background Transparent so outline distinguishes it from filled assistant bubble

- Drop Plain tool burst footer; assistant follow-up bubble already carries the time

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

- Drop the 36x36 spacer that mid-run assistant bubbles inherited; continuation bubbles now sit at the same left inset as tool burst cards above them, so the agent column reads as a single straight edge.

- Add 20px top padding above the first message in the scroll content so the conversation does not crowd the window edge.

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

- When no assistant avatar shown, drop the leftSlot's right margin so assistant bubbles share the same left edge (16px) as tool burst cards.

- Tool burst row header now uses bubblePadding instead of (12,8,12,8) and a 32px MinHeight, so tool rows match chat bubble heights.

- Composer outer padding 14->16 to align dropdowns/input flush with chat bubble left edge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Assistant footer leftInset now respects per-entry avatar visibility (not the global flag), so continuation entries' timestamps align with the bubble's left edge instead of being indented 44px.

- Tool burst button hover/press alphas 0x22/0x33 -> 0x10/0x1C for a subtler reveal that doesn't darken the card on every pointer pass.

- Tool card background back to a faint LayerOnAcrylicFillColorDefault tint so the card has gentle presence instead of looking like a pure outline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
User/assistant footer insets now include bubblePadding so timestamps
sit flush with the bubble's text content edge instead of the outer
bubble border.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the user avatar is hidden, the rightSlot column still added an
8px left margin even though the column body was empty, leaving an
8px gap on the right of the bubble. Gate the margin on showUserAvatar
so the bubble actually reaches the container's right edge when the
avatar is off — this makes rightInset (= bubblePadding.Right) place
the timestamp flush with the bubble's inner text right edge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The TaskHeader burst style reserved an avatar slot (36+8px) so the
list card lined up with the assistant bubble's text edge, but the
Plain and FooterReframe styles started flush at the gutter. When the
assistant entry above the burst showed an avatar, the tool cards
appeared 44px further left than the bubble.

Extracted the avatar-slot wrap into a helper and applied it to all
three burst styles so user/assistant/tool share the same left edge
regardless of burst style.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Default tool burst rendering switches from Plain (verbose per-row stack) to TaskList:
 - While any step is InProgress: auto-expanded, shows 'Working on X...'
 - When the burst completes: auto-collapses to a single one-line summary card
   ('Ran N steps' or last result snippet) with a chevron to expand
 - Click chevron to override; per-step rows still individually expandable for
   full args/output (3-tier disclosure)

Addresses Scott's feedback: 'I'd like to be able to have tool calls summarized,
or made smaller, or collapsible, so there would be some way to be clear that
work is happening, and if I want to see the verbose logs, I could.'

No data-flow changes — purely the default value of the existing ToolBurstStyle
enum. The dev exploration panel still exposes Plain/TaskHeader/CompactSummary/
FooterReframe/TaskList for tuning.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Tool card aligns under bubble (toolLeftMargin = gutter + avatarSlot + 16) with right edge matched (MaxWidth -= indent). Plain/FooterReframe/CompactSummary/TaskHeader unified.
- Footer priority: hide sender/model by default, surface input/output tokens + context % pills.
- Preset record defaults updated to match (new presets inherit token/ctx ON, sender/model OFF).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Reorder timeline display within each turn so ToolCall bursts render AFTER the assistant reply (or the thinking indicator if none yet). Gateway still emits tool_start before assistant_delta; only the visual order changes.
- Inline 'agent is thinking…' indicator right after the most recent User entry instead of pinning to bottom of timeline, so tool cards visually hang below it.
- Tool burst card HAlign Left→Stretch (Plain/FooterReframe/CompactSummary/TaskHeader) so the right edge fills to the bubble's max right boundary instead of shrinking to content.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assistant bubble previously used HAlign=Left with no MaxWidth, so its right edge tracked content width. Tool burst card used HAlign=Stretch with MaxWidth=704, filling further right than the bubble.

Give the assistant card MaxWidth=720 and HAlign=Stretch so it always pins to the same max right boundary as the tool card (60 + 720 = 76 + 704 = 780). Tool card now sits indented 16px inside the bubble's left edge with an identical right stroke, reading as a true child of the bubble above.

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

WinUI's HAlign=Stretch + finite MaxWidth centers the element inside its slot (rather than pinning it left), which detached the bubble from the avatar + timestamp column. Revert to HAlign=Left so the bubble grows from the avatar's edge; MaxWidth=720 still caps the right edge so long messages line up with the tool burst card's right stroke. Short messages keep the previous content-width behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-on-wide-screen)

Same WinUI quirk as the assistant bubble: HAlign=Stretch with finite MaxWidth centers the element inside an oversized slot rather than pinning it left. On wide screens the tool burst card drifted away from the bubble's left edge.

Revert all four tool burst styles (Plain, FooterReframe, CompactSummary, TaskHeader) to HAlign=Left. Both bubble and tool card now anchor on the left next to the avatar/timestamp column; right edges line up when both fill their MaxWidth (720 / 720-indent).

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

Tool card needs to fill its 704-wide slot so the right stroke aligns with the assistant bubble's right edge, but HAlign=Stretch alone centers the card on wide screens (WinUI's Stretch + finite MaxWidth quirk).

Wrap the card in an Auto/Star Grid: the Auto column sizes to the card's MaxWidth (704), keeping the card pinned to toolLeftMargin and filling the slot. The Star column absorbs the rest. Applied to Plain, FooterReframe, CompactSummary, TaskHeader.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assistant bubble is content-sized (HAlign=Left, MaxWidth=720), so we can't predict its rendered width at layout time. Result: the tool card's right edge rarely matched the bubble's right edge, especially with short replies.

Solution: thread a per-turn Border[1] slot from RenderAssistantEntry into RenderToolBurst. The assistant bubble's Border drops itself into slot[0] on materialize; the tool card subscribes to bubble.SizeChanged and sets its own Width = bubble.ActualWidth - toolIndent. The two cards' left indent and right edges now stay exactly parallel as the bubble grows, regardless of content length.

Reset slot at each User entry boundary so tool cards never bind to a prior turn's bubble. Falls back to MaxWidth/AnchorLeft when no bubble exists (tool-only turn).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Scott feedback: when an agent's reply bubble lands and the tool burst beneath it is fully terminal, fold the N step rows into a single collapsed summary (clickable chevron to expand). While tools are still running, keep showing them as per-step rows so each Running/Done pill stays visible.

Added ToolBurstStyle.Auto and made it the process-wide default. Auto resolves per burst at render time:
  - count == 1                        -> Plain (one inline row)
  - count >= 2 && all terminal        -> CompactSummary (1-line + chevron)
  - count >= 2 && any InProgress      -> Plain (live status visible)

CompactSummary's existing expand machinery (expandedToolChips HashSet) is reused, so the collapse/expand state persists for the session.

Exposed Auto in the exploration panel dropdown for testing the other styles.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two follow-ups from Scott's screenshot:

1. The collapsed CompactSummary header used FlexRow(ColumnGap=6)+padding(12,8,12,8)+MinHeight=22 while BuildRow used Grid[Auto,Auto,Auto,Star,Auto]+margin(6,0,0,0)+bubblePadding+MinHeight=32. Rebuilt the summary header with the exact same Grid template, margins, padding, and MinHeight as the step rows so the chevron / lightning / Task label / Done pill axes line up vertically when the burst is expanded.

2. Removed the trailing FooterCaption(timeStr/TaskFooter()) from CompactSummary, TaskHeader, and FooterReframe returns. The assistant bubble above the tool burst already shows its own timestamp/model/tokens footer, so the time under the tool card was redundant noise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When an assistant message is followed by a tool burst in the same turn,
defer the timestamp/model/tokens footer so it renders BELOW the tool
card(s) instead of between the bubble and the tools. Order becomes:

  bubble -> tool card(s) -> timestamp/model/tokens

Implementation mirrors the existing bubbleSlot pattern: RenderAssistantEntry
accepts an Element[1] footerSlot; when supplied, the built footer is
handed back to the caller and the inline footer slot in the VStack
collapses to Empty(). The outer loop precomputes turn boundaries, hands
out a footerSlot whenever a tool entry follows in the same turn, and
splices the captured footer Element into timelineRows just after the
last entry of the turn (alongside the thinking indicator splice).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the assistant has produced a reply AND the tool burst would render
as a single visible row (one chip OR a collapsed multi-step summary),
embed the tool card inside the assistant bubble's content area instead
of rendering it as a sibling card below.

  bubble {
    text...
    [tool card]   <- nested
  }

In-flight multi-step bursts (Plain expanded) and bursts arriving before
the assistant reply still render externally so live progress stays
visible. Plain / TaskHeader / TaskList / FooterReframe styles never
nest — only Auto and CompactSummary opt in.

Implementation:
- RenderAssistantEntry gains an Element? nestedTool param. When set,
  the bubble wraps its markdown text in a VStack(8, text, nestedTool)
  so the tool card sits flush below the message with an 8px top gap,
  inside the bubble's existing padding/border/radius.
- RenderToolBurst gains a bool nested flag. In nested mode CardOf
  drops MaxWidth/HAlign.Left and the bubbleSlot Width binding (the
  parent bubble already constrains us); a new Wrap helper bypasses
  AnchorLeft and the toolLeftMargin/gutter outer margin so the card
  stretches inside the bubble.
- Outer loop precomputes turn boundaries, looks ahead from the last
  assistant entry of the turn for a contiguous tool burst, and asks
  BurstIsNestable to gate the decision (count==1 OR all-terminal under
  Auto/CompactSummary). Consumed orderedIdx positions are tracked in a
  HashSet so the external render branch emits Empty() for them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Markdown text has tight line-height with no trailing descender, so an
8px top gap reads as visibly tighter than the 8px bottom padding below
the nested tool card. Set the top gap to bubblePadding.Bottom + 4 so
the optical spacing matches the gap from the card to the bubble's
bottom edge across all PaddingDensity presets.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The +16 indent on external tool cards exists so the card visually nests
inside the assistant bubble's text column. When the agent is still
'thinking…' and no reply bubble has streamed, that indent makes the
tool card hang further right than the thinking indicator above it.

When bubbleSlot is null (no assistant entry seen in this turn) drop the
indent so the tool card aligns flush under the thinking indicator
instead of jutting right.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
High priority fixes:
- AutomationName: only set bubble-level name when no nested tool card
  is present so Narrator can traverse into the nested card.
- Status colors: replace hardcoded Done-green / Running-orange / pill
  white with SystemFillColorSuccessBrush / CautionBrush and
  TextOnAccentFillColorPrimaryBrush so they adapt to dark/HC themes.
- Hover/pressed: 3 button surfaces now use SubtleFillColorTertiary
  (hover) and SubtleFillColorSecondary (pressed) themed brushes. The
  tool card uses the lighter Tertiary on hover for a more subtle look
  (Scott feedback) and to stay visible in dark/HC.
- SizeChanged handler leak on the bubble-Width binding fixed: subscribe
  on Loaded, detach on Unloaded with a stable handler reference so
  re-renders don't accumulate listeners.

Medium priority fixes:
- toolCardBgBrush: LayerOnAcrylicFillColorDefaultBrush
  → CardBackgroundFillColorDefaultBrush (semantic match — the bubble
  surface is opaque, not acrylic).
- Tool card MinWidth = 360 so the 5-column header grid doesn't clip
  when the assistant reply is short and the bubble shrinks below it.
- HC border thickness bumped to 2px via AccessibilitySettings probe
  (graceful 1px fallback if API throws in unpackaged hosts).
- BurstIsNestable now returns false when any tool errored — errors
  stay external so the failure is visually prominent.

Low priority fixes:
- (int)bubblePadding.Bottom + 4 → (int)Math.Round(...) for fractional
  density values.
- Remove redundant FontSize = 12 sets after Caption() (Caption already
  sets 12). Status pills bumped 10 → 11 for readability.
- Add uniform-CornerRadius assumption comments at two card sites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The chat UI styling Kenny iterated on was being persisted to a local
preset (%APPDATA%\\OpenClawTray\\chat-exploration-presets.json with
IsDefault=true) that overrode the in-memory defaults on startup. New
installs and other users were therefore landing on the original code
defaults (Mica / Comfortable / Both avatars / 14px / 32px send button)
instead of the look reviewed by Scott.

Bake the preset values into the actual code defaults so a fresh install
matches the design without needing the JSON preset file:

  BackdropMode      Mica          -> Acrylic
  PaddingDensity    Comfortable   -> Cozy
  AvatarMode        Both          -> AgentOnly
  ComposerIconSize  14            -> 16
  SendButtonSize    32            -> 40

Updated in three places to keep them consistent:
- ChatExplorationState (the in-memory defaults applied at startup)
- ChatExplorationPreset record (defaults when a preset omits a field
  during deserialization)
- ChatVariationPresets.Calm (so users who hit Reset or pick the Calm
  variation land on the same baseline)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wrap BuildSection in a 2-col Grid with a transparent phantom chevron in col 0 (same glyph + 6px right margin) so the section's lightning glyph and the code block beneath it land at the same x as the header lightning above, regardless of density/font metrics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Composer (OpenClawComposer.cs):
- Paste image preview rendered above text in single user bubble
- X-button hover changes circular background opacity instead of
  fading the X glyph (uses opaque SolidBackgroundFillColor* brushes)
- Strip stray top/bottom line on TextBox via theme resource overrides
  (TextControlBorderThemeThickness / focused / pointerOver)
- Tighten composer<->actions-row gap (actionsRow margin -8/-4)

Reducer (ChatTimelineReducer.cs):
- UpsertAssistant reconcile path now scans backward for the most
  recent Assistant entry, stopping at User boundary, so streams of
  shape `text -> tool -> tool output -> final text` no longer
  duplicate the assistant text into a second bubble.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- AnchorLeft Grid: single Star col so card measure is bounded by chat viewport
- headerRow / summaryHeader: outer [Star, Auto] wrapping inner content + Done pill
- Summary Caption: TextWrapping=Wrap + MaxLines=1 + CharacterEllipsis for safe trim
- CardOf nested branch: MinWidth=360; external Sync clamps bubble width with Math.Max(360, w)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bubble CornerRadius (16) equals bubblePadding.Right (16) in Cozy preset,
so the bubble's corner arc reaches the inner content edge and clips the
Done pill at the card's right side. Move the nested card in by a full
bubbleRadius (and drop MinWidth=360 which forced the card wider than
the bubble in narrow viewports) so the pill is comfortably inside the
bubble's rounded shape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bkudiess bkudiess force-pushed the bkudiess/mxc-drop-node-bridge branch from bf876b1 to 50ecb64 Compare May 21, 2026 06:37
bkudiess and others added 3 commits May 21, 2026 09:54
Removes the Node.js + @microsoft/mxc-sdk + tools/mxc/run-command.cjs path for
MXC AppContainer sandboxing and replaces it with a pure-C# pipeline that calls
wxc-exec.exe directly. node.exe is no longer required at runtime.

Key changes:
- MxcConfigBuilder: pure function building wxc-exec ContainerConfig from
  SandboxExecutionRequest + scratch dir. Owns env allow-list + scrub, PATH
  tool resolution, shell command-line construction, cwd auto-grant.
- DirectAppContainerExecutor: ISandboxExecutor that creates per-invocation
  scratch dir, builds the config, logs a redacted summary (full JSON behind
  OPENCLAW_MXC_LOG_FULL_CONFIG=1), handles timeout/cancel, and falls back to
  --config <file> when the base64 config exceeds the cmdline limit.
- MxcExecutor (in OrcaCore.Models/Services for cross-project compat):
  additive caps + RunWithConfigFileAsync. Uses ProcessStartInfo.ArgumentList
  to avoid manual quoting hazards. WaitForExit() after kill so async stdout
  /stderr handlers drain before we read.
- MxcAvailability: no more RunCommandScriptPath; probes tools/mxc/<arch>/
  wxc-exec.exe first, legacy node_modules fallback.
- MxcCommandRunner: applies PR openclaw#480 diagnostic logging
  (LogSandboxRequest/LogSandboxResult) so sandbox settings round-tripping
  through wxc-exec is verifiable. CWD is logged as <set>/<null>, not the
  literal path.
- csproj: replaces 4 SDK/bridge copy targets with CopyWxcExecToOutput,
  CopyWxcExecToPublish, ValidateWxcExecShipped, ValidateWxcExecPublished,
  and a ValidateMxcArchMapping target that errors on unmapped RIDs.
- Tests: MxcConfigBuilderTests with 4 SDK-captured golden JSONs
  (LockedDown/Balanced/Permissive/Custom) for byte-equivalence vs the
  SDK output, plus env-scrub case-insensitivity, ResolveToolDirsFromPath,
  cwd auto-grant, timeout defaulting. DirectAppContainerExecutorTests
  cover the fail-fast paths. Symmetric golden compare with an explicit
  allow-list for the fields the SDK leaves empty.

Security hardening surfaced by adversarial review:
- IsDriveRoot guard in ResolveToolDirsFromPath prevents a misconfigured PATH
  entry from granting the whole system drive as readonly.
- CONNECTION_STRING and CONNSTR markers added to credential env-scrub.
- Env dict uses OrdinalIgnoreCase so an agent cannot inject a case-variant
  duplicate of a host-allowlisted var (APPDATA vs appdata).
- Reject embedded quotes in --config path; ArgumentList everywhere else.
- Host-side CancelAfter mirrors the builder's effective timeout (no
  unbounded wait when request.TimeoutMs is 0).
- ct.ThrowIfCancellationRequested() before the timeout branch so caller
  cancellation isn't mislabeled as TimedOut.
- UTF-8 byte count for the base64 cmdline-overflow threshold.

Removed:
- src/OpenClaw.Shared/Mxc/OneShotAppContainerExecutor.cs
- tools/mxc/run-command.cjs

Subsumes openclaw#480.

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

The cross-project byte-compat rationale that motivated the OrcaCore.Models /
OrcaCore.Services namespaces was a false premise carried over from the seed
files; no OrcaCore project consumes these types. Cleanups:

- Move MxcConfig.cs + MxcExecutor.cs out of Mxc/Direct/ up one level.
- Rename namespaces to OpenClaw.Shared.Mxc (kills 4 misleading using
  directives across the codebase).
- Drop MxcExecutor.ResolveExePath static (dead: path always comes from
  MxcAvailability) and tighten the ctor to a required string.
- Drop MxcExecutor.TryCreate (dead: callers ctor + catch directly).
- Drop MxcConfig.Containment default ('appcontainer') and the now-dead
  test asserting it. SDK output omits the field; the builder already does.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When MXC sandboxing isn't available on this host (e.g. Windows 10, build
< 26100, missing wxc-exec.exe), the agent must still be able to execute
commands. Previously the runner denied every system.run with a -1 exit
code, stranding users on older Windows.

New policy:
- Top-level !_isSandboxAvailable() route now falls back to the host
  runner with a clear [mxc] system.run UNCONTAINED warning instead of
  returning a deny.
- SandboxUnavailableException at runtime (executor reports unavailable
  on first call) also falls back to host AND invalidates the cached
  availability so the next call re-probes.
- SandboxPage messaging updated: 'Commands run uncontained on this
  machine' instead of 'commands blocked'. The page still disables its
  preset controls so users can't tweak settings that don't apply.

Tests updated to match: 5 previously-asserting-deny cases now assert
host fallback with the expected stdout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
christineyan4 and others added 22 commits May 21, 2026 10:45
…erf optimizations

* feat: tool metadata cache, tool call toggle, and stale-closure fix

- Cache tool metadata (name, label) to local JSON so tool call types
  persist across app restarts instead of falling back to 'exec'
- Improve ClassifyFlattenedToolOutput heuristic to detect bash, view,
  grep, git, edit, glob from output text patterns
- Add tool call show/hide toggle button to composer toolbar with
  tooltips on all toolbar buttons
- Add CompactSummary as default tool burst style with expand/collapse
- Fix stale-closure bug in ChatExplorationState.Changed handlers
  across Timeline, Composer, ChatRoot, and ExplorationsPanel — event
  handler captured initial UseState value (0), so Set(1) only triggered
  one re-render; subsequent toggles were no-ops
- Optimize SyncChildren with fast path for empty panels (skip
  RemoveFromParent O(n) scan) and pre-clear panel on visibility toggle
- Enable NavigationCacheMode on ChatPage to avoid full reload on
  navigation

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

* fix: debounce cache writes, atomic file save, closure fixes, and new tests

- Debounce SaveToolMetaCache with 500ms timer instead of Task.Run per event
- Atomic file write (write .tmp then File.Move) to prevent partial JSON
- ChatRoot: read explorationRevRef.Current inside dispatcher lambda
- Revert in-place HashSet mutation to proper Set(new HashSet)
- Add ToolMetaCacheTests (10 tests): sequential matching, exhaustion, guards
- Add classifier edge case tests (9 tests): view, git, exec defaults

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

* fix(tray): serialize tool metadata cache writes

Move tool metadata timer replacement under the provider lock, version debounced saves so stale timer callbacks cannot overwrite newer snapshots, and serialize unique-temp-file writes to avoid cache corruption under rapid tool events.

Add coverage that concurrent metadata additions flush complete valid JSON without leaving temporary files behind.

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

---------

Co-authored-by: Christine Yan <christineyan@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Scott Hanselman <scott@hanselman.com>
…Updates

Introduces OpenClaw.Shared.AppVersionInfo as the single source of truth for the app version (resolved at runtime from the tray assembly), and replaces ~9 duplicated version lookups across the UI.

Rewrites the update-check flow as a two-stage pipeline (metadata gate + install flag) with bounded waits, fresh UpdateInfo writes, and catches for COM/Cancel/InvalidOperation exceptions. Adds a ContentDialog so 'Check for Updates' gives visible feedback when the app is current, when a check fails, or (DEBUG) when skipped.

Adds 7 localized Update_* keys across en-us/fr-fr/nl-nl/zh-cn/zh-tw, with a LatinScriptInvariantResourceKeys allow-list so fr/nl keep 'OK' verbatim.

Bumps version to 0.4.7.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The BaseEnvAllowList (27 host env vars), DefaultToolNames (12-tool
whitelist), IsRequestEnvNameBlocked, and HasCredentialMarker were
ported from OpenClaw's own tools/mxc/run-command.cjs — not from
@microsoft/mxc-sdk. Keeping them treated that file as a source of
truth, which it isn't.

Behavior change: the C# builder now mirrors the SDK's
getAvailableToolsPolicy() shape (every existing PATH dir is granted
readonly, drive roots still skipped per the SDK bug workaround) and
no longer copies any host env vars. Agent-supplied request.Env is
passed through verbatim, plus TEMP/TMP/TMPDIR forced to scratch.

What stays:
- cwd auto-grant (AppContainer doesn't auto-grant cwd)
- Defensive FilterOutDenied re-pass (deny precedence)
- Drive-root guard (documented SDK bug workaround)
- TEMP/TMP/TMPDIR -> scratch (sandbox isolation correctness)
- Capability mapping (AllowOutbound -> internetClient)
- Shell quoting (cmd /S /C, powershell -EncodedCommand)

Tests: env-scrub theory and tool-whitelist tests removed (those
features no longer exist); new ResolvePathDirsForReadonly tests cover
the PATH walking behavior.

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

Two follow-ups to the previous trim:

(1) Restore env-scrub at the wxc-exec boundary, this time using the
    canonical openclaw policy from
    openclaw/openclaw:src/infra/host-env-security-policy.json (90
    blockedEverywhereKeys + 144 blockedOverrideOnlyKeys + 7 blocked
    prefixes). The JSON is copied into this repo at
    src/OpenClaw.Shared/Mxc/HostEnvSecurityPolicy.json and embedded
    as an assembly resource, mirroring the macOS Swift consumer
    (HostEnvSecurityPolicy.generated.swift). New HostEnvSecurityPolicy
    class exposes IsBlocked(name); MxcConfigBuilder.BuildEnv now uses
    it to filter agent-supplied env.

(2) Sandbox page now discloses the PATH→readonly implicit grant so
    users understand which dev tools (git, node, python, dotnet, ...)
    are reachable inside the sandbox without explicit configuration.
    Small secondary-text note placed inside the Files section, between
    the access dropdowns and the Custom Folders box.

Tests:
- HostEnvSecurityPolicyTests covers blocked-key matching, prefix
  matching (case-insensitive), malformed-name rejection, benign
  pass-through, and a sanity check on policy size.
- MxcConfigBuilderTests gains Build_BlocksDangerousAgentEnv asserting
  NODE_OPTIONS/GITHUB_TOKEN/LD_PRELOAD/DYLD_INSERT_LIBRARIES/GIT_SSH_COMMAND
  are dropped from the resulting wxc-exec env.

When the upstream JSON changes, re-copy HostEnvSecurityPolicy.json
and rerun tests.

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

Adds explicit provenance comments so readers don't have to trace git
history to understand why these pieces exist:

- HostEnvSecurityPolicy.cs: expanded XML doc spells out the source
  (openclaw/openclaw:src/infra/host-env-security-policy.json), the
  macOS analog (HostEnvSanitizer.swift + HostEnvSecurityPolicy.generated.swift),
  why we enforce at the wxc-exec boundary (defense-in-depth, not gateway
  centralization), and the threat-model rationale for merging the
  'everywhere' and 'override-only' buckets in our agent-env context.
- MxcConfigBuilder.BuildEnv: comment cites HostEnvSanitizer.sanitize as
  the direct macOS counterpart and explains the scrubbing rationale.
- MxcConfigBuilder.ResolvePathDirsForReadonly: comment cites the SDK
  function we mirror (getAvailableToolsPolicy in
  @microsoft/mxc-sdk:dist/policy.js) plus the drive-root SDK bug
  workaround.
- HostEnvSecurityPolicy.md: new sibling README next to the JSON that
  describes the update workflow, schema, and how/why we differ from
  the macOS code-generation approach.

No behavior change; comments + docs only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s from App.xaml.cs (openclaw#493)

* refactor: remove dead OnRecordingStateChanged handler and its localization strings

NodeService.RecordingStateChanged was never subscribed in App.xaml.cs,
so the handler and its six Activity_Recording* resource keys were unreachable
at runtime. Removing them and the matching entries from all five locale
resw files so localization tests remain green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: extract AppRunMarker from App.xaml.cs

The three static run-marker methods (CheckPreviousRun, MarkRunStarted,
MarkRunEnded) depended only on a file path. Moving them to a dedicated
class removes 37 lines from App and gives the behavior a named home.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: extract CliUninstallHandler from App.xaml.cs

The --uninstall CLI path (RunCliUninstallAsync, CliRedact, AttachConsole
P/Invoke) had no dependency on App instance state. Moving it to a
dedicated static class removes ~150 lines from App and gives the
headless uninstall entry point a clear, named home.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: AlexAlves87 <alexalves87@github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…#471)

* feat: add ExecApprovalsCoordinator and ICanPresentEvaluator

Wires the full two-pass approval pipeline: validate → normalize →
buildContext → evaluate(pass1) → prompt/fallback → evaluate(pass2).
ICanPresentEvaluator keeps the coordinator UI-free and testable without
Win32 APIs. SemaphoreSlim serializes prompt and second pass for
concurrent requests. Allowlist persistence and use recording are stubs.
Coordinator not wired in production; enforced by test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: wrap HandleAsync in outer catch to guarantee typed deny on unexpected exceptions

Without an outer catch, exceptions from ResolveReadOnly, CanPresent,
FallbackDecision, or an out-of-range prompt outcome escaped HandleAsync
untyped, breaking the fail-closed contract. Any unhandled exception now
returns InternalError("unexpected-exception") with an Error-level log
instead of propagating to the caller. Regression test added.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: AlexAlves87 <alexalves87@github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ilities (openclaw#495)

Add 8 tests covering the null-guard, case-insensitive lookup, and
return-value contracts of the untested GetLocalNodeCapabilities helper.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Emit redacted human and JSONL setup traces for install, pairing, gateway, repair, and remove flows.

Co-authored-by: Mike Harsh <mharsh@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After Step 5a removes the distro-specific VHD directory
(e.g. %LOCALAPPDATA%\OpenClawTray\wsl\OpenClawGateway\), the
parent wsl\ directory was left as an empty orphan, which also
prevented the %LOCALAPPDATA%\OpenClawTray\ directory from being
removed by the MSIX uninstaller.

Changes:
- Add Step 5b in LocalGatewayUninstall.cs: after VHD parent dir
  cleanup, delete the wsl\ parent directory if it is empty.
  Non-empty wsl\ dirs (e.g. other distros still present) are
  preserved safely.
- Add WslParentDirAbsent postcondition; include it in
  AllRequiredPostconditionsMet and AppendPostconditionErrors.
- Update validate-wsl-gateway-uninstall.ps1: add wsl_parent_dir
  path constant, include wsl_parent_dir_absent in postconditions
  and required-checks list, and capture it in the pre-state
  snapshot.
- Add 3 unit tests (WslParentDirCleanup_*) covering empty→deleted,
  non-empty→skipped, and already-absent→skipped scenarios.

Closes openclaw#467

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.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>
DeepLinkParser had no test coverage despite being pure testable logic
(no I/O, no Windows dependencies). This adds 27 tests covering:

- ParseDeepLink: null/whitespace/wrong-scheme → null
- ParseDeepLink: path extraction (with/without trailing slash)
- ParseDeepLink: Windows-canonicalized form (slash before query)
- ParseDeepLink: single and multi-parameter extraction
- ParseDeepLink: case-insensitive parameter lookup
- ParseDeepLink: URL-encoded value decoding
- ParseDeepLink: empty query → empty Parameters dict
- GetQueryParam: null/empty query or empty key → null
- GetQueryParam: value retrieval, case-insensitivity, URL decoding

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Issue openclaw#351 was closed but the four undescribed tools were never added to
CommandDescriptions in McpToolBridge.cs or skill.md. MCP clients and the
local winnode.exe CLI now see the correct parameter shapes and return
types for all four tools.

Changes:
- McpToolBridge.cs: add location.*, device.*, browser.* CommandDescriptions
- skill.md: add Location, Device, and Browser control sections with full
  parameter docs and privacy notes

The existing SkillMdDriftTests suite verifies that every command in
CommandDescriptions has a matching H3 heading in skill.md — all 115
tests pass with this change.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tizer)

The second env scrub I added at the wxc-exec boundary was redundant.
SystemCapability.HandleRunAsync already runs ExecEnvSanitizer.Sanitize
at the front door for every system.run and rejects the whole command
if any dangerous env var is present — by the time MxcConfigBuilder.BuildEnv
sees the env, it's already been validated. Adding a second filter was
dead code and conflated two separate questions (which list to use, and
where in the pipeline to scrub).

Reverted:
- MxcConfigBuilder.BuildEnv: drops HostEnvSecurityPolicy dependency,
  back to agent-env pass-through + structural validity (NUL/CR/LF/'=')
  + TEMP/TMP/TMPDIR scratch override.
- Removed src/OpenClaw.Shared/Mxc/HostEnvSecurityPolicy.{cs,json,md}
  (not used anywhere else).
- Removed tests/OpenClaw.Shared.Tests/Mxc/HostEnvSecurityPolicyTests.cs.
- Removed the redundant Build_BlocksDangerousAgentEnv assertion from
  MxcConfigBuilderTests.

Net architecture: one env scrub in the chain, in the existing place
(ExecEnvSanitizer at SystemCapability boundary). Whether to upgrade
ExecEnvSanitizer's hand-curated list to the canonical openclaw policy
is a separate question that doesn't need to land in this PR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The doc comments in MxcConfigBuilder, DirectAppContainerExecutor,
ISandboxExecutor, and ShellCommandLine still referenced things that no
longer exist after the recent revert: the env scrub at this layer (now
in ExecEnvSanitizer at the front door), the JS bridge (deleted), the
old tool-name whitelist (replaced with 'walk all PATH dirs'), and the
OrcaCore.Services namespace (collapsed into OpenClaw.Shared.Mxc).

Rewrites:
- MxcConfigBuilder class summary: lists what the builder actually does
  today (translate SandboxPolicy + request -> MxcConfig, with the four
  necessary additions: PATH dirs, scratch, cwd auto-grant, deny
  re-filter). Explicitly notes env scrub happens upstream.
- MxcConfigBuilder.Build inline comments: trim and reword to be clear
  about WHY each addition exists.
- ResolvePathDirsForReadonly doc: short version, no SDK-bug archeology.
- BuildEnv doc: short version, points at ExecEnvSanitizer for the actual
  scrub.
- DirectAppContainerExecutor: drop OrcaCore.Services and 'previously
  split with the JS bridge' references.
- ShellCommandLine: describe what it does instead of citing the deleted
  JS implementation.
- SandboxExecutionRequest.MaxOutputBytes: drop run-command.cjs mention.

No behavior change; doc comments only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ChatPage.xaml: add x:Uid to WaitingPanel title/status TextBlocks and
  RetryChatButton; reuse existing WebChatErrorTitle / WebChatOpenBrowserButton
  keys for the error panel controls
- InstancesPage.xaml: add x:Uid to BackToConnectionLink TextBlock
- Resources.resw (all 5 locales): add ChatPage_WaitingTitle.Text,
  ChatPage_WaitingStatusText.Text, and InstancesPage_BackToConnectionText.Text

Reduces hard-coded XAML string warnings from 286 to 280.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ack)

The runner's top-level !_isSandboxAvailable() guard already routes to
the host fallback for every call when MXC isn't available, so the
ISandboxExecutor passed in for that case is never invoked. The
UnavailableSandboxExecutor that always-throws-SandboxUnavailableException
was the placeholder injected before the openclaw#494 fix; it's now dead code.

- Deleted src/OpenClaw.Shared/Mxc/UnavailableSandboxExecutor.cs.
- NodeService.BuildSystemRunRunner: always constructs
  DirectAppContainerExecutor (one less branch, one less variable).
  Diagnostic log still reports MXC unavailability when applicable.
- MxcCommandRunnerTests.RunAsync_UnavailableExecutor_FallsBackToHost:
  switched from UnavailableSandboxExecutor to
  FakeSandboxExecutor { ThrowsUnavailable=true }; same coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Six small cleanups that together drop ~95 lines of dead code, dead
fields, dead comments, and one-use helper types. No behavior change
except the diagnostic log no longer carries a redundant
securityLevel preset label.

1. MxcCommandRunner: delete DetectPreset + MatchesPreset (~50 lines).
   They existed only to put a LockedDown|Balanced|Permissive|Custom
   label on the diagnostic log, hardcoding preset thresholds that had
   to track SandboxPage. The log already emits all the underlying
   settings; the label was redundant.

2. ShellCommandLine class in MxcConfigBuilder.cs: changed from public
   to internal -- only MxcConfigBuilder.Build calls it.

3. MxcAppContainer.Name field: deleted. Deprecated in the SDK since
   0.4.0-alpha and never set on our side.

4. MxcFilesystem.ExecutablePath field: deleted. Not in any SDK schema
   and never set on our side.

5. Stripped Additive (OpenClaw) comments throughout MxcConfig.cs.
   They tagged fields as "additive vs the original OrcaCore seed
   file"; since we deleted the OrcaCore claim the distinction is gone.

6. MxcArchHelper: inlined as a private static method of
   MxcAvailability.

Tests: dropped the securityLevel:"Custom" assertion in
RunAsync_LogsSandboxSettingsSnapshotAndPolicy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the Node-based MXC command bridge with direct wxc-exec.exe invocation and keep local Windows sandbox behavior reliable.

Adds least-privilege cwd read grants, the temporary readonly drive-root shell startup workaround, NTFS/ReFS diagnostics, local MXC test gating, and GitHub Actions self-skip for MXC integration tests.

Fixes openclaw#494

Co-authored-by: Barbara Kudiess <7658216+bkudiess@users.noreply.github.com>

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@shanselman shanselman marked this pull request as ready for review May 22, 2026 03:19
@shanselman shanselman merged commit cf611d4 into openclaw:master May 22, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows 10 (build 19044): Node Sandbox blocks all commands with no way to disable

7 participants