mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +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>
95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
import { ipcMain } from 'electron'
|
|
import { URL } from 'node:url'
|
|
import {
|
|
LOOPBACK_LOCALHOST_HOSTS,
|
|
normalizeLocalhostHostname,
|
|
type LocalhostWorktreeLabelResult,
|
|
type LocalhostWorktreeLabelRoute
|
|
} from '../../shared/localhost-worktree-labels'
|
|
import type { Store } from '../persistence'
|
|
import { localhostWorktreeLabelProxy } from '../localhost-worktree-label-proxy'
|
|
import {
|
|
getStoreWorkspacePortProbes,
|
|
scanWorkspacePortProbes
|
|
} from '../ports/workspace-port-ownership'
|
|
|
|
export function registerLocalhostWorktreeLabelHandlers(store: Store): void {
|
|
ipcMain.handle(
|
|
'localhostWorktreeLabels:register',
|
|
async (_event, rawArgs: unknown): Promise<LocalhostWorktreeLabelResult> => {
|
|
const route = parseRegisterArgs(rawArgs)
|
|
// Why: the proxy will forward to any host it's given, so we restrict the
|
|
// target to loopback or a host:port that matches a live workspace port —
|
|
// otherwise this IPC is an open proxy / SSRF vector.
|
|
await assertAllowedTarget(store, route.targetUrl)
|
|
return localhostWorktreeLabelProxy.registerRoute(route)
|
|
}
|
|
)
|
|
}
|
|
|
|
async function assertAllowedTarget(store: Store, targetUrl: string): Promise<void> {
|
|
let parsed: URL
|
|
try {
|
|
parsed = new URL(targetUrl)
|
|
} catch {
|
|
throw new Error('Localhost label target is not a valid URL.')
|
|
}
|
|
const targetHost = normalizeLocalhostHostname(parsed.hostname)
|
|
if (LOOPBACK_LOCALHOST_HOSTS.has(targetHost)) {
|
|
return
|
|
}
|
|
|
|
// Why: URL drops the port for protocol defaults (e.g. http://host/ on 80),
|
|
// so compare against the effective port rather than the raw (empty) string.
|
|
const targetPort = parsed.port || (parsed.protocol === 'https:' ? '443' : '80')
|
|
const scan = await scanWorkspacePortProbes(getStoreWorkspacePortProbes(store))
|
|
const matches = scan.ports.some((port) => {
|
|
if (String(port.port) !== targetPort) {
|
|
return false
|
|
}
|
|
if (normalizeLocalhostHostname(port.connectHost) === targetHost) {
|
|
return true
|
|
}
|
|
const advertisedUrl = 'advertisedUrl' in port ? port.advertisedUrl : undefined
|
|
if (!advertisedUrl) {
|
|
return false
|
|
}
|
|
try {
|
|
return normalizeLocalhostHostname(new URL(advertisedUrl).hostname) === targetHost
|
|
} catch {
|
|
return false
|
|
}
|
|
})
|
|
if (!matches) {
|
|
throw new Error('Localhost label target is not an allowed workspace port.')
|
|
}
|
|
}
|
|
|
|
function parseRegisterArgs(value: unknown): LocalhostWorktreeLabelRoute {
|
|
if (!value || typeof value !== 'object') {
|
|
throw new Error('Invalid localhost label route.')
|
|
}
|
|
const candidate = value as Record<string, unknown>
|
|
const targetUrl = readRequiredString(candidate.targetUrl, 'targetUrl')
|
|
const projectName = readRequiredString(candidate.projectName, 'projectName')
|
|
const worktreeName = readRequiredString(candidate.worktreeName, 'worktreeName')
|
|
return {
|
|
targetUrl,
|
|
projectName,
|
|
worktreeName,
|
|
repoId: readOptionalString(candidate.repoId),
|
|
worktreeId: readOptionalString(candidate.worktreeId)
|
|
}
|
|
}
|
|
|
|
function readRequiredString(value: unknown, field: string): string {
|
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
throw new Error(`Invalid localhost label ${field}.`)
|
|
}
|
|
return value.trim()
|
|
}
|
|
|
|
function readOptionalString(value: unknown): string | null {
|
|
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null
|
|
}
|