Skip to content
This repository was archived by the owner on Oct 30, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
hardware_tag:
description: Hardware version to pair with this CLI
required: true
default: v0.1.10
default: v0.1.11
hardware_run_id:
description: Optional hardware workflow run providing the tested wheel before its first release
required: false
Expand All @@ -24,7 +24,7 @@ jobs:
package:
runs-on: ubuntu-24.04
env:
HARDWARE_TAG: ${{ inputs.hardware_tag || 'v0.1.10' }}
HARDWARE_TAG: ${{ inputs.hardware_tag || 'v0.1.11' }}
steps:
- uses: actions/checkout@v4
with:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ see the [launcher build and publication instructions](https://github.com/Orchard

## Documentation

- [iPad mini 1 private tethered boot and NFS handoff](docs/p105.md)
- [Release publishing and updates](docs/releases.md)
- [Device operation protocol](docs/device-protocol.md)
- [Host adapter contract](docs/adapter-contract.md)
Expand Down
15 changes: 10 additions & 5 deletions docs/adapter-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,30 @@
| `connect(device)` | Context-managed serial bootstrap and pinned SSH access bound to the selected ECID/boot. No host-side device storage/service recipes. |
| `connect_saved(device)` | Context-managed use of the selected device/boot's retained pinned credentials. Never reprovision credentials or drive a login prompt. |
| `forget_access(scope)` | Remove only the confirmed device/boot's cached host credentials. Device-side revocation and its confirmed receipt happen first. |
| `capture_adt(device=None)` | Fresh, bounded, private Apple ADT capture for this adapter's model. When supplied, bind the confirmed device selection to the captured identity. Do not log or implicitly retain the raw tree. |
| `capture_inputs(device=None)` | Return a `PrivateCapture` with kind, profile, ECID and private bytes. J71b captures a fresh ADT; P105 checks the live selection against the owner's hashed MtCl capture. Raw inputs stay private. |
| `firmware_inputs(clock, progress)` | Return verified cached/acquired host firmware inputs; account download time separately. No device hardware or storage mutation. |
| `prepare_mixin(adt, firmware_dir, outdir)` | Return `(Path, MixinReceipt)` for a private data-only archive bound to the selected device/recipe. Preserve firmware hash, containment, calibration and output guards. |
| `prepare_mixin(inputs, firmware_dir, outdir)` | Return `(Path, MixinReceipt)` for a private data-only archive bound to the selected device/recipe. Preserve firmware hash, containment, calibration and output guards. |

Arguments and return types are defined in `contracts.py`, not repeated in a second runtime schema. Image-facing protocol validation, manifest validation and hardware identity verification remain in their existing owners.

## Records

- `SelectedDevice`: required `mode`, `location`, non-null validated `ecid` and explicit boolean `resumed`; optional observed chip/board/product/serial details. `resumed` is true for running recovery and false for DFU/Pongo. Common orchestration requires this explicit state. A USB location may change across enumeration and is not persistent device identity.
- `SelectedDevice`: required `mode`, `location`, non-null validated `ecid` and explicit boolean `resumed`; optional observed chip/board/product/serial details and iOS UDID. Modes include DFU, Pongo, iOS, iBEC and Linux. A running Linux recovery has `resumed=true`. After a P105 upload, the adapter updates the selection with the observed Linux boot identity before connecting. USB locations can change across enumeration.
- `Connection`: SSH arguments, validated `BootState` and a current-endpoint check performed before every RPC. Private state is keyed by profile/ECID/boot, never by USB port.
- `BootAssets`: local paths for the selected operation. Running recovery needs no upload; other states require recovery, and DFU additionally requires Pongo. Installation root is not interpreted by the adapter's boot method.
- `BootAssets`: verified local paths for the selected boot method. J71b uses recovery and Pongo. P105 uses recovery, staging loader, private iBEC and kloader. The installation root belongs to the installation workflow.
- `BootRequest`: minimum boot input, including the recovery SHA receipt. Both manifest-resolved and explicit-file boot use it; no source-specific fallback adapter API exists.
- `MixinReceipt`: schema 1, `private-mixin`, exact compressed byte count and SHA-256.
- `PrivateCapture`: kind `apple-adt` or `p105-mtcl`, adapter profile, validated ECID and private bytes. P105's JSON payload records its panel capture and digest. Keep its device binding with the private input.

TypedDict records describe the boundary. They do not replace validation of untrusted USB data or manifests, and do not imply that every dictionary constructed by an arbitrary caller is safe.

## Enforcement

The registry checks required member presence, profile identity and presentation metadata without invoking discovery. Python runtime Protocol checks do not verify function signatures; static checking does that. The real J71b module is assigned directly to `Adapter` in `tests/typecheck_adapter.py`, so signature drift is rejected without creating a wrapper implementation.
The registry checks required member presence, profile identity and presentation
metadata without invoking discovery. Static checks assign both registered
modules directly to `Adapter` in `tests/typecheck_adapter.py`. Ambiguous USB
discovery stops automatic selection across adapters. `--profile` preserves an
explicit selection through the common workflows.

From `cli/`:

Expand Down
29 changes: 29 additions & 0 deletions docs/device-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,32 @@ Host code must not infer device paths or encode module loads, filesystem offsets
- Pre-exec PID 1 failures attempt to restore moved pseudo-filesystem aliases and restart key-only recovery diagnostics, never retrying handoff/storage. Reported failures stop host readiness polling immediately. Kernel failure or failure inside the final root-switch executable can still require DFU recovery.
- The desktop shell reports readiness only after constructing its root and successfully capturing a compositor frame to `/dev/null`. The device checks the boot ID, live shell process and owned Wayland socket.
- No screenshot is retained. Readiness does not attest touch gestures, brightness interaction or Wi-Fi association.
## P105 tethered recovery

P105 uses protocol 1 with a limited operation set:

| Operation | Behavior |
| --- | --- |
| `status` | Report profile, provisioned ECID, live kernel boot ID, root mode, readiness and the flash fence |
| `diagnostics` | Add the kernel version, MTD inventory and a bounded kernel log |
| `bootstrap` | Provision one diagnostic key through the physical RAM serial console; return the pinned host key and USB SSH endpoint |
| `handoff` | Queue one NFS pivot bound to profile, ECID, boot ID and operation UUID |

`handoff` takes `nfs_root` in the form `10.55.0.1:/absolute/export/path`.
Recovery accepts an authenticated request before PID 1 mounts the export. It
checks the P105 systemd marker and ARM executable, then prepares hardware and
RAM diagnostics before moving mounts. A rejected mount or root validation leaves the RAM console and
its diagnostic state available. Requests receive one dispatch per attempt.

The USB serial number carries the owner's ECID and the current kernel boot
ID. The ECID is provisioned from the verified iOS selection; P105's static
device tree supplies its board identity. The ordinary kernel disables its
NAND controller. Each ready status checks the live board and controller fence.

During a mount move, an in-flight status can report `mode=handoff` from the
protected queued identity. It stays unready until the live checks succeed in
the NFS root. The serial-pinned SSH key travels in RAM across this transition.
The host stores access by profile, ECID and boot ID.

The internal storage operations described for J71b below remain outside the
P105 operation set. The host checks the reported operations before dispatch.
140 changes: 140 additions & 0 deletions docs/p105.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# iPad mini 1 Wi-Fi: tethered recovery and NFS

This contribution pairs CLI 0.1.11 with hardware 0.1.11. Install both local
wheels while these branches await publication. The hardware package supplies
the P105 resources required by the adapter.

The `p105` adapter supports `iPad2,5` / P105AP running the qualified jailbreak
on iOS 9.3.5 build 13G36. It stages the verified kloader and plaintext iBEC over
a UDID-bound SSH proxy, waits for the same ECID in Recovery, then sends the
private bundle and staging loader once. Reconnect the USB cable when prompted.

Recovery reports its provisioned ECID and the live kernel boot ID over USB.
That ECID comes from the owner's verified iOS selection. P105 supplies panel
calibration through MtCl; J71b supplies a live ADT. SSH bootstrap pins the
recovery host key through the physical serial console. Saved credentials are
scoped to the selected device and boot.

The adapter supports status, bounded diagnostics and a handoff to the owner's
existing USB NFS export. Internal storage preparation awaits a qualified A5
storage backend. Keep the host serving NFS throughout the desktop session.

Physical qualification reached the Omarchy desktop and preserved the RAM USB
rescue console through repeated boots. Touch remains slow. One repeat required
a touchscreen service restart after calibration failed; that driver issue
remains open. See the companion hardware validation record for the measured
scope. Diagnostic readiness reports the reached systemd targets; inspect
diagnostics and individual services when input fails.

## Prepare the private recovery

Use the P105 resources from the companion hardware contribution. In
`minimal-linux`, `p105/build-base.py` builds the signed ARMv7 Alpine package
base. Its ARM64 package manager uses the official ARMv7 signing keys and
extracts target packages without executing them. Retain its base receipt.

`p105/prepare-recovery.py` combines that base with an explicit owner initramfs,
the matching panel calibration and the verified target record. The hardware
producer recognizes its `etc/orchard/p105.json`, keeps the desktop kernel
prerequisites, and selects a USB gadget whose serial number carries the boot
identity. The RAM environment waits for an authenticated NFS request.

```sh
python3 p105/build-base.py --output /private/p105/alpine-base.tar
python3 p105/prepare-recovery.py \
--base /private/p105/alpine-base.tar \
--base-receipt /private/p105/alpine-base.json \
--initramfs /private/p105/owner-initramfs \
--calibration /private/p105/owner-MtCl.bin \
--target /private/p105/target.json \
--output /private/p105/recovery-root
```

Build that root through the installed hardware package's `kernel/p105/build`,
passing it as `--private-initramfs` with the separate `--calibration` input.
The resulting components and their receipt stay private. Compose their boot
directory with the installed CLI:

```sh
orchard p105-manifest --components /private/p105/components \
--ibec /private/p105/iBEC.autogo --kloader /private/p105/kloader \
--output /private/p105/boot
```

This creates an owner-local `boot.toml` and copies the verified boot assets.
The resolver requires local files and the manifest's private flag for this
boot method. The qualified iBEC and kloader hashes are checked again before
staging. A changed component receipt or another device's ECID stops the boot.

## Owner configuration

Set `ORCHARD_P105_CONFIG` to an owner-only JSON file, or place it at
`ORCHARD_HOME/private/p105.json`. Its `schema` is 1. The required fields are:

| Field | Owner input |
| --- | --- |
| `target` | Object containing the selected `udid` and integer `ecid` |
| `tools` | Directory containing `ideviceinfo`, `iproxy`, `irecovery`, and `sshpass` when password authentication is used |
| `ios.known_hosts` | Private file containing the trusted iOS key under alias `p105-ios-usb` |
| `ios.identity` or `ios.password_file` | Explicit owner-only SSH identity or password file |
| `components` | Directory containing the verified private recovery components |
| `firmware_root` | Owner initramfs with the pinned firmware destinations |
| `calibration` | Object containing `file`, integer `ecid` and the capture's `sha256` |

All paths are absolute. Keep the calibration capture and configuration bound
to the same iPad. `orchard --profile p105 capture-inputs --output FILE` writes
a protected MtCl record after checking the selected device. Private mixins
accept this record with `--inputs`; J71b retains its `--adt` input.

The host also needs SSH, `ssh-keygen`, `showmount` and Podman for preparation.
USB utilities can come from the owner's vetted Legacy iOS Kit installation.
Provisioning the jailbreak and trusting the initial iOS SSH key remain owner
setup steps.

## Boot and hand off

Prepare the Arch ARMv7 root with the hardware runtime's `p105-systemd-v1`
marker and Python 3.9 or later. Export it over NFSv3/TCP to `10.55.0.2`.
The host address is `10.55.0.1/24` on the selected USB link.

```sh
orchard --profile p105 boot /private/p105/boot/boot.toml \
--nfs-root 10.55.0.1:/absolute/exported/root
orchard --profile p105 status
orchard --profile p105 diagnostics
```

Omit `--nfs-root` to stop in RAM recovery. Provision its first diagnostic
session with `orchard --profile p105 status --recovery`. A later
`orchard --profile p105 handoff --nfs-root HOST:/PATH` uses the saved session.

The host verifies the requested export before dispatch. Recovery accepts one
handoff per boot and validates the systemd root before moving mounts. The
installed root receives the hardware payload and RAM diagnostic keys. A
separate diagnostic listener keeps the serial-pinned key through the pivot;
the root's existing SSH configuration retains its own files beneath the
temporary authorization bind mount. The physical rescue console runs from RAM.

Host reports preserve an uncertain outcome before each upload or handoff.
After an uncertain upload, inspect the device and return to iOS before starting
a fresh attempt. A timeout while waiting for the cable can continue from iBEC:
repeat the same command within 15 minutes. Orchard requires the newest saved
attempt to be awaiting iBEC, with matching ECID and boot files. An upload phase
with an unknown outcome blocks continuation. Uploads and NFS requests receive
one dispatch per attempt.

## Host link ownership

Orchard obtains the network interface and its MAC from the selected USB
device's registry or sysfs children while checking its device and boot identity.
This handles macOS reporting a masked MAC through `ifconfig`. An existing host
address is retained. A missing address can be added with the host's normal
administrator prompt; its private
`network.json` records ownership. After shutting down Linux, run:

```sh
orchard p105-network-cleanup --record /path/to/this/boot/network.json
```

Cleanup removes an address only when that record says Orchard added it. The
owner continues to manage the existing NFS export and its service.
12 changes: 6 additions & 6 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ python -m pip install --upgrade pip build
python -m build --wheel
```

Build hardware first, then CLI. Wheel versions come from each repository's `pyproject.toml`. The release pair prepared here is CLI `0.1.10` and hardware `0.1.10`. It includes the expanded J71b Wi-Fi firmware recipe; consuming images must require Orchard `0.1.10` or newer.
Build hardware first, then CLI. Wheel versions come from each repository's `pyproject.toml`. The release pair prepared here is CLI `0.1.11` and hardware `0.1.11`. This pair adds private P105 tethered recovery and NFS handoff. P105 requires the companion hardware resources; public image assembly remains a separate release step.

## Prepare assets locally

From the CLI repository, pass explicit paths to both wheels:

```sh
python tools/prepare_release.py \
--cli-wheel dist/orchard_cli-0.1.10-py3-none-any.whl \
--hardware-wheel /path/to/orchard_hardware-0.1.10-py3-none-any.whl \
--output out/publication/host-0.1.10
--cli-wheel dist/orchard_cli-0.1.11-py3-none-any.whl \
--hardware-wheel /path/to/orchard_hardware-0.1.11-py3-none-any.whl \
--output out/publication/host-0.1.11
```

Use the actual wheel filenames for later versions. The tool reads their metadata and computes hashes. Output must be a new directory:
Expand All @@ -44,8 +44,8 @@ Before publishing, install the pair into a fresh virtual environment, run `pytho
```sh
python3 -m venv out/release-test
out/release-test/bin/python -m pip install --only-binary=:all: \
dist/orchard_cli-0.1.10-py3-none-any.whl \
/path/to/orchard_hardware-0.1.10-py3-none-any.whl
dist/orchard_cli-0.1.11-py3-none-any.whl \
/path/to/orchard_hardware-0.1.11-py3-none-any.whl
out/release-test/bin/python -m pip check
PYTHONDONTWRITEBYTECODE=1 out/release-test/bin/python -m unittest discover -s tests -v
out/release-test/bin/orchard resolve tests/fixtures/first/index.toml
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"

[project]
name = "orchard-cli"
version = "0.1.10"
version = "0.1.11"
description = "Apple device Linux boot and installation tooling"
readme = "README.md"
license = "GPL-2.0-or-later AND MIT"
license-files = ["LICENSE", "NOTICE", "LICENSES/*"]
requires-python = ">=3.9"
dependencies = ["orchard-hardware>=0.1.10,<0.2", "jsonschema>=4.25,<5", "tomli>=2; python_version < '3.11'"]
dependencies = ["orchard-hardware>=0.1.11,<0.2", "jsonschema>=4.25,<5", "tomli>=2; python_version < '3.11'"]

[project.urls]
Repository = "https://github.com/OrchardLinux/cli"
Expand All @@ -23,7 +23,7 @@ orch = "orchard_cli.command:main"

[tool.setuptools]
include-package-data = false
packages = ["orchard_cli", "orchard_cli.j71b", "orchard_cli.apple", "orchard_cli.transports", "orchard_cli_schemas"]
packages = ["orchard_cli", "orchard_cli.j71b", "orchard_cli.p105", "orchard_cli.apple", "orchard_cli.transports", "orchard_cli_schemas"]
package-dir = {"" = "src", orchard_cli_schemas = "schemas"}

[tool.setuptools.package-data]
Expand All @@ -36,7 +36,7 @@ dev = ["mypy==1.18.2"]
[tool.mypy]
python_version = "3.9"
mypy_path = "src"
files = ["src/orchard_cli/contracts.py", "src/orchard_cli/devices.py", "src/orchard_cli/j71b/adapter.py", "src/orchard_cli/j71b/inputs.py", "src/orchard_cli/j71b/mixin.py", "src/orchard_cli/service.py", "src/orchard_cli/command.py", "tests/typecheck_adapter.py"]
files = ["src/orchard_cli/contracts.py", "src/orchard_cli/devices.py", "src/orchard_cli/j71b/adapter.py", "src/orchard_cli/j71b/inputs.py", "src/orchard_cli/j71b/mixin.py", "src/orchard_cli/p105", "src/orchard_cli/service.py", "src/orchard_cli/command.py", "tests/typecheck_adapter.py"]
check_untyped_defs = true
follow_imports = "silent"
ignore_missing_imports = true
Expand Down
46 changes: 46 additions & 0 deletions schemas/v1/boot.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@
"version": {
"type": "string",
"minLength": 1
},
"loader": {
"$ref": "#/$defs/receipt"
},
"ibec": {
"$ref": "#/$defs/receipt"
},
"kloader": {
"$ref": "#/$defs/receipt"
},
"private": {
"type": "boolean"
},
"target_ecid": {
"type": "integer",
"minimum": 1,
"maximum": 18446744073709551615
}
},
"required": [
Expand Down Expand Up @@ -67,6 +84,35 @@
"pongo"
]
}
},
{
"if": {
"properties": {
"boot_method": {
"const": "jailbreak-kdfu"
}
},
"required": [
"boot_method"
]
},
"then": {
"required": [
"loader",
"ibec",
"kloader",
"private",
"target_ecid"
],
"properties": {
"device": {
"const": "p105"
},
"private": {
"const": true
}
}
}
}
],
"$defs": {
Expand Down
Loading