dsh-office-tools 1.0.0 → 1.0.2

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 CHANGED
@@ -6,6 +6,8 @@ Eight model-facing Office file tools for DeepSeek Harness, running entirely in t
6
6
 
7
7
  > Install: `dsh plugin --profile web add github:kw78/dsh-office-tools`
8
8
 
9
+ > DSH Store lists a fixed Commit; verify the installed source against it: `ae1ddd40f98debf4b1430748d287951f35faaf36` (tag `v1.0.0`). Later commits on `main` carry test/doc additions only unless the version bumps.
10
+
9
11
  Eight model-facing Office file tools for DeepSeek Harness, running entirely in the plugin host half — dependency-free since 1.0.0: every package is generated and parsed by this plugin's own OOXML engine, and every byte crosses the official DSH filesystem service (`ctx.fs`), never the raw file system.
10
12
 
11
13
  | Tool | Purpose |
package/README.zh.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  > 安装:`dsh plugin --profile web add github:kw78/dsh-office-tools`
6
6
 
7
+ > DSH Store 固定 Commit 校验源:`ae1ddd40f98debf4b1430748d287951f35faaf36`(tag `v1.0.0`)。其后的 `main` 提交仅为测试/文档补充,除非版本号提升。
8
+
7
9
  为 DeepSeek Harness 提供 8 个模型可调用的 Office 文件工具,全部运行在 host 半 —— 1.0.0 起零运行时依赖:所有文件由插件自带的 OOXML 引擎生成与解析,所有字节都经由官方文件服务 `ctx.fs` 传输,插件自身不直接触碰文件系统。
8
10
 
9
11
  | 工具 | 作用 |
package/lib/index.js CHANGED
@@ -357,6 +357,15 @@ import { extname, isAbsolute, join, relative, resolve } from "node:path";
357
357
  var MAX_TEXT_CHARS = 2e5;
358
358
  var MAX_READ_CELLS = 2e5;
359
359
  var MAX_WRITE_CELLS = 2e5;
360
+ async function resolveWritePolicy(ctx, exec) {
361
+ if (ctx.fs.sandboxMode === void 0) return void 0;
362
+ const service = typeof ctx.get === "function" ? ctx.get("sandboxPolicy") : void 0;
363
+ if (service === void 0 || service === null) {
364
+ throw new Error("office tools: the mounted filesystem confines but ctx.sandboxPolicy is missing");
365
+ }
366
+ if (typeof service.resolve !== "function") return void 0;
367
+ return service.resolve(exec.agent === void 0 ? {} : { session: exec.agent.session });
368
+ }
360
369
  function workspaceRootOf(exec) {
361
370
  const cwd = exec.agent?.session.header.cwd;
362
371
  if (cwd === void 0 || cwd === "") {
@@ -374,19 +383,20 @@ function assertLexicallyWithin(root, candidate) {
374
383
  throw new Error(`path "${candidate}" escapes the session workspace "${root}"`);
375
384
  }
376
385
  }
377
- async function resolveOfficePath(exec, ctx, rawPath, allowedExts, mustExist) {
386
+ async function resolveOfficePath(exec, ctx, rawPath, allowedExts, mustExist, writePolicy) {
378
387
  exec.signal.throwIfAborted();
379
388
  if (rawPath.trim() === "") throw new Error("path must be a non-empty string");
380
389
  const root = workspaceRootOf(exec);
381
- const candidate = resolve(isAbsolute(rawPath) ? rawPath : join(root, rawPath));
382
- assertLexicallyWithin(root, candidate);
390
+ const effectiveRoot = writePolicy?.workspaceRoot ? resolve(writePolicy.workspaceRoot) : root;
391
+ const candidate = resolve(isAbsolute(rawPath) ? rawPath : join(effectiveRoot, rawPath));
392
+ assertLexicallyWithin(effectiveRoot, candidate);
383
393
  const ext = extname(candidate).toLowerCase();
384
394
  if (!allowedExts.includes(ext)) {
385
395
  throw new Error(`expected ${allowedExts.join(" or ")} file, got extension "${ext || "(none)"}"`);
386
396
  }
387
397
  const fs = ctx.fs;
388
- const rootTarget = await fs.resolve(".", { cwd: root, signal: exec.signal });
389
- const target = await fs.resolve(rawPath, { cwd: root, signal: exec.signal });
398
+ const rootTarget = await fs.resolve(".", { cwd: effectiveRoot, signal: exec.signal });
399
+ const target = await fs.resolve(rawPath, { cwd: effectiveRoot, signal: exec.signal });
390
400
  if (!fs.contains(rootTarget, target)) {
391
401
  throw new Error(`path "${rawPath}" escapes the session workspace "${root}"`);
392
402
  }
@@ -412,9 +422,9 @@ async function readOfficeBytes(exec, ctx, target) {
412
422
  exec.signal.throwIfAborted();
413
423
  return { bytes, sizeBytes: bytes.byteLength };
414
424
  }
415
- async function saveOfficeText(exec, ctx, target, text) {
425
+ async function saveOfficeText(exec, ctx, target, text, writePolicy) {
416
426
  exec.signal.throwIfAborted();
417
- await ctx.fs.writeText(target, text, void 0, exec.signal);
427
+ await ctx.fs.writeText(target, text, void 0, exec.signal, writePolicy);
418
428
  exec.signal.throwIfAborted();
419
429
  return Buffer.byteLength(text, "utf-8");
420
430
  }
@@ -854,7 +864,8 @@ function registerExcelCreate(ctx) {
854
864
  locations: [{ path: args.path }]
855
865
  }),
856
866
  async execute(args, exec) {
857
- const target = await resolveOfficePath(exec, ctx, args.path, [".xlsx"], false);
867
+ const writePolicy = await resolveWritePolicy(ctx, exec);
868
+ const target = await resolveOfficePath(exec, ctx, args.path, [".xlsx"], false, writePolicy);
858
869
  await assertMayCreate(exec, ctx, target.target, args.overwrite ?? false);
859
870
  validateSheetSpecs(args.sheets);
860
871
  exec.signal.throwIfAborted();
@@ -879,7 +890,7 @@ function registerExcelCreate(ctx) {
879
890
  });
880
891
  const text = buildXlsxText(models);
881
892
  exec.signal.throwIfAborted();
882
- const sizeBytes = await saveOfficeText(exec, ctx, target.target, text);
893
+ const sizeBytes = await saveOfficeText(exec, ctx, target.target, text, writePolicy);
883
894
  return { path: target.display, sizeBytes, sheets: summaries };
884
895
  }
885
896
  }));
@@ -1018,7 +1029,8 @@ function registerExcelUpdate(ctx) {
1018
1029
  locations: [{ path: args.path }]
1019
1030
  }),
1020
1031
  async execute(args, exec) {
1021
- const target = await resolveOfficePath(exec, ctx, args.path, [".xlsx"], true);
1032
+ const writePolicy = await resolveWritePolicy(ctx, exec);
1033
+ const target = await resolveOfficePath(exec, ctx, args.path, [".xlsx"], true, writePolicy);
1022
1034
  if ((args.sheets?.length ?? 0) === 0 && (args.cell_updates?.length ?? 0) === 0) {
1023
1035
  throw new Error("excel_update needs at least one entry in sheets or cell_updates");
1024
1036
  }
@@ -1062,7 +1074,7 @@ function registerExcelUpdate(ctx) {
1062
1074
  exec.signal.throwIfAborted();
1063
1075
  const orderedModels = names.map((name2) => models.get(name2)).filter((model) => model !== void 0).map((model, index) => ({ ...model, part: `worksheets/sheet${index + 1}.xml` }));
1064
1076
  const text = buildXlsxText(orderedModels);
1065
- const sizeBytes = await saveOfficeText(exec, ctx, target.target, text);
1077
+ const sizeBytes = await saveOfficeText(exec, ctx, target.target, text, writePolicy);
1066
1078
  return {
1067
1079
  path: target.display,
1068
1080
  sizeBytes,
@@ -1743,7 +1755,8 @@ ${sketchSlide(value.slideWidthInches, value.slideHeightInches, slide.elements)}`
1743
1755
  locations: [{ path: args.path }]
1744
1756
  }),
1745
1757
  async execute(args, exec) {
1746
- const target = await resolveOfficePath(exec, ctx, args.path, [".pptx"], false);
1758
+ const writePolicy = await resolveWritePolicy(ctx, exec);
1759
+ const target = await resolveOfficePath(exec, ctx, args.path, [".pptx"], false, writePolicy);
1747
1760
  await assertMayCreate(exec, ctx, target.target, args.overwrite ?? false);
1748
1761
  if ((args.slides?.length ?? 0) > 0) validateSlideSpecs(args.slides);
1749
1762
  if (args.title === void 0 && (args.slides?.length ?? 0) === 0) {
@@ -1752,7 +1765,7 @@ ${sketchSlide(value.slideWidthInches, value.slideHeightInches, slide.elements)}`
1752
1765
  exec.signal.throwIfAborted();
1753
1766
  const { text, layout } = await buildPptxText(args, exec, ctx, target);
1754
1767
  exec.signal.throwIfAborted();
1755
- const sizeBytes = await saveOfficeText(exec, ctx, target.target, text);
1768
+ const sizeBytes = await saveOfficeText(exec, ctx, target.target, text, writePolicy);
1756
1769
  const result = {
1757
1770
  path: target.display,
1758
1771
  sizeBytes,
@@ -1937,7 +1950,8 @@ function registerWordUpdate(ctx) {
1937
1950
  locations: [{ path: args.path }]
1938
1951
  }),
1939
1952
  async execute(args, exec) {
1940
- const target = await resolveOfficePath(exec, ctx, args.path, [".docx"], true);
1953
+ const writePolicy = await resolveWritePolicy(ctx, exec);
1954
+ const target = await resolveOfficePath(exec, ctx, args.path, [".docx"], true, writePolicy);
1941
1955
  const { paragraphs: paragraphCount, cells } = wordCreateCounts(args);
1942
1956
  if ((args.paragraphs?.length ?? 0) === 0 && (args.bullets?.length ?? 0) === 0 && args.table === void 0) {
1943
1957
  throw new Error("word_update needs at least one of paragraphs, bullets, or table");
@@ -1966,7 +1980,7 @@ function registerWordUpdate(ctx) {
1966
1980
  }
1967
1981
  const text = buildAsciiZip(parts);
1968
1982
  exec.signal.throwIfAborted();
1969
- const sizeBytes = await saveOfficeText(exec, ctx, target.target, text);
1983
+ const sizeBytes = await saveOfficeText(exec, ctx, target.target, text, writePolicy);
1970
1984
  return {
1971
1985
  path: target.display,
1972
1986
  sizeBytes,
@@ -2220,7 +2234,8 @@ function registerWordCreate(ctx) {
2220
2234
  locations: [{ path: args.path }]
2221
2235
  }),
2222
2236
  async execute(args, exec) {
2223
- const target = await resolveOfficePath(exec, ctx, args.path, [".docx"], false);
2237
+ const writePolicy = await resolveWritePolicy(ctx, exec);
2238
+ const target = await resolveOfficePath(exec, ctx, args.path, [".docx"], false, writePolicy);
2224
2239
  await assertMayCreate(exec, ctx, target.target, args.overwrite ?? false);
2225
2240
  const { paragraphs: paragraphCount, cells } = wordCreateCounts(args);
2226
2241
  if (paragraphCount > 1e4) throw new Error("too many paragraphs/bullets/table rows (maximum 10000)");
@@ -2230,7 +2245,7 @@ function registerWordCreate(ctx) {
2230
2245
  }
2231
2246
  const text = buildDocxText(args);
2232
2247
  exec.signal.throwIfAborted();
2233
- const sizeBytes = await saveOfficeText(exec, ctx, target.target, text);
2248
+ const sizeBytes = await saveOfficeText(exec, ctx, target.target, text, writePolicy);
2234
2249
  const result = {
2235
2250
  path: target.display,
2236
2251
  sizeBytes,
@@ -8,8 +8,12 @@
8
8
  * never touches the file system directly: workspace containment, symlink
9
9
  * resolution, and atomic publication all belong to the backend, and this
10
10
  * module only adds the Office-tool path policy on top (extension allow-lists,
11
- * size caps, overwrite refusal, display paths).
11
+ * size caps, overwrite refusal, display paths). Writes carry the per-call
12
+ * sandbox policy (`ctx.sandboxPolicy.resolve({ session })` when a confining
13
+ * backend is mounted) so sandboxing backends fence them by the session's
14
+ * effective policy rather than the deployment default.
12
15
  */
16
+ import type { Context } from '@deepseek-ai/cordis';
13
17
  import type { FileSystem, FsTarget } from '@deepseek-ai/dsh-fs';
14
18
  import type { ToolRunContext } from '@deepseek-ai/dsh-tools';
15
19
  /** Cap for text materialized into a single tool result. */
@@ -22,6 +26,22 @@ export declare const MAX_WRITE_CELLS = 200000;
22
26
  export interface FsContext {
23
27
  fs: FileSystem;
24
28
  }
29
+ /**
30
+ * The per-call sandbox execution policy `ctx.fs.writeText` accepts as its
31
+ * fifth parameter (`SandboxExecutionPolicy` from `@deepseek-ai/dsh-sandbox`):
32
+ * a sandboxing backend fences the write by it, the bare backend ignores it,
33
+ * and omitting it leaves the backend its deployment default — whose writable
34
+ * roots need not include this session's workspace. Derived from the official
35
+ * `writeText` signature so the plugin never depends on `@deepseek-ai/dsh-sandbox`
36
+ * directly.
37
+ */
38
+ export type OfficeWritePolicy = NonNullable<Parameters<FileSystem['writeText']>[4]>;
39
+ /**
40
+ * Resolve the per-call write policy the way the official fs tool layer does:
41
+ * skip when the mounted backend does not confine (`fs.sandboxMode` undefined),
42
+ * otherwise ask the shared `sandboxPolicy` service for the calling session.
43
+ */
44
+ export declare function resolveWritePolicy(ctx: Context & FsContext, exec: ToolRunContext): Promise<OfficeWritePolicy | undefined>;
25
45
  export interface ResolvedOfficePath {
26
46
  /** The path exactly as the model passed it. */
27
47
  input: string;
@@ -41,8 +61,12 @@ export interface ResolvedOfficePath {
41
61
  * @param rawPath - the model-supplied path string.
42
62
  * @param allowedExts - acceptable lowercased extensions WITH dots (e.g. `.docx`).
43
63
  * @param mustExist - when true, stat the target and refuse anything but a regular file.
64
+ * @param writePolicy - the per-call write policy, when a sandbox controller is
65
+ * mounted; its `workspaceRoot` is the authoritative workspace the write is
66
+ * fenced to, so relative paths resolve against it exactly like the official
67
+ * fs tools do. Omitted (reads, bare backends) keeps the session cwd.
44
68
  */
45
- export declare function resolveOfficePath(exec: ToolRunContext, ctx: FsContext, rawPath: string, allowedExts: readonly string[], mustExist: boolean): Promise<ResolvedOfficePath>;
69
+ export declare function resolveOfficePath(exec: ToolRunContext, ctx: FsContext, rawPath: string, allowedExts: readonly string[], mustExist: boolean, writePolicy?: OfficeWritePolicy): Promise<ResolvedOfficePath>;
46
70
  /**
47
71
  * Read a bounded Office file through the backend, observing tool-call
48
72
  * cancellation and refusing files above the hard cap before any transfer.
@@ -54,9 +78,11 @@ export declare function readOfficeBytes(exec: ToolRunContext, ctx: FsContext, ta
54
78
  /**
55
79
  * Publish one generated package: the text is pure ASCII (asserted by the zip
56
80
  * planner), so the backend's atomic UTF-8 write lands it on disk
57
- * byte-identical. Returns the on-disk size.
81
+ * byte-identical. The per-call write policy rides along as `writeText`'s
82
+ * fifth parameter so a sandboxing backend fences the write by the session's
83
+ * effective policy instead of its deployment default. Returns the on-disk size.
58
84
  */
59
- export declare function saveOfficeText(exec: ToolRunContext, ctx: FsContext, target: FsTarget, text: string): Promise<number>;
85
+ export declare function saveOfficeText(exec: ToolRunContext, ctx: FsContext, target: FsTarget, text: string, writePolicy?: OfficeWritePolicy): Promise<number>;
60
86
  /**
61
87
  * Reject an overwrite when `overwrite` is false and the target already
62
88
  * exists. Callers use this BEFORE doing expensive generation so the model
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-office-tools",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "DSH model-facing Office tools: create, read, and update Word (.docx), Excel (.xlsx), and PowerPoint (.pptx) files inside the session workspace, entirely through the official DSH filesystem service.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -114,5 +114,47 @@
114
114
  "publishConfig": {
115
115
  "access": "public"
116
116
  },
117
- "packageManager": "pnpm@11.7.0"
117
+ "packageManager": "pnpm@10.34.5",
118
+ "dshWorkshop": {
119
+ "schema": "omdsh-workshop-package/v1",
120
+ "type": "plugin",
121
+ "integration": {
122
+ "protocol": "harness-profile",
123
+ "artifact": "cordis.patch.yml"
124
+ },
125
+ "install": {
126
+ "mode": "transactional",
127
+ "adapter": "profile-bundle",
128
+ "failurePolicy": "generation-rollback",
129
+ "touchesCurrentBeforeActivation": false
130
+ },
131
+ "lifecycle": {
132
+ "activation": "restart-profile",
133
+ "dispose": "supported"
134
+ },
135
+ "permissions": [
136
+ "files:workspace-read",
137
+ "files:workspace-write"
138
+ ],
139
+ "compatibility": {
140
+ "dshVersions": [
141
+ "0.1.1-rc.2",
142
+ "0.1.2-alpha.4",
143
+ "0.1.2-alpha.5",
144
+ "0.1.2-rc.1"
145
+ ]
146
+ },
147
+ "capability": {
148
+ "id": "word_create",
149
+ "kind": "tool",
150
+ "invocation": "word_create({\"path\": \"hub-check.docx\", \"paragraphs\": [\"dsh-hub intake check\"]}) in a session with workspace cwd",
151
+ "expected": "a real .docx (first two bytes PK) exists at the workspace path; the result reports sizeBytes > 0; word_read returns 'dsh-hub intake check'"
152
+ },
153
+ "evidence": {
154
+ "install": null,
155
+ "failureIsolation": null,
156
+ "hotReload": null,
157
+ "remove": null
158
+ }
159
+ }
118
160
  }