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
14 changes: 9 additions & 5 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ async function setupInference(sandboxName, model, provider) {
// Create nvidia-nim provider
run(
`openshell provider create --name nvidia-nim --type openai ` +
`--credential ${shellQuote("NVIDIA_API_KEY=" + process.env.NVIDIA_API_KEY)} ` +
`--credential ${shellQuote("NVIDIA_API_KEY")} ` +
`--config "OPENAI_BASE_URL=https://integrate.api.nvidia.com/v1" 2>&1 || true`,
{ ignoreError: true }
);
Expand All @@ -824,10 +824,12 @@ async function setupInference(sandboxName, model, provider) {
}
const baseUrl = getLocalProviderBaseUrl(provider);
run(
`OPENAI_API_KEY=dummy ` +
`openshell provider create --name vllm-local --type openai ` +
`--credential "OPENAI_API_KEY=dummy" ` +
`--credential "OPENAI_API_KEY" ` +
`--config "OPENAI_BASE_URL=${baseUrl}" 2>&1 || ` +
`openshell provider update vllm-local --credential "OPENAI_API_KEY=dummy" ` +
`OPENAI_API_KEY=dummy ` +
`openshell provider update vllm-local --credential "OPENAI_API_KEY" ` +
`--config "OPENAI_BASE_URL=${baseUrl}" 2>&1 || true`,
{ ignoreError: true }
);
Expand All @@ -844,10 +846,12 @@ async function setupInference(sandboxName, model, provider) {
}
const baseUrl = getLocalProviderBaseUrl(provider);
run(
`OPENAI_API_KEY=ollama ` +
`openshell provider create --name ollama-local --type openai ` +
`--credential "OPENAI_API_KEY=ollama" ` +
`--credential "OPENAI_API_KEY" ` +
`--config "OPENAI_BASE_URL=${baseUrl}" 2>&1 || ` +
`openshell provider update ollama-local --credential "OPENAI_API_KEY=ollama" ` +
`OPENAI_API_KEY=ollama ` +
`openshell provider update ollama-local --credential "OPENAI_API_KEY" ` +
`--config "OPENAI_BASE_URL=${baseUrl}" 2>&1 || true`,
{ ignoreError: true }
);
Expand Down
4 changes: 3 additions & 1 deletion nemoclaw-blueprint/orchestrator/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,10 @@ def action_apply(
"--type",
provider_type,
]
target_cred_env = "OPENAI_API_KEY"
if credential:
provider_args.extend(["--credential", f"OPENAI_API_KEY={credential}"])
os.environ[target_cred_env] = credential
provider_args.extend(["--credential", target_cred_env])
if endpoint:
provider_args.extend(["--config", f"OPENAI_BASE_URL={endpoint}"])

Expand Down
83 changes: 83 additions & 0 deletions test/credential-exposure.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Security regression test: credential values must never appear in --credential
// CLI arguments. OpenShell reads credential values from the environment when
// only the env-var name is passed (e.g. --credential "NVIDIA_API_KEY"), so
// there is no reason to pass the secret itself on the command line where it
// would be visible in `ps aux` output.

"use strict";

const fs = require("node:fs");
const path = require("node:path");

const ONBOARD_JS = path.join(__dirname, "..", "bin", "lib", "onboard.js");
const RUNNER_PY = path.join(
__dirname,
"..",
"nemoclaw-blueprint",
"orchestrator",
"runner.py",
);

// Matches --credential followed by a value containing "=" (i.e. KEY=VALUE).
// Catches quoted KEY=VALUE patterns in JS and Python f-string interpolation.
// Assumes credentials are always in quoted strings (which matches our codebase).
// NOTE: unquoted forms like `--credential KEY=VALUE` would not be detected.
const JS_EXPOSURE_RE = /--credential\s+[^"]*"[A-Z_]+=/;
const JS_CREDENTIAL_CONCAT_RE = /--credential.*=.*process\.env\./;
const PY_EXPOSURE_RE = /--credential.*=.*\{/;

describe("credential exposure in process arguments", () => {
it("onboard.js must not pass KEY=VALUE to --credential", () => {
const src = fs.readFileSync(ONBOARD_JS, "utf-8");
const lines = src.split("\n");

const violations = lines.filter(
(line) =>
(JS_EXPOSURE_RE.test(line) || JS_CREDENTIAL_CONCAT_RE.test(line)) &&
// Allow comments that describe the old pattern
!line.trimStart().startsWith("//"),
);

expect(violations).toEqual([]);
});

it("runner.py must not pass KEY=VALUE to --credential", () => {
const src = fs.readFileSync(RUNNER_PY, "utf-8");
const lines = src.split("\n");

const violations = lines.filter(
(line) =>
PY_EXPOSURE_RE.test(line) &&
line.includes("--credential") &&
!line.trimStart().startsWith("#"),
);

expect(violations).toEqual([]);
});

it("onboard.js --credential flags pass env var names only", () => {
const src = fs.readFileSync(ONBOARD_JS, "utf-8");

// Find all --credential arguments and verify they contain only a key name
// (no "=" sign in the credential value)
const credentialArgs = src.match(/--credential\s+"([^"]+)"/g) || [];
const credentialShellQuote =
src.match(/--credential\s+\$\{shellQuote\("([^"]+)"\)\}/g) || [];

const allArgs = [...credentialArgs, ...credentialShellQuote];
expect(allArgs.length).toBeGreaterThan(0);

for (const arg of allArgs) {
// Extract the credential value from the match
const valueMatch =
arg.match(/--credential\s+"([^"]+)"/) ||
arg.match(/--credential\s+\$\{shellQuote\("([^"]+)"\)\}/);
if (valueMatch) {
expect(valueMatch[1]).not.toContain("=");
}
}
});
});
Loading