native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Document SSH notification transport options

khoi b6fb2c1c 61356be1

+232
+232
docs/ssh-notifications-over-ssh.md
··· 1 + # SSH Notifications Over SSH 2 + 3 + ## Goal 4 + 5 + Show Supacode notifications when a coding agent is running on a remote machine over `ssh`. 6 + 7 + ## Decision 8 + 9 + This is feasible. 10 + 11 + The simplest path is to support remote notifications by emitting terminal notification escape sequences from the remote host and letting the existing local Ghostty -> Supacode notification path handle them. 12 + 13 + Structured remote hook parity with the current local socket-based integration is also feasible, but it is a larger feature and should be treated as a second phase. 14 + 15 + ## Current Supacode Behavior 16 + 17 + ### Local terminal notifications already work 18 + 19 + Supacode already receives terminal desktop notifications from Ghostty: 20 + 21 + - `ThirdParty/ghostty/src/terminal/osc.zig` 22 + - `ThirdParty/ghostty/src/terminal/osc/parsers/osc9.zig` 23 + - `ThirdParty/ghostty/src/terminal/osc/parsers/rxvt_extension.zig` 24 + - `ThirdParty/ghostty/src/Surface.zig` 25 + - `supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift` 26 + - `supacode/Features/Terminal/Models/WorktreeTerminalState.swift` 27 + - `supacode/Features/Terminal/BusinessLogic/WorktreeTerminalManager.swift` 28 + - `supacode/Features/App/Reducer/AppFeature.swift` 29 + - `supacode/Clients/Notifications/SystemNotificationClient.swift` 30 + 31 + The flow is: 32 + 33 + 1. A process inside the terminal emits `OSC 9` or `OSC 777`. 34 + 2. Ghostty parses the escape sequence into a desktop notification action. 35 + 3. `GhosttySurfaceBridge` forwards the title/body into `WorktreeTerminalState`. 36 + 4. `WorktreeTerminalState` stores the in-app notification and emits a terminal event. 37 + 5. `AppFeature` decides whether to show a macOS system notification and/or sound. 38 + 39 + This path is transport-agnostic. If the bytes arrive through a local shell or through a remote `ssh` PTY, the local Ghostty surface sees the same terminal stream. 40 + 41 + ### Local structured coding-agent hooks do not work remotely 42 + 43 + The current Claude/Codex integrations are local-only. 44 + 45 + Each terminal surface injects: 46 + 47 + - `SUPACODE_WORKTREE_ID` 48 + - `SUPACODE_TAB_ID` 49 + - `SUPACODE_SURFACE_ID` 50 + - `SUPACODE_SOCKET_PATH` 51 + 52 + Relevant files: 53 + 54 + - `supacode/Features/Terminal/Models/WorktreeTerminalState.swift` 55 + - `supacode/Features/Settings/BusinessLogic/AgentHookSettingsCommand.swift` 56 + - `supacode/Infrastructure/AgentHookSocketServer.swift` 57 + 58 + The installed hook commands send either busy-state updates or raw JSON payloads to a local Unix domain socket under `/tmp/supacode-<uid>/pid-<pid>`. 59 + 60 + That cannot work from a remote host: 61 + 62 + - the remote process cannot access the local Unix socket 63 + - the remote shell does not automatically inherit the local `SUPACODE_*` environment 64 + - the current hook command shape assumes local socket connectivity 65 + 66 + ## Existing Signals In The Repo 67 + 68 + There is already a local helper for the terminal-notification path: 69 + 70 + - `bins/osc9-notify.sh` 71 + 72 + There are already tests that verify the deduplication behavior between hook notifications and OSC notifications: 73 + 74 + - `supacodeTests/AgentBusyStateTests.swift` 75 + 76 + This means the local app side is already prepared to accept terminal-originated notifications and coalesce them against richer hook notifications. 77 + 78 + ## Recommended Implementation 79 + 80 + ## Phase 1 81 + 82 + Add an SSH-safe remote notification mode that uses terminal escape sequences instead of the local Unix socket. 83 + 84 + ### Why this is the right first step 85 + 86 + - It reuses the existing app-side pipeline. 87 + - It avoids new transport infrastructure. 88 + - It solves the user-visible problem in `SUP-39`. 89 + - It keeps the change narrow and forward-only. 90 + 91 + ### Notification protocol choice 92 + 93 + Use `OSC 777` for agent hooks. 94 + 95 + Why: 96 + 97 + - `OSC 9` only gives a body in the current Ghostty path. 98 + - `OSC 777` supports both title and body. 99 + - Supacode already receives both title and body from Ghostty for desktop notifications. 100 + 101 + `OSC 9` can remain useful for smoke testing and manual scripts, but `OSC 777` is the better hook transport. 102 + 103 + ### Implementation shape 104 + 105 + Add a second hook command builder that emits terminal notifications instead of writing to the local socket. 106 + 107 + The hook command should: 108 + 109 + 1. Read the raw JSON payload from `stdin`. 110 + 2. Extract: 111 + - `title` 112 + - `message` 113 + - `last_assistant_message` 114 + - `hook_event_name` 115 + 3. Choose a title/body: 116 + - title = payload title if present, otherwise agent name 117 + - body = `message` or `last_assistant_message` 118 + 4. Sanitize terminal control characters and delimiters. 119 + 5. Emit `OSC 777`. 120 + 121 + The easiest portable implementation is a small helper script that uses `python3` to parse JSON and print the escape sequence. 122 + 123 + ### Command selection 124 + 125 + The hook installer should generate commands using this rule: 126 + 127 + - if `SUPACODE_SOCKET_PATH` exists, use the current local socket command 128 + - otherwise, if the process is running in an SSH session, use the remote OSC command 129 + 130 + Remote detection should prefer explicit signal over inference: 131 + 132 + - first: `SUPACODE_REMOTE_NOTIFICATIONS=1` 133 + - second: `SSH_CONNECTION` or `SSH_TTY` 134 + 135 + This avoids changing local behavior and keeps the new path opt-in or clearly scoped to remote sessions. 136 + 137 + ### Scope 138 + 139 + Phase 1 should only cover notifications. 140 + 141 + It should not try to preserve: 142 + 143 + - local hook busy-state updates 144 + - tab/surface targeting parity beyond the active remote terminal stream 145 + - remote install automation 146 + 147 + ### Acceptance criteria 148 + 149 + - a remote Claude/Codex hook can emit a Supacode notification over `ssh` 150 + - the local app records the notification in the worktree list 151 + - the local app can show a macOS system notification 152 + - the existing hook-vs-OSC deduplication still works 153 + - local non-SSH hook behavior is unchanged 154 + 155 + ### Tests 156 + 157 + Add tests for: 158 + 159 + - remote helper payload parsing 160 + - `OSC 777` title/body delivery through the existing bridge callback 161 + - command selection logic between local socket and remote OSC modes 162 + - sanitization of control characters 163 + 164 + ## Phase 2 165 + 166 + Add a real remote relay for structured hook parity. 167 + 168 + This is the path if Supacode needs: 169 + 170 + - busy-state rings for remote agents 171 + - richer remote targeting semantics 172 + - remote commands that land in the local socket transport instead of going through terminal escape sequences 173 + - remote installation/bootstrap owned by Supacode 174 + 175 + ### Architecture 176 + 177 + The relay shape should be: 178 + 179 + 1. Supacode starts a local authenticated relay server. 180 + 2. Supacode opens an SSH reverse tunnel back to that local relay. 181 + 3. Supacode installs remote metadata or helper wrappers on the remote host. 182 + 4. Remote hook commands talk to the tunneled relay endpoint. 183 + 5. The local relay forwards the request into the existing local Unix socket or directly into app state. 184 + 185 + ### What this unlocks 186 + 187 + - remote busy-state updates with the existing structured model 188 + - remote notifications without shell JSON parsing in the app-specific hook command 189 + - future remote agent commands beyond notifications 190 + 191 + ### Why this is not phase 1 192 + 193 + - it introduces authentication, bootstrap, lifecycle, and reconnect behavior 194 + - it is a larger surface area than the problem requires 195 + - it should be justified by remote busy-state or broader remote orchestration goals 196 + 197 + ## Risks And Open Questions 198 + 199 + ### tmux and nested terminal layers 200 + 201 + Plain `ssh` PTY delivery should work. Nested layers such as `tmux` may require validation because escape-sequence passthrough behavior depends on terminal configuration. 202 + 203 + This should be treated as a compatibility matrix item for phase 1 validation, not as a blocker to the first implementation. 204 + 205 + ### Remote helper availability 206 + 207 + The simplest JSON parser is `python3`. If remote environments without Python need to be supported, Supacode should ship a tiny helper binary or install a standalone script during remote bootstrap. 208 + 209 + ### Automatic installation on the remote host 210 + 211 + Phase 1 does not require Supacode to edit remote config files automatically. If product requirements demand a one-click remote install, that belongs with the relay/bootstrap work in phase 2. 212 + 213 + ## Proposed Work Split 214 + 215 + ### Issue 1 216 + 217 + Implement SSH-safe remote notifications via `OSC 777`. 218 + 219 + ### Issue 2 220 + 221 + Add a remote relay for structured hook parity and busy-state updates. 222 + 223 + ## Summary 224 + 225 + Supacode can show notifications over `ssh` today if the remote process emits terminal notification escape sequences. 226 + 227 + The missing piece is not the app-side notification pipeline. The missing piece is the remote integration path. 228 + 229 + The recommended order is: 230 + 231 + 1. ship SSH-safe hook notifications over `OSC 777` 232 + 2. add a real remote relay only if remote structured parity is needed