@golproductions/check 3.2.11 → 3.2.13

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 +52 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golproductions/check",
3
- "version": "3.2.11",
3
+ "version": "3.2.13",
4
4
  "description": "Less hallucinations. A command firewall for AI agents: validates every shell command before execution, so fabricated commands are denied before they run.",
5
5
  "bin": {
6
6
  "check": "src/index.js",
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ import { homedir, platform, arch } from "node:os";
6
6
  import { pipeline } from "node:stream/promises";
7
7
  import { Readable } from "node:stream";
8
8
 
9
- const VERSION = "3.2.11";
9
+ const VERSION = "3.2.13";
10
10
  const BINARY_VERSION = "3.0.0";
11
11
  const API = "https://triage.golproductions.com/preflight";
12
12
  const CDN = "https://pub-e55366a7f5994be9be04f0e205179f4a.r2.dev/releases";
@@ -692,7 +692,11 @@ function out(f, ok, reason) {
692
692
  process.stdout.write(JSON.stringify({ decision: "deny", reason }));
693
693
  process.exit(2);
694
694
  }
695
- const r = { cursor: { permission: "deny", user_message: reason, agent_message: reason }, claude: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } };
695
+ // The reason is for the AGENT only: it reads the shell's words, fixes the
696
+ // command, and moves on. The user never sees the stumble, just clean flow.
697
+ // (Claude Code's permissionDecisionReason and Cursor's agent_message feed
698
+ // the model; user_message would put the error on the user's screen.)
699
+ const r = { cursor: { permission: "deny", agent_message: reason }, claude: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } };
696
700
  process.stdout.write(JSON.stringify(r[f] || r.claude));
697
701
  process.exit(0);
698
702
  }
@@ -802,19 +806,34 @@ function bashPath() {
802
806
  return null;
803
807
  }
804
808
 
809
+ // When the shell refuses a name, keep its own words. The reason shown on a
810
+ // deny must come from the interpreter that would have run the command, not
811
+ // from us: deterministic (same machine, same words) and never argued with.
812
+ // Normalized as "<name>: <the shell's phrase>", pure string ops.
813
+ function shellWhy(raw, name) {
814
+ if (!raw) return null;
815
+ const line = raw.split(/\r?\n/).find(l => l.includes(name));
816
+ if (!line) return null;
817
+ let tail = line.slice(line.indexOf(name) + name.length);
818
+ tail = tail.replace(/^['"\s:]+/, "").replace(/\s+$/, "");
819
+ return tail ? name + ": " + tail : null;
820
+ }
821
+
805
822
  function shellKnows(name, cwd, shell) {
806
823
  try {
807
824
  if (shell === "powershell") {
808
825
  const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command",
809
- "if (Get-Command $env:CHECK_PROBE -ErrorAction SilentlyContinue) { exit 0 }; exit 1"],
810
- { cwd, timeout: 4000, stdio: "ignore", env: { ...process.env, CHECK_PROBE: name } });
811
- return r.error ? null : r.status === 0;
826
+ "try { $null = Get-Command $env:CHECK_PROBE -ErrorAction Stop; exit 0 } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }"],
827
+ { cwd, timeout: 4000, encoding: "utf8", env: { ...process.env, CHECK_PROBE: name } });
828
+ if (r.error) return null;
829
+ return { known: r.status === 0, why: r.status === 0 ? null : shellWhy(r.stderr, name) };
812
830
  }
813
831
  const bash = bashPath();
814
832
  if (!bash) return null;
815
- const r = spawnSync(bash, ["-c", 'type -- "$1" >/dev/null 2>&1', "check", name],
816
- { cwd, timeout: 4000, stdio: "ignore" });
817
- return r.error ? null : r.status === 0;
833
+ const r = spawnSync(bash, ["-c", 'type -- "$1"', "check", name],
834
+ { cwd, timeout: 4000, encoding: "utf8" });
835
+ if (r.error) return null;
836
+ return { known: r.status === 0, why: r.status === 0 ? null : shellWhy(r.stderr, name) };
818
837
  } catch { return null; }
819
838
  }
820
839
 
@@ -822,7 +841,7 @@ function shellKnows(name, cwd, shell) {
822
841
  // word proven resolvable, false = at least one proven unresolvable, null =
823
842
  // cannot be determined without executing (subshells, expansions) — abstain,
824
843
  // never guess.
825
- function allBinariesExist(command, cwd, shell) {
844
+ function allBinariesExist(command, cwd, shell, misses) {
826
845
  try {
827
846
  let bases = segmentBases(command);
828
847
  if (shell !== "powershell") bases = bases.filter(b => !SHELL_BUILTINS.has(b));
@@ -830,9 +849,12 @@ function allBinariesExist(command, cwd, shell) {
830
849
  if (bases.some(b => /[(){}$`]/.test(b))) return null;
831
850
  let verdict = true;
832
851
  for (const b of bases) {
833
- const known = shellKnows(b, cwd, shell);
834
- if (known === null) return null;
835
- if (known === false) verdict = false;
852
+ const probe = shellKnows(b, cwd, shell);
853
+ if (probe === null) return null;
854
+ if (probe.known === false) {
855
+ verdict = false;
856
+ if (misses) misses.push(probe.why || b + ": not found");
857
+ }
836
858
  }
837
859
  return verdict;
838
860
  } catch { return null; }
@@ -868,7 +890,17 @@ async function main() {
868
890
  const controller = new AbortController();
869
891
  const timer = setTimeout(() => controller.abort(), 5000);
870
892
  // Ask the same interpreter that will run the command.
871
- const exists = allBinariesExist(c, p.cwd, p.tool_name === "PowerShell" ? "powershell" : "bash");
893
+ const misses = [];
894
+ const exists = allBinariesExist(c, p.cwd, p.tool_name === "PowerShell" ? "powershell" : "bash", misses);
895
+ // LOCAL FAST DENY: the shell just proved a command word does not resolve.
896
+ // The verdict is already decided; a server round trip would only repeat
897
+ // it slower and burn a check. No network, no charge, the shell's words,
898
+ // and the whole answer in the time the probe took. The server still owns
899
+ // every verdict the shell cannot see (URLs, flags, packages, syntax).
900
+ if (exists === false && misses.length) {
901
+ out(f, false, "denied by the shell that would run it. " + misses.join("; "));
902
+ return;
903
+ }
872
904
  // Send only what the verdict needs. No transcript paths, no usernames
873
905
  // beyond what cwd itself carries — the check is about the command.
874
906
  const body = { command: c, cwd: p.cwd || process.cwd(), platform: f, channel: "npm", tool_name: p.tool_name, v: VERSION };
@@ -914,7 +946,13 @@ async function main() {
914
946
  }
915
947
 
916
948
  if (d.verdict === "runnable") { out(f, true); }
917
- else { out(f, false, d.reason || "denied — address the issue before continuing"); }
949
+ else {
950
+ // Prefer the shell's own words: when the interpreter that would run
951
+ // this command already said why it can't, that sentence is the reason.
952
+ // Server reason covers everything the shell can't see (URLs, syntax).
953
+ const local = misses.length ? "denied by the shell that would run it. " + misses.join("; ") : null;
954
+ out(f, false, local || d.reason || "denied. Address the issue before continuing.");
955
+ }
918
956
  } catch {
919
957
  // Network failure or timeout: fail OPEN. An unreachable validation
920
958
  // service must never block the user's work (Terms, section 5).