sentinelayer-cli 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1080,7 +1080,8 @@ Ledger contract:
1080
1080
 
1081
1081
  ## Requirements
1082
1082
 
1083
- - Node `>=20.0`
1083
+ - Node.js `^20.0.0 || ^22.0.0` (the package `engines`)
1084
+ - on Windows, Node.js 22 (libuv 1.48.0 or later): the CLI looks up the programs it runs (git, gh, python, npm and others) on `PATH` only, and an older runtime cannot do that, so on Windows the CLI exits with an error before running anything under Node.js 20
1084
1085
  - network access to Sentinelayer API/web
1085
1086
  - optional: GitHub CLI (`gh`) authenticated for secret injection
1086
1087
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -93,6 +93,9 @@ const EXACT_ENV_KEYS_TO_STRIP = new Set([
93
93
  "MONGODB_URI",
94
94
  "SSH_SIGNING_KEY",
95
95
  "SSH_PRIVATE_KEY",
96
+ "SSH_AUTH_SOCK",
97
+ "PGPASSWORD",
98
+ "MYSQL_PWD",
96
99
  "KUBECONFIG",
97
100
  "KUBE_CONFIG_DATA",
98
101
  ]);
@@ -114,6 +117,7 @@ const ENV_KEY_PREFIX_PATTERNS = [
114
117
  /^TWILIO_/i,
115
118
  /^SENDGRID_/i,
116
119
  /^RESEND_/i,
120
+ /^NPM_CONFIG_.*_AUTH(TOKEN)?$/i, // npm's _auth and _authToken, global or per registry
117
121
  ];
118
122
 
119
123
  const ENV_KEY_SUFFIX_PATTERNS = [
@@ -126,6 +130,11 @@ const ENV_KEY_SUFFIX_PATTERNS = [
126
130
  /_ACCESS_KEY$/i,
127
131
  /_AUTH_TOKEN$/i,
128
132
  /_SESSION_TOKEN$/i,
133
+ /_KEY$/i,
134
+ /_DSN$/i,
135
+ /_PASS$/i,
136
+ /_CREDENTIALS?$/i,
137
+ /_CONNECTION_STRING$/i,
129
138
  ];
130
139
 
131
140
  /**
package/src/cli.js CHANGED
@@ -2,6 +2,7 @@ import process from "node:process";
2
2
  import { Command } from "commander";
3
3
 
4
4
  import { CLI_VERSION, runLegacyCliWithErrorHandling } from "./legacy-cli.js";
5
+ import { restrictProgramLookupToPath } from "./program-lookup.js";
5
6
 
6
7
  const COMMAND_REGISTRARS = {
7
8
  init: {
@@ -242,6 +243,15 @@ export async function buildCliProgram({
242
243
  }
243
244
 
244
245
  export async function runCli(rawArgs = process.argv.slice(2)) {
246
+ // Programs named without a path come from PATH only, never from the working directory, before
247
+ // anything is spawned. On a Windows runtime that cannot guarantee that, nothing runs.
248
+ const programLookup = restrictProgramLookupToPath();
249
+ if (!programLookup.ok) {
250
+ console.error(programLookup.message);
251
+ process.exitCode = 1;
252
+ return;
253
+ }
254
+
245
255
  // Where the user's credentials may go is decided once, here, from the real environment and the
246
256
  // global config, and frozen: nothing that changes process.env later can move it.
247
257
  const { freezeProcessTrustContext } = await import("./auth/credential-destinations.js");
@@ -1,5 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
- import { extname } from "node:path";
2
+ import os from "node:os";
3
+ import { extname, resolve as resolvePath } from "node:path";
3
4
  import { promisify } from "node:util";
4
5
 
5
6
  import { parse } from "@babel/parser";
@@ -149,7 +150,7 @@ async function resolvePythonExecutable() {
149
150
  pythonExecutablePromise = (async () => {
150
151
  for (const executable of PYTHON_EXECUTABLE_CANDIDATES) {
151
152
  try {
152
- await execFileAsync(executable, ["--version"], { timeout: 5000 });
153
+ await execFileAsync(executable, ["--version"], { timeout: 5000, cwd: os.tmpdir() });
153
154
  return executable;
154
155
  } catch {
155
156
  continue;
@@ -193,9 +194,13 @@ async function parsePythonAstSpecifiers(absolutePath = "") {
193
194
  };
194
195
  }
195
196
  try {
196
- const { stdout } = await execFileAsync(executable, ["-c", PYTHON_AST_SCRIPT, absolutePath], {
197
+ // Isolated (-I: no script or working directory on sys.path, no PYTHON* variables) and run from
198
+ // a neutral directory, so a repository's own ast.py or json.py is never imported; the file to
199
+ // parse is passed as an argument.
200
+ const { stdout } = await execFileAsync(executable, ["-I", "-c", PYTHON_AST_SCRIPT, resolvePath(absolutePath)], {
197
201
  timeout: 10000,
198
202
  maxBuffer: 1024 * 1024,
203
+ cwd: os.tmpdir(),
199
204
  });
200
205
  const parsed = JSON.parse(String(stdout || "{}"));
201
206
  const specifiers = Array.isArray(parsed.specifiers) ? parsed.specifiers : [];
@@ -1,5 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import fsp from "node:fs/promises";
3
+ import os from "node:os";
3
4
  import path, { extname } from "node:path";
4
5
  import { promisify } from "node:util";
5
6
 
@@ -370,7 +371,7 @@ async function resolvePythonExecutable() {
370
371
  pythonExecutablePromise = (async () => {
371
372
  for (const executable of PYTHON_EXECUTABLE_CANDIDATES) {
372
373
  try {
373
- await execFileAsync(executable, ["--version"], { timeout: 5000 });
374
+ await execFileAsync(executable, ["--version"], { timeout: 5000, cwd: os.tmpdir() });
374
375
  return executable;
375
376
  } catch {
376
377
  continue;
@@ -454,9 +455,13 @@ async function parsePythonCallgraph(absolutePath = "", content = "", normalizedL
454
455
  };
455
456
  }
456
457
  try {
457
- const { stdout } = await execFileAsync(executable, ["-c", PYTHON_CALLGRAPH_SCRIPT, absolutePath], {
458
+ // Isolated (-I: no script or working directory on sys.path, no PYTHON* variables) and run from
459
+ // a neutral directory, so a repository's own ast.py or json.py is never imported; the file to
460
+ // parse is passed as an argument.
461
+ const { stdout } = await execFileAsync(executable, ["-I", "-c", PYTHON_CALLGRAPH_SCRIPT, path.resolve(absolutePath)], {
458
462
  timeout: 15000,
459
463
  maxBuffer: 1024 * 1024,
464
+ cwd: os.tmpdir(),
460
465
  });
461
466
  const parsed = JSON.parse(String(stdout || "{}"));
462
467
  return {
@@ -0,0 +1,54 @@
1
+ import process from "node:process";
2
+
3
+ // Windows looks up a program named without a path in the working directory before PATH. The CLI
4
+ // runs git, gh, python, npm and other tools by name, often from inside a repository, so it turns
5
+ // that lookup off with NoDefaultCurrentDirectoryInExePath. Node.js spawns through libuv, which
6
+ // honours that variable from libuv 1.48.0 (Node.js 22); an older libuv ignores it. On such a
7
+ // runtime the CLI does not run at all on Windows, rather than run with the lookup on.
8
+ export const MIN_WINDOWS_LIBUV = Object.freeze([1, 48, 0]);
9
+
10
+ function parseVersion(value) {
11
+ const parts = String(value || "").split(".");
12
+ if (parts.length < 2 || parts.length > 3) {
13
+ return null;
14
+ }
15
+ const numbers = parts.map((part) => (/^\d+$/.test(part) ? Number(part) : NaN));
16
+ return numbers.every(Number.isInteger) ? [...numbers, 0].slice(0, 3) : null;
17
+ }
18
+
19
+ function atLeast(version, minimum) {
20
+ for (let index = 0; index < minimum.length; index += 1) {
21
+ if (version[index] !== minimum[index]) {
22
+ return version[index] > minimum[index];
23
+ }
24
+ }
25
+ return true;
26
+ }
27
+
28
+ /**
29
+ * Turn off working-directory program lookup for this process and the children that inherit its
30
+ * environment. Returns { ok: false, message } instead when this is Windows and the runtime would
31
+ * ignore the setting.
32
+ */
33
+ export function restrictProgramLookupToPath({
34
+ platform = process.platform,
35
+ libuv = process.versions.uv,
36
+ nodeVersion = process.version,
37
+ env = process.env,
38
+ } = {}) {
39
+ if (platform !== "win32") {
40
+ return { ok: true };
41
+ }
42
+ const version = parseVersion(libuv);
43
+ if (!version || !atLeast(version, MIN_WINDOWS_LIBUV)) {
44
+ return {
45
+ ok: false,
46
+ message:
47
+ `On Windows, sentinelayer-cli needs Node.js 22 or later (libuv ${MIN_WINDOWS_LIBUV.join(".")} or later). ` +
48
+ `This is Node.js ${nodeVersion} with libuv ${libuv || "unknown"}, which looks up programs in the current ` +
49
+ "directory before PATH. Upgrade Node.js and run the command again.",
50
+ };
51
+ }
52
+ env.NoDefaultCurrentDirectoryInExePath = "1";
53
+ return { ok: true };
54
+ }
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import fsp from "node:fs/promises";
4
4
  import path from "node:path";
5
5
 
6
+ import { buildScrubbedEnv } from "../agents/shared-tools/shell.js";
6
7
  import { resolveOutputRoot } from "../config/service.js";
7
8
  import { resolveCodebaseIngest } from "../ingest/engine.js";
8
9
  import { runSpecBindingChecks } from "./spec-binding.js";
@@ -844,6 +845,13 @@ function buildStaticChecks(ingest = {}) {
844
845
  ];
845
846
  }
846
847
 
848
+ // The repository's own scripts run here (lint, typecheck, format check, tests). Running its code is
849
+ // the point; handing it this machine's credentials is not. SentinelLayer, provider, GitHub, cloud
850
+ // and other secret-looking variables are removed from its environment.
851
+ export function staticCheckEnv(env = process.env) {
852
+ return { ...buildScrubbedEnv(env), CI: "1", FORCE_COLOR: "0" };
853
+ }
854
+
847
855
  async function executeStaticCheck({ check, targetPath, runDir } = {}) {
848
856
  const checksDir = path.join(runDir, "checks");
849
857
  await fsp.mkdir(checksDir, { recursive: true });
@@ -853,11 +861,7 @@ async function executeStaticCheck({ check, targetPath, runDir } = {}) {
853
861
  cwd: targetPath,
854
862
  encoding: "utf-8",
855
863
  timeout: STATIC_CHECK_TIMEOUT_MS,
856
- env: {
857
- ...process.env,
858
- CI: "1",
859
- FORCE_COLOR: "0",
860
- },
864
+ env: staticCheckEnv(),
861
865
  });
862
866
 
863
867
  const durationMs = Date.now() - startedAt;
@@ -927,6 +931,8 @@ async function executeStaticCheck({ check, targetPath, runDir } = {}) {
927
931
  };
928
932
  }
929
933
 
934
+ export const __localReviewForTests = Object.freeze({ executeStaticCheck });
935
+
930
936
  async function runStaticAnalysisLayer({ targetPath, ingest, runDir, maxFindings = MAX_FINDINGS } = {}) {
931
937
  const checks = buildStaticChecks(ingest);
932
938
  const results = [];