Skip to content

native chat: identify a paired device by its device id, never its credential - #26777

Merged
brennanb2025 merged 2 commits into
mainfrom
brennanb2025/chat-caller-device-id
Oct 9, 2026
Merged

brennanb2025 merged 2 commits into
mainfrom
brennanb2025/chat-caller-device-id

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​89 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​89
Prod 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​17 $\color{#cf222e}{\Huge{\mathbf{−}}}$​5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​12

ELI5

When a paired phone or remote desktop talks to Orca, its connection carries a secret login token. Native chat used that token as the phone's name. It kept the name on disk with every message, retry record and prompt answer. It also sent it to the other screens showing the chat, which display it as "Answered on " under an answered question. So a device's login token was stored at rest and shown to other clients.

After this PR, native chat names a paired device by its device id, the same id the rest of Orca already uses for that device. The token is no longer stored or shown anywhere in native chat.

What Changed

The problem. The authenticated websocket for paired devices hands every request the device's raw bearer token as clientId (runtime-rpc-websocket-dispatch.ts). Native chat's structuredCallerFor (structured-agent-session-gate.ts) turned clientId into the chat caller key. That key is written to:

  • the operation ledger's caller_key (retry protection, about 24 hours);
  • chat records, which compare it for /clear and rewind;
  • attachment upload ownership;
  • queued-message settlement rows;
  • the resolvedBy field of an answered prompt, which is published to every client and rendered as "Answered on {device}".

So any paired device's credential sat in the chat database and appeared in other clients' transcripts.

Every native-chat place the key lands now gets the device id. Each one takes its key from structuredCallerFor, so this one change covers all of them:

  • Prompt answers: resolution.resolvedBy is the request's caller key (structured-agent-session-mutation-turn-context.ts:33 → structured-agent-session-turns-prompt.ts:119).
  • Attachments: upload ownership comes from structuredCallerFor(ctx).callerKey (structured-agent-session-attachments.ts:31).
  • Everything else on the native-chat surface: the agentSession.* methods (send, /compact, /clear, rewind, Stop, settings, goals, queued Send Now/Delete/Resume, restart continue, the legacy send path) use the same function. Their ledger rows, chat-record callerKey fields and queued-message settlement keys all come from it.
  • Other callers: agent launches already used rpcCallerOperationKey (the device id). Orchestration and restart continuation use their own non-secret keys.

User-facing change.

  • Before: an answered prompt from a paired device showed "Answered on" followed by that device's login token.
  • After: it shows the device id. Nothing else changes for users, and no device is asked to re-pair.

Mechanism. structuredCallerFor now reads the identity the host stamped from the authenticated connection (ctx.caller). For a paired device that is rpcCallerOperationKey(ctx.caller), the device id. That is the same key the launch ledger already uses for paired devices, and it survives credential rotation. ctx.caller is set on both the streaming and the one-shot dispatch paths. pairedDeviceId is only passed on the streaming path, so this PR doesn't use it. The desktop app (desktop-renderer) and the local CLI (trusted-local:runtime) keep their existing keys, so their in-flight retries and stored records are unaffected.

Why

The caller key is durable, shared identity, so it has to be a non-secret, stable name for the caller, not the secret the caller authenticates with. The repo already has that name in rpcCallerOperationKey. Using it here removes the credential from every stored and published native-chat key at once, rather than masking it at each place it lands.

Alternatives considered:

  • Hash the token, as orchestration does for its caller fingerprint. Rejected: a hash still changes whenever the credential rotates, so retries and records would stop matching after a rotation. The device id is the identity that survives rotation and is already the paired-device key in the launch ledger.
  • Switch every caller to rpcCallerOperationKey. Rejected for this PR: it would also rename the desktop's key from desktop-renderer to trusted-local:desktop. That is a separate namespace change with no security benefit.

No migration. Native chat has no real users yet. On development machines:

  • existing ledger rows written under a token age out on their own (about 24 hours);
  • old chat records and old prompt answers keep the key they were stored with until those chats are deleted.

New writes never store the token.

Not covered here (labelled):

  • Old development data is not scrubbed. Ledger rows age out in about 24 hours. Chat records and prompt answers written before this change keep the token until those chats are deleted. INTENDED: native chat has no real users yet, and the token matters only while that pairing is still valid.
  • One-shot dispatch path, outside native chat. The terminal createAgentSession/ensureAgentSession handlers key a caller by pairedDeviceId ?? clientId, and the one-shot dispatch path doesn't pass pairedDeviceId. No paired device uses that path today, since paired sockets always stream, so the credential isn't reachable there. It is fragile, though. TEMPORARY: a follow-up should key those handlers by rpcCallerOperationKey(ctx.caller) too.
  • The "Answered on" label shows a raw device id instead of a device name. Better than a credential, but not friendly. Follow-up: show the paired device's name.

Linked Issue

None. Found while reviewing how native chat stores retry identity (#26672, which picks this up from main).

Visual Proof

N/A: this is a host-side identity change. The only visible effect is the text after "Answered on" under a prompt a paired device answered, and that wasn't captured in the app. The proof is the automated test below: it fails on main by finding the credential in the host's stored files, and passes with this change.

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • New structured-agent-session-paired-caller.test.ts:

    • Unit: a paired device is keyed by its device id on both dispatch paths, whether or not pairedDeviceId is passed. The desktop and the local CLI keep desktop-renderer and trusted-local:runtime.
    • End to end: a real structured host, driven over agentSession.* as a paired device whose clientId is a credential, creates a chat and sends a message. The test then scans every file in the host's state directory. The credential appears in none of them, and the device id does appear.
    • Fails on main: with main's structuredCallerFor, one stored file contained the credential.
  • 29 structured RPC and integration test files (293 tests) pass, including attachments, adoption replay, holds and the launch-replay tests.

  • One narrow security review found no remaining path where a paired device's credential becomes a stored or published native-chat key. The only transport that puts a credential in clientId always stamps the paired-device identity.

  • CI at 90e62e0, after syncing with main: all checks pass. The first run was red only because main itself failed to typecheck, which test(native-chat): fix main typecheck after the batched journal commit #26781 fixed.

  • orca-ci-checks (lint, code quality, localization, typecheck) passed 23/23.

  • Platform: macOS. The change is host-side, so SSH and WSL hosts run the same code.

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Author: @BrennanKB5.

Security: removes a bearer credential from durable storage and from data published to other clients. Mobile and remote clients: no wire or protocol change. They send the same requests, and only the host-side key changes. Backwards compatibility: there are no real native-chat users, so there is no migration (see above).

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…s credential

A paired device's socket passes its bearer credential as clientId, and native
chat used it as the caller key it stores in ledger rows, receipts, chat records
and prompt answers (shown to other clients). Name a paired device by the device
id its socket proved instead.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review summary

The problem. A paired phone or remote desktop sends its login token as clientId on every request. Native chat used that token as the device's caller key, and that key is kept on disk:

  • operation ledger rows;
  • chat-record fields for /clear and rewind;
  • attachment upload ownership;
  • queued-message settlement;
  • prompt answers.

Prompt answers are also sent to every other client, which shows the key as "Answered on …". So a device credential was stored at rest and shown to other screens.

What changes for the user. An answered prompt from a paired device now says "Answered on " instead of the device's token. Nothing else changes: no re-pairing, and no wire or protocol change. The desktop app and local CLI keep their existing keys.

How. structuredCallerFor now reads the identity the host stamped from the authenticated connection. For a paired device, that is the device id (rpcCallerOperationKey), which is the same key agent launches already use and which survives credential rotation. Every native-chat key comes from this one function, so prompt answers, attachments and all agentSession.* actions are covered.

Review. One narrow security review traced every native-chat caller-key producer and every transport that puts a credential in clientId. It found no remaining path, and no P0-P2. Deferred, and labelled in the body:

  • Development data written before this change still holds tokens until it ages out or those chats are deleted. This is INTENDED, since there are no real users.
  • The terminal agent create/ensure handlers would use the token on the one-shot dispatch path, which no paired device uses today. TEMPORARY: a follow-up should key them by device id too.
  • "Answered on" shows a raw device id. Follow-up: show the device name.
  • Unproven: a phone retrying an in-doubt id across a host upgrade gets the new key.

Verified

  • New test: a real host is driven as a paired device to create a chat and send a message, then every stored file is scanned. The token appears nowhere, and the device id does. The same test fails on main, where it finds the token in a stored file.
  • Locally, before the main sync: 29 structured-chat test files / 293 tests pass.
  • After the main sync (90e62e0): the new test and the SQLite runtime boundary test pass, and orca-ci-checks passes 23/23.
  • CI at 90e62e0: every check passed, including the 5 test shards, the typecheck, cross-version wire compatibility and both packages. The first run was red only because main itself failed to typecheck, which test(native-chat): fix main typecheck after the batched journal commit #26781 fixed.

Not verified

  • The "Answered on" label in the running app.
  • A real phone or remote-desktop pairing.
  • SSH and WSL hosts. They run the same host code, but it wasn't exercised there.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 9, 2026 23:25
@brennanb2025
brennanb2025 merged commit 0f7c06a into main Oct 9, 2026
35 checks passed
brennanb2025 added a commit that referenced this pull request Oct 15, 2026
…s credential (#26777)

A paired device's socket passes its bearer credential as clientId, and native
chat used it as the caller key it stores in ledger rows, receipts, chat records
and prompt answers (shown to other clients). Name a paired device by the device
id its socket proved instead.

(cherry picked from commit 0f7c06a)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant