@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.
- package/package.json +1 -1
- 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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
"
|
|
810
|
-
{ cwd, timeout: 4000,
|
|
811
|
-
|
|
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"
|
|
816
|
-
{ cwd, timeout: 4000,
|
|
817
|
-
|
|
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
|
|
834
|
-
if (
|
|
835
|
-
if (known === 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
|
|
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 {
|
|
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).
|