@vibelord/lanes 1.2.0 → 1.3.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/package.json +1 -1
- package/payload/lanes/README.md +4 -3
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +301 -45
- package/payload/lanes/container/lane +42 -1
- package/payload/lanes/container/lane-mcp +65 -6
- package/payload/lanes/container/lane_core.py +178 -0
- package/payload/lanes/docs/ARCHITECTURE.md +192 -20
- package/payload/lanes/docs/DESIGN.md +6 -3
- package/payload/lanes/docs/INSTALL.md +25 -8
- package/payload/lanes/docs/RUNBOOK.md +369 -8
- package/payload/lanes/docs/USERGUIDE.md +201 -25
- package/payload/lanes/holders.lock +5 -0
- package/payload/lanes/install/enable-docs.sh +78 -0
- package/payload/lanes/install/host-install.sh +39 -5
- package/payload/lanes/libexec/docs_store.py +995 -0
- package/payload/lanes/libexec/docsd.py +2010 -0
- package/payload/lanes/libexec/mdlite.py +575 -0
- package/payload/lanes/libexec/members.py +5 -0
- package/payload/lanes/libexec/notify.py +65 -5
- package/payload/lanes/libexec/sidebar.py +106 -13
- package/payload/lanes/libexec/statusd.py +97 -5
- package/payload/lanes/libexec/tokens.py +31 -9
- package/payload/lanes/libexec/update.py +94 -22
- package/payload/lanes/libexec/uploads.py +538 -0
- package/payload/lanes/systemd/lanes-docsd.service +29 -0
- package/payload/lanes/web/Caddyfile +24 -1
- package/payload/lanes/web/README.md +86 -8
- package/payload/lanes/web/browser.js +2 -4
- package/payload/lanes/web/docs.css +52 -0
- package/payload/lanes/web/docs.js +36 -0
- package/payload/lanes/web/mobile.js +3 -2
- package/payload/lanes/web/upload.css +17 -0
- package/payload/lanes/web/upload.js +84 -0
- package/payload/manifest.json +4 -3
|
@@ -41,7 +41,7 @@ Phone / tablet / laptop ── HTTPS (Tailscale :8443) ┤
|
|
|
41
41
|
▼
|
|
42
42
|
Linux workspace host (one unprivileged account)
|
|
43
43
|
one Zellij view session per device: workspace tabs, lane sidebar, 1–3 viewports
|
|
44
|
-
lanes-statusd · registry sync · upload intake · web services
|
|
44
|
+
lanes-statusd · registry sync · upload intake · docs service · web services
|
|
45
45
|
│ /proc/<container init pid>/root
|
|
46
46
|
▼
|
|
47
47
|
Workspace containers (home /home/coder, uid 1000)
|
|
@@ -52,7 +52,7 @@ Phone / tablet / laptop ── HTTPS (Tailscale :8443) ┤
|
|
|
52
52
|
|
|
53
53
|
| Layer | Owns | Lifetime |
|
|
54
54
|
| --- | --- | --- |
|
|
55
|
-
| Host | Views, the registry, status, uploads, web services | Services restart freely; nothing here holds an agent |
|
|
55
|
+
| Host | Views, the registry, status, uploads, the docs store, web services | Services restart freely; nothing here holds an agent |
|
|
56
56
|
| Container | Each lane's tmux session and the agent in it | Survives every view, reconnect and rebuild |
|
|
57
57
|
| View | One Zellij session per device (`mac`, `phone`, `browser`, …) | Disposable: `lanes rebuild <device>` recreates it, agents untouched |
|
|
58
58
|
|
|
@@ -442,7 +442,7 @@ redeemed from a second machine yet.
|
|
|
442
442
|
|
|
443
443
|
After a server update — with `--update-only` the installer runs these steps itself once the new release is switched
|
|
444
444
|
in, in this order: `systemctl --user daemon-reload`, `lanes sync`, the statusd restart, a `try-restart` of each
|
|
445
|
-
optional service that runs (economy, notify, enrolment, `lanes-mobile`, `lanes-web` and the Studio Host — restarted,
|
|
445
|
+
optional service that runs (economy, notify, enrolment, docs, `lanes-mobile`, `lanes-web` and the Studio Host — restarted,
|
|
446
446
|
not re-checked as `enable-studio-host.sh` does; one that is off stays off), `lanes install-container` for every
|
|
447
447
|
container `lanes sync` recorded, `lanes reload-views` and `lanes ensure --if-layout-changed`. A step that fails is
|
|
448
448
|
reported and the next one still runs. `lanes sync` exiting 1 — a board with an error, such as a container that no longer exists, whose
|
|
@@ -550,7 +550,7 @@ would put the bad release back, is refused (install it by hand with `npx @vibelo
|
|
|
550
550
|
tree's `VERSION` and the commit of its npm stamp (none for a checkout), the tree (`pkg/<version>` for an npm
|
|
551
551
|
install), whether `~/.local/share/lanes/current` names that tree (`current pkg/1.2.0 ✗` when it names another: this
|
|
552
552
|
`lanes` was started from an old tree), then the tree each long-running process recorded when it last started
|
|
553
|
-
(`~/.local/state/lanes/run/<name>.tree`: statusd, enrold, notify, economy, studio_host and the newest sidebar).
|
|
553
|
+
(`~/.local/state/lanes/run/<name>.tree`: statusd, enrold, docsd, notify, economy, studio_host and the newest sidebar).
|
|
554
554
|
Each `✗` there names a process that does not run the current code: its last start ran another tree
|
|
555
555
|
(`statusd runs 1.1.0 ✗`), or ran the current tree before its code last changed (`statusd started before its tree
|
|
556
556
|
changed ✗` — a checkout updated in place keeps its path, so `lanes version` compares the record's time with the
|
|
@@ -564,7 +564,8 @@ would put the bad release back, is refused (install it by hand with `npx @vibelo
|
|
|
564
564
|
- The client installer prints `lanes client <version>` and copies `VERSION` into `~/.local/share/lanes/client/`.
|
|
565
565
|
`lanes-launch --version` prints it (`lanes-launch 1.2.0`; a checkout's client reads `lanes/VERSION`; `dev` without
|
|
566
566
|
either), and the dim line a window shows while it connects names it: `lanes 1.2.0: connecting to lanes…`.
|
|
567
|
-
- On a phone or tablet, **More** shows the server's release as its first row, **Lanes v1.2.0**, above **
|
|
567
|
+
- On a phone or tablet, **More** shows the server's release as its first row, **Lanes v1.2.0**, above **Docs** and
|
|
568
|
+
**Text size**:
|
|
568
569
|
the shell asks the enrolment service's `/enrol/site` once when it opens (the release that service runs; the server
|
|
569
570
|
installer restarts it on every install). Nothing is shown when the service does not answer.
|
|
570
571
|
- `lanes-launch` passes that version to `lanes attach` (`--client-version`). A client older than the server gets one
|
|
@@ -822,7 +823,14 @@ Top to bottom, a desktop sidebar is:
|
|
|
822
823
|
would leave the pane. Then `zmx lanes (N)` when any exist, then **Unregistered (N)**
|
|
823
824
|
with its `+ New shell` row. A list longer than the pane ends in `⋮ N more`; moving onto that row or clicking it
|
|
824
825
|
scrolls instead of opening.
|
|
825
|
-
3. **Upload strip** (panes of 14 rows or more): ` ↑ uploads · click, then drop (U)` (section 5).
|
|
826
|
+
3. **Upload strip** (panes of 14 rows or more): ` ↑ uploads · click, then drop (U)` (section 5). Once this
|
|
827
|
+
workspace's lanes have published documents (the docs cockpit) it reads ` ↑ uploads · ▤ docs 3 (U)` — the count
|
|
828
|
+
is every document of its lanes, whoever published it — and the whole strip is a link to the docs page,
|
|
829
|
+
`https://<server>:8443/lanes-docs/` (⌘-click in Ghostty and iTerm2, Ctrl-click elsewhere); a plain click still
|
|
830
|
+
arms the upload. The cursor row's name is a link to that lane's own documents
|
|
831
|
+
(`/lanes-docs/?ws=<workspace>&lane=<lane id>`) while it has any, and `d` shows the link in the footer. The links
|
|
832
|
+
need the server's Tailscale name (statusd learns it as the enrolment service does): without it the strip still
|
|
833
|
+
counts and `d` shows the path alone. What the page holds and who sees what: section 4, [Docs](#docs).
|
|
826
834
|
4. **Strip row** (panes of 18 rows or more): the fleet economy line when the opt-in poller runs (section 8), otherwise
|
|
827
835
|
the other workspaces that need you, e.g. ` ◆1 Worlds · ◇2 Code`; blank when there is nothing to say.
|
|
828
836
|
5. A rule, then the **footer**: one line, by priority.
|
|
@@ -845,7 +853,7 @@ Unregistered section, no upload strip and no `+`, and its footer has no `x close
|
|
|
845
853
|
| 35 | `×` close, on unregistered sessions only |
|
|
846
854
|
|
|
847
855
|
Marks and their ASCII twins: `▌` `|` cursor · `›` `=` viewing · `²` `2` / `³` `3` other viewport · `▲` `^` pinned · `·` `.` viewer ·
|
|
848
|
-
`…` `~` clipped · `×` `x` close · `+` `+` new / add · `⋮` `:` more rows · `↑` `^` uploads · `↓` `v` drop now ·
|
|
856
|
+
`…` `~` clipped · `×` `x` close · `+` `+` new / add · `⋮` `:` more rows · `↑` `^` uploads · `↓` `v` drop now · `▤` `#` docs ·
|
|
849
857
|
`⚠` `!` warning or starving · `↓` `v` downgraded · `$` `$` paid. The full tables, colours and mockups are in
|
|
850
858
|
[DESIGN.md](DESIGN.md).
|
|
851
859
|
|
|
@@ -863,7 +871,7 @@ draws the same footer (its picker says ` Choose a workspace · Tap Cancel`, and
|
|
|
863
871
|
idle). An empty workspace shows `no lanes declared · + New shell` on its New shell row.
|
|
864
872
|
|
|
865
873
|
**Sidebar keys** (`?` shows the short form of this table in place of the list until the next key; that key only closes
|
|
866
|
-
the sheet). Its first row says which lanes: `lanes <installed version> · view <the version this sidebar runs>`. The sheet puts companion keys on one line (`J K`, `Tab S-Tab`, `+ U`, `u z`, `p h`, `H o`, `r ?`), leaves `PageUp` / `PageDown`
|
|
874
|
+
the sheet). Its first row says which lanes: `lanes <installed version> · view <the version this sidebar runs>`. The sheet puts companion keys on one line (`J K`, `Tab S-Tab`, `+ U d`, `u z`, `p h`, `H o`, `r ?`), leaves `PageUp` / `PageDown`
|
|
867
875
|
to this table and ends with `Ctrl-Alt-l` — not a sidebar key but the chord that brings the keyboard here from a lane
|
|
868
876
|
(section [Keybindings](#keybindings)). Every key below does something in the sidebar, and no other key does
|
|
869
877
|
(`tests/unit/test_sidebar.py` `DocumentedKeys` presses each one and every other key a keyboard sends):
|
|
@@ -891,6 +899,7 @@ to this table and ends with `Ctrl-Alt-l` — not a sidebar key but the chord tha
|
|
|
891
899
|
| `H` | Hide this workspace on every device you own: `y` arms, `Enter` hides — its tab closes, its lanes keep running (the last workspace shown stays) |
|
|
892
900
|
| `o` | Show / hide again the lanes you hid (shown faint) |
|
|
893
901
|
| `U` | Arm the upload strip |
|
|
902
|
+
| `d` | Show the cursor lane's docs link in the footer for 3 s: `https://<server>:8443/lanes-docs/?ws=<workspace>&lane=<lane id>`, as much as the row holds, the whole line one link to all of it (⌘/Ctrl-click, or your terminal's Copy Link); then the keyboard goes back to the lane. ` no docs in ‘name’` on a lane without documents |
|
|
894
903
|
| `r` | Reload and repaint; while the footer says `update ready · reload (r)`, rebuild this view on the installed release (the window reconnects, agents are untouched); the keyboard goes back to the lane |
|
|
895
904
|
| `?` | The key sheet |
|
|
896
905
|
|
|
@@ -929,7 +938,7 @@ back clears it at once. On the phone the picker is a whole tab and has no such l
|
|
|
929
938
|
**A message stays 3 seconds, or until your next action.** What an action says in the footer (` Closed sh`,
|
|
930
939
|
` Renamed → …`, ` Cancelled`, ` view not running yet`, or why a close failed) stays after the keyboard has gone back
|
|
931
940
|
to the lane, for 3 seconds (within a second more: the sidebar looks once a second), then the legend returns. The next
|
|
932
|
-
action you start in the sidebar — opening a lane, `x`, `R`, `P`, `a`, `u`, `z`, `/`, `U`, `+`, `r`, `?`, `Tab`, a chord —
|
|
941
|
+
action you start in the sidebar — opening a lane, `x`, `R`, `P`, `a`, `u`, `z`, `/`, `U`, `+`, `r`, `?`, `d`, `Tab`, a chord —
|
|
933
942
|
replaces it at once. Moving the cursor, typing in a field, `Esc`, the wheel, a mouse release or the focus leaving and
|
|
934
943
|
coming back does not clear it.
|
|
935
944
|
|
|
@@ -1220,13 +1229,14 @@ The frontend chooses once per page load, by capability:
|
|
|
1220
1229
|
| --- | --- | --- |
|
|
1221
1230
|
| Phone | An iPhone / iPod / Android phone user agent, or a touch screen at most 600 px wide or 500 px tall | The phone shell: **Workspaces**, **Lanes**, **Panes**, **Type here** and **More** controls; the lane picker |
|
|
1222
1231
|
| Tablet | Any other touch screen | The same shell with the controls in a top bar and the workspace list as a left panel; the whole tab is shown, so a desktop view's sidebar sits beside its lanes (**More → Single pane** shows one pane for that load) |
|
|
1223
|
-
| Desktop | A fine pointer (mouse, trackpad) | The plain Zellij web client plus a **Lanes** chip (top right) that opens workspace and lane pickers in an overlay bar; the terminal keeps its size and every key |
|
|
1232
|
+
| Desktop | A fine pointer (mouse, trackpad) | The plain Zellij web client plus a **Lanes** chip (top right) that opens workspace and lane pickers and a **Docs** link (the docs page in a new tab) in an overlay bar; the terminal keeps its size and every key |
|
|
1224
1233
|
|
|
1225
1234
|
### Phone controls
|
|
1226
1235
|
|
|
1227
1236
|
- **Workspaces** (or the title in the header) opens the workspace sheet with a search field (each card shows its
|
|
1228
1237
|
counts, `◆ 1 · ◇ 2`); **Lanes** opens the current workspace's lane picker; **Panes** switches panes; **More** opens
|
|
1229
|
-
the client's own menu, where Lanes adds **Text size** (remembered on the phone)
|
|
1238
|
+
the client's own menu, where Lanes adds **Text size** (remembered on the phone), above it **Docs** — the docs page,
|
|
1239
|
+
`/lanes-docs/`, in a new tab, so the terminal stays where it is — and at the top the server's release
|
|
1230
1240
|
(**Lanes v1.2.0**, see [Versions](#versions)). The light/dark toggle repaints the whole terminal.
|
|
1231
1241
|
- The keyboard opens only when you tap **Type here** or another input. Swipes, live updates and reconnects leave it
|
|
1232
1242
|
closed. **Type here** is an ordinary text field: the normal keyboard with its suggestion bar, never the password
|
|
@@ -1259,12 +1269,110 @@ The frontend chooses once per page load, by capability:
|
|
|
1259
1269
|
on a row while the picker asks cancels the question or the name first, then opens that lane, so nothing is left
|
|
1260
1270
|
half-asked on the picker when you come back to it.
|
|
1261
1271
|
|
|
1272
|
+
### Docs
|
|
1273
|
+
|
|
1274
|
+
The Docs page is where an agent shows you its work and where you hand files to an agent — every item with a sha256
|
|
1275
|
+
receipt the server computed. There is one per server, `https://<server>:8443/lanes-docs/`: on the tailnet only, and
|
|
1276
|
+
open only to your own tailnet login. No token and no second password: you see your own uploads and what was
|
|
1277
|
+
published for you, and nobody else's documents.
|
|
1278
|
+
|
|
1279
|
+
**Open it.**
|
|
1280
|
+
|
|
1281
|
+
| From | How |
|
|
1282
|
+
| --- | --- |
|
|
1283
|
+
| Phone or tablet | **More → Docs**: the page opens in a new tab, so the terminal stays where it was |
|
|
1284
|
+
| Laptop browser | The **Lanes** chip (top right), then **Docs** in its bar (a new tab) |
|
|
1285
|
+
| Desktop sidebar | The upload strip says ` ↑ uploads · ▤ docs 3 (U)` once the workspace's lanes have documents: ⌘-click (Ghostty, iTerm2) or Ctrl-click it for the page; the cursor row's name is a link to that lane's documents, and `d` shows that link in the footer ([The sidebar](#the-sidebar)) |
|
|
1286
|
+
| A push notification | A `published` push opens the document when the server's `action_url` uses `{doc}` (section 8) |
|
|
1287
|
+
| An address you type, bookmark or open from a terminal | `/lanes-docs/` everything you may see · `/lanes-docs/?ws=<workspace>&lane=<lane id>` one lane (either key alone works) · `/lanes-docs/d/<id>` one document · `/lanes-docs/d/<id>/raw` its bytes · `/lanes-docs/receipt/<id>.json` its receipt |
|
|
1288
|
+
|
|
1289
|
+
Keep the slash: `/lanes-docs` without it is a view's address and opens the terminal page instead.
|
|
1290
|
+
|
|
1291
|
+
**What it shows.** The index lists your documents by workspace, then lane, newest first, with counts, and above them
|
|
1292
|
+
the upload form. A document opens by its kind:
|
|
1293
|
+
|
|
1294
|
+
| Kind | Opens as |
|
|
1295
|
+
| --- | --- |
|
|
1296
|
+
| `md` (`.md`, `.markdown`) | Rendered on the page, with ```` ```mermaid ```` fences drawn as diagrams (a fence Mermaid cannot read stays as its source); over 1 MiB it is not rendered: download it |
|
|
1297
|
+
| `html`, `svg` | A link, **Open the HTML artifact** (or SVG): the artifact on a page of its own, in a sandbox (below) |
|
|
1298
|
+
| `file` (anything else) | Its facts and a download |
|
|
1299
|
+
|
|
1300
|
+
Every document page also has **Download**, **Receipt** and the facts: kind, name, size, workspace, lane, time, who
|
|
1301
|
+
put it there, how it came (`publish`, `web`, `ssh`) and its sha256.
|
|
1302
|
+
|
|
1303
|
+
**Publish from a lane.** An agent publishes with the MCP tool `lanes_publish` (`path`, optional `title`, `for`,
|
|
1304
|
+
`kind`; section 7, [Agent tools (MCP)](#agent-tools-mcp)); in a lane's shell you do it with the `lane` command:
|
|
1305
|
+
|
|
1306
|
+
```sh
|
|
1307
|
+
~/.lz/bin/lane publish report.md # the kind follows the extension
|
|
1308
|
+
~/.lz/bin/lane publish out/queue.svg --title 'Queue design' --for anna@example.com
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
`lane publish <file> [--title T] [--for <login>] [--kind md|html|svg|file]` takes a file of the container under
|
|
1312
|
+
`/home/coder`, at most 32 MiB, and copies nothing: it hashes the file once, leaves a small request in
|
|
1313
|
+
`~/.lz/state/publish/<lane id>/` and prints it as JSON — the request `id`, `path`, `kind`, `bytes`, `sha256`, the
|
|
1314
|
+
`receipt` path the answer will have, `picked_up_by: "lanes-docsd"` and `expected_within_s: 5`. Within about 5 s the
|
|
1315
|
+
server reads the file out of the container, checks that its sha256 is the one the request named (a file changed in
|
|
1316
|
+
between is refused, not stored), keeps its own copy and writes the receipt beside the request: `outcome: "ok"` with
|
|
1317
|
+
the document's `id`, `sha256`, `bytes` and `url` (`/lanes-docs/d/<id>`), or `outcome: "refused"` with a `reason`
|
|
1318
|
+
(RUNBOOK "Docs service", publish pick-up). The agent sees that answer as `published` in `lanes_status` of its lane.
|
|
1319
|
+
The document is listed under the lane it came from, and the sidebar's ` ▤ docs N` counts it within a few seconds.
|
|
1320
|
+
Change the file only after the receipt is there; publishing it again makes a new document.
|
|
1321
|
+
|
|
1322
|
+
**Who sees what.**
|
|
1323
|
+
|
|
1324
|
+
- An upload: your login alone.
|
|
1325
|
+
- A published document: the login `--for` names (it must be a member's); without `--for`, the person who last opened
|
|
1326
|
+
that lane, on any of their devices; when nobody has, everyone whose member record grants the lane's workspace. A
|
|
1327
|
+
record that lists no workspace grants every workspace the server serves.
|
|
1328
|
+
- Anyone else gets *not found*, exactly as for a document that does not exist. The one exception is the count:
|
|
1329
|
+
everyone who sees a lane's ` ▤ docs N` sees how many documents it holds (a number, never a title).
|
|
1330
|
+
|
|
1331
|
+
**Upload, with a receipt.** On the Docs page pick the lane, drop files on the dashed area — or tap it to choose them,
|
|
1332
|
+
several at once, 32 MiB in all — and **Upload**. Each file lands in that workspace's `uploads/` folder in its
|
|
1333
|
+
container, exactly as every other upload does ([Uploads](#5-uploads)), and the page lists its receipt: the name (a
|
|
1334
|
+
link to the document), size, sha256 and where it landed (`/home/coder/<repo>/uploads/<name>`) — give the agent that
|
|
1335
|
+
path. Every other way in (the sidebar's strip, the watch folder, `lanes-upload`, the macOS droplet) records the same
|
|
1336
|
+
receipt for you (section 5, "Receipts").
|
|
1337
|
+
|
|
1338
|
+
**The sandbox.** Nothing an agent publishes is trusted. Markdown is rendered on the server by Lanes's own renderer:
|
|
1339
|
+
raw HTML in it stays text, a link opens in a new tab and tells the target nothing of where it came from, and an image
|
|
1340
|
+
shows only from the Docs pages themselves or inline. An HTML or SVG artifact is never shown inside a Docs page:
|
|
1341
|
+
**Open the … artifact** loads it as a page of its own under a browser sandbox, where its scripts run on an opaque
|
|
1342
|
+
origin of their own — they cannot read the Docs pages, cannot reach your terminal session or its sign-in, and cannot
|
|
1343
|
+
type into a lane. No Docs page can be framed by another page. That is how the server serves it (unit-tested); a check
|
|
1344
|
+
in a real browser is the operator's (RUNBOOK checklist; DEF-1853).
|
|
1345
|
+
|
|
1346
|
+
**Encrypted at rest.** Off by default. With the operator's `age` switch on, the server keeps new store copies
|
|
1347
|
+
encrypted; your receipts stay the sha256 of what you sent and the files in `uploads/` stay plain for the agents
|
|
1348
|
+
(section 5, "Encrypted at rest").
|
|
1349
|
+
|
|
1350
|
+
**For the administrator.**
|
|
1351
|
+
|
|
1352
|
+
- The full server install, `npx @vibelord/lanes@<release> --server`, enables the docs service (`lanes-docsd`) and
|
|
1353
|
+
installs the page's pinned Mermaid (`install/enable-docs.sh`). An update with `--update-only` enables nothing, so a
|
|
1354
|
+
server set up before the docs service needs one full install, or `systemctl --user enable --now
|
|
1355
|
+
lanes-docsd.service`. `lanes install-container <name>` gives a container `lane publish` and the `lanes_publish`
|
|
1356
|
+
tool (new agent sessions load it).
|
|
1357
|
+
- **The pin gate.** The Docs page answers nobody until the Serve header check has run and its result is pinned with
|
|
1358
|
+
`lanes enrol mode request` (RUNBOOK "Enrolment service", Operator verification). Until then every request is
|
|
1359
|
+
refused `not-pinned`.
|
|
1360
|
+
- `lanes doctor` prints five docs lines (section 6): the service answering on its loopback port, the pin gate, the
|
|
1361
|
+
store's size, the Mermaid pin and the `age` switch.
|
|
1362
|
+
- `lanes docs push <id>` prints one document's receipt and exits 2, `not configured`: the hook for pushing documents
|
|
1363
|
+
to the data service's publication surface, which is not built yet.
|
|
1364
|
+
- The unit, `docs.log`, the routes, the limits and the operator's live checklist:
|
|
1365
|
+
[RUNBOOK](RUNBOOK.md#docs-service-lanes-docsd-w10-w3).
|
|
1366
|
+
|
|
1367
|
+
Not yet verified on a device: the Docs page on a real phone (the rendering, a Mermaid diagram, the drop area), the
|
|
1368
|
+
artifact sandbox in a real browser (DEF-1853), and the Mermaid route through a running Caddy (DEF-1871).
|
|
1369
|
+
|
|
1262
1370
|
### What works, what does not, what is unverified
|
|
1263
1371
|
|
|
1264
1372
|
- Works: everything the desktop view shows, through the same Zellij sessions; the phone has its own view selection of
|
|
1265
|
-
the same agents; the server keeps serving when your Mac sleeps
|
|
1266
|
-
|
|
1267
|
-
|
|
1373
|
+
the same agents; the server keeps serving when your Mac sleeps; uploads from the phone and what agents publish, on
|
|
1374
|
+
the Docs page (**More → Docs**, [Docs](#docs)).
|
|
1375
|
+
- Does not: a public URL (Tailscale only), approving an agent's permission prompt from a push notification.
|
|
1268
1376
|
- Not yet verified on a device: the quick keys, the lane filter, the text keyboard, paste, **Copy** and **Paste** on a
|
|
1269
1377
|
real iPhone (the [runbook](RUNBOOK.md) lists the check), the light/dark terminal repaint, the counts that the
|
|
1270
1378
|
picker's pane title carries to the Workspaces sheet and the **Lanes** button (a phone view created before that
|
|
@@ -1274,7 +1382,10 @@ The frontend chooses once per page load, by capability:
|
|
|
1274
1382
|
## 5. Uploads
|
|
1275
1383
|
|
|
1276
1384
|
Files land in the workspace's `uploads/` folder in its container, the same folder Cursor users drop files into. The
|
|
1277
|
-
in-container path ends up on your clipboard and in a notification, and can be typed into the lane for the agent.
|
|
1385
|
+
in-container path ends up on your clipboard and in a notification, and can be typed into the lane for the agent. Every
|
|
1386
|
+
upload, whichever way it came, also gets a **receipt**: its sha256 computed on the server as the bytes arrived, kept
|
|
1387
|
+
with a copy in the docs store, so it shows on the Docs page (`/lanes-docs/`, section 4, [Docs](#docs)) and in the
|
|
1388
|
+
sidebar's ` ▤ docs N` count.
|
|
1278
1389
|
|
|
1279
1390
|
| Way | How |
|
|
1280
1391
|
| --- | --- |
|
|
@@ -1283,12 +1394,41 @@ in-container path ends up on your clipboard and in a notification, and can be ty
|
|
|
1283
1394
|
| Command | `lanes-upload [-w <workspace>] [--paste] <files…>` · `lanes-upload --clipboard [-w <workspace>] [--paste]` (clipboard image or copied files) |
|
|
1284
1395
|
| macOS droplet | `~/Applications/Lanes Upload.app`: drop files on it; double-click uploads the clipboard screenshot |
|
|
1285
1396
|
| Omarchy | `Super+Shift+U` uploads the clipboard and types the path into the lane |
|
|
1397
|
+
| Docs page (phone, laptop) | **More → Docs** on the phone, **Docs** in the laptop bar, or `https://<server>:8443/lanes-docs/`: pick the lane, then drop files on the dashed area or tap it to choose them (several at once, 32 MiB in all), **Upload**. Each file's receipt is listed: its name (a link to the document), size, sha256 and where it landed in the container (`/home/coder/<repo>/uploads/<name>`) — tell the agent that path |
|
|
1286
1398
|
| Server | `lanes upload <workspace> \| --active <device> [--paste]` reads a tar stream on stdin |
|
|
1287
1399
|
|
|
1288
1400
|
**Where files land.** `<workspace folder>/uploads/<name>` in the container, where the workspace folder is its first
|
|
1289
1401
|
lane's repository folder (else the workspace's working directory). An existing name is never overwritten: the next
|
|
1290
|
-
one becomes `<name>-1.<ext>`. The folder is created on first use and, in a git repository
|
|
1291
|
-
yet, added
|
|
1402
|
+
one becomes `<name>-1.<ext>`. The folder is created on first use and, in a git repository whose `.git/info/exclude`
|
|
1403
|
+
does not name it yet, added there, so uploads are never committed. The Docs page lands a file exactly the same way
|
|
1404
|
+
(its name keeps only letters, digits, `.`, `_` and `-`). The server refuses to follow a symbolic link anywhere on the
|
|
1405
|
+
way, so an agent cannot redirect an upload outside its container: a folder reached through a link answers
|
|
1406
|
+
`uploads/ … not a directory (a symbolic link is refused)` (or, on the Docs page, *cannot be written now*).
|
|
1407
|
+
|
|
1408
|
+
**Receipts.** Each file is also a document in the docs store with a receipt — `sha256`, size, time and its page
|
|
1409
|
+
`/lanes-docs/d/<id>` — for you only (`for` your login): the Docs page lists it under its workspace and lane, and the
|
|
1410
|
+
sidebar counts it. `lanes upload` prints one `# docs: <name> <size> B sha256 <hash> <id>` line per file on stderr (the
|
|
1411
|
+
helper's log, or your terminal with `lanes-upload`). The kind follows the extension: `.md` opens rendered (with Mermaid
|
|
1412
|
+
diagrams), `.html`/`.svg` open as sandboxed artifacts, anything else as a download. The store keeps its own copy, so
|
|
1413
|
+
an agent editing its `uploads/` file never changes what the receipt names. What gets no receipt, though it still
|
|
1414
|
+
lands: a file over 32 MiB sent with `lanes upload` (the store keeps at most 32 MiB a file), a device no member record
|
|
1415
|
+
owns (the server logs its hash in `docs.log`), and files past the 64th of one `lanes upload`. A file name taken in only
|
|
1416
|
+
one of the two places (an agent's own file in `uploads/`, or an earlier upload you removed from the store) can get
|
|
1417
|
+
`-1` in one and not the other; the receipt names both.
|
|
1418
|
+
|
|
1419
|
+
**Encrypted at rest (optional).** The operator can have the server keep its docs-store copies encrypted with
|
|
1420
|
+
[`age`](https://age-encryption.org) (`LANES_DOCS_AGE_RECIPIENT`, [RUNBOOK](RUNBOOK.md#docs-service-lanes-docsd-w10-w3)).
|
|
1421
|
+
Nothing changes in `uploads/` — the agent reads plain files there — and receipts stay the hash of what you sent. When
|
|
1422
|
+
the docs service also holds the key, documents open as usual; when it does not, a download answers *encrypted at
|
|
1423
|
+
rest* and you fetch the file from the workspace's `uploads/` folder instead.
|
|
1424
|
+
|
|
1425
|
+
**Opening the Docs page from elsewhere.** Any link to a document opens it: the shell's **Docs** link, the sidebar's
|
|
1426
|
+
links, a notification, the address bar, and a link on another website — a web mail, a chat in the browser, ntfy's web
|
|
1427
|
+
app. What the docs service refuses from another website is everything that is not you following a link: a page's own
|
|
1428
|
+
background requests, images and scripts, and an upload — so an HTML artifact cannot read anything of yours, and only
|
|
1429
|
+
the Docs page itself uploads. A browser too old to say where a request came from sees *Refused: this request came from
|
|
1430
|
+
another site*. Viewing very quickly (over 300 pages a minute) answers *Too many requests* for a
|
|
1431
|
+
minute; an upload that trickles in slower than about 64 KiB/s is stopped and says so, with nothing stored.
|
|
1292
1432
|
|
|
1293
1433
|
**Why click first.** A terminal delivers a drop as a paste of the local path into the focused pane, so the strip must
|
|
1294
1434
|
hold the focus when you let go. The server receives only the path; the **upload helper** on your machine sends the
|
|
@@ -1516,10 +1656,15 @@ Names must be non-empty and free of control characters. The id, resolve file and
|
|
|
1516
1656
|
present (`✓`), missing or pointing elsewhere (`✗` once a web unit is enabled, `⚠` before), or not checked (`⚠`: no
|
|
1517
1657
|
tailscale, no answer, or no Tailscale name on the host — then `lanes web` configures no Serve and prints a loopback
|
|
1518
1658
|
URL, so doctor does not ask for the mapping either).
|
|
1659
|
+
- The docs service (W10): `lanes-docsd` enabled, active and answering on its loopback port (`✓`; `⚠` not enabled; `✗`
|
|
1660
|
+
enabled but not running, or not answering), the pin gate (`✓ docs: pinned`, else `⚠ docs: refuses until the Serve
|
|
1661
|
+
header check is pinned`), the store (documents, bytes, `docs.log`'s size), the pinned Mermaid (`✓`, `⚠` not installed
|
|
1662
|
+
— `install/enable-docs.sh`, which every server install and update runs —, `✗` a file that is not the pinned build) and
|
|
1663
|
+
the `age` switch (`✗` when it is set without an `age` binary); RUNBOOK "Docs service" has every line.
|
|
1519
1664
|
- The release that runs, against `~/.local/share/lanes/current` (the tree an install makes current; without the link,
|
|
1520
1665
|
a `⚠` and this `lanes`'s own tree): the link names a tree that exists, whose `VERSION` agrees with its
|
|
1521
1666
|
`pkg/<version>` name and its npm stamp, and that this `lanes` runs (`✗` with the fix: run the current one, or re-run
|
|
1522
|
-
the server installer). Every lanes process running now (statusd, enrold, notify, economy, a sidebar — found in the
|
|
1667
|
+
the server installer). Every lanes process running now (statusd, enrold, docsd, notify, economy, a sidebar — found in the
|
|
1523
1668
|
same process-table sweep by the command Lanes starts it with, `python3 -S -E <tree>/libexec/<script>`, so a file
|
|
1524
1669
|
open in an editor or a one-shot such as `notify.py --test` is not one) has a [run record](#versions) of the current
|
|
1525
1670
|
code (`✗ … statusd runs 1.1.0`, or `statusd runs without a run record (started by an older release)`, then
|
|
@@ -1655,6 +1800,7 @@ verbs, so the holder stays swappable.
|
|
|
1655
1800
|
| `lane new <id> [--ws W] [--title T] [--cwd DIR] [--icon I] [--color C] -- <command…>` | Create a zmx-held lane |
|
|
1656
1801
|
| `lane gc --sockets` | Unlink this container's tmux socket files that tmux is refused on (no server listens); a live, empty or silent server keeps its file |
|
|
1657
1802
|
| `lane mcp install [--agent claude\|codex\|all] [--write]` | Add the Lanes MCP server to `~/.claude.json` and `~/.codex/config.toml` ("Agent tools (MCP)" below); a dry run until `--write` |
|
|
1803
|
+
| `lane publish <file> [--title T] [--for <login>] [--kind md\|html\|svg\|file]` | Put a file of this container on the docs page from the lane it runs in ("Publish to the docs page" below); prints the request as JSON |
|
|
1658
1804
|
|
|
1659
1805
|
Every tmux client these verbs start runs with `-N`, so none of them can start a tmux server: an attach to a lane
|
|
1660
1806
|
whose server has just ended fails instead of leaving an empty server and its socket file behind.
|
|
@@ -1675,6 +1821,18 @@ file is written in place (it keeps its mode and owner), swap and backup files go
|
|
|
1675
1821
|
beside the file, modelines are off, a file without a final newline keeps none, and no `~/.viminfo` is written.
|
|
1676
1822
|
Everything it creates is private (`umask 077`). Quitting the editor closes the pane.
|
|
1677
1823
|
|
|
1824
|
+
### Publish to the docs page
|
|
1825
|
+
|
|
1826
|
+
An agent (or you, in a lane's shell) puts a file on the [Docs page](#docs) with `~/.lz/bin/lane publish <file>` or
|
|
1827
|
+
the MCP tool `lanes_publish`: Markdown with Mermaid diagrams is rendered there, an HTML or SVG artifact opens
|
|
1828
|
+
sandboxed on its own page, and any other file is a download, each with the sha256 the server computed. The verb writes
|
|
1829
|
+
a small request in `~/.lz/state/publish/<lane>/` and copies nothing; within about 5 s the docs service on the server
|
|
1830
|
+
reads the file from the container, checks its hash against the request's (a file changed in between is refused, not
|
|
1831
|
+
stored) and writes `<id>.receipt.json` beside the request — `lanes_status` shows it as `published`, with the
|
|
1832
|
+
document's link. The file must be under `/home/coder` and at most 32 MiB. Who sees it (`--for`, else the person who
|
|
1833
|
+
last opened the lane, else everyone whose member record grants its workspace), the receipt's fields and how each kind
|
|
1834
|
+
opens: section 4, [Docs](#docs).
|
|
1835
|
+
|
|
1678
1836
|
### Agent tools (MCP)
|
|
1679
1837
|
|
|
1680
1838
|
`lane-mcp` is an MCP server in each container (`~/.lz/bin/lane-mcp`). Claude Code and Codex start it from their own
|
|
@@ -1691,7 +1849,7 @@ only read:
|
|
|
1691
1849
|
Where the answers come from: `~/.lz/registry.json`, which `lanes sync` writes, and `~/.lz/status.json`, which the
|
|
1692
1850
|
status daemon writes. A container that has neither yet answers with empty lists and says why.
|
|
1693
1851
|
|
|
1694
|
-
|
|
1852
|
+
Seven tools write. Each reaches the lanes of its own container only, and answers what it changed, what picks the
|
|
1695
1853
|
change up (`picked_up_by`) and how soon (`expected_within_s`):
|
|
1696
1854
|
|
|
1697
1855
|
| Tool | Changes | Seen on the boards |
|
|
@@ -1701,6 +1859,7 @@ change up (`picked_up_by`) and how soon (`expected_within_s`):
|
|
|
1701
1859
|
| `lanes_rename` | Sets the lane's `displayName` in its terminals.json; the id never changes. The lane's own title removes it. A name the host sets for the lane in its workspace manifest still wins, and the answer says so (`host_override`) | same |
|
|
1702
1860
|
| `lanes_new` | `name`, optional `cwd`, `command`, `workspace`, `repo`. Declares a lane on the board's shell socket `lanes-shells-<workspace>` and starts its session (detached, 100x30) with the lane's identity; the workspace defaults to the agent's own board, the folder to the repository's, the command to `bash -l`. When that starts the board's shell server, the server gets the account and locale variables (and `TMUX_TMPDIR`), never the agent's own environment, which every later shell on the board would inherit | the session at once; the lane after the next `lanes sync` |
|
|
1703
1861
|
| `lanes_send` | `lane`, `text`, optional `enter` (default true), `force`. Types printable text into the lane, then Enter half a second later as a separate keystroke, so an agent's prompt submits it instead of taking it as a paste | at once |
|
|
1862
|
+
| `lanes_publish` | `path` (an absolute path under `/home/coder`), optional `title`, `for` (a login), `kind` (`md`, `html`, `svg`, `file`; default from the extension). Writes a publish request for the file from the agent's own lane ("Publish to the docs page" below); the docs service reads the file itself and answers with a receipt that `lanes_status` shows as `published` | on the docs page within about 5 s |
|
|
1704
1863
|
| `lanes_signal` | `status` (the agent words: `working`, `needs_input`, `needs_approval`, `done`, `errored`, `shell`, `idle`), optional `note`. Reports the agent's own status through `lane-signal`; the note (one line, at most 120 characters) is shown as the lane's task line. The report stands until the agent does something new: its next tool call, a new prompt, a permission prompt, a failed turn or the session's end. The end of its turn keeps it (a `working` report ends there), so an agent reports `needs_input`, `done` or `errored` as the last thing before it stops. A `working` note stays while the agent keeps working | on the next status cycle, within about 5 s |
|
|
1705
1864
|
|
|
1706
1865
|
What they refuse. A refusal is `isError` with `{"refused": <code>, "why", "human"}`: `human` says what a person can
|
|
@@ -1715,7 +1874,10 @@ refuses an unregistered session (register it first) and a name with a control ch
|
|
|
1715
1874
|
folder outside `/home/coder` or one that does not exist (`invalid_cwd`), a name or session in use and a command with
|
|
1716
1875
|
a control character. `lanes_send` types nothing while the lane's agent is working or waiting for an approval
|
|
1717
1876
|
(`agent_working`, `agent_needs_approval`) unless `force` is true, and never a control character: no newline, tab,
|
|
1718
|
-
escape or Ctrl-C (`invalid_text`).
|
|
1877
|
+
escape or Ctrl-C (`invalid_text`). `lanes_publish` refuses a file outside `/home/coder` once every link is resolved
|
|
1878
|
+
(`outside_home`), anything but a regular file (`not_a_file`), a file over 32 MiB (`too_large`), a title or login that
|
|
1879
|
+
is not one printable line (`invalid_title`, `invalid_for`), and an agent outside a lane (`not_in_lane`). Every tool
|
|
1880
|
+
refuses a lane of another container (`not_in_container`). The edits go
|
|
1719
1881
|
through the same minimal editor as everything else here: one span of strict JSON, the file re-read just before the
|
|
1720
1882
|
atomic write, never the root `throttle` object, never a rename or reorder of terminals. The file is the
|
|
1721
1883
|
repository's, so its history is git's: no backup file is written.
|
|
@@ -1845,7 +2007,7 @@ you configure, so enabling it is your decision. Not yet run against a real ntfy
|
|
|
1845
2007
|
| `topic` | required | `[A-Za-z0-9_-]{1,64}`; on a public server the topic name is the only secret |
|
|
1846
2008
|
| `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) |
|
|
1847
2009
|
| `quiet_when_device_active_s` | `120` | Presence window; `0` never suppresses — except for the one person present when their profile sets a `notify.quiet_s` of its own |
|
|
1848
|
-
| `action_url` | unset | A URL template over `{lane}`; becomes the tap target and an **Open lane** button |
|
|
2010
|
+
| `action_url` | unset | A URL template over `{lane}`; becomes the tap target and an **Open lane** button. `{doc}` is the docs page of the document a `published` push names (`/lanes-docs/d/<id>`, so `https://<server>:8443{doc}` opens it) and empty in a needs-you push. The docs service sends a `published` push (lane name, kind and size, never the title) when it stores a document an agent published, at most one per lane per minute, muted like the others while you are at a desk |
|
|
1849
2011
|
|
|
1850
2012
|
Nothing is sent when the status did not change, when the service just started (no burst on restart), or when a desk
|
|
1851
2013
|
device was present within the presence window: a `lanes attach` from any device, or a lane opened from a desk
|
|
@@ -1975,6 +2137,7 @@ All units run in `lanes.slice` and are rendered by the server installer from `la
|
|
|
1975
2137
|
| `lanes-gc.timer` / `.service` | Daily | Server install | `lanes gc`: truncate zmx logs over 5 MB, drop stale dynamic resolve files, prune old npm-staged trees — each folder keeps its 3 newest and every older tree `current`, `current.previous`, a view layout, a running daemon's run record or a process of this account uses; none when `/proc` cannot be read ([INSTALL](INSTALL.md#6-updates-optional-operations-and-removal)) |
|
|
1976
2138
|
| `lanes-update.timer` / `.service` | 15 min after boot, then every 30 min (+0–5 min) | Server install (the full one: `--update-only` enables nothing — `systemctl --user enable --now lanes-update.timer` on a server set up before 1.2.0) | `lanes update --timer`: install the newest npm release when it is newer ([Update](#update)); a checkout server is left alone; stopped after 1 h |
|
|
1977
2139
|
| `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)) |
|
|
2140
|
+
| `lanes-docsd.service` | Always on | Server install (enabled and restarted by every one) | Docs service on `127.0.0.1:8185`, reached as `https://<host>:8443/lanes-docs/`: login-bound documents, sandboxed artifacts and uploads with receipts; refuses every request until `lanes enrol mode request` is pinned ([RUNBOOK](RUNBOOK.md#docs-service-lanes-docsd-w10-w3)) |
|
|
1978
2141
|
| `lanes-web.service` | Always on | `enable-web.sh` (via `lanes web` / `enable-phone.sh`) | Zellij web server on `127.0.0.1:8082` |
|
|
1979
2142
|
| `lanes-mobile.service` | Always on | `enable-web.sh` | Caddy frontend on `127.0.0.1:8083` |
|
|
1980
2143
|
| `lanes-studio-host.service` | Always on | `enable-studio-host.sh` (opt-in) | The co-located Throttle Studio Host |
|
|
@@ -2027,6 +2190,7 @@ Clients also get `lanes-helper.service` and `lanes-upload.path`/`.service` (Linu
|
|
|
2027
2190
|
| 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) |
|
|
2028
2191
|
| Containers | `lanes hooks install --agent claude\|codex (--container <name> \| --home <dir>) [--dry-run \| --write]` | Section 7 |
|
|
2029
2192
|
| Uploads | `lanes upload <ws> \| --active <device> [--paste]` | Read a tar stream into that workspace's `uploads/` |
|
|
2193
|
+
| Uploads | `lanes docs push <id>` | Print one docs-store document's receipt (any login's: you are the operator) and exit 2 `not configured`: the hook for the data service's publication surface, not built yet ([Docs](#docs)) |
|
|
2030
2194
|
| Client | `lanes hello [device] [--client-version V]` | Prove path, key and server version; ` update=<version>` when V is older |
|
|
2031
2195
|
| Client | `lanes client-config` | Site facts for clients (`LANES_*` lines from `~/.config/lanes/site.conf`) |
|
|
2032
2196
|
| Access | `lanes keys list \| add <device> <pubkey-file\|-> \| revoke <device> \| adopt [--dry-run] [--login LOGIN]` | Who may log in: the lanes-managed block of `~/.ssh/authorized_keys`, projected from `members/`; `--login` names the owner of every key adopt moves in and folds their views into that profile |
|
|
@@ -2073,7 +2237,7 @@ Server (`~/.local/state/lanes/`, `$LANES_STATE`):
|
|
|
2073
2237
|
| --- | --- |
|
|
2074
2238
|
| `registry.json`, `registry.synced`, `resolve/`, `resolve.dynamic/` | The published registry, the last sync time, per-lane targets |
|
|
2075
2239
|
| `status.json`, `health` | statusd's snapshot and the one-line health record |
|
|
2076
|
-
| `run/<name>.tree` | The tree each long-running process started from — `statusd`, `enrold`, `economy`, `notify`, `studio_host` (the enable step its enrolment timer runs) and `sidebar` (the most recently started one): one resolved path, rewritten at every start, so a record that differs from where `~/.local/share/lanes/current` points is a process still running an older version |
|
|
2240
|
+
| `run/<name>.tree` | The tree each long-running process started from — `statusd`, `enrold`, `docsd`, `economy`, `notify`, `studio_host` (the enable step its enrolment timer runs) and `sidebar` (the most recently started one): one resolved path, rewritten at every start, so a record that differs from where `~/.local/share/lanes/current` points is a process still running an older version |
|
|
2077
2241
|
| `containers.json`, `containers/` | Container init pids and home volumes |
|
|
2078
2242
|
| `devices/<device>.json` | Each known device: layout, viewports, base device, owner, look (colour, theme, glyphs, dim; `device_only` lists the keys `--device-only` kept for this device alone), desktop notifications on/off and the quick-edit editor |
|
|
2079
2243
|
| `layouts/<device>.kdl` | The layout a view session was built from |
|
|
@@ -2095,7 +2259,9 @@ Server (`~/.local/state/lanes/`, `$LANES_STATE`):
|
|
|
2095
2259
|
| `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 |
|
|
2096
2260
|
| `enrol-requests/<id>.json` | Enrolment requests waiting for, or past, a member's approval (24 h) |
|
|
2097
2261
|
| `enrol.log` | The enrolment audit log, one line per attempt, invite, revocation, offboard and mode change (0600, append-only; never a code) |
|
|
2098
|
-
| `enrol-mode` | Request mode as `lanes enrol mode request\|invite` pinned it (0600; absent = auto); the enrolment service
|
|
2262
|
+
| `enrol-mode` | Request mode as `lanes enrol mode request\|invite` pinned it (0600; absent = auto); the enrolment service and the docs service read it on every request (docsd serves only while it says `request`) |
|
|
2263
|
+
| `docs/<login>/<ws>/<lane>/<id>/`, `docs/counts.json` | The docs store: each document with its `meta.json` and sha256 `receipt.json` (0700/0600; `<name>.age` when encrypted at rest); counts per lane. Written by every upload (the Docs page and `lanes upload`) and by the docs service's publish pick-up (`lane publish`, `lanes_publish`), read by the docs service ([RUNBOOK](RUNBOOK.md#docs-service-lanes-docsd-w10-w3)) |
|
|
2264
|
+
| `docs.log` | The docs audit log: one line per intake, removal, read, upload refusal, publish request and docs-service denial (0600, append-only; never content, a title or a name) |
|
|
2099
2265
|
| `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") |
|
|
2100
2266
|
| `onboard/<login>-<UTC>.json` | One receipt per `lanes onboard` run: each step with its status (section 7) |
|
|
2101
2267
|
| `update/last.json`, `update/last.log`, `update/.lock` | The last `install.sh --server --update-only` run: `{checked_at, latest, installed, previous, result, steps: [{name, rc}], log}` with `result` `ok`, `partial`, `refused` or `failed` (a step whose exit status is a warning — `lanes sync` reporting a board with an error — carries `"warning": true`; keys it does not own are kept), and that run's full output; the run lock every server install and update holds while it runs (`{pid, mode, started}` of the last one to take it). `lanes update` adds `check` — its last check, `{at, by: timer\|hand, channel, tag, latest, installed, outcome, said[, exit]}`, `outcome` one of `current`, `older`, `bad`, `unanswered`, `refused`, `installing`, `installed`, `failed` — `bad` (the release a rollback left, written with the record of its flip, before its post-commit) and `rerun` (the release whose post-commit it ran again; a rollback drops it); a rollback's run is recorded with the step `rollback` first, `partial` until its post-commit has run |
|
|
@@ -2105,7 +2271,8 @@ Server (`~/.local/state/lanes/`, `$LANES_STATE`):
|
|
|
2105
2271
|
Other server paths: `~/.config/lanes/` (`workspaces/`, `site.conf`, `notify.conf`, `update.conf` — `channel=stable`
|
|
2106
2272
|
makes `lanes update` follow the `lanes-stable` dist-tag — and the optional invite key files
|
|
2107
2273
|
`resend.key` and `tailscale-api.key`), `~/.local/share/lanes/` (pinned
|
|
2108
|
-
binaries in `bin/`, the
|
|
2274
|
+
binaries in `bin/`, the docs page's pinned Mermaid in `vendor/` — `install/enable-docs.sh`, which every server install and update runs —, the `current`
|
|
2275
|
+
link to the installed tree — the path units and layouts name —,
|
|
2109
2276
|
`current.previous`, the tree it named before the last install that moved it, and `.install.lock`, the lock installs
|
|
2110
2277
|
and unit re-renders take turns on; npm-staged `pkg/<version>/`), and
|
|
2111
2278
|
`~/.lz/<container>` (links to each
|
|
@@ -2193,12 +2360,18 @@ Server and views:
|
|
|
2193
2360
|
| `LANES_ENROL_PORT` | `libexec/enrold.py` | `8184` | The enrolment service's loopback port (the Caddyfile's `/enrol/*` route must follow) |
|
|
2194
2361
|
| `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 |
|
|
2195
2362
|
| `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") |
|
|
2363
|
+
| `LANES_DOCS_PORT` | `libexec/docsd.py` | `8185` | The docs service's loopback port (the Caddyfile's `/lanes-docs/*` route must follow) |
|
|
2364
|
+
| `LANES_DOCS_DOMAINS` | `libexec/docsd.py` | `LANES_ENROL_DOMAINS`, else `beyondwork.ai` | Login domains the docs service answers (comma or space separated; `*` = any) |
|
|
2365
|
+
| `LANES_DOCS_AGE_RECIPIENT` | `libexec/docs_store.py` (the docs service and `lanes upload`) | unset (off) | Keep new docs-store copies as `age -r <recipient>` output; the server install refuses it without an `age` binary ([RUNBOOK](RUNBOOK.md#docs-service-lanes-docsd-w10-w3)) |
|
|
2366
|
+
| `LANES_DOCS_AGE_IDENTITY` | `libexec/docsd.py` | unset | The `age` identity file the docs service decrypts with; without it an encrypted document's download answers 409 |
|
|
2196
2367
|
| `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) |
|
|
2197
2368
|
| `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 |
|
|
2198
2369
|
| `LANES_TESTS_FORCE` | `tests/guards.py` | unset | `1` runs live suites on a busy host (on purpose only) |
|
|
2199
2370
|
|
|
2200
2371
|
`LANES_THROTTLE_*` and `LANES_NOTIFY_CONF` set in the installer's environment are rendered into their units only. The
|
|
2201
|
-
`LANES_ENROL_*` settings go in a drop-in, `systemctl --user edit lanes-enrold.service`, which a re-install keeps
|
|
2372
|
+
`LANES_ENROL_*` settings go in a drop-in, `systemctl --user edit lanes-enrold.service`, which a re-install keeps; the
|
|
2373
|
+
`LANES_DOCS_*` settings in lanes-docsd's, `systemctl --user edit lanes-docsd.service` (`lanes upload` reads the age
|
|
2374
|
+
switch from the same files).
|
|
2202
2375
|
`LANES_REPO`, `LANES_ZELLIJ` and `LANES_WS` are set by Lanes itself for its units and panes.
|
|
2203
2376
|
|
|
2204
2377
|
Client:
|
|
@@ -2248,6 +2421,7 @@ Container:
|
|
|
2248
2421
|
| `lanes preview` / `next` / `prev` / `edit` without `--focused` | `2` |
|
|
2249
2422
|
| `lanes edit --focused` | the editor's own status once a file is picked (`docker exec` passes on `lane-edit`'s) · `0` closed with `q`/`Esc`, nothing to edit, or another Edit pane in this tab now has the focus · `1` another Edit pane is open in another tab (or zellij would not focus it), zellij did not answer, a lane that is not a tmux lane, a container that is not running or has no `lane-edit`, no git checkout, docker could not be started, or another error |
|
|
2250
2423
|
| `lanes version` | `0` (a stale process is shown, not an exit status) · `2` an argument other than `--json` |
|
|
2424
|
+
| `lanes docs push <id>` | `2` not configured (the receipt printed, nothing sent) · `1` no document with that id, an id that is not one, or another argument (the usage) |
|
|
2251
2425
|
| `lanes update` | `0` nothing to do (up to date, not due, an older or bad release) or installed `ok` · `4` partial (the install's, or a post-commit run again) · `3` refused, nothing done: another update or install is running, a checkout server, no installed release, a bad `update.conf`, no release to roll back to — under `--timer` these are `0` · `1` failed: the registry gave no release, no usable recorded npx, the install failed or recorded nothing · `2` usage; `--status`: `0`, or `1` when nothing is recorded yet |
|
|
2252
2426
|
| `lanes studio-host status` | `0` only when running enrolled; `1` otherwise; `2` usage |
|
|
2253
2427
|
| `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 |
|
|
@@ -2256,9 +2430,11 @@ Container:
|
|
|
2256
2430
|
| `enable-studio-host.sh` | `0` running or waiting for enrolment · `2` usage · `3` typed refusal · `1` failure |
|
|
2257
2431
|
| `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 |
|
|
2258
2432
|
| `enable-web.sh` | `0` · `2` usage · `1` Caddy missing |
|
|
2433
|
+
| `enable-docs.sh` | `0` the pinned Mermaid installed or already there · `1` a prerequisite or the download failed · `2` usage · `3` refused (no valid pin, a digest mismatch, the member missing): nothing installed |
|
|
2259
2434
|
| `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 |
|
|
2260
2435
|
| `lanes-window` | `64` invalid suffix, or `--device-only` without `--ascii`/`--light`/`--dark` · `69` no Ghostty (macOS) or neither Ghostty nor Foot (Linux) |
|
|
2261
2436
|
| `lanes-upload` | `64` usage · `66` no such file or empty clipboard · otherwise the transfer's status |
|
|
2262
2437
|
| `lane status` | `1` when the lane is not running |
|
|
2438
|
+
| `lane publish <file>` | `0` the request is written (the receipt follows within about 5 s) · `1` refused or a usage error (one line on stderr) |
|
|
2263
2439
|
| `lane-edit <file>` (container) | the editor's own status · `2` usage · `127` no editor on PATH; on a terminal both refusals wait for `Enter` first |
|
|
2264
2440
|
| Live test suites (`tests/*.py`) | `75` host busy, not started |
|
|
@@ -16,3 +16,8 @@ glow 3.0.0 13e05e4b2acc18d2aee44291aefe6325b077ec321b631a0cfa780e8e3bc33f78 http
|
|
|
16
16
|
# a locally built binary with `lanes studio-host install --from PATH [--sha256 HEX]` (also sha256-verified).
|
|
17
17
|
# Template (fill from the release's two .sha256 sidecars, then uncomment):
|
|
18
18
|
# throttle-rc node-<sha12> <tarball-sha256> https://github.com/bwllaming/bw-tools/releases/download/node/<sha12>/throttle-rc-x86_64-unknown-linux-gnu.tar.gz opt-in throttle-rc-x86_64-unknown-linux-gnu/throttle-rc <binary-sha256>
|
|
19
|
+
# mermaid 11.17.2 (the docs page's diagrams, plan W10 W6, LNS-107): the npm tarball, 17,796,365 B, whose sha512 is the
|
|
20
|
+
# registry's dist.integrity; the member is the classic-script (IIFE) build, 3,572,661 B, that web/docs.js loads with
|
|
21
|
+
# this digest as its SRI. Both sha256s measured 2026-10-08. install/enable-docs.sh installs it into
|
|
22
|
+
# ~/.local/share/lanes/vendor/; to bump it see docs/RUNBOOK.md "Mermaid pin".
|
|
23
|
+
mermaid 11.17.2 6ad2f42c3fc26bbf9e45cbb6d11898972573ea52b33a5f4ff51952899f950ffd https://registry.npmjs.org/mermaid/-/mermaid-11.17.2.tgz opt-in package/dist/mermaid.min.js 581ed7d74bd9048d0e3a91363927d72ef22942d7722546b27f7cc29e35390eb8
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# The docs page's pinned Mermaid (plan W10 W6, LNS-107; docs/RUNBOOK.md "Mermaid pin"). Every server install runs it
|
|
3
|
+
# right after lanes-docsd's restart and every update (--update-only) as the post-commit step `enable-docs` (W8, LNS-109:
|
|
4
|
+
# host-install.sh, libexec/update.py DOCS_STEP), never fatally; it runs by hand as well. As the workspace account, never
|
|
5
|
+
# root. Reads holders.lock's `mermaid` opt-in pin, downloads its npm tarball into a temporary
|
|
6
|
+
# directory, checks the tarball's sha256, extracts ONLY the pinned member (never a link), checks the member's sha256,
|
|
7
|
+
# and installs it as $LANES_SHARE/vendor/mermaid-<version>.min.js (0644, by rename) with the relative link
|
|
8
|
+
# mermaid.min.js -> that file. Caddy serves the directory at /lanes-assets/vendor/ (web/Caddyfile); web/docs.js
|
|
9
|
+
# loads the versioned name with the member digest as its SRI. Idempotent: an installed file whose digest is the pin's
|
|
10
|
+
# is kept and nothing is downloaded; one that differs is replaced from a verified download. Older versions stay.
|
|
11
|
+
# Exit status: 0 installed, or already present; 1 a prerequisite is missing or the download failed; 2 usage;
|
|
12
|
+
# 3 refused — holders.lock has no valid mermaid pin, a digest differs, or the member is missing or not a regular file.
|
|
13
|
+
# A refusal or failure installs nothing.
|
|
14
|
+
set -eu
|
|
15
|
+
repo=$(cd "$(dirname "$0")/.." && pwd)
|
|
16
|
+
usage='usage: enable-docs.sh [--help]'
|
|
17
|
+
case "$#:${1:-}" in
|
|
18
|
+
0:) ;;
|
|
19
|
+
1:-h|1:--help)
|
|
20
|
+
echo "$usage"
|
|
21
|
+
echo 'Installs the Mermaid that holders.lock pins (opt-in line `mermaid`) into ~/.local/share/lanes/vendor/'
|
|
22
|
+
echo '($LANES_SHARE/vendor), after checking the sha256 of the npm tarball and of its member.'
|
|
23
|
+
echo 'Exit status: 0 installed or already present; 1 a prerequisite is missing or the download failed;'
|
|
24
|
+
echo '2 usage; 3 refused (no valid pin, a sha256 mismatch, the member missing or not a regular file).'
|
|
25
|
+
exit 0 ;;
|
|
26
|
+
*) echo "$usage" >&2; exit 2 ;;
|
|
27
|
+
esac
|
|
28
|
+
[ "$(id -u)" != 0 ] || { echo 'Run as the workspace account, not root.' >&2; exit 1; }
|
|
29
|
+
for command in curl tar sha256sum install mktemp; do
|
|
30
|
+
command -v "$command" >/dev/null 2>&1 || { echo "Missing prerequisite: $command (see docs/INSTALL.md)" >&2; exit 1; }
|
|
31
|
+
done
|
|
32
|
+
share=${LANES_SHARE:-$HOME/.local/share/lanes}
|
|
33
|
+
lock=$repo/holders.lock
|
|
34
|
+
refuse() { echo "enable-docs: refused, nothing was installed: $*" >&2; exit 3; }
|
|
35
|
+
hex64() { [ "${#1}" -eq 64 ] && case $1 in *[!0-9a-f]*) false ;; esac; }
|
|
36
|
+
|
|
37
|
+
# The pin: exactly one `mermaid` line of the 7-field opt-in grammar (holders.lock's header; studio_host.parse_lock).
|
|
38
|
+
pins=$(grep -v '^[[:space:]]*#' "$lock" | awk '$1 == "mermaid"') || :
|
|
39
|
+
[ -n "$pins" ] || refuse "$lock has no mermaid line"
|
|
40
|
+
[ "$(printf '%s\n' "$pins" | wc -l)" -eq 1 ] || refuse "$lock pins mermaid more than once"
|
|
41
|
+
# shellcheck disable=SC2086 # the line's words are the fields; no pathname expansion of them
|
|
42
|
+
set -f
|
|
43
|
+
set -- $pins
|
|
44
|
+
set +f
|
|
45
|
+
[ "$#" -eq 7 ] && [ "$5" = opt-in ] || refuse "$lock: the mermaid line is not 'name version sha256 url opt-in member sha256'"
|
|
46
|
+
version=$2 sha=$3 url=$4 member=$6 member_sha=$7
|
|
47
|
+
case $version in ''|*[!A-Za-z0-9._-]*) refuse "$lock: mermaid version '$version' is not a plain word" ;; esac
|
|
48
|
+
hex64 "$sha" || refuse "$lock: the mermaid tarball digest is not 64 lowercase hex"
|
|
49
|
+
hex64 "$member_sha" || refuse "$lock: the mermaid member digest is not 64 lowercase hex"
|
|
50
|
+
case $url in https://*) ;; *) refuse "$lock: the mermaid url is not https" ;; esac
|
|
51
|
+
case /$member/ in //*|*/../*|*/./*|*//*) refuse "$lock: member '$member' is not a plain relative path" ;; esac
|
|
52
|
+
|
|
53
|
+
vendor=$share/vendor
|
|
54
|
+
target=$vendor/mermaid-$version.min.js
|
|
55
|
+
if [ -f "$target" ] && [ ! -L "$target" ] && [ "$(sha256sum "$target" | cut -d' ' -f1)" = "$member_sha" ]; then
|
|
56
|
+
ln -sfn "mermaid-$version.min.js" "$vendor/mermaid.min.js"
|
|
57
|
+
echo "ok mermaid $version ($target)"
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
temporary=$(mktemp -d)
|
|
62
|
+
trap 'rm -rf "$temporary"' EXIT
|
|
63
|
+
trap 'exit 130' INT
|
|
64
|
+
trap 'exit 143' TERM HUP
|
|
65
|
+
curl -fsSL --retry 3 -o "$temporary/mermaid.tgz" "$url" || { echo "enable-docs: download failed: $url" >&2; exit 1; }
|
|
66
|
+
actual=$(sha256sum "$temporary/mermaid.tgz" | cut -d' ' -f1)
|
|
67
|
+
[ "$actual" = "$sha" ] || refuse "sha256 mismatch for the mermaid $version tarball: $actual, pinned $sha"
|
|
68
|
+
mkdir "$temporary/x"
|
|
69
|
+
tar -xzf "$temporary/mermaid.tgz" -C "$temporary/x" "$member" 2>/dev/null || refuse "the tarball has no $member"
|
|
70
|
+
file=$temporary/x/$member
|
|
71
|
+
[ -f "$file" ] && [ ! -L "$file" ] || refuse "$member in the tarball is not a regular file"
|
|
72
|
+
actual=$(sha256sum "$file" | cut -d' ' -f1)
|
|
73
|
+
[ "$actual" = "$member_sha" ] || refuse "sha256 mismatch for $member: $actual, pinned $member_sha"
|
|
74
|
+
mkdir -p "$vendor"
|
|
75
|
+
install -m 0644 "$file" "$vendor/.mermaid-$version.min.js.$$"
|
|
76
|
+
mv -f "$vendor/.mermaid-$version.min.js.$$" "$target"
|
|
77
|
+
ln -sfn "mermaid-$version.min.js" "$vendor/mermaid.min.js"
|
|
78
|
+
echo "installed $target (sha256 $member_sha)"
|