dsh-tool-docx 0.5.0
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/LICENSE +53 -0
- package/README.i18n.yaml +7 -0
- package/README.md +202 -0
- package/README.ru.md +202 -0
- package/README.zh.md +202 -0
- package/cordis.patch.yml +18 -0
- package/lib/fs-binary-D0seEN6R.js +63 -0
- package/lib/fs-binary-local-n8Ls-bn_.js +155 -0
- package/lib/fs-binary-local-plugin.js +24 -0
- package/lib/fs-binary-local.js +2 -0
- package/lib/fs-binary-sandbox-BobaE5Hr.js +135 -0
- package/lib/fs-binary-sandbox-plugin.js +31 -0
- package/lib/fs-binary-sandbox.js +2 -0
- package/lib/index.js +1507 -0
- package/lib/invariant.js +23 -0
- package/lib/types/caps.d.ts +14 -0
- package/lib/types/docx/extract.d.ts +18 -0
- package/lib/types/docx/generate.d.ts +17 -0
- package/lib/types/docx/zip.d.ts +17 -0
- package/lib/types/error.d.ts +20 -0
- package/lib/types/fs-binary-local-plugin.d.ts +19 -0
- package/lib/types/fs-binary-local.d.ts +45 -0
- package/lib/types/fs-binary-sandbox-plugin.d.ts +22 -0
- package/lib/types/fs-binary-sandbox.d.ts +51 -0
- package/lib/types/fs-binary.d.ts +52 -0
- package/lib/types/fsio-bytes.d.ts +27 -0
- package/lib/types/index.d.ts +32 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/markdown.d.ts +35 -0
- package/lib/types/path-contains.d.ts +21 -0
- package/lib/types/sandbox.d.ts +63 -0
- package/lib/types/tool-utils.d.ts +56 -0
- package/lib/types/tools/create.d.ts +19 -0
- package/lib/types/tools/edit.d.ts +19 -0
- package/lib/types/tools/read.d.ts +16 -0
- package/lib/types/types.d.ts +77 -0
- package/package.json +114 -0
package/README.zh.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# dsh-tool-docx
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文 | [Русский](README.ru.md)
|
|
4
|
+
|
|
5
|
+
面向模型的微软 Word(`.docx`)工具,用于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness):`docx_read` 将文档提取为 Markdown 或结构化 JSON 块,`docx_create` 从 Markdown 生成新的 `.docx`,`docx_edit` 从 Markdown 替换文档内容并保留其 title/author/created 属性。`.docx` 是 ZIP 封装的 XML 部件,因此每个工具都通过有界的 `ctx.fs.readBytes` 原语读取整个包,并通过插件的 `fsBinary` 二进制写入服务(或原生提供 `writeBytes` 的宿主 `ctx.fs`)写入包——与文本工具使用相同的原子、沙箱围栏变更,且从不替换宿主自身的文件系统。
|
|
6
|
+
|
|
7
|
+
本仓库是插件的**独立分发**。该插件是**为 DeepSeek Harness 编写的原创作品**——由本项目独立开发,最初在本地 `deepseek-harness` 检出中进行(harness 是它的运行目标),而非共享仓库中某个插件的副本。它接入 harness 的 `tools`、`fs` 与 `systemPrompt` 服务,并可针对已发布的 `@deepseek-ai/*` 包独立构建和测试,因此可以安装到任何 harness 检出中。
|
|
8
|
+
|
|
9
|
+
## 需求
|
|
10
|
+
|
|
11
|
+
- 一个文件系统 seam 提供**读取**原语 `fs.readBytes` 的 DeepSeek Harness 宿主(`0.1.0-rc.7` 线)——`readBytes` 自 `@deepseek-ai/dsh-fs@0.1.0-rc.7` 起已发布。**写入**侧(`writeBytes`)不在任何已发布的 `dsh-fs` 版本中,因此捆绑包会挂载插件的[二进制 fs 提供者](#二进制-fs-提供者):一个独立的 `fsBinary` 服务,在不触碰 `ctx.fs` 的前提下实现 `writeBytes`(沙箱宿主下带围栏)。没有任何二进制写入器——既无 `fsBinary` 服务也无原生 `ctx.fs.writeBytes` 时——`docx_read` 仍可工作,而 `docx_create`/`docx_edit` 会以带类型的 `DOCX_HOST_FS_UNSUPPORTED` 错误失败并指明修复方式。
|
|
12
|
+
- harness 提供 peer 服务(`0.1.0-rc.7` 线):`cordis`、`dsh-tools`、`dsh-fs`、`dsh-llm`、`dsh-sandbox`、`dsh-sandbox-policy`、`dsh-system-prompt`、`dsh-invariants`、`dsh-user-approval`、`dsh-session`。
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
官方安装路径是 harness 自带的插件管理器——一条命令即可将包安装到配置文件,profile 启动器会自动激活捆绑包的 [`cordis.patch.yml`](cordis.patch.yml) 层(包声明了 `dsh.bundle.patch`):
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
dsh plugin --profile web add github:BroBFG/dsh-tool-docx#v0.5.0
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`dsh plugin` 会在配置文件目录内运行 pnpm,并根据已安装状态协调 `dsh.profile.bundles`,因此无需其他任何操作——不需要 `allowBuilds` 条目(包自带构建好的 `lib/`,没有 build 脚本)、不需要手工编辑 `cordis.patch.yml`、不需要 `--patch` overlay。之后重启 harness。
|
|
23
|
+
|
|
24
|
+
- **更新**:`dsh plugin --profile web update dsh-tool-docx`,或用新 tag 重新 `add`。
|
|
25
|
+
- **移除**:`dsh plugin --profile web remove dsh-tool-docx`。
|
|
26
|
+
- **本地开发**:`dsh plugin --profile web add ../dsh-tool-docx`(相对 spec 以调用目录为锚点)或 `dsh plugin --profile web add link:../dsh-tool-docx`。
|
|
27
|
+
|
|
28
|
+
> npm 发布已计划但尚未提供;包以独立名称 `dsh-tool-docx` 发布(`dsh-tool-*` 生态惯例),不依赖 `@deepseek-ai` scope。
|
|
29
|
+
|
|
30
|
+
## 工具
|
|
31
|
+
|
|
32
|
+
| 工具 | 用途 |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `docx_read(file_path, format?, max_chars?)` | 将文档正文提取为 Markdown(默认)或结构化 JSON 块以及 `docProps`。发出 `fs/observed`。 |
|
|
35
|
+
| `docx_create(file_path, markdown, title?, author?)` | 从 Markdown 生成新的 `.docx`。受 `createIfAbsent` 保护:绝不盲目覆盖现有文件。 |
|
|
36
|
+
| `docx_edit(file_path, markdown)` | 读取当前文档(校验其为 docx),保留 `docProps`,从完整 Markdown 重新生成正文,并以版本守卫写回(并发变更时返回 `DOCX_STALE`)。 |
|
|
37
|
+
|
|
38
|
+
三个工具都针对调用代理的会话 cwd 解析相对路径,在变更前派发 `fs/write-intent` 瀑布(观察策略插件可以提供自己的 intent),并在完成时记录 `fs/observed`——因此沙箱围栏、升权字段和写前读取策略对 docx 变更的作用与对 `write`/`edit` 完全一致。
|
|
39
|
+
|
|
40
|
+
## 配置
|
|
41
|
+
|
|
42
|
+
| 字段 | 默认值 | 含义 |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `maxDocxBytes` | 64 MiB | 整个 `.docx` 文件的字节上限(读取 + ZIP 展开)。 |
|
|
45
|
+
| `maxMarkdownChars` | 1 000 000 | create/edit 的 Markdown 输入字符上限。 |
|
|
46
|
+
| `maxReadChars` | 200 000 | `docx_read` 返回的 Markdown 字符上限。 |
|
|
47
|
+
|
|
48
|
+
## 二进制 fs 提供者
|
|
49
|
+
|
|
50
|
+
基础包的 `fs-sandbox` 行继续**原样**提供 `ctx.fs`。捆绑包在它旁边添加 docx 工具和一个二进制提供者,后者将二进制 `writeBytes` 原语注册为**独立的 `fsBinary` 服务**——宿主文件系统永远不会被替换,因此该插件不可能破坏 harness 启动;最坏情况下,没有提供者时变更类工具会报告 `DOCX_HOST_FS_UNSUPPORTED`:
|
|
51
|
+
|
|
52
|
+
- **`dsh-tool-docx/fs-binary-sandbox-plugin`**(捆绑包挂载;沙箱宿主推荐)——注册的 `fsBinary.writeBytes` 与每次沙箱变更走**相同的按调用策略围栏**:`workspace-write` 包含检查、`read-only` 拒绝、`danger-full-access` 放行、拒绝时 `FS_SANDBOX_DENIED`(在工具层映射为 `DOCX_SANDBOX_DENIED`)。
|
|
53
|
+
- **`dsh-tool-docx/fs-binary-local-plugin`**——注册**无围栏**的 `fsBinary.writeBytes`,用于最小环境(测试、headless 脚本)或宿主已在提供者之上围栏的场景。要改用它而不是沙箱版本,可在配置文件的 `cordis.patch.yml` 中覆盖捆绑包行:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
- id: fs-binary-sandbox
|
|
57
|
+
disabled: true
|
|
58
|
+
- insert:
|
|
59
|
+
- id: fs-binary-local
|
|
60
|
+
name: dsh-tool-docx/fs-binary-local-plugin
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
两者都使用与 harness seam 相同的 probe → intent 守卫(`createIfAbsent` / `replaceIfVersion`)→ 原子发布流程:私有 owner-only staging 目录、fsync、然后原子发布(`createIfAbsent` 用硬链接 no-replace 原语),并按目标串行化。第一版省略 harness 的 Win32 DACL 保留仪式——替换文件继承暂存临时文件的所有者 ACL。
|
|
64
|
+
|
|
65
|
+
对于想刻意把完整后端**挂载为 `ctx.fs`**(替换 `fs-sandbox`)的宿主,包还提供提供者类 `dsh-tool-docx/fs-binary-sandbox` 与 `dsh-tool-docx/fs-binary-local`;替换行的配方仍然适用:
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
- id: fs-sandbox
|
|
69
|
+
disabled: true
|
|
70
|
+
- insert:
|
|
71
|
+
- id: fs-binary-sandbox
|
|
72
|
+
name: dsh-tool-docx/fs-binary-sandbox
|
|
73
|
+
- id: tool-docx
|
|
74
|
+
name: dsh-tool-docx
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 设计说明
|
|
78
|
+
|
|
79
|
+
- **提取**(`src/docx/extract.ts`)用 `fast-xml-parser` 遍历 `word/document.xml`:标题(`Heading1`–`Heading6`、`Title`)、粗体/斜体/删除线 run、通过 `word/numbering.xml` 的嵌套列表(bullet 与 decimal)、管道表格(合并单元格近似)、通过 `word/_rels/document.xml.rels` 的外部超链接,以及按计数占位的嵌入图片。不支持的构造降级为警告,绝不会失败。
|
|
80
|
+
- **生成**(`src/docx/generate.ts`)用 `docx` 库渲染块模型:ATX 标题、带样式的行内 run、9 级 bullet/数字编号、管道表格和 `[text](url)` 外部超链接。`parseMarkdown`(src/markdown.ts)接受提取器输出的子集,因此读 → 改 → 写往返是稳定的。
|
|
81
|
+
- **上限在 seam 层而非工具层强制**——整文件字节上限流入 `ctx.fs.readBytes`(`FS_TOO_LARGE` 映射为 `DOCX_TOO_LARGE`),ZIP 读取器对未压缩总量施加同一上限,因此压缩炸弹无法无界展开。
|
|
82
|
+
- **沙箱对等**——`src/sandbox.ts` 镜像 `dsh-tool-fs` 的升权 API(仅在受限后端下暴露 `sandbox_permissions`/`justification`,拒绝标记映射);提取共享控制器属于延后工作(见下文)。
|
|
83
|
+
- **宿主文件系统契约**——`src/fs-binary.ts` 声明工具所需的二进制契约,并在调用时解析写入器:优先使用已挂载的 `fsBinary` 服务,否则使用原生提供 `writeBytes` 的宿主 `ctx.fs`。没有任何二进制写入器时,它抛出 `DOCX_HOST_FS_UNSUPPORTED` 并指明修复方式,而不是晦涩的 `fs.writeBytes is not a function`;`docx_read` 只需已发布的 `ctx.fs.readBytes`。
|
|
84
|
+
|
|
85
|
+
## 模型体验
|
|
86
|
+
|
|
87
|
+
### 系统提示词
|
|
88
|
+
|
|
89
|
+
#### 模型看到什么
|
|
90
|
+
|
|
91
|
+
插件应用时注册一次下面的 `tool:docx-read` 段落:
|
|
92
|
+
|
|
93
|
+
##### docx 指南段落
|
|
94
|
+
|
|
95
|
+
```markdown
|
|
96
|
+
MS Word .docx files are binary (ZIP+XML) and the read tool cannot read them. Use docx_read to extract a document as Markdown (default) or structured JSON blocks, docx_create to generate a new .docx from Markdown, and docx_edit to replace a document's content from Markdown while preserving its title/author/created properties. Legacy .doc is not supported — convert it to .docx first.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Token 影响
|
|
100
|
+
|
|
101
|
+
插件挂载期间每次请求有固定的指南成本;作用域工具限制不影响该段落。
|
|
102
|
+
|
|
103
|
+
#### KV Cache 影响
|
|
104
|
+
|
|
105
|
+
在指南文本不变时前缀稳定。插件生命周期或文本变更可能从首个变更的提示词段开始使复用失效。
|
|
106
|
+
|
|
107
|
+
### 工具 schema
|
|
108
|
+
|
|
109
|
+
#### 模型看到什么
|
|
110
|
+
|
|
111
|
+
生成的 `docx_read`、`docx_create` 和 `docx_edit` schema——参数与规范输出见上方[工具](#工具)表格汇总。字节/字符上限是部署设置而非模型参数;升权字段只在受限文件系统后端下出现。
|
|
112
|
+
|
|
113
|
+
#### Token 影响
|
|
114
|
+
|
|
115
|
+
每个挂载工具每次请求有固定的 schema 成本;配置禁用会同时移除 schema 和指南,而作用域限制只移除 schema。
|
|
116
|
+
|
|
117
|
+
#### KV Cache 影响
|
|
118
|
+
|
|
119
|
+
在定义与可见性不变时前缀稳定。配置启用、插件生命周期或作用域限制可能从首个变更的 schema token 开始使复用失效。
|
|
120
|
+
|
|
121
|
+
### 读取结果
|
|
122
|
+
|
|
123
|
+
#### 模型看到什么
|
|
124
|
+
|
|
125
|
+
成功的 `docx_read` 渲染提取的 Markdown(或美化后的 JSON 块)。截断时追加 `\n… (truncated)`;失败是带类型的消息,例如 `file not found: <path>`、`the document is encrypted (password-protected); decryption is not supported`,或旧版提示 `legacy .doc format is not supported — convert the document to .docx first`。
|
|
126
|
+
|
|
127
|
+
#### Token 影响
|
|
128
|
+
|
|
129
|
+
数据相关结果由 `maxReadChars`(或调用的 `max_chars`)限制,并在压缩前重复发送。
|
|
130
|
+
|
|
131
|
+
#### KV Cache 影响
|
|
132
|
+
|
|
133
|
+
仅追加;新可见内容跟随可复用请求前缀,不会使既有 KV-cache 条目失效。
|
|
134
|
+
|
|
135
|
+
### 创建/编辑结果
|
|
136
|
+
|
|
137
|
+
#### 模型看到什么
|
|
138
|
+
|
|
139
|
+
成功的创建/编辑渲染一个简短的 `<path>`/`<type>docx</type>` 信封并附带字节大小——绝不返回文档正文。近似警告(图片、合并单元格、代码块)携带在规范 `warnings` 数组中并以纯文本渲染。
|
|
140
|
+
|
|
141
|
+
#### Token 影响
|
|
142
|
+
|
|
143
|
+
只有保留的调用参数(含完整 Markdown 输入)和简短结果增加 token;生成的包字节绝不进入会话日志。
|
|
144
|
+
|
|
145
|
+
#### KV Cache 影响
|
|
146
|
+
|
|
147
|
+
仅追加;新可见内容跟随可复用请求前缀,不会使既有 KV-cache 条目失效。
|
|
148
|
+
|
|
149
|
+
### 参数错误
|
|
150
|
+
|
|
151
|
+
#### 模型看到什么
|
|
152
|
+
|
|
153
|
+
空 `file_path` 变成 `Error: file_path must be a non-empty string`;markdown 超出输入上限变成 `Error: markdown exceeds the <n>-character limit`。
|
|
154
|
+
|
|
155
|
+
#### Token 影响
|
|
156
|
+
|
|
157
|
+
只有失败的调用增加这些保留 token。
|
|
158
|
+
|
|
159
|
+
#### KV Cache 影响
|
|
160
|
+
|
|
161
|
+
仅追加;新可见内容跟随可复用请求前缀,不会使既有 KV-cache 条目失效。
|
|
162
|
+
|
|
163
|
+
## 已知限制与延后工作
|
|
164
|
+
|
|
165
|
+
- **不支持旧版 `.doc`(OLE)**——二进制 OLE 需要 LibreOffice 或 Word COM 转换;工具返回 `DOCX_LEGACY_DOC` 并提示先转换为 `.docx`。
|
|
166
|
+
- **不提取或嵌入图片**——`docx_read` 统计图片并输出占位符;`docx_create`/`docx_edit` 丢弃图片语法并给出警告。提取图片字节并在生成时嵌入属于延后工作。
|
|
167
|
+
- **往返会重新生成文档**——样式、页面设置、页眉页脚和分节符不会被保留;编辑会用默认样式重建正文,只保留 title/author/created。版式保真不是往返的目标。
|
|
168
|
+
- **合并单元格近似**——`gridSpan`/`vMerge` 降级为普通管道表格单元格并给出警告;脚注、尾注、文本框和分页符会被丢弃(含警告)。
|
|
169
|
+
- **沙箱控制器与 `dsh-tool-fs` 重复**——提取共享的 `FsSandboxController` 属于延后工作;在此之前两份拷贝必须保持同步。
|
|
170
|
+
- **Markdown 输入子集**——引用块、水平线、嵌套围栏和图片无法表示;它们降级为段落并给出警告(围栏代码变成代码样式段落)。
|
|
171
|
+
|
|
172
|
+
## 开发
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
pnpm install
|
|
176
|
+
pnpm typecheck # tsc 检查 src/
|
|
177
|
+
pnpm build # tsc → lib/types + tsdown → lib/index.js, lib/invariant.js
|
|
178
|
+
pnpm test # vitest:单元转换测试 + 基于假 fs 的消费者测试
|
|
179
|
+
pnpm pack # 生成 npm tarball(files:lib/index.js、lib/invariant.js、lib/types/**/*.d.ts)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
结构:
|
|
183
|
+
|
|
184
|
+
- `src/docx/`——ZIP/XML 提取与 `docx` 库生成;
|
|
185
|
+
- `src/tools/`——三个工具注册;
|
|
186
|
+
- `src/fs-binary.ts`——二进制文件系统契约与调用时写入器解析(`fsBinary` 服务或原生 `ctx.fs.writeBytes`);
|
|
187
|
+
- `src/fs-binary-local.ts`、`src/fs-binary-sandbox.ts`、`src/fsio-bytes.ts`、`src/path-contains.ts`——二进制 fs 提供者类以及原子写入器与包含性辅助;
|
|
188
|
+
- `src/fs-binary-sandbox-plugin.ts`、`src/fs-binary-local-plugin.ts`——注册 `fsBinary` 服务的命名空间插件(捆绑包的默认挂载);
|
|
189
|
+
- `tests/`——转换往返测试、提供者测试与针对已发布 `ToolRuntime` 服务的消费者测试(自 `dsh-tools@0.1.0-rc.7` 起导出)。
|
|
190
|
+
|
|
191
|
+
## 与 deepseek-harness 的关系
|
|
192
|
+
|
|
193
|
+
该插件是**为 DeepSeek Harness 编写的原创独立项目**——它接入 harness 的公开服务(`tools`、`fs`、`systemPrompt`),并在本地 `deepseek-harness` 检出中开发以对 harness 进行测试。它不是共享 `deepseek-harness` 仓库中某个插件的副本,也不属于该仓库;本仓库是规范的发行渠道。两点实现说明:
|
|
194
|
+
|
|
195
|
+
1. `src/fs-binary.ts`(宿主契约 + 写入器解析)——二进制 `readBytes`/`writeBytes` 契约是插件设计的一部分。`readBytes` 自 `0.1.0-rc.7` 起已发布在 harness 的 `dsh-fs` 发布线中;`writeBytes` 没有,因此[二进制 fs 提供者](#二进制-fs-提供者)以独立的 `fsBinary` 服务提供它,而不是修补宿主;
|
|
196
|
+
2. `tests/` 针对已发布的 `ToolRuntime` 服务运行(自 `dsh-tools@0.1.0-rc.7` 起导出),因此消费者测试走真实的注册表流水线而非本地替身。
|
|
197
|
+
|
|
198
|
+
该插件完全在本仓库中针对已发布的 `@deepseek-ai/*` 包进行开发与维护;harness 检出只是用于集成验证的运行目标,不携带本插件的副本。
|
|
199
|
+
|
|
200
|
+
## 许可证
|
|
201
|
+
|
|
202
|
+
MIT © 2026 BroBFG。`src/sandbox.ts` 的部分内容、`src/fsio-bytes.ts` 中的原子写入模式与 `src/path-contains.ts` 中的包含性逻辑衍生自 [deepseek-harness](https://github.com/deepseek-ai/deepseek-harness)(MIT,Copyright (c) 2026 DeepSeek)——见 [LICENSE](LICENSE)。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# dsh-tool-docx bundle patch: mounts the plugin into a profile layer stack.
|
|
2
|
+
# Applied as a bundle layer (after dsh-base and the mode bundle) when the
|
|
3
|
+
# package is installed into a profile with `dsh plugin --profile <name> add`.
|
|
4
|
+
#
|
|
5
|
+
# The base bundle's `fs-sandbox` row keeps providing `ctx.fs` UNCHANGED. This
|
|
6
|
+
# plugin adds two rows next to it: `fs-binary-sandbox` registers the binary
|
|
7
|
+
# `writeBytes` primitive as a SEPARATE `fsBinary` service (fenced by the same
|
|
8
|
+
# per-call policy as every sandbox mutation), and `tool-docx` mounts the docx
|
|
9
|
+
# tool suite, which resolves the binary writer from `fsBinary` (or a host
|
|
10
|
+
# `ctx.fs` that natively provides `writeBytes`) at call time. The host
|
|
11
|
+
# filesystem is never replaced, so this bundle cannot break the harness boot —
|
|
12
|
+
# at worst, without the provider, the mutating docx tools report
|
|
13
|
+
# `DOCX_HOST_FS_UNSUPPORTED`.
|
|
14
|
+
- insert:
|
|
15
|
+
- id: fs-binary-sandbox
|
|
16
|
+
name: dsh-tool-docx/fs-binary-sandbox-plugin
|
|
17
|
+
- id: tool-docx
|
|
18
|
+
name: dsh-tool-docx
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { HarnessError } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
3
|
+
//#region lib/types/error.js
|
|
4
|
+
/**
|
|
5
|
+
* Typed error vocabulary for the docx tools: a stable machine-routable code
|
|
6
|
+
* distinct from the human-readable message, plus the mapping from the
|
|
7
|
+
* filesystem seam's `FsError` codes.
|
|
8
|
+
* @module dsh-tool-docx/error
|
|
9
|
+
*/
|
|
10
|
+
/** Typed docx failure. Extends {@link HarnessError} for a stable code and `cause` chaining. */
|
|
11
|
+
var DocxError = class extends HarnessError {
|
|
12
|
+
code;
|
|
13
|
+
constructor(message, code, options) {
|
|
14
|
+
super(message, code, options);
|
|
15
|
+
this.code = code;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/** Map a filesystem-seam failure to the docx vocabulary; other errors pass through.
|
|
19
|
+
* @param error - the thrown filesystem error (or any other value).
|
|
20
|
+
* @returns the mapped `DocxError`, or the original value when it is not an `FsError`.
|
|
21
|
+
*/
|
|
22
|
+
function mapFsError(error) {
|
|
23
|
+
if (!(error instanceof FsError)) return error;
|
|
24
|
+
switch (error.code) {
|
|
25
|
+
case "FS_NOT_FOUND": return new DocxError(error.message, "DOCX_NOT_FOUND", { cause: error });
|
|
26
|
+
case "FS_NOT_REGULAR_FILE": return new DocxError(error.message, "DOCX_NOT_REGULAR_FILE", { cause: error });
|
|
27
|
+
case "FS_TOO_LARGE": return new DocxError(error.message, "DOCX_TOO_LARGE", { cause: error });
|
|
28
|
+
case "FS_NOT_OBSERVED": return new DocxError(error.message, "DOCX_EXISTS", { cause: error });
|
|
29
|
+
case "FS_STALE_VERSION": return new DocxError(error.message, "DOCX_STALE", { cause: error });
|
|
30
|
+
case "FS_ABORTED": return error;
|
|
31
|
+
default: return new DocxError(error.message, "DOCX_WRITE_ERROR", { cause: error });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region lib/types/fs-binary.js
|
|
36
|
+
/** The service name the plugin's binary providers register under. */
|
|
37
|
+
const FS_BINARY_SERVICE = "fsBinary";
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the binary writer for this context: the plugin's `fsBinary` service
|
|
40
|
+
* when mounted, else a host `ctx.fs` that natively provides `writeBytes`.
|
|
41
|
+
* @param ctx - the plugin context; `fsBinary` is resolved optionally.
|
|
42
|
+
* @returns the bound writer, or `undefined` when no binary writer is mounted.
|
|
43
|
+
*/
|
|
44
|
+
function resolveWriteBytes(ctx) {
|
|
45
|
+
const fsBinary = ctx.get(FS_BINARY_SERVICE);
|
|
46
|
+
if (fsBinary?.writeBytes !== void 0) return fsBinary.writeBytes.bind(fsBinary);
|
|
47
|
+
const fs = ctx.fs;
|
|
48
|
+
if (typeof fs.writeBytes === "function") return fs.writeBytes.bind(fs);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the binary writer or fail with the typed host-requirement error.
|
|
52
|
+
* @param ctx - the plugin context.
|
|
53
|
+
* @returns the bound writer.
|
|
54
|
+
* @throws `DOCX_HOST_FS_UNSUPPORTED` when neither the `fsBinary` service nor a
|
|
55
|
+
* native `ctx.fs.writeBytes` is available.
|
|
56
|
+
*/
|
|
57
|
+
function requireWriteBytes(ctx) {
|
|
58
|
+
const writeBytes = resolveWriteBytes(ctx);
|
|
59
|
+
if (writeBytes === void 0) throw new DocxError(`the host filesystem seam lacks the binary writeBytes primitive — install the dsh-tool-docx bundle (dsh plugin --profile web add dsh-tool-docx), which mounts the ${FS_BINARY_SERVICE} provider, or use a deepseek-harness build that includes fs.writeBytes natively`, "DOCX_HOST_FS_UNSUPPORTED");
|
|
60
|
+
return writeBytes;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { mapFsError as i, requireWriteBytes as n, DocxError as r, FS_BINARY_SERVICE as t };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
2
|
+
import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { chmod, link, mkdir, open, rename, rm } from "node:fs/promises";
|
|
6
|
+
//#region lib/types/fsio-bytes.js
|
|
7
|
+
/**
|
|
8
|
+
* Minimal atomic binary write for the plugin's local filesystem provider.
|
|
9
|
+
* Mirrors the deepseek-harness `fs-local` `writeFileAtomic` semantics — a
|
|
10
|
+
* private owner-only staging directory, an exclusive temp file, fsync, then an
|
|
11
|
+
* atomic publish; a `createIfAbsent` publish uses a hard-link no-replace
|
|
12
|
+
* primitive so a concurrent creator wins (`FS_NOT_OBSERVED`) — for a
|
|
13
|
+
* `Uint8Array` payload. The Win32 DACL-preservation ceremony of the harness
|
|
14
|
+
* original is intentionally omitted in this first version (a replacement
|
|
15
|
+
* inherits the temp file's owner-only ACL).
|
|
16
|
+
* @module dsh-tool-docx/fsio-bytes
|
|
17
|
+
*/
|
|
18
|
+
function throwIfAborted(signal, verb) {
|
|
19
|
+
if (signal?.aborted) throw new FsError(`${verb} aborted`, "FS_ABORTED");
|
|
20
|
+
}
|
|
21
|
+
function isAbortError(error) {
|
|
22
|
+
return error instanceof Error && error.name === "AbortError";
|
|
23
|
+
}
|
|
24
|
+
function isEEXIST(error) {
|
|
25
|
+
return error instanceof Error && error.code === "EEXIST";
|
|
26
|
+
}
|
|
27
|
+
function isENOENT(error) {
|
|
28
|
+
return error instanceof Error && error.code === "ENOENT";
|
|
29
|
+
}
|
|
30
|
+
function errorMessage(error) {
|
|
31
|
+
return error instanceof Error ? error.message : String(error);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Atomically write raw bytes to `absolutePath`: stage an owner-only temp file
|
|
35
|
+
* in a private sibling directory, fsync, then publish. With `createIfAbsent`,
|
|
36
|
+
* publish uses a hard link that fails if the target appeared concurrently
|
|
37
|
+
* (`FS_NOT_OBSERVED`); otherwise the temp is renamed over the target.
|
|
38
|
+
* @param absolutePath - destination path (typically a target key); missing
|
|
39
|
+
* parent directories are created.
|
|
40
|
+
* @param data - the raw bytes to write.
|
|
41
|
+
* @param signal - cancellation checked before and during the write.
|
|
42
|
+
* @param createIfAbsent - when provided, publish with the no-replace primitive
|
|
43
|
+
* and reject a concurrent creator with `FS_NOT_OBSERVED`.
|
|
44
|
+
*/
|
|
45
|
+
async function writeFileAtomicBytes(absolutePath, data, signal, createIfAbsent) {
|
|
46
|
+
throwIfAborted(signal, "write");
|
|
47
|
+
const directory = dirname(absolutePath);
|
|
48
|
+
await mkdir(directory, { recursive: true });
|
|
49
|
+
throwIfAborted(signal, "write");
|
|
50
|
+
const stagingDir = join(directory, `.${basename(absolutePath)}.${process.pid}.${randomUUID()}.tmpdir`);
|
|
51
|
+
const tempPath = join(stagingDir, `${basename(absolutePath)}.tmp`);
|
|
52
|
+
let handle;
|
|
53
|
+
let stagingCreated = false;
|
|
54
|
+
try {
|
|
55
|
+
await mkdir(stagingDir, { mode: 448 });
|
|
56
|
+
stagingCreated = true;
|
|
57
|
+
await chmod(stagingDir, 448);
|
|
58
|
+
handle = await open(tempPath, "wx", 384);
|
|
59
|
+
await handle.chmod(384);
|
|
60
|
+
await handle.writeFile(Buffer.from(data), { ...signal ? { signal } : {} });
|
|
61
|
+
await handle.sync();
|
|
62
|
+
await handle.close();
|
|
63
|
+
handle = void 0;
|
|
64
|
+
throwIfAborted(signal, "write");
|
|
65
|
+
if (createIfAbsent !== void 0) try {
|
|
66
|
+
await link(tempPath, absolutePath);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (isEEXIST(error) || isENOENT(error)) throw new FsError(`cannot overwrite existing "${createIfAbsent.displayPath}" without reading it first`, "FS_NOT_OBSERVED");
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
else await rename(tempPath, absolutePath);
|
|
72
|
+
try {
|
|
73
|
+
await rm(stagingDir, {
|
|
74
|
+
recursive: true,
|
|
75
|
+
force: true
|
|
76
|
+
});
|
|
77
|
+
} catch (_cleanupFailure) {}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
let failure = isAbortError(error) ? new FsError("write aborted", "FS_ABORTED") : error;
|
|
80
|
+
if (handle) try {
|
|
81
|
+
await handle.close();
|
|
82
|
+
} catch (closeError) {
|
|
83
|
+
failure = new FsError(`write failed (${errorMessage(failure)}) and temp close failed (${errorMessage(closeError)})`, "FS_NOT_FOUND", { cause: failure });
|
|
84
|
+
}
|
|
85
|
+
if (!stagingCreated) throw failure;
|
|
86
|
+
try {
|
|
87
|
+
await rm(stagingDir, {
|
|
88
|
+
recursive: true,
|
|
89
|
+
force: true
|
|
90
|
+
});
|
|
91
|
+
} catch {}
|
|
92
|
+
throw failure;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region lib/types/fs-binary-local.js
|
|
97
|
+
/**
|
|
98
|
+
* The plugin's own distribution of the harness's binary filesystem seam: a
|
|
99
|
+
* local `ctx.fs` provider that adds the `writeBytes` primitive to the
|
|
100
|
+
* published `@deepseek-ai/dsh-fs-local` backend. Mount this plugin as `ctx.fs`
|
|
101
|
+
* (in place of `dsh-fs-local`) on a host whose filesystem seam lacks
|
|
102
|
+
* `writeBytes`; the docx tools then run unchanged, with the same
|
|
103
|
+
* probe → intent-guard → atomic-publish flow the harness seam provides.
|
|
104
|
+
*
|
|
105
|
+
* For a sandboxed host (a `SandboxedFileSystem` mounted as `ctx.fs`), mount
|
|
106
|
+
* `dsh-tool-docx/fs-binary-sandbox` instead — it preserves the policy fence.
|
|
107
|
+
* @module dsh-tool-docx/fs-binary-local
|
|
108
|
+
*/
|
|
109
|
+
/**
|
|
110
|
+
* The probe → intent-guard → atomic-publish body shared by the plugin's binary
|
|
111
|
+
* fs providers: stat the target, enforce the version/absence intent
|
|
112
|
+
* (`FS_STALE_VERSION` / `FS_NOT_OBSERVED`), publish atomically, and report the
|
|
113
|
+
* fresh version.
|
|
114
|
+
* @param provider - the filesystem whose `stat` observes the target (the
|
|
115
|
+
* provider itself, after any sandbox fence has run).
|
|
116
|
+
* @param target - the (possibly fence-checked) target to write.
|
|
117
|
+
* @param data - the raw bytes to write.
|
|
118
|
+
* @param expected - the write intent guarding the write; omit for unconditional.
|
|
119
|
+
* @param signal - cancellation.
|
|
120
|
+
* @returns the create/update outcome with the fresh version.
|
|
121
|
+
*/
|
|
122
|
+
async function performByteWrite(provider, target, data, expected, signal) {
|
|
123
|
+
const existing = await provider.stat(target, signal);
|
|
124
|
+
if (existing !== void 0 && existing.type !== "file") throw new FsError(`cannot write "${target.displayPath}": not a regular file`, "FS_NOT_REGULAR_FILE");
|
|
125
|
+
if (expected?.kind === "replaceIfVersion") {
|
|
126
|
+
if (existing === void 0 || existing.version !== expected.version) throw new FsError(`cannot write "${target.displayPath}": file changed since it was read`, "FS_STALE_VERSION");
|
|
127
|
+
} else if (expected?.kind === "createIfAbsent" && existing !== void 0) throw new FsError(`cannot overwrite existing "${target.displayPath}" without reading it first`, "FS_NOT_OBSERVED");
|
|
128
|
+
await writeFileAtomicBytes(String(target.targetKey), data, signal, expected?.kind === "createIfAbsent" ? { displayPath: target.displayPath } : void 0);
|
|
129
|
+
const fresh = await provider.stat(target, signal);
|
|
130
|
+
if (fresh === void 0) throw new FsError(`cannot stat "${target.displayPath}" after write`, "FS_IO_ERROR");
|
|
131
|
+
return {
|
|
132
|
+
operation: existing === void 0 ? "create" : "update",
|
|
133
|
+
version: fresh.version
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The local filesystem backend with the binary write primitive. Inherits the
|
|
138
|
+
* full published `LocalFileSystem` contract (text reads/writes, `readBytes`,
|
|
139
|
+
* intent handling) and adds `writeBytes`. Per-targetKey operations are
|
|
140
|
+
* serialized with a tail promise, mirroring the backend's own lock discipline.
|
|
141
|
+
*/
|
|
142
|
+
var DocxBinaryFileSystem = class extends LocalFileSystem {
|
|
143
|
+
byteLocks = /* @__PURE__ */ new Map();
|
|
144
|
+
withByteLock(key, op) {
|
|
145
|
+
const next = (this.byteLocks.get(key) ?? Promise.resolve()).then(op, op);
|
|
146
|
+
this.byteLocks.set(key, next.then(() => void 0, () => void 0));
|
|
147
|
+
return next;
|
|
148
|
+
}
|
|
149
|
+
/** Write raw bytes with an optional version/absence guard. */
|
|
150
|
+
async writeBytes(target, data, expected, signal) {
|
|
151
|
+
return this.withByteLock(String(target.targetKey), () => performByteWrite(this, target, data, expected, signal));
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
//#endregion
|
|
155
|
+
export { performByteWrite as n, DocxBinaryFileSystem as t };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { t as FS_BINARY_SERVICE } from "./fs-binary-D0seEN6R.js";
|
|
2
|
+
import { n as performByteWrite } from "./fs-binary-local-n8Ls-bn_.js";
|
|
3
|
+
//#region lib/types/fs-binary-local-plugin.js
|
|
4
|
+
/**
|
|
5
|
+
* `fs-binary-local-plugin` — the mount for minimal hosts without a sandbox
|
|
6
|
+
* policy: a namespace plugin that registers the binary write primitive as the
|
|
7
|
+
* SEPARATE `fsBinary` service, unfenced, over the host's own `ctx.fs`. The
|
|
8
|
+
* host filesystem is left untouched. Sandboxed hosts should mount
|
|
9
|
+
* `fs-binary-sandbox-plugin` instead, which applies the policy fence.
|
|
10
|
+
* @module dsh-tool-docx/fs-binary-local-plugin
|
|
11
|
+
*/
|
|
12
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
13
|
+
const name = "fs-binary-local";
|
|
14
|
+
/** Services required: the host filesystem (reads/stat/resolve and the write body). */
|
|
15
|
+
const inject = ["fs"];
|
|
16
|
+
/**
|
|
17
|
+
* Register the `fsBinary` service: unfenced binary writes over `ctx.fs`.
|
|
18
|
+
* @param ctx - the plugin context; execution uses its `fs` service.
|
|
19
|
+
*/
|
|
20
|
+
function apply(ctx) {
|
|
21
|
+
ctx.provide(FS_BINARY_SERVICE, { writeBytes: (target, data, expected, signal) => performByteWrite(ctx.fs, target, data, expected, signal) });
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { n as performByteWrite } from "./fs-binary-local-n8Ls-bn_.js";
|
|
2
|
+
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
3
|
+
import { writableRoots } from "@deepseek-ai/dsh-sandbox";
|
|
4
|
+
import { dirname, sep } from "node:path";
|
|
5
|
+
import { stat } from "node:fs/promises";
|
|
6
|
+
import { SandboxedFileSystem } from "@deepseek-ai/dsh-fs-sandbox";
|
|
7
|
+
//#region lib/types/path-contains.js
|
|
8
|
+
/**
|
|
9
|
+
* Path-containment mechanics for the plugin's sandboxed filesystem provider —
|
|
10
|
+
* ported from the deepseek-harness `fs-sandbox` package (MIT, see LICENSE):
|
|
11
|
+
* the lexical fast path handles canonical spellings, and filesystem identity
|
|
12
|
+
* supplies the conservative fallback for alias-equivalent roots (Windows 8.3
|
|
13
|
+
* names, casing).
|
|
14
|
+
* @module dsh-tool-docx/path-contains
|
|
15
|
+
*/
|
|
16
|
+
const MISSING_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
|
|
17
|
+
function isMissing(error) {
|
|
18
|
+
const code = error.code;
|
|
19
|
+
return MISSING_CODES.has(code);
|
|
20
|
+
}
|
|
21
|
+
function comparablePath(path, caseSensitive) {
|
|
22
|
+
return caseSensitive ? path : path.toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
function isLexicallyUnder(path, root, caseSensitive) {
|
|
25
|
+
const comparableTarget = comparablePath(path, caseSensitive);
|
|
26
|
+
const comparableRoot = comparablePath(root, caseSensitive);
|
|
27
|
+
if (comparableTarget === comparableRoot) return true;
|
|
28
|
+
const prefix = comparableRoot.endsWith(sep) ? comparableRoot : comparableRoot + sep;
|
|
29
|
+
return comparableTarget.startsWith(prefix);
|
|
30
|
+
}
|
|
31
|
+
async function statIfPresent(path) {
|
|
32
|
+
try {
|
|
33
|
+
return await stat(path, { bigint: true });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (isMissing(error)) return void 0;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function sameIdentity(left, right) {
|
|
40
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Determine whether a canonical target is a writable root or lies beneath it.
|
|
44
|
+
* The lexical fast path handles normal canonical spellings; when spellings
|
|
45
|
+
* differ, walk the target's existing ancestors and compare filesystem identity
|
|
46
|
+
* with the root.
|
|
47
|
+
* @param path - canonical target key, which may end in a missing suffix.
|
|
48
|
+
* @param root - canonical writable root.
|
|
49
|
+
* @param caseSensitive - whether lexical comparison preserves case; defaults
|
|
50
|
+
* to the host filesystem convention used by supported platforms.
|
|
51
|
+
* @returns whether the target is the root or a descendant of it.
|
|
52
|
+
*/
|
|
53
|
+
async function isPathUnder(path, root, caseSensitive = process.platform !== "win32") {
|
|
54
|
+
if (isLexicallyUnder(path, root, caseSensitive)) return true;
|
|
55
|
+
const rootInfo = await statIfPresent(root);
|
|
56
|
+
if (!rootInfo) return false;
|
|
57
|
+
let ancestor = path;
|
|
58
|
+
while (true) {
|
|
59
|
+
const ancestorInfo = await statIfPresent(ancestor);
|
|
60
|
+
if (ancestorInfo && sameIdentity(ancestorInfo, rootInfo)) return true;
|
|
61
|
+
const parent = dirname(ancestor);
|
|
62
|
+
if (parent === ancestor) return false;
|
|
63
|
+
ancestor = parent;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region lib/types/fs-binary-sandbox.js
|
|
68
|
+
/**
|
|
69
|
+
* The sandbox-preserving binary fs provider: `DocxSandboxedFileSystem` extends
|
|
70
|
+
* the published `@deepseek-ai/dsh-fs-sandbox` `SandboxedFileSystem` and adds the
|
|
71
|
+
* binary `writeBytes` primitive through the SAME policy fence as the base's
|
|
72
|
+
* `writeText`/`editText`. The fence itself (`checkWriteTarget`) is shared with
|
|
73
|
+
* the `fs-binary-sandbox-plugin` namespace plugin, which registers it as the
|
|
74
|
+
* separate `fsBinary` service — the recommended mount for sandboxed hosts,
|
|
75
|
+
* because it never replaces the host's own `ctx.fs`.
|
|
76
|
+
* @module dsh-tool-docx/fs-binary-sandbox
|
|
77
|
+
*/
|
|
78
|
+
/**
|
|
79
|
+
* Enforce the per-call policy against `target` and return the exact target the
|
|
80
|
+
* mutation must use — the same containment the base `SandboxedFileSystem`
|
|
81
|
+
* applies to its mutations (`danger-full-access` passes unfenced, `read-only`
|
|
82
|
+
* denies, `workspace-write` re-canonicalizes now and requires containment under
|
|
83
|
+
* a writable root). Throws `FS_SANDBOX_DENIED` on refusal.
|
|
84
|
+
* @param resolveTarget - resolves a display path to a fresh canonical target
|
|
85
|
+
* (the provider's own `resolve`, so the checked identity is the mutated one).
|
|
86
|
+
* @param policy - the per-call mode and workspace root.
|
|
87
|
+
* @param target - the caller's resolved target.
|
|
88
|
+
* @returns the fresh target the mutation must use.
|
|
89
|
+
*/
|
|
90
|
+
async function checkWriteTarget(resolveTarget, policy, target) {
|
|
91
|
+
const { mode } = policy;
|
|
92
|
+
if (mode === "danger-full-access") return target;
|
|
93
|
+
if (mode === "read-only") throw new FsError(`cannot write "${target.displayPath}": file access denied under read-only mode`, "FS_SANDBOX_DENIED");
|
|
94
|
+
const fresh = await resolveTarget(target.displayPath);
|
|
95
|
+
let contained = false;
|
|
96
|
+
for (const root of writableRoots(policy)) if (await isPathUnder(fresh.targetKey, root)) {
|
|
97
|
+
contained = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (!contained) throw new FsError(`cannot write "${target.displayPath}": file access denied under workspace-write mode`, "FS_SANDBOX_DENIED");
|
|
101
|
+
return fresh;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The sandboxed filesystem backend with the binary write primitive. Inherits
|
|
105
|
+
* the full `SandboxedFileSystem` contract (local text/binary reads, fenced
|
|
106
|
+
* `writeText`/`editText`) and adds fenced `writeBytes`: the per-call policy
|
|
107
|
+
* fence runs first (the same containment check the base applies to mutations),
|
|
108
|
+
* then the probe → intent-guard → atomic-publish body. Per-targetKey
|
|
109
|
+
* operations are serialized with a tail promise. Intended for hosts that want
|
|
110
|
+
* the full backend mounted AS `ctx.fs` (replacing `fs-sandbox` deliberately);
|
|
111
|
+
* the default mount for sandboxed hosts is the `fs-binary-sandbox-plugin`,
|
|
112
|
+
* which registers this same fenced write under the separate `fsBinary` service.
|
|
113
|
+
*/
|
|
114
|
+
var DocxSandboxedFileSystem = class extends SandboxedFileSystem {
|
|
115
|
+
byteLocks = /* @__PURE__ */ new Map();
|
|
116
|
+
withByteLock(key, op) {
|
|
117
|
+
const next = (this.byteLocks.get(key) ?? Promise.resolve()).then(op, op);
|
|
118
|
+
this.byteLocks.set(key, next.then(() => void 0, () => void 0));
|
|
119
|
+
return next;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Enforce the per-call policy against `target` and return the exact target
|
|
123
|
+
* the mutation must use, via the shared {@link checkWriteTarget}.
|
|
124
|
+
*/
|
|
125
|
+
async checkedWriteTarget(target, sandboxPolicy) {
|
|
126
|
+
return checkWriteTarget((path) => this.resolve(path), sandboxPolicy ?? this.ctx.sandboxPolicy.resolve(), target);
|
|
127
|
+
}
|
|
128
|
+
/** Write raw bytes with an optional version/absence guard, through the policy fence. */
|
|
129
|
+
async writeBytes(target, data, expected, signal, sandboxPolicy) {
|
|
130
|
+
const checked = await this.checkedWriteTarget(target, sandboxPolicy);
|
|
131
|
+
return this.withByteLock(String(checked.targetKey), () => performByteWrite(this, checked, data, expected, signal));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
//#endregion
|
|
135
|
+
export { checkWriteTarget as n, DocxSandboxedFileSystem as t };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { t as FS_BINARY_SERVICE } from "./fs-binary-D0seEN6R.js";
|
|
2
|
+
import { n as performByteWrite } from "./fs-binary-local-n8Ls-bn_.js";
|
|
3
|
+
import { n as checkWriteTarget } from "./fs-binary-sandbox-BobaE5Hr.js";
|
|
4
|
+
//#region lib/types/fs-binary-sandbox-plugin.js
|
|
5
|
+
/**
|
|
6
|
+
* `fs-binary-sandbox-plugin` — the recommended mount for sandboxed hosts: a
|
|
7
|
+
* namespace plugin that registers the binary write primitive as the SEPARATE
|
|
8
|
+
* `fsBinary` service, fenced by the same per-call policy as the harness's
|
|
9
|
+
* `fs-sandbox` mutations. The host's own `ctx.fs` (and its `fs-sandbox` row)
|
|
10
|
+
* is left untouched, so this plugin can never break the host filesystem — at
|
|
11
|
+
* worst the docx write tools report `DOCX_HOST_FS_UNSUPPORTED` when it is not
|
|
12
|
+
* mounted.
|
|
13
|
+
* @module dsh-tool-docx/fs-binary-sandbox-plugin
|
|
14
|
+
*/
|
|
15
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
16
|
+
const name = "fs-binary-sandbox";
|
|
17
|
+
/** Services required: the host filesystem (reads/stat/resolve) and the policy. */
|
|
18
|
+
const inject = ["fs", "sandboxPolicy"];
|
|
19
|
+
/**
|
|
20
|
+
* Register the `fsBinary` service: fenced binary writes through the same
|
|
21
|
+
* containment the sandbox applies to every mutation.
|
|
22
|
+
* @param ctx - the plugin context; execution uses its `fs` and `sandboxPolicy`.
|
|
23
|
+
*/
|
|
24
|
+
function apply(ctx) {
|
|
25
|
+
ctx.provide(FS_BINARY_SERVICE, { writeBytes: (target, data, expected, signal, sandboxPolicy) => {
|
|
26
|
+
const policy = sandboxPolicy ?? ctx.sandboxPolicy.resolve();
|
|
27
|
+
return checkWriteTarget((path) => ctx.fs.resolve(path), policy, target).then((checked) => performByteWrite(ctx.fs, checked, data, expected, signal));
|
|
28
|
+
} });
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { apply, inject, name };
|