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
11 changes: 8 additions & 3 deletions .github/workflows/e2e-vitest-scenarios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -982,11 +982,16 @@ jobs:

- name: Run credential migration live test
# Migrated from test/e2e/test-credential-migration.sh. This live test
# needs NVIDIA_INFERENCE_API_KEY only as the staged legacy credential value; it
# preserves the default NVIDIA provider/key migration path while
# pinning a lower-quota catalog model in the test fixture.
# stages NVIDIA_INFERENCE_API_KEY through legacy credentials.json as the
# custom provider's COMPATIBLE_API_KEY. The hosted service behind this
# repo-scoped secret is inference-api.nvidia.com, not Build/NVIDIA
# Endpoints, so the test must exercise the compatible-provider route.
env:
NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }}
NEMOCLAW_PROVIDER: custom
NEMOCLAW_ENDPOINT_URL: https://inference-api.nvidia.com/v1
NEMOCLAW_MODEL: nvidia/nvidia/nemotron-3-super-v3
NEMOCLAW_COMPAT_MODEL: nvidia/nvidia/nemotron-3-super-v3
run: |
set -euo pipefail
npx vitest run --project e2e-scenarios-live \
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/nightly-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1544,11 +1544,18 @@ jobs:

- name: Run credential migration Vitest test
# Trusted-code boundary: this job runs the checked-out target ref with
# NVIDIA_INFERENCE_API_KEY because it validates live credential migration into the
# OpenShell gateway. Keep checkout credentials disabled, do not pass
# NVIDIA_INFERENCE_API_KEY because it validates live credential
# migration into the OpenShell gateway. The hosted service behind this
# repo-scoped secret is inference-api.nvidia.com, not Build/NVIDIA
# Endpoints, so the test stages it as the custom provider's
# COMPATIBLE_API_KEY. Keep checkout credentials disabled, do not pass
# GITHUB_TOKEN, and rely on reviewed/maintainer-dispatched refs.
env:
NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }}
NEMOCLAW_PROVIDER: custom
NEMOCLAW_ENDPOINT_URL: https://inference-api.nvidia.com/v1
NEMOCLAW_MODEL: nvidia/nvidia/nemotron-3-super-v3
NEMOCLAW_COMPAT_MODEL: nvidia/nvidia/nemotron-3-super-v3
E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/credential-migration
NEMOCLAW_RUN_E2E_SCENARIOS: "1"
NEMOCLAW_SANDBOX_NAME: "e2e-cred-migration"
Expand Down
60 changes: 60 additions & 0 deletions test/e2e-scenario/fixtures/hosted-inference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

const HOSTED_INFERENCE_SECRET = "NVIDIA_INFERENCE_API_KEY";
const HOSTED_INFERENCE_CREDENTIAL_ENV = "COMPATIBLE_API_KEY";
const HOSTED_INFERENCE_PROVIDER = "custom";
const HOSTED_INFERENCE_PROVIDER_NAME = "compatible-endpoint";
const DEFAULT_HOSTED_INFERENCE_BASE_URL = "https://inference-api.nvidia.com/v1";
const DEFAULT_HOSTED_INFERENCE_MODEL = "nvidia/nvidia/nemotron-3-super-v3";

export interface HostedInferenceSecrets {
required(name: string): string;
}

export interface HostedInferenceOptions {
model?: string;
}

export interface HostedInferenceConfig {
apiKey: string;
sourceSecretName: typeof HOSTED_INFERENCE_SECRET;
credentialEnv: typeof HOSTED_INFERENCE_CREDENTIAL_ENV;
provider: typeof HOSTED_INFERENCE_PROVIDER;
providerName: typeof HOSTED_INFERENCE_PROVIDER_NAME;
env: NodeJS.ProcessEnv;
model: string;
endpointUrl: string;
contractLabel: string;
}

export function requireHostedInferenceConfig(
secrets: HostedInferenceSecrets,
env: NodeJS.ProcessEnv = process.env,
options: HostedInferenceOptions = {},
): HostedInferenceConfig {
const apiKey = secrets.required(HOSTED_INFERENCE_SECRET);
const endpointUrl = env.NEMOCLAW_ENDPOINT_URL || DEFAULT_HOSTED_INFERENCE_BASE_URL;
const model =
env.NEMOCLAW_MODEL ||
env.NEMOCLAW_COMPAT_MODEL ||
options.model ||
DEFAULT_HOSTED_INFERENCE_MODEL;
return {
apiKey,
sourceSecretName: HOSTED_INFERENCE_SECRET,
credentialEnv: HOSTED_INFERENCE_CREDENTIAL_ENV,
provider: HOSTED_INFERENCE_PROVIDER,
providerName: HOSTED_INFERENCE_PROVIDER_NAME,
endpointUrl,
model,
env: {
NEMOCLAW_PROVIDER: HOSTED_INFERENCE_PROVIDER,
NEMOCLAW_ENDPOINT_URL: endpointUrl,
NEMOCLAW_MODEL: model,
NEMOCLAW_COMPAT_MODEL: model,
[HOSTED_INFERENCE_CREDENTIAL_ENV]: apiKey,
},
contractLabel: "NVIDIA_INFERENCE_API_KEY is staged as the compatible endpoint credential",
};
}
55 changes: 32 additions & 23 deletions test/e2e-scenario/live/credential-migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts";
import type { HostCliClient } from "../fixtures/clients/host.ts";
import { validateSandboxName } from "../fixtures/clients/sandbox.ts";
import { expect, test } from "../fixtures/e2e-test.ts";
import { requireHostedInferenceConfig } from "../fixtures/hosted-inference.ts";
import { shouldRunLiveE2EScenarios } from "../fixtures/live-project-gate.ts";

// Migrated from test/e2e/test-credential-migration.sh. This is a focused live
Expand All @@ -17,10 +18,10 @@ import { shouldRunLiveE2EScenarios } from "../fixtures/live-project-gate.ts";
// a successful real onboard registers the migrated value with the OpenShell
// gateway, the plaintext file is removed after success, credentials list reads
// from the gateway, and secure unlink removes a planted symlink without touching
// its target. The live onboard intentionally follows the legacy default NVIDIA
// Endpoints path: NVIDIA_INFERENCE_API_KEY is present only in the legacy file, absent from
// the onboard child env, and must migrate into the nvidia-prod gateway provider.
// No registry, migration ledger, or shared helper is introduced.
// its target. The repository secret is named NVIDIA_INFERENCE_API_KEY, but the
// hosted E2E service is the OpenAI-compatible inference-api.nvidia.com endpoint,
// so the migration contract stages that value as COMPATIBLE_API_KEY and expects
// the compatible-endpoint gateway provider.

const REPO_ROOT = path.resolve(import.meta.dirname, "../../..");
const CLI_ENTRYPOINT = path.join(REPO_ROOT, "bin", "nemoclaw.js");
Expand Down Expand Up @@ -107,7 +108,10 @@ async function cleanupCredentialMigrationState(host: HostCliClient, home: string
host.command("node", [CLI_ENTRYPOINT, SANDBOX_NAME, "destroy", "--yes"], {
artifactName: "cleanup-nemoclaw-destroy",
env,
redactionValues: [process.env.NVIDIA_INFERENCE_API_KEY ?? ""],
redactionValues: [
process.env.NVIDIA_INFERENCE_API_KEY ?? "",
process.env.COMPATIBLE_API_KEY ?? "",
],
timeoutMs: 120_000,
}),
);
Expand Down Expand Up @@ -138,15 +142,18 @@ runCredentialMigrationTest(
"credential migration stages legacy file into gateway and removes plaintext safely",
{ timeout: ONBOARD_TIMEOUT_MS + INSTALL_TIMEOUT_MS + 5 * 60_000 },
async ({ artifacts, cleanup, host, secrets, skip }) => {
// Use the existing nightly secret as the legacy NVIDIA credential. The
// onboard child env below deliberately does not receive NVIDIA_INFERENCE_API_KEY, so
// Use the existing nightly secret as the legacy provider credential. The
// onboard child env below deliberately does not receive that credential, so
// the only source is ~/.nemoclaw/credentials.json — matching the retired
// shell lane's migration contract.
const migratedCredentialValue = secrets.required("NVIDIA_INFERENCE_API_KEY");
expect(
migratedCredentialValue.startsWith("nvapi-"),
"NVIDIA_INFERENCE_API_KEY must start with nvapi-",
).toBe(true);
const hostedInference = requireHostedInferenceConfig(secrets, process.env, {
model: CREDENTIAL_MIGRATION_MODEL,
});
const migratedCredentialValue = hostedInference.apiKey;
const {
[hostedInference.credentialEnv]: _omittedCredential,
...hostedInferenceEnvWithoutCredential
} = hostedInference.env;
expect(fs.existsSync(CLI_ENTRYPOINT), "bin/nemoclaw.js missing").toBe(true);
expect(
fs.existsSync(DIST_CREDENTIAL_STORE),
Expand Down Expand Up @@ -183,8 +190,9 @@ runCredentialMigrationTest(
sandboxName: SANDBOX_NAME,
contracts: [
"legacy credentials.json stages allowlisted provider keys into onboard env",
"successful default NVIDIA Endpoints onboard registers the migrated value with OpenShell gateway",
"onboard keeps the default NVIDIA provider/key/endpoint/policy path while pinning a low-quota catalog model",
`successful onboard registers the migrated value with the ${hostedInference.providerName} OpenShell gateway provider`,
`${hostedInference.sourceSecretName} is migrated into the ${hostedInference.credentialEnv} provider credential`,
`onboard uses the ${hostedInference.provider} provider and ${hostedInference.endpointUrl} endpoint path`,
"successful onboard removes plaintext credentials.json",
"tampered non-credential keys do not become gateway providers",
"credentials list reads providers from the gateway, not disk",
Expand All @@ -201,7 +209,7 @@ runCredentialMigrationTest(
legacyFile,
JSON.stringify(
{
NVIDIA_INFERENCE_API_KEY: migratedCredentialValue,
[hostedInference.credentialEnv]: migratedCredentialValue,
OPENSHELL_GATEWAY: "evil-gw-from-tampered-file",
NODE_OPTIONS: "--require=/tmp/evil.js",
},
Expand All @@ -214,11 +222,9 @@ runCredentialMigrationTest(
const onboard = await host.command("node", [CLI_ENTRYPOINT, "onboard", "--non-interactive"], {
artifactName: "onboard-from-legacy-credentials",
env: testEnv(home, {
...hostedInferenceEnvWithoutCredential,
NEMOCLAW_SANDBOX_NAME: SANDBOX_NAME,
NEMOCLAW_RECREATE_SANDBOX: "1",
// Keep the default NVIDIA provider/key/endpoint/policy path while
// avoiding the high-quota default Nemotron validation model.
NEMOCLAW_MODEL: CREDENTIAL_MIGRATION_MODEL,
}),
redactionValues: [migratedCredentialValue],
timeoutMs: ONBOARD_TIMEOUT_MS,
Expand Down Expand Up @@ -247,9 +253,10 @@ runCredentialMigrationTest(
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(line));
expect(providerNames, `expected migrated NVIDIA provider\n${providersText}`).toContain(
"nvidia-prod",
);
expect(
providerNames,
`expected migrated ${hostedInference.providerName} provider\n${providersText}`,
).toContain(hostedInference.providerName);
expect(providerNames).not.toContain("OPENSHELL_GATEWAY");
expect(providerNames).not.toContain("NODE_OPTIONS");

Expand Down Expand Up @@ -292,15 +299,17 @@ runCredentialMigrationTest(
await artifacts.writeJson("scenario-result.json", {
id: "credential-migration",
sandboxName: SANDBOX_NAME,
model: CREDENTIAL_MIGRATION_MODEL,
model: hostedInference.model || CREDENTIAL_MIGRATION_MODEL,
provider: hostedInference.providerName,
credentialEnv: hostedInference.credentialEnv,
providerNames,
assertions: {
onboardSucceeded: onboard.exitCode === 0,
migrationNoticeEmitted: onboardText.includes(
"Staged 1 legacy credential(s) for migration to the OpenShell gateway.",
),
legacyFileRemovedAfterOnboard: !fs.existsSync(legacyFile),
migratedNvidiaProviderRegistered: providerNames.includes("nvidia-prod"),
migratedProviderRegistered: providerNames.includes(hostedInference.providerName),
tamperedKeysExcluded:
!providerNames.includes("OPENSHELL_GATEWAY") && !providerNames.includes("NODE_OPTIONS"),
credentialsListReadsGateway: credentialsText.includes(
Expand Down
58 changes: 58 additions & 0 deletions test/e2e-scenario/support-tests/hosted-inference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";

import { requireHostedInferenceConfig } from "../fixtures/hosted-inference.ts";

function secrets(values: Record<string, string | undefined>) {
return {
required: (name: string) => {
const value = values[name];
if (!value) throw new Error(`missing ${name}`);
return value;
},
};
}

describe("hosted inference E2E config", () => {
it("uses NVIDIA_INFERENCE_API_KEY as the hosted compatible endpoint source secret", () => {
const cfg = requireHostedInferenceConfig(
secrets({ NVIDIA_INFERENCE_API_KEY: "repo-hosted-key" }),
{},
);

expect(cfg.sourceSecretName).toBe("NVIDIA_INFERENCE_API_KEY");
expect(cfg.provider).toBe("custom");
expect(cfg.providerName).toBe("compatible-endpoint");
expect(cfg.credentialEnv).toBe("COMPATIBLE_API_KEY");
expect(cfg.env.COMPATIBLE_API_KEY).toBe("repo-hosted-key");
});

it("does not require an nvapi-prefixed source secret", () => {
const cfg = requireHostedInferenceConfig(
secrets({
NVIDIA_INFERENCE_API_KEY: "sk-compatible-key",
}),
{},
);

expect(cfg.apiKey).toBe("sk-compatible-key");
expect(cfg.credentialEnv).toBe("COMPATIBLE_API_KEY");
});

it("configures the custom provider route for inference-api.nvidia.com", () => {
const cfg = requireHostedInferenceConfig(
secrets({ NVIDIA_INFERENCE_API_KEY: "repo-hosted-key" }),
{ NEMOCLAW_MODEL: "nvidia/custom-model" },
);

expect(cfg.env).toMatchObject({
NEMOCLAW_PROVIDER: "custom",
NEMOCLAW_ENDPOINT_URL: "https://inference-api.nvidia.com/v1",
NEMOCLAW_MODEL: "nvidia/custom-model",
NEMOCLAW_COMPAT_MODEL: "nvidia/custom-model",
COMPATIBLE_API_KEY: "repo-hosted-key",
});
});
});
Loading