@vibelord/lanes 1.0.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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/bin/lanes-install.mjs +158 -0
  4. package/lib/plan.mjs +65 -0
  5. package/package.json +37 -0
  6. package/payload/lanes/README.md +152 -0
  7. package/payload/lanes/VERSION +1 -0
  8. package/payload/lanes/bin/lanes +1035 -0
  9. package/payload/lanes/client/ghostty/common.conf +22 -0
  10. package/payload/lanes/client/ghostty/macos.conf +6 -0
  11. package/payload/lanes/client/ghostty/omarchy.conf +19 -0
  12. package/payload/lanes/client/install.sh +16 -0
  13. package/payload/lanes/client/lanes-launch +315 -0
  14. package/payload/lanes/client/lanes-upload +147 -0
  15. package/payload/lanes/client/lanes-window +36 -0
  16. package/payload/lanes/client/macos-helper.applescript +14 -0
  17. package/payload/lanes/client/omarchy/discover.sh +56 -0
  18. package/payload/lanes/client/omarchy/lanes.conf +5 -0
  19. package/payload/lanes/client/omarchy/lanes.desktop +9 -0
  20. package/payload/lanes/client/omarchy/lanes.lua +14 -0
  21. package/payload/lanes/client/ssh.stanza.in +54 -0
  22. package/payload/lanes/container/env.sh +16 -0
  23. package/payload/lanes/container/lane +268 -0
  24. package/payload/lanes/container/lane-run +34 -0
  25. package/payload/lanes/docs/ARCHITECTURE.md +93 -0
  26. package/payload/lanes/docs/HOST-GUARD.md +108 -0
  27. package/payload/lanes/docs/INSTALL.md +192 -0
  28. package/payload/lanes/docs/RUNBOOK.md +385 -0
  29. package/payload/lanes/docs/SHARING.md +124 -0
  30. package/payload/lanes/docs/ZMX-RING0.md +51 -0
  31. package/payload/lanes/docs/images/desktop.png +0 -0
  32. package/payload/lanes/docs/images/iphone.png +0 -0
  33. package/payload/lanes/holders/exec.sh +21 -0
  34. package/payload/lanes/holders/tmux.sh +111 -0
  35. package/payload/lanes/holders/zmx.sh +22 -0
  36. package/payload/lanes/holders.lock +5 -0
  37. package/payload/lanes/install/enable-phone.sh +43 -0
  38. package/payload/lanes/install/enable-web.sh +16 -0
  39. package/payload/lanes/install/host-install.sh +72 -0
  40. package/payload/lanes/install/services.py +61 -0
  41. package/payload/lanes/install.sh +603 -0
  42. package/payload/lanes/libexec/cursor_commit.py +159 -0
  43. package/payload/lanes/libexec/cursor_sync.py +166 -0
  44. package/payload/lanes/libexec/guard.py +231 -0
  45. package/payload/lanes/libexec/lanes_lib.py +610 -0
  46. package/payload/lanes/libexec/legacy_sessions.py +130 -0
  47. package/payload/lanes/libexec/session_actions.py +276 -0
  48. package/payload/lanes/libexec/sidebar +8 -0
  49. package/payload/lanes/libexec/sidebar.py +872 -0
  50. package/payload/lanes/libexec/statusd.py +764 -0
  51. package/payload/lanes/libexec/tmpscan.py +921 -0
  52. package/payload/lanes/libexec/view.sh +179 -0
  53. package/payload/lanes/scripts/adopt-sharing-candidate.py +109 -0
  54. package/payload/lanes/site/README.md +28 -0
  55. package/payload/lanes/site/beyondwork/known_hosts +1 -0
  56. package/payload/lanes/site/beyondwork/site.conf +11 -0
  57. package/payload/lanes/site/site.example.conf +10 -0
  58. package/payload/lanes/sudo/host-guard.sh +356 -0
  59. package/payload/lanes/sudo/one-time-root.sh +32 -0
  60. package/payload/lanes/systemd/lanes-ensure.service +7 -0
  61. package/payload/lanes/systemd/lanes-ensure.timer +10 -0
  62. package/payload/lanes/systemd/lanes-gc.service +7 -0
  63. package/payload/lanes/systemd/lanes-gc.timer +9 -0
  64. package/payload/lanes/systemd/lanes-guard.service +8 -0
  65. package/payload/lanes/systemd/lanes-guard.timer +10 -0
  66. package/payload/lanes/systemd/lanes-mobile.service +23 -0
  67. package/payload/lanes/systemd/lanes-statusd.service +15 -0
  68. package/payload/lanes/systemd/lanes-sync.service +10 -0
  69. package/payload/lanes/systemd/lanes-sync.timer +10 -0
  70. package/payload/lanes/systemd/lanes-tmpreap.service +11 -0
  71. package/payload/lanes/systemd/lanes-tmpreap.timer +13 -0
  72. package/payload/lanes/systemd/lanes-web.service +13 -0
  73. package/payload/lanes/systemd/lanes.slice +11 -0
  74. package/payload/lanes/web/Caddyfile +37 -0
  75. package/payload/lanes/web/README.md +35 -0
  76. package/payload/lanes/web/icon-192.png +0 -0
  77. package/payload/lanes/web/icon.svg +4 -0
  78. package/payload/lanes/web/index.html +30 -0
  79. package/payload/lanes/web/manifest.webmanifest +15 -0
  80. package/payload/lanes/web/mobile.css +301 -0
  81. package/payload/lanes/web/mobile.js +367 -0
  82. package/payload/lanes/workspaces.example/README.md +27 -0
  83. package/payload/lanes/workspaces.example/catalog.json +7 -0
  84. package/payload/lanes/workspaces.example/commit-targets.json +8 -0
  85. package/payload/lanes/workspaces.example/terminals.example.json +19 -0
  86. package/payload/lanes/workspaces.example/workspaces.product.json +15 -0
  87. package/payload/lanes/workspaces.example/workspaces.research.json +16 -0
  88. package/payload/lanes/zellij/config.kdl +360 -0
  89. package/payload/manifest.json +31 -0
@@ -0,0 +1,179 @@
1
+ #!/bin/sh
2
+ # Viewport supervisor: shows whichever lane this view's target file names. It never exits on its own:
3
+ # a zellij command pane whose command exits is held ("ENTER to re-run") and can be closed, so the view
4
+ # loops forever and shows a status screen instead of dying.
5
+ # usage: view.sh <device> <workspace> [slot]
6
+ # switch lane: write the lane id to $STATE/views/<device>/<workspace>.<slot>.target, then `kill -USR1` this pid.
7
+ set -u
8
+ device=${1:?device} ws=${2:?workspace} slot=${3:-main}
9
+ mobile=0; [ "${4:-}" != --mobile ] || mobile=1
10
+ case "$device$ws$slot" in *[!A-Za-z0-9_.-]*) echo 'invalid view name'; exit 64 ;; esac
11
+ state=${LANES_STATE:-$HOME/.local/state/lanes}
12
+ repo=$(cd "$(dirname "$0")/.." && pwd)
13
+ dir=$state/views/$device
14
+ mkdir -p "$dir"
15
+ target_file=$dir/$ws.$slot.target
16
+ err_file=$dir/$ws.$slot.err
17
+ echo $$ > "$dir/$ws.$slot.pid"
18
+ export LANES_STATE="$state" LANES_WS="$ws"
19
+
20
+ child=
21
+ switching=0
22
+ on_switch() {
23
+ switching=1
24
+ if [ -n "$child" ]; then kill -TERM "$child" 2>/dev/null || :; fi
25
+ }
26
+ on_stop() {
27
+ trap - HUP TERM
28
+ if [ -n "$child" ]; then kill -TERM "$child" 2>/dev/null || :; fi
29
+ exit 0
30
+ }
31
+ sizing=0
32
+ on_winch() {
33
+ # Only while we are waiting for a usable size; never disturb an attached lane client.
34
+ if [ "$sizing" = 1 ] && [ -n "$child" ]; then kill -TERM "$child" 2>/dev/null || :; fi
35
+ }
36
+ trap on_switch USR1
37
+ trap on_winch WINCH
38
+ trap on_stop HUP TERM
39
+ trap '' INT QUIT # Ctrl-C on a status screen must not kill the view
40
+
41
+ # Run a child asynchronously so traps fire immediately. dash redirects stdin of async commands to
42
+ # /dev/null before applying <&0, so keep the terminal on fd 9 first (proven in coder-launch/supervise.sh).
43
+ run() {
44
+ exec 9<&0
45
+ "$@" <&9 9<&- 2>"$err_file" &
46
+ child=$!
47
+ exec 9<&-
48
+ rc=0
49
+ wait "$child" || rc=$?
50
+ # A trapped signal interrupts wait; make sure the child is really gone before moving on.
51
+ while kill -0 "$child" 2>/dev/null; do wait "$child" 2>/dev/null || :; done
52
+ child=
53
+ return "$rc"
54
+ }
55
+
56
+ reset_terminal() {
57
+ # Leave alt screen, mouse modes, bracketed paste, kitty keyboard; reset SGR; clear screen + scrollback.
58
+ printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[<u\033[0m\033[?25h\033[2J\033[3J\033[H'
59
+ }
60
+
61
+ # status_screen <title> <text> <dim line> [<lead>]: a lead, when given, is the bold headline above the title.
62
+ status_screen() {
63
+ reset_terminal
64
+ if [ -n "${4:-}" ]; then printf '\n \033[1m%s\033[0m\n\n %s\n' "$4" "$1"
65
+ else printf '\n \033[1m%s\033[0m\n' "$1"; fi
66
+ printf '\n %s\n\n \033[2m%s\033[0m\n' "$2" "$3"
67
+ }
68
+
69
+ # Is the socket of the lane just shown still there? Then only this VIEWER ended: a lane lives in its holder's server
70
+ # inside the container and never depends on a viewer. File tests and builtins only — no fork, no tmux, no docker:
71
+ # this runs exactly when the host may be too slow for any of them. Unsure = say nothing.
72
+ lane_socket_exists() {
73
+ case "$holder" in
74
+ tmux)
75
+ # statusd's flag: this server's PROCESS runs although its socket file was deleted (emptied /tmp).
76
+ if grep -q -x -F "$f2/$f3" "$state/flags/sockets-lost" 2>/dev/null; then return 0; fi
77
+ init=; { IFS= read -r init < "$state/containers/$f2.pid"; } 2>/dev/null || :
78
+ case "$init" in ''|*[!0-9]*) return 1 ;; esac
79
+ [ -S "${LANES_PROC_ROOT:-/proc}/$init/root/tmp/tmux-${LANES_CONTAINER_UID:-1000}/$f3" ] ;;
80
+ zmx) [ -S "$HOME/.lz/$f2/s/$f3" ] ;;
81
+ *) return 1 ;;
82
+ esac
83
+ }
84
+
85
+ # One sentence about the host, or nothing, from statusd's one-line heartbeat $STATE/health
86
+ # ("at=<epoch> level=<ok|busy|critical> …": the key order is part of the format, see lanes_lib.health_line).
87
+ # Its age is judged with OUR clock ($1 = now): a statusd that froze cannot report its own freeze.
88
+ health_hint() {
89
+ host_hint= health=
90
+ { IFS= read -r health < "$state/health"; } 2>/dev/null || : # no file, or no final newline: judged below
91
+ at=${health#at=}; at=${at%% *}
92
+ level=${health#* level=}; level=${level%% *}
93
+ case "$at" in ''|*[!0-9]*) return 0 ;; esac
94
+ case "$level" in
95
+ critical) host_hint='The host is under memory pressure: attaching can take a while.' ;;
96
+ busy) host_hint='The host is busy: attaching can take a while.' ;;
97
+ esac
98
+ if [ $(($1 - at)) -gt 30 ]; then host_hint="${host_hint:+$host_hint }The status data is $(($1 - at)) s old."; fi
99
+ }
100
+
101
+ delay=1
102
+ while :; do
103
+ # A background-created zellij session has no real size yet: idle until a client has attached once.
104
+ while [ ! -e "$state/clients/$device.ready" ] || { [ "$mobile" = 1 ] && [ ! -e "$dir/$ws.$slot.shown" ]; }; do
105
+ status_screen "$ws" 'waiting for the first client…' ''
106
+ run sleep 2 || :
107
+ done
108
+
109
+ # zellij sizes a tab only once some client has looked at it; until then the pane has a made-up size
110
+ # (e.g. 14x49). Attaching from such a pane would squeeze the lane, so wait for a sane size first.
111
+ while :; do
112
+ size=$(stty size 2>/dev/null || echo '0 0')
113
+ rows=${size% *} cols=${size#* }
114
+ min_cols=40 min_rows=8
115
+ [ "$mobile" != 1 ] || { min_cols=20; min_rows=4; }
116
+ if [ "${cols:-0}" -ge "$min_cols" ] 2>/dev/null && [ "${rows:-0}" -ge "$min_rows" ] 2>/dev/null; then break; fi
117
+ status_screen "$ws" 'waiting to be shown…' ''
118
+ sizing=1; run sleep 3 || :; sizing=0
119
+ done
120
+
121
+ lane=$(head -n 1 "$target_file" 2>/dev/null || :)
122
+ # Only the first viewport opens the workspace's default lane; extra viewports stay empty until asked.
123
+ if [ -z "$lane" ] && [ "$slot" = main ]; then lane=$(head -n 1 "$state/defaults/$ws" 2>/dev/null || :); fi
124
+ case "$lane" in
125
+ ''|*[!A-Za-z0-9_.-]*)
126
+ if [ "$slot" = main ]; then hint='Pick one in the sidebar (Ctrl-Alt-/ to find a lane).'
127
+ else hint='In the sidebar, move to a lane and press 2 (or 3) to show it here.'; fi
128
+ status_screen "$ws" 'No lane selected.' "$hint"
129
+ switching=0; run sleep 5 || :; continue ;;
130
+ esac
131
+ resolve=$state/resolve/$lane
132
+ if [ ! -r "$resolve" ]; then resolve=$state/resolve.dynamic/$lane; fi
133
+ if [ ! -r "$resolve" ]; then
134
+ status_screen "$lane" 'This lane is no longer in the registry.' 'Pick another lane in the sidebar.'
135
+ switching=0; run sleep 5 || :; continue
136
+ fi
137
+ { IFS= read -r holder; IFS= read -r f2; IFS= read -r f3; IFS= read -r f4; IFS= read -r title; } < "$resolve"
138
+
139
+ reset_terminal
140
+ switching=0
141
+ started=$(date +%s)
142
+ case "$holder" in
143
+ tmux) run "$repo/holders/tmux.sh" "$f2" "$f3" "$f4" || : ;;
144
+ zmx) run "$repo/holders/zmx.sh" "$f2" "$f3" "$f4" || : ;;
145
+ exec) run "$repo/holders/exec.sh" "$f2" "$f3" "$f4" || : ;;
146
+ *) rc=64 ;;
147
+ esac
148
+ if [ "$switching" = 1 ]; then delay=1; continue; fi
149
+
150
+ # The holder's client has just left its own last word on the screen (tmux: "[terminated]", "[lost server]"), which
151
+ # reads like the death of the lane. Replace it AT ONCE — before date / tail below, which are forks and can take
152
+ # seconds on a thrashing host. Not after 64 / 66 / 69: there the holder itself found no such lane or server.
153
+ #
154
+ # The headline says only what `lane_socket_exists` proves — the lane's SERVER answered — because that is
155
+ # all a fork-free check can know. It used to say "THE LANE IS STILL RUNNING", and a lane KILLED on a
156
+ # server that holds other lanes produces exactly this shape: MEASURED (tmux 3.4, default config) the
157
+ # client of a killed session exits 0 with `[exited]` while the server stays up, so a Throttle Studio
158
+ # `Stop` of a lane this viewport was showing headlined that the lane still ran, one second before the
159
+ # holder answered 66 "no such tmux session" (DEF-965). The retry below is what learns the session's fate.
160
+ lead=
161
+ untouched=' — the lane itself is untouched'
162
+ case "$rc" in
163
+ # The holder itself found no such lane or server: nothing may claim the lane is untouched.
164
+ 64|66|69) untouched= ;;
165
+ *) if lane_socket_exists; then
166
+ lead="THIS VIEWER DISCONNECTED — the lane's tmux server is still running"
167
+ status_screen "${title:-$lane}" 'reconnecting…' "$holder · $f2 · $f3 · $f4" "$lead"
168
+ fi ;;
169
+ esac
170
+ ended=$(date +%s)
171
+ if [ $((ended - started)) -ge 30 ]; then delay=1; fi
172
+ reason=$(tail -n 1 "$err_file" 2>/dev/null || :)
173
+ health_hint "$ended"
174
+ status_screen "${title:-$lane}" "${reason:-viewer detached (exit $rc)}${untouched}; retrying in ${delay}s.${host_hint:+
175
+ $host_hint}" "$holder · $f2 · $f3 · $f4" "$lead"
176
+ run sleep "$delay" || :
177
+ delay=$((delay * 2))
178
+ if [ "$delay" -gt 15 ]; then delay=15; fi
179
+ done
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python3
2
+ """Offline adoption of PR #352's deployed schema-37 candidate.
3
+
4
+ Stop the platform first. Connection comes from libpq's PG* environment/service.
5
+ Normal platform startup continues to reject mismatched migration journals.
6
+ """
7
+ import argparse
8
+ import hashlib
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ import subprocess
13
+ import tempfile
14
+
15
+ ROOT = Path(__file__).resolve().parents[2]
16
+ MIGRATIONS = ROOT / "crates/throttle-platform/migrations"
17
+ LOCK_KEY = 0x5448524F5F4D4752
18
+ CANDIDATE_DIGEST = "625d4c9c7807116fe5cf5aca2c52bb6efacfc4755b55b24836cebaf60267865c"
19
+
20
+
21
+ def sha256(path):
22
+ digest = hashlib.sha256()
23
+ with path.open("rb") as stream:
24
+ for block in iter(lambda: stream.read(1024 * 1024), b""):
25
+ digest.update(block)
26
+ return digest.hexdigest()
27
+
28
+
29
+ def chain():
30
+ files = sorted(MIGRATIONS.glob("[0-9][0-9][0-9][0-9]_*.sql"))
31
+ files = [p for p in files if int(p.name[:4]) <= 39]
32
+ if [int(p.name[:4]) for p in files] != list(range(1, 40)):
33
+ raise RuntimeError("expected the canonical migration chain 1..39")
34
+ if sha256(files[38]) != CANDIDATE_DIGEST:
35
+ raise RuntimeError("sharing SQL differs from the deployed candidate; refusing adoption")
36
+ return files
37
+
38
+
39
+ def adoption_sql():
40
+ files = chain()
41
+ rows = ",\n".join(f"({i}, decode('{sha256(p)}', 'hex'))" for i, p in enumerate(files, 1))
42
+ return f"""BEGIN;
43
+ SET LOCAL lock_timeout = '5s';
44
+ SET LOCAL statement_timeout = '120s';
45
+ DO $guard$ BEGIN
46
+ IF NOT pg_try_advisory_xact_lock({LOCK_KEY}) THEN
47
+ RAISE EXCEPTION 'platform migration lock is held; stop the platform first';
48
+ END IF;
49
+ END $guard$;
50
+ LOCK public.schema_migrations IN ACCESS EXCLUSIVE MODE;
51
+ CREATE TEMP TABLE expected_chain(version integer PRIMARY KEY, sha256 bytea) ON COMMIT DROP;
52
+ INSERT INTO expected_chain VALUES {rows};
53
+ DO $guard$ BEGIN
54
+ IF (SELECT count(*) FROM public.schema_migrations) <> 37 OR
55
+ EXISTS (SELECT 1 FROM public.schema_migrations j
56
+ FULL JOIN expected_chain e ON e.version = j.version AND e.version <= 36
57
+ WHERE (e.version <= 36 OR j.version <= 36) AND
58
+ (j.version IS NULL OR e.version IS NULL OR j.sha256 <> e.sha256)) OR
59
+ NOT EXISTS (SELECT 1 FROM public.schema_migrations
60
+ WHERE version = 37 AND sha256 = decode('{CANDIDATE_DIGEST}', 'hex')) THEN
61
+ RAISE EXCEPTION 'not the exact PR 352 candidate journal; no changes made';
62
+ END IF;
63
+ IF to_regclass('studio.lane_shares') IS NULL OR
64
+ to_regclass('studio.lane_sharing_policy') IS NULL OR
65
+ to_regclass('studio.hosts') IS NOT NULL THEN
66
+ RAISE EXCEPTION 'candidate catalog differs; no changes made';
67
+ END IF;
68
+ END $guard$;
69
+ -- Keep sharing data, exact SQL digest and original applied_at.
70
+ UPDATE public.schema_migrations SET version = 39 WHERE version = 37;
71
+ {files[36].read_text()}
72
+ {files[37].read_text()}
73
+ INSERT INTO public.schema_migrations(version, sha256)
74
+ SELECT version, sha256 FROM expected_chain WHERE version IN (37,38);
75
+ DO $guard$ BEGIN
76
+ IF EXISTS (SELECT 1 FROM expected_chain e FULL JOIN public.schema_migrations j USING (version)
77
+ WHERE e.version IS NULL OR j.version IS NULL OR e.sha256 <> j.sha256) THEN
78
+ RAISE EXCEPTION 'canonical journal verification failed';
79
+ END IF;
80
+ END $guard$;
81
+ COMMIT;
82
+ """
83
+
84
+
85
+ def main():
86
+ parser = argparse.ArgumentParser(description=__doc__)
87
+ parser.add_argument("--apply", action="store_true", required=True)
88
+ parser.add_argument("--platform-stopped", action="store_true", required=True)
89
+ parser.add_argument("--backup-dir", type=Path, required=True)
90
+ args = parser.parse_args()
91
+ sql = adoption_sql()
92
+ os.umask(0o077)
93
+ args.backup_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
94
+ work = Path(tempfile.mkdtemp(prefix="sharing-candidate-", dir=args.backup_dir))
95
+ backup = work / "before.dump"
96
+ subprocess.run(["pg_dump", "--no-password", "--format=custom", "--file", str(backup)], check=True)
97
+ subprocess.run(["pg_restore", "--list", str(backup)], check=True, stdout=subprocess.DEVNULL)
98
+ receipt = {"backup": str(backup.resolve()), "backup_sha256": sha256(backup), "status": "backed_up"}
99
+ receipt_path = work / "receipt.json"
100
+ receipt_path.write_text(json.dumps(receipt, indent=2) + "\n")
101
+ result = subprocess.run(["psql", "-X", "--no-password", "-q", "-v", "ON_ERROR_STOP=1"], input=sql, text=True)
102
+ receipt["status"] = "canonical_schema_39" if result.returncode == 0 else "refused_or_rolled_back"
103
+ receipt_path.write_text(json.dumps(receipt, indent=2) + "\n")
104
+ print(json.dumps({"receipt": str(receipt_path.resolve()), "status": receipt["status"]}))
105
+ raise SystemExit(result.returncode)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
@@ -0,0 +1,28 @@
1
+ # Choose a deployment
2
+
3
+ The software has no default server. A profile is plain `KEY=VALUE` data and the server's public host key;
4
+ it contains no client private key or login token.
5
+
6
+ For your own host, copy `site.example.conf` to `site.conf`, fill in its address/account/host-key alias, and
7
+ put a verified `known_hosts` file beside it. These two local files are Git-ignored. Then run `./install.sh`
8
+ from `lanes/`. You can keep the pair elsewhere and use `./install.sh --site /path/to/site.conf` instead.
9
+
10
+ A known_hosts record has the form:
11
+
12
+ ```text
13
+ workspace-server ssh-ed25519 AAAA...verified-public-host-key...
14
+ ```
15
+
16
+ Use the alias from `LANES_SERVER_ALIAS`. Get the key from the administrator or an already trusted server
17
+ session (`/etc/ssh/ssh_host_ed25519_key.pub`); a network key scan alone does not authenticate it.
18
+
19
+ For the existing Beyond Work server, select `./install.sh --site site/beyondwork/site.conf` explicitly.
20
+ That directory preserves the existing deployment address and pinned public key as one named profile.
21
+
22
+ The installer saves a client-owned copy of the profile and key in `~/.config/lanes/client-site.conf` and
23
+ `client-known-hosts`. Updates and `--check` reuse them. To switch deployments, pass `--site` again.
24
+ SSH refuses changed host keys until the administrator's new fingerprint has been verified.
25
+
26
+ Server-side board catalogs belong in `~/.config/lanes/workspaces/`; real workspaces, credentials and generated
27
+ state are not part of the software package. Optional server facts such as a public SSH fallback are served from
28
+ `~/.config/lanes/site.conf` by `lanes client-config`. See [installation](../docs/INSTALL.md).
@@ -0,0 +1 @@
1
+ ceobox ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEq1SYj7AHFMLna0FIaIluqXDOAjJKg5RTXcd9OzqfHw
@@ -0,0 +1,11 @@
1
+ # Site file: the workspace server this package connects to. Plain KEY=VALUE (read, never executed).
2
+ # Only what a client needs BEFORE its first connection lives here. Everything else (for example the public
3
+ # fallback address) is fetched from the server once the client is authorized: lanes client-config
4
+ LANES_SITE_SCHEMA=1
5
+ LANES_SITE=beyondwork
6
+ # known_hosts alias the server's host key is pinned under (site/known_hosts)
7
+ LANES_SERVER_ALIAS=ceobox
8
+ # Tailscale address of the server (the address is only meaningful inside the tailnet) and its MagicDNS name
9
+ LANES_SERVER_HOST=100.91.241.52
10
+ LANES_SERVER_NAME=ceobox.tail70a9fd.ts.net
11
+ LANES_SERVER_USER=dev
@@ -0,0 +1,10 @@
1
+ # Copy to site/site.conf, edit, then run ./install.sh.
2
+ # Plain data, not a shell script. Put the matching public host key in site/known_hosts.
3
+ LANES_SITE_SCHEMA=1
4
+ LANES_SITE=my-team
5
+ LANES_SERVER_ALIAS=workspace-server
6
+ LANES_SERVER_HOST=workspace-server.example.ts.net
7
+ LANES_SERVER_NAME=workspace-server.example.ts.net
8
+ LANES_SERVER_USER=workspace
9
+ # Optional public SSH fallback (use only if your server exposes SSH there):
10
+ # LANES_SERVER_PUBLIC=ssh.example.com