Repository navigation
native chat: identify a paired device by its device id, never its credential - #26777
Conversation
…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.
Review summaryThe problem. A paired phone or remote desktop sends its login token as
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. Review. One narrow security review traced every native-chat caller-key producer and every transport that puts a credential in
Verified
Not verified
|
…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)
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'sstructuredCallerFor(structured-agent-session-gate.ts) turnedclientIdinto the chat caller key. That key is written to:caller_key(retry protection, about 24 hours);/clearand rewind;resolvedByfield 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:resolution.resolvedByis the request's caller key (structured-agent-session-mutation-turn-context.ts:33→structured-agent-session-turns-prompt.ts:119).structuredCallerFor(ctx).callerKey(structured-agent-session-attachments.ts:31).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-recordcallerKeyfields and queued-message settlement keys all come from it.rpcCallerOperationKey(the device id). Orchestration and restart continuation use their own non-secret keys.User-facing change.
Mechanism.
structuredCallerFornow reads the identity the host stamped from the authenticated connection (ctx.caller). For a paired device that isrpcCallerOperationKey(ctx.caller), the device id. That is the same key the launch ledger already uses for paired devices, and it survives credential rotation.ctx.calleris set on both the streaming and the one-shot dispatch paths.pairedDeviceIdis 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:
rpcCallerOperationKey. Rejected for this PR: it would also rename the desktop's key fromdesktop-renderertotrusted-local:desktop. That is a separate namespace change with no security benefit.No migration. Native chat has no real users yet. On development machines:
New writes never store the token.
Not covered here (labelled):
createAgentSession/ensureAgentSessionhandlers key a caller bypairedDeviceId ?? clientId, and the one-shot dispatch path doesn't passpairedDeviceId. 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 byrpcCallerOperationKey(ctx.caller)too.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:pairedDeviceIdis passed. The desktop and the local CLI keepdesktop-rendererandtrusted-local:runtime.agentSession.*as a paired device whoseclientIdis 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.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
clientIdalways 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
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
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)