@lawaty/lacode 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -213,17 +213,15 @@ Three things differ, and all three are worth reading rather than assuming:
213
213
  published package. They drive one person's SSH topology, so each is on the side
214
214
  it belongs to:
215
215
 
216
- | Script | Runs on | Why it is Linux-only |
217
- | --- | --- | --- |
218
- | `bin/oc`, `bin/oc-notify` | the **remote host** that runs opencode | Server-side. They are pushed to `~/bin/` on each host by `bin/oc-sync` and notify *your desktop* over the tunnel. |
219
- | `bin/oc-notify-receiver` | your **desktop** | Delivers through `notify-send` (libnotify), and the persistent forward that reaches it is a systemd user unit. |
220
- | `bin/oc-sync` | your **desktop** | Installs those systemd user units and runs `loginctl enable-linger`. |
221
-
222
- None of this is a limitation of the package: a macOS client installs the plugin
223
- and uses the `lacode` CLI, and both are portable. The scripts above are the
224
- author's personal remote-tunnel tooling. `bin/oc-sync` refuses to run where it
225
- cannot work and says which `lacode` subcommand to use instead, rather than
226
- failing somewhere less obvious.
216
+ - **Remote hosts are plain npm clients.** A remote host installs the plugin the
217
+ same way (`opencode plugin @lawaty/lacode -g`); there is nothing to rsync. The
218
+ notification hop is a direct ssh (`ControlMaster` + `ControlPersist`) set up
219
+ once with `lacode tunnel setup` — no reverse forward, no listening port, no
220
+ systemd unit. See [Remote hosts](#remote-hosts) and docs/TUNNEL.md.
221
+ - The repo still carries the author's personal, Linux-only tooling from the older
222
+ reverse-forward setup (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`,
223
+ `bin/oc-sync`). None of it is in the published package, and none of it is needed
224
+ for the npm-client path above.
227
225
 
228
226
  **The test suite runs on macOS.** It is written to pass on a Mac as well as on
229
227
  Linux, so "it works on a Mac" is a supported claim rather than an intention. The
@@ -233,7 +231,7 @@ or no GNU `timeout` — rather than passing silently or failing confusingly.
233
231
 
234
232
  ### The `lacode` CLI
235
233
 
236
- The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`) — see
234
+ The package also ships the `lacode` CLI (`notify`, `forward`, `doctor`, `receive`, `tunnel setup`) — see
237
235
  [Remote hosts](#remote-hosts). The plugin install does **not** put it on your
238
236
  `PATH`; run it from the package's `node_modules/.bin`, or install just the CLI
239
237
  globally with `npm i -g @lawaty/lacode` if you want the bare command. (That
package/docs/TUNNEL.md CHANGED
@@ -9,31 +9,23 @@ gets it back.
9
9
 
10
10
  ```
11
11
  remote host your desktop
12
- ┌──────────────────┐ ┌────────────────────────┐
13
- │ opencode session │ │ oc-notify-receiver │
14
- │ │ │ ssh (reverse forward) │ (forced command, │
15
- │ ▼ │ ───────────────────────► │ restrict) │
16
- │ bin/lacode │ ONE argument: │ │ │
17
- │ notify │ title\nmessage\n │ ▼ │
18
- │ │ urgency\nicon │ notify-send │
12
+ ┌──────────────────┐ direct ssh ┌────────────────────────┐
13
+ │ opencode session │ (ControlPersist) │ lacode receive │
14
+ │ │ │ ───────────────────────► │ (authorized_keys │
15
+ │ ▼ │ ONE argument: │ forced command) │
16
+ │ lacode notify │ title\nmessage\n │ │ │
17
+ │ │ urgency\nicon │ ▼ │
18
+ │ │ │ notify-send / │
19
+ │ │ │ osascript │
19
20
  └──────────────────┘ └────────────────────────┘
20
21
  ```
21
22
 
22
- **Direct ssh (the default).** Set `OC_LOCAL_HOST` and the hop is a plain `ssh`
23
- to the desktop with `ControlMaster=auto` + `ControlPersist`, so one warm
24
- connection carries every notification. There is no reverse forward, no listening
25
- port, no systemd unit and nothing to sync: the plugin owns the transport. The
26
- desktop needs one `authorized_keys` line, which `lacode tunnel setup` prints. The
27
- reverse-forward description below is the legacy path, used only when
28
- `OC_LOCAL_HOST` is unset.
29
-
30
- **Reverse forward (legacy).** Two things are load-bearing here.
31
-
32
- **The reverse forward.** The remote opens an SSH connection to *your* machine
33
- and asks sshd on the desktop to listen on a port. A notification is then an ssh
34
- call to `localhost:<port>` with a dedicated key that is authorized to run
35
- exactly one command. It is not a login, it is not a shell, and it does not
36
- depend on which terminal connected first.
23
+ **Direct ssh, one warm connection.** The hop is a plain `ssh` to the desktop with
24
+ `ControlMaster=auto` + `ControlPersist`, so the first notification opens the
25
+ connection and every later one rides it. There is no reverse forward, no
26
+ listening port, no systemd unit and nothing to sync: the plugin owns the
27
+ transport, and it exists only while opencode runs — which is the only time a
28
+ notification matters.
37
29
 
38
30
  **One argument, not four.** ssh concatenates everything after the host into a
39
31
  single command string, and a forced command starts with *no* positional
@@ -51,17 +43,46 @@ the free-text fields before assembling or reading it, and `urgency` and `icon`
51
43
  come from whitelists. No field can contain the delimiter, so a message full of
52
44
  spaces, quotes or `*` survives intact.
53
45
 
46
+ ## Setting it up
47
+
48
+ Once, on the **remote host** (where opencode runs):
49
+
50
+ ```bash
51
+ lacode tunnel setup
52
+ ```
53
+
54
+ It generates the dedicated key (`~/.ssh/lacode_notify` by default) and prints the
55
+ environment to export there, and the single `authorized_keys` line to add on the
56
+ desktop:
57
+
58
+ ```
59
+ OC_LOCAL_USER=<desktop user>
60
+ OC_LOCAL_HOST=<desktop hostname>
61
+ OC_NOTIFY_KEY=~/.ssh/lacode_notify
62
+ ```
63
+
64
+ On the **desktop**, add the printed line to `~/.ssh/authorized_keys`:
65
+
66
+ ```
67
+ restrict,command="lacode receive" ssh-ed25519 AAAA… lacode-notify
68
+ ```
69
+
70
+ `restrict` does the security work: no shell, no PTY, no agent forwarding, no port
71
+ forwarding. The key can do exactly one thing — hand one notification to
72
+ `lacode receive` — and nothing else. Use the absolute path to `lacode` if it is
73
+ not on the forced command's `PATH`.
74
+
54
75
  ## Sending
55
76
 
56
77
  The CLI picks the transport by where it is running — `SSH_CLIENT` or `SSH_TTY`
57
78
  means remote, so it forwards; otherwise it delivers locally through whichever
58
- desktop notifier this platform has: `notify-send` everywhere else, `osascript` on
59
- macOS, with `terminal-notifier` as the macOS fallback when `osascript` is missing.
79
+ desktop notifier this platform has: `notify-send` on Linux, `osascript` on macOS,
80
+ with `terminal-notifier` as the macOS fallback when `osascript` is missing.
60
81
 
61
82
  ```bash
62
- lacode notify "build finished" "lapode: 4 files changed" normal dialog-information
83
+ lacode notify "build finished" "4 files changed" normal dialog-information
63
84
  lacode notify "question" "which database should I use?" critical dialog-warning
64
- lacode forward web-01 -- systemctl --user status oc-tunnel
85
+ lacode forward web-01 -- systemctl --user status something
65
86
  lacode doctor
66
87
  ```
67
88
 
@@ -76,108 +97,38 @@ handler, never spliced into the script source — a session title is not trusted
76
97
  input. `lacode doctor` prints the channel that would actually run, so it is
77
98
  useful on a Mac rather than reporting only whether a Linux binary is installed.
78
99
 
79
- `lacode doctor` prints what the environment resolves to and never gates on it.
100
+ ## The desktop side: `lacode receive`
101
+
102
+ `lacode receive` is the forced command. It reads the newline payload from
103
+ `$SSH_ORIGINAL_COMMAND`, sanitizes each field again at the trust boundary, and
104
+ delivers locally through the platform channel — `notify-send` on Linux,
105
+ `osascript`/`terminal-notifier` on macOS. Because it delegates to the same local
106
+ channel as a local send, **the desktop half is not Linux-only**: a Mac desktop
107
+ receives notifications too.
108
+
109
+ It never forwards: `SSH_CLIENT` is set for a forced command as well, so it calls
110
+ the local channel directly rather than `notify()` (which would loop back).
80
111
 
81
112
  ## Configuration
82
113
 
83
114
  | Variable | Default | Meaning |
84
115
  | --- | --- | --- |
85
116
  | `OC_LOCAL_USER` | *(unset)* | The desktop user the hop authenticates as. **Required for the remote hop.** |
86
- | `OC_LOCAL_HOST` | *(unset)* | Desktop hostname for the **direct ssh** hop. Set it and the reverse forward/port/systemd unit are not used at all. |
87
- | `OC_TUNNEL_PORT` | `10022` | Reverse-forward listener port on the desktop. Give each remote host its own. |
117
+ | `OC_LOCAL_HOST` | *(unset)* | Desktop hostname for the direct ssh hop. The default; set it and no reverse forward or port is used. |
88
118
  | `OC_NOTIFY_KEY` | `~/.ssh/lacode_notify` | The dedicated key. Never a personal key. |
89
119
  | `OC_CONNECT_TIMEOUT_MS` | `5000` | Ceiling on the hop. |
90
120
  | `OC_SSH_TIMEOUT_MS` | `30000` | Ceiling on `forward`. |
121
+ | `OC_TUNNEL_PORT` | `10022` | **Legacy** reverse-forward listener port. Used only when `OC_LOCAL_HOST` is unset. |
91
122
 
92
123
  `OC_LOCAL_USER` is read rather than probed on purpose. `id -un` on a remote
93
124
  account frequently returns `root`, and a dbus path built from that reaches
94
125
  nobody's desktop. Guessing the desktop user is worse than failing.
95
126
 
96
- ## Setting the tunnel up on the desktop
97
-
98
- The receiver runs on the machine with the desktop, as an `authorized_keys`
99
- forced command restricted to that one script. `$HOME` must be the expanded
100
- absolute path — a `~` is not expanded there, and an unexpanded one produces an
101
- `authorized_keys` entry that names a file that does not exist:
102
-
103
- ```
104
- restrict,command="/home/yourname/.local/bin/lacode-notify-receiver" ssh-ed25519 AAAA… oc-notify tunnel auth
105
- ```
106
-
107
- `restrict` is doing the security work: no shell, no PTY, no agent forwarding, no
108
- port forwarding. The key can do exactly one thing and nothing else.
109
-
110
- The receiver must satisfy this contract:
111
-
112
- 1. read the payload from **`$SSH_ORIGINAL_COMMAND`**, never from `$1`..`$4`;
113
- 2. **never** `eval` it, never hand it to `sh -c`, never splice it into an
114
- argument list — it is attacker-controlled, because it is whatever the remote
115
- user typed;
116
- 3. split it on newlines into title / message / urgency / icon, each with a
117
- default — **using only constructs bash 3.2 has**, not just bash 4+;
118
- 4. whitelist `urgency` against `low|normal|critical` and `icon` against the six
119
- supported names, both of which become command-line flags;
120
- 5. resolve `DISPLAY` / `DBUS_SESSION_BUS_ADDRESS` for the session it is running
121
- in, then `exec notify-send "$TITLE" "$MSG" --urgency=… --icon=…`.
122
-
123
- Point 3 is not a detail. `mapfile`/`readarray` are bash 4.0+, and where they are
124
- missing — macOS ships bash 3.2, as do some minimal Linux images — the array is
125
- simply never built, every field falls back to its default, and the desktop gets
126
- a notification titled "LaCode" with an empty body. No error is printed anywhere
127
- that anyone will read, because the receiver's stderr goes to whoever happened to
128
- open the ssh connection. A `while IFS= read -r` loop is equivalent and works
129
- everywhere; `bin/oc-notify-receiver` uses one.
130
-
131
- ### This half of the hop is Linux
132
-
133
- The receiver delivers through `notify-send`, and the forward that reaches it is
134
- a systemd `--user` unit (below). Both are Linux, so **the desktop half of this
135
- tunnel does not exist on macOS**, and that is by design rather than a gap.
136
-
137
- On a Mac you do not need it: `lacode notify` run locally picks `osascript` (or
138
- `terminal-notifier`) and shows the notification itself, with no tunnel, no
139
- `authorized_keys` entry and no systemd unit involved. The tunnel exists for the
140
- case opencode runs *somewhere else*, and a Mac is not going to be running it.
141
-
142
- If you point the receiver at a machine with no `notify-send` anyway, it exits
143
- non-zero and says so, naming `lacode notify` as the path that does work there.
144
- `bin/oc-sync` refuses the same machine for the same reason, and names each
145
- missing prerequisite instead of failing somewhere less obvious.
146
-
147
- > **Note.** The published package ships the built CLI (`dist/lacode.js`, linked
148
- > as `lacode`) and the tunnel behind it (`dist/tunnel.js`, importable as
149
- > `@lawaty/lacode/tunnel`), not a receiver script. The receiver is the
150
- > desktop-side half and is yours to install — keep it out of your shell's
151
- > `$PATH` reach and pinned in `authorized_keys`.
152
-
153
- If the two halves disagree you get a blank popup, which points nowhere. A
154
- receiver that sees a payload with no newline in it should say so on stderr
155
- rather than silently defaulting everything.
156
-
157
- ## Holding the forward open
158
-
159
- The tunnel has to exist whenever a notification is sent. Anything that survives
160
- your logout works: a `--user` systemd unit is the usual choice, one per remote
161
- host, each with its own port.
162
-
163
- ```ini
164
- # ~/.config/systemd/user/oc-tunnel@<host>.service
165
- [Service]
166
- ExecStart=/usr/bin/ssh -NT -R 10022:localhost -o ClearAllForwardings=yes -o ExitOnForwardFailure=yes \
167
- -i %h/.ssh/lacode_notify you@localhost
168
- ```
169
-
170
- Then `systemctl --user enable --now oc-tunnel@web-01`.
171
-
172
- Two details that matter:
173
-
174
- - **`-o ExitOnForwardFailure=yes`** — without it, ssh stays up with no forward
175
- and the notifications fail silently.
176
- - **`-o ClearAllForwardings=yes`** on every *other* ssh call that touches that
177
- host, including rsync's `-e`. A `RemoteForward` in `~/.ssh/config` competes
178
- for the same port, and a half-claimed listener starves the one session that
179
- needed it.
127
+ ## Legacy: the reverse forward
180
128
 
181
- Do not hold the forward open with a `RemoteForward` line in `~/.ssh/config`. It
182
- is per-connection: the port exists only while some terminal is open, and it is
183
- claimed by whichever terminal connects first.
129
+ The original design opened a reverse forward (`ssh -R`) held by a `systemd --user`
130
+ unit on the desktop, and delivered to a `notify-send`-only bash receiver
131
+ (`bin/oc-notify-receiver`). It is kept only for hosts that still set
132
+ `OC_TUNNEL_PORT` and leave `OC_LOCAL_HOST` unset: `sendRemote` falls back to an
133
+ ssh call to `localhost:<port>`. New setups should use the direct ssh above. See
134
+ the git history for the old systemd unit shape.
@@ -258,7 +258,7 @@ does not.
258
258
  - [ ] Given a body containing `"`, `'`, `;`, `$(…)` or backticks, When the macOS send runs, Then it reaches `display notification` byte-for-byte as data and nothing is executed.
259
259
  - [ ] Given a machine has no notifier for its platform, When a notification is sent, Then the result is a failed delivery with a reason and no exception, and `lacode doctor` names the channel that would run — or reports that there is none — instead of reporting a binary that is not used there.
260
260
  - [ ] Given the CLI runs on macOS, When the send does not complete, Then it is still bounded by a timeout, exactly as on Linux.
261
- - [ ] Given the repository's own shell scripts, When each is classified, Then the doc says which side of the hop it runs on — `bin/oc` and `bin/oc-notify` on the remote host that runs opencode, `bin/oc-notify-receiver` and `bin/oc-sync` on the desktop — and says plainly why the desktop pair is Linux-only (a `notify-send` channel behind a systemd user unit), so a Mac user is not sent to debug an ssh script that was never meant to run on their machine.
261
+ - [ ] Given the notification hop, When it is documented, Then the doc leads with the shipped path — the npm-installed plugin, a direct ssh hop (`ControlMaster`) set up by `lacode tunnel setup`, and the portable `lacode receive` forced command that delivers through the platform channel (`notify-send` or `osascript`) — and marks the older reverse-forward scripts (`bin/oc`, `bin/oc-notify`, `bin/oc-notify-receiver`, `bin/oc-sync`) as the author's legacy tooling, so a Mac user is not sent to debug a script that was never meant to run on their machine.
262
262
  - [ ] Given a machine without GNU `timeout` or with bash older than 4.0, When `bin/oc-sync` runs, Then it refuses **before** touching anything, names each missing prerequisite, and names the `lacode` subcommand to use instead. It must never report a misleading cause: the host list is read with `mapfile`, so without it the run used to die at "no hosts configured" — a confident wrong answer that sends the reader to edit `.env`.
263
263
  - [ ] Given `bin/oc-notify-receiver` runs where `notify-send` does not exist, When it is invoked, Then it exits non-zero and names both the missing channel and the portable path, because its stderr reaches whoever opened the ssh connection and nobody else, and a bare "command not found" reads as a desktop that quietly stopped notifying.
264
264
  - [ ] Given the payload is parsed, When the receiver splits `$SSH_ORIGINAL_COMMAND`, Then it splits on newlines only using constructs that exist in bash 3.2 as well as bash 5, and the suite **proves the split by running the receiver** rather than by finding the word `mapfile` in the file. Under bash 3.2 — the version macOS ships — a bash-4-only builtin left every field at its default, so the popup said "LaCode" with an empty body and no error anywhere: the same silent blank notification the argv bug caused once, reached again through a different door.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawaty/lacode",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Ultimate opencode setup for Go users — cost-controlled pooled subagents, hang recovery, and notifications for opencode.",
5
5
  "license": "MIT",
6
6
  "type": "module",