@tiphareth/dsh-hardssh 0.1.2 → 0.2.3
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 +30 -0
- package/README.md +77 -50
- package/SKILLS.md +140 -0
- package/lib/base/index.js +95 -0
- package/lib/client.js +1171 -1321
- package/lib/fs.js +333 -416
- package/lib/index.js +5428 -2991
- package/lib/ledger-B-LXlftp.js +43 -0
- package/lib/ledger-D2ezq1iW.js +376 -0
- package/lib/model-Cp7f70Mb.js +5 -0
- package/lib/registry-CViuzKYI.js +421 -0
- package/lib/subprocess.js +92 -549
- package/lib/types/backend.d.ts +73 -78
- package/lib/types/base/capability.d.ts +4 -0
- package/lib/types/base/ledger-router.d.ts +42 -26
- package/lib/types/base/ledger.d.ts +30 -19
- package/lib/types/base/model.d.ts +17 -8
- package/lib/types/base/plugin.d.ts +11 -0
- package/lib/types/base/registry.d.ts +28 -31
- package/lib/types/base/router.d.ts +0 -7
- package/lib/types/client/api.d.ts +8 -8
- package/lib/types/client/connect-host.d.ts +13 -0
- package/lib/types/client/index.d.ts +6 -4
- package/lib/types/client/locales.d.ts +15 -1
- package/lib/types/client/session-connect-gate.d.ts +71 -0
- package/lib/types/client/ssh/api.d.ts +11 -9
- package/lib/types/client/ssh/apply.d.ts +14 -8
- package/lib/types/client/ssh/locales.d.ts +8 -1
- package/lib/types/client/ssh/ops-tab.d.ts +34 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
- package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
- package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
- package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
- package/lib/types/client/ssh/session-target.d.ts +17 -0
- package/lib/types/client/state.d.ts +4 -1
- package/lib/types/client/workspace-panel-entry.d.ts +30 -0
- package/lib/types/client/workspace-panel.d.ts +24 -0
- package/lib/types/client-http.d.ts +77 -6
- package/lib/types/core.d.ts +5 -16
- package/lib/types/fs.d.ts +42 -25
- package/lib/types/index.d.ts +138 -9
- package/lib/types/ledger.d.ts +16 -111
- package/lib/types/protocol.d.ts +0 -5
- package/lib/types/providers/index.d.ts +6 -3
- package/lib/types/providers/local/provider.d.ts +83 -23
- package/lib/types/providers/ssh/provider.d.ts +24 -57
- package/lib/types/remote/environment.d.ts +2 -0
- package/lib/types/remote/remote-fs.d.ts +12 -1
- package/lib/types/remote/remote-process.d.ts +22 -1
- package/lib/types/remote/remote-subprocess.d.ts +13 -1
- package/lib/types/remote/remote-terminal.d.ts +4 -0
- package/lib/types/remote-search.d.ts +3 -3
- package/lib/types/routes.d.ts +36 -8
- package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
- package/lib/types/runtime/workspace-core.d.ts +73 -38
- package/lib/types/runtime/workspace-migration.d.ts +113 -0
- package/lib/types/ssh/connection/lease.d.ts +23 -0
- package/lib/types/ssh/connection/manager.d.ts +359 -0
- package/lib/types/ssh/connection/pool.d.ts +8 -1
- package/lib/types/ssh/engine.d.ts +97 -267
- package/lib/types/ssh/known-hosts.d.ts +22 -5
- package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
- package/lib/types/ssh/plugin.d.ts +10 -15
- package/lib/types/ssh/protocol.d.ts +18 -0
- package/lib/types/ssh/routes.d.ts +15 -7
- package/lib/types/ssh/sftp/service.d.ts +216 -0
- package/lib/types/ssh/store.d.ts +20 -8
- package/lib/types/ssh/terminal/service.d.ts +49 -0
- package/lib/types/ssh/tunnel/service.d.ts +26 -0
- package/lib/types/ssh/vault.d.ts +51 -4
- package/lib/types/subprocess.d.ts +13 -21
- package/lib/types/switch/switch-fs.d.ts +52 -2
- package/lib/types/switch/switch-subprocess.d.ts +32 -3
- package/lib/types/tools.d.ts +7 -6
- package/lib/types/workspace-tool-ops.d.ts +33 -0
- package/lib/types/workspace.d.ts +18 -0
- package/lib/vault-3gpWct2Q.js +559 -0
- package/lib/workspace.js +2 -0
- package/package.json +59 -26
- package/scripts/export-legacy-workspaces.mjs +136 -0
- package/src/backend.ts +155 -483
- package/src/base/capability.ts +4 -0
- package/src/base/ledger-router.ts +214 -65
- package/src/base/ledger.ts +175 -48
- package/src/base/model.ts +18 -8
- package/src/base/plugin.ts +15 -4
- package/src/base/registry.ts +49 -41
- package/src/base/router.ts +0 -8
- package/src/client/api.ts +8 -36
- package/src/client/connect-host.ts +199 -0
- package/src/client/directory-flow.tsx +91 -36
- package/src/client/index.ts +56 -53
- package/src/client/locales.ts +14 -0
- package/src/client/session-connect-gate.ts +163 -0
- package/src/client/ssh/api.ts +245 -108
- package/src/client/ssh/apply.ts +23 -24
- package/src/client/ssh/locales.ts +16 -2
- package/src/client/ssh/ops-tab.tsx +79 -0
- package/src/client/ssh/panel/ClusterTab.tsx +9 -20
- package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
- package/src/client/ssh/panel/SshPanel.tsx +81 -77
- package/src/client/ssh/panel/TerminalTab.tsx +18 -37
- package/src/client/ssh/panel/TransferTab.tsx +43 -33
- package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
- package/src/client/ssh/panel/panel.module.css +90 -127
- package/src/client/ssh/session-target.ts +69 -0
- package/src/client/state.ts +9 -1
- package/src/client/workspace-badges.ts +99 -96
- package/src/client/workspace-panel-entry.tsx +91 -0
- package/src/client/workspace-panel.tsx +227 -0
- package/src/client/workspace.module.css +76 -2
- package/src/client-http.ts +118 -18
- package/src/core.ts +37 -47
- package/src/fs.ts +171 -85
- package/src/index.ts +535 -244
- package/src/ledger.ts +62 -416
- package/src/protocol.ts +0 -6
- package/src/providers/index.ts +8 -4
- package/src/providers/local/provider.ts +259 -87
- package/src/providers/ssh/provider.ts +158 -155
- package/src/remote/environment.ts +22 -1
- package/src/remote/remote-fs.ts +123 -34
- package/src/remote/remote-process.ts +265 -203
- package/src/remote/remote-subprocess.ts +61 -18
- package/src/remote/remote-terminal.ts +106 -21
- package/src/remote-search.ts +122 -26
- package/src/routes.ts +416 -395
- package/src/runtime/dsh-capabilities.ts +19 -0
- package/src/runtime/workspace-core.ts +171 -88
- package/src/runtime/workspace-migration.ts +472 -0
- package/src/ssh/connection/lease.ts +35 -0
- package/src/ssh/connection/manager.ts +1083 -0
- package/src/ssh/connection/pool.ts +341 -275
- package/src/ssh/engine.ts +269 -1477
- package/src/ssh/known-hosts.ts +42 -18
- package/src/ssh/local-transfer-policy.ts +83 -0
- package/src/ssh/plugin.ts +20 -19
- package/src/ssh/protocol.ts +22 -1
- package/src/ssh/routes.ts +419 -187
- package/src/ssh/sftp/service.ts +967 -0
- package/src/ssh/store.ts +169 -72
- package/src/ssh/terminal/service.ts +177 -0
- package/src/ssh/tools.ts +42 -20
- package/src/ssh/tunnel/service.ts +217 -0
- package/src/ssh/vault.ts +245 -91
- package/src/subprocess.ts +88 -71
- package/src/switch/switch-fs.ts +141 -10
- package/src/switch/switch-subprocess.ts +80 -6
- package/src/tools.ts +217 -221
- package/src/workspace-tool-ops.ts +101 -0
- package/src/workspace.ts +52 -0
- package/lib/environment-BL1jddfB.js +0 -449
- package/lib/switch-fs-CAJpFY9C.js +0 -193
- package/lib/switch-fs-RrZtG2gv.js +0 -210
- package/lib/types/client/manager-button.d.ts +0 -32
- package/lib/types/client/ssh/mount.d.ts +0 -13
- package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
- package/lib/types/client/ssh/panel/controller.d.ts +0 -23
- package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
- package/lib/types/client/workspace-gate.d.ts +0 -15
- package/lib/types/remote-runner.d.ts +0 -83
- package/lib/types/seam-state.d.ts +0 -69
- package/src/client/manager-button.tsx +0 -269
- package/src/client/ssh/mount.tsx +0 -83
- package/src/client/ssh/panel/HostsTab.tsx +0 -236
- package/src/client/ssh/panel/controller.ts +0 -48
- package/src/client/ssh/sidebar-entry.ts +0 -123
- package/src/client/workspace-gate.ts +0 -225
- package/src/remote-runner.ts +0 -201
- package/src/seam-state.ts +0 -185
package/src/ssh/known-hosts.ts
CHANGED
|
@@ -110,10 +110,18 @@ export class KnownHostsStore {
|
|
|
110
110
|
return this.records.get(alias)
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
/** First encounter: record as pending
|
|
113
|
+
/** First encounter: record as pending. Re-observing keeps the recorded
|
|
114
|
+
* status/fingerprint but backfills a previously empty host/port. */
|
|
114
115
|
observe(alias: string, meta: { host: string; port: number; keyType: string; fingerprintSha256: string }): void {
|
|
115
116
|
const existing = this.records.get(alias)
|
|
116
|
-
if (existing !== undefined)
|
|
117
|
+
if (existing !== undefined) {
|
|
118
|
+
let changed = false
|
|
119
|
+
if (existing.host === '' && meta.host !== '') { existing.host = meta.host; changed = true }
|
|
120
|
+
if (existing.port === 0 && meta.port > 0) { existing.port = meta.port; changed = true }
|
|
121
|
+
if (existing.keyType === 'ssh-unknown' && meta.keyType !== 'ssh-unknown') { existing.keyType = meta.keyType; changed = true }
|
|
122
|
+
if (changed) this.save()
|
|
123
|
+
return
|
|
124
|
+
}
|
|
117
125
|
this.records.set(alias, {
|
|
118
126
|
alias,
|
|
119
127
|
host: meta.host,
|
|
@@ -127,10 +135,13 @@ export class KnownHostsStore {
|
|
|
127
135
|
this.save()
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
/** pending → trusted (must match the recorded fingerprint).
|
|
131
|
-
|
|
138
|
+
/** pending → trusted (must match the recorded fingerprint). Optional
|
|
139
|
+
* host/port backfill completes a record whose first observe had no target. */
|
|
140
|
+
trust(alias: string, target?: { host?: string; port?: number }): void {
|
|
132
141
|
const record = this.records.get(alias)
|
|
133
142
|
if (record === undefined) throw new Error(`no host-key record for '${alias}'`)
|
|
143
|
+
if (record.host === '' && target?.host !== undefined && target.host !== '') record.host = target.host
|
|
144
|
+
if (record.port === 0 && target?.port !== undefined && target.port > 0) record.port = target.port
|
|
134
145
|
record.status = 'trusted'
|
|
135
146
|
record.confirmedAt = Date.now()
|
|
136
147
|
this.save()
|
|
@@ -151,36 +162,49 @@ export class KnownHostsStore {
|
|
|
151
162
|
export class HostKeyPolicy {
|
|
152
163
|
constructor(private readonly knownHosts: KnownHostsStore) {}
|
|
153
164
|
|
|
154
|
-
/** Evaluate one server key: trusted / unknown (records pending) / mismatch.
|
|
155
|
-
|
|
165
|
+
/** Evaluate one server key: trusted / unknown (records pending) / mismatch.
|
|
166
|
+
* `target` supplies the host/port the engine is actually connecting to so
|
|
167
|
+
* the audit record is complete instead of `''`/0. */
|
|
168
|
+
check(alias: string, rawKey: Buffer, target?: { host?: string; port?: number }): HostKeyCheck {
|
|
156
169
|
const recorded = this.knownHosts.lookup(alias)
|
|
157
170
|
const actual = fingerprintOf(rawKey)
|
|
158
171
|
if (recorded === undefined) {
|
|
159
172
|
// First encounter: remember as pending, but NOT trusted — the caller
|
|
160
173
|
// must refuse the connection until the operator confirms.
|
|
161
174
|
this.knownHosts.observe(alias, {
|
|
162
|
-
host:
|
|
163
|
-
port: 0,
|
|
175
|
+
host: target?.host ?? '',
|
|
176
|
+
port: target?.port ?? 0,
|
|
164
177
|
keyType: keyTypeOf(rawKey),
|
|
165
178
|
fingerprintSha256: actual,
|
|
166
179
|
})
|
|
167
180
|
return { kind: 'unknown', fingerprintSha256: actual }
|
|
168
181
|
}
|
|
169
|
-
if (recorded.status !== 'trusted')
|
|
182
|
+
if (recorded.status !== 'trusted') {
|
|
183
|
+
this.knownHosts.observe(alias, {
|
|
184
|
+
host: target?.host ?? '',
|
|
185
|
+
port: target?.port ?? 0,
|
|
186
|
+
keyType: keyTypeOf(rawKey),
|
|
187
|
+
fingerprintSha256: recorded.fingerprint,
|
|
188
|
+
})
|
|
189
|
+
return { kind: 'unknown', fingerprintSha256: recorded.fingerprint }
|
|
190
|
+
}
|
|
170
191
|
if (fingerprintsEqual(recorded.fingerprint, actual)) return { kind: 'trusted' }
|
|
171
192
|
return { kind: 'mismatch', expected: recorded.fingerprint, actual }
|
|
172
193
|
}
|
|
173
194
|
}
|
|
174
195
|
|
|
175
|
-
/**
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Parse the key type out of a real SSH wire public-key blob. The blob is
|
|
198
|
+
* `string algorithm, byte[] key` where every string is a 4-byte big-endian
|
|
199
|
+
* length followed by raw bytes — NOT the `ssh-ed25519 AAAA…` authorized_keys
|
|
200
|
+
* text form, so a text-oriented decode would always yield 'ssh-unknown'.
|
|
201
|
+
*/
|
|
202
|
+
export function keyTypeOf(rawKey: Buffer): string {
|
|
203
|
+
if (rawKey.length < 4) return 'ssh-unknown'
|
|
204
|
+
const length = rawKey.readUInt32BE(0)
|
|
205
|
+
if (length === 0 || length > 64 || 4 + length > rawKey.length) return 'ssh-unknown'
|
|
206
|
+
const algorithm = rawKey.subarray(4, 4 + length).toString('latin1')
|
|
207
|
+
return /^[a-z0-9@.\-]+$/u.test(algorithm) ? algorithm : 'ssh-unknown'
|
|
184
208
|
}
|
|
185
209
|
|
|
186
210
|
function isRecord(value: unknown): value is KnownHostRecord {
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
|
|
10
|
+
import { existsSync, realpathSync, statSync } from 'node:fs'
|
|
11
|
+
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path'
|
|
12
|
+
import type { ToolRunContext } from '@deepseek-ai/dsh-tools'
|
|
13
|
+
|
|
14
|
+
export type LocalTransferDirection = 'upload-source' | 'download-destination'
|
|
15
|
+
|
|
16
|
+
function outside(root: string, candidate: string): boolean {
|
|
17
|
+
const rel = relative(root, candidate)
|
|
18
|
+
return rel === '..' || rel.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || isAbsolute(rel)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fail(path: string, root: string): never {
|
|
22
|
+
throw new Error(`local transfer path '${path}' is outside the invoking session workspace '${root}'`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Canonicalize a not-yet-existing destination through its nearest existing
|
|
26
|
+
* ancestor, preserving the suffix beneath that canonical ancestor. */
|
|
27
|
+
function canonicalFuturePath(path: string): string {
|
|
28
|
+
const suffix: string[] = [basename(path)]
|
|
29
|
+
let cursor = dirname(path)
|
|
30
|
+
for (;;) {
|
|
31
|
+
if (existsSync(cursor)) {
|
|
32
|
+
return resolve(realpathSync.native(cursor), ...suffix)
|
|
33
|
+
}
|
|
34
|
+
const parent = dirname(cursor)
|
|
35
|
+
if (parent === cursor) throw new Error(`cannot resolve an existing parent for local destination '${path}'`)
|
|
36
|
+
suffix.unshift(basename(cursor))
|
|
37
|
+
cursor = parent
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate and canonicalize one local transfer path.
|
|
43
|
+
*
|
|
44
|
+
* Uploads must name an existing regular file. Downloads may create the final
|
|
45
|
+
* file and missing parent directories, but their nearest existing ancestor is
|
|
46
|
+
* canonicalized so an in-workspace symlink/junction cannot redirect the write
|
|
47
|
+
* outside the session root.
|
|
48
|
+
*/
|
|
49
|
+
export function secureLocalTransferPath(
|
|
50
|
+
exec: ToolRunContext,
|
|
51
|
+
localPath: string,
|
|
52
|
+
direction: LocalTransferDirection,
|
|
53
|
+
): string {
|
|
54
|
+
const cwd = exec.agent?.session.header.cwd
|
|
55
|
+
if (cwd === undefined) {
|
|
56
|
+
throw new Error('local transfer denied: the tool call has no invoking session workspace')
|
|
57
|
+
}
|
|
58
|
+
if (!isAbsolute(cwd)) throw new Error('local transfer denied: invoking session cwd is not absolute')
|
|
59
|
+
if (!isAbsolute(localPath)) throw new Error('local transfer path must be absolute')
|
|
60
|
+
|
|
61
|
+
let root: string
|
|
62
|
+
try {
|
|
63
|
+
root = realpathSync.native(cwd)
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error(`local transfer denied: invoking session workspace '${cwd}' is unavailable`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const requested = resolve(localPath)
|
|
69
|
+
let canonical: string
|
|
70
|
+
if (direction === 'upload-source') {
|
|
71
|
+
try {
|
|
72
|
+
canonical = realpathSync.native(requested)
|
|
73
|
+
} catch {
|
|
74
|
+
throw new Error(`local upload source '${localPath}' does not exist`)
|
|
75
|
+
}
|
|
76
|
+
if (!statSync(canonical).isFile()) throw new Error(`local upload source '${localPath}' is not a regular file`)
|
|
77
|
+
} else {
|
|
78
|
+
canonical = existsSync(requested) ? realpathSync.native(requested) : canonicalFuturePath(requested)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (outside(root, canonical)) fail(localPath, root)
|
|
82
|
+
return canonical
|
|
83
|
+
}
|
package/src/ssh/plugin.ts
CHANGED
|
@@ -25,29 +25,21 @@ import { sshClusterTool, sshDownloadTool, sshExecTool, sshListTool, sshTunnelToo
|
|
|
25
25
|
* user settings survive the package merge unchanged. */
|
|
26
26
|
export const SSH_SETTINGS_NAMESPACE = 'dsh-ssh'
|
|
27
27
|
|
|
28
|
-
/** SSH-capability config, validated by the same-named schemastery schema.
|
|
28
|
+
/** SSH-capability config, validated by the same-named schemastery schema.
|
|
29
|
+
* `secretStorage` is deliberately NOT a key here: the mode decides which
|
|
30
|
+
* objects are constructed at plugin load, so it is a single source in the
|
|
31
|
+
* top-level plugin config (`Config.secretStorage`). A value left in this
|
|
32
|
+
* namespace by hand is reported as drift by src/index.ts, never applied. */
|
|
29
33
|
export interface SshConfig {
|
|
30
34
|
/** When true (default), a system-prompt section announces the SSH capability. */
|
|
31
35
|
announceToAgent?: boolean
|
|
32
36
|
/** Master switch for the SSH surfaces (routes, tools, prompt section). */
|
|
33
37
|
enabled?: boolean
|
|
34
|
-
/**
|
|
35
|
-
* Secret storage mode:
|
|
36
|
-
* - 'none' (default): passwords/passphrases are NEVER persisted; each
|
|
37
|
-
* session's first connection prompts for them (VSCode Remote-SSH style)
|
|
38
|
-
* and they live in memory (session password table) for the connection
|
|
39
|
-
* pool lifetime (idle 30 min auto-disconnect; new terminals reuse it).
|
|
40
|
-
* - 'vault': store secrets encrypted at rest (AES-256-GCM) under a master
|
|
41
|
-
* password / DSH_CREDENTIAL_PASSWORD; for headless agents that must run
|
|
42
|
-
* password hosts unattended.
|
|
43
|
-
*/
|
|
44
|
-
secretStorage?: 'none' | 'vault'
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
export const SshConfig: z<SshConfig> = z.object({
|
|
48
41
|
announceToAgent: z.boolean().default(true),
|
|
49
42
|
enabled: z.boolean().default(true),
|
|
50
|
-
secretStorage: z.union([z.const('none'), z.const('vault')]).default('none'),
|
|
51
43
|
})
|
|
52
44
|
|
|
53
45
|
/** Schema default, re-read for hand-built test contexts. */
|
|
@@ -57,7 +49,7 @@ const DEFAULT_ANNOUNCE = true
|
|
|
57
49
|
const SECTION_ORDER = 150
|
|
58
50
|
|
|
59
51
|
/** Model-facing announcement: SSH operations capability, capabilities, limits. */
|
|
60
|
-
export const SSH_GUIDANCE = '本机已安装 dsh-hardssh 插件(内建 DSH 远程 SSH 运维):侧边栏「SSH」入口;SSH 运维与 SSH 工作区在 dsh-hardssh 中统一维护。能力:主机配置存 ~/.dsh/dsh-ssh.json(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent
|
|
52
|
+
export const SSH_GUIDANCE = '本机已安装 dsh-hardssh 插件(内建 DSH 远程 SSH 运维):侧边栏「SSH」入口;SSH 运维与 SSH 工作区在 dsh-hardssh 中统一维护。能力:主机配置存 ~/.dsh/dsh-ssh.json(可从 ~/.ssh/config 导入);持久连接池复用长连接(空闲 30 分钟自动断开);ssh_list 列出主机、ssh_exec 执行远程命令、ssh_upload/ssh_download 传输文件、ssh_tunnel 本地端口转发(访问远程数据库/内网服务)、ssh_cluster 集群并发执行;支持密钥/密码认证、passphrase 密钥与 ProxyJump 跳板机;Web 终端走 WebSocket。限制:主机操作由用户在 GUI 中配置后 agent 方可使用;凭据默认不落盘(secretStorage=none 时仅存于本进程内存,按连接存活期复用、连接池回收即失效;secretStorage=vault 时以 AES-256-GCM 加密存于本机 vault 文件,`DSH_CREDENTIAL_PASSWORD` 自动解锁默认关闭,需 vaultAutoUnlock: env 显式开启);命令输出与错误经过凭据脱敏(已揭示/会话内的密码会被替换),但仍可能含其它敏感信息;连接建立失败会自动重连,但命令或文件操作一旦提交就不会自动重试——结果不明确时应先核实远端状态,再决定是否人工重试;传输/执行消耗真实远程资源,先确认再操作。用户提到「SSH / 远程服务器 / 服务器操作 / 跳板机 / 隧道 / 部署 / 上传下载」时即指本插件,请据此协作。'
|
|
61
53
|
|
|
62
54
|
/** Shared instances the SSH surfaces run on. */
|
|
63
55
|
export interface SshCapabilityDeps {
|
|
@@ -67,8 +59,10 @@ export interface SshCapabilityDeps {
|
|
|
67
59
|
knownHosts?: import('./known-hosts.ts').KnownHostsStore
|
|
68
60
|
/** Credential vault; absent → vault endpoints disabled. */
|
|
69
61
|
vault?: import('./vault.ts').Vault
|
|
70
|
-
/** SSH-bound workspace
|
|
71
|
-
ledger
|
|
62
|
+
/** SSH-bound workspace record source (host-delete reference guard). The
|
|
63
|
+
* legacy ledger in legacy mode, the generic WorkspaceCore-backed projection
|
|
64
|
+
* store in the default generic mode. */
|
|
65
|
+
ledger?: import('../backend.ts').WorkspaceStoreView
|
|
72
66
|
}
|
|
73
67
|
|
|
74
68
|
/**
|
|
@@ -92,7 +86,7 @@ export function mountSshCapability(ctx: Context, deps: SshCapabilityDeps): void
|
|
|
92
86
|
// dynamic webServer inject (headless profiles lack the service — mirroring
|
|
93
87
|
// the workspace routes in src/index.ts). The enabled switch is honored at
|
|
94
88
|
// registration time; toggling it later takes effect on reload.
|
|
95
|
-
const { routes, upgrade } = makeRoutes({
|
|
89
|
+
const { routes, upgrade, dispose: disposeRoutes } = makeRoutes({
|
|
96
90
|
store,
|
|
97
91
|
engine,
|
|
98
92
|
knownHosts: deps.knownHosts,
|
|
@@ -103,12 +97,19 @@ export function mountSshCapability(ctx: Context, deps: SshCapabilityDeps): void
|
|
|
103
97
|
onHostInvalidated: (alias) => invalidateRemoteEnvironment(engine, alias),
|
|
104
98
|
})
|
|
105
99
|
ctx.inject(['webServer'], (scoped) => {
|
|
106
|
-
|
|
100
|
+
// Tear down the terminal server (live PTYs, SSH shells, WebSocketServer)
|
|
101
|
+
// on unload/HMR even when the switch is off or nothing was registered.
|
|
102
|
+
const dispose = (): void => { void disposeRoutes() }
|
|
103
|
+
if (!resolve().enabled) return dispose
|
|
107
104
|
const disposers = routes.map(route => scoped.webServer.register(route))
|
|
108
105
|
const upgradeDisposer = scoped.webServer.registerUpgrade(upgrade)
|
|
109
106
|
return () => {
|
|
110
|
-
for (const
|
|
107
|
+
for (const routeDisposer of disposers) routeDisposer()
|
|
108
|
+
// Unregistering the upgrade route does not release its terminals: close
|
|
109
|
+
// every live PTY/SSH shell and the instance's WebSocketServer so unload
|
|
110
|
+
// and HMR leave nothing running (A-06). Idempotent.
|
|
111
111
|
upgradeDisposer()
|
|
112
|
+
dispose()
|
|
112
113
|
}
|
|
113
114
|
})
|
|
114
115
|
|
package/src/ssh/protocol.ts
CHANGED
|
@@ -85,6 +85,16 @@ export interface ClusterResult {
|
|
|
85
85
|
stderr?: string
|
|
86
86
|
durationMs?: number
|
|
87
87
|
error?: string
|
|
88
|
+
/** Stable machine code for a per-host failure (B-14) — lets callers retry
|
|
89
|
+
* on an auth/TOFU gate instead of parsing the human message. */
|
|
90
|
+
code?: 'NEEDS_PASSWORD' | 'HOST_KEY_UNKNOWN' | 'HOST_KEY_MISMATCH' | 'ALIAS_NOT_FOUND' | 'ABORTED' | 'TIMEOUT' | 'ERROR'
|
|
91
|
+
/** Which secret the host needs when `code` is NEEDS_PASSWORD. */
|
|
92
|
+
secret?: 'password' | 'passphrase'
|
|
93
|
+
/** Server-key fingerprint for HOST_KEY_UNKNOWN / HOST_KEY_MISMATCH. */
|
|
94
|
+
hostKeyFingerprint?: string
|
|
95
|
+
/** Expected/actual fingerprints for HOST_KEY_MISMATCH. */
|
|
96
|
+
expected?: string
|
|
97
|
+
actual?: string
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
/** SFTP transfer progress frame (upload stream). */
|
|
@@ -224,4 +234,15 @@ export type KnownHostAction = 'trust' | 'forget'
|
|
|
224
234
|
/** NDJSON transfer stream line shapes (upload). */
|
|
225
235
|
export type TransferStreamLine =
|
|
226
236
|
| { type: 'progress'; progress: TransferProgress }
|
|
227
|
-
|
|
237
|
+
/** The final remote rename is about to start; loss after this point means
|
|
238
|
+
* the target's result is unknown until inspected. */
|
|
239
|
+
| { type: 'commit' }
|
|
240
|
+
| {
|
|
241
|
+
type: 'result'
|
|
242
|
+
ok: boolean
|
|
243
|
+
transferredBytes?: number
|
|
244
|
+
error?: string
|
|
245
|
+
code?: string
|
|
246
|
+
secret?: 'password' | 'passphrase'
|
|
247
|
+
hostKeyFingerprint?: string
|
|
248
|
+
}
|