@vibelord/lanes 1.0.2 → 1.0.4
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 +3 -0
- package/package.json +1 -1
- package/payload/lanes/README.md +58 -21
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +21 -8
- package/payload/lanes/container/env.sh +1 -1
- package/payload/lanes/container/lane +5 -5
- package/payload/lanes/container/lane-signal +4 -4
- package/payload/lanes/docs/ARCHITECTURE.md +16 -8
- package/payload/lanes/docs/DESIGN.md +3 -3
- package/payload/lanes/docs/INSTALL.md +38 -2
- package/payload/lanes/docs/RUNBOOK.md +57 -27
- package/payload/lanes/docs/SHARING.md +239 -4
- package/payload/lanes/docs/USERGUIDE.md +1239 -0
- package/payload/lanes/holders.lock +10 -0
- package/payload/lanes/install/enable-studio-host.sh +14 -0
- package/payload/lanes/install/host-install.sh +5 -2
- package/payload/lanes/install/services.py +101 -4
- package/payload/lanes/install.sh +1 -1
- package/payload/lanes/libexec/agent_state.py +1 -1
- package/payload/lanes/libexec/cursor_sync.py +2 -2
- package/payload/lanes/libexec/economy_poll.py +3 -3
- package/payload/lanes/libexec/lanes_lib.py +1 -1
- package/payload/lanes/libexec/notify.py +3 -3
- package/payload/lanes/libexec/registrations.py +1 -1
- package/payload/lanes/libexec/session_actions.py +1 -1
- package/payload/lanes/libexec/sidebar.py +7 -7
- package/payload/lanes/libexec/statusd.py +25 -10
- package/payload/lanes/libexec/studio_host.py +1608 -0
- package/payload/lanes/systemd/lanes-economy.service +1 -1
- package/payload/lanes/systemd/lanes-studio-host-enrol.service +11 -0
- package/payload/lanes/systemd/lanes-studio-host-enrol.timer +10 -0
- package/payload/lanes/systemd/lanes-studio-host.service +37 -0
- package/payload/lanes/web/README.md +2 -2
- package/payload/lanes/zellij/config.kdl +1 -1
- package/payload/manifest.json +4 -3
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# lanes — runbook
|
|
2
2
|
|
|
3
3
|
`lanes` is the host-level workspace server for a deployment's container terminals. The canonical source is
|
|
4
|
-
`bw-tools/lanes`; [INSTALL.md](INSTALL.md) covers the full setup
|
|
5
|
-
its parts
|
|
6
|
-
|
|
4
|
+
`bw-tools/lanes`; [INSTALL.md](INSTALL.md) covers the full setup, [ARCHITECTURE.md](ARCHITECTURE.md) explains
|
|
5
|
+
its parts, and the [user guide](USERGUIDE.md) is the day-to-day reference for every surface, key and command.
|
|
6
|
+
Run host commands as the workspace account. Deployment-specific incidents below retain their original names and
|
|
7
|
+
measurements; substitute your account, containers and paths in operational commands.
|
|
7
8
|
|
|
8
9
|
## Daily use (client)
|
|
9
10
|
|
|
@@ -15,7 +16,7 @@ original names and measurements; substitute your account, containers and paths i
|
|
|
15
16
|
| One lane, no multiplexer, native Ghostty scrollback | `lanes-launch lane <lane-id>` (ids: `lanes-launch ls`) |
|
|
16
17
|
| Switch workspace | `Ctrl-Alt-←/→` (zellij tabs) |
|
|
17
18
|
| Next / previous lane | `Ctrl-Alt-↓` / `Ctrl-Alt-↑` (works from inside the lane: `lanes next\|prev --focused` in a floating pane that closes at once points the FOCUSED viewport — the first one when the sidebar has the focus — at the next / previous lane of its workspace in registry order, wrapping; nothing is typed and the focus stays where it was; on a tab without a workspace, e.g. Host, it does nothing). In the sidebar `J` / `K` still step through the list as it is drawn |
|
|
18
|
-
| Lock the view | `Ctrl-Alt-g` locks (every key
|
|
19
|
+
| Lock the view | `Ctrl-Alt-g` locks (the lane gets every key except the `shared` chords that stay live while locked — the Ctrl-Alt arrows, `/`, `u`, `c`, `s`, PageUp/PageDown/End; the mode ribbon shows LOCKED in on-accent-urgent) and unlocks. `Ctrl-g` is never bound: it always reaches the lane |
|
|
19
20
|
| Find a lane by name | `Ctrl-Alt-/`, type a few letters, `Enter` |
|
|
20
21
|
| Browse the sidebar | click it (or `Ctrl-Alt-p` then `←`), `↑/↓` or `j/k`, `Enter`; selecting a lane focuses it again |
|
|
21
22
|
| Go to the next lane that needs you | `Tab` in the sidebar opens the next lane in **Needs you** (`Shift-Tab` the previous), cycling from the lane this pane shows — or from the cursor lane when the shown one does not need you; ` nothing needs you` when the section is empty |
|
|
@@ -50,7 +51,7 @@ Since plan Task 26 the phone's picker pane has no fixed layout name, so Zellij c
|
|
|
50
51
|
session laid out after Task 26: an existing phone device keeps the old picker pane until `lanes rebuild <device>`
|
|
51
52
|
(`upgrade-session` and `ensure` add missing tabs and rename titles, they never re-lay-out a tab; `lanes doctor` names
|
|
52
53
|
such a device). Whether it
|
|
53
|
-
reaches the phone is still unchecked in a browser (DEF-
|
|
54
|
+
reaches the phone is still unchecked in a browser (DEF-1531). A lane that
|
|
54
55
|
STARTS needing you (`◆` `✗` `◇`) raises one desktop notification, `name: task`, through OSC 9 — once per transition,
|
|
55
56
|
never for the lane this pane shows, never on a phone, and not at all under `LANES_NOTIFY=0`. A sidebar that starts
|
|
56
57
|
(or restarts) while lanes already need you announces each of them once. Both are bytes in the frame the sidebar
|
|
@@ -120,13 +121,16 @@ lanes rebuild <device> recreate a view session lanes ensure recrea
|
|
|
120
121
|
lanes invariant save|check prove no live pane changed (pid + window size)
|
|
121
122
|
lanes device list the devices ensure keeps lanes device forget <name> stop recreating one
|
|
122
123
|
systemctl --user status lanes-statusd lanes-sync.timer lanes-ensure.timer lanes-web
|
|
124
|
+
lanes studio-host status [--check] the optional co-located Studio Host
|
|
125
|
+
lanes studio-host re-key --yes after the owner revoked the Host: a new machine key for the next deploy
|
|
126
|
+
(refused while lanes-sharing-host or throttle-rc-studio-host still runs)
|
|
123
127
|
```
|
|
124
128
|
|
|
125
129
|
- **What `lanes doctor` says about the boards' recipes (plan Task 18):** one line counts the tmux lanes per create
|
|
126
130
|
recipe class — `idempotent` (a `tmux new -A…`: creates, or attaches when the session exists; the canonical recipe),
|
|
127
131
|
`attach-only` (`attach-session` and no `new`), `null` (no recipe), `external` (a script or anything else, run as
|
|
128
132
|
written) and `retired-path` (names the retired in-container `~/.config/coder-launch`). One line says how many of
|
|
129
|
-
them create with their identity (`-e LANE_ID=…`, DEF-
|
|
133
|
+
them create with their identity (`-e LANE_ID=…`, DEF-1559). Warnings (`⚠`, never a `✗`, the exit code is
|
|
130
134
|
unchanged): lanes that `lane open` cannot recreate once they end (attach-only, null, retired-path — fix with
|
|
131
135
|
`lanes cursor-sync --canonicalize`), lanes bound to a numeric session name (tmux numbers unnamed sessions itself,
|
|
132
136
|
so a recreated one gets another number), more than 4 lanes on one container's tmux socket `default` (the server every
|
|
@@ -137,7 +141,7 @@ systemctl --user status lanes-statusd lanes-sync.timer lanes-ensure.timer lanes-
|
|
|
137
141
|
file is there, nobody listens) unlinks it, printing each path; a server that answers (even with no session), does
|
|
138
142
|
not answer within 5 s, or refuses for another reason (EACCES, …) keeps its file. The errno decides, never tmux's
|
|
139
143
|
wording: tmux 3.4 prints `no server running on <path>` for ECONNREFUSED and `error connecting to <path>
|
|
140
|
-
(<strerror>)` only for other errnos (DEF-
|
|
144
|
+
(<strerror>)` only for other errnos (DEF-1540).
|
|
141
145
|
- **A device you no longer use (an old laptop, a phone you replaced):** `lanes device forget <name>` removes
|
|
142
146
|
`devices/<name>.json`, so `lanes ensure` (`lanes-ensure.timer`) stops recreating its view session and `sync` /
|
|
143
147
|
`upgrade-session` stop updating it. A view session that is running is left alone and says so; once it ends, nothing
|
|
@@ -179,6 +183,30 @@ systemctl --user status lanes-statusd lanes-sync.timer lanes-ensure.timer lanes-
|
|
|
179
183
|
- **A new workspace:** add it to `~/.config/lanes/workspaces/catalog.json` + a manifest, `lanes sync`, then
|
|
180
184
|
`lanes rebuild <device>` (the only time a layout changes).
|
|
181
185
|
- **Registry invalid:** the workspace keeps its previous lanes; the sidebar footer and `lanes doctor` show why.
|
|
186
|
+
- **The Studio Host (optional, `lanes-studio-host.service`):** `lanes studio-host status` names the state —
|
|
187
|
+
`running`, `waiting-for-enrolment` (the deploy has not written a receipt yet; the unit is enabled and the enrol
|
|
188
|
+
timer active, so it installs the receipt within 5 minutes), `receipt-refused` (a receipt is on disk and the
|
|
189
|
+
installer refused it — the code, when, and the next retry are named; the timer's one line per tick is in
|
|
190
|
+
`journalctl --user -u lanes-studio-host-enrol.service`), `key-refused` (the registration key was tried and
|
|
191
|
+
`studio enroll` refused it — code, exit, when, next retry), `credential-for-gone-key` (a receipt on disk for a
|
|
192
|
+
machine key this machine no longer holds, for a row it has NOT measured revoked: revoke the row in the console,
|
|
193
|
+
then `lanes studio-host re-key --yes`; the installer never enrols a second machine beside it by itself),
|
|
194
|
+
`not-enabled` (not enrolled and the unit or the enrol
|
|
195
|
+
timer is not enabled, so nothing retries: re-run `sh install/enable-studio-host.sh`),
|
|
196
|
+
`stopped` (exit 3 is the Host's typed refusal: `journalctl --user -u lanes-studio-host.service -n 30`, then
|
|
197
|
+
`systemctl --user start lanes-studio-host.service` or re-run `sh install/enable-studio-host.sh` — a check red only on
|
|
198
|
+
that run's own recorded refusal no longer blocks the start; the retry timer does NOT restart it on the same
|
|
199
|
+
enrolment),
|
|
200
|
+
`receipt-unreadable` / `key-unreadable` (the file exists but this account cannot read it — e.g. a receipt a deploy
|
|
201
|
+
run as another account wrote 0600: give it to this account), `conflict` (another Host unit runs here: one Host per
|
|
202
|
+
machine), `not-installed`, or `unknown` (no answer from the user manager; a shell without `XDG_RUNTIME_DIR` falls
|
|
203
|
+
back to `/run/user/<uid>`). `status --check` exits 1 (`running-check-failed`) when the unit runs but its own
|
|
204
|
+
`studio host --check` does not pass. A receipt refused `receipt_server_mismatch`: fix the Host's `server` in
|
|
205
|
+
`~/.config/throttle-rc/studio-host.toml` — the timer retries the same receipt when the config changes, a manual
|
|
206
|
+
`sh install/enable-studio-host.sh` retries it at once. A new binary: `lanes studio-host install --from FILE --sha256 HEX`, then re-run
|
|
207
|
+
`sh install/enable-studio-host.sh` (checks, then restarts). Revoked by the owner (`journalctl` says revoked; the
|
|
208
|
+
deploy warns `host_revoked`): `lanes studio-host re-key --yes`, then deploy again — see
|
|
209
|
+
[Sharing](SHARING.md#after-the-owner-revokes-this-machine). See [Sharing](SHARING.md#co-located-studio-host-one-host-per-lanes-server).
|
|
182
210
|
|
|
183
211
|
## When the host is overloaded
|
|
184
212
|
|
|
@@ -402,7 +430,7 @@ The sidebar's own colours and glyphs come from one token table (`libexec/tokens.
|
|
|
402
430
|
and the `say` line names it. A plain `./install.sh` re-run without `--palette` removes the include and all palette
|
|
403
431
|
files: inherit-first is the default. NOT yet looked at on a real Ghostty (2026-10-04: no Mac or Omarchy client was
|
|
404
432
|
used; the installer was exercised against a fake `ghostty` that reports a version, fake everyday configs and a
|
|
405
|
-
model of Ghostty's load order read from its source). DEF-
|
|
433
|
+
model of Ghostty's load order read from its source). DEF-1547 stays open until that is seen: run `--palette` on a
|
|
406
434
|
Mac (Ghostty ≥ 1.1) and on Omarchy, and the window background must be `#101318` (the dark `surface` token).
|
|
407
435
|
- OSC 11 on a real Ghostty view is NOT yet verified (2026-10-04: Task 22 ran on the server with no Mac client
|
|
408
436
|
attached, and no live session was touched). To check: with the Mac in light mode, `lanes attach mac` from the Mac,
|
|
@@ -436,17 +464,17 @@ only normalizes HOW a terminal reaches its lane:
|
|
|
436
464
|
are additionally projected into the file Cursor reads (`"cursor"` in `workspaces/*.json`).
|
|
437
465
|
- `lanes cursor-sync <ws> --canonicalize` (dry run; add `--write` to apply) replaces every tmux lane's recorded
|
|
438
466
|
create recipe that cannot create the lane — none, attach-only, the retired `~/.config/coder-launch` path (the three
|
|
439
|
-
classes `lanes doctor` warns about, DEF-
|
|
467
|
+
classes `lanes doctor` warns about, DEF-1541), or a script missing in the container — with the
|
|
440
468
|
canonical `tmux new -ADs … -e LANE_ID=… -e LANE_WS=… bash -l` recipe (old value kept as `createPrevious`).
|
|
441
469
|
`--identity` adds the same `-e` flags to a working plain `tmux -L S new -ADs N [-c D] CMD` recipe, keeping CMD's
|
|
442
470
|
argv; any other working recipe is left alone and its lane carries no identity. These `-e` flags are the ONLY way a
|
|
443
|
-
tmux lane gets `LANE_ID`/the trace: `lane open` deliberately exports none (DEF-
|
|
471
|
+
tmux lane gets `LANE_ID`/the trace: `lane open` deliberately exports none (DEF-1559 — the first client on a shared
|
|
444
472
|
board socket would otherwise hand its identity to every later lane there). Both change the file only: a RUNNING
|
|
445
473
|
lane keeps its environment and gains the identity when it is next created. Before enabling it on a live board,
|
|
446
|
-
read DEF-
|
|
474
|
+
read DEF-1560: a `lanes/<ws>/<id>` trace makes the whole lane — main session, subagents, fan-out — one proxy
|
|
447
475
|
fairness bucket. Applying `--canonicalize --write` or `--identity --write` to a live workspace is an operator gate
|
|
448
476
|
(plan "Operator gates"): it rewrites operator-declared recipes in the repo's `.vscode/terminals.json` (old value kept
|
|
449
|
-
as `createPrevious`, file backup under `cursor-sync-backups/<ts>/` as above) and produces the first live `lanes/<ws>/<id>` trace — decide DEF-
|
|
477
|
+
as `createPrevious`, file backup under `cursor-sync-backups/<ts>/` as above) and produces the first live `lanes/<ws>/<id>` trace — decide DEF-1560 lane-level
|
|
450
478
|
vs per-session fairness first.
|
|
451
479
|
- Size policy "the lanes view wins": lanes-view tmux clients run with `TERM=xterm-lanesview` (a user-level terminfo
|
|
452
480
|
alias of xterm-256color). While such a client shows a lane, every other client of that lane (Cursor terminals,
|
|
@@ -462,7 +490,7 @@ event-driven: Claude Code or Codex runs `~/.lz/bin/lane-signal <status>` on a tu
|
|
|
462
490
|
`~/.lz/state/hooks/<lane id>.json` in the container, and `lanes-statusd` prefers that over the other signals while
|
|
463
491
|
its agent runs and its last event is under an hour old. Nothing installs a hook for you. (`~/.lz/state/<name>.json`
|
|
464
492
|
itself is `lane-run`'s phase record of a zmx lane; the hook never writes there. A hooked agent in a zmx lane writes
|
|
465
|
-
its sidecar under the zmx session name, which the sidebar does not show yet — DEF-
|
|
493
|
+
its sidecar under the zmx session name, which the sidebar does not show yet — DEF-1573.)
|
|
466
494
|
|
|
467
495
|
1. `lanes install-container <name>` — puts `lane-signal` in `~/.lz/bin/` and the definitions in `~/.lz/hooks/`.
|
|
468
496
|
2. `lanes hooks install --agent claude --container <name>` — prints the unified diff it would make to that
|
|
@@ -504,7 +532,7 @@ Codex 0.160 has no `codex --help hooks`; the names come from the binary. Not yet
|
|
|
504
532
|
loads a USER-level `~/.codex/hooks.json` the way it loads a project's (the only hooks.json files measured here are
|
|
505
533
|
project files), and what a hook's parent process is under each harness (lane-signal skips one shell either way).
|
|
506
534
|
|
|
507
|
-
Known limit (DEF-
|
|
535
|
+
Known limit (DEF-1515): after you approve a permission prompt, the lane shows `needs_approval` until the approved
|
|
508
536
|
tool call returns (`PostToolUse`), because neither harness signals "approved". A long approved command therefore
|
|
509
537
|
reads as waiting for you until it finishes. A lane whose agent crashed loses its sidecar at once (dead pid); one
|
|
510
538
|
whose agent is alive but silent keeps the last word for up to an hour.
|
|
@@ -535,7 +563,7 @@ in priority order and whole parts dropped from the end to fit 34 cells:
|
|
|
535
563
|
|
|
536
564
|
A leading `?` and an all-dim line mean the figures are frozen: the poller kept its last fleet strip because `/status`
|
|
537
565
|
failed (`"stale": true`), or the poller stopped — the poller rewrites `economy.json` at least every 60 s, and a sidebar
|
|
538
|
-
treats a document more than 180 s old by its own clock as frozen (DEF-
|
|
566
|
+
treats a document more than 180 s old by its own clock as frozen (DEF-1528). A lane's tail cell follows the same rule:
|
|
539
567
|
a `⚠` or `↓` drawn dim (not amber) is the last known figure, not a live alarm — the poller kept that lane's record
|
|
540
568
|
because `/sessions` failed (`"stale": true` on the lane), or the whole document is frozen. With
|
|
541
569
|
`LANES_THROTTLE_CONTROL=none` every fleet figure is `null`: the strip row then shows fleet attention exactly as without
|
|
@@ -553,7 +581,7 @@ default reaches them from the host:
|
|
|
553
581
|
(`[server] host = "127.0.0.1"`, measured 2026-10-04 in the `work` workspace container) is not reachable from the host at all, and
|
|
554
582
|
`http://127.0.0.1:19181` on the host is a different loopback. Until the control port is reachable from the host —
|
|
555
583
|
an operator change to the daemon's bind or forwarding, outside lanes — set `LANES_THROTTLE_CONTROL=none`: the
|
|
556
|
-
fleet strip and live per-lane figures stay `null`, the log fold still works (DEF-
|
|
584
|
+
fleet strip and live per-lane figures stay `null`, the log fold still works (DEF-1525, open).
|
|
557
585
|
- The log lives in the container's home. Set `LANES_THROTTLE_CONTAINER=<container name>`: the poller reads
|
|
558
586
|
`/home/coder/.local/share/throttle` through that container's home volume as `lanes sync` recorded it in
|
|
559
587
|
`~/.local/state/lanes/containers.json` (the container must be on a board). The host user's own
|
|
@@ -565,7 +593,7 @@ Check what the poller can see before you enable it — `--once` names every sour
|
|
|
565
593
|
volume sits under the docker data root (`docker info -f '{{.DockerRootDir}}'`: `/var/lib/docker` by default,
|
|
566
594
|
`/data/docker` on ceobox, where the workspace homes are named volumes at `<root>/volumes/coder-<id>-home/_data`),
|
|
567
595
|
which only root normally traverses. The log half then needs the same operator decision as the control half
|
|
568
|
-
(DEF-
|
|
596
|
+
(DEF-1525): point `LANES_THROTTLE_LOG_DIR` at a directory this user can traverse, or run the poller where the daemon
|
|
569
597
|
runs:
|
|
570
598
|
|
|
571
599
|
```sh
|
|
@@ -575,10 +603,11 @@ systemctl --user enable --now lanes-economy.service
|
|
|
575
603
|
journalctl --user -u lanes-economy.service # one line each time a source goes down or comes back
|
|
576
604
|
```
|
|
577
605
|
|
|
578
|
-
Before you enable it, settle DEF-
|
|
606
|
+
Before you enable it, settle DEF-1560 (the lane-stable trace's fairness consequence) if any lane was created with
|
|
579
607
|
the identity recipe; lanes without it are joined to their requests by `agent.session_id` alone.
|
|
580
608
|
|
|
581
|
-
Settings (environment of the installer — `install/services.py` renders them into this unit only
|
|
609
|
+
Settings (environment of the installer — `install/services.py` renders them into this unit only, from
|
|
610
|
+
`./install.sh --server` and from `install/enable-studio-host.sh` alike, which re-renders every lanes unit — or a
|
|
582
611
|
`systemctl --user edit lanes-economy.service` drop-in):
|
|
583
612
|
|
|
584
613
|
| Variable | Default | Meaning |
|
|
@@ -620,17 +649,18 @@ When nothing is sent:
|
|
|
620
649
|
(`clients/<device>.<client>.pid`), or a lane opened from the sidebar of a desk device
|
|
621
650
|
(`clients/<device>.seen.json`, counted only for a device that has a `.pid` — one `lanes attach` has run for —
|
|
622
651
|
and whose `devices/<device>.json` is not `mobile: true`) that recently. Typing in a lane refreshes neither, so after two minutes at the keyboard without attaching or
|
|
623
|
-
opening a lane the pushes resume (DEF-
|
|
652
|
+
opening a lane the pushes resume (DEF-1532). A suppressed transition is dropped, not sent later. Phone presence
|
|
624
653
|
is decided by the device's mobile layout, not by how it attached: a Safari phone writes no `.pid`, and an
|
|
625
654
|
SSH-only phone that ran `lanes attach phone --mobile` (below) is still a mobile device, so opening a lane from
|
|
626
|
-
the phone's picker never mutes pushes — you keep getting them while you are away from every desk (DEF-
|
|
655
|
+
the phone's picker never mutes pushes — you keep getting them while you are away from every desk (DEF-1533).
|
|
627
656
|
Only a phone's own `lanes attach` in the last two minutes counts, like any attach.
|
|
628
657
|
|
|
629
658
|
A failed send (server down, `HTTP 429`, no network) is one journal line naming the lane and the failure class — the
|
|
630
659
|
topic and the token are never logged — and is not retried.
|
|
631
660
|
|
|
632
|
-
Config `~/.config/lanes/notify.conf` (`LANES_NOTIFY_CONF` in the server installer's environment
|
|
633
|
-
|
|
661
|
+
Config `~/.config/lanes/notify.conf` (`LANES_NOTIFY_CONF` in the server installer's environment — or
|
|
662
|
+
`install/enable-studio-host.sh`'s, which re-renders every lanes unit — renders another path into this unit only),
|
|
663
|
+
`key=value` lines, `#` comments, re-read on change:
|
|
634
664
|
|
|
635
665
|
| Key | Default | Meaning |
|
|
636
666
|
|---|---|---|
|
|
@@ -763,12 +793,12 @@ what the agent needs and since when with its harness (`Needs you · 25m · claud
|
|
|
763
793
|
agent's task. Its first line counts the lanes that need you (`◆1 need approval · ◇2 need you · ▸6 working`). The same
|
|
764
794
|
counts come from the picker pane's title (see "Title and notifications" above): the header subtitle on the picker,
|
|
765
795
|
the page title, a number on the **Lanes** button for lanes needing approval or input, and `◆ 1 · ◇ 2` on each
|
|
766
|
-
workspace card. Nothing is fetched or polled for them. Not yet confirmed (DEF-
|
|
796
|
+
workspace card. Nothing is fetched or polled for them. Not yet confirmed (DEF-1531): until plan Task 26 (2026-10-04)
|
|
767
797
|
the phone's picker pane had the fixed layout name `Lanes`, which Zellij reports instead of the sidebar's title, so the
|
|
768
798
|
phone showed no counts. Task 26 removed that name from the generated layout, which reaches a phone only when its
|
|
769
799
|
session is laid out again (`lanes rebuild <device>`; see the device check below); whether the title
|
|
770
800
|
now reaches the phone is still unchecked in a browser — until then only the picker's own first line is known to
|
|
771
|
-
show them (DEF-
|
|
801
|
+
show them (DEF-1531).
|
|
772
802
|
|
|
773
803
|
The phone's light/dark toggle repaints the whole terminal, not only the shell: the page applies its own sixteen-colour
|
|
774
804
|
palette on attach, on every toggle and after every Zellij config reload (DEF-1413; not yet looked at on a device).
|
|
@@ -780,9 +810,9 @@ sends its text as UTF-8, so a filter like `ø` matches.
|
|
|
780
810
|
|
|
781
811
|
Device check (plan Task 13 step 3, not yet run): on a real iPhone in Safari and from the Home Screen, tap each quick
|
|
782
812
|
key on a lane tab and confirm that the keyboard does NOT open, that `y`/`⏎`/arrows arrive in the lane, that a short tap
|
|
783
|
-
on `n`/`^C` sends nothing and a hold sends it, and (plan Task 26 has landed; DEF-
|
|
813
|
+
on `n`/`^C` sends nothing and a hold sends it, and (plan Task 26 has landed; DEF-1531 waits on this) that the Workspaces
|
|
784
814
|
sheet shows each picker's counts. A phone device created before Task 26 keeps the old picker pane (`upgrade-session`
|
|
785
|
-
does not re-lay-out a tab): run `lanes rebuild <device>` first (`lanes doctor` must not list it under DEF-
|
|
815
|
+
does not re-lay-out a tab): run `lanes rebuild <device>` first (`lanes doctor` must not list it under DEF-1531), or no
|
|
786
816
|
counts appear and the check says nothing about the title path. Record the
|
|
787
817
|
device, iOS version and result here. The unit suite covers only the picker's text, not the browser.
|
|
788
818
|
|
|
@@ -30,9 +30,10 @@ An existing in-container Studio Host reads `~/.lz/registry.json` by default.
|
|
|
30
30
|
That is the registered-lane projection written by `lanes sync`. No additional
|
|
31
31
|
configuration is needed for this source.
|
|
32
32
|
|
|
33
|
-
For a Lanes server hosting several workspace containers,
|
|
34
|
-
|
|
35
|
-
`~/.config/throttle-rc/studio-host.toml
|
|
33
|
+
For a Lanes server hosting several workspace containers, prefer the installer-owned
|
|
34
|
+
[co-located Studio Host](#co-located-studio-host-one-host-per-lanes-server) below. Hand-run, the server's
|
|
35
|
+
`~/.config/throttle-rc/studio-host.toml` reads (add `mode = "lanes-server"` under `[inventory]` for the
|
|
36
|
+
interactive adapter; without it the Host serves the read-only sharing capture only):
|
|
36
37
|
|
|
37
38
|
```toml
|
|
38
39
|
server = "https://throttle.example.com"
|
|
@@ -72,6 +73,236 @@ eight-hour Host session. If you deployed that candidate, use the
|
|
|
72
73
|
before deploying current builds. The normal migrator correctly refuses that
|
|
73
74
|
journal; do not edit its digest or reset the database.
|
|
74
75
|
|
|
76
|
+
## Co-located Studio Host (one Host per lanes server)
|
|
77
|
+
|
|
78
|
+
On a lanes server — the account that runs `lanes-statusd` and reaches every workspace container's tmux sockets
|
|
79
|
+
through `/proc/<container-pid>/root/tmp/tmux-<uid>/` — ONE Studio Host serves every board, installed and supervised
|
|
80
|
+
by the lanes installer as `lanes-studio-host.service` (design: `docs/design/throttle-studio.md` §8.10). It replaces
|
|
81
|
+
one Host per workspace container and the hand-built `lanes-sharing-host.service`. Laptops keep the in-container Host
|
|
82
|
+
and `throttle-rc studio service`; never run both on one machine (the enable step refuses).
|
|
83
|
+
|
|
84
|
+
**Status of the pieces.** Assembled on the integrated tree (2026-10-04): the installer below
|
|
85
|
+
(`tests/unit/test_studio_host.py`), the Host's interactive `lanes-server` adapter, `studio publish-key` /
|
|
86
|
+
`adopt-receipt` and the platform's `server studio-adopt-host`. Only a `throttle-rc` built before 2026-10-04 refuses
|
|
87
|
+
those verbs `not_assembled`, and the enable step then stops with exit 1 before enabling anything. A live lanes
|
|
88
|
+
server and a live deploy are operator-gated; see "Deploy-provisioned enrolment" for the handoff's precondition.
|
|
89
|
+
|
|
90
|
+
### 1. Install the binary
|
|
91
|
+
|
|
92
|
+
No `node/<sha12>` release that carries the co-location CLI is pinned yet, so install a locally built binary. It is
|
|
93
|
+
hashed, copied to `~/.local/share/lanes/bin/throttle-rc-local-<sha12>`, re-hashed, and selected through the link
|
|
94
|
+
`~/.local/share/lanes/bin/throttle-rc` that the unit runs; `throttle-rc.pin.json` beside it records the digest:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
cargo build --release --locked -p throttle-rc # on a build machine; copy the file to the server
|
|
98
|
+
lanes studio-host install --from ./throttle-rc --sha256 <the sha256 you computed where you built it>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Once a release is tagged, its line goes into `holders.lock` as an `opt-in` pin (the file's comment has the
|
|
102
|
+
template) and `lanes studio-host install` with no `--from` downloads it — `gh release download` for the private
|
|
103
|
+
repository, else `curl` — and refuses unless BOTH the tarball and the binary inside it match their digests.
|
|
104
|
+
`./install.sh --server` never downloads an opt-in pin. Nothing is restarted by `install`.
|
|
105
|
+
|
|
106
|
+
### 2. Enable
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
sh install/enable-studio-host.sh --server https://throttle.example.com --dry-run # the plan; changes nothing
|
|
110
|
+
sh install/enable-studio-host.sh --server https://throttle.example.com
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
In order, and refusing (exit 3) before anything changes when another Host unit is enabled or running
|
|
114
|
+
(`another_host_unit`), the hand-built unit exists (`hand_built_unit_present`, below), no binary is installed
|
|
115
|
+
(`no_binary`) or an existing config is not in lanes-server mode (`config_mode`, fixed by `--set-mode`):
|
|
116
|
+
|
|
117
|
+
1. writes `~/.config/throttle-rc/studio-host.toml` (0600) in lanes-server mode if there is none —
|
|
118
|
+
`[inventory] mode = "lanes-server"`, `interactive = true`, `sharing_state_dir = $LANES_STATE`,
|
|
119
|
+
`sharing_container_uid = 1000` (`--container-uid`), `machine_id` from `--machine-id` or `<hostname>-lanes`.
|
|
120
|
+
An existing file is never rewritten except, with `--set-mode`, its one `mode` key (a `.bak-<time>` is kept);
|
|
121
|
+
2. runs `throttle-rc studio publish-key --out ~/.local/state/lanes/studio-host/public.jwk` — the machine key is
|
|
122
|
+
created HERE if absent and only its PUBLIC half (0644) is written for the deploy;
|
|
123
|
+
3. enrols, if it can: a deploy receipt at `~/.local/state/lanes/studio-host/receipt.json` (`--receipt PATH`, which is
|
|
124
|
+
remembered in the enrolment record so the enrol timer and `status` watch that path too) is installed
|
|
125
|
+
with `throttle-rc studio adopt-receipt --config <the config>`, which refuses a receipt issued for another key
|
|
126
|
+
(`receipt_key_mismatch`) or by another server URL (`receipt_server_mismatch`); else a registration key
|
|
127
|
+
file at `~/.config/throttle-rc/registration.key` (`--registration-key-file`, 0600, remembered like `--receipt`) is
|
|
128
|
+
handed to `throttle-rc studio enroll <server> --registration-key-file` — the script names the file and never opens
|
|
129
|
+
it (its readability is access(2) and the timer's memory is the file's stat identity, never a digest of the secret;
|
|
130
|
+
review round 5). A refused or failed receipt/key does not stop the next step: the units are still rendered and enabled, and the
|
|
131
|
+
run exits with the refusal (3, or 1 for an operational failure) saying the unit is waiting;
|
|
132
|
+
4. renders the lanes units (`install/services.py`), enables `lanes-studio-host.service` and
|
|
133
|
+
`lanes-studio-host-enrol.timer`;
|
|
134
|
+
5. enrolled: runs `throttle-rc studio host --check`, then starts the Host (restarts it when it was running, so a new
|
|
135
|
+
config or binary is in force) and exits 0 only when it is still running `--settle` seconds (default 5) later.
|
|
136
|
+
Not enrolled (and nothing refused): exits 0 **waiting for enrolment** — the unit is enabled and exits 3 (a clean
|
|
137
|
+
stop, never a restart loop) until a receipt or key arrives.
|
|
138
|
+
|
|
139
|
+
`lanes-studio-host-enrol.timer` runs `enable-studio-host.sh --retry` every 5 minutes. It acts only on something
|
|
140
|
+
new — a receipt it has not adopted and has not refused (typed, exit 3) within the last hour under the same config, a
|
|
141
|
+
key file not tried in the last hour (a chmod/chown of it counts as new), a machine key that is gone (it publishes a
|
|
142
|
+
new one — over an existing `credential.json` only as described below), an enrolment the Host has not been started with — so a Host that stopped on a revoked enrolment is not restarted into the same refusal. The
|
|
143
|
+
hour applies to the timer only: a manual run of the script always tries the registration key and always tries a
|
|
144
|
+
refused receipt — enrolled or not (review round 4: an enrolled machine used to leave a refused NEW receipt alone for
|
|
145
|
+
ever). A receipt refused with `receipt_server_mismatch` is tried again by the timer as soon as `studio-host.toml`
|
|
146
|
+
changes (the deploy re-emits the SAME bytes, so fixing the Host's `server` is enough); an operational failure of a
|
|
147
|
+
RECEIPT (exit 1/2: a read-only disk, an unloadable config, a timeout) is never remembered. A registration key is
|
|
148
|
+
remembered for the hour after ANY attempt (each try may spend one of its uses), with what `studio enroll` answered:
|
|
149
|
+
`status` says `key-refused` (code, exit, time, next retry) instead of `waiting-for-enrolment`, and a refused key
|
|
150
|
+
SOURCE (`registration_key_source_refused`, the binary's exit 2) is the installer's typed exit 3. A new receipt for
|
|
151
|
+
THIS machine's key is adopted over an existing `credential.json` for the same key (a relabel, a new host row after a
|
|
152
|
+
platform reset); `adopt-receipt` refuses one that would replace a receipt issued for ANOTHER key
|
|
153
|
+
(`receipt_replaces_enrolment`) unless `--new-key` says so, and refuses a receipt for an enrolment this machine
|
|
154
|
+
MEASURED revoked (`receipt_for_revoked_key`). `status` says `receipt-refused` for a refused receipt on disk whether
|
|
155
|
+
or not an older credential exists. A `credential.json` whose machine key is gone, or is not the key this machine
|
|
156
|
+
published, is an enrolment only when it binds the published key (`status --json` `key_binding`). The installer
|
|
157
|
+
re-keys over it — publishes a new key, then `adopt-receipt --new-key` / `studio enroll --new-key` — ONLY when this
|
|
158
|
+
machine MEASURED that host row revoked (the Host's `state.json`); otherwise it refuses `credential_for_gone_key`,
|
|
159
|
+
publishes nothing, and `status` says `credential-for-gone-key`: revoke the row (console, or `throttle-rc studio revoke
|
|
160
|
+
--id <host>`), then `lanes studio-host re-key --yes` — the explicit decision to enrol this machine under a new key
|
|
161
|
+
(review round 6: the installer used to decide that itself). A receipt, key or config this account cannot read is a typed refusal
|
|
162
|
+
(`receipt_unreadable`/`key_unreadable`/`config_unreadable`, exit 3) naming the file's owner and mode — never a
|
|
163
|
+
traceback; `status` says `receipt-unreadable`/`key-unreadable`/`config-unreadable`. Both studio units ALWAYS pin `XDG_CONFIG_HOME`/`XDG_DATA_HOME` — the roots this enable step ran with
|
|
164
|
+
(recorded in `~/.local/state/lanes/studio-host/xdg-roots.json`; a later `./install.sh --server` renders the studio
|
|
165
|
+
units from that record, never from its own shell, and warns when they differ), else `%h/.config` and
|
|
166
|
+
`%h/.local/share` — and every `throttle-rc` child the installer runs gets the same two, so neither the user manager's
|
|
167
|
+
environment nor a reinstall from another shell points the supervised Host at another `credential.json`. A manual run
|
|
168
|
+
on an enrolled machine whose `studio host --check` is red ONLY on the refusal the Host's own last run recorded (the
|
|
169
|
+
owner's tunnel cap, a build mismatch fixed by `lanes studio-host install`, three displacements) starts the Host
|
|
170
|
+
anyway — its first phase replaces that record, and the settle window reports a refusal that still holds. The unit files are
|
|
171
|
+
written where the user MANAGER reads them: `$XDG_CONFIG_HOME/systemd/user` when `systemctl --user show-environment`
|
|
172
|
+
carries it, else `~/.config/systemd/user`. The adopt refusal's code is read from `throttle-rc studio adopt-receipt`'s
|
|
173
|
+
own line (`throttle-rc studio adopt-receipt: refused <code>: …`); a line with no code is remembered as `unknown`.
|
|
174
|
+
|
|
175
|
+
### Deploy-provisioned enrolment (no human step)
|
|
176
|
+
|
|
177
|
+
The platform deploy binds the published PUBLIC key to its owner (`throttle-rc server studio-adopt-host`, design
|
|
178
|
+
§8.10 C4) and writes the receipt where this server finds it:
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
THROTTLE_RC_STUDIO_ADOPT_JWK=/home/dev/.local/state/lanes/studio-host/public.jwk
|
|
182
|
+
THROTTLE_RC_STUDIO_ADOPT_OWNER=owner@example.com
|
|
183
|
+
THROTTLE_RC_STUDIO_ADOPT_RECEIPT=/home/dev/.local/state/lanes/studio-host/receipt.json
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The step is `deploy/studio-adopt-host.sh`, sourced by `deploy/deploy-ceobox-rc.sh`; a receipt path whose directory is
|
|
187
|
+
absent or not writable is refused `receipt_dir_unusable` (exit 2) before the platform is asked.
|
|
188
|
+
|
|
189
|
+
Run the deploy **as the lanes account**: `~/.local/state/lanes/studio-host/` is kept at 0700 and the deploy writes
|
|
190
|
+
the receipt 0600 as whoever ran it, so a deploy run by another account cannot read `public.jwk` and leaves a receipt
|
|
191
|
+
the Host cannot read (DEF-1429 in `docs/defect-ledger.md`; a group-readable handoff directory is the unbuilt
|
|
192
|
+
alternative). `--receipt PATH` points the installer at another path the lanes account can read; it is remembered,
|
|
193
|
+
so the enrol timer adopts a receipt that appears there later and `status` reports it (naming the default path again
|
|
194
|
+
forgets it). The receipt carries no secret.
|
|
195
|
+
|
|
196
|
+
**Precondition — one filesystem (DEF-1441).** The handoff is a file exchange, so the deploy must run where both
|
|
197
|
+
paths above exist: on the docker host, as the lanes account, from a checkout of this repository (the compose build
|
|
198
|
+
context), with an env file that account can read named by `THROTTLE_RC_ENV_FILE` — the script's default is the
|
|
199
|
+
literal `/home/coder/.config/throttle-rc/ceobox.env`, which a lanes account named anything else does not have:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
THROTTLE_RC_ENV_FILE=~/.config/throttle-rc/ceobox.env bash deploy/deploy-ceobox-rc.sh # as the lanes account
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Copying that env file (it holds the database password) to the lanes account is a secret-custody step the operator
|
|
206
|
+
owns. On ceobox the deploy is normally run from the Coder workspace container, which has neither `/home/dev` nor the
|
|
207
|
+
lanes account, so the zero-step path above does NOT execute there as-is (`[INFERRED-NOT-EXECUTABLE]`, design §8.10
|
|
208
|
+
C4 "Topology precondition"); until one of those two is arranged, use the registration-key path. An owner who has not signed in yet makes the adopt exit 3 and the deploy
|
|
209
|
+
retries next time; the timer picks the receipt up within 5 minutes of it appearing. `lanes studio-host status` then
|
|
210
|
+
says `enrolment: via deploy`.
|
|
211
|
+
|
|
212
|
+
### After the owner revokes this machine
|
|
213
|
+
|
|
214
|
+
A revoked key is never adopted again: the Host stops (exit 3) and later deploys WARN `host_revoked` and finish. Give
|
|
215
|
+
the machine a new key:
|
|
216
|
+
|
|
217
|
+
```sh
|
|
218
|
+
lanes studio-host re-key # the plan; changes nothing (exit 2)
|
|
219
|
+
lanes studio-host re-key --yes # stop the Host; remove the machine key, credential.json, public.jwk, receipt.json
|
|
220
|
+
# and the Host's refused state; publish a NEW public key
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Then deploy again: the deploy adopts the new key, and the timer installs the receipt and starts the Host. A new key
|
|
224
|
+
is a new machine in the console (design §8.7 "What a new key costs"); any `throttle-rc` login session on this
|
|
225
|
+
account was bound to the old key and must log in again.
|
|
226
|
+
|
|
227
|
+
### Check it
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
lanes studio-host status # state: running | running-unenrolled | waiting-for-enrolment | receipt-refused | key-refused | credential-for-gone-key | receipt-unreadable | key-unreadable | config-unreadable | not-enabled | stopped | failed | conflict | not-installed | unknown
|
|
231
|
+
lanes studio-host status --check # also runs `throttle-rc studio host --check`; its exit binds (below)
|
|
232
|
+
journalctl --user -u lanes-studio-host.service -n 30
|
|
233
|
+
journalctl --user -u lanes-studio-host-enrol.service -n 20 # the enrol timer's one line per tick
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`waiting-for-enrolment` is said only while the Host unit is enabled AND the enrol timer is active; otherwise the
|
|
237
|
+
machine is `not-enabled` (nothing retries: re-run the enable script). `receipt-refused` names a receipt on disk the
|
|
238
|
+
installer recorded as refused (its code, when, and when the timer tries it again — an hour, or at once after
|
|
239
|
+
`studio-host.toml` changes); its remedy is the code's (`receipt_server_mismatch`: the config's `server`;
|
|
240
|
+
`receipt_key_mismatch`: redeploy with this machine's public key), then `sh install/enable-studio-host.sh` to try now.
|
|
241
|
+
`key-refused` is the registration key's twin (`studio enroll`'s code and exit; fix the file, its mode or the key, then
|
|
242
|
+
re-run the script — the timer also retries a changed file on its next tick). `credential-for-gone-key` is a receipt
|
|
243
|
+
on disk for a key this machine no longer holds, for a row it has not measured revoked: revoke it, then
|
|
244
|
+
`lanes studio-host re-key --yes`.
|
|
245
|
+
|
|
246
|
+
`status` re-hashes the selected binary against its record, names the config's mode, the enrolment's `via`, both
|
|
247
|
+
units and any other Host unit, and says `unknown` — never a guess — when the user manager does not answer. It exits
|
|
248
|
+
0 only when the Host runs enrolled (an active unit with no enrolment is `running-unenrolled`, exit 1; with
|
|
249
|
+
`--check`, an active unit whose `studio host --check` is non-zero is `running-check-failed`, exit 1). A caller without
|
|
250
|
+
`XDG_RUNTIME_DIR` (a `sudo -u` or cron shell) reaches the manager at `/run/user/<uid>` when that exists, and
|
|
251
|
+
`re-key` refuses `manager_unreachable` rather than remove the key under a Host whose state the manager did not
|
|
252
|
+
answer, and `another_host_running` (exit 3, nothing changed) while `lanes-sharing-host.service` or
|
|
253
|
+
`throttle-rc-studio-host.service` is active: every Host unit of this account signs with the same machine key, so
|
|
254
|
+
re-keying under the hand-built Host would end its sessions. Migrate (or stop) that unit first. It refuses
|
|
255
|
+
`rekey_path_unremovable` (exit 3, nothing changed) when a file it must remove — e.g. a remembered `--receipt` in the
|
|
256
|
+
deploying account's directory — sits in a directory this account cannot write, and it removes the machine key LAST.
|
|
257
|
+
`status` also reports an installed unit that has lost a load-bearing line
|
|
258
|
+
(`SuccessExitStatus=3`, `KillSignal=SIGINT`, `KillMode=process`, the slice, the statusd ordering).
|
|
259
|
+
|
|
260
|
+
### Migrating the hand-built `lanes-sharing-host.service`
|
|
261
|
+
|
|
262
|
+
ceobox's first Host was a hand-written unit running `~/.local/bin/throttle-rc-lanes studio host` with a sharing-only
|
|
263
|
+
config (no `mode`, so in-container). That Host predates enrolment receipts: it authenticated with a stored human
|
|
264
|
+
login SESSION beside its machine key, and holds NO `credential.json` (docs/handoffs/2026-09-22-lanes-sharing.md).
|
|
265
|
+
The lanes unit never uses a session, so such a machine is enrolled as part of the migration. Read the unit before
|
|
266
|
+
replacing it:
|
|
267
|
+
|
|
268
|
+
```sh
|
|
269
|
+
systemctl --user cat lanes-sharing-host.service
|
|
270
|
+
lanes studio-host install --from ./throttle-rc --sha256 <digest>
|
|
271
|
+
sh install/enable-studio-host.sh --migrate-hand-built --dry-run
|
|
272
|
+
sh install/enable-studio-host.sh --migrate-hand-built
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`--migrate-hand-built` refuses a unit that does not run `studio host`, or that names a `--config` other than
|
|
276
|
+
`~/.config/throttle-rc/studio-host.toml` (`%h`, `${HOME}`, `$HOME` and `~` are resolved to the account's home before
|
|
277
|
+
the comparison). Otherwise it sets `mode = "lanes-server"` in the existing config (keeping a `.bak-<time>`), runs the
|
|
278
|
+
replacement's own `throttle-rc studio host --check` on that config (an enrolled machine), and only when it passes
|
|
279
|
+
saves the unit's text to `~/.local/state/lanes/studio-host/migrated/`, runs
|
|
280
|
+
`systemctl --user disable --now lanes-sharing-host.service`, and then starts the new unit. A refusing check
|
|
281
|
+
(`host_check_failed`; e.g. a version-1 credential whose key binding is `unchecked`) leaves the hand-built unit
|
|
282
|
+
running and enabled, restores the config from the `.bak`, and names both — as does ANY failure before the
|
|
283
|
+
hand-built unit is stopped (a failed `publish-key`, an unreadable receipt or key, a failed `disable --now`; review
|
|
284
|
+
round 5: its older binary refuses the `mode` key at its next start). A RUNNING hand-built unit on a machine with no
|
|
285
|
+
`credential.json` (ceobox's shape) is enrolled first, and the **published public key is the first artifact**:
|
|
286
|
+
|
|
287
|
+
1. `sh install/enable-studio-host.sh --migrate-hand-built` with nothing to enrol from installs the binary, publishes
|
|
288
|
+
this machine's PUBLIC key to `~/.local/state/lanes/studio-host/public.jwk` (the existing machine key is loaded,
|
|
289
|
+
never rewritten; the hand-built Host, its unit and its config are untouched) and refuses
|
|
290
|
+
`hand_built_host_unenrolled` (exit 3), naming the `THROTTLE_RC_STUDIO_ADOPT_*` values for the deploy;
|
|
291
|
+
2. the deploy adopts that key and writes the receipt to the receipt path (see the runbook's one-filesystem
|
|
292
|
+
precondition; a deploy that runs before step 1 defers — "not published yet" — and finishes) — or put the owner's
|
|
293
|
+
registration key in `~/.config/throttle-rc/registration.key` (0600) instead;
|
|
294
|
+
3. re-run `sh install/enable-studio-host.sh --migrate-hand-built`: it installs the receipt (or enrols) FIRST, with the
|
|
295
|
+
same machine key, while the hand-built Host still runs, and only then checks, stops and replaces it (a refused
|
|
296
|
+
enrolment leaves it running with its config restored). The machine key (`~/.local/share/throttle-rc/machine/key.json`)
|
|
297
|
+
is never read or moved, and an existing enrolment receipt is kept: the new Host serves as the same machine (the same
|
|
298
|
+
key, so the same thumbprint and machine id once enrolled). The old unit file and the old binary are left for
|
|
299
|
+
you to delete. A later run treats the disabled, stopped unit as migrated while the enrolment record under
|
|
300
|
+
`~/.local/state/lanes/studio-host/` survives; if that record is lost, the enrol timer still never refuses on the
|
|
301
|
+
stopped, disabled unit, and a manual run asks for `--migrate-hand-built` again (idempotent: it saves the text again).
|
|
302
|
+
|
|
303
|
+
**Rollback:** `systemctl --user disable --now lanes-studio-host.service lanes-studio-host-enrol.timer`, restore the
|
|
304
|
+
config's `.bak-<time>`, then `systemctl --user enable --now lanes-sharing-host.service`.
|
|
305
|
+
|
|
75
306
|
## Enable publishers, then expose workspaces
|
|
76
307
|
|
|
77
308
|
1. Sign in to Throttle and open **Shared lanes → Manage sharing**.
|
|
@@ -112,7 +343,11 @@ interactive `/phone` endpoint.
|
|
|
112
343
|
|
|
113
344
|
Focused regression coverage lives in the platform's `pg_http_studio` sharing
|
|
114
345
|
tests, `pg_studio_schema`, the Host's `studio_host_sharing` real-tmux test, and
|
|
115
|
-
`views/shared-lanes/view.test.mjs`. The
|
|
346
|
+
`views/shared-lanes/view.test.mjs`. The co-located Host's installer pieces (the
|
|
347
|
+
rendered unit against `tests/unit/golden/lanes-studio-host.service`, the opt-in
|
|
348
|
+
pin, `lanes studio-host`, `install/enable-studio-host.sh` and the hand-built
|
|
349
|
+
migration) are `tests/unit/test_studio_host.py`, run with the other lanes unit
|
|
350
|
+
tests: `python3 -m unittest discover -s lanes/tests/unit`. The Node tests run without an npm install:
|
|
116
351
|
|
|
117
352
|
```sh
|
|
118
353
|
node --test crates/throttle-platform/src/http/console/views/shared-lanes/view.test.mjs
|