Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.1.2-2 — 2026-11-02

- Support DeepSeek Harness 0.2.1-rc.5, including bounded direct tool calls in PTC mode.
- Avoid blocking unrelated inline file operations while retaining detection of common inline searches.
- Make incomplete search coverage and continuation requests clearer in the DSH result card.

## 1.1.2-1 — 2026-10-27

- Store Pi search policy guidance in a separate prompt section so tool activation can preserve cached prefixes and signed thinking blocks.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ For conventional search enforcement in other hosts:
- **Claude Code:** `/plugin marketplace add lightsifter/sift-light`, then `/plugin install sift-light@sift-light`.
- **Codex:** `codex plugin marketplace add lightsifter/sift-light`, then `codex plugin add sift-light@sift-light`; review and trust the hook in `/hooks`.
- **Kimi Code:** `/plugins install /absolute/path/plugins/sift-light` using the plugin directory from this repository or installed package, then confirm trust and run `/reload`.
- **DeepSeek Harness 0.2.1-rc.3:** Install the built package with `dsh plugin --profile web add /absolute/path/to/sift-light`, or use the desktop plugin manager. New sessions can call `sift_light`. Minimal uses a one-shot shell, PTC uses native tool calls, and persistent terminal input and scripted workflows are denied. Ordinary subagent calls remain available. This adapter is pinned to that Harness version and requires an update and verification before upgrading Harness.
- **DeepSeek Harness 0.2.1-rc.5:** Install the built package using the desktop plugin manager. New sessions can call `sift_light`. Minimal uses a one-shot shell. PTC permits a direct tool call or a batch of up to 10 direct calls with literal arguments per `run_code`; arbitrary programs, persistent terminal input, and scripted workflows are denied. Ordinary subagent calls remain available. Restart after enabling the plugin. This adapter is pinned to that Harness version and requires verification before upgrading Harness.

The DeepSeek Harness adapter checks built-in `grep`, `glob`, recognized shell search commands, and some file-reading-plus-matching or directory traversal patterns in inline programs such as `node -e` and `python -c`. Inline checks are heuristic: they can reject unrelated code and miss other forms. This is not an OS sandbox: custom scripts, dynamic code, other plugins, and host configuration overrides can bypass these checks, so it cannot guarantee that all local searches use sift-light. Search runs with the host process's permissions. `SIFT_LIGHT_ENFORCE_SEARCH` controls hooks in other hosts and does not change this adapter's restrictions.

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ Codex 插件包含 `local-search` 技能,用于发现和调用延迟加载的
- **Claude Code:** 先运行 `/plugin marketplace add lightsifter/sift-light`,再运行 `/plugin install sift-light@sift-light`。
- **Codex:** 先运行 `codex plugin marketplace add lightsifter/sift-light`,再运行 `codex plugin add sift-light@sift-light`,通过 `/hooks` 查看并信任钩子。
- **Kimi Code:** 使用本仓库或安装包中的插件目录,运行 `/plugins install /absolute/path/plugins/sift-light`,确认信任后执行 `/reload`。
- **DeepSeek Harness 0.2.1-rc.3:** 使用已构建的包安装,例如 `dsh plugin --profile web add /absolute/path/to/sift-light`;桌面版通过插件管理器安装同一个包。新会话中可调用 `sift_light`。Minimal 使用单次 shell,PTC 改为普通工具调用,持久终端输入和 workflow 脚本编排会被拒绝;普通子代理仍可用。该适配目前固定支持此版本,升级 Harness 前需要更新并验证插件。
- **DeepSeek Harness 0.2.1-rc.5:** 在桌面版插件管理器中安装已构建的包。新会话中可调用 `sift_light`。Minimal 使用单次 shell;PTC 的每次 `run_code` 允许直接工具调用,或最多 10 个使用字面量参数的直接调用组成的批次;任意程序、持久终端输入和 workflow 脚本编排会被拒绝,普通子代理仍可用。启用插件后请重启。适配固定支持此版本,升级 Harness 前需要验证插件。

DeepSeek Harness 插件检查内置 `grep`、`glob`、可识别的 shell 搜索命令,以及部分 `node -e`、`python -c` 内联程序中的文件读取加内容匹配或目录遍历。内联代码检查使用启发式规则,可能误拦,也不能识别所有写法。它不是操作系统级沙箱:自定义脚本、动态代码、其他插件以及宿主配置覆盖仍可能绕过检查,因此不能保证所有本地搜索都经过 sift-light。搜索在宿主进程权限下运行。`SIFT_LIGHT_ENFORCE_SEARCH` 仅控制其他宿主的钩子,不改变此适配的限制。

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sift-light",
"version": "1.1.2-1",
"version": "1.1.2-2",
"description": "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
"keywords": [
"ai-agent",
Expand Down Expand Up @@ -141,7 +141,7 @@
},
"engines": {
"bun": ">=1.4.0",
"dsh": "=0.2.1-rc.3",
"dsh": "=0.2.1-rc.5",
"node": ">=22.19.0"
},
"packageManager": "bun@1.4.2",
Expand Down
2 changes: 1 addition & 1 deletion plugins/sift-light/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sift-light",
"version": "1.1.2-1",
"version": "1.1.2-2",
"description": "Require sift-light for conventional local searches while keeping development tools available.",
"author": {
"name": "baoer"
Expand Down
2 changes: 1 addition & 1 deletion plugins/sift-light/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sift-light",
"version": "1.1.2-1",
"version": "1.1.2-2",
"description": "Require sift-light for conventional local searches while keeping development tools available.",
"author": {
"name": "baoer"
Expand Down
2 changes: 1 addition & 1 deletion plugins/sift-light/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"args": [
"--yes",
"--package",
"sift-light-runtime@npm:sift-light@1.1.2-1",
"sift-light-runtime@npm:sift-light@1.1.2-2",
"sift-light-mcp",
"--stdio"
],
Expand Down
2 changes: 1 addition & 1 deletion plugins/sift-light/dsh-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ function searchCard({ block, phase, openFile, locale }) {
background: color(i),
borderRadius: 99
}
})) : null)), view.rows.length > rows.length ? h("div", { style: styles.label }, chinese ? `另有 ${view.rows.length - rows.length} 个文件未展示` : `${view.rows.length - rows.length} more files not shown`) : null) : valid && view.total === 0 ? h("div", { style: { ...styles.rows, ...styles.label } }, chinese ? "没有找到匹配结果" : "No results found") : null, valid && view.more ? h("div", { style: { ...styles.rows, ...styles.label } }, chinese ? "还有后续结果" : "More results available") : null, text ? h("details", { style: styles.details }, h("summary", { style: { ...styles.label, cursor: "pointer" } }, chinese ? "查看搜索详情" : "View search details"), h("pre", { style: styles.output }, text)) : null);
})) : null)), view.rows.length > rows.length ? h("div", { style: styles.label }, chinese ? `另有 ${view.rows.length - rows.length} 个文件未展示` : `${view.rows.length - rows.length} more files not shown`) : null) : valid && view.total === 0 && !view.partial && !block.isError ? h("div", { style: { ...styles.rows, ...styles.label } }, chinese ? "没有找到匹配结果" : "No results found") : null, valid && view.partial ? h("div", { style: { ...styles.rows, ...styles.label } }, chinese ? `搜索范围或结果不完整${view.unavailable ? `;${view.unavailable} 个位置无法读取` : ""}。请展开详情查看原因。` : `Search coverage or results are incomplete${view.unavailable ? `; ${view.unavailable} locations could not be read` : ""}. Open details for the reason.`) : null, valid && view.more ? h("div", { style: { ...styles.rows, ...styles.label } }, chinese ? "还有后续结果;展开详情查看继续请求。" : "More results are available; open details for the continuation request.") : null, text ? h("details", { style: styles.details, open: block.isError || undefined }, h("summary", { style: { ...styles.label, cursor: "pointer" } }, chinese ? "查看搜索详情" : "View search details"), h("pre", { style: styles.output }, text)) : null);
}
var inject = ["slots", "locale"];
function apply(ctx) {
Expand Down
209 changes: 199 additions & 10 deletions plugins/sift-light/dsh-entry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2899,7 +2899,7 @@ ${JSON.stringify(details)}`,
// package.json
var package_default = {
name: "sift-light",
version: "1.1.2-1",
version: "1.1.2-2",
description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
keywords: [
"ai-agent",
Expand Down Expand Up @@ -3040,7 +3040,7 @@ var package_default = {
},
engines: {
bun: ">=1.4.0",
dsh: "=0.2.1-rc.3",
dsh: "=0.2.1-rc.5",
node: ">=22.19.0"
},
packageManager: "bun@1.4.2",
Expand Down Expand Up @@ -16812,6 +16812,171 @@ async function createMcpSearchFeatures(environment = process.env) {
};
}

// src/dsh-ptc-policy.ts
var inheritedNames = new Set([
"constructor",
"prototype",
"toString",
"valueOf",
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
"toLocaleString"
]);
function validToolName(name) {
return !name.startsWith("__") && !inheritedNames.has(name);
}
function validJsonObject(value) {
let normalized = "";
let quote = "";
let escaped = false;
for (let index = 0;index < value.length; index++) {
const char = value[index];
if (quote === "`") {
if (escaped) {
if (char !== undefined)
normalized += JSON.stringify(char).slice(1, -1);
escaped = false;
} else if (char === "\\") {
normalized += "\\\\";
escaped = true;
} else if (char === "`") {
normalized += '"';
quote = "";
} else if (char === "$" && value[index + 1] === "{") {
return false;
} else if (char !== undefined) {
normalized += JSON.stringify(char).slice(1, -1);
}
continue;
}
if (quote === '"') {
normalized += char;
if (escaped)
escaped = false;
else if (char === "\\")
escaped = true;
else if (char === '"')
quote = "";
continue;
}
if (quote === "'") {
if (escaped) {
if (char === "'")
normalized += "'";
else if (char && '"\\/bfnrtu'.includes(char))
normalized += `\\${char}`;
else
return false;
escaped = false;
} else if (char === "\\")
escaped = true;
else if (char === "'") {
normalized += '"';
quote = "";
} else if (char === '"')
normalized += "\\\"";
else if (char && char.charCodeAt(0) < 32)
return false;
else
normalized += char;
continue;
}
if (char === '"' || char === "'" || char === "`") {
quote = char;
normalized += '"';
} else {
normalized += char;
if (char === "{" || char === ",") {
const key = /^(\s*)([A-Za-z_$][\w$]*)(\s*:)/u.exec(value.slice(index + 1));
if (key?.[1] !== undefined && key[2] && key[3] !== undefined) {
normalized += `${key[1]}"${key[2]}"${key[3]}`;
index += key[0].length;
}
}
}
}
if (quote || escaped)
return false;
try {
const parsed = JSON.parse(normalized);
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
} catch {
return false;
}
}
function isDirectBatch(code) {
const forms = [
/^\s*return\s+await\s+Promise\.all\(\s*\[([\s\S]*)\]\s*\)\s*;?\s*$/u,
/^\s*console\.log\(\s*await\s+Promise\.all\(\s*\[([\s\S]*)\]\s*\)\s*\)\s*;?\s*$/u
];
const outer = forms.map((form) => form.exec(code)).find(Boolean);
if (!outer?.[1])
return false;
const calls = outer[1];
let cursor = 0;
let count = 0;
while (cursor < calls.length) {
const prefix = /^\s*tools\.([A-Za-z_$][\w$]*)\s*\(\s*/u.exec(calls.slice(cursor));
if (!prefix?.[1] || !validToolName(prefix[1]))
return false;
cursor += prefix[0].length;
if (calls[cursor] !== "{")
return false;
const start = cursor;
let depth = 0;
let quote = "";
let escaped = false;
for (;cursor < calls.length; cursor++) {
const char = calls[cursor];
if (quote) {
if (escaped)
escaped = false;
else if (char === "\\")
escaped = true;
else if (char === quote)
quote = "";
} else if (char === '"' || char === "'" || char === "`")
quote = char;
else if (char === "{")
depth++;
else if (char === "}" && --depth === 0) {
cursor++;
break;
}
}
if (depth !== 0 || !validJsonObject(calls.slice(start, cursor)))
return false;
const suffix = /^\s*\)\s*(,|$)/u.exec(calls.slice(cursor));
if (!suffix)
return false;
cursor += suffix[0].length;
count++;
if (count > 10)
return false;
if (suffix[1] !== "," || calls.slice(cursor).trim() === "")
return calls.slice(cursor).trim() === "";
}
return false;
}
function isDirectPtcToolCall(value) {
if (typeof value !== "object" || value === null || !("code" in value))
return false;
const code = value.code;
if (typeof code !== "string" || code.length > 1e5)
return false;
const forms = [
/^\s*await\s+tools\.([A-Za-z_$][\w$]*)\s*\(\s*(\{[\s\S]*\})\s*\)\s*;?\s*$/u,
/^\s*return\s+await\s+tools\.([A-Za-z_$][\w$]*)\s*\(\s*(\{[\s\S]*\})\s*\)\s*;?\s*$/u,
/^\s*console\.log\(\s*await\s+tools\.([A-Za-z_$][\w$]*)\s*\(\s*(\{[\s\S]*\})\s*\)\s*\)\s*;?\s*$/u,
/^\s*console\.log\(\s*JSON\.stringify\(\s*await\s+tools\.([A-Za-z_$][\w$]*)\s*\(\s*(\{[\s\S]*\})\s*\)\s*\)\s*\)\s*;?\s*$/u
];
const match = forms.map((form) => form.exec(code)).find(Boolean);
if (match?.[1] && match[2])
return validToolName(match[1]) && validJsonObject(match[2]);
return isDirectBatch(code);
}

// src/search-policy-recovery.ts
import { isAbsolute as isAbsolute9, relative as relative9, resolve as resolve28, sep as sep5 } from "node:path";
var CONTENT_MANUAL_REASON = "the command is not one standalone static rg search using the supported option and single-target subset";
Expand Down Expand Up @@ -17184,15 +17349,32 @@ function classifyInlineSearch(name, args) {
if (node) {
if (/\b(?:readdir(?:Sync)?|opendir(?:Sync)?|glob(?:Sync)?)\s*\(/u.test(program))
return "files";
const readsFile = /\b(?:readFile(?:Sync)?|createReadStream)\s*\(/u.test(program);
const searches = /\.(?:includes|indexOf|match|matchAll|search|test)\s*\(/u.test(program);
return readsFile && searches ? "content" : undefined;
const directSearch = /\breadFile(?:Sync)?\s*\([^)]*\)\s*\.(?:includes|indexOf|match|matchAll|search)\s*\(/u;
if (directSearch.test(program))
return "content";
const text = /\b(?:const|let|var)\s+([\w$]+)\s*=\s*(?:(?:[\w$.]+|require\s*\([^)]*\))\.)?readFile(?:Sync)?\s*\([^)]*\)/gu;
for (const match of program.matchAll(text)) {
const variable = match[1];
const calls = /(?<![\w$])([\w$]+)\s*\.(?:includes|indexOf|match|matchAll|search)\s*\(|\.test\s*\(\s*([\w$]+)\s*\)/gu;
for (const call of program.matchAll(calls)) {
if (call[1] === variable || call[2] === variable)
return "content";
}
}
return;
}
if (/\b(?:os\.walk|os\.listdir|os\.scandir|glob\.(?:i?glob)|\w+\.(?:rglob|glob))\s*\(/u.test(program))
return "files";
const readsFile = /\b(?:open|\w+\.read_text|\w+\.read_bytes)\s*\(/u.test(program);
const searches = /\b(?:re\.(?:search|match|findall|finditer)|\w+\.(?:find|index|count))\s*\(|\bin\s+\w/u.test(program);
return readsFile && searches ? "content" : undefined;
const read = String.raw`(?:open\s*\([^)]*\)\s*\.read\s*\(\s*\)|\w+\.read_(?:text|bytes)\s*\([^)]*\))`;
if (new RegExp(String.raw`["'][^"']+["']\s+in\s+${read}|${read}\s*\.(?:find|index|count)\s*\(`, "u").test(program))
return "content";
const text = new RegExp(String.raw`\b(\w+)\s*=\s*${read}`, "gu");
for (const match of program.matchAll(text)) {
const variable = match[1];
if (new RegExp(String.raw`["'][^"']+["']\s+in\s+${variable}\b|\b${variable}\s*\.(?:find|index|count)\s*\(|\bre\.(?:search|match|findall|finditer)\s*\([^,]+,\s*${variable}\b`, "u").test(program))
return "content";
}
return new RegExp(String.raw`\bre\.(?:search|match|findall|finditer)\s*\([^,]+,\s*${read}`, "u").test(program) ? "content" : undefined;
}

// src/search-policy-commands.ts
Expand Down Expand Up @@ -18116,7 +18298,9 @@ async function apply(ctx) {
const approved = new Set;
ctx.tools.register({
name: "sift_light",
description: SIFT_LIGHT_MODEL_DESCRIPTION,
description: `${SIFT_LIGHT_MODEL_DESCRIPTION}

In DSH PTC mode, each run_code must contain either one direct tool call with literal arguments or return await Promise.all([tools.sift_light({"path":".","pattern":"one"}), tools.sift_light({"path":".","pattern":"two"})]) with up to 10 direct calls. Return the complete result, for example: return await tools.sift_light({"path":".","pattern":"needle","literal":true}) or return await tools.read({"file_path":"/absolute/path"}). Template strings without interpolation are allowed; keys may be unquoted. Do not declare variables or process results. Separate calls are allowed. Other programs are blocked by the search policy.`,
parameters: JSON.parse(JSON.stringify(siftLightSchema)),
output: {
schema: {
Expand Down Expand Up @@ -18159,7 +18343,12 @@ async function apply(ctx) {
return downstream;
});
ctx.tools.guard((exec) => {
if (["run_code", "workflow", "ralph"].includes(exec.name)) {
if (exec.name === "run_code") {
if (isDirectPtcToolCall(exec.arguments))
return;
return 'Each run_code must contain a direct call to an available tool with literal arguments, for example: return await tools.read({"file_path":"/absolute/path"}) or return await tools.sift_light({"path":".","pattern":"needle","literal":true}). Template strings without interpolation are allowed; keys may be unquoted. For parallel calls use return await Promise.all([tools.sift_light({"path":".","pattern":"one"}), tools.sift_light({"path":".","pattern":"two"})]) with literal arguments, up to 10 calls. Do not declare variables or transform results.';
}
if (["workflow", "ralph"].includes(exec.name)) {
return "Programmatic execution is disabled by sift-light search policy; use native tools.";
}
if (terminalNames.has(exec.name))
Expand Down
Loading
Loading