Initial SyncGames tree: agent, Android, deploy, docs.

Session-gated MinIO save sync with AppImage GUI, CLI edit/session flow, and Gitea release helper.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
2026-07-14 22:06:36 -05:00
co-authored by Cursor
commit 0d6b0b2f80
76 changed files with 5697 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
# 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/<game-id>/meta.json` | Lease, checksums, retention |
| `games/<game-id>/live/<rel>` | Current SSOT save file(s) |
| `games/<game-id>/history/<device-id>/<iso-ts>/<rel>` | Immutable snapshot |
| `retired/<game-id>-<date>/…` | Soft-deleted games |
- `<game-id>`: lowercase slug, `[a-z0-9-]+`
- `<device-id>`: stable per install (e.g. `pc-desk`, `phone-pixel`)
- `<iso-ts>`: UTC `YYYYMMDDTHHMMSSZ`
- `<rel>`: 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/<id>/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/<device>/<ts>/…`.
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/<device>/`; 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:<hex>`.
- 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:<hex>`.
## 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` |