@vibelord/lanes 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -15
- package/bin/lanes-install.mjs +8 -2
- package/lib/plan.mjs +34 -1
- package/package.json +2 -2
- package/payload/lanes/README.md +39 -18
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +994 -49
- package/payload/lanes/client/lanes-launch +160 -14
- package/payload/lanes/client/ssh.stanza.in +11 -2
- package/payload/lanes/docs/ARCHITECTURE.md +86 -2
- package/payload/lanes/docs/INSTALL.md +296 -43
- package/payload/lanes/docs/RUNBOOK.md +453 -27
- package/payload/lanes/docs/USERGUIDE.md +449 -76
- package/payload/lanes/install/enable-phone.sh +8 -20
- package/payload/lanes/install/enable-web.sh +2 -2
- package/payload/lanes/install/host-install.sh +6 -0
- package/payload/lanes/install.sh +888 -52
- package/payload/lanes/libexec/enrol.py +695 -0
- package/payload/lanes/libexec/enrold.py +1264 -0
- package/payload/lanes/libexec/keys.py +644 -0
- package/payload/lanes/libexec/lanes_lib.py +14 -6
- package/payload/lanes/libexec/members.py +297 -0
- package/payload/lanes/libexec/notify.py +146 -21
- package/payload/lanes/libexec/qr.py +217 -0
- package/payload/lanes/libexec/site_edit.py +237 -0
- package/payload/lanes/libexec/web_tokens.py +315 -0
- package/payload/lanes/site/README.md +13 -1
- package/payload/lanes/site/beyondwork/site.conf +3 -0
- package/payload/lanes/site/site.example.conf +5 -0
- package/payload/lanes/sudo/one-time-root.sh +1 -1
- package/payload/lanes/systemd/lanes-enrold.service +22 -0
- package/payload/lanes/systemd/lanes-notify.service +4 -4
- package/payload/lanes/systemd/lanes-web.service +4 -0
- package/payload/lanes/web/Caddyfile +10 -0
- package/payload/lanes/web/README.md +8 -2
- package/payload/lanes/workspaces.example/README.md +6 -1
- package/payload/manifest.json +7 -4
|
@@ -20,7 +20,7 @@ Labels used below:
|
|
|
20
20
|
## Contents
|
|
21
21
|
|
|
22
22
|
1. [What Lanes is](#1-what-lanes-is)
|
|
23
|
-
2. [
|
|
23
|
+
2. [Join a site](#2-join-a-site)
|
|
24
24
|
3. [Desktop use](#3-desktop-use)
|
|
25
25
|
4. [Phone, tablet and laptop browser](#4-phone-tablet-and-laptop-browser)
|
|
26
26
|
5. [Uploads](#5-uploads)
|
|
@@ -107,24 +107,105 @@ fleet economy strip (overflow spend against the daily cap, new 429s, model tiers
|
|
|
107
107
|
economy cell per lane (`⚠` starving, `↓` downgraded, `$` mostly paid). Throttle's **Shared lanes** console and the
|
|
108
108
|
co-located Studio Host (section 10) give read-only and browser access to registered lanes through Throttle SSO.
|
|
109
109
|
|
|
110
|
-
## 2.
|
|
110
|
+
## 2. Join a site
|
|
111
|
+
|
|
112
|
+
A site is one Lanes server and the people who use it. Joining takes three things: the site's tailnet, the client, and
|
|
113
|
+
this machine's key authorized on the server. With an invite, one command does the last two. The administrator's side
|
|
114
|
+
(the server install, invites, approvals, updates) follows the client's.
|
|
111
115
|
|
|
112
116
|
### What you need
|
|
113
117
|
|
|
114
118
|
| Role | Requirements |
|
|
115
119
|
| --- | --- |
|
|
116
|
-
| Client (Mac or Linux desktop) | macOS or Linux, OpenSSH, a route to the server (Tailscale for the supplied profile). Node.js 18+ only for the npm path. Ghostty is recommended for a separate window (Linux falls back to Foot); `lanes-launch` works in any terminal |
|
|
120
|
+
| Client (Mac or Linux desktop) | macOS or Linux, OpenSSH, a route to the server (Tailscale for the supplied profile; the installer checks it and opens its login, see "Tailscale" below). Node.js 18+ only for the npm path. Ghostty is recommended for a separate window (Linux falls back to Foot); `lanes-launch` works in any terminal |
|
|
117
121
|
| Server (workspace host) | Linux x86_64, Python 3.9+, systemd user services, OpenSSH server, Docker, tmux (tested with 3.4), `curl`, `tar`, `sha256sum`, `infocmp`, `tic`, `install`, `systemctl`. Run as an unprivileged account with Docker access, never as root |
|
|
118
122
|
| Containers | Homes mounted at `/home/coder` with uid 1000; Bash, Python 3 and tmux (compatible with the host's tmux) |
|
|
119
123
|
| Browser access (optional) | Caddy and Tailscale (with HTTPS) on the server |
|
|
120
124
|
|
|
125
|
+
### Join with an invite
|
|
126
|
+
|
|
127
|
+
A member of the site sends you an invite: five lines from `lanes invite` ("Invite a user" below) with a one-time
|
|
128
|
+
**invite code**. Then, on your Mac or Linux machine:
|
|
129
|
+
|
|
130
|
+
1. **Join the tailnet**, on this machine and on your phone: open the Tailscale link in the invite (or ask the member
|
|
131
|
+
for one). Without Tailscale, install it first (macOS: `brew install --cask tailscale`; Linux:
|
|
132
|
+
[tailscale.com/download](https://tailscale.com/download)). Logging in can wait: the installer opens `tailscale login`
|
|
133
|
+
itself ("Tailscale" below).
|
|
134
|
+
2. **Run the installer with the code:**
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
npx @vibelord/lanes --invite LN-xxxx-xxxxxxxxxxxx # the code's prefix picks the site (LN = beyondwork)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
3. **Open Lanes**, in your own terminal (the two web commands show a login token there):
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
lanes-launch # your desktop view in this terminal (lanes-window: in its own window)
|
|
144
|
+
lanes-launch web phone # your phone's view: a QR code of its private URL, then a login token
|
|
145
|
+
lanes-launch web laptop # this laptop's browser view, the same way
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| Step | The installer prints |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| Site | `site beyondwork, inferred from the invite code's prefix LN` (npm only; an explicit `--site` wins) |
|
|
151
|
+
| Tailscale | `tailscale: connected as <login> (<address>)`, after opening the login when needed ("Tailscale" below) |
|
|
152
|
+
| SSH alias | `ssh: host alias 'lanes' -> dev@<MagicDNS name> (fallback lanes-ip -> <tailnet address>), key …` |
|
|
153
|
+
| Invite | `invite: redeemed — this machine's key is now authorized for <login> (device <device>)` |
|
|
154
|
+
| Server | `server: ok device=<device> version=<version> boards=<n>`, then `Ready.` and the commands of step 3 |
|
|
155
|
+
|
|
156
|
+
`LANES_INVITE=<code>` works like `--invite`. The code stays secret until it is redeemed: the installer sends it in the
|
|
157
|
+
request body (never on a command line) and never prints or stores it, and a code typed at the prompt below is read
|
|
158
|
+
with echo off. A code whose prefix belongs to another site is refused before anything is installed. Phone and
|
|
159
|
+
browser views: section 4. Uploads: section 5.
|
|
160
|
+
|
|
161
|
+
When the server already has a device of this machine's name — two laptops with one hostname, or a replaced laptop
|
|
162
|
+
whose old enrolment is still there — it answers `409 (device-taken)` and uses nothing up: the installer says so and
|
|
163
|
+
names the re-run, `npx @vibelord/lanes --invite <code> --device <another name>` (it remembers the name). If the old
|
|
164
|
+
machine is gone, a member can end it first with `lanes offboard <device> --yes`. `409 (key-taken)` means the server
|
|
165
|
+
holds this key for another device: the installer names a key of its own (`--key ~/.ssh/lanes_ed25519`). Retrying
|
|
166
|
+
with a valid code this way never pauses the machine: only wrong codes count toward the 10-minute limit.
|
|
167
|
+
|
|
168
|
+
**Operator gate**: the server must run the enrolment service `lanes-enrold` behind its private HTTPS front (installed
|
|
169
|
+
and enabled by `npx @vibelord/lanes --server`). **Not yet verified on a device**: no code has been redeemed from a
|
|
170
|
+
second machine yet.
|
|
171
|
+
|
|
172
|
+
### No code: ask for access
|
|
173
|
+
|
|
174
|
+
Run the installer in your own terminal without a code (`npx @vibelord/lanes --site beyondwork`, or `./install.sh`
|
|
175
|
+
from a checkout). When the server does not know this machine's key, it asks `Invite code (Enter = ask the members of
|
|
176
|
+
this site for access instead):`. Press Enter: the installer files an access request — `request: filed as <id> for
|
|
177
|
+
<device> — a member approves it on their phone (https://<server>:8443/enrol/pending) or on the server: lanes enrol
|
|
178
|
+
approve <id>` — and counts down up to 10 minutes (`LANES_ENROL_WAIT`) while a member decides:
|
|
179
|
+
|
|
180
|
+
| A member decides | Where |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| On a phone or laptop on the tailnet | `https://<server>:8443/enrol/pending`: **Approve** or **Deny** next to your login and device (open to a tailnet login that owns a member device) |
|
|
183
|
+
| On the server or over `ssh lanes` | `lanes enrol list`, then `lanes enrol approve <id>` (or `lanes enrol deny <id>`) |
|
|
184
|
+
| From a push | With the opt-in push notifier on, each new request arrives as one notification naming you, your device and that command (section 8) |
|
|
185
|
+
|
|
186
|
+
Approved, the installer says `request: approved — this machine's key is now authorized for <login> (device <device>)`
|
|
187
|
+
and goes on to the server check and `Ready.` If nobody answers in time, it exits 3 and the request stays open for 24
|
|
188
|
+
hours: `npx @vibelord/lanes --resume` (or `./install.sh --resume`) waits again, and so does a plain re-run in a
|
|
189
|
+
terminal. A site whose enrolment service cannot see tailnet logins admits by invite only: there Enter says `request:
|
|
190
|
+
this site admits by invite only — ask a member for an invite, then ./install.sh --invite CODE`.
|
|
191
|
+
|
|
192
|
+
### No enrolment service: the administrator authorizes the key
|
|
193
|
+
|
|
194
|
+
A server without `lanes-enrold`, a refused, used or expired code, or an install with no code and no terminal (`--yes`,
|
|
195
|
+
a pipe) ends with this machine's **public** key printed for the administrator (exit 3), as before enrolment existed.
|
|
196
|
+
The administrator authorizes it on the server with `lanes keys add <device> <file.pub>` (or `lanes keys add <device> -`
|
|
197
|
+
and paste the line), which writes it into the lanes-managed block of the server account's `~/.ssh/authorized_keys`
|
|
198
|
+
(the client never edits that file). Then run `npx @vibelord/lanes --check` (or `./install.sh --check`) and
|
|
199
|
+
`lanes-launch`.
|
|
200
|
+
|
|
121
201
|
### Client from npm
|
|
122
202
|
|
|
123
|
-
|
|
124
|
-
|
|
203
|
+
Without an invite code, name the site yourself; the same command updates an installed client. The package
|
|
204
|
+
`@vibelord/lanes` is public — no npm login or token. It carries the committed `lanes/` tree and runs that
|
|
205
|
+
tree's own `install.sh` with the same options and exit codes; its only site facts are the bundled profiles' public host
|
|
206
|
+
keys and tailnet names and addresses.
|
|
125
207
|
|
|
126
208
|
```sh
|
|
127
|
-
npm login --scope=@vibelord # once per machine (or a read-only token in ~/.npmrc)
|
|
128
209
|
npx @vibelord/lanes --site beyondwork # install or update the client
|
|
129
210
|
lanes-window # open the desktop view
|
|
130
211
|
```
|
|
@@ -134,6 +215,7 @@ lanes-window # open the desktop view
|
|
|
134
215
|
| `--site NAME` | A bundled profile (`beyondwork`); a value containing `/` or ending in `.conf` is your own file |
|
|
135
216
|
| `--stage-only` | Copy the bundled tree to `~/.local/share/lanes/pkg/<version>/` and print the path; run nothing |
|
|
136
217
|
| `--version` | Print the package version |
|
|
218
|
+
| `--invite CODE` | Hand the code to the installer; without `--site`, the code's prefix picks the bundled profile ("Join with an invite" above) |
|
|
137
219
|
| `--wrapper-help` | Wrapper help only |
|
|
138
220
|
|
|
139
221
|
Every other option goes to `install.sh`. The wrapper stages the tree under `~/.local/share/lanes/pkg/<version>/`
|
|
@@ -142,6 +224,8 @@ version a server unit still runs from. See [`npm/lanes/README.md`](../../npm/lan
|
|
|
142
224
|
|
|
143
225
|
### Client from a checkout
|
|
144
226
|
|
|
227
|
+
For working on Lanes, or for a site without a bundled profile:
|
|
228
|
+
|
|
145
229
|
```sh
|
|
146
230
|
git clone https://github.com/bwllaming/bw-tools.git
|
|
147
231
|
cd bw-tools/lanes
|
|
@@ -155,32 +239,73 @@ lanes-window
|
|
|
155
239
|
| `--check` | Verify the commands, SSH alias, key, upload helper and server; changes nothing |
|
|
156
240
|
| `--uninstall` | Remove what the client installer added (your SSH key, the known_hosts entry and `~/Lanes Uploads` are kept) |
|
|
157
241
|
| `--server [--check \| --no-start]` | Install or update the Linux server instead (must be the first argument; see below) |
|
|
158
|
-
| `--site FILE` | Server profile; default `site/site.conf`, else the
|
|
159
|
-
| `--device NAME` | This machine's view session name (default `<user>-<hostname>`, lowercased, at most 32 characters
|
|
242
|
+
| `--site FILE` | Server profile; default the remembered one, else `site/site.conf`, else the installed `~/.config/lanes/client-site.conf` |
|
|
243
|
+
| `--device NAME` | This machine's view session name (default `<user>-<hostname>`, lowercased, at most 32 characters) |
|
|
160
244
|
| `--key PATH` | SSH private key (default: the one already configured, else `~/.ssh/id_ed25519`, else a new `~/.ssh/lanes_ed25519`) |
|
|
161
245
|
| `--link` | Symlink the client into this checkout instead of copying it (for working on Lanes) |
|
|
162
|
-
| `--coder-launch` | Also provide the command name `coder-launch` (an existing one is kept as `coder-launch-legacy`) |
|
|
163
|
-
| `--no-desktop` |
|
|
164
|
-
| `--no-uploads` |
|
|
165
|
-
| `--palette` | Give the lanes window the cockpit's own Ghostty palette (section 9)
|
|
166
|
-
| `--
|
|
246
|
+
| `--coder-launch` / `--no-coder-launch` | Also provide the command name `coder-launch` (an existing one is kept as `coder-launch-legacy`) / take it away again (the kept one returns) |
|
|
247
|
+
| `--no-desktop` / `--desktop` | No Lanes.app, `.desktop` entry or Hyprland bindings — an earlier install's are removed (Hyprland line with a backup) / back on |
|
|
248
|
+
| `--no-uploads` / `--uploads` | No uploads watch-folder agent or upload helper service — an earlier install's are stopped and removed, `~/Lanes Uploads` and its files stay / back on |
|
|
249
|
+
| `--palette` / `--no-palette` | Give the lanes window the cockpit's own Ghostty palette (section 9) / put your everyday look back |
|
|
250
|
+
| `--invite CODE` | Join with an invite code when the server does not know this machine's key yet ("Join with an invite" above; also `LANES_INVITE`); not remembered |
|
|
251
|
+
| `--resume` | Wait again for this machine's open access request (`~/.config/lanes/enrol-request`; "No code: ask for access" above); not remembered |
|
|
252
|
+
| `--yes`, `-y` | Never prompt (a newly created key then has no passphrase; no invite prompt either) |
|
|
167
253
|
| `-h`, `--help` | Print the option summary |
|
|
168
254
|
|
|
255
|
+
**Remembered options.** Every install writes its choices — `--device`, `--key`, `--site`, `--coder-launch`,
|
|
256
|
+
`--no-desktop`, `--no-uploads`, `--palette` and their opposites — to `~/.config/lanes/install-options` (plain
|
|
257
|
+
`KEY=VALUE`, mode 0600, read without executing it). A re-run without flags, and so every update, repeats the last
|
|
258
|
+
install; a flag given again overrides the remembered value and is remembered from then on. A profile bundled with
|
|
259
|
+
the package (`--site beyondwork`) is remembered by its name, so an update reads the new package's copy and its pinned
|
|
260
|
+
host key; your own profile file is remembered by its path (when that file is gone, the installed copy is used).
|
|
261
|
+
`--check` only reads the file, `--uninstall` removes it. `--link` and `--yes` are per run. A client installed before
|
|
262
|
+
the file existed (1.0.x) is read once from what it left behind: the palette include, the `coder-launch` link, and
|
|
263
|
+
desktop or uploads as off only when nothing of them is left (no app, launcher entry, Hyprland line or bind file; no
|
|
264
|
+
watch-folder README, agent or service) — a launcher entry you deleted by hand while keeping the key binding leaves the
|
|
265
|
+
desktop on, so that first update takes nothing away. What is installed follows the remembered choice both ways: an
|
|
266
|
+
option turned off takes away what an earlier install added for it — `--no-uploads` stops and removes the watch-folder
|
|
267
|
+
agent and the upload helper service (on macOS the two launchd agents, Lanes Helper.app and the Lanes Upload droplet)
|
|
268
|
+
and the agent hand-off block, keeping `~/Lanes Uploads` and its files; `--no-desktop` removes Lanes.app or the
|
|
269
|
+
launcher entry and the Hyprland line (backup `.pre-lanes-off.<UTC>`) with its bind file; `--no-palette` and
|
|
270
|
+
`--no-coder-launch` likewise. Nothing is touched on a machine that never had them, and `--check` reports the upload
|
|
271
|
+
helper as off.
|
|
272
|
+
|
|
169
273
|
What a client install changes: `lanes-launch`, `lanes-window` and `lanes-upload` in `~/.local/bin` (copied to
|
|
170
274
|
`~/.local/share/lanes/client` unless `--link`); the SSH alias `lanes` in `~/.ssh/config.d/lanes.conf` plus one
|
|
171
|
-
`Include` line at the top of `~/.ssh/config` (backup kept), with a `lanes-public` fallback when the site provides one
|
|
275
|
+
`Include` line at the top of `~/.ssh/config` (backup kept), with a `lanes-public` fallback when the site provides one —
|
|
276
|
+
when this machine resolves the profile's MagicDNS name (`LANES_SERVER_NAME`), `lanes` names the server by it and a
|
|
277
|
+
second alias `lanes-ip` holds the tailnet address (otherwise `lanes` holds the address, as before; both pin the same
|
|
278
|
+
host key, keyed by the profile's alias);
|
|
172
279
|
the server's host key pinned from the profile's `known_hosts`; a separate Ghostty configuration
|
|
173
|
-
`~/.config/lanes/ghostty.conf` (your everyday Ghostty config is read, never written);
|
|
174
|
-
`~/Applications/Lanes.app`; Linux a "Lanes" launcher entry and, on Omarchy/Hyprland,
|
|
175
|
-
and the uploads watch folder plus the upload helper service (section 5)
|
|
280
|
+
`~/.config/lanes/ghostty.conf` (your everyday Ghostty config is read, never written); `~/.config/lanes/install-options`;
|
|
281
|
+
desktop integration (macOS `~/Applications/Lanes.app`; Linux a "Lanes" launcher entry and, on Omarchy/Hyprland,
|
|
282
|
+
validated key bindings); and the uploads watch folder plus the upload helper service (section 5) — on Linux with a
|
|
283
|
+
key that has a passphrase also one marked block in your login profile (section 5, "The upload helper and a
|
|
284
|
+
passphrase").
|
|
285
|
+
|
|
286
|
+
### Tailscale
|
|
176
287
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
288
|
+
A profile whose server has a Tailscale name (`LANES_SERVER_NAME` ends in `.ts.net`, as the supplied
|
|
289
|
+
`beyondwork` profile does) is reached only through the tailnet, so the installer looks at Tailscale before it tries
|
|
290
|
+
the server (a profile without such a name skips this):
|
|
291
|
+
|
|
292
|
+
| The installer prints | What happened | What you do |
|
|
293
|
+
| --- | --- | --- |
|
|
294
|
+
| `tailscale: not installed — macOS: brew install --cask tailscale · Linux: https://tailscale.com/download` | No `tailscale` command on `PATH` (on macOS also none inside `Tailscale.app` in `~/Applications` or `/Applications`). The client is installed; exit 4 | Install Tailscale, then run the installer again |
|
|
295
|
+
| ``tailscale: needs login — opening `tailscale login`… (approve in the browser)`` | This machine is logged out of the tailnet; the installer runs `tailscale login` once (its login URL is printed where no browser opens) and waits up to 120 s, with a countdown, until the machine is `Running` and online | Approve the login in the browser |
|
|
296
|
+
| `` tailscale: `tailscale login` failed `` | The login gave up at once — on Linux, an account without operator rights (`Access denied`) | `sudo tailscale up` once (`sudo tailscale set --operator=$USER` lets you log in without sudo), then run the installer again |
|
|
297
|
+
| `tailscale: installed, but not running` / `switched off` / `waits for a tailnet admin` | `tailscaled` does not answer / Tailscale is switched off / the tailnet requires device approval | Start the app (Linux: `sudo systemctl enable --now tailscaled`) / switch it on / ask the admin; the installer keeps counting down meanwhile |
|
|
298
|
+
| `tailscale: not on the tailnet after 120 s (state: …)` | The wait ran out; the client is installed and the server was not tried; exit 4 | Connect Tailscale, then run the installer again |
|
|
299
|
+
| `tailscale: connected as <login> (<address>)` | This machine is in the tailnet | Nothing: the server check follows |
|
|
180
300
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
301
|
+
`LANES_TAILSCALE_WAIT=<seconds>` changes the 120-second bound; Ctrl-C ends the wait and the login it opened.
|
|
302
|
+
`--check` only looks: it never opens a login and never waits (a logged-out machine is exit 4 with the advice above).
|
|
303
|
+
|
|
304
|
+
### Exit status
|
|
305
|
+
|
|
306
|
+
The same for `./install.sh` and `npx @vibelord/lanes`: `0` ready · `3` installed, but this machine's key is not yet
|
|
307
|
+
authorized on the server · `4` installed, but this machine is not in the tailnet, or the server is unreachable or
|
|
308
|
+
did not answer in time · `1` error.
|
|
184
309
|
|
|
185
310
|
### Server
|
|
186
311
|
|
|
@@ -205,7 +330,9 @@ glow 3.0.0 unless a glow is already on the view server's PATH) into `~/.local/sh
|
|
|
205
330
|
`xterm-lanesview` terminfo alias, renders every unit in `lanes/systemd/` into the user manager's unit directory,
|
|
206
331
|
links `~/.local/share/lanes/current` and `~/.local/bin/lanes` to this checkout, creates an empty catalog only when no
|
|
207
332
|
configuration exists, runs `lanes sync`, and enables `lanes-statusd.service`, `lanes-sync.timer`,
|
|
208
|
-
`lanes-ensure.timer
|
|
333
|
+
`lanes-ensure.timer`, `lanes-gc.timer` and the enrolment service `lanes-enrold.service` (restarted by every install, so
|
|
334
|
+
it runs the installed code; it restarts a running `lanes-mobile` too, so Caddy reads the new `/enrol/*` route). Web
|
|
335
|
+
access, the Studio Host, the economy poller, push notifications,
|
|
209
336
|
the `/tmp` reaper and the OOM guards stay off (section 11 lists each unit).
|
|
210
337
|
|
|
211
338
|
Then configure the workspaces (section 6) and install the container contract:
|
|
@@ -223,12 +350,59 @@ it creates `~/.tmp` and appends one marker-guarded line (`# lanes-env`) to the l
|
|
|
223
350
|
the pinned zmx binaries without overwriting a running one; and it links `~/.lz/<container>` on the host. It installs
|
|
224
351
|
no agent hook (section 7).
|
|
225
352
|
|
|
353
|
+
For a new user's container, `lanes onboard <login> --container <name> --workspace <id> --repo /home/coder/<repo>`
|
|
354
|
+
prints every step and changes nothing. It checks the container first, so an unknown container stops it before the
|
|
355
|
+
board is written. Add `--write` to apply the steps in order: the board, `install-container`, the
|
|
356
|
+
Claude and Codex hooks, `throttle setup --install-clients` inside the container when Throttle is installed and
|
|
357
|
+
configured there (otherwise it prints the one-time setup to run on a terminal), `lanes sync`,
|
|
358
|
+
`cursor-sync --canonicalize --write` when the repository has a
|
|
359
|
+
`.vscode/terminals.json`, and `lanes doctor`. It ends with a table and a receipt under
|
|
360
|
+
`~/.local/state/lanes/onboard/`. Running it again changes nothing that is already done
|
|
361
|
+
([install guide](INSTALL.md#onboard-a-users-container); what the agents get: section 7).
|
|
362
|
+
|
|
363
|
+
### Invite a user (administrator)
|
|
364
|
+
|
|
365
|
+
Any member can invite a new person from the server, or over `ssh lanes`, **in their own terminal** — the code is
|
|
366
|
+
shown once and only to a terminal (piped or redirected, `lanes invite` exits `2` and mints nothing):
|
|
367
|
+
|
|
368
|
+
```text
|
|
369
|
+
$ lanes invite anna --email anna@example.com --workspaces bw-tools,throttle
|
|
370
|
+
Invite for anna — valid 7 d, one redemption
|
|
371
|
+
1. Join the tailnet (laptop and phone): ask your admin for the Tailscale invite
|
|
372
|
+
2. On your Mac/Linux: npx @vibelord/lanes --invite LN-7q4k-m2xpf9c3d8e2
|
|
373
|
+
3. Then: lanes-launch · lanes-launch web phone · lanes-launch web laptop
|
|
374
|
+
Takes ~5 min.
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Send those five lines to the person (`--md` prints them as Markdown to paste). The code `LN-<id>-<secret>` is
|
|
378
|
+
case-insensitive and redeems **once**, within 7 days (`--expires 3d`, `--expires 12h`; 1 h to 30 d); the server keeps
|
|
379
|
+
only its hash in `invites/<id>.json`. `--email` records who the invite is for — on a server whose enrolment service
|
|
380
|
+
cannot see tailnet logins (invite-only) that address becomes the new member's owner, so give it — and, when
|
|
381
|
+
`~/.config/lanes/resend.key` exists, mails the five lines — put a `from=Name <addr>` line on a domain verified in
|
|
382
|
+
Resend under the key, or the kit goes from Resend's test sender, which delivers only to the Resend account's own
|
|
383
|
+
address (anyone else: Resend answers 403, and the note names the missing line). Step 1's link is
|
|
384
|
+
`--tailscale-link URL` (an invite from the Tailscale admin console), else one minted with
|
|
385
|
+
`~/.config/lanes/tailscale-api.key`, else the person asks you for one.
|
|
386
|
+
Both key files are Lanes' own and optional (RUNBOOK "Invite, approve, offboard"). Step 2 redeems the code at the
|
|
387
|
+
enrolment service through the client's `--invite` (W8 B3, the same release). **Operator gate**: the server needs
|
|
388
|
+
`lanes-enrold` installed (`npx @vibelord/lanes --server`). **Not yet verified on a device**: no code has been
|
|
389
|
+
redeemed from a second machine yet.
|
|
390
|
+
|
|
391
|
+
| Command | Does |
|
|
392
|
+
| --- | --- |
|
|
393
|
+
| `lanes invite list` | Every invite: `live`, `redeemed` (by which device), `revoked`, `expired` |
|
|
394
|
+
| `lanes invite revoke <id>` | The code is refused from now on (a redeemed invite is not revoked: offboard the device) |
|
|
395
|
+
| `lanes enrol list [--all]` | Requests filed by machines without a code; `--all` adds decided and expired ones |
|
|
396
|
+
| `lanes enrol approve <id> [--as <device>]` / `deny <id>` | Decide one, as `/enrol/pending` does on the phone; `--as` names your own device ([install guide](INSTALL.md#approve-a-request)) |
|
|
397
|
+
| `lanes enrol mode [request\|invite\|auto]` | Pin whether the server takes access requests, across restarts — `request` once the Serve header check passed, `invite` for invites only ([install guide](INSTALL.md#approve-a-request)) |
|
|
398
|
+
| `lanes offboard <device> --yes` | In your own terminal: revoke its key, forget the device, sign its browsers and phones out (the login tokens of the device, its windows and its `lanes-launch web` views), end its view session; it names its windows (`<device>-2`) to offboard by name ([install guide](INSTALL.md#offboard-a-machine)) |
|
|
399
|
+
|
|
226
400
|
### Update
|
|
227
401
|
|
|
228
402
|
| Where | Update | Then |
|
|
229
403
|
| --- | --- | --- |
|
|
230
|
-
| Client (npm) | `npx @vibelord/lanes@latest` with the
|
|
231
|
-
| Client (checkout) | `git pull`, then `./install.sh` | `./install.sh --check` |
|
|
404
|
+
| Client (npm) | `lanes-launch update`, which runs `npx --yes @vibelord/lanes@latest` with every choice in `~/.config/lanes/install-options` given again as its flag (the command is printed first; flags you add follow them and win, e.g. `lanes-launch update --no-palette`); or `npx @vibelord/lanes@latest` yourself (your remembered options are repeated; add a flag only to change one) | `npx @vibelord/lanes --check` |
|
|
405
|
+
| Client (checkout) | `git pull`, then `./install.sh` (`lanes-launch update` refuses a client that runs from a checkout: npm would replace it with a copy) | `./install.sh --check` |
|
|
232
406
|
| Server (npm) | `npx @vibelord/lanes@latest --server` (runs from `~/.local/share/lanes/pkg/<version>/`; older trees a service still uses are kept) | the restarts below |
|
|
233
407
|
| Server (checkout) | `git pull` in the installed checkout, then `./install.sh --server` (configured workspaces and agent sessions are preserved) | the restarts below |
|
|
234
408
|
|
|
@@ -253,6 +427,17 @@ before a server update. To roll back, restore those files; do not rebuild device
|
|
|
253
427
|
installer calls it to prove the SSH path, the key and the server version; an answer without that greeting is
|
|
254
428
|
reported as "an older server?".
|
|
255
429
|
- The client installer prints `lanes client <version>` and copies `VERSION` into `~/.local/share/lanes/client/`.
|
|
430
|
+
- `lanes-launch` passes that version to `lanes attach` (`--client-version`). A client older than the server gets one
|
|
431
|
+
dim line before the view opens, and again after it detaches: `lanes client 1.0.4 is older than the server (1.1.0) —
|
|
432
|
+
run lanes-launch update`. A `lanes-launch` from before the flag (1.0.4 or earlier; it has no `update` verb) is told
|
|
433
|
+
`update it: npx @vibelord/lanes@latest` only by a server newer than 1.0.4 — a 1.0.4 server says nothing to it.
|
|
434
|
+
Only major.minor.patch is compared (`1.1.0-rc.1` counts as `1.1.0`); a `dev` version on either side, and your own
|
|
435
|
+
`lanes attach` on the server, get no line.
|
|
436
|
+
- `lanes-launch web` asks the server's `lanes hello` first, on the same connection. A 1.0.x server does not know the
|
|
437
|
+
options a 1.1.0 client adds (`--default-device`, `--no-qr`), so the client leaves them out there and says so in one
|
|
438
|
+
line: `the server runs lanes 1.0.4, older than this client: no QR code, and the upload box of this view does not
|
|
439
|
+
reach this machine` — the view and its login token still work. The client updates itself first (public npm), the
|
|
440
|
+
server when its operator runs `npx @vibelord/lanes@latest --server`, so this mixed window is expected.
|
|
256
441
|
|
|
257
442
|
### Uninstall
|
|
258
443
|
|
|
@@ -281,11 +466,16 @@ before a server update. To roll back, restore those files; do not rebuild device
|
|
|
281
466
|
| `lanes-launch <device> [--ascii] [--theme dark\|light\|auto] [--glyphs M] [--color C]` | The view with a recorded look |
|
|
282
467
|
| `lanes-launch lane <lane-id>` | One lane straight in this terminal, no multiplexer (native terminal scrollback) |
|
|
283
468
|
| `lanes-launch ls` | List the lanes with live status (`lanes ls` on the server) |
|
|
469
|
+
| `lanes-launch web phone` / `laptop` / `<device>` | Not a terminal view: a browser view's private URL and a login token (section 4) |
|
|
470
|
+
|
|
471
|
+
`web`, `update`, `lane` and `ls` are verbs: a device or workspace with one of those names is not reached through
|
|
472
|
+
`lanes-launch <name>`.
|
|
284
473
|
|
|
285
474
|
Closing the window detaches; nothing on the server stops. `lanes-launch` never ends by itself: when the connection
|
|
286
475
|
drops, the server drops a slow client, or the server refuses, it says so, waits with a countdown (backing off up to
|
|
287
|
-
60 s; `Enter` reconnects now) and reattaches. After two quick transport failures it uses the
|
|
288
|
-
|
|
476
|
+
60 s; `Enter` reconnects now) and reattaches. After two quick transport failures it uses the next fallback the SSH
|
|
477
|
+
configuration defines, first `lanes-ip` (the server's tailnet address, when `lanes` names it by its MagicDNS name), then
|
|
478
|
+
`lanes-public` (when the site defines one), and after the last it goes back to `lanes`. Exits 64–78 are server refusals and retry after 60 s. Its log is
|
|
289
479
|
`~/.local/state/lanes-client/launch-<target>.log`. When the server rebuilt the view, the client reconnects straight
|
|
290
480
|
away.
|
|
291
481
|
|
|
@@ -298,7 +488,9 @@ away.
|
|
|
298
488
|
entry with `layout: "lanes"` sits just before Host unless its `position` is `workspace`.
|
|
299
489
|
- Phones and browsers are separate devices (`phone`, `browser`, or a name you choose); a device keeps the layout
|
|
300
490
|
(desktop or phone) it was created with.
|
|
301
|
-
- On the server: `lanes device list` shows each device (phone or desktop with its viewports,
|
|
491
|
+
- On the server: `lanes device list` shows each device (phone or desktop with its viewports, the owner's login once
|
|
492
|
+
the machine was enrolled through an invite or a request — the current member's, so a handed-over machine shows its
|
|
493
|
+
new owner — running or not);
|
|
302
494
|
`lanes device forget <name>` stops `lanes ensure` recreating it; `lanes rebuild <device>` recreates a stuck view.
|
|
303
495
|
|
|
304
496
|
### Panes and viewports
|
|
@@ -490,6 +682,19 @@ in the window's config). Not yet verified on a Mac.
|
|
|
490
682
|
|
|
491
683
|
## 4. Phone, tablet and laptop browser
|
|
492
684
|
|
|
685
|
+
From an installed client, in your own terminal (the login token is shown only there), one command per browser:
|
|
686
|
+
|
|
687
|
+
```sh
|
|
688
|
+
lanes-launch web phone # your phone: the view <your device>-phone, phone layout
|
|
689
|
+
lanes-launch web laptop # this laptop's browser: the view <your device>-web, desktop layout
|
|
690
|
+
lanes-launch web ipad # any other name is that view (a new one gets the desktop layout)
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Each prints the private URL with a QR code of it, then a fresh login token. Open the URL on the device (scan the code
|
|
694
|
+
with a phone's camera), sign in with the token, and on a phone add it to the Home Screen. The server does the rest in
|
|
695
|
+
the same step: it prepares the view, enables the web services and adds the Tailscale Serve mapping when it is
|
|
696
|
+
missing. The device must be on the tailnet ("Join with an invite", step 1).
|
|
697
|
+
|
|
493
698
|
### How it is served
|
|
494
699
|
|
|
495
700
|
```text
|
|
@@ -504,47 +709,88 @@ A token grants terminal access as the server account.
|
|
|
504
709
|
|
|
505
710
|
### One-time server setup (administrator)
|
|
506
711
|
|
|
507
|
-
1. Install Caddy
|
|
508
|
-
8443 for Lanes.
|
|
712
|
+
1. Install Caddy (2.5 or later: the `/enrol/*` route uses `trusted_proxies`) and Tailscale on the server and enable
|
|
713
|
+
Tailscale HTTPS for the tailnet. Reserve ports 8082, 8083, 8443 and 8184 (the enrolment service) for Lanes.
|
|
509
714
|
2. Allow the workspace account to manage Tailscale Serve: `sudo sh sudo/one-time-root.sh --user <account>` (it changes
|
|
510
715
|
SSH daemon settings only with `--ssh-keepalive`).
|
|
716
|
+
3. Nothing more: the first `lanes-launch web …` from a client (or `lanes web <device>` on the server) enables the
|
|
717
|
+
loopback web services and adds the Tailscale Serve mapping `https:8443 -> http://127.0.0.1:8083` when it is missing
|
|
718
|
+
("Laptop and tablet" below has the rules). When Tailscale refuses the account it exits 64 and prints the step of
|
|
719
|
+
item 2. The same front carries the enrolment service's `/enrol/*` routes (section 2).
|
|
511
720
|
|
|
512
721
|
### Phone (Safari, Home Screen)
|
|
513
722
|
|
|
514
723
|
From an installed client, in your own terminal (the token is shown only there):
|
|
515
724
|
|
|
516
725
|
```sh
|
|
517
|
-
|
|
726
|
+
lanes-launch web phone # this machine's phone view: <your device>-phone, phone layout
|
|
518
727
|
```
|
|
519
728
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
729
|
+
The server prepares the view `<your device>-phone` (for example `anna-mbp-phone`), enables the web services, makes
|
|
730
|
+
Tailscale Serve forward private HTTPS 8443 to them (added when missing), prints the private URL with a QR code of it
|
|
731
|
+
and then a fresh token. On the phone: connect Tailscale, scan the code with the camera (or open the printed
|
|
732
|
+
`https://<server>.<tailnet>.ts.net:8443/<your device>-phone` in Safari), paste only the token value (Zellij prints
|
|
733
|
+
`NAME: TOKEN`), then **Share → Add to Home Screen**.
|
|
734
|
+
|
|
735
|
+
`lanes-launch web phone|laptop|<device> [--desktop|--mobile] [--no-qr]` runs `lanes web <view> <layout> --token
|
|
736
|
+
--default-device <your device>` on the server over `ssh -t lanes`:
|
|
523
737
|
|
|
524
|
-
|
|
525
|
-
|
|
738
|
+
| Target | View on the server | Layout |
|
|
739
|
+
| --- | --- | --- |
|
|
740
|
+
| `phone` | `<your device>-phone` | phone (`--mobile`) |
|
|
741
|
+
| `laptop` | `<your device>-web` | desktop (`--desktop`) |
|
|
742
|
+
| any other name | that device, as given | a known device keeps its layout; a new one is desktop |
|
|
743
|
+
|
|
744
|
+
`--desktop` or `--mobile` overrides the layout. `phone` and `laptop` are named after this machine's device (from
|
|
745
|
+
`~/.config/lanes/device`), so the server records this machine as the view's base, as `lanes attach` does for `mac-2`;
|
|
746
|
+
a name over 32 characters is refused here (name the view yourself: `lanes-launch web <name>`). Any other option is
|
|
747
|
+
refused before a connection (exit 64). The exit status is the server's: 64 when ssh got no terminal for the token,
|
|
748
|
+
when a running view has the other layout (choose another name), or when the server could not make the Tailscale
|
|
749
|
+
Serve mapping (it prints the operator's step). Before the token, the server draws a QR code of the
|
|
750
|
+
URL (only the URL: the token does not exist yet) so the phone's camera opens it; `--no-qr` leaves it out. A device
|
|
751
|
+
literally named `phone` or `laptop` (the `phone` view `enable-phone.sh`
|
|
752
|
+
makes) is reached with `lanes web phone` on the server.
|
|
753
|
+
|
|
754
|
+
`enable-phone.sh [--host SSH_ALIAS | --local] [--desktop|--mobile] [device]` is the older one-shot for the device
|
|
755
|
+
`phone`: `--host` connects with the key the client installer chose (no password fallback, no agent forwarding);
|
|
756
|
+
`--local` runs on the server, where it is a thin wrapper over `lanes web <device> <layout> --token`; `--mobile` (the
|
|
526
757
|
default, device `phone`) or `--desktop` (device `browser`) picks the layout. On macOS `--host lanes` is assumed when
|
|
527
758
|
neither is given. An SSH-only phone client can instead run `lanes attach phone --mobile`.
|
|
528
759
|
|
|
529
760
|
### Laptop and tablet
|
|
530
761
|
|
|
531
|
-
Give each browser its own device.
|
|
762
|
+
Give each browser its own device. From an installed client, in your own terminal:
|
|
763
|
+
|
|
764
|
+
```sh
|
|
765
|
+
lanes-launch web laptop # this laptop's browser view: <your device>-web, desktop layout
|
|
766
|
+
lanes-launch web ipad # any other name is that device (a new one gets the desktop layout)
|
|
767
|
+
lanes-launch web ipad --mobile # the phone layout instead
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
On the server the same command is `lanes web`:
|
|
532
771
|
|
|
533
772
|
```sh
|
|
534
773
|
lanes web ipad --desktop # desktop sidebar layout (a new device's default); prints the private URL
|
|
535
774
|
lanes web ipad --desktop --token # the same, then a fresh login token in this terminal
|
|
536
|
-
lanes web ipad --print-only # the exact prepare call and
|
|
775
|
+
lanes web ipad --print-only # the exact prepare call, URL and Serve step; changes nothing
|
|
537
776
|
```
|
|
538
777
|
|
|
539
|
-
From a client: `
|
|
540
|
-
device `browser`).
|
|
541
|
-
|
|
542
|
-
`lanes web <device> [--desktop|--mobile] [--viewports N] [--token | --print-only]
|
|
543
|
-
loopback services through `install/enable-web.sh`,
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
778
|
+
From a client: `lanes-launch web laptop` (the view `<your device>-web`), `lanes-launch web <device> --desktop`, or
|
|
779
|
+
`sh install/enable-phone.sh --host lanes --desktop <device>` (without a name, `--desktop` uses the device `browser`).
|
|
780
|
+
|
|
781
|
+
`lanes web <device> [--desktop|--mobile] [--viewports N] [--token | --print-only] [--default-device NAME] [--no-qr]`
|
|
782
|
+
prepares the view (before it prints anything), enables the loopback services through `install/enable-web.sh`, makes
|
|
783
|
+
Tailscale Serve forward `https:8443` to `http://127.0.0.1:8083`, and prints `key: value` lines (`device`, `layout`,
|
|
784
|
+
`state`, `prepare`, `services`, `url`, then `note` / `next` / `serve` when they apply). Without `--desktop`/`--mobile`
|
|
785
|
+
a known device keeps its recorded layout; only a new device defaults to desktop. A running view of the other layout
|
|
786
|
+
is refused, never rebuilt: choose another device name. `--token` needs your own terminal (exit 64 otherwise); it then
|
|
787
|
+
draws a QR code of the URL — `qrencode -t ANSIUTF8` when the server has it, else a built-in encoder for URLs up to
|
|
788
|
+
106 bytes — and none for a loopback URL or with `--no-qr`; the token is never in it. The Serve step leaves a present
|
|
789
|
+
mapping alone (`serve: present …`), adds a missing one (`serve: added …`), and never replaces a mapping of 8443 to
|
|
790
|
+
anything else; when it cannot make the mapping it exits 64 and prints the operator's step (`sudo tailscale set
|
|
791
|
+
--operator=<account>`, or the command that frees the port). `--default-device` (what `lanes-launch web` passes)
|
|
792
|
+
records the view's base machine; `--print-only` records nothing. Without a Tailscale name the URL is
|
|
793
|
+
`http://127.0.0.1:8083/<device>`, which opens only on the host itself, and Serve is not touched.
|
|
548
794
|
|
|
549
795
|
On a captive-portal network (hotel, train, aircraft): complete the portal sign-in first, then connect Tailscale, then
|
|
550
796
|
open the URL and paste the token.
|
|
@@ -553,10 +799,23 @@ open the URL and paste the token.
|
|
|
553
799
|
|
|
554
800
|
| Task | Command |
|
|
555
801
|
| --- | --- |
|
|
556
|
-
| Create (shown once, in your terminal) | `lanes web <device> --token
|
|
557
|
-
|
|
|
558
|
-
|
|
559
|
-
|
|
802
|
+
| Create (shown once, in your terminal) | `lanes-launch web phone\|laptop\|<device>` on a client, `lanes web <device> --token` on the server, or `enable-phone.sh` |
|
|
803
|
+
| List, with the device each was created for | `lanes token list` |
|
|
804
|
+
| Sign one device out | `lanes token revoke <device>` (`--name token_N` for a token Lanes did not record) |
|
|
805
|
+
| Sign a whole machine out when it leaves (its phone and browser views too) | `lanes offboard <device> --yes` |
|
|
806
|
+
| Sign every browser and phone out | `lanes token revoke-all --yes` |
|
|
807
|
+
|
|
808
|
+
Lanes never prints or logs a token itself. Existing tokens stay valid when you create a new one. Zellij 0.45.1
|
|
809
|
+
names tokens `token_1`, `token_2`, … and cannot name them after a device, so Lanes records which device each
|
|
810
|
+
`lanes web --token` was for (`~/.local/state/lanes/web-tokens.json`). Revoking any token but the newest would stop
|
|
811
|
+
Zellij creating new ones; `lanes token revoke` refuses that case and names `revoke-all`
|
|
812
|
+
([RUNBOOK](RUNBOOK.md#login-tokens)).
|
|
813
|
+
|
|
814
|
+
The tokens live in `~/.local/share/zellij/tokens.db` on the server: `lanes-web.service` pins
|
|
815
|
+
`XDG_DATA_HOME=%h/.local/share` and the token commands use the same root, whatever your shell exports. If the
|
|
816
|
+
running server reads another database (one started before that pin, under a user manager with its own
|
|
817
|
+
`XDG_DATA_HOME`), `lanes doctor` shows a `✗`, and `lanes web --token` (exit 64) and `lanes token` refuse: run
|
|
818
|
+
`lanes web <device>`, then `systemctl --user restart lanes-web.service`, then mint the token again.
|
|
560
819
|
|
|
561
820
|
### The three shell modes
|
|
562
821
|
|
|
@@ -636,6 +895,25 @@ Full Disk Access is not needed; the allowlist and macOS's folder permission are
|
|
|
636
895
|
Transfers have an idle timeout (two minutes without incoming data), not a total-duration limit. Logs:
|
|
637
896
|
`~/.local/state/lanes-client/helper-service.log` (macOS) or `journalctl --user -u lanes-helper` (Linux).
|
|
638
897
|
|
|
898
|
+
**The upload helper and a passphrase** (Linux). A systemd user service cannot ask for a passphrase, so with a
|
|
899
|
+
protected key the helper logs in through your SSH agent — the one the systemd user manager holds, looked up again
|
|
900
|
+
before every login. The service starts at boot (`default.target`), often before your desktop session hands its agent
|
|
901
|
+
to the user manager (GNOME or KDE keyring, uwsm); it uses that agent from its next login attempt, at most a minute
|
|
902
|
+
later, without a restart. The installer hands the agent of the terminal it runs in to
|
|
903
|
+
the user manager (`systemctl --user import-environment SSH_AUTH_SOCK`) before it restarts the helper, and appends one
|
|
904
|
+
block between `# >>> lanes agent hand-off >>>` and `# <<< lanes agent hand-off <<<` to the file your login shell reads
|
|
905
|
+
(zsh `~/.zprofile`; bash the first of `~/.bash_profile`, `~/.bash_login`, `~/.profile` that exists; other shells
|
|
906
|
+
`~/.profile`). At each local login the block hands that login's agent over and restarts the helper — only when the
|
|
907
|
+
user manager does not already have that agent, and never from an SSH login (`SSH_CONNECTION`), whose forwarded agent
|
|
908
|
+
disappears with it. A re-run adds nothing; `--uninstall` and `--no-uploads` remove the block (backup kept). Installed
|
|
909
|
+
over SSH, nothing is handed over until your next local login. fish reads no POSIX profile: the installer names the
|
|
910
|
+
line to add instead.
|
|
911
|
+
A profile the installer cannot write (a read-only dotfiles checkout, a root-owned or immutable file, a full disk) is
|
|
912
|
+
left exactly as it was — a write that stops part-way is put back from its backup — and the installer names the line
|
|
913
|
+
to add, hands this terminal's agent over and finishes; `--uninstall` likewise leaves the block in such a file and says
|
|
914
|
+
which lines to delete.
|
|
915
|
+
The agent must hold the key (`AddKeysToAgent yes` in the `lanes` alias adds it on the first interactive login).
|
|
916
|
+
|
|
639
917
|
| Strip says | Meaning | Do |
|
|
640
918
|
| --- | --- | --- |
|
|
641
919
|
| `✗ not allowed on this machine: … - run: lanes-upload --allow FOLDER` | The file's folder is not on the allowlist (or you chose Deny) | `lanes-upload --allow <folder>`, then drop again |
|
|
@@ -657,7 +935,9 @@ catalog.json ──► workspaces.<workspace>.json ──► <repo>/.vscode/term
|
|
|
657
935
|
|
|
658
936
|
The site directory is `$LANES_WORKSPACES`, else `~/.config/lanes/workspaces/` when it holds a `catalog.json`, else
|
|
659
937
|
`workspaces/` in the checkout. Start from `lanes/workspaces.example/` ([reference](../workspaces.example/README.md)).
|
|
660
|
-
|
|
938
|
+
For a board with one repository, `lanes workspaces add <id> --container <name> --repo /home/coder/<repo> --write`
|
|
939
|
+
writes both files below. Without `--write` it prints the diff only. An id already in the catalog is checked, never
|
|
940
|
+
rewritten. A repository that another board already declares is refused. After any edit: `lanes sync && lanes doctor`.
|
|
661
941
|
|
|
662
942
|
**`catalog.json`**: `version` (1), `workspaces`: `{id, title, manifest}` in tab order. The `id` is also the upload
|
|
663
943
|
and watch-folder name; `title` is the tab. Changing a title renames the existing tab. Optional `sshWorkspaces` adds
|
|
@@ -668,7 +948,9 @@ fixed terminals on another host ([README](../README.md#register-a-workspace-once
|
|
|
668
948
|
to lane titles), `displayNames` (display overrides keyed by terminal name), `bindingOverrides` (a tmux binding for a
|
|
669
949
|
terminal that has none), and `cursor` (a second file to project the workspace into). `remoteShell.cwd` sets the
|
|
670
950
|
working directory; `monitor` adds the Throttle monitor tab; `discoveryRoots` extends the directories that associate
|
|
671
|
-
unregistered sessions with the workspace.
|
|
951
|
+
unregistered sessions with the workspace. A repository belongs to one board: `lanes cursor-sync` rewrites each
|
|
952
|
+
`registry` (and `cursor` file) with the lane ids of the board that names it, so a file two boards name flips on every
|
|
953
|
+
run.
|
|
672
954
|
|
|
673
955
|
**`.vscode/terminals.json`** (in the repository, inside the container):
|
|
674
956
|
|
|
@@ -786,6 +1068,13 @@ Names must be non-empty and free of control characters. The id, resolve file and
|
|
|
786
1068
|
lanes bound to a numeric session name, more than 4 lanes on one container's tmux socket `default`, orphan tmux
|
|
787
1069
|
socket files in a container (report only; inside the container `~/.lz/bin/lane gc --sockets` removes the ones tmux
|
|
788
1070
|
is refused on), and records left in the retired `legacy-sessions/` directory.
|
|
1071
|
+
- Browser and phone views: `lanes-web` / `lanes-mobile` active (`✓`), not enabled (`⚠`: no browser view yet, or
|
|
1072
|
+
running without being enabled), enabled but not running (`✗`); the login-token database the running `lanes-web`
|
|
1073
|
+
reads, from its own environment: the one `lanes token` uses (`✓`), another one (`✗`, see
|
|
1074
|
+
[Tokens](#tokens)), or not compared (`⚠`); the Tailscale Serve mapping `https:8443 -> http://127.0.0.1:8083`
|
|
1075
|
+
present (`✓`), missing or pointing elsewhere (`✗` once a web unit is enabled, `⚠` before), or not checked (`⚠`: no
|
|
1076
|
+
tailscale, no answer, or no Tailscale name on the host — then `lanes web` configures no Serve and prints a loopback
|
|
1077
|
+
URL, so doctor does not ask for the mapping either).
|
|
789
1078
|
|
|
790
1079
|
## 7. Agents in lanes
|
|
791
1080
|
|
|
@@ -804,9 +1093,39 @@ anything, picks the first signal that applies:
|
|
|
804
1093
|
|
|
805
1094
|
A Codex title left on a bare shell with no child process is `shell` (the harness ended).
|
|
806
1095
|
|
|
1096
|
+
### A user's agents in one step: `lanes onboard`
|
|
1097
|
+
|
|
1098
|
+
On the server, one command gives a new user's workspace container what its agents need. Without `--write` it prints
|
|
1099
|
+
every diff and command and changes nothing:
|
|
1100
|
+
|
|
1101
|
+
```sh
|
|
1102
|
+
lanes onboard anna --container ws-anna --workspace anna-tools --repo /home/coder/tools # the plan
|
|
1103
|
+
lanes onboard anna --container ws-anna --workspace anna-tools --repo /home/coder/tools --write # apply it
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
| Step | What the agents get |
|
|
1107
|
+
| --- | --- |
|
|
1108
|
+
| Board | A workspace tab for the repository (`lanes workspaces add`, section 6) on every device's view |
|
|
1109
|
+
| Contract | `lane`, `lane-run`, `lane-signal` and the hook definitions in the container's `~/.lz` (`lanes install-container`) |
|
|
1110
|
+
| Hooks | The lane-signal hooks for Claude Code and Codex ("Hooks" below), so the sidebar's agent glyphs are event-driven |
|
|
1111
|
+
| Throttle | `throttle setup --install-clients` inside the container when Throttle is installed and configured there, so the agents' requests go through the pool; otherwise the one-time setup to run on a terminal is printed (a first setup signs in, which needs a person; Lanes runs without Throttle) |
|
|
1112
|
+
| Recipes | `lanes cursor-sync <id> --canonicalize --write` when the repository has a `.vscode/terminals.json`: each lane's create recipe gives it its identity and trace ("Lane identity" below) |
|
|
1113
|
+
| Sync and doctor | The registry knows the new lanes; `lanes doctor`'s result is reported, never fatal |
|
|
1114
|
+
|
|
1115
|
+
The receipt is `~/.local/state/lanes/onboard/<login>-<UTC>.json`, and running the command again changes nothing that
|
|
1116
|
+
is already done. `--no-mcp` leaves out `lane-mcp`, the in-container MCP server, which has not shipped yet. Every step
|
|
1117
|
+
and status: [install guide](INSTALL.md#onboard-a-users-container).
|
|
1118
|
+
|
|
1119
|
+
**Laptop agents on the pool (operator-gated).** Agents a user runs on their own laptop, outside any lane, can use the
|
|
1120
|
+
same Throttle pool over the tailnet once the operator shares it (`throttle remote share --tailnet`, unpublished). When
|
|
1121
|
+
the server's `~/.config/lanes/site.conf` names `LANES_THROTTLE_REMOTE_URL`, `lanes onboard` prints and records that
|
|
1122
|
+
step for the user: `throttle remote join <pairing code>` on the laptop
|
|
1123
|
+
([install guide](INSTALL.md#laptop-agents-on-the-throttle-pool-operator-gated)).
|
|
1124
|
+
|
|
807
1125
|
### Hooks (opt-in, per container, dry run first)
|
|
808
1126
|
|
|
809
|
-
Hooks make the state event-driven. Nothing installs one
|
|
1127
|
+
Hooks make the state event-driven. Nothing installs one unasked: `lanes onboard --write` (above) installs both, or
|
|
1128
|
+
install them one by one.
|
|
810
1129
|
|
|
811
1130
|
```sh
|
|
812
1131
|
lanes install-container <name> # puts lane-signal in ~/.lz/bin
|
|
@@ -816,7 +1135,9 @@ lanes hooks install --agent codex --container <name> --write # ~/.codex/hook
|
|
|
816
1135
|
```
|
|
817
1136
|
|
|
818
1137
|
`lanes hooks install --agent claude|codex (--container <name> | --home <dir>) [--dry-run | --write]`: a dry run
|
|
819
|
-
unless `--write`; your own hooks and keys stay, and an earlier lane-signal install is replaced, not duplicated.
|
|
1138
|
+
unless `--write`; your own hooks and keys stay, and an earlier lane-signal install is replaced, not duplicated. An
|
|
1139
|
+
install that is already current is left where it is, even when another tool (Throttle's `setup --install-clients`)
|
|
1140
|
+
has put its own group before or after it or re-sorted the file's keys, so running it again writes nothing. For
|
|
820
1141
|
Codex it also adds a top-level `notify` to `~/.codex/config.toml` only when none exists. Codex asks you to review and
|
|
821
1142
|
trust new hooks; Lanes never writes that trust. Expect hooks to reach agent sessions started after the install
|
|
822
1143
|
(inferred from how both harnesses read their configuration, not measured). Undo:
|
|
@@ -930,8 +1251,11 @@ Covered in section 3: one notification per lane entering Needs you, from the sid
|
|
|
930
1251
|
### Push notifications (opt-in, operator gate)
|
|
931
1252
|
|
|
932
1253
|
`lanes-notify.service` sends one push to an [ntfy](https://ntfy.sh) topic when a lane's agent enters needs you
|
|
933
|
-
(`Needs you`, `Needs approval`, `Error`). It reads only `status.json
|
|
934
|
-
|
|
1254
|
+
(`Needs you`, `Needs approval`, `Error`). It reads only `status.json`, the registry and `enrol-requests/`, every 3 s.
|
|
1255
|
+
A push says
|
|
1256
|
+
`<lane name>: <agent task> — <status>` and `lanes-launch lane <id>`. It also pushes once per new enrolment request
|
|
1257
|
+
(`Enrol request`: `<name or login> · <device>` and `lanes enrol approve <id>`), never muted by presence, once per
|
|
1258
|
+
request even across restarts. It sends lane names, task titles and requesters' names and device names to the server
|
|
935
1259
|
you configure, so enabling it is your decision. Not yet run against a real ntfy server or phone.
|
|
936
1260
|
|
|
937
1261
|
`~/.config/lanes/notify.conf` (`LANES_NOTIFY_CONF` at install time renders another path), `key=value` lines:
|
|
@@ -940,7 +1264,7 @@ you configure, so enabling it is your decision. Not yet run against a real ntfy
|
|
|
940
1264
|
| --- | --- | --- |
|
|
941
1265
|
| `url` | required | The ntfy server, `https://…` |
|
|
942
1266
|
| `topic` | required | `[A-Za-z0-9_-]{1,64}`; on a public server the topic name is the only secret |
|
|
943
|
-
| `token_file` | unset | A file holding an ntfy access token (an unreadable file disables sending) |
|
|
1267
|
+
| `token_file` | unset | A file holding an ntfy access token (an unreadable file disables sending; written, rotated or made readable later, it is picked up within 3 s) |
|
|
944
1268
|
| `quiet_when_device_active_s` | `120` | Presence window; `0` never suppresses |
|
|
945
1269
|
| `action_url` | unset | A URL template over `{lane}`; becomes the tap target and an **Open lane** button |
|
|
946
1270
|
|
|
@@ -981,8 +1305,9 @@ the per-device flags; the variables matter when you run the sidebar by hand.
|
|
|
981
1305
|
|
|
982
1306
|
**Ghostty palette** (opt-in): by default the lanes window inherits your everyday Ghostty look. `./install.sh --palette`
|
|
983
1307
|
installs `~/.config/lanes/ghostty-palette.conf` generated from the tokens (a light/dark theme pair on Ghostty 1.1 or
|
|
984
|
-
newer when your everyday config sets no colours explicitly, else the dark palette) and includes it last;
|
|
985
|
-
|
|
1308
|
+
newer when your everyday config sets no colours explicitly, else the dark palette) and includes it last; the choice
|
|
1309
|
+
is remembered, and `--no-palette` removes it. Not yet verified on a real Ghostty: the window background should then
|
|
1310
|
+
be `#101318`.
|
|
986
1311
|
|
|
987
1312
|
**Fonts**: no Nerd Font is needed. Any monospace font with Latin-1, Box Drawing, Geometric Shapes, Dingbats, Arrows and
|
|
988
1313
|
`⋮ ⏎ ⚠ …` renders the cockpit; `--ascii` is the remedy for any other font.
|
|
@@ -1036,12 +1361,13 @@ All units run in `lanes.slice` and are rendered by the server installer from `la
|
|
|
1036
1361
|
| `lanes-sync.timer` / `.service` | Every 2 min | Server install | `lanes sync` |
|
|
1037
1362
|
| `lanes-ensure.timer` / `.service` | Every 1 min | Server install | `lanes ensure`: recreate missing device view sessions |
|
|
1038
1363
|
| `lanes-gc.timer` / `.service` | Daily | Server install | `lanes gc`: truncate zmx logs over 5 MB, drop stale dynamic resolve files |
|
|
1039
|
-
| `lanes-
|
|
1364
|
+
| `lanes-enrold.service` | Always on | Server install (enabled and restarted by every one) | Enrolment service on `127.0.0.1:8184`, reached as `https://<host>:8443/enrol/*`: invite redemption, enrolment requests, `/enrol/pending` approvals ([RUNBOOK](RUNBOOK.md#enrolment-service-lanes-enrold-w8-a3)) |
|
|
1365
|
+
| `lanes-web.service` | Always on | `enable-web.sh` (via `lanes web` / `enable-phone.sh`) | Zellij web server on `127.0.0.1:8082` |
|
|
1040
1366
|
| `lanes-mobile.service` | Always on | `enable-web.sh` | Caddy frontend on `127.0.0.1:8083` |
|
|
1041
1367
|
| `lanes-studio-host.service` | Always on | `enable-studio-host.sh` (opt-in) | The co-located Throttle Studio Host |
|
|
1042
1368
|
| `lanes-studio-host-enrol.timer` / `.service` | Every 5 min | `enable-studio-host.sh` (opt-in) | Adopt a new receipt or key, then start the Host |
|
|
1043
1369
|
| `lanes-economy.service` | Every 10 s | Nobody (opt-in, operator gate) | Economy projection `economy.json` |
|
|
1044
|
-
| `lanes-notify.service` | Every 3 s | Nobody (opt-in, operator gate) | ntfy push on needs-you transitions |
|
|
1370
|
+
| `lanes-notify.service` | Every 3 s | Nobody (opt-in, operator gate) | ntfy push on needs-you transitions and new enrolment requests |
|
|
1045
1371
|
| `lanes-tmpreap.timer` / `.service` | Hourly | Nobody (opt-in) | `lanes tmp reap`; a dry run until `~/.local/state/lanes/flags/tmpreap.armed` exists |
|
|
1046
1372
|
| `lanes-guard.timer` / `.service` | Every 60 s | Nobody (opt-in) | `lanes guard --once`: raise `oom_score_adj` of builds in the containers |
|
|
1047
1373
|
|
|
@@ -1059,9 +1385,11 @@ Clients also get `lanes-helper.service` and `lanes-upload.path`/`.service` (Linu
|
|
|
1059
1385
|
| Registry | `lanes register <lane-id> [workspace]` | Add an unregistered session to a workspace without restarting it |
|
|
1060
1386
|
| Registry | `lanes rename <lane-id> <new-name>` | Change a lane's display name (not in the usage text) |
|
|
1061
1387
|
| Registry | `lanes workspaces` | Print the workspace ids (used by the client installer) |
|
|
1388
|
+
| Registry | `lanes workspaces add <id> --container <name> --repo <path> [--title T] [--session S] [--write]` | Add a one-repository board to the site catalog (a dry run until `--write`; section 6) |
|
|
1062
1389
|
| Views | `lanes attach <device> [--client ID] [--mobile] [--viewports N] [--ascii] [--glyphs …] [--theme …] [--color …]` | Attach this terminal to the device's view (create it if needed); record the look |
|
|
1063
1390
|
| Views | `lanes prepare <device> [--mobile\|--desktop] [--viewports N]` | Create a view without attaching; a different explicit layout is refused |
|
|
1064
|
-
| Views | `lanes web <device> [--desktop\|--mobile] [--viewports N] [--token \| --print-only]` | Browser view in one step (section 4) |
|
|
1391
|
+
| Views | `lanes web <device> [--desktop\|--mobile] [--viewports N] [--token \| --print-only]` | Browser view in one step, Tailscale Serve included (section 4) |
|
|
1392
|
+
| Views | `lanes token list` / `revoke <device>` / `revoke --name NAME` / `revoke-all [--yes]` | Web login tokens and their devices (section 4) |
|
|
1065
1393
|
| Views | `lanes open <lane-id>` | Attach this terminal straight to one lane |
|
|
1066
1394
|
| Views | `lanes layout <device> [--mobile]` | Print the generated Zellij layout |
|
|
1067
1395
|
| Views | `lanes rebuild <device>` | Recreate a device's view (agents never touched) |
|
|
@@ -1073,11 +1401,19 @@ Clients also get `lanes-helper.service` and `lanes-upload.path`/`.service` (Linu
|
|
|
1073
1401
|
| Views | `lanes preview --focused [--device D]` | What `Ctrl-Alt-m` runs |
|
|
1074
1402
|
| Editor | `lanes cursor-sync [ws…] [--write] [--diff] [--canonicalize] [--identity]` | Section 6 |
|
|
1075
1403
|
| Editor | `lanes cursor-commit [ws…] [--push]` | Section 6 |
|
|
1076
|
-
| Containers | `lanes install-container <name
|
|
1404
|
+
| Containers | `lanes install-container <name> [--no-mcp]` | Install the `lane` contract (section 2); `--no-mcp` is for the future `lane-mcp` and changes nothing today |
|
|
1405
|
+
| Containers | `lanes onboard <login> --container <name> [--workspace <id> --repo <path> [--title T]] [--write] [--no-mcp]` | One user's container in one command: board, contract, Claude and Codex hooks, Throttle attachment, canonical recipes, sync, doctor, and a receipt (a dry run until `--write`; section 2) |
|
|
1077
1406
|
| Containers | `lanes hooks install --agent claude\|codex (--container <name> \| --home <dir>) [--dry-run \| --write]` | Section 7 |
|
|
1078
1407
|
| Uploads | `lanes upload <ws> \| --active <device> [--paste]` | Read a tar stream into that workspace's `uploads/` |
|
|
1079
1408
|
| Client | `lanes hello [device]` | Prove path, key and server version |
|
|
1080
1409
|
| Client | `lanes client-config` | Site facts for clients (`LANES_*` lines from `~/.config/lanes/site.conf`) |
|
|
1410
|
+
| Access | `lanes keys list \| add <device> <pubkey-file\|-> \| revoke <device> \| adopt [--dry-run]` | Who may log in: the lanes-managed block of `~/.ssh/authorized_keys`, projected from `members/` |
|
|
1411
|
+
| Access | `lanes keys owner <device> <login> [--name NAME] [--email ADDR]` | Name the person behind a member; a key added by hand has none, and `/enrol/pending` lets in only a login a member names ([RUNBOOK](RUNBOOK.md#enrolment-service-lanes-enrold-w8-a3)) |
|
|
1412
|
+
| Access | `lanes invite <name> [--email ADDR] [--expires 7d] [--workspaces a,b] [--tailscale-link URL] [--md]` | Mint a one-time code and print the welcome kit, on a terminal only (section 2, "Invite a user") |
|
|
1413
|
+
| Access | `lanes invite list \| revoke <id>` | The invites and their state; refuse a code from now on |
|
|
1414
|
+
| Access | `lanes enrol list [--all] \| approve <id> [--as <device>] \| deny <id> [--as <device>]` | The enrolment requests; decide one as `/enrol/pending` does |
|
|
1415
|
+
| Access | `lanes enrol mode [request\|invite\|auto]` | Pin request mode across restarts (or say what is pinned) |
|
|
1416
|
+
| Access | `lanes offboard <device> --yes` | Revoke the key, forget the device, sign out the web login tokens of the machine and its views, end its view (own terminal only) |
|
|
1081
1417
|
| Health | `lanes doctor` | Full health check (section 6) |
|
|
1082
1418
|
| Health | `lanes pressure` | Memory / io / cpu pressure, swap, per-container memory and `/tmp`; plain files only; exit 1 unless ok |
|
|
1083
1419
|
| Health | `lanes invariant save\|check` | Snapshot / verify every live pane's pid and window size |
|
|
@@ -1096,9 +1432,12 @@ The client helpers call three more verbs (`rebuilt`, `upload-requests`, `upload-
|
|
|
1096
1432
|
| --- | --- |
|
|
1097
1433
|
| `lanes-window [--ascii] [--light\|--dark] [suffix]` | Open the lanes window (section 3) |
|
|
1098
1434
|
| `lanes-launch [device \| workspace \| lane <id> \| ls]` | The reconnecting client (section 3) |
|
|
1435
|
+
| `lanes-launch web phone\|laptop\|<device> [--desktop\|--mobile] [--no-qr]` | A browser view plus a login token, in this terminal (section 4) |
|
|
1436
|
+
| `lanes-launch update [install.sh options]` | The newest client from npm with your remembered install options (section 2) |
|
|
1099
1437
|
| `lanes-launch --helper <device> [--service \| --while-pid PID]` | Run only the upload helper (the service form is what the installer sets up) |
|
|
1100
1438
|
| `lanes-upload …` | Uploads (section 5) |
|
|
1101
|
-
| `sh install/enable-phone.sh …` | Phone or browser view plus token (section 4) |
|
|
1439
|
+
| `sh install/enable-phone.sh …` | Phone or browser view plus token; on the server a wrapper over `lanes web … --token` (section 4) |
|
|
1440
|
+
| `npx @vibelord/lanes --invite <code>` / `--site NAME` / `--resume` | Join a site with an invite, install or update without one, wait again for an access request (section 2) |
|
|
1102
1441
|
| `./install.sh …` | Install, check, update, uninstall (section 2) |
|
|
1103
1442
|
|
|
1104
1443
|
### State files
|
|
@@ -1118,13 +1457,22 @@ Server (`~/.local/state/lanes/`, `$LANES_STATE`):
|
|
|
1118
1457
|
| `user-sessions/<id>.json` | Names of unregistered sessions |
|
|
1119
1458
|
| `upload-requests/<device>/` | Upload hand-off between sidebar and helper |
|
|
1120
1459
|
| `economy.json`, `economy.cursor.json` | The opt-in economy projection and its log cursor |
|
|
1460
|
+
| `notify.enrol-seen.json` | The enrolment requests the opt-in push notifier already announced (0600, pruned to the requests on disk); deleting it announces the ones still pending once more |
|
|
1121
1461
|
| `flags/` | `coexist`, `readonly`, `tmpreap.armed`, `sockets-lost` |
|
|
1122
1462
|
| `cursor-sync-backups/` | Backups taken by `cursor-sync --write` |
|
|
1123
1463
|
| `invariant.json` | `lanes invariant save` |
|
|
1124
1464
|
| `tmpscan/` | `lanes tmp` plans and `deleted.log` |
|
|
1125
1465
|
| `studio-host/` | Optional Studio Host: public key, receipt, enrolment record |
|
|
1126
|
-
|
|
1127
|
-
|
|
1466
|
+
| `members/<device>.json`, `members.lock` | Who each enrolled machine key belongs to (`lanes keys`; the managed block of `~/.ssh/authorized_keys` is its projection) and the lock every writer of it holds |
|
|
1467
|
+
| `invites/<id>.json` | Invites minted by `lanes invite`: the code's hash only (never the code), who minted it, expiry, invitee, and the redemption or revocation |
|
|
1468
|
+
| `enrol-requests/<id>.json` | Enrolment requests waiting for, or past, a member's approval (24 h) |
|
|
1469
|
+
| `enrol.log` | The enrolment audit log, one line per attempt, invite, revocation, offboard and mode change (0600, append-only; never a code) |
|
|
1470
|
+
| `enrol-mode` | Request mode as `lanes enrol mode request\|invite` pinned it (0600; absent = auto); the enrolment service reads it on every request |
|
|
1471
|
+
| `web-tokens.json`, `web-tokens.lock` | Which view — and which machine — each web login token `lanes web --token` minted was for (names and creation times only, never a token; `lanes offboard` signs a machine out by it) and the lock every mint and revocation holds (section 4, "Tokens") |
|
|
1472
|
+
| `onboard/<login>-<UTC>.json` | One receipt per `lanes onboard` run: each step with its status (section 7) |
|
|
1473
|
+
|
|
1474
|
+
Other server paths: `~/.config/lanes/` (`workspaces/`, `site.conf`, `notify.conf`, and the optional invite key files
|
|
1475
|
+
`resend.key` and `tailscale-api.key`), `~/.local/share/lanes/` (pinned
|
|
1128
1476
|
binaries in `bin/`, the `current` checkout link, npm-staged `pkg/<version>/`), and `~/.lz/<container>` (links to each
|
|
1129
1477
|
container's `.lz`).
|
|
1130
1478
|
|
|
@@ -1132,9 +1480,9 @@ In each container (`/home/coder/.lz/`): `bin/` (`lane`, `lane-run`, `lane-signal
|
|
|
1132
1480
|
definitions), `env.sh`, `registry.json` (the projection `lanes sync` writes), `state/hooks/` (hook sidecars),
|
|
1133
1481
|
`state/<zmx name>.json` (`lane-run` phase records), `meta/` and `s/` (zmx metadata and sockets).
|
|
1134
1482
|
|
|
1135
|
-
Client: `~/.config/lanes/` (`device`, `ghostty.conf`, `client-site.conf`, `client-known-hosts`,
|
|
1136
|
-
`upload-allow`, `uploads-dir
|
|
1137
|
-
`helper-service.log`), `~/.local/share/lanes/client/`.
|
|
1483
|
+
Client: `~/.config/lanes/` (`device`, `install-options`, `ghostty.conf`, `client-site.conf`, `client-known-hosts`,
|
|
1484
|
+
`site.cache`, `upload-allow`, `uploads-dir`, and `enrol-request` while an access request is open),
|
|
1485
|
+
`~/.local/state/lanes-client/` (`launch-<target>.log`, `helper.alive`, `helper-service.log`), `~/.local/share/lanes/client/`.
|
|
1138
1486
|
|
|
1139
1487
|
### NEVER
|
|
1140
1488
|
|
|
@@ -1162,8 +1510,19 @@ From the [RUNBOOK](RUNBOOK.md#never):
|
|
|
1162
1510
|
| Phone keyboard does not open, or opens unexpectedly | The keyboard opens only from **Type here** or an input | Tap **Type here**; quick keys never open it | [RUNBOOK](RUNBOOK.md#web--phone) |
|
|
1163
1511
|
| Phone shows no counts | Phone view laid out before the picker change | `lanes rebuild <device>` | [RUNBOOK](RUNBOOK.md#web--phone) |
|
|
1164
1512
|
| `lanes web` printed a `127.0.0.1` URL | The host has no Tailscale name | Connect Tailscale on the host; there is no public fallback | [INSTALL](INSTALL.md#laptop-and-tablet-browser) |
|
|
1513
|
+
| `lanes web` exits 64 and prints `sudo tailscale set --operator=…` | The workspace account may not manage Tailscale Serve (or HTTPS is off for the tailnet) | Run the printed step as the server admin, then `lanes web` again | [RUNBOOK](RUNBOOK.md#tailscale-serve) |
|
|
1514
|
+
| `lanes doctor`: `✗ lanes-web.service reads login tokens from …`, or a fresh token is refused at sign-in | The running web server reads another token database than `lanes token` (started before the `XDG_DATA_HOME` pin) | `lanes web <device>`, `systemctl --user restart lanes-web.service`, then `lanes web <device> --token` | [RUNBOOK](RUNBOOK.md#login-tokens) |
|
|
1515
|
+
| `lanes web --token` says the next token name exists | Zellij 0.45.1 names tokens `token_<count + 1>`; an older token was revoked | `lanes token revoke-all --yes` (every browser signs in again) | [RUNBOOK](RUNBOOK.md#login-tokens) |
|
|
1165
1516
|
| Upload `✗ not allowed on this machine` | Folder not on the helper's allowlist | `lanes-upload --allow <folder>` | [RUNBOOK](RUNBOOK.md#uploads-the-cursor-uploads-folder-without-cursor) |
|
|
1166
1517
|
| Upload `✗ no upload helper` | Helper service not serving | `./install.sh --check` on the client | [RUNBOOK](RUNBOOK.md#what-you-see-and-what-it-means) |
|
|
1518
|
+
| Installer: `tailscale: not installed`, `needs login` or `not on the tailnet after 120 s`, exit 4 | This machine is not in the tailnet the profile's server lives in | Install or connect Tailscale (the installer opens `tailscale login` itself), then run the installer again | [Section 2](#tailscale) |
|
|
1519
|
+
| Installer: `invite: not redeemed — the server answered …` or `enrolment: no answer from …`, exit 3 | The code was refused (used, expired, revoked) or the server runs no enrolment service yet | Ask a member for a new invite, or send the printed public key to the administrator | [Section 2](#no-enrolment-service-the-administrator-authorizes-the-key) |
|
|
1520
|
+
| Installer: `request: no answer within 10:00 — it stays open`, exit 3 | No member has approved the access request yet | `npx @vibelord/lanes --resume` (or `./install.sh --resume`) waits again | [Section 2](#no-code-ask-for-access) |
|
|
1521
|
+
| A phone or browser of an offboarded machine still signs in | Its token is not attributed to the machine: minted before 1.1.0, outside `lanes web --token`, or `lanes offboard` warned or ended incomplete at step 3 | Read offboard's step 3 line; `lanes token list`, then `lanes token revoke --name token_N` (or `lanes token revoke-all --yes`) | [install guide](INSTALL.md#offboard-a-machine) |
|
|
1522
|
+
| Installer: `invite: not redeemed — the server answered 409 (device-taken)` (or `key-taken`), exit 3 | The server already has a device of that name (two machines with one hostname, or an earlier enrolment), or holds this key for another device | The installer names the re-run: the same command with `--device NAME` (the invite is still valid) or with a key of its own; if the old machine is gone, a member runs `lanes offboard <device> --yes` | [Section 2](#join-with-an-invite) |
|
|
1523
|
+
| `lanes-launch web`: "the server runs lanes 1.0.x, older than this client" | This client is newer than the server (a client updates itself first) | The view and its token work; there is no QR code and the view's upload box does not reach this machine until the server runs `npx @vibelord/lanes@latest --server` | [Versions](#versions) |
|
|
1524
|
+
| `/enrol/pending`: "Refused: the browser did not say where the form came from (no Sec-Fetch-Site)" | The Approve/Deny POST came without the browser's `Sec-Fetch-Site` header: curl or a script, or a browser that does not send it | Approve from a current browser, or on the server: `lanes enrol approve <id>` | [install guide](INSTALL.md#approve-a-request) |
|
|
1525
|
+
| Nobody can file an access request after the server restarted (`requests-disabled`), although it worked before | Request mode is unpinned and the first request through Serve after the restart carried no tailnet identity (a tagged device) | On the server: `lanes enrol mode request` (after the header check) | [install guide](INSTALL.md#approve-a-request) |
|
|
1167
1526
|
| Installer: "an older server?" / `lane open`: "registry is newer than this lane" / "site file … newer schema" | Client, server or container contract at different versions | Update the server (`./install.sh --server`), re-run `lanes install-container <name>`, update the client checkout | Section 2 |
|
|
1168
1527
|
| Client window holds with a countdown | Connection lost or refused | Leave it open; `Enter` retries now | [RUNBOOK](RUNBOOK.md#what-you-see-and-what-it-means) |
|
|
1169
1528
|
| Wrong colours in Codex's input box | Codex sampled another client's background at start | Use the same light/dark choice on every client | [RUNBOOK](RUNBOOK.md#tuis-and-the-terminals-colours-codexs-dark-box-the-stars) |
|
|
@@ -1190,9 +1549,15 @@ Server and views:
|
|
|
1190
1549
|
| `LANES_NOTIFY_CONF` | `libexec/notify.py` | `~/.config/lanes/notify.conf` | Push notifier config path |
|
|
1191
1550
|
| `LANES_HOST_GUARD_CONF` | `sudo/host-guard.sh` | `/etc/lanes-host-guard.conf` | Root guard config |
|
|
1192
1551
|
| `LANES_PROC_ROOT`, `LANES_CGROUP_ROOT` | `bin/lanes`, `libexec/guard.py`, `libexec/view.sh` (the first); `libexec/guard.py` (the second) | `/proc`, `/sys/fs/cgroup` | Test injection only |
|
|
1552
|
+
| `LANES_ENROL_PORT` | `libexec/enrold.py` | `8184` | The enrolment service's loopback port (the Caddyfile's `/enrol/*` route must follow) |
|
|
1553
|
+
| `LANES_ENROL_DOMAINS` | `libexec/enrold.py` | `beyondwork.ai` | Login domains that may file an access request or approve one on `/enrol/pending` (comma or space separated; `*` = any); invites are not gated |
|
|
1554
|
+
| `LANES_ENROL_REQUEST_MODE` | `libexec/enrold.py` | unset | `1` forces request mode on, `0` forces invite-only, over any `lanes enrol mode` pin; unset, the pin decides, and without one the first request through Tailscale Serve after a start (RUNBOOK "Enrolment service") |
|
|
1555
|
+
| `LANES_DEVICE` | `libexec/enrol.py` (`lanes invite`) | unset | Recorded as `minted_by.device` of an invite when it is a device name (a claim, not an identity) |
|
|
1556
|
+
| `SSH_CLIENT` | `libexec/enrol.py` (`lanes enrol approve\|deny`) | set by sshd | Its address is recorded in the approver `cli[:<device>]@<address>`; `local` without one |
|
|
1193
1557
|
| `LANES_TESTS_FORCE` | `tests/guards.py` | unset | `1` runs live suites on a busy host (on purpose only) |
|
|
1194
1558
|
|
|
1195
|
-
`LANES_THROTTLE_*` and `LANES_NOTIFY_CONF` set in the installer's environment are rendered into their units only.
|
|
1559
|
+
`LANES_THROTTLE_*` and `LANES_NOTIFY_CONF` set in the installer's environment are rendered into their units only. The
|
|
1560
|
+
`LANES_ENROL_*` settings go in a drop-in, `systemctl --user edit lanes-enrold.service`, which a re-install keeps.
|
|
1196
1561
|
`LANES_REPO`, `LANES_ZELLIJ` and `LANES_WS` are set by Lanes itself for its units and panes.
|
|
1197
1562
|
|
|
1198
1563
|
Client:
|
|
@@ -1200,8 +1565,12 @@ Client:
|
|
|
1200
1565
|
| Variable | Read by | Default | Effect |
|
|
1201
1566
|
| --- | --- | --- | --- |
|
|
1202
1567
|
| `LANES_HOST` | `lanes-launch`, `lanes-upload` | `lanes` | SSH alias of the server |
|
|
1203
|
-
| `LANES_FALLBACK_HOST` | `lanes-launch` | `lanes-public` | Fallback SSH
|
|
1568
|
+
| `LANES_FALLBACK_HOST` | `lanes-launch` | `lanes-ip lanes-public` | Fallback SSH aliases, tried in order (each only when the SSH config defines it) |
|
|
1204
1569
|
| `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME` | `install.sh`, client scripts | `~/.config`, `~/.local/share`, `~/.local/state` | Where client files go |
|
|
1570
|
+
| `LANES_TAILSCALE_WAIT` | `install.sh` | `120` | Seconds an install waits for this machine to join the tailnet (a `.ts.net` profile) |
|
|
1571
|
+
| `LANES_INVITE` | `install.sh`, `npm/lanes` wrapper | unset | An invite code, as `--invite` (the wrapper picks the site from its prefix) |
|
|
1572
|
+
| `LANES_ENROL_WAIT`, `LANES_ENROL_POLL` | `install.sh` | `600`, `5` | Seconds an install waits for an access request's approval, and between two looks |
|
|
1573
|
+
| `LANES_ENROL_URL` | `install.sh` | `https://<LANES_SERVER_NAME>:8443` | The enrolment service's base URL (development and tests) |
|
|
1205
1574
|
| `LANES_PKG_ROOT` | `npm/lanes` wrapper | `~/.local/share/lanes/pkg` | Where npm-staged trees go |
|
|
1206
1575
|
| `LANES_NPM_ENV_FILE`, `LANES_NPM_TOKEN_KEY` | `npm/lanes/scripts/publish.sh` | `.env.local`, `NPMJS2` | Maintainers only: the publish token's file and key |
|
|
1207
1576
|
|
|
@@ -1221,16 +1590,20 @@ Container:
|
|
|
1221
1590
|
|
|
1222
1591
|
| Command | Codes |
|
|
1223
1592
|
| --- | --- |
|
|
1224
|
-
| `install.sh`, `npx @vibelord/lanes` | `0` ready · `3` key not yet authorized · `4` server unreachable or too slow · `1` error |
|
|
1593
|
+
| `install.sh`, `npx @vibelord/lanes` | `0` ready · `3` key not yet authorized (no enrolment service, a refused invite, or an unanswered request) · `4` not in the tailnet, server unreachable or too slow · `1` error (also: a malformed invite code, or one for another site) |
|
|
1225
1594
|
| `install.sh --server` (`host-install.sh`) | `0` · `1` missing prerequisite, not Linux x86_64, or run as root · `2` unknown option |
|
|
1226
1595
|
| `lanes <verb>` | `0` · `1` error (message `lanes: …`) or unknown verb (usage printed) |
|
|
1227
1596
|
| `lanes sync` | `1` when any workspace's registry had errors (previous lanes kept) |
|
|
1228
1597
|
| `lanes doctor`, `lanes pressure`, `lanes invariant check`, `lanes sockets` | `1` when something needs attention |
|
|
1229
|
-
| `lanes attach <workspace-id>` without a default device, `lanes web --token` without a terminal | `64` |
|
|
1598
|
+
| `lanes attach <workspace-id>` without a default device, `lanes web --token` without a terminal or while the running `lanes-web.service` reads another token database, `lanes web` when Tailscale Serve cannot be set (operator step printed) | `64` |
|
|
1599
|
+
| `lanes token revoke-all` without `--yes` | `2` (nothing changed) |
|
|
1230
1600
|
| `lanes preview` / `next` / `prev` without `--focused` | `2` |
|
|
1231
1601
|
| `lanes studio-host status` | `0` only when running enrolled; `1` otherwise; `2` usage |
|
|
1602
|
+
| `lanes invite <name>` | `0` minted (the kit printed; a failed email or Tailscale mint is a note, not an error) · `2` not on a terminal, nothing minted · `1` error |
|
|
1603
|
+
| `lanes invite revoke <id>` | `0` revoked or already revoked · `1` already redeemed (offboard the device) or no such invite |
|
|
1604
|
+
| `lanes offboard <device> --yes` | `0` · `2` without `--yes` or not on a terminal (the plan printed, nothing changed) · `1` stopped at the key or device step, a key line left outside the block, the web login tokens could not be revoked, or the view could not be ended |
|
|
1232
1605
|
| `enable-studio-host.sh` | `0` running or waiting for enrolment · `2` usage · `3` typed refusal · `1` failure |
|
|
1233
|
-
| `enable-phone.sh` | `0` · `2` usage · `64` not in your own terminal · `1`
|
|
1606
|
+
| `enable-phone.sh` | `0` · `2` usage · `64` not in your own terminal, Tailscale Serve refused, or the running `lanes-web` reads another token database (from `lanes web`) · `1` a `lanes web` error |
|
|
1234
1607
|
| `enable-web.sh` | `0` · `2` usage · `1` Caddy missing |
|
|
1235
1608
|
| `lanes-launch` | `64` invalid name or option (it never exits on its own otherwise); server exits 64–78 are refusals it retries after 60 s |
|
|
1236
1609
|
| `lanes-window` | `64` invalid suffix · `69` no Ghostty (macOS) or neither Ghostty nor Foot (Linux) |
|