Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ Use these NemoClaw precedents for durable evidence shape, not as inherited concl
- `docs/security/openclaw-2026.6.10-dependency-review.md` and
`test/openclaw-dependency-review.test.ts` for a tracked dependency review with contract tests;
- `docs/security/openshell-0.0.72-compatibility-review.mdx` for a runtime compatibility boundary;
- `scripts/checks/dependency-pins.ts` and `test/dependency-pins-check.test.ts` for selector
- `scripts/checks/dependency-pins.mts` and `test/dependency-pins-check.test.ts` for selector
coherence; and
- `scripts/check-installer-hash.sh` and `test/installer-hash-check.test.ts` for independently
trusted release manifests and consumed artifacts.
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ jobs:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
persist-credentials: false

- name: Checkout trusted CI actions
Expand Down Expand Up @@ -340,7 +341,7 @@ jobs:
# covered without executing a mutable replacement action.
- name: Validate changed live E2E mock parity (bootstrap)
if: ${{ steps.trusted-shard-capabilities.outputs.e2e-support != 'true' && matrix.shard == 1 }}
run: npx tsx scripts/checks/e2e-mock-parity.ts --base HEAD^1 --head HEAD^2
run: npx tsx scripts/checks/e2e-mock-parity.mts --base HEAD^1 --head HEAD^2

- name: Run E2E support shard (bootstrap)
if: ${{ steps.trusted-shard-capabilities.outputs.e2e-support != 'true' }}
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@
"test:coverage:cli": "npm run clean:cli && npm run build:cli && tsx scripts/check-dist-sourcemaps.mts dist && vitest run --project cli --project integration --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary --coverage.reportsDirectory=coverage/cli --coverage.include=\"bin/**/*.js\" --coverage.include=\"src/**/*.ts\" --coverage.exclude=\"test/**/*.js\" --coverage.exclude=\"test/**/*.ts\" && tsx scripts/check-coverage-ratchet.mts coverage/cli/coverage-summary.json ci/coverage-threshold-cli.json \"CLI coverage\"",
"test:coverage:plugin": "vitest run --project plugin --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary --coverage.reportsDirectory=coverage/plugin --coverage.include=\"nemoclaw/src/**/*.ts\" --coverage.include=\"nemoclaw/src/**/*.cts\" --coverage.exclude=\"**/*.test.ts\" && tsx scripts/check-coverage-ratchet.mts coverage/plugin/coverage-summary.json ci/coverage-threshold-plugin.json \"Plugin coverage\"",
"test:live-e2e": "npm run clean:cli && npm run build:cli && NEMOCLAW_RUN_LIVE_E2E=1 vitest run --project e2e-live",
"test:imports:check": "tsx scripts/checks/no-test-dist-imports.ts",
"test:projects:check": "tsx scripts/checks/vitest-project-overlap.ts",
"test:titles:check": "tsx scripts/checks/test-title-style.ts",
"test:imports:check": "tsx scripts/checks/no-test-dist-imports.mts",
"test:projects:check": "tsx scripts/checks/vitest-project-overlap.mts",
"test:titles:check": "tsx scripts/checks/test-title-style.mts",
"bench": "tsx scripts/bench/run.mts",
"check": "npx prek run --all-files --stage pre-commit && npx prek run --all-files --stage manual",
"check:diff": "npx prek run --from-ref origin/main --to-ref HEAD --stage pre-commit && npx commitlint --from origin/main --to HEAD && npx prek run --from-ref origin/main --to-ref HEAD --stage pre-push",
"checks": "tsx scripts/checks/run.ts",
"checks": "tsx scripts/checks/run.mts",
"lint": "npx @biomejs/biome lint . && npm run checks",
"lint:fix": "npx @biomejs/biome lint --write . && npm run checks",
"lint:ts": "cd nemoclaw && npm run check",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ function deriveDependencyPins(rootDir: string = REPO_ROOT): {
},
};

if (pins.openshell.minVersion && !NUMERIC_VERSION_RE.test(pins.openshell.minVersion)) {
failures.push("nemoclaw-blueprint/blueprint.yaml min_openshell_version must match X.Y.Z");
}
if (pins.openshell.maxVersion && !NUMERIC_VERSION_RE.test(pins.openshell.maxVersion)) {
failures.push("nemoclaw-blueprint/blueprint.yaml max_openshell_version must match X.Y.Z");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import * as ts from "typescript";
import { SUPPORTED_CREDENTIAL_ENV_NAMES } from "../../src/lib/security/credential-env";

const { SUPPORTED_CREDENTIAL_ENV_NAMES } = await import("../../src/lib/security/credential-env");
const CREDENTIAL_ENV_KEYS = SUPPORTED_CREDENTIAL_ENV_NAMES;

const MESSAGE =
Expand Down Expand Up @@ -251,7 +251,7 @@ function scriptKindForPath(filePath: string): ts.ScriptKind {
function main(): void {
const filePaths = process.argv.slice(2).filter((arg) => arg !== "--");
if (filePaths.length === 0) {
console.error("Usage: tsx scripts/checks/direct-credential-env.ts FILE...");
console.error("Usage: tsx scripts/checks/direct-credential-env.mts FILE...");
process.exitCode = 2;
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ function changedFiles(base: string, head: string): string[] {
function main(): void {
const base = argument("--base");
const head = argument("--head") ?? "HEAD";
if (!base) throw new Error("usage: e2e-mock-parity.ts --base <git-ref> [--head <git-ref>]");
if (!base) throw new Error("usage: e2e-mock-parity.mts --base <git-ref> [--head <git-ref>]");

const manifestPath = path.join(REPO_ROOT, DEFAULT_PARITY_MANIFEST);
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as MockParityManifest;
Expand Down
45 changes: 45 additions & 0 deletions scripts/checks/hermes-light-skin-boundary.mts
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

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const { NEMOCLAW_HERMES_LIGHT_SKIN_REVIEWED_HERMES_VERSIONS } = await import(
"../../src/lib/domain/sandbox/connect-env"
);

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const HERMES_DOCKERFILE_BASE = "agents/hermes/Dockerfile.base";

export function checkHermesLightSkinBoundary(options: {
dockerfileText: string;
reviewedVersions: readonly string[];
}): string | null {
const { dockerfileText, reviewedVersions } = options;
const pinnedVersion = dockerfileText.match(/^ARG HERMES_VERSION=(\S+)$/m)?.[1];
if (!pinnedVersion) {
return `${HERMES_DOCKERFILE_BASE}: could not find ARG HERMES_VERSION`;
}
if (!reviewedVersions.includes(pinnedVersion)) {
return [
"Hermes light terminal compatibility skin needs re-review.",
`${HERMES_DOCKERFILE_BASE} pins ${pinnedVersion}, but connect-env.ts was reviewed for ${reviewedVersions.join(", ")}.`,
"Remove the NemoClaw-managed light skin if upstream Hermes is readable in light terminals, or update the reviewed version constant after validating it still needs the shim.",
].join(" ");
}
return null;
}

function main(): void {
const dockerfileText = fs.readFileSync(path.join(REPO_ROOT, HERMES_DOCKERFILE_BASE), "utf8");
const error = checkHermesLightSkinBoundary({
dockerfileText,
reviewedVersions: NEMOCLAW_HERMES_LIGHT_SKIN_REVIEWED_HERMES_VERSIONS,
});
if (error) throw new Error(error);
}

if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? "")) {
main();
}
31 changes: 0 additions & 31 deletions scripts/checks/hermes-light-skin-boundary.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";
Expand Down Expand Up @@ -29,24 +29,26 @@ function toRepoPath(absPath: string): string {
}

function isProductionTsFile(absPath: string): boolean {
return absPath.endsWith(".ts") && !absPath.endsWith(".test.ts") && !absPath.endsWith(".spec.ts");
return (
/\.(?:cts|mts|ts|tsx)$/.test(absPath) && !/\.(?:test|spec)\.(?:cts|mts|ts|tsx)$/.test(absPath)
);
}

function* walk(dir: string): Generator<string> {
if (!existsSync(dir)) return;
const rootStats = statSync(dir);
const rootStats = lstatSync(dir);
if (rootStats.isSymbolicLink()) return;
if (rootStats.isFile()) {
if (isProductionTsFile(dir)) yield dir;
return;
}
if (!rootStats.isDirectory()) return;
for (const entry of readdirSync(dir)) {
if (SKIP_DIRS.has(entry)) continue;
const absPath = path.join(dir, entry);
const stats = statSync(absPath);
if (stats.isDirectory()) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (SKIP_DIRS.has(entry.name) || entry.isSymbolicLink()) continue;
const absPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
yield* walk(absPath);
} else if (stats.isFile() && isProductionTsFile(absPath)) {
} else if (entry.isFile() && isProductionTsFile(absPath)) {
yield absPath;
}
}
Expand All @@ -58,7 +60,7 @@ function sourceFileFor(absPath: string): ts.SourceFile {
readFileSync(absPath, "utf8"),
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
absPath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
);
}

Expand Down Expand Up @@ -111,9 +113,19 @@ function collectImportRefs(absPath: string): ImportRef[] {
function resolveInternalImport(fromAbsPath: string, specifier: string): string | null {
if (!specifier.startsWith(".")) return null;
const base = path.resolve(path.dirname(fromAbsPath), specifier);
const candidates = [base, `${base}.ts`, `${base}.tsx`, path.join(base, "index.ts")];
const extensions = [".ts", ".tsx", ".mts", ".cts"];
const candidates = [
base,
...extensions.map((extension) => `${base}${extension}`),
...extensions.map((extension) => path.join(base, `index${extension}`)),
];
const found = candidates.find((candidate) => existsSync(candidate));
return found ? toRepoPath(found) : toRepoPath(`${base}.ts`);
if (!found) return toRepoPath(`${base}.ts`);
try {
return toRepoPath(realpathSync(found));
} catch {
return toRepoPath(found);
}
}

function isDomainFile(repoPath: string): boolean {
Expand All @@ -134,7 +146,7 @@ function isMessagingManifestFile(repoPath: string): boolean {

function isActionFile(repoPath: string): boolean {
if (repoPath.startsWith("src/lib/actions/")) return true;
return /(^|\/)[^/]+-actions?\.ts$/.test(repoPath);
return /(^|\/)[^/]+-actions?\.(?:cts|mts|ts|tsx)$/.test(repoPath);
}

function importTargetsForbiddenLayer(
Expand Down Expand Up @@ -319,27 +331,59 @@ function checkMessagingManifestFile(

function checkCommandFile(absPath: string, repoPath: string, violations: Violation[]): void {
const sourceFile = sourceFileFor(absPath);
let commandClassCount = 0;

function isCommandBase(expression: ts.ExpressionWithTypeArguments): boolean {
const text = expression.expression.getText(sourceFile);
return text === "Command" || text === "NemoClawCommand";
}
const identifierBases = new Set<string>();
const namespaceBases = new Map<string, ReadonlySet<string>>();

function visit(node: ts.Node): void {
for (const statement of sourceFile.statements) {
if (
ts.isClassDeclaration(node) &&
node.heritageClauses?.some(
(clause) =>
clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.some(isCommandBase),
)
!ts.isImportDeclaration(statement) ||
!ts.isStringLiteral(statement.moduleSpecifier) ||
!statement.importClause ||
statement.importClause.isTypeOnly
) {
commandClassCount += 1;
continue;
}

const moduleSpecifier = statement.moduleSpecifier.text;
const exportedBases =
moduleSpecifier === "@oclif/core"
? new Set(["Command"])
: resolveInternalImport(absPath, moduleSpecifier) ===
"src/lib/cli/nemoclaw-oclif-command.ts"
? new Set(["NemoClawCommand"])
: null;
if (!exportedBases) continue;

const bindings = statement.importClause.namedBindings;
if (bindings && ts.isNamedImports(bindings)) {
for (const binding of bindings.elements) {
if (binding.isTypeOnly) continue;
const importedName = binding.propertyName?.text ?? binding.name.text;
if (exportedBases.has(importedName)) identifierBases.add(binding.name.text);
}
} else if (bindings && ts.isNamespaceImport(bindings)) {
namespaceBases.set(bindings.name.text, exportedBases);
}
ts.forEachChild(node, visit);
}

visit(sourceFile);
function isCommandBase(expression: ts.ExpressionWithTypeArguments): boolean {
const base = expression.expression;
if (ts.isIdentifier(base)) return identifierBases.has(base.text);
return (
ts.isPropertyAccessExpression(base) &&
ts.isIdentifier(base.expression) &&
namespaceBases.get(base.expression.text)?.has(base.name.text) === true
);
}

const commandClassCount = sourceFile.statements.filter(
(statement) =>
ts.isClassDeclaration(statement) &&
statement.heritageClauses?.some(
(clause) =>
clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.some(isCommandBase),
),
).length;
if (commandClassCount !== 1) {
addViolation(
violations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { fileURLToPath } from "node:url";

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const SCAN_ROOTS = ["bin", "src", "scripts", "test", "nemoclaw/src"];
const SOURCE_EXTENSIONS = new Set([".cjs", ".cts", ".js", ".mjs", ".ts", ".tsx"]);
const SOURCE_EXTENSIONS = new Set([".cjs", ".cts", ".js", ".mjs", ".mts", ".ts", ".tsx"]);
const SKIP_DIRS = new Set([".git", "coverage", "dist", "node_modules"]);
const FORBIDDEN_DIRECTIVE = ["v8", "ignore"].join(" ");
const FORBIDDEN_DIRECTIVE_PATTERN = new RegExp(
Expand Down Expand Up @@ -91,7 +91,7 @@ function* walkSourceFiles(dir: string): Generator<string> {
}
}

function isScannedSourcePath(filePath: string): boolean {
export function isScannedSourcePath(filePath: string): boolean {
return (
filePath.length > 0 &&
SCAN_ROOTS.some((root) => filePath === root || filePath.startsWith(`${root}/`)) &&
Expand Down
Loading
Loading