@golproductions/check 3.2.11 → 3.2.12

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 +38 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golproductions/check",
3
- "version": "3.2.11",
3
+ "version": "3.2.12",
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.12";
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";
@@ -802,19 +802,34 @@ function bashPath() {
802
802
  return null;
803
803
  }
804
804
 
805
+ // When the shell refuses a name, keep its own words. The reason shown on a
806
+ // deny must come from the interpreter that would have run the command, not
807
+ // from us: deterministic (same machine, same words) and never argued with.
808
+ // Normalized as "<name>: <the shell's phrase>", pure string ops.
809
+ function shellWhy(raw, name) {
810
+ if (!raw) return null;
811
+ const line = raw.split(/\r?\n/).find(l => l.includes(name));
812
+ if (!line) return null;
813
+ let tail = line.slice(line.indexOf(name) + name.length);
814
+ tail = tail.replace(/^['"\s:]+/, "").replace(/\s+$/, "");
815
+ return tail ? name + ": " + tail : null;
816
+ }
817
+
805
818
  function shellKnows(name, cwd, shell) {
806
819
  try {
807
820
  if (shell === "powershell") {
808
821
  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;
822
+ "try { $null = Get-Command $env:CHECK_PROBE -ErrorAction Stop; exit 0 } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }"],
823
+ { cwd, timeout: 4000, encoding: "utf8", env: { ...process.env, CHECK_PROBE: name } });
824
+ if (r.error) return null;
825
+ return { known: r.status === 0, why: r.status === 0 ? null : shellWhy(r.stderr, name) };
812
826
  }
813
827
  const bash = bashPath();
814
828
  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;
829
+ const r = spawnSync(bash, ["-c", 'type -- "$1"', "check", name],
830
+ { cwd, timeout: 4000, encoding: "utf8" });
831
+ if (r.error) return null;
832
+ return { known: r.status === 0, why: r.status === 0 ? null : shellWhy(r.stderr, name) };
818
833
  } catch { return null; }
819
834
  }
820
835
 
@@ -822,7 +837,7 @@ function shellKnows(name, cwd, shell) {
822
837
  // word proven resolvable, false = at least one proven unresolvable, null =
823
838
  // cannot be determined without executing (subshells, expansions) — abstain,
824
839
  // never guess.
825
- function allBinariesExist(command, cwd, shell) {
840
+ function allBinariesExist(command, cwd, shell, misses) {
826
841
  try {
827
842
  let bases = segmentBases(command);
828
843
  if (shell !== "powershell") bases = bases.filter(b => !SHELL_BUILTINS.has(b));
@@ -830,9 +845,12 @@ function allBinariesExist(command, cwd, shell) {
830
845
  if (bases.some(b => /[(){}$`]/.test(b))) return null;
831
846
  let verdict = true;
832
847
  for (const b of bases) {
833
- const known = shellKnows(b, cwd, shell);
834
- if (known === null) return null;
835
- if (known === false) verdict = false;
848
+ const probe = shellKnows(b, cwd, shell);
849
+ if (probe === null) return null;
850
+ if (probe.known === false) {
851
+ verdict = false;
852
+ if (misses) misses.push(probe.why || b + ": not found");
853
+ }
836
854
  }
837
855
  return verdict;
838
856
  } catch { return null; }
@@ -868,7 +886,8 @@ async function main() {
868
886
  const controller = new AbortController();
869
887
  const timer = setTimeout(() => controller.abort(), 5000);
870
888
  // Ask the same interpreter that will run the command.
871
- const exists = allBinariesExist(c, p.cwd, p.tool_name === "PowerShell" ? "powershell" : "bash");
889
+ const misses = [];
890
+ const exists = allBinariesExist(c, p.cwd, p.tool_name === "PowerShell" ? "powershell" : "bash", misses);
872
891
  // Send only what the verdict needs. No transcript paths, no usernames
873
892
  // beyond what cwd itself carries — the check is about the command.
874
893
  const body = { command: c, cwd: p.cwd || process.cwd(), platform: f, channel: "npm", tool_name: p.tool_name, v: VERSION };
@@ -914,7 +933,13 @@ async function main() {
914
933
  }
915
934
 
916
935
  if (d.verdict === "runnable") { out(f, true); }
917
- else { out(f, false, d.reason || "denied — address the issue before continuing"); }
936
+ else {
937
+ // Prefer the shell's own words: when the interpreter that would run
938
+ // this command already said why it can't, that sentence is the reason.
939
+ // Server reason covers everything the shell can't see (URLs, syntax).
940
+ const local = misses.length ? "denied by the shell that would run it. " + misses.join("; ") : null;
941
+ out(f, false, local || d.reason || "denied. Address the issue before continuing.");
942
+ }
918
943
  } catch {
919
944
  // Network failure or timeout: fail OPEN. An unreachable validation
920
945
  // service must never block the user's work (Terms, section 5).