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
112 changes: 112 additions & 0 deletions .github/workflows/e2e-vitest-scenarios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4150,6 +4150,117 @@ jobs:
docker logout docker.io || true
rm -rf "${DOCKER_CONFIG}"

sandbox-operations-vitest:
needs: generate-matrix
if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',sandbox-operations-vitest,') || contains(format(',{0},', inputs.scenarios), ',sandbox-operations,') }}
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,
# artifact upload, and unconditional credential/resource cleanup.
timeout-minutes: 60
env:
FREE_STANDING_VITEST_JOB: "1"
FREE_STANDING_SCENARIO_ID: "sandbox-operations"
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/sandbox-operations
NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js
NEMOCLAW_RUN_E2E_SCENARIOS: "1"
NEMOCLAW_E2E_USE_HOSTED_INFERENCE: "1"
NEMOCLAW_NON_INTERACTIVE: "1"
NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1"
# Open permits the scenario's inference/log probes; TC-SBX-11 separately
# proves that sandbox-to-sandbox network isolation remains enforced.
NEMOCLAW_POLICY_TIER: "open"
OPENSHELL_GATEWAY: "nemoclaw"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- 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: Verify CLI launcher
run: |
test -x "${NEMOCLAW_CLI_BIN}"
"${NEMOCLAW_CLI_BIN}" --version

- name: Install OpenShell CLI
run: |
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: Configure isolated Docker auth directory
run: echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config-sandbox-operations" >> "$GITHUB_ENV"

- 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
mkdir -p "${DOCKER_CONFIG}"
chmod 700 "${DOCKER_CONFIG}"
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: Run sandbox operations live test
env:
NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }}
run: |
set -euo pipefail
export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"
npx vitest run --project e2e-scenarios-live \
test/e2e-scenario/live/sandbox-operations.test.ts \
--silent=false --reporter=default

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

- name: Clean up Docker auth
if: always()
run: |
set -euo pipefail
docker logout docker.io || true
rm -rf "${DOCKER_CONFIG}"

sandbox-survival-vitest:
needs: generate-matrix
if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',sandbox-survival-vitest,') || contains(format(',{0},', inputs.scenarios), ',sandbox-survival,') }}
Expand Down Expand Up @@ -5785,6 +5896,7 @@ jobs:
issue-4462-scope-upgrade-approval-vitest,
onboard-resume-vitest,
model-router-provider-routed-inference-vitest,
sandbox-operations-vitest,
sandbox-survival-vitest,
diagnostics-vitest,
snapshot-commands-vitest,
Expand Down
31 changes: 30 additions & 1 deletion test/e2e-scenario/fixtures/clients/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@ import { trustedShellCommand } from "../shell-probe.ts";
import {
artifactLabel,
assertExitZero,
type CommandRunner,
outputContainsSandbox,
resultText,
type CommandRunner,
} from "./command.ts";

export interface HostClientOptions {
cliPath?: string;
cwd?: string;
}

const GATEWAY_ALREADY_ABSENT =
/gateway[^\n]*(?:does not exist|not found)|No (?:active )?gateway|No gateway metadata found/i;
const GATEWAY_REMOVE_UNSUPPORTED =
/unrecognized subcommand ['"]remove['"]|unknown command ['"]remove['"]/i;

export class HostCliClient {
private readonly runner: CommandRunner;
private readonly cliPath: string;
Expand Down Expand Up @@ -122,6 +127,30 @@ export class HostCliClient {
assertExitZero(result, `cleanup destroy sandbox ${sandboxName}`);
}

async cleanupGatewayRegistration(
gatewayName: string,
options: ShellProbeRunOptions = {},
): Promise<void> {
const artifactName = options.artifactName ?? `cleanup-gateway-${artifactLabel(gatewayName)}`;
const remove = await this.command("openshell", ["gateway", "remove", gatewayName], {
...options,
artifactName: `${artifactName}-remove`,
});
if (remove.exitCode === 0 || GATEWAY_ALREADY_ABSENT.test(resultText(remove))) return;
if (!GATEWAY_REMOVE_UNSUPPORTED.test(resultText(remove))) {
assertExitZero(remove, `cleanup gateway registration ${gatewayName}`);
}

// Remove this fallback once the supported OpenShell floor no longer
// includes builds whose local-registration verb was `gateway destroy`.
const destroy = await this.command("openshell", ["gateway", "destroy", "-g", gatewayName], {
...options,
artifactName: `${artifactName}-legacy-destroy`,
});
if (destroy.exitCode === 0 || GATEWAY_ALREADY_ABSENT.test(resultText(destroy))) return;
assertExitZero(destroy, `cleanup gateway registration ${gatewayName}`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

async bestEffortCleanupSandbox(
sandboxName: string,
options: ShellProbeRunOptions = {},
Expand Down
Loading
Loading