Repository navigation
Inbox mobile: the Workspaces source in the iPhone app (M7) - #1797
Merged
Merged
Conversation
Sign in to Workspaces in the system browser sheet on the sign-in door's phone arm, keep the normal session in the app's own cookie store, and draw Workspaces' comment threads with the same list, thread, cards and reply bar as the local Inbox: a second SourceClient beside the device door's. The title menu switches between sources with each one's waiting count; picks and Send go through the answers door with Idempotency-Key, the decision tick rides the Send, live updates come by ws ticket, and sign-out ends the session at Workspaces. Behind WORKSPACES_ORIGIN (Debug and TestFlight on, the App Store build off). Proof: apps/ios/scripts/workspaces-proof.ts against staging with a test account (local only).
…M7, M8 finding) Plannotator/Workspaces.entitlements claims applinks: and webcredentials: for $(WORKSPACES_HOST), so a signed build on a phone receives the https return (ASWebAuthenticationSession needs webcredentials for an https callback). WORKSPACES_HOST is now the one switch: Debug and a new TestFlight configuration name staging and carry the entitlement; Release, the App Store build, leaves it empty, with no Workspaces source and no entitlement. WORKSPACES_ORIGIN derives from it. Production is WORKSPACES_HOST=workspaces.plannotator.ai.
… review item 1) The key hashes the comment, question, revision and answer, so a retry of the same pick carries the same key and W2 replays its first answer; a pick whose answer was lost is retried once. The staging proof sends the phone's pick again with the app's session and derived key (200, the first result) and under another key (409).
…S_HOST and the TestFlight configuration
backnotprop
force-pushed
the
feat/mobile-m7-workspaces
branch
from
October 8, 2026 17:39
b693d9a to
56d35d6
Compare
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mobile plan step M7: the Workspaces source in the iPhone app
The person signs in to Workspaces from the app through the real AuthKit page in the system browser sheet, and the app keeps a normal
humanSessionin its own cookie store. Workspaces then sits beside the computer's Inbox as a second source, one at a time, chosen from the title menu with each source's waiting count, and its comment threads are drawn with the same list, thread, question cards and reply bar. The phone picks, ticks a decision and sends through the answers door, the list updates live by ws ticket, and sign-out ends the session at Workspaces.Records:
PLAN.mdstep M7, briefbriefs/M7-workspaces.md, the mobile decisions (7 to 10, 13), the iPhone record (1.1, 1.4, 1.5A, 9.1), the W0 transcripts and the W1 and W2 reviews. The contract coded against is Workspaces' live OpenAPI (https://staging.workspaces.plannotator.ai/v1/openapi.yaml);adr/implementation/inbox-mobile.mdis unchanged (it leaves the Workspaces source to W0 to W2).What changed
apps/ios/PlannotatorKit/Sources/PlannotatorKit/SourceClient.swift(new): the one protocol a shown source fills.InboxClientconforms as it is. M1 had no protocol; this is it.WorkspacesClient.swift(new): the doors the phone uses. The sign-in door's phone arm (/auth/desktop/login,POST /auth/desktop/sessionwith the PKCE verifier,POST /auth/desktop/logout),GET /v1/me, the device doorPUT /v1/me/devices/{deviceId},POST /v1/ws-ticketand the change-channel socket, and a document's text. Cookies live in the app's own store (not Safari's; the sheet is ephemeral). Every response may rotate them, and a mutation echoes thecsrfcookie asX-CSRF-Token. A 401 on a session call issignedOut.WorkspacesSource.swift(new): Workspaces mapped onto the Inbox's wire models. A notification row withthread_keyannotation:<id>is a list row, placed in the Inbox's six sections. The thread isgetDocumentAnnotationwith its replies and questions. A pick goes throughPOST .../answerswith oneIdempotency-Keyper pick: the comment, question, revision and answer, hashed (pickKey). A retry of the same pick carries the same key, so W2 replays its first answer, and a pick whose answer is lost is retried once. Each new pick is on a new revision, so it gets a new key. Send isfinal: truewith the words and the decision ticks; ticks are kept on the phone until the Send, because Workspaces stores none before. Opening a thread marks its rows seen and opened. Resolve isPATCHstate. Live changes come from the personal channel plus each team's channel: a row on a team workspace is announced on the team channel (W0 2.15). A thread's id is<workspace>/<document>/<comment>, so each call addresses the comment from the id alone.InboxClient.swift:InboxErrorgainsworkspacesUnreachableandsignedOut, plusisUnreachable.Models.swift:InboxThreadgains an optionaldocument(Workspaces only; the Inbox never sends it).App/WorkspacesAccount.swift(new) holds the account, the sign-in flow (ASWebAuthenticationSession), the simulator's loopback return and the push-token delegate.AppModeladds sign-in, sign-out, the switcher's counts and push registration.SourceSessiontakes either source (id,name,kind,any SourceClient).App/SourceWords.swift(new) gives each source its own words for the empty, unreachable, removed and gone states.Inbox/InboxTab.swift: 1.1's Workspaces tile, and 1.5A's switcher (a picker of sources, each with its subtitle, mark and checkmark, then Add a source). Add a source offers the computer's Inbox or Workspaces.Settings/SettingsTab.swift: 9.1's Workspaces row.Settings/WorkspacesScreen.swift(new): the account, its teams and Sign Out.Thread/DocumentRow.swift(new): the comment's document and quote, opening its text natively.InboxList,ThreadScreenandReplyBaruse the source's own words. There is no Delete for Workspaces. A teammate's reply shows their name.project.yml,Info.plist,Plannotator/Workspaces.entitlements: theWORKSPACES_HOSTbuild setting, the TestFlight configuration and the associated domain (see below).Assets.xcassets/WorkspacesIconis the Workspaces app icon, the same image as staging's/apple-touch-icon.png.PlannotatorUITests/WorkspacesProofTests.swiftandscripts/workspaces-proof.ts(new). README and CLAUDE.md entries updated.The build setting (owner item 9: the first App Store release is local only)
WORKSPACES_HOSTdecides whether a build has a Workspaces source at all. There is no runtime switch.staging.workspaces.plannotator.ai. They carryPlannotator/Workspaces.entitlements:applinks:andwebcredentials:for that host, so a signed build on a phone receives thehttpssign-in return. This was added after the M8 lane's finding.WORKSPACES_ORIGIN(Info.plistWorkspacesOrigin) derives from the host.xcodebuild archive -configuration TestFlight. Production isWORKSPACES_HOST=workspaces.plannotator.ai, which gives both the origin and the entitlement.Record sections made real
built/1.1-*.pngbesiderecord/1.1-*.png).Proofs and how to run them
WorkspacesProofTests, run by the harness):mobile-m7-<pid>(made and deleted by the harness): 1 test, 358 s. In order, it covers:plannotator://signin?state=<not ours>&ticket=forged) is dropped: no alert, no sign-in.workspaces.create_annotation) asks two questions in a comment that tags the person. The row lands live by ticket, with no pull.409 question_revision_conflict.workspaces.list_annotations(asked_by: me): "Go ahead." and "Answer: Yes", both questionssent, the first with adecision_id. The project's decisions list it.research/operational/mobile-proofs/M7-2026-10-08/(built/besiderecord/1.1, 1.4, 1.5A, 9.1;M7-sign-in-pick-send.mp4;proof-run.log, which holds no secret).user-001of the stress harness'sstress-b), signed in for the lane by the Workspaces repo's ownscripts/stress/lib/login.mjs. The agent's key was minted from that person for the lane (mobile-m7 proof agent) and revoked after; never the owner's. Each run makes its own workspace and deletes it.bun apps/ios/scripts/proof.ts --binary .local/plannotatorpassed (the M1 flow, 250 s).WorkspacesProofTestsskips itself there.swift testinapps/ios/PlannotatorKit, 12 of 12.CI: local only. CI has no Workspaces secrets, and plannotator-ops and the owner have not agreed to add them. The CI iPhone job runs the Inbox proof, and the Workspaces test skips there.
Still needs a signed build or the device:
httpsreturn through the associated domain. The entitlement is in the app; it waits on the Team ID (owner item 1) for signing and for staging's association file. The simulator build returns through the loopback arm instead (deviation 2).Gates run
bun run typecheck: clean.bun run check:release-version: consistent.xcodebuild build-for-testingfor the iOS Simulator: succeeded.swift test: passed.gitleaks git . --config .gitleaks.toml --log-opts="origin/main..HEAD": no leaks.5d39c9812(M8 merged). The conflicts were CLAUDE.md, the README and the generated project; the project was regenerated. M8'sRELEASE.mdandAPP-REVIEW.mdnow followWORKSPACES_HOST: the TestFlight archive is-configuration TestFlight, with an entitlement check. The Swift is the code the staging proof and the M1 flow ran on (both green after the review items).bun testwas not run, because no TypeScript package changed.gen-colors --checkis untouched.What it leaves out
From the review (follow-ups, not in this PR):
Also left out:
DELETE /v1/meis planned, not live; App Review 5.1.1(v), M8).Deviations from the record, the brief or the contract, with the reason
.toolbarTitleDisplayMode(.inline)keeps "Inbox ⌄" and the source under it always reachable. A customlargeTitletoolbar item was tried; it rendered centred and was invisible to VoiceOver.httpsmobile shape and the desktop loopback shape. A simulator build (#if targetEnvironment(simulator)) therefore uses the loopback shape. A one-request listener on 127.0.0.1, for this sign-in's nonce path only, answers with a redirect to the sheet'splannotator://signin, so the sheet completes as the spike proved. A device build uses thehttpscallback. Both arms redeem only a return carrying the sign-in's ownstate. A return arriving from outside (onOpenURL, a universal link) finishes only the sign-in in progress with that state, and is otherwise dropped (proved).POST .../replies): the answers door needs at least one question. That door takes noIdempotency-Key, so a lost answer there is settled the way M1 settles one, by re-reading the thread.actors); its newest event is the person's own Send.PUT /v1/me/devices/{dev_ULID}runs when iOS hands a token after notifications are allowed (sandbox in Debug, production otherwise). It is not proved in the simulator: there is noaps-environmententitlement without the Team ID, and the ask is M5's switch.Findings outside the app
POST /auth/desktop/logoutanswers 204 and revokes the WorkOS session, but the sealed cookie keeps answering 200 until its access token expires. That took 307 s in a curl check and 227 to 229 s in the proof, because the session check verifies the token without calling WorkOS (by design). The app forgets the cookie at once, so the phone is signed out. A copied cookie would work for up to about five minutes. This is for the W1 owner and the auth board, not this PR.Overlap with lanes in flight
ThreadScreen.swift,ReplyBar.swiftandproject.yml. TheirProofSupport.swifthelpers are duplicated privately inWorkspacesProofTests(about 30 lines) until one lands.SourceSession.swift(heresourcebecomesid,nameandkind),InboxTab.swift,InboxList.swift, and its Decisions tab, which the Workspaces source should plug into.WorkspacesAccount.swiftholds a minimalAppDelegate; M5's will absorb it), and 9.1's Notifications block.Limits
None. The sign-in door's existing 1800 s handshake and 120 s ticket windows are the server's own.