Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
119 changes: 112 additions & 7 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1160,13 +1160,6 @@ jobs:
export OPENSHELL_BIN
"$OPENSHELL_BIN" --version
npx tsx tools/e2e/live-vitest-invocation.mts run --test-path test/e2e/live/mcp-bridge.test.ts
if [[ "$NEMOCLAW_MCP_BRIDGE_AGENT" == "deepagents" ]]; then
npx vitest run --project e2e-live \
test/e2e/live/openshell-credential-generation-window.test.ts \
-t '^openshell-credential-generation-window$' \
--no-file-parallelism \
--silent=false --reporter=default --reporter=test/e2e/risk-signal-reporter.ts
fi

- name: Finalize runner comparison telemetry
if: ${{ always() && github.repository == 'NVIDIA/NemoClaw' && github.ref == 'refs/heads/main' && inputs.checkout_sha == '' && (matrix.agent == 'hermes' || matrix.agent == 'deepagents') }}
Expand All @@ -1193,6 +1186,117 @@ jobs:
shell: bash
run: bash .github/scripts/docker-auth-cleanup.sh

openshell-credential-generation-window:
needs: generate-matrix
if: ${{ (github.event_name != 'workflow_dispatch' || (inputs.jobs == '' && inputs.targets == '')) || contains(format(',{0},', inputs.jobs), ',mcp-bridge,') || contains(format(',{0},', inputs.targets), ',mcp-bridge,') || contains(format(',{0},', inputs.jobs), ',openshell-credential-generation-window,') || contains(format(',{0},', inputs.targets), ',openshell-credential-generation-window,') }}
runs-on: ubuntu-latest
permissions:
contents: read
# Keep the credential-generation lifecycle on a fresh runner so it can
# execute in parallel with, and fail independently from, the Deep Agents
# MCP lifecycle without sharing destructive sandbox state.
timeout-minutes: 90
env:
E2E_JOB: "1"
E2E_TARGET_ID: "openshell-credential-generation-window"
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/live/openshell-credential-generation-window
NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js
NEMOCLAW_OPENSHELL_CHANNEL: stable
NEMOCLAW_OPENSHELL_EXACT_MAIN_PROOF: "1"
NEMOCLAW_RUN_LIVE_E2E: "1"
OPENSHELL_DOCKER_SUPERVISOR_IMAGE: ghcr.io/nvidia/openshell/supervisor@sha256:f4226253a3525c3832adac5b38b419a0f27d1e915effe565b5885e20f93cd5e9
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 and verify cloudflared prerequisite
env:
CLOUDFLARED_VERSION: "2026.6.1"
CLOUDFLARED_DEB_SHA256: "ccd02ec216c62bfa573395d8f72cb2e91e95cbdf8726a8acc06b3e2d9aa31526"
run: |
set -euo pipefail
cloudflared_deb="${RUNNER_TEMP}/cloudflared-${CLOUDFLARED_VERSION}-linux-amd64.deb"
curl -fL "https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64.deb" -o "${cloudflared_deb}"
printf '%s %s\n' "${CLOUDFLARED_DEB_SHA256}" "${cloudflared_deb}" | sha256sum -c -
package="$(dpkg-deb -f "${cloudflared_deb}" Package)"
version="$(dpkg-deb -f "${cloudflared_deb}" Version)"
architecture="$(dpkg-deb -f "${cloudflared_deb}" Architecture)"
if [[ "${package}" != "cloudflared" || "${version}" != "${CLOUDFLARED_VERSION}" || "${architecture}" != "amd64" ]]; then
printf 'Unexpected cloudflared package metadata: package=%s version=%s architecture=%s\n' "${package}" "${version}" "${architecture}" >&2
exit 1
fi
sudo dpkg -i "${cloudflared_deb}"
cloudflared --version | grep -F "cloudflared version ${CLOUDFLARED_VERSION}"

- name: Generate MCP test TLS
run: bash test/e2e/setup-mcp-test-tls.sh

- name: Install OpenShell CLI
env:
NEMOCLAW_OPENSHELL_FORCE_INSTALL: "1"
run: |
set -euo pipefail
bash scripts/install-openshell.sh
mkdir -p "$E2E_ARTIFACT_DIR/openshell-credential-generation-window"
jq -n '{
schemaVersion: 1,
sourceRepository: "NVIDIA/OpenShell",
releaseTag: "v0.0.85",
sourceSha: "3dee5570a46076a57a3b056f35f35ebc0861ac85",
artifacts: {
cli: {binarySha256: "222d9d53a142691d7a7de2c692f38e52d24066f9f633d53746c5fef775861bc8"},
gateway: {binarySha256: "33bb479d936c3c1b17dd475df05747be9de74564fb67d69a4c33cdd01181d02f"},
standaloneSandbox: {binarySha256: "863ef21ab7ef623f5e7a8728c4e5532b46bfbae3ace3b800665a1c6353a1f7d2"}
}
}' > "$E2E_ARTIFACT_DIR/openshell-credential-generation-window/openshell-exact-main-provenance.json"

- name: Run OpenShell credential generation-window live test
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"
exit 1
fi
export OPENSHELL_BIN
"$OPENSHELL_BIN" --version
npx vitest run --project e2e-live \
test/e2e/live/openshell-credential-generation-window.test.ts \
-t '^openshell-credential-generation-window$' \
--no-file-parallelism \
--silent=false --reporter=default --reporter=test/e2e/risk-signal-reporter.ts

- id: credential_window_artifact_secret_scan
name: Scan credential-window artifacts for fixture credentials
if: always()
run: >-
npx tsx tools/e2e/assert-mcp-artifact-secrets-absent.mts
e2e-artifacts/live/openshell-credential-generation-window

- name: Upload credential-window artifacts
if: ${{ always() && steps.credential_window_artifact_secret_scan.outcome == 'success' }}
uses: NVIDIA/NemoClaw/.github/actions/upload-e2e-artifacts@7768e15eb90d3ee2d33432f481dfe8747e4f6d57
with:
name: e2e-openshell-credential-generation-window
path: e2e-artifacts/live/openshell-credential-generation-window/

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

mcp-bridge-dev:
needs: generate-matrix
# Moving OpenShell dev artifacts are compatibility evidence only and must
Expand Down Expand Up @@ -5850,6 +5954,7 @@ jobs:
shared-e2e,
openshell-gateway-auth-contract,
mcp-bridge,
openshell-credential-generation-window,
mcp-bridge-dev,
skill-agent,
openclaw-skill-cli,
Expand Down
10 changes: 6 additions & 4 deletions docs/security/openshell-0.0.85-migration-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -553,14 +553,16 @@ of its credential, and the upstream ledger is required to contain no literal
resolve placeholder.
The stable workflow keeps the OpenClaw, Hermes, and Deep Agents MCP lifecycles
on three fresh-runner shards with separate artifacts. It runs the
`openshell-credential-generation-window` live target once, after the Deep Agents
case, rather than repeating that proof in every shard.
`openshell-credential-generation-window` live target once on a fourth fresh
runner in parallel with the Deep Agents case, rather than serializing or
repeating that proof in the agent matrix.
Comment thread
apurvvkumaria marked this conversation as resolved.
Resolved
The workflow first ran this bounded target against the exact reviewed development
artifacts and scanned its artifacts for the whole generated-secret prefix. The
stable-source review retains the upstream
`expired_retained_generation_does_not_resolve` unit. The default stable job now
binds the target to tag commit `3dee5570`, the extracted release binaries, and the
immutable supervisor index; its final result for the PR SHA remains a merge gate.
binds the target to tag commit `3dee5570`, the extracted release binaries, and
the immutable supervisor index; its final result for the PR SHA remains a merge
gate.
Comment thread
apurvvkumaria marked this conversation as resolved.
Resolved

### v0.0.82 to v0.0.83

Expand Down
6 changes: 3 additions & 3 deletions test/cloudflared-update-check-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,11 @@ describe("cloudflared update-check workflow contract", () => {
expect(checkout?.with?.["persist-credentials"]).toBe(false);
});

it("extracts exactly four identical reviewed version and SHA256 pins", () => {
it("extracts exactly five identical reviewed version and SHA256 pins", () => {
const versions = pinValues(e2e, "CLOUDFLARED_VERSION");
const hashes = pinValues(e2e, "CLOUDFLARED_DEB_SHA256");
expect(versions).toHaveLength(4);
expect(hashes).toHaveLength(4);
expect(versions).toHaveLength(5);
expect(hashes).toHaveLength(5);
Comment thread
apurvvkumaria marked this conversation as resolved.
Resolved
expect(new Set(versions).size).toBe(1);
expect(new Set(hashes).size).toBe(1);
expect(versions[0]).toMatch(/^[0-9]{4}\.[0-9]{1,2}\.[0-9]+$/u);
Expand Down
12 changes: 8 additions & 4 deletions test/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,14 @@ lanes:
- `rebuild-hermes-stale-base`;
- the `hermes` and `deepagents` shards of `mcp-bridge`.

The OpenClaw shards of the matrix jobs, the `openclaw` MCP shard, and
`mcp-bridge-dev` remain on `ubuntu-latest`; unrelated jobs retain their
existing runner assignments. Before setting the variable, an organization
owner must:
The OpenClaw shards of the matrix jobs, the `openclaw` MCP shard,
`mcp-bridge-dev`, and `openshell-credential-generation-window` remain on
`ubuntu-latest`; unrelated jobs retain their existing runner assignments.
The credential-generation window runs as an independent fresh-runner job in
parallel with the stable MCP agent matrix. Default full-suite dispatches and
explicit `mcp-bridge` selections run both jobs, while the credential-window job
keeps its own exact-release provenance, secret scan, and artifact.
Before setting the variable, an organization owner must:

1. Create a GitHub-hosted Ubuntu x64 larger runner with 8 vCPU, 32 GB RAM, and
300 GB SSD in a dedicated runner group.
Expand Down
78 changes: 67 additions & 11 deletions test/e2e/support/mcp-workflow-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,34 +74,67 @@ describe("MCP workflow artifact boundary", () => {
}
});

it("rejects re-serializing the credential generation-window proof behind the MCP matrix", () => {
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,
{
needs?: string | string[];
steps: Array<{ name?: string; run?: string }>;
}
>;
};
const mcpRun = workflow.jobs["mcp-bridge"].steps.find(
(step) => step.name === "Run MCP OpenShell provider live test",
);
requireFixture(mcpRun?.run, "MCP stable lifecycle fixture is missing");
mcpRun.run += "\ntest/e2e/live/openshell-credential-generation-window.test.ts";
workflow.jobs["openshell-credential-generation-window"].needs = [
"generate-matrix",
"mcp-bridge",
];
fs.writeFileSync(workflowPath, YAML.stringify(workflow));

expect(validateMcpOpenShellWorkflowBoundary(workflowPath)).toEqual(
expect.arrayContaining([
"mcp-bridge must not serialize the independent credential generation-window proof",
"openshell-credential-generation-window must depend only on matrix generation so it can run in parallel",
]),
);
} finally {
fs.rmSync(directory, { force: true, recursive: true });
}
});

it.each([
{
expected: "mcp-bridge must isolate the credential generation-window proof to one shard",
mutate: (run: string) =>
run.replace('if [[ "$NEMOCLAW_MCP_BRIDGE_AGENT" == "deepagents" ]]; then', "if true; then"),
name: "runs on every shard",
},
{
expected: "mcp-bridge must run the credential generation-window lifecycle",
expected: "openshell-credential-generation-window must run its exact isolated live proof",
mutate: (run: string) =>
run.replace("test/e2e/live/openshell-credential-generation-window.test.ts", ""),
name: "is missing",
},
{
expected:
"mcp-bridge credential generation-window proof must publish canonical risk-signal evidence",
"openshell-credential-generation-window must publish one canonical risk-signal stream",
mutate: (run: string) => run.replace("--reporter=test/e2e/risk-signal-reporter.ts", ""),
name: "omits its risk-signal reporter",
},
])("rejects a credential generation-window proof that $name", ({ expected, mutate, name }) => {
])("rejects an independent credential generation-window proof that $name", ({
expected,
mutate,
name,
}) => {
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<{ name?: string; run?: string }> }>;
};
const run = workflow.jobs["mcp-bridge"].steps.find(
(step) => step.name === "Run MCP OpenShell provider live test",
const run = workflow.jobs["openshell-credential-generation-window"].steps.find(
(step) => step.name === "Run OpenShell credential generation-window live test",
);
requireFixture(run?.run, "MCP stable lifecycle fixture is missing");
const updatedRun = mutate(run.run);
Expand All @@ -115,6 +148,29 @@ describe("MCP workflow artifact boundary", () => {
}
});

it.each([
"report-to-pr",
"scorecard",
])("requires %s to wait for the independent credential-window result", (terminalJob) => {
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, { needs: string[] }>;
};
workflow.jobs[terminalJob].needs = workflow.jobs[terminalJob].needs.filter(
(job) => job !== "openshell-credential-generation-window",
);
fs.writeFileSync(workflowPath, YAML.stringify(workflow));

expect(validateMcpOpenShellWorkflowBoundary(workflowPath)).toContain(
`${terminalJob} must wait for openshell-credential-generation-window`,
);
} finally {
fs.rmSync(directory, { force: true, recursive: true });
}
});

it("rejects upload action or path drift from the reviewed shared boundary", () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-mcp-workflow-"));
const workflowPath = path.join(directory, "e2e.yaml");
Expand Down
Loading
Loading