Skip to content

Add inbound pairing approval dialog (Mac-parity) - #778

Merged
shanselman merged 5 commits into
openclaw:masterfrom
bkudiess:bkudiess/connection-pairing-audit
Jun 21, 2026
Merged

Add inbound pairing approval dialog (Mac-parity)#778
shanselman merged 5 commits into
openclaw:masterfrom
bkudiess:bkudiess/connection-pairing-audit

Conversation

@bkudiess

@bkudiess bkudiess commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

What & why

When another device or node requests pairing with the gateway, the Windows operator now gets a focused approval dialog + awareness toast — matching the macOS DevicePairingApprovalPrompter / NodePairingApprovalPrompter — instead of only a passive "Pending approvals" banner that's easy to miss.

The plumbing already existed: the client receives the gateway's real-time device.pair.requested / node.pair.requested push events, but only refreshed an in-memory list. This PR adds the missing "last mile": a proactive, transparent, security-conscious decision surface.

How it works

Pure core (OpenClaw.Connection, fully unit-tested)

  • PendingApproval — unified device/node request model (ID truncation, ::ffff: IP normalization, repair flag)
  • PairingApprovalQueue — diff engine producing Added / ResolvedKeys / ConfirmedDecisions. A submitted approve/reject is optimistic-pending: suppressed from the actionable set so the UI advances, but only reported confirmed once the gateway drops it from the pending list (the authoritative signal — the approve/reject RPC only confirms the frame was sent). If the gateway never acts within 10s the submission expires and the request re-surfaces. A null list for a kind means "no fresh snapshot" (carried forward), never "now empty". Own-node requests are filtered by matching any advertised id.
  • PairingScopeDescriptions — operator scope → friendly label (ported from the Mac prompt)

Orchestration + UI (OpenClaw.Tray.WinUI)

  • PairingApprovalCoordinator — bridges the queue to the live operator client; gated on operator.pairing/admin scope + the ShowPairingApprovalDialog setting; in-flight + reconnect-race guards; a 20s safety-net reconcile poll that recovers requests dropped by the gateway's dropIfSlow=true broadcast
  • PairingApprovalDialog — code-built WindowEx (sidesteps the documented XAML-compiler bug); shows requester identity + the operator scopes being granted, kind-aware Approve label, ~1.5s anti-clickthrough delay, queue navigation, Approve / Reject / Decide-later
  • Wiring: GatewayService.PairListsChanged feed, awareness + confirmation toasts, review_pairing toast action, reset on disconnect, foreground-steal limited to once per reconnect burst, shutdown teardown
  • New ShowPairingApprovalDialog setting (default on); the in-page banner remains as the passive fallback

Localized across all 5 locales (en/fr/nl/zh-CN/zh-TW); CONNECTION_ARCHITECTURE.md updated.

Security posture

  • Approve is never the default button and is briefly disabled per request (anti click-through)
  • Connection and approval scope are re-checked at decision time (not just connection)
  • The dialog shows exactly which operator scopes are being granted, in plain language
  • The local node's own pairing is never prompted (handled by the separate auto-approve path)

Reviewer guide

  • Logic to scrutinize: PairingApprovalQueue.Reconcile (the optimistic-pending state machine) and PairingApprovalCoordinator.DecideAsync / OnPairListsUpdated.
  • Decision semantics: a confirmation toast fires on confirmed resolution (request left the pending list), not on send-ack.
  • Known, accepted limitations (documented, not bugs): in a rare multi-operator race the confirmation toast may show the wrong verb (the gateway remains the source of truth, visible in the devices list); a submitted decision can be delayed re-surfacing if approval scope is lost mid-connection (self-heals on reconnect). Both stem from the approve/reject RPC being send-acked rather than gateway-acked; fully closing them would require a response-aware RPC that reaches into the auto-approve core path + ConnectionPage and depends on unverified gateway reply behavior — intentionally out of scope.

Validation

  • ./build.ps1 — all 5 projects ✅
  • OpenClaw.Connection.Tests ✅ 303 (incl. confirmed-resolution / timeout-resurface / null-list / own-id-set cases)
  • OpenClaw.Shared.Tests ✅ 2049 · OpenClaw.Tray.Tests ✅ 959

Runtime popup behavior should be confirmed with a manual smoke test against a live gateway (per AGENTS.md).

Copilot and others added 2 commits June 17, 2026 16:17
When another device or node requests pairing with the gateway, the Windows
operator now gets a focused approval dialog plus an awareness toast — matching
the macOS pairing prompt — instead of only a passive in-page banner. The
gateway already pushed device/node.pair.requested to the client; previously it
only refreshed an in-page list.

- OpenClaw.Connection: PendingApproval, PairingApprovalQueue (pure diff engine
  with decided-suppression + own-node filter), PairingScopeDescriptions
  (friendly operator-scope labels) + 32 unit tests
- PairingApprovalCoordinator: queue + operator approve/reject RPCs, scope/setting
  gating, in-flight guards, and a 20s safety-net reconcile poll that recovers
  pair requests dropped by the gateway's dropIfSlow broadcast
- PairingApprovalDialog: code-built WindowEx (avoids the XAML-compiler bug);
  friendly scope list, kind-aware Approve label, ~1.5s anti-clickthrough delay,
  queue navigation, localized, accessible
- Wiring: GatewayService.PairListsChanged feed, awareness + confirmation toasts,
  review_pairing toast action, ShowPairingApprovalDialog setting (default on),
  reset on disconnect, own-node re-reconcile, shutdown teardown
- Localization across all 5 locales; CONNECTION_ARCHITECTURE.md updated

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses 7 findings from a Claude Opus 4.8 + GPT-5.3-Codex adversarial review
(4 HIGH-consensus, 3 single-model).

Confirmed-resolution model (#1/#2 HIGH): a send-ack only means the approve/reject
frame left the socket, not that the gateway accepted it. PairingApprovalQueue now
treats a decision as optimistic-pending (MarkSubmitted) and reports it confirmed
only when the request actually leaves the pending list; the success toast fires on
that confirmation, not on send-ack. If the gateway never acts within 10s the
submission expires and the request re-surfaces for retry — no permanent hide, no
false "approved" toast. Also guards against a disconnect (empty list while not
connected) being misread as mass confirmations.

Stale/closed dialog continuation (#3 HIGH): PairingApprovalDialog.DecideAsync now
captures the decision key, bails if the window closed or the queue advanced while
the RPC was in flight, and re-arms the approve guard on failure instead of
force-enabling Approve (preserving the anti-clickthrough delay). Render() is
IsClosed-guarded.

Own-node before identity known (#4 HIGH): NodeId and FullDeviceId can differ, and
the filter failed open while FullDeviceId was null. The coordinator now defers ALL
node requests while node mode is active but the own device id is unknown; they
re-surface (correctly self-filtered) once it is known.

Minor (#5/#6/openclaw#7): foreground-steal is limited to once per reconnect burst;
legacy device-id-as-requestId fallback is now safe (re-surfaces) and logged;
Reset() clears in-flight/poll state.

Tests: PairingApprovalQueue tests updated for MarkSubmitted + 4 new cases
(confirmed resolution, reject flag, timeout re-surface, node deferral).
Connection 301, Shared 2049, Tray 959 all green; full build passes.

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

clawsweeper Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codex review: needs real behavior proof before merge. Reviewed June 21, 2026, 3:07 AM ET / 07:07 UTC.

Summary
The PR adds a Windows tray inbound device/node pairing approval queue, focused approval dialog/toast, default-on setting, localization, docs, and tests.

Reproducibility: yes. for the review blockers: source inspection shows a current-main type-name collision and an approval-target safety regression in the new queue path. This is not an original bug report with a separate runtime reproduction.

Review metrics: 2 noteworthy metrics.

  • Diff size: 21 files, +2228/-5. The PR spans connection logic, tray UI, settings, resources, docs, and tests, so review needs both source and runtime evidence.
  • Persisted setting: 1 default-on setting added. Existing users would get proactive pairing dialog/toast behavior unless they opt out.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🧂 unranked krab
Result: blocked until real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Rename or alias the new approval-kind enum so current-main pairing models remain unambiguous.
  • [P2] Preserve the existing duplicate fallback-ID safety guard in the new queue/dialog path and add regression coverage.
  • [P1] Add redacted live gateway proof showing the dialog/toast and approve/reject outcomes.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The PR body lists build/tests but no redacted live gateway screenshot, recording, terminal output, linked artifact, or logs proving the dialog/toast and approve/reject outcome; after adding proof, updating the PR body should trigger a fresh ClawSweeper review, or a maintainer can comment @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Mantis proof suggestion
A visible Windows dialog/toast backed by live gateway pairing is best verified with desktop proof in addition to unit tests. A maintainer can ask Mantis to capture proof by posting this exact PR comment:

@openclaw-mantis visual task: verify a live inbound device/node pairing request opens the Windows approval dialog/toast and approve/reject updates the pending list.

Risk before merge

  • [P2] The new default-on dialog path can approve or reject gateway device/node access, and duplicate legacy fallback IDs can still produce an actionable wrong-target decision.
  • [P1] The branch is not current-main compatible until the new approval-kind type is renamed or aliased away from OpenClaw.Shared.PairingApprovalKind.
  • [P1] No redacted live gateway proof currently shows the dialog/toast and approve/reject result in a real setup.
  • [P1] The default-on setting changes existing users from passive in-page review to proactive dialog/toast behavior and needs maintainer product acceptance.

Maintainer options:

  1. Fix approval-target safety before merge (recommended)
    Rename or alias the new approval-kind model, preserve the duplicate fallback-ID guard in the dialog queue, and add focused regression coverage before maintainer approval.
  2. Accept the default-on approval surface explicitly
    After source blockers and live proof are resolved, maintainers can choose to accept the upgrade behavior that proactively focuses pairing approvals for existing users.
  3. Pause for product/security review
    If the default-on security UX or gateway decision semantics are not yet settled, keep the PR paused rather than landing a new core approval surface.

Next step before merge

  • [P1] Human review is needed because the source blockers are concrete but automation cannot supply the contributor's live gateway proof or product acceptance for the default-on approval surface.

Security
Needs attention: The diff has no dependency or supply-chain change, but the new approval path needs attention because duplicate legacy fallback IDs can still produce an actionable pairing decision.

Review findings

  • [P1] Rename or alias the approval kind — src/OpenClaw.Connection/PendingApproval.cs:9
  • [P1] Keep duplicate fallback IDs non-actionable — src/OpenClaw.Connection/PairingApprovalQueue.cs:99
Review details

Best possible solution:

Land the feature only after the branch is current-main compatible, duplicate legacy fallback IDs remain non-actionable, maintainers accept the default-on security UX, and redacted live gateway proof shows the dialog/toast plus approve/reject behavior.

Do we have a high-confidence way to reproduce the issue?

Yes for the review blockers: source inspection shows a current-main type-name collision and an approval-target safety regression in the new queue path. This is not an original bug report with a separate runtime reproduction.

Is this the best way to solve the issue?

No: the Mac-parity direction is plausible, but this patch must preserve existing fallback-target safety, avoid the current-main enum collision, and provide live gateway proof before it is the maintainable solution.

Full review comments:

  • [P1] Rename or alias the approval kind — src/OpenClaw.Connection/PendingApproval.cs:9
    Current main already defines OpenClaw.Shared.PairingApprovalKind; this PR adds OpenClaw.Connection.PairingApprovalKind and new tray/test code imports both namespaces before using PairingApprovalKind.Device/Node unqualified. Rename the new enum, for example to PendingApprovalKind, or explicitly alias it so the branch remains compatible with current main.
    Confidence: 0.9
  • [P1] Keep duplicate fallback IDs non-actionable — src/OpenClaw.Connection/PairingApprovalQueue.cs:99
    The existing Connections page disables decisions when legacy gateways omit RequestId and multiple pending rows share the fallback DeviceId/NodeId. The new queue collapses duplicate keys with last-wins behavior, leaving one actionable dialog target for an ambiguous request; drop or mark those duplicate-fallback requests non-actionable and add regression coverage.
    Confidence: 0.88

Overall correctness: patch is incorrect
Overall confidence: 0.9

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 62a4e774c157.

Label changes

Label justifications:

  • P2: This is a normal-priority user-visible pairing security/UX improvement with limited blast radius, but it is not merge-ready.
  • merge-risk: 🚨 security-boundary: The PR adds a default-on UI path that can approve or reject gateway pairing requests, and the diff currently weakens an existing wrong-target guard.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body lists build/tests but no redacted live gateway screenshot, recording, terminal output, linked artifact, or logs proving the dialog/toast and approve/reject outcome; after adding proof, updating the PR body should trigger a fresh ClawSweeper review, or a maintainer can comment @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

Security concerns:

  • [medium] Ambiguous fallback IDs can approve the wrong request — src/OpenClaw.Connection/PairingApprovalQueue.cs:99
    The new queue collapses duplicate fallback keys instead of disabling ambiguous legacy requests as current main does, which can let the dialog send an approve/reject RPC for a target the UI cannot distinguish.
    Confidence: 0.88

What I checked:

Likely related people:

  • ranjeshj: Introduced the current shared approval-kind model and recently carried broad connection/pairing hardening in the files this PR builds on. (role: recent area contributor; confidence: high; commits: ea36b12f9e4c, 429be9ba9368; files: src/OpenClaw.Shared/Models.cs, src/OpenClaw.Shared/WindowsNodeClient.cs, src/OpenClaw.Connection/GatewayConnectionManager.cs)
  • fuller-stack-dev: Recently added node reapproval and Command Center approval-state behavior in the same pairing UX boundary. (role: recent adjacent contributor; confidence: medium; commits: cb68abf8e75e; files: src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs, tests/OpenClaw.Tray.Tests/ConnectionPagePlanApprovalBehaviorTests.cs)
  • shanselman: Previously ported safer node pairing approval event handling and also authored the latest hardening commit on this PR branch. (role: historical adjacent contributor; confidence: medium; commits: 674b0e5a8373, 32c1e18dcb48; files: src/OpenClaw.Shared/WindowsNodeClient.cs, src/OpenClaw.Tray.WinUI/Services/PairingApprovalCoordinator.cs)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 18, 2026
Addresses an adversarial review of the final PR state. The prior "defer all
node requests while own id unknown" workaround was a regression, so this
replaces it with a robust own-node filter and tightens the confirmed-resolution
model.

F1 (regression): the deferNodeRequests mechanism dropped the ENTIRE node list
while node mode was on but FullDeviceId was null — making legitimate remote node
approvals vanish and potentially false-confirming in-flight node decisions.
Removed entirely.

F4: own-node identification now matches the node's actually-advertised id(s)
(NodeId and/or FullDeviceId) rather than assuming NodeId == FullDeviceId. The
queue takes a set of candidate own-ids; App supplies both. This removes the need
for deferral and closes the identifier-space gap.

F6/F2: Reconcile now treats a null device/node list as "no fresh snapshot for
that kind" (carried forward), not "now empty" — so a partial snapshot never
drops a kind's entries or confirms its submissions by absence. This also kills
the transient-empty-while-connected mass-confirmation edge.

F3: DecideAsync re-validates the connection after the approve/reject await before
recording the optimistic submission; a decision that raced a disconnect is not
recorded (it cleanly re-surfaces on reconnect) instead of being stranded and
later false-confirmed.

F8: read the operator client once per OnPairListsUpdated.

Accepted with rationale (documented): F5 (expiry starves only if scope is lost
mid-connection — rare, self-heals on reconnect) and F7 (confirmation toast may
show a slightly stale display name — cosmetic). F2's multi-operator wrong-verb
case is an inherent limit of not having a response-aware ack; single-operator
(the common Windows-companion case) is correct.

Tests: queue tests updated for the own-id set; +3 new cases (match any advertised
id, null-list-no-confirm/carry-forward, null-node-list-doesn't-drop).
Connection 303, Tray 959, Shared 2049; full build passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. labels Jun 18, 2026
No behavior change. Removes two genuinely-unused public members on
PairingApprovalCoordinator (CanApprove, Find — prompt gating uses
CanApproveWith(client) and lookups use the queue directly) and refreshes
doc comments/log text that still referenced the old "decided" model and
"null = empty list" semantics. The confirmed-resolution wording now matches
the implementation.

Connection 303, Tray 959, full build green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bkudiess
bkudiess marked this pull request as ready for review June 18, 2026 18:59
Avoid recording approvals after the operator client changes during an in-flight decision, and route stale review-pairing toast activations to Connection instead of silently no-oping.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@shanselman
shanselman merged commit 1a79731 into openclaw:master Jun 21, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P2 Normal priority bug or improvement with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants