@tiphareth/dsh-hardssh 0.1.2-alpha → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +49 -25
  3. package/SKILLS.md +140 -0
  4. package/lib/base/index.js +95 -0
  5. package/lib/client.js +1165 -1329
  6. package/lib/fs.js +307 -416
  7. package/lib/index.js +5428 -2991
  8. package/lib/ledger-B-LXlftp.js +43 -0
  9. package/lib/ledger-D2ezq1iW.js +376 -0
  10. package/lib/model-Cp7f70Mb.js +5 -0
  11. package/lib/registry-CViuzKYI.js +421 -0
  12. package/lib/subprocess.js +92 -549
  13. package/lib/types/backend.d.ts +73 -78
  14. package/lib/types/base/capability.d.ts +4 -0
  15. package/lib/types/base/ledger-router.d.ts +42 -26
  16. package/lib/types/base/ledger.d.ts +30 -19
  17. package/lib/types/base/model.d.ts +17 -8
  18. package/lib/types/base/plugin.d.ts +11 -0
  19. package/lib/types/base/registry.d.ts +28 -31
  20. package/lib/types/base/router.d.ts +0 -7
  21. package/lib/types/client/api.d.ts +8 -8
  22. package/lib/types/client/connect-host.d.ts +13 -0
  23. package/lib/types/client/index.d.ts +6 -4
  24. package/lib/types/client/locales.d.ts +15 -1
  25. package/lib/types/client/session-connect-gate.d.ts +71 -0
  26. package/lib/types/client/ssh/api.d.ts +11 -9
  27. package/lib/types/client/ssh/apply.d.ts +14 -8
  28. package/lib/types/client/ssh/locales.d.ts +8 -1
  29. package/lib/types/client/ssh/ops-tab.d.ts +34 -0
  30. package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
  31. package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
  32. package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
  33. package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
  34. package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
  35. package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
  36. package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
  37. package/lib/types/client/ssh/session-target.d.ts +17 -0
  38. package/lib/types/client/state.d.ts +4 -1
  39. package/lib/types/client/workspace-panel-entry.d.ts +30 -0
  40. package/lib/types/client/workspace-panel.d.ts +24 -0
  41. package/lib/types/client-http.d.ts +77 -6
  42. package/lib/types/core.d.ts +5 -16
  43. package/lib/types/fs.d.ts +18 -25
  44. package/lib/types/index.d.ts +138 -9
  45. package/lib/types/ledger.d.ts +16 -111
  46. package/lib/types/protocol.d.ts +0 -5
  47. package/lib/types/providers/index.d.ts +6 -3
  48. package/lib/types/providers/local/provider.d.ts +83 -23
  49. package/lib/types/providers/ssh/provider.d.ts +24 -57
  50. package/lib/types/remote/environment.d.ts +2 -0
  51. package/lib/types/remote/remote-fs.d.ts +12 -1
  52. package/lib/types/remote/remote-process.d.ts +22 -1
  53. package/lib/types/remote/remote-subprocess.d.ts +13 -1
  54. package/lib/types/remote/remote-terminal.d.ts +4 -0
  55. package/lib/types/remote-search.d.ts +3 -3
  56. package/lib/types/routes.d.ts +36 -8
  57. package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
  58. package/lib/types/runtime/workspace-core.d.ts +73 -38
  59. package/lib/types/runtime/workspace-migration.d.ts +113 -0
  60. package/lib/types/ssh/connection/lease.d.ts +23 -0
  61. package/lib/types/ssh/connection/manager.d.ts +359 -0
  62. package/lib/types/ssh/connection/pool.d.ts +8 -1
  63. package/lib/types/ssh/engine.d.ts +97 -267
  64. package/lib/types/ssh/known-hosts.d.ts +22 -5
  65. package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
  66. package/lib/types/ssh/plugin.d.ts +10 -15
  67. package/lib/types/ssh/protocol.d.ts +18 -0
  68. package/lib/types/ssh/routes.d.ts +15 -7
  69. package/lib/types/ssh/sftp/service.d.ts +216 -0
  70. package/lib/types/ssh/store.d.ts +20 -8
  71. package/lib/types/ssh/terminal/service.d.ts +49 -0
  72. package/lib/types/ssh/tunnel/service.d.ts +26 -0
  73. package/lib/types/ssh/vault.d.ts +51 -4
  74. package/lib/types/subprocess.d.ts +13 -21
  75. package/lib/types/switch/switch-fs.d.ts +52 -2
  76. package/lib/types/switch/switch-subprocess.d.ts +32 -3
  77. package/lib/types/tools.d.ts +7 -6
  78. package/lib/types/workspace-tool-ops.d.ts +33 -0
  79. package/lib/types/workspace.d.ts +18 -0
  80. package/lib/vault-3gpWct2Q.js +559 -0
  81. package/lib/workspace.js +2 -0
  82. package/package.json +36 -11
  83. package/scripts/export-legacy-workspaces.mjs +136 -0
  84. package/src/backend.ts +155 -483
  85. package/src/base/capability.ts +4 -0
  86. package/src/base/ledger-router.ts +214 -65
  87. package/src/base/ledger.ts +175 -48
  88. package/src/base/model.ts +18 -8
  89. package/src/base/plugin.ts +15 -4
  90. package/src/base/registry.ts +49 -41
  91. package/src/base/router.ts +0 -8
  92. package/src/client/api.ts +8 -36
  93. package/src/client/connect-host.ts +199 -0
  94. package/src/client/directory-flow.tsx +91 -36
  95. package/src/client/index.ts +56 -53
  96. package/src/client/locales.ts +14 -0
  97. package/src/client/session-connect-gate.ts +163 -0
  98. package/src/client/ssh/api.ts +245 -108
  99. package/src/client/ssh/apply.ts +23 -24
  100. package/src/client/ssh/locales.ts +16 -2
  101. package/src/client/ssh/ops-tab.tsx +79 -0
  102. package/src/client/ssh/panel/ClusterTab.tsx +9 -20
  103. package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
  104. package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
  105. package/src/client/ssh/panel/SshPanel.tsx +81 -77
  106. package/src/client/ssh/panel/TerminalTab.tsx +18 -37
  107. package/src/client/ssh/panel/TransferTab.tsx +43 -33
  108. package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
  109. package/src/client/ssh/panel/panel.module.css +90 -127
  110. package/src/client/ssh/session-target.ts +69 -0
  111. package/src/client/state.ts +9 -1
  112. package/src/client/workspace-badges.ts +99 -99
  113. package/src/client/workspace-panel-entry.tsx +91 -0
  114. package/src/client/workspace-panel.tsx +227 -0
  115. package/src/client/workspace.module.css +76 -2
  116. package/src/client-http.ts +118 -18
  117. package/src/core.ts +37 -47
  118. package/src/fs.ts +137 -85
  119. package/src/index.ts +535 -244
  120. package/src/ledger.ts +62 -416
  121. package/src/protocol.ts +0 -6
  122. package/src/providers/index.ts +8 -4
  123. package/src/providers/local/provider.ts +259 -87
  124. package/src/providers/ssh/provider.ts +158 -155
  125. package/src/remote/environment.ts +22 -1
  126. package/src/remote/remote-fs.ts +123 -34
  127. package/src/remote/remote-process.ts +265 -203
  128. package/src/remote/remote-subprocess.ts +61 -18
  129. package/src/remote/remote-terminal.ts +106 -21
  130. package/src/remote-search.ts +122 -26
  131. package/src/routes.ts +416 -395
  132. package/src/runtime/dsh-capabilities.ts +19 -0
  133. package/src/runtime/workspace-core.ts +171 -88
  134. package/src/runtime/workspace-migration.ts +472 -0
  135. package/src/ssh/connection/lease.ts +35 -0
  136. package/src/ssh/connection/manager.ts +1083 -0
  137. package/src/ssh/connection/pool.ts +341 -275
  138. package/src/ssh/engine.ts +269 -1477
  139. package/src/ssh/known-hosts.ts +42 -18
  140. package/src/ssh/local-transfer-policy.ts +83 -0
  141. package/src/ssh/plugin.ts +20 -19
  142. package/src/ssh/protocol.ts +22 -1
  143. package/src/ssh/routes.ts +419 -187
  144. package/src/ssh/sftp/service.ts +967 -0
  145. package/src/ssh/store.ts +169 -72
  146. package/src/ssh/terminal/service.ts +177 -0
  147. package/src/ssh/tools.ts +42 -20
  148. package/src/ssh/tunnel/service.ts +217 -0
  149. package/src/ssh/vault.ts +245 -91
  150. package/src/subprocess.ts +88 -71
  151. package/src/switch/switch-fs.ts +141 -10
  152. package/src/switch/switch-subprocess.ts +80 -6
  153. package/src/tools.ts +217 -221
  154. package/src/workspace-tool-ops.ts +101 -0
  155. package/src/workspace.ts +52 -0
  156. package/lib/environment-BL1jddfB.js +0 -449
  157. package/lib/switch-fs-CAJpFY9C.js +0 -193
  158. package/lib/switch-fs-RrZtG2gv.js +0 -210
  159. package/lib/types/client/manager-button.d.ts +0 -32
  160. package/lib/types/client/ssh/mount.d.ts +0 -13
  161. package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
  162. package/lib/types/client/ssh/panel/controller.d.ts +0 -23
  163. package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
  164. package/lib/types/client/workspace-gate.d.ts +0 -15
  165. package/lib/types/remote-runner.d.ts +0 -83
  166. package/lib/types/seam-state.d.ts +0 -69
  167. package/src/client/manager-button.tsx +0 -269
  168. package/src/client/ssh/mount.tsx +0 -83
  169. package/src/client/ssh/panel/HostsTab.tsx +0 -236
  170. package/src/client/ssh/panel/controller.ts +0 -48
  171. package/src/client/ssh/sidebar-entry.ts +0 -123
  172. package/src/client/workspace-gate.ts +0 -232
  173. package/src/remote-runner.ts +0 -201
  174. package/src/seam-state.ts +0 -185
@@ -1,101 +1,50 @@
1
1
  /**
2
- * The SSH engine: a per-alias persistent connection pool (ssh2) with
2
+ * The SSH engine facade: a per-alias persistent connection pool (ssh2) with
3
3
  * multi-hop jump support, command execution, PTY shells, SFTP transfers,
4
4
  * local port-forward tunnels and cluster execution —the DSH counterpart of
5
5
  * ssh-skill's daemon + scripts, living entirely in the host process.
6
+ *
7
+ * This module stays the public entry point (its export surface and the
8
+ * SshEngine class identity are load-bearing: production keys caches on the
9
+ * engine object in a WeakMap). The implementation lives in focused internal
10
+ * components, each owning one resource family:
11
+ *
12
+ * connection/manager.ts pooled connections, connect chains, secrets,
13
+ * retry/replay policy, exec/cluster/test, redaction
14
+ * sftp/service.ts SFTP subsystem channel cache + every SFTP operation
15
+ * terminal/service.ts standalone PTY shell / streaming exec transports
16
+ * tunnel/service.ts local listeners, forwarded sockets, tunnel leases
17
+ *
18
+ * Dependency direction is one-way (sftp/terminal/tunnel → connection); no
19
+ * component imports this facade back, so there is no cycle.
6
20
  */
7
- import { type ConnectConfig } from 'ssh2';
8
- import { type SshConnectionService } from './connection/pool.ts';
9
- import type { ClusterResult, ExecResult, SshHostEntry, SshHostSummary, TestResult, TransferProgress, TunnelInfo } from './protocol.ts';
21
+ import type { Writable } from 'node:stream';
22
+ import type { SshConnectionService } from './connection/pool.ts';
23
+ import { type EngineDeps, type EngineOptions, type ExecOptions, type SshInvalidateOptions } from './connection/manager.ts';
24
+ import type { SshHostSummary, TransferProgress, TunnelInfo } from './protocol.ts';
10
25
  import type { HostStoreView } from '../core.ts';
11
- import { HostKeyPolicy, type HostKeyCheck, type KnownHostsStore } from './known-hosts.ts';
12
- /** Options used when retiring pooled connections after host configuration changes. */
13
- export interface SshInvalidateOptions {
14
- /**
15
- * Also invalidate hosts whose ProxyJump chain depends, directly or
16
- * transitively, on the changed alias.
17
- */
18
- includeDependents?: boolean;
19
- /**
20
- * drain: reject/reconnect subsequent acquisitions, allow existing leases
21
- * to complete before closing their transport.
22
- * force: close the current transport immediately.
23
- */
24
- mode?: 'drain' | 'force';
25
- }
26
- /** Default engine knobs. */
27
- export interface EngineOptions {
28
- /** Connections idle longer than this are closed (ms). */
29
- idleTimeoutMs?: number;
30
- /** SSH handshake timeout (ms). */
31
- connectTimeoutMs?: number;
32
- /** Keepalive ping interval (ms). */
33
- keepaliveIntervalMs?: number;
34
- /** Cap on captured stdout/stderr bytes per exec (ms). */
35
- maxOutputBytes?: number;
36
- /** Default exec timeout (ms). */
37
- defaultExecTimeoutMs?: number;
38
- /** Default cluster concurrency. */
39
- defaultMaxWorkers?: number;
40
- /** SFTP concurrent channel count for transfers. */
41
- sftpConcurrency?: number;
42
- /** Optional server-host-key algorithm whitelist (e.g. ['ssh-ed25519']). */
43
- hostKeyAlgorithms?: string[];
44
- }
45
- /**
46
- * Optional engine dependencies for host-key TOFU and secret resolution.
47
- * All fields are optional and their absence preserves the pre-security
48
- * behavior exactly (plaintext inline auth, no host verification) — the
49
- * existing tests and call sites keep working unchanged.
50
- */
51
- export interface EngineDeps {
52
- /** Known-hosts trust store; when set, connections require a trusted host key. */
53
- knownHosts?: KnownHostsStore;
54
- /** Fingerprint check policy (defaults to a HostKeyPolicy over knownHosts). */
55
- hostKeyPolicy?: HostKeyPolicy;
56
- /**
57
- * Secret resolution for one entry. Absent: read `password`/`passphrase`
58
- * inline from the entry (plaintext store / test compatibility).
59
- */
60
- resolveSecrets?: (entry: SshHostEntry) => Promise<ResolvedAuthDeps>;
61
- /** Optional server-host-key algorithm whitelist override. */
62
- hostKeyAlgorithms?: string[];
63
- }
64
- /** The resolved-auth shape passed into connect config building (vault-aware). */
65
- export interface ResolvedAuthDeps {
66
- kind: SshHostEntry['auth']['kind'];
67
- keyPath?: string;
68
- password?: string;
69
- passphrase?: string;
70
- }
71
- /**
72
- * Thrown when a connection needs a password/passphrase that is not yet
73
- * available in this session (secretStorage='none' and the user hasn't entered
74
- * it yet). The GUI intercepts this and prompts for the credential, then
75
- * injects it via engine.setSessionPassword and retries.
76
- */
77
- export declare class NeedsPasswordError extends Error {
78
- /** Which secret the connection needs: 'password' or 'passphrase'. */
79
- readonly secret: 'password' | 'passphrase';
80
- constructor(alias: string, secret: 'password' | 'passphrase');
81
- }
82
26
  /**
83
- * How much an operation may be retried:
84
- * - never: one acquisition + one operation attempt.
85
- * - connect-only: connection acquisition may be retried, but once the
86
- * operation function starts it is invoked at most once (default).
87
- * - idempotent: the operation may also be retried until it calls
88
- * markCommitted() (i.e. while the exec channel is still opening).
27
+ * Enforce an exec budget ON THE SERVER as well as in the client.
89
28
  *
90
- * SFTP operations must never use 'idempotent': they have no commit point, so
91
- * a replay after a mid-flight timeout would duplicate a remote write.
29
+ * The client-side deadline only stops WAITING: measured against a real host
30
+ * (a CentOS 7 host running OpenSSH 7.4), a timed-out exec left BOTH the wrapper
31
+ * shell and its child running — `bash -c sleep 30` plus `sleep 30` were still
32
+ * alive 20s later — because ssh2's channel `signal` request was not delivered
33
+ * to the remote process, and closing the channel does not kill it either.
34
+ * Wrapping the command in coreutils `timeout` moves the deadline INSIDE the
35
+ * session, which does reap the whole process group (verified on the same host:
36
+ * exit 124 within the budget, zero orphans).
37
+ *
38
+ * The wrapper degrades to running the command verbatim when the server has no
39
+ * `timeout` (busybox or a minimal image), so no host becomes unusable.
40
+ *
41
+ * @param command - the caller's command, run unchanged inside the wrapper.
42
+ * @param budgetMs - the client-side budget this mirrors.
43
+ * @returns the command to hand to the remote shell.
92
44
  */
93
- export type RetryPolicy = 'never' | 'connect-only' | 'idempotent';
94
- /** Options for a one-shot remote command. */
95
- export interface ExecOptions {
96
- timeoutMs?: number;
97
- retry?: RetryPolicy;
98
- }
45
+ export declare function wrapCommandWithServerBudget(command: string, budgetMs: number): string;
46
+ export { buildConnectConfig, DEFAULTS, NeedsPasswordError, sshAgentConfig, } from './connection/manager.ts';
47
+ export type { ConnectionManager, EngineDeps, EngineOptions, ExecOptions, ResolvedAuthDeps, RetryPolicy, SshInvalidateOptions, } from './connection/manager.ts';
99
48
  /** A live PTY shell session. */
100
49
  export interface ShellSession {
101
50
  /** Assign to receive remote output. */
@@ -121,82 +70,37 @@ export interface ShellSession {
121
70
  * input burst. Used by the subprocess capability seam's remote provider.
122
71
  */
123
72
  export interface ExecSession extends ShellSession {
73
+ /** Native ssh2 channel input. Using its real Writable contract preserves
74
+ * bytes, backpressure, errors and finish semantics for subprocess stdin. */
75
+ readonly stdin: Writable;
124
76
  /** Assign to receive the remote stderr stream. */
125
77
  onErrData?: (data: Buffer) => void;
126
78
  /** Send an SSH signal (e.g. 'TERM', 'KILL') to the remote process. */
127
79
  signal(name: string): void;
128
- /** Write the final input burst and half-close stdin. */
80
+ /** Compatibility half-close for non-streaming callers. */
129
81
  end(data?: string): void;
130
82
  }
131
- /** One host-key verification outcome captured during a connect attempt (used
132
- * to rewrite the generic handshake failure into a typed host-key error). */
133
- interface HostKeyOutcome {
134
- alias: string;
135
- check: HostKeyCheck;
136
- }
137
- /**
138
- * Resolve the ssh-agent socket to offer to ssh2 (zero-input key auth,
139
- * VSCode-style): `$SSH_AUTH_SOCK` — the standard OpenSSH agent socket (also
140
- * exported by Git for Windows' ssh-agent and WSL). Deliberately NOT probing
141
- * named pipes (Pageant / Windows OpenSSH agent): an absent pipe makes ssh2's
142
- * agent query stall the whole handshake until readyTimeout instead of
143
- * falling through to the next method. Keep Pageant compatibility for a
144
- * future explicit opt-in. An agent that yields no keys makes ssh2 fall
145
- * through to the configured methods (privateKey → password), so enabling it
146
- * when a socket is present is safe. Exported for tests.
147
- */
148
- export declare function sshAgentConfig(): string | undefined;
149
- /** Build the ssh2 connect config for one entry (key read from disk). The
150
- * timeout/keepalive knobs come from EngineOptions so they actually take
151
- * effect instead of being hard-coded. Exported for tests. */
152
- export declare function buildConnectConfig(entry: SshHostEntry, options: Pick<Required<EngineOptions>, 'connectTimeoutMs' | 'keepaliveIntervalMs'>, sock?: ConnectConfig['sock'], buildContext?: {
153
- hostKeyPolicy?: HostKeyPolicy;
154
- hostKeyAlgorithms?: string[];
155
- /** Writes the verified/refused outcome back to the caller's capture slot. */
156
- setOutcome?: (value: HostKeyOutcome) => void;
157
- /** Vault-resolved authentication (overrides entry.auth secrets). */
158
- authOverride?: ResolvedAuthDeps;
159
- }): ConnectConfig;
160
83
  /**
161
- * The engine. Owns the pool, tunnels, and all operations. One instance per
162
- * plugin apply; dispose() closes every connection.
84
+ * The SSH engine. Owns the connection pool, tunnels, standalone terminal
85
+ * transports and SFTP channels. One instance per plugin apply.
86
+ *
87
+ * Kept as the public facade over ConnectionManager/TunnelService/
88
+ * TerminalService/SftpService: every method here is a thin delegation, so the
89
+ * class identity (and therefore WeakMap cache keys in production) is stable.
163
90
  */
164
91
  export declare class SshEngine {
165
- private readonly store;
166
- private readonly opts;
167
- private readonly tunnels;
168
- /**
169
- * One cached SFTP subsystem channel per live client. `Client.sftp()` opens a
170
- * NEW subsystem channel on every call and OpenSSH caps open sessions per
171
- * connection (MaxSessions, default 10) —reopening SFTP per operation lets
172
- * channels pile up on the pooled long-lived connection until listing/reading
173
- * fails intermittently. Caching one channel per client fixes that; the pool
174
- * drops the cache via onDispose when a connection is torn down.
175
- */
176
- private readonly sftpChannels;
177
- private readonly connectionPool;
178
- private readonly deps;
179
- private readonly hostKeyPolicy;
180
- private nextTunnelId;
181
- /**
182
- * Session-scoped secrets (secretStorage='none'): keyed by alias, populated
183
- * by the GUI on first connect, used by connectChain's resolve step, and
184
- * cleared on dispose. Never persisted.
185
- */
186
- private readonly sessionPasswords;
92
+ private readonly manager;
93
+ private readonly sftpService;
94
+ private readonly tunnelService;
95
+ private readonly terminalService;
187
96
  /**
188
97
  * @param store - the host config store.
189
- * @param options - engine knobs (defaults applied).
98
+ * @param options - engine knobs (defaults applied after validation).
190
99
  * @param deps - optional security deps (host-key TOFU, secret resolution).
191
100
  * Absent → pre-security behavior (inline auth, no host verification).
192
101
  */
193
102
  constructor(store: HostStoreView, options?: EngineOptions, deps?: EngineDeps);
194
- /**
195
- * Provide a secret for `alias` for THIS session only (never persisted).
196
- * Used by the GUI when a connection needs a password/passphrase under
197
- * secretStorage='none'. Once set, pooled connections reuse it until the
198
- * session ends or clearSessionSecrets() is called.
199
- */
103
+ /** Provide a secret for `alias` for THIS session only (never persisted). */
200
104
  setSessionPassword(alias: string, secret: {
201
105
  password?: string;
202
106
  passphrase?: string;
@@ -208,6 +112,8 @@ export declare class SshEngine {
208
112
  } | undefined;
209
113
  /** Drop every session secret (e.g. on secretStorage change / lock). */
210
114
  clearSessionSecrets(): void;
115
+ /** Redact credentials known to this session and the optional vault guard. */
116
+ redact(text: string): string;
211
117
  /** Secret-free host list (filtered by the optional query). */
212
118
  list(query?: string): SshHostSummary[];
213
119
  /** One host summary by alias. */
@@ -223,45 +129,24 @@ export declare class SshEngine {
223
129
  /** Aliases with a live pooled transport right now (for connection-state
224
130
  * indicators — the GUI badge colors bound workspaces by it). */
225
131
  connectedAliases(): string[];
226
- /**
227
- * Retire the pooled connection for one alias.
228
- *
229
- * ConnectionPool knows nothing about HostStore or ProxyJump configuration
230
- * (every target owns its complete jump chain, no hop records are shared),
231
- * so dependent-host expansion belongs here: with includeDependents the
232
- * transitive reverse ProxyJump closure of `alias` is invalidated too.
233
- */
132
+ /** Retire the pooled connection for one alias (optionally its dependents). */
234
133
  invalidate(alias: string, options?: SshInvalidateOptions): void;
235
134
  /**
236
- * Run `fn` with a live client for `alias`.
237
- *
238
- * Acquisition retry and operation replay are deliberately separate:
239
- * - never: one acquire + one operation attempt.
240
- * - connect-only: acquire may be retried, but fn is invoked at most once —
241
- * once fn starts, the remote may already have observed the request, so
242
- * replay could duplicate non-idempotent work.
243
- * - idempotent: fn may be retried until it calls control.markCommitted()
244
- * (exec marks this when the server accepted the channel).
245
- *
246
- * A failed operation retires the transport via lease.markBroken(); the
247
- * lease is always released before the next acquire attempt.
135
+ * Record one channel that never acknowledged close after a hard timeout.
136
+ * Exposed for the exec grace path and for diagnostics/tests.
137
+ * @param alias - the host the channel belonged to.
138
+ * @returns true when the pooled transport was drained.
248
139
  */
249
- private withClient;
250
- /** Resolve an entry's authentication for one connect: session password
251
- * table first (secretStorage='none'), then deps.resolveSecrets (vault),
252
- * then the inline store entry; when a password/passphrase is required but
253
- * unavailable, throw NeedsPasswordError for the GUI to prompt. */
254
- private resolveEntryAuth;
140
+ noteLeakedChannel(alias: string): boolean;
141
+ /** A late channel close: un-count one half-open channel for this alias. */
142
+ noteChannelClosed(alias: string): void;
143
+ /** Run one command on `alias` (reusing the pooled connection). */
144
+ exec(alias: string, command: string, timeoutMs?: number): Promise<import('./protocol.ts').ExecResult>;
145
+ exec(alias: string, command: string, options: ExecOptions): Promise<import('./protocol.ts').ExecResult>;
255
146
  /**
256
- * Build one full jump chain for an entry: hop clients connected through in
257
- * order, each forwarding a stream to the next destination, ending with the
258
- * target client. Shared by the pool and standalone shell sessions.
147
+ * Run one command against many hosts concurrently. Per-host failures carry
148
+ * the typed, secret-free fields declared on `ClusterResult`.
259
149
  */
260
- private connectChain;
261
- /** Run one command on `alias` (reusing the pooled connection). */
262
- exec(alias: string, command: string, timeoutMs?: number): Promise<ExecResult>;
263
- exec(alias: string, command: string, options: ExecOptions): Promise<ExecResult>;
264
- /** Run one command against many hosts concurrently. */
265
150
  cluster(options: {
266
151
  command: string;
267
152
  aliases?: string[];
@@ -269,14 +154,9 @@ export declare class SshEngine {
269
154
  tags?: string[];
270
155
  timeoutMs?: number;
271
156
  maxWorkers?: number;
272
- }): Promise<ClusterResult[]>;
273
- /**
274
- * Open one standalone channel on its own connection (never a pooled one).
275
- * Shared by openShell/openExec (P1-18): alias lookup, jump chain,
276
- * channel-open timeout, idempotent teardown, and late-callback cleanup live
277
- * here, so the two public methods keep only their session-specific assembly.
278
- */
279
- private openStandaloneChannel;
157
+ /** Aborts every per-host run when the caller disconnects. */
158
+ signal?: AbortSignal;
159
+ }): Promise<import('./protocol.ts').ClusterResult[]>;
280
160
  /** Open a PTY shell session for the web terminal (standalone connection). */
281
161
  openShell(alias: string, size: {
282
162
  cols: number;
@@ -288,36 +168,22 @@ export declare class SshEngine {
288
168
  * can never tear down a pooled exec/tunnel sharing the alias.
289
169
  */
290
170
  openExec(alias: string, command: string): Promise<ExecSession>;
291
- /** Upload one local file (or directory tree) to a remote path. */ upload(alias: string, localPath: string, remotePath: string, recursive: boolean, onProgress?: (progress: TransferProgress) => void): Promise<{
171
+ /** Upload one local file (or directory tree) to a remote path. */
172
+ upload(alias: string, localPath: string, remotePath: string, recursive: boolean, onProgress?: (progress: TransferProgress) => void, signal?: AbortSignal): Promise<{
292
173
  bytes: number;
293
174
  files: number;
294
175
  }>;
295
176
  /** Download one remote file to a local path. */
296
- download(alias: string, remotePath: string, localPath: string, onProgress?: (progress: TransferProgress) => void): Promise<{
177
+ download(alias: string, remotePath: string, localPath: string, onProgress?: (progress: TransferProgress) => void, signal?: AbortSignal): Promise<{
297
178
  bytes: number;
298
179
  }>;
299
180
  /** List a remote directory (file browser). Bounded by a timeout so a
300
181
  * stalled SFTP request fails instead of leaving the file tree spinning. */
301
- ls(alias: string, path: string): Promise<import('./protocol.ts').RemoteDirEntry[]>;
302
- /**
303
- * Resolve many remote paths to their canonical form in one SFTP pass
304
- * (P1-26): one lease + one batch of `sftp.realpath` calls instead of N
305
- * `realpath` execs. A path that cannot resolve (dangling symlink, vanished
306
- * entry) fails the whole batch — callers treat an unresolvable listing as
307
- * an error rather than silently using an uncanonical path.
308
- */
309
- realpaths(alias: string, remotePaths: readonly string[]): Promise<string[]>;
310
- /**
311
- * Classify readdir entries, following symlinks so a link to a directory
312
- * (e.g. AutoDL's /root/autodl-tmp) lists as a directory instead of 'other'.
313
- * Symlinks are stat'd in PARALLEL batches: serializing them turns a conda /
314
- * venv bin full of links into N round-trips (seconds to tens of seconds on a
315
- * slow link) — batching keeps it to a handful of round-trips. The whole pass
316
- * is bounded by ls()'s timeout.
317
- */
318
- private classifyEntries;
182
+ ls(alias: string, path: string, signal?: AbortSignal): Promise<import('./protocol.ts').RemoteDirEntry[]>;
183
+ /** Resolve many remote paths to their canonical form in one SFTP pass. */
184
+ realpaths(alias: string, remotePaths: readonly string[], signal?: AbortSignal): Promise<string[]>;
319
185
  /** Stat one remote path (file browser / conflict checks). Bounded by a timeout. */
320
- stat(alias: string, remotePath: string): Promise<{
186
+ stat(alias: string, remotePath: string, signal?: AbortSignal): Promise<{
321
187
  type: 'dir' | 'file' | 'other';
322
188
  size: number;
323
189
  mtimeMs: number;
@@ -327,7 +193,7 @@ export declare class SshEngine {
327
193
  * Lstat one remote path without following the final symlink. Returns
328
194
  * undefined when the path is absent (the fs seam's lstat contract).
329
195
  */
330
- lstat(alias: string, remotePath: string): Promise<{
196
+ lstat(alias: string, remotePath: string, signal?: AbortSignal): Promise<{
331
197
  type: 'file' | 'directory' | 'symlink' | 'other';
332
198
  size: number;
333
199
  mtimeMs: number;
@@ -336,20 +202,15 @@ export declare class SshEngine {
336
202
  /**
337
203
  * Open a remote file read stream (the fs seam's streamText). The returned
338
204
  * stream must be consumed or destroyed; the pooled connection stays busy
339
- * for the stream's lifetime.
340
- */
341
- /**
342
- * Open a remote file read stream (the fs seam's streamText). The returned
343
- * stream must be consumed or destroyed; the pooled connection stays busy
344
- * for the stream's lifetime (P0-10: a 'stream' lease, released on
205
+ * for the stream's lifetime (a 'stream' lease, released on
345
206
  * end/close/error/destroy — not when this function returns).
346
207
  */
347
- readStream(alias: string, remotePath: string): Promise<import('node:stream').Readable>;
208
+ readStream(alias: string, remotePath: string, signal?: AbortSignal): Promise<import('node:stream').Readable>;
348
209
  /**
349
210
  * Read one remote file fully into memory (text or binary) with its mtime.
350
211
  * The workspace plugin's text gate (UTF-8 + size caps) lives on its caller.
351
212
  */
352
- readFile(alias: string, remotePath: string): Promise<{
213
+ readFile(alias: string, remotePath: string, maxBytes?: number, signal?: AbortSignal): Promise<{
353
214
  content: Buffer;
354
215
  mtime: number;
355
216
  size: number;
@@ -357,14 +218,13 @@ export declare class SshEngine {
357
218
  /**
358
219
  * Write one remote file from memory (parents are created). When
359
220
  * `expectedMtime` is given, a stat-then-write conflict check throws before
360
- * any byte is written (the GUI and the workspace tools use it for
361
- * overwrite protection).
221
+ * any byte is written (overwrite protection for the GUI and workspace tools).
362
222
  */
363
- writeFile(alias: string, remotePath: string, content: Buffer, expectedMtime?: number): Promise<{
223
+ writeFile(alias: string, remotePath: string, content: Buffer, expectedMtime?: number, signal?: AbortSignal): Promise<{
364
224
  mtime: number;
365
225
  }>;
366
226
  /** Create a remote directory chain (mkdir -p semantics). */
367
- mkdir(alias: string, remotePath: string): Promise<void>;
227
+ mkdir(alias: string, remotePath: string, signal?: AbortSignal): Promise<void>;
368
228
  /**
369
229
  * Remove a remote file or directory. Directories require `recursive: true`
370
230
  * and are walked depth-first (children first, then the directory itself).
@@ -374,61 +234,31 @@ export declare class SshEngine {
374
234
  * recursed into — the old stat/readdir-attr check could delete the link
375
235
  * target's contents.
376
236
  */
377
- rm(alias: string, remotePath: string, recursive?: boolean): Promise<void>;
237
+ rm(alias: string, remotePath: string, recursive?: boolean, signal?: AbortSignal): Promise<void>;
378
238
  /** Rename / move a remote path (mv semantics, same filesystem). */
379
- rename(alias: string, fromPath: string, toPath: string): Promise<void>;
380
- /** Reject a promise after `ms` (unref'd so it never keeps the process alive).
381
- * `onTimeout` (when given) runs right before the rejection: ssh2 SFTP
382
- * requests have no cancel API, so callers that hold an abort handle (e.g.
383
- * a read stream) destroy it here — otherwise the underlying transfer would
384
- * keep running (and, for reads, keep buffering) after the caller was told
385
- * it timed out. */
386
- private withTimeout;
387
- /** Stat wrapper (one SFTP stat call). */
388
- private sftpStat;
389
- /** Lstat wrapper (does NOT follow symlinks — the deletion safety gate). */
390
- private sftpLstat;
391
- /**
392
- * The (cached) SFTP channel for a pooled client. `Client.sftp()` opens a new
393
- * subsystem channel per call, so this memoizes one channel per live client;
394
- * when the channel closes the cache entry is dropped so the next call opens
395
- * SFTP on the replacement connection. Failed opens are also evicted so a
396
- * transient channel failure can be retried.
397
- */
398
- private sftpFor;
399
- /** Create a remote directory chain (stat-then-mkdir per segment). */
400
- private ensureRemoteDir;
401
- private fastPut;
402
- private fastGet;
403
- /** Remove one tunnel's transport-failure listener (both events). */
404
- private removeTunnelClientFailureListener;
405
- /**
406
- * Mark every tunnel sharing a failed physical SSH client as failed. Failed
407
- * records stay in this.tunnels so listTunnels() exposes the terminal state
408
- * and stopTunnel() remains the single place that deletes + releases.
409
- */
410
- private markTunnelsFailedForClient;
411
- /** Start a local port-forward tunnel (listens on 127.0.0.1 only). */
239
+ rename(alias: string, fromPath: string, toPath: string, signal?: AbortSignal): Promise<void>;
412
240
  startTunnel(alias: string, options: {
413
241
  remotePort: number;
414
242
  remoteHost?: string;
415
243
  localPort?: number;
416
244
  }): Promise<TunnelInfo>;
417
- /** All active tunnels. */
418
245
  listTunnels(): TunnelInfo[];
419
- /** Stop one tunnel (closes listener and live sockets, releases its lease). */
420
246
  stopTunnel(id: string): boolean;
421
- /** Stop all tunnels (optionally for one alias). */
422
247
  stopAllTunnels(alias?: string): number;
423
248
  /** Probe connectivity: connect, run `true`, close. Typed errors the GUI
424
249
  * must react to (host-key TOFU, session password) are NOT flattened into
425
250
  * a plain message — callers (routes → panel / workspace gate) key their
426
251
  * interactive dialogs on the typed error. Everything else (unreachable,
427
252
  * timeout, auth failure) returns a failed result. */
428
- test(alias: string): Promise<TestResult>;
429
- /** Close every pooled connection and tunnel, and wipe the in-memory
430
- * session password table (secrets are never persisted anywhere). */
253
+ test(alias: string, signal?: AbortSignal): Promise<import('./protocol.ts').TestResult>;
254
+ /**
255
+ * Close every resource this engine owns and wipe the in-memory session
256
+ * password table (secrets are never persisted anywhere).
257
+ *
258
+ * Order: tunnel → terminal → SFTP → connection, so a resource is always
259
+ * closed before the transport that carries it, and each resource has exactly
260
+ * one dispose point.
261
+ */
431
262
  dispose(): void;
432
263
  }
433
- export {};
434
264
  //# sourceMappingURL=engine.d.ts.map
@@ -55,15 +55,20 @@ export declare class KnownHostsStore {
55
55
  private load;
56
56
  private save;
57
57
  lookup(alias: string): KnownHostRecord | undefined;
58
- /** First encounter: record as pending (non-destructive re-observe keeps status). */
58
+ /** First encounter: record as pending. Re-observing keeps the recorded
59
+ * status/fingerprint but backfills a previously empty host/port. */
59
60
  observe(alias: string, meta: {
60
61
  host: string;
61
62
  port: number;
62
63
  keyType: string;
63
64
  fingerprintSha256: string;
64
65
  }): void;
65
- /** pending → trusted (must match the recorded fingerprint). */
66
- trust(alias: string): void;
66
+ /** pending → trusted (must match the recorded fingerprint). Optional
67
+ * host/port backfill completes a record whose first observe had no target. */
68
+ trust(alias: string, target?: {
69
+ host?: string;
70
+ port?: number;
71
+ }): void;
67
72
  /** Forget a host (key rotation /误信 reset → next connect re-TOFU). */
68
73
  forget(alias: string): void;
69
74
  list(): KnownHostRecord[];
@@ -72,9 +77,21 @@ export declare class KnownHostsStore {
72
77
  export declare class HostKeyPolicy {
73
78
  private readonly knownHosts;
74
79
  constructor(knownHosts: KnownHostsStore);
75
- /** Evaluate one server key: trusted / unknown (records pending) / mismatch. */
76
- check(alias: string, rawKey: Buffer): HostKeyCheck;
80
+ /** Evaluate one server key: trusted / unknown (records pending) / mismatch.
81
+ * `target` supplies the host/port the engine is actually connecting to so
82
+ * the audit record is complete instead of `''`/0. */
83
+ check(alias: string, rawKey: Buffer, target?: {
84
+ host?: string;
85
+ port?: number;
86
+ }): HostKeyCheck;
77
87
  }
88
+ /**
89
+ * Parse the key type out of a real SSH wire public-key blob. The blob is
90
+ * `string algorithm, byte[] key` where every string is a 4-byte big-endian
91
+ * length followed by raw bytes — NOT the `ssh-ed25519 AAAA…` authorized_keys
92
+ * text form, so a text-oriented decode would always yield 'ssh-unknown'.
93
+ */
94
+ export declare function keyTypeOf(rawKey: Buffer): string;
78
95
  /** Error: the host key is not trusted yet (first encounter, awaiting confirm). */
79
96
  export declare class HostKeyUnknownError extends Error {
80
97
  readonly fingerprintSha256: string;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Host-side path policy for ssh_upload / ssh_download.
3
+ *
4
+ * These tools bridge remote I/O to the DSH host, so accepting an arbitrary
5
+ * absolute local path would bypass the normal FileSystem sandbox. The only
6
+ * trusted per-call root available to a tool body is the validated absolute cwd
7
+ * stored on the invoking Agent's immutable SessionHeader.
8
+ */
9
+ import type { ToolRunContext } from '@deepseek-ai/dsh-tools';
10
+ export type LocalTransferDirection = 'upload-source' | 'download-destination';
11
+ /**
12
+ * Validate and canonicalize one local transfer path.
13
+ *
14
+ * Uploads must name an existing regular file. Downloads may create the final
15
+ * file and missing parent directories, but their nearest existing ancestor is
16
+ * canonicalized so an in-workspace symlink/junction cannot redirect the write
17
+ * outside the session root.
18
+ */
19
+ export declare function secureLocalTransferPath(exec: ToolRunContext, localPath: string, direction: LocalTransferDirection): string;
20
+ //# sourceMappingURL=local-transfer-policy.d.ts.map
@@ -15,27 +15,20 @@ import type { SshEngine } from './engine.ts';
15
15
  /** Settings namespace of the SSH capability. Kept as 'dsh-ssh' so existing
16
16
  * user settings survive the package merge unchanged. */
17
17
  export declare const SSH_SETTINGS_NAMESPACE = "dsh-ssh";
18
- /** SSH-capability config, validated by the same-named schemastery schema. */
18
+ /** SSH-capability config, validated by the same-named schemastery schema.
19
+ * `secretStorage` is deliberately NOT a key here: the mode decides which
20
+ * objects are constructed at plugin load, so it is a single source in the
21
+ * top-level plugin config (`Config.secretStorage`). A value left in this
22
+ * namespace by hand is reported as drift by src/index.ts, never applied. */
19
23
  export interface SshConfig {
20
24
  /** When true (default), a system-prompt section announces the SSH capability. */
21
25
  announceToAgent?: boolean;
22
26
  /** Master switch for the SSH surfaces (routes, tools, prompt section). */
23
27
  enabled?: boolean;
24
- /**
25
- * Secret storage mode:
26
- * - 'none' (default): passwords/passphrases are NEVER persisted; each
27
- * session's first connection prompts for them (VSCode Remote-SSH style)
28
- * and they live in memory (session password table) for the connection
29
- * pool lifetime (idle 30 min auto-disconnect; new terminals reuse it).
30
- * - 'vault': store secrets encrypted at rest (AES-256-GCM) under a master
31
- * password / DSH_CREDENTIAL_PASSWORD; for headless agents that must run
32
- * password hosts unattended.
33
- */
34
- secretStorage?: 'none' | 'vault';
35
28
  }
36
29
  export declare const SshConfig: z<SshConfig>;
37
30
  /** Model-facing announcement: SSH operations capability, capabilities, limits. */
38
- export declare const SSH_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-hardssh \u63D2\u4EF6\uFF08\u5185\u5EFA DSH \u8FDC\u7A0B SSH \u8FD0\u7EF4\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CSSH\u300D\u5165\u53E3\uFF1BSSH \u8FD0\u7EF4\u4E0E SSH \u5DE5\u4F5C\u533A\u5728 dsh-hardssh \u4E2D\u7EDF\u4E00\u7EF4\u62A4\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 ~/.dsh/dsh-ssh.json\uFF08\u53EF\u4ECE ~/.ssh/config \u5BFC\u5165\uFF09\uFF1B\u6301\u4E45\u8FDE\u63A5\u6C60\u590D\u7528\u957F\u8FDE\u63A5\uFF08\u7A7A\u95F2 30 \u5206\u949F\u81EA\u52A8\u65AD\u5F00\uFF09\uFF1Bssh_list \u5217\u51FA\u4E3B\u673A\u3001ssh_exec \u6267\u884C\u8FDC\u7A0B\u547D\u4EE4\u3001ssh_upload/ssh_download \u4F20\u8F93\u6587\u4EF6\u3001ssh_tunnel \u672C\u5730\u7AEF\u53E3\u8F6C\u53D1\uFF08\u8BBF\u95EE\u8FDC\u7A0B\u6570\u636E\u5E93/\u5185\u7F51\u670D\u52A1\uFF09\u3001ssh_cluster \u96C6\u7FA4\u5E76\u53D1\u6267\u884C\uFF1B\u652F\u6301\u5BC6\u94A5/\u5BC6\u7801\u8BA4\u8BC1\u3001passphrase \u5BC6\u94A5\u4E0E ProxyJump \u8DF3\u677F\u673A\uFF1BWeb \u7EC8\u7AEF\u8D70 WebSocket\u3002\u9650\u5236\uFF1A\u4E3B\u673A\u64CD\u4F5C\u7531\u7528\u6237\u5728 GUI \u4E2D\u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF1B\u5BC6\u7801\u4EE5\u660E\u6587\u5B58\u5728\u7528\u6237\u4E3B\u76EE\u5F55\u79C1\u6709\u6587\u4EF6\uFF08\u6743\u9650 0600\uFF09\uFF1B\u547D\u4EE4\u8F93\u51FA\u539F\u6837\u8FD4\u56DE\u3001\u53EF\u80FD\u542B\u654F\u611F\u4FE1\u606F\uFF1B\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25\u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF0C\u4F46\u547D\u4EE4\u6216\u6587\u4EF6\u64CD\u4F5C\u4E00\u65E6\u63D0\u4EA4\u5C31\u4E0D\u4F1A\u81EA\u52A8\u91CD\u8BD5\u2014\u2014\u7ED3\u679C\u4E0D\u660E\u786E\u65F6\u5E94\u5148\u6838\u5B9E\u8FDC\u7AEF\u72B6\u6001\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u4EBA\u5DE5\u91CD\u8BD5\uFF1B\u4F20\u8F93/\u6267\u884C\u6D88\u8017\u771F\u5B9E\u8FDC\u7A0B\u8D44\u6E90\uFF0C\u5148\u786E\u8BA4\u518D\u64CD\u4F5C\u3002\u7528\u6237\u63D0\u5230\u300CSSH / \u8FDC\u7A0B\u670D\u52A1\u5668 / \u670D\u52A1\u5668\u64CD\u4F5C / \u8DF3\u677F\u673A / \u96A7\u9053 / \u90E8\u7F72 / \u4E0A\u4F20\u4E0B\u8F7D\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
31
+ export declare const SSH_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-hardssh \u63D2\u4EF6\uFF08\u5185\u5EFA DSH \u8FDC\u7A0B SSH \u8FD0\u7EF4\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CSSH\u300D\u5165\u53E3\uFF1BSSH \u8FD0\u7EF4\u4E0E SSH \u5DE5\u4F5C\u533A\u5728 dsh-hardssh \u4E2D\u7EDF\u4E00\u7EF4\u62A4\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 ~/.dsh/dsh-ssh.json\uFF08\u53EF\u4ECE ~/.ssh/config \u5BFC\u5165\uFF09\uFF1B\u6301\u4E45\u8FDE\u63A5\u6C60\u590D\u7528\u957F\u8FDE\u63A5\uFF08\u7A7A\u95F2 30 \u5206\u949F\u81EA\u52A8\u65AD\u5F00\uFF09\uFF1Bssh_list \u5217\u51FA\u4E3B\u673A\u3001ssh_exec \u6267\u884C\u8FDC\u7A0B\u547D\u4EE4\u3001ssh_upload/ssh_download \u4F20\u8F93\u6587\u4EF6\u3001ssh_tunnel \u672C\u5730\u7AEF\u53E3\u8F6C\u53D1\uFF08\u8BBF\u95EE\u8FDC\u7A0B\u6570\u636E\u5E93/\u5185\u7F51\u670D\u52A1\uFF09\u3001ssh_cluster \u96C6\u7FA4\u5E76\u53D1\u6267\u884C\uFF1B\u652F\u6301\u5BC6\u94A5/\u5BC6\u7801\u8BA4\u8BC1\u3001passphrase \u5BC6\u94A5\u4E0E ProxyJump \u8DF3\u677F\u673A\uFF1BWeb \u7EC8\u7AEF\u8D70 WebSocket\u3002\u9650\u5236\uFF1A\u4E3B\u673A\u64CD\u4F5C\u7531\u7528\u6237\u5728 GUI \u4E2D\u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF1B\u51ED\u636E\u9ED8\u8BA4\u4E0D\u843D\u76D8\uFF08secretStorage=none \u65F6\u4EC5\u5B58\u4E8E\u672C\u8FDB\u7A0B\u5185\u5B58\uFF0C\u6309\u8FDE\u63A5\u5B58\u6D3B\u671F\u590D\u7528\u3001\u8FDE\u63A5\u6C60\u56DE\u6536\u5373\u5931\u6548\uFF1BsecretStorage=vault \u65F6\u4EE5 AES-256-GCM \u52A0\u5BC6\u5B58\u4E8E\u672C\u673A vault \u6587\u4EF6\uFF0C`DSH_CREDENTIAL_PASSWORD` \u81EA\u52A8\u89E3\u9501\u9ED8\u8BA4\u5173\u95ED\uFF0C\u9700 vaultAutoUnlock: env \u663E\u5F0F\u5F00\u542F\uFF09\uFF1B\u547D\u4EE4\u8F93\u51FA\u4E0E\u9519\u8BEF\u7ECF\u8FC7\u51ED\u636E\u8131\u654F\uFF08\u5DF2\u63ED\u793A/\u4F1A\u8BDD\u5185\u7684\u5BC6\u7801\u4F1A\u88AB\u66FF\u6362\uFF09\uFF0C\u4F46\u4ECD\u53EF\u80FD\u542B\u5176\u5B83\u654F\u611F\u4FE1\u606F\uFF1B\u8FDE\u63A5\u5EFA\u7ACB\u5931\u8D25\u4F1A\u81EA\u52A8\u91CD\u8FDE\uFF0C\u4F46\u547D\u4EE4\u6216\u6587\u4EF6\u64CD\u4F5C\u4E00\u65E6\u63D0\u4EA4\u5C31\u4E0D\u4F1A\u81EA\u52A8\u91CD\u8BD5\u2014\u2014\u7ED3\u679C\u4E0D\u660E\u786E\u65F6\u5E94\u5148\u6838\u5B9E\u8FDC\u7AEF\u72B6\u6001\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u4EBA\u5DE5\u91CD\u8BD5\uFF1B\u4F20\u8F93/\u6267\u884C\u6D88\u8017\u771F\u5B9E\u8FDC\u7A0B\u8D44\u6E90\uFF0C\u5148\u786E\u8BA4\u518D\u64CD\u4F5C\u3002\u7528\u6237\u63D0\u5230\u300CSSH / \u8FDC\u7A0B\u670D\u52A1\u5668 / \u670D\u52A1\u5668\u64CD\u4F5C / \u8DF3\u677F\u673A / \u96A7\u9053 / \u90E8\u7F72 / \u4E0A\u4F20\u4E0B\u8F7D\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
39
32
  /** Shared instances the SSH surfaces run on. */
40
33
  export interface SshCapabilityDeps {
41
34
  store: import('./store.ts').SecureHostStore;
@@ -44,8 +37,10 @@ export interface SshCapabilityDeps {
44
37
  knownHosts?: import('./known-hosts.ts').KnownHostsStore;
45
38
  /** Credential vault; absent → vault endpoints disabled. */
46
39
  vault?: import('./vault.ts').Vault;
47
- /** SSH-bound workspace ledger (host-delete reference guard). */
48
- ledger?: import('../ledger.ts').SshWorkspaceLedger;
40
+ /** SSH-bound workspace record source (host-delete reference guard). The
41
+ * legacy ledger in legacy mode, the generic WorkspaceCore-backed projection
42
+ * store in the default generic mode. */
43
+ ledger?: import('../backend.ts').WorkspaceStoreView;
49
44
  }
50
45
  /**
51
46
  * Mount the SSH operations surfaces (routes, tools, settings, prompt) against