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
107 changes: 105 additions & 2 deletions .github/workflows/e2e-vitest-scenarios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
default: ""
type: string
jobs:
description: "Optional comma-separated free-standing live Vitest job ids. Empty runs default-enabled jobs only when scenarios is also empty; explicit-only jobs such as jetson-nvmap-gpu-vitest are skipped unless selected."
description: "Optional comma-separated free-standing live Vitest job ids. Empty runs default-enabled jobs only when scenarios is also empty; explicit-only jobs such as jetson-nvmap-gpu-vitest and sandbox-rlimits-connect-vitest are skipped unless selected."
required: false
default: ""
type: string
Expand Down Expand Up @@ -2399,6 +2399,103 @@ jobs:
if-no-files-found: ignore
retention-days: 14

sandbox-rlimits-connect-vitest:
needs: generate-matrix
if: ${{ contains(format(',{0},', inputs.jobs), ',sandbox-rlimits-connect-vitest,') || contains(format(',{0},', inputs.scenarios), ',sandbox-rlimits-connect,') }}
runs-on: ubuntu-latest
timeout-minutes: 60
env:
FREE_STANDING_VITEST_JOB: "1"
FREE_STANDING_SCENARIO_ID: "sandbox-rlimits-connect"
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/sandbox-rlimits-connect
NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js
NEMOCLAW_RUN_E2E_SCENARIOS: "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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Authenticate to Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${DOCKERHUB_USERNAME}" || -z "${DOCKERHUB_TOKEN}" ]]; then
echo "::notice::Docker Hub credentials not configured; continuing with anonymous pulls."
exit 0
fi
login_succeeded=0
for attempt in 1 2 3; do
if echo "${DOCKERHUB_TOKEN}" | timeout 30s docker login docker.io --username "${DOCKERHUB_USERNAME}" --password-stdin; then
login_succeeded=1
break
fi
if [[ "$attempt" -lt 3 ]]; then
echo "::warning::Docker Hub login attempt ${attempt} failed; retrying."
sleep 5
fi
done
if [[ "$login_succeeded" -ne 1 ]]; then
echo "::warning::Docker Hub login failed after 3 attempts; continuing with anonymous pulls."
fi

- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.0.0
with:
node-version: 22
cache: npm

- name: Install root dependencies
run: npm ci --ignore-scripts

- name: Build CLI
run: npm run build:cli

- 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 vitest run --project e2e-scenarios-live \
test/e2e-scenario/live/sandbox-rlimits-connect.test.ts \
--silent=false --reporter=default

- name: Upload sandbox rlimit connect artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-vitest-scenarios-sandbox-rlimits-connect
path: e2e-artifacts/vitest/sandbox-rlimits-connect/
include-hidden-files: false
if-no-files-found: ignore
retention-days: 14

overlayfs-autofix-vitest:
needs: generate-matrix
if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',overlayfs-autofix-vitest,') || contains(format(',{0},', inputs.scenarios), ',overlayfs-autofix,') }}
Expand Down Expand Up @@ -5518,6 +5615,7 @@ jobs:
rebuild-hermes-vitest,
rebuild-hermes-stale-base-vitest,
sandbox-rebuild-vitest,
sandbox-rlimits-connect-vitest,
overlayfs-autofix-vitest,
state-backup-restore-vitest,
upgrade-stale-sandbox-vitest,
Expand Down Expand Up @@ -5584,6 +5682,11 @@ jobs:
scenario: 'jetson-nvmap-gpu',
reason: 'default dispatch excludes Jetson until a stable Jetson runner is available',
},
{
job: 'sandbox-rlimits-connect-vitest',
scenario: 'sandbox-rlimits-connect',
reason: 'default dispatch excludes the destructive rlimit fork/connect probe unless selected',
},
];
const scenariosRejected = rawRequestedScenarios && !selectorValidationPassed;
const jobsRejected = rawRequestedJobs && !selectorValidationPassed;
Expand Down Expand Up @@ -5669,7 +5772,7 @@ jobs:
? '**Requested jobs:** _(selector rejected by workflow validation)_'
: requestedJobs
? `**Requested jobs:** \`${requestedJobs}\``
: '**Requested jobs:** _(default — all default-enabled free-standing jobs; explicit-only jobs such as `jetson-nvmap-gpu-vitest` are skipped unless selected)_',
: '**Requested jobs:** _(default — all default-enabled free-standing jobs; explicit-only jobs such as `jetson-nvmap-gpu-vitest` and `sandbox-rlimits-connect-vitest` are skipped unless selected)_',
`**Summary:** ${passed.length} passed, ${failed.length} failed, ${cancelled.length} cancelled, ${skipped.length} skipped`,
'',
'| Job | Result |',
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,7 @@ RUN chmod 444 /usr/local/lib/nemoclaw/sandbox-rlimits.sh \
&& if ! grep -q "sandbox-rlimits.sh" /etc/profile.d/nemoclaw-rlimits.sh 2>/dev/null; then \
printf '%s\n' \
'# NemoClaw sandbox resource limits — see sandbox-rlimits.sh (#2173)' \
'[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits' \
'[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits --quiet || true' \
> /etc/profile.d/nemoclaw-rlimits.sh \
&& chmod 444 /etc/profile.d/nemoclaw-rlimits.sh; \
fi \
Expand All @@ -969,7 +969,7 @@ RUN chmod 444 /usr/local/lib/nemoclaw/sandbox-rlimits.sh \
'[ -f /tmp/nemoclaw-proxy-env.sh ] && . /tmp/nemoclaw-proxy-env.sh' \
'' \
'# NemoClaw sandbox resource limits — see sandbox-rlimits.sh (#2173)' \
'[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits' \
'[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits --quiet || true' \
''; \
grep -Ev 'NemoClaw runtime proxy config|nemoclaw-proxy-env[.]sh|NemoClaw sandbox resource limits|sandbox-rlimits[.]sh' /etc/bash.bashrc || true; \
} > /etc/bash.bashrc.new \
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ RUN printf '%s\n' \
RUN chmod 444 /usr/local/lib/nemoclaw/sandbox-rlimits.sh \
&& printf '%s\n' \
'# NemoClaw sandbox resource limits — see sandbox-rlimits.sh (#2173)' \
'[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits' \
'[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits --quiet || true' \
> /etc/profile.d/nemoclaw-rlimits.sh \
&& chmod 444 /etc/profile.d/nemoclaw-rlimits.sh \
&& printf '%s\n' \
Expand All @@ -179,7 +179,7 @@ RUN chmod 444 /usr/local/lib/nemoclaw/sandbox-rlimits.sh \
'[ -f /tmp/nemoclaw-proxy-env.sh ] && . /tmp/nemoclaw-proxy-env.sh' \
'' \
'# NemoClaw sandbox resource limits — see sandbox-rlimits.sh (#2173)' \
'[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits' \
'[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits --quiet || true' \
''; \
cat /etc/bash.bashrc; \
} > /etc/bash.bashrc.new \
Expand Down
4 changes: 2 additions & 2 deletions agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,13 @@ RUN chmod 755 /usr/local/bin/nemoclaw-start /usr/local/lib/nemoclaw/sandbox-init
&& mkdir -p /etc/profile.d \
&& printf '%s\n' \
'# NemoClaw sandbox resource limits — see sandbox-rlimits.sh (#2173)' \
'[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits' \
'[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits --quiet || true' \
> /etc/profile.d/nemoclaw-rlimits.sh \
&& chmod 444 /etc/profile.d/nemoclaw-rlimits.sh \
&& (chmod 644 /etc/bash.bashrc 2>/dev/null || true) \
&& { printf '%s\n' \
'# NemoClaw sandbox resource limits — see sandbox-rlimits.sh (#2173)' \
'[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits' \
'[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits --quiet || true' \
''; \
if [ -f /etc/bash.bashrc ]; then \
grep -Ev 'NemoClaw sandbox resource limits|sandbox-rlimits[.]sh' /etc/bash.bashrc || true; \
Expand Down
5 changes: 4 additions & 1 deletion src/lib/onboard/messaging-channel-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ const getMessagingInputValue = (input: ChannelInputSpec): string | null => {
*/
export function detectMessagingChannelsFromEnv(agent: AgentDefinition | null = null): string[] {
const manifestRegistry = createBuiltInChannelManifestRegistry();
const availabilityContext = getMessagingManifestAvailabilityContext(agent);
const availabilityContext = getMessagingManifestAvailabilityContext(
agent,
manifestRegistry.list(),
);
const availableChannels = manifestRegistry.listAvailable(availabilityContext);
return availableChannels
.filter((manifest) => hasMessagingManifestRequiredInputs(manifest, getMessagingInputValue))
Expand Down
8 changes: 8 additions & 0 deletions test/e2e-scenario/live/sandbox-rlimits-connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ function numericProbe(text: string, key: string): number {
return Number(match?.[1] ?? "NaN");
}

function expectNoRlimitStartupDiagnostics(text: string): void {
expect(text, "connect shell startup must not print rlimit security diagnostics").not.toMatch(
/\[SECURITY\].*(?:Effective|Could not set).*(?:nproc|nofile).*limit/iu,
);
}

function connectAcceptanceScript(cliPath: string, sandboxName: string): string {
const cli = JSON.stringify(cliPath);
const sandbox = JSON.stringify(sandboxName);
Expand Down Expand Up @@ -66,6 +72,7 @@ runConnectRlimitTest(
"bash -lc 'ulimit -u; ulimit -n'",
"bash -ic 'ulimit -u; ulimit -n'",
"ulimit -a",
"shell startup does not emit [SECURITY] rlimit diagnostics before user commands",
"for i in $(seq 1 5000); do sleep 60 & done 2>&1 | tail -5",
],
hermesCoverage:
Expand Down Expand Up @@ -127,6 +134,7 @@ runConnectRlimitTest(
expect(connect.exitCode, output).toBe(0);
expect(output).toContain("__NEMOCLAW_RLIMIT_CONNECT_BEGIN__");
expect(output).toContain("__NEMOCLAW_RLIMIT_CONNECT_END__");
expectNoRlimitStartupDiagnostics(output);

expect(numericProbe(output, "login_nproc")).toBeLessThanOrEqual(4096);
expect(numericProbe(output, "login_nofile")).toBeLessThanOrEqual(65536);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";
import {
evaluateE2eVitestWorkflowDispatchSelectors,
readFreeStandingJobsInventory,
} from "../../../tools/e2e-scenarios/workflow-boundary.mts";

describe("rlimit connect workflow boundary", () => {
it("maps the rlimit connect acceptance selector to its explicit Vitest job", () => {
const inventory = readFreeStandingJobsInventory();
expect(inventory.allowedJobs).toContain("sandbox-rlimits-connect-vitest");
expect(inventory.scenarioToJob.get("sandbox-rlimits-connect")).toBe(
"sandbox-rlimits-connect-vitest",
);

expect(
evaluateE2eVitestWorkflowDispatchSelectors({ scenarios: "sandbox-rlimits-connect" }),
).toMatchObject({
valid: true,
liveScenariosRuns: false,
selectedFreeStandingJobs: ["sandbox-rlimits-connect-vitest"],
registryScenarios: [],
});
expect(
evaluateE2eVitestWorkflowDispatchSelectors({ jobs: "sandbox-rlimits-connect-vitest" }),
).toMatchObject({
valid: true,
liveScenariosRuns: false,
selectedFreeStandingJobs: ["sandbox-rlimits-connect-vitest"],
registryScenarios: [],
});
expect(
evaluateE2eVitestWorkflowDispatchSelectors({ jobs: "", scenarios: "" })
.selectedFreeStandingJobs,
).not.toContain("sandbox-rlimits-connect-vitest");
});
});
4 changes: 2 additions & 2 deletions test/sandbox-provisioning.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -902,7 +902,7 @@ describe("sandbox provisioning: unified .openclaw layout (#2227)", () => {
const rlimitLib = path.join(tmp, "sandbox-rlimits.sh");
const bashrc = path.join(tmp, "bash.bashrc");
const runtimeEnvShim = "[ -f /tmp/nemoclaw-proxy-env.sh ] && . /tmp/nemoclaw-proxy-env.sh";
const rlimitShim = `[ -f ${rlimitLib} ] && . ${rlimitLib} && harden_resource_limits --quiet && verify_resource_limits`;
const rlimitShim = `[ -f ${rlimitLib} ] && . ${rlimitLib} && harden_resource_limits --quiet && verify_resource_limits --quiet || true`;

try {
fs.mkdirSync(path.dirname(profileHook), { recursive: true });
Expand Down Expand Up @@ -946,7 +946,7 @@ describe("sandbox provisioning: unified .openclaw layout (#2227)", () => {
const rlimitLib = path.join(tmp, "sandbox-rlimits.sh");
const bashrc = path.join(tmp, "bash.bashrc");
const runtimeEnvShim = "[ -f /tmp/nemoclaw-proxy-env.sh ] && . /tmp/nemoclaw-proxy-env.sh";
const rlimitShim = `[ -f ${rlimitLib} ] && . ${rlimitLib} && harden_resource_limits --quiet && verify_resource_limits`;
const rlimitShim = `[ -f ${rlimitLib} ] && . ${rlimitLib} && harden_resource_limits --quiet && verify_resource_limits --quiet || true`;

try {
fs.mkdirSync(path.dirname(profileHook), { recursive: true });
Expand Down
37 changes: 35 additions & 2 deletions test/sandbox-rlimit-hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function copyRlimitFixtureWithNprocLimit(rlimitLib: string, limit: number): void
}

function rlimitShim(rlimitLib: string): string {
return `[ -f ${rlimitLib} ] && . ${rlimitLib} && harden_resource_limits --quiet && verify_resource_limits`;
return `[ -f ${rlimitLib} ] && . ${rlimitLib} && harden_resource_limits --quiet && verify_resource_limits --quiet || true`;
}

type ProbeValues = Record<string, string | undefined>;
Expand Down Expand Up @@ -167,6 +167,36 @@ function expectSystemRlimitHookBypassesShadowedUlimit(hookPath: string): void {
expect(Number(values.nofile)).toBeLessThanOrEqual(65536);
}

function expectSystemRlimitHookIsSilentWhenVerificationFails(
hookPath: string,
rlimitLib: string,
): void {
fs.chmodSync(rlimitLib, 0o644);
fs.writeFileSync(
rlimitLib,
[
"harden_resource_limits() { :; }",
"verify_resource_limits() {",
' if [ "${1:-}" != "--quiet" ]; then',
' echo "[SECURITY] noisy verification failure" >&2',
" fi",
" return 1",
"}",
].join("\n"),
);
const probe = ["set -euo pipefail", `source ${JSON.stringify(hookPath)}`, 'printf "OK\\n"'].join(
"\n",
);
const result = spawnSync("bash", ["--noprofile", "--norc", "-c", probe], {
encoding: "utf-8",
timeout: 5000,
});

expect(result.status).toBe(0);
expect(result.stdout).toBe("OK\n");
expect(result.stderr).toBe("");
}

function expectRlimitLibIsPosixShSafe(rlimitLib: string): void {
const probe = [
"set -e",
Expand Down Expand Up @@ -388,6 +418,7 @@ describe("sandbox rlimit system hooks (#2173)", () => {
expectSystemRlimitHookEnforcesLimits(rlimitHook);
expectSystemRlimitHookEnforcesLimits(bashrc);
expectSystemRlimitHookBypassesShadowedUlimit(rlimitHook);
expectSystemRlimitHookIsSilentWhenVerificationFails(rlimitHook, rlimitLib);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
Expand Down Expand Up @@ -447,7 +478,7 @@ describe("sandbox rlimit system hooks (#2173)", () => {
"# NemoClaw runtime proxy config — see /tmp/nemoclaw-proxy-env.sh (#2704)",
"[ -f /tmp/nemoclaw-proxy-env.sh ] && . /tmp/nemoclaw-proxy-env.sh",
"# NemoClaw sandbox resource limits — see sandbox-rlimits.sh (#2173)",
"[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits",
"[ -f /usr/local/lib/nemoclaw/sandbox-rlimits.sh ] && . /usr/local/lib/nemoclaw/sandbox-rlimits.sh && harden_resource_limits --quiet && verify_resource_limits --quiet || true",
].join("\n"),
);
const command = dockerRunCommandBetween(
Expand All @@ -467,6 +498,7 @@ describe("sandbox rlimit system hooks (#2173)", () => {
expect(occurrenceCount(bashrcBody, expectedRlimitShim)).toBe(1);
expectSystemRlimitHookEnforcesLimits(rlimitHook);
expectSystemRlimitHookEnforcesLimits(bashrc);
expectSystemRlimitHookIsSilentWhenVerificationFails(bashrc, rlimitLib);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
Expand Down Expand Up @@ -517,6 +549,7 @@ describe("sandbox rlimit system hooks (#2173)", () => {
expect(fs.readFileSync(bashrc, "utf-8")).toContain(expectedRlimitShim);
expectSystemRlimitHookEnforcesLimits(profileHook);
expectSystemRlimitHookEnforcesLimits(bashrc);
expectSystemRlimitHookIsSilentWhenVerificationFails(bashrc, rlimitLib);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
Expand Down
Loading
Loading