sift-light 1.0.3-3 → 1.0.3-4
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/CHANGELOG.md +5 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/plugins/sift-light/.claude-plugin/plugin.json +1 -1
- package/plugins/sift-light/.codex-plugin/plugin.json +1 -1
- package/plugins/sift-light/.mcp.json +1 -1
- package/plugins/sift-light/hooks/search-policy.mjs +239 -14
- package/plugins/sift-light/kimi.plugin.json +2 -2
- package/plugins/sift-light/omp-extension.mjs +241 -16
- package/plugins/sift-light/package.json +1 -1
- package/src/mcp-server.mjs +1 -1
- package/src/search-policy-shell.ts +89 -24
- package/src/search-policy-stdin-filter.ts +174 -0
- package/src/search-policy.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.3-4 — 2026-10-05
|
|
4
|
+
|
|
5
|
+
- Allow recognized stdin-only filters at any pipeline position, including `cmd | grep pattern | head` and `cmd | grep pattern | wc -l`. Deny file operands, recursive search, file-sourced patterns, input redirection, and unsafe dynamic arguments even when a filter receives piped input ([#109](https://github.com/lightsifter/sift-light/issues/109)).
|
|
6
|
+
- Apply the same input-aware policy to Pi, OMP, and native plugin hooks, including PowerShell file-object pipeline handling.
|
|
7
|
+
|
|
3
8
|
## 1.0.3-3 — 2026-10-04
|
|
4
9
|
|
|
5
10
|
- Keep explicit paths outside the MCP project isolated after zero matches by default, and evaluate content-search and file-discovery globs/exclusions from the selected external root. Explicit `scope: "expand"` still retries the MCP project ([#106](https://github.com/lightsifter/sift-light/issues/106)).
|
package/README.md
CHANGED
|
@@ -179,7 +179,7 @@ Kimi Code's web mode can start plugin MCP servers from its installation director
|
|
|
179
179
|
|
|
180
180
|
Restart after installation. Native hooks use hard enforcement by default. To keep the native plugin, MCP tool and model guidance without denying conventional searches, start the host with `SIFT_LIGHT_ENFORCE_SEARCH=prefer`; use `off` to disable only hook enforcement. Accepted values are `hard`, `prefer`, and `off`; an unsupported value fails closed instead of silently allowing a search. The setting is inherited from the host process, so a project cannot lower a user's policy merely by committing a repository configuration file. You can still disable the complete integration through Claude Code `/plugin`, Codex `/hooks`, or Kimi `/plugins disable sift-light` followed by `/reload`.
|
|
181
181
|
|
|
182
|
-
In hard mode, a direct search such as `grep warning report.txt` is denied. A
|
|
182
|
+
In hard mode, a direct search such as `grep warning report.txt` is denied. A recognized stdin-only filter such as `cat report.txt | grep warning | head` remains available at any pipeline position because it filters output from a non-search producer. A filter with a file operand, recursive option, file-sourced pattern, or input redirection is denied even when piped. Put grep options before the pattern and quote shell wildcard characters; unknown options and dynamic arguments fail closed. PowerShell `Select-String` is also denied when a known file-object producer such as `Get-Item` feeds it. `find src | grep test` and `rg warning src | grep result` remain denied because their pipelines contain a direct search producer. If one subcommand in a compound shell call is denied, the host executes none of that call. The denial identifies the detected search and tells the agent to retry non-search operations separately.
|
|
183
183
|
|
|
184
184
|
Local searches stay on your machine. Only grant access to files your agent is allowed to read. HTTP deployments need an authenticated gateway before public exposure; see [Security](SECURITY.md).
|
|
185
185
|
|
package/README.zh-CN.md
CHANGED
|
@@ -179,7 +179,7 @@ Kimi Code 的 web 模式可能从安装目录启动插件 MCP 服务。如果相
|
|
|
179
179
|
|
|
180
180
|
安装后重启。原生钩子默认使用严格模式。如需保留原生插件、MCP 工具和模型指引,但不硬性拒绝其他搜索,请使用 `SIFT_LIGHT_ENFORCE_SEARCH=prefer` 启动宿主;设置为 `off` 只关闭钩子强制策略。可用值为 `hard`、`prefer` 和 `off`,非法值会安全拒绝并明确报错,不会静默放行。该设置由宿主进程继承,因此项目不能仅靠提交仓库配置文件降低用户的全局策略。仍可通过 Claude Code `/plugin`、Codex `/hooks`,或 Kimi `/plugins disable sift-light` 后执行 `/reload` 来关闭整个集成。
|
|
181
181
|
|
|
182
|
-
严格模式下,`grep warning report.txt` 这类直接搜索会被拒绝;`cat report.txt | grep warning`
|
|
182
|
+
严格模式下,`grep warning report.txt` 这类直接搜索会被拒绝;`cat report.txt | grep warning | head` 这类可静态确认只读取标准输入的过滤命令可放在管道中的任意位置,因为它过滤的是非搜索命令的输出。即使位于管道中,带文件参数、递归选项、从文件读取模式或输入重定向的过滤命令仍会被拒绝。请把 grep 选项放在模式之前,并用引号保护 shell 通配符;无法识别的选项和动态参数会保守拦截。PowerShell 中,`Get-Item` 等已知会输出文件对象的命令接入 `Select-String` 时也会被拒绝。`find src | grep test` 和 `rg warning src | grep result` 仍会被拒绝,因为管道中包含直接搜索来源。复合 shell 调用中只要一个子命令被拒绝,宿主就不会执行其中任何操作;拒绝信息会指出检测到的搜索,并要求 Agent 单独重试非搜索操作。
|
|
183
183
|
|
|
184
184
|
本地搜索在你的机器上进行。请只允许 Agent 读取已获授权的文件。HTTP 服务对外开放前需要认证网关,详见[安全说明](SECURITY.md)。
|
|
185
185
|
|
package/package.json
CHANGED
|
@@ -3779,9 +3779,182 @@ function classifyCommand(argv, language, depth = 0) {
|
|
|
3779
3779
|
return classifyCommand(args2.slice(start2), language, depth + 1);
|
|
3780
3780
|
}
|
|
3781
3781
|
|
|
3782
|
+
// src/search-policy-stdin-filter.ts
|
|
3783
|
+
var GREP_SWITCHES = new Set("abcEFGHhiILlnoPqsTUuvwxyzZ".split(""));
|
|
3784
|
+
var GREP_VALUE_OPTIONS = new Set(["A", "B", "C", "m"]);
|
|
3785
|
+
var GREP_LONG_SWITCHES = new Set([
|
|
3786
|
+
"--basic-regexp",
|
|
3787
|
+
"--extended-regexp",
|
|
3788
|
+
"--fixed-strings",
|
|
3789
|
+
"--perl-regexp",
|
|
3790
|
+
"--ignore-case",
|
|
3791
|
+
"--no-ignore-case",
|
|
3792
|
+
"--invert-match",
|
|
3793
|
+
"--word-regexp",
|
|
3794
|
+
"--line-regexp",
|
|
3795
|
+
"--count",
|
|
3796
|
+
"--files-with-matches",
|
|
3797
|
+
"--files-without-match",
|
|
3798
|
+
"--only-matching",
|
|
3799
|
+
"--quiet",
|
|
3800
|
+
"--silent",
|
|
3801
|
+
"--no-messages",
|
|
3802
|
+
"--byte-offset",
|
|
3803
|
+
"--line-number",
|
|
3804
|
+
"--with-filename",
|
|
3805
|
+
"--no-filename",
|
|
3806
|
+
"--initial-tab",
|
|
3807
|
+
"--null",
|
|
3808
|
+
"--null-data",
|
|
3809
|
+
"--text",
|
|
3810
|
+
"--binary",
|
|
3811
|
+
"--line-buffered",
|
|
3812
|
+
"--no-group-separator"
|
|
3813
|
+
]);
|
|
3814
|
+
var GREP_LONG_VALUES = new Set([
|
|
3815
|
+
"--after-context",
|
|
3816
|
+
"--before-context",
|
|
3817
|
+
"--context",
|
|
3818
|
+
"--max-count",
|
|
3819
|
+
"--binary-files",
|
|
3820
|
+
"--label",
|
|
3821
|
+
"--group-separator",
|
|
3822
|
+
"--include",
|
|
3823
|
+
"--exclude",
|
|
3824
|
+
"--exclude-dir"
|
|
3825
|
+
]);
|
|
3826
|
+
function isStdinOnlyGrep(args2) {
|
|
3827
|
+
const positionals = [];
|
|
3828
|
+
let hasPatternOption = false;
|
|
3829
|
+
let optionsEnded = false;
|
|
3830
|
+
for (let index = 0;index < args2.length; index += 1) {
|
|
3831
|
+
const arg = args2[index];
|
|
3832
|
+
if (arg === null || arg === undefined)
|
|
3833
|
+
return false;
|
|
3834
|
+
if (positionals.length > 0 && arg !== "-")
|
|
3835
|
+
return false;
|
|
3836
|
+
if (!optionsEnded && arg === "--") {
|
|
3837
|
+
optionsEnded = true;
|
|
3838
|
+
continue;
|
|
3839
|
+
}
|
|
3840
|
+
if (!optionsEnded && arg.startsWith("--")) {
|
|
3841
|
+
const separator = arg.indexOf("=");
|
|
3842
|
+
const option = separator < 0 ? arg : arg.slice(0, separator);
|
|
3843
|
+
const attached = separator < 0 ? undefined : arg.slice(separator + 1);
|
|
3844
|
+
if (option === "--regexp") {
|
|
3845
|
+
hasPatternOption = true;
|
|
3846
|
+
} else if (option === "--color" || option === "--colour") {
|
|
3847
|
+
continue;
|
|
3848
|
+
} else if (GREP_LONG_SWITCHES.has(option) && attached === undefined) {
|
|
3849
|
+
continue;
|
|
3850
|
+
} else if (!GREP_LONG_VALUES.has(option)) {
|
|
3851
|
+
return false;
|
|
3852
|
+
}
|
|
3853
|
+
if (attached === undefined) {
|
|
3854
|
+
index += 1;
|
|
3855
|
+
if (args2[index] === null || args2[index] === undefined)
|
|
3856
|
+
return false;
|
|
3857
|
+
}
|
|
3858
|
+
continue;
|
|
3859
|
+
}
|
|
3860
|
+
if (!optionsEnded && arg.startsWith("-") && arg !== "-") {
|
|
3861
|
+
for (let optionIndex = 1;optionIndex < arg.length; optionIndex += 1) {
|
|
3862
|
+
const option = arg[optionIndex];
|
|
3863
|
+
if (option === "e") {
|
|
3864
|
+
hasPatternOption = true;
|
|
3865
|
+
} else if (option !== undefined && GREP_SWITCHES.has(option)) {
|
|
3866
|
+
continue;
|
|
3867
|
+
} else if (option === undefined || !GREP_VALUE_OPTIONS.has(option)) {
|
|
3868
|
+
return false;
|
|
3869
|
+
}
|
|
3870
|
+
if (optionIndex === arg.length - 1) {
|
|
3871
|
+
index += 1;
|
|
3872
|
+
if (args2[index] === null || args2[index] === undefined)
|
|
3873
|
+
return false;
|
|
3874
|
+
}
|
|
3875
|
+
break;
|
|
3876
|
+
}
|
|
3877
|
+
continue;
|
|
3878
|
+
}
|
|
3879
|
+
positionals.push(arg);
|
|
3880
|
+
}
|
|
3881
|
+
return hasPatternOption ? positionals.every((operand) => operand === "-") : positionals.length > 0 && positionals.slice(1).every((operand) => operand === "-");
|
|
3882
|
+
}
|
|
3883
|
+
var SELECT_STRING_SWITCHES = new Set([
|
|
3884
|
+
"simplematch",
|
|
3885
|
+
"casesensitive",
|
|
3886
|
+
"quiet",
|
|
3887
|
+
"list",
|
|
3888
|
+
"noemphasis",
|
|
3889
|
+
"notmatch",
|
|
3890
|
+
"allmatches",
|
|
3891
|
+
"raw",
|
|
3892
|
+
"verbose",
|
|
3893
|
+
"debug"
|
|
3894
|
+
]);
|
|
3895
|
+
var SELECT_STRING_VALUES = new Set([
|
|
3896
|
+
"culture",
|
|
3897
|
+
"encoding",
|
|
3898
|
+
"context",
|
|
3899
|
+
"erroraction",
|
|
3900
|
+
"warningaction",
|
|
3901
|
+
"informationaction",
|
|
3902
|
+
"outvariable",
|
|
3903
|
+
"pipelinevariable"
|
|
3904
|
+
]);
|
|
3905
|
+
function isPipelineOnlySelectString(args2, namedParameters) {
|
|
3906
|
+
let hasPattern = false;
|
|
3907
|
+
for (let index = 0;index < args2.length; index += 1) {
|
|
3908
|
+
const arg = args2[index];
|
|
3909
|
+
if (arg === null || arg === undefined)
|
|
3910
|
+
return false;
|
|
3911
|
+
if (!namedParameters[index]) {
|
|
3912
|
+
if (hasPattern)
|
|
3913
|
+
return false;
|
|
3914
|
+
hasPattern = true;
|
|
3915
|
+
continue;
|
|
3916
|
+
}
|
|
3917
|
+
const separator = arg.indexOf(":");
|
|
3918
|
+
const option = (separator < 0 ? arg.slice(1) : arg.slice(1, separator)).toLowerCase();
|
|
3919
|
+
if (SELECT_STRING_SWITCHES.has(option) && separator < 0)
|
|
3920
|
+
continue;
|
|
3921
|
+
if (option !== "pattern" && !SELECT_STRING_VALUES.has(option))
|
|
3922
|
+
return false;
|
|
3923
|
+
if (option === "pattern") {
|
|
3924
|
+
if (hasPattern)
|
|
3925
|
+
return false;
|
|
3926
|
+
hasPattern = true;
|
|
3927
|
+
}
|
|
3928
|
+
if (separator < 0) {
|
|
3929
|
+
index += 1;
|
|
3930
|
+
if (args2[index] === null || args2[index] === undefined)
|
|
3931
|
+
return false;
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
return hasPattern;
|
|
3935
|
+
}
|
|
3936
|
+
function isStdinOnlySearchFilter(words, language, namedParameters) {
|
|
3937
|
+
const executable = words[0];
|
|
3938
|
+
if (executable === null || executable === undefined)
|
|
3939
|
+
return false;
|
|
3940
|
+
const name2 = executableName(executable);
|
|
3941
|
+
if (language === "powershell")
|
|
3942
|
+
return ["select-string", "sls"].includes(name2.toLowerCase()) && isPipelineOnlySelectString(words.slice(1), namedParameters.slice(1));
|
|
3943
|
+
return ["grep", "egrep", "fgrep"].includes(name2) && isStdinOnlyGrep(words.slice(1));
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3782
3946
|
// src/search-policy-shell.ts
|
|
3783
3947
|
var MAX_POLICY_COMMAND_BYTES = 64 * 1024;
|
|
3784
3948
|
var MAX_SHELL_NESTING = 4;
|
|
3949
|
+
var POWERSHELL_FILE_OBJECT_PRODUCERS = new Set([
|
|
3950
|
+
"get-item",
|
|
3951
|
+
"gi",
|
|
3952
|
+
"get-childitem",
|
|
3953
|
+
"gci",
|
|
3954
|
+
"dir",
|
|
3955
|
+
"ls"
|
|
3956
|
+
]);
|
|
3957
|
+
var POWERSHELL_STRING_CONVERTERS = new Set(["out-string", "oss", "convertto-json"]);
|
|
3785
3958
|
function decodeBashDoubleQuoted(value) {
|
|
3786
3959
|
let result = "";
|
|
3787
3960
|
for (let index = 0;index < value.length; index += 1) {
|
|
@@ -3867,6 +4040,18 @@ function commandWordNodes(node, language) {
|
|
|
3867
4040
|
function commandWords(node, language) {
|
|
3868
4041
|
return commandWordNodes(node, language).map((word) => literalWord(word, language));
|
|
3869
4042
|
}
|
|
4043
|
+
function hasBashWordExpansion(text) {
|
|
4044
|
+
if (text.startsWith("~") || text.startsWith("="))
|
|
4045
|
+
return true;
|
|
4046
|
+
for (let index = 0;index < text.length; index += 1) {
|
|
4047
|
+
if (text[index] === "\\") {
|
|
4048
|
+
index += 1;
|
|
4049
|
+
} else if ("*?[]{}".includes(text[index] ?? "")) {
|
|
4050
|
+
return true;
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
return false;
|
|
4054
|
+
}
|
|
3870
4055
|
function hasUntranslatedShellSyntax(node, language) {
|
|
3871
4056
|
const text = node.text;
|
|
3872
4057
|
if (language === "powershell") {
|
|
@@ -3879,30 +4064,70 @@ function hasUntranslatedShellSyntax(node, language) {
|
|
|
3879
4064
|
if (["raw_string", "string", "string_content"].includes(node.type))
|
|
3880
4065
|
return false;
|
|
3881
4066
|
if (["word", "number", "command_name"].includes(node.type))
|
|
3882
|
-
return
|
|
4067
|
+
return hasBashWordExpansion(text);
|
|
3883
4068
|
if (node.type === "concatenation")
|
|
3884
4069
|
return node.namedChildren.some((child) => child !== null && hasUntranslatedShellSyntax(child, language));
|
|
3885
4070
|
return false;
|
|
3886
4071
|
}
|
|
4072
|
+
function hasInputRedirectNodes(owner) {
|
|
4073
|
+
return owner.descendantsOfType(["file_redirect", "herestring_redirect", "heredoc_redirect"]).some((redirect) => redirect !== null && /^(?:\d+)?</u.test(redirect.text));
|
|
4074
|
+
}
|
|
4075
|
+
function hasInputRedirect(node) {
|
|
4076
|
+
if (hasInputRedirectNodes(node))
|
|
4077
|
+
return true;
|
|
4078
|
+
for (let parent = node.parent;parent; parent = parent.parent) {
|
|
4079
|
+
if (parent.type !== "redirected_statement")
|
|
4080
|
+
continue;
|
|
4081
|
+
const body2 = parent.childForFieldName("body");
|
|
4082
|
+
const last = body2?.descendantsOfType("command").at(-1) ?? (body2?.type === "command" ? body2 : undefined);
|
|
4083
|
+
if (last?.startIndex === node.startIndex && last.endIndex === node.endIndex && parent.childrenForFieldName("redirect").some((redirect) => redirect !== null && hasInputRedirectNodes(redirect)))
|
|
4084
|
+
return true;
|
|
4085
|
+
}
|
|
4086
|
+
return false;
|
|
4087
|
+
}
|
|
3887
4088
|
function isSafePipelineFilter(node, language, words) {
|
|
3888
|
-
const
|
|
3889
|
-
if (
|
|
4089
|
+
const wordNodes = commandWordNodes(node, language);
|
|
4090
|
+
if (!isStdinOnlySearchFilter(words, language, wordNodes.map((word) => word.type === "command_parameter")))
|
|
3890
4091
|
return false;
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
if (
|
|
4092
|
+
if (wordNodes.some((word) => hasUntranslatedShellSyntax(word, language)))
|
|
4093
|
+
return false;
|
|
4094
|
+
if (hasInputRedirect(node))
|
|
3894
4095
|
return false;
|
|
3895
4096
|
const pipeline = node.parent;
|
|
3896
|
-
if (!pipeline || pipeline
|
|
4097
|
+
if (!pipeline || !["pipeline", "pipeline_chain"].includes(pipeline.type))
|
|
3897
4098
|
return false;
|
|
3898
|
-
const
|
|
3899
|
-
const
|
|
3900
|
-
if (
|
|
4099
|
+
const stages = pipeline.namedChildren.filter((child) => child !== null);
|
|
4100
|
+
const position = stages.findIndex((stage) => stage.startIndex <= node.startIndex && stage.endIndex >= node.endIndex);
|
|
4101
|
+
if (position < 1)
|
|
3901
4102
|
return false;
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
4103
|
+
let fileObjectFlow = false;
|
|
4104
|
+
for (const [index, stage] of stages.entries()) {
|
|
4105
|
+
if (index === position)
|
|
4106
|
+
continue;
|
|
4107
|
+
const commands = stage.descendantsOfType("command");
|
|
4108
|
+
for (const candidate of commands) {
|
|
4109
|
+
if (candidate === null)
|
|
4110
|
+
continue;
|
|
4111
|
+
const candidateWords = commandWords(candidate, language);
|
|
4112
|
+
const decision = classifyCommand(candidateWords, language);
|
|
4113
|
+
if (decision.kind || decision.nested)
|
|
4114
|
+
return false;
|
|
4115
|
+
if (language !== "powershell" || index > position)
|
|
4116
|
+
continue;
|
|
4117
|
+
const executable = candidateWords[0];
|
|
4118
|
+
if (executable === null || executable === undefined)
|
|
4119
|
+
continue;
|
|
4120
|
+
const name2 = executableName(executable).toLowerCase();
|
|
4121
|
+
if (POWERSHELL_FILE_OBJECT_PRODUCERS.has(name2))
|
|
4122
|
+
fileObjectFlow = true;
|
|
4123
|
+
}
|
|
4124
|
+
if (language === "powershell" && index < position && stage.type === "command") {
|
|
4125
|
+
const executable = commandWords(stage, language)[0];
|
|
4126
|
+
if (executable !== null && executable !== undefined && POWERSHELL_STRING_CONVERTERS.has(executableName(executable).toLowerCase()))
|
|
4127
|
+
fileObjectFlow = false;
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
return !fileObjectFlow;
|
|
3906
4131
|
}
|
|
3907
4132
|
|
|
3908
4133
|
class ShellSearchPolicy {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sift-light",
|
|
3
|
-
"version": "1.0.3-
|
|
3
|
+
"version": "1.0.3-4",
|
|
4
4
|
"description": "Require sift-light for conventional local searches while keeping development tools available.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "baoer"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"args": [
|
|
14
14
|
"--yes",
|
|
15
15
|
"--package",
|
|
16
|
-
"sift-light-runtime@npm:sift-light@1.0.3-
|
|
16
|
+
"sift-light-runtime@npm:sift-light@1.0.3-4",
|
|
17
17
|
"sift-light-mcp",
|
|
18
18
|
"--stdio"
|
|
19
19
|
],
|
|
@@ -1845,7 +1845,7 @@ function createCtagsStructureProvider(options = {}) {
|
|
|
1845
1845
|
// package.json
|
|
1846
1846
|
var package_default = {
|
|
1847
1847
|
name: "sift-light",
|
|
1848
|
-
version: "1.0.3-
|
|
1848
|
+
version: "1.0.3-4",
|
|
1849
1849
|
description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
|
|
1850
1850
|
keywords: [
|
|
1851
1851
|
"ai-agent",
|
|
@@ -20458,9 +20458,182 @@ function classifyCommand(argv, language, depth = 0) {
|
|
|
20458
20458
|
return classifyCommand(args2.slice(start2), language, depth + 1);
|
|
20459
20459
|
}
|
|
20460
20460
|
|
|
20461
|
+
// src/search-policy-stdin-filter.ts
|
|
20462
|
+
var GREP_SWITCHES = new Set("abcEFGHhiILlnoPqsTUuvwxyzZ".split(""));
|
|
20463
|
+
var GREP_VALUE_OPTIONS = new Set(["A", "B", "C", "m"]);
|
|
20464
|
+
var GREP_LONG_SWITCHES = new Set([
|
|
20465
|
+
"--basic-regexp",
|
|
20466
|
+
"--extended-regexp",
|
|
20467
|
+
"--fixed-strings",
|
|
20468
|
+
"--perl-regexp",
|
|
20469
|
+
"--ignore-case",
|
|
20470
|
+
"--no-ignore-case",
|
|
20471
|
+
"--invert-match",
|
|
20472
|
+
"--word-regexp",
|
|
20473
|
+
"--line-regexp",
|
|
20474
|
+
"--count",
|
|
20475
|
+
"--files-with-matches",
|
|
20476
|
+
"--files-without-match",
|
|
20477
|
+
"--only-matching",
|
|
20478
|
+
"--quiet",
|
|
20479
|
+
"--silent",
|
|
20480
|
+
"--no-messages",
|
|
20481
|
+
"--byte-offset",
|
|
20482
|
+
"--line-number",
|
|
20483
|
+
"--with-filename",
|
|
20484
|
+
"--no-filename",
|
|
20485
|
+
"--initial-tab",
|
|
20486
|
+
"--null",
|
|
20487
|
+
"--null-data",
|
|
20488
|
+
"--text",
|
|
20489
|
+
"--binary",
|
|
20490
|
+
"--line-buffered",
|
|
20491
|
+
"--no-group-separator"
|
|
20492
|
+
]);
|
|
20493
|
+
var GREP_LONG_VALUES = new Set([
|
|
20494
|
+
"--after-context",
|
|
20495
|
+
"--before-context",
|
|
20496
|
+
"--context",
|
|
20497
|
+
"--max-count",
|
|
20498
|
+
"--binary-files",
|
|
20499
|
+
"--label",
|
|
20500
|
+
"--group-separator",
|
|
20501
|
+
"--include",
|
|
20502
|
+
"--exclude",
|
|
20503
|
+
"--exclude-dir"
|
|
20504
|
+
]);
|
|
20505
|
+
function isStdinOnlyGrep(args2) {
|
|
20506
|
+
const positionals = [];
|
|
20507
|
+
let hasPatternOption = false;
|
|
20508
|
+
let optionsEnded = false;
|
|
20509
|
+
for (let index = 0;index < args2.length; index += 1) {
|
|
20510
|
+
const arg = args2[index];
|
|
20511
|
+
if (arg === null || arg === undefined)
|
|
20512
|
+
return false;
|
|
20513
|
+
if (positionals.length > 0 && arg !== "-")
|
|
20514
|
+
return false;
|
|
20515
|
+
if (!optionsEnded && arg === "--") {
|
|
20516
|
+
optionsEnded = true;
|
|
20517
|
+
continue;
|
|
20518
|
+
}
|
|
20519
|
+
if (!optionsEnded && arg.startsWith("--")) {
|
|
20520
|
+
const separator = arg.indexOf("=");
|
|
20521
|
+
const option = separator < 0 ? arg : arg.slice(0, separator);
|
|
20522
|
+
const attached = separator < 0 ? undefined : arg.slice(separator + 1);
|
|
20523
|
+
if (option === "--regexp") {
|
|
20524
|
+
hasPatternOption = true;
|
|
20525
|
+
} else if (option === "--color" || option === "--colour") {
|
|
20526
|
+
continue;
|
|
20527
|
+
} else if (GREP_LONG_SWITCHES.has(option) && attached === undefined) {
|
|
20528
|
+
continue;
|
|
20529
|
+
} else if (!GREP_LONG_VALUES.has(option)) {
|
|
20530
|
+
return false;
|
|
20531
|
+
}
|
|
20532
|
+
if (attached === undefined) {
|
|
20533
|
+
index += 1;
|
|
20534
|
+
if (args2[index] === null || args2[index] === undefined)
|
|
20535
|
+
return false;
|
|
20536
|
+
}
|
|
20537
|
+
continue;
|
|
20538
|
+
}
|
|
20539
|
+
if (!optionsEnded && arg.startsWith("-") && arg !== "-") {
|
|
20540
|
+
for (let optionIndex = 1;optionIndex < arg.length; optionIndex += 1) {
|
|
20541
|
+
const option = arg[optionIndex];
|
|
20542
|
+
if (option === "e") {
|
|
20543
|
+
hasPatternOption = true;
|
|
20544
|
+
} else if (option !== undefined && GREP_SWITCHES.has(option)) {
|
|
20545
|
+
continue;
|
|
20546
|
+
} else if (option === undefined || !GREP_VALUE_OPTIONS.has(option)) {
|
|
20547
|
+
return false;
|
|
20548
|
+
}
|
|
20549
|
+
if (optionIndex === arg.length - 1) {
|
|
20550
|
+
index += 1;
|
|
20551
|
+
if (args2[index] === null || args2[index] === undefined)
|
|
20552
|
+
return false;
|
|
20553
|
+
}
|
|
20554
|
+
break;
|
|
20555
|
+
}
|
|
20556
|
+
continue;
|
|
20557
|
+
}
|
|
20558
|
+
positionals.push(arg);
|
|
20559
|
+
}
|
|
20560
|
+
return hasPatternOption ? positionals.every((operand) => operand === "-") : positionals.length > 0 && positionals.slice(1).every((operand) => operand === "-");
|
|
20561
|
+
}
|
|
20562
|
+
var SELECT_STRING_SWITCHES = new Set([
|
|
20563
|
+
"simplematch",
|
|
20564
|
+
"casesensitive",
|
|
20565
|
+
"quiet",
|
|
20566
|
+
"list",
|
|
20567
|
+
"noemphasis",
|
|
20568
|
+
"notmatch",
|
|
20569
|
+
"allmatches",
|
|
20570
|
+
"raw",
|
|
20571
|
+
"verbose",
|
|
20572
|
+
"debug"
|
|
20573
|
+
]);
|
|
20574
|
+
var SELECT_STRING_VALUES = new Set([
|
|
20575
|
+
"culture",
|
|
20576
|
+
"encoding",
|
|
20577
|
+
"context",
|
|
20578
|
+
"erroraction",
|
|
20579
|
+
"warningaction",
|
|
20580
|
+
"informationaction",
|
|
20581
|
+
"outvariable",
|
|
20582
|
+
"pipelinevariable"
|
|
20583
|
+
]);
|
|
20584
|
+
function isPipelineOnlySelectString(args2, namedParameters) {
|
|
20585
|
+
let hasPattern = false;
|
|
20586
|
+
for (let index = 0;index < args2.length; index += 1) {
|
|
20587
|
+
const arg = args2[index];
|
|
20588
|
+
if (arg === null || arg === undefined)
|
|
20589
|
+
return false;
|
|
20590
|
+
if (!namedParameters[index]) {
|
|
20591
|
+
if (hasPattern)
|
|
20592
|
+
return false;
|
|
20593
|
+
hasPattern = true;
|
|
20594
|
+
continue;
|
|
20595
|
+
}
|
|
20596
|
+
const separator = arg.indexOf(":");
|
|
20597
|
+
const option = (separator < 0 ? arg.slice(1) : arg.slice(1, separator)).toLowerCase();
|
|
20598
|
+
if (SELECT_STRING_SWITCHES.has(option) && separator < 0)
|
|
20599
|
+
continue;
|
|
20600
|
+
if (option !== "pattern" && !SELECT_STRING_VALUES.has(option))
|
|
20601
|
+
return false;
|
|
20602
|
+
if (option === "pattern") {
|
|
20603
|
+
if (hasPattern)
|
|
20604
|
+
return false;
|
|
20605
|
+
hasPattern = true;
|
|
20606
|
+
}
|
|
20607
|
+
if (separator < 0) {
|
|
20608
|
+
index += 1;
|
|
20609
|
+
if (args2[index] === null || args2[index] === undefined)
|
|
20610
|
+
return false;
|
|
20611
|
+
}
|
|
20612
|
+
}
|
|
20613
|
+
return hasPattern;
|
|
20614
|
+
}
|
|
20615
|
+
function isStdinOnlySearchFilter(words, language, namedParameters) {
|
|
20616
|
+
const executable = words[0];
|
|
20617
|
+
if (executable === null || executable === undefined)
|
|
20618
|
+
return false;
|
|
20619
|
+
const name2 = executableName(executable);
|
|
20620
|
+
if (language === "powershell")
|
|
20621
|
+
return ["select-string", "sls"].includes(name2.toLowerCase()) && isPipelineOnlySelectString(words.slice(1), namedParameters.slice(1));
|
|
20622
|
+
return ["grep", "egrep", "fgrep"].includes(name2) && isStdinOnlyGrep(words.slice(1));
|
|
20623
|
+
}
|
|
20624
|
+
|
|
20461
20625
|
// src/search-policy-shell.ts
|
|
20462
20626
|
var MAX_POLICY_COMMAND_BYTES = 64 * 1024;
|
|
20463
20627
|
var MAX_SHELL_NESTING = 4;
|
|
20628
|
+
var POWERSHELL_FILE_OBJECT_PRODUCERS = new Set([
|
|
20629
|
+
"get-item",
|
|
20630
|
+
"gi",
|
|
20631
|
+
"get-childitem",
|
|
20632
|
+
"gci",
|
|
20633
|
+
"dir",
|
|
20634
|
+
"ls"
|
|
20635
|
+
]);
|
|
20636
|
+
var POWERSHELL_STRING_CONVERTERS = new Set(["out-string", "oss", "convertto-json"]);
|
|
20464
20637
|
function decodeBashDoubleQuoted(value) {
|
|
20465
20638
|
let result = "";
|
|
20466
20639
|
for (let index = 0;index < value.length; index += 1) {
|
|
@@ -20546,6 +20719,18 @@ function commandWordNodes(node, language) {
|
|
|
20546
20719
|
function commandWords(node, language) {
|
|
20547
20720
|
return commandWordNodes(node, language).map((word) => literalWord(word, language));
|
|
20548
20721
|
}
|
|
20722
|
+
function hasBashWordExpansion(text) {
|
|
20723
|
+
if (text.startsWith("~") || text.startsWith("="))
|
|
20724
|
+
return true;
|
|
20725
|
+
for (let index = 0;index < text.length; index += 1) {
|
|
20726
|
+
if (text[index] === "\\") {
|
|
20727
|
+
index += 1;
|
|
20728
|
+
} else if ("*?[]{}".includes(text[index] ?? "")) {
|
|
20729
|
+
return true;
|
|
20730
|
+
}
|
|
20731
|
+
}
|
|
20732
|
+
return false;
|
|
20733
|
+
}
|
|
20549
20734
|
function hasUntranslatedShellSyntax(node, language) {
|
|
20550
20735
|
const text = node.text;
|
|
20551
20736
|
if (language === "powershell") {
|
|
@@ -20558,30 +20743,70 @@ function hasUntranslatedShellSyntax(node, language) {
|
|
|
20558
20743
|
if (["raw_string", "string", "string_content"].includes(node.type))
|
|
20559
20744
|
return false;
|
|
20560
20745
|
if (["word", "number", "command_name"].includes(node.type))
|
|
20561
|
-
return
|
|
20746
|
+
return hasBashWordExpansion(text);
|
|
20562
20747
|
if (node.type === "concatenation")
|
|
20563
20748
|
return node.namedChildren.some((child) => child !== null && hasUntranslatedShellSyntax(child, language));
|
|
20564
20749
|
return false;
|
|
20565
20750
|
}
|
|
20751
|
+
function hasInputRedirectNodes(owner) {
|
|
20752
|
+
return owner.descendantsOfType(["file_redirect", "herestring_redirect", "heredoc_redirect"]).some((redirect) => redirect !== null && /^(?:\d+)?</u.test(redirect.text));
|
|
20753
|
+
}
|
|
20754
|
+
function hasInputRedirect(node) {
|
|
20755
|
+
if (hasInputRedirectNodes(node))
|
|
20756
|
+
return true;
|
|
20757
|
+
for (let parent = node.parent;parent; parent = parent.parent) {
|
|
20758
|
+
if (parent.type !== "redirected_statement")
|
|
20759
|
+
continue;
|
|
20760
|
+
const body2 = parent.childForFieldName("body");
|
|
20761
|
+
const last = body2?.descendantsOfType("command").at(-1) ?? (body2?.type === "command" ? body2 : undefined);
|
|
20762
|
+
if (last?.startIndex === node.startIndex && last.endIndex === node.endIndex && parent.childrenForFieldName("redirect").some((redirect) => redirect !== null && hasInputRedirectNodes(redirect)))
|
|
20763
|
+
return true;
|
|
20764
|
+
}
|
|
20765
|
+
return false;
|
|
20766
|
+
}
|
|
20566
20767
|
function isSafePipelineFilter(node, language, words) {
|
|
20567
|
-
const
|
|
20568
|
-
if (
|
|
20768
|
+
const wordNodes = commandWordNodes(node, language);
|
|
20769
|
+
if (!isStdinOnlySearchFilter(words, language, wordNodes.map((word) => word.type === "command_parameter")))
|
|
20569
20770
|
return false;
|
|
20570
|
-
|
|
20571
|
-
|
|
20572
|
-
if (
|
|
20771
|
+
if (wordNodes.some((word) => hasUntranslatedShellSyntax(word, language)))
|
|
20772
|
+
return false;
|
|
20773
|
+
if (hasInputRedirect(node))
|
|
20573
20774
|
return false;
|
|
20574
20775
|
const pipeline = node.parent;
|
|
20575
|
-
if (!pipeline || pipeline
|
|
20776
|
+
if (!pipeline || !["pipeline", "pipeline_chain"].includes(pipeline.type))
|
|
20576
20777
|
return false;
|
|
20577
|
-
const
|
|
20578
|
-
const
|
|
20579
|
-
if (
|
|
20778
|
+
const stages = pipeline.namedChildren.filter((child) => child !== null);
|
|
20779
|
+
const position = stages.findIndex((stage) => stage.startIndex <= node.startIndex && stage.endIndex >= node.endIndex);
|
|
20780
|
+
if (position < 1)
|
|
20580
20781
|
return false;
|
|
20581
|
-
|
|
20582
|
-
|
|
20583
|
-
|
|
20584
|
-
|
|
20782
|
+
let fileObjectFlow = false;
|
|
20783
|
+
for (const [index, stage] of stages.entries()) {
|
|
20784
|
+
if (index === position)
|
|
20785
|
+
continue;
|
|
20786
|
+
const commands = stage.descendantsOfType("command");
|
|
20787
|
+
for (const candidate of commands) {
|
|
20788
|
+
if (candidate === null)
|
|
20789
|
+
continue;
|
|
20790
|
+
const candidateWords = commandWords(candidate, language);
|
|
20791
|
+
const decision = classifyCommand(candidateWords, language);
|
|
20792
|
+
if (decision.kind || decision.nested)
|
|
20793
|
+
return false;
|
|
20794
|
+
if (language !== "powershell" || index > position)
|
|
20795
|
+
continue;
|
|
20796
|
+
const executable = candidateWords[0];
|
|
20797
|
+
if (executable === null || executable === undefined)
|
|
20798
|
+
continue;
|
|
20799
|
+
const name2 = executableName(executable).toLowerCase();
|
|
20800
|
+
if (POWERSHELL_FILE_OBJECT_PRODUCERS.has(name2))
|
|
20801
|
+
fileObjectFlow = true;
|
|
20802
|
+
}
|
|
20803
|
+
if (language === "powershell" && index < position && stage.type === "command") {
|
|
20804
|
+
const executable = commandWords(stage, language)[0];
|
|
20805
|
+
if (executable !== null && executable !== undefined && POWERSHELL_STRING_CONVERTERS.has(executableName(executable).toLowerCase()))
|
|
20806
|
+
fileObjectFlow = false;
|
|
20807
|
+
}
|
|
20808
|
+
}
|
|
20809
|
+
return !fileObjectFlow;
|
|
20585
20810
|
}
|
|
20586
20811
|
|
|
20587
20812
|
class ShellSearchPolicy {
|
|
@@ -20663,7 +20888,7 @@ class ShellSearchPolicy {
|
|
|
20663
20888
|
}
|
|
20664
20889
|
|
|
20665
20890
|
// src/search-policy.ts
|
|
20666
|
-
var SEARCH_POLICY_GUIDANCE = "Local content and filename searches must use sift-light. Built-in search tools and direct search commands are blocked before execution; filtering output from an unrelated producer at
|
|
20891
|
+
var SEARCH_POLICY_GUIDANCE = "Local content and filename searches must use sift-light. Built-in search tools and direct search commands are blocked before execution; recognized stdin-only filtering of output from an unrelated producer remains available at any pipeline position. File operands, recursive search options, file-sourced patterns and input redirection are not filter exceptions. Use pattern for contents or mode=files with query for filenames. Keep read/edit/write, tests and builds available. After a denial, call sift-light once with the stated repair; do not paste the denial into the request, repeat the blocked call, use another shell/custom script, or weaken the search mode.";
|
|
20667
20892
|
var PI_REPLACED_SEARCH_TOOLS = new Set(["grep", "find"]);
|
|
20668
20893
|
var PREFERRED_SEARCH_GUIDANCE = "Prefer sift-light for local content and filename searches because it provides bounded evidence, coverage and continuation details. Conventional search entries remain available in advisory mode.";
|
|
20669
20894
|
var contentTools = new Set(["grep", "Grep", "SearchFileContent"]);
|
package/src/mcp-server.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { URL as URL2 } from "node:url";
|
|
|
7
7
|
// package.json
|
|
8
8
|
var package_default = {
|
|
9
9
|
name: "sift-light",
|
|
10
|
-
version: "1.0.3-
|
|
10
|
+
version: "1.0.3-4",
|
|
11
11
|
description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
|
|
12
12
|
keywords: [
|
|
13
13
|
"ai-agent",
|
|
@@ -6,9 +6,19 @@ import {
|
|
|
6
6
|
type SearchKind,
|
|
7
7
|
type ShellLanguage,
|
|
8
8
|
} from "./search-policy-commands.js";
|
|
9
|
+
import { isStdinOnlySearchFilter } from "./search-policy-stdin-filter.js";
|
|
9
10
|
|
|
10
11
|
export const MAX_POLICY_COMMAND_BYTES = 64 * 1024;
|
|
11
12
|
const MAX_SHELL_NESTING = 4;
|
|
13
|
+
const POWERSHELL_FILE_OBJECT_PRODUCERS = new Set([
|
|
14
|
+
"get-item",
|
|
15
|
+
"gi",
|
|
16
|
+
"get-childitem",
|
|
17
|
+
"gci",
|
|
18
|
+
"dir",
|
|
19
|
+
"ls",
|
|
20
|
+
]);
|
|
21
|
+
const POWERSHELL_STRING_CONVERTERS = new Set(["out-string", "oss", "convertto-json"]);
|
|
12
22
|
|
|
13
23
|
export interface ShellSearchMatch {
|
|
14
24
|
kind: SearchKind;
|
|
@@ -116,6 +126,18 @@ function commandWords(node: Node, language: ShellLanguage): Array<string | null>
|
|
|
116
126
|
return commandWordNodes(node, language).map((word) => literalWord(word, language));
|
|
117
127
|
}
|
|
118
128
|
|
|
129
|
+
function hasBashWordExpansion(text: string): boolean {
|
|
130
|
+
if (text.startsWith("~") || text.startsWith("=")) return true;
|
|
131
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
132
|
+
if (text[index] === "\\") {
|
|
133
|
+
index += 1;
|
|
134
|
+
} else if ("*?[]{}".includes(text[index] ?? "")) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
119
141
|
function hasUntranslatedShellSyntax(node: Node, language: ShellLanguage): boolean {
|
|
120
142
|
const text = node.text;
|
|
121
143
|
if (language === "powershell") {
|
|
@@ -131,8 +153,7 @@ function hasUntranslatedShellSyntax(node: Node, language: ShellLanguage): boolea
|
|
|
131
153
|
);
|
|
132
154
|
}
|
|
133
155
|
if (["raw_string", "string", "string_content"].includes(node.type)) return false;
|
|
134
|
-
if (["word", "number", "command_name"].includes(node.type))
|
|
135
|
-
return /[*?[\]{}]/u.test(text) || text.startsWith("~") || text.startsWith("=");
|
|
156
|
+
if (["word", "number", "command_name"].includes(node.type)) return hasBashWordExpansion(text);
|
|
136
157
|
if (node.type === "concatenation")
|
|
137
158
|
return node.namedChildren.some(
|
|
138
159
|
(child) => child !== null && hasUntranslatedShellSyntax(child, language),
|
|
@@ -140,36 +161,80 @@ function hasUntranslatedShellSyntax(node: Node, language: ShellLanguage): boolea
|
|
|
140
161
|
return false;
|
|
141
162
|
}
|
|
142
163
|
|
|
164
|
+
function hasInputRedirectNodes(owner: Node): boolean {
|
|
165
|
+
return owner
|
|
166
|
+
.descendantsOfType(["file_redirect", "herestring_redirect", "heredoc_redirect"])
|
|
167
|
+
.some((redirect) => redirect !== null && /^(?:\d+)?</u.test(redirect.text));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hasInputRedirect(node: Node): boolean {
|
|
171
|
+
if (hasInputRedirectNodes(node)) return true;
|
|
172
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
173
|
+
if (parent.type !== "redirected_statement") continue;
|
|
174
|
+
const body = parent.childForFieldName("body");
|
|
175
|
+
const last =
|
|
176
|
+
body?.descendantsOfType("command").at(-1) ?? (body?.type === "command" ? body : undefined);
|
|
177
|
+
if (
|
|
178
|
+
last?.startIndex === node.startIndex &&
|
|
179
|
+
last.endIndex === node.endIndex &&
|
|
180
|
+
parent
|
|
181
|
+
.childrenForFieldName("redirect")
|
|
182
|
+
.some((redirect) => redirect !== null && hasInputRedirectNodes(redirect))
|
|
183
|
+
)
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
143
189
|
function isSafePipelineFilter(
|
|
144
190
|
node: Node,
|
|
145
191
|
language: ShellLanguage,
|
|
146
192
|
words: Array<string | null>,
|
|
147
193
|
): boolean {
|
|
148
|
-
const
|
|
149
|
-
if (executable === null || executable === undefined) return false;
|
|
150
|
-
const name = executableName(executable);
|
|
151
|
-
const filterNames =
|
|
152
|
-
language === "powershell"
|
|
153
|
-
? new Set(["select-string", "sls"])
|
|
154
|
-
: new Set(["grep", "egrep", "fgrep"]);
|
|
155
|
-
if (!filterNames.has(language === "powershell" ? name.toLowerCase() : name)) return false;
|
|
156
|
-
const pipeline = node.parent;
|
|
157
|
-
if (!pipeline || pipeline.type !== "pipeline") return false;
|
|
158
|
-
const commands = pipeline.namedChildren.filter(
|
|
159
|
-
(child): child is Node => child !== null && child.type === "command",
|
|
160
|
-
);
|
|
161
|
-
const last = commands.at(-1);
|
|
194
|
+
const wordNodes = commandWordNodes(node, language);
|
|
162
195
|
if (
|
|
163
|
-
!
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
196
|
+
!isStdinOnlySearchFilter(
|
|
197
|
+
words,
|
|
198
|
+
language,
|
|
199
|
+
wordNodes.map((word) => word.type === "command_parameter"),
|
|
200
|
+
)
|
|
167
201
|
)
|
|
168
202
|
return false;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
203
|
+
if (wordNodes.some((word) => hasUntranslatedShellSyntax(word, language))) return false;
|
|
204
|
+
if (hasInputRedirect(node)) return false;
|
|
205
|
+
const pipeline = node.parent;
|
|
206
|
+
if (!pipeline || !["pipeline", "pipeline_chain"].includes(pipeline.type)) return false;
|
|
207
|
+
const stages = pipeline.namedChildren.filter((child): child is Node => child !== null);
|
|
208
|
+
const position = stages.findIndex(
|
|
209
|
+
(stage) => stage.startIndex <= node.startIndex && stage.endIndex >= node.endIndex,
|
|
210
|
+
);
|
|
211
|
+
if (position < 1) return false;
|
|
212
|
+
let fileObjectFlow = false;
|
|
213
|
+
for (const [index, stage] of stages.entries()) {
|
|
214
|
+
if (index === position) continue;
|
|
215
|
+
const commands = stage.descendantsOfType("command");
|
|
216
|
+
for (const candidate of commands) {
|
|
217
|
+
if (candidate === null) continue;
|
|
218
|
+
const candidateWords = commandWords(candidate, language);
|
|
219
|
+
const decision = classifyCommand(candidateWords, language);
|
|
220
|
+
if (decision.kind || decision.nested) return false;
|
|
221
|
+
if (language !== "powershell" || index > position) continue;
|
|
222
|
+
const executable = candidateWords[0];
|
|
223
|
+
if (executable === null || executable === undefined) continue;
|
|
224
|
+
const name = executableName(executable).toLowerCase();
|
|
225
|
+
if (POWERSHELL_FILE_OBJECT_PRODUCERS.has(name)) fileObjectFlow = true;
|
|
226
|
+
}
|
|
227
|
+
if (language === "powershell" && index < position && stage.type === "command") {
|
|
228
|
+
const executable = commandWords(stage, language)[0];
|
|
229
|
+
if (
|
|
230
|
+
executable !== null &&
|
|
231
|
+
executable !== undefined &&
|
|
232
|
+
POWERSHELL_STRING_CONVERTERS.has(executableName(executable).toLowerCase())
|
|
233
|
+
)
|
|
234
|
+
fileObjectFlow = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return !fileObjectFlow;
|
|
173
238
|
}
|
|
174
239
|
|
|
175
240
|
/** WASM grammars are shipped with the hook; parsing never reads shell scripts or executes code. */
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { executableName, type ShellLanguage } from "./search-policy-commands.js";
|
|
2
|
+
|
|
3
|
+
// Only options whose operands cannot name an input file are accepted here. Unknown
|
|
4
|
+
// options fail closed, since grep variants and PowerShell aliases can add file inputs.
|
|
5
|
+
const GREP_SWITCHES = new Set("abcEFGHhiILlnoPqsTUuvwxyzZ".split(""));
|
|
6
|
+
const GREP_VALUE_OPTIONS = new Set(["A", "B", "C", "m"]);
|
|
7
|
+
const GREP_LONG_SWITCHES = new Set([
|
|
8
|
+
"--basic-regexp",
|
|
9
|
+
"--extended-regexp",
|
|
10
|
+
"--fixed-strings",
|
|
11
|
+
"--perl-regexp",
|
|
12
|
+
"--ignore-case",
|
|
13
|
+
"--no-ignore-case",
|
|
14
|
+
"--invert-match",
|
|
15
|
+
"--word-regexp",
|
|
16
|
+
"--line-regexp",
|
|
17
|
+
"--count",
|
|
18
|
+
"--files-with-matches",
|
|
19
|
+
"--files-without-match",
|
|
20
|
+
"--only-matching",
|
|
21
|
+
"--quiet",
|
|
22
|
+
"--silent",
|
|
23
|
+
"--no-messages",
|
|
24
|
+
"--byte-offset",
|
|
25
|
+
"--line-number",
|
|
26
|
+
"--with-filename",
|
|
27
|
+
"--no-filename",
|
|
28
|
+
"--initial-tab",
|
|
29
|
+
"--null",
|
|
30
|
+
"--null-data",
|
|
31
|
+
"--text",
|
|
32
|
+
"--binary",
|
|
33
|
+
"--line-buffered",
|
|
34
|
+
"--no-group-separator",
|
|
35
|
+
]);
|
|
36
|
+
const GREP_LONG_VALUES = new Set([
|
|
37
|
+
"--after-context",
|
|
38
|
+
"--before-context",
|
|
39
|
+
"--context",
|
|
40
|
+
"--max-count",
|
|
41
|
+
"--binary-files",
|
|
42
|
+
"--label",
|
|
43
|
+
"--group-separator",
|
|
44
|
+
"--include",
|
|
45
|
+
"--exclude",
|
|
46
|
+
"--exclude-dir",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function isStdinOnlyGrep(args: readonly (string | null)[]): boolean {
|
|
50
|
+
const positionals: string[] = [];
|
|
51
|
+
let hasPatternOption = false;
|
|
52
|
+
let optionsEnded = false;
|
|
53
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
54
|
+
const arg = args[index];
|
|
55
|
+
if (arg === null || arg === undefined) return false;
|
|
56
|
+
// Some grep implementations (and GNU grep with POSIXLY_CORRECT) stop option
|
|
57
|
+
// parsing at the first operand. Later option-looking words can be file names.
|
|
58
|
+
if (positionals.length > 0 && arg !== "-") return false;
|
|
59
|
+
if (!optionsEnded && arg === "--") {
|
|
60
|
+
optionsEnded = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!optionsEnded && arg.startsWith("--")) {
|
|
64
|
+
const separator = arg.indexOf("=");
|
|
65
|
+
const option = separator < 0 ? arg : arg.slice(0, separator);
|
|
66
|
+
const attached = separator < 0 ? undefined : arg.slice(separator + 1);
|
|
67
|
+
if (option === "--regexp") {
|
|
68
|
+
hasPatternOption = true;
|
|
69
|
+
} else if (option === "--color" || option === "--colour") {
|
|
70
|
+
continue;
|
|
71
|
+
} else if (GREP_LONG_SWITCHES.has(option) && attached === undefined) {
|
|
72
|
+
continue;
|
|
73
|
+
} else if (!GREP_LONG_VALUES.has(option)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (attached === undefined) {
|
|
77
|
+
index += 1;
|
|
78
|
+
if (args[index] === null || args[index] === undefined) return false;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!optionsEnded && arg.startsWith("-") && arg !== "-") {
|
|
83
|
+
for (let optionIndex = 1; optionIndex < arg.length; optionIndex += 1) {
|
|
84
|
+
const option = arg[optionIndex];
|
|
85
|
+
if (option === "e") {
|
|
86
|
+
hasPatternOption = true;
|
|
87
|
+
} else if (option !== undefined && GREP_SWITCHES.has(option)) {
|
|
88
|
+
continue;
|
|
89
|
+
} else if (option === undefined || !GREP_VALUE_OPTIONS.has(option)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (optionIndex === arg.length - 1) {
|
|
93
|
+
index += 1;
|
|
94
|
+
if (args[index] === null || args[index] === undefined) return false;
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
positionals.push(arg);
|
|
101
|
+
}
|
|
102
|
+
return hasPatternOption
|
|
103
|
+
? positionals.every((operand) => operand === "-")
|
|
104
|
+
: positionals.length > 0 && positionals.slice(1).every((operand) => operand === "-");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const SELECT_STRING_SWITCHES = new Set([
|
|
108
|
+
"simplematch",
|
|
109
|
+
"casesensitive",
|
|
110
|
+
"quiet",
|
|
111
|
+
"list",
|
|
112
|
+
"noemphasis",
|
|
113
|
+
"notmatch",
|
|
114
|
+
"allmatches",
|
|
115
|
+
"raw",
|
|
116
|
+
"verbose",
|
|
117
|
+
"debug",
|
|
118
|
+
]);
|
|
119
|
+
const SELECT_STRING_VALUES = new Set([
|
|
120
|
+
"culture",
|
|
121
|
+
"encoding",
|
|
122
|
+
"context",
|
|
123
|
+
"erroraction",
|
|
124
|
+
"warningaction",
|
|
125
|
+
"informationaction",
|
|
126
|
+
"outvariable",
|
|
127
|
+
"pipelinevariable",
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
function isPipelineOnlySelectString(
|
|
131
|
+
args: readonly (string | null)[],
|
|
132
|
+
namedParameters: readonly boolean[],
|
|
133
|
+
): boolean {
|
|
134
|
+
let hasPattern = false;
|
|
135
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
136
|
+
const arg = args[index];
|
|
137
|
+
if (arg === null || arg === undefined) return false;
|
|
138
|
+
if (!namedParameters[index]) {
|
|
139
|
+
if (hasPattern) return false; // Further positional values can bind to -Path.
|
|
140
|
+
hasPattern = true;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const separator = arg.indexOf(":");
|
|
144
|
+
const option = (separator < 0 ? arg.slice(1) : arg.slice(1, separator)).toLowerCase();
|
|
145
|
+
if (SELECT_STRING_SWITCHES.has(option) && separator < 0) continue;
|
|
146
|
+
if (option !== "pattern" && !SELECT_STRING_VALUES.has(option)) return false;
|
|
147
|
+
if (option === "pattern") {
|
|
148
|
+
if (hasPattern) return false;
|
|
149
|
+
hasPattern = true;
|
|
150
|
+
}
|
|
151
|
+
if (separator < 0) {
|
|
152
|
+
index += 1;
|
|
153
|
+
if (args[index] === null || args[index] === undefined) return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return hasPattern;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The exception is for filters consuming the pipeline, never for file operands. */
|
|
160
|
+
export function isStdinOnlySearchFilter(
|
|
161
|
+
words: readonly (string | null)[],
|
|
162
|
+
language: ShellLanguage,
|
|
163
|
+
namedParameters: readonly boolean[],
|
|
164
|
+
): boolean {
|
|
165
|
+
const executable = words[0];
|
|
166
|
+
if (executable === null || executable === undefined) return false;
|
|
167
|
+
const name = executableName(executable);
|
|
168
|
+
if (language === "powershell")
|
|
169
|
+
return (
|
|
170
|
+
["select-string", "sls"].includes(name.toLowerCase()) &&
|
|
171
|
+
isPipelineOnlySelectString(words.slice(1), namedParameters.slice(1))
|
|
172
|
+
);
|
|
173
|
+
return ["grep", "egrep", "fgrep"].includes(name) && isStdinOnlyGrep(words.slice(1));
|
|
174
|
+
}
|
package/src/search-policy.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { recoverShellSearch } from "./search-policy-recovery.js";
|
|
|
3
3
|
import { ShellSearchPolicy, type ShellSearchMatch } from "./search-policy-shell.js";
|
|
4
4
|
|
|
5
5
|
export const SEARCH_POLICY_GUIDANCE =
|
|
6
|
-
"Local content and filename searches must use sift-light. Built-in search tools and direct search commands are blocked before execution; filtering output from an unrelated producer at
|
|
6
|
+
"Local content and filename searches must use sift-light. Built-in search tools and direct search commands are blocked before execution; recognized stdin-only filtering of output from an unrelated producer remains available at any pipeline position. File operands, recursive search options, file-sourced patterns and input redirection are not filter exceptions. Use pattern for contents or mode=files with query for filenames. Keep read/edit/write, tests and builds available. After a denial, call sift-light once with the stated repair; do not paste the denial into the request, repeat the blocked call, use another shell/custom script, or weaken the search mode.";
|
|
7
7
|
export const PI_REPLACED_SEARCH_TOOLS = new Set(["grep", "find"]);
|
|
8
8
|
export const PREFERRED_SEARCH_GUIDANCE =
|
|
9
9
|
"Prefer sift-light for local content and filename searches because it provides bounded evidence, coverage and continuation details. Conventional search entries remain available in advisory mode.";
|