@vibelord/lanes 1.0.3 → 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 +55 -15
- package/bin/lanes-install.mjs +8 -2
- package/lib/plan.mjs +34 -1
- package/package.json +2 -2
- package/payload/lanes/README.md +91 -33
- 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 +88 -3
- package/payload/lanes/docs/INSTALL.md +297 -43
- package/payload/lanes/docs/RUNBOOK.md +458 -31
- package/payload/lanes/docs/USERGUIDE.md +1612 -0
- 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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
The same `lanes/` directory installs either a desktop client or the workspace server. A deployment needs
|
|
4
4
|
one server and one client installation per desktop. Safari uses the optional web services on the server.
|
|
5
5
|
Configuration identifies your server and workspaces; credentials and generated runtime state stay outside Git.
|
|
6
|
+
For daily use after installation, see the [user guide](USERGUIDE.md).
|
|
6
7
|
|
|
7
8
|
## 1. Prepare a workspace host
|
|
8
9
|
|
|
@@ -13,6 +14,12 @@ Install missing packages using your distribution's package manager. The installe
|
|
|
13
14
|
Run the server as an unprivileged workspace account with Docker access. Container homes must be mounted at
|
|
14
15
|
`/home/coder` with UID 1000; the host account must be able to read the mounted files and reach those users'
|
|
15
16
|
Unix sockets through `/proc`. This is the current container contract, including on a host provisioned without Coder.
|
|
17
|
+
The host account must also have UID 1000, the container's. `lanes install-container`, `lanes hooks install` and
|
|
18
|
+
`lanes onboard` write into the container homes as the host account, so a file they create belongs to the host
|
|
19
|
+
account's UID. With any other UID the agent inside cannot read the private ones (`.claude/settings.json` is 0600)
|
|
20
|
+
and cannot write into a directory the host account created. On such a host `lanes onboard` stops at its first check
|
|
21
|
+
and writes nothing (exit 1), `lanes hooks install --write` into a container refuses with the reason, and
|
|
22
|
+
`lanes doctor` warns about each container. Run them from a host account with UID 1000.
|
|
16
23
|
Containers need Bash, Python 3 and tmux. Use compatible tmux versions on the host and containers (tested with
|
|
17
24
|
tmux 3.4): the host client attaches directly to the container server. Zmx is installed as an optional holder,
|
|
18
25
|
not a migration of existing lanes.
|
|
@@ -39,20 +46,31 @@ only if no configuration exists. Host protection, temporary-file deletion and we
|
|
|
39
46
|
|
|
40
47
|
## 2. Configure workspaces and editor sync
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
Add one board per repository. `lanes workspaces add` writes the site catalog and the board's manifest for you:
|
|
50
|
+
`catalog.json` gains `{id, title, manifest}` and a new `workspaces.<id>.json` names the container and the
|
|
51
|
+
repository's `.vscode/terminals.json`. It prints the diff first and writes only with `--write`:
|
|
43
52
|
|
|
44
53
|
```sh
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
$EDITOR ~/.config/lanes/workspaces/catalog.json
|
|
48
|
-
$EDITOR ~/.config/lanes/workspaces/workspaces.product.json
|
|
49
|
-
$EDITOR ~/.config/lanes/workspaces/workspaces.research.json
|
|
54
|
+
lanes workspaces add product --title Product --container my-workspace-container --repo /home/coder/product
|
|
55
|
+
lanes workspaces add product --title Product --container my-workspace-container --repo /home/coder/product --write
|
|
50
56
|
lanes install-container my-workspace-container
|
|
51
57
|
lanes sync
|
|
52
58
|
lanes doctor
|
|
53
59
|
```
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
The catalog goes where the server reads it: `$LANES_WORKSPACES`, else `~/.config/lanes/workspaces/` (created on a
|
|
62
|
+
fresh host), or a single-site checkout's own `workspaces/catalog.json`. The first write keeps a
|
|
63
|
+
`catalog.json.bak-<UTC>Z` copy beside it. `--repo` is the repository folder as
|
|
64
|
+
the container sees it, under `/home/coder`. The title defaults to the id, and every tab title must be unique.
|
|
65
|
+
A repository belongs to one board: a container and folder that another board already declares is refused before
|
|
66
|
+
anything is written, because `lanes cursor-sync` would rewrite that repository's `.vscode/terminals.json` for both
|
|
67
|
+
boards.
|
|
68
|
+
Running the command again for the same id changes nothing. If the catalog already describes that id differently,
|
|
69
|
+
the command prints the differences, writes nothing and exits 1. Boards with several repositories, prefixes,
|
|
70
|
+
`remoteShell`, a monitor tab or SSH workspaces are still edited by hand: start from
|
|
71
|
+
[`workspaces.example/`](../workspaces.example/README.md) (`cp -R workspaces.example/. ~/.config/lanes/workspaces/`).
|
|
72
|
+
|
|
73
|
+
A complete terminal registry is provided in
|
|
56
74
|
[`workspaces.example/terminals.example.json`](../workspaces.example/terminals.example.json): adapt it to the
|
|
57
75
|
repository's `.vscode/terminals.json` inside the container. Every terminal needs an explicit tmux socket/session
|
|
58
76
|
binding; there is no guessed command parsing. See [the configuration reference](../workspaces.example/README.md).
|
|
@@ -69,6 +87,80 @@ A lane that has not started is shown as absent. Open its terminal in Cursor / VS
|
|
|
69
87
|
The viewer itself only attaches. `lanes cursor-commit --push` is a separate, explicit publishing action;
|
|
70
88
|
it is not run by installation or automatic sync.
|
|
71
89
|
|
|
90
|
+
### Onboard a user's container
|
|
91
|
+
|
|
92
|
+
`lanes onboard` does every server-side step for one user's workspace container in one command. Without `--write` it
|
|
93
|
+
prints each diff and each command it would run, and changes nothing:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
lanes onboard anna --container ws-anna --workspace anna-tools --repo /home/coder/tools
|
|
97
|
+
lanes onboard anna --container ws-anna --workspace anna-tools --repo /home/coder/tools --write
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Both forms first check the container with `docker inspect` (the `inspect` step), which only reads. An unknown
|
|
101
|
+
container, for example a mistyped `--container`, stops the run before anything is written, so no board names a
|
|
102
|
+
container that does not exist. The same step compares the host account's UID and the home volume's owner with the
|
|
103
|
+
container's UID 1000 ([section 1](#1-prepare-a-workspace-host)). On a mismatch the step fails and the run stops
|
|
104
|
+
there, in a dry run too: no board, no contract, no hooks, no Throttle step and no other write. Every later step is
|
|
105
|
+
listed as `not-run` with the reason, and the exit code is 1, as for an unknown container. Run it again from a host
|
|
106
|
+
account with UID 1000. With `--write` it then applies these steps in order:
|
|
107
|
+
|
|
108
|
+
1. **Board.** Runs `lanes workspaces add`, when `--workspace` and `--repo` are given. `--title` defaults to the id
|
|
109
|
+
or to the title the board already has. A board that already disagrees stops the run before the container is
|
|
110
|
+
touched.
|
|
111
|
+
2. **Contract.** Runs `lanes install-container`.
|
|
112
|
+
3. **Agent hooks.** Runs `lanes hooks install --write`, first for Claude and then for Codex. Your own hooks and
|
|
113
|
+
keys are kept, with a backup of each changed file. A Lanes hook group that is already installed stays where
|
|
114
|
+
it is, even when Throttle's setup has added its own group around it.
|
|
115
|
+
4. **Throttle.** Asks the container, as UID 1000, whether it has a `throttle` and whether Throttle is configured
|
|
116
|
+
there (`~/.config/throttle/config.toml` exists). Only a configured Throttle gets `throttle setup
|
|
117
|
+
--install-clients`, which then runs without a person. A first setup needs one: it signs in to claude.ai with a
|
|
118
|
+
code pasted on a terminal, or adopts the container's own Claude login as a pool account. So when Throttle is
|
|
119
|
+
missing or not configured, the command prints the step to run once on a terminal,
|
|
120
|
+
`docker exec -it -u 1000 <container> sh -lc 'throttle setup --install-clients'`, records it as `manual` and
|
|
121
|
+
continues, because Lanes runs without Throttle. The setup output is not shown: it names accounts and credential
|
|
122
|
+
sources. When the server's
|
|
123
|
+
`~/.config/lanes/site.conf` sets `LANES_THROTTLE_REMOTE_URL`, the command also prints a laptop step: `throttle remote join` with a
|
|
124
|
+
pairing code from the operator.
|
|
125
|
+
5. **Sync.** Runs `lanes sync`, so the registry knows the board.
|
|
126
|
+
6. **Recipes.** Runs `lanes cursor-sync <id> --canonicalize --write` when `<repo>/.vscode/terminals.json` exists
|
|
127
|
+
in the container. When the file is missing, the command points at
|
|
128
|
+
[`terminals.example.json`](../workspaces.example/terminals.example.json) instead. When this step rewrote the
|
|
129
|
+
file, a second `lanes sync` copies the new recipes into the container's `~/.lz/registry.json`, which
|
|
130
|
+
`lane open` reads.
|
|
131
|
+
7. **Doctor.** Runs `lanes doctor`. Its result is reported but does not stop onboarding.
|
|
132
|
+
|
|
133
|
+
The run ends with a `key: value` table and a receipt in `~/.local/state/lanes/onboard/<login>-<UTC>.json`:
|
|
134
|
+
`{version, login, container, workspace, at, steps: [{name, status, detail}]}`. A status is one of `planned`,
|
|
135
|
+
`changed`, `unchanged`, `ok`, `skipped`, `manual` (a step left for a person, with its command), `warn`, `failed` or
|
|
136
|
+
`not-run`. When a step stops the run, every step after it is still listed, as `not-run`, in the table and the receipt.
|
|
137
|
+
The exit code is 1 only when a step failed. Running the command again changes nothing that is already done.
|
|
138
|
+
`--no-mcp` leaves out `lane-mcp`, the in-container MCP server, which has not shipped yet. Until it does, the
|
|
139
|
+
receipt records that step as skipped.
|
|
140
|
+
Each user's board still shows on every device's view. A per-device subset of boards is a separate follow-up.
|
|
141
|
+
|
|
142
|
+
### Laptop agents on the Throttle pool (operator-gated)
|
|
143
|
+
|
|
144
|
+
Lanes agents run in the containers and reach Throttle there. Agents a user runs on their own laptop can use the same
|
|
145
|
+
pool over the tailnet, without a public relay: Throttle's `throttle remote share --tailnet` and `throttle remote join`
|
|
146
|
+
(source-current, unpublished; the Throttle user guide's "Remote clients over Tailscale" section, which lands with that
|
|
147
|
+
change, in [docs/throttle-userguide.md](../../docs/throttle-userguide.md)). Lanes only prints the laptop step; every
|
|
148
|
+
operator step stays with the operator, because each one changes the production daemon:
|
|
149
|
+
|
|
150
|
+
1. Decide to share. The remote key is also the daemon's control-plane admin credential: hand it only to people you
|
|
151
|
+
would give operator access.
|
|
152
|
+
2. Where the daemon's configuration lives, run `throttle remote share --tailnet` (the key from `--key-file` or stdin,
|
|
153
|
+
never an argument). It sets only `server.remote_key` and prints the rest.
|
|
154
|
+
3. Do the printed steps: `throttle setup --install-clients` (loopback clients need the managed token once a remote key
|
|
155
|
+
is set), `server.host` for a containerised daemon, `throttle restart`, and on the host `tailscale serve --bg
|
|
156
|
+
--https=10000 http://<address>:<data port>`. Lanes' own front keeps 8443.
|
|
157
|
+
4. Put the resulting URL in the server's `~/.config/lanes/site.conf` as
|
|
158
|
+
`LANES_THROTTLE_REMOTE_URL=https://<host>.<tailnet>.ts.net:10000`.
|
|
159
|
+
|
|
160
|
+
From then on `lanes onboard` prints, and records as a `manual` step in its receipt, `on the user's laptop: throttle
|
|
161
|
+
remote join <pairing code>`; the pairing code comes from the operator's share. Without that line in `site.conf`,
|
|
162
|
+
onboarding says nothing about laptops. No Lanes command shares the pool or touches the daemon.
|
|
163
|
+
|
|
72
164
|
## 3. Give clients a site profile
|
|
73
165
|
|
|
74
166
|
Copy [`site/site.example.conf`](../site/site.example.conf) and fill in the server's reachable hostname,
|
|
@@ -83,10 +175,9 @@ The actual known_hosts line has three fields: alias, `ssh-ed25519`, and the base
|
|
|
83
175
|
`ssh_host_ed25519_key` (the private key). [Site instructions](../site/README.md) cover profiles and updates.
|
|
84
176
|
The supplied `site/beyondwork/` profile is an explicit choice for the existing team server, not a default.
|
|
85
177
|
|
|
86
|
-
On each Mac or Linux desktop, install from npm (
|
|
178
|
+
On each Mac or Linux desktop, install from npm (public package, no npm login; Node.js 18+):
|
|
87
179
|
|
|
88
180
|
```sh
|
|
89
|
-
npm login # once per machine, or a read-only token in ~/.npmrc
|
|
90
181
|
npx @vibelord/lanes --site beyondwork # bundled team profile; or --site /path/to/site.conf
|
|
91
182
|
```
|
|
92
183
|
|
|
@@ -101,57 +192,202 @@ cd bw-tools/lanes
|
|
|
101
192
|
|
|
102
193
|
OpenSSH is required. Install Ghostty for a dedicated window, or run `lanes-launch` in your existing terminal.
|
|
103
194
|
Linux also supports Foot, plus optional Omarchy/Hyprland shortcuts. Tailscale is required when your profile uses
|
|
104
|
-
a tailnet address.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
195
|
+
a tailnet address. When the profile's `LANES_SERVER_NAME` ends in `.ts.net`, the installer checks Tailscale before
|
|
196
|
+
the server: without it, it prints `tailscale: not installed — macOS: brew install --cask tailscale · Linux:
|
|
197
|
+
https://tailscale.com/download` and exits 4 (the client is installed); on a logged-out machine it prints
|
|
198
|
+
``tailscale: needs login — opening `tailscale login`… (approve in the browser)``, runs `tailscale login` and waits up
|
|
199
|
+
to 120 s (`LANES_TAILSCALE_WAIT`) with a countdown until the machine is online, then prints `tailscale: connected as
|
|
200
|
+
<login> (<address>)`. On Linux an account without Tailscale operator rights is told to run `sudo tailscale up` once.
|
|
201
|
+
`--check` only looks. The installer installs its own configuration; your normal terminal remains usable.
|
|
202
|
+
|
|
203
|
+
When this machine resolves the profile's MagicDNS name, the SSH alias `lanes` names the server by it (a server that
|
|
204
|
+
gets a new tailnet address keeps working), and a second alias, `lanes-ip`, holds the address. `lanes-launch` falls
|
|
205
|
+
back to `lanes-ip` after two immediate connect failures, then to `lanes-public` when the site has one. A machine
|
|
206
|
+
without MagicDNS keeps the address in `lanes`. Every alias pins the same host key under the profile's alias.
|
|
207
|
+
|
|
208
|
+
### Join with an invite or a request
|
|
209
|
+
|
|
210
|
+
On a server that runs the enrolment service `lanes-enrold` (every server install enables it), a new machine
|
|
211
|
+
authorizes itself. The user's side, step by step, is the user guide's
|
|
212
|
+
[Join a site](USERGUIDE.md#2-join-a-site): join the tailnet, run the installer with the code, then `lanes-launch`,
|
|
213
|
+
`lanes-launch web phone` and `lanes-launch web laptop`. With an invite code, `npx @vibelord/lanes --invite
|
|
214
|
+
LN-xxxx-xxxxxxxxxxxx` (the code's prefix picks the bundled profile whose `site.conf` says `LANES_SITE_CODE=LN`; an
|
|
215
|
+
explicit `--site` wins) installs the client, redeems the code and prints `invite: redeemed — this machine's key is now
|
|
216
|
+
authorized for <login> (device <device>)`. Without a code, the installer asks on a terminal; Enter files an access
|
|
217
|
+
request that a member approves (the installer waits up to 10 minutes; `--resume` waits again). The installer calls
|
|
218
|
+
`GET /enrol/site`, then `POST /enrol/redeem` or `POST /enrol/request` plus `GET /enrol/request/<id>`, on
|
|
219
|
+
`https://<LANES_SERVER_NAME>:8443` (with curl, or python3 without it; `--resolve` to the tailnet address when MagicDNS is
|
|
220
|
+
off). What the client reads of lanes-enrold's wire contract is in [the architecture](ARCHITECTURE.md#joining-a-site).
|
|
221
|
+
The code is never shown, stored, or put on a command line. A malformed code, or one with another site's prefix, is refused before anything is installed
|
|
222
|
+
(exit 1).
|
|
223
|
+
|
|
224
|
+
Without the service, after a refused code, or with no code and no terminal, authorize the printed public key on the
|
|
225
|
+
server, as the workspace account: `lanes keys add <device> <file.pub>` (or `lanes keys add <device> -` and paste the
|
|
226
|
+
line) writes it into the lanes-managed block of `~/.ssh/authorized_keys` (ed25519 only; every line outside the block
|
|
227
|
+
is kept; see the runbook's "Who may log in"). The client never edits that file; on the server the enrolment service
|
|
228
|
+
and `lanes keys` write it. Then `./install.sh --check` and `lanes-window`.
|
|
229
|
+
|
|
230
|
+
### Client options
|
|
231
|
+
|
|
232
|
+
Useful options:
|
|
108
233
|
|
|
109
234
|
- `--device NAME`: independent view name; a stable per-machine name is the default.
|
|
110
|
-
- `--key PATH`: choose an existing SSH private key; the key stays on the client.
|
|
111
|
-
- `--
|
|
235
|
+
- `--key PATH`: choose an existing SSH private key; the key stays on the client (enrolment takes an ed25519 key).
|
|
236
|
+
- `--invite CODE` / `--resume`: join with an invite code, or wait again for an open access request (per run).
|
|
237
|
+
- `--no-desktop` / `--no-uploads`: omit those integrations; on a machine where an earlier install added them they are
|
|
238
|
+
taken away (the upload agents stopped and removed, the launcher entry and key binding removed; `~/Lanes Uploads`
|
|
239
|
+
and its files stay). `--desktop` / `--uploads` put them back.
|
|
240
|
+
- `--palette` / `--no-palette`, `--coder-launch` / `--no-coder-launch`: the cockpit's Ghostty palette and the
|
|
241
|
+
`coder-launch` command name, on or off.
|
|
112
242
|
- `--link`: develop against the checkout instead of installing a client copy.
|
|
113
243
|
- `--yes`: noninteractive installation; a newly created dedicated key has no passphrase.
|
|
114
244
|
|
|
115
|
-
|
|
245
|
+
Every choice except `--invite`, `--resume`, `--link` and `--yes` is remembered in `~/.config/lanes/install-options`
|
|
246
|
+
(plain `KEY=VALUE`, mode 0600, never executed; the runbook's
|
|
247
|
+
[Remembered install options](RUNBOOK.md#remembered-install-options) lists the keys): subsequent installs, updates and checks can omit them, and a flag given again overrides
|
|
248
|
+
the remembered value. A bundled profile is remembered by name (an update reads the new package's copy), your own
|
|
249
|
+
profile file by path; the selected profile is also copied to `~/.config/lanes/client-site.conf`. `--uninstall`
|
|
250
|
+
removes both. On Linux with a key that has a passphrase, the upload helper service logs in through the SSH agent your
|
|
251
|
+
systemd user manager holds, looked up again before every login (so an agent your desktop session hands over after
|
|
252
|
+
the service started is used from its next login on, at most a minute later); the installer hands your terminal's
|
|
253
|
+
agent over and adds one marked block (`# >>> lanes agent hand-off >>>`) to your login profile that repeats this at
|
|
254
|
+
each local login (`--no-uploads` and `--uninstall` remove it); a profile it cannot write (a read-only dotfiles
|
|
255
|
+
checkout, a full disk) is left as it was, the installer prints the line to add and finishes the install; see [the
|
|
256
|
+
user guide](USERGUIDE.md#5-uploads).
|
|
116
257
|
On macOS, the signed Lanes Helper app requests access to protected folders. Allow Downloads/Desktop/Documents
|
|
117
258
|
as needed. The allowlist is separate from macOS permission; inspect it with `lanes-upload --allowed`.
|
|
118
259
|
|
|
119
|
-
|
|
260
|
+
### Invite a user
|
|
120
261
|
|
|
121
|
-
|
|
122
|
-
loopback ports **8082** (native Zellij) and **8083** (Lanes frontend); private HTTPS uses **8443**.
|
|
123
|
-
Reserve those ports for Lanes on this host. Caddy runs as a separate user service with its admin API disabled.
|
|
262
|
+
Instead of hand-adding each key, a member mints an invite on the server (or over `ssh lanes`), in their own terminal:
|
|
124
263
|
|
|
125
|
-
|
|
264
|
+
```sh
|
|
265
|
+
lanes invite anna --email anna@example.com --workspaces bw-tools,throttle # prints the five-line welcome kit
|
|
266
|
+
lanes invite list # live / redeemed / revoked / expired
|
|
267
|
+
lanes invite revoke <id> # refuse that code from now on
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The kit carries a one-time code `LN-<id>-<secret>` (case-insensitive, valid 7 days; `--expires 1h`…`30d`) that the
|
|
271
|
+
new machine redeems at the enrolment service (`lanes-enrold`, RUNBOOK "Enrolment service") through the client's
|
|
272
|
+
`--invite` (W8 B3): its key enters the
|
|
273
|
+
lanes-managed block of `~/.ssh/authorized_keys` without anyone editing it. Only the code's hash is stored; the code is
|
|
274
|
+
printed once, and only to a terminal (`lanes invite` refuses with exit `2` when piped). Give `--email` on an
|
|
275
|
+
invite-only server: it becomes the new member's owner login. Two optional, Lanes-owned key files add to it —
|
|
276
|
+
`~/.config/lanes/resend.key` (a Resend API key on its first line) mails the kit to `--email`; to reach anyone but
|
|
277
|
+
you, add a `from=Name <addr>` line on a domain verified in Resend, because without it the kit goes from Resend's test
|
|
278
|
+
sender, which delivers only to the Resend account's own address (Resend answers 403 and the note says so). The
|
|
279
|
+
other, `~/.config/lanes/tailscale-api.key` (a user-owned Tailscale API access token), mints step 1's tailnet invite
|
|
280
|
+
link; `--tailscale-link URL` pastes one from the Tailscale admin console instead. Keep both files `0600`.
|
|
281
|
+
|
|
282
|
+
A machine that comes without a code asks instead: `lanes enrol list`, then `lanes enrol approve <id>` ("Approve a
|
|
283
|
+
request" below). A machine leaves with `lanes offboard <device> --yes` ("Offboard a machine" below).
|
|
284
|
+
|
|
285
|
+
### Approve a request
|
|
286
|
+
|
|
287
|
+
A machine that comes without a code files an access request when its user presses Enter at the installer's prompt
|
|
288
|
+
(the user guide's [No code: ask for access](USERGUIDE.md#no-code-ask-for-access)); the installer waits up to 10
|
|
289
|
+
minutes, and the request stays open for 24 hours. Any member decides it:
|
|
126
290
|
|
|
127
291
|
```sh
|
|
128
|
-
|
|
292
|
+
lanes enrol list # the open requests: id, device, login, the key's fingerprint
|
|
293
|
+
lanes enrol approve <id> [--as <your-device>] # writes the member and its key line, exactly as the web page does
|
|
294
|
+
lanes enrol deny <id>
|
|
295
|
+
lanes enrol list --all # also the approved, denied and expired ones
|
|
129
296
|
```
|
|
130
297
|
|
|
131
|
-
|
|
132
|
-
|
|
298
|
+
On a phone or laptop on the tailnet, `https://<server>:8443/enrol/pending` lists the same requests with **Approve**
|
|
299
|
+
and **Deny**. It admits a tailnet login that owns a member record, and keys added by hand (`lanes keys add`, `lanes
|
|
300
|
+
keys adopt`) record no owner, so name yourself once on one of your own devices: `lanes keys owner <your-device>
|
|
301
|
+
<your tailnet login>`. With the opt-in push notifier (`lanes-notify.service`), each new request arrives as one push
|
|
302
|
+
naming the requester, the device and the approve command. The CLI records where a decision came from
|
|
303
|
+
(`cli[:<device>]@<address>`), not who made it (everyone over SSH is the one workspace account); the web page records
|
|
304
|
+
the real tailnet login. Requests need request mode, which needs Tailscale Serve to hand the service a tailnet
|
|
305
|
+
identity. Run the runbook's [operator verification](RUNBOOK.md#enrolment-service-lanes-enrold-w8-a3) once after the
|
|
306
|
+
first server install, then pin the result, so it holds across every restart and reinstall:
|
|
133
307
|
|
|
134
308
|
```sh
|
|
135
|
-
|
|
309
|
+
lanes enrol mode request # the header check passed: take access requests
|
|
310
|
+
lanes enrol mode invite # it did not (or other accounts share this host): invites only
|
|
311
|
+
lanes enrol mode # what is pinned; `auto` unpins it
|
|
136
312
|
```
|
|
137
313
|
|
|
138
|
-
|
|
314
|
+
Unpinned (`auto`), the first request that comes through Tailscale Serve after each start decides, so a tagged device
|
|
315
|
+
or a forged header at the wrong moment can turn requests off — or on — until the next restart. `LANES_ENROL_DOMAINS`
|
|
316
|
+
(default `beyondwork.ai`) limits which logins may file a request, and which may approve one on the web page.
|
|
317
|
+
|
|
318
|
+
### Offboard a machine
|
|
319
|
+
|
|
320
|
+
In your own terminal (without `--yes`, or piped, it prints the plan, changes nothing and exits 2):
|
|
139
321
|
|
|
140
322
|
```sh
|
|
141
|
-
|
|
323
|
+
lanes offboard anna-mbp --yes # its key, its device record, its browsers and phones, its view session
|
|
324
|
+
lanes offboard anna-mbp-2 --yes # each window it names: forget the device, end that view
|
|
142
325
|
```
|
|
143
326
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
327
|
+
`lanes offboard <device> --yes` takes the key out of the lanes-managed block of `~/.ssh/authorized_keys` and removes
|
|
328
|
+
`members/<device>.json`, forgets the device so `lanes ensure` does not bring its view back, signs the machine's
|
|
329
|
+
browsers and phones out, and ends its view session. No lane, agent or container is touched, and an SSH session
|
|
330
|
+
already open with the key keeps running until it ends. Signing out revokes every web login token `lanes web --token`
|
|
331
|
+
recorded for the machine: its own, its windows' (`<device>-2` from `lanes-window 2`), and those of the views
|
|
332
|
+
`lanes-launch web phone|laptop` opened for it (`<device>-phone`, `<device>-web`) — never another machine's. It
|
|
333
|
+
revokes them even when that leaves Zellij 0.45.1 unable to create the next token, and then names `lanes token
|
|
334
|
+
revoke-all --yes`, which repairs that (and signs every browser and phone out); a token nobody can attribute (minted
|
|
335
|
+
before 1.1.0, or outside `lanes web --token`) is listed with `lanes token revoke --name NAME`
|
|
336
|
+
([runbook](RUNBOOK.md#login-tokens)). A step that fails (Zellij does not answer, the web server reads another token
|
|
337
|
+
database) leaves the offboard incomplete, exit 1: run it again. The windows share the revoked key; offboard names them
|
|
338
|
+
and leaves their views running, so offboard each by name to end it. A redeemed invite is not revoked by `lanes invite
|
|
339
|
+
revoke`: offboarding its device is how access ends.
|
|
340
|
+
|
|
341
|
+
## 4. Optional Safari / Home Screen access
|
|
342
|
+
|
|
343
|
+
Install Caddy (2.5 or later: the `/enrol/*` route uses `reverse_proxy { trusted_proxies … }`, which older versions
|
|
344
|
+
refuse; the Debian 12 and Ubuntu 24.04 packages are 2.6) and Tailscale on the server and enable Tailscale HTTPS for
|
|
345
|
+
its tailnet. The web services use
|
|
346
|
+
loopback ports **8082** (native Zellij) and **8083** (Lanes frontend); private HTTPS uses **8443**. The enrolment
|
|
347
|
+
service every server install runs (`lanes-enrold`) listens on loopback **8184** and is reached as `/enrol/*` through
|
|
348
|
+
the frontend.
|
|
349
|
+
Reserve those ports for Lanes on this host. Caddy runs as a separate user service with its admin API disabled.
|
|
350
|
+
|
|
351
|
+
Once, a server administrator allows the workspace account to manage Tailscale Serve:
|
|
352
|
+
|
|
353
|
+
```sh
|
|
354
|
+
sudo sh sudo/one-time-root.sh --user workspace
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
This does not change SSH daemon settings unless `--ssh-keepalive` is explicitly supplied. That is the whole server
|
|
358
|
+
setup: each user then runs, from their installed Mac/Linux client, in their own interactive terminal:
|
|
359
|
+
|
|
360
|
+
```sh
|
|
361
|
+
lanes-launch web phone # their phone's view, <device>-phone (phone layout)
|
|
362
|
+
lanes-launch web laptop # their laptop's browser view, <device>-web (desktop layout)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`lanes-launch web` runs `lanes web <view> --token --default-device <device>` on the server over `ssh -t lanes`, with
|
|
366
|
+
the SSH key the client installer selected (no password fallback, no agent forwarding). On the server itself the same
|
|
367
|
+
step is `lanes web phone --mobile --token`; `sh install/enable-phone.sh --host lanes` from a client is the older
|
|
368
|
+
one-shot for the device `phone`. `lanes web` prepares the view, enables the loopback services, makes Tailscale Serve
|
|
369
|
+
forward private HTTPS 8443 to the frontend, prints the URL with a QR code of it and shows a fresh token in your
|
|
370
|
+
terminal. The Serve step reads `tailscale serve status --json` first: a mapping of `https:8443` to
|
|
371
|
+
`http://127.0.0.1:8083` is left alone, a missing one is added with
|
|
372
|
+
`tailscale serve --bg --https=8443 http://127.0.0.1:8083`, and a mapping of 8443 to anything else is never replaced
|
|
373
|
+
(exit 64, with the command that frees the port). When Tailscale refuses the account, `lanes web` exits 64 and prints
|
|
374
|
+
the `sudo tailscale set --operator=<account>` step above. Existing tokens remain valid.
|
|
147
375
|
|
|
148
376
|
Connect Tailscale on the phone, open the printed URL in Safari, paste only the token value, then
|
|
149
|
-
**Share → Add to Home Screen**. A token grants terminal access as the server account.
|
|
377
|
+
**Share → Add to Home Screen**. A token grants terminal access as the server account. Manage tokens with:
|
|
150
378
|
|
|
151
379
|
```sh
|
|
152
|
-
|
|
380
|
+
lanes token list # every token, with the device it was created for
|
|
381
|
+
lanes token revoke phone # sign that device's token out
|
|
382
|
+
lanes token revoke --name token_1 # a token created before Lanes recorded devices, or outside it
|
|
383
|
+
lanes token revoke-all --yes # every token: each browser and phone signs in again
|
|
153
384
|
```
|
|
154
385
|
|
|
386
|
+
Zellij 0.45.1 cannot name a token: it calls each new one `token_<number of tokens + 1>`, so Lanes records which
|
|
387
|
+
device `lanes web --token` created it for. Because of that naming rule, revoking any token but the newest would leave
|
|
388
|
+
Zellij unable to create the next one; `lanes token revoke` refuses such a revocation and names `revoke-all` instead.
|
|
389
|
+
Revoking a token also deletes its sign-in sessions, so that browser has to sign in again.
|
|
390
|
+
|
|
155
391
|
`sh install/enable-web.sh` starts only the loopback services and prints no token. The frontend and services
|
|
156
392
|
run on the workspace host; a Mac does not need to stay awake for the phone to work.
|
|
157
393
|
|
|
@@ -167,21 +403,35 @@ The same private frontend serves every browser and chooses its controls once, wh
|
|
|
167
403
|
whole tab, so a desktop view's lane sidebar sits beside its lanes (**More → Single pane** shows one pane).
|
|
168
404
|
- **Phone**: the phone shell described above.
|
|
169
405
|
|
|
170
|
-
Give each browser its own view.
|
|
171
|
-
prints its private URL
|
|
406
|
+
Give each browser its own view. From an installed client, in your own terminal, one command does it all on the
|
|
407
|
+
server — prepares the view, enables the loopback web services, prints its private URL with a QR code of it (for a
|
|
408
|
+
phone camera; `--no-qr` leaves it out) and a fresh login token:
|
|
409
|
+
|
|
410
|
+
```sh
|
|
411
|
+
lanes-launch web phone # <your device>-phone with the phone layout
|
|
412
|
+
lanes-launch web laptop # <your device>-web with the desktop layout
|
|
413
|
+
lanes-launch web ipad --desktop # any other name is that device as given; --desktop / --mobile picks the layout
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
On the server the same command is `lanes web` (what `lanes-launch web` runs, with `--token` and
|
|
417
|
+
`--default-device <your device>`):
|
|
172
418
|
|
|
173
419
|
```sh
|
|
174
420
|
lanes web ipad --desktop # desktop sidebar layout (a new device's default); --mobile: phone lane pickers
|
|
175
|
-
lanes web ipad --desktop --token # the same, then a fresh login token in this terminal
|
|
176
|
-
lanes web ipad --print-only # shows the exact prepare call and
|
|
421
|
+
lanes web ipad --desktop --token # the same, then a QR code of the URL and a fresh login token in this terminal
|
|
422
|
+
lanes web ipad --print-only # shows the exact prepare call, URL and Serve step; changes nothing
|
|
177
423
|
```
|
|
178
424
|
|
|
179
|
-
From an installed client, `sh install/enable-phone.sh --host lanes
|
|
180
|
-
selected SSH key; without a name, `--desktop` uses the device `browser`. The
|
|
181
|
-
for `--desktop`. A device keeps the layout it was created with: `lanes web
|
|
182
|
-
(so `lanes web phone` reuses the phone's view), and an explicit request for the
|
|
183
|
-
another device name. `lanes web` reads the node's Tailscale name
|
|
184
|
-
|
|
425
|
+
From an installed client, `lanes-launch web <device> --desktop` or `sh install/enable-phone.sh --host lanes
|
|
426
|
+
--desktop ipad` does the same over the selected SSH key; without a name, `--desktop` uses the device `browser`. The
|
|
427
|
+
server needs this version of Lanes for `--desktop`. A device keeps the layout it was created with: `lanes web
|
|
428
|
+
<device>` without a flag reuses it (so `lanes web phone` reuses the phone's view), and an explicit request for the
|
|
429
|
+
other layout is refused; choose another device name. `lanes web` reads the node's Tailscale name and ensures the
|
|
430
|
+
Tailscale Serve mapping itself, as described above (its `serve:` line says `present` or `added`), so neither
|
|
431
|
+
`lanes-launch web` nor `enable-phone.sh` has a Serve step of its own. `lanes doctor` checks the same mapping (only
|
|
432
|
+
when the host has a Tailscale name, as `lanes web` does), the two web units, and that the running `lanes-web` reads
|
|
433
|
+
the login tokens from `~/.local/share/zellij/tokens.db`, the database `lanes web --token` and `lanes token` use (both
|
|
434
|
+
sides pin `XDG_DATA_HOME=%h/.local/share`; see the [runbook](RUNBOOK.md#login-tokens)).
|
|
185
435
|
|
|
186
436
|
On a network with a captive portal (hotel, train or aircraft Wi-Fi), connect in this order:
|
|
187
437
|
|
|
@@ -235,6 +485,10 @@ use the same overrides when reinstalling. `LANES_WORKSPACES` is also carried int
|
|
|
235
485
|
when supplied; the standard configuration directory is the simplest default.
|
|
236
486
|
|
|
237
487
|
After updating server code, restart the status observer with `systemctl --user restart lanes-statusd.service`.
|
|
488
|
+
The installer itself restarts the enrolment service (`lanes-enrold`) and a running web frontend (`lanes-mobile`, so
|
|
489
|
+
Caddy reads the new tree's Caddyfile; browser clients reconnect). On the first server install with enrolment, run the
|
|
490
|
+
runbook's operator verification and pin its result with `lanes enrol mode request` (or `invite`) — see
|
|
491
|
+
[Approve a request](#approve-a-request); a pinned mode survives this restart and every later one.
|
|
238
492
|
If you enabled the Studio Host and selected a new `throttle-rc` or edited its config, restart
|
|
239
493
|
`lanes-studio-host` (re-running `install/enable-studio-host.sh` does that, after `studio host --check`).
|
|
240
494
|
If web service definitions or the pinned Zellij binary changed, restart `lanes-web` and `lanes-mobile` too;
|
|
@@ -247,7 +501,7 @@ Normal paths:
|
|
|
247
501
|
| --- | --- |
|
|
248
502
|
| `~/.config/lanes/` | Site profile, client settings and server workspace configuration |
|
|
249
503
|
| `~/.local/share/lanes/` | Client copy, pinned binaries, download cache, installed checkout link and npm-staged trees (`pkg/<version>/`) |
|
|
250
|
-
| `~/.local/state/lanes/` | Host registry, device selections, status, upload handoff and housekeeping projections |
|
|
504
|
+
| `~/.local/state/lanes/` | Host registry, device selections, status, upload handoff and housekeeping projections; `members/`, `invites/`, `enrol-requests/` and the append-only `enrol.log` of the enrolment service |
|
|
251
505
|
| `~/.local/state/lanes/studio-host/` | Optional Studio Host: published public key, deploy receipt, enrolment record, migrated unit text (`lanes studio-host re-key --yes` clears the key half after a revoke) |
|
|
252
506
|
| `~/.config/throttle-rc/`, `~/.local/share/throttle-rc/` | Optional Studio Host config, machine key and enrolment receipt (throttle-rc's own files) |
|
|
253
507
|
| `~/.local/state/lanes-client/` | Client helper heartbeat and logs |
|