Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ export async function deleteCredential(
* Entries are keyed by connection name only. Only `saveConnection` writes them, for the saved (global) connection
* of that name; a project's own connections file never stores a secret here. A project file that defines a
* connection of the same name without its secret was reading the saved connection's secret, and loses it when that
* saved connection is removed or re-saved without it.
* saved connection is removed or re-saved without it. The registry keeps the entries on removal while an env var or
* the local config of a project this process currently serves still names the connection; projects it does not
* serve (closed, never opened, or in another process) are not consulted.
*/
export async function forgetCredentials(name: string, keep?: ConnectionConfig): Promise<string[]> {
const store = await getBackend()
Expand Down
418 changes: 283 additions & 135 deletions packages/opencode/src/altimate/native/connections/registry.ts

Large diffs are not rendered by default.

248 changes: 133 additions & 115 deletions packages/opencode/src/altimate/native/connections/ssh-tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,29 @@
*
* Uses the `ssh2` package (dynamic import). If ssh2 is not installed,
* throws a clear error message.
*
* Tunnels are not keyed by connection name. Each `openTunnel()` call returns a
* handle owned by the caller (the connection registry's connect attempt), and
* only that handle closes it: an attempt that was superseded can never register
* over, or close, a newer attempt's tunnel.
*/

import type { ConnectionConfig } from "@altimateai/drivers"
import type { Server, Socket } from "net"
import type { Duplex } from "stream"

export interface TunnelInfo {
localPort: number
close(): void
}

/** A tunnel being opened. `close()` works before or after `ready` settles and is idempotent. */
export interface TunnelHandle {
/** Resolves once the local forwarding port listens; rejects on failure or when closed first. */
readonly ready: Promise<TunnelInfo>
close(): void
}

export interface SshConfig {
ssh_host: string
ssh_port?: number
Expand All @@ -22,161 +36,165 @@ export interface SshConfig {
port: number
}

/** Active tunnels keyed by connection name. */
const activeTunnels = new Map<string, TunnelInfo>()
/** Every tunnel opened and not yet closed, for process-exit cleanup and `closeAllTunnels()`. */
const openTunnels = new Set<TunnelHandle>()

/** Clean up all tunnels on process exit. */
let cleanupRegistered = false
function ensureCleanup(): void {
if (cleanupRegistered) return
cleanupRegistered = true
const cleanup = () => {
for (const [, tunnel] of activeTunnels) {
try {
tunnel.close()
} catch {
// best-effort cleanup
}
}
activeTunnels.clear()
}
process.once("exit", cleanup)
process.once("exit", closeAllTunnels)
process.once("SIGINT", () => {
cleanup()
closeAllTunnels()
process.exit(0)
})
process.once("SIGTERM", () => {
cleanup()
closeAllTunnels()
process.exit(0)
})
}

/**
* Start an SSH tunnel for a connection.
* Returns a TunnelInfo with the local port to connect to.
* Open an SSH tunnel that forwards a local port to `config.host:config.port`.
*
* One terminal state covers every path: an error, a close before `ready`, or a
* late `ready`/listen callback after either closes the client, the local server
* and forwarded sockets, and never publishes a tunnel.
*/
export async function startTunnel(
name: string,
config: SshConfig,
): Promise<TunnelInfo> {
// Close existing tunnel for this name
const existing = activeTunnels.get(name)
if (existing) {
existing.close()
activeTunnels.delete(name)
}

let ssh2: any
try {
// @ts-expect-error — optional dependency
ssh2 = await import("ssh2")
} catch {
throw new Error(
"SSH tunnel requires the ssh2 package. Run: bun add ssh2 @types/ssh2",
)
export function openTunnel(config: SshConfig): TunnelHandle {
let closed = false
let client: any
let server: Server | undefined
Comment thread
anandgupta42 marked this conversation as resolved.
const sockets = new Set<Socket>()
let rejectReady: ((e: Error) => void) | undefined

const close = () => {
if (closed) return
closed = true
openTunnels.delete(handle)
for (const socket of sockets) socket.destroy()
try {
server?.close()
} catch {
// ignore
}
try {
client?.end()
} catch {
// ignore
}
rejectReady?.(new Error("SSH tunnel was closed before it was ready"))
}

ensureCleanup()

const net = await import("net")

return new Promise<TunnelInfo>((resolve, reject) => {
const client = new ssh2.Client()

client.on("ready", () => {
// Create a local TCP server that forwards to remote host:port via SSH
const server = net.createServer((localSocket) => {
client.forwardOut(
"127.0.0.1",
0,
config.host,
config.port,
(err: Error | undefined, stream: any) => {
if (err) {
const ready = (async (): Promise<TunnelInfo> => {
let ssh2: any
try {
// @ts-expect-error — optional dependency
ssh2 = await import("ssh2")
} catch {
throw new Error("SSH tunnel requires the ssh2 package. Run: bun add ssh2 @types/ssh2")
}
const net = await import("net")
if (closed) throw new Error("SSH tunnel was closed before it was ready")
ensureCleanup()

return new Promise<TunnelInfo>((resolve, reject) => {
rejectReady = reject
client = new ssh2.Client()

client.on("ready", () => {
if (closed) return
// Create a local TCP server that forwards to remote host:port via SSH
server = net.createServer((localSocket) => {
let remoteStream: Duplex | undefined
localSocket.on("error", () => {
remoteStream?.destroy()
localSocket.destroy()
})
if (closed) {
localSocket.destroy()
return
}
sockets.add(localSocket)
localSocket.on("close", () => sockets.delete(localSocket))
client.forwardOut("127.0.0.1", 0, config.host, config.port, (err: Error | undefined, stream: any) => {
remoteStream = stream
stream?.on("error", () => {
localSocket.destroy()
stream.destroy()
})
if (err || closed || localSocket.destroyed) {
stream?.destroy()
localSocket.destroy()
return
}
localSocket.pipe(stream).pipe(localSocket)
},
)
})
Comment thread
anandgupta42 marked this conversation as resolved.
})

server.on("error", (err: Error) => {
reject(new Error(`SSH tunnel local server error: ${err.message}`))
close()
})

server.listen(0, "127.0.0.1", () => {
if (closed) return
const addr = server?.address()
const localPort = typeof addr === "object" && addr ? addr.port : 0
resolve({ localPort, close })
})
})

server.listen(0, "127.0.0.1", () => {
const addr = server.address()
const localPort =
typeof addr === "object" && addr ? addr.port : 0

const tunnelInfo: TunnelInfo = {
localPort,
close() {
try {
server.close()
} catch {
// ignore
}
try {
client.end()
} catch {
// ignore
}
activeTunnels.delete(name)
},
}

activeTunnels.set(name, tunnelInfo)
resolve(tunnelInfo)
client.on("error", (err: Error) => {
reject(new Error(`SSH connection error: ${err.message}`))
close()
})

server.on("error", (err: Error) => {
client.end()
reject(new Error(`SSH tunnel local server error: ${err.message}`))
// A clean remote disconnect ends the tunnel too: before ready it rejects, after it closes the listener.
client.on("close", () => {
reject(new Error("SSH connection closed"))
close()
})
})

client.on("error", (err: Error) => {
reject(new Error(`SSH connection error: ${err.message}`))
})

const connectOptions: Record<string, unknown> = {
host: config.ssh_host,
port: config.ssh_port ?? 22,
username: config.ssh_user ?? "root",
}

if (config.ssh_private_key) {
connectOptions.privateKey = config.ssh_private_key
} else if (config.ssh_password) {
connectOptions.password = config.ssh_password
}
const connectOptions: Record<string, unknown> = {
host: config.ssh_host,
port: config.ssh_port ?? 22,
username: config.ssh_user ?? "root",
}

client.connect(connectOptions)
})
}
if (config.ssh_private_key) {
connectOptions.privateKey = config.ssh_private_key
} else if (config.ssh_password) {
connectOptions.password = config.ssh_password
}

/** Get an active tunnel by connection name. */
export function getActiveTunnel(name: string): TunnelInfo | undefined {
return activeTunnels.get(name)
}
client.connect(connectOptions)
})
})()

/** Close a specific tunnel by name. */
export function closeTunnel(name: string): void {
const tunnel = activeTunnels.get(name)
if (tunnel) {
tunnel.close()
activeTunnels.delete(name)
}
const handle: TunnelHandle = { ready, close }
openTunnels.add(handle)
// Any failure ends in the terminal state; the caller still observes the rejection through `ready`.
ready.catch(() => close())
return handle
}

/** Close all active tunnels. */
/** Close every open tunnel (process teardown and tests). */
export function closeAllTunnels(): void {
for (const [, tunnel] of activeTunnels) {
for (const tunnel of [...openTunnels]) {
try {
tunnel.close()
} catch {
// best-effort
}
}
activeTunnels.clear()
openTunnels.clear()
}

/** Number of tunnels currently open or opening (diagnostics and tests). */
export function openTunnelCount(): number {
return openTunnels.size
}

/**
Expand Down
6 changes: 3 additions & 3 deletions packages/opencode/src/altimate/tools/warehouse-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as Precedence from "../workspace/precedence"
// altimate_change end

// altimate_change start — point at the locations the registry actually reads
import { CONN_ENV_PREFIX, globalConfigPath, localConfigPath, projectConfigApplies } from "../native/connections/registry"
import { CONN_ENV_PREFIX, globalConfigPath, localConfigPath } from "../native/connections/registry"

/** The no-connections hint. Paths come from the registry's own helpers so it cannot drift from what is loaded. */
export function noConnectionsMessage(): string {
Expand All @@ -17,8 +17,8 @@ export function noConnectionsMessage(): string {
"",
"Alternatively, connections are read from these sources (later ones override earlier ones):",
` 1. ${globalConfigPath()} (global)`,
...(projectConfigApplies() ? [` 2. ${localConfigPath()} (this project)`] : []),
` ${projectConfigApplies() ? 3 : 2}. ${CONN_ENV_PREFIX}<NAME> environment variables, each holding one connection's config as JSON (set them in the shell before starting altimate-code; a running process cannot see variables exported later)`,
` 2. ${localConfigPath()} (this project)`,
` 3. ${CONN_ENV_PREFIX}<NAME> environment variables, each holding one connection's config as JSON (set them in the shell before starting altimate-code; a running process cannot see variables exported later)`,
"",
"After adding the first connection file, call warehouse_list again. Edits to an existing connections.json take effect after a restart. Example connections.json:",
"{",
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/project/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ export const Instance = {
return input.fn()
})
},
// altimate_change start — enumerate open projects even before their warehouse registry is loaded
directories(): string[] {
return [...cache.keys()]
},
// altimate_change end
get current() {
return context.use()
},
Expand Down
Loading
Loading