You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit accdec3
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: CHANGELOG.md
+34-1Lines changed: 34 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,6 +4,38 @@ All notable changes to shelldeck are listed here. The format follows [Keep a Cha
4
4
5
5
## [Unreleased]
6
6
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
+
7
39
## [0.0.13] - 2026-10-11
8
40
9
41
### Fixed
@@ -225,7 +257,8 @@ First public release.
225
257
- Mandatory password with per-browser logins, idle lock, and HTTPS or SSH-tunnel remote access.
226
258
-`sd` CLI with a startup banner, `sd search` (LLM-free code search), and one-line installers for Windows and Linux.
Copy file name to clipboardExpand all lines: CLAUDE.md
+13-1Lines changed: 13 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -51,7 +51,10 @@ uv tool install --force -e . # global sd/shelldeck from this checkout (th
51
51
-**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.
52
52
-**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"]`).
53
53
-**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.
55
58
-`guard` adds `SECURITY_HEADERS` (no framing, nosniff, no referrer) to every response and `Cache-Control: no-store` to `/api/*`.
56
59
-`serve` refuses plain HTTP on non-loopback addresses without `--cert/--key` or `--insecure-http`.
57
60
- The old seeded `nopassword` is deleted on start.
@@ -69,6 +72,7 @@ uv tool install --force -e . # global sd/shelldeck from this checkout (th
69
72
-**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.
70
73
-`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.
71
74
- 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.
72
76
- 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.
73
77
- Without the supervisor (tests, `server.run` directly) `manager` stays an in-process `PtyManager`.
74
78
- 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
-`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.
148
160
-**Release plumbing.**
149
161
-`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).
150
162
-`.github/workflows/ci.yml`: lint, a Windows+Ubuntu × py3.12–3.14 test matrix, and an installer smoke test.
Copy file name to clipboardExpand all lines: README.md
+33-2Lines changed: 33 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -321,10 +321,39 @@ sd share --new-link # another one-use link for a second device
321
321
sd share stop
322
322
```
323
323
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.
326
326
- The first share asks you to accept the terms. Stopping a share signs out every browser that came through it.
327
327
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
+
<imgsrc="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
+
328
357
**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.
329
358
330
359
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
335
364
-**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.
336
365
-**One device at a time.** Logging in elsewhere takes over and idles the rest; the Devices page revokes any login.
337
366
-**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.
338
369
-**Forgot the password?** On the host: `sd login-link` (one-time URL, 5 minutes) or `sd reset-password`. Never from the browser.
339
370
-**Your data** lives in `~/.config/shelldeck/` (`SHELLDECK_HOME` moves it).
0 commit comments