Skip to content

Commit f4e1cf3

Browse files
karkarlCopilot
andauthored
refactor(tray): move Permissions into shared-state MVVM ownership (#1075)
* refactor(tray): move Permissions into shared-state MVVM ownership Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(tray): refresh capabilities in MCP-only mode Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(tray): rebuild MCP-only capabilities on restart 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: Karen Lai <7976322+karkarl@users.noreply.github.com> Copilot-Session: d01a60a1-e002-4e6c-b3a2-3ced75fb5b66
1 parent 4206611 commit f4e1cf3

42 files changed

Lines changed: 6003 additions & 1154 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/ARCHITECTURE.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ multi-PR refactor plan for the reasoning behind each boundary.
3939
execution, WebSocket/MCP hosting. No UI types. No background work started from
4040
constructors.
4141
- **App** (`App.xaml.cs`): composition root and top-level lifecycle only.
42+
- **Shared mutable domains**: one observable service/store owns each persisted
43+
domain. View models consume snapshots and field-scoped or compare-and-swap
44+
mutations; they never own backing files, concrete managers, file observers, or
45+
parallel mutable caches.
4246

4347
## Single-source owners
4448

@@ -57,12 +61,15 @@ These are the canonical homes. Do not reintroduce private copies elsewhere.
5761
| UI-thread marshaling for presentation code | `IUiDispatcher` | authoritative |
5862
| Page view-model activation/deactivation + disposal lifetime | `NavigationScopeManager` | authoritative |
5963
| Presentation-layer DI composition root | `AppServiceRegistration` (root `ServiceProvider`, owned by `App`) | authoritative |
60-
| Settings snapshot read + batched save + non-echoing change notification | `ISettingsStore` | authoritative |
64+
| Settings snapshot read + field-scoped save + origin-aware change notification | `ISettingsStore` | authoritative |
65+
| V2 exec-approvals snapshot/CAS persistence + observation | `ExecApprovalsStore` through `IExecApprovalsPresentationStore` | authoritative |
6166
| Settings page load/persist view logic | `SettingsPageViewModel` | authoritative |
6267
| Native tool identity, display arguments, payload extraction, and flattened-history projection | `NativeToolProjector` | authoritative |
6368
| Managed-local listener provenance and strong-credential authorization | `ManagedLocalGatewayPortProvenanceService` | authoritative |
6469
| Exact Gateway wizard terminal-restart compatibility and bounded retry policy | `GatewayWizardRestartRecoveryPolicy` | authoritative |
6570
| Managed-local automatic repair eligibility and orchestration | `ManagedLocalGatewayAutoRepairMonitor` + `ManagedLocalGatewayRepairCoordinator` | authoritative |
71+
| Permissions page state, settings commands, and exec-approvals presentation | `PermissionsPageViewModel` | authoritative |
72+
| Permissions runtime status projection | `PermissionsPageRuntimeSource` | authoritative |
6673
| Capability UI metadata | `NodeCapabilityUiCatalog` (planned) | planned |
6774
| Capability registration/gating | `NodeCapabilityRegistrationPolicy` (planned) | planned |
6875
| Local MCP exposure policy | `McpCapabilityPolicy` (planned) | planned |
@@ -79,6 +86,7 @@ These are the canonical homes. Do not reintroduce private copies elsewhere.
7986
| `src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs` | `ComposerViewModel`, `SlashCommandPalette`, `AttachmentPreviewStrip`, `VoiceComposerController` |
8087
| `src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs` | `ConnectionPagePlan` (pure), `ConnectionPageViewModel`, `GatewayDirectConnectService`, gateway row models |
8188
| `src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs` | settings read/persist → `SettingsPageViewModel` + `ISettingsStore`; keep gateway-uninstall, uptime timer, saved-indicator, and app-info in the view |
89+
| `src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs` | state/commands → `PermissionsPageViewModel`; runtime projection → `PermissionsPageRuntimeSource`; persistence → `ISettingsStore` and `IExecApprovalsPresentationStore`; keep exact WinUI rendering, clipboard/privacy actions, and save-hint timer in the view |
8290
| `src/OpenClaw.Tray.WinUI/Services/NodeService.cs` | `McpServerHost`, `CanvasWindowManager`, `MediaCapabilityHost`, `RecordingConsentService`, `NodeCapabilityRegistry` |
8391
| `src/OpenClaw.Shared/OpenClawGatewayClient.cs` | `PendingRequestRegistry`, `ConnectEnvelopeBuilder`, `GatewayMessageRouter`, per-domain API facades |
8492
| `src/OpenClaw.Shared/Models.cs` | per-domain model files + `*Mapper` classes |
@@ -144,8 +152,12 @@ leading and trailing pipe. Columns, in order:
144152
| chat-history-replay-projection | authoritative | src/OpenClaw.Tray.WinUI/Chat/OpenClawChatDataProvider.cs | array-valued history content ordering projection | ChatHistoryReplayProjection | provider applies projected text and tool parts to the reducer | interleaved text, calls, and results replay in source order without clearing active tool correlation | OpenClawChatDataProviderTests.LoadHistoryAsync_InterleavedContentParts_PreserveChronologyAndCorrelation | behavioral | - |
145153
| reactor-tool-rendering-closed | closed | src/OpenClaw.Tray.WinUI/Chat/ReactorChatTimeline.cs | per-tool and grouped activity summary/detail rendering implementation | ToolCallCardRenderer | row projection, virtualization, hover state, assistant runs, and renderer delegation only | ReactorChatTimeline contains no tool detail renderer and delegates both standalone and grouped tool rows | ChatTimelinePresentationTests.ReactorTimeline_DelegatesToolAndActivityRenderingToFocusedOwner | source-shape | when ReactorChatTimeline is replaced as the production virtualization owner |
146154
| functional-chat-default-mount | closed | src/OpenClaw.Tray.WinUI/Chat/FunctionalChatHostExtensions.cs | mounting the FunctionalUI chat tree as the default ChatPage or ChatWindow surface | ReactorChatHostExtensions and OpenClawReactorChatRoot | legacy FunctionalUI chat files may remain for focused compatibility coverage only | ChatPage and ChatWindow mount the Reactor root directly into their existing ChatHost Borders; no FunctionalUI component mounts or nests Reactor on the default path | review-only: user explicitly deferred new tests for this migration; required build and existing shared/tray suites still run | review-only | when legacy FunctionalUI chat surfaces are removed |
147-
| settings-store | authoritative | src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs | hand-rolled save/echo suppression flags for two-way settings binding | ISettingsStore | PermissionsPage and other surfaces may read SettingsManager directly until migrated | a save originating from Update does not echo Changed to the caller and external saves are republished on the UI thread | SettingsStoreTests.Update_DoesNotEchoChangedToSelf | behavioral | when all settings surfaces read and write through ISettingsStore |
155+
| settings-store | authoritative | settings and permission UI surfaces | direct SettingsManager mutation and blanket self-write suppression | ISettingsStore | non-permission legacy surfaces may read SettingsManager until migrated; direct saves publish origin null | every save publishes one versioned event; only the matching writer ignores its own origin while all other active consumers refresh | SettingsSharedStateContractTests.TwoActiveSettingsPageViewModels_IgnoreOnlyOwnWrites_InBothDirections | behavioral | when every settings surface reads and writes through ISettingsStore |
148156
| settings-page-vm | authoritative | src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs | settings load, persist, echo-guard, and auto-save wiring | SettingsPageViewModel | code-behind keeps gateway-uninstall, gateway-info and uptime timer, saved-indicator visual, and app-info population | each settings control persists its field through the store preserving mutate-save-notify order and does not re-persist on external change | SettingsPageViewModelTests.ExternalChange_ReloadsWithoutRePersisting | behavioral | when the Settings page holds no settings persistence logic in code-behind |
157+
| exec-approvals-store | authoritative | src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs | direct exec-approvals.json snapshot, CAS persistence, file observation, and mutable policy cache | ExecApprovalsStore through IExecApprovalsPresentationStore | SystemCapability and NodeService consume the same App-owned concrete store for runtime enforcement | pure reads create nothing; CAS rejects stale hashes; one store-owned observer publishes each distinct external replacement once and retains the last valid presentation snapshot on typed failure | ExecApprovalsStoreTests.Changed_ExternalCorruptThenValid_RaisesFailureThenRecovery | behavioral | - |
158+
| permissions-page-vm | authoritative | src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs | permission settings state, exec-approvals mutations, node/MCP/voice status decisions, and lifecycle subscriptions | PermissionsPageViewModel plus PermissionsPageRuntimeSource | code-behind keeps exact WinUI row/card construction, localization application, colors, visibility, clipboard/token reads, privacy launch, and save-hint timer | activation is pure; field-scoped settings writes preserve save-then-notify; V2 mutations preserve unrelated fields through CAS retry; deactivate/dispose releases subscriptions | PermissionsPageViewModelTests.ExternalValidChange_UpdatesOnce_AndCorruptRetainsLastValidDisplay | behavioral | - |
159+
| permissions-page-direct-owners-closed | closed | src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs | direct SettingsManager, ConnectionManager, and ExecApprovalsStore ownership or subscriptions | PermissionsPageViewModel plus authoritative stores | WinUI-only rendering and platform actions listed in permissions-page-vm | the page applies semantic state only; the view model is WinUI/App/SettingsManager/file-IO free and never creates a parallel mutable domain cache | PermissionsPageContractTests.PermissionsPageViewModel_StaysWinUiAndAppFree | source-shape | when PermissionsPage is replaced by a different view technology |
160+
| shared-mutable-domain-owner | authoritative | presentation pages and view models | backing-file or concrete-manager ownership, per-VM observers, and independent mutable copies of persisted domains | one observable service/store per shared mutable domain | immutable view state projected from authoritative snapshots | different active consumers converge through versioned origin-aware events or CAS snapshots without echo storms, stale whole-snapshot replay, or lost unrelated updates | SettingsSharedStateContractTests.PermissionsPageViewModel_ReceivesOneExternalUpdate_PerDistinctAppSurfaceOrigin | behavioral | - |
149161
| exec-reusable-binding | authoritative | src/OpenClaw.Shared/ExecApprovals/ExecCommandResolution.cs | deriving durable allowlist identities and Allow Always patterns from multi-segment shell resolution | ExecReusableCommandBinder | ExecCommandResolver.Resolve stays the singular resolution used by the state machine and prompt display | at most one identity may be durably authorized per request and it is a fully qualified existing `.exe` image whose arguments are pinned by the generated rule | ExecReusableCommandBinderTests.MultiElementCarrierTail_Binds | behavioral | - |
150162
| exec-multi-segment-allowlist-closed | closed | src/OpenClaw.Shared/ExecApprovals/ExecCommandResolution.cs | ResolveForAllowlist and ResolveAllowAlwaysPatterns feeding allowlist matching or Allow Always patterns | ExecReusableCommandBinder | the two methods remain compiled with their historical tests until removed but have no production callers | the approval pipeline derives AllowlistResolutions and AllowAlwaysPatterns only from ExecReusableCommandBinder.TryBind | ExecApprovalV2NormalizationPipelineOwnershipTests.Normalizer_DerivesDurableIdentity_OnlyFromReusableBinder | source-shape | when ResolveForAllowlist and ResolveAllowAlwaysPatterns are deleted |
151163
| canonical-cmd-carrier | authoritative | src/OpenClaw.Shared/Mxc/MxcConfigBuilder.cs | recognizing the cmd.exe /d /s /c carrier and extracting its command payload | CanonicalCmdCarrier | MxcConfigBuilder keeps cmd command-mode switch detection and command-line construction | the approvals binder and the MXC command-line builder agree on which argv shapes are the canonical cmd carrier and what payload they carry | CanonicalCmdCarrierTests.BinderAndMxcBuilder_AgreeOnCarrierRecognition | behavioral | - |
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace OpenClaw.Shared.ExecApprovals;
6+
7+
public interface IExecApprovalsPresentationStore
8+
{
9+
event EventHandler<ExecApprovalsChangedEventArgs>? Changed;
10+
11+
Task<ExecApprovalsReadOnlySnapshotResult> GetSnapshotReadOnlyAsync(CancellationToken cancellationToken = default);
12+
13+
ExecApprovalsWriterOrigin CreateWriterOrigin();
14+
15+
Task<ExecApprovalsSnapshot?> ReplaceAsync(
16+
string baseHash,
17+
ExecApprovalsFile replacement,
18+
ExecApprovalsWriterOrigin? origin,
19+
Func<ExecApprovalsFile, ExecApprovalsFile, string?>? deltaValidator = null);
20+
}
21+
22+
public sealed class ExecApprovalsWriterOrigin
23+
{
24+
internal ExecApprovalsWriterOrigin()
25+
{
26+
}
27+
}
28+
29+
public enum ExecApprovalsChangeKind
30+
{
31+
SnapshotUpdated,
32+
SnapshotRecovered,
33+
SnapshotInvalid,
34+
}
35+
36+
public enum ExecApprovalsSnapshotFailureKind
37+
{
38+
LegacyMigrationRequired,
39+
UntrustedPath,
40+
UnsupportedVersion,
41+
MalformedJson,
42+
ReadFailed,
43+
}
44+
45+
public sealed record ExecApprovalsSnapshotFailure(
46+
ExecApprovalsSnapshotFailureKind Kind,
47+
string Hash,
48+
int? Version,
49+
string Message);
50+
51+
public sealed record ExecApprovalsReadOnlySnapshotResult(
52+
ExecApprovalsSnapshot? Snapshot,
53+
ExecApprovalsSnapshotFailure? Failure,
54+
ExecApprovalsSnapshot? LastValidSnapshot)
55+
{
56+
public bool IsSuccess => Failure is null;
57+
}
58+
59+
public sealed class ExecApprovalsChangedEventArgs : EventArgs
60+
{
61+
public ExecApprovalsChangedEventArgs(
62+
long sequence,
63+
ExecApprovalsChangeKind kind,
64+
string hash,
65+
int? version,
66+
ExecApprovalsSnapshot? snapshot,
67+
ExecApprovalsSnapshotFailure? failure,
68+
ExecApprovalsSnapshot? lastValidSnapshot,
69+
ExecApprovalsWriterOrigin? origin)
70+
{
71+
Sequence = sequence;
72+
Kind = kind;
73+
Hash = hash;
74+
Version = version;
75+
Snapshot = snapshot;
76+
Failure = failure;
77+
LastValidSnapshot = lastValidSnapshot;
78+
Origin = origin;
79+
}
80+
81+
public long Sequence { get; }
82+
83+
public ExecApprovalsChangeKind Kind { get; }
84+
85+
public string Hash { get; }
86+
87+
public int? Version { get; }
88+
89+
public ExecApprovalsSnapshot? Snapshot { get; }
90+
91+
public ExecApprovalsSnapshotFailure? Failure { get; }
92+
93+
public ExecApprovalsSnapshot? LastValidSnapshot { get; }
94+
95+
public ExecApprovalsWriterOrigin? Origin { get; }
96+
}

0 commit comments

Comments
 (0)