sift-light 1.0.3-2 → 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 CHANGED
@@ -1,5 +1,14 @@
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
+
8
+ ## 1.0.3-3 — 2026-10-04
9
+
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)).
11
+
3
12
  ## 1.0.3-2 — 2026-09-30
4
13
 
5
14
  - Accept an empty `sourceCursor` placeholder outside `mode: "inspect"` by removing the key before mode validation, with the normalization disclosed in text and `details.requestNotes` ([#101](https://github.com/lightsifter/sift-light/issues/101)). Nonempty source continuation tokens still require inspection mode; empty inspection tokens and ordinary `cursor` validation retain their existing failure behavior.
package/README.md CHANGED
@@ -44,7 +44,7 @@ The first uncached Concept or hybrid search loads the local model and may take t
44
44
 
45
45
  Ask the agent to restrict a search to one folder when that is the scope you need. If you remember only part of a filename, start by finding the file and then inspect its contents—like narrowing a cabinet down to a shelf and then a document.
46
46
 
47
- Multi-word `files` queries require each word literally in the path; a single abbreviation still supports fuzzy matching. Use `hybrid` or `concept` for business intent. Ordinary content searches retain their default zero-result expansion; use `scope: "strict"` to stay within an explicit path. Expansion is announced before returned evidence.
47
+ Multi-word `files` queries require each word literally in the path; a single abbreviation still supports fuzzy matching. Use `hybrid` or `concept` for business intent. Content searches inside the active project still expand after zero results by default. An explicit path outside the project stays isolated; pass `scope: "expand"` to retry the server project. Use `scope: "strict"` to prevent in-project expansion. Expansion is announced before returned evidence.
48
48
 
49
49
  ### Know what has been shown
50
50
 
@@ -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 trailing filter such as `cat report.txt | grep warning` remains available because it filters output from a non-search producer; `find src | grep test` and `rg warning src | grep result` remain denied because their pipelines already 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.
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
@@ -44,7 +44,7 @@ Concept 或 hybrid 较慢时,会在默认五秒等待窗口内返回 `status:
44
44
 
45
45
  需要只查某个文件夹时,可以明确告诉 Agent 限定范围。只记得文件名的一部分,也可以先找文件,再看内容。像先确定资料柜的哪一层,再逐步缩小到要找的那一份。
46
46
 
47
- `files` 多词查询要求每个词都在路径中按字面出现;单个缩写仍支持模糊匹配。业务意图使用 `hybrid` 或 `concept`。普通内容搜索保留零匹配后扩大范围的既有默认;需要限定目录时传 `scope: "strict"`。范围扩大的提示会先于证据显示。
47
+ `files` 多词查询要求每个词都在路径中按字面出现;单个缩写仍支持模糊匹配。业务意图使用 `hybrid` 或 `concept`。项目内路径仍保留零匹配后扩展到项目根目录的既有默认。显式指定到当前 MCP 项目之外的路径时,零匹配后仍默认只查该路径;需要继续查服务器项目时传 `scope: "expand"`。需要始终限定显式路径时传 `scope: "strict"`。范围扩大前会先给出提示。
48
48
 
49
49
  ### 看到了多少,说得清楚
50
50
 
@@ -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` 仍可用,因为它只是过滤一个非搜索命令的输出。`find src | grep test` 和 `rg warning src | grep result` 仍会被拒绝,因为管道中已经包含直接搜索来源。复合 shell 调用中只要一个子命令被拒绝,宿主就不会执行其中任何操作;拒绝信息会指出检测到的搜索,并要求 Agent 单独重试非搜索操作。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sift-light",
3
- "version": "1.0.3-2",
3
+ "version": "1.0.3-4",
4
4
  "description": "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sift-light",
3
- "version": "1.0.3-2",
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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sift-light",
3
- "version": "1.0.3-2",
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"
@@ -5,7 +5,7 @@
5
5
  "args": [
6
6
  "--yes",
7
7
  "--package",
8
- "sift-light-runtime@npm:sift-light@1.0.3-2",
8
+ "sift-light-runtime@npm:sift-light@1.0.3-4",
9
9
  "sift-light-mcp",
10
10
  "--stdio"
11
11
  ],
@@ -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 /[*?[\]{}]/u.test(text) || text.startsWith("~") || text.startsWith("=");
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 executable = words[0];
3889
- if (executable === null || executable === undefined)
4089
+ const wordNodes = commandWordNodes(node, language);
4090
+ if (!isStdinOnlySearchFilter(words, language, wordNodes.map((word) => word.type === "command_parameter")))
3890
4091
  return false;
3891
- const name2 = executableName(executable);
3892
- const filterNames = language === "powershell" ? new Set(["select-string", "sls"]) : new Set(["grep", "egrep", "fgrep"]);
3893
- if (!filterNames.has(language === "powershell" ? name2.toLowerCase() : name2))
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.type !== "pipeline")
4097
+ if (!pipeline || !["pipeline", "pipeline_chain"].includes(pipeline.type))
3897
4098
  return false;
3898
- const commands = pipeline.namedChildren.filter((child) => child !== null && child.type === "command");
3899
- const last = commands.at(-1);
3900
- if (!last || last.startIndex !== node.startIndex || last.endIndex !== node.endIndex || commands.length < 2)
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
- return commands.slice(0, -1).every((candidate) => {
3903
- const decision = classifyCommand(commandWords(candidate, language), language);
3904
- return !decision.kind && !decision.nested;
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-2",
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-2",
16
+ "sift-light-runtime@npm:sift-light@1.0.3-4",
17
17
  "sift-light-mcp",
18
18
  "--stdio"
19
19
  ],