Establish watch relay contracts and platform package wiring

This commit is contained in:
2026-09-18 09:34:26 +04:00
parent a92698e1e6
commit 9b87299bf5
22 changed files with 778 additions and 159 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ Apple TN3135 restricts ordinary watch apps' low-level networking, including URLS
| Packages/WristivoKit/SamsungProtocol | Key mapping and frame/URL encoding | Pure encoding, no socket |
| Packages/WristivoKit/GestureCore | Normalized directional excursion state machine | Algorithm seam, no raw sensor classifier |
| Adapters/TVTransport | iPhone URLSession WebSocket state machine | Planned |
| Adapters/WatchBridge | WCSession lifecycle and live delivery | Planned |
| Adapters/WatchBridge | WCSession lifecycle and live delivery | Source/test package and injection seams ready; live WCSession implementation pending |
| Adapters/MotionCapture | Core Motion, coordinate calibration, normalization | Planned |
| Adapters/SecureStorage | Keychain token and trust persistence | Planned |
+9 -3
View File
@@ -1,6 +1,6 @@
# Command contract v1
Source of truth: `Packages/WristivoKit/Sources/RemoteCore`. The integration owner maintains these types. This document defines production adapter behavior in addition to the foundation types; the scaffold does not yet implement the relay/session handshake.
Source of truth: `Packages/WristivoKit/Sources/RemoteCore`, including `WatchSession.swift`. The integration owner maintains these types. The v1 control-plane DTOs, codec and actor protocol are now defined. WCSession delivery and the production phone actor are still to be implemented; protocol definitions are not a functioning handshake.
## Commands and envelope
@@ -8,7 +8,9 @@ Source of truth: `Packages/WristivoKit/Sources/RemoteCore`. The integration owne
`CommandEnvelope`: `schemaVersion` (1), `id` (UUID), `sessionID` (UUID), `televisionID` (UUID), `sequence` (UInt64), `issuedAt` (Date), `expiresAt` (Date), `command`. UUIDs identify local opaque records, not TV serial numbers or network addresses. Date encoding on the future bridge is explicitly JSON milliseconds since Unix epoch; both encoders/decoders must set the same strategy. Package round-trip tests alone are not sufficient to validate that adapter.
A session is issued by the phone for one selected paired TV, bound to the currently activated paired WCSession. The watch cannot select an arbitrary target by manufacturing an envelope. Proposed control-plane operations: `beginSession` returns v1, sessionID, televisionID and expiry policy; `endSession` invalidates that exact session; a new phone process, TV switch, re-pair, bridge deactivation or app stop invalidates old authorization. Add typed control-plane DTOs under integration ownership before transport implementation. Starting a session must never issue a TV key.
A session is issued by the phone for one selected paired TV, bound to the currently activated paired WCSession. The watch cannot select an arbitrary target by manufacturing an envelope. `BeginRemoteSessionRequest` returns `RemoteSessionGrant` with v1, sessionID, televisionID, issuedAt and maximumCommandLifetimeMilliseconds (1...1000); `EndRemoteSessionRequest` ends that exact session and returns `RemoteSessionEnded`. Errors use `RelayFailure`, which conforms to Error for Result return values. A new phone process, TV switch, re-pair, bridge deactivation or app stop invalidates old authorization. Starting a session must never issue a TV key.
Use `RelayJSON.encode(_:)` and `RelayJSON.decode(_:from:)` for all bridge payloads. They enforce a 4096-byte payload limit and the millisecond date strategy. Control DTO invariants are checked on encode and decode; direct Codable use bypasses that validation. The phone actor must still validate control requests from local callers. Command decoding performs no semantic admission and consumes no duplicate/sequence state. A grant is not a command; its issue date does not extend command lifetime. Apply the existing one-second command deadline independently to each command.
Sequence starts at 0 or 1 and strictly increases within a session; all senders in a given session must share one counter. At UInt64 exhaustion start a new session rather than wrapping. The receiver rejects wrong target/session/version, invalid/non-finite times, future timestamps beyond 250 ms, messages over one second old, expiry in the past, expiry preceding issue, or a declared lifetime longer than one second. No arbitrary clock-skew widening: report need to resync/start a new session. Test actual paired-device skew. Adapter also maintains a receiver-local monotonic deadline so wall-clock changes cannot extend pending work.
@@ -18,12 +20,16 @@ Validate decoded values at every trust boundary; Codable construction alone does
`CommandReceipt` correlates the envelope ID. Status is sentToTransport, rejected, expired, duplicate or unavailable. A successful send only means the adapter completed the socket write; Samsung may provide no per-key execution acknowledgement. Never display "TV confirmed" based only on that receipt. Timeout after possible delivery is uncertain; ask the user to issue a new action if needed rather than replaying.
Planned bridge container: `["schemaVersion": 1, "kind": "command", "payload": Data]` with bounded JSON payload (4 KiB maximum), delivered using sendMessage/replyHandler. Validate container types and payload size before decoding. Reply uses the same versioned container with kind `receipt`. Exactly one reply per incoming request, including errors. No passwords/tokens/raw motion in these messages. Test Codable round trips, invalid payloads, schema mismatches and restart cases.
Frozen bridge container: exactly `["schemaVersion": 1, "kind": String, "payload": Data]`, with bounded JSON payload (4 KiB maximum), delivered using sendMessage/replyHandler. `RelayMessageKind` freezes beginSession/session, endSession/ended, command/receipt, and error. Validate exact container keys/types and payload size before decoding. Boolean NSNumber is not an integer version; reject it explicitly. Unknown kinds, extra/missing container keys, inappropriate request/reply direction, unsupported container versions and malformed payloads fail closed with an error reply. DTO fields may evolve only under the compatibility policy below. Exactly one reply per incoming request, including errors. The request's WCSession reply closure supplies correlation for control requests; command receipts additionally match CommandEnvelope.id. Never execute a reply payload as a request. No passwords/tokens/raw motion in these messages.
## Adapter interfaces
Existing `RemoteTransport.send(_ envelope: CommandEnvelope) async -> CommandReceipt` is the command sink. The sender has no implicit retry guarantee. Platform agents propose small lifecycle protocols for connection, session acquisition, token repository and motion source through the integrator; do not hide setup inside `send` or start pairing in response to a gesture.
`PhoneCommandCoordinating: Actor` is now the injected phone seam. A/integration implements it on the same actor that owns CommandAdmission and TV dispatch, alongside RemoteTransport if useful. B does not implement a second authoritative actor. Its methods are `watchBridgeDidActivate(peer:)`, `beginWatchSession(_:peer:)`, `endWatchSession(_:peer:)`, `submitWatchCommand(_:peer:)` and `watchBridgeDidDeactivate(peer:)`. A fake conforming actor is sufficient for B's independent implementation/tests; the live A actor is required only for end-to-end integration.
The local `WatchPeerGeneration` is a fresh UUID per phone-side bridge activation. It is not Codable and must not cross the wire, persist, identify watch hardware, or come from the remote message. B serializes lifecycle delivery, awaits activation before forwarding requests, and passes the captured generation on every call. The phone actor rejects requests from non-current peers and ignores delayed deactivation for old peers. Repeated activation of the same peer is idempotent; activating a new peer invalidates previous grants. Repeated begin requests create fresh sessions and invalidate the previous session; B must serialize starts and ignore delayed replies after a lifecycle or local request-generation change. Stale end requests return sessionInvalidated and never end a newer session. After every suspension, the actor rechecks generation/session/target/deadline before sending. A protocol cannot enforce these semantics; A's tests must prove them.
GestureCore receives normalized inputs from the future MotionCapture adapter. Its thresholds are algorithm fixtures, not physically calibrated degrees or validated human-motion confidence. Sensor coordinate transforms and physical traces are required before claiming wrist-swipe recognition. Watch and phone UI depend on domain contracts, never Samsung frame strings.
## Compatibility change policy
+2
View File
@@ -21,6 +21,8 @@ The coordinating engineer is the integration owner. Agents implement focused bra
Package paths in the table mean `Packages/WristivoKit/Sources/<module>` and `Tests/<module>Tests`. A new adapter needs tests in an adjacent Tests directory; integrator creates target membership before the implementation branch starts. QA may run acceptance planning immediately, but must not edit another agent's tests or report planned cases as passed.
**B prerequisites are now established.** Follow [the Watch relay handoff](WATCH-RELAY-HANDOFF.md): frozen session DTOs/codec/actor protocol live in RemoteCore/WatchSession.swift; the adjacent WatchBridge package provides three source/test targets and its platform products are linked to the apps. B works in a separate worktree, uses a fake actor until A is integrated, and does not need to take over the integration role. Adapter Package.swift files, dependency changes and Xcode project wiring remain integrator-owned; new Swift source/test files within existing target paths are B-owned and automatically included.
## A — TV transport prompt
> Implement an iPhone-only TV connection actor and pure bounded event parser. Use SamsungRemoteEncoder without copying a community implementation. Support explicit connect/pair/deny/timeout/revoke/disconnect, authorize before ready, serialize commands, consume admission once, recheck deadlines before sends, and cancel old-generation operations on TV/session changes. Integrate injected token/trust storage from E. No blanket TLS trust, arbitrary key API, silent plaintext fallback or replay on reconnect. Use synthetic redacted event fixtures for malformed/oversize/unknown/missing-token responses and fake socket tests for deadline, ordering and cancellation. Deliver a manual physical pairing script showing the name Wristivo, reconnect and revoke. Do not mark the TV compatible until the owner runs it. Begin with a hardware feasibility spike for the candidate endpoint and TLS policy before expanding UI dependencies.
+28
View File
@@ -0,0 +1,28 @@
# Watch relay — integration prerequisites ready
Scope: shared seams and compile/test membership. This is not a completed relay or a hardware verification.
## Instructions for B
Use the dedicated `feat/watch-relay` worktree prepared by the integrating engineer. Do not switch the original TV-transport checkout or edit shared contracts/project files. The integration commit in this worktree supplies all prerequisites below. A branch name alone does not isolate work: separate working directories are required when multiple agents run.
1. Read RemoteCore/WatchSession.swift and docs/CONTRACTS.md for exact types and semantics. The historical proposal's method signatures have been superseded.
2. Implement only Adapters/WatchBridge sources/tests. All three target memberships already exist and discover new Swift files automatically. The package manifest and Xcode generator remain integration-owned; ask only for an actual dependency/target change, not ordinary new source files.
3. Use PhoneCommandCoordinating through a fake actor while A implements production conformance. B owns WCSession activation, facades, live delivery, bounded containers, routing, timeout and reply-once handling. A/integration owns the sole authorized session/admission/TV-dispatch actor.
4. Use RelayJSON for all payloads; no default JSON date coders. Control DTO validation does not replace command admission. Match command receipt IDs and expected reply kinds. A timeout or an old-generation reply never retries an action or re-arms input.
5. Establish and capture a fresh local WatchPeerGeneration for each phone bridge activation. Deliver lifecycle changes in order and await activation before forwarding requests. Pass that peer on every actor call. Never accept a peer ID from wire data or put it in application context.
6. Keep OS APIs behind injectable facades and platform guards. Test fake behavior on macOS without subclassing WCSession; compile real delegates on each native SDK. Do not start WCSession or change the demo app behavior at module import.
## Required B tests
Exactly-once completion on success/error/timeout races; stale callback after deactivate/reactivate; ignored delayed begin grant/end acknowledgement; unreachable and unactivated session; wrong kind/version/types/extra keys; Bool-as-version rejection; 4096/4097-byte boundary; malformed/unknown payload; receipt ID mismatch; bounded pending requests; expiry while waiting; no replay after reconnect; application-context allowlist excludes tokens, raw motion and commands. Validate phone/watch counterpart replacement and reinstall recovery with fakes first.
Run `bash Scripts/check.sh`. The integrator also runs both app builds before merge. Hardware testing of locked-phone wake, relaunch, actual reachability and detached-debugger lifecycle remains unperformed and mandatory before feature completion. Do not block fake-driven implementation on access to a TV.
## Responsibilities for A/integration
Implement PhoneCommandCoordinating on the single existing/new TV command actor, validate caller and peer lifecycle, require a ready selected TV for a grant, and make stale end/deactivation harmless. Test actor reentrancy and recheck all state before socket writes. Do not forward an admitted command into another consuming admission gate. The storage/TLS proposal from A is outside this relay-prerequisite patch and remains separate integration work.
## Delivery
Return a branch/commit, focused test evidence, explicit native compilation status, documented errors and remaining hardware gates. Do not push main. The user is handling remote pushes; this prerequisite change does not push anything.