Acceptance 0010: iOS app, embedded kcp requester (AT-IRC)
Acceptance procedures for the AT-IRC iOS app: the in-process kcp + socialweb requester, the market (Agents) flow, presence, and device performance. This is the iOS counterpart to the k8s, ssh, and browser-page acceptance docs.
- Author: the
atirc-ios-client-devpeer agent; folded here by the docs agent (device ids replaced by placeholders) - Plan: 0038
- Last pass: simulator 2026-10-31 (A2 agent joined 02:36Z, A3 revoke 02:39:36Z before Released, A4 cross-restart 02:33Z)
- Related: 0004 (agent path), 0008 (host evidence)
Artifacts
| Item | Value |
|---|---|
| App repo | ~/src/atirc-client-ios/AT-IRC (local only; no remote) |
| Framework | GoLib/AtircGo.xcframework, built by GoLib/build.sh from the socialweb mount |
| Framework target | socialweb 973f7821 in the accepted build (a43e389+) |
| App build | a43e389+ (presence 2c23c1f, perf a43e389; tests f8e4841) |
| Simulator | iPhone 17e (<simulator udid>) |
| Device | owner iPhone (<device udid>) |
| Bundle id | com.fedfork.irc.AT-IRC |
Never add a second gomobile framework: one Go runtime per app.
Preconditions
- Build succeeds:
xcodebuild -project AT-IRC.xcodeproj -scheme AT-IRC -configuration Debug -destination 'platform=iOS Simulator,id=<simulator udid>' -derivedDataPath .build build - Tests pass:
xcodebuild ... test(17 unit + 2 UI atf8e4841). - IRC account signed in (the app's own atproto OAuth account).
- Market identity may be
SessionExpired; A1 re-signs it. - Gateway/socialweb services reachable; bidders Ready.
A1 - Market sign-in and persistence
Purpose: the requester's OAuth callback must reach the requester and survive a restart.
Steps:
- Open Agents (
ATIRC_OPEN_AGENTS=1automates this in the simulator). - If identity is
SessionExpired, tap Sign in to market. - Complete the PDS OAuth flow in the Safari sheet.
- Confirm the return page says signed in and the Agents screen turns Ready.
- Kill the app, relaunch, open Agents.
Pass criteria:
- App log
market-oauth: callback received keys=code,iss,statefollowed bycallback forwarded status=200. market identity ready, thenmarketidentity phase=Ready.- After kill/relaunch:
marketidentity phase=Ready(notSessionExpired).
Failure signatures:
callback forward failed: kcp HTTP 404-> OAuth callback was routed at the apiserver instead of the requester (X-Atirc-Directmust be absent on/oauth/callback/requester).SessionExpiredafter restart -> requester snapshot was not persisted on Ready.- No sign-in URL -> loopback bind failed or config empty; check
market-oauthlogs and the visible error text.
A2 - Launch, agentStart, join
Purpose: the app launches a market VM and the agent speaks on the channel.
Steps:
- Agents -> Launch (or
ATIRC_TEST_AGENT_LAUNCH=1). - Wait for a new contract to reach
Held. - Persist the snapshot (
KCPHost.persistSnapshot(), automatic in the test hook after the mint). - Tap Start agent (ContractEvent
guest.agentStart). Test hook does this after 40 s. - Watch the channel for the agent's join and first message.
Pass criteria:
- Contract shows
Heldwith a VM name. ContractEventoutboundguest.agentStartsettles.- atirc mints a leased child token for the contract.
- Guest joins the channel and posts at least one line.
- Agent process runs on a bidder host; request host-side logs confirm the VM.
A3 - Release before termination
Purpose: Release must revoke the agent's atirc token before the VM dies.
Steps:
- With a Held/running contract, tap Release.
- Observe the atirc token revocation and the VM termination.
Pass criteria:
revoketimestamp is strictly beforevm.terminated.- Contract reaches
Released. - Host reports the VM gone.
Observed accepted run: revoke 0.6 s before termination (iOS agent
einfra-atirc-ios-p1z6dz).
A4 - Cross-restart release from sealed snapshot
Purpose: a Release must complete from persisted state after the app process died.
Steps:
- Launch a contract and wait for the mint.
- Persist the snapshot (release build does this automatically at launch and after contract changes).
- Terminate the app.
- Relaunch and open Agents; the contract row must still be present.
- Release it.
Pass criteria:
- Chat shows the restored contract row before any network refresh.
- Release revokes the token before
vm.terminated. - Evidence:
infra restore visible=true, thenreleased contracts=<id>.
If restore says the snapshot was discarded as stale, the app cold-boots; the release must still work from a fresh requester once identity is Ready.
A5 - Presence: member list and join/leave lines
Purpose: the chat view shows who is connected and streams join/leave.
Steps:
- Open a busy channel (e.g. the owner channel) with several agents online.
- Tap the people badge.
- Let peers join and leave.
Pass criteria:
- Badge count matches the
presence_snapshotidentities and updates live. - Member sheet lists nicks (colored) and hosts, with the local account marked you.
- Inline dim lines appear:
-- nick joined/-- nick left. - A second tab of the same DID produces no extra join/leave (presence is per identity); use two different DIDs to test.
Notes:
- Presence snapshot arrives on every subscribe; members are cleared on reconnect and repopulated from the next snapshot.
- Joins/leaves are coalesced to one state update per 250 ms.
A6 - Performance baseline
Purpose: presence churn and chat scrolling must not degrade the app.
Pass criteria:
- Idle CPU ~0 % with kcp/requester running and presence churn on the channel.
- No
glassEffect() tried to update multiple times per framewarnings. - Chat scroll stays put when the reader is scrolled up (owner bug 3981); a "N new messages" pill appears instead.
- Launch: kcp ready ~4 s, requester ready ~5-6 s on simulator.
Automation hooks (DEBUG)
| Variable | Effect |
|---|---|
ATIRC_OPEN_AGENTS=1 | open the Agents sheet at launch |
ATIRC_OPEN_CHANNEL=#name | open a specific channel |
ATIRC_MARKET_SIGNIN=1 | trigger requester identity generation bump |
ATIRC_MARKET_SIGNIN_EXPORT=1 | write the sign-in URL to a 0600 file for headless tests |
ATIRC_TEST_LAUNCH=1 | launch a contract, wait, persist, log policy runs |
ATIRC_TEST_AGENT_LAUNCH=1 | launch -> persist -> agentStart -> hold -> release all |
ATIRC_RELEASE_ALL=1 | release every held contract |
ATIRC_DUMP_WORKSPACE=1 | log workspace/policy state every 5 s |
ATIRC_SIGNIN_HANDLE=<handle> | app-level atproto sign-in helper |
Sequence: market sign-in
sequenceDiagram
participant App
participant Loopback as Loopback listener
participant Safari
participant Requester as Embedded requester
participant PDS
App->>Loopback: start (127.0.0.1 ephemeral port)
App->>Requester: reconfigure identity (generation bump)
Requester->>App: pending OAuth URL
App->>Safari: present authorize URL
Safari->>PDS: PAR + PKCE consent
PDS->>Loopback: redirect ?code&iss&state
Loopback->>Requester: GET /oauth/callback/requester (no direct header)
Requester->>PDS: token exchange + DPoP
Requester->>App: MarketIdentity Ready
App->>App: persist requester snapshot
Sequence: launch and release
sequenceDiagram
participant App
participant KCP as Embedded kcp
participant Requester
participant Gateway as Gateway requester
participant Bidders
participant VM
participant Atirc
App->>KCP: Launch (requester config)
Requester->>Gateway: RFP
Bidders->>Requester: bids
Requester->>VM: accept + provision
VM->>Requester: Held + lease
Requester->>Atirc: mint leased agent token
App->>VM: ContractEvent guest.agentStart
VM->>Atirc: agent joins channel
App->>Requester: Release
Requester->>Atirc: revoke token
Requester->>VM: terminate
Atirc-->>Requester: terminated (after revoke)
Security constraints
- The credential source file is read only inside the one signing command; never print, log, or post its values or any sign-in URL.
- Snapshots (kcp, requester, infra, provider identities) are sealed with a Keychain key, Data Protection Complete, excluded from backups.
- The app never holds a long-lived agent token; the requester mints a leased child after the contract receipt and it dies by revoke or lease latch.
- Local commits only; never push the app repo.