Skip to content
Merged
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
65 changes: 32 additions & 33 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@
const nim: typeof import("./inference/nim") = require("./inference/nim");
const onboardSession: typeof import("./state/onboard-session") = require("./state/onboard-session");
const { OnboardRuntimeBoundary }: typeof import("./onboard/runtime-boundary") = require("./onboard/runtime-boundary");
const { handleAgentSetupState }: typeof import("./onboard/machine/handlers/agent-setup") = require("./onboard/machine/handlers/agent-setup");
const { handleGatewayState }: typeof import("./onboard/machine/handlers/gateway") = require("./onboard/machine/handlers/gateway");
const { handlePreflightState }: typeof import("./onboard/machine/handlers/preflight") = require("./onboard/machine/handlers/preflight");
const { handleProviderInferenceState }: typeof import("./onboard/machine/handlers/provider-inference") = require("./onboard/machine/handlers/provider-inference");
Expand Down Expand Up @@ -9543,41 +9544,39 @@
selectedMessagingChannels = sandboxStateResult.selectedMessagingChannels;
const webSearchSupported = sandboxStateResult.webSearchSupported;

if (agent) {
await agentOnboard.handleAgentSetup(sandboxName, model, provider, agent, resume, session, {
step,
runCaptureOpenshell,
openshellShellCommand,
openshellBinary: getOpenshellBinary(),
const agentSetupResult = await handleAgentSetupState({
agent,
sandboxName,
model,
provider,
resume,
session,
hermesAuthMethod,
hermesToolGateways,
deps: {
handleAgentSetup: agentOnboard.handleAgentSetup,
agentSetupContext: () => ({
step,
runCaptureOpenshell,
openshellShellCommand,
openshellBinary: getOpenshellBinary(),
startRecordedStep,
recordStepComplete,
recordStepFailed,
skippedStepMessage,
}),
ensureAgentDashboardForward,
recordStepSkipped,
isOpenclawReady,
skippedStepMessage,
startRecordedStep,
setupOpenclaw,
syncNemoClawConfigInSandbox,
recordStepComplete,
recordStepFailed,
skippedStepMessage,
});
ensureAgentDashboardForward(sandboxName, agent);
await recordStepSkipped("openclaw");
} else {
const resumeOpenclaw = resume && sandboxName && isOpenclawReady(sandboxName);
if (resumeOpenclaw) {
skippedStepMessage("openclaw", sandboxName);
// Rebuild leaves /sandbox/.nemoclaw/config.json as Dockerfile's
// zero-byte placeholder; re-sync to avoid loadOnboardConfig
// SyntaxError. Fixes #3999.
syncNemoClawConfigInSandbox(sandboxName, provider, model);
await recordStepComplete(
"openclaw",
toSessionUpdates({ sandboxName, provider, model, hermesAuthMethod, hermesToolGateways }),
);
} else {
await startRecordedStep("openclaw", { sandboxName, provider, model });
await setupOpenclaw(sandboxName, model, provider);
await recordStepComplete(
"openclaw",
toSessionUpdates({ sandboxName, provider, model, hermesAuthMethod, hermesToolGateways }),
);
}
await recordStepSkipped("agent_setup");
}
toSessionUpdates: (updates) => toSessionUpdates(updates as Parameters<typeof toSessionUpdates>[0]),
},
});
session = agentSetupResult.session;

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

The value assigned to session here is unused.

const latestSession = onboardSession.loadSession();
const recordedPolicyPresets = Array.isArray(latestSession?.policyPresets)
Expand Down
162 changes: 162 additions & 0 deletions src/lib/onboard/machine/handlers/agent-setup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";

import { createSession, type Session, type SessionUpdates } from "../../../state/onboard-session";
import { handleAgentSetupState, type AgentSetupStateOptions } from "./agent-setup";

type Agent = { name: string; displayName: string };

function createDeps(overrides: Partial<AgentSetupStateOptions<Agent>["deps"]> = {}) {
let session = createSession();
const calls = {
handleAgentSetup: vi.fn(async () => undefined),
context: vi.fn(() => ({ ctx: true })),
ensureDashboard: vi.fn(() => 18789),
skipped: vi.fn(async (stepName: string) => {
session.steps[stepName].status = "skipped";
return session;
}),
openclawReady: vi.fn(() => false),
skippedMessage: vi.fn(),
startStep: vi.fn(async () => undefined),
setupOpenclaw: vi.fn(async () => undefined),
syncConfig: vi.fn(),
complete: vi.fn(async (stepName: string, updates: SessionUpdates = {}) => {
session.steps[stepName].status = "complete";
Object.assign(session, updates);
return session;
}),
};
return {
calls,
deps: {
handleAgentSetup: calls.handleAgentSetup,
agentSetupContext: calls.context,
ensureAgentDashboardForward: calls.ensureDashboard,
recordStepSkipped: calls.skipped,
isOpenclawReady: calls.openclawReady,
skippedStepMessage: calls.skippedMessage,
startRecordedStep: calls.startStep,
setupOpenclaw: calls.setupOpenclaw,
syncNemoClawConfigInSandbox: calls.syncConfig,
recordStepComplete: calls.complete,
toSessionUpdates: (updates: Record<string, unknown>) => updates as SessionUpdates,
...overrides,
},
};
}

function baseOptions(
deps: AgentSetupStateOptions<Agent>["deps"],
agent: Agent | null = null,
): AgentSetupStateOptions<Agent> {
return {
agent,
sandboxName: "my-assistant",
model: "model",
provider: "provider",
resume: false,
session: createSession(),
hermesAuthMethod: null,
hermesToolGateways: [],
deps,
};
}

describe("handleAgentSetupState", () => {
it("delegates non-OpenClaw agent setup and skips openclaw", async () => {
const { deps, calls } = createDeps();
const agent = { name: "hermes", displayName: "Hermes" };
const session = createSession();

const result = await handleAgentSetupState({ ...baseOptions(deps, agent), session, resume: true });

expect(calls.handleAgentSetup).toHaveBeenCalledWith(
"my-assistant",
"model",
"provider",
agent,
true,
session,
{ ctx: true },
);
expect(calls.ensureDashboard).toHaveBeenCalledWith("my-assistant", agent);
expect(calls.skipped).toHaveBeenCalledWith("openclaw");
expect(calls.setupOpenclaw).not.toHaveBeenCalled();
expect(result.session?.steps.openclaw.status).toBe("skipped");
});

it("skips OpenClaw setup on resume when OpenClaw is ready", async () => {
const { deps, calls } = createDeps({ isOpenclawReady: vi.fn(() => true) });

const result = await handleAgentSetupState({ ...baseOptions(deps), resume: true });

expect(calls.skippedMessage).toHaveBeenCalledWith("openclaw", "my-assistant");
expect(calls.startStep).not.toHaveBeenCalled();
expect(calls.setupOpenclaw).not.toHaveBeenCalled();
expect(calls.syncConfig).toHaveBeenCalledWith("my-assistant", "provider", "model");
expect(calls.complete).toHaveBeenCalledWith(
"openclaw",
expect.objectContaining({ sandboxName: "my-assistant", provider: "provider", model: "model" }),
);
expect(calls.skipped).toHaveBeenCalledWith("agent_setup");
expect(result.session).toMatchObject({
sandboxName: "my-assistant",
provider: "provider",
model: "model",
steps: { openclaw: { status: "complete" }, agent_setup: { status: "skipped" } },
});
});

it("runs OpenClaw setup and skips agent_setup for the default agent", async () => {
const { deps, calls } = createDeps();

const result = await handleAgentSetupState({
...baseOptions(deps),
hermesAuthMethod: "oauth",
hermesToolGateways: ["github"],
});

expect(calls.startStep).toHaveBeenCalledWith("openclaw", {
sandboxName: "my-assistant",
provider: "provider",
model: "model",
});
expect(calls.setupOpenclaw).toHaveBeenCalledWith("my-assistant", "model", "provider");
expect(calls.syncConfig).not.toHaveBeenCalled();
expect(calls.complete).toHaveBeenCalledWith(
"openclaw",
expect.objectContaining({
sandboxName: "my-assistant",
provider: "provider",
model: "model",
hermesAuthMethod: "oauth",
hermesToolGateways: ["github"],
}),
);
expect(calls.skipped).toHaveBeenCalledWith("agent_setup");
expect(result.session).toMatchObject({
sandboxName: "my-assistant",
provider: "provider",
model: "model",
hermesAuthMethod: "oauth",
hermesToolGateways: ["github"],
steps: { openclaw: { status: "complete" }, agent_setup: { status: "skipped" } },
});
});

it("returns a session when the input session is null", async () => {
const { deps } = createDeps();

const result = await handleAgentSetupState({ ...baseOptions(deps), session: null });

expect(result.session).toMatchObject({
sandboxName: "my-assistant",
provider: "provider",
model: "model",
steps: { openclaw: { status: "complete" }, agent_setup: { status: "skipped" } },
});
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
89 changes: 89 additions & 0 deletions src/lib/onboard/machine/handlers/agent-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { Session, SessionUpdates } from "../../../state/onboard-session";

export interface AgentSetupStateOptions<Agent> {
agent: Agent | null;
sandboxName: string;
model: string;
provider: string;
resume: boolean;
session: Session | null;
hermesAuthMethod: string | null;
hermesToolGateways: string[];
deps: {
handleAgentSetup(
sandboxName: string,
model: string,
provider: string,
agent: Agent,
resume: boolean,
session: Session | null,
context: unknown,
): Promise<void>;
agentSetupContext(): unknown;
ensureAgentDashboardForward(sandboxName: string, agent: Agent): number;
recordStepSkipped(stepName: string): Promise<Session>;
isOpenclawReady(sandboxName: string): boolean;
skippedStepMessage(stepName: string, detail?: string | null): void;
startRecordedStep(
stepName: string,
updates: { sandboxName: string; provider: string; model: string },
): Promise<void>;
setupOpenclaw(sandboxName: string, model: string, provider: string): Promise<void>;
syncNemoClawConfigInSandbox(sandboxName: string, provider: string, model: string): void;
recordStepComplete(stepName: string, updates: SessionUpdates): Promise<Session>;
toSessionUpdates(updates: Record<string, unknown>): SessionUpdates;
};
}

export interface AgentSetupStateResult {
session: Session | null;
}

export async function handleAgentSetupState<Agent>({
agent,
sandboxName,
model,
provider,
resume,
session,
hermesAuthMethod,
hermesToolGateways,
deps,
}: AgentSetupStateOptions<Agent>): Promise<AgentSetupStateResult> {
if (agent) {
await deps.handleAgentSetup(
sandboxName,
model,
provider,
agent,
resume,
session,
deps.agentSetupContext(),
);
deps.ensureAgentDashboardForward(sandboxName, agent);
session = await deps.recordStepSkipped("openclaw");
return { session };
}

const resumeOpenclaw = resume && sandboxName && deps.isOpenclawReady(sandboxName);
if (resumeOpenclaw) {
deps.skippedStepMessage("openclaw", sandboxName);
deps.syncNemoClawConfigInSandbox(sandboxName, provider, model);
await deps.recordStepComplete(
"openclaw",
deps.toSessionUpdates({ sandboxName, provider, model, hermesAuthMethod, hermesToolGateways }),
);
} else {
await deps.startRecordedStep("openclaw", { sandboxName, provider, model });
await deps.setupOpenclaw(sandboxName, model, provider);
await deps.recordStepComplete(
"openclaw",
deps.toSessionUpdates({ sandboxName, provider, model, hermesAuthMethod, hermesToolGateways }),
);
}
session = await deps.recordStepSkipped("agent_setup");
return { session };
}
Loading