import { spawn, spawnSync } from "node:child_process"; import { createHash, randomBytes } from "node:crypto"; import { appendFileSync, mkdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { type UniDeskConfig, repoRoot } from "./config"; import { decodeApplyPatchV2BulkRead, formatApplyPatchV2BulkReplacementPayload, isApplyPatchV2HelpArgs, runApplyPatchV2, type ApplyPatchV2BulkReplacementWritePlan, type ApplyPatchV2Executor, type ApplyPatchV2FileSystem, } from "./apply-patch-v2"; import { isSshFileTransferOperation, runSshFileTransferOperation, type SshRemoteCommandExecutor, type SshRemoteCommandStreamHandlers, } from "./ssh-file-transfer"; export interface ParsedSshArgs { remoteCommand: string | null; requiresStdin: boolean; invocationKind: SshInvocationKind; stdinPrefix?: string; stdinSuffix?: string; requiredHelpers?: SshHelperName[]; } export type SshInvocationKind = "interactive" | "argv" | "helper" | "ssh-like"; export type SshHelperName = "apply_patch" | "glob" | "skill-discover"; export interface ParsedSshRoute { providerId: string; plane: "host" | "k3s" | "win"; entry: string | null; namespace: string | null; resource: string | null; container: string | null; workspace: string | null; raw: string; } export interface ParsedSshInvocation { providerId: string; route: ParsedSshRoute; parsed: ParsedSshArgs; } export interface SshCaptureResult { exitCode: number; stdout: string; stderr: string; } export type SshCaptureBackend = "local-backend-core-broker" | "remote-frontend-websocket"; export interface SshCaptureBackendPlan { backend: SshCaptureBackend; remoteHost: string | null; reason: string; localBackendCore: { dockerExecutable: boolean; backendCoreContainer: boolean; error: string | null; }; } export interface SshFailureHint { code: "ssh-like-command-friction"; providerId: string; trigger: "timeout-or-kex" | "exit-255"; exitCode: number; message: string; try: string; triage: string; note: string; } export interface SshRuntimeTimingHint { code: "ssh-runtime-timing"; level: "info" | "warning"; providerId: string; route: string; transport: "backend-core-broker" | "frontend-websocket"; invocationKind: SshInvocationKind; exitCode: number; elapsedMs: number; elapsedSeconds: number; thresholdMs: number; slow: boolean; message: string; note: string; } export interface SshRuntimeTimeoutHint { code: "ssh-runtime-timeout"; level: "warning"; providerId: string; route: string; transport: "backend-core-broker" | "frontend-websocket"; invocationKind: SshInvocationKind; timeoutMs: number; timeoutSeconds: number; message: string; action: string; note: string; } export interface SshStdoutTruncationHint { code: "ssh-stdout-truncated"; level: "warning"; providerId: string; route: string; transport: "backend-core-broker" | "frontend-websocket"; invocationKind: SshInvocationKind; thresholdBytes: number; observedBytesAtTruncation: number; dumpPath: string | null; dumpError: string | null; message: string; action: string; note: string; } const argvQuotedSshSubcommands = new Set(["git", "rg", "grep", "sed", "nl", "stat", "du", "ls", "cat", "head", "tail", "wc", "pwd"]); const nativeK3sKubeconfig = "/etc/rancher/k3s/k3s.yaml"; const windowsBridgeCwd = "/mnt/c/Windows"; const windowsPowerShellExePath = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"; const windowsCmdExeNativePath = "C:\\Windows\\System32\\cmd.exe"; const defaultSshSlowWarningMs = 10_000; const defaultSshRuntimeTimeoutMs = 60_000; const maxSshRuntimeTimeoutMs = 60_000; const defaultSshStdoutStreamMaxBytes = 256 * 1024; const minSshStdoutStreamMaxBytes = 4 * 1024; const maxSshStdoutStreamMaxBytes = 16 * 1024 * 1024; const sshStdoutDumpDir = join(tmpdir(), "unidesk-cli-output"); export const sshShellCompatibilityPrelude = 'printf(){ if [ "${1+x}" = x ] && [ "$1" = "-v" ] && [ -n "${BASH_VERSION:-}" ]; then command printf "$@"; return $?; fi; if [ "${1+x}" = x ] && [ "$1" = "--" ]; then shift; fi; command printf -- "$@"; }'; export const sshUserToolPathPrelude = [ 'for unidesk_path_dir in "$HOME/.bun/bin" "$HOME/.local/bin" "$HOME/bin" "/root/.bun/bin"; do', ' [ -d "$unidesk_path_dir" ] || continue', ' case ":$PATH:" in', ' *":$unidesk_path_dir:"*) ;;', ' *) PATH="$unidesk_path_dir:$PATH" ;;', " esac", "done", "export PATH", ].join("\n"); const k3sResourceKindAliases = new Set(["pod", "po", "pods", "deployment", "deploy", "deployments", "statefulset", "sts", "daemonset", "ds", "job", "jobs"]); const k3sPodRoutePrefixes = ["pod:", "po:", "pods:"]; const legacyK3sOperationRouteSegments = new Set([ "guard", "kubectl", "exec", "script", "apply-patch", "apply-patch-v1", "patch", "patch-v1", "v2", "logs", "get", "describe", "top", "rollout", "wait", "config", "version", "cluster-info", "auth", "api-resources", "api-versions", ]); export const remoteApplyPatchSource = String.raw`#!/bin/sh set -eu allow_loose=0 die() { printf 'apply_patch: %s\n' "$*" >&2 exit 1 } mk_temp() { mktemp "${"$"}{TMPDIR:-/tmp}/unidesk-apply-patch.XXXXXX" || exit 1 } ensure_parent() { case "$1" in */*) dir=${"$"}{1%/*} [ -n "$dir" ] || dir=/ mkdir -p "$dir" ;; esac } read_text_preserve_newlines() { marker="__UNIDESK_APPLY_PATCH_EOF_$$__" text=$(cat "$1"; printf '%s' "$marker") || die "failed to read $1" printf '%s' "${"$"}{text%"$marker"}" } write_file() { target=$1 source=$2 ensure_parent "$target" cp "$source" "$target" || die "failed to write $target" } line_number_for_prefix() { newline_count=$(printf '%s' "$1" | tr -cd '\n' | wc -c | tr -d ' ') printf '%s\n' $((newline_count + 1)) } replace_once_with_perl() { command -v perl >/dev/null 2>&1 || return 127 perl -0777 -e ' use strict; use warnings; sub fail { print STDERR "apply_patch: ", @_, "\n"; exit 1; } sub read_all { my ($path, $label) = @_; open my $fh, "<", $path or fail("failed to read $label"); binmode $fh; local $/; my $data = <$fh>; close $fh; return defined $data ? $data : ""; } my ($target, $search_file, $replace_file, $hunk_id, $allow_loose, $out) = @ARGV; my $old = read_all($target, $target); my $search = read_all($search_file, "hunk search"); my $replacement = read_all($replace_file, "hunk replacement"); my $new; if ($search eq "") { fail("hunk $hunk_id in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review") if $allow_loose ne "1"; print STDERR "apply_patch: hunk $hunk_id matched $target:1 (loose)\n"; $new = $replacement . $old; } else { my $pos = -1; my $offset = 0; my $count = 0; while (1) { my $found = index($old, $search, $offset); last if $found < 0; $pos = $found if $count == 0; $count += 1; last if $count > 1 && $allow_loose ne "1"; $offset = $found + length($search); } fail("hunk $hunk_id context not found in $target") if $count == 0; fail("hunk $hunk_id context matched multiple locations in $target; add more unchanged context or pass --allow-loose after manual review") if $count > 1 && $allow_loose ne "1"; my $prefix = substr($old, 0, $pos); my $suffix = substr($old, $pos + length($search)); my $line = ($prefix =~ tr/\n//) + 1; print STDERR "apply_patch: hunk $hunk_id matched $target:$line\n"; $new = $prefix . $replacement . $suffix; } open my $ofh, ">", $out or fail("failed to render patched file"); binmode $ofh; print $ofh $new or fail("failed to render patched file"); close $ofh or fail("failed to render patched file"); ' "$@" } replace_once() { target=$1 search_file=$2 replace_file=$3 hunk_id=$4 [ -e "$target" ] || die "file not found: $target" fast_out=$(mk_temp) if replace_once_with_perl "$target" "$search_file" "$replace_file" "$hunk_id" "$allow_loose" "$fast_out"; then write_file "$target" "$fast_out" rm -f "$fast_out" return 0 else status=$? fi rm -f "$fast_out" [ "$status" = 127 ] || exit "$status" marker="__UNIDESK_APPLY_PATCH_EOF_$$__" old=$(cat "$target"; printf '%s' "$marker") || die "failed to read $target" old=${"$"}{old%"$marker"} search=$(cat "$search_file"; printf '%s' "$marker") || die "failed to read hunk search" search=${"$"}{search%"$marker"} replacement=$(cat "$replace_file"; printf '%s' "$marker") || die "failed to read hunk replacement" replacement=${"$"}{replacement%"$marker"} if [ -z "$search" ]; then [ "$allow_loose" = 1 ] || die "hunk $hunk_id in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review" printf 'apply_patch: hunk %s matched %s:1 (loose)\n' "$hunk_id" "$target" >&2 new=$replacement$old else case "$old" in *"$search"*) prefix=${"$"}{old%%"$search"*} suffix=${"$"}{old#*"$search"} case "$suffix" in *"$search"*) [ "$allow_loose" = 1 ] || die "hunk $hunk_id context matched multiple locations in $target; add more unchanged context or pass --allow-loose after manual review" ;; esac printf 'apply_patch: hunk %s matched %s:%s\n' "$hunk_id" "$target" "$(line_number_for_prefix "$prefix")" >&2 new=$prefix$replacement$suffix ;; *) die "hunk $hunk_id context not found in $target" ;; esac fi out=$(mk_temp) printf '%s' "$new" > "$out" || die "failed to render patched file" write_file "$target" "$out" rm -f "$out" } apply_update() { target=$1 body=$2 in_hunk=0 search_file= replace_file= changed=0 hunk_number=0 has_add=0 has_delete=0 seen_add=0 anchor_before_add=0 anchor_after_add=0 explicit_eof=0 finish_hunk() { [ "$in_hunk" = 1 ] || return 0 if [ "$allow_loose" != 1 ]; then [ -s "$search_file" ] || die "hunk $hunk_number in $target has no context; add unchanged/deleted anchor lines or pass --allow-loose after manual review" if [ "$has_add" = 1 ] && [ "$has_delete" = 0 ]; then if [ "$anchor_before_add" != 1 ] || { [ "$anchor_after_add" != 1 ] && [ "$explicit_eof" != 1 ]; }; then die "hunk $hunk_number in $target is insert-only without both leading and trailing context; include nearby unchanged lines after the insertion or pass --allow-loose after manual review" fi fi fi replace_once "$target" "$search_file" "$replace_file" "$hunk_number" rm -f "$search_file" "$replace_file" search_file= replace_file= in_hunk=0 changed=1 } while IFS= read -r line || [ -n "$line" ]; do case "$line" in "*** End of File"*) continue ;; "@@"*) finish_hunk search_file=$(mk_temp) replace_file=$(mk_temp) hunk_number=$((hunk_number + 1)) has_add=0 has_delete=0 seen_add=0 anchor_before_add=0 anchor_after_add=0 explicit_eof=0 in_hunk=1 continue ;; esac [ "$in_hunk" = 1 ] || die "expected hunk header in $target" case "$line" in " "*) text=${"$"}{line#?} printf '%s\n' "$text" >> "$search_file" printf '%s\n' "$text" >> "$replace_file" if [ "$seen_add" = 1 ]; then anchor_after_add=1; else anchor_before_add=1; fi ;; "-"*) text=${"$"}{line#?} printf '%s\n' "$text" >> "$search_file" has_delete=1 if [ "$seen_add" = 1 ]; then anchor_after_add=1; else anchor_before_add=1; fi ;; "+"*) text=${"$"}{line#?} printf '%s\n' "$text" >> "$replace_file" has_add=1 seen_add=1 ;; "\\ No newline at end of file") ;; "*** End of File"*) explicit_eof=1 ;; *) die "bad hunk line in $target: $line" ;; esac done < "$body" finish_hunk [ "$changed" = 1 ] || [ -e "$target" ] || die "file not found: $target" } is_top_header() { case "$1" in "*** Add File: "*|"*** Update File: "*|"*** Delete File: "*|"*** End Patch") return 0 ;; *) return 1 ;; esac } pushed=0 pushed_line= next_patch_line() { if [ "$pushed" = 1 ]; then line=$pushed_line pushed=0 pushed_line= return 0 fi if IFS= read -r line; then return 0 fi [ -n "${"$"}{line:-}" ] } push_patch_line() { pushed_line=$1 pushed=1 } parse_add_file() { target=$1 [ ! -e "$target" ] || die "file already exists: $target" tmp=$(mk_temp) while next_patch_line; do if is_top_header "$line"; then push_patch_line "$line" break fi case "$line" in "+"*) printf '%s\n' "${"$"}{line#?}" >> "$tmp" ;; *) rm -f "$tmp" die "add file lines must start with + for $target" ;; esac done write_file "$target" "$tmp" rm -f "$tmp" } parse_update_file() { target=$1 move_to= body=$(mk_temp) if next_patch_line; then case "$line" in "*** Move to: "*) move_to=${"$"}{line#"*** Move to: "} ;; *) push_patch_line "$line" ;; esac fi while next_patch_line; do if is_top_header "$line"; then push_patch_line "$line" break fi case "$line" in "*** Move to: "*) rm -f "$body" die "move marker must appear before update hunks" ;; *) printf '%s\n' "$line" >> "$body" ;; esac done if [ -s "$body" ]; then apply_update "$target" "$body" elif [ -z "$move_to" ] && [ ! -e "$target" ]; then rm -f "$body" die "file not found: $target" fi rm -f "$body" if [ -n "$move_to" ]; then [ -e "$target" ] || die "file not found: $target" [ ! -e "$move_to" ] || die "target file already exists: $move_to" ensure_parent "$move_to" mv "$target" "$move_to" || die "failed to move $target to $move_to" fi } main() { while [ "$#" -gt 0 ]; do case "${"$"}1" in -h|--help) printf 'apply_patch: sh-only helper; reads *** Begin Patch format from stdin; --allow-loose bypasses low-context hunk guards\n' return 0 ;; --allow-loose) allow_loose=1 shift ;; *) die "unsupported option: ${"$"}1" ;; esac done next_patch_line || die "patch must start with *** Begin Patch" [ "$line" = "*** Begin Patch" ] || die "patch must start with *** Begin Patch" while next_patch_line; do case "$line" in "*** End Patch") printf 'Done!\n' return 0 ;; "*** Add File: "*) parse_add_file "${"$"}{line#"*** Add File: "}" ;; "*** Delete File: "*) target=${"$"}{line#"*** Delete File: "} rm -f "$target" || die "failed to delete $target" ;; "*** Update File: "*) parse_update_file "${"$"}{line#"*** Update File: "}" ;; *) die "unexpected patch line: $line" ;; esac done die "missing *** End Patch" } main "$@" `; const remoteGlobSource = String.raw`#!/usr/bin/env python3 import argparse import glob import os import sys def main(): parser = argparse.ArgumentParser(description="remote glob helper for UniDesk ssh passthrough") parser.add_argument("patterns", nargs="*", help="glob patterns relative to --root unless absolute") parser.add_argument("--root", default=".", help="base directory for relative patterns") parser.add_argument("--pattern", action="append", default=[], help="additional glob pattern") parser.add_argument("--contains", action="append", default=[], help="match path names containing text") parser.add_argument("--icontains", action="append", default=[], help="case-insensitive contains match") parser.add_argument("--type", choices=["any", "f", "d"], default="any", help="filter by any/file/dir") parser.add_argument("--limit", type=int, default=0, help="maximum number of rows to print") parser.add_argument("--sort", action="store_true", help="sort output") parser.add_argument("--absolute", action="store_true", help="print absolute paths") args = parser.parse_args() if args.limit < 0: print("glob: --limit must be >= 0", file=sys.stderr) return 2 root = os.path.abspath(args.root) patterns = list(args.patterns) + list(args.pattern) for text in args.contains: patterns.append(f"**/*{text}*") for text in args.icontains: # Python glob is case-sensitive on Linux, so filter from a broad recursive scan. patterns.append("**/*") if not patterns: patterns = ["*"] seen = set() rows = [] lowered_contains = [text.lower() for text in args.icontains] for pattern in patterns: effective = pattern if os.path.isabs(pattern) else os.path.join(root, pattern) for path in glob.iglob(effective, recursive=True): full = os.path.abspath(path) if full in seen: continue if lowered_contains and not any(text in os.path.basename(full).lower() or text in full.lower() for text in lowered_contains): continue if args.type == "f" and not os.path.isfile(full): continue if args.type == "d" and not os.path.isdir(full): continue seen.add(full) rows.append(full if args.absolute else os.path.relpath(full, root)) if args.sort: rows.sort() if args.limit > 0: rows = rows[:args.limit] for row in rows: print(row) return 0 if __name__ == "__main__": raise SystemExit(main()) `; const remoteSkillDiscoverSource = String.raw`#!/usr/bin/env python3 import argparse import getpass import json import os import platform import socket import sys from datetime import datetime, timezone from pathlib import Path SKIP_PARTS = {"node_modules", ".git", ".state", "logs", "references", "__pycache__"} def is_wsl(): try: release = Path("/proc/sys/kernel/osrelease").read_text(errors="ignore").lower() except Exception: release = "" return "microsoft" in release or "wsl" in release or "WSL_INTEROP" in os.environ def to_windows_path(path): text = str(path) if text.startswith("/mnt/") and len(text) >= 7 and text[5].isalpha() and text[6] == "/": drive = text[5].upper() rest = text[7:].replace("/", "\\") return drive + ":\\" + rest return None def read_bounded(path, limit=16384): try: data = path.read_bytes()[:limit] return data.decode("utf-8", errors="replace") except Exception: return "" def frontmatter_value(line): if ":" not in line: return None key, value = line.split(":", 1) return key.strip().lower(), value.strip().strip("\"'") def parse_skill_metadata(skill_md): text = read_bounded(skill_md) name = skill_md.parent.name description = "" lines = text.splitlines() if lines and lines[0].strip() == "---": for line in lines[1:]: if line.strip() == "---": break item = frontmatter_value(line) if item is None: continue key, value = item if key == "name" and value: name = value if key == "description" and value: description = value if not description: for line in lines: stripped = line.strip() if stripped and not stripped.startswith("---") and not stripped.startswith("#"): description = stripped break return name, description def iter_skill_files(root, max_depth): try: iterator = root.rglob("SKILL.md") for skill_md in iterator: try: rel = skill_md.relative_to(root) except ValueError: continue directory_parts = rel.parts[:-1] if len(directory_parts) == 0 or len(directory_parts) > max_depth: continue if any(part in SKIP_PARTS for part in directory_parts): continue yield skill_md except Exception as exc: raise RuntimeError(str(exc)) from exc def unique_paths(paths): seen = set() output = [] for raw in paths: path = Path(raw).expanduser() key = str(path) if key in seen: continue seen.add(key) output.append(path) return output def default_wsl_roots(): home = Path.home() roots = [home / ".agents" / "skills", home / ".codex" / "skills"] for raw in ("/root/.agents/skills", "/root/.codex/skills"): path = Path(raw) if str(path) not in {str(item) for item in roots}: roots.append(path) return roots def default_windows_roots(): if not is_wsl(): return [] users = Path("/mnt/c/Users") roots = [] try: children = list(users.iterdir()) if users.exists() else [] except Exception: children = [] for child in children: try: if not child.is_dir(): continue except Exception: continue lower = child.name.lower() if lower in {"all users", "default", "default user", "public"}: continue roots.append(child / ".agents" / "skills") roots.append(child / ".codex" / "skills") return roots def scan_root(scope, root, max_depth): try: root_exists = root.exists() root_error = None except Exception as exc: root_exists = False root_error = str(exc) record = { "scope": scope, "path": str(root), "windowsPath": to_windows_path(root), "exists": root_exists, "skillCount": 0, "error": root_error, } skills = [] if not record["exists"]: return record, skills try: for skill_md in iter_skill_files(root, max_depth): name, description = parse_skill_metadata(skill_md) skill = { "scope": scope, "name": name, "description": description, "path": str(skill_md.parent), "skillMd": str(skill_md), "windowsPath": to_windows_path(skill_md.parent), "root": str(root), } skills.append(skill) except Exception as exc: record["error"] = str(exc) record["skillCount"] = len(skills) return record, skills def main(): parser = argparse.ArgumentParser(description="discover WSL/Linux and Windows skill directories from a UniDesk ssh passthrough session") parser.add_argument("--scope", choices=["all", "wsl", "windows"], default="all", help="which skill roots to scan") parser.add_argument("--max-depth", type=int, default=4, help="maximum directory depth below each skill root") parser.add_argument("--limit", type=int, default=0, help="maximum skill rows to return; 0 means unlimited") parser.add_argument("--root", action="append", default=[], help="extra WSL/Linux skill root") parser.add_argument("--windows-root", action="append", default=[], help="extra Windows skill root, expressed as /mnt//...") args = parser.parse_args() if args.max_depth <= 0: print(json.dumps({"ok": False, "error": "--max-depth must be positive"}, ensure_ascii=False)) return 2 if args.limit < 0: print(json.dumps({"ok": False, "error": "--limit must be >= 0"}, ensure_ascii=False)) return 2 roots = [] if args.scope in ("all", "wsl"): roots.extend(("wsl", path) for path in default_wsl_roots()) roots.extend(("wsl", Path(raw).expanduser()) for raw in args.root) if args.scope in ("all", "windows"): roots.extend(("windows", path) for path in default_windows_roots()) roots.extend(("windows", Path(raw).expanduser()) for raw in args.windows_root) seen = set() unique = [] for scope, path in roots: key = (scope, str(path)) if key in seen: continue seen.add(key) unique.append((scope, path)) root_records = [] skills = [] for scope, root in unique: record, found = scan_root(scope, root, args.max_depth) root_records.append(record) skills.extend(found) scope_order = {"wsl": 0, "windows": 1} skills.sort(key=lambda item: (scope_order.get(str(item["scope"]), 9), str(item["name"]).lower(), str(item["path"]))) total_before_limit = len(skills) if args.limit > 0: skills = skills[:args.limit] counts = {"total": len(skills), "totalBeforeLimit": total_before_limit, "wsl": 0, "windows": 0} for skill in skills: scope = str(skill["scope"]) if scope in counts: counts[scope] += 1 payload = { "ok": True, "command": "unidesk ssh skills", "generatedAt": datetime.now(timezone.utc).isoformat(), "node": { "hostname": socket.gethostname(), "user": getpass.getuser(), "home": str(Path.home()), "platform": platform.platform(), "isWsl": is_wsl(), "python": sys.version.split()[0], }, "counts": counts, "roots": root_records, "skills": skills, } print(json.dumps(payload, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main()) `; const sshOptionsWithValue = new Set([ "-B", "-b", "-c", "-D", "-E", "-e", "-F", "-I", "-i", "-J", "-L", "-l", "-m", "-O", "-o", "-p", "-Q", "-R", "-S", "-W", "-w", ]); export function isSshSkillDiscoveryArgs(args: string[]): boolean { const subcommand = args[0] ?? ""; return subcommand === "skills" || subcommand === "skill-discover" || subcommand === "discover-skills" || (subcommand === "skill" && args[1] === "discover"); } export function parseSshArgs(args: string[]): ParsedSshArgs { const subcommand = args[0] ?? ""; if (isSshFileTransferOperation(args)) { return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" }; } if (isSshSkillDiscoveryArgs(args)) { const toolArgs = subcommand === "skill" ? ["skill-discover", ...args.slice(2)] : ["skill-discover", ...args.slice(1)]; return { remoteCommand: shellArgv(toolArgs), requiresStdin: false, invocationKind: "helper", requiredHelpers: ["skill-discover"] }; } if (subcommand === "apply-patch") { if (isApplyPatchV2HelpArgs(args.slice(1))) { return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" }; } return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" }; } if (subcommand === "apply-patch-v1") { const toolArgs = ["apply_patch", ...args.slice(1)]; return { remoteCommand: shellArgv(toolArgs), requiresStdin: true, invocationKind: "helper", requiredHelpers: ["apply_patch"] }; } if (subcommand === "patch" || subcommand === "patch-v1" || subcommand === "v2") { throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper"); } if (subcommand === "py") { return { remoteCommand: buildPythonStdinCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" }; } if (subcommand === "script" || subcommand === "sh") { return buildShellCommand(args.slice(1)); } if (subcommand === "shell") { return buildShellStringCommand(args.slice(1)); } if (subcommand === "argv" || subcommand === "exec") { const toolArgs = args.slice(1); if (toolArgs.length === 0) throw new Error(`ssh ${subcommand} requires a command`); validateDirectArgvCommand(subcommand, toolArgs); return { remoteCommand: shellArgv(toolArgs), requiresStdin: false, invocationKind: "argv" }; } if (subcommand === "find") { return { remoteCommand: buildFindCommand(args.slice(1)), requiresStdin: false, invocationKind: "helper" }; } if (subcommand === "glob") { return { remoteCommand: shellArgv(["glob", ...args.slice(1)]), requiresStdin: false, invocationKind: "helper", requiredHelpers: ["glob"] }; } if (subcommand === "k3s") { throw new Error("ssh k3s shorthand is unsupported; put k3s in the route, for example: trans D601:k3s kubectl get nodes"); } if (argvQuotedSshSubcommands.has(subcommand)) { return { remoteCommand: shellArgv(args), requiresStdin: false, invocationKind: "argv" }; } const remote: string[] = []; let remoteStarted = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index] ?? ""; if (remoteStarted) { remote.push(arg); continue; } if (arg === "--") { remoteStarted = true; continue; } if (arg.startsWith("-") && arg !== "-") { if (sshOptionsWithValue.has(arg) && index + 1 < args.length) index += 1; continue; } remoteStarted = true; remote.push(arg); } return { remoteCommand: remote.length === 0 ? null : remote.join(" "), requiresStdin: false, invocationKind: remote.length === 0 ? "interactive" : "ssh-like", }; } export function normalizeSshOperationArgs(args: string[]): string[] { return args[0] === "--" ? args.slice(1) : args; } export function sshRouteSeparatorCompatibilityHint(rawArgs: string[], normalizedArgs: string[]): string { if (rawArgs === normalizedArgs || rawArgs[0] !== "--") return ""; const operation = normalizedArgs[0] ?? ""; const operationText = operation.length === 0 ? "operation" : `operation \`${operation}\``; return `UNIDESK_SSH_HINT route-level -- is ignored before ${operationText}; canonical form is \`trans ${normalizedArgs.join(" ")}\`. Keep -- only inside operations such as \`script -- \` or \`exec -- \`.\n`; } function validateDirectArgvCommand(commandName: string, toolArgs: string[]): void { if (toolArgs.length !== 1) return; const command = toolArgs[0] ?? ""; if (!looksLikeShellCommandString(command)) return; throw new Error( `ssh ${commandName} received one shell-like command string; ${commandName} executes a single process and treats that string as the executable path. ` + `Use \`trans script -- ${shellQuote(command)}\` for one-line shell logic, or split argv tokens, for example \`trans ${commandName} ls -la\`.` ); } function looksLikeShellCommandString(value: string): boolean { return /\s/u.test(value) || /&&|\|\||[;|<>]/u.test(value); } export function parseSshInvocation(target: string, args: string[]): ParsedSshInvocation { const route = parseSshRoute(target); const operationArgs = normalizeSshOperationArgs(args); if (route.plane === "k3s") { return { providerId: route.providerId, route, parsed: parseK3sRouteArgs(route, operationArgs) }; } if (route.plane === "win") { return { providerId: route.providerId, route, parsed: parseWinRouteArgs(route, operationArgs) }; } if ((operationArgs[0] ?? "") === "k3s") { throw new Error(`ssh k3s shorthand is unsupported; use route syntax instead: trans ${route.providerId}:k3s ${operationArgs.slice(1).join(" ")}`.trim()); } return { providerId: route.providerId, route, parsed: parseSshArgs(operationArgs) }; } export function parseSshRoute(target: string): ParsedSshRoute { if (!target) throw new Error("ssh requires provider id, for example: trans D601"); const firstColon = target.indexOf(":"); if (firstColon < 0) { return hostSshRoute(target, target, null); } const providerId = target.slice(0, firstColon); const tail = target.slice(firstColon + 1); if (!providerId) throw new Error("ssh route requires a provider id before ':'"); if (tail.length === 0) { return hostSshRoute(providerId, target, null); } if (tail.startsWith("/")) { return hostSshRoute(providerId, target, tail); } if (tail === "win32" || tail.startsWith("win32/") || tail.startsWith("win32:")) { throw new Error(`unsupported ssh route plane: win32; use ${providerId}:win or ${providerId}:win/c/path`); } if (tail === "win" || tail.startsWith("win/")) { return winSshRoute(providerId, target, parseWinRouteWorkspace(providerId, tail)); } if (tail.startsWith("win:")) { throw new Error(`ssh win workspace route uses slash syntax, for example: trans ${providerId}:win/c/test cmd cd`); } const [plane, ...rest] = tail.split(":"); if (plane === undefined || plane.length === 0 || plane === "host") { const workspace = rest.length > 0 ? rest.join(":") : null; if (workspace !== null && !workspace.startsWith("/")) throw new Error("ssh host workspace route requires an absolute path after provider:host:"); return hostSshRoute(providerId, target, workspace); } if (plane !== "k3s") throw new Error(`unsupported ssh route plane: ${plane}`); const normalizedRest = normalizeK3sRouteRestSegments(rest); const [first, second, third, fourth] = normalizedRest; const operationInRoute = [first, second, third].map((segment) => segment === undefined ? undefined : routeSegmentHead(segment)).find((segment) => segment !== undefined && legacyK3sOperationRouteSegments.has(segment)); if (operationInRoute !== undefined) throw new Error(k3sOperationInRouteMessage(target, operationInRoute)); if (fourth !== undefined) throw new Error("ssh k3s target route supports at most provider:k3s:namespace:resource:container"); const targetRoute = parseK3sRouteTargetSegments(second ?? null, third ?? null); return { providerId, plane: "k3s", entry: null, namespace: first && first.length > 0 ? first : null, resource: targetRoute.resource, container: targetRoute.container, workspace: targetRoute.workspace, raw: target, }; } function hostSshRoute(providerId: string, raw: string, workspace: string | null): ParsedSshRoute { return { providerId, plane: "host", entry: null, namespace: null, resource: null, container: null, workspace, raw }; } function winSshRoute(providerId: string, raw: string, workspace: string | null): ParsedSshRoute { return { providerId, plane: "win", entry: null, namespace: null, resource: null, container: null, workspace, raw }; } function parseWinRouteWorkspace(providerId: string, tail: string): string | null { if (tail === "win") return null; const suffix = tail.slice("win/".length); const slashIndex = suffix.indexOf("/"); const drive = slashIndex < 0 ? suffix : suffix.slice(0, slashIndex); if (!/^[A-Za-z]$/u.test(drive)) { throw new Error(`ssh win workspace route requires a drive letter, for example: ssh ${providerId}:win/c/test cmd cd`); } const rest = slashIndex < 0 ? "" : suffix.slice(slashIndex + 1); const segments = rest.split("/").filter((segment) => segment.length > 0); return `${drive.toUpperCase()}:\\${segments.join("\\")}`; } function parseWinRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs { const operation = args[0] ?? ""; if (operation.length === 0) { throw new Error(`ssh ${route.raw} requires a Windows operation, for example: ssh ${route.providerId}:win cmd ver or ssh ${route.providerId}:win skills`); } if (operation === "upload" || operation === "download") { return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" }; } if (operation === "apply-patch") { if (isApplyPatchV2HelpArgs(args.slice(1))) { return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" }; } return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" }; } if (operation === "apply-patch-v1") { throw new Error(`ssh ${route.raw} apply-patch-v1 is not supported for Windows routes; use apply-patch v2`); } if (operation === "patch" || operation === "patch-v1" || operation === "v2") { throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper"); } if (operation === "skills" || operation === "skill-discover" || operation === "discover-skills") { return { remoteCommand: buildWindowsPowerShellInvocation(buildWindowsSkillsDiscoveryScript(args.slice(1))), requiresStdin: false, invocationKind: "helper", }; } if (operation === "ps" || operation === "powershell" || operation === "powershell.exe") { const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1); if (commandArgs.length >= 2 && (commandArgs[0] === "-File" || commandArgs[0] === "-file")) { const fileArgs = commandArgs.slice(1).map((a: string) => a.replace(/\\/g, "/")).join(" "); return { remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellInlineLauncherScript("& " + fileArgs, route.workspace)), requiresStdin: false, invocationKind: "helper", }; } if (commandArgs.length === 0) { return { remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellStdinLauncherScript(route.workspace)), requiresStdin: true, invocationKind: "helper", }; } return { remoteCommand: buildWindowsPowerShellInvocation(buildWindowsPowerShellInlineLauncherScript(commandArgs.join(" "), route.workspace)), requiresStdin: false, invocationKind: "helper", }; } if (operation !== "cmd" && operation !== "cmd.exe") { throw new Error(`unsupported ssh win operation: ${operation}; use ssh ${route.providerId}:win ps, ssh ${route.providerId}:win cmd , ssh ${route.providerId}:win apply-patch, or ssh ${route.providerId}:win skills`); } const commandArgs = args[1] === "--" ? args.slice(2) : args.slice(1); if (commandArgs.length === 0) { return { remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdStdinLauncherScript(route.workspace)), requiresStdin: true, invocationKind: "helper", }; } return { remoteCommand: buildWindowsPowerShellInvocation(buildWindowsCmdLauncherScript(buildWindowsCmdLine(commandArgs.join(" "), route.workspace))), requiresStdin: false, invocationKind: "argv", }; } function buildWindowsCmdLine(userCommand: string, cwd: string | null): string { const parts = [ "chcp 65001>nul", 'set "PYTHONUTF8=1"', 'set "PYTHONIOENCODING=utf-8"', ]; if (cwd !== null) parts.push(`cd /d ${windowsCmdQuote(cwd)}`); parts.push(userCommand); return parts.join(" && "); } function windowsCmdQuote(value: string): string { if (/[\r\n"]/u.test(value)) throw new Error("ssh win workspace path must not contain quotes or newlines"); return `"${value}"`; } function parseWindowsSkillsOptions(args: string[]): { limit: number; scopes: Array<"agents" | "codex"> } { let limit = 100; let scope: "agents" | "codex" | "all" = "agents"; for (let index = 0; index < args.length; index += 1) { const arg = args[index] ?? ""; if (arg === "--limit") { const value = args[index + 1]; if (value === undefined) throw new Error("ssh win skills --limit requires a value"); limit = positiveInt(value, "ssh win skills --limit"); index += 1; continue; } if (arg === "--scope") { const value = args[index + 1]; if (value === undefined) throw new Error("ssh win skills --scope requires a value"); if (value !== "agents" && value !== "codex" && value !== "all") throw new Error("ssh win skills --scope must be one of: agents, codex, all"); scope = value; index += 1; continue; } if (arg === "--all") { scope = "all"; continue; } throw new Error(`unsupported ssh win skills option: ${arg}`); } return { limit, scopes: scope === "all" ? ["agents", "codex"] : [scope] }; } function buildWindowsSkillsDiscoveryScript(args: string[]): string { const options = parseWindowsSkillsOptions(args); const rootEntries = options.scopes.map((scope) => { const relative = scope === "agents" ? ".agents\\skills" : ".codex\\skills"; return `@{ Scope = ${powerShellSingleQuote(scope)}; Path = (Join-Path $env:USERPROFILE ${powerShellSingleQuote(relative)}) }`; }).join(", "); return [ "$ErrorActionPreference = 'Stop';", "$ProgressPreference = 'SilentlyContinue';", "[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();", "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new();", "$OutputEncoding = [System.Text.UTF8Encoding]::new();", `$limit = ${options.limit};`, `$roots = @(${rootEntries});`, "$rootRecords = @();", "$skills = @();", "foreach ($root in $roots) {", " $exists = Test-Path -LiteralPath $root.Path -PathType Container;", " $rootRecords += [pscustomobject]@{ scope = $root.Scope; path = $root.Path; exists = $exists };", " if (-not $exists) { continue };", " foreach ($dir in @(Get-ChildItem -LiteralPath $root.Path -Directory -Force | Sort-Object Name)) {", " if ($skills.Count -ge $limit) { break };", " $skillFile = Join-Path $dir.FullName 'SKILL.md';", " $hasSkillMd = Test-Path -LiteralPath $skillFile -PathType Leaf;", " if (-not $hasSkillMd) { continue };", " $name = $dir.Name;", " $description = '';", " if ($hasSkillMd) {", " foreach ($line in @(Get-Content -LiteralPath $skillFile -Encoding UTF8 -TotalCount 80)) {", " if ($line -match '^name:\\s*(.+)$') { $name = $Matches[1].Trim(); continue };", " if ($line -match '^description:\\s*(.+)$') { $description = $Matches[1].Trim(); continue };", " }", " }", " $skills += [pscustomobject]@{ scope = $root.Scope; name = $name; directoryName = $dir.Name; path = $dir.FullName; skillFile = $skillFile; hasSkillMd = $hasSkillMd; description = $description };", " }", "}", "$payload = [pscustomobject]@{ ok = $true; command = 'unidesk ssh win skills'; generatedAt = (Get-Date).ToUniversalTime().ToString('o'); user = $env:USERNAME; userProfile = $env:USERPROFILE; counts = [pscustomobject]@{ roots = $rootRecords.Count; skills = $skills.Count; limit = $limit }; roots = $rootRecords; skills = @($skills) };", "$payload | ConvertTo-Json -Depth 6;", ].join(" "); } function powerShellSingleQuote(value: string): string { return `'${value.replace(/'/g, "''")}'`; } function buildWindowsCmdLauncherScript(cmdLine: string): string { return [ "$ErrorActionPreference = 'Stop';", "[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();", "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new();", "$OutputEncoding = [System.Text.UTF8Encoding]::new();", "$env:PYTHONUTF8 = '1';", "$env:PYTHONIOENCODING = 'utf-8';", `& ${powerShellSingleQuote(windowsCmdExeNativePath)} /d /s /c ${powerShellSingleQuote(cmdLine)};`, "exit $LASTEXITCODE;", ].join(" "); } function buildWindowsCmdStdinLauncherScript(cwd: string | null): string { const prefixLines = [ "@echo off", "chcp 65001>nul", 'set "PYTHONUTF8=1"', 'set "PYTHONIOENCODING=utf-8"', ...(cwd === null ? [] : [`cd /d ${windowsCmdQuote(cwd)}`]), ]; return [ "$ErrorActionPreference = 'Stop';", "$ProgressPreference = 'SilentlyContinue';", "[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();", "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new();", "$OutputEncoding = [System.Text.UTF8Encoding]::new();", "$env:PYTHONUTF8 = '1';", "$env:PYTHONIOENCODING = 'utf-8';", "$script = [Console]::In.ReadToEnd();", "$script = $script -replace \"`r`n\", \"`n\";", "$script = $script -replace \"`r\", \"`n\";", "$script = $script -replace \"`n\", \"`r`n\";", "if (-not $script.EndsWith(\"`r`n\")) { $script += \"`r`n\" }", "if ([string]::IsNullOrWhiteSpace($script)) { [Console]::Error.WriteLine('ssh win cmd requires a command line or stdin batch script'); exit 2 }", `$prefix = ${powerShellSingleQuote(`${prefixLines.join("\r\n")}\r\n`)};`, "$temp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), ('unidesk-win-cmd-' + [guid]::NewGuid().ToString('N') + '.cmd'));", "try {", " [System.IO.File]::WriteAllText($temp, $prefix + $script, [System.Text.UTF8Encoding]::new($false));", " $cmdArg = '\"' + $temp + '\"';", ` & ${powerShellSingleQuote(windowsCmdExeNativePath)} /d /s /c $cmdArg;`, " $code = $LASTEXITCODE;", "} finally {", " Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue;", "}", "exit $code;", ].join(" "); } const windowsPowerShellJsonSafetyPreludeLines = [ "function ConvertTo-UniDeskPlainJsonValue {", " param([object]$Value, [int]$Depth = 6, [int]$Level = 0)", " if ($null -eq $Value) { return $null }", " if ($Value -is [string]) { return [string]$Value }", " if ($Value -is [char]) { return [string]$Value }", " if ($Value -is [bool] -or $Value -is [byte] -or $Value -is [sbyte] -or $Value -is [int16] -or $Value -is [uint16] -or $Value -is [int] -or $Value -is [uint32] -or $Value -is [long] -or $Value -is [uint64] -or $Value -is [single] -or $Value -is [double] -or $Value -is [decimal]) { return $Value }", " if ($Value -is [datetime]) { return $Value.ToUniversalTime().ToString('o') }", " if ($Level -ge $Depth) { return [string]$Value }", " if ($Value -is [System.Collections.IDictionary]) {", " $map = [ordered]@{}", " foreach ($key in $Value.Keys) { $map[[string]$key] = ConvertTo-UniDeskPlainJsonValue -Value $Value[$key] -Depth $Depth -Level ($Level + 1) }", " return $map", " }", " if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) {", " $items = New-Object System.Collections.ArrayList", " foreach ($item in $Value) { [void]$items.Add((ConvertTo-UniDeskPlainJsonValue -Value $item -Depth $Depth -Level ($Level + 1))) }", " return $items.ToArray()", " }", " $skip = @('PSPath','PSParentPath','PSChildName','PSDrive','PSProvider','ReadCount')", " $props = @($Value.PSObject.Properties | Where-Object { @('NoteProperty','Property','AliasProperty') -contains $_.MemberType.ToString() -and -not ($skip -contains $_.Name) })", " if ($props.Count -eq 0) { return [string]$Value }", " $out = [ordered]@{}", " foreach ($prop in $props) {", " try { $out[$prop.Name] = ConvertTo-UniDeskPlainJsonValue -Value $prop.Value -Depth $Depth -Level ($Level + 1) }", " catch { $out[$prop.Name] = '' }", " }", " return $out", "}", "function ConvertTo-Json {", " [CmdletBinding()]", " param(", " [Parameter(ValueFromPipeline=$true)] [object]$InputObject,", " [int]$Depth = 2,", " [switch]$Compress,", " [switch]$EnumsAsStrings", " )", " begin { $items = New-Object System.Collections.ArrayList }", " process { [void]$items.Add($InputObject) }", " end {", " if ($items.Count -eq 0) { $value = $null } elseif ($items.Count -eq 1) { $value = $items[0] } else { $value = $items.ToArray() }", " $plain = ConvertTo-UniDeskPlainJsonValue -Value $value -Depth $Depth -Level 0", " if ($Compress) {", " Microsoft.PowerShell.Utility\\ConvertTo-Json -InputObject $plain -Depth $Depth -Compress", " } else {", " Microsoft.PowerShell.Utility\\ConvertTo-Json -InputObject $plain -Depth $Depth", " }", " }", "}", ]; export function windowsPowerShellScriptPrelude(cwd: string | null): string { return [ "$ErrorActionPreference = 'Stop'", "$ProgressPreference = 'SilentlyContinue'", "[Console]::InputEncoding = [System.Text.UTF8Encoding]::new()", "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()", "$OutputEncoding = [System.Text.UTF8Encoding]::new()", "$env:PYTHONUTF8 = '1'", "$env:PYTHONIOENCODING = 'utf-8'", ...windowsPowerShellJsonSafetyPreludeLines, ...(cwd === null ? [] : [`Set-Location -LiteralPath ${powerShellSingleQuote(cwd)}`]), ].join("\r\n") + "\r\n"; } function buildWindowsPowerShellInlineLauncherScript(command: string, cwd: string | null): string { if (command.trim().length === 0) throw new Error("ssh win ps requires a command or stdin PowerShell script"); return buildWindowsPowerShellScriptRunner(powerShellSingleQuote(command), cwd); } function buildWindowsPowerShellStdinLauncherScript(cwd: string | null): string { return buildWindowsPowerShellScriptRunner("[Console]::In.ReadToEnd()", cwd); } function buildWindowsPowerShellScriptRunner(scriptExpression: string, cwd: string | null): string { return [ "$ErrorActionPreference = 'Stop';", "$ProgressPreference = 'SilentlyContinue';", "[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();", "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new();", "$OutputEncoding = [System.Text.UTF8Encoding]::new();", `$script = ${scriptExpression};`, "$script = $script -replace \"`r`n\", \"`n\";", "$script = $script -replace \"`r\", \"`n\";", "$script = $script -replace \"`n\", \"`r`n\";", "if (-not $script.EndsWith(\"`r`n\")) { $script += \"`r`n\" }", "if ([string]::IsNullOrWhiteSpace($script)) { [Console]::Error.WriteLine('ssh win ps requires a command or stdin PowerShell script'); exit 2 }", "$script += \"`r`nif (`$global:LASTEXITCODE -is [int] -and `$global:LASTEXITCODE -ne 0) { exit `$global:LASTEXITCODE }`r`n\";", `$prefix = ${powerShellSingleQuote(windowsPowerShellScriptPrelude(cwd))};`, "$temp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), ('unidesk-win-ps-' + [guid]::NewGuid().ToString('N') + '.ps1'));", "try {", " [System.IO.File]::WriteAllText($temp, $prefix + $script, [System.Text.UTF8Encoding]::new($true));", " & (Join-Path $PSHOME 'powershell.exe') -NoProfile -ExecutionPolicy Bypass -File $temp;", " $code = $LASTEXITCODE;", " if ($null -eq $code) { $code = 0 }", "} finally {", " Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue;", "}", "exit $code;", ].join(" "); } export function buildWindowsPowerShellInvocation(script: string): string { return shellArgv([ windowsPowerShellExePath, "-NoProfile", "-ExecutionPolicy", "Bypass", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64"), ]); } export function sshRoutePayloadCwd(route: ParsedSshRoute): string | undefined { if (route.plane === "host") return route.workspace ?? undefined; if (route.plane === "win") return windowsBridgeCwd; return undefined; } function routeSegmentHead(segment: string): string { return segment.split("/")[0] ?? segment; } function parseK3sRouteTargetSegments(rawResource: string | null, rawContainer: string | null): { resource: string | null; container: string | null; workspace: string | null } { const resourceParts = splitK3sResourceWorkspace(rawResource); const containerParts = splitK3sContainerWorkspace(rawContainer); return { resource: resourceParts.resource, container: containerParts.container, workspace: combineK3sRouteWorkspace(resourceParts.workspace, containerParts.workspace), }; } function normalizeK3sRouteRestSegments(rest: string[]): string[] { const normalized: string[] = []; for (let index = 0; index < rest.length; index += 1) { const current = rest[index] ?? ""; const podPrefix = k3sPodRoutePrefixes.find((prefix) => current === prefix.slice(0, -1) || current.startsWith(prefix)); if (podPrefix === undefined) { normalized.push(current); continue; } if (current === podPrefix.slice(0, -1)) { const next = rest[index + 1]; if (next === undefined || next.length === 0) throw new Error("ssh k3s pod: route requires a pod name after pod:"); normalized.push(`pod/${next}`); index += 1; continue; } const podName = current.slice(podPrefix.length); if (podName.length === 0) throw new Error("ssh k3s pod: route requires a pod name after pod:"); normalized.push(`pod/${podName}`); } return normalized; } function splitK3sResourceWorkspace(value: string | null): { resource: string | null; workspace: string | null } { if (value === null || value.length === 0) return { resource: null, workspace: null }; const parts = value.split("/"); if (parts.length <= 1) return { resource: value, workspace: null }; if (isK3sResourceKindAlias(parts[0] ?? "") && (parts[1] ?? "").length > 0) { const workspaceParts = parts.slice(2); return { resource: `${parts[0]}/${parts[1]}`, workspace: workspaceParts.length > 0 ? `/${workspaceParts.join("/")}` : null, }; } return { resource: parts[0] ?? value, workspace: parts.length > 1 ? `/${parts.slice(1).join("/")}` : null, }; } function splitK3sContainerWorkspace(value: string | null): { container: string | null; workspace: string | null } { if (value === null || value.length === 0) return { container: null, workspace: null }; const parts = value.split("/"); if (parts.length <= 1) return { container: value, workspace: null }; return { container: parts[0] ?? value, workspace: `/${parts.slice(1).join("/")}` }; } function combineK3sRouteWorkspace(first: string | null, second: string | null): string | null { if (first !== null && second !== null && first !== second) throw new Error("ssh k3s route workspace can be specified once, either after the workload or after the container"); return first ?? second; } function isK3sResourceKindAlias(value: string): boolean { return k3sResourceKindAliases.has(value); } function k3sOperationInRouteMessage(target: string, operation: string): string { const providerId = target.split(":")[0] || ""; if (operation === "v2" || operation === "patch" || operation === "patch-v1") { return `ssh k3s route must locate a target only; remote patch entrypoints are "apply-patch" for the default v2 engine and "apply-patch-v1" for the legacy helper instead of "${target}"`; } const operationExample = operation === "guard" ? "guard" : `${operation} ...`; return `ssh k3s route must locate a target only; put operation "${operation}" after the route, for example "ssh ${providerId}:k3s ${operationExample}" or "ssh ${providerId}:k3s:: ${operationExample}" instead of "${target}"`; } export function shellArgv(args: string[]): string { return args.map(shellQuote).join(" "); } export function shellQuote(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'`; } function positiveInt(value: string, option: string): number { const parsed = Number(value); if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${option} must be a positive integer`); return parsed; } function optionalPositiveInt(value: string, option: string): string { return String(positiveInt(value, option)); } function findOptionValue(args: string[], index: number, option: string): string { const value = args[index + 1]; if (value === undefined || value.length === 0) throw new Error(`ssh find ${option} requires a value`); return value; } function buildFindCommand(args: string[]): string { const paths: string[] = []; const predicates: string[] = []; const patternPredicates: Array<[string, string]> = []; let limit: number | null = null; let sortOutput = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index] ?? ""; if (arg === "--limit") { const value = findOptionValue(args, index, arg); limit = positiveInt(value, "ssh find --limit"); index += 1; continue; } if (arg === "--sort") { sortOutput = true; continue; } if (arg === "--max-depth" || arg === "-maxdepth" || arg === "--min-depth" || arg === "-mindepth") { const value = findOptionValue(args, index, arg); const findArg = arg === "--max-depth" ? "-maxdepth" : arg === "--min-depth" ? "-mindepth" : arg; predicates.push(findArg, String(positiveInt(value, `ssh find ${arg}`))); index += 1; continue; } if (arg === "--type" || arg === "-type") { const value = findOptionValue(args, index, arg); if (!/^[bcdpfls]$/u.test(value)) throw new Error("ssh find --type must be one of: b c d p f l s"); predicates.push("-type", value); index += 1; continue; } if (arg === "--name" || arg === "-name" || arg === "--iname" || arg === "-iname" || arg === "--path" || arg === "-path" || arg === "--ipath" || arg === "-ipath") { const value = findOptionValue(args, index, arg); const findArg = arg.startsWith("--") ? `-${arg.slice(2)}` : arg; patternPredicates.push([findArg, value]); index += 1; continue; } if (arg === "--contains" || arg === "--icontains") { const value = findOptionValue(args, index, arg); const findArg = arg === "--contains" ? "-name" : "-iname"; patternPredicates.push([findArg, `*${value}*`]); index += 1; continue; } if (arg === "--mtime" || arg === "-mtime" || arg === "--mmin" || arg === "-mmin" || arg === "--size" || arg === "-size") { const value = findOptionValue(args, index, arg); const findArg = arg.startsWith("--") ? `-${arg.slice(2)}` : arg; predicates.push(findArg, value); index += 1; continue; } if (arg.startsWith("-")) { throw new Error(`unsupported ssh find option: ${arg}`); } paths.push(arg); } const findArgs = ["find", ...(paths.length > 0 ? paths : ["."]), ...predicates]; if (patternPredicates.length === 1) { const [kind, pattern] = patternPredicates[0]!; findArgs.push(kind, pattern); } else if (patternPredicates.length > 1) { findArgs.push("("); patternPredicates.forEach(([kind, pattern], index) => { if (index > 0) findArgs.push("-o"); findArgs.push(kind, pattern); }); findArgs.push(")"); } findArgs.push("-print"); let command = shellArgv(findArgs); if (sortOutput) command = `${command} | sort`; if (limit !== null) command = `${command} | head -n ${limit}`; return command; } function parseK3sRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs { if (route.entry === null && route.namespace === null && route.resource === null) { return parseK3sControlPlaneOperation(route, args); } if (route.namespace === null || route.resource === null) { throw new Error("ssh k3s target route requires provider:k3s::"); } return parseK3sTargetOperation(route, args); } function parseK3sControlPlaneOperation(route: ParsedSshRoute, args: string[]): ParsedSshArgs { const operation = args[0] ?? "guard"; if (operation === "upload" || operation === "download") { throw new Error(`ssh ${route.providerId}:k3s ${operation} requires a workload route: ssh ${route.providerId}:k3s:: ${operation} ...`); } if (operation === "apply-patch" || operation === "apply-patch-v1") { throw new Error(`ssh ${route.providerId}:k3s apply-patch requires a workload route: ssh ${route.providerId}:k3s:: apply-patch`); } if (operation === "patch" || operation === "patch-v1" || operation === "v2") { throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper"); } if (operation === "script" || operation === "sh") { return buildK3sScriptOperation(args.slice(1)); } if (operation === "shell") { const parsed = parseShellStringOperationArgs(args.slice(1), `ssh ${route.providerId}:k3s shell`); return { remoteCommand: shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" }; } if (operation === "guard") { if (args.length > 1) throw new Error(`ssh ${route.providerId}:k3s guard does not accept extra arguments`); return { remoteCommand: buildK3sGuardCommand(route.providerId), requiresStdin: false, invocationKind: "helper" }; } return { remoteCommand: buildK3sCommand(route.providerId, args), requiresStdin: false, invocationKind: "helper" }; } function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedSshArgs { const targetArgs = k3sRouteTargetArgs(route); if (args.length === 0) { return { remoteCommand: buildK3sTargetObjectCommand("get", route, ["-o", "wide"]), requiresStdin: false, invocationKind: "helper", }; } const operation = args[0] ?? ""; const operationArgs = args.slice(1); if (operation === "upload" || operation === "download") { return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" }; } if (operation === "apply-patch") { if (isApplyPatchV2HelpArgs(operationArgs)) { return { remoteCommand: null, requiresStdin: false, invocationKind: "helper" }; } return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" }; } if (operation === "apply-patch-v1") return buildK3sApplyPatchCommand([...targetArgs, ...operationArgs]); if (operation === "patch" || operation === "patch-v1" || operation === "v2") { throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper"); } if (operation === "script") return buildK3sScriptOperation([...targetArgs, ...operationArgs]); if (operation === "shell") { const parsed = parseShellStringOperationArgs(operationArgs, `ssh ${route.raw} shell`); return { remoteCommand: buildK3sExecCommand([...targetArgs, "--", parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" }; } if (operation === "logs") return { remoteCommand: buildK3sLogsCommand([...targetArgs, ...operationArgs]), requiresStdin: false, invocationKind: "helper" }; if (operation === "argv") { validateDirectArgvCommand(operation, operationArgs); return { remoteCommand: buildK3sExecCommand([...targetArgs, ...k3sRouteCommandArgs(operationArgs)]), requiresStdin: false, invocationKind: "argv" }; } if (operation === "get" || operation === "describe") { return { remoteCommand: buildK3sTargetObjectCommand(operation, route, operationArgs), requiresStdin: false, invocationKind: "helper" }; } if (operation === "kubectl") throw new Error(`ssh k3s kubectl is a control-plane operation; use ssh ${route.providerId}:k3s kubectl ...`); if (operation === "exec") { const execArgs = k3sRouteExecOperationArgs(operationArgs); return { remoteCommand: buildK3sExecCommand([...targetArgs, ...execArgs]), requiresStdin: execArgs.includes("--stdin") || execArgs.includes("-i"), invocationKind: "helper", }; } return { remoteCommand: buildK3sExecCommand([...targetArgs, ...k3sRouteCommandArgs(args)]), requiresStdin: false, invocationKind: "helper" }; } function buildK3sTargetObjectCommand(action: "get" | "describe", route: ParsedSshRoute, args: string[]): string { if (route.namespace === null || route.resource === null) throw new Error(`ssh k3s ${action} target requires namespace and workload route`); if (args.includes("--follow") || args.includes("-f")) throw new Error(`ssh k3s target ${action} does not support follow mode`); return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", action, "-n", route.namespace, normalizeK3sRouteResource(route.resource), ...args]); } function buildK3sTargetCommand(route: ParsedSshRoute, command: string[], options: { stdin?: boolean } = {}): string { return buildK3sExecCommand([...k3sRouteTargetArgs(route), ...(options.stdin === true ? ["--stdin"] : []), "--", ...command]); } function k3sRouteTargetArgs(route: ParsedSshRoute): string[] { if (route.namespace === null) throw new Error(`ssh route ${route.raw} requires a namespace segment`); if (route.resource === null) throw new Error(`ssh route ${route.raw} requires a workload or pod segment`); return [ "--namespace", route.namespace, "--resource", normalizeK3sRouteResource(route.resource), ...(route.container === null ? [] : ["--container", route.container]), ...(route.workspace === null ? [] : ["--workdir", route.workspace]), ]; } function k3sRouteCommandArgs(args: string[]): string[] { if (args.length === 0) throw new Error("ssh k3s target route requires a command to exec"); return args[0] === "--" ? args : ["--", ...args]; } function k3sRouteExecOperationArgs(args: string[]): string[] { if (args.length === 0) throw new Error("ssh k3s target exec operation requires a command to exec"); const result: string[] = []; for (let index = 0; index < args.length; index += 1) { const arg = args[index] ?? ""; if (arg === "--") { if (index === args.length - 1) throw new Error("ssh k3s target exec operation requires a command after --"); return [...result, ...args.slice(index)]; } if (arg === "--stdin" || arg === "-i" || arg === "--tty" || arg === "-t") { result.push(arg); continue; } if (arg === "--container" || arg === "-c" || arg === "--workdir" || arg === "--cwd") { const value = args[index + 1]; if (value === undefined || value.length === 0) throw new Error(`ssh k3s target exec ${arg} requires a value`); result.push(arg, value); index += 1; continue; } return [...result, "--", ...args.slice(index)]; } throw new Error("ssh k3s target exec operation requires a command to exec"); } function buildK3sCommand(providerId: string, args: string[]): string { const action = args[0] ?? ""; if (action.length === 0 || action === "--help" || action === "-h" || action === "help") { throw new Error("ssh k3s requires a subcommand: guard, kubectl, get, describe, logs or exec"); } if (action === "guard") return buildK3sGuardCommand(providerId); if (action === "exec") return buildK3sExecCommand(args.slice(1)); if (action === "script") { const parsed = buildK3sScriptOperation(args.slice(1)); if (parsed.remoteCommand === null) throw new Error("ssh k3s script resolved to an interactive command unexpectedly"); return parsed.remoteCommand; } if (action === "logs") return buildK3sLogsCommand(args.slice(1)); if (action === "kubectl") { const kubectlArgs = args.slice(1); if (kubectlArgs.length === 0) throw new Error("ssh k3s kubectl requires kubectl arguments"); return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]); } return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...args]); } function buildK3sGuardCommand(providerId: string): string { const provider = providerId.toUpperCase(); const providerNodeCheck = provider === "D601" ? "printf '%s\\n' \"$nodes\" | grep -Fx d601 >/dev/null || { printf 'native_k3s_guard=blocked provider=%s reason=d601-node-missing\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }" : provider === "G14" ? "printf '%s\\n' \"$nodes\" | grep -Fx ubuntu-rog-zephyrus-g14-ga401iv-ga401iv >/dev/null || { printf 'native_k3s_guard=blocked provider=%s reason=g14-node-missing\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }" : "[ -n \"$nodes\" ] || { printf 'native_k3s_guard=blocked provider=%s reason=node-list-empty\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }"; const script = [ "set -euo pipefail", `export KUBECONFIG=${shellQuote(nativeK3sKubeconfig)}`, `export UNIDESK_K3S_PROVIDER_ID=${shellQuote(providerId)}`, "context=$(kubectl config current-context)", "server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')", "nodes=$(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{\"\\n\"}{end}')", "printf 'kubeconfig=%s\\n' \"$KUBECONFIG\"", "printf 'provider=%s\\n' \"$UNIDESK_K3S_PROVIDER_ID\"", "printf 'context=%s\\n' \"$context\"", "printf 'server=%s\\n' \"$server\"", "printf 'nodes=%s\\n' \"$(printf '%s' \"$nodes\" | tr '\\n' ' ')\"", providerNodeCheck, "case \"$server\" in *127.0.0.1:11700*|*docker-desktop*) printf 'native_k3s_guard=blocked reason=docker-desktop-context server=%s\\n' \"$server\" >&2; exit 1;; esac", "printf 'native_k3s_guard=ok provider=%s\\n' \"$UNIDESK_K3S_PROVIDER_ID\"", ].join("; "); return shellArgv(["bash", "-c", script]); } interface K3sTargetOptions { namespace: string | null; resource: string | null; selector: string | null; container: string | null; workspace: string | null; stdin: boolean; tty: boolean; shell: string | null; command: string[]; kubectlOptions: string[]; } interface ParseK3sTargetOptionsOptions { requireCommand: boolean; allowCommand?: boolean; allowShell?: boolean; allowSelector?: boolean; } function buildK3sExecCommand(args: string[]): string { const parsed = parseK3sTargetOptions(args, "ssh k3s exec", { requireCommand: true }); if (parsed.namespace === null) throw new Error("ssh k3s exec requires --namespace "); if (parsed.selector !== null) throw new Error("ssh k3s exec does not support --selector"); if (parsed.resource === null) throw new Error("ssh k3s exec requires --deployment , --pod or --resource "); const kubectlArgs = [ "exec", "-n", parsed.namespace, parsed.resource, ...(parsed.container === null ? [] : ["-c", parsed.container]), ...(parsed.stdin ? ["-i"] : []), ...(parsed.tty ? ["-t"] : []), ...parsed.kubectlOptions, "--", ...withK3sWorkspace(parsed, parsed.command), ]; return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]); } function buildK3sScriptOperation(args: string[]): ParsedSshArgs { const parsed = parseK3sTargetOptions(args, "ssh k3s script", { requireCommand: false, allowCommand: true, allowShell: true }); if (parsed.shell === null && parsed.command.length > 0) { return { remoteCommand: buildK3sInlineScriptCommand(parsed), requiresStdin: false, invocationKind: "helper" }; } return { remoteCommand: buildK3sStdinScriptCommand(parsed), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() }; } function buildK3sStdinScriptCommand(parsed: K3sTargetOptions): string { if (parsed.namespace === null && parsed.resource === null) return buildK3sHostScriptCommand(parsed); if (parsed.namespace === null) throw new Error("ssh k3s script target requires --namespace "); if (parsed.selector !== null) throw new Error("ssh k3s script does not support --selector"); if (parsed.resource === null) throw new Error("ssh k3s script target requires --deployment , --pod or --resource "); if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body"); const shell = parsed.shell ?? "sh"; const kubectlArgs = [ "exec", "-i", "-n", parsed.namespace, parsed.resource, ...(parsed.container === null ? [] : ["-c", parsed.container]), ...parsed.kubectlOptions, "--", ...withK3sWorkspace(parsed, [shell, "-s", "--", ...parsed.command]), ]; return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]); } function buildK3sInlineScriptCommand(parsed: K3sTargetOptions): string { if (parsed.command.length === 0) throw new Error("ssh k3s script -- requires a command"); if (parsed.selector !== null) throw new Error("ssh k3s script -- does not support --selector"); if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body"); if (parsed.stdin) throw new Error("ssh k3s script -- does not accept --stdin"); const command = parsed.command.length === 1 ? ["sh", "-c", shellScriptWithCompatibility(parsed.command[0] ?? "")] : parsed.command; if (parsed.namespace === null && parsed.resource === null) { if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container"); if (parsed.workspace !== null) throw new Error("ssh k3s script without a workload does not accept --workdir"); if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s script without a workload does not accept kubectl log options"); return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, ...command]); } if (parsed.namespace === null) throw new Error("ssh k3s script target requires --namespace "); if (parsed.resource === null) throw new Error("ssh k3s script target requires --deployment , --pod or --resource "); const kubectlArgs = [ "exec", "-n", parsed.namespace, parsed.resource, ...(parsed.container === null ? [] : ["-c", parsed.container]), ...parsed.kubectlOptions, "--", ...withK3sWorkspace(parsed, command), ]; return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]); } function buildK3sApplyPatchCommand(args: string[]): ParsedSshArgs { const parsed = parseK3sTargetOptions(args, "ssh k3s apply-patch", { requireCommand: false, allowCommand: true }); if (parsed.namespace === null) throw new Error("ssh k3s apply-patch requires --namespace "); if (parsed.resource === null) throw new Error("ssh k3s apply-patch requires --deployment , --pod or --resource "); if (parsed.tty) throw new Error("ssh k3s apply-patch does not support --tty; stdin is reserved for the patch body"); if (parsed.stdin) throw new Error("ssh k3s apply-patch does not accept --stdin; stdin is always the patch body"); if (parsed.shell !== null) throw new Error("ssh k3s apply-patch does not accept --shell"); if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s apply-patch does not accept kubectl log options"); const kubectlArgs = [ "exec", "-i", "-n", parsed.namespace, parsed.resource, ...(parsed.container === null ? [] : ["-c", parsed.container]), "--", ...withK3sWorkspace(parsed, ["sh", "-s", "--", ...parsed.command]), ]; const wrapper = podApplyPatchStdinWrapper(); return { remoteCommand: shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]), requiresStdin: true, invocationKind: "helper", stdinPrefix: wrapper.prefix, stdinSuffix: wrapper.suffix, }; } function buildK3sHostScriptCommand(parsed: K3sTargetOptions): string { if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body"); if (parsed.stdin) throw new Error("ssh k3s script does not accept --stdin; stdin is always the script body"); if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container"); if (parsed.workspace !== null) throw new Error("ssh k3s script without a workload does not accept --workdir"); if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s script without a workload does not accept kubectl log options"); const shell = parsed.shell ?? "sh"; return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, shell, "-s", "--", ...parsed.command]); } function shellStringFromArgs(args: string[], commandName = "ssh shell"): string { if (args.length === 0) throw new Error(`${commandName} requires a command string`); return args.join(" "); } function parseShellStringOperationArgs(args: string[], commandName: string): { shell: string; command: string } { let shell = "sh"; const commandArgs: string[] = []; for (let index = 0; index < args.length; index += 1) { const arg = args[index] ?? ""; if (arg === "--shell") { shell = k3sScriptShell(k3sOptionValue(args, index, `${commandName} --shell`), `${commandName} --shell`); index += 1; continue; } commandArgs.push(arg); } return { shell, command: shellStringFromArgs(commandArgs, commandName) }; } function buildK3sLogsCommand(args: string[]): string { const parsed = parseK3sTargetOptions(args, "ssh k3s logs", { requireCommand: false, allowSelector: true }); if (parsed.namespace === null) throw new Error("ssh k3s logs requires --namespace "); if (parsed.resource === null && parsed.selector === null) throw new Error("ssh k3s logs requires --deployment , --pod , --resource or --selector "); if (parsed.resource !== null && parsed.selector !== null) throw new Error("ssh k3s logs accepts either a resource or --selector, not both"); if (parsed.stdin || parsed.tty) throw new Error("ssh k3s logs does not support --stdin or --tty"); if (parsed.workspace !== null) throw new Error("ssh k3s logs does not accept --workdir"); const kubectlArgs = [ "logs", "-n", parsed.namespace, ...(parsed.selector === null ? [parsed.resource ?? ""] : ["-l", parsed.selector]), ...(parsed.container === null ? [] : ["-c", parsed.container]), ...parsed.kubectlOptions, ]; return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]); } function parseK3sTargetOptions(args: string[], commandName: string, options: ParseK3sTargetOptionsOptions): K3sTargetOptions { let namespace: string | null = null; let resource: string | null = null; let selector: string | null = null; let container: string | null = null; let workspace: string | null = null; let stdin = false; let tty = false; let shell: string | null = null; const kubectlOptions: string[] = []; const command: string[] = []; let afterDoubleDash = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index] ?? ""; if (afterDoubleDash) { command.push(arg); continue; } if (arg === "--") { afterDoubleDash = true; continue; } if (arg === "--namespace" || arg === "-n") { namespace = k3sOptionValue(args, index, `${commandName} ${arg}`); index += 1; continue; } const namespaceValue = k3sEqualsOptionValue(arg, "--namespace", commandName); if (namespaceValue !== null) { namespace = namespaceValue; continue; } if (arg === "--deployment" || arg === "--deploy") { resource = `deployment/${k3sOptionValue(args, index, `${commandName} ${arg}`)}`; index += 1; continue; } const deploymentValue = k3sEqualsOptionValue(arg, "--deployment", commandName) ?? k3sEqualsOptionValue(arg, "--deploy", commandName); if (deploymentValue !== null) { resource = `deployment/${deploymentValue}`; continue; } if (arg === "--pod") { resource = `pod/${k3sOptionValue(args, index, `${commandName} ${arg}`)}`; index += 1; continue; } const podValue = k3sEqualsOptionValue(arg, "--pod", commandName); if (podValue !== null) { resource = `pod/${podValue}`; continue; } if (arg === "--resource") { resource = normalizeK3sResource(k3sOptionValue(args, index, `${commandName} ${arg}`)); index += 1; continue; } const resourceValue = k3sEqualsOptionValue(arg, "--resource", commandName); if (resourceValue !== null) { resource = normalizeK3sResource(resourceValue); continue; } if (arg === "--selector" || arg === "-l") { if (options.allowSelector !== true) throw new Error(`${commandName} does not support ${arg}`); selector = k3sOptionValue(args, index, `${commandName} ${arg}`); index += 1; continue; } const selectorValue = k3sEqualsOptionValue(arg, "--selector", commandName) ?? k3sEqualsOptionValue(arg, "-l", commandName); if (selectorValue !== null) { if (options.allowSelector !== true) throw new Error(`${commandName} does not support ${arg.split("=", 1)[0]}`); selector = selectorValue; continue; } if (arg === "--container" || arg === "-c") { container = k3sOptionValue(args, index, `${commandName} ${arg}`); index += 1; continue; } const containerValue = k3sEqualsOptionValue(arg, "--container", commandName); if (containerValue !== null) { container = containerValue; continue; } if (arg === "--workdir" || arg === "--cwd") { workspace = k3sWorkspaceValue(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} ${arg}`); index += 1; continue; } const workdirValue = k3sEqualsOptionValue(arg, "--workdir", commandName) ?? k3sEqualsOptionValue(arg, "--cwd", commandName); if (workdirValue !== null) { workspace = k3sWorkspaceValue(workdirValue, `${commandName} ${arg.split("=", 1)[0]}`); continue; } if (arg === "--shell") { if (!options.allowShell) throw new Error(`${commandName} does not support --shell`); shell = k3sScriptShell(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} ${arg}`); index += 1; continue; } const shellValue = k3sEqualsOptionValue(arg, "--shell", commandName); if (shellValue !== null) { if (!options.allowShell) throw new Error(`${commandName} does not support --shell`); shell = k3sScriptShell(shellValue, `${commandName} --shell`); continue; } if (arg === "--stdin" || arg === "-i") { stdin = true; continue; } if (arg === "--tty" || arg === "-t") { tty = true; continue; } if (arg === "--tail") { kubectlOptions.push("--tail", optionalPositiveInt(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} --tail`)); index += 1; continue; } const tailValue = k3sEqualsOptionValue(arg, "--tail", commandName); if (tailValue !== null) { kubectlOptions.push("--tail", optionalPositiveInt(tailValue, `${commandName} --tail`)); continue; } if (arg === "--since" || arg === "--since-time" || arg === "--limit-bytes") { kubectlOptions.push(arg, k3sOptionValue(args, index, `${commandName} ${arg}`)); index += 1; continue; } const sinceValue = k3sEqualsOptionValue(arg, "--since", commandName); const sinceTimeValue = k3sEqualsOptionValue(arg, "--since-time", commandName); const limitBytesValue = k3sEqualsOptionValue(arg, "--limit-bytes", commandName); if (sinceValue !== null || sinceTimeValue !== null || limitBytesValue !== null) { const optionName = sinceValue !== null ? "--since" : sinceTimeValue !== null ? "--since-time" : "--limit-bytes"; const optionValue = sinceValue ?? sinceTimeValue ?? limitBytesValue; kubectlOptions.push(optionName, optionValue ?? ""); continue; } if (arg === "--previous" || arg === "--timestamps" || arg === "--all-containers" || arg === "--prefix") { kubectlOptions.push(arg); continue; } if (arg === "--follow" || arg === "-f") { throw new Error(`${commandName} does not support ${arg}; use a bounded logs command and poll again`); } if (arg.startsWith("-")) throw new Error(`unsupported ${commandName} option: ${arg}`); if (resource === null) { resource = normalizeK3sResource(arg); continue; } throw new Error(`unexpected ${commandName} argument before --: ${arg}`); } if (options.requireCommand && command.length === 0) throw new Error(`${commandName} requires -- [args...]`); if (!options.requireCommand && options.allowCommand !== true && command.length > 0) throw new Error(`${commandName} does not accept a command after --`); return { namespace, resource, selector, container, workspace, stdin, tty, shell, command, kubectlOptions }; } function k3sEqualsOptionValue(arg: string, option: string, commandName: string): string | null { const prefix = `${option}=`; if (!arg.startsWith(prefix)) return null; const value = arg.slice(prefix.length); if (value.length === 0) throw new Error(`${commandName} ${option} requires a value`); return value; } function k3sOptionValue(args: string[], index: number, option: string): string { const value = args[index + 1]; if (value === undefined || value.length === 0) throw new Error(`${option} requires a value`); return value; } function k3sWorkspaceValue(value: string, option: string): string { if (!value.startsWith("/")) throw new Error(`${option} must be an absolute pod workspace path`); return value; } function withK3sWorkspace(parsed: K3sTargetOptions, command: string[]): string[] { if (parsed.workspace === null) return command; if (command.length === 0) throw new Error("ssh k3s workspace route requires a command to execute"); return ["sh", "-c", 'cd "$1" || exit; shift; exec "$@"', "unidesk-cwd", parsed.workspace, ...command]; } function normalizeK3sResource(value: string): string { if (value.includes("/")) return value; return `pod/${value}`; } function normalizeK3sRouteResource(value: string): string { if (value.startsWith("deploy/")) return `deployment/${value.slice("deploy/".length)}`; if (value.startsWith("po/")) return `pod/${value.slice("po/".length)}`; if (value.startsWith("pod:")) return `pod/${value.slice("pod:".length)}`; if (value.startsWith("po:")) return `pod/${value.slice("po:".length)}`; if (value.startsWith("pods:")) return `pod/${value.slice("pods:".length)}`; if (value.includes("/")) return value; return `deployment/${value}`; } function k3sScriptShell(value: string, option: string): string { if (!/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${option} must be a shell executable name or path without whitespace`); return value; } function shellScriptPrelude(): string { return `${sshUserToolPathPrelude}\n${sshShellCompatibilityPrelude}`; } function shellScriptWithCompatibility(command: string): string { return `${shellScriptPrelude()}\n${command}`; } function shellScriptStdinPrefix(): string { return `${shellScriptPrelude()}\n`; } function buildShellCommand(args: string[]): ParsedSshArgs { let shell = "sh"; const scriptArgs: string[] = []; let afterDoubleDash = false; let directArgvMode = false; let shellOptionSeen = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index] ?? ""; if (afterDoubleDash) { scriptArgs.push(arg); continue; } if (arg === "--") { if (!shellOptionSeen && scriptArgs.length === 0) { directArgvMode = true; scriptArgs.push(...args.slice(index + 1)); break; } afterDoubleDash = true; continue; } if (arg === "--shell") { shell = k3sScriptShell(k3sOptionValue(args, index, "ssh script --shell"), "ssh script --shell"); shellOptionSeen = true; index += 1; continue; } if (arg.startsWith("-")) throw new Error(`unsupported ssh script option: ${arg}`); scriptArgs.push(arg); } if (directArgvMode) { if (scriptArgs.length === 0) throw new Error("ssh script -- requires a command"); if (scriptArgs.length === 1) return { remoteCommand: shellArgv([shell, "-c", shellScriptWithCompatibility(scriptArgs[0] ?? "")]), requiresStdin: false, invocationKind: "helper" }; return { remoteCommand: shellArgv(scriptArgs), requiresStdin: false, invocationKind: "argv" }; } return { remoteCommand: shellArgv([shell, "-s", "--", ...scriptArgs]), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() }; } function buildShellStringCommand(args: string[]): ParsedSshArgs { const parsed = parseShellStringOperationArgs(args, "ssh shell"); return { remoteCommand: shellArgv([parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" }; } function podApplyPatchStdinWrapper(): { prefix: string; suffix: string } { const toolMarker = "__UNIDESK_APPLY_PATCH_TOOL__"; const patchMarker = "__UNIDESK_APPLY_PATCH_PAYLOAD__"; if (remoteApplyPatchSource.includes(toolMarker)) throw new Error("remote apply_patch source contains reserved heredoc marker"); return { prefix: [ "set -eu", 'UNIDESK_SSH_TOOL_DIR="${UNIDESK_SSH_TOOL_DIR:-/tmp/unidesk-ssh-tools}"', 'mkdir -p "$UNIDESK_SSH_TOOL_DIR"', `cat > "$UNIDESK_SSH_TOOL_DIR/apply_patch" <<'${toolMarker}'`, remoteApplyPatchSource.trimEnd(), toolMarker, 'chmod 700 "$UNIDESK_SSH_TOOL_DIR/apply_patch"', `"$UNIDESK_SSH_TOOL_DIR/apply_patch" "$@" <<'${patchMarker}'`, ].join("\n") + "\n", suffix: `\n${patchMarker}\n`, }; } function buildPythonStdinCommand(args: string[]): string { const pythonArgs = args.map(shellQuote).join(" "); const execArgs = pythonArgs.length > 0 ? ` "$UNIDESK_SSH_PY_FILE" ${pythonArgs}` : ' "$UNIDESK_SSH_PY_FILE"'; return [ 'UNIDESK_SSH_PY_FILE="$(mktemp /tmp/unidesk-ssh-py.XXXXXX.py)" || exit 1', `trap 'rm -f "$UNIDESK_SSH_PY_FILE"' EXIT`, 'cat > "$UNIDESK_SSH_PY_FILE"', `python3 -u${execArgs}`, ].join("; "); } const remoteToolSources: Record = { apply_patch: remoteApplyPatchSource, glob: remoteGlobSource, "skill-discover": remoteSkillDiscoverSource, }; function remoteToolBootstrapCommand(helpers: readonly SshHelperName[] = []): string { const uniqueHelpers = [...new Set(helpers)]; if (uniqueHelpers.length === 0) return ""; const commands = [ "UNIDESK_SSH_TOOL_DIR=/tmp/unidesk-ssh-tools", 'mkdir -p "$UNIDESK_SSH_TOOL_DIR"', ]; for (const helper of uniqueHelpers) { const source = remoteToolSources[helper]; const encoded = Buffer.from(source, "utf8").toString("base64"); commands.push(`printf %s ${shellQuote(encoded)} | base64 -d > "$UNIDESK_SSH_TOOL_DIR/${helper}"`); commands.push(`chmod 700 "$UNIDESK_SSH_TOOL_DIR/${helper}"`); } commands.push('export PATH="$UNIDESK_SSH_TOOL_DIR:$PATH"'); return commands.join("; "); } export function wrapSshRemoteCommand(command: string | null, helpers: readonly SshHelperName[] = []): string { const bootstrap = remoteToolBootstrapCommand(helpers); const prelude = [sshUserToolPathPrelude, bootstrap].filter((part) => part.length > 0).join("; "); const prefix = prelude.length > 0 ? `${prelude}; ` : ""; if (command === null) return `${prefix}exec "\${SHELL:-/bin/bash}" -l`; return `${prefix}stty -echo 2>/dev/null || true; ${command}`; } function safeProviderId(providerId: string): string { return /^[A-Za-z0-9_.-]{1,64}$/u.test(providerId) ? providerId : ""; } function classifySshLikeFailure(exitCode: number, stderrText: string): SshFailureHint["trigger"] | null { const normalized = stderrText.toLowerCase(); if ( normalized.includes("kex_exchange_identification") || normalized.includes("ssh_exchange_identification") || normalized.includes("connection closed by remote host") || normalized.includes("connection reset by peer") || normalized.includes("connection timed out") || normalized.includes("operation timed out") || normalized.includes("timed out waiting for provider session") || normalized.includes("the operation was aborted") ) { return "timeout-or-kex"; } return exitCode === 255 ? "exit-255" : null; } export function sshFailureHint(providerId: string, parsed: ParsedSshArgs, exitCode: number, stderrText: string): SshFailureHint | null { if (parsed.invocationKind !== "ssh-like") return null; const trigger = classifySshLikeFailure(exitCode, stderrText); if (trigger === null) return null; const shownProviderId = safeProviderId(providerId); return { code: "ssh-like-command-friction", providerId: shownProviderId, trigger, exitCode, message: "ssh-like remote command failed before proving Host SSH is globally unavailable; prefer structured argv or stdin script passthrough for non-interactive commands.", try: `trans ${shownProviderId} script <<'SCRIPT'`, triage: `bun scripts/cli.ts provider triage ${shownProviderId} --observed-scope ssh --observed-error ''`, note: "This hint intentionally does not echo the original remote command.", }; } export function formatSshFailureHint(hint: SshFailureHint): string { return `UNIDESK_SSH_HINT ${JSON.stringify(hint)}\n`; } export function sshSlowWarningThresholdMs(env: NodeJS.ProcessEnv = process.env): number { const raw = env.UNIDESK_SSH_SLOW_WARNING_MS; const parsed = raw === undefined ? NaN : Number(raw); if (!Number.isFinite(parsed) || parsed <= 0) return defaultSshSlowWarningMs; return Math.max(1000, Math.trunc(parsed)); } export function sshRuntimeTimingHint(options: { invocation: ParsedSshInvocation; transport: SshRuntimeTimingHint["transport"]; exitCode: number; startedAtMs: number; finishedAtMs?: number; thresholdMs?: number; }): SshRuntimeTimingHint { const finishedAtMs = options.finishedAtMs ?? Date.now(); const thresholdMs = options.thresholdMs ?? sshSlowWarningThresholdMs(); const elapsedMs = Math.max(0, Math.round(finishedAtMs - options.startedAtMs)); const elapsedSeconds = Number((elapsedMs / 1000).toFixed(3)); const slow = elapsedMs > thresholdMs; const thresholdSeconds = Number((thresholdMs / 1000).toFixed(3)); return { code: "ssh-runtime-timing", level: slow ? "warning" : "info", providerId: safeProviderId(options.invocation.providerId), route: options.invocation.route.raw, transport: options.transport, invocationKind: options.invocation.parsed.invocationKind, exitCode: options.exitCode, elapsedMs, elapsedSeconds, thresholdMs, slow, message: slow ? `ssh operation took ${elapsedSeconds}s, above the ${thresholdSeconds}s warning threshold; consider checking provider/session latency, remote command cost, helper bootstrap, or tran/apply-patch optimization before repeating high-frequency operations.` : `ssh operation completed in ${elapsedSeconds}s.`, note: "Timing hint is written to stderr and intentionally does not echo the original remote command.", }; } export function formatSshRuntimeTimingHint(hint: SshRuntimeTimingHint): string { if (!hint.slow) return ""; return `UNIDESK_SSH_TIMING ${JSON.stringify(hint)}\n`; } export function sshRuntimeTimeoutMs(env: NodeJS.ProcessEnv = process.env): number { const raw = env.UNIDESK_SSH_RUNTIME_TIMEOUT_MS ?? env.UNIDESK_TRAN_RUNTIME_TIMEOUT_MS; const parsed = raw === undefined ? NaN : Number(raw); if (!Number.isFinite(parsed) || parsed <= 0) return defaultSshRuntimeTimeoutMs; return Math.min(maxSshRuntimeTimeoutMs, Math.max(1000, Math.trunc(parsed))); } export function sshRuntimeTimeoutHint(options: { invocation: ParsedSshInvocation; transport: SshRuntimeTimeoutHint["transport"]; timeoutMs: number; }): SshRuntimeTimeoutHint { const timeoutSeconds = Number((options.timeoutMs / 1000).toFixed(3)); return { code: "ssh-runtime-timeout", level: "warning", providerId: safeProviderId(options.invocation.providerId), route: options.invocation.route.raw, transport: options.transport, invocationKind: options.invocation.parsed.invocationKind, timeoutMs: options.timeoutMs, timeoutSeconds, message: `ssh/tran operation exceeded the ${timeoutSeconds}s top-level runtime limit and was disconnected.`, action: "Use short query plus poll semantics; do not keep tran open waiting for long CI/CD, trace, logs, or build progress.", note: "Timeout hint is written to stderr and intentionally does not echo the original remote command.", }; } export function formatSshRuntimeTimeoutHint(hint: SshRuntimeTimeoutHint): string { return `UNIDESK_SSH_RUNTIME_TIMEOUT ${JSON.stringify(hint)}\n`; } export function sshStdoutStreamMaxBytes(env: NodeJS.ProcessEnv = process.env): number { const raw = env.UNIDESK_SSH_STDOUT_STREAM_MAX_BYTES ?? env.UNIDESK_TRAN_STDOUT_STREAM_MAX_BYTES; const parsed = raw === undefined ? NaN : Number(raw); if (!Number.isFinite(parsed) || parsed <= 0) return defaultSshStdoutStreamMaxBytes; return Math.min(maxSshStdoutStreamMaxBytes, Math.max(minSshStdoutStreamMaxBytes, Math.trunc(parsed))); } export function sshStdoutTruncationHint(options: { invocation: ParsedSshInvocation; transport: SshStdoutTruncationHint["transport"]; thresholdBytes: number; observedBytesAtTruncation: number; dumpPath: string | null; dumpError?: string | null; }): SshStdoutTruncationHint { return { code: "ssh-stdout-truncated", level: "warning", providerId: safeProviderId(options.invocation.providerId), route: options.invocation.route.raw, transport: options.transport, invocationKind: options.invocation.parsed.invocationKind, thresholdBytes: options.thresholdBytes, observedBytesAtTruncation: options.observedBytesAtTruncation, dumpPath: options.dumpPath, dumpError: options.dumpError ?? null, message: `ssh stdout exceeded ${options.thresholdBytes} bytes; stdout is bounded and the complete stream is written to a local dump when possible.`, action: "Inspect the dump path, or rerun a narrower remote command with tail/paging instead of emitting full logs or huge JSON.", note: "This hint is written to stderr and intentionally does not echo the original remote command.", }; } export function formatSshStdoutTruncationHint(hint: SshStdoutTruncationHint): string { return `UNIDESK_SSH_STDOUT_TRUNCATED ${JSON.stringify(hint)}\n`; } function sshStdoutDumpPath(invocation: ParsedSshInvocation): string { mkdirSync(sshStdoutDumpDir, { recursive: true, mode: 0o700 }); const timestamp = new Date().toISOString().replace(/[:.]/gu, "-"); const suffix = randomBytes(4).toString("hex"); const slug = `${invocation.providerId}-${invocation.route.raw}-${invocation.route.plane}` .replace(/[^A-Za-z0-9._-]+/gu, "-") .replace(/^-+|-+$/gu, "") .slice(0, 80) || "ssh"; return join(sshStdoutDumpDir, `${timestamp}-${process.pid}-${suffix}-${slug}.stdout.bin`); } export function createSshStdoutForwarder(options: { invocation: ParsedSshInvocation; transport: SshStdoutTruncationHint["transport"]; maxBytes?: number; stdout?: NodeJS.WritableStream; }): { write: (chunk: Buffer) => string | null } { const stdout = options.stdout ?? process.stdout; const maxBytes = options.maxBytes ?? sshStdoutStreamMaxBytes(); let observedBytes = 0; let forwardedBytes = 0; let truncated = false; let dumpPath: string | null = null; let dumpError: string | null = null; const bufferedChunks: Buffer[] = []; const appendDump = (chunk: Buffer): void => { if (dumpError !== null) return; try { if (dumpPath === null) { dumpPath = sshStdoutDumpPath(options.invocation); writeFileSync(dumpPath, Buffer.alloc(0), { mode: 0o600 }); for (const buffered of bufferedChunks) appendFileSync(dumpPath, buffered); bufferedChunks.length = 0; } appendFileSync(dumpPath, chunk); } catch (error) { dumpError = error instanceof Error ? error.message : String(error); dumpPath = null; } }; return { write(chunk: Buffer): string | null { observedBytes += chunk.length; if (!truncated && observedBytes <= maxBytes) { bufferedChunks.push(Buffer.from(chunk)); stdout.write(chunk); forwardedBytes += chunk.length; return null; } if (!truncated) { truncated = true; const remaining = Math.max(0, maxBytes - forwardedBytes); if (remaining > 0) { stdout.write(chunk.subarray(0, remaining)); forwardedBytes += remaining; } appendDump(chunk); return formatSshStdoutTruncationHint(sshStdoutTruncationHint({ invocation: options.invocation, transport: options.transport, thresholdBytes: maxBytes, observedBytesAtTruncation: observedBytes, dumpPath, dumpError, })); } appendDump(chunk); return null; }, }; } function brokerSource(): string { return String.raw` const open = JSON.parse(process.argv[2] || process.argv[1] || "{}"); const token = process.env.PROVIDER_TOKEN || process.env.UNIDESK_PROVIDER_TOKEN || ""; const baseUrl = process.env.UNIDESK_SSH_BROKER_URL || "ws://backend-core:8080/ws/ssh"; const url = baseUrl + "?token=" + encodeURIComponent(token); const ws = new WebSocket(url); let exitCode = 255; let canSend = false; let sessionReady = false; let opened = false; const pending = []; const pendingInput = []; const openTimer = setTimeout(() => { if (opened) return; process.stderr.write("unidesk ssh bridge timed out waiting for provider session\n"); try { ws.close(); } catch {} process.exit(255); }, Number(open.openTimeoutMs || 15000)); const runtimeTimeoutMs = Number(open.runtimeTimeoutMs || 60000); const runtimeTimeoutMode = open.runtimeTimeoutMode === "inactivity" ? "inactivity" : "wall-clock"; let runtimeTimer = null; function armRuntimeTimer() { if (runtimeTimer !== null) clearTimeout(runtimeTimer); runtimeTimer = setTimeout(() => { const noun = runtimeTimeoutMode === "inactivity" ? "inactivity timeout" : "runtime timeout"; process.stderr.write("unidesk ssh bridge " + noun + "; use short query plus poll semantics for long non-streaming work\n"); exitCode = 124; try { ws.close(); } catch {} setTimeout(() => process.exit(124), 250).unref?.(); }, runtimeTimeoutMs); } function clearRuntimeTimer() { if (runtimeTimer !== null) clearTimeout(runtimeTimer); runtimeTimer = null; } armRuntimeTimer(); function send(value) { const text = JSON.stringify(value); if (!canSend || ws.readyState !== WebSocket.OPEN) { pending.push(text); return; } ws.send(text); } function flush() { while (pending.length > 0 && ws.readyState === WebSocket.OPEN) { ws.send(pending.shift()); } } function sendInput(value) { const text = JSON.stringify(value); if (!sessionReady || ws.readyState !== WebSocket.OPEN) { pendingInput.push(text); return; } ws.send(text); } function flushInput() { if (!sessionReady || ws.readyState !== WebSocket.OPEN) return; while (pendingInput.length > 0) { ws.send(pendingInput.shift()); } } function decodeData(data) { return typeof data === "string" ? data : Buffer.from(data).toString("utf8"); } ws.addEventListener("open", () => { canSend = true; send({ type: "ssh.open", providerId: open.providerId, command: open.command || undefined, cwd: open.cwd || undefined, tty: open.tty === true, cols: open.cols || 100, rows: open.rows || 30, }); flush(); }); ws.addEventListener("message", (event) => { const message = JSON.parse(decodeData(event.data)); if (runtimeTimeoutMode === "inactivity") armRuntimeTimer(); if (message.type === "ssh.data") { opened = true; const chunk = Buffer.from(message.data || "", "base64"); if (message.stream === "stderr") process.stderr.write(chunk); else process.stdout.write(chunk); return; } if (message.type === "ssh.opened") { opened = true; sessionReady = true; clearTimeout(openTimer); if (open.stdinEotOnEnd === true) setTimeout(flushInput, 200); else flushInput(); return; } if (message.type === "ssh.dispatched") { return; } if (message.type === "ssh.error") { clearTimeout(openTimer); clearRuntimeTimer(); process.stderr.write(String(message.message || "ssh bridge error") + "\n"); exitCode = 255; ws.close(); return; } if (message.type === "ssh.exit") { clearTimeout(openTimer); clearRuntimeTimer(); exitCode = Number.isInteger(message.exitCode) ? message.exitCode : 255; ws.close(); } }); ws.addEventListener("close", () => { clearTimeout(openTimer); clearRuntimeTimer(); process.exit(exitCode); }); ws.addEventListener("error", () => { process.stderr.write("unidesk ssh bridge websocket error\n"); process.exit(255); }); process.stdin.on("data", (chunk) => { sendInput({ type: "ssh.input", data: Buffer.from(chunk).toString("base64"), encoding: "base64" }); }); process.stdin.on("end", () => { if (open.stdinEotOnEnd === true) { sendInput({ type: "ssh.input", data: Buffer.from([4]).toString("base64"), encoding: "base64" }); } sendInput({ type: "ssh.eof" }); }); `; } function terminalSize(): { cols: number; rows: number } { return { cols: Number(process.stdout.columns) > 0 ? Number(process.stdout.columns) : 100, rows: Number(process.stdout.rows) > 0 ? Number(process.stdout.rows) : 30, }; } export function remoteCommandForRoute(route: ParsedSshRoute, command: string[], options: { stdin?: boolean } = {}): string { if (route.plane === "k3s") return buildK3sTargetCommand(route, command, options); if (route.plane === "win") throw new Error(`ssh apply-patch does not support win routes yet: ${route.raw}`); return shellArgv(command); } type WindowsApplyPatchFsOperation = | "stat" | "read-b64-block" | "read-bulk-b64" | "apply-replacements-bulk-stdin" | "write-b64-stdin" | "write-b64-begin" | "write-b64-append-stdin" | "write-b64-commit" | "delete"; const windowsApplyPatchWriteB64ChunkChars = 12_000; function createWindowsApplyPatchFileSystem(config: UniDeskConfig, invocation: ParsedSshInvocation): ApplyPatchV2FileSystem { async function checked(operation: WindowsApplyPatchFsOperation, args: string[], input?: string): Promise { const command = buildWindowsPowerShellInvocation(windowsApplyPatchFsScript(invocation.route.workspace, operation, args)); const result = await runSshCaptureRemoteCommand(config, invocation, command, input); if (result.exitCode === 0) return result; throw new Error(`windows apply-patch fs operation failed: ${operation} ${JSON.stringify({ route: invocation.route.raw, args: args.slice(0, 4), exitCode: result.exitCode, stdout: result.stdout.slice(-2000), stderr: result.stderr.slice(-4000), })}`); } return { async stat(filePath) { const result = await checked("stat", [filePath]); const [bytesText, sha256] = result.stdout.trim().split(/\s+/u); const bytes = Number(bytesText); if (!Number.isSafeInteger(bytes) || bytes < 0 || !/^[0-9a-f]{64}$/u.test(sha256 ?? "")) { throw new Error(`windows apply-patch fs stat returned invalid metadata: ${JSON.stringify({ filePath, stdout: result.stdout.slice(0, 500) })}`); } return { bytes, sha256: sha256! }; }, async readBlock(filePath, blockIndex, blockBytes) { const result = await checked("read-b64-block", [filePath, String(blockIndex), String(blockBytes)]); const encoded = result.stdout.replace(/\s+/gu, ""); return encoded.length === 0 ? Buffer.alloc(0) : Buffer.from(encoded, "base64"); }, async writeFile(filePath, content) { const encoded = content.toString("base64"); const expectedBytes = String(content.length); const expectedSha256 = createHash("sha256").update(content).digest("hex"); try { await checked("write-b64-stdin", [filePath, expectedBytes, expectedSha256], encoded); return; } catch { const token = `${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}-${expectedSha256.slice(0, 12)}`; await checked("write-b64-begin", [filePath, token]); for (const chunk of chunkString(encoded, windowsApplyPatchWriteB64ChunkChars)) { await checked("write-b64-append-stdin", [filePath, token], chunk); } await checked("write-b64-commit", [filePath, token, expectedBytes, expectedSha256]); } }, async deleteFile(filePath) { await checked("delete", [filePath]); }, async readFiles(filePaths) { const result = await checked("read-bulk-b64", [String(filePaths.length)], `${filePaths.join("\n")}\n`); return decodeApplyPatchV2BulkRead(result.stdout, filePaths); }, async applyReplacementsBulk(filePaths, plans: Map) { const { targets, payload } = formatApplyPatchV2BulkReplacementPayload(filePaths, plans); if (targets.length === 0) return; await checked("apply-replacements-bulk-stdin", [String(targets.length)], payload); }, }; } function windowsApplyPatchFsScript(basePath: string | null, operation: WindowsApplyPatchFsOperation, args: string[]): string { const target = args[0] ?? ""; const arg1 = args[1] ?? ""; const arg2 = args[2] ?? ""; const arg3 = args[3] ?? ""; return [ "$ErrorActionPreference = 'Stop';", "$ProgressPreference = 'SilentlyContinue';", "[Console]::InputEncoding = [System.Text.UTF8Encoding]::new();", "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new();", "$OutputEncoding = [System.Text.UTF8Encoding]::new();", `$basePath = ${powerShellSingleQuote(basePath ?? "")};`, `$operation = ${powerShellSingleQuote(operation)};`, `$targetArg = ${powerShellSingleQuote(target)};`, `$arg1 = ${powerShellSingleQuote(arg1)};`, `$arg2 = ${powerShellSingleQuote(arg2)};`, `$arg3 = ${powerShellSingleQuote(arg3)};`, "function Fail([string]$Message, [int]$Code) { [Console]::Error.WriteLine($Message); exit $Code }", "function Resolve-UnideskPath([string]$Raw) { if ([string]::IsNullOrWhiteSpace($Raw)) { Fail 'empty apply-patch path' 2 }; if ([System.IO.Path]::IsPathRooted($Raw)) { return [System.IO.Path]::GetFullPath($Raw) }; if (-not [string]::IsNullOrWhiteSpace($basePath)) { return [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($basePath, $Raw)) }; return [System.IO.Path]::GetFullPath($Raw) }", "function Ensure-Parent([string]$Target) { $parent = [System.IO.Path]::GetDirectoryName($Target); if (-not [string]::IsNullOrWhiteSpace($parent)) { [System.IO.Directory]::CreateDirectory($parent) | Out-Null } }", "function Get-Sha256([string]$Path) { return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() }", "function Get-Sha256Bytes([byte[]]$Bytes) { $sha = [System.Security.Cryptography.SHA256]::Create(); try { return ([System.BitConverter]::ToString($sha.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() } }", "function Decode-Utf8([string]$Encoded) { return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Encoded)) }", "function Encode-Utf8([string]$Value) { return [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Value)) }", "function Set-TmpPaths([string]$Target, [string]$Token) { if ($Token -notmatch '^[A-Za-z0-9_.-]+$') { Fail 'invalid apply-patch temp token' 2 }; $dir = [System.IO.Path]::GetDirectoryName($Target); if ([string]::IsNullOrWhiteSpace($dir)) { $dir = (Get-Location).ProviderPath }; $base = [System.IO.Path]::GetFileName($Target); $script:tmp = [System.IO.Path]::Combine($dir, '.' + $base + '.unidesk-v2-' + $Token + '.tmp'); $script:tmpB64 = $script:tmp + '.b64' }", "function Verify-Temp([string]$Target, [string]$Tmp, [Int64]$ExpectedBytes, [string]$ExpectedSha256) { $actualBytes = ([System.IO.FileInfo]$Tmp).Length; if ($actualBytes -ne $ExpectedBytes) { Remove-Item -LiteralPath $Tmp -Force -ErrorAction SilentlyContinue; Fail ('apply-patch byte count mismatch for ' + $Target + ': expected=' + $ExpectedBytes + ' actual=' + $actualBytes) 23 }; $actualSha256 = Get-Sha256 $Tmp; if ($actualSha256 -ne $ExpectedSha256) { Remove-Item -LiteralPath $Tmp -Force -ErrorAction SilentlyContinue; Fail ('apply-patch sha256 mismatch for ' + $Target + ': expected=' + $ExpectedSha256 + ' actual=' + $actualSha256) 24 } }", "function Decode-ToTarget([string]$Target, [string]$Encoded, [Int64]$ExpectedBytes, [string]$ExpectedSha256) { Ensure-Parent $Target; Set-TmpPaths $Target ([guid]::NewGuid().ToString('N')); try { $bytes = [Convert]::FromBase64String(($Encoded -replace '\\s','')) } catch { Fail ('apply-patch base64 decode failed for ' + $Target + ': ' + $_.Exception.Message) 22 }; [System.IO.File]::WriteAllBytes($script:tmp, $bytes); Verify-Temp $Target $script:tmp $ExpectedBytes $ExpectedSha256; Move-Item -LiteralPath $script:tmp -Destination $Target -Force; $actualSha256 = Get-Sha256 $Target; if ($actualSha256 -ne $ExpectedSha256) { Fail ('apply-patch final sha256 mismatch for ' + $Target) 25 } }", "$target = Resolve-UnideskPath $targetArg;", "switch ($operation) {", " 'stat' { if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { Fail ('file not found: ' + $target) 1 }; $bytes = ([System.IO.FileInfo]$target).Length; $digest = Get-Sha256 $target; [Console]::Out.WriteLine(([string]$bytes) + ' ' + $digest); break }", " 'read-b64-block' { $blockIndex = [Int64]$arg1; $blockSize = [Int32]$arg2; if ($blockIndex -lt 0 -or $blockSize -le 0) { Fail 'invalid read block args' 2 }; $fs = [System.IO.File]::Open($target, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite); try { [void]$fs.Seek($blockIndex * $blockSize, [System.IO.SeekOrigin]::Begin); $buffer = New-Object byte[] $blockSize; $read = $fs.Read($buffer, 0, $blockSize); if ($read -gt 0) { [Console]::Out.Write([Convert]::ToBase64String($buffer, 0, $read)) } } finally { $fs.Dispose() }; break }", " 'read-bulk-b64' { $expectedCount = [Int32]$targetArg; $items = @([Console]::In.ReadToEnd() -split \"`r?`n\" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }); if ($items.Count -ne $expectedCount) { Fail ('bulk read record count mismatch: expected=' + $expectedCount + ' actual=' + $items.Count) 23 }; [Console]::Out.WriteLine('UNIDESK_APPLY_PATCH_V2_BULK_READ ' + $items.Count); foreach ($item in $items) { $path = Resolve-UnideskPath $item; if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { Fail ('file not found: ' + $path) 1 }; $bytes = [System.IO.File]::ReadAllBytes($path); $pathB64 = Encode-Utf8 $item; $digest = Get-Sha256Bytes $bytes; [Console]::Out.Write($pathB64 + ' ' + $bytes.Length + ' ' + $digest + ' '); [Console]::Out.Write([System.Convert]::ToBase64String($bytes)); [Console]::Out.WriteLine() }; break }", " 'apply-replacements-bulk-stdin' { $expectedCount = [Int32]$targetArg; $records = @([Console]::In.ReadToEnd() -split \"`r?`n\" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }); if ($records.Count -ne $expectedCount) { Fail ('bulk replacement record count mismatch: expected=' + $expectedCount + ' actual=' + $records.Count) 23 }; $utf8 = [System.Text.UTF8Encoding]::new($false); $plans = New-Object System.Collections.Generic.List[object]; $index = 0; foreach ($record in $records) { $index += 1; $fields = $record -split '\\s+'; if ($fields.Count -ne 6) { Fail 'bulk replacement malformed record' 23 }; $targetRelative = Decode-Utf8 $fields[0]; $resolved = Resolve-UnideskPath $targetRelative; $originalBytes = [Int64]$fields[1]; $originalSha256 = $fields[2]; $finalBytes = [Int64]$fields[3]; $finalSha256 = $fields[4]; $replacementsText = $fields[5]; if (-not (Test-Path -LiteralPath $resolved -PathType Leaf)) { Fail ('file not found: ' + $resolved) 1 }; $source = [System.IO.File]::ReadAllBytes($resolved); if ($source.Length -ne $originalBytes -or (Get-Sha256Bytes $source) -ne $originalSha256) { Fail ('bulk replacement original integrity mismatch for ' + $resolved) 23 }; $lines = New-Object System.Collections.Generic.List[string]; $sourceText = [System.Text.Encoding]::UTF8.GetString($source); foreach ($line in ($sourceText -split \"`n\", -1)) { $lines.Add($line) | Out-Null }; if ($lines.Count -gt 0 -and $lines[$lines.Count - 1] -eq '') { $lines.RemoveAt($lines.Count - 1) }; $replacements = New-Object System.Collections.Generic.List[object]; if (-not [string]::IsNullOrEmpty($replacementsText)) { foreach ($item in ($replacementsText -split ';')) { if ([string]::IsNullOrWhiteSpace($item)) { continue }; $parts = $item -split ',', 3; if ($parts.Count -ne 3) { Fail 'bulk replacement malformed replacement' 23 }; $newText = Decode-Utf8 $parts[2]; $newLines = New-Object System.Collections.Generic.List[string]; foreach ($line in ($newText -split \"`n\", -1)) { $newLines.Add($line) | Out-Null }; if ($newLines.Count -gt 0 -and $newLines[$newLines.Count - 1] -eq '') { $newLines.RemoveAt($newLines.Count - 1) }; $replacements.Add([pscustomobject]@{ start = [Int32]$parts[0]; oldLength = [Int32]$parts[1]; newLines = [string[]]$newLines }) | Out-Null } }; foreach ($replacement in @($replacements | Sort-Object -Property start -Descending)) { if ($replacement.start -lt 0 -or $replacement.oldLength -lt 0 -or ($replacement.start + $replacement.oldLength) -gt $lines.Count) { Fail ('bulk replacement out of bounds for ' + $resolved) 23 }; $removeCount = $replacement.oldLength; if ($removeCount -gt 0) { $lines.RemoveRange($replacement.start, $removeCount) }; if ($replacement.newLines.Count -gt 0) { $lines.InsertRange($replacement.start, [string[]]$replacement.newLines) } }; $outputText = if ($lines.Count -eq 0) { '' } else { ([string]::Join(\"`n\", [string[]]$lines) + \"`n\") }; $outputBytes = [System.Text.Encoding]::UTF8.GetBytes($outputText); if ($outputBytes.Length -ne $finalBytes -or (Get-Sha256Bytes $outputBytes) -ne $finalSha256) { Fail ('bulk replacement final integrity mismatch for ' + $resolved) 23 }; $tmp = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($resolved), ('.' + [System.IO.Path]::GetFileName($resolved) + '.unidesk-v2-bulk-' + [guid]::NewGuid().ToString('N') + '.tmp')); [System.IO.File]::WriteAllText($tmp, $outputText, $utf8); $plans.Add([pscustomobject]@{ target = $resolved; tmp = $tmp; sha256 = $finalSha256 }) | Out-Null }; foreach ($plan in $plans) { Ensure-Parent $plan.target; Move-Item -LiteralPath $plan.tmp -Destination $plan.target -Force; if ((Get-Sha256 $plan.target) -ne $plan.sha256) { Fail ('bulk replacement final sha256 mismatch for ' + $plan.target) 25 } }; break }", " 'write-b64-stdin' { Decode-ToTarget $target ([Console]::In.ReadToEnd()) ([Int64]$arg1) $arg2; break }", " 'write-b64-begin' { Ensure-Parent $target; Set-TmpPaths $target $arg1; [System.IO.File]::WriteAllText($script:tmpB64, '', [System.Text.Encoding]::ASCII); break }", " 'write-b64-append-stdin' { Set-TmpPaths $target $arg1; $chunk = ([Console]::In.ReadToEnd()) -replace '\\s',''; [System.IO.File]::AppendAllText($script:tmpB64, $chunk, [System.Text.Encoding]::ASCII); break }", " 'write-b64-commit' { Set-TmpPaths $target $arg1; $encoded = [System.IO.File]::ReadAllText($script:tmpB64, [System.Text.Encoding]::ASCII); Remove-Item -LiteralPath $script:tmpB64 -Force -ErrorAction SilentlyContinue; Decode-ToTarget $target $encoded ([Int64]$arg2) $arg3; break }", " 'delete' { Remove-Item -LiteralPath $target -Force -ErrorAction SilentlyContinue; break }", " default { Fail ('unsupported apply-patch fs op: ' + $operation) 2 }", "}", ].join(" "); } function chunkString(value: string, chunkSize: number): string[] { const chunks: string[] = []; for (let index = 0; index < value.length; index += chunkSize) { chunks.push(value.slice(index, index + chunkSize)); } return chunks.length > 0 ? chunks : [""]; } async function runSshCaptureCommand(config: UniDeskConfig, invocation: ParsedSshInvocation, command: string[], input?: string): Promise { const remoteCommand = remoteCommandForRoute(invocation.route, command, { stdin: input !== undefined }); return await runSshCaptureRemoteCommand(config, invocation, remoteCommand, input); } async function runSshCaptureRemoteCommand(config: UniDeskConfig, invocation: ParsedSshInvocation, remoteCommand: string, input?: string): Promise { const startedAtMs = Date.now(); const size = terminalSize(); const runtimeTimeoutMs = sshRuntimeTimeoutMs(); const payload = { providerId: invocation.providerId, command: wrapSshRemoteCommand(remoteCommand), cwd: sshRoutePayloadCwd(invocation.route), tty: false, stdinEotOnEnd: false, openTimeoutMs: Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000)), runtimeTimeoutMs, cols: size.cols, rows: size.rows, }; const payloadJson = JSON.stringify(payload); const encodedBrokerSource = Buffer.from(brokerSource(), "utf8").toString("base64"); const script = [ "set -eu", `payload=${shellQuote(payloadJson)}`, "if command -v backend-core >/dev/null 2>&1; then", ' exec backend-core --ssh-broker "$payload"', "fi", `export UNIDESK_SSH_BROKER_URL=${shellQuote("ws://127.0.0.1:8080/ws/ssh")}`, 'broker_js="$(mktemp "${TMPDIR:-/tmp}/unidesk-ssh-broker.XXXXXX")"', 'trap \'rm -f "$broker_js"\' EXIT', `printf %s ${shellQuote(encodedBrokerSource)} | base64 -d >"$broker_js"`, 'bun "$broker_js" "$payload"', ].join("\n"); const child = spawn("docker", ["exec", "-i", "unidesk-backend-core", "sh", "-c", script], { cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"], }); if (input !== undefined) { writeChunkedStdin(child.stdin, input); } else { child.stdin.end(); } let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString("utf8"); }); child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString("utf8"); }); return await new Promise((resolve) => { let settled = false; let killTimer: NodeJS.Timeout | null = null; const finish = (exitCode: number): void => { if (settled) return; settled = true; clearTimeout(runtimeTimer); if (killTimer !== null) clearTimeout(killTimer); const timingHint = formatSshRuntimeTimingHint(sshRuntimeTimingHint({ invocation, transport: "backend-core-broker", exitCode, startedAtMs, })); if (timingHint) stderr += timingHint; resolve({ exitCode, stdout, stderr }); }; const runtimeTimer = setTimeout(() => { const hint = formatSshRuntimeTimeoutHint(sshRuntimeTimeoutHint({ invocation, transport: "backend-core-broker", timeoutMs: runtimeTimeoutMs, })); stderr += hint; try { child.kill("SIGTERM"); } catch { // Ignore. } killTimer = setTimeout(() => { try { child.kill("SIGKILL"); } catch { // Ignore. } }, 2000); finish(124); }, runtimeTimeoutMs); child.on("error", (error) => { stderr += `unidesk ssh failed to start broker: ${error.message}\n`; finish(255); }); child.on("close", (code) => finish(code ?? 255)); }); } async function runSshStreamRemoteCommand( config: UniDeskConfig, invocation: ParsedSshInvocation, remoteCommand: string, handlers: SshRemoteCommandStreamHandlers, input?: string, options: { inactivityTimeoutMs?: number } = {}, ): Promise { const streamInvocation = { ...invocation, parsed: { ...invocation.parsed, remoteCommand, requiresStdin: input !== undefined, invocationKind: "helper" as const }, }; const startedAtMs = Date.now(); const size = terminalSize(); const inactivityTimeoutMs = options.inactivityTimeoutMs ?? sshRuntimeTimeoutMs(); const runtimeTimeoutMs = options.inactivityTimeoutMs === undefined ? inactivityTimeoutMs : Math.max(inactivityTimeoutMs * 4, 60 * 60_000); const payload = { providerId: invocation.providerId, command: wrapSshRemoteCommand(remoteCommand), cwd: sshRoutePayloadCwd(invocation.route), tty: false, stdinEotOnEnd: false, openTimeoutMs: Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000)), runtimeTimeoutMs, runtimeTimeoutMode: options.inactivityTimeoutMs === undefined ? "wall-clock" : "inactivity", cols: size.cols, rows: size.rows, }; const payloadJson = JSON.stringify(payload); const encodedBrokerSource = Buffer.from(brokerSource(), "utf8").toString("base64"); const script = [ "set -eu", `payload=${shellQuote(payloadJson)}`, "if command -v backend-core >/dev/null 2>&1; then", ' exec backend-core --ssh-broker "$payload"', "fi", `export UNIDESK_SSH_BROKER_URL=${shellQuote("ws://127.0.0.1:8080/ws/ssh")}`, 'broker_js="$(mktemp "${TMPDIR:-/tmp}/unidesk-ssh-broker.XXXXXX")"', 'trap \'rm -f "$broker_js"\' EXIT', `printf %s ${shellQuote(encodedBrokerSource)} | base64 -d >"$broker_js"`, 'bun "$broker_js" "$payload"', ].join("\n"); const child = spawn("docker", ["exec", "-i", "unidesk-backend-core", "sh", "-c", script], { cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"], }); if (input !== undefined) { writeChunkedStdin(child.stdin, input); } else { child.stdin.end(); } let stdout = ""; let stderr = ""; let streamError: unknown = null; let streamWrites = Promise.resolve(); const queueStreamWrite = (chunk: Buffer, stream: "stdout" | "stderr"): void => { streamWrites = streamWrites.then(async () => { if (stream === "stdout") await handlers.onStdout(chunk); else if (handlers.onStderr !== undefined) await handlers.onStderr(chunk); }).catch((error) => { streamError = error; stderr += `unidesk ssh stream sink failed: ${error instanceof Error ? error.message : String(error)}\n`; try { child.kill("SIGTERM"); } catch { // Ignore kill failures after a local stream sink error. } }); }; child.stdout.on("data", (chunk: Buffer) => { queueStreamWrite(chunk, "stdout"); }); child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString("utf8"); queueStreamWrite(chunk, "stderr"); }); return await new Promise((resolve) => { let settled = false; let killTimer: NodeJS.Timeout | null = null; let inactivityTimer: NodeJS.Timeout | null = null; const refreshActivityTimer = (): void => { if (settled || options.inactivityTimeoutMs === undefined) return; if (inactivityTimer !== null) clearTimeout(inactivityTimer); inactivityTimer = setTimeout(() => { const hint = formatSshRuntimeTimeoutHint(sshRuntimeTimeoutHint({ invocation: streamInvocation, transport: "backend-core-broker", timeoutMs: inactivityTimeoutMs, })); stderr += hint; try { child.kill("SIGTERM"); } catch { // Ignore. } killTimer = setTimeout(() => { try { child.kill("SIGKILL"); } catch { // Ignore. } }, 2000); finish(124); }, inactivityTimeoutMs); }; const finish = (exitCode: number): void => { if (settled) return; settled = true; clearTimeout(runtimeTimer); if (inactivityTimer !== null) clearTimeout(inactivityTimer); if (killTimer !== null) clearTimeout(killTimer); void streamWrites.then(() => { const finalCode = streamError === null ? exitCode : 255; const timingHint = formatSshRuntimeTimingHint(sshRuntimeTimingHint({ invocation: streamInvocation, transport: "backend-core-broker", exitCode: finalCode, startedAtMs, })); if (timingHint) stderr += timingHint; resolve({ exitCode: finalCode, stdout, stderr }); }); }; const runtimeTimer = setTimeout(() => { const hint = formatSshRuntimeTimeoutHint(sshRuntimeTimeoutHint({ invocation: streamInvocation, transport: "backend-core-broker", timeoutMs: runtimeTimeoutMs, })); stderr += hint; try { child.kill("SIGTERM"); } catch { // Ignore. } killTimer = setTimeout(() => { try { child.kill("SIGKILL"); } catch { // Ignore. } }, 2000); finish(124); }, runtimeTimeoutMs); refreshActivityTimer(); child.on("error", (error) => { stderr += `unidesk ssh failed to start broker: ${error.message}\n`; finish(255); }); child.on("close", (code) => finish(code ?? 255)); child.stdout.on("data", refreshActivityTimer); child.stderr.on("data", refreshActivityTimer); }); } async function runRemoteSsh(config: UniDeskConfig, host: string, providerId: string, args: string[]): Promise { const { runRemoteCli } = await import("./remote"); return await runRemoteCli({ host, user: "root", port: 22, projectRoot: repoRoot, identityFile: null, transport: "frontend", args: ["ssh", providerId, ...args], }, config); } export async function runSshCommandCapture(config: UniDeskConfig, target: string, args: string[], input?: string): Promise { const invocation = parseSshInvocation(target, args); const parsed = invocation.parsed; if (parsed.remoteCommand === null) throw new Error(`ssh ${target} capture requires a non-interactive operation`); const stdin = parsed.stdinPrefix !== undefined || parsed.stdinSuffix !== undefined ? `${parsed.stdinPrefix ?? ""}${input ?? ""}${parsed.stdinSuffix ?? ""}` : input; const plan = sshCaptureBackendPlan(config, process.env); if (plan.backend === "remote-frontend-websocket" && plan.remoteHost !== null) { return await runRemoteSshCapture(config, plan.remoteHost, target, args, stdin); } const local = await runSshCaptureRemoteCommand(config, invocation, parsed.remoteCommand, stdin); const fallbackHost = sshCaptureRemoteHost(config, process.env); if (local.exitCode !== 0 && fallbackHost !== null && isLocalSshCaptureBackendUnavailable(local)) { return await runRemoteSshCapture(config, fallbackHost, target, args, stdin); } return local; } async function runRemoteSshCapture(config: UniDeskConfig, host: string, target: string, args: string[], input?: string): Promise { const { runRemoteSshCommandCapture } = await import("./remote"); return await runRemoteSshCommandCapture(config, host, target, args, input); } export function sshCaptureBackendPlan(config: UniDeskConfig, env: NodeJS.ProcessEnv = process.env): SshCaptureBackendPlan { const localBackendCore = detectLocalBackendCoreStatus(); const remoteHost = sshCaptureRemoteHost(config, env); const runnerEnv = isRunnerEnvironment(env); if (runnerEnv && remoteHost !== null) return { backend: "remote-frontend-websocket", remoteHost, reason: "runner-environment", localBackendCore }; if (!localBackendCore.backendCoreContainer && remoteHost !== null) return { backend: "remote-frontend-websocket", remoteHost, reason: "local-backend-core-unavailable", localBackendCore }; return { backend: "local-backend-core-broker", remoteHost: null, reason: "main-server-local-backend-core", localBackendCore }; } function sshCaptureRemoteHost(config: UniDeskConfig, env: NodeJS.ProcessEnv): string | null { return normalizeRemoteHost(env.UNIDESK_MAIN_SERVER_IP) ?? normalizeRemoteHost(env.UNIDESK_MAIN_SERVER_HOST) ?? normalizeRemoteHost(env.CODE_QUEUE_DEV_CONTAINER_MASTER_HOST) ?? normalizeRemoteHost(config.network.publicHost); } function normalizeRemoteHost(raw: string | undefined): string | null { const value = raw?.trim() ?? ""; if (value.length === 0 || value === "localhost" || value === "127.0.0.1" || value === "::1") return null; return value.replace(/\/+$/u, ""); } function isRunnerEnvironment(env: NodeJS.ProcessEnv): boolean { return Boolean( env.AGENTRUN_BOOT_MODE || env.AGENTRUN_RUN_ID || env.AGENTRUN_K8S_JOB_NAME || env.CODE_QUEUE_SERVICE_ROLE || env.CODE_QUEUE_INSTANCE_ID || env.KUBERNETES_SERVICE_HOST, ); } function detectLocalBackendCoreStatus(): SshCaptureBackendPlan["localBackendCore"] { const result = spawnSync("docker", ["ps", "--format", "{{.Names}}"], { encoding: "utf8", timeout: 2000 }); if (result.error !== undefined) return { dockerExecutable: false, backendCoreContainer: false, error: result.error.message }; const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim(); return { dockerExecutable: result.status === 0, backendCoreContainer: result.status === 0 && String(result.stdout ?? "").split(/\r?\n/u).includes("unidesk-backend-core"), error: result.status === 0 ? null : output || `docker ps exited ${result.status ?? "unknown"}`, }; } function isLocalSshCaptureBackendUnavailable(result: SshCaptureResult): boolean { return /No such container: unidesk-backend-core|failed to start broker|Executable not found.*"docker"|docker: not found|Cannot connect to the Docker daemon/iu.test(result.stderr); } function writeChunkedStdin(stdin: NodeJS.WritableStream, input: string): void { const buffer = Buffer.from(input, "utf8"); const chunkSize = 32 * 1024; for (let offset = 0; offset < buffer.length; offset += chunkSize) { stdin.write(buffer.subarray(offset, Math.min(buffer.length, offset + chunkSize))); } stdin.end(); } export async function runSsh(config: UniDeskConfig, providerId: string, args: string[]): Promise { const normalizedArgs = normalizeSshOperationArgs(args); process.stderr.write(sshRouteSeparatorCompatibilityHint(args, normalizedArgs)); const plan = sshCaptureBackendPlan(config, process.env); if (plan.backend === "remote-frontend-websocket" && plan.remoteHost !== null) { return await runRemoteSsh(config, plan.remoteHost, providerId, normalizedArgs); } const invocation = parseSshInvocation(providerId, normalizedArgs); const parsed = invocation.parsed; const operationName = normalizedArgs[0] ?? ""; if (isSshFileTransferOperation(normalizedArgs)) { const executor: SshRemoteCommandExecutor = { runRemoteCommand: (remoteCommand, input) => runSshCaptureRemoteCommand(config, invocation, remoteCommand, input), streamRemoteCommand: (remoteCommand, handlers, input, options) => runSshStreamRemoteCommand(config, invocation, remoteCommand, handlers, input, options), }; return await runSshFileTransferOperation(invocation, normalizedArgs, executor, { buildRouteCommand: remoteCommandForRoute, buildWindowsPowerShellCommand: buildWindowsPowerShellInvocation, }); } if (operationName === "apply-patch") { const executor: ApplyPatchV2Executor = invocation.route.plane === "win" ? { fs: createWindowsApplyPatchFileSystem(config, invocation) } : { run: (command, input) => runSshCaptureCommand(config, invocation, command, input) }; return await runApplyPatchV2({ executor, stdin: process.stdin, stdout: process.stdout, stderr: process.stderr, argv: normalizedArgs.slice(1), timing: { providerId: invocation.providerId, route: invocation.route.raw, transport: "backend-core-broker", }, }); } const startedAtMs = Date.now(); const size = terminalSize(); const openTimeoutMs = Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000)); const runtimeTimeoutMs = sshRuntimeTimeoutMs(); const payload = { providerId: invocation.providerId, command: wrapSshRemoteCommand(parsed.remoteCommand, parsed.requiredHelpers), cwd: sshRoutePayloadCwd(invocation.route), tty: parsed.remoteCommand === null, stdinEotOnEnd: parsed.remoteCommand !== null && parsed.requiresStdin !== true, openTimeoutMs, runtimeTimeoutMs, cols: size.cols, rows: size.rows, }; const payloadJson = JSON.stringify(payload); const encodedBrokerSource = Buffer.from(brokerSource(), "utf8").toString("base64"); const script = [ "set -eu", `payload=${shellQuote(payloadJson)}`, "if command -v backend-core >/dev/null 2>&1; then", ' exec backend-core --ssh-broker "$payload"', "fi", `export UNIDESK_SSH_BROKER_URL=${shellQuote("ws://127.0.0.1:8080/ws/ssh")}`, 'broker_js="$(mktemp "${TMPDIR:-/tmp}/unidesk-ssh-broker.XXXXXX")"', 'trap \'rm -f "$broker_js"\' EXIT', `printf %s ${shellQuote(encodedBrokerSource)} | base64 -d >"$broker_js"`, 'bun "$broker_js" "$payload"', ].join("\n"); const child = spawn("docker", [ "exec", "-i", "unidesk-backend-core", "sh", "-c", script, ], { cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"], }); const rawMode = parsed.remoteCommand === null && process.stdin.isTTY; if (rawMode) process.stdin.setRawMode(true); process.stdin.resume(); if (parsed.stdinPrefix !== undefined || parsed.stdinSuffix !== undefined) { if (parsed.stdinPrefix) child.stdin.write(parsed.stdinPrefix); process.stdin.pipe(child.stdin, { end: false }); process.stdin.once("end", () => { if (parsed.stdinSuffix) child.stdin.write(parsed.stdinSuffix); child.stdin.end(); }); } else { process.stdin.pipe(child.stdin); } let stderrTail = ""; const appendStderrTail = (chunk: Buffer | string): void => { const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk; stderrTail = (stderrTail + text).slice(-16_384); }; if (parsed.remoteCommand === null) { child.stdout.pipe(process.stdout); } else { const stdoutForwarder = createSshStdoutForwarder({ invocation, transport: "backend-core-broker", }); child.stdout.on("data", (chunk: Buffer) => { const hint = stdoutForwarder.write(chunk); if (hint !== null) { appendStderrTail(hint); process.stderr.write(hint); } }); } child.stderr.on("data", (chunk: Buffer) => { appendStderrTail(chunk); process.stderr.write(chunk); }); return await new Promise((resolve) => { let settled = false; let timedOut = false; let killTimer: NodeJS.Timeout | null = null; const runtimeTimer = setTimeout(() => { if (settled) return; timedOut = true; const hint = sshRuntimeTimeoutHint({ invocation, transport: "backend-core-broker", timeoutMs: runtimeTimeoutMs, }); const formatted = formatSshRuntimeTimeoutHint(hint); appendStderrTail(formatted); process.stderr.write(formatted); try { child.stdin.destroy(); } catch { // Ignore stdin teardown failures on the timeout path. } try { child.kill("SIGTERM"); } catch { // Ignore kill failures and fall through to finish. } killTimer = setTimeout(() => { try { child.kill("SIGKILL"); } catch { // Process may have already exited. } }, 2000); finish(124); }, runtimeTimeoutMs); const restore = (): void => { clearTimeout(runtimeTimer); if (killTimer && !timedOut) clearTimeout(killTimer); process.stdin.unpipe(child.stdin); if (rawMode) process.stdin.setRawMode(false); }; const finish = (exitCode: number): void => { if (settled) return; settled = true; restore(); const hint = timedOut ? null : sshFailureHint(invocation.providerId, parsed, exitCode, stderrTail); if (hint !== null) process.stderr.write(formatSshFailureHint(hint)); const timingHint = formatSshRuntimeTimingHint(sshRuntimeTimingHint({ invocation, transport: "backend-core-broker", exitCode, startedAtMs, })); if (timingHint) process.stderr.write(timingHint); resolve(exitCode); }; child.on("error", (error) => { const message = `unidesk ssh failed to start broker: ${error.message}\n`; appendStderrTail(message); process.stderr.write(message); finish(255); }); child.on("close", (code) => { finish(code ?? 255); }); }); }