mirror of
https://github.com/NVIDIA/NemoClaw.git
synced 2026-07-03 03:37:16 +00:00
chore(tooling): switch linting to biome (#2862)
## Summary Switches JavaScript and TypeScript linting/formatting from ESLint and Prettier to Biome. Keeps the credential environment-variable security check by replacing the custom ESLint rule with a standalone TypeScript guard wired into npm scripts and prek. ## Changes - Add `biome.json` and update root/plugin npm scripts plus `Makefile` targets to use Biome. - Replace prek Prettier/ESLint hooks with Biome format/lint hooks. - Remove ESLint/Prettier configs and dependencies from root and plugin projects. - Add `scripts/check-direct-credential-env.ts` and update tests to preserve the direct credential env guard. - Remove stale ESLint suppression comments from source and tests. ## Type of Change - [x] Code change (feature, bug fix, or refactor) - [ ] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Verification - [x] `npx prek run --all-files` passes - [x] `npm test` passes - [x] Tests added or updated for new or changed behavior - [x] No secrets, API keys, or credentials committed - [ ] Docs updated for user-facing behavior changes - [ ] `make docs` builds without warnings (doc changes only) - [ ] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) ## AI Disclosure - [x] AI-assisted — tool: pi coding agent --- Signed-off-by: Carlos Villela <cvillela@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Migrated formatting/linting from ESLint/Prettier to Biome and removed legacy formatter configs and many inline lint suppressions. * **New Features** * Added an automated CLI check that scans source files for direct reads of credential environment variables. * **Documentation** * Updated linting guidance to reflect Biome-based configuration and project linting behavior. * **Bug Fixes** * Interactive consent now prints " Installation cancelled" on prompt errors and returns false. * **Tests** * Added/updated tests for credential-guarding logic and the interactive consent error path. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Carlos Villela <cvillela@nvidia.com>
This commit is contained in:
parent
fdcc8c18f8
commit
defeffc3e9
44 changed files with 941 additions and 2860 deletions
|
|
@ -16,8 +16,8 @@
|
|||
# Priority groups (prek runs same-priority hooks in parallel):
|
||||
# 0 — General file fixers (whitespace, EOF, line endings)
|
||||
# 4 — SPDX header insertion (--fix)
|
||||
# 5 — Shell / TS formatters (shfmt, prettier)
|
||||
# 6 — Fixes that should follow formatters (ruff check --fix, eslint --fix)
|
||||
# 5 — Shell / JS / TS formatters (shfmt, Biome)
|
||||
# 6 — Fixes that should follow formatters (ruff check --fix, Biome lint --write)
|
||||
# 10 — Linters and read-only checks
|
||||
# 20 — Project-level checks (vitest, coverage, ratchet)
|
||||
|
||||
|
|
@ -96,40 +96,22 @@ repos:
|
|||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: prettier-plugin
|
||||
name: Prettier (plugin)
|
||||
entry: bash -c 'root="$(git rev-parse --show-toplevel)" && cd "$root/nemoclaw" && files=() && for f in "$@"; do files+=("${f#nemoclaw/}"); done && npx prettier --write "${files[@]}"' --
|
||||
- id: biome-format
|
||||
name: Biome format
|
||||
entry: npx biome format --write
|
||||
language: system
|
||||
files: ^nemoclaw/.*\.ts$
|
||||
pass_filenames: true
|
||||
priority: 5
|
||||
|
||||
- id: prettier-js
|
||||
name: Prettier (JavaScript)
|
||||
entry: npx prettier --write
|
||||
language: system
|
||||
files: ^(bin|test)/.*\.js$
|
||||
files: ^(biome\.json|package(-lock)?\.json|nemoclaw/package(-lock)?\.json|commitlint\.config\.js|bin/.*\.js|src/.*\.ts|scripts/.*\.(js|ts)|test/.*\.js|nemoclaw/src/.*\.ts)$
|
||||
pass_filenames: true
|
||||
priority: 5
|
||||
|
||||
# ── Priority 6: auto-fix after formatting ─────────────────────────────────
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: eslint-plugin
|
||||
name: ESLint (plugin)
|
||||
entry: bash -c 'root="$(git rev-parse --show-toplevel)" && cd "$root/nemoclaw" && files=() && for f in "$@"; do files+=("${f#nemoclaw/}"); done && npx eslint --fix "${files[@]}"' --
|
||||
- id: biome-lint-fix
|
||||
name: Biome lint fixes
|
||||
entry: npx biome lint --write
|
||||
language: system
|
||||
files: ^nemoclaw/.*\.ts$
|
||||
pass_filenames: true
|
||||
priority: 6
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: eslint-cli
|
||||
name: ESLint (CLI)
|
||||
entry: npx eslint --fix
|
||||
language: system
|
||||
files: ^(bin|test|scripts|docs/_ext)/.*\.js$
|
||||
files: ^(commitlint\.config\.js|bin/.*\.js|src/.*\.ts|scripts/.*\.(js|ts)|test/.*\.js|docs/_ext/.*\.js|nemoclaw/src/.*\.ts)$
|
||||
pass_filenames: true
|
||||
priority: 6
|
||||
|
||||
|
|
@ -167,6 +149,14 @@ repos:
|
|||
files: ^(nemoclaw-blueprint/.*\.yaml$|nemoclaw/openclaw\.plugin\.json$|schemas/.*\.json$)
|
||||
priority: 10
|
||||
|
||||
- id: direct-credential-env
|
||||
name: Direct credential env guard
|
||||
entry: npx tsx scripts/check-direct-credential-env.ts
|
||||
language: system
|
||||
files: ^src/lib/onboard\.ts$
|
||||
pass_filenames: true
|
||||
priority: 10
|
||||
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: v0.11.0.1
|
||||
hooks:
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
11
AGENTS.md
11
AGENTS.md
|
|
@ -1,3 +1,6 @@
|
|||
<!-- SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -->
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
|
||||
# Agent Instructions
|
||||
|
||||
## Project Overview
|
||||
|
|
@ -106,13 +109,13 @@ For shell scripts use `#` comments. For Markdown use HTML comments.
|
|||
|
||||
- `bin/` launcher and remaining `scripts/*.js`: **CommonJS** (`require`/`module.exports`), Node.js 22.16+
|
||||
- `test/`: **ESM** (`import`/`export`)
|
||||
- ESLint config in `eslint.config.mjs`
|
||||
- Cyclomatic complexity limit: 20 (ratcheting down to 15)
|
||||
- Biome config in `biome.json`
|
||||
- Keep function complexity low; existing complexity hotspots are tracked separately
|
||||
- Unused vars pattern: prefix with `_`
|
||||
|
||||
### TypeScript
|
||||
|
||||
- Plugin code in `nemoclaw/src/` with its own ESLint config
|
||||
- Plugin code in `nemoclaw/src/` is linted and formatted by the root Biome config
|
||||
- CLI type-checking via `tsconfig.cli.json`
|
||||
- Plugin type-checking via `nemoclaw/tsconfig.json`
|
||||
|
||||
|
|
@ -166,7 +169,7 @@ All hooks managed by [prek](https://prek.j178.dev/) (installed via `npm install`
|
|||
### Gotchas
|
||||
|
||||
- `npm install` at root triggers `prek install` which sets up git hooks. If hooks fail, check that `core.hooksPath` is unset: `git config --unset core.hooksPath`
|
||||
- The `nemoclaw/` subdirectory has its own `package.json`, `node_modules/`, and ESLint config — it's a separate npm project
|
||||
- The `nemoclaw/` subdirectory has its own `package.json` and `node_modules`, while sharing the root Biome config — it's a separate npm project
|
||||
- SPDX headers are auto-inserted by pre-commit hooks; don't worry about adding them manually
|
||||
- Coverage thresholds are ratcheted in `ci/coverage-threshold-*.json` — new code should not decrease CLI or plugin coverage
|
||||
- The `.claude/skills` symlink points to `.agents/skills` — both paths resolve to the same content
|
||||
|
|
|
|||
8
Makefile
8
Makefile
|
|
@ -1,4 +1,4 @@
|
|||
.PHONY: check lint format lint-ts format-ts check-installer-hash docs docs-strict docs-live docs-clean
|
||||
.PHONY: check lint format format-biome lint-ts format-ts check-installer-hash docs docs-strict docs-live docs-clean
|
||||
|
||||
check:
|
||||
npx prek run --all-files
|
||||
|
|
@ -10,10 +10,10 @@ lint: check
|
|||
lint-ts:
|
||||
cd nemoclaw && npm run check
|
||||
|
||||
format: format-ts format-cli
|
||||
format: format-biome
|
||||
|
||||
format-cli:
|
||||
npx prettier --write 'bin/**/*.js' 'test/**/*.js'
|
||||
format-biome:
|
||||
npx biome format --write .
|
||||
|
||||
format-ts:
|
||||
cd nemoclaw && npm run lint:fix && npm run format
|
||||
|
|
|
|||
152
biome.json
Normal file
152
biome.json
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": [
|
||||
"biome.json",
|
||||
"commitlint.config.js",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"nemoclaw/package.json",
|
||||
"nemoclaw/package-lock.json",
|
||||
"src/**/*.ts",
|
||||
"bin/**/*.js",
|
||||
"scripts/**/*.js",
|
||||
"scripts/**/*.ts",
|
||||
"test/**/*.js",
|
||||
"docs/_ext/**/*.js",
|
||||
"nemoclaw/src/**/*.ts",
|
||||
"!dist",
|
||||
"!nemoclaw/dist",
|
||||
"!node_modules",
|
||||
"!nemoclaw/node_modules",
|
||||
"!docs/_build"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100,
|
||||
"lineEnding": "lf"
|
||||
},
|
||||
"javascript": {
|
||||
"globals": [
|
||||
"Buffer",
|
||||
"CustomEvent",
|
||||
"DOMParser",
|
||||
"DocumentLoader",
|
||||
"Element",
|
||||
"Event",
|
||||
"HTMLElement",
|
||||
"MutationObserver",
|
||||
"SearchEngine",
|
||||
"SearchPageManager",
|
||||
"URL",
|
||||
"URLSearchParams",
|
||||
"Utils",
|
||||
"__dirname",
|
||||
"__filename",
|
||||
"clearInterval",
|
||||
"clearTimeout",
|
||||
"console",
|
||||
"document",
|
||||
"exports",
|
||||
"fetch",
|
||||
"lunr",
|
||||
"module",
|
||||
"process",
|
||||
"require",
|
||||
"setInterval",
|
||||
"setTimeout",
|
||||
"window"
|
||||
],
|
||||
"formatter": {
|
||||
"quoteStyle": "double",
|
||||
"semicolons": "always",
|
||||
"trailingCommas": "all"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"formatter": {
|
||||
"trailingCommas": "none"
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": false,
|
||||
"correctness": {
|
||||
"noUndeclaredVariables": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": [
|
||||
"bin/**/*.js",
|
||||
"commitlint.config.js",
|
||||
"scripts/**/*.js",
|
||||
"test/**/*.js",
|
||||
"docs/_ext/**/*.js"
|
||||
],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"noUnusedVariables": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["src/**/*.ts"],
|
||||
"formatter": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["docs/_ext/**/*.js"],
|
||||
"formatter": {
|
||||
"enabled": false
|
||||
},
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"noUnusedVariables": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["nemoclaw/src/**/*.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"complexity": {
|
||||
"useOptionalChain": "error"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "error"
|
||||
},
|
||||
"nursery": {
|
||||
"noFloatingPromises": "error",
|
||||
"useExhaustiveSwitchCases": "error",
|
||||
"useNullishCoalescing": "error"
|
||||
},
|
||||
"style": {
|
||||
"noCommonJs": "error",
|
||||
"useExportType": "error",
|
||||
"useImportType": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -7,17 +7,7 @@ module.exports = {
|
|||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"feat",
|
||||
"fix",
|
||||
"docs",
|
||||
"chore",
|
||||
"refactor",
|
||||
"test",
|
||||
"ci",
|
||||
"perf",
|
||||
"merge",
|
||||
],
|
||||
["feat", "fix", "docs", "chore", "refactor", "test", "ci", "perf", "merge"],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,105 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* ESLint rule: no-direct-credential-env
|
||||
*
|
||||
* Flags direct `process.env` access for known provider credential keys
|
||||
* in read context. Use `resolveProviderCredential()` or `getCredential()`
|
||||
* instead, which resolve from both env and ~/.nemoclaw/credentials.json.
|
||||
*
|
||||
* See #2306.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const CREDENTIAL_ENV_KEYS = new Set([
|
||||
"NVIDIA_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"GEMINI_API_KEY",
|
||||
"COMPATIBLE_API_KEY",
|
||||
"COMPATIBLE_ANTHROPIC_API_KEY",
|
||||
]);
|
||||
|
||||
const MESSAGE =
|
||||
"Direct process.env access for provider credentials bypasses credentials.json. " +
|
||||
"Use resolveProviderCredential() or getCredential() instead. See #2306.";
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow direct process.env access for known provider credential keys in read context",
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
noDirectCredentialEnv: MESSAGE,
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
// Only flag reads, not assignments (process.env.KEY = value is OK)
|
||||
if (isAssignmentTarget(node)) return;
|
||||
|
||||
// Must be process.env.SOMETHING or process.env[something]
|
||||
if (!isProcessEnvAccess(node)) return;
|
||||
|
||||
// Static access: process.env.NVIDIA_API_KEY
|
||||
if (!node.computed && node.property.type === "Identifier") {
|
||||
if (CREDENTIAL_ENV_KEYS.has(node.property.name)) {
|
||||
context.report({ node, messageId: "noDirectCredentialEnv" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Static computed access: process.env["NVIDIA_API_KEY"]
|
||||
if (node.computed && node.property.type === "Literal") {
|
||||
if (
|
||||
typeof node.property.value === "string" &&
|
||||
CREDENTIAL_ENV_KEYS.has(node.property.value)
|
||||
) {
|
||||
context.report({ node, messageId: "noDirectCredentialEnv" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Dynamic access: process.env[credentialEnv]
|
||||
if (node.computed && node.property.type === "Identifier") {
|
||||
if (/credential/i.test(node.property.name)) {
|
||||
context.report({ node, messageId: "noDirectCredentialEnv" });
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if this node is in a write context (assignment target or delete operand).
|
||||
* e.g. process.env.KEY = value → true (assignment)
|
||||
* delete process.env.KEY → true (deletion)
|
||||
* process.env.KEY → false (read context)
|
||||
*/
|
||||
function isAssignmentTarget(node) {
|
||||
const parent = node.parent;
|
||||
if (!parent) return false;
|
||||
if (parent.type === "AssignmentExpression" && parent.left === node) return true;
|
||||
if (parent.type === "UnaryExpression" && parent.operator === "delete") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this MemberExpression is accessing process.env
|
||||
* (i.e., the object is process.env and we're accessing a property of it).
|
||||
*/
|
||||
function isProcessEnvAccess(node) {
|
||||
const obj = node.object;
|
||||
if (obj.type !== "MemberExpression") return false;
|
||||
if (obj.object.type !== "Identifier" || obj.object.name !== "process") return false;
|
||||
if (obj.property.type !== "Identifier" || obj.property.name !== "env") return false;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import js from "@eslint/js";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import { createRequire } from "node:module";
|
||||
const require = createRequire(import.meta.url);
|
||||
const noDirectCredentialEnv = require("./eslint-rules/no-direct-credential-env.js");
|
||||
|
||||
export default [
|
||||
// Ignore build artifacts, vendored code, and the nemoclaw sub-project (has its own config)
|
||||
{
|
||||
ignores: ["nemoclaw/**", "node_modules/**", "dist/**", "docs/_build/**"],
|
||||
},
|
||||
|
||||
// ── bin/ and scripts/ — CommonJS, Node.js ──────────────────────────────
|
||||
{
|
||||
...js.configs.recommended,
|
||||
files: ["bin/**/*.js", "scripts/**/*.js", "commitlint.config.js"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "commonjs",
|
||||
globals: {
|
||||
require: "readonly",
|
||||
module: "readonly",
|
||||
exports: "readonly",
|
||||
__dirname: "readonly",
|
||||
__filename: "readonly",
|
||||
process: "readonly",
|
||||
console: "readonly",
|
||||
Buffer: "readonly",
|
||||
setTimeout: "readonly",
|
||||
clearTimeout: "readonly",
|
||||
setInterval: "readonly",
|
||||
clearInterval: "readonly",
|
||||
URL: "readonly",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" },
|
||||
],
|
||||
// Cyclomatic complexity — ratchet down to 15 as we refactor suppressed functions
|
||||
complexity: ["error", { max: 20 }],
|
||||
},
|
||||
},
|
||||
|
||||
// ── test/ — ESM, Node.js (vitest globals come from imports) ────────────
|
||||
{
|
||||
...js.configs.recommended,
|
||||
files: ["test/**/*.js"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
// Tests are ESM but some test CJS modules via require()/require.cache
|
||||
require: "readonly",
|
||||
process: "readonly",
|
||||
console: "readonly",
|
||||
Buffer: "readonly",
|
||||
setTimeout: "readonly",
|
||||
clearTimeout: "readonly",
|
||||
setInterval: "readonly",
|
||||
clearInterval: "readonly",
|
||||
URL: "readonly",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// ── test/ — TypeScript syntax, Node.js runtime ─────────────────────────
|
||||
{
|
||||
...js.configs.recommended,
|
||||
files: ["test/**/*.ts"],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
require: "readonly",
|
||||
process: "readonly",
|
||||
console: "readonly",
|
||||
Buffer: "readonly",
|
||||
setTimeout: "readonly",
|
||||
clearTimeout: "readonly",
|
||||
setInterval: "readonly",
|
||||
clearInterval: "readonly",
|
||||
URL: "readonly",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
"no-unused-vars": "off",
|
||||
},
|
||||
},
|
||||
|
||||
// ── src/lib/onboard.ts — credential env guard (#2306) ───────────────────
|
||||
{
|
||||
files: ["src/lib/onboard.ts"],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: {
|
||||
nemoclaw: {
|
||||
rules: { "no-direct-credential-env": noDirectCredentialEnv },
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"nemoclaw/no-direct-credential-env": "error",
|
||||
},
|
||||
},
|
||||
|
||||
// ── docs/ — browser JS ─────────────────────────────────────────────────
|
||||
{
|
||||
...js.configs.recommended,
|
||||
files: ["docs/**/*.js"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "script",
|
||||
globals: {
|
||||
window: "readonly",
|
||||
document: "readonly",
|
||||
console: "readonly",
|
||||
fetch: "readonly",
|
||||
URL: "readonly",
|
||||
setTimeout: "readonly",
|
||||
clearTimeout: "readonly",
|
||||
CustomEvent: "readonly",
|
||||
HTMLElement: "readonly",
|
||||
Element: "readonly",
|
||||
Event: "readonly",
|
||||
MutationObserver: "readonly",
|
||||
DOMParser: "readonly",
|
||||
URLSearchParams: "readonly",
|
||||
lunr: "readonly",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import tseslint from "@typescript-eslint/eslint-plugin";
|
||||
import tsparser from "@typescript-eslint/parser";
|
||||
import prettier from "eslint-config-prettier";
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ["src/**/*.ts"],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: {
|
||||
projectService: {
|
||||
allowDefaultProject: ["src/*.test.ts", "src/*/*.test.ts"],
|
||||
},
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tseslint,
|
||||
},
|
||||
rules: {
|
||||
...tseslint.configs["strict-type-checked"]?.rules,
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ argsIgnorePattern: "^_" },
|
||||
],
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/consistent-type-exports": "error",
|
||||
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
||||
"@typescript-eslint/prefer-nullish-coalescing": [
|
||||
"error",
|
||||
{ ignorePrimitives: { string: true } },
|
||||
],
|
||||
"@typescript-eslint/prefer-optional-chain": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.test.ts"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/unbound-method": "off",
|
||||
},
|
||||
},
|
||||
prettier,
|
||||
];
|
||||
1573
nemoclaw/package-lock.json
generated
1573
nemoclaw/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -14,10 +14,10 @@
|
|||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
||||
"format": "prettier --write 'src/**/*.ts'",
|
||||
"format:check": "prettier --check 'src/**/*.ts'",
|
||||
"lint": "biome lint src",
|
||||
"lint:fix": "biome lint --write src",
|
||||
"format": "biome format --write src",
|
||||
"format:check": "biome format src",
|
||||
"check": "npm run lint && npm run format:check && tsc --noEmit",
|
||||
"clean": "rm -rf dist/"
|
||||
},
|
||||
|
|
@ -29,12 +29,8 @@
|
|||
"yaml": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.14",
|
||||
"@types/node": "^22.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
||||
"@typescript-eslint/parser": "^8.57.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.4.0",
|
||||
"vitest": "^4.1.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,8 +24,9 @@ vi.mock("node:fs", async (importOriginal) => {
|
|||
};
|
||||
});
|
||||
|
||||
const { getNetworkEntries, getPrivateNetworks, isPrivateHostname, resetCache } =
|
||||
await import("./private-networks.js");
|
||||
const { getNetworkEntries, getPrivateNetworks, isPrivateHostname, resetCache } = await import(
|
||||
"./private-networks.js"
|
||||
);
|
||||
|
||||
const VALID_YAML = `
|
||||
ipv4:
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ vi.mock("execa", () => ({
|
|||
}));
|
||||
|
||||
vi.mock("./ssrf.js", () => ({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
validateEndpointUrl: vi.fn(async (url: string) => ({ url, pinnedUrl: url })),
|
||||
}));
|
||||
|
||||
|
|
@ -729,14 +728,17 @@ describe("runner", () => {
|
|||
|
||||
// ── Path traversal rejection ──────────────────────────────────
|
||||
|
||||
it.each(["../../etc", "../tmp", "valid.with.dots", "foo\x00bar", "/absolute/path"])(
|
||||
"rejects malicious run ID: %j",
|
||||
(rid) => {
|
||||
expect(() => {
|
||||
actionStatus(rid);
|
||||
}).toThrow(/Invalid run ID/);
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
"../../etc",
|
||||
"../tmp",
|
||||
"valid.with.dots",
|
||||
"foo\x00bar",
|
||||
"/absolute/path",
|
||||
])("rejects malicious run ID: %j", (rid) => {
|
||||
expect(() => {
|
||||
actionStatus(rid);
|
||||
}).toThrow(/Invalid run ID/);
|
||||
});
|
||||
|
||||
it("accepts a legitimate hyphenated run ID", () => {
|
||||
const rid = "nc-20260406-abc12345";
|
||||
|
|
@ -801,12 +803,16 @@ describe("runner", () => {
|
|||
|
||||
// ── Path traversal rejection ──────────────────────────────────
|
||||
|
||||
it.each(["../../etc", "../tmp", "valid.with.dots", "foo\x00bar", "/absolute/path", ""])(
|
||||
"rejects malicious run ID: %j",
|
||||
async (rid) => {
|
||||
await expect(actionRollback(rid)).rejects.toThrow(/Invalid run ID/);
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
"../../etc",
|
||||
"../tmp",
|
||||
"valid.with.dots",
|
||||
"foo\x00bar",
|
||||
"/absolute/path",
|
||||
"",
|
||||
])("rejects malicious run ID: %j", async (rid) => {
|
||||
await expect(actionRollback(rid)).rejects.toThrow(/Invalid run ID/);
|
||||
});
|
||||
|
||||
it("defaults sandbox_name to 'openclaw' when not in plan", async () => {
|
||||
const runDir = `${RUNS_DIR}/nc-run-1`;
|
||||
|
|
|
|||
|
|
@ -1490,16 +1490,17 @@ describe("commands/migration-state", () => {
|
|||
}
|
||||
};
|
||||
|
||||
it.each(["__proto__", "constructor", "prototype"])(
|
||||
"rejects unsafe path segment: %s",
|
||||
(segment) => {
|
||||
const doc: Record<string, unknown> = {};
|
||||
expect(() => {
|
||||
setConfigValue(doc, `${segment}.polluted`, "true");
|
||||
}).toThrow(/Unsafe config path segment/);
|
||||
expectPrototypeClean();
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
"__proto__",
|
||||
"constructor",
|
||||
"prototype",
|
||||
])("rejects unsafe path segment: %s", (segment) => {
|
||||
const doc: Record<string, unknown> = {};
|
||||
expect(() => {
|
||||
setConfigValue(doc, `${segment}.polluted`, "true");
|
||||
}).toThrow(/Unsafe config path segment/);
|
||||
expectPrototypeClean();
|
||||
});
|
||||
|
||||
it("rejects __proto__ in nested position", () => {
|
||||
const doc: Record<string, unknown> = {};
|
||||
|
|
@ -1509,16 +1510,16 @@ describe("commands/migration-state", () => {
|
|||
expectPrototypeClean();
|
||||
});
|
||||
|
||||
it.each(["foo.prototype.bar", "foo.constructor.bar"])(
|
||||
"rejects unsafe segment in nested path: %s",
|
||||
(configPath) => {
|
||||
const doc: Record<string, unknown> = {};
|
||||
expect(() => {
|
||||
setConfigValue(doc, configPath, "true");
|
||||
}).toThrow(/Unsafe config path segment/);
|
||||
expectPrototypeClean();
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
"foo.prototype.bar",
|
||||
"foo.constructor.bar",
|
||||
])("rejects unsafe segment in nested path: %s", (configPath) => {
|
||||
const doc: Record<string, unknown> = {};
|
||||
expect(() => {
|
||||
setConfigValue(doc, configPath, "true");
|
||||
}).toThrow(/Unsafe config path segment/);
|
||||
expectPrototypeClean();
|
||||
});
|
||||
|
||||
it("allows legitimate dotted paths", () => {
|
||||
const doc: Record<string, unknown> = {};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { parsePort } from "./ports.js";
|
|||
const ENV_KEY = "TEST_PLUGIN_PORT";
|
||||
|
||||
function clearEnv(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete process.env[ENV_KEY];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -190,7 +190,6 @@ describe("before_tool_call secret scanner hook (#1233)", () => {
|
|||
const onCalls = vi.mocked(api.on).mock.calls;
|
||||
const hookCall = onCalls.find(([name]) => name === "before_tool_call");
|
||||
expect(hookCall).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by expect above
|
||||
return hookCall![1];
|
||||
}
|
||||
|
||||
|
|
|
|||
1059
package-lock.json
generated
1059
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
|
@ -16,10 +16,11 @@
|
|||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write 'bin/**/*.js' 'scripts/**/*.ts' 'test/**/*.{js,ts}'",
|
||||
"format:check": "prettier --check 'bin/**/*.js' 'scripts/**/*.ts' 'test/**/*.{js,ts}'",
|
||||
"lint": "biome lint . && npm run check:credential-env",
|
||||
"lint:fix": "biome lint --write . && npm run check:credential-env",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome format .",
|
||||
"check:credential-env": "tsx scripts/check-direct-credential-env.ts src/lib/onboard.ts",
|
||||
"typecheck": "tsc -p jsconfig.json",
|
||||
"build:cli": "tsc -p tsconfig.src.json && tsc -p nemoclaw-blueprint/tsconfig.json",
|
||||
"typecheck:cli": "tsc -p tsconfig.cli.json",
|
||||
|
|
@ -64,17 +65,14 @@
|
|||
"url": "https://github.com/NVIDIA/NemoClaw.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.14",
|
||||
"@commitlint/cli": "^20.5.0",
|
||||
"@commitlint/config-conventional": "^20.5.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@j178/prek": "^0.3.6",
|
||||
"@types/node": "^25.5.2",
|
||||
"@typescript-eslint/parser": "^8.58.1",
|
||||
"@vitest/coverage-v8": "^4.1.0",
|
||||
"ajv": "^8.17.0",
|
||||
"eslint": "^10.1.0",
|
||||
"execa": "^9.6.1",
|
||||
"prettier": "^3.8.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vitest": "^4.1.0"
|
||||
|
|
|
|||
244
scripts/check-direct-credential-env.ts
Normal file
244
scripts/check-direct-credential-env.ts
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Guards src/lib/onboard.ts against direct reads of provider credential env vars.
|
||||
*
|
||||
* Direct `process.env.NVIDIA_API_KEY`-style reads bypass credentials.json. Use
|
||||
* resolveProviderCredential() or getCredential() for credential resolution unless
|
||||
* a narrowly-scoped raw env check is intentional and explicitly suppressed.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as ts from "typescript";
|
||||
|
||||
const CREDENTIAL_ENV_KEYS = new Set([
|
||||
"NVIDIA_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"GEMINI_API_KEY",
|
||||
"COMPATIBLE_API_KEY",
|
||||
"COMPATIBLE_ANTHROPIC_API_KEY",
|
||||
]);
|
||||
|
||||
const MESSAGE =
|
||||
"Direct process.env access for provider credentials bypasses credentials.json. " +
|
||||
"Use resolveProviderCredential() or getCredential() instead. See #2306.";
|
||||
|
||||
const SUPPRESSION_TOKEN_PATTERN =
|
||||
/\b(?:check-direct-credential-env-ignore|no-direct-credential-env)\b/;
|
||||
const COMMENT_LINE_PREFIX_PATTERN = /^\s*(?:\/\/|\/\*|\*)/;
|
||||
|
||||
export interface DirectCredentialEnvViolation {
|
||||
filePath: string;
|
||||
line: number;
|
||||
column: number;
|
||||
key: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function findDirectCredentialEnvReads(
|
||||
sourceText: string,
|
||||
filePath = "source.ts",
|
||||
): DirectCredentialEnvViolation[] {
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
scriptKindForPath(filePath),
|
||||
);
|
||||
const violations: DirectCredentialEnvViolation[] = [];
|
||||
|
||||
function visit(node: ts.Node): void {
|
||||
const credentialKey = credentialKeyForProcessEnvAccess(node);
|
||||
if (
|
||||
credentialKey &&
|
||||
!isAssignmentOrDeleteTarget(node) &&
|
||||
!hasSuppressionComment(sourceFile, node)
|
||||
) {
|
||||
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
||||
violations.push({
|
||||
filePath,
|
||||
line: position.line + 1,
|
||||
column: position.character + 1,
|
||||
key: credentialKey,
|
||||
text: node.getText(sourceFile),
|
||||
});
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
return violations;
|
||||
}
|
||||
|
||||
export function checkFiles(filePaths: readonly string[]): DirectCredentialEnvViolation[] {
|
||||
return filePaths.flatMap((filePath) =>
|
||||
findDirectCredentialEnvReads(readFileSync(filePath, "utf-8"), filePath),
|
||||
);
|
||||
}
|
||||
|
||||
export function formatViolations(violations: readonly DirectCredentialEnvViolation[]): string {
|
||||
return violations
|
||||
.map(
|
||||
(violation) =>
|
||||
`${violation.filePath}:${violation.line}:${violation.column} ${MESSAGE} (${violation.text})`,
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function credentialKeyForProcessEnvAccess(node: ts.Node): string | null {
|
||||
if (ts.isPropertyAccessExpression(node) && isProcessEnvExpression(node.expression)) {
|
||||
return CREDENTIAL_ENV_KEYS.has(node.name.text) ? node.name.text : null;
|
||||
}
|
||||
|
||||
if (!ts.isElementAccessExpression(node) || !isProcessEnvExpression(node.expression)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const argument = stripParentheses(node.argumentExpression);
|
||||
if (ts.isStringLiteral(argument) || ts.isNoSubstitutionTemplateLiteral(argument)) {
|
||||
return CREDENTIAL_ENV_KEYS.has(argument.text) ? argument.text : null;
|
||||
}
|
||||
|
||||
if (ts.isIdentifier(argument) && /credential/i.test(argument.text)) {
|
||||
return `[${argument.text}]`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isProcessEnvExpression(expression: ts.Expression): boolean {
|
||||
return (
|
||||
ts.isPropertyAccessExpression(expression) &&
|
||||
ts.isIdentifier(expression.expression) &&
|
||||
expression.expression.text === "process" &&
|
||||
expression.name.text === "env"
|
||||
);
|
||||
}
|
||||
|
||||
function isAssignmentOrDeleteTarget(node: ts.Node): boolean {
|
||||
let current = node;
|
||||
let parent = node.parent;
|
||||
|
||||
while (
|
||||
parent &&
|
||||
isTransparentExpressionWrapper(parent) &&
|
||||
getWrappedExpression(parent) === current
|
||||
) {
|
||||
current = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return (
|
||||
(parent !== undefined &&
|
||||
ts.isBinaryExpression(parent) &&
|
||||
parent.left === current &&
|
||||
isAssignmentOperator(parent.operatorToken.kind)) ||
|
||||
(parent !== undefined && ts.isDeleteExpression(parent) && parent.expression === current)
|
||||
);
|
||||
}
|
||||
|
||||
function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
|
||||
return (
|
||||
kind === ts.SyntaxKind.EqualsToken ||
|
||||
kind === ts.SyntaxKind.PlusEqualsToken ||
|
||||
kind === ts.SyntaxKind.MinusEqualsToken ||
|
||||
kind === ts.SyntaxKind.AsteriskEqualsToken ||
|
||||
kind === ts.SyntaxKind.AsteriskAsteriskEqualsToken ||
|
||||
kind === ts.SyntaxKind.SlashEqualsToken ||
|
||||
kind === ts.SyntaxKind.PercentEqualsToken ||
|
||||
kind === ts.SyntaxKind.LessThanLessThanEqualsToken ||
|
||||
kind === ts.SyntaxKind.GreaterThanGreaterThanEqualsToken ||
|
||||
kind === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken ||
|
||||
kind === ts.SyntaxKind.AmpersandEqualsToken ||
|
||||
kind === ts.SyntaxKind.BarEqualsToken ||
|
||||
kind === ts.SyntaxKind.CaretEqualsToken ||
|
||||
kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken ||
|
||||
kind === ts.SyntaxKind.BarBarEqualsToken ||
|
||||
kind === ts.SyntaxKind.QuestionQuestionEqualsToken
|
||||
);
|
||||
}
|
||||
|
||||
function isTransparentExpressionWrapper(node: ts.Node): boolean {
|
||||
return (
|
||||
ts.isParenthesizedExpression(node) ||
|
||||
ts.isNonNullExpression(node) ||
|
||||
ts.isAsExpression(node) ||
|
||||
ts.isSatisfiesExpression(node) ||
|
||||
ts.isTypeAssertionExpression(node)
|
||||
);
|
||||
}
|
||||
|
||||
function getWrappedExpression(node: ts.Node): ts.Node | undefined {
|
||||
if (
|
||||
ts.isParenthesizedExpression(node) ||
|
||||
ts.isNonNullExpression(node) ||
|
||||
ts.isAsExpression(node) ||
|
||||
ts.isSatisfiesExpression(node) ||
|
||||
ts.isTypeAssertionExpression(node)
|
||||
) {
|
||||
return node.expression;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function stripParentheses(expression: ts.Expression): ts.Expression {
|
||||
let current = expression;
|
||||
while (ts.isParenthesizedExpression(current)) {
|
||||
current = current.expression;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function hasSuppressionComment(sourceFile: ts.SourceFile, node: ts.Node): boolean {
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
||||
const lines = sourceFile.getFullText().split(/\r?\n/);
|
||||
return [line - 1, line].some((candidate) => {
|
||||
const text = lines[candidate];
|
||||
return (
|
||||
text !== undefined &&
|
||||
COMMENT_LINE_PREFIX_PATTERN.test(text) &&
|
||||
SUPPRESSION_TOKEN_PATTERN.test(text)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function scriptKindForPath(filePath: string): ts.ScriptKind {
|
||||
switch (path.extname(filePath)) {
|
||||
case ".cjs":
|
||||
case ".js":
|
||||
case ".mjs":
|
||||
return ts.ScriptKind.JS;
|
||||
case ".jsx":
|
||||
return ts.ScriptKind.JSX;
|
||||
case ".tsx":
|
||||
return ts.ScriptKind.TSX;
|
||||
default:
|
||||
return ts.ScriptKind.TS;
|
||||
}
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const filePaths = process.argv.slice(2).filter((arg) => arg !== "--");
|
||||
if (filePaths.length === 0) {
|
||||
console.error("Usage: tsx scripts/check-direct-credential-env.ts FILE...");
|
||||
process.exitCode = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
const violations = checkFiles(filePaths);
|
||||
if (violations.length > 0) {
|
||||
console.error(formatViolations(violations));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? "")) {
|
||||
main();
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@ type ManifestValue = ManifestScalar | ManifestRecord | ManifestValue[];
|
|||
type ManifestRecord = { [key: string]: ManifestValue };
|
||||
type StringMap = { [key: string]: string };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const yaml: { load(input: string): unknown } = require("js-yaml");
|
||||
|
||||
export interface AgentHealthProbe {
|
||||
|
|
|
|||
|
|
@ -460,7 +460,6 @@ export function promptSecret(question: string): Promise<string> {
|
|||
|
||||
if (ch === "\u001b") {
|
||||
const rest = text.slice(i);
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const match = rest.match(/^\u001b(?:\[[0-9;?]*[~A-Za-z]|\][^\u0007]*\u0007|.)/);
|
||||
if (match) {
|
||||
i += match[0].length - 1;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
|
||||
const GATEWAY_NAME = "nemoclaw";
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||
|
||||
function stripAnsi(value: string): string {
|
||||
|
|
|
|||
|
|
@ -111,7 +111,6 @@ export function summarizeProbeFailure(body = "", status = 0, curlStatus = 0, std
|
|||
return summarizeProbeError(body, status);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export function runCurlProbe(argv: string[], opts: CurlProbeOptions = {}): CurlProbeResult {
|
||||
const bodyFile = secureTempFile("nemoclaw-curl-probe", ".json");
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -125,7 +125,6 @@ export function getOpenClawPrimaryModel(provider: string, model?: string): strin
|
|||
|
||||
export function parseGatewayInference(output: string | null | undefined): GatewayInference | null {
|
||||
if (!output) return null;
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const stripped = output.replace(/\u001b\[[0-9;]*m/g, "");
|
||||
const lines = stripped.split("\n");
|
||||
let inGateway = false;
|
||||
|
|
|
|||
|
|
@ -9,13 +9,11 @@
|
|||
import type { CurlProbeResult } from "./http-probe";
|
||||
import { runCurlProbe } from "./http-probe";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { shellQuote, runCapture } = require("./runner");
|
||||
|
||||
import { VLLM_PORT, OLLAMA_PORT, OLLAMA_PROXY_PORT } from "./ports";
|
||||
import { sleepSeconds } from "./wait";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { isWsl } = require("./platform");
|
||||
|
||||
/** Port containers use to reach Ollama — proxy on non-WSL, direct on WSL2. */
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { isSafeModelId } from "./validation";
|
|||
import { validateNvidiaEndpointModel } from "./provider-models";
|
||||
|
||||
// credentials.ts still uses CommonJS-style exports.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { getCredential, prompt } = require("./credentials");
|
||||
|
||||
export const BACK_TO_SELECTION = "__NEMOCLAW_BACK_TO_SELECTION__";
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@
|
|||
//
|
||||
// NIM container management — pull, start, stop, health-check NIM images.
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { runCapture } = require("./runner");
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const {
|
||||
dockerContainerInspectFormat,
|
||||
dockerForceRm,
|
||||
|
|
@ -16,9 +14,7 @@ const {
|
|||
dockerRunDetached,
|
||||
dockerStop,
|
||||
} = require("./docker");
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { sleepSeconds } = require("./wait");
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const nimImages = require("../../bin/lib/nim-images.json");
|
||||
|
||||
import { VLLM_PORT } from "./ports";
|
||||
|
|
|
|||
|
|
@ -162,7 +162,6 @@ function probeResponsesToolCalling(endpointUrl, model, apiKey, options = {}) {
|
|||
}
|
||||
|
||||
// ── OpenAI-like probe ────────────────────────────────────────────
|
||||
// eslint-disable-next-line complexity
|
||||
function probeOpenAiLikeEndpoint(endpointUrl, model, apiKey, options = {}) {
|
||||
const useQueryParam = options.authMode === "query-param";
|
||||
const normalizedKey = apiKey ? normalizeCredentialValue(apiKey) : "";
|
||||
|
|
|
|||
|
|
@ -313,7 +313,6 @@ export function createSession(overrides: Partial<Session> = {}): Session {
|
|||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
export function normalizeSession(data: Session | SessionJsonValue | undefined): Session | null {
|
||||
if (!isObject(data) || data.version !== SESSION_VERSION) return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1105,7 +1105,7 @@ function upsertProvider(
|
|||
// openshell receives `--credential <ENV>` and reads the value from the
|
||||
// `env` block passed here, falling back to the inherited process.env.
|
||||
// Use getCredential() for the env-fallback branch (per the
|
||||
// no-direct-credential-env eslint rule from PR #2306) — it mirrors
|
||||
// direct credential env guard from PR #2306) — it mirrors
|
||||
// openshell's resolution order while the staging contract has
|
||||
// already populated the same value into process.env.
|
||||
const upsertedValue = env[credentialEnv] ?? getCredential(credentialEnv);
|
||||
|
|
@ -2762,7 +2762,6 @@ function waitForSandboxReady(sandboxName: string, attempts = 10, delaySeconds =
|
|||
|
||||
// ── Step 1: Preflight ────────────────────────────────────────────
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async function preflight(): Promise<ReturnType<typeof nim.detectGpu>> {
|
||||
step(1, 8, "Preflight checks");
|
||||
|
||||
|
|
@ -3871,7 +3870,6 @@ function formatOnboardConfigSummary({
|
|||
].join("\n");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async function createSandbox(
|
||||
gpu: ReturnType<typeof nim.detectGpu>,
|
||||
model: string,
|
||||
|
|
@ -4864,7 +4862,6 @@ async function createSandbox(
|
|||
|
||||
// ── Step 3: Inference selection ──────────────────────────────────
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
type ProviderChoice = { key: string; label: string };
|
||||
|
||||
function providerNameToOptionKey(
|
||||
|
|
@ -5251,8 +5248,9 @@ async function setupNim(
|
|||
// Check raw process.env first — NEMOCLAW_PROVIDER_KEY is a user-facing
|
||||
// override that should take precedence before resolving from credentials.json.
|
||||
const _nvProviderKey = (process.env.NEMOCLAW_PROVIDER_KEY || "").trim();
|
||||
// eslint-disable-next-line nemoclaw/no-direct-credential-env -- intentional: checking if env is already set before applying NEMOCLAW_PROVIDER_KEY override
|
||||
if (_nvProviderKey && !process.env.NVIDIA_API_KEY) {
|
||||
// check-direct-credential-env-ignore -- intentional: checking if env is already set before applying NEMOCLAW_PROVIDER_KEY override
|
||||
const existingNvidiaKey = normalizeCredentialValue(process.env.NVIDIA_API_KEY ?? "");
|
||||
if (_nvProviderKey && !existingNvidiaKey) {
|
||||
process.env.NVIDIA_API_KEY = _nvProviderKey;
|
||||
}
|
||||
if (isNonInteractive()) {
|
||||
|
|
@ -5290,9 +5288,12 @@ async function setupNim(
|
|||
// isn't already set, use NEMOCLAW_PROVIDER_KEY as the API key for this provider.
|
||||
// Check raw process.env — the override must apply before resolving from credentials.json.
|
||||
const _providerKeyHint = (process.env.NEMOCLAW_PROVIDER_KEY || "").trim();
|
||||
// eslint-disable-next-line nemoclaw/no-direct-credential-env -- intentional: checking if env is already set before applying NEMOCLAW_PROVIDER_KEY override
|
||||
if (_providerKeyHint && credentialEnv && !process.env[credentialEnv]) {
|
||||
process.env[credentialEnv] = _providerKeyHint;
|
||||
if (_providerKeyHint && credentialEnv) {
|
||||
// check-direct-credential-env-ignore -- intentional: checking if env is already set before applying NEMOCLAW_PROVIDER_KEY override
|
||||
const existingCredentialKey = normalizeCredentialValue(process.env[credentialEnv] ?? "");
|
||||
if (!existingCredentialKey) {
|
||||
process.env[credentialEnv] = _providerKeyHint;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNonInteractive()) {
|
||||
|
|
@ -5869,7 +5870,6 @@ async function setupNim(
|
|||
|
||||
// ── Step 4: Inference provider ───────────────────────────────────
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async function setupInference(
|
||||
sandboxName: string | null,
|
||||
model: string,
|
||||
|
|
@ -6516,7 +6516,6 @@ async function setupOpenclaw(sandboxName: string, model: string, provider: strin
|
|||
|
||||
// ── Step 7: Policy presets ───────────────────────────────────────
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async function _setupPolicies(
|
||||
sandboxName: string,
|
||||
options: {
|
||||
|
|
@ -7140,7 +7139,6 @@ function computeSetupPresetSuggestions(
|
|||
return suggestions;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async function setupPoliciesWithSelection(
|
||||
sandboxName: string,
|
||||
options: {
|
||||
|
|
@ -7986,7 +7984,6 @@ function skippedStepMessage(
|
|||
|
||||
// ── Main ─────────────────────────────────────────────────────────
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async function onboard(opts: OnboardOptions = {}): Promise<void> {
|
||||
setOnboardBrandingAgent(opts.agent || process.env.NEMOCLAW_AGENT || null);
|
||||
NON_INTERACTIVE = opts.nonInteractive || process.env.NEMOCLAW_NON_INTERACTIVE === "1";
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ export interface CaptureOpenshellResult {
|
|||
signal?: NodeJS.Signals | null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||
|
||||
export function stripAnsi(value = ""): string {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import path from "node:path";
|
|||
import { DASHBOARD_PORT } from "./ports";
|
||||
|
||||
// runner.ts still uses CommonJS-style exports — use require here.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { runCapture } = require("./runner");
|
||||
|
||||
type RunCaptureFn = typeof import("./runner").runCapture;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { getCurlTimingArgs, runCurlProbe } from "./http-probe";
|
|||
import type { ModelCatalogFetchResult, ModelValidationResult } from "./onboard-types";
|
||||
|
||||
// credentials.ts still uses CommonJS-style exports.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { normalizeCredentialValue } = require("./credentials");
|
||||
|
||||
export const BUILD_ENDPOINT_URL = "https://integrate.api.nvidia.com/v1";
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import { loadSession } from "./onboard-session";
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||
|
||||
function stripAnsi(text: string | null | undefined): string {
|
||||
|
|
|
|||
|
|
@ -116,7 +116,6 @@ export function resolveSkillPaths(
|
|||
|
||||
// Re-export shellQuote from runner.ts — a repo-wide test enforces
|
||||
// a single definition lives in runner.ts.
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { shellQuote } = require("./runner");
|
||||
export { shellQuote };
|
||||
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ function parseJson<T>(text: string): T {
|
|||
|
||||
// Reflect.get is used throughout the codebase as a type-safe alternative to
|
||||
// direct property access on loosely-typed objects. Unlike an `as Record<…>`
|
||||
// cast it never widens the target type and avoids eslint no-unsafe-member-access
|
||||
// warnings. See also: deploy.ts, onboard.ts, ws-proxy-fix.ts.
|
||||
// cast it never widens the target type and keeps loosely-typed member access
|
||||
// explicit. See also: deploy.ts, onboard.ts, ws-proxy-fix.ts.
|
||||
function readStringProperty(value: object | null, key: string): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
|
|
@ -212,11 +212,16 @@ export async function ensureUsageNoticeConsent({
|
|||
}
|
||||
|
||||
// credentials is still CJS
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const ask = promptFn || require("./credentials").prompt;
|
||||
const answer = String(await ask(` ${config.interactivePrompt}`))
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const ask: PromptFn = promptFn ?? (require("./credentials") as { prompt: PromptFn }).prompt;
|
||||
let answer: string;
|
||||
try {
|
||||
answer = String(await ask(` ${config.interactivePrompt}`))
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
} catch {
|
||||
writeLine(" Installation cancelled");
|
||||
return false;
|
||||
}
|
||||
if (answer !== "yes") {
|
||||
writeLine(" Installation cancelled");
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -855,7 +855,6 @@ function printGatewayLifecycleHint(output = "", sandboxName = "", writer = conso
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async function getReconciledSandboxGatewayState(sandboxName: string) {
|
||||
let lookup = getSandboxGatewayState(sandboxName);
|
||||
if (lookup.state === "present") {
|
||||
|
|
@ -1553,7 +1552,6 @@ async function sandboxConnect(sandboxName: string) {
|
|||
exitWithSpawnResult(result);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async function sandboxStatus(sandboxName: string) {
|
||||
const sb = registry.getSandbox(sandboxName);
|
||||
const live = parseGatewayInference(
|
||||
|
|
@ -4158,7 +4156,6 @@ function printConnectOrderHint(candidate: string | null): void {
|
|||
|
||||
const [cmd, ...args] = process.argv.slice(2);
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
(async () => {
|
||||
// No command → help
|
||||
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ if (!certSetup.ok) {
|
|||
`This test must not silently skip in CI — install openssl on the runner.`,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[http-proxy-fix-e2e] skipping locally: ${certSetup.reason}`);
|
||||
}
|
||||
const key = certSetup.ok ? certSetup.key : Buffer.alloc(0);
|
||||
|
|
@ -97,7 +96,6 @@ type CapturedRequest = {
|
|||
|
||||
function loadWrapper() {
|
||||
delete require.cache[FIX_PATH];
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require(FIX_PATH);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ type RewrittenOptions = http.RequestOptions & {
|
|||
function loadWrapper() {
|
||||
// Clear cached copies so the IIFE re-runs and reads our test env.
|
||||
delete require.cache[FIX_PATH];
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require(FIX_PATH);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,136 +2,101 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Tests for the no-direct-credential-env ESLint rule.
|
||||
* Tests for the direct credential env guard.
|
||||
*
|
||||
* Verifies that the rule flags direct process.env reads for known
|
||||
* credential keys while allowing assignments, deletions, and
|
||||
* non-credential keys.
|
||||
* Verifies that the guard flags direct process.env reads for known credential
|
||||
* keys while allowing assignments, deletions, suppressions, and non-credential
|
||||
* keys.
|
||||
*
|
||||
* See #2306.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { RuleTester } from "eslint";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { findDirectCredentialEnvReads } from "../scripts/check-direct-credential-env";
|
||||
|
||||
// Import the CJS rule via dynamic import
|
||||
const rulePath = path.join(import.meta.dirname, "..", "eslint-rules", "no-direct-credential-env.js");
|
||||
const rule = (await import(pathToFileURL(rulePath).href)).default;
|
||||
describe("direct credential env guard", () => {
|
||||
it.each([
|
||||
// Assignments (write context) — allowed
|
||||
'process.env.NVIDIA_API_KEY = "test";',
|
||||
"process.env.OPENAI_API_KEY = value;",
|
||||
"process.env[credentialEnv] = providerKey;",
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
},
|
||||
});
|
||||
// Deletions (write context) — allowed
|
||||
"delete process.env.NVIDIA_API_KEY;",
|
||||
"delete process.env.ANTHROPIC_API_KEY;",
|
||||
|
||||
describe("ESLint rule: nemoclaw/no-direct-credential-env", () => {
|
||||
it("flags and allows the expected patterns", () => {
|
||||
ruleTester.run("no-direct-credential-env", rule, {
|
||||
valid: [
|
||||
// Assignments (write context) — allowed
|
||||
{ code: 'process.env.NVIDIA_API_KEY = "test";' },
|
||||
{ code: 'process.env.OPENAI_API_KEY = value;' },
|
||||
{ code: "process.env[credentialEnv] = providerKey;" },
|
||||
// Non-credential env vars — allowed
|
||||
"const x = process.env.NEMOCLAW_MODEL;",
|
||||
"const x = process.env.HOME;",
|
||||
"const x = process.env.PATH;",
|
||||
|
||||
// Deletions (write context) — allowed
|
||||
{ code: "delete process.env.NVIDIA_API_KEY;" },
|
||||
{ code: "delete process.env.ANTHROPIC_API_KEY;" },
|
||||
// NEMOCLAW_PROVIDER_KEY is a user-facing override, not credential resolution.
|
||||
"const x = process.env.NEMOCLAW_PROVIDER_KEY;",
|
||||
|
||||
// Non-credential env vars — allowed
|
||||
{ code: "const x = process.env.NEMOCLAW_MODEL;" },
|
||||
{ code: "const x = process.env.HOME;" },
|
||||
{ code: "const x = process.env.PATH;" },
|
||||
// Correct patterns — allowed
|
||||
'const key = getCredential("NVIDIA_API_KEY");',
|
||||
'const key = resolveProviderCredential("NVIDIA_API_KEY");',
|
||||
|
||||
// NEMOCLAW_PROVIDER_KEY is a user-facing override, not credential resolution
|
||||
{ code: "const x = process.env.NEMOCLAW_PROVIDER_KEY;" },
|
||||
// Bracketed string-literal assignments — allowed
|
||||
'process.env["NVIDIA_API_KEY"] = "test";',
|
||||
|
||||
// Correct patterns — allowed
|
||||
{ code: 'const key = getCredential("NVIDIA_API_KEY");' },
|
||||
{ code: 'const key = resolveProviderCredential("NVIDIA_API_KEY");' },
|
||||
// Dynamic access with non-credential variable name — allowed
|
||||
"const x = process.env[someKey];",
|
||||
"const x = process.env[envName];",
|
||||
|
||||
// Bracketed string-literal assignments — allowed
|
||||
{ code: 'process.env["NVIDIA_API_KEY"] = "test";' },
|
||||
|
||||
// Dynamic access with non-credential variable name — allowed
|
||||
{ code: "const x = process.env[someKey];" },
|
||||
{ code: "const x = process.env[envName];" },
|
||||
],
|
||||
|
||||
invalid: [
|
||||
// Static reads of known credential keys
|
||||
{
|
||||
code: "const key = process.env.NVIDIA_API_KEY;",
|
||||
errors: [{ messageId: "noDirectCredentialEnv" }],
|
||||
},
|
||||
{
|
||||
code: "const key = process.env.OPENAI_API_KEY;",
|
||||
errors: [{ messageId: "noDirectCredentialEnv" }],
|
||||
},
|
||||
{
|
||||
code: "const key = process.env.ANTHROPIC_API_KEY;",
|
||||
errors: [{ messageId: "noDirectCredentialEnv" }],
|
||||
},
|
||||
{
|
||||
code: "const key = process.env.GEMINI_API_KEY;",
|
||||
errors: [{ messageId: "noDirectCredentialEnv" }],
|
||||
},
|
||||
{
|
||||
code: "const key = process.env.COMPATIBLE_API_KEY;",
|
||||
errors: [{ messageId: "noDirectCredentialEnv" }],
|
||||
},
|
||||
{
|
||||
code: "const key = process.env.COMPATIBLE_ANTHROPIC_API_KEY;",
|
||||
errors: [{ messageId: "noDirectCredentialEnv" }],
|
||||
},
|
||||
|
||||
// Conditional check (read context)
|
||||
{
|
||||
code: "if (!process.env.NVIDIA_API_KEY) {}",
|
||||
errors: [{ messageId: "noDirectCredentialEnv" }],
|
||||
},
|
||||
|
||||
// Bracketed string-literal reads
|
||||
{
|
||||
code: 'const key = process.env["NVIDIA_API_KEY"];',
|
||||
errors: [{ messageId: "noDirectCredentialEnv" }],
|
||||
},
|
||||
{
|
||||
code: 'if (!process.env["OPENAI_API_KEY"]) {}',
|
||||
errors: [{ messageId: "noDirectCredentialEnv" }],
|
||||
},
|
||||
|
||||
// Dynamic read with credential-containing variable name
|
||||
{
|
||||
code: "if (!process.env[credentialEnv]) {}",
|
||||
errors: [{ messageId: "noDirectCredentialEnv" }],
|
||||
},
|
||||
{
|
||||
code: "const x = process.env[resolvedCredentialEnv];",
|
||||
errors: [{ messageId: "noDirectCredentialEnv" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
// Explicitly suppressed raw-env reads — allowed
|
||||
"// check-direct-credential-env-ignore -- raw env check required\nconst key = process.env.NVIDIA_API_KEY;",
|
||||
"// no-direct-credential-env -- backward-compatible suppression\nconst key = process.env.NVIDIA_API_KEY;",
|
||||
])("allows %s", (code) => {
|
||||
expect(findDirectCredentialEnvReads(code)).toEqual([]);
|
||||
});
|
||||
|
||||
it("onboard.ts has zero violations (Phase 1 already fixed all patterns)", async () => {
|
||||
const { spawnSync } = await import("node:child_process");
|
||||
it.each([
|
||||
// Static reads of known credential keys
|
||||
["const key = process.env.NVIDIA_API_KEY;", "NVIDIA_API_KEY"],
|
||||
["const key = process.env.OPENAI_API_KEY;", "OPENAI_API_KEY"],
|
||||
["const key = process.env.ANTHROPIC_API_KEY;", "ANTHROPIC_API_KEY"],
|
||||
["const key = process.env.GEMINI_API_KEY;", "GEMINI_API_KEY"],
|
||||
["const key = process.env.COMPATIBLE_API_KEY;", "COMPATIBLE_API_KEY"],
|
||||
[
|
||||
"const key = process.env.COMPATIBLE_ANTHROPIC_API_KEY;",
|
||||
"COMPATIBLE_ANTHROPIC_API_KEY",
|
||||
],
|
||||
|
||||
// Conditional check (read context)
|
||||
["if (!process.env.NVIDIA_API_KEY) {}", "NVIDIA_API_KEY"],
|
||||
|
||||
// Bracketed string-literal reads
|
||||
['const key = process.env["NVIDIA_API_KEY"];', "NVIDIA_API_KEY"],
|
||||
['if (!process.env["OPENAI_API_KEY"]) {}', "OPENAI_API_KEY"],
|
||||
|
||||
// Dynamic read with credential-containing variable name
|
||||
["if (!process.env[credentialEnv]) {}", "[credentialEnv]"],
|
||||
["const x = process.env[resolvedCredentialEnv];", "[resolvedCredentialEnv]"],
|
||||
|
||||
// Suppression token inside non-comment text must not suppress.
|
||||
[
|
||||
"const marker = 'no-direct-credential-env';\nconst key = process.env.NVIDIA_API_KEY;",
|
||||
"NVIDIA_API_KEY",
|
||||
],
|
||||
])("flags %s", (code, key) => {
|
||||
expect(findDirectCredentialEnvReads(code)).toMatchObject([{ key }]);
|
||||
});
|
||||
|
||||
it("onboard.ts has zero violations", () => {
|
||||
const repoRoot = path.join(import.meta.dirname, "..");
|
||||
const result = spawnSync(
|
||||
"npx",
|
||||
["eslint", "src/lib/onboard.ts", "--format", "json"],
|
||||
["tsx", "scripts/check-direct-credential-env.ts", "src/lib/onboard.ts"],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: "utf-8",
|
||||
timeout: 60_000,
|
||||
},
|
||||
);
|
||||
const output = JSON.parse(result.stdout);
|
||||
const violations = output[0].messages.filter(
|
||||
(m: any) => m.ruleId === "nemoclaw/no-direct-credential-env",
|
||||
);
|
||||
expect(violations).toHaveLength(0);
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3515,7 +3515,6 @@ const { createSandbox } = require(${onboardPath});
|
|||
// Without this, a CHAT_UI_URL set in the developer's shell or CI would be
|
||||
// inherited, causing chatUiUrl to use the wrong port and making the forward
|
||||
// command assertion below fail spuriously.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { CHAT_UI_URL: _stripped, ...inheritedEnv } = process.env;
|
||||
const result = spawnSync(process.execPath, [scriptPath], {
|
||||
cwd: repoRoot,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,20 @@ describe("usage notice", () => {
|
|||
expect(lines.join("\n")).toContain("Installation cancelled");
|
||||
});
|
||||
|
||||
it("cancels interactive onboarding when the prompt fails", async () => {
|
||||
const lines: string[] = [];
|
||||
const ok = await ensureUsageNoticeConsent({
|
||||
nonInteractive: false,
|
||||
promptFn: async () => {
|
||||
throw new Error("prompt failed");
|
||||
},
|
||||
writeLine: (line: string) => lines.push(line),
|
||||
});
|
||||
|
||||
expect(ok).toBe(false);
|
||||
expect(lines.join("\n")).toContain("Installation cancelled");
|
||||
});
|
||||
|
||||
it("records interactive acceptance when the user types yes", async () => {
|
||||
const config = loadUsageNoticeConfig();
|
||||
const ok = await ensureUsageNoticeConsent({
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ describe("isDangerousHost", () => {
|
|||
});
|
||||
|
||||
it.each([undefined, null, 42, {}, []])("returns false for non-string %s", (v) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
expect(isDangerousHost(v as any)).toBe(false);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue