commit 1f6115b789b741d49b9f6b6e6bbb7eb943d38928 Author: saphid <4596216+saphid@users.noreply.github.com> Date: Fri Sep 25 15:38:43 2026 +1000 Steam Frame from a Mac: research docs and helper scripts README with the minimum-typing checklist (Developer Mode toggle + Set User Password; the rest runs from the Mac), docs for SSH, streaming, file transfer, and open questions with sourced confidence levels, plus Mac-side zsh helpers and a fallback headset bootstrap. Scripts are UNTESTED against hardware: checked with zsh -n / bash -n / shellcheck only. Two SWE-2 Max read-only review passes (devin -p --model swe-2-max); verified findings fixed. Co-Authored-By: Claude Opus 5.5 (1M context) diff --git a/README.md b/README.md new file mode 100644 index 0000000..57bffbf --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Steam Frame ↔ Mac + +This repo holds notes and Mac-side helpers for controlling a Valve Steam Frame +(standalone VR headset: SteamOS 3, Arch-based, arm64, Snapdragon 8 Gen 3) from +this Mac, with as little typing on the headset's virtual keyboard as possible. + +Status: research written 2026-09-25. **None of the scripts have been run +against a real Frame yet.** Treat them as untested until the checklist below has +been done once. + +## Minimum typing on the headset + +Valve's own developer docs say SSH, ADB, and RDP are all turned on through a +**UI toggle**. You don't need a terminal, `passwd`, or `systemctl`. The only +thing you type on the headset is a password you choose. + +On the Frame: + +1. **Steam Settings → System → Enable Developer Mode** (a toggle, no typing). +2. Scroll down to the **Developer** section and click **Set User Password**. + Type a password. **This is the only thing you type on the headset.** Pick + something short, because you'll type it once more on the Mac and then + never again. +3. (Optional, no typing) Note the IP address from **Quick Settings** or + **Steam Settings → Internet**, in case `frame.local` doesn't resolve. +4. (Optional) Check **Steam Settings → System → Hostname**. Leaving it as + `frame` means the scripts work without any extra setup. + +On the Mac: + +```sh +cd ~/projects/steam-frame +./scripts/connect.sh # or: ./scripts/connect.sh 192.168.1.50 +ssh frame # passwordless from now on +``` + +`connect.sh` does four things: + +- finds the headset (`frame.local`, then `frame`, or the IP/host you pass in) +- creates a dedicated key (`~/.ssh/id_ed25519_frame`) +- adds a `Host frame` block to `~/.ssh/config` +- runs `ssh-copy-id`, which asks for the Developer Mode password once + +Run `./scripts/connect.sh --harden` later if you want to turn off SSH password +logins. + +Sources: [Valve: Setting up your Steam Frame for development](https://partner.steamgames.com/doc/steamhardware/steamframe/setup), +[Valve: Steam Frame Debugging](https://partner.steamgames.com/doc/steamhardware/steamframe/debugging) +(both **confirmed on Steam Frame**, Valve official). + +**Fallback, only if the Developer Mode toggle doesn't give you SSH.** From the +Mac, run `./scripts/serve-bootstrap.sh`. It prints a one-liner of about 30 +characters, like `curl -fsS mac.local:8765|bash`, to type into Konsole on the +Frame's Linux desktop. The script it serves installs your Mac's public key and +enables `sshd`. See [docs/ssh.md](docs/ssh.md#fallback-bootstrap-one-liner). + +## Recommended options + +| Goal | Recommended | Confidence | +|---|---|---| +| Shell on the Frame | `ssh frame` (user `steamos`) | Confirmed (Valve docs) | +| **See/control the Frame from the Mac** | **Steam Link for macOS → connect to `frame`** (Valve names this). Alternatives: RDP to `xrdp` with Microsoft *Windows App* for the Linux desktop, or `adb`/`scrcpy` for the Android (Lepton) layer only | Steam Link and xrdp confirmed on Frame; the Mac RDP client is inferred | +| **Show the Mac's desktop inside the Frame** | **macOS Screen Sharing (built-in VNC) → Remmina (Flatpak, aarch64) on the Frame's Linux desktop**, installed over SSH | Inferred: each piece is documented, but the combination hasn't been tested on a Frame | +| File transfer | `scp` / `rsync` over the `frame` alias (`scripts/push.sh`) | Inferred from confirmed SSH | +| Paste Mac clipboard into the headset | `scripts/paste-to-frame.sh` (`pbpaste` → `ssh` → `wl-copy`), or the clipboard sync in an RDP session | Inferred / untested | + +Details: [docs/ssh.md](docs/ssh.md), [docs/streaming.md](docs/streaming.md), +[docs/file-transfer.md](docs/file-transfer.md), +[docs/open-questions.md](docs/open-questions.md). + +## Scripts (all untested against hardware) + +| Script | Runs on | Purpose | +|---|---|---| +| `scripts/connect.sh` | Mac | Discover, set up key and `~/.ssh/config`, copy key, optional `--harden` | +| `scripts/install-apps.sh` | Mac → Frame | Install Flatpaks (Remmina, Moonlight, …) on the Frame over SSH as `--user` | +| `scripts/paste-to-frame.sh` | Mac → Frame | Send the Mac clipboard (or stdin) to the Frame clipboard | +| `scripts/push.sh` | Mac → Frame | `rsync` files to `~/Downloads` (or a given path) on the Frame | +| `scripts/serve-bootstrap.sh` | Mac | Fallback: serve `bootstrap-on-frame.sh` with your public key embedded | +| `scripts/bootstrap-on-frame.sh` | Frame | Fallback: install the key and enable `sshd` | + +## Security notes + +- With Developer Mode on, `sshd`, ADB (Wi-Fi, port 5555, while a Lepton + session is running), and xrdp are all reachable on your LAN. Use trusted + networks only. Turn Developer Mode off when you don't need it. +- `steamos` has `sudo`, protected by the same Developer Mode password. Once + you've switched to key auth, a short password still protects `sudo` and + RDP, so pick one that isn't trivially guessable. +- Don't port-forward 22, 3389, or 5555 from your router. For remote access, + use Tailscale (Flatpak/package availability for the Frame hasn't been + checked). diff --git a/docs/file-transfer.md b/docs/file-transfer.md new file mode 100644 index 0000000..6d6c831 --- /dev/null +++ b/docs/file-transfer.md @@ -0,0 +1,38 @@ +# File transfer and clipboard + +The confidence labels are the same as in [ssh.md](ssh.md). Everything here +depends on SSH working through the `frame` alias from `scripts/connect.sh`. + +## Options + +| Option | Command | Confidence | Notes | +|---|---|---|---| +| **scp / rsync over SSH** | `./scripts/push.sh file-or-dir [dest]`, or `rsync -a --progress x frame:Downloads/` | **Inferred.** SSH is confirmed. Valve recommends WinSCP (SFTP) for Windows ([debugging](https://partner.steamgames.com/doc/steamhardware/steamframe/debugging)), which means SFTP is enabled. | Recommended. The Mac ships `rsync` (newer macOS uses `openrsync`, which supports the flags used here). `rsync` must also exist on the Frame. It's in SteamOS on Deck; if it's missing on the Frame, `push.sh` falls back to `scp`. | +| SFTP GUI | Finder can't do SFTP. Use Cyberduck / Transmit / ForkLift with `sftp://steamos@frame.local` | Inferred | Good for browsing. | +| `adb push` | `adb push x /sdcard/Download/` (Lepton) | Confirmed that ADB exists ([adb_lepton](https://partner.steamgames.com/doc/steamhardware/steamframe/adb_lepton)) | Only reaches the Android container's storage. | +| SteamOS Devkit Client | "Title Upload" | Confirmed (Frame) ([loadgames](https://partner.steamgames.com/doc/steamhardware/steamframe/loadgames)) | For deploying apps and games, not general files. macOS support for the Devkit Client wasn't confirmed. | +| Syncthing | A Syncthing Flatpak on the Frame (`./scripts/install-apps.sh `), app on the Mac | Guess (which Syncthing Flatpak, and whether it has an aarch64 build, not checked) | Good for an ongoing shared folder. | +| KDE Connect | KDE Connect on both | Guess | There's a macOS build of KDE Connect, but whether it's present or installable on the Frame wasn't confirmed. It would give you clipboard sync, file send, and remote input. Worth checking on-device. | +| microSD | Physical card | Confirmed that the slot exists ([Wikipedia](https://en.wikipedia.org/wiki/Steam_Frame)) | Offline fallback. | + +## Clipboard + +`scripts/paste-to-frame.sh` sends the Mac clipboard (or stdin) to the +headset's desktop clipboard. You can then paste in the headset with the +virtual keyboard's paste key or a right-click → Paste. + +```sh +./scripts/paste-to-frame.sh # sends pbpaste +echo "https://example.com" | ./scripts/paste-to-frame.sh - +``` + +How it works (untested). Over SSH, the script finds the logged-in Plasma +session's `XDG_RUNTIME_DIR` and `wayland-*` socket, then runs `wl-copy`. If +there's no Wayland socket or no `wl-copy`, it tries `xclip` with `DISPLAY=:0`. +It assumes the in-headset desktop is a normal Plasma session owned by +`steamos`. That isn't known yet: the headset desktop may be a KWin session +nested inside SteamVR. If both methods fail, the script prints what it found so +the approach can be adjusted. + +A simpler fallback: `ssh frame 'cat > ~/clip.txt'` < file, then open it in the +headset. diff --git a/docs/open-questions.md b/docs/open-questions.md new file mode 100644 index 0000000..e67e82c --- /dev/null +++ b/docs/open-questions.md @@ -0,0 +1,63 @@ +# Open questions and on-device checks + +Research as of 2026-09-25, eight days after the Frame's retail release +(2026-09-18). Most first-party detail comes from Valve's Steamworks developer +pages. Searches of Reddit and the Steam forums turned up **almost no +end-user reports** about SSH, desktop streaming, or macOS. Treat that as +"not documented yet", not "doesn't work". + +## Check on the headset (in order) + +1. **Is Developer Mode available on a retail unit?** Valve's pages are aimed at + developers. Confirm that **Steam Settings → System → Enable Developer Mode** + and **Developer → Set User Password** both exist on your OS channel (Stable + vs Beta). +2. **Does SSH work straight after that, with no terminal steps?** From the Mac, + run `nc -z frame.local 22`, then `./scripts/connect.sh`. +3. **Does `frame.local` resolve from the Mac (mDNS/Avahi)?** If not, use the IP + and set up a DHCP reservation. +4. **Does SSH stay enabled after a reboot and after an OS update?** Also check + that `~/.ssh/authorized_keys` survives an update. +5. **Is the `sshd_config.d` include present?** Check before `--harden`: + `ssh frame 'grep -n Include /etc/ssh/sshd_config'`. +6. **What does Steam Link on macOS show when connected to `frame`?** Is it the + VR view, a flat mirror, or the desktop? Does keyboard/mouse input reach the + headset? +7. **Does the xrdp session work from Microsoft Windows App on macOS?** Valve + only documents Windows Remote Desktop Connection. Is clipboard sync + supported? +8. **What kind of session is the in-headset Linux desktop?** It could be a + normal Plasma Wayland session (with a `wayland-*` socket in + `/run/user/$(id -u)`), X11, or something nested in SteamVR. This decides + whether `paste-to-frame.sh` works. `ssh frame 'ls /run/user/$(id -u); loginctl list-sessions'`. +9. **Are `wl-copy`, `xclip`, and `rsync` present on the image?** + `ssh frame 'command -v wl-copy xclip rsync flatpak'`. +10. **Can Flatpaks be installed `--user` over SSH, and do they appear in the + headset's desktop?** Test with `./scripts/install-apps.sh remmina`. +11. **Remmina → macOS Screen Sharing:** does it connect, and is it usable at + Retina resolutions? Is the pre-seeded profile path + (`~/.var/app/org.remmina.Remmina/data/remmina/`) the one Remmina + actually reads? +12. **Moonlight Flatpak (aarch64) + Sunshine on macOS:** worth trying only if + VNC is too slow. +13. **KDE Connect**: is it preinstalled or installable on the Frame, and does + it pair with KDE Connect for macOS? +14. **Bluetooth keyboard pairing** on the Frame, for the rare times you do need + to type locally. +15. **ADB**: does `adb shell` over USB-C from a Mac (not just a Windows PC) + reach the Linux side? Does USB power from the Mac cope? +16. **Tailscale**: can it be installed persistently (Flatpak? a + userspace `tailscaled` in `~`?) for access off the home LAN? + +## Unconfirmed claims made in these docs + +- `frame.local` works. This comes from one secondary search summary, with no + primary source found. +- `/home` and `/etc` persist across Frame OS updates. This is inferred from + Steam Deck behaviour. +- The whole Mac → Frame desktop path (VNC → Remmina). Each part is documented + separately, but the combination is untested. +- Steam Remote Play with a Mac as host is broken. That's based on community + reports, not tested with the Frame. +- None of the `scripts/` have run against real hardware. They were only + syntax-checked on the Mac (see the commit message). diff --git a/docs/ssh.md b/docs/ssh.md new file mode 100644 index 0000000..1c209c0 --- /dev/null +++ b/docs/ssh.md @@ -0,0 +1,126 @@ +# SSH into the Steam Frame + +Confidence labels: + +- **Confirmed (Frame)**: Valve's Steam Frame docs or a Frame-specific source. +- **Inferred (Deck/SteamOS)**: true on Steam Deck or SteamOS generally, but + not checked on a Frame. +- **Guess**: reasoned, with no source. + +## How access is turned on + +| Claim | Confidence | Source | +|---|---|---| +| **Steam Settings → System → Enable Developer Mode** enables SSH, ADB, and RDP | Confirmed (Frame) | [setup](https://partner.steamgames.com/doc/steamhardware/steamframe/setup), [debugging](https://partner.steamgames.com/doc/steamhardware/steamframe/debugging) | +| A password is set in **Developer → Set User Password**. There is no default password. | Confirmed (Frame) | [setup](https://partner.steamgames.com/doc/steamhardware/steamframe/setup) | +| The default user is **`steamos`**, not `deck` | Confirmed (Frame) | [debugging](https://partner.steamgames.com/doc/steamhardware/steamframe/debugging): `ssh steamos@frame` | +| The default hostname is **`frame`**, and can be changed in **Steam Settings → System → Hostname** | Confirmed (Frame) | [setup](https://partner.steamgames.com/doc/steamhardware/steamframe/setup), [adb_lepton](https://partner.steamgames.com/doc/steamhardware/steamframe/adb_lepton) | +| The IP address is shown in Quick Settings or **Steam Settings → Internet** | Confirmed (Frame) | [adb_lepton](https://partner.steamgames.com/doc/steamhardware/steamframe/adb_lepton) | +| The rootfs is read-only. `sudo steamos-readonly disable` makes it writable. | Confirmed (Frame) | [debugging](https://partner.steamgames.com/doc/steamhardware/steamframe/debugging) | +| `sudo pacman` works. Helper aliases `cdd` (Frame scripts dir), `cdl` (Steam logs), and `lepton` exist. | Confirmed (Frame) | [debugging](https://partner.steamgames.com/doc/steamhardware/steamframe/debugging) | +| There's a full KDE Plasma Linux desktop inside the headset, reachable from the SteamVR dashboard | Confirmed (Frame, press) | [Road to VR review](https://roadtovr.com/valve-steam-frame-review/), [UploadVR](https://www.uploadvr.com/flatpaks-open-source-steam-frame/) | +| On Deck, the manual route is Desktop Mode → Konsole → `passwd` → `sudo systemctl enable --now sshd` | Inferred (Deck) | [pimylifeup](https://pimylifeup.com/steam-deck-ssh/), [gist](https://gist.github.com/chphr/9c0791de6d2c659af3bf5890d9080973) | + +On Deck, SSH needs the manual terminal steps. On the Frame, the Developer Mode +UI handles both the password and the SSH service. That's why the headset-side +checklist in the README involves no terminal at all. + +## Name resolution from a Mac + +Valve's examples use a bare `frame`. That works on Windows through +LLMNR/NetBIOS. **On macOS, a bare single-label name usually doesn't resolve** +unless your router's DNS registers DHCP client names. + +- A secondary source says `frame.local`, a DNS alias, or the IP all work + (search-result summary only, no primary source found). SteamOS on Deck + normally answers `steamdeck.local` over mDNS (Avahi). **Inferred**: the Frame + probably answers `frame.local`. +- `scripts/connect.sh` tries `frame.local`, then `frame`. If neither works, it tells you to re-run it with the IP. + Once you have a working address, the `Host frame` alias means you just type + `ssh frame`. +- To check discovery yourself: `dns-sd -G v4 frame.local` (Ctrl-C to stop), or + `dscacheutil -q host -a name frame.local`. +- A DHCP reservation for the headset on your router makes the IP stable. That's + the most reliable fallback. + +## Key-based login (done by `scripts/connect.sh`) + +```sh +ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_frame -N '' -C "mac->steam-frame" +ssh-copy-id -i ~/.ssh/id_ed25519_frame.pub steamos@frame.local +``` + +`~/.ssh/config` block (managed between marker lines by the script): + +``` +Host frame + HostName frame.local + User steamos + IdentityFile ~/.ssh/id_ed25519_frame + IdentitiesOnly yes + ServerAliveInterval 30 +``` + +`~/.ssh/authorized_keys` lives under `/home`, which SteamOS keeps across OS +updates (inferred from Deck; the Frame uses the same A/B image scheme). + +## Keeping `sshd` enabled across updates + +- **Frame**: SSH is tied to the Developer Mode toggle, so it should survive + updates as long as Developer Mode stays on. (Inferred: Valve doesn't say how + the toggle is implemented.) +- **Deck (for comparison)**: `systemctl enable sshd` usually persists because + `/etc` is an overlay that survives updates. Changes under `/usr` do not. +- Don't `pacman -S` anything you depend on for access. Packages installed into + the read-only rootfs are **wiped by OS updates** on SteamOS. Use Flatpaks + (`--user`) or `~/` for anything that needs to persist. + +## Hardening (optional: `./scripts/connect.sh --harden`) + +The script writes `/etc/ssh/sshd_config.d/01-frame-keys-only.conf` with +`PasswordAuthentication no` and `KbdInteractiveAuthentication no`, then reloads +`sshd`. First, it checks that key login works in BatchMode, so you can't lock +yourself out. + +- Needs `sudo` (Developer Mode password), entered on the **Mac**. +- It assumes `/etc/ssh/sshd_config` includes `sshd_config.d/*.conf`, which is + the Arch default. The script checks for this and stops if the include is + missing. +- `/etc` drop-ins normally persist across SteamOS updates (inferred from Deck). +- It doesn't affect RDP (xrdp) or `sudo`, which still use the password. +- Undo: `ssh frame 'sudo rm /etc/ssh/sshd_config.d/01-frame-keys-only.conf && sudo systemctl reload sshd'`. + +## Other shells + +- **ADB over USB-C** to the native Linux OS: + `adb shell`. Plug the headset into the Mac. Valve notes that USB power may be + insufficient. Install with `brew install android-platform-tools`. This is + useful if Wi-Fi SSH is broken. + (Confirmed (Frame): [debugging](https://partner.steamgames.com/doc/steamhardware/steamframe/debugging)) +- **ADB over Wi-Fi** reaches the **Lepton (Android) container**, not Linux: + `adb connect frame:5555`. It only works while "Lepton Development" or an + Android app is running. + (Confirmed (Frame): [adb_lepton](https://partner.steamgames.com/doc/steamhardware/steamframe/adb_lepton)) +- **RDP**: xrdp with user `steamos` and the Developer Mode password (see + [streaming.md](streaming.md)). + +## Fallback bootstrap one-liner + +Use this only if the Developer Mode toggle doesn't give you SSH (for example, +an OS build without it). + +1. On the Mac: `./scripts/serve-bootstrap.sh`. It serves + `bootstrap-on-frame.sh`, with your `~/.ssh/id_ed25519_frame.pub` embedded, + on port 8765, and prints the exact one-liner. +2. On the Frame's Linux desktop, open **Konsole** and type the printed line, + roughly `curl -fsS mac.local:8765|bash` (~30 characters). If `mac.local` + doesn't resolve, the script prints an IP form instead. +3. The bootstrap installs the key into `~steamos/.ssh/authorized_keys`, and + then runs `sudo systemctl enable --now sshd`. `sudo` asks for a password, + and if none is set yet, it tells you to run `passwd` first. That means + typing the password on the headset one more time. +4. Stop the server on the Mac with Ctrl-C. + +This is plain HTTP on your LAN, and it only serves a public key, so the +content isn't secret. Anyone on the LAN who can spoof your Mac's address could +serve a different script, though, so use it only on a trusted network. diff --git a/docs/streaming.md b/docs/streaming.md new file mode 100644 index 0000000..aa2b7e3 --- /dev/null +++ b/docs/streaming.md @@ -0,0 +1,61 @@ +# Screen and desktop streaming + +This covers two directions: + +- **A. Frame → Mac**: see and control the headset from the Mac. +- **B. Mac → Frame**: use the Mac's desktop inside the headset. + +The confidence labels are the same as in [ssh.md](ssh.md). + +## A. See and control the Frame from the Mac + +| Option | What you get | Confidence | Notes | +|---|---|---|---| +| **Steam Link (macOS app) → `frame`** | A remote view of the headset | **Confirmed (Frame)**: Valve says to "use Steam Link on iOS, Android, or desktop to view the headset remotely by connecting to 'frame'" ([debugging](https://partner.steamgames.com/doc/steamhardware/steamframe/debugging)) | Steam Link for macOS exists ([Tom's Guide](https://www.tomsguide.com/news/macbook-gaming-just-got-a-killer-upgrade-with-steam-link-heres-how-it-looks)). It's the lowest-effort option. Whether you get the VR view or a flat mirror, and whether input works, is unverified. | +| **RDP to xrdp** | A separate Linux (Xorg) desktop session as `steamos` | **Confirmed (Frame)** for the server ([debugging](https://partner.steamgames.com/doc/steamhardware/steamframe/debugging)); **Inferred** for the Mac client | On the Mac, use Microsoft **Windows App** (the old "Microsoft Remote Desktop") from the App Store. Add PC `frame.local` (or the IP), user `steamos`, and the Developer Mode password. Valve says Xorg is the default session. This is a *separate* X session, not a mirror of what's in the headset. It's good for running GUI apps and supports clipboard sync. | +| **ADB + scrcpy (Lepton only)** | A mirror of the Android container | **Guess** | `brew install scrcpy android-platform-tools`, then `adb connect frame.local:5555` while Lepton Development is running ([adb_lepton](https://partner.steamgames.com/doc/steamhardware/steamframe/adb_lepton)), then `scrcpy`. This only shows Android apps, not SteamOS. | +| VNC server on the Frame (krfb / wayvnc) | A mirror of the Plasma desktop | **Inferred (SteamOS)** | Deck users run krfb in Desktop Mode ([one.vg](https://one.vg/blog/remote-control-your-steam-deck)). On the Frame, the in-headset desktop is a virtual screen, and krfb isn't known to be preinstalled. RDP and Steam Link cover this case, so it's not recommended. | + +**Recommendation for A:** start with Steam Link for macOS, because Valve +documents it. Use Windows App (RDP) when you want a proper Linux desktop on the +Mac with keyboard, mouse, and clipboard. + +## B. Show the Mac's desktop inside the Frame + +The Frame's streaming features are built around a **Windows PC running +SteamVR** plus the USB Wi-Fi 6E dongle. Even Linux hosts had VR-streaming +problems at launch +([Steam discussion](https://steamcommunity.com/app/4165890/discussions/0/528765047224280796/), +[gbl08ma](https://gbl08ma.com/posts/steam-frame-a-linux-machine-doesnt-support-linux/)). +**macOS isn't a supported SteamVR host**, so for the Mac we're only looking at +flat 2D desktop streaming into a window on the Frame's Linux desktop. + +| Option | Setup | Confidence | Verdict | +|---|---|---|---| +| **macOS Screen Sharing (VNC) → Remmina on the Frame** | **Mac:** System Settings → General → Sharing → Screen Sharing on → (i) → enable "VNC viewers may control screen with password". **Frame:** `./scripts/install-apps.sh remmina` from the Mac, then open Remmina in the headset and connect to `vnc://.local` | **Inferred.** Remmina is on Flathub for **aarch64** with VNC and RDP ([Flathub](https://flathub.org/apps/org.remmina.Remmina)). The Frame desktop runs Flatpaks ([UploadVR](https://www.uploadvr.com/flatpaks-open-source-steam-frame/)). macOS VNC is built in. | **Recommended.** Nothing to install on the Mac, and it's easy to set up. Latency is fine for productivity but not for games. You'll type the Mac's hostname once in Remmina on the headset, then save the profile. To avoid even that, the script can pre-seed a Remmina profile over SSH (see below). | +| Sunshine (Mac) → Moonlight (Frame Flatpak) | `brew install` Sunshine on the Mac, then `./scripts/install-apps.sh moonlight` | Moonlight Flatpak supports **aarch64** ([Flathub](https://flathub.org/apps/com.moonlight_stream.Moonlight)). **Sunshine on macOS is poorly supported**: install problems on Apple Silicon/Sequoia, and no virtual gamepads ([LizardByte discussion #777](https://github.com/orgs/LizardByte/discussions/777)). | Try it if VNC is too laggy. Expect some friction. | +| Steam Remote Play with the Mac as host | Steam on the Mac, Steam Link/Remote Play on the Frame | macOS-hosted Remote Play is reported broken or flaky in 2024–2026 ([Steam discussion](https://steamcommunity.com/groups/homestream/discussions/1/574921459914429988/)) | Not recommended. It's only for games, if it works at all. | +| Immersed / Virtual Desktop | Vendor apps | Immersed has a Mac agent but no known Frame client. Virtual Desktop's developer said he'd "try" to port it ([NewsBreak](https://www.newsbreak.com/news/4892834783961-virtual-desktop-dev-says-he-ll-try-to-bring-the-app-to-steam-frame)). | Not available as of 2026-09-25. Check again later. | +| WiVRn / ALVR | VR streaming from a Linux or Windows PC | Irrelevant for a Mac host (no SteamVR/OpenXR runtime on macOS) | N/A | + +### Pre-seeding the Remmina profile (no typing in the headset) + +`scripts/install-apps.sh remmina --vnc-host .local` writes +`~/.var/app/org.remmina.Remmina/data/remmina/mac-screen-sharing.remmina` on the Frame over +SSH. The profile then appears in Remmina's list, and you just click it. You'll +still be asked for the VNC password in the headset the first time, unless you +choose to save it. Remmina stores passwords encrypted with a per-install key, +so the script doesn't try to write the password. (The Remmina file format is +standard; the Flatpak data path is inferred.) + +## Input and text entry without the virtual keyboard + +- **A Bluetooth keyboard and mouse** paired to the Frame is the obvious way to + avoid the virtual keyboard. Road to VR says there are "only a few things + you'd actually want to do" on the Linux desktop unless you connect a + keyboard and mouse. + (Pairing a BT keyboard on the Frame is inferred from SteamOS; not verified.) +- **Clipboard from the Mac**: `scripts/paste-to-frame.sh` (see + [file-transfer.md](file-transfer.md#clipboard)). +- **RDP session**: Windows App syncs the clipboard with xrdp, but only inside + that RDP session. diff --git a/scripts/bootstrap-on-frame.sh b/scripts/bootstrap-on-frame.sh new file mode 100755 index 0000000..735eea6 --- /dev/null +++ b/scripts/bootstrap-on-frame.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Runs ON the Steam Frame (fallback path only; normally Developer Mode's +# toggle + "Set User Password" is enough and this is not needed). +# Served by scripts/serve-bootstrap.sh, which substitutes the public key. +# +# UNTESTED against real hardware. Idempotent. +set -eu + +KEY='__PUBKEY__' + +mkdir -p "$HOME/.ssh" +chmod 700 "$HOME/.ssh" +touch "$HOME/.ssh/authorized_keys" +chmod 600 "$HOME/.ssh/authorized_keys" +if grep -qxF "$KEY" "$HOME/.ssh/authorized_keys"; then + echo "key already present" +else + echo "$KEY" >> "$HOME/.ssh/authorized_keys" + echo "key added" +fi + +echo "Enabling sshd. If sudo asks for a password you never set, press Ctrl-C," +echo "set one in Steam Settings > Developer > Set User Password (or run: passwd)," +echo "then re-run the same one-liner." +sudo systemctl enable --now sshd + +echo +echo "sshd: $(systemctl is-active sshd) user: $(id -un) host: $(hostname)" +ip -4 -brief addr show scope global 2>/dev/null || true +echo "Now on the Mac: scripts/connect.sh" diff --git a/scripts/connect.sh b/scripts/connect.sh new file mode 100755 index 0000000..79ef887 --- /dev/null +++ b/scripts/connect.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env zsh +# Mac-side: find the Steam Frame, create a key, add a `Host frame` alias to +# ~/.ssh/config, copy the key, and optionally disable SSH password logins. +# +# UNTESTED against real hardware. Idempotent: safe to re-run. +# +# Usage: +# scripts/connect.sh [HOST_OR_IP] # set up key + alias +# scripts/connect.sh [HOST_OR_IP] --harden # also disable password auth +# +# Env: FRAME_USER (default steamos), FRAME_ALIAS (default frame). +set -euo pipefail + +FRAME_USER=${FRAME_USER:-steamos} +FRAME_ALIAS=${FRAME_ALIAS:-frame} +KEY="$HOME/.ssh/id_ed25519_frame" +CONFIG="$HOME/.ssh/config" +BEGIN_MARK="# >>> steam-frame ($FRAME_ALIAS) >>>" +END_MARK="# <<< steam-frame ($FRAME_ALIAS) <<<" + +harden=0 +host_arg="" +for arg in "$@"; do + case "$arg" in + --harden) harden=1 ;; + -h|--help) sed -n '2,11p' "$0"; exit 0 ;; + *) host_arg="$arg" ;; + esac +done + +port_open() { + # nc resolves through the system resolver (including mDNS for .local). + nc -z -G 3 "$1" 22 >/dev/null 2>&1 +} + +pick_host() { + local candidates=() + [[ -n "$host_arg" ]] && candidates+=("$host_arg") + candidates+=("$FRAME_ALIAS.local" "$FRAME_ALIAS") + local h + for h in "${candidates[@]}"; do + if port_open "$h"; then + print -r -- "$h"; return 0 + fi + print -u2 " - $h: not resolvable or port 22 closed" + done + return 1 +} + +print "==> Looking for the Steam Frame" +if ! HOST=$(pick_host); then + print -u2 "Could not reach the Frame on port 22." + print -u2 "Check: Developer Mode on + user password set; same Wi-Fi; no client isolation." + print -u2 "Then re-run with the IP from Quick Settings: scripts/connect.sh 192.168.x.y" + exit 1 +fi +print " found: $HOST" + +print "==> SSH key" +mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" +if [[ ! -f "$KEY" ]]; then + ssh-keygen -q -t ed25519 -N '' -C "mac->steam-frame" -f "$KEY" + print " created $KEY" +else + print " exists: $KEY" +fi + +print "==> ~/.ssh/config alias '$FRAME_ALIAS' -> $HOST" +touch "$CONFIG" && chmod 600 "$CONFIG" +tmp=$(mktemp) +# Drop any previous managed block, then PREPEND a fresh one: ssh uses the first +# value it sees per option, so this block must precede any other "Host frame" +# or "Host *". The trailing "Host *" returns the rest of the file to global scope. +awk -v b="$BEGIN_MARK" -v e="$END_MARK" ' + $0==b {skip=1; next} + $0==e {skip=0; next} + !skip {print} +' "$CONFIG" > "$tmp" +{ + print -r -- "$BEGIN_MARK" + print -r -- "Host $FRAME_ALIAS" + print -r -- " HostName $HOST" + print -r -- " User $FRAME_USER" + print -r -- " IdentityFile $KEY" + print -r -- " IdentitiesOnly yes" + print -r -- " ServerAliveInterval 30" + print -r -- "Host *" + print -r -- "$END_MARK" + cat "$tmp" +} > "$CONFIG" +rm -f "$tmp" + +print "==> Checking key login" +if ssh -o BatchMode=yes -o ConnectTimeout=5 "$FRAME_ALIAS" true 2>/dev/null; then + print " key login already works" +else + print " copying key (enter the Developer Mode password once)" + ssh-copy-id -i "$KEY.pub" -o IdentitiesOnly=yes "$FRAME_USER@$HOST" + ssh -o BatchMode=yes -o ConnectTimeout=5 "$FRAME_ALIAS" true \ + || { print -u2 "Key login still failing after ssh-copy-id."; exit 1; } + print " key login OK" +fi + +if (( harden )); then + print "==> Disabling SSH password auth (sudo password asked on the Frame)" + # shellcheck disable=SC2016 + if ! ssh -t "$FRAME_ALIAS" ' + set -e + grep -Eiq "^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf" /etc/ssh/sshd_config \ + || { echo "sshd_config has no sshd_config.d include; not hardening."; exit 1; } + printf "PasswordAuthentication no\nKbdInteractiveAuthentication no\n" \ + | { sudo mkdir -p /etc/ssh/sshd_config.d; sudo tee /etc/ssh/sshd_config.d/01-frame-keys-only.conf >/dev/null; } + sudo sshd -t + sudo systemctl reload sshd + echo "password auth disabled" + '; then + print -u2 "!! Hardening failed. If the drop-in was written, it will disable password SSH" + print -u2 "!! on the next sshd restart. To undo it:" + print -u2 "!! ssh $FRAME_ALIAS 'sudo rm -f /etc/ssh/sshd_config.d/01-frame-keys-only.conf'" + exit 1 + fi + if ssh -o BatchMode=yes -o ConnectTimeout=5 "$FRAME_ALIAS" true; then + print " key login still OK after hardening" + else + print -u2 "!! Key login FAILED after hardening. Password SSH is now off." + print -u2 "!! Recover via RDP or 'adb shell' (USB-C), then run:" + print -u2 "!! sudo rm /etc/ssh/sshd_config.d/01-frame-keys-only.conf && sudo systemctl reload sshd" + exit 1 + fi +fi + +print "\nDone. Try: ssh $FRAME_ALIAS" diff --git a/scripts/install-apps.sh b/scripts/install-apps.sh new file mode 100755 index 0000000..e6d9c46 --- /dev/null +++ b/scripts/install-apps.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env zsh +# Mac-side: install Flatpaks on the Steam Frame over SSH (per-user, so they +# survive SteamOS updates and need no sudo / steamos-readonly changes). +# +# UNTESTED against real hardware. Idempotent. +# +# Usage: +# scripts/install-apps.sh remmina [--vnc-host my-mac.local] +# scripts/install-apps.sh moonlight +# scripts/install-apps.sh org.example.SomeApp # any Flathub app ID +# +# --vnc-host pre-seeds a Remmina profile pointing at the Mac's built-in +# Screen Sharing (VNC, port 5900) so nothing needs typing in the headset. +set -euo pipefail + +FRAME_ALIAS=${FRAME_ALIAS:-frame} +vnc_host="" +apps=() + +while (( $# )); do + case "$1" in + --vnc-host) vnc_host=${2:?--vnc-host needs a hostname}; shift 2 + [[ "$vnc_host" =~ '^[A-Za-z0-9.-]+$' ]] || { print -u2 "Bad hostname: $vnc_host"; exit 2; } ;; + -h|--help) sed -n '2,13p' "$0"; exit 0 ;; + remmina) apps+=(org.remmina.Remmina); shift ;; + moonlight) apps+=(com.moonlight_stream.Moonlight); shift ;; + [A-Za-z]*.*[A-Za-z0-9_]) [[ "$1" =~ '^[A-Za-z0-9_.-]+$' ]] || { print -u2 "Bad app ID: $1"; exit 2; }; apps+=("$1"); shift ;; + *) print -u2 "Unknown app '$1' (use remmina, moonlight, or a Flathub app ID)"; exit 2 ;; + esac +done + +if (( ${#apps} == 0 )) && [[ -z "$vnc_host" ]]; then + sed -n '2,13p' "$0"; exit 2 +fi + +if (( ${#apps} )); then + print "==> Installing on $FRAME_ALIAS: ${apps[*]}" + ssh "$FRAME_ALIAS" " + set -e + flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo + flatpak install --user -y flathub ${(j: :)${(@q)apps}} + " +fi + +if [[ -n "$vnc_host" ]]; then + print "==> Writing Remmina profile for vnc://$vnc_host" + ssh "$FRAME_ALIAS" " + set -e + d=\$HOME/.var/app/org.remmina.Remmina/data/remmina + mkdir -p \"\$d\" + cat > \"\$d/mac-screen-sharing.remmina\" <<'EOF' +[remmina] +name=Mac Screen Sharing +protocol=VNC +server=$vnc_host:5900 +colordepth=32 +quality=9 +viewonly=0 +showcursor=1 +EOF + echo \"wrote \$d/mac-screen-sharing.remmina\" + " + print "On the Mac: System Settings > General > Sharing > Screen Sharing (i) >" + print " enable 'VNC viewers may control screen with password' and set one." +fi diff --git a/scripts/paste-to-frame.sh b/scripts/paste-to-frame.sh new file mode 100755 index 0000000..78322cd --- /dev/null +++ b/scripts/paste-to-frame.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env zsh +# Mac-side: put text on the Steam Frame desktop clipboard. +# +# UNTESTED against real hardware. Assumes the in-headset desktop is a Plasma +# session owned by the SSH user; prints diagnostics if that assumption fails. +# +# Usage: +# scripts/paste-to-frame.sh # sends the Mac clipboard (pbpaste) +# some-cmd | scripts/paste-to-frame.sh - +set -euo pipefail + +FRAME_ALIAS=${FRAME_ALIAS:-frame} + +# Runs on the Frame. Clipboard text arrives on stdin. setsid keeps the +# clipboard-serving process alive after the SSH session closes. +remote=$(cat <<'EOF' +set -u +tmp=$(mktemp) +cat > "$tmp" +rt=/run/user/$(id -u) +sock=$(ls "$rt" 2>/dev/null | grep -E '^wayland-[0-9]+$' | head -n 1) +if [ -n "$sock" ] && command -v wl-copy >/dev/null 2>&1 \ + && XDG_RUNTIME_DIR=$rt WAYLAND_DISPLAY=$sock setsid wl-copy < "$tmp" >/dev/null 2>&1; then + echo "copied via wl-copy ($sock)" +elif command -v xclip >/dev/null 2>&1 \ + && DISPLAY=:0 setsid xclip -selection clipboard -i < "$tmp" >/dev/null 2>&1; then + echo "copied via xclip (DISPLAY=:0)" +else + echo "clipboard copy failed; diagnostics:" >&2 + echo " runtime dir: $(ls "$rt" 2>&1 | tr '\n' ' ')" >&2 + echo " wl-copy: $(command -v wl-copy || echo missing) xclip: $(command -v xclip || echo missing)" >&2 + loginctl list-sessions --no-legend 2>&1 | sed 's/^/ session: /' >&2 + rm -f "$tmp"; exit 2 +fi +rm -f "$tmp" +EOF +) +b64=$(print -rn -- "$remote" | base64) + +if [[ "${1:-}" == "-" ]]; then + ssh "$FRAME_ALIAS" "bash -c \"\$(echo $b64 | base64 -d)\"" +else + pbpaste | ssh "$FRAME_ALIAS" "bash -c \"\$(echo $b64 | base64 -d)\"" +fi diff --git a/scripts/push.sh b/scripts/push.sh new file mode 100755 index 0000000..2fc4676 --- /dev/null +++ b/scripts/push.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env zsh +# Mac-side: copy a file or folder to the Steam Frame. +# +# UNTESTED against real hardware. +# +# Usage: scripts/push.sh SOURCE [REMOTE_DEST] (default dest: ~/Downloads/) +set -euo pipefail + +FRAME_ALIAS=${FRAME_ALIAS:-frame} +src=${1:?usage: push.sh SOURCE [REMOTE_DEST]} +dest=${2:-Downloads/} + +if ssh "$FRAME_ALIAS" 'command -v rsync >/dev/null'; then + rsync -a --progress "$src" "$FRAME_ALIAS:${(q)dest}" # remote shell parses the path +else + print -u2 "rsync not found on the Frame; falling back to scp" + scp -r "$src" "$FRAME_ALIAS:$dest" # modern scp uses SFTP: no remote shell parsing +fi diff --git a/scripts/serve-bootstrap.sh b/scripts/serve-bootstrap.sh new file mode 100755 index 0000000..f81be34 --- /dev/null +++ b/scripts/serve-bootstrap.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env zsh +# Mac-side (fallback only): serve bootstrap-on-frame.sh over plain HTTP on the +# LAN, with this Mac's Frame public key embedded, and print the short +# one-liner to type in Konsole on the headset. Ctrl-C to stop. +# +# UNTESTED against real hardware. Serves only a public key; use on a trusted LAN. +set -euo pipefail + +PORT=${PORT:-8765} +here=${0:A:h} +KEY="$HOME/.ssh/id_ed25519_frame" + +if [[ ! -f "$KEY.pub" ]]; then + mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" + ssh-keygen -q -t ed25519 -N '' -C "mac->steam-frame" -f "$KEY" +fi +pub=$(<"$KEY.pub") + +dir=$(mktemp -d) +trap 'rm -rf "$dir"' EXIT +# index.html so the bare URL works; curl doesn't care about the name. +sed "s|__PUBKEY__|$pub|" "$here/bootstrap-on-frame.sh" > "$dir/index.html" + +name="$(scutil --get LocalHostName 2>/dev/null || hostname -s).local" +ip=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || true) + +print "Type ONE of these in Konsole on the Frame:" +print " curl -fsS $name:$PORT|bash" +[[ -n "$ip" ]] && print " curl -fsS $ip:$PORT|bash" +print "Serving from $dir on port $PORT (Ctrl-C to stop)..." +python3 -m http.server "$PORT" --directory "$dir"