dsh-multi-folder 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 +2 -2
- package/README.zh.md +2 -2
- package/docs/design.md +26 -4
- package/lib/index.js +125 -20
- package/package.json +2 -1
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
|
|
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`
|
|
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,13 +42,25 @@ 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).
|
|
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.
|
|
49
|
-
5. Anything else — unknown tools, paths outside every secondary directory,
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
5. Anything else — unknown tools, paths outside every secondary directory, missing
|
|
58
|
+
optional services (`shell`, `jobs`), or a failure **before** the target is
|
|
59
|
+
resolved into a secondary directory (path resolution, config lookup, service
|
|
60
|
+
lookup) — falls through to `next()` and the default pipeline. An explicit
|
|
61
|
+
escalation request (`sandbox_permissions` carrying a non-empty mode string) also
|
|
62
|
+
belongs to the default pipeline, which owns the approval flow; a `null`/empty
|
|
63
|
+
value is not a request and is intercepted normally.
|
|
52
64
|
|
|
53
65
|
**Why mode parity is free:** the mode field of the standing policy is never touched.
|
|
54
66
|
The DSH sandbox backends treat the per-call policy as fully specified and fence by its
|
|
@@ -269,6 +281,16 @@ window.__ModuleLoader__.load({
|
|
|
269
281
|
Lifting this to real multi-root confinement needs an upstream change
|
|
270
282
|
(`SandboxExecutionPolicy` carrying extra write roots and the ACL runner
|
|
271
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.
|
|
272
294
|
- Intercepted secondary-directory mutations do not participate in the
|
|
273
295
|
`fs/write-intent` / `fs/edit-intent` intent guards (the interception calls
|
|
274
296
|
the backend unconditionally, as a full replacement of the tool body), but
|
package/lib/index.js
CHANGED
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* here with the session's standing sandbox policy re-rooted to that
|
|
17
17
|
* directory — identical semantics to the primary workspace in every mode
|
|
18
18
|
* (read-only denies, workspace-write allows, danger-full-access allows).
|
|
19
|
+
* Interception OWNS such a call from the moment its target is resolved
|
|
20
|
+
* inside a secondary directory: an interception failure is reported as the
|
|
21
|
+
* call's own error and NEVER falls through to the default pipeline, which
|
|
22
|
+
* fences every call against the PRIMARY workspace root and would therefore
|
|
23
|
+
* turn any ordinary failure (a missing `old_string`, a locked target, an
|
|
24
|
+
* unreadable file) into the spurious
|
|
25
|
+
* `[sandbox: file access denied under workspace-write mode]` marker.
|
|
19
26
|
* Interception hydrates the configuration AWAITED (never a fire-and-forget
|
|
20
27
|
* read) and looks the dirs up by BOTH the policy root and the header cwd
|
|
21
28
|
* spelling, so a cold first call and a symlinked workspace cannot fall
|
|
@@ -25,7 +32,12 @@
|
|
|
25
32
|
* Background shell runs (`run_in_background: true`) register with the
|
|
26
33
|
* generic jobs runtime (`ctx.jobs`) under the same re-rooted policy,
|
|
27
34
|
* mirroring the shipped pwsh/bash tools so `job_output` / `job_kill` and
|
|
28
|
-
* finish notices keep working.
|
|
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
|
|
29
41
|
* already work.
|
|
30
42
|
* 3. Prompt injection: one ordered system-prompt section rendered per
|
|
31
43
|
* assembly from the configured directories of the assembling session.
|
|
@@ -445,8 +457,9 @@ export function apply(ctx) {
|
|
|
445
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, ' +
|
|
446
458
|
'but each command can write inside only ONE root — the directory its workdir resolves to. ' +
|
|
447
459
|
'A command whose cwd stays the primary workspace CANNOT create files inside a secondary directory. ' +
|
|
448
|
-
'For shell tools, pass `workdir`
|
|
449
|
-
'
|
|
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). ' +
|
|
450
463
|
'Reads from these directories work without `workdir`. The primary workspace remains the default working directory.'
|
|
451
464
|
)
|
|
452
465
|
},
|
|
@@ -515,6 +528,52 @@ export function apply(ctx) {
|
|
|
515
528
|
}
|
|
516
529
|
}
|
|
517
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
|
+
|
|
518
577
|
/**
|
|
519
578
|
* One consuming background read, shaped for `job_output`: the raw delta plus
|
|
520
579
|
* loss/spill notices and sandbox markers, mirroring the shipped pwsh/bash
|
|
@@ -544,6 +603,30 @@ export function apply(ctx) {
|
|
|
544
603
|
return read.delta + (read.delta.length > 0 && !read.delta.endsWith('\n') ? '\n' : '') + notices.join('\n')
|
|
545
604
|
}
|
|
546
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Model-facing failure for a call the interception OWNS (see the ownership
|
|
608
|
+
* rule below). A claimed call is never handed back to the default pipeline,
|
|
609
|
+
* so its real error reaches the model in the shipped tools' error envelope —
|
|
610
|
+
* `Error: <message>` plus the `FS_*` code — instead of the primary-rooted
|
|
611
|
+
* sandbox denial. A genuine sandbox denial (read-only mode) keeps the standard
|
|
612
|
+
* marker plus the one-shot escalation hint, exactly as the shipped
|
|
613
|
+
* `write`/`edit` tools render it, so the escalation flow is unchanged.
|
|
614
|
+
*/
|
|
615
|
+
const ownedFailure = (error, policy) => {
|
|
616
|
+
const code = error && typeof error.code === 'string' && error.code.length > 0 ? error.code : undefined
|
|
617
|
+
const message =
|
|
618
|
+
code === 'FS_SANDBOX_DENIED'
|
|
619
|
+
? '[sandbox: file access denied under ' + policy.mode + ' mode]\n' +
|
|
620
|
+
'[sandbox: escalation available \u2014 retry this exact operation once with sandbox_permissions ' +
|
|
621
|
+
'(the narrowest wider mode that suffices) + justification; the approval prompt asks the user]'
|
|
622
|
+
: String(error && error.message ? error.message : error)
|
|
623
|
+
return {
|
|
624
|
+
isError: true,
|
|
625
|
+
error: { message, ...(code === undefined ? {} : { info: { code } }) },
|
|
626
|
+
content: [{ type: 'text', text: 'Error: ' + message }],
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
547
630
|
ctx.on('tools/execute', async (exec, next) => {
|
|
548
631
|
// Hydration must be AWAITED on the interception path, not fire-and-forget:
|
|
549
632
|
// a first call that arrives before the config read resolves would see an
|
|
@@ -560,12 +643,21 @@ export function apply(ctx) {
|
|
|
560
643
|
await loadDirs(headerCwd)
|
|
561
644
|
}
|
|
562
645
|
if (!INTERCEPT_TOOLS.has(exec.name)) return next()
|
|
646
|
+
// Non-null once the call's target has been resolved into a configured
|
|
647
|
+
// secondary directory: from that point the interception OWNS the call and a
|
|
648
|
+
// failure must be reported as this call's error, never handed back to the
|
|
649
|
+
// default pipeline (which fences against the PRIMARY workspace root and
|
|
650
|
+
// would answer any failure with the spurious workspace-write denial).
|
|
651
|
+
let owned = null
|
|
563
652
|
try {
|
|
564
653
|
const args = exec.arguments
|
|
565
654
|
const standing = sandboxPolicy.resolve(exec.agent ? { session: exec.agent.session } : {})
|
|
566
655
|
const primary = standing.workspaceRoot
|
|
567
|
-
// An explicit escalation request belongs to the default pipeline
|
|
568
|
-
|
|
656
|
+
// An explicit escalation request belongs to the default pipeline (it owns
|
|
657
|
+
// the approval flow). Only a non-empty mode string is a request: a
|
|
658
|
+
// null/empty value is not, and must not hand a secondary-directory
|
|
659
|
+
// mutation to the primary-rooted pipeline.
|
|
660
|
+
if (args && typeof args.sandbox_permissions === 'string' && args.sandbox_permissions.length > 0) return next()
|
|
569
661
|
// The policy root is realpath-canonicalized by sandbox-policy while
|
|
570
662
|
// hydration is keyed by the session cwd as spelled in the header; on a
|
|
571
663
|
// workspace reached through a symlinked/junctioned ancestor the two
|
|
@@ -579,7 +671,20 @@ export function apply(ctx) {
|
|
|
579
671
|
if (filePath === null) return next()
|
|
580
672
|
// Resolve first so `..`, symlinks, and case differences canonicalize
|
|
581
673
|
// before containment matching (same cwd the shipped tools use).
|
|
582
|
-
|
|
674
|
+
let target
|
|
675
|
+
try {
|
|
676
|
+
target = await fs.resolve(filePath, { cwd: primary })
|
|
677
|
+
} catch (error) {
|
|
678
|
+
// An unresolvable ABSOLUTE path that is lexically inside a secondary
|
|
679
|
+
// directory is still this plugin's call to answer: the default pipeline
|
|
680
|
+
// would resolve it against the PRIMARY root and report the sandbox
|
|
681
|
+
// denial, hiding the resolution failure.
|
|
682
|
+
if (dirs !== null && isAbsolute(filePath)) {
|
|
683
|
+
const rawHit = longestRootFirst(dirs).find((d) => pathInside(filePath, d))
|
|
684
|
+
if (rawHit !== undefined) return ownedFailure(error, { ...standing, workspaceRoot: rawHit })
|
|
685
|
+
}
|
|
686
|
+
return next()
|
|
687
|
+
}
|
|
583
688
|
const abs = fs.processPath(target)
|
|
584
689
|
// Security boundary: configuration is user-managed. Reject direct
|
|
585
690
|
// write/edit attempts against the host-owned config file, even before
|
|
@@ -601,6 +706,7 @@ export function apply(ctx) {
|
|
|
601
706
|
const policy = { ...standing, workspaceRoot: hit }
|
|
602
707
|
|
|
603
708
|
if (exec.name === 'write') {
|
|
709
|
+
owned = { policy }
|
|
604
710
|
const outcome = await fs.writeText(target, String(args.content), undefined, exec.signal, policy)
|
|
605
711
|
// Keep the observation layer coherent with the shipped write tool's
|
|
606
712
|
// contract: a successful create/update is a presence observation.
|
|
@@ -628,6 +734,7 @@ export function apply(ctx) {
|
|
|
628
734
|
const newString = args && typeof args.new_string === 'string' ? args.new_string : null
|
|
629
735
|
if (oldString === null || newString === null) return next()
|
|
630
736
|
const replaceAll = args.replace_all === true
|
|
737
|
+
owned = { policy }
|
|
631
738
|
const outcome = await fs.editText(
|
|
632
739
|
target,
|
|
633
740
|
{ oldString, newString, replaceAll },
|
|
@@ -675,27 +782,19 @@ export function apply(ctx) {
|
|
|
675
782
|
// Background runs get the SAME re-rooted policy as foreground runs.
|
|
676
783
|
// They register with the generic jobs runtime (`ctx.jobs`) exactly
|
|
677
784
|
// like the shipped pwsh/bash tools do, so `job_output` / `job_kill`
|
|
678
|
-
// and the finish notice keep working for the intercepted job.
|
|
679
|
-
// background process outlives the tool call, so no caller signal is
|
|
680
|
-
// forwarded; `shell.start` ignores `timeoutMs` by design.
|
|
785
|
+
// and the finish notice keep working for the intercepted job.
|
|
681
786
|
if (args && args.run_in_background === true) {
|
|
682
787
|
// An aborted call belongs to the default pipeline, which raises the
|
|
683
788
|
// canonical abort error before anything starts.
|
|
684
789
|
if (exec.signal && exec.signal.aborted) return next()
|
|
685
790
|
const jobs = ctx.get('jobs')
|
|
686
791
|
if (jobs === undefined) return next()
|
|
792
|
+
owned = { policy }
|
|
687
793
|
const jobId = jobs.start({
|
|
688
794
|
kind: exec.name,
|
|
689
795
|
label: String(args.command),
|
|
690
796
|
...(exec.agent ? { owner: exec.agent } : {}),
|
|
691
|
-
run: () =>
|
|
692
|
-
const proc = shell.start(shell.resolve(request))
|
|
693
|
-
return {
|
|
694
|
-
cancel: () => void proc.kill(),
|
|
695
|
-
done: proc.done.then(() => processOutcome(proc)),
|
|
696
|
-
readOutput: () => renderProcessRead(proc.readOutput(), proc.sandbox),
|
|
697
|
-
}
|
|
698
|
-
},
|
|
797
|
+
run: () => startBackgroundJob(shell, request),
|
|
699
798
|
})
|
|
700
799
|
return {
|
|
701
800
|
isError: false,
|
|
@@ -704,6 +803,7 @@ export function apply(ctx) {
|
|
|
704
803
|
}
|
|
705
804
|
}
|
|
706
805
|
|
|
806
|
+
owned = { policy }
|
|
707
807
|
const result = await shell.run(shell.resolve({ ...request, signal: exec.signal }))
|
|
708
808
|
if (result.aborted) {
|
|
709
809
|
return {
|
|
@@ -744,9 +844,14 @@ export function apply(ctx) {
|
|
|
744
844
|
return { isError: false, value, content: [{ type: 'text', text: shellRender(value) }] }
|
|
745
845
|
}
|
|
746
846
|
return next()
|
|
747
|
-
} catch {
|
|
748
|
-
//
|
|
749
|
-
|
|
847
|
+
} catch (error) {
|
|
848
|
+
// A failure BEFORE the call was claimed (path resolution, config lookup,
|
|
849
|
+
// shell service lookup) falls back to the default pipeline. A failure
|
|
850
|
+
// AFTER the claim — the mutation or the shell run itself — does not: the
|
|
851
|
+
// default pipeline fences the call against the PRIMARY workspace root, so
|
|
852
|
+
// it could only answer with the spurious workspace-write denial and would
|
|
853
|
+
// hide the real cause (for example a missing `old_string`).
|
|
854
|
+
return owned === null ? next() : ownedFailure(error, owned.policy)
|
|
750
855
|
}
|
|
751
856
|
})
|
|
752
857
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-multi-folder",
|
|
3
|
-
"version": "0.2.
|
|
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",
|