fullcourtdefense-cli 1.34.21 → 1.34.25
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 +1 -0
- package/dist/actionIdentity.d.ts +1 -0
- package/dist/actionIdentity.js +2 -0
- package/dist/actionPolicyEngine.d.ts +5 -0
- package/dist/actionPolicyEngine.js +273 -30
- package/dist/agentTerminal.d.ts +1 -3
- package/dist/agentTerminal.js +14 -29
- package/dist/approvalPrompt.d.ts +7 -0
- package/dist/approvalPrompt.js +46 -0
- package/dist/askDialog.d.ts +87 -0
- package/dist/askDialog.js +209 -0
- package/dist/blockExplanation.js +18 -6
- package/dist/boundaryVerification.d.ts +20 -0
- package/dist/boundaryVerification.js +147 -0
- package/dist/commands/daemon.js +198 -70
- package/dist/commands/deterministicGuard.d.ts +2 -0
- package/dist/commands/deterministicGuard.js +161 -9
- package/dist/commands/hook.js +81 -29
- package/dist/commands/installClaudeHook.js +11 -5
- package/dist/commands/installCursorHook.js +5 -4
- package/dist/commands/mcpGateway.d.ts +2 -0
- package/dist/commands/mcpGateway.js +163 -56
- package/dist/commands/onboard.js +6 -5
- package/dist/commands/onboardingSummary.d.ts +5 -0
- package/dist/commands/onboardingSummary.js +11 -0
- package/dist/commands/protectedRun.d.ts +14 -0
- package/dist/commands/protectedRun.js +207 -0
- package/dist/containment-runtime/index.mjs +30118 -0
- package/dist/containment-runtime/licenses/_anthropic-ai_sandbox-runtime.txt +201 -0
- package/dist/containment-runtime/licenses/_pondwader_socks5-server.txt +20 -0
- package/dist/containment-runtime/licenses/commander.txt +22 -0
- package/dist/containment-runtime/licenses/node-forge.txt +331 -0
- package/dist/containment-runtime/licenses/zod.txt +21 -0
- package/dist/containment-runtime/runtime-version.json +1 -0
- package/dist/containment-runtime/vendor/java-proxy-agent/build.ts +95 -0
- package/dist/containment-runtime/vendor/java-proxy-agent/srt-proxy-agent.jar +0 -0
- package/dist/containment-runtime/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/dist/containment-runtime/vendor/seccomp/build.ts +69 -0
- package/dist/containment-runtime/vendor/seccomp/x64/apply-seccomp +0 -0
- package/dist/containment-runtime/vendor/srt-win/arm64/srt-win.exe +0 -0
- package/dist/containment-runtime/vendor/srt-win/build.ts +21 -0
- package/dist/containment-runtime/vendor/srt-win/x64/srt-win.exe +0 -0
- package/dist/containmentLease.d.ts +5 -0
- package/dist/containmentLease.js +72 -0
- package/dist/detectionUpdates.d.ts +23 -3
- package/dist/detectionUpdates.js +45 -14
- package/dist/detectorArtifact.d.ts +17 -0
- package/dist/detectorArtifact.js +35 -0
- package/dist/detectorBaseline.d.ts +2 -0
- package/dist/detectorBaseline.js +5 -0
- package/dist/detectorModule.d.ts +9 -0
- package/dist/detectorModule.js +84 -0
- package/dist/detectorRuntime.d.ts +21 -0
- package/dist/detectorRuntime.js +49 -0
- package/dist/detectorUpdates.d.ts +21 -0
- package/dist/detectorUpdates.js +86 -0
- package/dist/devConfirm.d.ts +97 -15
- package/dist/devConfirm.js +447 -117
- package/dist/distress.d.ts +2 -0
- package/dist/distress.js +2 -0
- package/dist/frictionRollout.js +103 -0
- package/dist/gatewayRuntime.d.ts +19 -0
- package/dist/gatewayRuntime.js +93 -0
- package/dist/hookSlim.js +17 -18
- package/dist/index.js +35 -0
- package/dist/localDetectionUpdates.d.ts +3 -1
- package/dist/localDetectionUpdates.js +8 -1
- package/dist/localSafetySnapshot.d.ts +9 -5
- package/dist/localSafetySnapshot.js +42 -15
- package/dist/mcpToolReview.d.ts +17 -0
- package/dist/mcpToolReview.js +127 -0
- package/dist/mcpToolTrust.d.ts +22 -0
- package/dist/mcpToolTrust.js +66 -0
- package/dist/nativeHook.d.ts +4 -0
- package/dist/nativeHook.js +31 -17
- package/dist/policyRefresh.d.ts +3 -0
- package/dist/policyRefresh.js +27 -0
- package/dist/protectionMaintenance.d.ts +10 -0
- package/dist/protectionMaintenance.js +64 -0
- package/dist/protectionProfile.d.ts +26 -0
- package/dist/protectionProfile.js +63 -0
- package/dist/runtimeConfig.d.ts +2 -0
- package/dist/runtimeConfig.js +65 -26
- package/dist/selfTest.d.ts +2 -0
- package/dist/selfTest.js +9 -1
- package/dist/sessionLimits.d.ts +2 -0
- package/dist/sessionLimits.js +24 -0
- package/dist/telemetry.d.ts +5 -0
- package/dist/telemetry.js +8 -0
- package/dist/version.json +1 -1
- package/package.json +10 -6
|
@@ -168,7 +168,11 @@ const METADATA_ENDPOINTS = [
|
|
|
168
168
|
{ itemId: 'alibaba_metadata_ip', value: '100.100.100.200', label: 'Alibaba cloud metadata endpoint' },
|
|
169
169
|
];
|
|
170
170
|
const OUTBOUND_TOOL_HINT = /(?:http|fetch|request|curl|wget|webhook|upload|send|email|mail|slack|discord|teams|post|put|publish|notify)/i;
|
|
171
|
-
const
|
|
171
|
+
const WEB_LOOKUP_TOOL_HINT = /(?:^|__)web(?:[_.-]*run)$/i;
|
|
172
|
+
// Match execution words, not substrings: webrun and describe_subscription do
|
|
173
|
+
// not execute their text. The web lookup contract also distinguishes web.run
|
|
174
|
+
// from generic run tools. Explicit command fields remain actionable everywhere.
|
|
175
|
+
const COMMAND_TOOL_HINT = /\b(?:shell|bash|command|cmd|powershell|pwsh|terminal|exec|execute|subprocess|run|script|repl)\b/i;
|
|
172
176
|
const SQL_TOOL_HINT = /(?:sql|query|database|postgres|mysql|sqlite|snowflake|bigquery|db)/i;
|
|
173
177
|
const COMMAND_KEYS = new Set([
|
|
174
178
|
'command',
|
|
@@ -802,7 +806,9 @@ function isLocalFileEditor(toolName, candidates) {
|
|
|
802
806
|
});
|
|
803
807
|
}
|
|
804
808
|
function isOutboundContext(toolName, candidates) {
|
|
805
|
-
|
|
809
|
+
// Web lookup arguments leave the machine, but query text is neither a shell
|
|
810
|
+
// command nor the destination URL. Keep secret egress separate from execution.
|
|
811
|
+
if (OUTBOUND_TOOL_HINT.test(toolName) || WEB_LOOKUP_TOOL_HINT.test(toolName))
|
|
806
812
|
return true;
|
|
807
813
|
if (isLocalFileEditor(toolName, candidates))
|
|
808
814
|
return false;
|
|
@@ -810,12 +816,17 @@ function isOutboundContext(toolName, candidates) {
|
|
|
810
816
|
}
|
|
811
817
|
function isCommandContext(toolName, candidate) {
|
|
812
818
|
const lastKey = candidate.keyPath.split('.').pop() || '';
|
|
813
|
-
|
|
819
|
+
const nameWords = toolName.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_\-.]+/g, ' ');
|
|
820
|
+
return (!WEB_LOOKUP_TOOL_HINT.test(toolName) && COMMAND_TOOL_HINT.test(nameWords))
|
|
821
|
+
|| COMMAND_KEYS.has(lastKey) || COMMAND_KEYS.has(candidate.keyPath);
|
|
814
822
|
}
|
|
815
823
|
function isSqlContext(toolName, candidate) {
|
|
816
824
|
const lastKey = candidate.keyPath.split('.').pop() || '';
|
|
817
825
|
return SQL_TOOL_HINT.test(toolName) || SQL_KEYS.has(lastKey) || SQL_KEYS.has(candidate.keyPath);
|
|
818
826
|
}
|
|
827
|
+
function isWebLookupTarget(toolName, candidate) {
|
|
828
|
+
return WEB_LOOKUP_TOOL_HINT.test(toolName) && candidate.keyPath.split('.').pop() === 'ref_id';
|
|
829
|
+
}
|
|
819
830
|
/**
|
|
820
831
|
* Metadata SSRF only matters where the string can actually BECOME a request: a shell
|
|
821
832
|
* command, an outbound/HTTP tool, or a URL-typed argument. Source code or file contents
|
|
@@ -825,6 +836,8 @@ function isSqlContext(toolName, candidate) {
|
|
|
825
836
|
function isMetadataRequestContext(toolName, candidate) {
|
|
826
837
|
if (isCommandContext(toolName, candidate))
|
|
827
838
|
return true;
|
|
839
|
+
if (isWebLookupTarget(toolName, candidate))
|
|
840
|
+
return true;
|
|
828
841
|
if (OUTBOUND_TOOL_HINT.test(toolName))
|
|
829
842
|
return true;
|
|
830
843
|
const lastKey = candidate.keyPath.split('.').pop() || '';
|
|
@@ -838,6 +851,8 @@ function isMetadataRequestContext(toolName, candidate) {
|
|
|
838
851
|
function isSensitivePathContext(toolName, candidate) {
|
|
839
852
|
if (isCommandContext(toolName, candidate))
|
|
840
853
|
return true;
|
|
854
|
+
if (isWebLookupTarget(toolName, candidate))
|
|
855
|
+
return true;
|
|
841
856
|
if (/(?:read|open|cat|copy|download|upload|access|stat|list|glob|search|find|grep)/i.test(toolName))
|
|
842
857
|
return true;
|
|
843
858
|
const lastKey = candidate.keyPath.split('.').pop() || '';
|
|
@@ -873,7 +888,8 @@ const DATA_TO_INTERPRETER = /\b(?:iex|invoke-expression|invoke-command)\b|\beval
|
|
|
873
888
|
// a shell command line. `node -e "console.log('rm -rf /')"` prints a string;
|
|
874
889
|
// `python -c "print('DROP TABLE')"` prints a string. That code only reaches the
|
|
875
890
|
// shell through a process-spawning API — then (and only then) the whole line is
|
|
876
|
-
// kept, exactly like `bash -c`.
|
|
891
|
+
// kept, exactly like `bash -c`. Direct file arguments have a separate sensitive
|
|
892
|
+
// path view below, so a reader does not turn its other string data into shell code.
|
|
877
893
|
const LANG_INTERPRETER_CODE_BEFORE = /\b(?:node|nodejs|deno|bun|python[0-9.]*|py|perl|ruby|php|lua|tsx|ts-node)(?:\.exe)?\s+(?:-c|-e|-r|eval)[= ]\s*$/i;
|
|
878
894
|
const CODE_SPAWNS_PROCESS = /\b(?:child_process|execSync|execFileSync|spawnSync|spawn|execFile|exec|fork|os\.system|os\.popen|os\.exec[lvpe]*|subprocess|Popen|system|passthru|shell_exec|proc_open|popen|Process\.Start|Deno\.run|Deno\.Command|Bun\.spawn)\b/;
|
|
879
895
|
const BACKTICK_SHELL_INTERPRETER = /\b(?:perl|ruby)(?:\.exe)?\s+(?:-c|-e)[= ]\s*$/i;
|
|
@@ -882,6 +898,38 @@ function interpreterCodeSpawnsProcess(before, code) {
|
|
|
882
898
|
return true;
|
|
883
899
|
return BACKTICK_SHELL_INTERPRETER.test(before) && /`[^`]*`|%x[\[{(]/.test(code);
|
|
884
900
|
}
|
|
901
|
+
/** First path argument of direct file APIs in inline language code, not printed examples. */
|
|
902
|
+
function interpreterFileArguments(text) {
|
|
903
|
+
if (text.length > 32_000)
|
|
904
|
+
return '';
|
|
905
|
+
const paths = [];
|
|
906
|
+
let calls = 0;
|
|
907
|
+
text.replace(/"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'/g, (whole, dq, sq, offset) => {
|
|
908
|
+
if (!LANG_INTERPRETER_CODE_BEFORE.test(text.slice(Math.max(0, offset - 80), offset)))
|
|
909
|
+
return whole;
|
|
910
|
+
const code = dq !== undefined ? dq : (sq || '');
|
|
911
|
+
// Preserve offsets while masking string contents: a call printed inside a
|
|
912
|
+
// string is not an invocation; a sensitive path in another argument is data.
|
|
913
|
+
const instructions = code.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, literal => ' '.repeat(literal.length));
|
|
914
|
+
for (const call of instructions.matchAll(/\b(?:open|fopen|readFile(?:Sync)?|createReadStream|ReadAllText|ReadAllBytes)\s*\(/g)) {
|
|
915
|
+
if (++calls > 48)
|
|
916
|
+
break;
|
|
917
|
+
const start = (call.index || 0) + call[0].length;
|
|
918
|
+
let depth = 1;
|
|
919
|
+
for (let end = start; end < instructions.length; end++) {
|
|
920
|
+
const ch = instructions[end];
|
|
921
|
+
if ((ch === ')' && --depth === 0) || (ch === ',' && depth === 1)) {
|
|
922
|
+
paths.push(code.slice(start, end));
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
if (ch === '(')
|
|
926
|
+
depth++;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return whole;
|
|
930
|
+
});
|
|
931
|
+
return paths.join('\n');
|
|
932
|
+
}
|
|
885
933
|
// $( … ) always executes; backtick pairs only with ≥2 chars of content
|
|
886
934
|
// (PowerShell escapes like `n / `t are single-char and inert).
|
|
887
935
|
const INTERPOLATES = /\$\(|`[^`]{2,}`/;
|
|
@@ -918,7 +966,104 @@ function stripInertDataSegments(text) {
|
|
|
918
966
|
});
|
|
919
967
|
return out;
|
|
920
968
|
}
|
|
921
|
-
|
|
969
|
+
const FILE_METADATA_PROPERTIES = new Set([
|
|
970
|
+
'name', 'fullname', 'basename', 'extension', 'length', 'attributes', 'exists',
|
|
971
|
+
'creationtime', 'creationtimeutc', 'lastaccesstime', 'lastaccesstimeutc',
|
|
972
|
+
'lastwritetime', 'lastwritetimeutc', 'directoryname', 'psiscontainer',
|
|
973
|
+
]);
|
|
974
|
+
/** Prove a whole pipeline returns only file metadata, never a file reader/handle consumer. */
|
|
975
|
+
function isFileMetadataQuery(statement) {
|
|
976
|
+
// Expressions, substitutions, redirections and script blocks are unproven.
|
|
977
|
+
if (/[@`{}()[\]<>;&\r\n]/.test(statement))
|
|
978
|
+
return false;
|
|
979
|
+
const tokens = statement.trim().match(/"[^"\r\n]*"|'(?:[^'\r\n]|'')*'|[|,]|[^\s"'|,]+/g) || [];
|
|
980
|
+
if (!/^(?:get-item|gi)$/i.test(tokens[0] || ''))
|
|
981
|
+
return false;
|
|
982
|
+
const pipe = tokens.indexOf('|');
|
|
983
|
+
const args = tokens.slice(1, pipe < 0 ? undefined : pipe);
|
|
984
|
+
let paths = 0;
|
|
985
|
+
for (let i = 0; i < args.length; i++) {
|
|
986
|
+
const arg = args[i];
|
|
987
|
+
if (/^-(?:literalpath|path|force)$/i.test(arg) || arg === ',')
|
|
988
|
+
continue;
|
|
989
|
+
if (/^-(?:erroraction|ea)$/i.test(arg)) {
|
|
990
|
+
if (!/^(?:silentlycontinue|stop|continue|ignore)$/i.test(args[++i] || ''))
|
|
991
|
+
return false;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
// Output variables can retain FileInfo objects for a later content read.
|
|
995
|
+
if (arg.startsWith('-'))
|
|
996
|
+
return false;
|
|
997
|
+
paths++;
|
|
998
|
+
}
|
|
999
|
+
if (!paths)
|
|
1000
|
+
return false;
|
|
1001
|
+
if (pipe < 0)
|
|
1002
|
+
return true; // Default Get-Item formatting contains metadata only.
|
|
1003
|
+
if (!/^(?:select-object|select)$/i.test(tokens[pipe + 1] || ''))
|
|
1004
|
+
return false;
|
|
1005
|
+
const properties = tokens.slice(pipe + 2);
|
|
1006
|
+
if (/^-property$/i.test(properties[0] || ''))
|
|
1007
|
+
properties.shift();
|
|
1008
|
+
const names = properties.filter(token => token !== ',');
|
|
1009
|
+
return names.length > 0 && names.every(token => FILE_METADATA_PROPERTIES.has(token.replace(/^['"]|['"]$/g, '').toLowerCase()));
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Sensitive-path view of a shell command. Omit only proven metadata queries;
|
|
1013
|
+
* preserve all other statements and their consumers. Quotes and nested scopes
|
|
1014
|
+
* keep separators inside data/code from creating fictitious cmdlet invocations.
|
|
1015
|
+
* Unknown execution/rebinding forms retain the original conservative scan.
|
|
1016
|
+
*/
|
|
1017
|
+
function credentialAccessCommandText(text) {
|
|
1018
|
+
if (text.length > 32_000)
|
|
1019
|
+
return text;
|
|
1020
|
+
// PowerShell 7 permits a pipeline to continue on a line starting with `|`,
|
|
1021
|
+
// even across comment lines. A trailing comma also continues an argument list.
|
|
1022
|
+
// Keep these forms intact so a later consumer cannot lose its source path.
|
|
1023
|
+
const allowMetadata = !(/(?:^|[\r\n])[ \t]*\||,[ \t]*(?:#[^\r\n]*)?[\r\n]/.test(text)
|
|
1024
|
+
|| /`|(?:^|[^&])&(?:[^&]|$)|@['"]|(?:^|[;&|\r\n])\s*\.\s|\b(?:iex|invoke-expression|invoke-command|eval|function|filter|set-alias|new-alias|sal|nal|set-item|new-item|update-typedata|add-type|import-module|ipmo|PSDefaultParameterValues)\b/i.test(text));
|
|
1025
|
+
const statements = [];
|
|
1026
|
+
let current = '', quote = '', depth = 0;
|
|
1027
|
+
for (let i = 0; i < text.length; i++) {
|
|
1028
|
+
const ch = text[i];
|
|
1029
|
+
if (quote) {
|
|
1030
|
+
current += ch;
|
|
1031
|
+
if (ch === quote) {
|
|
1032
|
+
if (quote === "'" && text[i + 1] === "'")
|
|
1033
|
+
current += text[++i];
|
|
1034
|
+
else
|
|
1035
|
+
quote = '';
|
|
1036
|
+
}
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (ch === '"' || ch === "'") {
|
|
1040
|
+
quote = ch;
|
|
1041
|
+
current += ch;
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
if (ch === '(' || ch === '{' || ch === '[')
|
|
1045
|
+
depth++;
|
|
1046
|
+
if (ch === ')' || ch === '}' || ch === ']') {
|
|
1047
|
+
if (--depth < 0)
|
|
1048
|
+
return text;
|
|
1049
|
+
}
|
|
1050
|
+
const conditional = (ch === '&' || ch === '|') && text[i + 1] === ch;
|
|
1051
|
+
if (depth === 0 && (conditional || ch === ';' || ch === '\r' || ch === '\n')) {
|
|
1052
|
+
if (conditional)
|
|
1053
|
+
i++;
|
|
1054
|
+
statements.push(current);
|
|
1055
|
+
current = '';
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
current += ch === '|' || ch === '&' || ch === ';' ? ` ${ch} ` : ch;
|
|
1059
|
+
}
|
|
1060
|
+
if (quote || depth)
|
|
1061
|
+
return text;
|
|
1062
|
+
statements.push(current);
|
|
1063
|
+
// A real shell separator is a boundary of the preceding path, even without spaces.
|
|
1064
|
+
return statements.map(statement => allowMetadata && isFileMetadataQuery(statement) ? 'fcd_file_metadata' : statement).join('\n');
|
|
1065
|
+
}
|
|
1066
|
+
function scanTextValue(toolName, rawValue, options, commandContext = false) {
|
|
922
1067
|
// Honeypot decoys and org custom patterns scan the RAW value: a decoy path
|
|
923
1068
|
// even inside message data is exfiltration staging, and custom rules define
|
|
924
1069
|
// their own scope.
|
|
@@ -937,7 +1082,8 @@ function scanTextValue(toolName, rawValue, options) {
|
|
|
937
1082
|
// NOTE: a disabled item must FALL THROUGH to the later rules, not end the
|
|
938
1083
|
// scan — otherwise turning off (or gating) a path rule would mask a real
|
|
939
1084
|
// reverse shell / destructive command in the same value.
|
|
940
|
-
const sensitivePath = containsSensitiveCredentialPath(
|
|
1085
|
+
const sensitivePath = containsSensitiveCredentialPath(commandContext
|
|
1086
|
+
? `${credentialAccessCommandText(value)}\n${interpreterFileArguments(rawValue)}` : value);
|
|
941
1087
|
if (sensitivePath) {
|
|
942
1088
|
const finding = builtIn(sensitivePath.itemId, 'sensitive_files', 'sensitive_file', 'local-sensitive-credential-path', `Blocked local agent access to ${sensitivePath.label}.`, value, sensitivePath.label, options);
|
|
943
1089
|
if (finding)
|
|
@@ -985,6 +1131,12 @@ function isActionableContext(toolName, candidate) {
|
|
|
985
1131
|
|| OUTBOUND_TOOL_HINT.test(toolName);
|
|
986
1132
|
}
|
|
987
1133
|
function scanDeterministicToolCall(toolName, toolArgs, options) {
|
|
1134
|
+
// MCP adapters keep the server separately from its method. Restore that
|
|
1135
|
+
// identity for the known web lookup contract; keep other method matching
|
|
1136
|
+
// unchanged so a server's name cannot turn its documentation into a command.
|
|
1137
|
+
const qualifiedName = options?.mcpServerName ? `mcp__${options.mcpServerName}__${toolName}` : toolName;
|
|
1138
|
+
if (WEB_LOOKUP_TOOL_HINT.test(qualifiedName))
|
|
1139
|
+
toolName = qualifiedName;
|
|
988
1140
|
const candidates = collectStrings(toolArgs);
|
|
989
1141
|
const outbound = isOutboundContext(toolName, candidates);
|
|
990
1142
|
for (const candidate of candidates) {
|
|
@@ -998,7 +1150,7 @@ function scanDeterministicToolCall(toolName, toolArgs, options) {
|
|
|
998
1150
|
return custom;
|
|
999
1151
|
}
|
|
1000
1152
|
for (const candidate of candidates) {
|
|
1001
|
-
const finding = scanTextValue(toolName, candidate.value, options);
|
|
1153
|
+
const finding = scanTextValue(toolName, candidate.value, options, isCommandContext(toolName, candidate));
|
|
1002
1154
|
if (!finding)
|
|
1003
1155
|
continue;
|
|
1004
1156
|
// Context gates: a pattern is only dangerous where it can act — a command, a request
|
|
@@ -1210,13 +1362,13 @@ function scanReferencedScript(command, cwd, options) {
|
|
|
1210
1362
|
// string mentions and rescan so command-shaped rules (reverse shell, destructive
|
|
1211
1363
|
// delete...) in the SAME file still surface.
|
|
1212
1364
|
let scanOptions = options;
|
|
1213
|
-
let finding = scanTextValue('script_file', content, scanOptions);
|
|
1365
|
+
let finding = scanTextValue('script_file', content, scanOptions, true);
|
|
1214
1366
|
while (finding
|
|
1215
1367
|
&& (finding.category === 'sensitive_file' || finding.category === 'metadata_ssrf')
|
|
1216
1368
|
&& !scriptActsOnFinding(content, finding.category)) {
|
|
1217
1369
|
const baseDisabled = scanOptions?.disabledBuiltInItemIds || Array.from(exports.DEFAULT_DISABLED_BUILT_INS);
|
|
1218
1370
|
scanOptions = { ...(scanOptions || {}), disabledBuiltInItemIds: [...baseDisabled, finding.itemId] };
|
|
1219
|
-
finding = scanTextValue('script_file', content, scanOptions);
|
|
1371
|
+
finding = scanTextValue('script_file', content, scanOptions, true);
|
|
1220
1372
|
}
|
|
1221
1373
|
if (!finding)
|
|
1222
1374
|
continue;
|
package/dist/commands/hook.js
CHANGED
|
@@ -63,6 +63,7 @@ const verdictIpc_1 = require("../verdictIpc");
|
|
|
63
63
|
const machineIdentity_1 = require("../machineIdentity");
|
|
64
64
|
const actionPolicyEngine_1 = require("../actionPolicyEngine");
|
|
65
65
|
const devConfirm_1 = require("../devConfirm");
|
|
66
|
+
const askDialog_1 = require("../askDialog");
|
|
66
67
|
// Shared with the slim hook entry (hookSlim.ts) so both entries log to the
|
|
67
68
|
// same rotated hook.log and read stdin identically.
|
|
68
69
|
const hookIo_1 = require("../hookIo");
|
|
@@ -480,7 +481,7 @@ function buildToolCall(event, p) {
|
|
|
480
481
|
|| str(nested?.command) || str(nested?.cmd);
|
|
481
482
|
if (!command)
|
|
482
483
|
return null;
|
|
483
|
-
return { toolName: 'shell', toolArgs: { command, cwd: str(p.cwd) || str(nested?.cwd) ||
|
|
484
|
+
return { toolName: 'shell', toolArgs: { command, cwd: str(p.cwd) || str(nested?.cwd) || process.cwd() } };
|
|
484
485
|
}
|
|
485
486
|
case 'mcp': {
|
|
486
487
|
const toolName = str(p.tool_name) || str(p.toolName) || str(p.name) || 'mcp';
|
|
@@ -757,6 +758,7 @@ function tryDeveloperConfirmation(ctx, call, verdict, payload, where) {
|
|
|
757
758
|
if (ctx.shadow)
|
|
758
759
|
return false; // monitor machines never prompt — the caller records would-approve
|
|
759
760
|
const prepared = prepareDeveloperConfirmation(ctx.event, call, payload, {
|
|
761
|
+
policyHash: ctx.effectivePolicyHash,
|
|
760
762
|
policyId: verdict.policyId,
|
|
761
763
|
policyName: verdict.policyName,
|
|
762
764
|
matchedRule: verdict.matchedRule,
|
|
@@ -768,7 +770,7 @@ function tryDeveloperConfirmation(ctx, call, verdict, payload, where) {
|
|
|
768
770
|
});
|
|
769
771
|
if (prepared.outcome === 'allow')
|
|
770
772
|
ctx.respond(false, undefined, prepared.agentMsg);
|
|
771
|
-
ctx.respond('ask', prepared.userMsg, prepared.agentMsg);
|
|
773
|
+
ctx.respond('ask', prepared.userMsg, prepared.agentMsg, prepared.envelope);
|
|
772
774
|
}
|
|
773
775
|
/**
|
|
774
776
|
* The developer-confirmation core, independent of WHAT asked for it: an Action
|
|
@@ -779,10 +781,11 @@ function tryDeveloperConfirmation(ctx, call, verdict, payload, where) {
|
|
|
779
781
|
* Returns what to answer; the caller emits it (respond() ends the process).
|
|
780
782
|
*/
|
|
781
783
|
function prepareDeveloperConfirmation(event, call, payload, source) {
|
|
782
|
-
const { operation } = (0, actionPolicyEngine_1.inferToolContext)(call.toolName, call.toolArgs);
|
|
784
|
+
const { operation, context } = (0, actionPolicyEngine_1.inferToolContext)(call.toolName, call.toolArgs);
|
|
783
785
|
const sessId = sessionId(payload);
|
|
784
786
|
const policyLabel = source.policyName ? ` (${source.policyName})` : '';
|
|
785
787
|
const offlineEnforced = source.where !== 'local-first';
|
|
788
|
+
const policyHash = source.policyHash || source.localFinding?.policyHash;
|
|
786
789
|
// Sweep first: markers the human never answered are recorded as not approved.
|
|
787
790
|
for (const expired of (0, devConfirm_1.sweepExpiredConfirmations)()) {
|
|
788
791
|
(0, telemetry_1.spoolEvent)({
|
|
@@ -794,12 +797,14 @@ function prepareDeveloperConfirmation(event, call, payload, source) {
|
|
|
794
797
|
offlineEnforced: true,
|
|
795
798
|
});
|
|
796
799
|
}
|
|
797
|
-
const remembered = source.remember === 'none' ? undefined : (0, devConfirm_1.isConfirmationRemembered)({ toolName: call.toolName, operation, policyId: source.policyId, matchedRule: source.matchedRule }, sessId);
|
|
800
|
+
const remembered = source.remember === 'none' ? undefined : (0, devConfirm_1.isConfirmationRemembered)({ toolName: call.toolName, operation, argsDigest: (0, devConfirm_1.actionDigest)(call.toolArgs), policyId: source.policyId, matchedRule: source.matchedRule, policyHash: source.policyHash || source.localFinding?.policyHash }, sessId);
|
|
798
801
|
if (remembered) {
|
|
799
802
|
(0, hookIo_1.dbg)({ phase: 'dev_confirm_remembered', event, tool: call.toolName, operation, remember: remembered.remember, where: source.where });
|
|
800
803
|
spoolCallEvent(call, {
|
|
801
804
|
decision: 'allow',
|
|
802
805
|
reason: `developer confirmed earlier (${remembered.remember} memory)${policyLabel}`,
|
|
806
|
+
ruleId: source.policyId || source.localFinding?.ruleId,
|
|
807
|
+
policyHash: source.policyHash || source.localFinding?.policyHash,
|
|
803
808
|
approvalScope: 'developer',
|
|
804
809
|
approvedBy: 'developer',
|
|
805
810
|
offlineEnforced,
|
|
@@ -807,6 +812,25 @@ function prepareDeveloperConfirmation(event, call, payload, source) {
|
|
|
807
812
|
(0, telemetry_1.triggerFlush)(false);
|
|
808
813
|
return { outcome: 'allow', agentMsg: `FullCourtDefense: ${call.toolName} was already confirmed by the developer in this ${remembered.remember === '1h' ? 'hour' : 'session'}.` };
|
|
809
814
|
}
|
|
815
|
+
// A standing "Always allow" the developer gave in our dialog for this scope under
|
|
816
|
+
// this exact rule (same policy hash). Rules that forbid memory never reach here.
|
|
817
|
+
const grantScope = (0, devConfirm_1.grantScopeFor)(event, call.toolName, call.toolArgs, operation, context);
|
|
818
|
+
const grant = source.remember === 'none' ? undefined : (0, devConfirm_1.findDeveloperGrant)(grantScope, { policyId: source.policyId, matchedRule: source.matchedRule, policyHash });
|
|
819
|
+
if (grant) {
|
|
820
|
+
const covers = (0, devConfirm_1.describeGrantScope)(grant.scope);
|
|
821
|
+
(0, hookIo_1.dbg)({ phase: 'dev_confirm_granted', event, tool: call.toolName, operation, grant: grant.key, where: source.where });
|
|
822
|
+
spoolCallEvent(call, {
|
|
823
|
+
decision: 'allow',
|
|
824
|
+
reason: `developer granted Always allow for ${covers}${policyLabel}`,
|
|
825
|
+
ruleId: source.policyId || source.localFinding?.ruleId,
|
|
826
|
+
policyHash,
|
|
827
|
+
approvalScope: 'developer',
|
|
828
|
+
approvedBy: 'developer',
|
|
829
|
+
offlineEnforced,
|
|
830
|
+
});
|
|
831
|
+
(0, telemetry_1.triggerFlush)(false);
|
|
832
|
+
return { outcome: 'allow', agentMsg: `FullCourtDefense: the developer has a standing "Always allow" for ${covers} under this rule.` };
|
|
833
|
+
}
|
|
810
834
|
const marker = (0, devConfirm_1.raiseDeveloperConfirmation)({
|
|
811
835
|
event,
|
|
812
836
|
toolName: call.toolName,
|
|
@@ -814,6 +838,7 @@ function prepareDeveloperConfirmation(event, call, payload, source) {
|
|
|
814
838
|
argsDigest: (0, devConfirm_1.actionDigest)(call.toolArgs),
|
|
815
839
|
policyId: source.policyId,
|
|
816
840
|
policyName: source.policyName,
|
|
841
|
+
policyHash: source.policyHash || source.localFinding?.policyHash,
|
|
817
842
|
matchedRule: source.matchedRule,
|
|
818
843
|
sessionId: sessId,
|
|
819
844
|
remember: source.remember,
|
|
@@ -822,6 +847,8 @@ function prepareDeveloperConfirmation(event, call, payload, source) {
|
|
|
822
847
|
(0, hookIo_1.dbg)({ phase: 'dev_confirm_ask', event, tool: call.toolName, operation, key: marker.key, remember: source.remember, where: source.where });
|
|
823
848
|
spoolCallEvent(call, {
|
|
824
849
|
decision: 'approval',
|
|
850
|
+
ruleId: source.policyId || source.localFinding?.ruleId,
|
|
851
|
+
policyHash: source.policyHash || source.localFinding?.policyHash,
|
|
825
852
|
reason: `developer confirmation requested via IDE prompt${policyLabel}${source.reason ? `: ${source.reason}` : ''}`,
|
|
826
853
|
approvalScope: 'developer',
|
|
827
854
|
offlineEnforced,
|
|
@@ -840,6 +867,24 @@ function prepareDeveloperConfirmation(event, call, payload, source) {
|
|
|
840
867
|
outcome: 'ask',
|
|
841
868
|
userMsg: `${explained.userMessage}\nAllow this ${event}?`,
|
|
842
869
|
agentMsg: `FullCourtDefense paused this ${event} for the developer's confirmation${policyLabel}: ${reason}. Wait for the developer's answer in the IDE prompt — do not retry or work around it. ${explained.agentMessage}`,
|
|
870
|
+
// For the per-event hook process in the developer's session: everything the
|
|
871
|
+
// three-button dialog needs. Stays on this machine (IPC / stdout of our own
|
|
872
|
+
// process) and is stripped before the IDE reads the verdict; the grant it
|
|
873
|
+
// may create stores only the scope and a digest, never the command text.
|
|
874
|
+
envelope: {
|
|
875
|
+
v: 1,
|
|
876
|
+
event,
|
|
877
|
+
toolName: call.toolName,
|
|
878
|
+
argsDigest: marker.argsDigest,
|
|
879
|
+
operation,
|
|
880
|
+
scope: grantScope,
|
|
881
|
+
policyId: source.policyId,
|
|
882
|
+
policyName: source.policyName,
|
|
883
|
+
policyHash,
|
|
884
|
+
matchedRule: source.matchedRule,
|
|
885
|
+
remember: source.remember,
|
|
886
|
+
reason: explained.userMessage,
|
|
887
|
+
},
|
|
843
888
|
};
|
|
844
889
|
}
|
|
845
890
|
/**
|
|
@@ -916,14 +961,14 @@ function tryLocalPolicyEnforcement(ctx, call, detail, opts = {}) {
|
|
|
916
961
|
if (ctx.shadow) {
|
|
917
962
|
// Truth-in-reporting: the action RAN — 'warn' (advisory), never 'allow'
|
|
918
963
|
// (which hides the would-block from the console) and never 'block'.
|
|
919
|
-
spoolCallEvent(call, { decision: 'warn', reason: `[monitor] local policy would ${local.verdict} (offline): ${reason}`, offlineEnforced: true });
|
|
964
|
+
spoolCallEvent(call, { decision: 'warn', ruleId: local.policyId, policyHash: ctx.effectivePolicyHash, reason: `[monitor] local policy would ${local.verdict} (offline): ${reason}`, offlineEnforced: true });
|
|
920
965
|
(0, telemetry_1.triggerFlush)(false);
|
|
921
966
|
ctx.respond(false, undefined, `[FullCourtDefense shadow] would ${local.verdict === 'block' ? 'block' : 'require approval for'} ${call.toolName} (offline, locally cached policy): ${reason}`);
|
|
922
967
|
}
|
|
923
968
|
const approvalNote = local.verdict === 'require_approval'
|
|
924
969
|
? ' This action requires human approval, which is not possible while the policy service is unreachable.'
|
|
925
970
|
: '';
|
|
926
|
-
spoolCallEvent(call, { decision: 'block', reason: `local policy (offline): ${reason}`, offlineEnforced: true });
|
|
971
|
+
spoolCallEvent(call, { decision: 'block', ruleId: local.policyId, policyHash: ctx.effectivePolicyHash, reason: `local policy (offline): ${reason}`, offlineEnforced: true });
|
|
927
972
|
(0, telemetry_1.triggerFlush)(true);
|
|
928
973
|
const explained = explainPolicyStop('block', ctx.event, call, local, policies, `Enforced from the locally cached policy while the policy service is unreachable (${detail}).${approvalNote}`);
|
|
929
974
|
ctx.respond(true, explained.userMessage, `${explained.agentMessage} Do not retry until the connection is restored or the policy allows it.`);
|
|
@@ -939,7 +984,7 @@ function tryLocalPolicyEnforcement(ctx, call, detail, opts = {}) {
|
|
|
939
984
|
? warning.agentMsg
|
|
940
985
|
: `${warning.agentMsg} (policy service unreachable — evaluated from the locally cached org policies).`);
|
|
941
986
|
}
|
|
942
|
-
spoolCallEvent(call, { decision: 'allow', reason: `local policy allow (${monitorPath ? 'monitor' : 'offline'}): ${detail}`, offlineEnforced: true });
|
|
987
|
+
spoolCallEvent(call, { decision: 'allow', ruleId: local.policyId, policyHash: ctx.effectivePolicyHash, reason: `local policy allow (${monitorPath ? 'monitor' : 'offline'}): ${detail}`, offlineEnforced: true });
|
|
943
988
|
(0, telemetry_1.triggerFlush)(false);
|
|
944
989
|
ctx.respond(false, undefined, monitorPath
|
|
945
990
|
? undefined
|
|
@@ -1182,15 +1227,25 @@ function processIo(stdin, stdinErr) {
|
|
|
1182
1227
|
* an allow verdict, never an error the thin client would surface.
|
|
1183
1228
|
*/
|
|
1184
1229
|
async function evaluateHookRequest(args, stdinRaw, config) {
|
|
1230
|
+
return evaluateBuffered(args, stdinRaw, '', config, { isTTY: false, exitsProcess: false });
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Run the hook against a buffered io and return the verdict instead of writing
|
|
1234
|
+
* it. `exitsProcess: true` keeps the standalone hook's delivery contract (spool +
|
|
1235
|
+
* detached flusher) while still letting the caller post-process the verdict —
|
|
1236
|
+
* the per-event process needs that to open the developer's dialog on an `ask`.
|
|
1237
|
+
*/
|
|
1238
|
+
async function evaluateBuffered(args, stdinRaw, stdinErr, config, mode) {
|
|
1185
1239
|
let out = '';
|
|
1186
1240
|
let errOut = '';
|
|
1187
1241
|
const io = {
|
|
1188
1242
|
stdin: stdinRaw,
|
|
1189
|
-
|
|
1243
|
+
stdinErr: stdinErr || undefined,
|
|
1244
|
+
isTTY: mode.isTTY,
|
|
1190
1245
|
write: (text) => { out += text; },
|
|
1191
1246
|
writeErr: (text) => { errOut += text; },
|
|
1192
1247
|
exit: (code) => { throw new HookExitSignal(code); },
|
|
1193
|
-
exitsProcess:
|
|
1248
|
+
exitsProcess: mode.exitsProcess,
|
|
1194
1249
|
};
|
|
1195
1250
|
try {
|
|
1196
1251
|
await hookCommandInner(args, config, io);
|
|
@@ -1246,25 +1301,16 @@ async function hookCommandOuter(args, config, startedAt) {
|
|
|
1246
1301
|
const ipc = await (0, verdictIpc_1.requestDaemonVerdict)(args, raw, undefined, startedAt);
|
|
1247
1302
|
if (ipc.outcome === 'verdict') {
|
|
1248
1303
|
(0, hookIo_1.dbg)({ phase: 'ipc_verdict', event: args.event, exitCode: ipc.exitCode });
|
|
1249
|
-
|
|
1250
|
-
try {
|
|
1251
|
-
process.stderr.write(ipc.stderr);
|
|
1252
|
-
}
|
|
1253
|
-
catch { /* stderr gone */ }
|
|
1254
|
-
}
|
|
1255
|
-
if (ipc.stdout) {
|
|
1256
|
-
try {
|
|
1257
|
-
process.stdout.write(ipc.stdout);
|
|
1258
|
-
}
|
|
1259
|
-
catch { /* stdout gone */ }
|
|
1260
|
-
}
|
|
1261
|
-
process.exit(ipc.exitCode ?? 0);
|
|
1304
|
+
await (0, askDialog_1.emitVerdictForIde)(args, { stdout: ipc.stdout || '', stderr: ipc.stderr || '', exitCode: ipc.exitCode ?? 0 }, 'ipc');
|
|
1262
1305
|
}
|
|
1263
1306
|
// Daemon down / busy / refused — evaluate locally as always. Logged so a
|
|
1264
1307
|
// machine that silently stopped using the fast path is diagnosable.
|
|
1265
1308
|
(0, hookIo_1.dbg)({ phase: 'ipc_unavailable', event: args.event, detail: ipc.detail });
|
|
1266
1309
|
}
|
|
1267
|
-
|
|
1310
|
+
// Buffered so an `ask` can be put to the developer in OUR dialog before the
|
|
1311
|
+
// IDE reads the verdict; exitsProcess stays true — this process ends right after.
|
|
1312
|
+
const result = await evaluateBuffered(args, raw, stdinErr, config, { isTTY: !!process.stdin.isTTY, exitsProcess: true });
|
|
1313
|
+
await (0, askDialog_1.emitVerdictForIde)(args, result, 'local');
|
|
1268
1314
|
}
|
|
1269
1315
|
catch (err) {
|
|
1270
1316
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1517,7 +1563,7 @@ async function hookCommandInner(args, config, io) {
|
|
|
1517
1563
|
pendingLocalAsk = pendingLocalAsk || { finding, call };
|
|
1518
1564
|
};
|
|
1519
1565
|
const hasPendingLocalAsk = () => Boolean(pendingLocalAsk);
|
|
1520
|
-
const respond = (blocked, userMsg, agentMsg) => {
|
|
1566
|
+
const respond = (blocked, userMsg, agentMsg, askEnvelope) => {
|
|
1521
1567
|
let ask = blocked === 'ask' && event !== 'prompt';
|
|
1522
1568
|
const denied = blocked === true;
|
|
1523
1569
|
if (!denied && !ask && pendingLocalAsk && event !== 'prompt') {
|
|
@@ -1535,6 +1581,7 @@ async function hookCommandInner(args, config, io) {
|
|
|
1535
1581
|
if (prepared.outcome === 'ask') {
|
|
1536
1582
|
ask = true;
|
|
1537
1583
|
userMsg = prepared.userMsg;
|
|
1584
|
+
askEnvelope = prepared.envelope;
|
|
1538
1585
|
}
|
|
1539
1586
|
agentMsg = [prepared.agentMsg, agentMsg].filter(Boolean).join(' ');
|
|
1540
1587
|
}
|
|
@@ -1575,7 +1622,11 @@ async function hookCommandInner(args, config, io) {
|
|
|
1575
1622
|
if (agentMsg)
|
|
1576
1623
|
decision.agent_message = agentMsg;
|
|
1577
1624
|
}
|
|
1578
|
-
|
|
1625
|
+
// An ask carries the dialog envelope for the per-event hook process in the
|
|
1626
|
+
// developer's session (askDialog.ts); it is removed before the IDE reads the verdict.
|
|
1627
|
+
if (ask && askEnvelope)
|
|
1628
|
+
decision = (0, askDialog_1.attachAskEnvelope)(decision, askEnvelope);
|
|
1629
|
+
(0, hookIo_1.dbg)({ phase: 'verdict', event, format: hookFormat, blocked, decision: ask && askEnvelope ? { ...decision, [askDialog_1.ASK_ENVELOPE_FIELD]: '(envelope)' } : decision });
|
|
1579
1630
|
if (hookFormat === 'claude' && event === 'prompt' && denied) {
|
|
1580
1631
|
// Claude Desktop/Code variants do not all honor the structured
|
|
1581
1632
|
// UserPromptSubmit decision consistently. Exit 2 is the documented hard
|
|
@@ -1783,6 +1834,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1783
1834
|
const toolOutcome = (0, deterministicGuard_1.resolveDeterministicOutcome)(opts => (0, deterministicGuard_1.scanDeterministicToolCall)(call.toolName, call.toolArgs, opts), (0, localSafetySnapshot_1.snapshotToScanOptions)(snapshot, {
|
|
1784
1835
|
cwd: typeof call.toolArgs.cwd === 'string' ? call.toolArgs.cwd : process.cwd(),
|
|
1785
1836
|
inspectScripts: event === 'shell',
|
|
1837
|
+
mcpServerName: event === 'mcp' ? mcpServerName(payload) : undefined,
|
|
1786
1838
|
}));
|
|
1787
1839
|
deferLocalWarnings(toolOutcome.warnings, call.toolName, event);
|
|
1788
1840
|
const localBlock = toolOutcome.blockingFinding;
|
|
@@ -1947,7 +1999,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1947
1999
|
// read "N blocked of N recent" with every allow missing. Same delivery
|
|
1948
2000
|
// as a block: spool (sync file append) + detached flusher that outlives
|
|
1949
2001
|
// this process. The IDE is not kept waiting on the network.
|
|
1950
|
-
spoolCallEvent(call, { decision: 'allow', reason: 'local policy allow (block mode, local-first)' });
|
|
2002
|
+
spoolCallEvent(call, { decision: 'allow', policyHash: effectivePolicyHash, ruleId: localVerdict.policyId, reason: 'local policy allow (block mode, local-first)' });
|
|
1951
2003
|
(0, telemetry_1.triggerFlush)(true);
|
|
1952
2004
|
respond(false);
|
|
1953
2005
|
return;
|
|
@@ -1958,7 +2010,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1958
2010
|
void fetch(gateUrl, { method: 'POST', headers, body: gateBody, signal: AbortSignal.timeout(4000) })
|
|
1959
2011
|
.then(() => (0, policyGateHealth_1.recordGateSuccess)())
|
|
1960
2012
|
.catch(() => {
|
|
1961
|
-
spoolCallEvent(call, { decision: 'allow', reason: 'local policy allow (block mode, local-first); gate report failed — spooled', offlineEnforced: true });
|
|
2013
|
+
spoolCallEvent(call, { decision: 'allow', policyHash: effectivePolicyHash, ruleId: localVerdict.policyId, reason: 'local policy allow (block mode, local-first); gate report failed — spooled', offlineEnforced: true });
|
|
1962
2014
|
(0, telemetry_1.triggerFlush)(false);
|
|
1963
2015
|
});
|
|
1964
2016
|
respond(false);
|
|
@@ -1966,7 +2018,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1966
2018
|
}
|
|
1967
2019
|
if (localVerdict.verdict === 'block') {
|
|
1968
2020
|
const reason = localVerdict.reason || `${call.toolName}: blocked by org Action Policy`;
|
|
1969
|
-
spoolCallEvent(call, { decision: 'block', reason: `local policy (block mode, local-first): ${reason}`, offlineEnforced: true });
|
|
2021
|
+
spoolCallEvent(call, { decision: 'block', policyHash: effectivePolicyHash, ruleId: localVerdict.policyId, reason: `local policy (block mode, local-first): ${reason}`, offlineEnforced: true });
|
|
1970
2022
|
(0, telemetry_1.triggerFlush)(true);
|
|
1971
2023
|
const explained = explainPolicyStop('block', event, call, localVerdict, ctx.localPolicies);
|
|
1972
2024
|
respond(true, explained.userMessage, explained.agentMessage);
|
|
@@ -1980,7 +2032,7 @@ async function enforceActionPolicy(ctx) {
|
|
|
1980
2032
|
return;
|
|
1981
2033
|
}
|
|
1982
2034
|
// require_approval, developer scope + human present → IDE-native ask (no org queue).
|
|
1983
|
-
if (tryDeveloperConfirmation({ event, respond, shadow, localPolicies: ctx.localPolicies }, call, localVerdict, payload, 'local-first'))
|
|
2035
|
+
if (tryDeveloperConfirmation({ event, respond, shadow, effectivePolicyHash, localPolicies: ctx.localPolicies }, call, localVerdict, payload, 'local-first'))
|
|
1984
2036
|
return;
|
|
1985
2037
|
// require_approval (org scope) → synchronous gate below creates the approval action.
|
|
1986
2038
|
}
|
|
@@ -44,6 +44,7 @@ const os = __importStar(require("os"));
|
|
|
44
44
|
const path = __importStar(require("path"));
|
|
45
45
|
const config_1 = require("../config");
|
|
46
46
|
const hookIo_1 = require("../hookIo");
|
|
47
|
+
const gatewayRuntime_1 = require("../gatewayRuntime");
|
|
47
48
|
const restartNotice_1 = require("./restartNotice");
|
|
48
49
|
const COLOR = {
|
|
49
50
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
@@ -94,15 +95,16 @@ function hookScriptPath() {
|
|
|
94
95
|
|| (0, hookIo_1.slimHookEntryPath)(path.join(__dirname, '..', 'index.js'))
|
|
95
96
|
|| cliScript;
|
|
96
97
|
}
|
|
97
|
-
function hookInvocationPrefix() {
|
|
98
|
+
function hookInvocationPrefix(existingCommand) {
|
|
98
99
|
const q = (s) => (/\s/.test(s) ? `"${s}"` : s);
|
|
99
|
-
|
|
100
|
+
const runtime = (0, gatewayRuntime_1.resolveHookRuntime)({ executable: process.execPath, entry: hookScriptPath() }, existingCommand);
|
|
101
|
+
return `${q(runtime.executable)} ${q(runtime.entry)}`;
|
|
100
102
|
}
|
|
101
103
|
function repairManagedCommandPath(command) {
|
|
102
104
|
const marker = command.indexOf(' hook ');
|
|
103
105
|
if (marker < 0)
|
|
104
106
|
return { command, stale: true };
|
|
105
|
-
const expected = `${hookInvocationPrefix()}${command.slice(marker)}`;
|
|
107
|
+
const expected = `${hookInvocationPrefix(command)}${command.slice(marker)}`;
|
|
106
108
|
return { command: expected, stale: expected !== command };
|
|
107
109
|
}
|
|
108
110
|
function managedCommands(settings) {
|
|
@@ -212,8 +214,12 @@ function syncCopilotToolHooks(command) {
|
|
|
212
214
|
for (const event of ['PreToolUse', 'PostToolUse']) {
|
|
213
215
|
const entries = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
214
216
|
const kept = entries.filter(entry => !isManagedCommand(entry));
|
|
215
|
-
if (command)
|
|
216
|
-
|
|
217
|
+
if (command) {
|
|
218
|
+
const existing = entries.find(isManagedCommand)?.command;
|
|
219
|
+
const marker = command.indexOf(' hook ');
|
|
220
|
+
const ownedCommand = existing && marker >= 0 ? `${hookInvocationPrefix(existing)}${command.slice(marker)}` : command;
|
|
221
|
+
kept.push({ type: 'command', command: `${ownedCommand} --agent-client copilot`, timeoutSec: event === 'PreToolUse' ? TOOL_HOOK_TIMEOUT_SEC : POST_TOOL_HOOK_TIMEOUT_SEC });
|
|
222
|
+
}
|
|
217
223
|
if (kept.length)
|
|
218
224
|
hooks[event] = kept;
|
|
219
225
|
else
|
|
@@ -42,6 +42,7 @@ const os = __importStar(require("os"));
|
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const config_1 = require("../config");
|
|
44
44
|
const hookIo_1 = require("../hookIo");
|
|
45
|
+
const gatewayRuntime_1 = require("../gatewayRuntime");
|
|
45
46
|
const restartNotice_1 = require("./restartNotice");
|
|
46
47
|
const COLOR = {
|
|
47
48
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
@@ -153,19 +154,19 @@ function isManaged(entry) {
|
|
|
153
154
|
return typeof entry?.command === 'string'
|
|
154
155
|
&& (entry.command.includes(MANAGED_MARKER) || entry.command.includes(MANAGED_TAG));
|
|
155
156
|
}
|
|
156
|
-
function hookInvocationPrefix() {
|
|
157
|
-
const
|
|
157
|
+
function hookInvocationPrefix(existingCommand) {
|
|
158
|
+
const runtime = (0, gatewayRuntime_1.resolveHookRuntime)({ executable: process.execPath, entry: hookScriptPath() }, existingCommand);
|
|
158
159
|
const q = (s) => (/\s/.test(s) ? `"${s}"` : s);
|
|
159
160
|
// Repair rewrites stale managed entries to the slim entry too — this is how
|
|
160
161
|
// an already-enrolled fleet migrates to the slim hook on upgrade, without a
|
|
161
162
|
// reinstall (protection repair runs from the daemon after every update).
|
|
162
|
-
return `${q(
|
|
163
|
+
return `${q(runtime.executable)} ${q(runtime.entry)}`;
|
|
163
164
|
}
|
|
164
165
|
function repairManagedCommandPath(command) {
|
|
165
166
|
const marker = command.indexOf(' hook ');
|
|
166
167
|
if (marker < 0)
|
|
167
168
|
return { command, stale: true };
|
|
168
|
-
const expected = `${hookInvocationPrefix()}${command.slice(marker)}`;
|
|
169
|
+
const expected = `${hookInvocationPrefix(command)}${command.slice(marker)}`;
|
|
169
170
|
return { command: expected, stale: expected !== command };
|
|
170
171
|
}
|
|
171
172
|
/** A managed command mutated to weaken enforcement (`--shadow true` injected). */
|
|
@@ -3,6 +3,8 @@ import { BotGuardConfig } from '../config';
|
|
|
3
3
|
export type GatewayClient = 'cursor' | 'claude-code' | 'claude-desktop' | 'codex' | 'gemini-cli' | 'windsurf' | 'vscode';
|
|
4
4
|
export declare const ALL_GATEWAY_CLIENTS: GatewayClient[];
|
|
5
5
|
export interface McpGatewayArgs {
|
|
6
|
+
/** Original MCP configuration key; independent of the display agent name. */
|
|
7
|
+
mcpServerName?: string;
|
|
6
8
|
mcpCommand?: string;
|
|
7
9
|
mcpArgs?: string;
|
|
8
10
|
/** Remote MCP server URL (Streamable HTTP / SSE) — alternative to --mcp-command. */
|