Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7c809c5
refactor(onboard): make recovery and sandbox entry strict
cv Jul 28, 2026
6d2a218
refactor(onboard): make runtime the machine authority
cv Jul 28, 2026
f58ca88
test(onboard): cover exact sandbox phase entry
cv Jul 28, 2026
a7266db
merge(onboard): refresh FSM stack base
cv Jul 28, 2026
4e91c88
merge(onboard): refresh FSM stack from main
cv Jul 28, 2026
9b8ee24
merge(onboard): refresh FSM stack base
cv Jul 28, 2026
03a4341
chore(architecture): ratchet state root files
cv Jul 28, 2026
13ba3df
docs(onboard): state runtime ownership contract
cv Jul 28, 2026
4788d97
merge(onboard): refresh FSM stack from main
cv Jul 28, 2026
90dd9cd
merge(onboard): refresh FSM stack base
cv Jul 28, 2026
83d454f
merge: resolve conflicts with main
github-actions[bot] Aug 3, 2026
6650e85
fix(onboard): resolve runtime authority stack refresh
cv Aug 3, 2026
dc1ccb3
test(onboard): allow gateway reconcile coverage slack
cv Aug 3, 2026
ca084ef
refactor(onboard): remove legacy step mutation API
cv Aug 3, 2026
8fed8a7
refactor(onboard): remove duplicate step mutation API
cv Aug 3, 2026
5ea6980
test(package): allow policy boundary package slack
cv Aug 3, 2026
c85a048
fix(onboard): complete step mutation cleanup
cv Aug 3, 2026
46f8196
Merge remote-tracking branch 'origin/codex/onboard-fsm-runtime-author…
cv Aug 3, 2026
b1263d8
test(architecture): keep mutation absence guard linear
cv Aug 3, 2026
e78098d
Merge remote-tracking branch 'origin/codex/onboard-fsm-runtime-author…
cv Aug 3, 2026
a7c3fb9
fix(onboard): preserve interrupted session guidance
cv Aug 3, 2026
6a7a061
Merge remote-tracking branch 'origin/main' into codex/merge-train-7716
cv Aug 3, 2026
b1f9f0c
fix(onboard): preserve terminal transition errors
cv Aug 3, 2026
bcd3c03
merge: resolve conflicts with main
github-actions[bot] Aug 3, 2026
8381e19
merge(main): refresh #7716 with current main
cv Aug 3, 2026
48a2766
docs(onboard): clarify transition bookkeeping
cv Aug 3, 2026
dfd7278
docs(onboard): align step bookkeeping terms
cv Aug 3, 2026
73648b4
merge(onboard): refresh #7716 on current main
cv Aug 3, 2026
6f9db55
merge: refresh PR #7716 from main
cv Aug 3, 2026
e2dcffb
test(onboard): correct runtime boundary descriptions
cv Aug 3, 2026
288ebe2
merge: refresh #7716 onto main
cv Aug 3, 2026
26d4549
fix(onboard): restore skipped-step events
cv Aug 3, 2026
7c83651
merge: refresh #7716 onto main
cv Aug 3, 2026
7dfbb44
merge: resolve conflicts with main
github-actions[bot] Aug 3, 2026
c1552c6
refactor(onboard): extract flow handoff context (#7717)
cv Aug 3, 2026
49f779c
Merge branch 'main' into codex/onboard-fsm-runtime-authority
cv Aug 4, 2026
8da6eb3
merge(main): refresh #7716 after MCP runtime remediation
cv Aug 4, 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
2 changes: 1 addition & 1 deletion ci/source-architecture-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"src/lib/onboard": 310,
"src/lib/actions": 19,
"src/lib/actions/sandbox": 183,
"src/lib/state": 38,
"src/lib/state": 37,
"src/lib/inference": 62,
"scripts": 45
}
Expand Down
5 changes: 5 additions & 0 deletions ci/source-shape-test-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,11 @@
"test": "keeps migrated provider identities and implementations behind the one bundle composition",
"category": "compatibility"
},
{
"file": "test/source-architecture.test.ts",
"test": "keeps removed step mutation APIs out of production source (#7703)",
"category": "compatibility"
},
{
"file": "test/runtime-provider-source-shape.test.ts",
"test": "keeps the dormant managed-bootstrap protocol driver-neutral",
Expand Down
76 changes: 17 additions & 59 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ const {
restoreDefaultAfterRecreate,
}: typeof import("./onboard/cancel-rollback") = require("./onboard/cancel-rollback");
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
const { createProviderInferenceOnboardFlowPhase, createSandboxOnboardFlowPhase, runCoreOnboardFlowSlice }: typeof import("./onboard/machine/core-flow-phases") = require("./onboard/machine/core-flow-phases");
const { createProviderInferenceOnboardFlowPhase, createSandboxOnboardFlowPhase, prepareCoreOnboardFlowContext, prepareFinalOnboardFlowContext, runCoreOnboardFlowSlice }: typeof import("./onboard/machine/core-flow-phases") = require("./onboard/machine/core-flow-phases");
const {
createFinalOnboardFlowPhases,
runFinalOnboardFlowSlice,
Expand Down Expand Up @@ -4250,35 +4250,23 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {
recordRepairEvent,
});

const initialContext = initialFlowResult.context;
if (!initialContext.sandboxGpuConfig) {
throw new Error("Preflight did not produce a sandbox GPU configuration.");
}
session = initialFlowResult.session;
const sandboxGpuConfig = initialContext.sandboxGpuConfig;
const { gpuPassthrough } = initialContext;
const gpu = initialContext.gpu ?? null;

// #2753: for an unfinished sandbox, an explicit requested name precedes
// the checkpointed name from the interrupted session.
let sandboxName =
recordedSandboxName || requestedSandboxName || checkpointedSandboxName || null;
if (sandboxName && RESERVED_SANDBOX_NAMES.has(sandboxName)) {
console.error(
` Reserved name in resumed session: '${sandboxName}' is a ${cliDisplayName()} CLI command.`,
);
console.error(" Start a fresh onboard with --name <sandbox> to choose a different name.");
process.exit(1);
}
const coreFlowContext: InitialOnboardFlowContext = {
...initialContext,
session,
sandboxName,
const coreFlowContext = prepareCoreOnboardFlowContext({
initial: initialFlowResult,
recordedSandboxName,
requestedSandboxName,
checkpointedSandboxName,
selectedMessagingChannels,
gpu,
sandboxGpuConfig,
gpuPassthrough,
};
assertSandboxNameAllowed: (sandboxName) => {
if (!RESERVED_SANDBOX_NAMES.has(sandboxName)) return;
console.error(
` Reserved name in resumed session: '${sandboxName}' is a ${cliDisplayName()} CLI command.`,
);
console.error(" Start a fresh onboard with --name <sandbox> to choose a different name.");
process.exit(1);
},
});
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
const runCoreGatewayOpenshell = setupInferenceFactory.createGatewayScopedOpenshellRunner(runOpenshell, GATEWAY_NAME);
// biome-ignore format: keep src/lib/onboard.ts net-neutral for growth guardrail.
Expand Down Expand Up @@ -4444,38 +4432,8 @@ async function runOnboard(opts: OnboardOptions = {}): Promise<void> {
recordRepairEvent,
});
setupInferenceFactory.selectGatewayForFollowupOrExit(GATEWAY_NAME, runOpenshell);
const coreContext = coreFlowResult.context;
session = coreContext.session;
sandboxName = coreContext.sandboxName;
if (!sandboxName || !coreContext.model || !coreContext.provider) {
throw new Error("Onboarding state is incomplete after sandbox setup.");
}
const model = coreContext.model;
const provider = coreContext.provider;
const endpointUrl = coreContext.endpointUrl;
const credentialEnv = coreContext.credentialEnv;
const hermesAuthMethod = coreContext.hermesAuthMethod;
const hermesToolGateways = coreContext.hermesToolGateways;
const nimContainer = coreContext.nimContainer;
let webSearchConfig = coreContext.webSearchConfig as WebSearchConfig | null;
const webSearchSupported = coreContext.webSearchSupported;

const finalFlowContext: InitialOnboardFlowContext = {
...coreContext,
session,
sandboxName,
model,
provider,
endpointUrl,
credentialEnv,
hermesAuthMethod,
hermesToolGateways,
nimContainer,
webSearchConfig,
selectedMessagingChannels: coreContext.selectedMessagingChannels,
webSearchSupported,
};
let liveFinalFlowContext = finalFlowContext;
const finalFlowContext = prepareFinalOnboardFlowContext(coreFlowResult);
let liveFinalFlowContext: InitialOnboardFlowContext = finalFlowContext;

const finalFlowPhases = createFinalOnboardFlowPhases<
InitialOnboardFlowContext,
Expand Down
4 changes: 2 additions & 2 deletions src/lib/onboard/lifecycle-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ The schema and sanitation authority is `Session` plus `normalizeSession`/`filter
| Field group | Fields | Writer/owner and state meaning |
|---|---|---|
| Session envelope | `version`, `sessionId`, `mode`, `startedAt`, `updatedAt`, `status`, `resumable` | `createSession`, save/update helpers, and completion/failure paths. Values are always known after creation. |
| Progress and recovery | `lastStepStarted`, `lastCompletedStep`, `failure`, `steps`, `machine`, `sandboxPromptProgress`, `stagedCredentialProviders`, `checkpoint` | Step-mutation helpers and `OnboardRuntime` own whole-step progress; the OpenClaw sandbox handler owns prompt-group completion markers. `stagedCredentialProviders` contains only names registered before sandbox setup so OpenClaw resume can require both durable ownership and an exact live binding. A marker is trusted only when its matching persisted value is present and valid, including an explicit `null` where supported. `checkpoint` is the dedicated versioned resume contract: a secret-free tri-state decision record plus durable sandbox identity, effect-group receipts, and logical web-search and messaging provider bindings, serialized alongside the session under its own `schemaVersion` with fail-closed handling of an unknown future version. The primary inference provider binding remains owned and revalidated by the provider and inference phases instead of entering this checkpoint ledger. |
| Progress and recovery | `lastStepStarted`, `lastCompletedStep`, `failure`, `steps`, `machine`, `sandboxPromptProgress`, `stagedCredentialProviders`, `checkpoint` | Step helpers record step-progress bookkeeping and context updates accepted by `filterSafeUpdates`. `OnboardRuntime` owns machine transitions, terminal state, and machine events. Explicit session recovery and the process-exit failure backstop are separate recovery boundaries. The OpenClaw sandbox handler owns prompt-group completion markers. `stagedCredentialProviders` contains only names registered before sandbox setup so OpenClaw resume can require both durable ownership and an exact live binding. A marker is trusted only when its matching persisted value is present and valid, including an explicit `null` where supported. `checkpoint` is the dedicated versioned resume contract: a secret-free tri-state decision record plus durable sandbox identity, effect-group receipts, and logical web-search and messaging provider bindings, serialized alongside the session under its own `schemaVersion` with fail-closed handling of an unknown future version. The primary inference provider binding remains owned and revalidated by the provider and inference phases instead of entering this checkpoint ledger. |
| Target identity | `agent`, `sandboxName`, `metadata.gatewayName`, `metadata.fromDockerfile` | Onboard selection, sandbox handler/registration, and rebuild session preparation. A completed sandbox step or valid `sandboxPromptProgress.sandboxName` marker is the trust gate for a recorded name. |
| Inference intent | `provider`, `model`, `endpointUrl`, `credentialEnv`, `preferredInferenceApi`, `compatibleEndpointReasoning`, `nimContainer`, `webSearchConfig` | Provider/inference handlers and `runInferenceSet`. Known credential state is an environment-variable name or presence metadata, never the value. `redactUrl` masks userinfo and fragments, redacts values under sensitive parameter names, and redacts canonical token-shaped values even under benign parameter names. |
| Agent and policy intent | `hermesAuthMethod`, `toolDisclosure`, `hermesToolGateways`, `policyPresets` | Agent setup and policy handling. Channel commands update matching-session `policyPresets` only best-effort. Nullable fields conflate unset, declined, and cleared where the CLI makes those distinctions. |
Expand All @@ -200,7 +200,7 @@ The schema and sanitation authority is `Session` plus `normalizeSession`/`filter
| Runtime metadata | `routerPid`, `routerCredentialHash`, `gpuPassthrough` | Router and sandbox setup/recovery. PID is a live-process hint; credential hash is a digest; GPU is a concrete boolean. |
| Legacy migration proof | `migratedLegacyValueHashes` | Onboard legacy migration writes SHA-256 digests keyed by environment name; session filtering guarantees string records but does not independently validate digest shape. |

The registry is separately owned by `src/lib/state/registry.ts`; backup/recovery manifests are owned by their rebuild and recreate modules. Step helpers normally use `RECORD_ONLY_STEP_MUTATION_OPTIONS`; `LEGACY_MACHINE_STEP_MUTATION_OPTIONS` is the compatibility path that also moves the machine snapshot.
The registry is separately owned by `src/lib/state/registry.ts`; backup and recovery manifests are owned by their rebuild and recreate modules. Step helpers do not change the machine snapshot or emit machine events.

## Duplicated decision points

Expand Down
21 changes: 10 additions & 11 deletions src/lib/onboard/machine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ The target shape is a machine-driven onboarding runner:

In that final shape, `src/lib/onboard.ts` should be a thin entrypoint. State handlers should own state-specific prompts, resume validation, repair decisions, and side effects.

`flow-handoff.ts` validates required data and constructs context at the initial-to-core and core-to-final boundaries. The entrypoint supplies process-bound dependencies and reserved-name output.

The strict runner owns exact `init`, `preflight`, `provider_selection`, `inference`, and `sandbox` entry. If the durable state is later than a slice entry, earlier phases run as evented prerequisite repairs. A repair must return a legal, update-free transition chain and must not change the durable entry state.

## State ownership
Expand All @@ -41,15 +43,12 @@ A state handler may perform many smaller operations, but it should expose only s

## Session steps versus machine state

The persisted onboarding session still tracks step-level progress for resumability. Step recording is older than the FSM and is currently used as a compatibility bridge.

Long term:

- `OnboardRuntime` should own machine transitions and machine revision increments.
- Session step helpers should record only step status (`pending`, `in_progress`, `complete`, `failed`, `skipped`).
- State handlers should return explicit results instead of implicitly moving the machine by calling step helpers.
The persisted onboarding session tracks step-level progress for resumability.

Until that migration completes, step helpers may still infer machine snapshots for compatibility with older sessions and tests.
- `OnboardRuntime` owns normal machine transitions, revision increments, terminal state, and machine events.
- Session step helpers record step-progress bookkeeping and context updates accepted by `filterSafeUpdates`. They cannot change the machine snapshot.
- State handlers return explicit results. They do not move the machine through step helpers.
- Explicit session recovery and the process-exit failure backstop are narrow exceptions.

## Handler contract

Expand Down Expand Up @@ -87,15 +86,15 @@ without a state transition so a later process can resume the same non-terminal s

## Runtime responsibilities

`OnboardRuntime` is the intended authority for:
`OnboardRuntime` is the authority for:

- validating transitions against `transitions.ts`;
- validating result source, target, kind, and graph transitions;
- applying safe session context updates;
- marking terminal states;
- emitting redacted lifecycle, state, repair, resume-conflict, and hook events;
- normalizing older sessions before strict execution.

The runtime should reject invalid transitions before they can be persisted.
Step helpers record step-progress bookkeeping and context updates accepted by `filterSafeUpdates`. They cannot change the machine snapshot or emit machine events. Explicit session recovery and the process-exit failure backstop are separate recovery boundaries. They validate their snapshot changes and run before or outside handler execution.

## Event semantics

Expand Down
14 changes: 8 additions & 6 deletions src/lib/onboard/machine/core-flow-phases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function context(
hermesToolGateways: [],
preferredInferenceApi: null,
compatibleEndpointReasoning: null,

compatibleEndpointReasoningEffort: null,
nimContainer: null,
webSearchConfig: null,
Expand Down Expand Up @@ -137,6 +138,7 @@ function createPhases(
hermesToolGateways: ["local"],
preferredInferenceApi: "chat",
compatibleEndpointReasoning: null,

compatibleEndpointReasoningEffort: null,
nimContainer: "nim-test",
})),
Expand All @@ -162,8 +164,10 @@ function createPhases(
recordRepairEvent: vi.fn(async () => createSession()),
hydrateCredentialEnv: vi.fn(),
configureCompatibleEndpointReasoning: vi.fn(async () => "false" as const),
clearCompatibleEndpointReasoning: vi.fn(() => null),

configureCompatibleEndpointReasoningEffort: vi.fn(async () => null),
clearCompatibleEndpointReasoning: vi.fn(() => null),

clearCompatibleEndpointReasoningEffort: vi.fn(() => null),
repairLocalInferenceSystemdOverrideOrExit: vi.fn(),
isNonInteractive: () => true,
Expand Down Expand Up @@ -206,11 +210,8 @@ function createPhases(
resolvePath: (value) => value,
agentSupportsWebSearch: () => true,
note: vi.fn(),

cliName: () => "nemoclaw",
retireReplacedSandboxWorkload: vi.fn(() => ({
status: "skipped" as const,
reason: "replacement-unproven" as const,
})),
updateSession: vi.fn((mutator) => mutator(createSession()) ?? createSession()),
getStoredMessagingChannelConfig: () => null,
hydrateMessagingChannelConfig: (config) => config,
Expand Down Expand Up @@ -394,6 +395,7 @@ describe("core onboard flow phases", () => {
hermesToolGateways: [],
preferredInferenceApi: "chat",
compatibleEndpointReasoning: null,

compatibleEndpointReasoningEffort: null,
nimContainer: null,
}));
Expand Down Expand Up @@ -883,7 +885,7 @@ describe("core onboard flow phases", () => {
resume: true,
recordRepairEvent: repairRecorder(),
}),
).rejects.toThrow("Unexpected onboarding live flow state before slice entry");
).rejects.toThrow("Unexpected onboarding flow state before slice entry");
expect(providerInference.run).not.toHaveBeenCalled();
expect(sandbox.run).not.toHaveBeenCalled();
});
Expand Down
10 changes: 8 additions & 2 deletions src/lib/onboard/machine/core-flow-phases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
type ProviderInferenceStateOptions,
} from "./handlers/provider-inference";
import { handleSandboxState, type SandboxStateOptions } from "./handlers/sandbox";
import { UnexpectedLiveOnboardFlowSliceStateError } from "./live-flow-slice";
import { UnexpectedOnboardFlowSliceStateError } from "./flow-slice-error";
import {
type OnboardPrerequisiteRepairEventRecorder,
runOnboardPrerequisiteRepair,
Expand All @@ -32,6 +32,8 @@ import type { OnboardMachineRunnerResult, OnboardMachineRunnerRuntime } from "./
import { runOnboardSequenceWithRunner, type OnboardSequencePhase } from "./sequence-runner";
import type { OnboardMachineState } from "./types";

export { prepareCoreOnboardFlowContext, prepareFinalOnboardFlowContext } from "./flow-handoff";

export interface EndpointProvenanceOptions {
endpointSource?: InferenceEndpointSource | null;
endpointSourceProvider?: string | null;
Expand Down Expand Up @@ -67,6 +69,7 @@ export interface SandboxOnboardFlowPhaseOptions<
gatewayName: string;
authoritativeResumeConfig?: boolean;
authoritativePolicyTier?: string | null;

recreateJournalTargetIntentFingerprint?: string | null;
resumeAgentChanged: boolean;
requestedObservabilityEnabled?: boolean | null;
Expand Down Expand Up @@ -160,6 +163,7 @@ export function createProviderInferenceOnboardFlowPhase<
hermesToolGateways: context.hermesToolGateways,
preferredInferenceApi: context.preferredInferenceApi,
compatibleEndpointReasoning: context.compatibleEndpointReasoning,

compatibleEndpointReasoningEffort: context.compatibleEndpointReasoningEffort,
nimContainer: context.nimContainer,
webSearchConfig: context.webSearchConfig,
Expand All @@ -184,6 +188,7 @@ export function createProviderInferenceOnboardFlowPhase<
hermesToolGateways: providerInferenceResult.hermesToolGateways,
preferredInferenceApi: providerInferenceResult.preferredInferenceApi,
compatibleEndpointReasoning: providerInferenceResult.compatibleEndpointReasoning,

compatibleEndpointReasoningEffort:
providerInferenceResult.compatibleEndpointReasoningEffort,
nimContainer: providerInferenceResult.nimContainer,
Expand Down Expand Up @@ -215,6 +220,7 @@ export function createSandboxOnboardFlowPhase<
gatewayName: options.gatewayName,
authoritativeResumeConfig: options.authoritativeResumeConfig,
authoritativePolicyTier: options.authoritativePolicyTier,

recreateJournalTargetIntentFingerprint: options.recreateJournalTargetIntentFingerprint,
endpointSource: endpointProvenance.endpointSource,
resumeAgentChanged: options.resumeAgentChanged,
Expand Down Expand Up @@ -281,7 +287,7 @@ export async function runCoreOnboardFlowSlice<Context extends OnboardFlowContext
]
: ["provider_selection", "inference", "sandbox", "openclaw", "agent_setup"];
if (!allowedStates.includes(state)) {
throw new UnexpectedLiveOnboardFlowSliceStateError(
throw new UnexpectedOnboardFlowSliceStateError(
state,
["provider_selection", "inference", "sandbox"],
allowedStates.filter(
Expand Down
Loading
Loading