Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,13 @@ verify_downloaded_script() {

resolve_default_sandbox_name() {
local registry_file="${HOME}/.nemoclaw/sandboxes.json"
local sandbox_name="${NEMOCLAW_SANDBOX_NAME:-}"
local sandbox_name=""

# Prefer the sandbox name from the current onboard session — it reflects
# the sandbox just created, whereas sandboxes.json may hold a stale default
# from a previous gateway that no longer exists (#1839).
local session_file="${HOME}/.nemoclaw/onboard-session.json"
if [[ -z "$sandbox_name" && -f "$session_file" ]] && command_exists node; then
if [[ -f "$session_file" ]] && command_exists node; then
sandbox_name="$(
node -e '
const fs = require("fs");
Expand All @@ -191,6 +191,10 @@ resolve_default_sandbox_name() {
)"
fi

if [[ -z "$sandbox_name" ]]; then
sandbox_name="${NEMOCLAW_SANDBOX_NAME:-}"
fi

if [[ -z "$sandbox_name" && -f "$registry_file" ]] && command_exists node; then
sandbox_name="$(
node -e '
Expand All @@ -207,7 +211,11 @@ resolve_default_sandbox_name() {
)"
fi

printf "%s" "${sandbox_name:-my-assistant}"
local fallback="my-assistant"
if [[ "${NEMOCLAW_AGENT:-}" == "hermes" ]]; then
fallback="hermes"
fi
printf "%s" "${sandbox_name:-$fallback}"
}

resolve_onboarded_agent() {
Expand Down
14 changes: 14 additions & 0 deletions src/lib/agent-onboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0

import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest";
import fs from "node:fs";
import path from "node:path";
// Import from compiled dist/ so coverage is attributed correctly.
import { printDashboardUi } from "../../dist/lib/agent-onboard";
import type { AgentDefinition } from "./agent-defs";
Expand Down Expand Up @@ -120,3 +122,15 @@ describe("printDashboardUi — regression for #2078 (port 8642 is not a chat UI)
expect(output).toContain("http://127.0.0.1:19000/#token=tok");
});
});

describe("handleAgentSetup guards", () => {
it("fails onboarding instead of completing when the agent binary or health probe is missing", () => {
const source = fs.readFileSync(path.join(import.meta.dirname, "agent-onboard.ts"), "utf-8");

expect(source).toContain("verifyAgentBinaryAvailable");
expect(source).toContain("failAgentSetup");
expect(source).toContain('onboardSession.markStepFailed("agent_setup"');
expect(source).toContain("gateway did not respond within");
expect(source).not.toContain("gateway may still be starting");
});
});
52 changes: 49 additions & 3 deletions src/lib/agent-onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import fs from "fs";
import os from "os";
import path from "path";

import { ROOT, run } from "./runner";
import { ROOT, run, shellQuote } from "./runner";
import { dockerBuild, dockerImageInspect } from "./docker";
import { loadAgent, resolveAgentName, type AgentDefinition } from "./agent-defs";
import { getAgentBranding } from "./branding";
import { getProviderSelectionConfig } from "./inference-config";
import * as onboardSession from "./onboard-session";
import { sleepSeconds } from "./wait";
Expand Down Expand Up @@ -110,6 +111,40 @@ function sleep(seconds: number): void {
sleepSeconds(seconds);
}

function agentCliName(agent: AgentDefinition): string {
return getAgentBranding(agent.name).cli;
}

function agentExecutableName(agent: AgentDefinition): string {
const configuredPath = typeof agent.binary_path === "string" ? agent.binary_path.trim() : "";
return path.basename(configuredPath || agent.name);
}

function verifyAgentBinaryAvailable(
sandboxName: string,
agent: AgentDefinition,
runCaptureOpenshell: OnboardContext["runCaptureOpenshell"],
): boolean {
const executable = agentExecutableName(agent);
const binaryPath = typeof agent.binary_path === "string" ? agent.binary_path.trim() : "";
const script = [
`command -v ${shellQuote(executable)} >/dev/null 2>&1 && echo ok && exit 0`,
binaryPath ? `[ -x ${shellQuote(binaryPath)} ] && echo ok && exit 0` : "true",
"exit 1",
].join("; ");
const result = runCaptureOpenshell(["sandbox", "exec", sandboxName, "sh", "-lc", script], {
ignoreError: true,
});
return Boolean(result && result.includes("ok"));
}

function failAgentSetup(sandboxName: string, agent: AgentDefinition, message: string): never {
onboardSession.markStepFailed("agent_setup", message);
console.error(` \u2717 ${message}`);
console.error(` Check: ${agentCliName(agent)} ${sandboxName} logs --follow`);
process.exit(1);
}

/**
* Handle the full agent setup step (step 7) including resume detection.
* For non-OpenClaw agents: writes config into the sandbox and verifies
Expand Down Expand Up @@ -154,6 +189,14 @@ export async function handleAgentSetup(
startRecordedStep("agent_setup", { sandboxName, provider, model });
step(7, 8, `Setting up ${agent.displayName} inside sandbox`);

if (!verifyAgentBinaryAvailable(sandboxName, agent, runCaptureOpenshell)) {
failAgentSetup(
sandboxName,
agent,
`${agent.displayName} binary '${agentExecutableName(agent)}' is missing inside sandbox '${sandboxName}'`,
);
}

const selectionConfig = getProviderSelectionConfig(provider, model);
if (selectionConfig) {
const sandboxConfig = {
Expand Down Expand Up @@ -195,8 +238,11 @@ export async function handleAgentSetup(
if (healthy) {
console.log(` \u2713 ${agent.displayName} gateway is healthy`);
} else {
console.log(` \u26a0 ${agent.displayName} gateway did not respond within ${timeoutSecs}s.`);
console.log(` The gateway may still be starting. Check: nemoclaw ${sandboxName} logs`);
failAgentSetup(
sandboxName,
agent,
`${agent.displayName} gateway did not respond within ${timeoutSecs}s`,
);
}
} else {
console.log(` \u2713 ${agent.displayName} configured inside sandbox`);
Expand Down
42 changes: 34 additions & 8 deletions src/lib/inventory-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,33 @@ describe("inventory commands", () => {
expect(lines).toContain(" Recovered 1 sandbox entry from the live OpenShell gateway.");
expect(lines).toContain(" alpha *");
expect(lines).toContain(
" model: nvidia/nemotron-3-super-120b-a12b provider: nvidia-prod GPU policies: pypi",
" agent: openclaw model: nvidia/nemotron-3-super-120b-a12b provider: nvidia-prod GPU policies: pypi",
);
});

it("prints the per-sandbox agent type in list output", async () => {
const lines: string[] = [];
await listSandboxesCommand({
recoverRegistryEntries: async () => ({
sandboxes: [
{
name: "hermes",
model: "nvidia/nemotron-3-super-120b-a12b",
provider: "nvidia-prod",
gpuEnabled: false,
policies: [],
agent: "hermes",
},
],
defaultSandbox: "hermes",
}),
getLiveInference: () => null,
loadLastSession: () => null,
log: (message = "") => lines.push(message),
});

expect(lines).toContain(
" agent: hermes model: nvidia/nemotron-3-super-120b-a12b provider: nvidia-prod CPU policies: none",
);
});

Expand Down Expand Up @@ -153,19 +179,19 @@ describe("inventory commands", () => {

// Default sandbox reflects live gateway state, with an onboarded drift note.
expect(lines).toContain(
" model: live-model provider: live-provider GPU policies: none",
" agent: openclaw model: live-model provider: live-provider GPU policies: none",
);
// Stale stored row for the default sandbox must not leak through.
expect(lines).not.toContain(
" model: configured-alpha provider: configured-provider GPU policies: none",
" agent: openclaw model: configured-alpha provider: configured-provider GPU policies: none",
);
expect(lines).toContain(
" (onboarded: model=configured-alpha, provider=configured-provider)",
);
// Non-default sandbox keeps its stored config — the gateway only applies
// to whichever sandbox is currently connected.
expect(lines).toContain(
" model: configured-beta provider: beta-provider CPU policies: none",
" agent: openclaw model: configured-beta provider: beta-provider CPU policies: none",
);
});

Expand All @@ -190,7 +216,7 @@ describe("inventory commands", () => {
});

expect(lines).toContain(
" model: configured-alpha provider: configured-provider GPU policies: none",
" agent: openclaw model: configured-alpha provider: configured-provider GPU policies: none",
);
expect(lines.some((l) => l.includes("onboarded"))).toBe(false);
});
Expand All @@ -216,7 +242,7 @@ describe("inventory commands", () => {
});

expect(lines).toContain(
" model: configured-alpha provider: configured-provider GPU policies: none",
" agent: openclaw model: configured-alpha provider: configured-provider GPU policies: none",
);
expect(lines.some((l) => l.includes("onboarded"))).toBe(false);
});
Expand All @@ -243,7 +269,7 @@ describe("inventory commands", () => {
});

expect(lines).toContain(
" model: live-model provider: configured-provider GPU policies: none",
" agent: openclaw model: live-model provider: configured-provider GPU policies: none",
);
expect(lines).toContain(" (onboarded: model=configured-alpha)");
});
Expand All @@ -270,7 +296,7 @@ describe("inventory commands", () => {
});

expect(lines).toContain(
" model: configured-alpha provider: live-provider GPU policies: none",
" agent: openclaw model: configured-alpha provider: live-provider GPU policies: none",
);
expect(lines).toContain(" (onboarded: provider=configured-provider)");
});
Expand Down
5 changes: 4 additions & 1 deletion src/lib/inventory-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,11 @@ export function renderSandboxInventoryText(
const gpu = sandbox.gpuEnabled ? "GPU" : "CPU";
const presets = sandbox.policies.length > 0 ? sandbox.policies.join(", ") : "none";
const connected = sandbox.connected ? " ●" : "";
const agent = sandbox.agent || "openclaw";
log(` ${sandbox.name}${def}${connected}`);
log(` model: ${model} provider: ${provider} ${gpu} policies: ${presets}`);
log(
` agent: ${agent} model: ${model} provider: ${provider} ${gpu} policies: ${presets}`,
);
if (modelDrifted || providerDrifted) {
const parts: string[] = [];
if (modelDrifted) parts.push(`model=${sandbox.model || "unknown"}`);
Expand Down
Loading
Loading