Skip to content

Commit accdec3

Browse files
authored
feat: iPhone/iPad app, trusted devices, end-to-end encryption; v0.0.14 (#22)
Native iPhone/iPad app (ios/), trusted devices via encrypted ntfy.sh relay, X25519+AES-GCM end-to-end channel, share approval code, tunnel cookie/HSTS and QR fixes, bounded restart + supervisor guard.
1 parent 1865bbc commit accdec3

44 files changed

Lines changed: 3330 additions & 28 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,38 @@ All notable changes to shelldeck are listed here. The format follows [Keep a Cha
44

55
## [Unreleased]
66

7+
## [0.0.14] - 2026-10-12
8+
9+
### Added
10+
11+
- **iPhone and iPad app** (`ios/`), native SwiftUI, built and installed from Windows + WSL with xtool and a free Apple ID. See `ios/README.md` for setup, prerequisites, scripts and how to install it for someone else.
12+
- Tabs:
13+
- **Terminals**: SwiftTerm, with each agent's state.
14+
- **System**: CPU, memory, GPU, and usage per terminal.
15+
- **Schedule**: jobs and tasks.
16+
- **Scratchpad**.
17+
- **Settings**: several PCs, and alert sounds when an agent needs you or finishes.
18+
- On iPad: projects in a sidebar with up to four tiled terminals beside it, split-view Scratchpad, and every orientation and multitasking.
19+
- **Trusted devices.** After the app is allowed once through a share, it finds every later share by itself, with no QR or approval (the password is still asked).
20+
- Each new share address reaches it through ntfy.sh, sealed with AES-256-GCM.
21+
- The Devices page lists trusted devices and revokes them. A new one raises an alert on the PC.
22+
- An iPhone and an iPad on the same Apple ID share their trusted PCs through iCloud Keychain.
23+
- **End-to-end encryption for the app.** The share QR carries the PC's public key in its `#` fragment, so the key never reaches Cloudflare. All app traffic (API, terminals, password) runs through one X25519 + AES-256-GCM channel (`/e2e`), so the tunnel and Wi-Fi only see ciphertext.
24+
- **Matching code on share approval.** A device that opens a share link shows a 4-digit code, and the Allow prompt (toast, Devices, `sd share`) shows the same code.
25+
26+
### Changed
27+
28+
- Through a share, responses send `Strict-Transport-Security`.
29+
- The new dependency `cryptography` is used for the channel and the relay.
30+
31+
### Fixed
32+
33+
- Through a share, the login cookie wasn't marked `Secure`: sd-ui only sees http from cloudflared.
34+
- The Share dialog's QR code could be cut off. It now scales to fit.
35+
- `sd restart` could start a second server while the first was still shutting down, splitting the shells between two.
36+
- Shutdown now waits at most 5 seconds.
37+
- `sd` waits for a supervisor that's still alive instead of starting another.
38+
739
## [0.0.13] - 2026-10-11
840

941
### Fixed
@@ -225,7 +257,8 @@ First public release.
225257
- Mandatory password with per-browser logins, idle lock, and HTTPS or SSH-tunnel remote access.
226258
- `sd` CLI with a startup banner, `sd search` (LLM-free code search), and one-line installers for Windows and Linux.
227259

228-
[Unreleased]: https://github.com/codejunction/shelldeck/compare/v0.0.13...HEAD
260+
[Unreleased]: https://github.com/codejunction/shelldeck/compare/v0.0.14...HEAD
261+
[0.0.14]: https://github.com/codejunction/shelldeck/releases/tag/v0.0.14
229262
[0.0.13]: https://github.com/codejunction/shelldeck/releases/tag/v0.0.13
230263
[0.0.12]: https://github.com/codejunction/shelldeck/releases/tag/v0.0.12
231264
[0.0.11]: https://github.com/codejunction/shelldeck/releases/tag/v0.0.11

‎CLAUDE.md‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ uv tool install --force -e . # global sd/shelldeck from this checkout (th
5151
- **Approval:** opening the link makes a pending grant (sha256 of a random `sd_share` cookie) and serves `_WAIT_PAGE`, which polls `/share/status`. Any non-tunnel browser (toast, Share dialog, Devices) or the `sd share` window (`Allow it? [y/N]`) answers via `POST /api/share/decide`; first answer wins. `_trusted()` refuses the tunnel host except `/share/*` unless the grant is `ok`; the password is still required.
5252
- **Lifetime:** no lease; a share runs until `DELETE /api/share` (`sd share stop`, *Stop sharing*, Ctrl+C in the `sd share` that started it), cloudflared exiting (`_tunnel_exited`) or server shutdown. Stopping signs tunnel logins out. Revoking a share login deletes its grant (`grant["session"]`).
5353
- **Terms:** `share.TERMS` / `TERMS_VERSION`; `POST /api/share` is `409 terms_required` until `POST /api/share/terms {accept:true}` (host only) writes `<config>/share-consent` (`auth.accept_terms`, owner-only like the password). Bump the version when the text changes in substance.
54-
- Tunnel requests are remote (`X-Forwarded-For`/`CF-Connecting-IP`). Cloudflare terminates TLS and sees plaintext; the README says so.
54+
- Tunnel requests are remote (`X-Forwarded-For`/`CF-Connecting-IP`). Cloudflare terminates TLS and sees plaintext; the README says so. `guard` adds HSTS when `_via_share`, and `_login_response` marks the cookie `Secure` there (sd-ui itself sees http).
55+
- **Matching code:** each grant has a 4-digit `code` (`share_open`), returned by `/share/status` to that browser only and in `_pending()` for the host (toast, Devices, `sd share` prompt, the app).
56+
- **Trusted devices** (`trusted_devices` table, `relay.py`): `POST /api/trusted` (remote app sessions only, `auth.remote`) returns a device token (stored as sha256), an ntfy.sh topic and an AES-GCM key. The `sd_device` cookie makes `_grant` return an `ok` grant (`device:<id>`, once per share) without a link. `_announce` posts the sealed share URL to every device's topic on start and every `REPEAT_S` (ntfy keeps 12h). `/api/trusted/me` lets the app reuse its trust instead of enrolling again; `DELETE /api/trusted/{id}` (not via the tunnel) revokes; enrolling broadcasts `{"type":"trusted"}` (toast, Devices table).
57+
- **End-to-end channel** (`e2e.py`, `/e2e` WebSocket): the iOS app's only transport. X25519 with the server's static key (`<config>/e2e-key`, in the share QR's `#k=` fragment, or `/e2e/key` over the LAN) plus ephemerals, HKDF-SHA256, AES-256-GCM with per-direction counter nonces. Inside, `{"t":"http"|"ws"}` messages are dispatched to `app` as ASGI with the outer connection's host/forwarded headers (`E2E_OUTER`), so the gate, grants and logins apply unchanged. A frame that fails to decrypt closes the channel. `e2e.Client` mirrors the app for tests.
5558
- `guard` adds `SECURITY_HEADERS` (no framing, nosniff, no referrer) to every response and `Cache-Control: no-store` to `/api/*`.
5659
- `serve` refuses plain HTTP on non-loopback addresses without `--cert/--key` or `--insecure-http`.
5760
- The old seeded `nopassword` is deleted on start.
@@ -69,6 +72,7 @@ uv tool install --force -e . # global sd/shelldeck from this checkout (th
6972
- **Two processes.** `serve` is a supervisor (`cli._supervise`, process name `shelldeck`) that runs `sd-pty` (`shelldeck ptyhost`) and `sd-ui` (`shelldeck ui`); `_role_argv` starts the sibling `sd-pty`/`sd-ui` exe (frozen build or the installed scripts in pyproject, so Task Manager shows them under shelldeck), else `python -m shelldeck`. Subcommands are passed explicitly; there is no argv0 dispatch.
7073
- `shelldeck/ptyhost.py`: `Host` (in sd-pty) owns a `PtyManager`, records scrollback, saves it every 15s and quits on stdin EOF (the supervisor holds the pipe, so a stop or a dead supervisor ends the shells). `PtyClient` (in sd-ui, a `PtyManager` subclass set in `lifespan` when `SHELLDECK_PTY_ADDR` is set) mirrors scrollback locally and sends one-way messages. Loopback TCP, one JSON line per message, token from the supervisor, one UI connection at a time. On connect the host sends `state` (alive terminals, scrollback, extra env); `_host_terminal` restarts their pumps and report tokens. Bump `PROTOCOL` on an incompatible change.
7174
- sd-ui exit codes: 0 stops everything, `RESTART_UI` (75) restarts sd-ui only (`/api/restart`, `sd restart`, updates), `RESTART_ALL` (76) also restarts sd-pty (`_host_lost`). While `STOPPING` under a `PtyClient`, `_pump` must not terminate shells.
75+
- uvicorn's `timeout_graceful_shutdown=5`: a handler stuck on a tunnel socket once kept sd-ui alive with its port closed. The supervisor writes `<config>/supervisor-<port>.pid`; `ensure_server` waits for a live supervisor (`_supervisor_alive`) instead of spawning a second server while sd-ui restarts.
7276
- sd-pty gets `CREATE_NEW_PROCESS_GROUP` / `start_new_session`, so a console Ctrl+C stops sd-ui, not the shells. The supervisor exports `SHELLDECK_PORT`/`SHELLDECK_HOME` (and, frozen, the root on PATH plus `frozen.ensure_shims()`) because shells inherit sd-pty's env.
7377
- Without the supervisor (tests, `server.run` directly) `manager` stays an in-process `PtyManager`.
7478
- Echo through the host is ~2.6ms median on Windows.
@@ -145,6 +149,14 @@ uv tool install --force -e . # global sd/shelldeck from this checkout (th
145149
- `ui.js`: api, icons, dialogs, menus, toasts, localStorage `store`.
146150
- `vendor/`: pinned xterm.js 5.5 + fit + web-links + webgl + search.
147151
- `tests/`: `test_shelldeck.py` (API, security, settings, tasks, scheduler, static caching, real PTY round trip), `test_cli.py` (banner, web, browser/app window, serve guards, render, search), `test_gitgraph.py`.
152+
- `ios/`: the native iPhone/iPad app (SwiftUI, xtool; see `ios/README.md`).
153+
- `Channel.swift` is the e2e transport. Every API call and socket goes through it; the cookie jar is per PC, in the Keychain.
154+
- `Trust.swift` holds the trusted PCs (iCloud Keychain, synchronizable, keyed by e2e key).
155+
- `Model.swift` polls and runs the alarm socket and local alerts. Views are one per tab. `TerminalScreen.swift` has SwiftTerm and the iPad `PaneGrid`.
156+
- SwiftTerm is fetched by `vendor.sh`, pinned to a commit: its manifest drops the iOS sources when it runs on Linux.
157+
- Build in WSL: `sh ios/scripts/install.sh [udid]`. The USB path is `ios/scripts/bridge.sh`: Apple Devices' usbmuxd on Windows via socat and `muxrelay.py`. usbipd + Linux usbmuxd times out (65536-byte packets).
158+
- Swift 5 language mode on purpose.
159+
- Screenshots in `docs/assets/ios/` come from a demo shelldeck with made-up data, never the real one.
148160
- **Release plumbing.**
149161
- `install.ps1` / `install.sh`: one-line installers. Default is the standalone build from the GitHub release (`SHELLDECK_VERSION`, `SHELLDECK_ARCHIVE`, `SHELLDECK_ROOT`, `SHELLDECK_NO_MODIFY_PATH`; macOS and non-x64 fall back to uv); `-Pip` / `--pip` / `SHELLDECK_PIP=1` installs with uv then `uv tool install shelldeck` (`SHELLDECK_SOURCE` overrides the source and CI feeds it the built wheel; `UV_NO_MODIFY_PATH` skips PATH edits).
150162
- `.github/workflows/ci.yml`: lint, a Windows+Ubuntu × py3.12–3.14 test matrix, and an installer smoke test.

‎README.md‎

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,10 +321,39 @@ sd share --new-link # another one-use link for a second device
321321
sd share stop
322322
```
323323

324-
- Each link works **once**, for **10 minutes**, and the device then waits until you allow it on this machine. It still needs your password (12+ characters for sharing).
325-
- Cloudflare ends TLS, so it can see the traffic. For sensitive work use SSH or Tailscale.
324+
- Each link works **once**, for **10 minutes**, and the device then waits until you allow it on this machine. The device shows a 4-digit code, and the Allow prompt (toast, Devices, `sd share`) shows the same code: allow only if they match. It still needs your password (12+ characters for sharing).
325+
- Cloudflare ends TLS, so in a browser it can see the traffic. The iPhone/iPad app encrypts end to end instead (below). For sensitive work in a browser use SSH or Tailscale.
326326
- The first share asks you to accept the terms. Stopping a share signs out every browser that came through it.
327327

328+
**iPhone and iPad app.** The native app in [`ios/`](ios/README.md) is built and installed from Windows + WSL with [xtool](https://github.com/xtool-org/xtool). A free Apple ID is enough; no Mac or paid developer account. It has tabs for:
329+
330+
- **Terminals** (SwiftTerm) with each agent's state.
331+
- **System** usage.
332+
- **Schedule** (jobs and tasks).
333+
- **Scratchpad**.
334+
- **Settings** (several PCs, alert sounds).
335+
336+
On iPad, projects sit in a sidebar with up to four tiled terminals beside it.
337+
338+
<img src="https://raw.githubusercontent.com/codejunction/shelldeck/main/docs/assets/ios/ipad-terminals.png" alt="shelldeck on iPad: projects in a sidebar and four tiled terminals (an API server log, a test run, a Vite dev server and a Terraform plan)" width="100%">
339+
340+
<table>
341+
<tr>
342+
<td width="25%"><img src="https://raw.githubusercontent.com/codejunction/shelldeck/main/docs/assets/ios/iphone-terminals.png" alt="iPhone: terminals grouped by project"></td>
343+
<td width="25%"><img src="https://raw.githubusercontent.com/codejunction/shelldeck/main/docs/assets/ios/iphone-terminal.png" alt="iPhone: a terminal with the Esc, Ctrl, Tab and arrow key bar"></td>
344+
<td width="25%"><img src="https://raw.githubusercontent.com/codejunction/shelldeck/main/docs/assets/ios/iphone-system.png" alt="iPhone: CPU, memory and GPU of the PC, and usage per terminal"></td>
345+
<td width="25%"><img src="https://raw.githubusercontent.com/codejunction/shelldeck/main/docs/assets/ios/iphone-scratchpad.png" alt="iPhone: a scratchpad note"></td>
346+
</tr>
347+
</table>
348+
349+
- **Connecting.** Scan the share's QR in the app once and allow the device. From then on it's a **trusted device**: it finds every new share by itself, with no QR or approval (the password is still asked). The new share address reaches it through [ntfy.sh](https://ntfy.sh), encrypted with a key only your PC and the device hold.
350+
- **End-to-end encrypted.** The share QR also carries the PC's public key, in the part of the link after `#`, which never leaves the phone. All of the app's traffic (API calls, terminals, your password) goes through one X25519 + AES-256-GCM channel to that key, so Cloudflare and your Wi-Fi only see ciphertext.
351+
- **Several devices.** An iPhone and an iPad on the same Apple ID share their trusted PCs through iCloud Keychain.
352+
- **Managing them.** The Devices page lists trusted devices and revokes them, and a new one raises an alert on the PC.
353+
- **Limits of a free Apple ID.** The app has to be reinstalled every 7 days (`ios/scripts/install.sh`), and alerts ring only while the app is open.
354+
355+
Setup, prerequisites and installing it for someone else: [ios/README.md](ios/README.md).
356+
328357
**HTTPS on the VM's address:** `sd serve --host 0.0.0.0 --cert cert.pem --key key.pem`. Plain HTTP on a non-local address is refused (`--insecure-http` overrides). A first visit from another machine also needs the setup code `sd serve` prints.
329358

330359
On phones, a key bar adds Esc, Tab, Ctrl and arrows, and dialogs open as bottom sheets.
@@ -335,6 +364,8 @@ On phones, a key bar adds Esc, Tab, Ctrl and arrows, and dialogs open as bottom
335364
- **Password required.** PBKDF2-SHA256 (600k iterations); logins are `HttpOnly`, `SameSite=Strict` cookies stored as SHA-256; failed logins slow down; 30 minutes idle locks the browser.
336365
- **One device at a time.** Logging in elsewhere takes over and idles the rest; the Devices page revokes any login.
337366
- **The CLI** uses a token file readable only by your account, rotated on every start.
367+
- **Sharing** stays shut until you allow a device, after checking that its matching code is the one you see. Through the tunnel, responses send HSTS and the login cookie is `Secure`. Trusted app devices skip only the approval, never the password; revoke them in Devices.
368+
- **The iPhone/iPad app** talks to the PC only through an end-to-end encrypted channel: X25519 + AES-256-GCM, with the PC's key pinned from the share QR. Neither the tunnel nor ntfy.sh can read or forge its traffic.
338369
- **Forgot the password?** On the host: `sd login-link` (one-time URL, 5 minutes) or `sd reset-password`. Never from the browser.
339370
- **Your data** lives in `~/.config/shelldeck/` (`SHELLDECK_HOME` moves it).
340371

116 KB
Loading

‎docs/assets/ios/ipad-terminals.png‎

422 KB
Loading
74 KB
Loading

‎docs/assets/ios/iphone-system.png‎

137 KB
Loading
210 KB
Loading
86.4 KB
Loading

‎ios/.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.build/
2+
.bsp/
3+
xtool/
4+
Vendor/
5+
*.ipa

0 commit comments

Comments
 (0)