fullcourtdefense-cli 1.35.7 → 1.35.9
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/dist/actionIdentity.js +2 -1
- package/dist/actionPolicyEngine.d.ts +38 -0
- package/dist/actionPolicyEngine.js +383 -24
- package/dist/approvalPrompt.js +2 -2
- package/dist/askDialog.js +5 -4
- package/dist/blockExplanation.js +2 -0
- package/dist/callerEnv.d.ts +33 -0
- package/dist/callerEnv.js +90 -0
- package/dist/commandNarrative.js +111 -4
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/deterministicGuard.d.ts +32 -0
- package/dist/commands/deterministicGuard.js +214 -19
- package/dist/commands/hook.d.ts +34 -1
- package/dist/commands/hook.js +188 -31
- package/dist/commands/mcpGateway.js +2 -0
- package/dist/commands/taintLedger.js +3 -1
- package/dist/commands/workloadProtect.js +6 -2
- package/dist/devConfirm.d.ts +28 -12
- package/dist/devConfirm.js +134 -48
- package/dist/hookSlim.d.ts +1 -1
- package/dist/hookSlim.js +22 -2
- package/dist/nativeHook.d.ts +2 -1
- package/dist/nativeHook.js +109 -3
- package/dist/notify.js +4 -0
- package/dist/shellAst.d.ts +117 -0
- package/dist/shellAst.js +590 -0
- package/dist/shellAstLoader.d.ts +32 -0
- package/dist/shellAstLoader.js +271 -0
- package/dist/telemetry.d.ts +20 -0
- package/dist/telemetry.js +7 -0
- package/dist/vendor/tree-sitter/LICENSE.tree-sitter-bash +21 -0
- package/dist/vendor/tree-sitter/LICENSE.web-tree-sitter +21 -0
- package/dist/vendor/tree-sitter/VERSIONS.json +4 -0
- package/dist/vendor/tree-sitter/tree-sitter-bash.wasm +0 -0
- package/dist/vendor/tree-sitter/tree-sitter.cjs +4031 -0
- package/dist/vendor/tree-sitter/tree-sitter.wasm +0 -0
- package/dist/verdictIpc.js +5 -2
- package/dist/verdictIpcClient.d.ts +11 -1
- package/dist/verdictIpcClient.js +12 -2
- package/dist/version.json +1 -1
- package/package.json +9 -6
|
@@ -35,6 +35,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.CREDENTIAL_COMMAND_RULES = exports.DEFAULT_DISABLED_BUILT_INS = void 0;
|
|
37
37
|
exports.toLocalSafetyRuleAction = toLocalSafetyRuleAction;
|
|
38
|
+
exports.setShellAstProvider = setShellAstProvider;
|
|
39
|
+
exports.setShellAstMode = setShellAstMode;
|
|
40
|
+
exports.getShellAstMode = getShellAstMode;
|
|
41
|
+
exports.takeShellAstComparison = takeShellAstComparison;
|
|
38
42
|
exports.unquotedCode = unquotedCode;
|
|
39
43
|
exports.stripInertDataSegments = stripInertDataSegments;
|
|
40
44
|
exports.scanDeterministicToolCall = scanDeterministicToolCall;
|
|
@@ -722,25 +726,211 @@ function credentialCommandReason(value, options) {
|
|
|
722
726
|
// stall. Root targets (`/`, `/*`) stay blocked even inside containers (the
|
|
723
727
|
// Patria case: an agent nuking a running container's filesystem).
|
|
724
728
|
const CONTAINER_EXEC_BEFORE = /\b(?:docker|podman|nerdctl|kubectl)(?:\.exe)?\s+(?:compose\s+)?(?:exec|run)\b[^;&|]*$/i;
|
|
725
|
-
|
|
729
|
+
let shellAstProvider;
|
|
730
|
+
let shellAstMode = 'shadow';
|
|
731
|
+
let lastShellAstComparison;
|
|
732
|
+
function setShellAstProvider(provider) {
|
|
733
|
+
shellAstProvider = provider;
|
|
734
|
+
}
|
|
735
|
+
function setShellAstMode(mode) { shellAstMode = mode; }
|
|
736
|
+
function getShellAstMode() { return shellAstMode; }
|
|
737
|
+
/** Shadow telemetry: the comparison recorded by the most recent shell scan (consumed once). */
|
|
738
|
+
function takeShellAstComparison() {
|
|
739
|
+
const value = lastShellAstComparison;
|
|
740
|
+
lastShellAstComparison = undefined;
|
|
741
|
+
return value;
|
|
742
|
+
}
|
|
743
|
+
function shellAstFactsFullyComplete(facts) {
|
|
744
|
+
if (!facts || !facts.complete)
|
|
745
|
+
return false;
|
|
746
|
+
for (const command of facts.commands)
|
|
747
|
+
if (command.wrapper?.inner && !shellAstFactsFullyComplete(command.wrapper.inner))
|
|
748
|
+
return false;
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
const AST_TRANSPARENT_WRAPPERS = new Set(['sudo', 'doas', 'pkexec', 'run0', 'gosu', 'runuser', 'env', 'nice', 'nohup', 'ionice', 'chrt', 'stdbuf', 'timeout', 'command', 'builtin', 'time', 'exec']);
|
|
752
|
+
/**
|
|
753
|
+
* The destructive-rm decision from structure: which program really runs
|
|
754
|
+
* (through `sudo` / `env` / `time` / `exec`), which words are its flags after
|
|
755
|
+
* quote resolution (`rm "-rf" /` ≡ `rm -rf /`, `-r -f` ≡ `-rf`, `--recursive
|
|
756
|
+
* --force`), and what each target is once the cwd scope is known. Inner scripts
|
|
757
|
+
* of `bash -c` / `docker run … sh -c` are included; inside a container a bare
|
|
758
|
+
* `*` is the container workdir (cleanup), the root `/` is still the root.
|
|
759
|
+
*/
|
|
760
|
+
function astDestructiveRm(facts, inContainer = false) {
|
|
761
|
+
for (const command of facts.commands) {
|
|
762
|
+
if (command.wrapper?.inner) {
|
|
763
|
+
const hit = astDestructiveRm(command.wrapper.inner, inContainer || command.wrapper.kind === 'container');
|
|
764
|
+
if (hit)
|
|
765
|
+
return hit;
|
|
766
|
+
}
|
|
767
|
+
let argv = command.argv;
|
|
768
|
+
let literal = command.literal;
|
|
769
|
+
let name = command.name;
|
|
770
|
+
// Unwrap transparent wrappers: `sudo -u root rm -rf /`, `env X=1 rm -rf /`.
|
|
771
|
+
for (let guard = 0; guard < 4 && AST_TRANSPARENT_WRAPPERS.has(name); guard++) {
|
|
772
|
+
let i = 1;
|
|
773
|
+
while (i < argv.length && ((argv[i].startsWith('-') && argv[i] !== '-') || /^[A-Za-z_][A-Za-z0-9_]*=/.test(argv[i]))) {
|
|
774
|
+
// `sudo -u root`: the option value follows on the next word.
|
|
775
|
+
if (/^-(?:u|g|h|C|p|r|t|U|D)$/.test(argv[i]))
|
|
776
|
+
i++;
|
|
777
|
+
i++;
|
|
778
|
+
}
|
|
779
|
+
if (i >= argv.length || !literal[i]) {
|
|
780
|
+
name = '';
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
argv = argv.slice(i);
|
|
784
|
+
literal = literal.slice(i);
|
|
785
|
+
name = argv[0].replace(/^.*\//, '').replace(/\.exe$/i, '').toLowerCase();
|
|
786
|
+
}
|
|
787
|
+
if (name !== 'rm')
|
|
788
|
+
continue;
|
|
789
|
+
let recursive = false;
|
|
790
|
+
let force = false;
|
|
791
|
+
const targets = [];
|
|
792
|
+
let flagsDone = false;
|
|
793
|
+
for (let i = 1; i < argv.length; i++) {
|
|
794
|
+
const word = argv[i];
|
|
795
|
+
if (!flagsDone && word === '--') {
|
|
796
|
+
flagsDone = true;
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
if (!flagsDone && /^--/.test(word)) {
|
|
800
|
+
if (/^--recursive$/i.test(word))
|
|
801
|
+
recursive = true;
|
|
802
|
+
if (/^--force$/i.test(word))
|
|
803
|
+
force = true;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (!flagsDone && /^-[A-Za-z]+$/.test(word)) {
|
|
807
|
+
if (/[rR]/.test(word))
|
|
808
|
+
recursive = true;
|
|
809
|
+
if (/f/.test(word))
|
|
810
|
+
force = true;
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
targets.push({ value: word, literal: literal[i] });
|
|
814
|
+
}
|
|
815
|
+
if (!recursive || !force)
|
|
816
|
+
continue;
|
|
817
|
+
for (const target of targets) {
|
|
818
|
+
const t = target.value.trim().replace(/\\/g, '/');
|
|
819
|
+
const lower = t.toLowerCase();
|
|
820
|
+
if (/^\/+\*?$/.test(t))
|
|
821
|
+
return { itemId: 'rm_rf_root', reason: 'recursive force delete of filesystem root' };
|
|
822
|
+
if (/^(?:~|\$home|\$\{home\}|%userprofile%|\$env:userprofile)(?:\/+\*?)?$/.test(lower))
|
|
823
|
+
return { itemId: 'rm_rf_home', reason: 'recursive force delete of the home directory' };
|
|
824
|
+
if (/^(?:\*|\.\/+\*)$/.test(t)) {
|
|
825
|
+
if (inContainer || command.cwdScope === 'scoped')
|
|
826
|
+
continue;
|
|
827
|
+
return { itemId: 'rm_rf_root', reason: 'recursive force delete of the current directory tree (rm -rf *)' };
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return undefined;
|
|
832
|
+
}
|
|
833
|
+
const RM_SCOPE_ITEMS = new Set(['rm_rf_root', 'rm_rf_home']);
|
|
834
|
+
/**
|
|
835
|
+
* Destructive-command decision with the AST in the loop. Returns the regex
|
|
836
|
+
* verdict in 'off'/'shadow' (recording the comparison in shadow), and in
|
|
837
|
+
* 'primary' lets a fully complete AST decide the two rm-scope items — clearing
|
|
838
|
+
* a regex false positive or adding a structurally proven hit the regex missed —
|
|
839
|
+
* while every other destructive rule keeps its regex verdict.
|
|
840
|
+
*/
|
|
841
|
+
function destructiveCommandDecision(value) {
|
|
842
|
+
const regexHit = destructiveCommandReason(value);
|
|
843
|
+
if (shellAstMode === 'off' || !shellAstProvider)
|
|
844
|
+
return regexHit;
|
|
845
|
+
let facts;
|
|
846
|
+
try {
|
|
847
|
+
facts = shellAstProvider(value);
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
facts = undefined;
|
|
851
|
+
}
|
|
852
|
+
const complete = shellAstFactsFullyComplete(facts);
|
|
853
|
+
const astHit = complete && facts ? astDestructiveRm(facts) : undefined;
|
|
854
|
+
const regexRm = regexHit && RM_SCOPE_ITEMS.has(regexHit.itemId) ? regexHit.itemId : undefined;
|
|
855
|
+
lastShellAstComparison = { mode: shellAstMode, complete, regex: regexRm, ast: astHit?.itemId, agree: !complete || regexRm === astHit?.itemId };
|
|
856
|
+
if (shellAstMode !== 'primary' || !complete)
|
|
857
|
+
return regexHit;
|
|
858
|
+
if (astHit)
|
|
859
|
+
return astHit;
|
|
860
|
+
if (regexRm) {
|
|
861
|
+
// The AST cleared the rm-scope hit; other destructive rules still apply.
|
|
862
|
+
return destructiveCommandReason(value, { skipRmScope: true });
|
|
863
|
+
}
|
|
864
|
+
return regexHit;
|
|
865
|
+
}
|
|
866
|
+
// Every `cd <target>` statement start (cwd persists across `&&`, `;`, `|`,
|
|
867
|
+
// newlines and `then`/`do`); the caller keeps the LAST one before the `rm`.
|
|
868
|
+
const CD_STATEMENT = /(?:^|[;&|\n(]|\bthen\b|\bdo\b)\s*cd\s+(?:--\s+)?("[^"]*"|'[^']*'|[^\s;&|)]+)/gi;
|
|
869
|
+
/**
|
|
870
|
+
* Bare `rm -rf *` wipes whatever the CURRENT directory is. When the same
|
|
871
|
+
* script has already `cd`'d into a scoped directory — a literal subpath such
|
|
872
|
+
* as `x`, `./build`, `"$SP/chk"`, `/tmp/work` — the star is that directory's
|
|
873
|
+
* contents: everyday scratch cleanup (`mkdir -p x && cd x && rm -rf * && unzip …`
|
|
874
|
+
* on a bank workstation, 2026-09-24). It stays destructive when the cd target
|
|
875
|
+
* is a filesystem root, a home root, a drive root, `..`/`-`/`.`, or a BARE
|
|
876
|
+
* variable (`cd "$X"` with X unset lands in $HOME). No cd at all keeps the
|
|
877
|
+
* conservative reading: the agent's cwd is usually the project.
|
|
878
|
+
*/
|
|
879
|
+
function cdScopedBeforeBareStar(before) {
|
|
880
|
+
let last = null;
|
|
881
|
+
CD_STATEMENT.lastIndex = 0;
|
|
882
|
+
for (let m = CD_STATEMENT.exec(before); m; m = CD_STATEMENT.exec(before))
|
|
883
|
+
last = m;
|
|
884
|
+
if (!last)
|
|
885
|
+
return false;
|
|
886
|
+
// A `)` after the cd closes a subshell: `(cd x && …); rm -rf *` runs the rm
|
|
887
|
+
// back in the ORIGINAL directory, which the cd never changed.
|
|
888
|
+
if (before.slice(last.index + last[0].length).includes(')'))
|
|
889
|
+
return false;
|
|
890
|
+
const target = last[1].replace(/^["']|["']$/g, '').trim().replace(/\\/g, '/');
|
|
891
|
+
if (!target)
|
|
892
|
+
return false;
|
|
893
|
+
const lower = target.toLowerCase();
|
|
894
|
+
if (/^(?:\/+\*?|~\/*\*?|\.{1,2}\/?|-|\$home\/?|\$\{home\}\/?|%userprofile%\/?|\$env:userprofile\/?|[a-z]:\/*\*?)$/.test(lower))
|
|
895
|
+
return false;
|
|
896
|
+
if (/^(?:\$\{?[a-z_][a-z0-9_]*\}?|%[a-z_][a-z0-9_]*%|\$env:[a-z_][a-z0-9_]*)\/?$/.test(lower))
|
|
897
|
+
return false;
|
|
898
|
+
// At least one real path component that is not `.`/`..`.
|
|
899
|
+
return lower.split('/').some(part => part && part !== '.' && part !== '..');
|
|
900
|
+
}
|
|
901
|
+
function destructiveCommandReason(value, opts = {}) {
|
|
726
902
|
return matchCommand(value, (text, lower) => {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
903
|
+
if (!opts.skipRmScope) {
|
|
904
|
+
// Flag cluster is order-independent (`-rf`, `-fr`, `-rfv`). Terminators
|
|
905
|
+
// include `)` so command substitution — `$(rm -rf /)` runs BEFORE the
|
|
906
|
+
// outer command — is caught too. Path-aware: only the root `/`, the root
|
|
907
|
+
// wildcard `/*`, and a bare `*` are treated as destructive — scoped paths
|
|
908
|
+
// (`/tmp/...`, `/var/cache/...`, `./build`) are everyday cleanup.
|
|
909
|
+
if (/\b(?:sudo\s+)?rm\s+-(?=[a-z]*r)(?=[a-z]*f)[a-z]+\s+["']?\/\*?["']?(?:\s|$|[;&|)])/.test(lower))
|
|
910
|
+
return { itemId: 'rm_rf_root', reason: 'recursive force delete of filesystem root' };
|
|
911
|
+
// Split or long flags: `rm -r -f /`, `rm -f -r /`, `rm --recursive --force /`
|
|
912
|
+
// (the structural parser saw these first — 2026-09-24; the floor must too).
|
|
913
|
+
const splitFlags = /\b(?:sudo\s+)?rm\s+((?:-(?:[a-z]+|-recursive|-force|-no-preserve-root)\s+){2,})["']?\/\*?["']?(?:\s|$|[;&|)])/.exec(lower);
|
|
914
|
+
if (splitFlags && /(?:^|\s)-(?:[a-z]*r|-recursive)/.test(splitFlags[1]) && /(?:^|\s)-(?:[a-z]*f|-force)/.test(splitFlags[1]))
|
|
915
|
+
return { itemId: 'rm_rf_root', reason: 'recursive force delete of filesystem root' };
|
|
916
|
+
// Home-ROOT only (`~`, `$HOME`, `%USERPROFILE%`, optional `/` or `/*`).
|
|
917
|
+
// A subdirectory (`~/.cache`, `~/node_modules`) is everyday cleanup.
|
|
918
|
+
if (/\b(?:sudo\s+)?rm\s+-(?=[a-z]*r)(?=[a-z]*f)[a-z]+\s+["']?(?:~|\$home|\$\{home\}|%userprofile%|\$env:userprofile)(?:\/\*?)?["']?(?:\s|$|[;&|)])/.test(lower)) {
|
|
919
|
+
return { itemId: 'rm_rf_home', reason: 'recursive force delete of the home directory' };
|
|
920
|
+
}
|
|
921
|
+
// Bare `*` deletes the current directory tree — destructive on the HOST,
|
|
922
|
+
// but inside a container exec/run it is the container workdir (cleanup),
|
|
923
|
+
// and after an explicit `cd <subdir>` it is that scratch folder's contents.
|
|
924
|
+
// EVERY bare-star rm on the line is judged: `(cd x && rm -rf *); rm -rf *`
|
|
925
|
+
// has one scoped and one unscoped removal, and the unscoped one wipes.
|
|
926
|
+
const bareStars = /\b(?:sudo\s+)?rm\s+-(?=[a-z]*r)(?=[a-z]*f)[a-z]+\s+\*(?:\s|$|[;&|)])/g;
|
|
927
|
+
for (let bareStar = bareStars.exec(lower); bareStar; bareStar = bareStars.exec(lower)) {
|
|
928
|
+
const before = lower.slice(0, bareStar.index);
|
|
929
|
+
if (!CONTAINER_EXEC_BEFORE.test(before) && !cdScopedBeforeBareStar(before)) {
|
|
930
|
+
return { itemId: 'rm_rf_root', reason: 'recursive force delete of the current directory tree (rm -rf *)' };
|
|
931
|
+
}
|
|
932
|
+
}
|
|
738
933
|
}
|
|
739
|
-
// Bare `*` deletes the current directory tree — destructive on the HOST,
|
|
740
|
-
// but inside a container exec/run it is the container workdir (cleanup).
|
|
741
|
-
const bareStar = /\b(?:sudo\s+)?rm\s+-(?=[a-z]*r)(?=[a-z]*f)[a-z]+\s+\*(?:\s|$|[;&|)])/.exec(lower);
|
|
742
|
-
if (bareStar && !CONTAINER_EXEC_BEFORE.test(lower.slice(0, bareStar.index)))
|
|
743
|
-
return { itemId: 'rm_rf_root', reason: 'recursive force delete of filesystem root' };
|
|
744
934
|
// Windows recursive quiet drive delete via del/erase/rd/rmdir. Flag order
|
|
745
935
|
// is independent (`/s /q` and `/q /s` both wipe), and the dequoted variant
|
|
746
936
|
// handles `"C:\"`. rd/rmdir are the same wipe under a different verb — the
|
|
@@ -1029,6 +1219,11 @@ function interpreterCodeSpawnsProcess(before, code) {
|
|
|
1029
1219
|
return true;
|
|
1030
1220
|
return BACKTICK_SHELL_INTERPRETER.test(before) && /`[^`]*`|%x[\[{(]/.test(code);
|
|
1031
1221
|
}
|
|
1222
|
+
// An HTTP client in the same code makes a metadata endpoint literal a request target, not prose.
|
|
1223
|
+
const CODE_OPENS_REQUEST = /\b(?:urlopen|Request|requests|httpx|urllib3|aiohttp|http\.client|HTTPS?Connection|fetch|axios|got|XMLHttpRequest|https?\.(?:get|request)|Net::HTTP|LWP|HTTP::Tiny|URI\.open|curl_exec|file_get_contents|fsockopen|socket)\b/;
|
|
1224
|
+
function interpreterCodeRequestsMetadata(code) {
|
|
1225
|
+
return Boolean(containsMetadataEndpoint(code)) && CODE_OPENS_REQUEST.test(code);
|
|
1226
|
+
}
|
|
1032
1227
|
/** First path argument of direct file APIs in inline language code, not printed examples. */
|
|
1033
1228
|
function interpreterFileArguments(text) {
|
|
1034
1229
|
if (text.length > 32_000)
|
|
@@ -1116,7 +1311,7 @@ function stripInertDataSegments(text) {
|
|
|
1116
1311
|
const before = src.slice(Math.max(0, offset - 80), offset);
|
|
1117
1312
|
// Language-interpreter code: data unless it spawns a process (see above).
|
|
1118
1313
|
if (LANG_INTERPRETER_CODE_BEFORE.test(before))
|
|
1119
|
-
return interpreterCodeSpawnsProcess(before, body) ? whole : INERT_DATA;
|
|
1314
|
+
return interpreterCodeSpawnsProcess(before, body) || interpreterCodeRequestsMetadata(body) ? whole : INERT_DATA;
|
|
1120
1315
|
if (CODE_CONTEXT_BEFORE.test(before))
|
|
1121
1316
|
return whole; // code position: executes
|
|
1122
1317
|
if (dq !== undefined && INTERPOLATES.test(body))
|
|
@@ -1353,7 +1548,7 @@ function scanTextValue(toolName, rawValue, options, commandContext = false) {
|
|
|
1353
1548
|
if (finding)
|
|
1354
1549
|
return finding;
|
|
1355
1550
|
}
|
|
1356
|
-
const destructive =
|
|
1551
|
+
const destructive = destructiveCommandDecision(value);
|
|
1357
1552
|
if (destructive) {
|
|
1358
1553
|
return builtIn(destructive.itemId, 'destructive_filesystem', 'destructive_command', 'local-destructive-command', `Blocked ${destructive.reason}.`, value, destructive.reason, options);
|
|
1359
1554
|
}
|
package/dist/commands/hook.d.ts
CHANGED
|
@@ -193,7 +193,27 @@ export interface HookIo {
|
|
|
193
193
|
* mode, where `exit` only unwinds the evaluation and the process lives on.
|
|
194
194
|
*/
|
|
195
195
|
exitsProcess: boolean;
|
|
196
|
+
/** Evaluated inside the resident daemon for a thin client (see DaemonDeclined). */
|
|
197
|
+
viaDaemon?: boolean;
|
|
198
|
+
/** Set when this evaluation's verdict is a stop still being settled (approval wait). */
|
|
199
|
+
pendingStop?: string;
|
|
196
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* The daemon declines a request whose verdict is a human wait (org approval,
|
|
203
|
+
* taint approve-once). The thin client stops listening after ~2.5s and
|
|
204
|
+
* re-evaluates locally, so a daemon-side wait only opened a second approval
|
|
205
|
+
* request nobody would ever read. Declining sends the client to its local
|
|
206
|
+
* evaluation at once, where the one wait runs in the developer's own process.
|
|
207
|
+
*/
|
|
208
|
+
export declare class DaemonDeclined extends Error {
|
|
209
|
+
}
|
|
210
|
+
/** Absolute time a human wait must end by: its own cap, and never past the IDE's timeout. */
|
|
211
|
+
export declare function humanWaitDeadline(waitMs: number, now?: number, startedAt?: number, ideTimeoutMs?: number): number;
|
|
212
|
+
/** Verdict for an unexpected error that escaped the evaluation (process-level handlers). */
|
|
213
|
+
export declare function crashVerdictForProcess(event: string | undefined, message: string): {
|
|
214
|
+
stdout: string;
|
|
215
|
+
exitCode: number;
|
|
216
|
+
};
|
|
197
217
|
/**
|
|
198
218
|
* Daemon-side entry: evaluate one hook request in-process and return the
|
|
199
219
|
* verdict instead of writing/exiting. Everything the per-event hook process
|
|
@@ -202,7 +222,9 @@ export interface HookIo {
|
|
|
202
222
|
* Mirrors hookCommand's fail-open contract: any unexpected exception yields
|
|
203
223
|
* an allow verdict, never an error the thin client would surface.
|
|
204
224
|
*/
|
|
205
|
-
export declare function evaluateHookRequest(args: HookArgs, stdinRaw: string, config: BotGuardConfig
|
|
225
|
+
export declare function evaluateHookRequest(args: HookArgs, stdinRaw: string, config: BotGuardConfig, opts?: {
|
|
226
|
+
viaDaemon?: boolean;
|
|
227
|
+
}): Promise<{
|
|
206
228
|
stdout: string;
|
|
207
229
|
stderr: string;
|
|
208
230
|
exitCode: number;
|
|
@@ -217,4 +239,15 @@ export declare function evaluateHookRequest(args: HookArgs, stdinRaw: string, co
|
|
|
217
239
|
* through respond() inside and never reach this catch.
|
|
218
240
|
*/
|
|
219
241
|
export declare function hookCommand(args: HookArgs, config: BotGuardConfig): Promise<void>;
|
|
242
|
+
/**
|
|
243
|
+
* Errors that escape every await (an async 'error' event from a child process,
|
|
244
|
+
* a stray rejection) never reach the try/catch around the evaluation. Without
|
|
245
|
+
* these handlers Node prints a stack and exits 1 with NO verdict, which every
|
|
246
|
+
* client reads as a non-blocking hook error — the action runs. Route them
|
|
247
|
+
* through the same contract as a caught crash instead.
|
|
248
|
+
*/
|
|
249
|
+
export declare function installHookCrashHandlers(verdict: (message: string) => {
|
|
250
|
+
stdout: string;
|
|
251
|
+
exitCode: number;
|
|
252
|
+
}): void;
|
|
220
253
|
export {};
|