# SyncGames protocol (Python ↔ Android) Schema version: **1** Both clients MUST implement these rules identically. Diverging behavior is a LOPE risk. ## Object key layout Bucket: configured (default `syncgames`). | Key | Purpose | |-----|---------| | `games//meta.json` | Lease, checksums, retention | | `games//live/` | Current SSOT save file(s) | | `games//history///` | Immutable snapshot | | `retired/-/…` | Soft-deleted games | - ``: lowercase slug, `[a-z0-9-]+` - ``: stable per install (e.g. `pc-desk`, `phone-pixel`) - ``: UTC `YYYYMMDDTHHMMSSZ` - ``: relative path using `/`, no `..` segments ## meta.json ```json { "schema": 1, "game_id": "elden-ring-seamless", "live_hash": "sha256:…", "file_checksums": { "ER0000.co2": "sha256:…" }, "lease": { "holder": "pc-desk", "expires_at": "2026-07-14T03:00:00Z", "session_id": "uuid" }, "versions_to_keep": 5, "updated_at": "2026-07-13T22:00:00Z", "updated_by": "pc-desk" } ``` - `live_hash`: SHA-256 of the sorted concatenation of `path\\0hexdigest\\n` for every live object (canonical tree hash). - `lease` may be `null` when idle. - Clients MUST treat unknown JSON fields as forward-compatible (ignore). ## Lease rules 1. **Acquire** before mutating native installs from SSOT (start session). 2. Acquire succeeds if `lease` is null/expired OR `holder` equals this device. 3. Default TTL: **6 hours** (refreshable by same holder via start again). 4. Another device MUST refuse start while lease is valid for a different holder. 5. **Release** on successful end session (set `lease` to null). Crash: wait for expiry or operator clears via `doctor --break-lease` (destructive admin). ## start_session 1. Load `meta.json` (if missing, treat as empty live + null lease). 2. Acquire lease (conditional overwrite: read-modify-write; if lost race, abort). 3. Download all `games//live/*` objects. 4. Install into configured native path(s) (create parents; overwrite existing). 5. Persist local session state: `{ parent_live_hash, session_id, started_at }`. ## end_session 1. Require local session state for game. 2. Refuse if configured process/AppID still running (Linux); Android warns if user confirms force. 3. Compute WIP tree hash from native files. 4. **Hash gate:** if `parent_live_hash != meta.live_hash` and not `force`, **abort** (stale WIP). 5. Upload snapshot to `history///…`. 6. Upload/replace all `live/…` objects to match WIP (delete remote live keys no longer present). 7. Update `meta.json`: new hashes, `lease=null`, `updated_by=device`. 8. Prune: list `history//`; keep newest `versions_to_keep`; delete older prefixes. 9. Clear local session state. ## restore 1. User selects `device/ts` history prefix explicitly. 2. Acquire lease (or require idle + force). 3. Copy history objects → `live/`. 4. Update `meta.json` hashes; leave lease held by restoring device until they end or release. 5. Never auto-restore. ## Checksum algorithm - Per file: SHA-256 of raw bytes, encoded `sha256:`. - Tree hash: sort relative paths lexicographically (UTF-8), for each path append `path + "\\0" + hex_digest + "\\n"`, then SHA-256 that byte string, encoded `sha256:`. ## S3 addressing - Path-style: `https://syncgames-s3.example.com/syncgames/games/...` - Region can be `us-east-1` dummy; path-style + custom endpoint required. - TLS required on WAN. ## Error codes (CLI / UI mapping) | Condition | Message key | |-----------|-------------| | Lease held by other | `lease_held` | | Hash gate fail | `stale_wip` | | Network / S3 | `store_error` | | Game still running | `game_running` | | Missing config | `config_error` |