mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 00:02:39 +00:00
* Add worktree labels for localhost ports * Fix localhost label test fixtures * Extend localhost worktree labels to agents * Address localhost label review feedback * Remove localhost label page injection; stream responses untouched Drop the title/favicon HTML injection from the localhost label proxy. The proxy now only relabels the hostname and pipes responses straight through, so app CSP/cookies/bodies are preserved and large/streamed responses are no longer buffered in memory. - Normalize wildcard bind hosts (0.0.0.0 -> 127.0.0.1, :: -> ::1) before using them as a proxy connect target. - Delete the favicon SVG generator and dead repoIcon/badgeColor plumbing. - Remove orphaned LocalhostLabelMock i18n keys. Co-authored-by: Orca <help@stably.ai> * Harden localhost label proxy from review - IPC register: restrict proxy target to loopback or a matching scanned workspace port (close open-proxy/SSRF surface from untrusted renderer). - Proxy: guard against ERR_HTTP_HEADERS_SENT on mid-stream upstream error; add error/cleanup listeners on client request/response and upgrade socket. - labelLocalhostUrl: fall back to the raw URL when the proxy rejects a target (e.g. https) instead of throwing. - Port label route: include worktreePath so button-open and terminal/CLI paths produce the same label. - Terminal OSC link hover: discard stale async tooltip results via a hover token, matching the WebLinks path. Co-authored-by: Orca <help@stably.ai> * Match default-port advertised hosts in localhost label target check Co-authored-by: Orca <help@stably.ai> * Consolidate duplicated localhost label helpers - Move the loopback host set, loopback-URL parser, and wildcard connect-host normalizer into shared/localhost-worktree-labels.ts; proxy, runtime, terminal link routing, and the IPC guard now share one copy. - Extract the port -> repo -> worktree -> project label-route lookup into workspace-port-localhost-label-selector.ts (a hook plus an imperative resolver), replacing the block triplicated across the ports surfaces. Co-authored-by: Orca <help@stably.ai> * Extract command-code prompt-status seed to its own module Keeps launch-agent-in-new-tab.ts under the max-lines limit after the localhost-hint additions, without a lint disable. Co-authored-by: Orca <help@stably.ai> * Remove agent-facing localhost mechanism Orca does not mutate user prompts or inject prompt snippets, so drop the localhost-open agent hint entirely: - Remove appendLocalhostOpeningHint / includeLocalhostOpeningHint and the hint constant from tui-agent-startup; agent prompts are no longer rewritten. - Remove the ORCA_LOCALHOST_OPEN env var from local agent terminals (it was only discoverable via the now-removed hint). - Remove the orca localhost label|open CLI commands, their workspacePorts RPC methods, and the runtime labelLocalhostUrl/openLocalhostUrl methods. The feature is now purely structural: the loopback label proxy plus the ports-panel and terminal-link 'Open in Browser' paths, which surface a clickable labeled URL without touching agent prompts. Co-authored-by: Orca <help@stably.ai> * Make localhost worktree labels opt-in (default off) Serving a dev app under a different host than localhost:<port> can break apps that bind cookies/sessions to localhost, so the feature should not change existing users' Open-in-Browser behavior on upgrade. - Default localhostWorktreeLabelsEnabled to false. - Flip the gates to enable only when explicitly true (=== true / !== true) instead of treating undefined as enabled. - Update the setting switch to checked only when explicitly enabled. - Drop the now-unused runtime store settings field. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Orca <help@stably.ai>
128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
import http from 'node:http'
|
|
import type { AddressInfo } from 'node:net'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { LocalhostWorktreeLabelProxy } from './localhost-worktree-label-proxy'
|
|
|
|
const upstreamServers: http.Server[] = []
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
upstreamServers
|
|
.splice(0)
|
|
.map((server) => new Promise<void>((resolve) => server.close(() => resolve())))
|
|
)
|
|
})
|
|
|
|
async function startUpstream(
|
|
handler: (request: http.IncomingMessage, response: http.ServerResponse) => void
|
|
): Promise<number> {
|
|
const server = http.createServer(handler)
|
|
upstreamServers.push(server)
|
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
|
|
return (server.address() as AddressInfo).port
|
|
}
|
|
|
|
function fetchThroughProxy(
|
|
labeledUrl: string
|
|
): Promise<{ status: number; body: string; headers: http.IncomingHttpHeaders }> {
|
|
const url = new URL(labeledUrl)
|
|
return new Promise((resolve, reject) => {
|
|
// Why: *.orca.localhost is not resolvable DNS; connect to the proxy on
|
|
// loopback and carry the label through the Host header instead.
|
|
const request = http.request(
|
|
{
|
|
host: '127.0.0.1',
|
|
port: Number(url.port),
|
|
path: `${url.pathname}${url.search}`,
|
|
headers: { host: url.host }
|
|
},
|
|
(response) => {
|
|
const chunks: Buffer[] = []
|
|
response.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
|
|
response.on('end', () =>
|
|
resolve({
|
|
status: response.statusCode ?? 0,
|
|
body: Buffer.concat(chunks).toString('utf8'),
|
|
headers: response.headers
|
|
})
|
|
)
|
|
}
|
|
)
|
|
request.on('error', reject)
|
|
request.end()
|
|
})
|
|
}
|
|
|
|
describe('localhost worktree label proxy', () => {
|
|
it('rejects https targets because the label proxy serves plain http', async () => {
|
|
const proxy = new LocalhostWorktreeLabelProxy()
|
|
|
|
await expect(
|
|
proxy.registerRoute({
|
|
targetUrl: 'https://localhost:5173/',
|
|
projectName: 'Snap Studio',
|
|
worktreeName: 'main'
|
|
})
|
|
).rejects.toThrow('Only http workspace ports can be labeled.')
|
|
})
|
|
|
|
it('streams responses through untouched, preserving the app CSP and body', async () => {
|
|
const port = await startUpstream((_request, response) => {
|
|
response.writeHead(200, {
|
|
'content-type': 'text/html; charset=utf-8',
|
|
'content-security-policy': "default-src 'self'"
|
|
})
|
|
response.end('<html><head></head><body>hello</body></html>')
|
|
})
|
|
const proxy = new LocalhostWorktreeLabelProxy()
|
|
const { url } = await proxy.registerRoute({
|
|
targetUrl: `http://localhost:${port}/`,
|
|
projectName: 'Snap Studio',
|
|
worktreeName: 'analytics'
|
|
})
|
|
|
|
const result = await fetchThroughProxy(url)
|
|
|
|
expect(result.status).toBe(200)
|
|
// The proxy only relabels the hostname; the page is delivered verbatim
|
|
// with no injected favicon/title and the CSP header intact.
|
|
expect(result.body).toBe('<html><head></head><body>hello</body></html>')
|
|
expect(result.headers['content-security-policy']).toBe("default-src 'self'")
|
|
})
|
|
|
|
it('normalizes a 0.0.0.0 target to a connectable loopback host', async () => {
|
|
const port = await startUpstream((_request, response) => {
|
|
response.writeHead(200, { 'content-type': 'text/plain' })
|
|
response.end('ok')
|
|
})
|
|
const proxy = new LocalhostWorktreeLabelProxy()
|
|
const { url } = await proxy.registerRoute({
|
|
targetUrl: `http://0.0.0.0:${port}/`,
|
|
projectName: 'Snap Studio',
|
|
worktreeName: 'main'
|
|
})
|
|
|
|
const result = await fetchThroughProxy(url)
|
|
|
|
expect(result.status).toBe(200)
|
|
expect(result.body).toBe('ok')
|
|
})
|
|
|
|
it('returns 404 for unregistered orca.localhost labels', async () => {
|
|
const port = await startUpstream((_request, response) => {
|
|
response.end('ok')
|
|
})
|
|
const proxy = new LocalhostWorktreeLabelProxy()
|
|
const { url } = await proxy.registerRoute({
|
|
targetUrl: `http://localhost:${port}/`,
|
|
projectName: 'Snap Studio',
|
|
worktreeName: 'main'
|
|
})
|
|
const proxyPort = new URL(url).port
|
|
|
|
const result = await fetchThroughProxy(`http://unknown-label.orca.localhost:${proxyPort}/`)
|
|
|
|
expect(result.status).toBe(404)
|
|
})
|
|
})
|