Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
6b753bf
docs(onboard): document FSM migration target
cv May 27, 2026
fb1b32d
refactor(onboard): centralize machine state metadata
cv May 27, 2026
c3e4ad6
refactor(onboard): derive session step mapping from FSM metadata
cv May 27, 2026
603832c
refactor(onboard): derive progress labels from FSM metadata
cv May 27, 2026
4fad8e7
fix(onboard): emit lifecycle events for onboarding start
cv May 28, 2026
f99e9cb
fix(onboard): emit machine events for resume conflicts
cv May 28, 2026
2b60df4
refactor(onboard): introduce explicit state result types
cv May 28, 2026
30341b0
refactor(onboard): apply explicit state results through runtime
cv May 28, 2026
d4ad2d9
refactor(onboard): make finalization return FSM result
cv May 28, 2026
356c947
refactor(onboard): make agent setup return FSM result
cv May 28, 2026
2296519
refactor(onboard): make policy setup return FSM result
cv May 28, 2026
67a9a1e
refactor(onboard): make preflight and gateway return FSM results
cv May 28, 2026
46f4a49
refactor(onboard): make sandbox return branch FSM result
cv May 28, 2026
9cc15f5
refactor(onboard): return FSM results from provider inference
cv May 28, 2026
dbbb273
refactor(onboard): add FSM runner shell
cv May 28, 2026
6b27a0b
refactor(onboard): consume handler FSM results compatibly
cv May 28, 2026
44009ad
refactor(onboard): allow step recording without machine transitions
cv May 28, 2026
cd6e5f7
refactor(onboard): plumb step mutation options through runtime
cv May 28, 2026
e266e3b
refactor(onboard): add record-only FSM runner adapter
cv May 28, 2026
bf4da0b
refactor(onboard): return ordered provider FSM results
cv May 28, 2026
212ff4d
refactor(onboard): run live sequence with record-only steps
cv May 28, 2026
f69f60a
refactor(onboard): let FSM handlers return result sequences
cv May 29, 2026
727ac69
refactor(onboard): add sequence runner adapter
cv May 29, 2026
75f82f7
refactor(onboard): support FSM runner stop states
cv May 29, 2026
59deee6
refactor(onboard): define FSM flow context
cv May 29, 2026
25c5abf
refactor(onboard): extract preflight and gateway FSM phases
cv May 29, 2026
4d9cc9f
refactor(onboard): extract provider and sandbox FSM phases
cv May 29, 2026
8a5b54a
refactor(onboard): extract agent policy finalization FSM phases
cv May 29, 2026
a1af752
merge(onboard): sync FSM stop states with main
cv Jun 9, 2026
5900708
merge(onboard): sync flow context with stop states
cv Jun 9, 2026
8576e5f
merge(onboard): sync preflight phases with flow context
cv Jun 9, 2026
42eeb90
merge(onboard): sync provider sandbox phases with preflight phases
cv Jun 9, 2026
dc8c463
test(onboard): cover sandbox branch metadata passthrough
cv Jun 9, 2026
5c76573
merge(onboard): sync finalization phases with provider sandbox phases
cv Jun 9, 2026
68722ed
test(onboard): run finalization phases through sequence runner
cv Jun 9, 2026
5c7b7ee
Merge branch 'main' into stack/onboard-fsm-flow-context
jyaunches Jun 9, 2026
83c1412
Merge branch 'main' into stack/onboard-fsm-flow-context
cv Jun 9, 2026
6abbbb4
Merge branch 'stack/onboard-fsm-flow-context' into stack/onboard-fsm-…
cv Jun 9, 2026
c07f0dd
Merge branch 'stack/onboard-fsm-preflight-gateway-phases' into stack/…
cv Jun 9, 2026
cf6c2a9
Merge branch 'stack/onboard-fsm-provider-sandbox-phases' into stack/o…
cv Jun 9, 2026
23e8577
Merge branch 'main' into stack/onboard-fsm-flow-context
cv Jun 9, 2026
abbd8c4
chore: apply static formatting for FSM flow stack
cv Jun 9, 2026
b592eac
Merge remote-tracking branch 'origin/stack/onboard-fsm-flow-context' …
cv Jun 9, 2026
adbbdfa
chore(onboard): format preflight gateway FSM phase
cv Jun 9, 2026
8a6bca2
Merge remote-tracking branch 'origin/stack/onboard-fsm-preflight-gate…
cv Jun 9, 2026
baeab37
chore(onboard): format provider sandbox FSM phase
cv Jun 9, 2026
5118bd7
Merge remote-tracking branch 'origin/stack/onboard-fsm-provider-sandb…
cv Jun 9, 2026
a64242b
chore(onboard): format finalization FSM phases
cv Jun 9, 2026
e55a6d4
Merge branch 'main' into stack/onboard-fsm-agent-policy-finalization-…
cv Jun 9, 2026
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
182 changes: 182 additions & 0 deletions src/lib/onboard/machine/flow-phases/agent-policy-finalization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

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

import {
createSession,
filterSafeUpdates,
MACHINE_SNAPSHOT_VERSION,
normalizeSession,
sanitizeFailure,
type Session,
type SessionUpdates,
} from "../../../state/onboard-session";
import type { OnboardFlowContext } from "../flow-context";
import { advanceTo, completeOnboardMachine } from "../result";
import { OnboardRuntime, type OnboardRuntimeDeps } from "../runtime";
import { runOnboardSequenceWithRunner } from "../sequence-runner";
import {
createAgentSetupPhase,
createFinalizationPhase,
createOpenclawSetupPhase,
createPoliciesPhase,
createPostVerifyPhase,
} from "./agent-policy-finalization";

function context(): OnboardFlowContext<null, null, null> {
return {
resume: false,
fresh: false,
session: createSession(),
agent: null,
recordedSandboxName: null,
requestedSandboxName: null,
sandboxName: "my-assistant",
fromDockerfile: null,
model: "model",
provider: "provider",
endpointUrl: null,
credentialEnv: null,
hermesAuthMethod: null,
hermesToolGateways: [],
preferredInferenceApi: null,
nimContainer: null,
webSearchConfig: null,
webSearchSupported: true,
selectedMessagingChannels: [],
gpu: null,
sandboxGpuConfig: null,
gpuPassthrough: false,
};
}

function cloneSession(session: Session): Session {
return normalizeSession(JSON.parse(JSON.stringify(session))) ?? session;
}

function createRuntime(initialSession: Session = createSession()) {
let session = cloneSession(initialSession);
const updateSession = (mutator: (value: Session) => Session | void): Session => {
session = cloneSession(mutator(cloneSession(session)) ?? session);
return cloneSession(session);
Comment on lines +60 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve in-place session mutations when mutator returns void.

At Line 61, mutator(...) ?? session drops mutations if the mutator uses the allowed void return path. Use the mutated draft as fallback to honor the OnboardRuntimeDeps.updateSession contract.

Suggested patch
 const updateSession = (mutator: (value: Session) => Session | void): Session => {
-  session = cloneSession(mutator(cloneSession(session)) ?? session);
+  const draft = cloneSession(session);
+  session = cloneSession(mutator(draft) ?? draft);
   return cloneSession(session);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const updateSession = (mutator: (value: Session) => Session | void): Session => {
session = cloneSession(mutator(cloneSession(session)) ?? session);
return cloneSession(session);
const updateSession = (mutator: (value: Session) => Session | void): Session => {
const draft = cloneSession(session);
session = cloneSession(mutator(draft) ?? draft);
return cloneSession(session);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard/machine/flow-phases/agent-policy-finalization.test.ts` around
lines 60 - 62, The updateSession helper currently discards in-place mutations
because it uses `mutator(cloneSession(session)) ?? session`; change it to keep
the mutated draft when the mutator returns void by capturing the draft result
and falling back to that draft instead of the original session. In other words,
call the mutator with a cloned draft, store the draft after the mutator runs,
and use the mutator's return value if present or the mutated draft otherwise
before assigning/returning the cloned session; update references to
`updateSession`, `mutator`, `cloneSession`, `Session`, and the
`OnboardRuntimeDeps.updateSession` contract accordingly.

};
const deps: OnboardRuntimeDeps = {
loadSession: () => cloneSession(session),
createSession,
saveSession: (next) => {
session = cloneSession(next);
return cloneSession(session);
},
updateSession,
markStepStarted: () => cloneSession(session),
markStepComplete: (_stepName, updates: SessionUpdates = {}) =>
updateSession((current) => {
Object.assign(current, filterSafeUpdates(updates));
return current;
}),
markStepCompleteRecordOnly: (_stepName, updates: SessionUpdates = {}) =>
updateSession((current) => {
Object.assign(current, filterSafeUpdates(updates));
return current;
}),
markStepSkipped: () => cloneSession(session),
markStepFailed: (stepName, message) =>
updateSession((current) => {
current.status = "failed";
current.failure = sanitizeFailure({ step: stepName, message, recordedAt: "now" });
return current;
}),
markStepFailedRecordOnly: () => cloneSession(session),
completeSession: (updates: SessionUpdates = {}) =>
updateSession((current) => {
Object.assign(current, filterSafeUpdates(updates));
current.status = "complete";
current.resumable = false;
return current;
}),
filterSafeUpdates,
emitEvent: () => undefined,
now: () => "2026-05-29T00:00:00.000Z",
};
return new OnboardRuntime(deps);
}

describe("agent/policy/finalization phases", () => {
it("creates branch-specific setup phases", async () => {
const agentPhase = createAgentSetupPhase(async () => ({ result: advanceTo("policies") }));
const openclawPhase = createOpenclawSetupPhase(async () => ({ result: advanceTo("policies") }));

expect(agentPhase.state).toBe("agent_setup");
expect(openclawPhase.state).toBe("openclaw");
await expect(agentPhase.run(context())).resolves.toMatchObject({
result: { next: "policies" },
});
await expect(openclawPhase.run(context())).resolves.toMatchObject({
result: { next: "policies" },
});
});

it("maps policies context updates", async () => {
const phase = createPoliciesPhase(async () => ({
context: { selectedMessagingChannels: ["slack"] },
result: advanceTo("finalizing"),
}));

const result = await phase.run(context());

expect(phase.state).toBe("policies");
expect(result.context.selectedMessagingChannels).toEqual(["slack"]);
expect(result.result).toMatchObject({ next: "finalizing" });
});

it("creates finalization and post-verify phases", async () => {
const finalizing = createFinalizationPhase(async () => ({ result: advanceTo("post_verify") }));
const postVerify = createPostVerifyPhase(async () => ({
result: completeOnboardMachine({ sandboxName: "my-assistant" }),
}));

expect(finalizing.state).toBe("finalizing");
expect(postVerify.state).toBe("post_verify");
await expect(finalizing.run(context())).resolves.toMatchObject({
result: { next: "post_verify" },
});
await expect(postVerify.run(context())).resolves.toMatchObject({
result: { type: "complete" },
});
});

it("runs branch-to-completion phases through the strict FSM runner", async () => {
const initialSession = createSession({
machine: {
version: MACHINE_SNAPSHOT_VERSION,
state: "openclaw",
stateEnteredAt: "2026-05-29T00:00:00.000Z",
revision: 0,
},
});

const result = await runOnboardSequenceWithRunner({
context: context(),
runtime: createRuntime(initialSession),
phases: [
createOpenclawSetupPhase(async () => ({ result: advanceTo("policies") })),
createPoliciesPhase(async () => ({
context: { selectedMessagingChannels: ["slack"] },
result: advanceTo("finalizing"),
})),
createFinalizationPhase(async () => ({ result: advanceTo("post_verify") })),
createPostVerifyPhase(async () => ({
result: completeOnboardMachine({ sandboxName: "my-assistant" }),
})),
],
});

expect(result.session).toMatchObject({
status: "complete",
sandboxName: "my-assistant",
machine: { state: "complete" },
});
expect(result.context.selectedMessagingChannels).toEqual(["slack"]);
});
});
57 changes: 57 additions & 0 deletions src/lib/onboard/machine/flow-phases/agent-policy-finalization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { OnboardFlowContext, OnboardFlowPhaseResult } from "../flow-context";
import { mergeOnboardFlowContext, onboardFlowPhaseResult } from "../flow-context";
import type { OnboardSequencePhase } from "../sequence-runner";

type FlowPhaseHandler<Context extends OnboardFlowContext> = (context: Context) => Promise<{
context?: Partial<Context>;
result: OnboardFlowPhaseResult<Context>["result"];
}>;

function createFlowPhase<Context extends OnboardFlowContext>(
state: OnboardSequencePhase<Context>["state"],
runPhase: FlowPhaseHandler<Context>,
): OnboardSequencePhase<Context> {
return {
state,
async run(context) {
const result = await runPhase(context);
return onboardFlowPhaseResult(
result.context ? mergeOnboardFlowContext(context, result.context) : context,
result.result,
);
},
};
}

export function createAgentSetupPhase<Context extends OnboardFlowContext>(
runAgentSetup: FlowPhaseHandler<Context>,
): OnboardSequencePhase<Context> {
return createFlowPhase("agent_setup", runAgentSetup);
}

export function createOpenclawSetupPhase<Context extends OnboardFlowContext>(
runOpenclawSetup: FlowPhaseHandler<Context>,
): OnboardSequencePhase<Context> {
return createFlowPhase("openclaw", runOpenclawSetup);
}

export function createPoliciesPhase<Context extends OnboardFlowContext>(
runPolicies: FlowPhaseHandler<Context>,
): OnboardSequencePhase<Context> {
return createFlowPhase("policies", runPolicies);
}

export function createFinalizationPhase<Context extends OnboardFlowContext>(
runFinalization: FlowPhaseHandler<Context>,
): OnboardSequencePhase<Context> {
return createFlowPhase("finalizing", runFinalization);
}

export function createPostVerifyPhase<Context extends OnboardFlowContext>(
runPostVerify: FlowPhaseHandler<Context>,
): OnboardSequencePhase<Context> {
return createFlowPhase("post_verify", runPostVerify);
}