Skip to content

macOS/onboarding: prompt for remote gateway auth tokens#43100

Merged
ngutman merged 3 commits intomainfrom
feat/macos-onboarding-gateway-token-prompt
Mar 11, 2026
Merged

macOS/onboarding: prompt for remote gateway auth tokens#43100
ngutman merged 3 commits intomainfrom
feat/macos-onboarding-gateway-token-prompt

Conversation

@ngutman
Copy link
Contributor

@ngutman ngutman commented Mar 11, 2026

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: macOS onboarding did not explain when a remote gateway actually needed a shared auth token, and it showed a generic success state even when the connection succeeded via a paired-device token.
  • Why it matters: users could not tell when to enter gateway.remote.token, where to find the gateway token on the host, or why one Mac connected while a fresh client would still be rejected.
  • What changed: added a shared remote probe that surfaces structured connect-auth failures and successful auth source, shows onboarding/settings token guidance when the gateway requires token auth, and labels paired-device success explicitly.
  • What did NOT change (scope boundary): this PR does not add password-auth UI, and it does not change gateway auth semantics on the server.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • 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

  • None

User-visible / Behavior Changes

  • Remote onboarding now shows a token-required prompt only when the gateway handshake reports token auth is required.
  • The prompt explains that the token comes from the gateway host and points users to openclaw config get gateway.auth.token, OPENCLAW_GATEWAY_TOKEN, and openclaw doctor --generate-gateway-token.
  • Successful remote checks now distinguish Connected via paired device from generic gateway readiness.
  • macOS Settings reuses the same remote probe result so onboarding and Settings show consistent auth messaging.

Security Impact (required)

  • New permissions/capabilities? (Yes/No): No
  • Secrets/tokens handling changed? (Yes/No): Yes
  • New/changed network calls? (Yes/No): No
  • Command/tool execution surface changed? (Yes/No): No
  • Data access scope changed? (Yes/No): No
  • If any Yes, explain risk + mitigation:
    The PR changes only how macOS classifies and presents existing gateway auth outcomes. Shared tokens are still stored/resolved through existing config paths, and the new UI copy is explicit about the token living on the gateway host.

Repro + Verification

Environment

  • OS: macOS 14 arm64
  • Runtime/container: SwiftPM debug build for apps/macos
  • Model/provider: N/A
  • Integration/channel (if any): Remote gateway over direct wss:// and SSH tunnel
  • Relevant config (redacted): gateway host configured with gateway.auth.mode=token; local macOS app tested both with and without stored device-token state

Steps

  1. Configure a remote gateway in macOS onboarding or Settings without a gateway.remote.token value.
  2. Probe a gateway that requires shared token auth.
  3. Probe again from a Mac/device that already has paired-device auth available.

Expected

  • Token-required gateways show a targeted prompt that explains where to find or generate the gateway token.
  • Token mismatch and host-misconfigured cases show different messages.
  • Successful paired-device auth is labeled explicitly instead of looking like “token not needed”.

Actual

  • With this PR, onboarding/settings now surface the expected token guidance and auth-source-specific success copy.
  • Without this PR, the UI showed a generic ready state and did not explain why an already-paired Mac could connect.

Evidence

Attach at least one:

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

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios:
    • swift test --package-path apps/macos --filter OnboardingRemoteAuthPromptTests
    • swift test --package-path apps/macos --filter GatewayChannelConnectTests
    • Relaunched the macOS app in onboarding mode after clearing local app prefs and device-auth state.
    • Confirmed the direct Tailscale Serve path could still authenticate this Mac without a shared token, which is why the new paired-device success label is needed.
    • Verified the SSH-tunneled path with a fresh OPENCLAW_STATE_DIR and no stored device token returns unauthorized: gateway token missing (provide gateway auth token).
  • Edge cases checked:
    • token missing
    • token mismatch
    • gateway token not configured on host
    • password-auth gateway classification
    • paired-device success copy
  • What you did not verify:
    • full end-to-end SwiftUI UI automation for the onboarding flow
    • settings UI snapshot coverage for every auth-source variant

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.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

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

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: revert this branch or restore the previous onboarding/settings probe/UI behavior.
  • Files/config to restore: apps/macos/Sources/OpenClaw/RemoteGatewayProbe.swift, onboarding/settings auth prompt handling, and the structured connect-auth plumbing in apps/shared/OpenClawKit.
  • Known bad symptoms reviewers should watch for: onboarding shows the token field too aggressively, or success labels misclassify shared-token vs paired-device auth.

Risks and Mitigations

List only real risks for this PR. Add/remove entries as needed. If none, write None.

  • Risk: remote auth behavior differs between direct Tailscale endpoints and SSH-tunneled loopback endpoints, so reviewers may misread a direct success as “token not required”.
    • Mitigation: the probe now surfaces paired-device success explicitly, and the manual verification above exercises the SSH path with fresh auth state to confirm the true token-required case.

@aisle-research-bot
Copy link

aisle-research-bot bot commented Mar 11, 2026

🔒 Aisle Security Analysis

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

# Severity Title
1 🟠 High SSH MITM risk: Remote gateway probe auto-trusts unknown host keys (StrictHostKeyChecking=accept-new)
2 🟡 Medium Remote gateway auth token can be persisted in plaintext config during remote probe flow
3 🔵 Low Untrusted gateway-provided auth error message logged with OSLog privacy .public

1. 🟠 SSH MITM risk: Remote gateway probe auto-trusts unknown host keys (StrictHostKeyChecking=accept-new)

Property Value
Severity High
CWE CWE-295
Location apps/macos/Sources/OpenClaw/RemoteGatewayProbe.swift:183-190

Description

RemoteGatewayProbe.sshCheckCommand performs an SSH reachability check using permissive host key verification:

  • The SSH option StrictHostKeyChecking=accept-new automatically trusts the first-seen host key without user confirmation.
  • If an attacker can intercept/redirect the initial connection (or if known_hosts is cleared), the app may silently trust the attacker’s SSH server (TOFU without prompt), enabling man-in-the-middle.
  • This probe is used as a preflight before establishing remote transport; successful MITM can allow interception/modification of subsequent tunneled traffic and authentication material (e.g., gateway token/device pairing flows) that rely on the SSH channel being to the intended host.

Vulnerable code:

let options = [
    "-o", "BatchMode=yes",
    "-o", "ConnectTimeout=5",
    "-o", "StrictHostKeyChecking=accept-new",
    "-o", "UpdateHostKeys=yes",
]

Recommendation

Do not automatically trust unknown SSH host keys.

Recommended approaches:

  1. Fail closed for unknown keys and require the user to explicitly verify/accept the host fingerprint (out-of-band) before continuing.
  2. Use an app-managed known_hosts file to avoid relying on global state and to support explicit key pinning.

Example (fail closed + app known_hosts):

let knownHostsPath = "\(appSupportDir)/known_hosts"
let options = [
  "-o", "BatchMode=yes",
  "-o", "ConnectTimeout=5",
  "-o", "StrictHostKeyChecking=yes",
  "-o", "UserKnownHostsFile=\(knownHostsPath)",
  ​// consider disabling automatic rewrite unless you have a UX for it:
  "-o", "UpdateHostKeys=no",
]

For first-time connections, implement a UX that:

  • retrieves the presented host key (or fingerprint),
  • shows it to the user,
  • instructs the user to compare it with the gateway host’s displayed fingerprint,
  • and only then writes it to the app’s known_hosts file.

2. 🟡 Remote gateway auth token can be persisted in plaintext config during remote probe flow

Property Value
Severity Medium
CWE CWE-312
Location apps/macos/Sources/OpenClaw/RemoteGatewayProbe.swift:131-136

Description

The new remote onboarding probe flow syncs gateway configuration to disk before probing connectivity, which can cause the gateway token to be written to a local JSON config file in plaintext.

Key points:

  • RemoteGatewayProbe.run() now calls AppStateStore.shared.syncGatewayConfigNow() before performing the probe.
  • syncGatewayConfigNow() writes the merged gateway config via OpenClawConfigFile.saveDict(...).
  • When the user has typed a token (which sets remoteTokenDirty = true), the token is serialized into the config dictionary as a normal string under gateway.remote.token.
  • The config file is stored at OpenClawPaths.configURL (default ~/.openclaw/openclaw.json, or OPENCLAW_CONFIG_PATH override) and saveDict does not enforce restrictive permissions (e.g., 0600) or use Keychain-backed secret storage.
  • In onboarding, the probe temporarily switches connectionMode to .remote to “reuse the shared remote endpoint stack”; because syncing only writes remote settings when connectionMode == .remote, this temporary switch is enough to persist remote settings/tokens even if the user did not intend to commit them.

Vulnerable flow (trigger):

@​MainActor
static func run() async -> RemoteGatewayProbeResult {
    AppStateStore.shared.syncGatewayConfigNow()
    ...
}

Persistence sink:

@​MainActor
func syncGatewayConfigNow() {
    let synced = Self.syncedGatewayRoot(... remoteToken: self.remoteToken,
                                        remoteTokenDirty: self.remoteTokenDirty)
    ...
    OpenClawConfigFile.saveDict(synced.root)
}

Impact:

  • A gateway auth token is a reusable credential. Persisting it in plaintext on disk can lead to credential theft by local attackers/processes that can read the config file (especially if default filesystem permissions allow broader reads than intended).

Recommendation

Avoid persisting the gateway token in plaintext and avoid writing it during a probe unless the user explicitly confirms saving.

Suggested fixes:

  1. Store the token in macOS Keychain and keep only an opaque reference in openclaw.json (e.g., a $secretRef-style identifier).
  2. Enforce filesystem permissions for any on-disk config that may contain secrets:
    • ~/.openclaw directory: 0700
    • openclaw.json: 0600

Example (permission hardening after write):

try data.write(to: url, options: [.atomic])
try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
try FileManager.default.setAttributes([.posixPermissions: 0o700],
                                     ofItemAtPath: url.deletingLastPathComponent().path)
  1. For onboarding probes, keep probe-only settings in memory (do not call syncGatewayConfigNow()), or gate persistence behind a dedicated “Save remote settings” / “Remember token” toggle.

3. 🔵 Untrusted gateway-provided auth error message logged with OSLog privacy .public

Property Value
Severity Low
CWE CWE-117
Location apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift:257-266

Description

The gateway's websocket connect response can include an arbitrary error message string. This string is propagated into GatewayConnectAuthError.message/errorDescription and then logged using privacy: .public.

  • Input (remote-controlled): res.error?["message"] from the gateway JSON response
  • Propagation: stored in GatewayConnectAuthError.message and returned via error.localizedDescription
  • Sink: logger.error(... \(error.localizedDescription, privacy: .public))

Impact:

  • If a gateway endpoint is malicious/compromised (or if a legitimate gateway includes sensitive details in the message), the client will write that data into system logs without redaction.
  • The message may also contain newlines/control characters, enabling log forging/injection (CWE-117) and making log analysis unreliable.

Vulnerable code (sink):

self.logger.error(
  "gateway watchdog reconnect paused for non-recoverable auth failure \(error.localizedDescription, privacy: .public)"
)

Source of the logged string:

let msg = (res.error?["message"]?.value as? String) ?? "gateway connect failed"
throw GatewayConnectAuthError(message: msg, ...)

Recommendation

Avoid logging remote/server-provided messages as .public.

Recommended mitigations:

  1. Log structured, non-user-controlled fields (e.g., detail code / next step) and redact free-form messages:
if let authError = error as? GatewayConnectAuthError {
    self.logger.error(
        "gateway reconnect paused for auth failure code=\(authError.detailCodeRaw ?? "unknown", privacy: .public)"
    )
} else {
    self.logger.error("gateway reconnect paused for auth failure")
}
  1. If you must include the message for debugging, mark it private and sanitize control characters (e.g., replace newlines):
let safeMsg = error.localizedDescription
    .replacingOccurrences(of: "\n", with: " ")
    .replacingOccurrences(of: "\r", with: " ")

self.logger.error("gateway auth failure \(safeMsg, privacy: .private)")

Also consider enforcing a max length for server-provided messages before logging/surfacing them.


Analyzed PR: #43100 at commit 00e2ad8

Last updated on: 2026-03-11T12:57:01Z

@openclaw-barnacle openclaw-barnacle bot added app: macos App: macos size: L maintainer Maintainer-authored PR labels Mar 11, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 11, 2026

Greptile Summary

This PR adds structured remote-gateway auth error handling and contextual token-guidance UI to the macOS onboarding and Settings flows. The core change is a new RemoteGatewayProbe type that centralises the connection check (previously duplicated in GeneralSettings) and surfaces typed GatewayConnectAuthError failures rather than a generic string. GatewayConnectAuthError is promoted from a private type to a public API in GatewayErrors.swift with full GatewayConnectAuthDetailCode/GatewayConnectRecoveryNextStep enums, and the wrap() passthrough fix ensures structured auth errors are not swallowed by generic NSError wrapping. The onboarding token field now appears conditionally (on probe result or when a token is already set) instead of always. Test coverage for the new mapping and UI-visibility logic is solid.

  • syncGatewayConfigNow() is missing an @MainActor annotation — it accesses multiple main-actor-isolated UI-state properties; all callers today happen to be @MainActor, but there is no compile-time enforcement of this requirement.
  • probeRemoteConnection() unconditionally sets connectionMode = .remote before running the probe — if the remote connection section is visible because showAdvancedConnection == true (not because the user selected remote mode), clicking "Check connection" silently commits and persists the remote mode choice, which may be unexpected.

Confidence Score: 4/5

  • Safe to merge with minor actor-annotation and UX side-effect concerns noted above.
  • The changes are well-scoped, well-tested, and the critical wrap() passthrough fix is necessary for the feature to work correctly. The two flagged issues are a missing @MainActor annotation (no runtime risk today but a future thread-safety hazard) and an implicit connection-mode state change in probeRemoteConnection() (a UX concern rather than a correctness bug). Neither blocks shipping.
  • apps/macos/Sources/OpenClaw/AppState.swift (missing @MainActor on syncGatewayConfigNow) and apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift (implicit connectionMode = .remote side effect in probeRemoteConnection).

Comments Outside Diff (1)

  1. apps/macos/Sources/OpenClaw/AppState.swift, line 46-61 (link)

    Missing @MainActor annotation on syncGatewayConfigNow()

    The refactored function directly accesses self.connectionMode, self.remoteTransport, self.remoteTarget, self.remoteIdentity, self.remoteUrl, and other UI-state properties without an actor annotation. The original syncGatewayConfigIfNeeded() addressed this explicitly by capturing all values into locals before the Task { @MainActor in … } dispatch. The new version relies on callers to always run on the main actor — which holds today (both the Task { @MainActor in } wrapper and RemoteGatewayProbe.run() are @MainActor) — but there is no compile-time enforcement.

    Adding @MainActor to syncGatewayConfigNow() would make the requirement explicit and prevent a silent data-race if the function is ever called from a non-isolated context in the future.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/macos/Sources/OpenClaw/AppState.swift
    Line: 46-61
    
    Comment:
    **Missing `@MainActor` annotation on `syncGatewayConfigNow()`**
    
    The refactored function directly accesses `self.connectionMode`, `self.remoteTransport`, `self.remoteTarget`, `self.remoteIdentity`, `self.remoteUrl`, and other UI-state properties without an actor annotation. The original `syncGatewayConfigIfNeeded()` addressed this explicitly by capturing all values into locals _before_ the `Task { @MainActor in … }` dispatch. The new version relies on callers to always run on the main actor — which holds today (both the `Task { @MainActor in }` wrapper and `RemoteGatewayProbe.run()` are `@MainActor`) — but there is no compile-time enforcement.
    
    Adding `@MainActor` to `syncGatewayConfigNow()` would make the requirement explicit and prevent a silent data-race if the function is ever called from a non-isolated context in the future.
    
    How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 1ac86d7

@ngutman
Copy link
Contributor Author

ngutman commented Mar 11, 2026

Addressed the follow-up review feedback in 5bb4ed579:

  • added an explicit @MainActor annotation to AppState.syncGatewayConfigNow() so the isolation requirement is spelled out at the call site, even though AppState is already main-actor isolated.
  • made onboarding's remote probe preserve the user's original connectionMode after probing, while suppressing the feedback reset that would otherwise clear the result on restore.
  • added a focused onboarding test for the transient restore path.

Re-ran:

  • swift test --package-path apps/macos --filter OnboardingRemoteAuthPromptTests
  • swift test --package-path apps/macos --filter GatewayChannelConnectTests

@ngutman ngutman self-assigned this Mar 11, 2026
Copy link

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

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: afd5e9387b

ℹ️ 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 +470 to +473
if shouldRestoreMode {
// Reuse the shared remote endpoint stack for probing without committing the user's mode choice.
self.state.connectionMode = .remote
}

Choose a reason for hiding this comment

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

P1 Badge Avoid mutating global connection mode during probe

This probe path temporarily sets state.connectionMode to .remote even when the user has not selected Remote mode, which triggers real mode-change side effects outside onboarding. AppState.connectionMode updates are observed by the app shell (apps/macos/Sources/OpenClaw/MenuBar.swift), which calls ConnectionModeCoordinator.apply; the .remote branch in apps/macos/Sources/OpenClaw/ConnectionModeCoordinator.swift stops the local gateway and starts remote tunnel/control-channel setup. As a result, clicking Check connection from a Local/Unconfigured onboarding context can tear down the active local runtime and flap connection state despite being a read-only diagnostic.

Useful? React with 👍 / 👎.

@ngutman ngutman force-pushed the feat/macos-onboarding-gateway-token-prompt branch from afd5e93 to 00e2ad8 Compare March 11, 2026 11:52
@ngutman ngutman merged commit 144c1b8 into main Mar 11, 2026
22 checks passed
@ngutman ngutman deleted the feat/macos-onboarding-gateway-token-prompt branch March 11, 2026 11:53
@ngutman
Copy link
Contributor Author

ngutman commented Mar 11, 2026

Landed after rebasing the PR head onto main.

  • Local gate:
    • pnpm build
    • pnpm check
    • pnpm test failed in unrelated baseline tests: src/memory/embeddings.test.ts and src/memory/embeddings-voyage.test.ts with getaddrinfo ENOTFOUND example.com; reproduced on origin/main
  • Prepared head: 00e2ad8
  • Merge commit: 144c1b8

Thanks @ngutman!

hydro13 pushed a commit to andyliu/openclaw that referenced this pull request Mar 11, 2026
Merged via squash.

Prepared head SHA: 00e2ad8
Co-authored-by: ngutman <[email protected]>
Co-authored-by: ngutman <[email protected]>
Reviewed-by: @ngutman
Treedy2020 pushed a commit to Treedy2020/openclaw that referenced this pull request Mar 11, 2026
Merged via squash.

Prepared head SHA: 00e2ad8
Co-authored-by: ngutman <[email protected]>
Co-authored-by: ngutman <[email protected]>
Reviewed-by: @ngutman
mrosmarin added a commit to mrosmarin/openclaw that referenced this pull request Mar 11, 2026
* main: (49 commits)
  fix(agents): add nodes to owner-only tool policy fallbacks
  fix(gateway): propagate real gateway client into plugin subagent runtime
  fix(gateway): enforce caller-scope subsetting in device.token.rotate
  fix(terminal): stabilize skills table width across Terminal.app and iTerm (openclaw#42849)
  fix(models): guard optional model input capabilities  (openclaw#42096)
  macOS/onboarding: prompt for remote gateway auth tokens (openclaw#43100)
  fix(macos): use foundationValue when serializing browser proxy POST body (openclaw#43069)
  feat(ios): add local beta release flow (openclaw#42991)
  docs(changelog): update context pruning PR reference
  fix(context-pruning): cover image-only tool-result pruning
  fix(context-pruning): prune image-containing tool results instead of skipping them (openclaw#41789)
  fix(agents): include azure-openai in Responses API store override (openclaw#42934)
  fix(telegram): fall back on ambiguous first preview sends
  fix(telegram): prevent duplicate messages with slow LLM providers (openclaw#41932)
  Providers: add Opencode Go support (openclaw#42313)
  fix(sandbox): sanitize Docker env before marking OPENCLAW_CLI (openclaw#42256)
  macOS: add chat model selector and persist thinking (openclaw#42314)
  fix: clear pnpm prod audit vulnerabilities
  fix(build): restore full gate
  fix(gateway): split conversation reset from admin reset
  ...
dhoman pushed a commit to dhoman/chrono-claw that referenced this pull request Mar 11, 2026
Merged via squash.

Prepared head SHA: 00e2ad8
Co-authored-by: ngutman <[email protected]>
Co-authored-by: ngutman <[email protected]>
Reviewed-by: @ngutman
ahelpercn pushed a commit to ahelpercn/openclaw that referenced this pull request Mar 12, 2026
Merged via squash.

Prepared head SHA: 00e2ad8
Co-authored-by: ngutman <[email protected]>
Co-authored-by: ngutman <[email protected]>
Reviewed-by: @ngutman
Ruijie-Ysp pushed a commit to Ruijie-Ysp/clawdbot that referenced this pull request Mar 12, 2026
Merged via squash.

Prepared head SHA: 00e2ad8
Co-authored-by: ngutman <[email protected]>
Co-authored-by: ngutman <[email protected]>
Reviewed-by: @ngutman
leozhengliu-pixel pushed a commit to leozhengliu-pixel/openclaw that referenced this pull request Mar 13, 2026
Merged via squash.

Prepared head SHA: 00e2ad8
Co-authored-by: ngutman <[email protected]>
Co-authored-by: ngutman <[email protected]>
Reviewed-by: @ngutman
plabzzxx pushed a commit to plabzzxx/openclaw that referenced this pull request Mar 13, 2026
Merged via squash.

Prepared head SHA: 00e2ad8
Co-authored-by: ngutman <[email protected]>
Co-authored-by: ngutman <[email protected]>
Reviewed-by: @ngutman
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: macos App: macos maintainer Maintainer-authored PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant