Skip to content

feat(onboard): add managed shared-state transactions - #7962

Closed
ericksoa wants to merge 6 commits into
feat/buildless-managed-image-applicationfrom
feat/buildless-shared-state-transactions
Closed

feat(onboard): add managed shared-state transactions#7962
ericksoa wants to merge 6 commits into
feat/buildless-managed-image-applicationfrom
feat/buildless-shared-state-transactions

Conversation

@ericksoa

@ericksoa ericksoa commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds root-owned, transactional managed shared-state application and the first Docker adapter. The slice applies and rolls back validated OpenClaw, Hermes, and LangChain Deep Agents Code state atomically while keeping production buildless activation and durable crash recovery in later review units.

Related Issue

Part of #7744

Changes

  • Define the managed shared-state transaction contract, ownership/mode checks, commit receipt, and idempotent rollback.
  • Add Docker staging and root-apply adapters that execute with env -i.
  • Forward only the six allowlisted OpenClaw scheduler controls through the clean root path.
  • Validate application controls before completion-file inspection or filesystem/transaction mutation.
  • Refresh a verified same-profile runtime and completion digest without starting a duplicate shared-state transaction.
  • Cover OpenClaw, Hermes, and DCode transaction, replay, ownership, mode, cleanup, and failure behavior.
  • Intentionally expose no production cutover caller in this slice. PR3.10 owns the transactional bootstrap/cutover integration after the driver-neutral lifecycle exists; PR3.12 owns restart-spanning persistence; PR3.15 owns production activation. Wiring these primitives directly into current onboarding here would create the partial runtime activation prohibited by [Epic] Support native Podman with buildless managed onboarding #7744.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification:
  • Tests not applicable — justification:
  • Docs updated for user-facing behavior changes
  • Docs not applicable — justification: This transaction layer remains dormant and does not change a supported CLI, configuration, runtime selection, workflow, or support statement.
  • Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging)
  • Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: Exact-diff and independent P1/P2 reviews covered clean-exec forwarding, pre-mutation validation, same-profile replay, ownership/mode enforcement, atomic commit, and rollback. The absence of a production caller is required by this review boundary: PR3.10 integrates cutover only after PR3.6–3.9 establish lifecycle parity, and PR3.15 activates the complete all-agent path. No P1/P2 remains inside this slice.
  • Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue:

Documentation Writer Review

  • Documentation writer subagent reviewed the completed changes
  • Result: no-docs-needed
  • Evidence: The exact ten-file diff (+3,251/-37) adds dormant transaction/adaptor internals and tests only. Durable recovery and user-visible activation remain explicitly deferred.
  • Agent: Codex Desktop

DGX Station Hardware Evidence

  • Tested on DGX Station
  • Tested commit:
  • Station profile/scenario:
  • Result:
  • Supporting evidence:

Verification

  • Exact validated head/base: b1726a676da10e26e08e045432698f0f1dfa1407 / 3f3bdbbde388cfb3e984e5a627bebc29cab8c606
  • Review budget: 10 files, +3,251/-37; no documentation paths.
  • PR description includes a Signed-off-by: line and every commit appears as Verified in GitHub
  • Normal pre-commit, commit-msg, and pre-push hooks passed, or npm run validate:pr passed after refreshing origin/main when hooks were skipped or unavailable
  • Targeted behavior tests pass for the current change set, or tests are marked not applicable above — command/result or justification: 87 slice-focused tests and 290 cross-slice regression tests passed; CLI and plugin builds passed; npm run validate:pr passed; git diff --check is clean.
  • Applicable broad gate passed — npm test for broad runtime/test-harness changes; npm run check for repo-wide validation/coverage changes — command/result: Exact-head required CI is the broad gate for this dormant transaction slice.
  • Quality Gates section completed with required justifications or waivers
  • No secrets, API keys, or credentials committed
  • npm run docs builds without warnings (doc changes only)
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Stack

  • Base: PR3.4b branch feat/buildless-managed-image-application at 3f3bdbbde388cfb3e984e5a627bebc29cab8c606
  • This slice: PR3.5 branch feat/buildless-shared-state-transactions at b1726a676da10e26e08e045432698f0f1dfa1407
  • Next: PR3.6 introduces the driver-neutral lifecycle and sandbox-action parity. It is not part of this review diff.
  • Buildless support remains disabled until every supported agent and required qualification gate passes.

Signed-off-by: Aaron Erickson aerickson@nvidia.com

Summary by CodeRabbit

  • New Features

    • Added secure managed-startup application for Docker-based environments.
    • Added completion verification and recovery support for startup operations.
    • Added transactional shared-state handling with rollback and commit workflows.
    • Added validation for profiles, runtime settings, corporate certificates, file integrity, permissions, and process identity.
    • Added protection against tampered, oversized, malformed, or non-canonical startup data.
  • Tests

    • Expanded coverage for successful operations, retries, recovery, rollback, idempotency, and security validation.

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds canonical managed-startup root-apply requests, completion-marker verification, authenticated shared-state transactions, Docker-backed root execution, and Docker commit or rollback handling. It also adds comprehensive tests for validation, replay, filesystem safety, recovery, and agent isolation.

Changes

Managed-startup transactional application

Layer / File(s) Summary
Root-apply contracts and runtime completion
src/lib/onboard/managed-startup/root-apply.ts, src/lib/onboard/managed-startup/image-runtime.ts, src/lib/onboard/managed-startup/*image-runtime.test.ts, src/lib/onboard/managed-startup/*root-apply.test.ts
Adds canonical request serialization and parsing, bounded transport, runtime handoff files, completion markers, completion verification, root request application, and related tests.
Shared-state snapshot, rollback, and commit
src/lib/onboard/managed-startup/shared-state-transaction.ts, src/lib/onboard/managed-startup/managed-startup-shared-state-transaction.test.ts
Adds authenticated filesystem transactions that snapshot metadata and contents, restore managed state, enforce profile and agent isolation, and remove committed receipts.
Docker root application and verification
src/lib/onboard/managed-startup/docker-root-apply.ts, src/lib/onboard/managed-startup/docker-root-apply.test.ts
Adds exact container and immutable image validation, fixed root execution, environment allowlisting, acknowledgement retry, transaction probing, and failure metadata.
Docker shared-state finalization and recovery
src/lib/onboard/managed-startup/docker-shared-state.ts, src/lib/onboard/managed-startup/docker-shared-state.test.ts
Adds receipt preservation, commit handling, isolated rollback execution, failed-container cleanup, idempotent finalization, and recovery tests.

Estimated code review effort: 5 (Critical) | ~100 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant DockerRootApply
  participant ImageRuntime
  participant SharedTransaction
  participant DockerSharedState
  Caller->>DockerRootApply: submit root apply request
  DockerRootApply->>ImageRuntime: execute root application
  ImageRuntime->>SharedTransaction: begin or resume transaction
  DockerRootApply->>DockerSharedState: finalize transaction
  DockerSharedState->>SharedTransaction: commit or rollback shared state
Loading

Possibly related PRs

  • NVIDIA/NemoClaw#7946: Provides managed-startup profile schemas, fingerprints, runtime-input inventories, and transport contracts used by this PR.
  • NVIDIA/NemoClaw#7959: Shares managed-startup profile and runtime code consumed by the root-apply and completion flows.

Suggested labels: area: sandbox

Suggested reviewers: apurvvkumaria, cv

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding managed shared-state transaction support for onboarding.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/buildless-shared-state-transactions

Comment @coderabbitai help to get the list of available commands.

@ericksoa ericksoa added area: onboarding Onboarding FSM, provider setup, sandbox launch, or first-run flow area: security Security controls, permissions, secrets, or hardening feature PR adds or expands user-visible functionality platform: container Affects Docker, containerd, Podman, or images labels Jul 31, 2026
@github-code-quality

github-code-quality Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/plugin

The overall coverage in commit b1726a6 in the feat/buildless-share... branch remains at 96%, unchanged from commit 91fc63e in the feat/buildless-manag... branch.


Updated July 31, 2026 04:22 UTC

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — Blocking findings reported

Advisor assessment: Blockers require maintainer review
Next action: Review the blockers below.
Findings: 1 blocker · 0 warnings · 0 suggestions

Model lanes

  • GPT-5.6 Terra (primary): Completed · high confidence · 1 blocker · 0 warnings · 0 suggestions
  • Nemotron 3 Ultra (second opinion): Completed · high confidence · 0 blockers · 1 warning · 1 suggestion
  • Model comparison: normalized findings differ; normalized E2E selections differ; Nemotron reported 1 fewer blocker, 1 more warning, 1 more suggestion.

Nemotron output stays in workflow artifacts and does not change the assessment above.

E2E guidance

Advisory only. E2E / PR Gate selects and runs jobs independently.

Recommended E2E: onboard-repair, onboard-resume, cloud-onboard

Blockers

PRA-1 Blocker — Connect the managed root transaction to container cutover

  • Location: src/lib/onboard/managed-startup/docker-root-apply.ts:113
  • Category: correctness
  • Problem: The new root applicator and shared-state finalizer are exported but have no production caller. Managed startup therefore continues without creating, retaining, committing, or rolling back the transaction.
  • Impact: A managed startup failure can leave changed shared sandbox configuration because the new rollback mechanism is never invoked by the onboarding flow.
  • Fix: Call the root applicator from the managed container startup path, retain its transaction result, and call the finalizer at the existing supervisor cutover decision.
  • Verification: Search production onboarding sources for both exported function names after wiring; each must have a caller on the managed startup path.
  • Test coverage: Add an onboarding integration test that forces supervisor failure after root application and verifies the managed sandbox files are restored through the production cutover path.
  • Evidence: src/lib/onboard/managed-startup/docker-root-apply.ts:113 exports applyDockerManagedStartupRootRequest. src/lib/onboard/managed-startup/docker-shared-state.ts:205 exports finalizeDockerManagedStartupSharedState. Repository search found no production call sites for either export.

Workflow run details

This automated review informs maintainers. Warnings and suggestions do not require a response. A maintainer decides whether to merge.

ericksoa added 4 commits July 30, 2026 18:52
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
Preserve the exact reviewed tree while moving the stacked base to restacked PR3.4b.

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
Carry the reviewed PR3.5 slice unchanged onto the CodeRabbit feedback restack through PR3.4b.

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
@ericksoa

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 6

🧹 Nitpick comments (7)
src/lib/onboard/managed-startup/docker-root-apply.test.ts (1)

244-281: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add the malformed-inspect cases to this table.

inspectExactContainer has two uncovered rejections that parse external Docker output: malformed JSON at docker-root-apply.ts Line 88 and an inspect array that does not hold exactly one element at Line 91. The existing table already provides the shape for both.

🧪 Proposed table rows
     {
       label: "short caller identity",
       containerId: "b".repeat(12),
       inspect: stableInspect(),
       error: /full lowercase Docker container ID/u,
     },
+    {
+      label: "malformed inspect output",
+      containerId: CONTAINER_ID,
+      inspect: "{not json",
+      error: /malformed inspect output/u,
+    },
+    {
+      label: "ambiguous inspect result",
+      containerId: CONTAINER_ID,
+      inspect: `[${stableInspect().slice(1, -1)},${stableInspect().slice(1, -1)}]`,
+      error: /exactly one managed-startup container/u,
+    },
🤖 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/managed-startup/docker-root-apply.test.ts` around lines 244 -
281, Add table cases in the “rejects $label before root exec” test for malformed
JSON from inspectExactContainer and for an inspect result array whose length is
not exactly one. Configure each case with the corresponding dockerCapture
output, assert the expected rejection message, and preserve the existing
assertion that dockerSpawnSync is not called.
src/lib/onboard/managed-startup/docker-shared-state.test.ts (1)

179-211: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert that idempotent cleanup stays silent.

The test title claims idempotent cleanup. completeCommit removes the receipt parent at Line 189 before production cleanup runs, so cleanupReceiptBestEffort at docker-shared-state.ts Line 57 runs against a missing directory. If cleanupTempDir throws there, the helper logs a warning and the test still passes. Assert that console.warn is not called, so the test proves the silent path.

🧪 Proposed assertion
+    const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
+
     for (let attempt = 0; attempt < 2; attempt += 1) {
       expect(
         finalizeDockerManagedStartupSharedState(
           { transaction: transaction(), patchResult: result(), supervisorReady: true },
           { dockerRun },
         ),
       ).toEqual({ supervisorReady: true, failure: null });
     }
+    expect(warn).not.toHaveBeenCalled();
     expect(new Set(receiptPaths).size).toBe(2);
🤖 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/managed-startup/docker-shared-state.test.ts` around lines 179
- 211, Update the test “uses unique receipt paths and treats already-completed
cleanup idempotently” to spy on console.warn before invoking
finalizeDockerManagedStartupSharedState, then assert it was not called after
both attempts complete. Restore the spy afterward so the test verifies
cleanupReceiptBestEffort remains silent when the receipt directory was already
removed.
src/lib/onboard/managed-startup/docker-root-apply.ts (1)

184-210: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the failure-metadata attachment.

The same cast-and-assign block appears twice, at Line 190 and Line 204. getDockerManagedStartupFailureTransaction reads the property with a third cast at Line 53. A small helper removes the duplication and keeps the property name in one place.

♻️ Proposed helper
function failWithTransaction(
  message: string,
  transaction: DockerManagedStartupTransaction,
): never {
  const error = new Error(message) as Error & {
    managedStartupTransaction?: DockerManagedStartupTransaction;
  };
  error.managedStartupTransaction = transaction;
  throw error;
}
       const receiptProbeDetail = commandDetail(receiptProbe);
-      const error = new Error(
-        `Managed startup root application completed, but transaction state could not be verified in exact container ${pinned.containerId.slice(0, 12)}${
-          receiptProbeDetail ? `: ${receiptProbeDetail}` : ""
-        }`,
-      );
-      (
-        error as Error & {
-          managedStartupTransaction?: DockerManagedStartupTransaction;
-        }
-      ).managedStartupTransaction = transaction;
-      throw error;
+      failWithTransaction(
+        `Managed startup root application completed, but transaction state could not be verified in exact container ${pinned.containerId.slice(0, 12)}${
+          receiptProbeDetail ? `: ${receiptProbeDetail}` : ""
+        }`,
+        transaction,
+      );
     }
     lastFailure = commandDetail(result);
   }
-  const error = new Error(
+  failWithTransaction(
     `Managed startup root application failed in exact container ${pinned.containerId.slice(0, 12)}${
       lastFailure ? `: ${lastFailure}` : ""
     }`,
+    transaction,
   );
-  (
-    error as Error & {
-      managedStartupTransaction?: DockerManagedStartupTransaction;
-    }
-  ).managedStartupTransaction = transaction;
-  throw error;
 }
🤖 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/managed-startup/docker-root-apply.ts` around lines 184 - 210,
Extract the repeated managedStartupTransaction cast-and-assign logic into a
helper near the startup failure handling, such as failWithTransaction(message,
transaction), and have it create, attach, and throw the error. Replace both
throw blocks in the managed startup flow with this helper, preserving their
existing messages and transaction metadata.
src/lib/onboard/managed-startup/shared-state-transaction.ts (1)

485-623: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

The manifest codec round-trip is consistent. Consider extracting it.

The canonical check at Line 619 depends on exact key order. I traced each writer against each reader: the manifest literal at Line 779, the file receipt at snapshotFile Line 408, the absent file receipt at Line 392, and the directory receipts at Line 429 and Line 439 all match the reconstruction order at Line 558, Line 540, Line 580, Line 592, and Line 612. The round-trip holds today.

That coupling is invisible and spans 400 lines. The manifest codec is pure logic and needs no filesystem. Move canonicalManifest, parseManifest, requireExactKeys, safeMetadata, safeRelativePath, and the receipt types into a separate module. The codec then becomes unit-testable without a real filesystem, and a future field reorder breaks a focused test instead of a rollback in production.

As per path instructions from src/lib/README.md: "Keep host-boundary operations such as Docker and filesystem access isolated in adapter-style modules with injectable dependencies, while keeping validation and policy logic separate and testable."

🤖 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/managed-startup/shared-state-transaction.ts` around lines 485
- 623, Extract the pure manifest codec from the startup transaction module into
a separate module: move canonicalManifest, parseManifest, requireExactKeys,
safeMetadata, safeRelativePath, and the related receipt types together. Update
the transaction implementation to import and use the extracted symbols without
changing canonical serialization or validation behavior, including exact key
ordering and round-trip checks.

Source: Path instructions

src/lib/onboard/managed-startup-root-apply.test.ts (1)

79-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the missing corporate-CA transport-mismatch case.

createManagedStartupRootApplyRequest rejects a mismatch between the profile CA state and the transport at root-apply.ts Line 90. No test covers that branch. Two directions can fail: a profile with corporateCa.bundleSha256 set and no corporateCaB64, and a profile without a CA plus a supplied corporateCaB64. Both are security-relevant, because they decide whether the root applier installs a trust bundle.

🧪 Proposed additional test
  it("rejects a corporate CA transport that disagrees with the profile", () => {
    const withCa = managedStartupE2eProfile("openclaw", false, true);
    const withoutCa = managedStartupE2eProfile("openclaw");

    expect(() =>
      createManagedStartupRootApplyRequest({
        agent: withCa.agent,
        encodedProfile: encodeManagedStartupProfile(withCa),
      }),
    ).toThrow(/transport does not match the profile/u);
    expect(() =>
      createManagedStartupRootApplyRequest({
        agent: withoutCa.agent,
        encodedProfile: encodeManagedStartupProfile(withoutCa),
        corporateCaB64: Buffer.from(MANAGED_STARTUP_E2E_CORPORATE_CA_PEM).toString("base64"),
      }),
    ).toThrow(/transport does not match the profile/u);
  });
🤖 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/managed-startup-root-apply.test.ts` around lines 79 - 89, Add
a test alongside the existing createManagedStartupRootApplyRequest cases
covering both corporate-CA transport mismatches: a profile with
corporateCa.bundleSha256 but no corporateCaB64, and a profile without a CA but
with corporateCaB64 supplied. Assert each call throws the expected “transport
does not match the profile” error, reusing the existing CA fixture and profile
helpers.
src/lib/onboard/managed-startup/root-apply.ts (1)

137-182: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Parsing validates the same request three times.

parseManagedStartupRootApplyRequest calls createManagedStartupRootApplyRequest at Line 167. It then calls serializeManagedStartupRootApplyRequest at Line 178, which calls createManagedStartupRootApplyRequest again. Each call decodes the profile, computes the profile fingerprint, and hashes the corporate CA. The input is bounded to 320 KiB and this path runs once per root application, so the cost is acceptable. If you want a cheaper canonical check, add an internal serializer that accepts an already-normalized request and skips revalidation.

🤖 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/managed-startup/root-apply.ts` around lines 137 - 182, Avoid
repeated request validation in parseManagedStartupRootApplyRequest by adding an
internal serializer that accepts the already-normalized request without calling
createManagedStartupRootApplyRequest. Use this serializer for the canonical
comparison, while retaining the existing initial construction and validation
flow.
src/lib/onboard/managed-startup-shared-state-transaction.test.ts (1)

36-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add direct coverage for readOnlyReceipt.

Add tests for writable-mount rejection, begin and commit rejection, and rollback that retains the transaction directory. Mock fs.openSync to return EROFS for the write probe. The Docker test only checks the --read-only-receipt argument.

🤖 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/managed-startup-shared-state-transaction.test.ts` around
lines 36 - 61, Extend the tests for the managed startup shared-state transaction
around the existing setup in the test suite to cover readOnlyReceipt behavior:
reject writable mounts, reject begin and commit operations, and ensure rollback
preserves the transaction directory. Mock fs.openSync to raise EROFS for the
write probe, and in the Docker-related assertion verify only the
--read-only-receipt argument.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/lib/onboard/managed-startup-image-runtime.test.ts`:
- Around line 614-634: Replace the ineffective CA-leak assertion in the test
around writeCompletionFixture with a public-boundary test: apply a managed
startup root request via applyManagedStartupRootRequest using a profile with
corporateCaB64 set, then read the produced runtime environment file and assert
it omits the CA payload. Ensure the test exercises the production write path in
image-runtime rather than only inspecting fixture-generated content; otherwise
narrow the test to the completion-marker behavior it actually verifies.

In `@src/lib/onboard/managed-startup/docker-shared-state.test.ts`:
- Around line 41-45: Update removeReceiptParents to inspect
path.dirname(receiptPath) and only call fs.rmSync with recursive deletion when
that directory matches the dedicated nemoclaw-managed-startup-receipt
temp-directory naming pattern; retain the existing filtering of empty receipt
paths and skip any parent that does not pass the guard.

In `@src/lib/onboard/managed-startup/docker-shared-state.ts`:
- Around line 257-263: Update the failure-handling branch around the `failure`
construction so errors from `quiesceManagedStartupContainer` or
`rollbackManagedStartupSharedState` are caught and composed with the original
commit failure detail. Reuse the message-composition approach from the
receipt-preservation path around line 226, preserving both the commit error and
any cleanup error before returning `{ supervisorReady: false, failure }`.

In `@src/lib/onboard/managed-startup/image-runtime.ts`:
- Around line 1533-1541: Update the usage message in readCliAgent to include
--rollback-shared-state-transaction alongside the other accepted commands,
matching the option handled by main. Leave the argument validation and other
usage entries unchanged.
- Around line 1219-1238: Update verifyManagedStartupImageCompletion and
waitForManagedStartupImageCompletion so a digest mismatch is explicitly
identifiable as a retryable transient failure, allowing the waiter to continue
polling until the deadline. Preserve ENOENT retry behavior, but keep all other
verification errors fatal.

In `@src/lib/onboard/managed-startup/shared-state-transaction.ts`:
- Around line 201-237: Update readStableFile to include fs.constants.O_NONBLOCK
in the flags passed to fs.openSync alongside O_RDONLY and O_NOFOLLOW, matching
readStableRegularFileSnapshot. Preserve the existing post-open regular-file
validation and stable metadata checks so FIFO replacements are rejected without
blocking.

---

Nitpick comments:
In `@src/lib/onboard/managed-startup-root-apply.test.ts`:
- Around line 79-89: Add a test alongside the existing
createManagedStartupRootApplyRequest cases covering both corporate-CA transport
mismatches: a profile with corporateCa.bundleSha256 but no corporateCaB64, and a
profile without a CA but with corporateCaB64 supplied. Assert each call throws
the expected “transport does not match the profile” error, reusing the existing
CA fixture and profile helpers.

In `@src/lib/onboard/managed-startup-shared-state-transaction.test.ts`:
- Around line 36-61: Extend the tests for the managed startup shared-state
transaction around the existing setup in the test suite to cover readOnlyReceipt
behavior: reject writable mounts, reject begin and commit operations, and ensure
rollback preserves the transaction directory. Mock fs.openSync to raise EROFS
for the write probe, and in the Docker-related assertion verify only the
--read-only-receipt argument.

In `@src/lib/onboard/managed-startup/docker-root-apply.test.ts`:
- Around line 244-281: Add table cases in the “rejects $label before root exec”
test for malformed JSON from inspectExactContainer and for an inspect result
array whose length is not exactly one. Configure each case with the
corresponding dockerCapture output, assert the expected rejection message, and
preserve the existing assertion that dockerSpawnSync is not called.

In `@src/lib/onboard/managed-startup/docker-root-apply.ts`:
- Around line 184-210: Extract the repeated managedStartupTransaction
cast-and-assign logic into a helper near the startup failure handling, such as
failWithTransaction(message, transaction), and have it create, attach, and throw
the error. Replace both throw blocks in the managed startup flow with this
helper, preserving their existing messages and transaction metadata.

In `@src/lib/onboard/managed-startup/docker-shared-state.test.ts`:
- Around line 179-211: Update the test “uses unique receipt paths and treats
already-completed cleanup idempotently” to spy on console.warn before invoking
finalizeDockerManagedStartupSharedState, then assert it was not called after
both attempts complete. Restore the spy afterward so the test verifies
cleanupReceiptBestEffort remains silent when the receipt directory was already
removed.

In `@src/lib/onboard/managed-startup/root-apply.ts`:
- Around line 137-182: Avoid repeated request validation in
parseManagedStartupRootApplyRequest by adding an internal serializer that
accepts the already-normalized request without calling
createManagedStartupRootApplyRequest. Use this serializer for the canonical
comparison, while retaining the existing initial construction and validation
flow.

In `@src/lib/onboard/managed-startup/shared-state-transaction.ts`:
- Around line 485-623: Extract the pure manifest codec from the startup
transaction module into a separate module: move canonicalManifest,
parseManifest, requireExactKeys, safeMetadata, safeRelativePath, and the related
receipt types together. Update the transaction implementation to import and use
the extracted symbols without changing canonical serialization or validation
behavior, including exact key ordering and round-trip checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 699fdce2-336b-45ed-9ae3-dbe5cf94594d

📥 Commits

Reviewing files that changed from the base of the PR and between 46fbe19 and b2027a9.

📒 Files selected for processing (10)
  • src/lib/onboard/managed-startup-image-runtime.test.ts
  • src/lib/onboard/managed-startup-root-apply.test.ts
  • src/lib/onboard/managed-startup-shared-state-transaction.test.ts
  • src/lib/onboard/managed-startup/docker-root-apply.test.ts
  • src/lib/onboard/managed-startup/docker-root-apply.ts
  • src/lib/onboard/managed-startup/docker-shared-state.test.ts
  • src/lib/onboard/managed-startup/docker-shared-state.ts
  • src/lib/onboard/managed-startup/image-runtime.ts
  • src/lib/onboard/managed-startup/root-apply.ts
  • src/lib/onboard/managed-startup/shared-state-transaction.ts

Comment on lines +614 to +634
it("accepts merged CA paths without putting the CA payload in the readable handoff", () => {
const fixture = writeCompletionFixture(
managedStartupE2eProfile("langchain-deepagents-code", false, true),
true,
);
mockDescriptorOwnership(0n, 0n);
expect(
verifyManagedStartupImageCompletion(
"langchain-deepagents-code",
fixture.fingerprint,
fixture.completionFile,
fixture.runtimeEnvironmentFile,
),
).toEqual({
agent: "langchain-deepagents-code",
fingerprint: fixture.fingerprint,
});
expect(fs.readFileSync(fixture.runtimeEnvironmentFile, "utf8")).not.toContain(
"NEMOCLAW_CORPORATE_CA_B64",
);
});

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The CA-leak assertion cannot fail.

writeCompletionFixture builds the runtime environment string itself at Line 507. It passes only mapped.runtimeEnvironment and mapped.configurationEnvironment. The corporate CA base64 never enters that input, because the profile carries only corporateCa.bundleSha256. The assertion at Line 631 therefore checks the fixture, not the production write path at image-runtime.ts Line 1381.

Assert the property through the public boundary instead. Apply a profile that carries a corporate CA through applyManagedStartupRootRequest with corporateCaB64 set, then read the written runtime environment file. Alternatively, narrow the test title to the completion-marker claim it does verify.

As per path instructions: "Prefer observable outcomes through the public boundary over source-text, private-shape, or mock-call assertions" and "Flag ... conditionals that make a test pass without exercising its claim."

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 630-630: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(fixture.runtimeEnvironmentFile, "utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 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/managed-startup-image-runtime.test.ts` around lines 614 -
634, Replace the ineffective CA-leak assertion in the test around
writeCompletionFixture with a public-boundary test: apply a managed startup root
request via applyManagedStartupRootRequest using a profile with corporateCaB64
set, then read the produced runtime environment file and assert it omits the CA
payload. Ensure the test exercises the production write path in image-runtime
rather than only inspecting fixture-generated content; otherwise narrow the test
to the completion-marker behavior it actually verifies.

Source: Path instructions

Comment on lines +41 to +45
function removeReceiptParents(...receiptPaths: readonly string[]): void {
for (const receiptPath of receiptPaths.filter((candidate) => candidate.length > 0)) {
fs.rmSync(path.dirname(receiptPath), { force: true, recursive: true });
}
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the recursive delete in removeReceiptParents.

removeReceiptParents calls fs.rmSync with recursive: true on path.dirname(receiptPath). The only guard is a non-empty string check. The production helper cleanupTempDir(receiptPath, RECEIPT_TEMP_PREFIX) takes a prefix argument, which indicates it validates the directory before removing it. This test helper drops that validation.

The assertions at Line 100 and Line 176 imply the parent is a dedicated temp directory, so the current layout is safe. A change to the temp-path layout would turn this helper into a recursive delete of a shared directory. Check the directory name against nemoclaw-managed-startup-receipt before removing it.

🛡️ Proposed guard
 function removeReceiptParents(...receiptPaths: readonly string[]): void {
   for (const receiptPath of receiptPaths.filter((candidate) => candidate.length > 0)) {
-    fs.rmSync(path.dirname(receiptPath), { force: true, recursive: true });
+    const parent = path.dirname(receiptPath);
+    if (!path.basename(parent).startsWith("nemoclaw-managed-startup-receipt")) {
+      throw new Error(`refusing to remove an unexpected receipt parent: ${parent}`);
+    }
+    fs.rmSync(parent, { force: true, recursive: true });
   }
 }
📝 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
function removeReceiptParents(...receiptPaths: readonly string[]): void {
for (const receiptPath of receiptPaths.filter((candidate) => candidate.length > 0)) {
fs.rmSync(path.dirname(receiptPath), { force: true, recursive: true });
}
}
function removeReceiptParents(...receiptPaths: readonly string[]): void {
for (const receiptPath of receiptPaths.filter((candidate) => candidate.length > 0)) {
const parent = path.dirname(receiptPath);
if (!path.basename(parent).startsWith("nemoclaw-managed-startup-receipt")) {
throw new Error(`refusing to remove an unexpected receipt parent: ${parent}`);
}
fs.rmSync(parent, { force: true, recursive: true });
}
}
🤖 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/managed-startup/docker-shared-state.test.ts` around lines 41
- 45, Update removeReceiptParents to inspect path.dirname(receiptPath) and only
call fs.rmSync with recursive deletion when that directory matches the dedicated
nemoclaw-managed-startup-receipt temp-directory naming pattern; retain the
existing filtering of empty receipt paths and skip any parent that does not pass
the guard.

Comment on lines +257 to +263
const failure = new Error(
`OpenShell supervisor reconnected, but managed shared-state commit failed: ${commandDetail(commit)}`,
);
quiesceManagedStartupContainer(transaction, deps);
rollbackManagedStartupSharedState(transaction, receiptPath, deps);
if (!input.patchResult) removeFailedUnbackedContainer(transaction, deps);
return { supervisorReady: false, failure };

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

A quiesce failure discards the commit failure detail.

failure holds the commit error, including commandDetail(commit). quiesceManagedStartupContainer at Line 260 and rollbackManagedStartupSharedState at Line 261 throw their own errors. When either throws, the commit detail is lost, and the operator sees only "Could not quiesce ..." with no reason for the rollback.

The receipt-preservation path at Line 226 already composes both messages. Apply the same handling here.

🛠️ Proposed fix to preserve both failures
     const failure = new Error(
       `OpenShell supervisor reconnected, but managed shared-state commit failed: ${commandDetail(commit)}`,
     );
-    quiesceManagedStartupContainer(transaction, deps);
-    rollbackManagedStartupSharedState(transaction, receiptPath, deps);
+    try {
+      quiesceManagedStartupContainer(transaction, deps);
+      rollbackManagedStartupSharedState(transaction, receiptPath, deps);
+    } catch (recoveryError) {
+      throw new Error(
+        `${failure.message}; recovery also failed: ${
+          recoveryError instanceof Error ? recoveryError.message : String(recoveryError)
+        }`,
+      );
+    }
     if (!input.patchResult) removeFailedUnbackedContainer(transaction, deps);
     return { supervisorReady: false, failure };
📝 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 failure = new Error(
`OpenShell supervisor reconnected, but managed shared-state commit failed: ${commandDetail(commit)}`,
);
quiesceManagedStartupContainer(transaction, deps);
rollbackManagedStartupSharedState(transaction, receiptPath, deps);
if (!input.patchResult) removeFailedUnbackedContainer(transaction, deps);
return { supervisorReady: false, failure };
const failure = new Error(
`OpenShell supervisor reconnected, but managed shared-state commit failed: ${commandDetail(commit)}`,
);
try {
quiesceManagedStartupContainer(transaction, deps);
rollbackManagedStartupSharedState(transaction, receiptPath, deps);
} catch (recoveryError) {
throw new Error(
`${failure.message}; recovery also failed: ${
recoveryError instanceof Error ? recoveryError.message : String(recoveryError)
}`,
);
}
if (!input.patchResult) removeFailedUnbackedContainer(transaction, deps);
return { supervisorReady: false, failure };
🤖 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/managed-startup/docker-shared-state.ts` around lines 257 -
263, Update the failure-handling branch around the `failure` construction so
errors from `quiesceManagedStartupContainer` or
`rollbackManagedStartupSharedState` are caught and composed with the original
commit failure detail. Reuse the message-composition approach from the
receipt-preservation path around line 226, preserving both the commit error and
any cleanup error before returning `{ supervisorReady: false, failure }`.

Comment on lines +1219 to +1238
export function waitForManagedStartupImageCompletion(
expectedAgentInput: string,
expectedFingerprint: string,
timeoutSeconds = 600,
): { readonly agent: ManagedStartupAgent; readonly fingerprint: string } {
if (!Number.isSafeInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600) {
fail("startup completion wait timeout must be an integer from 1 to 3600 seconds");
}
const deadline = Date.now() + timeoutSeconds * 1000;
while (true) {
try {
return verifyManagedStartupImageCompletion(expectedAgentInput, expectedFingerprint);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
if (Date.now() >= deadline) {
fail(`startup completion was not published within ${String(timeoutSeconds)} seconds`);
}
return `export ${name}=${shellSingleQuote(value)}`;
});
return `${[...unsetLines, ...exportLines].join("\n")}\n`;
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
}
}

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

A concurrent replay makes the completion wait fail permanently.

waitForManagedStartupImageCompletion retries only when the error carries code === "ENOENT". verifyManagedStartupImageCompletion reports a digest mismatch through fail(...), which throws a plain Error without a code. That error is not retryable.

applyManagedStartupImageProfile writes the two files in two separate atomic renames at Line 1381 and Line 1382. Between the two renames, the runtime environment file holds new bytes and the completion marker still holds the previous digest. A replay refreshes the non-fingerprinted application-runtime values, so the digest does change between applications, as the comment at Line 1435 describes. A waiter that polls inside that window aborts instead of waiting for the new marker.

Treat the digest mismatch as a retryable transient state until the deadline expires, and keep other failures fatal.

🛠️ Proposed fix to retry a transient digest mismatch

Mark the transient failure at the digest check:

   if (runtimeEnvironmentSha256 !== marker.runtimeEnvironmentSha256) {
-    fail("managed startup completion marker runtime environment digest mismatch");
+    throw Object.assign(
+      new Error(
+        "Managed startup image runtime failed: managed startup completion marker runtime environment digest mismatch",
+      ),
+      { managedStartupCompletionTransient: true },
+    );
   }

Then retry it in the wait loop:

     } catch (error) {
-      if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
+      const transient =
+        (error as NodeJS.ErrnoException).code === "ENOENT" ||
+        (error as { managedStartupCompletionTransient?: boolean }).managedStartupCompletionTransient === true;
+      if (!transient) throw error;
       if (Date.now() >= deadline) {
         fail(`startup completion was not published within ${String(timeoutSeconds)} seconds`);
       }
🤖 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/managed-startup/image-runtime.ts` around lines 1219 - 1238,
Update verifyManagedStartupImageCompletion and
waitForManagedStartupImageCompletion so a digest mismatch is explicitly
identifiable as a retryable transient failure, allowing the waiter to continue
polling until the deadline. Preserve ENOENT retry behavior, but keep all other
verification errors fatal.

Comment on lines +1533 to +1541
function readCliAgent(argv: readonly string[], expectedLength = 2): string {
const index = argv.indexOf("--agent");
if (index < 0 || index + 1 >= argv.length || argv.length !== 2) {
fail("usage: managed-startup-image-runtime --agent <agent>");
if (index < 0 || index + 1 >= argv.length || argv.length !== expectedLength) {
fail(
"usage: managed-startup-image-runtime [--apply-root-stdin|--wait-for-completion|--verify-completion|--begin-shared-state-transaction|--commit-shared-state-transaction] --agent <agent>",
);
}
return argv[index + 1] as string;
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The usage message omits --rollback-shared-state-transaction.

main accepts --rollback-shared-state-transaction at Line 1598, but the usage text does not list it. An operator who runs the recovery command with wrong arguments receives a message that hides the command. Add the command to the list.

📝 Proposed fix for the usage text
     fail(
-      "usage: managed-startup-image-runtime [--apply-root-stdin|--wait-for-completion|--verify-completion|--begin-shared-state-transaction|--commit-shared-state-transaction] --agent <agent>",
+      "usage: managed-startup-image-runtime [--apply-root-stdin|--wait-for-completion|--verify-completion|--begin-shared-state-transaction|--commit-shared-state-transaction|--rollback-shared-state-transaction] --agent <agent>",
     );
📝 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
function readCliAgent(argv: readonly string[], expectedLength = 2): string {
const index = argv.indexOf("--agent");
if (index < 0 || index + 1 >= argv.length || argv.length !== 2) {
fail("usage: managed-startup-image-runtime --agent <agent>");
if (index < 0 || index + 1 >= argv.length || argv.length !== expectedLength) {
fail(
"usage: managed-startup-image-runtime [--apply-root-stdin|--wait-for-completion|--verify-completion|--begin-shared-state-transaction|--commit-shared-state-transaction] --agent <agent>",
);
}
return argv[index + 1] as string;
}
function readCliAgent(argv: readonly string[], expectedLength = 2): string {
const index = argv.indexOf("--agent");
if (index < 0 || index + 1 >= argv.length || argv.length !== expectedLength) {
fail(
"usage: managed-startup-image-runtime [--apply-root-stdin|--wait-for-completion|--verify-completion|--begin-shared-state-transaction|--commit-shared-state-transaction|--rollback-shared-state-transaction] --agent <agent>",
);
}
return argv[index + 1] as string;
}
🤖 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/managed-startup/image-runtime.ts` around lines 1533 - 1541,
Update the usage message in readCliAgent to include
--rollback-shared-state-transaction alongside the other accepted commands,
matching the option handled by main. Leave the argument validation and other
usage entries unchanged.

Comment on lines +201 to +237
function readStableFile(target: string, maxBytes: number): StableFile {
const noFollow = fs.constants.O_NOFOLLOW;
if (typeof noFollow !== "number") fail("O_NOFOLLOW is unavailable");
let descriptor: number;
try {
descriptor = fs.openSync(target, fs.constants.O_RDONLY | noFollow);
} catch {
fail(`could not safely open ${target}`);
}
try {
const before = fs.fstatSync(descriptor, { bigint: true });
if (
!before.isFile() ||
before.nlink !== 1n ||
before.size < 0n ||
before.size > BigInt(maxBytes)
) {
fail(`refusing unsafe or oversized transaction file ${target}`);
}
const bytes = Buffer.alloc(Number(before.size));
let offset = 0;
while (offset < bytes.length) {
const count = fs.readSync(descriptor, bytes, offset, bytes.length - offset, offset);
if (count === 0) break;
offset += count;
}
const overflow = Buffer.alloc(1);
const overflowCount = fs.readSync(descriptor, overflow, 0, 1, offset);
const after = fs.fstatSync(descriptor, { bigint: true });
if (offset !== bytes.length || overflowCount !== 0 || !sameStableMetadata(before, after)) {
fail(`${target} changed while it was captured`);
}
return { bytes, stat: before };
} finally {
fs.closeSync(descriptor);
}
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add O_NONBLOCK so a planted FIFO cannot hang the root apply.

readStableFile opens the target with O_RDONLY | O_NOFOLLOW only. If the path is a FIFO with no writer, fs.openSync blocks until a writer appears. The before.isFile() check at Line 213 runs after the open, so it cannot prevent the hang.

The check order makes this reachable. snapshotFile calls fs.lstatSync at Line 388 and rejects non-regular files, then readStableFile opens the path at Line 206. The sandbox account owns the paths under /sandbox and can replace a regular file with a FIFO between those two steps. Root then blocks forever inside beginManagedStartupSharedStateTransaction. fileMatchesReceipt at Line 715 and verifyRestoration at Line 955 read the same sandbox-controlled paths.

readStableRegularFileSnapshot in src/lib/onboard/managed-startup/image-runtime.ts at Line 762 already adds O_NONBLOCK for this reason. Use the same flag here. The post-open isFile() check then rejects the FIFO instead of blocking.

🔒 Proposed fix to open without blocking
 function readStableFile(target: string, maxBytes: number): StableFile {
   const noFollow = fs.constants.O_NOFOLLOW;
   if (typeof noFollow !== "number") fail("O_NOFOLLOW is unavailable");
+  const nonblock = typeof fs.constants.O_NONBLOCK === "number" ? fs.constants.O_NONBLOCK : 0;
   let descriptor: number;
   try {
-    descriptor = fs.openSync(target, fs.constants.O_RDONLY | noFollow);
+    descriptor = fs.openSync(target, fs.constants.O_RDONLY | noFollow | nonblock);
   } catch {
     fail(`could not safely open ${target}`);
   }
📝 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
function readStableFile(target: string, maxBytes: number): StableFile {
const noFollow = fs.constants.O_NOFOLLOW;
if (typeof noFollow !== "number") fail("O_NOFOLLOW is unavailable");
let descriptor: number;
try {
descriptor = fs.openSync(target, fs.constants.O_RDONLY | noFollow);
} catch {
fail(`could not safely open ${target}`);
}
try {
const before = fs.fstatSync(descriptor, { bigint: true });
if (
!before.isFile() ||
before.nlink !== 1n ||
before.size < 0n ||
before.size > BigInt(maxBytes)
) {
fail(`refusing unsafe or oversized transaction file ${target}`);
}
const bytes = Buffer.alloc(Number(before.size));
let offset = 0;
while (offset < bytes.length) {
const count = fs.readSync(descriptor, bytes, offset, bytes.length - offset, offset);
if (count === 0) break;
offset += count;
}
const overflow = Buffer.alloc(1);
const overflowCount = fs.readSync(descriptor, overflow, 0, 1, offset);
const after = fs.fstatSync(descriptor, { bigint: true });
if (offset !== bytes.length || overflowCount !== 0 || !sameStableMetadata(before, after)) {
fail(`${target} changed while it was captured`);
}
return { bytes, stat: before };
} finally {
fs.closeSync(descriptor);
}
}
function readStableFile(target: string, maxBytes: number): StableFile {
const noFollow = fs.constants.O_NOFOLLOW;
if (typeof noFollow !== "number") fail("O_NOFOLLOW is unavailable");
const nonblock = typeof fs.constants.O_NONBLOCK === "number" ? fs.constants.O_NONBLOCK : 0;
let descriptor: number;
try {
descriptor = fs.openSync(target, fs.constants.O_RDONLY | noFollow | nonblock);
} catch {
fail(`could not safely open ${target}`);
}
try {
const before = fs.fstatSync(descriptor, { bigint: true });
if (
!before.isFile() ||
before.nlink !== 1n ||
before.size < 0n ||
before.size > BigInt(maxBytes)
) {
fail(`refusing unsafe or oversized transaction file ${target}`);
}
const bytes = Buffer.alloc(Number(before.size));
let offset = 0;
while (offset < bytes.length) {
const count = fs.readSync(descriptor, bytes, offset, bytes.length - offset, offset);
if (count === 0) break;
offset += count;
}
const overflow = Buffer.alloc(1);
const overflowCount = fs.readSync(descriptor, overflow, 0, 1, offset);
const after = fs.fstatSync(descriptor, { bigint: true });
if (offset !== bytes.length || overflowCount !== 0 || !sameStableMetadata(before, after)) {
fail(`${target} changed while it was captured`);
}
return { bytes, stat: before };
} finally {
fs.closeSync(descriptor);
}
}
🤖 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/managed-startup/shared-state-transaction.ts` around lines 201
- 237, Update readStableFile to include fs.constants.O_NONBLOCK in the flags
passed to fs.openSync alongside O_RDONLY and O_NOFOLLOW, matching
readStableRegularFileSnapshot. Preserve the existing post-open regular-file
validation and stable metadata checks so FIFO replacements are rejected without
blocking.

Carry the reviewed PR3.5 slice unchanged onto the serialized PR3.4a transaction contract through PR3.4b.

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
@ericksoa

Copy link
Copy Markdown
Contributor Author

Superseded by #7969. The implementation tree and PR3.5 review patch are identical; #7969 exists only because this branch's published restack commit has an immutable 104-character body line that fails commitlint, and repository rules correctly prohibit rewriting it. Continue the clean stack from #7969.

@ericksoa ericksoa closed this Jul 31, 2026
jyaunches pushed a commit that referenced this pull request Jul 31, 2026
<!-- markdownlint-disable MD041 -->
## Summary

Adds root-owned, transactional managed shared-state application and the
first Docker adapter. The slice applies and rolls back validated
OpenClaw, Hermes, and LangChain Deep Agents Code state atomically while
keeping production buildless activation and durable crash recovery in
later review units.

## Related Issue

Part of #7744

Supersedes #7962; the implementation tree and review patch are
identical, but this branch corrects an immutable restack commit-message
line that violated `commitlint`.

## Changes

- Define the managed shared-state transaction contract, ownership/mode
checks, commit receipt, and idempotent rollback.
- Add Docker staging and root-apply adapters that execute with `env -i`.
- Forward only the six allowlisted OpenClaw scheduler controls through
the clean root path.
- Validate application controls before completion-file inspection or
filesystem/transaction mutation.
- Refresh a verified same-profile runtime and completion digest without
starting a duplicate shared-state transaction.
- Cover OpenClaw, Hermes, and DCode transaction, replay, ownership,
mode, cleanup, and failure behavior.
- Intentionally expose no production cutover caller in this slice.
PR3.10 owns the transactional bootstrap/cutover integration after the
driver-neutral lifecycle exists; PR3.12 owns restart-spanning
persistence; PR3.15 owns production activation. Wiring these primitives
directly into current onboarding here would create the partial runtime
activation prohibited by #7744.

## Type of Change

- [x] Code change (feature, bug fix, or refactor)
- [ ] Code change with doc updates
- [ ] Doc only (prose changes, no code sample modifications)
- [ ] Doc only (includes code sample changes)

## Quality Gates

- [x] Tests added or updated for changed behavior
- [ ] Existing tests cover changed behavior — justification:
- [ ] Tests not applicable — justification:
- [ ] Docs updated for user-facing behavior changes
- [x] Docs not applicable — justification: This transaction layer
remains dormant and does not change a supported CLI, configuration,
runtime selection, workflow, or support statement.
- [x] Sensitive paths changed (security, policy, credentials, preflight,
onboarding, inference, runner, sandbox, or messaging)
- [x] Sensitive-path review completed or maintainer-approved waiver
recorded — reviewer/approval link/justification: Exact-diff and
independent P1/P2 reviews covered clean-exec forwarding, pre-mutation
validation, same-profile replay, ownership/mode enforcement, atomic
commit, and rollback. The absence of a production caller is required by
this review boundary: PR3.10 integrates cutover only after PR3.6–3.9
establish lifecycle parity, and PR3.15 activates the complete all-agent
path. No P1/P2 remains inside this slice.
- [ ] Non-success, skipped, or missing CI check accepted by maintainer —
check name, approval link, and follow-up issue:

## Documentation Writer Review

- [x] Documentation writer subagent reviewed the completed changes
- Result: `no-docs-needed`
- Evidence: Reviewed the exact 10-file, `+3,251/-37` patch from
`7eb0369e7a0bc91f6ae99fc1eaad3cd274a34c8a` through
`83f29daf129adf51bbffa43106633ee748b7eefc` against the NemoClaw Writing
Guide and controlled terms. The stable patch ID is unchanged from
`13932b9b…511141002`: `9a4ead01527f33245de5ed62412dca1395e33a13`. The
patch adds dormant internal managed-startup image-runtime, root-apply,
shared-state transaction, and Docker-adapter primitives with co-located
source tests. It changes no Markdown, `docs/`, CLI command or action,
public configuration, default, output, workflow, or live-E2E path.
Production-import scans found no activation caller outside the new
internal module graph, so no user-facing documentation change is needed
for this restack.
- Agent: Codex Desktop
<!-- docs-review-head-sha: 83f29da -->
<!-- docs-review-agents-blob-sha: c052d60 -->

## DGX Station Hardware Evidence

- [ ] Tested on DGX Station
- Tested commit:
- Station profile/scenario:
- Result:
- Supporting evidence:

## Verification

- Exact locally validated head/base:
`83f29daf129adf51bbffa43106633ee748b7eefc` /
`7eb0369e7a0bc91f6ae99fc1eaad3cd274a34c8a` (exact-tree local validation
passed; refreshed remote qualification will run)
- Review budget: 10 files, `+3,251/-37`; no documentation paths. Stable
patch ID: `9a4ead01527f33245de5ed62412dca1395e33a13`.
- [x] PR description includes a `Signed-off-by:` line and every commit
appears as `Verified` in GitHub
- [x] Normal `pre-commit`, `commit-msg`, and `pre-push` hooks passed, or
`npm run validate:pr` passed after refreshing `origin/main` when hooks
were skipped or unavailable
- [x] Targeted behavior tests pass for the current change set, or tests
are marked not applicable above — command/result or justification: 87
slice-focused tests and 290 cross-slice regression tests passed; CLI and
plugin builds passed; `npm run validate:pr` passed; `git diff --check`
is clean.
- [ ] Applicable broad gate passed — `npm test` for broad
runtime/test-harness changes; `npm run check` for repo-wide
validation/coverage changes — command/result: Exact-head required CI is
the broad gate for this dormant transaction slice.
- [x] Quality Gates section completed with required justifications or
waivers
- [x] No secrets, API keys, or credentials committed
- [ ] `npm run docs` builds without warnings (doc changes only)
- [ ] Doc pages follow the [style
guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md)
(doc changes only)
- [ ] New doc pages include SPDX header and frontmatter (new pages only)

## Stack

- Base: merged PR3.4b #7961 on `main` at
`7eb0369e7a0bc91f6ae99fc1eaad3cd274a34c8a`
- This slice: PR3.5 branch `feat/buildless-shared-state-transactions-v2`
at `83f29daf129adf51bbffa43106633ee748b7eefc`
- Next: PR3.6 introduces the driver-neutral lifecycle and sandbox-action
parity. It is not part of this review diff.
- Buildless support remains disabled until every supported agent and
required qualification gate passes.

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added managed startup support for applying configuration through
Docker.
* Added completion verification and waiting so startup progress can be
confirmed.
* Added transaction-based handling for shared application state,
including commit and rollback.

* **Reliability Improvements**
* Added safeguards for invalid, oversized, tampered, or incomplete
startup data.
  * Improved recovery after interrupted or failed startup operations.
* Preserved file ownership, permissions, and contents during state
restoration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: onboarding Onboarding FSM, provider setup, sandbox launch, or first-run flow area: security Security controls, permissions, secrets, or hardening feature PR adds or expands user-visible functionality platform: container Affects Docker, containerd, Podman, or images

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant