tollgate-cli 0.2.1 → 0.2.3
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 +10 -0
- package/dist/tollgate.mjs +442 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,16 @@ Every other command runs untouched; the hook adds about 50 ms.
|
|
|
34
34
|
| `.claude/settings.local.json` | No (added to `.gitignore`): your personal hook registration |
|
|
35
35
|
| `~/.config/tollgate/` | Never in a repo: your token (file mode 0600) and the hook bundle |
|
|
36
36
|
|
|
37
|
+
## What the hook can see through
|
|
38
|
+
|
|
39
|
+
Agents rarely type the risky command itself. The hook looks at what a command really runs:
|
|
40
|
+
|
|
41
|
+
- **Package scripts:** `pnpm db:deploy`, `npm run release`, `yarn deploy`, `bun run x` are looked up in the nearest `package.json` (with `pre`/`post` scripts and scripts that call other scripts), so the card shows _pnpm db:deploy → prisma migrate deploy_ with the migration SQL.
|
|
42
|
+
- **Lists and wrappers:** every part of `a && b; c | d` is checked on its own, so `npm publish && echo --dry-run` is still a publish. Prefixes like `VAR=x`, `env`, `sudo`, `npx`, `pnpm exec`, `dotenv --` and literal `bash -c '…'`, `eval '…'`, `( … )` and `$( … )` are looked inside.
|
|
43
|
+
- **Code nobody can read first:** `curl … | sh`, `base64 -d | sh`, `bash <(curl …)` and commands built from a variable (`eval "$CMD"`) are gated as _opaque_.
|
|
44
|
+
|
|
45
|
+
Tollgate is not a shell parser and not a sandbox. It does **not** read Makefiles (`make deploy`), shell files (`./deploy.sh`), npm lifecycle scripts like `prepublishOnly`, or workspace scripts selected with `--filter`. It guards against an agent's mistakes, not against someone determined to get around it on their own machine.
|
|
46
|
+
|
|
37
47
|
## What leaves your machine
|
|
38
48
|
|
|
39
49
|
Only for commands that match a preset: the command and working directory after **local redaction** (known token formats, `KEY=value` secrets, passwords in URLs, secret flags, and every value from your `.env*` files), Claude Code's one-line description of the command (shown to approvers as the agent's reason), your git branch, the last commit subject, and for migrations the newest migration's SQL (capped at 2 KB). The card shows how many values were hidden. The server redacts everything again.
|
package/dist/tollgate.mjs
CHANGED
|
@@ -5242,14 +5242,14 @@ var memo = {
|
|
|
5242
5242
|
var _a3;
|
|
5243
5243
|
(_a3 = inst._zod).deferred ?? (_a3.deferred = []);
|
|
5244
5244
|
inst._zod.deferred.push(() => {
|
|
5245
|
-
const
|
|
5245
|
+
const base2 = inst._zod.parse;
|
|
5246
5246
|
const wrapped = (payload, ctx) => {
|
|
5247
5247
|
if (ctx.direction !== "backward" && isBackEdge(ctx, payload.value))
|
|
5248
5248
|
throw new $ZodCyclicError();
|
|
5249
|
-
return
|
|
5249
|
+
return base2(payload, ctx);
|
|
5250
5250
|
};
|
|
5251
5251
|
inst._zod.parse = wrapped;
|
|
5252
|
-
if (inst._zod.run ===
|
|
5252
|
+
if (inst._zod.run === base2)
|
|
5253
5253
|
inst._zod.run = wrapped;
|
|
5254
5254
|
});
|
|
5255
5255
|
},
|
|
@@ -5261,15 +5261,15 @@ var memo = {
|
|
|
5261
5261
|
let lastBucket;
|
|
5262
5262
|
(_a3 = inst._zod).deferred ?? (_a3.deferred = []);
|
|
5263
5263
|
inst._zod.deferred.push(() => {
|
|
5264
|
-
const
|
|
5264
|
+
const base2 = inst._zod.parse;
|
|
5265
5265
|
const wrapped = (payload, ctx) => {
|
|
5266
5266
|
if (isRecursiveInst === void 0) {
|
|
5267
5267
|
const walked = isRecursive(inst, /* @__PURE__ */ new Set(), false);
|
|
5268
5268
|
if (walked === NONE) {
|
|
5269
|
-
inst._zod.parse =
|
|
5269
|
+
inst._zod.parse = base2;
|
|
5270
5270
|
if (inst._zod.run === wrapped)
|
|
5271
|
-
inst._zod.run =
|
|
5272
|
-
return
|
|
5271
|
+
inst._zod.run = base2;
|
|
5272
|
+
return base2(payload, ctx);
|
|
5273
5273
|
}
|
|
5274
5274
|
if (walked === PROVEN || rechecked)
|
|
5275
5275
|
isRecursiveInst = true;
|
|
@@ -5278,7 +5278,7 @@ var memo = {
|
|
|
5278
5278
|
}
|
|
5279
5279
|
const input2 = payload.value;
|
|
5280
5280
|
if (!isRef(input2))
|
|
5281
|
-
return
|
|
5281
|
+
return base2(payload, ctx);
|
|
5282
5282
|
let state = ctx[STATE];
|
|
5283
5283
|
if (!state) {
|
|
5284
5284
|
state = { buckets: /* @__PURE__ */ new WeakMap(), backEdges: void 0 };
|
|
@@ -5307,7 +5307,7 @@ var memo = {
|
|
|
5307
5307
|
}
|
|
5308
5308
|
handoff = bucket;
|
|
5309
5309
|
const depth = open.length;
|
|
5310
|
-
const result =
|
|
5310
|
+
const result = base2(payload, ctx);
|
|
5311
5311
|
handoff = void 0;
|
|
5312
5312
|
const entry = open.length > depth ? open.pop() : void 0;
|
|
5313
5313
|
if (result instanceof Promise) {
|
|
@@ -5322,7 +5322,7 @@ var memo = {
|
|
|
5322
5322
|
return result;
|
|
5323
5323
|
};
|
|
5324
5324
|
inst._zod.parse = wrapped;
|
|
5325
|
-
if (inst._zod.run ===
|
|
5325
|
+
if (inst._zod.run === base2)
|
|
5326
5326
|
inst._zod.run = wrapped;
|
|
5327
5327
|
});
|
|
5328
5328
|
}
|
|
@@ -13231,9 +13231,9 @@ function codePointLengthVar(doc, ctx, accessor, inDoubt) {
|
|
|
13231
13231
|
doc.write(`const ${v} = typeof ${accessor} === "string" && ${inDoubt} ? ${cpLen}(${accessor}) : ${accessor}.length;`);
|
|
13232
13232
|
return v;
|
|
13233
13233
|
}
|
|
13234
|
-
function numericOperand(value,
|
|
13234
|
+
function numericOperand(value, label2) {
|
|
13235
13235
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
13236
|
-
throw new ZodCompileUnsupportedError(`${
|
|
13236
|
+
throw new ZodCompileUnsupportedError(`${label2} bound of type ${typeof value}`);
|
|
13237
13237
|
}
|
|
13238
13238
|
return `${value}`;
|
|
13239
13239
|
}
|
|
@@ -19727,6 +19727,337 @@ function date4(params) {
|
|
|
19727
19727
|
return _coercedDate(ZodDate, params);
|
|
19728
19728
|
}
|
|
19729
19729
|
|
|
19730
|
+
// ../../features/gates/shared/expand.ts
|
|
19731
|
+
var MAX_DEPTH = 4;
|
|
19732
|
+
var MAX_SEGMENTS = 64;
|
|
19733
|
+
var MAX_LABEL = 80;
|
|
19734
|
+
function scan(text, onChar) {
|
|
19735
|
+
const state = { quote: null, depth: 0, backtick: false };
|
|
19736
|
+
for (let i = 0; i < text.length; i++) {
|
|
19737
|
+
const ch = text[i];
|
|
19738
|
+
const skip = onChar(i, state);
|
|
19739
|
+
if (typeof skip === "number") {
|
|
19740
|
+
i = skip;
|
|
19741
|
+
continue;
|
|
19742
|
+
}
|
|
19743
|
+
if (state.quote === "'") {
|
|
19744
|
+
if (ch === "'") state.quote = null;
|
|
19745
|
+
continue;
|
|
19746
|
+
}
|
|
19747
|
+
if (ch === "\\") {
|
|
19748
|
+
i += 1;
|
|
19749
|
+
continue;
|
|
19750
|
+
}
|
|
19751
|
+
if (state.quote === '"') {
|
|
19752
|
+
if (ch === '"') state.quote = null;
|
|
19753
|
+
else if (ch === "$" && text[i + 1] === "(") {
|
|
19754
|
+
state.depth += 1;
|
|
19755
|
+
i += 1;
|
|
19756
|
+
} else if (ch === ")" && state.depth > 0) state.depth -= 1;
|
|
19757
|
+
continue;
|
|
19758
|
+
}
|
|
19759
|
+
if (ch === "'" || ch === '"') state.quote = ch;
|
|
19760
|
+
else if (ch === "`") state.backtick = !state.backtick;
|
|
19761
|
+
else if (ch === "(") state.depth += 1;
|
|
19762
|
+
else if (ch === ")" && state.depth > 0) state.depth -= 1;
|
|
19763
|
+
}
|
|
19764
|
+
}
|
|
19765
|
+
var atTop = (s) => s.quote === null && s.depth === 0 && !s.backtick;
|
|
19766
|
+
function splitList(text) {
|
|
19767
|
+
const parts = [];
|
|
19768
|
+
let start = 0;
|
|
19769
|
+
scan(text, (i, s) => {
|
|
19770
|
+
if (!atTop(s)) return;
|
|
19771
|
+
const ch = text[i];
|
|
19772
|
+
const next = text[i + 1];
|
|
19773
|
+
const prev = text[i - 1];
|
|
19774
|
+
if (ch === "#" && (i === 0 || /\s/.test(prev))) {
|
|
19775
|
+
parts.push(text.slice(start, i));
|
|
19776
|
+
const end = text.indexOf("\n", i);
|
|
19777
|
+
start = end === -1 ? text.length : end + 1;
|
|
19778
|
+
return end === -1 ? text.length : end;
|
|
19779
|
+
}
|
|
19780
|
+
if (ch === ";" || ch === "\n") {
|
|
19781
|
+
parts.push(text.slice(start, i));
|
|
19782
|
+
start = i + 1;
|
|
19783
|
+
} else if (ch === "&" && next === "&" || ch === "|" && next === "|") {
|
|
19784
|
+
parts.push(text.slice(start, i));
|
|
19785
|
+
start = i + 2;
|
|
19786
|
+
return i + 1;
|
|
19787
|
+
} else if (ch === "&" && prev !== ">" && prev !== "<" && prev !== "|" && next !== ">") {
|
|
19788
|
+
parts.push(text.slice(start, i));
|
|
19789
|
+
start = i + 1;
|
|
19790
|
+
}
|
|
19791
|
+
});
|
|
19792
|
+
parts.push(text.slice(start));
|
|
19793
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
19794
|
+
}
|
|
19795
|
+
function splitPipeline(text) {
|
|
19796
|
+
const parts = [];
|
|
19797
|
+
let start = 0;
|
|
19798
|
+
scan(text, (i, s) => {
|
|
19799
|
+
if (!atTop(s) || text[i] !== "|") return;
|
|
19800
|
+
parts.push(text.slice(start, i));
|
|
19801
|
+
const skipAmp = text[i + 1] === "&";
|
|
19802
|
+
start = i + (skipAmp ? 2 : 1);
|
|
19803
|
+
return skipAmp ? i + 1 : void 0;
|
|
19804
|
+
});
|
|
19805
|
+
parts.push(text.slice(start));
|
|
19806
|
+
return parts.map((part) => part.trim()).filter(Boolean);
|
|
19807
|
+
}
|
|
19808
|
+
function shellWords(text) {
|
|
19809
|
+
const words = [];
|
|
19810
|
+
let current = "";
|
|
19811
|
+
let inWord = false;
|
|
19812
|
+
let quote = null;
|
|
19813
|
+
let depth = 0;
|
|
19814
|
+
let backtick = false;
|
|
19815
|
+
for (let i = 0; i < text.length; i++) {
|
|
19816
|
+
const ch = text[i];
|
|
19817
|
+
if (quote === "'") {
|
|
19818
|
+
if (ch === "'") quote = null;
|
|
19819
|
+
else current += ch;
|
|
19820
|
+
continue;
|
|
19821
|
+
}
|
|
19822
|
+
if (ch === "\\" && i + 1 < text.length) {
|
|
19823
|
+
current += text[i + 1];
|
|
19824
|
+
inWord = true;
|
|
19825
|
+
i += 1;
|
|
19826
|
+
continue;
|
|
19827
|
+
}
|
|
19828
|
+
if (quote === '"') {
|
|
19829
|
+
if (ch === '"' && depth === 0) quote = null;
|
|
19830
|
+
else {
|
|
19831
|
+
if (ch === "$" && text[i + 1] === "(") depth += 1;
|
|
19832
|
+
else if (ch === ")" && depth > 0) depth -= 1;
|
|
19833
|
+
current += ch;
|
|
19834
|
+
}
|
|
19835
|
+
continue;
|
|
19836
|
+
}
|
|
19837
|
+
if (depth > 0 || backtick) {
|
|
19838
|
+
if (ch === "(") depth += 1;
|
|
19839
|
+
else if (ch === ")") depth -= 1;
|
|
19840
|
+
else if (ch === "`") backtick = false;
|
|
19841
|
+
current += ch;
|
|
19842
|
+
continue;
|
|
19843
|
+
}
|
|
19844
|
+
if (/\s/.test(ch)) {
|
|
19845
|
+
if (inWord) words.push(current);
|
|
19846
|
+
current = "";
|
|
19847
|
+
inWord = false;
|
|
19848
|
+
continue;
|
|
19849
|
+
}
|
|
19850
|
+
inWord = true;
|
|
19851
|
+
if (ch === "'" || ch === '"') quote = ch;
|
|
19852
|
+
else if (ch === "$" && text[i + 1] === "(") {
|
|
19853
|
+
depth = 1;
|
|
19854
|
+
current += "$(";
|
|
19855
|
+
i += 1;
|
|
19856
|
+
} else if (ch === "`") {
|
|
19857
|
+
backtick = true;
|
|
19858
|
+
current += ch;
|
|
19859
|
+
} else current += ch;
|
|
19860
|
+
}
|
|
19861
|
+
if (inWord) words.push(current);
|
|
19862
|
+
return words;
|
|
19863
|
+
}
|
|
19864
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
19865
|
+
var base = (word) => word.slice(word.lastIndexOf("/") + 1);
|
|
19866
|
+
function skipFlags(words, withValue = /* @__PURE__ */ new Set()) {
|
|
19867
|
+
let i = 0;
|
|
19868
|
+
while (i < words.length && words[i].startsWith("-") && words[i] !== "--") {
|
|
19869
|
+
i += withValue.has(words[i]) ? 2 : 1;
|
|
19870
|
+
}
|
|
19871
|
+
if (words[i] === "--") i += 1;
|
|
19872
|
+
return words.slice(i);
|
|
19873
|
+
}
|
|
19874
|
+
var SUDO_VALUE = /* @__PURE__ */ new Set(["-u", "-g", "-C", "-D", "-h", "-p", "-r", "-t", "-U", "-T"]);
|
|
19875
|
+
var NPX_VALUE = /* @__PURE__ */ new Set(["-p", "--package", "-c", "--call"]);
|
|
19876
|
+
var ENV_VALUE = /* @__PURE__ */ new Set(["-u", "--unset", "-C", "--chdir", "-S", "--split-string"]);
|
|
19877
|
+
function stripPrefixes(input2) {
|
|
19878
|
+
let words = input2;
|
|
19879
|
+
for (let guard = 0; guard < 16 && words.length > 0; guard++) {
|
|
19880
|
+
const first = base(words[0]);
|
|
19881
|
+
const second = words[1];
|
|
19882
|
+
let next = null;
|
|
19883
|
+
if (ASSIGNMENT.test(words[0])) next = words.slice(1);
|
|
19884
|
+
else if (first === "env") {
|
|
19885
|
+
next = skipFlags(words.slice(1), ENV_VALUE);
|
|
19886
|
+
while (next.length > 0 && ASSIGNMENT.test(next[0])) next = next.slice(1);
|
|
19887
|
+
} else if (first === "sudo" || first === "doas") next = skipFlags(words.slice(1), SUDO_VALUE);
|
|
19888
|
+
else if (["time", "nohup", "command", "exec", "builtin"].includes(first)) {
|
|
19889
|
+
next = skipFlags(words.slice(1));
|
|
19890
|
+
} else if (first === "nice") next = skipFlags(words.slice(1), /* @__PURE__ */ new Set(["-n"]));
|
|
19891
|
+
else if (first === "npx" || first === "bunx") next = skipFlags(words.slice(1), NPX_VALUE);
|
|
19892
|
+
else if (first === "pnpm" && (second === "exec" || second === "dlx")) {
|
|
19893
|
+
next = skipFlags(words.slice(2));
|
|
19894
|
+
} else if (first === "yarn" && (second === "dlx" || second === "exec")) {
|
|
19895
|
+
next = skipFlags(words.slice(2));
|
|
19896
|
+
} else if (first === "npm" && (second === "exec" || second === "x")) {
|
|
19897
|
+
next = skipFlags(words.slice(2), NPX_VALUE);
|
|
19898
|
+
} else if (first === "dotenv" || first === "dotenvx") {
|
|
19899
|
+
const dash = words.indexOf("--");
|
|
19900
|
+
next = dash === -1 ? skipFlags(words.slice(1), /* @__PURE__ */ new Set(["-e", "-f", "-v", "-c"])) : words.slice(dash + 1);
|
|
19901
|
+
if (first === "dotenvx" && next[0] === "run")
|
|
19902
|
+
next = skipFlags(next.slice(1), /* @__PURE__ */ new Set(["-f"]));
|
|
19903
|
+
} else if (first === "cross-env") {
|
|
19904
|
+
next = words.slice(1);
|
|
19905
|
+
while (next.length > 0 && ASSIGNMENT.test(next[0])) next = next.slice(1);
|
|
19906
|
+
}
|
|
19907
|
+
if (next === null) break;
|
|
19908
|
+
words = next;
|
|
19909
|
+
}
|
|
19910
|
+
return words;
|
|
19911
|
+
}
|
|
19912
|
+
var SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
19913
|
+
function innerScript(words) {
|
|
19914
|
+
if (words.length === 0) return null;
|
|
19915
|
+
const first = base(words[0]);
|
|
19916
|
+
if (SHELLS.has(first)) {
|
|
19917
|
+
const flag = words.findIndex((word, i) => i > 0 && /^-[A-Za-z]*c[A-Za-z]*$/.test(word));
|
|
19918
|
+
return flag !== -1 && words[flag + 1] !== void 0 ? words[flag + 1] : null;
|
|
19919
|
+
}
|
|
19920
|
+
if (first === "eval") return words.slice(1).join(" ") || null;
|
|
19921
|
+
return null;
|
|
19922
|
+
}
|
|
19923
|
+
function substitutions(text) {
|
|
19924
|
+
const found = [];
|
|
19925
|
+
for (let i = 0; i < text.length; i++) {
|
|
19926
|
+
if (text[i] === "'") {
|
|
19927
|
+
const end = text.indexOf("'", i + 1);
|
|
19928
|
+
if (end === -1) break;
|
|
19929
|
+
i = end;
|
|
19930
|
+
continue;
|
|
19931
|
+
}
|
|
19932
|
+
if (text[i] === "$" && text[i + 1] === "(" && text[i + 2] !== "(") {
|
|
19933
|
+
let depth = 1;
|
|
19934
|
+
let j = i + 2;
|
|
19935
|
+
for (; j < text.length && depth > 0; j++) {
|
|
19936
|
+
if (text[j] === "(") depth += 1;
|
|
19937
|
+
else if (text[j] === ")") depth -= 1;
|
|
19938
|
+
}
|
|
19939
|
+
found.push(text.slice(i + 2, j - 1));
|
|
19940
|
+
i = j - 1;
|
|
19941
|
+
} else if (text[i] === "`") {
|
|
19942
|
+
const end = text.indexOf("`", i + 1);
|
|
19943
|
+
if (end === -1) break;
|
|
19944
|
+
found.push(text.slice(i + 1, end));
|
|
19945
|
+
i = end;
|
|
19946
|
+
}
|
|
19947
|
+
}
|
|
19948
|
+
return found.filter((inner) => inner.trim());
|
|
19949
|
+
}
|
|
19950
|
+
var PNPM_BUILTINS = new Set(
|
|
19951
|
+
"add i install update up upgrade remove rm uninstall un link ln unlink import rebuild rb prune fetch patch patch-commit exec dlx create publish pack audit list ls outdated why store root bin config c get set env init setup server deploy licenses doctor approve-builds ignored-builds self-update cat-file cat-index find-hash recursive multi m help".split(
|
|
19952
|
+
" "
|
|
19953
|
+
)
|
|
19954
|
+
);
|
|
19955
|
+
var YARN_BUILTINS = new Set(
|
|
19956
|
+
"add install remove upgrade up publish init info why dlx exec workspace workspaces config cache link unlink pack version global import npm plugin set constraints dedupe explain node rebuild search stage unplug patch patch-commit login logout owner tag team audit autoclean check generate-lock-entry help licenses list outdated policies upgrade-interactive".split(
|
|
19957
|
+
" "
|
|
19958
|
+
)
|
|
19959
|
+
);
|
|
19960
|
+
var RUNNER_VALUE = /* @__PURE__ */ new Set([
|
|
19961
|
+
"-C",
|
|
19962
|
+
"--dir",
|
|
19963
|
+
"--prefix",
|
|
19964
|
+
"--filter",
|
|
19965
|
+
"-F",
|
|
19966
|
+
"-w",
|
|
19967
|
+
"--workspace",
|
|
19968
|
+
"--cwd"
|
|
19969
|
+
]);
|
|
19970
|
+
var NPM_LIFECYCLE = {
|
|
19971
|
+
test: "test",
|
|
19972
|
+
t: "test",
|
|
19973
|
+
tst: "test",
|
|
19974
|
+
start: "start",
|
|
19975
|
+
stop: "stop",
|
|
19976
|
+
restart: "restart"
|
|
19977
|
+
};
|
|
19978
|
+
function scriptName(words, scripts) {
|
|
19979
|
+
if (words.length < 2) return null;
|
|
19980
|
+
const tool = base(words[0]);
|
|
19981
|
+
const rest = skipFlags(words.slice(1), RUNNER_VALUE);
|
|
19982
|
+
const sub = rest[0];
|
|
19983
|
+
if (!sub) return null;
|
|
19984
|
+
const after = skipFlags(rest.slice(1), RUNNER_VALUE)[0] ?? null;
|
|
19985
|
+
if (tool === "npm") {
|
|
19986
|
+
if (["run", "run-script", "rum", "urn"].includes(sub)) return after;
|
|
19987
|
+
return NPM_LIFECYCLE[sub] ?? null;
|
|
19988
|
+
}
|
|
19989
|
+
if (tool === "pnpm") {
|
|
19990
|
+
if (sub === "run" || sub === "run-script") return after;
|
|
19991
|
+
return PNPM_BUILTINS.has(sub) ? null : sub;
|
|
19992
|
+
}
|
|
19993
|
+
if (tool === "yarn") {
|
|
19994
|
+
if (sub === "run") return after;
|
|
19995
|
+
return YARN_BUILTINS.has(sub) ? null : sub;
|
|
19996
|
+
}
|
|
19997
|
+
if (tool === "bun") {
|
|
19998
|
+
if (sub === "run") return after;
|
|
19999
|
+
return scripts && sub in scripts ? sub : null;
|
|
20000
|
+
}
|
|
20001
|
+
return null;
|
|
20002
|
+
}
|
|
20003
|
+
function label(words) {
|
|
20004
|
+
const text = words.join(" ");
|
|
20005
|
+
return text.length > MAX_LABEL ? `${text.slice(0, MAX_LABEL - 1)}\u2026` : text;
|
|
20006
|
+
}
|
|
20007
|
+
function expandCommand(command, options) {
|
|
20008
|
+
const maxDepth = options.maxDepth ?? MAX_DEPTH;
|
|
20009
|
+
const maxSegments = options.maxSegments ?? MAX_SEGMENTS;
|
|
20010
|
+
const segments = [];
|
|
20011
|
+
let scripts;
|
|
20012
|
+
const loadScripts = () => {
|
|
20013
|
+
if (scripts === void 0) {
|
|
20014
|
+
try {
|
|
20015
|
+
scripts = options.readScripts?.(options.cwd) ?? null;
|
|
20016
|
+
} catch {
|
|
20017
|
+
scripts = null;
|
|
20018
|
+
}
|
|
20019
|
+
}
|
|
20020
|
+
return scripts;
|
|
20021
|
+
};
|
|
20022
|
+
const visit2 = (text, via, depth, seen) => {
|
|
20023
|
+
for (const pipeline of splitList(text)) {
|
|
20024
|
+
if (segments.length >= maxSegments) return;
|
|
20025
|
+
const group = /^\(([\s\S]*)\)$/.exec(pipeline) ?? /^\{([\s\S]*?);?\s*\}$/.exec(pipeline);
|
|
20026
|
+
if (group) {
|
|
20027
|
+
if (depth < maxDepth) visit2(group[1], via, depth + 1, seen);
|
|
20028
|
+
continue;
|
|
20029
|
+
}
|
|
20030
|
+
const commands = splitPipeline(pipeline).map((part) => stripPrefixes(shellWords(part)));
|
|
20031
|
+
const flat = commands.map((words) => words.join(" ")).filter(Boolean).join(" | ");
|
|
20032
|
+
if (flat) segments.push({ text: flat, via });
|
|
20033
|
+
if (depth >= maxDepth) continue;
|
|
20034
|
+
for (const inner of substitutions(pipeline)) {
|
|
20035
|
+
visit2(inner, [...via, "$(\u2026)"], depth + 1, seen);
|
|
20036
|
+
}
|
|
20037
|
+
for (const words of commands) {
|
|
20038
|
+
const script = innerScript(words);
|
|
20039
|
+
if (script) {
|
|
20040
|
+
const wrapper = base(words[0]) === "eval" ? "eval" : `${base(words[0])} -c`;
|
|
20041
|
+
visit2(script, [...via, wrapper], depth + 1, seen);
|
|
20042
|
+
}
|
|
20043
|
+
const name = scriptName(words, loadScripts() ?? null);
|
|
20044
|
+
const all = loadScripts();
|
|
20045
|
+
if (!name || !all || seen.has(name) || !(name in all)) continue;
|
|
20046
|
+
const nextSeen = new Set(seen).add(name);
|
|
20047
|
+
const from = [...via, label(words)];
|
|
20048
|
+
for (const key of [`pre${name}`, name, `post${name}`]) {
|
|
20049
|
+
const body = all[key];
|
|
20050
|
+
if (typeof body === "string" && body.trim()) {
|
|
20051
|
+
visit2(body.slice(0, 2e3), from, depth + 1, nextSeen);
|
|
20052
|
+
}
|
|
20053
|
+
}
|
|
20054
|
+
}
|
|
20055
|
+
}
|
|
20056
|
+
};
|
|
20057
|
+
visit2(command, [], 0, /* @__PURE__ */ new Set());
|
|
20058
|
+
return segments;
|
|
20059
|
+
}
|
|
20060
|
+
|
|
19730
20061
|
// ../../features/gates/shared/presets.ts
|
|
19731
20062
|
var presetSchema = external_exports.object({
|
|
19732
20063
|
name: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,39}$/).min(1),
|
|
@@ -19786,7 +20117,8 @@ var DEFAULT_PRESETS = [
|
|
|
19786
20117
|
{
|
|
19787
20118
|
name: "publish",
|
|
19788
20119
|
patterns: [
|
|
19789
|
-
|
|
20120
|
+
// Flags (and their values) may sit between the tool and "publish": pnpm --filter cli publish.
|
|
20121
|
+
"\\b(npm|pnpm|yarn)\\b[^;&|\\n]*\\s+publish(\\s|$)",
|
|
19790
20122
|
"\\bcargo\\s+publish\\b",
|
|
19791
20123
|
"\\btwine\\s+upload\\b"
|
|
19792
20124
|
],
|
|
@@ -19797,8 +20129,8 @@ var DEFAULT_PRESETS = [
|
|
|
19797
20129
|
{
|
|
19798
20130
|
name: "infra",
|
|
19799
20131
|
patterns: [
|
|
19800
|
-
"\\bterraform\\s+(apply|destroy)\\
|
|
19801
|
-
"\\bpulumi\\s+(up|destroy)\\
|
|
20132
|
+
"\\bterraform\\b[^;&|\\n]*\\s+(apply|destroy)(\\s|$)",
|
|
20133
|
+
"\\bpulumi\\b[^;&|\\n]*\\s+(up|destroy)(\\s|$)",
|
|
19802
20134
|
"\\baws\\s+.*\\bdelete-[a-z-]+\\b",
|
|
19803
20135
|
"\\baws\\s+s3\\s+(rm|rb)\\b",
|
|
19804
20136
|
"\\bgcloud\\s+.*\\bdelete\\b"
|
|
@@ -19806,6 +20138,21 @@ var DEFAULT_PRESETS = [
|
|
|
19806
20138
|
unless: [DRY_RUN],
|
|
19807
20139
|
quorum: 2
|
|
19808
20140
|
},
|
|
20141
|
+
{
|
|
20142
|
+
// Code Tollgate can't read before it runs: fetched from the network, decoded, or built
|
|
20143
|
+
// from a variable at run time. A person should look before it runs.
|
|
20144
|
+
name: "opaque",
|
|
20145
|
+
patterns: [
|
|
20146
|
+
"\\b(curl|wget)\\b[^|;&]*\\|\\s*(sudo(\\s+-\\S+)*\\s+)?(env\\s+)?(ba|z|da|k)?sh\\b",
|
|
20147
|
+
"\\bbase64\\s+(-d|-D|--decode)\\b[^|;&]*\\|\\s*(sudo\\s+)?(ba|z|da|k)?sh\\b",
|
|
20148
|
+
"\\b(ba|z|da|k)?sh\\s+<\\(",
|
|
20149
|
+
"(^|[\\s;&|])(source|\\.)\\s+<\\(",
|
|
20150
|
+
"(^|[\\s;&|])eval\\s+[\"']?[$`]",
|
|
20151
|
+
"\\b(ba|z|da|k)?sh(\\s+-\\w+)*\\s+-\\w*c\\s+[\"']?[$`]"
|
|
20152
|
+
],
|
|
20153
|
+
env: "other",
|
|
20154
|
+
quorum: 1
|
|
20155
|
+
},
|
|
19809
20156
|
{
|
|
19810
20157
|
name: "destructive-fs",
|
|
19811
20158
|
patterns: [
|
|
@@ -19834,8 +20181,8 @@ function compile2(source) {
|
|
|
19834
20181
|
}
|
|
19835
20182
|
return regexCache.get(source) ?? null;
|
|
19836
20183
|
}
|
|
19837
|
-
function resolvePath(
|
|
19838
|
-
const parts = (target.startsWith("/") ? target : `${
|
|
20184
|
+
function resolvePath(base2, target) {
|
|
20185
|
+
const parts = (target.startsWith("/") ? target : `${base2}/${target}`).split("/");
|
|
19839
20186
|
const out = [];
|
|
19840
20187
|
for (const part of parts) {
|
|
19841
20188
|
if (part === "" || part === ".") continue;
|
|
@@ -19875,6 +20222,35 @@ function matchPreset(presets, input2) {
|
|
|
19875
20222
|
}
|
|
19876
20223
|
return null;
|
|
19877
20224
|
}
|
|
20225
|
+
function withDefaultPresets(presets) {
|
|
20226
|
+
const defaults = new Map(DEFAULT_PRESETS.map((preset) => [preset.name, preset]));
|
|
20227
|
+
const names = new Set(presets.map((preset) => preset.name));
|
|
20228
|
+
const merged = presets.map((preset) => {
|
|
20229
|
+
const builtIn = defaults.get(preset.name);
|
|
20230
|
+
return builtIn ? { ...preset, patterns: [.../* @__PURE__ */ new Set([...preset.patterns, ...builtIn.patterns])] } : preset;
|
|
20231
|
+
});
|
|
20232
|
+
return [...merged, ...DEFAULT_PRESETS.filter((preset) => !names.has(preset.name))];
|
|
20233
|
+
}
|
|
20234
|
+
var ENV_RANK = { prod: 2, staging: 1, other: 0 };
|
|
20235
|
+
function matchCommand(presets, input2) {
|
|
20236
|
+
const pieces = [
|
|
20237
|
+
{ text: input2.command, via: [] },
|
|
20238
|
+
...expandCommand(input2.command, { cwd: input2.cwd, readScripts: input2.readScripts }),
|
|
20239
|
+
...input2.extra ?? []
|
|
20240
|
+
];
|
|
20241
|
+
let best = null;
|
|
20242
|
+
for (const piece of pieces) {
|
|
20243
|
+
const match = matchPreset(presets, {
|
|
20244
|
+
command: piece.text,
|
|
20245
|
+
cwd: input2.cwd,
|
|
20246
|
+
repoRoot: input2.repoRoot
|
|
20247
|
+
});
|
|
20248
|
+
if (!match) continue;
|
|
20249
|
+
const better = best === null || match.quorum > best.quorum || match.quorum === best.quorum && ENV_RANK[match.env] > ENV_RANK[best.env] || match.quorum === best.quorum && match.env === best.env && best.via.length === 0 && piece.via.length > 0 && best.preset.name === match.preset.name;
|
|
20250
|
+
if (better) best = { ...match, segment: piece.text, via: piece.via };
|
|
20251
|
+
}
|
|
20252
|
+
return best;
|
|
20253
|
+
}
|
|
19878
20254
|
|
|
19879
20255
|
// src/api.ts
|
|
19880
20256
|
var REQUEST_TIMEOUT_MS = 5e3;
|
|
@@ -19954,6 +20330,28 @@ function findRepoRoot(cwd) {
|
|
|
19954
20330
|
dir = parent;
|
|
19955
20331
|
}
|
|
19956
20332
|
}
|
|
20333
|
+
function readScripts(cwd, repoRoot) {
|
|
20334
|
+
let dir = cwd;
|
|
20335
|
+
for (; ; ) {
|
|
20336
|
+
const file2 = join(dir, "package.json");
|
|
20337
|
+
if (existsSync(file2)) {
|
|
20338
|
+
try {
|
|
20339
|
+
const scripts = JSON.parse(readFileSync(file2, "utf8")).scripts;
|
|
20340
|
+
if (!scripts || typeof scripts !== "object") return null;
|
|
20341
|
+
return Object.fromEntries(
|
|
20342
|
+
Object.entries(scripts).filter(
|
|
20343
|
+
(entry) => typeof entry[1] === "string"
|
|
20344
|
+
)
|
|
20345
|
+
);
|
|
20346
|
+
} catch {
|
|
20347
|
+
return null;
|
|
20348
|
+
}
|
|
20349
|
+
}
|
|
20350
|
+
const parent = dirname(dir);
|
|
20351
|
+
if (dir === repoRoot || parent === dir || !dir.startsWith(repoRoot)) return null;
|
|
20352
|
+
dir = parent;
|
|
20353
|
+
}
|
|
20354
|
+
}
|
|
19957
20355
|
function readConfigFile(repoRoot) {
|
|
19958
20356
|
const raw = JSON.parse(readFileSync(join(repoRoot, CONFIG_FILE), "utf8"));
|
|
19959
20357
|
return configSchema.parse(raw);
|
|
@@ -20116,9 +20514,16 @@ var createGateSchema = external_exports.object({
|
|
|
20116
20514
|
context: external_exports.string().max(2048).default(""),
|
|
20117
20515
|
// Claude Code's own one-line description of the Bash call ("Apply pending migrations").
|
|
20118
20516
|
intent: external_exports.string().max(300).nullable().optional(),
|
|
20517
|
+
// What a wrapper or package script really runs ("pnpm db:deploy" -> "prisma migrate deploy"),
|
|
20518
|
+
// redacted by the hook. The server matches it again; it can only raise a gate, never lower it.
|
|
20519
|
+
via: external_exports.array(
|
|
20520
|
+
external_exports.object({ from: external_exports.string().min(1).max(300), runs: external_exports.string().min(1).max(1e3) }).strict()
|
|
20521
|
+
).max(5).optional(),
|
|
20119
20522
|
redactions: external_exports.number().int().min(0).max(1e4).default(0),
|
|
20120
20523
|
cmdHash: external_exports.string().regex(SHA256_RE),
|
|
20121
|
-
sessionId: external_exports.string().max(200).optional()
|
|
20524
|
+
sessionId: external_exports.string().max(200).optional(),
|
|
20525
|
+
// How long this hook waits before parking the gate (CLI 0.2.3+), for the countdown.
|
|
20526
|
+
waitSec: external_exports.number().int().min(5).max(600).optional()
|
|
20122
20527
|
}).strict();
|
|
20123
20528
|
var consumeGrantSchema = external_exports.object({ cmdHash: external_exports.string().regex(SHA256_RE) }).strict();
|
|
20124
20529
|
|
|
@@ -20231,7 +20636,12 @@ async function decide(input2, deps) {
|
|
|
20231
20636
|
if (!command.trim()) return { kind: "none" };
|
|
20232
20637
|
const config2 = deps.loadConfig(cwd);
|
|
20233
20638
|
if (!config2) return { kind: "none", note: "no .tollgate.json" };
|
|
20234
|
-
const match =
|
|
20639
|
+
const match = matchCommand(withDefaultPresets(config2.presets), {
|
|
20640
|
+
command,
|
|
20641
|
+
cwd,
|
|
20642
|
+
repoRoot: config2.repoRoot,
|
|
20643
|
+
readScripts: deps.readScripts ? (dir) => deps.readScripts(dir, config2.repoRoot) : void 0
|
|
20644
|
+
});
|
|
20235
20645
|
if (!match) return { kind: "none" };
|
|
20236
20646
|
if (deps.env.TOLLGATE_BYPASS === "1") {
|
|
20237
20647
|
deps.audit("bypass", { preset: match.preset.name, repo: config2.repo });
|
|
@@ -20257,7 +20667,12 @@ async function decide(input2, deps) {
|
|
|
20257
20667
|
const secrets = deps.envSecrets(config2.repoRoot);
|
|
20258
20668
|
const redactedCommand = redact(command, secrets);
|
|
20259
20669
|
const redactedCwd = redact(cwd, secrets);
|
|
20260
|
-
const context = redact(deps.context(command
|
|
20670
|
+
const context = redact(deps.context(`${command}
|
|
20671
|
+
${match.segment}`, config2.repoRoot), secrets);
|
|
20672
|
+
const via = match.via.length > 0 ? {
|
|
20673
|
+
from: redact(match.via.join(" \u2192 "), secrets),
|
|
20674
|
+
runs: redact(match.segment, secrets)
|
|
20675
|
+
} : null;
|
|
20261
20676
|
const branch = deps.gitBranch(config2.repoRoot);
|
|
20262
20677
|
const description = input2.tool_input?.description;
|
|
20263
20678
|
const intent = typeof description === "string" && description.trim() ? redact(description.trim(), secrets) : null;
|
|
@@ -20270,9 +20685,11 @@ async function decide(input2, deps) {
|
|
|
20270
20685
|
branch: branch ? truncate(redact(branch, secrets).text, 200) : null,
|
|
20271
20686
|
context: truncate(context.text, 2048),
|
|
20272
20687
|
intent: intent ? truncate(intent.text, 300) : null,
|
|
20273
|
-
|
|
20688
|
+
via: via ? [{ from: truncate(via.from.text, 300), runs: truncate(via.runs.text, 1e3) }] : void 0,
|
|
20689
|
+
redactions: redactedCommand.count + redactedCwd.count + context.count + (intent?.count ?? 0) + (via ? via.from.count + via.runs.count : 0),
|
|
20274
20690
|
cmdHash,
|
|
20275
|
-
sessionId: input2.session_id
|
|
20691
|
+
sessionId: input2.session_id,
|
|
20692
|
+
waitSec: Math.round((deadline - started) / 1e3)
|
|
20276
20693
|
});
|
|
20277
20694
|
deps.progress(
|
|
20278
20695
|
`Tollgate: waiting on your team \xB7 TG-${gate.code} (0/${gate.quorum}) \xB7 ${gate.url}`
|
|
@@ -20388,7 +20805,7 @@ function writeRepoFiles(input2) {
|
|
|
20388
20805
|
}
|
|
20389
20806
|
|
|
20390
20807
|
// src/main.ts
|
|
20391
|
-
var HELP = `tollgate ${"0.2.
|
|
20808
|
+
var HELP = `tollgate ${"0.2.3"}: your team approves risky agent commands before they run
|
|
20392
20809
|
|
|
20393
20810
|
Usage:
|
|
20394
20811
|
npx tollgate-cli init --token <tg_\u2026> [--server <url>] [--repo <owner/name>]
|
|
@@ -20405,6 +20822,7 @@ function hookDeps() {
|
|
|
20405
20822
|
envSecrets,
|
|
20406
20823
|
gitBranch,
|
|
20407
20824
|
context: buildContext,
|
|
20825
|
+
readScripts,
|
|
20408
20826
|
env: process.env,
|
|
20409
20827
|
now: () => Date.now(),
|
|
20410
20828
|
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
@@ -20572,7 +20990,7 @@ async function main() {
|
|
|
20572
20990
|
return runUnlink();
|
|
20573
20991
|
case "--version":
|
|
20574
20992
|
case "-v":
|
|
20575
|
-
process.stdout.write(`${"0.2.
|
|
20993
|
+
process.stdout.write(`${"0.2.3"}
|
|
20576
20994
|
`);
|
|
20577
20995
|
return;
|
|
20578
20996
|
default:
|