Skip to content

feat(onboard): enforce one OpenShell gateway lifecycle authority - #6842

Closed
souvikDevloper wants to merge 20 commits into
NVIDIA:mainfrom
souvikDevloper:feat/6576-gateway-lifecycle-authority
Closed

feat(onboard): enforce one OpenShell gateway lifecycle authority#6842
souvikDevloper wants to merge 20 commits into
NVIDIA:mainfrom
souvikDevloper:feat/6576-gateway-lifecycle-authority

Conversation

@souvikDevloper

@souvikDevloper souvikDevloper commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Problem

Refs #6576 (parent epic #6401).

Canonical onboarding recognizes two things: the packaged OpenShell gateway user service, or a standalone gateway it starts itself. A platform image that supervises the gateway with its own system service is neither, so hasOpenShellGatewayUserService() returns false and startDockerDriverGateway() falls straight through to the standalone cutover — which reaps the live listener while the platform service immediately restarts it. The result is the persistent :8080 bind conflict described in the issue, with neither layer authoritative.

The root cause is that gateway ownership was implicit: whichever code path ran first became the owner.

What this changes

Ownership becomes explicit and is enforced before any effect.

1. A public, versioned gateway-management contract (src/lib/onboard/gateway-management.ts)

Declares the mode, endpoint, state location, expected service/runtime identity, and required capabilities. It is platform-neutral — no Brev/GCP control-plane concepts in core NemoClaw — and is consumable by the platform profile in #6404 either as a file (NEMOCLAW_GATEWAY_MANAGEMENT) or as an in-process declaration.

{
  "version": 1,
  "mode": "externally-supervised",
  "endpoint": "http://127.0.0.1:8080",
  "stateDir": "/var/lib/openshell/gateway",
  "supervisor": {
    "kind": "systemd-system",
    "serviceName": "openshell-gateway.service",
    "execPath": "/usr/local/bin/openshell-gateway"
  },
  "requiredCapabilities": ["sandbox.create"]
}

Parsing is fail-closed. An unrecognized version, an unknown capability, or a malformed field is an error — never a silent downgrade to "no declaration", since treating a bad declaration as absent is exactly how a host ends up with two authorities. Unknown fields are rejected rather than ignored, so a credential cannot travel through a surface that gets persisted and emitted.

2. One resolved owner, and effect enforcement (src/lib/onboard/gateway-ownership.ts)

resolveGatewayOwner() establishes exactly one authority per run. assertGatewayEffectAllowed() guards start/stop/restart/destroy/replace/standalone-fallback against it: an externally supervised gateway is attached to and validated, never started, stopped, or replaced. Standalone fallback is prohibited outright in that mode.

evaluateGatewayAttachment() is a pure function deciding whether attachment is safe, and fails on an inactive supervisor, an absent listener, an unknown listener, an unenumerable listener set, a mismatched executable identity, or multiple owners.

3. The gateway FSM handler enforces the owner before all effects and on resume

When supervision is declared, the handler takes an attach-only path that runs no destructive effect and fails before any provider, policy, sandbox, or registry mutation. Resume takes the same path as a fresh run — a recorded complete gateway step is not evidence that the declared owner still holds the port.

4. startDockerDriverGateway() is guarded directly

That function is the only path that starts a gateway process and is reachable from onboarding, rebuild, and recovery, so the guard sits there too rather than only in the FSM.

5. Redacted ownership in machine events

Owner mode, source, endpoint, and supervisor identity flow into advanceTo metadata via describeGatewayOwner(). No credential value is persisted or emitted.

When nothing is declared, behavior is unchanged: the packaged service if installed, otherwise standalone — but the owner is now explicit rather than implied.

Scope

This is the upstream contract and enforcement the issue asks for. It deliberately does not include:

Happy to split further or fold any of those in if maintainers prefer.

Testing

47 new tests; npm run typecheck:cli and biome clean.

  • Contract: version/mode/supervisor/capability validation, endpoint rejecting embedded credentials and query strings, unknown-field rejection, fail-closed file and JSON loading, profile-over-file precedence.
  • Ownership: owner resolution for all three sources, every lifecycle effect refused under external supervision and permitted under self-management, and each attachment failure mode (multiple owners, inactive supervisor, absent listener, unknown listener, incomplete scan, identity mismatch, unhealthy gateway).
  • FSM handler: attaches with zero lifecycle effects, never falls back to standalone when the supervisor is inactive, fails before any effect on a competing listener or identity mismatch, and revalidates the owner on resume rather than trusting the recorded step.

I verified against upstream/main that this introduces no new test failures (the failing suites in my local checkout fail identically on a clean baseline for an unrelated missing dependency).

Note the direct-credential-env repo check also fails on a clean upstream/main checkout here, so it is pre-existing and unrelated.

Summary by CodeRabbit

  • New Features
    • Added a “gateway-management contract” defining externally supervised vs controller-managed lifecycle authority with strict, fail-closed validation.
    • Externally supervised onboarding now attaches only (no lifecycle start/stop/replace) while recording detailed gateway ownership metadata.
  • Documentation
    • Added a “Gateway Lifecycle Authority” deployment guide covering configuration, validation rules, and remediation.
  • Tests
    • Expanded coverage for contract parsing/loading, ownership/attachment probing, host-runtime gating, externally supervised onboarding, and recovery refusal.
  • Refactor
    • Centralized gateway host-runtime wiring with an early start-authorization guard and added raw gateway listener scanning for Docker-driver hosts.

Signed-off-by: Souvik Ghosh 138186578+souvikDevloper@users.noreply.github.com

@copy-pr-bot

copy-pr-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 81c62701-18fe-48a5-9bf4-4f24d6d268ab

📥 Commits

Reviewing files that changed from the base of the PR and between 27e65c0 and a353b5d.

📒 Files selected for processing (2)
  • docs/deployment/gateway-lifecycle-authority.mdx
  • docs/index.yml
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/index.yml
  • docs/deployment/gateway-lifecycle-authority.mdx

📝 Walkthrough

Walkthrough

Adds a versioned gateway-management contract, ownership and attachment validation, host runtime probing, and onboarding enforcement that prevents NemoClaw from performing lifecycle effects on externally supervised gateways. Documentation and navigation describe configuration, validation failures, and remediation.

Changes

Gateway lifecycle authority

Layer / File(s) Summary
Management contract and declaration loading
src/lib/onboard/gateway-management.ts, src/lib/onboard/gateway-management.test.ts, docs/deployment/gateway-lifecycle-authority.mdx, docs/index.yml
Defines strict versioned declarations, loading precedence, ownership modes, validation rules, tests, and deployment documentation.
Ownership resolution and attachment evaluation
src/lib/onboard/gateway-ownership.ts, src/lib/onboard/gateway-ownership.test.ts
Resolves gateway ownership, rejects forbidden lifecycle effects, evaluates supervisor/listener/health evidence, and emits redacted diagnostics.
Host runtime probing and listener discovery
src/lib/onboard/gateway-host-runtime.ts, src/lib/onboard/gateway-host-runtime.test.ts, src/lib/onboard/docker-driver-gateway-port-listener.ts, src/lib/onboard/docker-driver-gateway-runtime.ts
Collects host ownership and attachment evidence, exposes raw listener scans, and wires authorization and environment helpers into onboarding.
Onboarding and recovery enforcement
src/lib/onboard.ts, src/lib/onboard/gateway-recovery.ts, src/lib/onboard/machine/handlers/gateway.ts, src/lib/onboard/machine/**/*test.ts
Adds startup and recovery gates, routes externally supervised gateways through attach-only onboarding, and records gateway ownership in transition metadata.

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

Sequence Diagram(s)

sequenceDiagram
  participant Onboarding
  participant GatewayState
  participant HostRuntime
  participant OwnershipRules
  Onboarding->>GatewayState: resolve gateway owner
  GatewayState->>HostRuntime: collect listener and health evidence
  HostRuntime-->>GatewayState: return attachment probe
  GatewayState->>OwnershipRules: validate external ownership
  OwnershipRules-->>GatewayState: allow attachment or return failure
  GatewayState->>Onboarding: record owner metadata and advance
Loading

Possibly related PRs

  • NVIDIA/NemoClaw#7246: Both introduce the Gateway Lifecycle Authority model and fail-closed onboarding wiring across the gateway management, ownership, host runtime, and listener-scan modules.

Suggested labels: area: security, area: docs

Suggested reviewers: jyaunches, cv, apurvvkumaria

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.83% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: enforcing a single OpenShell gateway lifecycle authority during onboarding.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
docs/index.yml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


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

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — Informational

Advisor assessment: Informational / medium confidence
Next action: No advisor follow-up needed.
Findings: 0 blockers · 0 warnings · 0 suggestions
Status: No actionable findings remain in the canonical review ledger.

Model lanes

  • GPT-5.6 Terra (primary): Completed · medium confidence · 0 blockers · 0 warnings · 0 suggestions
  • Nemotron 3 Ultra (second opinion): Completed · high confidence · 0 blockers · 0 warnings · 0 suggestions
  • Model comparison: normalized findings match; normalized E2E selections differ; severity counts match.

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: cloud-onboard, onboard-repair, onboard-resume

2 optional E2E recommendations
  • gateway-guard-recovery
  • gateway-drift-preflight

Workflow run details

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

@souvikDevloper
souvikDevloper force-pushed the feat/6576-gateway-lifecycle-authority branch from 6b6763a to 0298d72 Compare July 14, 2026 08:45
Canonical onboarding recognized the packaged OpenShell gateway user
service or a standalone gateway it started itself. When a platform image
supervises the gateway with its own service, neither was detected, so
onboarding fell through to the standalone cutover: it reaped the live
listener, the platform service restarted it, and the host was left with a
persistent bind conflict and no authoritative owner.

Make ownership explicit and enforce it before any effect.

- Add a public, versioned gateway-management contract declaring the mode,
  endpoint, state location, expected service/runtime identity, and required
  capabilities. Parsing is fail-closed: an unknown version, capability, or
  field is an error, never a silent downgrade to self-management. Unknown
  fields are rejected so credentials cannot travel through metadata that is
  persisted and emitted.
- Resolve exactly one lifecycle owner per run and guard every gateway
  lifecycle effect against it. An externally supervised gateway is attached
  to and validated, never started, stopped, or replaced -- including via the
  standalone fallback.
- Make the gateway FSM handler attach rather than manage when supervision is
  declared, and fail before any provider, policy, sandbox, or registry effect
  on an inactive supervisor, an unknown listener, a mismatched identity, or
  multiple owners. Resume revalidates the owner instead of trusting a
  recorded complete step.
- Guard startDockerDriverGateway itself, covering the rebuild and recovery
  callers that can also start a gateway.
- Report redacted owner, source, and attachment state through machine events.

Host-side wiring lives in a new onboard/gateway-host-runtime module, which
also takes over getGatewayStartEnv, keeping src/lib/onboard.ts net-neutral
per the codebase-growth guardrail.

Refs NVIDIA#6576
@souvikDevloper
souvikDevloper force-pushed the feat/6576-gateway-lifecycle-authority branch from 0298d72 to 9f83c99 Compare July 14, 2026 08:47

@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/gateway-management.ts (1)

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

Derive SUPERVISOR_KINDS from a single source of truth, matching the capabilities pattern.

GatewaySupervisorKind is a standalone union while SUPERVISOR_KINDS is a separately maintained Set — unlike SUPPORTED_GATEWAY_CAPABILITIES/GatewayCapability in this same file, which derive the type from one as const array. If a new kind is added to the union but the Set isn't updated, valid declarations of that kind silently fail closed with no compile-time signal.

♻️ Proposed fix
-export type GatewaySupervisorKind = "systemd-system" | "systemd-user" | "external";
+export const SUPPORTED_GATEWAY_SUPERVISOR_KINDS = ["systemd-system", "systemd-user", "external"] as const;
+export type GatewaySupervisorKind = (typeof SUPPORTED_GATEWAY_SUPERVISOR_KINDS)[number];
-const SUPERVISOR_KINDS = new Set<GatewaySupervisorKind>([
-  "systemd-system",
-  "systemd-user",
-  "external",
-]);
+const SUPERVISOR_KINDS = new Set<string>(SUPPORTED_GATEWAY_SUPERVISOR_KINDS);

Also applies to: 85-89

🤖 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/gateway-management.ts` at line 50, Make GatewaySupervisorKind
and SUPERVISOR_KINDS share one source of truth, following the
SUPPORTED_GATEWAY_CAPABILITIES/GatewayCapability pattern: define the supervisor
kinds in an as-const collection, derive the union type from its elements, and
construct the Set from that collection. Update the declarations around
GatewaySupervisorKind and SUPERVISOR_KINDS without changing the supported kind
values.
src/lib/onboard/gateway-host-runtime.ts (2)

84-93: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

require("node:child_process") flagged by static analysis — likely a false positive here.

spawnSyncImpl is invoked with an argument array (["systemctl", ...scope, "is-active", supervisor.serviceName]), not a shell string, so this isn't exploitable OS command injection. Still, using a lazy require() instead of a top-level import { spawnSync } from "node:child_process" mixes CJS/ESM styles for no apparent reason, since deps.spawnSyncImpl already exists as the test seam.

♻️ Optional cleanup
+import { spawnSync as defaultSpawnSync } from "node:child_process";
+
 ...
-    const spawnSyncImpl =
-      deps.spawnSyncImpl ??
-      (require("node:child_process") as typeof import("node:child_process")).spawnSync;
+    const spawnSyncImpl = deps.spawnSyncImpl ?? defaultSpawnSync;
🤖 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/gateway-host-runtime.ts` around lines 84 - 93, Update the
spawnSyncImpl initialization in the supervisor status-checking function to use a
top-level spawnSync import from node:child_process as its default, while
preserving deps.spawnSyncImpl as the test override. Remove the lazy require
expression and keep the existing argument-array invocation and result handling
unchanged.

Source: Linters/SAST tools


137-147: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant condition.

stableGatewayImage is only non-null when openshellVersion is truthy, so stableGatewayImage && openshellVersion can just check openshellVersion.

🤖 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/gateway-host-runtime.ts` around lines 137 - 147, Simplify the
condition in the stable gateway image setup to check only openshellVersion,
since stableGatewayImage is derived from it and is non-null whenever it is
truthy. Preserve the existing environment assignments and applyOverlayfsAutoFix
behavior inside the conditional.
docs/deployment/gateway-lifecycle-authority.mdx (1)

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

Split combined sentences onto separate lines; remove em dashes.

Several paragraphs pack two sentences onto one line (14, 16, 60, 65, 75, 85), and lines 16 and 65 use em dashes.

✏️ Proposed fix (representative lines)
-Platform images sometimes supervise the gateway with their own service. If canonical onboarding does not know that, both layers believe they own the same gateway and the same port: onboarding replaces the live listener, the platform service immediately restarts it, and the host is left with a persistent bind conflict and no authoritative owner.
+Platform images sometimes supervise the gateway with their own service.
+If canonical onboarding does not know that, both layers believe they own the same gateway and the same port: onboarding replaces the live listener, the platform service immediately restarts it, and the host is left with a persistent bind conflict and no authoritative owner.

-The gateway-management contract makes that ownership explicit. NemoClaw either manages the packaged canonical gateway service, or it attaches to a gateway that an external supervisor owns — never both, and it never silently falls back to starting its own gateway when external supervision was declared.
+The gateway-management contract makes that ownership explicit.
+NemoClaw either manages the packaged canonical gateway service, or it attaches to a gateway that an external supervisor owns, never both, and it never silently falls back to starting its own gateway when external supervision was declared.

As per coding guidelines, "Keep one sentence per line in Markdown and MDX source files" and "Avoid filler, hype, rhetorical questions, emoji, em dashes, and unnecessary bold text."

Also applies to: 16-16, 60-60, 65-65, 75-75, 85-85

🤖 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 `@docs/deployment/gateway-lifecycle-authority.mdx` at line 14, Update the
Markdown/MDX paragraphs on the referenced lines so each sentence occupies its
own source line. Replace em dashes in the paragraphs around lines 16 and 65 with
appropriate punctuation or sentence breaks, while preserving the existing
meaning and content.

Source: Coding guidelines

🤖 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/gateway-host-runtime.ts`:
- Around line 59-79: Remove the cachedOwner state and recompute the gateway
owner on every getGatewayOwner() call using the current declaration and
hasOpenShellGatewayUserService() result. Keep malformed declaration errors and
the existing resolveGatewayOwner inputs unchanged so assertGatewayStartAllowed()
and FSM ownership checks use fresh state.

In `@src/lib/onboard/machine/handlers/gateway.ts`:
- Around line 278-282: Remove the unused session parameter from
attachToExternallySupervisedGateway and update its call site to stop passing
session; preserve the existing returned session from
deps.recordStepComplete("gateway").

---

Nitpick comments:
In `@docs/deployment/gateway-lifecycle-authority.mdx`:
- Line 14: Update the Markdown/MDX paragraphs on the referenced lines so each
sentence occupies its own source line. Replace em dashes in the paragraphs
around lines 16 and 65 with appropriate punctuation or sentence breaks, while
preserving the existing meaning and content.

In `@src/lib/onboard/gateway-host-runtime.ts`:
- Around line 84-93: Update the spawnSyncImpl initialization in the supervisor
status-checking function to use a top-level spawnSync import from
node:child_process as its default, while preserving deps.spawnSyncImpl as the
test override. Remove the lazy require expression and keep the existing
argument-array invocation and result handling unchanged.
- Around line 137-147: Simplify the condition in the stable gateway image setup
to check only openshellVersion, since stableGatewayImage is derived from it and
is non-null whenever it is truthy. Preserve the existing environment assignments
and applyOverlayfsAutoFix behavior inside the conditional.

In `@src/lib/onboard/gateway-management.ts`:
- Line 50: Make GatewaySupervisorKind and SUPERVISOR_KINDS share one source of
truth, following the SUPPORTED_GATEWAY_CAPABILITIES/GatewayCapability pattern:
define the supervisor kinds in an as-const collection, derive the union type
from its elements, and construct the Set from that collection. Update the
declarations around GatewaySupervisorKind and SUPERVISOR_KINDS without changing
the supported kind values.
🪄 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: bb43b065-6f72-4ba9-b3f0-5d772c1ed1d9

📥 Commits

Reviewing files that changed from the base of the PR and between ac97fdc and 0298d72.

📒 Files selected for processing (11)
  • docs/deployment/gateway-lifecycle-authority.mdx
  • docs/index.yml
  • src/lib/onboard.ts
  • src/lib/onboard/gateway-host-runtime.ts
  • src/lib/onboard/gateway-management.test.ts
  • src/lib/onboard/gateway-management.ts
  • src/lib/onboard/gateway-ownership.test.ts
  • src/lib/onboard/gateway-ownership.ts
  • src/lib/onboard/machine/handlers/gateway.test.ts
  • src/lib/onboard/machine/handlers/gateway.ts
  • src/lib/onboard/machine/initial-flow-phases.test.ts

Comment thread src/lib/onboard/gateway-host-runtime.ts
Comment thread src/lib/onboard/machine/handlers/gateway.ts

@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

♻️ Duplicate comments (1)
src/lib/onboard/gateway-host-runtime.ts (1)

59-79: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Stale gateway owner cached for the process lifetime.

cachedOwner is memoized in the createGatewayHostRuntime() closure. Once resolved, later calls never re-read NEMOCLAW_GATEWAY_MANAGEMENT or hasOpenShellGatewayUserService(), so assertGatewayStartAllowed() and the FSM ownership checks can act on stale ownership if state changes mid-process (e.g., across rebuild/recovery attempts in the same run). This was flagged on a previous commit of this file and remains unresolved.

♻️ Proposed fix
 export function createGatewayHostRuntime(deps: GatewayHostRuntimeDeps): GatewayHostRuntime {
-  let cachedOwner: GatewayOwner | null = null;
-
   /**
    * Resolve the one gateway lifecycle authority for this process. A malformed
    * declaration throws instead of degrading to self-management: a host that
    * meant to hand the gateway to an external supervisor must never silently get
    * a second NemoClaw-owned gateway on the same port.
    */
   function getGatewayOwner(): GatewayOwner {
-    if (cachedOwner) return cachedOwner;
     const loaded = loadGatewayManagementDeclaration();
     if (!loaded.ok) {
       throw new Error(`Invalid gateway management declaration: ${loaded.reason}`);
     }
-    cachedOwner = resolveGatewayOwner({
+    return resolveGatewayOwner({
       declaration: loaded.declaration,
       hasPackagedService: hasOpenShellGatewayUserService(),
     });
-    return cachedOwner;
   }
🤖 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/gateway-host-runtime.ts` around lines 59 - 79, Remove the
process-lifetime memoization in createGatewayHostRuntime and make
getGatewayOwner re-read loadGatewayManagementDeclaration and
hasOpenShellGatewayUserService on every call before resolving ownership.
Preserve the existing malformed-declaration error behavior and ensure
assertGatewayStartAllowed and FSM ownership checks receive the current
GatewayOwner.
🤖 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/gateway-host-runtime.ts`:
- Around line 81-93: Add a short timeout option to the spawnSyncImpl invocation
in isSupervisorUnitActive, alongside the existing UTF-8 encoding configuration.
Preserve the current result.error and result.status === null handling so
timed-out systemctl probes return null.

---

Duplicate comments:
In `@src/lib/onboard/gateway-host-runtime.ts`:
- Around line 59-79: Remove the process-lifetime memoization in
createGatewayHostRuntime and make getGatewayOwner re-read
loadGatewayManagementDeclaration and hasOpenShellGatewayUserService on every
call before resolving ownership. Preserve the existing malformed-declaration
error behavior and ensure assertGatewayStartAllowed and FSM ownership checks
receive the current GatewayOwner.
🪄 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: b1124431-4a4f-4095-8d44-818fd6981cdd

📥 Commits

Reviewing files that changed from the base of the PR and between 0298d72 and 9f83c99.

📒 Files selected for processing (11)
  • docs/deployment/gateway-lifecycle-authority.mdx
  • docs/index.yml
  • src/lib/onboard.ts
  • src/lib/onboard/gateway-host-runtime.ts
  • src/lib/onboard/gateway-management.test.ts
  • src/lib/onboard/gateway-management.ts
  • src/lib/onboard/gateway-ownership.test.ts
  • src/lib/onboard/gateway-ownership.ts
  • src/lib/onboard/machine/handlers/gateway.test.ts
  • src/lib/onboard/machine/handlers/gateway.ts
  • src/lib/onboard/machine/initial-flow-phases.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • docs/index.yml
  • docs/deployment/gateway-lifecycle-authority.mdx
  • src/lib/onboard/machine/initial-flow-phases.test.ts
  • src/lib/onboard/gateway-management.test.ts
  • src/lib/onboard/machine/handlers/gateway.test.ts
  • src/lib/onboard/machine/handlers/gateway.ts
  • src/lib/onboard.ts
  • src/lib/onboard/gateway-ownership.test.ts
  • src/lib/onboard/gateway-ownership.ts

Comment thread src/lib/onboard/gateway-host-runtime.ts
…d endpoint

Addresses two blocking findings from the PR review advisor.

The ownership guard sat only in startDockerDriverGateway(), so a
non-Docker-driver host could still reach `openshell gateway start` through
startGatewayWithOptions() or a recovery branch, launching a second gateway
on a host whose external supervisor already owns one. Move the guard to the
shared start entrypoint and add it to recoverGatewayRuntime() and to
startGatewayForRecovery(), which reaches a raw start on the cross-port and
non-default-name branches.

Attachment also probed the process-global gateway port rather than the
declared endpoint, so a declaration naming a different port was assessed
against a different local listener. Probe the declared endpoint, and reject
a declaration whose port is not the one this process operates, since every
downstream consumer is bound to that port. Enforce requiredCapabilities on
the effect path instead of only at parse time.

OpenShell exposes no gateway capability-discovery endpoint, so capabilities
are validated against what this build implements. That is recorded as a
source boundary rather than papered over with invented discovery.

Refs NVIDIA#6576
@souvikDevloper

Copy link
Copy Markdown
Contributor Author

Thanks — both advisor blockers were real. Fixed in 2d60fe37.

PRA-1 — recovery could bypass the guard. Correct, and worse than reported: three paths reached a gateway start without it. startGatewayWithOptions() on a non-Docker-driver host, recoverGatewayRuntime()'s non-Docker branch, and startGatewayForRecovery(), whose cross-port / non-default-name branch calls openshell gateway start directly without going through startGatewayWithOptions at all.

Per the recommendation, the guard now sits on the shared start entrypoint (startGatewayWithOptions) rather than only in startDockerDriverGateway, plus on recoverGatewayRuntime() and at the top of startGatewayForRecovery() so every recovery branch is covered. Regression test added: recovery on the cross-port branch under an externally-supervised declaration proves neither runOpenshell nor startGatewayWithOptions is called.

PRA-2 — endpoint and capabilities. Both parts confirmed.

Endpoint: the probe now targets owner.endpoint instead of the process-global port. On the port question I went with rejecting rather than probing: every downstream consumer (sandbox bridge, registry, dashboard forwards) is bound to GATEWAY_PORT, so a declaration naming a different port isn't a second gateway worth probing — it's a misconfiguration that would validate one gateway and then use another. It now fails closed with endpoint_port_mismatch and tells the operator to align the declaration or set NEMOCLAW_GATEWAY_PORT. Happy to switch to full multi-port attachment if you'd rather.

Capabilities: requiredCapabilities is now enforced on the effect path (capability_unsupported), not just at parse time.

One thing I want to be straight about rather than quietly satisfy: I did not implement gateway-side capability discovery, because OpenShell exposes no endpoint to discover capabilities from. Inventing a probe would be worse than the gap. What the check does honestly do is fail closed when a declaration requires a capability this NemoClaw build does not implement — which is the version-skew case the contract must not let through (newer platform profile, older NemoClaw). It's recorded as an explicit source boundary in gateway-ownership.ts, in the style of the existing boundary comment in docker-driver-gateway-runtime.ts, and can be replaced when OpenShell reports capabilities.

If maintainers would rather not ship a partially-verifiable field, dropping requiredCapabilities from contract v1 is a clean alternative — the version gate means it can be added later without a breaking change. Happy to do that instead.

codebase-growth-guardrails passes (src/lib/onboard.ts net-neutral at +17/-17). New failure modes are covered by tests; no new failures against a clean main baseline.

@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

🤖 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/gateway-ownership.ts`:
- Around line 244-251: Update the capability-mismatch message in the gateway
ownership declaration flow to use plural wording when listing unsupported
capabilities, while preserving the existing unsupported.join(", ") values and
failure behavior.
🪄 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: 6491f606-50fe-4500-bc6e-c53116df9505

📥 Commits

Reviewing files that changed from the base of the PR and between 9f83c99 and 2d60fe3.

📒 Files selected for processing (9)
  • docs/deployment/gateway-lifecycle-authority.mdx
  • src/lib/onboard.ts
  • src/lib/onboard/gateway-host-runtime.ts
  • src/lib/onboard/gateway-ownership.test.ts
  • src/lib/onboard/gateway-ownership.ts
  • src/lib/onboard/gateway-recovery.test.ts
  • src/lib/onboard/gateway-recovery.ts
  • src/lib/onboard/machine/handlers/gateway.test.ts
  • src/lib/onboard/machine/initial-flow-phases.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/lib/onboard/gateway-ownership.test.ts
  • src/lib/onboard.ts
  • docs/deployment/gateway-lifecycle-authority.mdx
  • src/lib/onboard/gateway-host-runtime.ts
  • src/lib/onboard/machine/handlers/gateway.test.ts

Comment on lines +244 to +251
return {
ok: false,
code: "capability_unsupported",
message:
`The declaration requires gateway capability ${unsupported.join(", ")}, which this NemoClaw ` +
`build does not provide. Onboarding stops before any effect rather than attaching to a gateway ` +
`it cannot drive as declared.`,
};

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

Pluralize the capability-mismatch message.

unsupported.join(", ") can contain multiple capability names, but the message reads "requires gateway capability X, Y" — grammatically awkward for more than one item.

✏️ Proposed wording fix
       code: "capability_unsupported",
       message:
-        `The declaration requires gateway capability ${unsupported.join(", ")}, which this NemoClaw ` +
+        `The declaration requires gateway ${unsupported.length > 1 ? "capabilities" : "capability"} ${unsupported.join(", ")}, which this NemoClaw ` +
         `build does not provide. Onboarding stops before any effect rather than attaching to a gateway ` +
         `it cannot drive as declared.`,
📝 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
return {
ok: false,
code: "capability_unsupported",
message:
`The declaration requires gateway capability ${unsupported.join(", ")}, which this NemoClaw ` +
`build does not provide. Onboarding stops before any effect rather than attaching to a gateway ` +
`it cannot drive as declared.`,
};
return {
ok: false,
code: "capability_unsupported",
message:
`The declaration requires gateway ${unsupported.length > 1 ? "capabilities" : "capability"} ${unsupported.join(", ")}, which this NemoClaw ` +
`build does not provide. Onboarding stops before any effect rather than attaching to a gateway ` +
`it cannot drive as declared.`,
};
🤖 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/gateway-ownership.ts` around lines 244 - 251, Update the
capability-mismatch message in the gateway ownership declaration flow to use
plural wording when listing unsupported capabilities, while preserving the
existing unsupported.join(", ") values and failure behavior.

@wscurran wscurran added area: onboarding Onboarding FSM, provider setup, sandbox launch, or first-run flow area: sandbox OpenShell sandbox lifecycle, runtime, config, or recovery feature PR adds or expands user-visible functionality platform: brev Affects Brev hosted development environments labels Jul 14, 2026
@wscurran

Copy link
Copy Markdown
Contributor

@cv cv self-assigned this Jul 14, 2026
cv
cv previously requested changes Jul 14, 2026

@cv cv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocking review on the current head (2d60fe3):\n\n1. The product-scope gate is not satisfied. This PR creates a supported, versioned external-supervisor integration and publishes it as canonical documentation, but #6576, its parent #6401, and the supplying profile #6404 are all still open with needs: design. Before this can be approved, an accepted issue or design decision needs to establish the supported surface and its ownership, lifecycle, compatibility, security, and validation expectations.\n\n2. A valid systemd-owned gateway cannot attach through the current host probe. gateway-host-runtime.ts:123 calls getDockerDriverGatewayPortListenerScan(), whose returned PID set is filtered through Docker-driver process recognition and requireDockerDriverEnv (docker-driver-gateway-port-listener.ts:111-119 and docker-driver-gateway-runtime.ts:451-454). A normal declared systemd-system executable without those markers is discarded and then rejected as unknown_listener before execPath can be compared. Externally supervised attachment needs raw, complete listener enumeration independent of Docker-driver identity, plus a host-runtime test for a real declared systemd listener.\n\n3. The refactor regresses configured gateway ports. gateway-host-runtime.ts imports the module-load GATEWAY_PORT constant and uses it in getGatewayStartEnv(), while onboard.ts maintains the authoritative mutable gateway binding selected for the run. CI reproduces this in test/gateway-start-wait.test.ts: the requested 9443 environment receives 8080 values. Pass the current authoritative port into the host runtime (preferably through an accessor) and test both configured and resumed bindings.\n\n4. The declaration-controlled endpoint is passed directly to the HTTP readiness request, but parseEndpoint accepts arbitrary HTTP(S) hosts. Constrain it to the explicitly supported local gateway origins before any request; add negative coverage for remote, link-local, and metadata-style destinations.\n\n5. The synchronous systemctl is-active probe has no timeout. Add a short timeout and preserve the existing null/unprobeable handling so onboarding cannot hang indefinitely on a wedged supervisor query.\n\nThe branch also currently fails DCO (the PR body has no Signed-off-by declaration), CLI typecheck in the new gateway handler test, the CLI shard above, and the aggregate/E2E gate. Please get the exact-head required checks green after the design and code blockers are resolved.

…red endpoint

Addresses the blocking review on 2d60fe3.

A valid systemd-owned gateway could not attach. The attachment probe used
getDockerDriverGatewayPortListenerScan(), whose PID set is filtered through
Docker-driver process recognition and requireDockerDriverEnv. An ordinary
systemd-run gateway carries none of those markers, so it was discarded and
then rejected as unknown_listener before its execPath could be compared --
externally supervised attachment could never succeed. Add an unfiltered
listener enumeration and use it for the ownership probe, keeping the
Docker-driver-filtered scan for the callers that need runtime identity.

The declaration-controlled endpoint was passed to the HTTP readiness request
while parseEndpoint accepted arbitrary HTTP(S) hosts. Constrain the endpoint
to the supported local gateway origins so a declaration cannot direct a
request at a remote, link-local, or cloud-metadata address.

The refactor also regressed configured gateway ports: the host runtime read
the module-load GATEWAY_PORT constant, while onboard.ts owns the mutable
authoritative binding. Pass the current port through an accessor, resolve the
gateway env module lazily so a reloaded environment is honored, and cover both
the configured and default bindings.

Also from review:
- Add a timeout to the synchronous systemctl is-active probe so a wedged
  supervisor query cannot hang onboarding; unprobeable still reports null.
- Recompute the gateway owner per call instead of memoizing it, since
  onboarding can install the packaged service underneath a cached value.
- Fix the CLI typecheck failure in the gateway handler test.
- Drop the unused session parameter from the attach path.

Signed-off-by: Souvik Ghosh <138186578+souvikDevloper@users.noreply.github.com>
@souvikDevloper

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, @cv. Points 2–5 were real; all are fixed in 27e65c08. Point 1 I can't resolve with code, and I'd rather say so plainly than pretend otherwise.

1. Product-scope gate — acknowledged, and I'll defer to you.

You're right that #6576, #6401, and #6404 are all still open with needs: design, and that this PR creates a supported external-supervisor surface and publishes it as canonical documentation. I opened this from the issue's "Upstream NemoClaw work" list without an accepted design decision behind it, and CONTRIBUTING.md is explicit that design comes first for changes this size. That's on me.

I don't think this should be approved ahead of that decision, and I'm not asking you to. Options, in the order I'd suggest:

Tell me which you prefer and I'll do it. I'd rather not add more code to this until that's settled.

2. Systemd-owned gateway could not attach — confirmed, and it made the feature inert.

You're exactly right, and this was the worst bug in the PR: getDockerDriverGatewayPortListenerScan() filters candidates through isDockerDriverGatewayProcess / requireDockerDriverEnv, so an ordinary systemd-run gateway with none of those markers was discarded, then rejected as unknown_listener before execPath was ever compared. Externally supervised attachment could never have succeeded — the exact case the PR exists to serve.

Added getGatewayPortListenerRawScan(): unfiltered, alive-checked enumeration with the same complete semantics, and the ownership probe now uses it. The Docker-driver-filtered scan is unchanged for the callers that genuinely need runtime identity (it now composes on top of the raw scan). Identity for a supervised gateway is established where it belongs — against the declared execPath.

Coverage: a new gateway-host-runtime.test.ts exercises a real declared systemd listener end-to-end (raw enumeration → declared-exec match → successful attach), plus the unprobeable-supervisor and lazy-port cases.

3. Configured gateway port regression — confirmed.

The host runtime captured the module-load GATEWAY_PORT while onboard.ts owns the mutable authoritative binding. It now takes a gatewayPort() accessor, and the gateway env module is resolved lazily so a reloaded environment is honored (that was the second half of the failure — test/gateway-start-wait.test.ts invalidates only four modules from the require cache, and a hoisted binding pinned the stale one). gateway-start-wait passes locally now, and there's a host-runtime test for a rebound port.

4. Endpoint SSRF — confirmed.

parseEndpoint accepted any HTTP(S) host and the endpoint became the target of a readiness request. It's now constrained to the supported local gateway origins (loopback only), with negative coverage for remote, cloud-metadata (169.254.169.254), link-local, and non-loopback private destinations.

5. systemctl is-active timeout — confirmed. Added a 5s timeout; a timeout surfaces as an error, which the existing null / unprobeable path already handles.

Also from CodeRabbit: the owner is now recomputed per call rather than memoized (onboarding can install the packaged service underneath a cached value), and the unused session parameter is gone.

CI: DCO sign-off added to the PR body. The CLI typecheck failure (TS2352 in the gateway handler test) and the CLI shard failure (gateway-start-wait) were both real and are fixed — I had been running tsconfig.src.json locally instead of typecheck:cli, which is why I missed the first one. codebase-growth-guardrails stays green (src/lib/onboard.ts net-neutral at +21/-21). Locally the affected suites pass and the onboard suite shows no new failures against a clean main baseline.

Signed-off-by: Souvik Ghosh 138186578+souvikDevloper@users.noreply.github.com

@souvikDevloper

Copy link
Copy Markdown
Contributor Author

may i get the ci now @cv

cv
cv previously requested changes Jul 14, 2026

@cv cv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Follow-up review on 27e65c0: the earlier implementation issues in points 2-5 are materially addressed, but the new raw-listener path exposes one additional ownership blocker.

The raw scan proves only that some live PID holds the port. In gateway-ownership.ts, identity checks are conditional on supervisor.execPath; because that field is optional, a declaration with execPath null accepts any live local listener that answers the health probe. Even when an executable path is declared, systemctl is-active and an executable-path match do not prove that the listening PID belongs to the declared unit. A separately launched process using the same binary can pass while the named unit is merely active. Bind the listener PID to the declared supervisor identity (for systemd, authoritative unit PID/cgroup evidence), or fail closed when that relationship cannot be established.

The new "real systemd listener" test does not exercise that boundary: it mocks getGatewayPortListenerRawScan(), cannot read /proc/4242/exe, and then overwrites listenerExecPath before evaluation. Add coverage for the raw enumeration and executable lookup, plus the PID-to-supervisor relationship and negative cases for a same-binary PID outside the unit and a missing execPath.

Security review verdict for this head: FAIL on supervisor/listener identity and its regression coverage. The separate product-scope gate also remains unresolved, as acknowledged above.

@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 (1)
src/lib/onboard/gateway-host-runtime.test.ts (1)

36-52: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

The real declared-endpoint readiness branch is never exercised.

createDeps() always injects probeGatewayHttpReady, so every test short-circuits before waitForDeclaredGatewayHttpReady's endpoint-targeted branch (constructing ${owner.endpoint}/ and calling isGatewayHttpReady) in gateway-host-runtime.ts (lines 133-139). Production wiring omits this override, so that URL-construction logic — the piece explicitly called out as fixed to "target the declared endpoint" — has no test coverage at all.

Add at least one test that omits probeGatewayHttpReady and mocks isGatewayHttpReady/waitForGatewayHttpReady from ./gateway-http-readiness to assert the constructed URL matches the declared endpoint.

As per path instructions, tests under **/*.test.{ts,js,mts,mjs,cts,cjs} should be reviewed "for behavioral confidence rather than implementation lock-in," and broad mocks that bypass the behavior under test should be flagged.

🤖 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/gateway-host-runtime.test.ts` around lines 36 - 52, Add a
focused test for the real declared-endpoint readiness path in the gateway
runtime tests by omitting the createDeps probeGatewayHttpReady override and
mocking the gateway-http-readiness isGatewayHttpReady/waitForGatewayHttpReady
dependencies. Assert that readiness invokes the check with the declared
owner.endpoint plus a trailing slash, while preserving existing test behavior
elsewhere.

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.

Nitpick comments:
In `@src/lib/onboard/gateway-host-runtime.test.ts`:
- Around line 36-52: Add a focused test for the real declared-endpoint readiness
path in the gateway runtime tests by omitting the createDeps
probeGatewayHttpReady override and mocking the gateway-http-readiness
isGatewayHttpReady/waitForGatewayHttpReady dependencies. Assert that readiness
invokes the check with the declared owner.endpoint plus a trailing slash, while
preserving existing test behavior elsewhere.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 95f7c99c-6bc2-44c1-b5ae-2542371698fc

📥 Commits

Reviewing files that changed from the base of the PR and between 2d60fe3 and 27e65c0.

📒 Files selected for processing (9)
  • src/lib/onboard.ts
  • src/lib/onboard/docker-driver-gateway-port-listener.ts
  • src/lib/onboard/docker-driver-gateway-runtime.ts
  • src/lib/onboard/gateway-host-runtime.test.ts
  • src/lib/onboard/gateway-host-runtime.ts
  • src/lib/onboard/gateway-management.test.ts
  • src/lib/onboard/gateway-management.ts
  • src/lib/onboard/machine/handlers/gateway.test.ts
  • src/lib/onboard/machine/handlers/gateway.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/lib/onboard/machine/handlers/gateway.ts
  • src/lib/onboard/gateway-management.test.ts
  • src/lib/onboard.ts
  • src/lib/onboard/gateway-management.ts
  • src/lib/onboard/machine/handlers/gateway.test.ts

@jyaunches jyaunches added v0.0.84 and removed v0.0.83 labels Jul 14, 2026
Addresses the follow-up security review on 27e65c0.

The raw listener scan proved only that some live PID held the port, not that
the PID belonged to the declared unit. Identity was gated on supervisor.execPath,
which was optional, so a declaration with execPath absent accepted any live
local listener that answered the health probe. Even with execPath declared,
`systemctl is-active` plus an executable-path match do not prove the listening
PID is the unit's: a separate process running the same binary passes both while
the named unit is merely active.

Bind the listening PID to the declared supervisor with authoritative cgroup
evidence:

- Make supervisor.execPath required, so there is always something to check the
  listening process against.
- Read the listener's /proc/<pid>/cgroup and confirm it names the declared
  systemd unit. A same-binary process in a login session scope or the user
  slice does not match. When the relationship cannot be established (unreadable
  cgroup, non-systemd supervisor), attachment fails closed rather than trusting
  an executable-path match.

Regression coverage:
- cgroupBelongsToUnit: v1/v2 layouts, user-manager units, a login-session-scope
  impostor, and a prefix-collision unit.
- The host-runtime "real systemd listener" test now drives the probe's own
  /proc/exe and /proc/cgroup reads and evaluates the probe output directly,
  instead of overwriting listenerExecPath before evaluation. Added a
  same-binary-outside-the-unit case and an unreadable-cgroup fail-closed case.
- Raw listener enumeration is covered directly: it reports a listener the
  Docker-driver filter discards, drops dead PIDs, and flags an incomplete scan.

Signed-off-by: Souvik Ghosh <138186578+souvikDevloper@users.noreply.github.com>
@souvikDevloper

Copy link
Copy Markdown
Contributor Author

Thanks — you're right, and the raw-scan fix traded one hole for a subtler one. Fixed in 4a18bce5.

The gap: the raw scan proved a live PID held the port, not that it was the declared unit's PID. Identity was gated on supervisor.execPath (optional), so a null execPath accepted any local listener that answered the health probe; and even with execPath set, systemctl is-active + an exec match don't prove the listening PID is the unit's — a separate process running the same binary passes both while the unit is merely active. Confirmed.

Fix — authoritative PID→unit binding:

  • supervisor.execPath is now required. There is always something to check the listener against; a null-execPath declaration is rejected at parse time.
  • The probe reads the listener's /proc/<pid>/cgroup and confirms it names the declared systemd unit (cgroupBelongsToUnit, handling cgroup v1/v2 and user-manager unit paths). A same-binary process in a login-session scope or the user slice does not match → identity_mismatch. When the relationship can't be established (unreadable cgroup, or a non-systemd external supervisor), attachment fails closed (unknown_listener) rather than trusting an exec-path match.

So the new evaluateGatewayAttachment requires listenerSupervisorMatch === true; false and null both refuse.

On the test you flagged — you were right that it mocked the scan, couldn't read /proc/4242/exe, and overwrote listenerExecPath before evaluation. The /proc/exe and /proc/cgroup reads are now injectable, so the "real systemd listener" test drives the probe's own lookups and evaluates the probe output directly, no overwrite. Added:

  • cgroupBelongsToUnit unit tests: v1/v2 layouts, user-manager units, a login-session-scope impostor, a prefix-collision unit (...service.d).
  • host-runtime: same-binary-outside-the-unit → identity_mismatch; unreadable cgroup → fail-closed.
  • raw enumeration covered directly in docker-driver-gateway-port-listener.test.ts: reports a listener the Docker-driver filter discards, drops dead PIDs, flags an incomplete scan.

external supervisor kind: it has no cgroup/systemd binding mechanism, so listenerSupervisorMatch is null and it fails closed today. That's honest — there's no authoritative way to bind a PID to an opaque external supervisor yet. If you'd rather I drop external from the supported kinds for v1 (leaving only the two systemd kinds, which are verifiable), say the word; the version gate makes re-adding it non-breaking.

138 affected-suite tests pass; typecheck:cli and the guardrail are green (onboard.ts untouched this round). DCO sign-off is on the commit and the PR body.

Product-scope gate (your point 1): still yours to call, and I'm treating it as unresolved. I don't think this should merge ahead of an accepted design on #6576/#6404. My standing offer: reduce this to just the ownership guard (a safety fix independent of the new public surface) and take the versioned contract to a design issue — or park it entirely. Happy to act on whichever you prefer.

Signed-off-by: Souvik Ghosh 138186578+souvikDevloper@users.noreply.github.com

@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

🤖 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/gateway-ownership.test.ts`:
- Line 252: Rename the suite currently titled “cgroupBelongsToUnit” to a
behavior-oriented description of systemd cgroup ownership matching, without
changing the tests or implementation.
🪄 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: dddcef7b-3f28-4b92-b5ab-64df6af47df5

📥 Commits

Reviewing files that changed from the base of the PR and between 27e65c0 and 4a18bce.

📒 Files selected for processing (10)
  • docs/deployment/gateway-lifecycle-authority.mdx
  • src/lib/onboard/docker-driver-gateway-port-listener.test.ts
  • src/lib/onboard/gateway-host-runtime.test.ts
  • src/lib/onboard/gateway-host-runtime.ts
  • src/lib/onboard/gateway-management.test.ts
  • src/lib/onboard/gateway-management.ts
  • src/lib/onboard/gateway-ownership.test.ts
  • src/lib/onboard/gateway-ownership.ts
  • src/lib/onboard/machine/handlers/gateway.test.ts
  • src/lib/onboard/machine/initial-flow-phases.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/lib/onboard/gateway-host-runtime.test.ts
  • src/lib/onboard/gateway-management.test.ts
  • src/lib/onboard/machine/initial-flow-phases.test.ts
  • docs/deployment/gateway-lifecycle-authority.mdx
  • src/lib/onboard/machine/handlers/gateway.test.ts
  • src/lib/onboard/gateway-ownership.ts

});
});

describe("cgroupBelongsToUnit", () => {

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

Use a behavior-oriented suite title.

cgroupBelongsToUnit exposes the private helper name rather than the behavior under test; name the systemd cgroup ownership-matching behavior instead.

Suggested change
-describe("cgroupBelongsToUnit", () => {
+describe("systemd cgroup ownership matching", () => {

As per coding guidelines, “Use behavior-oriented test titles.”

📝 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
describe("cgroupBelongsToUnit", () => {
describe("systemd cgroup ownership matching", () => {
🤖 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/gateway-ownership.test.ts` at line 252, Rename the suite
currently titled “cgroupBelongsToUnit” to a behavior-oriented description of
systemd cgroup ownership matching, without changing the tests or implementation.

Source: Coding guidelines

Persist and revalidate gateway ownership across resume, bind the exact declared endpoint, and reject unsupported HTTPS declarations in contract v1.

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
@jyaunches

Copy link
Copy Markdown
Contributor

Maintainer update for exact head a353b5d17:

  • Aaron confirmed that this gateway-lifecycle-authority approach is acceptable.
  • The branch now includes main at f99249499.
  • The three blockers from the review on c2e422693 are addressed: the owner identity persists across process resume, downstream OpenShell commands bind to the validated declared endpoint, and contract v1 rejects HTTPS.
  • The deployment page now scopes the guarantee to onboarding and warns that uninstall and final sandbox-destroy cleanup do not enforce this declaration.
  • Local validation passes: CLI build, CLI type-check, 24 affected test files (346 tests), the docs build with 0 errors, normal commit hooks, and normal pre-push hooks.
  • GitHub reports every PR commit as Verified.

@apurvvkumaria, please re-review the current head.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

@jyaunches, reviewing the current PR head and the reported blocker resolutions now.

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

@jyaunches
jyaunches requested a review from apurvvkumaria July 22, 2026 13:43

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

Security and merge-readiness review of exact head a353b5d17: PASS across secrets, input validation/SSRF, authorization, dependencies, logging, data protection, configuration, security tests, and system-security/TOCTOU controls. The declaration is loopback-only and credential-free; listener ownership is bound to the declared systemd unit and executable; resume ownership is durable; destructive preflight/recovery paths fail closed under external supervision; and downstream OpenShell commands are bound to the validated endpoint. Local targeted validation and all GitHub checks pass. No actionable security findings.

@jyaunches
jyaunches dismissed apurvvkumaria’s stale review July 22, 2026 16:22

Dismissed as stale: this review targets c2e4226. Its three technical requests—durable owner binding across resume, exact endpoint registration/selection before downstream operations, and resolving the HTTPS readiness mismatch—are implemented and tested on current head a353b5d. Maintainer direction has also resolved the product-scope dependency.

@prekshivyas

Copy link
Copy Markdown
Collaborator

Closing this PR as superseded by #7246 following the accepted lifecycle-authority design in #6576.

Thank you @souvikDevloper for the original contribution. #7246 retains the central ideas introduced here — an explicit authority contract, pure ownership evaluation, systemd listener attestation, and composed lifecycle guards — while adapting them to the repository’s newer per-gateway binding and checkpoint-v2 architecture.

The accepted design now also requires authority revalidation and zero process-management effects during stop, final-sandbox cleanup, and uninstall. This branch explicitly leaves those paths out and is no longer a safe fallback implementation. Work to complete those remaining boundaries continues in #7246.

jyaunches added a commit that referenced this pull request Jul 24, 2026
## Summary

External systemd supervisors can now remain the sole OpenShell gateway
lifecycle authority while NemoClaw validates and attaches to their exact
endpoint. The authority is persisted across onboarding resume, and every
preflight, start, recovery, attachment, stop, final-sandbox cleanup, and
uninstall path fails closed instead of creating or tearing down the
wrong gateway.

Thank you to @souvikDevloper for the original contribution in #6842, and
to @apurvvkumaria for the follow-up work. The underlying onboarding
architecture shifted to per-gateway bindings and versioned checkpoints
while that PR was open, so this needed a fresh implementation. Its
central ideas and structure remain here: an explicit authority contract,
pure ownership evaluation, host attestation, and composed lifecycle
guards.

## Related Issue

Refs #6576

Supersedes #6842

## Changes

- Add a versioned, secret-free gateway management declaration for the
external systemd supervisor required by #6576. A direct port-conflict
exception is insufficient because preflight cleanup, recovery, resume,
and downstream gateway selection are separate effect boundaries; the
authority contract and composed preflight tests protect all of them.
- Bind the canonical gateway name, port, owner, endpoint, supervisor,
and capabilities into checkpoint schema v2 before gateway effects.
Resume migrates v1 checkpoints and rejects cross-process authority
drift.
- Expose the checkpointed management mode and redacted owner through
`nemoclaw status`, `status --json`, and `nemoclaw debug`, without the
external state directory or credential values.
- Reject an endpoint-port mismatch before host inspection or any HTTP
health request.
- Attest the listener through its systemd cgroup, executable, complete
listener set, capabilities, and exact HTTP or mTLS gRPC health endpoint.
- Register and select the exact validated endpoint before provider
selection, including first attachment with no prior OpenShell
registration and replacement of stale registration metadata.
- Revalidate the exact gateway authority before stop, final-sandbox
cleanup, and uninstall. External gateways keep their process, Docker
resources, and OpenShell binaries; NemoClaw never uses legacy `gateway
destroy` for them.
- Document managed and externally supervised modes, TLS bundle layout,
registration behavior, teardown behavior, and resume remediation.

## Type of Change

- [ ] Code change (feature, bug fix, or refactor)
- [x] 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:
- [x] Docs updated for user-facing behavior changes
- [ ] Docs not applicable — justification:
- [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-head security
review](#7246 (comment))
passes declaration input, listener identity, systemd ownership, mTLS
material, exact endpoint selection, lifecycle effects, teardown
revalidation, and durable resume boundaries.
- [ ] 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: `docs-updated`
- Evidence: Head `bcbbef70c` adds only an empty signed CI-retry commit.
Its tree and effective PR documentation diff are identical to reviewed
head `1bd90ae67`. `git diff --check` passed.
- Agent: Codex Desktop documentation writer subagent
<!-- docs-review-head-sha: bcbbef7 -->
<!-- docs-review-agents-blob-sha: 9c9b36d -->

## Verification

- [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 check:diff` passed when hooks were skipped or unavailable
- [x] Targeted behavior tests pass for the current change set, or tests
are marked not applicable above — focused authority and credential tests
passed 11/11; changed tests passed 356/356; the hermetic GPU recreate
test passed 2/2 with CI coverage; CLI type-check, `npm run check:diff`,
and normal commit/pre-push hooks passed.
- [x] Applicable broad gate passed — [exact-head
CI](https://github.com/NVIDIA/NemoClaw/actions/runs/30090344917) and
[E2E](https://github.com/NVIDIA/NemoClaw/actions/runs/30090978569)
passed for `bcbbef70c` on current base `46ecb8677`; [final
coordination](https://github.com/NVIDIA/NemoClaw/actions/runs/30091477102)
passed.
- [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) — passed
with 0 errors and 2 pre-existing Fern warnings
- [x] Doc pages follow the [style
guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md)
(doc changes only)
- [x] New doc pages include SPDX header and frontmatter (new pages only)

---
Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>


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

* **Documentation**
* Added a new “Gateway Lifecycle Authority” deployment page describing
versioned externally supervised gateway lifecycle and strict
attach/recovery validation.
* **New Features**
* Externally supervised gateway attachment mode with non-destructive
behavior, TLS readiness checks, and identity/health probing.
* Durable gateway authority checkpointing (schema v2) with resume
revalidation.
* Extended `status`/`debug` to display redacted gateway authority
details.
* Added an unfiltered gateway port listener scan to improve ownership
detection.
* **Bug Fixes**
* Ensured preflight and recovery refuse destructive actions when gateway
lifecycle authority is externally supervised.
* **Tests**
* Expanded coverage across onboarding attachment gating,
checkpointing/resume, recovery guarding, cleanup/reuse, and listener
enumeration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Julie Yaunches <jyaunches@nvidia.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-authored-by: Apurv Kumaria <akumaria@nvidia.com>
Co-authored-by: Prekshi Vyas <prekshiv@nvidia.com>
Co-authored-by: cjagwani <cjagwani@nvidia.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: sandbox OpenShell sandbox lifecycle, runtime, config, or recovery feature PR adds or expands user-visible functionality platform: brev Affects Brev hosted development environments

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants