Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
69 changes: 2 additions & 67 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ on:
default: ""
type: string
jobs:
description: "Optional comma-separated E2E test IDs. Empty runs default-enabled tests only when targets is also empty; explicit-only tests openshell-gateway-auth-contract, mcp-bridge-dev, hermes-gpu-startup, sandbox-rlimits-connect, jetson-nvmap-gpu, and staging-brev-launchable are skipped unless selected."
description: "Optional comma-separated E2E test IDs. Empty runs default-enabled tests only when targets is also empty; explicit-only tests openshell-gateway-auth-contract, mcp-bridge-dev, hermes-gpu-startup, jetson-nvmap-gpu, and staging-brev-launchable are skipped unless selected."
required: false
default: ""
type: string
Expand Down Expand Up @@ -3350,70 +3350,6 @@ jobs:
shell: bash
run: bash .github/scripts/docker-auth-cleanup.sh

sandbox-rlimits-connect:
needs: generate-matrix
if: ${{ contains(format(',{0},', inputs.jobs), ',sandbox-rlimits-connect,') || contains(format(',{0},', inputs.targets), ',sandbox-rlimits-connect,') }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
E2E_JOB: "1"
E2E_DEFAULT_ENABLED: "0"
E2E_TARGET_ID: "sandbox-rlimits-connect"
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/sandbox-rlimits-connect
NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js
NEMOCLAW_RUN_LIVE_E2E: "1"
NEMOCLAW_E2E_CONNECT_RLIMITS: "1"
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
NEMOCLAW_SANDBOX_NAME: e2e-rlimits-connect
OPENSHELL_GATEWAY: nemoclaw
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ inputs.checkout_repository || github.repository }}
ref: ${{ inputs.checkout_sha || github.sha }}
persist-credentials: false

- *dockerhub-auth

- name: Prepare E2E workspace
uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75

- name: Install OpenShell
env:
NEMOCLAW_NON_INTERACTIVE: "1"
run: |
set -euo pipefail
env -u DOCKER_CONFIG -u DOCKERHUB_USERNAME -u DOCKERHUB_TOKEN -u NVIDIA_API_KEY -u NVIDIA_INFERENCE_API_KEY -u GITHUB_TOKEN bash scripts/install-openshell.sh

- name: Run sandbox rlimit connect live test
env:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
run: |
set -euo pipefail
export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"
if command -v openshell >/dev/null 2>&1; then
OPENSHELL_BIN="$(command -v openshell)"
elif [ -x "$HOME/.local/bin/openshell" ]; then
OPENSHELL_BIN="$HOME/.local/bin/openshell"
else
echo "::error::OpenShell CLI not found after install"
ls -la /usr/local/bin/openshell "$HOME/.local/bin/openshell" 2>&1 || true
exit 1
fi
export OPENSHELL_BIN
"$OPENSHELL_BIN" --version
npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/sandbox-rlimits-connect.test.ts

- name: Upload sandbox rlimit connect artifacts
if: always()
uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57

- name: Clean up Docker auth
if: always()
shell: bash
run: bash .github/scripts/docker-auth-cleanup.sh

overlayfs-autofix:
needs: generate-matrix
if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',overlayfs-autofix,') || contains(format(',{0},', inputs.targets), ',overlayfs-autofix,') }}
Expand Down Expand Up @@ -4613,7 +4549,7 @@ jobs:

sandbox-operations:
needs: generate-matrix
if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',sandbox-operations,') || contains(format(',{0},', inputs.targets), ',sandbox-operations,') }}
if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',sandbox-operations,') || contains(format(',{0},', inputs.jobs), ',sandbox-rlimits-connect,') || contains(format(',{0},', inputs.targets), ',sandbox-operations,') || contains(format(',{0},', inputs.targets), ',sandbox-rlimits-connect,') }}
runs-on: ubuntu-latest
# The live test receives 45 minutes for two onboards plus process/gateway
# recovery. The remaining 15 minutes cover checkout, build, OpenShell setup,
Expand Down Expand Up @@ -5802,7 +5738,6 @@ jobs:
rebuild-openclaw,
rebuild-hermes,
rebuild-hermes-stale-base,
sandbox-rlimits-connect,
overlayfs-autofix,
state-backup-restore,
openshell-gateway-upgrade,
Expand Down
9 changes: 9 additions & 0 deletions test/e2e/fixtures/resource-limit-diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

const SECURITY_RESOURCE_LIMIT_DIAGNOSTIC =
/\[SECURITY\][^\r\n]*(?:resource limits?|nproc|nofile)/iu;

export function containsSecurityResourceLimitDiagnostic(output: string): boolean {
return SECURITY_RESOURCE_LIMIT_DIAGNOSTIC.test(output);
}
67 changes: 67 additions & 0 deletions test/e2e/live/sandbox-operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
type HostedInferenceConfig,
requireHostedInferenceConfig,
} from "../fixtures/hosted-inference.ts";
import { containsSecurityResourceLimitDiagnostic } from "../fixtures/resource-limit-diagnostics.ts";
import type { ShellProbeResult } from "../fixtures/shell-probe.ts";
import { ubuntuRepoDocker } from "../registry/matrix.ts";

Expand All @@ -36,6 +37,65 @@ const REGISTRY_FILE = path.join(process.env.HOME ?? os.homedir(), ".nemoclaw", "
const GATEWAY_CONTAINER = "openshell-cluster-nemoclaw";
const GATEWAY_PORT = process.env.NEMOCLAW_GATEWAY_PORT ?? "8080";

function numericProbe(text: string, key: string): number {
const match = text.match(new RegExp(`${key}=(\\d+)`));
expect(match, `Missing ${key} in connect output:\n${text}`).not.toBeNull();
return Number(match?.[1] ?? "NaN");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

function connectRlimitProbeScript(cliPath: string): string {
const cli = JSON.stringify(cliPath);
const shellProbe = [
"set +e",
'nproc_soft="$(builtin ulimit -Su)"',
'nproc_hard="$(builtin ulimit -Hu)"',
'nofile_soft="$(builtin ulimit -Sn)"',
'nofile_hard="$(builtin ulimit -Hn)"',
"(builtin ulimit -Su 5000) >/dev/null 2>&1",
'raise_nproc="$?"',
"(builtin ulimit -Sn 1048576) >/dev/null 2>&1",
'raise_nofile="$?"',
"set -e",
'printf "nproc_soft=%s\\nnproc_hard=%s\\nnofile_soft=%s\\nnofile_hard=%s\\nraise_nproc=%s\\nraise_nofile=%s\\n" "$nproc_soft" "$nproc_hard" "$nofile_soft" "$nofile_hard" "$raise_nproc" "$raise_nofile"',
].join("; ");
return [
"set -euo pipefail",
`cat <<'NEMOCLAW_CONNECT_RLIMITS' | ${cli} connect`,
"set -euo pipefail",
'printf "__NEMOCLAW_RLIMIT_CONNECT_BEGIN__\\n"',
`bash -lc '${shellProbe}' | sed 's/^/login_/'`,
`bash -ic '${shellProbe}' 2>&1 | sed 's/^/interactive_/'`,
'printf "__NEMOCLAW_RLIMIT_CONNECT_END__\\n"',
"exit",
"NEMOCLAW_CONNECT_RLIMITS",
].join("\n");
}

async function assertConnectResourceLimits(host: HostCliClient): Promise<string> {
const connect = await host.command("bash", ["-lc", connectRlimitProbeScript(host.commandPath)], {
artifactName: "tc-sbx-13-connect-rlimits",
env: buildAvailabilityProbeEnv(),
timeoutMs: 3 * 60_000,
});
const output = resultText(connect);
expectExitZero(connect, "nemoclaw connect resource-limit probe");
expect(output).toContain("__NEMOCLAW_RLIMIT_CONNECT_BEGIN__");
expect(output).toContain("__NEMOCLAW_RLIMIT_CONNECT_END__");
expect(
containsSecurityResourceLimitDiagnostic(output),
"connect shell startup must not print resource-limit security diagnostics",
).toBe(false);
for (const shell of ["login", "interactive"]) {
expect(numericProbe(output, `${shell}_nproc_soft`)).toBeLessThanOrEqual(4096);
expect(numericProbe(output, `${shell}_nproc_hard`)).toBeLessThanOrEqual(4096);
expect(numericProbe(output, `${shell}_nofile_soft`)).toBeLessThanOrEqual(65536);
expect(numericProbe(output, `${shell}_nofile_hard`)).toBeLessThanOrEqual(65536);
expect(numericProbe(output, `${shell}_raise_nproc`)).not.toBe(0);
expect(numericProbe(output, `${shell}_raise_nofile`)).not.toBe(0);
}
return output;
}

async function onboardSandbox(
host: HostCliClient,
cleanup: CleanupRegistry,
Expand Down Expand Up @@ -627,6 +687,7 @@ test(
e2ePhases: [
"confirm Docker and clear the sandbox operation fixtures",
"onboard the primary sandbox",
"validate connected shell resource limits",
"exercise primary CLI inference and logs",
"exercise terminal registry and process recovery",
"onboard the secondary sandbox",
Expand Down Expand Up @@ -657,6 +718,7 @@ test(
"TC-SBX-10 two sandboxes list with model/provider metadata",
"TC-SBX-11 sandboxes cannot reach each other by hostname",
"TC-SBX-12 destroying the non-final sandbox preserves the survivor and final destroy releases the gateway port through the macOS default or explicit non-macOS cleanup",
"TC-SBX-13 bare connect routes to the default sandbox and enforces login and interactive shell resource limits without startup diagnostics (#2173)",
],
});

Expand All @@ -673,6 +735,10 @@ test(
progress.phase("onboard the primary sandbox");
await onboardSandbox(host, cleanup, SANDBOX_A, "onboard-sandbox-a", hosted);

progress.phase("validate connected shell resource limits");
const connectRlimitOutput = await assertConnectResourceLimits(host);
await artifacts.writeText("connect-rlimits-output.txt", connectRlimitOutput);

progress.phase("exercise primary CLI inference and logs");
await expectListed(host, SANDBOX_A, "tc-sbx-01-list-sandbox-a");
await assertAgentCanAnswer(host, SANDBOX_A);
Expand Down Expand Up @@ -717,6 +783,7 @@ test(
finalDestroyCleanupMode,
finalGatewayPortReleased: true,
gatewayRecovery,
connectRlimitsValidated: true,
legacySource: "test/e2e/test-sandbox-operations.sh",
});
},
Expand Down
141 changes: 0 additions & 141 deletions test/e2e/live/sandbox-rlimits-connect.test.ts

This file was deleted.

12 changes: 1 addition & 11 deletions test/e2e/mock-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@
},
{
"live": "test/e2e/live/sandbox-operations.test.ts",
"liveOnlyReason": "The final-destroy gateway release contract needs two real Docker/OpenShell sandboxes plus host 127.0.0.1 port binding; a fast mock cannot prove the shared-gateway ownership boundary."
"liveOnlyReason": "The resource-limit and final-destroy contracts require real Docker and OpenShell sandboxes, the repository CLI, and host port binding; fast mocks cannot verify these runtime boundaries."
},
{
"live": "test/e2e/live/onboard-repair.test.ts",
Expand Down Expand Up @@ -140,16 +140,6 @@
"test/e2e/support/e2e-clients.test.ts"
]
},
{
"live": "test/e2e/live/sandbox-rlimits-connect.test.ts",
"fast": [
"test/cli/dispatch-basics.test.ts",
"test/e2e/support/e2e-workflow.test.ts",
"test/sandbox-rlimit-hooks.test.ts",
"test/e2e/support/e2e-cleanup-resources.test.ts",
"test/e2e/support/e2e-clients.test.ts"
]
},
{
"live": "test/e2e/live/snapshot-commands.test.ts",
"fast": [
Expand Down
Loading
Loading