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
33 changes: 23 additions & 10 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1952,6 +1952,14 @@ jobs:
NEMOCLAW_OPENSHELL_CHANNEL: dev
NEMOCLAW_RUN_LIVE_E2E: "1"
steps:
# setup-node probes package managers in the workspace.
# Run it before candidate checkout with automatic caching disabled.
- name: Set up Node.js for trusted OpenShell verification
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
package-manager-cache: false

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ inputs.checkout_repository || github.repository }}
Expand All @@ -1960,16 +1968,6 @@ jobs:

- *dockerhub-auth

- name: Prepare E2E workspace
uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75
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: Checkout trusted OpenShell dev tooling
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -2045,6 +2043,21 @@ jobs:
PATH="$shim_dir:$PATH" \
bash "${{ github.workspace }}/.trusted-openshell-dev-artifact/scripts/install-openshell.sh"

# Dependency preparation can read candidate project configuration. Keep
# it after the trusted installer has consumed the verified OpenShell
# assets and Docker credentials have been revoked.
- name: Prepare E2E workspace
uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@f6304bc25fc35bfaa441c8c2fbfee38f72805a75
with:
build-cli: "false"

# The restore action executes the candidate CLI for its final identity
# check. Candidate-controlled state starts with dependency preparation.
- 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 Down
25 changes: 14 additions & 11 deletions test/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,17 +470,20 @@ 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
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
that case, so the run cannot report a product failure before reaching product
assertions.
The OpenClaw, Hermes, and LangChain Deep Agents Code shards restore and verify that same artifact with the trusted workflow revision.
The `actions/setup-node` step selects Node.js 22 and disables automatic package manager caching before candidate checkout.
An exact-argument and asset-allowlisted `gh` shim presents only the 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.
Each product shard revokes Docker credentials, then installs OpenShell before candidate dependency preparation begins.
Dependency preparation can read candidate project configuration and is the first candidate-controlled execution boundary.
The candidate CLI artifact restore runs after dependency preparation.
This ordering protects the bytes consumed by the trusted installer before candidate-controlled execution starts.
It does not make the installed OpenShell files immutable after dependency preparation starts on the same runner.
Subsequent product steps operate in candidate-controlled state.
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 that case, so the run cannot report a product failure before reaching product assertions.

## Larger-runner routing

Expand Down
154 changes: 154 additions & 0 deletions test/e2e/support/cli-artifact-workflow-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,160 @@ describe("exact-commit CLI artifact workflow boundary", () => {
expect(validateCliArtifactWorkflowBoundary(readWorkflow())).toEqual([]);
});

it("rejects dependency caching before trusted installation (#9051)", () => {
const workflow = workflowFixture();
const setupNode = requireStep(
workflow,
"mcp-bridge-dev",
"Set up Node.js for trusted OpenShell verification",
);
setupNode.with = { ...setupNode.with, "package-manager-cache": true };

expect(validateCliArtifactWorkflowBoundary(workflow)).toContain(
"mcp-bridge-dev must set up Node.js without dependency caching before candidate checkout",
);
});

it("rejects package manager probes after candidate checkout (#9051)", () => {
const workflow = workflowFixture();
const steps = workflow.jobs["mcp-bridge-dev"].steps!;
const setupNode = requireStep(
workflow,
"mcp-bridge-dev",
"Set up Node.js for trusted OpenShell verification",
);
const [movedSetupNode] = steps.splice(steps.indexOf(setupNode), 1);
const candidateCheckoutIndex = steps.findIndex((step) =>
step.uses?.startsWith("actions/checkout@"),
);
steps.splice(candidateCheckoutIndex + 1, 0, movedSetupNode!);

expect(validateCliArtifactWorkflowBoundary(workflow)).toContain(
"mcp-bridge-dev must set up Node.js without dependency caching before candidate checkout",
);
});

it("rejects candidate execution before trusted development installation (#9051)", () => {
const workflow = workflowFixture();
const steps = workflow.jobs["mcp-bridge-dev"].steps!;
const candidateCheckoutIndex = steps.findIndex((step) =>
step.uses?.startsWith("actions/checkout@"),
);
steps.splice(candidateCheckoutIndex + 1, 0, {
name: "Execute candidate CLI before trusted installation",
run: "node bin/nemoclaw.js --version",
});

expect(validateCliArtifactWorkflowBoundary(workflow)).toContain(
"mcp-bridge-dev must preserve every reviewed step through trusted installation",
);
});

it("rejects candidate execution embedded in the trusted development installer (#9051)", () => {
const workflow = workflowFixture();
const install = requireStep(
workflow,
"mcp-bridge-dev",
"Install immutable OpenShell dev artifact",
);
install.run = `bash test/e2e/setup-mcp-test-tls.sh\n${install.run ?? ""}`;

expect(validateCliArtifactWorkflowBoundary(workflow)).toContain(
"mcp-bridge-dev must preserve every reviewed step through trusted installation",
);
});

it("rejects candidate-controlled process hooks in the trusted development job (#9051)", () => {
const workflow = workflowFixture();
const job = workflow.jobs["mcp-bridge-dev"];
expect(job).toBeDefined();
job!.env = {
...job!.env,
NODE_OPTIONS: "--require=./candidate-preload.cjs",
};

expect(validateCliArtifactWorkflowBoundary(workflow)).toContain(
"mcp-bridge-dev must not use candidate-controlled process hooks before trusted installation",
);
});

it("rejects candidate-controlled process hooks in the workflow environment (#9051)", () => {
const workflow = workflowFixture() as Workflow & { env?: Record<string, string> };
workflow.env = {
...workflow.env,
NODE_OPTIONS: "--require=./candidate-preload.cjs",
};

expect(validateCliArtifactWorkflowBoundary(workflow)).toContain(
"workflow must not set process startup hooks before CLI artifact restore",
);
});

it("scans the complete job for process hooks when trusted installation is missing (#9051)", () => {
const workflow = workflowFixture();
const job = workflow.jobs["mcp-bridge-dev"];
const steps = job.steps!;
job.steps = steps.filter(
(step) => step.name !== "Install immutable OpenShell dev artifact",
);
const prepare = requireStep(workflow, "mcp-bridge-dev", "Prepare E2E workspace");
prepare.env = { NODE_OPTIONS: "--require=./candidate-preload.cjs" };

expect(validateCliArtifactWorkflowBoundary(workflow)).toEqual(
expect.arrayContaining([
"mcp-bridge-dev must not use candidate-controlled process hooks before trusted installation",
"mcp-bridge-dev must preserve every reviewed step through trusted installation",
]),
);
});

it("rejects skipping the trusted development installer (#9051)", () => {
const workflow = workflowFixture();
const install = requireStep(
workflow,
"mcp-bridge-dev",
"Install immutable OpenShell dev artifact",
);
install.if = "${{ false }}";

expect(validateCliArtifactWorkflowBoundary(workflow)).toContain(
"mcp-bridge-dev must preserve every reviewed step through trusted installation",
);
});

it("rejects skipping post-install dependency preparation (#9051)", () => {
const workflow = workflowFixture();
const prepare = requireStep(workflow, "mcp-bridge-dev", "Prepare E2E workspace");
prepare.if = "${{ false }}";

expect(validateCliArtifactWorkflowBoundary(workflow)).toContain(
"mcp-bridge-dev must preserve reviewed dependency preparation and candidate CLI restore after trusted installation",
);
});

it("rejects removing post-install dependency preparation and candidate CLI restore (#9051)", () => {
const workflow = workflowFixture();
const steps = workflow.jobs["mcp-bridge-dev"].steps!;
workflow.jobs["mcp-bridge-dev"].steps = steps.filter(
(step) =>
step.name !== "Prepare E2E workspace" &&
step.name !== "Restore exact-commit CLI artifact",
);

expect(validateCliArtifactWorkflowBoundary(workflow)).toContain(
"mcp-bridge-dev must verify and restore the exact CLI artifact exactly once",
);
});

it("rejects removing the trusted development consumer job (#9051)", () => {
const workflow = workflowFixture();
delete workflow.jobs["mcp-bridge-dev"];

expect(validateCliArtifactWorkflowBoundary(workflow)).toContain(
"workflow is missing required CLI artifact consumer mcp-bridge-dev",
);
});

it("reports both an unreadable action and a missing producer", () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "cli-artifact-missing-action-"));
try {
Expand Down
Loading
Loading