feat: publish read-only iOS music export skill

This commit is contained in:
Rodger Werkhoven 2026-08-10 14:25:24 +02:00
commit 45ade372f6
15 changed files with 2872 additions and 0 deletions

View file

@ -0,0 +1,110 @@
---
name: rodgers-ios-music-export
description: Safely export music from a USB-connected iOS device in a local Codex session on macOS without Finder or Music sync, when an existing pairing is present or pairing status is unknown. Use for reverse-transfer, recovery, exact genre filtering such as Electro versus Electronic, local destination-folder creation, resumable copies, or integrity verification. Proven on iPad mini 2 with iOS 12.5.8; unknown schemas fail closed.
---
# Rodger's iOS Music Export
Export selected, locally stored tracks from one paired iOS device while keeping the device read-only. Use the bundled standard-library Python client instead of Finder or Music sync.
## Safety contract
- Treat the iOS device as read-only. Never invoke Pair, Unpair, sync, upload, delete, rename, database mutation, or AFC write operations.
- Do not click Finder's or Music's `Sync` button. Sync can reconcile the device against the Mac library and remove device content.
- Require exactly one USB-connected iOS device and an existing valid macOS pairing. If pairing is absent, stop and explain that approving Apple's visible Trust prompt creates persistent pairing state on the Mac and device. Obtain explicit permission before that visible action; never create pairing records programmatically.
- Write only to the destination and manifest paths the user authorized. Preserve unexpected or untrusted existing files and stop on collisions.
- Interpret a requested genre as an exact, case-insensitive database match. `Electro` matches `electro`; it does not match `Electronic`, `Elektronisch`, or `Electro House`.
- Verify count, byte total, SHA-256 hashes, and—when `ffprobe` is present—audio streams and embedded genres before claiming completion.
- Never expose the device UDID, pairing record, host certificate, private key, or other credentials in output.
## Workflow
### 1. Establish live state
Confirm that this is local Codex on macOS with direct USB access, the intended device is attached, and no Finder/Music sync is running. Proceed when the device is charging; when it is not charging and below 30%, pause and ask the user to connect power. Inspect Finder or Music visibly for battery and sync state when the read-only probe reports that those values are unavailable. Then run:
```bash
python3 <skill-dir>/scripts/ios_music_export.py probe
```
The probe returns only the device name, model identifiers, iOS version, build, and class. If local sandboxing denies access to `/var/run/usbmuxd`, rerun the same bounded command with the user's normal Codex approval mechanism.
### 2. Resolve the exact genre label
If the requested label is uncertain or adjacent labels could be confused, inventory the device's exact labels and counts:
```bash
python3 <skill-dir>/scripts/ios_music_export.py genres
```
Do not silently broaden the user's genre. Ask only if no exact label exists or their wording genuinely permits multiple labels.
### 3. Build a read-only plan
```bash
python3 <skill-dir>/scripts/ios_music_export.py plan \
--genre "Electro" \
--destination "/Users/name/Downloads/RODGER's MUSIX"
```
Add `--show-files` when filenames or metadata need review. The plan takes two byte-identical database snapshots, runs SQLite integrity checks, consolidates them locally, validates the schema, stats every selected source file, and reports total exact matches, transferable and excluded counts, bytes, free space, and headroom. Stop if snapshots differ repeatedly, the schema is unsupported, a source is missing, or database and device signatures disagree.
Before copying, state the exact genre, destination, track count, byte total, available space, and any known limitations. A direct user request to export is sufficient authorization for destination creation and local copies; do not add an unnecessary confirmation pause.
### 4. Export and verify
```bash
python3 <skill-dir>/scripts/ios_music_export.py export \
--genre "Electro" \
--destination "/Users/name/Downloads/RODGER's MUSIX"
```
By default the audit manifest is a hidden TSV beside—not inside—the music folder. A persistent empty `<manifest>.lock` sidecar serializes exports to that manifest and contains no device or music data. The exporter:
1. obtains two byte-identical database snapshots and consolidates a checked local copy;
2. selects only the exact genre;
3. validates source files and free space;
4. creates conflict-safe `Artist — Title.ext` names;
5. creates each final track with a no-clobber open, so a late local collision is preserved;
6. records source signatures, state transitions, event checksums, and SHA-256 hashes in a locked append-only manifest;
7. verifies the complete destination.
Use `--manifest <path>` only when the user needs a specific audit location. Use `--require-embedded-genre` only when `ffprobe` is available and the user explicitly requires every file's embedded tag to equal the database genre; iOS database metadata remains the selection authority.
### 5. Resume safely
Repeat the identical export command after an interruption. Existing files are accepted only when the latest checksum-bound manifest event binds them to a pseudonymous device fingerprint, database snapshot, source signature, size, and nonempty hash. Completed tracks and a hash-proven final file in `ready_to_commit` state resume safely. A `pending` event never authorizes a final file. If interruption leaves a final file while its latest event is still `pending`, preserve it and stop; ask the user to choose a new destination or explicitly decide how to handle that exact file. Never delete, replace, or rename it implicitly.
If a process interruption tears only the final manifest event, recover under the manifest lock by truncating that incomplete tail only after every preceding event and checksum validates. Never repair, replace, or append to a manifest whose complete events belong to another export plan.
### 6. Verify an existing export offline
```bash
python3 <skill-dir>/scripts/ios_music_export.py verify \
--genre "Electro" \
--destination "/Users/name/Downloads/RODGER's MUSIX" \
--manifest "/Users/name/Downloads/.RODGER's MUSIX.rodgers-ios-music-export.tsv"
```
If `ffprobe` is unavailable, hashes, counts, and sizes still run and the result explicitly marks the audio probe as skipped. Do not install dependencies unless the user approves installation.
## Completion report
Lead with the verified result and include:
- device name and iOS version, without its UDID;
- exact genre predicate used;
- destination and manifest paths;
- manifest lock path;
- total exact matches, transferable and excluded counts, total bytes, and extension counts;
- copied versus resumed counts;
- hash/size result and audio-probe status;
- embedded genre counts when probed;
- explicit confirmation that no sync or media, library, or AFC write occurred; if Apple Trust was used, disclose the pairing-state change separately;
- any DRM, cloud-only, unsupported-schema, pairing, or verification limitation.
Do not claim success from a plan, progress counter, or copy completion alone. Success requires the final local verification result.
## Troubleshooting and compatibility
Read [references/compatibility.md](references/compatibility.md) for the proven device matrix, protocol boundaries, common errors, and limitations before changing the script or working around a failure.

View file

@ -0,0 +1,7 @@
interface:
display_name: "Rodger's iOS Music Export"
short_description: "Export iOS music safely on a Mac"
brand_color: "#151515"
default_prompt: "Use $rodgers-ios-music-export to export matching music from a USB iOS device without syncing it."
policy:
allow_implicit_invocation: true

View file

@ -0,0 +1,100 @@
# Compatibility and safety reference
## Proven baseline
The workflow was end-to-end proven on:
- macOS on Apple silicon with Apple's MobileDevice framework and system `usbmuxd`;
- an already paired iPad mini 2 (`iPad4,5` / `J86AP`);
- iOS 12.5.8 build 16H88;
- the on-device `MediaLibrary.sqlitedb` schema checked by the bundled script;
- 295 exact case-insensitive `Electro` matches, totaling 2,173,003,367 bytes;
- 262 `.m4a` and 33 `.mp3` files, all size-checked, SHA-256 hashed, audio-probed, and found with exact embedded `Electro` genre tags.
This baseline proves the method, not universal compatibility. Later iOS releases may hide the classic music database, alter its schema, restrict AFC access, or represent cloud media differently. The script validates known tables and columns and fails closed instead of guessing.
## Read-only protocol boundary
The live-device client uses only:
- usbmuxd `ListDevices`, `ReadPairRecord`, and `Connect`;
- lockdownd `StartSession`, `GetValue`, `StartService`, and `StopSession`;
- the existing pair record held in memory;
- AFC `READ_DIR`, `GET_FILE_INFO`, `FILE_OPEN` with `AFC_FOPEN_RDONLY`, `FILE_READ`, and `FILE_CLOSE`.
No AFC write opcode is defined or called, and each generic protocol dispatcher enforces a read-only allowlist. Lockdown TLS excludes anonymous and null ciphers and pins the peer certificate to the existing pair record. The host certificate and private key are materialized only inside a mode-0700 temporary directory as mode-0600 files because Python's TLS API requires paths; they are loaded locally and the directory is immediately removed.
## Database snapshot
The database lives at:
```text
/iTunes_Control/iTunes/MediaLibrary.sqlitedb
```
For every attempt, the script captures the main database and `-wal` sidecar twice. Each capture must remain metadata-stable while read, and both complete captures must have identical SHA-256 hashes. SQLite rebuilds local shared-memory state; `PRAGMA quick_check` must pass before and after the stable set is consolidated with SQLite's backup API into one local database. Repeated differences produce a hard failure. Stop playback, leave device-side music activity idle, and retry; never modify or checkpoint the device database.
## Selection semantics
The database query uses equality with SQLite `COLLATE NOCASE`. It intentionally does not use substring matching, prefix matching, fuzzy matching, language translation, or a genre family. The plan reports total exact matches, transferable rows, and rows excluded because no local file is present.
Database-controlled source paths must be relative, contain no `.` or `..` components, and resolve below `/iTunes_Control/Music`, `/Purchases`, or `/CloudAssets`. Every source must report an explicit regular-file type. The manifest binds each row to a one-way SHA-256 device fingerprint, consolidated database hash, source size, modification time, block signature, and the bytes received during download.
The local filename is normalized to NFC, removes control characters, replaces `/` and `:`, stays under the macOS filename byte limit, and adds deterministic `[2]`, `[3]`, and later suffixes for collisions.
## Expected failures
### Permission denied for `/var/run/usbmuxd`
The command is inside a sandbox that blocks the system socket. Rerun only the same bounded command through Codex's approval path. This is not evidence that the device is absent.
### No existing pair record
Stop before taking action. Explain that unlocking the device and approving Apple's visible Trust prompt creates persistent pairing state on both the Mac and device, then obtain the user's explicit permission. Do not create or alter pairing records with the script. If the user approves and completes the visible Trust flow, report that pairing state changed; do not claim that the device remained entirely unchanged.
### Exactly one USB device expected
Disconnect unintended iOS devices, then retry. The skill deliberately avoids guessing among multiple devices.
### Database changes during every snapshot
Stop playback and device-side library changes. Keep the iOS Music app idle and rerun. Do not sync.
### Unsupported MediaLibrary schema
Stop. Collect only non-sensitive table and column names for a compatibility update; do not weaken the schema guard or guess column meanings.
### Stale database size or missing selected source
The device library metadata and storage no longer agree. Stop and retry after the device is idle. Never substitute another file merely because its name looks related.
### Untrusted existing destination file
Preserve it. Use a new destination or ask the user how to resolve the collision. Do not overwrite, move, or delete it implicitly.
### Manifest lock is held
Another exporter is using the same manifest. Wait for it to finish and retry. The persistent empty `.lock` file carries no device or music data; its advisory lock exists only while an export process is active.
### Truncated final manifest event
The TSV manifest is an append-only event log with a SHA-256 checksum on every event. During export resume, a trailing event without a complete newline is truncated only while the manifest lock is held and only after the entire preceding event prefix validates. A malformed header, failed checksum on a complete event, or event bound to another export plan is preserved and causes a hard stop.
## Media limitations
- DRM-protected subscription media may be encrypted or unusable off-device.
- Cloud-only tracks without a local media file are excluded.
- The skill does not remove DRM, download cloud media, authenticate Apple accounts, import into Music, or sync content back to a device.
- `ffprobe` is optional. Without it, the script still proves file count, size, and SHA-256 equality between the received stream and the verified local file, but cannot prove the presence of an audio stream or inspect embedded genre tags.
- A SHA-256 manifest proves local transfer and persistence integrity. It does not decrypt DRM, prove legal ownership, or assert that a cloud service holds the same bytes.
## Maintenance gate
For a new iOS schema or protocol behavior:
1. preserve the existing proven path;
2. add a synthetic regression fixture for the new schema;
3. make the narrowest compatibility change;
4. validate on a noncritical paired device with read-only commands first;
5. prove a small export and its hashes before a large export;
6. update this compatibility matrix with the exact device and iOS build.

File diff suppressed because it is too large Load diff