Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions docs/manage-sandboxes/recover-rebuild-sandboxes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,18 @@ Leaving the agent with `/exit` does not revoke it, and users do not refresh it m

During the lease, `launch` still verifies the owning OpenShell gateway, exact live sandbox identity, registry and agent configuration, effective policy, inference route, required forwards, and semantic runtime health.
Configured inference must return HTTP 2xx from the semantic `inference.local` probe, which is stricter than the HTTP 200–499 reachability diagnostic used by ordinary `connect`.
<AgentOnly variant="openclaw">

For OpenClaw, `connect --probe-only` settles the existing allowlisted pairing flow before it publishes a credential-free pairing qualification with the lease.
The readiness evidence binds the OpenClaw version and trusted registry and agent manifest configuration.
Its pairing qualification binds the canonical CLI client, exact paired device identity, required operator role and scopes, owning OpenShell gateway, sandbox lifecycle identity, and fixed lease epoch.
Before lease acceptance, `launch` makes a bounded, read-only observation of the current OpenClaw-owned pairing state through the owning OpenShell gateway.
It skips the complete pairing approval pass only when the qualification still matches exactly and no relevant allowlisted request is pending.
Any missing, unreadable, malformed, ambiguous, or changed observation runs the complete pairing approval pass.
A relevant allowlisted pending request also runs that complete path.

</AgentOnly>
Hermes and LangChain Deep Agents Code retain their existing session setup on the lease-accepted path.
When those checks pass, it can skip duplicate recovery, readiness polling, and inference-route repair.
The lease is not a health guarantee or repair authority.

Expand Down
14 changes: 14 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,20 @@ During that lease, another `launch` still verifies these conditions:
- The recorded inference selection matches the live route, and `inference.local` returns HTTP 2xx from its semantic probe when inference is configured. This is stricter than the HTTP 200–499 reachability diagnostic used by ordinary `connect`.
- The agent runtime and its required host-side forwards pass their semantic health checks.

<AgentOnly variant="openclaw">

For OpenClaw, `connect --probe-only` also settles the existing allowlisted pairing flow before it publishes the lease.
The readiness evidence binds the OpenClaw version and trusted registry and agent manifest configuration.
Its credential-free pairing qualification binds the canonical CLI client, exact paired device identity, required operator role and scopes, owning OpenShell gateway, sandbox lifecycle identity, and fixed lease epoch.
Before accepting that evidence, `launch` makes a bounded, read-only observation of the current OpenClaw-owned pairing state through the owning OpenShell gateway.
It skips the complete pairing approval pass only when the evidence still matches exactly and no relevant allowlisted request is pending.
Missing, unreadable, malformed, ambiguous, or changed pairing evidence runs the complete pairing approval pass.
A relevant allowlisted pending request also runs that complete path, so late scope requests remain eligible for approval.

</AgentOnly>

Hermes and LangChain Deep Agents Code retain their existing session setup on the lease-accepted path.

After these checks pass, `launch` can skip duplicate recovery, readiness polling, and inference-route repair.
The lease does not replace a health check or authorize repair.
For missing, expired, malformed, inaccessible, mismatched, or unhealthy evidence, NemoClaw fences any prior acceptable evidence before it runs the complete preflight.
Expand Down
4 changes: 2 additions & 2 deletions docs/security/gateway-authentication-controls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ The auto-pair watcher automatically approves device pairing requests from recogn

| Aspect | Detail |
|---|---|
| Default | Startup auto-pairing and `connect`-time approval share one policy. NemoClaw approves devices only when `clientId` is `cli`, `openclaw-cli`, or `openclaw-control-ui`, and only for `operator.pairing`, `operator.read`, and `operator.write` scopes. An allowlisted `clientMode` alone is never sufficient; all other clients or scopes are rejected and logged. |
| Default | Startup auto-pairing and `connect`-time approval share one policy. A lease-qualified `launch` checks current pairing state and runs the complete approval path when the stored qualification no longer matches or a relevant allowlisted request is pending. NemoClaw approves devices only when `clientId` is `cli`, `openclaw-cli`, or `openclaw-control-ui`, and only for `operator.pairing`, `operator.read`, and `operator.write` scopes. An allowlisted `clientMode` alone is never sufficient; all other clients or scopes are rejected and logged. |
| What you can change | This is not a user-facing knob. The allowlist is defined by NemoClaw's OpenClaw device-approval helper. |
| Risk if relaxed | Approving all device types without validation lets rogue or unexpected clients pair with the gateway unchallenged. |
| Recommendation | No action needed. NemoClaw handles this automatically at startup and during `connect` for late scope upgrades. If you see `[auto-pair] rejected unknown client=...` in the logs, investigate the source of the unexpected connection. |
| Recommendation | No action needed. NemoClaw handles this automatically at startup, during `connect`, and through the complete `launch` fallback for late scope upgrades. If you see `[auto-pair] rejected unknown client=...` in the logs, investigate the source of the unexpected connection. |

### Approve Administrative Scopes Manually

Expand Down
19 changes: 18 additions & 1 deletion src/lib/actions/sandbox/auto-pair-approval-connect.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { afterEach, describe, expect, it, vi } from "vitest";
import { performance } from "node:perf_hooks";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import {
runConnectAutoPairApprovalPass,
Expand All @@ -15,8 +16,14 @@ import {
} from "./connect-autopair-budget";

describe("connect auto-pair approval pass", () => {
beforeEach(() => {
performance.clearMeasures("nemoclaw.openclaw-pairing.complete-fallback");
});

afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
performance.clearMeasures("nemoclaw.openclaw-pairing.complete-fallback");
});

it("uses the shared connect approval budget", () => {
Expand All @@ -35,6 +42,16 @@ describe("connect auto-pair approval pass", () => {
});
});

it("records the named complete pairing fallback stage without a timing threshold (#9023)", () => {
const runApprovalPass = vi.fn();

runConnectAutoPairApprovalPass("alpha", "nemoclaw", runApprovalPass);

expect(
performance.getEntriesByName("nemoclaw.openclaw-pairing.complete-fallback"),
).toHaveLength(1);
});

it("pins sandbox exec to the owning OpenShell gateway despite ambient gateway drift (#8942)", () => {
vi.stubEnv("OPENSHELL_GATEWAY", "ambient-sibling");
const spawn = vi.fn((_binary: string, _args: readonly string[]) => ({
Expand Down
20 changes: 16 additions & 4 deletions src/lib/actions/sandbox/auto-pair-approval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1163,8 +1163,20 @@ export function runConnectAutoPairApprovalPass(
return;
}
}
runApprovalPass(sandboxName, {
budget: CONNECT_AUTO_PAIR_BUDGET,
gatewayName: owningGatewayName,
});
const startedAt = performance.now();
try {
runApprovalPass(sandboxName, {
budget: CONNECT_AUTO_PAIR_BUDGET,
gatewayName: owningGatewayName,
});
} finally {
try {
performance.measure("nemoclaw.openclaw-pairing.complete-fallback", {
start: startedAt,
end: performance.now(),
});
} catch {
// Performance measurements never control the complete pairing pass.
}
}
}
54 changes: 54 additions & 0 deletions src/lib/actions/sandbox/connect-qualified-session-setup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";

import type { SandboxEntry } from "../../state/registry";
import {
completeInteractiveSessionSetup,
completeReadinessQualifiedInteractiveSessionSetup,
} from "./connect";

function entry(agent: string): SandboxEntry {
return {
name: "alpha",
agent,
gatewayName: "nemoclaw-8080",
gatewayPort: 8080,
provider: null,
model: null,
gpuEnabled: false,
policies: [],
} as SandboxEntry;
}

describe("readiness-qualified interactive session setup", () => {
it("delegates complete OpenClaw fallback to the existing pairing path once (#9023)", () => {
const runApprovalPass = vi.fn();

completeInteractiveSessionSetup("alpha", entry("openclaw"), runApprovalPass);

expect(runApprovalPass).toHaveBeenCalledOnce();
expect(runApprovalPass).toHaveBeenCalledWith("alpha", "nemoclaw");
});

it("does not run the complete pairing path for qualified OpenClaw state (#9023)", () => {
const runApprovalPass = vi.fn();

completeReadinessQualifiedInteractiveSessionSetup("alpha", entry("openclaw"), runApprovalPass);

expect(runApprovalPass).not.toHaveBeenCalled();
});

it.each(["hermes", "langchain-deepagents-code", "unknown-agent"])(
"keeps the complete session path for %s (#9023)",
(agent) => {
const runApprovalPass = vi.fn();

completeReadinessQualifiedInteractiveSessionSetup("alpha", entry(agent), runApprovalPass);

expect(runApprovalPass).toHaveBeenCalledOnce();
expect(runApprovalPass).toHaveBeenCalledWith("alpha", "nemoclaw");
},
);
});
16 changes: 15 additions & 1 deletion src/lib/actions/sandbox/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1217,10 +1217,24 @@ export function printInteractiveSessionHints(sandboxName: string): void {
export function completeInteractiveSessionSetup(
sandboxName: string,
sb: SandboxEntry | null,
runApprovalPass = runConnectAutoPairApprovalPass,
): void {
maybeEnsureHermesToolGatewayBroker(sb);
const gatewayName = sb ? resolveSandboxGatewayName(sb) : getSandboxTargetGatewayName(sandboxName);
runConnectAutoPairApprovalPass(sandboxName, gatewayName);
runApprovalPass(sandboxName, gatewayName);
}

/** Preserve non-OpenClaw setup after current OpenClaw pairing qualification. */
export function completeReadinessQualifiedInteractiveSessionSetup(
sandboxName: string,
sb: SandboxEntry | null,
runApprovalPass = runConnectAutoPairApprovalPass,
): void {
maybeEnsureHermesToolGatewayBroker(sb);
const agentName = String(sb?.agent ?? "").trim();
if (agentName === "openclaw") return;
const gatewayName = sb ? resolveSandboxGatewayName(sb) : getSandboxTargetGatewayName(sandboxName);
runApprovalPass(sandboxName, gatewayName);
}

/**
Expand Down
Loading
Loading