Skip to content

fix(uninstall): isolate scoped gateway shutdown - #8677

Merged
apurvvkumaria merged 8 commits into
mainfrom
codex/fix-8663-scoped-uninstall-isolation
Aug 10, 2026
Merged

fix(uninstall): isolate scoped gateway shutdown#8677
apurvvkumaria merged 8 commits into
mainfrom
codex/fix-8663-scoped-uninstall-isolation

Conversation

@apurvvkumaria

@apurvvkumaria apurvvkumaria commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Generated Docker gateway configurations now assign one state-root-specific OpenShell sandbox namespace to each NemoClaw gateway.
Scoped uninstall proves the selected gateway configuration, running process namespace, PID state, name, and port immediately before signaling that process.
This keeps graceful Docker teardown scoped to the selected gateway, fails closed on ambiguous identity, and preserves full single-gateway uninstall behavior.

Related Issue

Fixes #8663

Changes

  • Confirm the root cause as OpenShell multi-gateway lifecycle coupling.
    Both gateways used the Docker driver's implicit default sandbox namespace, and graceful gateway shutdown stopped every managed container in that namespace.
    The linked CI evidence showed selected scoped state cleanup before the sibling sandbox destabilized, which ruled against shared NemoClaw state deletion as the cause.
  • Generate a stable Docker sandbox_namespace from the current UID and full gateway state root.
    A process environment marker joins that namespace to runtime identity, so onboarding restarts a legacy process instead of accepting stale configuration.
    The supported Podman schema does not expose this field, so Podman TOML omits it and scoped Podman process teardown fails closed while full single-gateway teardown remains unchanged.
  • Restrict scoped host-process teardown to the selected PID file.
    Require an owned PID file and runtime marker, current-user process ownership, the loaded namespace, and exact gateway name and port before signaling.
    Revalidate the proof immediately before SIGTERM and any SIGKILL fallback.
    Missing or conflicting evidence returns nonzero without a host-wide process sweep or fuzzy match.
  • Require an active managed user service's exact MainPID to load the selected namespace before systemctl --user disable --now.
    Inactive services retain the existing scoped cleanup behavior.
  • Preserve the existing graceful stop, orphan scan, and shared-state cleanup behavior for full single-gateway uninstall.
  • Capture both gateways' PID authority, executable, start identity, command line, listener ownership, namespace, runtime marker, and sandbox phase around live uninstall.
    Probe the surviving sandbox phase, listener, scoped nemoclaw list, and dashboard HTTP three times with delays.
  • Document the fail-closed recovery contract for legacy, modified, or Podman gateway configurations.

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:
  • 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: Independent reviews checked scoped target proof, PID reuse, namespace uniqueness and driver support, full-uninstall preservation, and survivor probes. The actionable namespace-collision, stale Podman evidence, pre-signal revalidation, and E2E identity/probe gaps were corrected.
  • 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: docs-updated
  • Evidence: docs/manage-sandboxes/uninstall-nemoclaw.mdx; docs/reference/commands.mdx; final re-review returned no-doc-change
  • Agent: Codex Desktop

DGX Station Hardware Evidence

  • Tested on DGX Station
  • Tested commit: Not applicable
  • Station profile/scenario: Not applicable
  • Result: Not applicable
  • Supporting evidence: scripts/prepare-dgx-station-host.sh is unchanged.

Verification

  • 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 — the required four-file CLI command passed 75 tests; gateway config/auth/launch passed 16 tests; Podman E2E support passed 10 tests; semantic E2E checks passed 20 tests; live target collection succeeded; npm run typecheck:cli, npm run checks:repository, npm run docs, and both npx prek run --from-ref main --to-ref HEAD and the current origin/main range passed.
  • Applicable broad gate passed — npm test for broad runtime/test-harness changes; npm run check for repo-wide validation/coverage changes — command/result: Not run. The change is limited to gateway startup and scoped uninstall, and the targeted suites plus repository checks cover the changed boundaries.
  • 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)

Live E2E

The concurrent-gateway-ports live E2E is not claimed as passing.

  • The trusted workflow attempt stopped before product execution because the PR E2E controller does not allowlist this target.
  • The local macOS and Colima attempt stopped before onboarding because the pinned Debian packages were unavailable to the image builder, and apt-get exited with status 100.
  • The remaining validation is an authoritative Ubuntu or Brev run of concurrent-gateway-ports after the trusted controller permits the target.

The documentation build passed with zero errors and two Fern warnings.


Signed-off-by: Apurv Kumaria akumaria@nvidia.com

Summary by CodeRabbit

  • Bug Fixes

    • Improved gateway uninstall safety by verifying process ownership, namespace, identity, and port state before cleanup.
    • Uninstall now fails safely and preserves gateway services and local state when ownership cannot be proven.
    • Prevented cleanup from affecting unrelated concurrent gateways.
  • Documentation

    • Added guidance for namespace verification, recovery, and retrying failed scoped uninstalls.
  • Tests

    • Expanded coverage for concurrent gateways, mismatched runtime evidence, and safe failure behavior.

Signed-off-by: Apurv Kumaria <akumaria@nvidia.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR scopes gateway identity and cleanup to each state directory. It validates process and systemd ownership before signaling, fails closed when proof is missing, and expands unit, integration, documentation, and live E2E coverage.

Changes

Gateway isolation

Layer / File(s) Summary
Gateway identity and namespace contracts
src/lib/onboard/docker-driver-gateway-*, src/lib/onboard/gateway-process-*.ts
Gateway IDs and Docker sandbox namespaces now derive from state directories. Process matching validates canonical targets and configurable expected flags.
Scoped host gateway shutdown
src/lib/onboard/host-gateway-process.ts, src/lib/onboard/host-gateway-process-target.test.ts
Scoped cleanup requires PID-file, runtime-marker, namespace, command-line, endpoint, UID, and port evidence. It disables host-wide discovery and rechecks ownership before signaling.
Scoped uninstall and systemd teardown
src/lib/actions/uninstall/run-plan.ts, src/lib/actions/uninstall/run-plan-gateway-*.test.ts
Uninstall propagates process probes and state directories, validates systemd MainPID ownership, reports incomplete cleanup, and adds fail-closed service coverage.
Gateway isolation validation
test/e2e/live/concurrent-gateway-ports.test.ts, docs/manage-sandboxes/uninstall-nemoclaw.mdx, docs/reference/commands.mdx
Live tests capture gateway evidence and repeatedly verify the surviving gateway. Documentation describes namespace requirements and recovery behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant UninstallRunPlan
  participant Systemd
  participant HostGatewayProcess
  participant DefaultGateway
  Operator->>UninstallRunPlan: request scoped uninstall
  UninstallRunPlan->>Systemd: verify selected service MainPID and namespace
  UninstallRunPlan->>HostGatewayProcess: validate and stop selected gateway
  HostGatewayProcess-->>UninstallRunPlan: return ownership and port result
  UninstallRunPlan-->>Operator: complete or preserve state on failure
  DefaultGateway-->>Operator: remain available during survivor probes
Loading

Possibly related PRs

  • NVIDIA/NemoClaw#7663: Both changes validate PID ownership and recheck identity before signaling.
  • NVIDIA/NemoClaw#8416: Both changes harden scoped uninstall failure handling in run-plan.ts.
  • NVIDIA/NemoClaw#8517: Both changes modify Docker-scoped uninstall behavior, but this PR adds namespace and process ownership verification.

Suggested labels: area: routing, bug-fix, security

Suggested reviewers: cv, ericksoa

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The changes address scoped isolation, fail-closed checks, full uninstall preservation, and regression coverage for issue #8663, but authoritative live E2E success is unverified. Run and pass concurrent-gateway-ports.test.ts in the authoritative Ubuntu or Brev environment, then attach the result.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: isolating scoped gateway shutdown during uninstall.
Out of Scope Changes check ✅ Passed The code, tests, configuration changes, and documentation directly support the scoped gateway isolation objectives in issue #8663.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-8663-scoped-uninstall-isolation

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

@github-code-quality

github-code-quality Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/plugin

The overall coverage in commit 5816159 in the codex/fix-8663-scope... branch remains at 96%, unchanged from commit 04e28aa in the main branch.

TypeScript / code-coverage/cli

The overall coverage in commit 5816159 in the codex/fix-8663-scope... branch remains at 82%, unchanged from commit 04e28aa in the main branch.

Show a code coverage summary of the most impacted files.
File main 04e28aa codex/fix-8663-scope... 5816159 +/-
src/lib/adapter...ainer-engine.ts 95% 85% -10%
src/lib/onboard...eway-process.ts 95% 88% -7%
src/lib/onboard...etson-groups.ts 100% 96% -4%
src/lib/onboard...teway-config.ts 100% 97% -3%
src/lib/onboard...der/registry.ts 96% 96% 0%
src/lib/onboard...ntime-marker.ts 80% 81% +1%
src/lib/onboard...-gateway-env.ts 92% 93% +1%
src/lib/onboard...-patch-clone.ts 86% 89% +3%
src/lib/onboard...er-gpu-patch.ts 77% 80% +3%
src/lib/onboard...ndbox-create.ts 83% 100% +17%

Updated August 10, 2026 07:06 UTC

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — No blocking findings reported

Advisor assessment: No blocking advisor findings reported
Next action: Review the warnings below.
Findings: 0 blockers · 1 warning · 0 suggestions

Model lanes

  • GPT-5.6 Terra (primary): Completed · high confidence · 0 blockers · 1 warning · 0 suggestions
  • Nemotron 3 Ultra (second opinion): Failed after a partial review · low confidence · 0 blockers · 0 warnings · 0 suggestions

Second-opinion terminology and E2E selections are advisory. Live E2E does not run automatically for pull requests.

3 semantic terminology decisions

Terminology decisions are advisory. They affect the assessment only when a separate finding identifies concrete semantic impact.

  • justified — sandbox namespace at docs/manage-sandboxes/uninstall-nemoclaw.mdx:87: Keep the state-root-specific modifier when the state-directory distinction controls scoped gateway teardown.
  • established — scoped cleanup at docs/manage-sandboxes/uninstall-nemoclaw.mdx:86: Use scoped cleanup consistently where the selected-gateway limitation is already clear.
  • established — runtime evidence at docs/manage-sandboxes/uninstall-nemoclaw.mdx:88: Keep runtime evidence for retained identity material and name individual files when a procedure requires a specific file.

E2E guidance

Advisory only. A maintainer can dispatch the default E2E suite against this exact revision.

Recommended E2E: concurrent-gateway-ports, onboard-repair, onboard-resume, cloud-onboard

1 optional E2E recommendation
  • double-onboard
1 warning · 0 suggestions

Warnings

Warnings do not block.

PRA-1 Warning — Cover the active service namespace mismatch

  • Location: src/lib/actions/uninstall/run-plan-gateway-service.test.ts:250
  • Category: tests
  • Problem: The new scoped-service test covers an absent configuration namespace, but no test covers a valid Docker configuration with a nonzero systemd MainPID whose process environment has a different sandbox namespace.
  • Impact: A future change can weaken the active-service process-namespace check without failing the current service-path regression test, allowing scoped cleanup to stop a process that belongs to another gateway.
  • Recommendation: Add a service-path unit test with a valid selected Docker configuration, a nonzero MainPID, and a mismatched NEMOCLAW_OPENSHELL_SANDBOX_NAMESPACE. Assert exit code 1 and no systemctl disable --now call.
  • Verification: Inspect the new test's systemctl show response and process-environment stub, then confirm the call log has no systemctl disable --now invocation.
  • Test coverage: In run-plan-gateway-service.test.ts, simulate systemctl show returning a nonzero MainPID and inject a different process namespace; assert scoped uninstall preserves the service and does not signal systemctl disable --now.
  • Evidence: src/lib/actions/uninstall/run-plan-gateway-service.test.ts:250-273 tests only the absent-config namespace path. src/lib/actions/uninstall/run-plan.ts:1016-1040 checks both configuration evidence and a nonzero MainPID process namespace before systemctl disable --now.

Workflow run details

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

@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: 1

🧹 Nitpick comments (8)
src/lib/onboard/host-gateway-process.test.ts (1)

676-734: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding scoped cases for a missing PID file and a dead recorded PID.

The table covers the identity mismatch vectors well. Two scoped branches in stopHostGatewayProcesses have no coverage here:

  • Lines 794-816: the PID file is absent or unreadable while the marker still exists, or the port is still occupied. The expected outcomes are "scoped gateway PID/runtime evidence is incomplete or invalid" and "gateway port 18080 is occupied without PID-file ownership evidence".
  • Lines 852-860: the recorded PID is dead but the port remains occupied. The expected outcome is "recorded PID <pid> is dead but port 18080 remains occupied".

Both branches decide whether runtime evidence is deleted, so a regression there would silently remove the operator's evidence. The fixture already supports this through omitMarker, listenerPids, and the isPortFree dependency, so the cases are cheap to add.

🤖 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/host-gateway-process.test.ts` around lines 676 - 734, Add
table-driven cases to the existing stopHostGatewayProcesses coverage for an
absent or unreadable PID file with the marker present, an occupied port without
PID-file ownership evidence, and a dead recorded PID while the port remains
occupied. Configure the existing scopedGatewayFixture options and isPortFree
dependency to reach each branch, assert the specified ownership-failure
messages, and verify runtime evidence is preserved rather than deleted.

Source: Path instructions

src/lib/onboard/host-gateway-process.ts (1)

399-606: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider splitting scopedGatewayOwnershipProof into staged proof helpers.

The function runs about 15 sequential guards across roughly 200 lines and repeats the { cmdline, compatContainerName, reason } return shape at each rejection. The logic is correct, but the size makes each guard hard to review and hard to test in isolation.

Consider extracting three stages that each return a reason or a value: state-file ownership (lines 413-455), runtime-marker identity (lines 457-492), and process/listener/Docker identity (lines 494-604). A small reject(reason) helper would also remove the repeated literal return shape.

This is a readability improvement only. Do not change the guard order, because the current order proves cheap evidence before it runs ps, lsof, and docker.

🤖 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/host-gateway-process.ts` around lines 399 - 606, Refactor
scopedGatewayOwnershipProof into staged helpers for state-file ownership,
runtime-marker identity, and process/listener/Docker identity, preserving the
existing guard order and behavior. Have each stage return either its proven
value or a rejection reason, and add a small reject helper to centralize the
repeated { cmdline, compatContainerName, reason } shape. Keep expensive process,
listener, and Docker checks in the final stage after the cheaper evidence
checks.

Source: Coding guidelines

src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts (1)

23-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

withManagedGatewayAuthority is duplicated across two uninstall test files. Both files define the same fixture helper. The bodies differ only in the source field of the resolved authority. The isPortFree default and the lsof availability override are identical, so a future change to the scoped-teardown probe contract must be applied twice.

  • src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts#L23-L43: move this helper into a shared test-support module and accept the source value as a parameter.
  • src/lib/actions/uninstall/run-plan-gateway-process-isolation.test.ts#L21-L38: import the shared helper and pass "standalone" as the source value.
🤖 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/actions/uninstall/run-plan-gateway-segregation.test.ts` around lines
23 - 43, Move withManagedGatewayAuthority from
src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts#L23-L43 into a
shared test-support module, adding a source parameter while preserving its
existing defaults and authority fields. Update
src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts#L23-L43 to import
and use the shared helper with its current source value, and update
src/lib/actions/uninstall/run-plan-gateway-process-isolation.test.ts#L21-L38 to
import it and pass "standalone".
src/lib/actions/uninstall/run-plan.ts (3)

1360-1604: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Split the scoped teardown into named phases.

removeManagedDefaultGatewayUserServiceScoped spans about 245 lines and holds roughly twenty decision points. It covers target resolution, inactive-unit disable, pre-stop identity proof, drop-in install, post-reload revalidation, the stop, post-stop proof, and finalization.

Extract the inactive-unit branch (1434-1492), the identity proof (1494-1552), and the finalization (1577-1603) into named helpers. The control flow already returns boolean at every exit, so the split needs no behavior change.

As per coding guidelines: "Keep function complexity low, follow the Biome configuration".

🤖 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/actions/uninstall/run-plan.ts` around lines 1360 - 1604, Split
removeManagedDefaultGatewayUserServiceScoped into named helpers for the
inactive-unit cleanup, pre-stop identity/drop-in validation and stop flow, and
post-stop finalization. Preserve the existing boolean outcomes, warnings,
rollback behavior, and sequencing; keep target resolution in the main function
while extracting the branches corresponding to inactive cleanup, identity proof,
and finalization into focused helpers.

Source: Coding guidelines


1181-1245: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Share one executable-resolution helper instead of two.

runtimeProcessExecutable duplicates processExecutable in src/lib/onboard/host-gateway-process.ts (lines 187-207). The two versions already differ: this one omits the ps -p <pid> -o comm= fallback and omits the path.isAbsolute check on the lsof value.

Both versions decide process ownership before a SIGKILL path runs. Two copies can drift and return different verdicts for the same PID. Extract one adapter-owned helper and call it from both modules.

The same applies to runtimeProcessStartIdentity, scopedGatewayListenerPids, and normalizedExecutablePath. These are host and process boundary probes, so they belong behind an adapter rather than inside an uninstall action module.

As per path instructions: "actions orchestrate, domain modules make pure decisions, adapters own host/process/network boundaries" and "Flag cross-layer cycles, duplicate sources of truth".

🤖 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/actions/uninstall/run-plan.ts` around lines 1181 - 1245, Extract the
host/process boundary helpers runtimeProcessExecutable,
runtimeProcessStartIdentity, scopedGatewayListenerPids, and
normalizedExecutablePath into one shared adapter-owned module, incorporating the
existing processExecutable behavior including the comm= fallback and
absolute-path validation. Update both run-plan and host-gateway-process to reuse
these shared helpers, removing their local duplicates while preserving their
current ownership and port-probing behavior.

Source: Path instructions


1104-1110: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add coverage for the rendered NemoClaw unit template

scripts/lib/openshell-gateway.service.in renders one argument-free ExecStart, so the uninstall path accepts the installed unit. The fixture does not render this template. Add a test that asserts the managed ExecStart remains argument-free.

🤖 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/actions/uninstall/run-plan.ts` around lines 1104 - 1110, Add test
coverage for the rendered NemoClaw unit template, verifying its managed
ExecStart contains exactly one absolute, argument-free executable path so
declaredGatewayServiceBinary accepts it. Render the template through the
existing test fixture or setup path and assert the resulting unit preserves this
contract.
src/lib/actions/uninstall/run-plan-gateway-service.test.ts (2)

557-625: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add fail-closed cases for executable mismatch and a deleted executable.

The three cases cover listener ownership, fragment ambiguity, and UID mismatch. Two branches of the same ownership gate stay unexercised:

  • normalizedExecutablePath(processExecutable) !== normalizedExecutablePath(before.execStartPath) (run-plan.ts lines 1501-1502).
  • processExecutable.endsWith(" (deleted)") (run-plan.ts lines 1500 and 1540).

Both branches guard the SIGKILL path. The fixture already parameterizes identity, so each case needs one more entry plus a readProcessExecutable override.

💚 Proposed additional cases
     {
       label: "the managed process owner differs from the current user",
       listenerPid: 41_201,
       processUid: CURRENT_UID + 1,
     },
+    {
+      executable: "/usr/bin/some-other-binary",
+      label: "the running executable differs from the unit ExecStart",
+      listenerPid: 41_201,
+    },
+    {
+      executable: "/tmp/openshell-gateway (deleted)",
+      label: "the running executable was deleted on disk",
+      listenerPid: 41_201,
+    },

Then read the override in the dependency block:

-        readProcessExecutable: () => gatewayBin,
+        readProcessExecutable: () => identity.executable ?? gatewayBin,

As per path instructions: "Flag copied production algorithms, broad mocks that bypass the behavior under test, and conditionals that make a test pass without exercising its claim."

🤖 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/actions/uninstall/run-plan-gateway-service.test.ts` around lines 557
- 625, Extend the parameterized identity cases in the uninstall test to cover an
executable path mismatch and a process executable ending with “ (deleted)”. Add
the corresponding identity data and make the readProcessExecutable dependency
return the case-specific executable, ensuring both ownership-gate branches are
exercised and the existing fail-closed assertions remain unchanged.

Source: Path instructions


164-192: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse managedSystemdShow for the synthesized inactive response.

Lines 177-189 rebuild the systemd show fixture by hand. managedSystemdShow already produces this shape. The two copies already differ (KillSignal=15 here, SIGTERM there). Both values fail the production check, so behavior matches today, but the duplication can drift.

♻️ Proposed fix
-          const servicePath = getNemoclawOpenShellGatewayUserServicePath(test.home, test.env);
-          const gatewayBin = `${test.home}/.local/bin/openshell-gateway`;
-          return ok(
-            [
-              `FragmentPath=${servicePath}`,
-              `ExecStart={ path=${gatewayBin} ; argv[]=${gatewayBin} ; }`,
-              "ExecStop=",
-              "ExecStopPost=",
-              "ActiveState=inactive",
-              "MainPID=0",
-              "Restart=on-failure",
-              "KillSignal=15",
-              "KillMode=control-group",
-            ].join("\n"),
-          );
+          return ok(managedSystemdShow(test, { active: false, mainPid: 0 }));
🤖 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/actions/uninstall/run-plan-gateway-service.test.ts` around lines 164
- 192, Replace the hand-built inactive systemd response in the run callback with
the existing managedSystemdShow fixture/helper, passing the same service and
gateway context needed by that symbol. Preserve the current conditions that
synthesize the response only for the matching systemctl show call with empty
successful output, and remove the duplicated field list.
🤖 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/actions/uninstall/run-plan-gateway-process-isolation.test.ts`:
- Around line 167-203: Replace the conditional dispatch logic in both
command-router mocks with signature-keyed lookup tables: update the kill and run
mocks at
src/lib/actions/uninstall/run-plan-gateway-process-isolation.test.ts:167-203,
preserving events recording, and update the second run mock at
src/lib/actions/uninstall/run-plan-gateway-process-isolation.test.ts:271-288,
preserving calls recording. Ensure both sites use the same lookup shape while
retaining their existing command responses and assertions.

---

Nitpick comments:
In `@src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts`:
- Around line 23-43: Move withManagedGatewayAuthority from
src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts#L23-L43 into a
shared test-support module, adding a source parameter while preserving its
existing defaults and authority fields. Update
src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts#L23-L43 to import
and use the shared helper with its current source value, and update
src/lib/actions/uninstall/run-plan-gateway-process-isolation.test.ts#L21-L38 to
import it and pass "standalone".

In `@src/lib/actions/uninstall/run-plan-gateway-service.test.ts`:
- Around line 557-625: Extend the parameterized identity cases in the uninstall
test to cover an executable path mismatch and a process executable ending with “
(deleted)”. Add the corresponding identity data and make the
readProcessExecutable dependency return the case-specific executable, ensuring
both ownership-gate branches are exercised and the existing fail-closed
assertions remain unchanged.
- Around line 164-192: Replace the hand-built inactive systemd response in the
run callback with the existing managedSystemdShow fixture/helper, passing the
same service and gateway context needed by that symbol. Preserve the current
conditions that synthesize the response only for the matching systemctl show
call with empty successful output, and remove the duplicated field list.

In `@src/lib/actions/uninstall/run-plan.ts`:
- Around line 1360-1604: Split removeManagedDefaultGatewayUserServiceScoped into
named helpers for the inactive-unit cleanup, pre-stop identity/drop-in
validation and stop flow, and post-stop finalization. Preserve the existing
boolean outcomes, warnings, rollback behavior, and sequencing; keep target
resolution in the main function while extracting the branches corresponding to
inactive cleanup, identity proof, and finalization into focused helpers.
- Around line 1181-1245: Extract the host/process boundary helpers
runtimeProcessExecutable, runtimeProcessStartIdentity,
scopedGatewayListenerPids, and normalizedExecutablePath into one shared
adapter-owned module, incorporating the existing processExecutable behavior
including the comm= fallback and absolute-path validation. Update both run-plan
and host-gateway-process to reuse these shared helpers, removing their local
duplicates while preserving their current ownership and port-probing behavior.
- Around line 1104-1110: Add test coverage for the rendered NemoClaw unit
template, verifying its managed ExecStart contains exactly one absolute,
argument-free executable path so declaredGatewayServiceBinary accepts it. Render
the template through the existing test fixture or setup path and assert the
resulting unit preserves this contract.

In `@src/lib/onboard/host-gateway-process.test.ts`:
- Around line 676-734: Add table-driven cases to the existing
stopHostGatewayProcesses coverage for an absent or unreadable PID file with the
marker present, an occupied port without PID-file ownership evidence, and a dead
recorded PID while the port remains occupied. Configure the existing
scopedGatewayFixture options and isPortFree dependency to reach each branch,
assert the specified ownership-failure messages, and verify runtime evidence is
preserved rather than deleted.

In `@src/lib/onboard/host-gateway-process.ts`:
- Around line 399-606: Refactor scopedGatewayOwnershipProof into staged helpers
for state-file ownership, runtime-marker identity, and process/listener/Docker
identity, preserving the existing guard order and behavior. Have each stage
return either its proven value or a rejection reason, and add a small reject
helper to centralize the repeated { cmdline, compatContainerName, reason }
shape. Keep expensive process, listener, and Docker checks in the final stage
after the cheaper evidence checks.
🪄 Autofix

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: cca0b901-2410-4bc0-8f8b-9e738d81b6dc

📥 Commits

Reviewing files that changed from the base of the PR and between 5003bce and b157d5f.

📒 Files selected for processing (13)
  • src/lib/actions/uninstall/run-plan-gateway-process-isolation.test.ts
  • src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts
  • src/lib/actions/uninstall/run-plan-gateway-service.test.ts
  • src/lib/actions/uninstall/run-plan.ts
  • src/lib/onboard/docker-driver-gateway-prelaunch.test.ts
  • src/lib/onboard/docker-driver-gateway-prelaunch.ts
  • src/lib/onboard/gateway-process-identity.ts
  • src/lib/onboard/gateway-process-target-identity.ts
  • src/lib/onboard/host-gateway-process-target.test.ts
  • src/lib/onboard/host-gateway-process.test.ts
  • src/lib/onboard/host-gateway-process.ts
  • src/lib/tunnel/gateway-port-release-test-helpers.ts
  • test/e2e/live/concurrent-gateway-ports.test.ts

Comment thread src/lib/actions/uninstall/run-plan-gateway-process-isolation.test.ts Outdated

@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.

🧹 Nitpick comments (2)
src/lib/onboard/host-gateway-process.test.ts (1)

768-803: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reference the sibling PID by a shared constant instead of the literal 9_990_808.

siblingPid is declared inside scopedGatewayFixture at Line 96. These tests repeat its numeric value to model "a sibling owns the listener". If siblingPid changes, the literal becomes an unrelated PID. The tests can then still pass while no longer exercising sibling listener ownership. Hoist the PID to a module-level constant and use it in both places.

♻️ Proposed refactor
+const SIBLING_PID = 9_990_808;
+
 function scopedGatewayFixture(options: ScopedGatewayFixtureOptions = {}) {
   const selectedPid = 9_991_880;
-  const siblingPid = 9_990_808;
+  const siblingPid = SIBLING_PID;
     const fixture = scopedGatewayFixture({
-      listenerPids: [9_990_808],
+      listenerPids: [SIBLING_PID],
       omitMarker: true,
     const fixture = scopedGatewayFixture({
-      listenerPids: [9_990_808],
+      listenerPids: [SIBLING_PID],
       portFree: false,
🤖 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/host-gateway-process.test.ts` around lines 768 - 803, Hoist
the sibling PID used by scopedGatewayFixture into a module-level constant,
replace the fixture’s local siblingPid value and both test literals with that
shared constant, and preserve the existing sibling-listener ownership scenarios.

Source: Path instructions

src/lib/actions/uninstall/run-plan-gateway-service.test.ts (1)

222-230: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Key the default systemctl show fallback on an explicit miss, not on an empty successful response.

The wrapper substitutes inactiveSystemdShow whenever the delegated responder returns status 0 with empty stdout. A test that intentionally returns ok("") for the systemctl show signature is silently overridden with inactive metadata. Track whether the caller-supplied run handled the signature instead.

♻️ Proposed refactor
       run: (command, args, options) => {
         const signature = commandSignature(command, args);
-        const response = defaultResponses.get(signature) ?? run(command, args, options);
-        const defaultSystemdShow =
-          response.status === 0 && response.stdout === ""
-            ? defaultSystemdShows.get(signature)
-            : undefined;
-        return defaultSystemdShow ?? response;
+        const seeded = defaultResponses.get(signature);
+        if (seeded !== undefined) return seeded;
+        const response = run(command, args, options);
+        // `run` defaults to `() => ok()`, which signals "unhandled" here.
+        const unhandled = run === defaultRun && response.status === 0 && response.stdout === "";
+        return unhandled ? (defaultSystemdShows.get(signature) ?? response) : response;
       },

defaultRun is the () => ok() default destructured at Line 172.

🤖 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/actions/uninstall/run-plan-gateway-service.test.ts` around lines 222
- 230, Update the run wrapper in the test to track whether
defaultResponses.get(signature) explicitly matched before delegating to
defaultRun. Apply the defaultSystemdShows fallback only when that lookup missed,
not when the delegated response has status 0 and empty stdout, so
caller-supplied ok("") responses remain unchanged.
🤖 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.

Nitpick comments:
In `@src/lib/actions/uninstall/run-plan-gateway-service.test.ts`:
- Around line 222-230: Update the run wrapper in the test to track whether
defaultResponses.get(signature) explicitly matched before delegating to
defaultRun. Apply the defaultSystemdShows fallback only when that lookup missed,
not when the delegated response has status 0 and empty stdout, so
caller-supplied ok("") responses remain unchanged.

In `@src/lib/onboard/host-gateway-process.test.ts`:
- Around line 768-803: Hoist the sibling PID used by scopedGatewayFixture into a
module-level constant, replace the fixture’s local siblingPid value and both
test literals with that shared constant, and preserve the existing
sibling-listener ownership scenarios.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 7298a7d5-6bbe-46d6-9a3b-d7efcae2e7b3

📥 Commits

Reviewing files that changed from the base of the PR and between b157d5f and 9b23cd0.

📒 Files selected for processing (5)
  • src/lib/actions/uninstall/run-plan-gateway-process-isolation.test.ts
  • src/lib/actions/uninstall/run-plan-gateway-service.test.ts
  • src/lib/onboard/host-gateway-process.test.ts
  • src/lib/onboard/host-gateway-process.ts
  • test/e2e/live/concurrent-gateway-ports.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/lib/actions/uninstall/run-plan-gateway-process-isolation.test.ts
  • test/e2e/live/concurrent-gateway-ports.test.ts
  • src/lib/onboard/host-gateway-process.ts

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
@apurvvkumaria
apurvvkumaria marked this pull request as draft August 10, 2026 05:42
@copy-pr-bot

copy-pr-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

Signed-off-by: Apurv Kumaria <akumaria@nvidia.com>
@github-actions

Copy link
Copy Markdown
Contributor

@apurvvkumaria
apurvvkumaria marked this pull request as ready for review August 10, 2026 07:20
@apurvvkumaria
apurvvkumaria merged commit fa2b577 into main Aug 10, 2026
81 of 84 checks passed
@apurvvkumaria
apurvvkumaria deleted the codex/fix-8663-scoped-uninstall-isolation branch August 10, 2026 07:26
@github-actions github-actions Bot added the v0.0.106 Release target label Aug 10, 2026

@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: 2

🧹 Nitpick comments (4)
src/lib/onboard/host-gateway-process-target.test.ts (3)

185-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the specific rejection reason in each cross-match case.

Each case asserts only ownershipFailures?.length is 1. Different cases trigger different guards, so a case can pass for the wrong reason and a regression in one guard can be masked by another.

Two examples from stopHostGatewayProcesses:

  • { name: "nemoclaw", port: 18080 } is rejected by the canonicalGatewayTargetMatches precondition, before any namespace or PID proof runs.
  • { name: "nemoclaw", port: 8080 } passes that precondition and is rejected later by the runtime-marker port comparison.

The adjacent test at Line 200 already asserts the exact reason string. Apply the same assertion per case so each case proves the guard it names.

Line 121 also pins isPortFree: () => true, so the "selected gateway port remains occupied after its process stopped" rejection is never exercised. Consider one case that returns false after the stop.

♻️ Proposed test change
   it.each([
-    ["PID file", { pidFilePid: 9_999_602 }],
-    ["command line", { cmdline: "openshell-gateway[nemoclaw=nemoclaw;port=8080]" }],
-    ["gateway name", { name: "nemoclaw", port: 18080 }],
-    ["gateway port", { name: "nemoclaw", port: 8080 }],
-    ["loaded namespace", { namespace: "default" }],
-  ])("fails closed when a sibling cross-matches by %s (`#8663`)", (_case, overrides) => {
+    [
+      "PID file",
+      { pidFilePid: 9_999_602 },
+      "PID 9999602: PID file and runtime marker do not identify the same process",
+    ],
+    [
+      "command line",
+      { cmdline: "openshell-gateway[nemoclaw=nemoclaw;port=8080]" },
+      "PID 9999601: process command line does not identify the selected gateway name and port",
+    ],
+    [
+      "gateway name",
+      { name: "nemoclaw", port: 18080 },
+      "scoped cleanup requires one canonical name, port, and PID file",
+    ],
+    [
+      "gateway port",
+      { name: "nemoclaw", port: 8080 },
+      "PID 9999601: runtime marker does not identify the selected gateway",
+    ],
+    [
+      "loaded namespace",
+      { namespace: "default" },
+      "PID 9999601: gateway process owner and loaded sandbox namespace cannot be proven",
+    ],
+  ])("fails closed when a sibling cross-matches by %s (`#8663`)", (_case, overrides, reason) => {
     const { kill, pidFile, result } = stopScopedTarget(overrides);
 
     expect(result.stopped).toEqual([]);
-    expect(result.ownershipFailures?.length).toBe(1);
+    expect(result.ownershipFailures).toEqual([reason]);
     expect(kill).not.toHaveBeenCalled();
     expect(fs.existsSync(pidFile)).toBe(true);
   });

Verify each expected string against the current source before applying.

🤖 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/host-gateway-process-target.test.ts` around lines 185 - 198,
Strengthen the parameterized cross-match test around stopScopedTarget and
stopHostGatewayProcesses by asserting the exact ownership-failure reason
expected for each override, matching the adjacent test’s reason-string assertion
and current source text. Also add a case or adjust setup so isPortFree returns
false after the process stops, exercising the occupied-port rejection path
currently bypassed by the fixed true value.

Source: Path instructions


121-121: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Every new test pins the gateway port as free, so both port-based fail-closed rejections stay unexecuted. stopHostGatewayProcesses adds two rejections that depend on isPortFree returning false: "recorded process is dead but its selected port remains occupied" and "selected gateway port remains occupied after its process stopped". Both test files stub isPortFree to always return true, so neither rejection runs in this PR. A regression in either guard would let uninstall report success while a sibling listener is still bound, which is the failure reported in issue #8663.

  • src/lib/onboard/host-gateway-process-target.test.ts#L121-L121: add a scoped case where isPortFree returns false after the stop, and assert the "port remains occupied after its process stopped" rejection with the PID file preserved.
  • src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts#L26-L26: keep isPortFree: () => true as the helper default, and add one segregation case that overrides it with isPortFree: () => false to prove uninstall exits nonzero rather than removing the selected gateway state.
🤖 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/host-gateway-process-target.test.ts` at line 121, Add
coverage for both port-occupied fail-closed paths: in
src/lib/onboard/host-gateway-process-target.test.ts:121-121, add a scoped case
where isPortFree returns false after stopping and assert the “port remains
occupied after its process stopped” rejection while preserving the PID file; in
src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts:26-26, retain
isPortFree: () => true as the default and add a segregation case overriding it
to false that verifies uninstall exits nonzero without removing the selected
gateway state.

Source: Path instructions


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

Remove temporary directories created by the test helpers. The helpers create directories with fs.mkdtempSync, but teardown removes neither the directories nor their remaining files.

🤖 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/host-gateway-process-target.test.ts` at line 78, Update the
test helpers around stateDir creation with fs.mkdtempSync to track each
temporary directory and remove it, including any remaining files, during
teardown. Ensure cleanup runs for every directory created by the helpers without
changing the test behavior.

Source: Learnings

src/lib/actions/uninstall/run-plan-gateway-service.test.ts (1)

128-133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the active-MainPID branch and tighten the systemctl assertion.

Two points on this fixture and test.

First, Line 132 always answers the MainPID probe with "0\n". In removeNemoclawOpenShellGatewayUserService, mainPid === 0 skips processUsesStateScopedSandboxNamespace completely. No test in this file reaches the mainPid > 0 branch, which is the guard that proves a running service process belongs to the selected gateway. That guard is a stated objective of this PR. Add a case that returns a non-zero MainPID and a process environment whose namespace does not match, then assert the service survives.

Second, Line 271 asserts that no systemctl command was recorded. That holds only because the helper at Line 131 intercepts the --property=MainPID probe before delegating to the test run. The source does invoke systemctl for that probe. Assert the specific signaling command instead, so the test states the real guarantee.

♻️ Proposed assertion change
     expect(result.exitCode).toBe(1);
     expect(fs.existsSync(servicePath)).toBe(true);
-    expect(calls.some(([command]) => command === "systemctl")).toBe(false);
+    expect(
+      calls.some(([command, ...args]) => command === "systemctl" && args.includes("disable")),
+    ).toBe(false);

Line 253 also calls writeGatewayState(test) only for its return path and then overwrites the file. Assigning the path to a local first would make the intent clear.

Also applies to: 250-271

🤖 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/actions/uninstall/run-plan-gateway-service.test.ts` around lines 128
- 133, Extend the tests around removeNemoclawOpenShellGatewayUserService with a
non-zero MainPID and mismatched process namespace, asserting the selected
service is preserved. Update the systemctl assertion to verify the specific
signaling command rather than claiming no systemctl invocation occurred. In the
test setup, store the path returned by writeGatewayState(test) in a local before
overwriting the file.

Source: Path instructions

🤖 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/docker-driver-gateway-config.ts`:
- Around line 66-106: Update the onboarding upgrade/uninstall flow around
hasStateScopedSandboxNamespace to migrate or rewrite an existing legacy
openshell-gateway.toml so its Docker driver sandbox_namespace uses
gatewayIdForStateDir(stateDir) before scoped uninstall. Make the migration
idempotent and preserve valid scoped configurations, then add coverage for
upgrade, restart, resume, and uninstall paths.

In `@src/lib/onboard/host-gateway-process.ts`:
- Around line 214-216: Update the owner validation in the host-gateway identity
check to reject an empty trimmed `owner.stdout` before converting it with
`Number`. Preserve the existing nonzero-status and UID comparison checks,
ensuring `ps` output must contain a value that matches `uid`.

---

Nitpick comments:
In `@src/lib/actions/uninstall/run-plan-gateway-service.test.ts`:
- Around line 128-133: Extend the tests around
removeNemoclawOpenShellGatewayUserService with a non-zero MainPID and mismatched
process namespace, asserting the selected service is preserved. Update the
systemctl assertion to verify the specific signaling command rather than
claiming no systemctl invocation occurred. In the test setup, store the path
returned by writeGatewayState(test) in a local before overwriting the file.

In `@src/lib/onboard/host-gateway-process-target.test.ts`:
- Around line 185-198: Strengthen the parameterized cross-match test around
stopScopedTarget and stopHostGatewayProcesses by asserting the exact
ownership-failure reason expected for each override, matching the adjacent
test’s reason-string assertion and current source text. Also add a case or
adjust setup so isPortFree returns false after the process stops, exercising the
occupied-port rejection path currently bypassed by the fixed true value.
- Line 121: Add coverage for both port-occupied fail-closed paths: in
src/lib/onboard/host-gateway-process-target.test.ts:121-121, add a scoped case
where isPortFree returns false after stopping and assert the “port remains
occupied after its process stopped” rejection while preserving the PID file; in
src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts:26-26, retain
isPortFree: () => true as the default and add a segregation case overriding it
to false that verifies uninstall exits nonzero without removing the selected
gateway state.
- Line 78: Update the test helpers around stateDir creation with fs.mkdtempSync
to track each temporary directory and remove it, including any remaining files,
during teardown. Ensure cleanup runs for every directory created by the helpers
without changing the test behavior.
🪄 Autofix

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: aba65e9d-3f1a-4a73-bbe4-7edc77990702

📥 Commits

Reviewing files that changed from the base of the PR and between eac996c and 5816159.

📒 Files selected for processing (14)
  • docs/manage-sandboxes/uninstall-nemoclaw.mdx
  • docs/reference/commands.mdx
  • src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts
  • src/lib/actions/uninstall/run-plan-gateway-service.test.ts
  • src/lib/actions/uninstall/run-plan.ts
  • src/lib/onboard/docker-driver-gateway-config.ts
  • src/lib/onboard/docker-driver-gateway-env.ts
  • src/lib/onboard/docker-driver-gateway-launch.test.ts
  • src/lib/onboard/docker-driver-gateway-launch.ts
  • src/lib/onboard/gateway-process-identity.ts
  • src/lib/onboard/gateway-process-target-identity.ts
  • src/lib/onboard/host-gateway-process-target.test.ts
  • src/lib/onboard/host-gateway-process.ts
  • test/e2e/live/concurrent-gateway-ports.test.ts
💤 Files with no reviewable changes (1)
  • src/lib/onboard/gateway-process-identity.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/e2e/live/concurrent-gateway-ports.test.ts

Comment on lines +66 to +106
/** Prove that a NemoClaw-owned Docker gateway config uses its state-scoped namespace. */
export function hasStateScopedSandboxNamespace(stateDir: string): boolean {
if (typeof process.getuid !== "function" || typeof fs.constants.O_NOFOLLOW !== "number") {
return false;
}
const configPath = path.join(stateDir, DOCKER_DRIVER_GATEWAY_CONFIG_NAME);
let descriptor: number | undefined;
try {
const state = fs.lstatSync(stateDir);
descriptor = fs.openSync(configPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
const config = fs.fstatSync(descriptor);
if (
!state.isDirectory() ||
state.isSymbolicLink() ||
!config.isFile() ||
config.nlink !== 1 ||
state.uid !== process.getuid() ||
config.uid !== state.uid ||
config.size > 64 * 1024
) {
return false;
}
const expected = `sandbox_namespace = ${tomlString(gatewayIdForStateDir(stateDir))}`;
let inDriverTable = false;
const matches = fs
.readFileSync(descriptor, "utf-8")
.split(/\r?\n/)
.filter((line) => {
const trimmed = line.trim();
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
inDriverTable = trimmed === "[openshell.drivers.docker]";
return false;
}
return inDriverTable && trimmed.startsWith("sandbox_namespace =");
});
return matches.length === 1 && matches[0]?.trim() === expected;
} catch {
return false;
} finally {
if (descriptor !== undefined) 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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find every producer of openshell-gateway.toml and of sandbox_namespace entries.
set -euo pipefail

rg -n --type=ts -C4 'DOCKER_DRIVER_GATEWAY_CONFIG_NAME|openshell-gateway\.toml'
rg -n --type=ts -C3 'sandbox_namespace'
rg -n --type=ts -C6 'function tomlString'

Repository: NVIDIA/NemoClaw

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
fd -i 'docker-driver-gateway-config|onboard' src/lib || true

printf '%s\n' '--- tracked references ---'
git ls-files | rg '(^|/)(docker-driver-gateway-config|onboard)([^/]*|/.*)$' || true
rg -n -S -C4 'DOCKER_DRIVER_GATEWAY_CONFIG_NAME|openshell-gateway\.toml|sandbox_namespace|writeDockerDriverGatewayConfig|tomlString' . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/lib/onboard/docker-driver-gateway-config.ts"
printf '%s\n' '--- implementation outline ---'
ast-grep outline "$file" || true

printf '%s\n' '--- implementation ---'
cat -n "$file"

printf '%s\n' '--- exact symbol references ---'
rg -n --type=ts 'DOCKER_DRIVER_GATEWAY_CONFIG_NAME|writeDockerDriverGatewayConfig|hasStateScopedSandboxNamespace|gatewayIdForStateDir|sandbox_namespace' src/lib test src/commands 2>/dev/null || true

printf '%s\n' '--- config filename references outside TypeScript ---'
rg -n -S 'openshell-gateway\.toml|sandbox_namespace' . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' --glob '!agents/**' 2>/dev/null || true

Repository: NVIDIA/NemoClaw

Length of output: 28818


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- config and writer tests ---'
for file in \
  src/lib/onboard/docker-driver-gateway-config-toml.test.ts \
  src/lib/onboard/docker-driver-gateway-config-auth-contract.test.ts \
  src/lib/onboard/docker-driver-gateway-launch.test.ts \
  src/lib/onboard/docker-driver-gateway-cutover.ts \
  src/lib/onboard/docker-driver-gateway-compat.ts \
  src/lib/actions/uninstall/run-plan.ts
do
  if [ -f "$file" ]; then
    printf '\n### %s\n' "$file"
    wc -l "$file"
    rg -n -C5 'buildDockerDriverGatewayConfigToml|writeDockerDriverGatewayConfig|OPENSHELL_GATEWAY_CONFIG|openshell-gateway\.toml|sandbox_namespace|upgrade|legacy|compat|cutover|rename|copyFile|writeFile' "$file" || true
  fi
done

printf '%s\n' '--- all config-builder and file-write call sites ---'
rg -n --type=ts -C3 'buildDockerDriverGatewayConfigToml|writeDockerDriverGatewayConfig|writeFileSync|writeFileAtomic|copyFileSync|renameSync' src/lib/onboard src/lib/actions test/e2e 2>/dev/null | rg -C2 'GatewayConfig|gateway config|openshell-gateway|sandbox_namespace|writeDocker|buildDocker|copyFileSync|renameSync|writeFileSync' || true

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- direct call sites ---'
rg -n --type=ts 'buildDockerDriverGatewayConfigToml|writeDockerDriverGatewayConfig|prepareDockerDriverGatewayConfigEnv' \
  src test --glob '!**/*.map' || true

printf '%s\n' '--- config TOML tests ---'
sed -n '1,260p' src/lib/onboard/docker-driver-gateway-config-toml.test.ts

printf '%s\n' '--- launch tests around namespace assertions ---'
sed -n '80,190p' src/lib/onboard/docker-driver-gateway-launch.test.ts

printf '%s\n' '--- cutover and compatibility source ---'
for file in src/lib/onboard/docker-driver-gateway-cutover.ts src/lib/onboard/docker-driver-gateway-compat.ts; do
  printf '\n### %s\n' "$file"
  cat -n "$file"
done

printf '%s\n' '--- migration references ---'
rg -n -i -C4 'docker.driver.gateway|gateway config|sandbox namespace|legacy identity|identity upgrade|migration|upgrade' \
  docs/security src/lib/onboard test/onboard-gateway-legacy-identity-upgrade-runtime.test.ts \
  --glob '!**/*.test.ts' 2>/dev/null || true

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact builder/writer call sites ---'
rg -n --type=ts 'buildDockerDriverGatewayConfigToml|writeDockerDriverGatewayConfig|prepareDockerDriverGatewayConfigEnv' src test || true

printf '%s\n' '--- config TOML test assertions ---'
rg -n -C8 'sandbox_namespace|gatewayIdForStateDir|buildDockerDriverGatewayConfigToml|writeDockerDriverGatewayConfig|legacy|upgrade' \
  src/lib/onboard/docker-driver-gateway-config-toml.test.ts || true

printf '%s\n' '--- launch test assertions ---'
rg -n -C8 'sandbox_namespace|gatewayIdForStateDir|buildDockerDriverGatewayConfigToml|legacy|upgrade' \
  src/lib/onboard/docker-driver-gateway-launch.test.ts || true

printf '%s\n' '--- compatibility implementation ---'
wc -l src/lib/onboard/docker-driver-gateway-compat.ts
cat -n src/lib/onboard/docker-driver-gateway-compat.ts

printf '%s\n' '--- gateway upgrade test references ---'
rg -n -C8 'legacy|upgrade|config|namespace|gatewayIdForStateDir|openshell-gateway' \
  test/onboard-gateway-legacy-identity-upgrade-runtime.test.ts src/lib/onboard/*gateway*test.ts 2>/dev/null | head -n 500 || true

Repository: NVIDIA/NemoClaw

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- runtime config preparation ---'
sed -n '240,285p' src/lib/onboard/docker-driver-gateway-env.ts
sed -n '105,155p' src/lib/onboard/docker-driver-gateway-launch.ts
sed -n '1,80p' test/support/openshell-gateway-config-helpers.ts

printf '%s\n' '--- uninstall ownership gate and tests ---'
sed -n '1000,1050p' src/lib/actions/uninstall/run-plan.ts
rg -n -C10 'hasStateScopedSandboxNamespace|sandbox_namespace|legacy|unscoped|state-scoped' \
  src/lib/actions/uninstall src/lib/onboard/host-gateway-process-target.test.ts \
  src/lib/onboard/docker-driver-gateway-config-auth-contract.test.ts \
  src/lib/onboard/docker-driver-gateway-config-toml.test.ts || true

printf '%s\n' '--- OpenShell upgrade path ---'
rg -n -C8 'installOpenshell|needsUpgrade|upgrade|prepareDockerDriverGatewayConfigEnv|writeDockerDriverGatewayConfig' \
  src/lib/onboard/openshell-install.ts src/lib/onboard/openshell-pin.ts src/lib/onboard.ts \
  src/lib/onboard/docker-driver-gateway-env.ts src/lib/onboard/docker-driver-gateway-launch.ts || true

Repository: NVIDIA/NemoClaw

Length of output: 50371


Migrate legacy gateway TOML before scoped uninstall

openshell-install upgrades OpenShell without rewriting an existing openshell-gateway.toml. A legacy sandbox_namespace = "nemoclaw" therefore fails hasStateScopedSandboxNamespace, and scoped uninstall cannot stop the gateway. Add an idempotent migration or rewrite the file before uninstall. Add coverage for upgrade, restart, resume, and uninstall.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 89-90: 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(descriptor, "utf-8")
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/docker-driver-gateway-config.ts` around lines 66 - 106,
Update the onboarding upgrade/uninstall flow around
hasStateScopedSandboxNamespace to migrate or rewrite an existing legacy
openshell-gateway.toml so its Docker driver sandbox_namespace uses
gatewayIdForStateDir(stateDir) before scoped uninstall. Make the migration
idempotent and preserve valid scoped configurations, then add coverage for
upgrade, restart, resume, and uninstall paths.

Comment on lines +214 to +216
const uid = typeof process.getuid === "function" ? process.getuid() : -1;
const owner = deps.run("ps", ["-p", String(pid), "-o", "uid="], { env: deps.env });
if (owner.status !== 0 || Number(owner.stdout.trim()) !== uid) return false;

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject an empty ps uid result explicitly.

Line 216 compares Number(owner.stdout.trim()) !== uid. Number("") evaluates to 0. If ps exits with status 0 but prints nothing, the owner check passes for a root-owned run (uid === 0) without any real ownership evidence. This is the first proof in the scoped identity chain, so it must fail closed.

removeNemoclawOpenShellGatewayUserService in src/lib/actions/uninstall/run-plan.ts already guards the same shape with !inspected.stdout.trim(). Apply the same guard here.

🛡️ Proposed fix
   const uid = typeof process.getuid === "function" ? process.getuid() : -1;
   const owner = deps.run("ps", ["-p", String(pid), "-o", "uid="], { env: deps.env });
-  if (owner.status !== 0 || Number(owner.stdout.trim()) !== uid) return false;
+  const ownerUid = owner.stdout.trim();
+  if (owner.status !== 0 || !ownerUid || Number(ownerUid) !== uid) return false;
📝 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 uid = typeof process.getuid === "function" ? process.getuid() : -1;
const owner = deps.run("ps", ["-p", String(pid), "-o", "uid="], { env: deps.env });
if (owner.status !== 0 || Number(owner.stdout.trim()) !== uid) return false;
const uid = typeof process.getuid === "function" ? process.getuid() : -1;
const owner = deps.run("ps", ["-p", String(pid), "-o", "uid="], { env: deps.env });
const ownerUid = owner.stdout.trim();
if (owner.status !== 0 || !ownerUid || Number(ownerUid) !== uid) return false;
🤖 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/host-gateway-process.ts` around lines 214 - 216, Update the
owner validation in the host-gateway identity check to reject an empty trimmed
`owner.stdout` before converting it with `Number`. Preserve the existing
nonzero-status and UID comparison checks, ensuring `ps` output must contain a
value that matches `uid`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v0.0.106 Release target

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scoped uninstall destabilizes a sibling gateway sandbox

3 participants