dsh-multi-folder 0.2.3 → 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 CHANGED
@@ -71,10 +71,10 @@ Each confined command runs under **exactly ONE writable root** — the workspace
71
71
 
72
72
  - A command whose cwd stays the **primary workspace cannot create files inside a secondary directory**. `git -C <secondary> commit`, `cd <secondary>` inside a script, `git clone <url> <secondary>`, or absolute-path writes all fail with an OS-level `Permission denied` (e.g. `fatal: Unable to create '.../.git/index.lock': Permission denied`).
73
73
  - Symmetrically, a command re-rooted to a secondary directory cannot write to the **primary workspace** (or another secondary directory) in the same invocation.
74
- - **Rule for file-creating commands: set `workdir` to the directory the command writes into.** For git, run the command from inside the repository (pass `workdir` pointing at it) instead of using `git -C` from the primary workspace.
74
+ - **Rule for file-creating commands: set `workdir` to the directory the command writes into**, and pass it as an **absolute** path — a relative `workdir` is resolved against the primary workspace, and changing the process directory inside the command (`Set-Location` / `cd`) does not widen the writable root (the write then fails with an OS-level denial, Windows error 5). For git, run the command from inside the repository (pass `workdir` pointing at it) instead of using `git -C` from the primary workspace. The rule applies to `run_in_background: true` runs exactly as to foreground ones.
75
75
  - Reads are unrestricted and need no `workdir`.
76
76
 
77
- When a shell run ends in such a denial and references a configured secondary directory, the plugin attaches a short diagnostic hint to the tool result explaining the workdir fix.
77
+ When a shell run ends in such a denial and references a configured secondary directory, the plugin attaches a short diagnostic hint to the tool result explaining the workdir fix. A **background** run's denial surfaces later instead — in that job's `job_output` stream, after the tool call has already returned — so read the job output and re-run it with an absolute `workdir`.
78
78
 
79
79
  ## How it works
80
80
 
package/README.zh.md CHANGED
@@ -71,10 +71,10 @@ Agent 无需任何额外操作:`read` / `glob` / `grep` 随处可用;`write`
71
71
 
72
72
  - cwd 停留在**主工作区**的命令**不能在副目录创建文件**。`git -C <副目录> commit`、脚本内 `cd <副目录>`、`git clone <url> <副目录>`、按绝对路径写文件等都会以操作系统级 `Permission denied` 失败(例如 `fatal: Unable to create '.../.git/index.lock': Permission denied`)。
73
73
  - 对称地,被换根到副目录的命令在同一次调用中也**不能写主工作区**(或另一个副目录)。
74
- - **创建文件的命令必须把 `workdir` 设为它要写入的目录。** 对 git 而言,请进入仓库目录执行(`workdir` 指向该仓库),而不是从主工作区用 `git -C`。
74
+ - **创建文件的命令必须把 `workdir` 设为它要写入的目录,且必须使用该目录的绝对路径**——相对 `workdir` 只会相对主工作区解析;在命令内部切换进程目录(`Set-Location` / `cd`)也不会扩大可写根(写入会以操作系统级拒绝失败,Windows 错误码 5)。对 git 而言,请进入仓库目录执行(`workdir` 指向该仓库),而不是从主工作区用 `git -C`。该规则同样适用于 `run_in_background: true` 的后台任务。
75
75
  - 读操作不受限制,无需 `workdir`。
76
76
 
77
- 当 shell 命令以这类拒绝失败且命令引用了已配置的副目录时,插件会在工具结果后附带一条简短的诊断提示,说明 workdir 的修正方式。
77
+ 当 shell 命令以这类拒绝失败且命令引用了已配置的副目录时,插件会在工具结果后附带一条简短的诊断提示,说明 workdir 的修正方式。**后台任务**的拒绝发生在工具调用返回之后,只出现在该任务的 `job_output` 输出里,那时不会再附带提示——请读取任务输出,并用绝对 `workdir` 重跑。
78
78
 
79
79
  ## 工作原理
80
80
 
package/docs/design.md CHANGED
@@ -42,7 +42,15 @@ A listener on the `tools/execute` around-dispatch waterfall handles `write`, `ed
42
42
  request registered through the generic jobs runtime (`ctx.jobs`) exactly like
43
43
  the shipped shell tools (`kind` = tool name, `owner` = calling agent, streamed
44
44
  reads shaped for `job_output` with sandbox markers, terminal outcome in the
45
- `completed`/`killed` vocabulary). A caller-aborted call falls through to the
45
+ `completed`/`killed`/`failed` vocabulary). `shell.start` is **async** (it
46
+ publishes the handle only once launch preparation — Windows ACL grants
47
+ included — succeeded, and rejects when preparation is cancelled or fails), so
48
+ the launch is adapted to the jobs runtime's synchronous `run(): JobHooks`
49
+ contract the same way the shipped tools' `processJob` does: the handle is
50
+ awaited, the job-owned `AbortSignal` travels into `shell.resolve` (a cancelled
51
+ job aborts preparation, not just an already-published process), a rejected
52
+ preparation settles the job as `failed` with the real cause, and a read before
53
+ publication is empty. A caller-aborted call falls through to the
46
54
  default pipeline, which raises the canonical abort error.
47
55
  The result carries the same canonical value/content shapes as the shipped tools, so
48
56
  downstream presentation keeps working.
@@ -273,6 +281,16 @@ window.__ModuleLoader__.load({
273
281
  Lifting this to real multi-root confinement needs an upstream change
274
282
  (`SandboxExecutionPolicy` carrying extra write roots and the ACL runner
275
283
  accepting several workspace write SIDs).
284
+ - A **relative** `workdir` never re-roots a run: the shipped shell tools resolve
285
+ it against the session workspace (the primary root), so only an ABSOLUTE path
286
+ into a secondary directory is intercepted. Likewise, changing the process
287
+ directory inside the command (`Set-Location` / `cd`) moves the process cwd but
288
+ not the ACL write root — the reported symptom is an OS-level access denial on
289
+ the file write (Windows error 5, e.g. `torch.save`'s
290
+ `open file failed with error code: 5`), not a sandbox marker. On a BACKGROUND
291
+ run that denial surfaces in the job's `job_output` stream after the tool call
292
+ has already returned, so the `tools/post-execute` hint cannot see it; the fix
293
+ is the same — re-run with an absolute `workdir` inside the secondary directory.
276
294
  - Intercepted secondary-directory mutations do not participate in the
277
295
  `fs/write-intent` / `fs/edit-intent` intent guards (the interception calls
278
296
  the backend unconditionally, as a full replacement of the tool body), but
package/lib/index.js CHANGED
@@ -32,7 +32,12 @@
32
32
  * Background shell runs (`run_in_background: true`) register with the
33
33
  * generic jobs runtime (`ctx.jobs`) under the same re-rooted policy,
34
34
  * mirroring the shipped pwsh/bash tools so `job_output` / `job_kill` and
35
- * finish notices keep working. Reads (read/glob/grep) are unfenced and
35
+ * finish notices keep working. `shell.start` is ASYNC (it publishes the
36
+ * handle only once launch preparation, Windows ACL grants included,
37
+ * succeeded), so the launcher is adapted to the jobs runtime's synchronous
38
+ * hooks contract exactly like the shipped tools' `processJob`: the job-owned
39
+ * AbortSignal drives preparation cancellation and a rejected preparation
40
+ * settles the job as `failed`. Reads (read/glob/grep) are unfenced and
36
41
  * already work.
37
42
  * 3. Prompt injection: one ordered system-prompt section rendered per
38
43
  * assembly from the configured directories of the assembling session.
@@ -452,8 +457,9 @@ export function apply(ctx) {
452
457
  '\nYou have the SAME read/write/edit and command-execution permissions on these directories as on the primary workspace under the current sandbox mode, ' +
453
458
  'but each command can write inside only ONE root — the directory its workdir resolves to. ' +
454
459
  'A command whose cwd stays the primary workspace CANNOT create files inside a secondary directory. ' +
455
- 'For shell tools, pass `workdir` pointing inside one of these directories — foreground and background (`run_in_background`) runs alike. ' +
456
- 'File-creating commands, git included, MUST set `workdir` to the secondary directory: do not run `git -C <secondary>` or `cd <secondary>` inside a command launched from the primary workspace. ' +
460
+ 'For shell tools, pass `workdir` holding the ABSOLUTE path of one of these directories — foreground and background (`run_in_background`) runs alike. ' +
461
+ 'A relative `workdir` is resolved against the PRIMARY workspace, never against a secondary directory. ' +
462
+ 'File-creating commands, git included, MUST set `workdir` to the secondary directory: do not run `git -C <secondary>` or `cd <secondary>` inside a command launched from the primary workspace — changing the process directory inside the command (`Set-Location` / `cd`) does NOT widen the writable root, so writes into a secondary directory then fail with an OS-level access denial (Windows error 5). ' +
457
463
  'Reads from these directories work without `workdir`. The primary workspace remains the default working directory.'
458
464
  )
459
465
  },
@@ -522,6 +528,52 @@ export function apply(ctx) {
522
528
  }
523
529
  }
524
530
 
531
+ /**
532
+ * Adapt one asynchronous background launch to the jobs runtime's SYNCHRONOUS
533
+ * hooks contract, mirroring the shipped pwsh/bash tools' `processJob`.
534
+ *
535
+ * `shell.start` is ASYNC — it resolves the process handle only after launch
536
+ * preparation (Windows ACL grants included) and rejects when preparation is
537
+ * cancelled or fails — so the handle can never be dereferenced from `run()`.
538
+ * Calling it as if it returned a process made every background run in a
539
+ * secondary directory fail immediately with
540
+ * `Cannot read properties of undefined (reading 'then')` (`proc.done` read
541
+ * off the un-awaited promise). The job-owned AbortSignal travels into
542
+ * `shell.resolve`, so `cancel` stops a launch that has not published a handle
543
+ * yet, and a rejected preparation settles the job as `failed` instead of
544
+ * leaving it running forever. A background process outlives the tool call, so
545
+ * no CALLER signal is forwarded; `shell.start` ignores `timeoutMs` by design.
546
+ */
547
+ const startBackgroundJob = (shell, request) => {
548
+ const controller = new AbortController()
549
+ let proc
550
+ const done = (async () => {
551
+ try {
552
+ proc = await shell.start(shell.resolve({ ...request, signal: controller.signal }))
553
+ try {
554
+ if (controller.signal.aborted) proc.kill()
555
+ } finally {
556
+ await proc.done
557
+ }
558
+ return processOutcome(proc)
559
+ } catch (error) {
560
+ return {
561
+ status: controller.signal.aborted && proc === undefined ? 'killed' : 'failed',
562
+ detail: error && error.message !== undefined ? String(error.message) : String(error),
563
+ }
564
+ }
565
+ })()
566
+ return {
567
+ cancel: (reason) => {
568
+ if (controller.signal.aborted) return
569
+ controller.abort(reason)
570
+ if (proc !== undefined) proc.kill()
571
+ },
572
+ done,
573
+ readOutput: () => (proc === undefined ? '' : renderProcessRead(proc.readOutput(), proc.sandbox)),
574
+ }
575
+ }
576
+
525
577
  /**
526
578
  * One consuming background read, shaped for `job_output`: the raw delta plus
527
579
  * loss/spill notices and sandbox markers, mirroring the shipped pwsh/bash
@@ -730,9 +782,7 @@ export function apply(ctx) {
730
782
  // Background runs get the SAME re-rooted policy as foreground runs.
731
783
  // They register with the generic jobs runtime (`ctx.jobs`) exactly
732
784
  // like the shipped pwsh/bash tools do, so `job_output` / `job_kill`
733
- // and the finish notice keep working for the intercepted job. A
734
- // background process outlives the tool call, so no caller signal is
735
- // forwarded; `shell.start` ignores `timeoutMs` by design.
785
+ // and the finish notice keep working for the intercepted job.
736
786
  if (args && args.run_in_background === true) {
737
787
  // An aborted call belongs to the default pipeline, which raises the
738
788
  // canonical abort error before anything starts.
@@ -744,14 +794,7 @@ export function apply(ctx) {
744
794
  kind: exec.name,
745
795
  label: String(args.command),
746
796
  ...(exec.agent ? { owner: exec.agent } : {}),
747
- run: () => {
748
- const proc = shell.start(shell.resolve(request))
749
- return {
750
- cancel: () => void proc.kill(),
751
- done: proc.done.then(() => processOutcome(proc)),
752
- readOutput: () => renderProcessRead(proc.readOutput(), proc.sandbox),
753
- }
754
- },
797
+ run: () => startBackgroundJob(shell, request),
755
798
  })
756
799
  return {
757
800
  isError: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-multi-folder",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "DeepSeek Harness plugin: secondary working directories for a project. The agent keeps the primary workspace as cwd, gains equal write/exec permissions on configured secondary directories under workspace-write mode, and is notified of configuration changes at the next message boundary. Configurable from the session header AND from the session-creation page (before the first message) through a sessionless multiFolder remote API.",
5
5
  "keywords": [
6
6
  "dsh-plugin",
@@ -42,6 +42,7 @@
42
42
  "compatibility": {
43
43
  "node": ">=20",
44
44
  "dshReleases": {
45
+ "0.1.6-alpha.1": "compatible",
45
46
  "0.1.2-alpha.5": "compatible",
46
47
  "0.1.2-alpha.4": "compatible",
47
48
  "0.1.2-alpha.3": "compatible",