dsh-over-ssh 0.1.1 → 0.2.1
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 +10 -8
- package/agents/agent-manifest.json +6 -6
- package/agents/linux-amd64/dsh-over-ssh-agent +0 -0
- package/agents/linux-arm64/dsh-over-ssh-agent +0 -0
- package/lib/client.js +567 -246
- package/lib/file-references.js +20 -8
- package/lib/fs.js +141 -41
- package/lib/index.js +3268 -875
- package/lib/types/src/config.d.ts +76 -0
- package/lib/types/src/host/app/backend.d.ts +35 -2
- package/lib/types/src/host/domain/roots/session-roots.d.ts +23 -0
- package/lib/types/src/host/domain/session/session-admin.d.ts +5 -0
- package/lib/types/src/host/infra/transport/forward.d.ts +75 -0
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +5 -0
- package/lib/types/src/host/infra/transport/transport.d.ts +26 -0
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +18 -1
- package/lib/types/src/wiring/helpers.d.ts +10 -1
- package/package.json +1 -1
|
@@ -105,7 +105,26 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
105
105
|
* 立刻把自己的条目淘汰掉,行为既不是"关缓存"也不是"缓存"。
|
|
106
106
|
*/
|
|
107
107
|
readCacheTotalMaxBytes: z<number, number, "defined">;
|
|
108
|
+
/**
|
|
109
|
+
* 端口转发的空闲回收窗口(毫秒);`0` 关闭回收。
|
|
110
|
+
*
|
|
111
|
+
* 判据是"没有任何活跃连接且距最后一次活动超过这个窗口"。没有回收的话,一个被 agent 起过、
|
|
112
|
+
* 之后没人用的转发会一直占着本地端口(用户实测关心的问题)。默认 30 分钟:比一次开发会话的
|
|
113
|
+
* 间隔长,比"忘了关一整天"短。
|
|
114
|
+
*/
|
|
115
|
+
forwardIdleTimeoutMs: z<number, number, "defined">;
|
|
108
116
|
terminalStateDir: z<string, string, "plain">;
|
|
117
|
+
/**
|
|
118
|
+
* 是否给 `remote_exec` / `remote_exec_background` 套**远端写入围栏**(Tier 2 的 Landlock)。
|
|
119
|
+
*
|
|
120
|
+
* **默认关**是刻意的:围栏只放行 workspace 根与本会话添加的目录,构建工具往 `$HOME/.cache`、
|
|
121
|
+
* `/tmp` 写缓存时会被拒——默认开启会把正常命令弄坏。关着的时候 shell 命令能碰到 SSH 用户能碰的
|
|
122
|
+
* 一切,这与"添加目录"的文件边界是两件事,工具描述里如实写着。
|
|
123
|
+
*
|
|
124
|
+
* 打开之后,`remote_exec` 只能写:命令起始的 workspace 根、本会话添加的可写根。Tier 1(无代理)
|
|
125
|
+
* 没有 Landlock 能力,此项对它们无效。
|
|
126
|
+
*/
|
|
127
|
+
execWriteFence: z<boolean, boolean, "defined">;
|
|
109
128
|
transferChunkBytes: z<number, number, "defined">;
|
|
110
129
|
/** Chunks in flight at once during a transfer. 1 restores the sequential behaviour. */
|
|
111
130
|
transferConcurrency: z<number, number, "defined">;
|
|
@@ -172,7 +191,26 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
172
191
|
* 立刻把自己的条目淘汰掉,行为既不是"关缓存"也不是"缓存"。
|
|
173
192
|
*/
|
|
174
193
|
readCacheTotalMaxBytes: z<number, number, "defined">;
|
|
194
|
+
/**
|
|
195
|
+
* 端口转发的空闲回收窗口(毫秒);`0` 关闭回收。
|
|
196
|
+
*
|
|
197
|
+
* 判据是"没有任何活跃连接且距最后一次活动超过这个窗口"。没有回收的话,一个被 agent 起过、
|
|
198
|
+
* 之后没人用的转发会一直占着本地端口(用户实测关心的问题)。默认 30 分钟:比一次开发会话的
|
|
199
|
+
* 间隔长,比"忘了关一整天"短。
|
|
200
|
+
*/
|
|
201
|
+
forwardIdleTimeoutMs: z<number, number, "defined">;
|
|
175
202
|
terminalStateDir: z<string, string, "plain">;
|
|
203
|
+
/**
|
|
204
|
+
* 是否给 `remote_exec` / `remote_exec_background` 套**远端写入围栏**(Tier 2 的 Landlock)。
|
|
205
|
+
*
|
|
206
|
+
* **默认关**是刻意的:围栏只放行 workspace 根与本会话添加的目录,构建工具往 `$HOME/.cache`、
|
|
207
|
+
* `/tmp` 写缓存时会被拒——默认开启会把正常命令弄坏。关着的时候 shell 命令能碰到 SSH 用户能碰的
|
|
208
|
+
* 一切,这与"添加目录"的文件边界是两件事,工具描述里如实写着。
|
|
209
|
+
*
|
|
210
|
+
* 打开之后,`remote_exec` 只能写:命令起始的 workspace 根、本会话添加的可写根。Tier 1(无代理)
|
|
211
|
+
* 没有 Landlock 能力,此项对它们无效。
|
|
212
|
+
*/
|
|
213
|
+
execWriteFence: z<boolean, boolean, "defined">;
|
|
176
214
|
transferChunkBytes: z<number, number, "defined">;
|
|
177
215
|
/** Chunks in flight at once during a transfer. 1 restores the sequential behaviour. */
|
|
178
216
|
transferConcurrency: z<number, number, "defined">;
|
|
@@ -285,7 +323,26 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
285
323
|
* 立刻把自己的条目淘汰掉,行为既不是"关缓存"也不是"缓存"。
|
|
286
324
|
*/
|
|
287
325
|
readCacheTotalMaxBytes: z<number, number, "defined">;
|
|
326
|
+
/**
|
|
327
|
+
* 端口转发的空闲回收窗口(毫秒);`0` 关闭回收。
|
|
328
|
+
*
|
|
329
|
+
* 判据是"没有任何活跃连接且距最后一次活动超过这个窗口"。没有回收的话,一个被 agent 起过、
|
|
330
|
+
* 之后没人用的转发会一直占着本地端口(用户实测关心的问题)。默认 30 分钟:比一次开发会话的
|
|
331
|
+
* 间隔长,比"忘了关一整天"短。
|
|
332
|
+
*/
|
|
333
|
+
forwardIdleTimeoutMs: z<number, number, "defined">;
|
|
288
334
|
terminalStateDir: z<string, string, "plain">;
|
|
335
|
+
/**
|
|
336
|
+
* 是否给 `remote_exec` / `remote_exec_background` 套**远端写入围栏**(Tier 2 的 Landlock)。
|
|
337
|
+
*
|
|
338
|
+
* **默认关**是刻意的:围栏只放行 workspace 根与本会话添加的目录,构建工具往 `$HOME/.cache`、
|
|
339
|
+
* `/tmp` 写缓存时会被拒——默认开启会把正常命令弄坏。关着的时候 shell 命令能碰到 SSH 用户能碰的
|
|
340
|
+
* 一切,这与"添加目录"的文件边界是两件事,工具描述里如实写着。
|
|
341
|
+
*
|
|
342
|
+
* 打开之后,`remote_exec` 只能写:命令起始的 workspace 根、本会话添加的可写根。Tier 1(无代理)
|
|
343
|
+
* 没有 Landlock 能力,此项对它们无效。
|
|
344
|
+
*/
|
|
345
|
+
execWriteFence: z<boolean, boolean, "defined">;
|
|
289
346
|
transferChunkBytes: z<number, number, "defined">;
|
|
290
347
|
/** Chunks in flight at once during a transfer. 1 restores the sequential behaviour. */
|
|
291
348
|
transferConcurrency: z<number, number, "defined">;
|
|
@@ -352,7 +409,26 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
352
409
|
* 立刻把自己的条目淘汰掉,行为既不是"关缓存"也不是"缓存"。
|
|
353
410
|
*/
|
|
354
411
|
readCacheTotalMaxBytes: z<number, number, "defined">;
|
|
412
|
+
/**
|
|
413
|
+
* 端口转发的空闲回收窗口(毫秒);`0` 关闭回收。
|
|
414
|
+
*
|
|
415
|
+
* 判据是"没有任何活跃连接且距最后一次活动超过这个窗口"。没有回收的话,一个被 agent 起过、
|
|
416
|
+
* 之后没人用的转发会一直占着本地端口(用户实测关心的问题)。默认 30 分钟:比一次开发会话的
|
|
417
|
+
* 间隔长,比"忘了关一整天"短。
|
|
418
|
+
*/
|
|
419
|
+
forwardIdleTimeoutMs: z<number, number, "defined">;
|
|
355
420
|
terminalStateDir: z<string, string, "plain">;
|
|
421
|
+
/**
|
|
422
|
+
* 是否给 `remote_exec` / `remote_exec_background` 套**远端写入围栏**(Tier 2 的 Landlock)。
|
|
423
|
+
*
|
|
424
|
+
* **默认关**是刻意的:围栏只放行 workspace 根与本会话添加的目录,构建工具往 `$HOME/.cache`、
|
|
425
|
+
* `/tmp` 写缓存时会被拒——默认开启会把正常命令弄坏。关着的时候 shell 命令能碰到 SSH 用户能碰的
|
|
426
|
+
* 一切,这与"添加目录"的文件边界是两件事,工具描述里如实写着。
|
|
427
|
+
*
|
|
428
|
+
* 打开之后,`remote_exec` 只能写:命令起始的 workspace 根、本会话添加的可写根。Tier 1(无代理)
|
|
429
|
+
* 没有 Landlock 能力,此项对它们无效。
|
|
430
|
+
*/
|
|
431
|
+
execWriteFence: z<boolean, boolean, "defined">;
|
|
356
432
|
transferChunkBytes: z<number, number, "defined">;
|
|
357
433
|
/** Chunks in flight at once during a transfer. 1 restores the sequential behaviour. */
|
|
358
434
|
transferConcurrency: z<number, number, "defined">;
|
|
@@ -69,6 +69,17 @@ export interface BackendWrite {
|
|
|
69
69
|
}
|
|
70
70
|
/** How a backend confines writes, mirroring the core's `SandboxMode`. */
|
|
71
71
|
export type BackendSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
72
|
+
/**
|
|
73
|
+
* Per-call write fence; structurally compatible with the platform's `SandboxExecutionPolicy`.
|
|
74
|
+
*
|
|
75
|
+
* 用结构化类型而不是 `import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox'`:app 层的端口
|
|
76
|
+
* 不该依赖平台包(`tests/architecture.test.ts` 的可测性规则)。只有真正把写入交给平台沙箱的
|
|
77
|
+
* {@link PlatformLocalBackend} 会读它,其余后端各有一套自己的围栏。
|
|
78
|
+
*/
|
|
79
|
+
export interface BackendWritePolicy {
|
|
80
|
+
readonly mode: BackendSandboxMode;
|
|
81
|
+
readonly workspaceRoot: string;
|
|
82
|
+
}
|
|
72
83
|
/** One backend serving one root. */
|
|
73
84
|
export interface Backend {
|
|
74
85
|
/** Stable id, also the namespace prefix of every target key it produces. */
|
|
@@ -113,8 +124,11 @@ export interface Backend {
|
|
|
113
124
|
* Write text, atomically where the backend can.
|
|
114
125
|
*
|
|
115
126
|
* @param expectedVersion - when non-empty the write must fail if the current version differs.
|
|
127
|
+
* @param policy - the per-call sandbox policy, when the caller has one. Only backends that delegate to a
|
|
128
|
+
* platform sandbox consume it ({@link PlatformLocalBackend}); the others enforce their own fence and
|
|
129
|
+
* ignore it. Structural on purpose — the app layer must not import platform types.
|
|
116
130
|
*/
|
|
117
|
-
writeText(absolute: string, text: string, expectedVersion: string | undefined, signal?: AbortSignal): Promise<BackendWrite>;
|
|
131
|
+
writeText(absolute: string, text: string, expectedVersion: string | undefined, signal?: AbortSignal, policy?: BackendWritePolicy): Promise<BackendWrite>;
|
|
118
132
|
/**
|
|
119
133
|
* Apply a unified diff to one file, atomically.
|
|
120
134
|
*
|
|
@@ -131,9 +145,10 @@ export interface Backend {
|
|
|
131
145
|
* apply the **same** three-state semantics as that fallback (not found / ambiguous / replaceAll) and
|
|
132
146
|
* must honour `expectedVersion` across its own read-modify-write.
|
|
133
147
|
*/
|
|
134
|
-
editText?(absolute: string, oldString: string, newString: string, replaceAll: boolean, expectedVersion: string | undefined, signal?: AbortSignal): Promise<BackendWrite & {
|
|
148
|
+
editText?(absolute: string, oldString: string, newString: string, replaceAll: boolean, expectedVersion: string | undefined, signal?: AbortSignal, policy?: BackendWritePolicy): Promise<BackendWrite & {
|
|
135
149
|
readonly replaced: number;
|
|
136
150
|
}>;
|
|
151
|
+
/** Per-call write fence, structurally compatible with the platform's `SandboxExecutionPolicy`. */
|
|
137
152
|
/**
|
|
138
153
|
* Create a directory, and its parents when asked (FR-2.2.2: the file tree's 「新建目录」).
|
|
139
154
|
*
|
|
@@ -171,6 +186,24 @@ export interface Backend {
|
|
|
171
186
|
* caller's `755` is octal digits, not decimal seven hundred and fifty-five, and that confusion is silent.
|
|
172
187
|
*/
|
|
173
188
|
chmod?(absolute: string, mode: string, signal?: AbortSignal): Promise<void>;
|
|
189
|
+
/**
|
|
190
|
+
* Create a symbolic link at `linkPath` pointing at `target`.
|
|
191
|
+
*
|
|
192
|
+
* Optional, and it must **refuse** an existing path rather than replace it: silently turning a real file
|
|
193
|
+
* into a symlink is data loss, and "there used to be a file here" is not recoverable from the link.
|
|
194
|
+
*
|
|
195
|
+
* @param target - what the link points at (kept verbatim; a relative target resolves against the link's dir).
|
|
196
|
+
* @param linkPath - the link to create.
|
|
197
|
+
* @param signal - cancellation.
|
|
198
|
+
*/
|
|
199
|
+
symlink?(target: string, linkPath: string, signal?: AbortSignal): Promise<void>;
|
|
200
|
+
/**
|
|
201
|
+
* Read a symbolic link's target, or `undefined` when the path is not a link.
|
|
202
|
+
*
|
|
203
|
+
* `undefined` (not an error) is the honest answer for "not a link": the caller asked a question whose
|
|
204
|
+
* answer is "no". Only a real failure throws.
|
|
205
|
+
*/
|
|
206
|
+
readlink?(absolute: string, signal?: AbortSignal): Promise<string | undefined>;
|
|
174
207
|
/** Host path a child process in this backend's world can open, if any. */
|
|
175
208
|
processPath?(absolute: string): string | undefined;
|
|
176
209
|
/** `file:` URL for this path in the backend's world. */
|
|
@@ -56,6 +56,29 @@ export interface SessionIdentity {
|
|
|
56
56
|
/** The asking session's fork ancestors, nearest first. */
|
|
57
57
|
readonly ancestors?: readonly string[] | undefined;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* How much of one root counts as **the session's workspace** for a `workspace-write` mutation.
|
|
61
|
+
*
|
|
62
|
+
* 产品决定(2026-11-04,用户明确):**「添加到会话的目录」= 该目录临时成为 workspace 的一部分,可写。**
|
|
63
|
+
* 在此之前插件对 `workspace-write` 只做两件事(`read-only` 拒绝、`root.writable === false` 拒绝),写入
|
|
64
|
+
* 范围实际是"本会话能看见的任何可写根"——比平台声明宽,且和"添加目录"的语义没有关系。
|
|
65
|
+
*
|
|
66
|
+
* 三个取值:
|
|
67
|
+
* - `whole-root`:整个根都属于 workspace(会话自己添加的目录;或映射到 workspace 的远端根——远端根与
|
|
68
|
+
* 本地锚点是 1:1 的,锚点就是 workspace);
|
|
69
|
+
* - `workspace-only`:根包含 workspace,但只有 `workspaceRoot` 之内可写(平台自己的语义:`target` 必须
|
|
70
|
+
* canonicalize 到 `workspaceRoot` 或其临时目录下);
|
|
71
|
+
* - `deny`:既不是会话添加的目录,也不是 workspace 本身。
|
|
72
|
+
*
|
|
73
|
+
* @param root - 一个已注册的根。
|
|
74
|
+
* @param workspaceRoot - 平台为本次调用解析出的 workspace 根(绝对本地路径)。
|
|
75
|
+
* @param cwd - 调用方的会话工作目录,用于判定历史条目(ADR-0013 之前只有 `workspaces`)。
|
|
76
|
+
* @param identity - 调用方的会话身份,用于判定本会话添加的条目。
|
|
77
|
+
* @returns 该根在 `workspace-write` 下的可写范围。
|
|
78
|
+
*/
|
|
79
|
+
export declare function workspaceWriteArea(root: RootRef, workspaceRoot: string | undefined, cwd: string | undefined, identity?: SessionIdentity): WorkspaceWriteArea;
|
|
80
|
+
/** See {@link workspaceWriteArea}. */
|
|
81
|
+
export type WorkspaceWriteArea = 'whole-root' | 'workspace-only' | 'deny';
|
|
59
82
|
/** The target shape the tools consume; kept structural so this module imports no integration code. */
|
|
60
83
|
export interface SessionRootTarget {
|
|
61
84
|
readonly connectionId: string;
|
|
@@ -158,6 +158,11 @@ export interface DeleteDecision {
|
|
|
158
158
|
* 默认不可被单独删除);
|
|
159
159
|
* 5. selecting a parent **always** takes its whole descendant subtree along, ordered **children first**.
|
|
160
160
|
*
|
|
161
|
+
* Rule 4 has one exception: an **orphan** sub-session — its parent is not in the inventory, or the parent is
|
|
162
|
+
* itself unreadable — can never be deleted "with its parent", because the parent does not exist to delete. Left
|
|
163
|
+
* refused, such a row is visible in the panel and permanently undeletable (审计 2026-11-05 session P2). It is
|
|
164
|
+
* therefore treated as a root: deletable on its own, and its own subtree still comes along by rule 5.
|
|
165
|
+
*
|
|
161
166
|
* Rule 5 is unconditional on purpose. It used to be an opt-in `includeSubsessions` switch, and that was the
|
|
162
167
|
* wrong shape: leaving a parent's children behind orphans their logs — the parent is gone, so nothing
|
|
163
168
|
* navigates to them, and the platform's listing hides them by `parentId` nesting, so the bytes stay on disk
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 端口转发:请求 / 句柄 / 转发器三个形状,以及请求校验。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独一个模块:两个 Tier 都要实现同一份契约,而调用方(Lead 之后接进 `Transport`)只需要这一份。
|
|
5
|
+
* 放在 `infra/transport/` 顶层而不是 `ssh/` 下,是因为它是**跨适配器**的端口:`alias-forward` 与
|
|
6
|
+
* `ssh2-forward` 各自实现它,谁都不该 import 对方。
|
|
7
|
+
*
|
|
8
|
+
* 本地端一律绑 `127.0.0.1`:转发出去的端口是给本机浏览器用的(DSH 侧栏只能开本地 URL),绑到
|
|
9
|
+
* `0.0.0.0` 等于把一个远端服务暴露给整个局域网。这是一条安全决定,不是默认值凑巧。
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-over-ssh/infra/transport/forward
|
|
12
|
+
*/
|
|
13
|
+
/** 本地监听地址;固定环回,见模块注释。 */
|
|
14
|
+
export declare const FORWARD_LOCAL_HOST = "127.0.0.1";
|
|
15
|
+
/** 一次转发请求。 */
|
|
16
|
+
export interface ForwardRequest {
|
|
17
|
+
/** 远端要连的主机(相对**远端**解析;`localhost` 指远端自己)。 */
|
|
18
|
+
readonly remoteHost: string;
|
|
19
|
+
/** 远端端口,1..65535。 */
|
|
20
|
+
readonly remotePort: number;
|
|
21
|
+
/** 本地端口;省略则自动选一个空闲端口。 */
|
|
22
|
+
readonly localPort?: number;
|
|
23
|
+
}
|
|
24
|
+
/** 一次已建立的转发。 */
|
|
25
|
+
export interface ForwardHandle {
|
|
26
|
+
/** 真实的本地端口(自动选择时也一定有值,调用方拿它开 URL)。 */
|
|
27
|
+
readonly localPort: number;
|
|
28
|
+
readonly remoteHost: string;
|
|
29
|
+
readonly remotePort: number;
|
|
30
|
+
/** 幂等:第二次调用是 no-op,不抛错。 */
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* 订阅「这条转发已经关掉」,无论关闭的**原因**是什么。
|
|
34
|
+
*
|
|
35
|
+
* 为什么需要它:空闲回收(默认 30 分钟)、连接退役、`closeAll` 都能让一条转发消失,而调用方手里只有
|
|
36
|
+
* 一个 handle;没有这个回调时,调用方的台账(`remote_forward list`)会一直列出已经关掉的转发。
|
|
37
|
+
*
|
|
38
|
+
* 可选:只有实现了回收的传输才提供。回调恰好触发一次;可重复订阅。
|
|
39
|
+
*
|
|
40
|
+
* @param listener - 收到关闭通知(含回收)时调用。
|
|
41
|
+
*/
|
|
42
|
+
readonly onClosed?: (listener: () => void) => void;
|
|
43
|
+
}
|
|
44
|
+
/** 一个连接的转发台账。 */
|
|
45
|
+
export interface Forwarder {
|
|
46
|
+
/** 建立一条转发;失败必须不留本地监听、也不留子进程。 */
|
|
47
|
+
forward(req: ForwardRequest, signal?: AbortSignal): Promise<ForwardHandle>;
|
|
48
|
+
/** 当前活着的转发(只读快照语义:调用方不得依赖它的身份稳定)。 */
|
|
49
|
+
readonly forwards: readonly ForwardHandle[];
|
|
50
|
+
/** 收掉全部转发,供 dispose 使用;幂等。 */
|
|
51
|
+
closeAll(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 校验并规整一个转发请求。
|
|
55
|
+
*
|
|
56
|
+
* **为什么在拼 argv 之前就校验**:`-L` 的规格串是 `local:remoteHost:remotePort`,靠冒号分隔。主机名里
|
|
57
|
+
* 多一个冒号(`host:80`、裸 `::1`)会把参数解析成完全不同的目标;空白与逗号同理。ssh 对这些的报错
|
|
58
|
+
* 文案不稳定,映射不成可操作的错误码,所以这里主动拒绝。
|
|
59
|
+
*
|
|
60
|
+
* 方括号 IPv6(`[::1]`)是**允许**的:那是 ssh 自己的写法,`-L` 里的冒号被方括号包住,解析无歧义。
|
|
61
|
+
*
|
|
62
|
+
* @param request - 调用方的原始请求。
|
|
63
|
+
* @returns 去掉首尾空白、字段已校验的请求。
|
|
64
|
+
* @throws SshDevError `CONNECTION_INVALID` 当主机或端口不合法。
|
|
65
|
+
*/
|
|
66
|
+
export declare function normalizeForwardRequest(request: ForwardRequest): ForwardRequest;
|
|
67
|
+
/**
|
|
68
|
+
* 一行描述一条转发,日志与错误文案共用。
|
|
69
|
+
*
|
|
70
|
+
* `localPort` 缺席时写 `?`:那时端口还没选出来,写 0 会被误读成"转发到 0 号端口"。
|
|
71
|
+
*
|
|
72
|
+
* @param forward - 部分句柄(至少要有远端目标)。
|
|
73
|
+
* @returns 形如 `127.0.0.1:8080 -> db:5432` 的一行。
|
|
74
|
+
*/
|
|
75
|
+
export declare function forwardLabel(forward: Pick<ForwardHandle, 'remoteHost' | 'remotePort'> & Partial<Pick<ForwardHandle, 'localPort'>>): string;
|
|
@@ -27,6 +27,7 @@ import type { HostKeyPolicy } from '../../../domain/roots/connection.ts';
|
|
|
27
27
|
import { SshDevError } from '../../../domain/errors.ts';
|
|
28
28
|
import { type ExecRequest, type ExecResult, type ReadFileRangeRequest, type ReadFileRequest, type RemoteFileChunk, type RemoteWriteOutcome, type RemoveRequest, type RenameRequest, type Transport, type WriteFileRangeRequest, type WriteFileRequest } from '../transport.ts';
|
|
29
29
|
import { TransportBase } from '../transport-base.ts';
|
|
30
|
+
import type { ForwardHandle, ForwardRequest } from '../forward.ts';
|
|
30
31
|
/** Authentication material for one hop. */
|
|
31
32
|
export interface Ssh2Auth {
|
|
32
33
|
readonly password?: string;
|
|
@@ -227,6 +228,10 @@ export declare class Ssh2Transport extends TransportBase implements Transport {
|
|
|
227
228
|
*/
|
|
228
229
|
connect(signal?: AbortSignal): Promise<void>;
|
|
229
230
|
/** Close the session. Idempotent. */
|
|
231
|
+
/** {@inheritDoc Transport.forward} */
|
|
232
|
+
forward(request: ForwardRequest, signal?: AbortSignal): Promise<ForwardHandle>;
|
|
233
|
+
/** {@inheritDoc Transport.closeForwards} */
|
|
234
|
+
closeForwards(): Promise<void>;
|
|
230
235
|
dispose(): Promise<void>;
|
|
231
236
|
/** Run one remote command over a channel of the shared session. */
|
|
232
237
|
exec(request: ExecRequest, signal?: AbortSignal): Promise<ExecResult>;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module dsh-over-ssh/infra/transport/transport
|
|
9
9
|
*/
|
|
10
|
+
import type { ForwardHandle, ForwardRequest } from './forward.ts';
|
|
10
11
|
import type { RemoteCapabilities } from '../../domain/capability.ts';
|
|
11
12
|
/** Lifecycle of one transport instance. */
|
|
12
13
|
export type TransportState = 'idle' | 'connecting' | 'ready' | 'lost';
|
|
@@ -35,6 +36,15 @@ export interface ExecRequest {
|
|
|
35
36
|
* *encoded* payload into the file, i.e. silent corruption).
|
|
36
37
|
*/
|
|
37
38
|
readonly stdinBase64?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Landlock write fence: when non-empty, the command may only write under these roots.
|
|
41
|
+
*
|
|
42
|
+
* **Tier 2 only.** The agent builds the fence before `exec` and refuses with `REMOTE_UNSUPPORTED` when it
|
|
43
|
+
* cannot (no Landlock, missing root); a Tier 1 transport has no fence at all and must refuse the field
|
|
44
|
+
* rather than silently run unfenced — "the fence was requested but skipped" is a security lie, not a
|
|
45
|
+
* degradation. Surfaced through `limits.execWriteFence`.
|
|
46
|
+
*/
|
|
47
|
+
readonly writeRoots?: readonly string[];
|
|
38
48
|
}
|
|
39
49
|
/** One finished remote command. */
|
|
40
50
|
export interface ExecResult {
|
|
@@ -458,6 +468,22 @@ export interface Transport {
|
|
|
458
468
|
* hunk matching rules, which is where a patch applier goes wrong.
|
|
459
469
|
*/
|
|
460
470
|
patchFile?(request: PatchFileRequest, signal?: AbortSignal): Promise<PatchFileResult>;
|
|
471
|
+
/**
|
|
472
|
+
* 建立一条本地 → 远端的 TCP 转发(端口转发)。
|
|
473
|
+
*
|
|
474
|
+
* 可选:**系统 ssh 的两条路径(Tier 1 与默认 Tier 2)都实现了它**,`ssh2` 传输没有——那一条会诚实
|
|
475
|
+
* 地不声明这个方法,调用方据此报"该连接不支持端口转发",而不是给一个连不上的端口。
|
|
476
|
+
*
|
|
477
|
+
* 只绑 `127.0.0.1`:转发出来的端口是给本机浏览器/客户端用的,绑 `0.0.0.0` 等于把一个远端服务暴露给
|
|
478
|
+
* 整个局域网。要给别人用请走 SSH 自己的 `GatewayPorts` 配置,而不是让插件默认放宽。
|
|
479
|
+
*
|
|
480
|
+
* @param request - 远端目标与可选本地端口。
|
|
481
|
+
* @param signal - 取消建立过程(已建立的转发不会因此关闭)。
|
|
482
|
+
* @returns 句柄,`localPort` 一定是真实端口(自动选择时也回填)。
|
|
483
|
+
*/
|
|
484
|
+
forward?(request: ForwardRequest, signal?: AbortSignal): Promise<ForwardHandle>;
|
|
485
|
+
/** 关掉这条传输上的所有转发(dispose 时调用);幂等。 */
|
|
486
|
+
closeForwards?(): Promise<void>;
|
|
461
487
|
/** Native delete (`fs.remove`); absent on Tier 1, which uses its shell primitive instead. */
|
|
462
488
|
removePath?(request: RemovePathRequest, signal?: AbortSignal): Promise<void>;
|
|
463
489
|
/** Native rename/move (`fs.rename`); absent on Tier 1, which uses its shell primitive. */
|
|
@@ -284,7 +284,24 @@ export declare class RoutedFsProvider extends FileSystem {
|
|
|
284
284
|
*/
|
|
285
285
|
private syncSessionRoots;
|
|
286
286
|
private routedTarget;
|
|
287
|
-
/**
|
|
287
|
+
/**
|
|
288
|
+
* Enforce the per-call policy and the root's own writability.
|
|
289
|
+
*
|
|
290
|
+
* Three gates, in order:
|
|
291
|
+
* 1. `read-only` denies every mutation (the platform's own contract);
|
|
292
|
+
* 2. a root registered `writable: false` denies;
|
|
293
|
+
* 3. under **`workspace-write`**, the target must be inside the session's workspace — where "workspace"
|
|
294
|
+
* includes the directories **this session added** (product decision 2026-11-04: 「添加到会话的目录」=
|
|
295
|
+
* 该目录临时成为 workspace 的一部分). Before that decision the plugin admitted every writable root it
|
|
296
|
+
* could route, which made the mode wider than the platform's own `fs-sandbox` fence
|
|
297
|
+
* (`workspace-write` = "canonicalizes under `workspaceRoot` or the platform temp area").
|
|
298
|
+
*
|
|
299
|
+
* A call without a per-call policy keeps the historical behaviour (the mode comes from
|
|
300
|
+
* {@link sandboxMode}); the area rule needs `workspaceRoot`, which only the policy carries.
|
|
301
|
+
*
|
|
302
|
+
* @param routed - the resolved target.
|
|
303
|
+
* @param policy - the per-call sandbox policy the tool layer resolved.
|
|
304
|
+
*/
|
|
288
305
|
private assertWritable;
|
|
289
306
|
/**
|
|
290
307
|
* Previous text content, when it is readable text inside the cap; `undefined` otherwise.
|
|
@@ -60,7 +60,16 @@ export declare function resolveAuth(ctx: Context, profile: ConnectionProfile): P
|
|
|
60
60
|
export declare function resolvePasswordSecret(credentials: CredentialsLike, key: string): Promise<string | undefined>;
|
|
61
61
|
/** Whether the optional `ssh2` dependency can be loaded in this installation. */
|
|
62
62
|
export declare function isSsh2Available(): boolean;
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Expand a leading `~` to the user's home directory.
|
|
65
|
+
*
|
|
66
|
+
* **One implementation.** 这里原来自己写了一份,和 `host/domain/roots/ssh-config.ts` 的那份并存;两份对
|
|
67
|
+
* `~\\seg` 的处理并不同(`join` 折叠分隔符,字符串拼接不会),同一个配置在"连接凭据路径"与"ssh-config
|
|
68
|
+
* 导入"两条路上可能落到不同路径。domain 的那份是纯函数(`home` 由调用方传入),全仓只保留它。
|
|
69
|
+
*
|
|
70
|
+
* @param path - the configured path, possibly starting with `~`.
|
|
71
|
+
* @returns the absolute path, or the input unchanged when it has no leading `~`.
|
|
72
|
+
*/
|
|
64
73
|
export declare function expandHome(path: string): string;
|
|
65
74
|
/** One configured connection. */
|
|
66
75
|
export interface ConnectionEntry {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-over-ssh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "SSH remote development for DeepSeek Harness: connections (key/password/ProxyJump/ssh config), remote files without sync, remote terminal, remote workspaces, remote @-completion, upload/download.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|