Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6a86b42
feat(onboard): bind external gateway authority
jyaunches Jul 20, 2026
f9e163e
merge: sync main into gateway authority change
jyaunches Jul 22, 2026
f4a5c22
fix(onboard): harden external gateway identity checks
jyaunches Jul 22, 2026
b1cad8c
merge: sync main into gateway authority change
jyaunches Jul 22, 2026
c5bd355
fix(onboard): revalidate external gateway attachment
jyaunches Jul 22, 2026
dfbd96d
fix(onboard): harden external gateway identity
jyaunches Jul 22, 2026
122bb63
chore(ci): retry transient E2E gate
jyaunches Jul 22, 2026
9881a09
chore(ci): refresh terminal E2E gate
jyaunches Jul 22, 2026
7958a0c
fix(onboard): revalidate gateway authority before attach
prekshivyas Jul 23, 2026
564e08c
fix(onboard): verify gateway supervisor scope
prekshivyas Jul 23, 2026
4c75508
Merge remote-tracking branch 'origin/main' into codex/review-7246
prekshivyas Jul 23, 2026
82cdc33
fix(onboard): expose validated gateway authority
prekshivyas Jul 23, 2026
928fd84
Merge remote-tracking branch 'origin/main' into codex/review-7246
prekshivyas Jul 23, 2026
f8852af
test(debug): keep authority diagnostics linear
prekshivyas Jul 23, 2026
e4d1804
Merge branch 'main' into codex/pr-7110
cjagwani Jul 23, 2026
96716b5
Merge branch 'main' into codex/pr-7110
cjagwani Jul 23, 2026
d99ab32
fix(onboard): scope recovery authority to target
prekshivyas Jul 23, 2026
2539a25
fix(onboard): require recovery authority guard
prekshivyas Jul 23, 2026
eb689c2
merge: refresh main into gateway authority change
prekshivyas Jul 24, 2026
57ebaf7
fix(gateway): preserve external teardown authority
prekshivyas Jul 24, 2026
aff3e3e
test(gateway): keep teardown coverage linear
prekshivyas Jul 24, 2026
bf8d449
Merge remote-tracking branch 'origin/main' into codex/pr-7110
prekshivyas Jul 24, 2026
6236527
docs(gateway): clarify external authority contract
prekshivyas Jul 24, 2026
f757878
fix(credentials): revalidate gateway authority
prekshivyas Jul 24, 2026
5c11a27
ci: retry E2E after runner loss
prekshivyas Jul 24, 2026
6598822
test(onboard): isolate GPU recreate DNS probe
prekshivyas Jul 24, 2026
079d913
merge: refresh PR with main
prekshivyas Jul 24, 2026
df711cd
merge: refresh PR with latest main
prekshivyas Jul 24, 2026
1bd90ae
ci: retry E2E after runner hang
prekshivyas Jul 24, 2026
bcbbef7
ci: retry E2E after canceled resume run
prekshivyas Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions docs/deployment/gateway-lifecycle-authority.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
---
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
title: "Declare the OpenShell Gateway Lifecycle Authority"
sidebar-title: "Gateway Lifecycle Authority"
description: "Declare whether NemoClaw or an external platform supervisor owns the OpenShell gateway lifecycle, so one component controls each gateway port."
description-agent: "Explains the versioned gateway-management declaration, external supervisor validation, gateway registration, and resume safeguards. Use when a platform supervises the OpenShell gateway or onboarding reports an ownership, listener, or checkpoint conflict."
keywords: ["nemoclaw gateway lifecycle", "externally supervised gateway", "openshell gateway service", "gateway port conflict", "gateway ownership"]
content:
type: "reference"
---
At every point, exactly one component owns the OpenShell gateway lifecycle.

Platform images sometimes supervise the gateway with their own service.
Without an explicit authority, both the platform and NemoClaw can try to own the same process and port.
The gateway-management contract prevents that conflict by making NemoClaw manage the gateway or attach to a gateway that an external supervisor owns.

## Management modes

Choose the mode that matches the component responsible for process lifecycle operations.

| Mode | Lifecycle owner | NemoClaw behavior |
|---|---|---|
| `nemoclaw-managed` | NemoClaw | Starts, stops, replaces, and recovers the gateway. |
| `externally-supervised` | A declared platform supervisor | Validates and attaches to the running gateway without managing its process. |

When nothing is declared, NemoClaw keeps its existing managed behavior.
It uses the packaged gateway user service when that service is installed and otherwise manages a standalone gateway.

## Declare the mode

Set `NEMOCLAW_GATEWAY_MANAGEMENT` to the path of a JSON declaration.

```bash
export NEMOCLAW_GATEWAY_MANAGEMENT=/etc/nemoclaw/gateway-management.json
```

An externally supervised declaration identifies the exact local endpoint, state directory, systemd unit, and executable.

```json
{
"version": 1,
"mode": "externally-supervised",
"endpoint": "https://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": ["gateway.health", "sandbox.create"]
}
```

A managed declaration omits `endpoint`, `stateDir`, and `supervisor`.

```json
{
"version": 1,
"mode": "nemoclaw-managed",
"requiredCapabilities": []
}
```

The declaration uses the following fields.

| Field | Meaning |
|---|---|
| `version` | Contract version. Only `1` is supported. |
| `mode` | `nemoclaw-managed` or `externally-supervised`. |
| `endpoint` | Exact bare origin for an externally supervised gateway. Use `http` or `https`, host `127.0.0.1` or `[::1]`, and the gateway port that this NemoClaw process uses. Credentials, DNS names such as `localhost`, paths, query strings, and fragments are rejected. |
| `stateDir` | Absolute external gateway state directory. Keep it outside NemoClaw-owned state paths such as `~/.local/state/nemoclaw`; uninstall removes those owned paths and does not otherwise target the declared external directory. For HTTPS, it contains the client trust bundle. |
| `supervisor` | Required for `externally-supervised` and rejected for `nemoclaw-managed`. `kind` is `systemd-system` or `systemd-user`. `serviceName` names one `.service` unit, and `execPath` is the absolute gateway executable path. |
| `requiredCapabilities` | Capabilities onboarding needs. Supported values are `gateway.health`, `sandbox.create`, `sandbox.exec`, and `gpu.passthrough`. |

`requiredCapabilities` is checked against the capabilities implemented by this NemoClaw build.
OpenShell does not expose capability discovery, so this field does not probe or attest capabilities on the running gateway.
The listener, supervisor, identity, and health checks below validate the live gateway separately.

The endpoint port must match `NEMOCLAW_GATEWAY_PORT`.
When the endpoint omits a port, `http` resolves to port `80` and `https` resolves to port `443` for this comparison.
Onboarding rejects a mismatch before making a health request or inspecting the host listener because validating one endpoint and operating a different gateway would break the authority boundary.

<Note>
The declaration is versioned and secret-free.
NemoClaw rejects unknown fields and unsupported capabilities instead of ignoring them.
The authority may be persisted in checkpoints, diagnostics, and machine events, so do not place credentials or other secrets in it.
</Note>

<Note>
Only systemd supervisors are supported in version 1.
NemoClaw verifies the listener executable, the declared system or user manager scope, and membership in the declared unit's cgroup.
A matching executable alone is not sufficient because the same binary can run outside the declared unit.
</Note>

## Configure HTTPS health checks

An HTTPS endpoint requires the external gateway's mTLS client files under the declared `stateDir`.

| File | Purpose |
|---|---|
| `stateDir/tls/ca.crt` | Certificate authority used to verify the gateway. |
| `stateDir/tls/client/tls.crt` | Client certificate presented to the gateway. |
| `stateDir/tls/client/tls.key` | Client private key used for mTLS. |

All three paths must be readable regular files.
NemoClaw uses this bundle for the mTLS gRPC health request at the exact declared endpoint path `/openshell.v1.OpenShell/Health`.
For an HTTP endpoint, NemoClaw sends the readiness request to the exact declared origin.

## Validate and attach the gateway

NemoClaw validates external authority before provider, policy, sandbox, or sandbox-registry mutations.
NemoClaw requires systemd to confirm that the declared unit is active.
It checks that exactly one live process holds the configured port.
The process identity must remain stable while NemoClaw reads `/proc` evidence and confirms the listener a second time.
The process must belong to the unit's cgroup under the declared systemd manager scope, and its executable must match `execPath`.
The exact endpoint must answer its health check.

After validation, NemoClaw registers the exact endpoint under the canonical gateway name and selects it as the active OpenShell gateway.
Immediately before registration, NemoClaw reloads the authority and rejects any change from the authority bound to the run.
Port `8080` uses the name `nemoclaw`, and another port uses `nemoclaw-<port>`.
NemoClaw verifies the named and active registrations before advancing to provider selection.
It then repeats the health, supervisor, listener, executable, cgroup, and process identity checks and compares them with the initial evidence.
If registration, selection, or repeated validation fails, NemoClaw removes the attempted registration and stops before provider selection.

Onboarding fails when any of the following conditions applies:

- The endpoint port differs from the gateway port for this process.
- A required capability is unsupported.
- The declared supervisor is inactive, or systemd cannot confirm its active state.
- Nothing is listening on the declared endpoint.
- The listener set cannot be enumerated completely.
- NemoClaw cannot identify a listener process for the occupied port.
- More than one process holds the port.
- The listener process or listener set changes during identity verification.
- The listener does not belong to the declared systemd unit.
- The listener executable does not match `execPath`.
- The endpoint does not answer the required health check.
- The authority differs from the authority already bound to this run or checkpoint.

## Preserve authority across resume

NemoClaw records the resolved, secret-free authority in onboarding checkpoint schema version `2` before gateway preflight.
The record includes the canonical gateway name and port, mode, source, endpoint, state directory, supervisor identity, and required capabilities.

Resume resolves the current authority again and compares the complete record with the checkpoint.
If any value changes, including the per-port gateway binding, resume fails before gateway effects and directs you to start a fresh onboarding run.
A completed gateway step does not bypass listener, supervisor, identity, health, registration, or checkpoint validation.

## Inspect the selected authority

Run `nemoclaw status` to see the management mode and redacted owner identity selected by the last onboarding run.
The JSON form includes the same secret-free fields under `gatewayAuthority`.

```bash
nemoclaw status
nemoclaw status --json
```

`nemoclaw debug` also records `gatewayAuthority` in `onboard-session-summary.txt`.
The status and debug views omit the external state directory and never include credential values.

## Keep external lifecycle paths inert

External supervision prevents every NemoClaw gateway lifecycle path from affecting the supervised process.
Preflight skips stale gateway cleanup, orphaned container cleanup, volume removal, and registry cleanup associated with a NemoClaw-owned runtime.
The shared gateway start guard blocks normal starts.
The external attachment path has no standalone fallback.
Recovery and rebuild guard every start branch, including non-default port recovery.
The gateway state handler validates and attaches instead of starting, stopping, restarting, destroying, or replacing the gateway.

Stop, final-sandbox cleanup, and uninstall reload the declaration before gateway teardown.
When a valid checkpoint exists, they compare the current authority with that checkpoint.
The comparison uses the exact gateway name and port.
If the authority changed, teardown stops before it scans listeners or changes gateway runtime resources.

`nemoclaw stop` does not scan or signal the externally supervised gateway.
Final-sandbox cleanup can stop local dashboard forwards and remove the modern local gateway registration.
It does not signal the gateway, use the legacy `gateway destroy` fallback, or remove its Docker volumes.
Uninstall can delete the selected sandboxes, providers, and local registration.
It keeps the externally supervised gateway process, Docker resources, and OpenShell binaries.

Registering and selecting the validated endpoint changes only the local OpenShell gateway registration used by downstream commands.
It does not transfer process ownership to NemoClaw.

## Fix a failure

Resolve an external gateway failure through the declared supervisor.
For `systemd-system`, inspect the declared service with:

```bash
systemctl status openshell-gateway.service
```

For `systemd-user`, use the user manager:

```bash
systemctl --user status openshell-gateway.service
```

Bring the supervised gateway up, leave exactly one verified process holding the configured port, and rerun `nemoclaw onboard`.
To hand the lifecycle back to NemoClaw, remove the platform gateway service and declare `nemoclaw-managed`, or remove the declaration.
3 changes: 3 additions & 0 deletions docs/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ navigation:
- page: "Deploy to a Headless Server"
path: _build/agent-variants/deployment/deploy-to-headless-server.openclaw.generated.mdx
slug: deploy-to-headless-server
- page: "Gateway Lifecycle Authority"
path: deployment/gateway-lifecycle-authority.mdx
slug: gateway-lifecycle-authority
- section: "Monitoring"
slug: monitoring
collapsed: open-by-default
Expand Down
22 changes: 19 additions & 3 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3140,11 +3140,21 @@ $$nemoclaw gc [--dry-run] [--yes|-y|--force]

### `$$nemoclaw uninstall`

Run `uninstall.sh` to remove NemoClaw sandboxes, gateway resources, related images and containers, and local state.
Run `uninstall.sh` to remove NemoClaw sandboxes, local gateway resources, related images and containers, and local state.
The CLI runs the local `uninstall.sh` shipped with the installed npm package.
If that local script is missing, the CLI does not auto-fetch a remote copy.
It prints the versioned URL of the matching `uninstall.sh` so you can download, review, and run it manually.

When the gateway is externally supervised, uninstall preserves its process, Docker resources, and OpenShell binaries.
It still deletes the selected sandboxes and attempts to remove the modern local gateway registration.
When no sibling gateways remain, it also deletes NemoClaw provider registrations.
It does not use the legacy `gateway destroy` command for that gateway.
<AgentOnly variant="openclaw">

Refer to [Declare the OpenShell Gateway Lifecycle Authority](../deployment/gateway-lifecycle-authority).

</AgentOnly>

Uninstall also stops any orphaned `openshell` host processes left behind by previous onboard or destroy cycles, including `openshell sandbox create`, `openshell ssh-proxy`, and SSH sessions spawned by OpenShell.
Earlier releases only stopped `openshell forward` processes, so those orphans accumulated across runs.

Expand All @@ -3153,7 +3163,9 @@ For Local Ollama setups, uninstall also stops matching Ollama auth proxy process
For Hermes setups, uninstall inspects the selected gateway's managed port-forward watcher state, stops each verified watcher process and its sandbox-scoped forward, and leaves sibling gateway state untouched.
If any watcher or forward cleanup cannot be confirmed, uninstall exits nonzero and preserves the selected gateway's watcher state so you can retry cleanup.

On Linux, uninstall removes `~/.local/state/nemoclaw`, which contains Docker-driver gateway SQLite data, audit logs, VM-driver state, and standalone-fallback gateway PID files.
On Linux, uninstall removes `~/.local/state/nemoclaw`, which contains NemoClaw-owned Docker-driver gateway SQLite data, audit logs, VM-driver state, and standalone-fallback gateway PID files.
Keep a declared external gateway state directory outside that NemoClaw-owned path.
Uninstall does not otherwise target the declared external directory.

| Flag | Effect |
|---|---|
Expand Down Expand Up @@ -3204,7 +3216,11 @@ Decision matrix:
The preserved entries survive uninstall as inert files on disk.
Reinstall NemoClaw and re-onboard the sandbox before `$$nemoclaw <name> snapshot restore` can use them.

Preserving `sandboxes.json` does not make the recorded sandboxes recoverable on their own: uninstall removes the gateway registration, provider registrations, and Docker image those records depend on.
The preserved `sandboxes.json` file does not make the recorded sandboxes recoverable on its own.
Uninstall deletes the selected sandboxes and attempts to remove the local gateway registration.
When no sibling gateways remain, it also deletes provider registrations.
For a NemoClaw-managed gateway, it also removes the Docker image.
For an externally supervised gateway, it preserves Docker resources, but the registry still cannot recover deleted sandbox and provider resources.
Uninstall warns about this at preserve time.
After reinstalling, the installer reports such records as not found on their recorded gateway instead of claiming they were recovered; run `$$nemoclaw <name> destroy` to clear a stranded record, then `$$nemoclaw onboard` to rebuild it.
Pass `--destroy-user-data` at uninstall time if you prefer to purge the registry along with its dependencies.
Expand Down
40 changes: 38 additions & 2 deletions src/commands/credentials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
prompt: vi.fn().mockResolvedValue("yes"),
recoverNamedGatewayRuntime: vi.fn().mockResolvedValue({ recovered: true }),
recoverNamedGatewayRuntime: vi.fn().mockResolvedValue({ recovered: true, attempted: false }),
runOpenshellProviderCommand: vi.fn(),
recordExtraProvider: vi.fn(),
forgetExtraProvider: vi.fn(),
resolveGatewayCredentialMutationAuthority: vi.fn(),
}));

vi.mock("../lib/credentials/store", () => ({
Expand All @@ -21,7 +22,11 @@ vi.mock("../lib/actions/global", () => ({
recordExtraProvider: mocks.recordExtraProvider,
forgetExtraProvider: mocks.forgetExtraProvider,
}));
vi.mock("../lib/onboard/gateway-teardown-authority", () => ({
resolveGatewayCredentialMutationAuthority: mocks.resolveGatewayCredentialMutationAuthority,
}));

import { runCredentialsAddAction } from "../lib/actions/credentials-add";
import CredentialsCommand from "./credentials";
import CredentialsListCommand from "./credentials/list";
import CredentialsResetCommand from "./credentials/reset";
Expand All @@ -31,8 +36,10 @@ const rootDir = process.cwd();
describe("credentials oclif adapter source coverage", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.recoverNamedGatewayRuntime.mockResolvedValue({ recovered: true });
mocks.recoverNamedGatewayRuntime.mockResolvedValue({ recovered: true, attempted: false });
mocks.runOpenshellProviderCommand.mockReturnValue({ status: 0, stdout: "nvidia-prod\n" });
mocks.resolveGatewayCredentialMutationAuthority.mockReturnValue({});
process.exitCode = undefined;
});

it("prints top-level credentials usage", async () => {
Expand All @@ -56,6 +63,7 @@ describe("credentials oclif adapter source coverage", () => {
await CredentialsListCommand.run([], rootDir);

expect(mocks.recoverNamedGatewayRuntime).toHaveBeenCalledWith();
expect(mocks.resolveGatewayCredentialMutationAuthority).not.toHaveBeenCalled();
expect(mocks.runOpenshellProviderCommand).toHaveBeenCalledWith(
["provider", "list", "--names"],
{
Expand Down Expand Up @@ -90,4 +98,32 @@ describe("credentials oclif adapter source coverage", () => {
log.mockRestore();
expect(output).toContain("Removed provider 'nvidia-prod'");
});

it("rejects add and reset before provider mutation when the gateway is healthy but authority changed since onboarding (#6576)", async () => {
mocks.resolveGatewayCredentialMutationAuthority.mockImplementation(() => {
throw new Error(
"Gateway lifecycle authority changed since onboarding; provider credential mutation will not perform gateway effects.",
);
});
const error = vi.spyOn(console, "error").mockImplementation(() => {});

const add = await runCredentialsAddAction({
provider: "custom-provider",
type: "custom",
credentials: [],
configPairs: [],
fromExisting: true,
});
await CredentialsResetCommand.run(["nvidia-prod", "--yes"], rootDir);

expect(add.exitCode).toBe(1);
expect(add.failureLines.join("\n")).toContain(
"gateway lifecycle authority could not be revalidated",
);
expect(mocks.resolveGatewayCredentialMutationAuthority).toHaveBeenCalledTimes(2);
expect(mocks.runOpenshellProviderCommand).not.toHaveBeenCalled();
expect(error.mock.calls.flat().join("\n")).toContain(
"gateway lifecycle authority could not be revalidated",
);
});
});
8 changes: 6 additions & 2 deletions src/commands/credentials/reset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { OPENSHELL_OPERATION_TIMEOUT_MS } from "../../lib/adapters/openshell/tim
import { CLI_NAME } from "../../lib/cli/branding";
import { yesFlag } from "../../lib/cli/common-flags";
import { NemoClawCommand } from "../../lib/cli/nemoclaw-oclif-command";
import { isBridgeProviderName, recoverGatewayOrExit } from "../../lib/credentials/command-support";
import {
isBridgeProviderName,
recoverGatewayForCredentialMutationOrExit,
} from "../../lib/credentials/command-support";
import { prompt as askPrompt, KNOWN_CREDENTIAL_ENV_KEYS } from "../../lib/credentials/store";
import {
deleteProviderWithRecovery,
Expand Down Expand Up @@ -65,7 +68,8 @@ export default class CredentialsResetCommand extends NemoClawCommand {
}
}

if (!(await recoverGatewayOrExit("reach", (lines) => this.failWithLines(lines)))) return;
if (!(await recoverGatewayForCredentialMutationOrExit((lines) => this.failWithLines(lines))))
return;

// `provider delete` trips on FailedPrecondition when the provider is still
// attached to a sandbox (e.g. `<sandbox>-brave-search` after onboard). The
Expand Down
Loading
Loading