From 0a76700c097eda332bd586f191a042bc0979d5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=9D=E5=84=BF?= <274762368+xcjy8bao@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:22:10 +0800 Subject: [PATCH 1/2] Release sift-light 1.1.2-2 --- CHANGELOG.md | 6 + README.md | 2 +- README.zh-CN.md | 2 +- package.json | 4 +- plugins/sift-light/.claude-plugin/plugin.json | 2 +- plugins/sift-light/.codex-plugin/plugin.json | 2 +- plugins/sift-light/.mcp.json | 2 +- plugins/sift-light/dsh-client.js | 2 +- plugins/sift-light/dsh-entry.mjs | 206 +++++++++++++++++- plugins/sift-light/dsh.patch.yml | 132 ----------- plugins/sift-light/hooks/search-policy.mjs | 26 ++- plugins/sift-light/kimi.plugin.json | 4 +- plugins/sift-light/omp-extension.mjs | 30 ++- plugins/sift-light/package.json | 2 +- scripts/dsh-ptc-preset.yml | 131 ----------- scripts/search-policy-artifact.ts | 4 +- src/dsh-client.mjs | 17 +- src/dsh-plugin.ts | 9 +- src/dsh-ptc-policy.ts | 147 +++++++++++++ src/mcp-server.mjs | 4 +- src/search-policy-inline.ts | 50 ++++- validation/dsh-guard.test.mjs | 105 +++++++++ validation/dsh-inline-search.test.ts | 6 + validation/dsh-ptc-policy.test.ts | 46 ++++ 24 files changed, 624 insertions(+), 317 deletions(-) delete mode 100644 scripts/dsh-ptc-preset.yml create mode 100644 src/dsh-ptc-policy.ts create mode 100644 validation/dsh-guard.test.mjs create mode 100644 validation/dsh-ptc-policy.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 21477506..6ddfe8c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index e047f2c8..df36ee71 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/README.zh-CN.md b/README.zh-CN.md index da53dd71..066dd79e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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` 仅控制其他宿主的钩子,不改变此适配的限制。 diff --git a/package.json b/package.json index 792c21d4..0b5c2763 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/plugins/sift-light/.claude-plugin/plugin.json b/plugins/sift-light/.claude-plugin/plugin.json index 9bf7cfb9..6c758344 100644 --- a/plugins/sift-light/.claude-plugin/plugin.json +++ b/plugins/sift-light/.claude-plugin/plugin.json @@ -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" diff --git a/plugins/sift-light/.codex-plugin/plugin.json b/plugins/sift-light/.codex-plugin/plugin.json index b0ef115e..c501453b 100644 --- a/plugins/sift-light/.codex-plugin/plugin.json +++ b/plugins/sift-light/.codex-plugin/plugin.json @@ -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" diff --git a/plugins/sift-light/.mcp.json b/plugins/sift-light/.mcp.json index 735bb290..ef060b81 100644 --- a/plugins/sift-light/.mcp.json +++ b/plugins/sift-light/.mcp.json @@ -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" ], diff --git a/plugins/sift-light/dsh-client.js b/plugins/sift-light/dsh-client.js index b3b94e41..04a1497a 100644 --- a/plugins/sift-light/dsh-client.js +++ b/plugins/sift-light/dsh-client.js @@ -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) { diff --git a/plugins/sift-light/dsh-entry.mjs b/plugins/sift-light/dsh-entry.mjs index 9daffb81..69c65b8d 100644 --- a/plugins/sift-light/dsh-entry.mjs +++ b/plugins/sift-light/dsh-entry.mjs @@ -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", @@ -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", @@ -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"; @@ -17184,15 +17349,29 @@ 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].replace(/[$]/gu, "\\$"); + if (new RegExp(`(? { - 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)) diff --git a/plugins/sift-light/dsh.patch.yml b/plugins/sift-light/dsh.patch.yml index 8c36e8ad..ed9a1f65 100644 --- a/plugins/sift-light/dsh.patch.yml +++ b/plugins/sift-light/dsh.patch.yml @@ -1,135 +1,3 @@ -# Derived from DeepSeek Harness 0.2.1-rc.3 (MIT); see dsh-upstream-LICENSE. -- id: preset-ptc - name: "@deepseek-ai/dsh-agent-preset" - config: - id: ptc - order: 2 - plugins: - - id: persona - name: "@deepseek-ai/dsh-persona" - config: - prefix: You are a coding agent powered by the {{model}} model. - - id: agent-instructions - name: "@deepseek-ai/dsh-agent-instructions" - config: - maxBytes: 65536 - - id: time-context - name: "@deepseek-ai/dsh-time-context" - - id: tool-bash - name: "@deepseek-ai/dsh-tool-bash" - disabled: !!js process.platform === 'win32' - - id: tool-pwsh - name: "@deepseek-ai/dsh-tool-pwsh" - disabled: !!js process.platform !== 'win32' - - id: tool-fs - name: "@deepseek-ai/dsh-tool-fs" - - id: tool-fs-search - name: "@deepseek-ai/dsh-tool-fs-search" - config: - sampleOverCapGlobResults: false - - id: tool-jobs - name: "@deepseek-ai/dsh-tool-jobs" - - id: tool-schedule - name: "@deepseek-ai/dsh-tool-schedule" - - id: skill-filesystem - name: "@deepseek-ai/dsh-skill-filesystem" - - id: tool-skill - name: "@deepseek-ai/dsh-tool-skill" - - id: command-goal - name: "@deepseek-ai/dsh-command-goal" - - id: tool-goal - name: "@deepseek-ai/dsh-tool-goal" - - id: planning - name: cordis:group - group: true - isolate: - planMode: true - config: - - id: plan-mode - name: "@deepseek-ai/dsh-plan-mode" - config: - section: | - You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode. - - Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery. - - The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode. - - Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out. - - Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions. - - When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation. - - id: compaction - name: cordis:group - group: true - isolate: - compaction: true - toolResultPruner: true - config: - - id: compaction-basic - name: "@deepseek-ai/dsh-compaction-basic" - - id: command-compact - name: "@deepseek-ai/dsh-command-compact" - - id: tool-result-pruner - name: "@deepseek-ai/dsh-compaction-tool-result-pruner" - config: - thresholdChars: 8192 - headChars: 4096 - tailChars: 1024 - - id: delegation - name: cordis:group - group: true - isolate: - workflowEngine: true - config: - - id: tool-subagent-control - name: "@deepseek-ai/dsh-tool-subagent-control" - - id: tool-subagent-list-agents - name: "@deepseek-ai/dsh-tool-subagent-control/list-agents" - - id: tool-subagent - name: "@deepseek-ai/dsh-tool-subagent" - config: - provider: spawn - toolName: subagent - modelSelectionSettings: true - - id: tool-subagent-fork - name: "@deepseek-ai/dsh-tool-subagent" - config: - provider: fork - toolName: subagent_fork - - id: workflow-ptc - name: "@deepseek-ai/dsh-workflow-ptc" - disabled: true - config: - provider: spawn - - id: tool-workflow - name: "@deepseek-ai/dsh-tool-workflow" - disabled: true - - id: tool-ask-user - name: "@deepseek-ai/dsh-tool-ask-user" - - id: tool-todo - name: "@deepseek-ai/dsh-tool-todo" - config: - allowParallelInProgress: true - - id: tool-web - name: "@deepseek-ai/dsh-tool-web" - config: - fetch: true - searchTimeoutMs: 60000 - - id: tool-presentation - name: "@deepseek-ai/dsh-agent-tool-presentation" - config: - mode: native - - id: present - name: "@deepseek-ai/dsh-tool-present" - - id: tool-plugin-manager - name: "@deepseek-ai/dsh-plugin-manager/tools" - disabled: true - - id: tool-plugin-market - name: "@deepseek-ai/dsh-plugin-market/tools" - disabled: true - - id: preset-minimal name: '@deepseek-ai/dsh-agent-preset' config: diff --git a/plugins/sift-light/hooks/search-policy.mjs b/plugins/sift-light/hooks/search-policy.mjs index c514c8af..2a7fd6d8 100755 --- a/plugins/sift-light/hooks/search-policy.mjs +++ b/plugins/sift-light/hooks/search-policy.mjs @@ -3647,15 +3647,29 @@ function classifyInlineSearch(name2, args2) { 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].replace(/[$]/gu, "\\$"); + if (new RegExp(`(?=1.4.0", - dsh: "=0.2.1-rc.3", + dsh: "=0.2.1-rc.5", node: ">=22.19.0" }, packageManager: "bun@1.4.2", @@ -28645,15 +28645,29 @@ function classifyInlineSearch(name2, args2) { 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].replace(/[$]/gu, "\\$"); + if (new RegExp(`(? { clientPath, `window.__ModuleLoader__.load({ id: "sift-light", factory: (require) => {\nvar module = { exports: {} }; var exports = module.exports;\n${client}\nreturn module.exports;\n} });\n`, ); - const ptcPreset = await readFile(join(repository, "scripts/dsh-ptc-preset.yml"), "utf8"); - const dshPatch = `${ptcPreset} -- id: preset-minimal + const dshPatch = `- id: preset-minimal name: '@deepseek-ai/dsh-agent-preset' config: id: minimal diff --git a/src/dsh-client.mjs b/src/dsh-client.mjs index 4afd5abc..469de516 100644 --- a/src/dsh-client.mjs +++ b/src/dsh-client.mjs @@ -327,24 +327,35 @@ function searchCard({ block, phase, openFile, locale }) { ) : null, ) - : valid && view.total === 0 + : 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 available", + chinese + ? "还有后续结果;展开详情查看继续请求。" + : "More results are available; open details for the continuation request.", ) : null, text ? h( "details", - { style: styles.details }, + { style: styles.details, open: block.isError || undefined }, h( "summary", { style: { ...styles.label, cursor: "pointer" } }, diff --git a/src/dsh-plugin.ts b/src/dsh-plugin.ts index 1cf4ca44..af505ec7 100644 --- a/src/dsh-plugin.ts +++ b/src/dsh-plugin.ts @@ -2,6 +2,7 @@ import { resolve } from "node:path"; import { createDefaultSiftLightMcpService, parseSiftLightInput } from "./mcp.js"; import { compactMcpModelText } from "./mcp-model-output.js"; import { createMcpSearchFeatures } from "./mcp-semantic-judge.js"; +import { isDirectPtcToolCall } from "./dsh-ptc-policy.js"; import { SearchPolicy } from "./search-policy.js"; import { SIFT_LIGHT_MODEL_DESCRIPTION, siftLightSchema } from "./tool-schema.js"; import { dashboard } from "./tui/dashboard.js"; @@ -72,7 +73,7 @@ export async function apply(ctx: DshContext): Promise { ctx.tools.register({ name: "sift_light", - description: SIFT_LIGHT_MODEL_DESCRIPTION, + description: `${SIFT_LIGHT_MODEL_DESCRIPTION}\n\nIn 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.`, // TypeBox schemas carry symbols that DSH rejects in model-facing JSON schemas. parameters: JSON.parse(JSON.stringify(siftLightSchema)) as unknown, output: { @@ -122,7 +123,11 @@ export async function apply(ctx: DshContext): Promise { return downstream; }); ctx.tools.guard((exec) => { - if (["run_code", "workflow", "ralph"].includes(exec.name)) { + if (exec.name === "run_code") { + if (isDirectPtcToolCall(exec.arguments)) return undefined; + 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)) diff --git a/src/dsh-ptc-policy.ts b/src/dsh-ptc-policy.ts new file mode 100644 index 00000000..d2d5cbc4 --- /dev/null +++ b/src/dsh-ptc-policy.ts @@ -0,0 +1,147 @@ +const inheritedNames = new Set([ + "constructor", + "prototype", + "toString", + "valueOf", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "toLocaleString", +]); + +function validToolName(name: string): boolean { + return !name.startsWith("__") && !inheritedNames.has(name); +} + +function validJsonObject(value: string): boolean { + // Normalize a safe subset of JavaScript object literals into JSON without + // evaluating code. Template strings are allowed only without interpolation. + // Values remain literals; computed keys and expressions fail. + 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) < 0x20) 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: unknown = JSON.parse(normalized); + return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed); + } catch { + return false; + } +} + +// Only JSON-compatible literals are admitted: expressions inside arguments could access +// the runtime without passing through the nested tool policy. +function isDirectBatch(code: string): boolean { + 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; +} + +/** Permit direct tool invocations; DSH applies its policy to each nested call. */ +export function isDirectPtcToolCall(value: unknown): boolean { + if (typeof value !== "object" || value === null || !("code" in value)) return false; + const code = value.code; + if (typeof code !== "string" || code.length > 100_000) 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); +} diff --git a/src/mcp-server.mjs b/src/mcp-server.mjs index 61389c7a..dc90264c 100755 --- a/src/mcp-server.mjs +++ b/src/mcp-server.mjs @@ -2903,7 +2903,7 @@ import { URL as URL2 } from "node:url"; // 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", @@ -3044,7 +3044,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", diff --git a/src/search-policy-inline.ts b/src/search-policy-inline.ts index c3476f58..b4b5269a 100644 --- a/src/search-policy-inline.ts +++ b/src/search-policy-inline.ts @@ -14,9 +14,22 @@ export function classifyInlineSearch( 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]!.replace(/[$]/gu, "\\$"); + if ( + new RegExp( + `(? ({ + name, + arguments: args, + token: Symbol(), + signal: new AbortController().signal, +}); + +test("DSH guard permits ordinary inline file operations while blocking searches", async () => { + const directory = await mkdtemp(join(tmpdir(), "sift-125-")); + let preExecute; + let cleanup; + try { + await writeFile(join(directory, "items.json"), '[{"name":"item","kind":"y"}]'); + await apply({ + tools: { register() {}, guard() {} }, + workingDirectory: { ensure: async () => directory }, + on(event, listener) { + if (event === "tools/pre-execute") preExecute = listener; + }, + effect(fn) { + cleanup = fn(); + }, + }); + const run = async (command) => { + const decision = await preExecute( + call("bash", { command, workdir: directory }), + async () => ({ kind: "allow" }), + ); + return decision.kind === "allow" + ? execFileSync("bash", ["-c", command], { cwd: directory, encoding: "utf8" }) + : decision.kind; + }; + expect( + await run( + `python3 -c "open('probe.txt','w').write(chr(10).join('line %d' % i for i in range(3)))"`, + ), + ).toBe(""); + expect(await readFile(join(directory, "probe.txt"), "utf8")).toBe("line 0\nline 1\nline 2"); + expect( + await run( + `python3 -c "import json; d=json.load(open('items.json')); print([r['name'] for r in d if r['kind']=='y'])"`, + ), + ).toContain("item"); + expect(await run(`python3 -c 'print("marker" in open("probe.txt").read())'`)).toBe("deny"); + expect(await run("printf before; find . -type f")).toBe("deny"); + } finally { + await cleanup?.(); + await rm(directory, { recursive: true, force: true }); + } +}); + +test("nested calls still pass through the DSH search guard", async () => { + let guard; + let preExecute; + let cleanup; + const ctx = { + tools: { + register() {}, + guard(fn) { + guard = fn; + }, + }, + workingDirectory: { ensure: async () => "/tmp" }, + on(event, listener) { + if (event === "tools/pre-execute") preExecute = listener; + }, + effect(fn) { + cleanup = fn(); + }, + }; + await apply(ctx); + try { + expect(guard).toBeDefined(); + expect(preExecute).toBeDefined(); + expect( + guard(call("run_code", { code: 'return await tools.bash({"command":"pwd"})' })), + ).toBeUndefined(); + expect( + guard( + call("run_code", { + code: "console.log(await Promise.all([tools.sift_light({pattern: 'needle'}), tools.bash({command: 'pwd'})]))", + }), + ), + ).toBeUndefined(); + expect( + guard(call("run_code", { code: "return await tools.bash({command: process.cwd()})" })), + ).toContain("must contain a direct call"); + expect(guard(call("grep", {}))).toContain("blocks grep"); + const shell = call("bash", { command: "pwd", workdir: "/tmp" }); + expect((await preExecute(shell, async () => ({ kind: "allow" }))).kind).toBe("allow"); + expect(guard(shell)).toBeUndefined(); + const blocked = call("bash", { command: "rg needle .", workdir: "/tmp" }); + expect((await preExecute(blocked, async () => ({ kind: "allow" }))).kind).toBe("deny"); + expect(guard(blocked)).toContain("could not check"); + } finally { + await cleanup?.(); + } +}); diff --git a/validation/dsh-inline-search.test.ts b/validation/dsh-inline-search.test.ts index d60e94b0..62bbcd41 100644 --- a/validation/dsh-inline-search.test.ts +++ b/validation/dsh-inline-search.test.ts @@ -13,6 +13,7 @@ describe("DSH inline search detection", () => { `bash -c 'node -e "fs.readdirSync()"'`, `python3 -c 'print("marker" in open("sample.txt").read())'`, `python3 -c 'import re; print(re.search("marker", open("sample.txt").read()))'`, + `python3 -c 's = open("sample.txt").read(); print("marker" in s)'`, `python3 -c 'import os; print(list(os.walk("src")))'`, `python3 -c 'import glob; print(glob.glob("src/*.ts"))'`, ]; @@ -27,6 +28,11 @@ describe("DSH inline search detection", () => { `node -e 'console.log(require("fs").readFileSync("sample.txt", "utf8"))'`, `python3 -c 'print("marker" in "marker")'`, `python3 -c 'print(open("sample.txt").read())'`, + `python3 -c "open('/tmp/sift_policy_probe.txt','w').write(chr(10).join('line %d' % i for i in range(3)))"`, + `python3 -c "import json; d=json.load(open('/tmp/items.json')); print([r['name'] for r in d if r['kind']=='y'])"`, + `python3 -c 's = open("sample.txt").read(); print([x for x in s])'`, + `python3 -c 'print([x for x in open("sample.txt").read()])'`, + `node -e 'const d=JSON.parse(require("fs").readFileSync("sample.json")); console.log(d.some(r => r.name.includes("x")))'`, "node scripts/build.js", "python3 scripts/check.py", "bun test", diff --git a/validation/dsh-ptc-policy.test.ts b/validation/dsh-ptc-policy.test.ts new file mode 100644 index 00000000..81316681 --- /dev/null +++ b/validation/dsh-ptc-policy.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from "bun:test"; +import { isDirectPtcToolCall } from "../src/dsh-ptc-policy.js"; + +describe("DSH PTC policy", () => { + test("accepts direct calls with JSON arguments and rejects programs", () => { + for (const code of [ + 'return await tools.sift_light({"path":".","pattern":"needle"})', + 'console.log(await tools.bash({"command":"pwd"}));', + 'console.log(JSON.stringify(await tools.sift_light({"pattern":"}); evil()"})))', + "await tools.sift_light({})", + `return await tools.sift_light({path: '.', pattern: 'marker'})`, + `console.log(await Promise.all([tools.sift_light({path: '.', pattern: 'a}b'}), tools.bash({command: 'pwd'})]))`, + String.raw`return await tools.sift_light({pattern: 'don\'t say "hello"'})`, + String.raw`return await tools.sift_light({pattern: '\u0061'})`, + 'return await Promise.all([tools.sift_light({path:".", pattern:"x"}),])', + 'return await Promise.all([tools.sift_light({path: ".", pattern: "x", literal: true}), tools.bash({command: "pwd"})])', + 'return await tools.sift_light({pattern: "{unquoted: text}", nested: {key: 1}})', + "return await tools.visualize({fragment: `
\n\n
`})", + 'return await Promise.all([tools.visualize({fragment: `
{}
`}), tools.read({path: "."})])', + "return await tools.visualize({fragment: `escaped \\` and \\${literal}`})", + 'return await Promise.all([tools.sift_light({"pattern":"x"}), tools.bash({"command":"pwd"})])', + 'return await Promise.all([tools.sift_light({"pattern":"} , \\""})])', + ]) + expect(isDirectPtcToolCall({ code }), code).toBe(true); + for (const code of [ + 'await tools.sift_light({}); await tools.bash({"command":"pwd"})', + 'return await tools.sift_light({"pattern": process.cwd()})', + "console.log(await tools.constructor({}))", + 'return await tools.sift_light({"x":1}); require("fs").readdirSync(".")', + 'return await tools.sift_light({"x":1,})', + "return await tools.sift_light([])", + `return await tools.sift_light({pattern: 'x' + process.cwd()})`, + `console.log(await Promise.all([tools.sift_light({pattern: 'x'}), process.exit()]))`, + String.raw`return await tools.sift_light({pattern: '\x41'})`, + "return await tools.visualize({fragment: `${process.exit()}`})", + "return await tools.visualize({fragment: `\\\\${process.exit()}`})", + "return await tools.visualize({fragment: `unclosed})", + 'return await Promise.all([tools.bash({"command": process.cwd()})])', + "return await Promise.all([tools.sift_light({}), process.exit()])", + "return await Promise.all([tools.constructor({})])", + `return await Promise.all([${Array(11).fill("tools.sift_light({})").join(",")}])`, + ]) + expect(isDirectPtcToolCall({ code }), code).toBe(false); + expect(isDirectPtcToolCall(null)).toBe(false); + }); +}); From 2480de2357d8850c300b49e26ae2fa42b8e2f681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=9D=E5=84=BF?= <274762368+xcjy8bao@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:26:48 +0800 Subject: [PATCH 2/2] Avoid dynamic regex escaping in inline policy --- plugins/sift-light/dsh-entry.mjs | 9 ++++++--- plugins/sift-light/hooks/search-policy.mjs | 9 ++++++--- plugins/sift-light/omp-extension.mjs | 9 ++++++--- src/search-policy-inline.ts | 14 ++++++-------- validation/dsh-inline-search.test.ts | 2 ++ 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/plugins/sift-light/dsh-entry.mjs b/plugins/sift-light/dsh-entry.mjs index 69c65b8d..5c3cf706 100644 --- a/plugins/sift-light/dsh-entry.mjs +++ b/plugins/sift-light/dsh-entry.mjs @@ -17354,9 +17354,12 @@ function classifyInlineSearch(name, args) { 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].replace(/[$]/gu, "\\$"); - if (new RegExp(`(? { const commands = [ `node -e 'const f=require("fs"); console.log(f.readFileSync("sample.txt","utf8").includes("marker"))'`, `env node --eval 'const s = require("fs").readFileSync("sample.txt", "utf8"); console.log(/marker/.test(s))'`, + `node -e 'const $s = fs.readFileSync("sample.txt"); console.log($s.includes("marker"))'`, `bash -c 'node -e "fs.readdirSync()"'`, `python3 -c 'print("marker" in open("sample.txt").read())'`, `python3 -c 'import re; print(re.search("marker", open("sample.txt").read()))'`, @@ -26,6 +27,7 @@ describe("DSH inline search detection", () => { const commands = [ `node -e 'console.log("marker".includes("mark"))'`, `node -e 'console.log(require("fs").readFileSync("sample.txt", "utf8"))'`, + `node -e 'const s = fs.readFileSync("sample.txt"); console.log(other_s.includes("marker"))'`, `python3 -c 'print("marker" in "marker")'`, `python3 -c 'print(open("sample.txt").read())'`, `python3 -c "open('/tmp/sift_policy_probe.txt','w').write(chr(10).join('line %d' % i for i in range(3)))"`,