Repository navigation
Finite for iOS: native client on the existing wire, no fixture server - #1028
Conversation
…wn key
A SwiftUI app for iOS 26 (ios/), organized like the web shell: the
launcher, chats with streaming, Stop and same-id retry, History, Your
Apps in web sheets, and Settings. The phone pairs through the existing
/cli approval with a secp256k1 key of its own, kept in Keychain, and
signs every request with NIP-98. Apps and brains open through
POST /api/f/{name}/session, the platform route in its own PR.
It talks to a real platform only: no fixture server and no unsigned
mode. Development runs against `cargo xtask dev`; unit tests run the
app's HTTP and live-socket code against a stub of the platform's answers,
including a lost acknowledgement, a refusal, a rate limit and a dropped
stream. A 4xx refusal of a post is final: the post leaves the outbox and
its words return to the composer.
The app keeps no copy of limits the server enforces. Renaming an agent
stays on the web, where its job is rewritten with its name.
The eval guard reads the app's Swift: WKWebView's evaluateJavaScript and
callAsyncJavaScript are refused like DevTools' evaluation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
futurepaul
left a comment
There was a problem hiding this comment.
Codex review @ 60bfeef: CHANGES
-
[P2] Never persist a draft that the loader rejects —
ios/Sources/ChatDraftStore.swift:53.savechecks only the encoded file size, whileloadalso rejects a composer over 32,768 UTF-8 bytes and attachments over eight. The composer accepts arbitrary pasted text, so pasting 32,769 ASCII characters and leaving the chat saves an unreadable draft. On reopening,ChatSession.initcatches that rejection and setspersistence = nil, hiding the saved draft/pending sends and preventing further sends in that chat. Refusal recovery can also concatenate otherwise valid drafts or attachment sets past these bounds. A focused Swift probe reproduced the successful save followed byinvalidResponseon load. Share validation between save/load and keep editing/refusal recovery within a recoverable representation; add an oversized-paste and refused-post-plus-existing-draft restart test. -
[P2] Scan executable Swift string interpolations —
xtask/src/eval_sinks.rs:247. Reusinglex_rustmarks Swift interpolation expressions as literal text. Consequentlylet result = "\(try await web.evaluateJavaScript(source))"executes the evaluator but produces no hits, while the equivalent direct call is caught. I verified both the actual scanner result and execution of the interpolation in Swift. Recognize Swift interpolation expression boundaries (including its raw/multiline forms) and scan their code; add planted interpolation cases for both native evaluators alongside the direct-call tests.
Non-blocking:
ios/Sources/AppAppearanceView.swift:28still disables Generate icons forhint.count > 200, despite the requested removal of copied server limits. Remove that condition so the server can return its own refusal text.
Validation: cargo xtask check passed. Reviewed key custody/signing, scoped web navigation and missing-session-route handling, same-id retry, and monotonic live/history cursors. Focused Mac probes used synthetic data; the draft-store probe omitted iOS-only file-protection settings to run on macOS. I did not rerun the simulator unit/UI suites.
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now. Additional findings at 60bfeef1bf6c841c0e22555e055d0823c77b8f27. The concurrent author-posted Codex review already covers the draft-persistence and interpolation-guard findings; those are not duplicated below:
-
[P2] Recover enough work history to retain the running turn. ChatSession.swift:130. Both channels initially subscribe with
last:50. A running turn with a start followed by 25 call/step pairs already puts its start outside that window (the contract allows 200 calls and 200 steps).Timeline.activeTurnsrequires that start, so opening this conversation reports no active turn and hides Stop while the agent is still working. Earlier-message paging only reads chat. Recover the active work state independently of the small message window and test opening/reopening a busy turn after more than 50 work records. -
[P2] Retry web sheets with the fresh redemption URL. FragmentWebSheet.swift:46. After a redeemed page encounters a main-frame failure, Try again clears
errorbut retains the oldurl. While the new requests await, the webview is recreated with that consumed token. Assigning the new redemption later does not load it becauseupdateUIViewis empty. Clear the old URL before retrying, or handle URL changes explicitly; test a failure after successful redemption followed by retry. This applies with the separately scoped native-session server changes deployed. -
[P2] Isolate optional archive loading from agent resolution. FragmentApp.swift:51. A Session Archive containing only cron/subagent imports legitimately has no
index/chat.json: the importer writes only kinds it holds, and the web shell treats this 404 as an empty list. Here that request throws before the agent-identity loop on every reload. Existing chats lose their agent grouping/names and membership selections cannot resolve. Treat the absent index as empty and let agent resolution proceed even when the optional archive read fails; test a cron-only archive and a transient archive failure. -
[P2] Download exports through the actual authenticated wire. SettingsView.swift:147. A ready
ExportViewcontainsdownload, a relative/api/export/{id}/downloadpath—noturl. Consequently a completed export never exposes the Share action. Simply renaming the lookup still fails the absolute-scheme check, and sharing the endpoint would not carry the phone's signing key. Resolve the path against the platform, download with native authentication, and share the resulting file; cover building → ready → download with the real response shape.
One smaller settings mismatch: SettingsView.swift:107 reads computer state, but ComputerView exposes phase, so State always displays “—”.
Validation: independently ran cargo test --locked --offline -p xtask eval_sinks -- --nocapture: 6 passed. GitHub's check job passed; e2e build/shards and Docker lanes were skipped. Findings above are source/contract traces, not executed iOS reproductions. This Linux environment has no Swift/Xcode/simulator; I could not run the native unit/UI tests, exercise the app, verify mobile layout, or capture genuine screenshots. No Cloudflare/workerd/container integration or external-service behavior was exercised by the focused Rust tests. The separately scoped session endpoint/deployment dependency is acknowledged, not counted as a defect here.
Review of #1028: - Editing, saving and loading a chat's draft hold one set of bounds (SavedChat): a paste past 32 KiB is cut at a character and said so; a save writes only what a load takes back. A saved file past its bounds loads what fits and says what was cut; one that is no saved chat is cleared and says so. No saved file stops a chat from opening or sending. - A refused send is never retried. Its words return to an empty composer; beside a draft they wait to be edited (never joined past a message's size), across restarts. - The eval guard lexes Swift itself: strings, multiline and raw ones (`#"…"#`), with each interpolation's code (`\(…)`, `\#(…)`) read as code, its own strings as text. - Generating icons no longer checks the hint's length: the server's refusal says it. - Connect shows Debug's --pair-server first, from its first frame, and the UI test never taps Connect for any other address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
futurepaul
left a comment
There was a problem hiding this comment.
Codex review @ 8f9171f: CHANGES
- [P2] Isolate the UI test before launching the app —
ios/UITests/FragmentUITests.swift:17. The new field guard prevents tapping Connect against the wrong address, but it runs afterapp.launch(). If the simulator already holds an approved production account,AccountConnection.initstill loads it andconnection.apistill targets its saved origin.FragmentRootimmediately runs account validation (FragmentApp.swift:138) and the shell's list load/watch (:189–190) against that origin; ten seconds later this test merely skips because the Connect field is absent. Thus this supposedly local test can still send signed requests to production without ever tapping Connect. Give this test an isolated, initially empty account store selected beforeAccountConnectioninitializes, or enforce an equivalent startup-level test-origin boundary; cover a pre-existing approved non-local account without contacting that origin.
The previous draft and interpolation findings are fixed, and the copied icon-hint limit is removed. cargo xtask check passed; the original interpolation probe now reports a hit, and focused Mac probes passed fitted draft round trips, over-bound recovery, retained refusal state, unreadable-file cleanup and subsequent saving (using synthetic data and omitting iOS file-protection options for macOS). I did not rerun simulator suites or contact production.
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at 8f9171fb24d8470e385ba26a12fd57f5eaf1348e.
[P2] Restore the original recipients when editing a refused message. ChatSession.swift:69–71. The new reopen flow restores text, attachments and reply context, but leaves recipients from the current composer. In a group chat, send a message to A and receive a transient failure; select B and compose another draft; retry A's message and receive a final refusal. After clearing/sending the B draft, tap Edit on the refused A message and resend: send uses the current B recipients (line 166), so the POST addresses B (line 188). Restore refused.recipients when reopening succeeds, while preserving the newer draft's recipients until then. Extend the new refusal/restart test with different recipient lists and assert the final POST's to.
The original oversized-draft failure is addressed in the source: editing/loading now normalize bounds and a failed load no longer disables persistence. The Swift interpolation regression cases pass, and the copied icon-hint limit is removed. The four findings in my previous review remain unchanged and still apply; their details are not reposted. The concurrent author-posted review separately covers UI-test account isolation.
Validation: independently ran cargo test --locked --offline -p xtask eval_sinks -- --nocapture on this exact checkout: 6 passed, including the added ordinary/raw/multiline/nested Swift interpolation cases. The recipient finding and draft recovery confirmation are source/test traces, not executed iOS reproductions. This Linux environment still lacks Swift/Xcode/simulator tooling, so the new native tests, device file protection, app interactions, screenshots and mobile layout were not independently exercised. No workerd/container integration or external services were exercised by these focused Rust tests.
Review of #1028: the UI test's guard ran after launch, when the app had already loaded and validated whatever account the simulator held, and could have asked that account's server. `--ui-test` (Debug only) now selects, before anything reads it, a Keychain service and Application Support folder of the launch's own, emptied as it starts (LaunchScope), and refuses any origin past loopback before a request: in the saved account it loads, in an address typed at Connect, and in every signed request. The UI tests launch that way; one seeds an approved account past loopback (`--ui-test-saved-account`) and finds it never used, another taps Connect for Finite's own address and is refused without a request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
futurepaul
left a comment
There was a problem hiding this comment.
Codex review @ 9f579ce: PASS
Verified that FragmentApp.init starts the isolated scope before constructing FragmentRoot and its account, that test Keychain/draft paths differ from the normal account's, and that saved-account loading, Connect, signed HTTP and socket requests enforce the loopback boundary. Release excludes the test-argument handling, and the production signed session retains its redirect refusal. cargo xtask check passed; network-free Mac probes of the actual scope/transport code passed Debug and Release argument handling, external-origin/request/socket rejection and redirect refusal, using synthetic identity/storage seams; I did not rerun the simulator suites.
alexlwn123
left a comment
There was a problem hiding this comment.
Pass for the launch-isolation delta 8f9171fb..9f579ce8b084f54d33440bbe99a6f92c6c85aadf; no new actionable defect found.
Source review confirms that the supplied UI tests select their separate Keychain/Application Support stores before account initialization. A seeded non-loopback account is rejected before an API client exists, and production pairing is refused before any request. The signed HTTP/WebSocket path also checks the launch scope. This addresses the previous UI-test account-isolation finding. The new source and tests are registered in the Xcode project.
The product findings in my initial review and recipient-recovery review remain unchanged; this COMMENT records only the clean isolation delta.
Validation: cargo test --locked --offline -p xtask eval_sinks -- --nocapture on this exact checkout: 6 passed, including the repository source scan. The isolation conclusions are source/test inspection. Swift/Xcode/simulator are unavailable here, so native compilation, Keychain/file-protection behavior, UI tests, app interactions, screenshots and mobile layout were not independently executed. No workerd/container integration or external services were exercised by the focused Rust tests.
…download Review of #1028 (alexlwn123): - Which turns run is kept apart from the record window. A chat reads a page of work (1000), and a turn begun before it has its start read back from earlier work (ten pages at most, as the web's chat page reads), so Stop shows for a long turn however the chat is opened. - A web sheet's load drops its spent redemption before asking again, and each redemption is a new web view: Try again never opens a used token (WebSheetModel). - An archive's chats index is optional: 404 is none, as the web's History reads it, and a failed read keeps what was read and never stops agent resolution, which now comes first. - Exports: the ready ExportView's `download` path, checked as this export's on the platform, downloads signed to a file of the phone's own, shared from there. - Editing a refused message restores its addressees; the newer draft's stay until then. - Settings reads a computer's `phase`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
futurepaul
left a comment
There was a problem hiding this comment.
Codex review @ 987d998: CHANGES
-
[P2] Keep an end's author until the start establishes ownership —
ios/Sources/Models.swift:145. When the backlog contains agent B'sturn.endnaming A's older running turn,startedBy == nilaccepts it immediately and removes the turn fromunresolvedTurns; no read-back finds A's start, so Stop disappears. Even subsequently learning A's start leavesendedtrue. The work channel admits other agent members, and docs/chat-records.md explicitly requires later records to belong to the start's principal. Retain pending ends with their principals, keep ownership unresolved until the start is found, and only apply the matching agent's end; test an end arriving before its start from both the same and a different agent. -
[P2] Do not discard end evidence that backward paging still needs —
ios/Sources/Models.swift:150–152. The arbitrary eviction of ended turns can erase an end already received, thenlearntreats that turn's older start as a new active turn. A probe using the actual Timeline code reproduced this with a normal 1,000-record backlog (one boundary turn's end, 499 completed turn pairs, and an unresolved busy turn) followed by the preceding page: the completed boundary turn was resurrected in all 30 runs. Preserve the ended/ownership evidence across the bounded read-back, or restrict older-page learning to the specific unresolved turns being recovered, so earlier starts cannot create spurious active turns. -
[P2] Make the work history request fit the client's response bound —
ios/Sources/ChatSession.swift:177. This now requests 1,000 records, butFragmentAPI.historyrejects responses over 4,194,304 bytes and thistry?silently abandons recovery. HTTPchannel_page/read_channel_beforelimit record count only; the 1 MiB page limit applies to live frames, not this HTTP route. Valid work records can therefore produce a page above 4 MiB (for example, many prompt-closed records carrying permitted answer text), leaving an older busy turn's start unread on every reopen. Use bounded smaller HTTP batches with an explicit total-work bound, or consume the larger response without violating the client's memory bound; add a valid large-page regression case.
The work subscription's last:1000 itself is within the live protocol limit. Reviewed the fresh web-view/redemption retry, optional archive reads, signed same-origin export download, recipient restoration and phase display without finding another blocker. cargo xtask check passed; focused network-free Mac probes reproduced findings 1 and 2 using the actual Timeline implementation. I did not rerun simulator or e2e suites.
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now at 987d9983792030b80eeb373a7306f6e4f19e4396. Two additional export-screen regressions in this delta:
-
[P2] Put export refresh on the Form that receives the gesture. SettingsView.swift:203. Starting an export stores
buildingand disables the action, but there is no polling or explicit refresh button. The new.refreshableis on the child Section; the containing Form still invokesSettingsDetail.load()(line 82), which now immediately returns for.data(line 163). Thus pull-to-refresh cannot advance the export to ready, and Download appears only after leaving/reopening the screen. Wire refresh to the owning Form or provide an explicit/polling refresh path. Test building → ready → Download while remaining on the screen. This follows SwiftUI's environment-scoped refresh action; it was not exercised in a simulator here. -
[P2] Keep creating a fresh export available alongside a ready one. SettingsView.swift:192–197. The ready branch exposes only Download/Share and hides
Export everything. Once any snapshot is ready, users cannot export subsequent data changes from the phone until that snapshot expires. The previous screen always offered the start action;POST /api/exportexplicitly replaces the previous export, and the web'scell/shell/exports.jsoffers “Export again” beside Download. Keep the start action available when ready, clear the downloaded-file state when starting again, and test ready → new building export.
Source/test inspection confirms the recipient restore, optional archive handling, fresh-redemption retry, authenticated export path and computer-phase fixes. The original 50-work-record case is addressed, but the concurrent author-posted review reports additional turn-state/backfill defects, including the same eviction issue I found; those are not duplicated here.
Validation: independently ran cargo test --locked --offline -p xtask eval_sinks -- --nocapture: 6 passed. GitHub check passed; e2e build/shards and Docker lanes were skipped. This Linux environment has no Swift/Xcode/simulator, so native tests, UI interactions, screenshots, mobile layout and device file protection remain unverified. These findings are source/contract traces; the focused Rust tests exercised no workerd/container integration or external services.
…de again Review of #1028 at 987d998: - Which turns run is the web chat page's logic (chat.js turnOf, control.js running), ported: a turn is its agent's and its id, a record counts only toward its own poster's turn, and a turn runs once its own start is read and until its end is, in either order. Another agent's end never stops it, and paging back never revives an ended turn: only turns both begun and ended are ever dropped (past 4096). - Earlier work is read back as the web's earlierWork reads it, to the oldest message shown, and also while a turn lacks its start, in batches of 50 (each answer within the app's 4 MiB, which the HTTP route's count-only pages do not promise), at most 1000 records a visit; a failed read says so. - An export is followed while it builds (every 2 s, as exports.js), the Form's pull to refresh reaches it, and Export again stays beside Download, dropping the old file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
futurepaul
left a comment
There was a problem hiding this comment.
Codex review @ c2a5c91: CHANGES
- [P2] Prevent an older export refresh from overwriting a new export —
ios/Sources/AppSettingsAPI.swift:216. Settings now starts two initial GETs: the Form's.task { await load() }callsexports.refresh(), andExportSectionindependently callsmodel.refresh(). When the first finishes,runsetsbusy = falseeven though the second is pending, enabling Export everything. If the user starts an export and its POST returnsbuilding, the older GET can then return its earliernull/ready snapshot and overwriteexport, stopping the state-keyed polling task or exposing the old archive instead. A network-free Mac probe of the actual ExportModel reproducedbusy=falsewith the second read pending, thenbuildingafter POST, thennilafter the older GET. Remove the duplicate initial refresh and invalidate/discard stale reads across start/refresh operations (or serialize operations), with a held-GET → successful POST → released-old-GET test; pending refreshes must not prematurely clear busy.
The three prior turn/read-back findings are fixed: agent-plus-id ownership matches the web's turnOf/running semantics, end-only evidence survives eviction, and 50 serialized bodies capped at 64 KiB plus their metadata fit under 4 MiB (the cell returns bodies as RawValue, not re-escaped strings). The read-back loop is bounded to 20 batches/1,000 records per invocation and reports failures; Form refresh and Export again are now wired. cargo xtask check passed, and focused Mac probes passed the original ownership/backward-page scenarios and retention past 4,096 turns; I did not rerun simulator suites.
alexlwn123
left a comment
There was a problem hiding this comment.
Fail for now — reviewed c2a5c91de92d9ea86d316b5d00054ae8f541f43f against 987d9983792030b80eeb373a7306f6e4f19e4396.
[P2] Clear the downloaded archive when refreshing to a different export — ios/Sources/AppSettingsAPI.swift:214. Download export A on the phone, make export B through web Settings, then pull to refresh on the phone after B is ready. refresh() updates export to B but leaves file pointing at A. ExportSection therefore displays B's status while “Share export” shares A's older snapshot, and hides the button to download B. This occurs with sequential, successful requests; it is separate from the concurrent stale-GET issue already reported. Associate downloaded files with their export ID and invalidate them when refreshed identity changes, including preventing an old in-flight download from restoring a mismatched file. Add an A-download → B-refresh test that requires downloading B before sharing.
The prior Form-refresh and Export-again findings are fixed. Source review also confirms the turn ownership, end-only retention, and 50-record backfill fixes. I independently confirmed the stale-refresh race covered by the concurrent review, so I have not duplicated it as another finding.
Validation: cargo test --locked --offline -p xtask eval_sinks -- --nocapture passed all 6 selected tests; diff whitespace check passed. GitHub check passes; the green e2e summary has skipped build/shard jobs, and Docker lanes are skipped. This Linux environment has no Swift/Xcode/simulator tooling: the new native tests, actual app interactions, device/file-protection behavior, and screenshots were not executed. No live services or workerd/container integration were exercised locally; the finding above is source analysis.
Review of #1028 at c2a5c91: - ExportModel runs reads and makes one after another, applying each answer in the order sent: an older read held in flight never lands after a newer export, and `busy` holds until every one has answered. The section's own first read is gone; the Form's is the one. - A downloaded archive is kept with its export's id: once the export shown is another (refreshed to one made on the web, or made again) it is deleted, and a download still in flight for one no longer shown is never kept. - The test stub answers each request on a queue of its own, so a test holding one answer (Gate) holds no other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
futurepaul
left a comment
There was a problem hiding this comment.
Codex review @ 57bc0ba: PASS
The stale-refresh finding is fixed: the Form owns the initial read, export reads/starts execute serially, and pending-operation counting keeps busy true until all complete. Downloaded files are tied to the shown ready export and deleted on replacement/expiration; an obsolete in-flight download is discarded rather than exposed for sharing. cargo xtask check passed, and network-free Mac probes of the actual ExportModel passed held-GET/queued-POST ordering, busy lifetime, A-to-B archive replacement, stale-download cleanup and same-id expiration; I did not rerun simulator suites.
alexlwn123
left a comment
There was a problem hiding this comment.
Pass for this export-fix delta — reviewed 57bc0ba43c28a26271420da4bc243967a486b756 against c2a5c91de92d9ea86d316b5d00054ae8f541f43f. No new actionable findings.
Both previous export findings are addressed: the Form owns the initial read, refresh/start operations execute in order, and pending-operation counting preserves busy state. Downloaded files are tied to the displayed ready export, removed when it changes or expires, and discarded if an obsolete download completes later. New stub tests cover the held-read/queued-start and archive-replacement/late-download sequences.
The native-session server changes are inherited from master 5439b95e; source tracing found the iOS signing, consent, return-path, redemption response and revocation contracts consistent with that endpoint.
Executed locally: cargo test --locked --offline -p xtask eval_sinks -- --nocapture — 6 selected tests passed; git diff --check passed. GitHub's check job, including cargo xtask check, passed.
This is source review of the Swift fixes: this Linux environment has no Swift/Xcode/simulator tooling, so I did not execute the new native tests, app interactions, device/file-protection checks or screenshots. No live services or workerd/container integration were exercised locally. The e2e summary is green with its build/shard jobs skipped; Docker lanes are also skipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
futurepaul
left a comment
There was a problem hiding this comment.
Codex review @ 59b9e40: PASS
No blocking findings in 57bc0ba..59b9e40.
Non-blocking
- Merge verification: Git's recomputed conflict-free merge tree is exactly HEAD's tree (
eff0a1a829cae519d81407a0fc1b8d4b856fd186). Every file changed by this merge matches its master parent,afa58362. There are no iOS or xtask changes; the docs delta only documents the early socket. - Incoming compatibility: #1034 changes browser socket startup (
cell/client.mjs:217,templates/chat/site/index.html:19), not the native live wire; native chat uses its own signed socket (ios/Sources/FragmentAPI.swift:191). #1035 changes shell frame-origin discovery, while native web sheets still read canonical status URLs and redeem scoped sessions (ios/Sources/FragmentWebSheet.swift:19). #1036 adds CLI read/multi-file write behavior without changing the server API the phone sends or decodes. No integration regression found. - Earlier fixes remain present: busy-turn Stop and bounded read-back (
ios/Sources/ChatSession.swift:155); fresh redemption and web-view identity (ios/Sources/FragmentWebSheet.swift:19,:79); same-origin signed export download (ios/Sources/AppSettingsAPI.swift:173); computerphase(ios/Sources/SettingsView.swift:112); refused-message recipients (ios/Sources/ChatSession.swift:73); agent-owned end-before-start evidence and retention (ios/Sources/Models.swift:152); serialized export reads/starts, busy lifetime, and deletion/discard of obsolete downloaded archives (ios/Sources/AppSettingsAPI.swift:234). Inspected their corresponding regression tests, including the large work-history response, held GET/queued POST, and late A-download after B-refresh cases.
Validation: flock /tmp/fragment-check.lock env TMPDIR=/tmp cargo xtask check completed successfully (exit 0), including source guards, JavaScript checks, schema lock, host tests and host/Wasm Clippy. The initial restricted-network attempt failed fetching pinned Node; the network-enabled rerun passed. git diff --check 57bc0ba4 HEAD passed. Swift regression tests were source-inspected, not rerun; the reported 48 native tests and 2 UI tests on Mac are supplied evidence. No simulator, hosted services or e2e lanes were exercised in this review.
alexlwn123
left a comment
There was a problem hiding this comment.
Pass for this merge delta — reviewed 59b9e402b7c67a9c89820f13e7f9d81068a865a8 against 57bc0ba43c28a26271420da4bc243967a486b756. No new actionable findings.
The recomputed merge tree exactly matches HEAD, and all 18 changed files match incoming master afa58362. The iOS and xtask sources are unchanged, preserving the prior export, turn-recovery, recipient and web-sheet fixes. Source tracing found no native integration regression: browser early-socket adoption leaves the native live protocol unchanged; web sheets redeem scoped cookies before page scripts run; platform-shell origin discovery and the CLI changes leave the phone's API contracts intact.
Executed: cargo test --locked --offline -p fragment-templates passed 19 unit tests and 3 page-import tests; git diff --check passed. GitHub's main check passed. Its green e2e summary has skipped build/shard jobs; Docker lanes were skipped.
Coverage limits: no Swift/Xcode/simulator is available here, so native tests, app interactions, screenshots and mobile overflow were not executed. Browser/workerd/container integration and live services were not exercised locally; the executed tests are host-side tests. The selected runtime reports a different configuration from the requested saved review environment; repository remote and exact checkout were independently verified.
The native iOS client from #1026 (split from that draft, which stays as it is), on master, with its local fixture server cut.
What it is.
ios/is a SwiftUI app for iOS 26, organized like the web shell: the launcher, chats (streaming, Stop, same-id retry, history), History, Your Apps in web sheets, and Settings. The phone pairs through the existing/cliapproval with its own secp256k1 key, kept in Keychain, and signs every request with NIP-98. Private apps and brains open throughPOST /api/f/{name}/session, the platform route in its own PR.docs/ios.mdis the contract andios/README.mdthe how-to.Changes from #1026:
ios/dev-server(a second implementation of the wire) is gone, and with it the unsigned preview mode (--server, Try the local preview, Settings → Local development). Every API call signs. Debug keeps--pair-serverforcargo xtask dev.ios/Tests/Stub.swift): aURLProtocolfor HTTP, plus a smallLiveSocketseam, becauseURLProtocolnever sees WebSocket tasks. The fixture's deliberate faults are now tests:docs/ios.mdnotes the add-onlyoffersfield to use if that changes.evaluateJavaScriptandcallAsyncJavaScriptare refused like DevTools' evaluation (xtask/src/eval_sinks.rs).cargo xtask devcome in the next PR.Checks
cargo xtask checkpasses on the head merged with masterbcc50fcd8.ios/.🤖 Generated with Claude Code