Skip to content
Closed
45 changes: 45 additions & 0 deletions .github/workflows/codeql.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# CodeQL security scanning — runs on PRs and weekly schedule.

name: codeql

on:
pull_request:
types: [opened, synchronize, reopened]
schedule:
- cron: "0 6 * * 1" # Every Monday at 06:00 UTC

permissions:
contents: read
security-events: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
analyze:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
language: [javascript-typescript, python]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}

- name: Autobuild
uses: github/codeql-action/autobuild@v3

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
116 changes: 116 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Automated release pipeline: runs tests, publishes to npm, creates GitHub Release
# with changelog generated from conventional commits.
#
# Triggered by pushing a semver tag (e.g., v0.2.0).

name: release

on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+*"

permissions:
contents: write

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm

- name: Install root dependencies
run: npm install

- name: Install and build TypeScript plugin
working-directory: nemoclaw
run: |
npm install
npm run build

- name: Run unit tests
run: node --test test/*.test.js

- name: Run TypeScript unit tests
working-directory: nemoclaw
run: npx vitest run

publish-npm:
needs: test
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"

- name: Install and build
run: |
npm install
cd nemoclaw && npm install && npm run build && cd ..

- name: Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

github-release:
needs: test
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Generate changelog from conventional commits
id: changelog
run: |
# Find the previous tag
PREV_TAG=$(git tag --sort=-version:refname | head -2 | tail -1)
if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "${{ github.ref_name }}" ]; then
PREV_TAG=$(git rev-list --max-parents=0 HEAD)
fi

echo "## What's Changed" > /tmp/changelog.md
echo "" >> /tmp/changelog.md

# Group commits by type
for type_label in "feat:Features" "fix:Bug Fixes" "docs:Documentation" "chore:Maintenance" "refactor:Refactoring" "test:Tests" "ci:CI/CD" "perf:Performance"; do
type="${type_label%%:*}"
label="${type_label##*:}"
commits=$(git log "${PREV_TAG}..HEAD" --pretty=format:"- %s (%h)" --grep="^${type}" 2>/dev/null || true)
if [ -n "$commits" ]; then
echo "### ${label}" >> /tmp/changelog.md
echo "$commits" >> /tmp/changelog.md
echo "" >> /tmp/changelog.md
fi
done

echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${{ github.ref_name }}" >> /tmp/changelog.md

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
body_path: /tmp/changelog.md
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
67 changes: 66 additions & 1 deletion bin/lib/credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const { execSync } = require("child_process");
const { execSync, spawnSync } = require("child_process");

const CREDS_DIR = path.join(process.env.HOME || "/tmp", ".nemoclaw");
const CREDS_FILE = path.join(CREDS_DIR, "credentials.json");
Expand Down Expand Up @@ -74,13 +74,77 @@ async function ensureApiKey() {
process.exit(1);
}

// Validate the key against the NVIDIA API before saving
console.log(" Validating API key...");
const validation = validateApiKey(key);
if (validation.ok) {
console.log(" ✓ API key is valid");
} else if (validation.fatal) {
console.error(` ✗ ${validation.message}`);
process.exit(1);
} else {
console.log(` ⓘ ${validation.message}`);
}

saveCredential("NVIDIA_API_KEY", key);
process.env.NVIDIA_API_KEY = key;
console.log("");
console.log(" Key saved to ~/.nemoclaw/credentials.json (mode 600)");
console.log("");
}

/**
* Validate an NVIDIA API key by making a lightweight test request.
* Returns { ok, fatal, message } where:
* ok: true if the key is confirmed valid
* fatal: true if the key is definitively invalid (should not proceed)
* message: human-readable status
*/
function validateApiKey(key) {
try {
// Pass the auth header via stdin using -H @- so the API key
// does not appear in process arguments visible via ps aux.
const result = spawnSync(
"curl",
[
"-sf",
"-o", "/dev/null",
"-w", "%{http_code}",
"-H", "@-",
"https://integrate.api.nvidia.com/v1/models",
],
{
input: `Authorization: Bearer ${key}`,
encoding: "utf-8",
timeout: 15000,
stdio: ["pipe", "pipe", "pipe"],
}
);
// Check for local spawn errors (curl missing, timeout) before inspecting stdout.
if (result.error) {
if (result.error.code === "ENOENT") {
return { ok: false, fatal: false, message: "Could not validate key (curl is not installed). Proceeding with saved key." };
}
const reason = result.error.code === "ETIMEDOUT" ? "timed out" : result.error.message || "unknown error";
return { ok: false, fatal: false, message: `Could not validate key (${reason}). Proceeding with saved key.` };
}
const httpCode = (result.stdout || "").trim();
if (httpCode === "200") {
return { ok: true, fatal: false, message: "API key validated successfully" };
}
if (httpCode === "401" || httpCode === "403") {
return { ok: false, fatal: true, message: "API key is invalid or expired. Check https://build.nvidia.com/settings/api-keys" };
}
if (httpCode === "000" || !httpCode) {
return { ok: false, fatal: false, message: "Could not reach NVIDIA API (network issue). Key format looks valid — proceeding." };
}
return { ok: false, fatal: false, message: `Unexpected response (HTTP ${httpCode}). Key format looks valid — proceeding.` };
} catch {
// Network failure — don't block onboarding, just warn
return { ok: false, fatal: false, message: "Could not validate key (network error). Proceeding with saved key." };
}
}

function isRepoPrivate(repo) {
try {
const json = execSync(`gh api repos/${repo} --jq .private 2>/dev/null`, { encoding: "utf-8" }).trim();
Expand Down Expand Up @@ -138,4 +202,5 @@ module.exports = {
ensureApiKey,
ensureGithubToken,
isRepoPrivate,
validateApiKey,
};
32 changes: 32 additions & 0 deletions bin/lib/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Debug logger for verbose/diagnostic output.
// Usage:
// LOG_LEVEL=debug nemoclaw onboard
// nemoclaw --verbose onboard

const LOG_LEVELS = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };

let currentLevel = LOG_LEVELS.info;

function setLevel(level) {
const resolved = LOG_LEVELS[level];
if (resolved !== undefined) {
currentLevel = resolved;
}
}

function debug(...args) {
if (currentLevel >= LOG_LEVELS.debug) {
console.error(" [debug]", ...args);
}
}

// Initialize from environment
const envLevel = (process.env.LOG_LEVEL || process.env.NEMOCLAW_LOG_LEVEL || "").toLowerCase();
if (envLevel && LOG_LEVELS[envLevel] !== undefined) {
setLevel(envLevel);
}

module.exports = { LOG_LEVELS, setLevel, debug };
Loading