@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.
- package/dashboard-dist/assets/{index-CUIjS_Vb.js → index-DZf6pNPE.js} +2 -2
- package/dashboard-dist/assets/{index-CUIjS_Vb.js.map → index-DZf6pNPE.js.map} +1 -1
- package/dashboard-dist/index.html +1 -1
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +48 -0
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/ide.d.ts +20 -0
- package/dist/agent-farm/commands/ide.d.ts.map +1 -0
- package/dist/agent-farm/commands/ide.js +138 -0
- package/dist/agent-farm/commands/ide.js.map +1 -0
- package/dist/agent-farm/commands/inbox.d.ts.map +1 -1
- package/dist/agent-farm/commands/inbox.js +15 -3
- package/dist/agent-farm/commands/inbox.js.map +1 -1
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +16 -1
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +16 -7
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/tower.d.ts +21 -9
- package/dist/agent-farm/commands/tower.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower.js +90 -30
- package/dist/agent-farm/commands/tower.js.map +1 -1
- package/dist/agent-farm/db/tower-owner.d.ts +168 -0
- package/dist/agent-farm/db/tower-owner.d.ts.map +1 -0
- package/dist/agent-farm/db/tower-owner.js +369 -0
- package/dist/agent-farm/db/tower-owner.js.map +1 -0
- package/dist/agent-farm/db/types.d.ts +30 -5
- package/dist/agent-farm/db/types.d.ts.map +1 -1
- package/dist/agent-farm/db/types.js.map +1 -1
- package/dist/agent-farm/lib/ide-record.d.ts +65 -0
- package/dist/agent-farm/lib/ide-record.d.ts.map +1 -0
- package/dist/agent-farm/lib/ide-record.js +118 -0
- package/dist/agent-farm/lib/ide-record.js.map +1 -0
- package/dist/agent-farm/lib/tunnel-client.d.ts +22 -0
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.js +53 -5
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
- package/dist/agent-farm/servers/gate-profiles.d.ts +23 -2
- package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -1
- package/dist/agent-farm/servers/gate-profiles.js +43 -2
- package/dist/agent-farm/servers/gate-profiles.js.map +1 -1
- package/dist/agent-farm/servers/ide-forward.d.ts +62 -0
- package/dist/agent-farm/servers/ide-forward.d.ts.map +1 -0
- package/dist/agent-farm/servers/ide-forward.js +274 -0
- package/dist/agent-farm/servers/ide-forward.js.map +1 -0
- package/dist/agent-farm/servers/ide-server.d.ts +68 -0
- package/dist/agent-farm/servers/ide-server.d.ts.map +1 -0
- package/dist/agent-farm/servers/ide-server.js +281 -0
- package/dist/agent-farm/servers/ide-server.js.map +1 -0
- package/dist/agent-farm/servers/mailbox-delivery.d.ts +213 -4
- package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-delivery.js +489 -68
- package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.d.ts +18 -4
- package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.js +94 -21
- package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
- package/dist/agent-farm/servers/message-write.d.ts +138 -11
- package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
- package/dist/agent-farm/servers/message-write.js +144 -35
- package/dist/agent-farm/servers/message-write.js.map +1 -1
- package/dist/agent-farm/servers/overview.d.ts +28 -12
- package/dist/agent-farm/servers/overview.d.ts.map +1 -1
- package/dist/agent-farm/servers/overview.js +130 -79
- package/dist/agent-farm/servers/overview.js.map +1 -1
- package/dist/agent-farm/servers/render-gate.d.ts +53 -0
- package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
- package/dist/agent-farm/servers/render-gate.js +135 -24
- package/dist/agent-farm/servers/render-gate.js.map +1 -1
- package/dist/agent-farm/servers/session-submit.d.ts +7 -4
- package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
- package/dist/agent-farm/servers/session-submit.js +7 -4
- package/dist/agent-farm/servers/session-submit.js.map +1 -1
- package/dist/agent-farm/servers/tower-cron.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-cron.js +4 -1
- package/dist/agent-farm/servers/tower-cron.js.map +1 -1
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +6 -7
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +53 -13
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +64 -7
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +53 -8
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.js +18 -1
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
- package/dist/agent-farm/utils/message-format.js +5 -6
- package/dist/agent-farm/utils/message-format.js.map +1 -1
- package/dist/agent-farm/utils/port.d.ts +22 -0
- package/dist/agent-farm/utils/port.d.ts.map +1 -0
- package/dist/agent-farm/utils/port.js +35 -0
- package/dist/agent-farm/utils/port.js.map +1 -0
- package/dist/agent-farm/utils/server-utils.d.ts +26 -0
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +36 -0
- package/dist/agent-farm/utils/server-utils.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +61 -1
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/consult/tree-tripwire.d.ts +177 -0
- package/dist/commands/consult/tree-tripwire.d.ts.map +1 -0
- package/dist/commands/consult/tree-tripwire.js +356 -0
- package/dist/commands/consult/tree-tripwire.js.map +1 -0
- package/dist/commands/doctor.d.ts +69 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +163 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/generate-image.d.ts +5 -0
- package/dist/commands/generate-image.d.ts.map +1 -1
- package/dist/commands/generate-image.js +225 -4
- package/dist/commands/generate-image.js.map +1 -1
- package/dist/lib/agent-env.d.ts +86 -0
- package/dist/lib/agent-env.d.ts.map +1 -0
- package/dist/lib/agent-env.js +126 -0
- package/dist/lib/agent-env.js.map +1 -0
- package/dist/lib/forge-contracts.d.ts +10 -0
- package/dist/lib/forge-contracts.d.ts.map +1 -1
- package/dist/lib/forge-contracts.js.map +1 -1
- package/dist/lib/forge-rate-limit.d.ts +45 -0
- package/dist/lib/forge-rate-limit.d.ts.map +1 -0
- package/dist/lib/forge-rate-limit.js +89 -0
- package/dist/lib/forge-rate-limit.js.map +1 -0
- package/dist/lib/forge.d.ts +28 -0
- package/dist/lib/forge.d.ts.map +1 -1
- package/dist/lib/forge.js +63 -10
- package/dist/lib/forge.js.map +1 -1
- package/dist/lib/github.d.ts +13 -1
- package/dist/lib/github.d.ts.map +1 -1
- package/dist/lib/github.js +58 -15
- package/dist/lib/github.js.map +1 -1
- package/dist/lib/test-env.d.ts +17 -0
- package/dist/lib/test-env.d.ts.map +1 -1
- package/dist/lib/test-env.js +20 -0
- package/dist/lib/test-env.js.map +1 -1
- package/dist/terminal/pty-session.d.ts +122 -4
- package/dist/terminal/pty-session.d.ts.map +1 -1
- package/dist/terminal/pty-session.js +142 -7
- package/dist/terminal/pty-session.js.map +1 -1
- package/dist/terminal/session-manager.d.ts +23 -2
- package/dist/terminal/session-manager.d.ts.map +1 -1
- package/dist/terminal/session-manager.js +115 -36
- package/dist/terminal/session-manager.js.map +1 -1
- package/dist/terminal/session-screen.d.ts +34 -5
- package/dist/terminal/session-screen.d.ts.map +1 -1
- package/dist/terminal/session-screen.js +52 -6
- package/dist/terminal/session-screen.js.map +1 -1
- package/dist/terminal/terminal-replies.d.ts +102 -0
- package/dist/terminal/terminal-replies.d.ts.map +1 -0
- package/dist/terminal/terminal-replies.js +181 -0
- package/dist/terminal/terminal-replies.js.map +1 -0
- package/package.json +4 -4
- package/scripts/forge/github/pr-list.sh +6 -2
- package/scripts/forge/github/pr-search.sh +4 -2
- package/scripts/forge/gitlab/issue-search.sh +1 -0
- package/scripts/forge/gitlab/pr-search.sh +35 -2
- package/scripts/forge/linear/auth-status.sh +1 -0
- package/scripts/forge/linear/issue-comment.sh +1 -0
- package/scripts/forge/linear/issue-list.sh +1 -0
- package/scripts/forge/linear/issue-search.sh +1 -0
- package/scripts/forge/linear/issue-view.sh +1 -0
- package/scripts/forge/linear/recently-closed.sh +1 -0
- package/scripts/forge/linear/user-identity.sh +1 -0
- package/skeleton/.claude/skills/generate-image/SKILL.md +19 -2
- package/skeleton/.codex/skills/generate-image/SKILL.md +19 -2
- package/skeleton/resources/commands/agent-farm.md +27 -2
- package/skeleton/roles/consultant.md +32 -0
- package/skeleton/templates/AGENTS.md +1 -1
- 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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"}
|