From 4b42805c08dfc94aa31d94bd066f2ad45bcd3ff2 Mon Sep 17 00:00:00 2001 From: Yimo Jiang Date: Wed, 20 May 2026 04:53:13 +0000 Subject: [PATCH] fix(cli): guard OpenShell ssh-config calls Signed-off-by: Yimo Jiang --- docs/reference/cli-selection-guide.mdx | 5 +- src/lib/actions/sandbox/process-recovery.ts | 13 ++-- src/lib/actions/sandbox/skill-install.ts | 11 ++-- src/lib/adapters/openshell/client.test.ts | 66 ++++++++++++++++++++- src/lib/adapters/openshell/client.ts | 43 ++++++++++++-- src/lib/adapters/openshell/runtime.ts | 14 ++++- src/lib/diagnostics/debug.ts | 11 ++-- src/lib/sandbox/version.test.ts | 30 +++++----- src/lib/sandbox/version.ts | 20 ++++--- src/lib/share-command-deps.ts | 12 ++-- src/lib/state/sandbox.ts | 6 +- 11 files changed, 173 insertions(+), 58 deletions(-) diff --git a/docs/reference/cli-selection-guide.mdx b/docs/reference/cli-selection-guide.mdx index 88536fd92bf..751db7724cd 100644 --- a/docs/reference/cli-selection-guide.mdx +++ b/docs/reference/cli-selection-guide.mdx @@ -98,7 +98,8 @@ Use `openshell` when the docs explicitly call for a live OpenShell gateway opera ```console $ openshell sandbox list $ openshell sandbox get - $ openshell logs --tail + $ openshell logs -n 20 + $ openshell doctor check ``` - Run one-off commands or move files without starting a NemoClaw chat session: @@ -151,7 +152,7 @@ $ openshell sandbox exec -n my-assistant -- cat /tmp/gateway.log Use `nemoclaw status` and `nemoclaw logs` first. They combine NemoClaw registry data, OpenShell state, OpenClaw process health, inference health, policy details, and messaging-channel warnings. -Use `openshell sandbox list`, `openshell sandbox get`, or `openshell logs` when debugging lower-level OpenShell behavior. +Use `openshell sandbox list`, `openshell sandbox get`, `openshell logs -n 20`, or `openshell doctor check` when debugging lower-level OpenShell behavior. ### Approve Blocked Network Requests diff --git a/src/lib/actions/sandbox/process-recovery.ts b/src/lib/actions/sandbox/process-recovery.ts index 255755edd14..e4c8e445b8a 100644 --- a/src/lib/actions/sandbox/process-recovery.ts +++ b/src/lib/actions/sandbox/process-recovery.ts @@ -6,21 +6,22 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import * as agentRuntime from "../../agent/runtime"; -import { DASHBOARD_PORT } from "../../core/ports"; -import { ROOT, shellQuote } from "../../runner"; import { captureOpenshell, captureOpenshellForStatus, + captureSandboxSshConfig, getOpenshellBinary, isCommandTimeout, runOpenshell, } from "../../adapters/openshell/runtime"; import { OPENSHELL_PROBE_TIMEOUT_MS } from "../../adapters/openshell/timeouts"; -import * as registry from "../../state/registry"; -import { parseForwardList } from "../../state/sandbox-session"; +import * as agentRuntime from "../../agent/runtime"; import { G, R } from "../../cli/terminal-style"; +import { DASHBOARD_PORT } from "../../core/ports"; import { sleepSeconds } from "../../core/wait"; +import { ROOT, shellQuote } from "../../runner"; +import * as registry from "../../state/registry"; +import { parseForwardList } from "../../state/sandbox-session"; export type SandboxCommandResult = { status: number; @@ -83,7 +84,7 @@ export function executeSandboxCommand( sandboxName: string, command: string, ): SandboxCommandResult | null { - const sshConfigResult = captureOpenshell(["sandbox", "ssh-config", sandboxName], { + const sshConfigResult = captureSandboxSshConfig(sandboxName, { ignoreError: true, timeout: OPENSHELL_PROBE_TIMEOUT_MS, }); diff --git a/src/lib/actions/sandbox/skill-install.ts b/src/lib/actions/sandbox/skill-install.ts index e6da0fd6805..956fc9441e4 100644 --- a/src/lib/actions/sandbox/skill-install.ts +++ b/src/lib/actions/sandbox/skill-install.ts @@ -5,13 +5,13 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; - +import { captureSandboxSshConfig } from "../../adapters/openshell/runtime"; +import { OPENSHELL_PROBE_TIMEOUT_MS } from "../../adapters/openshell/timeouts"; import * as agentRuntime from "../../agent/runtime"; import { CLI_NAME } from "../../cli/branding"; -import { captureOpenshell } from "../../adapters/openshell/runtime"; -import { ensureLiveSandboxOrExit } from "./gateway-state"; -import * as skillInstall from "../../skill-install"; import { D, G, R, YW } from "../../cli/terminal-style"; +import * as skillInstall from "../../skill-install"; +import { ensureLiveSandboxOrExit } from "./gateway-state"; export function printSkillInstallUsage(): void { console.log(""); @@ -169,8 +169,9 @@ export async function installSandboxSkill( const paths = skillInstall.resolveSkillPaths(agent, frontmatter.name); // 4. Get SSH config - const sshConfigResult = captureOpenshell(["sandbox", "ssh-config", sandboxName], { + const sshConfigResult = captureSandboxSshConfig(sandboxName, { ignoreError: true, + timeout: OPENSHELL_PROBE_TIMEOUT_MS, }); if (sshConfigResult.status !== 0) { console.error(" Failed to obtain SSH configuration for the sandbox."); diff --git a/src/lib/adapters/openshell/client.test.ts b/src/lib/adapters/openshell/client.test.ts index d4bcaad1fa0..95bbabc2dc0 100644 --- a/src/lib/adapters/openshell/client.test.ts +++ b/src/lib/adapters/openshell/client.test.ts @@ -6,8 +6,9 @@ import type { SpawnSyncReturns } from "node:child_process"; import { describe, expect, it } from "vitest"; import { - captureOpenshellCommandAsync, captureOpenshellCommand, + captureOpenshellCommandAsync, + captureSandboxSshConfigCommand, getInstalledOpenshellVersion, type OpenshellSpawnSync, parseVersionFromText, @@ -153,6 +154,59 @@ describe("openshell helpers", () => { }); }); + it("verifies sandbox existence before requesting SSH config", () => { + const calls: string[][] = []; + const spawnSyncImpl: OpenshellSpawnSync = (_command, args) => { + calls.push([...args]); + if (args.join(" ") === "sandbox get alpha") { + return makeSpawnResult({ status: 0, stdout: "alpha Ready\n", stderr: "" }); + } + return makeSpawnResult({ + status: 0, + stdout: "Host openshell-alpha\n", + stderr: "", + }); + }; + + const result = captureSandboxSshConfigCommand("openshell", "alpha", { spawnSyncImpl }); + + expect(result).toEqual({ status: 0, output: "Host openshell-alpha" }); + expect(calls).toEqual([ + ["sandbox", "get", "alpha"], + ["sandbox", "ssh-config", "alpha"], + ]); + }); + + it("does not request SSH config when the sandbox is missing", () => { + const calls: string[][] = []; + const spawnSyncImpl: OpenshellSpawnSync = (_command, args) => { + calls.push([...args]); + return makeSpawnResult({ status: 1, stdout: "", stderr: "sandbox not found\n" }); + }; + + const result = captureSandboxSshConfigCommand("openshell", "bogus", { spawnSyncImpl }); + + expect(result).toEqual({ status: 1, output: "sandbox 'bogus' not found" }); + expect(calls).toEqual([["sandbox", "get", "bogus"]]); + }); + + it("preserves non-NotFound sandbox lookup failures", () => { + const calls: string[][] = []; + const spawnSyncImpl: OpenshellSpawnSync = (_command, args) => { + calls.push([...args]); + return makeSpawnResult({ + status: 1, + stdout: "", + stderr: "transport error\nConnection refused\n", + }); + }; + + const result = captureSandboxSshConfigCommand("openshell", "alpha", { spawnSyncImpl }); + + expect(result).toEqual({ status: 1, output: "transport error\nConnection refused" }); + expect(calls).toEqual([["sandbox", "get", "alpha"]]); + }); + it("bounds async captures and reports timeout metadata", async () => { const script = [ "const { spawn } = require('node:child_process');", @@ -173,6 +227,16 @@ describe("openshell helpers", () => { expect(result.signal).toBeTruthy(); }); + it("includes stderr in async capture output when requested", async () => { + const result = await captureOpenshellCommandAsync( + process.execPath, + ["-e", "process.stdout.write('hello\\n'); process.stderr.write('boom\\n'); process.exitCode = 1;"], + { ignoreError: true, includeStderr: true }, + ); + + expect(result).toEqual({ status: 1, output: "hello\nboom", signal: null }); + }); + it("uses the injected exit handler on failure", () => { expect(() => runOpenshellCommand("openshell", ["status"], { diff --git a/src/lib/adapters/openshell/client.ts b/src/lib/adapters/openshell/client.ts index 5e50370b7f6..940445dbd01 100644 --- a/src/lib/adapters/openshell/client.ts +++ b/src/lib/adapters/openshell/client.ts @@ -2,12 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { - spawn, - spawnSync, type ChildProcess, type SpawnSyncOptions, type SpawnSyncOptionsWithStringEncoding, type SpawnSyncReturns, + spawn, + spawnSync, } from "node:child_process"; export type OpenshellSpawnSync = ( @@ -32,7 +32,9 @@ export interface RunOpenshellOptions extends OpenshellSpawnOptions { stdio?: SpawnSyncOptions["stdio"]; } -export interface CaptureOpenshellOptions extends OpenshellSpawnOptions {} +export interface CaptureOpenshellOptions extends OpenshellSpawnOptions { + includeStderr?: boolean; +} export interface CaptureOpenshellAsyncOptions extends CaptureOpenshellOptions { killGraceMs?: number; @@ -88,6 +90,14 @@ function isIgnoredTimeout(error: Error, opts: OpenshellSpawnOptions): boolean { return opts.ignoreError === true && (error as NodeJS.ErrnoException).code === "ETIMEDOUT"; } +function shouldIncludeStderr(opts: CaptureOpenshellOptions): boolean { + return opts.includeStderr === true || opts.ignoreError !== true; +} + +function captureOutput(result: SpawnSyncReturns, opts: CaptureOpenshellOptions): string { + return `${result.stdout || ""}${shouldIncludeStderr(opts) ? result.stderr || "" : ""}`.trim(); +} + function timeoutError(binary: string, args: string[], timeout: number): NodeJS.ErrnoException { const error = new Error( `spawn ${binary} ${args.join(" ")} timed out after ${timeout} ms`, @@ -158,7 +168,7 @@ export function captureOpenshellCommand( if (isIgnoredTimeout(result.error, opts)) { return { status: result.status, - output: `${result.stdout || ""}${opts.ignoreError ? "" : result.stderr || ""}`.trim(), + output: captureOutput(result, opts), error: result.error, signal: result.signal, }; @@ -167,10 +177,31 @@ export function captureOpenshellCommand( } return { status: result.status ?? 1, - output: `${result.stdout || ""}${opts.ignoreError ? "" : result.stderr || ""}`.trim(), + output: captureOutput(result, opts), }; } +export function captureSandboxSshConfigCommand( + binary: string, + sandboxName: string, + opts: CaptureOpenshellOptions = {}, +): CaptureOpenshellResult { + const sandboxGet = captureOpenshellCommand(binary, ["sandbox", "get", sandboxName], { + ...opts, + ignoreError: true, + includeStderr: true, + }); + if (sandboxGet.status !== 0) { + const output = sandboxGet.output || `failed to query sandbox '${sandboxName}'`; + const sandboxMissing = /\bnot[- ]?found\b/i.test(output); + return { + ...sandboxGet, + output: sandboxMissing ? `sandbox '${sandboxName}' not found` : output, + }; + } + return captureOpenshellCommand(binary, ["sandbox", "ssh-config", sandboxName], opts); +} + export function captureOpenshellCommandAsync( binary: string, args: string[], @@ -200,7 +231,7 @@ export function captureOpenshellCommandAsync( if (forceTimer) clearTimeout(forceTimer); }; - const buildOutput = () => `${stdout}${opts.ignoreError ? "" : stderr}`.trim(); + const buildOutput = () => `${stdout}${shouldIncludeStderr(opts) ? stderr : ""}`.trim(); const settle = ( status: number | null, diff --git a/src/lib/adapters/openshell/runtime.ts b/src/lib/adapters/openshell/runtime.ts index 3dfb7842d64..f69c5c358d5 100644 --- a/src/lib/adapters/openshell/runtime.ts +++ b/src/lib/adapters/openshell/runtime.ts @@ -8,11 +8,12 @@ import { ROOT } from "../../runner"; import { captureOpenshellCommand, captureOpenshellCommandAsync, + captureSandboxSshConfigCommand, getInstalledOpenshellVersion, runOpenshellCommand, } from "./client"; -import { OPENSHELL_PROBE_TIMEOUT_MS } from "./timeouts"; import { resolveOpenshell } from "./resolve"; +import { OPENSHELL_PROBE_TIMEOUT_MS } from "./timeouts"; type CommandArgs = string[]; @@ -59,6 +60,17 @@ export function captureOpenshell(args: CommandArgs, opts: RunnerOptions = {}) { }); } +export function captureSandboxSshConfig(sandboxName: string, opts: RunnerOptions = {}) { + return captureSandboxSshConfigCommand(getOpenshellBinary(), sandboxName, { + cwd: ROOT, + env: opts.env, + ignoreError: opts.ignoreError, + timeout: opts.timeout, + errorLine: console.error, + exit: (code: number) => process.exit(code), + }); +} + export function getStatusProbeTimeoutMs(): number { const raw = process.env.NEMOCLAW_STATUS_PROBE_TIMEOUT_MS; const parsed = raw ? Number(raw) : NaN; diff --git a/src/lib/diagnostics/debug.ts b/src/lib/diagnostics/debug.ts index 5ef9f0c2bff..09759982468 100644 --- a/src/lib/diagnostics/debug.ts +++ b/src/lib/diagnostics/debug.ts @@ -55,6 +55,7 @@ function section(title: string): void { // --------------------------------------------------------------------------- import { redactFull as redact } from "../security/redact"; + export { redact }; // --------------------------------------------------------------------------- @@ -296,18 +297,14 @@ function collectSandboxInternals( ): void { if (!commandExists("openshell")) return; - // Check if sandbox exists + // Check if sandbox exists. OpenShell ssh-config may succeed for unknown + // names, so verify the live sandbox first. try { - const output = execFileSync("openshell", ["sandbox", "list"], { + execFileSync("openshell", ["sandbox", "get", sandboxName], { encoding: "utf-8", timeout: 10_000, stdio: ["ignore", "pipe", "ignore"], }); - const names = output - .split("\n") - .map((l) => l.trim().split(/\s+/)[0]) - .filter((n) => n && n.toLowerCase() !== "name"); - if (!names.includes(sandboxName)) return; } catch { return; } diff --git a/src/lib/sandbox/version.test.ts b/src/lib/sandbox/version.test.ts index 8597ae35d8c..15137a3344a 100644 --- a/src/lib/sandbox/version.test.ts +++ b/src/lib/sandbox/version.test.ts @@ -1,10 +1,10 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; -import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs"; -import { join } from "node:path"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // Mock heavy dependencies that pull in the full module graph vi.mock("../adapters/openshell/resolve.js", () => ({ @@ -28,7 +28,7 @@ vi.mock("../adapters/openshell/client.js", () => ({ } return true; }, - captureOpenshellCommand: vi.fn(), + captureSandboxSshConfigCommand: vi.fn(), })); vi.mock("../agent/defs.js", () => ({ @@ -47,11 +47,11 @@ vi.mock("child_process", async (importOriginal) => { return { ...actual, spawnSync: vi.fn() }; }); -import { checkAgentVersion, formatStalenessWarning } from "./version.js"; -import * as registry from "../state/registry.js"; -import { captureOpenshellCommand } from "../adapters/openshell/client.js"; -import { OPENSHELL_PROBE_TIMEOUT_MS } from "../adapters/openshell/timeouts.js"; import { spawnSync } from "child_process"; +import { captureSandboxSshConfigCommand } from "../adapters/openshell/client.js"; +import { OPENSHELL_PROBE_TIMEOUT_MS } from "../adapters/openshell/timeouts.js"; +import * as registry from "../state/registry.js"; +import { checkAgentVersion, formatStalenessWarning } from "./version.js"; describe("checkAgentVersion", () => { let tmpDir: string; @@ -113,7 +113,7 @@ describe("checkAgentVersion", () => { it("slow path: probes via SSH when no cached version", () => { registry.registerSandbox({ name: "test-sb", agent: null }); - vi.mocked(captureOpenshellCommand).mockReturnValue({ + vi.mocked(captureSandboxSshConfigCommand).mockReturnValue({ status: 0, output: "Host openshell-test-sb\n HostName 127.0.0.1\n", }); @@ -131,9 +131,9 @@ describe("checkAgentVersion", () => { expect(result.detectionMethod).toBe("ssh-exec"); expect(result.sandboxVersion).toBe("2026.4.24"); expect(result.isStale).toBe(false); - expect(captureOpenshellCommand).toHaveBeenCalledWith( + expect(captureSandboxSshConfigCommand).toHaveBeenCalledWith( "/usr/local/bin/openshell", - ["sandbox", "ssh-config", "test-sb"], + "test-sb", { ignoreError: true, timeout: OPENSHELL_PROBE_TIMEOUT_MS }, ); @@ -145,7 +145,7 @@ describe("checkAgentVersion", () => { it("returns unavailable when SSH config fails", () => { registry.registerSandbox({ name: "test-sb", agent: null }); - vi.mocked(captureOpenshellCommand).mockReturnValue({ + vi.mocked(captureSandboxSshConfigCommand).mockReturnValue({ status: 1, output: "", }); @@ -157,7 +157,7 @@ describe("checkAgentVersion", () => { it("can skip live probing when no cached version is available", () => { registry.registerSandbox({ name: "test-sb", agent: null }); - vi.mocked(captureOpenshellCommand).mockClear(); + vi.mocked(captureSandboxSshConfigCommand).mockClear(); vi.mocked(spawnSync).mockClear(); const result = checkAgentVersion("test-sb", { skipProbe: true }); @@ -165,7 +165,7 @@ describe("checkAgentVersion", () => { expect(result.detectionMethod).toBe("unavailable"); expect(result.sandboxVersion).toBeNull(); expect(result.isStale).toBe(false); - expect(captureOpenshellCommand).not.toHaveBeenCalled(); + expect(captureSandboxSshConfigCommand).not.toHaveBeenCalled(); expect(spawnSync).not.toHaveBeenCalled(); }); @@ -176,7 +176,7 @@ describe("checkAgentVersion", () => { agentVersion: "2026.3.11", }); - vi.mocked(captureOpenshellCommand).mockReturnValue({ + vi.mocked(captureSandboxSshConfigCommand).mockReturnValue({ status: 0, output: "Host openshell-test-sb\n HostName 127.0.0.1\n", }); diff --git a/src/lib/sandbox/version.ts b/src/lib/sandbox/version.ts index b9b19596906..5e57aa12664 100644 --- a/src/lib/sandbox/version.ts +++ b/src/lib/sandbox/version.ts @@ -13,12 +13,15 @@ import fs from "fs"; import os from "os"; import path from "path"; -import { parseVersionFromText, versionGte } from "../adapters/openshell/client.js"; -import * as registry from "../state/registry.js"; -import { loadAgent } from "../agent/defs.js"; +import { + captureSandboxSshConfigCommand, + parseVersionFromText, + versionGte, +} from "../adapters/openshell/client.js"; import { resolveOpenshell } from "../adapters/openshell/resolve.js"; -import { captureOpenshellCommand } from "../adapters/openshell/client.js"; import { OPENSHELL_PROBE_TIMEOUT_MS } from "../adapters/openshell/timeouts.js"; +import { loadAgent } from "../agent/defs.js"; +import * as registry from "../state/registry.js"; export interface VersionCheckResult { sandboxVersion: string | null; @@ -47,11 +50,10 @@ export function probeAgentVersion(sandboxName: string): string | null { const openshellBinary = resolveOpenshell(); if (!openshellBinary) return null; - const sshConfigResult = captureOpenshellCommand( - openshellBinary, - ["sandbox", "ssh-config", sandboxName], - { ignoreError: true, timeout: OPENSHELL_PROBE_TIMEOUT_MS }, - ); + const sshConfigResult = captureSandboxSshConfigCommand(openshellBinary, sandboxName, { + ignoreError: true, + timeout: OPENSHELL_PROBE_TIMEOUT_MS, + }); if (sshConfigResult.status !== 0) return null; const tmpFile = path.join(os.tmpdir(), `nemoclaw-ver-${process.pid}-${Date.now()}.conf`); diff --git a/src/lib/share-command-deps.ts b/src/lib/share-command-deps.ts index 9c7286474cc..e9f8e382819 100644 --- a/src/lib/share-command-deps.ts +++ b/src/lib/share-command-deps.ts @@ -1,12 +1,12 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { CLI_NAME } from "./cli/branding"; import { OPENSHELL_PROBE_TIMEOUT_MS } from "./adapters/openshell/timeouts"; +import { CLI_NAME } from "./cli/branding"; import { G, R } from "./cli/terminal-style"; export interface ShareCommandDeps { - /** Run `openshell sandbox ssh-config ` and return output. */ + /** Verify sandbox existence, then run `openshell sandbox ssh-config ` and return output. */ getSshConfig: (sandboxName: string) => { status: number | null; output: string }; /** Ensure the sandbox is live, exit process if not. */ ensureLive: (sandboxName: string) => Promise; @@ -28,11 +28,15 @@ export interface ShareCommandDeps { } export function buildShareCommandDeps(): ShareCommandDeps { - const { captureOpenshell } = require("./adapters/openshell/runtime") as { + const { captureOpenshell, captureSandboxSshConfig } = require("./adapters/openshell/runtime") as { captureOpenshell: ( args: string[], opts?: { ignoreError?: boolean; timeout?: number }, ) => { status: number | null; output: string }; + captureSandboxSshConfig: ( + sandboxName: string, + opts?: { ignoreError?: boolean; timeout?: number }, + ) => { status: number | null; output: string }; }; const { ensureLiveSandboxOrExit } = require("./actions/sandbox/gateway-state") as { ensureLiveSandboxOrExit: (sandboxName: string) => Promise; @@ -40,7 +44,7 @@ export function buildShareCommandDeps(): ShareCommandDeps { return { getSshConfig: (sandboxName: string) => - captureOpenshell(["sandbox", "ssh-config", sandboxName], { + captureSandboxSshConfig(sandboxName, { ignoreError: true, timeout: OPENSHELL_PROBE_TIMEOUT_MS, }), diff --git a/src/lib/state/sandbox.ts b/src/lib/state/sandbox.ts index 042ddb7ffee..72508fbed44 100644 --- a/src/lib/state/sandbox.ts +++ b/src/lib/state/sandbox.ts @@ -25,8 +25,9 @@ import os from "node:os"; import path from "node:path"; import { spawnSync } from "child_process"; -import { captureOpenshellCommand } from "../adapters/openshell/client.js"; +import { captureSandboxSshConfigCommand } from "../adapters/openshell/client.js"; import { resolveOpenshell } from "../adapters/openshell/resolve.js"; +import { OPENSHELL_PROBE_TIMEOUT_MS } from "../adapters/openshell/timeouts.js"; import type { AgentStateFile } from "../agent/defs.js"; import { loadAgent } from "../agent/defs.js"; import { shellQuote } from "../runner.js"; @@ -469,8 +470,9 @@ function getSshConfig(sandboxName: string): string | null { const openshellBinary = resolveOpenshell(); if (!openshellBinary) return null; - const result = captureOpenshellCommand(openshellBinary, ["sandbox", "ssh-config", sandboxName], { + const result = captureSandboxSshConfigCommand(openshellBinary, sandboxName, { ignoreError: true, + timeout: OPENSHELL_PROBE_TIMEOUT_MS, }); if (result.status !== 0) return null; return result.output;