cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Document 012 serve: setup, security model and limits

Dami committed 2c61af0
docs/ssh.md covers the flags, setup, what is open and closed, per
session state, two sessions on one file, the ssh.* telemetry events and
the limits. The architecture, files, testing and docs index pages, the
README and the roadmap (SSH mode done, live sharing still shelved)
point to it.
commit
2c61af0831f94d0570cf114cf46224188750294f
parent
92ff13232dac807d7f1c00c991f04c67d6687d20
author
Dami <dami.etoile@gmail.com>
7 files changed, +177 -4
MREADME.md2+
MROADMAP.md6+-
Mdocs/README.md1+
Mdocs/architecture.md24+++++-
Mdocs/files.md5+-
Adocs/ssh.md139++++++++++++++++++++++++++++++
Mdocs/testing.md4+
M README.md +2 -0
M ROADMAP.md +4 -2
M docs/README.md +1 -0
M docs/architecture.md +23 -1
@@ -4,7 +4,7 @@
44 Lip Gloss v2.55 66 ```7-cmd/012 entry point: flags, JEV setup, opening or importing a file7+cmd/012 entry point: flags, JEV setup, opening or importing a file, 012 serve88 internal/sheet the engine: cells, evaluation, functions, recalculation, undo, files99 internal/formula the formula language: references, lexer, parser, printer, rewriting1010 internal/numfmt number formats, rounding, General, date serials
@@ -12,6 +12,8 @@ internal/fileio import and export: CSV, TSV, XLSX, SQLite, Parquet, Lotus .wk1
1212 internal/chart chart layout, text rendering and kitty image encoding1313 internal/jev JEV configuration, answer cache and TypeSafe client1414 internal/telemetry opt-in JSON log and OTLP export of spans, events and frame stats15+internal/serve the SSH server: auth, host key, a Model per session (charm.land/wish/v2)16+internal/confine resolving typed file names, confined to a directory when served1517 internal/stress synthetic worst-case sheets for the -tags stress benchmarks1618 internal/ui the Bubble Tea model: modes, menus, overlays, rendering1719 theme style roles and the widgets drawn with them (frames, key chips)
@@ -203,6 +205,26 @@ saved with sheets. Work is bounded by the chart's size, not its data: only
203205 the categories that fit are drawn, pie slices are found by binary search,204206 and a pie image supersamples only pixels on a slice edge or the rim.205207 208+## Serving over SSH209+210+`internal/serve` wraps `charm.land/ssh` (wish's server) with only a211+session channel, public-key auth against authorized_keys and a shell212+request allowed. Each session builds a `ui.Model` of its own and runs it213+in its own `tea.Program` over the session (wish's emulated PTY), with214+the client's environment and window size; `Model.Serve` gives it the215+client's environment for terminal detection and a `confine.Root` for216+file names. Everything a model knows lives in the model, so sessions217+share nothing but read-only tables (the command and function218+registries), the process-wide telemetry and, with JEV on, the HTTP219+client; each gets its own `jev.Cache`, whose queue belongs to that220+session's program. See [ssh.md](ssh.md).221+222+**File names.** The UI keeps names as typed and turns them into paths223+only to read or write, through the model's `confine.Root`: the zero224+root (the local app) uses them as they are, a served session's root225+keeps them inside its directory. Every open, save, import, download and226+file listing goes through `Model.path` or the root.227+206228 ## Telemetry207229 208230 `internal/telemetry` is off unless asked for. When on, spans, events and
M docs/files.md +4 -1
A docs/ssh.md +139 -0
@@ -0,0 +1,139 @@
1+# Serving over SSH2+3+`012 serve` runs an SSH server that gives every session its own 012, in4+the client's own terminal, on the files of one directory. It's for5+reaching your sheets from another machine or a tablet with an SSH app,6+without installing anything there. It is not shared editing: two7+sessions are two separate spreadsheets (see8+[the roadmap](../ROADMAP.md#later-sharing-a-live-sheet-shelved)).9+10+```sh11+012 serve ~/sheets # serve ~/sheets on 127.0.0.1:231212+ssh -p 2312 localhost # from this machine13+```14+15+```16+012 serve [--listen addr] [--authorized-keys file] [--host-key file]17+ [--idle-timeout 30m] [--max-sessions 8] [--log file] [--otlp url] [dir]18+```19+20+| Flag | Default | What |21+|---|---|---|22+| `dir` | `.` | The served directory. Sessions open, save, import and download only inside it |23+| `--listen` | `127.0.0.1:2312` | Address to listen on. Loopback only unless you change it |24+| `--authorized-keys` | `~/.ssh/authorized_keys` | Public keys allowed in, in OpenSSH's format |25+| `--host-key` | `<config dir>/012/ssh_host_ed25519_key` | The server's key, generated on first run |26+| `--idle-timeout` | `30m` | End a session after this long without input; `0` never does |27+| `--max-sessions` | `8` | Sessions at once; more are turned away with a message |28+| `--log`, `--otlp` | off | Telemetry, as for the app ([observability.md](observability.md)) |29+30+The config directory is `os.UserConfigDir()`: `~/Library/Application31+Support` on macOS, `~/.config` on Linux. On start, 012 prints the served32+directory, the address, the host key's fingerprint and the command to33+connect.34+35+## Setup36+37+1. Put the public keys that may connect in an authorized_keys file. Your38+ own `~/.ssh/authorized_keys` works, or keep a separate one for 012:39+40+ ```sh41+ cat ~/.ssh/id_ed25519.pub > ~/.config/012/authorized_keys42+ chmod 600 ~/.config/012/authorized_keys43+ 012 serve --authorized-keys ~/.config/012/authorized_keys ~/sheets44+ ```45+46+2. Connect with `ssh -p 2312 host`. The first time, check the host key47+ fingerprint ssh shows against the one 012 printed, or add the key to48+ `known_hosts` yourself from `ssh_host_ed25519_key.pub` next to it.49+50+3. To reach it from another machine, prefer keeping 012 on loopback and51+ tunnelling through the machine's own sshd52+ (`ssh -L 2312:127.0.0.1:2312 host`, then `ssh -p 2312 localhost`), or53+ a private network such as Tailscale with `--listen 100.x.y.z:2312`.54+ Listening beyond loopback prints a warning.55+56+Stop the server with Ctrl+C. Running sessions end with it, and unsaved57+changes in them are lost.58+59+## Security model60+61+The server exposes the files of one directory to whoever holds one of62+the authorized keys. Everything else is closed.63+64+- **Authentication.** Public keys only, checked against the65+ authorized_keys file, which is read again on every login so edits66+ apply at once. No passwords, no keyboard-interactive, no certificates.67+ The file must not be writable by group or others, as with sshd's68+ `StrictModes`. Keys carrying options 012 can't honor (`command=`,69+ `from=`, `no-pty`, `restrict` without `pty`, `expiry-time=`,70+ `cert-authority` and so on) are skipped, so a key restricted to, say,71+ a backup command never gets a spreadsheet. The user name is logged but72+ not checked.73+- **Host key.** An ed25519 key generated on first run, written `0600`74+ in a `0700` directory. 012 refuses to start if the key is readable by75+ others.76+- **What a session can do.** Run 012, nothing else. Exec requests77+ (`ssh host cat /etc/passwd`), subsystems (`sftp`, `scp`), local and78+ remote port forwarding and X11 are refused; agent forwarding requests79+ are accepted by the SSH library but never used. A session without a80+ terminal is told to use `ssh -t` and closed.81+- **Files.** Names typed in File > Open, Save as, Import and Download82+ resolve inside the served directory. Refused, with "outside the served83+ directory": absolute paths, `..` that climbs out, hidden files and84+ directories (`.env`, `.git`, `.ssh`), and symbolic links that lead85+ outside or nowhere. Links that stay inside work. Error messages name86+ files relative to the served directory, not where it is on the87+ server. Files are written with the server's user and permissions.88+- **JEV.** On only when the server process's own environment has89+ `TYPESAFE_API_KEY`; sessions never read `.env` files. Every session90+ has its own answer cache, and all of them spend the server's key.91+- **Logs.** Logins, rejected keys, failed handshakes, sessions turned92+ away and session ends go to stderr and to telemetry as `ssh.*` events93+ with the user name, the key's SHA256 fingerprint and the remote94+ address, never file contents or cells.95+96+| Event | When | Attributes |97+|---|---|---|98+| `ssh.session` | a session starts | `user`, `key`, `remote`, `term`, `width`, `height` |99+| `ssh.session_end` | it ends | `user`, `key`, `remote`, `duration`, `idle` (closed for idling) |100+| `ssh.rejected` | a key not in authorized_keys | `user`, `key`, `remote` |101+| `ssh.full` | a session turned away at `--max-sessions` | `user`, `key`, `remote` |102+| `ssh.failed` | a handshake or login fails | `remote`, `error` |103+| `ssh.auth_error` | authorized_keys can't be read or parsed | `error` |104+105+The `frames` summaries and the `ssh_sessions` gauge cover every session106+together: telemetry is per process.107+108+What it doesn't protect against: someone who can already write to the109+served directory on the server (a symbolic link swapped in between the110+check and the open), and anything a holder of an authorized key does111+with the files they're given.112+113+## Each session114+115+Every session is a separate 012 with nothing shared but the process:116+its own workbook, undo history, clipboard, theme, JEV cache and117+terminal state. What it learns about the terminal comes from the118+client: the window size and its changes, the colors (from the `TERM`119+the client sends), the light or dark background, kitty graphics, and120+the clipboard, which Ctrl+C sets on the client's machine through OSC 52.121+122+## Two sessions, one file123+124+Opening the same file in two sessions gives two copies. Saving over a125+file that changed on disk since it was opened or last saved in this126+session (someone else saved it) asks first on the context line:127+Enter overwrites it, S saves under another name, Esc cancels. The same128+check protects the local app from other programs writing the file.129+130+## Limits131+132+- Sessions start on a new sheet; there's no way to open a file from the133+ ssh command line, since commands are refused.134+- An idle session is closed without saving.135+- Each session holds about 1.3 MiB of heap on a new sheet, plus about136+ 300 B per cell of the sheets it opens; two sessions opening one file137+ hold two copies. With 50 sessions typing at once on loopback, frames138+ still arrive within one frame interval (p95 16.6 ms), the same as with139+ 10: see [limits.md](limits.md#serving-over-ssh).
M docs/testing.md +4 -0