@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.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/bin/lanes-install.mjs +158 -0
- package/lib/plan.mjs +65 -0
- package/package.json +37 -0
- package/payload/lanes/README.md +152 -0
- package/payload/lanes/VERSION +1 -0
- package/payload/lanes/bin/lanes +1035 -0
- package/payload/lanes/client/ghostty/common.conf +22 -0
- package/payload/lanes/client/ghostty/macos.conf +6 -0
- package/payload/lanes/client/ghostty/omarchy.conf +19 -0
- package/payload/lanes/client/install.sh +16 -0
- package/payload/lanes/client/lanes-launch +315 -0
- package/payload/lanes/client/lanes-upload +147 -0
- package/payload/lanes/client/lanes-window +36 -0
- package/payload/lanes/client/macos-helper.applescript +14 -0
- package/payload/lanes/client/omarchy/discover.sh +56 -0
- package/payload/lanes/client/omarchy/lanes.conf +5 -0
- package/payload/lanes/client/omarchy/lanes.desktop +9 -0
- package/payload/lanes/client/omarchy/lanes.lua +14 -0
- package/payload/lanes/client/ssh.stanza.in +54 -0
- package/payload/lanes/container/env.sh +16 -0
- package/payload/lanes/container/lane +268 -0
- package/payload/lanes/container/lane-run +34 -0
- package/payload/lanes/docs/ARCHITECTURE.md +93 -0
- package/payload/lanes/docs/HOST-GUARD.md +108 -0
- package/payload/lanes/docs/INSTALL.md +192 -0
- package/payload/lanes/docs/RUNBOOK.md +385 -0
- package/payload/lanes/docs/SHARING.md +124 -0
- package/payload/lanes/docs/ZMX-RING0.md +51 -0
- package/payload/lanes/docs/images/desktop.png +0 -0
- package/payload/lanes/docs/images/iphone.png +0 -0
- package/payload/lanes/holders/exec.sh +21 -0
- package/payload/lanes/holders/tmux.sh +111 -0
- package/payload/lanes/holders/zmx.sh +22 -0
- package/payload/lanes/holders.lock +5 -0
- package/payload/lanes/install/enable-phone.sh +43 -0
- package/payload/lanes/install/enable-web.sh +16 -0
- package/payload/lanes/install/host-install.sh +72 -0
- package/payload/lanes/install/services.py +61 -0
- package/payload/lanes/install.sh +603 -0
- package/payload/lanes/libexec/cursor_commit.py +159 -0
- package/payload/lanes/libexec/cursor_sync.py +166 -0
- package/payload/lanes/libexec/guard.py +231 -0
- package/payload/lanes/libexec/lanes_lib.py +610 -0
- package/payload/lanes/libexec/legacy_sessions.py +130 -0
- package/payload/lanes/libexec/session_actions.py +276 -0
- package/payload/lanes/libexec/sidebar +8 -0
- package/payload/lanes/libexec/sidebar.py +872 -0
- package/payload/lanes/libexec/statusd.py +764 -0
- package/payload/lanes/libexec/tmpscan.py +921 -0
- package/payload/lanes/libexec/view.sh +179 -0
- package/payload/lanes/scripts/adopt-sharing-candidate.py +109 -0
- package/payload/lanes/site/README.md +28 -0
- package/payload/lanes/site/beyondwork/known_hosts +1 -0
- package/payload/lanes/site/beyondwork/site.conf +11 -0
- package/payload/lanes/site/site.example.conf +10 -0
- package/payload/lanes/sudo/host-guard.sh +356 -0
- package/payload/lanes/sudo/one-time-root.sh +32 -0
- package/payload/lanes/systemd/lanes-ensure.service +7 -0
- package/payload/lanes/systemd/lanes-ensure.timer +10 -0
- package/payload/lanes/systemd/lanes-gc.service +7 -0
- package/payload/lanes/systemd/lanes-gc.timer +9 -0
- package/payload/lanes/systemd/lanes-guard.service +8 -0
- package/payload/lanes/systemd/lanes-guard.timer +10 -0
- package/payload/lanes/systemd/lanes-mobile.service +23 -0
- package/payload/lanes/systemd/lanes-statusd.service +15 -0
- package/payload/lanes/systemd/lanes-sync.service +10 -0
- package/payload/lanes/systemd/lanes-sync.timer +10 -0
- package/payload/lanes/systemd/lanes-tmpreap.service +11 -0
- package/payload/lanes/systemd/lanes-tmpreap.timer +13 -0
- package/payload/lanes/systemd/lanes-web.service +13 -0
- package/payload/lanes/systemd/lanes.slice +11 -0
- package/payload/lanes/web/Caddyfile +37 -0
- package/payload/lanes/web/README.md +35 -0
- package/payload/lanes/web/icon-192.png +0 -0
- package/payload/lanes/web/icon.svg +4 -0
- package/payload/lanes/web/index.html +30 -0
- package/payload/lanes/web/manifest.webmanifest +15 -0
- package/payload/lanes/web/mobile.css +301 -0
- package/payload/lanes/web/mobile.js +367 -0
- package/payload/lanes/workspaces.example/README.md +27 -0
- package/payload/lanes/workspaces.example/catalog.json +7 -0
- package/payload/lanes/workspaces.example/commit-targets.json +8 -0
- package/payload/lanes/workspaces.example/terminals.example.json +19 -0
- package/payload/lanes/workspaces.example/workspaces.product.json +15 -0
- package/payload/lanes/workspaces.example/workspaces.research.json +16 -0
- package/payload/lanes/zellij/config.kdl +360 -0
- 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
|