@golproductions/check 3.2.4 → 3.2.5

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +87 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golproductions/check",
3
- "version": "3.2.4",
3
+ "version": "3.2.5",
4
4
  "description": "The anti-hallucination layer for AI coding agents. Stops hallucinated commands, packages, and URLs before they run.",
5
5
  "bin": {
6
6
  "check": "src/index.js",
package/src/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, writeFileSync, mkdirSync, existsSync, createWriteStream, chmodSync, unlinkSync, rmSync } from "node:fs";
3
+ import { spawnSync } from "node:child_process";
3
4
  import { join } from "node:path";
4
5
  import { homedir, platform, arch } from "node:os";
5
6
  import { pipeline } from "node:stream/promises";
6
7
  import { Readable } from "node:stream";
7
8
 
8
- const VERSION = "3.2.1";
9
+ const VERSION = "3.2.5";
9
10
  const BINARY_VERSION = "3.0.0";
10
11
  const API = "https://triage.golproductions.com/preflight";
11
12
  const CDN = "https://pub-e55366a7f5994be9be04f0e205179f4a.r2.dev/releases";
@@ -723,6 +724,84 @@ function outCreditsExhausted(f, reason) {
723
724
  process.exit(0);
724
725
  }
725
726
 
727
+ // The machine's own PATH resolution is the ground truth for whether a
728
+ // command exists. This is the same lookup the shell performs at launch:
729
+ // walk PATH (plus PATHEXT on Windows), pure filesystem reads, deterministic.
730
+ // The server honors binary_exists as proof, so real-but-unlisted tools
731
+ // (claude, codex, aider, internal CLIs) are never falsely blocked.
732
+ const PREFIX_WORDS = new Set(["sudo", "nohup", "nice", "time", "timeout", "env"]);
733
+
734
+ // POSIX shell builtins and keywords. Frozen by spec since 1992: these are
735
+ // provided by the shell itself, not the filesystem, and are treated as
736
+ // present. This set is evergreen — it does not rot the way a tool list does.
737
+ const SHELL_BUILTINS = new Set([
738
+ "cd", "echo", "printf", "pwd", "export", "set", "unset", "alias", "unalias",
739
+ "true", "false", "test", "[", "[[", "exit", "return", "break", "continue",
740
+ "shift", "source", ".", "eval", "exec", "trap", "wait", "jobs", "fg", "bg",
741
+ "read", "readonly", "local", "declare", "typeset", "let", "getopts", "hash",
742
+ "type", "command", "builtin", "ulimit", "umask", "times", "dirs", "pushd",
743
+ "popd", "for", "while", "until", "if", "then", "else", "elif", "fi", "do",
744
+ "done", "case", "esac", "in", "function", "select", "{", "}", "!",
745
+ ]);
746
+
747
+ function segmentBases(command) {
748
+ const bases = [];
749
+ for (const seg of command.split(/\|\||&&|[|;\n]/)) {
750
+ const words = seg.trim().split(/\s+/).filter(Boolean);
751
+ let base = null;
752
+ for (const w of words) {
753
+ const bare = w.replace(/^["']+|["']+$/g, "");
754
+ if (!bare) continue;
755
+ if (PREFIX_WORDS.has(bare)) continue;
756
+ if (/^[\d.]+[smhd]?$/.test(bare)) continue;
757
+ if (bare.includes("=") && !bare.startsWith("-")) continue;
758
+ if (bare.startsWith("-")) continue;
759
+ base = bare;
760
+ break;
761
+ }
762
+ if (base) bases.push(base);
763
+ }
764
+ return bases;
765
+ }
766
+
767
+ // THE ROD: do not model the shell — ask it. `type` is the shell reporting
768
+ // on itself: builtins, keywords, functions, and PATH binaries all answer
769
+ // through the exact resolution the shell will use at launch. A model of the
770
+ // shell can drift from the shell; the shell cannot drift from itself.
771
+ function shellKnows(name, cwd, shell) {
772
+ try {
773
+ if (shell === "powershell") {
774
+ const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command",
775
+ "if (Get-Command $env:CHECK_PROBE -ErrorAction SilentlyContinue) { exit 0 }; exit 1"],
776
+ { cwd, timeout: 4000, stdio: "ignore", env: { ...process.env, CHECK_PROBE: name } });
777
+ return r.error ? null : r.status === 0;
778
+ }
779
+ const r = spawnSync("bash", ["-c", 'type -- "$1" >/dev/null 2>&1', "check", name],
780
+ { cwd, timeout: 4000, stdio: "ignore" });
781
+ return r.error ? null : r.status === 0;
782
+ } catch { return null; }
783
+ }
784
+
785
+ // Tri-state ground truth from the interpreter itself: true = every command
786
+ // word proven resolvable, false = at least one proven unresolvable, null =
787
+ // cannot be determined without executing (subshells, expansions) — abstain,
788
+ // never guess.
789
+ function allBinariesExist(command, cwd, shell) {
790
+ try {
791
+ let bases = segmentBases(command);
792
+ if (shell !== "powershell") bases = bases.filter(b => !SHELL_BUILTINS.has(b));
793
+ if (bases.length === 0) return true;
794
+ if (bases.some(b => /[(){}$`]/.test(b))) return null;
795
+ let verdict = true;
796
+ for (const b of bases) {
797
+ const known = shellKnows(b, cwd, shell);
798
+ if (known === null) return null;
799
+ if (known === false) verdict = false;
800
+ }
801
+ return verdict;
802
+ } catch { return null; }
803
+ }
804
+
726
805
  async function main() {
727
806
  let f = "claude";
728
807
  try {
@@ -739,10 +818,16 @@ async function main() {
739
818
  if (!hookKey) { out(f, false, "check: GOL_CLIENT_ID not set. Get your key at https://golproductions.com/check"); return; }
740
819
  const controller = new AbortController();
741
820
  const timer = setTimeout(() => controller.abort(), 5000);
821
+ // Ask the same interpreter that will run the command.
822
+ const exists = allBinariesExist(c, p.cwd, p.tool_name === "PowerShell" ? "powershell" : "bash");
823
+ // Send only what the verdict needs. No transcript paths, no usernames
824
+ // beyond what cwd itself carries — the check is about the command.
825
+ const body = { command: c, cwd: p.cwd || process.cwd(), platform: f, channel: "npm", tool_name: p.tool_name, v: VERSION };
826
+ if (exists !== null) body.binary_exists = exists;
742
827
  const res = await fetch(API, {
743
828
  method: "POST",
744
829
  headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": hookKey, "User-Agent": "c/" + VERSION },
745
- body: JSON.stringify({ command: c, cwd: p.cwd || process.cwd(), platform: f, channel: "npm", tool_name: p.tool_name, transcript_path: p.transcript_path, v: VERSION }),
830
+ body: JSON.stringify(body),
746
831
  signal: controller.signal
747
832
  });
748
833
  clearTimeout(timer);