Files
orca/src/shared/workspace-ports.ts
T
OrcaWinandm4air 180e8c186f feat(ports): host-scoped port scan and stop on the server (#27188)
* feat(ports): host-scoped port scan and stop on the server

Add workspacePorts.scanHost/killHost, addressed by workspace and resolved on the
server to the workspace's own execution host (this machine or one of its SSH
hosts). Stop is bound to the host its row was scanned on and is refused on SSH
hosts, where ownership cannot be proven. Advertised as
workspace-ports.host-scoped.v1.

* fix(ports): only this host's workspaces authorize Stop; a failed relay scan is not empty

Port ownership probes now come from host routing instead of the repo row's
connectionId, so an SSH repo that carries only executionHostId (or a runtime-
hosted repo) can no longer make a local listener look workspace-owned.

The relay's ports.detect now fails when it could not read the socket table
(unreadable /proc/net/tcp, or neither Windows scanner answering) instead of
answering an empty list, so a host scan reports unavailable rather than none.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-10 01:27:49 -07:00

89 lines
2.2 KiB
TypeScript

export type WorkspacePortProbe = {
id: string
repoId: string
displayName: string
path: string
}
export type WorkspacePortAttributionConfidence = 'cwd' | 'command' | 'none'
export type WorkspacePortOwner = {
worktreeId: string
repoId: string
displayName: string
path: string
confidence: WorkspacePortAttributionConfidence
}
type WorkspacePortBase = {
id: string
/** Address reported by the OS listener. May be a wildcard bind. */
bindHost: string
/** Address the renderer should copy/open. Wildcard binds are normalized to localhost. */
connectHost: string
port: number
pid?: number
processName?: string
protocol: 'http' | 'https' | 'unknown'
}
export type WorkspacePort =
| (WorkspacePortBase & {
kind: 'workspace'
owner: WorkspacePortOwner
/** Origin captured from terminal output (e.g. Vite's `Network: https://...:3001/`).
* Only set when a workspace-attributed PTY printed a URL whose port matches
* this listener. Origin only — never includes path, query, fragment, or
* userinfo. Prefer this over `protocol://connectHost:port` for the open and
* copy-link actions. */
advertisedUrl?: string
})
| (WorkspacePortBase & {
kind: 'container'
})
| (WorkspacePortBase & {
kind: 'external'
})
export type WorkspacePortScanRequest = {
repoId?: string
}
export type WorkspacePortAdvertisedUrlChangedEvent = {
worktreeId: string
port: number
}
export type WorkspacePortKillRequest = {
repoId?: string
pid: number
port: number
}
export type WorkspacePortKillResult =
| { ok: true }
| {
ok: false
reason: string
}
export type WorkspacePortScanResult = {
platform: NodeJS.Platform | 'unknown'
scannedAt: number
ports: WorkspacePort[]
unavailableReason?: string
}
/** A scan of one execution host, named endpoint-relative (`local` or `ssh:<target>` as the host sees it). */
export type WorkspacePortHostScanResult = WorkspacePortScanResult & {
executionHostId: string
}
export type WorkspacePortHostKillRequest = {
worktree: string
/** The host the row was scanned on; Stop is refused when the workspace now resolves elsewhere. */
executionHostId: string
pid: number
port: number
}