feat: add scoped ssh client passthrough

This commit is contained in:
Codex
2026-06-02 07:39:21 +00:00
parent 2a4f6d7791
commit 0c28961f9a
7 changed files with 116 additions and 7 deletions
+17 -3
View File
@@ -56,6 +56,7 @@ export interface AutoRemoteCiPublishPlan {
interface FrontendSession {
baseUrl: string;
cookie: string;
sshClientToken: string | null;
}
interface FetchJsonResult {
@@ -519,7 +520,16 @@ async function loginFrontend(host: string, config: UniDeskConfig): Promise<Front
}
const cookie = res.responseHeaders?.["set-cookie"]?.split(";")[0] ?? "";
if (cookie.length === 0) throw new RemoteCliFailure("auth-missing", `frontend login via ${baseUrl} did not return a session cookie`, { baseUrl, status: res.status ?? null });
return { baseUrl, cookie };
return { baseUrl, cookie, sshClientToken: null };
}
function sshClientTokenFromEnv(env: NodeJS.ProcessEnv = process.env): string | null {
const token = env.UNIDESK_SSH_CLIENT_TOKEN?.trim() ?? "";
return token.length > 0 ? token : null;
}
function scopedSshFrontendSession(host: string, config: UniDeskConfig, token: string): FrontendSession {
return { baseUrl: frontendBaseUrl(host, config), cookie: "", sshClientToken: token };
}
async function frontendJson(session: FrontendSession, path: string, init?: RequestInit, timeoutMs = 8000, maxResponseBytes = 5_000_000): Promise<FetchJsonResult> {
@@ -901,7 +911,10 @@ function openFrontendSshWebSocket(session: FrontendSession): WebSocket {
url: string,
options?: { headers?: Record<string, string> },
) => WebSocket;
return new WebSocketWithHeaders(frontendSshWebSocketUrl(session), { headers: { cookie: session.cookie } });
const headers = session.sshClientToken === null
? { cookie: session.cookie }
: { authorization: `Bearer ${session.sshClientToken}` };
return new WebSocketWithHeaders(frontendSshWebSocketUrl(session), { headers });
}
async function runRemoteSshWebSocket(
@@ -1287,8 +1300,9 @@ async function runRemoteCliOverFrontend(options: RemoteCliOptions, config: UniDe
const args = options.args.length === 0 ? ["help"] : options.args;
const name = commandName(args);
try {
const session = await loginFrontend(options.host, config);
const [top, sub] = args;
const scopedSshToken = top === "ssh" ? sshClientTokenFromEnv() : null;
const session = scopedSshToken === null ? await loginFrontend(options.host, config) : scopedSshFrontendSession(options.host, config, scopedSshToken);
if (top === "help" || top === "--help" || top === "-h") {
emitRemoteJson(name, {
transport: "frontend",
@@ -1187,6 +1187,7 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
const remoteSource = readFileSync(new URL("./src/remote.ts", import.meta.url), "utf8");
assertCondition(remoteSource.includes("UNIDESK_REMOTE_HTTP_CLIENT") && remoteSource.includes("isCodeQueueRunnerEnv(env) ? \"curl\" : \"fetch\""), "remote frontend transport must default to curl HTTP in Code Queue runner environments", remoteSource);
assertCondition(remoteSource.includes("frontendSshWebSocketUrl") && remoteSource.includes("runRemoteSshWebSocket"), "remote frontend ssh must go through the streaming websocket implementation", remoteSource);
assertCondition(remoteSource.includes("UNIDESK_SSH_CLIENT_TOKEN") && remoteSource.includes("authorization: `Bearer ${session.sshClientToken}`"), "remote frontend ssh must support scoped bearer-token clients without frontend admin login", remoteSource);
assertCondition(!remoteSource.includes("remote frontend transport does not stream stdin"), "remote frontend ssh must not reject stdin-backed helpers", remoteSource);
assertCondition(!remoteSource.includes("source: \"cli-remote-ssh\""), "remote frontend ssh must not use host.ssh dispatch task polling", remoteSource);
@@ -1200,6 +1201,8 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
assertCondition(frontendSource.includes('url.pathname === "/ws/ssh"') && frontendSource.includes("proxySshWebSocket"), "frontend must expose an authenticated /ws/ssh proxy", frontendSource);
assertCondition(frontendSource.includes("coreSshWebSocketUrl") && frontendSource.includes('url.searchParams.set("token"'), "frontend /ws/ssh proxy must connect to backend-core ssh bridge with the provider token", frontendSource);
assertCondition(frontendSource.includes("PROVIDER_TOKEN_FILE") && frontendSource.includes("/run/secrets/unidesk_provider_token"), "frontend ssh proxy must support file-based provider token injection for runtime hotfix and secret mounts", frontendSource);
assertCondition(frontendSource.includes("UNIDESK_SSH_CLIENT_TOKEN") && frontendSource.includes("UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST"), "frontend ssh proxy must support scoped client-token configuration", frontendSource);
assertCondition(frontendSource.includes("route-not-allowed") && frontendSource.includes("sshRouteAllowed") && frontendSource.includes("ssh.open"), "frontend ssh proxy must reject disallowed scoped-client routes before opening a provider session", frontendSource);
const composeSource = readFileSync(new URL("../docker-compose.yml", import.meta.url), "utf8");
assertCondition(composeSource.includes('PROVIDER_TOKEN: "${UNIDESK_PROVIDER_TOKEN}"'), "frontend compose service must receive provider token for the ssh proxy", composeSource);