@dsh-cc/session-cwd 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 +201 -0
- package/README.md +45 -0
- package/lib/api.d.ts +36 -0
- package/lib/api.d.ts.map +1 -0
- package/lib/api.js +45 -0
- package/lib/api.js.map +1 -0
- package/lib/events.d.ts +43 -0
- package/lib/events.d.ts.map +1 -0
- package/lib/events.js +51 -0
- package/lib/events.js.map +1 -0
- package/lib/index.d.ts +49 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +60 -0
- package/lib/index.js.map +1 -0
- package/lib/listener.d.ts +71 -0
- package/lib/listener.d.ts.map +1 -0
- package/lib/listener.js +129 -0
- package/lib/listener.js.map +1 -0
- package/lib/state.d.ts +60 -0
- package/lib/state.d.ts.map +1 -0
- package/lib/state.js +74 -0
- package/lib/state.js.map +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @dsh-cc/session-cwd
|
|
2
|
+
|
|
3
|
+
Session-scoped working directory for DeepSeek Harness CC — the WS1 + WS2
|
|
4
|
+
foundation of the [worktree-session-isolation design](../../../docs/plans/worktree-session-isolation.md).
|
|
5
|
+
|
|
6
|
+
## What it owns
|
|
7
|
+
|
|
8
|
+
- **`worktree/entered` session event** (WS1): registered into the persistence
|
|
9
|
+
layer's `KNOWN_SESSION_EVENT_TYPES` at module load, so logs containing it
|
|
10
|
+
resume cleanly. Payload: `{ path }` — the new absolute session cwd.
|
|
11
|
+
`ExitWorktree` restores the previous directory through the same event
|
|
12
|
+
(last-wins fold).
|
|
13
|
+
- **Foldable state** (`src/state.ts`): `foldSessionCwd` folds the current cwd
|
|
14
|
+
out of the event log; a process-local `SessionCwdStore` overlays the live
|
|
15
|
+
value so reads are immediate and sessions stay independent in one process.
|
|
16
|
+
- **APIs** (`src/api.ts`): `getSessionCwd(agent)` (live overlay → durable fold
|
|
17
|
+
→ session header → fallback) and `setSessionCwd(agent, path)` (durable
|
|
18
|
+
event + overlay; absolute paths only).
|
|
19
|
+
- **Workspace boundary guard** (WS2, `src/listener.ts`): a
|
|
20
|
+
`tools/pre-execute` listener registered with `{ prepend: true }` — ahead of
|
|
21
|
+
the permission-rules waterfall — that checks every fs-family call's target
|
|
22
|
+
path against the session cwd. Out-of-workspace targets return
|
|
23
|
+
`{ kind: 'ask' }` ("Operation targets path outside session workspace") in
|
|
24
|
+
every mode except `bypassPermissions`, which allows (the existing audit
|
|
25
|
+
trail covers it). Non-fs tools, pathless calls, and agents without a
|
|
26
|
+
resolvable cwd pass through untouched; the guard is a pre-execute
|
|
27
|
+
convenience, not a hard security boundary.
|
|
28
|
+
|
|
29
|
+
## Consumers
|
|
30
|
+
|
|
31
|
+
- `tool-git-worktree`: `EnterWorktree` calls `setSessionCwd(worktreePath)`;
|
|
32
|
+
`ExitWorktree` restores `session.originalCwd` the same way.
|
|
33
|
+
- TUI driver: project/history resolution prefers the durable fold over the
|
|
34
|
+
boot-time header cwd.
|
|
35
|
+
- CC preset (`packages/preset/cc/agent.cordis.yml`) composes the plugin, which
|
|
36
|
+
installs the boundary guard.
|
|
37
|
+
|
|
38
|
+
## Tests
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd packages/workspace/session-cwd && npx vitest run tests/
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
(In a worktree whose root `node_modules` is a symlink the sandbox cannot write
|
|
45
|
+
into, add `--configLoader runner` to skip the `.vite-temp` bundle step.)
|
package/lib/api.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session cwd APIs. `getSessionCwd` resolves the authoritative session
|
|
3
|
+
* working directory (live overlay → durable `worktree/entered` fold →
|
|
4
|
+
* session header → fallback); `setSessionCwd` records the change durably by
|
|
5
|
+
* appending a `worktree/entered` event and updating the live overlay.
|
|
6
|
+
*
|
|
7
|
+
* @module @dsh-cc/session-cwd/api
|
|
8
|
+
*/
|
|
9
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
10
|
+
import { type SessionCwdStore } from './state.ts';
|
|
11
|
+
/** Options accepted by the cwd APIs; default to the shared process store. */
|
|
12
|
+
export interface SessionCwdOptions {
|
|
13
|
+
/** The store to read/write; defaults to the process-wide singleton. */
|
|
14
|
+
store?: SessionCwdStore;
|
|
15
|
+
/** Final fallback when neither the log nor the header records a cwd. */
|
|
16
|
+
fallback?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Read the authoritative session working directory. Resolution order: the
|
|
20
|
+
* live store overlay, the durable `worktree/entered` fold, the session
|
|
21
|
+
* header cwd, then the caller's fallback (defaulting to the process cwd).
|
|
22
|
+
* @param agent - the live agent whose session cwd is being read.
|
|
23
|
+
* @param options - store and fallback overrides.
|
|
24
|
+
* @returns the absolute session cwd.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getSessionCwd(agent: Agent, options?: SessionCwdOptions): string;
|
|
27
|
+
/**
|
|
28
|
+
* Change the session's working directory: append a durable `worktree/entered`
|
|
29
|
+
* event (last-wins fold) and update the live overlay. The path must be
|
|
30
|
+
* absolute and is normalized before writing.
|
|
31
|
+
* @param agent - the live agent whose session cwd is changing.
|
|
32
|
+
* @param path - the new absolute working directory.
|
|
33
|
+
* @param options - store override.
|
|
34
|
+
*/
|
|
35
|
+
export declare function setSessionCwd(agent: Agent, path: string, options?: SessionCwdOptions): void;
|
|
36
|
+
//# sourceMappingURL=api.d.ts.map
|
package/lib/api.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAEnD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,YAAY,CAAA;AAElE,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,uEAAuE;IACvE,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAOnF;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,IAAI,CAQ/F"}
|
package/lib/api.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session cwd APIs. `getSessionCwd` resolves the authoritative session
|
|
3
|
+
* working directory (live overlay → durable `worktree/entered` fold →
|
|
4
|
+
* session header → fallback); `setSessionCwd` records the change durably by
|
|
5
|
+
* appending a `worktree/entered` event and updating the live overlay.
|
|
6
|
+
*
|
|
7
|
+
* @module @dsh-cc/session-cwd/api
|
|
8
|
+
*/
|
|
9
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
10
|
+
import { appendWorktreeEntered } from "./events.js";
|
|
11
|
+
import { sessionCwdStore } from "./state.js";
|
|
12
|
+
/**
|
|
13
|
+
* Read the authoritative session working directory. Resolution order: the
|
|
14
|
+
* live store overlay, the durable `worktree/entered` fold, the session
|
|
15
|
+
* header cwd, then the caller's fallback (defaulting to the process cwd).
|
|
16
|
+
* @param agent - the live agent whose session cwd is being read.
|
|
17
|
+
* @param options - store and fallback overrides.
|
|
18
|
+
* @returns the absolute session cwd.
|
|
19
|
+
*/
|
|
20
|
+
export function getSessionCwd(agent, options = {}) {
|
|
21
|
+
const { store = sessionCwdStore, fallback } = options;
|
|
22
|
+
const sessionId = String(agent.session.id);
|
|
23
|
+
return store.resolve(sessionId, agent.session.events)
|
|
24
|
+
?? agent.session.header.cwd
|
|
25
|
+
?? fallback
|
|
26
|
+
?? process.cwd();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Change the session's working directory: append a durable `worktree/entered`
|
|
30
|
+
* event (last-wins fold) and update the live overlay. The path must be
|
|
31
|
+
* absolute and is normalized before writing.
|
|
32
|
+
* @param agent - the live agent whose session cwd is changing.
|
|
33
|
+
* @param path - the new absolute working directory.
|
|
34
|
+
* @param options - store override.
|
|
35
|
+
*/
|
|
36
|
+
export function setSessionCwd(agent, path, options = {}) {
|
|
37
|
+
if (!isAbsolute(path)) {
|
|
38
|
+
throw new TypeError(`session cwd must be an absolute path: "${path}"`);
|
|
39
|
+
}
|
|
40
|
+
const { store = sessionCwdStore } = options;
|
|
41
|
+
const normalized = resolve(path);
|
|
42
|
+
appendWorktreeEntered(agent.session, normalized);
|
|
43
|
+
store.set(String(agent.session.id), normalized);
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=api.js.map
|
package/lib/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAE/C,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,eAAe,EAAwB,MAAM,YAAY,CAAA;AAUlE;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,KAAY,EAAE,UAA6B,EAAE;IACzE,MAAM,EAAE,KAAK,GAAG,eAAe,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAA;IACrD,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC1C,OAAO,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;WAChD,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG;WACxB,QAAQ;WACR,OAAO,CAAC,GAAG,EAAE,CAAA;AACpB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,KAAY,EAAE,IAAY,EAAE,UAA6B,EAAE;IACvF,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,SAAS,CAAC,0CAA0C,IAAI,GAAG,CAAC,CAAA;IACxE,CAAC;IACD,MAAM,EAAE,KAAK,GAAG,eAAe,EAAE,GAAG,OAAO,CAAA;IAC3C,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChC,qBAAqB,CAAC,KAAK,CAAC,OAAO,EAAE,UAAU,CAAC,CAAA;IAChD,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,UAAU,CAAC,CAAA;AACjD,CAAC"}
|
package/lib/events.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable session cwd events. This module registers the `worktree/entered`
|
|
3
|
+
* session event type into the upstream `KNOWN_SESSION_EVENT_TYPES` set at
|
|
4
|
+
* load (required for persistence-layer compatibility: persistence refuses
|
|
5
|
+
* unknown event types unless the type is registered there). The set is typed
|
|
6
|
+
* `ReadonlySet` but is a live `Set` — same cross-repo registration pattern as
|
|
7
|
+
* `permission/mode` in `@dsh-cc/permission-rules`.
|
|
8
|
+
*
|
|
9
|
+
* The event payload is a local wire face (`WorktreeEnteredWire`) rather than
|
|
10
|
+
* `SessionEventMap['worktree/entered']` so both the CI pin (type absent from
|
|
11
|
+
* the typed map) and a newer local harness typecheck: `Session.append` is
|
|
12
|
+
* compiler-validated against the upstream event map, so appends go through a
|
|
13
|
+
* widened function face.
|
|
14
|
+
*
|
|
15
|
+
* @module @dsh-cc/session-cwd/events
|
|
16
|
+
*/
|
|
17
|
+
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
|
|
18
|
+
/** The session event type carrying a session cwd change. */
|
|
19
|
+
export declare const WORKTREE_ENTERED_EVENT = "worktree/entered";
|
|
20
|
+
/**
|
|
21
|
+
* The `worktree/entered` payload as written by this plugin: the new absolute
|
|
22
|
+
* session working directory. `ExitWorktree` restores the previous directory
|
|
23
|
+
* through the same event (last-wins fold), so one type covers both moves.
|
|
24
|
+
*/
|
|
25
|
+
export interface WorktreeEnteredEventData {
|
|
26
|
+
path: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fold the session's current working directory from its event log: the last
|
|
30
|
+
* `worktree/entered` path, or `undefined` when the session never recorded one
|
|
31
|
+
* (callers apply the process/session default).
|
|
32
|
+
* @param events - session events in log order (other event types are skipped).
|
|
33
|
+
* @returns the last recorded cwd, or `undefined` without one.
|
|
34
|
+
*/
|
|
35
|
+
export declare function foldSessionCwd(events: readonly SessionEvent[]): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Append one durable `worktree/entered` event. `path` must be absolute; it is
|
|
38
|
+
* written verbatim (callers normalize before calling).
|
|
39
|
+
* @param session - the session the cwd change belongs to.
|
|
40
|
+
* @param path - the new absolute session working directory.
|
|
41
|
+
*/
|
|
42
|
+
export declare function appendWorktreeEntered(session: Session, path: string): void;
|
|
43
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAErE,4DAA4D;AAC5D,eAAO,MAAM,sBAAsB,qBAAqB,CAEvD;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAA;CACb;AAaD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,GAAG,SAAS,CAQlF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAG1E"}
|
package/lib/events.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable session cwd events. This module registers the `worktree/entered`
|
|
3
|
+
* session event type into the upstream `KNOWN_SESSION_EVENT_TYPES` set at
|
|
4
|
+
* load (required for persistence-layer compatibility: persistence refuses
|
|
5
|
+
* unknown event types unless the type is registered there). The set is typed
|
|
6
|
+
* `ReadonlySet` but is a live `Set` — same cross-repo registration pattern as
|
|
7
|
+
* `permission/mode` in `@dsh-cc/permission-rules`.
|
|
8
|
+
*
|
|
9
|
+
* The event payload is a local wire face (`WorktreeEnteredWire`) rather than
|
|
10
|
+
* `SessionEventMap['worktree/entered']` so both the CI pin (type absent from
|
|
11
|
+
* the typed map) and a newer local harness typecheck: `Session.append` is
|
|
12
|
+
* compiler-validated against the upstream event map, so appends go through a
|
|
13
|
+
* widened function face.
|
|
14
|
+
*
|
|
15
|
+
* @module @dsh-cc/session-cwd/events
|
|
16
|
+
*/
|
|
17
|
+
import { KNOWN_SESSION_EVENT_TYPES } from '@deepseek-ai/dsh-session';
|
|
18
|
+
/** The session event type carrying a session cwd change. */
|
|
19
|
+
export const WORKTREE_ENTERED_EVENT = 'worktree/entered';
|
|
20
|
+
KNOWN_SESSION_EVENT_TYPES.add(WORKTREE_ENTERED_EVENT);
|
|
21
|
+
/** Read a log event through the extended `worktree/entered` face. */
|
|
22
|
+
function asEnteredEvent(event) {
|
|
23
|
+
return event;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fold the session's current working directory from its event log: the last
|
|
27
|
+
* `worktree/entered` path, or `undefined` when the session never recorded one
|
|
28
|
+
* (callers apply the process/session default).
|
|
29
|
+
* @param events - session events in log order (other event types are skipped).
|
|
30
|
+
* @returns the last recorded cwd, or `undefined` without one.
|
|
31
|
+
*/
|
|
32
|
+
export function foldSessionCwd(events) {
|
|
33
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
34
|
+
const event = asEnteredEvent(events[i]);
|
|
35
|
+
if (event.type === WORKTREE_ENTERED_EVENT && typeof event.data?.path === 'string') {
|
|
36
|
+
return event.data.path;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Append one durable `worktree/entered` event. `path` must be absolute; it is
|
|
43
|
+
* written verbatim (callers normalize before calling).
|
|
44
|
+
* @param session - the session the cwd change belongs to.
|
|
45
|
+
* @param path - the new absolute session working directory.
|
|
46
|
+
*/
|
|
47
|
+
export function appendWorktreeEntered(session, path) {
|
|
48
|
+
const data = { path };
|
|
49
|
+
session.append(WORKTREE_ENTERED_EVENT, data);
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAA;AAGpE,4DAA4D;AAC5D,MAAM,CAAC,MAAM,sBAAsB,GAAG,kBAAkB,CAEvD;AAAC,yBAAyC,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAA;AAiBvE,qEAAqE;AACrE,SAAS,cAAc,CAAC,KAAmB;IACzC,OAAO,KAAuC,CAAA;AAChD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,MAA+B;IAC5D,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,CAAA;QACxC,IAAI,KAAK,CAAC,IAAI,KAAK,sBAAsB,IAAI,OAAO,KAAK,CAAC,IAAI,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;YAClF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAA;QACxB,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAgB,EAAE,IAAY;IAClE,MAAM,IAAI,GAA6B,EAAE,IAAI,EAAE,CAC9C;IAAC,OAAO,CAAC,MAAkF,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAA;AAC5H,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped working directory for DeepSeek Harness CC (WS1 + WS2).
|
|
3
|
+
* Registers the `worktree/entered` session event type at load (required for
|
|
4
|
+
* persistence compatibility), maintains foldable session cwd state, exposes
|
|
5
|
+
* `getSessionCwd` / `setSessionCwd`, and installs the `tools/pre-execute`
|
|
6
|
+
* workspace boundary guard (prepend, ahead of permission-rules) that routes
|
|
7
|
+
* out-of-workspace fs targets to an approval ask.
|
|
8
|
+
*
|
|
9
|
+
* @module @dsh-cc/session-cwd
|
|
10
|
+
*/
|
|
11
|
+
import z from '@deepseek-ai/schemastery';
|
|
12
|
+
import './events.ts';
|
|
13
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
14
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
15
|
+
import { type SessionCwdStore } from './state.ts';
|
|
16
|
+
import { type SessionCwdOptions } from './api.ts';
|
|
17
|
+
export { WORKTREE_ENTERED_EVENT, foldSessionCwd, appendWorktreeEntered, type WorktreeEnteredEventData } from './events.ts';
|
|
18
|
+
export { SessionCwdStore, sessionCwdStore, foldSessionCwdState, reduceSessionCwdState, EMPTY_SESSION_CWD_STATE, type SessionCwdState } from './state.ts';
|
|
19
|
+
export { getSessionCwd, setSessionCwd, type SessionCwdOptions } from './api.ts';
|
|
20
|
+
export { registerSessionCwdBoundary, boundaryDecision, isFsTool, targetPathOf, isInsideWorkspace, readPermissionMode, DEFAULT_FS_TOOLS, type BoundaryListenerConfig, } from './listener.ts';
|
|
21
|
+
/** Plugin runtime configuration. All optional; the schema applies the defaults shown. */
|
|
22
|
+
export interface Config {
|
|
23
|
+
/** Whether the pre-execute workspace boundary guard is installed (default true). */
|
|
24
|
+
boundaryEnabled?: boolean;
|
|
25
|
+
/** Tool names treated as filesystem operations; defaults to the standard fs set. */
|
|
26
|
+
fsTools?: string[];
|
|
27
|
+
}
|
|
28
|
+
/** Runtime configuration schema. */
|
|
29
|
+
export declare const Config: z<Config>;
|
|
30
|
+
/** The plugin module face consumed by the Cordis loader. */
|
|
31
|
+
export declare const inject: readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* Plugin entry point: install the workspace boundary guard. The event-type
|
|
34
|
+
* registration happens at module load (the `./events.ts` side-effect import
|
|
35
|
+
* above), so even API-only consumers register the type.
|
|
36
|
+
* @param ctx - the Cordis context.
|
|
37
|
+
* @param config - runtime configuration.
|
|
38
|
+
*/
|
|
39
|
+
export declare function apply(ctx: Context, config?: Config): void;
|
|
40
|
+
/** Convenience face for host code that does not import the pure APIs directly. */
|
|
41
|
+
export declare const sessionCwd: {
|
|
42
|
+
/** Read the authoritative session working directory. */
|
|
43
|
+
get(agent: Agent, options?: SessionCwdOptions): string;
|
|
44
|
+
/** Change the session working directory (durable `worktree/entered` event). */
|
|
45
|
+
set(agent: Agent, path: string, options?: SessionCwdOptions): void;
|
|
46
|
+
/** The process-wide cwd overlay. */
|
|
47
|
+
store: SessionCwdStore;
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAIxC,OAAO,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAKnD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,YAAY,CAAA;AAClE,OAAO,EAAgC,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAE/E,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,qBAAqB,EAAE,KAAK,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAC1H,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAA;AACxJ,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAC/E,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,QAAQ,EACR,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,sBAAsB,GAC5B,MAAM,eAAe,CAAA;AAEtB,yFAAyF;AACzF,MAAM,WAAW,MAAM;IACrB,oFAAoF;IACpF,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB;AAED,oCAAoC;AACpC,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAG3B,CAAA;AAEF,4DAA4D;AAC5D,eAAO,MAAM,MAAM,EAAE,SAAS,MAAM,EAAO,CAAA;AAE3C;;;;;;GAMG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,MAAW,GAAG,IAAI,CAO7D;AAED,kFAAkF;AAClF,eAAO,MAAM,UAAU;IACrB,wDAAwD;eAC7C,KAAK,YAAY,iBAAiB,GAAG,MAAM;IAGtD,+EAA+E;eACpE,KAAK,QAAQ,MAAM,YAAY,iBAAiB,GAAG,IAAI;IAGlE,oCAAoC;WACV,eAAe;CAC1C,CAAA"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped working directory for DeepSeek Harness CC (WS1 + WS2).
|
|
3
|
+
* Registers the `worktree/entered` session event type at load (required for
|
|
4
|
+
* persistence compatibility), maintains foldable session cwd state, exposes
|
|
5
|
+
* `getSessionCwd` / `setSessionCwd`, and installs the `tools/pre-execute`
|
|
6
|
+
* workspace boundary guard (prepend, ahead of permission-rules) that routes
|
|
7
|
+
* out-of-workspace fs targets to an approval ask.
|
|
8
|
+
*
|
|
9
|
+
* @module @dsh-cc/session-cwd
|
|
10
|
+
*/
|
|
11
|
+
import z from '@deepseek-ai/schemastery';
|
|
12
|
+
// Side-effect module import: adds `worktree/entered` to the persistence
|
|
13
|
+
// layer's known-event-type set at plugin load — required even when only the
|
|
14
|
+
// pure APIs are consumed.
|
|
15
|
+
import "./events.js";
|
|
16
|
+
import { registerSessionCwdBoundary, } from "./listener.js";
|
|
17
|
+
import { sessionCwdStore } from "./state.js";
|
|
18
|
+
import { getSessionCwd, setSessionCwd } from "./api.js";
|
|
19
|
+
export { WORKTREE_ENTERED_EVENT, foldSessionCwd, appendWorktreeEntered } from "./events.js";
|
|
20
|
+
export { SessionCwdStore, sessionCwdStore, foldSessionCwdState, reduceSessionCwdState, EMPTY_SESSION_CWD_STATE } from "./state.js";
|
|
21
|
+
export { getSessionCwd, setSessionCwd } from "./api.js";
|
|
22
|
+
export { registerSessionCwdBoundary, boundaryDecision, isFsTool, targetPathOf, isInsideWorkspace, readPermissionMode, DEFAULT_FS_TOOLS, } from "./listener.js";
|
|
23
|
+
/** Runtime configuration schema. */
|
|
24
|
+
export const Config = z.object({
|
|
25
|
+
boundaryEnabled: z.boolean().default(true),
|
|
26
|
+
fsTools: z.array(z.string()),
|
|
27
|
+
});
|
|
28
|
+
/** The plugin module face consumed by the Cordis loader. */
|
|
29
|
+
export const inject = [];
|
|
30
|
+
/**
|
|
31
|
+
* Plugin entry point: install the workspace boundary guard. The event-type
|
|
32
|
+
* registration happens at module load (the `./events.ts` side-effect import
|
|
33
|
+
* above), so even API-only consumers register the type.
|
|
34
|
+
* @param ctx - the Cordis context.
|
|
35
|
+
* @param config - runtime configuration.
|
|
36
|
+
*/
|
|
37
|
+
export function apply(ctx, config = {}) {
|
|
38
|
+
if (config.boundaryEnabled ?? true) {
|
|
39
|
+
const listenerConfig = {
|
|
40
|
+
...config.fsTools !== undefined ? { fsTools: config.fsTools } : {},
|
|
41
|
+
};
|
|
42
|
+
registerSessionCwdBoundary(ctx, listenerConfig);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Convenience face for host code that does not import the pure APIs directly. */
|
|
46
|
+
export const sessionCwd = {
|
|
47
|
+
/** Read the authoritative session working directory. */
|
|
48
|
+
get(agent, options) {
|
|
49
|
+
return getSessionCwd(agent, options);
|
|
50
|
+
},
|
|
51
|
+
/** Change the session working directory (durable `worktree/entered` event). */
|
|
52
|
+
set(agent, path, options) {
|
|
53
|
+
setSessionCwd(agent, path, options);
|
|
54
|
+
},
|
|
55
|
+
/** The process-wide cwd overlay. */
|
|
56
|
+
store: sessionCwdStore,
|
|
57
|
+
};
|
|
58
|
+
// No default export: cordis-plugin-loader unwrapExports prefers `.default`, so
|
|
59
|
+
// a convenience object without `apply` fails preset mount as "invalid plugin".
|
|
60
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,CAAC,MAAM,0BAA0B,CAAA;AACxC,wEAAwE;AACxE,4EAA4E;AAC5E,0BAA0B;AAC1B,OAAO,aAAa,CAAA;AAGpB,OAAO,EACL,0BAA0B,GAE3B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,eAAe,EAAwB,MAAM,YAAY,CAAA;AAClE,OAAO,EAAE,aAAa,EAAE,aAAa,EAA0B,MAAM,UAAU,CAAA;AAE/E,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,qBAAqB,EAAiC,MAAM,aAAa,CAAA;AAC1H,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,uBAAuB,EAAwB,MAAM,YAAY,CAAA;AACxJ,OAAO,EAAE,aAAa,EAAE,aAAa,EAA0B,MAAM,UAAU,CAAA;AAC/E,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,QAAQ,EACR,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,GAEjB,MAAM,eAAe,CAAA;AAUtB,oCAAoC;AACpC,MAAM,CAAC,MAAM,MAAM,GAAc,CAAC,CAAC,MAAM,CAAC;IACxC,eAAe,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAC1C,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;CAC7B,CAAC,CAAA;AAEF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,MAAM,GAAsB,EAAE,CAAA;AAE3C;;;;;;GAMG;AACH,MAAM,UAAU,KAAK,CAAC,GAAY,EAAE,SAAiB,EAAE;IACrD,IAAI,MAAM,CAAC,eAAe,IAAI,IAAI,EAAE,CAAC;QACnC,MAAM,cAAc,GAA2B;YAC7C,GAAG,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE;SACnE,CAAA;QACD,0BAA0B,CAAC,GAAG,EAAE,cAAc,CAAC,CAAA;IACjD,CAAC;AACH,CAAC;AAED,kFAAkF;AAClF,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,wDAAwD;IACxD,GAAG,CAAC,KAAY,EAAE,OAA2B;QAC3C,OAAO,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IACtC,CAAC;IACD,+EAA+E;IAC/E,GAAG,CAAC,KAAY,EAAE,IAAY,EAAE,OAA2B;QACzD,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,CAAA;IACrC,CAAC;IACD,oCAAoC;IACpC,KAAK,EAAE,eAAkC;CAC1C,CAAA;AAED,+EAA+E;AAC/E,+EAA+E"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem boundary enforcement (WS2). A `tools/pre-execute` listener
|
|
3
|
+
* registered with `{ prepend: true }` — ahead of the permission-rules
|
|
4
|
+
* waterfall — that inspects every `fs`-family call, resolves its target path
|
|
5
|
+
* against the session cwd (WS1), and routes out-of-workspace targets to an
|
|
6
|
+
* approval ask unless the session is in `bypassPermissions` (allowed with the
|
|
7
|
+
* existing audit trail). The guard is a pre-execute convenience, not a hard
|
|
8
|
+
* security boundary: it does not intercept system calls.
|
|
9
|
+
*
|
|
10
|
+
* @module @dsh-cc/session-cwd/listener
|
|
11
|
+
*/
|
|
12
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
13
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
14
|
+
import type { PreToolDecision, ToolExecution } from '@dsh-cc/tools';
|
|
15
|
+
import { type SessionCwdStore } from './state.ts';
|
|
16
|
+
/** Listener configuration. */
|
|
17
|
+
export interface BoundaryListenerConfig {
|
|
18
|
+
/** Tool names treated as filesystem operations; defaults to {@link DEFAULT_FS_TOOLS}. */
|
|
19
|
+
fsTools?: readonly string[];
|
|
20
|
+
/** The store the listener resolves the session cwd from; defaults to the shared one. */
|
|
21
|
+
store?: SessionCwdStore;
|
|
22
|
+
}
|
|
23
|
+
/** The standard fs/edit tool set, matched case-insensitively. */
|
|
24
|
+
export declare const DEFAULT_FS_TOOLS: readonly string[];
|
|
25
|
+
/** Whether one tool name is an fs operation (case-insensitive match). */
|
|
26
|
+
export declare function isFsTool(name: string, fsTools?: readonly string[]): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Extract the target path of one call from its arguments. The first present
|
|
29
|
+
* string-valued target key wins; tools without a path argument yield
|
|
30
|
+
* `undefined` (the listener then passes them through untouched).
|
|
31
|
+
* @param exec - the pending tool execution.
|
|
32
|
+
* @returns the raw target path, or `undefined` when the call has none.
|
|
33
|
+
*/
|
|
34
|
+
export declare function targetPathOf(exec: ToolExecution): string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Whether a target path lies inside the workspace root (boundary is
|
|
37
|
+
* workspace-scoped, not path-scoped). Both sides are resolved, so `..`
|
|
38
|
+
* segments and relative targets cannot slip past.
|
|
39
|
+
* @param target - the raw target path.
|
|
40
|
+
* @param root - the workspace root (the session cwd).
|
|
41
|
+
* @returns true when the target is the root or inside it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isInsideWorkspace(target: string, root: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Read the session's current permission mode from its event log: the last
|
|
46
|
+
* `permission/mode` value, or `undefined` without one. Local wire-face fold —
|
|
47
|
+
* the mode is an upstream-extensible session event, not owned here.
|
|
48
|
+
* @param events - session events in log order.
|
|
49
|
+
* @returns the recorded mode, or `undefined`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function readPermissionMode(events: readonly SessionEvent[]): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* The boundary decision for one call, factored out of the listener for direct
|
|
54
|
+
* testing. `undefined` means "not my decision" (the caller must call `next`).
|
|
55
|
+
* @param exec - the pending tool execution.
|
|
56
|
+
* @param config - listener configuration.
|
|
57
|
+
* @returns an ask decision for out-of-workspace targets, `allow` for
|
|
58
|
+
* bypassPermissions, or `undefined` when the guard has no opinion.
|
|
59
|
+
*/
|
|
60
|
+
export declare function boundaryDecision(exec: ToolExecution, config?: BoundaryListenerConfig): PreToolDecision | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Register the boundary guard on `tools/pre-execute` with `{ prepend: true }`
|
|
63
|
+
* so it runs before the permission-rules waterfall and every later listener.
|
|
64
|
+
* Non-fs tools, pathless calls, agents without a resolvable cwd, and
|
|
65
|
+
* in-workspace targets pass through to `next` untouched.
|
|
66
|
+
* @param ctx - the Cordis context.
|
|
67
|
+
* @param config - listener configuration.
|
|
68
|
+
* @returns a disposer that unregisters the listener.
|
|
69
|
+
*/
|
|
70
|
+
export declare function registerSessionCwdBoundary(ctx: Context, config?: BoundaryListenerConfig): () => void;
|
|
71
|
+
//# sourceMappingURL=listener.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AACnE,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,YAAY,CAAA;AAElE,8BAA8B;AAC9B,MAAM,WAAW,sBAAsB;IACrC,yFAAyF;IACzF,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,wFAAwF;IACxF,KAAK,CAAC,EAAE,eAAe,CAAA;CACxB;AAED,iEAAiE;AACjE,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAS7C,CAAA;AAED,yEAAyE;AACzE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,SAAS,MAAM,EAAqB,GAAG,OAAO,CAG7F;AAKD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAQpE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAOvE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,GAAG,SAAS,CAQtF;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,aAAa,EACnB,MAAM,GAAE,sBAA2B,GAClC,eAAe,GAAG,SAAS,CAe7B;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,sBAA2B,GAAG,MAAM,IAAI,CAMxG"}
|
package/lib/listener.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem boundary enforcement (WS2). A `tools/pre-execute` listener
|
|
3
|
+
* registered with `{ prepend: true }` — ahead of the permission-rules
|
|
4
|
+
* waterfall — that inspects every `fs`-family call, resolves its target path
|
|
5
|
+
* against the session cwd (WS1), and routes out-of-workspace targets to an
|
|
6
|
+
* approval ask unless the session is in `bypassPermissions` (allowed with the
|
|
7
|
+
* existing audit trail). The guard is a pre-execute convenience, not a hard
|
|
8
|
+
* security boundary: it does not intercept system calls.
|
|
9
|
+
*
|
|
10
|
+
* @module @dsh-cc/session-cwd/listener
|
|
11
|
+
*/
|
|
12
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
13
|
+
import { sessionCwdStore } from "./state.js";
|
|
14
|
+
/** The standard fs/edit tool set, matched case-insensitively. */
|
|
15
|
+
export const DEFAULT_FS_TOOLS = [
|
|
16
|
+
'edit',
|
|
17
|
+
'write',
|
|
18
|
+
'read',
|
|
19
|
+
'multi_edit',
|
|
20
|
+
'notebook_edit',
|
|
21
|
+
'str_replace_editor',
|
|
22
|
+
'glob',
|
|
23
|
+
'grep',
|
|
24
|
+
];
|
|
25
|
+
/** Whether one tool name is an fs operation (case-insensitive match). */
|
|
26
|
+
export function isFsTool(name, fsTools = DEFAULT_FS_TOOLS) {
|
|
27
|
+
const normalized = name.toLowerCase();
|
|
28
|
+
return fsTools.some(tool => tool.toLowerCase() === normalized);
|
|
29
|
+
}
|
|
30
|
+
/** Argument keys inspected, in order, for a call's target path. */
|
|
31
|
+
const TARGET_KEYS = ['file_path', 'notebook_path', 'path', 'workdir', 'cwd'];
|
|
32
|
+
/**
|
|
33
|
+
* Extract the target path of one call from its arguments. The first present
|
|
34
|
+
* string-valued target key wins; tools without a path argument yield
|
|
35
|
+
* `undefined` (the listener then passes them through untouched).
|
|
36
|
+
* @param exec - the pending tool execution.
|
|
37
|
+
* @returns the raw target path, or `undefined` when the call has none.
|
|
38
|
+
*/
|
|
39
|
+
export function targetPathOf(exec) {
|
|
40
|
+
const args = exec.arguments;
|
|
41
|
+
if (args === undefined || typeof args !== 'object')
|
|
42
|
+
return undefined;
|
|
43
|
+
for (const key of TARGET_KEYS) {
|
|
44
|
+
const value = args[key];
|
|
45
|
+
if (typeof value === 'string' && value.length > 0)
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether a target path lies inside the workspace root (boundary is
|
|
52
|
+
* workspace-scoped, not path-scoped). Both sides are resolved, so `..`
|
|
53
|
+
* segments and relative targets cannot slip past.
|
|
54
|
+
* @param target - the raw target path.
|
|
55
|
+
* @param root - the workspace root (the session cwd).
|
|
56
|
+
* @returns true when the target is the root or inside it.
|
|
57
|
+
*/
|
|
58
|
+
export function isInsideWorkspace(target, root) {
|
|
59
|
+
// Relative targets resolve against the workspace root, not the process cwd.
|
|
60
|
+
const targetPath = isAbsolute(target) ? resolve(target) : resolve(root, target);
|
|
61
|
+
const rootPath = resolve(root);
|
|
62
|
+
if (targetPath === rootPath)
|
|
63
|
+
return true;
|
|
64
|
+
const rel = relative(rootPath, targetPath);
|
|
65
|
+
return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read the session's current permission mode from its event log: the last
|
|
69
|
+
* `permission/mode` value, or `undefined` without one. Local wire-face fold —
|
|
70
|
+
* the mode is an upstream-extensible session event, not owned here.
|
|
71
|
+
* @param events - session events in log order.
|
|
72
|
+
* @returns the recorded mode, or `undefined`.
|
|
73
|
+
*/
|
|
74
|
+
export function readPermissionMode(events) {
|
|
75
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
76
|
+
const event = events[i];
|
|
77
|
+
if (event.type === 'permission/mode' && typeof event.data?.mode === 'string') {
|
|
78
|
+
return event.data.mode;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The boundary decision for one call, factored out of the listener for direct
|
|
85
|
+
* testing. `undefined` means "not my decision" (the caller must call `next`).
|
|
86
|
+
* @param exec - the pending tool execution.
|
|
87
|
+
* @param config - listener configuration.
|
|
88
|
+
* @returns an ask decision for out-of-workspace targets, `allow` for
|
|
89
|
+
* bypassPermissions, or `undefined` when the guard has no opinion.
|
|
90
|
+
*/
|
|
91
|
+
export function boundaryDecision(exec, config = {}) {
|
|
92
|
+
if (!isFsTool(exec.name, config.fsTools ?? DEFAULT_FS_TOOLS))
|
|
93
|
+
return undefined;
|
|
94
|
+
const target = targetPathOf(exec);
|
|
95
|
+
if (target === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
const agent = exec.agent;
|
|
98
|
+
if (agent === undefined)
|
|
99
|
+
return undefined;
|
|
100
|
+
const store = config.store ?? sessionCwdStore;
|
|
101
|
+
const root = store.resolve(String(agent.session.id), agent.session.events) ?? agent.session.header.cwd;
|
|
102
|
+
if (root === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
if (isInsideWorkspace(target, root))
|
|
105
|
+
return undefined;
|
|
106
|
+
if (readPermissionMode(agent.session.events) === 'bypassPermissions')
|
|
107
|
+
return { kind: 'allow' };
|
|
108
|
+
return {
|
|
109
|
+
kind: 'ask',
|
|
110
|
+
reason: `Operation targets path outside session workspace: ${resolve(target)} (workspace: ${resolve(root)})`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Register the boundary guard on `tools/pre-execute` with `{ prepend: true }`
|
|
115
|
+
* so it runs before the permission-rules waterfall and every later listener.
|
|
116
|
+
* Non-fs tools, pathless calls, agents without a resolvable cwd, and
|
|
117
|
+
* in-workspace targets pass through to `next` untouched.
|
|
118
|
+
* @param ctx - the Cordis context.
|
|
119
|
+
* @param config - listener configuration.
|
|
120
|
+
* @returns a disposer that unregisters the listener.
|
|
121
|
+
*/
|
|
122
|
+
export function registerSessionCwdBoundary(ctx, config = {}) {
|
|
123
|
+
const dispose = ctx.on('tools/pre-execute', async (exec, next) => {
|
|
124
|
+
const decision = boundaryDecision(exec, config);
|
|
125
|
+
return decision ?? next();
|
|
126
|
+
}, { prepend: true });
|
|
127
|
+
return dispose;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=listener.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listener.js","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAI9D,OAAO,EAAE,eAAe,EAAwB,MAAM,YAAY,CAAA;AAUlE,iEAAiE;AACjE,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IACjD,MAAM;IACN,OAAO;IACP,MAAM;IACN,YAAY;IACZ,eAAe;IACf,oBAAoB;IACpB,MAAM;IACN,MAAM;CACP,CAAA;AAED,yEAAyE;AACzE,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,UAA6B,gBAAgB;IAClF,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;IACrC,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,UAAU,CAAC,CAAA;AAChE,CAAC;AAED,mEAAmE;AACnE,MAAM,WAAW,GAAsB,CAAC,WAAW,EAAE,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,CAAA;AAE/F;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,IAAmB;IAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,SAAgD,CAAA;IAClE,IAAI,IAAI,KAAK,SAAS,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IACpE,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;QACvB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,KAAK,CAAA;IACjE,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc,EAAE,IAAY;IAC5D,4EAA4E;IAC5E,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IAC/E,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B,IAAI,UAAU,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IACxC,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAA;IAC1C,OAAO,GAAG,KAAK,EAAE,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;AACtF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAA+B;IAChE,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAA2D,CAAA;QACjF,IAAI,KAAK,CAAC,IAAI,KAAK,iBAAiB,IAAI,OAAO,KAAK,CAAC,IAAI,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7E,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAA;QACxB,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAmB,EACnB,SAAiC,EAAE;IAEnC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,IAAI,gBAAgB,CAAC;QAAE,OAAO,SAAS,CAAA;IAC9E,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAA;IACjC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAA;IACxB,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACzC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,eAAe,CAAA;IAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAA;IACtG,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACxC,IAAI,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC;QAAE,OAAO,SAAS,CAAA;IACrD,IAAI,kBAAkB,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,mBAAmB;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;IAC9F,OAAO;QACL,IAAI,EAAE,KAAK;QACX,MAAM,EAAE,qDAAqD,OAAO,CAAC,MAAM,CAAC,gBAAgB,OAAO,CAAC,IAAI,CAAC,GAAG;KAC7G,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,0BAA0B,CAAC,GAAY,EAAE,SAAiC,EAAE;IAC1F,MAAM,OAAO,GAAG,GAAG,CAAC,EAAE,CAAC,mBAAmB,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAA4B,EAAE;QACzF,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAC/C,OAAO,QAAQ,IAAI,IAAI,EAAE,CAAA;IAC3B,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;IACrB,OAAO,OAAO,CAAA;AAChB,CAAC"}
|
package/lib/state.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foldable session cwd state. The durable state is the session event log
|
|
3
|
+
* itself (`worktree/entered`, last-wins); this module folds it into a typed
|
|
4
|
+
* state value and adds a process-local live overlay for the current session
|
|
5
|
+
* (the same pattern as tool-git-worktree's active worktree session), which
|
|
6
|
+
* lets `getSessionCwd` read back before the event round-trips and keeps
|
|
7
|
+
* sessions independent inside one process.
|
|
8
|
+
*
|
|
9
|
+
* @module @dsh-cc/session-cwd/state
|
|
10
|
+
*/
|
|
11
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
12
|
+
/** The folded cwd state of one session: the current cwd, when known. */
|
|
13
|
+
export interface SessionCwdState {
|
|
14
|
+
readonly cwd?: string;
|
|
15
|
+
}
|
|
16
|
+
/** The empty state: no cwd recorded in this fold. */
|
|
17
|
+
export declare const EMPTY_SESSION_CWD_STATE: SessionCwdState;
|
|
18
|
+
/**
|
|
19
|
+
* Reduce one event into the state. Only `worktree/entered` events change the
|
|
20
|
+
* state; everything else passes through unchanged (a fold, not a filter).
|
|
21
|
+
* @param state - the state so far.
|
|
22
|
+
* @param event - the next session event in log order.
|
|
23
|
+
* @returns the next state.
|
|
24
|
+
*/
|
|
25
|
+
export declare function reduceSessionCwdState(state: SessionCwdState, event: SessionEvent): SessionCwdState;
|
|
26
|
+
/**
|
|
27
|
+
* Fold the cwd state over a session event log, in log order.
|
|
28
|
+
* @param events - session events in log order.
|
|
29
|
+
* @returns the folded state (empty when no event applied).
|
|
30
|
+
*/
|
|
31
|
+
export declare function foldSessionCwdState(events: readonly SessionEvent[]): SessionCwdState;
|
|
32
|
+
/**
|
|
33
|
+
* Process-local cwd overlay keyed by session id. Writes are applied on
|
|
34
|
+
* `setSessionCwd`; reads fall through to the durable event-log fold so a
|
|
35
|
+
* restarted process still resolves the cwd from the persisted session.
|
|
36
|
+
*/
|
|
37
|
+
export declare class SessionCwdStore {
|
|
38
|
+
private readonly live;
|
|
39
|
+
/**
|
|
40
|
+
* Record the live cwd for one session.
|
|
41
|
+
* @param sessionId - the session key.
|
|
42
|
+
* @param cwd - the new absolute cwd.
|
|
43
|
+
*/
|
|
44
|
+
set(sessionId: string, cwd: string): void;
|
|
45
|
+
/** The live cwd for one session, or `undefined` without a local write. */
|
|
46
|
+
get(sessionId: string): string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the authoritative cwd for one session: the live overlay first,
|
|
49
|
+
* then the durable event-log fold.
|
|
50
|
+
* @param sessionId - the session key.
|
|
51
|
+
* @param events - the session's event log, in log order.
|
|
52
|
+
* @returns the resolved cwd, or `undefined` when nothing is recorded.
|
|
53
|
+
*/
|
|
54
|
+
resolve(sessionId: string, events: readonly SessionEvent[]): string | undefined;
|
|
55
|
+
/** Drop the live overlay for one session (used on session dispose). */
|
|
56
|
+
clear(sessionId: string): void;
|
|
57
|
+
}
|
|
58
|
+
/** The process-wide cwd overlay shared by the plugin, APIs, and listener. */
|
|
59
|
+
export declare const sessionCwdStore: SessionCwdStore;
|
|
60
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAG5D,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,qDAAqD;AACrD,eAAO,MAAM,uBAAuB,EAAE,eAAoB,CAAA;AAE1D;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,GAAG,eAAe,CAIlG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,eAAe,CAIpF;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IAEjD;;;;OAIG;IACH,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzC,0EAA0E;IAC1E,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI1C;;;;;;OAMG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,GAAG,SAAS;IAI/E,uEAAuE;IACvE,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;CAG/B;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,iBAAwB,CAAA"}
|
package/lib/state.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foldable session cwd state. The durable state is the session event log
|
|
3
|
+
* itself (`worktree/entered`, last-wins); this module folds it into a typed
|
|
4
|
+
* state value and adds a process-local live overlay for the current session
|
|
5
|
+
* (the same pattern as tool-git-worktree's active worktree session), which
|
|
6
|
+
* lets `getSessionCwd` read back before the event round-trips and keeps
|
|
7
|
+
* sessions independent inside one process.
|
|
8
|
+
*
|
|
9
|
+
* @module @dsh-cc/session-cwd/state
|
|
10
|
+
*/
|
|
11
|
+
import { foldSessionCwd } from "./events.js";
|
|
12
|
+
/** The empty state: no cwd recorded in this fold. */
|
|
13
|
+
export const EMPTY_SESSION_CWD_STATE = {};
|
|
14
|
+
/**
|
|
15
|
+
* Reduce one event into the state. Only `worktree/entered` events change the
|
|
16
|
+
* state; everything else passes through unchanged (a fold, not a filter).
|
|
17
|
+
* @param state - the state so far.
|
|
18
|
+
* @param event - the next session event in log order.
|
|
19
|
+
* @returns the next state.
|
|
20
|
+
*/
|
|
21
|
+
export function reduceSessionCwdState(state, event) {
|
|
22
|
+
const wire = event;
|
|
23
|
+
if (wire.type !== 'worktree/entered' || typeof wire.data?.path !== 'string')
|
|
24
|
+
return state;
|
|
25
|
+
return { cwd: wire.data.path };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Fold the cwd state over a session event log, in log order.
|
|
29
|
+
* @param events - session events in log order.
|
|
30
|
+
* @returns the folded state (empty when no event applied).
|
|
31
|
+
*/
|
|
32
|
+
export function foldSessionCwdState(events) {
|
|
33
|
+
let state = EMPTY_SESSION_CWD_STATE;
|
|
34
|
+
for (const event of events)
|
|
35
|
+
state = reduceSessionCwdState(state, event);
|
|
36
|
+
return state;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Process-local cwd overlay keyed by session id. Writes are applied on
|
|
40
|
+
* `setSessionCwd`; reads fall through to the durable event-log fold so a
|
|
41
|
+
* restarted process still resolves the cwd from the persisted session.
|
|
42
|
+
*/
|
|
43
|
+
export class SessionCwdStore {
|
|
44
|
+
live = new Map();
|
|
45
|
+
/**
|
|
46
|
+
* Record the live cwd for one session.
|
|
47
|
+
* @param sessionId - the session key.
|
|
48
|
+
* @param cwd - the new absolute cwd.
|
|
49
|
+
*/
|
|
50
|
+
set(sessionId, cwd) {
|
|
51
|
+
this.live.set(sessionId, cwd);
|
|
52
|
+
}
|
|
53
|
+
/** The live cwd for one session, or `undefined` without a local write. */
|
|
54
|
+
get(sessionId) {
|
|
55
|
+
return this.live.get(sessionId);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the authoritative cwd for one session: the live overlay first,
|
|
59
|
+
* then the durable event-log fold.
|
|
60
|
+
* @param sessionId - the session key.
|
|
61
|
+
* @param events - the session's event log, in log order.
|
|
62
|
+
* @returns the resolved cwd, or `undefined` when nothing is recorded.
|
|
63
|
+
*/
|
|
64
|
+
resolve(sessionId, events) {
|
|
65
|
+
return this.live.get(sessionId) ?? foldSessionCwd(events);
|
|
66
|
+
}
|
|
67
|
+
/** Drop the live overlay for one session (used on session dispose). */
|
|
68
|
+
clear(sessionId) {
|
|
69
|
+
this.live.delete(sessionId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** The process-wide cwd overlay shared by the plugin, APIs, and listener. */
|
|
73
|
+
export const sessionCwdStore = new SessionCwdStore();
|
|
74
|
+
//# sourceMappingURL=state.js.map
|
package/lib/state.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAO5C,qDAAqD;AACrD,MAAM,CAAC,MAAM,uBAAuB,GAAoB,EAAE,CAAA;AAE1D;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAsB,EAAE,KAAmB;IAC/E,MAAM,IAAI,GAAG,KAA+D,CAAA;IAC5E,IAAI,IAAI,CAAC,IAAI,KAAK,kBAAkB,IAAI,OAAO,IAAI,CAAC,IAAI,EAAE,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IACzF,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAA+B;IACjE,IAAI,KAAK,GAAG,uBAAuB,CAAA;IACnC,KAAK,MAAM,KAAK,IAAI,MAAM;QAAE,KAAK,GAAG,qBAAqB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IACvE,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,eAAe;IACT,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAA;IAEjD;;;;OAIG;IACH,GAAG,CAAC,SAAiB,EAAE,GAAW;QAChC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;IAC/B,CAAC;IAED,0EAA0E;IAC1E,GAAG,CAAC,SAAiB;QACnB,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACjC,CAAC;IAED;;;;;;OAMG;IACH,OAAO,CAAC,SAAiB,EAAE,MAA+B;QACxD,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,cAAc,CAAC,MAAM,CAAC,CAAA;IAC3D,CAAC;IAED,uEAAuE;IACvE,KAAK,CAAC,SAAiB;QACrB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAC7B,CAAC;CACF;AAED,6EAA6E;AAC7E,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsh-cc/session-cwd",
|
|
3
|
+
"description": "Session-scoped working directory state for DeepSeek Harness CC: durable worktree/entered session events, foldable cwd state, getSessionCwd/setSessionCwd APIs, and a pre-execute workspace boundary guard",
|
|
4
|
+
"version": "0.5.0",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/dsh-cc/dsh-cc.git",
|
|
8
|
+
"directory": "packages/workspace/session-cwd"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"default": "./lib/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./src/*": "./src/*",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib"
|
|
21
|
+
],
|
|
22
|
+
"license": "Apache-2.0",
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@deepseek-ai/cordis": ">=0.1.1-rc.2",
|
|
25
|
+
"@deepseek-ai/dsh-agent": ">=0.1.1-rc.2",
|
|
26
|
+
"@deepseek-ai/dsh-llm": ">=0.1.1-rc.2",
|
|
27
|
+
"@deepseek-ai/dsh-session": ">=0.1.1-rc.2",
|
|
28
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
29
|
+
"@dsh-cc/tools": "^0.5.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@deepseek-ai/cordis": "link:../../../../deepseek-harness/vendor/cordis",
|
|
33
|
+
"@deepseek-ai/dsh-agent": "link:../../../../deepseek-harness/packages/core/agent",
|
|
34
|
+
"@deepseek-ai/dsh-llm": "link:../../../../deepseek-harness/packages/llm/llm",
|
|
35
|
+
"@deepseek-ai/dsh-session": "link:../../../../deepseek-harness/packages/core/session",
|
|
36
|
+
"@deepseek-ai/schemastery": "link:../../../../deepseek-harness/vendor/schemastery",
|
|
37
|
+
"@dsh-cc/tools": "link:../../core/tools"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|