also looking at this
feat(engine): answer texts with Codex as well as Claude Code
#7
18 files
+1080
-142
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -9,6 +9,7 @@ import { CHAT_STYLE_DIRECT } from './messages-db' | ||
| 9 | 9 | * rule is an allowlist, never a denylist, and every check fails closed. |
| 10 | 10 | * |
| 11 | 11 | * A message is a command when ALL of these hold: |
| 12 | * - it arrived over iMessage, never SMS (see below), | |
| 12 | 13 | * - it is in a one-to-one chat (never a group: anyone could be added to one), |
| 13 | 14 | * - that chat is with an allowed handle, |
| 14 | 15 | * - it was sent BY that handle - or, in a chat with one of this Mac's own |
| @@ -33,7 +34,26 @@ export type Verdict = | ||
| 33 | 34 | | { accept: true, text: string } |
| 34 | 35 | | { accept: false, reason: string } |
| 35 | 36 | |
| 37 | /** | |
| 38 | * Only iMessage carries an identity worth trusting. | |
| 39 | * | |
| 40 | * With Text Message Forwarding on, which is the default once an iPhone is | |
| 41 | * paired and is the setup this product assumes, forwarded SMS lands in chat.db | |
| 42 | * as a direct chat whose identifier is the sender's number. SMS sender IDs are | |
| 43 | * spoofable through commodity bulk-SMS gateways; iMessage sender IDs are not. | |
| 44 | * Without this check, anyone who knows the owner's phone number could send one | |
| 45 | * SMS that passes every other test here and reaches an agent running with | |
| 46 | * approvals disabled in the owner's home directory. | |
| 47 | * | |
| 48 | * iMessage is also the only service that reaches this Mac over satellite, which | |
| 49 | * is what Uplink is for, so nothing real is lost. | |
| 50 | */ | |
| 51 | const COMMAND_SERVICE = 'iMessage' | |
| 52 | ||
| 36 | 53 | export function classify(message: IncomingMessage, ctx: FilterContext): Verdict { |
| 54 | if (message.service !== COMMAND_SERVICE) | |
| 55 | return { accept: false, reason: `not ${COMMAND_SERVICE}` } | |
| 56 | ||
| 37 | 57 | if (message.chatStyle !== CHAT_STYLE_DIRECT) |
| 38 | 58 | return { accept: false, reason: 'group chat' } |
| 39 | 59 | |
| @@ -107,8 +107,18 @@ describe('classify', () => { | ||
| 107 | 107 | expect(classify({ ...base, chatIdentifier: ME, chatGuid: `iMessage;-;${ME}`, sender: FRIEND }, selfCtx)).toEqual({ accept: false, reason: 'sender mismatch' }) |
| 108 | 108 | }) |
| 109 | 109 | |
| 110 | it('refuses SMS even from an allowed handle, because sender IDs are spoofable', () => { | |
| 111 | // With Text Message Forwarding on, a forwarded SMS is a direct chat whose | |
| 112 | // identifier is the sender's number, so every other check here passes. | |
| 113 | expect(classify({ ...base, service: 'SMS' }, ctx)).toEqual({ accept: false, reason: 'not iMessage' }) | |
| 114 | expect(classify({ ...base, service: 'SMS', chatIdentifier: ME, chatGuid: `SMS;-;${ME}`, isFromMe: true, sender: null }, ctx)) | |
| 115 | .toEqual({ accept: false, reason: 'not iMessage' }) | |
| 116 | }) | |
| 117 | ||
| 110 | 118 | it('fails closed on everything else', () => { |
| 111 | 119 | const rejected: Array<[Partial<IncomingMessage>, string]> = [ |
| 120 | [{ service: 'SMS' }, 'not iMessage'], | |
| 121 | [{ service: 'RCS' }, 'not iMessage'], | |
| 112 | 122 | [{ chatIdentifier: '+15559999999', sender: '+15559999999' }, 'chat not allowed'], |
| 113 | 123 | [{ chatStyle: 43 }, 'group chat'], |
| 114 | 124 | [{ isFromMe: true, sender: null }, 'outgoing message'], |