dsh-context-toolkit 0.2.2 → 0.2.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/README.md +3 -1
- package/README.zh.md +3 -1
- package/client.js +3 -3
- package/index.js +70 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,6 +98,7 @@ await ctk_search({ queries: ['rate limit', 'auth'] }); // only the relevant exc
|
|
|
98
98
|
| `denyCurlWget` | `true` | Block curl / wget outright |
|
|
99
99
|
| `bashNudgeMinCommandBytes` | `4096` | Nudge threshold for long commands (0 disables) |
|
|
100
100
|
| `maxReadBytesBeforeAsk` | `51200` | Nudge threshold for whole-file reads (0 disables) |
|
|
101
|
+
| `nudgeFetchTools` | `true` | Nudge the host fetchers (`web_fetch` / `read_page`) too |
|
|
101
102
|
| `trustedReadBasenames` | `package.json` etc. | Filenames exempt from the read nudge |
|
|
102
103
|
| `compressNudgeTokens` | `120000` | Occupancy warning threshold (tokens) |
|
|
103
104
|
| `searchLimit` | `5` | Excerpts per query in `ctk_search` |
|
|
@@ -113,7 +114,7 @@ Tool switches: `tools.execute`, `tools.executeFile`, `tools.batchExecute`, `tool
|
|
|
113
114
|
The offline suites need no DSH runtime:
|
|
114
115
|
|
|
115
116
|
```bash
|
|
116
|
-
node smoke.mjs #
|
|
117
|
+
node smoke.mjs # 70 checks: contract, runtime discovery, tools, gate (segment matching, prefix/path normalisation, host-aligned path base, script_path and host-fetcher coverage), resource disposal, knowledge base, observability, version reporting
|
|
117
118
|
node test-client.mjs # 27 checks: browser-half contract and bilingual dictionary parity
|
|
118
119
|
node test-package.mjs # 20 checks: publish manifest and the zero-dependency constraint
|
|
119
120
|
node release-check.mjs # 27 checks: release readiness (manifest, deps, tool count, gate matching, version, SHA coverage, no hardcoded paths)
|
|
@@ -126,6 +127,7 @@ node release-check.mjs # 27 checks: release readiness (manifest, deps, tool coun
|
|
|
126
127
|
- **A single run captures at most 256 KB**; anything beyond is counted but not retained. The text handed back is additionally capped at 4000 characters, so even a 1 MB single-line output cannot enter the conversation whole.
|
|
127
128
|
- **The knowledge base deduplicates by path**: filing the same path again replaces the earlier copy.
|
|
128
129
|
- **The gate is precise, not exhaustive.** A flood tool is refused only when it is the command word of some segment — after exec prefixes (`sudo`, `exec`, …) and path/extension normalisation are stripped — and `script_path` is read up to the first 256 KB. A tool a program invokes internally (`python -c 'import wget'`) is outside its remit.
|
|
130
|
+
- **Paths resolve the way the host resolves them.** For `script_path` and for `read`'s `file_path`, a relative path is resolved against the session working directory — the same base the host uses to open the file. A gate reading that string against its own cwd would miss anything the host can still run.
|
|
129
131
|
|
|
130
132
|
## License
|
|
131
133
|
|
package/README.zh.md
CHANGED
|
@@ -98,6 +98,7 @@ await ctk_search({ queries: ['rate limit', 'auth'] }); // 只取相关片段
|
|
|
98
98
|
| `denyCurlWget` | `true` | 硬拦 curl / wget |
|
|
99
99
|
| `bashNudgeMinCommandBytes` | `4096` | 超长命令的引导阈值(0 = 关闭) |
|
|
100
100
|
| `maxReadBytesBeforeAsk` | `51200` | 大文件整读的引导阈值(0 = 关闭) |
|
|
101
|
+
| `nudgeFetchTools` | `true` | 把宿主取网页工具(`web_fetch` / `read_page`)也纳入整页引导 |
|
|
101
102
|
| `trustedReadBasenames` | `package.json` 等 | 不受整读门禁限制的文件名 |
|
|
102
103
|
| `compressNudgeTokens` | `120000` | 占用提示阈值(tokens) |
|
|
103
104
|
| `searchLimit` | `5` | `ctk_search` 每次查询的片段数上限 |
|
|
@@ -113,7 +114,7 @@ await ctk_search({ queries: ['rate limit', 'auth'] }); // 只取相关片段
|
|
|
113
114
|
离线套件不依赖 DSH 运行时:
|
|
114
115
|
|
|
115
116
|
```bash
|
|
116
|
-
node smoke.mjs #
|
|
117
|
+
node smoke.mjs # 70 项:契约、运行时探测、三工具、门禁(分段匹配 + 前缀/路径归一 + 路径基准对齐 + script_path 与取网页工具覆盖)、资源释放、知识库、观测、版本自述
|
|
117
118
|
node test-client.mjs # 27 项:浏览器半体契约与双语字典一致性
|
|
118
119
|
node test-package.mjs # 20 项:发布清单与零依赖约束
|
|
119
120
|
node release-check.mjs # 27 项:发布就绪(清单/依赖/工具数/门禁匹配/版本/SHA 覆盖/无硬编码路径)
|
|
@@ -126,6 +127,7 @@ node release-check.mjs # 27 项:发布就绪(清单/依赖/工具数/门禁
|
|
|
126
127
|
- **单次捕获上限 256 KB**,超出部分只计数不保留;回给对话的文本另有 4000 字符硬帽,因此 1 MB 的单行输出也不会整段进入上下文。
|
|
127
128
|
- **知识库按路径去重**:同一路径再次归档是替换,不是追加。
|
|
128
129
|
- **门禁精确而非穷尽**:只有当洪水工具是某个分段的「命令词」时才拒绝(会先剥掉 `sudo`/`exec` 等前缀、归一化路径与 `.exe` 等扩展名),`script_path` 最多读取前 256 KB。程序内部发起的调用(如 `python -c 'import wget'`)不在其职责内。
|
|
130
|
+
- **路径基准与宿主一致**:门禁读取 `script_path` 与 `read` 的 `file_path` 时,按**会话工作目录**解析相对路径(与宿主实际打开文件的基准相同);否则「宿主能跑、门禁看不见」就是一条现成的绕过路径。
|
|
129
131
|
|
|
130
132
|
## 许可
|
|
131
133
|
|
package/client.js
CHANGED
|
@@ -17,8 +17,8 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
let jsxs = jsxRuntime.jsxs;
|
|
18
18
|
|
|
19
19
|
//#region locales
|
|
20
|
-
const zh = {"title":"上下文工具箱","description":"在沙箱里跑代码、把大块材料归档成可检索的本地知识库、并挡住会灌满对话的抓取与整读。附一张实时上下文占用面板。","hint":"下列开关即时生效,无需重启:门禁在每次工具调用前读取。","routing":"路由门禁","routingHint":"总开关。开启后,每次工具调用执行前都会经过门禁检查。","denyCurlWget":"硬拦 curl / wget","denyCurlWgetHint":"这两个工具的设计就是把整页内容塞进对话;拦掉,改用 ctk_fetch_and_index。","bashNudgeMinCommandBytes":"长命令引导阈值(字节)","bashNudgeMinCommandBytesHint":"命令超过这个长度时提示改用 ctk_batch_execute。填 0 表示不提示。","maxReadBytesBeforeAsk":"大文件整读引导阈值(字节)","maxReadBytesBeforeAskHint":"整读超过这个大小的文件时提示改用 ctk_execute_file。填 0 表示不提示。","trustedReadBasenames":"不受整读门禁限制的文件名","trustedReadBasenamesHint":"逗号分隔;这些文件即使很大也允许整读。","compressNudgeTokens":"占用提示阈值(tokens)","compressNudgeTokensHint":"表面占用达到此值时,占用报告给出压缩建议。填 0 表示不提示。","searchLimit":"检索返回条数上限","searchLimitHint":"ctk_search 每次查询返回的片段数上限。","snippetChars":"片段最大字符数","snippetCharsHint":"ctk_search 每个片段返回的字符上限,超出部分省略。","fetchMaxBytes":"单页抓取上限(字节)","fetchMaxBytesHint":"ctk_fetch_and_index 抓取单页的最大字节数,超出部分丢弃。","storageDir":"知识库根目录","storageDirHint":"留空 = <DSH_HOME>/context-toolkit。","toolsTitle":"工具开关","toolsHint":"逐个启用或停用 ctk_* 工具。","overridden":"已覆盖","reset":"恢复默认","readOnly":"本部署的设置为只读。","unavailable":"该插件当前未加载,暂时无法配置。","save":"保存","saving":"保存中…","saveFailed":"本部署没有接受这些值,已保留供你修改。","invalidNumber":"请填数字;留空表示使用默认值。","note":"门禁开关即时生效;工具开关改动后建议重开页面确认。","ctxTitle":"上下文情况","ctxLoading":"正在读取…","ctxReload":"刷新","ctxFailed":"读取失败:","ctxNoSession":"本会话暂无可测量的投影。","ctxStale":"以下为最近一次实测快照({time}):网页请求没有会话边界,实时值请在对话中调用 ctk_context_report。","ctxTotal":"总压力 {n} tokens","ctxSurface":"表面占用 {n} tokens","ctxBudget":"提示阈值 {n} tokens","ctxOver":"已超过提示阈值,建议把大块输出改走 ctk_execute / ctk_index。","ctxUnder":"低于提示阈值,暂不需要干预。","ctxSystem":"系统提示","ctxTools":"工具 schema","ctxMessages":"会话消息","ctxEstimated":"(估算)","ctxNodes":"占用大头(前 {n} 项)","ctxLedger":"本插件节约台账","ctxLedgerLine":"门禁已拦截 {n} 次,约 {bytes} 字节未进入上下文;沙箱保留 {sandbox} 字节;已注册 {tools} 个 ctk_* 工具。","ctxKnowledge":"知识库:{docs} 文档 / {chunks} 片段({bytes} 字节)","ctxStorage":"知识库位置:{path}","tools.execute":"ctk_execute(在沙箱里跑代码,只把结论带回来)","tools.executeHint":"用于「只想要结论、不想让原始输出占满上下文」的分析型任务。","tools.executeFile":"ctk_execute_file(对某个文件跑代码,只回结论)","tools.executeFileHint":"适合读大文件后做统计或抽取,文件原文留在沙箱里。","tools.batchExecute":"ctk_batch_execute(一次跑多条命令,逐条状态)","tools.batchExecuteHint":"多条命令一次调用,只有结论回来。","tools.index":"ctk_index(把大块材料存进本地知识库)","tools.indexHint":"把长文档、日志、规范归档,之后用 ctk_search 按需取用。","tools.search":"ctk_search(按需检索知识库,取片段而非整份)","tools.searchHint":"知识库里已有的内容优先用它检索,不要整份读进来。","tools.fetchAndIndex":"ctk_fetch_and_index(抓网页并直接并入索引)","tools.fetchAndIndexHint":"抓整页网页又不让它淹没对话时用;curl/wget 会被门禁拦掉。","tools.purge":"ctk_purge(删除已索引内容,不可恢复)","tools.purgeHint":"清理知识库用;会永久删除,调用必须带 confirm:true。","tools.stats":"ctk_stats(节约台账与知识库用量)","tools.statsHint":"想知道这个插件省了多少、归档了多少时看它。","tools.doctor":"ctk_doctor(自检:运行时、临时目录、知识库往返)","tools.doctorHint":"出现异常(子进程跑不起来、索引失败)时先跑它定位。","tools.contextReport":"ctk_context_report(会话上下文占用报告)","tools.contextReportHint":"让模型可以主动查询本会话的上下文占用与构成。","restart":"需重启"};
|
|
21
|
-
const en = {"title":"Context Toolkit","description":"Run code in a sandbox, file large material into a local searchable knowledge base, and block the fetches and whole-file reads that flood a conversation. Ships a live context-occupancy panel.","hint":"These switches apply immediately, no restart: the gate is read before every tool call.","routing":"Routing gate","routingHint":"Master switch. When on, every tool call passes through the gate before running.","denyCurlWget":"Block curl / wget","denyCurlWgetHint":"Both exist to paste whole responses into the conversation; blocked here, use ctk_fetch_and_index instead.","bashNudgeMinCommandBytes":"Long-command nudge (bytes)","bashNudgeMinCommandBytesHint":"Commands longer than this get a nudge toward ctk_batch_execute. 0 disables it.","maxReadBytesBeforeAsk":"Whole-file read nudge (bytes)","maxReadBytesBeforeAskHint":"Reading a file larger than this gets a nudge toward ctk_execute_file. 0 disables it.","trustedReadBasenames":"Filenames exempt from the read nudge","trustedReadBasenamesHint":"Comma separated; these may be read whole however large they are.","compressNudgeTokens":"Occupancy warning (tokens)","compressNudgeTokensHint":"At this surface occupancy the report suggests compaction. 0 disables it.","searchLimit":"Search result limit","searchLimitHint":"How many excerpts ctk_search returns per query.","snippetChars":"Excerpt size (chars)","snippetCharsHint":"Character ceiling on each excerpt from ctk_search.","fetchMaxBytes":"Per-page fetch ceiling (bytes)","fetchMaxBytesHint":"ctk_fetch_and_index keeps at most this many bytes of each page.","storageDir":"Knowledge-base root","storageDirHint":"Empty = <DSH_HOME>/context-toolkit.","toolsTitle":"Tool switches","toolsHint":"Enable or disable individual ctk_* tools.","overridden":"overridden","reset":"reset","readOnly":"This deployment is read-only.","unavailable":"The plugin is not loaded right now; it cannot be configured.","save":"Save","saving":"Saving…","saveFailed":"This deployment did not accept those values; they are kept for you to edit.","invalidNumber":"Numbers only; blank means the default.","note":"Gate switches apply immediately; reopen the page to confirm tool-switch changes.","ctxTitle":"Context occupancy","ctxLoading":"Reading…","ctxReload":"Refresh","ctxFailed":"Read failed: ","ctxNoSession":"No measurable projection in this session yet.","ctxStale":"Snapshot from the last real measurement ({time}): a page request has no session boundary; ask ctk_context_report in the conversation for a live reading.","ctxTotal":"{n} tokens total","ctxSurface":"{n} tokens on the surface","ctxBudget":"warning at {n} tokens","ctxOver":"Above the warning threshold — route bulk output through ctk_execute / ctk_index.","ctxUnder":"Below the warning threshold; nothing to do.","ctxSystem":"System prompt","ctxTools":"Tool schemas","ctxMessages":"Conversation","ctxEstimated":" (estimated)","ctxNodes":"Biggest contributors (top {n})","ctxLedger":"This plugin's ledger","ctxLedgerLine":"The gate blocked {n} calls, about {bytes} bytes kept out of context; the sandbox kept out {sandbox} bytes; {tools} ctk_* tools registered.","ctxKnowledge":"Knowledge base: {docs} documents / {chunks} chunks ({bytes} bytes)","ctxStorage":"Knowledge base at {path}","tools.execute":"ctk_execute (run code in the sandbox, return only the answer)","tools.executeHint":"For analysis where you want the conclusion, not the raw output.","tools.executeFile":"ctk_execute_file (run code over a file, return only the answer)","tools.executeFileHint":"Read a big file, compute over it, keep its bytes in the sandbox.","tools.batchExecute":"ctk_batch_execute (many commands in one call, per-command status)","tools.batchExecuteHint":"One call for many commands; only conclusions come back.","tools.index":"ctk_index (file large material into the local knowledge base)","tools.indexHint":"Archive long docs, logs or specs, then pull sections with ctk_search.","tools.search":"ctk_search (retrieve excerpts on demand, not the whole thing)","tools.searchHint":"Prefer this for material already in the knowledge base.","tools.fetchAndIndex":"ctk_fetch_and_index (fetch a page straight into the index)","tools.fetchAndIndexHint":"When a whole page would flood the conversation; curl/wget are gated.","tools.purge":"ctk_purge (delete indexed content — permanent)","tools.purgeHint":"Clears the knowledge base; permanent, so the call must confirm:true.","tools.stats":"ctk_stats (this plugin's savings ledger and knowledge usage)","tools.statsHint":"How much this plugin saved, and how much it filed.","tools.doctor":"ctk_doctor (self-check: runtimes, scratch dir, knowledge round-trip)","tools.doctorHint":"Run it first when a child process or indexing misbehaves.","tools.contextReport":"ctk_context_report (session context occupancy report)","tools.contextReportHint":"Lets the model ask for this session context occupancy and composition.","restart":"needs a restart"};
|
|
20
|
+
const zh = {"title":"上下文工具箱","description":"在沙箱里跑代码、把大块材料归档成可检索的本地知识库、并挡住会灌满对话的抓取与整读。附一张实时上下文占用面板。","hint":"下列开关即时生效,无需重启:门禁在每次工具调用前读取。","routing":"路由门禁","routingHint":"总开关。开启后,每次工具调用执行前都会经过门禁检查。","denyCurlWget":"硬拦 curl / wget","denyCurlWgetHint":"这两个工具的设计就是把整页内容塞进对话;拦掉,改用 ctk_fetch_and_index。","bashNudgeMinCommandBytes":"长命令引导阈值(字节)","bashNudgeMinCommandBytesHint":"命令超过这个长度时提示改用 ctk_batch_execute。填 0 表示不提示。","maxReadBytesBeforeAsk":"大文件整读引导阈值(字节)","maxReadBytesBeforeAskHint":"整读超过这个大小的文件时提示改用 ctk_execute_file。填 0 表示不提示。","nudgeFetchTools":"取网页工具也纳入整页引导","nudgeFetchToolsHint":"把宿主自带的 web_fetch / read_page 也纳入引导:它们与 shell 里的 curl 取回同样一整页,却不在 shell 门禁的视野里。关闭则只拦 shell 一侧。","trustedReadBasenames":"不受整读门禁限制的文件名","trustedReadBasenamesHint":"逗号分隔;这些文件即使很大也允许整读。","compressNudgeTokens":"占用提示阈值(tokens)","compressNudgeTokensHint":"表面占用达到此值时,占用报告给出压缩建议。填 0 表示不提示。","searchLimit":"检索返回条数上限","searchLimitHint":"ctk_search 每次查询返回的片段数上限。","snippetChars":"片段最大字符数","snippetCharsHint":"ctk_search 每个片段返回的字符上限,超出部分省略。","fetchMaxBytes":"单页抓取上限(字节)","fetchMaxBytesHint":"ctk_fetch_and_index 抓取单页的最大字节数,超出部分丢弃。","storageDir":"知识库根目录","storageDirHint":"留空 = <DSH_HOME>/context-toolkit。","toolsTitle":"工具开关","toolsHint":"逐个启用或停用 ctk_* 工具。","overridden":"已覆盖","reset":"恢复默认","readOnly":"本部署的设置为只读。","unavailable":"该插件当前未加载,暂时无法配置。","save":"保存","saving":"保存中…","saveFailed":"本部署没有接受这些值,已保留供你修改。","invalidNumber":"请填数字;留空表示使用默认值。","note":"门禁开关即时生效;工具开关改动后建议重开页面确认。","ctxTitle":"上下文情况","ctxLoading":"正在读取…","ctxReload":"刷新","ctxFailed":"读取失败:","ctxNoSession":"本会话暂无可测量的投影。","ctxStale":"以下为最近一次实测快照({time}):网页请求没有会话边界,实时值请在对话中调用 ctk_context_report。","ctxTotal":"总压力 {n} tokens","ctxSurface":"表面占用 {n} tokens","ctxBudget":"提示阈值 {n} tokens","ctxOver":"已超过提示阈值,建议把大块输出改走 ctk_execute / ctk_index。","ctxUnder":"低于提示阈值,暂不需要干预。","ctxSystem":"系统提示","ctxTools":"工具 schema","ctxMessages":"会话消息","ctxEstimated":"(估算)","ctxNodes":"占用大头(前 {n} 项)","ctxLedger":"本插件节约台账","ctxLedgerLine":"门禁已拦截 {n} 次,约 {bytes} 字节未进入上下文;沙箱保留 {sandbox} 字节;已注册 {tools} 个 ctk_* 工具。","ctxKnowledge":"知识库:{docs} 文档 / {chunks} 片段({bytes} 字节)","ctxStorage":"知识库位置:{path}","tools.execute":"ctk_execute(在沙箱里跑代码,只把结论带回来)","tools.executeHint":"用于「只想要结论、不想让原始输出占满上下文」的分析型任务。","tools.executeFile":"ctk_execute_file(对某个文件跑代码,只回结论)","tools.executeFileHint":"适合读大文件后做统计或抽取,文件原文留在沙箱里。","tools.batchExecute":"ctk_batch_execute(一次跑多条命令,逐条状态)","tools.batchExecuteHint":"多条命令一次调用,只有结论回来。","tools.index":"ctk_index(把大块材料存进本地知识库)","tools.indexHint":"把长文档、日志、规范归档,之后用 ctk_search 按需取用。","tools.search":"ctk_search(按需检索知识库,取片段而非整份)","tools.searchHint":"知识库里已有的内容优先用它检索,不要整份读进来。","tools.fetchAndIndex":"ctk_fetch_and_index(抓网页并直接并入索引)","tools.fetchAndIndexHint":"抓整页网页又不让它淹没对话时用;curl/wget 会被门禁拦掉。","tools.purge":"ctk_purge(删除已索引内容,不可恢复)","tools.purgeHint":"清理知识库用;会永久删除,调用必须带 confirm:true。","tools.stats":"ctk_stats(节约台账与知识库用量)","tools.statsHint":"想知道这个插件省了多少、归档了多少时看它。","tools.doctor":"ctk_doctor(自检:运行时、临时目录、知识库往返)","tools.doctorHint":"出现异常(子进程跑不起来、索引失败)时先跑它定位。","tools.contextReport":"ctk_context_report(会话上下文占用报告)","tools.contextReportHint":"让模型可以主动查询本会话的上下文占用与构成。","restart":"需重启"};
|
|
21
|
+
const en = {"title":"Context Toolkit","description":"Run code in a sandbox, file large material into a local searchable knowledge base, and block the fetches and whole-file reads that flood a conversation. Ships a live context-occupancy panel.","hint":"These switches apply immediately, no restart: the gate is read before every tool call.","routing":"Routing gate","routingHint":"Master switch. When on, every tool call passes through the gate before running.","denyCurlWget":"Block curl / wget","denyCurlWgetHint":"Both exist to paste whole responses into the conversation; blocked here, use ctk_fetch_and_index instead.","bashNudgeMinCommandBytes":"Long-command nudge (bytes)","bashNudgeMinCommandBytesHint":"Commands longer than this get a nudge toward ctk_batch_execute. 0 disables it.","maxReadBytesBeforeAsk":"Whole-file read nudge (bytes)","maxReadBytesBeforeAskHint":"Reading a file larger than this gets a nudge toward ctk_execute_file. 0 disables it.","nudgeFetchTools":"Nudge the host fetchers too","nudgeFetchToolsHint":"Also nudge the host own web_fetch / read_page: they pull back the same whole page as a shell curl, and the shell gate never sees them. Off leaves that door unwatched.","trustedReadBasenames":"Filenames exempt from the read nudge","trustedReadBasenamesHint":"Comma separated; these may be read whole however large they are.","compressNudgeTokens":"Occupancy warning (tokens)","compressNudgeTokensHint":"At this surface occupancy the report suggests compaction. 0 disables it.","searchLimit":"Search result limit","searchLimitHint":"How many excerpts ctk_search returns per query.","snippetChars":"Excerpt size (chars)","snippetCharsHint":"Character ceiling on each excerpt from ctk_search.","fetchMaxBytes":"Per-page fetch ceiling (bytes)","fetchMaxBytesHint":"ctk_fetch_and_index keeps at most this many bytes of each page.","storageDir":"Knowledge-base root","storageDirHint":"Empty = <DSH_HOME>/context-toolkit.","toolsTitle":"Tool switches","toolsHint":"Enable or disable individual ctk_* tools.","overridden":"overridden","reset":"reset","readOnly":"This deployment is read-only.","unavailable":"The plugin is not loaded right now; it cannot be configured.","save":"Save","saving":"Saving…","saveFailed":"This deployment did not accept those values; they are kept for you to edit.","invalidNumber":"Numbers only; blank means the default.","note":"Gate switches apply immediately; reopen the page to confirm tool-switch changes.","ctxTitle":"Context occupancy","ctxLoading":"Reading…","ctxReload":"Refresh","ctxFailed":"Read failed: ","ctxNoSession":"No measurable projection in this session yet.","ctxStale":"Snapshot from the last real measurement ({time}): a page request has no session boundary; ask ctk_context_report in the conversation for a live reading.","ctxTotal":"{n} tokens total","ctxSurface":"{n} tokens on the surface","ctxBudget":"warning at {n} tokens","ctxOver":"Above the warning threshold — route bulk output through ctk_execute / ctk_index.","ctxUnder":"Below the warning threshold; nothing to do.","ctxSystem":"System prompt","ctxTools":"Tool schemas","ctxMessages":"Conversation","ctxEstimated":" (estimated)","ctxNodes":"Biggest contributors (top {n})","ctxLedger":"This plugin's ledger","ctxLedgerLine":"The gate blocked {n} calls, about {bytes} bytes kept out of context; the sandbox kept out {sandbox} bytes; {tools} ctk_* tools registered.","ctxKnowledge":"Knowledge base: {docs} documents / {chunks} chunks ({bytes} bytes)","ctxStorage":"Knowledge base at {path}","tools.execute":"ctk_execute (run code in the sandbox, return only the answer)","tools.executeHint":"For analysis where you want the conclusion, not the raw output.","tools.executeFile":"ctk_execute_file (run code over a file, return only the answer)","tools.executeFileHint":"Read a big file, compute over it, keep its bytes in the sandbox.","tools.batchExecute":"ctk_batch_execute (many commands in one call, per-command status)","tools.batchExecuteHint":"One call for many commands; only conclusions come back.","tools.index":"ctk_index (file large material into the local knowledge base)","tools.indexHint":"Archive long docs, logs or specs, then pull sections with ctk_search.","tools.search":"ctk_search (retrieve excerpts on demand, not the whole thing)","tools.searchHint":"Prefer this for material already in the knowledge base.","tools.fetchAndIndex":"ctk_fetch_and_index (fetch a page straight into the index)","tools.fetchAndIndexHint":"When a whole page would flood the conversation; curl/wget are gated.","tools.purge":"ctk_purge (delete indexed content — permanent)","tools.purgeHint":"Clears the knowledge base; permanent, so the call must confirm:true.","tools.stats":"ctk_stats (this plugin's savings ledger and knowledge usage)","tools.statsHint":"How much this plugin saved, and how much it filed.","tools.doctor":"ctk_doctor (self-check: runtimes, scratch dir, knowledge round-trip)","tools.doctorHint":"Run it first when a child process or indexing misbehaves.","tools.contextReport":"ctk_context_report (session context occupancy report)","tools.contextReportHint":"Lets the model ask for this session context occupancy and composition.","restart":"needs a restart"};
|
|
22
22
|
//#endregion
|
|
23
23
|
|
|
24
24
|
/** Fill the {name} holes in a translated sentence. */
|
|
@@ -30,7 +30,7 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
//#region fields
|
|
33
|
-
const FIELDS = [{"field":"routing","kind":"bool"},{"field":"denyCurlWget","kind":"bool"},{"field":"bashNudgeMinCommandBytes","kind":"number"},{"field":"maxReadBytesBeforeAsk","kind":"number"},{"field":"compressNudgeTokens","kind":"number"},{"field":"searchLimit","kind":"number"},{"field":"snippetChars","kind":"number"},{"field":"fetchMaxBytes","kind":"number"},{"field":"trustedReadBasenames","kind":"text"},{"field":"storageDir","kind":"text"}];
|
|
33
|
+
const FIELDS = [{"field":"routing","kind":"bool"},{"field":"denyCurlWget","kind":"bool"},{"field":"bashNudgeMinCommandBytes","kind":"number"},{"field":"maxReadBytesBeforeAsk","kind":"number"},{"field":"nudgeFetchTools","kind":"bool"},{"field":"compressNudgeTokens","kind":"number"},{"field":"searchLimit","kind":"number"},{"field":"snippetChars","kind":"number"},{"field":"fetchMaxBytes","kind":"number"},{"field":"trustedReadBasenames","kind":"text"},{"field":"storageDir","kind":"text"}];
|
|
34
34
|
const TOOL_FIELDS = ["execute","executeFile","batchExecute","index","search","fetchAndIndex","purge","stats","contextReport","doctor"];
|
|
35
35
|
const STATUS_PATH = "/_dsh/context-toolkit/status";
|
|
36
36
|
//#endregion
|
package/index.js
CHANGED
|
@@ -135,6 +135,7 @@ export const Config = z.object({
|
|
|
135
135
|
routing: z.boolean().default(true).volatile().description('是否启用 tools/pre-execute 路由门禁'),
|
|
136
136
|
denyCurlWget: z.boolean().default(true).volatile().description('硬拦 curl/wget 这类洪水工具'),
|
|
137
137
|
bashNudgeMinCommandBytes: z.number().min(0).default(4096).volatile().description('长命令软引导阈值(字节)'),
|
|
138
|
+
nudgeFetchTools: z.boolean().default(true).volatile().description('把宿主取网页工具(web_fetch/read_page)也纳入整页软引导'),
|
|
138
139
|
maxReadBytesBeforeAsk: z.number().min(0).default(51200).volatile().description('大文件整读软引导阈值(字节)'),
|
|
139
140
|
trustedReadBasenames: z.array(z.string())
|
|
140
141
|
.default(['package.json', 'tsconfig.json', 'cordis.patch.yml', 'cordis.yml'])
|
|
@@ -283,6 +284,17 @@ export function apply(ctx, rawConfig) {
|
|
|
283
284
|
/** Names whose whole purpose is to pour raw bytes into the conversation. */
|
|
284
285
|
const FLOOD_TOOLS = new Set(['curl', 'wget', 'httpie', 'http', 'aria2c'])
|
|
285
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Host tools whose entire output is a page's bytes.
|
|
289
|
+
*
|
|
290
|
+
* Refusing curl inside a shell while leaving the host's own fetcher untouched is a
|
|
291
|
+
* gate that watches one door of a two-door room: web_fetch pulls the same page into
|
|
292
|
+
* the transcript, and the plugin's stated job is keeping bulk output out of it. These
|
|
293
|
+
* are nudged, not denied — the fetch is often exactly what the caller wants, and
|
|
294
|
+
* ctk_fetch_and_index does the same job while filing the result for later.
|
|
295
|
+
*/
|
|
296
|
+
const FETCH_TOOLS = new Set(['web_fetch', 'read_page'])
|
|
297
|
+
|
|
286
298
|
/**
|
|
287
299
|
* Every tool name that runs a shell command, so the gate can read its `command`.
|
|
288
300
|
*
|
|
@@ -365,18 +377,50 @@ const SCRIPT_READ_MAX = 256 * 1024
|
|
|
365
377
|
* the same bypass the shell-name list fixed, one field over: whichever field carries
|
|
366
378
|
* the payload, the gate has to follow it.
|
|
367
379
|
*/
|
|
368
|
-
function shellTextOf(args) {
|
|
380
|
+
function shellTextOf(args, exec) {
|
|
369
381
|
const parts = []
|
|
370
382
|
if (typeof args.command === 'string') parts.push(args.command)
|
|
371
383
|
if (typeof args.script_path === 'string' && args.script_path.trim()) {
|
|
384
|
+
const path = resolveScriptPath(args.script_path, args.cwd, exec)
|
|
372
385
|
try {
|
|
373
|
-
const st = statSync(
|
|
374
|
-
if (st.isFile()) parts.push(readFileSync(
|
|
386
|
+
const st = statSync(path)
|
|
387
|
+
if (st.isFile()) parts.push(readFileSync(path, 'utf8').slice(0, SCRIPT_READ_MAX))
|
|
375
388
|
} catch { /* an unreadable script is the shell tool's error to report, not the gate's */ }
|
|
376
389
|
}
|
|
377
390
|
return parts.join('\n')
|
|
378
391
|
}
|
|
379
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Resolve a user-named path against the base the host will use to open it.
|
|
395
|
+
*
|
|
396
|
+
* Both branches that read a path the caller named — a script to run, a file to read —
|
|
397
|
+
* go through here, because getting the base wrong is invisible until it is a bypass:
|
|
398
|
+
* statSync resolves a relative path against this process's cwd (the install directory)
|
|
399
|
+
* while the host resolves it against the *session* working directory. The read failed,
|
|
400
|
+
* the catch swallowed it, and the call was waved through although the host would act on
|
|
401
|
+
* the file. Fixed for script_path, then found again unchanged in the read branch.
|
|
402
|
+
*
|
|
403
|
+
* Every branch that reads a path the caller named — a script, a file to read — must
|
|
404
|
+
* use this. The gate originally statSync'd the raw string, which resolves against this
|
|
405
|
+
* process's cwd (the install directory) while the host resolves against the session
|
|
406
|
+
* working directory; the read failed, the catch swallowed it, and the call was waved
|
|
407
|
+
* through although the host would act on the file. That mistake was fixed for
|
|
408
|
+
* script_path and then found again, unchanged, in the read branch.
|
|
409
|
+
*/
|
|
410
|
+
function resolveScriptPath(requested, requestedCwd, exec) {
|
|
411
|
+
if (isAbsolute(requested)) return requested
|
|
412
|
+
let session = null
|
|
413
|
+
try {
|
|
414
|
+
session = exec && exec.agent && exec.agent.session && exec.agent.session.header
|
|
415
|
+
? exec.agent.session.header.cwd : null
|
|
416
|
+
} catch { /* no session on this exec: fall back to the process default */ }
|
|
417
|
+
const base = session || process.cwd()
|
|
418
|
+
const cwd = typeof requestedCwd === 'string' && requestedCwd.trim()
|
|
419
|
+
? (isAbsolute(requestedCwd) ? requestedCwd : resolve(base, requestedCwd))
|
|
420
|
+
: base
|
|
421
|
+
return resolve(cwd, requested)
|
|
422
|
+
}
|
|
423
|
+
|
|
380
424
|
function floodReason(toolName, args, text) {
|
|
381
425
|
if (!SHELL_TOOL_NAMES.has(toolName)) return undefined
|
|
382
426
|
const command = text === undefined ? shellTextOf(args) : text
|
|
@@ -407,7 +451,7 @@ function registerRoutingGate(ctx, config, stats) {
|
|
|
407
451
|
|
|
408
452
|
// Read the call's text once and reuse it: a script can be 256 KB, and the deny
|
|
409
453
|
// path used to read it three times to answer three questions about one string.
|
|
410
|
-
const shellText = SHELL_TOOL_NAMES.has(toolName) ? shellTextOf(args) : ''
|
|
454
|
+
const shellText = SHELL_TOOL_NAMES.has(toolName) ? shellTextOf(args, exec) : ''
|
|
411
455
|
|
|
412
456
|
if (config.denyCurlWget !== false) {
|
|
413
457
|
const reason = floodReason(toolName, args, shellText)
|
|
@@ -434,11 +478,30 @@ function registerRoutingGate(ctx, config, stats) {
|
|
|
434
478
|
}
|
|
435
479
|
}
|
|
436
480
|
|
|
481
|
+
// The host's own fetchers: same payload as a shell curl, one field over.
|
|
482
|
+
if (FETCH_TOOLS.has(toolName) && config.nudgeFetchTools !== false) {
|
|
483
|
+
const url = typeof args.url === 'string' ? args.url : ''
|
|
484
|
+
if (url) {
|
|
485
|
+
stats.denied += 1
|
|
486
|
+
stats.deniedBytes += url.length
|
|
487
|
+
const reason = 'dsh-context-toolkit: ' + toolName + ' returns a whole page. '
|
|
488
|
+
+ 'Use ctk_fetch_and_index(url, source) and then ctk_search(queries) so the bytes are '
|
|
489
|
+
+ 'filed and only the matching sections come back. Fetch it whole only when the page '
|
|
490
|
+
+ 'is genuinely small or you must have every byte.'
|
|
491
|
+
if (hasService(ctx, 'approval')) return { kind: 'ask', reason }
|
|
492
|
+
return next()
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
437
496
|
if (toolName === 'read' && config.maxReadBytesBeforeAsk > 0) {
|
|
438
|
-
const
|
|
497
|
+
const raw = typeof args.file_path === 'string' ? args.file_path
|
|
439
498
|
: (typeof args.path === 'string' ? args.path : '')
|
|
499
|
+
// Resolve the way the host will: a relative path is session-relative, not
|
|
500
|
+
// install-directory-relative. Reading the raw string made this branch blind to
|
|
501
|
+
// every relative path, which is how a 70 KB file entered the transcript whole.
|
|
502
|
+
const p = raw ? resolveScriptPath(raw, args.cwd, exec) : ''
|
|
440
503
|
const bounded = args.offset !== undefined || args.limit !== undefined
|
|
441
|
-
if (p && !bounded && !(config.trustedReadBasenames || []).includes(baseNameOf(p))) {
|
|
504
|
+
if (p && !bounded && !(config.trustedReadBasenames || []).includes(baseNameOf(raw || p))) {
|
|
442
505
|
try {
|
|
443
506
|
const st = statSync(p)
|
|
444
507
|
if (st.isFile() && st.size > config.maxReadBytesBeforeAsk) {
|
|
@@ -459,7 +522,7 @@ function registerRoutingGate(ctx, config, stats) {
|
|
|
459
522
|
})
|
|
460
523
|
|
|
461
524
|
if (config.denyCurlWget !== false && ctx.tools && typeof ctx.tools.guard === 'function') {
|
|
462
|
-
ctx.tools.guard((exec) =>
|
|
525
|
+
ctx.tools.guard((exec) => { const a = (exec && exec.arguments) || {}; return floodReason(exec && exec.name, a, shellTextOf(a, exec)) })
|
|
463
526
|
}
|
|
464
527
|
}
|
|
465
528
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-context-toolkit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "DeepSeek Harness bundle: native ctk_* sandbox tools, a routing gate that keeps bulk output out of the transcript, and a local full-text knowledge base. No third-party runtime dependencies.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|