Skip to content

feat(ios): add exec approval push flow#59732

Closed
ngutman wants to merge 273 commits into
mainfrom
ios/exec-approval-push
Closed

feat(ios): add exec approval push flow#59732
ngutman wants to merge 273 commits into
mainfrom
ios/exec-approval-push

Conversation

@ngutman

@ngutman ngutman commented Apr 2, 2026

Copy link
Copy Markdown
Member

Summary

  • Problem: iOS could pair via QR bootstrap, but exec approvals did not have a native push/action flow and QR onboarding did not hand off enough operator auth to approve exec requests on-device.
  • Why it matters: iPhone users had to fall back to other channels or manually enter shared auth just to approve system.run requests, and push alerts were easy to misconfigure after a fresh install.
  • What changed: added iOS APNs delivery for exec approvals, native notification actions plus in-app notification-open dialog handling, QR bootstrap handoff for both node/operator device tokens, and immediate notification permission prompting after successful QR bootstrap onboarding.
  • What did NOT change (scope boundary): no approval inbox/history UI, no reconnect-time approval reconciliation/list RPC, no changes to existing Discord/Telegram approval semantics, and no broader auth/admin scope expansion beyond the QR bootstrap baseline needed for iOS exec approvals.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #
  • Related #
  • This PR fixes a bug or regression

Root Cause / Regression History (if applicable)

  • Root cause: QR setup codes issued a single-use bootstrap token, but the gateway only handed off the connected role's device token and the iOS app intentionally refused to start the operator loop from bootstrap-only auth. Separately, notification authorization was not requested during QR onboarding, so APNs transport registration could exist without user-visible alert permission.
  • Missing detection / guardrail: there was no end-to-end iPhone coverage for "fresh install -> QR bootstrap -> allow notifications -> receive exec approval push and resolve it on phone".
  • Prior context (git blame, prior PR, issue, or refactor if known): the existing onboarding path was node-bootstrap-oriented and the earlier exec approval implementation already depended on operator-scoped auth (operator.approvals).
  • Why this regressed now: exec approvals surfaced a pre-existing gap in QR bootstrap handoff because iOS now needed operator-scoped device auth on the same phone.
  • If unknown, what was ruled out: APNs auth, topic, bundle id, and sandbox delivery were ruled out with live push.test requests returning 200 against the phone's registered APNs token.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • src/gateway/server.auth.control-ui.suite.ts
    • src/infra/device-bootstrap.test.ts
    • src/infra/push-apns.test.ts
    • apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift
    • apps/ios/Tests/ExecApprovalNotificationBridgeTests.swift
    • apps/ios/Tests/NodeAppModelInvokeTests.swift
  • Scenario the test should lock in: QR bootstrap silently approves the initial iPhone device pairing, returns/stores both node and operator device tokens, requests notification permission after first bootstrap success, and delivers/cleans up exec approval APNs notifications correctly.
  • Why this is the smallest reliable guardrail: the bug crossed gateway auth, protocol payloads, token persistence, APNs delivery, and iOS UI/action handling, so no single layer alone could prove the user flow.
  • Existing test that already covers this (if any): prior tests covered generic APNs registration and separate approval flows, but not the combined QR-bootstrap-to-exec-approval path.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • iOS now receives native push notifications for exec approval requests.
  • iOS users can resolve exec approvals from notification actions (Allow Once, Allow Always when allowed, Deny).
  • Tapping the notification body opens the app and shows a compact in-app exec approval dialog.
  • QR bootstrap onboarding now hands off operator-capable device auth for iOS exec approvals without requiring manual shared token entry.
  • After a fresh QR bootstrap onboarding, the app immediately requests iOS notification permission so visible push alerts work on the same install.

Diagram (if applicable)

Before:
[scan QR] -> [node bootstrap only] -> [no operator token on phone] -> [no exec approval push/action flow]

After:
[scan QR] -> [silent bootstrap pairing for node+operator baseline]
            -> [hello-ok returns node+operator device tokens]
            -> [iOS stores both tokens and prompts for notifications]
            -> [exec approval APNs alert] -> [notification action or in-app dialog] -> [exec.approval.resolve]

Security Impact (required)

  • New permissions/capabilities? (Yes/No) Yes
  • Secrets/tokens handling changed? (Yes/No) Yes
  • New/changed network calls? (Yes/No) Yes
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) Yes
  • If any Yes, explain risk + mitigation:
    • QR bootstrap now hands off operator.approvals in addition to the existing iOS operator baseline, but still does not grant operator.admin or operator.pairing.
    • Bootstrap remains single-use and is revoked after the silent approval handoff.
    • Exec approval actions still resolve through the existing backend exec.approval.resolve path and existing allowed-decision rules.

Repro + Verification

Environment

  • OS: macOS + physical iPhone
  • Runtime/container: Node 24 / pnpm / Xcode 26.4 toolchain
  • Model/provider: N/A
  • Integration/channel (if any): iOS app + local gateway + direct APNs sandbox
  • Relevant config (redacted): loopback gateway on :18789, tailnet wss://nimrods-macbook-pro-m4.tail06a72.ts.net, direct APNs auth from local maintainer credentials

Steps

  1. Build and install the iOS dev app on a physical iPhone.
  2. Start a clean local gateway state, generate a QR setup code, and scan it from the iPhone.
  3. Allow the notification permission prompt shown immediately after bootstrap onboarding.
  4. Send a generic APNs test push, then trigger an exec approval request.
  5. Approve from the notification and verify exec.approval.waitDecision resolves.

Expected

  • QR onboarding silently pairs the phone for both node and operator roles.
  • Notification permission is requested during bootstrap onboarding.
  • Generic APNs alert appears on the phone.
  • Exec approval notification appears and resolves through the existing backend approval flow.

Actual

  • Verified on a real iPhone with the updated build.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios:
    • Fresh iPhone reinstall + QR bootstrap onboarding against an isolated local gateway.
    • Notification permission prompt appears immediately after successful QR bootstrap onboarding.
    • Generic APNs push reaches the phone after granting permission.
    • Exec approval push reaches the phone and resolves successfully from the phone.
    • Notification-open path presents the in-app exec approval dialog.
  • Edge cases checked:
    • Allow Always only appears when permitted.
    • Resolved/expired approvals clean up iOS notifications.
    • QR bootstrap handoff stores both node and operator device tokens.
  • What you did not verify:
    • Android/macOS equivalents.
    • A broader approval inbox/history UX.
    • Reconnect-time approval reconciliation beyond the minimal v1 push/action/dialog flow.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: QR bootstrap now grants a slightly wider iOS operator baseline than before.
    • Mitigation: limited to operator.approvals, operator.read, operator.write, and operator.talk.secrets; bootstrap stays single-use and does not grant admin/pairing scopes.
  • Risk: APNs notification permission timing could change first-run UX.
    • Mitigation: the prompt is only triggered after successful QR bootstrap onboarding, exactly when the app is about to rely on visible exec approval alerts.
  • Risk: iOS-only approval routing could drift from existing approval semantics.
    • Mitigation: notification actions and in-app dialog both reuse the existing exec.approval.resolve backend flow and allowed-decision rules.

@ngutman
ngutman requested a review from a team as a code owner April 2, 2026 14:38
@openclaw-barnacle openclaw-barnacle Bot added app: ios App: ios app: web-ui App: web-ui gateway Gateway runtime size: XL maintainer Maintainer-authored PR labels Apr 2, 2026
@greptile-apps

greptile-apps Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds end-to-end iOS exec approval push/action flow: APNs delivery of exec approval alerts (direct and relay transport), native notification actions and an in-app dialog for resolving approvals, and a QR bootstrap handoff that seeds both node and operator device tokens so users don't need manual shared auth. The implementation is well-structured, correctly scope-gates the new exec.approval.get method, and the operator.approvals scope expansion is appropriately bounded.

Confidence Score: 5/5

  • Safe to merge; all findings are P2 UX/style concerns with no blocking correctness or security issues.
  • The core auth flows (bootstrap token binding, scope gating, single-use revocation) are correctly implemented. Both inline findings are P2: one is a stale in-app dialog that the user can always cancel, the other is a non-live expiry label. Neither blocks the primary user flow or introduces data integrity risk.
  • apps/ios/Sources/OpenClawApp.swift — the resolved-push early return path should also clear a matching in-app exec approval dialog.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/ios/Sources/OpenClawApp.swift
Line: 110-116

Comment:
**In-app dialog not dismissed on resolved-push early return**

When the AppDelegate handles a resolved wake push here it removes system notifications and returns early — `appModel.handleSilentPushWake` (line 123) is never reached. That means `clearPendingExecApprovalPromptIfMatches` is never called, leaving the in-app exec approval dialog visible even after the approval was resolved externally. The user can still tap Cancel, or attempt to resolve (which surfaces a stale error that dismisses it), but the stale dialog is never proactively hidden.

A straightforward fix is to also clear the matching prompt from the model at the early-return site:

```swift
if await ExecApprovalNotificationBridge.handleResolvedPushIfNeeded(
    userInfo: userInfo,
    notificationCenter: notificationCenter)
{
    if let approvalId = ExecApprovalNotificationBridge.approvalID(from: userInfo) {
        self.appModel?.clearPendingExecApprovalPromptIfMatches(approvalId)
    }
    completionHandler(.newData)
    return
}
```

(Or expose a small `dismissExecApprovalPrompt(approvalId:)` helper on `NodeAppModel` if the clear method is not public.)

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/ios/Sources/Gateway/ExecApprovalPromptDialog.swift
Line: 149-164

Comment:
**Expiry label is a one-time snapshot and won't count down**

`expiresText` is computed with a fresh `Date()` when the view body renders, but there is no timer that re-renders the card periodically. Once the dialog is presented the "Expires" row will show a frozen value (e.g., "about 3 minutes") until some other observable change triggers a re-render. For a short-lived approval this can mislead the user into thinking they have more time than they do.

A `TimelineView(.periodic(from:, by:))` wrapper around the expiry row, or a simple `Timer`-backed `@State` variable that ticks every 30–60 seconds, would keep the display accurate without meaningful overhead.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (3): Last reviewed commit: "fix(ios): harden exec approval onboardin..." | Re-trigger Greptile

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3efce18c68

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +110 to +114
if await ExecApprovalNotificationBridge.handleResolvedPushIfNeeded(
userInfo: userInfo,
notificationCenter: notificationCenter)
{
completionHandler(.newData)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear in-app approval prompt on resolved silent push

When a silent exec.approval.resolved push arrives, this early return removes notifications but skips the appModel path that clears pendingExecApprovalPrompt. In the case where the app is running and the exec-approval dialog is open, resolving the same approval from another device leaves a stale dialog onscreen until the user dismisses it or hits an error on submit. Clear the matching prompt before returning when appModel is available.

Useful? React with 👍 / 👎.

Comment thread src/gateway/exec-approval-ios-push.ts Outdated
Comment on lines +173 to +174
await Promise.allSettled(
params.plan.targets.map(async (target) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle rejected APNs send tasks instead of swallowing them

sendRequestedPushes uses Promise.allSettled but never checks for rejected results, so thrown APNs send errors are silently dropped. In that failure mode handleRequested still reports delivery as available, which prevents the no-route fast-expire path and leaves requests pending until timeout even when iOS push was the only route. Inspect settled results (and log/mark undeliverable when all sends reject) to avoid silent hangs.

Useful? React with 👍 / 👎.

@aisle-research-bot

aisle-research-bot Bot commented Apr 2, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 3 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Sensitive command details exposed via exec.approval.get (no secret redaction)
2 🟡 Medium iOS exec-approval cleanup pushes may be sent to unauthorized operator devices when explicit targets are missing
3 🔵 Low Approval ID enumeration via ambiguous prefix error message
1. 🟠 Sensitive command details exposed via exec.approval.get (no secret redaction)
Property Value
Severity High
CWE CWE-200
Location src/gateway/server-methods/exec-approval.ts:99-113

Description

The new exec.approval.get method returns commandText / commandPreview derived from the pending approval request.

  • commandText is taken from request.command or systemRunPlan.commandText and only passed through sanitizeExecApprovalDisplayText, which only escapes invisible Unicode control characters.
  • This does not redact secrets that commonly appear in shell commands (API keys, tokens in URLs/headers, --password, --token, etc.).
  • The returned data is intended for mobile/iOS approval UX (including push), increasing the chance of accidental disclosure (lock screen notifications, logs, screenshots).

Vulnerable code:

const { commandText, commandPreview } = resolveExecApprovalCommandDisplay(resolved.snapshot.request);
respond(true, { commandText, commandPreview, ... }, undefined);

And the “sanitization” performed:

export function sanitizeExecApprovalDisplayText(commandText: string): string {
  return commandText.replace(EXEC_APPROVAL_INVISIBLE_CHAR_REGEX, formatCodePointEscape);
}

Recommendation

Redact secrets before returning/displaying commands, especially for mobile/push contexts.

Suggested approach:

  1. Keep sanitizeExecApprovalDisplayText for spoofing/invisible chars.
  2. Add a separate redaction layer (e.g., redactExecApprovalSecrets) that removes/obscures common secret patterns in command strings and previews.
  3. Consider returning only a safe preview by default and gating full command text behind an explicit user action.

Example:

function redactExecApprovalSecrets(text: string): string {
  return text// naive examples; expand with robust patterns
    .replace(/(token|apikey|api_key|password)=[^\s]+/gi, "$1=***")
    .replace(/(--(token|password)\s+)([^\s]+)/gi, "$1***");
}

const display = resolveExecApprovalCommandDisplay(snapshot.request);
const commandText = redactExecApprovalSecrets(display.commandText);
const commandPreview = display.commandPreview ? redactExecApprovalSecrets(display.commandPreview) : null;

Also ensure iOS push payloads avoid including full commands; use a generic message like “Approval requested” until the app is unlocked.

2. 🟡 iOS exec-approval cleanup pushes may be sent to unauthorized operator devices when explicit targets are missing
Property Value
Severity Medium
CWE CWE-862
Location src/gateway/exec-approval-ios-push.ts:288-314

Description

handleResolved/handleExpired send an APNs “resolved” (cleanup) notification using requireApprovalScope:false. When explicitNodeIds is undefined (e.g., gateway restart, crash, or any path where approvalTargetsById lacks the approval id), resolveDeliveryPlan() falls back to resolvePairedTargets() and will target all paired iOS operator devices, even if their active operator token does not include the operator.approvals scope.

Impact:

  • Unauthorized/low-privilege operator devices can receive openclaw.kind=exec.approval.resolved with the approvalId, leaking existence/identifiers of exec approval requests to devices that were never eligible to approve them.
  • This is a privacy leak and can become a social-engineering vector (e.g., a device learns approval IDs and can poll/guess state, or uses the signal to prompt user action).

Vulnerable code:

const explicitNodeIds = approvalTargetsById.get(resolved.id);
...
const plan = await resolveDeliveryPlan({
  requireApprovalScope: false,
  explicitNodeIds,
  log: params.log,
});

Recommendation

Do not fall back to broad paired-device targeting for cleanup pushes when the original target set is unknown.

Safer options:

  1. Only send cleanup pushes to the original target set; if explicitNodeIds is missing, skip push delivery.
  2. If you must broadcast cleanup, still enforce approval scope (requireApprovalScope:true) so only devices eligible for approvals receive the approvalId.

Example fix (option 1):

async handleResolved(resolved: ExecApprovalResolved): Promise<void> {
  const explicitNodeIds = approvalTargetsById.get(resolved.id);
  approvalTargetsById.delete(resolved.id);
  if (!explicitNodeIds?.length) return; // don't broadcast

  const plan = await resolveDeliveryPlan({
    requireApprovalScope: false,
    explicitNodeIds,
    log: params.log,
  });
  await sendResolvedPushes({ approvalId: resolved.id, plan, log: params.log });
}

Example fix (option 2):

const plan = await resolveDeliveryPlan({
  requireApprovalScope: true,
  explicitNodeIds,
  log: params.log,
});
3. 🔵 Approval ID enumeration via ambiguous prefix error message
Property Value
Severity Low
CWE CWE-203
Location src/gateway/server-methods/exec-approval.ts:46-55

Description

resolvePendingApprovalRecord accepts ID prefixes (lookupPendingId) and, when multiple pending approvals share the same prefix, it returns an error containing up to 3 matching approval IDs.

  • An attacker with access to exec.approval.get / exec.approval.resolve can probe short prefixes and learn valid approval IDs.
  • This creates an information disclosure side-channel and assists brute-force/guessing attacks against approval IDs.

Vulnerable code:

if (resolvedId.kind === "ambiguous") {
  const candidates = resolvedId.ids.slice(0, 3).join(", ");
  ...
  message: `ambiguous approval id prefix; matches: ${candidates}${remainder}. Use the full id.`,
}

And the prefix matching behavior:

if (id.toLowerCase().startsWith(lowerPrefix)) matches.push(id);

Recommendation

Avoid leaking candidate approval IDs in error messages.

  • Prefer a generic message (e.g., "ambiguous approval id prefix; use the full id") without listing matches.
  • Optionally disable prefix-based lookup entirely for remote/operator APIs; require full IDs.

Example:

if (resolvedId.kind === "ambiguous") {
  return {
    ok: false as const,
    response: {
      code: ErrorCodes.INVALID_REQUEST,
      message: "ambiguous approval id prefix; use the full id",
    },
  };
}

If prefix matching is needed for CLI UX, restrict it to trusted/local CLI contexts rather than network-exposed methods.


Analyzed PR: #59732 at commit a9b1366

Last updated on: 2026-04-02T16:00:33Z

@ngutman

ngutman commented Apr 2, 2026

Copy link
Copy Markdown
Member Author

Addressed the review feedback from the earlier PR iteration:

  • restored explicit QR bootstrap single-use coverage and node-token verification in src/gateway/server.auth.control-ui.suite.ts
  • documented and regression-tested the intentional non-operator scope normalization in src/infra/device-pairing.ts
  • made the iOS gateway/operator polling loops stop promptly on task cancellation in apps/ios/Sources/Model/NodeAppModel.swift
  • tightened bootstrap profile lookup so we only read it on the silent-bootstrap path while keeping the token handoff flow unchanged

Validation rerun:

  • pnpm test -- src/gateway/server.auth.control-ui.test.ts src/infra/device-pairing.test.ts -t 'auto-approves fresh node bootstrap pairing from qr setup code|requires approval for bootstrap-auth role upgrades on already-paired devices|requires approval for bootstrap-auth operator pairing outside the qr baseline profile|normalizes legacy node token scopes back to \[\] on re-approval|preserves explicit empty scope baselines for node device tokens'
  • pnpm build
  • xcodebuild -project apps/ios/OpenClaw.xcodeproj -scheme OpenClaw -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.6' -only-testing:'OpenClawTests/NodeAppModelInvokeTests/execApprovalPromptPresentationTracksLatestNotificationTap()' -only-testing:'OpenClawTests/NodeAppModelInvokeTests/successfulBootstrapOnboardingRequestsNotificationAuthorization()' test

Would appreciate a re-review.

@ngutman

ngutman commented Apr 2, 2026

Copy link
Copy Markdown
Member Author

@greptile-apps and @codex please re-review this PR.

Follow-up fixes are now pushed in 0fb80f51cb:

  • restored bootstrap single-use + node-token coverage in src/gateway/server.auth.control-ui.suite.ts
  • documented and regression-tested the intentional non-operator scope normalization in src/infra/device-pairing.ts
  • made the iOS operator/gateway wait loops cancellation-aware in apps/ios/Sources/Model/NodeAppModel.swift
  • narrowed bootstrap profile lookup to the silent-bootstrap path in src/gateway/server/ws-connection/message-handler.ts

Revalidated with:

  • pnpm test -- src/gateway/server.auth.control-ui.test.ts src/infra/device-pairing.test.ts -t 'auto-approves fresh node bootstrap pairing from qr setup code|requires approval for bootstrap-auth role upgrades on already-paired devices|requires approval for bootstrap-auth operator pairing outside the qr baseline profile|normalizes legacy node token scopes back to \[\] on re-approval|preserves explicit empty scope baselines for node device tokens'
  • pnpm build
  • xcodebuild -project apps/ios/OpenClaw.xcodeproj -scheme OpenClaw -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.6' -only-testing:'OpenClawTests/NodeAppModelInvokeTests/execApprovalPromptPresentationTracksLatestNotificationTap()' -only-testing:'OpenClawTests/NodeAppModelInvokeTests/successfulBootstrapOnboardingRequestsNotificationAuthorization()' test

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0fb80f51cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +110 to +114
if await ExecApprovalNotificationBridge.handleResolvedPushIfNeeded(
userInfo: userInfo,
notificationCenter: notificationCenter)
{
completionHandler(.newData)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route resolved exec-approval pushes through app model

This early return consumes exec.approval.resolved pushes before NodeAppModel.handleSilentPushWake runs, so pendingExecApprovalPrompt is never cleared in-app. In the common case where a prompt is open and the approval is resolved from another device, the modal remains stale until manually dismissed, even though the cleanup push arrived.

Useful? React with 👍 / 👎.

Comment on lines +799 to +803
bootstrapProfile = await getBoundDeviceBootstrapProfile({
token: bootstrapTokenCandidate,
deviceId: device.id,
publicKey: devicePublicKey,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use canonical key consistently for bootstrap profile lookup

The bootstrap profile lookup uses devicePublicKey (normalized form), but bootstrap-token binding earlier stores whatever key string came from the client. For clients sending a valid non-canonical form (for example PEM or padded/base64 variant), auth succeeds but getBoundDeviceBootstrapProfile returns null, disabling silent bootstrap approval and preventing the expected token handoff during onboarding.

Useful? React with 👍 / 👎.

@ngutman

ngutman commented Apr 2, 2026

Copy link
Copy Markdown
Member Author

@greptile-apps @codex @aisle-research-bot please re-review the latest commit a9b136652a.

This follow-up hardens the exact QR-onboarding + exec-approval-push flow without preserving the old broad iOS token behavior:

  • APNs exec-approval alerts are now lock-screen-safe and no longer include command text or request metadata in the payload
  • added exec.approval.get so iOS fetches full approval details after authenticated app open instead of trusting push payload contents
  • OpenClawKit now only persists hello-ok device tokens during the bootstrap handoff path, with a strict node / bounded operator allowlist
  • replaced the implicit bootstrap operator-scope approval path with an explicit bootstrap pairing helper on the gateway
  • iOS exec-approval push targeting now follows the active operator token scopes instead of historical baseline scopes
  • added regression coverage for the bootstrap handoff, new approval fetch path, payload hardening, and active-token push targeting

Validation rerun:

  • pnpm check
  • pnpm build
  • pnpm test -- src/infra/push-apns.test.ts src/gateway/server-methods/server-methods.test.ts src/gateway/server.auth.control-ui.test.ts src/infra/device-pairing.test.ts -t 'exec approval|bootstrap|node token|prompt presentation'
  • pnpm test -- src/gateway/exec-approval-ios-push.test.ts
  • xcodebuild -project apps/ios/OpenClaw.xcodeproj -scheme OpenClaw -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.6' -only-testing:'OpenClawTests/ExecApprovalNotificationBridgeTests/parsePromptMapsDefaultNotificationTap()' -only-testing:'OpenClawTests/NodeAppModelInvokeTests/execApprovalPromptPresentationTracksLatestNotificationTap()' -only-testing:'OpenClawTests/NodeAppModelInvokeTests/successfulBootstrapOnboardingRequestsNotificationAuthorization()' test

Note: swift test --package-path apps/shared/OpenClawKit --filter GatewayNodeSessionTests is still blocked by pre-existing unrelated actor-isolation failures in apps/shared/OpenClawKit/Tests/OpenClawKitTests/TalkSystemSpeechSynthesizerTests.swift, but the updated OpenClawKit sources were compiled successfully through the iOS app build/test path above.

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ngutman
ngutman marked this pull request as draft April 3, 2026 08:34
gumadeiras and others added 14 commits April 3, 2026 12:11
* fix: normalize kimi anthropic tool payloads

* fix: normalize kimi anthropic tool payloads (#59440)
* fix(browser): keep static helper seams cold

* fix(browser): narrow sandbox helper facade imports

* fix(browser): harden host inspection helpers
* fix(providers): centralize stream request headers

* Update src/agents/provider-request-config.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
…ation (#59112)

* fix(exec): strip invalid security/ask enum values during config normalization

* fix(exec): narrow invalid approvals config cleanup

---------

Co-authored-by: scoootscooob <[email protected]>
* fix: keep acp prompts alive across gateway reconnects

* fix: bound ACP prompts after disconnect grace

* fix: preserve ACP send timeout semantics

* fix: defer pre-ack ACP disconnect failures

* fix: reconcile ACP runs after reconnect

* fix: keep ACP reconnect deadlines monotonic

* fix: keep pre-ack ACP deadlines after reconnect

* fix: keep ACP prompts alive across gateway reconnects (#59473)

* fix: reject superseded ACP pre-ack prompts (#59473)

* style: format ACP reconnect regression updates (#59473)

* style: format ACP reconnect regression updates (#59473)

* fix: guard ACP send acceptance by run id (#59473)

* fix: scope ACP reconnect deadline by prompt (#59473)

* fix: recheck ACP prompts at reconnect deadline (#59473)

* fix: key ACP reconnect deadline by run (#59473)
)

* fix(ui): install devDependencies during ui:build

* fix: keep ui:build self-heal documented (#59267) (thanks @juliabush)

---------

Co-authored-by: Ayaan Zaidi <[email protected]>
* fix(qqbot): restrict structured payload local paths

* fix(qqbot): narrow structured payload file access

* test(qqbot): cover payload path traversal guards

* fix(qqbot): reduce structured payload log exposure

* fix(qqbot): preserve inline image payload URLs
@openclaw-barnacle openclaw-barnacle Bot added channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: voice-call Channel integration: voice-call channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser app: android App: android app: macos App: macos extensions: lobster Extension: lobster cli CLI command changes scripts Repository scripts commands Command implementations docker Docker and sandbox tooling agents Agent runtime and tooling channel: feishu Channel integration: feishu channel: irc extensions: anthropic extensions: openai extensions: minimax extensions: modelstudio extensions: kimi-coding extensions: moonshot extensions: fal extensions: tavily channel: qqbot extensions: stepfun labels Apr 3, 2026
@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: android App: android app: ios App: ios app: macos App: macos app: web-ui App: web-ui channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: imessage Channel integration: imessage channel: irc channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: qqbot channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: voice-call Channel integration: voice-call channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser cli CLI command changes commands Command implementations docker Docker and sandbox tooling docs Improvements or additions to documentation extensions: anthropic extensions: fal extensions: kimi-coding extensions: lobster Extension: lobster extensions: minimax extensions: modelstudio extensions: moonshot extensions: openai extensions: stepfun extensions: tavily gateway Gateway runtime maintainer Maintainer-authored PR scripts Repository scripts size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.