Skip to content
Closed
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
84 changes: 29 additions & 55 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1782,16 +1782,13 @@ jobs:
source_commit: ${{ steps.resolve_openshell_dev_artifact.outputs.source_commit }}
manifest_sha256: ${{ steps.resolve_openshell_dev_artifact.outputs.manifest_sha256 }}
steps:
- name: Checkout trusted OpenShell dev tooling
- name: Checkout trusted OpenShell dev artifact resolver
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ github.repository }}
ref: ${{ inputs.workflow_sha || github.workflow_sha }}
path: .trusted-openshell-dev-artifact
persist-credentials: false
sparse-checkout: |
scripts/install-openshell.sh
tools/e2e/openshell-dev-artifact.mts

- name: Set up Node for OpenShell dev artifact resolution
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
Expand Down Expand Up @@ -1839,15 +1836,14 @@ jobs:
ref: ${{ inputs.checkout_sha || github.sha }}
persist-credentials: false

- name: Checkout trusted OpenShell dev tooling
- name: Checkout trusted OpenShell dev artifact tooling
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ github.repository }}
ref: ${{ inputs.workflow_sha || github.workflow_sha }}
path: .trusted-openshell-dev-artifact
persist-credentials: false
sparse-checkout: |
scripts/install-openshell.sh
tools/e2e/openshell-dev-artifact.mts

- *dockerhub-auth
Expand All @@ -1857,11 +1853,6 @@ jobs:
with:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

- name: Restore immutable OpenShell dev artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
Expand All @@ -1881,6 +1872,32 @@ jobs:
"$OPENSHELL_DEV_EXPECTED_SOURCE_COMMIT"
"$OPENSHELL_DEV_EXPECTED_MANIFEST_SHA256"

- name: Revoke Docker auth before OpenShell development tooling
uses: NVIDIA/NemoClaw/.github/actions/docker-auth-cleanup@d5f37099766ca82a4516e7d8f0de117cda197fe3

- name: Install immutable OpenShell dev artifact
env:
OPENSHELL_DEV_ARTIFACT_DIR: ${{ runner.temp }}/openshell-dev-artifact
OPENSHELL_DEV_BINARY_DIR: ${{ runner.temp }}/openshell-dev-binaries
OPENSHELL_DEV_EXPECTED_MANIFEST_SHA256: ${{ needs.openshell-dev-artifact.outputs.manifest_sha256 }}
OPENSHELL_DEV_EXPECTED_SOURCE_COMMIT: ${{ needs.openshell-dev-artifact.outputs.source_commit }}
run: |
set -euo pipefail
node --experimental-strip-types --no-warnings \
"${{ github.workspace }}/.trusted-openshell-dev-artifact/tools/e2e/openshell-dev-artifact.mts" prepare \
"$OPENSHELL_DEV_ARTIFACT_DIR" \
"$OPENSHELL_DEV_BINARY_DIR" \
"$OPENSHELL_DEV_EXPECTED_SOURCE_COMMIT" \
"$OPENSHELL_DEV_EXPECTED_MANIFEST_SHA256"
sudo install -m 755 "$OPENSHELL_DEV_BINARY_DIR/openshell" /usr/local/bin/openshell
sudo install -m 755 "$OPENSHELL_DEV_BINARY_DIR/openshell-gateway" /usr/local/bin/openshell-gateway
sudo install -m 755 "$OPENSHELL_DEV_BINARY_DIR/openshell-sandbox" /usr/local/bin/openshell-sandbox

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

- name: Install and verify cloudflared prerequisite
# Update posture: keep this dev compatibility lane on the same reviewed
# version/SHA256 pair as the stable lane; workflow-contract tests fail
Expand All @@ -1906,48 +1923,6 @@ jobs:
- name: Generate MCP test TLS
run: bash test/e2e/setup-mcp-test-tls.sh

- name: Revoke Docker auth before OpenShell development tooling
shell: bash
run: bash .github/scripts/docker-auth-cleanup.sh

- name: Install immutable OpenShell dev artifact
env:
NEMOCLAW_ACCEPT_DEV_UNVERIFIED_INSTALL: "1"
NEMOCLAW_OPENSHELL_FORCE_INSTALL: "1"
OPENSHELL_DEV_ASSET_DIR: ${{ runner.temp }}/openshell-dev-artifact/assets
run: |
set -euo pipefail
shim_dir="$(mktemp -d)"
trap 'rm -rf "$shim_dir"' EXIT
cat >"$shim_dir/gh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
if [[ "$#" -ne 10 || "$1" != "release" || "$2" != "download" || "$3" != "dev" || "$4" != "--repo" || "$5" != "NVIDIA/OpenShell" || "$6" != "--pattern" || "$8" != "--dir" || "${10}" != "--clobber" ]]; then
printf 'Unsupported gh invocation for retained OpenShell assets.\n' >&2
exit 64
fi
asset="$7"
destination="$9"
case "$asset" in
openshell-x86_64-unknown-linux-musl.tar.gz | openshell-checksums-sha256.txt | openshell-gateway-x86_64-unknown-linux-gnu.tar.gz | openshell-gateway-checksums-sha256.txt | openshell-sandbox-x86_64-unknown-linux-gnu.tar.gz | openshell-sandbox-checksums-sha256.txt) ;;
*)
printf 'Unsupported retained OpenShell asset: %s\n' "$asset" >&2
exit 64
;;
esac
source_asset="${OPENSHELL_DEV_ASSET_DIR}/${asset}"
[[ -f "$source_asset" && ! -L "$source_asset" && "$destination" = /* && -d "$destination" && ! -L "$destination" ]]
cp -- "$source_asset" "$destination/$asset"
EOF
cat >"$shim_dir/curl" <<'EOF'
#!/usr/bin/env bash
printf 'Network fallback is disabled for retained OpenShell assets.\n' >&2
exit 1
EOF
chmod 700 "$shim_dir/gh" "$shim_dir/curl"
PATH="$shim_dir:$PATH" \
bash "${{ github.workspace }}/.trusted-openshell-dev-artifact/scripts/install-openshell.sh"

- id: mcp_runtime_compatibility
name: Classify OpenShell credential-boundary compatibility
run: |
Expand Down Expand Up @@ -1988,8 +1963,7 @@ jobs:

- name: Clean up Docker auth
if: always()
shell: bash
run: bash .github/scripts/docker-auth-cleanup.sh
uses: NVIDIA/NemoClaw/.github/actions/docker-auth-cleanup@d5f37099766ca82a4516e7d8f0de117cda197fe3


# Manual PR qualification also requires the exact candidate activation contract.
Expand Down
1 change: 0 additions & 1 deletion scripts/checks/vitest-project-overlap.mts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ const INSTALLER_INTEGRATION_TESTS = new Set([
"test/install-forward-restore-diagnostics.test.ts",
"test/install-hermes-forward-restore.test.ts",
"test/install-managed-cli-reuse.test.ts",
"test/install-openshell-e2e-artifact.test.ts",
"test/install-openshell-version-pin.test.ts",
"test/install-openshell-version-check.test.ts",
"test/install-preflight-docker-bootstrap.test.ts",
Expand Down
8 changes: 2 additions & 6 deletions test/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,12 +468,8 @@ Linux x64 archive and checksum file. It rejects release drift during download,
then uploads the verified bytes under a content-addressed name with the shared
14-day E2E retention policy.

The OpenClaw, Hermes, and LangChain Deep Agents Code shards restore and verify
that same artifact with the trusted workflow revision. An exact-argument and
asset-allowlisted `gh` shim presents only those retained files to the unchanged
trusted `scripts/install-openshell.sh` path. A separate `curl` shim blocks
network fallback. The installer still checks the release checksums and archive
structure before installation. A missing, replaced, or corrupt upstream asset
The OpenClaw, Hermes, and Deep Agents Code shards restore and verify that same
artifact before installation. A missing, replaced, or corrupt upstream asset
fails the resolver as an infrastructure failure. The job error reports the
failed identifier and source URL, and `resolution.json` records them when the
artifact directory remains writable. The three product shards do not start in
Expand Down
19 changes: 18 additions & 1 deletion test/e2e/support/dockerhub-auth-workflow-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,20 @@ describe("shared Docker Hub authentication workflow boundary (#6961)", () => {
testTimeout(15_000),
);

it("rejects an unpinned final cleanup action in the OpenShell dev job (#9051)", () => {
expect(validateE2eWorkflowBoundary()).toEqual([]);

const errors = validateMutation((workflow) => {
const cleanup = namedStep(workflow.jobs["mcp-bridge-dev"], CLEANUP_STEP_NAME);
expect(cleanup).toBeDefined();
cleanup!.uses = "NVIDIA/NemoClaw/.github/actions/docker-auth-cleanup@main";
});

expect(errors).toContain(
"mcp-bridge-dev Docker Hub cleanup step must use the pinned cleanup action",
);
});

it("rejects missing auth and cleanup coverage for every classified image job", () => {
const workflow = loadWorkflow();
const requiredJobs = imageJobNames(workflow);
Expand Down Expand Up @@ -408,14 +422,17 @@ describe("shared Docker Hub authentication workflow boundary (#6961)", () => {
}
});

for (const jobName of requiredJobs) {
for (const jobName of requiredJobs.filter((name) => name !== "mcp-bridge-dev")) {
expect(errors).toContain(
`${jobName} Docker Hub cleanup step must contain exactly name, if, shell, and run`,
);
expect(errors).toContain(
`${jobName} Docker Hub cleanup step must run only ${CLEANUP_HELPER_RUN}`,
);
}
expect(errors).toContain(
"mcp-bridge-dev Docker Hub cleanup step must contain exactly name, if, and uses",
);
});

it("treats every new E2E job as image-consuming unless it is explicitly exempt", () => {
Expand Down
138 changes: 128 additions & 10 deletions test/e2e/support/mcp-workflow-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,30 +308,26 @@ describe("MCP workflow artifact boundary", () => {
const restore = dev.steps.find(
(step) => step.name === "Restore immutable OpenShell dev artifact",
);
const verify = dev.steps.find(
(step) => step.name === "Verify immutable OpenShell dev artifact",
);
const install = dev.steps.find(
(step) => step.name === "Install immutable OpenShell dev artifact",
);
requireFixture(restore?.with, "OpenShell dev artifact restore fixture is missing");
requireFixture(verify?.run, "OpenShell dev artifact verification fixture is missing");
requireFixture(install?.run, "OpenShell dev artifact installation fixture is missing");
restore.uses = "actions/download-artifact@main";
restore.with.name = "openshell-dev-latest";
verify.run = verify.run.replace(".trusted-openshell-dev-artifact/", "");
install.env = { NEMOCLAW_ACCEPT_DEV_UNVERIFIED_INSTALL: "1" };
install.run = "bash scripts/install-openshell.sh";
install.run = install.run.replace(" prepare ", " resolve ");
install.run += "\nbash scripts/install-openshell.sh\n";
fs.writeFileSync(workflowPath, YAML.stringify(workflow));

expect(validateMcpOpenShellWorkflowBoundary(workflowPath)).toEqual(
expect.arrayContaining([
"mcp-bridge-dev must depend on its reviewed artifact producers",
"mcp-bridge-dev must use the reviewed immutable artifact downloader",
"mcp-bridge-dev must restore exactly the resolver's content-addressed artifact",
"mcp-bridge-dev must verify the immutable OpenShell artifact before installation",
"mcp-bridge-dev installer must receive only the retained OpenShell asset directory",
"mcp-bridge-dev must install retained assets through the trusted no-network release path",
"mcp-bridge-dev artifact installation must receive only its reviewed artifact identity",
"mcp-bridge-dev must install only the verified same-run binaries",
"mcp-bridge-dev must not modify or invoke the base-trusted release installer",
]),
);
} finally {
Expand All @@ -344,7 +340,7 @@ describe("MCP workflow artifact boundary", () => {
name: "candidate checkout ref",
mutate: (job: { steps: Array<Record<string, unknown>> }) => {
const checkout = job.steps.find(
(step) => step.name === "Checkout trusted OpenShell dev tooling",
(step) => step.name === "Checkout trusted OpenShell dev artifact resolver",
);
requireFixture(
checkout?.with,
Expand Down Expand Up @@ -391,6 +387,128 @@ describe("MCP workflow artifact boundary", () => {
}
});

it.each([
{
name: "candidate revision",
mutate: (job: { steps: Array<Record<string, unknown>> }) => {
const checkout = job.steps.find(
(step) => step.name === "Checkout trusted OpenShell dev artifact tooling",
);
requireFixture(checkout?.with, "trusted OpenShell dev tooling checkout is missing");
const withValues = checkout.with as Record<string, unknown>;
withValues.ref = "${{ inputs.checkout_sha || github.sha }}";
},
expected: "mcp-bridge-dev must check out only the trusted OpenShell dev artifact tooling",
},
{
name: "candidate verifier invocation",
mutate: (job: { steps: Array<Record<string, unknown>> }) => {
const verify = job.steps.find(
(step) => step.name === "Verify immutable OpenShell dev artifact",
);
requireFixture(
typeof verify?.run === "string",
"trusted OpenShell dev artifact verification is missing",
);
verify.run = verify.run.replace(
"${{ github.workspace }}/.trusted-openshell-dev-artifact/",
"${{ github.workspace }}/.candidate-runtime/",
);
},
expected: "mcp-bridge-dev must verify the immutable OpenShell artifact before installation",
},
{
name: "candidate preparer invocation",
mutate: (job: { steps: Array<Record<string, unknown>> }) => {
const install = job.steps.find(
(step) => step.name === "Install immutable OpenShell dev artifact",
);
requireFixture(
typeof install?.run === "string",
"trusted OpenShell dev artifact preparation is missing",
);
install.run = install.run.replace(
"${{ github.workspace }}/.trusted-openshell-dev-artifact/",
"${{ github.workspace }}/.candidate-runtime/",
);
},
expected: "mcp-bridge-dev must install only the verified same-run binaries",
},
])("rejects a $name for OpenShell dev artifact consumption (#9051)", ({ expected, mutate }) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-mcp-workflow-"));
const workflowPath = path.join(directory, "e2e.yaml");
try {
const workflow = YAML.parse(fs.readFileSync(".github/workflows/e2e.yaml", "utf8")) as {
jobs: Record<string, { steps: Array<Record<string, unknown>> }>;
};
mutate(workflow.jobs["mcp-bridge-dev"]);
fs.writeFileSync(workflowPath, YAML.stringify(workflow));

expect(validateMcpOpenShellWorkflowBoundary(workflowPath)).toContain(expected);
} finally {
fs.rmSync(directory, { force: true, recursive: true });
}
});

it.each(["Restore exact-commit CLI artifact", "Generate MCP test TLS"])(
"rejects %s before trusted OpenShell installation (#9051)",
(candidateStepName) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-mcp-workflow-"));
const workflowPath = path.join(directory, "e2e.yaml");
try {
const workflow = YAML.parse(fs.readFileSync(".github/workflows/e2e.yaml", "utf8")) as {
jobs: Record<string, { steps: Array<Record<string, unknown>> }>;
};
const steps = workflow.jobs["mcp-bridge-dev"].steps;
const candidateStepIndex = steps.findIndex((step) => step.name === candidateStepName);
requireFixture(candidateStepIndex >= 0, `${candidateStepName} fixture is missing`);
const [candidateStep] = steps.splice(candidateStepIndex, 1);
const installIndex = steps.findIndex(
(step) => step.name === "Install immutable OpenShell dev artifact",
);
requireFixture(candidateStep, `${candidateStepName} fixture is missing`);
requireFixture(installIndex >= 0, "trusted OpenShell installation fixture is missing");
steps.splice(installIndex, 0, candidateStep);
fs.writeFileSync(workflowPath, YAML.stringify(workflow));

expect(validateMcpOpenShellWorkflowBoundary(workflowPath)).toContain(
"mcp-bridge-dev must verify and install OpenShell before candidate code runs",
);
} finally {
fs.rmSync(directory, { force: true, recursive: true });
}
},
);

it.each([
{
stepName: "Revoke Docker auth before OpenShell development tooling",
expected: "mcp-bridge-dev must revoke Docker auth before OpenShell development tooling",
},
{
stepName: "Clean up Docker auth",
expected: "mcp-bridge-dev must use the canonical unconditional Docker auth cleanup",
},
])("rejects an unpinned cleanup action in $stepName (#9051)", ({ expected, stepName }) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-mcp-workflow-"));
const workflowPath = path.join(directory, "e2e.yaml");
try {
const workflow = YAML.parse(fs.readFileSync(".github/workflows/e2e.yaml", "utf8")) as {
jobs: Record<string, { steps: Array<Record<string, unknown>> }>;
};
const step = workflow.jobs["mcp-bridge-dev"].steps.find(
(candidate) => candidate.name === stepName,
);
requireFixture(step, `${stepName} fixture is missing`);
step.uses = "NVIDIA/NemoClaw/.github/actions/docker-auth-cleanup@main";
fs.writeFileSync(workflowPath, YAML.stringify(workflow));

expect(validateMcpOpenShellWorkflowBoundary(workflowPath)).toContain(expected);
} finally {
fs.rmSync(directory, { force: true, recursive: true });
}
});

it("rejects any additional artifact upload outside the scanned directory", () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-mcp-workflow-"));
const workflowPath = path.join(directory, "e2e.yaml");
Expand Down
Loading
Loading