@cluesmith/codev 3.3.3 → 3.3.4

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 (176) hide show
  1. package/dashboard-dist/assets/{index-CUIjS_Vb.js → index-DZf6pNPE.js} +2 -2
  2. package/dashboard-dist/assets/{index-CUIjS_Vb.js.map → index-DZf6pNPE.js.map} +1 -1
  3. package/dashboard-dist/index.html +1 -1
  4. package/dist/agent-farm/cli.d.ts.map +1 -1
  5. package/dist/agent-farm/cli.js +48 -0
  6. package/dist/agent-farm/cli.js.map +1 -1
  7. package/dist/agent-farm/commands/ide.d.ts +20 -0
  8. package/dist/agent-farm/commands/ide.d.ts.map +1 -0
  9. package/dist/agent-farm/commands/ide.js +138 -0
  10. package/dist/agent-farm/commands/ide.js.map +1 -0
  11. package/dist/agent-farm/commands/inbox.d.ts.map +1 -1
  12. package/dist/agent-farm/commands/inbox.js +15 -3
  13. package/dist/agent-farm/commands/inbox.js.map +1 -1
  14. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  15. package/dist/agent-farm/commands/send.js +16 -1
  16. package/dist/agent-farm/commands/send.js.map +1 -1
  17. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  18. package/dist/agent-farm/commands/spawn-worktree.js +16 -7
  19. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  20. package/dist/agent-farm/commands/tower.d.ts +21 -9
  21. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  22. package/dist/agent-farm/commands/tower.js +90 -30
  23. package/dist/agent-farm/commands/tower.js.map +1 -1
  24. package/dist/agent-farm/db/tower-owner.d.ts +168 -0
  25. package/dist/agent-farm/db/tower-owner.d.ts.map +1 -0
  26. package/dist/agent-farm/db/tower-owner.js +369 -0
  27. package/dist/agent-farm/db/tower-owner.js.map +1 -0
  28. package/dist/agent-farm/db/types.d.ts +30 -5
  29. package/dist/agent-farm/db/types.d.ts.map +1 -1
  30. package/dist/agent-farm/db/types.js.map +1 -1
  31. package/dist/agent-farm/lib/ide-record.d.ts +65 -0
  32. package/dist/agent-farm/lib/ide-record.d.ts.map +1 -0
  33. package/dist/agent-farm/lib/ide-record.js +118 -0
  34. package/dist/agent-farm/lib/ide-record.js.map +1 -0
  35. package/dist/agent-farm/lib/tunnel-client.d.ts +22 -0
  36. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  37. package/dist/agent-farm/lib/tunnel-client.js +53 -5
  38. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  39. package/dist/agent-farm/servers/gate-profiles.d.ts +23 -2
  40. package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -1
  41. package/dist/agent-farm/servers/gate-profiles.js +43 -2
  42. package/dist/agent-farm/servers/gate-profiles.js.map +1 -1
  43. package/dist/agent-farm/servers/ide-forward.d.ts +62 -0
  44. package/dist/agent-farm/servers/ide-forward.d.ts.map +1 -0
  45. package/dist/agent-farm/servers/ide-forward.js +274 -0
  46. package/dist/agent-farm/servers/ide-forward.js.map +1 -0
  47. package/dist/agent-farm/servers/ide-server.d.ts +68 -0
  48. package/dist/agent-farm/servers/ide-server.d.ts.map +1 -0
  49. package/dist/agent-farm/servers/ide-server.js +281 -0
  50. package/dist/agent-farm/servers/ide-server.js.map +1 -0
  51. package/dist/agent-farm/servers/mailbox-delivery.d.ts +213 -4
  52. package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
  53. package/dist/agent-farm/servers/mailbox-delivery.js +489 -68
  54. package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
  55. package/dist/agent-farm/servers/mailbox-wiring.d.ts +18 -4
  56. package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
  57. package/dist/agent-farm/servers/mailbox-wiring.js +94 -21
  58. package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
  59. package/dist/agent-farm/servers/message-write.d.ts +138 -11
  60. package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
  61. package/dist/agent-farm/servers/message-write.js +144 -35
  62. package/dist/agent-farm/servers/message-write.js.map +1 -1
  63. package/dist/agent-farm/servers/overview.d.ts +28 -12
  64. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  65. package/dist/agent-farm/servers/overview.js +130 -79
  66. package/dist/agent-farm/servers/overview.js.map +1 -1
  67. package/dist/agent-farm/servers/render-gate.d.ts +53 -0
  68. package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
  69. package/dist/agent-farm/servers/render-gate.js +135 -24
  70. package/dist/agent-farm/servers/render-gate.js.map +1 -1
  71. package/dist/agent-farm/servers/session-submit.d.ts +7 -4
  72. package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
  73. package/dist/agent-farm/servers/session-submit.js +7 -4
  74. package/dist/agent-farm/servers/session-submit.js.map +1 -1
  75. package/dist/agent-farm/servers/tower-cron.d.ts.map +1 -1
  76. package/dist/agent-farm/servers/tower-cron.js +4 -1
  77. package/dist/agent-farm/servers/tower-cron.js.map +1 -1
  78. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  79. package/dist/agent-farm/servers/tower-instances.js +6 -7
  80. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  81. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  82. package/dist/agent-farm/servers/tower-routes.js +53 -13
  83. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  84. package/dist/agent-farm/servers/tower-server.js +64 -7
  85. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  86. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  87. package/dist/agent-farm/servers/tower-terminals.js +53 -8
  88. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  89. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  90. package/dist/agent-farm/servers/tower-websocket.js +18 -1
  91. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  92. package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
  93. package/dist/agent-farm/utils/message-format.js +5 -6
  94. package/dist/agent-farm/utils/message-format.js.map +1 -1
  95. package/dist/agent-farm/utils/port.d.ts +22 -0
  96. package/dist/agent-farm/utils/port.d.ts.map +1 -0
  97. package/dist/agent-farm/utils/port.js +35 -0
  98. package/dist/agent-farm/utils/port.js.map +1 -0
  99. package/dist/agent-farm/utils/server-utils.d.ts +26 -0
  100. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  101. package/dist/agent-farm/utils/server-utils.js +36 -0
  102. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  103. package/dist/cli.js +2 -2
  104. package/dist/cli.js.map +1 -1
  105. package/dist/commands/consult/index.d.ts.map +1 -1
  106. package/dist/commands/consult/index.js +61 -1
  107. package/dist/commands/consult/index.js.map +1 -1
  108. package/dist/commands/consult/tree-tripwire.d.ts +177 -0
  109. package/dist/commands/consult/tree-tripwire.d.ts.map +1 -0
  110. package/dist/commands/consult/tree-tripwire.js +356 -0
  111. package/dist/commands/consult/tree-tripwire.js.map +1 -0
  112. package/dist/commands/doctor.d.ts +69 -0
  113. package/dist/commands/doctor.d.ts.map +1 -1
  114. package/dist/commands/doctor.js +163 -0
  115. package/dist/commands/doctor.js.map +1 -1
  116. package/dist/commands/generate-image.d.ts +5 -0
  117. package/dist/commands/generate-image.d.ts.map +1 -1
  118. package/dist/commands/generate-image.js +225 -4
  119. package/dist/commands/generate-image.js.map +1 -1
  120. package/dist/lib/agent-env.d.ts +86 -0
  121. package/dist/lib/agent-env.d.ts.map +1 -0
  122. package/dist/lib/agent-env.js +126 -0
  123. package/dist/lib/agent-env.js.map +1 -0
  124. package/dist/lib/forge-contracts.d.ts +10 -0
  125. package/dist/lib/forge-contracts.d.ts.map +1 -1
  126. package/dist/lib/forge-contracts.js.map +1 -1
  127. package/dist/lib/forge-rate-limit.d.ts +45 -0
  128. package/dist/lib/forge-rate-limit.d.ts.map +1 -0
  129. package/dist/lib/forge-rate-limit.js +89 -0
  130. package/dist/lib/forge-rate-limit.js.map +1 -0
  131. package/dist/lib/forge.d.ts +28 -0
  132. package/dist/lib/forge.d.ts.map +1 -1
  133. package/dist/lib/forge.js +63 -10
  134. package/dist/lib/forge.js.map +1 -1
  135. package/dist/lib/github.d.ts +13 -1
  136. package/dist/lib/github.d.ts.map +1 -1
  137. package/dist/lib/github.js +58 -15
  138. package/dist/lib/github.js.map +1 -1
  139. package/dist/lib/test-env.d.ts +17 -0
  140. package/dist/lib/test-env.d.ts.map +1 -1
  141. package/dist/lib/test-env.js +20 -0
  142. package/dist/lib/test-env.js.map +1 -1
  143. package/dist/terminal/pty-session.d.ts +122 -4
  144. package/dist/terminal/pty-session.d.ts.map +1 -1
  145. package/dist/terminal/pty-session.js +142 -7
  146. package/dist/terminal/pty-session.js.map +1 -1
  147. package/dist/terminal/session-manager.d.ts +23 -2
  148. package/dist/terminal/session-manager.d.ts.map +1 -1
  149. package/dist/terminal/session-manager.js +115 -36
  150. package/dist/terminal/session-manager.js.map +1 -1
  151. package/dist/terminal/session-screen.d.ts +34 -5
  152. package/dist/terminal/session-screen.d.ts.map +1 -1
  153. package/dist/terminal/session-screen.js +52 -6
  154. package/dist/terminal/session-screen.js.map +1 -1
  155. package/dist/terminal/terminal-replies.d.ts +102 -0
  156. package/dist/terminal/terminal-replies.d.ts.map +1 -0
  157. package/dist/terminal/terminal-replies.js +181 -0
  158. package/dist/terminal/terminal-replies.js.map +1 -0
  159. package/package.json +4 -4
  160. package/scripts/forge/github/pr-list.sh +6 -2
  161. package/scripts/forge/github/pr-search.sh +4 -2
  162. package/scripts/forge/gitlab/issue-search.sh +1 -0
  163. package/scripts/forge/gitlab/pr-search.sh +35 -2
  164. package/scripts/forge/linear/auth-status.sh +1 -0
  165. package/scripts/forge/linear/issue-comment.sh +1 -0
  166. package/scripts/forge/linear/issue-list.sh +1 -0
  167. package/scripts/forge/linear/issue-search.sh +1 -0
  168. package/scripts/forge/linear/issue-view.sh +1 -0
  169. package/scripts/forge/linear/recently-closed.sh +1 -0
  170. package/scripts/forge/linear/user-identity.sh +1 -0
  171. package/skeleton/.claude/skills/generate-image/SKILL.md +19 -2
  172. package/skeleton/.codex/skills/generate-image/SKILL.md +19 -2
  173. package/skeleton/resources/commands/agent-farm.md +27 -2
  174. package/skeleton/roles/consultant.md +32 -0
  175. package/skeleton/templates/AGENTS.md +1 -1
  176. package/skeleton/templates/CLAUDE.md +1 -1
@@ -0,0 +1,274 @@
1
+ /**
2
+ * IDE prefix forward (Issue #1668).
3
+ *
4
+ * Tower forwards a fixed `/ide/` prefix to a single local VS Code server-web
5
+ * process — plain HTTP here and, for the workbench's own remote-protocol
6
+ * sockets, a raw bidirectional WebSocket pipe (Tower never parses those frames;
7
+ * they are VS Code's protocol, not Tower's — see `tower-websocket.ts` for the
8
+ * contrast with the PTY terminal bridge).
9
+ *
10
+ * Authorization is decided upstream by `isForwardAuthorized` (the HTTP path also
11
+ * passes the general `isRequestAllowed` choke point first); this module only
12
+ * proxies, after that decision. Header hygiene and `x-forwarded-*` stamping
13
+ * happen here, strictly after auth (Issue #1668 §D2).
14
+ */
15
+ import http from 'node:http';
16
+ import { URL } from 'node:url';
17
+ import { readCloudConfig } from '../lib/cloud-config.js';
18
+ import { TUNNEL_PROXY_HEADER } from '../lib/tunnel-client.js';
19
+ import { TOWER_KEY_HEADER, LEGACY_WEB_KEY_HEADER } from '@cluesmith/codev-types';
20
+ import { IDE_DEFAULT_PREFIX, readIdeConnectionToken } from '../lib/ide-record.js';
21
+ import { getRecordedIdePort, ensureIdeServerLive } from './ide-server.js';
22
+ /** Hop-by-hop headers not forwarded between connections (RFC 7230 §6.1). */
23
+ const HOP_BY_HOP = new Set([
24
+ 'connection',
25
+ 'keep-alive',
26
+ 'proxy-authenticate',
27
+ 'proxy-authorization',
28
+ 'te',
29
+ 'trailers',
30
+ 'transfer-encoding',
31
+ 'upgrade',
32
+ ]);
33
+ /**
34
+ * Headers stripped before forwarding to the IDE server:
35
+ * - `x-forwarded-port` / `x-original-host`: a leaked upstream port makes the
36
+ * browser dial the wrong WebSocket port, breaking the live session.
37
+ * - `codev-tower-key` / `codev-web-key`: Tower's auth credential — the IDE
38
+ * server must never see it.
39
+ * - `x-codev-tunnel-proxy`: Tower's tunnel marker — never forwarded, so it
40
+ * cannot leak or be trusted downstream (marker integrity, #1674).
41
+ * - `host`: replaced with the loopback target authority below.
42
+ */
43
+ const STRIP_BEFORE_FORWARD = new Set([
44
+ 'x-forwarded-port',
45
+ 'x-original-host',
46
+ TOWER_KEY_HEADER,
47
+ LEGACY_WEB_KEY_HEADER,
48
+ TUNNEL_PROXY_HEADER,
49
+ 'host',
50
+ ]);
51
+ /** True if this request arrived over the tunnel (stamped by `TunnelClient`, #1588). */
52
+ function isTunnelBorne(headers) {
53
+ return headers[TUNNEL_PROXY_HEADER] === '1';
54
+ }
55
+ /** The connection-token cookie name the VS Code server reads. */
56
+ const IDE_TOKEN_COOKIE = 'vscode-tkn';
57
+ /**
58
+ * Merge the IDE connection token into a Cookie header value as `vscode-tkn=…`,
59
+ * preserving every existing cookie and never clobbering an already-present
60
+ * `vscode-tkn` (Issue #1668). Exported for testing.
61
+ */
62
+ export function mergeConnectionTokenCookie(existing, token) {
63
+ const current = Array.isArray(existing) ? existing.join('; ') : existing ?? '';
64
+ // Never clobber: if a vscode-tkn is already present, leave the header as-is.
65
+ if (new RegExp(`(^|;\\s*)${IDE_TOKEN_COOKIE}=`).test(current))
66
+ return current;
67
+ const pair = `${IDE_TOKEN_COOKIE}=${token}`;
68
+ return current ? `${current}; ${pair}` : pair;
69
+ }
70
+ /**
71
+ * Derive the tunnel's public authority from the cloud config, matching the
72
+ * `accessUrl` Tower advertises (`tower-tunnel.ts`: `${server_url}/t/${tower_name}/`).
73
+ * Returns null when the tower is not cloud-registered.
74
+ */
75
+ export function getPublicAuthority() {
76
+ const cfg = readCloudConfig();
77
+ if (!cfg)
78
+ return null;
79
+ let host;
80
+ let proto;
81
+ try {
82
+ const url = new URL(cfg.server_url);
83
+ host = url.host;
84
+ proto = url.protocol === 'http:' ? 'http' : 'https';
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ return { host, proto, prefix: `/t/${cfg.tower_name}${IDE_DEFAULT_PREFIX.replace(/\/$/, '')}` };
90
+ }
91
+ /**
92
+ * Build the header set forwarded to the IDE server: drop hop-by-hop and
93
+ * Tower-internal headers, set the loopback `Host`, and — only when tunnel-borne
94
+ * — stamp `x-forwarded-host` / `x-forwarded-proto` / `x-forwarded-prefix` from
95
+ * the public authority so the workbench builds correct `remoteAuthority` and
96
+ * base-path URLs and never sees a stale upstream port. Response caching headers
97
+ * (`Cache-Control` / `ETag`) are a RESPONSE concern and preserved separately.
98
+ *
99
+ * Pure and exported for unit testing.
100
+ */
101
+ export function buildForwardHeaders(incoming, port, publicAuthority, connectionToken) {
102
+ const out = {};
103
+ for (const [key, value] of Object.entries(incoming)) {
104
+ if (value === undefined)
105
+ continue;
106
+ const lower = key.toLowerCase();
107
+ if (HOP_BY_HOP.has(lower))
108
+ continue;
109
+ if (STRIP_BEFORE_FORWARD.has(lower))
110
+ continue;
111
+ out[key] = value;
112
+ }
113
+ out['host'] = `127.0.0.1:${port}`;
114
+ if (isTunnelBorne(incoming) && publicAuthority) {
115
+ out['x-forwarded-host'] = publicAuthority.host;
116
+ out['x-forwarded-proto'] = publicAuthority.proto;
117
+ out['x-forwarded-prefix'] = publicAuthority.prefix;
118
+ }
119
+ // Present the IDE server's connection token as a cookie. The server is
120
+ // spawned with `--connection-token-file` and 403s without it, and it cannot
121
+ // ride the browser's cookie jar through the relay (Set-Cookie Path=/ide never
122
+ // matches /t/<tower>/ide/), so Tower injects it here — merging with, never
123
+ // clobbering, any existing Cookie header (Issue #1668).
124
+ if (connectionToken) {
125
+ out['cookie'] = mergeConnectionTokenCookie(incoming.cookie, connectionToken);
126
+ }
127
+ return out;
128
+ }
129
+ /** True if `pathname` targets the IDE forward prefix (`/ide` or `/ide/…`). */
130
+ export function matchIdePrefix(pathname) {
131
+ const bare = IDE_DEFAULT_PREFIX.replace(/\/$/, ''); // "/ide"
132
+ return pathname === bare || pathname.startsWith(IDE_DEFAULT_PREFIX);
133
+ }
134
+ /** Copy response headers, dropping hop-by-hop; preserves `Cache-Control` / `ETag`. */
135
+ function copyResponseHeaders(headers) {
136
+ const out = {};
137
+ for (const [key, value] of Object.entries(headers)) {
138
+ if (value === undefined)
139
+ continue;
140
+ if (HOP_BY_HOP.has(key.toLowerCase()))
141
+ continue;
142
+ out[key] = value;
143
+ }
144
+ return out;
145
+ }
146
+ /**
147
+ * Forward a plain HTTP request to the local IDE server. Assumes authorization
148
+ * has already passed. Path and query are forwarded unchanged (the workbench
149
+ * runs with `--server-base-path /ide` and selects the workspace from `?folder=`).
150
+ * A dead / unreachable server yields 502, never a crash.
151
+ */
152
+ export async function forwardIdeHttp(req, res, log) {
153
+ // Hot path: resolve the target port from the record — no port scan. A dead
154
+ // server fails the connect below, which triggers a respawn (Issue #1668).
155
+ const port = getRecordedIdePort();
156
+ if (port === null) {
157
+ res.writeHead(502, { 'Content-Type': 'application/json' });
158
+ res.end(JSON.stringify({ error: 'IDE server unavailable' }));
159
+ return;
160
+ }
161
+ const headers = buildForwardHeaders(req.headers, port, getPublicAuthority(), readIdeConnectionToken());
162
+ const proxyReq = http.request({ hostname: '127.0.0.1', port, path: req.url || '/', method: req.method, headers }, (proxyRes) => {
163
+ res.writeHead(proxyRes.statusCode ?? 502, copyResponseHeaders(proxyRes.headers));
164
+ proxyRes.pipe(res);
165
+ proxyRes.on('error', () => {
166
+ if (!res.writableEnded)
167
+ res.end();
168
+ });
169
+ });
170
+ proxyReq.on('error', (err) => {
171
+ log('WARN', `IDE HTTP forward error: ${err.message}`);
172
+ onUpstreamConnectError(err, log);
173
+ if (!res.headersSent) {
174
+ res.writeHead(502, { 'Content-Type': 'application/json' });
175
+ res.end(JSON.stringify({ error: 'Bad Gateway: IDE server unavailable' }));
176
+ }
177
+ else if (!res.writableEnded) {
178
+ res.end();
179
+ }
180
+ });
181
+ req.pipe(proxyReq);
182
+ req.on('error', () => proxyReq.destroy());
183
+ }
184
+ /**
185
+ * On a refused/reset upstream connection the recorded server is down. Kick a
186
+ * respawn off the hot path (fire-and-forget) so the next request lands; this
187
+ * request still 502s. The respawn (cold path) is where the `lsof` probe lives.
188
+ */
189
+ function onUpstreamConnectError(err, log) {
190
+ if (err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET') {
191
+ ensureIdeServerLive(log).catch(() => { });
192
+ }
193
+ }
194
+ /**
195
+ * Forward a WebSocket upgrade to the local IDE server as a raw bidirectional
196
+ * byte pipe. Tower authenticates the upgrade (caller) and then gets out of the
197
+ * way — the workbench speaks VS Code's own remote protocol, which Tower must not
198
+ * parse. Mirrors `tunnel-client.ts handleWebSocketConnect`'s pipe.
199
+ */
200
+ export async function forwardIdeWebSocket(req, socket, head, log) {
201
+ const port = getRecordedIdePort();
202
+ if (port === null) {
203
+ socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
204
+ socket.destroy();
205
+ return;
206
+ }
207
+ // Header hygiene drops the hop-by-hop `Connection`/`Upgrade` pair, so re-add
208
+ // them for the upstream upgrade (as `tunnel-client.ts` does). The client's
209
+ // `Sec-WebSocket-Key` is preserved by `buildForwardHeaders` and forwarded as-is
210
+ // so the upstream's `Sec-WebSocket-Accept` — relayed back verbatim — validates
211
+ // against the client's own key.
212
+ const headers = buildForwardHeaders(req.headers, port, getPublicAuthority(), readIdeConnectionToken());
213
+ headers['Connection'] = 'Upgrade';
214
+ headers['Upgrade'] = 'websocket';
215
+ const upstream = http.request({
216
+ hostname: '127.0.0.1',
217
+ port,
218
+ path: req.url || '/',
219
+ method: 'GET',
220
+ headers,
221
+ });
222
+ upstream.on('upgrade', (_upstreamRes, upstreamSocket, upstreamHead) => {
223
+ // Replay the handshake response and any buffered bytes, then pipe both ways.
224
+ socket.write(rebuildUpgradeResponse(_upstreamRes));
225
+ if (upstreamHead.length > 0)
226
+ socket.write(upstreamHead);
227
+ if (head.length > 0)
228
+ upstreamSocket.write(head);
229
+ upstreamSocket.pipe(socket);
230
+ socket.pipe(upstreamSocket);
231
+ const closeBoth = () => {
232
+ if (!socket.destroyed)
233
+ socket.destroy();
234
+ if (!upstreamSocket.destroyed)
235
+ upstreamSocket.destroy();
236
+ };
237
+ socket.on('error', closeBoth);
238
+ upstreamSocket.on('error', closeBoth);
239
+ socket.on('close', closeBoth);
240
+ upstreamSocket.on('close', closeBoth);
241
+ });
242
+ upstream.on('response', (upstreamRes) => {
243
+ // Upstream declined the upgrade (e.g. 404) — relay the status and close.
244
+ socket.write(`HTTP/1.1 ${upstreamRes.statusCode ?? 502} ${upstreamRes.statusMessage ?? ''}\r\n\r\n`);
245
+ socket.destroy();
246
+ });
247
+ upstream.on('error', (err) => {
248
+ log('WARN', `IDE WS forward error: ${err.message}`);
249
+ onUpstreamConnectError(err, log);
250
+ if (!socket.destroyed) {
251
+ socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
252
+ socket.destroy();
253
+ }
254
+ });
255
+ upstream.end();
256
+ }
257
+ /** Reconstruct the HTTP/1.1 101 switching-protocols response line + headers. */
258
+ function rebuildUpgradeResponse(res) {
259
+ const statusLine = `HTTP/1.1 ${res.statusCode ?? 101} ${res.statusMessage ?? 'Switching Protocols'}`;
260
+ const lines = [statusLine];
261
+ for (const [key, value] of Object.entries(res.headers)) {
262
+ if (value === undefined)
263
+ continue;
264
+ if (Array.isArray(value)) {
265
+ for (const v of value)
266
+ lines.push(`${key}: ${v}`);
267
+ }
268
+ else {
269
+ lines.push(`${key}: ${value}`);
270
+ }
271
+ }
272
+ return lines.join('\r\n') + '\r\n\r\n';
273
+ }
274
+ //# sourceMappingURL=ide-forward.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ide-forward.js","sourceRoot":"","sources":["../../../src/agent-farm/servers/ide-forward.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * IDE server lifecycle, Tower-side (Issue #1668).
3
+ *
4
+ * Tower is the spawner: `afx ide start` drives this through the local-only
5
+ * `/api/ide` endpoint, and the same spawn path is reused by boot reconcile and
6
+ * by on-demand respawn when a `/ide/` request arrives and the server is down.
7
+ * Runtime state lives in the `~/.agent-farm/ide-server.json` record (Issue
8
+ * #1668 §D3) — no config file, no `global.db` change. Liveness is discovered by
9
+ * port (`getProcessesOnPort`), the same way Tower finds its own process.
10
+ */
11
+ import http from 'node:http';
12
+ import { type IdeServerRecord } from '../lib/ide-record.js';
13
+ type LogFn = (level: 'INFO' | 'ERROR' | 'WARN', message: string) => void;
14
+ /**
15
+ * The recorded IDE server port, or null if none — the forward's hot-path target
16
+ * resolution. A plain record read, no port scan (Issue #1668): a dead server
17
+ * fails the proxy connect immediately, which is the only liveness check the
18
+ * forward needs.
19
+ */
20
+ export declare function getRecordedIdePort(): number | null;
21
+ export interface SpawnIdeOptions {
22
+ serverPath: string;
23
+ port?: number;
24
+ defaultFolder?: string;
25
+ }
26
+ /**
27
+ * Spawn the IDE server detached (so it survives a Tower restart, like a
28
+ * shellper) and write the runtime record. Resolves to the record once the
29
+ * server is listening, or throws if the binary is missing or never binds.
30
+ */
31
+ export declare function spawnIdeServer(opts: SpawnIdeOptions, log: LogFn): Promise<IdeServerRecord>;
32
+ /**
33
+ * Stop the IDE server on the recorded port: SIGTERM, wait for exit, escalate to
34
+ * SIGKILL for survivors (mirrors `towerStop`, #1668 plan §4), then delete the
35
+ * record. Cold path — `getProcessesOnPort` is fine here.
36
+ */
37
+ export declare function stopIdeServer(log: LogFn): Promise<{
38
+ stopped: boolean;
39
+ port: number | null;
40
+ }>;
41
+ export interface IdeStatus {
42
+ record: IdeServerRecord | null;
43
+ alive: boolean;
44
+ pid: number | null;
45
+ }
46
+ /** Report the IDE server's status from the record plus a live port probe. */
47
+ export declare function ideServerStatus(): IdeStatus;
48
+ /**
49
+ * Ensure the IDE server is live and return its port, or null if it cannot be
50
+ * made live (no record, or a respawn that never bound). A live server is the
51
+ * fast path (no spawn). A recorded-but-dead server is respawned from the
52
+ * recorded `serverPath`; concurrent callers share one in-flight respawn.
53
+ */
54
+ export declare function ensureIdeServerLive(log?: LogFn): Promise<number | null>;
55
+ /**
56
+ * Boot-time reconcile: adopt a live server (no orphan) or respawn a recorded
57
+ * but dead one. Best-effort — never throws into Tower boot.
58
+ */
59
+ export declare function reconcileIdeServer(log: LogFn): Promise<void>;
60
+ /**
61
+ * Local-only `/api/ide` management endpoint driving the lifecycle above:
62
+ * `POST` starts (body `{ serverPath, port?, defaultFolder? }`), `DELETE` stops,
63
+ * `GET` reports status. Reached only post-`isRequestAllowed` (key required) and
64
+ * blocked from the tunnel like `/api/tunnel/*` — it spawns/kills a process.
65
+ */
66
+ export declare function handleIdeApi(req: http.IncomingMessage, res: http.ServerResponse, log: LogFn): Promise<void>;
67
+ export {};
68
+ //# sourceMappingURL=ide-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ide-server.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/ide-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAM7B,OAAO,EAGL,KAAK,eAAe,EAMrB,MAAM,sBAAsB,CAAC;AAE9B,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AA4BzE;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,IAAI,CAElD;AAYD,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,EAAE,KAAK,GAAG,OAAO,CAAC,eAAe,CAAC,CA6EhG;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,KAAK,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAkClG;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,IAAI,SAAS,CAK3C;AAKD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,GAAE,KAAgB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsBvF;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAQlE;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,GAAG,EAAE,KAAK,GACT,OAAO,CAAC,IAAI,CAAC,CA6Cf"}
@@ -0,0 +1,281 @@
1
+ /**
2
+ * IDE server lifecycle, Tower-side (Issue #1668).
3
+ *
4
+ * Tower is the spawner: `afx ide start` drives this through the local-only
5
+ * `/api/ide` endpoint, and the same spawn path is reused by boot reconcile and
6
+ * by on-demand respawn when a `/ide/` request arrives and the server is down.
7
+ * Runtime state lives in the `~/.agent-farm/ide-server.json` record (Issue
8
+ * #1668 §D3) — no config file, no `global.db` change. Liveness is discovered by
9
+ * port (`getProcessesOnPort`), the same way Tower finds its own process.
10
+ */
11
+ import { spawn } from 'node:child_process';
12
+ import { existsSync } from 'node:fs';
13
+ import { getProcessesOnPort } from '../utils/port.js';
14
+ import { sanitizeAgentEnv } from '../../lib/agent-env.js';
15
+ import { parseJsonBody } from '../utils/server-utils.js';
16
+ import { IDE_DEFAULT_PREFIX, IDE_DEFAULT_PORT, readIdeRecord, writeIdeRecord, deleteIdeRecord, ensureIdeConnectionToken, getIdeConnectionTokenPath, } from '../lib/ide-record.js';
17
+ const READY_TIMEOUT_MS = 15_000;
18
+ const READY_POLL_MS = 200;
19
+ const STOP_EXIT_TIMEOUT_MS = 5_000;
20
+ const STOP_POLL_MS = 200;
21
+ /**
22
+ * True if some process is listening on the port. Uses `lsof` (`getProcessesOnPort`),
23
+ * which is ~100 ms and synchronous, so it is a **COLD-path only** check — restart
24
+ * reconcile, `afx ide status`, start-collision detection, and respawn. The request
25
+ * forward must NEVER call this (Issue #1668): it reads the port from the record and
26
+ * lets the upstream connect error be the liveness truth-teller.
27
+ */
28
+ function isPortLive(port) {
29
+ return getProcessesOnPort(port).length > 0;
30
+ }
31
+ /** True if a PID is alive (cheap, non-blocking). */
32
+ function isPidAlive(pid) {
33
+ try {
34
+ process.kill(pid, 0);
35
+ return true;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /**
42
+ * The recorded IDE server port, or null if none — the forward's hot-path target
43
+ * resolution. A plain record read, no port scan (Issue #1668): a dead server
44
+ * fails the proxy connect immediately, which is the only liveness check the
45
+ * forward needs.
46
+ */
47
+ export function getRecordedIdePort() {
48
+ return readIdeRecord()?.port ?? null;
49
+ }
50
+ /** Poll until a process is listening on `port`, or the timeout elapses. */
51
+ async function waitForPort(port, timeoutMs) {
52
+ const deadline = Date.now() + timeoutMs;
53
+ while (Date.now() < deadline) {
54
+ if (isPortLive(port))
55
+ return true;
56
+ await new Promise((r) => setTimeout(r, READY_POLL_MS));
57
+ }
58
+ return isPortLive(port);
59
+ }
60
+ /**
61
+ * Spawn the IDE server detached (so it survives a Tower restart, like a
62
+ * shellper) and write the runtime record. Resolves to the record once the
63
+ * server is listening, or throws if the binary is missing or never binds.
64
+ */
65
+ export async function spawnIdeServer(opts, log) {
66
+ const port = opts.port ?? IDE_DEFAULT_PORT;
67
+ if (!opts.serverPath) {
68
+ throw new Error('IDE server path is required (no default — set --server-path)');
69
+ }
70
+ if (!existsSync(opts.serverPath)) {
71
+ throw new Error(`IDE server binary not found at ${opts.serverPath}`);
72
+ }
73
+ // Start-collision: a prior server recorded on a DIFFERENT port would be orphaned
74
+ // (still running, no longer tracked) if we just overwrote the singleton record.
75
+ // Stop it first so "start on a new port" never leaves a detached stray (#1668).
76
+ const existing = readIdeRecord();
77
+ if (existing && existing.port !== port && isPortLive(existing.port)) {
78
+ log('WARN', `IDE server already recorded on port ${existing.port}; stopping it before starting on ${port}`);
79
+ await stopIdeServer(log);
80
+ }
81
+ // Already live on the port — idempotent: adopt and record, don't double-spawn.
82
+ if (isPortLive(port)) {
83
+ log('INFO', `IDE server already listening on port ${port}; adopting`);
84
+ const pid = getProcessesOnPort(port)[0] ?? 0;
85
+ const record = {
86
+ prefix: IDE_DEFAULT_PREFIX,
87
+ port,
88
+ serverPath: opts.serverPath,
89
+ pid,
90
+ defaultFolder: opts.defaultFolder,
91
+ startedAt: new Date().toISOString(),
92
+ };
93
+ writeIdeRecord(record);
94
+ return record;
95
+ }
96
+ const basePath = IDE_DEFAULT_PREFIX.replace(/\/$/, ''); // "/ide"
97
+ const tokenFile = getIdeConnectionTokenPath();
98
+ ensureIdeConnectionToken();
99
+ const args = [
100
+ '--host', '127.0.0.1',
101
+ '--port', String(port),
102
+ '--server-base-path', basePath,
103
+ '--connection-token-file', tokenFile,
104
+ '--accept-server-license-terms',
105
+ ];
106
+ if (opts.defaultFolder) {
107
+ args.push('--default-folder', opts.defaultFolder);
108
+ }
109
+ log('INFO', `Spawning IDE server: ${opts.serverPath} ${args.join(' ')}`);
110
+ const child = spawn(opts.serverPath, args, {
111
+ cwd: process.cwd(),
112
+ env: sanitizeAgentEnv(process.env),
113
+ detached: true,
114
+ stdio: 'ignore',
115
+ });
116
+ if (!child.pid) {
117
+ throw new Error('Failed to spawn IDE server process');
118
+ }
119
+ child.unref();
120
+ const ready = await waitForPort(port, READY_TIMEOUT_MS);
121
+ if (!ready) {
122
+ throw new Error(`IDE server did not start listening on port ${port} within ${READY_TIMEOUT_MS / 1000}s`);
123
+ }
124
+ const record = {
125
+ prefix: IDE_DEFAULT_PREFIX,
126
+ port,
127
+ serverPath: opts.serverPath,
128
+ pid: child.pid,
129
+ defaultFolder: opts.defaultFolder,
130
+ startedAt: new Date().toISOString(),
131
+ };
132
+ writeIdeRecord(record);
133
+ log('INFO', `IDE server listening on port ${port} (pid ${child.pid})`);
134
+ return record;
135
+ }
136
+ /**
137
+ * Stop the IDE server on the recorded port: SIGTERM, wait for exit, escalate to
138
+ * SIGKILL for survivors (mirrors `towerStop`, #1668 plan §4), then delete the
139
+ * record. Cold path — `getProcessesOnPort` is fine here.
140
+ */
141
+ export async function stopIdeServer(log) {
142
+ const record = readIdeRecord();
143
+ if (!record) {
144
+ return { stopped: false, port: null };
145
+ }
146
+ const pids = getProcessesOnPort(record.port);
147
+ for (const pid of pids) {
148
+ try {
149
+ process.kill(pid, 'SIGTERM');
150
+ }
151
+ catch {
152
+ // already gone
153
+ }
154
+ }
155
+ // Wait for the SIGTERMed processes to actually exit before deleting the record;
156
+ // escalate any survivors to SIGKILL, so a lingering process can't be re-adopted.
157
+ const deadline = Date.now() + STOP_EXIT_TIMEOUT_MS;
158
+ let survivors = pids.filter(isPidAlive);
159
+ while (survivors.length > 0 && Date.now() < deadline) {
160
+ await new Promise((r) => setTimeout(r, STOP_POLL_MS));
161
+ survivors = survivors.filter(isPidAlive);
162
+ }
163
+ for (const pid of survivors) {
164
+ try {
165
+ process.kill(pid, 'SIGKILL');
166
+ }
167
+ catch {
168
+ // exited between the check and the kill
169
+ }
170
+ }
171
+ deleteIdeRecord();
172
+ const killedNote = survivors.length > 0 ? `, SIGKILL escalated for ${survivors.length}` : '';
173
+ log('INFO', `Stopped IDE server on port ${record.port} (${pids.length} process(es)${killedNote})`);
174
+ return { stopped: pids.length > 0, port: record.port };
175
+ }
176
+ /** Report the IDE server's status from the record plus a live port probe. */
177
+ export function ideServerStatus() {
178
+ const record = readIdeRecord();
179
+ if (!record)
180
+ return { record: null, alive: false, pid: null };
181
+ const pids = getProcessesOnPort(record.port);
182
+ return { record, alive: pids.length > 0, pid: pids[0] ?? null };
183
+ }
184
+ // Guards against two concurrent `/ide/` requests both triggering a respawn.
185
+ let respawnInFlight = null;
186
+ /**
187
+ * Ensure the IDE server is live and return its port, or null if it cannot be
188
+ * made live (no record, or a respawn that never bound). A live server is the
189
+ * fast path (no spawn). A recorded-but-dead server is respawned from the
190
+ * recorded `serverPath`; concurrent callers share one in-flight respawn.
191
+ */
192
+ export async function ensureIdeServerLive(log = () => { }) {
193
+ const record = readIdeRecord();
194
+ if (!record)
195
+ return null;
196
+ if (isPortLive(record.port))
197
+ return record.port;
198
+ if (respawnInFlight)
199
+ return respawnInFlight;
200
+ respawnInFlight = (async () => {
201
+ try {
202
+ log('INFO', `IDE server on port ${record.port} is down; respawning from ${record.serverPath}`);
203
+ const fresh = await spawnIdeServer({ serverPath: record.serverPath, port: record.port, defaultFolder: record.defaultFolder }, log);
204
+ return fresh.port;
205
+ }
206
+ catch (err) {
207
+ log('WARN', `IDE server respawn failed: ${err.message}`);
208
+ return null;
209
+ }
210
+ finally {
211
+ respawnInFlight = null;
212
+ }
213
+ })();
214
+ return respawnInFlight;
215
+ }
216
+ /**
217
+ * Boot-time reconcile: adopt a live server (no orphan) or respawn a recorded
218
+ * but dead one. Best-effort — never throws into Tower boot.
219
+ */
220
+ export async function reconcileIdeServer(log) {
221
+ const record = readIdeRecord();
222
+ if (!record)
223
+ return;
224
+ if (isPortLive(record.port)) {
225
+ log('INFO', `Adopted running IDE server on port ${record.port}`);
226
+ return;
227
+ }
228
+ await ensureIdeServerLive(log);
229
+ }
230
+ /**
231
+ * Local-only `/api/ide` management endpoint driving the lifecycle above:
232
+ * `POST` starts (body `{ serverPath, port?, defaultFolder? }`), `DELETE` stops,
233
+ * `GET` reports status. Reached only post-`isRequestAllowed` (key required) and
234
+ * blocked from the tunnel like `/api/tunnel/*` — it spawns/kills a process.
235
+ */
236
+ export async function handleIdeApi(req, res, log) {
237
+ if (req.method === 'GET') {
238
+ const status = ideServerStatus();
239
+ res.writeHead(200, { 'Content-Type': 'application/json' });
240
+ res.end(JSON.stringify(status));
241
+ return;
242
+ }
243
+ if (req.method === 'DELETE') {
244
+ const result = await stopIdeServer(log);
245
+ res.writeHead(200, { 'Content-Type': 'application/json' });
246
+ res.end(JSON.stringify(result));
247
+ return;
248
+ }
249
+ if (req.method === 'POST') {
250
+ let body;
251
+ try {
252
+ body = await parseJsonBody(req);
253
+ }
254
+ catch {
255
+ res.writeHead(400, { 'Content-Type': 'application/json' });
256
+ res.end(JSON.stringify({ error: 'Invalid JSON body' }));
257
+ return;
258
+ }
259
+ const serverPath = typeof body.serverPath === 'string' ? body.serverPath : '';
260
+ const port = typeof body.port === 'number' ? body.port : undefined;
261
+ const defaultFolder = typeof body.defaultFolder === 'string' ? body.defaultFolder : undefined;
262
+ if (!serverPath) {
263
+ res.writeHead(400, { 'Content-Type': 'application/json' });
264
+ res.end(JSON.stringify({ error: 'serverPath is required' }));
265
+ return;
266
+ }
267
+ try {
268
+ const record = await spawnIdeServer({ serverPath, port, defaultFolder }, log);
269
+ res.writeHead(200, { 'Content-Type': 'application/json' });
270
+ res.end(JSON.stringify({ ok: true, ...record }));
271
+ }
272
+ catch (err) {
273
+ res.writeHead(500, { 'Content-Type': 'application/json' });
274
+ res.end(JSON.stringify({ error: err.message }));
275
+ }
276
+ return;
277
+ }
278
+ res.writeHead(405, { 'Content-Type': 'application/json' });
279
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
280
+ }
281
+ //# sourceMappingURL=ide-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ide-server.js","sourceRoot":"","sources":["../../../src/agent-farm/servers/ide-server.ts"],"names":[],"mappings":"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"}